mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
Merge a094bc13b6 into 53fb175855
This commit is contained in:
commit
c7fe612a98
6 changed files with 99 additions and 7 deletions
|
|
@ -54,6 +54,7 @@ import {
|
|||
messageTime,
|
||||
relativeTimeLong,
|
||||
} from "../utils/chatTime";
|
||||
import { filterAgentAttributedProjectFiles } from "../produced-files";
|
||||
import type {
|
||||
AgentEvent,
|
||||
ChatMessage,
|
||||
|
|
@ -698,14 +699,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({
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ import type {
|
|||
SkillSummary,
|
||||
} from '../types';
|
||||
import { historyWithApiAttachmentContext } from '../api-attachment-context';
|
||||
import { filterAgentAttributedProjectFiles } from '../produced-files';
|
||||
import {
|
||||
commentsToAttachments,
|
||||
historyWithCommentAttachmentContext,
|
||||
|
|
@ -2108,7 +2109,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(
|
||||
|
|
@ -2667,7 +2669,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
|
||||
|
|
@ -4918,7 +4920,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
|
||||
|
|
|
|||
12
apps/web/src/produced-files.ts
Normal file
12
apps/web/src/produced-files.ts
Normal file
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -301,4 +301,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(
|
||||
<AssistantMessage
|
||||
message={baseMessage({
|
||||
content: '',
|
||||
events: [
|
||||
{ kind: 'status', label: 'starting', detail: 'Claude' } as ChatMessage['events'][number],
|
||||
{ kind: 'status', label: 'initializing', detail: 'claude-opus' } as ChatMessage['events'][number],
|
||||
],
|
||||
producedFiles: [],
|
||||
})}
|
||||
streaming={false}
|
||||
projectId="proj-1"
|
||||
projectFiles={[
|
||||
{
|
||||
name: 'board.sketch.json',
|
||||
path: 'board.sketch.json',
|
||||
size: 2048,
|
||||
mtime: 1700000004,
|
||||
kind: 'sketch',
|
||||
} as ProjectFile,
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('board.sketch.json')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
38
apps/web/tests/produced-files.test.ts
Normal file
38
apps/web/tests/produced-files.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
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,
|
||||
mime: kind === 'sketch' ? 'application/json' : 'text/html',
|
||||
};
|
||||
}
|
||||
|
||||
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']);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue