From 1641cba234f12a3732964d0ff7a2c6f25ca13847 Mon Sep 17 00:00:00 2001 From: duskzhen Date: Wed, 29 Oct 2025 17:59:44 +0800 Subject: [PATCH 1/3] fix(mcp): restore permission approval flow --- src/main/presenter/threadPresenter/index.ts | 40 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index 451d0088c..b870cbe0a 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -187,6 +187,7 @@ export class ThreadPresenter implements IThreadPresenter { tool_call_server_description, tool_call_response_raw, tool_call, + permission_request, totalUsage, image_data } = msg @@ -383,6 +384,26 @@ export class ThreadPresenter implements IThreadPresenter { } } } else if (tool_call === 'permission-required') { + const permissionType = permission_request?.permissionType ?? 'write' + const extra: Record = { + needsUserAction: true, + permissionType + } + + const serverName = permission_request?.serverName || tool_call_server_name + if (serverName) { + extra.serverName = serverName + } + + const toolName = permission_request?.toolName || tool_call_name + if (toolName) { + extra.toolName = toolName + } + + if (permission_request) { + extra.permissionRequest = JSON.stringify(permission_request) + } + if (lastBlock && lastBlock.type === 'tool_call' && lastBlock.tool_call) { lastBlock.status = 'success' } @@ -401,7 +422,8 @@ export class ThreadPresenter implements IThreadPresenter { server_name: tool_call_server_name, server_icons: tool_call_server_icons, server_description: tool_call_server_description - } + }, + extra }) this.searchingMessages.add(eventId) @@ -412,8 +434,17 @@ export class ThreadPresenter implements IThreadPresenter { lastBlock.type === 'action' && lastBlock.action_type === 'tool_call_permission' ) { - lastBlock.status = 'success' + lastBlock.status = 'granted' lastBlock.content = 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 + } + } } this.searchingMessages.delete(eventId) state.isSearching = false @@ -423,8 +454,11 @@ export class ThreadPresenter implements IThreadPresenter { lastBlock.type === 'action' && lastBlock.action_type === 'tool_call_permission' ) { - lastBlock.status = 'error' + lastBlock.status = 'denied' lastBlock.content = tool_call_response || '' + if (lastBlock.extra) { + lastBlock.extra.needsUserAction = false + } } this.searchingMessages.delete(eventId) state.isSearching = false From 9cbdae094af8dd879d6406f381862d11de4138b1 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 29 Oct 2025 18:59:33 +0800 Subject: [PATCH 2/3] fix: permission request in loop --- src/main/presenter/threadPresenter/index.ts | 478 ++++++++++++++++-- .../threadPresenter/promptBuilder.ts | 133 ++++- src/main/presenter/threadPresenter/types.ts | 2 + .../message/MessageBlockPermissionRequest.vue | 1 + 4 files changed, 577 insertions(+), 37 deletions(-) diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index b870cbe0a..1210b9fad 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -11,6 +11,7 @@ import { IConfigPresenter, ILlmProviderPresenter, MCPToolResponse, + MCPToolDefinition, ChatMessage, LLMAgentEventData } from '@shared/presenter' @@ -35,7 +36,11 @@ import { formatUserMessageContent, getNormalizedUserMessageText } from './messageContent' -import { preparePromptContent, buildContinueToolCallContext } from './promptBuilder' +import { + preparePromptContent, + buildContinueToolCallContext, + buildPostToolExecutionContext +} from './promptBuilder' import { buildConversationExportContent, generateExportFilename, @@ -426,6 +431,17 @@ export class ThreadPresenter implements IThreadPresenter { extra }) + if (state) { + state.pendingToolCall = { + id: tool_call_id || '', + name: tool_call_name || '', + params: tool_call_params || '', + serverName: tool_call_server_name, + serverIcons: tool_call_server_icons, + serverDescription: tool_call_server_description + } + } + this.searchingMessages.add(eventId) state.isSearching = true } else if (tool_call === 'permission-granted') { @@ -448,6 +464,16 @@ export class ThreadPresenter implements IThreadPresenter { } this.searchingMessages.delete(eventId) state.isSearching = false + if (state) { + state.pendingToolCall = { + id: tool_call_id || '', + name: tool_call_name || '', + params: tool_call_params || '', + serverName: tool_call_server_name, + serverIcons: tool_call_server_icons, + serverDescription: tool_call_server_description + } + } } else if (tool_call === 'permission-denied') { if ( lastBlock && @@ -462,6 +488,9 @@ export class ThreadPresenter implements IThreadPresenter { } this.searchingMessages.delete(eventId) state.isSearching = false + if (state) { + state.pendingToolCall = undefined + } } else if (tool_call === 'continue') { if ( lastBlock && @@ -494,6 +523,9 @@ export class ThreadPresenter implements IThreadPresenter { } this.searchingMessages.delete(eventId) state.isSearching = false + if (state) { + state.pendingToolCall = undefined + } } } @@ -2799,6 +2831,52 @@ export class ThreadPresenter implements IThreadPresenter { } } + // 2.1 同步内存中的生成状态,避免后续覆盖数据库中的授权结果 + const generatingState = this.generatingMessages.get(messageId) + if (generatingState) { + const statePermissionBlockIndex = generatingState.message.content.findIndex( + (block) => + block.type === 'action' && + block.action_type === 'tool_call_permission' && + block.tool_call?.id === toolCallId + ) + + if (statePermissionBlockIndex !== -1) { + const statePermissionBlock = generatingState.message.content[statePermissionBlockIndex] + generatingState.message.content[statePermissionBlockIndex] = { + ...statePermissionBlock, + ...permissionBlock, + extra: permissionBlock.extra + ? { + ...permissionBlock.extra + } + : undefined, + tool_call: permissionBlock.tool_call + ? { + ...permissionBlock.tool_call + } + : undefined + } + } else { + console.warn( + `[ThreadPresenter] Permission block not found in generating state, synchronizing snapshot` + ) + generatingState.message.content = content.map((block) => ({ + ...block, + extra: block.extra + ? { + ...block.extra + } + : undefined, + tool_call: block.tool_call + ? { + ...block.tool_call + } + : undefined + })) + } + } + // 3. 保存消息更新 await this.messageManager.editMessage(messageId, JSON.stringify(content)) console.log(`[ThreadPresenter] Updated permission block status to: ${permissionBlock.status}`) @@ -2845,10 +2923,10 @@ export class ThreadPresenter implements IThreadPresenter { await this.restartAgentLoopAfterPermission(messageId) } else { console.log( - `[ThreadPresenter] Permission denied, ending generation for message: ${messageId}` + `[ThreadPresenter] Permission denied, continuing generation with error context for message: ${messageId}` ) - // 6. 权限被拒绝 - 正常结束消息 - await this.finalizeMessageAfterPermissionDenied(messageId) + // 6. 权限被拒绝 - 继续agent loop,将拒绝信息作为工具调用失败结果 + await this.continueAfterPermissionDenied(messageId) } } catch (error) { console.error(`[ThreadPresenter] Failed to handle permission response:`, error) @@ -2928,7 +3006,18 @@ export class ThreadPresenter implements IThreadPresenter { const state = this.generatingMessages.get(messageId) if (state) { console.log(`[ThreadPresenter] Message still in generating state, resuming from memory`) - await this.resumeStreamCompletion(conversationId, messageId) + if (state.pendingToolCall) { + console.log( + `[ThreadPresenter] Pending tool call detected after permission grant, executing tool before resuming` + ) + await this.resumeAfterPermissionWithPendingToolCall( + state, + message as AssistantMessage, + conversationId + ) + } else { + await this.resumeStreamCompletion(conversationId, messageId) + } return } @@ -2946,7 +3035,8 @@ export class ThreadPresenter implements IThreadPresenter { promptTokens: 0, reasoningStartTime: null, reasoningEndTime: null, - lastReasoningTime: null + lastReasoningTime: null, + pendingToolCall: this.findPendingToolCallAfterPermission(content) || undefined }) console.log(`[ThreadPresenter] Created new generating state for message: ${messageId}`) @@ -2969,50 +3059,164 @@ export class ThreadPresenter implements IThreadPresenter { } } - // 权限被拒绝后完成消息 - private async finalizeMessageAfterPermissionDenied(messageId: string): Promise { - console.log(`[ThreadPresenter] Finalizing message after permission denied: ${messageId}`) + // 权限被拒绝后继续生成,将拒绝信息作为工具调用失败告知LLM + private async continueAfterPermissionDenied(messageId: string): Promise { + console.log(`[ThreadPresenter] Continuing generation after permission denied: ${messageId}`) try { const message = await this.messageManager.getMessage(messageId) - if (!message) return + if (!message || message.role !== 'assistant') { + const errorMsg = `Message not found or not an assistant message (messageId: ${messageId})` + console.error(`[ThreadPresenter] ${errorMsg}`) + throw new Error(errorMsg) + } + const conversationId = message.conversationId const content = message.content as AssistantMessageBlock[] - // 将所有loading状态的块设为success,但保留权限块的状态 - content.forEach((block) => { - if (block.type === 'action' && block.action_type === 'tool_call_permission') { - // 权限块保持其当前状态(granted/denied/error) - return - } - if (block.status === 'loading') { - block.status = 'success' - } + // 查找被拒绝的权限块 + const deniedPermissionBlock = content.find( + (block) => + block.type === 'action' && + block.action_type === 'tool_call_permission' && + block.status === 'denied' + ) + + if (!deniedPermissionBlock?.tool_call) { + console.warn(`[ThreadPresenter] No denied permission block found for message: ${messageId}`) + return + } + + const toolCall = deniedPermissionBlock.tool_call + + // 构建工具调用失败的响应消息 + const errorMessage = `Tool execution failed: Permission denied by user for ${toolCall.name || 'this tool'}` + + console.log(`[ThreadPresenter] Notifying LLM about permission denial: ${errorMessage}`) + + // 发送工具调用失败事件给renderer + eventBus.sendToRenderer(STREAM_EVENTS.RESPONSE, SendTarget.ALL_WINDOWS, { + eventId: messageId, + tool_call: 'end', + 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 }) - // 添加权限被拒绝的提示 - content.push({ - type: 'error', - content: 'Permission denied by user', - status: 'error', - timestamp: Date.now() + // 获取或创建生成状态 + let state = this.generatingMessages.get(messageId) + if (!state) { + // 重新创建生成状态 + const assistantMessage = message as AssistantMessage + state = { + message: assistantMessage, + conversationId, + startTime: Date.now(), + firstTokenTime: null, + promptTokens: 0, + reasoningStartTime: null, + reasoningEndTime: null, + lastReasoningTime: null + } + this.generatingMessages.set(messageId, state) + } + + // 清除pending tool call(如果有) + state.pendingToolCall = undefined + + // 获取会话和上下文 + const { conversation, contextMessages, userMessage } = await this.prepareConversationContext( + conversationId, + messageId + ) + + const { + providerId, + modelId, + temperature, + maxTokens, + enabledMcpTools, + thinkingBudget, + reasoningEffort, + verbosity, + enableSearch, + forcedSearch, + searchStrategy + } = conversation.settings + + const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) + + // 将 snake_case 转换为 camelCase 并构建包含工具调用失败信息的上下文 + const completedToolCall = { + id: toolCall.id || '', + name: toolCall.name || '', + params: toolCall.params || '', + response: errorMessage, + // 注意:从 snake_case 转换为 camelCase + serverName: toolCall.server_name, + serverIcons: toolCall.server_icons, + serverDescription: toolCall.server_description + } + + const finalContent = await buildPostToolExecutionContext({ + conversation, + contextMessages, + userMessage, + currentAssistantMessage: state.message, + completedToolCall, + modelConfig }) - await this.messageManager.editMessage(messageId, JSON.stringify(content)) - await this.messageManager.updateMessageStatus(messageId, 'sent') + console.log( + `[ThreadPresenter] Restarting agent loop with tool failure context for message: ${messageId}` + ) + + // 继续agent loop + const stream = this.llmProviderPresenter.startStreamCompletion( + providerId, + finalContent, + modelId, + messageId, + temperature, + maxTokens, + enabledMcpTools, + thinkingBudget, + reasoningEffort, + verbosity, + enableSearch, + forcedSearch, + searchStrategy + ) + + for await (const event of stream) { + const msg = event.data + if (event.type === 'response') { + await this.handleLLMAgentResponse(msg) + } else if (event.type === 'error') { + await this.handleLLMAgentError(msg) + } else if (event.type === 'end') { + await this.handleLLMAgentEnd(msg) + } + } + + console.log(`[ThreadPresenter] Successfully continued after permission denial: ${messageId}`) + } catch (error) { + console.error(`[ThreadPresenter] Failed to continue after permission denial:`, error) // 清理生成状态 this.generatingMessages.delete(messageId) - // 发送结束事件 - eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, { - eventId: messageId, - userStop: false - }) + try { + await this.messageManager.handleMessageError(messageId, String(error)) + } catch (updateError) { + console.error(`[ThreadPresenter] Failed to update message error status:`, updateError) + } - console.log(`[ThreadPresenter] Message finalized after permission denial: ${messageId}`) - } catch (error) { - console.error(`[ThreadPresenter] Failed to finalize message after permission denial:`, error) + throw error } } @@ -3139,6 +3343,210 @@ export class ThreadPresenter implements IThreadPresenter { } } + private async resumeAfterPermissionWithPendingToolCall( + state: GeneratingMessageState, + message: AssistantMessage, + conversationId: string + ): Promise { + const pendingToolCall = state.pendingToolCall + if (!pendingToolCall || !pendingToolCall.id || !pendingToolCall.name) { + console.warn( + `[ThreadPresenter] Pending tool call data missing, falling back to standard resume` + ) + await this.resumeStreamCompletion(conversationId, message.id) + return + } + + try { + const { conversation, contextMessages, userMessage } = await this.prepareConversationContext( + conversationId, + message.id + ) + + const { + providerId, + modelId, + temperature, + maxTokens, + enabledMcpTools, + thinkingBudget, + reasoningEffort, + verbosity, + enableSearch, + forcedSearch, + searchStrategy + } = conversation.settings + + const modelConfig = this.configPresenter.getModelConfig(modelId, providerId) + if (!modelConfig) { + console.warn( + `[ThreadPresenter] Model config not found for ${modelId} (${providerId}), falling back to standard resume` + ) + await this.resumeStreamCompletion(conversationId, message.id) + return + } + + let toolDef: MCPToolDefinition | undefined + try { + const toolDefinitions = await presenter.mcpPresenter.getAllToolDefinitions(enabledMcpTools) + toolDef = toolDefinitions.find((definition) => { + if (definition.function.name !== pendingToolCall.name) { + return false + } + if (pendingToolCall.serverName) { + return definition.server.name === pendingToolCall.serverName + } + return true + }) + } catch (error) { + console.error('[ThreadPresenter] Failed to load tool definitions:', error) + } + + if (!toolDef) { + console.warn( + `[ThreadPresenter] Tool definition not found for ${pendingToolCall.name}, falling back to standard resume` + ) + await this.resumeStreamCompletion(conversationId, message.id) + return + } + + const resolvedToolDef = toolDef as MCPToolDefinition + + await this.handleLLMAgentResponse({ + eventId: message.id, + tool_call: 'running', + tool_call_id: pendingToolCall.id, + tool_call_name: pendingToolCall.name, + tool_call_params: pendingToolCall.params, + tool_call_server_name: resolvedToolDef.server.name, + tool_call_server_icons: resolvedToolDef.server.icons, + tool_call_server_description: resolvedToolDef.server.description + }) + + let toolContent = '' + let toolRawData: MCPToolResponse | null = null + try { + const toolCallResult = await presenter.mcpPresenter.callTool({ + id: pendingToolCall.id, + type: 'function', + function: { + name: pendingToolCall.name, + arguments: pendingToolCall.params + }, + server: resolvedToolDef.server + }) + toolContent = toolCallResult.content + toolRawData = toolCallResult.rawData + } catch (toolError) { + console.error('[ThreadPresenter] Failed to execute pending tool call:', toolError) + await this.handleLLMAgentResponse({ + eventId: message.id, + tool_call: 'error', + tool_call_id: pendingToolCall.id, + tool_call_name: pendingToolCall.name, + tool_call_params: pendingToolCall.params, + tool_call_response: toolError instanceof Error ? toolError.message : String(toolError), + tool_call_server_name: resolvedToolDef.server.name, + tool_call_server_icons: resolvedToolDef.server.icons, + tool_call_server_description: resolvedToolDef.server.description + }) + throw toolError + } + + if (toolRawData?.requiresPermission) { + console.warn( + `[ThreadPresenter] Tool ${pendingToolCall.name} still requires permission after grant` + ) + await this.handleLLMAgentResponse({ + eventId: message.id, + tool_call: 'permission-required', + tool_call_id: pendingToolCall.id, + tool_call_name: pendingToolCall.name, + tool_call_params: pendingToolCall.params, + tool_call_server_name: + toolRawData.permissionRequest?.serverName || resolvedToolDef.server.name, + tool_call_server_icons: resolvedToolDef.server.icons, + tool_call_server_description: resolvedToolDef.server.description, + tool_call_response: toolContent, + permission_request: toolRawData.permissionRequest + }) + // 新的权限请求会触发新的处理流程 + return + } + + const serializedResponse = toolContent + + await this.handleLLMAgentResponse({ + eventId: message.id, + tool_call: 'end', + tool_call_id: pendingToolCall.id, + tool_call_name: pendingToolCall.name, + tool_call_params: pendingToolCall.params, + tool_call_response: serializedResponse, + tool_call_server_name: resolvedToolDef.server.name, + tool_call_server_icons: resolvedToolDef.server.icons, + tool_call_server_description: resolvedToolDef.server.description, + tool_call_response_raw: toolRawData ?? undefined + }) + + state.pendingToolCall = undefined + + const finalContent = await buildPostToolExecutionContext({ + conversation, + contextMessages, + userMessage, + currentAssistantMessage: state.message, + completedToolCall: { + ...pendingToolCall, + response: serializedResponse + }, + modelConfig + }) + + const stream = this.llmProviderPresenter.startStreamCompletion( + providerId, + finalContent, + modelId, + message.id, + temperature, + maxTokens, + enabledMcpTools, + thinkingBudget, + reasoningEffort, + verbosity, + enableSearch, + forcedSearch, + searchStrategy + ) + + for await (const event of stream) { + const msg = event.data + if (event.type === 'response') { + await this.handleLLMAgentResponse(msg) + } else if (event.type === 'error') { + await this.handleLLMAgentError(msg) + } else if (event.type === 'end') { + await this.handleLLMAgentEnd(msg) + } + } + } catch (error) { + console.error( + '[ThreadPresenter] Failed to resume after permission with pending tool call:', + error + ) + + this.generatingMessages.delete(message.id) + + try { + await this.messageManager.handleMessageError(message.id, String(error)) + } catch (updateError) { + console.error(`[ThreadPresenter] Failed to update message error status:`, updateError) + } + + throw error + } + } + // 等待MCP服务重启完成并准备就绪 private async waitForMcpServiceReady( serverName: string, diff --git a/src/main/presenter/threadPresenter/promptBuilder.ts b/src/main/presenter/threadPresenter/promptBuilder.ts index 084324d22..39d1661a5 100644 --- a/src/main/presenter/threadPresenter/promptBuilder.ts +++ b/src/main/presenter/threadPresenter/promptBuilder.ts @@ -1,6 +1,12 @@ import { approximateTokenSize } from 'tokenx' import { presenter } from '@/presenter' -import { AssistantMessageBlock, Message, MessageFile, UserMessageContent } from '@shared/chat' +import { + AssistantMessage, + AssistantMessageBlock, + Message, + MessageFile, + UserMessageContent +} from '@shared/chat' import { ModelType } from '@shared/model' import { CONVERSATION, @@ -14,7 +20,14 @@ import { ContentEnricher } from './contentEnricher' import { buildUserMessageContext, getNormalizedUserMessageText } from './messageContent' import { generateSearchPrompt } from './searchManager' -type PendingToolCall = { id: string; name: string; params: string } +export type PendingToolCall = { + id: string + name: string + params: string + serverName?: string + serverIcons?: string + serverDescription?: string +} type VisionUserMessageContent = UserMessageContent & { images?: string[] } export interface PreparePromptContentParams { @@ -38,6 +51,15 @@ export interface ContinueToolCallContextParams { modelConfig: ModelConfig } +export interface PostToolExecutionContextParams { + conversation: CONVERSATION + contextMessages: Message[] + userMessage: Message + currentAssistantMessage: AssistantMessage + completedToolCall: PendingToolCall & { response: string } + modelConfig: ModelConfig +} + export async function preparePromptContent({ conversation, userContent, @@ -209,6 +231,113 @@ export async function buildContinueToolCallContext({ return formattedMessages } +export async function buildPostToolExecutionContext({ + conversation, + contextMessages, + userMessage, + currentAssistantMessage, + completedToolCall, + modelConfig +}: PostToolExecutionContextParams): 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 + }) + + const assistantPreface = collectAssistantTextBeforePermission(currentAssistantMessage?.content) + if (assistantPreface.trim().length > 0) { + formattedMessages.push({ + role: 'assistant', + content: assistantPreface + }) + } + + if (modelConfig.functionCall) { + formattedMessages.push({ + role: 'assistant', + tool_calls: [ + { + id: completedToolCall.id, + type: 'function', + function: { + name: completedToolCall.name, + arguments: completedToolCall.params + } + } + ] + }) + + formattedMessages.push({ + role: 'tool', + tool_call_id: completedToolCall.id, + content: completedToolCall.response + }) + } else { + const formattedRecord = `${JSON.stringify({ + function_call_record: { + name: completedToolCall.name, + arguments: completedToolCall.params, + response: completedToolCall.response + } + })}` + + formattedMessages.push({ + role: 'assistant', + content: formattedRecord + '\n' + }) + + const userPromptText = + '以上是你刚执行的工具调用及其响应信息,已帮你插入,请仔细阅读工具响应,并继续你的回答。' + formattedMessages.push({ + role: 'user', + content: [{ type: 'text', text: userPromptText }] + }) + } + + return formattedMessages +} + +function collectAssistantTextBeforePermission(blocks: AssistantMessageBlock[] | undefined): string { + if (!blocks?.length) { + return '' + } + + const parts: string[] = [] + + for (const block of blocks) { + if (block.type === 'action' && block.action_type === 'tool_call_permission') { + break + } + + if (block.type === 'content' && typeof block.content === 'string') { + parts.push(block.content) + } + + if (block.type === 'reasoning_content' && typeof block.content === 'string') { + parts.push(block.content) + } + } + + return parts.join('') +} + function selectContextMessages( contextMessages: Message[], userMessage: Message, diff --git a/src/main/presenter/threadPresenter/types.ts b/src/main/presenter/threadPresenter/types.ts index a99400243..ae537fbb8 100644 --- a/src/main/presenter/threadPresenter/types.ts +++ b/src/main/presenter/threadPresenter/types.ts @@ -1,4 +1,5 @@ import type { AssistantMessage } from '@shared/chat' +import type { PendingToolCall } from './promptBuilder' export interface GeneratingMessageState { message: AssistantMessage @@ -11,6 +12,7 @@ export interface GeneratingMessageState { lastReasoningTime: number | null isSearching?: boolean isCancelled?: boolean + pendingToolCall?: PendingToolCall totalUsage?: { prompt_tokens: number completion_tokens: number diff --git a/src/renderer/src/components/message/MessageBlockPermissionRequest.vue b/src/renderer/src/components/message/MessageBlockPermissionRequest.vue index 6a0d43a37..8f291ba9d 100644 --- a/src/renderer/src/components/message/MessageBlockPermissionRequest.vue +++ b/src/renderer/src/components/message/MessageBlockPermissionRequest.vue @@ -208,6 +208,7 @@ const getStatusIconClass = () => { const getStatusText = () => { switch (props.block.status) { case 'granted': + case 'success': return t('components.messageBlockPermissionRequest.granted') case 'denied': return t('components.messageBlockPermissionRequest.denied') From 6409add9175bd8b9152208e3f07968619cd5e4aa Mon Sep 17 00:00:00 2001 From: zerob13 Date: Wed, 29 Oct 2025 19:02:34 +0800 Subject: [PATCH 3/3] fix: coderabbit review --- src/main/presenter/threadPresenter/index.ts | 24 ++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index 1210b9fad..39b170e04 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -389,7 +389,29 @@ export class ThreadPresenter implements IThreadPresenter { } } } else if (tool_call === 'permission-required') { - const permissionType = permission_request?.permissionType ?? 'write' + // Define allowed permission types + const ALLOWED_PERMISSION_TYPES = ['read', 'write', 'all'] as const + type PermissionType = (typeof ALLOWED_PERMISSION_TYPES)[number] + + // Validate and sanitize permission type + let permissionType: PermissionType = 'read' // Default to 'read' for safety + const requestedType = 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( + `[ThreadPresenter] Invalid permission type received: "${requestedType}". Defaulting to "read". Allowed types: ${ALLOWED_PERMISSION_TYPES.join(', ')}` + ) + } + } else if (requestedType !== undefined) { + console.warn( + `[ThreadPresenter] Permission type is not a string: ${typeof requestedType}. Defaulting to "read".` + ) + } + const extra: Record = { needsUserAction: true, permissionType