fix(webui): bind landing first message to created chat
This commit is contained in:
@@ -339,6 +339,7 @@ export function ThreadShell({
|
|||||||
const filePreviewWidthRef = useRef(FILE_PREVIEW_DEFAULT_WIDTH);
|
const filePreviewWidthRef = useRef(FILE_PREVIEW_DEFAULT_WIDTH);
|
||||||
const filePreviewCloseTimerRef = useRef<number | null>(null);
|
const filePreviewCloseTimerRef = useRef<number | null>(null);
|
||||||
const pendingFirstRef = useRef<PendingFirstMessage | null>(null);
|
const pendingFirstRef = useRef<PendingFirstMessage | null>(null);
|
||||||
|
const [pendingFirstTargetChatId, setPendingFirstTargetChatId] = useState<string | null>(null);
|
||||||
const viewportRef = useRef<ThreadViewportHandle | null>(null);
|
const viewportRef = useRef<ThreadViewportHandle | null>(null);
|
||||||
const messageCacheRef = useRef<Map<string, UIMessage[]>>(new Map());
|
const messageCacheRef = useRef<Map<string, UIMessage[]>>(new Map());
|
||||||
/** Last chatId we associated with the in-memory thread (for cache-on-switch). */
|
/** Last chatId we associated with the in-memory thread (for cache-on-switch). */
|
||||||
@@ -555,15 +556,22 @@ export function ThreadShell({
|
|||||||
messageCacheRef.current.set(chatId, projectWebuiThreadMessages(messages));
|
messageCacheRef.current.set(chatId, projectWebuiThreadMessages(messages));
|
||||||
}, [chatId, loading, messages]);
|
}, [chatId, loading, messages]);
|
||||||
|
|
||||||
|
// The landing composer queues the first message while `new_chat` is in flight.
|
||||||
|
// Only the chat created for that send may consume it; selecting another chat
|
||||||
|
// while creation is pending must not leak the message there.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chatId) return;
|
if (!chatId || pendingFirstTargetChatId !== chatId) return;
|
||||||
const pending = pendingFirstRef.current;
|
const pending = pendingFirstRef.current;
|
||||||
if (!pending) return;
|
if (!pending) {
|
||||||
|
setPendingFirstTargetChatId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
pendingFirstRef.current = null;
|
pendingFirstRef.current = null;
|
||||||
|
setPendingFirstTargetChatId(null);
|
||||||
setScrollToLatestUserPromptSignal((value) => value + 1);
|
setScrollToLatestUserPromptSignal((value) => value + 1);
|
||||||
send(pending.content, pending.images, pending.options);
|
send(pending.content, pending.images, pending.options);
|
||||||
setBooting(false);
|
setBooting(false);
|
||||||
}, [chatId, send]);
|
}, [chatId, pendingFirstTargetChatId, send]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -585,11 +593,15 @@ export function ThreadShell({
|
|||||||
if (booting) return;
|
if (booting) return;
|
||||||
setBooting(true);
|
setBooting(true);
|
||||||
pendingFirstRef.current = { content, images, options: withWorkspaceScope(options) };
|
pendingFirstRef.current = { content, images, options: withWorkspaceScope(options) };
|
||||||
|
setPendingFirstTargetChatId(null);
|
||||||
const newId = await onCreateChat?.(workspaceScope);
|
const newId = await onCreateChat?.(workspaceScope);
|
||||||
if (!newId) {
|
if (!newId) {
|
||||||
pendingFirstRef.current = null;
|
pendingFirstRef.current = null;
|
||||||
|
setPendingFirstTargetChatId(null);
|
||||||
setBooting(false);
|
setBooting(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
setPendingFirstTargetChatId(newId);
|
||||||
},
|
},
|
||||||
[booting, onCreateChat, withWorkspaceScope, workspaceScope],
|
[booting, onCreateChat, withWorkspaceScope, workspaceScope],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -507,6 +507,71 @@ describe("ThreadShell", () => {
|
|||||||
expect(onNewChat).not.toHaveBeenCalled();
|
expect(onNewChat).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("binds a pending landing message to the chat created for it", async () => {
|
||||||
|
const client = makeClient();
|
||||||
|
let resolveCreate: ((chatId: string) => void) | null = null;
|
||||||
|
const onCreateChat = vi.fn(() => new Promise<string>((resolve) => {
|
||||||
|
resolveCreate = resolve;
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
wrap(
|
||||||
|
client,
|
||||||
|
<ThreadShell
|
||||||
|
session={null}
|
||||||
|
title="nanobot"
|
||||||
|
onToggleSidebar={() => {}}
|
||||||
|
onCreateChat={onCreateChat}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText("Message input"), {
|
||||||
|
target: { value: "must not leak" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||||
|
|
||||||
|
await waitFor(() => expect(onCreateChat).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
rerender(
|
||||||
|
wrap(
|
||||||
|
client,
|
||||||
|
<ThreadShell
|
||||||
|
session={session("existing-chat")}
|
||||||
|
title="Existing chat"
|
||||||
|
onToggleSidebar={() => {}}
|
||||||
|
onCreateChat={onCreateChat}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
resolveCreate?.("chat-new");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.sendMessage).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
rerender(
|
||||||
|
wrap(
|
||||||
|
client,
|
||||||
|
<ThreadShell
|
||||||
|
session={session("chat-new")}
|
||||||
|
title="New chat"
|
||||||
|
onToggleSidebar={() => {}}
|
||||||
|
onCreateChat={onCreateChat}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expectSendMessageWithTurn(client, "chat-new", "must not leak"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the first landing message when new chat history is still empty", async () => {
|
it("keeps the first landing message when new chat history is still empty", async () => {
|
||||||
const client = makeClient();
|
const client = makeClient();
|
||||||
const onCreateChat = vi.fn().mockResolvedValue("chat-new");
|
const onCreateChat = vi.fn().mockResolvedValue("chat-new");
|
||||||
|
|||||||
Reference in New Issue
Block a user