Multi-agent audit + aggressive cleanup across server/web/coder/booterm, delivered behind a DEFER discipline so none of the in-flight files were touched. Removes dead code/deps/columns, dedups server + coder helpers, and splits the oversized modules (tools.ts, opencode-server.ts, sentinel-summaries, turn.ts, TerminalPane.tsx) behind stable contracts. Adds 78 parity/unit tests (server 587, coder 323); fixes two latent bugs (ChatPane queue keys, FileViewerOverlay blank-line parity). Intended tag: v2.7.12-audit-cleanup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
4.2 KiB
TypeScript
89 lines
4.2 KiB
TypeScript
/**
|
|
* Shared ACP `Client` builder — the callback closures every ACP connection needs
|
|
* (worktree-scoped FS bridge + permission/elicitation routing + session updates).
|
|
*
|
|
* Extracted (v2.7 audit reshape) from the byte-identical `buildClient` closures in
|
|
* `acp-dispatch.ts` (one-shot) and `backends/warm-acp.ts` (warm). The two differed
|
|
* only in WHERE the per-turn context comes from (a fixed dispatch vs. the warm
|
|
* backend's `activeTurn`) and a trivially-equivalent permission gate — both are now
|
|
* supplied via the `resolveTurn` callback, so the FS/permission/elicitation wiring
|
|
* lives once. Behavior is preserved exactly:
|
|
* - `sessionUpdate` drops when `resolveTurn()` returns null (between turns).
|
|
* - permission/elicitation route to the UI only when BOTH a taskId AND sessionId
|
|
* are present (warm always has a sessionId, so this matches its prior
|
|
* `turn?.taskId` gate); otherwise the same auto-select-first / decline fallback.
|
|
*/
|
|
import type {
|
|
Client,
|
|
SessionNotification,
|
|
RequestPermissionRequest,
|
|
RequestPermissionResponse,
|
|
ReadTextFileRequest,
|
|
ReadTextFileResponse,
|
|
WriteTextFileRequest,
|
|
WriteTextFileResponse,
|
|
CreateTerminalRequest,
|
|
CreateTerminalResponse,
|
|
CreateElicitationRequest,
|
|
CreateElicitationResponse,
|
|
} from '@agentclientprotocol/sdk';
|
|
import { readWorktreeTextFile, writeWorktreeTextFile } from './acp-client-fs.js';
|
|
import { waitForPermissionResponse, waitForElicitationResponse } from './permission-waiter.js';
|
|
|
|
/** The per-turn context an ACP `Client` closure needs, resolved lazily per call. */
|
|
export interface AcpTurnContext {
|
|
/** Per-turn task id, for routing permission/elicitation prompts back to the UI. */
|
|
taskId: string | undefined;
|
|
/** BooCode session id (for permission-waiter's broker frames). */
|
|
sessionId: string | undefined;
|
|
/** Per-turn mode id (autonomous-mode gate in permission-waiter). */
|
|
modeId: string | undefined;
|
|
/** The agent name (for permission-waiter routing). */
|
|
agent: string;
|
|
/** Forward a session/update notification to the turn's event sink. */
|
|
onSessionUpdate: (params: SessionNotification) => void | Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Build the ACP `Client` callbacks once per connection. `resolveTurn` is called at
|
|
* the moment each callback fires and returns the live turn context (or null when no
|
|
* turn is active — `sessionUpdate` then drops, matching the warm backend's
|
|
* between-turns behavior). The FS bridge is scoped to `worktreePath`.
|
|
*/
|
|
export function buildAcpClient(worktreePath: string, resolveTurn: () => AcpTurnContext | null): Client {
|
|
return {
|
|
sessionUpdate: async (params: SessionNotification): Promise<void> => {
|
|
const turn = resolveTurn();
|
|
if (!turn) return; // between turns — drop (no orphan settles a future turn)
|
|
await turn.onSessionUpdate(params);
|
|
},
|
|
requestPermission: async (params: RequestPermissionRequest): Promise<RequestPermissionResponse> => {
|
|
const turn = resolveTurn();
|
|
if (turn && turn.taskId && turn.sessionId) {
|
|
return waitForPermissionResponse(turn.taskId, turn.sessionId, turn.agent, turn.modeId, params);
|
|
}
|
|
const firstOption = params.options[0];
|
|
if (firstOption) return { outcome: { outcome: 'selected', optionId: firstOption.optionId } };
|
|
return { outcome: { outcome: 'cancelled' } };
|
|
},
|
|
readTextFile: async (params: ReadTextFileRequest): Promise<ReadTextFileResponse> => {
|
|
const content = await readWorktreeTextFile(worktreePath, params.path, params.line, params.limit);
|
|
return { content };
|
|
},
|
|
writeTextFile: async (params: WriteTextFileRequest): Promise<WriteTextFileResponse> => {
|
|
await writeWorktreeTextFile(worktreePath, params.path, params.content);
|
|
return {};
|
|
},
|
|
createTerminal: async (_params: CreateTerminalRequest): Promise<CreateTerminalResponse> => {
|
|
return { terminalId: 'noop' };
|
|
},
|
|
unstable_createElicitation: async (params: CreateElicitationRequest): Promise<CreateElicitationResponse> => {
|
|
const turn = resolveTurn();
|
|
if (turn && turn.taskId && turn.sessionId) {
|
|
return waitForElicitationResponse(turn.taskId, turn.sessionId, turn.agent, turn.modeId, params);
|
|
}
|
|
return { action: 'decline' };
|
|
},
|
|
};
|
|
}
|