Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates).
108 lines
3 KiB
TypeScript
108 lines
3 KiB
TypeScript
/**
|
|
* Shared OpenCode client manager.
|
|
* Reuses an existing server on port 4096; starts one on a random port as fallback.
|
|
* Tracks spawned servers so they can be cleaned up on process exit.
|
|
*/
|
|
import { execSync } from 'node:child_process';
|
|
import { existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
|
|
const activeServers = new Set<{ close(): void }>();
|
|
|
|
// Clean up spawned OpenCode servers on process exit
|
|
function cleanup() {
|
|
for (const server of activeServers) {
|
|
try {
|
|
server.close();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
activeServers.clear();
|
|
}
|
|
|
|
process.on('beforeExit', cleanup);
|
|
process.on('SIGTERM', cleanup);
|
|
process.on('SIGINT', cleanup);
|
|
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
/** Cached resolved binary path */
|
|
let _resolvedBinary: string | undefined | null = null;
|
|
|
|
/** Resolve the opencode binary, with caching. */
|
|
function resolveOpencodeBinary(): string | undefined {
|
|
if (_resolvedBinary !== null) return _resolvedBinary ?? undefined;
|
|
|
|
// PATH lookup
|
|
try {
|
|
const cmd = isWindows ? 'where opencode 2>nul' : 'which opencode 2>/dev/null';
|
|
const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 })
|
|
.trim()
|
|
.split(/\r?\n/)[0]
|
|
?.trim();
|
|
if (result && existsSync(result)) {
|
|
_resolvedBinary = result;
|
|
return result;
|
|
}
|
|
} catch {
|
|
/* not on PATH */
|
|
}
|
|
|
|
// Common install locations
|
|
const home = homedir();
|
|
const candidates = isWindows
|
|
? [
|
|
join(process.env.APPDATA || '', 'npm', 'opencode.cmd'),
|
|
join(process.env.NVM_SYMLINK || '', 'opencode.cmd'),
|
|
join(process.env.FNM_MULTISHELL_PATH || '', 'opencode.cmd'),
|
|
join(home, 'scoop', 'shims', 'opencode.exe'),
|
|
]
|
|
: [
|
|
join(home, '.opencode', 'bin', 'opencode'),
|
|
join(home, '.npm-global', 'bin', 'opencode'),
|
|
'/usr/local/bin/opencode',
|
|
'/opt/homebrew/bin/opencode',
|
|
join(home, '.local', 'bin', 'opencode'),
|
|
];
|
|
|
|
for (const c of candidates) {
|
|
if (c && existsSync(c)) {
|
|
_resolvedBinary = c;
|
|
return c;
|
|
}
|
|
}
|
|
|
|
_resolvedBinary = undefined;
|
|
return undefined;
|
|
}
|
|
|
|
export async function getOpencodeClient(binaryPath?: string) {
|
|
const { createOpencodeClient, createOpencode } = await import('../opencode/index');
|
|
|
|
// Try connecting to an existing server first
|
|
try {
|
|
const client = createOpencodeClient();
|
|
await client.config.providers(); // probe
|
|
return { client, server: undefined };
|
|
} catch {
|
|
// No running server — start a temporary one on a random port
|
|
const resolvedPath = binaryPath ?? resolveOpencodeBinary();
|
|
const timeout = isWindows ? 15_000 : 5000;
|
|
const oc = await createOpencode({ port: 0, binaryPath: resolvedPath, timeout });
|
|
activeServers.add(oc.server);
|
|
return { client: oc.client, server: oc.server };
|
|
}
|
|
}
|
|
|
|
export function releaseOpencodeServer(server: { close(): void } | undefined) {
|
|
if (!server) return;
|
|
try {
|
|
server.close();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
activeServers.delete(server);
|
|
}
|