Add /thinking command and improve TUI UX

- Add /thinking slash command with autocomplete for setting reasoning levels (off, minimal, low, medium, high)
- Fix Ctrl+C behavior: remove hardcoded exit in TUI, let focused component handle it
- Add empty lines before and after tool execution components for better visual separation
- Fix stats rendering: display stats AFTER tool executions complete (matches web-ui behavior)
- Remove "Press Ctrl+C again to exit" message, show "(esc to interrupt)" in loader instead
- Add bash tool abort signal support with immediate SIGKILL on interrupt
- Make Text and Markdown components return empty arrays when no actual text content
- Add setCustomBgRgb() method to Markdown for dynamic background colors
This commit is contained in:
Mario Zechner 2025-11-11 20:28:10 +01:00
parent c5083bb7cb
commit dc1e2f928b
7 changed files with 516 additions and 166 deletions

View file

@ -1,9 +1,6 @@
import type { AgentTool } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
const bashSchema = Type.Object({
command: Type.String({ description: "Bash command to execute" }),
@ -15,23 +12,54 @@ export const bashTool: AgentTool<typeof bashSchema> = {
description:
"Execute a bash command in the current working directory. Returns stdout and stderr. Commands run with a 30 second timeout.",
parameters: bashSchema,
execute: async (_toolCallId: string, { command }: { command: string }) => {
try {
const { stdout, stderr } = await execAsync(command, {
timeout: 30000,
maxBuffer: 10 * 1024 * 1024, // 10MB
});
execute: async (_toolCallId: string, { command }: { command: string }, signal?: AbortSignal) => {
return new Promise((resolve) => {
const child = exec(
command,
{
timeout: 30000,
maxBuffer: 10 * 1024 * 1024, // 10MB
},
(error, stdout, stderr) => {
if (signal) {
signal.removeEventListener("abort", onAbort);
}
let output = "";
if (stdout) output += stdout;
if (stderr) output += stderr ? `\nSTDERR:\n${stderr}` : "";
if (signal?.aborted) {
resolve({
output: `Command aborted by user\nSTDOUT: ${stdout}\nSTDERR: ${stderr}`,
details: undefined,
});
return;
}
return { output: output || "(no output)", details: undefined };
} catch (error: any) {
return {
output: `Error executing command: ${error.message}\nSTDOUT: ${error.stdout || ""}\nSTDERR: ${error.stderr || ""}`,
details: undefined,
let output = "";
if (stdout) output += stdout;
if (stderr) output += stderr ? `\nSTDERR:\n${stderr}` : "";
if (error && !error.killed) {
resolve({
output: `Error executing command: ${error.message}\n${output}`,
details: undefined,
});
} else {
resolve({ output: output || "(no output)", details: undefined });
}
},
);
// Handle abort signal
const onAbort = () => {
child.kill("SIGKILL");
};
}
if (signal) {
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener("abort", onAbort, { once: true });
}
}
});
},
};