-
Notifications
You must be signed in to change notification settings - Fork 614
fix: acp process spawn failed on nvm #1112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds 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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2025-10-12T00:58:06.513ZApplied to files:
🔇 Additional comments (5)
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. Comment |
There was a problem hiding this 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 validationThe vulnerability identified is confirmed as real and critical.
CODE_EXECUTION_FORBIDDEN_PATTERNSuses global regex flags, and when.test()is called repeatedly on different code strings, the patterns'lastIndexpersists 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 thegflag 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
lastIndexbefore each.test()call incheckCodeSafety()(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 positivesThe runtime handling refactor is generally solid:
replaceWithRuntimeCommandnow consistently routesnode,npm,npxanduv/uvxthrough the bundled runtimes (with correct.exe/.cmdhandling on Windows andbin/*on Unix).processCommandWithArgsnormalizes both the maincommandand any barenode/npm/npx/uvarguments, 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
.includeson the full path/arg can accidentally classify unrelated binaries as “Node/UV commands” (e.g. a command under/opt/rnode/binor an arg containingsome-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 inreplaceWithRuntimeCommand), 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
createTempScriptuses onlyDate.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
outputBuffergrows 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 outputThis keeps memory bounded while still allowing prompt detection and logging.
334-345: Simplify env construction to avoid double-mergingprocess.env
buildEnvironmentVariablesalready starts fromprocess.envand layers runtime/registry/profile env on top, butspawnis 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: envVarsand, if desired, move the initial
process.envcopy out ofbuildEnvironmentVariablesso its responsibility is clearly “build complete env for PTY”.Also applies to: 200-206
137-152: Single global PTY session – confirm this is intentional
AcpInitHelpertracks a singleactiveShellandstartInteractiveSessionalways callskillTerminal()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/killTerminalcalls 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.idinstead of a single globalactiveShell.Also applies to: 173-176, 506-555
src/shared/types/presenters/legacy.presenters.d.ts (1)
535-535: Implementation verified—typing improvement remains optionalThe
initializeAcpAgent(agentId: string, isBuiltin: boolean)method has a concrete implementation wired correctly:
- Implementation at
src/main/presenter/configPresenter/index.ts:1021validates theagentIdparameter at runtime (throws if the agent is not found in builtins or customs list)- Call site at
src/renderer/settings/components/AcpSettings.vue:699invokes 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 AcpBuiltinAgentIdcast 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 sendingacp-terminal:killon close instead of gating onisRunningRight now the close button only sends
acp-terminal:killwhenisRunningistrue. Given that process startup and theacp-init:startevent are slightly delayed, there’s a small window where a process may already be spawning butisRunningis stillfalse, so closing the dialog won’t request a kill. SincekillTerminal()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
handleOutputalready 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) todebugor behind a flag; high‑volume PTY output will spam the console.- Before logging
data.type, guard againsttypebeing missing/undefined on future payloads, to avoid loggingundefinedor 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
setupIpcListenersandremoveIpcListenersare paired and gated onwindow.electron, which is good. Just be aware that if this component is ever opened/closed very quickly or reused in another context,setupIpcListenerscan be called multiple times beforeremoveIpcListenersruns, leading to duplicate handlers.This is largely mitigated by always calling
removeAllListenersfor 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 agentsThe
initializingmap and helpers (isInitializing/setInitializing) give precise, per‑agent “starting…” state while you open the terminal and callconfigPresenter.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 whileinitializingAnyis true, or at least whileterminalDialogOpenis 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 possibleThe added
initialize*and nestedterminal.*keys mirror the other locales and line up with howAcpSettingsandAcpTerminalDialoguse 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 instanceImporting
ipcMain, callingsetupIpcHandlers()in the constructor, and routing:
'acp-terminal:input'→writeToTerminal'acp-terminal:kill'→killTerminalis a clean way to centralize PTY control in the config presenter. Just make sure
ConfigPresenteris only instantiated once in the main process; otherwise you’ll accumulate duplicateipcMain.onhandlers for these channels.Also applies to: 45-50, 188-191, 236-243
1018-1074:initializeAcpAgentlogic is correct; consider handling missingwebContentsexplicitlyThis method:
- Reads
useBuiltinRuntimefrom ACP settings.- Optionally pulls npm/uv registries from
mcpPresenter.- Locates the settings window’s
webContentsfor 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:
Guard on missing settings window
IfsettingsWindowis missing or destroyed,webContentsisundefined, 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') }Optional: log whether the target agent is enabled
For debugging, loggingagent.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 onlyThe node/npm/npx and uv/uvx remapping against
this.nodeRuntimePath/this.uvRuntimePathis 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
setupRuntimesalready ensured. You could skip thefs.existsSynccalls in this method and trust the precomputednodeRuntimePath/uvRuntimePath, or gate them behind a debug-only assertion.- On non‑Windows, the
targetCommandbranching could be collapsed to justconst targetCommand = basenamewithout changing behavior.These are minor readability/perf nits; behavior looks good as-is.
454-480: TightenisNodeCommanddetection to avoid false positives and unintended env whitelistingThe 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, oruvxas a substring (e.g./opt/tools/node_exporter, a custom script withuvin its name, etc.) will be treated as a Node/UV command.- Any argument containing those substrings will also flip
isNodeCommandto true, even when the actual executable is unrelated (e.g.python script.py --mode=node).Because
isNodeCommanddrives the strict env whitelist logic, this can unintentionally strip most ofprocess.envfor 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/.cmdwrappers) while avoiding accidental matches on unrelated commands or arguments.The rest of the env/PATH shaping logic (whitelisting for Node/UV commands, full
process.envfor others, and gating builtin runtime paths onuseBuiltinRuntime) 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 intendedThe 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/nullas “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 thatnull/omission means “don’t touch env” and empty strings are ignored.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 completeThe new
initialize*andterminalkeys 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 localesAll expected
initialize*andterminalkeys 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 wiredThe added
initialize*andterminalkeys 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 correctAll required initialization and
terminalkeys 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 usableThe new
profileSwitched,initialize*, andterminalblocks 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 schemaInitialization and
terminalkeys 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
setupRuntimesnow only tracks a Node.js runtime and logs clearly when neither Node nor E2B is usable.executeJavaScriptCodeand therun_node_codehandler both guard onthis.nodeRuntimePath, whileListToolsRequestonly exposesrun_node_codewhen that path exists, so the UI can’t offer a non-functional tool.- E2B execution is cleanly separated via
run_codewhenuseE2Bis 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 consistentThe new
acp.initialize*strings andacp.terminalblock (includingstatusand{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 flowThe added
acp.initialize*strings andacp.terminalobject (includingstatusand{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 solidThe scoped styles correctly stretch xterm to the container, customize scrollbars, and include a proper
.sr-onlyclass 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 stateThe new “Initialize” buttons for built‑in and custom agents correctly:
- Disable per‑agent while
isInitializing(agent.id, isBuiltin)is true.- Toggle the label between
initializingandinitializeusing the new i18n keys.- Call into the shared
handleInitializeAgenthelper, 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
initializeFailedtoast, and closes the dialog.This matches the comments about backend start delays and ensures the renderer is listening before streaming begins. The
finallyblock 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 improvementThe added handler on
child.stdinto 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.
| 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() | ||
| } | ||
| } | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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> | nulland clearing it in the!openbranch and inonBeforeUnmount. - 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.
There was a problem hiding this 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
~/.zshrcor fall back to plainenv. 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.
getShellEnvironmentonly 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:killdirectly in the constructor is reasonable, but if ConfigPresenter is ever constructed more than once (tests, HMR, multiple windows), you’ll accumulate duplicateipcMain.onhandlers. 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
webContentswiring intoinitializeBuiltinAgent/initializeCustomAgentlook coherent. Right now you ignore the returnedIPty | 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
RuntimeHelperfor 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 whatAcpProcessManagerdoes, 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.envbeforeenvVars, butbuildEnvironmentVariablesalready includesprocess.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_]*)/gwhich only matches uppercase environment variable names like$PATHbut not lowercase variants like$pathor$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
📒 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
useBuiltinRuntimechanges 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
RuntimeHelperand removing Bun support keeps runtime handling uniform with the rest of the app. Conditional exposure ofrun_node_codebased ongetNodeRuntimePath()plus the runtime check inexecuteJavaScriptCodeavoids 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
errorhandler onchild.stdinand swallowingEPIPEavoids 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 (
claudefor claude-code-acp,codexfor codex-acp) that won't matchisNodeCommandand would skip thegetShellEnvironment()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
AcpAgentConfigentries and spawned viaspawnAgentProcess, they would bypass thegetShellEnvironment()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-acpandcodex-acpare re-spawned viaspawnAgentProcessafter initialization, or if they persist in the shell session. If re-spawned, broadenisNodeCommandor applygetShellEnvironment()to all commands whenuseBuiltinRuntime === 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 specifiesnodeornpmanduseBuiltinRuntimeis 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
uvanduvxexecutables to exist before settinguvRuntimePath. If only one is present, the path is set tonull. Verify this is the intended behavior—some installations might have onlyuvwithuvxas an alias or separate installation.Also consider logging when in packaged mode but
app.asarstring 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=falsebehavior flagged in the previous comment.
289-321: LGTM!The PATH normalization and default paths methods are appropriate for cross-platform environment handling.
| 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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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' | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -100Length 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 3Length 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 2Length 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.exeinstead of PowerShell (loses Unix shell compatibility)
Decide on the approach and implement proper error handling for Windows.
| 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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 = trueat ~500ms, the fallback at ~500ms might still see it asfalseand also set it totrue - 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.
Summary by CodeRabbit
New Features
Internationalization
Chores / Refactor
Dependencies
Version
✏️ Tip: You can customize this high-level summary in your review settings.