- Implemented diagnostics functionality to start, stop, and monitor background check commands. - Created a DiagnosticsPanel for real-time output display in the UI. - Added support for auto-detecting default diagnostics commands based on project configuration. - Introduced run_checks tool to execute project verification commands and report results. - Enhanced tools-extra with functions to parse AGENTS.md and package.json for check commands. - Added diff review functionality to visualize changes made in the last turn. - Implemented tests for diagnostics and run_checks functionalities to ensure reliability.
118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
import { join } from 'node:path';
|
|
|
|
/**
|
|
* Live diagnostics: a background check command whose output is shown in the UI
|
|
* but never reaches the model's context.
|
|
*
|
|
* The difference from `startBackground` in tools.ts is deliberate. A background
|
|
* bash command is a tool result the model owns; its tail feeds the transcript
|
|
* through the bash listener. Diagnostics are the opposite: a check the *user*
|
|
* wants to watch (tsc in watch mode, a test watcher) while the model works. Its
|
|
* output would be pure noise in the prompt — a file-watcher re-emits the whole
|
|
* tree on every save — so it is buffered here, separate from the bash journal,
|
|
* and only the UI reads it.
|
|
*
|
|
* One at a time: a diagnostics panel is a single line of state, and running two
|
|
* watchers (e.g. tsc + a test watcher) is what the model's own tools are for.
|
|
*/
|
|
|
|
export type DiagState = {
|
|
command: string;
|
|
proc: Bun.Subprocess;
|
|
/** Append-only, capped. Tail is what the panel shows. */
|
|
tail: string;
|
|
exit: number | null;
|
|
startedAt: number;
|
|
};
|
|
|
|
let current: DiagState | undefined;
|
|
|
|
const MAX_DIAG = 20_000;
|
|
const cap = (s: string) => (s.length <= MAX_DIAG ? s : s.slice(-MAX_DIAG));
|
|
|
|
/** Start a diagnostics command, replacing any running one (old one is killed). */
|
|
export function diagStart(command: string): { started: boolean; replaced?: boolean; command: string } {
|
|
diagStop();
|
|
const shell = process.platform === 'win32' ? ['cmd', '/c', command] : ['bash', '-lc', command];
|
|
let proc: Bun.Subprocess;
|
|
try {
|
|
proc = Bun.spawn(shell, { cwd: process.cwd(), stdout: 'pipe', stderr: 'pipe' });
|
|
} catch (e) {
|
|
throw new Error(`could not start diagnostics: ${e instanceof Error ? e.message : String(e)}`);
|
|
}
|
|
current = { command, proc, tail: '', exit: null, startedAt: Date.now() };
|
|
void proc.exited.then((code) => {
|
|
if (current?.proc === proc) current!.exit = code;
|
|
});
|
|
const pump = async (stream: ReadableStream<Uint8Array> | undefined) => {
|
|
if (!stream) return;
|
|
const decoder = new TextDecoder();
|
|
for await (const chunk of stream) {
|
|
const text = decoder.decode(chunk, { stream: true });
|
|
if (!text) continue;
|
|
if (current?.proc === proc) current!.tail = cap(current!.tail + text);
|
|
}
|
|
};
|
|
void pump(proc.stdout as ReadableStream<Uint8Array>);
|
|
void pump(proc.stderr as ReadableStream<Uint8Array>);
|
|
return { started: true, command };
|
|
}
|
|
|
|
/** Kill the running diagnostics command, if any. */
|
|
export function diagStop(): { stopped: boolean; command?: string } {
|
|
const d = current;
|
|
if (!d) return { stopped: false };
|
|
current = undefined;
|
|
try {
|
|
d.proc.kill();
|
|
} catch {
|
|
// already gone
|
|
}
|
|
return { stopped: true, command: d.command };
|
|
}
|
|
|
|
/** Snapshot for the UI panel. `exit` stays null while running. */
|
|
export function diagStatus(): { running: boolean; command?: string; exit: number | null; tail: string; startedAt: number } {
|
|
if (!current) return { running: false, exit: null, tail: '', startedAt: 0 };
|
|
return {
|
|
running: current!.exit === null,
|
|
command: current!.command,
|
|
exit: current!.exit,
|
|
tail: current!.tail,
|
|
startedAt: current!.startedAt,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Warm the diagnostics from config at boot, but never crash boot on a bad
|
|
* command string — the user can fix it with /diagnostics stop + start.
|
|
*/
|
|
export function bootDiagnostics(configDiagnostics: string | undefined): void {
|
|
if (!configDiagnostics?.trim()) return;
|
|
try {
|
|
diagStart(configDiagnostics.trim());
|
|
} catch {
|
|
// keep boot clean; /diagnostics start will report the real error
|
|
}
|
|
}
|
|
|
|
/** A default check command, mirroring run_checks' detection but for watch-style loops. */
|
|
export async function defaultDiagnosticsCommand(cwd: string): Promise<string | undefined> {
|
|
const has = async (p: string) => Bun.file(join(cwd, p)).exists();
|
|
if ((await has('bun.lock')) || (await has('package.json'))) {
|
|
if (await has('tsconfig.json')) return 'bun run typecheck --watch';
|
|
return 'bun test --watch';
|
|
}
|
|
if (await has('Cargo.toml')) return 'cargo watch -x check';
|
|
if (await has('go.mod')) return 'go build ./...';
|
|
return undefined;
|
|
}
|
|
|
|
/** Kill any running diagnostics on shutdown; best-effort, never throws. */
|
|
export function shutdownDiagnostics(): void {
|
|
try {
|
|
diagStop();
|
|
} catch {
|
|
// nothing to reap
|
|
}
|
|
} |