fix: name each tool once, and stream the reasoning live (#18)

This commit was merged in pull request #18.
This commit is contained in:
2026-10-06 14:18:50 +00:00
parent ff99778e5d
commit 550a2bbefe
7 changed files with 426 additions and 58 deletions
+24 -8
View File
@@ -7,6 +7,7 @@ import { isSelfHosted } from "../ollama";
import { OllamaError } from "../ollama";
import type { ResolvedPreferences } from "../preferences";
import { createSession, createTurn, saveSession, withTitleFromFirstTurn } from "../sessions";
import { EMPTY_TRACE, afterToolCall, withFinalTrace, withThinking, type ThinkingTrace } from "../thinking";
import { toolsFor } from "../tools";
import type { PendingAttachment, Session, VisionSupport } from "../types";
import { errorMessage, isAbortError } from "../utils";
@@ -61,6 +62,13 @@ export interface UseChat {
session: Session;
isStreaming: boolean;
streamingResponse: string;
/**
* The reasoning to show right now: only the part since the last tool call.
*
* Deliberately not the whole trace. The full one is carried on the turn for
* storage; this is the live view, and it is cleared each time a tool starts so
* a long research turn does not stack every round's thinking on screen.
*/
streamingThinking: string;
/** Short activity line such as "Searching the web", shown while a turn is in flight. */
status?: string;
@@ -102,7 +110,7 @@ export function useChat(options: UseChatOptions): UseChat {
const abortRef = useRef<AbortController | null>(null);
const liveRef = useRef(false);
const responseRef = useRef("");
const thinkingRef = useRef("");
const thinkingRef = useRef<ThinkingTrace>(EMPTY_TRACE);
const statusRef = useRef<string | undefined>(undefined);
const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -131,14 +139,14 @@ export function useChat(options: UseChatOptions): UseChat {
flushTimerRef.current = null;
if (!liveRef.current) return;
setStreamingResponse(responseRef.current);
setStreamingThinking(thinkingRef.current);
setStreamingThinking(thinkingRef.current.current);
}, FLUSH_INTERVAL_MS);
}, []);
const flushNow = useCallback(() => {
clearFlushTimer();
setStreamingResponse(responseRef.current);
setStreamingThinking(thinkingRef.current);
setStreamingThinking(thinkingRef.current.current);
}, [clearFlushTimer]);
const setStatusValue = useCallback((value: string | undefined) => {
@@ -185,7 +193,7 @@ export function useChat(options: UseChatOptions): UseChat {
commit(started);
responseRef.current = "";
thinkingRef.current = "";
thinkingRef.current = EMPTY_TRACE;
setStreamingResponse("");
setStreamingThinking("");
setError(undefined);
@@ -242,11 +250,18 @@ export function useChat(options: UseChatOptions): UseChat {
scheduleFlush();
},
onThinkingDelta: (delta) => {
thinkingRef.current += delta;
thinkingRef.current = withThinking(thinkingRef.current, delta);
scheduleFlush();
},
onStatus: (value) => setStatusValue(value),
onToolStart: (_name, label, count) => setActivity({ label, count }),
onToolStart: (_name, label, count) => {
// The reasoning that produced this call is spent, so the live
// segment is cleared and the tool line takes its place. The full
// trace is untouched, because the stored turn keeps all of it.
thinkingRef.current = afterToolCall(thinkingRef.current);
setActivity({ label, count });
flushNow();
},
onToolCall: (record) => {
toolCalls = [...toolCalls, record];
},
@@ -254,7 +269,7 @@ export function useChat(options: UseChatOptions): UseChat {
});
responseRef.current = result.content;
thinkingRef.current = result.thinking;
thinkingRef.current = withFinalTrace(result.thinking);
toolCalls = result.toolCalls;
} catch (thrown) {
if (isAbortError(thrown)) {
@@ -268,7 +283,7 @@ export function useChat(options: UseChatOptions): UseChat {
const finished = {
...turn,
response: responseRef.current,
thinking: thinkingRef.current,
thinking: thinkingRef.current.full,
toolCalls,
error: failure,
};
@@ -306,6 +321,7 @@ export function useChat(options: UseChatOptions): UseChat {
setError(undefined);
setStreamingResponse("");
setStreamingThinking("");
thinkingRef.current = EMPTY_TRACE;
commit(next);
},
[commit, setStatusValue],