Skip to content

feat: agentic tool op#1307

Merged
zerob13 merged 9 commits intodevfrom
feat/agentic-tool-op
Feb 11, 2026
Merged

feat: agentic tool op#1307
zerob13 merged 9 commits intodevfrom
feat/agentic-tool-op

Conversation

@zerob13
Copy link
Collaborator

@zerob13 zerob13 commented Feb 10, 2026

Summary by CodeRabbit

  • New Features

    • edit_file: exact, case-sensitive in-file replacements with JSON diff previews
    • process tool: background command sessions (start/poll/log/write/kill) and session management
    • Background exec session manager and session APIs exported
  • Permission & UX

    • Pre-execution permission pre-checks and batch permission gating with single-resume semantics
    • Permission-updated stream event and UI sync improvements
  • Documentation

    • Specs/plans/tasks for edit_file, process tool, and permission stabilization
  • Tests

    • Expanded tests for multi-permission flows, locks, and resume behavior

…tool calls

- Add session-scoped permission cache in ToolManager (memory-only)
- Support remember=false for session-level permissions
- Add preCheckToolPermission() chain: ToolManager -> MCPPresenter -> ToolPresenter -> ToolCallProcessor
- Pause execution flow when any tool requires permission, returning all permission requests at once
- Update grantPermission signature to support session-only permissions
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Adds an edit_file filesystem tool, a background process execution manager, and a comprehensive batch permission pre-check/resume flow with session-scoped permission caching and UI integration; wires pre-check APIs across presenters, updates session/runtime locking, and adds related tests and docs.

Changes

Cohort / File(s) Summary
Specs & Plans
docs/specs/edit-file-tool/{plan,spec,tasks}.md, docs/specs/permission-flow-stabilization/{plan,spec,tasks}.md, docs/specs/process-tool/{plan,spec,tasks}.md
New design/spec/task documents for edit_file, permission-flow stabilization, and process (background) tool implementations.
Edit-file tool
src/main/presenter/agentPresenter/acp/agentFileSystemHandler.ts, src/main/presenter/agentPresenter/acp/agentToolManager.ts
Adds editFile handler and EditFileArgsSchema, registers edit_file tool and aliases, normalizes params, enforces write permission checks, performs case-sensitive replace-all with diff JSON response.
Background process execution
src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts, src/main/presenter/agentPresenter/acp/agentBashHandler.ts, src/main/presenter/agentPresenter/acp/agentToolManager.ts, src/main/presenter/agentPresenter/acp/index.ts
Introduces BackgroundExecSessionManager singleton (start/poll/log/write/kill/clear/remove/offload), background+yieldMs on execute_command, executeCommandBackground flow, process tool schema/routing, and re-exports.
Permission orchestration & session runtime
src/main/presenter/agentPresenter/permission/permissionHandler.ts, src/main/presenter/agentPresenter/session/{sessionContext,sessionManager}.ts, src/main/presenter/agentPresenter/loop/{toolCallProcessor,toolCallHandler,agentLoopHandler}.ts, src/main/presenter/agentPresenter/index.ts
Implements batch pre-checks, pendingPermissions array, permissionResumeLock, single-flight resume/locking, atomic permission block updates, resume helpers that sequentially execute/skip tool_calls, and startLoop skipLockAcquisition wiring.
Tool permission APIs & routing
src/main/presenter/mcpPresenter/{index,toolManager}.ts, src/main/presenter/toolPresenter/index.ts, src/main/presenter/agentPresenter/acp/agentToolManager.ts
Adds preCheckToolPermission surface across presenters and ToolPresenter, session-scoped permission caching in ToolManager, conversationId-aware checks, and integrates pre-checks into tool routing (special-cases process tool).
Streaming / events / UI integration
src/main/events.ts, src/renderer/src/{events.ts,stores/chat.ts}, src/main/presenter/agentPresenter/streaming/{llmEventHandler,streamGenerationHandler}.ts, src/renderer/src/components/message/MessageBlockPermissionRequest.vue
Adds PERMISSION_UPDATED stream event, flushStreamUpdates API, addPendingPermission usage, renderer IPC handler for permission updates, and UI state preservation for pending permission blocks.
Loop/processor changes
src/main/presenter/agentPresenter/loop/toolCallProcessor.ts, src/main/presenter/agentPresenter/loop/toolCallHandler.ts
Batch pre-check flow in processor, permission metadata preservation in batch requests, and insertion of a loading tool_call block upon permission grant to anchor resumed execution.
Types, exports & limits
src/main/presenter/agentPresenter/acp/index.ts, src/shared/types/presenters/legacy.presenters.d.ts, src/main/presenter/llmProviderPresenter/baseProvider.ts
Re-exports for background manager, added preCheck/grantPermission signatures and types, and increased MAX_TOOL_CALLS to 12800.
Tests
test/main/presenter/{sessionPresenter/permissionHandler.test.ts,agentPresenter/acp/agentFileSystemHandler.test.ts,agentPresenter/loop/toolCallProcessor.test.ts,agentPresenter/permission/permissionHandler.resume.test.ts
New/updated tests covering multi-permission scenarios, resume locking, editFile line-ending normalization, batch pre-check behavior, and processor updates.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Renderer/UI
    participant Presenter as ToolPresenter / McpPresenter
    participant ToolMgr as AgentToolManager / ToolManager
    participant Session as SessionManager
    participant Permission as PermissionHandler
    participant Executor as Tool Executor / BackgroundExecSessionManager

    UI->>Presenter: request tool / user grants permission
    Presenter->>ToolMgr: preCheckToolPermission(MCPToolCall)
    ToolMgr-->>Presenter: PermissionRequestPayload or null
    alt permission required (batch)
      Presenter->>Permission: emit batch permission-required (persist pendingPermissions)
      Permission->>Session: addPendingPermission(messageId, toolCallId,...)
      Permission->>UI: stream:permission-updated
      UI->>Permission: user grants/denies
      Permission->>Session: updatePermissionBlocks(granted)
      Permission->>Permission: acquire permissionResumeLock(messageId)
      Permission->>Executor: execute allowed tool_calls sequentially
      Executor-->>Permission: tool result / running session meta
      Permission->>Session: flushStreamUpdates(messageId)
      Permission->>UI: stream events for tool outcomes
    else no permission required
      Presenter->>Executor: execute tool immediately
      Executor-->>Presenter: result
      Presenter->>UI: stream events
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 I hopped through specs and code all night,

Replaced some text and launched a flight,
Background shells hum, permissions light,
Diffs appear — the patch feels right,
🥕 Hop, commit, and out of sight.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: agentic tool op' is vague and incomplete, using a truncated phrase that doesn't clearly communicate the main changes. Expand the title to be more descriptive, e.g. 'feat: add edit_file, background process execution, and permission flow stabilization' or break into multiple PRs with clearer titles.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/agentic-tool-op

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/presenter/mcpPresenter/toolManager.ts (1)

226-226: ⚠️ Potential issue | 🟡 Minor

Chinese comments violate the coding guidelines.

Lines 226, 273, 289, 297, 303, 571, 598, 621, 651–652, 661, 674 contain Chinese comments. As per coding guidelines, all logs and comments in **/*.{js,ts,tsx,jsx,vue,mjs,cjs} files must be in English.

Also applies to: 273-273, 289-289, 297-297, 303-303, 571-571, 598-598, 621-621, 651-652, 661-661, 674-674

src/main/presenter/agentPresenter/acp/agentToolManager.ts (1)

1103-1133: ⚠️ Potential issue | 🔴 Critical

edit_file missing from collectWriteTargets — write-permission check bypassed.

assertWritePermission at line 995 calls collectWriteTargets for edit_file, but collectWriteTargets has no case 'edit_file' branch and falls through to default, returning []. This causes the permission assertion to silently return, allowing edit_file to modify files outside the allowed directories without triggering a permission prompt.

🐛 Add edit_file to collectWriteTargets
       case 'text_replace': {
         const pathArg = args.path
         return typeof pathArg === 'string' ? [pathArg] : []
       }
+      case 'edit_file': {
+        const pathArg = args.path
+        return typeof pathArg === 'string' ? [pathArg] : []
+      }
       case 'move_files': {
src/main/presenter/agentPresenter/permission/permissionHandler.ts (1)

861-917: ⚠️ Potential issue | 🔴 Critical

restartAgentLoopAfterPermission does not acquire the permission resume lock before delegating to resumeToolExecutionAfterPermissions.

resumeToolExecutionAfterPermissions validates the lock at line 466 and returns early if no lock is held. Since restartAgentLoopAfterPermission never calls acquirePermissionResumeLock, every call to this method will silently bail out at the lock-validation step, making the resume a no-op.

Either acquire the lock before calling resumeToolExecutionAfterPermissions, or make the lock check inside resumeToolExecutionAfterPermissions conditional.

🐛 Acquire lock before delegating
  async restartAgentLoopAfterPermission(messageId: string, toolCallId?: string): Promise<void> {
    console.log('[PermissionHandler] Restarting agent loop after permission', messageId)

    try {
      const message = await this.ctx.messageManager.getMessage(messageId)
      if (!message) {
        throw new Error(`Message not found (${messageId})`)
      }

+     const conversationId = message.conversationId
+     const lockAcquired = presenter.sessionManager.acquirePermissionResumeLock(
+       conversationId,
+       messageId
+     )
+     if (!lockAcquired) {
+       console.log('[PermissionHandler] Resume already in progress for this message, skipping')
+       return
+     }
+
      // ... rest of the method
🤖 Fix all issues with AI agents
In `@docs/specs/edit-file-tool/plan.md`:
- Around line 46-55: The normalization currently builds args with computed
fields first then spreads ...args, which re-overwrites path/oldText/newText with
original undefined values; change the construction so you spread the original
args first and then set the computed fields last (e.g., args = { ...args, path:
args.path ?? args.file_path, oldText: args.oldText ?? args.old_string, newText:
args.newText ?? args.new_string, base_directory: args.base_directory }) so the
computed path/file_path, oldText/old_string and newText/new_string wins and
base_directory is preserved; update the block that checks toolName ===
'edit_file' and assigns to args accordingly.

In `@src/main/presenter/agentPresenter/acp/agentFileSystemHandler.ts`:
- Around line 1108-1114: The pre-check currently uses raw
content.includes(oldText) but the replacement works on
normalizedContent/normalizedOldText, so normalize the file content and the
oldText before the existence check: after reading content from
fs.readFile(validPath, 'utf-8') produce normalizedContent and normalizedOldText
(same normalization used by the replacement logic) and then perform
normalizedContent.includes(normalizedOldText); if not found, throw the same
Error. Update references in the check to normalizedContent and normalizedOldText
so line ending differences don't cause false negatives.

In `@src/main/presenter/agentPresenter/acp/agentToolManager.ts`:
- Around line 69-83: Duplicate Zod schemas exist: private processToolSchema and
fileSystemSchemas.process; remove the duplicate and consolidate by using a
single source. Replace the private field processToolSchema with a reference to
fileSystemSchemas.process (or delete fileSystemSchemas.process and point
fileSystemSchemas.process = this.processToolSchema) and update all usages
(notably callProcessTool and getFileSystemToolDefinitions) to reference the
single canonical schema (fileSystemSchemas.process or processToolSchema
depending on which you keep) so validation and schema lookups use the same
object.

In `@src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts`:
- Around line 407-440: The appendOutput method currently uses blocking
fs.appendFileSync/writeFileSync inside stdout/stderr handlers; replace these
sync calls by creating and reusing a per-session fs.WriteStream (e.g., add
session.outputStream) or use fs.promises with a small write-queue so writes are
non-blocking: on first time session.outputFilePath is set, create
session.outputStream = fs.createWriteStream(session.outputFilePath, { flags: 'a'
}); when shouldOffload is true, write via
session.outputStream.write(session.outputBuffer or data) (and clear outputBuffer
after draining) and handle stream 'error' events to fall back to appending to
session.outputBuffer; remove appendFileSync and writeFileSync and ensure
totalOutputLength, outputBuffer and outputFilePath logic in appendOutput and
BackgroundSession are updated accordingly.
- Around line 567-575: The readFromFile implementation currently calls
fs.readFileSync and loads the entire file, causing memory spikes for large
outputs; change it to open the file descriptor (fs.openSync or async equivalent)
and use a positional read (fs.readSync) into a sized Buffer so you only read the
requested range, mirroring the approach used by readLastCharsFromFile; ensure
you handle the offset/limit semantics (clarify/convert between character vs byte
offsets) and close the descriptor after reading, and preserve the existing error
handling/logging in readFromFile.

In `@src/main/presenter/agentPresenter/permission/permissionHandler.ts`:
- Around line 677-691: In executeSingleToolCall, guard against getMessage
returning null before accessing message.content: after calling
this.ctx.messageManager.getMessage(state.message.id) check if message is null
(or undefined) and handle it (e.g., log/warn and return false or otherwise abort
the tool call) so the subsequent cast to AssistantMessageBlock[] and the
permissionBlock lookup are not performed on a null object; update references in
this method (message, message.content, permissionBlock) accordingly to avoid the
TypeError.
- Around line 456-463: The null-guard is ineffective because
presenter.sessionManager.getSession(conversationId) is async—either await the
call (const session = await presenter.sessionManager.getSession(conversationId))
so the if (!session) check works and you only call
presenter.sessionManager.releasePermissionResumeLock(conversationId) on actual
null, or if the intent is to not create a session use the synchronous lookup
method (presenter.sessionManager.getSessionSync(conversationId)) instead; update
the code paths using getSession/getSessionSync accordingly to ensure the lock is
held and released correctly when no session exists.

In `@src/main/presenter/mcpPresenter/toolManager.ts`:
- Around line 651-672: The checkSessionPermission incorrectly treats any server
permission as blanket access; update checkSessionPermission to respect the
hierarchy all > write > read by comparing permission levels instead of blindly
returning true when any `${serverName}:...` key exists: for each permKey in
this.sessionPermissions.get(conversationId) that startsWith `${serverName}:`,
parse the stored permission token (e.g., split on ':' to get
'read'|'write'|'all'), map these to numeric levels (read=1, write=2, all=3) and
only return true if storedLevel >= requestedLevel (map _permissionType
similarly), keep the existing console.log when granting access and return false
otherwise.

In `@test/main/presenter/sessionPresenter/permissionHandler.test.ts`:
- Around line 329-374: The tests call acquirePermissionResumeLock,
getPermissionResumeLock, and releasePermissionResumeLock on
presenter.sessionManager but the test presenter mock lacks those methods,
causing TypeError; update the test setup to add mocked implementations for
presenter.sessionManager (or replace the mock with a real SessionManager
instance) that implement acquirePermissionResumeLock(agentId, messageId),
getPermissionResumeLock(agentId) and releasePermissionResumeLock(agentId) (e.g.,
use jest.fn() or a small in-memory lock map implementation) so the tests in the
"Permission resume lock" describe block can run against those methods.
- Around line 376-424: The test "should update permissions with same or lower
permission level" is incomplete and references out-of-scope variables; fix it by
giving the test its own local mocks and assertions: declare a local messageId
constant and a mocked messageManager (e.g., mock messageManager.getMessage to
return the prepared message content), then call the permission-handling function
under test (the permission handler in sessionPresenter/permissionHandler used by
this suite) to simulate granting 'write' permission, and finally add expect(...)
assertions that both the 'read' and 'write' permission action blocks are updated
to the expected status (e.g., not pending / granted) and that any
persistence/update helpers were called with the correct payloads; alternatively,
if this behavior is tested elsewhere, remove this no-op test to avoid misleading
test counts.
- Around line 279-327: The mock for presenter.sessionManager is missing the
methods used by the new tests (addPendingPermission, getPendingPermissions,
hasPendingPermissions, acquirePermissionResumeLock, getPermissionResumeLock,
releasePermissionResumeLock), causing TypeErrors; fix by updating the test setup
to either replace presenter.sessionManager with a real SessionManager instance
(constructed with mocked dependencies) or extend the mock to implement those
methods with the expected behaviors (append/return pending permission entries
and manage resume-lock state) so that calls to addPendingPermission,
getPendingPermissions, hasPendingPermissions, acquirePermissionResumeLock,
getPermissionResumeLock, and releasePermissionResumeLock succeed in the tests.
🟡 Minor comments (12)
src/renderer/src/stores/chat.ts-2189-2198 (1)

2189-2198: ⚠️ Potential issue | 🟡 Minor

Comment on line 2190 must be in English.

The comment // 监听权限更新事件,刷新消息以显示最新的权限状态 should be written in English per coding guidelines.

Proposed fix
-    // 监听权限更新事件,刷新消息以显示最新的权限状态
+    // Listen for permission update events to refresh messages and display the latest permission state

As per coding guidelines: **/*.{ts,tsx} — "All logs and comments must be in English" and src/renderer/src/**/*.{ts,tsx,vue} — "Ensure all code comments are in English."

docs/specs/process-tool/tasks.md-127-135 (1)

127-135: ⚠️ Potential issue | 🟡 Minor

Fenced code block missing language specifier (MD040).

The dependency diagram code block at line 127 lacks a language identifier. Use ```text or ```mermaid (if converted) to satisfy markdownlint.

Proposed fix
-```
+```text
 Task 1.1 ──┬──→ Task 2.1 ──┬──→ Task 2.2
src/renderer/src/events.ts-52-53 (1)

52-53: ⚠️ Potential issue | 🟡 Minor

Comment on line 53 must be in English.

The comment // 权限状态更新,通知前端刷新UI should be in English per coding guidelines.

Proposed fix
-  ERROR: 'stream:error', // 替代 stream-error
-  PERMISSION_UPDATED: 'stream:permission-updated' // 权限状态更新,通知前端刷新UI
+  ERROR: 'stream:error', // Replaces stream-error
+  PERMISSION_UPDATED: 'stream:permission-updated' // Permission state updated, notify frontend to refresh UI

Note: The existing comments on other lines (e.g., // 替代 stream-response) also use Chinese, but since those aren't changed lines, flagging only the new addition here. As per coding guidelines: **/*.{js,ts,tsx,jsx,vue,mjs,cjs} — "All logs and comments must be in English."

src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts-14-18 (1)

14-18: ⚠️ Potential issue | 🟡 Minor

OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS looks like a leftover from another project.

This environment variable name doesn't match the PI_BASH_* naming convention used by the other variables. If it's intentional for backward compatibility, a brief comment would help; otherwise, consider removing it.

docs/specs/edit-file-tool/tasks.md-102-102 (1)

102-102: ⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced code block.

Static analysis (markdownlint MD040) flags this code block as missing a language specifier. Use ```text or similar for the commit message block.

src/main/presenter/mcpPresenter/toolManager.ts-636-648 (1)

636-648: ⚠️ Potential issue | 🟡 Minor

grantPermission with remember: false and no conversationId is a silent no-op.

When remember is false and conversationId is undefined, the function only logs and does nothing — the permission is never stored anywhere. Callers likely expect at least a best-effort grant.

src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts-489-529 (1)

489-529: ⚠️ Potential issue | 🟡 Minor

killInternal unconditionally overrides status to 'killed' at line 528.

If the process happens to exit normally (exit code 0) just as killInternal fires, the exit event handler (line 455) sets status = 'done', then line 528 overrides it to 'killed'. Consider only setting session.status = 'killed' if it's still 'running'.

Proposed fix
-    session.status = 'killed'
+    if (session.status === 'running') {
+      session.status = 'killed'
+    }
src/main/presenter/agentPresenter/acp/agentBashHandler.ts-336-346 (1)

336-346: ⚠️ Potential issue | 🟡 Minor

Inconsistent timestamp capture.

Date.now() is called separately on lines 344 and 345, which may produce slightly different values. The foreground path captures startedAt once and reuses it. Use a single variable for consistency.

Proposed fix
+    const startedAt = Date.now()
     // Emit initial terminal snippet
     await this.emitTerminalSnippet(conversationId, {
       id: result.sessionId,
       status: 'running',
       command,
       cwd,
       output: '',
       truncated: false,
       exitCode: null,
-      startedAt: Date.now(),
-      timestamp: Date.now()
+      startedAt,
+      timestamp: startedAt
     })
src/main/presenter/agentPresenter/acp/agentBashHandler.ts-26-27 (1)

26-27: ⚠️ Potential issue | 🟡 Minor

yieldMs is declared in the schema but never consumed.

yieldMs is validated by ExecuteCommandArgsSchema but never destructured or passed to any execution path. Even though the schema description in agentToolManager.ts indicates it's intended for foreground mode, the parameter is not actually used in the foreground execution logic. Remove it from the schema until it's properly wired up to avoid silently ignoring parameters passed by callers.

src/main/presenter/agentPresenter/acp/agentToolManager.ts-1338-1345 (1)

1338-1345: ⚠️ Potential issue | 🟡 Minor

Misleading comment and dead code path for read operations.

The comment on line 1340 says "Check for path in read operations too", but the condition targets.length === 0 && isWriteOperation only triggers for write operations. The readTools list (lines 1261–1267) and isReadOperation variable (line 1313) are computed but never drive any permission check logic — read operations always return null.

If read pre-checks are intentionally skipped, remove the readTools list and isReadOperation variable, and fix the comment. If read pre-checks should exist, the condition needs to be targets.length === 0 && (isWriteOperation || isReadOperation).

src/main/presenter/agentPresenter/session/sessionManager.ts-273-283 (1)

273-283: ⚠️ Potential issue | 🟡 Minor

Lock for a different messageId is silently overwritten.

If permissionResumeLock already exists for a different messageId, this method overwrites it without warning and returns true. This could mask a scenario where a previous resume flow hasn't completed yet. Consider either returning false (or logging a warning) when overwriting a lock held by another message.

🛡️ Suggested guard against overwriting a different message's lock
 acquirePermissionResumeLock(agentId: string, messageId: string): boolean {
   const session = this.sessions.get(agentId)
   if (!session) return false
   const runtime = this.ensureRuntime(session)
   if (runtime.permissionResumeLock?.messageId === messageId) {
     return false
   }
+  if (runtime.permissionResumeLock) {
+    console.warn(
+      `[SessionManager] Overwriting existing lock for message ${runtime.permissionResumeLock.messageId} with ${messageId}`
+    )
+  }
   runtime.permissionResumeLock = { messageId, startedAt: Date.now() }
   session.updatedAt = Date.now()
   return true
 }
src/main/presenter/agentPresenter/acp/agentToolManager.ts-1007-1013 (1)

1007-1013: ⚠️ Potential issue | 🟡 Minor

Switch case variable declaration leaks to other cases.

The const commandResult declared inside the execute_command case is not block-scoped, which Biome/ESLint correctly flags as noSwitchDeclarations.

🔧 Wrap in a block
-        case 'execute_command':
+        case 'execute_command': {
           if (!this.bashHandler) {
             throw new Error('Bash handler not initialized for execute_command tool')
           }
           const commandResult = await this.bashHandler.executeCommand(parsedArgs, {
             conversationId
           })
           return {
             content:
               typeof commandResult === 'string' ? commandResult : JSON.stringify(commandResult)
           }
+        }
🧹 Nitpick comments (17)
src/renderer/src/stores/chat.ts (1)

2191-2198: The messageId from the payload is only logged, not used for a targeted refresh.

Currently bumpMessageCacheVersion() refreshes the entire message list. If the intent is to eventually support targeted message refreshes, consider using handleMessageEdited(payload.messageId) instead, which would fetch and enrich only the affected message. If the current broad refresh is intentional (e.g., multiple permission blocks may change), this is fine as-is.

src/main/presenter/agentPresenter/loop/toolCallHandler.ts (1)

515-517: Empty if block — dead code that should be removed.

Lines 516-517 contain an empty conditional body that appears to be a leftover from debugging or an incomplete implementation.

Proposed fix
-    const lastBlock = state.message.content[state.message.content.length - 1]
-    if (lastBlock && lastBlock.type === 'tool_call' && lastBlock.tool_call) {
-    }
-
     this.finalizeLastBlock(state)
src/main/presenter/agentPresenter/session/sessionContext.ts (1)

44-46: Consider deprecating pendingPermission in favor of pendingPermissions.

Both pendingPermission (singular) and pendingPermissions (array) coexist, where the singular is always synced to pendingPermissions[0] in SessionManager. This dual-field pattern adds maintenance burden and risks drift. If the singular field exists purely for backward compatibility, consider marking it with a @deprecated JSDoc annotation so future contributors know to migrate away from it.

docs/specs/process-tool/plan.md (1)

7-30: Add language identifiers to fenced code blocks.

Multiple fenced code blocks use bare triple-backticks without a language specifier, triggering MD040. For ASCII diagrams and plaintext flows, use ```text or ```plaintext.

Also applies to: 89-96, 100-106, 110-114, 118-123, 127-136, 140-155

docs/specs/permission-flow-stabilization/plan.md (1)

21-27: Add a language identifier to the fenced code block.

The state machine diagram should specify a language (e.g., ```text) to satisfy MD040.

src/main/presenter/toolPresenter/index.ts (2)

210-230: Extract duplicated argument-parsing logic into a shared helper.

The JSON-parse-with-jsonrepair-fallback block (lines 210–230) is a near-exact copy of lines 139–156 in callTool. Consider extracting a private method like parseToolArguments(argsString: string): Record<string, unknown> to eliminate the duplication.

♻️ Proposed helper extraction
+  private parseToolArguments(argsString: string): Record<string, unknown> {
+    if (!argsString.trim().length) return {}
+    try {
+      return JSON.parse(argsString) as Record<string, unknown>
+    } catch (error) {
+      console.warn('[ToolPresenter] Failed to parse tool arguments, trying jsonrepair:', error)
+      try {
+        return JSON.parse(jsonrepair(argsString)) as Record<string, unknown>
+      } catch (error) {
+        console.warn('[ToolPresenter] Failed to repair tool arguments, using empty args.', error)
+        return {}
+      }
+    }
+  }

Then in both callTool and preCheckToolPermission:

-      let args: Record<string, unknown> = {}
-      const argsString = request.function.arguments || ''
-      if (argsString.trim().length > 0) {
-        try {
-          args = JSON.parse(argsString) as Record<string, unknown>
-        } catch (error) {
-          ...
-        }
-      }
+      const args = this.parseToolArguments(request.function.arguments || '')

204-208: Add a warning log when agentToolManager is null for an agent-sourced tool.

Line 200 logs a warning when the tool source is missing, but lines 206–208 silently return null when agentToolManager hasn't been initialized. This makes debugging harder since an agent tool would silently skip permission checks.

Proposed fix
     if (source === 'agent') {
       // Agent tools: delegate to AgentToolManager for pre-check
       if (!this.agentToolManager) {
+        console.warn(`[ToolPresenter] Agent tool manager not initialized for permission check of '${toolName}'`)
         return null
       }
src/main/presenter/mcpPresenter/toolManager.ts (1)

740-743: sessionPermissions is not cleaned up in destroy().

Event listeners are removed in destroy(), but sessionPermissions is never cleared, which could lead to stale data if a ToolManager instance is reused or references linger.

Proposed fix
  public destroy(): void {
    eventBus.off(MCP_EVENTS.CLIENT_LIST_UPDATED, this.handleServerListUpdate)
    eventBus.off(MCP_EVENTS.CONFIG_CHANGED, this.handleConfigChange)
+   this.sessionPermissions.clear()
  }
src/main/presenter/agentPresenter/acp/agentBashHandler.ts (1)

298-349: Duplicated permission-check-and-throw block between foreground and background paths.

The permission check + CommandPermissionRequiredError throw pattern in executeCommandBackground (lines 310–328) is nearly identical to lines 65–85. A shared helper would reduce duplication.

♻️ Possible helper
+  private assertCommandPermission(command: string, conversationId?: string): void {
+    if (!this.commandPermissionHandler) return
+    const permissionCheck = this.commandPermissionHandler.checkPermission(conversationId, command)
+    if (!permissionCheck.allowed) {
+      const commandInfo = this.commandPermissionHandler.buildCommandInfo(command)
+      throw new CommandPermissionRequiredError(
+        'components.messageBlockPermissionRequest.description.commandWithRisk',
+        {
+          toolName: 'execute_command',
+          serverName: 'agent-filesystem',
+          permissionType: 'command',
+          description: 'Execute command requires approval.',
+          command,
+          commandSignature: commandInfo.signature,
+          commandInfo,
+          conversationId,
+        }
+      )
+    }
+  }

Then replace both blocks with this.assertCommandPermission(command, options.conversationId).

Also applies to: 65-85

src/main/presenter/agentPresenter/loop/toolCallProcessor.ts (2)

62-66: ToolCall interface duplicates existing inline type.

ToolCallExecutionContext.toolCalls is already typed as Array<{ id: string; name: string; arguments: string }>, which is exactly the same shape. Consider reusing that type or at least using ToolCall consistently throughout the file (including in ToolCallExecutionContext).


133-142: isBatchPermission and totalInBatch are untyped additions to permission_request.

These fields are emitted in the batch path but aren't part of the existing permission_request type used in the per-tool path (line 350). The LLMAgentEvent data is likely loosely typed, but this ad-hoc shape divergence could confuse consumers. Consider documenting the contract or aligning the types.

src/main/presenter/agentPresenter/session/sessionManager.ts (1)

165-172: Misleading variable name hasExistingLock.

acquirePermissionResumeLock returns true on successful acquisition and false when the lock already exists. The variable name hasExistingLock reads as the inverse of what it represents, making the if (!hasExistingLock) guard confusing.

🔧 Suggested rename for clarity
-      const hasExistingLock = this.acquirePermissionResumeLock(agentId, messageId)
-      if (!hasExistingLock) {
+      const lockAcquired = this.acquirePermissionResumeLock(agentId, messageId)
+      if (!lockAcquired) {
src/main/presenter/agentPresenter/acp/agentToolManager.ts (2)

881-884: Unreachable process-tool guard in callFileSystemTool.

callTool (line 418) already dispatches process before reaching callFileSystemTool. This early-return block is dead code in the normal flow. If it's intentional as a safety net, a brief comment would help; otherwise, consider removing it.


895-903: Alias normalization for edit_file is pragmatic but loosely typed.

Mapping file_path → path, old_string → oldText, new_string → newText silently accepts non-standard parameter names. This is presumably for LLM compatibility. Consider logging when aliases are actually used so misuse is visible during debugging.

src/main/presenter/agentPresenter/permission/permissionHandler.ts (3)

36-70: canBatchUpdate now only matches the exact same tool call, making the "batch" concept a single-item update.

Lines 50–52 enforce targetPermission.tool_call?.id === grantedPermission.tool_call?.id, so only the block whose tool_call.id matches the granted one will pass. The secondary guard at lines 59–67 is therefore redundant — it re-checks the same tool_call ID equality that line 52 already guarantees.

If single-block-at-a-time is intentional (matching the "CRITICAL FIX" comment), consider simplifying the function and renaming it (e.g., shouldUpdateBlock) to avoid confusion about "batching."


632-637: denied: true property added to PendingToolCall is never read.

The denied flag is set here via spread + cast, but executeSingleToolCall re-checks permissionBlock?.status === 'denied' from the database instead of reading this flag. The cast as PendingToolCall also hides the type mismatch since PendingToolCall has no denied field.

♻️ Remove unused property
        } else if (permissionBlock.status === 'denied') {
-          // Denied - will generate error response later
          const pendingCall = this.buildPendingToolCallFromBlock(block)
-          if (pendingCall) {
-            toolCalls.push({ ...pendingCall, denied: true } as PendingToolCall)
-          }
+          if (pendingCall) toolCalls.push(pendingCall)

496-502: Lock is released at multiple "SINGLE EXIT POINT" locations — comments are misleading.

The lock is released at lines 522, 569, 577, and 592. While each path is correct, the repeated "SINGLE EXIT POINT" comments are inaccurate and could mislead future maintainers. Consider using a try/finally pattern to guarantee release at one true exit point.

Also applies to: 546-548, 516-523, 562-572, 574-577, 578-593

- Extract unified PermissionRequestPayload interface for type consistency
- Fix batch permission requests to include extended fields (paths, providerId, etc.)
- Change resume logic to process all resolved tool calls, skipping completed blocks
- Normalize line endings before text matching in editFile
- Optimize frontend generation state to maintain working status during user interactions
- Add test cases for batch permission and resume behavior
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/presenter/agentPresenter/permission/permissionHandler.ts (1)

864-920: ⚠️ Potential issue | 🔴 Critical

restartAgentLoopAfterPermission must acquire the resume lock before calling resumeToolExecutionAfterPermissions, or it will early-exit at the lock validation check.

resumeToolExecutionAfterPermissions checks for an active lock at line 466–478. Without an acquired lock, the method returns early (line 478) without executing any tool calls. Compare this to handlePermissionResponse (line 270–282), which correctly acquires the lock before calling this method. The commented-out conversationId at line 877 suggests this was left incomplete.

🐛 Proposed fix: Acquire lock before calling resume
      // UNIFIED FLOW: Use resumeToolExecutionAfterPermissions for all cases
      // This method handles:
      // 1. Setting up the generating state
      // 2. Finding and executing all granted tool calls
      // 3. Continuing with stream completion
+     const conversationId = message.conversationId
+     const lockAcquired = presenter.sessionManager.acquirePermissionResumeLock(
+       conversationId,
+       messageId
+     )
+     if (!lockAcquired) {
+       console.log('[PermissionHandler] Resume already in progress for this message, skipping')
+       return
+     }
      await this.resumeToolExecutionAfterPermissions(messageId, true)
🤖 Fix all issues with AI agents
In `@src/renderer/src/stores/chat.ts`:
- Around line 2241-2250: The PERMISSION_UPDATED handler only calls
bumpMessageCacheVersion(), which won’t refresh message content; update the ipc
listener (STREAM_EVENTS.PERMISSION_UPDATED) to call
handleMessageEdited(payload.messageId) after logging so the latest message is
fetched from the main process, then bumpMessageCacheVersion() if needed; also
change the inline comment to English per coding guidelines. Ensure you reference
the existing ipc handler that uses window.electron.ipcRenderer.on and the
functions handleMessageEdited and bumpMessageCacheVersion.
🧹 Nitpick comments (8)
src/renderer/src/stores/chat.ts (1)

1409-1435: Pending user-action detection: consider adding a role guard before accessing .content.

The code casts enrichedMessage to AssistantMessage (line 1409) without first confirming enrichedMessage.role === 'assistant'. While in practice the generating messages cache should only contain assistant messages, a defensive check would prevent a subtle bug if a non-assistant message ever ends up in the cache (e.g., due to a future refactor).

🛡️ Suggested defensive check
       const enrichedMessage = await enrichMessageWithExtra(updatedMessage)
-      const assistantContent = (enrichedMessage as AssistantMessage).content
-      const hasPendingUserAction =
-        Array.isArray(assistantContent) &&
-        assistantContent.some(
+      const assistantContent =
+        enrichedMessage.role === 'assistant'
+          ? (enrichedMessage as AssistantMessage).content
+          : undefined
+      const hasPendingUserAction =
+        Array.isArray(assistantContent) &&
+        assistantContent.some(
src/main/presenter/toolPresenter/index.ts (2)

16-40: PreCheckedPermissionResult duplicates PermissionRequestPayload from toolCallProcessor.ts.

Both types share nearly identical fields (toolName, serverName, permissionType, description, paths, command, commandInfo, providerId, requestId, sessionId, agentId, agentName, conversationId, rememberable, plus an index signature). Consider extracting a shared type to avoid drift between the two definitions.


191-246: Duplicated JSON parse/repair logic between callTool and preCheckToolPermission.

The argument parsing block (lines 206–225) is nearly identical to lines 150–167 in callTool. Extract a shared private helper to reduce duplication.

♻️ Proposed helper extraction
+  private parseToolArguments(argsString: string): Record<string, unknown> {
+    if (argsString.trim().length === 0) return {}
+    try {
+      return JSON.parse(argsString) as Record<string, unknown>
+    } catch (error) {
+      console.warn('[ToolPresenter] Failed to parse tool arguments, trying jsonrepair:', error)
+      try {
+        return JSON.parse(jsonrepair(argsString)) as Record<string, unknown>
+      } catch (error) {
+        console.warn('[ToolPresenter] Failed to repair tool arguments, using empty args.', error)
+        return {}
+      }
+    }
+  }

Then use const args = this.parseToolArguments(request.function.arguments || '') in both methods.

src/main/presenter/agentPresenter/permission/permissionHandler.ts (4)

36-70: Redundant tool_call ID check in canBatchUpdate.

Line 52 already returns false when targetPermission.tool_call?.id !== grantedPermission.tool_call?.id. The secondary check at line 66 for command / agent-filesystem / deepchat-settings is therefore always true and can never cause a false return. The entire if block (lines 59–67) is dead code.

♻️ Remove redundant guard
   // CRITICAL FIX: Only batch the exact same tool call (same tool_call.id)
   // This ensures user approval of one tool doesn't auto-approve other tools from the same server
   if (targetPermission.tool_call?.id !== grantedPermission.tool_call?.id) return false
 
   // Check permission type hierarchy
   const targetPermissionType = (targetPermission.extra?.permissionType as string) || 'read'
   if (!isPermissionSufficient(grantedPermissionType, targetPermissionType)) return false
 
-  // For special permission types, still require exact tool call matching (already checked above)
-  const targetType = targetPermission.extra?.permissionType as string
-  if (
-    targetType === 'command' ||
-    targetServerName === 'agent-filesystem' ||
-    targetServerName === 'deepchat-settings'
-  ) {
-    // Additional safety: these types should never be batched across different tool calls
-    return targetPermission.tool_call?.id === grantedPermission.tool_call?.id
-  }
-
   return true

875-878: Remove commented-out code.

Line 877 has a commented-out variable declaration (// const conversationId = message.conversationId). This is a leftover from refactoring.

🧹 Clean up
-      // const conversationId = message.conversationId

148-165: Verbose debug logging left in production code.

Lines 148–164 dump all pending permission blocks to console when updatedCount === 0. This is useful during development but noisy in production. Consider gating behind a debug flag or removing it.


697-709: Repeated as any casts on handleLLMAgentResponse calls.

Every call to handleLLMAgentResponse within executeSingleToolCall uses as any to bypass type checking (lines 707, 760, 793, 827, 843). This hides potential type mismatches and defeats the benefit of strict TypeScript. Consider defining a proper interface/union type for the event payloads.

Also applies to: 750-761, 783-794, 813-828, 832-844

src/main/presenter/agentPresenter/loop/toolCallProcessor.ts (1)

47-81: Local PermissionType and PermissionRequestPayload re-declare shared types.

PermissionType on line 53 duplicates src/shared/chat.d.ts:153, and PermissionRequestPayload (lines 63–81) overlaps with src/shared/types/core/llm-events.ts:173-191 and PreCheckedPermissionResult in toolPresenter/index.ts. Consider importing the shared types to keep a single source of truth.

Comment on lines +2241 to +2250

// 监听权限更新事件,刷新消息以显示最新的权限状态
window.electron.ipcRenderer.on(
STREAM_EVENTS.PERMISSION_UPDATED,
(_, payload: { messageId: string }) => {
console.log('[Chat Store] Permission updated, refreshing message:', payload.messageId)
// 触发消息缓存刷新,使UI显示下一个待处理的权限
bumpMessageCacheVersion()
}
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for PERMISSION_UPDATED to understand when and how it's emitted
rg -n 'PERMISSION_UPDATED' --type=ts --type=js -C 5

Repository: ThinkInAIXYZ/deepchat

Length of output: 4367


🏁 Script executed:

# Search for handleMessageEdited implementation to understand what it does
rg -n 'handleMessageEdited' --type=ts --type=js -C 3

Repository: ThinkInAIXYZ/deepchat

Length of output: 1448


🏁 Script executed:

# Search for bumpMessageCacheVersion to understand its purpose
rg -n 'bumpMessageCacheVersion' --type=ts --type=js -C 3

Repository: ThinkInAIXYZ/deepchat

Length of output: 2296


🏁 Script executed:

# Check the broader context around the PERMISSION_UPDATED handler in chat.ts
sed -n '2230,2260p' src/renderer/src/stores/chat.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 873


🏁 Script executed:

# Look for other IPC event listeners to understand the pattern used in this store
rg -n 'ipcRenderer\.on' src/renderer/src/stores/chat.ts -C 3

Repository: ThinkInAIXYZ/deepchat

Length of output: 3120


Bump cache version alone won't display permission changes—fetch the message to ensure content is updated.

The PERMISSION_UPDATED event signals that permissions have changed, but bumpMessageCacheVersion() only re-evaluates dependent computed properties (messageItems, etc.) with the existing cached message objects. If the message content hasn't already been refreshed by a stream:response or MESSAGE_EDITED event, the UI won't reflect the permission update.

Call handleMessageEdited(payload.messageId) to fetch the latest message from the main process and guarantee the UI displays the updated permission state:

♻️ Fetch the message to ensure content is fresh
     window.electron.ipcRenderer.on(
       STREAM_EVENTS.PERMISSION_UPDATED,
       (_, payload: { messageId: string }) => {
         console.log('[Chat Store] Permission updated, refreshing message:', payload.messageId)
-        // 触发消息缓存刷新,使UI显示下一个待处理的权限
-        bumpMessageCacheVersion()
+        // Fetch the latest message to ensure UI reflects updated permission state
+        handleMessageEdited(payload.messageId)
       }
     )

Also ensure comments use English per coding guidelines.

🤖 Prompt for AI Agents
In `@src/renderer/src/stores/chat.ts` around lines 2241 - 2250, The
PERMISSION_UPDATED handler only calls bumpMessageCacheVersion(), which won’t
refresh message content; update the ipc listener
(STREAM_EVENTS.PERMISSION_UPDATED) to call
handleMessageEdited(payload.messageId) after logging so the latest message is
fetched from the main process, then bumpMessageCacheVersion() if needed; also
change the inline comment to English per coding guidelines. Ensure you reference
the existing ipc handler that uses window.electron.ipcRenderer.on and the
functions handleMessageEdited and bumpMessageCacheVersion.

@zerob13 zerob13 changed the title fea:tagentic tool op feat: agentic tool op Feb 11, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/presenter/agentPresenter/acp/agentToolManager.ts (1)

1089-1119: ⚠️ Potential issue | 🔴 Critical

collectWriteTargets does not handle edit_file, so assertWritePermission is bypassed at runtime.

edit_file is in the write tools list and assertWritePermission is called for it (line 981), but collectWriteTargets has no case for 'edit_file'. It falls through to default and returns []. Since assertWritePermission returns early when targets.length === 0 (line 1067), no write-permission check is enforced for edit_file at execution time.

The pre-check in preCheckToolPermission works around this via the fallback on lines 1325–1331, but the actual enforcement path is skipped.

🐛 Add edit_file case to collectWriteTargets
       case 'text_replace': {
         const pathArg = args.path
         return typeof pathArg === 'string' ? [pathArg] : []
       }
+      case 'edit_file': {
+        const pathArg = args.path
+        return typeof pathArg === 'string' ? [pathArg] : []
+      }
       case 'move_files': {
🤖 Fix all issues with AI agents
In `@src/main/presenter/agentPresenter/acp/agentToolManager.ts`:
- Around line 989-999: The 'execute_command' switch case declares const
commandResult which can leak into other cases; fix it by adding a block scope
for that case (wrap the case body in { ... }) so local variables like
commandResult and the check for this.bashHandler are confined, and keep the
existing logic that calls this.bashHandler.executeCommand(parsedArgs, {
conversationId }) and returns the content (string or JSON.stringify) intact.

In `@src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts`:
- Around line 453-467: The current flush logic only runs when
session.outputFilePath && session.totalOutputLength >
config.offloadThresholdChars, causing unread stdout/stderr to be dropped for
non-offloaded sessions on exit; update the flush block in
BackgroundExecSessionManager (the method handling the exit/cleanup for a session
object) to attempt reading and appending any remaining stdout/stderr for all
sessions (regardless of offload) by calling session.child.stdout?.read() and
session.child.stderr?.read() and then invoking this.appendOutput(session, ... ,
config) when data exists, preserving the existing try/catch and logger.warn
behavior.

In `@src/main/presenter/agentPresenter/permission/permissionHandler.ts`:
- Around line 647-653: Remove the unused `denied` property and the forced cast
when pushing pending tool calls: instead of spreading and casting the result of
buildPendingToolCallFromBlock into a new object with a `denied` flag, push the
PendingToolCall returned by buildPendingToolCallFromBlock directly (i.e.,
toolCalls.push(pendingCall)). This eliminates the incorrect `as PendingToolCall`
type assertion and the unused `denied` field; note that executeSingleToolCall
re-checks permissionBlock?.status === 'denied' so the extra flag is unnecessary.
Ensure references to PendingToolCall, buildPendingToolCallFromBlock, and
executeSingleToolCall remain unchanged otherwise.

In `@src/main/presenter/mcpPresenter/toolManager.ts`:
- Around line 342-345: The current preCheckToolPermission method returns null
when this.toolNameToTargetMap is missing, which can be misinterpreted as
"permission granted"; change preCheckToolPermission to instead throw a
descriptive Error (e.g., "Tool target map unavailable for permission check")
when this.toolNameToTargetMap is falsy so callers (and callTool) cannot proceed
silently, and update any call sites expecting null accordingly; reference the
preCheckToolPermission method and this.toolNameToTargetMap to locate and
implement the change.
- Around line 637-648: The code path where remember is false and conversationId
is absent currently only logs "Temporary permission granted" but doesn't store
or apply the permission, so callers get no effect; fix by adding and using an
in-memory ephemeral permission store (e.g., this.temporaryPermissions as a Set
or Map) inside ToolManager and, where the current snippet builds the key
(`${serverName}:${permissionType}`), add that key to the ephemeral store when
conversationId is missing; also update any permission-checking logic (the places
that currently consult this.sessionPermissions) to consult
this.temporaryPermissions as well so the non-persistent grant actually takes
effect.
- Around line 289-295: The new Chinese comments in ToolManager should be
replaced with English; locate the comment blocks near the permission check in
the method that references conversationId and checkSessionPermission(serverName,
permissionType) (inside the ToolManager class) and translate them to concise
English equivalents (e.g., "Prioritize session-level memory permissions (current
session auto-executes)") and similarly convert the other Chinese comments around
the related permission/cache logic at the other modified spots; update the
inline comments and any console/log messages to English while keeping the
original intent and leaving function/class names unchanged.
- Around line 651-688: Translate the remaining Chinese comments to English in
the ToolManager methods: change the comment above checkSessionPermission
("检查会话级别的权限" and the explanatory line about permission hierarchy) to an English
description (e.g., "Check session-level permissions" and "Permission hierarchy:
all > write > read"), and translate the comment above clearSessionPermissions
("清除会话的临时权限") to "Clear temporary session permissions"; update any accompanying
inline comments in checkSessionPermission or references to
this.sessionPermissions to English as well so all comments/logs in
checkSessionPermission and clearSessionPermissions are fully in English.
🧹 Nitpick comments (3)
docs/specs/edit-file-tool/plan.md (1)

7-31: Fenced code block missing language specifier.

The ASCII diagram block on line 7 should have a language identifier (e.g., text or plaintext) to satisfy markdownlint MD040.

-```
+```text
 ┌─────────────────────────────────────────────────────────────┐
test/main/presenter/sessionPresenter/permissionHandler.test.ts (1)

269-316: These tests exercise mock behavior rather than PermissionHandler logic.

The "Session manager helpers" and "Permission resume lock" tests directly invoke the presenterMock.sessionManager methods, not the PermissionHandler. While this validates the mock is wired correctly (important for the integration tests above), it doesn't test any production SessionManager code. Consider renaming the describe block to something like "Session manager mock behavior" to clarify intent.

src/main/presenter/agentPresenter/acp/agentToolManager.ts (1)

866-869: Redundant process-tool guard inside callFileSystemTool.

callTool (line 403) already routes to callProcessTool before reaching callFileSystemTool. This guard is unreachable dead code in normal flow. While it's harmless as a safety net, consider removing it or adding a comment explaining its defensive intent.

Comment on lines 989 to 999
case 'execute_command':
if (!this.bashHandler) {
throw new Error('Bash handler not initialized for execute_command tool')
}
const commandResult = await this.bashHandler.executeCommand(parsedArgs, {
conversationId
})
return {
content: await this.bashHandler.executeCommand(parsedArgs, { conversationId })
content:
typeof commandResult === 'string' ? commandResult : JSON.stringify(commandResult)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Switch case declaration without block scope.

Biome correctly flags const commandResult on line 993 as a declaration that leaks into other switch cases. Wrap in braces.

♻️ Wrap in block scope
-        case 'execute_command':
+        case 'execute_command': {
           if (!this.bashHandler) {
             throw new Error('Bash handler not initialized for execute_command tool')
           }
           const commandResult = await this.bashHandler.executeCommand(parsedArgs, {
             conversationId
           })
           return {
             content:
               typeof commandResult === 'string' ? commandResult : JSON.stringify(commandResult)
           }
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'execute_command':
if (!this.bashHandler) {
throw new Error('Bash handler not initialized for execute_command tool')
}
const commandResult = await this.bashHandler.executeCommand(parsedArgs, {
conversationId
})
return {
content: await this.bashHandler.executeCommand(parsedArgs, { conversationId })
content:
typeof commandResult === 'string' ? commandResult : JSON.stringify(commandResult)
}
case 'execute_command': {
if (!this.bashHandler) {
throw new Error('Bash handler not initialized for execute_command tool')
}
const commandResult = await this.bashHandler.executeCommand(parsedArgs, {
conversationId
})
return {
content:
typeof commandResult === 'string' ? commandResult : JSON.stringify(commandResult)
}
}
🧰 Tools
🪛 Biome (2.3.14)

[error] 993-997: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In `@src/main/presenter/agentPresenter/acp/agentToolManager.ts` around lines 989 -
999, The 'execute_command' switch case declares const commandResult which can
leak into other cases; fix it by adding a block scope for that case (wrap the
case body in { ... }) so local variables like commandResult and the check for
this.bashHandler are confined, and keep the existing logic that calls
this.bashHandler.executeCommand(parsedArgs, { conversationId }) and returns the
content (string or JSON.stringify) intact.

Comment on lines +453 to +467
// Flush any remaining output
if (session.outputFilePath && session.totalOutputLength > config.offloadThresholdChars) {
try {
const remainingStdout = session.child.stdout?.read?.()
const remainingStderr = session.child.stderr?.read?.()
if (remainingStdout) {
this.appendOutput(session, remainingStdout.toString('utf-8'), config)
}
if (remainingStderr) {
this.appendOutput(session, remainingStderr.toString('utf-8'), config)
}
} catch (error) {
logger.warn(`[BackgroundExec] Failed to flush remaining output:`, error)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remaining output flush is skipped for non-offloaded sessions.

The guard on line 454 only flushes remaining stdout/stderr when the session has already been offloaded to a file. However, when using exit (not close), streams may still have unread data regardless of offload status. For non-offloaded sessions, any data arriving between the last data event and the exit event will be silently dropped.

🐛 Proposed fix: flush remaining output for all sessions
       // Flush any remaining output
-      if (session.outputFilePath && session.totalOutputLength > config.offloadThresholdChars) {
-        try {
-          const remainingStdout = session.child.stdout?.read?.()
-          const remainingStderr = session.child.stderr?.read?.()
-          if (remainingStdout) {
-            this.appendOutput(session, remainingStdout.toString('utf-8'), config)
-          }
-          if (remainingStderr) {
-            this.appendOutput(session, remainingStderr.toString('utf-8'), config)
-          }
-        } catch (error) {
-          logger.warn(`[BackgroundExec] Failed to flush remaining output:`, error)
+      try {
+        const remainingStdout = session.child.stdout?.read?.()
+        const remainingStderr = session.child.stderr?.read?.()
+        if (remainingStdout) {
+          this.appendOutput(session, remainingStdout.toString('utf-8'), config)
         }
+        if (remainingStderr) {
+          this.appendOutput(session, remainingStderr.toString('utf-8'), config)
+        }
+      } catch (error) {
+        logger.warn(`[BackgroundExec] Failed to flush remaining output:`, error)
       }
🤖 Prompt for AI Agents
In `@src/main/presenter/agentPresenter/acp/backgroundExecSessionManager.ts` around
lines 453 - 467, The current flush logic only runs when session.outputFilePath
&& session.totalOutputLength > config.offloadThresholdChars, causing unread
stdout/stderr to be dropped for non-offloaded sessions on exit; update the flush
block in BackgroundExecSessionManager (the method handling the exit/cleanup for
a session object) to attempt reading and appending any remaining stdout/stderr
for all sessions (regardless of offload) by calling session.child.stdout?.read()
and session.child.stderr?.read() and then invoking this.appendOutput(session,
... , config) when data exists, preserving the existing try/catch and
logger.warn behavior.

Comment on lines +647 to +653
} else if (permissionBlock.status === 'denied') {
// Denied - will generate error response later
const pendingCall = this.buildPendingToolCallFromBlock(block)
if (pendingCall) {
toolCalls.push({ ...pendingCall, denied: true } as PendingToolCall)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for PendingToolCall type definition
echo "=== Searching for PendingToolCall type definition ==="
rg -n "interface PendingToolCall|type PendingToolCall" --type=ts -A 15

echo ""
echo "=== Searching for denied field usage ==="
rg -n "denied\s*:" --type=ts src/main/presenter/agentPresenter/permission/

echo ""
echo "=== Checking executeSingleToolCall function ==="
rg -n "executeSingleToolCall" --type=ts -B 2 -A 30 | head -100

Repository: ThinkInAIXYZ/deepchat

Length of output: 11179


🏁 Script executed:

#!/bin/bash
# Check the file to see the specific lines mentioned in the review
echo "=== Content of permissionHandler.ts around lines 647-653 ==="
sed -n '640,660p' src/main/presenter/agentPresenter/permission/permissionHandler.ts

echo ""
echo "=== Content around line 714 (executeSingleToolCall) ==="
sed -n '700,750p' src/main/presenter/agentPresenter/permission/permissionHandler.ts

Repository: ThinkInAIXYZ/deepchat

Length of output: 2804


🏁 Script executed:

#!/bin/bash
# Search for all usages of the 'denied' property/field in the codebase
echo "=== Searching for references to .denied property ==="
rg -n "\.denied" --type=ts src/

echo ""
echo "=== Searching for where PendingToolCall is used ==="
rg -n "PendingToolCall" --type=ts | head -50

Repository: ThinkInAIXYZ/deepchat

Length of output: 2289


Remove the unused denied flag and type cast from line 651.

The denied property is not defined in the PendingToolCall type (defined in messageBuilder.ts:27-34) and is never used anywhere in the codebase. The spread operator and as PendingToolCall cast at line 651 hide a type error. The executeSingleToolCall method (line 692+) re-fetches the permission block from the database and checks permissionBlock?.status === 'denied' directly, never referencing the denied flag from the parameter.

Simply push the pendingCall as-is without adding the unused property:

toolCalls.push(pendingCall)
🤖 Prompt for AI Agents
In `@src/main/presenter/agentPresenter/permission/permissionHandler.ts` around
lines 647 - 653, Remove the unused `denied` property and the forced cast when
pushing pending tool calls: instead of spreading and casting the result of
buildPendingToolCallFromBlock into a new object with a `denied` flag, push the
PendingToolCall returned by buildPendingToolCallFromBlock directly (i.e.,
toolCalls.push(pendingCall)). This eliminates the incorrect `as PendingToolCall`
type assertion and the unused `denied` field; note that executeSingleToolCall
re-checks permissionBlock?.status === 'denied' so the extra flag is unnecessary.
Ensure references to PendingToolCall, buildPendingToolCallFromBlock, and
executeSingleToolCall remain unchanged otherwise.

Comment on lines +289 to +295
// 1. 优先检查 session 级别的内存权限(当前会话自动执行)
if (conversationId && this.checkSessionPermission(conversationId, serverName, permissionType)) {
console.log(
`[ToolManager] Permission granted via session cache: server '${serverName}' has '${permissionType}' permission`
)
return true
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Chinese comments on changed lines violate coding guidelines.

Lines 289, 297, 303 contain Chinese comments in newly modified code. The same applies to lines 651–652 and 685. Per coding guidelines, all comments must be in English.

♻️ Translate to English
-    // 1. 优先检查 session 级别的内存权限(当前会话自动执行)
+    // 1. Check session-level in-memory permission first (auto-execute for current session)
-    // 2. 检查持久化的 'all' 权限
+    // 2. Check persisted 'all' permission
-    // 3. 检查持久化的特定权限类型
+    // 3. Check persisted specific permission type

As per coding guidelines: **/*.{js,ts,tsx,jsx,vue,mjs,cjs}: All logs and comments must be in English.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 1. 优先检查 session 级别的内存权限(当前会话自动执行)
if (conversationId && this.checkSessionPermission(conversationId, serverName, permissionType)) {
console.log(
`[ToolManager] Permission granted via session cache: server '${serverName}' has '${permissionType}' permission`
)
return true
}
// 1. Check session-level in-memory permission first (auto-execute for current session)
if (conversationId && this.checkSessionPermission(conversationId, serverName, permissionType)) {
console.log(
`[ToolManager] Permission granted via session cache: server '${serverName}' has '${permissionType}' permission`
)
return true
}
🤖 Prompt for AI Agents
In `@src/main/presenter/mcpPresenter/toolManager.ts` around lines 289 - 295, The
new Chinese comments in ToolManager should be replaced with English; locate the
comment blocks near the permission check in the method that references
conversationId and checkSessionPermission(serverName, permissionType) (inside
the ToolManager class) and translate them to concise English equivalents (e.g.,
"Prioritize session-level memory permissions (current session auto-executes)")
and similarly convert the other Chinese comments around the related
permission/cache logic at the other modified spots; update the inline comments
and any console/log messages to English while keeping the original intent and
leaving function/class names unchanged.

Comment on lines +342 to +345
if (!this.toolNameToTargetMap) {
console.error('[ToolManager] Tool target map is not available for permission check.')
return null
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Returning null when tool map is unavailable masks a lookup failure.

When this.toolNameToTargetMap is null, the method returns null (meaning "no permission needed"), which could silently skip a permission check. While callTool would fail separately, preCheckToolPermission callers may interpret null as "safe to proceed."

Consider returning null with a more specific log, or throwing, so callers don't assume permission is granted.

🤖 Prompt for AI Agents
In `@src/main/presenter/mcpPresenter/toolManager.ts` around lines 342 - 345, The
current preCheckToolPermission method returns null when this.toolNameToTargetMap
is missing, which can be misinterpreted as "permission granted"; change
preCheckToolPermission to instead throw a descriptive Error (e.g., "Tool target
map unavailable for permission check") when this.toolNameToTargetMap is falsy so
callers (and callTool) cannot proceed silently, and update any call sites
expecting null accordingly; reference the preCheckToolPermission method and
this.toolNameToTargetMap to locate and implement the change.

Comment on lines +637 to 648
if (conversationId) {
const key = `${serverName}:${permissionType}`
const existing = this.sessionPermissions.get(conversationId) ?? new Set<string>()
existing.add(key)
this.sessionPermissions.set(conversationId, existing)
console.log(
`[ToolManager] Session permission stored: ${key} for conversation ${conversationId}`
)
} else {
console.log(`[ToolManager] Temporary permission granted (no conversationId)`)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Non-persistent grant with no conversationId is a silent no-op.

When remember is false and conversationId is absent (line 645–647), the permission is neither persisted nor session-cached. The log says "Temporary permission granted" but nothing actually happens. This could confuse callers who expect the grant to take effect.

🤖 Prompt for AI Agents
In `@src/main/presenter/mcpPresenter/toolManager.ts` around lines 637 - 648, The
code path where remember is false and conversationId is absent currently only
logs "Temporary permission granted" but doesn't store or apply the permission,
so callers get no effect; fix by adding and using an in-memory ephemeral
permission store (e.g., this.temporaryPermissions as a Set or Map) inside
ToolManager and, where the current snippet builds the key
(`${serverName}:${permissionType}`), add that key to the ephemeral store when
conversationId is missing; also update any permission-checking logic (the places
that currently consult this.sessionPermissions) to consult
this.temporaryPermissions as well so the non-persistent grant actually takes
effect.

Comment on lines +651 to +688
// 检查会话级别的权限
// 当前会话权限遵循层级:all > write > read
checkSessionPermission(
conversationId: string,
serverName: string,
permissionType: 'read' | 'write' | 'all'
): boolean {
const sessionPerms = this.sessionPermissions.get(conversationId)
if (!sessionPerms) return false

const permissionLevelMap: Record<'read' | 'write' | 'all', number> = {
read: 1,
write: 2,
all: 3
}
const requiredLevel = permissionLevelMap[permissionType]
const prefix = `${serverName}:`

for (const permKey of sessionPerms) {
if (!permKey.startsWith(prefix)) continue

const storedPermission = permKey.slice(prefix.length) as 'read' | 'write' | 'all'
const storedLevel = permissionLevelMap[storedPermission]
if (storedLevel >= requiredLevel) {
console.log(
`[ToolManager] Session auto-execute: server '${serverName}' has granted permission '${permKey}' in conversation '${conversationId}', required='${permissionType}'`
)
return true
}
}

return false
}

// 清除会话的临时权限
clearSessionPermissions(conversationId: string): void {
this.sessionPermissions.delete(conversationId)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Session permission hierarchy is now correctly implemented; translate remaining Chinese comments.

The checkSessionPermission method now properly enforces all > write > read using numeric levels, addressing the previous review's security concern about blanket server access.

However, lines 651–652 and 685 still have Chinese comments:

♻️ Translate to English
-  // 检查会话级别的权限
-  // 当前会话权限遵循层级:all > write > read
+  // Check session-level permission
+  // Session permission hierarchy: all > write > read
   checkSessionPermission(
-  // 清除会话的临时权限
+  // Clear session's temporary permissions
   clearSessionPermissions(conversationId: string): void {

As per coding guidelines: **/*.{js,ts,tsx,jsx,vue,mjs,cjs}: All logs and comments must be in English.

🤖 Prompt for AI Agents
In `@src/main/presenter/mcpPresenter/toolManager.ts` around lines 651 - 688,
Translate the remaining Chinese comments to English in the ToolManager methods:
change the comment above checkSessionPermission ("检查会话级别的权限" and the explanatory
line about permission hierarchy) to an English description (e.g., "Check
session-level permissions" and "Permission hierarchy: all > write > read"), and
translate the comment above clearSessionPermissions ("清除会话的临时权限") to "Clear
temporary session permissions"; update any accompanying inline comments in
checkSessionPermission or references to this.sessionPermissions to English as
well so all comments/logs in checkSessionPermission and clearSessionPermissions
are fully in English.

@zerob13 zerob13 merged commit ae48f19 into dev Feb 11, 2026
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant