mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-08 05:18:49 +03:00
fix(webui): preserve automation source on streamed replies
This commit is contained in:
@@ -37,7 +37,7 @@ interface ActiveAssistantCursor {
|
||||
}
|
||||
|
||||
type PendingStreamEvent =
|
||||
| { kind: "delta"; text: string; turn: UIMessageTurnFields }
|
||||
| { kind: "delta"; text: string; turn: UIMessageTurnFields; source?: UIMessage["source"] }
|
||||
| { kind: "reasoning"; text: string; turn: UIMessageTurnFields };
|
||||
|
||||
type UIMessageTurnFields = Pick<UIMessage, "turnId" | "turnPhase" | "turnSeq">;
|
||||
@@ -778,7 +778,12 @@ export function useNanobotStream(
|
||||
}, []);
|
||||
|
||||
const appendAnswerChunk = useCallback(
|
||||
(prev: UIMessage[], chunk: string, turn: UIMessageTurnFields = {}): UIMessage[] => {
|
||||
(
|
||||
prev: UIMessage[],
|
||||
chunk: string,
|
||||
turn: UIMessageTurnFields = {},
|
||||
source?: UIMessage["source"],
|
||||
): UIMessage[] => {
|
||||
let next = prev;
|
||||
let targetIndex = resolveActiveAssistantIndex(next, turn);
|
||||
|
||||
@@ -809,6 +814,7 @@ export function useNanobotStream(
|
||||
content: target.content + chunk,
|
||||
isStreaming: true,
|
||||
...turn,
|
||||
...(source ? { source } : {}),
|
||||
};
|
||||
closedAssistantStreamIdsRef.current.delete(merged.id);
|
||||
activeAssistantRef.current = { id: merged.id, index: targetIndex };
|
||||
@@ -823,7 +829,7 @@ export function useNanobotStream(
|
||||
let next = prev;
|
||||
for (const event of events) {
|
||||
if (event.kind === "delta") {
|
||||
next = appendAnswerChunk(next, event.text, event.turn);
|
||||
next = appendAnswerChunk(next, event.text, event.turn, event.source);
|
||||
} else {
|
||||
if (closeActiveAssistantStream()) clearActivitySegment();
|
||||
next = attachReasoningChunk(
|
||||
@@ -843,6 +849,7 @@ export function useNanobotStream(
|
||||
closeAnswerSegment?: boolean;
|
||||
finalAnswerText?: string;
|
||||
turn?: UIMessageTurnFields;
|
||||
source?: UIMessage["source"];
|
||||
}) => {
|
||||
if (streamFrameRef.current !== null) {
|
||||
window.cancelAnimationFrame(streamFrameRef.current);
|
||||
@@ -855,7 +862,8 @@ export function useNanobotStream(
|
||||
const events = pendingStreamEventsRef.current;
|
||||
const finalAnswerText = options?.finalAnswerText;
|
||||
const turn = options?.turn ?? {};
|
||||
if (events.length === 0 && finalAnswerText === undefined) {
|
||||
const source = options?.source;
|
||||
if (events.length === 0 && finalAnswerText === undefined && source === undefined) {
|
||||
if (options?.closeAnswerSegment) closeActiveAssistantStream();
|
||||
return;
|
||||
}
|
||||
@@ -873,6 +881,7 @@ export function useNanobotStream(
|
||||
content: finalAnswerText,
|
||||
isStreaming: true,
|
||||
...turn,
|
||||
...(source ? { source } : {}),
|
||||
};
|
||||
next = replaceMessageAt(next, targetIndex, merged);
|
||||
if (!options?.closeAnswerSegment) {
|
||||
@@ -890,6 +899,7 @@ export function useNanobotStream(
|
||||
content: finalAnswerText,
|
||||
isStreaming: true,
|
||||
...turn,
|
||||
...(source ? { source } : {}),
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
@@ -900,6 +910,18 @@ export function useNanobotStream(
|
||||
buffer.current = { messageId: id };
|
||||
}
|
||||
}
|
||||
} else if (source) {
|
||||
const targetIndex =
|
||||
resolveActiveAssistantIndex(next, turn)
|
||||
?? findStreamingAssistantIndex(next, closedAssistantStreamIdsRef.current, turn);
|
||||
if (targetIndex !== null) {
|
||||
const target = next[targetIndex];
|
||||
next = replaceMessageAt(next, targetIndex, {
|
||||
...target,
|
||||
...turn,
|
||||
source,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (options?.closeAnswerSegment) closeActiveAssistantStream();
|
||||
return next;
|
||||
@@ -1027,6 +1049,7 @@ export function useNanobotStream(
|
||||
kind: "delta",
|
||||
text: chunk,
|
||||
turn: turnFieldsFromEvent(ev, "answer"),
|
||||
source: ev.source,
|
||||
});
|
||||
schedulePendingStreamFlush();
|
||||
return;
|
||||
@@ -1054,6 +1077,7 @@ export function useNanobotStream(
|
||||
closeAnswerSegment: !mergeNext,
|
||||
...(typeof ev.text === "string" ? { finalAnswerText: ev.text } : {}),
|
||||
turn,
|
||||
source: ev.source,
|
||||
});
|
||||
if (suppressStreamUntilTurnEndRef.current) return;
|
||||
if (ev.resuming) {
|
||||
|
||||
@@ -1185,12 +1185,16 @@ export type InboundEvent =
|
||||
chat_id: string;
|
||||
text: string;
|
||||
stream_id?: string;
|
||||
/** Lightweight provenance for proactive streamed assistant messages. */
|
||||
source?: UIMessageSource;
|
||||
} & InboundTurnMetadata)
|
||||
| ({
|
||||
event: "stream_end";
|
||||
chat_id: string;
|
||||
stream_id?: string;
|
||||
text?: string;
|
||||
/** Lightweight provenance for proactive streamed assistant messages. */
|
||||
source?: UIMessageSource;
|
||||
/** This answer segment ended, but the active agent turn will continue. */
|
||||
resuming?: boolean;
|
||||
/** The next answer segment continues this same assistant message. */
|
||||
|
||||
@@ -314,6 +314,63 @@ describe("useNanobotStream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves proactive automation source metadata on streamed assistant messages", () => {
|
||||
const fake = fakeClient();
|
||||
const { result } = renderHook(() => useNanobotStream("chat-cron-stream", EMPTY_MESSAGES), {
|
||||
wrapper: wrap(fake.client),
|
||||
});
|
||||
const source = { kind: "cron", label: "Repo check" };
|
||||
|
||||
act(() => {
|
||||
fake.emit("chat-cron-stream", {
|
||||
event: "delta",
|
||||
chat_id: "chat-cron-stream",
|
||||
text: "Repo ",
|
||||
source,
|
||||
});
|
||||
fake.emit("chat-cron-stream", {
|
||||
event: "stream_end",
|
||||
chat_id: "chat-cron-stream",
|
||||
source,
|
||||
});
|
||||
fake.emit("chat-cron-stream", {
|
||||
event: "turn_end",
|
||||
chat_id: "chat-cron-stream",
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.messages[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: "Repo ",
|
||||
isStreaming: false,
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves proactive automation source metadata on stream_end final text", () => {
|
||||
const fake = fakeClient();
|
||||
const { result } = renderHook(() => useNanobotStream("chat-cron-stream-end", EMPTY_MESSAGES), {
|
||||
wrapper: wrap(fake.client),
|
||||
});
|
||||
const source = { kind: "cron", label: "Repo check" };
|
||||
|
||||
act(() => {
|
||||
fake.emit("chat-cron-stream-end", {
|
||||
event: "stream_end",
|
||||
chat_id: "chat-cron-stream-end",
|
||||
text: "Repo clean.",
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.messages[0]).toMatchObject({
|
||||
role: "assistant",
|
||||
content: "Repo clean.",
|
||||
isStreaming: true,
|
||||
source,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not start streaming from completed trailing activity after an answer", () => {
|
||||
const fake = fakeClient();
|
||||
const initialMessages = [
|
||||
|
||||
Reference in New Issue
Block a user