Skip to content

Add shortcut pins: one-click prompt injection from the sidebar - #930

Merged
selfcontained merged 13 commits into
mainfrom
agt_6ebe2779c848/build-action-pin-type
Aug 11, 2026
Merged

Add shortcut pins: one-click prompt injection from the sidebar#930
selfcontained merged 13 commits into
mainfrom
agt_6ebe2779c848/build-action-pin-type

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

Agents can now pin a shortcut — a button whose label is the button text and whose value is 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 — InjectionCoordinatorTmuxTerminal.sendCommand with gate: false, since the click is the user acting. No new dispatch/callback system, no new permission model.

POST /api/v1/agents/:id/pins/:pinId/run looks 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.
  • variantdefault / primary / destructive, mapping to existing buttonVariants.
  • 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

upsertPin previously 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 — clean
  • pnpm run finalize:web — clean
  • Server vitest: 2591 passed, 8 skipped
  • Web vitest: 740 passed (6 new in pins-panel.test.tsx covering render, confirm accept/cancel, and both disabled paths)
  • pnpm run test:e2e: 179 passed, 12 skipped (new case in media-sidebar.spec.ts covering button + caption render, the confirm dialog, and the run endpoint firing)
  • Validated live against an isolated dev stack across two mocked scenarios: an Idea Inbox sidebar with grouped shortcuts, and a blocking decision gate where the group name is the question.

Notes

Scope is deliberately just the shortcut type. The multi-question/form concept is tracked separately as interactive-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

selfcontainedand others added 13 commits August 10, 2026 18:51
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>
@selfcontained
selfcontained merged commit 851d0eb into mainAug 11, 2026
1 check passed
@selfcontained
selfcontained deleted the agt_6ebe2779c848/build-action-pin-type branch August 11, 2026 05:21
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>
Sign up for freeto 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.

1 participant

@selfcontained