From 1b19bd5e6b7cf0a15f41c6515a46098d24bfebf1 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sun, 8 Mar 2026 16:43:45 +0800 Subject: [PATCH 1/3] fix: session hooks and notifactions --- src/main/events.ts | 1 - .../agentPresenter/acp/chatSettingsTools.ts | 9 +- src/main/presenter/agentPresenter/index.ts | 13 - .../agentPresenter/loop/toolCallProcessor.ts | 50 ---- .../streaming/llmEventHandler.ts | 109 +------ .../streaming/streamGenerationHandler.ts | 25 -- src/main/presenter/configPresenter/index.ts | 14 - .../deepchatAgentPresenter/dispatch.ts | 42 ++- .../presenter/deepchatAgentPresenter/index.ts | 265 +++++++++++++++++- .../deepchatAgentPresenter/process.ts | 37 ++- .../presenter/deepchatAgentPresenter/types.ts | 28 ++ .../presenter/hooksNotifications/index.ts | 59 ++-- .../hooksNotifications/newSessionBridge.ts | 42 +++ src/main/presenter/index.ts | 18 +- src/main/presenter/newAgentPresenter/index.ts | 6 + src/renderer/public/sounds/sfx-fc.mp3 | Bin 10989 -> 0 bytes src/renderer/public/sounds/sfx-typing.mp3 | Bin 4461 -> 0 bytes .../settings/components/CommonSettings.vue | 14 - src/renderer/src/events.ts | 1 - src/renderer/src/i18n/da-DK/settings.json | 1 - src/renderer/src/i18n/en-US/settings.json | 1 - src/renderer/src/i18n/fa-IR/settings.json | 1 - src/renderer/src/i18n/fr-FR/settings.json | 1 - src/renderer/src/i18n/he-IL/settings.json | 1 - src/renderer/src/i18n/ja-JP/settings.json | 1 - src/renderer/src/i18n/ko-KR/settings.json | 1 - src/renderer/src/i18n/pt-BR/settings.json | 1 - src/renderer/src/i18n/ru-RU/settings.json | 1 - src/renderer/src/i18n/zh-CN/settings.json | 1 - src/renderer/src/i18n/zh-HK/settings.json | 1 - src/renderer/src/i18n/zh-TW/settings.json | 1 - src/renderer/src/stores/sound.ts | 57 ---- src/shared/types/agent-interface.d.ts | 1 + src/shared/types/chatSettings.ts | 9 +- .../types/presenters/legacy.presenters.d.ts | 3 - src/types/i18n.d.ts | 1 - .../agentPresenter/chatSettingsTools.test.ts | 19 +- .../deepchatAgentPresenter.test.ts | 116 +++++++- .../newAgentPresenter/integration.test.ts | 83 ++++++ 39 files changed, 675 insertions(+), 359 deletions(-) create mode 100644 src/main/presenter/hooksNotifications/newSessionBridge.ts delete mode 100644 src/renderer/public/sounds/sfx-fc.mp3 delete mode 100644 src/renderer/public/sounds/sfx-typing.mp3 delete mode 100644 src/renderer/src/stores/sound.ts diff --git a/src/main/events.ts b/src/main/events.ts index 1dee34b2c..3877802a8 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -24,7 +24,6 @@ export const CONFIG_EVENTS = { AUTO_SCROLL_CHANGED: 'config:auto-scroll-changed', NOTIFICATIONS_CHANGED: 'config:notifications-changed', CONTENT_PROTECTION_CHANGED: 'config:content-protection-changed', - SOUND_ENABLED_CHANGED: 'config:sound-enabled-changed', // 新增:声音开关变更事件 COPY_WITH_COT_CHANGED: 'config:copy-with-cot-enabled-changed', TRACE_DEBUG_CHANGED: 'config:trace-debug-changed', // Trace 调试功能开关变更事件 PROXY_RESOLVED: 'config:proxy-resolved', diff --git a/src/main/presenter/agentPresenter/acp/chatSettingsTools.ts b/src/main/presenter/agentPresenter/acp/chatSettingsTools.ts index ec81a3fa8..ee856bb96 100644 --- a/src/main/presenter/agentPresenter/acp/chatSettingsTools.ts +++ b/src/main/presenter/agentPresenter/acp/chatSettingsTools.ts @@ -45,7 +45,7 @@ const FONT_SIZE_LEVELS = [0, 1, 2, 3, 4] as const const toggleSchema = z .object({ - setting: z.enum(['soundEnabled', 'copyWithCotEnabled']).describe('Toggle setting id.'), + setting: z.enum(['copyWithCotEnabled']).describe('Toggle setting id.'), enabled: z.boolean().describe('Enable or disable the setting.') }) .strict() @@ -192,8 +192,6 @@ export class ChatSettingsToolHandler { private getCurrentValue(key: string): ChatSettingValue | undefined { const configPresenter = this.options.configPresenter switch (key) { - case 'soundEnabled': - return configPresenter.getSoundEnabled() case 'copyWithCotEnabled': return configPresenter.getCopyWithCotEnabled() case 'language': @@ -224,9 +222,6 @@ export class ChatSettingsToolHandler { try { switch (setting) { - case 'soundEnabled': - configPresenter.setSoundEnabled(enabled) - break case 'copyWithCotEnabled': configPresenter.setCopyWithCotEnabled(enabled) break @@ -409,7 +404,7 @@ export const buildChatSettingsToolDefinitions = (allowedTools: string[]): MCPToo type: 'function', function: { name: CHAT_SETTINGS_TOOL_NAMES.toggle, - description: 'Toggle a DeepChat setting (sound or copy COT).', + description: 'Toggle a DeepChat setting.', parameters: zodToJsonSchema(toggleSchema) as { type: string properties: Record diff --git a/src/main/presenter/agentPresenter/index.ts b/src/main/presenter/agentPresenter/index.ts index cf027cbc5..73632d82c 100644 --- a/src/main/presenter/agentPresenter/index.ts +++ b/src/main/presenter/agentPresenter/index.ts @@ -137,19 +137,6 @@ export class AgentPresenter implements IAgentPresenter { false, this.buildMessageMetadata(conversation) ) - try { - const promptPreview = this.extractUserMessageText(content) - presenter.hooksNotifications.dispatchEvent('UserPromptSubmit', { - conversationId: agentId, - messageId: userMessage.id, - promptPreview, - providerId: conversation.settings.providerId, - modelId: conversation.settings.modelId - }) - } catch (error) { - console.warn('[AgentPresenter] Failed to dispatch UserPromptSubmit hook:', error) - } - try { await this.resolvePendingQuestionIfNeeded(agentId, userMessage.id, content) } catch (error) { diff --git a/src/main/presenter/agentPresenter/loop/toolCallProcessor.ts b/src/main/presenter/agentPresenter/loop/toolCallProcessor.ts index 97a561ee7..9f3f533ae 100644 --- a/src/main/presenter/agentPresenter/loop/toolCallProcessor.ts +++ b/src/main/presenter/agentPresenter/loop/toolCallProcessor.ts @@ -11,7 +11,6 @@ import path from 'path' import { isNonRetryableError } from './errorClassification' import { resolveToolOffloadPath } from '../../sessionPresenter/sessionPaths' import { parseQuestionToolArgs, QUESTION_TOOL_NAME } from '../tools/questionTool' -import { presenter } from '@/presenter' interface ToolCallProcessorOptions { getAllToolDefinitions: (context: ToolCallExecutionContext) => Promise @@ -104,8 +103,6 @@ export class ToolCallProcessor { ): AsyncGenerator { let toolCallCount = context.currentToolCallCount let needContinueConversation = context.toolCalls.length > 0 - const shouldDispatchToolHooks = context.providerId === 'acp' - let toolDefinitions = await this.options.getAllToolDefinitions(context) // Step 1: Pre-check all tool permissions in batch @@ -311,21 +308,6 @@ export class ToolCallProcessor { } try { - if (shouldDispatchToolHooks) { - try { - presenter.hooksNotifications.dispatchEvent('PreToolUse', { - conversationId: context.conversationId, - tool: { - callId: toolCall.id, - name: toolCall.name, - params: toolCall.arguments - } - }) - } catch (error) { - console.warn('[ToolCallProcessor] Failed to dispatch PreToolUse hook:', error) - } - } - const toolResponse = await this.options.callTool(mcpToolInput) const requiresPermission = Boolean(toolResponse.rawData?.requiresPermission) @@ -369,22 +351,6 @@ export class ToolCallProcessor { toolCall.name ) - if (shouldDispatchToolHooks) { - try { - presenter.hooksNotifications.dispatchEvent('PostToolUse', { - conversationId: context.conversationId, - tool: { - callId: toolCall.id, - name: toolCall.name, - params: toolCall.arguments, - response: toolContent - } - }) - } catch (error) { - console.warn('[ToolCallProcessor] Failed to dispatch PostToolUse hook:', error) - } - } - if (supportsFunctionCall) { this.appendNativeFunctionCallMessages(context.conversationMessages, toolCall, { content: toolContentForModel @@ -436,22 +402,6 @@ export class ToolCallProcessor { ) const errorMessage = toolError instanceof Error ? toolError.message : String(toolError) - if (shouldDispatchToolHooks) { - try { - presenter.hooksNotifications.dispatchEvent('PostToolUseFailure', { - conversationId: context.conversationId, - tool: { - callId: toolCall.id, - name: toolCall.name, - params: toolCall.arguments, - error: errorMessage - } - }) - } catch (error) { - console.warn('[ToolCallProcessor] Failed to dispatch PostToolUseFailure hook:', error) - } - } - // Check if error is non-retryable (should stop the loop) const errorForClassification: Error | string = toolError instanceof Error ? toolError : String(toolError) diff --git a/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts b/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts index 780823d70..60e67125a 100644 --- a/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts +++ b/src/main/presenter/agentPresenter/streaming/llmEventHandler.ts @@ -151,51 +151,7 @@ export class LLMEventHandler { if (tool_call && !shouldSkipToolCall) { if (isAcpProvider) { - try { - if (tool_call === 'start') { - presenter.hooksNotifications.dispatchEvent('PreToolUse', { - conversationId: state.conversationId, - messageId: eventId, - providerId: state.message.model_provider, - modelId: state.message.model_id, - tool: { - callId: tool_call_id, - name: tool_call_name, - params: tool_call_params - } - }) - } - if (tool_call === 'end') { - presenter.hooksNotifications.dispatchEvent('PostToolUse', { - conversationId: state.conversationId, - messageId: eventId, - providerId: state.message.model_provider, - modelId: state.message.model_id, - tool: { - callId: tool_call_id, - name: tool_call_name, - params: tool_call_params, - response: msg.tool_call_response ? String(msg.tool_call_response) : undefined - } - }) - } - if (tool_call === 'error') { - presenter.hooksNotifications.dispatchEvent('PostToolUseFailure', { - conversationId: state.conversationId, - messageId: eventId, - providerId: state.message.model_provider, - modelId: state.message.model_id, - tool: { - callId: tool_call_id, - name: tool_call_name, - params: tool_call_params, - error: msg.tool_call_response ? String(msg.tool_call_response) : undefined - } - }) - } - } catch (error) { - console.warn('[LLMEventHandler] Failed to dispatch ACP tool hooks:', error) - } + // Legacy hook dispatch disabled. New session hooks are emitted by the new agent pipeline. } switch (tool_call) { @@ -217,22 +173,6 @@ export class LLMEventHandler { payload: msg.permission_request ?? {} }) presenter.sessionManager.setStatus(state.conversationId, 'waiting_permission') - try { - presenter.hooksNotifications.dispatchEvent('PermissionRequest', { - conversationId: state.conversationId, - messageId: eventId, - providerId: state.message.model_provider, - modelId: state.message.model_id, - tool: { - callId: tool_call_id, - name: tool_call_name, - params: tool_call_params - }, - permission: msg.permission_request ?? null - }) - } catch (error) { - console.warn('[LLMEventHandler] Failed to dispatch PermissionRequest hook:', error) - } await this.toolCallHandler.processToolCallPermission(state, msg, currentTime) break case 'question-required': @@ -445,8 +385,6 @@ export class LLMEventHandler { async handleLLMAgentEnd(msg: LLMAgentEventData): Promise { const { eventId, userStop } = msg const state = this.generatingMessages.get(eventId) - const errorSnapshot = this.errorByEventId.get(eventId) - if (state) { if (state.adaptiveBuffer) { await this.contentBufferHandler.flushAdaptiveBuffer(eventId) @@ -512,51 +450,8 @@ export class LLMEventHandler { presenter.sessionManager.clearPendingQuestion(state.conversationId) } - const stopReason = errorSnapshot ? 'error' : userStop ? 'user_stop' : 'complete' - const stopPayload = { - reason: stopReason, - userStop: Boolean(userStop) - } - const usage = state?.totalUsage ?? errorSnapshot?.usage ?? null - const errorInfo = errorSnapshot?.error ?? null - let conversationId = state?.conversationId ?? errorSnapshot?.conversationId - let providerId = state?.message.model_provider ?? errorSnapshot?.providerId - let modelId = state?.message.model_id ?? errorSnapshot?.modelId - - if (!conversationId) { - try { - const message = await this.messageManager.getMessage(eventId) - conversationId = message.conversationId - providerId = providerId ?? message.model_provider - modelId = modelId ?? message.model_id - } catch { - // ignore - } - } - try { - try { - presenter.hooksNotifications.dispatchEvent('Stop', { - conversationId, - providerId, - modelId, - stop: stopPayload - }) - } catch (error) { - console.warn('[LLMEventHandler] Failed to dispatch Stop hook:', error) - } - try { - presenter.hooksNotifications.dispatchEvent('SessionEnd', { - conversationId, - providerId, - modelId, - stop: stopPayload, - usage, - error: errorInfo - }) - } catch (error) { - console.warn('[LLMEventHandler] Failed to dispatch SessionEnd hook:', error) - } + // Legacy hook dispatch disabled. New session hooks are emitted by the new agent pipeline. } finally { this.errorByEventId.delete(eventId) } diff --git a/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts b/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts index d1c93b3d0..6dbf443d4 100644 --- a/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts +++ b/src/main/presenter/agentPresenter/streaming/streamGenerationHandler.ts @@ -118,19 +118,6 @@ export class StreamGenerationHandler extends BaseHandler { verbosity: currentVerbosity } = currentConversation.settings - try { - presenter.hooksNotifications.dispatchEvent('SessionStart', { - conversationId, - messageId: userMessage.id, - promptPreview: userContent, - providerId: currentProviderId, - modelId: currentModelId, - workdir: agentWorkspacePath ?? null - }) - } catch (error) { - console.warn('[StreamGenerationHandler] Failed to dispatch SessionStart hook:', error) - } - const stream = this.ctx.llmProviderPresenter.startStreamCompletion( currentProviderId, finalContent, @@ -258,18 +245,6 @@ export class StreamGenerationHandler extends BaseHandler { await this.updateGenerationState(state, promptTokens) - try { - presenter.hooksNotifications.dispatchEvent('SessionStart', { - conversationId, - messageId: userMessage.id, - promptPreview: 'continue', - providerId, - modelId - }) - } catch (error) { - console.warn('[StreamGenerationHandler] Failed to dispatch SessionStart hook:', error) - } - if (toolCallResponse && toolCall) { eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { eventId: state.message.id, diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index fa989c534..3c123abae 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -83,7 +83,6 @@ interface IAppSettings { syncFolderPath?: string // Sync folder path lastSyncTime?: number // Last sync time customSearchEngines?: string // Custom search engines JSON string - soundEnabled?: boolean // Whether sound effects are enabled copyWithCotEnabled?: boolean loggingEnabled?: boolean // Whether logging is enabled floatingButtonEnabled?: boolean // Whether floating button is enabled @@ -153,7 +152,6 @@ export class ConfigPresenter implements IConfigPresenter { syncEnabled: false, syncFolderPath: path.join(this.userDataPath, 'sync'), lastSyncTime: 0, - soundEnabled: false, copyWithCotEnabled: true, loggingEnabled: false, floatingButtonEnabled: false, @@ -962,18 +960,6 @@ export class ConfigPresenter implements IConfigPresenter { }, 1000) } - // Get sound effects switch status - getSoundEnabled(): boolean { - const value = this.getSetting('soundEnabled') ?? false - return value === undefined || value === null ? false : value - } - - // Set sound effects switch status - setSoundEnabled(enabled: boolean): void { - this.setSetting('soundEnabled', enabled) - eventBus.sendToRenderer(CONFIG_EVENTS.SOUND_ENABLED_CHANGED, SendTarget.ALL_WINDOWS, enabled) - } - getCopyWithCotEnabled(): boolean { return this.uiSettingsHelper.getCopyWithCotEnabled() } diff --git a/src/main/presenter/deepchatAgentPresenter/dispatch.ts b/src/main/presenter/deepchatAgentPresenter/dispatch.ts index 2b5ffe5ba..c7c2c82eb 100644 --- a/src/main/presenter/deepchatAgentPresenter/dispatch.ts +++ b/src/main/presenter/deepchatAgentPresenter/dispatch.ts @@ -6,7 +6,7 @@ import type { MCPToolCall, MCPContentItem, MCPResourceContent } from '@shared/ty import type { IToolPresenter } from '@shared/types/presenters/tool.presenter' import type { AssistantMessageBlock, PermissionMode } from '@shared/types/agent-interface' import { parseQuestionToolArgs, QUESTION_TOOL_NAME } from '../agentPresenter/tools/questionTool' -import type { IoParams, PendingToolInteraction, StreamState } from './types' +import type { IoParams, PendingToolInteraction, ProcessHooks, StreamState } from './types' import type { ChatMessage } from '@shared/types/core/chat-message' import { nanoid } from 'nanoid' import type { ToolOutputGuard } from './toolOutputGuard' @@ -380,7 +380,8 @@ export async function executeTools( permissionMode: PermissionMode, toolOutputGuard: ToolOutputGuard, contextLength: number, - maxTokens: number + maxTokens: number, + hooks?: ProcessHooks ): Promise<{ executed: number pendingInteractions: PendingToolInteraction[] @@ -442,6 +443,12 @@ export async function executeTools( } try { + hooks?.onPreToolUse?.({ + callId: tc.id, + name: tc.name, + params: tc.arguments + }) + if (toolCall.function.name === QUESTION_TOOL_NAME) { const parsedQuestion = parseQuestionToolArgs(tc.arguments) if (!parsedQuestion.success) { @@ -487,6 +494,11 @@ export async function executeTools( if (permissionMode === 'full_access') { await autoGrantPermission(io.sessionId, preCheckedPermission) } else { + hooks?.onPermissionRequest?.(preCheckedPermission, { + callId: tc.id, + name: tc.name, + params: tc.arguments + }) const interaction = appendPermissionActionBlock( state, io, @@ -518,6 +530,11 @@ export async function executeTools( const retryCallResult = await toolPresenter.callTool(toolCall) toolRawData = retryCallResult.rawData } else { + hooks?.onPermissionRequest?.(pendingPermission, { + callId: tc.id, + name: tc.name, + params: tc.arguments + }) const interaction = appendPermissionActionBlock( state, io, @@ -583,6 +600,21 @@ export async function executeTools( content: toolMessageContent }) updateToolCallBlock(state.blocks, tc.id, toolMessageContent, isToolError) + if (isToolError) { + hooks?.onPostToolUseFailure?.({ + callId: tc.id, + name: tc.name, + params: tc.arguments, + error: toolMessageContent + }) + } else { + hooks?.onPostToolUse?.({ + callId: tc.id, + name: tc.name, + params: tc.arguments, + response: toolMessageContent + }) + } } catch (err) { const errorText = err instanceof Error ? err.message : String(err) conversation.push({ @@ -591,6 +623,12 @@ export async function executeTools( content: `Error: ${errorText}` }) updateToolCallBlock(state.blocks, tc.id, `Error: ${errorText}`, true) + hooks?.onPostToolUseFailure?.({ + callId: tc.id, + name: tc.name, + params: tc.arguments, + error: `Error: ${errorText}` + }) } state.dirty = true diff --git a/src/main/presenter/deepchatAgentPresenter/index.ts b/src/main/presenter/deepchatAgentPresenter/index.ts index 487dd9e68..ddaf8fdd6 100644 --- a/src/main/presenter/deepchatAgentPresenter/index.ts +++ b/src/main/presenter/deepchatAgentPresenter/index.ts @@ -39,6 +39,7 @@ import { DeepChatSessionStore, type SessionSummaryState } from './sessionStore' import type { PendingToolInteraction, ProcessResult } from './types' import { ToolOutputGuard } from './toolOutputGuard' import type { ProviderRequestTracePayload } from '../llmProviderPresenter/requestTrace' +import type { NewSessionHooksBridge } from '../hooksNotifications/newSessionBridge' type PendingInteractionEntry = { interaction: PendingToolInteraction @@ -98,6 +99,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { private readonly runtimeState: Map = new Map() private readonly sessionGenerationSettings: Map = new Map() private readonly abortControllers: Map = new Map() + private readonly sessionAgentIds: Map = new Map() private readonly sessionProjectDirs: Map = new Map() private readonly systemPromptCache: Map = new Map() private readonly sessionCompactionStates: Map = new Map() @@ -105,12 +107,14 @@ export class DeepChatAgentPresenter implements IAgentImplementation { private readonly resumingMessages: Set = new Set() private readonly compactionService: CompactionService private readonly toolOutputGuard: ToolOutputGuard + private readonly hooksBridge?: NewSessionHooksBridge constructor( llmProviderPresenter: ILlmProviderPresenter, configPresenter: IConfigPresenter, sqlitePresenter: SQLitePresenter, - toolPresenter?: IToolPresenter + toolPresenter?: IToolPresenter, + hooksBridge?: NewSessionHooksBridge ) { this.llmProviderPresenter = llmProviderPresenter this.configPresenter = configPresenter @@ -124,6 +128,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { this.configPresenter ) this.toolOutputGuard = new ToolOutputGuard() + this.hooksBridge = hooksBridge const recovered = this.messageStore.recoverPendingMessages() if (recovered > 0) { @@ -134,6 +139,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { async initSession( sessionId: string, config: { + agentId?: string providerId: string modelId: string projectDir?: string | null @@ -159,6 +165,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { permissionMode, generationSettings ) + this.sessionAgentIds.set(sessionId, config.agentId?.trim() || 'deepchat') this.sessionProjectDirs.set(sessionId, projectDir) this.sessionGenerationSettings.set(sessionId, generationSettings) this.runtimeState.set(sessionId, { @@ -181,6 +188,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { this.messageStore.deleteBySession(sessionId) this.sessionStore.delete(sessionId) this.runtimeState.delete(sessionId) + this.sessionAgentIds.delete(sessionId) this.sessionGenerationSettings.delete(sessionId) this.sessionProjectDirs.delete(sessionId) this.systemPromptCache.delete(sessionId) @@ -293,6 +301,15 @@ export class DeepChatAgentPresenter implements IAgentImplementation { ) } + this.dispatchHook('UserPromptSubmit', { + sessionId, + messageId: userMessageId, + promptPreview: normalizedInput.text, + providerId: state.providerId, + modelId: state.modelId, + projectDir + }) + const systemPrompt = appendSummarySection(baseSystemPrompt, summaryState.summaryText) const messages = buildContext( sessionId, @@ -322,11 +339,27 @@ export class DeepChatAgentPresenter implements IAgentImplementation { sessionId, messageId: assistantMessageId, messages, - projectDir + projectDir, + promptPreview: normalizedInput.text }) this.applyProcessResultStatus(sessionId, result) } catch (err) { console.error('[DeepChatAgent] processMessage error:', err) + const errorMessage = err instanceof Error ? err.message : String(err) + this.dispatchHook('Stop', { + sessionId, + providerId: state.providerId, + modelId: state.modelId, + projectDir, + stop: { reason: 'error', userStop: false } + }) + this.dispatchHook('SessionEnd', { + sessionId, + providerId: state.providerId, + modelId: state.modelId, + projectDir, + error: { message: errorMessage } + }) this.setSessionStatus(sessionId, 'error') } } @@ -397,12 +430,38 @@ export class DeepChatAgentPresenter implements IAgentImplementation { } const permissionPayload = this.parsePermissionPayload(actionBlock) const permissionType = permissionPayload?.permissionType ?? 'write' + const state = this.runtimeState.get(sessionId) if (response.granted) { this.markPermissionResolved(actionBlock, true, permissionType) await this.grantPermissionForPayload(sessionId, permissionPayload, toolCall) + this.dispatchHook('PreToolUse', { + sessionId, + messageId, + providerId: state?.providerId, + modelId: state?.modelId, + projectDir: this.resolveProjectDir(sessionId), + tool: { + callId: toolCall.id, + name: toolCall.name, + params: toolCall.params + } + }) const execution = await this.executeDeferredToolCall(sessionId, toolCall) if (execution.terminalError) { + this.dispatchHook('PostToolUseFailure', { + sessionId, + messageId, + providerId: state?.providerId, + modelId: state?.modelId, + projectDir: this.resolveProjectDir(sessionId), + tool: { + callId: toolCall.id, + name: toolCall.name, + params: toolCall.params, + error: execution.terminalError + } + }) this.updateToolCallResponse(blocks, toolCall.id, execution.terminalError, true) this.messageStore.setMessageError(messageId, blocks) this.emitMessageRefresh(sessionId, messageId) @@ -412,9 +471,45 @@ export class DeepChatAgentPresenter implements IAgentImplementation { messageId, error: execution.terminalError }) + this.dispatchHook('Stop', { + sessionId, + messageId, + providerId: state?.providerId, + modelId: state?.modelId, + projectDir: this.resolveProjectDir(sessionId), + stop: { reason: 'error', userStop: false } + }) + this.dispatchHook('SessionEnd', { + sessionId, + messageId, + providerId: state?.providerId, + modelId: state?.modelId, + projectDir: this.resolveProjectDir(sessionId), + error: { message: execution.terminalError } + }) this.setSessionStatus(sessionId, 'error') return { resumed: false } } + this.dispatchHook(execution.isError ? 'PostToolUseFailure' : 'PostToolUse', { + sessionId, + messageId, + providerId: state?.providerId, + modelId: state?.modelId, + projectDir: this.resolveProjectDir(sessionId), + tool: execution.isError + ? { + callId: toolCall.id, + name: toolCall.name, + params: toolCall.params, + error: execution.responseText + } + : { + callId: toolCall.id, + name: toolCall.name, + params: toolCall.params, + response: execution.responseText + } + }) this.updateToolCallResponse( blocks, toolCall.id, @@ -428,6 +523,19 @@ export class DeepChatAgentPresenter implements IAgentImplementation { } if (execution.requiresPermission && execution.permissionRequest) { + this.dispatchHook('PermissionRequest', { + sessionId, + messageId, + providerId: state?.providerId, + modelId: state?.modelId, + projectDir: this.resolveProjectDir(sessionId), + permission: execution.permissionRequest, + tool: { + callId: toolCall.id, + name: toolCall.name, + params: toolCall.params + } + }) actionBlock.status = 'pending' actionBlock.content = execution.permissionRequest.description actionBlock.extra = { @@ -578,6 +686,92 @@ export class DeepChatAgentPresenter implements IAgentImplementation { this.setSessionStatus(sessionId, 'idle') } + private dispatchTerminalHooks( + sessionId: string, + state: DeepChatSessionState | undefined, + result: ProcessResult + ): void { + if (!state || result.status === 'paused') { + return + } + + this.dispatchHook('Stop', { + sessionId, + providerId: state.providerId, + modelId: state.modelId, + projectDir: this.resolveProjectDir(sessionId), + stop: { + reason: + result.stopReason ?? + (result.status === 'completed' + ? 'complete' + : result.status === 'aborted' + ? 'user_stop' + : 'error'), + userStop: result.status === 'aborted' + } + }) + this.dispatchHook('SessionEnd', { + sessionId, + providerId: state.providerId, + modelId: state.modelId, + projectDir: this.resolveProjectDir(sessionId), + usage: result.usage ?? null, + error: + result.errorMessage || result.terminalError + ? { + message: result.errorMessage ?? result.terminalError + } + : null + }) + } + + private dispatchHook( + event: + | 'UserPromptSubmit' + | 'SessionStart' + | 'PreToolUse' + | 'PostToolUse' + | 'PostToolUseFailure' + | 'PermissionRequest' + | 'Stop' + | 'SessionEnd', + context: { + sessionId: string + messageId?: string + promptPreview?: string + providerId?: string + modelId?: string + projectDir?: string | null + tool?: { + callId?: string + name?: string + params?: string + response?: string + error?: string + } + permission?: Record | null + stop?: { + reason?: string + userStop?: boolean + } | null + usage?: Record | null + error?: { + message?: string + stack?: string + } | null + } + ): void { + try { + this.hooksBridge?.dispatch(event, { + ...context, + agentId: this.sessionAgentIds.get(context.sessionId) ?? 'deepchat' + }) + } catch (error) { + console.warn(`[DeepChatAgent] Failed to dispatch ${event} hook:`, error) + } + } + async getMessages(sessionId: string): Promise { return this.messageStore.getMessages(sessionId) } @@ -740,8 +934,17 @@ export class DeepChatAgentPresenter implements IAgentImplementation { projectDir: string | null tools?: MCPToolDefinition[] initialBlocks?: AssistantMessageBlock[] + promptPreview?: string }): Promise { - const { sessionId, messageId, messages, projectDir, tools: providedTools, initialBlocks } = args + const { + sessionId, + messageId, + messages, + projectDir, + tools: providedTools, + initialBlocks, + promptPreview + } = args const state = this.runtimeState.get(sessionId) if (!state) { throw new Error(`Session ${sessionId} not found`) @@ -805,6 +1008,15 @@ export class DeepChatAgentPresenter implements IAgentImplementation { this.abortControllers.set(sessionId, abortController) try { + this.dispatchHook('SessionStart', { + sessionId, + messageId, + promptPreview, + providerId: state.providerId, + modelId: state.modelId, + projectDir + }) + return await processStream({ messages, tools, @@ -818,6 +1030,49 @@ export class DeepChatAgentPresenter implements IAgentImplementation { permissionMode: state.permissionMode, toolOutputGuard: this.toolOutputGuard, initialBlocks, + hooks: { + onPreToolUse: (tool) => { + this.dispatchHook('PreToolUse', { + sessionId, + messageId, + providerId: state.providerId, + modelId: state.modelId, + projectDir, + tool + }) + }, + onPostToolUse: (tool) => { + this.dispatchHook('PostToolUse', { + sessionId, + messageId, + providerId: state.providerId, + modelId: state.modelId, + projectDir, + tool + }) + }, + onPostToolUseFailure: (tool) => { + this.dispatchHook('PostToolUseFailure', { + sessionId, + messageId, + providerId: state.providerId, + modelId: state.modelId, + projectDir, + tool + }) + }, + onPermissionRequest: (permission, tool) => { + this.dispatchHook('PermissionRequest', { + sessionId, + messageId, + providerId: state.providerId, + modelId: state.modelId, + projectDir, + permission, + tool + }) + } + }, io: { sessionId, messageId, @@ -837,11 +1092,13 @@ export class DeepChatAgentPresenter implements IAgentImplementation { sessionId: string, result: ProcessResult | null | undefined ): void { + const state = this.runtimeState.get(sessionId) if (!result || !result.status) { this.setSessionStatus(sessionId, 'idle') return } if (result.status === 'completed') { + this.dispatchTerminalHooks(sessionId, state, result) this.setSessionStatus(sessionId, 'idle') return } @@ -850,9 +1107,11 @@ export class DeepChatAgentPresenter implements IAgentImplementation { return } if (result.status === 'aborted') { + this.dispatchTerminalHooks(sessionId, state, result) this.setSessionStatus(sessionId, 'idle') return } + this.dispatchTerminalHooks(sessionId, state, result) this.setSessionStatus(sessionId, 'error') } diff --git a/src/main/presenter/deepchatAgentPresenter/process.ts b/src/main/presenter/deepchatAgentPresenter/process.ts index 5c059029c..1b12b7ada 100644 --- a/src/main/presenter/deepchatAgentPresenter/process.ts +++ b/src/main/presenter/deepchatAgentPresenter/process.ts @@ -56,6 +56,7 @@ export async function processStream(params: ProcessParams): Promise 0 ? modelConfig.contextLength : UNKNOWN_CONTEXT_LIMIT, - maxTokens + maxTokens, + hooks ) toolCallCount += executed.executed echo.flush() @@ -155,7 +160,10 @@ export async function processStream(params: ProcessParams): Promise { + const usage: Record = {} + if (typeof state.metadata.totalTokens === 'number') { + usage.totalTokens = state.metadata.totalTokens + } + if (typeof state.metadata.inputTokens === 'number') { + usage.inputTokens = state.metadata.inputTokens + } + if (typeof state.metadata.outputTokens === 'number') { + usage.outputTokens = state.metadata.outputTokens + } + return usage +} diff --git a/src/main/presenter/deepchatAgentPresenter/types.ts b/src/main/presenter/deepchatAgentPresenter/types.ts index b4ca8561c..a3da6dac3 100644 --- a/src/main/presenter/deepchatAgentPresenter/types.ts +++ b/src/main/presenter/deepchatAgentPresenter/types.ts @@ -38,6 +38,30 @@ export interface IoParams { abortSignal: AbortSignal } +export interface ProcessHooks { + onPreToolUse?: (tool: { callId?: string; name?: string; params?: string }) => void + onPostToolUse?: (tool: { + callId?: string + name?: string + params?: string + response?: string + }) => void + onPostToolUseFailure?: (tool: { + callId?: string + name?: string + params?: string + error?: string + }) => void + onPermissionRequest?: ( + permission: Record, + tool: { + callId?: string + name?: string + params?: string + } + ) => void +} + export interface PendingToolInteraction { type: 'question' | 'permission' messageId: string @@ -79,6 +103,9 @@ export interface ProcessResult { status: 'completed' | 'paused' | 'aborted' | 'error' pendingInteractions?: PendingToolInteraction[] terminalError?: string + stopReason?: string + usage?: Record + errorMessage?: string } export interface ProcessParams { @@ -101,6 +128,7 @@ export interface ProcessParams { permissionMode: PermissionMode toolOutputGuard: ToolOutputGuard initialBlocks?: AssistantMessageBlock[] + hooks?: ProcessHooks io: IoParams } diff --git a/src/main/presenter/hooksNotifications/index.ts b/src/main/presenter/hooksNotifications/index.ts index 7df94037c..8c7061688 100644 --- a/src/main/presenter/hooksNotifications/index.ts +++ b/src/main/presenter/hooksNotifications/index.ts @@ -3,7 +3,7 @@ import log from 'electron-log' import { spawn } from 'child_process' import fs from 'fs' import path from 'path' -import type { IConfigPresenter, ISessionPresenter } from '@shared/presenter' +import type { IConfigPresenter } from '@shared/presenter' import { RuntimeHelper } from '@/lib/runtimeHelper' import { HookCommandResult, @@ -21,7 +21,7 @@ const TRUNCATION_SUFFIX = ' ...(truncated)' const MAX_RETRIES = 2 const CONFIRMO_HOOK_RELATIVE_PATH = path.join('.confirmo', 'hooks', 'confirmo-hook.js') -type HookDispatchContext = { +export type HookDispatchContext = { conversationId?: string messageId?: string promptPreview?: string @@ -49,6 +49,26 @@ type HookDispatchContext = { isTest?: boolean } +type HookConversationLookup = { + settings: { + providerId?: string + modelId?: string + } +} + +type HookMessageLookup = { + content: unknown +} + +type HooksNotificationsDeps = { + getConversation?: (conversationId: string) => Promise + getMessage?: (messageId: string) => Promise + resolveWorkspaceContext?: ( + conversationId?: string, + modelId?: string + ) => Promise<{ agentWorkspacePath: string | null }> +} + class SerialQueue { private chain: Promise = Promise.resolve() @@ -129,13 +149,7 @@ export class HooksNotificationsService { constructor( private readonly configPresenter: IConfigPresenter, - private readonly deps: { - sessionPresenter: ISessionPresenter - resolveWorkspaceContext: ( - conversationId?: string, - modelId?: string - ) => Promise<{ agentWorkspacePath: string | null }> - } + private readonly deps: HooksNotificationsDeps ) {} getConfigSnapshot(): HooksNotificationsSettings { @@ -251,12 +265,15 @@ export class HooksNotificationsService { let workdir = context.workdir if (conversationId && (!providerId || !modelId)) { + const getConversation = this.deps.getConversation try { - const conversation = await this.deps.sessionPresenter.getConversation(conversationId) - providerId = providerId ?? conversation.settings.providerId - modelId = modelId ?? conversation.settings.modelId - if (!agentId && conversation.settings.providerId === 'acp') { - agentId = conversation.settings.modelId + if (getConversation) { + const conversation = await getConversation(conversationId) + providerId = providerId ?? conversation.settings.providerId + modelId = modelId ?? conversation.settings.modelId + if (!agentId && conversation.settings.providerId === 'acp') { + agentId = conversation.settings.modelId + } } } catch (error) { log.warn('[HooksNotifications] Failed to load conversation info:', error) @@ -264,9 +281,12 @@ export class HooksNotificationsService { } if (conversationId && !workdir) { + const resolveWorkspaceContext = this.deps.resolveWorkspaceContext try { - const resolved = await this.deps.resolveWorkspaceContext(conversationId, modelId) - workdir = resolved.agentWorkspacePath ?? null + if (resolveWorkspaceContext) { + const resolved = await resolveWorkspaceContext(conversationId, modelId) + workdir = resolved.agentWorkspacePath ?? null + } } catch (error) { log.warn('[HooksNotifications] Failed to resolve workdir:', error) } @@ -274,9 +294,12 @@ export class HooksNotificationsService { let promptPreview = context.promptPreview if (!promptPreview && context.messageId) { + const getMessage = this.deps.getMessage try { - const message = await this.deps.sessionPresenter.getMessage(context.messageId) - promptPreview = extractPromptPreview(message.content) + if (getMessage) { + const message = await getMessage(context.messageId) + promptPreview = extractPromptPreview(message.content) + } } catch (error) { log.warn('[HooksNotifications] Failed to read message for preview:', error) } diff --git a/src/main/presenter/hooksNotifications/newSessionBridge.ts b/src/main/presenter/hooksNotifications/newSessionBridge.ts new file mode 100644 index 000000000..f37aeb926 --- /dev/null +++ b/src/main/presenter/hooksNotifications/newSessionBridge.ts @@ -0,0 +1,42 @@ +import type { HookEventName } from '@shared/hooksNotifications' +import type { HookDispatchContext } from './index' + +type HookDispatcher = { + dispatchEvent(event: HookEventName, context: HookDispatchContext): void +} + +export type NewSessionHookContext = { + sessionId: string + agentId?: string | null + projectDir?: string | null + messageId?: string + promptPreview?: string + providerId?: string + modelId?: string + tool?: HookDispatchContext['tool'] + permission?: HookDispatchContext['permission'] + stop?: HookDispatchContext['stop'] + usage?: HookDispatchContext['usage'] + error?: HookDispatchContext['error'] +} + +export class NewSessionHooksBridge { + constructor(private readonly dispatcher: HookDispatcher) {} + + dispatch(event: HookEventName, context: NewSessionHookContext): void { + this.dispatcher.dispatchEvent(event, { + conversationId: context.sessionId, + messageId: context.messageId, + promptPreview: context.promptPreview, + providerId: context.providerId, + modelId: context.modelId, + agentId: context.agentId ?? null, + workdir: context.projectDir ?? null, + tool: context.tool, + permission: context.permission ?? null, + stop: context.stop ?? null, + usage: context.usage ?? null, + error: context.error ?? null + }) + } +} diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 364931671..2600202a7 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -64,6 +64,7 @@ import { ConversationExporterService } from './exporter' import { SkillPresenter } from './skillPresenter' import { SkillSyncPresenter } from './skillSyncPresenter' import { HooksNotificationsService } from './hooksNotifications' +import { NewSessionHooksBridge } from './hooksNotifications/newSessionBridge' import { NewAgentPresenter } from './newAgentPresenter' import { DeepChatAgentPresenter } from './deepchatAgentPresenter' import { ProjectPresenter } from './projectPresenter' @@ -202,12 +203,21 @@ export class Presenter implements IPresenter { // Initialize Skill Sync presenter this.skillSyncPresenter = new SkillSyncPresenter(this.skillPresenter, this.configPresenter) + // Initialize Hooks & Notifications service + this.hooksNotifications = new HooksNotificationsService(this.configPresenter, { + getConversation: this.sessionPresenter.getConversation.bind(this.sessionPresenter), + getMessage: this.sessionPresenter.getMessage.bind(this.sessionPresenter), + resolveWorkspaceContext: this.sessionManager.resolveWorkspaceContext.bind(this.sessionManager) + }) + const newSessionHooksBridge = new NewSessionHooksBridge(this.hooksNotifications) + // Initialize new agent architecture presenters const deepchatAgentPresenter = new DeepChatAgentPresenter( this.llmproviderPresenter as unknown as ILlmProviderPresenter, this.configPresenter, this.sqlitePresenter as unknown as import('./sqlitePresenter').SQLitePresenter, - this.toolPresenter + this.toolPresenter, + newSessionHooksBridge ) this.newAgentPresenter = new NewAgentPresenter( deepchatAgentPresenter, @@ -221,12 +231,6 @@ export class Presenter implements IPresenter { this.devicePresenter ) - // Initialize Hooks & Notifications service - this.hooksNotifications = new HooksNotificationsService(this.configPresenter, { - sessionPresenter: this.sessionPresenter, - resolveWorkspaceContext: this.sessionManager.resolveWorkspaceContext.bind(this.sessionManager) - }) - this.setupEventBus() // 设置事件总线监听 } diff --git a/src/main/presenter/newAgentPresenter/index.ts b/src/main/presenter/newAgentPresenter/index.ts index 6cfc18504..f2acfba9b 100644 --- a/src/main/presenter/newAgentPresenter/index.ts +++ b/src/main/presenter/newAgentPresenter/index.ts @@ -103,12 +103,14 @@ export class NewAgentPresenter { // Initialize agent-side session const initConfig: { + agentId?: string providerId: string modelId: string projectDir: string | null permissionMode: PermissionMode generationSettings?: Partial } = { + agentId, providerId, modelId, projectDir, @@ -182,6 +184,7 @@ export class NewAgentPresenter { isDraft: true }) await this.ensureSessionRuntimeInitialized(agent, sessionId, { + agentId, providerId: 'acp', modelId: agentId, projectDir, @@ -193,6 +196,7 @@ export class NewAgentPresenter { } } else { await this.ensureSessionRuntimeInitialized(agent, record.id, { + agentId, providerId: 'acp', modelId: agentId, projectDir, @@ -314,6 +318,7 @@ export class NewAgentPresenter { try { await agent.initSession(targetSessionId, { + agentId: sourceSession.agentId, providerId: sourceState.providerId, modelId: sourceState.modelId, projectDir: sourceSession.projectDir ?? null, @@ -941,6 +946,7 @@ export class NewAgentPresenter { agent: IAgentImplementation, sessionId: string, config: { + agentId?: string providerId: string modelId: string projectDir: string diff --git a/src/renderer/public/sounds/sfx-fc.mp3 b/src/renderer/public/sounds/sfx-fc.mp3 deleted file mode 100644 index 63fe8002ff673fe39014ff80a481e46025078240..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10989 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 z zbSuJuHGls_bviT#7icZsZeT_wMZGv z?<4%-70MJDA7Dd==hIOFjGpQBVNy(BhJ`ULYr~{*V&*8KfS!{L=*_l*+`jm=i49VcP)(_ZUlYPm>-mELmhCd zOB}j>yuI5y={|hq!*M=$o6U#1zBXcjfRP|<`^Hi2NhydgapZboA+ZP%$855wewKRk zAWYXlaOanjv<~Mc?>n{RO*1b02H(!}_|Po5r6hXx7`Lnp{DzNfD#GrvxFJ}Cm8foy zA!+p=_8-bV)0{W}3NO?j1-d+rY)peyIIW7q2~^tVAM#=v`jbrG${pwaK*xG$>Ryk@ zD2^5YlBme>6H4PGT_TFjl{>sPr*psOuY&{+%BrjL(^~VgRJcMElkk6CvhvV)BgYw8 zcS*5F$6wxPkYZMei*xG17t=`1-(A*;C0VjPmpPOk4dHuLqGjyufoe|p{(>%@I8q83I{Q!23Mw@1QDy>n zTy(!_gKr6^x|0dlojB8XJ9>^DbY3mu7El6lru1ùZncwt>y>HnmvJ~4G2^m$NT zWguod1WSFFhF8Oy_L`8tT5@o*gK11cQ!t-anIUP2y*7a%zFMQwRfEI}W?21=H;?YC z&bAY{p5G^!ZB|AzzTg1CMS+#E!`q?Ua?y3vtsE@X;(z;g%zN~fDWdb{xI@GL7*I@N zG8`L=3rn`eE3%X`sbxLxb}h0dH*&4Q35& z4Z)8D govi>z9byOI=gtw8Njvubcm3i2&BcfRHx^3%KTM&%r2qf` diff --git a/src/renderer/public/sounds/sfx-typing.mp3 b/src/renderer/public/sounds/sfx-typing.mp3 deleted file mode 100644 index bfe3833a4ff06f122e27d1402021bb45c79611f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4461 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 - uiSettingsStore.autoScrollEnabled) -const soundEnabled = computed(() => soundStore.soundEnabled) const copyWithCotEnabled = computed(() => uiSettingsStore.copyWithCotEnabled) const traceDebugEnabled = computed(() => uiSettingsStore.traceDebugEnabled) @@ -62,10 +52,6 @@ const handleAutoScrollChange = (value: boolean) => { uiSettingsStore.setAutoScrollEnabled(value) } -const handleSoundChange = (value: boolean) => { - soundStore.setSoundEnabled(value) -} - const handleCopyWithCotChange = (value: boolean) => { uiSettingsStore.setCopyWithCotEnabled(value) } diff --git a/src/renderer/src/events.ts b/src/renderer/src/events.ts index 8ab300d1f..9b86051a8 100644 --- a/src/renderer/src/events.ts +++ b/src/renderer/src/events.ts @@ -24,7 +24,6 @@ export const CONFIG_EVENTS = { NOTIFICATIONS_CHANGED: 'config:notifications-changed', CONTENT_PROTECTION_CHANGED: 'config:content-protection-changed', LANGUAGE_CHANGED: 'config:language-changed', // 新增:语言变更事件 - SOUND_ENABLED_CHANGED: 'config:sound-enabled-changed', // 新增:声音启用状态变更事件 COPY_WITH_COT_CHANGED: 'config:copy-with-cot-enabled-changed', TRACE_DEBUG_CHANGED: 'config:trace-debug-changed', // Trace 调试功能开关变更事件 FONT_FAMILY_CHANGED: 'config:font-family-changed', diff --git a/src/renderer/src/i18n/da-DK/settings.json b/src/renderer/src/i18n/da-DK/settings.json index 39739488f..d64328c3b 100644 --- a/src/renderer/src/i18n/da-DK/settings.json +++ b/src/renderer/src/i18n/da-DK/settings.json @@ -41,7 +41,6 @@ "contentProtectionEnableDesc": "Forhindrer skærmdelingsapps i at optage DeepChat-vinduet og beskytter din fortrolighed. Ikke alle apps respekterer dette; i nogle miljøer kan et sort vindue blive vist.", "contentProtectionDisableDesc": "Tillad skærmdelingsapps at optage DeepChat-vinduet.", "contentProtectionRestartNotice": "Denne ændring genstarter applikationen. Vil du fortsætte?", - "soundEnabled": "Aktivér lydeffekter", "copyWithCotEnabled": "Kopiér COT-detaljer", "traceDebugEnabled": "Trace-kald", "loggingEnabled": "Aktivér logning", diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index 6bd4c3c44..8d35ad284 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -41,7 +41,6 @@ "contentProtectionEnableDesc": "Prevent screen sharing apps from capturing the DeepChat window to help protect your privacy. Not all apps honor this setting; in some environments a black window may remain.", "contentProtectionDisableDesc": "Allow screen sharing apps to capture the DeepChat window.", "contentProtectionRestartNotice": "Changing this setting will restart the application. Do you want to continue?", - "soundEnabled": "Enable Sound Effects", "copyWithCotEnabled": "Copy COT Details", "traceDebugEnabled": "Trace Call", "loggingEnabled": "Enable Logging", diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 7210f89f7..8ce2c0161 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -42,7 +42,6 @@ "contentProtectionEnableDesc": "روشن کردن حفاظت نمایشگر از ضبط پنجره دیپ‌چت به‌دست برنامه‌های هم‌رسانی نمایشگر جلوگیری میکند و از حریم خصوصی محتوای شما محافظت می‌کند. توجه داشته باشید که این ویژگی همه رابط‌ها را کاملاً مخفی نمی‌کند. لطفاً از این ویژگی به‌طور مسئولانه و مطابق با مقررات استفاده کنید. همچنین، برخی برنامه‌های هم‌رسانی نمایشگر ممکن است از این ویژگی پشتیبانی نکنند. در برخی محیط‌ها ممکن است پنجره‌ای سیاه باقی بماند.", "contentProtectionDisableDesc": "خاموش کردن حفاظت نمایشگر به برنامه‌های هم‌رسانی نمایشگر اجازه می‌دهد پنجره دیپ‌چت را ضبط کنند.", "contentProtectionRestartNotice": "تغییر این تنظیم برنامه را بازراه‌اندازی می‌کند. آیا می‌خواهید ادامه دهید؟", - "soundEnabled": "روشن کردن جلوه‌های صوتی", "copyWithCotEnabled": "رونوشت دارای جزئیات COT", "loggingEnabled": "روشن کردن ثبت رخدادها", "loggingDialogTitle": "پذیرش تغییر تنظیم ثبت رخدادها", diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index 860f7cc83..433a611d3 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -42,7 +42,6 @@ "contentProtectionEnableDesc": "L'activation de la protection de l'écran empêche les logiciels de partage d'écran de capturer la fenêtre DeepChat, protégeant ainsi la confidentialité de votre contenu. Notez que cette fonctionnalité ne masquera pas complètement toutes les interfaces. Veuillez utiliser cette fonctionnalité de manière responsable et conformément aux réglementations. De plus, tous les logiciels de partage d'écran ne prennent pas en charge cette fonctionnalité. Certains environnements peuvent laisser une fenêtre noire.", "contentProtectionDisableDesc": "La désactivation de la protection de l'écran permettra aux logiciels de partage d'écran de capturer la fenêtre DeepChat.", "contentProtectionRestartNotice": "La modification de ce paramètre redémarrera l'application. Voulez-vous continuer ?", - "soundEnabled": "Activer le son", "copyWithCotEnabled": "Copier les infos COT", "loggingEnabled": "Activer la journalisation", "loggingDialogTitle": "Confirmer le changement de paramètre de journalisation", diff --git a/src/renderer/src/i18n/he-IL/settings.json b/src/renderer/src/i18n/he-IL/settings.json index 690b08995..06e164b92 100644 --- a/src/renderer/src/i18n/he-IL/settings.json +++ b/src/renderer/src/i18n/he-IL/settings.json @@ -41,7 +41,6 @@ "contentProtectionEnableDesc": "מנע מיישומי שיתוף מסך ללכוד את חלון DeepChat כדי לעזור להגן על פרטיותך. לא כל האפליקציות מכבדות הגדרה זו; בסביבות מסוימות עשוי להופיע חלון שחור.", "contentProtectionDisableDesc": "אפשר ליישומי שיתוף מסך ללכוד את חלון DeepChat.", "contentProtectionRestartNotice": "שינוי הגדרה זו יגרום להפעלה מחדש של האפליקציה. האם להמשיך?", - "soundEnabled": "הפעל אפקטים קוליים", "copyWithCotEnabled": "העתק פרטי COT (שרשרת מחשבה)", "traceDebugEnabled": "מעקב אחר קריאה (Trace Call)", "loggingEnabled": "הפעל רישום לוגים", diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index db687c27d..ddd366d83 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -24,7 +24,6 @@ "contentProtectionEnableDesc": "DeepChatウィンドウの画面共有によるキャプチャを防止し、プライバシー保護に役立ちます。すべてのアプリがこの設定に対応するわけではなく、環境によっては黒いウィンドウが残る場合があります。", "contentProtectionDisableDesc": "画面共有アプリによるDeepChatウィンドウのキャプチャを許可します。", "contentProtectionRestartNotice": "この設定を変更するとアプリケーションが再起動します。続行しますか?", - "soundEnabled": "サウンドを有効にする", "copyWithCotEnabled": "COT情報をコピー", "loggingEnabled": "ログを有効にする", "loggingDialogTitle": "ログ設定の変更確認", diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index 3c77d3d54..5f2310f79 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -42,7 +42,6 @@ "contentProtectionEnableDesc": "화면 보호를 활성화하면 화면 공유 소프트웨어가 DeepChat 창을 캡쳐할 수 없습니다. 이 기능은 모든 인터페이스를 완전히 숨기지 않습니다. 이 기능을 사용할 때는 항상 규정을 준수하세요. 또한, 모든 화면 공유 소프트웨어가 이 기능을 지원하지 않을 수 있습니다. 또한, 일부 환경에서는 검은색 창이 남을 수 있습니다.", "contentProtectionDisableDesc": "화면 보호를 비활성화하면 화면 공유 소프트웨어가 DeepChat 창을 캡쳐할 수 있습니다.", "contentProtectionRestartNotice": "이 설정을 변경하면 프로그램이 재시작됩니다. 계속하시겠습니까?", - "soundEnabled": "소리 활성화", "copyWithCotEnabled": "COT 정보 복사", "loggingEnabled": "로그 활성화", "loggingDialogTitle": "로그 설정 변경 확인", diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json index df5bb0f93..b2c26fb92 100644 --- a/src/renderer/src/i18n/pt-BR/settings.json +++ b/src/renderer/src/i18n/pt-BR/settings.json @@ -41,7 +41,6 @@ "contentProtectionEnableDesc": "Evita que aplicativos de compartilhamento de tela capturem a janela do DeepChat para ajudar a proteger sua privacidade. Nem todos os aplicativos respeitam essa configuração; em alguns ambientes, uma janela preta pode permanecer.", "contentProtectionDisableDesc": "Permite que aplicativos de compartilhamento de tela capturem a janela do DeepChat.", "contentProtectionRestartNotice": "Alterar esta configuração reiniciará o aplicativo. Deseja continuar?", - "soundEnabled": "Habilitar Efeitos Sonoros", "copyWithCotEnabled": "Copiar Detalhes COT", "loggingEnabled": "Habilitar Registro (Logging)", "loggingDialogTitle": "Confirmar Mudança na Configuração de Registro", diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index ec519579d..2effc0a17 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -37,7 +37,6 @@ "testSearchEngineDesc": "Будет выполнен тестовый поиск по запросу \"погода\" с использованием поисковой системы {engine}.", "testSearchEngineNote": "Если поисковая страница требует входа или других действий, вы можете выполнить их в тестовом окне. Пожалуйста, закройте тестовое окно, когда закончите.", "theme": "Тема", - "soundEnabled": "Звуковые уведомления", "copyWithCotEnabled": "Скопировать информацию COT", "loggingEnabled": "Включить логирование", "loggingDialogTitle": "Подтверждение изменения настроек логирования", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 3258a73de..3ba48e831 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -46,7 +46,6 @@ "contentProtectionEnableDesc": "开启投屏保护可以防止投屏软件捕获DeepChat主窗口,用来保护您的内容隐私。请注意,此功能不会彻底隐藏所有界面,请合理合规使用。并且,并不是所有投屏软件都遵守用户隐私设定,该功能可能会在一些不遵守隐私设定的投屏软件上失效,且部分环境中可能会残留一个黑色窗体。", "contentProtectionDisableDesc": "关闭投屏保护将允许投屏软件捕获DeepChat窗口。", "contentProtectionRestartNotice": "切换此设置将导致程序重启,请确认是否继续?", - "soundEnabled": "启用音效", "copyWithCotEnabled": "复制COT信息", "traceDebugEnabled": "追踪调用", "loggingEnabled": "启用日志", diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index 23111183e..37e8a3b1d 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -42,7 +42,6 @@ "contentProtectionEnableDesc": "開啟投屏保護可以防止投屏軟件捕獲DeepChat主窗口,用來保護您的內容隱私。請注意,此功能不會徹底隱藏所有界面,請合理合規使用。並且,並不是所有投屏軟件都遵守用戶隱私設定,該功能可能在一些不遵守隱私設定的投屏軟件上失效,且部分環境中可能會殘留一個黑色窗體。", "contentProtectionDisableDesc": "關閉投屏保護將允許投屏軟件捕獲DeepChat窗口。", "contentProtectionRestartNotice": "切換此設置將導致程序重啟,請確認是否繼續?", - "soundEnabled": "啟用音效", "copyWithCotEnabled": "複製COT資訊", "loggingEnabled": "啟用日誌", "loggingDialogTitle": "確認日誌設定變更", diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 23cb993b1..3b022b1b4 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -42,7 +42,6 @@ "contentProtectionEnableDesc": "開啟畫面保護可以防止錄影軟體擷取 DeepChat 主視窗,用來保護您的內容隱私。請注意,此功能不會徹底隱藏所有介面,請合理合規使用。並且,並不是所有錄影軟體都遵守使用者隱私設定,該功能可能會在一些不遵守隱私設定的錄影軟體上失效,某些環境中可能會殘留一個黑色視窗。", "contentProtectionDisableDesc": "關閉畫面保護將允許錄影軟體擷取 DeepChat 視窗。", "contentProtectionRestartNotice": "切換此設定將會重新啟動應用程式,請問您是否要繼續?", - "soundEnabled": "啟用音效", "copyWithCotEnabled": "複製COT資訊", "loggingEnabled": "啟用日誌", "loggingDialogTitle": "確認日誌設定變更", diff --git a/src/renderer/src/stores/sound.ts b/src/renderer/src/stores/sound.ts deleted file mode 100644 index 8ba0750b8..000000000 --- a/src/renderer/src/stores/sound.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { usePresenter } from '@/composables/usePresenter' -import { CONFIG_EVENTS } from '@/events' -import { defineStore } from 'pinia' -import { ref, onMounted } from 'vue' - -export const useSoundStore = defineStore('sound', () => { - const soundEnabled = ref(false) // 声音是否启用,默认禁用 - const configPresenter = usePresenter('configPresenter') - - // 初始化设置 - const initSound = async () => { - try { - soundEnabled.value = await configPresenter.getSoundEnabled() - setupSoundEnabledListener() - } catch (error) { - console.error('初始化音效失败:', error) - } - } - - // 设置音效开关状态 - const setSoundEnabled = async (enabled: boolean) => { - // 更新本地状态 - soundEnabled.value = Boolean(enabled) - - // 调用ConfigPresenter设置值 - await configPresenter.setSoundEnabled(enabled) - } - - // 获取音效开关状态 - const getSoundEnabled = async (): Promise => { - return await configPresenter.getSoundEnabled() - } - - // 设置音效开关监听器 - const setupSoundEnabledListener = () => { - // 监听音效开关变更事件 - window.electron.ipcRenderer.on( - CONFIG_EVENTS.SOUND_ENABLED_CHANGED, - (_event, enabled: boolean) => { - soundEnabled.value = enabled - } - ) - } - - // 在 store 创建时初始化 - onMounted(async () => { - await initSound() - }) - - return { - soundEnabled, - initSound, - setSoundEnabled, - getSoundEnabled, - setupSoundEnabledListener - } -}) diff --git a/src/shared/types/agent-interface.d.ts b/src/shared/types/agent-interface.d.ts index 82f3b28ac..8a7768016 100644 --- a/src/shared/types/agent-interface.d.ts +++ b/src/shared/types/agent-interface.d.ts @@ -37,6 +37,7 @@ export interface IAgentImplementation { initSession( sessionId: string, config: { + agentId?: string providerId: string modelId: string projectDir?: string | null diff --git a/src/shared/types/chatSettings.ts b/src/shared/types/chatSettings.ts index 16c3c5a63..c1a596270 100644 --- a/src/shared/types/chatSettings.ts +++ b/src/shared/types/chatSettings.ts @@ -15,17 +15,12 @@ export type ChatLanguage = export type ChatTheme = 'dark' | 'light' | 'system' -export type ChatSettingId = - | 'soundEnabled' - | 'copyWithCotEnabled' - | 'language' - | 'theme' - | 'fontSizeLevel' +export type ChatSettingId = 'copyWithCotEnabled' | 'language' | 'theme' | 'fontSizeLevel' export type ChatSettingValue = boolean | number | ChatLanguage | ChatTheme export type ToggleChatSettingRequest = { - setting: 'soundEnabled' | 'copyWithCotEnabled' + setting: 'copyWithCotEnabled' enabled: boolean } diff --git a/src/shared/types/presenters/legacy.presenters.d.ts b/src/shared/types/presenters/legacy.presenters.d.ts index 0f7f7f7e8..b190b84a8 100644 --- a/src/shared/types/presenters/legacy.presenters.d.ts +++ b/src/shared/types/presenters/legacy.presenters.d.ts @@ -546,9 +546,6 @@ export interface IConfigPresenter { getEnabledProviders(): LLM_PROVIDER[] getModelDefaultConfig(modelId: string, providerId?: string): ModelConfig getAllEnabledModels(): Promise<{ providerId: string; models: RENDERER_MODEL_META[] }[]> - // Sound effect settings - getSoundEnabled(): boolean - setSoundEnabled(enabled: boolean): void // Chain of Thought copy settings getCopyWithCotEnabled(): boolean setCopyWithCotEnabled(enabled: boolean): void diff --git a/src/types/i18n.d.ts b/src/types/i18n.d.ts index 1bc01efc9..5e04eb93b 100644 --- a/src/types/i18n.d.ts +++ b/src/types/i18n.d.ts @@ -1117,7 +1117,6 @@ declare module 'vue-i18n' { contentProtectionEnableDesc: string contentProtectionDisableDesc: string contentProtectionRestartNotice: string - soundEnabled: string copyWithCotEnabled: string traceDebugEnabled: string loggingEnabled: string diff --git a/test/main/presenter/agentPresenter/chatSettingsTools.test.ts b/test/main/presenter/agentPresenter/chatSettingsTools.test.ts index 8c2994433..de4fdb3ce 100644 --- a/test/main/presenter/agentPresenter/chatSettingsTools.test.ts +++ b/test/main/presenter/agentPresenter/chatSettingsTools.test.ts @@ -9,8 +9,6 @@ import { describe('ChatSettingsToolHandler', () => { const configPresenter = { - getSoundEnabled: vi.fn(), - setSoundEnabled: vi.fn(), getCopyWithCotEnabled: vi.fn(), setCopyWithCotEnabled: vi.fn(), getSetting: vi.fn(), @@ -43,7 +41,6 @@ describe('ChatSettingsToolHandler', () => { beforeEach(() => { vi.clearAllMocks() - configPresenter.getSoundEnabled.mockReturnValue(false) configPresenter.getCopyWithCotEnabled.mockReturnValue(true) configPresenter.getSetting.mockReturnValue('chat') configPresenter.setTheme.mockResolvedValue(false) @@ -57,34 +54,34 @@ describe('ChatSettingsToolHandler', () => { it('rejects toggle when skill is inactive', async () => { skillPresenter.getActiveSkills.mockResolvedValue([]) const handler = buildHandler() - const result = await handler.toggle({ setting: 'soundEnabled', enabled: true }, 'conv-1') + const result = await handler.toggle({ setting: 'copyWithCotEnabled', enabled: true }, 'conv-1') expect(result.ok).toBe(false) if (!result.ok) { expect(result.errorCode).toBe('skill_inactive') } - expect(configPresenter.setSoundEnabled).not.toHaveBeenCalled() + expect(configPresenter.setCopyWithCotEnabled).not.toHaveBeenCalled() }) it('rejects invalid toggle payloads', async () => { const handler = buildHandler() - const result = await handler.toggle({ setting: 'soundEnabled', enabled: 'true' }, 'conv-1') + const result = await handler.toggle({ setting: 'unknownSetting', enabled: 'true' }, 'conv-1') expect(result.ok).toBe(false) if (!result.ok) { expect(result.errorCode).toBe('invalid_request') } - expect(configPresenter.setSoundEnabled).not.toHaveBeenCalled() + expect(configPresenter.setCopyWithCotEnabled).not.toHaveBeenCalled() }) - it('applies soundEnabled toggle', async () => { + it('applies copyWithCotEnabled toggle', async () => { const handler = buildHandler() - const result = await handler.toggle({ setting: 'soundEnabled', enabled: true }, 'conv-1') + const result = await handler.toggle({ setting: 'copyWithCotEnabled', enabled: false }, 'conv-1') - expect(configPresenter.setSoundEnabled).toHaveBeenCalledWith(true) + expect(configPresenter.setCopyWithCotEnabled).toHaveBeenCalledWith(false) expect(result.ok).toBe(true) if (result.ok) { - expect(result.previousValue).toBe(false) + expect(result.previousValue).toBe(true) } }) diff --git a/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts b/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts index aa654021b..49d89d1db 100644 --- a/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts +++ b/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts @@ -4,6 +4,7 @@ import os from 'os' import path from 'path' import { app } from 'electron' import { DeepChatAgentPresenter } from '@/presenter/deepchatAgentPresenter/index' +import { NewSessionHooksBridge } from '@/presenter/hooksNotifications/newSessionBridge' vi.mock('nanoid', () => ({ nanoid: vi.fn(() => 'mock-msg-id') })) @@ -199,6 +200,7 @@ describe('DeepChatAgentPresenter', () => { let configPresenter: ReturnType let toolPresenter: ReturnType let agent: DeepChatAgentPresenter + let hookDispatcher: { dispatchEvent: ReturnType } let tempHome: string | null = null let getPathSpy: ReturnType | null = null @@ -216,7 +218,14 @@ describe('DeepChatAgentPresenter', () => { llmProvider = createMockLlmProviderPresenter() configPresenter = createMockConfigPresenter() toolPresenter = createMockToolPresenter() - agent = new DeepChatAgentPresenter(llmProvider, configPresenter, sqlitePresenter, toolPresenter) + hookDispatcher = { dispatchEvent: vi.fn() } + agent = new DeepChatAgentPresenter( + llmProvider, + configPresenter, + sqlitePresenter, + toolPresenter, + new NewSessionHooksBridge(hookDispatcher) + ) }) afterEach(async () => { @@ -383,6 +392,111 @@ describe('DeepChatAgentPresenter', () => { ) }) + it('dispatches lifecycle hooks through new session bridge', async () => { + ;(processStream as ReturnType).mockImplementationOnce(async () => ({ + status: 'completed', + stopReason: 'complete', + usage: { totalTokens: 3 } + })) + + await agent.initSession('s1', { + agentId: 'deepchat', + providerId: 'openai', + modelId: 'gpt-4', + projectDir: '/tmp/project' + }) + await agent.processMessage('s1', 'Hello bridge') + + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'UserPromptSubmit', + expect.objectContaining({ + conversationId: 's1', + agentId: 'deepchat', + workdir: '/tmp/project', + promptPreview: 'Hello bridge' + }) + ) + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'SessionStart', + expect.objectContaining({ + conversationId: 's1', + agentId: 'deepchat', + workdir: '/tmp/project' + }) + ) + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'Stop', + expect.objectContaining({ + conversationId: 's1', + stop: expect.objectContaining({ reason: 'complete', userStop: false }) + }) + ) + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'SessionEnd', + expect.objectContaining({ + conversationId: 's1' + }) + ) + }) + + it('dispatches tool and permission hooks through process callbacks', async () => { + ;(processStream as ReturnType).mockImplementationOnce(async (params) => { + params.hooks?.onPreToolUse?.({ + callId: 'tool-1', + name: 'write_file', + params: '{"path":"a.txt"}' + }) + params.hooks?.onPermissionRequest?.( + { + permissionType: 'write', + description: 'Need permission' + }, + { + callId: 'tool-1', + name: 'write_file', + params: '{"path":"a.txt"}' + } + ) + params.hooks?.onPostToolUseFailure?.({ + callId: 'tool-1', + name: 'write_file', + params: '{"path":"a.txt"}', + error: 'permission denied' + }) + return { + status: 'error', + stopReason: 'error', + errorMessage: 'permission denied' + } + }) + + await agent.initSession('s1', { agentId: 'coder', providerId: 'acp', modelId: 'coder' }) + await agent.processMessage('s1', 'Run tool') + + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'PreToolUse', + expect.objectContaining({ + conversationId: 's1', + agentId: 'coder', + tool: expect.objectContaining({ callId: 'tool-1', name: 'write_file' }) + }) + ) + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'PermissionRequest', + expect.objectContaining({ + conversationId: 's1', + permission: expect.objectContaining({ permissionType: 'write' }) + }) + ) + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'PostToolUseFailure', + expect.objectContaining({ + conversationId: 's1', + tool: expect.objectContaining({ error: 'permission denied' }) + }) + ) + }) + it('includes conversation history in LLM call', async () => { // Set up: first user message already in DB as sent const existingMessages = [ diff --git a/test/main/presenter/newAgentPresenter/integration.test.ts b/test/main/presenter/newAgentPresenter/integration.test.ts index 1af8db12e..55824b279 100644 --- a/test/main/presenter/newAgentPresenter/integration.test.ts +++ b/test/main/presenter/newAgentPresenter/integration.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { NewAgentPresenter } from '@/presenter/newAgentPresenter/index' import { DeepChatAgentPresenter } from '@/presenter/deepchatAgentPresenter/index' +import { NewSessionHooksBridge } from '@/presenter/hooksNotifications/newSessionBridge' vi.mock('nanoid', () => { let counter = 0 @@ -541,6 +542,88 @@ describe('Integration: createSession end-to-end', () => { }) }) +describe('Integration: ACP hooks bridge', () => { + let sqlitePresenter: ReturnType + let llmProvider: ReturnType + let configPresenter: ReturnType + let agentPresenter: NewAgentPresenter + let hookDispatcher: { dispatchEvent: ReturnType } + + beforeEach(() => { + vi.clearAllMocks() + sqlitePresenter = createMockSqlitePresenter() + llmProvider = createMockLlmProviderPresenter() + configPresenter = createMockConfigPresenter() + configPresenter.getAcpAgents.mockResolvedValue([{ id: 'coder', name: 'Coder' }]) + hookDispatcher = { dispatchEvent: vi.fn() } + + const deepchatAgent = new DeepChatAgentPresenter( + llmProvider, + configPresenter, + sqlitePresenter, + createMockToolPresenter(), + new NewSessionHooksBridge(hookDispatcher) + ) + agentPresenter = new NewAgentPresenter( + deepchatAgent as any, + llmProvider, + configPresenter, + sqlitePresenter + ) + }) + + it('dispatches lifecycle hooks for ACP sessions through the new bridge', async () => { + const session = await agentPresenter.createSession( + { + agentId: 'coder', + providerId: 'acp', + modelId: 'coder', + message: 'Inspect workspace', + projectDir: '/tmp/acp-project' + }, + 1 + ) + + await new Promise((r) => setTimeout(r, 50)) + + expect(session.agentId).toBe('coder') + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'UserPromptSubmit', + expect.objectContaining({ + conversationId: session.id, + agentId: 'coder', + workdir: '/tmp/acp-project', + providerId: 'acp', + modelId: 'coder', + promptPreview: 'Inspect workspace' + }) + ) + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'SessionStart', + expect.objectContaining({ + conversationId: session.id, + agentId: 'coder', + workdir: '/tmp/acp-project', + providerId: 'acp', + modelId: 'coder' + }) + ) + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'Stop', + expect.objectContaining({ + conversationId: session.id, + stop: expect.objectContaining({ userStop: false }) + }) + ) + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'SessionEnd', + expect.objectContaining({ + conversationId: session.id + }) + ) + }) +}) + describe('Integration: multi-turn context', () => { let sqlitePresenter: ReturnType let llmProvider: ReturnType From a2eac8a0b2bc0ae5b98db1ed3aba1714306c6ed3 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sun, 8 Mar 2026 17:02:10 +0800 Subject: [PATCH 2/3] fix: chatstatusbar width --- src/renderer/src/components/chat/ChatStatusBar.vue | 11 ++++++++++- src/renderer/src/pages/ChatPage.vue | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/renderer/src/components/chat/ChatStatusBar.vue b/src/renderer/src/components/chat/ChatStatusBar.vue index 6261514f4..9eaaa42d0 100644 --- a/src/renderer/src/components/chat/ChatStatusBar.vue +++ b/src/renderer/src/components/chat/ChatStatusBar.vue @@ -1,5 +1,5 @@ - + From 9e4a8206a8bd7ca365c8d49a743d57c0186eb2b7 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sun, 8 Mar 2026 17:44:19 +0800 Subject: [PATCH 3/3] fix: review issue --- .../deepchatAgentPresenter/dispatch.ts | 18 ++- .../presenter/deepchatAgentPresenter/index.ts | 116 ++++++++++++++---- .../deepchatAgentPresenter.test.ts | 65 ++++++++++ .../deepchatAgentPresenter/dispatch.test.ts | 109 +++++++++++++++- 4 files changed, 274 insertions(+), 34 deletions(-) diff --git a/src/main/presenter/deepchatAgentPresenter/dispatch.ts b/src/main/presenter/deepchatAgentPresenter/dispatch.ts index c7c2c82eb..79db30b22 100644 --- a/src/main/presenter/deepchatAgentPresenter/dispatch.ts +++ b/src/main/presenter/deepchatAgentPresenter/dispatch.ts @@ -443,12 +443,6 @@ export async function executeTools( } try { - hooks?.onPreToolUse?.({ - callId: tc.id, - name: tc.name, - params: tc.arguments - }) - if (toolCall.function.name === QUESTION_TOOL_NAME) { const parsedQuestion = parseQuestionToolArgs(tc.arguments) if (!parsedQuestion.success) { @@ -511,6 +505,12 @@ export async function executeTools( } } + hooks?.onPreToolUse?.({ + callId: tc.id, + name: tc.name, + params: tc.arguments + }) + const toolCallResult = await toolPresenter.callTool(toolCall) let toolRawData = toolCallResult.rawData @@ -580,6 +580,12 @@ export async function executeTools( if (guardedResult.kind === 'terminal_error') { updateToolCallBlock(state.blocks, tc.id, guardedResult.message, true) + hooks?.onPostToolUseFailure?.({ + callId: tc.id, + name: tc.name, + params: tc.arguments, + error: guardedResult.message + }) state.dirty = true executed += 1 flushBlocksToRenderer(io, state.blocks) diff --git a/src/main/presenter/deepchatAgentPresenter/index.ts b/src/main/presenter/deepchatAgentPresenter/index.ts index ddaf8fdd6..417e74dcc 100644 --- a/src/main/presenter/deepchatAgentPresenter/index.ts +++ b/src/main/presenter/deepchatAgentPresenter/index.ts @@ -93,6 +93,7 @@ const isVerbosity = (value: unknown): value is SessionGenerationSettings['verbos export class DeepChatAgentPresenter implements IAgentImplementation { private readonly llmProviderPresenter: ILlmProviderPresenter private readonly configPresenter: IConfigPresenter + private readonly sqlitePresenter: SQLitePresenter private readonly toolPresenter: IToolPresenter | null private readonly sessionStore: DeepChatSessionStore private readonly messageStore: DeepChatMessageStore @@ -118,6 +119,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { ) { this.llmProviderPresenter = llmProviderPresenter this.configPresenter = configPresenter + this.sqlitePresenter = sqlitePresenter this.toolPresenter = toolPresenter ?? null this.sessionStore = new DeepChatSessionStore(sqlitePresenter) this.messageStore = new DeepChatMessageStore(sqlitePresenter) @@ -165,7 +167,10 @@ export class DeepChatAgentPresenter implements IAgentImplementation { permissionMode, generationSettings ) - this.sessionAgentIds.set(sessionId, config.agentId?.trim() || 'deepchat') + this.sessionAgentIds.set( + sessionId, + config.agentId?.trim() || this.getSessionAgentId(sessionId) || 'deepchat' + ) this.sessionProjectDirs.set(sessionId, projectDir) this.sessionGenerationSettings.set(sessionId, generationSettings) this.runtimeState.set(sessionId, { @@ -198,6 +203,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { async getSessionState(sessionId: string): Promise { const state = this.runtimeState.get(sessionId) if (state) { + this.getSessionAgentId(sessionId) if (this.hasPendingInteractions(sessionId)) { state.status = 'generating' } @@ -208,6 +214,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { const dbSession = this.sessionStore.get(sessionId) if (!dbSession) return null + this.getSessionAgentId(sessionId) const rebuilt: DeepChatSessionState = { status: this.hasPendingInteractions(sessionId) ? 'generating' : 'idle', providerId: dbSession.provider_id, @@ -398,6 +405,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { let waitingForUserMessage = false let resumeBudgetToolCall: ResumeBudgetToolCall | null = null + let emitResolvedToolHook: (() => void) | null = null const actionBlock = blocks[currentEntry.blockIndex] const toolCall = actionBlock.tool_call if (!toolCall?.id) { @@ -431,6 +439,8 @@ export class DeepChatAgentPresenter implements IAgentImplementation { const permissionPayload = this.parsePermissionPayload(actionBlock) const permissionType = permissionPayload?.permissionType ?? 'write' const state = this.runtimeState.get(sessionId) + const projectDir = this.resolveProjectDir(sessionId) + let shouldDispatchResolvedToolHook = false if (response.granted) { this.markPermissionResolved(actionBlock, true, permissionType) @@ -440,7 +450,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { messageId, providerId: state?.providerId, modelId: state?.modelId, - projectDir: this.resolveProjectDir(sessionId), + projectDir, tool: { callId: toolCall.id, name: toolCall.name, @@ -454,7 +464,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { messageId, providerId: state?.providerId, modelId: state?.modelId, - projectDir: this.resolveProjectDir(sessionId), + projectDir, tool: { callId: toolCall.id, name: toolCall.name, @@ -476,7 +486,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { messageId, providerId: state?.providerId, modelId: state?.modelId, - projectDir: this.resolveProjectDir(sessionId), + projectDir, stop: { reason: 'error', userStop: false } }) this.dispatchHook('SessionEnd', { @@ -484,32 +494,12 @@ export class DeepChatAgentPresenter implements IAgentImplementation { messageId, providerId: state?.providerId, modelId: state?.modelId, - projectDir: this.resolveProjectDir(sessionId), + projectDir, error: { message: execution.terminalError } }) this.setSessionStatus(sessionId, 'error') return { resumed: false } } - this.dispatchHook(execution.isError ? 'PostToolUseFailure' : 'PostToolUse', { - sessionId, - messageId, - providerId: state?.providerId, - modelId: state?.modelId, - projectDir: this.resolveProjectDir(sessionId), - tool: execution.isError - ? { - callId: toolCall.id, - name: toolCall.name, - params: toolCall.params, - error: execution.responseText - } - : { - callId: toolCall.id, - name: toolCall.name, - params: toolCall.params, - response: execution.responseText - } - }) this.updateToolCallResponse( blocks, toolCall.id, @@ -528,7 +518,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { messageId, providerId: state?.providerId, modelId: state?.modelId, - projectDir: this.resolveProjectDir(sessionId), + projectDir, permission: execution.permissionRequest, tool: { callId: toolCall.id, @@ -544,11 +534,28 @@ export class DeepChatAgentPresenter implements IAgentImplementation { permissionType: execution.permissionRequest.permissionType, permissionRequest: JSON.stringify(execution.permissionRequest) } + } else { + shouldDispatchResolvedToolHook = true } } else { this.markPermissionResolved(actionBlock, false, permissionType) this.updateToolCallResponse(blocks, toolCall.id, 'User denied the request.', true) + shouldDispatchResolvedToolHook = true } + + emitResolvedToolHook = shouldDispatchResolvedToolHook + ? () => { + this.dispatchResolvedToolHook({ + sessionId, + messageId, + providerId: state?.providerId, + modelId: state?.modelId, + projectDir, + blocks, + toolCall + }) + } + : null } else { throw new Error(`Unsupported action type: ${actionBlock.action_type}`) } @@ -558,12 +565,14 @@ export class DeepChatAgentPresenter implements IAgentImplementation { this.emitMessageRefresh(sessionId, messageId) if (remainingPending.length > 0) { + emitResolvedToolHook?.() this.messageStore.updateMessageStatus(messageId, 'pending') this.setSessionStatus(sessionId, 'generating') return { resumed: false } } if (waitingForUserMessage) { + emitResolvedToolHook?.() this.messageStore.updateMessageStatus(messageId, 'sent') this.setSessionStatus(sessionId, 'idle') return { resumed: false, waitingForUserMessage: true } @@ -575,6 +584,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { blocks, resumeBudgetToolCall ) + emitResolvedToolHook?.() return { resumed } } finally { this.interactionLocks.delete(lockKey) @@ -765,13 +775,65 @@ export class DeepChatAgentPresenter implements IAgentImplementation { try { this.hooksBridge?.dispatch(event, { ...context, - agentId: this.sessionAgentIds.get(context.sessionId) ?? 'deepchat' + agentId: this.getSessionAgentId(context.sessionId) ?? 'deepchat' }) } catch (error) { console.warn(`[DeepChatAgent] Failed to dispatch ${event} hook:`, error) } } + private getSessionAgentId(sessionId: string): string | undefined { + const cached = this.sessionAgentIds.get(sessionId)?.trim() + if (cached) { + return cached + } + + const persisted = this.sqlitePresenter.newSessionsTable?.get(sessionId)?.agent_id?.trim() + if (persisted) { + this.sessionAgentIds.set(sessionId, persisted) + return persisted + } + + return undefined + } + + private dispatchResolvedToolHook(params: { + sessionId: string + messageId: string + providerId?: string + modelId?: string + projectDir?: string | null + blocks: AssistantMessageBlock[] + toolCall: NonNullable + }): void { + const resolvedBlock = params.blocks.find( + (block) => block.type === 'tool_call' && block.tool_call?.id === params.toolCall.id + ) + const responseText = resolvedBlock?.tool_call?.response ?? '' + const isError = resolvedBlock?.status === 'error' + + this.dispatchHook(isError ? 'PostToolUseFailure' : 'PostToolUse', { + sessionId: params.sessionId, + messageId: params.messageId, + providerId: params.providerId, + modelId: params.modelId, + projectDir: params.projectDir, + tool: isError + ? { + callId: params.toolCall.id, + name: params.toolCall.name, + params: params.toolCall.params, + error: responseText + } + : { + callId: params.toolCall.id, + name: params.toolCall.name, + params: params.toolCall.params, + response: responseText + } + }) + } + async getMessages(sessionId: string): Promise { return this.messageStore.getMessages(sessionId) } diff --git a/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts b/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts index 49d89d1db..f92460f33 100644 --- a/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts +++ b/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts @@ -77,6 +77,9 @@ function createMockSqlitePresenter() { summary_updated_at: null } return { + newSessionsTable: { + get: vi.fn() + }, deepchatSessionsTable: { create: vi.fn(), get: vi.fn(), @@ -439,6 +442,34 @@ describe('DeepChatAgentPresenter', () => { ) }) + it('rehydrates agentId from persisted new session rows before dispatching hooks', async () => { + sqlitePresenter.newSessionsTable.get.mockReturnValue({ + id: 's1', + agent_id: 'coder' + }) + sqlitePresenter.deepchatSessionsTable.get.mockReturnValue({ + id: 's1', + provider_id: 'acp', + model_id: 'coder', + permission_mode: 'full_access' + }) + ;(processStream as ReturnType).mockImplementationOnce(async () => ({ + status: 'completed', + stopReason: 'complete' + })) + + await agent.getSessionState('s1') + await agent.processMessage('s1', 'Reopened session', { projectDir: '/tmp/project' }) + + expect(hookDispatcher.dispatchEvent).toHaveBeenCalledWith( + 'UserPromptSubmit', + expect.objectContaining({ + conversationId: 's1', + agentId: 'coder' + }) + ) + }) + it('dispatches tool and permission hooks through process callbacks', async () => { ;(processStream as ReturnType).mockImplementationOnce(async (params) => { params.hooks?.onPreToolUse?.({ @@ -1836,6 +1867,23 @@ describe('DeepChatAgentPresenter', () => { 'remaining context window is insufficient' ) expect(updatedBlocks[0].tool_call.response).not.toContain('[Tool output offloaded]') + const postToolUseCalls = hookDispatcher.dispatchEvent.mock.calls.filter( + ([event]) => event === 'PostToolUse' + ) + const postToolUseFailureCalls = hookDispatcher.dispatchEvent.mock.calls.filter( + ([event]) => event === 'PostToolUseFailure' + ) + expect(postToolUseCalls).toHaveLength(0) + expect(postToolUseFailureCalls).toHaveLength(1) + expect(postToolUseFailureCalls[0][1]).toEqual( + expect.objectContaining({ + conversationId: 's1', + tool: expect.objectContaining({ + callId: 'tc1', + error: expect.stringContaining('remaining context window is insufficient') + }) + }) + ) await expect( fs.access(path.join(tempHome, '.deepchat', 'sessions', 's1', 'tool_tc1.offload')) ).rejects.toThrow() @@ -1879,6 +1927,23 @@ describe('DeepChatAgentPresenter', () => { expect(updatedBlocks[0].tool_call.response).toBe('User denied the request.') expect(updatedBlocks[0].status).toBe('error') expect(updatedBlocks[1].status).toBe('denied') + const postToolUseCalls = hookDispatcher.dispatchEvent.mock.calls.filter( + ([event]) => event === 'PostToolUse' + ) + const postToolUseFailureCalls = hookDispatcher.dispatchEvent.mock.calls.filter( + ([event]) => event === 'PostToolUseFailure' + ) + expect(postToolUseCalls).toHaveLength(0) + expect(postToolUseFailureCalls).toHaveLength(1) + expect(postToolUseFailureCalls[0][1]).toEqual( + expect.objectContaining({ + conversationId: 's1', + tool: expect.objectContaining({ + callId: 'tc1', + error: 'User denied the request.' + }) + }) + ) expect(processStream).toHaveBeenCalledTimes(1) }) }) diff --git a/test/main/presenter/deepchatAgentPresenter/dispatch.test.ts b/test/main/presenter/deepchatAgentPresenter/dispatch.test.ts index 33d335d51..17a90cacc 100644 --- a/test/main/presenter/deepchatAgentPresenter/dispatch.test.ts +++ b/test/main/presenter/deepchatAgentPresenter/dispatch.test.ts @@ -8,6 +8,7 @@ import { createState } from '@/presenter/deepchatAgentPresenter/types' import type { MCPToolDefinition } from '@shared/presenter' import type { IToolPresenter } from '@shared/types/presenters/tool.presenter' import { ToolOutputGuard } from '@/presenter/deepchatAgentPresenter/toolOutputGuard' +import { QUESTION_TOOL_NAME } from '@/presenter/agentPresenter/tools/questionTool' vi.mock('@/eventbus', () => ({ eventBus: { sendToRenderer: vi.fn() }, @@ -163,6 +164,99 @@ describe('dispatch', () => { expect(toolBlock!.status).toBe('success') }) + it('does not emit PreToolUse for question interactions that pause execution', async () => { + const hooks = { + onPreToolUse: vi.fn(), + onPermissionRequest: vi.fn(), + onPostToolUse: vi.fn(), + onPostToolUseFailure: vi.fn() + } + const toolPresenter = createMockToolPresenter() + + state.blocks.push({ + type: 'tool_call', + content: '', + status: 'pending', + timestamp: Date.now(), + tool_call: { id: 'tc1', name: QUESTION_TOOL_NAME, params: '', response: '' } + }) + state.completedToolCalls = [ + { + id: 'tc1', + name: QUESTION_TOOL_NAME, + arguments: JSON.stringify({ + question: 'Continue?', + options: [{ label: 'Yes' }] + }) + } + ] + + const result = await executeTools( + state, + [], + 0, + [makeTool(QUESTION_TOOL_NAME)], + toolPresenter, + 'gpt-4', + io, + 'full_access', + new ToolOutputGuard(), + 32000, + 1024, + hooks + ) + + expect(result.pendingInteractions).toHaveLength(1) + expect(hooks.onPreToolUse).not.toHaveBeenCalled() + expect(toolPresenter.callTool).not.toHaveBeenCalled() + }) + + it('does not emit PreToolUse before a pre-checked permission pause', async () => { + const hooks = { + onPreToolUse: vi.fn(), + onPermissionRequest: vi.fn(), + onPostToolUse: vi.fn(), + onPostToolUseFailure: vi.fn() + } + const toolPresenter = createMockToolPresenter() as IToolPresenter & { + preCheckToolPermission: ReturnType + } + toolPresenter.preCheckToolPermission = vi.fn().mockResolvedValue({ + needsPermission: true, + permissionType: 'write', + description: 'Need permission' + }) + + state.blocks.push({ + type: 'tool_call', + content: '', + status: 'pending', + timestamp: Date.now(), + tool_call: { id: 'tc1', name: 'write_file', params: '{"path":"a.txt"}', response: '' } + }) + state.completedToolCalls = [{ id: 'tc1', name: 'write_file', arguments: '{"path":"a.txt"}' }] + + const result = await executeTools( + state, + [], + 0, + [makeTool('write_file')], + toolPresenter, + 'gpt-4', + io, + 'default', + new ToolOutputGuard(), + 32000, + 1024, + hooks + ) + + expect(result.pendingInteractions).toHaveLength(1) + expect(hooks.onPreToolUse).not.toHaveBeenCalled() + expect(hooks.onPermissionRequest).toHaveBeenCalledTimes(1) + expect(toolPresenter.callTool).not.toHaveBeenCalled() + }) + it('enriches tool_call blocks with server info', async () => { const tools = [makeTool('get_weather')] const toolPresenter = createMockToolPresenter({ get_weather: 'Sunny' }) @@ -606,6 +700,12 @@ describe('dispatch', () => { const longScreenshot = JSON.stringify({ data: 'x'.repeat(7000) }) const toolPresenter = createMockToolPresenter({ yo_browser_cdp_send: longScreenshot }) const conversation: any[] = [] + const hooks = { + onPreToolUse: vi.fn(), + onPermissionRequest: vi.fn(), + onPostToolUse: vi.fn(), + onPostToolUseFailure: vi.fn() + } state.blocks.push({ type: 'tool_call', @@ -638,12 +738,19 @@ describe('dispatch', () => { 'full_access', new ToolOutputGuard(), 1, - 1 + 1, + hooks ) expect(executed.terminalError).toContain('remaining context window is too small') expect(conversation.find((message: any) => message.role === 'tool')).toBeUndefined() expect(state.blocks[0].status).toBe('error') + expect(hooks.onPostToolUseFailure).toHaveBeenCalledWith({ + callId: 'tc1', + name: 'yo_browser_cdp_send', + params: '{"method":"Page.captureScreenshot"}', + error: expect.stringContaining('remaining context window is too small') + }) await expect( fs.access(path.join(tempHome, '.deepchat', 'sessions', 's1', 'tool_tc1.offload')) ).rejects.toThrow()