mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +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,
|
messageTime,
|
||||||
relativeTimeLong,
|
relativeTimeLong,
|
||||||
} from "../utils/chatTime";
|
} from "../utils/chatTime";
|
||||||
|
import { filterAgentAttributedProjectFiles } from "../produced-files";
|
||||||
import type {
|
import type {
|
||||||
AgentEvent,
|
AgentEvent,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
|
|
@ -698,14 +699,14 @@ function inferProducedFilesFromTurn({
|
||||||
if (fileOps.length > 0) return [];
|
if (fileOps.length > 0) return [];
|
||||||
const start = message.startedAt - 1_000;
|
const start = message.startedAt - 1_000;
|
||||||
const end = message.endedAt + 60_000;
|
const end = message.endedAt + 60_000;
|
||||||
return projectFiles
|
return filterAgentAttributedProjectFiles(
|
||||||
.filter((file) => {
|
projectFiles.filter((file) => {
|
||||||
if (file.type === "dir") return false;
|
if (file.type === "dir") return false;
|
||||||
if (!file.name || file.name.startsWith(".")) return false;
|
if (!file.name || file.name.startsWith(".")) return false;
|
||||||
if (file.name.includes("/.")) return false;
|
if (file.name.includes("/.")) return false;
|
||||||
return file.mtime >= start && file.mtime <= end;
|
return file.mtime >= start && file.mtime <= end;
|
||||||
})
|
}),
|
||||||
.sort((a, b) => b.mtime - a.mtime);
|
).sort((a, b) => b.mtime - a.mtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFeedbackEligible({
|
function isFeedbackEligible({
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ import type {
|
||||||
SkillSummary,
|
SkillSummary,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { historyWithApiAttachmentContext } from '../api-attachment-context';
|
import { historyWithApiAttachmentContext } from '../api-attachment-context';
|
||||||
|
import { filterAgentAttributedProjectFiles } from '../produced-files';
|
||||||
import {
|
import {
|
||||||
commentsToAttachments,
|
commentsToAttachments,
|
||||||
historyWithCommentAttachmentContext,
|
historyWithCommentAttachmentContext,
|
||||||
|
|
@ -2108,7 +2109,8 @@ export function ProjectView({
|
||||||
nextFiles = await refreshProjectFiles();
|
nextFiles = await refreshProjectFiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const diff = nextFiles.filter((f) => !beforeFileNames.has(f.name));
|
const diff =
|
||||||
|
computeProducedFiles(beforeFileNames, nextFiles) ?? [];
|
||||||
const produced = mergeRecoveredArtifact(diff, recoveredExistingArtifact);
|
const produced = mergeRecoveredArtifact(diff, recoveredExistingArtifact);
|
||||||
if (produced.length > 0) {
|
if (produced.length > 0) {
|
||||||
updateMessageById(
|
updateMessageById(
|
||||||
|
|
@ -2667,7 +2669,7 @@ export function ProjectView({
|
||||||
await persistArtifact(parsedArtifact, nextFiles);
|
await persistArtifact(parsedArtifact, nextFiles);
|
||||||
nextFiles = await refreshProjectFiles();
|
nextFiles = await refreshProjectFiles();
|
||||||
}
|
}
|
||||||
const produced = nextFiles.filter((f) => !beforeFileNames.has(f.name));
|
const produced = computeProducedFiles(beforeFileNames, nextFiles) ?? [];
|
||||||
setMessages((curr) => {
|
setMessages((curr) => {
|
||||||
const updated = curr.map((m) =>
|
const updated = curr.map((m) =>
|
||||||
m.id === assistantId
|
m.id === assistantId
|
||||||
|
|
@ -4918,7 +4920,7 @@ export function computeProducedFiles(
|
||||||
): ProjectFile[] | undefined {
|
): ProjectFile[] | undefined {
|
||||||
if (!beforeNames) return undefined;
|
if (!beforeNames) return undefined;
|
||||||
const set = beforeNames instanceof Set ? beforeNames : new Set(beforeNames);
|
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
|
// 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();
|
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']);
|
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', () => {
|
it('returns undefined when no baseline is provided', () => {
|
||||||
expect(computeProducedFiles(undefined, [] as never)).toBeUndefined();
|
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