diff --git a/docs/features/assistant-workbar/execution-history-storage.md b/docs/features/assistant-workbar/execution-history-storage.md index 82990f3f..8a5e2ff8 100644 --- a/docs/features/assistant-workbar/execution-history-storage.md +++ b/docs/features/assistant-workbar/execution-history-storage.md @@ -42,13 +42,13 @@ 普通未打开会话只返回标题、项目、置顶状态、上下文设置和消息数量/首条角色,不解析或跨 IPC 传输正文、工具、子任务及图片操作元数据。 - 当前会话、现有 KeepAlive 视图,以及 Main 仍持有请求、数据库中仍有活动 Task 或 - streaming 消息的会话返回完整消息。列表包含最近 100 条、较早活动会话、请求中的已缓存 - 会话及全部置顶会话,按置顶优先、组内更新时间倒序返回;仅置顶不会加载完整历史。 + streaming 消息的会话返回完整消息。列表包含全部 active 会话,不限制数量, + 按置顶优先、组内更新时间倒序返回;仅置顶不会加载完整历史。 - 打开历史通过 `conversations.get(id)` 读取全部消息,不使用会截断历史的分页上限。 加载时明确显示进度;失败保留会话与输入,提供原地重试。发送、队列派发和压缩上下文 - 必须先确认完整历史;复制与导出临时读取完整内容,不把未打开会话留入视图缓存。 + 必须先确认完整历史,发送时不按消息条数截断;复制与导出临时读取完整内容,不把未打开会话留入视图缓存。 - 搜索由 Main 在 SQLite 中读取标题与正文,不读取过程元数据,保持原来的 Unicode - 大小写匹配和字面子串规则。范围包含 Renderer 已保留的较早会话;前端同时匹配尚未 + 大小写匹配和字面子串规则。范围包含全部 active 会话;前端同时匹配尚未 落库的消息。搜索状态只保留命中的 ID,不额外持有整份历史对象。 - KeepAlive 淘汰后,在现有保存周期中释放已经确认落库且空闲的详情及持久化比较引用。 未保存消息、正在执行的会话、待答/待审批内容不被淘汰。读取中的请求按会话合并; diff --git a/src/main/assistant/assistant-database.test.ts b/src/main/assistant/assistant-database.test.ts index d3d166f1..6b3d5b79 100644 --- a/src/main/assistant/assistant-database.test.ts +++ b/src/main/assistant/assistant-database.test.ts @@ -390,8 +390,8 @@ describe('AssistantDatabase', () => { expect(database.searchConversations(' ')).toEqual([]) const added = { id: randomUUID(), title: 'Newer', updatedAt: 1000 } database.saveLocalConversations([{ header: added, messages: [] }]) - expect(database.listConversationSummaries().some(item => item.id === id)).toBe(false) - expect(database.searchConversations('äpfel 中文 0 %_', [id])).toEqual([id]) + expect(database.listConversationSummaries().some(item => item.id === id)).toBe(true) + expect(database.searchConversations('äpfel 中文 0 %_')).toEqual([id]) expect(database.listConversationSummaries([id]).find(item => item.id === id)?.messages).toHaveLength(16) // A header-only save of a summary cannot replace or erase messages. database.saveLocalConversations([{ header: { id, title: 'Renamed', updatedAt: 101 }, messages: [] }]) @@ -5832,7 +5832,7 @@ describe('AssistantDatabase', () => { database.close() }) - it('gets a targeted conversation outside the latest 100', async () => { + it('lists and searches all conversations beyond the latest 100', async () => { const database = await createDatabase() database.saveLocalConversations( Array.from({ length: 100 }, (_, index) => ({ @@ -5856,12 +5856,14 @@ describe('AssistantDatabase', () => { } ]) - expect(database.listConversations()).toHaveLength(100) + expect(database.listConversations()).toHaveLength(101) expect( database.listConversations().some( (conversation) => conversation.id === oldestId ) - ).toBe(false) + ).toBe(true) + expect(database.listConversationSummaries()).toHaveLength(101) + expect(database.searchConversations('第 101 个对话')).toEqual([oldestId]) expect(database.getConversation(oldestId)).toMatchObject({ id: oldestId, title: '第 101 个对话', diff --git a/src/main/assistant/assistant-database.ts b/src/main/assistant/assistant-database.ts index 235380ce..775db82d 100644 --- a/src/main/assistant/assistant-database.ts +++ b/src/main/assistant/assistant-database.ts @@ -2791,7 +2791,7 @@ export class AssistantDatabase { return this.readConversationList(new Set(detailIds)) } - private listConversationRows(extraIds: ReadonlySet = new Set()): ConversationRow[] { + private listConversationRows(): ConversationRow[] { const database = this.requireDatabase() return database .prepare( @@ -2803,24 +2803,14 @@ export class AssistantDatabase { pinned, updated_at FROM conversations WHERE status = 'active' - AND ( - id IN (SELECT id FROM conversations WHERE status = 'active' - ORDER BY updated_at DESC LIMIT 100) - OR id IN (SELECT conversation_id FROM (${activeVisibleTaskSelect})) - OR id IN (SELECT value FROM json_each(?)) - OR pinned = 1 - OR EXISTS (SELECT 1 FROM messages - WHERE conversation_id = conversations.id - AND state = 'streaming') - ) ORDER BY pinned DESC, updated_at DESC` ) - .all(JSON.stringify([...extraIds])) as ConversationRow[] + .all() as ConversationRow[] } private readConversationList(detailIds?: ReadonlySet): import('../../shared/assistant-contracts').ConversationListSnapshot[] { const database = this.requireDatabase() - const conversations = this.listConversationRows(detailIds) + const conversations = this.listConversationRows() const messageStatement = database.prepare( `SELECT id, conversation_id, role, content, state, metadata_json, created_at @@ -2859,14 +2849,14 @@ export class AssistantDatabase { }) } - searchConversations(query: string, conversationIds: string[] = []): string[] { + searchConversations(query: string): string[] { const normalized = query.trim().toLocaleLowerCase() if (!normalized) return [] const database = this.requireDatabase() // Read only searchable text, never tool/subagent/image metadata. Use the // same Unicode case folding as the renderer's existing search. const messages = database.prepare('SELECT content FROM messages WHERE conversation_id = ?') - return this.listConversationRows(new Set(conversationIds)).filter(conversation => + return this.listConversationRows().filter(conversation => conversation.title.toLocaleLowerCase().includes(normalized) || (messages.all(conversation.id) as { content: string }[]).some(message => message.content.toLocaleLowerCase().includes(normalized)) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 8c0a4f4a..b990e71c 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -7040,8 +7040,8 @@ export function registerIpcHandlers( }) registerHandler(ipcChannels.conversationsSearch, (event, input: unknown) => { assertTrustedSender(event, window) - const { query, conversationIds } = conversationSearchRequestSchema.parse(input) - return assistantDatabase.searchConversations(query, conversationIds) + const { query } = conversationSearchRequestSchema.parse(input) + return assistantDatabase.searchConversations(query) }) registerHandler( diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index 04516f07..18e0a197 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -5628,7 +5628,7 @@ describe("App", () => { }); }); - it("sends all persisted history beyond 500 messages", async () => { + it("retains all history beyond 500 messages across consecutive sends", async () => { const messages = Array.from({ length: 502 }, (_, index) => ({ id: crypto.randomUUID(), role: index % 2 ? "assistant" as const : "user" as const, @@ -5653,6 +5653,22 @@ describe("App", () => { expect(run.mock.calls[0]?.[0].historyMessageIds).toEqual( messages.map(({ id }) => id), ); + const requestId = run.mock.calls[0]![0].requestId; + act(() => { + agentListener?.({ requestId, type: "text", delta: "Continued reply" }); + agentListener?.({ requestId, type: "done" }); + }); + fireEvent.change(screen.getByLabelText("向 GoodBuddy 提问"), { + target: { value: "Continue again" }, + }); + await waitFor(() => expect(screen.getByLabelText("发送")).toBeEnabled()); + fireEvent.click(screen.getByLabelText("发送")); + await waitFor(() => expect(run).toHaveBeenCalledTimes(2)); + expect(run.mock.calls[1]?.[0].history).toEqual([ + ...messages.map(({ role, content }) => ({ role, content })), + { role: "user", content: "Continue" }, + { role: "assistant", content: "Continued reply" }, + ]); }); it("keeps a tool failure in details and hides retry after continuing", async () => { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index d3a22be4..a10662bf 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -7684,7 +7684,7 @@ function App(): React.JSX.Element { ? prompt.slice(0, 24) : conversation.title, updatedAt: Date.now(), - messages: [...conversation.messages.slice(-499), userMessage], + messages: [...conversation.messages, userMessage], } : conversation, ), @@ -7754,7 +7754,7 @@ function App(): React.JSX.Element { ...conversation, updatedAt: Date.now(), messages: [ - ...conversation.messages.slice(-499), + ...conversation.messages, assistantMessage, ], activeRequest: undefined,