Skip to content

Commit 77e65f7

Browse files
committed
fix(webapp): keep an empty chat open instead of showing a new one
Selecting a stored chat with no messages dropped you into a fresh draft, as if the chat had been deleted.
1 parent 3902794 commit 77e65f7

3 files changed

Lines changed: 98 additions & 20 deletions

File tree

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

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { TurnActivity } from "./DashboardAgentMessages";
2020
import { DashboardAgentHeader } from "./DashboardAgentHeader";
2121
import type { DashboardAgentChat as DashboardAgentChatListItem } from "./DashboardAgentHistory";
2222
import type { SuggestedPrompt } from "@internal/dashboard-agent-contracts";
23+
import { resolveOpenedChat, type OpenedChatResponse } from "./opened-chat";
2324
import type { AgentPageContext } from "./page-context-types";
2425
import { agentPageLabel } from "./page-label";
2526
import { AgentPanelColumn } from "./panel-layout";
@@ -158,27 +159,10 @@ export function DashboardAgentPanel({
158159
console.error(`Dashboard agent: failed to open chat ${id} (${res.status})`);
159160
toast.error("We couldn't open that chat. Try again in a moment.");
160161
}
161-
const data = res.ok
162-
? ((await res.json()) as {
163-
messages?: UIMessage[];
164-
session?: { publicAccessToken: string; lastEventId: string | null } | null;
165-
})
166-
: { messages: [], session: null };
162+
const data = res.ok ? ((await res.json()) as OpenedChatResponse) : undefined;
167163
if (seq !== openChatRequestSeq.current) return;
168-
if (data.messages && data.messages.length > 0) {
169-
setActive({
170-
chatId: id,
171-
messages: data.messages,
172-
session: data.session?.publicAccessToken
173-
? {
174-
publicAccessToken: data.session.publicAccessToken,
175-
lastEventId: data.session.lastEventId ?? undefined,
176-
}
177-
: null,
178-
});
179-
} else {
180-
setActive(null);
181-
}
164+
const opened = resolveOpenedChat(id, data);
165+
setActive(opened.kind === "gone" ? null : opened);
182166
} catch (error) {
183167
console.error(`Dashboard agent: failed to open chat ${id}`, error);
184168
toast.error("We couldn't open that chat. Try again in a moment.");
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { UIMessage } from "@ai-sdk/react";
2+
import { describe, expect, it } from "vitest";
3+
import { resolveOpenedChat } from "./opened-chat";
4+
5+
const CHAT_ID = "chat_abc123";
6+
7+
const message: UIMessage = {
8+
id: "msg_1",
9+
role: "user",
10+
parts: [{ type: "text", text: "why did this run fail?" }],
11+
};
12+
13+
describe("resolveOpenedChat", () => {
14+
it("opens a chat that has messages", () => {
15+
const opened = resolveOpenedChat(CHAT_ID, { messages: [message], session: null });
16+
17+
expect(opened).toEqual({ kind: "chat", chatId: CHAT_ID, messages: [message], session: null });
18+
});
19+
20+
it("still opens a chat that exists but has no messages", () => {
21+
const opened = resolveOpenedChat(CHAT_ID, { messages: [], session: null });
22+
23+
expect(opened.kind).toBe("chat");
24+
expect(opened).toEqual({ kind: "chat", chatId: CHAT_ID, messages: [], session: null });
25+
});
26+
27+
it("treats a chat with no messages field the same way", () => {
28+
expect(resolveOpenedChat(CHAT_ID, {}).kind).toBe("chat");
29+
});
30+
31+
it("reports a chat the server would not return as gone", () => {
32+
expect(resolveOpenedChat(CHAT_ID, undefined)).toEqual({ kind: "gone" });
33+
});
34+
35+
it("carries the session through, dropping a null last event id", () => {
36+
const opened = resolveOpenedChat(CHAT_ID, {
37+
messages: [message],
38+
session: { publicAccessToken: "pat_1", lastEventId: null },
39+
});
40+
41+
expect(opened).toMatchObject({ session: { publicAccessToken: "pat_1" } });
42+
expect(opened.kind === "chat" && opened.session?.lastEventId).toBeUndefined();
43+
});
44+
45+
it("keeps a last event id when there is one", () => {
46+
const opened = resolveOpenedChat(CHAT_ID, {
47+
messages: [],
48+
session: { publicAccessToken: "pat_1", lastEventId: "evt_9" },
49+
});
50+
51+
expect(opened).toMatchObject({ session: { publicAccessToken: "pat_1", lastEventId: "evt_9" } });
52+
});
53+
54+
it("has no session when the token is missing", () => {
55+
expect(resolveOpenedChat(CHAT_ID, { messages: [message] })).toMatchObject({ session: null });
56+
});
57+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { UIMessage } from "@ai-sdk/react";
2+
3+
export type OpenedChatResponse = {
4+
messages?: UIMessage[];
5+
session?: { publicAccessToken: string; lastEventId: string | null } | null;
6+
};
7+
8+
export type OpenedChat =
9+
| {
10+
kind: "chat";
11+
chatId: string;
12+
messages: UIMessage[];
13+
session: { publicAccessToken: string; lastEventId?: string } | null;
14+
}
15+
// Deleted, or belonging to someone else: the read failed, so there is no chat to show.
16+
| { kind: "gone" };
17+
18+
/** An empty transcript is still a chat, so only a failed read drops you into a new one. */
19+
export function resolveOpenedChat(
20+
chatId: string,
21+
response: OpenedChatResponse | undefined
22+
): OpenedChat {
23+
if (!response) return { kind: "gone" };
24+
25+
const session = response.session;
26+
return {
27+
kind: "chat",
28+
chatId,
29+
messages: response.messages ?? [],
30+
session: session?.publicAccessToken
31+
? {
32+
publicAccessToken: session.publicAccessToken,
33+
lastEventId: session.lastEventId ?? undefined,
34+
}
35+
: null,
36+
};
37+
}

0 commit comments

Comments
 (0)