fix(webui): render local CLI image artifacts

This commit is contained in:
Xubin Ren
2026-05-24 19:43:20 +08:00
parent 9efdce276f
commit c9ff64fc0f
13 changed files with 461 additions and 10 deletions
+35 -5
View File
@@ -193,6 +193,15 @@ function stampLastAssistantLatency(prev: UIMessage[], latencyMs: number): UIMess
return prev;
}
function findLatestAssistantAnswerIndex(prev: UIMessage[]): number | null {
for (let i = prev.length - 1; i >= 0; i -= 1) {
const m = prev[i];
if (m.role === "assistant" && m.kind !== "trace") return i;
if (m.role === "user") break;
}
return null;
}
function absorbCompleteAssistantMessage(
prev: UIMessage[],
message: Omit<UIMessage, "id" | "role" | "createdAt">,
@@ -489,23 +498,41 @@ export function useNanobotStream(
[appendAnswerChunk, ensureActivitySegmentId],
);
const flushPendingStreamEvents = useCallback((options?: { closeAnswerSegment?: boolean }) => {
const flushPendingStreamEvents = useCallback((options?: {
closeAnswerSegment?: boolean;
finalAnswerText?: string;
}) => {
if (streamFrameRef.current !== null) {
window.cancelAnimationFrame(streamFrameRef.current);
streamFrameRef.current = null;
}
const events = pendingStreamEventsRef.current;
if (events.length === 0) {
const finalAnswerText = options?.finalAnswerText;
if (events.length === 0 && finalAnswerText === undefined) {
if (options?.closeAnswerSegment) closeActiveAssistantStream();
return;
}
pendingStreamEventsRef.current = [];
setMessages((prev) => {
const next = applyPendingStreamEvents(prev, events);
let next = events.length > 0 ? applyPendingStreamEvents(prev, events) : prev;
if (finalAnswerText !== undefined) {
const targetIndex =
resolveActiveAssistantIndex(next)
?? findStreamingAssistantIndex(next, closedAssistantStreamIdsRef.current)
?? findLatestAssistantAnswerIndex(next);
if (targetIndex !== null) {
const target = next[targetIndex];
next = replaceMessageAt(next, targetIndex, {
...target,
content: finalAnswerText,
isStreaming: true,
});
}
}
if (options?.closeAnswerSegment) closeActiveAssistantStream();
return next;
});
}, [applyPendingStreamEvents, closeActiveAssistantStream]);
}, [applyPendingStreamEvents, closeActiveAssistantStream, resolveActiveAssistantIndex]);
const schedulePendingStreamFlush = useCallback(() => {
if (streamFrameRef.current !== null) return;
@@ -583,7 +610,10 @@ export function useNanobotStream(
}
if (ev.event === "stream_end") {
flushPendingStreamEvents({ closeAnswerSegment: true });
flushPendingStreamEvents({
closeAnswerSegment: true,
...(typeof ev.text === "string" ? { finalAnswerText: ev.text } : {}),
});
if (suppressStreamUntilTurnEndRef.current) return;
// stream_end only means the text segment finished — the model may
// still be executing tools. Do NOT reset isStreaming here; the