fix(hooks): add stack traces to hook errors, fix tools.ts theme bug

- HookError now includes optional stack field
- Hook error display shows stack trace in dim color below error message
- tools.ts: create SettingsListTheme using the theme passed to ctx.ui.custom()
  instead of using getSettingsListTheme() which depends on global theme
This commit is contained in:
Mario Zechner 2026-01-04 18:33:28 +01:00
parent 91fae8b2f0
commit 6390ff87ef
4 changed files with 34 additions and 6 deletions

View file

@ -9,9 +9,8 @@
* 2. Use /tools to open the tool selector
*/
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent/hooks";
import { Container, type SettingItem, SettingsList } from "@mariozechner/pi-tui";
import { Container, type SettingItem, SettingsList, type SettingsListTheme } from "@mariozechner/pi-tui";
// State persisted to session
interface ToolsState {
@ -88,10 +87,20 @@ export default function toolsHook(pi: HookAPI) {
})(),
);
// Create theme for SettingsList using the passed theme
const settingsTheme: SettingsListTheme = {
label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
value: (text: string, selected: boolean) =>
selected ? theme.fg("accent", text) : theme.fg("muted", text),
description: (text: string) => theme.fg("dim", text),
cursor: theme.fg("accent", "→ "),
hint: (text: string) => theme.fg("dim", text),
};
const settingsList = new SettingsList(
items,
Math.min(items.length + 2, 15),
getSettingsListTheme(),
settingsTheme,
(id, newValue) => {
// Update enabled state and apply immediately
if (newValue === "enabled") {

View file

@ -410,10 +410,12 @@ export class HookRunner {
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
this.emitError({
hookPath: hook.path,
event: event.type,
error: message,
stack,
});
}
}
@ -477,10 +479,12 @@ export class HookRunner {
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
this.emitError({
hookPath: hook.path,
event: "context",
error: message,
stack,
});
}
}
@ -523,10 +527,12 @@ export class HookRunner {
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
this.emitError({
hookPath: hook.path,
event: "before_agent_start",
error: message,
stack,
});
}
}

View file

@ -911,4 +911,5 @@ export interface HookError {
hookPath: string;
event: string;
error: string;
stack?: string;
}

View file

@ -524,7 +524,7 @@ export class InteractiveMode {
// Subscribe to hook errors
hookRunner.onError((error) => {
this.showHookError(error.hookPath, error.error);
this.showHookError(error.hookPath, error.error, error.stack);
});
// Set up hook-registered shortcuts
@ -863,9 +863,21 @@ export class InteractiveMode {
/**
* Show a hook error in the UI.
*/
private showHookError(hookPath: string, error: string): void {
const errorText = new Text(theme.fg("error", `Hook "${hookPath}" error: ${error}`), 1, 0);
private showHookError(hookPath: string, error: string, stack?: string): void {
const errorMsg = `Hook "${hookPath}" error: ${error}`;
const errorText = new Text(theme.fg("error", errorMsg), 1, 0);
this.chatContainer.addChild(errorText);
if (stack) {
// Show stack trace in dim color, indented
const stackLines = stack
.split("\n")
.slice(1) // Skip first line (duplicates error message)
.map((line) => theme.fg("dim", ` ${line.trim()}`))
.join("\n");
if (stackLines) {
this.chatContainer.addChild(new Text(stackLines, 1, 0));
}
}
this.ui.requestRender();
}