From 2881243df3cfe65ff8197ba062f92d17faf34950 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 23:27:08 -0500 Subject: [PATCH 1/2] fix(desktop): tighten the gap between adjacent transcript scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The turn block gap is the space between the reply and the work around it, so a back-to-back run of thinking headers and tool rows spent the full gap between every line and read as a stack of cards rather than one column. Adjacent scaffolding now ticks at a third of it. The hook is the block list the rhythm rule already enumerates, minus prose, rather than `data-conversation-scaffold`: that marker is absent on a multi-call tool group — most of a real run — and present on rows nested inside one, so it both missed the lines that needed tightening and moved ones that didn't. An open file edit is the exception. A diff is the deliverable, read like a PR, so it keeps the full block gap on both sides while the scaffolding around it stays tight. A streaming turn seals blocks into separate bubbles where the flex gap would restore the full gap, so that seal is corrected to match — and skipped when either side carries a diff. --- apps/desktop/src/styles.css | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 980501ded4e4..e304eb9487e9 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -406,6 +406,10 @@ /* Gap between top-level turn blocks (prose ↔ tools ↔ thinking) — enough air that scaffolding reads as separate from the reply, not crammed into it. */ --turn-block-gap: 0.75rem; + /* Between adjacent scaffolding, though: a back-to-back run of thinking + headers and tool rows is one sequence, not blocks to hold apart. Derived + so retuning the block gap carries it along. */ + --scaffold-block-gap: calc(var(--turn-block-gap) / 3); /* Tight gap between tool rows inside a single action group, so a back-to-back run still reads as one cohesive sequence. */ --tool-row-gap: 0.375rem; @@ -1522,6 +1526,28 @@ text-* variant utilities. */ .btn-arc { margin-top: var(--paragraph-gap); } +/* And except scaffolding ↔ scaffolding: the full gap is the space between the + reply and the work around it, so a back-to-back run of thinking headers, + tool rows and status lines spent it between every line and read as stacked + cards. Same list as the rule above minus prose — `data-conversation-scaffold` + is the wrong hook here: it's absent on a multi-call tool GROUP, which is most + of a real run, and present on rows nested inside one. */ +[data-slot='aui_assistant-message-content'] + > :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall']) + + :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall']) { + margin-top: var(--scaffold-block-gap); +} + +/* Except a file edit, which is the deliverable rather than another line in the + run — a diff you read like a PR, so it keeps the full block gap on both + sides while the scaffolding around it stays tight. `data-file-edit` is only + on the row while it's open, so a collapsed edit rejoins the run. Margins on + the block itself, not adjacency pairs: it needs the clearance whatever sits + next to it, and adjacent margins collapse rather than stacking. */ +[data-slot='aui_assistant-message-content'] > [data-tool-row][data-file-edit] { + margin-block: var(--turn-block-gap); +} + /* A streaming turn is sealed into several assistant bubbles as it goes (`message.interim`) and rehydrates into fewer, so the same two blocks are sometimes siblings inside one bubble and sometimes split across two. The @@ -1532,6 +1558,23 @@ text-* variant utilities. */ .btn-arc { margin-top: calc(var(--turn-block-gap) - var(--conversation-turn-gap)); } +/* Unless the blocks either side of that seal are both scaffolding, in which + case the scaffold gap is the target and the flex gap already overshoots it. + Topping up to the full block gap here is what made a live run relax between + rows and then tighten once it rehydrated into one bubble. A bubble carrying + an open diff is excluded — it keeps the full gap, same as in-flow. */ +[data-slot='aui_turn-pair'] + > [data-slot='aui_assistant-message-root']:has( + > [data-slot='aui_assistant-message-content'] + > :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall']):last-child + ):not(:has([data-file-edit])) + + [data-slot='aui_assistant-message-root']:has( + > [data-slot='aui_assistant-message-content'] + > :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall']):first-child + ):not(:has([data-file-edit])) { + margin-top: calc(var(--scaffold-block-gap) - var(--conversation-turn-gap)); +} + /* Message action bars — flat icon hits with default dim; only the hovered/focused control is full-strength. */ [data-slot='aui_msg-actions'] button { border: 0; From 16ef964fdb8a600d449fe872d3f658213187e551 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 23:27:29 -0500 Subject: [PATCH 2/2] feat(desktop): rest the thinking caret at a faint hint instead of invisible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A run of thinking headers is the one place the disclosure affordance isn't otherwise discoverable — every other one sits in a row you're already reaching for. The resting opacity becomes a token so only that surface opts in; DisclosureRow is shared with tool rows and run headers, which keep the invisible-until-hover default. --- apps/desktop/src/components/chat/disclosure-row.tsx | 2 +- apps/desktop/src/styles.css | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/chat/disclosure-row.tsx b/apps/desktop/src/components/chat/disclosure-row.tsx index 56cd6d9a3ddd..fc81ea867c70 100644 --- a/apps/desktop/src/components/chat/disclosure-row.tsx +++ b/apps/desktop/src/components/chat/disclosure-row.tsx @@ -55,7 +55,7 @@ export function DisclosureRow({ 'flex h-(--conversation-line-height) shrink-0 items-center justify-center transition-opacity duration-150', open ? 'opacity-80' - : 'opacity-0 group-hover/disclosure-row:opacity-80 group-focus-within/disclosure-row:opacity-80' + : 'opacity-(--disclosure-caret-rest) group-hover/disclosure-row:opacity-80 group-focus-within/disclosure-row:opacity-80' )} > diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index e304eb9487e9..b37d94bc2941 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -413,6 +413,9 @@ /* Tight gap between tool rows inside a single action group, so a back-to-back run still reads as one cohesive sequence. */ --tool-row-gap: 0.375rem; + /* Resting opacity of a disclosure caret — invisible until hover, except + where a surface opts into a faint hint of the affordance. */ + --disclosure-caret-rest: 0; /* Paragraph spacing — vertical gap between prose paragraphs, both inside a markdown block and between consecutive prose parts. Single knob; tweak freely. */ @@ -1548,6 +1551,14 @@ text-* variant utilities. */ .btn-arc { margin-block: var(--turn-block-gap); } +/* Thinking headers carry a faint resting caret. A run of them is the one place + the affordance isn't otherwise discoverable — every other disclosure sits in + a row you're already reaching for. The scaffold fade multiplies this, so the + painted value lands well under the number here. */ +[data-slot='aui_thinking-disclosure'] { + --disclosure-caret-rest: 0.4; +} + /* A streaming turn is sealed into several assistant bubbles as it goes (`message.interim`) and rehydrates into fewer, so the same two blocks are sometimes siblings inside one bubble and sometimes split across two. The