Skip to content

Rebuild the page actions row: Copy as markdown and Open in Claude - #3330

Draft
enf0rc3 wants to merge 5 commits into
mainfrom
wl/open-in-llm
Draft

Rebuild the page actions row: Copy as markdown and Open in Claude#3330
enf0rc3 wants to merge 5 commits into
mainfrom
wl/open-in-llm

Conversation

@enf0rc3

@enf0rc3 enf0rc3 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Implements two designs from Documentation vision: Open in LLM and Copy to clipboard.

What changed

The "Use Octopus docs with AI" pill at the foot of the article is gone. Under the page header, next to Edit on GitHub, there are now two actions:

Copy as markdown — fetches this page's .md companion and puts it on the clipboard. Swaps to a check icon and "Copied" for two seconds, then reverts.

Open in Claude with a caret — the primary half opens Claude with a prompt pointing at the page's .md URL. The caret opens a 256px menu of Open in Claude, Open in ChatGPT, Open in Gemini, as link-styled rows with a provider logo and an external-link icon.

Both are the Button component, so borders, radius, hover, pressed and focus states come from the design-system tokens. Button gains an as prop so the caret can be a <summary>; the split button's two halves collapse into one divider via a negative margin, which keeps each half's focus ring unclipped. Menu chrome uses --colorMenuListBackground* and --shadowMedium, so both themes work with no extra rules.

Four icons added under src/assets/icons/, exported from the Figma file and applied as CSS masks so they take --colorIconPrimary in a button and --colorTextLinkDefault in the menu.

Reusing the shared copy module

copy-button.js (from #3317) grows two things rather than the copy action reimplementing them:

  • showResult writes to .btn__label when there is one, and falls back to the tooltip otherwise. The design wants the result in the label; the two existing icon-only consumers are unchanged.
  • copyFetchedOnClick covers text that has to be fetched. It hands ClipboardItem a pending promise so the click's user activation survives the request — awaiting the fetch first and then writing loses it in Safari, which is the trap the module's own comment warns about.

The button also locks its measured width before the label shortens, so "Open in Claude" beside it doesn't shuffle. Measured rather than hardcoded, so it holds for any translation.

The eligibility check both actions need moved to pageMarkdownUrl(), so neither component re-derives the slug.

Fixes a markdown emitter bug on Windows

globSync returns backslash-separated paths on Windows and llm-md-emitter.ts fed them into forward-slash logic in two places. Two lines of posix: true:

  • pathToSlug strips a trailing /index, so argo-cd\index kept its suffix and the page was written to dist/docs/argo-cd/index.md. Every request for /docs/argo-cd.md 404d, taking the page actions and llms.txt parity with it.
  • The shared content lookup keys off path.posix.join, which leaves a backslash mid-key and never matches the include paths in the source files. A page whose include failed to resolve was judged ineligible, so 1252 rather than 846 pages now get a .md companion on a Windows build.

Linux was unaffected, which is why this survived. It is in this PR because it is what the two new actions point at.

One thing to call out

Gemini points at AI Studio. gemini.google.com/app has no native prompt parameter — the ?prompt= trick needs a browser extension. The label stays "Open in Gemini" per the design while the href is aistudio.google.com/prompts/new_chat?prompt=…, which does prefill. Happy to change the label if that reads wrong.

Testing

astro build from a clean dist: exit 0, 2673 pages, 1252 .md emitted.

tests/llm-endpoints.spec.ts: 13 passed, 0 failed. The two CopyMarkdown tests are rewritten and two added for the copy action, including one that clicks it and reads the clipboard back to confirm the page markdown actually arrives. Before the emitter fix, five of these failed.

Verified in a real browser in both themes: menu open and closed, and the copy button through rest, copied and reverted.

🤖 Generated with Claude Code

@team-marketing-branch-protections

Copy link
Copy Markdown

Pull request environment is available at https://stoctodocspr3330.z22.web.core.windows.net.

You can view the ephemeral environment status in Octopus Deploy.

This environment will be automatically deprovisioned when the pull request is closed, or after 7 days of inactivity.

@enf0rc3
enf0rc3 force-pushed the wl/open-in-llm branch 2 times, most recently from 3c7c692 to 4eb9610 Compare August 11, 2026 02:41
@enf0rc3 enf0rc3 changed the title Turn the AI dropdown into an Open in Claude split button Rebuild the page actions row: Copy as markdown and Open in Claude Aug 11, 2026
enf0rc3 and others added 4 commits August 11, 2026 14:42
The "Use Octopus docs with AI" pill offered three markdown actions. The
design replaces it with a split button: a primary "Open in Claude", and a
caret that opens a list of assistants. Each entry hands the assistant this
page's .md URL to read.

The markdown actions come back as a separate copy button once the shared
copy module lands.

Both halves reuse the .btn component, so the pair matches "Edit on GitHub"
beside it in the page actions row, and the whole control moves from the
foot of the article up under the header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The split button hand-wrote `class="btn btn--small"`. Astro compiles
Button.astro's `.btn` rules to `.btn[data-astro-cid-ekguhzzh]` and only
stamps that attribute on elements Button itself renders, so none of the
button styles reached this markup.

Reuse the component instead of its class names. Button gains an `as` prop
so a `<details>` disclosure trigger can be a real Button: a `<summary>` is
a list item that carries its own marker, and dragging over a trigger
selects its text rather than opening it, so both resets belong with the
rest of the button styles. `<summary>` has no native disabled state, so
`disabled` maps to `aria-disabled`, which the existing state rules already
key off.

Move the `.octo-llm` rules out of main.css into the component. Reaching
into Button from here needs `:global()`, because the parent's scope
attribute lands on the Button root but never on the icon spans inside it.
Keeping `.octo-llm` in front of the `:global()` leaves the compiled
selector scoped, so these rules cannot escape the component.

Verified against the built output: the emitted CSS is unchanged by the
pending move of button styles into an imported stylesheet, since that
changes where the rules are authored and not what they compile to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page actions row lost its copy action when the AI dropdown became a
split button, on the grounds that it would come back once the shared copy
module landed. It has, so this brings it back as its own button.

copy-button.js grows two things. A labelled button now shows its result in
its label rather than a tooltip, which is what the design asks for and what
an icon-only button cannot do. And copyFetchedOnClick covers text that has
to be fetched: it hands ClipboardItem a pending promise so the click's user
activation survives the request, which awaiting the fetch first would spend.

The eligibility check both page actions need moves to pageMarkdownUrl, so
neither component re-derives the slug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
globSync returns backslash-separated paths on Windows, and both call sites
feed them into forward-slash logic.

pathToSlug strips a trailing `/index`, so `argo-cd\index` kept its suffix and
the page was written to dist/docs/argo-cd/index.md. Every request for
/docs/argo-cd.md 404d, which took the page actions and llms.txt parity with
it.

The shared content lookup keys off path.posix.join, which leaves a backslash
mid-key and never matches the include paths written in the source files. A
page whose include failed to resolve was judged ineligible, so 1252 rather
than 846 pages now get a .md companion on a Windows build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`unclipped` and `neighbour` were only in comments, and `hrefs` only a local
name, so none of them needs a dictionary entry. `neighbour` in particular
would have committed the repo to a British spelling that the dictionary does
not otherwise establish.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants