Skip to content

Commit 2b75a1e

Browse files
committed
chore: merge review-fix packet (prompt re-send guard)
2 parents f1f589d + a696f61 commit 2b75a1e

3 files changed

Lines changed: 30 additions & 7 deletions

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgentPanel.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,18 @@ export function DashboardAgentPanel({
191191
// Bumped on each open so a slower earlier open can't overwrite a newer one.
192192
const openChatRequestSeq = useRef(0);
193193

194+
// Bound to its chat, which remounts with a fresh guard ref on every switch.
195+
const [sendRequest, setSendRequest] = useState<
196+
{ text: string; seq: number; chatId: string } | undefined
197+
>(undefined);
198+
194199
// The one way the panel changes chat: it invalidates any in-flight open and abandons a
195200
// half-configured watch card, which would otherwise be submitted against the new chat.
196201
const claimChatSlot = useCallback(() => {
197202
dispatchWatchCard({ type: "chat-changed" });
203+
// A request belongs to the chat it was made in: the remounting chat has a fresh guard ref,
204+
// so a kept request would be sent a second time.
205+
setSendRequest(undefined);
198206
return ++openChatRequestSeq.current;
199207
}, []);
200208

@@ -345,10 +353,6 @@ export function DashboardAgentPanel({
345353
onUnreadWorkChange?.(unreadWorkCount(chats, active?.chatId));
346354
}, [chats, chatsLoaded, active?.chatId, onUnreadWorkChange]);
347355

348-
// Bound to its chat, which remounts with a fresh guard ref on every switch.
349-
const [sendRequest, setSendRequest] = useState<
350-
{ text: string; seq: number; chatId: string } | undefined
351-
>(undefined);
352356
const handledRequestSeq = useRef<number | undefined>(undefined);
353357
useEffect(() => {
354358
if (!requestedMessage || handledRequestSeq.current === requestedMessage.seq) return;

apps/webapp/app/components/dashboard-agent/explicit-prompt.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ describe("the panel sends every explicit prompt", () => {
5555
expect(chat).not.toMatch(/prefill/i);
5656
});
5757

58+
it("carries a first prompt on the new chat itself, not on a request that a switch clears", () => {
59+
const effect = panel.slice(panel.indexOf("const target = explicitPromptTarget({"));
60+
expect(effect.indexOf("void createChat(requestedMessage.text);")).toBeLessThan(
61+
effect.indexOf("setSendRequest({ ...requestedMessage")
62+
);
63+
expect(panel).toContain("pendingFirstMessage: data.headStarted ? undefined : text,");
64+
});
65+
66+
it("drops the request when the chat slot changes, so switching back cannot re-send it", () => {
67+
const claim = panel.slice(
68+
panel.indexOf("const claimChatSlot = useCallback(() => {"),
69+
panel.indexOf("const openChat = useCallback(")
70+
);
71+
expect(claim).toContain("setSendRequest(undefined);");
72+
});
73+
5874
it("submits the request in the chat rather than typing it into the composer", () => {
5975
expect(chat).toContain("submit(sendRequest.text);");
6076
expect(chat).not.toContain("setInput(sendRequest.text)");

apps/webapp/app/components/dashboard-agent/watch-card-state.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,13 @@ describe("every chat change goes through one door", () => {
9494
it("bumps the open sequence in exactly one place, next to the card reset", () => {
9595
const bumps = panel.match(/openChatRequestSeq\.current\s*(\+\+|\+=)|\+\+openChatRequestSeq/g);
9696
expect(bumps).toHaveLength(1);
97-
// Whitespace-tolerant: the formatter is free to reindent or rewrap these two lines.
98-
expect(panel).toMatch(
99-
/dispatchWatchCard\(\{\s*type:\s*"chat-changed",?\s*\}\);\s*return \+\+openChatRequestSeq\.current;/
97+
const claim = panel.slice(
98+
panel.indexOf("const claimChatSlot = useCallback(() => {"),
99+
panel.indexOf("const openChat = useCallback(")
100100
);
101+
// Whitespace-tolerant: the formatter is free to reindent or rewrap the call.
102+
expect(claim).toMatch(/dispatchWatchCard\(\{\s*type:\s*"chat-changed",?\s*\}\);/);
103+
expect(claim).toContain("return ++openChatRequestSeq.current;");
101104
});
102105

103106
it("claims a slot before every setActive that lands in a different chat", () => {

0 commit comments

Comments
 (0)