Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
156 lines
4.5 KiB
TypeScript
156 lines
4.5 KiB
TypeScript
/**
|
|
* Short-lived ACP probe — opens a session and reads models/modes from the response.
|
|
*/
|
|
import { spawn } from 'node:child_process';
|
|
import {
|
|
ClientSideConnection,
|
|
type Client,
|
|
type NewSessionResponse,
|
|
type ReadTextFileRequest,
|
|
type ReadTextFileResponse,
|
|
type WriteTextFileRequest,
|
|
type WriteTextFileResponse,
|
|
type CreateTerminalRequest,
|
|
type CreateTerminalResponse,
|
|
type RequestPermissionRequest,
|
|
type RequestPermissionResponse,
|
|
} from '@agentclientprotocol/sdk';
|
|
import { deriveModesFromACP, deriveModelDefinitionsFromACP } from './acp-derive.js';
|
|
import { getManifestDefaultModeId, getManifestModes } from './provider-manifest.js';
|
|
import { resolveAcpSpawnArgs } from './acp-spawn.js';
|
|
import { createAcpNdJsonStream } from './acp-stream.js';
|
|
import type { ProviderModel, ProviderMode } from './provider-types.js';
|
|
import type { AgentCommand } from './agent-commands-cache.js';
|
|
|
|
const PROBE_TIMEOUT_MS = 30_000;
|
|
|
|
export interface AcpProbeResult {
|
|
ok: boolean;
|
|
models: ProviderModel[];
|
|
modes: ProviderMode[];
|
|
defaultModeId: string | null;
|
|
commands: AgentCommand[];
|
|
error?: string;
|
|
}
|
|
|
|
function parseSessionResponse(session: NewSessionResponse, agent: string): AcpProbeResult {
|
|
const fallbackModes = getManifestModes(agent);
|
|
const { modes, currentModeId } = deriveModesFromACP(
|
|
fallbackModes,
|
|
session.modes,
|
|
session.configOptions,
|
|
);
|
|
const models = deriveModelDefinitionsFromACP(session.models, session.configOptions);
|
|
|
|
return {
|
|
ok: true,
|
|
models,
|
|
modes,
|
|
defaultModeId: currentModeId ?? getManifestDefaultModeId(agent),
|
|
commands: [],
|
|
};
|
|
}
|
|
|
|
export async function probeAcpProvider(
|
|
agent: string,
|
|
installPath: string,
|
|
cwd: string,
|
|
): Promise<AcpProbeResult> {
|
|
const args = resolveAcpSpawnArgs(agent);
|
|
if (!args) {
|
|
return {
|
|
ok: false,
|
|
models: [],
|
|
modes: getManifestModes(agent),
|
|
defaultModeId: getManifestDefaultModeId(agent),
|
|
commands: [],
|
|
error: 'no ACP spawn args',
|
|
};
|
|
}
|
|
|
|
const child = spawn(installPath, args, {
|
|
cwd,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: { ...process.env },
|
|
});
|
|
|
|
let killed = false;
|
|
const kill = () => {
|
|
if (!killed) {
|
|
killed = true;
|
|
child.kill('SIGTERM');
|
|
setTimeout(() => child.kill('SIGKILL'), 2_000);
|
|
}
|
|
};
|
|
|
|
const timeout = setTimeout(kill, PROBE_TIMEOUT_MS);
|
|
|
|
const probedCommands: AgentCommand[] = [];
|
|
|
|
try {
|
|
const stream = createAcpNdJsonStream(child);
|
|
|
|
const connection = new ClientSideConnection(
|
|
(_agentInterface): Client => ({
|
|
async sessionUpdate(params) {
|
|
const update = params.update;
|
|
if (update.sessionUpdate === 'available_commands_update') {
|
|
for (const cmd of update.availableCommands) {
|
|
probedCommands.push({
|
|
name: cmd.name,
|
|
description: cmd.description ?? undefined,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
|
const first = params.options[0];
|
|
if (first) {
|
|
return { outcome: { outcome: 'selected', optionId: first.optionId } };
|
|
}
|
|
return { outcome: { outcome: 'cancelled' } };
|
|
},
|
|
async readTextFile(_params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
|
|
return { content: '' };
|
|
},
|
|
async writeTextFile(_params: WriteTextFileRequest): Promise<WriteTextFileResponse> {
|
|
return {};
|
|
},
|
|
async createTerminal(_params: CreateTerminalRequest): Promise<CreateTerminalResponse> {
|
|
return { terminalId: 'noop' };
|
|
},
|
|
}),
|
|
stream,
|
|
);
|
|
|
|
await connection.initialize({
|
|
protocolVersion: 1,
|
|
clientInfo: { name: 'boocoder-probe', version: '2.2.0' },
|
|
clientCapabilities: {},
|
|
});
|
|
|
|
const session = await connection.newSession({ cwd, mcpServers: [] });
|
|
const result = parseSessionResponse(session, agent);
|
|
result.commands = probedCommands;
|
|
await connection.closeSession({ sessionId: session.sessionId }).catch(() => {});
|
|
return result;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return {
|
|
ok: false,
|
|
models: [],
|
|
modes: getManifestModes(agent),
|
|
defaultModeId: getManifestDefaultModeId(agent),
|
|
commands: probedCommands,
|
|
error: message,
|
|
};
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
kill();
|
|
await new Promise<void>((resolve) => {
|
|
child.on('close', resolve);
|
|
setTimeout(resolve, 2_000);
|
|
});
|
|
}
|
|
}
|