diff --git a/apps/desktop/src/components/chat/disclosure-row.tsx b/apps/desktop/src/components/chat/disclosure-row.tsx index 56cd6d9a3dd..fc81ea867c7 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 7fa197d9a46..e411f451df0 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -410,9 +410,16 @@ /* 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; + /* 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. */ @@ -1526,6 +1533,36 @@ 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); +} + +/* 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 @@ -1536,6 +1573,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;