import type { JsonValue } from "@berry/api";
import { code, type ToolRenderer, type ToolView } from "@berry/api/web.ts";
import * as j from "@char/justin";
import { ansiOutput } from "./ansi-view.ts";
const argsOf = j.validation.compile(
j.obj({ command: j.optional(j.string), timeout: j.optional(j.number) }),
);
const outputOf = j.validation.compile(j.obj({ output: j.string }));
const rawOutputOf = j.validation.compile(j.obj({ rawOutput: j.string }));
const exitCodeOf = j.validation.compile(j.obj({ exitCode: j.number }));
export function bashView(language = "bash"): ToolView {
const prompt =
;
const rendered = as HTMLPreElement;
const showOutput = ansiOutput(rendered);
const exit = as HTMLSpanElement;
let shown: { command: string; streaming: boolean } | null = null;
return {
element: (
{prompt}
{rendered}
{exit}
),
update(call) {
const args = argsOf(call.args).value;
const command = args?.command ?? "";
const streaming = call.status === "streaming";
// highlighted once the model has written all of it
if (command !== shown?.command || streaming !== shown.streaming) {
prompt.replaceChildren(
code(command, {
...(streaming ? {} : { language }),
gutter: (i) => (i === 0 ? "$" : ""),
}),
);
}
// a direct call's result is the text the model saw, which carries the exit code itself
const result = call.result;
const output = (typeof result === "string" ? result : outputOf(result).value?.output) ??
rawOutputOf(call.details).value?.rawOutput ?? call.output ?? call.error ?? "";
showOutput(output);
shown = { command, streaming };
const exitCode = exitCodeOf(result).value?.exitCode;
exit.textContent = exitCode === undefined || exitCode === 0 ? "" : `exit ${exitCode}`;
exit.hidden = exit.textContent === "";
},
};
}
/** The exit code a call ended with: in a script's result, or at the end of the text a direct call's model saw. */
function exitCode(result: JsonValue | null): number | null {
if (typeof result === "string") {
const code = result.match(/\[exit code (-?\d+)(?:;[^\]]*)?\]\s*$/)?.[1];
return code === undefined ? null : Number(code);
}
return exitCodeOf(result).value?.exitCode ?? null;
}
export const bashRenderer: ToolRenderer = {
summary: (args) => argsOf(args).value?.command?.split("\n")[0] ?? null,
timeout: (args) => argsOf(args).value?.timeout ?? null,
outcome: (call) => {
const code = exitCode(call.result);
return code === null || code === 0 ? null : `exit ${code}`;
},
body: () => bashView(),
};
export const powershellRenderer: ToolRenderer = {
...bashRenderer,
body: () => bashView("powershell"),
};