diff --git a/e2e-tests/concurrent_chat.spec.ts b/e2e-tests/concurrent_chat.spec.ts
new file mode 100644
index 0000000..88a1f67
--- /dev/null
+++ b/e2e-tests/concurrent_chat.spec.ts
@@ -0,0 +1,25 @@
+import { test } from "./helpers/test_helper";
+import { expect } from "@playwright/test";
+
+test("concurrent chat", async ({ po }) => {
+ await po.setUp();
+ await po.sendPrompt("tc=chat1 [sleep=medium]", {
+ skipWaitForCompletion: true,
+ });
+ // Need a short wait otherwise the click on Apps tab is ignored.
+ await po.sleep(2_000);
+
+ await po.goToAppsTab();
+ await po.sendPrompt("tc=chat2");
+ await po.snapshotMessages();
+ await po.clickChatActivityButton();
+
+ // Chat #1 will be the last in the list
+ expect(
+ await po.page.getByTestId(`chat-activity-list-item-1`).textContent(),
+ ).toContain("Chat #1");
+ await po.page.getByTestId(`chat-activity-list-item-1`).click();
+ await po.snapshotMessages({ timeout: 12_000 });
+
+ //
+});
diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts
index ec22aee..9e783c1 100644
--- a/e2e-tests/helpers/test_helper.ts
+++ b/e2e-tests/helpers/test_helper.ts
@@ -400,7 +400,8 @@ export class PageObject {
async snapshotMessages({
replaceDumpPath = false,
- }: { replaceDumpPath?: boolean } = {}) {
+ timeout,
+ }: { replaceDumpPath?: boolean; timeout?: number } = {}) {
if (replaceDumpPath) {
// Update page so that "[[dyad-dump-path=*]]" is replaced with a placeholder path
// which is stable across runs.
@@ -417,7 +418,9 @@ export class PageObject {
);
});
}
- await expect(this.page.getByTestId("messages-list")).toMatchAriaSnapshot();
+ await expect(this.page.getByTestId("messages-list")).toMatchAriaSnapshot({
+ timeout,
+ });
}
async approveProposal() {
@@ -461,6 +464,16 @@ export class PageObject {
await this.page.getByTestId(`${mode}-mode-button`).click();
}
+ async clickChatActivityButton() {
+ await this.page.getByTestId("chat-activity-button").click();
+ }
+
+ async snapshotChatActivityList() {
+ await expect(
+ this.page.getByTestId("chat-activity-list"),
+ ).toMatchAriaSnapshot();
+ }
+
async clickRecheckProblems() {
await this.page.getByTestId("recheck-button").click();
}
diff --git a/e2e-tests/snapshots/concurrent_chat.spec.ts_concurrent-chat-1.aria.yml b/e2e-tests/snapshots/concurrent_chat.spec.ts_concurrent-chat-1.aria.yml
new file mode 100644
index 0000000..7c2eb48
--- /dev/null
+++ b/e2e-tests/snapshots/concurrent_chat.spec.ts_concurrent-chat-1.aria.yml
@@ -0,0 +1,8 @@
+- paragraph: tc=chat2
+- paragraph: chat2
+- button:
+ - img
+- img
+- text: less than a minute ago
+- button "Retry":
+ - img
\ No newline at end of file
diff --git a/e2e-tests/snapshots/concurrent_chat.spec.ts_concurrent-chat-2.aria.yml b/e2e-tests/snapshots/concurrent_chat.spec.ts_concurrent-chat-2.aria.yml
new file mode 100644
index 0000000..8d9fa85
--- /dev/null
+++ b/e2e-tests/snapshots/concurrent_chat.spec.ts_concurrent-chat-2.aria.yml
@@ -0,0 +1,8 @@
+- paragraph: tc=chat1 [sleep=medium]
+- paragraph: chat1
+- button:
+ - img
+- img
+- text: less than a minute ago
+- button "Retry":
+ - img
\ No newline at end of file
diff --git a/src/app/TitleBar.tsx b/src/app/TitleBar.tsx
index e91d4cb..a5a587e 100644
--- a/src/app/TitleBar.tsx
+++ b/src/app/TitleBar.tsx
@@ -20,7 +20,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import { PreviewHeader } from "@/components/preview_panel/PreviewHeader";
+import { ActionHeader } from "@/components/preview_panel/ActionHeader";
export const TitleBar = () => {
const [selectedAppId] = useAtom(selectedAppIdAtom);
@@ -97,7 +97,7 @@ export const TitleBar = () => {
{/* Preview Header */}
{location.pathname === "/chat" && (
-
+
)}
diff --git a/src/atoms/chatAtoms.ts b/src/atoms/chatAtoms.ts
index 36aac80..bccb010 100644
--- a/src/atoms/chatAtoms.ts
+++ b/src/atoms/chatAtoms.ts
@@ -2,14 +2,14 @@ import type { Message } from "@/ipc/ipc_types";
import { atom } from "jotai";
import type { ChatSummary } from "@/lib/schemas";
-// Atom to hold the chat history
-export const chatMessagesAtom = atom([]);
-export const chatErrorAtom = atom(null);
+// Per-chat atoms implemented with maps keyed by chatId
+export const chatMessagesByIdAtom = atom