feat: add error handling to orchestrator and UI tool call display

Adds error callbacks to LLM orchestrator for tool and stream failures, updates UI to show failed tool calls with error details, removes deprecated tool-definitions.ts file, and updates agent prompt to remove `--dangerously-skip-permissions` flag from Claude Code commands.
This commit is contained in:
Mohamed Boudra
2025-10-20 00:05:29 +02:00
parent 853a73cc05
commit 07dae1c520
16 changed files with 95 additions and 188 deletions
@@ -133,6 +133,33 @@ export async function processUserMessage(params: {
});
}
},
onToolError: async (toolCallId, toolName, error) => {
// Broadcast tool error to WebSocket
if (params.wsServer) {
params.wsServer.broadcastActivityLog({
id: toolCallId,
timestamp: new Date(),
type: "error",
content: `Tool ${toolName} failed: ${
error instanceof Error ? error.message : String(error)
}`,
metadata: { toolCallId, toolName, error },
});
}
},
onError: async (error) => {
// Broadcast general stream error to WebSocket
if (params.wsServer) {
params.wsServer.broadcastActivityLog({
id: uuidv4(),
timestamp: new Date(),
type: "error",
content: `Stream error: ${
error instanceof Error ? error.message : String(error)
}`,
});
}
},
onFinish: async () => {
// Wait for any final pending TTS to complete
if (pendingTTS) {
@@ -1,171 +0,0 @@
/**
* OpenAI function calling schemas for terminal operations
* These tools are exposed to the LLM for terminal control
*/
export const terminalTools = [
{
type: "function",
function: {
name: "list_terminals",
description:
"List all available terminals in the voice-dev session. Returns terminal ID, name, working directory, and current command for each terminal.",
parameters: {
type: "object",
properties: {},
required: [],
},
},
},
{
type: "function",
function: {
name: "create_terminal",
description:
"Create a new terminal (tmux window) with a specific name and working directory. Optionally execute an initial command after creation. Terminal names must be unique.",
parameters: {
type: "object",
properties: {
name: {
type: "string",
description:
"Unique name for the terminal. Should be descriptive of what the terminal is used for (e.g., 'web-dev', 'api-server', 'tests').",
},
workingDirectory: {
type: "string",
description:
"Absolute path to the working directory for this terminal. Can use ~ for home directory.",
},
initialCommand: {
type: "string",
description:
"Optional command to execute after creating the terminal (e.g., 'npm run dev', 'python -m venv venv').",
},
},
required: ["name", "workingDirectory"],
},
},
},
{
type: "function",
function: {
name: "capture_terminal",
description:
"Capture and return the output from a terminal. Returns the last N lines of terminal content. Useful for checking command results or monitoring running processes.",
parameters: {
type: "object",
properties: {
terminalId: {
type: "string",
description:
"The terminal ID (window ID format: @123) to capture output from. Get this from list_terminals.",
},
lines: {
type: "number",
description:
"Number of lines to capture from the terminal history. Defaults to 200.",
},
wait: {
type: "number",
description:
"Optional milliseconds to wait before capturing output. Useful when you just sent a command and want to wait for it to complete.",
},
},
required: ["terminalId"],
},
},
},
{
type: "function",
function: {
name: "send_text",
description:
"Type text into a terminal. Can optionally press Enter to execute the text as a command. Can optionally return the output after execution. This is the primary way to run commands in terminals.",
parameters: {
type: "object",
properties: {
terminalId: {
type: "string",
description:
"The terminal ID (window ID format: @123) to send text to. Get this from list_terminals.",
},
text: {
type: "string",
description:
"The text to type into the terminal. Can be a command, input for a running program, or any text.",
},
pressEnter: {
type: "boolean",
description:
"Whether to press Enter after typing the text. Set to true to execute commands. Defaults to false.",
},
return_output: {
type: "object",
description:
"Optional. If provided, will wait for terminal activity to settle and return the output.",
properties: {
lines: {
type: "number",
description: "Number of lines to capture. Defaults to 200.",
},
waitForSettled: {
type: "boolean",
description:
"Whether to wait for terminal activity to settle before returning output. Polls terminal output and waits 500ms after last change. Defaults to true.",
},
maxWait: {
type: "number",
description:
"Maximum milliseconds to wait for activity to settle. Defaults to 120000 (2 minutes).",
},
},
},
},
required: ["terminalId", "text"],
},
},
},
{
type: "function",
function: {
name: "rename_terminal",
description:
"Rename an existing terminal. The new name must be unique among all terminals.",
parameters: {
type: "object",
properties: {
terminalId: {
type: "string",
description:
"The terminal ID (window ID format: @123) to rename. Get this from list_terminals.",
},
newName: {
type: "string",
description:
"The new unique name for the terminal. Should be descriptive of the terminal's purpose.",
},
},
required: ["terminalId", "newName"],
},
},
},
{
type: "function",
function: {
name: "kill_terminal",
description:
"Close and destroy a terminal. This will terminate any running processes in the terminal. Use with caution.",
parameters: {
type: "object",
properties: {
terminalId: {
type: "string",
description:
"The terminal ID (window ID format: @123) to kill. Get this from list_terminals.",
},
},
required: ["terminalId"],
},
},
},
] as const;
+9
View File
@@ -520,6 +520,10 @@
border-color: #22c55e;
}
.tool-call-card[data-status="failed"] {
border-color: #ef4444;
}
.tool-call-header {
width: 100%;
display: flex;
@@ -575,6 +579,11 @@
color: #86efac;
}
.tool-call-card[data-status="failed"] .tool-call-status {
background-color: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
.status-icon {
flex-shrink: 0;
}
+20 -1
View File
@@ -21,7 +21,8 @@ type LogEntry =
toolName: string;
args: any;
result?: any;
status: "executing" | "completed";
error?: any;
status: "executing" | "completed" | "failed";
};
function App() {
@@ -138,6 +139,23 @@ function App() {
return;
}
// Handle tool errors - update tool call status to failed
if (data.type === "error" && data.metadata?.toolCallId) {
const { toolCallId, error } = data.metadata as {
toolCallId: string;
error: any;
};
setLogs((prev) =>
prev.map((log) =>
log.type === "tool_call" && log.id === toolCallId
? { ...log, error, status: "failed" as const }
: log
)
);
// Don't return - still add the error message to the log
}
// Map activity log types to UI log types
let logType: "system" | "info" | "success" | "error" | "user" | "assistant" = "info";
if (data.type === "transcript") logType = "user";
@@ -336,6 +354,7 @@ function App() {
toolName={log.toolName}
args={log.args}
result={log.result}
error={log.error}
status={log.status}
/>
) : (
@@ -1,17 +1,19 @@
import { useState } from "react";
import { ChevronDown, ChevronRight, CheckCircle, Loader2 } from "lucide-react";
import { ChevronDown, ChevronRight, CheckCircle, Loader2, XCircle } from "lucide-react";
export interface ToolCallCardProps {
toolName: string;
args: any;
result?: any;
status: "executing" | "completed";
error?: any;
status: "executing" | "completed" | "failed";
}
export function ToolCallCard({
toolName,
args,
result,
error,
status,
}: ToolCallCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
@@ -34,6 +36,8 @@ export function ToolCallCard({
<div className="tool-call-status">
{status === "executing" ? (
<Loader2 size={14} className="status-icon spinning" />
) : status === "failed" ? (
<XCircle size={14} className="status-icon" />
) : (
<CheckCircle size={14} className="status-icon" />
)}
@@ -54,6 +58,13 @@ export function ToolCallCard({
<pre className="section-content">{JSON.stringify(result, null, 2)}</pre>
</div>
)}
{error !== undefined && (
<div className="tool-call-section">
<div className="section-header">Error</div>
<pre className="section-content">{JSON.stringify(error, null, 2)}</pre>
</div>
)}
</div>
)}
</div>