diff --git a/frontend/src/components/workspace/chats/chat-box.tsx b/frontend/src/components/workspace/chats/chat-box.tsx
index a57aa522c..628ed3211 100644
--- a/frontend/src/components/workspace/chats/chat-box.tsx
+++ b/frontend/src/components/workspace/chats/chat-box.tsx
@@ -44,13 +44,20 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
useEffect(() => {
+ const threadArtifacts = Array.isArray(thread.values.artifacts)
+ ? thread.values.artifacts
+ : undefined;
+
if (threadIdRef.current !== threadId) {
threadIdRef.current = threadId;
deselect();
+ setArtifacts([]);
}
// Update artifacts from the current thread
- setArtifacts(thread.values.artifacts);
+ if (threadArtifacts) {
+ setArtifacts(threadArtifacts);
+ }
// DO NOT automatically deselect the artifact when switching threads, because the artifacts auto discovering is not work now.
// if (
@@ -64,9 +71,9 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
autoSelectFirstArtifact
) {
- if (thread?.values?.artifacts?.length > 0) {
+ if (threadArtifacts && threadArtifacts.length > 0) {
setAutoSelectFirstArtifact(false);
- selectArtifact(thread.values.artifacts[0]!);
+ selectArtifact(threadArtifacts[0]!);
}
}
}, [
@@ -149,7 +156,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
- {thread.values.artifacts?.length === 0 ? (
+ {artifacts.length === 0 ? (
}
title="No artifact selected"
@@ -163,7 +170,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
diff --git a/frontend/src/core/threads/types.ts b/frontend/src/core/threads/types.ts
index 2fffc85fd..72ff9aa27 100644
--- a/frontend/src/core/threads/types.ts
+++ b/frontend/src/core/threads/types.ts
@@ -5,7 +5,7 @@ import type { Todo } from "../todos";
export interface AgentThreadState extends Record {
title: string;
messages: Message[];
- artifacts: string[];
+ artifacts?: string[];
todos?: Todo[];
}
diff --git a/frontend/tests/e2e/artifact-stream-state.spec.ts b/frontend/tests/e2e/artifact-stream-state.spec.ts
new file mode 100644
index 000000000..2768bae6c
--- /dev/null
+++ b/frontend/tests/e2e/artifact-stream-state.spec.ts
@@ -0,0 +1,96 @@
+import { expect, test, type Route } from "@playwright/test";
+
+import { mockLangGraphAPI } from "./utils/mock-api";
+
+const THREAD_ID = "00000000-0000-0000-0000-000000003788";
+const RUN_ID = "00000000-0000-0000-0000-000000003789";
+const ARTIFACT_PATH = "/artifact-fixtures/report.md";
+const THREAD_MESSAGES = [
+ {
+ type: "human",
+ id: "msg-human-artifact",
+ content: [{ type: "text", text: "Create a markdown report" }],
+ },
+ {
+ type: "ai",
+ id: "msg-ai-artifact",
+ content: "Created a markdown report.",
+ },
+];
+
+function streamWithoutArtifacts(route: Route) {
+ const events = [
+ {
+ event: "metadata",
+ data: { run_id: RUN_ID, thread_id: THREAD_ID },
+ },
+ {
+ event: "values",
+ data: {
+ messages: [
+ {
+ type: "human",
+ id: "msg-human-artifact-follow-up",
+ content: [{ type: "text", text: "Continue" }],
+ },
+ {
+ type: "ai",
+ id: "msg-ai-artifact-follow-up",
+ content: "Updated response while the artifact list is omitted.",
+ },
+ ],
+ },
+ },
+ ];
+
+ return route.fulfill({
+ status: 200,
+ contentType: "text/event-stream",
+ body: events
+ .map((event) => {
+ return `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`;
+ })
+ .join(""),
+ });
+}
+
+test("keeps artifact trigger after stream values omit artifacts", async ({
+ page,
+}) => {
+ mockLangGraphAPI(page, {
+ threads: [
+ {
+ thread_id: THREAD_ID,
+ title: "Artifact stream state",
+ artifacts: [ARTIFACT_PATH],
+ messages: THREAD_MESSAGES,
+ },
+ ],
+ });
+
+ await page.route("**/api/langgraph/threads/*/runs/stream", (route) => {
+ return streamWithoutArtifacts(route);
+ });
+
+ await page.goto(`/workspace/chats/${THREAD_ID}`);
+
+ const artifactTrigger = page.getByRole("button", { name: /artifacts/i });
+ await expect(artifactTrigger).toBeVisible({ timeout: 15_000 });
+
+ const textarea = page.getByPlaceholder(/how can i assist you/i);
+ await textarea.fill("Continue");
+ await textarea.press("Enter");
+
+ await expect(
+ page.getByText("Updated response while the artifact list is omitted."),
+ ).toBeVisible({ timeout: 10_000 });
+ await expect(artifactTrigger).toBeVisible();
+
+ await artifactTrigger.click();
+
+ const artifactsPanel = page.locator("#artifacts");
+ await expect(artifactsPanel.getByText("report.md")).toBeVisible();
+ await artifactsPanel.getByText("report.md").click();
+
+ await expect(artifactsPanel.getByRole("combobox")).toContainText("report.md");
+});