Add batch pin writes, update-by-id, bulk delete, and collapsible groups - #946
Merged
selfcontained merged 5 commits intoAug 12, 2026
Merged
Conversation
Managing many pins scaled badly: there was no batch write, and `label` was the only match key for an update — so even a pure relabel meant deleting and recreating each pin with every decoration restated. The concrete trigger was 42 tool calls to relabel 21 pins. Pins are addressed by id everywhere except the write path: you can list, delete, and inject a shortcut by id, but not update one. That asymmetry is what forced the delete-and-recreate, since changing a label changed the only handle the writer understood. - `dispatch_pin` accepts an optional `id`. With one, the pin is matched by id and `label` becomes an ordinary editable field, making a rename a single-field patch. Without one, behaviour is unchanged. - New `dispatch_pins` writes a batch in one `mutatePins` transaction — one lock, one write, one `agent.upsert` event instead of N of each. Mode `replace` requires a `group` and rebuilds exactly that group in the order given. There is deliberately no whole-list replace: every destructive batch must name the group it may clear, so a call cannot delete a pin the agent forgot to restate. - `dispatch_delete_pin` also takes `ids` or `group`. - Group headings in the sidebar collapse, with a member count. Groups over 8 pins start collapsed; an explicit choice always wins over that default and persists per agent and group. `type` is now optional rather than defaulting to "string". Defaulting it made a relabel silently demote a shortcut pin to a plain string and drop its icon — found by exercising the tools against a dev instance, not by the tests. An omitted type inherits from the stored pin, and the value is validated against the resolved type rather than the request's. Array-level write logic moves to `pin-write.ts` so the single and batch paths share one definition of matching, merging, and validation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gaps From the frontend-ux and backend-security persona reviews of #946. - `collapseScope` no longer falls back to a literal "default" bucket. It is `string | null`; `null` keeps collapse ephemeral rather than persisting under a shared key, and agent history now passes its `agentId` so its groups are namespaced like everywhere else. - The group heading's `aria-labelledby` now targets the name span rather than the whole button, so the group is announced as "Ready to build" and not as the collapse action plus its count. The member region stays mounted with `hidden` so `aria-controls` always resolves; its members still unmount while collapsed. - `replacePinGroup` resolved every spec against the original array, so two entries creating the same new label both looked unmatched and both were appended — breaking case-insensitive label uniqueness, which the sidebar and label-addressed updates depend on. Uniqueness is now asserted over the finished array. - `dispatch_delete_pin` accepted `group: ""`. Since a missing group compares equal to "", that deleted every ungrouped pin. Blank group names are now rejected at the schema (trim + min length) and again in `removePinGroup`/`replacePinGroup`, so no caller can reach the destructive path without naming a group. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`type` was made inheritable so a relabel couldn't silently demote a shortcut pin, but `value` was left required — so a rename still restated the prompt it wasn't changing, and "rename is a one-field patch" wasn't actually true. Same gap, same fix. An omitted `value` now inherits from the pin being updated. It stays mandatory when creating, since a pin with no value has nothing to display; that check moves into `toStorablePin`, which is the one place that knows whether an update or a create is happening. Validation still runs on the merged pin, so an update that changes only the type re-checks the value it inherited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From the architecture-review persona. Two were real defects.
**Create never stripped shortcut-only fields.** `mergePin` enforced "a
non-shortcut pin carries no shortcut decorations" on the update branch
only; both create branches bypassed it. Before `type` became optional
the handler's isShortcut gate hid this, but an omitted type now lets
icon/variant/confirm/disabled through — so dispatch_pin({label, value,
confirm: true}) stored a string pin with confirm on it, and since
variant/confirm/disabled aren't clearable by empty string the agent
could never remove them. The strip moves out of `mergePin` into
`finalizePin`, which every write path runs.
**`replacePinGroup` didn't file members under the group.** It rebuilt
positions but never wrote `group`, so correctness depended on a
compensating map in `handleUpsertPins` — the primitive couldn't honour
its own name, and every existing test passed `group` on the specs, so
they encoded the caller's behaviour rather than the module's contract.
It now applies the group itself and the handler's map is gone.
Also:
- Hoisted `pinFields` so the two tool schemas share their constraints
instead of duplicating them; only descriptions differ per tool.
- `upsertPins` no longer returns a `pins` field its caller ignored, and
the batch echo is a thin {id, label, group} projection — 50 pins of
full values to convey ordering was the wrong trade.
- `PinGroup` branches above the hooks into Persisted/Ephemeral wrappers
over a presentational view, rather than calling both `useAtom` and
`useState`. Drops the placeholder atom that was never written through.
- Group DOM ids come from `useId`. Both sidebars are always mounted, so
name-derived ids collided and the new `aria-controls` resolved to the
other instance's region.
- Narrowed `PinSpec["type"]` to `PinType`, so the handler's check is
carried by the compiler instead of being re-run downstream.
- Moved two JSDoc blocks back onto the symbols they describe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Hoisting `pinFields.group` collapsed two rules that only looked alike. A pin's own group must accept "" — that is the documented way to clear it, and `CLEARABLE_FIELDS` handles it downstream — but the hoisted field carried the `.trim().min(1)` needed by the *scoping* params, so clearing a group became impossible. A regression introduced by the dedup itself. `pinFields.group` is now `.max(100)` only and used by the per-pin fields; `pinFields.scopingGroup` keeps the strict rule and is used by the two places where a blank name would widen a delete into "every ungrouped pin". Added a test for the clear path, which nothing covered. Also point `McpRequestContext.upsertPins` at `PinSummary[]`. It still declared `PinListing[]` after the echo was thinned; the deps thread through as `unknown`, so the boundary type was quietly wrong rather than failing the build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
selfcontained
deleted the
agt_58b788799838/build-pin-bulk-ops-grouping-affordances
branch
August 12, 2026 20:10
selfcontained added a commit
that referenced
this pull request
Aug 13, 2026
The #945/#946 wave added five tools and changed how three others are addressed. #945 removed get_agent_history and get_parent_context from every doc surface in the same commit that introduced dispatch_review_get_feedback, brain_get_event, brain_get_list_item and whiteboard_howto — so the lists looked freshly maintained while being short by five entries. - docs-pane Repo Tools "Built-in tools": the five new tools, plus pin update-by-id, dispatch_pins batch writes with merge/replace, delete by id/ids/group, and reading one pin back whole by id. A closing paragraph states the list/detail policy response.ts now encodes, so an agent reading the docs knows a truncated listing has a matching full read. - dispatch_launch_agent: with a templateId the template's own prompt is now rendered and filled from templateArgs (#941) — it was previously used only for worktree settings, and the bullet still described it as just "or template". - media.tsx Pins tab: pin groups collapse under a heading with a member count, groups over eight start collapsed, and the choice persists per agent and group (#946). - automations.tsx job-agent lists: the new tools, plus persona_templates / persona_upsert / persona_validate and dispatch_archive_agent, which are in JOB_TOOLS but were never enumerated there. - README interactive-agents table and the persona-agents list. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 13, 2026
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.
Managing many pins scaled badly for an agent: no batch write, and
labelwas the only match key for an update — so even a pure relabel meant deleting and recreating each pin with every decoration restated. The concrete trigger was 42 tool calls to relabel 21 pins.Pins are already addressed by id everywhere except the write path — you can list one by id, delete by id, and inject a shortcut's prompt by id (that last one is the whole trust boundary for
POST /terminal/inject-pin/:pinId). OnlyupsertPincouldn't. That asymmetry is what forced delete-and-recreate: changing a label changed the only handle the writer understood.What's new
dispatch_pintakes an optionalid. With one, the pin is matched by id andlabelbecomes an ordinary editable field — a rename is a single-field patch. Without one, behaviour is byte-for-byte what it was, so no existing usage shifts.New
dispatch_pinswrites a batch in one transaction. Applying N pins throughupsertPincosts NBEGIN/SELECT … FOR UPDATE/COMMITcycles, NgetAgentreads, and Nagent.upsertevents; batched it's one of each, and a failure can't leave the set half-applied. The 42-call relabel becomes one call.Mode
replacerequires agroupand rebuilds exactly that group in the order given. There is deliberately no whole-list replace — every destructive batch must name the group it's allowed to clear, so no call can delete a pin the agent forgot to restate. The rebuilt block is anchored where the group already sat, so replacing its contents doesn't make it jump position.dispatch_delete_pinalso takesidsorgroupfor bulk removal.Group headings collapse, with a member count. Groups over 8 pins start collapsed; an explicit choice always beats that default and persists per agent and group. Keying on agent id rather than name means it survives a session rename.
One bug worth calling out
typeis now optional rather than defaulting to"string". The default made a relabel silently demote a shortcut pin to a plain string and strip its icon and variant — which would have broken the exact workflow this PR exists to fix. The unit tests were green when this was happening; it only surfaced from exercising the tools against a dev instance. An omitted type now inherits from the stored pin, and the value is validated against the resolved type rather than the request's, so an update can't sneak a bad value past the validator by omitting the type.A second thing the tests couldn't have caught:
upsertPinswasn't threaded through the MCP route context, soregisterBatchPinToolreturned early and the tool never registered on a real server. Also found live.Notes
toPinListingthrows without an id, so an id-less pin would already be breakingdispatch_list_pins.pin-write.tsso the single and batch paths share one definition of matching, merging, and validation; a batch can't accept a shapedispatch_pinwould reject.dispatch_pins' description points atdispatch_pinfor field semantics rather than restating them, since every agent pays that context cost.Verification
pnpm run check,pnpm run test(156 server + 58 web files),pnpm run test:e2e(180 passed),pnpm run finalize:weball green. Exercised end-to-end against a dev instance over the real MCP endpoint: batch create, relabel-by-id preserving type/icon/variant/group, group-scoped replace leaving outside pins untouched, replace-without-group rejected, unknown id rejected, bulk delete by group. Collapse verified in-browser at 1440px and 390px, including the sticky-choice-across-reload path and the two mounted sidebar instances staying in sync.🤖 Generated with Claude Code