Tighten conversation rhythm, flatten the tool list, and smooth streaming text

Conversation rhythm:
- Single `--paragraph-gap` knob drives paragraph spacing both inside a
  markdown block and between consecutive prose parts, out-specifying Tailwind
  Typography's prose margins. Code cards carry the same gap themselves so it
  holds at any Streamdown nesting depth.
- Two-tier vertical rhythm: `--turn-block-gap` separates scaffolding (tools /
  thinking) from the reply; `--tool-row-gap` keeps a tool run tight.
- Drop the prose indent so prose, tools, todos, and thinking share one left
  edge. `---` renders as quiet spacing, not a heavy rule.

Flat tool list:
- Tools always render as a standalone-row stack, never a "Tool actions · N
  steps" group. assistant-ui slices the tool range unstably (interleaved live
  vs. reconstructed-consecutive when settled), so grouping reshuffled the whole
  turn the instant it settled. Flat rows are pixel-identical either way.
- Inline approvals can no longer be buried in a collapsed group body.
- Remove the now-dead grouping helpers from tool-fallback-model.

Empty thinking:
- Suppress reasoning disclosures with no visible text (encrypted / spinner-
  coerced reasoning) instead of leaving an empty "Thinking" header.
- Tail stall indicator returns "thinking" when a running turn goes quiet.

Streaming cadence:
- Smooth character-reveal decouples visible cadence from bursty arrival.
- Flush queued text deltas before applying tool events so a tool row can't
  jump ahead of its preceding text.
- Disable Nagle on the GUI WebSocket so per-token frames aren't coalesced.

Polish: clarify/patch/vision_analyze tool meta, queue-panel + diff-lines
spacing, sticky human bubble expands on focus (not hover).
This commit is contained in:
Brooklyn Nicholson
2026-06-06 10:45:31 -05:00
parent 6bbc5eefa0
commit 9d31577590
12 changed files with 287 additions and 355 deletions
+56 -37
View File
@@ -278,11 +278,21 @@
--composer-shell-pad-block-end: 0.625rem;
--message-text-indent: 0.75rem;
--conversation-text-font-size: 0.8125rem;
--conversation-tool-font-size: var(--conversation-text-font-size);
--conversation-tool-font-size: 0.6875rem;
--conversation-caption-font-size: 0.75rem;
--conversation-line-height: 1.125rem;
--conversation-caption-line-height: 1rem;
--conversation-turn-gap: 0.375rem;
/* 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;
/* 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;
/* Paragraph spacing — vertical gap between prose paragraphs, both inside a
markdown block and between consecutive prose parts. Single knob; tweak
freely. */
--paragraph-gap: 0.45rem;
--sticky-human-top: 0.23rem;
--file-tree-row-height: 1.375rem;
@@ -798,14 +808,27 @@ canvas {
font-size: inherit;
}
/* Streamed prose hangs slightly indented from the tool/todo column so the
reading column reads as a "reply" within the conversation gutter. Tools,
todos, and thinking blocks keep the existing --message-text-indent so they
remain flush with the user message text above them. */
[data-slot='aui_assistant-message-content'] > .aui-md {
padding-inline-start: var(--md-text-indent, 0.5rem);
/* Tailwind Typography sets `.prose :where(p) { margin: 1.25em }` (~16px). That
selector ties our `my-*` utility on specificity and wins on source order, so
paragraph spacing must be reclaimed here at higher specificity. One tight
top-margin (bottom zeroed to avoid doubling), first child reset to flush. */
[data-slot='aui_assistant-message-content'] .aui-md :where(p) {
margin-block: var(--paragraph-gap) 0;
}
/* First rendered element of a prose block is flush — the block-level gap above
(tool / paragraph) already provides the separation. Reach one level deep too:
Streamdown wraps blocks in a `div.space-y-*`, so the real first line is the
first child's first child. */
[data-slot='aui_assistant-message-content'] .aui-md > :first-child,
[data-slot='aui_assistant-message-content'] .aui-md > :first-child > :first-child {
margin-top: 0;
}
/* Prose, tools, todos, and thinking all share one left edge (the message
content's --message-text-indent). No extra prose indent — a single gutter
reads cleaner than a ragged tool-vs-reply column. */
[data-slot='aui_user-message-root'] {
top: var(--sticky-human-top);
}
@@ -816,12 +839,13 @@ canvas {
}
/* Sticky human bubbles clamp to ~2 lines with a soft bottom fade so a long
prompt doesn't dominate the viewport while you read the response stuck
beneath it. The clamp lifts on hover / focus (clicking the bubble opens the
edit composer, which already shows the full text). --human-msg-full is the
measured content height (set in UserMessage) so expand/collapse animates to
the real height instead of overshooting the cap. */
prompt doesn't dominate the viewport. The clamp lifts on focus only (clicking
opens the edit composer, which shows the full text) — not on hover, so the
bubble doesn't jump as the pointer passes over it. --human-msg-full is the
measured content height (set in UserMessage) so it animates to the real
height instead of overshooting the cap. */
.sticky-human-clamp {
cursor: pointer;
max-height: calc(2 * var(--dt-line-height) * var(--conversation-text-font-size) + 0.15rem);
overflow: hidden;
transition: max-height 0.08s cubic-bezier(0.4, 0, 0.2, 1);
@@ -832,7 +856,6 @@ canvas {
mask-image: linear-gradient(to bottom, #000 55%, transparent);
}
.composer-human-message:hover .sticky-human-clamp,
.composer-human-message:focus-within .sticky-human-clamp {
max-height: min(var(--human-msg-full, 24rem), 24rem);
overflow-y: auto;
@@ -992,7 +1015,7 @@ canvas {
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block'] {
contain: none;
overflow: visible;
margin-block: 0.375rem !important;
margin-block: var(--paragraph-gap) 0 !important;
padding: 0 !important;
gap: 0 !important;
border: 0 !important;
@@ -1006,6 +1029,11 @@ canvas {
}
[data-slot='aui_assistant-message-content'] .aui-md [data-slot='code-card'] {
/* Streamdown nests blocks, so the container's child-combinator rhythm can't
reach the card. Carry the paragraph gap on the card itself (top-owned);
collapses cleanly with the wrapper's margin when one is present, and the
first-child reset still flushes a leading code block. */
margin-block: var(--paragraph-gap) 0;
position: relative;
transition:
border-color 180ms ease-out,
@@ -1075,34 +1103,25 @@ canvas {
opacity: 1;
}
/* Conversation block rhythm. Consecutive tool calls stay tight so a step
sequence reads as one action group; the gap between any scaffolding
block and adjacent prose bumps up so the model's reply visually
separates from its scaffolding. */
[data-slot='tool-block'] + [data-slot='tool-block'] {
margin-top: 0.375rem;
}
[data-slot='tool-block']:has(> :nth-child(2)) + [data-slot='tool-block'] {
margin-top: 0.625rem;
}
/* Conversation block rhythm. assistant-ui renders each range as a direct child
of the message content with no per-part wrapper, so adjacency rules cover
every pairing — first block needs no reset, nested tool rows are untouched.
Two tiers: scaffolding (tool / thinking) gets a roomy block gap so it reads
as separate from the reply; consecutive prose collapses to a tight paragraph
rhythm so split-out text parts don't look like a big gap. */
/* Scaffolding adjacent to anything → roomy block gap. */
[data-slot='aui_assistant-message-content']
:is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'])
+ .aui-md,
> :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'])
+ :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], .aui-md),
[data-slot='aui_assistant-message-content']
.aui-md
> .aui-md
+ :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure']) {
margin-top: 1rem;
margin-top: var(--turn-block-gap);
}
[data-slot='aui_assistant-message-content'] [data-slot='aui_thinking-disclosure'] + [data-slot='tool-block'],
[data-slot='aui_assistant-message-content'] [data-slot='tool-block'] + [data-slot='aui_thinking-disclosure'] {
margin-top: 0.75rem;
}
[data-slot='aui_assistant-message-content'] > [data-slot='tool-block']:first-child {
margin-top: 0;
/* Prose ↔ prose → tight paragraph rhythm, matching in-block paragraph spacing. */
[data-slot='aui_assistant-message-content'] > .aui-md + .aui-md {
margin-top: var(--paragraph-gap);
}
/* Message action bars — flat icon hits with default dim; only the hovered/focused control is full-strength. */