Add shortcut pins: one-click prompt injection from the sidebar - #930
Merged
Conversation
Agents can pin a `shortcut` — a button whose label is the button text and whose value is a prompt injected into that agent's session on click, exactly as typing it would. This generalizes the ad hoc convention the Idea Inbox invented: pin a trigger phrase, have the user copy-paste it back, then do loose NLU on whatever came back. Delivery reuses the existing quick-phrase pathway (InjectionCoordinator -> TmuxTerminal.sendCommand) with gate: false, since the click is the user acting. The prompt is looked up server-side by pin ID, so a client can only fire prompts the agent itself pinned. Shortcut pins take an icon from a fixed set, a button variant, and an opt-in `confirm` that shows the exact prompt before sending. Hovering any shortcut shows the full prompt and names the agent it goes to. Two pin fields are generic rather than shortcut-only: - `metadata`: a one-line inline-markdown caption under any pin, capped at 200 characters with block elements stripped at render. - `group`: pins sharing a name render under one heading, anchored at the first member so re-pinning can't relocate the block. The group name doubles as the header for a set of choices. Also makes upsertPin position-stable: updating an existing label now edits in place instead of remove-then-append, so refreshing a pin no longer shuffles it to the bottom of the sidebar. The max-pins check applies only when inserting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`metadata` already means "arbitrary key-value object" elsewhere in the codebase (dispatch_event, media, brain), so a plain string field by that name misleads. Everything built around it was already named caption — PinCaption, validatePinCaption, the pin-caption test id — only the wire field disagreed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three fixes for the same failure: an agent asked to group an existing
shortcut ended up with neither pin grouped, the existing pin's icon and
variant reset, and no signal that anything went wrong.
1. group and icon were accepted by the dispatch_pin schema and handled
downstream, but the tool's call site never forwarded them, so they
were silently discarded. This is the actual bug; the agent's calls
were correct.
2. upsertPin now merges onto the stored pin instead of replacing it.
Adding a group no longer requires restating every decoration, and
omitting one no longer wipes it. An empty string clears a field.
Extracted as mergePin so the rule is unit-testable without a DB.
3. dispatch_pin reports "Created"/"Updated" plus the stored pin, so an
agent can see what its call actually produced rather than assuming
its input round-tripped. dispatch_list_pins now returns decorations
too, so an agent can read current state before changing it.
upsertPin returns { agent, pin, created } to support the above.
Tests: handler-level coverage that every decoration reaches the manager
(fails against the old code), pure mergePin coverage, and DB-level
coverage of merge, empty-string clearing, and position stability.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>The run endpoint decides what a click is allowed to inject, and until now only an E2E case touched it. Adds fast route-level tests for every rejection path: unknown agent, unknown pin ID, a pin that is not a shortcut (otherwise any pinned string becomes injectable), no live session, and an unauthenticated request. Trims the E2E case to the one thing only E2E can prove — that a click in the real sidebar reaches the run endpoint for the right pin. Label, caption, variant, and disabled-state rendering are already covered by pins-panel unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Agent session name on its own line in a stronger treatment, "will receive the following:" muted beneath it, then the phrase to be injected. Reads as a statement about where the prompt goes rather than a single dense sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The single flat block was hard to parse. Now: popover background so the tooltip separates from the card surface behind it, agent session name in accent monospace, the muted "will receive the following:" line beneath it, and the prompt in its own darker code-block panel with a divider — monospace, matching how the terminal will actually show it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uses chart-4 (purple) for the tooltip surface and border — the one accent in the palette no button variant claims, and it is defined in every theme, so the tooltip never collides with a shortcut's own primary/destructive styling. Mixed into the popover token rather than applied flat so it stays a tint in light themes too. The prompt block is now inset with padding on all sides and its own rounded border, instead of running edge to edge and blending into the surrounding surface. Agent name drops back to foreground now that the surface carries the color. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drops the purple tint: every accent token in the palette maps onto a button variant's hue in at least one theme, so an accent background always competes with something. Instead the popover token is mixed toward the foreground, which lifts the tooltip off the card in dark themes and deepens it in light ones. The prompt block stays on the darker background token, so it still reads as an inset panel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bugs: - Launch guidance advertised `type: "action"`, which no longer exists — every agent reads that rule, and following it fails validation. - Grouping never reached the agent-history pin view, which renders pins directly. `PinList` now owns layoutPins + PinItem so both consumers get grouping; a third cannot miss it. - Seeded pins carried no `id`, so every seeded shortcut rendered as a button that silently did nothing. Seeds now carry stable IDs, and a pin without one renders disabled with its own reason. - Merge kept shortcut-only fields across a type change, stranding icon/variant/confirm on pins that cannot use them. Hardening: - `icon` is validated in pins.ts alongside `variant`, so the invariant survives any non-MCP write path; the allowlist now lives there as the single server-side source, with a guard test asserting lockstep with the web icon map. - `resolvePinShortcutIcon` uses Object.hasOwn, so "constructor" and "__proto__" fall back instead of rendering a non-component. - `normalizeInitialPins` — the second write path into agents.pins — runs the same validators as dispatch_pin. - On coarse pointers the hover tooltip is unreachable, so a tap would fire a prompt the user never saw; shortcuts route through the confirm dialog there regardless of the pin's own setting. Contracts: - Tool output projects through a shared `PinListing`, so dispatch_pin's echo and dispatch_list_pins speak one vocabulary and internal fields cannot leak into agent-visible text. - `PinType`/`PinShortcutVariant` have one server-side source (pins.ts). - Run endpoint moved to /terminal/inject-pin/:pinId, matching its siblings, and selection extracted to `resolveShortcutRun` so the "server picks the text, not the client" claim is directly tested. - Caption no longer leaks raw markdown into a native title tooltip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ch-tap disclosure - Deletes the now-unimported PIN_SHORTCUT_ICON_NAMES from the web module. - Wraps normalizeInitialPins validation in AgentError(400) so a bad initialPins payload reads as a client error rather than a 500. - Hybrid laptops report `pointer: fine`, so a finger tap skipped the disclosure the coarse-pointer rule adds; the click's pointerType now routes touch taps through the confirm dialog too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The `default` shortcut variant inherited dark-theme glass, so on every light theme it painted white-at-6% over a light sidebar and read as a disabled field. Shortcuts now paint from theme tokens; verified live in `light` (bg now rgb(218,224,231) with a visible border, was effectively transparent). - Nothing guarded an in-flight run, so a double-click sent the prompt twice. The firing pin is blocked until its request settles and shows a spinner; the toast names the shortcut so stacked toasts differ. - The confirm dialog has no DialogTrigger, so focus fell to <body> on close. The opening button is tracked and refocused, which matters more now that touch routes every shortcut through it. - Unavailable shortcuts used the native `disabled`, dropping them out of the tab order along with the only explanation of why. They now use aria-disabled and stay focusable so the tooltip can be reached. - Group headings are `role="group"` + aria-labelledby, so the question a set of shortcuts answers is announced with them. - Touch targets go to 44px on coarse pointers, up from 32px. - Destructive shortcuts carry a warning glyph, so the cue survives themes where primary and destructive are nearly the same colour. - The tooltip shows the full label (truncated in the button) and flags a long prompt as scrollable rather than silently clipping it. - Caption clamp raised to three lines, matching the budget the 200-char server cap was sized against, and tightened toward its own button so it is no longer equidistant between two shortcuts. - A successful run closes the mobile sheet, which otherwise covers the terminal the shortcut just acted on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… at 160 The token-based fill still measured flat on solarized-light and solarized-dark, where `--muted` is identical to `--card` — the surface the sidebar actually paints — so the fill had no contrast and only the border carried the button. It now mixes `--card` toward `--foreground`, the same approach the tooltip uses, with a `foreground/25` border. Computed across all 12 themes: every fill is now >= 1.17 against the card, with the two flat cases going 1.00 -> 1.17/1.20. Verified live in solarized-light: fill 1.193, border 10.65. Caption cap drops 200 -> 160. Measured in the 400px rail, 160 plain characters fill the three-line clamp exactly and 180 clips, so the old cap let a caption through that the clamp would silently cut — and the comment claiming otherwise is now accurate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 words down to 36. Every agent reads this block at launch, and one pin type was carrying more weight there than the whole pin system above it. The mechanics it was restating — label is the button text, value is the prompt, caption is a note — are already in the dispatch_pin tool description, which an agent reads when it calls the tool. What is left is only what launch guidance is for: when to reach for a shortcut, and the waiting_user pairing it would otherwise miss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
selfcontained added a commit
that referenced
this pull request
Aug 12, 2026
Worktrees deep-dive against workspace-prep.ts, shared/git/worktree.ts, tmux/setup-script.ts, and archive.ts: - "When creation fails" claimed the card shows an Attention badge. That badge renders only for status `error`; a worktree failure goes through markSetupFailed, which sets `stopped` — and the reconciler never revisits it, since its query only selects running/stopping/creating/ archiving. What users actually see is the status line flipping to Blocked with the git error, so say that. - Both launch paths fetch the starting branch from origin and fork the new branch from `origin/<branch>`; checking out the starting branch directly uses the local copy. Neither was documented. - Removing a worktree also deletes the branch Dispatch created for it, and "Archive and remove worktree" force-deletes it (`branch -D`) including unpushed commits. The section only described the directory. - The tmux setup script skips the dependency install for terminal-type agents. Also from the diff since the last audit: the shortcut-pin `disabled` state (#937) in the Pins paragraph, the two terminal injection routes inject-text (#935) and inject-pin (#930) in the api-spec Terminal table, and a new ambient tip for shortcut pins deep-linked to a new media#media-sidebar anchor. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Agents can now pin a shortcut — a button whose label is the button text and whose
valueis a prompt injected into that agent's session when clicked, exactly as if you'd typed it. This generalizes the ad hoc convention the Idea Inbox invented: pin a trigger phrase, copy-paste it back into chat, and have the agent do loose NLU on whatever came back.How it works
Delivery reuses the existing quick-phrase pathway —
InjectionCoordinator→TmuxTerminal.sendCommandwithgate: false, since the click is the user acting. No new dispatch/callback system, no new permission model.POST /api/v1/agents/:id/pins/:pinId/runlooks the prompt up server-side by pin ID, so a client can only fire prompts the agent itself pinned. Returns 409 when the agent has no live tmux session; the button renders disabled in that state (including in agent history, where nothing can be delivered).Shortcut-specific fields
icon— one of a fixed set of 25 lucide names, enumerated in the tool schema so an agent can only pick something the sidebar can render.variant—default/primary/destructive, mapping to existingbuttonVariants.confirm— opt-in dialog showing the exact prompt before sending, for destructive or hard-to-undo actions.Every shortcut carries a constant
↵glyph regardless of the agent's icon choice, and hovering shows a tooltip naming the target agent plus the full prompt.Two generic pin fields
Both apply to any pin type, not just shortcuts:
metadata— a one-line caption under the pin rendering inline markdown (bold, italic,code, strikethrough). Capped at 200 characters with block elements stripped at render and a 3-line clamp, so it stays a subtitle. For comparison, markdown pins allow 2000.group— pins sharing a name render under one heading. The group is anchored where its first member sits, so re-pinning a member can't relocate the block. The group name doubles as the header for a set of choices, which removes the need for a separate header pin above a group of buttons.Stable pin ordering
upsertPinpreviously filtered by label and pushed, so updating any pin moved it to the end of the array — refreshing a status pin quietly reshuffled the sidebar, and would have torn grouped pins apart. It now edits in place when the label exists and appends only when new; an agent that wants a pin moved deletes and re-pins it. The max-pins check now applies only on insert, so updating a pin at the cap no longer spuriously fails.Testing
pnpm run check— cleanpnpm run finalize:web— cleanpins-panel.test.tsxcovering render, confirm accept/cancel, and both disabled paths)pnpm run test:e2e: 179 passed, 12 skipped (new case inmedia-sidebar.spec.tscovering button + caption render, the confirm dialog, and the run endpoint firing)Notes
Scope is deliberately just the
shortcuttype. The multi-question/form concept is tracked separately asinteractive-question-presentation; a second action kind (navigate, run-a-job) would force a discriminated union on the pin, which is worth doing once there's evidence about which kind gets wanted.🤖 Generated with Claude Code