refactor: simplify cross-session messaging

This commit is contained in:
chengyongru
2026-08-19 01:15:56 +08:00
committed by chengyongru
parent 0e184965e8
commit 251a1ccd40
78 changed files with 1578 additions and 7569 deletions
+22 -42
View File
@@ -33,7 +33,6 @@ import type {
OutboundCliAppMention,
OutboundMcpPresetMention,
OutboundMedia,
SessionHandle,
SessionMention,
GoalStateWsPayload,
MessageDeliveryStatus,
@@ -170,7 +169,6 @@ export interface SendOptions {
cliApps?: OutboundCliAppMention[];
mcpPresets?: OutboundMcpPresetMention[];
sessionMentions?: SessionMention[];
sessionHandles?: SessionHandle[];
quotedContext?: string;
workspaceScope?: WorkspaceScopePayload | null;
sideChannel?: boolean;
@@ -190,7 +188,6 @@ function eventExtendsModelActivity(ev: InboundEvent): boolean {
ev.event === "delta"
|| ev.event === "reasoning_delta"
|| ev.event === "file_edit"
|| ev.event === "session_message"
) return true;
return ev.event === "message"
&& (ev.kind === "tool_hint" || ev.kind === "progress" || ev.kind === "reasoning");
@@ -221,31 +218,27 @@ function transitionTurnDelivery(
return changed ? next : messages;
}
function appendLiveSessionMessage(
function appendProjectedSessionInput(
messages: UIMessage[],
event: Extract<InboundEvent, { event: "session_message" }>,
event: Extract<InboundEvent, { event: "user_message" }>,
): UIMessage[] {
const messageId = event.session_message?.message_id?.trim();
if (!messageId || event.session_message.direction !== "incoming") return messages;
const sessionMessage = event.provenance?.session_message;
const messageId = sessionMessage?.message_id?.trim();
if (!sessionMessage || !messageId) return messages;
if (messages.some((message) => message.sessionMessage?.message_id === messageId)) return messages;
const row: UIMessage = {
id: `session-message:${messageId}`,
role: "user",
content: event.text,
createdAt: Number.isFinite(event.created_at_ms) ? event.created_at_ms : Date.now(),
sessionMessage: event.session_message,
createdAt: typeof event.created_at_ms === "number"
&& Number.isFinite(event.created_at_ms)
? event.created_at_ms
: Date.now(),
sessionMessage,
...turnFieldsFromEvent(event, "user"),
};
const sameTurnIndex = event.turn_id
? messages.findIndex((message) => message.turnId === event.turn_id)
: -1;
if (sameTurnIndex < 0) return [...messages, row];
return [
...messages.slice(0, sameTurnIndex),
row,
...messages.slice(sameTurnIndex),
];
return [...messages, row];
}
export function useNanobotStream(
@@ -675,18 +668,6 @@ export function useNanobotStream(
});
}, [cancelStreamEndTimer, client]);
useEffect(() => {
return client.onRunStatus((updatedChatId, startedAt) => {
if (updatedChatId !== chatId) return;
// Canonical HTTP reconciliation can settle a turn before its delayed
// WebSocket completion frame reaches this mounted thread. The client
// then fences that duplicate frame, so keep the pane-local timer in
// sync with the client's authoritative per-chat run projection.
setRunStartedAt(startedAt);
if (startedAt !== null) setIsStreaming(true);
});
}, [chatId, client]);
// Reset local state when switching chats. Do not reset on every
// ``initialMessages`` update: a brand-new chat can receive an empty/404
// history response after the optimistic first message has already rendered.
@@ -752,6 +733,13 @@ export function useNanobotStream(
}
if (ev.event === "message_accepted") return;
if (ev.event === "user_message") {
if (ev.provenance?.session_message) {
flushPendingStreamEvents({ closeAnswerSegment: true });
clearActivitySegment();
setIsStreaming(true);
setMessages((prev) => appendProjectedSessionInput(prev, ev));
return;
}
setMessages((prev) => {
if (ev.turn_id && prev.some((message) => (
message.role === "user" && message.turnId === ev.turn_id
@@ -766,7 +754,10 @@ export function useNanobotStream(
turnPhase: "user",
turnSeq: 0,
deliveryStatus: "accepted",
createdAt: Date.now(),
createdAt: typeof ev.created_at_ms === "number"
&& Number.isFinite(ev.created_at_ms)
? ev.created_at_ms
: Date.now(),
...(ev.media_urls?.length ? { media: ev.media_urls } : {}),
...(ev.cli_apps?.length ? { cliApps: ev.cli_apps } : {}),
...(ev.mcp_presets?.length ? { mcpPresets: ev.mcp_presets } : {}),
@@ -844,20 +835,12 @@ export function useNanobotStream(
const shouldCloseAnswerBeforeEvent =
ev.event === "file_edit"
|| ev.event === "session_message"
|| (
ev.event === "message"
&& (ev.kind === "tool_hint" || ev.kind === "progress")
);
flushPendingStreamEvents({ closeAnswerSegment: shouldCloseAnswerBeforeEvent });
if (ev.event === "session_message") {
clearActivitySegment();
setIsStreaming(true);
setMessages((prev) => appendLiveSessionMessage(prev, ev));
return;
}
if (ev.event === "reasoning_end") {
if (suppressStreamUntilTurnEndRef.current) return;
setMessages((prev) => closeReasoningStream(prev, Date.now()));
@@ -1188,9 +1171,6 @@ export function useNanobotStream(
...(options?.sessionMentions?.length
? { sessionMentions: options.sessionMentions }
: {}),
...(options?.sessionHandles?.length
? { sessionHandles: options.sessionHandles }
: {}),
},
];
});