fix(chat): thinking execution graph (#880)

This commit is contained in:
Haze
2026-04-20 20:53:26 +08:00
committed by GitHub
parent 7fa4852c1d
commit 9a1575114d
9 changed files with 227 additions and 274 deletions

View File

@@ -2,12 +2,10 @@ import { invokeIpc } from '@/lib/api-client';
import { hostApiFetch } from '@/lib/host-api';
import { useGatewayStore } from '@/stores/gateway';
import {
clearHistoryPoll,
enrichWithCachedImages,
enrichWithToolResultFiles,
getLatestOptimisticUserMessage,
getMessageText,
hasNonToolAssistantContent,
isInternalMessage,
isToolResultRole,
loadMissingPreviews,
@@ -160,6 +158,18 @@ export function createHistoryActions(
return toMs(msg.timestamp) >= userMsTs;
};
// If we're sending but haven't received streaming events, check
// whether the loaded history reveals assistant activity (tool calls,
// narration, etc.). Setting pendingFinal surfaces the execution
// graph / activity indicator in the UI.
//
// Note: we intentionally do NOT set sending=false here. Run
// completion is exclusively signalled by the Gateway's phase
// 'completed' event (handled in gateway.ts) or by receiving a
// 'final' streaming event (handled in runtime-event-handlers.ts).
// Attempting to infer completion from message history is fragile
// and leads to premature sending=false during server-side tool
// execution.
if (isSendingNow && !pendingFinal) {
const hasRecentAssistantActivity = [...filteredMessages].reverse().some((msg) => {
if (msg.role !== 'assistant') return false;
@@ -169,25 +179,6 @@ export function createHistoryActions(
set({ pendingFinal: true });
}
}
// If pendingFinal, check whether the AI produced a final text response.
// Only finalize when the candidate is the very last message in the
// history — intermediate assistant messages (narration + tool_use) are
// followed by tool-result messages and must NOT be treated as the
// completed response, otherwise `pendingFinal` is cleared too early
// and the streaming reply bubble never renders.
if (pendingFinal || get().pendingFinal) {
const recentAssistant = [...filteredMessages].reverse().find((msg) => {
if (msg.role !== 'assistant') return false;
if (!hasNonToolAssistantContent(msg)) return false;
return isAfterUserMsg(msg);
});
const lastMsg = filteredMessages[filteredMessages.length - 1];
if (recentAssistant && lastMsg === recentAssistant) {
clearHistoryPoll();
set({ sending: false, activeRunId: null, pendingFinal: false });
}
}
return true;
};