feat(web): collapse tool activity into one line#7152

merged7
main
ui-refresh/tool-activity-web
openedAug 16, 2026byMmaria
View on GitHubTwo-way sync enabledPR last imported 3d ago

What changed

Tool calls now collapse into one compact expandable line for both live and completed work. Live activity uses the moving focus animation, and the working row keeps a reliable Worked for timer by falling back to the latest user message timestamp while the turn is being acknowledged.

This PR intentionally leaves turn folding, deeper lifecycle reconciliation, and mobile parity to the follow-up PR.

Screenshots

BeforeAfter
Tool activity before Grouped tool activity after

Validation

  • Timeline logic and render tests
  • Session work-log tests
  • Web typecheck
  • Changed-file lint and formatting checks

Built with GPT-5.6-sol in the Codex harness.

Note

Collapse tool activity into one-line rows in MessagesTimeline

  • Reworks deriveMessagesTimelineRows in MessagesTimeline.logic.ts to group active tool runs into a single work-live row and summarize prior runs as collapsed work-toggle rows.
  • Adds summarizeToolGroup and related helpers to generate natural-language summaries (e.g., "Changed 2 files") and icons for collapsed tool groups.
  • Introduces LiveWorkEntryTimelineRow and ThinkingActivityRow components with CSS shimmer animations in MessagesTimeline.tsx to display live activity as a single animated line.
  • Updates session-logic.ts to collapse interleaved tool lifecycle updates by (turnId, toolCallId) and refine deriveActiveWorkStartedAt timing.
  • Behavioral Change: MessagesTimeline no longer accepts the activeTurnInProgress prop; tool lifecycle rows from different turns will no longer collapse together.

Macroscope summarized 60218ae.


Note

Medium Risk Large changes to timeline row derivation and rendering affect how every in-flight and completed tool is shown; session collapse key changes could mis-merge or split tool rows if edge cases are wrong.

Overview Tool activity in the chat timeline is grouped into compact rows instead of many separate lifecycle lines. Active runs use a work-live row with animated focus and labels like “Running pnpm” (from shell parsing); settled tool-only groups use work-toggle with summaries (“Ran 2 commands”, “Changed 3 files”), icons, and failure affordances. Row derivation handles adjacent in-progress tools, commentary boundaries, stale tools after a new send, and expanded group layout.

Session / work log now keeps toolCallId on entries, collapses interleaved tool.updated / tool.completed by (turnId, toolCallId), and splits display vs output failure detection via workEntryDisplayIndicatesToolFailure.

Chat chrome: activeTurnInProgress is removed from MessagesTimeline; deriveActiveWorkStartedAt can use latestUserMessageAt while a turn is acknowledged. Plain tool rows drop success/neutral badges in favor of one-line copy. CSS adds live-activity-focus shimmer with reduced-motion support.

Reviewed by Cursor Bugbot for commit 60218aec86a15f1a563cb910cab495b8faf1c3d7. Bugbot is set up for automated code reviews on this repo. Configure here.

Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c860b58f-a190-4026-a292-558459e13872

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ The exact PR base did not have a successful artifact. Baseline uses the latest successful main measurement shown below.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire12.6 KiB13.4 KiB+849 B (+6.6%)15.1 KiB✅
CodexThread snapshot wire6.3 KiB6.9 KiB+623 B (+9.7%)7.3 KiB✅
CodexLive turn WebSocket wire6.3 KiB6.5 KiB+226 B (+3.5%)7.8 KiB✅
CodexLive turn WebSocket decoded51.8 KiB55.0 KiB+3.2 KiB (+6.1%)66.4 KiB✅
CodexLive turn messages16160 (0.0%)21✅
ClaudeTotal thread wire12.6 KiB13.5 KiB+877 B (+6.8%)15.1 KiB✅
ClaudeThread snapshot wire6.3 KiB6.9 KiB+629 B (+9.8%)7.3 KiB✅
ClaudeLive turn WebSocket wire6.3 KiB6.6 KiB+248 B (+3.8%)7.8 KiB✅
ClaudeLive turn WebSocket decoded52.7 KiB55.8 KiB+3.2 KiB (+6.0%)66.4 KiB✅
ClaudeLive turn messages16160 (0.0%)21✅

Baseline: bcfd485 · PR result: 60218ae · Source CI: success

Scenario and decoded snapshot size

10 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.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Synced from GitHub

Reviewed the changed chat timeline UI (ChangedFilesTree.tsx, MessagesTimeline.tsx, index.css) for shared-primitive usage, Tailwind/CSS ownership and preserved interaction semantics. The AgentSpawnCtaRow migration to Button and the new mask/animation utilities living in index.css both look right. Three concrete issues below: one order-dependent cascade conflict on the new live-activity overlay, one responsive-mode inconsistency in the rebuilt subagent card, and one lost failure affordance.

Posted via Macroscope — UI Consistency

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces a new UI feature for collapsing tool activity into summarized rows with animations. It adds significant new rendering logic, shell command parsing, new component types, and CSS animations—constituting a substantial user-facing behavior change that warrants human review.

You can add or adjust custom eligibility rules. Learn more.

apps/web/src/components/chat/MessagesTimeline.tsxline 1461Outdated · 967e2eaOpen thread
……@@ -1370,19 +1392,138 @@ const WorkGroupSection = memo(function WorkGroupSection({
1458+ type="button"
1459+ className="group/live-work flex min-h-6 w-full max-w-full cursor-pointer items-center rounded-md text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
1460+ aria-expanded={row.expanded}
1461+ onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)}
MmacroscopeappBoton line 1461Aug 16, 2026

🟡 Medium chat/MessagesTimeline.tsx:1461

Expanding a live tool group does not show its currently running tool details: aria-expanded changes, but WorkGroupSection filters the inProgress entry through workEntryIndicatesToolNeutralStatus, leaving the expanded group visually empty. Update the expanded-child filtering so the active entry is retained for live groups.

No longer relevant as of 96c8a4df8d37b35ef87d3c9cf78e6bf35d1ece76

Synced from GitHub

Reviewed the timeline/changed-files refresh for shared-primitive use, Tailwind ownership, and interaction/accessibility preservation. The three findings from the previous run are addressed (the shimmer overlay no longer sets inset-0 against the CSS utility's own left/width, the subagent card now uses @container/agent-group queries, and the failure icon is exposed via role="img"). Three new issues introduced by this diff are below.

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 16, 2026
Synced from GitHub

Reviewed the timeline/changed-files refresh for shared-primitive use, Tailwind ownership, and interaction/accessibility preservation. The three findings from the previous run are addressed (the shimmer overlay no longer sets inset-0 against the CSS utility's own left/width, the subagent card now uses @container/agent-group queries, and the failure icon is exposed via role="img"). Three new issues introduced by this diff are below.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 2416–2537Outdated · 63a901eOpen thread
MmacroscopeappBoton lines 2416–2537Aug 16, 2026

chat/MessagesTimeline.tsx:2416

The status text (3 failed / 2 working / Completed) was previously always rendered; gating it on @[32rem]/agent-group means that in a narrow chat column the only remaining failure signal is the aria-hidden colour dot on line 2408, so the state becomes colour-only and invisible to assistive tech. Suggest keeping a text fallback that is only announced when the visible status is hidden.

diff
1 <div className="flex min-w-0 flex-1 items-center gap-1.5 px-1 py-1 text-left text-xs leading-4">
2+ <span className="sr-only @[32rem]/agent-group:hidden">{status}</span>
3 <span className="ml-auto hidden shrink-0 items-center gap-2 text-[11px] text-muted-foreground @[32rem]/agent-group:flex">

No longer relevant as of d2095f27f302cd6d4c8bc795bb6f8e38c509d90e

apps/web/src/components/chat/MessagesTimeline.tsxline 2203Outdated · 2cfabfeOpen thread
……@@ -2025,32 +2174,152 @@ function workEntryPreview(
2200+ tokenStarted = true;
2201+ continue;
2202+ }
2203+ if (character === "\\" && quote !== "'") {
MmacroscopeappBoton line 2203Aug 16, 2026

🟡 Medium chat/MessagesTimeline.tsx:2203

tokenizeShellCommand removes the backslashes in quoted Windows paths, so "C:\Program Files\node.exe" script.js becomes C:Program Filesnode.exe; commandProgramName then cannot split the path and displays Running C:Program Filesnode.exe instead of Running node.exe. Preserve Windows path separators while retaining shell escape handling.

✅ Resolved in d4e205311888b825f7796b07c672a484cda751b5

apps/web/src/session-logic.tsline 1109Outdated · 8408cc3Open thread
……@@ -1007,19 +1082,31 @@ function shouldCollapseToolLifecycleEntries(
10201106 return true;
10211107 }
10221108 return (
1109+ previous.activityKind !== "tool.started" &&
CcursorBoton line 1109Aug 16, 2026

Completed tools jump in timeline

Medium Severity

Merging a tool.started row into its completion copies the completion createdAt (and id) onto the start row. deriveTimelineEntries then re-sorts by createdAt, so a finishing call teleports later in the timeline and can split a live batch around intervening messages.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 8408cc3160f1a19b29f0a1afca773f31d71471b6. Configure here.

Synced from GitHub

One finding on the new live-activity sweep CSS. The earlier findings from previous runs (clipped focus ring on the changed-files preview buttons, colour-only subagent status, dead workingStepLabel, inset-0 vs. live-activity-focus ownership, colour-only tool failure) all look addressed in this revision.

Posted via Macroscope — UI Consistency

Synced from GitHub

One finding on the new live-activity sweep CSS. The earlier findings from previous runs (clipped focus ring on the changed-files preview buttons, colour-only subagent status, dead workingStepLabel, inset-0 vs. live-activity-focus ownership, colour-only tool failure) all look addressed in this revision.

Posted via Macroscope — UI Consistency

Synced from GitHub

One finding: the two new --animate-live-activity-focus* theme tokens are dead and duplicate the timing that the @utility blocks hardcode. Everything else in this revision looks consistent — the earlier focus-ring clipping, the container-query status text, and the workingStepLabel regression are all resolved.

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 16, 2026
Synced from GitHub

One finding: the two new --animate-live-activity-focus* theme tokens are dead and duplicate the timing that the @utility blocks hardcode. Everything else in this revision looks consistent — the earlier focus-ring clipping, the container-query status text, and the workingStepLabel regression are all resolved.

Posted via Macroscope — UI Consistency

apps/web/src/index.csslines 142–143Outdated · d3d2a2dOpen thread
……@@ -139,6 +139,8 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
140140 compositor updates discrete frames instead of every vsync. */
141141 --animate-status-pulse: status-pulse 2s infinite;
142+ --animate-live-activity-focus: live-activity-focus 2.2s steps(24) infinite;
143+ --animate-live-activity-focus-counter: live-activity-focus-counter 2.2s steps(24) infinite;
MmacroscopeappBoton lines 142–143Aug 16, 2026

src/index.css:143

These two tokens are never consumed: no animate-live-activity-focus / animate-live-activity-focus-counter class exists anywhere, and the @utility live-activity-focus (line 482) and @utility live-activity-focus-counter (line 494) hardcode the same 2.2s steps(24) infinite shorthand. That leaves the animation timing with two sources of truth, one of which is inert — editing the token silently changes nothing. Because this is @theme inline, the variables are not emitted either, so the utilities cannot reference them via var().

Suggest dropping the dead tokens and keeping the shorthand in the utilities as the single owner; the @keyframes stay in the output because Tailwind retains @theme keyframes whose name appears in an emitted animation: declaration.

diff
1 --animate-status-pulse: status-pulse 2s infinite;
2- --animate-live-activity-focus: live-activity-focus 2.2s steps(24) infinite;
3- --animate-live-activity-focus-counter: live-activity-focus-counter 2.2s steps(24) infinite;
4 --animate-ghost-pulse: ghost-pulse 2.4s infinite;

No longer relevant as of a95aceb534eb0828d41b98fecb21d880c7ad7262

apps/web/src/components/chat/MessagesTimeline.tsxline 1472Outdated · d3d2a2dOpen thread
……@@ -1370,19 +1397,141 @@ const WorkGroupSection = memo(function WorkGroupSection({
1469+ onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)}
1470+ >
1471+ <LiveActivityRow
1472+ label={liveWorkEntryLabel(row.entry, ctx.workspaceRoot)}
MmacroscopeappBoton line 1472Aug 16, 2026

🟡 Medium chat/MessagesTimeline.tsx:1472

A completed successful work-live entry is displayed as if it is still running: liveWorkEntryLabel formats its command as Running <program>. When the trailing active batch contains only completed entries, this leaves the timeline showing a stale running status and hides the separate Thinking indicator until later commentary arrives. Use a completion-aware label/rendering path for completed entries so the row no longer reports an active command.

No longer relevant as of 4816e2c5c929842a3f13ba60496401d64b476023

Synced from GitHub

UI consistency review found 3 issues in the timeline row restyle and the changed-files card. Details inline.

Posted via Macroscope — UI Consistency

Synced from GitHub

One finding: removing the per-row success/neutral indicators leaves activeTurnInProgress in the timeline activity context with no reader, so the context still invalidates its remaining consumers on every transition without any visual effect. Details inline.

Posted via Macroscope — UI Consistency

Synced from GitHub

One finding on the expanded tool-call body; everything else in the tool-activity rework (container-query subagent card, inset focus rings, live/thinking rows, reserved icon slot) looks consistent with the shared system.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.logic.tsline 314Outdated · 6531753Open thread
……@@ -242,6 +262,90 @@ export function normalizeCompactToolLabel(value: string): string {
311+ case "other":
312+ return `Used ${count} ${count === 1 ? "tool" : "tools"}`;
313+ }
314+}
CcursorBoton line 314Aug 16, 2026

Grep summarized as web search

Medium Severity

toolGroupAction treats every itemType === "web_search" entry as a web search, so completed groups get labels like Searched the web. In this codebase, codebase grep activities also use that itemType, so the new summary copy mislabels local search as web search.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 653175303ca37c3814a5c0719ae02e231aa87347. Configure here.

Synced from GitHub

UI consistency review: one new finding on the live activity row's leading icon slot. One previously reported issue is still open (see summary).

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 16, 2026
Synced from GitHub

One consistency finding on the new live-activity row; everything previously flagged on this branch (dead --animate-live-activity-focus tokens, @theme-scoped keyframes, inset-0 vs the CSS utility's own left/width, viewport breakpoints on the subagent card, colour-only failure state, missing hover affordance on expandable work rows, the collapsed icon slot, select-all on the expanded body, and the now-unused summarizeChangedFileScopes) reads as resolved at this head.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1441–1453Outdated · 6531753Open thread
……@@ -1370,19 +1397,141 @@ const WorkGroupSection = memo(function WorkGroupSection({
1450+ highlighted ? "text-foreground" : "text-icon-muted",
1451+ )}
1452+ >
1453+ <WorkEntryIconSvg name={iconName} className="block size-4 shrink-0 stroke-[1.8]" />
MmacroscopeappBoton lines 1441–1453Aug 16, 2026

The non-highlighted copy of this row is meant to read like the static activity rows it stacks with, but it differs from them in two ways:

  • The icon drops opacity-70. WorkGroupToggleTimelineRow's summary icon (line 1527) and PlainWorkEntryRow's icon (line 2602) both render text-icon-muted + opacity-70, so the live row shows the same icon set at a visibly darker tone than the rows directly above and below it, even though its label uses the identical text-secondary-label.
  • The content inset is px-1 rather than the px-0.5 those rows use, so the live row's icon/label column sits 2px right of the tool-group summary row and of its own expanded children (which are rendered through PlainWorkEntryRow), giving one disclosure group a ragged left edge.

Suggest restoring the muted icon tone here, and using px-0.5 when an icon is present while keeping px-1 for the icon-less Thinking row so it stays aligned with the "Working for" label above it.

suggestion
1 <WorkEntryIconSvg
2 name={iconName}
3 className={cn("block size-4 shrink-0 stroke-[1.8]", !highlighted && "opacity-70")}
4 />

Posted via Macroscope — UI Consistency

Synced from GitHub

Two consistency findings in the new live-activity/subagent rows. The alignment/tone note on LiveActivityContent from the previous run still applies and is not repeated here.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1515–1542Outdated · b6fe8b0Open thread
MmacroscopeappBoton lines 1515–1542Aug 16, 2026

This PR moves the activity column to a single scale — the new summary branch here uses a size-6 icon slot with a size-4 glyph and text-sm leading-relaxed, PlainWorkEntryRow moved from size-5/size-3.5/text-[12px] to the same size-6/size-4/text-sm (lines 2557, 2612, 2620), and TurnFoldTimelineRow moved to text-sm leading-relaxed too. The count branch of this same component (lines 1550-1562) keeps the old metrics, so the +N previous log entries toggle now sits 0.25rem left of the rows it reveals and renders at 12px next to their 14px — a ragged left edge inside one group, where the two used to align.

Suggest bringing the count branch to the same slot and type scale:

diff
1 <button
2 type="button"
3- className="flex w-full cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-[12px] leading-5 transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
4+ className="flex min-h-6 w-full cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-sm leading-relaxed transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
5 aria-expanded={row.expanded}
6 onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)}
7 >
8- <span className="flex size-5 shrink-0 items-center justify-center text-icon-muted">
9+ <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted">
10 <ChevronDownIcon
11 className={cn(
12- "size-3.5 shrink-0 opacity-70 transition-transform duration-200",
13+ "size-4 shrink-0 opacity-70 transition-transform duration-200",
14 row.expanded && "rotate-180",
15 )}
16 />
17 </span>

Posted via Macroscope — UI Consistency

Synced from GitHub

One new UI-consistency finding on the rewritten working row: it swaps the activity column's secondary-label token for muted-foreground, which diverges under imported/custom app themes.

Previously reported items that are still open in this revision are not re-posted (live activity icon tone vs. the static rows, and the now-unread activeTurnInProgress field on TimelineRowActivityCtx).

Posted via Macroscope — UI Consistency

Synced from GitHub

Two consistency findings in the new activity column. Everything else I re-checked from earlier rounds (inset focus rings on the changed-files preview, the reserved icon slot for icon-less activity lines, the tooltip on the agent-card icon button, the sr-only status fallback, select-none on the decorative sweep copy, top-level @keyframes) looks resolved.

Posted via Macroscope — UI Consistency

Synced from GitHub

One new finding on the expanded tool-group rows. The previously reported WorkingTimelineRow token mismatch (text-muted-foreground vs the text-secondary-label used by every activity line it introduces) is unchanged and is not re-reported here.

Posted via Macroscope — UI Consistency

Synced from GitHub

One new consistency finding in the live activity row's failure state; see the inline comment. Previously reported items on this branch (working-header spacing/token, per-entry section landmarks for expanded groups, Read File icon mismatch) are unchanged and not re-posted.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1467–1468Outdated · caca860Open thread
……@@ -1370,19 +1395,167 @@ const WorkGroupSection = memo(function WorkGroupSection({
1465+ failed ? "text-destructive" : highlighted ? "text-foreground" : "text-icon-muted",
1466+ )}
1467+ role={announceFailure ? "img" : undefined}
1468+ aria-label={announceFailure ? "Tool call failed" : undefined}
MmacroscopeappBoton lines 1467–1468Aug 16, 2026

chat/MessagesTimeline.tsx:1468

announceFailure is passed unconditionally by LiveActivityRow (line 1417), and it is not gated on failed here, so the icon slot gets role="img" aria-label="Tool call failed" on every live tool row — including healthy inProgress ones. LiveWorkEntryTimelineRow only sets its own aria-label when the entry failed, so a running row's accessible name is computed from its contents and comes out as "Tool call failed Running psql". Suggest gating the announcement on the failure state so it only fires for the x icon.

suggestion
1 role={announceFailure && failed ? "img" : undefined}
2 aria-label={announceFailure && failed ? "Tool call failed" : undefined}

No longer relevant as of b06fab9f30e3227956de80097ee960bd400e6f1f

MmacroscopeappBotreviewed · 1 inline threadAug 16, 2026
Synced from GitHub

Reviewed the timeline/activity restyle for shared-primitive and Tailwind ownership consistency. Two consistency findings in MessagesTimeline.tsx; the rest of the change (container-query subagent card, Button/Tooltip reuse, inset focus rings, mask/keyframes kept in index.css) looks consistent with the surrounding system.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1116–1116Outdated · c2f4f35Open thread
……@@ -1093,7 +1113,7 @@ function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn-
10941114 data-scroll-anchor-ignore
10951115 onClick={() => ctx.onToggleTurnFold(row.turnId)}
1096- className="flex cursor-pointer select-none items-center gap-1 rounded-md px-1 text-xs text-muted-foreground tabular-nums transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
1116+ className="flex cursor-pointer select-none items-center gap-1 rounded-md px-1 text-sm leading-relaxed text-muted-foreground tabular-nums transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
MmacroscopeappBoton lines 1116–1116Aug 16, 2026

WorkingTimelineRow now renders the same shell as this fold header (border-b border-border/60 pb-2 pt-1 wrapper, px-1 text-sm leading-relaxed … tabular-nums label), so "Working for 12s" and "Worked for 12s" are visual twins that swap in place when a turn settles — but they resolve different tokens: this row stays text-muted-foreground while the working header (line 1306) and every activity line this fold reveals (LiveActivityContent line 1465, the tool-group summary line 1562, PlainWorkEntryRow line 2605) use text-secondary-label. --secondary-label only aliases --muted-foreground in the default palette; imported themes derive it from a different palette entry (themePalette.ts line 1185 textMuted vs line 1183 mutedForeground), so under a custom theme the header visibly changes color the moment the turn completes. Suggest using the same token for both headers.

suggestion
1 className="flex cursor-pointer select-none items-center gap-1 rounded-md px-1 text-sm leading-relaxed text-secondary-label tabular-nums transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — 2 findings in the changed web UI.

  • apps/web/src/components/ThreadTerminalDrawer.tsx: the new terminal tab row re-creates RightPanelTabs' tab treatment inline and has already drifted from it.
  • apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx: the compacted approval detail can no longer be read in full (no wrap, no keyboard scroll) and its label is not exposed.

Everything previously flagged on this branch (live-row failure announcement, activeTurnInProgress/workingStepLabel dead context fields, summarizeChangedFileScopes, the inert --animate-live-activity-focus tokens, the inset-0 vs live-activity-focus collision, ring clipping in the changed-files preview) is resolved at this head.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — inline comments could not be attached to the diff, so the two findings are below.

1. apps/web/src/components/ThreadTerminalDrawer.tsx (terminal tab row, ~lines 1688-1706)

This tab row is a verbatim re-creation of the panel tab strip in RightPanelTabs.tsx (lines 630-665): same group/tab flex h-6 items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs shell, same active/hover treatment, and the same group/close leading slot that swaps the identity glyph for XIcon on group-hover/tab / group-focus-visible/close. Two call sites now own the same durable treatment, and they have already diverged:

  • hover tint: hover:bg-accent/50 here vs hover:bg-accent/60 there;
  • coarse pointers: only this copy pins the close glyph visible (pointer-coarse:hidden / pointer-coarse:block), so touch users can close a terminal tab but not a panel tab;
  • the wrapper drops cursor-pointer (kept there), so the pointer only changes over the inner label button.

Suggest lifting the shell plus close slot into one named tab row (leading slot, label, close handler) and keeping width/context classes at each call site, so the two strips cannot drift further. If a shared component is out of scope for this PR, at least align the hover token and the coarse-pointer close visibility between the two.

2. apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx (lines 31-37)

This box is now the only rendering of what is being approved, and the compaction removes two ways of reading it. whitespace-pre replaces the previous whitespace-pre-wrap [overflow-wrap:anywhere], so a long single-line command (the common case) is clipped and can only be read by scrolling this element — and the element is not focusable, so keyboard users cannot scroll it at all. The request kind (Command / File to read / File change) also survives only in aria-label, which is ignored here: <code> maps to role code, where naming is prohibited, so assistive tech gets neither the label nor a way to reach the text.

Suggest giving the scroll region a role that supports its name plus a tab stop, so the whole command stays reachable while the drawer keeps its one-line height:

diff
1 <code
2+ role="group"
3+ tabIndex={0}
4 aria-label={detailAriaLabel}
5- className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85"
6+ className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
7 data-approval-detail="complete"
8 >

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 2508–2532Outdated · 3a54895Open thread
MmacroscopeappBoton lines 2508–2532Aug 17, 2026

chat/MessagesTimeline.tsx:2508

This card is now a verbatim re-creation of ChangedFilesCard (ChangedFilesTree.tsx lines 54-141): identical shell rounded-xl border border-border/70 bg-secondary p-1.5 dark:border-transparent dark:bg-input/32, identical flex items-center justify-between gap-2 px-1 header, and the same Button size="xs" variant="outline" + Tooltip + @[24rem]/<container>:inline label action. Two call sites now own the same durable, theme-sensitive treatment — including the dark border/background pair — so a change to one silently diverges from the other, which is exactly how the padding/radius already drifted between these cards before this PR realigned them. Suggest extracting the shell plus the responsive icon+label action into a small shared timeline-card primitive, leaving only the container name, contents, and -mx-1/mt-4 positioning at each call site.

No longer relevant as of 4816e2c5c929842a3f13ba60496401d64b476023

apps/web/src/components/chat/ChangedFilesTree.tsxlines 266–266Outdated · 3a54895Open thread
……@@ -277,11 +259,11 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
282264 </span>
283265 {hasNonZeroStat(node.stat) && (
284- <span className="ml-auto shrink-0 font-mono text-[10px] tabular-nums">
266+ <span className="ml-1 shrink-0 font-mono text-[10px] tabular-nums">
MmacroscopeappBoton lines 266–266Aug 17, 2026

Switching ml-auto → ml-1 here (and at line 301) removes the shared right edge these stats were aligned against, but they still render with DiffStatLabel's default layout="aligned", whose only purpose is that alignment: inline-grid grid-cols-[4ch_4ch] gap-2 text-right. Now that each block starts immediately after a variable-length filename, the fixed 4ch columns can never line up across rows — they only add ragged internal whitespace between the name, +N and -N. The new preview row in the same card renders the identical datum in the same hugging position with layout="inline" (line 179), so the two halves of the card now disagree. Suggest using the inline layout in both tree rows (or restoring ml-auto if the alignment column was meant to stay).

diff
1 <span className="ml-1 shrink-0 font-mono text-[10px] tabular-nums">
2- <DiffStatLabel additions={node.stat.additions} deletions={node.stat.deletions} />
3+ <DiffStatLabel
4+ additions={node.stat.additions}
5+ deletions={node.stat.deletions}
6+ layout="inline"
7+ />

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.logic.tsline 358Outdated · aa78a62Open thread
……@@ -242,6 +262,106 @@ export function normalizeCompactToolLabel(value: string): string {
355+}
356+
357+function workGroupIdentity(timelineEntryId: string, entry: WorkLogEntry): string {
358+ return entry.toolCallId ? `tool:${entry.toolCallId}` : timelineEntryId;
MmacroscopeappBoton line 358Aug 17, 2026

🟡 Medium chat/MessagesTimeline.logic.ts:358

The live group's identity changes when its first tool call becomes neutral, so an expanded group loses its expandedWorkGroupIds match and remounts as the next call's group. activeWorkAnchor is selected from visibleActiveToolEntries, causing this function to receive a different toolCallId; derive the group key from a stable underlying entry (for example, the first activeToolEntries item) while keeping the latest visible entry for display.

✅ Resolved in 50b35d866d9d43c5cf2cfa3dbaa4907c8e0716d9

apps/web/src/components/chat/MessagesTimeline.tsxline 1505Outdated · aa78a62Open thread
……@@ -1370,19 +1397,172 @@ const WorkGroupSection = memo(function WorkGroupSection({
1502+ >
1503+ <LiveActivityRow label={label} iconName={workEntryIconName(row.entry)} failed={failed} />
1504+ </button>
1505+ );
CcursorBoton line 1505Aug 17, 2026

Live row ignores earlier failures

Medium Severity

LiveWorkEntryTimelineRow only runs workEntryIndicatesToolFailure on the latest live entry. Completed tool groups set hasFailure from every grouped entry. If an earlier call failed and a newer parallel call succeeded, the live pill drops the failure affordance even though the expanded batch still contains the failed call.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit aa78a62bed257493ded9c337ca485de30fdfceee. Configure here.

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review: one new finding on the tool-icon classification split. Three findings from earlier runs on this branch are still open and were not re-posted (ChangedFilesTree.tsx diff stats using the aligned grid after ml-auto → ml-1; AgentSpawnCtaRow re-creating the ChangedFilesCard shell; the turn-fold / working header token mismatch, and the full-opacity failure XIcon in the tool-group summary).

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1522–1523Outdated · b06fab9Open thread
……@@ -1370,19 +1397,171 @@ const WorkGroupSection = memo(function WorkGroupSection({
1520+ return "search";
1521+ case "other":
1522+ return "wrench";
1523+ case "mixed":
MmacroscopeappBoton lines 1522–1523Aug 17, 2026

chat/MessagesTimeline.tsx:1523

The two icon classifiers still disagree for the non-read cases. toolGroupAction funnels dynamic_tool_call, collab_agent_tool_call and tone-only tool entries into other, so the summary row draws wrench, while workEntryIconName gives those same entries hammer (line 2400) and bot (line 2402). One dynamic tool call therefore renders a hammer in the work-live row, switches to a wrench once it settles into Used 1 tool, and switches back to a hammer on the expanded child row — the icon changes twice for a single unchanged call.

The Read File case was fixed by teaching workEntryIconName about it; suggest closing the remaining gap the same way, by deriving this summary glyph from the group's per-entry workEntryIconName when the entries agree (which needs the entries, or their resolved icon name, carried on the work-toggle row) and keeping the action mapping only as the mixed-group fallback. Mapping other straight to hammer is not enough on its own, since it would then disagree with mcp_tool_call rows.

No longer relevant as of 9ae5f817411576b476a9e57b6680a2dbe9e3ca02

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review: one new finding on the changed-files header disclosure. Previously reported findings that are still present in this revision (preview chip squeezing, ml-1 vs DiffStatLabel layout="aligned", the ChangedFilesCard/subagent-card shell duplication, the wrench/hammer classifier split, and the full-opacity failure glyph on the group summary row) are not re-posted here.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/ChangedFilesTree.tsxfileOutdated · d434080Open thread

This header toggle loses flex-1 (and, just below, the Show files / Hide files hint), so the disclosure now sizes to chevron + label while the header row still spans the full card width under justify-between. The gap between the label and the Open diff / expand-all actions used to be part of the toggle and is now dead space, and with px-1 py-1 on text-xs leading-4 the remaining target is ~24px tall — below a comfortable coarse-pointer hit target for the card's primary control, whose only affordance left is the chevron. Suggest keeping the button flexible so the whole header row still toggles (the ChangedFilesTree.test.tsx class assertion needs the same update).

suggestion
1 className="flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-1 py-1 text-left transition-colors hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review: three findings, all in apps/web/src. Two are consistency divergences between controls this PR makes twins of; one is a helper left without a production consumer.

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review: one finding — the new failure affordance is derived two different ways for the same tool call, so the red X flips on when a live row settles into its collapsed summary.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1482–1485Outdated · d4e2053Open thread
……@@ -1370,19 +1391,174 @@ const WorkGroupSection = memo(function WorkGroupSection({
1482+ const failed = row.groupedEntries.some((entry) => {
1483+ const { command: _command, ...entryWithoutCommand } = entry;
1484+ return workEntryIndicatesToolFailure(entryWithoutCommand);
1485+ });
MmacroscopeappBoton lines 1482–1485Aug 17, 2026

chat/MessagesTimeline.tsx:1485

The live row strips command before asking workEntryIndicatesToolFailure, but the settled paths introduced alongside it do not: hasFailure for the tool-group summary (MessagesTimeline.logic.ts line 830) and showFailedIndicator on the expanded child row (line 2605) both run the heuristic over detail + command. Because the exception is keyed on which row renders the entry rather than on the entry's state, a completed call that this PR keeps in the live slot (see the "keeps a completed trailing call in the live slot" case) shows no failure while the turn runs, then gains a destructive X plus Tool call failed — and taints the whole group summary — the moment the turn settles, with nothing about the entry having changed. The PR's own test (rg 'command not found' src) is exactly such a command.

Suggest gating the exception on the lifecycle state it was written for, so an entry keeps one failure affordance in both rows:

suggestion
1 const failed = row.groupedEntries.some((entry) => {
2 if (entry.toolLifecycleStatus !== "inProgress") return workEntryIndicatesToolFailure(entry);
3 const { command: _command, ...entryWithoutCommand } = entry;
4 return workEntryIndicatesToolFailure(entryWithoutCommand);
5 });

No longer relevant as of dea09d603789154877b669854f6f10798511d657

Synced from GitHub

UI consistency review: one finding — failure detection for the same tool entry differs between the new live row and the collapsed/expanded rows it settles into.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.logic.tsline 832Outdated · d4e2053Open thread
……@@ -523,15 +812,48 @@ export function deriveMessagesTimelineRows(input: {
829+ onlyToolEntries: true,
830+ summary: summarizeToolGroup(visibleGroupedEntries),
831+ summaryKind,
832+ hasFailure: visibleGroupedEntries.some((entry) => workEntryIndicatesToolFailure(entry)),
CcursorBoton line 832Aug 17, 2026

Summary failure ignores command strip

Low Severity

Collapsed tool-group hasFailure calls workEntryIndicatesToolFailure on the full entry, including command. The live row strips command before the same check so search invocations like rg 'command not found' do not look failed. Settled summaries can still show failure chrome from benign command text.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit d4e205311888b825f7796b07c672a484cda751b5. Configure here.

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review: one new finding on the subagent card's surface geometry. Two findings from earlier runs on this PR are still present in the head and are not re-posted (the work-toggle summary failure XIcon rendering at full opacity while the live/child failure icons use opacity-70, and hasFailure in MessagesTimeline.logic.ts running workEntryIndicatesToolFailure over command while LiveWorkEntryTimelineRow deliberately strips it).

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 2532–2532Outdated · 266e267Open thread
……@@ -2189,71 +2526,94 @@ const AgentSpawnCtaRow = memo(function AgentSpawnCtaRow(props: { workEntry: Time
2211- <span className="text-info-foreground">{live ? "Open Agents ▸" : "View ▸"}</span>
2212- </span>
2213- </button>
2532+ <div className="@container/agent-group -mx-1 rounded-xl border border-border/70 bg-secondary p-1.5 dark:border-transparent dark:bg-input/32">
MmacroscopeappBoton lines 2532–2532Aug 17, 2026

chat/MessagesTimeline.tsx:2532

This card re-creates the timeline's card surface (border border-border/70 bg-secondary dark:border-transparent dark:bg-input/32) but with different geometry than the only other card in the chat column, ChangedFilesCard (ChangedFilesTree.tsx:59): rounded-xl + p-1.5 here vs rounded-2xl + p-2 there. Both can render under the same assistant turn, so the same surface treatment shows two different corner radii and inset paddings. Suggest aligning this card with the established geometry (or extracting the shared surface so the two call sites cannot drift again).

suggestion
1 <div className="@container/agent-group -mx-1 rounded-2xl border border-border/70 bg-secondary p-2 dark:border-transparent dark:bg-input/32">

No longer relevant as of f3e27d751bdb3fe2798e0037fbd65ccd1e8f05e1

apps/web/src/session-logic.tsline 1080Outdated · 266e267Open thread
……@@ -993,12 +1019,65 @@ function collapseDerivedWorkLogEntries(
10011077 collapsed.push(entry);
1078+ if (lifecycleKey !== undefined) {
1079+ toolLifecycleRowIndex.set(lifecycleKey, collapsed.length - 1);
1080+ }
CcursorBoton line 1080Aug 17, 2026

Duplicate rows after tool completion

Medium Severity

The new toolLifecycleRowIndex drops a call’s key once the merged row is tool.completed, and when a later same-key event cannot merge it deletes the index and still appends. A delayed tool.updated (or similar) for that toolCallId then becomes a second work-log row instead of folding into the completed call.

Reviewed by Cursor Bugbot for commit 266e26758498b080ad8a3966270993f6fc6a2496. Configure here.

Synced from GitHub

UI consistency review: one finding on the new tool-group summary row — error-tone work rows are swept into it and lose their destructive presentation.

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review: one finding on the subagent card's status presentation. Everything else previously raised on this branch (icon slot metrics, failure-glyph tone, live-row aria gating, card geometry/tooltip, expanded-group landmarks, row padding) reads as resolved at this head.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 2528–2544Outdated · bb47d9dOpen thread
……@@ -2189,71 +2519,94 @@ const AgentSpawnCtaRow = memo(function AgentSpawnCtaRow(props: { workEntry: Time
2541+ Σ {formatSubagentTokenCount(totalTokens)}
2542+ </span>
2543+ ) : null}
2544+ </span>
MmacroscopeappBoton lines 2528–2544Aug 17, 2026

chat/MessagesTimeline.tsx:2544

The status text is now gated on @[32rem]/agent-group, with only an sr-only copy below that width. The container is the card itself, so in a narrow chat column (split view, mobile web) sighted users lose 2 working / 1 failed / Completed entirely, and the sole remaining differentiator between a failed and a completed fleet is the size-1.5 dot — which is aria-hidden and carries no tooltip or label. Before this change the status was always rendered, so this is a color-only state at narrow widths.

Suggest keeping the status visible at all widths and gating only the token total, which is the part that needs the room:

suggestion
1 <span aria-hidden className={cn("size-1.5 shrink-0 rounded-full", dotClass)} />
2 <WorkEntryIconSvg name="bot" className="size-3.5 shrink-0 text-muted-foreground" />
3 <span className="min-w-0 truncate font-medium text-foreground">{lead}</span>
4 {workflowName ? (
5 <span className="hidden min-w-0 truncate text-[11px] text-muted-foreground @[40rem]/agent-group:inline">
6 {workflowName}
7 </span>
8 ) : null}
9 <span className="ml-auto flex shrink-0 items-center gap-2 text-[11px] text-muted-foreground">
10 <span>{status}</span>
11 {totalTokens > 0 ? (
12 <span className="hidden font-mono tabular-nums @[32rem]/agent-group:inline">
13 Σ {formatSubagentTokenCount(totalTokens)}
14 </span>
15 ) : null}
16 </span>

No longer relevant as of b99f41078abb2a18b42980fb85e85800b547e82b

apps/web/src/components/chat/MessagesTimeline.tsxline 2300Outdated · bb47d9dOpen thread
……@@ -2035,6 +2207,161 @@ function workEntryRawCommand(
2297+ if (wrapper !== "sudo" && /^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {
2298+ index += 1;
2299+ continue;
2300+ }
CcursorBoton line 2300Aug 17, 2026

Sudo env assignment mislabeled

Low Severity

commandProgramName skips VAR=value tokens for bare commands and env, but not after sudo. For sudo FOO=bar npm test, the live label becomes Running FOO=bar instead of Running npm, so the activity pill shows the environment assignment rather than the program being run.

Reviewed by Cursor Bugbot for commit bb47d9dea0c1715239b6290110709de5c82d0679. Configure here.

Synced from GitHub

UI consistency review: four findings, all in apps/web/src. The largest is the pull request detail panel's new early ghost return, which unmounts the panel chrome (including its only close control) while the detail request is pending.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — inline comments could not be attached to the diff, so the four findings are below.


1. apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx (lines 1097-1099)

tsx
1if (detailQuery.isPending && !detail) {
2 return <PullRequestDetailGhost />;
3}

This early return replaces the whole panel while detailQuery is pending, so the chrome goes with it. The Collapse pull request panel button (line 1467) and the tab bar are unmounted, which leaves a right-panel reader (context="thread" passes onClose) with no in-panel way out until the request resolves; previously the chrome rendered with detail === null and only the content region showed a ghost.

The same move also drops the per-tab ghost shape that the deleted comment described — PullRequestTimelineGhost and DiffPanelLoadingState are still imported and used further down (lines 1882, 1902), but the initial load now always shows the summary-shaped ghost, so switching to Timeline or Code mid-load flashes a summary outline.

Suggest keeping the ghost inside the content region (where detailQuery.error is already handled) and selecting it by tab, so the header keeps its close control in every loading state.


2. apps/web/src/hooks/useResizableWidth.ts (line 56)

Swapping the manual getLocalStorageItem/setLocalStorageItem pair for useLocalStorage also subscribes to the cross-window storage event (useLocalStorage.ts lines 116-120), which the comment removed in this diff ruled out on purpose: "No cross-tab subscription: panel width is per-window state."

PreviewPanelShell (line 72) shares this hook, so committing a drag in one Electron window or browser tab now snaps the right panel in every other open window — a behavior change to an existing consumer that the terminal sidebar did not need.

If the goal is only to keep the drawer-mode and panel-mode terminal sidebars (both on t3code:terminal-sidebar-width) in step within one window, suggest syncing on the in-page t3code:local_storage_change event only and leaving the storage listener out, so per-window width is preserved.


3. apps/web/src/components/chat/PanelLayoutControls.tsx (line 65)

The <span> wrapper is what makes the unavailable right-panel tooltip hoverable, but the terminal toggle directly above (lines 42-56) still uses render={<Toggle disabled={!terminalAvailable} />}. toggleVariants carries disabled:pointer-events-none (ui/toggle.tsx line 9), so its Terminal drawer is unavailable tooltip can never be reached by pointer — exactly the case this change repairs.

Two adjacent controls with the same "toggle that can be unavailable and must explain why" contract now compose two different ways. Suggest giving the terminal toggle the same wrapper, or extracting one small local helper both use, so the availability tooltip behaves the same for either panel.


4. apps/web/src/components/chat/ThreadSyncStatusPill.tsx (line 11)

.chat-composer-drawer-slot owns the attached drawer's width inset and centering (index.css line 833), but the cap that only makes sense together with it — max-w-[calc(48rem-2.75rem)] — is hardcoded here and again in ComposerBannerStack.tsx line 103, as is pb-[calc(var(--chat-composer-attachment-overlap)_+_0.375rem)] (also ComposerBannerStack.tsx line 205).

That 48rem is the composer form's max-w-3xl, so the shared geometry has three owners: the utility, and each consumer's copy of the cap. Changing the composer width would silently desync these drawers from the surface they attach to.

Suggest folding the cap into .chat-composer-drawer-slot and the attachment padding into .chat-composer-drawer-attached, leaving only contextual classes at the call sites.

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review of the changed timeline rows. Previously flagged items (agent-card geometry/status visibility, live-row failure announcement, expanded-group landmarks, working-row spacing/token, count-toggle metrics, duplicate selectable shimmer label, icon-only agent action) are resolved at this head. One introduced misalignment remains, noted inline.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1455–1455Outdated · b99f410Open thread
……@@ -1370,19 +1391,167 @@ const WorkGroupSection = memo(function WorkGroupSection({
1452+ return (
1453+ <div
1454+ className={cn(
1455+ "flex min-h-6 min-w-0 items-center gap-1.5 px-1 py-0.5",
MmacroscopeappBoton lines 1455–1455Aug 17, 2026

chat/MessagesTimeline.tsx:1455

The new live rows inset at px-1, but every settled row in the same activity column insets at px-0.5: the tool-group summary button (line 1533) and PlainWorkEntryRow (line 2646, whose WorkGroupSection wrapper is -mx-1 px-1, i.e. net zero). So the running tool's icon and label sit 2px right of the identical call once it settles into Used 1 tool, and expanding the live row indents its own children 2px left of the header that discloses them — the column this PR unifies to size-6/size-4/text-sm ends up with two x-origins.

Suggest matching the column's px-0.5 here; the shimmer geometry comes from the live-activity-focus* utilities, so it is unaffected. The gap-1.5 px-1 py-0.5 assertion in MessagesTimeline.test.tsx needs the same update.

suggestion
1 "flex min-h-6 min-w-0 items-center gap-1.5 px-0.5 py-0.5",

No longer relevant as of 4816e2c5c929842a3f13ba60496401d64b476023

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review: one finding on the activity column's type scale and row target for expanded tool-group children.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 2667–2670Outdated · 11bd37fOpen thread
……@@ -2289,84 +2643,35 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
2667+ <p
2668+ className={cn(
2669+ "flex min-w-0 w-full items-baseline gap-1.5 leading-relaxed",
2670+ isExpandedToolGroupEntry ? "text-[12px] leading-5" : "text-sm",
MmacroscopeappBoton lines 2667–2670Aug 17, 2026

chat/MessagesTimeline.tsx:2670

Expanded tool-group children are now the only rows left on the old 12px scale. This PR moves the whole activity column to text-sm leading-relaxed with a min-h-6 row box — the turn fold (line 1110), the working header (line 1300), the live row (line 1412), and both work-toggle branches (lines 1533, 1566) — while the row revealed under those headers keeps text-[12px] leading-5 and, combined with py-0 on line 2647, has no min-h-6, so it renders ~20px tall even though it is still role="button" when the call has output.

The live row makes the mismatch most visible: appendActiveWorkRows renders its grouped entries through this same path, so the entry shown as Running psql at 14px in the header reappears at 12px in the child row directly below it, in the same size-6/size-4 icon column and at the same indent.

Suggest giving the expanded child the column's scale and minimum height (keeping only the tighter vertical padding for density):

suggestion
1 "text-sm",

No longer relevant as of 2fe727dca96cf12d501bdd9d2e9170c438c170db

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review of the timeline activity column.

One new finding, posted inline. Several findings from earlier runs on this branch are still present at this head and are not re-commented (see the check details): expanded tool-group children still render at text-[12px] leading-5 / py-0 while every header in the column moved to text-sm leading-relaxed + min-h-6; the live row still insets at px-1 against px-0.5 for the settled rows it turns into; toolGroupSummaryIconName still maps tone-only tool entries to wrench where workEntryIconName gives zap/bot; and the working / turn-fold headers still resolve text-muted-foreground while every activity line they introduce resolves text-secondary-label.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.logic.tslines 898–899Outdated · a8275ddOpen thread
……@@ -566,8 +892,11 @@ export function deriveMessagesTimelineRows(input: {
896+ summary: null,
897+ summaryKind: null,
898+ hasFailure: visibleGroupedEntries.some((entry) =>
899+ workEntryDisplayIndicatesToolFailure(entry),
MmacroscopeappBoton lines 898–899Aug 17, 2026

hasFailure is only ever rendered by the summary branch of WorkGroupToggleTimelineRow (destructive icon plus the …, tool call failed accessible name, MessagesTimeline.tsx lines 1538-1550). This overflow branch produces the chevron variant (summary: null), which ignores the field entirely, so two collapsed disclosures of the same batch disagree: an all-tool group that hides a failed call announces the failure, while a mixed group that hides the same call stays silent.

The value is also computed over visibleGroupedEntries rather than hiddenEntries, so it would attribute a still-visible failure (which already renders its own X on its own row) to the collapsed set. Suggest scoping it to hiddenEntries and rendering the same failure affordance in the chevron branch, or dropping the field for this variant so the row type only carries state its renderer uses.

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review: one finding — the active-turn live tool batch does not apply the error-row exclusion that the settled grouping path applies, so error/denial rows lose their dedicated row treatment while a turn is running.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.logic.tslines 711–717Outdated · 9ae5f81Open thread
……@@ -471,38 +657,141 @@ export function deriveMessagesTimelineRows(input: {
714+ workLogEntryIsToolLike(entry.entry)
715+ ? [entry]
716+ : [],
717+ );
MmacroscopeappBoton lines 711–717Aug 17, 2026

chat/MessagesTimeline.logic.ts:717

The settled grouping path deliberately keeps tone === "error" entries out of tool-group summarization (line 816-821, covered by the new "keeps error entries visible instead of summarizing them as tools" test), but this active-turn collector only excludes agentSpawn. workLogEntryIsToolLike returns true for tone error (session-logic.ts:168), and runtime.error / tool.denied activities are emitted with the turn's turnId (ProviderRuntimeIngestion.ts:433-439), so while the turn is running such an entry is added to activeWorkEntryIds and skipped as its own row (line 791-793).

Result: a provider error or denied tool call is visible only if it happens to be the tail of the live batch — any later tool call hides it behind the collapsed live row until the turn settles, at which point it pops back out as a standalone destructive row. Even as the tail it renders through LiveActivityContent, which has no equivalent of PlainWorkEntryRow's destructive label treatment. Suggest applying the same exclusion here so error rows stay outside the live batch in both states.

suggestion
1 const activeToolEntries = activeEntries.flatMap((entry) =>
2 entry.kind === "work" &&
3 entry.entry.agentSpawn === undefined &&
4 entry.entry.tone !== "error" &&
5 workLogEntryIsToolLike(entry.entry)
6 ? [entry]
7 : [],
8 );

No longer relevant as of 4dcdd9652028633c90a77a8adc340a8943583457

Synced from GitHub

UI consistency review of the live tool-activity column: two findings, both on rows introduced by this PR.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review of the timeline activity column. Most of the earlier findings are resolved at this head (icon classifiers now agree across live/summary/expanded rows, the failure heuristic is display-only in every renderer, the expanded children moved to the text-sm scale, the working header matches the turn fold, the agent card matches the changed-files card shell, and announceFailure is gated). One remaining interaction gap is inline below.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — two shared-treatment findings in the composer drawer work. Both are ownership/consistency suggestions rather than functional breaks.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — inline comments could not be attached to the diff, so the two findings are below.

1. apps/web/src/components/chat/ComposerPendingApprovalActions.tsx (line 14 and the four size="micro" buttons)

size="micro" is h-5 at every width (ui/button.tsx lines 32-33), and these are the same buttons the collapsed-mobile approval branch renders (ChatComposer.tsx line 2766), so on touch the approve/decline decision controls drop from the previous size="sm" (h-8 below sm) to a 20px-tall target. Suggest keeping the compact desktop scale but restoring a coarse-pointer height below sm. sm:text-[11px] is also a no-op — micro carries a single unprefixed text-xs, so the base override already wins — and the same text-[11px] sm:text-[11px] override is repeated at ChatView.tsx line 4458; if 11px is the drawer's action scale it is worth owning once rather than in two call sites.

diff
1-const APPROVAL_ACTION_CLASS_NAME = "font-normal text-[11px] sm:text-[11px]";
2+const APPROVAL_ACTION_CLASS_NAME = "font-normal text-[11px] max-sm:h-7 max-sm:px-2";

2. apps/web/src/components/chat/ComposerCommandMenu.tsx (line 134)

This row and the stash drawer row (ComposerStashMenu.tsx line 128) now repeat the same treatment — cursor-pointer select-none gap-3 rounded-lg px-3 … hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit — but diverge on the one value that sets row height: py-2! here vs py-1.5! there. Both drawers open in the same anchored slot from the same composer, so switching between / results and the stash shows two densities for the same list row. Matching the stash value also matches CommandItem's own default (ui/command.tsx line 180), which makes the ! override unnecessary here.

diff
1- "cursor-pointer select-none gap-3 rounded-lg px-3 py-2! hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit",
2+ "cursor-pointer select-none gap-3 rounded-lg px-3 py-1.5 hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit",

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — one finding could not be attached inline, so it is below.

apps/web/src/components/chat/ComposerCommandMenu.tsx (line 134)

tsx
1"cursor-pointer select-none gap-3 rounded-lg px-3 py-2! hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit",

This row and the stash drawer's row (chat/ComposerStashMenu.tsx:110) are the same control opening in the same attached-drawer slot, and the durable treatment is now copied in both places — cursor-pointer select-none gap-3 rounded-lg px-3 py-*!, the neutralized hover/highlight states, and bg-accent! text-accent-foreground! for the active row. The two copies have already diverged: py-2! here vs py-1.5! there, so the command drawer and the stash drawer render at different row heights from the same composer shoulder. py-1.5 is also CommandItem's own default (ui/command.tsx:180), so the stash copy's ! overrides nothing.

Suggest expressing this once — a shared class constant, or a CommandItem variant for the composer drawer row — and keeping only per-list content classes at each call site, so the two drawers cannot drift apart in density.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — inline comments could not be attached to the diff, so the two findings are below.


1. apps/web/src/components/chat/ComposerPendingApprovalActions.tsx (lines 32-35, and 50-53)

tsx
1 variant="ghost-muted"
2 className={`${APPROVAL_ACTION_CLASS_NAME} text-destructive-foreground [:hover,[data-pressed]]:text-destructive-foreground`}

Decline and Approve pick ghost-muted and then override the variant's base and hover text colors at the call site, which is the contract ghost already encodes (border-transparent text-foreground, hover tints only the background). The [:hover,[data-pressed]]:text-destructive-foreground duplicate exists solely to undo ghost-muted's [:hover,[data-pressed]]:text-foreground; with variant="ghost" the destructive tone survives hover without the second declaration:

diff
1 <Button
2 size="micro"
3- variant="ghost-muted"
4- className={`${APPROVAL_ACTION_CLASS_NAME} text-destructive-foreground [:hover,[data-pressed]]:text-destructive-foreground`}
5+ variant="ghost"
6+ className={`${APPROVAL_ACTION_CLASS_NAME} text-destructive-foreground`}

Approve (lines 50-53) is the same case: variant="ghost" replaces ghost-muted + text-foreground exactly, so the row keeps one muted default (Cancel / Always allow this session) and two explicit non-muted actions without fighting the variant.


2. apps/web/src/components/chat/ComposerStashBadge.tsx (line 32)

tsx
1 props.menuOpen && "invisible pointer-events-none",

Making the tab invisible pointer-events-none while menuOpen removes the only way to collapse this disclosure from its own trigger: the stash drawer is a z-[70] portal that covers the tab's shoulder, and visibility: hidden also drops the button from hit testing and tab order, so aria-expanded="true" is reported on a control that can never be activated again.

It also strands the guard added in ComposerStashMenu.tsx (lines 49-51), which exempts [data-prompt-stash-badge="true"] from outside-pointer dismissal precisely so the badge can toggle the drawer closed — that branch is now unreachable.

Suggest either keeping the tab hit-testable above the open drawer so a second click collapses it (matching the aria-expanded contract), or, if the tab is meant to disappear behind the drawer, dropping the dead badge exemption in ComposerStashMenu so both modules describe the same dismissal path.

Posted via Macroscope — UI Consistency

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review — one new finding on the live tool row; see the inline comment. Findings already posted on earlier commits of this branch are not repeated.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1487–1494Outdated · 26af432Open thread
MmacroscopeappBoton lines 1487–1494Aug 17, 2026

chat/MessagesTimeline.tsx:1487

This button and the settled tool-group summary button (line 1535) are the same disclosure — both call ctx.onToggleWorkGroup with the same work-group:tool:<turn>:<callId> id and reveal the same children — but only the settled one gets pointer feedback (transition-colors hover:bg-accent/20, also used by the overflow toggle at line 1569 and by expandable PlainWorkEntryRow at line 2664). The live row declares cursor-pointer and aria-expanded with no hover or press state and no chevron, so while a turn is running the control looks inert, then grows a hover tint the moment the batch settles into Used 1 tool. The new test at MessagesTimeline.test.tsx (expect(markup).not.toContain("hover:bg-accent/20")) pins that divergence, so it would need updating with the fix.

group/live-work also has no group-*/live-work consumer anywhere, so it can go.

suggestion
1 className="flex min-h-6 w-full max-w-full cursor-pointer items-center rounded-md text-left transition-colors hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"

No longer relevant as of 4816e2c5c929842a3f13ba60496401d64b476023

Synced from GitHub

UI consistency review — one new finding on the overflow work-toggle's onlyToolEntries contract. A previously reported item (the disclosure chevron in PlainWorkEntryRow losing its text-icon-muted owner) is still open; not re-posted here.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — the inline comment could not be attached to the diff, so the finding is below.

apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx (lines 30-41)

tsx
1 <div
2 aria-label={detailAriaLabel}
3 className={cn("flex min-w-0 flex-1 items-center gap-2", className)}
4 role="group"
5 >

The removed heading (PENDING APPROVAL / Command approval requested) was the only text stating that the composer is blocked on an approval, and the replacement group is named Command / File to read / File change. fallbackLabel only renders when detail is empty, so in the normal case the word "approval" never appears anywhere in this drawer — the remaining cue is the data-variant="warning" tint on the drawer (ChatComposer.tsx:2728), i.e. a color-only state, and a screen reader hears a group called "Command" holding a code block plus Cancel / Decline / Approve.

Suggest naming the group with the approval kind so the state survives regardless of detail, and moving the field label onto the focusable <code> it describes:

diff
1 <div
2- aria-label={detailAriaLabel}
3+ aria-label={fallbackLabel}
4 className={cn("flex min-w-0 flex-1 items-center gap-2", className)}
5 role="group"
6 >
7 <code
8+ aria-label={detailAriaLabel}
9 className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85 [scrollbar-width:thin] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70 [&::-webkit-scrollbar]:h-1.5"
10 data-approval-detail="complete"
11 tabIndex={0}
12 >

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 1540–1549Outdated · 4dcdd96Open thread
……@@ -1370,41 +1391,203 @@ const WorkGroupSection = memo(function WorkGroupSection({
1546+ aria-label={row.hasFailure ? "Tool call failed" : undefined}
1547+ >
1548+ <WorkEntryIconSvg
1549+ name={row.hasFailure ? "x" : toolGroupSummaryIconName(row.summaryKind)}
MmacroscopeappBoton lines 1540–1549Aug 17, 2026

The aggregate failure icon here is not gated on collapse, so an expanded summary group swaps its category icon (eye / terminal / square-pen) for a destructive x and announces …, tool call failed while every failing child row below already renders its own x plus Tool call failed (PlainWorkEntryRow, lines 2620-2686).

The overflow toggle immediately below now avoids exactly that with showHiddenFailure = row.hasFailure && !row.expanded (line 1564), so the two disclosures in the same activity column disagree about aggregate failure state — one drops back to its normal icon when the failing rows become visible, the other doesn't. LiveWorkEntryTimelineRow (line 1482) has the same gap for its expanded batch.

Suggest deriving const showGroupFailure = row.hasFailure && !row.expanded; and using it for the icon name, wrapper color, role/aria-label, and the button's aria-label here (and the equivalent in the live row), so the aggregate marker only stands in for hidden rows.

Posted via Macroscope — UI Consistency

Intentional: the group summary keeps its aggregate failure identity while expanded. Expanded children explain which calls failed; they do not replace the status of the group. The overflow toggle differs because it describes only hidden rows.

MmacroscopeappBotreviewed · 1 inline threadAug 17, 2026
Synced from GitHub

UI consistency review — one finding on the expanded work-group spacing predicate.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 917–919Outdated · 4dcdd96Open thread
……@@ -921,25 +912,48 @@ type TimelineWorkEntry = Extract<MessagesTimelineRow, { kind: "work" }>["grouped
916+ const isLastExpandedToolGroupEntry = row.kind === "work" && row.isLastExpandedToolGroupEntry;
917+ const isExpandedToolGroupHeader =
918+ (row.kind === "work-toggle" && row.onlyToolEntries && row.expanded) ||
919+ (row.kind === "work-live" && row.expanded);
MmacroscopeappBoton lines 917–919Aug 17, 2026

chat/MessagesTimeline.tsx:919

isExpandedToolGroupHeader keys on row.onlyToolEntries && row.expanded, but the renderer only draws a group header (children below it) when row.onlyToolEntries && row.summary (WorkGroupToggleTimelineRow, line 1531). The overflow toggle — summary: null, pushed after its revealed rows in MessagesTimeline.logic.ts (lines 881-906) — can also carry onlyToolEntries: true, since it is now derived from hiddenEntries and workLogEntryIsToolLike is true for tone: "error" rows. With MAX_VISIBLE_WORK_LOG_ENTRIES = 1, a two-entry mixed group (e.g. one runtime.error row plus one tool row) hits exactly that: expanding it gives the trailing +1 previous tool call row pb-0 instead of pb-2, so it collapses against the next unrelated row while every revealed row above it keeps normal spacing.

Suggest gating on the same condition the renderer uses so the two decisions cannot drift:

suggestion
1 const isExpandedToolGroupHeader =
2 (row.kind === "work-toggle" && row.onlyToolEntries && !!row.summary && row.expanded) ||
3 (row.kind === "work-live" && row.expanded);

No longer relevant as of 96c8a4df8d37b35ef87d3c9cf78e6bf35d1ece76

Synced from GitHub

UI consistency review of the changed web surfaces. One finding on the new terminal sidebar tab; the earlier isExpandedToolGroupHeader gating issue is resolved by row.summary !== null in MessagesTimeline.tsx.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — the inline comment could not be attached to the diff, so the finding is below.

apps/web/src/components/ThreadTerminalDrawer.tsx (lines 1754-1762)

tsx
1<button
2 type="button"
3 className="group/close relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm hover:bg-muted"
4 aria-label={`Close ${displayLabel}`}
5 onClick={() => closeTerminalSafely(terminalId)}
6>
7 <TerminalSquare className="size-3 shrink-0 group-hover/tab:hidden group-focus-visible/close:hidden pointer-coarse:hidden" />
8 <XIcon className="hidden size-3 group-hover/tab:block group-focus-visible/close:block pointer-coarse:block" />
9</button>

This close control is a hand-rolled reconstruction of Button size="icon-micro" variant="ghost" — the primitive TerminalToolbarAction already uses a few lines above in the same sidebar (line 131) — but at size-4 instead of size-5, with hover:bg-muted instead of the ghost hover:bg-accent token and no focus-visible ring (its sibling tab-select button on line 1798 hand-rolls one).

The part that matters behaviorally is the coarse-pointer hit target: buttonVariants reserves one via pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 (ui/button.tsx line 11), and this row is exactly where that applies — pointer-coarse:block makes the destructive X permanently visible on touch, so the terminal-destroying tap target ends up 16×16 CSS px, immediately adjacent to the select button that occupies the rest of the 24px row.

Smallest fix: render the primitive and keep only the group hook at the call site, which restores the hit target, hover token, and focus ring while leaving the icon-swap groups intact.

diff
1- <button
2- type="button"
3- className="group/close relative flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm hover:bg-muted"
4+ <Button
5+ size="icon-micro"
6+ variant="ghost"
7+ className="group/close"
8 aria-label={`Close ${displayLabel}`}
9 onClick={() => closeTerminalSafely(terminalId)}
10 >
11 <TerminalSquare className="size-3 shrink-0 group-hover/tab:hidden group-focus-visible/close:hidden pointer-coarse:hidden" />
12 <XIcon className="hidden size-3 group-hover/tab:block group-focus-visible/close:block pointer-coarse:block" />
13- </button>
14+ </Button>

Button is already imported in this file.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — one finding on the id-less tool lifecycle change, inline.

Previously flagged findings that are still open in this head (not re-posted): the tool-group summary swaps its category icon for the destructive x and announces …, tool call failed even when expanded, while every failing child row below already carries its own indicator (MessagesTimeline.tsx lines 1540-1552; the overflow toggle right below it correctly gates on !row.expanded); the live work row is a disclosure with aria-expanded but no hover/press feedback or chevron, unlike the settled summary and overflow toggles that open the same group; and ThinkingActivityRow renders without the size-6 icon gutter every other row in the activity column reserves, so the label shifts horizontally the moment the first live tool row replaces it.

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 2232–2232Outdated · 10b4e43Open thread
……@@ -2035,6 +2221,170 @@ function workEntryRawCommand(
2229+};
2230+
2231+const COMMAND_WRAPPER_FLAGS: Record<CommandWrapper, ReadonlySet<string>> = {
2232+ env: new Set(["-0", "--null", "-i", "--ignore-environment", "--debug"]),
MmacroscopeappBoton lines 2232–2232Aug 19, 2026

🟡 Medium chat/MessagesTimeline.tsx:2232

commandProgramName returns null for env -v npm test, so the UI reports Running command instead of Running npm. COMMAND_WRAPPER_FLAGS.env includes --debug but not GNU env's equivalent -v; add the alias so this valid flag is skipped.

✅ Resolved in f19ee2b971e375e51d5637424f1687537acd5fa9

apps/web/src/components/chat/MessagesTimeline.logic.tslines 845–855Outdated · 10b4e43Open thread
……@@ -523,15 +827,53 @@ export function deriveMessagesTimelineRows(input: {
852+ ),
853+ });
854+ if (expanded) {
855+ for (const [entryIndex, workEntry] of visibleGroupedEntries.entries()) {
MmacroscopeappBoton lines 845–855Aug 19, 2026

chat/MessagesTimeline.logic.ts:855

summarizeToolGroup now counts only terminal lifecycle entries (toolGroupSummaryEntries, line 354), but hiddenCount, hasFailure, and the rows revealed on expand still use the full visibleGroupedEntries, so the header and its own contents disagree.

For the id-less lifecycle shape this PR keeps in the work log (does not leave id-less tool lifecycles running: a statusless tool.updated plus its tool.completed, session-logic.test.ts), the update marker is not neutral — workEntryIndicatesToolSuccess is true for an undefined status — so it survives the line 826 filter. The collapsed row reads Used 1 tool, and expanding it lists two rows for that one call, the intermediate marker rendered with the same settled treatment as the completion.

Suggest deriving the entry set once and using it for the label, the count, the failure flag, and the expanded children (toolGroupSummaryEntries already falls back to the raw entries when every entry is non-terminal, so an in-flight-only group still renders).

Same split inside the helper: toolGroupSummaryKind filters for the action set (line 367) but the icon fallback still maps over the unfiltered entries (line 374), so a non-terminal marker with a different itemType can push the group icon to mixed/hammer while the label ignores that entry.

No longer relevant as of f19ee2b971e375e51d5637424f1687537acd5fa9

apps/web/src/components/chat/MessagesTimeline.logic.tslines 734–736Outdated · f19ee2bOpen thread
……@@ -471,38 +671,142 @@ export function deriveMessagesTimelineRows(input: {
733+ const activeWorkEntryIds = new Set(activeToolEntries.map((entry) => entry.id));
734+ const visibleActiveToolEntries = activeToolEntries.filter((entry) =>
735+ isVisibleActiveToolEntry(entry.entry),
736+ );
MmacroscopeappBoton lines 734–736Aug 19, 2026

chat/MessagesTimeline.logic.ts:736

visibleActiveToolEntries never applies the terminal-entry rule that toolGroupSummaryEntries (line 355) applies to the settled group, so an id-less lifecycle pair is counted twice while the turn is running.

deriveWorkLogEntries deliberately keeps a statusless tool.updated marker alongside its tool.completed row for providers that emit no toolCallId (session-logic.test.ts, "does not leave id-less tool lifecycles running" — ids ["legacy-tool-update", "legacy-tool-complete"], statuses [undefined, "completed"]). Both are tool-like and neither is neutral (workEntryIndicatesToolSuccess is true for an undefined status), so both land in groupedEntries: the live row's label advances through the marker, and expanding it reveals two rows for one call. The moment the turn settles, the same batch renders as Used 1 tool with a single child (summarizeToolGroup test "counts id-less lifecycle markers as one completed tool"), so the same tool call visibly changes count on settle.

Suggest dropping superseded lifecycle markers here — entries whose sourceActivityKind is tool.started/tool.updated with an undefined toolLifecycleStatus once a later entry in the batch is terminal — rather than reusing toolGroupSummaryEntries as-is, which would also drop genuinely in-progress parallel calls whenever any sibling has completed.

No longer relevant as of 8f9910e4fb3c0cf05c6ba766ee6da472d901f96d

Synced from GitHub

UI consistency review of the tool-activity timeline rows. One new finding: the terminal-lifecycle filter added in the latest commit is applied only to the all-tool summary branch, so the same batch renders a different number of rows depending on its neighbours. Details inline.

Posted via Macroscope — UI Consistency

apps/web/src/session-logic.tslines 1075–1080Outdated · 17f6f2bOpen thread
……@@ -993,12 +1043,61 @@ function collapseDerivedWorkLogEntries(
1077+ candidate.itemType === previous.itemType &&
1078+ normalizeCompactToolLabel(candidate.toolTitle ?? candidate.label) ===
1079+ normalizeCompactToolLabel(previous.toolTitle ?? previous.label),
1080+ );
MmacroscopeappBoton lines 1075–1080Aug 19, 2026

🟡 Medium src/session-logic.ts:1075

An unfinished same-type/same-label tool row from an older turn makes a current turn's adjacent tool.started/tool.completed pair remain as two rows, so the lifecycle is not collapsed. hasCompetingIdlessCompletionTarget scans all earlier collapsed rows without matching candidate.turnId to previous.turnId; restrict that search to the same turn, as shouldCollapseToolLifecycleEntries does.

✅ Resolved in 374c3d5638887c480344a72db072c15d8126b28b

apps/web/src/components/chat/MessagesTimeline.logic.tsline 386Outdated · 17f6f2bOpen thread
……@@ -242,6 +262,173 @@ export function normalizeCompactToolLabel(value: string): string {
383+ workEntry.toolLifecycleStatus === undefined &&
384+ (workEntry.sourceActivityKind === "tool.started" ||
385+ workEntry.sourceActivityKind === "tool.updated");
386+ if (isStatuslessIdlessMarker && laterTerminalIdentities.has(identity)) continue;
MmacroscopeappBoton line 386Aug 19, 2026

🟡 Medium chat/MessagesTimeline.logic.ts:386

With concurrent same-label calls ordered started(A), started(B), completed(A), the live timeline drops B's still-running marker and shows only the completed call. The backward scan records one shared identity for A and B, so A's terminal marker supersedes both; correlate lifecycle events with a unique call identity before suppressing markers.

No longer relevant as of 6d25760e673c6eb1110d8ecf7692b1a75dab44e3

Synced from GitHub

UI consistency review of the changed web surfaces. Two findings, both in apps/web/src/components/ThreadTerminalDrawer.tsx. Findings already raised on earlier commits of this branch (approval action variants, command/stash drawer row density, live/summary tool rows, composer drawer geometry duplication, useResizableWidth cross-window persistence, PR detail ghost) are not repeated here.

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — the inline comments could not be attached to the diff, so the two findings are below. Findings already raised on earlier commits of this branch (approval action variants, command/stash drawer row density, live/summary tool rows, composer drawer geometry duplication, useResizableWidth cross-window persistence, PR detail ghost) are not repeated.


1. apps/web/src/components/ThreadTerminalDrawer.tsx (lines 1778-1782)

tsx
1<input
2 autoFocus
3 value={terminalRenameDraft}
4 aria-label="Rename terminal"
5 className="h-6 min-w-0 flex-1 rounded border border-input bg-background px-1.5 text-xs text-foreground outline-none focus:ring-2 focus:ring-ring"

This new inline rename field hand-rolls the Input primitive: border border-input bg-background reproduces its chrome, but at h-6 / rounded / focus:ring-2 instead of Input size="compact"'s h-7, rounded-md and has-focus-visible: ring, and it drops the dark-mode dark:bg-input/32 fill — so in dark themes the field paints the page background inside a bg-accent tab row rather than the input token.

The same inline-rename interaction elsewhere in this PR already goes through the primitive (pullRequest/PullRequestDetailPanel.tsx, <Input autoFocus size="sm" … />). Suggest rendering Input here too (importing it from ./ui/input) and keeping only min-w-0 flex-1 at the call site, so focus ring, radius, height and theming stay owned by one contract.


2. apps/web/src/components/ThreadTerminalDrawer.tsx (lines 1354-1356)

tsx
1const hideTerminalActionLabel = terminalToggleShortcutLabel
2 ? `Toggle terminal drawer (${terminalToggleShortcutLabel})`
3 : "Toggle terminal drawer";

onHide only closes the drawer (ChatView.tsx: hideTerminal = () => setTerminalOpen(false)), but this names it Toggle terminal drawer — the exact accessible name chat/PanelLayoutControls.tsx gives the real toggle in the header. Two controls then announce the same name for different behaviour, and the shortcut shown in parentheses toggles while this button can never reopen the drawer.

Suggest naming the control for what it does:

diff
1 const hideTerminalActionLabel = terminalToggleShortcutLabel
2- ? `Toggle terminal drawer (${terminalToggleShortcutLabel})`
3- : "Toggle terminal drawer";
4+ ? `Hide terminal drawer (${terminalToggleShortcutLabel})`
5+ : "Hide terminal drawer";

Posted via Macroscope — UI Consistency

apps/web/src/components/chat/MessagesTimeline.logic.tsline 832Outdated · 0395830Open thread
……@@ -471,38 +694,143 @@ export function deriveMessagesTimelineRows(input: {
505829
830+ if (activeWorkEntryIds.has(timelineEntry.id)) {
831+ continue;
832+ }
CcursorBoton line 832Aug 19, 2026

Thinking work vanishes from live timeline

High Severity

The live batch treats every non-error workLogEntryIsToolLike row as a tool, including thinking rows without agentSpawn such as background task progress. Those ids are stripped from the chronological list, but isVisibleActiveToolEntry still hides neutral thinking rows, so they never land in work-live either and disappear while the turn is active. After settle, the same predicate folds them into a tool summary.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 039583025bbe4ecfc209f957aa2309983aef4ace. Configure here.

Synced from GitHub

UI consistency review — the inline comment could not be attached to the diff, so the finding is below. Items already raised on earlier commits of this branch (live vs. settled tool-row hover/failure treatment, agent-spawn panel surface duplication, command/stash drawer row density, approval action variant overrides, composer drawer geometry duplication) are not repeated.

apps/web/src/components/chat/ComposerStashBadge.tsx (lines 67-70)

tsx
1 className={cn(
2 "chat-composer-stash-tab absolute -top-7 right-4 z-0 inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-t-xl border border-b-0 px-3 pb-1 text-xs leading-none",
3 "transition-[color,border-color] duration-200",
4 props.menuOpen && "pointer-events-none",

The two placements of this same control no longer agree on keyboard affordance. The new inline branch renders through Button, so it inherits focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 from buttonVariants (ui/button.tsx:11) — the new test asserts exactly that. This rewritten tab branch declares cursor-pointer and aria-expanded but carries no focus-visible: styling at all, and at z-0 behind [data-chat-composer-main-surface] the UA default outline is not a reliable substitute. The stash entry point therefore shows a focus state in one placement and none in the other.

Suggest carrying the same focus-visible ring on the tab (or rendering it through Button with the tab geometry at the call site):

diff
1 "chat-composer-stash-tab absolute -top-7 right-4 z-0 inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-t-xl border border-b-0 px-3 pb-1 text-xs leading-none",
2- "transition-[color,border-color] duration-200",
3+ "transition-[color,border-color] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",

Posted via Macroscope — UI Consistency

Synced from GitHub

UI consistency review — the inline comment could not be attached to the diff, so the finding is below. Items already raised on earlier commits of this branch (stash tab focus affordance, live vs. settled tool-row treatment, agent-spawn panel surface duplication, approval action variant overrides, composer drawer geometry duplication) are not repeated.

apps/web/src/components/chat/ComposerTasksBadge.tsx (lines 151-159, and the identical copy at 191-199)

tsx
1 <button
2 type="button"
3 aria-label="Dismiss tasks for this turn"
4 className="inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/65 hover:bg-muted/60 hover:text-foreground"
5 onClick={onDismiss}
6 onPointerDown={(event) => event.preventDefault()}
7 >
8 <XIcon aria-hidden className="size-3" />
9 </button>

The same "Dismiss tasks for this turn" action is implemented two different ways in this file. The inline placement uses the primitive (Button size="icon-micro" variant="ghost-muted", lines 102-111); this tab copy — and the identical copy in ComposerTasksDrawer — hand-rolls it at rounded-md instead of the variant's rounded-sm, hover:bg-muted/60 instead of the ghost-muted bg-accent token, and text-muted-foreground/65 instead of text-muted-foreground. It also drops two things buttonVariants guarantees (ui/button.tsx:11): the focus-visible:ring-2 ring-ring ring-offset-1 ring — so this control has no visible keyboard focus state while its inline twin does — and the pointer-coarse:after:min-h-11 min-w-11 touch hit target, which matters for a 20px control.

Suggested fix — route both copies through the primitive (already imported in this file):

diff
1- <button
2- type="button"
3- aria-label="Dismiss tasks for this turn"
4- className="inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/65 hover:bg-muted/60 hover:text-foreground"
5- onClick={onDismiss}
6- onPointerDown={(event) => event.preventDefault()}
7- >
8- <XIcon aria-hidden className="size-3" />
9- </button>
10+ <Button
11+ size="icon-micro"
12+ variant="ghost-muted"
13+ aria-label="Dismiss tasks for this turn"
14+ className="shrink-0"
15+ onClick={onDismiss}
16+ onPointerDown={(event) => event.preventDefault()}
17+ >
18+ <XIcon aria-hidden className="size-3" />
19+ </Button>

The two toggle buttons (lines 125-132 and 178-184) have the same missing focus-visible treatment; if they stay raw for their row geometry, they still need an explicit focus-visible: ring like the other timeline and composer disclosures.

Posted via Macroscope — UI Consistency

Added 7 commits

  • feat(web): group tool activity5122399
  • fix(web): align thinking activity5addb76
  • fix(web): keep active tool runs on one line2f6812a
  • fix(web): keep separated tool runs visible615b474
  • fix(web): ignore stale tool progress0f2a0a2
  • fix(web): reconcile grouped tool state1f7adbb
  • test(web): lock live turn activity copy60218ae