From 144106e8ac83e567a195e7b6388756150c5b887c Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Wed, 27 May 2026 21:51:26 +0800 Subject: [PATCH 1/2] fix(web): exclude user sketches from turn output attribution (#3089) Filter sketch workspace files out of produced-file inference and pre/post turn diffs so manually saved sketches are not shown under Files from this turn unless the agent explicitly emitted them. --- apps/web/src/components/AssistantMessage.tsx | 9 +++-- apps/web/src/components/ProjectView.tsx | 8 ++-- apps/web/src/produced-files.ts | 12 ++++++ .../components/AssistantMessage.test.tsx | 28 ++++++++++++++ .../ProjectView.reattach-restore.test.tsx | 11 ++++++ apps/web/tests/produced-files.test.ts | 37 +++++++++++++++++++ 6 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/produced-files.ts create mode 100644 apps/web/tests/produced-files.test.ts diff --git a/apps/web/src/components/AssistantMessage.tsx b/apps/web/src/components/AssistantMessage.tsx index 708db93b5..c7ae2c86e 100644 --- a/apps/web/src/components/AssistantMessage.tsx +++ b/apps/web/src/components/AssistantMessage.tsx @@ -51,6 +51,7 @@ import { messageTime, relativeTimeLong, } from "../utils/chatTime"; +import { filterAgentAttributedProjectFiles } from "../produced-files"; import type { AgentEvent, ChatMessage, @@ -679,14 +680,14 @@ function inferProducedFilesFromTurn({ if (fileOps.length > 0) return []; const start = message.startedAt - 1_000; const end = message.endedAt + 60_000; - return projectFiles - .filter((file) => { + return filterAgentAttributedProjectFiles( + projectFiles.filter((file) => { if (file.type === "dir") return false; if (!file.name || file.name.startsWith(".")) return false; if (file.name.includes("/.")) return false; return file.mtime >= start && file.mtime <= end; - }) - .sort((a, b) => b.mtime - a.mtime); + }), + ).sort((a, b) => b.mtime - a.mtime); } function isFeedbackEligible({ diff --git a/apps/web/src/components/ProjectView.tsx b/apps/web/src/components/ProjectView.tsx index 31b68015e..f514dd577 100644 --- a/apps/web/src/components/ProjectView.tsx +++ b/apps/web/src/components/ProjectView.tsx @@ -129,6 +129,7 @@ import type { SkillSummary, } from '../types'; import { historyWithApiAttachmentContext } from '../api-attachment-context'; +import { filterAgentAttributedProjectFiles } from '../produced-files'; import { commentsToAttachments, historyWithCommentAttachmentContext, @@ -2057,7 +2058,8 @@ export function ProjectView({ nextFiles = await refreshProjectFiles(); } } - const diff = nextFiles.filter((f) => !beforeFileNames.has(f.name)); + const diff = + computeProducedFiles(beforeFileNames, nextFiles) ?? []; const produced = mergeRecoveredArtifact(diff, recoveredExistingArtifact); if (produced.length > 0) { updateMessageById( @@ -2607,7 +2609,7 @@ export function ProjectView({ await persistArtifact(parsedArtifact, nextFiles); nextFiles = await refreshProjectFiles(); } - const produced = nextFiles.filter((f) => !beforeFileNames.has(f.name)); + const produced = computeProducedFiles(beforeFileNames, nextFiles) ?? []; setMessages((curr) => { const updated = curr.map((m) => m.id === assistantId @@ -4778,7 +4780,7 @@ export function computeProducedFiles( ): ProjectFile[] | undefined { if (!beforeNames) return undefined; const set = beforeNames instanceof Set ? beforeNames : new Set(beforeNames); - return next.filter((f) => !set.has(f.name)); + return filterAgentAttributedProjectFiles(next.filter((f) => !set.has(f.name))); } // Reattach with a recovered (on-disk) artifact must still include any diff --git a/apps/web/src/produced-files.ts b/apps/web/src/produced-files.ts new file mode 100644 index 000000000..97fcebe51 --- /dev/null +++ b/apps/web/src/produced-files.ts @@ -0,0 +1,12 @@ +import type { ProjectFile } from './types'; + +/** User-drawn sketch workspace files are not agent turn outputs (#3089). */ +export function isUserSketchProjectFile(file: ProjectFile): boolean { + return file.kind === 'sketch'; +} + +export function filterAgentAttributedProjectFiles( + files: readonly ProjectFile[], +): ProjectFile[] { + return files.filter((file) => !isUserSketchProjectFile(file)); +} diff --git a/apps/web/tests/components/AssistantMessage.test.tsx b/apps/web/tests/components/AssistantMessage.test.tsx index b38b58cd7..3d43f8d3d 100644 --- a/apps/web/tests/components/AssistantMessage.test.tsx +++ b/apps/web/tests/components/AssistantMessage.test.tsx @@ -223,4 +223,32 @@ describe('AssistantMessage recovered produced files', () => { expect(screen.getByText('iphone-device-reveal.mp4')).toBeTruthy(); }); + + it('does not infer user sketches as turn output files (#3089)', () => { + render( + , + ); + + expect(screen.queryByText('board.sketch.json')).toBeNull(); + }); }); diff --git a/apps/web/tests/components/ProjectView.reattach-restore.test.tsx b/apps/web/tests/components/ProjectView.reattach-restore.test.tsx index 5fcf68fd7..30c6666a5 100644 --- a/apps/web/tests/components/ProjectView.reattach-restore.test.tsx +++ b/apps/web/tests/components/ProjectView.reattach-restore.test.tsx @@ -151,6 +151,17 @@ describe('computeProducedFiles', () => { expect(produced?.map((f) => f.name)).toEqual(['new.pptx']); }); + it('excludes user sketch files from turn output attribution (#3089)', () => { + const before = ['existing.html']; + const next = [ + { name: 'existing.html', path: '/p/existing.html', size: 1, updatedAt: 0, kind: 'html' }, + { name: 'board.sketch.json', path: '/p/board.sketch.json', size: 2, updatedAt: 0, kind: 'sketch' }, + { name: 'new.pptx', path: '/p/new.pptx', size: 3, updatedAt: 0, kind: 'pdf' }, + ]; + const produced = computeProducedFiles(before, next as never); + expect(produced?.map((f) => f.name)).toEqual(['new.pptx']); + }); + it('returns undefined when no baseline is provided', () => { expect(computeProducedFiles(undefined, [] as never)).toBeUndefined(); }); diff --git a/apps/web/tests/produced-files.test.ts b/apps/web/tests/produced-files.test.ts new file mode 100644 index 000000000..d30621315 --- /dev/null +++ b/apps/web/tests/produced-files.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; + +import { + filterAgentAttributedProjectFiles, + isUserSketchProjectFile, +} from '../src/produced-files'; +import type { ProjectFile } from '../src/types'; + +function file(name: string, kind: ProjectFile['kind']): ProjectFile { + return { + name, + path: name, + size: 1, + mtime: 0, + kind, + }; +} + +describe('isUserSketchProjectFile', () => { + it('returns true for sketch workspace files', () => { + expect(isUserSketchProjectFile(file('board.sketch.json', 'sketch'))).toBe(true); + }); + + it('returns false for agent-generated artifacts', () => { + expect(isUserSketchProjectFile(file('index.html', 'html'))).toBe(false); + }); +}); + +describe('filterAgentAttributedProjectFiles', () => { + it('drops sketch files from turn output attribution (#3089)', () => { + const filtered = filterAgentAttributedProjectFiles([ + file('deck.html', 'html'), + file('notes.sketch.json', 'sketch'), + ]); + expect(filtered.map((entry) => entry.name)).toEqual(['deck.html']); + }); +}); From a094bc13b6a5e84465954b0eb1deb64820a74fb0 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Wed, 27 May 2026 21:56:39 +0800 Subject: [PATCH 2/2] fix(web): add mime field to produced-files test fixtures Fixes Preflight typecheck failure on #3089 PR. --- apps/web/tests/produced-files.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/tests/produced-files.test.ts b/apps/web/tests/produced-files.test.ts index d30621315..9f554fd55 100644 --- a/apps/web/tests/produced-files.test.ts +++ b/apps/web/tests/produced-files.test.ts @@ -13,6 +13,7 @@ function file(name: string, kind: ProjectFile['kind']): ProjectFile { size: 1, mtime: 0, kind, + mime: kind === 'sketch' ? 'application/json' : 'text/html', }; }