diff --git a/archives/code/dead-code-batch-2/README.md b/archives/code/dead-code-batch-2/README.md deleted file mode 100644 index c6cd9131b..000000000 --- a/archives/code/dead-code-batch-2/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Dead Code Batch 2 - -- Purpose: archive dead code that no longer participates in the active renderer or main runtime. -- Archived at: 2026-03-15 -- Rationale: static inspection confirmed these files have no live code references and are kept in - source form for precise rollback only. - -## Archived Paths - -- `src/renderer/src/components/NewThreadMock.vue` -- `src/renderer/src/components/mock/MockChatPage.vue` -- `src/renderer/src/components/mock/MockInputBox.vue` -- `src/renderer/src/components/mock/MockInputToolbar.vue` -- `src/renderer/src/components/mock/MockMessageList.vue` -- `src/renderer/src/components/mock/MockStatusBar.vue` -- `src/renderer/src/components/mock/MockTopBar.vue` -- `src/renderer/src/components/mock/MockWelcomePage.vue` -- `src/renderer/src/composables/useMockViewState.ts` -- `src/main/presenter/agentPresenter/tools/questionTool.ts` -- `src/main/presenter/agentPresenter/message/systemEnvPromptBuilder.ts` -- `src/main/presenter/agentPresenter/events.ts` -- `src/main/presenter/agentPresenter/message/index.ts` -- `src/main/presenter/agentPresenter/permission/index.ts` -- `src/main/presenter/agentPresenter/session/index.ts` -- `src/main/presenter/agentPresenter/streaming/index.ts` -- `src/main/presenter/agentPresenter/tool/index.ts` -- `src/main/presenter/agentPresenter/utility/index.ts` -- `src/main/presenter/searchPrompts/index.ts` -- `src/main/presenter/sessionPresenter/persistence/index.ts` -- `src/main/presenter/sessionPresenter/tab/index.ts` - -## Notes - -- This directory is not part of the runtime, build, typecheck, or test target set. -- Restore by moving files back to their original paths if a later audit proves they are still - needed. diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/events.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/events.ts deleted file mode 100644 index 336ce12bb..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/events.ts +++ /dev/null @@ -1 +0,0 @@ -export {} diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/message/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/message/index.ts deleted file mode 100644 index 4be94eb85..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/message/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './messageBuilder' -export * from './messageCompressor' -export * from './messageFormatter' -export * from './messageTruncator' -export * from './systemEnvPromptBuilder' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/message/systemEnvPromptBuilder.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/message/systemEnvPromptBuilder.ts deleted file mode 100644 index c39ec9ffb..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/message/systemEnvPromptBuilder.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - buildRuntimeCapabilitiesPrompt, - buildSystemEnvPrompt, - type BuildSystemEnvPromptOptions -} from '../../../lib/agentRuntime/systemEnvPromptBuilder' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/permission/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/permission/index.ts deleted file mode 100644 index 60da88787..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/permission/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PermissionHandler } from './permissionHandler' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/session/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/session/index.ts deleted file mode 100644 index 44666fa75..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/session/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type { SessionContext, SessionContextResolved, SessionStatus } from './sessionContext' -export { resolveSessionContext } from './sessionResolver' -export type { SessionResolveInput } from './sessionResolver' -export { SessionManager } from './sessionManager' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/streaming/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/streaming/index.ts deleted file mode 100644 index f86db5c7c..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/streaming/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { ContentBufferHandler } from './contentBufferHandler' -export { LLMEventHandler } from './llmEventHandler' -export { StreamGenerationHandler } from './streamGenerationHandler' -export type { GeneratingMessageState } from './types' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/tool/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/tool/index.ts deleted file mode 100644 index 824fcc298..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/tool/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { ToolCallCenter } from './toolCallCenter' -export { ToolRegistry } from './toolRegistry' -export { resolveToolRoute } from './toolRouter' -export type { ToolCallContext } from './toolCallCenter' -export type { ToolRegistryEntry, ToolRegistrySource } from './toolRegistry' -export type { ToolRouteDecision } from './toolRouter' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/tools/questionTool.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/tools/questionTool.ts deleted file mode 100644 index 1979f7342..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/tools/questionTool.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - parseQuestionToolArgs, - QUESTION_TOOL_NAME, - questionToolSchema, - type QuestionToolInput -} from '../../../lib/agentRuntime/questionTool' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/utility/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/utility/index.ts deleted file mode 100644 index 7271098b3..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/agentPresenter/utility/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { UtilityHandler } from './utilityHandler' -export { enhanceSystemPromptWithDateTime } from './promptEnhancer' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/searchPrompts/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/searchPrompts/index.ts deleted file mode 100644 index 5bb802615..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/searchPrompts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './searchPrompts' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/sessionPresenter/persistence/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/sessionPresenter/persistence/index.ts deleted file mode 100644 index 60f29b2b6..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/sessionPresenter/persistence/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ConversationPersister } from './conversationPersister.js' -export { MessagePersister } from './messagePersister.js' diff --git a/archives/code/dead-code-batch-2/src/main/presenter/sessionPresenter/tab/index.ts b/archives/code/dead-code-batch-2/src/main/presenter/sessionPresenter/tab/index.ts deleted file mode 100644 index 30a2de58a..000000000 --- a/archives/code/dead-code-batch-2/src/main/presenter/sessionPresenter/tab/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { TabManager, type ITabAdapter, type TabMetadata } from './tabManager.js' -export { TabAdapter } from './tabAdapter.js' diff --git a/archives/code/dead-code-batch-2/src/renderer/src/components/NewThreadMock.vue b/archives/code/dead-code-batch-2/src/renderer/src/components/NewThreadMock.vue deleted file mode 100644 index 55862f267..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/components/NewThreadMock.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - {{ t('chat.newThread.title') }} - - - - - - - - {{ selectedProject }} - - - - - {{ t('common.project.recent') }} - - - - {{ project.name }} - {{ project.path }} - - - - - - {{ t('common.project.openFolder') }} - - - - - - - - - - - - - - - - - - - diff --git a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockChatPage.vue b/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockChatPage.vue deleted file mode 100644 index fda8eaa27..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockChatPage.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputBox.vue b/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputBox.vue deleted file mode 100644 index 7e2fb8f50..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputBox.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - diff --git a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputToolbar.vue b/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputToolbar.vue deleted file mode 100644 index 7c7d695ad..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputToolbar.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - Attach - - - - - - - - - - - - - - Voice input - - - - - - - - - - - - diff --git a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockMessageList.vue b/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockMessageList.vue deleted file mode 100644 index df483226e..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockMessageList.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - {{ msg.content }} - - - - - - - - - - - - Claude 4 Sonnet - {{ msg.content }} - - - - - - - - diff --git a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockStatusBar.vue b/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockStatusBar.vue deleted file mode 100644 index 017b0b70e..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockStatusBar.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - Claude 4 Sonnet - - - - - - - Claude 4 Sonnet - - - - GPT-4o - - - - Gemini 2.5 Pro - - - - - - - - - - High - - - - - Low - Medium - High - Extra High - - - - - - - - - - {{ t('chat.permissionMode.default') }} - - - - - {{ - t('chat.permissionMode.default') - }} - Restricted - {{ - t('chat.permissionMode.fullAccess') - }} - - - - - - diff --git a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockTopBar.vue b/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockTopBar.vue deleted file mode 100644 index 91ab932ec..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockTopBar.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - {{ projectName }} - - - {{ title }} - - - - - - - - - - Share - - - - - - - - - More - - - - - - - - diff --git a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockWelcomePage.vue b/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockWelcomePage.vue deleted file mode 100644 index b25407d95..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockWelcomePage.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - {{ t('welcome.page.title') }} - - - {{ t('welcome.page.description') }} - - - - - - - {{ t(provider.nameKey) }} - - - - - {{ t('welcome.page.browseProviders') }} - - - - - - - {{ t('welcome.page.connectAgent') }} - - - - - - - - - {{ t('welcome.page.acpTitle') }} - {{ t('welcome.page.acpDescription') }} - - - - - - - - - - - diff --git a/archives/code/dead-code-batch-2/src/renderer/src/composables/useMockViewState.ts b/archives/code/dead-code-batch-2/src/renderer/src/composables/useMockViewState.ts deleted file mode 100644 index ebf8a8cb2..000000000 --- a/archives/code/dead-code-batch-2/src/renderer/src/composables/useMockViewState.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ref, computed } from 'vue' - -const _selectedSessionId = ref(null) -const _selectedSessionTitle = ref('') -const _selectedSessionProject = ref('') -const _showMockWelcome = ref(false) - -export function useMockViewState() { - const selectSession = (id: string | null, title: string = '', projectDir: string = '') => { - _selectedSessionId.value = id - _selectedSessionTitle.value = title - _selectedSessionProject.value = projectDir - } - - return { - mockSessionId: _selectedSessionId, - mockSessionTitle: _selectedSessionTitle, - mockSessionProject: _selectedSessionProject, - showMockWelcome: _showMockWelcome, - isMockChatActive: computed(() => _selectedSessionId.value !== null), - selectSession - } -} diff --git a/archives/code/dead-code-batch-3/README.md b/archives/code/dead-code-batch-3/README.md deleted file mode 100644 index bea975789..000000000 --- a/archives/code/dead-code-batch-3/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Dead Code Batch 3 - -- Purpose: archive retired MCP runtime code that is no longer part of the active in-memory server set. -- Archived at: 2026-03-26 -- Rationale: `meetingServer.ts` has been removed from live MCP registration and default config, but is retained in source form for precise rollback if the feature is rebuilt later. - -## Archived Paths - -- `src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts` - -## Notes - -- This directory is not part of the runtime, build, typecheck, or test target set. -- Restore by moving files back to their original paths only if a future audit proves the retired MCP server is needed again. diff --git a/archives/code/dead-code-batch-3/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts b/archives/code/dead-code-batch-3/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts deleted file mode 100644 index 991bc4f38..000000000 --- a/archives/code/dead-code-batch-3/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Server } from '@modelcontextprotocol/sdk/server/index.js' -import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' -import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' -import { z } from 'zod' -import { zodToJsonSchema } from 'zod-to-json-schema' - -const ParticipantSchema = z - .object({ - tab_id: z.number().optional(), - tab_title: z.string().optional(), - profile: z.string().optional() - }) - .refine( - (data) => { - const hasId = data.tab_id !== undefined && data.tab_id !== -1 - const hasTitle = data.tab_title !== undefined && data.tab_title.trim() !== '' - return (hasId && !hasTitle) || (!hasId && hasTitle) - }, - { - message: - '错误:必须且只能通过 "tab_id" 或 "tab_title" 中的一个来指定参会者,两者不能同时提供,也不能都为空。' - } - ) - -const StartMeetingArgsSchema = z.object({ - participants: z - .array(ParticipantSchema) - .min(2, { message: '会议至少需要两位参会者。' }) - .describe('参会者列表。'), - topic: z.string().describe('会议的核心讨论主题。'), - rounds: z.number().optional().default(3).describe('讨论的轮次数,默认为3轮。') -}) - -export class MeetingServer { - private server: Server - - constructor() { - this.server = new Server( - { name: 'meeting-server', version: '1.0.0' }, - { capabilities: { tools: {} } } - ) - this.setupRequestHandlers() - } - - public startServer(transport: Transport): void { - this.server.connect(transport) - } - - private setupRequestHandlers(): void { - this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [ - { - name: 'start_meeting', - description: - 'Legacy helper. Disabled while the app migrates to the window-native architecture.', - inputSchema: zodToJsonSchema(StartMeetingArgsSchema), - annotations: { - title: 'Start Meeting', - destructiveHint: false - } - } - ] - })) - - this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params - if (name !== 'start_meeting') { - throw new Error(`未知的工具: ${name}`) - } - - try { - StartMeetingArgsSchema.parse(args) - return { - content: [ - { - type: 'text', - text: [ - 'Legacy helper disabled.', - 'Reason: the old multi-tab meeting flow no longer matches the window-native architecture.' - ].join(' ') - } - ], - isError: true - } - } catch (error) { - return { - content: [ - { - type: 'text', - text: `会议启动失败: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - } - } - }) - } -} diff --git a/archives/code/dead-renderer-batch-1/README.md b/archives/code/dead-renderer-batch-1/README.md deleted file mode 100644 index c7d250859..000000000 --- a/archives/code/dead-renderer-batch-1/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Dead Renderer Batch 1 - -- Purpose: archive renderer dead code that is no longer on the active chat path. -- Archived at: 2026-03-15 -- Rationale: static inspection confirmed there are no active references in `src/`, `test/`, or live docs. Files are kept in source form for precise rollback only. - -## Archived Paths - -- `src/renderer/src/components/message/MessageMinimap.vue` -- `src/renderer/src/composables/message/useMessageMinimap.ts` -- `src/renderer/src/components/MessageNavigationSidebar.vue` -- `src/renderer/src/lib/messageRuntimeCache.ts` - -## Notes - -- This directory is not part of the runtime, build, typecheck, or test target set. -- Restore by moving files back to their original paths if a later audit proves they are still needed. diff --git a/archives/code/dead-renderer-batch-1/src/renderer/src/components/MessageNavigationSidebar.vue b/archives/code/dead-renderer-batch-1/src/renderer/src/components/MessageNavigationSidebar.vue deleted file mode 100644 index 15d2fd9e1..000000000 --- a/archives/code/dead-renderer-batch-1/src/renderer/src/components/MessageNavigationSidebar.vue +++ /dev/null @@ -1,366 +0,0 @@ - - - - - {{ t('chat.navigation.title') }} - - - - - - - - - - - - - - - - - - {{ - t('chat.navigation.searchResults', { - count: filteredMessages.length, - total: totalMessages - }) - }} - - - - - - - - - - - - - - - - - - - #{{ index + 1 }} - - - - {{ formatTime(message.timestamp) }} - - - - - - - - - - - - - {{ searchQuery ? t('chat.navigation.noResults') : t('chat.navigation.noMessages') }} - - - - - - - - - - - - - - - - - {{ t('chat.navigation.totalMessages', { count: totalMessages }) }} - - - - - - - - diff --git a/archives/code/dead-renderer-batch-1/src/renderer/src/components/message/MessageMinimap.vue b/archives/code/dead-renderer-batch-1/src/renderer/src/components/message/MessageMinimap.vue deleted file mode 100644 index 2e440aa51..000000000 --- a/archives/code/dead-renderer-batch-1/src/renderer/src/components/message/MessageMinimap.vue +++ /dev/null @@ -1,291 +0,0 @@ - - - - - - - - - - - - - - #{{ getPreviewData(bar.id)?.index }} · {{ getPreviewData(bar.id)?.role }} - - - {{ getPreviewData(bar.id)?.preview }} - - - - - - - - {{ overallContextUsage.toFixed(0) }}% - - - - - - - diff --git a/archives/code/dead-renderer-batch-1/src/renderer/src/composables/message/useMessageMinimap.ts b/archives/code/dead-renderer-batch-1/src/renderer/src/composables/message/useMessageMinimap.ts deleted file mode 100644 index d4f250ec3..000000000 --- a/archives/code/dead-renderer-batch-1/src/renderer/src/composables/message/useMessageMinimap.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ref, readonly, type DeepReadonly } from 'vue' -import type { ScrollInfo } from './types' - -export function useMessageMinimap(scrollInfoRef: DeepReadonly) { - const hoveredMessageId = ref(null) - - const handleHover = (messageId: string | null) => { - hoveredMessageId.value = messageId - } - - return { - hoveredMessageId: readonly(hoveredMessageId), - scrollInfo: scrollInfoRef, - handleHover - } -} diff --git a/archives/code/dead-renderer-batch-1/src/renderer/src/lib/messageRuntimeCache.ts b/archives/code/dead-renderer-batch-1/src/renderer/src/lib/messageRuntimeCache.ts deleted file mode 100644 index 842b7b1e3..000000000 --- a/archives/code/dead-renderer-batch-1/src/renderer/src/lib/messageRuntimeCache.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { Message } from '@shared/chat' - -type DomInfo = { - top: number - height: number -} - -type DomInfoEntry = { - id: string - top: number - height: number -} - -const MAX_CACHE_ENTRIES = 800 - -const messageCache = new Map() -const messageThreadMap = new Map() -const messageDomInfo = new Map() - -const touch = (messageId: string, message: Message) => { - if (!messageCache.has(messageId)) return - messageCache.delete(messageId) - messageCache.set(messageId, message) -} - -const prune = () => { - while (messageCache.size > MAX_CACHE_ENTRIES) { - const oldestId = messageCache.keys().next().value as string | undefined - if (!oldestId) return - messageCache.delete(oldestId) - messageThreadMap.delete(oldestId) - messageDomInfo.delete(oldestId) - } -} - -export const getCachedMessage = (messageId: string): Message | null => { - const message = messageCache.get(messageId) - if (!message) return null - touch(messageId, message) - return message -} - -export const hasCachedMessage = (messageId: string): boolean => { - return messageCache.has(messageId) -} - -export const cacheMessage = (message: Message) => { - messageCache.set(message.id, message) - messageThreadMap.set(message.id, message.conversationId) - touch(message.id, message) - prune() -} - -export const cacheMessages = (messages: Message[]) => { - for (const message of messages) { - cacheMessage(message) - } -} - -export const deleteCachedMessage = (messageId: string) => { - messageCache.delete(messageId) - messageThreadMap.delete(messageId) - messageDomInfo.delete(messageId) -} - -export const clearCachedMessagesForThread = (threadId: string) => { - for (const [messageId, conversationId] of messageThreadMap.entries()) { - if (conversationId === threadId) { - messageCache.delete(messageId) - messageThreadMap.delete(messageId) - messageDomInfo.delete(messageId) - } - } -} - -export const clearMessageCache = () => { - messageCache.clear() - messageThreadMap.clear() - messageDomInfo.clear() -} - -export const setMessageDomInfo = (entries: Array<{ id: string; top: number; height: number }>) => { - for (const entry of entries) { - messageDomInfo.set(entry.id, { top: entry.top, height: entry.height }) - } -} - -export const getMessageDomInfo = (messageId: string): DomInfo | null => { - return messageDomInfo.get(messageId) ?? null -} - -export const getAllMessageDomInfo = (): DomInfoEntry[] => { - return Array.from(messageDomInfo.entries()).map(([id, info]) => ({ - id, - top: info.top, - height: info.height - })) -} - -export const clearMessageDomInfo = () => { - messageDomInfo.clear() -} diff --git a/archives/code/legacy-agentpresenter-retirement/README.md b/archives/code/legacy-agentpresenter-retirement/README.md deleted file mode 100644 index cd30dfa9a..000000000 --- a/archives/code/legacy-agentpresenter-retirement/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Legacy AgentPresenter Retirement - -- Purpose: archive the retired `AgentPresenter` runtime after `newAgentPresenter + deepchatAgentPresenter` became the only live chat execution path. -- Archived at: 2026-03-23 -- Rationale: the remaining legacy runtime wiring, legacy loop compatibility surface, and legacy-only tests were removed from the live tree. Retained ACP/tool/message-formatting helpers were moved to new owner modules before archiving. - -## Archived Paths - -- `src/main/presenter/agentPresenter/` -- `src/shared/types/presenters/agent.presenter.d.ts` -- `test/main/presenter/agentPresenter/` -- `test/main/presenter/sessionPresenter/permissionHandler.test.ts` - -## Notes - -- This directory is not part of the runtime, build, typecheck, or test target set. -- Restore by moving files back to their original paths only if a future audit proves the retired legacy runtime is still needed. diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts deleted file mode 100644 index c4dc8700c..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - BackgroundExecSessionManager, - backgroundExecSessionManager, - type SessionMeta -} from '@/lib/agentRuntime/backgroundExecSessionManager' diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/index.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/index.ts deleted file mode 100644 index fe82c8297..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -export type * from './types' - -export { - AcpProcessManager, - type AcpProcessHandle, - type SessionNotificationHandler, - type PermissionResolver -} from './acpProcessManager' -export { AcpSessionManager, type AcpSessionRecord } from './acpSessionManager' -export { AcpSessionPersistence } from './acpSessionPersistence' -export { buildClientCapabilities, type AcpCapabilityOptions } from './acpCapabilities' -export { AcpMessageFormatter } from './acpMessageFormatter' -export { AcpContentMapper } from './acpContentMapper' -export { - LEGACY_MODEL_CONFIG_ID, - LEGACY_MODE_CONFIG_ID, - createEmptyAcpConfigState, - getAcpConfigOption, - getAcpConfigOptionByCategory, - getAcpConfigOptionLabel, - hasAcpConfigStateData, - getLegacyModeState, - normalizeAcpConfigState, - updateAcpConfigStateValue -} from './acpConfigState' -export { AcpFsHandler } from './acpFsHandler' -export { AcpTerminalManager } from './acpTerminalManager' -export { AgentFileSystemHandler } from './agentFileSystemHandler' -export { AgentToolManager, type AgentToolCallResult } from './agentToolManager' -export { AgentBashHandler } from './agentBashHandler' -export { - BackgroundExecSessionManager, - backgroundExecSessionManager, - type SessionMeta -} from './backgroundExecSessionManager' -export { getShellEnvironment, clearShellEnvironmentCache } from './shellEnvHelper' -export { convertMcpConfigToAcpFormat } from './mcpConfigConverter' -export { filterMcpServersByTransportSupport } from './mcpTransportFilter' diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/shellEnvHelper.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/shellEnvHelper.ts deleted file mode 100644 index 846227653..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/shellEnvHelper.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - clearShellEnvironmentCache, - getShellEnvironment, - getUserShell -} from '@/lib/agentRuntime/shellEnvHelper' diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/agent/index.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/agent/index.ts deleted file mode 100644 index 336ce12bb..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/agent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/index.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/index.ts deleted file mode 100644 index 03b076f5f..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/index.ts +++ /dev/null @@ -1,674 +0,0 @@ -import type { - CONVERSATION, - IAgentPresenter, - IConfigPresenter, - ILlmProviderPresenter, - IMCPPresenter, - ISessionPresenter, - ISkillPresenter, - ISQLitePresenter, - IToolPresenter, - MESSAGE_METADATA -} from '@shared/presenter' -import type { AssistantMessage, AssistantMessageBlock, UserMessageContent } from '@shared/chat' -import { eventBus, SendTarget } from '@/eventbus' -import { STREAM_EVENTS } from '@/events' -import type { SessionContextResolved } from './session/sessionContext' -import type { SessionManager } from './session/sessionManager' -import type { AgentSessionRuntimePort } from './session/sessionRuntimePort' -import type { - AgentMcpRuntimePort, - AgentPermissionRuntimePort, - AgentPromptRuntimePort -} from './runtimePorts' -import { MessageManager } from '../sessionPresenter/managers/messageManager' -import type { ThreadHandlerContext } from './types/handlerContext' -import { CommandPermissionService } from '../permission/commandPermissionService' -import { ContentBufferHandler } from './streaming/contentBufferHandler' -import { LLMEventHandler } from './streaming/llmEventHandler' -import { StreamGenerationHandler } from './streaming/streamGenerationHandler' -import type { GeneratingMessageState } from './streaming/types' -import { StreamUpdateScheduler } from './streaming/streamUpdateScheduler' -import { ToolCallHandler } from './loop/toolCallHandler' -import { PermissionHandler } from './permission/permissionHandler' -import { UtilityHandler } from './utility/utilityHandler' - -type AgentPresenterDependencies = { - sessionPresenter: ISessionPresenter - sessionManager: SessionManager - sqlitePresenter: ISQLitePresenter - llmProviderPresenter: ILlmProviderPresenter - configPresenter: IConfigPresenter - mcpPresenter: IMCPPresenter - skillPresenter: ISkillPresenter - toolPresenter: IToolPresenter - commandPermissionService: CommandPermissionService - permissionRuntime: AgentPermissionRuntimePort - messageManager?: MessageManager -} - -export class AgentPresenter implements IAgentPresenter { - private sessionPresenter: ISessionPresenter - private sessionManager: SessionManager - private sqlitePresenter: ISQLitePresenter - private llmProviderPresenter: ILlmProviderPresenter - private configPresenter: IConfigPresenter - private messageManager: MessageManager - private commandPermissionService: CommandPermissionService - private generatingMessages: Map = new Map() - private contentBufferHandler: ContentBufferHandler - private toolCallHandler: ToolCallHandler - private llmEventHandler: LLMEventHandler - private streamGenerationHandler: StreamGenerationHandler - private permissionHandler: PermissionHandler - private utilityHandler: UtilityHandler - private streamUpdateScheduler: StreamUpdateScheduler - private readonly sessionRuntime: AgentSessionRuntimePort - private readonly mcpRuntime: AgentMcpRuntimePort - private readonly promptRuntime: AgentPromptRuntimePort - private readonly permissionRuntime: AgentPermissionRuntimePort - - constructor(options: AgentPresenterDependencies) { - this.sessionPresenter = options.sessionPresenter - this.sessionManager = options.sessionManager - this.sqlitePresenter = options.sqlitePresenter - this.llmProviderPresenter = options.llmProviderPresenter - this.configPresenter = options.configPresenter - this.messageManager = options.messageManager ?? new MessageManager(options.sqlitePresenter) - this.commandPermissionService = options.commandPermissionService - this.permissionRuntime = options.permissionRuntime - this.sessionRuntime = { - getSession: (agentId) => this.sessionManager.getSession(agentId), - getSessionSync: (agentId) => this.sessionManager.getSessionSync(agentId), - resolveWorkspaceContext: (conversationId, modelId) => - this.sessionManager.resolveWorkspaceContext(conversationId, modelId), - startLoop: (agentId, messageId, runtimeOptions) => - this.sessionManager.startLoop(agentId, messageId, runtimeOptions), - setStatus: (agentId, status) => this.sessionManager.setStatus(agentId, status), - getStatus: (agentId) => this.sessionManager.getStatus(agentId), - updateRuntime: (agentId, updates) => this.sessionManager.updateRuntime(agentId, updates), - incrementToolCallCount: (agentId) => this.sessionManager.incrementToolCallCount(agentId), - clearPendingPermission: (agentId) => this.sessionManager.clearPendingPermission(agentId), - clearPendingQuestion: (agentId) => this.sessionManager.clearPendingQuestion(agentId), - addPendingPermission: (agentId, permission) => - this.sessionManager.addPendingPermission(agentId, permission), - removePendingPermission: (agentId, messageId, toolCallId) => - this.sessionManager.removePendingPermission(agentId, messageId, toolCallId), - getPendingPermissions: (agentId) => this.sessionManager.getPendingPermissions(agentId), - hasPendingPermissions: (agentId, messageId) => - this.sessionManager.hasPendingPermissions(agentId, messageId), - acquirePermissionResumeLock: (agentId, messageId) => - this.sessionManager.acquirePermissionResumeLock(agentId, messageId), - releasePermissionResumeLock: (agentId) => - this.sessionManager.releasePermissionResumeLock(agentId), - getPermissionResumeLock: (agentId) => this.sessionManager.getPermissionResumeLock(agentId) - } - this.mcpRuntime = { - callTool: (request) => options.mcpPresenter.callTool(request), - grantPermission: (serverName, permissionType, remember, conversationId) => - options.mcpPresenter.grantPermission(serverName, permissionType, remember, conversationId), - isServerRunning: (serverName) => options.mcpPresenter.isServerRunning(serverName) - } - this.promptRuntime = { - getInputChatMode: async () => - options.configPresenter.getSetting('input_chatMode') ?? undefined, - getSkillsEnabled: () => options.configPresenter.getSkillsEnabled(), - getActiveSkills: (conversationId) => options.skillPresenter.getActiveSkills(conversationId), - loadSkillContent: (name) => options.skillPresenter.loadSkillContent(name), - getMetadataPrompt: () => options.skillPresenter.getMetadataPrompt(), - getActiveSkillsAllowedTools: (conversationId) => - options.skillPresenter.getActiveSkillsAllowedTools(conversationId) - } - - this.streamUpdateScheduler = new StreamUpdateScheduler({ - messageManager: this.messageManager - }) - - const handlerContext: ThreadHandlerContext = { - sqlitePresenter: this.sqlitePresenter, - messageManager: this.messageManager, - llmProviderPresenter: this.llmProviderPresenter, - configPresenter: this.configPresenter, - sessionRuntime: this.sessionRuntime, - toolPresenter: options.toolPresenter, - mcpRuntime: this.mcpRuntime, - promptRuntime: this.promptRuntime, - permissionRuntime: this.permissionRuntime - } - - this.contentBufferHandler = new ContentBufferHandler({ - generatingMessages: this.generatingMessages, - streamUpdateScheduler: this.streamUpdateScheduler - }) - - this.toolCallHandler = new ToolCallHandler({ - sqlitePresenter: this.sqlitePresenter, - commandPermissionHandler: this.commandPermissionService, - streamUpdateScheduler: this.streamUpdateScheduler - }) - - this.llmEventHandler = new LLMEventHandler({ - generatingMessages: this.generatingMessages, - messageManager: this.messageManager, - contentBufferHandler: this.contentBufferHandler, - toolCallHandler: this.toolCallHandler, - streamUpdateScheduler: this.streamUpdateScheduler, - sessionRuntime: this.sessionRuntime, - onConversationUpdated: (state) => this.handleConversationUpdates(state) - }) - - this.streamGenerationHandler = new StreamGenerationHandler(handlerContext, { - generatingMessages: this.generatingMessages, - llmEventHandler: this.llmEventHandler - }) - - this.permissionHandler = new PermissionHandler(handlerContext, { - generatingMessages: this.generatingMessages, - streamGenerationHandler: this.streamGenerationHandler, - llmEventHandler: this.llmEventHandler, - commandPermissionHandler: this.commandPermissionService - }) - - this.utilityHandler = new UtilityHandler(handlerContext, { - getActiveConversation: (webContentsId) => - this.sessionPresenter.getActiveConversation(webContentsId), - getActiveConversationId: (webContentsId) => - this.sessionPresenter.getActiveConversationId(webContentsId), - createConversation: (title, settings, webContentsId) => - this.sessionPresenter.createConversation(title, settings, webContentsId) - }) - - // Legacy IPC surface: dynamic proxy for ISessionPresenter methods. - this.bindSessionPresenterMethods() - } - - async sendMessage( - agentId: string, - content: string, - webContentsId?: number, - selectedVariantsMap?: Record - ): Promise { - await this.logResolvedIfEnabled(agentId) - - const conversation = await this.sessionPresenter.getConversation(agentId) - const userMessage = await this.messageManager.sendMessage( - agentId, - content, - 'user', - '', - false, - this.buildMessageMetadata(conversation) - ) - try { - await this.resolvePendingQuestionIfNeeded(agentId, userMessage.id, content) - } catch (error) { - console.warn('[AgentPresenter] Failed to auto-resolve pending question:', error) - } - - const assistantMessage = await this.streamGenerationHandler.generateAIResponse( - agentId, - userMessage.id - ) - - this.trackGeneratingMessage(assistantMessage, agentId, webContentsId) - await this.updateConversationAfterUserMessage(agentId) - // Normal flow: skip lock acquisition (lock is only for permission resume) - await this.sessionManager.startLoop(agentId, assistantMessage.id, { skipLockAcquisition: true }) - - void this.streamGenerationHandler - .startStreamCompletion(agentId, assistantMessage.id, selectedVariantsMap) - .catch((error) => { - console.error('[AgentPresenter] Failed to start stream completion:', error) - const errorMessage = error instanceof Error ? error.message : String(error) - eventBus.sendToRenderer(STREAM_EVENTS.ERROR, SendTarget.ALL_WINDOWS, { - eventId: assistantMessage.id, - error: errorMessage - }) - }) - - return assistantMessage - } - - async continueLoop( - agentId: string, - messageId: string, - selectedVariantsMap?: Record - ): Promise { - await this.logResolvedIfEnabled(agentId) - - const assistantMessage = await this.createContinueMessage(agentId) - if (!assistantMessage) { - return null - } - - this.trackGeneratingMessage(assistantMessage, agentId) - await this.updateConversationAfterUserMessage(agentId) - // Normal flow: skip lock acquisition (lock is only for permission resume) - await this.sessionManager.startLoop(agentId, assistantMessage.id, { skipLockAcquisition: true }) - - void this.streamGenerationHandler - .continueStreamCompletion(agentId, messageId, selectedVariantsMap) - .catch((error) => { - console.error('[AgentPresenter] Failed to continue stream completion:', error) - const errorMessage = error instanceof Error ? error.message : String(error) - eventBus.sendToRenderer(STREAM_EVENTS.ERROR, SendTarget.ALL_WINDOWS, { - eventId: assistantMessage.id, - error: errorMessage - }) - }) - - return assistantMessage - } - - async cancelLoop(messageId: string): Promise { - try { - const message = await this.sessionPresenter.getMessage(messageId) - if (message) { - this.sessionManager.updateRuntime(message.conversationId, { userStopRequested: true }) - this.sessionManager.setStatus(message.conversationId, 'paused') - } - } catch (error) { - console.warn('[AgentPresenter] Failed to update session state for cancel:', error) - } - await this.stopMessageGeneration(messageId) - } - - async cleanupConversation(conversationId: string): Promise { - for (const [messageId, state] of this.generatingMessages) { - if (state.conversationId === conversationId) { - await this.stopMessageGeneration(messageId) - break - } - } - - this.sessionManager.removeSession(conversationId) - - try { - await this.llmProviderPresenter.clearAcpSession(conversationId) - } catch (error) { - console.warn('[AgentPresenter] Failed to clear ACP session:', error) - } - } - - async retryMessage( - messageId: string, - selectedVariantsMap?: Record - ): Promise { - const message = await this.messageManager.getMessage(messageId) - if (message.role !== 'assistant') { - throw new Error('只能重试助手消息') - } - - const userMessage = await this.messageManager.getMessage(message.parentId || '') - if (!userMessage) { - throw new Error('找不到对应的用户消息') - } - - const conversation = await this.sessionPresenter.getConversation(message.conversationId) - const assistantMessage = (await this.messageManager.retryMessage( - messageId, - this.buildMessageMetadata(conversation) - )) as AssistantMessage - - this.trackGeneratingMessage(assistantMessage, message.conversationId) - // Normal flow: skip lock acquisition (lock is only for permission resume) - await this.sessionManager.startLoop(message.conversationId, assistantMessage.id, { - skipLockAcquisition: true - }) - - void this.streamGenerationHandler - .startStreamCompletion(message.conversationId, messageId, selectedVariantsMap) - .catch((error) => { - console.error('[AgentPresenter] Failed to retry stream completion:', error) - const errorMessage = error instanceof Error ? error.message : String(error) - eventBus.sendToRenderer(STREAM_EVENTS.ERROR, SendTarget.ALL_WINDOWS, { - eventId: assistantMessage.id, - error: errorMessage - }) - }) - - return assistantMessage - } - - async regenerateFromUserMessage( - agentId: string, - userMessageId: string, - selectedVariantsMap?: Record - ): Promise { - await this.logResolvedIfEnabled(agentId) - return this.streamGenerationHandler.regenerateFromUserMessage( - agentId, - userMessageId, - selectedVariantsMap - ) - } - - async translateText(text: string, webContentsId: number): Promise { - return this.utilityHandler.translateText(text, webContentsId) - } - - async askAI(text: string, webContentsId: number): Promise { - return this.utilityHandler.askAI(text, webContentsId) - } - - async handlePermissionResponse( - messageId: string, - toolCallId: string, - granted: boolean, - permissionType: 'read' | 'write' | 'all' | 'command', - remember?: boolean - ): Promise { - await this.permissionHandler.handlePermissionResponse( - messageId, - toolCallId, - granted, - permissionType, - remember - ) - } - - async resolveQuestion( - messageId: string, - toolCallId: string, - answerText: string, - answerMessageId?: string - ): Promise { - await this.handleQuestionResolution(messageId, toolCallId, { - resolution: 'replied', - answerText, - answerMessageId - }) - } - - async rejectQuestion(messageId: string, toolCallId: string): Promise { - await this.handleQuestionResolution(messageId, toolCallId, { - resolution: 'rejected' - }) - } - - private async handleQuestionResolution( - messageId: string, - toolCallId: string, - payload: { - resolution: 'replied' | 'rejected' - answerText?: string - answerMessageId?: string - } - ): Promise { - if (!messageId || !toolCallId) { - return - } - - const message = await this.messageManager.getMessage(messageId) - if (!message || message.role !== 'assistant') { - throw new Error(`Message not found or not assistant (${messageId})`) - } - - const content = message.content as AssistantMessageBlock[] - const questionBlock = content.find( - (block) => - block.type === 'action' && - block.action_type === 'question_request' && - block.tool_call?.id === toolCallId - ) - - if (!questionBlock) { - throw new Error( - `Question block not found (messageId: ${messageId}, toolCallId: ${toolCallId})` - ) - } - - if (questionBlock.status !== 'pending') { - return - } - - const isReplied = payload.resolution === 'replied' - questionBlock.status = isReplied ? 'success' : 'denied' - questionBlock.extra = { - ...questionBlock.extra, - needsUserAction: false, - questionResolution: payload.resolution, - ...(isReplied && payload.answerText ? { answerText: payload.answerText } : {}), - ...(isReplied && payload.answerMessageId ? { answerMessageId: payload.answerMessageId } : {}) - } - - const generatingState = this.generatingMessages.get(messageId) - if (generatingState) { - const questionIndex = generatingState.message.content.findIndex( - (block) => - block.type === 'action' && - block.action_type === 'question_request' && - block.tool_call?.id === toolCallId - ) - if (questionIndex !== -1) { - const stateBlock = generatingState.message.content[questionIndex] - generatingState.message.content[questionIndex] = { - ...stateBlock, - ...questionBlock, - extra: questionBlock.extra ? { ...questionBlock.extra } : undefined, - tool_call: questionBlock.tool_call ? { ...questionBlock.tool_call } : undefined - } - } - } - - await this.messageManager.editMessage(messageId, JSON.stringify(content)) - if (message.status === 'pending') { - await this.messageManager.updateMessageStatus(messageId, 'sent') - } - this.sessionRuntime.clearPendingQuestion(message.conversationId) - this.sessionRuntime.setStatus(message.conversationId, 'idle') - } - - private async resolvePendingQuestionIfNeeded( - conversationId: string, - userMessageId: string, - rawContent: string - ): Promise { - const session = await this.sessionManager.getSession(conversationId) - const pendingQuestion = session.runtime?.pendingQuestion - if (!pendingQuestion?.messageId || !pendingQuestion.toolCallId) { - return - } - - const answerText = this.extractUserMessageText(rawContent) - if (!answerText.trim()) { - return - } - - await this.handleQuestionResolution(pendingQuestion.messageId, pendingQuestion.toolCallId, { - resolution: 'replied', - answerText, - answerMessageId: userMessageId - }) - } - - private extractUserMessageText(rawContent: string): string { - if (!rawContent) return '' - try { - const parsed = JSON.parse(rawContent) as UserMessageContent - if (typeof parsed.text === 'string') { - return parsed.text - } - if (Array.isArray(parsed.content)) { - return parsed.content.map((block) => block.content || '').join('') - } - } catch (error) { - console.warn('[AgentPresenter] Failed to parse user message content:', error) - } - return rawContent - } - - private buildMessageMetadata(conversation: CONVERSATION): MESSAGE_METADATA { - const { providerId, modelId } = conversation.settings - return { - contextUsage: 0, - totalTokens: 0, - generationTime: 0, - firstTokenTime: 0, - tokensPerSecond: 0, - inputTokens: 0, - outputTokens: 0, - model: modelId, - provider: providerId - } - } - - private trackGeneratingMessage( - message: AssistantMessage, - conversationId: string, - webContentsId?: number - ): void { - this.generatingMessages.set(message.id, { - message, - conversationId, - startTime: Date.now(), - firstTokenTime: null, - promptTokens: 0, - reasoningStartTime: null, - reasoningEndTime: null, - lastReasoningTime: null, - webContentsId - }) - } - - private async updateConversationAfterUserMessage(conversationId: string): Promise { - const { list: messages } = await this.messageManager.getMessageThread(conversationId, 1, 2) - if (messages.length === 1) { - await this.sqlitePresenter.updateConversation(conversationId, { - is_new: 0, - updatedAt: Date.now() - }) - return - } - - await this.sqlitePresenter.updateConversation(conversationId, { - updatedAt: Date.now() - }) - } - - private async createContinueMessage(agentId: string): Promise { - const continuePayload = JSON.stringify({ - text: 'continue', - files: [], - links: [], - search: false, - think: false, - continue: true - }) - - const conversation = await this.sessionPresenter.getConversation(agentId) - const userMessage = await this.messageManager.sendMessage( - agentId, - continuePayload, - 'user', - '', - false, - this.buildMessageMetadata(conversation) - ) - - return this.streamGenerationHandler.generateAIResponse(agentId, userMessage.id) - } - - private async handleConversationUpdates(state: GeneratingMessageState): Promise { - const conversation = await this.sessionPresenter.getConversation(state.conversationId) - - if (conversation.is_new === 1) { - try { - const title = await this.sessionPresenter.generateTitle(state.conversationId) - await this.sessionPresenter.renameConversation(state.conversationId, title) - } catch (error) { - console.error('[AgentPresenter] Failed to summarize title', { - conversationId: state.conversationId, - err: error - }) - } - } - - await this.sqlitePresenter.updateConversation(state.conversationId, { - updatedAt: Date.now() - }) - - const sessionPresenter = this.sessionPresenter as unknown as { - broadcastThreadListUpdate?: () => Promise - } - if (sessionPresenter.broadcastThreadListUpdate) { - await sessionPresenter.broadcastThreadListUpdate() - } - } - - private async stopMessageGeneration(messageId: string): Promise { - const state = this.generatingMessages.get(messageId) - if (!state) { - return - } - - this.sessionManager.updateRuntime(state.conversationId, { userStopRequested: true }) - this.sessionManager.setStatus(state.conversationId, 'paused') - this.sessionManager.clearPendingPermission(state.conversationId) - this.sessionManager.clearPendingQuestion(state.conversationId) - state.isCancelled = true - - if (state.adaptiveBuffer) { - await this.contentBufferHandler.flushAdaptiveBuffer(messageId) - } - - this.contentBufferHandler.cleanupContentBuffer(state) - - state.message.content.forEach((block) => { - if ( - block.status === 'loading' || - block.status === 'reading' || - block.status === 'optimizing' - ) { - block.status = 'success' - } - }) - - state.message.content.push({ - type: 'error', - content: 'common.error.userCanceledGeneration', - status: 'cancel', - timestamp: Date.now() - }) - - await this.messageManager.updateMessageStatus(messageId, 'error') - await this.messageManager.editMessage(messageId, JSON.stringify(state.message.content)) - await this.llmProviderPresenter.stopStream(messageId) - - this.generatingMessages.delete(messageId) - } - - private shouldLogResolved(): boolean { - return import.meta.env.VITE_AGENT_PRESENTER_DEBUG === '1' - } - - private async logResolvedIfEnabled(agentId: string): Promise { - if (!this.shouldLogResolved()) { - return - } - try { - const resolved = await this.resolveSession(agentId) - console.log('[AgentPresenter] SessionContext.resolved', { agentId, resolved }) - } catch (error) { - console.warn('[AgentPresenter] Failed to resolve session context', { agentId, error }) - } - } - - private async resolveSession(agentId: string): Promise { - const session = await this.sessionManager.getSession(agentId) - return session.resolved - } - - private bindSessionPresenterMethods(): void { - const sessionPresenter = this.sessionPresenter as unknown as Record - const sessionProto = Object.getPrototypeOf(sessionPresenter) as Record - for (const key of Object.getOwnPropertyNames(sessionProto)) { - if (key === 'constructor') continue - if (key in this) continue - const value = sessionPresenter[key] - if (typeof value === 'function') { - ;(this as Record)[key] = value.bind(this.sessionPresenter) - } - } - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts deleted file mode 100644 index 42f1e0440..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts +++ /dev/null @@ -1,669 +0,0 @@ -import { - ChatMessage, - IConfigPresenter, - IToolPresenter, - LLMAgentEvent, - MCPToolCall -} from '@shared/presenter' -import { BaseLLMProvider } from '@/presenter/llmProviderPresenter/baseProvider' -import { StreamState } from './loopState' -import { RateLimitManager } from '@/presenter/llmProviderPresenter/managers/rateLimitManager' -import { ToolCallProcessor } from './toolCallProcessor' -import { getAgentFilteredTools } from '../../mcpPresenter/agentMcpFilter' -import type { AgentSessionRuntimePort } from '../session/sessionRuntimePort' - -interface AgentLoopHandlerOptions { - configPresenter: IConfigPresenter - getProviderInstance: (providerId: string) => BaseLLMProvider - activeStreams: Map - canStartNewStream: () => boolean - rateLimitManager: RateLimitManager - sessionRuntime: Pick - getToolPresenter: () => IToolPresenter -} - -export class AgentLoopHandler { - private readonly toolCallProcessor: ToolCallProcessor - private currentSupportsVision = false - - constructor(private readonly options: AgentLoopHandlerOptions) { - this.toolCallProcessor = new ToolCallProcessor({ - getAllToolDefinitions: async (context) => { - // Get modelId from session - let modelId: string | undefined - if (context.conversationId) { - try { - const session = await this.options.sessionRuntime.getSession(context.conversationId) - modelId = session?.resolved.modelId - } catch { - // Ignore errors, modelId will be undefined - } - } - - const { chatMode, agentWorkspacePath } = await this.resolveWorkspaceContext( - context.conversationId, - modelId - ) - - const toolDefs = await this.getToolPresenter().getAllToolDefinitions({ - enabledMcpTools: context.enabledMcpTools, - chatMode, - supportsVision: this.currentSupportsVision, - agentWorkspacePath, - conversationId: context.conversationId - }) - - return await this.filterToolsForChatMode(toolDefs, chatMode, modelId) - }, - callTool: async (request: MCPToolCall) => { - return await this.getToolPresenter().callTool(request) - }, - preCheckToolPermission: async (request: MCPToolCall) => { - const toolPresenter = this.getToolPresenter() - if (!toolPresenter.preCheckToolPermission) { - return null - } - return await toolPresenter.preCheckToolPermission(request) - } - }) - } - - private getToolPresenter(): IToolPresenter { - const toolPresenter = this.options.getToolPresenter() - if (!toolPresenter) { - throw new Error('ToolPresenter is unavailable') - } - return toolPresenter - } - - /** - * Resolve workspace context (chatMode and agentWorkspacePath) for tool definitions - * @param conversationId Optional conversation ID - * @param modelId Optional model ID (required for acp agent mode) - * @returns Resolved workspace context - */ - private async resolveWorkspaceContext( - conversationId?: string, - modelId?: string - ): Promise<{ chatMode: 'agent' | 'acp agent'; agentWorkspacePath: string | null }> { - return this.options.sessionRuntime.resolveWorkspaceContext(conversationId, modelId) - } - - private requiresReasoningField(modelId: string): boolean { - const lower = modelId.toLowerCase() - return ( - lower.includes('deepseek-reasoner') || - lower.includes('kimi-k2-thinking') || - lower.includes('glm-4.7') - ) - } - - private isAgentToolDefinition(tool: { server?: { name: string } }): boolean { - const name = tool.server?.name - return Boolean(name && (name === 'yo-browser' || name.startsWith('agent-'))) - } - - private async filterToolsForChatMode( - tools: Awaited>, - chatMode: 'agent' | 'acp agent', - agentId?: string - ): Promise>> { - if (chatMode !== 'acp agent') return tools - if (!agentId) return [] - - const agentTools = tools.filter((tool) => this.isAgentToolDefinition(tool)) - const mcpTools = tools.filter((tool) => !this.isAgentToolDefinition(tool)) - const filteredMcp = await getAgentFilteredTools( - agentId, - undefined, - mcpTools, - this.options.configPresenter - ) - return [...filteredMcp, ...agentTools] - } - - async *startStreamCompletion( - providerId: string, - initialMessages: ChatMessage[], - modelId: string, - eventId: string, - temperature: number = 0.6, - maxTokens: number = 4096, - enabledMcpTools?: string[], - thinkingBudget?: number, - reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high', - verbosity?: 'low' | 'medium' | 'high', - conversationId?: string - ): AsyncGenerator { - console.log(`[Agent Loop] Starting agent loop for event: ${eventId} with model: ${modelId}`) - if (!this.options.canStartNewStream()) { - // Instead of throwing, yield an error event - yield { type: 'error', data: { eventId, error: 'Maximum concurrent stream limit reached' } } - return - // throw new Error('Maximum concurrent stream limit reached') - } - - const provider = this.options.getProviderInstance(providerId) - const abortController = new AbortController() - const modelConfig = this.options.configPresenter.getModelConfig(modelId, providerId) - - if (conversationId) { - modelConfig.conversationId = conversationId - } - - if (thinkingBudget !== undefined) { - modelConfig.thinkingBudget = thinkingBudget - } - if (reasoningEffort !== undefined) { - modelConfig.reasoningEffort = reasoningEffort - } - if (verbosity !== undefined) { - modelConfig.verbosity = verbosity - } - this.currentSupportsVision = Boolean(modelConfig?.vision) - - this.options.activeStreams.set(eventId, { - isGenerating: true, - providerId, - modelId, - abortController, - provider - }) - - // Agent Loop Variables - const conversationMessages: ChatMessage[] = [...initialMessages] - let needContinueConversation = true - let toolCallCount = 0 - const MAX_TOOL_CALLS = BaseLLMProvider.getMaxToolCalls() - const totalUsage: { - prompt_tokens: number - completion_tokens: number - total_tokens: number - context_length: number - } = { - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0, - context_length: modelConfig?.contextLength || 0 - } - - try { - // --- Agent Loop --- - while (needContinueConversation) { - if (abortController.signal.aborted) { - console.log('Agent loop aborted for event:', eventId) - break - } - - if (toolCallCount >= MAX_TOOL_CALLS) { - console.warn('Maximum tool call limit reached for event:', eventId) - yield { - type: 'response', - data: { - eventId, - maximum_tool_calls_reached: true - } - } - - break - } - - needContinueConversation = false - - // Prepare for LLM call - let currentContent = '' - let currentReasoning = '' - const currentToolCalls: Array<{ - id: string - name: string - arguments: string - }> = [] - const currentToolChunks: Record< - string, - { - name: string - arguments_chunk: string - server_name?: string - server_icons?: string - server_description?: string - } - > = {} - - try { - console.log(`[Agent Loop] Iteration ${toolCallCount + 1} for event: ${eventId}`) - // Resolve workspace context - const { chatMode, agentWorkspacePath } = await this.resolveWorkspaceContext( - conversationId, - modelId - ) - - // Get all tool definitions using ToolPresenter - const toolDefs = await this.getToolPresenter().getAllToolDefinitions({ - enabledMcpTools, - chatMode, - supportsVision: this.currentSupportsVision, - agentWorkspacePath, - conversationId - }) - const filteredToolDefs = await this.filterToolsForChatMode(toolDefs, chatMode, modelId) - - const canExecute = this.options.rateLimitManager.canExecuteImmediately(providerId) - if (!canExecute) { - const config = this.options.rateLimitManager.getProviderRateLimitConfig(providerId) - const currentQps = this.options.rateLimitManager.getCurrentQps(providerId) - const queueLength = this.options.rateLimitManager.getQueueLength(providerId) - - yield { - type: 'response', - data: { - eventId, - rate_limit: { - providerId, - qpsLimit: config.qpsLimit, - currentQps, - queueLength, - estimatedWaitTime: Math.max(0, 1000 - (Date.now() % 1000)) - } - } - } - - await this.options.rateLimitManager.executeWithRateLimit(providerId) - } else { - await this.options.rateLimitManager.executeWithRateLimit(providerId) - } - - // Call the provider's core stream method, expecting LLMCoreStreamEvent - const stream = provider.coreStream( - conversationMessages, - modelId, - modelConfig, - temperature, - maxTokens, - filteredToolDefs - ) - - // Process the standardized stream events - for await (const chunk of stream) { - if (abortController.signal.aborted) { - break - } - // console.log('presenter chunk', JSON.stringify(chunk), currentContent) - - // --- Event Handling (using LLMCoreStreamEvent structure) --- - switch (chunk.type) { - case 'text': - if (chunk.content) { - currentContent += chunk.content - yield { - type: 'response', - data: { - eventId, - content: chunk.content - } - } - } - break - case 'reasoning': - if (chunk.reasoning_content) { - currentReasoning += chunk.reasoning_content - yield { - type: 'response', - data: { - eventId, - reasoning_content: chunk.reasoning_content - } - } - } - break - case 'tool_call_start': - if (chunk.tool_call_id && chunk.tool_call_name) { - const toolDef = filteredToolDefs.find( - (tool) => tool.function.name === chunk.tool_call_name - ) - const serverName = toolDef?.server?.name - const serverIcons = toolDef?.server?.icons - const serverDescription = toolDef?.server?.description - - currentToolChunks[chunk.tool_call_id] = { - name: chunk.tool_call_name, - arguments_chunk: '', - server_name: serverName, - server_icons: serverIcons, - server_description: serverDescription - } - // Immediately send the start event to indicate the tool call has begun - yield { - type: 'response', - data: { - eventId, - tool_call: 'start', - tool_call_id: chunk.tool_call_id, - tool_call_name: chunk.tool_call_name, - tool_call_params: '', // Initial parameters are empty - tool_call_server_name: serverName, - tool_call_server_icons: serverIcons, - tool_call_server_description: serverDescription - } - } - } - break - case 'tool_call_chunk': - if ( - chunk.tool_call_id && - currentToolChunks[chunk.tool_call_id] && - chunk.tool_call_arguments_chunk - ) { - currentToolChunks[chunk.tool_call_id].arguments_chunk += - chunk.tool_call_arguments_chunk - - // Send update event to update parameter content in real-time - yield { - type: 'response', - data: { - eventId, - tool_call: 'update', - tool_call_id: chunk.tool_call_id, - tool_call_name: currentToolChunks[chunk.tool_call_id].name, - tool_call_params: currentToolChunks[chunk.tool_call_id].arguments_chunk, - tool_call_server_name: currentToolChunks[chunk.tool_call_id].server_name, - tool_call_server_icons: currentToolChunks[chunk.tool_call_id].server_icons, - tool_call_server_description: - currentToolChunks[chunk.tool_call_id].server_description - } - } - } - break - case 'tool_call_end': - if (chunk.tool_call_id && currentToolChunks[chunk.tool_call_id]) { - const completeArgs = - chunk.tool_call_arguments_complete ?? - currentToolChunks[chunk.tool_call_id].arguments_chunk - const toolCallName = currentToolChunks[chunk.tool_call_id].name - const serverName = currentToolChunks[chunk.tool_call_id].server_name - const serverIcons = currentToolChunks[chunk.tool_call_id].server_icons - const serverDescription = currentToolChunks[chunk.tool_call_id].server_description - - // For ACP provider, tool call execution is completed on agent side - // The tool_call_arguments_complete contains the execution result - // So we should immediately send 'end' event to mark it as successful - if (providerId === 'acp') { - // For ACP, tool_call_arguments_complete contains the execution result - // Use it directly as the response - yield { - type: 'response', - data: { - eventId, - tool_call: 'end', - tool_call_id: chunk.tool_call_id, - tool_call_name: toolCallName, - tool_call_params: completeArgs, - tool_call_response: completeArgs, - tool_call_server_name: serverName, - tool_call_server_icons: serverIcons, - tool_call_server_description: serverDescription - } - } - - // Don't add to currentToolCalls for ACP - execution already completed - delete currentToolChunks[chunk.tool_call_id] - } else { - // For non-ACP providers, tool call needs to be executed by ToolCallProcessor - currentToolCalls.push({ - id: chunk.tool_call_id, - name: toolCallName, - arguments: completeArgs - }) - - // Send final update event to ensure parameter completeness - yield { - type: 'response', - data: { - eventId, - tool_call: 'update', - tool_call_id: chunk.tool_call_id, - tool_call_name: toolCallName, - tool_call_params: completeArgs, - tool_call_server_name: serverName, - tool_call_server_icons: serverIcons, - tool_call_server_description: serverDescription - } - } - - delete currentToolChunks[chunk.tool_call_id] - } - } - break - case 'permission': { - const permission = chunk.permission - const permissionType = permission.permissionType ?? 'read' - const description = permission.description ?? '' - const toolName = permission.tool_call_name ?? permission.tool_call_id - const serverName = - permission.server_name ?? permission.agentName ?? permission.providerName ?? '' - - yield { - type: 'response', - data: { - eventId, - tool_call: 'permission-required', - tool_call_id: permission.tool_call_id, - tool_call_name: toolName, - tool_call_params: permission.tool_call_params, - tool_call_server_name: serverName, - tool_call_server_icons: permission.server_icons, - tool_call_server_description: - permission.server_description ?? permission.agentName, - tool_call_response: description, - permission_request: { - toolName, - serverName, - permissionType, - description, - providerId: permission.providerId, - requestId: permission.requestId, - sessionId: permission.sessionId, - agentId: permission.agentId, - agentName: permission.agentName, - conversationId: permission.conversationId, - options: permission.options, - rememberable: permission.metadata?.rememberable === false ? false : true - } - } - } - break - } - case 'usage': - if (chunk.usage) { - // console.log('usage', chunk.usage, totalUsage) - totalUsage.prompt_tokens += chunk.usage.prompt_tokens - totalUsage.completion_tokens += chunk.usage.completion_tokens - totalUsage.total_tokens += chunk.usage.total_tokens - totalUsage.context_length = modelConfig.contextLength - yield { - type: 'response', - data: { - eventId, - totalUsage: { ...totalUsage } // Yield accumulated usage - } - } - } - break - case 'image_data': - if (chunk.image_data) { - yield { - type: 'response', - data: { - eventId, - image_data: chunk.image_data - } - } - - currentContent += `\n[Image data received: ${chunk.image_data.mimeType}]\n` - } - break - case 'error': - console.error(`Provider stream error for event ${eventId}:`, chunk.error_message) - yield { - type: 'error', - data: { - eventId, - error: chunk.error_message || 'Provider stream error' - } - } - - needContinueConversation = false - break // Break inner loop on provider error - case 'rate_limit': - if (chunk.rate_limit) { - yield { - type: 'response', - data: { - eventId, - rate_limit: chunk.rate_limit - } - } - } - break - case 'stop': - console.log( - `Provider stream stopped for event ${eventId}. Reason: ${chunk.stop_reason}` - ) - if (chunk.stop_reason === 'tool_use') { - // Consolidate any remaining tool call chunks - for (const id in currentToolChunks) { - currentToolCalls.push({ - id: id, - name: currentToolChunks[id].name, - arguments: currentToolChunks[id].arguments_chunk - }) - } - - if (currentToolCalls.length > 0) { - needContinueConversation = true - } else { - console.warn( - `Stop reason was 'tool_use' but no tool calls were fully parsed for event ${eventId}.` - ) - needContinueConversation = false // Don't continue if no tools parsed - } - } else { - needContinueConversation = false - } - // Stop event itself doesn't need to be yielded here, handled by loop logic - break - } - } // End of inner loop (for await...of stream) - - if (abortController.signal.aborted) break // Break outer loop if aborted - - // --- Post-Stream Processing --- - - // 1. Add Assistant Message - const assistantMessage: ChatMessage = { - role: 'assistant', - content: currentContent - } - if ( - this.requiresReasoningField(modelId) && - currentToolCalls.length > 0 && - currentReasoning - ) { - ;(assistantMessage as any).reasoning_content = currentReasoning - } - // Only add if there's content or tool calls are expected - if (currentContent || (needContinueConversation && currentToolCalls.length > 0)) { - conversationMessages.push(assistantMessage) - } - - // 2. Execute Tool Calls if needed - if (needContinueConversation && currentToolCalls.length > 0) { - const processor = this.toolCallProcessor.process({ - eventId, - toolCalls: currentToolCalls, - enabledMcpTools, - conversationMessages, - modelConfig, - providerId, - abortSignal: abortController.signal, - currentToolCallCount: toolCallCount, - maxToolCalls: MAX_TOOL_CALLS, - conversationId - }) - - while (true) { - const { value, done } = await processor.next() - if (done) { - toolCallCount = value.toolCallCount - needContinueConversation = value.needContinueConversation - break - } - yield value - } - - if (abortController.signal.aborted) { - break // Check after tool loop - } - - if (!needContinueConversation) { - // If max tool calls reached or explicit stop, break outer loop - break - } - } else { - // No tool calls needed or requested, end the loop - needContinueConversation = false - } - } catch (error) { - if (abortController.signal.aborted) { - console.log(`Agent loop aborted during inner try-catch for event ${eventId}`) - break // Break outer loop if aborted here - } - console.error(`Agent loop inner error for event ${eventId}:`, error) - yield { - type: 'error', - data: { - eventId, - error: error instanceof Error ? error.message : String(error) - } - } - - needContinueConversation = false // Stop loop on inner error - } - } // --- End of Agent Loop (while) --- - console.log( - `[Agent Loop] Agent loop completed for event: ${eventId}, iterations: ${toolCallCount}` - ) - } catch (error) { - // Catch errors from the generator setup phase (before the loop) - if (abortController.signal.aborted) { - console.log(`Agent loop aborted during outer try-catch for event ${eventId}`) - } else { - console.error(`Agent loop outer error for event ${eventId}:`, error) - yield { - type: 'error', - data: { - eventId, - error: error instanceof Error ? error.message : String(error) - } - } - } - } finally { - // Finalize stream regardless of how the loop ended (completion, error, abort) - const userStop = abortController.signal.aborted - if (!userStop) { - // Yield final aggregated usage if not aborted - yield { - type: 'response', - data: { - eventId, - totalUsage - } - } - } - // Yield the final END event - yield { type: 'end', data: { eventId, userStop } } - - this.options.activeStreams.delete(eventId) - console.log('Agent loop finished for event:', eventId, 'User stopped:', userStop) - } - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/errorClassification.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/errorClassification.ts deleted file mode 100644 index 9c028a96d..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/errorClassification.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Error classification utility to identify non-retryable errors. - * Non-retryable errors should stop the agent loop, while all other errors - * should allow the loop to continue so LLM can decide whether to retry. - */ - -/** - * Checks if an error is non-retryable (should stop the agent loop). - * - * Non-retryable errors are those that won't be resolved by retrying: - * - Invalid input format (invalid URL, malformed JSON, etc.) - * - Explicit permission denied - * - Schema validation failures - * - Authentication errors that can't be resolved by retry - * - Malformed requests that won't work on retry - * - * All other errors (network errors, timeouts, destroyed objects, etc.) - * are considered retryable by default and should allow the loop to continue. - * - * @param error - The error to classify (Error object or string) - * @returns true if the error is non-retryable (should stop loop), false otherwise - */ -export function isNonRetryableError(error: Error | string): boolean { - const errorMessage = error instanceof Error ? error.message : String(error) - const lowerMessage = errorMessage.toLowerCase() - - // Invalid URL format - won't work on retry - if ( - lowerMessage.includes('invalid url') || - lowerMessage.includes('malformed url') || - lowerMessage.includes('url parse error') || - lowerMessage.includes('invalid uri') - ) { - return true - } - - // Invalid JSON format in arguments - won't work on retry - if ( - lowerMessage.includes('invalid json') || - lowerMessage.includes('json parse error') || - lowerMessage.includes('unexpected token') || - lowerMessage.includes('malformed json') - ) { - return true - } - - // Schema validation failures - wrong parameter types, missing required fields - if ( - lowerMessage.includes('schema validation') || - lowerMessage.includes('validation error') || - lowerMessage.includes('invalid argument') || - lowerMessage.includes('invalid parameter') || - lowerMessage.includes('required field') || - lowerMessage.includes('missing required') || - lowerMessage.includes('type error') || - lowerMessage.includes('type mismatch') - ) { - return false - } - - // Explicit permission denied (user explicitly denied, not a transient error) - if ( - lowerMessage.includes('permission denied') && - (lowerMessage.includes('explicitly') || lowerMessage.includes('user denied')) - ) { - return true - } - - // Authentication errors that can't be resolved by retry - if ( - lowerMessage.includes('authentication failed') && - (lowerMessage.includes('invalid credentials') || - lowerMessage.includes('invalid api key') || - lowerMessage.includes('unauthorized')) - ) { - return true - } - - // Malformed requests that won't work on retry - if ( - lowerMessage.includes('malformed request') || - lowerMessage.includes('invalid request format') || - lowerMessage.includes('bad request format') - ) { - return true - } - - // Tool definition not found - won't work on retry - if (lowerMessage.includes('tool definition not found') || lowerMessage.includes('unknown tool')) { - return true - } - - // All other errors are considered retryable by default - // This includes: - // - "Object has been destroyed" / "WebContents destroyed" - // - Network errors (SSL failures, connection errors, error codes -3, -100) - // - Timeout errors - // - Loading errors - // - Transient service errors - // - Rate limiting - return false -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/index.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/index.ts deleted file mode 100644 index 7892b7822..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { AgentLoopHandler } from './agentLoopHandler' -export { LoopOrchestrator } from './loopOrchestrator' -export { ToolCallHandler } from './toolCallHandler' -export { ToolCallProcessor } from './toolCallProcessor' -export { isNonRetryableError } from './errorClassification' -export type { StreamState } from './loopState' diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/loopOrchestrator.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/loopOrchestrator.ts deleted file mode 100644 index 1e93f905c..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/loopOrchestrator.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { LLMAgentEvent, LLMAgentEventData } from '@shared/presenter' - -type LLMEventConsumer = { - handleLLMAgentResponse: (msg: LLMAgentEventData) => Promise - handleLLMAgentError: (msg: LLMAgentEventData) => Promise - handleLLMAgentEnd: (msg: LLMAgentEventData) => Promise -} - -export class LoopOrchestrator { - constructor(private readonly consumer: LLMEventConsumer) {} - - async consume(stream: AsyncGenerator): Promise { - for await (const event of stream) { - const msg = event.data - if (event.type === 'response') { - await this.consumer.handleLLMAgentResponse(msg) - } else if (event.type === 'error') { - await this.consumer.handleLLMAgentError(msg) - } else if (event.type === 'end') { - await this.consumer.handleLLMAgentEnd(msg) - } - } - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/loopState.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/loopState.ts deleted file mode 100644 index 4586112a3..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/loopState.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { BaseLLMProvider } from '@/presenter/llmProviderPresenter/baseProvider' - -export interface StreamState { - isGenerating: boolean - providerId: string - modelId: string - abortController: AbortController - provider: BaseLLMProvider -} - -export type LoopState = { - loopId: string - conversationMessages: any[] - toolCallCount: number - needContinueConversation: boolean - currentContent: string - currentReasoning?: string - currentToolCalls: Array<{ id: string; name: string; arguments: string }> - totalUsage: { - prompt_tokens: number - completion_tokens: number - total_tokens: number - context_length: number - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/toolCallHandler.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/toolCallHandler.ts deleted file mode 100644 index 835c6c32e..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/toolCallHandler.ts +++ /dev/null @@ -1,610 +0,0 @@ -import type { AssistantMessageBlock } from '@shared/chat' -import type { QuestionInfo } from '@shared/types/core/question' -import { finalizeAssistantMessageBlocks } from '@shared/chat/messageBlocks' -import type { - LLMAgentEventData, - MCPToolResponse, - ISQLitePresenter, - MCPContentItem, - MCPResourceContent -} from '@shared/presenter' -import { nanoid } from 'nanoid' -import type { GeneratingMessageState } from '../streaming/types' -import type { StreamUpdateScheduler } from '../streaming/streamUpdateScheduler' -import type { CommandPermissionService } from '../../permission/commandPermissionService' - -interface PermissionRequestPayload { - permissionType?: string - toolName?: string - serverName?: string - providerId?: string - requestId?: string - sessionId?: string - agentId?: string - agentName?: string - conversationId?: string - command?: string - commandSignature?: string - commandInfo?: { - command: string - riskLevel: 'low' | 'medium' | 'high' | 'critical' - suggestion: string - signature?: string - baseCommand?: string - } - rememberable?: boolean - [key: string]: unknown -} - -export class ToolCallHandler { - private static readonly MCP_UI_MIME_TYPES = new Set([ - 'text/html', - 'text/uri-list', - 'application/vnd.mcp-ui.remote-dom' - ]) - - private readonly sqlitePresenter: ISQLitePresenter - private readonly commandPermissionHandler?: CommandPermissionService - private readonly streamUpdateScheduler: StreamUpdateScheduler - - constructor(options: { - sqlitePresenter: ISQLitePresenter - commandPermissionHandler?: CommandPermissionService - streamUpdateScheduler: StreamUpdateScheduler - }) { - this.sqlitePresenter = options.sqlitePresenter - this.commandPermissionHandler = options.commandPermissionHandler - this.streamUpdateScheduler = options.streamUpdateScheduler - } - - async processToolCallStart( - state: GeneratingMessageState, - event: LLMAgentEventData, - currentTime: number - ): Promise { - this.finalizeLastBlock(state) - state.message.content.push({ - type: 'tool_call', - content: '', - status: 'loading', - timestamp: currentTime, - tool_call: { - id: event.tool_call_id, - name: event.tool_call_name, - params: event.tool_call_params || '', - server_name: event.tool_call_server_name, - server_icons: event.tool_call_server_icons, - server_description: event.tool_call_server_description - } - }) - } - - async processToolCallUpdate( - state: GeneratingMessageState, - event: LLMAgentEventData - ): Promise { - const block = state.message.content.find( - (item) => - item.type === 'tool_call' && - item.tool_call?.id === event.tool_call_id && - item.status === 'loading' - ) - - if (!block || block.type !== 'tool_call' || !block.tool_call) { - return - } - - block.tool_call.params = event.tool_call_params || '' - - if (event.tool_call_server_name) { - block.tool_call.server_name = event.tool_call_server_name - } - if (event.tool_call_server_icons) { - block.tool_call.server_icons = event.tool_call_server_icons - } - if (event.tool_call_server_description) { - block.tool_call.server_description = event.tool_call_server_description - } - } - - async processToolCallEnd(state: GeneratingMessageState, event: LLMAgentEventData): Promise { - const toolCallBlock = state.message.content.find( - (block) => - block.type === 'tool_call' && - block.tool_call?.id === event.tool_call_id && - block.status === 'loading' - ) - - if (toolCallBlock && toolCallBlock.type === 'tool_call') { - toolCallBlock.status = 'success' - if (toolCallBlock.tool_call) { - toolCallBlock.tool_call.response = event.tool_call_response || '' - } - } - - const lastBlock = state.message.content[state.message.content.length - 1] - if ( - lastBlock && - lastBlock.type === 'action' && - lastBlock.action_type === 'tool_call_permission' - ) { - lastBlock.status = 'success' - } - - state.pendingToolCall = undefined - } - - async processToolCallError( - state: GeneratingMessageState, - event: LLMAgentEventData - ): Promise { - const toolCallBlock = state.message.content.find( - (block) => - block.type === 'tool_call' && - block.tool_call?.id === event.tool_call_id && - block.status === 'loading' - ) - - if (toolCallBlock && toolCallBlock.type === 'tool_call') { - toolCallBlock.status = 'error' - if (toolCallBlock.tool_call) { - toolCallBlock.tool_call.response = event.tool_call_response || '' - } - } - - state.pendingToolCall = undefined - } - - async processToolCallPermission( - state: GeneratingMessageState, - event: LLMAgentEventData, - currentTime: number - ): Promise { - if (event.tool_call === 'permission-required') { - this.handlePermissionRequired(state, event, currentTime) - return - } - - const lastBlock = state.message.content[state.message.content.length - 1] - if ( - lastBlock && - lastBlock.type === 'action' && - lastBlock.action_type === 'tool_call_permission' - ) { - if (event.tool_call === 'permission-granted') { - lastBlock.status = 'granted' - lastBlock.content = event.tool_call_response || '' - if (lastBlock.extra) { - lastBlock.extra.needsUserAction = false - if ( - !lastBlock.extra.grantedPermissions && - typeof lastBlock.extra.permissionType === 'string' - ) { - lastBlock.extra.grantedPermissions = lastBlock.extra.permissionType - } - } - - // CRITICAL FIX: Create the tool_call block so processToolCallUpdate/End can find it - // This ensures frontend state updates correctly after permission is granted - this.finalizeLastBlock(state) - state.message.content.push({ - type: 'tool_call', - content: '', - status: 'loading', - timestamp: currentTime, - tool_call: { - id: event.tool_call_id, - name: event.tool_call_name, - params: event.tool_call_params || '', - server_name: event.tool_call_server_name, - server_icons: event.tool_call_server_icons, - server_description: event.tool_call_server_description - } - }) - - state.pendingToolCall = this.buildPendingToolCall(event) - return - } - - if (event.tool_call === 'permission-denied') { - lastBlock.status = 'denied' - lastBlock.content = event.tool_call_response || '' - if (lastBlock.extra) { - lastBlock.extra.needsUserAction = false - } - state.pendingToolCall = undefined - return - } - - if (event.tool_call === 'continue') { - lastBlock.status = 'success' - return - } - } - } - - async processQuestionRequest( - state: GeneratingMessageState, - event: LLMAgentEventData, - currentTime: number - ): Promise { - const payload = event.question_request as QuestionInfo | undefined - if (!payload) return - - this.finalizeLastBlock(state) - - state.message.content.push({ - type: 'action', - content: '', - status: 'pending', - timestamp: currentTime, - action_type: 'question_request', - tool_call: { - id: event.tool_call_id, - name: event.tool_call_name, - params: event.tool_call_params || '', - server_name: event.tool_call_server_name, - server_icons: event.tool_call_server_icons, - server_description: event.tool_call_server_description - }, - extra: { - needsUserAction: true, - questionHeader: payload.header ?? '', - questionText: payload.question, - questionOptions: payload.options, - questionMultiple: Boolean(payload.multiple), - questionCustom: payload.custom !== false, - questionResolution: 'asked' - } - }) - - state.pendingToolCall = undefined - } - - async processMcpUiResourcesFromToolCall( - state: GeneratingMessageState, - event: LLMAgentEventData, - currentTime: number - ): Promise { - if (event.tool_call !== 'end') { - return false - } - - try { - const response = event.tool_call_response_raw as MCPToolResponse | null - const contentItems = Array.isArray(response?.content) - ? (response.content as MCPContentItem[]) - : [] - - const uiResourceItems = contentItems.filter((item): item is MCPResourceContent => { - if (item.type !== 'resource') { - return false - } - const uri = item.resource?.uri - const mimeType = item.resource?.mimeType || '' - return ( - typeof uri === 'string' && - uri.startsWith('ui://') && - ToolCallHandler.MCP_UI_MIME_TYPES.has(mimeType) - ) - }) - - if (uiResourceItems.length === 0) { - return false - } - - const uiBlocks: AssistantMessageBlock[] = uiResourceItems - .map((item) => { - const resource = item.resource - if (!resource?.uri) { - return null - } - - const mimeType = resource.mimeType || '' - if (!ToolCallHandler.MCP_UI_MIME_TYPES.has(mimeType)) { - return null - } - const typedMimeType = mimeType as - | 'text/html' - | 'text/uri-list' - | 'application/vnd.mcp-ui.remote-dom' - - const meta = ( - resource as MCPResourceContent['resource'] & { _meta?: Record } - )._meta - - return { - type: 'mcp_ui_resource', - status: 'success', - timestamp: currentTime, - mcp_ui_resource: { - uri: resource.uri, - mimeType: typedMimeType, - text: typeof resource.text === 'string' ? resource.text : undefined, - blob: typeof resource.blob === 'string' ? resource.blob : undefined, - _meta: meta && typeof meta === 'object' ? meta : undefined - } - } - }) - .filter(Boolean) as AssistantMessageBlock[] - - if (uiBlocks.length === 0) { - return false - } - - this.finalizeLastBlock(state) - state.message.content.push(...uiBlocks) - this.streamUpdateScheduler.enqueueDelta( - event.eventId, - state.conversationId, - state.message.parentId, - Boolean(state.message.is_variant), - state.webContentsId, - {}, - state.message.content - ) - return true - } catch (error) { - console.error('[ToolCallHandler] Error processing MCP UI resources from tool call:', error) - return false - } - } - - async processSearchResultsFromToolCall( - state: GeneratingMessageState, - event: LLMAgentEventData, - currentTime: number - ): Promise { - if (event.tool_call !== 'end') { - return false - } - - try { - const response = event.tool_call_response_raw as MCPToolResponse | null - const contentItems = Array.isArray(response?.content) - ? (response?.content as MCPContentItem[]) - : [] - - const resourceItems = contentItems.filter((item): item is MCPResourceContent => { - return ( - item.type === 'resource' && item.resource?.mimeType === 'application/deepchat-webpage' - ) - }) - - if (resourceItems.length === 0) { - return false - } - - const searchResults = resourceItems - .map((item) => { - try { - const blobContent = JSON.parse(item.resource?.text ?? '{}') as { - title?: string - url?: string - content?: string - icon?: string - description?: string - favicon?: string - rank?: number - } - return { - title: blobContent.title || '', - url: blobContent.url || item.resource?.uri || '', - content: blobContent.content || '', - description: blobContent.description || blobContent.content || '', - icon: blobContent.icon || '', - favicon: blobContent.favicon || '', - rank: blobContent.rank - } - } catch (error) { - console.error('[ToolCallHandler] Failed to parse search result blob:', error) - return null - } - }) - .filter(Boolean) as Array<{ - title: string - url: string - content: string - description: string - icon: string - favicon?: string - rank?: number - }> - - if (searchResults.length === 0) { - return false - } - - const searchId = nanoid() - const pages = searchResults - .filter((item) => item.icon || item.favicon) - .slice(0, 6) - .map((item) => ({ - url: item.url, - icon: item.icon || item.favicon || '' - })) - - const searchBlock: AssistantMessageBlock = { - id: searchId, - type: 'search', - content: '', - status: 'success', - timestamp: currentTime, - extra: { - total: searchResults.length, - searchId, - pages, - label: event.tool_call_name || 'web_search', - name: event.tool_call_name || 'web_search', - engine: event.tool_call_server_name || undefined, - provider: event.tool_call_server_name || undefined - } - } - - this.finalizeLastBlock(state) - state.message.content.push(searchBlock) - - for (const result of searchResults) { - await this.sqlitePresenter.addMessageAttachment( - event.eventId, - 'search_result', - JSON.stringify({ - title: result.title, - url: result.url, - content: result.content, - description: result.description, - icon: result.icon || result.favicon || '', - rank: typeof result.rank === 'number' ? result.rank : undefined, - searchId - }) - ) - } - - this.streamUpdateScheduler.enqueueDelta( - event.eventId, - state.conversationId, - state.message.parentId, - Boolean(state.message.is_variant), - state.webContentsId, - {}, - state.message.content - ) - return true - } catch (error) { - console.error('[ToolCallHandler] Error processing search results from tool call:', error) - return false - } - } - - private handlePermissionRequired( - state: GeneratingMessageState, - event: LLMAgentEventData, - currentTime: number - ): void { - const ALLOWED_PERMISSION_TYPES = ['read', 'write', 'all', 'command'] as const - type PermissionType = (typeof ALLOWED_PERMISSION_TYPES)[number] - - let permissionType: PermissionType = 'read' - const requestedType = event.permission_request?.permissionType - if (typeof requestedType === 'string') { - const normalizedType = requestedType.toLowerCase() - if (ALLOWED_PERMISSION_TYPES.includes(normalizedType as PermissionType)) { - permissionType = normalizedType as PermissionType - } else { - console.warn( - `[ToolCallHandler] Invalid permission type received: "${requestedType}". Defaulting to "read".` - ) - } - } else if (requestedType !== undefined) { - console.warn( - `[ToolCallHandler] Permission type is not a string: ${typeof requestedType}. Defaulting to "read".` - ) - } - - const lastBlock = state.message.content[state.message.content.length - 1] - if (lastBlock && lastBlock.type === 'tool_call' && lastBlock.tool_call) { - } - - this.finalizeLastBlock(state) - const permissionExtra: Record = { - needsUserAction: true - } - - const permissionRequest = event.permission_request as PermissionRequestPayload | undefined - permissionExtra.permissionType = permissionRequest?.permissionType ?? permissionType - if (permissionRequest) { - permissionExtra.permissionRequest = JSON.stringify(permissionRequest) - if (permissionRequest.commandInfo) { - permissionExtra.commandInfo = JSON.stringify(permissionRequest.commandInfo) - } else { - const commandFromRequest = permissionRequest.command - if (commandFromRequest && this.commandPermissionHandler) { - permissionExtra.commandInfo = JSON.stringify( - this.commandPermissionHandler.buildCommandInfo(commandFromRequest) - ) - } - } - if (permissionRequest.toolName) { - permissionExtra.toolName = permissionRequest.toolName - } - if (permissionRequest.serverName) { - permissionExtra.serverName = permissionRequest.serverName - } - if (permissionRequest.providerId) { - permissionExtra.providerId = permissionRequest.providerId - } - if (permissionRequest.requestId) { - permissionExtra.permissionRequestId = permissionRequest.requestId - } - if (permissionRequest.rememberable === false) { - permissionExtra.rememberable = false - } - if (permissionRequest.agentId) { - permissionExtra.agentId = permissionRequest.agentId - } - if (permissionRequest.agentName) { - permissionExtra.agentName = permissionRequest.agentName - } - if (permissionRequest.sessionId) { - permissionExtra.sessionId = permissionRequest.sessionId - } - if (!permissionExtra.commandInfo && this.commandPermissionHandler) { - try { - const parsedParams = JSON.parse(event.tool_call_params || '{}') as { command?: string } - if (typeof parsedParams.command === 'string' && parsedParams.command.trim()) { - permissionExtra.commandInfo = JSON.stringify( - this.commandPermissionHandler.buildCommandInfo(parsedParams.command) - ) - } - } catch { - // Ignore parsing failures for command info fallback. - } - } - } else { - if (event.tool_call_name) { - permissionExtra.toolName = event.tool_call_name - } - if (event.tool_call_server_name) { - permissionExtra.serverName = event.tool_call_server_name - } - } - - state.message.content.push({ - type: 'action', - content: event.tool_call_response || '', - status: 'pending', - timestamp: currentTime, - action_type: 'tool_call_permission', - tool_call: { - id: event.tool_call_id, - name: event.tool_call_name, - params: event.tool_call_params || '', - server_name: event.tool_call_server_name, - server_icons: event.tool_call_server_icons, - server_description: event.tool_call_server_description - }, - extra: permissionExtra - }) - - state.pendingToolCall = this.buildPendingToolCall(event) - } - - private buildPendingToolCall(event: LLMAgentEventData) { - if (!event.tool_call_id && !event.tool_call_name) { - return undefined - } - - return { - id: event.tool_call_id || '', - name: event.tool_call_name || '', - params: event.tool_call_params || '', - serverName: event.tool_call_server_name, - serverIcons: event.tool_call_server_icons, - serverDescription: event.tool_call_server_description - } - } - - private finalizeLastBlock(state: GeneratingMessageState): void { - finalizeAssistantMessageBlocks(state.message.content) - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/toolCallProcessor.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/toolCallProcessor.ts deleted file mode 100644 index 50a93f979..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/toolCallProcessor.ts +++ /dev/null @@ -1,726 +0,0 @@ -import { - ChatMessage, - LLMAgentEvent, - MCPToolCall, - MCPToolDefinition, - MCPToolResponse, - ModelConfig -} from '@shared/presenter' -import fs from 'fs/promises' -import path from 'path' -import { isNonRetryableError } from './errorClassification' -import { resolveToolOffloadPath } from '../../sessionPresenter/sessionPaths' -import { parseQuestionToolArgs, QUESTION_TOOL_NAME } from '@/lib/agentRuntime/questionTool' - -interface ToolCallProcessorOptions { - getAllToolDefinitions: (context: ToolCallExecutionContext) => Promise - callTool: (request: MCPToolCall) => Promise<{ content: unknown; rawData: MCPToolResponse }> - preCheckToolPermission?: (request: MCPToolCall) => Promise - onToolCallFinished?: (info: { - toolName: string - toolCallId: string - toolServerName?: string - conversationId?: string - status: 'success' | 'error' | 'permission' - }) => void -} - -interface ToolCallExecutionContext { - eventId: string - toolCalls: Array<{ id: string; name: string; arguments: string }> - enabledMcpTools?: string[] - conversationMessages: ChatMessage[] - modelConfig: ModelConfig - providerId?: string - abortSignal: AbortSignal - currentToolCallCount: number - maxToolCalls: number - conversationId?: string -} - -interface ToolCallProcessResult { - toolCallCount: number - needContinueConversation: boolean -} - -interface ToolCall { - id: string - name: string - arguments: string -} - -type PermissionType = 'read' | 'write' | 'all' | 'command' - -interface CommandInfoPayload { - command: string - riskLevel: 'low' | 'medium' | 'high' | 'critical' - suggestion: string - signature?: string - baseCommand?: string -} - -interface PermissionRequestPayload { - needsPermission: true - toolName: string - serverName: string - permissionType: PermissionType - description: string - command?: string - commandSignature?: string - commandInfo?: CommandInfoPayload - paths?: string[] - providerId?: string - requestId?: string - sessionId?: string - agentId?: string - agentName?: string - conversationId?: string - rememberable?: boolean - [key: string]: unknown -} - -interface PermissionRequestInfo { - toolCall: ToolCall - serverName: string - serverIcons?: string - serverDescription?: string - payload: PermissionRequestPayload -} - -const TOOL_OUTPUT_OFFLOAD_THRESHOLD = 5000 -const TOOL_OUTPUT_PREVIEW_LENGTH = 1024 -const QUESTION_ERROR_KEY = 'common.error.invalidQuestionRequest' - -// Tools that require offload when output exceeds threshold -// Tools not in this list will never trigger offload (e.g., read has its own pagination) -const TOOLS_REQUIRING_OFFLOAD = new Set(['exec', 'ls', 'find', 'grep', 'cdp_send']) - -export class ToolCallProcessor { - constructor(private readonly options: ToolCallProcessorOptions) {} - - async *process( - context: ToolCallExecutionContext - ): AsyncGenerator { - let toolCallCount = context.currentToolCallCount - let needContinueConversation = context.toolCalls.length > 0 - let toolDefinitions = await this.options.getAllToolDefinitions(context) - - // Step 1: Pre-check all tool permissions in batch - // If any tool requires permission, we pause and request permission for all at once - const permissionCheckResult = await this.batchPreCheckPermissions(context, toolDefinitions) - - if (permissionCheckResult.hasPendingPermissions) { - // Yield permission request event for all tools that need permission - for (const permissionRequest of permissionCheckResult.permissionRequests) { - const permissionPayload = { - ...permissionRequest.payload, - toolName: permissionRequest.toolCall.name, - serverName: permissionRequest.serverName, - permissionType: permissionRequest.payload.permissionType, - description: permissionRequest.payload.description, - conversationId: permissionRequest.payload.conversationId ?? context.conversationId, - // Mark this as part of a batch - isBatchPermission: true, - totalInBatch: permissionCheckResult.permissionRequests.length - } - - yield { - type: 'response', - data: { - eventId: context.eventId, - tool_call: 'permission-required', - tool_call_id: permissionRequest.toolCall.id, - tool_call_name: permissionRequest.toolCall.name, - tool_call_params: permissionRequest.toolCall.arguments, - tool_call_server_name: permissionRequest.serverName, - tool_call_server_icons: permissionRequest.serverIcons, - tool_call_server_description: permissionRequest.serverDescription, - tool_call_response: permissionRequest.payload.description, - permission_request: permissionPayload - } - } - } - - // Stop here and wait for user to grant permissions - // The loop will be restarted after permissions are granted - needContinueConversation = false - return { - toolCallCount, - needContinueConversation - } - } - - const resolveToolDefinition = async ( - toolName: string - ): Promise => { - const match = toolDefinitions.find((tool) => tool.function.name === toolName) - if (match) return match - toolDefinitions = await this.options.getAllToolDefinitions(context) - return toolDefinitions.find((tool) => tool.function.name === toolName) - } - - for (const [index, toolCall] of context.toolCalls.entries()) { - if (context.abortSignal.aborted) break - - if (toolCallCount >= context.maxToolCalls) { - console.warn('Max tool calls reached during execution phase for event:', context.eventId) - yield { - type: 'response', - data: { - eventId: context.eventId, - maximum_tool_calls_reached: true, - tool_call_id: toolCall.id, - tool_call_name: toolCall.name - } - } - - needContinueConversation = false - break - } - - toolCallCount++ - - const toolDef = await resolveToolDefinition(toolCall.name) - - if (!toolDef) { - console.error(`Tool definition not found for ${toolCall.name}. Skipping execution.`) - const errorMsg = `Tool definition for ${toolCall.name} not found.` - yield { - type: 'response', - data: { - eventId: context.eventId, - tool_call: 'error', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_response: errorMsg - } - } - - context.conversationMessages.push({ - role: 'user', - content: `Error: ${errorMsg}` - }) - continue - } - - const notifyToolCallFinished = (status: 'success' | 'error' | 'permission') => { - if (!this.options.onToolCallFinished) return - try { - this.options.onToolCallFinished({ - toolName: toolCall.name, - toolCallId: toolCall.id, - toolServerName: toolDef.server?.name, - conversationId: context.conversationId, - status - }) - } catch (error) { - console.warn('[ToolCallProcessor] onToolCallFinished handler failed:', error) - } - } - - const mcpToolInput: MCPToolCall = { - id: toolCall.id, - type: 'function', - function: { - name: toolCall.name, - arguments: toolCall.arguments - }, - server: toolDef.server, - conversationId: context.conversationId - } - - if (toolCall.name === QUESTION_TOOL_NAME) { - const isStandalone = context.toolCalls.length === 1 - const isLast = index === context.toolCalls.length - 1 - if (!isStandalone || !isLast) { - notifyToolCallFinished('error') - this.appendToolError( - context.conversationMessages, - context.modelConfig, - toolCall, - 'Question tool must be the only tool call in a turn.' - ) - yield { - type: 'response', - data: { - eventId: context.eventId, - question_error: QUESTION_ERROR_KEY, - tool_call_id: toolCall.id, - tool_call_name: toolCall.name - } - } - continue - } - - const parsedQuestion = parseQuestionToolArgs(toolCall.arguments || '') - if (!parsedQuestion.success) { - notifyToolCallFinished('error') - this.appendToolError( - context.conversationMessages, - context.modelConfig, - toolCall, - `Invalid question tool arguments: ${parsedQuestion.error}` - ) - yield { - type: 'response', - data: { - eventId: context.eventId, - question_error: QUESTION_ERROR_KEY, - tool_call_id: toolCall.id, - tool_call_name: toolCall.name - } - } - continue - } - - notifyToolCallFinished('success') - yield { - type: 'response', - data: { - eventId: context.eventId, - tool_call: 'question-required', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.arguments, - tool_call_server_name: toolDef.server.name, - tool_call_server_icons: toolDef.server.icons, - tool_call_server_description: toolDef.server.description, - question_request: parsedQuestion.data - } - } - - needContinueConversation = false - break - } - - yield { - type: 'response', - data: { - eventId: context.eventId, - tool_call: 'running', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.arguments, - tool_call_server_name: toolDef.server.name, - tool_call_server_icons: toolDef.server.icons, - tool_call_server_description: toolDef.server.description - } - } - - try { - const toolResponse = await this.options.callTool(mcpToolInput) - const requiresPermission = Boolean(toolResponse.rawData?.requiresPermission) - - if (requiresPermission) { - notifyToolCallFinished('permission') - console.log( - `[Agent Loop] Permission required for tool ${toolCall.name}, creating permission request` - ) - - yield { - type: 'response', - data: { - eventId: context.eventId, - tool_call: 'permission-required', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.arguments, - tool_call_server_name: toolResponse.rawData.permissionRequest?.serverName, - tool_call_server_icons: toolDef.server.icons, - tool_call_server_description: toolDef.server.description, - tool_call_response: toolResponse.content, - permission_request: toolResponse.rawData.permissionRequest - } - } - - needContinueConversation = false - break - } - - notifyToolCallFinished('success') - - if (context.abortSignal.aborted) break - - const supportsFunctionCall = context.modelConfig?.functionCall || false - - const toolContent = this.stringifyToolContent(toolResponse.content) - const toolContentForModel = await this.offloadToolContentIfNeeded( - toolContent, - toolCall.id, - context.conversationId, - toolCall.name - ) - - if (supportsFunctionCall) { - this.appendNativeFunctionCallMessages(context.conversationMessages, toolCall, { - content: toolContentForModel - }) - - yield { - type: 'response', - data: { - eventId: context.eventId, - tool_call: 'end', - tool_call_id: toolCall.id, - tool_call_response: toolContentForModel, - tool_call_name: toolCall.name, - tool_call_params: toolCall.arguments, - tool_call_server_name: toolDef.server.name, - tool_call_server_icons: toolDef.server.icons, - tool_call_server_description: toolDef.server.description, - tool_call_response_raw: toolResponse.rawData - } - } - } else { - this.appendLegacyFunctionCallMessages(context.conversationMessages, toolCall, { - content: toolContentForModel - }) - - yield { - type: 'response', - data: { - eventId: context.eventId, - tool_call: 'end', - tool_call_id: toolCall.id, - tool_call_response: toolContentForModel, - tool_call_name: toolCall.name, - tool_call_params: toolCall.arguments, - tool_call_server_name: toolDef.server.name, - tool_call_server_icons: toolDef.server.icons, - tool_call_server_description: toolDef.server.description, - tool_call_response_raw: toolResponse.rawData - } - } - } - } catch (toolError) { - notifyToolCallFinished('error') - if (context.abortSignal.aborted) break - - console.error( - `Tool execution error for ${toolCall.name} (event ${context.eventId}):`, - toolError - ) - const errorMessage = toolError instanceof Error ? toolError.message : String(toolError) - - // Check if error is non-retryable (should stop the loop) - const errorForClassification: Error | string = - toolError instanceof Error ? toolError : String(toolError) - const isNonRetryable = isNonRetryableError(errorForClassification) - - this.appendToolError( - context.conversationMessages, - context.modelConfig, - toolCall, - errorMessage - ) - - yield { - type: 'response', - data: { - eventId: context.eventId, - tool_call: 'error', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.arguments, - tool_call_response: errorMessage, - tool_call_server_name: toolDef.server.name, - tool_call_server_icons: toolDef.server.icons, - tool_call_server_description: toolDef.server.description - } - } - - // If error is non-retryable, stop the loop - // Otherwise, keep needContinueConversation = true (default) to let LLM decide - if (isNonRetryable) { - needContinueConversation = false - break - } - // For retryable errors, continue the loop (needContinueConversation remains true) - } - } - - return { - toolCallCount, - needContinueConversation - } - } - - private appendNativeFunctionCallMessages( - conversationMessages: ChatMessage[], - toolCall: { id: string; name: string; arguments: string }, - toolResponse: { content: unknown } - ): void { - const lastAssistantMsg = conversationMessages.findLast( - (message) => message.role === 'assistant' - ) - if (lastAssistantMsg) { - if (!lastAssistantMsg.tool_calls) lastAssistantMsg.tool_calls = [] - lastAssistantMsg.tool_calls.push({ - function: { - arguments: toolCall.arguments, - name: toolCall.name - }, - id: toolCall.id, - type: 'function' - }) - } else { - conversationMessages.push({ - role: 'assistant', - tool_calls: [ - { - function: { - arguments: toolCall.arguments, - name: toolCall.name - }, - id: toolCall.id, - type: 'function' - } - ] - }) - } - - const toolContent = this.stringifyToolContent(toolResponse.content) - conversationMessages.push({ - role: 'tool', - content: toolContent, - tool_call_id: toolCall.id - }) - } - - private appendLegacyFunctionCallMessages( - conversationMessages: ChatMessage[], - toolCall: { id: string; name: string; arguments: string }, - toolResponse: { content: unknown; rawData?: unknown } - ): void { - const formattedToolRecordText = - '' + - JSON.stringify({ - function_call_record: { - name: toolCall.name, - arguments: toolCall.arguments, - response: toolResponse.content - } - }) + - '' - - let lastAssistantMessage = conversationMessages.findLast( - (message) => message.role === 'assistant' - ) - - if (lastAssistantMessage) { - if (typeof lastAssistantMessage.content === 'string') { - lastAssistantMessage.content += formattedToolRecordText + '\n' - } else if (Array.isArray(lastAssistantMessage.content)) { - lastAssistantMessage.content.push({ - type: 'text', - text: formattedToolRecordText + '\n' - }) - } else { - lastAssistantMessage.content = [{ type: 'text', text: formattedToolRecordText + '\n' }] - } - } else { - conversationMessages.push({ - role: 'assistant', - content: [{ type: 'text', text: formattedToolRecordText + '\n' }] - }) - } - - const userPromptText = - '以上是你刚执行的工具调用及其响应信息,已帮你插入,请仔细阅读工具响应,并继续你的回答。' - conversationMessages.push({ - role: 'user', - content: [{ type: 'text', text: userPromptText }] - }) - } - - private async offloadToolContentIfNeeded( - content: string, - toolCallId: string, - conversationId?: string, - toolName?: string - ): Promise { - // Only offload tools in the whitelist - if (toolName && !TOOLS_REQUIRING_OFFLOAD.has(toolName)) { - return content - } - - if (content.length <= TOOL_OUTPUT_OFFLOAD_THRESHOLD) return content - if (!conversationId) return content - - const filePath = resolveToolOffloadPath(conversationId, toolCallId) - if (!filePath) return content - const sessionDir = path.dirname(filePath) - - try { - await fs.mkdir(sessionDir, { recursive: true }) - await fs.writeFile(filePath, content, 'utf-8') - } catch (error) { - console.warn('[ToolCallProcessor] Failed to offload tool output:', error) - return content - } - - const preview = content.slice(0, TOOL_OUTPUT_PREVIEW_LENGTH) - return this.buildToolOutputStub(content.length, preview, filePath) - } - - private buildToolOutputStub(totalLength: number, preview: string, filePath: string): string { - return [ - '[Tool output offloaded]', - `Total characters: ${totalLength}`, - `Full output saved to: ${filePath}`, - `first ${preview.length} chars:`, - preview - ].join('\n') - } - - private appendToolError( - conversationMessages: ChatMessage[], - modelConfig: ModelConfig, - toolCall: { id: string; name: string; arguments: string }, - errorMessage: string - ): void { - if (modelConfig?.functionCall) { - // For native function-calling models, ensure every tool error is still paired - // with a preceding assistant message that declares the tool_call in tool_calls. - const toolCallEntry = { - id: toolCall.id, - type: 'function' as const, - function: { - name: toolCall.name, - arguments: toolCall.arguments - } - } - - let lastAssistantMessage = conversationMessages.findLast( - (message) => message.role === 'assistant' - ) - - if (lastAssistantMessage) { - if (!lastAssistantMessage.tool_calls) { - lastAssistantMessage.tool_calls = [] - } - lastAssistantMessage.tool_calls.push(toolCallEntry) - } else { - // Extremely defensive fallback – create a synthetic assistant message - // so the OpenAI API still sees a valid tool_calls declaration. - lastAssistantMessage = { - role: 'assistant', - tool_calls: [toolCallEntry] - } - conversationMessages.push(lastAssistantMessage) - } - - conversationMessages.push({ - role: 'tool', - content: `The tool call with ID ${toolCall.id} and name ${toolCall.name} failed to execute: ${errorMessage}`, - tool_call_id: toolCall.id - }) - return - } - - const formattedErrorText = `编号为 ${toolCall.id} 的工具 ${toolCall.name} 调用执行失败: ${errorMessage}` - let lastAssistantMessage = conversationMessages.findLast( - (message) => message.role === 'assistant' - ) - if (lastAssistantMessage) { - if (typeof lastAssistantMessage.content === 'string') { - lastAssistantMessage.content += '\n' + formattedErrorText + '\n' - } else if (Array.isArray(lastAssistantMessage.content)) { - lastAssistantMessage.content.push({ - type: 'text', - text: '\n' + formattedErrorText + '\n' - }) - } else { - lastAssistantMessage.content = [{ type: 'text', text: '\n' + formattedErrorText + '\n' }] - } - } else { - conversationMessages.push({ - role: 'assistant', - content: [{ type: 'text', text: formattedErrorText + '\n' }] - }) - } - - const userPromptText = - '以上是你刚调用的工具及其执行的错误信息,已帮你插入,请根据情况继续回答或重新尝试。' - conversationMessages.push({ - role: 'user', - content: [{ type: 'text', text: userPromptText }] - }) - } - - private stringifyToolContent(content: unknown): string { - return typeof content === 'string' ? content : JSON.stringify(content) - } - - /** - * Batch pre-check permissions for all tool calls - * Returns info about tools that need permission, or empty if all have permission - */ - private async batchPreCheckPermissions( - context: ToolCallExecutionContext, - toolDefinitions: MCPToolDefinition[] - ): Promise<{ - hasPendingPermissions: boolean - permissionRequests: PermissionRequestInfo[] - }> { - // If no permission pre-check function provided, skip batch check - if (!this.options.preCheckToolPermission) { - return { hasPendingPermissions: false, permissionRequests: [] } - } - - const permissionRequests: PermissionRequestInfo[] = [] - const toolNameToDefMap = new Map(toolDefinitions.map((t) => [t.function.name, t])) - - for (const toolCall of context.toolCalls) { - const toolDef = toolNameToDefMap.get(toolCall.name) - if (!toolDef) continue - - // Skip question tool for permission check - if (toolCall.name === QUESTION_TOOL_NAME) continue - - const mcpToolInput: MCPToolCall = { - id: toolCall.id, - type: 'function', - function: { - name: toolCall.name, - arguments: toolCall.arguments - }, - server: toolDef.server, - conversationId: context.conversationId - } - - try { - const permissionResult = await this.options.preCheckToolPermission(mcpToolInput) - if (permissionResult) { - const permissionPayload: PermissionRequestPayload = { - ...permissionResult, - toolName: permissionResult.toolName || toolCall.name, - serverName: permissionResult.serverName || toolDef.server.name, - permissionType: permissionResult.permissionType, - description: permissionResult.description - } - - // Preserve the full permission payload (paths and custom fields included) - permissionRequests.push({ - toolCall, - serverName: permissionPayload.serverName, - serverIcons: toolDef.server?.icons, - serverDescription: toolDef.server?.description, - payload: permissionPayload - }) - } - } catch (error) { - console.warn( - `[ToolCallProcessor] Failed to pre-check permission for ${toolCall.name}:`, - error - ) - // If pre-check fails, we'll let the actual execution handle it - } - } - - return { - hasPendingPermissions: permissionRequests.length > 0, - permissionRequests - } - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageBuilder.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageBuilder.ts deleted file mode 100644 index 8599fe871..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageBuilder.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { approximateTokenSize } from 'tokenx' -import { AssistantMessage, Message, MessageFile, UserMessageContent } from '@shared/chat' -import { ModelType } from '@shared/model' -import { - CONVERSATION, - IToolPresenter, - ModelConfig, - SearchResult, - ChatMessage -} from '@shared/presenter' -import type { MCPToolDefinition } from '@shared/presenter' - -import { modelCapabilities } from '../../configPresenter/modelCapabilities' -import { enhanceSystemPromptWithDateTime } from '../utility/promptEnhancer' -import { ToolCallCenter } from '../tool/toolCallCenter' -import { nanoid } from 'nanoid' - -import { - addContextMessages, - buildUserMessageContext, - formatMessagesForCompletion, - mergeConsecutiveMessages -} from './messageFormatter' -import { selectContextMessages } from './messageTruncator' -import { - buildSkillsMetadataPrompt, - buildSkillsPrompt, - getSkillsAllowedTools -} from './skillsPromptBuilder' -import { - buildRuntimeCapabilitiesPrompt, - buildSystemEnvPrompt -} from '@/lib/agentRuntime/systemEnvPromptBuilder' -import type { AgentPromptRuntimePort } from '../runtimePorts' - -export type PendingToolCall = { - id: string - name: string - params: string - serverName?: string - serverIcons?: string - serverDescription?: string -} - -export interface PreparePromptContentParams { - conversation: CONVERSATION - userContent: string - contextMessages: Message[] - searchResults: SearchResult[] | null - userMessage: Message - vision: boolean - imageFiles: MessageFile[] - supportsFunctionCall: boolean - modelType?: ModelType - toolPresenter: IToolPresenter - promptRuntime: AgentPromptRuntimePort -} - -export interface ContinueToolCallContextParams { - conversation: CONVERSATION - contextMessages: Message[] - userMessage: Message - pendingToolCall: PendingToolCall - modelConfig: ModelConfig -} - -export interface PostToolExecutionContextParams { - conversation: CONVERSATION - contextMessages: Message[] - userMessage: Message - currentAssistantMessage: AssistantMessage - completedToolCall: PendingToolCall & { response: string } - modelConfig: ModelConfig -} - -function mergeToolSelections(base?: string[], extra?: string[]): string[] | undefined { - const merged = new Set() - base?.forEach((tool) => merged.add(tool)) - extra?.forEach((tool) => merged.add(tool)) - if (merged.size === 0) { - return undefined - } - return Array.from(merged) -} - -function appendPromptSection(base: string, section: string): string { - const trimmedSection = section.trim() - if (!trimmedSection) { - return base - } - if (!base) { - return trimmedSection - } - return `${base}\n\n${trimmedSection}` -} - -export interface AgentSystemPromptSections { - basePrompt: string - runtimePrompt?: string - skillsMetadataPrompt?: string - skillsPrompt?: string - envPrompt?: string - toolingPrompt?: string -} - -export function composeAgentSystemPromptSections(sections: AgentSystemPromptSections): string { - let composed = sections.basePrompt?.trim() ?? '' - composed = appendPromptSection(composed, sections.runtimePrompt ?? '') - composed = appendPromptSection(composed, sections.skillsMetadataPrompt ?? '') - composed = appendPromptSection(composed, sections.skillsPrompt ?? '') - composed = appendPromptSection(composed, sections.envPrompt ?? '') - composed = appendPromptSection(composed, sections.toolingPrompt ?? '') - return composed -} - -export async function preparePromptContent({ - conversation, - userContent, - contextMessages, - searchResults: _searchResults, - userMessage, - vision, - imageFiles, - supportsFunctionCall, - modelType, - toolPresenter, - promptRuntime -}: PreparePromptContentParams): Promise<{ - finalContent: ChatMessage[] - promptTokens: number -}> { - const { systemPrompt, contextLength, artifacts, enabledMcpTools } = conversation.settings - - function normalizeChatMode(mode: string | undefined): 'agent' | 'acp agent' | undefined { - if (mode === 'agent' || mode === 'acp agent') return mode - return undefined - } - - const rawChatMode = conversation.settings.chatMode - const rawFallback = await promptRuntime.getInputChatMode() - const chatMode: 'agent' | 'acp agent' = - normalizeChatMode(rawChatMode) ?? normalizeChatMode(rawFallback) ?? 'agent' - - const isImageGeneration = modelType === ModelType.ImageGeneration - - const finalSystemPrompt = enhanceSystemPromptWithDateTime(systemPrompt, { - isImageGeneration, - agentWorkspacePath: conversation.settings.agentWorkspacePath?.trim() || null - }) - - const { providerId, modelId } = conversation.settings - const supportsVision = modelCapabilities.supportsVision(providerId, modelId) - const toolCallCenter = new ToolCallCenter(toolPresenter) - let toolDefinitions: MCPToolDefinition[] = [] - let effectiveEnabledMcpTools = enabledMcpTools - - if (!isImageGeneration && chatMode === 'agent') { - const skillsAllowedTools = await getSkillsAllowedTools(promptRuntime, conversation.id) - effectiveEnabledMcpTools = mergeToolSelections(enabledMcpTools, skillsAllowedTools) - } - - if (!isImageGeneration) { - try { - toolDefinitions = await toolCallCenter.getAllToolDefinitions({ - enabledMcpTools: effectiveEnabledMcpTools, - chatMode, - supportsVision, - agentWorkspacePath: conversation.settings.agentWorkspacePath?.trim() || null, - conversationId: conversation.id - }) - } catch (error) { - console.warn('AgentPresenter: Failed to load tool definitions', error) - toolDefinitions = [] - } - } - - let runtimePrompt = '' - let skillsMetadataPrompt = '' - let skillsPrompt = '' - let envPrompt = '' - let toolingPrompt = '' - - if (!isImageGeneration && chatMode === 'agent') { - runtimePrompt = buildRuntimeCapabilitiesPrompt() - try { - skillsMetadataPrompt = await buildSkillsMetadataPrompt(promptRuntime) - skillsPrompt = await buildSkillsPrompt(promptRuntime, conversation.id) - } catch (error) { - console.warn('AgentPresenter: Failed to build skills prompt', error) - } - - try { - envPrompt = await buildSystemEnvPrompt({ - providerId, - modelId, - workdir: conversation.settings.agentWorkspacePath?.trim() || null - }) - } catch (error) { - console.warn('AgentPresenter: Failed to build system env prompt', error) - } - } - - if (!isImageGeneration && toolDefinitions.length > 0) { - toolingPrompt = toolCallCenter.buildToolSystemPrompt({ - conversationId: conversation.id - }) - } - - const finalSystemPromptWithExtras = composeAgentSystemPromptSections({ - basePrompt: finalSystemPrompt, - runtimePrompt, - skillsMetadataPrompt, - skillsPrompt, - envPrompt, - toolingPrompt - }) - - const systemPromptTokens = - !isImageGeneration && finalSystemPromptWithExtras - ? approximateTokenSize(finalSystemPromptWithExtras) - : 0 - const userMessageTokens = approximateTokenSize(userContent) - const toolDefinitionsTokens = toolDefinitions.reduce((acc, tool) => { - return acc + approximateTokenSize(JSON.stringify(tool)) - }, 0) - - const reservedTokens = systemPromptTokens + userMessageTokens + toolDefinitionsTokens - const remainingContextLength = contextLength - reservedTokens - - const selectedContextMessages = selectContextMessages( - contextMessages, - userMessage, - remainingContextLength, - supportsFunctionCall, - vision - ) - - const formattedMessages = formatMessagesForCompletion( - selectedContextMessages, - isImageGeneration ? '' : finalSystemPromptWithExtras, - artifacts, - userContent, - '', - imageFiles, - vision, - supportsFunctionCall - ) - - const mergedMessages = mergeConsecutiveMessages(formattedMessages) - - let promptTokens = 0 - const imageTokenCost = imageFiles.reduce((acc, file) => acc + (file.token ?? 0), 0) - for (let i = 0; i < mergedMessages.length; i++) { - const msg = mergedMessages[i] - - if (typeof msg.content === 'string' || msg.content === undefined) { - promptTokens += approximateTokenSize(msg.content || '') - continue - } - - const textContent = - msg.content?.reduce((acc, item) => { - if (item.type === 'text' && typeof item.text === 'string') { - return acc + item.text - } - return acc - }, '') ?? '' - - promptTokens += approximateTokenSize(textContent) - - const isFinalUserWithImages = - i === mergedMessages.length - 1 && - msg.role === 'user' && - Array.isArray(msg.content) && - msg.content.some((block) => block.type === 'image_url') - - if (isFinalUserWithImages && imageTokenCost > 0) { - promptTokens += imageTokenCost - } - } - - return { finalContent: mergedMessages, promptTokens } -} - -export async function buildContinueToolCallContext({ - conversation, - contextMessages, - userMessage, - pendingToolCall, - modelConfig -}: ContinueToolCallContextParams): Promise { - const { systemPrompt } = conversation.settings - const formattedMessages: ChatMessage[] = [] - - if (systemPrompt) { - const finalSystemPrompt = enhanceSystemPromptWithDateTime(systemPrompt, {}) - formattedMessages.push({ - role: 'system', - content: finalSystemPrompt - }) - } - - const contextChatMessages = addContextMessages(contextMessages, false, modelConfig.functionCall) - formattedMessages.push(...contextChatMessages) - - const userContent = userMessage.content as UserMessageContent - const finalUserContent = buildUserMessageContext(userContent) - - formattedMessages.push({ - role: 'user', - content: `${finalUserContent}\n\nTool call interrupted: ${pendingToolCall.name}` - }) - - return formattedMessages -} - -export async function buildPostToolExecutionContext({ - conversation, - contextMessages, - userMessage, - currentAssistantMessage, - completedToolCall, - modelConfig -}: PostToolExecutionContextParams): Promise { - const { systemPrompt } = conversation.settings - const formattedMessages: ChatMessage[] = [] - const supportsFunctionCall = Boolean(modelConfig?.functionCall) - - if (systemPrompt) { - const finalSystemPrompt = enhanceSystemPromptWithDateTime(systemPrompt, {}) - formattedMessages.push({ - role: 'system', - content: finalSystemPrompt - }) - } - - const contextChatMessages = addContextMessages(contextMessages, false, modelConfig.functionCall) - formattedMessages.push(...contextChatMessages) - - const userContent = userMessage.content as UserMessageContent - const finalUserContent = buildUserMessageContext(userContent) - - formattedMessages.push({ - role: 'user', - content: finalUserContent - }) - - const assistantText = currentAssistantMessage.content - .filter((block) => block.type === 'content') - .map((block) => block.content || '') - .join('\n') - .trim() - - // OpenAI-compatible function-calling requires: - // assistant(tool_calls: [...]) -> tool(tool_call_id=...) pairing. - if (supportsFunctionCall) { - const toolCallId = completedToolCall.id || nanoid(8) - formattedMessages.push({ - role: 'assistant', - content: assistantText || undefined, - tool_calls: [ - { - id: toolCallId, - type: 'function', - function: { - name: completedToolCall.name, - arguments: completedToolCall.params || '' - } - } - ] - }) - - formattedMessages.push({ - role: 'tool', - content: completedToolCall.response, - tool_call_id: toolCallId - }) - } else { - const formattedToolRecordText = - '' + - JSON.stringify({ - function_call_record: { - name: completedToolCall.name, - arguments: completedToolCall.params, - response: completedToolCall.response - } - }) + - '' - - const combinedText = [assistantText, formattedToolRecordText].filter(Boolean).join('\n') - formattedMessages.push({ - role: 'assistant', - content: combinedText || undefined - }) - - const userPromptText = - '以上是你刚执行的工具调用及其响应信息,已帮你插入,请仔细阅读工具响应,并继续你的回答。' - formattedMessages.push({ - role: 'user', - content: userPromptText - }) - } - - return formattedMessages -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageCompressor.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageCompressor.ts deleted file mode 100644 index 3ca9ea1f0..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageCompressor.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { approximateTokenSize } from 'tokenx' -import type { AssistantMessageBlock, Message } from '@shared/chat' - -function calculateToolCallBlockTokens(block: AssistantMessageBlock): number { - if (block.type !== 'tool_call' || !block.tool_call?.response) { - return 0 - } - - const nameTokens = approximateTokenSize(block.tool_call.name || '') - const paramsTokens = approximateTokenSize(block.tool_call.params || '') - const responseTokens = approximateTokenSize(block.tool_call.response || '') - - return nameTokens + paramsTokens + responseTokens -} - -function cloneMessageWithContent(message: Message): Message { - const cloned: Message = { ...message } - - if (Array.isArray(message.content)) { - cloned.content = message.content.map((block) => { - const clonedBlock: AssistantMessageBlock = { ...(block as AssistantMessageBlock) } - if (block.type === 'tool_call' && block.tool_call) { - clonedBlock.tool_call = { ...block.tool_call } - } - return clonedBlock - }) - } else if (message.content && typeof message.content === 'object') { - cloned.content = JSON.parse(JSON.stringify(message.content)) - } else { - cloned.content = message.content - } - - return cloned -} - -function removeToolCallsFromAssistant(message: Message): { - updatedMessage: Message - removedTokens: number - removedToolCalls: number -} { - if (message.role !== 'assistant' || !Array.isArray(message.content)) { - return { updatedMessage: message, removedTokens: 0, removedToolCalls: 0 } - } - - const clonedMessage = cloneMessageWithContent(message) - const clonedContent = clonedMessage.content as AssistantMessageBlock[] - - let removedTokens = 0 - let removedToolCalls = 0 - - const filteredBlocks = clonedContent.filter((block) => { - if (block.type !== 'tool_call' || !block.tool_call) { - return true - } - - removedTokens += calculateToolCallBlockTokens(block) - removedToolCalls += 1 - return false - }) - - clonedMessage.content = filteredBlocks - - return { updatedMessage: clonedMessage, removedTokens, removedToolCalls } -} - -export function compressToolCallsFromContext( - messages: Message[], - excessTokens: number, - supportsFunctionCall: boolean -): { compressedMessages: Message[]; removedTokens: number } { - if (!supportsFunctionCall || excessTokens <= 0) { - return { compressedMessages: messages, removedTokens: 0 } - } - - const messagesToEdit = messages.map((msg) => cloneMessageWithContent(msg)) - - const lastUserIndex = messagesToEdit.findLastIndex((message) => message.role === 'user') - let removedTokens = 0 - - for (let i = 0; i < messagesToEdit.length; i++) { - if (lastUserIndex !== -1 && i >= lastUserIndex) { - break - } - - const message = messagesToEdit[i] - if (message.role !== 'assistant') { - continue - } - - const { - updatedMessage, - removedTokens: toolCallTokens, - removedToolCalls - } = removeToolCallsFromAssistant(message) - - if (removedToolCalls > 0) { - messagesToEdit[i] = updatedMessage - console.debug( - `MessageCompressor: removed ${removedToolCalls} tool call block(s) (${toolCallTokens} tokens) from context` - ) - } - - removedTokens += toolCallTokens - - if (removedTokens >= excessTokens) { - break - } - } - - return { compressedMessages: messagesToEdit, removedTokens } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageTruncator.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageTruncator.ts deleted file mode 100644 index 00767b86d..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageTruncator.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { approximateTokenSize } from 'tokenx' -import type { AssistantMessageBlock, Message } from '@shared/chat' -import type { ChatMessage } from '@shared/presenter' -import { addContextMessages } from './messageFormatter' -import { compressToolCallsFromContext } from './messageCompressor' - -function calculateToolCallTokens(toolCall: NonNullable[number]): number { - const nameTokens = approximateTokenSize(toolCall.function?.name || '') - const argumentsTokens = approximateTokenSize(toolCall.function?.arguments || '') - return nameTokens + argumentsTokens -} - -function calculateMessageTokens(message: ChatMessage): number { - let tokens = 0 - - if (typeof message.content === 'string') { - tokens += approximateTokenSize(message.content) - } else if (Array.isArray(message.content)) { - const textContent = message.content.reduce((acc, item) => { - if (item.type === 'text' && typeof item.text === 'string') { - return acc + item.text - } - return acc - }, '') - tokens += approximateTokenSize(textContent) - } - - if (message.role === 'assistant' && Array.isArray(message.tool_calls)) { - message.tool_calls.forEach((toolCall) => { - tokens += calculateToolCallTokens(toolCall) - }) - } - - return tokens -} - -function calculateMessagesTokens(messages: ChatMessage[]): number { - return messages.reduce((acc, message) => acc + calculateMessageTokens(message), 0) -} - -function cloneMessageWithContent(message: Message): Message { - const cloned: Message = { ...message } - - if (Array.isArray(message.content)) { - cloned.content = message.content.map((block) => { - const clonedBlock: AssistantMessageBlock = { ...(block as AssistantMessageBlock) } - if (block.type === 'tool_call' && block.tool_call) { - clonedBlock.tool_call = { ...block.tool_call } - } - return clonedBlock - }) - } else if (message.content && typeof message.content === 'object') { - cloned.content = JSON.parse(JSON.stringify(message.content)) - } else { - cloned.content = message.content - } - - return cloned -} - -export function selectContextMessages( - contextMessages: Message[], - userMessage: Message, - remainingContextLength: number, - supportsFunctionCall: boolean, - vision: boolean -): Message[] { - if (remainingContextLength <= 0) { - return [] - } - - const messages = contextMessages - .filter((msg) => msg.id !== userMessage?.id) - .map((msg) => cloneMessageWithContent(msg)) - .reverse() - let selectedMessages = messages.filter((msg) => msg.status === 'sent') - - if (selectedMessages.length === 0) { - return [] - } - - let chatMessages = addContextMessages(selectedMessages, vision, supportsFunctionCall) - let totalTokens = calculateMessagesTokens(chatMessages) - console.log('totalTokens', totalTokens, 'remainingContextLength', remainingContextLength) - if (totalTokens > remainingContextLength) { - let excessTokens = totalTokens - remainingContextLength - - if (supportsFunctionCall) { - const { removedTokens } = compressToolCallsFromContext( - selectedMessages, - excessTokens, - supportsFunctionCall - ) - - totalTokens = Math.max(0, totalTokens - removedTokens) - chatMessages = addContextMessages(selectedMessages, vision, supportsFunctionCall) - totalTokens = calculateMessagesTokens(chatMessages) - } - - if (totalTokens > remainingContextLength) { - excessTokens = totalTokens - remainingContextLength - let removedTokens = 0 - - while (removedTokens < excessTokens && selectedMessages.length > 0) { - const userIndex = selectedMessages.findIndex((msg) => msg.role === 'user') - if (userIndex === -1) { - break - } - - const lastUserIndex = selectedMessages.findLastIndex((msg) => msg.role === 'user') - if (userIndex === lastUserIndex) { - break - } - - const userMsg = selectedMessages[userIndex] - const matchingAssistants = selectedMessages - .map((msg, idx) => ({ msg, idx })) - .filter(({ msg }) => msg.role === 'assistant' && msg.parentId === userMsg.id) - .sort((a, b) => a.msg.is_variant - b.msg.is_variant) - - if (matchingAssistants.length === 0) { - selectedMessages.splice(userIndex, 1) - continue - } - - const assistantIndex = matchingAssistants[0].idx - - const startIndex = Math.min(userIndex, assistantIndex) - const endIndex = Math.max(userIndex, assistantIndex) - - const pairMessages = selectedMessages.slice(startIndex, endIndex + 1) - const pairTokens = calculateMessagesTokens( - addContextMessages(pairMessages, vision, supportsFunctionCall) - ) - - console.log( - `PromptBuilder: removing one user/assistant pair from context (${pairTokens} tokens)` - ) - - selectedMessages.splice(startIndex, endIndex - startIndex + 1) - removedTokens += pairTokens - totalTokens = Math.max(0, totalTokens - pairTokens) - } - } - - chatMessages = addContextMessages(selectedMessages, vision, supportsFunctionCall) - totalTokens = calculateMessagesTokens(chatMessages) - } - - const userIds = new Set( - selectedMessages.filter((msg) => msg.role === 'user').map((msg) => msg.id) - ) - selectedMessages = selectedMessages.filter((msg) => { - if (msg.role === 'assistant') { - return msg.parentId && userIds.has(msg.parentId) - } - return true - }) - - return selectedMessages.reverse() -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/skillsPromptBuilder.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/skillsPromptBuilder.ts deleted file mode 100644 index 0f868c42f..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/skillsPromptBuilder.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { AgentPromptRuntimePort } from '../runtimePorts' - -/** - * Build the skills prompt section for the system prompt. - * Loads active skills content and formats them for injection. - * - * @param conversationId - The conversation ID to get active skills for - * @returns A formatted string containing all active skill contents, or empty string if no skills active - */ -export async function buildSkillsPrompt( - promptRuntime: AgentPromptRuntimePort, - conversationId: string -): Promise { - try { - if (!promptRuntime.getSkillsEnabled()) { - return '' - } - - const activeSkills = await promptRuntime.getActiveSkills(conversationId) - - if (activeSkills.length === 0) { - return '' - } - - const skillContents: string[] = [] - - for (const skillName of activeSkills) { - const skillContent = await promptRuntime.loadSkillContent(skillName) - if (skillContent && skillContent.content) { - skillContents.push(`## Skill: ${skillName}\n\n${skillContent.content}`) - } - } - - if (skillContents.length === 0) { - return '' - } - - return `# Active Skills\n\nThe following skills are currently active and provide specialized guidance:\n\n${skillContents.join('\n\n---\n\n')}` - } catch (error) { - console.warn('[SkillsPromptBuilder] Failed to build skills prompt:', error) - return '' - } -} - -/** - * Build the skills metadata prompt section for the system prompt. - * Lists available skills and how to activate them. - * Delegates to skillPresenter.getMetadataPrompt() to avoid code duplication. - */ -export async function buildSkillsMetadataPrompt( - promptRuntime: AgentPromptRuntimePort -): Promise { - try { - if (!promptRuntime.getSkillsEnabled()) { - return '' - } - - return await promptRuntime.getMetadataPrompt() - } catch (error) { - console.warn('[SkillsPromptBuilder] Failed to build skills metadata prompt:', error) - return '' - } -} - -/** - * Get allowed tools from active skills for a conversation. - * Used to extend MCP tool filtering based on skill requirements. - * - * @param conversationId - The conversation ID - * @returns Array of allowed tool names from active skills - */ -export async function getSkillsAllowedTools( - promptRuntime: AgentPromptRuntimePort, - conversationId: string -): Promise { - try { - if (!promptRuntime.getSkillsEnabled()) { - return [] - } - - return await promptRuntime.getActiveSkillsAllowedTools(conversationId) - } catch (error) { - console.warn('[SkillsPromptBuilder] Failed to get skills allowed tools:', error) - return [] - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/permission/permissionHandler.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/permission/permissionHandler.ts deleted file mode 100644 index 5fd0ed4b2..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/permission/permissionHandler.ts +++ /dev/null @@ -1,1221 +0,0 @@ -import type { AssistantMessage, AssistantMessageBlock } from '@shared/chat' -import type { MCPToolDefinition, MCPToolResponse } from '@shared/presenter' -import { buildPostToolExecutionContext, type PendingToolCall } from '../message/messageBuilder' -import type { GeneratingMessageState } from '../streaming/types' -import type { StreamGenerationHandler } from '../streaming/streamGenerationHandler' -import type { LLMEventHandler } from '../streaming/llmEventHandler' -import { BaseHandler, type ThreadHandlerContext } from '../types/handlerContext' -import { CommandPermissionService } from '../../permission/commandPermissionService' -import { eventBus, SendTarget } from '@/eventbus' -import { STREAM_EVENTS } from '@/events' - -// Permission level hierarchy: all > write > read -// 'command' is a special type that only matches itself -const PERMISSION_LEVELS: Record = { - all: 3, - write: 2, - read: 1, - command: 0 // command only matches command (exact match required) -} - -function isPermissionSufficient(granted: string, required: string): boolean { - // Special case: command permission only applies to command-type permissions - if (granted === 'command' || required === 'command') { - return granted === required - } - return (PERMISSION_LEVELS[granted] || 0) >= (PERMISSION_LEVELS[required] || 0) -} - -function canBatchUpdate( - targetPermission: AssistantMessageBlock, - grantedPermission: AssistantMessageBlock, - grantedPermissionType: string -): boolean { - if (targetPermission.status !== 'pending') return false - if (targetPermission.action_type !== 'tool_call_permission') return false - - const targetServerName = targetPermission.extra?.serverName as string - const grantedServerName = grantedPermission.extra?.serverName as string - - // Must be same server - if (targetServerName !== grantedServerName) return false - - // CRITICAL FIX: Only batch the exact same tool call (same tool_call.id) - // This ensures user approval of one tool doesn't auto-approve other tools from the same server - if (targetPermission.tool_call?.id !== grantedPermission.tool_call?.id) return false - - // Check permission type hierarchy - const targetPermissionType = (targetPermission.extra?.permissionType as string) || 'read' - if (!isPermissionSufficient(grantedPermissionType, targetPermissionType)) return false - - // For special permission types, still require exact tool call matching (already checked above) - const targetType = targetPermission.extra?.permissionType as string - if ( - targetType === 'command' || - targetServerName === 'agent-filesystem' || - targetServerName === 'deepchat-settings' - ) { - // Additional safety: these types should never be batched across different tool calls - return targetPermission.tool_call?.id === grantedPermission.tool_call?.id - } - - return true -} - -export class PermissionHandler extends BaseHandler { - private readonly generatingMessages: Map - private readonly streamGenerationHandler: StreamGenerationHandler - private readonly llmEventHandler: LLMEventHandler - private readonly commandPermissionHandler: CommandPermissionService - - constructor( - context: ThreadHandlerContext, - options: { - generatingMessages: Map - streamGenerationHandler: StreamGenerationHandler - llmEventHandler: LLMEventHandler - commandPermissionHandler: CommandPermissionService - } - ) { - super(context) - this.generatingMessages = options.generatingMessages - this.streamGenerationHandler = options.streamGenerationHandler - this.llmEventHandler = options.llmEventHandler - this.commandPermissionHandler = options.commandPermissionHandler - this.assertDependencies() - } - - private assertDependencies(): void { - void this.generatingMessages - void this.streamGenerationHandler - void this.llmEventHandler - void this.commandPermissionHandler - } - - async handlePermissionResponse( - messageId: string, - toolCallId: string, - granted: boolean, - permissionType: 'read' | 'write' | 'all' | 'command', - remember: boolean = true - ): Promise { - console.log('[PermissionHandler] Handling permission response', { - messageId, - toolCallId, - granted, - permissionType, - remember - }) - - try { - const message = await this.ctx.messageManager.getMessage(messageId) - if (!message || message.role !== 'assistant') { - throw new Error(`Message not found or not assistant message (${messageId})`) - } - - const conversationId = message.conversationId - - // Step 1: Update permission blocks status (separate from resume) - const { updatedCount, targetPermissionBlock } = await this.updatePermissionBlocks( - messageId, - toolCallId, - granted, - permissionType - ) - - console.log(`[PermissionHandler] Updated ${updatedCount} permission block(s)`) - - // Debug: if updatedCount is 0, print details - if (updatedCount === 0) { - const content = message.content as AssistantMessageBlock[] - const pendingBlocks = content.filter( - (b) => - b.type === 'action' && - b.action_type === 'tool_call_permission' && - b.status === 'pending' - ) - console.log( - '[PermissionHandler] Debug - All pending permission blocks:', - pendingBlocks.map((b) => ({ - toolCallId: b.tool_call?.id, - status: b.status, - serverName: b.extra?.serverName - })) - ) - console.log('[PermissionHandler] Debug - Looking for toolCallId:', toolCallId) - } - - // Step 2: Remove this permission from pending list (only if we actually updated something) - if (updatedCount > 0) { - this.sessionRuntime.removePendingPermission(conversationId, messageId, toolCallId) - this.notifyFrontendPermissionUpdate(conversationId, messageId) - } else { - console.warn( - '[PermissionHandler] No permission blocks were updated, skipping removal from pending list' - ) - // Still need to notify frontend to refresh in case there's a mismatch - this.notifyFrontendPermissionUpdate(conversationId, messageId) - return - } - - // Step 3: Check if this is ACP permission (special handling) - if (targetPermissionBlock) { - const parsedPermissionRequest = this.parsePermissionRequest(targetPermissionBlock) - const isAcpPermission = this.isAcpPermissionBlock( - targetPermissionBlock, - parsedPermissionRequest - ) - - if (isAcpPermission) { - await this.handleAcpPermissionFlow( - messageId, - targetPermissionBlock, - parsedPermissionRequest, - granted - ) - this.sessionRuntime.clearPendingPermission(conversationId) - this.sessionRuntime.setStatus(conversationId, 'generating') - return - } - } - - // Step 4: Handle specific permission types (command, agent-filesystem, deepchat-settings) - if (targetPermissionBlock && permissionType === 'command') { - if (granted) { - const command = this.getCommandFromPermissionBlock(targetPermissionBlock) - if (!command) { - throw new Error(`Unable to extract command from permission block (${messageId})`) - } - const signature = this.commandPermissionHandler.extractCommandSignature(command) - this.commandPermissionHandler.approve(conversationId, signature, remember) - } - // Continue to check for resume (don't return early) - } else if (targetPermissionBlock && granted && permissionType !== 'command') { - const serverName = targetPermissionBlock?.extra?.serverName as string - if (!serverName) { - throw new Error(`Server name not found in permission block (${messageId})`) - } - - if (serverName === 'agent-filesystem') { - const parsedPermissionRequest = this.parsePermissionRequest(targetPermissionBlock) - const paths = this.getStringArrayFromObject(parsedPermissionRequest, 'paths') - if (paths.length === 0) { - console.warn('[PermissionHandler] Missing filesystem paths in permission request') - // Mark as denied and continue - await this.updatePermissionBlocks(messageId, toolCallId, false, permissionType) - } else { - this.permissionRuntime.approveFileAccess?.(conversationId, paths, remember) - } - } else if (serverName === 'deepchat-settings') { - const parsedPermissionRequest = this.parsePermissionRequest(targetPermissionBlock) - const toolName = - this.getStringFromObject(parsedPermissionRequest, 'toolName') || - this.getStringFromObject( - targetPermissionBlock.extra as Record, - 'toolName' - ) - if (!toolName) { - console.warn('[PermissionHandler] Missing tool name in settings permission request') - await this.updatePermissionBlocks(messageId, toolCallId, false, permissionType) - } else { - this.permissionRuntime.approveSettingsAccess?.(conversationId, toolName, remember) - } - } else { - // MCP server permission - try { - await this.mcpRuntime.grantPermission( - serverName, - permissionType, - remember, - conversationId - ) - await this.waitForMcpServiceReady(serverName) - } catch (error) { - console.error('[PermissionHandler] Failed to grant MCP permission:', error) - throw error - } - } - } - - // Step 5: Check if there are still pending permissions in this message - const hasPendingPermissions = await this.hasPendingPermissionsInMessage(messageId) - if (hasPendingPermissions) { - console.log( - '[PermissionHandler] Still has pending permissions, waiting for all to be resolved' - ) - // Notify frontend to refresh permission UI (show next pending permission) - this.notifyFrontendPermissionUpdate(conversationId, messageId) - return - } - - // Step 6: All permissions resolved - try to acquire resume lock and execute - const lockAcquired = this.sessionRuntime.acquirePermissionResumeLock( - conversationId, - messageId - ) - if (!lockAcquired) { - console.log('[PermissionHandler] Resume already in progress for this message, skipping') - return - } - - // Step 7: Resume tool execution (idempotent - only one resume per message) - // Resume all resolved tool calls in this message (granted and denied handling) - // CRITICAL: Lock is released inside resumeToolExecutionAfterPermissions (success or error) - await this.resumeToolExecutionAfterPermissions(messageId, granted) - } catch (error) { - console.error('[PermissionHandler] Failed to handle permission response:', error) - - // CRITICAL: Ensure lock is released on error (belt-and-suspenders approach) - try { - const conversationId = await this.getConversationIdFromMessage(messageId) - if (conversationId) { - this.sessionRuntime.releasePermissionResumeLock(conversationId) - } - } catch (lockError) { - console.warn('[PermissionHandler] Failed to release lock during error handling:', lockError) - } - - try { - const message = await this.ctx.messageManager.getMessage(messageId) - if (message) { - await this.ctx.messageManager.handleMessageError(messageId, String(error)) - } - } catch (updateError) { - console.error('[PermissionHandler] Failed to update message status:', updateError) - } - - throw error - } - } - - /** - * Update permission block(s) status - * Returns the number of updated blocks and the target permission block - */ - private async updatePermissionBlocks( - messageId: string, - toolCallId: string, - granted: boolean, - permissionType: string - ): Promise<{ updatedCount: number; targetPermissionBlock: AssistantMessageBlock | undefined }> { - const message = await this.ctx.messageManager.getMessage(messageId) - if (!message || message.role !== 'assistant') { - throw new Error(`Message not found or not assistant message (${messageId})`) - } - - const content = message.content as AssistantMessageBlock[] - const targetPermissionBlock = content.find( - (block) => - block.type === 'action' && - block.action_type === 'tool_call_permission' && - block.tool_call?.id === toolCallId - ) - - if (!targetPermissionBlock) { - throw new Error( - `Permission block not found (messageId: ${messageId}, toolCallId: ${toolCallId})` - ) - } - - let updatedCount = 0 - - // Batch update: only update blocks that can be safely batched - for (const block of content) { - if (canBatchUpdate(block, targetPermissionBlock, permissionType)) { - block.status = granted ? 'granted' : 'denied' - if (block.extra) { - block.extra.needsUserAction = false - if (granted) { - // Only store valid MCP permission types; 'command' is handled separately - if ( - permissionType === 'read' || - permissionType === 'write' || - permissionType === 'all' - ) { - block.extra.grantedPermissions = permissionType - } - } - } - updatedCount++ - } - } - - // Update generating state snapshot - const generatingState = this.generatingMessages.get(messageId) - if (generatingState) { - for (let i = 0; i < generatingState.message.content.length; i++) { - const block = generatingState.message.content[i] - if ( - block.type === 'action' && - block.action_type === 'tool_call_permission' && - block.status === 'pending' - ) { - // Check if this block was updated in the main content - const updatedBlock = content.find( - (b) => - b.type === 'action' && - b.action_type === 'tool_call_permission' && - b.tool_call?.id === block.tool_call?.id - ) - if (updatedBlock && updatedBlock.status !== 'pending') { - generatingState.message.content[i] = { - ...block, - status: updatedBlock.status, - extra: updatedBlock.extra - } - } - } - } - } - - await this.ctx.messageManager.editMessage(messageId, JSON.stringify(content)) - - return { updatedCount, targetPermissionBlock } - } - - /** - * Resume stream completion when no tools need to be executed - */ - private async resumeStreamCompletion(conversationId: string, messageId: string): Promise { - // Use streamGenerationHandler's startStreamCompletion which handles the full context - await this.streamGenerationHandler.startStreamCompletion(conversationId, messageId) - } - - /** - * Check if there are still pending permissions in the message - */ - private async hasPendingPermissionsInMessage(messageId: string): Promise { - const message = await this.ctx.messageManager.getMessage(messageId) - if (!message || message.role !== 'assistant') { - return false - } - - const content = message.content as AssistantMessageBlock[] - return content.some( - (block) => - block.type === 'action' && - block.action_type === 'tool_call_permission' && - block.status === 'pending' - ) - } - - /** - * Get conversation ID from message ID - */ - private async getConversationIdFromMessage(messageId: string): Promise { - const message = await this.ctx.messageManager.getMessage(messageId) - if (!message) { - return null - } - return message.conversationId - } - - /** - * Resume tool execution after permission is granted - * CRITICAL SECTION: Lock is held throughout the entire resume flow - * - Early-exit checks prevent stale execution - * - Synchronous flush before executing tools - * - Lock released only at single exit point - * - All tools executed atomically (no lock release between tools) - */ - private async resumeToolExecutionAfterPermissions( - messageId: string, - _lastGranted: boolean, - grantedToolCallId?: string - ): Promise { - console.log( - '[PermissionHandler] Resuming tool execution after permissions', - messageId, - 'grantedToolCallId:', - grantedToolCallId - ) - - const conversationId = await this.getConversationIdFromMessage(messageId) - if (!conversationId) { - throw new Error(`Message not found (${messageId})`) - } - - // CRITICAL SECTION: Lock must be held throughout this entire method - // Early-exit checks: Validate session state before proceeding - const session = this.sessionRuntime.getSessionSync(conversationId) - if (!session) { - console.warn('[PermissionHandler] Session not found, skipping resume:', conversationId) - this.sessionRuntime.releasePermissionResumeLock(conversationId) - return - } - - // Verify the lock is still valid (same message) - const currentLock = this.sessionRuntime.getPermissionResumeLock(conversationId) - if (!currentLock || currentLock.messageId !== messageId) { - console.warn( - '[PermissionHandler] Lock mismatch or expired, skipping resume. Expected:', - messageId, - 'Got:', - currentLock?.messageId - ) - // CRITICAL: Always release lock if we don't proceed - it was acquired in handlePermissionResponse - if (currentLock) { - this.sessionRuntime.releasePermissionResumeLock(conversationId) - } - return - } - - // Ensure status is appropriate for tool execution - // Transition from waiting_permission to generating since we're resuming - const currentStatus = this.sessionRuntime.getStatus(conversationId) - if (currentStatus === 'waiting_permission') { - console.log('[PermissionHandler] Transitioning session from waiting_permission to generating') - this.sessionRuntime.setStatus(conversationId, 'generating') - } else if ( - currentStatus === 'idle' && - this.sessionRuntime.hasPendingPermissions(conversationId, messageId) - ) { - console.warn( - '[PermissionHandler] Session was idle during permission resume, forcing generating status' - ) - this.sessionRuntime.setStatus(conversationId, 'generating') - } else if (currentStatus !== 'generating') { - console.warn( - '[PermissionHandler] Session status not suitable for resume. Status:', - currentStatus - ) - this.sessionRuntime.releasePermissionResumeLock(conversationId) - return - } - - try { - // Step 1: Start the agent loop with pending permissions preservation - // skipLockAcquisition: PermissionHandler already holds the lock - await this.sessionRuntime.startLoop(conversationId, messageId, { - preservePendingPermissions: true, - skipLockAcquisition: true - }) - - // Step 2: Re-fetch message to get updated permission block statuses - const updatedMessage = await this.ctx.messageManager.getMessage(messageId) - if (!updatedMessage) { - throw new Error(`Message not found after permission update (${messageId})`) - } - - // Step 3: Get tool calls to execute - const toolCallsToExecute = this.getToolCallsToExecute( - updatedMessage.content as AssistantMessageBlock[], - grantedToolCallId - ) - - if (toolCallsToExecute.length === 0) { - console.log( - '[PermissionHandler] No tool calls to execute, continuing with stream completion' - ) - await this.resumeStreamCompletion(conversationId, messageId) - // SINGLE EXIT POINT: Release lock - this.sessionRuntime.releasePermissionResumeLock(conversationId) - return - } - - // Step 4: Set up generating state - let state = this.generatingMessages.get(messageId) - if (!state) { - state = { - message: updatedMessage as AssistantMessage, - conversationId, - startTime: Date.now(), - firstTokenTime: null, - promptTokens: 0, - reasoningStartTime: null, - reasoningEndTime: null, - lastReasoningTime: null - } - this.generatingMessages.set(messageId, state) - } else { - // CRITICAL FIX: Sync state.message.content with database to ensure tool_call blocks - // created by the frontend are available for processToolCallEnd to update - state.message.content = (updatedMessage as AssistantMessage).content - } - - // Step 5: SYNCHRONOUS FLUSH before executing tools - // This ensures all pending UI updates are persisted to DB before tool execution - await this.llmEventHandler.flushStreamUpdates(messageId) - - // Step 6: Execute tools sequentially (lock held throughout - NO RELEASE BETWEEN TOOLS) - let hasNewPermissionRequest = false - for (const toolCall of toolCallsToExecute) { - const canContinue = await this.executeSingleToolCall(state, toolCall, conversationId) - - if (!canContinue) { - // Permission required again - but we keep the lock until end of critical section - hasNewPermissionRequest = true - break - } - } - - // Ensure tool_call end/error updates are persisted before rebuilding next-turn context. - await this.llmEventHandler.flushStreamUpdates(messageId) - - // Step 7: Check if there are still pending permissions - const stillHasPending = await this.hasPendingPermissionsInMessage(messageId) - if (stillHasPending || hasNewPermissionRequest) { - console.log( - '[PermissionHandler] Tool(s) executed but more permissions pending, releasing lock and waiting' - ) - // SINGLE EXIT POINT: Release lock - this.sessionRuntime.releasePermissionResumeLock(conversationId) - this.notifyFrontendPermissionUpdate(conversationId, messageId) - return - } - - // Step 8: All permissions resolved, continue with stream completion - await this.continueAfterToolsExecuted(state, conversationId, messageId) - // SINGLE EXIT POINT: Release lock after successful completion - this.sessionRuntime.releasePermissionResumeLock(conversationId) - } catch (error) { - console.error('[PermissionHandler] Failed to resume tool execution:', error) - this.generatingMessages.delete(messageId) - - try { - const message = await this.ctx.messageManager.getMessage(messageId) - if (message) { - await this.ctx.messageManager.handleMessageError(messageId, String(error)) - } - } catch (updateError) { - console.error('[PermissionHandler] Failed to update message error status:', updateError) - } - - // SINGLE EXIT POINT: Ensure lock is released on error - this.sessionRuntime.releasePermissionResumeLock(conversationId) - throw error - } - } - - /** - * Get tool calls that should be executed - * If specificToolCallId is provided, only returns that specific tool call - * Otherwise returns all granted tool calls - */ - private getToolCallsToExecute( - content: AssistantMessageBlock[], - specificToolCallId?: string - ): PendingToolCall[] { - const toolCalls: PendingToolCall[] = [] - - for (const block of content) { - if (block.type !== 'tool_call' || !block.tool_call) continue - - const toolCallId = block.tool_call.id - if (!toolCallId) continue - // Only resume unfinished tool calls. - // Completed blocks (success/error) are historical records and must not be re-executed. - if (block.status !== 'loading') continue - - // If specific tool call ID is specified, only process that one - if (specificToolCallId && toolCallId !== specificToolCallId) { - continue - } - - // Find the associated permission block - const permissionBlock = content.find( - (b) => - b.type === 'action' && - b.action_type === 'tool_call_permission' && - b.tool_call?.id === toolCallId - ) - - // If there's a permission block, check its status - if (permissionBlock) { - if (permissionBlock.status === 'granted') { - const pendingCall = this.buildPendingToolCallFromBlock(block) - if (pendingCall) toolCalls.push(pendingCall) - } else if (permissionBlock.status === 'denied') { - // Denied - will generate error response later - const pendingCall = this.buildPendingToolCallFromBlock(block) - if (pendingCall) { - toolCalls.push({ ...pendingCall, denied: true } as PendingToolCall) - } - } - // If pending, skip (should not happen after permission resolution) - } else { - // No permission block needed - execute directly - const pendingCall = this.buildPendingToolCallFromBlock(block) - if (pendingCall) toolCalls.push(pendingCall) - } - } - - console.log( - `[PermissionHandler] Found ${toolCalls.length} tool calls to execute` + - (specificToolCallId ? ` (specific: ${specificToolCallId})` : ' (all granted)') - ) - return toolCalls - } - - private buildPendingToolCallFromBlock(block: AssistantMessageBlock): PendingToolCall | undefined { - if (!block.tool_call) return undefined - - const { id, name, params } = block.tool_call - if (!id || !name) { - console.warn('[PermissionHandler] Incomplete tool call info:', block.tool_call) - return undefined - } - - return { - id, - name, - params: params || '{}', - serverName: block.tool_call.server_name, - serverIcons: block.tool_call.server_icons, - serverDescription: block.tool_call.server_description - } - } - - /** - * Execute a single tool call - * Returns false if permission is required again - */ - private async executeSingleToolCall( - state: GeneratingMessageState, - toolCall: PendingToolCall, - conversationId: string - ): Promise { - // Check if this tool was denied - const message = await this.ctx.messageManager.getMessage(state.message.id) - if (!message) { - console.warn( - '[PermissionHandler] Message not found while executing tool call, aborting execution:', - state.message.id - ) - return false - } - const content = message.content as AssistantMessageBlock[] - const permissionBlock = content.find( - (b) => - b.type === 'action' && - b.action_type === 'tool_call_permission' && - b.tool_call?.id === toolCall.id - ) - - if (permissionBlock?.status === 'denied') { - // Generate error response for denied tool - await this.llmEventHandler.handleLLMAgentResponse({ - eventId: state.message.id, - tool_call: 'error', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_response: 'User denied the request.', - tool_call_server_name: toolCall.serverName, - tool_call_server_icons: toolCall.serverIcons, - tool_call_server_description: toolCall.serverDescription - } as any) - return true - } - - // Normal tool execution - try { - const { conversation } = await this.streamGenerationHandler.prepareConversationContext( - conversationId, - state.message.id - ) - - let toolDef: MCPToolDefinition | undefined - try { - const { chatMode, agentWorkspacePath } = await this.sessionRuntime.resolveWorkspaceContext( - conversationId, - conversation.settings.modelId - ) - const toolDefinitions = await this.toolPresenter.getAllToolDefinitions({ - enabledMcpTools: conversation.settings.enabledMcpTools, - chatMode, - supportsVision: false, - agentWorkspacePath, - conversationId - }) - toolDef = toolDefinitions.find((definition) => { - if (definition.function.name !== toolCall.name) return false - if (toolCall.serverName) { - return definition.server.name === toolCall.serverName - } - return true - }) - } catch (error) { - console.error('[PermissionHandler] Failed to load tool definitions:', error) - } - - if (!toolDef) { - console.warn('[PermissionHandler] Tool definition not found:', toolCall.name) - return true // Continue with next tool - } - - const resolvedServer = toolDef.server - - // Emit running state - await this.llmEventHandler.handleLLMAgentResponse({ - eventId: state.message.id, - tool_call: 'running', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_server_name: resolvedServer?.name || toolCall.serverName, - tool_call_server_icons: resolvedServer?.icons || toolCall.serverIcons, - tool_call_server_description: resolvedServer?.description || toolCall.serverDescription - } as any) - - // Execute tool - let toolContent = '' - let toolRawData: MCPToolResponse | null = null - try { - const toolCallResult = await this.toolPresenter.callTool({ - id: toolCall.id, - type: 'function', - function: { - name: toolCall.name, - arguments: toolCall.params - }, - server: resolvedServer, - conversationId - }) - toolContent = - typeof toolCallResult.content === 'string' - ? toolCallResult.content - : JSON.stringify(toolCallResult.content) - toolRawData = toolCallResult.rawData - } catch (toolError) { - console.error('[PermissionHandler] Failed to execute tool:', toolError) - await this.llmEventHandler.handleLLMAgentResponse({ - eventId: state.message.id, - tool_call: 'error', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_response: toolError instanceof Error ? toolError.message : String(toolError), - tool_call_server_name: resolvedServer?.name || toolCall.serverName, - tool_call_server_icons: resolvedServer?.icons || toolCall.serverIcons, - tool_call_server_description: resolvedServer?.description || toolCall.serverDescription - } as any) - return true // Continue with next tool - } - - // Check if permission is required again - if (toolRawData?.requiresPermission) { - // Add this permission to pending list and set session status - this.sessionRuntime.addPendingPermission(conversationId, { - messageId: state.message.id, - toolCallId: toolCall.id, - permissionType: - (toolRawData.permissionRequest?.permissionType as - | 'read' - | 'write' - | 'all' - | 'command') || 'read', - payload: toolRawData.permissionRequest ?? {} - }) - this.sessionRuntime.setStatus(conversationId, 'waiting_permission') - - await this.llmEventHandler.handleLLMAgentResponse({ - eventId: state.message.id, - tool_call: 'permission-required', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_server_name: - toolRawData.permissionRequest?.serverName || - resolvedServer?.name || - toolCall.serverName, - tool_call_server_icons: resolvedServer?.icons || toolCall.serverIcons, - tool_call_server_description: resolvedServer?.description || toolCall.serverDescription, - tool_call_response: toolContent, - permission_request: toolRawData.permissionRequest - } as any) - return false // Stop execution, permission required - } - - // Tool completed successfully - await this.llmEventHandler.handleLLMAgentResponse({ - eventId: state.message.id, - tool_call: 'end', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_response: toolContent, - tool_call_server_name: resolvedServer?.name || toolCall.serverName, - tool_call_server_icons: resolvedServer?.icons || toolCall.serverIcons, - tool_call_server_description: resolvedServer?.description || toolCall.serverDescription, - tool_call_response_raw: toolRawData ?? undefined - } as any) - - return true - } catch (error) { - console.error('[PermissionHandler] Error executing single tool call:', error) - return true // Continue with next tool - } - } - - /** - * Continue with model generation after all tools are executed - */ - private async continueAfterToolsExecuted( - _state: GeneratingMessageState, - conversationId: string, - messageId: string - ): Promise { - // Simplified: use streamGenerationHandler which handles full context - await this.streamGenerationHandler.startStreamCompletion(conversationId, messageId) - } - - /** - * Restart agent loop after permission is granted - * UNIFIED FLOW: Uses resumeToolExecutionAfterPermissions for all cases - */ - async restartAgentLoopAfterPermission(messageId: string, toolCallId?: string): Promise { - console.log('[PermissionHandler] Restarting agent loop after permission', messageId) - - try { - const message = await this.ctx.messageManager.getMessage(messageId) - if (!message) { - throw new Error(`Message not found (${messageId})`) - } - - // const conversationId = message.conversationId - - // Check server permissions for logging/debugging - const content = message.content as AssistantMessageBlock[] - const permissionBlock = content.find( - (block) => - block.type === 'action' && - block.action_type === 'tool_call_permission' && - block.tool_call?.id === toolCallId - ) - - if (permissionBlock?.extra?.serverName) { - try { - const servers = await this.ctx.configPresenter.getMcpServers() - const serverConfig = servers[permissionBlock.extra.serverName as string] - console.log('[PermissionHandler] Server permissions:', serverConfig?.autoApprove || []) - } catch (configError) { - console.warn('[PermissionHandler] Failed to verify server permissions:', configError) - } - } - - if (!permissionBlock) { - console.warn('[PermissionHandler] Granted permission block missing; continuing', messageId) - } - - // UNIFIED FLOW: Use resumeToolExecutionAfterPermissions for all cases - // This method handles: - // 1. Setting up the generating state - // 2. Finding and executing all granted tool calls - // 3. Continuing with stream completion - await this.resumeToolExecutionAfterPermissions(messageId, true) - } catch (error) { - console.error('[PermissionHandler] Failed to restart agent loop:', error) - this.generatingMessages.delete(messageId) - - try { - await this.ctx.messageManager.handleMessageError(messageId, String(error)) - } catch (updateError) { - console.error('[PermissionHandler] Failed to update message error status:', updateError) - } - - throw error - } - } - - async continueAfterPermissionDenied( - messageId: string, - resolvedPermissionBlock?: AssistantMessageBlock - ): Promise { - console.log('[PermissionHandler] Continuing after permission denied', messageId) - - try { - const message = await this.ctx.messageManager.getMessage(messageId) - if (!message || message.role !== 'assistant') { - throw new Error(`Message not found or not assistant (${messageId})`) - } - - const conversationId = message.conversationId - // Permission denied flow: skip lock acquisition (not part of permission resume critical section) - await this.sessionRuntime.startLoop(conversationId, messageId, { - preservePendingPermissions: true, - skipLockAcquisition: true - }) - const content = message.content as AssistantMessageBlock[] - const deniedPermissionBlock = - resolvedPermissionBlock || - content.find( - (block) => - block.type === 'action' && - block.action_type === 'tool_call_permission' && - block.status === 'denied' - ) - - if (!deniedPermissionBlock?.tool_call) { - console.warn('[PermissionHandler] No denied permission block for', messageId) - return - } - - const toolCall = deniedPermissionBlock.tool_call - const errorMessage = 'User denied the request.' - - let state = this.generatingMessages.get(messageId) - if (!state) { - state = { - message: message as AssistantMessage, - conversationId, - startTime: Date.now(), - firstTokenTime: null, - promptTokens: 0, - reasoningStartTime: null, - reasoningEndTime: null, - lastReasoningTime: null - } - this.generatingMessages.set(messageId, state) - } - - state.pendingToolCall = undefined - - await this.llmEventHandler.handleLLMAgentResponse({ - eventId: messageId, - tool_call: 'error', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_response: errorMessage, - tool_call_server_name: toolCall.server_name, - tool_call_server_icons: toolCall.server_icons, - tool_call_server_description: toolCall.server_description - } as any) - - const { conversation, contextMessages, userMessage } = - await this.streamGenerationHandler.prepareConversationContext(conversationId, messageId) - - const { - providerId, - modelId, - temperature, - maxTokens, - enabledMcpTools, - thinkingBudget, - reasoningEffort, - verbosity - } = conversation.settings - - const modelConfig = this.ctx.configPresenter.getModelConfig(modelId, providerId) - const completedToolCall = { - id: toolCall.id || '', - name: toolCall.name || '', - params: toolCall.params || '', - response: errorMessage, - serverName: toolCall.server_name, - serverIcons: toolCall.server_icons, - serverDescription: toolCall.server_description - } - - const finalContent = await buildPostToolExecutionContext({ - conversation, - contextMessages, - userMessage, - currentAssistantMessage: state.message, - completedToolCall, - modelConfig - }) - - const stream = this.llmProviderPresenter.startStreamCompletion( - providerId, - finalContent, - modelId, - messageId, - temperature, - maxTokens, - enabledMcpTools, - thinkingBudget, - reasoningEffort, - verbosity, - conversation.id - ) - - for await (const event of stream) { - const msg = event.data - if (event.type === 'response') { - await this.llmEventHandler.handleLLMAgentResponse(msg) - } else if (event.type === 'error') { - await this.llmEventHandler.handleLLMAgentError(msg) - } else if (event.type === 'end') { - await this.llmEventHandler.handleLLMAgentEnd(msg) - } - } - } catch (error) { - console.error('[PermissionHandler] Failed to continue after permission denied:', error) - this.generatingMessages.delete(messageId) - - try { - await this.ctx.messageManager.handleMessageError(messageId, String(error)) - } catch (updateError) { - console.error('[PermissionHandler] Failed to update message error status:', updateError) - } - - throw error - } - } - - async waitForMcpServiceReady(serverName: string, maxWaitTime: number = 3000): Promise { - const startTime = Date.now() - const checkInterval = 100 - - return new Promise((resolve) => { - const checkReady = async () => { - try { - const isRunning = await this.mcpRuntime.isServerRunning(serverName) - if (isRunning) { - setTimeout(() => resolve(), 200) - return - } - - if (Date.now() - startTime > maxWaitTime) { - console.warn('[PermissionHandler] Timeout waiting for MCP service', serverName) - resolve() - return - } - - setTimeout(checkReady, checkInterval) - } catch (error) { - console.error('[PermissionHandler] Error checking MCP service status:', error) - resolve() - } - } - - checkReady() - }) - } - - private parsePermissionRequest(block: AssistantMessageBlock): Record | null { - const raw = this.getExtraString(block, 'permissionRequest') - if (!raw) { - return null - } - try { - return JSON.parse(raw) as Record - } catch (error) { - console.warn('[PermissionHandler] Failed to parse permissionRequest payload:', error) - return null - } - } - - private isAcpPermissionBlock( - block: AssistantMessageBlock, - permissionRequest: Record | null - ): boolean { - const providerIdFromExtra = this.getExtraString(block, 'providerId') - const providerIdFromPayload = this.getStringFromObject(permissionRequest, 'providerId') - return providerIdFromExtra === 'acp' || providerIdFromPayload === 'acp' - } - - private async handleAcpPermissionFlow( - messageId: string, - block: AssistantMessageBlock, - permissionRequest: Record | null, - granted: boolean - ): Promise { - const requestId = - this.getExtraString(block, 'permissionRequestId') || - this.getStringFromObject(permissionRequest, 'requestId') - - if (!requestId) { - throw new Error(`Missing ACP permission request identifier for message ${messageId}`) - } - - await this.ctx.llmProviderPresenter.resolveAgentPermission(requestId, granted) - } - - private getExtraString(block: AssistantMessageBlock, key: string): string | undefined { - const extraValue = block.extra?.[key] - return typeof extraValue === 'string' ? extraValue : undefined - } - - private getStringFromObject( - source: Record | null, - key: string - ): string | undefined { - if (!source) { - return undefined - } - const value = source[key] - return typeof value === 'string' ? value : undefined - } - - private getStringArrayFromObject(source: Record | null, key: string): string[] { - if (!source) return [] - const value = source[key] - if (!Array.isArray(value)) return [] - return value.filter( - (entry): entry is string => typeof entry === 'string' && entry.trim().length > 0 - ) - } - - private getCommandFromPermissionBlock(block: AssistantMessageBlock): string | undefined { - const extraCommandInfo = this.getExtraString(block, 'commandInfo') - if (extraCommandInfo) { - try { - const parsed = JSON.parse(extraCommandInfo) as { command?: string } - if (parsed?.command) { - return parsed.command - } - } catch (error) { - console.warn('[PermissionHandler] Failed to parse commandInfo:', error) - } - } - - const permissionRequest = this.parsePermissionRequest(block) - const commandFromRequest = this.getStringFromObject(permissionRequest, 'command') - if (commandFromRequest) { - return commandFromRequest - } - - const commandInfoFromRequest = permissionRequest?.commandInfo - if (commandInfoFromRequest && typeof commandInfoFromRequest === 'object') { - const commandValue = (commandInfoFromRequest as { command?: unknown }).command - if (typeof commandValue === 'string') { - return commandValue - } - } - - const params = block.tool_call?.params - if (typeof params === 'string' && params.trim()) { - try { - const parsed = JSON.parse(params) as { command?: string } - if (typeof parsed.command === 'string') { - return parsed.command - } - } catch { - // Ignore parse errors; fall through to return undefined. - } - } - - console.warn('[PermissionHandler] No command found in permission block') - return undefined - } - - /** - * Notify frontend to refresh permission UI - * Called when a permission is processed and there may be more pending permissions - */ - private notifyFrontendPermissionUpdate(conversationId: string, messageId: string): void { - try { - const pendingPermissions = this.sessionRuntime.getPendingPermissions(conversationId) ?? [] - const nextPermission = pendingPermissions[0] - console.log('[PermissionHandler] Notifying frontend of permission update:', { - conversationId, - messageId, - remainingCount: pendingPermissions.length, - nextToolCallId: nextPermission?.toolCallId - }) - // Always notify so renderer can clear stale permission UI when count becomes zero. - eventBus.sendToRenderer(STREAM_EVENTS.PERMISSION_UPDATED, SendTarget.ALL_WINDOWS, { - conversationId, - messageId, - type: 'permission_update', - pendingCount: pendingPermissions.length, - nextPermission - }) - } catch (error) { - console.error('[PermissionHandler] Failed to notify frontend:', error) - } - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/persistence/index.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/persistence/index.ts deleted file mode 100644 index 336ce12bb..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/persistence/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/runtimePorts.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/runtimePorts.ts deleted file mode 100644 index f10baf47d..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/runtimePorts.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { - IFilePresenter, - ILlmProviderPresenter, - IMCPPresenter, - IWindowPresenter, - IYoBrowserPresenter -} from '@shared/presenter' -import type { ISkillPresenter, SkillContent } from '@shared/types/skill' - -export interface AgentMcpRuntimePort { - callTool: IMCPPresenter['callTool'] - grantPermission: IMCPPresenter['grantPermission'] - isServerRunning: IMCPPresenter['isServerRunning'] -} - -export interface AgentPromptRuntimePort { - getInputChatMode(): Promise - getSkillsEnabled(): boolean - getActiveSkills(conversationId: string): Promise - loadSkillContent(name: string): Promise - getMetadataPrompt(): Promise - getActiveSkillsAllowedTools(conversationId: string): Promise -} - -export interface AgentPermissionRuntimePort { - approveFileAccess?(conversationId: string, paths: string[], remember: boolean): void - getApprovedFilePaths?(conversationId: string): string[] - approveSettingsAccess?(conversationId: string, toolName: string, remember: boolean): void - consumeSettingsApproval?(conversationId: string, toolName: string): boolean -} - -export interface AgentToolRuntimePort { - resolveConversationWorkdir(conversationId: string): Promise - getSkillPresenter(): ISkillPresenter - getYoBrowserToolHandler(): IYoBrowserPresenter['toolHandler'] - getFilePresenter(): Pick - getLlmProviderPresenter(): Pick - createSettingsWindow(): ReturnType - sendToWindow( - windowId: number, - channel: string, - ...args: unknown[] - ): ReturnType - getApprovedFilePaths?(conversationId: string): string[] - consumeSettingsApproval?(conversationId: string, toolName: string): boolean -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionContext.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionContext.ts deleted file mode 100644 index 86f3e009a..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionContext.ts +++ /dev/null @@ -1,53 +0,0 @@ -export type SessionStatus = - | 'idle' - | 'generating' - | 'paused' - | 'waiting_permission' - | 'waiting_question' - | 'error' - -export type SessionContextResolved = { - chatMode: 'agent' | 'acp agent' - providerId: string - modelId: string - supportsVision: boolean - supportsFunctionCall: boolean - agentWorkspacePath: string | null - enabledMcpTools?: string[] - acpWorkdirMap?: Record -} - -export type PendingPermission = { - messageId: string - toolCallId: string - permissionType: 'read' | 'write' | 'all' | 'command' - payload: unknown -} - -export type PermissionResumeLock = { - messageId: string - startedAt: number -} - -export type SessionContext = { - sessionId: string - agentId: string - status: SessionStatus - createdAt: number - updatedAt: number - resolved: SessionContextResolved - runtime?: { - loopId?: string - currentMessageId?: string - toolCallCount: number - userStopRequested: boolean - pendingPermission?: PendingPermission - pendingPermissions?: PendingPermission[] - permissionResumeLock?: PermissionResumeLock - pendingQuestion?: { - messageId: string - toolCallId: string - } - pendingQuestionInitialized?: boolean - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionManager.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionManager.ts deleted file mode 100644 index 6b571d758..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionManager.ts +++ /dev/null @@ -1,438 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { app } from 'electron' -import type { IConfigPresenter, ISessionPresenter } from '@shared/presenter' -import type { AssistantMessageBlock } from '@shared/chat' -import type { - PendingPermission, - SessionContext, - SessionContextResolved, - SessionStatus -} from './sessionContext' -import { resolveSessionContext, type ChatMode } from './sessionResolver' - -type WorkspaceContext = { - chatMode: ChatMode - agentWorkspacePath: string | null -} - -interface SessionManagerOptions { - configPresenter: IConfigPresenter - sessionPresenter: ISessionPresenter -} - -export class SessionManager { - private readonly sessions = new Map() - private readonly options: SessionManagerOptions - - constructor(options: SessionManagerOptions) { - this.options = options - } - - /** - * Sessions are keyed by agentId (conversationId in agent/chat flows). - * ACP sessions use AcpSessionManager with separate ACP session IDs. - */ - getSessionSync(agentId: string): SessionContext | null { - return this.sessions.get(agentId) ?? null - } - - /** Resolves (or creates) the session keyed by agentId/conversationId. */ - async getSession(agentId: string): Promise { - const existing = this.sessions.get(agentId) - const resolved = await this.resolveSession(agentId) - const now = Date.now() - - if (existing) { - existing.resolved = resolved - existing.updatedAt = now - this.ensureRuntime(existing) - await this.hydratePendingQuestion(existing) - return existing - } - - const session: SessionContext = { - sessionId: agentId, - agentId, - status: 'idle', - createdAt: now, - updatedAt: now, - resolved, - runtime: { - toolCallCount: 0, - userStopRequested: false - } - } - this.sessions.set(agentId, session) - await this.hydratePendingQuestion(session) - return session - } - - async resolveSession(agentId: string): Promise { - const conversation = await this.options.sessionPresenter.getConversation(agentId) - const rawFallback = this.options.configPresenter.getSetting('input_chatMode') as - | string - | undefined - const modelConfig = this.options.configPresenter.getModelDefaultConfig( - conversation.settings.modelId, - conversation.settings.providerId - ) - - const resolved = resolveSessionContext({ - settings: conversation.settings, - fallbackChatMode: rawFallback === 'acp agent' ? 'acp agent' : undefined, - modelConfig - }) - - if (resolved.needsMigration) { - try { - await this.options.sessionPresenter.updateConversationSettings(agentId, { - chatMode: 'agent' - }) - } catch (error) { - console.warn('[SessionManager] Failed to migrate legacy chatMode:', error) - } - } - - if (rawFallback === 'chat') { - try { - await this.options.configPresenter.setSetting('input_chatMode', 'agent') - } catch (error) { - console.warn('[SessionManager] Failed to migrate legacy input_chatMode:', error) - } - } - - if (resolved.chatMode === 'agent') { - resolved.agentWorkspacePath = await this.resolveAgentWorkspacePath( - agentId, - conversation.settings.agentWorkspacePath ?? null - ) - } else if (resolved.chatMode === 'acp agent') { - const modelId = conversation.settings.modelId - resolved.agentWorkspacePath = - modelId && conversation.settings.acpWorkdirMap - ? (conversation.settings.acpWorkdirMap[modelId] ?? null) - : null - resolved.acpWorkdirMap = conversation.settings.acpWorkdirMap - } else { - resolved.agentWorkspacePath = null - } - - return resolved - } - - async resolveWorkspaceContext( - conversationId?: string, - modelId?: string - ): Promise { - if (!conversationId) { - const rawFallback = this.options.configPresenter.getSetting('input_chatMode') as - | string - | undefined - const fallbackChatMode: ChatMode = rawFallback === 'acp agent' ? 'acp agent' : 'agent' - return { chatMode: fallbackChatMode, agentWorkspacePath: null } - } - - try { - const session = await this.getSession(conversationId) - const resolved = session.resolved - if (resolved.chatMode === 'acp agent') { - const resolvedModelId = modelId ?? resolved.modelId - const map = resolved.acpWorkdirMap - const agentWorkspacePath = resolvedModelId && map ? (map[resolvedModelId] ?? null) : null - return { chatMode: resolved.chatMode, agentWorkspacePath } - } - - return { - chatMode: resolved.chatMode, - agentWorkspacePath: resolved.agentWorkspacePath ?? null - } - } catch (error) { - console.warn('[SessionManager] Failed to resolve workspace context:', error) - const rawFallback = this.options.configPresenter.getSetting('input_chatMode') as - | string - | undefined - const fallbackChatMode: ChatMode = rawFallback === 'acp agent' ? 'acp agent' : 'agent' - return { chatMode: fallbackChatMode, agentWorkspacePath: null } - } - } - - async startLoop( - agentId: string, - messageId: string, - options?: { preservePendingPermissions?: boolean; skipLockAcquisition?: boolean } - ): Promise { - const session = await this.getSession(agentId) - session.status = 'generating' - session.updatedAt = Date.now() - const runtime = this.ensureRuntime(session) - runtime.loopId = messageId - runtime.currentMessageId = messageId - runtime.toolCallCount = 0 - runtime.userStopRequested = false - - // CRITICAL: Acquire permission resume lock BEFORE clearing pending permissions - // This ensures atomic state transition during permission resume flow - // skipLockAcquisition is used when PermissionHandler already holds the lock - if (!options?.skipLockAcquisition) { - const hasExistingLock = this.acquirePermissionResumeLock(agentId, messageId) - if (!hasExistingLock) { - console.warn( - `[SessionManager] Lock already exists for message ${messageId}, skipping startLoop initialization` - ) - return - } - } - - // CRITICAL: Only clear pending permissions if not preserving them - // This is essential for multi-tool permission scenarios where we need to - // execute one tool while waiting for approval of others - if (!options?.preservePendingPermissions) { - runtime.pendingPermission = undefined - runtime.pendingPermissions = undefined - } - runtime.pendingQuestion = undefined - - // Note: lock is held via permissionResumeLock, will be released in PermissionHandler - // after all tools in this resume batch are processed - } - - setStatus(agentId: string, status: SessionStatus): void { - const session = this.sessions.get(agentId) - if (!session) return - session.status = status - session.updatedAt = Date.now() - } - - getStatus(agentId: string): SessionStatus | null { - const session = this.sessions.get(agentId) - if (!session) return null - return session.status - } - - updateRuntime(agentId: string, updates: Partial): void { - const session = this.sessions.get(agentId) - if (!session) return - const runtime = this.ensureRuntime(session) - session.runtime = { ...runtime, ...updates } - session.updatedAt = Date.now() - } - - incrementToolCallCount(agentId: string): void { - const session = this.sessions.get(agentId) - if (!session) return - const runtime = this.ensureRuntime(session) - runtime.toolCallCount = (runtime.toolCallCount ?? 0) + 1 - session.updatedAt = Date.now() - } - - clearPendingPermission(agentId: string): void { - this.updateRuntime(agentId, { pendingPermission: undefined, pendingPermissions: undefined }) - } - - clearPendingQuestion(agentId: string): void { - this.updateRuntime(agentId, { pendingQuestion: undefined }) - } - - addPendingPermission(agentId: string, permission: PendingPermission): void { - const session = this.sessions.get(agentId) - if (!session) return - const runtime = this.ensureRuntime(session) - if (!runtime.pendingPermissions) { - runtime.pendingPermissions = [] - } - const existingIndex = runtime.pendingPermissions.findIndex( - (p) => p.messageId === permission.messageId && p.toolCallId === permission.toolCallId - ) - if (existingIndex >= 0) { - runtime.pendingPermissions[existingIndex] = permission - } else { - runtime.pendingPermissions.push(permission) - } - runtime.pendingPermission = runtime.pendingPermissions[0] - session.updatedAt = Date.now() - } - - removePendingPermission(agentId: string, messageId: string, toolCallId: string): void { - const session = this.sessions.get(agentId) - if (!session) return - const runtime = this.ensureRuntime(session) - if (runtime.pendingPermissions) { - runtime.pendingPermissions = runtime.pendingPermissions.filter( - (p) => !(p.messageId === messageId && p.toolCallId === toolCallId) - ) - runtime.pendingPermission = runtime.pendingPermissions[0] - } - session.updatedAt = Date.now() - } - - getPendingPermissions(agentId: string): PendingPermission[] | undefined { - const session = this.sessions.get(agentId) - if (!session) return undefined - const runtime = this.ensureRuntime(session) - return runtime.pendingPermissions - } - - hasPendingPermissions(agentId: string, messageId?: string): boolean { - const pendingPerms = this.getPendingPermissions(agentId) - if (!pendingPerms || pendingPerms.length === 0) return false - if (messageId) { - return pendingPerms.some((p) => p.messageId === messageId) - } - return true - } - - acquirePermissionResumeLock(agentId: string, messageId: string): boolean { - const session = this.sessions.get(agentId) - if (!session) return false - const runtime = this.ensureRuntime(session) - if (runtime.permissionResumeLock?.messageId === messageId) { - return false - } - runtime.permissionResumeLock = { messageId, startedAt: Date.now() } - session.updatedAt = Date.now() - return true - } - - releasePermissionResumeLock(agentId: string): void { - this.updateRuntime(agentId, { permissionResumeLock: undefined }) - } - - private ensureRuntime(session: SessionContext): NonNullable { - if (!session.runtime) { - session.runtime = { - toolCallCount: 0, - userStopRequested: false - } - } else { - if (session.runtime.toolCallCount === undefined) { - session.runtime.toolCallCount = 0 - } - if (session.runtime.userStopRequested === undefined) { - session.runtime.userStopRequested = false - } - } - return session.runtime - } - - getPermissionResumeLock(agentId: string): { messageId: string; startedAt: number } | undefined { - const session = this.sessions.get(agentId) - if (!session) return undefined - const runtime = this.ensureRuntime(session) - return runtime.permissionResumeLock - } - - /** - * Remove a session and clean up all pending state. - * Critical for preventing stale permission data from affecting new sessions. - */ - removeSession(agentId: string): void { - const session = this.sessions.get(agentId) - if (session?.runtime) { - // Clear pending permissions to prevent stale data - session.runtime.pendingPermissions = undefined - session.runtime.pendingPermission = undefined - // Clear permission resume lock - session.runtime.permissionResumeLock = undefined - // Clear pending question - session.runtime.pendingQuestion = undefined - } - this.sessions.delete(agentId) - } - - private async hydratePendingQuestion(session: SessionContext): Promise { - const runtime = this.ensureRuntime(session) - if (runtime.pendingQuestionInitialized) return - runtime.pendingQuestionInitialized = true - if (runtime.pendingQuestion) return - - try { - const lastAssistant = await this.options.sessionPresenter.getLastAssistantMessage( - session.agentId - ) - if (!lastAssistant || lastAssistant.role !== 'assistant') { - return - } - - const blocks = lastAssistant.content as AssistantMessageBlock[] - if (!Array.isArray(blocks) || blocks.length === 0) { - return - } - - const pendingQuestionBlock = [...blocks].reverse().find((block) => { - if ( - block.type !== 'action' || - block.action_type !== 'question_request' || - block.status !== 'pending' - ) { - return false - } - if (block.extra && block.extra.needsUserAction === false) { - return false - } - return Boolean(block.tool_call?.id) - }) - - const toolCallId = pendingQuestionBlock?.tool_call?.id - if (!toolCallId) return - - runtime.pendingQuestion = { - messageId: lastAssistant.id, - toolCallId - } - session.status = 'waiting_question' - session.updatedAt = Date.now() - } catch (error) { - console.warn('[SessionManager] Failed to hydrate pending question:', error) - } - } - - private async resolveAgentWorkspacePath( - conversationId: string | null, - currentPath: string | null - ): Promise { - const trimmedPath = currentPath?.trim() - if (trimmedPath) return trimmedPath - - const fallback = await this.getDefaultAgentWorkspacePath(conversationId) - if (conversationId && fallback) { - try { - await this.options.sessionPresenter.updateConversationSettings(conversationId, { - agentWorkspacePath: fallback - }) - } catch (error) { - console.warn('[SessionManager] Failed to persist agent workspace path:', error) - } - } - return fallback - } - - private async getDefaultAgentWorkspacePath(conversationId?: string | null): Promise { - const tempRoot = path.join(app.getPath('temp'), 'deepchat-agent', 'workspaces') - try { - await fs.promises.mkdir(tempRoot, { recursive: true }) - } catch (error) { - console.warn( - '[SessionManager] Failed to create default workspace root, using system temp:', - error - ) - return app.getPath('temp') - } - - if (!conversationId) { - return tempRoot - } - - const workspaceDir = path.join(tempRoot, conversationId) - try { - await fs.promises.mkdir(workspaceDir, { recursive: true }) - return workspaceDir - } catch (error) { - console.warn( - '[SessionManager] Failed to create conversation workspace, using root temp workspace:', - error - ) - return tempRoot - } - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionResolver.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionResolver.ts deleted file mode 100644 index 614325631..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionResolver.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { CONVERSATION_SETTINGS, ModelConfig } from '@shared/presenter' -import type { SessionContextResolved } from './sessionContext' - -export type ChatMode = 'agent' | 'acp agent' - -export type SessionResolveInput = { - settings: CONVERSATION_SETTINGS - fallbackChatMode?: ChatMode - modelConfig?: ModelConfig -} - -export type SessionResolveResult = SessionContextResolved & { - needsMigration?: boolean -} - -function validateChatMode(mode: string | undefined): ChatMode | undefined { - if (mode === 'acp agent') return 'acp agent' - if (mode === 'agent') return 'agent' - return undefined -} - -function isLegacyChatMode(mode: string | undefined): boolean { - return mode === 'chat' -} - -export function resolveSessionContext(input: SessionResolveInput): SessionResolveResult { - const { settings, modelConfig } = input - - const rawChatMode = settings.chatMode - const rawFallback = input.fallbackChatMode - - const settingsNeedsMigration = isLegacyChatMode(rawChatMode) - - const chatMode = validateChatMode(rawChatMode) ?? validateChatMode(rawFallback) ?? 'agent' - - return { - chatMode, - providerId: settings.providerId, - modelId: settings.modelId, - supportsVision: modelConfig?.vision ?? false, - supportsFunctionCall: modelConfig?.functionCall ?? false, - agentWorkspacePath: chatMode === 'agent' ? (settings.agentWorkspacePath ?? null) : null, - enabledMcpTools: settings.enabledMcpTools, - acpWorkdirMap: chatMode === 'acp agent' ? settings.acpWorkdirMap : undefined, - needsMigration: settingsNeedsMigration - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionRuntimePort.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionRuntimePort.ts deleted file mode 100644 index 41c054308..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/session/sessionRuntimePort.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { - PendingPermission, - PermissionResumeLock, - SessionContext, - SessionStatus -} from './sessionContext' - -export type SessionRuntimeLoopOptions = { - preservePendingPermissions?: boolean - skipLockAcquisition?: boolean -} - -export type SessionRuntimeWorkspaceContext = { - chatMode: 'agent' | 'acp agent' - agentWorkspacePath: string | null -} - -export interface AgentSessionRuntimePort { - getSession(agentId: string): Promise - getSessionSync(agentId: string): SessionContext | null - resolveWorkspaceContext( - conversationId?: string, - modelId?: string - ): Promise - startLoop(agentId: string, messageId: string, options?: SessionRuntimeLoopOptions): Promise - setStatus(agentId: string, status: SessionStatus): void - getStatus(agentId: string): SessionStatus | null - updateRuntime(agentId: string, updates: Partial): void - incrementToolCallCount(agentId: string): void - clearPendingPermission(agentId: string): void - clearPendingQuestion(agentId: string): void - addPendingPermission(agentId: string, permission: PendingPermission): void - removePendingPermission(agentId: string, messageId: string, toolCallId: string): void - getPendingPermissions(agentId: string): PendingPermission[] | undefined - hasPendingPermissions(agentId: string, messageId?: string): boolean - acquirePermissionResumeLock(agentId: string, messageId: string): boolean - releasePermissionResumeLock(agentId: string): void - getPermissionResumeLock(agentId: string): PermissionResumeLock | undefined -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/contentBufferHandler.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/contentBufferHandler.ts deleted file mode 100644 index 386c46b63..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/contentBufferHandler.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { finalizeAssistantMessageBlocks } from '@shared/chat/messageBlocks' -import type { GeneratingMessageState } from './types' -import type { StreamUpdateScheduler } from './streamUpdateScheduler' - -export class ContentBufferHandler { - private readonly generatingMessages: Map - private readonly streamUpdateScheduler: StreamUpdateScheduler - - constructor(options: { - generatingMessages: Map - streamUpdateScheduler: StreamUpdateScheduler - }) { - this.generatingMessages = options.generatingMessages - this.streamUpdateScheduler = options.streamUpdateScheduler - } - - async flushAdaptiveBuffer(eventId: string): Promise { - const state = this.generatingMessages.get(eventId) - if (!state?.adaptiveBuffer) return - - const buffer = state.adaptiveBuffer - const now = Date.now() - - if (state.flushTimeout) { - clearTimeout(state.flushTimeout) - state.flushTimeout = undefined - } - - try { - if (buffer.content && buffer.sentPosition < buffer.content.length) { - const newContent = buffer.content.slice(buffer.sentPosition) - if (newContent) { - await this.processBufferedContent(state, eventId, newContent, now) - buffer.sentPosition = buffer.content.length - } - } - } catch (error) { - console.error('[ContentBufferHandler] ERROR flushing adaptive buffer', { - eventId, - err: error - }) - throw error - } finally { - state.adaptiveBuffer = undefined - } - } - - async processBufferedContent( - state: GeneratingMessageState, - eventId: string, - content: string, - currentTime: number - ): Promise { - const buffer = state.adaptiveBuffer - - if (buffer?.isLargeContent) { - await this.processLargeContentAsynchronously(state, eventId, content, currentTime) - return - } - - await this.processNormalContent(state, eventId, content, currentTime) - } - - async processLargeContentAsynchronously( - state: GeneratingMessageState, - eventId: string, - content: string, - currentTime: number - ): Promise { - const buffer = state.adaptiveBuffer - if (!buffer) return - - buffer.isProcessing = true - - try { - const chunks = this.splitLargeContent(content) - const totalChunks = chunks.length - - console.log( - `[ContentBufferHandler] Processing ${totalChunks} chunks asynchronously for ${content.length} bytes` - ) - - const lastBlock = state.message.content[state.message.content.length - 1] - let contentBlock: any - - if (lastBlock && lastBlock.type === 'content') { - contentBlock = lastBlock - } else { - this.finalizeLastBlock(state) - contentBlock = { - type: 'content', - content: '', - status: 'loading', - timestamp: currentTime - } - state.message.content.push(contentBlock) - } - - const batchSize = 5 - for (let batchStart = 0; batchStart < chunks.length; batchStart += batchSize) { - const batchEnd = Math.min(batchStart + batchSize, chunks.length) - const batch = chunks.slice(batchStart, batchEnd) - - const batchContent = batch.join('') - contentBlock.content += batchContent - - this.streamUpdateScheduler.enqueueDelta( - eventId, - state.conversationId, - state.message.parentId, - Boolean(state.message.is_variant), - state.webContentsId, - { content: batchContent }, - state.message.content - ) - - if (batchEnd < chunks.length) { - await new Promise((resolve) => setImmediate(resolve)) - } - } - - console.log(`[ContentBufferHandler] Completed processing ${totalChunks} chunks`) - } catch (error) { - console.error('[ContentBufferHandler] Error in processLargeContentAsynchronously:', error) - } finally { - buffer.isProcessing = false - } - } - - async processNormalContent( - state: GeneratingMessageState, - eventId: string, - content: string, - currentTime: number - ): Promise { - const lastBlock = state.message.content[state.message.content.length - 1] - - if (lastBlock && lastBlock.type === 'content') { - lastBlock.content += content - } else { - this.finalizeLastBlock(state) - state.message.content.push({ - type: 'content', - content, - status: 'loading', - timestamp: currentTime - }) - } - - this.streamUpdateScheduler.enqueueDelta( - eventId, - state.conversationId, - state.message.parentId, - Boolean(state.message.is_variant), - state.webContentsId, - { content }, - state.message.content - ) - } - - splitLargeContent(content: string): string[] { - const chunks: string[] = [] - let maxChunkSize = 4096 - - if (content.includes('data:image/')) { - maxChunkSize = 512 - } - - if (content.length > 50000) { - maxChunkSize = Math.min(maxChunkSize, 256) - } - - for (let i = 0; i < content.length; i += maxChunkSize) { - chunks.push(content.slice(i, i + maxChunkSize)) - } - - return chunks - } - - cleanupContentBuffer(state: GeneratingMessageState): void { - if (state.flushTimeout) { - clearTimeout(state.flushTimeout) - state.flushTimeout = undefined - } - if (state.throttleTimeout) { - clearTimeout(state.throttleTimeout) - state.throttleTimeout = undefined - } - state.adaptiveBuffer = undefined - state.lastRendererUpdateTime = undefined - } - - private finalizeLastBlock(state: GeneratingMessageState): void { - finalizeAssistantMessageBlocks(state.message.content) - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts deleted file mode 100644 index 404d8b85a..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts +++ /dev/null @@ -1,569 +0,0 @@ -import { eventBus, SendTarget } from '@/eventbus' -import { CONVERSATION_EVENTS, STREAM_EVENTS } from '@/events' -import type { AssistantMessageBlock } from '@shared/chat' -import { finalizeAssistantMessageBlocks } from '@shared/chat/messageBlocks' -import type { LLMAgentEventData, MESSAGE_METADATA } from '@shared/presenter' -import { approximateTokenSize } from 'tokenx' -import type { MessageManager } from '../../sessionPresenter/managers/messageManager' -import type { GeneratingMessageState } from './types' -import type { ContentBufferHandler } from './contentBufferHandler' -import type { ToolCallHandler } from '../loop/toolCallHandler' -import type { StreamUpdateScheduler } from './streamUpdateScheduler' -import type { AgentSessionRuntimePort } from '../session/sessionRuntimePort' - -type ConversationUpdateHandler = (state: GeneratingMessageState) => Promise - -type HookErrorSnapshot = { - error: { message: string; stack?: string } - usage?: Record | null - conversationId?: string - providerId?: string - modelId?: string -} - -export class LLMEventHandler { - private readonly generatingMessages: Map - private readonly messageManager: MessageManager - private readonly contentBufferHandler: ContentBufferHandler - private readonly toolCallHandler: ToolCallHandler - private readonly streamUpdateScheduler: StreamUpdateScheduler - private readonly sessionRuntime: AgentSessionRuntimePort - private readonly onConversationUpdated?: ConversationUpdateHandler - private readonly errorByEventId: Map = new Map() - - constructor(options: { - generatingMessages: Map - messageManager: MessageManager - contentBufferHandler: ContentBufferHandler - toolCallHandler: ToolCallHandler - streamUpdateScheduler: StreamUpdateScheduler - sessionRuntime: AgentSessionRuntimePort - onConversationUpdated?: ConversationUpdateHandler - }) { - this.generatingMessages = options.generatingMessages - this.messageManager = options.messageManager - this.contentBufferHandler = options.contentBufferHandler - this.toolCallHandler = options.toolCallHandler - this.streamUpdateScheduler = options.streamUpdateScheduler - this.sessionRuntime = options.sessionRuntime - this.onConversationUpdated = options.onConversationUpdated - } - - async handleLLMAgentResponse(msg: LLMAgentEventData): Promise { - const currentTime = Date.now() - const { - eventId, - content, - reasoning_content, - tool_call_id, - tool_call_name, - tool_call_params, - maximum_tool_calls_reached, - tool_call_server_name, - tool_call_server_icons, - tool_call_server_description, - tool_call_response_raw, - tool_call, - question_request, - question_error, - totalUsage, - image_data - } = msg - - const state = this.generatingMessages.get(eventId) - if (!state) { - return - } - - if (state.firstTokenTime === null && (content || reasoning_content)) { - state.firstTokenTime = currentTime - await this.messageManager.updateMessageMetadata(eventId, { - firstTokenTime: currentTime - state.startTime - }) - } - - if (totalUsage) { - state.totalUsage = totalUsage - state.promptTokens = totalUsage.prompt_tokens - } - - if (maximum_tool_calls_reached) { - this.finalizeLastBlock(state) - state.message.content.push({ - type: 'action', - content: 'common.error.maximumToolCallsReached', - status: 'success', - timestamp: currentTime, - action_type: 'maximum_tool_calls_reached', - tool_call: { - id: tool_call_id, - name: tool_call_name, - params: tool_call_params, - server_name: tool_call_server_name, - server_icons: tool_call_server_icons, - server_description: tool_call_server_description - }, - extra: { needContinue: true } - }) - this.streamUpdateScheduler.enqueueDelta( - eventId, - state.conversationId, - state.message.parentId, - Boolean(state.message.is_variant), - state.webContentsId, - {}, - state.message.content - ) - return - } - - if (question_error) { - this.finalizeLastBlock(state) - state.message.content.push({ - type: 'error', - content: question_error, - status: 'error', - timestamp: currentTime - }) - } - - if (reasoning_content) { - if (state.reasoningStartTime === null) { - state.reasoningStartTime = currentTime - await this.messageManager.updateMessageMetadata(eventId, { - reasoningStartTime: currentTime - state.startTime - }) - } - state.lastReasoningTime = currentTime - // Update reasoningEndTime in metadata for real-time display - await this.messageManager.updateMessageMetadata(eventId, { - reasoningEndTime: currentTime - state.startTime - }) - } - - const lastBlock = state.message.content[state.message.content.length - 1] - - if (tool_call_response_raw && tool_call === 'end') { - await this.toolCallHandler.processSearchResultsFromToolCall(state, msg, currentTime) - await this.toolCallHandler.processMcpUiResourcesFromToolCall(state, msg, currentTime) - } - - const shouldSkipToolCall = - tool_call && tool_call_name === 'question' && tool_call !== 'question-required' - const isAcpProvider = state.message.model_provider === 'acp' - - if (tool_call && !shouldSkipToolCall) { - if (isAcpProvider) { - // Legacy hook dispatch disabled. New session hooks are emitted by the new agent pipeline. - } - - switch (tool_call) { - case 'start': - this.sessionRuntime.incrementToolCallCount(state.conversationId) - await this.toolCallHandler.processToolCallStart(state, msg, currentTime) - break - case 'update': - case 'running': - await this.toolCallHandler.processToolCallUpdate(state, msg) - break - case 'permission-required': - this.sessionRuntime.addPendingPermission(state.conversationId, { - messageId: eventId, - toolCallId: tool_call_id || '', - permissionType: - (msg.permission_request?.permissionType as 'read' | 'write' | 'all' | 'command') || - 'read', - payload: msg.permission_request ?? {} - }) - this.sessionRuntime.setStatus(state.conversationId, 'waiting_permission') - await this.toolCallHandler.processToolCallPermission(state, msg, currentTime) - break - case 'question-required': - this.sessionRuntime.updateRuntime(state.conversationId, { - pendingQuestion: { - messageId: eventId, - toolCallId: tool_call_id || '' - } - }) - this.sessionRuntime.setStatus(state.conversationId, 'waiting_question') - await this.toolCallHandler.processQuestionRequest(state, msg, currentTime) - break - case 'permission-granted': - case 'permission-denied': - case 'continue': - await this.toolCallHandler.processToolCallPermission(state, msg, currentTime) - break - case 'error': - await this.toolCallHandler.processToolCallError(state, msg) - break - case 'end': - await this.toolCallHandler.processToolCallEnd(state, msg) - break - default: - break - } - } - - if (image_data?.data) { - const rawData = image_data.data ?? '' - let normalizedData = rawData - let normalizedMimeType = image_data.mimeType?.trim() ?? '' - - if ( - rawData.startsWith('imgcache://') || - rawData.startsWith('http://') || - rawData.startsWith('https://') - ) { - normalizedMimeType = 'deepchat/image-url' - } else if (rawData.startsWith('data:image/')) { - const match = rawData.match(/^data:([^;]+);base64,(.*)$/) - if (match?.[1] && match?.[2]) { - normalizedMimeType = match[1] - normalizedData = match[2] - } - } else if (!normalizedMimeType) { - normalizedMimeType = 'image/png' - } - - const imageBlock: AssistantMessageBlock = { - type: 'image', - status: 'success', - timestamp: currentTime, - content: 'image', - image_data: { - data: normalizedData, - mimeType: normalizedMimeType - } - } - state.message.content.push(imageBlock) - } - - if (content) { - if (!lastBlock || lastBlock.type !== 'content' || lastBlock.status !== 'loading') { - this.finalizeLastBlock(state) - state.message.content.push({ - type: 'content', - content: content || '', - status: 'loading', - timestamp: currentTime - }) - } else if (lastBlock.type === 'content') { - lastBlock.content += content - } - } - - if (reasoning_content) { - // Re-get lastBlock in case new blocks were added above - const currentLastBlock = state.message.content[state.message.content.length - 1] - if (!currentLastBlock || currentLastBlock.type !== 'reasoning_content') { - this.finalizeLastBlock(state) - const reasoningStartTime = currentTime - state.message.content.push({ - type: 'reasoning_content', - content: reasoning_content || '', - status: 'loading', - timestamp: currentTime, - reasoning_time: { - start: reasoningStartTime, - end: currentTime - } - }) - } else if (currentLastBlock.type === 'reasoning_content') { - currentLastBlock.content += reasoning_content - // Update reasoning_time.end in real-time during streaming - if (currentLastBlock.reasoning_time) { - currentLastBlock.reasoning_time.end = currentTime - } else { - const reasoningStartTime = currentLastBlock.timestamp ?? currentTime - currentLastBlock.reasoning_time = { - start: reasoningStartTime, - end: currentTime - } - } - } - } - - const delta: Partial = {} - if (content) delta.content = content - if (reasoning_content) { - delta.reasoning_content = reasoning_content - // Get the current reasoning_time from the last reasoning_content block - const lastBlock = state.message.content[state.message.content.length - 1] - if (lastBlock?.type === 'reasoning_content' && lastBlock.reasoning_time) { - delta.reasoning_time = lastBlock.reasoning_time - } - } - if (image_data) delta.image_data = image_data - if (totalUsage) delta.totalUsage = totalUsage - - if (tool_call && !shouldSkipToolCall) { - delta.tool_call = tool_call - delta.tool_call_id = tool_call_id - delta.tool_call_name = tool_call_name - delta.tool_call_params = tool_call_params - delta.tool_call_response = msg.tool_call_response - delta.tool_call_server_name = tool_call_server_name - delta.tool_call_server_icons = tool_call_server_icons - delta.tool_call_server_description = tool_call_server_description - delta.tool_call_response_raw = tool_call_response_raw - if (msg.permission_request !== undefined) { - delta.permission_request = msg.permission_request - } - if (question_request !== undefined) { - delta.question_request = question_request - } - } - - if (question_error) { - delta.question_error = question_error - } - - this.streamUpdateScheduler.enqueueDelta( - eventId, - state.conversationId, - state.message.parentId, - Boolean(state.message.is_variant), - state.webContentsId, - delta, - state.message.content - ) - } - - async handleLLMAgentError(msg: LLMAgentEventData): Promise { - const { eventId, error } = msg - const errorMessage = error instanceof Error ? error.message : String(error) - const errorStack = error instanceof Error ? error.stack : undefined - const state = this.generatingMessages.get(eventId) - const errorSnapshot: HookErrorSnapshot = { - error: errorStack ? { message: errorMessage, stack: errorStack } : { message: errorMessage }, - usage: state?.totalUsage, - conversationId: state?.conversationId, - providerId: state?.message.model_provider, - modelId: state?.message.model_id - } - - if (!errorSnapshot.conversationId) { - try { - const message = await this.messageManager.getMessage(eventId) - errorSnapshot.conversationId = message.conversationId - errorSnapshot.providerId = errorSnapshot.providerId ?? message.model_provider - errorSnapshot.modelId = errorSnapshot.modelId ?? message.model_id - } catch { - // ignore - } - } - - this.errorByEventId.set(eventId, errorSnapshot) - - if (state) { - if (state.adaptiveBuffer) { - await this.contentBufferHandler.flushAdaptiveBuffer(eventId) - } - - this.contentBufferHandler.cleanupContentBuffer(state) - } - - // Flush stream buffers before persisting error to avoid stale snapshot overwrites. - await this.streamUpdateScheduler.flushAll(eventId, 'final') - - await this.messageManager.handleMessageError(eventId, errorMessage) - - if (state) { - this.generatingMessages.delete(eventId) - this.sessionRuntime.setStatus(state.conversationId, 'error') - this.sessionRuntime.clearPendingPermission(state.conversationId) - this.sessionRuntime.clearPendingQuestion(state.conversationId) - } else { - const message = await this.messageManager.getMessage(eventId) - if (message) { - this.sessionRuntime.setStatus(message.conversationId, 'error') - this.sessionRuntime.clearPendingPermission(message.conversationId) - this.sessionRuntime.clearPendingQuestion(message.conversationId) - } - } - - eventBus.sendToRenderer(STREAM_EVENTS.ERROR, SendTarget.ALL_WINDOWS, msg) - } - - async handleLLMAgentEnd(msg: LLMAgentEventData): Promise { - const { eventId, userStop } = msg - const state = this.generatingMessages.get(eventId) - if (state) { - if (state.adaptiveBuffer) { - await this.contentBufferHandler.flushAdaptiveBuffer(eventId) - } - - this.contentBufferHandler.cleanupContentBuffer(state) - - const hasPendingPermissions = state.message.content.some( - (block) => - block.type === 'action' && - block.action_type === 'tool_call_permission' && - block.status === 'pending' - ) - const hasPendingQuestions = state.message.content.some( - (block) => - block.type === 'action' && - block.action_type === 'question_request' && - block.status === 'pending' - ) - - if (hasPendingPermissions || hasPendingQuestions) { - state.message.content.forEach((block) => { - if ( - !( - block.type === 'action' && - (block.action_type === 'tool_call_permission' || - block.action_type === 'question_request') - ) && - block.status === 'loading' - ) { - if (block.type !== 'tool_call') { - block.status = 'success' - } - } - }) - this.streamUpdateScheduler.enqueueDelta( - eventId, - state.conversationId, - state.message.parentId, - Boolean(state.message.is_variant), - state.webContentsId, - {}, - state.message.content - ) - if (hasPendingQuestions) { - // Question tool ends the assistant message even when waiting for user input. - await this.messageManager.updateMessageStatus(eventId, 'sent') - } - this.sessionRuntime.setStatus(state.conversationId, 'waiting_permission') - if (!hasPendingPermissions) { - this.sessionRuntime.setStatus(state.conversationId, 'waiting_question') - } - await this.streamUpdateScheduler.flushAll(eventId, 'final') - this.generatingMessages.delete(eventId) - eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, msg) - this.errorByEventId.delete(eventId) - return - } - - await this.finalizeMessage(state, eventId, Boolean(userStop)) - this.sessionRuntime.setStatus(state.conversationId, 'idle') - this.sessionRuntime.clearPendingPermission(state.conversationId) - this.sessionRuntime.clearPendingQuestion(state.conversationId) - } - - try { - // Legacy hook dispatch disabled. New session hooks are emitted by the new agent pipeline. - } finally { - this.errorByEventId.delete(eventId) - } - - await this.streamUpdateScheduler.flushAll(eventId, 'final') - eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, msg) - } - - async finalizeMessage( - state: GeneratingMessageState, - eventId: string, - userStop: boolean - ): Promise { - state.message.content.forEach((block) => { - if ( - block.type === 'action' && - (block.action_type === 'tool_call_permission' || block.action_type === 'question_request') - ) { - return - } - block.status = 'success' - }) - - let completionTokens = 0 - if (state.totalUsage) { - completionTokens = state.totalUsage.completion_tokens - } else { - for (const block of state.message.content) { - if ( - block.type === 'content' || - block.type === 'reasoning_content' || - block.type === 'tool_call' - ) { - completionTokens += approximateTokenSize(block.content) - } - } - } - - const hasContentBlock = state.message.content.some( - (block) => - block.type === 'content' || - block.type === 'reasoning_content' || - block.type === 'tool_call' || - block.type === 'image' - ) - - if (!hasContentBlock && !userStop) { - state.message.content.push({ - type: 'error', - content: 'common.error.noModelResponse', - status: 'error', - timestamp: Date.now() - }) - } - - const totalTokens = state.promptTokens + completionTokens - const generationTime = Date.now() - (state.firstTokenTime ?? state.startTime) - const safeMs = Math.max(1, generationTime) - const tokensPerSecond = completionTokens / (safeMs / 1000) - const contextUsage = state?.totalUsage?.context_length - ? (totalTokens / state.totalUsage.context_length) * 100 - : 0 - - const metadata: Partial = { - totalTokens, - inputTokens: state.promptTokens, - outputTokens: completionTokens, - generationTime, - firstTokenTime: state.firstTokenTime ? state.firstTokenTime - state.startTime : 0, - tokensPerSecond, - contextUsage - } - - if (state.reasoningStartTime !== null && state.lastReasoningTime !== null) { - metadata.reasoningStartTime = state.reasoningStartTime - state.startTime - metadata.reasoningEndTime = state.lastReasoningTime - state.startTime - } - - await this.messageManager.updateMessageMetadata(eventId, metadata) - await this.messageManager.updateMessageStatus(eventId, 'sent') - await this.messageManager.editMessage(eventId, JSON.stringify(state.message.content)) - this.generatingMessages.delete(eventId) - - if (this.onConversationUpdated) { - await this.onConversationUpdated(state) - } - - const finalMessage = await this.messageManager.getMessage(eventId) - if (finalMessage) { - eventBus.sendToMain(CONVERSATION_EVENTS.MESSAGE_GENERATED, { - conversationId: finalMessage.conversationId, - message: finalMessage - }) - } - } - - finalizeLastBlock(state: GeneratingMessageState): void { - finalizeAssistantMessageBlocks(state.message.content) - } - - /** - * Flush all pending stream updates for a message - * Used during permission resume to ensure UI state is synchronized with DB - */ - async flushStreamUpdates(eventId: string): Promise { - try { - await this.streamUpdateScheduler.flushAll(eventId, 'final') - } catch (error) { - console.error('[LLMEventHandler] Failed to flush stream updates:', error) - } - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts deleted file mode 100644 index c91f30339..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts +++ /dev/null @@ -1,597 +0,0 @@ -import { eventBus, SendTarget } from '@/eventbus' -import { STREAM_EVENTS } from '@/events' -import type { - AssistantMessage, - AssistantMessageBlock, - Message, - MessageFile, - UserMessage, - UserMessageContent -} from '@shared/chat' -import type { CONVERSATION, MCPToolResponse } from '@shared/presenter' -import { buildUserMessageContext, formatUserMessageContent } from '../message/messageFormatter' -import { preparePromptContent } from '../message/messageBuilder' -import type { GeneratingMessageState } from './types' -import { BaseHandler, type ThreadHandlerContext } from '../types/handlerContext' -import type { LLMEventHandler } from './llmEventHandler' -import { LoopOrchestrator } from '../loop/loopOrchestrator' - -interface StreamGenerationHandlerDeps { - generatingMessages: Map - llmEventHandler: LLMEventHandler -} - -export class StreamGenerationHandler extends BaseHandler { - private readonly generatingMessages: Map - private readonly llmEventHandler: LLMEventHandler - private readonly loopOrchestrator: LoopOrchestrator - - constructor(context: ThreadHandlerContext, deps: StreamGenerationHandlerDeps) { - super(context) - this.generatingMessages = deps.generatingMessages - this.llmEventHandler = deps.llmEventHandler - this.loopOrchestrator = new LoopOrchestrator(this.llmEventHandler) - this.assertDependencies() - } - - private assertDependencies(): void { - void this.generatingMessages - void this.llmEventHandler - void this.loopOrchestrator - } - - async startStreamCompletion( - conversationId: string, - queryMsgId?: string, - selectedVariantsMap?: Record - ): Promise { - const state = this.findGeneratingState(conversationId) - if (!state) { - console.warn('[StreamGenerationHandler] State not found, conversationId:', conversationId) - return - } - - try { - state.isCancelled = false - // Normal flow: skip lock acquisition (lock is only for permission resume) - await this.sessionRuntime.startLoop(conversationId, state.message.id, { - skipLockAcquisition: true - }) - - const { conversation, userMessage, contextMessages } = await this.prepareConversationContext( - conversationId, - queryMsgId, - selectedVariantsMap - ) - - const { chatMode, agentWorkspacePath } = await this.sessionRuntime.resolveWorkspaceContext( - conversationId, - conversation.settings.modelId - ) - if (chatMode === 'agent' && agentWorkspacePath) { - conversation.settings.agentWorkspacePath = agentWorkspacePath - } - - const { providerId, modelId } = conversation.settings - const modelConfig = this.ctx.configPresenter.getModelConfig(modelId, providerId) - if (!modelConfig) { - throw new Error(`Model config not found for provider ${providerId} and model ${modelId}`) - } - - this.throwIfCancelled(state.message.id) - - const { userContent, imageFiles } = await this.processUserMessageContent( - userMessage as UserMessage - ) - - this.throwIfCancelled(state.message.id) - - const { finalContent, promptTokens } = await preparePromptContent({ - conversation, - userContent, - contextMessages, - searchResults: null, - userMessage, - vision: Boolean(modelConfig?.vision), - imageFiles: modelConfig?.vision ? imageFiles : [], - supportsFunctionCall: modelConfig.functionCall, - modelType: modelConfig.type, - toolPresenter: this.toolPresenter, - promptRuntime: this.promptRuntime - }) - - this.throwIfCancelled(state.message.id) - - await this.updateGenerationState(state, promptTokens) - - this.throwIfCancelled(state.message.id) - - const currentConversation = await this.getConversation(conversationId) - const { - providerId: currentProviderId, - modelId: currentModelId, - temperature: currentTemperature, - maxTokens: currentMaxTokens, - enabledMcpTools: currentEnabledMcpTools, - thinkingBudget: currentThinkingBudget, - reasoningEffort: currentReasoningEffort, - verbosity: currentVerbosity - } = currentConversation.settings - - const stream = this.ctx.llmProviderPresenter.startStreamCompletion( - currentProviderId, - finalContent, - currentModelId, - state.message.id, - currentTemperature, - currentMaxTokens, - currentEnabledMcpTools, - currentThinkingBudget, - currentReasoningEffort, - currentVerbosity, - conversationId - ) - - await this.loopOrchestrator.consume(stream) - } catch (error) { - if (String(error).includes('userCanceledGeneration')) { - console.log('[StreamGenerationHandler] Message generation cancelled by user') - return - } - - console.error('[StreamGenerationHandler] Error during streaming generation:', error) - await this.ctx.messageManager.handleMessageError(state.message.id, String(error)) - throw error - } - } - - async continueStreamCompletion( - conversationId: string, - queryMsgId: string, - selectedVariantsMap?: Record - ): Promise { - const state = this.findGeneratingState(conversationId) - if (!state) { - console.warn('[StreamGenerationHandler] State not found, conversationId:', conversationId) - return - } - - try { - state.isCancelled = false - // Normal flow: skip lock acquisition (lock is only for permission resume) - await this.sessionRuntime.startLoop(conversationId, state.message.id, { - skipLockAcquisition: true - }) - - const queryMessage = await this.ctx.messageManager.getMessage(queryMsgId) - if (!queryMessage) { - throw new Error('Message not found') - } - - const content = queryMessage.content as AssistantMessageBlock[] - const lastActionBlock = content.filter((block) => block.type === 'action').pop() - - if (!lastActionBlock || lastActionBlock.type !== 'action') { - throw new Error('Last action block not found') - } - - let toolCallResponse: { content: string; rawData: MCPToolResponse } | null = null - const toolCall = lastActionBlock.tool_call - - if (lastActionBlock.action_type === 'maximum_tool_calls_reached' && toolCall) { - if (lastActionBlock.extra) { - lastActionBlock.extra = { - ...lastActionBlock.extra, - needContinue: false - } - } - await this.ctx.messageManager.editMessage(queryMsgId, JSON.stringify(content)) - - if (!toolCall.id || !toolCall.name || !toolCall.params) { - console.warn('[StreamGenerationHandler] Tool call parameters incomplete') - } else { - toolCallResponse = await this.mcpRuntime.callTool({ - id: toolCall.id, - type: 'function', - function: { - name: toolCall.name, - arguments: toolCall.params - }, - server: { - name: toolCall.server_name || '', - icons: toolCall.server_icons || '', - description: toolCall.server_description || '' - } - }) - } - } - - this.throwIfCancelled(state.message.id) - - const { conversation, contextMessages, userMessage } = await this.prepareConversationContext( - conversationId, - state.message.id, - selectedVariantsMap - ) - - this.throwIfCancelled(state.message.id) - - const { - providerId, - modelId, - temperature, - maxTokens, - enabledMcpTools, - thinkingBudget, - reasoningEffort, - verbosity - } = conversation.settings - const modelConfig = this.ctx.configPresenter.getModelConfig(modelId, providerId) - if (!modelConfig) { - throw new Error(`Model config not found for ${providerId}/${modelId}`) - } - - const { finalContent, promptTokens } = await preparePromptContent({ - conversation, - userContent: 'continue', - contextMessages, - searchResults: null, - userMessage, - vision: false, - imageFiles: [], - supportsFunctionCall: modelConfig.functionCall, - modelType: modelConfig.type, - toolPresenter: this.toolPresenter, - promptRuntime: this.promptRuntime - }) - - await this.updateGenerationState(state, promptTokens) - - if (toolCallResponse && toolCall) { - eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { - eventId: state.message.id, - content: '', - tool_call: 'start', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_response: toolCallResponse.content, - tool_call_server_name: toolCall.server_name, - tool_call_server_icons: toolCall.server_icons, - tool_call_server_description: toolCall.server_description - }) - eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { - eventId: state.message.id, - content: '', - tool_call: 'running', - tool_call_id: toolCall.id, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_response: toolCallResponse.content, - tool_call_server_name: toolCall.server_name, - tool_call_server_icons: toolCall.server_icons, - tool_call_server_description: toolCall.server_description - }) - eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { - eventId: state.message.id, - content: '', - tool_call: 'end', - tool_call_id: toolCall.id, - tool_call_response: toolCallResponse.content, - tool_call_name: toolCall.name, - tool_call_params: toolCall.params, - tool_call_server_name: toolCall.server_name, - tool_call_server_icons: toolCall.server_icons, - tool_call_server_description: toolCall.server_description, - tool_call_response_raw: toolCallResponse.rawData - }) - } - - const stream = this.ctx.llmProviderPresenter.startStreamCompletion( - providerId, - finalContent, - modelId, - state.message.id, - temperature, - maxTokens, - enabledMcpTools, - thinkingBudget, - reasoningEffort, - verbosity, - conversationId - ) - - await this.loopOrchestrator.consume(stream) - } catch (error) { - if (String(error).includes('userCanceledGeneration')) { - console.log('[StreamGenerationHandler] Message generation cancelled by user') - return - } - - console.error('[StreamGenerationHandler] Error during continue generation:', error) - await this.ctx.messageManager.handleMessageError(state.message.id, String(error)) - throw error - } - } - - async prepareConversationContext( - conversationId: string, - queryMsgId?: string, - selectedVariantsMap?: Record - ): Promise<{ - conversation: CONVERSATION - userMessage: Message - contextMessages: Message[] - }> { - const hasUsableAssistantContent = (msg: Message): boolean => { - if (msg.role !== 'assistant') return true - const blocks = msg.content as AssistantMessageBlock[] - return ( - Array.isArray(blocks) && - blocks.some((block) => { - if (block.type === 'content' && block.content) return true - if (block.type === 'tool_call' && block.tool_call) return true - if (block.type === 'reasoning_content' && block.content) return true - return false - }) - ) - } - - const applyVariantToAssistant = (msg: Message, variantId?: string): Message => { - if (msg.role !== 'assistant' || !msg.variants || msg.variants.length === 0) { - return msg - } - - const variants = msg.variants - - const findVariantById = (id?: string): Message | undefined => - id ? variants.find((variant) => variant.id === id) : undefined - - const findFallbackVariant = (): Message | undefined => { - for (let i = variants.length - 1; i >= 0; i--) { - const variant = variants[i] - if (hasUsableAssistantContent(variant)) { - return variant - } - } - return variants[variants.length - 1] - } - - const selectedVariant = findVariantById(variantId) || findFallbackVariant() - if (!selectedVariant) return msg - - return { - ...msg, - content: selectedVariant.content, - usage: selectedVariant.usage, - model_id: selectedVariant.model_id, - model_provider: selectedVariant.model_provider, - model_name: selectedVariant.model_name - } - } - - const conversation = await this.getConversation(conversationId) - let contextMessages: Message[] = [] - let userMessage: Message | null = null - - if (queryMsgId) { - const queryMessage = await this.ctx.messageManager.getMessage(queryMsgId) - if (!queryMessage) { - throw new Error('Message not found') - } - - if (queryMessage.role === 'user') { - userMessage = queryMessage - } else if (queryMessage.role === 'assistant') { - if (!queryMessage.parentId) { - throw new Error('Assistant message missing parentId') - } - userMessage = await this.ctx.messageManager.getMessage(queryMessage.parentId) - if (!userMessage) { - throw new Error('Trigger message not found') - } - } else { - throw new Error('Unsupported message type') - } - - contextMessages = await this.ctx.messageManager.getMessageHistory( - userMessage.id, - conversation.settings.contextLength - ) - } else { - userMessage = await this.ctx.messageManager.getLastUserMessage(conversationId) - if (!userMessage) { - throw new Error('User message not found') - } - contextMessages = await this.getContextMessages(conversation) - } - - if (selectedVariantsMap && Object.keys(selectedVariantsMap).length > 0) { - contextMessages = contextMessages.map((msg) => { - if (msg.role === 'assistant' && selectedVariantsMap[msg.id] && msg.variants) { - return applyVariantToAssistant(msg, selectedVariantsMap[msg.id]) - } - return msg - }) - } - - // Fallback: if assistant content is empty and no variant was explicitly chosen, pick a usable variant - contextMessages = contextMessages.map((msg) => { - if (msg.role === 'assistant' && !hasUsableAssistantContent(msg)) { - return applyVariantToAssistant(msg) - } - return msg - }) - - if (userMessage.role === 'user') { - const msgContent = userMessage.content as UserMessageContent - if (msgContent.content && !msgContent.text) { - msgContent.text = formatUserMessageContent(msgContent.content) - } - } - - const thinkEnabled = this.ctx.configPresenter.getSetting('input_deepThinking') as boolean - ;(userMessage.content as UserMessageContent).think = thinkEnabled - - return { conversation, userMessage, contextMessages } - } - - async processUserMessageContent( - userMessage: UserMessage - ): Promise<{ userContent: string; imageFiles: MessageFile[] }> { - const userContent = buildUserMessageContext(userMessage.content) - - const imageFiles = - userMessage.content.files?.filter((file) => { - const isImage = - file.mimeType.startsWith('data:image') || - /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name || '') - return isImage - }) || [] - - return { userContent, imageFiles } - } - - async updateGenerationState(state: GeneratingMessageState, promptTokens: number): Promise { - this.generatingMessages.set(state.message.id, { - ...state, - startTime: Date.now(), - firstTokenTime: null, - promptTokens - }) - - await this.ctx.messageManager.updateMessageMetadata(state.message.id, { - totalTokens: promptTokens, - generationTime: 0, - firstTokenTime: 0, - tokensPerSecond: 0 - }) - } - - findGeneratingState(conversationId: string): GeneratingMessageState | null { - return ( - Array.from(this.generatingMessages.values()).find( - (state) => state.conversationId === conversationId - ) || null - ) - } - - async regenerateFromUserMessage( - conversationId: string, - userMessageId: string, - selectedVariantsMap?: Record - ): Promise { - const userMessage = await this.ctx.messageManager.getMessage(userMessageId) - if (!userMessage || userMessage.role !== 'user') { - throw new Error('Can only regenerate based on user messages.') - } - - const conversation = await this.getConversation(conversationId) - const { providerId, modelId } = conversation.settings - - const assistantMessage = (await this.ctx.messageManager.sendMessage( - conversationId, - JSON.stringify([]), - 'assistant', - userMessageId, - false, - { - totalTokens: 0, - generationTime: 0, - firstTokenTime: 0, - tokensPerSecond: 0, - contextUsage: 0, - inputTokens: 0, - outputTokens: 0, - model: modelId, - provider: providerId - } - )) as AssistantMessage - - this.generatingMessages.set(assistantMessage.id, { - message: assistantMessage, - conversationId, - startTime: Date.now(), - firstTokenTime: null, - promptTokens: 0, - reasoningStartTime: null, - reasoningEndTime: null, - lastReasoningTime: null - }) - - this.startStreamCompletion(conversationId, userMessageId, selectedVariantsMap).catch( - (error) => { - console.error( - '[StreamGenerationHandler] Failed to start regeneration from user message:', - error - ) - } - ) - - return assistantMessage - } - - async generateAIResponse( - conversationId: string, - userMessageId: string - ): Promise { - try { - const triggerMessage = await this.ctx.messageManager.getMessage(userMessageId) - if (!triggerMessage) { - throw new Error('Trigger message not found') - } - - await this.ctx.messageManager.updateMessageStatus(userMessageId, 'sent') - - const conversation = await this.getConversation(conversationId) - const { providerId, modelId } = conversation.settings - const assistantMessage = (await this.ctx.messageManager.sendMessage( - conversationId, - JSON.stringify([]), - 'assistant', - userMessageId, - false, - { - contextUsage: 0, - totalTokens: 0, - generationTime: 0, - firstTokenTime: 0, - tokensPerSecond: 0, - inputTokens: 0, - outputTokens: 0, - model: modelId, - provider: providerId - } - )) as AssistantMessage - - return assistantMessage - } catch (error) { - await this.ctx.messageManager.updateMessageStatus(userMessageId, 'error') - console.error('[StreamGenerationHandler] Failed to generate AI response:', error) - throw error - } - } - - async getMessageHistory(messageId: string, limit: number = 100): Promise { - return this.ctx.messageManager.getMessageHistory(messageId, limit) - } - - private async getContextMessages(conversation: CONVERSATION): Promise { - let messageCount = Math.ceil(conversation.settings.contextLength / 300) - if (messageCount < 2) { - messageCount = 2 - } - return this.ctx.messageManager.getContextMessages(conversation.id, messageCount) - } - - private throwIfCancelled(messageId: string): void { - if (this.isMessageCancelled(messageId)) { - throw new Error('common.error.userCanceledGeneration') - } - } - - private isMessageCancelled(messageId: string): boolean { - const state = this.generatingMessages.get(messageId) - return !state || state.isCancelled === true - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/streamUpdateScheduler.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/streamUpdateScheduler.ts deleted file mode 100644 index cbcf4e646..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/streamUpdateScheduler.ts +++ /dev/null @@ -1,433 +0,0 @@ -import { eventBus, SendTarget } from '@/eventbus' -import { STREAM_EVENTS } from '@/events' -import type { LLMAgentEventData } from '@shared/presenter' - -export const STREAM_RENDER_FLUSH_INTERVAL_MS = 120 -export const STREAM_DB_FLUSH_INTERVAL_MS = 600 - -interface PendingDelta { - content?: string - reasoning_content?: string - reasoning_time?: { start: number; end: number } - tool_call?: LLMAgentEventData['tool_call'] - tool_call_id?: string - tool_call_name?: string - tool_call_params?: string - tool_call_response?: string | Array - tool_call_server_name?: string - tool_call_server_icons?: string - tool_call_server_description?: string - tool_call_response_raw?: unknown - permission_request?: LLMAgentEventData['permission_request'] - question_request?: LLMAgentEventData['question_request'] - question_error?: LLMAgentEventData['question_error'] - maximum_tool_calls_reached?: boolean - image_data?: { data: string; mimeType: string } - rate_limit?: LLMAgentEventData['rate_limit'] - totalUsage?: LLMAgentEventData['totalUsage'] -} - -interface SchedulerState { - eventId: string - conversationId: string - parentId?: string - isVariant: boolean - webContentsId?: number - pendingDelta: PendingDelta - contentSnapshot?: unknown - seq: number - hasSentInit: boolean - lastRenderFlushAt: number - lastDbFlushAt: number - renderTimer?: NodeJS.Timeout - dbTimer?: NodeJS.Timeout -} - -export class StreamUpdateScheduler { - private readonly states: Map = new Map() - private readonly messageManager: any - private readonly onFlushRender?: () => void - - constructor(options: { messageManager: any; onFlushRender?: () => void }) { - this.messageManager = options.messageManager - this.onFlushRender = options.onFlushRender - } - - private getStateOrCreate(options: { - eventId: string - conversationId: string - parentId?: string - isVariant: boolean - webContentsId?: number - }): SchedulerState { - let state = this.states.get(options.eventId) - if (!state) { - state = { - eventId: options.eventId, - conversationId: options.conversationId, - parentId: options.parentId, - isVariant: options.isVariant, - webContentsId: options.webContentsId, - pendingDelta: {}, - seq: 0, - hasSentInit: false, - lastRenderFlushAt: 0, - lastDbFlushAt: 0 - } - this.states.set(options.eventId, state) - } - return state - } - - enqueueDelta( - eventId: string, - conversationId: string, - parentId: string | undefined, - isVariant: boolean, - webContentsId: number | undefined, - delta: Partial, - contentSnapshot?: unknown - ): void { - if (delta.content && delta.reasoning_content) { - const { content, reasoning_content, ...rest } = delta - this.enqueueDelta( - eventId, - conversationId, - parentId, - isVariant, - webContentsId, - { - ...rest, - content - }, - contentSnapshot - ) - this.enqueueDelta( - eventId, - conversationId, - parentId, - isVariant, - webContentsId, - { - reasoning_content - }, - contentSnapshot - ) - return - } - - const state = this.getStateOrCreate({ - eventId, - conversationId, - parentId, - isVariant, - webContentsId - }) - - if (contentSnapshot !== undefined) { - state.contentSnapshot = contentSnapshot - } - - if (!state.hasSentInit) { - this.sendInit(state) - } - - const shouldFlushImmediately = this.shouldFlushBeforeEnqueue(state, delta) - if (shouldFlushImmediately) { - this.flushRender(state) - } - - if (delta.content) { - state.pendingDelta.content = (state.pendingDelta.content ?? '') + delta.content - } - if (delta.reasoning_content) { - state.pendingDelta.reasoning_content = - (state.pendingDelta.reasoning_content ?? '') + delta.reasoning_content - } - if (delta.reasoning_time) { - // Always use the latest reasoning_time (contains updated end time) - state.pendingDelta.reasoning_time = delta.reasoning_time - } - if (delta.tool_call !== undefined) { - state.pendingDelta.tool_call = delta.tool_call - } - if (delta.tool_call_id !== undefined) { - state.pendingDelta.tool_call_id = delta.tool_call_id - } - if (delta.tool_call_name !== undefined) { - state.pendingDelta.tool_call_name = delta.tool_call_name - } - if (delta.tool_call_params !== undefined) { - state.pendingDelta.tool_call_params = delta.tool_call_params - } - if (delta.tool_call_response !== undefined) { - state.pendingDelta.tool_call_response = delta.tool_call_response - } - if (delta.tool_call_server_name !== undefined) { - state.pendingDelta.tool_call_server_name = delta.tool_call_server_name - } - if (delta.tool_call_server_icons !== undefined) { - state.pendingDelta.tool_call_server_icons = delta.tool_call_server_icons - } - if (delta.tool_call_server_description !== undefined) { - state.pendingDelta.tool_call_server_description = delta.tool_call_server_description - } - if (delta.tool_call_response_raw !== undefined) { - state.pendingDelta.tool_call_response_raw = delta.tool_call_response_raw - } - if (delta.permission_request !== undefined) { - state.pendingDelta.permission_request = delta.permission_request - } - if (delta.question_request !== undefined) { - state.pendingDelta.question_request = delta.question_request - } - if (delta.question_error !== undefined) { - state.pendingDelta.question_error = delta.question_error - } - if (delta.maximum_tool_calls_reached !== undefined) { - state.pendingDelta.maximum_tool_calls_reached = delta.maximum_tool_calls_reached - } - if (delta.image_data !== undefined) { - state.pendingDelta.image_data = delta.image_data - } - if (delta.rate_limit !== undefined) { - state.pendingDelta.rate_limit = delta.rate_limit - } - if (delta.totalUsage !== undefined) { - state.pendingDelta.totalUsage = delta.totalUsage - } - - const now = Date.now() - - if (shouldFlushImmediately) { - this.flushRender(state) - } else { - const renderDelay = Math.max( - 0, - STREAM_RENDER_FLUSH_INTERVAL_MS - (now - state.lastRenderFlushAt) - ) - this.scheduleRenderFlush(state, renderDelay) - } - - const dbDelay = Math.max(0, STREAM_DB_FLUSH_INTERVAL_MS - (now - state.lastDbFlushAt)) - this.scheduleDbFlush(state, dbDelay) - } - - private sendInit(state: SchedulerState): void { - state.hasSentInit = true - - const eventData: LLMAgentEventData = { - eventId: state.eventId, - conversationId: state.conversationId, - parentId: state.parentId, - is_variant: state.isVariant, - stream_kind: 'init', - seq: state.seq - } - - eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, eventData) - } - - private scheduleRenderFlush(state: SchedulerState, delayMs: number): void { - if (state.renderTimer) return - - state.renderTimer = setTimeout(() => { - this.flushRender(state) - }, delayMs) - } - - private flushRender(state: SchedulerState): void { - if (state.renderTimer) { - clearTimeout(state.renderTimer) - state.renderTimer = undefined - } - - const delta = state.pendingDelta - const hasContent = Object.keys(delta).length > 0 - - if (hasContent) { - state.lastRenderFlushAt = Date.now() - state.seq += 1 - - const eventData: LLMAgentEventData = { - eventId: state.eventId, - conversationId: state.conversationId, - parentId: state.parentId, - is_variant: state.isVariant, - stream_kind: 'delta', - seq: state.seq, - content: delta.content, - reasoning_content: delta.reasoning_content, - reasoning_time: delta.reasoning_time, - tool_call: delta.tool_call, - tool_call_id: delta.tool_call_id, - tool_call_name: delta.tool_call_name, - tool_call_params: delta.tool_call_params, - tool_call_response: delta.tool_call_response, - tool_call_server_name: delta.tool_call_server_name, - tool_call_server_icons: delta.tool_call_server_icons, - tool_call_server_description: delta.tool_call_server_description, - tool_call_response_raw: delta.tool_call_response_raw, - permission_request: delta.permission_request, - question_request: delta.question_request, - question_error: delta.question_error, - maximum_tool_calls_reached: delta.maximum_tool_calls_reached, - image_data: delta.image_data, - rate_limit: delta.rate_limit, - totalUsage: delta.totalUsage - } - - eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, eventData) - state.pendingDelta = {} - } - - this.onFlushRender?.() - } - - private scheduleDbFlush(state: SchedulerState, delayMs: number): void { - if (state.dbTimer) return - - state.dbTimer = setTimeout(() => { - void this.flushDb(state) - }, delayMs) - } - - private getDeltaKind( - delta: Partial | PendingDelta - ): 'tool_call' | 'image' | 'rate_limit' | 'reasoning' | 'content' | 'mixed' | null { - if (delta.tool_call !== undefined) return 'tool_call' - if (delta.image_data !== undefined) return 'image' - if (delta.rate_limit !== undefined) return 'rate_limit' - if (delta.content && delta.reasoning_content) return 'mixed' - if (delta.reasoning_content) return 'reasoning' - if (delta.content) return 'content' - return null - } - - private shouldFlushBeforeEnqueue( - state: SchedulerState, - delta: Partial - ): boolean { - if (Object.keys(state.pendingDelta).length === 0) return false - - const pendingKind = this.getDeltaKind(state.pendingDelta) - const incomingKind = this.getDeltaKind(delta) - if (!pendingKind || !incomingKind) return false - - if (pendingKind !== incomingKind) return true - - if (pendingKind === 'image' || pendingKind === 'rate_limit') { - return true - } - - if (pendingKind === 'tool_call') { - if (state.pendingDelta.tool_call !== delta.tool_call) return true - if ( - state.pendingDelta.tool_call_id && - delta.tool_call_id && - state.pendingDelta.tool_call_id !== delta.tool_call_id - ) { - return true - } - if ( - state.pendingDelta.tool_call_name && - delta.tool_call_name && - state.pendingDelta.tool_call_name !== delta.tool_call_name - ) { - return true - } - } - - return false - } - - private flushDb(state: SchedulerState): Promise { - state.lastDbFlushAt = Date.now() - - if (state.dbTimer) { - clearTimeout(state.dbTimer) - state.dbTimer = undefined - } - - if (!state.contentSnapshot) { - return Promise.resolve() - } - - return this.messageManager - .editMessageSilently(state.eventId, JSON.stringify(state.contentSnapshot)) - .catch((err: unknown) => { - console.error('[StreamUpdateScheduler] Failed to flush DB:', err) - }) - } - - async flushAll(eventId: string, kind: 'final' = 'final'): Promise { - const state = this.states.get(eventId) - if (!state) return - - if (state.renderTimer) { - clearTimeout(state.renderTimer) - state.renderTimer = undefined - } - if (state.dbTimer) { - clearTimeout(state.dbTimer) - state.dbTimer = undefined - } - - const delta = state.pendingDelta - const hasContent = Object.keys(delta).length > 0 - - if (hasContent) { - state.seq += 1 - - const eventData: LLMAgentEventData = { - eventId: state.eventId, - conversationId: state.conversationId, - parentId: state.parentId, - is_variant: state.isVariant, - stream_kind: kind, - seq: state.seq, - content: delta.content, - reasoning_content: delta.reasoning_content, - reasoning_time: delta.reasoning_time, - tool_call: delta.tool_call, - tool_call_id: delta.tool_call_id, - tool_call_name: delta.tool_call_name, - tool_call_params: delta.tool_call_params, - tool_call_response: delta.tool_call_response, - tool_call_server_name: delta.tool_call_server_name, - tool_call_server_icons: delta.tool_call_server_icons, - tool_call_server_description: delta.tool_call_server_description, - tool_call_response_raw: delta.tool_call_response_raw, - permission_request: delta.permission_request, - question_request: delta.question_request, - question_error: delta.question_error, - maximum_tool_calls_reached: delta.maximum_tool_calls_reached, - image_data: delta.image_data, - rate_limit: delta.rate_limit, - totalUsage: delta.totalUsage - } - - eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, eventData) - } - - await this.flushDb(state) - - this.states.delete(eventId) - } - - cleanup(eventId: string): void { - const state = this.states.get(eventId) - if (!state) return - - if (state.renderTimer) { - clearTimeout(state.renderTimer) - state.renderTimer = undefined - } - if (state.dbTimer) { - clearTimeout(state.dbTimer) - state.dbTimer = undefined - } - - this.states.delete(eventId) - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/types.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/types.ts deleted file mode 100644 index e960eedf5..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { AssistantMessage } from '@shared/chat' -import type { PendingToolCall } from '../message/messageBuilder' - -export interface GeneratingMessageState { - message: AssistantMessage - conversationId: string - startTime: number - firstTokenTime: number | null - promptTokens: number - reasoningStartTime: number | null - reasoningEndTime: number | null - lastReasoningTime: number | null - isCancelled?: boolean - pendingToolCall?: PendingToolCall - totalUsage?: { - prompt_tokens: number - completion_tokens: number - total_tokens: number - context_length: number - } - adaptiveBuffer?: { - content: string - lastUpdateTime: number - updateCount: number - totalSize: number - isLargeContent: boolean - chunks?: string[] - currentChunkIndex?: number - sentPosition: number - isProcessing?: boolean - } - flushTimeout?: NodeJS.Timeout - throttleTimeout?: NodeJS.Timeout - lastRendererUpdateTime?: number - webContentsId?: number -} - -export type { StreamUpdateScheduler } from './streamUpdateScheduler' diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolCallCenter.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolCallCenter.ts deleted file mode 100644 index 86e09d92d..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolCallCenter.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { - IToolPresenter, - MCPToolCall, - MCPToolDefinition, - MCPToolResponse -} from '@shared/presenter' - -export type ToolCallContext = { - enabledMcpTools?: string[] - disabledAgentTools?: string[] - chatMode?: 'agent' | 'acp agent' - supportsVision?: boolean - agentWorkspacePath?: string | null - conversationId?: string -} - -export class ToolCallCenter { - constructor(private readonly toolPresenter: IToolPresenter) {} - - async getAllToolDefinitions(context: ToolCallContext): Promise { - return this.toolPresenter.getAllToolDefinitions(context) - } - - async callTool(request: MCPToolCall): Promise<{ content: unknown; rawData: MCPToolResponse }> { - return this.toolPresenter.callTool(request) - } - - buildToolSystemPrompt(context: { - conversationId?: string - toolDefinitions?: MCPToolDefinition[] - }): string { - return this.toolPresenter.buildToolSystemPrompt(context) - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolRegistry.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolRegistry.ts deleted file mode 100644 index 1836bb876..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolRegistry.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type ToolRegistrySource = 'mcp' | 'agent' | 'browser' | 'builtin' - -export type ToolRegistryEntry = { - name: string - source: ToolRegistrySource -} - -export class ToolRegistry { - private readonly registry = new Map() - - register(entry: ToolRegistryEntry): void { - this.registry.set(entry.name, entry) - } - - get(name: string): ToolRegistryEntry | undefined { - return this.registry.get(name) - } - - list(): ToolRegistryEntry[] { - return Array.from(this.registry.values()) - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolRouter.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolRouter.ts deleted file mode 100644 index 15a70cd65..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/tool/toolRouter.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ToolRegistrySource } from './toolRegistry' - -export type ToolRouteDecision = { - target: ToolRegistrySource - permissionType?: 'read' | 'write' | 'all' | 'command' -} - -export function resolveToolRoute(_toolName: string): ToolRouteDecision { - return { target: 'mcp' } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/types.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/types.ts deleted file mode 100644 index 88ffb3de0..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type { - SessionContext, - SessionContextResolved, - SessionStatus -} from './session/sessionContext' -export type { SessionResolveInput } from './session/sessionResolver' -export { resolveSessionContext } from './session/sessionResolver' diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/types/handlerContext.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/types/handlerContext.ts deleted file mode 100644 index 08e76622a..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/types/handlerContext.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { - IConfigPresenter, - ILlmProviderPresenter, - ISQLitePresenter, - IToolPresenter -} from '@shared/presenter' -import type { CONVERSATION } from '@shared/presenter' -import type { MessageManager } from '../../sessionPresenter/managers/messageManager' -import type { AgentSessionRuntimePort } from '../session/sessionRuntimePort' -import type { - AgentMcpRuntimePort, - AgentPermissionRuntimePort, - AgentPromptRuntimePort -} from '../runtimePorts' - -export type ThreadHandlerContext = { - sqlitePresenter: ISQLitePresenter - messageManager: MessageManager - llmProviderPresenter: ILlmProviderPresenter - configPresenter: IConfigPresenter - sessionRuntime: AgentSessionRuntimePort - toolPresenter: IToolPresenter - mcpRuntime: AgentMcpRuntimePort - promptRuntime: AgentPromptRuntimePort - permissionRuntime: AgentPermissionRuntimePort -} - -export class BaseHandler { - protected ctx: ThreadHandlerContext - - constructor(context: ThreadHandlerContext) { - this.ctx = context - } - - protected get sqlitePresenter(): ISQLitePresenter { - return this.ctx.sqlitePresenter - } - - protected get messageManager(): MessageManager { - return this.ctx.messageManager - } - - protected get llmProviderPresenter(): ILlmProviderPresenter { - return this.ctx.llmProviderPresenter - } - - protected get configPresenter(): IConfigPresenter { - return this.ctx.configPresenter - } - - protected get sessionRuntime(): AgentSessionRuntimePort { - return this.ctx.sessionRuntime - } - - protected get toolPresenter(): IToolPresenter { - return this.ctx.toolPresenter - } - - protected get mcpRuntime(): AgentMcpRuntimePort { - return this.ctx.mcpRuntime - } - - protected get promptRuntime(): AgentPromptRuntimePort { - return this.ctx.promptRuntime - } - - protected get permissionRuntime(): AgentPermissionRuntimePort { - return this.ctx.permissionRuntime - } - - protected async getMessage(messageId: string) { - return this.messageManager.getMessage(messageId) - } - - protected async getConversation(conversationId: string): Promise { - const conversation = await this.ctx.sqlitePresenter.getConversation(conversationId) - if (!conversation) { - throw new Error('Conversation not found') - } - return conversation - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/utility/promptEnhancer.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/utility/promptEnhancer.ts deleted file mode 100644 index 3f412787e..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/utility/promptEnhancer.ts +++ /dev/null @@ -1,16 +0,0 @@ -interface EnhanceOptions { - isImageGeneration?: boolean - agentWorkspacePath?: string | null - platform?: NodeJS.Platform -} - -export function enhanceSystemPromptWithDateTime( - systemPrompt: string, - options: EnhanceOptions = {} -): string { - const { isImageGeneration = false } = options - - if (isImageGeneration) return systemPrompt - - return systemPrompt?.trim() ?? '' -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/utility/utilityHandler.ts b/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/utility/utilityHandler.ts deleted file mode 100644 index 39cc2ed69..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/utility/utilityHandler.ts +++ /dev/null @@ -1,329 +0,0 @@ -import type { - ChatMessage, - CONVERSATION, - CONVERSATION_SETTINGS, - LLMAgentEventData -} from '@shared/presenter' -import type { AssistantMessageBlock, Message, UserMessageContent } from '@shared/chat' -import { BaseHandler, type ThreadHandlerContext } from '../types/handlerContext' -import { buildUserMessageContext } from '../message/messageFormatter' -import { - buildConversationExportContent, - generateExportFilename, - type ConversationExportFormat -} from '../../exporter/formats/conversationExporter' - -// Translation constants -const TRANSLATION_TEMPERATURE = 0.3 -const TRANSLATION_TIMEOUT_MS = 1000 - -// Message length constant for context calculation -const DEFAULT_MESSAGE_LENGTH = 300 - -export interface UtilityHandlerOptions { - getActiveConversation: (webContentsId: number) => Promise - getActiveConversationId: (webContentsId: number) => Promise - createConversation: ( - title: string, - settings: Partial, - webContentsId: number - ) => Promise -} - -export class UtilityHandler extends BaseHandler { - private readonly getActiveConversation: (webContentsId: number) => Promise - private readonly getActiveConversationId: (webContentsId: number) => Promise - private readonly createConversation: ( - title: string, - settings: Partial, - webContentsId: number - ) => Promise - - constructor(context: ThreadHandlerContext, options: UtilityHandlerOptions) { - super(context) - this.getActiveConversation = options.getActiveConversation - this.getActiveConversationId = options.getActiveConversationId - this.createConversation = options.createConversation - } - - async translateText(text: string, webContentsId: number): Promise { - try { - let conversation = await this.getActiveConversation(webContentsId) - if (!conversation) { - // Create a temporary conversation for translation - const defaultProvider = this.ctx.configPresenter.getDefaultProviders()[0] - const models = await this.ctx.llmProviderPresenter.getModelList(defaultProvider.id) - const defaultModel = models[0] - const conversationId = await this.createConversation( - 'Temporary translation conversation', - { - modelId: defaultModel.id, - providerId: defaultProvider.id - }, - webContentsId - ) - conversation = await this.getConversation(conversationId) - } - - const { providerId, modelId } = conversation.settings - const messages: ChatMessage[] = [ - { - role: 'system', - content: - 'You are a translation assistant. Translate the user input to Chinese and return only the translated text without any additional content.' - }, - { - role: 'user', - content: text - } - ] - - let translatedText = '' - const stream = this.ctx.llmProviderPresenter.startStreamCompletion( - providerId, - messages, - modelId, - 'translate-' + Date.now(), - TRANSLATION_TEMPERATURE, - TRANSLATION_TIMEOUT_MS - ) - - for await (const event of stream) { - if (event.type === 'response') { - const msg = event.data as LLMAgentEventData - if (msg.content) { - translatedText += msg.content - } - } else if (event.type === 'error') { - const msg = event.data as { eventId: string; error: string } - throw new Error(msg.error || 'Translation failed') - } - } - - return translatedText.trim() - } catch (error) { - console.error('Translation failed:', error) - if (error instanceof Error) { - error.message = 'Translation failed' - throw error - } - throw new Error('Translation failed') - } - } - - async askAI(text: string, webContentsId: number): Promise { - try { - let conversation = await this.getActiveConversation(webContentsId) - if (!conversation) { - // Create a temporary conversation for AI query - const defaultProvider = this.ctx.configPresenter.getDefaultProviders()[0] - const models = await this.ctx.llmProviderPresenter.getModelList(defaultProvider.id) - const defaultModel = models[0] - const conversationId = await this.createConversation( - '临时AI对话', - { - modelId: defaultModel.id, - providerId: defaultProvider.id - }, - webContentsId - ) - conversation = await this.getConversation(conversationId) - } - - const { providerId, modelId } = conversation.settings - const messages: ChatMessage[] = [ - { - role: 'system', - content: '你是一个AI助手。请简洁地回答用户的问题。' - }, - { - role: 'user', - content: text - } - ] - - let aiAnswer = '' - const stream = this.ctx.llmProviderPresenter.startStreamCompletion( - providerId, - messages, - modelId, - 'ask-ai-' + Date.now(), - 0.7, - 1000 - ) - - for await (const event of stream) { - if (event.type === 'response') { - const msg = event.data as LLMAgentEventData - if (msg.content) { - aiAnswer += msg.content - } - } else if (event.type === 'error') { - const msg = event.data as { eventId: string; error: string } - throw new Error(msg.error || 'AI回答失败') - } - } - - return aiAnswer.trim() - } catch (error) { - console.error('AI query failed:', error) - throw error - } - } - - async exportConversation( - conversationId: string, - format: ConversationExportFormat = 'markdown' - ): Promise<{ filename: string; content: string }> { - try { - // Get conversation - const conversation = await this.getConversation(conversationId) - if (!conversation) { - throw new Error('Conversation not found') - } - - // Get all messages - const { list: messages } = await this.ctx.messageManager.getMessageThread( - conversationId, - 1, - 10000 - ) - - // Filter out unsent messages - const validMessages = messages.filter((msg) => msg.status === 'sent') - - // Apply variant selection - const selectedVariantsMap = conversation.settings.selectedVariantsMap || {} - const variantAwareMessages = this.applyVariantSelection(validMessages, selectedVariantsMap) - - // Generate filename - const filename = generateExportFilename(format) - const content = buildConversationExportContent(conversation, variantAwareMessages, format) - - return { filename, content } - } catch (error) { - console.error('Failed to export conversation:', error) - throw error - } - } - - async summaryTitles(webContentsId?: number, conversationId?: string): Promise { - const activeId = - webContentsId !== undefined ? await this.getActiveConversationId(webContentsId) : null - const targetConversationId = conversationId ?? activeId ?? undefined - if (!targetConversationId) { - throw new Error('Conversation not found') - } - const conversation = await this.getConversation(targetConversationId) - if (!conversation) { - throw new Error('Conversation not found') - } - - // Get context messages - let messageCount = Math.ceil(conversation.settings.contextLength / DEFAULT_MESSAGE_LENGTH) - if (messageCount < 2) { - messageCount = 2 - } - const messages = await this.ctx.messageManager.getContextMessages(conversation.id, messageCount) - - const selectedVariantsMap = conversation.settings.selectedVariantsMap || {} - const variantAwareMessages = this.applyVariantSelection(messages, selectedVariantsMap) - const messagesWithLength = variantAwareMessages - .map((msg) => { - if (msg.role === 'user') { - const userContent = msg.content as UserMessageContent - const serializedContent = buildUserMessageContext(userContent) - return { - message: msg, - length: serializedContent.length, - formattedMessage: { - role: 'user' as const, - content: serializedContent - } - } - } else { - const content = (msg.content as AssistantMessageBlock[]) - .filter((block) => block.type === 'content') - .map((block) => block.content) - .join('\n') - return { - message: msg, - length: content.length, - formattedMessage: { - role: 'assistant' as const, - content: content - } - } - } - }) - .filter((item) => item.formattedMessage.content.length > 0) - const assistantModel = this.ctx.configPresenter.getSetting<{ - providerId: string - modelId: string - }>('assistantModel') - const fallbackProviderId = conversation.settings.providerId - const fallbackModelId = conversation.settings.modelId - const preferredProviderId = assistantModel?.providerId || fallbackProviderId - const preferredModelId = assistantModel?.modelId || fallbackModelId - - let title: string - try { - title = await this.ctx.llmProviderPresenter.summaryTitles( - messagesWithLength.map((item) => item.formattedMessage), - preferredProviderId, - preferredModelId - ) - } catch (error) { - const shouldFallback = - preferredProviderId !== fallbackProviderId || preferredModelId !== fallbackModelId - if (!shouldFallback) { - throw error - } - console.warn( - '[UtilityHandler] Failed to generate title with assistant model, fallback to conversation model', - { - preferredProviderId, - preferredModelId, - fallbackProviderId, - fallbackModelId, - error - } - ) - title = await this.ctx.llmProviderPresenter.summaryTitles( - messagesWithLength.map((item) => item.formattedMessage), - fallbackProviderId, - fallbackModelId - ) - } - let cleanedTitle = title.replace(/.*?<\/think>/g, '').trim() - cleanedTitle = cleanedTitle.replace(/^/, '').trim() - return cleanedTitle - } - - /** - * Applies variant selection to messages based on selectedVariantsMap. - * Returns messages with selected variant fields applied when a variant is selected. - */ - private applyVariantSelection( - messages: Message[], - selectedVariantsMap: Record - ): Message[] { - return messages.map((msg) => { - if (msg.role === 'assistant' && selectedVariantsMap[msg.id] && msg.variants) { - const selectedVariantId = selectedVariantsMap[msg.id] - const selectedVariant = msg.variants.find((v) => v.id === selectedVariantId) - - if (selectedVariant) { - const newMsg = JSON.parse(JSON.stringify(msg)) - newMsg.content = selectedVariant.content - newMsg.usage = selectedVariant.usage - newMsg.model_id = selectedVariant.model_id - newMsg.model_provider = selectedVariant.model_provider - newMsg.model_name = selectedVariant.model_name - return newMsg - } - } - return msg - }) - } -} diff --git a/archives/code/legacy-agentpresenter-retirement/src/shared/types/presenters/agent.presenter.d.ts b/archives/code/legacy-agentpresenter-retirement/src/shared/types/presenters/agent.presenter.d.ts deleted file mode 100644 index 84ef9a2d7..000000000 --- a/archives/code/legacy-agentpresenter-retirement/src/shared/types/presenters/agent.presenter.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { AssistantMessage } from '@shared/chat' - -export interface IAgentPresenter { - sendMessage( - agentId: string, - content: string, - webContentsId?: number, - selectedVariantsMap?: Record - ): Promise - continueLoop( - agentId: string, - messageId: string, - selectedVariantsMap?: Record - ): Promise - cancelLoop(messageId: string): Promise - retryMessage( - messageId: string, - selectedVariantsMap?: Record - ): Promise - regenerateFromUserMessage( - agentId: string, - userMessageId: string, - selectedVariantsMap?: Record - ): Promise - translateText(text: string, webContentsId: number): Promise - askAI(text: string, webContentsId: number): Promise - handlePermissionResponse( - messageId: string, - toolCallId: string, - granted: boolean, - permissionType: 'read' | 'write' | 'all' | 'command', - remember?: boolean - ): Promise - resolveQuestion( - messageId: string, - toolCallId: string, - answerText: string, - answerMessageId?: string - ): Promise - rejectQuestion(messageId: string, toolCallId: string): Promise - cleanupConversation(conversationId: string): Promise -} diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/agentPresenter.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/agentPresenter.test.ts deleted file mode 100644 index 32ebdf072..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/agentPresenter.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, it, expect } from 'vitest' -import * as agentPresenterModule from '../../../../src/main/presenter/agentPresenter' - -describe('AgentPresenter scaffolding', () => { - it('loads module entrypoint', () => { - expect(agentPresenterModule).toBeDefined() - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/loop/agentLoopHandler.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/loop/agentLoopHandler.test.ts deleted file mode 100644 index 8b3203c8b..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/loop/agentLoopHandler.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { AgentLoopHandler } from '@/presenter/agentPresenter/loop/agentLoopHandler' - -vi.mock('@/eventbus', () => ({ - eventBus: { - sendToRenderer: vi.fn() - }, - SendTarget: { - ALL_WINDOWS: 'ALL_WINDOWS' - } -})) - -describe('AgentLoopHandler session runtime wiring', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('resolves model and workspace through injected session runtime in tool processing', async () => { - const toolPresenter = { - getAllToolDefinitions: vi.fn().mockResolvedValue([ - { - function: { - name: 'read' - }, - server: { - name: 'agent-filesystem', - icons: '', - description: 'Filesystem' - } - } - ]), - preCheckToolPermission: vi.fn().mockResolvedValue(null), - callTool: vi.fn().mockResolvedValue({ - content: 'ok', - rawData: {} - }) - } - const sessionRuntime = { - getSession: vi.fn().mockResolvedValue({ - resolved: { - modelId: 'model-from-session' - } - }), - resolveWorkspaceContext: vi.fn().mockResolvedValue({ - chatMode: 'agent', - agentWorkspacePath: 'C:/workspace' - }) - } - const handler = new AgentLoopHandler({ - configPresenter: {} as any, - getProviderInstance: vi.fn() as any, - activeStreams: new Map(), - canStartNewStream: vi.fn().mockReturnValue(true), - rateLimitManager: {} as any, - sessionRuntime, - getToolPresenter: () => toolPresenter as any - }) - - const processor = (handler as any).toolCallProcessor.process({ - eventId: 'evt-loop', - toolCalls: [{ id: 'tool-1', name: 'read', arguments: '{}' }], - enabledMcpTools: [], - conversationMessages: [], - modelConfig: { - functionCall: false, - type: 'chat' - }, - abortSignal: new AbortController().signal, - currentToolCallCount: 0, - maxToolCalls: 5, - conversationId: 'conv-loop', - providerId: 'mock-provider' - }) - - while (true) { - const next = await processor.next() - if (next.done) { - break - } - } - - expect(sessionRuntime.getSession).toHaveBeenCalledWith('conv-loop') - expect(sessionRuntime.resolveWorkspaceContext).toHaveBeenCalledWith( - 'conv-loop', - 'model-from-session' - ) - expect(toolPresenter.getAllToolDefinitions).toHaveBeenCalled() - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/loop/toolCallProcessor.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/loop/toolCallProcessor.test.ts deleted file mode 100644 index b6efea844..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/loop/toolCallProcessor.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import fs from 'fs/promises' -import os from 'os' -import path from 'path' -import { app } from 'electron' -import { ToolCallProcessor } from '@/presenter/agentPresenter/loop/toolCallProcessor' -import type { - ChatMessage, - MCPToolDefinition, - MCPToolResponse, - ModelConfig -} from '@shared/presenter' - -vi.mock('@/presenter', () => ({ - presenter: { - hooksNotifications: { - dispatchEvent: vi.fn() - } - } -})) - -describe('ToolCallProcessor tool output offload', () => { - let tempHome: string - let getPathSpy: ReturnType - - const toolDefinition = { - type: 'function', - function: { - name: 'exec', - description: 'execute command', - parameters: { - type: 'object', - properties: {} - } - }, - server: { - name: 'mock', - icons: '', - description: '' - } - } as MCPToolDefinition - - beforeEach(async () => { - tempHome = await fs.mkdtemp(path.join(os.tmpdir(), 'tool-offload-')) - getPathSpy = vi.spyOn(app, 'getPath').mockReturnValue(tempHome) - }) - - afterEach(async () => { - getPathSpy.mockRestore() - await fs.rm(tempHome, { recursive: true, force: true }) - }) - - it('offloads large tool responses and returns stub content', async () => { - const longOutput = 'x'.repeat(5001) - const rawData = { content: longOutput } as MCPToolResponse - const processor = new ToolCallProcessor({ - getAllToolDefinitions: async () => [toolDefinition], - callTool: async () => ({ content: longOutput, rawData }) - }) - - const conversationMessages: ChatMessage[] = [{ role: 'assistant', content: 'hello' }] - const conversationId = 'conv-123' - const modelConfig = { functionCall: true } as ModelConfig - - const events: any[] = [] - for await (const event of processor.process({ - eventId: 'event-1', - toolCalls: [{ id: 'tool:1', name: 'exec', arguments: '{}' }], - enabledMcpTools: [], - conversationMessages, - modelConfig, - abortSignal: new AbortController().signal, - currentToolCallCount: 0, - maxToolCalls: 5, - conversationId - })) { - events.push(event) - } - - const endEvent = events.find( - (event) => event.type === 'response' && event.data?.tool_call === 'end' - ) - expect(endEvent).toBeDefined() - - const stub = endEvent.data.tool_call_response as string - const expectedPath = path.join( - tempHome, - '.deepchat', - 'sessions', - conversationId, - 'tool_tool_1.offload' - ) - expect(stub).toContain('[Tool output offloaded]') - expect(stub).toContain(`Total characters: ${longOutput.length}`) - expect(stub).toContain(expectedPath) - expect(endEvent.data.tool_call_response_raw).toBe(rawData) - - const saved = await fs.readFile(expectedPath, 'utf-8') - expect(saved).toBe(longOutput) - - const toolMessage = conversationMessages.find((message) => message.role === 'tool') - expect(toolMessage?.content).toContain('[Tool output offloaded]') - }) -}) - -describe('ToolCallProcessor question tool', () => { - const questionToolDef = { - type: 'function', - function: { - name: 'deepchat_question', - description: 'question tool', - parameters: { - type: 'object', - properties: {} - } - }, - server: { - name: 'agent-core', - icons: '❓', - description: 'Agent core tools' - } - } as MCPToolDefinition - - const executeCommandDef = { - type: 'function', - function: { - name: 'exec', - description: 'execute command', - parameters: { - type: 'object', - properties: {} - } - }, - server: { - name: 'mock', - icons: '', - description: '' - } - } as MCPToolDefinition - - const basicQuestionArgs = JSON.stringify({ - question: 'Choose one', - options: [{ label: 'A' }, { label: 'B' }] - }) - - it('emits question-required and pauses the loop', async () => { - const callTool = vi.fn() - const processor = new ToolCallProcessor({ - getAllToolDefinitions: async () => [questionToolDef], - callTool - }) - - const conversationMessages: ChatMessage[] = [{ role: 'assistant', content: 'hello' }] - const iterator = processor.process({ - eventId: 'event-question-1', - toolCalls: [{ id: 'tool-q1', name: 'deepchat_question', arguments: basicQuestionArgs }], - enabledMcpTools: [], - conversationMessages, - modelConfig: { functionCall: true } as ModelConfig, - abortSignal: new AbortController().signal, - currentToolCallCount: 0, - maxToolCalls: 5, - conversationId: 'conv-question' - }) - - const events: any[] = [] - let result: any = null - while (true) { - const { value, done } = await iterator.next() - if (done) { - result = value - break - } - events.push(value) - } - - const questionEvent = events.find( - (event) => event.type === 'response' && event.data?.tool_call === 'question-required' - ) - expect(questionEvent).toBeDefined() - expect(questionEvent.data.question_request?.question).toBe('Choose one') - expect(result.needContinueConversation).toBe(false) - expect(callTool).not.toHaveBeenCalled() - }) - - it('rejects non-standalone question tool calls', async () => { - const callTool = vi.fn(async () => ({ - content: 'ok', - rawData: { content: 'ok' } as MCPToolResponse - })) - const processor = new ToolCallProcessor({ - getAllToolDefinitions: async () => [questionToolDef, executeCommandDef], - callTool - }) - - const conversationMessages: ChatMessage[] = [{ role: 'assistant', content: 'hello' }] - const iterator = processor.process({ - eventId: 'event-question-2', - toolCalls: [ - { id: 'tool-q1', name: 'deepchat_question', arguments: basicQuestionArgs }, - { id: 'tool-2', name: 'exec', arguments: '{}' } - ], - enabledMcpTools: [], - conversationMessages, - modelConfig: { functionCall: true } as ModelConfig, - abortSignal: new AbortController().signal, - currentToolCallCount: 0, - maxToolCalls: 5, - conversationId: 'conv-question' - }) - - const events: any[] = [] - let result: any = null - while (true) { - const { value, done } = await iterator.next() - if (done) { - result = value - break - } - events.push(value) - } - - const errorEvent = events.find( - (event) => event.type === 'response' && event.data?.question_error - ) - expect(errorEvent).toBeDefined() - expect(result.needContinueConversation).toBe(true) - expect(callTool).toHaveBeenCalled() - }) -}) - -describe('ToolCallProcessor batch permission pre-check', () => { - it('preserves extended permission payload fields for batch permission requests', async () => { - const toolDefinition = { - type: 'function', - function: { - name: 'edit', - description: 'edit file', - parameters: { - type: 'object', - properties: {} - } - }, - server: { - name: 'agent-filesystem', - icons: '📁', - description: 'Agent filesystem' - } - } as MCPToolDefinition - - const callTool = vi.fn(async () => ({ - content: 'ok', - rawData: { content: 'ok' } as MCPToolResponse - })) - const preCheckToolPermission = vi.fn(async () => ({ - needsPermission: true as const, - toolName: 'edit', - serverName: 'agent-filesystem', - permissionType: 'write' as const, - description: 'Write access requires approval', - paths: ['src/main.ts'], - customMeta: { - source: 'batch-precheck' - } - })) - - const processor = new ToolCallProcessor({ - getAllToolDefinitions: async () => [toolDefinition], - callTool, - preCheckToolPermission - }) - - const conversationMessages: ChatMessage[] = [{ role: 'assistant', content: 'hello' }] - const iterator = processor.process({ - eventId: 'event-batch-permission', - toolCalls: [{ id: 'tool-1', name: 'edit', arguments: '{"path":"src/main.ts"}' }], - enabledMcpTools: [], - conversationMessages, - modelConfig: { functionCall: true } as ModelConfig, - abortSignal: new AbortController().signal, - currentToolCallCount: 0, - maxToolCalls: 5, - conversationId: 'conv-batch-permission' - }) - - const events: any[] = [] - let result: any = null - while (true) { - const { value, done } = await iterator.next() - if (done) { - result = value - break - } - events.push(value) - } - - const permissionEvent = events.find( - (event) => event.type === 'response' && event.data?.tool_call === 'permission-required' - ) - expect(permissionEvent).toBeDefined() - expect(permissionEvent.data.permission_request.paths).toEqual(['src/main.ts']) - expect(permissionEvent.data.permission_request.customMeta).toEqual({ - source: 'batch-precheck' - }) - expect(permissionEvent.data.permission_request.isBatchPermission).toBe(true) - expect(permissionEvent.data.permission_request.totalInBatch).toBe(1) - expect(callTool).not.toHaveBeenCalled() - expect(result.needContinueConversation).toBe(false) - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/message/systemEnvPromptBuilder.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/message/systemEnvPromptBuilder.test.ts deleted file mode 100644 index 4fef8fe73..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/message/systemEnvPromptBuilder.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import path from 'node:path' -import * as fs from 'node:fs' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { presenter } from '@/presenter' -import logger from '@shared/logger' -import { - buildRuntimeCapabilitiesPrompt, - buildSystemEnvPrompt -} from '@/lib/agentRuntime/systemEnvPromptBuilder' - -vi.mock('node:fs', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - existsSync: vi.fn(), - promises: { - ...actual.promises, - readFile: vi.fn() - } - } -}) - -vi.mock('@/presenter', () => ({ - presenter: { - configPresenter: { - getProviderModels: vi.fn(), - getCustomModels: vi.fn() - } - } -})) - -vi.mock('@shared/logger', () => ({ - default: { - warn: vi.fn() - } -})) - -describe('systemEnvPromptBuilder', () => { - beforeEach(() => { - vi.clearAllMocks() - ;(presenter.configPresenter.getProviderModels as ReturnType).mockReturnValue([]) - ;(presenter.configPresenter.getCustomModels as ReturnType).mockReturnValue([]) - ;(fs.existsSync as unknown as ReturnType).mockReturnValue(false) - ;(fs.promises.readFile as unknown as ReturnType).mockRejectedValue( - new Error('ENOENT') - ) - }) - - it('builds env prompt with git=yes and AGENTS instructions', async () => { - const workdir = path.resolve(path.sep, 'workspace', 'deepchat') - const gitPath = path.join(workdir, '.git') - const agentsPath = path.join(workdir, 'AGENTS.md') - - ;(fs.existsSync as unknown as ReturnType).mockImplementation( - (targetPath: string) => targetPath === gitPath - ) - ;(fs.promises.readFile as unknown as ReturnType).mockResolvedValue( - '# Repository Guidelines\nLine 2' - ) - ;(presenter.configPresenter.getProviderModels as ReturnType).mockReturnValue([ - { id: 'model-a', name: 'Model A' } - ]) - - const prompt = await buildSystemEnvPrompt({ - providerId: 'provider-x', - modelId: 'model-a', - workdir, - platform: 'win32', - now: new Date('2026-02-11T12:00:00.000Z') - }) - - expect(prompt).toContain('You are powered by the model named Model A.') - expect(prompt).toContain('The exact model ID is provider-x/model-a') - expect(prompt).toContain(`Working directory: ${workdir}`) - expect(prompt).toContain('Is directory a git repo: yes') - expect(prompt).toContain('Platform: win32') - expect(prompt).toContain("Today's date: Wed Feb 11 2026") - expect(prompt).toContain(`Instructions from: ${agentsPath}`) - expect(prompt).toContain('# Repository Guidelines\nLine 2') - }) - - it('falls back when AGENTS.md is missing and git=no', async () => { - const workdir = path.resolve(path.sep, 'workspace', 'deepchat') - const prompt = await buildSystemEnvPrompt({ - providerId: 'provider-y', - modelId: 'model-b', - workdir, - platform: 'linux', - now: new Date('2026-02-11T12:00:00.000Z') - }) - - expect(prompt).toContain('You are powered by the model named model-b.') - expect(prompt).toContain('The exact model ID is provider-y/model-b') - expect(prompt).toContain('Is directory a git repo: no') - expect(prompt).not.toContain('Instructions from:') - expect(prompt).not.toContain('[SystemEnvPromptBuilder] AGENTS.md not available') - expect(logger.warn).toHaveBeenCalledWith( - '[SystemEnvPromptBuilder] Failed to read AGENTS.md', - expect.objectContaining({ sourcePath: path.join(workdir, 'AGENTS.md') }) - ) - }) - - it('builds stable runtime capabilities prompt', () => { - const prompt = buildRuntimeCapabilitiesPrompt() - expect(prompt).toContain('## Runtime Capabilities') - expect(prompt).toContain('YoBrowser') - expect(prompt).toContain('process(list|poll|log|write|kill|remove)') - }) - - it('omits runtime capability lines when related tools are unavailable', () => { - const prompt = buildRuntimeCapabilitiesPrompt({ - hasYoBrowser: false, - hasExec: false, - hasProcess: false - }) - - expect(prompt).toBe('') - }) - - it('builds a partial runtime capabilities prompt when only exec is enabled', () => { - const prompt = buildRuntimeCapabilitiesPrompt({ - hasYoBrowser: false, - hasExec: true, - hasProcess: false - }) - - expect(prompt).toContain('exec(background: true)') - expect(prompt).toContain('yield window') - expect(prompt).not.toContain('YoBrowser') - expect(prompt).not.toContain('process(list|poll|log|write|kill|remove)') - }) - - it('falls back to unknown provider/model identity', async () => { - const workdir = path.resolve(path.sep, 'workspace', 'deepchat') - const prompt = await buildSystemEnvPrompt({ - providerId: ' ', - modelId: '', - workdir, - now: new Date('2026-02-11T12:00:00.000Z') - }) - - expect(prompt).toContain('You are powered by the model named unknown-model.') - expect(prompt).toContain('The exact model ID is unknown-provider/unknown-model') - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/messageBuilder.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/messageBuilder.test.ts deleted file mode 100644 index 0648c15e7..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/messageBuilder.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' - -// messageBuilder imports the full main-process presenter graph; mock it out so we can unit-test -// pure message-building utilities without initializing Electron/main presenters. -vi.mock('@/presenter', () => ({ presenter: {} })) - -describe('messageBuilder.composeAgentSystemPromptSections', () => { - it('keeps fixed V2.1 section order', async () => { - const { composeAgentSystemPromptSections } = - await import('@/presenter/agentPresenter/message/messageBuilder') - - const composed = composeAgentSystemPromptSections({ - basePrompt: 'BASE', - runtimePrompt: 'RUNTIME', - skillsMetadataPrompt: 'SKILLS_META', - skillsPrompt: 'SKILLS_ACTIVE', - envPrompt: 'ENV', - toolingPrompt: 'TOOLING' - }) - - expect(composed).toBe('BASE\n\nRUNTIME\n\nSKILLS_META\n\nSKILLS_ACTIVE\n\nENV\n\nTOOLING') - }) -}) - -describe('messageBuilder.buildPostToolExecutionContext', () => { - it('emits assistant(tool_calls) -> tool(tool_call_id) pairing when functionCall is enabled', async () => { - const { buildPostToolExecutionContext } = - await import('@/presenter/agentPresenter/message/messageBuilder') - - const messages = await buildPostToolExecutionContext({ - conversation: { settings: { systemPrompt: '' } } as any, - contextMessages: [], - userMessage: { role: 'user', content: { text: 'hi', files: [] } } as any, - currentAssistantMessage: { - role: 'assistant', - content: [{ type: 'content', content: 'calling tool' }] - } as any, - completedToolCall: { - id: 'call_1', - name: 'exec', - params: '{"command":"pwd"}', - response: '/tmp' - }, - modelConfig: { functionCall: true } as any - }) - - expect(messages.map((m) => m.role)).toEqual(['user', 'assistant', 'tool']) - expect(messages[1].tool_calls?.[0]?.id).toBe('call_1') - expect(messages[2].tool_call_id).toBe('call_1') - }) - - it('generates a stable tool_call_id when upstream id is empty', async () => { - const { buildPostToolExecutionContext } = - await import('@/presenter/agentPresenter/message/messageBuilder') - - const messages = await buildPostToolExecutionContext({ - conversation: { settings: { systemPrompt: '' } } as any, - contextMessages: [], - userMessage: { role: 'user', content: { text: 'hi', files: [] } } as any, - currentAssistantMessage: { - role: 'assistant', - content: [{ type: 'content', content: 'calling tool' }] - } as any, - completedToolCall: { - id: '', - name: 'exec', - params: '{"command":"pwd"}', - response: '/tmp' - }, - modelConfig: { functionCall: true } as any - }) - - const toolCallId = messages[1].tool_calls?.[0]?.id - expect(typeof toolCallId).toBe('string') - expect(toolCallId).toBeTruthy() - expect(messages[2].tool_call_id).toBe(toolCallId) - }) - - it('falls back to legacy function_call_record injection when functionCall is disabled', async () => { - const { buildPostToolExecutionContext } = - await import('@/presenter/agentPresenter/message/messageBuilder') - - const messages = await buildPostToolExecutionContext({ - conversation: { settings: { systemPrompt: '' } } as any, - contextMessages: [], - userMessage: { role: 'user', content: { text: 'hi', files: [] } } as any, - currentAssistantMessage: { - role: 'assistant', - content: [{ type: 'content', content: 'calling tool' }] - } as any, - completedToolCall: { - id: 'call_1', - name: 'exec', - params: '{"command":"pwd"}', - response: '/tmp' - }, - modelConfig: { functionCall: false } as any - }) - - expect(messages.map((m) => m.role)).toEqual(['user', 'assistant', 'user']) - expect(String(messages[1].content)).toContain('') - expect(messages.some((m) => m.role === 'tool')).toBe(false) - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/messageCompressor.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/messageCompressor.test.ts deleted file mode 100644 index 4fe004b73..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/messageCompressor.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { AssistantMessageBlock, Message, UserMessageContent } from '@shared/chat' -import { compressToolCallsFromContext } from '@/presenter/agentPresenter/message/messageCompressor' - -const baseUsage = { - context_usage: 0, - tokens_per_second: 0, - total_tokens: 0, - generation_time: 0, - first_token_time: 0, - reasoning_start_time: 0, - reasoning_end_time: 0, - input_tokens: 0, - output_tokens: 0 -} - -function createMessage(id: string, role: Message['role'], content: Message['content']): Message { - return { - id, - role, - content, - timestamp: Date.now(), - avatar: '', - name: '', - model_name: '', - model_id: '', - model_provider: '', - status: 'sent', - error: '', - usage: baseUsage, - conversationId: 'conversation', - is_variant: 0 - } -} - -function createUserContent(text: string): UserMessageContent { - return { - files: [], - links: [], - think: false, - search: false, - text, - content: [{ type: 'text', content: text }] - } -} - -function createAssistantWithToolCall(id: string): Message { - const toolCallBlock: AssistantMessageBlock = { - type: 'tool_call', - status: 'success', - timestamp: Date.now(), - tool_call: { - id: 'tool-1', - name: 'search', - params: '{"q":"hi"}', - response: 'ok' - } - } - - return createMessage(id, 'assistant', [toolCallBlock]) -} - -describe('messageCompressor', () => { - it('removes tool calls before the last user message when over budget', () => { - const assistant = createAssistantWithToolCall('assistant-1') - const user = createMessage('user-1', 'user', createUserContent('hello')) - - const { compressedMessages, removedTokens } = compressToolCallsFromContext( - [assistant, user], - 1, - true - ) - - const assistantBlocks = compressedMessages[0].content as AssistantMessageBlock[] - const hasToolCall = assistantBlocks.some((block) => block.type === 'tool_call') - - expect(hasToolCall).toBe(false) - expect(removedTokens).toBeGreaterThan(0) - }) - - it('keeps tool calls when function calling is disabled', () => { - const assistant = createAssistantWithToolCall('assistant-1') - const user = createMessage('user-1', 'user', createUserContent('hello')) - - const { compressedMessages, removedTokens } = compressToolCallsFromContext( - [assistant, user], - 1, - false - ) - - const assistantBlocks = compressedMessages[0].content as AssistantMessageBlock[] - const hasToolCall = assistantBlocks.some((block) => block.type === 'tool_call') - - expect(hasToolCall).toBe(true) - expect(removedTokens).toBe(0) - }) - - it('does not remove tool calls after the last user message', () => { - const user = createMessage('user-1', 'user', createUserContent('hello')) - const assistant = createAssistantWithToolCall('assistant-1') - - const { compressedMessages } = compressToolCallsFromContext([user, assistant], 1, true) - - const assistantBlocks = compressedMessages[1].content as AssistantMessageBlock[] - const hasToolCall = assistantBlocks.some((block) => block.type === 'tool_call') - - expect(hasToolCall).toBe(true) - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/permission/permissionHandler.resume.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/permission/permissionHandler.resume.test.ts deleted file mode 100644 index 46d1000e7..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/permission/permissionHandler.resume.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { AssistantMessage, AssistantMessageBlock } from '@shared/chat' -import { PermissionHandler } from '@/presenter/agentPresenter/permission/permissionHandler' -import type { ThreadHandlerContext } from '@/presenter/agentPresenter/types/handlerContext' -import type { MessageManager } from '@/presenter/sessionPresenter/managers/messageManager' -import type { StreamGenerationHandler } from '@/presenter/agentPresenter/streaming/streamGenerationHandler' -import type { LLMEventHandler } from '@/presenter/agentPresenter/streaming/llmEventHandler' -import type { GeneratingMessageState } from '@/presenter/agentPresenter/streaming/types' -import { CommandPermissionService } from '@/presenter/permission' - -vi.mock('@/presenter', () => ({ - presenter: {} -})) - -describe('PermissionHandler resume behavior', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('resumes all resolved tool calls after the final permission decision', async () => { - const conversationId = 'conv-resume' - const messageId = 'msg-resume' - let content: AssistantMessageBlock[] = [ - { - type: 'tool_call', - status: 'loading', - timestamp: Date.now(), - tool_call: { id: 'tool-1', name: 'read', params: '{}' } - }, - { - type: 'action', - action_type: 'tool_call_permission', - status: 'pending', - timestamp: Date.now(), - content: 'Permission required', - tool_call: { id: 'tool-1', name: 'read', params: '{}' }, - extra: { - needsUserAction: true, - serverName: 'agent-filesystem', - permissionType: 'read', - permissionRequest: JSON.stringify({ - toolName: 'read', - serverName: 'agent-filesystem', - permissionType: 'read', - description: 'Read access requires approval', - paths: ['src/a.txt'] - }) - } - }, - { - type: 'tool_call', - status: 'loading', - timestamp: Date.now(), - tool_call: { id: 'tool-2', name: 'read', params: '{}' } - }, - { - type: 'action', - action_type: 'tool_call_permission', - status: 'pending', - timestamp: Date.now(), - content: 'Permission required', - tool_call: { id: 'tool-2', name: 'read', params: '{}' }, - extra: { - needsUserAction: true, - serverName: 'agent-filesystem', - permissionType: 'read', - permissionRequest: JSON.stringify({ - toolName: 'read', - serverName: 'agent-filesystem', - permissionType: 'read', - description: 'Read access requires approval', - paths: ['src/b.txt'] - }) - } - } - ] - - const getCurrentMessage = (): AssistantMessage => - ({ - id: messageId, - conversationId, - role: 'assistant', - content - }) as AssistantMessage - - const messageManager = { - getMessage: vi.fn(async () => getCurrentMessage()), - editMessage: vi.fn(async (_id: string, nextContent: string) => { - content = JSON.parse(nextContent) as AssistantMessageBlock[] - return getCurrentMessage() - }), - handleMessageError: vi.fn() - } as unknown as MessageManager - - const sessionRuntime = { - removePendingPermission: vi.fn(), - acquirePermissionResumeLock: vi.fn().mockReturnValue(true), - releasePermissionResumeLock: vi.fn(), - getPendingPermissions: vi.fn().mockReturnValue([]), - clearPendingPermission: vi.fn(), - setStatus: vi.fn() - } - const mcpRuntime = { - grantPermission: vi.fn(), - isServerRunning: vi.fn().mockResolvedValue(true) - } - const permissionRuntime = { - approveFileAccess: vi.fn() - } - - const ctx: ThreadHandlerContext = { - sqlitePresenter: {} as never, - messageManager, - llmProviderPresenter: {} as never, - configPresenter: {} as never, - sessionRuntime: sessionRuntime as never, - toolPresenter: { - getAllToolDefinitions: vi.fn(), - callTool: vi.fn(), - buildToolSystemPrompt: vi.fn() - } as never, - mcpRuntime: mcpRuntime as never, - promptRuntime: { - getInputChatMode: vi.fn(), - getSkillsEnabled: vi.fn(), - getActiveSkills: vi.fn(), - loadSkillContent: vi.fn(), - getMetadataPrompt: vi.fn(), - getActiveSkillsAllowedTools: vi.fn() - } as never, - permissionRuntime: permissionRuntime as never - } - - const generatingMessages = new Map() - generatingMessages.set(messageId, { - message: getCurrentMessage(), - conversationId, - startTime: Date.now(), - firstTokenTime: null, - promptTokens: 0, - reasoningStartTime: null, - reasoningEndTime: null, - lastReasoningTime: null - }) - - const handler = new PermissionHandler(ctx, { - generatingMessages, - streamGenerationHandler: {} as StreamGenerationHandler, - llmEventHandler: {} as LLMEventHandler, - commandPermissionHandler: new CommandPermissionService() - }) - - const resumeSpy = vi - .spyOn( - handler as unknown as { resumeToolExecutionAfterPermissions: (...args: any[]) => any }, - 'resumeToolExecutionAfterPermissions' - ) - .mockResolvedValue(undefined) - - await handler.handlePermissionResponse(messageId, 'tool-1', true, 'read', false) - expect(resumeSpy).not.toHaveBeenCalled() - - await handler.handlePermissionResponse(messageId, 'tool-2', true, 'read', false) - expect(resumeSpy).toHaveBeenCalledTimes(1) - expect(resumeSpy).toHaveBeenCalledWith(messageId, true) - expect(resumeSpy.mock.calls[0][2]).toBeUndefined() - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/promptBuilder.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/promptBuilder.test.ts deleted file mode 100644 index 8e075bcb3..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/promptBuilder.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { approximateTokenSize } from 'tokenx' -import type { AssistantMessageBlock, Message, UserMessageContent } from '@shared/chat' -import { buildUserMessageContext } from '@/presenter/agentPresenter/message/messageFormatter' -import { selectContextMessages } from '@/presenter/agentPresenter/message/messageTruncator' - -const baseUsage = { - context_usage: 0, - tokens_per_second: 0, - total_tokens: 0, - generation_time: 0, - first_token_time: 0, - reasoning_start_time: 0, - reasoning_end_time: 0, - input_tokens: 0, - output_tokens: 0 -} - -function createUserContent(text: string): UserMessageContent { - return { - files: [], - links: [], - think: false, - search: false, - text, - content: [{ type: 'text', content: text }] - } -} - -function createMessage(id: string, role: Message['role'], content: Message['content']): Message { - return { - id, - role, - content, - timestamp: Date.now(), - avatar: '', - name: '', - model_name: '', - model_id: '', - model_provider: '', - status: 'sent', - error: '', - usage: baseUsage, - conversationId: 'conversation', - is_variant: 0 - } -} - -function createAssistantBlocks(text: string, toolCall?: AssistantMessageBlock['tool_call']) { - const blocks: AssistantMessageBlock[] = [ - { - type: 'content', - content: text, - status: 'success', - timestamp: Date.now() - } - ] - - if (toolCall) { - blocks.push({ - type: 'tool_call', - status: 'success', - timestamp: Date.now(), - tool_call: toolCall - }) - } - - return blocks -} - -function countUserTokens(content: UserMessageContent): number { - return approximateTokenSize(buildUserMessageContext(content)) -} - -function countAssistantTokens(text: string): number { - return approximateTokenSize(text) -} - -function countToolCallTokens(name: string, params: string, response: string): number { - return approximateTokenSize(name) + approximateTokenSize(params) + approximateTokenSize(response) -} - -describe('selectContextMessages', () => { - it('drops oldest pairs when no tool calls are present', () => { - const user1 = createMessage('user-1', 'user', createUserContent('hello one')) - const assistant1 = createMessage('assistant-1', 'assistant', createAssistantBlocks('reply one')) - const user2 = createMessage('user-2', 'user', createUserContent('hello two')) - const assistant2 = createMessage('assistant-2', 'assistant', createAssistantBlocks('reply two')) - const currentUser = createMessage('user-3', 'user', createUserContent('new question')) - - const pair2Tokens = - countUserTokens(user2.content as UserMessageContent) + countAssistantTokens('reply two') - - const selected = selectContextMessages( - [user1, assistant1, user2, assistant2], - currentUser, - pair2Tokens, - false, - false - ) - - expect(selected.map((message) => message.id)).toEqual(['user-2', 'assistant-2']) - }) - - it('removes tool call blocks before deleting message pairs', () => { - const toolCall = { - id: 'tool-1', - name: 'search', - params: '{"q":"hi"}', - response: 'tool response' - } - const user = createMessage('user-1', 'user', createUserContent('hello')) - const assistant = createMessage( - 'assistant-1', - 'assistant', - createAssistantBlocks('reply', toolCall) - ) - const currentUser = createMessage('user-2', 'user', createUserContent('new question')) - - const totalTokens = - countUserTokens(user.content as UserMessageContent) + - countAssistantTokens('reply') + - countToolCallTokens(toolCall.name, toolCall.params, toolCall.response) - - const remainingContextLength = - totalTokens - countToolCallTokens(toolCall.name, toolCall.params, toolCall.response) + 1 - - const selected = selectContextMessages( - [user, assistant], - currentUser, - remainingContextLength, - true, - false - ) - - const assistantBlocks = selected.find((message) => message.role === 'assistant') - ?.content as AssistantMessageBlock[] - const hasToolCallBlock = assistantBlocks.some((block) => block.type === 'tool_call') - - expect(selected.map((message) => message.id)).toEqual(['user-1', 'assistant-1']) - expect(hasToolCallBlock).toBe(false) - }) - - it('removes tool calls then drops oldest pairs when still over limit', () => { - const toolCall = { - id: 'tool-1', - name: 'search', - params: '{"q":"hi"}', - response: 'tool response' - } - const user1 = createMessage('user-1', 'user', createUserContent('hello one')) - const assistant1 = createMessage( - 'assistant-1', - 'assistant', - createAssistantBlocks('reply one', toolCall) - ) - const user2 = createMessage('user-2', 'user', createUserContent('hello two')) - const assistant2 = createMessage('assistant-2', 'assistant', createAssistantBlocks('reply two')) - const currentUser = createMessage('user-3', 'user', createUserContent('new question')) - - const pair2Tokens = - countUserTokens(user2.content as UserMessageContent) + countAssistantTokens('reply two') - - const remainingContextLength = pair2Tokens - 1 - - const selected = selectContextMessages( - [user1, assistant1, user2, assistant2], - currentUser, - remainingContextLength, - true, - false - ) - - expect(selected.map((message) => message.id)).toEqual(['user-2', 'assistant-2']) - }) - - it('returns empty when remaining context length is non-positive', () => { - const user = createMessage('user-1', 'user', createUserContent('hello')) - const assistant = createMessage('assistant-1', 'assistant', createAssistantBlocks('reply')) - - const selected = selectContextMessages([user, assistant], user, 0, false, false) - - expect(selected).toEqual([]) - }) - - it('returns empty when no sent context messages exist', () => { - const user = createMessage('user-1', 'user', createUserContent('hello')) - const assistant = createMessage('assistant-1', 'assistant', createAssistantBlocks('reply')) - user.status = 'pending' - assistant.status = 'pending' - - const selected = selectContextMessages([user, assistant], user, 100, false, false) - - expect(selected).toEqual([]) - }) - - it('retains image user messages when vision is enabled', () => { - const userWithImage = createMessage('user-1', 'user', createUserContent('hello')) - const imageContent = userWithImage.content as UserMessageContent & { images?: string[] } - imageContent.images = ['data:image/png;base64,abc'] - const assistant = createMessage('assistant-1', 'assistant', createAssistantBlocks('reply')) - const currentUser = createMessage('user-2', 'user', createUserContent('new question')) - - const selected = selectContextMessages( - [userWithImage, assistant], - currentUser, - 1000, - true, - true - ) - - expect(selected.map((message) => message.id)).toEqual(['user-1', 'assistant-1']) - }) - - it('keeps context edge messages when within limits', () => { - const user = createMessage('user-1', 'user', createUserContent('edge message')) - const assistant = createMessage('assistant-1', 'assistant', createAssistantBlocks('reply')) - const edgeUser = user as Message & { is_context_edge?: boolean } - edgeUser.is_context_edge = true - const currentUser = createMessage('user-2', 'user', createUserContent('new question')) - - const selected = selectContextMessages([user, assistant], currentUser, 1000, false, false) - - expect(selected.map((message) => message.id)).toEqual(['user-1', 'assistant-1']) - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/sessionManager.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/sessionManager.test.ts deleted file mode 100644 index e023f709f..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/sessionManager.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import path from 'path' -import { SessionManager } from '@/presenter/agentPresenter/session/sessionManager' - -vi.mock('electron', () => ({ - app: { - getPath: () => 'C:\\\\temp' - } -})) - -const baseSettings = { - providerId: 'provider-1', - modelId: 'model-1', - chatMode: 'agent' as const, - enabledMcpTools: [], - acpWorkdirMap: {}, - agentWorkspacePath: null -} - -const createConversation = (overrides?: Partial) => ({ - id: 'conv-1', - settings: { - ...baseSettings, - ...overrides - } -}) - -const createManager = (conversation: ReturnType) => { - const sessionPresenter = { - getConversation: vi.fn().mockResolvedValue(conversation), - updateConversationSettings: vi.fn().mockResolvedValue(undefined) - } as any - const configPresenter = { - getSetting: vi.fn().mockReturnValue('agent'), - getModelDefaultConfig: vi.fn().mockReturnValue({ - maxTokens: 0, - contextLength: 0, - vision: false, - functionCall: true, - reasoning: false, - type: 'chat' - }) - } as any - - return { - manager: new SessionManager({ configPresenter, sessionPresenter }), - sessionPresenter, - configPresenter - } -} - -describe('SessionManager', () => { - it('migrates legacy chat mode to agent mode silently', async () => { - const conversation = createConversation({ chatMode: 'chat' as any }) - const { manager } = createManager(conversation) - - const context = await manager.resolveWorkspaceContext( - conversation.id, - conversation.settings.modelId - ) - - // Chat mode should be migrated to agent mode - expect(context.chatMode).toBe('agent') - }) - - it('generates and persists workspace path for agent mode', async () => { - const conversation = createConversation({ chatMode: 'agent', agentWorkspacePath: null }) - const { manager, sessionPresenter } = createManager(conversation) - - const context = await manager.resolveWorkspaceContext( - conversation.id, - conversation.settings.modelId - ) - const expected = path.join('C:\\\\temp', 'deepchat-agent', 'workspaces', conversation.id) - - expect(context.chatMode).toBe('agent') - expect(context.agentWorkspacePath).toBe(expected) - expect(sessionPresenter.updateConversationSettings).toHaveBeenCalledWith(conversation.id, { - agentWorkspacePath: expected - }) - }) - - it('uses ACP workdir map for acp agent mode', async () => { - const conversation = createConversation({ - chatMode: 'acp agent', - acpWorkdirMap: { 'model-1': 'C:\\\\acp-workdir' } - }) - const { manager, sessionPresenter } = createManager(conversation) - - const context = await manager.resolveWorkspaceContext( - conversation.id, - conversation.settings.modelId - ) - - expect(context.chatMode).toBe('acp agent') - expect(context.agentWorkspacePath).toBe('C:\\\\acp-workdir') - expect(sessionPresenter.updateConversationSettings).not.toHaveBeenCalled() - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/skillsPromptBuilder.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/skillsPromptBuilder.test.ts deleted file mode 100644 index 237449ac8..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/skillsPromptBuilder.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { - buildSkillsMetadataPrompt, - buildSkillsPrompt, - getSkillsAllowedTools -} from '@/presenter/agentPresenter/message/skillsPromptBuilder' - -describe('skillsPromptBuilder', () => { - it('returns empty outputs when skills are disabled', async () => { - const promptRuntime = { - getSkillsEnabled: vi.fn().mockReturnValue(false), - getMetadataPrompt: vi.fn(), - getActiveSkills: vi.fn(), - loadSkillContent: vi.fn(), - getActiveSkillsAllowedTools: vi.fn() - } as any - - await expect(buildSkillsMetadataPrompt(promptRuntime)).resolves.toBe('') - await expect(buildSkillsPrompt(promptRuntime, 'conv-1')).resolves.toBe('') - await expect(getSkillsAllowedTools(promptRuntime, 'conv-1')).resolves.toEqual([]) - expect(promptRuntime.getMetadataPrompt).not.toHaveBeenCalled() - expect(promptRuntime.getActiveSkills).not.toHaveBeenCalled() - }) - - it('builds metadata, active skill prompt, and allowed tools from prompt runtime', async () => { - const promptRuntime = { - getSkillsEnabled: vi.fn().mockReturnValue(true), - getMetadataPrompt: vi.fn().mockResolvedValue('# Metadata'), - getActiveSkills: vi.fn().mockResolvedValue(['ocr', 'coder']), - loadSkillContent: vi.fn(async (name: string) => - name === 'ocr' - ? { name: 'ocr', content: 'OCR content' } - : { name: 'coder', content: 'Coder content' } - ), - getActiveSkillsAllowedTools: vi.fn().mockResolvedValue(['read', 'grep']) - } as any - - await expect(buildSkillsMetadataPrompt(promptRuntime)).resolves.toBe('# Metadata') - const prompt = await buildSkillsPrompt(promptRuntime, 'conv-1') - expect(prompt).toContain('## Skill: ocr') - expect(prompt).toContain('Coder content') - await expect(getSkillsAllowedTools(promptRuntime, 'conv-1')).resolves.toEqual(['read', 'grep']) - }) - - it('returns empty active skill prompt when no skill content can be loaded', async () => { - const promptRuntime = { - getSkillsEnabled: vi.fn().mockReturnValue(true), - getActiveSkills: vi.fn().mockResolvedValue(['ocr']), - loadSkillContent: vi.fn().mockResolvedValue(null) - } as any - - await expect(buildSkillsPrompt(promptRuntime, 'conv-1')).resolves.toBe('') - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/streaming/llmEventHandler.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/streaming/llmEventHandler.test.ts deleted file mode 100644 index f3c59a246..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/streaming/llmEventHandler.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { LLMEventHandler } from '@/presenter/agentPresenter/streaming/llmEventHandler' -import type { AgentSessionRuntimePort } from '@/presenter/agentPresenter/session/sessionRuntimePort' -import type { MessageManager } from '@/presenter/sessionPresenter/managers/messageManager' -import type { GeneratingMessageState } from '@/presenter/agentPresenter/streaming/types' - -const eventBusMock = vi.hoisted(() => ({ - sendToRenderer: vi.fn(), - sendToMain: vi.fn() -})) - -vi.mock('@/eventbus', () => ({ - eventBus: eventBusMock, - SendTarget: { - ALL_WINDOWS: 'ALL_WINDOWS' - } -})) - -function createState(eventId: string, blocks: any[] = []): GeneratingMessageState { - return { - message: { - id: eventId, - conversationId: 'conv-1', - parentId: 'user-1', - is_variant: false, - content: blocks - } as any, - conversationId: 'conv-1', - startTime: Date.now(), - firstTokenTime: null, - promptTokens: 0, - reasoningStartTime: null, - reasoningEndTime: null, - lastReasoningTime: null - } -} - -function createHandler(state: GeneratingMessageState) { - const generatingMessages = new Map([[state.message.id, state]]) - const messageManager = { - updateMessageMetadata: vi.fn().mockResolvedValue(undefined), - handleMessageError: vi.fn().mockResolvedValue(undefined), - getMessage: vi.fn().mockResolvedValue(state.message), - updateMessageStatus: vi.fn().mockResolvedValue(undefined), - editMessage: vi.fn().mockResolvedValue(undefined) - } as unknown as MessageManager - const sessionRuntime = { - incrementToolCallCount: vi.fn(), - addPendingPermission: vi.fn(), - setStatus: vi.fn(), - updateRuntime: vi.fn(), - clearPendingPermission: vi.fn(), - clearPendingQuestion: vi.fn() - } as unknown as AgentSessionRuntimePort - const handler = new LLMEventHandler({ - generatingMessages, - messageManager, - contentBufferHandler: { - cleanupContentBuffer: vi.fn(), - flushAdaptiveBuffer: vi.fn().mockResolvedValue(undefined) - } as any, - toolCallHandler: { - processToolCallStart: vi.fn().mockResolvedValue(undefined), - processToolCallUpdate: vi.fn().mockResolvedValue(undefined), - processToolCallPermission: vi.fn().mockResolvedValue(undefined), - processQuestionRequest: vi.fn().mockResolvedValue(undefined), - processToolCallError: vi.fn().mockResolvedValue(undefined), - processToolCallEnd: vi.fn().mockResolvedValue(undefined), - processSearchResultsFromToolCall: vi.fn().mockResolvedValue(undefined), - processMcpUiResourcesFromToolCall: vi.fn().mockResolvedValue(undefined) - } as any, - streamUpdateScheduler: { - enqueueDelta: vi.fn(), - flushAll: vi.fn().mockResolvedValue(undefined) - } as any, - sessionRuntime - }) - - return { - handler, - generatingMessages, - messageManager, - sessionRuntime - } -} - -describe('LLMEventHandler session runtime wiring', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('stores pending permissions through injected session runtime', async () => { - const state = createState('evt-permission') - const { handler, sessionRuntime } = createHandler(state) - - await handler.handleLLMAgentResponse({ - eventId: state.message.id, - tool_call: 'permission-required', - tool_call_id: 'tool-1', - permission_request: { - permissionType: 'write' - } - } as any) - - expect(sessionRuntime.addPendingPermission).toHaveBeenCalledWith('conv-1', { - messageId: state.message.id, - toolCallId: 'tool-1', - permissionType: 'write', - payload: { - permissionType: 'write' - } - }) - expect(sessionRuntime.setStatus).toHaveBeenCalledWith('conv-1', 'waiting_permission') - }) - - it('stores pending question through injected session runtime', async () => { - const state = createState('evt-question') - const { handler, sessionRuntime } = createHandler(state) - - await handler.handleLLMAgentResponse({ - eventId: state.message.id, - tool_call: 'question-required', - tool_call_id: 'tool-2' - } as any) - - expect(sessionRuntime.updateRuntime).toHaveBeenCalledWith('conv-1', { - pendingQuestion: { - messageId: state.message.id, - toolCallId: 'tool-2' - } - }) - expect(sessionRuntime.setStatus).toHaveBeenCalledWith('conv-1', 'waiting_question') - }) - - it('marks runtime as error through injected session runtime', async () => { - const state = createState('evt-error') - const { handler, generatingMessages, messageManager, sessionRuntime } = createHandler(state) - - await handler.handleLLMAgentError({ - eventId: state.message.id, - error: 'boom' - } as any) - - expect(messageManager.handleMessageError).toHaveBeenCalledWith(state.message.id, 'boom') - expect(sessionRuntime.setStatus).toHaveBeenCalledWith('conv-1', 'error') - expect(sessionRuntime.clearPendingPermission).toHaveBeenCalledWith('conv-1') - expect(sessionRuntime.clearPendingQuestion).toHaveBeenCalledWith('conv-1') - expect(generatingMessages.has(state.message.id)).toBe(false) - }) - - it('keeps waiting question status on end when pending question remains', async () => { - const state = createState('evt-end', [ - { - type: 'action', - action_type: 'question_request', - status: 'pending', - timestamp: Date.now(), - content: 'Need answer', - tool_call: { id: 'tool-3', name: 'question', params: '{}' } - } - ]) - const { handler, messageManager, sessionRuntime } = createHandler(state) - - await handler.handleLLMAgentEnd({ - eventId: state.message.id - } as any) - - expect(messageManager.updateMessageStatus).toHaveBeenCalledWith(state.message.id, 'sent') - expect(sessionRuntime.setStatus).toHaveBeenCalledWith('conv-1', 'waiting_permission') - expect(sessionRuntime.setStatus).toHaveBeenCalledWith('conv-1', 'waiting_question') - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/streaming/streamGenerationHandler.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/streaming/streamGenerationHandler.test.ts deleted file mode 100644 index 4421bcfe8..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/agentPresenter/streaming/streamGenerationHandler.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { StreamGenerationHandler } from '@/presenter/agentPresenter/streaming/streamGenerationHandler' -import type { ThreadHandlerContext } from '@/presenter/agentPresenter/types/handlerContext' -import type { AgentSessionRuntimePort } from '@/presenter/agentPresenter/session/sessionRuntimePort' -import type { MessageManager } from '@/presenter/sessionPresenter/managers/messageManager' -import type { GeneratingMessageState } from '@/presenter/agentPresenter/streaming/types' - -vi.mock('@/presenter/agentPresenter/message/messageBuilder', () => ({ - preparePromptContent: vi.fn().mockResolvedValue({ - finalContent: [{ role: 'user', content: 'hello' }], - promptTokens: 12 - }) -})) - -describe('StreamGenerationHandler session runtime wiring', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('uses injected session runtime for loop start and workspace resolution', async () => { - const conversationId = 'conv-stream' - const messageId = 'assistant-1' - const userMessageId = 'user-1' - const sessionRuntime = { - startLoop: vi.fn().mockResolvedValue(undefined), - resolveWorkspaceContext: vi.fn().mockResolvedValue({ - chatMode: 'agent', - agentWorkspacePath: 'C:/workspace' - }) - } as unknown as AgentSessionRuntimePort - const messageManager = { - getMessage: vi.fn().mockImplementation(async (id: string) => { - if (id === userMessageId) { - return { - id: userMessageId, - conversationId, - role: 'user', - content: { - text: 'hello', - files: [] - } - } - } - throw new Error(`Unexpected message lookup: ${id}`) - }), - getMessageHistory: vi.fn().mockResolvedValue([]), - updateMessageMetadata: vi.fn().mockResolvedValue(undefined) - } as unknown as MessageManager - const ctx: ThreadHandlerContext = { - sqlitePresenter: { - getConversation: vi.fn().mockResolvedValue({ - id: conversationId, - settings: { - providerId: 'mock-provider', - modelId: 'mock-model', - temperature: 0.7, - maxTokens: 1024, - enabledMcpTools: [], - thinkingBudget: undefined, - reasoningEffort: undefined, - verbosity: undefined, - contextLength: 2048 - } - }) - } as any, - messageManager, - llmProviderPresenter: { - startStreamCompletion: vi.fn().mockReturnValue( - (async function* () { - yield { type: 'end', data: { eventId: messageId } } - })() - ) - } as any, - configPresenter: { - getModelConfig: vi.fn().mockReturnValue({ - functionCall: false, - vision: false, - type: 'chat' - }), - getSetting: vi.fn().mockReturnValue(false) - } as any, - sessionRuntime, - toolPresenter: { - getAllToolDefinitions: vi.fn().mockResolvedValue([]), - buildToolSystemPrompt: vi.fn().mockReturnValue(''), - callTool: vi.fn() - } as any, - mcpRuntime: { - callTool: vi.fn(), - grantPermission: vi.fn(), - isServerRunning: vi.fn() - } as any, - promptRuntime: { - getInputChatMode: vi.fn(), - getSkillsEnabled: vi.fn().mockReturnValue(false), - getActiveSkills: vi.fn(), - loadSkillContent: vi.fn(), - getMetadataPrompt: vi.fn(), - getActiveSkillsAllowedTools: vi.fn().mockResolvedValue([]) - } as any, - permissionRuntime: {} as any - } - const llmEventHandler = { - handleLLMAgentResponse: vi.fn().mockResolvedValue(undefined), - handleLLMAgentError: vi.fn().mockResolvedValue(undefined), - handleLLMAgentEnd: vi.fn().mockResolvedValue(undefined) - } - const generatingMessages = new Map([ - [ - messageId, - { - message: { - id: messageId, - conversationId, - role: 'assistant', - content: [], - parentId: userMessageId - } as any, - conversationId, - startTime: Date.now(), - firstTokenTime: null, - promptTokens: 0, - reasoningStartTime: null, - reasoningEndTime: null, - lastReasoningTime: null - } - ] - ]) - - const handler = new StreamGenerationHandler(ctx, { - generatingMessages, - llmEventHandler: llmEventHandler as any - }) - - await handler.startStreamCompletion(conversationId, userMessageId) - - expect(sessionRuntime.startLoop).toHaveBeenCalledWith(conversationId, messageId, { - skipLockAcquisition: true - }) - expect(sessionRuntime.resolveWorkspaceContext).toHaveBeenCalledWith( - conversationId, - 'mock-model' - ) - expect(ctx.llmProviderPresenter.startStreamCompletion).toHaveBeenCalled() - }) -}) diff --git a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/sessionPresenter/permissionHandler.test.ts b/archives/code/legacy-agentpresenter-retirement/test/main/presenter/sessionPresenter/permissionHandler.test.ts deleted file mode 100644 index 5718839cf..000000000 --- a/archives/code/legacy-agentpresenter-retirement/test/main/presenter/sessionPresenter/permissionHandler.test.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { AssistantMessage, AssistantMessageBlock } from '@shared/chat' -import type { ILlmProviderPresenter } from '@shared/presenter' -import { PermissionHandler } from '@/presenter/agentPresenter/permission/permissionHandler' -import { CommandPermissionService } from '@/presenter/permission' -import { presenter } from '@/presenter' -import type { ThreadHandlerContext } from '@/presenter/agentPresenter/types/handlerContext' -import type { StreamGenerationHandler } from '@/presenter/agentPresenter/streaming/streamGenerationHandler' -import type { LLMEventHandler } from '@/presenter/agentPresenter/streaming/llmEventHandler' -import type { MessageManager } from '@/presenter/sessionPresenter/managers/messageManager' -import type { GeneratingMessageState } from '@/presenter/agentPresenter/streaming/types' - -const sessionState = vi.hoisted(() => ({ - pendingPermissions: new Map< - string, - Array<{ - messageId: string - toolCallId: string - permissionType: 'read' | 'write' | 'all' | 'command' - payload: Record - }> - >(), - locks: new Map(), - status: new Map(), - sessions: new Map() -})) - -const presenterMock = vi.hoisted(() => ({ - sessionManager: { - clearPendingPermission: vi.fn((agentId: string) => { - sessionState.pendingPermissions.delete(agentId) - }), - setStatus: vi.fn((agentId: string, status: string) => { - sessionState.status.set(agentId, status) - }), - getStatus: vi.fn((agentId: string) => { - return ( - (sessionState.status.get(agentId) as - | 'idle' - | 'generating' - | 'waiting_permission' - | 'waiting_question' - | null) ?? 'waiting_permission' - ) - }), - startLoop: vi.fn().mockResolvedValue(undefined), - removePendingPermission: vi.fn((agentId: string, messageId: string, toolCallId: string) => { - const pending = sessionState.pendingPermissions.get(agentId) ?? [] - sessionState.pendingPermissions.set( - agentId, - pending.filter((item) => !(item.messageId === messageId && item.toolCallId === toolCallId)) - ) - }), - addPendingPermission: vi.fn( - ( - agentId: string, - permission: { - messageId: string - toolCallId: string - permissionType: 'read' | 'write' | 'all' | 'command' - payload: Record - } - ) => { - const pending = sessionState.pendingPermissions.get(agentId) ?? [] - pending.push(permission) - sessionState.pendingPermissions.set(agentId, pending) - } - ), - getPendingPermissions: vi.fn((agentId: string) => { - return sessionState.pendingPermissions.get(agentId) ?? [] - }), - hasPendingPermissions: vi.fn((agentId: string, messageId?: string) => { - const pending = sessionState.pendingPermissions.get(agentId) ?? [] - if (!messageId) { - return pending.length > 0 - } - return pending.some((item) => item.messageId === messageId) - }), - acquirePermissionResumeLock: vi.fn((agentId: string, messageId: string) => { - if (sessionState.locks.get(agentId)?.messageId === messageId) { - return false - } - sessionState.locks.set(agentId, { messageId, startedAt: Date.now() }) - return true - }), - getPermissionResumeLock: vi.fn((agentId: string) => { - return sessionState.locks.get(agentId) - }), - releasePermissionResumeLock: vi.fn((agentId: string) => { - sessionState.locks.delete(agentId) - }), - getSessionSync: vi.fn((agentId: string) => { - return sessionState.sessions.get(agentId) ?? null - }) - } -})) - -vi.mock('@/presenter', () => ({ - presenter: presenterMock -})) - -const createAssistantMessage = ( - blocks: AssistantMessageBlock[], - conversationId: string, - messageId: string -): AssistantMessage => { - return { - id: messageId, - conversationId, - role: 'assistant', - content: blocks - } as AssistantMessage -} - -const createPermissionHandler = (options: { - message: AssistantMessage - llmProviderPresenter?: ILlmProviderPresenter -}) => { - let currentMessage = options.message - const messageManager = { - getMessage: vi.fn().mockImplementation(async () => currentMessage), - editMessage: vi.fn().mockImplementation(async (_id: string, rawContent: string) => { - const next = JSON.parse(rawContent) as AssistantMessageBlock[] - currentMessage = { - ...currentMessage, - content: next - } - return currentMessage - }), - handleMessageError: vi.fn() - } as unknown as MessageManager - - const ctx: ThreadHandlerContext = { - sqlitePresenter: {} as never, - messageManager, - llmProviderPresenter: options.llmProviderPresenter ?? ({} as ILlmProviderPresenter), - configPresenter: {} as never, - sessionRuntime: presenter.sessionManager as never, - toolPresenter: { - getAllToolDefinitions: vi.fn(), - callTool: vi.fn(), - buildToolSystemPrompt: vi.fn() - } as never, - mcpRuntime: { - grantPermission: vi.fn(), - isServerRunning: vi.fn().mockResolvedValue(true), - callTool: vi.fn() - } as never, - promptRuntime: { - getInputChatMode: vi.fn(), - getSkillsEnabled: vi.fn(), - getActiveSkills: vi.fn(), - loadSkillContent: vi.fn(), - getMetadataPrompt: vi.fn(), - getActiveSkillsAllowedTools: vi.fn() - } as never, - permissionRuntime: { - approveFileAccess: vi.fn(), - approveSettingsAccess: vi.fn() - } as never - } - - const generatingMessages = new Map() - generatingMessages.set(currentMessage.id, { - message: currentMessage, - conversationId: currentMessage.conversationId, - startTime: Date.now(), - firstTokenTime: null, - promptTokens: 0, - reasoningStartTime: null, - reasoningEndTime: null, - lastReasoningTime: null - }) - - const handler = new PermissionHandler(ctx, { - generatingMessages, - streamGenerationHandler: {} as StreamGenerationHandler, - llmEventHandler: {} as LLMEventHandler, - commandPermissionHandler: new CommandPermissionService() - }) - - return { handler, messageManager, getCurrentMessage: () => currentMessage } -} - -describe('PermissionHandler', () => { - beforeEach(() => { - vi.clearAllMocks() - sessionState.pendingPermissions.clear() - sessionState.locks.clear() - sessionState.status.clear() - sessionState.sessions.clear() - }) - - describe('ACP permissions', () => { - it('routes granted permissions through llmProviderPresenter for ACP blocks', async () => { - const conversationId = 'conv-1' - const messageId = 'msg-1' - const toolCallId = 'tool-1' - sessionState.sessions.set(conversationId, { id: conversationId }) - - const permissionBlock = { - type: 'action', - action_type: 'tool_call_permission', - status: 'pending', - timestamp: Date.now(), - content: 'components.messageBlockPermissionRequest.description.write', - tool_call: { id: toolCallId, name: 'acp-tool' }, - extra: { - needsUserAction: true, - providerId: 'acp', - permissionRequestId: 'req-123', - permissionRequest: JSON.stringify({ providerId: 'acp', requestId: 'req-123' }), - permissionType: 'write' - } - } as AssistantMessageBlock - - const message = createAssistantMessage([permissionBlock], conversationId, messageId) - const llmProviderPresenter = { - resolveAgentPermission: vi.fn().mockResolvedValue(undefined) - } as unknown as ILlmProviderPresenter - - const { handler } = createPermissionHandler({ - message, - llmProviderPresenter - }) - - await handler.handlePermissionResponse(messageId, toolCallId, true, 'write', false) - - expect(llmProviderPresenter.resolveAgentPermission).toHaveBeenCalledWith('req-123', true) - expect(presenter.sessionManager.clearPendingPermission).toHaveBeenCalledWith(conversationId) - expect(presenter.sessionManager.setStatus).toHaveBeenCalledWith(conversationId, 'generating') - }) - - it('routes denied permissions through llmProviderPresenter for ACP blocks', async () => { - const conversationId = 'conv-1' - const messageId = 'msg-1' - const toolCallId = 'tool-1' - sessionState.sessions.set(conversationId, { id: conversationId }) - - const permissionBlock = { - type: 'action', - action_type: 'tool_call_permission', - status: 'pending', - timestamp: Date.now(), - content: 'components.messageBlockPermissionRequest.description.write', - tool_call: { id: toolCallId, name: 'acp-tool' }, - extra: { - needsUserAction: true, - providerId: 'acp', - permissionRequestId: 'req-123', - permissionRequest: JSON.stringify({ providerId: 'acp', requestId: 'req-123' }), - permissionType: 'write' - } - } as AssistantMessageBlock - - const message = createAssistantMessage([permissionBlock], conversationId, messageId) - const llmProviderPresenter = { - resolveAgentPermission: vi.fn().mockResolvedValue(undefined) - } as unknown as ILlmProviderPresenter - - const { handler } = createPermissionHandler({ - message, - llmProviderPresenter - }) - - await handler.handlePermissionResponse(messageId, toolCallId, false, 'write', false) - - expect(llmProviderPresenter.resolveAgentPermission).toHaveBeenCalledWith('req-123', false) - }) - }) - - describe('Session manager helpers', () => { - it('stores and filters pending permissions by messageId', () => { - const sessionManager = presenter.sessionManager as unknown as { - addPendingPermission: (agentId: string, permission: any) => void - getPendingPermissions: (agentId: string) => Array<{ toolCallId: string }> - hasPendingPermissions: (agentId: string, messageId?: string) => boolean - } - - sessionManager.addPendingPermission('agent-1', { - messageId: 'msg-1', - toolCallId: 'tool-1', - permissionType: 'read', - payload: {} - }) - sessionManager.addPendingPermission('agent-1', { - messageId: 'msg-2', - toolCallId: 'tool-2', - permissionType: 'write', - payload: {} - }) - - const pending = sessionManager.getPendingPermissions('agent-1') - expect(pending).toHaveLength(2) - expect(sessionManager.hasPendingPermissions('agent-1', 'msg-1')).toBe(true) - expect(sessionManager.hasPendingPermissions('agent-1', 'msg-3')).toBe(false) - }) - - it('acquires and releases permission resume lock', () => { - const sessionManager = presenter.sessionManager as unknown as { - acquirePermissionResumeLock: (agentId: string, messageId: string) => boolean - getPermissionResumeLock: ( - agentId: string - ) => { messageId: string; startedAt: number } | undefined - releasePermissionResumeLock: (agentId: string) => void - } - - const firstAcquire = sessionManager.acquirePermissionResumeLock('agent-1', 'msg-1') - const secondAcquire = sessionManager.acquirePermissionResumeLock('agent-1', 'msg-1') - const lock = sessionManager.getPermissionResumeLock('agent-1') - - expect(firstAcquire).toBe(true) - expect(secondAcquire).toBe(false) - expect(lock?.messageId).toBe('msg-1') - expect(lock?.startedAt).toBeGreaterThan(0) - - sessionManager.releasePermissionResumeLock('agent-1') - expect(sessionManager.getPermissionResumeLock('agent-1')).toBeUndefined() - }) - }) - - describe('Permission level hierarchy', () => { - it('updates same tool-call permission blocks with same or lower required levels', async () => { - const conversationId = 'conv-2' - const messageId = 'msg-2' - const toolCallId = 'tool-1' - sessionState.sessions.set(conversationId, { id: conversationId }) - - const readPermissionBlock: AssistantMessageBlock = { - type: 'action', - action_type: 'tool_call_permission', - status: 'pending', - timestamp: Date.now(), - content: 'Permission required', - tool_call: { - id: toolCallId, - name: 'write_file', - params: '{"path":"a.txt"}' - }, - extra: { - needsUserAction: true, - serverName: 'mock-server', - permissionType: 'read' - } - } - - const writePermissionBlock: AssistantMessageBlock = { - type: 'action', - action_type: 'tool_call_permission', - status: 'pending', - timestamp: Date.now(), - content: 'Permission required', - tool_call: { - id: toolCallId, - name: 'write_file', - params: '{"path":"a.txt"}' - }, - extra: { - needsUserAction: true, - serverName: 'mock-server', - permissionType: 'write' - } - } - - const message = createAssistantMessage( - [readPermissionBlock, writePermissionBlock], - conversationId, - messageId - ) - const { handler, messageManager } = createPermissionHandler({ message }) - vi.spyOn( - handler as unknown as { resumeToolExecutionAfterPermissions: (...args: any[]) => any }, - 'resumeToolExecutionAfterPermissions' - ).mockResolvedValue(undefined) - - await handler.handlePermissionResponse(messageId, toolCallId, true, 'write', false) - - const updatedContent = JSON.parse( - (messageManager.editMessage as unknown as { mock: { calls: Array<[string, string]> } }).mock - .calls[0][1] - ) as AssistantMessageBlock[] - - const updatedPermissionBlocks = updatedContent.filter( - (block) => block.type === 'action' && block.action_type === 'tool_call_permission' - ) - expect(updatedPermissionBlocks).toHaveLength(2) - expect(updatedPermissionBlocks.every((block) => block.status === 'granted')).toBe(true) - expect(updatedPermissionBlocks.every((block) => block.extra?.needsUserAction === false)).toBe( - true - ) - }) - }) -}) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e6a6e7173..a69af7b1c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -7,22 +7,22 @@ ```mermaid flowchart LR Renderer["Renderer / Stores / Views"] --> Preload["preload IPC bridge"] - Preload --> NewAgent["newAgentPresenter"] + Preload --> NewAgent["agentSessionPresenter"] NewAgent --> Registry["AgentRegistry"] - Registry --> DeepChat["deepchatAgentPresenter"] + Registry --> DeepChat["agentRuntimePresenter"] DeepChat --> Tool["toolPresenter"] DeepChat --> Llm["llmProviderPresenter"] Tool --> Mcp["mcpPresenter"] Tool --> AgentTools["toolPresenter/agentTools"] Llm --> Acp["llmProviderPresenter/acp"] DeepChat --> SQLite["sqlitePresenter"] - NewAgent --> SessionDb["newAgentPresenter/sessionManager"] + NewAgent --> SessionDb["agentSessionPresenter/sessionManager"] ``` 主结论: -- `newAgentPresenter` 是 renderer 唯一会话入口。 -- `deepchatAgentPresenter` 持有聊天 runtime、流式执行、工具交互、暂停恢复。 +- `agentSessionPresenter` 是 renderer 唯一会话入口。 +- `agentRuntimePresenter` 持有聊天 runtime、流式执行、工具交互、暂停恢复。 - `toolPresenter` 统一路由 MCP tools 与本地 agent tools。 - `llmProviderPresenter` 统一管理 provider 实例、流状态和 ACP provider helper。 @@ -31,8 +31,8 @@ flowchart LR | 模块 | 位置 | 职责 | | --- | --- | --- | | `Presenter` 组装层 | `src/main/presenter/index.ts` | 组装 presenter 依赖,暴露主进程能力 | -| `NewAgentPresenter` | `src/main/presenter/newAgentPresenter/` | 会话创建、窗口绑定、agent 注册、IPC-facing API | -| `DeepChatAgentPresenter` | `src/main/presenter/deepchatAgentPresenter/` | 聊天 runtime、stream loop、tool interaction、message persistence | +| `AgentSessionPresenter` | `src/main/presenter/agentSessionPresenter/` | 会话创建、窗口绑定、agent 注册、IPC-facing API | +| `AgentRuntimePresenter` | `src/main/presenter/agentRuntimePresenter/` | 聊天 runtime、stream loop、tool interaction、message persistence | | `ToolPresenter` | `src/main/presenter/toolPresenter/` | 工具定义聚合、调用路由、权限预检查 | | `Agent tools` | `src/main/presenter/toolPresenter/agentTools/` | 文件系统、命令、settings 等本地工具 | | `LLMProviderPresenter` | `src/main/presenter/llmProviderPresenter/` | provider 实例、stream state、model 管理、embedding、ACP provider | @@ -43,7 +43,7 @@ flowchart LR ### 1. IPC / Session orchestration -`newAgentPresenter` 负责: +`agentSessionPresenter` 负责: - 创建/删除/激活会话 - 绑定 `webContentsId -> sessionId` @@ -53,7 +53,7 @@ flowchart LR ### 2. Chat runtime -`deepchatAgentPresenter` 负责: +`agentRuntimePresenter` 负责: - `processMessage()` 和 `processStream()` 主循环 - `sessionStore` / `messageStore` / `pendingInputStore` @@ -82,7 +82,7 @@ flowchart LR 这次 retirement 后仍然保留的 legacy 边界只有: -- `src/main/presenter/newAgentPresenter/legacyImportService.ts` +- `src/main/presenter/agentSessionPresenter/legacyImportService.ts` - legacy import hook / status tracking - 旧 `conversations/messages` 表,作为 import-only 与导出数据源 - `SessionPresenter` 作为 main 内部数据平面,不再是 renderer 主聊天入口 @@ -95,7 +95,7 @@ flowchart LR ## 归档与防回归 -- 退休代码已归档到 `archives/code/legacy-agentpresenter-retirement/` +- 退休 runtime 的历史结构已经固化到 `docs/archives/` 文档,不再保留源码级导航入口 - 历史架构文档见 [archives/legacy-agentpresenter-architecture.md](./archives/legacy-agentpresenter-architecture.md) - 历史流程文档见 [archives/legacy-agentpresenter-flows.md](./archives/legacy-agentpresenter-flows.md) - 防回归脚本:`scripts/agent-cleanup-guard.mjs` diff --git a/docs/FLOWS.md b/docs/FLOWS.md index 331399129..6b81c206b 100644 --- a/docs/FLOWS.md +++ b/docs/FLOWS.md @@ -8,9 +8,9 @@ ```mermaid sequenceDiagram participant R as Renderer - participant N as NewAgentPresenter + participant N as AgentSessionPresenter participant A as AgentRegistry - participant D as DeepChatAgentPresenter + participant D as AgentRuntimePresenter participant S as NewSessionManager R->>N: createSession(input, webContentsId) @@ -24,9 +24,9 @@ sequenceDiagram 关键文件: -- `src/main/presenter/newAgentPresenter/index.ts` -- `src/main/presenter/newAgentPresenter/sessionManager.ts` -- `src/main/presenter/deepchatAgentPresenter/index.ts` +- `src/main/presenter/agentSessionPresenter/index.ts` +- `src/main/presenter/agentSessionPresenter/sessionManager.ts` +- `src/main/presenter/agentRuntimePresenter/index.ts` ## 2. DeepChat 消息处理主循环 @@ -48,16 +48,16 @@ flowchart TD 关键文件: -- `src/main/presenter/deepchatAgentPresenter/process.ts` -- `src/main/presenter/deepchatAgentPresenter/dispatch.ts` -- `src/main/presenter/deepchatAgentPresenter/contextBuilder.ts` -- `src/main/presenter/deepchatAgentPresenter/messageStore.ts` +- `src/main/presenter/agentRuntimePresenter/process.ts` +- `src/main/presenter/agentRuntimePresenter/dispatch.ts` +- `src/main/presenter/agentRuntimePresenter/contextBuilder.ts` +- `src/main/presenter/agentRuntimePresenter/messageStore.ts` ## 3. 工具调用与权限 ```mermaid sequenceDiagram - participant D as DeepChatAgentPresenter + participant D as AgentRuntimePresenter participant T as ToolPresenter participant M as MCP Presenter participant G as AgentToolManager @@ -94,8 +94,8 @@ sequenceDiagram ```mermaid sequenceDiagram participant R as Renderer - participant N as NewAgentPresenter - participant D as DeepChatAgentPresenter + participant N as AgentSessionPresenter + participant D as AgentRuntimePresenter participant L as LLMProviderPresenter participant A as ACP helpers @@ -109,7 +109,7 @@ sequenceDiagram 关键文件: -- `src/main/presenter/newAgentPresenter/index.ts` +- `src/main/presenter/agentSessionPresenter/index.ts` - `src/main/presenter/llmProviderPresenter/index.ts` - `src/main/presenter/llmProviderPresenter/acp/` @@ -118,7 +118,7 @@ sequenceDiagram ```mermaid sequenceDiagram participant Hook as lifecycle import hook - participant N as NewAgentPresenter + participant N as AgentSessionPresenter participant I as LegacyChatImportService participant DB as SQLite / legacy tables @@ -133,5 +133,5 @@ sequenceDiagram 关键文件: -- `src/main/presenter/newAgentPresenter/legacyImportService.ts` +- `src/main/presenter/agentSessionPresenter/legacyImportService.ts` - `src/main/presenter/lifecyclePresenter/hooks/after-start/legacyImportHook.ts` diff --git a/docs/README.md b/docs/README.md index 86f523566..c4bf30e04 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,8 +7,8 @@ ```text Renderer -> preload IPC - -> newAgentPresenter - -> deepchatAgentPresenter + -> agentSessionPresenter + -> agentRuntimePresenter -> llmProviderPresenter / toolPresenter / mcpPresenter -> sqlitePresenter ``` @@ -21,11 +21,13 @@ Renderer | --- | --- | | [ARCHITECTURE.md](./ARCHITECTURE.md) | 当前主架构总览 | | [FLOWS.md](./FLOWS.md) | 当前消息、工具、ACP、导入流程 | -| [architecture/agent-system.md](./architecture/agent-system.md) | `newAgentPresenter` / `deepchatAgentPresenter` 细节 | +| [architecture/agent-system.md](./architecture/agent-system.md) | `agentSessionPresenter` / `agentRuntimePresenter` 细节 | | [architecture/tool-system.md](./architecture/tool-system.md) | `ToolPresenter`、agent tools、ACP helper 分层 | | [architecture/session-management.md](./architecture/session-management.md) | 新会话管理与 legacy 数据平面边界 | | [guides/code-navigation.md](./guides/code-navigation.md) | 当前代码导航入口 | | [guides/getting-started.md](./guides/getting-started.md) | 新开发者快速上手 | +| [architecture/baselines/dependency-report.md](./architecture/baselines/dependency-report.md) | 当前依赖与耦合基线 | +| [architecture/baselines/test-failure-groups.md](./architecture/baselines/test-failure-groups.md) | 当前测试失败分组基线 | ## 本次清理落库 @@ -34,7 +36,9 @@ Renderer | [docs/specs/legacy-agentpresenter-retirement/spec.md](./specs/legacy-agentpresenter-retirement/spec.md) | 本次 retirement 的目标、范围、兼容边界 | | [docs/specs/legacy-agentpresenter-retirement/plan.md](./specs/legacy-agentpresenter-retirement/plan.md) | 迁移/归档/验证计划 | | [docs/specs/legacy-agentpresenter-retirement/tasks.md](./specs/legacy-agentpresenter-retirement/tasks.md) | 已执行清单 | -| [archives/code/legacy-agentpresenter-retirement/README.md](../archives/code/legacy-agentpresenter-retirement/README.md) | 代码归档说明 | +| [docs/specs/architecture-simplification/spec.md](./specs/architecture-simplification/spec.md) | 整体减负治理规格 | +| [docs/specs/architecture-simplification/plan.md](./specs/architecture-simplification/plan.md) | 分层/基线/guard 计划 | +| [docs/specs/architecture-simplification/tasks.md](./specs/architecture-simplification/tasks.md) | 首期实施清单 | | [docs/specs/agent-cleanup/spec.md](./specs/agent-cleanup/spec.md) | cleanup 主规格,已更新到 retirement 完成态 | ## 活跃架构地图 @@ -46,6 +50,7 @@ docs/ ├── FLOWS.md ├── architecture/ │ ├── agent-system.md +│ ├── baselines/ │ ├── session-management.md │ ├── tool-system.md │ ├── event-system.md @@ -56,6 +61,7 @@ docs/ │ └── debugging.md ├── specs/ │ ├── agent-cleanup/ +│ ├── architecture-simplification/ │ └── legacy-agentpresenter-retirement/ └── archives/ ├── legacy-agentpresenter-architecture.md @@ -83,4 +89,4 @@ docs/ - 聊天执行链路: [architecture/agent-system.md](./architecture/agent-system.md) - 工具与权限: [architecture/tool-system.md](./architecture/tool-system.md) - 会话与兼容边界: [architecture/session-management.md](./architecture/session-management.md) -4. 如果需要对照旧实现,再去看 `archives/`。 +4. 如果需要对照旧实现,再去看 `archives/` 历史文档,不要依赖已经移除的历史源码快照。 diff --git a/docs/architecture/agent-system.md b/docs/architecture/agent-system.md index ff0610ece..48422d0e3 100644 --- a/docs/architecture/agent-system.md +++ b/docs/architecture/agent-system.md @@ -7,9 +7,9 @@ ```mermaid flowchart TD - UI["Renderer / IPC"] --> NewAgent["NewAgentPresenter"] + UI["Renderer / IPC"] --> NewAgent["AgentSessionPresenter"] NewAgent --> Registry["AgentRegistry"] - Registry --> DeepChat["DeepChatAgentPresenter"] + Registry --> DeepChat["AgentRuntimePresenter"] DeepChat --> Context["contextBuilder"] DeepChat --> Process["process.ts"] DeepChat --> Dispatch["dispatch.ts"] @@ -21,16 +21,16 @@ flowchart TD 主原则: -- renderer 只面向 `newAgentPresenter` -- `newAgentPresenter` 只做 session orchestration,不执行聊天 loop -- `deepchatAgentPresenter` 独占聊天 runtime +- renderer 只面向 `agentSessionPresenter` +- `agentSessionPresenter` 只做 session orchestration,不执行聊天 loop +- `agentRuntimePresenter` 独占聊天 runtime ## 模块布局 -### `newAgentPresenter/` +### `agentSessionPresenter/` ```text -newAgentPresenter/ +agentSessionPresenter/ ├── index.ts ├── agentRegistry.ts ├── sessionManager.ts @@ -46,10 +46,10 @@ newAgentPresenter/ - 暴露 renderer IPC 方法 - 保留 legacy import 流程 -### `deepchatAgentPresenter/` +### `agentRuntimePresenter/` ```text -deepchatAgentPresenter/ +agentRuntimePresenter/ ├── index.ts ├── process.ts ├── dispatch.ts @@ -75,12 +75,12 @@ deepchatAgentPresenter/ | 层 | 主文件 | 责任 | | --- | --- | --- | -| Session orchestration | `src/main/presenter/newAgentPresenter/index.ts` | session 生命周期与 IPC | -| Agent runtime | `src/main/presenter/deepchatAgentPresenter/index.ts` | run state、取消、恢复、模型/权限切换 | -| Stream loop | `src/main/presenter/deepchatAgentPresenter/process.ts` | 调用 provider、累计 blocks、驱动 tool loop | -| Tool dispatch | `src/main/presenter/deepchatAgentPresenter/dispatch.ts` | 调用 `ToolPresenter`、暂停交互、生成 tool 结果 | -| Context build | `src/main/presenter/deepchatAgentPresenter/contextBuilder.ts` | 历史裁剪、resume context、token budget | -| Persistence | `src/main/presenter/deepchatAgentPresenter/messageStore.ts` | 消息持久化与故障恢复 | +| Session orchestration | `src/main/presenter/agentSessionPresenter/index.ts` | session 生命周期与 IPC | +| Agent runtime | `src/main/presenter/agentRuntimePresenter/index.ts` | run state、取消、恢复、模型/权限切换 | +| Stream loop | `src/main/presenter/agentRuntimePresenter/process.ts` | 调用 provider、累计 blocks、驱动 tool loop | +| Tool dispatch | `src/main/presenter/agentRuntimePresenter/dispatch.ts` | 调用 `ToolPresenter`、暂停交互、生成 tool 结果 | +| Context build | `src/main/presenter/agentRuntimePresenter/contextBuilder.ts` | 历史裁剪、resume context、token budget | +| Persistence | `src/main/presenter/agentRuntimePresenter/messageStore.ts` | 消息持久化与故障恢复 | ## 兼容边界 @@ -101,10 +101,10 @@ deepchatAgentPresenter/ 如果要追一条真实消息链路,推荐顺序: -1. `src/main/presenter/newAgentPresenter/index.ts` -2. `src/main/presenter/deepchatAgentPresenter/index.ts` -3. `src/main/presenter/deepchatAgentPresenter/process.ts` -4. `src/main/presenter/deepchatAgentPresenter/dispatch.ts` +1. `src/main/presenter/agentSessionPresenter/index.ts` +2. `src/main/presenter/agentRuntimePresenter/index.ts` +3. `src/main/presenter/agentRuntimePresenter/process.ts` +4. `src/main/presenter/agentRuntimePresenter/dispatch.ts` 5. `src/main/presenter/toolPresenter/index.ts` ## 历史说明 @@ -117,4 +117,4 @@ deepchatAgentPresenter/ - `permissionHandler` - `startStreamCompletion` -需要对照旧实现时,只看归档文档和 `archives/code/legacy-agentpresenter-retirement/`。 +需要对照旧实现时,只看归档文档,不再把历史源码快照当作活跃导航入口。 diff --git a/docs/architecture/baselines/archive-reference-report.md b/docs/architecture/baselines/archive-reference-report.md new file mode 100644 index 000000000..79b127297 --- /dev/null +++ b/docs/architecture/baselines/archive-reference-report.md @@ -0,0 +1,6 @@ +# Archive Reference Baseline + +Generated on 2026-04-03. + +- Total references: 0 + diff --git a/docs/architecture/baselines/dependency-report.md b/docs/architecture/baselines/dependency-report.md new file mode 100644 index 000000000..46a2b1137 --- /dev/null +++ b/docs/architecture/baselines/dependency-report.md @@ -0,0 +1,118 @@ +# Dependency Baseline + +Generated on 2026-04-03. + +## main + +- Total files: 333 +- Internal dependency edges: 826 +- Cycles detected: 30 + +### Top outgoing dependencies + +- `presenter/index.ts`: 39 +- `presenter/llmProviderPresenter/managers/providerInstanceManager.ts`: 38 +- `presenter/configPresenter/index.ts`: 22 +- `presenter/agentRuntimePresenter/index.ts`: 20 +- `presenter/lifecyclePresenter/hooks/index.ts`: 17 +- `presenter/sqlitePresenter/index.ts`: 16 +- `presenter/remoteControlPresenter/index.ts`: 15 +- `presenter/llmProviderPresenter/index.ts`: 14 +- `presenter/toolPresenter/agentTools/agentToolManager.ts`: 14 +- `presenter/agentSessionPresenter/index.ts`: 13 +- `presenter/llmProviderPresenter/acp/index.ts`: 12 +- `presenter/filePresenter/mime.ts`: 11 +- `presenter/mcpPresenter/inMemoryServers/builder.ts`: 11 +- `presenter/skillSyncPresenter/adapters/index.ts`: 11 +- `presenter/filePresenter/FileAdapterConstructor.ts`: 10 + +### Top incoming dependencies + +- `events.ts`: 58 +- `eventbus.ts`: 57 +- `presenter/index.ts`: 44 +- `presenter/llmProviderPresenter/runtimePorts.ts`: 34 +- `presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts`: 24 +- `presenter/llmProviderPresenter/baseProvider.ts`: 19 +- `presenter/remoteControlPresenter/types.ts`: 18 +- `presenter/sqlitePresenter/tables/baseTable.ts`: 16 +- `presenter/sqlitePresenter/index.ts`: 12 +- `presenter/configPresenter/modelCapabilities.ts`: 11 +- `presenter/filePresenter/BaseFileAdapter.ts`: 11 +- `presenter/proxyConfig.ts`: 10 +- `lib/runtimeHelper.ts`: 9 +- `presenter/configPresenter/providerDbLoader.ts`: 9 +- `presenter/configPresenter/acpRegistryConstants.ts`: 8 + +### Cycle samples + +- `presenter/index.ts -> presenter/windowPresenter/index.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/windowPresenter/index.ts -> presenter/tabPresenter.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/windowPresenter/index.ts -> presenter/windowPresenter/FloatingChatWindow.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/shortcutPresenter.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/llmProviderPresenter/index.ts -> presenter/llmProviderPresenter/baseProvider.ts -> presenter/devicePresenter/index.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/llmProviderPresenter/index.ts -> presenter/llmProviderPresenter/managers/providerInstanceManager.ts -> presenter/llmProviderPresenter/providers/deepseekProvider.ts -> presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/llmProviderPresenter/index.ts -> presenter/llmProviderPresenter/managers/providerInstanceManager.ts -> presenter/llmProviderPresenter/providers/githubCopilotProvider.ts -> presenter/githubCopilotDeviceFlow.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/llmProviderPresenter/index.ts -> presenter/llmProviderPresenter/managers/providerInstanceManager.ts -> presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts -> presenter/index.ts` +- `presenter/filePresenter/mime.ts -> presenter/filePresenter/CsvFileAdapter.ts -> presenter/filePresenter/BaseFileAdapter.ts -> presenter/filePresenter/mime.ts` +- `presenter/index.ts -> presenter/sessionPresenter/index.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/sessionPresenter/index.ts -> presenter/sessionPresenter/managers/conversationManager.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/mcpPresenter/inMemoryServers/builder.ts -> presenter/mcpPresenter/inMemoryServers/deepResearchServer.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/mcpPresenter/inMemoryServers/builder.ts -> presenter/mcpPresenter/inMemoryServers/autoPromptingServer.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/mcpPresenter/inMemoryServers/builder.ts -> presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/serverManager.ts -> presenter/mcpPresenter/mcpClient.ts -> presenter/mcpPresenter/inMemoryServers/builder.ts -> presenter/mcpPresenter/inMemoryServers/builtinKnowledgeServer.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/mcpPresenter/toolManager.ts -> presenter/index.ts` +- `presenter/index.ts -> presenter/mcpPresenter/index.ts -> presenter/index.ts` +- `presenter/sqlitePresenter/index.ts -> presenter/agentSessionPresenter/legacyImportService.ts -> presenter/sqlitePresenter/index.ts` +- `presenter/index.ts -> presenter/syncPresenter/index.ts -> presenter/index.ts` + +## renderer + +- Total files: 235 +- Internal dependency edges: 478 +- Cycles detected: 4 + +### Top outgoing dependencies + +- `App.vue`: 24 +- `pages/ChatPage.vue`: 17 +- `views/PlaygroundTabView.vue`: 16 +- `components/message/MessageItemAssistant.vue`: 15 +- `i18n/index.ts`: 12 +- `components/chat/ChatStatusBar.vue`: 11 +- `components/ChatConfig.vue`: 9 +- `components/sidepanel/WorkspacePanel.vue`: 9 +- `pages/NewThreadPage.vue`: 9 +- `views/ChatTabView.vue`: 9 +- `components/AppBar.vue`: 8 +- `components/WindowSideBar.vue`: 8 +- `components/mcp-config/components/McpServers.vue`: 8 +- `components/message/MessageBlockContent.vue`: 8 +- `components/chat/composables/useChatInputMentions.ts`: 7 + +### Top incoming dependencies + +- `composables/usePresenter.ts`: 64 +- `events.ts`: 32 +- `components/chat/messageListItems.ts`: 16 +- `stores/ui/session.ts`: 15 +- `stores/artifact.ts`: 13 +- `stores/providerStore.ts`: 13 +- `stores/theme.ts`: 12 +- `stores/modelStore.ts`: 11 +- `stores/ui/agent.ts`: 11 +- `stores/uiSettingsStore.ts`: 10 +- `stores/mcp.ts`: 8 +- `stores/ui/sidepanel.ts`: 8 +- `components/icons/ModelIcon.vue`: 6 +- `components/use-toast.ts`: 6 +- `stores/language.ts`: 6 + +### Cycle samples + +- `stores/ui/session.ts -> stores/ui/message.ts -> stores/ui/session.ts` +- `components/json-viewer/JsonValue.ts -> components/json-viewer/JsonObject.ts -> components/json-viewer/JsonValue.ts` +- `components/json-viewer/JsonArray.ts -> components/json-viewer/JsonValue.ts -> components/json-viewer/JsonArray.ts` +- `composables/usePageCapture.example.ts -> composables/usePageCapture.example.ts` + diff --git a/docs/architecture/baselines/test-failure-groups.md b/docs/architecture/baselines/test-failure-groups.md new file mode 100644 index 000000000..0ef19eaaf --- /dev/null +++ b/docs/architecture/baselines/test-failure-groups.md @@ -0,0 +1,38 @@ +# Test Failure Groups + +Baseline captured on `2026-04-03`. + +## 真实行为回归 / 契约漂移 + +- `test/main/presenter/agentSessionPresenter/integration.test.ts` + - `configPresenter.getAgentType()` mock 契约缺失暴露了会话编排对配置查询的硬依赖。 +- `test/main/presenter/floatingButtonPresenter/*.test.ts` + - 布局断言和当前窗口吸边行为不一致。 +- `test/main/presenter/skillSyncPresenter/*.test.ts` + - Cursor format / conversion warnings 的行为与测试契约漂移。 +- `test/renderer/stores/sessionStore.test.ts` + - sidebar group 逻辑对 `sessionKind` 缺省值不兼容。 +- `test/renderer/composables/useModelCapabilities.test.ts` + - search capability 返回值未对齐测试预期。 + +## 测试陈旧 / 遗留测试未跟上实现 + +- `test/main/presenter/mcpClient.test.ts` + - 仍然断言旧的 runtime command translation 细节。 +- `test/main/presenter/agentSessionPresenter/messageManager.test.ts` + - 仍然调用已不再暴露的方法。 +- `test/renderer/composables/useSearchConfig.test.ts` + - 测试存在,但实现文件缺失。 +- `test/renderer/components/MermaidArtifact.test.ts` + - 组件结构与测试查询方式不再匹配。 +- 多个 renderer store test 的 `pinia` mock + - 当前 mock 方式污染 `setActivePinia/createPinia`。 + +## 环境问题 + +- `test/main/presenter/SyncPresenter.test.ts` + - `better-sqlite3-multiple-ciphers` 二进制与当前 Node ABI 不匹配。 +- `test/main/presenter/llmProviderPresenter.test.ts` + - 用例依赖超时 / 网络模拟不稳定。 +- 若干 renderer test 中的 `jsdom` navigation not implemented + - 不是业务行为错误,而是测试环境能力限制。 diff --git a/docs/architecture/baselines/zero-inbound-candidates.md b/docs/architecture/baselines/zero-inbound-candidates.md new file mode 100644 index 000000000..907b5d066 --- /dev/null +++ b/docs/architecture/baselines/zero-inbound-candidates.md @@ -0,0 +1,76 @@ +# Zero Inbound Candidates + +Generated on 2026-04-03. + +These files have no in-repo importers inside their scope and need manual classification before deletion. + +## main + +- Candidate count: 16 + +- `env.d.ts` +- `lib/system.ts` +- `lib/terminalHelper.ts` +- `presenter/browser/BrowserContextBuilder.ts` +- `presenter/configPresenter/aes.ts` +- `presenter/llmProviderPresenter/oauthHelper.ts` +- `presenter/llmProviderPresenter/providers/openAIProvider.ts` +- `presenter/mcpPresenter/agentMcpFilter.ts` +- `presenter/searchPrompts/searchPrompts.ts` +- `presenter/sessionPresenter/events.ts` +- `presenter/sessionPresenter/persistence/conversationPersister.ts` +- `presenter/sessionPresenter/persistence/messagePersister.ts` +- `presenter/sessionPresenter/tab/tabAdapter.ts` +- `presenter/sessionPresenter/types.ts` +- `presenter/sqlitePresenter/tables/attachments.ts` +- `presenter/workspacePresenter/fileCache.ts` + +## renderer + +- Candidate count: 44 + +- `components/ChatConfig.vue` +- `components/ChatConfig/ConfigSwitchField.vue` +- `components/FileItem.vue` +- `components/ModelSelect.vue` +- `components/ScrollablePopover.vue` +- `components/artifacts/ArtifactBlock.vue` +- `components/chat-input/SkillsIndicator.vue` +- `components/chat-input/VoiceCallWidget.vue` +- `components/chat-input/components/ToolbarButton.vue` +- `components/chat-input/composables/useAgentMcpData.ts` +- `components/chat-input/composables/useContextLength.ts` +- `components/chat-input/composables/useDragAndDrop.ts` +- `components/chat-input/composables/useInputHistory.ts` +- `components/chat-input/composables/useInputSettings.ts` +- `components/chat-input/composables/usePromptInputFiles.ts` +- `components/chat-input/composables/useRateLimitStatus.ts` +- `components/editor/mention/PromptParamsDialog.vue` +- `components/editor/mention/mention.ts` +- `components/editor/mention/slashMention.ts` +- `components/mcp-config/AgentMcpSelector.vue` +- `components/message/MessageActionButtons.vue` +- `components/message/MessageItemPlaceholder.vue` +- `components/message/ReferencePreview.vue` +- `components/settings/ModelConfigItem.vue` +- `composables/message/useMessageScroll.ts` +- `composables/useArtifactCodeEditor.ts` +- `composables/useArtifactContext.ts` +- `composables/useArtifactExport.ts` +- `composables/useArtifactViewMode.ts` +- `composables/useSearchConfig.ts` +- `composables/useViewportSize.ts` +- `env.d.ts` +- `lib/float.cursor.ts` +- `lib/gemini.ts` +- `lib/sanitizeText.ts` +- `main.ts` +- `stores/floatingButton.ts` +- `stores/prompts.ts` +- `stores/providerDeeplinkImport.ts` +- `stores/shortcutKey.ts` +- `stores/sync.ts` +- `stores/systemPromptStore.ts` +- `utils/maxOutputTokens.ts` +- `views/SettingsTabView.vue` + diff --git a/docs/architecture/new-ui-implementation-plan.md b/docs/architecture/new-ui-implementation-plan.md index bab6f5058..b03c83387 100644 --- a/docs/architecture/new-ui-implementation-plan.md +++ b/docs/architecture/new-ui-implementation-plan.md @@ -556,8 +556,8 @@ sessionStore.selectSession(id) - `chat.ts`: Remove session management logic, keep message logic **Deprecated**: -- `components/mock/*.vue`: Archived in `archives/code/dead-code-batch-2/` -- `composables/useMockViewState.ts`: Archived in `archives/code/dead-code-batch-2/` +- `components/mock/*.vue`: Removed after the new UI rollout, only historical docs remain +- `composables/useMockViewState.ts`: Removed after stores took over the state flow --- diff --git a/docs/architecture/session-management.md b/docs/architecture/session-management.md index c07da96e4..2598678b5 100644 --- a/docs/architecture/session-management.md +++ b/docs/architecture/session-management.md @@ -2,18 +2,18 @@ retirement 之后,会话管理被明确拆成两层: -- 活跃聊天层:`newAgentPresenter` + `NewSessionManager` +- 活跃聊天层:`agentSessionPresenter` + `NewSessionManager` - 兼容数据层:`SessionPresenter` ## 当前职责边界 | 组件 | 位置 | 当前职责 | | --- | --- | --- | -| `NewAgentPresenter` | `src/main/presenter/newAgentPresenter/index.ts` | renderer 唯一 session 入口 | -| `NewSessionManager` | `src/main/presenter/newAgentPresenter/sessionManager.ts` | `new_sessions` 记录、窗口绑定、session CRUD | -| `NewMessageManager` | `src/main/presenter/newAgentPresenter/messageManager.ts` | 新会话消息读取与 agent routing | -| `DeepChatSessionStore` | `src/main/presenter/deepchatAgentPresenter/sessionStore.ts` | 活跃 runtime 状态 | -| `DeepChatMessageStore` | `src/main/presenter/deepchatAgentPresenter/messageStore.ts` | 新消息持久化 | +| `AgentSessionPresenter` | `src/main/presenter/agentSessionPresenter/index.ts` | renderer 唯一 session 入口 | +| `NewSessionManager` | `src/main/presenter/agentSessionPresenter/sessionManager.ts` | `new_sessions` 记录、窗口绑定、session CRUD | +| `NewMessageManager` | `src/main/presenter/agentSessionPresenter/messageManager.ts` | 新会话消息读取与 agent routing | +| `DeepChatSessionStore` | `src/main/presenter/agentRuntimePresenter/sessionStore.ts` | 活跃 runtime 状态 | +| `DeepChatMessageStore` | `src/main/presenter/agentRuntimePresenter/messageStore.ts` | 新消息持久化 | | `SessionPresenter` | `src/main/presenter/sessionPresenter/index.ts` | legacy conversation/thread/export 兼容层 | | `sessionPresenter/messageFormatter.ts` | `src/main/presenter/sessionPresenter/messageFormatter.ts` | 用户消息上下文格式化与 exporter 复用 | @@ -22,9 +22,9 @@ retirement 之后,会话管理被明确拆成两层: ```mermaid sequenceDiagram participant R as Renderer - participant N as NewAgentPresenter + participant N as AgentSessionPresenter participant S as NewSessionManager - participant D as DeepChatAgentPresenter + participant D as AgentRuntimePresenter R->>N: createSession() N->>S: create() @@ -70,4 +70,4 @@ sequenceDiagram - 维护 exporter 使用的用户消息归一化 如果是当前聊天会话创建、发送消息、取消生成、tool interaction,请直接从 -`newAgentPresenter` 和 `deepchatAgentPresenter` 开始读。 +`agentSessionPresenter` 和 `agentRuntimePresenter` 开始读。 diff --git a/docs/architecture/tool-system.md b/docs/architecture/tool-system.md index f5d05846a..e6730f2e1 100644 --- a/docs/architecture/tool-system.md +++ b/docs/architecture/tool-system.md @@ -20,7 +20,7 @@ ```mermaid graph LR - DeepChat["DeepChatAgentPresenter"] --> ToolPresenter["ToolPresenter"] + DeepChat["AgentRuntimePresenter"] --> ToolPresenter["ToolPresenter"] ToolPresenter --> Mapper["ToolMapper"] ToolPresenter --> Mcp["McpPresenter"] ToolPresenter --> AgentTools["AgentToolManager"] @@ -37,14 +37,14 @@ graph LR 2. 从 `AgentToolManager` 拉取本地 agent tools。 3. 用 `ToolMapper` 记录来源,并在重名时优先保留 MCP tool。 -这意味着 `deepchatAgentPresenter` 不需要知道 tool 的真实来源,只需要持有统一的 +这意味着 `agentRuntimePresenter` 不需要知道 tool 的真实来源,只需要持有统一的 `MCPToolDefinition[]`。 ## 调用工具 ```mermaid sequenceDiagram - participant D as DeepChatAgentPresenter + participant D as AgentRuntimePresenter participant T as ToolPresenter participant Map as ToolMapper participant M as MCP tools @@ -76,7 +76,7 @@ port 负责提供: - conversation workdir 解析 - 已批准路径查询 - settings approval 消费 -- `newAgentPresenter` 会话上下文桥接 +- `agentSessionPresenter` 会话上下文桥接 权限能力拆分: diff --git a/docs/archives/legacy-agentpresenter-architecture.md b/docs/archives/legacy-agentpresenter-architecture.md index 4f96c4368..fc641c25f 100644 --- a/docs/archives/legacy-agentpresenter-architecture.md +++ b/docs/archives/legacy-agentpresenter-architecture.md @@ -2,7 +2,7 @@ 本文档从高层视角介绍 DeepChat 的系统架构,帮助开发者快速理解项目结构和组件关系。 -> **Note (2026-03-09):** 本文档描述的是原始 AgentPresenter 架构。新架构(P0 实现)使用 `newAgentPresenter` + `deepchatAgentPresenter` 作为主要入口,详见 [P0 Implementation Summary](./P0_IMPLEMENTATION_SUMMARY.md)。 +> **Note (2026-03-09):** 本文档描述的是原始 AgentPresenter 架构。当前主入口是 `agentSessionPresenter` + `agentRuntimePresenter`(formerly `newAgentPresenter` + `deepchatAgentPresenter`),详见 [P0 Implementation Summary](./P0_IMPLEMENTATION_SUMMARY.md)。 ## 🏗️ 核心组件关系 @@ -118,16 +118,16 @@ graph TB **职责**:管理 Agent Loop、LLM 流式响应、工具调用、权限协调 -| 组件 | 文件位置 | 行数 | 核心职责 | +| 组件 | 历史模块标识 | 行数 | 核心职责 | |------|---------|------|---------| -| AgentPresenter | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/index.ts` | 472 | Agent 编排入口,sendMessage/cancelLoop/continueLoop | -| agentLoopHandler | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts` | 670 | Agent Loop 主循环(while 循环) | -| streamGenerationHandler | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts` | 645 | 流生成协调,准备上下文、启动 Stream | -| loopOrchestrator | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/loopOrchestrator.ts` | ~30 | Loop 状态管理 | -| toolCallProcessor | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/toolCallProcessor.ts` | 445 | 工具调用执行、结果处理 | -| llmEventHandler | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts` | ~400 | 标准化 LLM 事件到内部格式 | -| permissionHandler | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/permission/permissionHandler.ts` | ~600 | 权限请求响应协调 | -| messageBuilder | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageBuilder.ts` | ~285 | 提示词构建、上下文压缩 | +| AgentPresenter | `legacy agent runtime / agentPresenter/index.ts` | 472 | Agent 编排入口,sendMessage/cancelLoop/continueLoop | +| agentLoopHandler | `legacy agent runtime / loop/agentLoopHandler.ts` | 670 | Agent Loop 主循环(while 循环) | +| streamGenerationHandler | `legacy agent runtime / streaming/streamGenerationHandler.ts` | 645 | 流生成协调,准备上下文、启动 Stream | +| loopOrchestrator | `legacy agent runtime / loop/loopOrchestrator.ts` | ~30 | Loop 状态管理 | +| toolCallProcessor | `legacy agent runtime / loop/toolCallProcessor.ts` | 445 | 工具调用执行、结果处理 | +| llmEventHandler | `legacy agent runtime / streaming/llmEventHandler.ts` | ~400 | 标准化 LLM 事件到内部格式 | +| permissionHandler | `legacy agent runtime / permission/permissionHandler.ts` | ~600 | 权限请求响应协调 | +| messageBuilder | `legacy agent runtime / message/messageBuilder.ts` | ~285 | 提示词构建、上下文压缩 | **关键流程**: 1. 用户发送消息 → `AgentPresenter.sendMessage()` @@ -143,13 +143,13 @@ graph TB **职责**:统一管理所有工具(MCP + Agent)、工具名称解析、路由分发 -| 组件 | 文件位置 | 行数 | 核心职责 | +| 组件 | 历史模块标识 | 行数 | 核心职责 | |------|---------|------|---------| | ToolPresenter | `src/main/presenter/toolPresenter/index.ts` | 161 | 统一工具定义接口、工具调用路由 | | ToolMapper | `src/main/presenter/toolPresenter/toolMapper.ts` | ~100 | 工具名→来源映射(mcp/agent) | | McpPresenter | `src/main/presenter/mcpPresenter/index.ts` | ~500 | MCP 服务器管理、工具定义、工具调用 | -| AgentToolManager | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/agentToolManager.ts` | 577 | Agent 文件系统 + Browser 工具 | -| AgentFileSystemHandler | `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/agentFileSystemHandler.ts` | 960 | 文件系统工具实现 | +| AgentToolManager | `legacy agent runtime / acp/agentToolManager.ts` | 577 | Agent 文件系统 + Browser 工具 | +| AgentFileSystemHandler | `legacy agent runtime / acp/agentFileSystemHandler.ts` | 960 | 文件系统工具实现 | **工具来源**: 1. **MCP 工具**:外部 MCP 服务器提供,通过 `McpPresenter` 管理 @@ -291,16 +291,16 @@ const response = await toolPresenter.callTool({ - ConversationManager: `src/main/presenter/sessionPresenter/managers/conversationManager.ts` **Agent 系统**: -- AgentPresenter: `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/index.ts:1-472` -- Agent Loop: `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/loop/agentLoopHandler.ts:1-670` -- Stream Generation: `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts:1-645` -- Message Builder: `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/message/messageBuilder.ts` +- AgentPresenter: `legacy agent runtime / agentPresenter/index.ts` +- Agent Loop: `legacy agent runtime / loop/agentLoopHandler.ts` +- Stream Generation: `legacy agent runtime / streaming/streamGenerationHandler.ts` +- Message Builder: `legacy agent runtime / message/messageBuilder.ts` **工具系统**: - ToolPresenter: `src/main/presenter/toolPresenter/index.ts:1-161` - ToolMapper: `src/main/presenter/toolPresenter/toolMapper.ts` -- AgentToolManager: `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/agentToolManager.ts:1-577` -- AgentFileSystemHandler: `archives/code/legacy-agentpresenter-retirement/src/main/presenter/agentPresenter/acp/agentFileSystemHandler.ts:1-960` +- AgentToolManager: `legacy agent runtime / acp/agentToolManager.ts` +- AgentFileSystemHandler: `legacy agent runtime / acp/agentFileSystemHandler.ts` - McpPresenter: `src/main/presenter/mcpPresenter/index.ts` **事件系统**: diff --git a/docs/archives/legacy-agentpresenter-flows.md b/docs/archives/legacy-agentpresenter-flows.md index baf6d0c46..31a44cff7 100644 --- a/docs/archives/legacy-agentpresenter-flows.md +++ b/docs/archives/legacy-agentpresenter-flows.md @@ -2,7 +2,7 @@ 本文档使用时序图详细描述 DeepChat 的关键业务流程,帮助开发者理解运行时行为。 -> **Note (2026-03-09):** 本文档描述的是原始 AgentPresenter 流程。新架构流程(newAgentPresenter + deepchatAgentPresenter)已实现,核心流程类似但入口不同。详见 [P0 Implementation Summary](./P0_IMPLEMENTATION_SUMMARY.md)。 +> **Note (2026-03-09):** 本文档描述的是原始 AgentPresenter 流程。当前主流程入口是 `agentSessionPresenter` + `agentRuntimePresenter`(formerly `newAgentPresenter` + `deepchatAgentPresenter`);核心流程类似但入口不同。详见 [P0 Implementation Summary](./P0_IMPLEMENTATION_SUMMARY.md)。 ## 1. 发送消息完整流程 diff --git a/docs/guides/code-navigation.md b/docs/guides/code-navigation.md index 61978b062..2c68751cd 100644 --- a/docs/guides/code-navigation.md +++ b/docs/guides/code-navigation.md @@ -7,10 +7,10 @@ 如果你要追主聊天链路,按这个顺序跳: 1. `src/main/presenter/index.ts` -2. `src/main/presenter/newAgentPresenter/index.ts` -3. `src/main/presenter/deepchatAgentPresenter/index.ts` -4. `src/main/presenter/deepchatAgentPresenter/process.ts` -5. `src/main/presenter/deepchatAgentPresenter/dispatch.ts` +2. `src/main/presenter/agentSessionPresenter/index.ts` +3. `src/main/presenter/agentRuntimePresenter/index.ts` +4. `src/main/presenter/agentRuntimePresenter/process.ts` +5. `src/main/presenter/agentRuntimePresenter/dispatch.ts` ## 按功能找代码 @@ -18,30 +18,30 @@ | 功能 | 位置 | 备注 | | --- | --- | --- | -| 创建会话 | `src/main/presenter/newAgentPresenter/index.ts` | `createSession()` | -| ACP draft 会话 | `src/main/presenter/newAgentPresenter/index.ts` | `ensureAcpDraftSession()` | -| session 记录管理 | `src/main/presenter/newAgentPresenter/sessionManager.ts` | `create/get/delete/bindWindow` | -| agent 注册 | `src/main/presenter/newAgentPresenter/agentRegistry.ts` | `register/resolve` | +| 创建会话 | `src/main/presenter/agentSessionPresenter/index.ts` | `createSession()` | +| ACP draft 会话 | `src/main/presenter/agentSessionPresenter/index.ts` | `ensureAcpDraftSession()` | +| session 记录管理 | `src/main/presenter/agentSessionPresenter/sessionManager.ts` | `create/get/delete/bindWindow` | +| agent 注册 | `src/main/presenter/agentSessionPresenter/agentRegistry.ts` | `register/resolve` | ### 消息发送与流式处理 | 功能 | 位置 | 备注 | | --- | --- | --- | -| 发送消息入口 | `src/main/presenter/newAgentPresenter/index.ts` | `sendMessage()` | -| agent runtime 入口 | `src/main/presenter/deepchatAgentPresenter/index.ts` | `processMessage()` | -| 主循环 | `src/main/presenter/deepchatAgentPresenter/process.ts` | stream + tool loop | -| 工具调度 | `src/main/presenter/deepchatAgentPresenter/dispatch.ts` | tool call / paused interaction | -| 上下文构建 | `src/main/presenter/deepchatAgentPresenter/contextBuilder.ts` | history fitting / resume context | -| 流式 echo | `src/main/presenter/deepchatAgentPresenter/echo.ts` | renderer 增量回显 | +| 发送消息入口 | `src/main/presenter/agentSessionPresenter/index.ts` | `sendMessage()` | +| agent runtime 入口 | `src/main/presenter/agentRuntimePresenter/index.ts` | `processMessage()` | +| 主循环 | `src/main/presenter/agentRuntimePresenter/process.ts` | stream + tool loop | +| 工具调度 | `src/main/presenter/agentRuntimePresenter/dispatch.ts` | tool call / paused interaction | +| 上下文构建 | `src/main/presenter/agentRuntimePresenter/contextBuilder.ts` | history fitting / resume context | +| 流式 echo | `src/main/presenter/agentRuntimePresenter/echo.ts` | renderer 增量回显 | ### 消息与状态持久化 | 功能 | 位置 | 备注 | | --- | --- | --- | -| runtime session state | `src/main/presenter/deepchatAgentPresenter/sessionStore.ts` | provider/model/permission/status | -| message persistence | `src/main/presenter/deepchatAgentPresenter/messageStore.ts` | assistant/user message write | -| pending input | `src/main/presenter/deepchatAgentPresenter/pendingInputStore.ts` | 启动后恢复 | -| compaction | `src/main/presenter/deepchatAgentPresenter/compactionService.ts` | 长上下文摘要 | +| runtime session state | `src/main/presenter/agentRuntimePresenter/sessionStore.ts` | provider/model/permission/status | +| message persistence | `src/main/presenter/agentRuntimePresenter/messageStore.ts` | assistant/user message write | +| pending input | `src/main/presenter/agentRuntimePresenter/pendingInputStore.ts` | 启动后恢复 | +| compaction | `src/main/presenter/agentRuntimePresenter/compactionService.ts` | 长上下文摘要 | ### 工具系统 @@ -67,7 +67,7 @@ | 功能 | 位置 | 备注 | | --- | --- | --- | -| legacy import | `src/main/presenter/newAgentPresenter/legacyImportService.ts` | 旧数据导入新表 | +| legacy import | `src/main/presenter/agentSessionPresenter/legacyImportService.ts` | 旧数据导入新表 | | legacy 会话兼容 | `src/main/presenter/sessionPresenter/index.ts` | main 内部 compatibility layer | | 用户消息格式化 | `src/main/presenter/sessionPresenter/messageFormatter.ts` | exporter 复用 | @@ -77,17 +77,17 @@ ```bash rg "createSession\\(" src/main -rg "processMessage\\(" src/main/presenter/deepchatAgentPresenter +rg "processMessage\\(" src/main/presenter/agentRuntimePresenter rg "callTool\\(" src/main/presenter/toolPresenter -rg --files src/main/presenter | rg "newAgentPresenter|deepchatAgentPresenter|agentTools|acp" +rg --files src/main/presenter | rg "agentSessionPresenter|agentRuntimePresenter|agentTools|acp" ``` ## 看到这些词时怎么理解 | 词 | 当前含义 | | --- | --- | -| `newAgentPresenter` | renderer 唯一聊天会话入口 | -| `deepchatAgentPresenter` | 当前聊天 runtime | +| `agentSessionPresenter` | renderer 唯一聊天会话入口 | +| `agentRuntimePresenter` | 当前聊天 runtime | | `SessionPresenter` | legacy conversation 兼容层,不是主链路 | | `agentPresenter` | 已退休;只会出现在 archive 或历史 spec 里 | @@ -104,4 +104,4 @@ rg --files src/main/presenter | rg "newAgentPresenter|deepchatAgentPresenter|age - `docs/archives/legacy-agentpresenter-architecture.md` - `docs/archives/legacy-agentpresenter-flows.md` -- `archives/code/legacy-agentpresenter-retirement/` +- `docs/archives/thread-presenter-migration-plan.md` diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 2f5fc37d0..53cbaafa7 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -37,8 +37,8 @@ pnpm test ```text Renderer - -> newAgentPresenter - -> deepchatAgentPresenter + -> agentSessionPresenter + -> agentRuntimePresenter -> toolPresenter / llmProviderPresenter ``` @@ -51,8 +51,8 @@ Renderer src/ ├── main/ │ ├── presenter/ -│ │ ├── newAgentPresenter/ # 当前会话入口 -│ │ ├── deepchatAgentPresenter/ # 当前聊天 runtime +│ │ ├── agentSessionPresenter/ # 当前会话入口 +│ │ ├── agentRuntimePresenter/ # 当前聊天 runtime │ │ ├── toolPresenter/ # 工具路由 │ │ │ └── agentTools/ # 本地 agent tools │ │ ├── llmProviderPresenter/ # provider 管理 @@ -72,8 +72,8 @@ src/ ## 进入代码的推荐顺序 1. `src/main/presenter/index.ts` -2. `src/main/presenter/newAgentPresenter/index.ts` -3. `src/main/presenter/deepchatAgentPresenter/index.ts` +2. `src/main/presenter/agentSessionPresenter/index.ts` +3. `src/main/presenter/agentRuntimePresenter/index.ts` 4. `src/main/presenter/toolPresenter/index.ts` 5. `src/main/presenter/llmProviderPresenter/index.ts` @@ -83,9 +83,9 @@ src/ 优先看: -- `src/main/presenter/newAgentPresenter/index.ts` -- `src/main/presenter/deepchatAgentPresenter/process.ts` -- `src/main/presenter/deepchatAgentPresenter/dispatch.ts` +- `src/main/presenter/agentSessionPresenter/index.ts` +- `src/main/presenter/agentRuntimePresenter/process.ts` +- `src/main/presenter/agentRuntimePresenter/dispatch.ts` ### 添加或修改 agent tool @@ -110,7 +110,7 @@ src/ 优先看: -- `src/main/presenter/newAgentPresenter/legacyImportService.ts` +- `src/main/presenter/agentSessionPresenter/legacyImportService.ts` - `src/main/presenter/sessionPresenter/index.ts` - `src/main/presenter/exporter/formats/` @@ -137,4 +137,4 @@ node scripts/agent-cleanup-guard.mjs - `docs/archives/legacy-agentpresenter-architecture.md` - `docs/archives/legacy-agentpresenter-flows.md` -- `archives/code/legacy-agentpresenter-retirement/` +- `docs/archives/thread-presenter-migration-plan.md` diff --git a/docs/specs/acp-session-config-options/plan.md b/docs/specs/acp-session-config-options/plan.md index 07f3037c9..cb625505c 100644 --- a/docs/specs/acp-session-config-options/plan.md +++ b/docs/specs/acp-session-config-options/plan.md @@ -82,7 +82,7 @@ UI refinement: ### Phase 3:Presenter 与事件 1. `AcpProvider` 增加 process/session config 读写接口 -2. `LLMProviderPresenter` 和 `NewAgentPresenter` 暴露代理方法 +2. `LLMProviderPresenter` 和 `AgentSessionPresenter` 暴露代理方法 3. 发出 `SESSION_CONFIG_OPTIONS_READY` ### Phase 4:Renderer 状态栏 @@ -98,7 +98,7 @@ UI refinement: 1. `AcpContentMapper` 覆盖 `config_option_update` 2. `AcpProvider.prepareSession` 发出 config-ready 事件 3. `AcpProvider.setSessionConfigOption` 使用 agent 返回的全量 state 回写缓存 -4. `NewAgentPresenter` 覆盖 ACP session config 读写代理 +4. `AgentSessionPresenter` 覆盖 ACP session config 读写代理 ### 4.2 Renderer diff --git a/docs/specs/acp-session-config-options/spec.md b/docs/specs/acp-session-config-options/spec.md index 20478b9ff..124c15d10 100644 --- a/docs/specs/acp-session-config-options/spec.md +++ b/docs/specs/acp-session-config-options/spec.md @@ -65,7 +65,7 @@ - [ ] 新增 `AcpConfigOption`、`AcpConfigOptionValue`、`AcpConfigState` - [ ] `ILlmProviderPresenter` 增加 ACP process/session config 读写接口 -- [ ] `INewAgentPresenter` 增加 ACP session config 读写接口 +- [ ] `IAgentSessionPresenter` 增加 ACP session config 读写接口 - [ ] 新增 renderer 事件 `ACP_WORKSPACE_EVENTS.SESSION_CONFIG_OPTIONS_READY` ## 验收标准 diff --git a/docs/specs/acp-session-config-options/tasks.md b/docs/specs/acp-session-config-options/tasks.md index 95eb0e862..cac639748 100644 --- a/docs/specs/acp-session-config-options/tasks.md +++ b/docs/specs/acp-session-config-options/tasks.md @@ -27,7 +27,7 @@ - [x] `AcpProcessHandle` / `AcpSessionRecord` 缓存统一 config state - [x] 新增 `SESSION_CONFIG_OPTIONS_READY` 事件 - [x] `ILlmProviderPresenter` 增加 ACP process/session config 读写接口 -- [x] `INewAgentPresenter` 增加 ACP session config 读写接口 +- [x] `IAgentSessionPresenter` 增加 ACP session config 读写接口 ## T4 Renderer 状态栏 @@ -41,7 +41,7 @@ - [x] 更新 `acpContentMapper.test.ts` - [x] 更新 `acpProvider.test.ts` -- [x] 更新 `newAgentPresenter.test.ts` +- [x] 更新 `agentSessionPresenter.test.ts` - [x] 更新 `ChatStatusBar.test.ts` ## T6 质量门禁 diff --git a/docs/specs/agent-cleanup/spec.md b/docs/specs/agent-cleanup/spec.md index fc11936e5..99e78ed53 100644 --- a/docs/specs/agent-cleanup/spec.md +++ b/docs/specs/agent-cleanup/spec.md @@ -7,8 +7,8 @@ The cleanup reached final runtime retirement on March 23, 2026. Current primary flow: - renderer active chat pages/stores/components -- `newAgentPresenter` -- `deepchatAgentPresenter` +- `agentSessionPresenter` +- `agentRuntimePresenter` - `toolPresenter` - `llmProviderPresenter` @@ -20,7 +20,7 @@ Completed in this retirement slice: - migrated retained ACP helpers into `src/main/presenter/llmProviderPresenter/acp/` - migrated retained agent tools into `src/main/presenter/toolPresenter/agentTools/` - moved retained user message formatting helpers into `src/main/presenter/sessionPresenter/` -- archived retired source and tests under `archives/code/legacy-agentpresenter-retirement/` +- removed retired source and tests from the active tree, with history preserved in docs ## Compatibility Boundary @@ -49,17 +49,17 @@ It now protects these invariants: - shared helper ownership moved to `src/main/lib/agentRuntime` - active renderer chat path moved off legacy message protocol -- dead renderer and mock/orphan code archived under `archives/code/` +- dead renderer and mock/orphan code removed from the active tree - new-session skills persisted in `new_sessions.active_skills` - legacy `agentPresenter/**` removed from global presenter access - provider-layer MCP global access removed -- final legacy runtime retirement completed and archived +- final legacy runtime retirement completed and documented ## Remaining Backlog The remaining work is no longer runtime-retirement work. It is adjacent cleanup only: -- export-only `@shared/chat` coupling in `newAgentPresenter` +- export-only `@shared/chat` coupling in `agentSessionPresenter` - non-active renderer residual import in `PromptEditorSheet` - adjacent provider globals such as `devicePresenter` / `oauthPresenter` - optional archival/normalization of older specs that still mention retired paths diff --git a/docs/specs/agent-cleanup/tasks.md b/docs/specs/agent-cleanup/tasks.md index e2fcd6148..b2f0b1cb6 100644 --- a/docs/specs/agent-cleanup/tasks.md +++ b/docs/specs/agent-cleanup/tasks.md @@ -5,8 +5,8 @@ - [x] Added cleanup docs and static guardrails - [x] Moved shared runtime helpers out of legacy presenter folders - [x] Moved active renderer chat path off `@shared/chat` -- [x] Archived dead renderer path code in `archives/code/dead-renderer-batch-1/` -- [x] Archived renderer mock/orphan code in `archives/code/dead-code-batch-2/` +- [x] Removed dead renderer path code from the active tree +- [x] Removed renderer mock/orphan code from the active tree - [x] Persisted new-session skills in `new_sessions.active_skills` - [x] Retired old-session skill fallback to legacy conversation settings - [x] Removed global `presenter.*` access from legacy runtime modules @@ -17,7 +17,7 @@ - [x] Migrated retained ACP helpers to `src/main/presenter/llmProviderPresenter/acp/` - [x] Migrated retained agent tools to `src/main/presenter/toolPresenter/agentTools/` - [x] Migrated retained message formatting helper to `src/main/presenter/sessionPresenter/` -- [x] Archived retired source and tests in `archives/code/legacy-agentpresenter-retirement/` +- [x] Removed retired source and tests from the active tree and preserved history in docs - [x] Refreshed active architecture / flow / navigation docs ## Kept Intentionally @@ -30,13 +30,13 @@ ## Remaining Backlog -- [ ] remove export-only `@shared/chat` coupling in `src/main/presenter/newAgentPresenter/index.ts` +- [ ] remove export-only `@shared/chat` coupling in `src/main/presenter/agentSessionPresenter/index.ts` - [ ] remove non-active renderer residual import in `PromptEditorSheet` - [ ] review adjacent provider globals such as `devicePresenter` / `oauthPresenter` - [ ] normalize older historical specs that still mention retired legacy paths -## Archive Batches +## Historical Cleanup Batches -- [x] `archives/code/dead-renderer-batch-1/` -- [x] `archives/code/dead-code-batch-2/` -- [x] `archives/code/legacy-agentpresenter-retirement/` +- [x] dead renderer batch +- [x] mock / orphan UI batch +- [x] legacy agent runtime batch diff --git a/docs/specs/agent-db-legacy-import/plan.md b/docs/specs/agent-db-legacy-import/plan.md index 3159dc53a..78b9ad031 100644 --- a/docs/specs/agent-db-legacy-import/plan.md +++ b/docs/specs/agent-db-legacy-import/plan.md @@ -10,12 +10,12 @@ - `legacy_import_status` 2. Searchresult migration - - `deepchatAgentPresenter/dispatch.ts` 解析 `application/deepchat-webpage` + - `agentRuntimePresenter/dispatch.ts` 解析 `application/deepchat-webpage` - 同时写入: - assistant `search` block - `deepchat_message_search_results` - - `newAgentPresenter.getSearchResults()` 从新表读取 - - 前端引用点改为 `newAgentPresenter.getSearchResults()` + - `agentSessionPresenter.getSearchResults()` 从新表读取 + - 前端引用点改为 `agentSessionPresenter.getSearchResults()` 3. Legacy import pipeline - 新增 `LegacyChatImportService` @@ -25,7 +25,7 @@ - 导入状态与错误写入 `legacy_import_status` 4. Retry and observability - - `newAgentPresenter` 暴露: + - `agentSessionPresenter` 暴露: - `getLegacyImportStatus()` - `retryLegacyImport()` - 失败后允许手动重试,重复导入保持幂等 diff --git a/docs/specs/agent-db-legacy-import/spec.md b/docs/specs/agent-db-legacy-import/spec.md index f5845a23b..7c75a6f15 100644 --- a/docs/specs/agent-db-legacy-import/spec.md +++ b/docs/specs/agent-db-legacy-import/spec.md @@ -13,7 +13,7 @@ - `message_attachments(search_result/search_results)` -> `deepchat_message_search_results` 3. `searchresult` 读写统一走新结构: - 新链路写入:tool 结果中的 `application/deepchat-webpage` - - 新链路读取:`newAgentPresenter.getSearchResults()` + - 新链路读取:`agentSessionPresenter.getSearchResults()` 4. 导入状态持久化与重试: - 新表 `legacy_import_status` - IPC:`getLegacyImportStatus` / `retryLegacyImport` diff --git a/docs/specs/agent-input-advanced-config/plan.md b/docs/specs/agent-input-advanced-config/plan.md index 76cfceea5..be421ee45 100644 --- a/docs/specs/agent-input-advanced-config/plan.md +++ b/docs/specs/agent-input-advanced-config/plan.md @@ -4,7 +4,7 @@ 1. 新增 `SessionGenerationSettings`(共享类型)。 2. `CreateSessionInput` 增加 `generationSettings?: Partial`。 -3. `INewAgentPresenter` 增加: +3. `IAgentSessionPresenter` 增加: - `getSessionGenerationSettings(sessionId)` - `updateSessionGenerationSettings(sessionId, settings)` 4. `IAgentImplementation` 增加可选: @@ -13,9 +13,9 @@ ## 2. 主进程(newAgent + deepchat) -1. `newAgentPresenter.createSession` 透传 `generationSettings` 到 `agent.initSession`。 -2. `newAgentPresenter` 新增 generation settings 读写代理,保持 permission 相关接口不变。 -3. `deepchatAgentPresenter`: +1. `agentSessionPresenter.createSession` 透传 `generationSettings` 到 `agent.initSession`。 +2. `agentSessionPresenter` 新增 generation settings 读写代理,保持 permission 相关接口不变。 +3. `agentRuntimePresenter`: - `initSession` 构造并持久化会话配置(模型默认 + 默认 system prompt + 覆盖值)。 - `processMessage` / `resumeAssistantMessage` 读取会话配置构建上下文。 - `runStreamForMessage` 使用会话 `temperature/maxTokens`,并将 diff --git a/docs/specs/agent-input-advanced-config/tasks.md b/docs/specs/agent-input-advanced-config/tasks.md index 3331854ba..b4234e46f 100644 --- a/docs/specs/agent-input-advanced-config/tasks.md +++ b/docs/specs/agent-input-advanced-config/tasks.md @@ -10,7 +10,7 @@ - [x] 新增 `SessionGenerationSettings` - [x] `CreateSessionInput` 增加 `generationSettings` -- [x] `INewAgentPresenter` 增加 generation settings 读写接口 +- [x] `IAgentSessionPresenter` 增加 generation settings 读写接口 - [x] `IAgentImplementation` 增加可选 generation settings 读写接口 ## T2 持久化层 @@ -50,10 +50,10 @@ ## T7 测试 -- [x] 更新 `deepchatAgentPresenter.test.ts` -- [x] 更新 `newAgentPresenter.test.ts` +- [x] 更新 `agentRuntimePresenter.test.ts` +- [x] 更新 `agentSessionPresenter.test.ts` - [x] 更新 `NewThreadPage.test.ts` -- [x] 更新 `newAgentPresenter/integration.test.ts` 兼容新参数 +- [x] 更新 `agentSessionPresenter/integration.test.ts` 兼容新参数 ## T8 质量门禁 diff --git a/docs/specs/agentpresenter-mvp-replacement/gap-analysis.md b/docs/specs/agentpresenter-mvp-replacement/gap-analysis.md index 08d7882e1..9df5ae5e6 100644 --- a/docs/specs/agentpresenter-mvp-replacement/gap-analysis.md +++ b/docs/specs/agentpresenter-mvp-replacement/gap-analysis.md @@ -2,7 +2,7 @@ ## Executive Summary -This document analyzes the functional gaps between the old architecture (`agentPresenter` + `sessionPresenter` + `chatStore`) and the new architecture (`deepchatAgentPresenter` + `newAgentPresenter` + `sessionStore`/`messageStore`). +This document analyzes the functional gaps between the old architecture (`agentPresenter` + `sessionPresenter` + `chatStore`) and the new architecture (`agentRuntimePresenter` + `agentSessionPresenter` + `sessionStore`/`messageStore`). **Critical Finding**: The new architecture has successfully implemented the core streaming and message persistence infrastructure, but lacks critical functionality in five key areas: @@ -21,7 +21,7 @@ This document analyzes the functional gaps between the old architecture (`agentP ### Current State (New Architecture) **Implemented:** -- ✅ Basic session creation via `newAgentPresenter.createSession()` +- ✅ Basic session creation via `agentSessionPresenter.createSession()` - ✅ Model resolution from default settings (`configPresenter.getDefaultModel()`) - ✅ Preferred model fallback logic - ✅ Project/workspace binding via `projectStore.selectedProject.path` @@ -29,7 +29,7 @@ This document analyzes the functional gaps between the old architecture (`agentP **File References:** - `src/renderer/src/pages/NewThreadPage.vue` (lines 70-112) -- `src/main/presenter/newAgentPresenter/index.ts` (lines 24-60) +- `src/main/presenter/agentSessionPresenter/index.ts` (lines 24-60) ### Missing Functionality @@ -44,7 +44,7 @@ This document analyzes the functional gaps between the old architecture (`agentP **New Architecture:** - `ChatStatusBar.vue` line 91: Shows "Default permissions" as **read-only button** (no dropdown) -- `newAgentPresenter` has no `permissionMode` parameter in `createSession()` +- `agentSessionPresenter` has no `permissionMode` parameter in `createSession()` - `new_sessions` table has no `permission_mode` column **Impact**: Users cannot choose permission level; all sessions default to unknown behavior. @@ -59,7 +59,7 @@ This document analyzes the functional gaps between the old architecture (`agentP **New Architecture:** - No validation in `NewThreadPage.onSubmit()` -- No validation in `newAgentPresenter.createSession()` +- No validation in `agentSessionPresenter.createSession()` #### 1.3 Default Model Loading from Settings @@ -85,7 +85,7 @@ This document analyzes the functional gaps between the old architecture (`agentP - Disable "Full access" if `!projectStore.selectedProject` - Show tooltip: "Bind workspace first to enable Full access" -3. **Update newAgentPresenter.createSession():** +3. **Update agentSessionPresenter.createSession():** ```typescript async createSession(input: { message: string @@ -104,12 +104,12 @@ This document analyzes the functional gaps between the old architecture (`agentP ### Current State (New Architecture) **Implemented:** -- ✅ `deepchatAgentPresenter.process.ts` executes tool calls -- ✅ `deepchatAgentPresenter.dispatch.ts` builds tool conversations +- ✅ `agentRuntimePresenter.process.ts` executes tool calls +- ✅ `agentRuntimePresenter.dispatch.ts` builds tool conversations **File References:** -- `src/main/presenter/deepchatAgentPresenter/process.ts` -- `src/main/presenter/deepchatAgentPresenter/dispatch.ts` +- `src/main/presenter/agentRuntimePresenter/process.ts` +- `src/main/presenter/agentRuntimePresenter/dispatch.ts` ### Missing Functionality @@ -152,7 +152,7 @@ New Flow (BROKEN): - `handlePermissionResponse()` resumed loop with approved tool calls **New Architecture:** -- `deepchatAgentPresenter` has no pause state +- `agentRuntimePresenter` has no pause state - `processStream()` is synchronous - no yield points for user input - No IPC method to resume after permission approval @@ -168,7 +168,7 @@ New Flow (BROKEN): **New Architecture:** - `messageStore` receives `STREAM_EVENTS.RESPONSE` with blocks - `MessageList` component unchanged (should still render blocks) -- **Missing**: No handler to call `newAgentPresenter` permission methods (don't exist yet) +- **Missing**: No handler to call `agentSessionPresenter` permission methods (don't exist yet) #### 2.4 Permission Approval Handler @@ -187,8 +187,8 @@ async handlePermissionResponse( ``` **New Architecture:** -- ❌ No `handlePermissionResponse()` in `newAgentPresenter` -- ❌ No `handlePermissionResponse()` in `deepchatAgentPresenter` +- ❌ No `handlePermissionResponse()` in `agentSessionPresenter` +- ❌ No `handlePermissionResponse()` in `agentRuntimePresenter` #### 2.5 Whitelist Management @@ -260,9 +260,9 @@ NEW ARCHITECTURE (CURRENT - BROKEN): ### Required Implementation -1. **Add permission checker to deepchatAgentPresenter:** +1. **Add permission checker to agentRuntimePresenter:** ```typescript - // deepchatAgentPresenter/index.ts + // agentRuntimePresenter/index.ts private permissionChecker: PermissionChecker constructor(..., toolPresenter: IToolPresenter) { @@ -290,7 +290,7 @@ NEW ARCHITECTURE (CURRENT - BROKEN): 3. **Add IPC methods:** ```typescript - // newAgentPresenter/index.ts + // agentSessionPresenter/index.ts async handlePermissionResponse( sessionId: string, messageId: string, @@ -315,15 +315,15 @@ NEW ARCHITECTURE (CURRENT - BROKEN): **Implemented:** - ✅ `NewSessionManager.create()` - creates session records - ✅ `NewSessionManager.bindWindow()` - binds session to window -- ✅ `newAgentPresenter.createSession()` - full creation flow -- ✅ `newAgentPresenter.activateSession()` - activation -- ✅ `newAgentPresenter.deactivateSession()` - deactivation +- ✅ `agentSessionPresenter.createSession()` - full creation flow +- ✅ `agentSessionPresenter.activateSession()` - activation +- ✅ `agentSessionPresenter.deactivateSession()` - deactivation - ✅ Session status tracking via `runtimeState` Map - ✅ Event emission: `SESSION_EVENTS.ACTIVATED`, `DEACTIVATED`, `STATUS_CHANGED` **File References:** -- `src/main/presenter/newAgentPresenter/sessionManager.ts` -- `src/main/presenter/newAgentPresenter/index.ts` +- `src/main/presenter/agentSessionPresenter/sessionManager.ts` +- `src/main/presenter/agentSessionPresenter/index.ts` - `src/renderer/src/stores/ui/session.ts` ### Missing Functionality @@ -345,7 +345,7 @@ settings: { **New Architecture:** ```typescript -// newAgentPresenter.createSession() +// agentSessionPresenter.createSession() input: { message, projectDir, agentId, providerId, modelId // MISSING: temperature, contextLength, maxTokens, @@ -384,7 +384,7 @@ input: { **Notes:** - `new_sessions` table stores sessions - `sessionStore.fetchSessions()` loads from DB -- State rebuilt from `deepchatAgentPresenter.getSessionState()` +- State rebuilt from `agentRuntimePresenter.getSessionState()` #### 3.5 Session Deletion and Cleanup @@ -464,9 +464,9 @@ createSession → initSession → bindWindow → processMessage → stream - ✅ Frontend `messageStore` listens to events and updates `streamingBlocks` **File References:** -- `src/main/presenter/deepchatAgentPresenter/process.ts` -- `src/main/presenter/deepchatAgentPresenter/accumulator.ts` -- `src/main/presenter/deepchatAgentPresenter/messageStore.ts` +- `src/main/presenter/agentRuntimePresenter/process.ts` +- `src/main/presenter/agentRuntimePresenter/accumulator.ts` +- `src/main/presenter/agentRuntimePresenter/messageStore.ts` - `src/renderer/src/stores/ui/message.ts` ### Missing Functionality @@ -521,7 +521,7 @@ AssistantMessageBlock: **Status**: ✅ Implemented **Notes:** -- Session status set to `'generating'` before stream (deepchatAgentPresenter line 101) +- Session status set to `'generating'` before stream (agentRuntimePresenter line 101) - Status set to `'idle'` or `'error'` after completion (lines 167, 175) - `SESSION_EVENTS.STATUS_CHANGED` emitted - `sessionStore` updates session status @@ -575,8 +575,8 @@ processMessage → create user message → create assistant message - ✅ Error handling for tool failures **File References:** -- `src/main/presenter/deepchatAgentPresenter/process.ts` -- `src/main/presenter/deepchatAgentPresenter/dispatch.ts` +- `src/main/presenter/agentRuntimePresenter/process.ts` +- `src/main/presenter/agentRuntimePresenter/dispatch.ts` ### Missing Functionality @@ -771,7 +771,7 @@ NEW ARCHITECTURE (CURRENT): 2. **Add resume method:** ```typescript - // deepchatAgentPresenter/index.ts + // agentRuntimePresenter/index.ts async resumeAfterPermission( sessionId: string, messageId: string, @@ -809,7 +809,7 @@ NEW ARCHITECTURE (CURRENT): - Pause stream waiting for approval 3. **Permission Response Handler** - - Add `handlePermissionResponse()` to newAgentPresenter + - Add `handlePermissionResponse()` to agentSessionPresenter - Implement resume mechanism after approval - Update session status ('paused' → 'generating') @@ -847,7 +847,7 @@ NEW ARCHITECTURE (CURRENT): 10. **Frontend Permission UI** - Ensure MessageList renders permission blocks - - Connect approve/reject buttons to newAgentPresenter + - Connect approve/reject buttons to agentSessionPresenter - Add "remember" checkbox ### P2: Medium (Nice to Have) @@ -942,11 +942,11 @@ NEW ARCHITECTURE (CURRENT): | Component | File Path | |-----------|-----------| -| NewAgentPresenter | `src/main/presenter/newAgentPresenter/index.ts` | -| DeepChatAgentPresenter | `src/main/presenter/deepchatAgentPresenter/index.ts` | -| ProcessStream | `src/main/presenter/deepchatAgentPresenter/process.ts` | -| Dispatch | `src/main/presenter/deepchatAgentPresenter/dispatch.ts` | -| MessageStore | `src/main/presenter/deepchatAgentPresenter/messageStore.ts` | +| AgentSessionPresenter | `src/main/presenter/agentSessionPresenter/index.ts` | +| AgentRuntimePresenter | `src/main/presenter/agentRuntimePresenter/index.ts` | +| ProcessStream | `src/main/presenter/agentRuntimePresenter/process.ts` | +| Dispatch | `src/main/presenter/agentRuntimePresenter/dispatch.ts` | +| MessageStore | `src/main/presenter/agentRuntimePresenter/messageStore.ts` | | SessionStore (UI) | `src/renderer/src/stores/ui/session.ts` | | MessageStore (UI) | `src/renderer/src/stores/ui/message.ts` | | NewThreadPage | `src/renderer/src/pages/NewThreadPage.vue` | diff --git a/docs/specs/agentpresenter-mvp-replacement/plan.md b/docs/specs/agentpresenter-mvp-replacement/plan.md index 597125e3e..c41dc9d67 100644 --- a/docs/specs/agentpresenter-mvp-replacement/plan.md +++ b/docs/specs/agentpresenter-mvp-replacement/plan.md @@ -2,16 +2,16 @@ ## 1. 当前基线(Updated 2026-02-28) -1. 新旧双栈并存:`newAgentPresenter/deepchatAgentPresenter` 与旧 `sessionPresenter/useChatStore` 同时存在。 +1. 新旧双栈并存:`agentSessionPresenter/agentRuntimePresenter` 与旧 `sessionPresenter/useChatStore` 同时存在。 2. 新 loop 已可运行,**streaming 和 message persistence 已完成**,但**权限流程完全缺失**。 3. 产品方向已确定:MVP 先替换核心能力,再完成 chat 模式彻底移除。 -4. **关键发现**:`deepchatAgentPresenter/dispatch.ts` 的 `executeTools()` 直接调用工具,**无任何权限检查**。 +4. **关键发现**:`agentRuntimePresenter/dispatch.ts` 的 `executeTools()` 直接调用工具,**无任何权限检查**。 ## 2. 核心架构决策 1. 会话真源:`new_sessions + deepchat_sessions`。 2. 消息真源:`deepchat_messages`。 -3. 主执行链路:`newAgentPresenter -> deepchatAgentPresenter`。 +3. 主执行链路:`agentSessionPresenter -> agentRuntimePresenter`。 4. 新 UI 页面不再依赖 `useChatStore` 与旧 `sessionPresenter` 主流程。 5. variants 本轮下线,fork 保留为唯一分叉能力。 @@ -65,7 +65,7 @@ ### Phase 0:稳定主链路 1. 清理新 UI 对 `useChatStore` 的依赖点。 -2. active session 查询与事件分发统一到 `newAgentPresenter`。 +2. active session 查询与事件分发统一到 `agentSessionPresenter`。 3. 建立最小回归测试基线。 ### Phase 1:权限 + Workspace(MVP 核心) @@ -94,7 +94,7 @@ ## 6. IPC 与类型面 -1. `INewAgentPresenter` 扩展: +1. `IAgentSessionPresenter` 扩展: - `setSessionPermissionMode` - `editUserMessage` - `retryAssistantMessage` @@ -138,9 +138,9 @@ - ❌ Session configuration: PARTIAL (missing advanced options) **Immediate Next Steps**: -1. Create `PermissionChecker` class in `deepchatAgentPresenter/` +1. Create `PermissionChecker` class in `agentRuntimePresenter/` 2. Modify `executeTools()` in `dispatch.ts` to check permissions before tool calls -3. Add `handlePermissionResponse()` IPC method to `newAgentPresenter` +3. Add `handlePermissionResponse()` IPC method to `agentSessionPresenter` 4. Update `ChatStatusBar.vue` to show permission mode dropdown 5. Add `permission_mode` column to `new_sessions` table 6. Create `permission_whitelists` table for session-scoped whitelists diff --git a/docs/specs/agentpresenter-mvp-replacement/spec.md b/docs/specs/agentpresenter-mvp-replacement/spec.md index 7ae21bdd2..e09c7005a 100644 --- a/docs/specs/agentpresenter-mvp-replacement/spec.md +++ b/docs/specs/agentpresenter-mvp-replacement/spec.md @@ -2,7 +2,7 @@ ## 概述 -以 `deepchatAgentPresenter` 新 loop 为唯一核心,分阶段替换旧 chat 体系。MVP 先完成权限、workspace 绑定、消息编辑、retry/regenerate、fork 五个核心能力,再推进设置收敛与 chat 模式清理。 +以 `agentRuntimePresenter` 新 loop 为唯一核心,分阶段替换旧 chat 体系。MVP 先完成权限、workspace 绑定、消息编辑、retry/regenerate、fork 五个核心能力,再推进设置收敛与 chat 模式清理。 ## 背景与目标 @@ -61,8 +61,8 @@ - [x] 权限判定与消息归属基于同一 `sessionId`。 **Implementation Notes** (added 2026-02-28): -- `newAgentPresenter.createSession()` already passes `projectDir` to session manager -- `deepchatAgentPresenter.processStream()` uses `sessionId` throughout +- `agentSessionPresenter.createSession()` already passes `projectDir` to session manager +- `agentRuntimePresenter.processStream()` uses `sessionId` throughout - CRITICAL GAP: `executeTools()` in dispatch.ts does NOT check permissions before calling tools - Must add `PermissionChecker` class and integrate before tool execution @@ -115,7 +115,7 @@ **Implementation Notes** (added 2026-02-28): - PARTIALLY IMPLEMENTED: new architecture uses agent defaults - MISSING: session-level configuration (temperature, contextLength, maxTokens) -- `newAgentPresenter.createSession()` only accepts providerId/modelId +- `agentSessionPresenter.createSession()` only accepts providerId/modelId - Must extend CreateSessionInput to include all config options - Old architecture: CONVERSATION_SETTINGS had 12+ fields - New architecture: must decide which settings to persist at session level @@ -123,7 +123,7 @@ ### G. 架构替换 - [x] 新 UI 主链路不依赖 `useChatStore` 与旧 `sessionPresenter`。 -- [x] `newAgentPresenter + deepchatAgentPresenter` 成为唯一主执行链路。 +- [x] `agentSessionPresenter + agentRuntimePresenter` 成为唯一主执行链路。 **Implementation Notes** (added 2026-02-28): - MOSTLY COMPLETE: New UI uses sessionStore/messageStore diff --git a/docs/specs/agentpresenter-mvp-replacement/tasks.md b/docs/specs/agentpresenter-mvp-replacement/tasks.md index 8ce4e848a..0a51de678 100644 --- a/docs/specs/agentpresenter-mvp-replacement/tasks.md +++ b/docs/specs/agentpresenter-mvp-replacement/tasks.md @@ -16,7 +16,7 @@ - **SQL**: `ALTER TABLE new_sessions ADD COLUMN permission_mode TEXT DEFAULT 'default'` - **File**: `src/main/presenter/sqlitePresenter/tables/newSessionsTable.ts` - [ ] session manager 增加读写 `permission_mode` 能力。 - - **File**: `src/main/presenter/newAgentPresenter/sessionManager.ts` + - **File**: `src/main/presenter/agentSessionPresenter/sessionManager.ts` - Add `permissionMode` to `create()` method - Add getter/setter methods - [ ] 补齐迁移与回填策略测试。 @@ -38,7 +38,7 @@ - [ ] `Full access` 禁用态提示“先绑定 workspace"。 - Add tooltip on disabled option - [ ] 选择结果写回 session 并可恢复。 - - Call `newAgentPresenter.updateSessionPermissionMode()` (NEW IPC) + - Call `agentSessionPresenter.updateSessionPermissionMode()` (NEW IPC) - Load on session activation **Priority**: P0 - MVP Blocker @@ -49,13 +49,13 @@ ## T3 Default 权限流程 🔴 P0 CRITICAL - [ ] 新链路接入权限请求消息块与审批动作。 - - **File**: `src/main/presenter/deepchatAgentPresenter/dispatch.ts` + - **File**: `src/main/presenter/agentRuntimePresenter/dispatch.ts` - Modify `executeTools()` to check permissions BEFORE calling tools - Create permission request block: `{ type: 'action', action_type: 'tool_call_permission', ... }` - Emit `STREAM_EVENTS.RESPONSE` with permission block - PAUSE stream processing (set session status to 'paused') - [ ] 实现 session 级白名单存储与查询。 - - **CREATE**: `src/main/presenter/deepchatAgentPresenter/permissionChecker.ts` + - **CREATE**: `src/main/presenter/agentRuntimePresenter/permissionChecker.ts` - Create `permission_whitelists` table: `{ sessionId, toolName, pathPattern, permissionType, createdAt }` - Query: `SELECT * FROM permission_whitelists WHERE sessionId = ? AND toolName = ?` - [ ] 白名单匹配规则为 `toolName + pathPattern`。 @@ -71,14 +71,14 @@ **Status**: NOT STARTED **Dependencies**: T1 **Key Files**: -- CREATE: `src/main/presenter/deepchatAgentPresenter/permissionChecker.ts` -- MODIFY: `src/main/presenter/deepchatAgentPresenter/dispatch.ts` -- MODIFY: `src/main/presenter/newAgentPresenter/index.ts` +- CREATE: `src/main/presenter/agentRuntimePresenter/permissionChecker.ts` +- MODIFY: `src/main/presenter/agentRuntimePresenter/dispatch.ts` +- MODIFY: `src/main/presenter/agentSessionPresenter/index.ts` ## T4 Full access 边界控制 🔴 P0 CRITICAL - [ ] 实现自动通过逻辑(仅对 `projectDir` 内操作)。 - - **File**: `src/main/presenter/deepchatAgentPresenter/permissionChecker.ts` + - **File**: `src/main/presenter/agentRuntimePresenter/permissionChecker.ts` - Check `session.permission_mode === 'full'` - If full access: auto-approve tools within projectDir - [ ] 实现路径归一化与越界检测。 @@ -99,9 +99,9 @@ ## T5 Workspace 与会话绑定 ✅ P0 COMPLETE - [x] 工具执行上下文绑定 `session.projectDir`。 - - **Status**: COMPLETE - `newAgentPresenter.createSession()` passes projectDir + - **Status**: COMPLETE - `agentSessionPresenter.createSession()` passes projectDir - [x] 统一传递 `conversationId = sessionId`。 - - **Status**: COMPLETE - `deepchatAgentPresenter.processStream()` uses sessionId throughout + - **Status**: COMPLETE - `agentRuntimePresenter.processStream()` uses sessionId throughout - [x] 权限与消息归属链路统一按 `sessionId` 路由。 - **Status**: COMPLETE - All new architecture uses sessionId @@ -112,11 +112,11 @@ ## T6 编辑历史 user 消息 🟡 P1 HIGH - [ ] 实现 `editUserMessage(sessionId, messageId, newContent)`。 - - **File**: `src/main/presenter/newAgentPresenter/index.ts` + - **File**: `src/main/presenter/agentSessionPresenter/index.ts` - **IPC**: Add `editUserMessage(sessionId, messageId, newContent)` method - Validate: only user messages can be edited - [ ] 执行"编辑点后消息截断"。 - - **File**: `src/main/presenter/deepchatAgentPresenter/messageStore.ts` + - **File**: `src/main/presenter/agentRuntimePresenter/messageStore.ts` - Add `deleteMessagesAfter(messageId)` method - Delete all messages with `orderSeq > editedMessage.orderSeq` - [ ] 自动触发 regenerate 并同步状态。 @@ -140,7 +140,7 @@ - Old: `agentPresenter.retryMessage()` created variants - New: Must create NEW assistant message (not replace) - [ ] 实现 retry/regenerate 追加 assistant 消息。 - - **File**: `src/main/presenter/newAgentPresenter/index.ts` + - **File**: `src/main/presenter/agentSessionPresenter/index.ts` - **IPC**: Add `retryMessage(sessionId, messageId)` method - Find the assistant message by messageId - Create new assistant message with same context @@ -162,7 +162,7 @@ ## T8 Fork 🟡 P1 HIGH - [ ] 实现 `forkSessionFromMessage(sessionId, messageId)`。 - - **File**: `src/main/presenter/newAgentPresenter/index.ts` + - **File**: `src/main/presenter/agentSessionPresenter/index.ts` - **IPC**: Add `forkSessionFromMessage(sessionId, messageId)` method - Get all messages up to and including messageId - Create new session with copied messages @@ -171,7 +171,7 @@ - Include the assistant message at fork point - New session can continue from that point - [ ] fork 后新 session 可继续发送与生成。 - - Initialize new session with deepchatAgentPresenter + - Initialize new session with agentRuntimePresenter - Enable sending messages and generating responses - [ ] 补齐 fork 前后消息隔离测试。 - Test: fork creates independent session @@ -191,7 +191,7 @@ - Remove conversation settings UI from ChatPage - Migrate to session-level config - [ ] 将 agent 默认配置下沉到 session 存储。 - - **File**: `src/main/presenter/newAgentPresenter/index.ts` + - **File**: `src/main/presenter/agentSessionPresenter/index.ts` - Extend `CreateSessionInput` to include: - `temperature?: number` - `contextLength?: number` diff --git a/docs/specs/app-spotlight-search/plan.md b/docs/specs/app-spotlight-search/plan.md index cd701d07e..1affdf968 100644 --- a/docs/specs/app-spotlight-search/plan.md +++ b/docs/specs/app-spotlight-search/plan.md @@ -142,7 +142,7 @@ Spotlight 作为 follow-up feature 分两层: 新增 Presenter 接口: -- `INewAgentPresenter.searchHistory(query, options?)` +- `IAgentSessionPresenter.searchHistory(query, options?)` 说明: diff --git a/docs/specs/app-spotlight-search/tasks.md b/docs/specs/app-spotlight-search/tasks.md index 156b76444..4973f3809 100644 --- a/docs/specs/app-spotlight-search/tasks.md +++ b/docs/specs/app-spotlight-search/tasks.md @@ -28,7 +28,7 @@ - [ ] 新增 `HistorySearchOptions` - [ ] 新增 `HistorySearchHit / SessionHit / MessageHit` -- [ ] `INewAgentPresenter` 增加 `searchHistory(query, options?)` +- [ ] `IAgentSessionPresenter` 增加 `searchHistory(query, options?)` - [ ] 补充共享类型导出 ## T4 设置导航 registry diff --git a/docs/specs/architecture-simplification/plan.md b/docs/specs/architecture-simplification/plan.md new file mode 100644 index 000000000..7008d31e1 --- /dev/null +++ b/docs/specs/architecture-simplification/plan.md @@ -0,0 +1,37 @@ +# Architecture Simplification Plan + +## Workstreams + +### 1. Quality Baseline + +- 添加 `scripts/generate-architecture-baseline.mjs`,把依赖环、零入边候选、归档引用报告落到 `docs/architecture/baselines/`。 +- 维护一份失败测试分组文档,区分真实行为回归、测试陈旧、环境问题。 +- 把这批基线纳入文档入口,后续每轮治理都基于同一套报告更新。 + +### 2. Main Composition / Lifecycle + +- 新增 `ConfigQueryPort`、`SessionRuntimePort`、`WindowRoutingPort` 等窄接口。 +- 让 `Presenter` 只在 composition root 组装 port;`agentSessionPresenter`、`agentRuntimePresenter` 只依赖 port。 +- 首批迁移目标: + - 会话 UI 刷新 + - 权限批准 / 清理 + - model catalog 查询 + +### 3. Renderer Transport / State + +- 增加 window context helper,避免各处各自读 `window.api`。 +- 增加 IPC subscription helper,把 `App.vue`、`stores/ui/session.ts`、`stores/ui/message.ts` 的监听注册收口。 +- 把流式状态拆到独立 `stream` store,消息缓存仍保留在 `message` store。 + +### 4. Archive / Docs + +- 首先把开发者导航从历史源码快照切到稳定历史文档。 +- 在文档脱钩和 guard 到位后删除历史源码实体。 + +## Validation + +- `pnpm run format` +- `pnpm run i18n` +- `pnpm run lint` +- `pnpm run typecheck` +- 针对本次改动的主链路测试与 composable/store 测试 diff --git a/docs/specs/architecture-simplification/spec.md b/docs/specs/architecture-simplification/spec.md new file mode 100644 index 000000000..e18998936 --- /dev/null +++ b/docs/specs/architecture-simplification/spec.md @@ -0,0 +1,40 @@ +# Architecture Simplification + +## Summary + +本规格定义首期“整体减负治理”的目标:不引入新功能,优先降低 `main` / `renderer` 的心智负担,清理无用代码,减少隐式调用链,并把生命周期、边界与兼容层显式化。 + +首期只做四类工作: + +- 基线收敛 +- 分层与依赖收口 +- 历史归档脱钩 +- 测试可信度恢复 + +## Goals + +- 为 `main` 和 `renderer` 建立可持续更新的结构基线,而不是一次性口头结论。 +- 限制活跃路径继续依赖全局 `presenter`、分散 IPC 监听和历史归档代码。 +- 让 `agentSessionPresenter`、`agentRuntimePresenter`、`App.vue`、`stores/ui/*` 的职责边界更清楚。 +- 为后续删除历史源码归档目录和死文件清理建立前置条件。 + +## Non-Goals + +- 不做新功能。 +- 不在首期重命名对外 IPC channel。 +- 不在首期移除 `SessionPresenter`、legacy import、旧表结构这些兼容边界。 +- 不要求一次性解决全部失败测试,只要求把失败分类、责任边界和修复入口理清。 + +## Acceptance Criteria + +- 仓库存在一组可更新的基线报告,至少覆盖依赖环、零入边候选、归档引用、失败测试分组。 +- 活跃主链路新增 anti-regression guard,阻止历史源码重新进入运行时依赖,阻止首批主链路继续回跳全局 `presenter`。 +- `agentSessionPresenter` / `agentRuntimePresenter` 首批关键路径改为依赖窄接口 port,而不是直接 import `presenter/index.ts`。 +- `renderer` 至少将 `App`、`session`、`message` 这条活跃链路的 IPC 监听收口到 helper / subscription hub,并把流式状态独立成单独状态面。 +- 文档中存在本次治理的 `spec.md`、`plan.md`、`tasks.md`,并且 `docs/README.md` 能导航到这些内容。 + +## Constraints + +- 行为兼容优先,优先保留用户可见行为和现有 IPC surface。 +- 只有经过人工分类的“真正死文件”可以删除。 +- 任何 archive 清理都要先完成文档脱钩。 diff --git a/docs/specs/architecture-simplification/tasks.md b/docs/specs/architecture-simplification/tasks.md new file mode 100644 index 000000000..9c1421fad --- /dev/null +++ b/docs/specs/architecture-simplification/tasks.md @@ -0,0 +1,31 @@ +# Architecture Simplification Tasks + +## Baseline + +- [x] 建立 architecture simplification spec / plan / tasks +- [x] 新增依赖 / 死代码 / archive 引用基线生成脚本 +- [x] 新增失败测试分组基线文档 + +## Main + +- [x] 引入窄接口 ports +- [x] 将 `agentSessionPresenter` 的会话 UI 刷新与权限清理改成依赖 port +- [x] 将 `agentRuntimePresenter` 的权限批准与 env prompt model lookup 改成依赖 port +- [x] 让 `Presenter` 成为 port 的唯一组装入口 + +## Renderer + +- [x] 引入 window context helper +- [x] 引入 IPC subscription helper +- [x] 收口 `App.vue`、`session`、`message` 的活跃监听链路 +- [x] 拆出独立 stream state store +- [x] 修复 `session store` 对缺失 `sessionKind` 的兼容分类 +- [x] 补齐 `useSearchConfig` 与 search capability 组合逻辑 + +## Guardrails / Docs + +- [x] 新增 architecture guard +- [x] 将 lint 串上 architecture guard +- [x] 更新 `docs/README.md`、`guides/*`、架构文档的治理入口 +- [x] 清理剩余 specs 对历史源码快照的直接文件级引用 +- [x] 删除历史源码归档实体 diff --git a/docs/specs/floating-agent-widget/plan.md b/docs/specs/floating-agent-widget/plan.md index 45cb02bd5..11034c940 100644 --- a/docs/specs/floating-agent-widget/plan.md +++ b/docs/specs/floating-agent-widget/plan.md @@ -4,7 +4,7 @@ 1. 保留现有独立悬浮窗口入口,但将其内容从“按钮 + 外部悬浮聊天窗入口”收敛为“任务小部件”。 2. 小部件数据由主进程聚合,renderer 只负责展示和交互。 -3. 仅复用 `newAgentPresenter.getSessionList()` 与 `DeepChatAgentPresenter` 的状态事件,不新增独立数据库表。 +3. 仅复用 `agentSessionPresenter.getSessionList()` 与 `AgentRuntimePresenter` 的状态事件,不新增独立数据库表。 4. 点击会话时不再打开旧的 `FloatingChatWindow`,而是唤起主窗口并激活对应会话。 ## 数据模型 @@ -17,7 +17,7 @@ 状态来源: -- 会话列表:`newAgentPresenter.getSessionList({ agentId: 'deepchat' })` +- 会话列表:`agentSessionPresenter.getSessionList({ agentId: 'deepchat' })` - 会话状态:`SessionWithState.status` - 语言:`configPresenter.getLanguage()` @@ -39,8 +39,8 @@ 以下场景统一触发 `floatingButtonPresenter.refreshWidgetState()`: -- `newAgentPresenter` 发出列表更新时 -- `deepchatAgentPresenter` 会话状态变化时 +- `agentSessionPresenter` 发出列表更新时 +- `agentRuntimePresenter` 会话状态变化时 - 标题自动生成完成时 主进程刷新后向悬浮 renderer 发送最新 snapshot。 @@ -49,7 +49,7 @@ 1. 悬浮 renderer 发送 `open-session(sessionId)` 2. 主进程选择目标 chat 窗口;若不存在则创建 -3. 主进程调用 `newAgentPresenter.activateSession(targetWebContentsId, sessionId)` +3. 主进程调用 `agentSessionPresenter.activateSession(targetWebContentsId, sessionId)` 4. 主进程显示并聚焦目标窗口 5. 目标 renderer 收到 `SESSION_EVENTS.ACTIVATED` 后切换到对应 chat route diff --git a/docs/specs/legacy-agentpresenter-retirement/spec.md b/docs/specs/legacy-agentpresenter-retirement/spec.md index 0c9799fa4..b80cd16e7 100644 --- a/docs/specs/legacy-agentpresenter-retirement/spec.md +++ b/docs/specs/legacy-agentpresenter-retirement/spec.md @@ -3,7 +3,7 @@ ## Summary Retire the live legacy `AgentPresenter -> SessionManager -> streaming/permission/loop` runtime -chain and make the current `newAgentPresenter + deepchatAgentPresenter` path the only active chat +chain and make the current `agentSessionPresenter + agentRuntimePresenter` path the only active chat execution flow. ## Scope @@ -16,7 +16,7 @@ In scope: - migrate still-needed ACP helpers into `llmProviderPresenter/acp/` - migrate still-needed agent tools into `toolPresenter/agentTools/` - migrate retained message-formatting helpers into `sessionPresenter/` -- archive retired source and tests +- retire source and tests from the active tree and preserve history in docs - document the retirement in active docs and specs Out of scope: @@ -34,14 +34,12 @@ The boundary kept after retirement: - old `conversations/messages` tables as import-only or export-facing sources - `SessionPresenter` as a main-internal compatibility/data adapter -## Archive Policy +## Historical Preservation Policy -Retired live code must move to: +Retired live code must leave the active tree. -- `archives/code/legacy-agentpresenter-retirement/` - -Only fully retired files go to archive. Files still used by active runtime are migrated into live -modules instead of archived in place. +Historical structure is preserved in `docs/archives/` and related cleanup specs. Files still used by +active runtime are migrated into live modules instead of being kept as dormant source snapshots. ## Acceptance @@ -51,5 +49,5 @@ The retirement is done when: 2. renderer no longer has public `agentPresenter` / `sessionPresenter` dependency 3. `ILlmProviderPresenter.startStreamCompletion()` is removed 4. migrated ACP / agent tool helpers compile and tests pass from their new locations -5. retired source/tests exist only under `archives/code/legacy-agentpresenter-retirement/` +5. retired source/tests no longer exist in the active tree, and their historical shape is preserved in docs 6. active docs describe the current architecture and link historical docs as archive material diff --git a/docs/specs/message-toolbar-actions/plan.md b/docs/specs/message-toolbar-actions/plan.md index 7552d97a7..3525e0062 100644 --- a/docs/specs/message-toolbar-actions/plan.md +++ b/docs/specs/message-toolbar-actions/plan.md @@ -2,7 +2,7 @@ ## 1. 关键决策 -1. 新页面消息操作以 `newAgentPresenter` 为唯一目标接口。 +1. 新页面消息操作以 `agentSessionPresenter` 为唯一目标接口。 2. 新架构下线 variants,不再做版本切换状态维护。 3. 交付拆两阶段: - Phase 1:前端接线、显隐与禁用策略。 @@ -33,7 +33,7 @@ ## 3. 新 API 设计(Phase 2) -在 `INewAgentPresenter` 增加: +在 `IAgentSessionPresenter` 增加: 1. `editUserMessage(sessionId: string, messageId: string, newText: string): Promise` 2. `retryFromUserMessage(sessionId: string, messageId: string): Promise` diff --git a/docs/specs/message-toolbar-actions/spec.md b/docs/specs/message-toolbar-actions/spec.md index 57b94c353..f1d961c8f 100644 --- a/docs/specs/message-toolbar-actions/spec.md +++ b/docs/specs/message-toolbar-actions/spec.md @@ -2,13 +2,13 @@ ## 概述 -本规格定义新架构会话页(`ChatPage + stores/ui/* + newAgentPresenter`)下的 `MessageToolbar` 功能补齐范围,目标是让按钮行为与产品语义一致,避免“可见但不可用/行为不一致”。 +本规格定义新架构会话页(`ChatPage + stores/ui/* + agentSessionPresenter`)下的 `MessageToolbar` 功能补齐范围,目标是让按钮行为与产品语义一致,避免“可见但不可用/行为不一致”。 本规格**不包含 Trace 数据落库设计**,Trace 由独立规格处理:`docs/specs/message-trace-storage/`。 ## 背景与目标 -1. 新 UI 主链路已迁移到 `newAgentPresenter + deepchatAgentPresenter`,但 `MessageToolbar` 仍大量依赖旧 `chatStore/sessionPresenter` 语义。 +1. 新 UI 主链路已迁移到 `agentSessionPresenter + agentRuntimePresenter`,但 `MessageToolbar` 仍大量依赖旧 `chatStore/sessionPresenter` 语义。 2. 当前按钮存在“新旧链路行为不一致”与“有 UI 无闭环”的问题。 3. 需要明确分阶段交付:先修前端行为与禁用策略,再补后端消息操作 API。 @@ -38,7 +38,7 @@ ### A. 架构范围 -- [ ] 新会话页链路仅使用 `stores/ui/session.ts`、`stores/ui/message.ts`、`newAgentPresenter`。 +- [ ] 新会话页链路仅使用 `stores/ui/session.ts`、`stores/ui/message.ts`、`agentSessionPresenter`。 - [ ] 不再依赖旧 `chatStore` 作为新页面消息操作入口。 - [ ] 旧页面链路可保留兼容,但不作为新页面实现基线。 diff --git a/docs/specs/message-toolbar-actions/tasks.md b/docs/specs/message-toolbar-actions/tasks.md index 8ca603caa..dcc04ee75 100644 --- a/docs/specs/message-toolbar-actions/tasks.md +++ b/docs/specs/message-toolbar-actions/tasks.md @@ -16,9 +16,9 @@ ## T2 Phase 2:新后端消息操作 API -- [ ] `INewAgentPresenter` 增加消息操作接口定义 -- [ ] `newAgentPresenter` 增加消息操作代理实现 -- [ ] `deepchatAgentPresenter` 实现编辑/重试/删除/fork 语义 +- [ ] `IAgentSessionPresenter` 增加消息操作接口定义 +- [ ] `agentSessionPresenter` 增加消息操作代理实现 +- [ ] `agentRuntimePresenter` 实现编辑/重试/删除/fork 语义 - [ ] MessageStore/SQLite 层补齐对应读写方法 ## T3 渲染层动作闭环 diff --git a/docs/specs/message-trace-storage/plan.md b/docs/specs/message-trace-storage/plan.md index f897ebaa8..f539e295f 100644 --- a/docs/specs/message-trace-storage/plan.md +++ b/docs/specs/message-trace-storage/plan.md @@ -35,7 +35,7 @@ ### 3.1 Trace 上下文 -在 `deepchatAgentPresenter.runStreamForMessage` 构建 trace context: +在 `agentRuntimePresenter.runStreamForMessage` 构建 trace context: 1. `sessionId` 2. `messageId` @@ -69,7 +69,7 @@ ### 5.1 IPC / Presenter -在 `INewAgentPresenter` 增加: +在 `IAgentSessionPresenter` 增加: 1. `listMessageTraces(messageId: string): Promise` 2. `getMessageTraceCount(messageId: string): Promise`(可选;也可在消息查询时聚合) diff --git a/docs/specs/message-trace-storage/tasks.md b/docs/specs/message-trace-storage/tasks.md index 76e88b1da..a5c8afc31 100644 --- a/docs/specs/message-trace-storage/tasks.md +++ b/docs/specs/message-trace-storage/tasks.md @@ -16,8 +16,8 @@ ## T2 类型与接口 - [x] 新增 `MessageTraceRecord` 共享类型 -- [x] `INewAgentPresenter` 增加 trace 查询接口 -- [x] `newAgentPresenter` 增加 trace 查询代理实现 +- [x] `IAgentSessionPresenter` 增加 trace 查询接口 +- [x] `agentSessionPresenter` 增加 trace 查询代理实现 ## T3 脱敏与截断 diff --git a/docs/specs/new-agent/plan.md b/docs/specs/new-agent/plan.md index 4ad4d7b64..7b0313084 100644 --- a/docs/specs/new-agent/plan.md +++ b/docs/specs/new-agent/plan.md @@ -33,23 +33,23 @@ ## 2. Design Decisions -### 2.1 Naming: `newAgentPresenter` not `agentPresenter` +### 2.1 Naming: `agentSessionPresenter` not `agentPresenter` -The old `agentPresenter` still exists and must keep working. The new one is registered as `newAgentPresenter` in IPresenter to avoid name collision. Renderer calls `usePresenter('newAgentPresenter')`. When old UI is removed, rename to `agentPresenter`. +The old `agentPresenter` still exists and must keep working. The new one is registered as `agentSessionPresenter` in IPresenter to avoid name collision. Renderer calls `usePresenter('agentSessionPresenter')`. When old UI is removed, rename to `agentPresenter`. ### 2.2 Database: new tables in same chat.db No separate DB file. New tables coexist with old tables in `chat.db`: - `new_sessions` — agentPresenter's thin session registry - `new_projects` — project directory history -- `deepchat_sessions` — deepchatAgentPresenter's session config -- `deepchat_messages` — deepchatAgentPresenter's messages +- `deepchat_sessions` — agentRuntimePresenter's session config +- `deepchat_messages` — agentRuntimePresenter's messages Prefix `new_` on `sessions` and `projects` to avoid any conflict with potential future SQLite reserved words or collisions. ### 2.3 Stream handling: reuse LLMCoreStreamEvent, transform to LLMAgentEventData -deepchatAgentPresenter consumes `LLMCoreStreamEvent` from `coreStream()` and: +agentRuntimePresenter consumes `LLMCoreStreamEvent` from `coreStream()` and: 1. Accumulates content into `AssistantMessageBlock[]` structure 2. Persists structured JSON content to `deepchat_messages` (batched DB writes every 600ms) 3. Transforms to `LLMAgentEventData` format with `conversationId` + `eventId` for routing @@ -126,9 +126,9 @@ On stream end, both flush immediately with final content. - `Agent`, `Session`, `SessionStatus`, `CreateSessionInput`, `ChatMessage`, `UserMessageContent`, `AssistantMessageBlock`, `MessageMetadata`, `Project` -**`presenters/new-agent.presenter.d.ts`** — IPC-facing interface: +**`presenters/agent-session.presenter.d.ts`** — IPC-facing interface: -- `INewAgentPresenter` — what the renderer can call: `createSession`, `sendMessage`, `getSessionList`, `getSession`, `getMessages`, `getMessageIds`, `getMessage`, `activateSession`, `deactivateSession`, `getActiveSession`, `getAgents`, `deleteSession` +- `IAgentSessionPresenter` — what the renderer can call: `createSession`, `sendMessage`, `getSessionList`, `getSession`, `getMessages`, `getMessageIds`, `getMessage`, `activateSession`, `deactivateSession`, `getActiveSession`, `getAgents`, `deleteSession` **`presenters/project.presenter.d.ts`** — IPC-facing interface: @@ -182,14 +182,14 @@ On stream end, both flush immediately with final content. Index: `CREATE INDEX idx_deepchat_messages_session ON deepchat_messages(session_id, order_seq)` -### 3.3 agentPresenter (`src/main/presenter/newAgentPresenter/`) +### 3.3 agentPresenter (`src/main/presenter/agentSessionPresenter/`) -**`index.ts`** — main presenter class implementing `INewAgentPresenter` +**`index.ts`** — main presenter class implementing `IAgentSessionPresenter` Owns: - `sessionManager` — thin CRUD over `new_sessions` table - `messageManager` — proxy that resolves agentId then delegates to agent -- `agentRegistry` — `Map`, populated in constructor with deepchatAgentPresenter +- `agentRegistry` — `Map`, populated in constructor with agentRuntimePresenter Methods (IPC-facing): - `createSession(input, webContentsId)` → sessionManager.create() + agent.initSession() + agent.processMessage() + emit ACTIVATED @@ -230,7 +230,7 @@ Event relay: - `resolve(agentId)` → returns `IAgentImplementation` or throws - `getAll()` → returns `Agent[]` list (for v0: just deepchat) -### 3.4 deepchatAgentPresenter (`src/main/presenter/deepchatAgentPresenter/`) +### 3.4 agentRuntimePresenter (`src/main/presenter/agentRuntimePresenter/`) **`index.ts`** — implements `IAgentImplementation` @@ -299,14 +299,14 @@ Methods: 3 touchpoints: -1. Import and add to IPresenter: `newAgentPresenter: INewAgentPresenter`, `projectPresenter: IProjectPresenter` +1. Import and add to IPresenter: `agentSessionPresenter: IAgentSessionPresenter`, `projectPresenter: IProjectPresenter` 2. Add class properties 3. Instantiate in constructor: - - `this.deepchatAgentPresenter = new DeepChatAgentPresenter(this.llmProviderPresenter, this.configPresenter, this.sqlitePresenter)` - - `this.newAgentPresenter = new NewAgentPresenter(this.deepchatAgentPresenter, this.configPresenter, this.sqlitePresenter, this.eventBus)` + - `this.agentRuntimePresenter = new AgentRuntimePresenter(this.llmProviderPresenter, this.configPresenter, this.sqlitePresenter)` + - `this.agentSessionPresenter = new AgentSessionPresenter(this.agentRuntimePresenter, this.configPresenter, this.sqlitePresenter, this.eventBus)` - `this.projectPresenter = new ProjectPresenter(this.sqlitePresenter, this.devicePresenter)` -Note: `deepchatAgentPresenter` is NOT exposed on IPresenter — it's internal. Only `newAgentPresenter` and `projectPresenter` are IPC-accessible. +Note: `agentRuntimePresenter` is NOT exposed on IPresenter — it's internal. Only `agentSessionPresenter` and `projectPresenter` are IPC-accessible. ### 3.7 Events (`src/main/events.ts`) @@ -327,14 +327,14 @@ STREAM_EVENTS reused as-is — same event names, same payload format. All stream **`session.ts`** — rewrite -- Uses `usePresenter('newAgentPresenter')` +- Uses `usePresenter('agentSessionPresenter')` - State: `sessions: Session[]`, `activeSessionId`, `groupMode` - Actions: `fetchSessions()`, `createSession(input)`, `selectSession(id)`, `closeSession()` - Listens to: `SESSION_EVENTS.LIST_UPDATED`, `SESSION_EVENTS.ACTIVATED`, `SESSION_EVENTS.DEACTIVATED`, `SESSION_EVENTS.STATUS_CHANGED` **`message.ts`** — new -- Uses `usePresenter('newAgentPresenter')` +- Uses `usePresenter('agentSessionPresenter')` - State: `messageIds: string[]`, `messageCache: Map`, `isStreaming: boolean`, `streamingBlocks: AssistantMessageBlock[]` - Actions: `loadMessages(sessionId)`, `getMessage(id)` - Listens to: `STREAM_EVENTS.RESPONSE` (update streaming blocks from `LLMAgentEventData`), `STREAM_EVENTS.END` (finalize), `STREAM_EVENTS.ERROR` @@ -342,7 +342,7 @@ STREAM_EVENTS reused as-is — same event names, same payload format. All stream **`agent.ts`** — rewrite -- Uses `usePresenter('newAgentPresenter')` +- Uses `usePresenter('agentSessionPresenter')` - State: `agents: Agent[]`, `selectedAgentId` - Actions: `fetchAgents()`, `selectAgent(id)` @@ -373,7 +373,7 @@ STREAM_EVENTS reused as-is — same event names, same payload format. All stream - `agentRegistry.register/resolve/getAll` — correct routing - `createSession` → calls sessionManager.create + agent.initSession + agent.processMessage -**deepchatAgentPresenter:** +**agentRuntimePresenter:** - `processMessage` → creates user message (JSON content), calls LLM, creates assistant message (JSON blocks) - `streamHandler` — given mock `AsyncGenerator`, verify: block accumulation, batched DB writes at 600ms, renderer flush at 120ms, final flush on stop - `messageStore` — CRUD operations against in-memory SQLite, verify JSON content round-trip @@ -386,7 +386,7 @@ STREAM_EVENTS reused as-is — same event names, same payload format. All stream ### 4.2 Integration Tests -- End-to-end: `newAgentPresenter.createSession()` → verify new_sessions row + deepchat_sessions row + deepchat_messages rows (with valid JSON content) + events emitted with conversationId +- End-to-end: `agentSessionPresenter.createSession()` → verify new_sessions row + deepchat_sessions row + deepchat_messages rows (with valid JSON content) + events emitted with conversationId - Coexistence: old `sessionPresenter.createSession()` still works — old tables unaffected - Crash recovery: insert pending message, reinitialize presenter, verify status changed to error diff --git a/docs/specs/new-agent/spec.md b/docs/specs/new-agent/spec.md index 0e99a155e..9a9f58487 100644 --- a/docs/specs/new-agent/spec.md +++ b/docs/specs/new-agent/spec.md @@ -18,7 +18,7 @@ See [Full-Stack Mismatch Analysis](../../architecture/new-ui-store-presenter-mis 1. **Agent interface protocol** — define the unified contract all agents implement 2. **agentPresenter** — router, thin session registry, event relay -3. **deepchatAgentPresenter** — single-turn chat: message → LLM → streamed response → persist +3. **agentRuntimePresenter** — single-turn chat: message → LLM → streamed response → persist 4. **projectPresenter** — thin project directory CRUD 5. **New DB tables** — `new_sessions`, `new_projects`, `deepchat_sessions`, `deepchat_messages` 6. **New renderer stores** — sessionStore, messageStore, agentStore, projectStore, draftStore @@ -41,14 +41,14 @@ See [Full-Stack Mismatch Analysis](../../architecture/new-ui-store-presenter-mis **Main process:** - `src/shared/types/agent-interface.d.ts` — agent interface protocol - `src/shared/types/chat-types.d.ts` — Agent, Session, Project, CreateSessionInput, ChatMessage, MessageBlock types -- `src/main/presenter/newAgentPresenter/index.ts` — agentPresenter (router) -- `src/main/presenter/newAgentPresenter/sessionManager.ts` — thin session registry -- `src/main/presenter/newAgentPresenter/messageManager.ts` — message proxy -- `src/main/presenter/newAgentPresenter/agentRegistry.ts` — agent discovery + routing -- `src/main/presenter/deepchatAgentPresenter/index.ts` — deepchat agent implementation -- `src/main/presenter/deepchatAgentPresenter/sessionStore.ts` — agent-owned session persistence -- `src/main/presenter/deepchatAgentPresenter/messageStore.ts` — agent-owned message persistence -- `src/main/presenter/deepchatAgentPresenter/streamHandler.ts` — LLM stream → message persistence + event emission +- `src/main/presenter/agentSessionPresenter/index.ts` — agentPresenter (router) +- `src/main/presenter/agentSessionPresenter/sessionManager.ts` — thin session registry +- `src/main/presenter/agentSessionPresenter/messageManager.ts` — message proxy +- `src/main/presenter/agentSessionPresenter/agentRegistry.ts` — agent discovery + routing +- `src/main/presenter/agentRuntimePresenter/index.ts` — deepchat agent implementation +- `src/main/presenter/agentRuntimePresenter/sessionStore.ts` — agent-owned session persistence +- `src/main/presenter/agentRuntimePresenter/messageStore.ts` — agent-owned message persistence +- `src/main/presenter/agentRuntimePresenter/streamHandler.ts` — LLM stream → message persistence + event emission - `src/main/presenter/projectPresenter/index.ts` — project CRUD - `src/main/presenter/sqlitePresenter/tables/` — new table definitions - `src/main/events.ts` — add SESSION_EVENTS @@ -81,13 +81,13 @@ See [Full-Stack Mismatch Analysis](../../architecture/new-ui-store-presenter-mis ### Functional Requirements - [ ] Agent interface protocol defined as TypeScript types in `src/shared/types/` -- [ ] agentPresenter registered in Presenter class, accessible via `usePresenter('newAgentPresenter')` +- [ ] agentPresenter registered in Presenter class, accessible via `usePresenter('agentSessionPresenter')` - [ ] agentPresenter.sessionManager creates session records in new `new_sessions` table - [ ] agentPresenter.agentRegistry returns `[{ id: 'deepchat', name: 'DeepChat', type: 'deepchat', enabled: true }]` -- [ ] agentPresenter routes `sendMessage()` to deepchatAgentPresenter based on session's agentId -- [ ] deepchatAgentPresenter calls LLM provider's `coreStream()` and receives `LLMCoreStreamEvent` stream -- [ ] deepchatAgentPresenter persists user message and assistant message in `deepchat_messages` table with structured JSON content -- [ ] deepchatAgentPresenter emits stream events (response/end/error) via EventBus with `conversationId` for routing +- [ ] agentPresenter routes `sendMessage()` to agentRuntimePresenter based on session's agentId +- [ ] agentRuntimePresenter calls LLM provider's `coreStream()` and receives `LLMCoreStreamEvent` stream +- [ ] agentRuntimePresenter persists user message and assistant message in `deepchat_messages` table with structured JSON content +- [ ] agentRuntimePresenter emits stream events (response/end/error) via EventBus with `conversationId` for routing - [ ] agentPresenter relays all stream events to renderer - [ ] Stream events batched: 120ms flush to renderer, 600ms flush to DB - [ ] On app restart, any messages with `status = 'pending'` are marked as `'error'` (crash recovery) @@ -104,11 +104,11 @@ See [Full-Stack Mismatch Analysis](../../architecture/new-ui-store-presenter-mis - [ ] `pnpm run typecheck` passes - [ ] `pnpm run lint` passes - [ ] `pnpm run format` passes -- [ ] Unit tests for agentPresenter routing, deepchatAgentPresenter stream handling, sessionManager CRUD +- [ ] Unit tests for agentPresenter routing, agentRuntimePresenter stream handling, sessionManager CRUD ## Constraints -- LLM provider HTTP clients are reused directly — deepchatAgentPresenter calls `BaseLLMProvider.coreStream()` and consumes the `AsyncGenerator` +- LLM provider HTTP clients are reused directly — agentRuntimePresenter calls `BaseLLMProvider.coreStream()` and consumes the `AsyncGenerator` - New tables live in the same SQLite database (`chat.db`) alongside old tables — no separate DB file - IPC routing is dynamic (`presenter[name]`) — no route registration needed beyond the 3 touchpoints - v0 sends a single user message and gets a single assistant response — no multi-turn context, no tool use diff --git a/docs/specs/new-agent/tasks.md b/docs/specs/new-agent/tasks.md index 144c9cd32..da2e045ef 100644 --- a/docs/specs/new-agent/tasks.md +++ b/docs/specs/new-agent/tasks.md @@ -3,7 +3,7 @@ ## T0 Shared Types & Events - [x] Create `src/shared/types/agent-interface.d.ts` — `IAgentImplementation`, `Agent`, `Session`, `SessionStatus`, `CreateSessionInput`, `ChatMessageRecord`, `UserMessageContent`, `AssistantMessageBlock`, `MessageMetadata`, `Project` (merged chat-types into this file) -- [x] Create `src/shared/types/presenters/new-agent.presenter.d.ts` — `INewAgentPresenter` interface +- [x] Create `src/shared/types/presenters/agent-session.presenter.d.ts` — `IAgentSessionPresenter` interface - [x] Create `src/shared/types/presenters/project.presenter.d.ts` — `IProjectPresenter` interface - [x] Export new types from `src/shared/types/presenters/index.d.ts` - [x] Add `SESSION_EVENTS` to `src/main/events.ts` (list-updated, activated, deactivated, status-changed) @@ -17,24 +17,24 @@ - [x] Create `src/main/presenter/sqlitePresenter/tables/deepchatMessages.ts` — `deepchat_messages` table with order_seq, JSON content, status (pending/sent/error), is_context_edge, metadata; index on (session_id, order_seq) - [x] Register new tables in `sqlitePresenter/index.ts` (initTables + migrate array) -## T2 deepchatAgentPresenter +## T2 agentRuntimePresenter - [x] Create `messageStore.ts` — CRUD over `deepchat_messages` - [x] Create `sessionStore.ts` — CRUD over `deepchat_sessions` - [x] Create `index.ts` — implements `IAgentImplementation`, wires sessionStore + messageStore + llmProviderPresenter, runs crash recovery on init -- [x] Unit tests: processMessage, recoverPendingMessages (`deepchatAgentPresenter.test.ts`) +- [x] Unit tests: processMessage, recoverPendingMessages (`agentRuntimePresenter.test.ts`) -## T3 agentPresenter (newAgentPresenter) +## T3 agentPresenter (agentSessionPresenter) -- [x] Create `src/main/presenter/newAgentPresenter/agentRegistry.ts` — register/resolve/getAll -- [x] Create `src/main/presenter/newAgentPresenter/sessionManager.ts` — CRUD over `new_sessions`, in-memory window bindings (webContentsId → sessionId) -- [x] Create `src/main/presenter/newAgentPresenter/messageManager.ts` — proxy resolves agentId then delegates to agent -- [x] Create `src/main/presenter/newAgentPresenter/index.ts` — implements `INewAgentPresenter`, wires sessionManager + messageManager + agentRegistry + event relay (all stream events carry conversationId) -- [x] Unit tests: sessionManager CRUD + window bindings (`test/main/presenter/newAgentPresenter/sessionManager.test.ts`) -- [x] Unit tests: agentRegistry register/resolve/getAll/has (`test/main/presenter/newAgentPresenter/agentRegistry.test.ts`) -- [x] Unit tests: messageManager delegation (`test/main/presenter/newAgentPresenter/messageManager.test.ts`) -- [x] Unit tests: createSession → verify sessionManager.create + agent.initSession + agent.processMessage called (`test/main/presenter/newAgentPresenter/newAgentPresenter.test.ts`) -- [x] Unit tests: sendMessage → verify agent routing (`test/main/presenter/newAgentPresenter/newAgentPresenter.test.ts`) +- [x] Create `src/main/presenter/agentSessionPresenter/agentRegistry.ts` — register/resolve/getAll +- [x] Create `src/main/presenter/agentSessionPresenter/sessionManager.ts` — CRUD over `new_sessions`, in-memory window bindings (webContentsId → sessionId) +- [x] Create `src/main/presenter/agentSessionPresenter/messageManager.ts` — proxy resolves agentId then delegates to agent +- [x] Create `src/main/presenter/agentSessionPresenter/index.ts` — implements `IAgentSessionPresenter`, wires sessionManager + messageManager + agentRegistry + event relay (all stream events carry conversationId) +- [x] Unit tests: sessionManager CRUD + window bindings (`test/main/presenter/agentSessionPresenter/sessionManager.test.ts`) +- [x] Unit tests: agentRegistry register/resolve/getAll/has (`test/main/presenter/agentSessionPresenter/agentRegistry.test.ts`) +- [x] Unit tests: messageManager delegation (`test/main/presenter/agentSessionPresenter/messageManager.test.ts`) +- [x] Unit tests: createSession → verify sessionManager.create + agent.initSession + agent.processMessage called (`test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts`) +- [x] Unit tests: sendMessage → verify agent routing (`test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts`) ## T4 projectPresenter @@ -43,15 +43,15 @@ ## T5 Presenter Registration -- [x] Add `INewAgentPresenter` and `IProjectPresenter` to `IPresenter` interface in `src/shared/types/presenters/legacy.presenters.d.ts` +- [x] Add `IAgentSessionPresenter` and `IProjectPresenter` to `IPresenter` interface in `src/shared/types/presenters/legacy.presenters.d.ts` - [x] Add properties and constructor instantiation in `src/main/presenter/index.ts` -- [x] Verify: `usePresenter('newAgentPresenter')` and `usePresenter('projectPresenter')` callable from renderer +- [x] Verify: `usePresenter('agentSessionPresenter')` and `usePresenter('projectPresenter')` callable from renderer ## T6 Renderer Stores -- [x] Rewrite `src/renderer/src/stores/ui/session.ts` — uses `newAgentPresenter`, listens to `SESSION_EVENTS`, uses `webContentsId` for activation -- [x] Create `src/renderer/src/stores/ui/message.ts` — uses `newAgentPresenter`, listens to `STREAM_EVENTS`, filters by conversationId, maintains streamingBlocks as AssistantMessageBlock[] -- [x] Rewrite `src/renderer/src/stores/ui/agent.ts` — uses `newAgentPresenter.getAgents()` +- [x] Rewrite `src/renderer/src/stores/ui/session.ts` — uses `agentSessionPresenter`, listens to `SESSION_EVENTS`, uses `webContentsId` for activation +- [x] Create `src/renderer/src/stores/ui/message.ts` — uses `agentSessionPresenter`, listens to `STREAM_EVENTS`, filters by conversationId, maintains streamingBlocks as AssistantMessageBlock[] +- [x] Rewrite `src/renderer/src/stores/ui/agent.ts` — uses `agentSessionPresenter.getAgents()` - [x] Rewrite `src/renderer/src/stores/ui/project.ts` — uses `projectPresenter` - [x] Create `src/renderer/src/stores/ui/draft.ts` — pre-session config, toCreateInput() @@ -68,8 +68,8 @@ - [x] `pnpm run lint` — passes (0 warnings, 0 errors) - [x] `pnpm run format` — passes - [x] Unit tests: all new modules passing -- [x] Integration test: createSession end-to-end — new_sessions row + deepchat_sessions row + deepchat_messages rows (valid JSON content) + events with conversationId (`test/main/presenter/newAgentPresenter/integration.test.ts`) -- [x] Integration test: crash recovery — insert pending message, reinit, verify status = error (`test/main/presenter/newAgentPresenter/integration.test.ts`) +- [x] Integration test: createSession end-to-end — new_sessions row + deepchat_sessions row + deepchat_messages rows (valid JSON content) + events with conversationId (`test/main/presenter/agentSessionPresenter/integration.test.ts`) +- [x] Integration test: crash recovery — insert pending message, reinit, verify status = error (`test/main/presenter/agentSessionPresenter/integration.test.ts`) - [x] Verify old UI regression: old `sessionPresenter` / `chatStore` still functional — zero impact - [x] Manual verify: run `pnpm run dev`, create session via NewThreadPage, see streamed response @@ -80,7 +80,7 @@ - [x] Create `contextBuilder.ts` — context assembly + truncation - [x] Modify `processMessage` in `index.ts` — wire context builder - [x] Unit tests for context builder (`contextBuilder.test.ts`) -- [x] Update `deepchatAgentPresenter.test.ts` — mock `getDefaultSystemPrompt`, verify multi-turn messages +- [x] Update `agentRuntimePresenter.test.ts` — mock `getDefaultSystemPrompt`, verify multi-turn messages - [x] Update `integration.test.ts` — verify multi-turn flow end-to-end - [x] Quality gate: typecheck, lint, format, tests @@ -108,5 +108,5 @@ - [x] Create `process.ts` — unified `processStream()` loop, single code path for tools and no-tools - [x] Update `index.ts` — replace `handleStream`/`agentLoop` with single `processStream()` call - [x] Delete `streamHandler.ts` and `agentLoop.ts` -- [x] Tests: `throttle.test.ts` (7), `accumulator.test.ts` (14), `echo.test.ts` (5), `dispatch.test.ts` (14), `process.test.ts` (9), updated `deepchatAgentPresenter.test.ts` (19) +- [x] Tests: `throttle.test.ts` (7), `accumulator.test.ts` (14), `echo.test.ts` (5), `dispatch.test.ts` (14), `process.test.ts` (9), updated `agentRuntimePresenter.test.ts` (19) - [x] Quality gate: typecheck, lint, format, 89 tests passing diff --git a/docs/specs/new-agent/v2-spec.md b/docs/specs/new-agent/v2-spec.md index 02c9cf93e..fb79edeb2 100644 --- a/docs/specs/new-agent/v2-spec.md +++ b/docs/specs/new-agent/v2-spec.md @@ -38,7 +38,7 @@ No new DB tables. Changes to existing types: The v2 goals are implemented in the v3 module structure. The original `streamHandler.ts` + `agentLoop.ts` were refactored into five focused modules: ``` -deepchatAgentPresenter/ +agentRuntimePresenter/ index.ts — session lifecycle + single processStream() call process.ts — unified loop: stream → accumulate → echo → dispatch accumulator.ts — accumulate(state, event): pure block mutations diff --git a/docs/specs/new-agent/v3-spec.md b/docs/specs/new-agent/v3-spec.md index f44bb005b..0cc94dec3 100644 --- a/docs/specs/new-agent/v3-spec.md +++ b/docs/specs/new-agent/v3-spec.md @@ -26,7 +26,7 @@ v3 refactored the stream processing into five focused pieces with clear boundari ## Current Structure ``` -deepchatAgentPresenter/ +agentRuntimePresenter/ index.ts — session lifecycle only (init, destroy, getState, cancel, getMessages) process.ts — the loop: stream → accumulate → echo → dispatch accumulator.ts — accumulate(state, event): mutate blocks by event type diff --git a/docs/specs/new-ui-chat-components/spec.md b/docs/specs/new-ui-chat-components/spec.md index 79fd0587d..7217a79bc 100644 --- a/docs/specs/new-ui-chat-components/spec.md +++ b/docs/specs/new-ui-chat-components/spec.md @@ -4,15 +4,15 @@ Chat components handle message display, input, and status configuration during active sessions. Each component's visual design must match its mock counterpart exactly. -## Archived Reference Files +## Historical Reference Map -| Component | Archived Mock File | +| Component | Historical Mock | |-----------|-----------| -| ChatTopBar | `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockTopBar.vue` | -| MessageList | `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockMessageList.vue` | -| InputBox | `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputBox.vue` | -| InputToolbar | `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputToolbar.vue` | -| StatusBar | `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockStatusBar.vue` | +| ChatTopBar | `MockTopBar` | +| MessageList | `MockMessageList` | +| InputBox | `MockInputBox` | +| InputToolbar | `MockInputToolbar` | +| StatusBar | `MockStatusBar` | ## File Locations @@ -29,7 +29,7 @@ src/renderer/src/components/chat/ ## 1. ChatTopBar -**Archived mock reference**: `dead-code-batch-2/.../MockTopBar.vue` +**Historical mock reference**: `MockTopBar` (removed from repo) **Layout**: ``` @@ -57,7 +57,7 @@ interface Props { ## 2. MessageList -**Archived mock reference**: `dead-code-batch-2/.../MockMessageList.vue` +**Historical mock reference**: `MockMessageList` (removed from repo) **Layout**: ``` @@ -96,7 +96,7 @@ Note: The existing `useChatStore` already handles message fetching and caching v ## 3. ChatInputBox -**Archived mock reference**: `dead-code-batch-2/.../MockInputBox.vue` +**Historical mock reference**: `MockInputBox` (removed from repo) **Layout**: ``` @@ -133,7 +133,7 @@ interface Emits { ## 4. ChatInputToolbar -**Archived mock reference**: `dead-code-batch-2/.../MockInputToolbar.vue` +**Historical mock reference**: `MockInputToolbar` (removed from repo) **Layout**: ``` @@ -159,7 +159,7 @@ interface Emits { ## 5. ChatStatusBar -**Archived mock reference**: `dead-code-batch-2/.../MockStatusBar.vue` +**Historical mock reference**: `MockStatusBar` (removed from repo) **Layout**: ``` diff --git a/docs/specs/new-ui-implementation/todo.md b/docs/specs/new-ui-implementation/todo.md index 02e7e6c67..ea6674b93 100644 --- a/docs/specs/new-ui-implementation/todo.md +++ b/docs/specs/new-ui-implementation/todo.md @@ -6,20 +6,20 @@ This document tracks the development progress of new UI feature implementation. **Architecture Design**: [new-ui-implementation-plan.md](../../architecture/new-ui-implementation-plan.md) -## Archived Reference File List +## Historical Reference Map -| Archived Mock File | Purpose | Target Replacement | +| Historical Mock | Purpose | Target Replacement | |-----------|---------|-------------------| -| `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockWelcomePage.vue` | Welcome page | `pages/WelcomePage.vue` | -| `archives/code/dead-code-batch-2/src/renderer/src/components/NewThreadMock.vue` | NewThread page | `pages/NewThreadPage.vue` | -| `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockChatPage.vue` | Chat page | `pages/ChatPage.vue` | -| `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockTopBar.vue` | Top bar | `components/chat/ChatTopBar.vue` | -| `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockMessageList.vue` | Message list | `components/chat/MessageList.vue` | -| `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputBox.vue` | Input box | `components/chat/ChatInputBox.vue` | -| `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockInputToolbar.vue` | Input toolbar | `components/chat/ChatInputToolbar.vue` | -| `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockStatusBar.vue` | Status bar | `components/chat/ChatStatusBar.vue` | +| `MockWelcomePage` | Welcome page | `pages/WelcomePage.vue` | +| `NewThreadMock` | NewThread page | `pages/NewThreadPage.vue` | +| `MockChatPage` | Chat page | `pages/ChatPage.vue` | +| `MockTopBar` | Top bar | `components/chat/ChatTopBar.vue` | +| `MockMessageList` | Message list | `components/chat/MessageList.vue` | +| `MockInputBox` | Input box | `components/chat/ChatInputBox.vue` | +| `MockInputToolbar` | Input toolbar | `components/chat/ChatInputToolbar.vue` | +| `MockStatusBar` | Status bar | `components/chat/ChatStatusBar.vue` | | `components/WindowSideBar.vue` | Sidebar | (refactored in place) | -| `archives/code/dead-code-batch-2/src/renderer/src/composables/useMockViewState.ts` | State management | Replaced by stores | +| `useMockViewState` | State management | Replaced by stores | --- @@ -240,15 +240,15 @@ This document tracks the development progress of new UI feature implementation. ### 5.5 Cleanup -- [x] Archive `components/mock/MockWelcomePage.vue` -> `archives/code/dead-code-batch-2/` -- [x] Archive `components/mock/MockChatPage.vue` -> `archives/code/dead-code-batch-2/` -- [x] Archive `components/mock/MockTopBar.vue` -> `archives/code/dead-code-batch-2/` -- [x] Archive `components/mock/MockMessageList.vue` -> `archives/code/dead-code-batch-2/` -- [x] Archive `components/mock/MockInputBox.vue` -> `archives/code/dead-code-batch-2/` -- [x] Archive `components/mock/MockInputToolbar.vue` -> `archives/code/dead-code-batch-2/` -- [x] Archive `components/mock/MockStatusBar.vue` -> `archives/code/dead-code-batch-2/` -- [x] Archive `components/NewThreadMock.vue` -> `archives/code/dead-code-batch-2/` -- [x] Archive `composables/useMockViewState.ts` -> `archives/code/dead-code-batch-2/` +- [x] Remove `components/mock/MockWelcomePage.vue` from the active tree +- [x] Remove `components/mock/MockChatPage.vue` from the active tree +- [x] Remove `components/mock/MockTopBar.vue` from the active tree +- [x] Remove `components/mock/MockMessageList.vue` from the active tree +- [x] Remove `components/mock/MockInputBox.vue` from the active tree +- [x] Remove `components/mock/MockInputToolbar.vue` from the active tree +- [x] Remove `components/mock/MockStatusBar.vue` from the active tree +- [x] Remove `components/NewThreadMock.vue` from the active tree +- [x] Remove `composables/useMockViewState.ts` from the active tree --- diff --git a/docs/specs/new-ui-pages/spec.md b/docs/specs/new-ui-pages/spec.md index c6f1fcbce..c32903e17 100644 --- a/docs/specs/new-ui-pages/spec.md +++ b/docs/specs/new-ui-pages/spec.md @@ -4,13 +4,13 @@ Three page components driven by the Page Router. No fallback to old ChatView — this is a full replacement. -## Archived Reference Files +## Historical Reference Map -| Page | Archived Mock File | +| Page | Historical Mock | |------|-----------| -| WelcomePage | `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockWelcomePage.vue` | -| NewThreadPage | `archives/code/dead-code-batch-2/src/renderer/src/components/NewThreadMock.vue` | -| ChatPage | `archives/code/dead-code-batch-2/src/renderer/src/components/mock/MockChatPage.vue` | +| WelcomePage | `MockWelcomePage` | +| NewThreadPage | `NewThreadMock` | +| ChatPage | `MockChatPage` | ## File Locations @@ -84,7 +84,7 @@ onMounted(async () => { ## 2. WelcomePage -**Archived mock reference**: `dead-code-batch-2/.../MockWelcomePage.vue` +**Historical mock reference**: `MockWelcomePage` (removed from repo) **Layout**: ``` @@ -133,7 +133,7 @@ onMounted(async () => { ## 3. NewThreadPage -**Archived mock reference**: `dead-code-batch-2/.../NewThreadMock.vue` +**Historical mock reference**: `NewThreadMock` (removed from repo) **Layout**: ``` @@ -188,7 +188,7 @@ const handleSubmit = (message: string) => { ## 4. ChatPage -**Archived mock reference**: `dead-code-batch-2/.../MockChatPage.vue` +**Historical mock reference**: `MockChatPage` (removed from repo) **Props**: ```typescript diff --git a/docs/specs/remote-tool-interactions/plan.md b/docs/specs/remote-tool-interactions/plan.md index 9c758ec0d..096970dd2 100644 --- a/docs/specs/remote-tool-interactions/plan.md +++ b/docs/specs/remote-tool-interactions/plan.md @@ -2,7 +2,7 @@ ## Summary -Implement a structured remote interaction loop for Telegram and Feishu so remote endpoints can resolve paused permission and question interactions without falling back to a generic desktop-only notice. The feature stays inside Electron main and reuses the existing `RemoteConversationRunner`, `RemoteCommandRouter`, `FeishuCommandRouter`, and `newAgentPresenter.respondToolInteraction(...)` flow. +Implement a structured remote interaction loop for Telegram and Feishu so remote endpoints can resolve paused permission and question interactions without falling back to a generic desktop-only notice. The feature stays inside Electron main and reuses the existing `RemoteConversationRunner`, `RemoteCommandRouter`, `FeishuCommandRouter`, and `agentSessionPresenter.respondToolInteraction(...)` flow. ## Goals @@ -30,7 +30,7 @@ Implement a structured remote interaction loop for Telegram and Feishu so remote ## Dependencies - `RemoteConversationSnapshot.pendingInteraction` in `RemoteConversationRunner` -- `newAgentPresenter.respondToolInteraction(...)` +- `agentSessionPresenter.respondToolInteraction(...)` - Existing Telegram outbound edit/send flows in `TelegramPoller` - Existing Feishu outbound text flow extended with card sending in `FeishuRuntime` - In-memory callback/token state in `RemoteBindingStore` diff --git a/docs/specs/settings-dashboard/plan.md b/docs/specs/settings-dashboard/plan.md index 1bc7f2831..09b9113f9 100644 --- a/docs/specs/settings-dashboard/plan.md +++ b/docs/specs/settings-dashboard/plan.md @@ -27,7 +27,7 @@ ## Dashboard Query -- Expose `newAgentPresenter.getUsageDashboard()`. +- Expose `agentSessionPresenter.getUsageDashboard()`. - Aggregate summary, 365-day calendar, provider breakdown, and model breakdown from `deepchat_usage_stats`. ## UI diff --git a/docs/specs/settings-dashboard/tasks.md b/docs/specs/settings-dashboard/tasks.md index b88f42874..fd2e3e02f 100644 --- a/docs/specs/settings-dashboard/tasks.md +++ b/docs/specs/settings-dashboard/tasks.md @@ -3,6 +3,6 @@ 1. Add shared dashboard types and cached token usage plumbing. 2. Add `deepchat_usage_stats` table and wire it into `SQLitePresenter`. 3. Record live usage stats on assistant finalize and error finalize. -4. Implement one-time historical backfill and dashboard query methods in `NewAgentPresenter`. +4. Implement one-time historical backfill and dashboard query methods in `AgentSessionPresenter`. 5. Add settings route, dashboard page, and i18n strings. 6. Add focused main/renderer tests and run format, i18n, and lint. diff --git a/docs/specs/subagent-orchestrator/plan.md b/docs/specs/subagent-orchestrator/plan.md index 439b34356..81986ebbb 100644 --- a/docs/specs/subagent-orchestrator/plan.md +++ b/docs/specs/subagent-orchestrator/plan.md @@ -5,9 +5,9 @@ 当前代码已经具备: 1. `new_sessions` + `deepchat_sessions` 的新会话栈 -2. `NewAgentPresenter` 负责 session 生命周期与 renderer IPC +2. `AgentSessionPresenter` 负责 session 生命周期与 renderer IPC 3. `ToolPresenter -> AgentToolManager` 的 agent tool 路由 -4. `DeepChatAgentPresenter` 的 tool call / permission / question / resume 流 +4. `AgentRuntimePresenter` 的 tool call / permission / question / resume 流 5. renderer 的 `MessageBlockToolCall`、`ChatToolInteractionOverlay`、`WorkspacePanel` 缺的部分是: @@ -41,7 +41,7 @@ 采用 main-only event: 1. `dispatch.flushBlocksToRenderer()` 在 child assistant block 流式刷新时,同时发 main-only event -2. `DeepChatAgentPresenter.setSessionStatus()` 和 `emitMessageRefresh()` 也发 main-only event +2. `AgentRuntimePresenter.setSessionStatus()` 和 `emitMessageRefresh()` 也发 main-only event 3. orchestrator 订阅这些事件,维护自己的内存态 queue 这样避免: @@ -80,7 +80,7 @@ tool 执行时通过 `IToolPresenter.callTool(..., { onProgress })` 回调: 1. 扩展 `DeepChatAgentConfig`、session record、tool progress、presenter interface 2. 更新 `new_sessions` schema / migration / table accessors -3. 扩展 `NewSessionManager` 与 `NewAgentPresenter` 的 create/list/delete API +3. 扩展 `NewSessionManager` 与 `AgentSessionPresenter` 的 create/list/delete API ### Phase 2:Tool Runtime @@ -98,7 +98,7 @@ tool 执行时通过 `IToolPresenter.callTool(..., { onProgress })` 回调: 1. 增加 main-only subagent runtime event 常量 2. `dispatch` 在 child assistant block streaming 时发 event -3. `DeepChatAgentPresenter` 在 message refresh / session status change 时发 event +3. `AgentRuntimePresenter` 在 message refresh / session status change 时发 event 4. `dispatch.executeTools()` 与 `executeDeferredToolCall()` 透传 `onProgress` / `signal` ### Phase 4:Renderer @@ -153,7 +153,7 @@ tool 执行时通过 `IToolPresenter.callTool(..., { onProgress })` 回调: ### 4.4 父删子级联 -在 `NewAgentPresenter.deleteSession()` 递归查 child: +在 `AgentSessionPresenter.deleteSession()` 递归查 child: 1. 父删除时先深度删除所有 child 2. child 单独删除不反查父 @@ -165,7 +165,7 @@ tool 执行时通过 `IToolPresenter.callTool(..., { onProgress })` 回调: 1. SQLite migration / default columns 2. `NewSessionManager` 读写新字段 -3. `NewAgentPresenter.getSessionList()` 过滤与级联删除 +3. `AgentSessionPresenter.getSessionList()` 过滤与级联删除 4. `AgentToolManager` tool gating 5. `subagent_orchestrator` 的 parallel / chain 执行顺序 6. progress snapshot 与 preview 裁剪 diff --git a/docs/specs/subagent-orchestrator/spec.md b/docs/specs/subagent-orchestrator/spec.md index d86aa2e4f..919b7bab3 100644 --- a/docs/specs/subagent-orchestrator/spec.md +++ b/docs/specs/subagent-orchestrator/spec.md @@ -133,7 +133,7 @@ V1 目标是把能力收口为单一 agent tool `subagent_orchestrator`,由 to ### 创建 -tool 内部通过 runtime port 调 `NewAgentPresenter` 创建 child session: +tool 内部通过 runtime port 调 `AgentSessionPresenter` 创建 child session: 1. `self` slot 继承父 `agentId` 2. `agent` slot 使用 `slot.targetAgentId` diff --git a/docs/specs/subagent-orchestrator/tasks.md b/docs/specs/subagent-orchestrator/tasks.md index e6b9e7511..2e07710d0 100644 --- a/docs/specs/subagent-orchestrator/tasks.md +++ b/docs/specs/subagent-orchestrator/tasks.md @@ -3,13 +3,13 @@ ## 1. Shared / Schema 1. 扩展 `DeepChatAgentConfig`、`DeepChatSubagentSlot`、session record、tool progress 类型 -2. 扩展 `INewAgentPresenter` / `IToolPresenter` +2. 扩展 `IAgentSessionPresenter` / `IToolPresenter` 3. 为 `new_sessions` 增加 subagent 相关列与迁移测试 ## 2. Main Session Layer 1. 更新 `NewSessionsTable` / `NewSessionManager` create-get-list-update -2. 更新 `NewAgentPresenter`: +2. 更新 `AgentSessionPresenter`: - create session / detached session 支持 `subagentEnabled` - `getSessionList()` 支持 `includeSubagents` / `parentSessionId` - `setSessionSubagentEnabled()` @@ -28,7 +28,7 @@ 1. 定义 main-only subagent runtime events 2. `dispatch` 发 child block update event -3. `DeepChatAgentPresenter` 发 child status / refresh event +3. `AgentRuntimePresenter` 发 child status / refresh event 4. `dispatch.executeTools()` / `executeDeferredToolCall()` 接 `onProgress` 5. 把 `subagentProgress` / `subagentFinal` 写回 assistant block extra diff --git a/docs/specs/telegram-remote-control/plan.md b/docs/specs/telegram-remote-control/plan.md index 886ab706c..85c3f30ae 100644 --- a/docs/specs/telegram-remote-control/plan.md +++ b/docs/specs/telegram-remote-control/plan.md @@ -4,8 +4,8 @@ - Add `src/main/presenter/remoteControlPresenter/` as a main-process presenter that exposes a small shared contract to the renderer through the existing `presenter:call` IPC path. - Keep Telegram transport in Electron main using native `fetch` and Bot API long polling. -- Reuse `newAgentPresenter.sendMessage()` and `DeepChatAgentPresenter` for message persistence, stream state, title generation, and stop behavior. -- Add detached session creation to `newAgentPresenter` so remote conversations do not require a renderer-bound window. +- Reuse `agentSessionPresenter.sendMessage()` and `AgentRuntimePresenter` for message persistence, stream state, title generation, and stop behavior. +- Add detached session creation to `agentSessionPresenter` so remote conversations do not require a renderer-bound window. ## Main-Process Modules @@ -21,8 +21,8 @@ - Creates detached sessions when needed. - Resolves a valid enabled DeepChat default agent before creating unbound Telegram sessions. - Lists recent sessions by the currently bound session's agent when a valid binding exists; otherwise falls back to the default DeepChat agent. - - Exposes current-session lookup and bound-session model switching through `newAgentPresenter.setSessionModel()`. - - Reuses `newAgentPresenter.sendMessage()` for plain-text Telegram input. + - Exposes current-session lookup and bound-session model switching through `agentSessionPresenter.setSessionModel()`. + - Reuses `agentSessionPresenter.sendMessage()` for plain-text Telegram input. - Tracks the active assistant message/event for `/stop`. - Exposes `statusText`, streamed answer `text`, and `finalText` for remote delivery while preserving compatibility snapshot fields. - `remoteCommandRouter` @@ -80,7 +80,7 @@ 3. Telegram poller receives private updates through `getUpdates`. 4. Parser normalizes message and callback payloads. 5. Router applies auth, command handling, and `/model` inline-menu transitions. -6. Plain text enters `newAgentPresenter.sendMessage()` using the bound or newly created detached session. +6. Plain text enters `agentSessionPresenter.sendMessage()` using the bound or newly created detached session. 7. `/model` callback actions edit a single bot menu message in place and answer the callback query. 8. Poller watches assistant message state, edits a temporary status message as status changes, streams answer text into a separate message, and deletes the status message after final sync. 9. If the assistant pauses on a permission/question action, Telegram returns a desktop-confirmation notice instead of bypassing approval. diff --git a/docs/specs/telegram-remote-control/tasks.md b/docs/specs/telegram-remote-control/tasks.md index a1beaf5cd..e5bcd65fa 100644 --- a/docs/specs/telegram-remote-control/tasks.md +++ b/docs/specs/telegram-remote-control/tasks.md @@ -5,7 +5,7 @@ - Rebuild runtime on settings changes and app init. 2. Detached session support - - Add `createDetachedSession()` to `newAgentPresenter`. + - Add `createDetachedSession()` to `agentSessionPresenter`. - Ensure first remote message still triggers title generation through the shared send path. 3. Remote runtime services diff --git a/docs/specs/tool-output-guardrails/plan.md b/docs/specs/tool-output-guardrails/plan.md index 784ab5e35..3cd0e1bd8 100644 --- a/docs/specs/tool-output-guardrails/plan.md +++ b/docs/specs/tool-output-guardrails/plan.md @@ -15,7 +15,7 @@ - downgrades tail items one by one to the fixed failure message - cleans up offload files for downgraded items - returns terminal fallback if the fully downgraded batch still does not fit -- Refactor `executeTools()` in `deepchatAgentPresenter/dispatch.ts` into two phases: +- Refactor `executeTools()` in `agentRuntimePresenter/dispatch.ts` into two phases: - execute tools and stage candidate outputs plus side effects - fit the staged batch, then commit final tool messages, blocks, hooks, and search persistence once - Keep `question` and `permission` pauses on the immediate path; they are not part of staged batch fitting. diff --git a/package.json b/package.json index 5412ac6c2..007524dd3 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,10 @@ "test:ui": "vitest --ui", "format:check": "oxfmt --check .", "format": "oxfmt .", - "lint": "pnpm run lint:agent-cleanup && oxlint .", + "lint": "pnpm run lint:agent-cleanup && pnpm run lint:architecture && oxlint .", + "lint:architecture": "node scripts/architecture-guard.mjs", "lint:agent-cleanup": "node scripts/agent-cleanup-guard.mjs", + "architecture:baseline": "node scripts/generate-architecture-baseline.mjs", "typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "vue-tsgo --project tsconfig.app.tsgo.json", "typecheck": "pnpm run typecheck:node && pnpm run typecheck:web", diff --git a/scripts/agent-cleanup-guard.mjs b/scripts/agent-cleanup-guard.mjs index 5a6dcdd23..08a19e447 100644 --- a/scripts/agent-cleanup-guard.mjs +++ b/scripts/agent-cleanup-guard.mjs @@ -22,8 +22,8 @@ const LEGACY_MAIN_DIRS = [ ] const PRIMARY_MAIN_GUARD_PATHS = [ - path.join(ROOT, 'src/main/presenter/newAgentPresenter'), - path.join(ROOT, 'src/main/presenter/deepchatAgentPresenter'), + path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), + path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), path.join(ROOT, 'src/main/presenter/skillPresenter'), path.join(ROOT, 'src/main/presenter/mcpPresenter/toolManager.ts'), path.join(ROOT, 'src/main/presenter/syncPresenter/index.ts') @@ -52,7 +52,7 @@ const LEGACY_AGENT_RUNTIME_GLOBALS = [ 'skillPresenter', 'filePermissionService', 'settingsPermissionService', - 'newAgentPresenter', + 'agentSessionPresenter', 'sessionPresenter', 'yoBrowserPresenter', 'filePresenter', @@ -152,8 +152,8 @@ function buildViolation(kind, filePath, specifier) { async function findViolations() { const scanRoots = [ - path.join(ROOT, 'src/main/presenter/newAgentPresenter'), - path.join(ROOT, 'src/main/presenter/deepchatAgentPresenter'), + path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), + path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), path.join(ROOT, 'src/main/presenter/skillPresenter'), path.join(ROOT, 'src/main/presenter/mcpPresenter/toolManager.ts'), path.join(ROOT, 'src/main/presenter/syncPresenter/index.ts'), diff --git a/scripts/architecture-guard.mjs b/scripts/architecture-guard.mjs new file mode 100644 index 000000000..5fe127058 --- /dev/null +++ b/scripts/architecture-guard.mjs @@ -0,0 +1,152 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' + +const ROOT = process.cwd() +const SOURCE_EXTENSIONS = new Set([ + '.js', + '.jsx', + '.mjs', + '.cjs', + '.ts', + '.tsx', + '.mts', + '.cts', + '.vue' +]) + +const MAIN_GUARD_PATHS = [ + path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), + path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), + path.join(ROOT, 'src/main/lib/agentRuntime') +] + +const RENDERER_IPC_GUARD_PATHS = [ + path.join(ROOT, 'src/renderer/src/App.vue'), + path.join(ROOT, 'src/renderer/src/stores/ui/session.ts'), + path.join(ROOT, 'src/renderer/src/stores/ui/message.ts'), + path.join(ROOT, 'src/renderer/src/lib/storeInitializer.ts') +] + +function toPosix(value) { + return value.split(path.sep).join('/') +} + +function relativePath(filePath) { + return toPosix(path.relative(ROOT, filePath)) +} + +function isSourceFile(filePath) { + return SOURCE_EXTENSIONS.has(path.extname(filePath)) +} + +function isUnder(targetPath, parentPath) { + const normalizedTarget = path.resolve(targetPath) + const normalizedParent = path.resolve(parentPath) + return ( + normalizedTarget === normalizedParent || + normalizedTarget.startsWith(`${normalizedParent}${path.sep}`) + ) +} + +async function collectFiles(entryPath) { + const stats = await fs.stat(entryPath) + if (stats.isFile()) { + return isSourceFile(entryPath) ? [entryPath] : [] + } + + const entries = await fs.readdir(entryPath, { withFileTypes: true }) + const files = [] + for (const entry of entries) { + const nextPath = path.join(entryPath, entry.name) + if (entry.isDirectory()) { + files.push(...(await collectFiles(nextPath))) + continue + } + if (entry.isFile() && isSourceFile(nextPath)) { + files.push(nextPath) + } + } + return files +} + +function extractModuleSpecifiers(source) { + const specifiers = new Set() + const patterns = [ + /\bimport\s+(?:type\s+)?[\s\S]*?\bfrom\s*['"]([^'"]+)['"]/g, + /\bexport\s+[\s\S]*?\bfrom\s*['"]([^'"]+)['"]/g, + /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g + ] + + for (const pattern of patterns) { + let match + while ((match = pattern.exec(source)) !== null) { + specifiers.add(match[1]) + } + } + + return [...specifiers] +} + +async function main() { + const scanRoots = [path.join(ROOT, 'src'), path.join(ROOT, 'docs')] + const fileSet = new Set() + + for (const root of scanRoots) { + for (const file of await collectFiles(root)) { + fileSet.add(file) + } + } + + const violations = [] + + for (const filePath of [...fileSet].sort()) { + const source = await fs.readFile(filePath, 'utf8') + const specifiers = extractModuleSpecifiers(source) + + if (isUnder(filePath, path.join(ROOT, 'src'))) { + for (const specifier of specifiers) { + if (specifier.includes('archives/code/')) { + violations.push(`[archive-import] ${relativePath(filePath)} -> ${specifier}`) + } + } + } + + if (MAIN_GUARD_PATHS.some((guardPath) => isUnder(filePath, guardPath))) { + for (const specifier of specifiers) { + if ( + specifier === '@/presenter' || + specifier === '@/presenter/index' || + specifier === '../index' || + specifier === '../../index' + ) { + violations.push(`[main-global-presenter] ${relativePath(filePath)} -> ${specifier}`) + } + } + } + + if (RENDERER_IPC_GUARD_PATHS.some((guardPath) => isUnder(filePath, guardPath))) { + if (source.includes('window.electron.ipcRenderer.on(')) { + violations.push(`[renderer-direct-ipc] ${relativePath(filePath)}`) + } + if (source.includes('window.electron.ipcRenderer.removeAllListeners(')) { + violations.push(`[renderer-remove-all-listeners] ${relativePath(filePath)}`) + } + } + } + + if (violations.length > 0) { + console.error('Architecture guard failed.') + for (const violation of violations) { + console.error(`- ${violation}`) + } + process.exit(1) + } + + console.log('Architecture guard passed.') +} + +main().catch((error) => { + console.error('Architecture guard failed to run:', error) + process.exit(1) +}) diff --git a/scripts/generate-architecture-baseline.mjs b/scripts/generate-architecture-baseline.mjs new file mode 100644 index 000000000..e1691eacd --- /dev/null +++ b/scripts/generate-architecture-baseline.mjs @@ -0,0 +1,358 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' + +const ROOT = process.cwd() +const REPORT_DIR = path.join(ROOT, 'docs/architecture/baselines') +const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.vue', '.d.ts']) +const EXCLUDED_DIRS = new Set(['node_modules', '.git', 'dist', 'out', 'build']) + +const ANALYSIS_TARGETS = [ + { + label: 'main', + root: path.join(ROOT, 'src/main') + }, + { + label: 'renderer', + root: path.join(ROOT, 'src/renderer/src') + } +] + +function toPosix(value) { + return value.split(path.sep).join('/') +} + +async function ensureDir(dirPath) { + await fs.mkdir(dirPath, { recursive: true }) +} + +async function walk(dirPath, output = []) { + const entries = await fs.readdir(dirPath, { withFileTypes: true }) + + for (const entry of entries) { + if (EXCLUDED_DIRS.has(entry.name)) { + continue + } + + const nextPath = path.join(dirPath, entry.name) + if (entry.isDirectory()) { + await walk(nextPath, output) + continue + } + + if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) { + output.push(nextPath) + } + } + + return output +} + +function extractSpecifiers(source) { + const specifiers = new Set() + const patterns = [ + /\bimport\s+(?:type\s+)?[\s\S]*?\bfrom\s*['"]([^'"]+)['"]/g, + /\bexport\s+[\s\S]*?\bfrom\s*['"]([^'"]+)['"]/g, + /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g + ] + + for (const pattern of patterns) { + let match + while ((match = pattern.exec(source)) !== null) { + specifiers.add(match[1]) + } + } + + return [...specifiers] +} + +async function resolveImport(specifier, importer, scopeRoot) { + const tryFile = async (basePath) => { + const candidates = [ + basePath, + `${basePath}.ts`, + `${basePath}.tsx`, + `${basePath}.js`, + `${basePath}.jsx`, + `${basePath}.vue`, + `${basePath}.d.ts`, + path.join(basePath, 'index.ts'), + path.join(basePath, 'index.tsx'), + path.join(basePath, 'index.js'), + path.join(basePath, 'index.jsx'), + path.join(basePath, 'index.vue'), + path.join(basePath, 'index.d.ts') + ] + + for (const candidate of candidates) { + try { + const stat = await fs.stat(candidate) + if (stat.isFile()) { + return candidate + } + } catch {} + } + + return null + } + + if (specifier.startsWith('@/')) { + return await tryFile(path.join(scopeRoot, specifier.slice(2))) + } + + if (specifier.startsWith('@shared/')) { + return await tryFile(path.join(ROOT, 'src/shared', specifier.slice('@shared/'.length))) + } + + if (specifier.startsWith('.')) { + return await tryFile(path.resolve(path.dirname(importer), specifier)) + } + + return null +} + +async function analyzeScope(label, scopeRoot) { + const files = await walk(scopeRoot) + const fileSet = new Set(files) + const edges = new Map(files.map((file) => [file, new Set()])) + const reverseEdges = new Map(files.map((file) => [file, new Set()])) + + for (const file of files) { + const source = await fs.readFile(file, 'utf8') + for (const specifier of extractSpecifiers(source)) { + const resolved = await resolveImport(specifier, file, scopeRoot) + if (!resolved || !fileSet.has(resolved)) { + continue + } + + edges.get(file).add(resolved) + reverseEdges.get(resolved).add(file) + } + } + + const cycles = [] + const cycleKeys = new Set() + const visiting = new Set() + const visited = new Set() + const stack = [] + + function traverse(node) { + visiting.add(node) + stack.push(node) + + for (const next of edges.get(node)) { + if (!visiting.has(next) && !visited.has(next)) { + traverse(next) + continue + } + + if (visiting.has(next)) { + const startIndex = stack.indexOf(next) + const cycle = stack.slice(startIndex).concat(next) + const key = cycle + .slice(0, -1) + .map((file) => path.relative(scopeRoot, file)) + .sort() + .join('|') + + if (!cycleKeys.has(key)) { + cycleKeys.add(key) + cycles.push(cycle) + } + } + } + + stack.pop() + visiting.delete(node) + visited.add(node) + } + + for (const file of files) { + if (!visited.has(file)) { + traverse(file) + } + } + + const topOutgoing = [...edges.entries()] + .map(([file, refs]) => ({ + file: path.relative(scopeRoot, file), + count: refs.size + })) + .sort((left, right) => right.count - left.count) + .slice(0, 15) + + const topIncoming = [...reverseEdges.entries()] + .map(([file, refs]) => ({ + file: path.relative(scopeRoot, file), + count: refs.size + })) + .sort((left, right) => right.count - left.count) + .slice(0, 15) + + const zeroInbound = [...reverseEdges.entries()] + .filter(([, refs]) => refs.size === 0) + .map(([file]) => path.relative(scopeRoot, file)) + .filter((file) => !/index\.(ts|tsx|js|jsx|vue|d\.ts)$/.test(file)) + .sort() + + return { + label, + totalFiles: files.length, + totalEdges: [...edges.values()].reduce((sum, refs) => sum + refs.size, 0), + cycles: cycles.map((cycle) => cycle.map((file) => path.relative(scopeRoot, file))), + topOutgoing, + topIncoming, + zeroInbound + } +} + +async function collectArchiveReferences() { + const scanRoots = [path.join(ROOT, 'docs'), path.join(ROOT, 'src')] + const references = [] + + for (const scanRoot of scanRoots) { + const files = await walk(scanRoot) + for (const file of files) { + const source = await fs.readFile(file, 'utf8') + const lines = source.split('\n') + + lines.forEach((line, index) => { + if (!line.includes('archives/code/')) { + return + } + + references.push({ + file: toPosix(path.relative(ROOT, file)), + line: index + 1, + text: line.trim() + }) + }) + } + } + + return references.sort((left, right) => + `${left.file}:${left.line}`.localeCompare(`${right.file}:${right.line}`) + ) +} + +function renderDependencyReport(scopes) { + const lines = [ + '# Dependency Baseline', + '', + `Generated on ${new Date().toISOString().slice(0, 10)}.`, + '' + ] + + for (const scope of scopes) { + lines.push(`## ${scope.label}`) + lines.push('') + lines.push(`- Total files: ${scope.totalFiles}`) + lines.push(`- Internal dependency edges: ${scope.totalEdges}`) + lines.push(`- Cycles detected: ${scope.cycles.length}`) + lines.push('') + lines.push('### Top outgoing dependencies') + lines.push('') + + for (const item of scope.topOutgoing) { + lines.push(`- \`${item.file}\`: ${item.count}`) + } + + lines.push('') + lines.push('### Top incoming dependencies') + lines.push('') + + for (const item of scope.topIncoming) { + lines.push(`- \`${item.file}\`: ${item.count}`) + } + + lines.push('') + lines.push('### Cycle samples') + lines.push('') + + if (scope.cycles.length === 0) { + lines.push('- None') + } else { + for (const cycle of scope.cycles.slice(0, 20)) { + lines.push(`- \`${cycle.join(' -> ')}\``) + } + } + + lines.push('') + } + + return lines.join('\n') +} + +function renderZeroInboundReport(scopes) { + const lines = [ + '# Zero Inbound Candidates', + '', + `Generated on ${new Date().toISOString().slice(0, 10)}.`, + '', + 'These files have no in-repo importers inside their scope and need manual classification before deletion.', + '' + ] + + for (const scope of scopes) { + lines.push(`## ${scope.label}`) + lines.push('') + lines.push(`- Candidate count: ${scope.zeroInbound.length}`) + lines.push('') + for (const file of scope.zeroInbound) { + lines.push(`- \`${file}\``) + } + lines.push('') + } + + return lines.join('\n') +} + +function renderArchiveReferenceReport(references) { + const lines = [ + '# Archive Reference Baseline', + '', + `Generated on ${new Date().toISOString().slice(0, 10)}.`, + '', + `- Total references: ${references.length}`, + '' + ] + + for (const reference of references) { + lines.push(`- \`${reference.file}:${reference.line}\` ${reference.text}`) + } + + return lines.join('\n') +} + +async function main() { + await ensureDir(REPORT_DIR) + const scopes = [] + + for (const target of ANALYSIS_TARGETS) { + scopes.push(await analyzeScope(target.label, target.root)) + } + + const archiveReferences = await collectArchiveReferences() + + await Promise.all([ + fs.writeFile( + path.join(REPORT_DIR, 'dependency-report.md'), + `${renderDependencyReport(scopes)}\n` + ), + fs.writeFile( + path.join(REPORT_DIR, 'zero-inbound-candidates.md'), + `${renderZeroInboundReport(scopes)}\n` + ), + fs.writeFile( + path.join(REPORT_DIR, 'archive-reference-report.md'), + `${renderArchiveReferenceReport(archiveReferences)}\n` + ) + ]) + + console.log('Architecture baseline reports updated in docs/architecture/baselines.') +} + +main().catch((error) => { + console.error('Failed to generate architecture baseline reports:', error) + process.exit(1) +}) diff --git a/src/main/lib/agentRuntime/systemEnvPromptBuilder.ts b/src/main/lib/agentRuntime/systemEnvPromptBuilder.ts index 1579b1196..5dfa52787 100644 --- a/src/main/lib/agentRuntime/systemEnvPromptBuilder.ts +++ b/src/main/lib/agentRuntime/systemEnvPromptBuilder.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs' import path from 'node:path' -import { presenter } from '@/presenter' import logger from '@shared/logger' +import type { ConfigQueryPort } from '@/presenter/runtimePorts' export interface BuildSystemEnvPromptOptions { providerId?: string @@ -10,6 +10,7 @@ export interface BuildSystemEnvPromptOptions { platform?: NodeJS.Platform now?: Date agentsFilePath?: string + modelLookup?: Pick } export interface RuntimeCapabilitiesPromptOptions { @@ -18,15 +19,19 @@ export interface RuntimeCapabilitiesPromptOptions { hasProcess?: boolean } -function resolveModelDisplayName(providerId: string, modelId: string): string | undefined { +function resolveModelDisplayName( + providerId: string, + modelId: string, + modelLookup?: Pick +): string | undefined { try { - const models = presenter.configPresenter?.getProviderModels?.(providerId) || [] + const models = modelLookup?.getProviderModels(providerId) || [] const match = models.find((model) => model.id === modelId) if (match?.name) { return match.name } - const customModels = presenter.configPresenter?.getCustomModels?.(providerId) || [] + const customModels = modelLookup?.getCustomModels(providerId) || [] const customMatch = customModels.find((model) => model.id === modelId) if (customMatch?.name) { return customMatch.name @@ -43,14 +48,15 @@ function resolveModelDisplayName(providerId: string, modelId: string): string | function resolveModelIdentity( providerId?: string, - modelId?: string + modelId?: string, + modelLookup?: Pick ): { modelName: string exactModelId: string } { const trimmedProviderId = providerId?.trim() || 'unknown-provider' const trimmedModelId = modelId?.trim() || 'unknown-model' - const displayName = resolveModelDisplayName(trimmedProviderId, trimmedModelId) + const displayName = resolveModelDisplayName(trimmedProviderId, trimmedModelId, modelLookup) return { modelName: displayName || trimmedModelId, @@ -133,7 +139,11 @@ export async function buildSystemEnvPrompt( ? path.resolve(options.agentsFilePath) : path.join(workdir, 'AGENTS.md') const agentsContent = await readAgentsInstructions(agentsFilePath) - const { modelName, exactModelId } = resolveModelIdentity(options.providerId, options.modelId) + const { modelName, exactModelId } = resolveModelIdentity( + options.providerId, + options.modelId, + options.modelLookup + ) const promptLines = [ `You are powered by the model named ${modelName}.`, diff --git a/src/main/presenter/deepchatAgentPresenter/accumulator.ts b/src/main/presenter/agentRuntimePresenter/accumulator.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/accumulator.ts rename to src/main/presenter/agentRuntimePresenter/accumulator.ts diff --git a/src/main/presenter/deepchatAgentPresenter/compactionService.ts b/src/main/presenter/agentRuntimePresenter/compactionService.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/compactionService.ts rename to src/main/presenter/agentRuntimePresenter/compactionService.ts diff --git a/src/main/presenter/deepchatAgentPresenter/contextBuilder.ts b/src/main/presenter/agentRuntimePresenter/contextBuilder.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/contextBuilder.ts rename to src/main/presenter/agentRuntimePresenter/contextBuilder.ts diff --git a/src/main/presenter/deepchatAgentPresenter/dispatch.ts b/src/main/presenter/agentRuntimePresenter/dispatch.ts similarity index 95% rename from src/main/presenter/deepchatAgentPresenter/dispatch.ts rename to src/main/presenter/agentRuntimePresenter/dispatch.ts index 7e893058c..1c3dc95a9 100644 --- a/src/main/presenter/deepchatAgentPresenter/dispatch.ts +++ b/src/main/presenter/agentRuntimePresenter/dispatch.ts @@ -1,6 +1,5 @@ import { eventBus, SendTarget } from '@/eventbus' import { STREAM_EVENTS } from '@/events' -import { presenter } from '@/presenter' import type { MCPToolCall, MCPContentItem, @@ -399,41 +398,12 @@ function normalizePermissionRequest( } async function autoGrantPermission( - conversationId: string, + hooks: ProcessHooks | undefined, + _conversationId: string, permission: NonNullable ): Promise { - const type = permission.permissionType - const serverName = permission.serverName || '' - const toolName = permission.toolName || '' - - if (type === 'command') { - const command = permission.command || permission.commandInfo?.command || '' - const signature = - permission.commandSignature || - permission.commandInfo?.signature || - (command ? presenter.commandPermissionService.extractCommandSignature(command) : '') - if (signature) { - presenter.commandPermissionService.approve(conversationId, signature, false) - } - return - } - - if ( - serverName === 'agent-filesystem' && - Array.isArray(permission.paths) && - permission.paths.length - ) { - presenter.filePermissionService?.approve(conversationId, permission.paths, false) - return - } - - if (serverName === 'deepchat-settings' && toolName) { - presenter.settingsPermissionService?.approve(conversationId, toolName, false) - return - } - - if (serverName && (type === 'read' || type === 'write' || type === 'all')) { - await presenter.mcpPresenter.grantPermission(serverName, type, false, conversationId) + if (hooks?.autoGrantPermission) { + await hooks.autoGrantPermission(permission) } } @@ -698,7 +668,7 @@ export async function executeTools( if (preCheckedPermission) { if (permissionMode === 'full_access') { - await autoGrantPermission(io.sessionId, preCheckedPermission) + await autoGrantPermission(hooks, io.sessionId, preCheckedPermission) } else { hooks?.onPermissionRequest?.(preCheckedPermission, { callId: tc.id, @@ -756,7 +726,7 @@ export async function executeTools( if (pendingPermission) { if (permissionMode === 'full_access') { - await autoGrantPermission(io.sessionId, pendingPermission) + await autoGrantPermission(hooks, io.sessionId, pendingPermission) const retryCallResult = await toolPresenter.callTool(toolCall, { onProgress: applyProgressUpdate, signal: io.abortSignal diff --git a/src/main/presenter/deepchatAgentPresenter/echo.ts b/src/main/presenter/agentRuntimePresenter/echo.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/echo.ts rename to src/main/presenter/agentRuntimePresenter/echo.ts diff --git a/src/main/presenter/deepchatAgentPresenter/index.ts b/src/main/presenter/agentRuntimePresenter/index.ts similarity index 96% rename from src/main/presenter/deepchatAgentPresenter/index.ts rename to src/main/presenter/agentRuntimePresenter/index.ts index e7668a163..969c63518 100644 --- a/src/main/presenter/deepchatAgentPresenter/index.ts +++ b/src/main/presenter/agentRuntimePresenter/index.ts @@ -4,8 +4,10 @@ import type { DeepChatSessionState, IAgentImplementation, MessageFile, + PendingInputEnqueueSource, PendingSessionInputRecord, PermissionMode, + QueuePendingInputOptions, SendMessageInput, SessionCompactionState, SessionGenerationSettings, @@ -18,6 +20,7 @@ import type { ChatMessage } from '@shared/types/core/chat-message' import type { IConfigPresenter, ILlmProviderPresenter, + ISkillPresenter, ModelConfig, RateLimitQueueSnapshot } from '@shared/presenter' @@ -38,7 +41,6 @@ import { buildRuntimeCapabilitiesPrompt, buildSystemEnvPrompt } from '@/lib/agentRuntime/systemEnvPromptBuilder' -import { presenter } from '@/presenter' import { buildContext, buildResumeContext, @@ -58,6 +60,7 @@ import type { ProviderRequestTracePayload } from '../llmProviderPresenter/reques import type { NewSessionHooksBridge } from '../hooksNotifications/newSessionBridge' import { providerDbLoader } from '../configPresenter/providerDbLoader' import { resolveSessionVisionTarget } from '../vision/sessionVisionResolver' +import type { ConfigQueryPort, SessionRuntimePort } from '../runtimePorts' import { buildAssistantPreviewMarkdown, buildAssistantResponseMarkdown, @@ -131,7 +134,7 @@ const createAbortError = (): Error => { return error } -export class DeepChatAgentPresenter implements IAgentImplementation { +export class AgentRuntimePresenter implements IAgentImplementation { private readonly llmProviderPresenter: ILlmProviderPresenter private readonly configPresenter: IConfigPresenter private readonly sqlitePresenter: SQLitePresenter @@ -155,6 +158,12 @@ export class DeepChatAgentPresenter implements IAgentImplementation { private readonly compactionService: CompactionService private readonly toolOutputGuard: ToolOutputGuard private readonly hooksBridge?: NewSessionHooksBridge + private readonly configQueryPort: Pick + private readonly sessionRuntimePort?: SessionRuntimePort + private readonly skillPresenter?: Pick< + ISkillPresenter, + 'getMetadataList' | 'getActiveSkills' | 'loadSkillContent' + > private nextRunSequence = 0 constructor( @@ -162,7 +171,15 @@ export class DeepChatAgentPresenter implements IAgentImplementation { configPresenter: IConfigPresenter, sqlitePresenter: SQLitePresenter, toolPresenter?: IToolPresenter, - hooksBridge?: NewSessionHooksBridge + hooksBridge?: NewSessionHooksBridge, + runtimePorts?: { + configQueryPort?: Pick + sessionRuntimePort?: SessionRuntimePort + skillPresenter?: Pick< + ISkillPresenter, + 'getMetadataList' | 'getActiveSkills' | 'loadSkillContent' + > + } ) { this.llmProviderPresenter = llmProviderPresenter this.configPresenter = configPresenter @@ -179,11 +196,21 @@ export class DeepChatAgentPresenter implements IAgentImplementation { this.configPresenter, async (sessionId) => { const agentId = this.getSessionAgentId(sessionId) ?? 'deepchat' + if (typeof this.configPresenter.resolveDeepChatAgentConfig !== 'function') { + return {} + } + return await this.configPresenter.resolveDeepChatAgentConfig(agentId) } ) this.toolOutputGuard = new ToolOutputGuard() this.hooksBridge = hooksBridge + this.configQueryPort = runtimePorts?.configQueryPort ?? { + getProviderModels: (providerId) => this.configPresenter.getProviderModels?.(providerId) ?? [], + getCustomModels: (providerId) => this.configPresenter.getCustomModels?.(providerId) ?? [] + } + this.sessionRuntimePort = runtimePorts?.sessionRuntimePort + this.skillPresenter = runtimePorts?.skillPresenter const recovered = this.messageStore.recoverPendingMessages() if (recovered > 0) { @@ -297,7 +324,8 @@ export class DeepChatAgentPresenter implements IAgentImplementation { async queuePendingInput( sessionId: string, - content: string | SendMessageInput + content: string | SendMessageInput, + options?: QueuePendingInputOptions ): Promise { const state = await this.getSessionState(sessionId) if (!state) { @@ -305,7 +333,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { } const shouldClaimImmediately = - this.isAwaitingToolQuestionFollowUp(sessionId) || + ((options?.source ?? 'send') === 'send' && this.isAwaitingToolQuestionFollowUp(sessionId)) || this.shouldStartQueuedInputImmediately(sessionId, state.status) const record = this.pendingInputCoordinator.queuePendingInput(sessionId, content, { state: shouldClaimImmediately ? 'claimed' : 'pending' @@ -314,7 +342,8 @@ export class DeepChatAgentPresenter implements IAgentImplementation { if (record.state === 'claimed') { void this.processMessage(sessionId, record.payload, { projectDir: this.resolveProjectDir(sessionId), - pendingQueueItemId: record.id + pendingQueueItemId: record.id, + pendingQueueItemSource: options?.source ?? 'send' }) return record } @@ -373,6 +402,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { projectDir?: string | null emitRefreshBeforeStream?: boolean pendingQueueItemId?: string + pendingQueueItemSource?: PendingInputEnqueueSource } ): Promise { const state = this.runtimeState.get(sessionId) @@ -503,7 +533,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { assistantMessageId = this.messageStore.createAssistantMessage(sessionId, assistantOrderSeq) this.throwIfAbortRequested(preStreamAbortSignal) - if (context?.pendingQueueItemId) { + if (context?.pendingQueueItemId && context.pendingQueueItemSource !== 'queue') { this.pendingInputCoordinator.consumeQueuedInput(sessionId, context.pendingQueueItemId) consumedPendingQueueItem = true } @@ -521,6 +551,20 @@ export class DeepChatAgentPresenter implements IAgentImplementation { tools, interleavedReasoning }) + if (context?.pendingQueueItemId && !consumedPendingQueueItem) { + if (context.pendingQueueItemSource === 'queue') { + if (result.status === 'completed' || result.status === 'paused') { + this.pendingInputCoordinator.consumeQueuedInput(sessionId, context.pendingQueueItemId) + consumedPendingQueueItem = true + } else { + this.rollbackClaimedQueueInputTurn(sessionId, context.pendingQueueItemId, userMessageId) + consumedPendingQueueItem = true + } + } else { + this.pendingInputCoordinator.consumeQueuedInput(sessionId, context.pendingQueueItemId) + consumedPendingQueueItem = true + } + } try { this.applyProcessResultStatus(sessionId, result, runId) } finally { @@ -533,10 +577,15 @@ export class DeepChatAgentPresenter implements IAgentImplementation { console.error('[DeepChatAgent] processMessage error:', err) if (context?.pendingQueueItemId && !consumedPendingQueueItem) { try { - this.pendingInputCoordinator.releaseClaimedQueueInput( - sessionId, - context.pendingQueueItemId - ) + if (context.pendingQueueItemSource === 'queue') { + this.rollbackClaimedQueueInputTurn(sessionId, context.pendingQueueItemId, userMessageId) + } else { + this.pendingInputCoordinator.releaseClaimedQueueInput( + sessionId, + context.pendingQueueItemId + ) + } + consumedPendingQueueItem = true } catch (releaseError) { console.warn('[DeepChatAgent] failed to release claimed queue input:', releaseError) } @@ -1595,6 +1644,9 @@ export class DeepChatAgentPresenter implements IAgentImplementation { } }) }, + autoGrantPermission: async (permission) => { + await this.sessionRuntimePort?.approvePermission(sessionId, permission) + }, normalizeToolResult: async (tool) => await this.normalizeToolResultContent({ sessionId: tool.sessionId, @@ -1730,6 +1782,19 @@ export class DeepChatAgentPresenter implements IAgentImplementation { return (reason === 'enqueue' || reason === 'resume') && status === 'error' } + private rollbackClaimedQueueInputTurn( + sessionId: string, + pendingQueueItemId: string, + userMessageId: string | null + ): void { + const userMessage = userMessageId ? this.messageStore.getMessage(userMessageId) : null + if (userMessage) { + this.invalidateSummaryIfNeeded(sessionId, userMessage.orderSeq) + this.messageStore.deleteFromOrderSeq(sessionId, userMessage.orderSeq) + } + this.pendingInputCoordinator.releaseClaimedQueueInput(sessionId, pendingQueueItemId) + } + private registerActiveGeneration( sessionId: string, messageId: string, @@ -1998,7 +2063,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { const dayKey = this.buildLocalDayKey(now) const skillsEnabled = this.configPresenter.getSkillsEnabled() - const skillPresenter = presenter?.skillPresenter + const skillPresenter = this.skillPresenter const availableSkillNames: string[] = [] const activeSkillNames: string[] = [] @@ -2102,7 +2167,8 @@ export class DeepChatAgentPresenter implements IAgentImplementation { providerId, modelId, workdir, - now + now, + modelLookup: this.configQueryPort }) } catch (error) { console.warn(`[DeepChatAgent] Failed to build env prompt for session ${sessionId}:`, error) @@ -3057,23 +3123,34 @@ export class DeepChatAgentPresenter implements IAgentImplementation { if (permissionType === 'command') { const command = payload.command || payload.commandInfo?.command || '' - const signature = - payload.commandSignature || - payload.commandInfo?.signature || - (command ? presenter.commandPermissionService.extractCommandSignature(command) : '') + const signature = payload.commandSignature || payload.commandInfo?.signature || command if (signature) { - presenter.commandPermissionService.approve(sessionId, signature, false) + await this.sessionRuntimePort?.approvePermission(sessionId, { + permissionType: 'command', + command, + commandSignature: signature, + commandInfo: payload.commandInfo + }) } return } if (serverName === 'agent-filesystem' && Array.isArray(payload.paths) && payload.paths.length) { - presenter.filePermissionService?.approve(sessionId, payload.paths, false) + await this.sessionRuntimePort?.approvePermission(sessionId, { + permissionType: 'write', + serverName, + toolName, + paths: payload.paths + }) return } if (serverName === 'deepchat-settings' && toolName) { - presenter.settingsPermissionService?.approve(sessionId, toolName, false) + await this.sessionRuntimePort?.approvePermission(sessionId, { + permissionType: 'write', + serverName, + toolName + }) return } @@ -3081,7 +3158,11 @@ export class DeepChatAgentPresenter implements IAgentImplementation { serverName && (permissionType === 'read' || permissionType === 'write' || permissionType === 'all') ) { - await presenter.mcpPresenter.grantPermission(serverName, permissionType, false, sessionId) + await this.sessionRuntimePort?.approvePermission(sessionId, { + permissionType, + serverName, + toolName + }) } } @@ -3720,11 +3801,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { status }) - try { - void presenter.floatingButtonPresenter.refreshWidgetState() - } catch (error) { - console.warn('[DeepChatAgent] Failed to refresh floating widget state:', error) - } + this.sessionRuntimePort?.refreshSessionUi() } private emitMessageRefresh(sessionId: string, messageId: string): void { diff --git a/src/main/presenter/deepchatAgentPresenter/internalSessionEvents.ts b/src/main/presenter/agentRuntimePresenter/internalSessionEvents.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/internalSessionEvents.ts rename to src/main/presenter/agentRuntimePresenter/internalSessionEvents.ts diff --git a/src/main/presenter/deepchatAgentPresenter/messageStore.ts b/src/main/presenter/agentRuntimePresenter/messageStore.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/messageStore.ts rename to src/main/presenter/agentRuntimePresenter/messageStore.ts diff --git a/src/main/presenter/deepchatAgentPresenter/messageTracePayload.ts b/src/main/presenter/agentRuntimePresenter/messageTracePayload.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/messageTracePayload.ts rename to src/main/presenter/agentRuntimePresenter/messageTracePayload.ts diff --git a/src/main/presenter/deepchatAgentPresenter/pendingInputCoordinator.ts b/src/main/presenter/agentRuntimePresenter/pendingInputCoordinator.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/pendingInputCoordinator.ts rename to src/main/presenter/agentRuntimePresenter/pendingInputCoordinator.ts diff --git a/src/main/presenter/deepchatAgentPresenter/pendingInputStore.ts b/src/main/presenter/agentRuntimePresenter/pendingInputStore.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/pendingInputStore.ts rename to src/main/presenter/agentRuntimePresenter/pendingInputStore.ts diff --git a/src/main/presenter/deepchatAgentPresenter/process.ts b/src/main/presenter/agentRuntimePresenter/process.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/process.ts rename to src/main/presenter/agentRuntimePresenter/process.ts diff --git a/src/main/presenter/deepchatAgentPresenter/sessionStore.ts b/src/main/presenter/agentRuntimePresenter/sessionStore.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/sessionStore.ts rename to src/main/presenter/agentRuntimePresenter/sessionStore.ts diff --git a/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts b/src/main/presenter/agentRuntimePresenter/toolOutputGuard.ts similarity index 100% rename from src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts rename to src/main/presenter/agentRuntimePresenter/toolOutputGuard.ts diff --git a/src/main/presenter/deepchatAgentPresenter/types.ts b/src/main/presenter/agentRuntimePresenter/types.ts similarity index 97% rename from src/main/presenter/deepchatAgentPresenter/types.ts rename to src/main/presenter/agentRuntimePresenter/types.ts index df53d0936..84263e9d3 100644 --- a/src/main/presenter/deepchatAgentPresenter/types.ts +++ b/src/main/presenter/agentRuntimePresenter/types.ts @@ -76,6 +76,9 @@ export interface ProcessHooks { reasoningContentLength: number toolCallCount: number }) => void + autoGrantPermission?: ( + permission: NonNullable + ) => Promise normalizeToolResult?: (tool: { sessionId: string toolCallId: string diff --git a/src/main/presenter/newAgentPresenter/agentRegistry.ts b/src/main/presenter/agentSessionPresenter/agentRegistry.ts similarity index 100% rename from src/main/presenter/newAgentPresenter/agentRegistry.ts rename to src/main/presenter/agentSessionPresenter/agentRegistry.ts diff --git a/src/main/presenter/newAgentPresenter/index.ts b/src/main/presenter/agentSessionPresenter/index.ts similarity index 95% rename from src/main/presenter/newAgentPresenter/index.ts rename to src/main/presenter/agentSessionPresenter/index.ts index c3860d7ee..ef662be83 100644 --- a/src/main/presenter/newAgentPresenter/index.ts +++ b/src/main/presenter/agentSessionPresenter/index.ts @@ -36,14 +36,13 @@ import type { CONVERSATION } from '@shared/presenter' import type { SQLitePresenter } from '../sqlitePresenter' -import type { DeepChatAgentPresenter } from '../deepchatAgentPresenter' +import type { AgentRuntimePresenter } from '../agentRuntimePresenter' import { AgentRegistry } from './agentRegistry' import { NewSessionManager } from './sessionManager' import { NewMessageManager } from './messageManager' import { LegacyChatImportService } from './legacyImportService' import { eventBus, SendTarget } from '@/eventbus' import { SESSION_EVENTS } from '@/events' -import { presenter } from '@/presenter' import { buildConversationExportContent, generateExportFilename, @@ -63,6 +62,7 @@ import { } from '../usageStats' import { rtkRuntimeService } from '@/lib/agentRuntime/rtkRuntimeService' import { resolveAcpAgentAlias } from '../configPresenter/acpRegistryConstants' +import type { SessionRuntimePort } from '../runtimePorts' type SearchableSessionRow = { id: string @@ -212,7 +212,7 @@ const extractSearchableMessageContent = (rawContent: string): string => { return rawContent } -export class NewAgentPresenter { +export class AgentSessionPresenter { private agentRegistry: AgentRegistry private sessionManager: NewSessionManager private messageManager: NewMessageManager @@ -221,14 +221,16 @@ export class NewAgentPresenter { private configPresenter: IConfigPresenter private legacyImportService: LegacyChatImportService private skillPresenter?: Pick + private sessionRuntimePort?: SessionRuntimePort private usageStatsBackfillPromise: Promise | null = null constructor( - deepchatAgent: DeepChatAgentPresenter, + agentRuntimePresenter: AgentRuntimePresenter, llmProviderPresenter: ILlmProviderPresenter, configPresenter: IConfigPresenter, sqlitePresenter: SQLitePresenter, - skillPresenter?: Pick + skillPresenter?: Pick, + sessionRuntimePort?: SessionRuntimePort ) { this.sqlitePresenter = sqlitePresenter this.llmProviderPresenter = llmProviderPresenter @@ -238,11 +240,12 @@ export class NewAgentPresenter { this.sessionManager = new NewSessionManager(sqlitePresenter) this.messageManager = new NewMessageManager(this.agentRegistry) this.legacyImportService = new LegacyChatImportService(sqlitePresenter) + this.sessionRuntimePort = sessionRuntimePort // Register the built-in deepchat agent this.agentRegistry.register( { id: 'deepchat', name: 'DeepChat', type: 'deepchat', enabled: true }, - deepchatAgent + agentRuntimePresenter ) } @@ -250,17 +253,17 @@ export class NewAgentPresenter { async createSession(input: CreateSessionInput, webContentsId: number): Promise { const agentId = input.agentId || 'deepchat' - console.log(`[NewAgentPresenter] createSession agent=${agentId} webContentsId=${webContentsId}`) + console.log( + `[AgentSessionPresenter] createSession agent=${agentId} webContentsId=${webContentsId}` + ) const normalizedInput = this.normalizeCreateSessionInput(input) const agentType = await this.getAgentType(agentId) const deepChatAgentConfig = - agentType === 'deepchat' - ? await this.configPresenter.resolveDeepChatAgentConfig(agentId) - : null + agentType === 'deepchat' ? await this.resolveDeepChatAgentConfigCompat(agentId) : null const projectDir = input.projectDir?.trim() || deepChatAgentConfig?.defaultProjectPath?.trim() || - this.configPresenter.getDefaultProjectPath() || + this.getDefaultProjectPathCompat() || null const disabledAgentTools = agentType === 'deepchat' @@ -300,7 +303,7 @@ export class NewAgentPresenter { deepChatAgentConfig, input.generationSettings ) - console.log(`[NewAgentPresenter] resolved provider=${providerId} model=${modelId}`) + console.log(`[AgentSessionPresenter] resolved provider=${providerId} model=${modelId}`) if (!providerId || !modelId) { throw new Error('No provider or model configured. Please set a default model in settings.') @@ -314,7 +317,7 @@ export class NewAgentPresenter { disabledAgentTools, subagentEnabled }) - console.log(`[NewAgentPresenter] session created id=${sessionId} title="${title}"`) + console.log(`[AgentSessionPresenter] session created id=${sessionId} title="${title}"`) // Initialize agent-side session const initConfig: { @@ -340,7 +343,7 @@ export class NewAgentPresenter { await this.cleanupFailedSessionInitialization(agent, sessionId) throw error } - console.log(`[NewAgentPresenter] agent.initSession done`) + console.log(`[AgentSessionPresenter] agent.initSession done`) // Bind to window and emit activated this.sessionManager.bindWindow(webContentsId, sessionId) @@ -376,14 +379,14 @@ export class NewAgentPresenter { // Queue the first message (non-blocking) after returning session ID if (normalizedInput.text.trim() || (normalizedInput.files?.length ?? 0) > 0) { - console.log(`[NewAgentPresenter] firing queuePendingInput (non-blocking)`) + console.log(`[AgentSessionPresenter] firing queuePendingInput (non-blocking)`) if (agent.queuePendingInput) { - agent.queuePendingInput(sessionId, normalizedInput).catch((err) => { - console.error('[NewAgentPresenter] queuePendingInput failed:', err) + agent.queuePendingInput(sessionId, normalizedInput, { source: 'send' }).catch((err) => { + console.error('[AgentSessionPresenter] queuePendingInput failed:', err) }) } else { agent.processMessage(sessionId, normalizedInput, { projectDir }).catch((err) => { - console.error('[NewAgentPresenter] processMessage failed:', err) + console.error('[AgentSessionPresenter] processMessage failed:', err) }) } } @@ -397,13 +400,11 @@ export class NewAgentPresenter { const title = input.title?.trim() || 'New Chat' const agentType = await this.getAgentType(agentId) const deepChatAgentConfig = - agentType === 'deepchat' - ? await this.configPresenter.resolveDeepChatAgentConfig(agentId) - : null + agentType === 'deepchat' ? await this.resolveDeepChatAgentConfigCompat(agentId) : null const projectDir = input.projectDir?.trim() || deepChatAgentConfig?.defaultProjectPath?.trim() || - this.configPresenter.getDefaultProjectPath() || + this.getDefaultProjectPathCompat() || null const disabledAgentTools = agentType === 'deepchat' @@ -666,7 +667,7 @@ export class NewAgentPresenter { session.projectDir ?? null ) if (agent.queuePendingInput) { - await agent.queuePendingInput(sessionId, normalizedInput) + await agent.queuePendingInput(sessionId, normalizedInput, { source: 'send' }) if (!hadMessages && !wasDraft) { void this.generateSessionTitle(sessionId, session.title, providerId, state?.modelId ?? '') } @@ -725,7 +726,7 @@ export class NewAgentPresenter { currentSession.agentId, currentSession.projectDir ?? null ) - return await agent.queuePendingInput(sessionId, normalizedInput) + return await agent.queuePendingInput(sessionId, normalizedInput, { source: 'queue' }) } async updateQueuedInput(sessionId: string, itemId: string, content: string | SendMessageInput) { @@ -875,7 +876,7 @@ export class NewAgentPresenter { await agent.destroySession(targetSessionId) } catch (cleanupError) { console.warn( - `[NewAgentPresenter] Failed to cleanup forked session runtime ${targetSessionId}:`, + `[AgentSessionPresenter] Failed to cleanup forked session runtime ${targetSessionId}:`, cleanupError ) } @@ -1058,7 +1059,7 @@ export class NewAgentPresenter { searchId: result.searchId ?? row.search_id ?? undefined }) } catch (error) { - console.warn('[NewAgentPresenter] Failed to parse search result row:', error) + console.warn('[AgentSessionPresenter] Failed to parse search result row:', error) } } @@ -1643,18 +1644,16 @@ export class NewAgentPresenter { this.sessionManager.update(sessionId, { title: normalized }) this.emitSessionListUpdated() } catch (error) { - console.warn(`[NewAgentPresenter] title generation skipped for session=${sessionId}:`, error) + console.warn( + `[AgentSessionPresenter] title generation skipped for session=${sessionId}:`, + error + ) } } private emitSessionListUpdated(): void { eventBus.sendToRenderer(SESSION_EVENTS.LIST_UPDATED, SendTarget.ALL_WINDOWS) - - try { - void presenter.floatingButtonPresenter.refreshWidgetState() - } catch (error) { - console.warn('[NewAgentPresenter] Failed to refresh floating widget state:', error) - } + this.sessionRuntimePort?.refreshSessionUi() } private async waitForSessionIdle(sessionId: string): Promise { @@ -1695,7 +1694,7 @@ export class NewAgentPresenter { return await this.buildSessionWithState(record) } catch (error) { console.warn( - `[NewAgentPresenter] Skipping unavailable session id=${record.id} agent=${record.agentId}:`, + `[AgentSessionPresenter] Skipping unavailable session id=${record.id} agent=${record.agentId}:`, error ) return null @@ -1725,18 +1724,54 @@ export class NewAgentPresenter { } private async getAgentType(agentId: string): Promise<'deepchat' | 'acp' | null> { + if (typeof this.configPresenter.getAgentType !== 'function') { + const resolvedAgentId = resolveAcpAgentAlias(agentId) + if (resolvedAgentId === 'deepchat') { + return 'deepchat' + } + const fallbackAgent = await this.configPresenter.getAgent?.(resolvedAgentId) + if (fallbackAgent?.type === 'acp' || fallbackAgent?.type === 'deepchat') { + return fallbackAgent.type + } + + const acpAgents = await this.configPresenter.getAcpAgents?.() + if (acpAgents?.some((agent) => resolveAcpAgentAlias(agent.id) === resolvedAgentId)) { + return 'acp' + } + + return null + } + return await this.configPresenter.getAgentType(resolveAcpAgentAlias(agentId)) } + private async resolveDeepChatAgentConfigCompat( + agentId: string + ): Promise> | null> { + if (typeof this.configPresenter.resolveDeepChatAgentConfig !== 'function') { + return {} as Awaited> + } + + return await this.configPresenter.resolveDeepChatAgentConfig(agentId) + } + + private getDefaultProjectPathCompat(): string | null { + if (typeof this.configPresenter.getDefaultProjectPath !== 'function') { + return null + } + + return this.configPresenter.getDefaultProjectPath() ?? null + } + private async resolveAssistantModelSelection( agentId: string, fallbackProviderId: string, fallbackModelId: string ): Promise<{ providerId: string; modelId: string }> { if ((await this.getAgentType(agentId)) === 'deepchat') { - const config = await this.configPresenter.resolveDeepChatAgentConfig(agentId) - const providerId = config.assistantModel?.providerId?.trim() - const modelId = config.assistantModel?.modelId?.trim() + const config = await this.resolveDeepChatAgentConfigCompat(agentId) + const providerId = config?.assistantModel?.providerId?.trim() + const modelId = config?.assistantModel?.modelId?.trim() if (providerId && modelId) { return { providerId, @@ -1809,9 +1844,7 @@ export class NewAgentPresenter { await this.llmProviderPresenter.clearAcpSession(sessionId) } await agent.destroySession(sessionId) - presenter.commandPermissionService.clearConversation(sessionId) - presenter.filePermissionService?.clearConversation(sessionId) - presenter.settingsPermissionService?.clearConversation(sessionId) + this.sessionRuntimePort?.clearSessionPermissions(sessionId) await this.skillPresenter?.clearNewAgentSessionSkills?.(sessionId) this.sessionManager.delete(sessionId) } @@ -1854,7 +1887,7 @@ export class NewAgentPresenter { return ids.length > 0 } catch (error) { console.warn( - `[NewAgentPresenter] Failed to inspect message ids for session=${sessionId}:`, + `[AgentSessionPresenter] Failed to inspect message ids for session=${sessionId}:`, error ) return true @@ -1937,7 +1970,7 @@ export class NewAgentPresenter { normalizedProjectDir ) } catch (error) { - console.warn('[NewAgentPresenter] Failed to sync ACP workdir for session:', { + console.warn('[AgentSessionPresenter] Failed to sync ACP workdir for session:', { conversationId, agentId, projectDir: normalizedProjectDir, @@ -1955,7 +1988,7 @@ export class NewAgentPresenter { await agent.destroySession(sessionId) } catch (cleanupError) { console.warn( - `[NewAgentPresenter] Failed to cleanup session runtime after initialization error ${sessionId}:`, + `[AgentSessionPresenter] Failed to cleanup session runtime after initialization error ${sessionId}:`, cleanupError ) } diff --git a/src/main/presenter/newAgentPresenter/legacyImportService.ts b/src/main/presenter/agentSessionPresenter/legacyImportService.ts similarity index 100% rename from src/main/presenter/newAgentPresenter/legacyImportService.ts rename to src/main/presenter/agentSessionPresenter/legacyImportService.ts diff --git a/src/main/presenter/newAgentPresenter/messageManager.ts b/src/main/presenter/agentSessionPresenter/messageManager.ts similarity index 100% rename from src/main/presenter/newAgentPresenter/messageManager.ts rename to src/main/presenter/agentSessionPresenter/messageManager.ts diff --git a/src/main/presenter/newAgentPresenter/sessionManager.ts b/src/main/presenter/agentSessionPresenter/sessionManager.ts similarity index 100% rename from src/main/presenter/newAgentPresenter/sessionManager.ts rename to src/main/presenter/agentSessionPresenter/sessionManager.ts diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts index f22aec505..6676aa5c8 100644 --- a/src/main/presenter/floatingButtonPresenter/index.ts +++ b/src/main/presenter/floatingButtonPresenter/index.ts @@ -545,28 +545,28 @@ export class FloatingButtonPresenter { } private async loadDeepChatSessions(): Promise { - const newAgentPresenter = presenter.newAgentPresenter as + const agentSessionPresenter = presenter.agentSessionPresenter as | { getSessionList?: (filters?: { agentId?: string }) => Promise } | undefined - if (!newAgentPresenter?.getSessionList) { + if (!agentSessionPresenter?.getSessionList) { return [] } - return await newAgentPresenter.getSessionList({ agentId: 'deepchat' }) + return await agentSessionPresenter.getSessionList({ agentId: 'deepchat' }) } private async openSession(sessionId: string): Promise { try { - const newAgentPresenter = presenter.newAgentPresenter as + const agentSessionPresenter = presenter.agentSessionPresenter as | { activateSession?: (webContentsId: number, sessionId: string) => Promise } | undefined - if (!newAgentPresenter?.activateSession) { + if (!agentSessionPresenter?.activateSession) { return } @@ -575,7 +575,7 @@ export class FloatingButtonPresenter { return } - await newAgentPresenter.activateSession(targetWindow.webContents.id, sessionId) + await agentSessionPresenter.activateSession(targetWindow.webContents.id, sessionId) presenter.windowPresenter.show(targetWindow.id, true) this.setExpanded(false) } catch (error) { diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 03da74983..3f1c1f667 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -27,7 +27,7 @@ import { IYoBrowserPresenter, ISkillPresenter, ISkillSyncPresenter, - INewAgentPresenter, + IAgentSessionPresenter, IProjectPresenter, IRemoteControlPresenter } from '@shared/presenter' @@ -64,15 +64,16 @@ import type { SkillSessionStatePort } from './skillPresenter' import { SkillSyncPresenter } from './skillSyncPresenter' import { HooksNotificationsService } from './hooksNotifications' import { NewSessionHooksBridge } from './hooksNotifications/newSessionBridge' -import { NewAgentPresenter } from './newAgentPresenter' -import { DeepChatAgentPresenter } from './deepchatAgentPresenter' +import { AgentSessionPresenter } from './agentSessionPresenter' +import { AgentRuntimePresenter } from './agentRuntimePresenter' import { ProjectPresenter } from './projectPresenter' import { RemoteControlPresenter } from './remoteControlPresenter' import type { RemoteControlPresenterLike } from './remoteControlPresenter/interface' import { AgentRepository } from './agentRepository' import type { SQLitePresenter } from './sqlitePresenter' import { normalizeDeepChatSubagentSlots } from '@shared/lib/deepchatSubagents' -import { subscribeDeepChatInternalSessionUpdates } from './deepchatAgentPresenter/internalSessionEvents' +import { subscribeDeepChatInternalSessionUpdates } from './agentRuntimePresenter/internalSessionEvents' +import type { ConfigQueryPort, SessionRuntimePort } from './runtimePorts' // IPC调用上下文接口 interface IPCCallContext { @@ -113,7 +114,7 @@ export class Presenter implements IPresenter { 'toolPresenter', 'skillPresenter', 'skillSyncPresenter', - 'newAgentPresenter', + 'agentSessionPresenter', 'projectPresenter' ]) @@ -165,7 +166,7 @@ export class Presenter implements IPresenter { lifecycleManager: ILifecycleManager skillPresenter: ISkillPresenter skillSyncPresenter: ISkillSyncPresenter - newAgentPresenter: INewAgentPresenter + agentSessionPresenter: IAgentSessionPresenter projectPresenter: IProjectPresenter hooksNotifications: HooksNotificationsService commandPermissionService: CommandPermissionService @@ -247,7 +248,7 @@ export class Presenter implements IPresenter { const agentToolRuntime: AgentToolRuntimePort = { resolveConversationWorkdir: async (conversationId) => { try { - const session = await this.newAgentPresenter?.getSession(conversationId) + const session = await this.agentSessionPresenter?.getSession(conversationId) const normalized = session?.projectDir?.trim() if (normalized) { return normalized @@ -262,7 +263,7 @@ export class Presenter implements IPresenter { return null }, resolveConversationSessionInfo: async (conversationId) => { - const session = await this.newAgentPresenter?.getSession(conversationId) + const session = await this.agentSessionPresenter?.getSession(conversationId) if (!session) { return null } @@ -270,16 +271,16 @@ export class Presenter implements IPresenter { const agent = await this.configPresenter.getAgent(session.agentId) const agentType = await this.configPresenter.getAgentType(session.agentId) const permissionMode = - typeof this.newAgentPresenter?.getPermissionMode === 'function' - ? await this.newAgentPresenter.getPermissionMode(session.id) + typeof this.agentSessionPresenter?.getPermissionMode === 'function' + ? await this.agentSessionPresenter.getPermissionMode(session.id) : 'full_access' const generationSettings = - typeof this.newAgentPresenter?.getSessionGenerationSettings === 'function' - ? await this.newAgentPresenter.getSessionGenerationSettings(session.id) + typeof this.agentSessionPresenter?.getSessionGenerationSettings === 'function' + ? await this.agentSessionPresenter.getSessionGenerationSettings(session.id) : null const disabledAgentTools = - typeof this.newAgentPresenter?.getSessionDisabledAgentTools === 'function' - ? await this.newAgentPresenter.getSessionDisabledAgentTools(session.id) + typeof this.agentSessionPresenter?.getSessionDisabledAgentTools === 'function' + ? await this.agentSessionPresenter.getSessionDisabledAgentTools(session.id) : [] const activeSkills = await this.skillPresenter.getActiveSkills(session.id) const availableSubagentSlots = @@ -309,12 +310,12 @@ export class Presenter implements IPresenter { } }, createSubagentSession: async (input) => { - const newAgentPresenter = this.newAgentPresenter as INewAgentPresenter & { + const agentSessionPresenter = this.agentSessionPresenter as IAgentSessionPresenter & { createSubagentSession?: (createInput: typeof input) => Promise<{ id: string } | null> } - const created = await newAgentPresenter.createSubagentSession?.(input) + const created = await agentSessionPresenter.createSubagentSession?.(input) if (!created?.id) { return null } @@ -322,10 +323,10 @@ export class Presenter implements IPresenter { return await agentToolRuntime.resolveConversationSessionInfo(created.id) }, sendConversationMessage: async (conversationId, content) => { - await this.newAgentPresenter.sendMessage(conversationId, content) + await this.agentSessionPresenter.sendMessage(conversationId, content) }, cancelConversation: async (conversationId) => { - await this.newAgentPresenter.cancelGeneration(conversationId) + await this.agentSessionPresenter.cancelGeneration(conversationId) }, subscribeDeepChatSessionUpdates: (listener) => subscribeDeepChatInternalSessionUpdates(listener), @@ -376,7 +377,7 @@ export class Presenter implements IPresenter { const skillSessionStatePort: SkillSessionStatePort = { hasNewSession: async (conversationId) => { try { - return Boolean(await this.newAgentPresenter?.getSession(conversationId)) + return Boolean(await this.agentSessionPresenter?.getSession(conversationId)) } catch { return false } @@ -392,10 +393,12 @@ export class Presenter implements IPresenter { sqlitePresenter.newEnvironmentsTable?.syncForSession(conversationId) }, repairImportedLegacySessionSkills: async (conversationId) => { - const newAgentPresenter = this.newAgentPresenter as INewAgentPresenter & { + const agentSessionPresenter = this.agentSessionPresenter as IAgentSessionPresenter & { repairImportedLegacySessionSkills?: (sessionId: string) => Promise } - return (await newAgentPresenter.repairImportedLegacySessionSkills?.(conversationId)) ?? [] + return ( + (await agentSessionPresenter.repairImportedLegacySessionSkills?.(conversationId)) ?? [] + ) } } @@ -411,21 +414,84 @@ export class Presenter implements IPresenter { getMessage: async () => null }) const newSessionHooksBridge = new NewSessionHooksBridge(this.hooksNotifications) + const configQueryPort: ConfigQueryPort = { + getProviderModels: (providerId) => this.configPresenter.getProviderModels?.(providerId) ?? [], + getCustomModels: (providerId) => this.configPresenter.getCustomModels?.(providerId) ?? [], + getAgentType: async (agentId) => await this.configPresenter.getAgentType(agentId) + } + const sessionRuntimePort: SessionRuntimePort = { + refreshSessionUi: () => { + try { + void this.floatingButtonPresenter.refreshWidgetState() + } catch (error) { + console.warn('[Presenter] Failed to refresh floating widget state:', error) + } + }, + clearSessionPermissions: (sessionId) => { + this.commandPermissionService.clearConversation(sessionId) + this.filePermissionService.clearConversation(sessionId) + this.settingsPermissionService.clearConversation(sessionId) + }, + approvePermission: async (sessionId, permission) => { + const permissionType = permission.permissionType + const serverName = permission.serverName || '' + const toolName = permission.toolName || '' + + if (permissionType === 'command') { + const command = permission.command || permission.commandInfo?.command || '' + const signature = + permission.commandSignature || + permission.commandInfo?.signature || + (command ? this.commandPermissionService.extractCommandSignature(command) : '') + if (signature) { + this.commandPermissionService.approve(sessionId, signature, false) + } + return + } + + if ( + serverName === 'agent-filesystem' && + Array.isArray(permission.paths) && + permission.paths.length > 0 + ) { + this.filePermissionService.approve(sessionId, permission.paths, false) + return + } + + if (serverName === 'deepchat-settings' && toolName) { + this.settingsPermissionService.approve(sessionId, toolName, false) + return + } + + if ( + serverName && + (permissionType === 'read' || permissionType === 'write' || permissionType === 'all') + ) { + await this.mcpPresenter.grantPermission(serverName, permissionType, false, sessionId) + } + } + } // Initialize new agent architecture presenters - const deepchatAgentPresenter = new DeepChatAgentPresenter( + const agentRuntimePresenter = new AgentRuntimePresenter( this.llmproviderPresenter as unknown as ILlmProviderPresenter, this.configPresenter, this.sqlitePresenter as unknown as import('./sqlitePresenter').SQLitePresenter, this.toolPresenter, - newSessionHooksBridge + newSessionHooksBridge, + { + configQueryPort, + sessionRuntimePort, + skillPresenter: this.skillPresenter + } ) - this.newAgentPresenter = new NewAgentPresenter( - deepchatAgentPresenter, + this.agentSessionPresenter = new AgentSessionPresenter( + agentRuntimePresenter, this.llmproviderPresenter as unknown as ILlmProviderPresenter, this.configPresenter, this.sqlitePresenter as unknown as import('./sqlitePresenter').SQLitePresenter, - this.skillPresenter + this.skillPresenter, + sessionRuntimePort ) this.projectPresenter = new ProjectPresenter( this.sqlitePresenter as unknown as import('./sqlitePresenter').SQLitePresenter, @@ -433,8 +499,8 @@ export class Presenter implements IPresenter { ) this.#remoteControlPresenter = new RemoteControlPresenter({ configPresenter: this.configPresenter, - newAgentPresenter: this.newAgentPresenter, - deepchatAgentPresenter, + agentSessionPresenter: this.agentSessionPresenter, + agentRuntimePresenter, windowPresenter: this.windowPresenter, tabPresenter: this.tabPresenter, getHooksNotificationsConfig: () => this.configPresenter.getHooksNotificationsConfig(), @@ -444,10 +510,10 @@ export class Presenter implements IPresenter { }) this.#remoteControlBridge = this.#remoteControlPresenter - // Update hooksNotifications with actual dependencies now that newAgentPresenter is ready + // Update hooksNotifications with actual dependencies now that agentSessionPresenter is ready this.hooksNotifications = new HooksNotificationsService(this.configPresenter, { - getSession: this.newAgentPresenter.getSession.bind(this.newAgentPresenter), - getMessage: this.newAgentPresenter.getMessage.bind(this.newAgentPresenter) + getSession: this.agentSessionPresenter.getSession.bind(this.agentSessionPresenter), + getMessage: this.agentSessionPresenter.getMessage.bind(this.agentSessionPresenter) }) this.setupEventBus() // 设置事件总线监听 diff --git a/src/main/presenter/lifecyclePresenter/hooks/after-start/legacyImportHook.ts b/src/main/presenter/lifecyclePresenter/hooks/after-start/legacyImportHook.ts index 4ceae3b40..b5f695aad 100644 --- a/src/main/presenter/lifecyclePresenter/hooks/after-start/legacyImportHook.ts +++ b/src/main/presenter/lifecyclePresenter/hooks/after-start/legacyImportHook.ts @@ -12,15 +12,15 @@ export const legacyImportHook: LifecycleHook = { throw new Error('legacyImportHook: Presenter not initialized') } - const newAgentPresenter = presenter.newAgentPresenter as unknown as { + const agentSessionPresenter = presenter.agentSessionPresenter as unknown as { startLegacyImport?: () => Promise } - if (!newAgentPresenter.startLegacyImport) { + if (!agentSessionPresenter.startLegacyImport) { return } // Fire and forget to avoid blocking app startup. - void newAgentPresenter.startLegacyImport().catch((error) => { + void agentSessionPresenter.startLegacyImport().catch((error) => { console.error('legacyImportHook: failed to start legacy import task:', error) }) } diff --git a/src/main/presenter/lifecyclePresenter/hooks/after-start/rtkHealthCheckHook.ts b/src/main/presenter/lifecyclePresenter/hooks/after-start/rtkHealthCheckHook.ts index 7f2deb8b9..3c0f7f495 100644 --- a/src/main/presenter/lifecyclePresenter/hooks/after-start/rtkHealthCheckHook.ts +++ b/src/main/presenter/lifecyclePresenter/hooks/after-start/rtkHealthCheckHook.ts @@ -12,14 +12,14 @@ export const rtkHealthCheckHook: LifecycleHook = { throw new Error('rtkHealthCheckHook: Presenter not initialized') } - const newAgentPresenter = presenter.newAgentPresenter as unknown as { + const agentSessionPresenter = presenter.agentSessionPresenter as unknown as { startRtkHealthCheck?: () => Promise } - if (!newAgentPresenter.startRtkHealthCheck) { + if (!agentSessionPresenter.startRtkHealthCheck) { return } - void newAgentPresenter.startRtkHealthCheck().catch((error) => { + void agentSessionPresenter.startRtkHealthCheck().catch((error) => { console.error('rtkHealthCheckHook: failed to start RTK health check:', error) }) } diff --git a/src/main/presenter/lifecyclePresenter/hooks/after-start/usageStatsBackfillHook.ts b/src/main/presenter/lifecyclePresenter/hooks/after-start/usageStatsBackfillHook.ts index 178ea261f..6b3b06a42 100644 --- a/src/main/presenter/lifecyclePresenter/hooks/after-start/usageStatsBackfillHook.ts +++ b/src/main/presenter/lifecyclePresenter/hooks/after-start/usageStatsBackfillHook.ts @@ -12,14 +12,14 @@ export const usageStatsBackfillHook: LifecycleHook = { throw new Error('usageStatsBackfillHook: Presenter not initialized') } - const newAgentPresenter = presenter.newAgentPresenter as unknown as { + const agentSessionPresenter = presenter.agentSessionPresenter as unknown as { startUsageStatsBackfill?: () => Promise } - if (!newAgentPresenter.startUsageStatsBackfill) { + if (!agentSessionPresenter.startUsageStatsBackfill) { return } - void newAgentPresenter.startUsageStatsBackfill().catch((error) => { + void agentSessionPresenter.startUsageStatsBackfill().catch((error) => { console.error('usageStatsBackfillHook: failed to start usage stats backfill:', error) }) } diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts index 85a445624..74306ec15 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts @@ -266,11 +266,11 @@ export class ConversationSearchServer { // 获取对话历史 private async getConversationHistory(conversationId: string, includeSystem: boolean = false) { try { - const session = await presenter.newAgentPresenter.getSession(conversationId) + const session = await presenter.agentSessionPresenter.getSession(conversationId) if (!session) { throw new Error(`Session not found: ${conversationId}`) } - const records = await presenter.newAgentPresenter.getMessages(conversationId) + const records = await presenter.agentSessionPresenter.getMessages(conversationId) const filteredMessages = includeSystem ? records diff --git a/src/main/presenter/mcpPresenter/toolManager.ts b/src/main/presenter/mcpPresenter/toolManager.ts index 282c04be9..134895241 100644 --- a/src/main/presenter/mcpPresenter/toolManager.ts +++ b/src/main/presenter/mcpPresenter/toolManager.ts @@ -281,7 +281,7 @@ export class ToolManager { } try { - const session = await presenter.newAgentPresenter.getSession(sessionId) + const session = await presenter.agentSessionPresenter.getSession(sessionId) const agentId = session?.agentId?.trim() const providerId = session?.providerId?.trim() if (session && providerId === 'acp' && agentId) { diff --git a/src/main/presenter/remoteControlPresenter/index.ts b/src/main/presenter/remoteControlPresenter/index.ts index 96965ff75..8f0c69343 100644 --- a/src/main/presenter/remoteControlPresenter/index.ts +++ b/src/main/presenter/remoteControlPresenter/index.ts @@ -683,8 +683,8 @@ export class RemoteControlPresenter { return new RemoteConversationRunner( { configPresenter: this.deps.configPresenter, - newAgentPresenter: this.deps.newAgentPresenter, - deepchatAgentPresenter: this.deps.deepchatAgentPresenter, + agentSessionPresenter: this.deps.agentSessionPresenter, + agentRuntimePresenter: this.deps.agentRuntimePresenter, windowPresenter: this.deps.windowPresenter, tabPresenter: this.deps.tabPresenter, resolveDefaultAgentId: async () => diff --git a/src/main/presenter/remoteControlPresenter/interface.ts b/src/main/presenter/remoteControlPresenter/interface.ts index cdcd74b01..63b130564 100644 --- a/src/main/presenter/remoteControlPresenter/interface.ts +++ b/src/main/presenter/remoteControlPresenter/interface.ts @@ -2,18 +2,18 @@ import type { HookTestResult, HooksNotificationsSettings } from '@shared/hooksNo import type { FeishuRemoteSettings, IConfigPresenter, - INewAgentPresenter, + IAgentSessionPresenter, IRemoteControlPresenter, ITabPresenter, IWindowPresenter, TelegramRemoteSettings } from '@shared/presenter' -import type { DeepChatAgentPresenter } from '../deepchatAgentPresenter' +import type { AgentRuntimePresenter } from '../agentRuntimePresenter' export interface RemoteControlPresenterDeps { configPresenter: IConfigPresenter - newAgentPresenter: INewAgentPresenter - deepchatAgentPresenter: DeepChatAgentPresenter + agentSessionPresenter: IAgentSessionPresenter + agentRuntimePresenter: AgentRuntimePresenter windowPresenter: IWindowPresenter tabPresenter: ITabPresenter getHooksNotificationsConfig: () => HooksNotificationsSettings diff --git a/src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts b/src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts index 71196f548..82913a99b 100644 --- a/src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts +++ b/src/main/presenter/remoteControlPresenter/services/remoteConversationRunner.ts @@ -7,12 +7,12 @@ import type { import type { SearchResult } from '@shared/types/core/search' import type { IConfigPresenter, - INewAgentPresenter, + IAgentSessionPresenter, RemoteChannel, ITabPresenter, IWindowPresenter } from '@shared/presenter' -import type { DeepChatAgentPresenter } from '../../deepchatAgentPresenter' +import type { AgentRuntimePresenter } from '../../agentRuntimePresenter' import { TELEGRAM_RECENT_SESSION_LIMIT, TELEGRAM_STREAM_POLL_INTERVAL_MS, @@ -77,8 +77,8 @@ export type RemoteOpenSessionResult = type RemoteConversationRunnerDeps = { configPresenter: IConfigPresenter - newAgentPresenter: INewAgentPresenter - deepchatAgentPresenter: DeepChatAgentPresenter + agentSessionPresenter: IAgentSessionPresenter + agentRuntimePresenter: AgentRuntimePresenter windowPresenter: IWindowPresenter tabPresenter: ITabPresenter resolveDefaultAgentId: () => Promise @@ -113,7 +113,7 @@ export class RemoteConversationRunner { ) } - const session = await this.deps.newAgentPresenter.createDetachedSession({ + const session = await this.deps.agentSessionPresenter.createDetachedSession({ title: title?.trim() || 'New Chat', agentId, ...(agentType === 'acp' @@ -138,7 +138,7 @@ export class RemoteConversationRunner { return null } - const session = await this.deps.newAgentPresenter.getSession(binding.sessionId) + const session = await this.deps.agentSessionPresenter.getSession(binding.sessionId) if (!session) { this.bindingStore.clearBinding(endpointKey) return null @@ -161,7 +161,7 @@ export class RemoteConversationRunner { async listSessions(endpointKey: string): Promise { const agentId = await this.resolveSessionListAgentId(endpointKey) - const sessions = await this.deps.newAgentPresenter.getSessionList({ + const sessions = await this.deps.agentSessionPresenter.getSessionList({ agentId }) const sorted = [...sessions] @@ -189,7 +189,7 @@ export class RemoteConversationRunner { throw new Error('Session index is out of range.') } - const session = await this.deps.newAgentPresenter.getSession(sessionId) + const session = await this.deps.agentSessionPresenter.getSession(sessionId) if (!session) { throw new Error('Selected session no longer exists.') } @@ -231,7 +231,7 @@ export class RemoteConversationRunner { throw new Error('No bound session. Send a message, /new, or /use first.') } - return await this.deps.newAgentPresenter.setSessionModel(session.id, providerId, modelId) + return await this.deps.agentSessionPresenter.setSessionModel(session.id, providerId, modelId) } async sendText( @@ -240,12 +240,12 @@ export class RemoteConversationRunner { bindingMeta?: RemoteEndpointBindingMeta ): Promise { const session = await this.ensureBoundSession(endpointKey, bindingMeta) - const beforeMessages = await this.deps.newAgentPresenter.getMessages(session.id) + const beforeMessages = await this.deps.agentSessionPresenter.getMessages(session.id) const lastOrderSeq = beforeMessages.at(-1)?.orderSeq ?? 0 const previousActiveEventId = - this.deps.deepchatAgentPresenter.getActiveGeneration(session.id)?.eventId ?? null + this.deps.agentRuntimePresenter.getActiveGeneration(session.id)?.eventId ?? null - await this.deps.newAgentPresenter.sendMessage(session.id, text) + await this.deps.agentSessionPresenter.sendMessage(session.id, text) const seededMessage = await this.waitForAssistantMessage(session.id, lastOrderSeq, 800, { ignoreMessageId: previousActiveEventId @@ -293,7 +293,7 @@ export class RemoteConversationRunner { throw new Error('No pending interaction was found.') } - const result = await this.deps.newAgentPresenter.respondToolInteraction( + const result = await this.deps.agentSessionPresenter.respondToolInteraction( session.id, interaction.messageId, interaction.toolCallId, @@ -327,14 +327,14 @@ export class RemoteConversationRunner { const activeEventId = this.bindingStore.getActiveEvent(endpointKey) ?? - this.deps.deepchatAgentPresenter.getActiveGeneration(session.id)?.eventId ?? + this.deps.agentRuntimePresenter.getActiveGeneration(session.id)?.eventId ?? null if (!activeEventId) { return false } - const stopped = await this.deps.deepchatAgentPresenter.cancelGenerationByEventId( + const stopped = await this.deps.agentRuntimePresenter.cancelGenerationByEventId( session.id, activeEventId ) @@ -359,7 +359,7 @@ export class RemoteConversationRunner { } } - await this.deps.newAgentPresenter.activateSession(window.webContents.id, session.id) + await this.deps.agentSessionPresenter.activateSession(window.webContents.id, session.id) this.deps.windowPresenter.show(window.id, true) return { status: 'ok', @@ -382,7 +382,7 @@ export class RemoteConversationRunner { const activeEventId = this.bindingStore.getActiveEvent(endpointKey) ?? - this.deps.deepchatAgentPresenter.getActiveGeneration(session.id)?.eventId ?? + this.deps.agentRuntimePresenter.getActiveGeneration(session.id)?.eventId ?? null return { @@ -454,7 +454,7 @@ export class RemoteConversationRunner { ignoreMessageId: string | null } ): Promise { - const session = await this.deps.newAgentPresenter.getSession(sessionId) + const session = await this.deps.agentSessionPresenter.getSession(sessionId) if (!session) { this.bindingStore.clearBinding(endpointKey) return { @@ -470,7 +470,7 @@ export class RemoteConversationRunner { } } - const activeGeneration = this.deps.deepchatAgentPresenter.getActiveGeneration(sessionId) + const activeGeneration = this.deps.agentRuntimePresenter.getActiveGeneration(sessionId) const trackedMessage = await this.resolveTrackedAssistantMessage( sessionId, tracking, @@ -547,12 +547,12 @@ export class RemoteConversationRunner { } private async loadSearchResults(messageId: string, searchId?: string): Promise { - if (typeof this.deps.newAgentPresenter.getSearchResults !== 'function') { + if (typeof this.deps.agentSessionPresenter.getSearchResults !== 'function') { return [] } try { - return await this.deps.newAgentPresenter.getSearchResults(messageId, searchId) + return await this.deps.agentSessionPresenter.getSearchResults(messageId, searchId) } catch (error) { console.warn('[RemoteConversationRunner] Failed to load search results:', { messageId, @@ -573,9 +573,9 @@ export class RemoteConversationRunner { ): Promise { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { - const activeGeneration = this.deps.deepchatAgentPresenter.getActiveGeneration(sessionId) + const activeGeneration = this.deps.agentRuntimePresenter.getActiveGeneration(sessionId) if (activeGeneration?.eventId && activeGeneration.eventId !== options?.ignoreMessageId) { - const message = await this.deps.newAgentPresenter.getMessage(activeGeneration.eventId) + const message = await this.deps.agentSessionPresenter.getMessage(activeGeneration.eventId) if (message?.role === 'assistant') { return message } @@ -611,7 +611,7 @@ export class RemoteConversationRunner { continue } - const message = await this.deps.newAgentPresenter.getMessage(messageId) + const message = await this.deps.agentSessionPresenter.getMessage(messageId) if (message?.role === 'assistant') { return message } @@ -629,7 +629,7 @@ export class RemoteConversationRunner { afterOrderSeq: number, ignoreMessageId?: string | null ): Promise { - const messages = await this.deps.newAgentPresenter.getMessages(sessionId) + const messages = await this.deps.agentSessionPresenter.getMessages(sessionId) const assistants = messages.filter( (message) => message.role === 'assistant' && @@ -662,7 +662,7 @@ export class RemoteConversationRunner { private async getCurrentPendingInteractionDetails( sessionId: string ): Promise { - const messages = await this.deps.newAgentPresenter.getMessages(sessionId) + const messages = await this.deps.agentSessionPresenter.getMessages(sessionId) const assistants = [...messages] .filter((message) => message.role === 'assistant') .sort((left, right) => right.orderSeq - left.orderSeq) diff --git a/src/main/presenter/runtimePorts.ts b/src/main/presenter/runtimePorts.ts new file mode 100644 index 000000000..133dd251d --- /dev/null +++ b/src/main/presenter/runtimePorts.ts @@ -0,0 +1,37 @@ +type ModelIdentity = { + id: string + name?: string | null +} + +export type SessionPermissionRequest = { + permissionType: 'read' | 'write' | 'all' | 'command' + serverName?: string + toolName?: string + command?: string + commandSignature?: string + paths?: string[] + commandInfo?: { + command: string + riskLevel: 'low' | 'medium' | 'high' | 'critical' + suggestion: string + signature?: string + baseCommand?: string + } +} + +export interface ConfigQueryPort { + getProviderModels(providerId: string): ModelIdentity[] + getCustomModels(providerId: string): ModelIdentity[] + getAgentType(agentId: string): Promise<'deepchat' | 'acp' | null> +} + +export interface SessionRuntimePort { + refreshSessionUi(): void + clearSessionPermissions(sessionId: string): void + approvePermission(sessionId: string, permission: SessionPermissionRequest): Promise +} + +export interface WindowRoutingPort { + createSettingsWindow(): Promise + sendToWindow(windowId: number, channel: string, ...args: unknown[]): void +} diff --git a/src/main/presenter/sqlitePresenter/index.ts b/src/main/presenter/sqlitePresenter/index.ts index e1cf019fb..e3ff5e9b6 100644 --- a/src/main/presenter/sqlitePresenter/index.ts +++ b/src/main/presenter/sqlitePresenter/index.ts @@ -323,7 +323,7 @@ export class SQLitePresenter implements ISQLitePresenter { importedMessages: number importedSearchResults: number }> { - const { LegacyChatImportService } = await import('../newAgentPresenter/legacyImportService') + const { LegacyChatImportService } = await import('../agentSessionPresenter/legacyImportService') const service = new LegacyChatImportService(this) return await service.importFromSourceDb(sourceDbPath, mode) } diff --git a/src/main/presenter/toolPresenter/runtimePorts.ts b/src/main/presenter/toolPresenter/runtimePorts.ts index 12f52266e..84d7cd529 100644 --- a/src/main/presenter/toolPresenter/runtimePorts.ts +++ b/src/main/presenter/toolPresenter/runtimePorts.ts @@ -13,7 +13,7 @@ import type { SessionKind } from '@shared/types/agent-interface' import type { ISkillPresenter } from '@shared/types/skill' -import type { DeepChatInternalSessionUpdate } from '../deepchatAgentPresenter/internalSessionEvents' +import type { DeepChatInternalSessionUpdate } from '../agentRuntimePresenter/internalSessionEvents' export interface ConversationSessionInfo { sessionId: string diff --git a/src/renderer/settings/components/DashboardSettings.vue b/src/renderer/settings/components/DashboardSettings.vue index 9a3278e77..f4ebcd474 100644 --- a/src/renderer/settings/components/DashboardSettings.vue +++ b/src/renderer/settings/components/DashboardSettings.vue @@ -718,7 +718,7 @@ type NostalgiaDetailItem = { } const { t, locale } = useI18n() -const newAgentPresenter = usePresenter('newAgentPresenter') +const agentSessionPresenter = usePresenter('agentSessionPresenter') const isLoading = ref(true) const isRetryingRtk = ref(false) @@ -1044,7 +1044,7 @@ async function loadDashboard(): Promise { try { isLoading.value = true errorMessage.value = '' - const nextDashboard = await newAgentPresenter.getUsageDashboard() + const nextDashboard = await agentSessionPresenter.getUsageDashboard() if (!isDashboardMounted) { return } @@ -1073,7 +1073,7 @@ async function retryRtkHealthCheck(): Promise { try { isRetryingRtk.value = true - await newAgentPresenter.retryRtkHealthCheck() + await agentSessionPresenter.retryRtkHealthCheck() await loadDashboard() } catch (error) { errorMessage.value = diff --git a/src/renderer/settings/components/RemoteSettings.vue b/src/renderer/settings/components/RemoteSettings.vue index 069c2a943..07677bade 100644 --- a/src/renderer/settings/components/RemoteSettings.vue +++ b/src/renderer/settings/components/RemoteSettings.vue @@ -896,7 +896,7 @@ import type { const channels: RemoteChannel[] = ['telegram', 'feishu'] const remoteControlPresenter = useRemoteControlPresenter() -const newAgentPresenter = usePresenter('newAgentPresenter') +const agentSessionPresenter = usePresenter('agentSessionPresenter') const projectPresenter = usePresenter('projectPresenter') const { t } = useI18n() const { toast } = useToast() @@ -1331,7 +1331,7 @@ const refreshPairingSnapshot = async (channel: RemoteChannel): Promise { - availableAgents.value = await newAgentPresenter.getAgents() + availableAgents.value = await agentSessionPresenter.getAgents() } const loadRecentProjects = async () => { diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 16c4f6b8d..62187761e 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -8,7 +8,6 @@ import { useSessionStore } from '@/stores/ui/session' import { useAgentStore } from '@/stores/ui/agent' import { useDraftStore, type StartDeeplinkPayload } from '@/stores/ui/draft' import { usePageRouterStore } from '@/stores/ui/pageRouter' -import { DEEPLINK_EVENTS, NOTIFICATION_EVENTS, SHORTCUT_EVENTS } from './events' import { Toaster } from '@shadcn/components/ui/sonner' import { useToast } from '@/components/use-toast' import { useUiSettingsStore } from '@/stores/uiSettingsStore' @@ -28,6 +27,7 @@ import { useDeviceVersion } from '@/composables/useDeviceVersion' import WindowSideBar from './components/WindowSideBar.vue' import SpotlightOverlay from '@/components/spotlight/SpotlightOverlay.vue' import { useSpotlightStore } from '@/stores/ui/spotlight' +import { useAppIpcRuntime } from '@/composables/useAppIpcRuntime' const DEV_WELCOME_OVERRIDE_KEY = '__deepchat_dev_force_welcome' @@ -291,9 +291,44 @@ const handleStartDeeplink = (_event: unknown, payload?: Omit
Attach
Voice input
{{ msg.content }}
Claude 4 Sonnet
- {{ t('welcome.page.description') }} -
{{ t('welcome.page.acpTitle') }}
{{ t('welcome.page.acpDescription') }}
- {{ searchQuery ? t('chat.navigation.noResults') : t('chat.navigation.noMessages') }} -