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

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. */