Repository navigation
feat(web): theme the orchestrator-V2 surfaces the sync left upstream-default - #168
Conversation
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
There was a problem hiding this comment.
Stale comment
Thermo-nuclear review: do not approve.
The tree belongs in
custom/and correctly reusessubagentGroupSummary/summarizeSubagentStatuses. The rest of the PR stands up a second subagent-display pipeline and a second background-work DTO beside helpers that already do this work. That is a structural regression, not a presentation layer.Keeping unused
V2SubagentGroupbehind the oxlint disable is the documented sync tactic — not a finding. Do not decomposetheme.custom.css/ ChatView / the yaml for these adds.Blockers
forkSubagentTreeis a parallel pipeline. Settled-status, progress/result pick, markdown strip, ISO round-trip, slug spelling, and batch timing are re-derived next toisTerminalSubagentStatus,subagentDetailPreview,formatModelSlugName,V2LifecycleRow'splainDetail/GENERIC_CHILD_END, andsubagentGroupTiming. Two copies will drift the first time upstream tweaks any of those. Keep Figma chrome inForkSubagentGroup; shrink the model to a live-roster join plus the existing helpers.STATUS_MARKis the one fork-owned table that should stay.ChatView maps a clone of
PendingBackgroundWorkItem. The composer-shell comment says assembly lives incomposerContextStrip, then ChatView (already ~12k) builds a looser DTO (kind: string, optionaltasks/onOpenThread, conditional spreads) for a single call site that always has items and a navigator.presentPendingBackgroundWorknever returns an empty list. Passpresentationthrough; deleteComposerBackgroundLivenessTask. The new guard pins the.map, so the bad shape is now a sync invariant.Also fix
- Timestamp round-trip (
DateTime→ ISO →Date.parse→ ISO) and liveness encoded twice (mark === "rain"vsgroup.active).- Thread-details hover keys on
[class*="group/thread-details-"]plus a blanketbutton:hover !important. Same class of smell as#167's.h-autopin: Tailwind internals, not a data stamp. The sweep badge in this PR already does the right thing (data-fork-sweep-badge).- Stopped members skip
SidebarV2StatusIndicatorfor a one-off 8px fill. Route stopped through the existing mark set.Sent by Cursor Automation: Thermo nuke 4.6
NoahHendrickson
left a comment
There was a problem hiding this comment.
Reviewed at 1cd2058, including its integration with the latest #167. Changes needed.
[P1] Restore the status-dot export when updating the stack. ForkSubagentGroup.tsx:40
This imports SidebarV2StatusDot, but #167's later d6fe721 cleanup removes that export from SidebarV2StatusIndicator.tsx. This branch was cut before that cleanup and does not change the indicator file. A clean merge with #167 at 78b5c2e therefore leaves this import pointing at a private function. I reproduced the combined tree with git merge-tree and ran a focused Rolldown bundle of the two resulting modules: it fails with MISSING_EXPORT for SidebarV2StatusDot. Rebase/update onto the current base and restore the export or use the public indicator API before merging; the earlier green typecheck/build does not cover this combined tree.
[P2] Update the timeline behavior tests alongside the replacement renderer. MessagesTimeline.tsx:2828
ForkSubagentGroup always renders its members, but six existing MessagesTimeline cases still expect the group to start collapsed and then click its disclosure. All six fail at MessagesTimeline.test.tsx:2111 (expected 0 member buttons, got 2), so this PR's Test Web check remains blocked even after bringing in the base's unrelated CI fixes. Update those cases to exercise the intended always-visible tree, preserving checks for live/completed/cancelled detail and child-thread navigation.
Validation: the focused forkSubagentTree + MessagesTimeline run has 82 passing tests and these 6 failures. I traced the other CI failures (knip's ThreadContextDragGhost, sidebar-width/settings tests, Windows profile fixture) to the older #167 base; its later fixes address those separately. No browser verification.
NoahHendrickson
left a comment
There was a problem hiding this comment.
Review: #168 V2 surface theming
Verdict: not mergeable yet. CI is red for two separate reasons, and three of the new rules/helpers misbehave on real data. The structure is mostly right (the tree lives in custom/, reuses subagentGroupSummary / summarizeSubagentStatuses, and the Figma chrome is cleanly separated), so this is a fix-up round, not a redo.
Blockers
-
Branch is behind its base. This branch forks off 82a31f8, before #167's two follow-up commits (d6fe721 knip ignores + the two upstream tests, 78b5c2e Windows profile test). That is the
ThreadContextDragGhost.tsxknip failure and theSettingsPanels.restore/threadSidebarWidthfailures in CI. Mergeorigin/claude/sync-2026-10-05in (or rebase). I reproduced all three locally on this head and they are not from your change. -
Six upstream
MessagesTimeline.test.tsxcases fail on the swap itself.shows 2 {running,completed,cancelled} subagents … grouping only multiple agents(6 cases) look for upstream's group button (aria-label="2 subagents") and expect the member buttons to be hidden until it is clicked; the fork tree renders theOpen Package auditbuttons immediately, soexpect(child()).toHaveLength(0)at line 2111 fails. Reproduced locally: 6 failed / 270 passed in that file. These need fork-fenced adjustments in the test (the convention #167 already uses in the same file) or a fork variant, plus the manifestwatch:entry. -
The thread-details hover rule wipes the automation Switch's checked fill.
theme.custom.css:2627-2636putsbackground-color: … !importanton everybutton:hoverinside[data-thread-details-card].ThreadAutomationsPanel(mounted inside the card atThreadDetailsPanel.tsx:226) renders a base-ui<Switch>, which is a<button role="switch">whose track isdata-checked:bg-primary; the!importantbeats the utility, so hovering an enabled automation turns its track into the 4% wash and it reads as off. Scope the rule to the row/action wrappers or exclude[role="switch"]. Cursor's point about keying ongroup/thread-details-*class substrings stands too: a data stamp is the fork's own convention (the sweep badge in this PR does it right). -
The pill label button inherits the context-row chip hover fill.
ComposerShell.css:108resetsbackground: transparentonbutton[data-fork-monitoring-list]at specificity (0,4,1), buttheme.custom.css:1072-1079([data-fork-composer-context-row] :is(button, …):is(:hover, [data-pressed], [data-popup-open]):not(:disabled)) is (0,5,1) and wins. Hovering "2 agents", and the whole time the popover is open, paints a 24px zero-padding chip-hover block inside the pill, the exact artifact the stop button's own comment in this file guards against. Give the list button its own:hover/[data-popup-open]rule the way the stop button does. -
formatForkSubagentModelmangles the ids Claude subagents actually report. For Claude,task.modelis the API's dated snapshot id (ClaudeAdapterV2.ts:5823,message.message.model). Running the formatter on those:slug fork formatModelSlugNameclaude-sonnet-4-5-20250929Sonnet 4.5.20250929 Claude Sonnet 4.5 20250929 claude-opus-4-6[1m]Opus 4 6[1m] Claude Opus 4.6[1m] claude-opus-5-5Opus 5.5 Claude Opus 5.5 The "join every consecutive numeric part with a dot" rule treats the date as a third version segment. Reuse
formatModelSlugNamefrom@t3tools/shared/modeland strip the vendor word, or go through the catalog like upstream'sresolveSubagentMetadatadoes (it never shows the date).
Agree with Cursor on reuse, with specifics
isForkSubagentSettledisisTerminalSubagentStatus(client-runtime/state/subagentRuntime.ts:84) over the same literal union; delete it.resolveDetail's progress/result preference issubagentDetailPreview(client-runtime/state/subagentDisplay.ts:121), already exported. Call it, then apply the fork-onlyGENERIC_CHILD_ENDfallback and the markdown strip on its output.resolveBatchTimingre-encodes liveness asmark === "rain"whiletree.activealready says it, and round-tripsDateTime→ ISO →Date.parse→ ISO. Compute fromtree.activeand the row timestamps once.ComposerBackgroundLivenessTask/ optionaltasks+onOpenThread:presentPendingBackgroundWorknever yields an empty roster, so the label-only fallback atComposerMonitoringPill.tsx:88-99is dead code and the conditional spreads incomposerContextStrip.tsxcover a missing pass-through. Passpresentation.itemsand a requiredonOpenThread; drop the clone type.- Keep
STATUS_MARK; that is the fork's vocabulary.
Nits
ComposerMonitoringPill.tsx:74-78:role="status"now always sits inside the trigger<button>(see dead fallback above). Children ofrole=buttonare presentational, so the "Stopping…" announcement the live region existed for is dropped. Put the live region on a sibling.ComposerMonitoringPill.tsx:101:<PopoverPopup className="w-64 p-1">restyles acomponents/uiexport;width="sm"andpadding="compact"are the knobs, andp-1leaves the inner Viewport's default 16px inset in place anyway. Lint is off forsrc/custom, so CI won't say this.ForkSubagentGroup.tsx:66:aria-labelon a role-less<div>is prohibited (axearia-prohibited-attr); upstream had it on the collapsible trigger button.Sidebar.tsx:1468: the newdata-fork-sweep-badgestamp is inside an existing fence, butsidebarV2RowActionHitArea.test.tsnever mentions it and the manifest entry'sfilesdon't listtheme.custom.css. A sync that drops the attribute reverts the badge to the--primarytint with no red test.- Manifest/body: the subagent entry says the batch's elapsed span rides the header;
resolveBatchTimingreturnsnullwhenever no row hasstartedAt, which is the common provider-native case, so the header often has no span. Fine, just state it.
Verified fine
- Roster join on
agent.id === item.subagentIdmatchesV2SubagentGroupandSubagentTimelineLink; every field the tree reads exists with that type;useAtomValue(atom, selector)is the overload upstream uses atMessagesTimeline.tsx:3060, same subscription granularity as upstream's group. onOpenRelatedThreadis a stableuseCallback; theuseMemodeps are complete; the pill is a direct child ofComposerSurface.ContextStrip, not nested in a button; Stop sits outside the Popover.- Settled set, detail preference, and batch timing rules match upstream's today (the duplication is a drift risk, not a current bug).
- Fork guards 495/495,
forkSubagentTree.test.tspasses,.fork/lint-owned.mjsclean on this head.git merge-treeof this branch with #169's is conflict-free.
Merge mechanics
Retarget to custom with gh pr edit 168 --base custom before #167 merges.
Reviewed by Claude Fable 5.1 via Claude Code (T3 Code); every finding re-verified against the source on a detached checkout of 1cd2058.
| button[data-popup-open], | ||
| [class*="group/thread-details-"]:hover, | ||
| [class*="group/thread-details-"]:has([data-popup-open]) | ||
| ) { |
There was a problem hiding this comment.
button:hover + !important here also hits the base-ui <Switch> (<button role="switch">) that ThreadAutomationsPanel renders inside this card (ThreadDetailsPanel.tsx:226). Its track is data-checked:bg-primary; this rule overrides it on hover, so an enabled automation reads as off while the pointer is over it. Scope to the row/action wrappers (a data stamp, as the sweep badge below does) or add :not([role="switch"]).
| /* The label is a button when the pill has tasks to list: same ink, no chrome | ||
| of its own; the popover it opens is a popover-popup and so takes the one | ||
| frost recipe (fork-popup-surface). Rows inside hover as the 4% lift. */ | ||
| :root[data-fork="noahhendrickson-t3code"] |
There was a problem hiding this comment.
Specificity: this reset is (0,4,1), but theme.custom.css:1072-1079 ([data-fork-composer-context-row] :is(button, …):is(:hover, [data-pressed], [data-popup-open]):not(:disabled)) is (0,5,1) and wins. Hovering the label, and the whole time the popover is open, paints the chip-hover fill as a 24px zero-padding block inside the pill. The stop button in this file avoids it with its own :hover:not(:disabled) rule; this button needs the same for :hover and [data-popup-open].
| if (parts[0] === "claude" && parts.length > 1) parts.shift(); | ||
| const words: string[] = []; | ||
| for (const part of parts) { | ||
| const numeric = /^\d+$/u.test(part); |
There was a problem hiding this comment.
Claude subagents report dated snapshot ids (ClaudeAdapterV2.ts:5823 stores message.message.model): claude-sonnet-4-5-20250929 → Sonnet 4.5.20250929, claude-opus-4-6[1m] → Opus 4 6[1m] (ran the function on both). The dot-join treats the date as a version segment. formatModelSlugName from @t3tools/shared/model already yields "Claude Opus 5.5"; strip the vendor word from that instead of a second parser.
| ); | ||
| } | ||
|
|
||
| /** The server's placeholder when a child ends without output; the mark already says it. */ |
There was a problem hiding this comment.
This is isTerminalSubagentStatus (packages/client-runtime/src/state/subagentRuntime.ts:84) over the same union, and resolveDetail below is subagentDetailPreview (subagentDisplay.ts:121) plus the fork's GENERIC_CHILD_END / markdown strip. Import both and keep only the strip here; the export is also unused outside this file.
|
|
||
| const tasks = props.tasks ?? []; | ||
| const labelNode = ( | ||
| <span role="status" className="min-w-0 truncate"> |
There was a problem hiding this comment.
tasks is non-empty whenever the pill renders (presentPendingBackgroundWork returns null for zero items and ChatView maps every item), so the labelNode-only branch below is dead and this role="status" always ends up inside the trigger <button>. Children of a button are presentational, so the "Stopping…" announcement is dropped. Move the live region to a sibling and make tasks/onOpenThread required.
| > | ||
| {labelNode} | ||
| </PopoverTrigger> | ||
| <PopoverPopup side="top" align="start" sideOffset={6} className="w-64 p-1"> |
There was a problem hiding this comment.
className="w-64 p-1" restyles a components/ui export (the no-restyle rule; lint is off for src/custom, so nothing flags it). width="sm" / padding="compact" are the knobs, and p-1 lands on the Popup while the inner Viewport keeps its default 16px inset, so the 6px rows sit inside a prose inset anyway.
| <WorkLogBlock continues={row.continuesWorkLog}> | ||
| <div | ||
| data-fork-subagent-tree="" | ||
| data-subagent-group |
There was a problem hiding this comment.
aria-label on a role-less <div> is prohibited (ARIA 1.2, axe aria-prohibited-attr). Upstream had it on the collapsible trigger button; here either give the container a role (group) or drop the label.
| * upstream's group timing. | ||
| */ | ||
| function resolveBatchTiming(rows: ReadonlyArray<ForkSubagentTreeRow>) { | ||
| let startMs: number | null = null; |
There was a problem hiding this comment.
tree.active already says whether the batch is live; mark === "rain" re-derives it from the sidebar vocabulary. And this round-trips DateTime → ISO (isoOrNull) → Date.parse → ISO. Compute the span once from tree.active and the row timestamps, and note that with no startedAt on any row (the usual provider-native case) the header gets no span at all.
| role="status" | ||
| /* fork:begin sidebar-v2-row-action-hit-area — see .fork/customizations.yaml#sidebar-v2-row-action-hit-area | ||
| The sweep's verb badge wears the card's own chip recipe (theme.custom.css | ||
| keys on this stamp) rather than upstream's primary tint. */ |
There was a problem hiding this comment.
The stamp is fenced, but nothing pins it: sidebarV2RowActionHitArea.test.ts never mentions data-fork-sweep-badge and the manifest entry's files don't list theme.custom.css. A sync that drops this attribute reverts the badge to the --primary tint with CI green.
| liveCount: presentation.items.filter((item) => item.kind === "subagent").length, | ||
| stopping: isStoppingBackgroundWork, | ||
| onStop: () => void handleStopBackgroundWork(), | ||
| // The tasks behind the count, for the pill's list: a subagent's row |
There was a problem hiding this comment.
Agree with Cursor here: this maps PendingBackgroundWorkItem into a looser clone for one caller that always has a roster and a navigator. Pass presentation.items and onOpenRelatedThread through to resolveComposerLivenessPillProps and let it (or the pill) read taskId/label/kind/childThreadId directly; then the new guard can pin that instead of the .map.
…default The V2 sync (#167) absorbed new upstream surfaces as upstream drew them. This brings each one onto the fork's recipes: - Thread details panel: its inner card joins the one popup frost rule (28px blur, --fork-popup-fill, 12% hairline), takes the composer vessel's radius, the 4% row lift and the vessel hairline on section dividers. - Subagent spawn batches: ForkSubagentGroup renders the Figma tree (TreeView lead, status summary with the batch's elapsed span, member rows with the sidebar marks, activity, model and elapsed) in place of upstream's avatar-stack group, fed from the live subagent roster. Un-parks fork-subagent-spawn-card. - Liveness pill: the count opens a frosted popover naming each pending task; a subagent's row opens its thread. - Sidebar sweep badge: the card's chip recipe instead of the primary tint. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The subagent tree model reuses upstream's helpers: subagentDetailPreview for the progress-or-result pick and formatModelSlugName for the model's name (vendor word dropped, snapshot date left off, so the dated ids Claude subagents report no longer read "Sonnet 4.5.20250929"). The batch's elapsed span is computed once from the DateTime values and the group's own active flag instead of a second timing rule over re-parsed ISO strings; isForkSubagentSettled is gone. Members draw through SidebarV2StatusMark, so stopped takes the idle circle instead of a one-off fill and the un-exported dot is not needed. - The liveness pill takes upstream's PendingBackgroundWorkItem roster as is: ChatView hands the presentation over, the strip derives liveness and the list, tasks and onOpenThread are required, the dead label-only branch is gone, the live region sits beside the trigger so "Stopping..." is announced, and the popover uses width="sm" / padding="compact". - The label button holds transparent on hover and while its popover is open, outranking the context row's chip-hover rule. - The thread details lift keys on fork-thread-details-lift, a hook fenced onto upstream's one hover-surface class string, so it reaches every row and split half and never the automations Switch; no Tailwind group name or blanket button:hover. - The grouped-batch MessagesTimeline cases no longer click a collapsed group open (fenced); the tree has no such state. - Guards pin the sweep badge stamp and its chip rule, the lift hook, the roster pass-through and the live region; the manifest lists the new fenced files and says when the header span exists. role="group" carries the tree's aria-label. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1cd2058 to
fe0fa05
Compare
|
Addressed at fe0fa05, after rebasing onto #169's branch ( Blockers
Reuse (Cursor + review)
Nits
Verified locally: web typecheck clean; fork guards + |
There was a problem hiding this comment.
Thermo-nuclear review at fe0fa05: approve. No major structural issues.
The skill bar is met: no structural regression, no leftover canonical-helper clone, no 1k crossing, no spaghetti at the fenced call sites. The three blockers from the earlier HEAD are gone.
forkSubagentTreejoins the live roster and callssubagentDetailPreview/subagentGroupSummary/summarizeSubagentStatuses/formatModelSlugName.STATUS_MARK, the localGENERIC_CHILD_END+plainDetailstrip after the preview, and batch timing fromDateTime+group.activestay as the requested fork shape — not a second display pipeline.- ChatView only passes
presentation+onOpenThread. The pill readsPendingBackgroundWorkItemdirectly; the clone DTO is gone. - Thread-details hover keys on
fork-thread-details-liftstamped on the shared hover-surface string, not[class*="group/thread-details-"]or blanketbutton:hover. The automations Switch is out of the selector.
Unused V2SubagentGroup remains the sync tactic. Do not decompose theme.custom.css / ChatView / MessagesTimeline / yaml for this add.
Sent by Cursor Automation: Thermo nuke 4.6


Stacked on #169 (itself stacked on #167). Merge after both.
Problem
The V2 sync absorbed several new upstream surfaces exactly as upstream draws them: the thread details panel opened on upstream's 16px glass with a 22px radius beside the fork's frosted menus, subagent spawn batches rendered upstream's avatar-stack group (the fork's Figma tree had been parked), the liveness pill lost the per-task list upstream's dropped banner used to carry, and the sidebar sweep badge lit up in the primary tint next to the card's muted chips.
What this does
data-thread-details-cardjoins the one popup frost rule (28px blur,--fork-popup-fill, 12% hairline) instead of being a surface of its own; it takes the composer vessel's radius, the fork's 4% row lift (upstream's hover utilities are!important, so the lift is too, stacking group + segment like upstream does) and the vessel hairline on section dividers. Toolbar pill marks stay off the panel rows.ForkSubagentGrouprenders the Figma tree (t3-fork 376:21366) at the one call site: TreeView lead, "Kicked off / Ran N subagents", status summary with the batch's elapsed span, member rows with the sidebar's rain/dot marks, title, live activity or result, model ("Opus 5.5" from the slug) and elapsed. Fed from the thread's live subagent roster like upstream's group. V2 carries no effort or token usage per subagent and no agents panel, so the meta line is model + elapsed and a member row opens its own thread. Un-parksfork-subagent-spawn-card; upstream'sV2SubagentGroupstays in the file for the next sync.data-fork-sweep-badgetakes the card's chip fill, pill hairline and radius.Manifest and guards updated for each (
fork-popup-surface,fork-subagent-spawn-card,fork-composer-shell,sidebar-v2-row-action-hit-area).Verified
.fork/lint-owned.mjsclean~/.t3-fork/userdata/attachments/fork-v2-surface-theming/for the PR (gh cannot upload images).Claude Fable 5.1 via Claude Code (T3 Code).
🤖 Generated with Claude Code