v2.1.0-provider-picker: BooCoder systemd migration + provider picker

- BooCoder moves from Docker to host systemd service (boocoder.service)
- Agent dispatch (ACP + PTY) switches from SSH to direct spawn/exec
- SSH helpers marked @deprecated (kept for one release cycle)
- Provider registry (5 providers: boocode, opencode, goose, claude, qwen)
- Agent probe with direct which/exec + model discovery (qwen settings, static claude models)
- GET /api/providers route with installed status, models, transport fallback
- ProviderPicker frontend component in CoderPane header
- External provider messages route through tasks row instead of inference enqueue
- Smart scroll: MessageList only auto-scrolls when near bottom (150px threshold)
- DB: available_agents gets models, label, transport columns
- Bug fix: loadContext SELECT includes allowed_read_paths
- Bug fix: cap hit sentinel inserted before buildMessagesPayload
- docker-compose.yml: boocoder service commented out, BOOCODER_URL env var added
- CLAUDE.md: updated docs for systemd, provider registry, JSONB gotcha, loadContext
This commit is contained in:
2026-05-25 19:20:53 +00:00
parent e423579e99
commit d8ffee1950
21 changed files with 687 additions and 222 deletions

View File

@@ -1,12 +1,12 @@
/**
* ACP dispatch — runs ACP-capable agents (opencode, goose) on the host via SSH.
* ACP dispatch — runs ACP-capable agents (opencode, goose) directly on the host.
*
* Uses the @agentclientprotocol/sdk to establish a structured JSON-RPC session
* with the agent subprocess. The SSH tunnel provides stdio transport.
* v2.1.1: BooCoder runs on the host now — agents are spawned directly,
* no SSH needed. Uses @agentclientprotocol/sdk for structured JSON-RPC.
*
* Flow:
* 1. SSH to host, start `opencode acp` (or `goose acp`) in the worktree
* 2. Wrap SSH child's stdin/stdout into NDJSON streams
* 1. Spawn `opencode acp` (or `goose acp`) in the worktree
* 2. Wrap child's stdin/stdout into NDJSON streams
* 3. Create a ClientSideConnection from the SDK
* 4. Initialize → newSession → prompt(task)
* 5. Collect session updates (tool calls, text output)
@@ -28,7 +28,7 @@ import {
type CreateTerminalRequest,
type CreateTerminalResponse,
} from '@agentclientprotocol/sdk';
import { sshSpawn } from './ssh.js';
import { spawn } from 'node:child_process';
export interface AcpDispatchResult {
exitCode: number;
@@ -42,17 +42,17 @@ export interface AcpDispatchOpts {
task: string;
worktreePath: string;
model?: string;
installPath?: string;
signal?: AbortSignal;
log: FastifyBaseLogger;
}
/** Map agent name to the ACP command it exposes. */
function acpCommand(agent: string): string | null {
function acpArgs(agent: string): string[] | null {
switch (agent) {
case 'opencode':
return 'opencode acp';
return ['acp'];
case 'goose':
return 'goose acp';
return ['acp'];
default:
return null;
}
@@ -114,10 +114,10 @@ function nodeWritableToWeb(nodeStream: NodeJS.WritableStream): WritableStream<Ui
* all session updates. Returns the collected output and tool calls.
*/
export async function dispatchViaAcp(opts: AcpDispatchOpts): Promise<AcpDispatchResult> {
const { agent, task, worktreePath, signal, log } = opts;
const { agent, task, worktreePath, installPath, signal, log } = opts;
const cmd = acpCommand(agent);
if (!cmd) {
const args = acpArgs(agent);
if (!args) {
return {
exitCode: 1,
output: `Agent '${agent}' does not support ACP.`,
@@ -126,12 +126,13 @@ export async function dispatchViaAcp(opts: AcpDispatchOpts): Promise<AcpDispatch
};
}
// Spawn SSH with the ACP command running in the worktree
const escapedPath = worktreePath.replace(/'/g, "'\\''");
const fullCommand = `cd '${escapedPath}' && ${cmd}`;
log.info({ agent, worktreePath }, 'acp-dispatch: spawning');
const child = sshSpawn(fullCommand);
const binary = installPath ?? agent;
log.info({ agent, binary, worktreePath }, 'acp-dispatch: spawning');
const child = spawn(binary, args, {
cwd: worktreePath,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env },
});
// Wire up abort
let killed = false;