mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-09 19:45:17 +02:00
fix(code-mode): run the ACP adapter as Node under Electron + resolve it from main
Two runtime failures that only surfaced inside the packaged/bundled Electron app (the headless harness used real node, so neither showed there): - "ACP connection closed": the main process spawns the adapter via process.execPath, which inside Electron is the Electron binary, not node — so the child never ran as Node and its ACP stdio stream closed immediately. Set ELECTRON_RUN_AS_NODE=1 on the adapter env (a no-op under real node). - "Cannot find module '@agentclientprotocol/claude-agent-acp'": the adapters were transitive (core) deps, unreachable from the esbuild-bundled main.cjs. Add them as direct deps of the main app so require.resolve finds them at runtime (and so they ship when packaged). Also capture the adapter's stderr + exit code and enrich connection errors, so a future failure reports the real cause instead of the opaque "ACP connection closed".
This commit is contained in:
parent
10ce73ae24
commit
a845eb14a6
4 changed files with 66 additions and 10 deletions
|
|
@ -13,6 +13,8 @@
|
|||
"make": "electron-forge make"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/claude-agent-acp": "^0.39.0",
|
||||
"@agentclientprotocol/codex-acp": "^0.0.44",
|
||||
"@x/core": "workspace:*",
|
||||
"@x/shared": "workspace:*",
|
||||
"chokidar": "^4.0.3",
|
||||
|
|
|
|||
|
|
@ -49,5 +49,12 @@ export function getAgentLaunchSpec(agent: CodingAgent): AgentLaunchSpec {
|
|||
if (exe) env.CLAUDE_CODE_EXECUTABLE = exe;
|
||||
}
|
||||
|
||||
// We spawn the adapter with process.execPath. Inside Electron's main process
|
||||
// that is the Electron binary, NOT node — so set ELECTRON_RUN_AS_NODE=1 to make
|
||||
// it behave as a plain Node runtime. (Harmless under a real node process, which
|
||||
// ignores the var.) Without this the child never runs as node and the ACP stdio
|
||||
// stream closes immediately ("ACP connection closed").
|
||||
env.ELECTRON_RUN_AS_NODE = '1';
|
||||
|
||||
return { command: process.execPath, args: [entry], env };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ export class AcpClient {
|
|||
private child?: ChildProcess;
|
||||
private connection?: ClientSideConnection;
|
||||
private loadSession_ = false;
|
||||
// Diagnostics: the adapter's stderr/exit are captured so a dropped connection
|
||||
// reports WHY (e.g. a crash) instead of the SDK's bare "ACP connection closed".
|
||||
private stderrTail = '';
|
||||
private exitInfo: string | null = null;
|
||||
|
||||
constructor(opts: AcpClientOptions) {
|
||||
this.agent = opts.agent;
|
||||
|
|
@ -104,9 +108,19 @@ export class AcpClient {
|
|||
const child = spawn(spec.command, spec.args, {
|
||||
cwd: this.cwd,
|
||||
env: spec.env,
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
// Capture stderr (not inherit) so we can attribute a dropped connection.
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
this.child = child;
|
||||
child.stderr?.on('data', (d: Buffer) => {
|
||||
this.stderrTail = (this.stderrTail + d.toString()).slice(-4000);
|
||||
});
|
||||
child.on('exit', (code, signal) => {
|
||||
this.exitInfo = `adapter exited (code ${code}${signal ? `, signal ${signal}` : ''})`;
|
||||
});
|
||||
child.on('error', (err) => {
|
||||
this.stderrTail = (this.stderrTail + `\nspawn error: ${err.message}`).slice(-4000);
|
||||
});
|
||||
|
||||
const stream = ndJsonStream(
|
||||
Writable.toWeb(child.stdin!) as WritableStream<Uint8Array>,
|
||||
|
|
@ -115,24 +129,51 @@ export class AcpClient {
|
|||
const client = this.buildClient();
|
||||
this.connection = new ClientSideConnection(() => client, stream);
|
||||
|
||||
const init = await this.connection.initialize({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
|
||||
});
|
||||
this.loadSession_ = init.agentCapabilities?.loadSession === true;
|
||||
try {
|
||||
const init = await this.connection.initialize({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
|
||||
});
|
||||
this.loadSession_ = init.agentCapabilities?.loadSession === true;
|
||||
} catch (e) {
|
||||
throw this.enrich(e, 'initialize');
|
||||
}
|
||||
}
|
||||
|
||||
async newSession(): Promise<string> {
|
||||
const res = await this.conn().newSession({ cwd: this.cwd, mcpServers: [] });
|
||||
return res.sessionId;
|
||||
try {
|
||||
const res = await this.conn().newSession({ cwd: this.cwd, mcpServers: [] });
|
||||
return res.sessionId;
|
||||
} catch (e) {
|
||||
throw this.enrich(e, 'newSession');
|
||||
}
|
||||
}
|
||||
|
||||
async loadSession(sessionId: string): Promise<void> {
|
||||
await this.conn().loadSession({ sessionId, cwd: this.cwd, mcpServers: [] });
|
||||
try {
|
||||
await this.conn().loadSession({ sessionId, cwd: this.cwd, mcpServers: [] });
|
||||
} catch (e) {
|
||||
throw this.enrich(e, 'loadSession');
|
||||
}
|
||||
}
|
||||
|
||||
async prompt(sessionId: string, text: string): Promise<PromptResponse> {
|
||||
return this.conn().prompt({ sessionId, prompt: [{ type: 'text', text }] });
|
||||
try {
|
||||
return await this.conn().prompt({ sessionId, prompt: [{ type: 'text', text }] });
|
||||
} catch (e) {
|
||||
throw this.enrich(e, 'prompt');
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap a connection error with the adapter's exit/stderr so failures are
|
||||
// self-explanatory rather than the SDK's opaque "ACP connection closed".
|
||||
private enrich(err: unknown, phase: string): Error {
|
||||
const base = err instanceof Error ? err.message : String(err);
|
||||
const parts = [
|
||||
this.exitInfo,
|
||||
this.stderrTail.trim() ? `adapter output: ${this.stderrTail.trim().slice(-1200)}` : '',
|
||||
].filter(Boolean);
|
||||
return new Error(parts.length ? `${base} — ${parts.join(' | ')} [during ${phase}]` : `${base} [during ${phase}]`);
|
||||
}
|
||||
|
||||
async cancel(sessionId: string): Promise<void> {
|
||||
|
|
|
|||
6
apps/x/pnpm-lock.yaml
generated
6
apps/x/pnpm-lock.yaml
generated
|
|
@ -47,6 +47,12 @@ importers:
|
|||
|
||||
apps/main:
|
||||
dependencies:
|
||||
'@agentclientprotocol/claude-agent-acp':
|
||||
specifier: ^0.39.0
|
||||
version: 0.39.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1))
|
||||
'@agentclientprotocol/codex-acp':
|
||||
specifier: ^0.0.44
|
||||
version: 0.0.44(zod@4.2.1)
|
||||
'@x/core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue