Repository navigation
feat(web): theme the orchestrator-V2 surfaces the sync left upstream-default - #168
NoahHendrickson wants to merge 1 commit into
Conversation
…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>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: 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.
Thermo-nuclear review: do not approve.
The tree belongs in custom/ and correctly reuses subagentGroupSummary / 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 V2SubagentGroup behind the oxlint disable is the documented sync tactic — not a finding. Do not decompose theme.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
| export function isForkSubagentSettled(status: ForkSubagentStatus): boolean { | ||
| return ( | ||
| status === "completed" || | ||
| status === "failed" || | ||
| status === "cancelled" || | ||
| status === "interrupted" | ||
| ); | ||
| } | ||
|
|
||
| /** The server's placeholder when a child ends without output; the mark already says it. */ | ||
| const GENERIC_CHILD_END = /^Child task ended with status\b/iu; | ||
|
|
||
| /** One line of a markdown result: drop list bullets, code ticks, and link targets. */ | ||
| function plainDetail(text: string): string { | ||
| return text | ||
| .replace(/\[([^\]]+)\]\([^)]*\)/gu, "$1") | ||
| .replace(/`/gu, "") | ||
| .replace(/^[ \t]*[-*][ \t]+/gmu, "") | ||
| .replace(/\s+/gu, " ") | ||
| .trim(); | ||
| } | ||
|
|
||
| function resolveDetail(input: { | ||
| readonly status: ForkSubagentStatus; | ||
| readonly progress: string | undefined; | ||
| readonly result: string | null; | ||
| }): string { | ||
| const progress = input.progress?.trim() || null; | ||
| const result = input.result?.trim() || null; | ||
| const raw = isForkSubagentSettled(input.status) ? result || progress : progress || result; | ||
| if (raw === null || GENERIC_CHILD_END.test(raw)) return STATUS_WORD[input.status]; | ||
| return plainDetail(raw) || STATUS_WORD[input.status]; | ||
| } | ||
|
|
||
| const ACRONYMS = new Set(["gpt", "glm", "grok"]); | ||
|
|
||
| /** | ||
| * A model slug the way the Figma tree spells it: vendor prefix dropped, words | ||
| * title-cased, a trailing version joined with dots — `claude-opus-5-5` reads | ||
| * "Opus 5.5", `gpt-6-astra` reads "GPT 6 Astra". Null when there is nothing to | ||
| * show; the raw slug when it has no dashes to work with. | ||
| */ | ||
| export function formatForkSubagentModel(model: string | null): string | null { | ||
| const slug = model?.trim().toLowerCase(); | ||
| if (!slug) return null; | ||
| const parts = slug.split(/[-_/]+/u).filter((part) => part.length > 0); | ||
| if (parts.length === 0) return null; | ||
| if (parts[0] === "claude" && parts.length > 1) parts.shift(); | ||
| const words: string[] = []; | ||
| for (const part of parts) { | ||
| const numeric = /^\d+$/u.test(part); | ||
| const previous = words.at(-1); | ||
| if (numeric && previous !== undefined && /^\d+(\.\d+)*$/u.test(previous)) { | ||
| words[words.length - 1] = `${previous}.${part}`; | ||
| continue; | ||
| } | ||
| words.push( | ||
| numeric | ||
| ? part | ||
| : ACRONYMS.has(part) | ||
| ? part.toUpperCase() | ||
| : part[0]!.toUpperCase() + part.slice(1), | ||
| ); | ||
| } | ||
| return words.join(" "); |
There was a problem hiding this comment.
This file is a fork-local reimplementation of the upstream subagent display model, not a thin adapter for Figma chrome.
Copied / re-derived from code that already exists:
isForkSubagentSettled≡isTerminalSubagentStatus(subagentRuntime.ts)GENERIC_CHILD_END/plainDetail/isoOrNullcopied fromV2LifecycleRowresolveDetail's progress/result pick issubagentDetailPreview's rule, then a local fallbackformatForkSubagentModelis a second slug parser.formatModelSlugName("claude-opus-5-5")is already"Claude Opus 5.5"; strip the vendor prefix and turn hyphens into spaces. That deletes the ACRONYMS / dotted-version loop and still yields"Opus 5.5"/"GPT 6 Astra".
Code judo: keep STATUS_MARK (sidebar vocabulary). For everything else, join the live roster onto the items and call the canonical helpers. Delete isForkSubagentSettled (exported, unused outside this file). Keep DateTime.Utc on the row instead of isoOrNull here — AgentElapsed can format once, and batch timing should not Date.parse strings this just produced.
Do not extract these helpers out of MessagesTimeline / V2LifecycleRow into a new shared module for this PR. Import what is already exported (isTerminalSubagentStatus, formatModelSlugName, subagentDetailPreview) and keep any leftover one-line display strip in this file.
| function resolveBatchTiming(rows: ReadonlyArray<ForkSubagentTreeRow>) { | ||
| let startMs: number | null = null; | ||
| let endMs: number | null = null; | ||
| let endUnknown = false; | ||
| for (const row of rows) { | ||
| if (row.startedAt) { | ||
| const ms = Date.parse(row.startedAt); | ||
| startMs = startMs === null ? ms : Math.min(startMs, ms); | ||
| } | ||
| if (row.completedAt) { | ||
| const ms = Date.parse(row.completedAt); | ||
| endMs = endMs === null ? ms : Math.max(endMs, ms); | ||
| } else { | ||
| endUnknown = true; | ||
| } | ||
| } | ||
| if (startMs === null) return null; | ||
| const live = rows.some((row) => row.mark === "rain"); | ||
| return { | ||
| status: live ? ("running" as const) : ("completed" as const), | ||
| startedAt: new Date(startMs).toISOString(), | ||
| completedAt: live || endUnknown || endMs === null ? null : new Date(endMs).toISOString(), | ||
| }; |
There was a problem hiding this comment.
subagentGroupTiming in MessagesTimeline already implements this rule (first launch → last settle, withhold the end when a settled member has no completedAt). This copy adds a Date.parse round-trip on ISO strings isoOrNull just wrote, then re-encodes liveness as mark === "rain" even though subagentGroupSummary already computed active from pending/running/waiting.
Do not pull subagentGroupTiming out of the 5.5k timeline file. Compute batch timing from DateTime (or from statuses) in one place, store the AgentElapsed shape once, and use tree.active. Stop treating the sidebar mark as a second live/settled flag.
| export interface ComposerBackgroundLivenessTask { | ||
| readonly id: string; | ||
| readonly label: string; | ||
| readonly kind: string; | ||
| readonly childThreadId: ThreadId | null; | ||
| } | ||
|
|
||
| type ComposerBackgroundLivenessPillBase = { | ||
| readonly stopping: boolean; | ||
| readonly onStop: () => void; | ||
| /** | ||
| * The tasks behind the count. With any present, the label opens a popover | ||
| * that names each one and opens a subagent's thread — what upstream's | ||
| * dropped background-work banner listed inline, without the banner. | ||
| */ | ||
| readonly tasks?: ReadonlyArray<ComposerBackgroundLivenessTask>; | ||
| readonly onOpenThread?: (threadId: ThreadId) => void; |
There was a problem hiding this comment.
PendingBackgroundWorkItem already has taskId, kind (BackgroundWorkKind), label, and childThreadId. This clone widens kind to string and makes tasks / onOpenThread optional for a component with one caller that always passes both.
The 4-state UI (inert label vs popover, inert row vs button) plus tasks ?? [] plus onOpenThread?. after a null check is optionality covering a missing pass-through. presentPendingBackgroundWork never returns an empty items list, so when this pill renders there is always a roster.
Delete ComposerBackgroundLivenessTask. Take presentation.items and a required onOpenThread. Drop the conditional spreads in composerContextStrip.
| // The tasks behind the count, for the pill's list: a subagent's row | ||
| // opens its thread (what upstream's dropped banner linked to). | ||
| tasks: presentation.items.map((item) => ({ | ||
| id: item.taskId, | ||
| label: item.label, | ||
| kind: item.kind, | ||
| childThreadId: item.childThreadId ?? null, | ||
| })), | ||
| onOpenThread: onOpenRelatedThread, |
There was a problem hiding this comment.
The comment a few lines up says ChatView only wires props and assembly lives in custom/composerContextStrip. This mapping is the opposite: a 12k last-resort file constructing a DTO that is PendingBackgroundWorkItem with renamed keys and ?? null.
Pass presentation (or presentation.items) into resolveComposerLivenessPillProps and derive liveness / liveCount / the roster there. The new composer-shell guard pins tasks: presentation.items.map(...), so a sync that keeps this clone stays green. Point the guard at presentation.items (or at ChatView not mapping).
| :root[data-fork="noahhendrickson-t3code"].dark | ||
| [data-thread-details-card] | ||
| :is( | ||
| button:hover, | ||
| button[data-pressed], | ||
| button[data-popup-open], | ||
| [class*="group/thread-details-"]:hover, | ||
| [class*="group/thread-details-"]:has([data-popup-open]) | ||
| ) { | ||
| background-color: var(--fork-outline-hover-bg) !important; | ||
| } |
There was a problem hiding this comment.
This keys fork hover on Tailwind group names (group/thread-details-link, group/thread-details-action from threadDetailsPanelStyles.ts) plus a blanket button:hover !important inside the card.
Same class of smell as #167's [data-fork-pill] > button.h-auto: the selector is tied to an upstream utility string, and button:hover restyles every button in the panel. A future class containing that substring inherits the lift.
This PR already stamps data-fork-sweep-badge for the sidebar chip. Do that here: a tiny fence on the card / split groups, CSS keys off the attr, guard pins the stamp. Do not parse upstream class strings.
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.


Stacked on #167. Merge after it.
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