From 6390ff87ef8d319e608ca519c4e6068bd3e22c28 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 4 Jan 2026 18:33:28 +0100 Subject: [PATCH] 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 --- packages/coding-agent/examples/hooks/tools.ts | 15 ++++++++++++--- packages/coding-agent/src/core/hooks/runner.ts | 6 ++++++ packages/coding-agent/src/core/hooks/types.ts | 1 + .../src/modes/interactive/interactive-mode.ts | 18 +++++++++++++++--- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/coding-agent/examples/hooks/tools.ts b/packages/coding-agent/examples/hooks/tools.ts index 908c9d18..cdb8d149 100644 --- a/packages/coding-agent/examples/hooks/tools.ts +++ b/packages/coding-agent/examples/hooks/tools.ts @@ -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") { diff --git a/packages/coding-agent/src/core/hooks/runner.ts b/packages/coding-agent/src/core/hooks/runner.ts index 47ea3e4d..1e31c296 100644 --- a/packages/coding-agent/src/core/hooks/runner.ts +++ b/packages/coding-agent/src/core/hooks/runner.ts @@ -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, }); } } diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 3f887dc0..22d6428c 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -911,4 +911,5 @@ export interface HookError { hookPath: string; event: string; error: string; + stack?: string; } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c395dd40..e944bcc9 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -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(); }