Skip to content

Conversation

@zerob13
Copy link
Collaborator

@zerob13 zerob13 commented Nov 21, 2025

Summary by CodeRabbit

  • New Features

    • Interactive terminal-based ACP agent initialization with an in-app terminal dialog, per-agent Initialize button, and ability to open the user’s default terminal to run commands.
  • Internationalization

    • Added initialization and terminal status translations across multiple languages.
  • Chores / Refactor

    • Standardized bundled runtime usage to Node across platforms and centralized shell/runtime environment handling.
  • Dependencies

    • Added PTY and xterm terminal dependencies to enable interactive terminal sessions.
  • Version

    • Bumped package version to 0.4.9.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

Walkthrough

Adds a centralized RuntimeHelper and shell env caching, PTY-based ACP agent initialization (node-pty) with IPC wiring, a cross-platform TerminalHelper, new terminal UI/dialog (xterm.js) and i18n strings, removes Bun runtime variants from package scripts, and updates package dependencies.

Changes

Cohort / File(s) Summary
Package & runtime deps
package.json
Bumped version; removed Bun-based installRuntime variants; standardized installRuntime to use node (uv retained for initial injection); added node-pty, @xterm/addon-fit, @xterm/xterm.
Runtime helper
src/main/lib/runtimeHelper.ts
New singleton RuntimeHelper to detect/initialize runtimes, replace commands with bundled runtime equivalents, expand paths, normalize PATH env, and provide default paths.
Shell env caching
src/main/presenter/llmProviderPresenter/agent/shellEnvHelper.ts
New getShellEnvironment() with caching and clearShellEnvironmentCache() to fetch user shell env and return filtered Node/tooling variables.
Terminal helper utility
src/main/lib/terminalHelper.ts
New cross-platform TerminalHelper.openTerminalAndExecute(command, keepOpen?) that builds temp scripts and launches macOS (Terminal.app/iTerm via AppleScript), Windows (wt.exe/cmd) and Linux terminals with fallbacks and error handling.
Acp PTY init helper
src/main/presenter/configPresenter/acpInitHelper.ts
New AcpInitHelper using node-pty: prepares runtime/env (including registry values), spawns interactive PTY shells, streams output to renderer, injects initial commands, and exposes initializeBuiltinAgent, initializeCustomAgent, writeToTerminal, killTerminal.
ConfigPresenter IPC & API
src/main/presenter/configPresenter/index.ts, src/shared/types/presenters/legacy.presenters.d.ts
Adds IPC handlers (acp-terminal:input, acp-terminal:kill) and initializeAcpAgent(agentId,isBuiltin) on IConfigPresenter; wiring to ACP init flows and clearing shell env cache when toggling runtimes.
Acp process/runtime management
src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts
Reworked runtime handling to use RuntimeHelper; removed Bun branches; unified Node/UV command replacement and PATH augmentation; improved env merging and stdin EPIPE handling.
MCP / Powerpack runtime cleanup
src/main/presenter/mcpPresenter/mcpClient.ts, src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts
Removed Bun runtime fields/branches; delegate runtime resolution to RuntimeHelper; Node runtime used for code execution paths.
Renderer: ACP settings UI
src/renderer/settings/components/AcpSettings.vue
Adds per-agent Initialize button, per-agent initializing state, handleInitializeAgent() to open terminal dialog and call presenter initialize flow with error handling.
Renderer: Terminal dialog
src/renderer/settings/components/AcpTerminalDialog.vue
New Vue component embedding xterm.js, subscribes to IPC for PTY data/events, handles input/kill, lifecycle and status states (idle/running/completed/error).
i18n additions
src/renderer/src/i18n/.../settings.json
Added initialization-related keys and a terminal object (status and lifecycle messages) across locales (en-US, fa-IR, fr-FR, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW).
Types
src/shared/types/presenters/legacy.presenters.d.ts
Added initializeAcpAgent(agentId: string, isBuiltin: boolean): Promise<void> to IConfigPresenter.
Tests
test/main/presenter/mcpClient.test.ts
Removed Bun-related test setup and tests that asserted runtime-path replacement / Bun detection.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Settings as AcpSettings.vue
    participant Dialog as AcpTerminalDialog
    participant Presenter as ConfigPresenter
    participant Init as AcpInitHelper
    participant PTY as node-pty
    participant Shell as Platform Shell

    User->>Settings: Click "Initialize"
    Settings->>Dialog: open dialog
    Settings->>Presenter: initializeAcpAgent(agentId,isBuiltin)
    Presenter->>Init: initializeBuiltinAgent / initializeCustomAgent
    Init->>Init: detect runtimes & build env (RuntimeHelper + shell env)
    Init->>PTY: spawn(shell, env)
    PTY->>Shell: launch shell process
    Shell-->>PTY: stdout/stderr
    PTY-->>Presenter: forward data via IPC
    Presenter-->>Dialog: acp-terminal:data
    Dialog->>User: display output

    rect rgba(120,180,160,0.12)
      User->>Dialog: type input
      Dialog->>Presenter: acp-terminal:input
      Presenter->>PTY: write(input)
      PTY->>Shell: inject input
    end

    Shell-->>PTY: exit
    PTY-->>Presenter: exit event
    Presenter-->>Dialog: acp-terminal:exit
    Dialog->>User: show status (completed/error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • AcpInitHelper: PTY lifecycle, readiness heuristic, env/PATH merging, registry injection, webContents validation.
    • RuntimeHelper & shellEnvHelper: correctness of path replacement, Windows vs POSIX behavior, caching/invalidation.
    • TerminalHelper: platform-specific launch commands, quoting/escaping, temp-file lifecycle.
    • AcpTerminalDialog: xterm initialization timing, IPC subscriptions and cleanup.
    • acpProcessManager / mcpClient / powerpackServer: ensure removal of Bun branches doesn't regress platform-specific behaviors.

Possibly related PRs

Suggested labels

codex

Poem

🐰 I hopped into terminals with a tiny script and cheer,

Swapped Bun for Node and warmed each PTY sphere.
xterm lights the dialog, commands begin to hum,
Agents wake and chatter — initialization done! 🎶

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title 'fix: acp process spawn failed on nvm' is vague and does not clearly describe what the actual changes accomplish. While it mentions a specific issue (nvm-related spawn failure), the changeset includes extensive refactoring of runtime handling, addition of terminal initialization features, and removal of Bun runtime support—far beyond fixing an nvm issue. Revise the title to accurately reflect the main changes, such as 'refactor: migrate runtime handling from Bun to Node/UV and add ACP agent initialization' or similar, to better communicate the scope of this comprehensive update.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/acp-spawn-issue3

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e745e7e and abd3c16.

📒 Files selected for processing (1)
  • package.json (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-12T00:58:06.513Z
Learnt from: zerob13
Repo: ThinkInAIXYZ/deepchat PR: 977
File: package.json:137-137
Timestamp: 2025-10-12T00:58:06.513Z
Learning: In this Electron project, renderer process dependencies (used only in src/renderer/) should be placed in devDependencies because they are bundled by electron-vite during the build process. Only main process dependencies (used in src/main/) need to be in the dependencies section.

Applied to files:

  • package.json
🔇 Additional comments (5)
package.json (5)

3-3: Version bump is appropriate.

The minor version bump (0.4.8 → 0.4.9) aligns with the bugfix nature of this PR.


46-52: Runtime script changes are consistent across platforms.

The replacement of bun-based runtimes with node-based runtimes is applied uniformly across all platform-specific scripts. Each maintains the uv + node injection pattern.


57-57: cleanRuntime script correctly updated.

The script now removes both uv and node runtime directories, which is consistent with the new installation flow.


89-89: Verify node-pty compatibility with the project's Node.js requirement.

The package specifies Node.js >=20.19.0 (line 9). Ensure that node-pty@^1.0.0 is compatible with this version and that native module compilation is handled correctly in the build process.


180-181: XTerm packages correctly placed in devDependencies.

These renderer-process dependencies are appropriately placed in devDependencies to be bundled by electron-vite during the build process, addressing the previous review feedback and aligning with Electron project best practices.


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.

@zerob13 zerob13 marked this pull request as ready for review November 21, 2025 07:56
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: 2

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/mcpPresenter/inMemoryServers/powerpackServer.ts (1)

55-72: Fix critical regex security flaw in code safety validation

The vulnerability identified is confirmed as real and critical. CODE_EXECUTION_FORBIDDEN_PATTERNS uses global regex flags, and when .test() is called repeatedly on different code strings, the patterns' lastIndex persists across invocations. This causes later validations to start searching from the wrong position, creating false negatives that allow dangerous code to bypass the security check.

In src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts (lines 56-72), remove the g flag from all 12 regex patterns:

-  /require\s*\(\s*['"](fs|child_process|path|dgram|cluster|v8|vm|module|worker_threads|repl|tls)['"]\s*\)/gi,
+  /require\s*\(\s*['"](fs|child_process|path|dgram|cluster|v8|vm|module|worker_threads|repl|tls)['"]\s*\)/i,
-  /process\.(exit|kill|abort|chdir|cwd|binding|dlopen|abort)/gi,
+  /process\.(exit|kill|abort|chdir|cwd|binding|dlopen|abort)/i,

Apply this change to all 12 patterns in the array. Alternatively, reset lastIndex before each .test() call in checkCodeSafety() (lines 154-155).

🧹 Nitpick comments (16)
src/main/presenter/mcpPresenter/mcpClient.ts (1)

171-245: Runtime unification looks good; tighten Node/UV command detection to avoid false positives

The runtime handling refactor is generally solid:

  • replaceWithRuntimeCommand now consistently routes node, npm, npx and uv/uvx through the bundled runtimes (with correct .exe/.cmd handling on Windows and bin/* on Unix).
  • processCommandWithArgs normalizes both the main command and any bare node/npm/npx/uv arguments, which is reasonable for commands that directly invoke these tools.
  • For Node/UV commands, you build a controlled env with a composed PATH and limited extra vars, while non‑Node commands inherit the full process env with PATH just augmented — this cleanly separates “managed” runtime invocations from everything else.

One thing worth tightening is isNodeCommand:

const isNodeCommand = ['node', 'npm', 'npx', 'uv', 'uvx'].some(
  (cmd) => command.includes(cmd) || args.some((arg) => arg.includes(cmd))
)

Using substring .includes on the full path/arg can accidentally classify unrelated binaries as “Node/UV commands” (e.g. a command under /opt/rnode/bin or an arg containing some-node-tool). That would push them through the Node/UV env whitelist, possibly dropping needed env vars.

A safer pattern is to base this on path.basename (similar to what you already do in replaceWithRuntimeCommand), e.g.:

const isNodeCommand = ['node', 'npm', 'npx', 'uv', 'uvx'].some((cmd) => {
  if (path.basename(command) === cmd) return true
  return args.some((arg) => path.basename(arg) === cmd)
})

This keeps the new runtime unification but avoids surprising reclassification of arbitrary commands.

Also applies to: 388-401, 415-418, 427-434, 472-479

src/main/lib/terminalHelper.ts (1)

205-211: Temp script naming & lifecycle could be more robust

createTempScript uses only Date.now() for filenames and never cleans up the temp files, so repeated calls can accumulate scripts in the temp directory and there is a (low) collision risk under high frequency.

Consider:

  • Adding a random suffix to the filename.
  • Optionally making the scripts self‑delete at the end (e.g., via an rm "$0"/del "%~f0" tail) if you don’t need them after execution.

Example change for safer naming:

-    const scriptPath = path.join(tempDir, `deepchat-acp-init-${Date.now()}${extension}`)
+    const random = Math.random().toString(36).slice(2, 8)
+    const scriptPath = path.join(
+      tempDir,
+      `deepchat-acp-init-${Date.now()}-${random}${extension}`
+    )
src/main/presenter/configPresenter/acpInitHelper.ts (3)

214-241: Avoid unbounded PTY output buffering for shell readiness

outputBuffer grows without bound and is only used for heuristic readiness detection and logging. Long-running installs (npm install -g ..., uv tool run ...) can easily produce multi‑MB output, which is unnecessary to keep in memory.

You can cap the buffer while preserving readiness behavior, e.g.:

-    let shellReady = false
-    let outputBuffer = ''
+    let shellReady = false
+    let outputBuffer = ''
+    const MAX_BUFFER = 8192
@@
-    pty.onData((data: string) => {
-      outputBuffer += data
+    pty.onData((data: string) => {
+      outputBuffer = (outputBuffer + data).slice(-MAX_BUFFER)
@@
-      if (!shellReady && outputBuffer.length > 0) {
+      if (!shellReady && outputBuffer.length > 0) {
         // Check for common shell prompt patterns or any non-empty output

This keeps memory bounded while still allowing prompt detection and logging.


334-345: Simplify env construction to avoid double-merging process.env

buildEnvironmentVariables already starts from process.env and layers runtime/registry/profile env on top, but spawn is called with { ...process.env, ...envVars }, effectively merging the system env twice.

You could simplify to reduce confusion and potential key inconsistencies:

-      env: { ...process.env, ...envVars } as Record<string, string>
+      env: envVars

and, if desired, move the initial process.env copy out of buildEnvironmentVariables so its responsibility is clearly “build complete env for PTY”.

Also applies to: 200-206


137-152: Single global PTY session – confirm this is intentional

AcpInitHelper tracks a single activeShell and startInteractiveSession always calls killTerminal() before spawning a new PTY. This means:

  • Starting initialization for one agent will immediately kill any in‑progress initialization for another agent/window.
  • All writeToTerminal / killTerminal calls operate on this single shared PTY.

If the UX is meant to support only one ACP initialization session globally, this is fine; otherwise, consider keying sessions by agent/conversation or webContents.id instead of a single global activeShell.

Also applies to: 173-176, 506-555

src/shared/types/presenters/legacy.presenters.d.ts (1)

535-535: Implementation verified—typing improvement remains optional

The initializeAcpAgent(agentId: string, isBuiltin: boolean) method has a concrete implementation wired correctly:

  • Implementation at src/main/presenter/configPresenter/index.ts:1021 validates the agentId parameter at runtime (throws if the agent is not found in builtins or customs list)
  • Call site at src/renderer/settings/components/AcpSettings.vue:699 invokes it with the correct contract
  • Parameters originate from user selection of builtin/custom agents in the UI

The implementation works as designed. However, the type signature could be tightened: the code uses an as AcpBuiltinAgentId cast in the builtin path (line 1054), which validates at runtime. A discriminated union would provide compile-time safety and eliminate the cast:

initializeAcpAgent(agentId: AcpBuiltinAgentId, isBuiltin: true): Promise<void>
initializeAcpAgent(agentId: string, isBuiltin: false): Promise<void>

This shifts validation from runtime to compile-time but is optional given the current runtime checks.

src/renderer/settings/components/AcpTerminalDialog.vue (3)

107-116: Consider always sending acp-terminal:kill on close instead of gating on isRunning

Right now the close button only sends acp-terminal:kill when isRunning is true. Given that process startup and the acp-init:start event are slightly delayed, there’s a small window where a process may already be spawning but isRunning is still false, so closing the dialog won’t request a kill. Since killTerminal() is already defensive, you can simplify and make this more robust by always sending the kill on manual close:

-const handleManualClose = () => {
-  // Kill process if running
-  if (isRunning.value) {
-    if (window.electron) {
-      window.electron.ipcRenderer.send('acp-terminal:kill')
-    }
-  }
+const handleManualClose = () => {
+  // Always request backend to stop the active shell; it will no-op if none exists
+  if (window.electron) {
+    window.electron.ipcRenderer.send('acp-terminal:kill')
+  }
   emit('update:open', false)
   emit('close')
}

165-220: Guard terminal writes a bit more defensively to avoid noise and logs on malformed data

handleOutput already copes with both string and { type, data } formats and falls back to per‑character writes for xterm parsing errors, which is good. Two small improvements to reduce noise and make it even more defensive:

  • Drop or downgrade the per‑chunk console.log (length/preview) to debug or behind a flag; high‑volume PTY output will spam the console.
  • Before logging data.type, guard against type being missing/undefined on future payloads, to avoid logging undefined or needing narrower typings later.

These are minor, but will keep logs cleaner under heavy initialization output.


251-275: IPC listener lifecycle is correct but could be made idempotent-safe

setupIpcListeners and removeIpcListeners are paired and gated on window.electron, which is good. Just be aware that if this component is ever opened/closed very quickly or reused in another context, setupIpcListeners can be called multiple times before removeIpcListeners runs, leading to duplicate handlers.

This is largely mitigated by always calling removeAllListeners for the channels on close, but if this dialog is ever used from multiple windows you may want to scope the listeners to a per‑webContents identifier instead of global channels. Not blocking for this PR.

src/renderer/settings/components/AcpSettings.vue (1)

317-319: Initialization state tracking is clear; consider preventing concurrent initializations across agents

The initializing map and helpers (isInitializing/setInitializing) give precise, per‑agent “starting…” state while you open the terminal and call configPresenter.initializeAcpAgent, which is good.

Currently, nothing prevents starting initialization of another agent while one is already in progress (only per‑agent). Because the underlying init helper maintains a single active PTY, concurrent calls might be confusing (shared terminal, last call wins).

If you want to keep UX simple, consider a lightweight global guard (e.g., const initializingAny = computed(() => Object.keys(initializing).length > 0)) and disable all Initialize buttons while initializingAny is true, or at least while terminalDialogOpen is true.

Also applies to: 668-709

src/renderer/src/i18n/pt-BR/settings.json (1)

886-908: ACP inicialização/terminal keys match structure; minor PT-BR phrasing tweaks possible

The added initialize* and nested terminal.* keys mirror the other locales and line up with how AcpSettings and AcpTerminalDialog use them, so structurally this is correct.

If you want to polish PT‑BR wording later (optional):

  • Use imperative or title‑case for button labels and titles, e.g. "initialize": "Inicializar" and "terminal.title": "Terminal de inicialização" for consistency with the rest of the locale.
  • Consider "close": "Fechar" and "running": "Em execução" to match common UI phrasing.

Functionally everything is fine; this is just language quality polish.

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

26-27: IPC wiring for terminal input/kill is straightforward; ensure single presenter instance

Importing ipcMain, calling setupIpcHandlers() in the constructor, and routing:

  • 'acp-terminal:input'writeToTerminal
  • 'acp-terminal:kill'killTerminal

is a clean way to centralize PTY control in the config presenter. Just make sure ConfigPresenter is only instantiated once in the main process; otherwise you’ll accumulate duplicate ipcMain.on handlers for these channels.

Also applies to: 45-50, 188-191, 236-243


1018-1074: initializeAcpAgent logic is correct; consider handling missing webContents explicitly

This method:

  • Reads useBuiltinRuntime from ACP settings.
  • Optionally pulls npm/uv registries from mcpPresenter.
  • Locates the settings window’s webContents for streaming.
  • For built‑in agents: validates existence, finds active profile, and calls initializeBuiltinAgent.
  • For custom agents: validates existence and calls initializeCustomAgent.

The happy path is solid, and the error messages for missing agents/profiles are clear.

Two small robustness improvements you might consider:

  1. Guard on missing settings window
    If settingsWindow is missing or destroyed, webContents is undefined, so init will still start but nothing will be streamed back to the terminal dialog (if someone calls this from another context). You may want to fail fast with a clear error instead:

    if (!webContents) {
      throw new Error('[ACP Init] Settings window not available for terminal streaming')
    }
  2. Optional: log whether the target agent is enabled
    For debugging, logging agent.enabled (for custom/built‑in) when initializing could help explain why an initialization is running for a disabled agent, if that ever becomes an issue.

Neither is blocking, but they’ll make failures easier to diagnose if this method is reused beyond the settings UI.

src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (3)

264-355: Runtime command replacement for node/npm/npx/uv looks sane; minor nits only

The node/npm/npx and uv/uvx remapping against this.nodeRuntimePath / this.uvRuntimePath is consistent and correctly falls back to the original command when embedded binaries are absent. This should avoid breaking user-installed runtimes when the bundled ones are missing or incomplete.

If you want to trim a bit of duplication / I/O:

  • The existence checks here repeat what setupRuntimes already ensured. You could skip the fs.existsSync calls in this method and trust the precomputed nodeRuntimePath / uvRuntimePath, or gate them behind a debug-only assertion.
  • On non‑Windows, the targetCommand branching could be collapsed to just const targetCommand = basename without changing behavior.

These are minor readability/perf nits; behavior looks good as-is.


454-480: Tighten isNodeCommand detection to avoid false positives and unintended env whitelisting

The current detection:

const isNodeCommand = ['node', 'npm', 'npx', 'uv', 'uvx'].some(
  (cmd) =>
    processedCommand.includes(cmd) ||
    processedArgs.some((arg) => typeof arg === 'string' && arg.includes(cmd))
)

is quite broad:

  • Any command whose full path contains node, npm, npx, uv, or uvx as a substring (e.g. /opt/tools/node_exporter, a custom script with uv in its name, etc.) will be treated as a Node/UV command.
  • Any argument containing those substrings will also flip isNodeCommand to true, even when the actual executable is unrelated (e.g. python script.py --mode=node).

Because isNodeCommand drives the strict env whitelist logic, this can unintentionally strip most of process.env for non‑Node tools, which is a behavioral change and potentially hard to debug.

You can make this more robust by classifying based on the executable name (with extension stripping on Windows), and ignoring arguments entirely:

-    // Determine if it's Node.js/UV related command
-    const isNodeCommand = ['node', 'npm', 'npx', 'uv', 'uvx'].some(
-      (cmd) =>
-        processedCommand.includes(cmd) ||
-        processedArgs.some((arg) => typeof arg === 'string' && arg.includes(cmd))
-    )
+    // Determine if it's Node.js/UV related command based on the executable name
+    const commandBase = path.basename(processedCommand).toLowerCase()
+    const commandName = commandBase.replace(/\.(exe|cmd)$/i, '')
+    const isNodeCommand = ['node', 'npm', 'npx', 'uv', 'uvx'].includes(commandName)

This keeps the intended behavior for node, npm, npx, uv, uvx (including absolute paths and .exe/.cmd wrappers) while avoiding accidental matches on unrelated commands or arguments.

The rest of the env/PATH shaping logic (whitelisting for Node/UV commands, full process.env for others, and gating builtin runtime paths on useBuiltinRuntime) looks consistent and should help with the nvm-related spawn failures.

Also applies to: 486-539, 561-585


595-628: Empty-string env handling changes semantics; confirm this is intended

The new guards:

if (value !== undefined && value !== '') { ... }
...
if (npmRegistry && npmRegistry !== '') { ... }
...
if (uvRegistry && uvRegistry !== '') { ... }

mean that:

  • Agent-provided env vars with '' as value are now silently ignored instead of being passed through.
  • NPM/UV registry helpers returning '' will no longer clear or override any existing registry-related vars.

This is probably what you want to avoid accidentally clobbering env with empty values, but it does remove the ability for an agent config to explicitly force an env var to empty (e.g., to clear a proxy or registry setting).

If explicit clearing is a use case you care about, consider treating undefined/null as “not set” and letting empty strings pass through:

-      Object.entries(agent.env).forEach(([key, value]) => {
-        if (value !== undefined && value !== '') {
+      Object.entries(agent.env).forEach(([key, value]) => {
+        if (value != null) {
           // ...
         }
       })

and drop the !== '' checks for the registry values as well. Otherwise, it would be good to document somewhere that null/omission means “don’t touch env” and empty strings are ignored.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 988b01b and af4f6e1.

📒 Files selected for processing (21)
  • package.json (3 hunks)
  • src/main/lib/terminalHelper.ts (1 hunks)
  • src/main/presenter/configPresenter/acpInitHelper.ts (1 hunks)
  • src/main/presenter/configPresenter/index.ts (5 hunks)
  • src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (9 hunks)
  • src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts (5 hunks)
  • src/main/presenter/mcpPresenter/mcpClient.ts (5 hunks)
  • src/renderer/settings/components/AcpSettings.vue (6 hunks)
  • src/renderer/settings/components/AcpTerminalDialog.vue (1 hunks)
  • src/renderer/src/i18n/en-US/settings.json (2 hunks)
  • src/renderer/src/i18n/fa-IR/settings.json (1 hunks)
  • src/renderer/src/i18n/fr-FR/settings.json (1 hunks)
  • src/renderer/src/i18n/ja-JP/settings.json (1 hunks)
  • src/renderer/src/i18n/ko-KR/settings.json (1 hunks)
  • src/renderer/src/i18n/pt-BR/settings.json (1 hunks)
  • src/renderer/src/i18n/ru-RU/settings.json (1 hunks)
  • src/renderer/src/i18n/zh-CN/settings.json (2 hunks)
  • src/renderer/src/i18n/zh-HK/settings.json (1 hunks)
  • src/renderer/src/i18n/zh-TW/settings.json (1 hunks)
  • src/shared/types/presenters/legacy.presenters.d.ts (1 hunks)
  • test/main/presenter/mcpClient.test.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • test/main/presenter/mcpClient.test.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-12T00:58:06.513Z
Learnt from: zerob13
Repo: ThinkInAIXYZ/deepchat PR: 977
File: package.json:137-137
Timestamp: 2025-10-12T00:58:06.513Z
Learning: In this Electron project, renderer process dependencies (used only in src/renderer/) should be placed in devDependencies because they are bundled by electron-vite during the build process. Only main process dependencies (used in src/main/) need to be in the dependencies section.

Applied to files:

  • package.json
🧬 Code graph analysis (3)
src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts (1)
src/main/presenter/llmProviderPresenter/managers/toolCallProcessor.ts (1)
  • process (34-233)
src/main/presenter/configPresenter/acpInitHelper.ts (1)
src/shared/types/presenters/legacy.presenters.d.ts (3)
  • AcpBuiltinAgentId (678-678)
  • AcpAgentProfile (680-686)
  • AcpAgentConfig (712-718)
src/main/presenter/configPresenter/index.ts (4)
test/mocks/electron.ts (1)
  • ipcMain (12-16)
src/main/presenter/configPresenter/acpInitHelper.ts (8)
  • writeToTerminal (121-135)
  • writeToTerminal (549-551)
  • killTerminal (137-152)
  • killTerminal (553-555)
  • initializeBuiltinAgent (45-83)
  • initializeBuiltinAgent (515-531)
  • initializeCustomAgent (88-119)
  • initializeCustomAgent (533-547)
src/main/presenter/index.ts (1)
  • presenter (223-223)
src/shared/types/presenters/legacy.presenters.d.ts (1)
  • AcpBuiltinAgentId (678-678)
🔇 Additional comments (15)
package.json (1)

46-52: LGTM on runtime migration away from Bun.

The removal of Bun runtime entries across all installRuntime variants and cleanRuntime is consistent and aligns with the PR objective. The retention of uv runtime alongside node suggests a clear separation (node for agents/spawning, uv for package management). No concerns here.

Also applies to: 57-57

src/renderer/src/i18n/fr-FR/settings.json (1)

886-908: ACP initialization terminal strings look consistent and complete

The new initialize* and terminal keys align with the ACP initialization flow and match the key set used in other locales; JSON shape and placeholders look correct.

src/renderer/src/i18n/ru-RU/settings.json (1)

886-908: Russian ACP initialization/terminal entries are aligned with other locales

All expected initialize* and terminal keys are present, properly nested, and keep the {code} placeholder; placement near existing ACP strings is appropriate.

src/renderer/src/i18n/ja-JP/settings.json (1)

886-908: ja-JP ACP initialization and terminal messages are correctly wired

The added initialize* and terminal keys match the shared schema across locales; placeholder usage and JSON structure are correct.

src/renderer/src/i18n/fa-IR/settings.json (1)

886-908: fa-IR ACP initialization/terminal translations are structurally correct

All required initialization and terminal keys are present and consistent with other locales; JSON is valid and placeholders like {code} remain intact.

src/renderer/src/i18n/ko-KR/settings.json (1)

886-908: ko-KR ACP initialization and terminal keys are complete and usable

The new profileSwitched, initialize*, and terminal blocks match the shared key schema and keep placeholders consistent; any minor phrasing polish can be done later without impacting functionality.

src/renderer/src/i18n/zh-TW/settings.json (1)

886-908: zh-TW ACP initialization/terminal additions match the shared schema

Initialization and terminal keys are correctly added under ACP, mirroring other locales; structure and placeholders (e.g. {code}) are preserved.

src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts (1)

138-145: Node-only runtime path and tool exposure logic are coherent

  • setupRuntimes now only tracks a Node.js runtime and logs clearly when neither Node nor E2B is usable.
  • executeJavaScriptCode and the run_node_code handler both guard on this.nodeRuntimePath, while ListToolsRequest only exposes run_node_code when that path exists, so the UI can’t offer a non-functional tool.
  • E2B execution is cleanly separated via run_code when useE2B is enabled.

This keeps ACP in-memory tooling predictable after removing Bun.

Also applies to: 169-197, 326-341, 442-444

src/renderer/src/i18n/zh-HK/settings.json (1)

887-907: ACP initialization/terminal i18n entries look consistent

The new acp.initialize* strings and acp.terminal block (including status and {code} placeholder) match the en‑US structure and appear linguistically consistent with the rest of the zh‑HK file. No issues from a code/i18n‑structure standpoint.

src/renderer/src/i18n/en-US/settings.json (1)

827-831: ACP init and terminal translations align with new flow

The added acp.initialize* strings and acp.terminal object (including status and {code} placeholders) are coherent, match the new PTY-based initialization flow, and are structurally consistent with other locales.

No structural or interpolation issues spotted.

Also applies to: 866-881

src/renderer/src/i18n/zh-CN/settings.json (1)

827-831: ACP 初始化与终端文案结构合理,键名与用法一致

initialize*terminal.* 键与设置页的使用位置(初始化按钮、终端状态)对得上,状态子键 idle/running/completed/error 也与 AcpTerminalDialog 中的逻辑完全一致,便于多语言保持同步。

Also applies to: 865-881

src/renderer/settings/components/AcpTerminalDialog.vue (1)

330-385: Terminal styling and accessibility look solid

The scoped styles correctly stretch xterm to the container, customize scrollbars, and include a proper .sr-only class for screen readers. This should give a predictable terminal surface in the dialog across themes.

src/renderer/settings/components/AcpSettings.vue (2)

128-139: Initialize buttons are wired correctly to per‑agent state

The new “Initialize” buttons for built‑in and custom agents correctly:

  • Disable per‑agent while isInitializing(agent.id, isBuiltin) is true.
  • Toggle the label between initializing and initialize using the new i18n keys.
  • Call into the shared handleInitializeAgent helper, keeping behavior consistent across agent types.

This is a clean integration point with the new terminal flow.

Also applies to: 215-226


682-709: Initialize flow sequencing with terminal dialog is reasonable

handleInitializeAgent:

  • Marks the agent as initializing.
  • Opens the terminal dialog before starting backend work.
  • Waits 600ms to let the dialog/terminal/IPC listeners settle, then calls configPresenter.initializeAcpAgent.
  • On error, logs, shows a localized initializeFailed toast, and closes the dialog.

This matches the comments about backend start delays and ensures the renderer is listening before streaming begins. The finally block correctly clears the per‑agent initializing flag regardless of outcome.

src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (1)

668-680: Stdin EPIPE handler is a solid robustness improvement

The added handler on child.stdin to swallow EPIPE while still logging other errors is appropriate for this stdio bridge:

child.stdin.on('error', (error: NodeJS.ErrnoException) => {
  if (error.code !== 'EPIPE') {
    console.error('[ACP] write error:', error)
  }
})

This should prevent noisy/unhandled errors when the agent process exits while higher layers still hold a reference to the writable stream, without masking real I/O failures.

No changes needed here.

Comment on lines +276 to +305
watch(
() => props.open,
async (open) => {
if (open) {
// Wait for dialog animation to complete before initializing
await nextTick()
// Give dialog time to render and container to get dimensions
setTimeout(() => {
try {
console.log('[AcpTerminal] Starting terminal initialization sequence')
// Initialize terminal first
ensureTerminal()
console.log('[AcpTerminal] Terminal initialized, setting up IPC listeners')
// Then set up listeners
setupIpcListeners()
status.value = 'idle'
isRunning.value = false
console.log('[AcpTerminal] Terminal ready for output')
} catch (error) {
console.error('[AcpTerminal] Failed to initialize terminal:', error)
status.value = 'error'
}
}, 150)
} else {
console.log('[AcpTerminal] Dialog closing, cleaning up')
removeIpcListeners()
cleanupTerminal()
}
}
)
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

Avoid race between delayed init setTimeout and dialog close

Both the watch on props.open and onMounted use a setTimeout(..., 150) after nextTick() to initialize the terminal and IPC listeners. If the dialog is closed before that timeout fires (e.g., user opens and closes very quickly), the delayed callback still runs and re‑creates the terminal + listeners even though open is already false. Cleanup only happens on the next open -> false transition or unmount.

To avoid initializing after close and to keep listener counts tight, consider:

  • Storing the timeout handle(s) in a let initTimer: ReturnType<typeof setTimeout> | null and clearing it in the !open branch and in onBeforeUnmount.
  • Adding a guard inside the timer:
setTimeout(() => {
  if (!props.open) return
  // ... existing init logic
}, 150)

This keeps behavior the same under normal use but avoids unnecessary terminal/IPC setup when the dialog is already closed.

Also applies to: 307-322

🤖 Prompt for AI Agents
In src/renderer/settings/components/AcpTerminalDialog.vue around lines 276-305
(and likewise apply same fix to lines ~307-322), avoid the race from the delayed
setTimeout by declaring a module-scoped let initTimer: ReturnType<typeof
setTimeout> | null = null, assign the timer handle to initTimer when calling
setTimeout, and inside the timeout callback early-return if !props.open before
running initialization; additionally clearTimeout(initTimer) and set initTimer =
null in the else branch when open is false and in onBeforeUnmount to ensure
pending timers are cancelled and no terminal/IPCs are created after the dialog
has closed.

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: 3

🧹 Nitpick comments (8)
src/main/presenter/llmProviderPresenter/agent/shellEnvHelper.ts (2)

51-77: Shell init coverage for zsh/other shells is minimal (optional enhancement).

For zsh and other shells you only source ~/.zshrc or fall back to plain env. Many users have PATH/NVM setup in ~/.zprofile/~/.profile (especially on macOS login shells), so ACP might still miss some PATH customizations in those setups. Consider optionally sourcing ~/.zprofile (and maybe ~/.profile) for zsh and adding a fish-specific branch (config.fish) if you see reports from those users.


160-239: Env filtering is conservative; confirm it matches ACP needs.

getShellEnvironment only preserves PATH/Path and a small Node/npm-related subset, dropping proxies, UV, and other app-specific env. That’s great for safety but may surprise users who rely on additional env (e.g., HTTP(S)_PROXY for CLIs). If ACP agents ever need more, consider expanding the allowlist or making it configurable.

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

26-52: IPC handlers are fine but assume a single ConfigPresenter instance.

Wiring acp-terminal:input/acp-terminal:kill directly in the constructor is reasonable, but if ConfigPresenter is ever constructed more than once (tests, HMR, multiple windows), you’ll accumulate duplicate ipcMain.on handlers. If that’s a possibility, consider guarding with a static flag or exposing a dispose method that removes the listeners.

Also applies to: 189-192, 237-244


1022-1078: ACP agent initialization flow is reasonable; consider exposing the PTY handle later.

The builtin/custom agent resolution, registry lookup, and webContents wiring into initializeBuiltinAgent / initializeCustomAgent look coherent. Right now you ignore the returned IPty | null; if future features need to inspect or manage the underlying PTY from configPresenter (e.g., to show status), you may want to surface that value or some status back to the caller, but it’s not required for this fix.

src/main/presenter/mcpPresenter/mcpClient.ts (1)

25-26: RuntimeHelper integration looks correct; consider deduplicating env-building logic with ACP.

Using RuntimeHelper for runtime discovery, path expansion, PATH normalization, and node/uv command replacement is a solid improvement and reads clearly. The PATH/env shaping for stdio transports, however, is now very similar to what AcpProcessManager does, but with slightly different allowlists and behavior. To avoid future drift and hard‑to‑debug differences between MCP and ACP runtimes, consider factoring out a shared helper for “build env + PATH for (node/uv vs generic) commands” that both classes can call.

Also applies to: 124-125, 176-214, 215-266, 286-314

src/main/presenter/configPresenter/acpInitHelper.ts (2)

197-204: Duplicate environment variable merging.

Line 203 spreads process.env before envVars, but buildEnvironmentVariables already includes process.env (lines 338-342). This causes environment variables to be included twice, which is redundant but shouldn't cause issues since the second merge overwrites with the same values.

Simplify by using only envVars:

     const pty = spawn(shell, shellArgs, {
       name: 'xterm-color',
       cols: 80,
       rows: 24,
       cwd: process.cwd(),
-      env: { ...process.env, ...envVars } as Record<string, string>
+      env: envVars as Record<string, string>
     })

319-431: Consider normalizing PATH case variants in profile env.

The method correctly merges environment variables with appropriate precedence: profile env → runtime paths → system env. PATH merging (lines 406-411) handles multiple case variants (PATH, Path, path), but if a profile specifies multiple variants, they'll all be merged without warning.

Consider logging a warning if multiple PATH case variants are detected in profile.env:

const pathKeys = Object.keys(profile.env).filter(k => ['PATH', 'Path', 'path'].includes(k))
if (pathKeys.length > 1) {
  console.warn('[ACP Init] Multiple PATH variants detected in profile:', pathKeys)
}

Otherwise, the environment building logic is sound.

src/main/lib/runtimeHelper.ts (1)

262-282: Consider expanding lowercase environment variables.

Line 277 uses regex /\$([A-Z_][A-Z0-9_]*)/g which only matches uppercase environment variable names like $PATH but not lowercase variants like $path or $home. Unix systems can have lowercase environment variables.

Consider allowing any case:

-    expandedPath = expandedPath.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, varName) => {
+    expandedPath = expandedPath.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, varName) => {
       return process.env[varName] || match
     })

Or rely solely on the ${VAR} syntax (lines 272-274) for explicit variable expansion if case-insensitivity is a concern.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 857a6a8 and e745e7e.

📒 Files selected for processing (7)
  • src/main/lib/runtimeHelper.ts (1 hunks)
  • src/main/presenter/configPresenter/acpInitHelper.ts (1 hunks)
  • src/main/presenter/configPresenter/index.ts (6 hunks)
  • src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (11 hunks)
  • src/main/presenter/llmProviderPresenter/agent/shellEnvHelper.ts (1 hunks)
  • src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts (6 hunks)
  • src/main/presenter/mcpPresenter/mcpClient.ts (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts (1)
src/main/lib/runtimeHelper.ts (1)
  • RuntimeHelper (9-322)
src/main/presenter/mcpPresenter/mcpClient.ts (1)
src/main/lib/runtimeHelper.ts (1)
  • RuntimeHelper (9-322)
src/main/presenter/configPresenter/acpInitHelper.ts (2)
src/shared/types/presenters/legacy.presenters.d.ts (3)
  • AcpBuiltinAgentId (678-678)
  • AcpAgentProfile (680-686)
  • AcpAgentConfig (712-718)
src/main/lib/runtimeHelper.ts (1)
  • RuntimeHelper (9-322)
src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (2)
src/main/lib/runtimeHelper.ts (1)
  • RuntimeHelper (9-322)
src/main/presenter/llmProviderPresenter/agent/shellEnvHelper.ts (1)
  • getShellEnvironment (160-240)
src/main/lib/runtimeHelper.ts (1)
test/mocks/electron.ts (1)
  • app (2-10)
src/main/presenter/configPresenter/index.ts (5)
test/mocks/electron.ts (1)
  • ipcMain (12-16)
src/main/presenter/configPresenter/acpInitHelper.ts (8)
  • writeToTerminal (119-133)
  • writeToTerminal (478-480)
  • killTerminal (135-150)
  • killTerminal (482-484)
  • initializeBuiltinAgent (43-81)
  • initializeBuiltinAgent (444-460)
  • initializeCustomAgent (86-117)
  • initializeCustomAgent (462-476)
src/main/presenter/llmProviderPresenter/agent/shellEnvHelper.ts (1)
  • clearShellEnvironmentCache (246-249)
src/main/presenter/index.ts (1)
  • presenter (223-223)
src/shared/types/presenters/legacy.presenters.d.ts (1)
  • AcpBuiltinAgentId (678-678)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-check (x64)
🔇 Additional comments (15)
src/main/presenter/configPresenter/index.ts (1)

971-976: Clearing shell env cache on runtime toggle is a good safeguard.

Invalidating the shell env cache when useBuiltinRuntime changes is the right move so subsequent ACP spawns see a fresh PATH (NVM/etc.) after switching back to system runtimes.

src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts (1)

14-15: Node-only runtime wiring for powerpack tools is consistent and safe.

Centralizing on RuntimeHelper and removing Bun support keeps runtime handling uniform with the rest of the app. Conditional exposure of run_node_code based on getNodeRuntimePath() plus the runtime check in executeJavaScriptCode avoids broken tools when the packaged Node runtime is missing. The timeout and temp-file cleanup behavior remain appropriate.

Also applies to: 79-87, 141-170, 299-315, 416-418

src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (2)

510-518: Ignoring stdin EPIPE errors is a good robustness improvement.

Adding an error handler on child.stdin and swallowing EPIPE avoids noisy “write after close” errors when agents exit while the protocol layer still has pending writes. Logging other error codes is also appropriate.


16-18: Verify how builtin agent profiles are used after initialization — bare CLI names bypass getShellEnvironment when spawned.

The original concern has merit: BUILTIN_INIT_COMMANDS does include bare CLI names (claude for claude-code-acp, codex for codex-acp) that won't match isNodeCommand and would skip the getShellEnvironment() merge.

However, I found that:

  • During initialization, these commands run within an interactive shell (startInteractiveSession), so they receive the full shell environment.
  • During spawning, if these agents are later stored as AcpAgentConfig entries and spawned via spawnAgentProcess, they would bypass the getShellEnvironment() logic since they don't contain "node"/"npm"/"npx"/"uv"/"uvx" substrings.

The gap depends on whether builtin agent profiles persist after initialization and are later spawned as separate processes (via spawnAgentProcess). The codebase suggests a separation between initialization (shell-based, full environment) and agent spawning (process-based, filtered environment), but I cannot conclusively determine the flow after initialization.

Recommendation: Confirm whether builtin agent profiles like claude-code-acp and codex-acp are re-spawned via spawnAgentProcess after initialization, or if they persist in the shell session. If re-spawned, broaden isNodeCommand or apply getShellEnvironment() to all commands when useBuiltinRuntime === false.

src/main/presenter/configPresenter/acpInitHelper.ts (6)

1-11: LGTM!

The imports and type definitions are appropriate for PTY-based terminal management and agent initialization.


36-38: LGTM!

Constructor properly initializes the runtime helper, and since initializeRuntimes() is idempotent, calling it here is safe.


43-81: LGTM!

The method properly validates the agent ID, builds the environment, and delegates to the interactive session handler. Error handling and logging are appropriate.


86-117: Consider runtime command replacement for custom agents.

Custom agent commands are not processed with RuntimeHelper.replaceWithRuntimeCommand, unlike builtin agents whose environment includes runtime paths. If a custom agent specifies node or npm and useBuiltinRuntime is true, those commands won't use the bundled runtimes unless PATH is correctly set.

Since environment PATH is updated in buildEnvironmentVariables (lines 346-382), this should work via PATH lookup. However, verify this is the intended behavior rather than direct command replacement.


119-150: LGTM!

Both utility methods properly handle null checks, errors, and cleanup. Logging is comprehensive and appropriate.


434-484: LGTM!

The singleton pattern with lazy initialization is correctly implemented, and the exported functions provide a clean API surface.

src/main/lib/runtimeHelper.ts (5)

1-27: LGTM!

The singleton pattern is correctly implemented with a private constructor and proper instance management.


33-81: Verify UV runtime availability when only one executable exists.

Lines 65 and 73 require both uv and uvx executables to exist before setting uvRuntimePath. If only one is present, the path is set to null. Verify this is the intended behavior—some installations might have only uv with uvx as an alias or separate installation.

Also consider logging when in packaged mode but app.asar string is not found in the path (line 40), which could indicate a packaging issue.

Otherwise, the initialization logic is sound.


87-97: LGTM!

Simple getters with appropriate null returns.


247-255: LGTM!

Convenience wrapper that processes both command and arguments. Note the checkExists=false behavior flagged in the previous comment.


289-321: LGTM!

The PATH normalization and default paths methods are appropriate for cross-platform environment handling.

Comment on lines +106 to +238
public replaceWithRuntimeCommand(
command: string,
useBuiltinRuntime: boolean,
checkExists: boolean = true
): string {
// If useBuiltinRuntime is false, return original command
if (!useBuiltinRuntime) {
return command
}

// Get command basename (remove path)
const basename = path.basename(command)

// Handle Node.js related commands (all platforms use same logic)
if (['node', 'npm', 'npx'].includes(basename)) {
if (this.nodeRuntimePath) {
if (process.platform === 'win32') {
if (basename === 'node') {
const nodeExe = path.join(this.nodeRuntimePath, 'node.exe')
if (checkExists) {
if (fs.existsSync(nodeExe)) {
return nodeExe
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodeExe
}
} else if (basename === 'npm') {
// Windows usually has npm as .cmd file
const npmCmd = path.join(this.nodeRuntimePath, 'npm.cmd')
if (checkExists) {
if (fs.existsSync(npmCmd)) {
return npmCmd
}
// Check if npm exists without .cmd extension
const npmPath = path.join(this.nodeRuntimePath, 'npm')
if (fs.existsSync(npmPath)) {
return npmPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default path without checking
if (fs.existsSync(npmCmd)) {
return npmCmd
}
return path.join(this.nodeRuntimePath, 'npm')
}
} else if (basename === 'npx') {
// On Windows, npx is typically a .cmd file
const npxCmd = path.join(this.nodeRuntimePath, 'npx.cmd')
if (checkExists) {
if (fs.existsSync(npxCmd)) {
return npxCmd
}
// Check if npx exists without .cmd extension
const npxPath = path.join(this.nodeRuntimePath, 'npx')
if (fs.existsSync(npxPath)) {
return npxPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default path without checking
if (fs.existsSync(npxCmd)) {
return npxCmd
}
return path.join(this.nodeRuntimePath, 'npx')
}
}
} else {
// Non-Windows platforms
let targetCommand: string
if (basename === 'node') {
targetCommand = 'node'
} else if (basename === 'npm') {
targetCommand = 'npm'
} else if (basename === 'npx') {
targetCommand = 'npx'
} else {
targetCommand = basename
}
const nodePath = path.join(this.nodeRuntimePath, 'bin', targetCommand)
if (checkExists) {
if (fs.existsSync(nodePath)) {
return nodePath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodePath
}
}
}
}

// UV command handling (all platforms)
if (['uv', 'uvx'].includes(basename)) {
if (!this.uvRuntimePath) {
return command
}

// Both uv and uvx use their corresponding commands
const targetCommand = basename === 'uvx' ? 'uvx' : 'uv'

if (process.platform === 'win32') {
const uvPath = path.join(this.uvRuntimePath, `${targetCommand}.exe`)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
} else {
const uvPath = path.join(this.uvRuntimePath, targetCommand)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
}
}

return command
}
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

Inconsistent checkExists behavior for npm/npx on Windows.

When checkExists=false, lines 150-154 (npm) and 171-174 (npx) still call fs.existsSync() to decide between .cmd and non-.cmd versions. This contradicts the parameter name and the comment "return default path without checking."

For consistency, when checkExists=false, return the expected default path without any filesystem checks:

           } else {
-            // For mcpClient: return default path without checking
-            if (fs.existsSync(npmCmd)) {
-              return npmCmd
-            }
-            return path.join(this.nodeRuntimePath, 'npm')
+            // For mcpClient: return default .cmd path without checking
+            return npmCmd
           }

Apply the same fix for npx at lines 171-174.

📝 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
public replaceWithRuntimeCommand(
command: string,
useBuiltinRuntime: boolean,
checkExists: boolean = true
): string {
// If useBuiltinRuntime is false, return original command
if (!useBuiltinRuntime) {
return command
}
// Get command basename (remove path)
const basename = path.basename(command)
// Handle Node.js related commands (all platforms use same logic)
if (['node', 'npm', 'npx'].includes(basename)) {
if (this.nodeRuntimePath) {
if (process.platform === 'win32') {
if (basename === 'node') {
const nodeExe = path.join(this.nodeRuntimePath, 'node.exe')
if (checkExists) {
if (fs.existsSync(nodeExe)) {
return nodeExe
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodeExe
}
} else if (basename === 'npm') {
// Windows usually has npm as .cmd file
const npmCmd = path.join(this.nodeRuntimePath, 'npm.cmd')
if (checkExists) {
if (fs.existsSync(npmCmd)) {
return npmCmd
}
// Check if npm exists without .cmd extension
const npmPath = path.join(this.nodeRuntimePath, 'npm')
if (fs.existsSync(npmPath)) {
return npmPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default path without checking
if (fs.existsSync(npmCmd)) {
return npmCmd
}
return path.join(this.nodeRuntimePath, 'npm')
}
} else if (basename === 'npx') {
// On Windows, npx is typically a .cmd file
const npxCmd = path.join(this.nodeRuntimePath, 'npx.cmd')
if (checkExists) {
if (fs.existsSync(npxCmd)) {
return npxCmd
}
// Check if npx exists without .cmd extension
const npxPath = path.join(this.nodeRuntimePath, 'npx')
if (fs.existsSync(npxPath)) {
return npxPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default path without checking
if (fs.existsSync(npxCmd)) {
return npxCmd
}
return path.join(this.nodeRuntimePath, 'npx')
}
}
} else {
// Non-Windows platforms
let targetCommand: string
if (basename === 'node') {
targetCommand = 'node'
} else if (basename === 'npm') {
targetCommand = 'npm'
} else if (basename === 'npx') {
targetCommand = 'npx'
} else {
targetCommand = basename
}
const nodePath = path.join(this.nodeRuntimePath, 'bin', targetCommand)
if (checkExists) {
if (fs.existsSync(nodePath)) {
return nodePath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodePath
}
}
}
}
// UV command handling (all platforms)
if (['uv', 'uvx'].includes(basename)) {
if (!this.uvRuntimePath) {
return command
}
// Both uv and uvx use their corresponding commands
const targetCommand = basename === 'uvx' ? 'uvx' : 'uv'
if (process.platform === 'win32') {
const uvPath = path.join(this.uvRuntimePath, `${targetCommand}.exe`)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
} else {
const uvPath = path.join(this.uvRuntimePath, targetCommand)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
}
}
return command
}
public replaceWithRuntimeCommand(
command: string,
useBuiltinRuntime: boolean,
checkExists: boolean = true
): string {
// If useBuiltinRuntime is false, return original command
if (!useBuiltinRuntime) {
return command
}
// Get command basename (remove path)
const basename = path.basename(command)
// Handle Node.js related commands (all platforms use same logic)
if (['node', 'npm', 'npx'].includes(basename)) {
if (this.nodeRuntimePath) {
if (process.platform === 'win32') {
if (basename === 'node') {
const nodeExe = path.join(this.nodeRuntimePath, 'node.exe')
if (checkExists) {
if (fs.existsSync(nodeExe)) {
return nodeExe
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodeExe
}
} else if (basename === 'npm') {
// Windows usually has npm as .cmd file
const npmCmd = path.join(this.nodeRuntimePath, 'npm.cmd')
if (checkExists) {
if (fs.existsSync(npmCmd)) {
return npmCmd
}
// Check if npm exists without .cmd extension
const npmPath = path.join(this.nodeRuntimePath, 'npm')
if (fs.existsSync(npmPath)) {
return npmPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default .cmd path without checking
return npmCmd
}
} else if (basename === 'npx') {
// On Windows, npx is typically a .cmd file
const npxCmd = path.join(this.nodeRuntimePath, 'npx.cmd')
if (checkExists) {
if (fs.existsSync(npxCmd)) {
return npxCmd
}
// Check if npx exists without .cmd extension
const npxPath = path.join(this.nodeRuntimePath, 'npx')
if (fs.existsSync(npxPath)) {
return npxPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
// For mcpClient: return default .cmd path without checking
return npxCmd
}
}
} else {
// Non-Windows platforms
let targetCommand: string
if (basename === 'node') {
targetCommand = 'node'
} else if (basename === 'npm') {
targetCommand = 'npm'
} else if (basename === 'npx') {
targetCommand = 'npx'
} else {
targetCommand = basename
}
const nodePath = path.join(this.nodeRuntimePath, 'bin', targetCommand)
if (checkExists) {
if (fs.existsSync(nodePath)) {
return nodePath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return nodePath
}
}
}
}
// UV command handling (all platforms)
if (['uv', 'uvx'].includes(basename)) {
if (!this.uvRuntimePath) {
return command
}
// Both uv and uvx use their corresponding commands
const targetCommand = basename === 'uvx' ? 'uvx' : 'uv'
if (process.platform === 'win32') {
const uvPath = path.join(this.uvRuntimePath, `${targetCommand}.exe`)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
} else {
const uvPath = path.join(this.uvRuntimePath, targetCommand)
if (checkExists) {
if (fs.existsSync(uvPath)) {
return uvPath
}
// If doesn't exist, return original command to let system find it via PATH
return command
} else {
return uvPath
}
}
}
return command
}
🤖 Prompt for AI Agents
src/main/lib/runtimeHelper.ts lines 106-238: the Windows branches for npm (lines
~150-154) and npx (lines ~171-174) still call fs.existsSync() when checkExists
is false, contradicting the parameter and comment; change those branches to stop
checking the filesystem and unconditionally return the default .cmd path
(npm.cmd / npx.cmd) when checkExists is false, mirroring the node and UV
handling and ensuring no filesystem checks occur in the non-checking code path.

Comment on lines +13 to +30
const BUILTIN_INIT_COMMANDS: Record<AcpBuiltinAgentId, InitCommandConfig> = {
'kimi-cli': {
commands: ['uv tool run --from kimi-cli kimi'],
description: 'Initialize Kimi CLI'
},
'claude-code-acp': {
commands: [
'npm i -g @zed-industries/claude-code-acp',
'npm install -g @anthropic-ai/claude-code',
'claude'
],
description: 'Initialize Claude Code ACP'
},
'codex-acp': {
commands: ['npm i -g @zed-industries/codex-acp', 'npm install -g @openai/codex-sdk', 'codex'],
description: 'Initialize Codex CLI ACP'
}
}
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 | 🟠 Major

🧩 Analysis chain

Verify command chaining behavior on Windows.

The commands defined here will be joined with ; on Windows (line 250, 299), which continues execution even if a command fails. For installation sequences, you likely want && to stop on first failure. Modern PowerShell supports &&.

Verify that continuing on failure is the intended behavior, or consider using && on all platforms:


🏁 Script executed:

#!/bin/bash
# Check PowerShell && support
pwsh -Command "Write-Host 'Testing' && Write-Host 'Chaining'"

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Find the command joining logic around lines 250 and 299
cat src/main/presenter/configPresenter/acpInitHelper.ts | head -300 | tail -100

Length of output: 3602


🏁 Script executed:

#!/bin/bash
# Search for platform-specific command joining logic
rg -n "join|separator|command" src/main/presenter/configPresenter/acpInitHelper.ts -C 3

Length of output: 6543


🏁 Script executed:

#!/bin/bash
# Look for Windows-specific handling or shell detection
rg -n "platform|windows|win32|shell|separator" src/main/presenter/configPresenter/acpInitHelper.ts -C 2

Length of output: 5848



---

🌐 Web query:

```web
PowerShell 5.1 vs 7+ && operator command chaining support

💡 Result:

Short answer:

  • PowerShell 5.1 (Windows PowerShell) does NOT support the shell-style pipeline-chaining operators && and ||. You had to use ; to separate statements, or test the automatic variable ($? or $LASTEXITCODE) / if blocks or use -and/-or (which behave differently). [2]

  • PowerShell 7+ (starting with 7.0) introduced pipeline chain operators && and ||. && runs the right-hand command only if the left-hand command succeeded; || runs the right-hand command only if the left-hand command failed (they use $? / $LASTEXITCODE to determine success). [1][3]

Example:

  • Before (5.1):
    Get-Process -Id $PID
    if ($?) { 'Second command' } — or use ; to run unconditionally. [2]

  • PowerShell 7+:
    Get-Process -Id $PID && 'Second command' # runs second only if first succeeded. [1][3]

Sources: [1] PowerShell 7 announcement/coverage; [2] StackOverflow discussion on token '&&' not valid in older PowerShell; [3] PowerShell 7 “what’s new” summary showing pipeline chain operators.


🌐 Web query:

Windows PowerShell command chaining error handling best practices

💡 Result:

Key best-practice patterns for chaining commands and handling errors in PowerShell:

  • Prefer structured Try/Catch/Finally for any critical sequence; convert non‑terminating errors to terminating so Catch runs (use -ErrorAction Stop or set $ErrorActionPreference where appropriate). [1][5]
  • Remember pipelines produce many non‑terminating errors by default—use -ErrorAction Stop on the cmdlet that can fail or capture errors with -ErrorVariable and test that variable after the pipeline. [6][5]
  • When writing advanced functions/cmdlets use $PSCmdlet.ThrowTerminatingError() or create ErrorRecord/throw for consistent, cmdlet‑style terminating errors (reserve ThrowTerminatingError for serious failures). [3][2]
  • Don’t rely on global flag variables; wrap the whole logical transaction in try/catch and handle cleanup in finally. Keep chains readable by splitting complex logic into functions. [7][2]
  • Preserve the original error object at the start of Catch (e.g. $err = $_) before running other commands; log full details (Exception.Message, ScriptStackTrace) and rethrow if appropriate. [7][1]
  • Use explicit input validation (parameter Validate* attributes), meaningful error messages, and consistent logging/telemetry for post‑mortem. Avoid SilentlyContinue/Ignore except for documented, low‑risk cases. [3][5]

If you want, I can give concise code snippets for each pattern (Try/Catch with pipelines, -ErrorVariable use, ThrowTerminatingError example).


🌐 Web query:

PowerShell version installed by default Windows 10 11

💡 Result:

Windows 10 and Windows 11 include Windows PowerShell 5.1 by default (PowerShell 7+ / "PowerShell (core)" must be installed separately). [1][2][3]

Sources:

  • Microsoft support / PowerShell docs (WMF 5.1 / PowerShell 5.1 shipped with Windows 10/11). [1]
  • PowerShell team blog (WMF 5.1 release). [2]
  • Summary / version history (PowerShell 5.1 remains pre-installed on Windows 10, Windows 11). [3]

Windows command chaining will continue on npm install failure.

Commands on Windows (PowerShell 5.1, default on Windows 10/11) use ; as separator, which continues execution even if a command fails. The suggested fix of using && won't work—PowerShell 5.1 doesn't support && (only PowerShell 7+ does).

For npm install sequences like npm i -g @anthropic-ai/claude-code; npm install -g @zed-industries/claude-code-acp; claude, if the first npm install fails, the subsequent commands still run, leaving the system in a broken state.

Options:

  • Require PowerShell 7+ (breaking change for Windows users)
  • Use PowerShell error checking: cmd1; if ($?) { cmd2 } (verbose, compatible)
  • Spawn commands separately with error handling instead of joining them
  • Use cmd.exe instead of PowerShell (loses Unix shell compatibility)

Decide on the approach and implement proper error handling for Windows.

Comment on lines +155 to +314
private startInteractiveSession(
initCommands: string[],
envVars: Record<string, string>,
webContents?: WebContents
): IPty | null {
console.log('[ACP Init] Starting interactive session:', {
commands: initCommands,
envVarCount: Object.keys(envVars).length,
hasWebContents: !!webContents
})

if (!webContents || webContents.isDestroyed()) {
console.error('[ACP Init] Cannot start session - webContents invalid or destroyed')
return null
}

// Kill existing shell if any
this.killTerminal()

const platform = process.platform
let shell: string
let shellArgs: string[] = []

if (platform === 'win32') {
shell = 'powershell.exe'
shellArgs = ['-NoLogo']
} else {
// Use user's default shell or bash/zsh
shell = process.env.SHELL || '/bin/bash'
// Force interactive mode for bash/zsh to get prompt and aliases
if (shell.endsWith('bash') || shell.endsWith('zsh')) {
shellArgs = ['-i']
}
}

console.log('[ACP Init] Spawning shell with PTY:', {
platform,
shell,
shellArgs,
cwd: process.cwd()
})

// Spawn PTY process
const pty = spawn(shell, shellArgs, {
name: 'xterm-color',
cols: 80,
rows: 24,
cwd: process.cwd(),
env: { ...process.env, ...envVars } as Record<string, string>
})

console.log('[ACP Init] PTY process spawned:', {
pid: pty.pid
})

this.activeShell = pty

// Track shell readiness for command injection
let shellReady = false
let outputBuffer = ''
let commandInjected = false
const maxWaitTime = 3000 // Maximum wait time for shell ready (3 seconds)
const startTime = Date.now()

// Handle PTY output (PTY combines stdout and stderr into a single stream)
pty.onData((data: string) => {
outputBuffer += data

console.log('[ACP Init] PTY data:', {
length: data.length,
preview: data.substring(0, 100).replace(/\n/g, '\\n'),
shellReady,
commandInjected
})

// Detect shell readiness by looking for prompt patterns or any meaningful output
if (!shellReady && outputBuffer.length > 0) {
// Check for common shell prompt patterns or any non-empty output
const hasPromptPattern =
/[$#>]\s*$/.test(outputBuffer) || outputBuffer.includes('\n') || outputBuffer.length > 10

if (hasPromptPattern || Date.now() - startTime > 500) {
shellReady = true
console.log('[ACP Init] Shell detected as ready, output length:', outputBuffer.length)
}
}

// Send output to renderer (PTY output is treated as stdout)
if (!webContents.isDestroyed()) {
webContents.send('acp-init:output', { type: 'stdout', data })
}

// Inject command once shell is ready
if (shellReady && !commandInjected && initCommands.length > 0) {
commandInjected = true
const separator = platform === 'win32' ? ';' : '&&'
const initCmd = initCommands.join(` ${separator} `)

console.log('[ACP Init] Injecting initialization command (shell ready):', {
command: initCmd,
outputBufferLength: outputBuffer.length
})

// Small delay to ensure shell is fully ready
setTimeout(() => {
try {
pty.write(initCmd + '\n')
console.log('[ACP Init] Command written to PTY')
} catch (error) {
console.warn('[ACP Init] Error writing command to PTY:', error)
}
}, 100)
}
})

// Handle process exit
pty.onExit(({ exitCode, signal }) => {
console.log('[ACP Init] Process exited:', {
pid: pty.pid,
code: exitCode,
signal,
commandInjected
})
if (!webContents.isDestroyed()) {
webContents.send('acp-init:exit', { code: exitCode, signal: signal || null })
}
if (this.activeShell === pty) {
this.activeShell = null
console.log('[ACP Init] Active shell cleared')
}
})

// Delay sending start event to ensure renderer listeners are set up
// Also inject command if shell doesn't become ready within timeout
setTimeout(() => {
if (!webContents.isDestroyed()) {
console.log('[ACP Init] Sending start event (delayed to ensure listeners ready)')
webContents.send('acp-init:start', { command: shell })
}

// Fallback: inject command if shell hasn't become ready yet
if (!commandInjected && initCommands.length > 0 && Date.now() - startTime < maxWaitTime) {
console.log('[ACP Init] Fallback: injecting command after delay (shell may be ready)')
commandInjected = true
const separator = platform === 'win32' ? ';' : '&&'
const initCmd = initCommands.join(` ${separator} `)

setTimeout(() => {
try {
pty.write(initCmd + '\n')
console.log('[ACP Init] Fallback command written to PTY')
} catch (error) {
console.warn('[ACP Init] Error writing fallback command to PTY:', error)
}
}, 200)
}
}, 500) // Delay to ensure renderer listeners are set up

return pty
}
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 | 🟠 Major

Potential race condition in command injection logic.

The shell-ready detection in the data handler (line 236) sets shellReady = true after 500ms as a fallback, then injects the command after a 100ms delay (~600ms total). Meanwhile, the outer timeout (line 289) also runs at 500ms and can inject the command after 200ms (~700ms total).

The commandInjected flag prevents double execution, but there's a race:

  • If data handler sets commandInjected = true at ~500ms, the fallback at ~500ms might still see it as false and also set it to true
  • Both timeouts then write to PTY, causing duplicate command execution

Consider removing the outer fallback logic (lines 296-310) since the data handler already includes a 500ms timeout-based ready detection (line 236). If you need the fallback, check commandInjected inside the nested timeout at line 302, not outside:

-      if (!commandInjected && initCommands.length > 0 && Date.now() - startTime < maxWaitTime) {
-        console.log('[ACP Init] Fallback: injecting command after delay (shell may be ready)')
-        commandInjected = true
-        const separator = platform === 'win32' ? ';' : '&&'
-        const initCmd = initCommands.join(` ${separator} `)
-
-        setTimeout(() => {
+      setTimeout(() => {
+        if (!commandInjected && initCommands.length > 0) {
+          console.log('[ACP Init] Fallback: injecting command after delay (shell may be ready)')
+          commandInjected = true
+          const separator = platform === 'win32' ? ';' : '&&'
+          const initCmd = initCommands.join(` ${separator} `)
+
           try {
             pty.write(initCmd + '\n')
             console.log('[ACP Init] Fallback command written to PTY')
           } catch (error) {
             console.warn('[ACP Init] Error writing fallback command to PTY:', error)
           }
-        }, 200)
-      }
+        }
+      }, 200)
🤖 Prompt for AI Agents
In src/main/presenter/configPresenter/acpInitHelper.ts around lines 155-314,
there's a race between the data-handler injection and the delayed outer fallback
(lines ~289-310) that can cause duplicate command writes; remove the outer
fallback injection block entirely OR modify it so it only schedules the nested
setTimeout if commandInjected is still false at the time the nested timeout
fires (i.e., keep the outer block to send the start event but move the
commandInjected check inside the nested setTimeout callback before calling
pty.write), and ensure all accesses to commandInjected remain single-threaded
and guarded to prevent double writes.

@zerob13 zerob13 merged commit e4a3ff4 into dev Nov 21, 2025
2 checks passed
@zerob13 zerob13 deleted the bugfix/acp-spawn-issue3 branch November 23, 2025 13:52
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.

2 participants