From 0e5a6c56584bb6ccd819a9261b1485b99d977f76 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Tue, 20 Oct 2026 20:55:01 +0200 Subject: [PATCH] feat(web): interleave thinking blocks with tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold thinking deltas into the turn's stream-ordered entries as a thinking entry, merged back-to-back the way the stream delivers them, instead of one flat string rendered above every call. Each thinking block now sits where it streamed — between the calls it precedes. Live turns keep their thinking expanded; finished turns render it collapsed. --- web/claw/app/components/ChatPanel.vue | 47 +++++++---------------- web/claw/app/components/TurnEntries.vue | 40 +++++++++++++++++-- web/claw/app/composables/useTranscript.ts | 30 ++++++++++----- 3 files changed, 70 insertions(+), 47 deletions(-) diff --git a/web/claw/app/components/ChatPanel.vue b/web/claw/app/components/ChatPanel.vue index d982097..6728624 100644 --- a/web/claw/app/components/ChatPanel.vue +++ b/web/claw/app/components/ChatPanel.vue @@ -50,8 +50,20 @@ function onScroll(): void { pinned.value = el.scrollHeight - el.scrollTop - el.clientHeight < 48 } +// Thinking grows inside the trailing entry without changing the +// entries length, so its total length joins the watch to keep the +// stream pinned at the bottom. +function liveThinkingLength(): number { + return ( + props.live?.entries.reduce( + (count, entry) => (entry.kind === 'thinking' ? count + entry.text.length : count), + 0, + ) ?? 0 + ) +} + watch( - () => [props.items.length, props.live?.reply.length, props.live?.thinking.length, props.live?.entries.length], + () => [props.items.length, props.live?.reply.length, liveThinkingLength(), props.live?.entries.length], async () => { if (!pinned.value) return await nextTick() @@ -224,10 +236,6 @@ function streamTone(status: StreamStatus): 'ok' | 'warn' | 'error' {
{{ agentName }}
-
- thinking -
{{ item.turn.thinking }}
-
{{ agentName }}
-
- thinking -
{{ live.thinking }}
-
-// A turn's stream-ordered middle: tool rows, the agent's asks, its -// handovers — each at the position the stream delivered it, so an ask -// sits at the tool call that raised it rather than at the turn's top. +// A turn's stream-ordered middle: tool rows, pieces of the thinking +// trace, the agent's asks, its handovers — each at the position the +// stream delivered it, so an ask sits at the tool call that raised it +// and thinking sits between the calls it precedes. import type { TurnEntry } from '~/composables/useTranscript' import type { ConsentAnswer } from '~/composables/useClaw' -defineProps<{ entries: TurnEntry[] }>() +defineProps<{ + entries: TurnEntry[] + /** A live turn's thinking blocks start expanded; finished ones collapse. */ + openThinking?: boolean +}>() const emit = defineEmits<{ consentAnswer: [id: string, answer: ConsentAnswer] @@ -18,6 +23,10 @@ const emit = defineEmits<{