diff --git a/src/main/assistant/assistant-database.test.ts b/src/main/assistant/assistant-database.test.ts index 106ed4f..6a58d06 100644 --- a/src/main/assistant/assistant-database.test.ts +++ b/src/main/assistant/assistant-database.test.ts @@ -974,19 +974,6 @@ describe('AssistantDatabase', () => { status: 'completed', completedAt: expect.any(String) }) - const artifact = database.createTextArtifact({ - projectId: project.id, - taskId, - title: '发布说明', - content: '# 发布说明\n\n内容' - }) - expect(database.listArtifacts(project.id)).toEqual([ - expect.objectContaining({ - id: artifact.id, - kind: 'markdown', - content: '# 发布说明\n\n内容' - }) - ]) const memory = database.createMemory({ scope: 'project', scopeId: project.id, @@ -2205,6 +2192,99 @@ describe('AssistantDatabase', () => { durable.close() }) + it('keeps duplicate chat replies out of artifact listings', async () => { + const database = await createDatabase() + const project = database.listProjects()[0]! + const chatTaskId = '00000000-0000-4000-8000-000000000231' + const channelTaskId = '00000000-0000-4000-8000-000000000232' + const scheduleTaskId = '00000000-0000-4000-8000-000000000233' + const delegationTaskId = '00000000-0000-4000-8000-000000000234' + database.createTask({ + id: chatTaskId, + projectId: project.id, + conversationId: 'conversation-chat', + title: '普通对话', + instructions: '回答问题', + workMode: 'ask' + }) + database.createTask({ + id: channelTaskId, + projectId: project.id, + conversationId: 'conversation-channel', + title: '远程对话', + instructions: '回答远程消息', + workMode: 'ask', + origin: 'delegation' + }) + database.createTask({ + id: scheduleTaskId, + projectId: project.id, + conversationId: 'schedule:daily', + title: '每日报告', + instructions: '生成报告', + workMode: 'ask', + origin: 'schedule' + }) + database.createTask({ + id: delegationTaskId, + projectId: project.id, + conversationId: 'delegation:weekly-report', + title: '委派报告', + instructions: '生成远程委派报告', + workMode: 'ask', + origin: 'delegation' + }) + const chatReply = database.createTextArtifact({ + projectId: project.id, + taskId: chatTaskId, + title: '普通对话', + content: '普通回复' + }) + const channelReply = database.createTextArtifact({ + projectId: project.id, + taskId: channelTaskId, + title: '远程对话', + content: '远程回复' + }) + const scheduledReport = database.createTextArtifact({ + projectId: project.id, + taskId: scheduleTaskId, + title: '每日报告', + content: '# 每日报告' + }) + const delegatedReport = database.createTextArtifact({ + projectId: project.id, + taskId: delegationTaskId, + title: '委派报告', + content: '# 委派报告' + }) + const generatedImage = database.createImageArtifact({ + projectId: project.id, + taskId: chatTaskId, + title: '生成图片', + mimeType: 'image/png', + base64: 'iVBORw0KGgo=' + }) + + const visibleArtifactIds = database + .listArtifacts(project.id) + .map((artifact) => artifact.id) + expect(visibleArtifactIds).toEqual( + expect.arrayContaining([ + scheduledReport.id, + delegatedReport.id, + generatedImage.id + ]) + ) + expect(visibleArtifactIds).not.toContain(chatReply.id) + expect(visibleArtifactIds).not.toContain(channelReply.id) + expect(database.getArtifact(chatReply.id)).toMatchObject({ + id: chatReply.id, + content: '普通回复' + }) + database.close() + }) + it('loads image artifact content only when requested by id', async () => { const database = await createDatabase() const artifact = database.createInlineArtifact({ diff --git a/src/main/assistant/assistant-database.ts b/src/main/assistant/assistant-database.ts index 078120d..a4bee40 100644 --- a/src/main/assistant/assistant-database.ts +++ b/src/main/assistant/assistant-database.ts @@ -3166,11 +3166,25 @@ export class AssistantDatabase { CASE WHEN kind = 'image' THEN NULL ELSE inline_content END AS inline_content, byte_size, created_at, updated_at` + const visibleArtifact = `NOT ( + kind = 'markdown' AND EXISTS ( + SELECT 1 + FROM tasks + WHERE tasks.id = artifacts.task_id + AND tasks.conversation_id IS NOT NULL + AND ( + tasks.origin = 'user' OR ( + tasks.origin = 'delegation' + AND tasks.conversation_id NOT LIKE 'delegation:%' + ) + ) + ) + )` const rows = projectId ? this.requireDatabase() .prepare( `SELECT ${columns} FROM artifacts - WHERE project_id = ? + WHERE project_id = ? AND ${visibleArtifact} ORDER BY created_at DESC LIMIT ?` ) @@ -3178,6 +3192,7 @@ export class AssistantDatabase { : this.requireDatabase() .prepare( `SELECT ${columns} FROM artifacts + WHERE ${visibleArtifact} ORDER BY created_at DESC LIMIT ?` ) diff --git a/src/main/ipc.test.ts b/src/main/ipc.test.ts index f0fd530..c282c52 100644 --- a/src/main/ipc.test.ts +++ b/src/main/ipc.test.ts @@ -3213,12 +3213,7 @@ describe('registerIpcHandlers agent terminal state', () => { ).toBe(expectedOutput) expect( harness.assistantDatabase.createTextArtifact - ).toHaveBeenCalledWith( - expect.objectContaining({ - taskId: requestId, - content: expectedOutput - }) - ) + ).not.toHaveBeenCalled() await harness.dispose() }) @@ -4663,6 +4658,9 @@ describe('registerIpcHandlers agent terminal state', () => { 'base64' ).toString('utf8') ).toBe('# 本周报告\n\n已完成。') + expect( + harness.assistantDatabase.createTextArtifact + ).not.toHaveBeenCalled() await harness.dispose() }) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index e320ed9..10bc696 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1551,7 +1551,7 @@ export function registerIpcHandlers( }) } } - if (output.trim()) { + if (origin !== 'channel' && output.trim()) { assistantDatabase.createTextArtifact({ projectId: schedule.projectId, taskId: requestId, @@ -1568,7 +1568,9 @@ export function registerIpcHandlers( body: origin === 'channel' ? '结果已回复,并保存到远程通道会话。' - : '结果已保存到 GoodBuddy 成果工作栏。' + : origin === 'schedule' + ? '结果已保存到 GoodBuddy 成果工作栏。' + : '结果已保存到成果工作栏和委派记录。' }) return { status: 'completed', @@ -2413,7 +2415,6 @@ export function registerIpcHandlers( activeRequests.set(request.requestId, controller) const execution = (async () => { - let outputText = '' let completed = false let runtimeErrorEvent: | Extract @@ -2784,15 +2785,6 @@ export function registerIpcHandlers( .slice(0, 120) }) : agentEvent - if ( - publicEvent.type === 'text' && - outputText.length < 1_000_000 - ) { - outputText += publicEvent.delta.slice( - 0, - 1_000_000 - outputText.length - ) - } if (publicEvent.type === 'tool') { toolStates.set(publicEvent.callId, publicEvent) } @@ -2831,23 +2823,13 @@ export function registerIpcHandlers( } if (publicEvent.type === 'done') { completed = true - if (outputText.trim()) { - assistantDatabase.createTextArtifact({ - projectId: request.projectId, - taskId: request.requestId, - title: parsedRequest.prompt - .split(/\r?\n/, 1)[0]! - .slice(0, 120), - content: outputText - }) - } assistantDatabase.updateTaskStatus( request.requestId, 'completed' ) showDesktopNotificationWhenUnfocused(window, { title: 'GoodBuddy 任务已完成', - body: '任务结果已保存到成果工作栏。' + body: '回复已生成,可返回会话查看。' }) if (!window.isDestroyed()) { window.webContents.send( diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index b67580c..9c7e8ec 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -6640,9 +6640,9 @@ describe('App', () => { screen.getByText(/Agent 打开网页后/) ).toBeInTheDocument() fireEvent.click(screen.getByRole('tab', { name: '成果' })) - expect(screen.getByText('对话与导入成果')).toBeInTheDocument() + expect(screen.getByText('生成与导入成果')).toBeInTheDocument() expect( - screen.getByText(/查看并预览由对话生成或手动导入/) + screen.getByText(/查看并预览由任务、自动化生成或手动导入/) ).toBeInTheDocument() expect( screen.queryByRole('tab', { name: '预览' }) @@ -6651,6 +6651,46 @@ describe('App', () => { expect(sidebar).not.toHaveClass('assistant-sidebar--open') }) + it('keeps completed chat replies out of the results sidebar', async () => { + render() + + fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), { + target: { value: '普通聊天问题' } + }) + fireEvent.click(await screen.findByLabelText('发送')) + await waitFor(() => expect(run).toHaveBeenCalledOnce()) + const request = run.mock.calls[0]?.[0] + if (!request) { + throw new Error('Missing request') + } + act(() => { + agentListener?.({ + requestId: request.requestId, + type: 'text', + delta: '这是一条普通聊天回复' + }) + agentListener?.({ + requestId: request.requestId, + type: 'done' + }) + }) + expect( + await screen.findByText('这是一条普通聊天回复') + ).toBeInTheDocument() + + fireEvent.click(screen.getByLabelText('切换助手工作栏')) + fireEvent.click(screen.getByRole('tab', { name: '成果' })) + const sidebar = screen.getByLabelText('助手工作栏') + expect( + within(sidebar).queryByText('这是一条普通聊天回复') + ).not.toBeInTheDocument() + expect( + within(sidebar).getByText( + '生成的文件、图片、报告和手动导入内容会显示在这里。' + ) + ).toBeInTheDocument() + }) + it('opens the live browser tab for the active conversation and can stop it', async () => { render() diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 59299b4..e503a44 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -2958,8 +2958,8 @@ function App(): React.JSX.Element { [conversations] ) const sidebarArtifacts = useMemo( - () => { - const persisted = assistantArtifacts + () => + assistantArtifacts .filter( (artifact) => !activeProjectId || artifact.projectId === activeProjectId @@ -2970,33 +2970,8 @@ function App(): React.JSX.Element { content: artifact.content ?? '', createdAt: new Date(artifact.createdAt).getTime(), mimeType: artifact.mimeType - })) - if (persisted.length > 0) { - return persisted - } - return (activeConversation?.messages ?? []) - .filter( - (message) => - message.role === 'assistant' && - message.state === 'complete' && - message.content.trim() - ) - .slice(-20) - .reverse() - .map((message, index) => ({ - id: message.id, - title: - message.content - .split(/\r?\n/, 1)[0] - ?.replace(/^#+\s*/, '') - .slice(0, 48) || - t('chat.assistantResult', { index: index + 1 }), - content: message.content, - createdAt: message.createdAt, - mimeType: 'text/markdown' - })) - }, - [activeConversation, activeProjectId, assistantArtifacts, t] + })), + [activeProjectId, assistantArtifacts] ) const enabledSidebarLibraries = useMemo( () => diff --git a/src/renderer/src/i18n/locales/en-US/app.ts b/src/renderer/src/i18n/locales/en-US/app.ts index 23a8ac8..b615c67 100644 --- a/src/renderer/src/i18n/locales/en-US/app.ts +++ b/src/renderer/src/i18n/locales/en-US/app.ts @@ -135,7 +135,6 @@ export const app = { }, chat: { user: 'You', - assistantResult: 'Assistant result {{index}}', welcome: { eyebrow: 'GOODBUDDY WORKSPACE', title: 'What would you like to accomplish today?', diff --git a/src/renderer/src/i18n/locales/en-US/workspace.ts b/src/renderer/src/i18n/locales/en-US/workspace.ts index a4e088d..d1d5490 100644 --- a/src/renderer/src/i18n/locales/en-US/workspace.ts +++ b/src/renderer/src/i18n/locales/en-US/workspace.ts @@ -87,7 +87,7 @@ export const workspace = { }, results: { label: 'Results', - description: 'View generated or imported content' + description: 'View generated or imported standalone results' } }, tasks: { @@ -151,10 +151,11 @@ export const workspace = { title: 'Results', loadingImage: 'Loading image…', description: - 'View and preview text, images, PDFs, and web content generated by conversations or imported manually.', - sectionTitle: 'Conversation and imported results', + 'View and preview text, images, PDFs, and web results generated by tasks or automations, or imported manually.', + sectionTitle: 'Generated and imported results', import: 'Import PDF, image, or web page', - empty: 'Completed responses will appear here as previewable results.' + empty: + 'Generated files, images, reports, and manually imported content will appear here.' }, browser: { title: 'Live browser', diff --git a/src/renderer/src/i18n/locales/zh-CN/app.ts b/src/renderer/src/i18n/locales/zh-CN/app.ts index 40201a3..99210d2 100644 --- a/src/renderer/src/i18n/locales/zh-CN/app.ts +++ b/src/renderer/src/i18n/locales/zh-CN/app.ts @@ -132,7 +132,6 @@ export const app = { }, chat: { user: '用户', - assistantResult: '助手成果 {{index}}', welcome: { eyebrow: 'GOODBUDDY 工作台', title: '今天想一起完成什么?', diff --git a/src/renderer/src/i18n/locales/zh-CN/workspace.ts b/src/renderer/src/i18n/locales/zh-CN/workspace.ts index 85ad293..b74efc1 100644 --- a/src/renderer/src/i18n/locales/zh-CN/workspace.ts +++ b/src/renderer/src/i18n/locales/zh-CN/workspace.ts @@ -83,7 +83,7 @@ export const workspace = { }, results: { label: '成果', - description: '查看对话生成或手动导入的内容' + description: '查看生成或手动导入的独立成果' } }, tasks: { @@ -142,10 +142,10 @@ export const workspace = { title: '成果', loadingImage: '正在加载图片…', description: - '查看并预览由对话生成或手动导入的文本、图片、PDF 与网页内容。', - sectionTitle: '对话与导入成果', + '查看并预览由任务、自动化生成或手动导入的文本、图片、PDF 与网页成果。', + sectionTitle: '生成与导入成果', import: '导入 PDF、图片或网页', - empty: '完成的回复会作为可预览成果显示在这里。' + empty: '生成的文件、图片、报告和手动导入内容会显示在这里。' }, browser: { title: '实时浏览器',