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

@ -30,13 +30,13 @@ export function QueuePanel({ busy, editingId, entries, onDelete, onEdit, onSendN
}
return (
<div className="rounded-t-2xl border border-b-0 border-border/65 bg-[color-mix(in_srgb,var(--dt-card)_70%,transparent)] pt-0.5 pb-1">
<div className="rounded-t-2xl border border-b-0 border-border/65 bg-[color-mix(in_srgb,var(--dt-card)_70%,transparent)] pt-0.5 pb-1 mx-1">
<button
className="flex w-full items-center gap-1.5 px-2 py-0.5 text-left text-[0.72rem] font-medium text-muted-foreground/92 transition-colors hover:text-foreground/90"
className="flex w-full items-center gap-1.5 px-2 text-left text-[0.6rem] font-medium text-muted-foreground/92 transition-colors hover:text-foreground/90"
onClick={() => setCollapsed(open => !open)}
type="button"
>
<DisclosureCaret className="shrink-0" open={!collapsed} size="0.875rem" />
<DisclosureCaret className="shrink-0" open={!collapsed} size="1em" />
<span className="truncate">{c.queued(entries.length)}</span>
</button>
@ -64,11 +64,7 @@ export function QueuePanel({ busy, editingId, entries, onDelete, onEdit, onSendN
<p className="truncate text-[0.73rem] leading-4 text-foreground/92">{entryPreview(entry, c)}</p>
{(attachmentsCount > 0 || isEditing) && (
<div className="mt-0.5 flex items-center gap-1.5 text-[0.64rem] text-muted-foreground/75">
{attachmentsCount > 0 && (
<span>
{c.attachments(attachmentsCount)}
</span>
)}
{attachmentsCount > 0 && <span>{c.attachments(attachmentsCount)}</span>}
{isEditing && (
<span className="text-[color-mix(in_srgb,var(--dt-composer-ring)_78%,var(--muted-foreground))]">
{c.editingInComposer}

View file

@ -410,6 +410,10 @@ export function useMessageStream({
phase: 'running' | 'complete',
sourceEventType?: string
) => {
// Text deltas flush on a timer but tool events apply now; flush first so
// a tool part can't jump ahead of the text that preceded it.
flushQueuedDeltas(sessionId)
if (!nativeSubagentSessionsRef.current.has(sessionId)) {
for (const subagentPayload of delegateTaskPayloads(payload, phase, sourceEventType)) {
upsertSubagent(
@ -428,7 +432,7 @@ export function useMessageStream({
{ pending: m => phase !== 'complete' || (m.pending ?? false) }
)
},
[mutateStream]
[flushQueuedDeltas, mutateStream]
)
const completeAssistantMessage = useCallback(