From a8866d7a83f12cf749d4316e6f01e80cfd41d540 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 27 Dec 2025 03:10:08 +0100 Subject: [PATCH] Refactor: shared exec utility, rename CustomMessageRenderer to HookMessageRenderer - Extract execCommand to src/core/exec.ts, shared by hooks and custom-tools - Rename CustomMessageRenderer -> HookMessageRenderer - Rename registerCustomMessageRenderer -> registerMessageRenderer - Renderer now receives HookMessage instead of CustomMessageEntry - CustomMessageComponent now has setExpanded() and responds to Ctrl+E toggle - Re-export ExecOptions/ExecResult from exec.ts for backward compatibility --- packages/coding-agent/CHANGELOG.md | 4 +- .../src/core/custom-tools/loader.ts | 94 +--------------- .../src/core/custom-tools/types.ts | 19 +--- packages/coding-agent/src/core/exec.ts | 104 ++++++++++++++++++ packages/coding-agent/src/core/hooks/index.ts | 4 +- .../coding-agent/src/core/hooks/loader.ts | 20 ++-- .../coding-agent/src/core/hooks/runner.ts | 85 +------------- packages/coding-agent/src/core/hooks/types.ts | 50 +++------ packages/coding-agent/src/core/sdk.ts | 8 +- .../interactive/components/custom-message.ts | 58 +++++++--- .../src/modes/interactive/interactive-mode.ts | 6 +- .../test/compaction-hooks.test.ts | 8 +- 12 files changed, 199 insertions(+), 261 deletions(-) create mode 100644 packages/coding-agent/src/core/exec.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d6e35849..5efd1ee1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -34,9 +34,9 @@ - `pi.send(text, attachments?)` replaced with `pi.sendMessage(message, triggerTurn?)` which creates `CustomMessageEntry` instead of user messages - New `pi.appendEntry(customType, data?)` to persist hook state (does NOT participate in LLM context) - New `pi.registerCommand(name, options)` to register custom slash commands - - New `pi.registerCustomMessageRenderer(customType, renderer)` to register custom renderers for `CustomMessageEntry` + - New `pi.registerMessageRenderer(customType, renderer)` to register custom renderers for hook messages - New `pi.exec(command, args, options?)` to execute shell commands (moved from `HookEventContext`/`HookCommandContext`) - - `CustomMessageRenderer` type: `(entry, options, theme) => Component | null` + - `HookMessageRenderer` type: `(message: HookMessage, options, theme) => Component | null` - Renderers return inner content; the TUI wraps it in a styled Box - New types: `HookMessage`, `RegisteredCommand`, `HookCommandContext` - Handler types renamed: `SendHandler` → `SendMessageHandler`, new `AppendEntryHandler` diff --git a/packages/coding-agent/src/core/custom-tools/loader.ts b/packages/coding-agent/src/core/custom-tools/loader.ts index 3772e0d7..e3e7d0d9 100644 --- a/packages/coding-agent/src/core/custom-tools/loader.ts +++ b/packages/coding-agent/src/core/custom-tools/loader.ts @@ -7,7 +7,6 @@ * for custom tools that depend on pi packages. */ -import { spawn } from "node:child_process"; import * as fs from "node:fs"; import { createRequire } from "node:module"; import * as os from "node:os"; @@ -15,15 +14,10 @@ import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { createJiti } from "jiti"; import { getAgentDir, isBunBinary } from "../../config.js"; +import type { ExecOptions } from "../exec.js"; +import { execCommand } from "../exec.js"; import type { HookUIContext } from "../hooks/types.js"; -import type { - CustomToolFactory, - CustomToolsLoadResult, - ExecOptions, - ExecResult, - LoadedCustomTool, - ToolAPI, -} from "./types.js"; +import type { CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool, ToolAPI } from "./types.js"; // Create require function to resolve module paths at runtime const require = createRequire(import.meta.url); @@ -87,88 +81,6 @@ function resolveToolPath(toolPath: string, cwd: string): string { return path.resolve(cwd, expanded); } -/** - * Execute a command and return stdout/stderr/code. - * Supports cancellation via AbortSignal and timeout. - */ -async function execCommand(command: string, args: string[], cwd: string, options?: ExecOptions): Promise { - return new Promise((resolve) => { - const proc = spawn(command, args, { - cwd, - shell: false, - stdio: ["ignore", "pipe", "pipe"], - }); - - let stdout = ""; - let stderr = ""; - let killed = false; - let timeoutId: NodeJS.Timeout | undefined; - - const killProcess = () => { - if (!killed) { - killed = true; - proc.kill("SIGTERM"); - // Force kill after 5 seconds if SIGTERM doesn't work - setTimeout(() => { - if (!proc.killed) { - proc.kill("SIGKILL"); - } - }, 5000); - } - }; - - // Handle abort signal - if (options?.signal) { - if (options.signal.aborted) { - killProcess(); - } else { - options.signal.addEventListener("abort", killProcess, { once: true }); - } - } - - // Handle timeout - if (options?.timeout && options.timeout > 0) { - timeoutId = setTimeout(() => { - killProcess(); - }, options.timeout); - } - - proc.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - proc.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - proc.on("close", (code) => { - if (timeoutId) clearTimeout(timeoutId); - if (options?.signal) { - options.signal.removeEventListener("abort", killProcess); - } - resolve({ - stdout, - stderr, - code: code ?? 0, - killed, - }); - }); - - proc.on("error", (err) => { - if (timeoutId) clearTimeout(timeoutId); - if (options?.signal) { - options.signal.removeEventListener("abort", killProcess); - } - resolve({ - stdout, - stderr: stderr || err.message, - code: 1, - killed, - }); - }); - }); -} - /** * Create a no-op UI context for headless modes. */ diff --git a/packages/coding-agent/src/core/custom-tools/types.ts b/packages/coding-agent/src/core/custom-tools/types.ts index d9e3297d..43713a61 100644 --- a/packages/coding-agent/src/core/custom-tools/types.ts +++ b/packages/coding-agent/src/core/custom-tools/types.ts @@ -9,6 +9,7 @@ import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@mario import type { Component } from "@mariozechner/pi-tui"; import type { Static, TSchema } from "@sinclair/typebox"; import type { Theme } from "../../modes/interactive/theme/theme.js"; +import type { ExecOptions, ExecResult } from "../exec.js"; import type { HookUIContext } from "../hooks/types.js"; import type { SessionEntry } from "../session-manager.js"; @@ -18,22 +19,8 @@ export type ToolUIContext = HookUIContext; /** Re-export for custom tools to use in execute signature */ export type { AgentToolUpdateCallback }; -export interface ExecResult { - stdout: string; - stderr: string; - code: number; - /** True if the process was killed due to signal or timeout */ - killed?: boolean; -} - -export interface ExecOptions { - /** AbortSignal to cancel the process */ - signal?: AbortSignal; - /** Timeout in milliseconds */ - timeout?: number; - /** Working directory */ - cwd?: string; -} +// Re-export for backward compatibility +export type { ExecOptions, ExecResult } from "../exec.js"; /** API passed to custom tool factory (stable across session changes) */ export interface ToolAPI { diff --git a/packages/coding-agent/src/core/exec.ts b/packages/coding-agent/src/core/exec.ts new file mode 100644 index 00000000..fccf5504 --- /dev/null +++ b/packages/coding-agent/src/core/exec.ts @@ -0,0 +1,104 @@ +/** + * Shared command execution utilities for hooks and custom tools. + */ + +import { spawn } from "node:child_process"; + +/** + * Options for executing shell commands. + */ +export interface ExecOptions { + /** AbortSignal to cancel the command */ + signal?: AbortSignal; + /** Timeout in milliseconds */ + timeout?: number; + /** Working directory */ + cwd?: string; +} + +/** + * Result of executing a shell command. + */ +export interface ExecResult { + stdout: string; + stderr: string; + code: number; + killed: boolean; +} + +/** + * Execute a shell command and return stdout/stderr/code. + * Supports timeout and abort signal. + */ +export async function execCommand( + command: string, + args: string[], + cwd: string, + options?: ExecOptions, +): Promise { + return new Promise((resolve) => { + const proc = spawn(command, args, { + cwd, + shell: false, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + let killed = false; + let timeoutId: NodeJS.Timeout | undefined; + + const killProcess = () => { + if (!killed) { + killed = true; + proc.kill("SIGTERM"); + // Force kill after 5 seconds if SIGTERM doesn't work + setTimeout(() => { + if (!proc.killed) { + proc.kill("SIGKILL"); + } + }, 5000); + } + }; + + // Handle abort signal + if (options?.signal) { + if (options.signal.aborted) { + killProcess(); + } else { + options.signal.addEventListener("abort", killProcess, { once: true }); + } + } + + // Handle timeout + if (options?.timeout && options.timeout > 0) { + timeoutId = setTimeout(() => { + killProcess(); + }, options.timeout); + } + + proc.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + if (timeoutId) clearTimeout(timeoutId); + if (options?.signal) { + options.signal.removeEventListener("abort", killProcess); + } + resolve({ stdout, stderr, code: code ?? 0, killed }); + }); + + proc.on("error", (_err) => { + if (timeoutId) clearTimeout(timeoutId); + if (options?.signal) { + options.signal.removeEventListener("abort", killProcess); + } + resolve({ stdout, stderr, code: 1, killed }); + }); + }); +} diff --git a/packages/coding-agent/src/core/hooks/index.ts b/packages/coding-agent/src/core/hooks/index.ts index 5bff545d..e16282bb 100644 --- a/packages/coding-agent/src/core/hooks/index.ts +++ b/packages/coding-agent/src/core/hooks/index.ts @@ -13,8 +13,8 @@ export type { AgentEndEvent, AgentStartEvent, BashToolResultEvent, - CustomMessageRenderer, - CustomMessageRenderOptions, + HookMessageRenderer, + HookMessageRenderOptions, CustomToolResultEvent, EditToolResultEvent, ExecOptions, diff --git a/packages/coding-agent/src/core/hooks/loader.ts b/packages/coding-agent/src/core/hooks/loader.ts index 12b9966a..6cfb88f8 100644 --- a/packages/coding-agent/src/core/hooks/loader.ts +++ b/packages/coding-agent/src/core/hooks/loader.ts @@ -11,11 +11,11 @@ import { createJiti } from "jiti"; import { getAgentDir } from "../../config.js"; import { execCommand } from "./runner.js"; import type { - CustomMessageRenderer, ExecOptions, HookAPI, HookFactory, HookMessage, + HookMessageRenderer, RegisteredCommand, } from "./types.js"; @@ -73,8 +73,8 @@ export interface LoadedHook { resolvedPath: string; /** Map of event type to handler functions */ handlers: Map; - /** Map of customType to custom message renderer */ - customMessageRenderers: Map; + /** Map of customType to hook message renderer */ + messageRenderers: Map; /** Map of command name to registered command */ commands: Map; /** Set the send message handler for this hook's pi.sendMessage() */ @@ -136,7 +136,7 @@ function createHookAPI( cwd: string, ): { api: HookAPI; - customMessageRenderers: Map; + messageRenderers: Map; commands: Map; setSendMessageHandler: (handler: SendMessageHandler) => void; setAppendEntryHandler: (handler: AppendEntryHandler) => void; @@ -147,7 +147,7 @@ function createHookAPI( let appendEntryHandler: AppendEntryHandler = () => { // Default no-op until mode sets the handler }; - const customMessageRenderers = new Map(); + const messageRenderers = new Map(); const commands = new Map(); // Cast to HookAPI - the implementation is more general (string event names) @@ -164,8 +164,8 @@ function createHookAPI( appendEntry(customType: string, data?: T): void { appendEntryHandler(customType, data); }, - registerCustomMessageRenderer(customType: string, renderer: CustomMessageRenderer): void { - customMessageRenderers.set(customType, renderer as CustomMessageRenderer); + registerMessageRenderer(customType: string, renderer: HookMessageRenderer): void { + messageRenderers.set(customType, renderer as HookMessageRenderer); }, registerCommand(name: string, options: { description?: string; handler: RegisteredCommand["handler"] }): void { commands.set(name, { name, ...options }); @@ -177,7 +177,7 @@ function createHookAPI( return { api, - customMessageRenderers, + messageRenderers, commands, setSendMessageHandler: (handler: SendMessageHandler) => { sendMessageHandler = handler; @@ -212,7 +212,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo // Create handlers map and API const handlers = new Map(); - const { api, customMessageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI( + const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI( handlers, cwd, ); @@ -225,7 +225,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo path: hookPath, resolvedPath, handlers, - customMessageRenderers, + messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler, diff --git a/packages/coding-agent/src/core/hooks/runner.ts b/packages/coding-agent/src/core/hooks/runner.ts index bfdebd28..768ffc86 100644 --- a/packages/coding-agent/src/core/hooks/runner.ts +++ b/packages/coding-agent/src/core/hooks/runner.ts @@ -2,17 +2,14 @@ * Hook runner - executes hooks and manages their lifecycle. */ -import { spawn } from "node:child_process"; import type { ModelRegistry } from "../model-registry.js"; import type { SessionManager } from "../session-manager.js"; import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js"; import type { - CustomMessageRenderer, - ExecOptions, - ExecResult, HookError, HookEvent, HookEventContext, + HookMessageRenderer, HookUIContext, RegisteredCommand, SessionEvent, @@ -32,78 +29,8 @@ const DEFAULT_TIMEOUT = 30000; */ export type HookErrorListener = (error: HookError) => void; -/** - * Execute a command and return stdout/stderr/code. - * Supports cancellation via AbortSignal and timeout. - */ -export async function execCommand( - command: string, - args: string[], - cwd: string, - options?: ExecOptions, -): Promise { - return new Promise((resolve) => { - const proc = spawn(command, args, { cwd, shell: false }); - - let stdout = ""; - let stderr = ""; - let killed = false; - let timeoutId: NodeJS.Timeout | undefined; - - const killProcess = () => { - if (!killed) { - killed = true; - proc.kill("SIGTERM"); - // Force kill after 5 seconds if SIGTERM doesn't work - setTimeout(() => { - if (!proc.killed) { - proc.kill("SIGKILL"); - } - }, 5000); - } - }; - - // Handle abort signal - if (options?.signal) { - if (options.signal.aborted) { - killProcess(); - } else { - options.signal.addEventListener("abort", killProcess, { once: true }); - } - } - - // Handle timeout - if (options?.timeout && options.timeout > 0) { - timeoutId = setTimeout(() => { - killProcess(); - }, options.timeout); - } - - proc.stdout?.on("data", (data) => { - stdout += data.toString(); - }); - - proc.stderr?.on("data", (data) => { - stderr += data.toString(); - }); - - proc.on("close", (code) => { - if (timeoutId) clearTimeout(timeoutId); - if (options?.signal) { - options.signal.removeEventListener("abort", killProcess); - } - resolve({ stdout, stderr, code: code ?? 0, killed }); - }); - - proc.on("error", (_err) => { - if (timeoutId) clearTimeout(timeoutId); - if (options?.signal) { - options.signal.removeEventListener("abort", killProcess); - } - resolve({ stdout, stderr, code: 1, killed }); - }); - }); -} +// Re-export execCommand for backward compatibility +export { execCommand } from "../exec.js"; /** * Create a promise that rejects after a timeout. @@ -241,12 +168,12 @@ export class HookRunner { } /** - * Get a custom message renderer for the given customType. + * Get a message renderer for the given customType. * Returns the first renderer found across all hooks, or undefined if none. */ - getCustomMessageRenderer(customType: string): CustomMessageRenderer | undefined { + getMessageRenderer(customType: string): HookMessageRenderer | undefined { for (const hook of this.hooks) { - const renderer = hook.customMessageRenderers.get(customType); + const renderer = hook.messageRenderers.get(customType); if (renderer) { return renderer; } diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 7798dd6a..7b329438 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -10,6 +10,7 @@ import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mario import type { Component } from "@mariozechner/pi-tui"; import type { Theme } from "../../modes/interactive/theme/theme.js"; import type { CompactionPreparation, CompactionResult } from "../compaction.js"; +import type { ExecOptions, ExecResult } from "../exec.js"; import type { ModelRegistry } from "../model-registry.js"; import type { CompactionEntry, CustomMessageEntry, SessionManager } from "../session-manager.js"; import type { EditToolDetails } from "../tools/edit.js"; @@ -21,29 +22,8 @@ import type { ReadToolDetails, } from "../tools/index.js"; -// ============================================================================ -// Execution Context -// ============================================================================ - -/** - * Result of executing a command via ctx.exec() - */ -export interface ExecResult { - stdout: string; - stderr: string; - code: number; - /** True if the process was killed due to signal or timeout */ - killed?: boolean; -} - -export interface ExecOptions { - /** AbortSignal to cancel the process */ - signal?: AbortSignal; - /** Timeout in milliseconds */ - timeout?: number; - /** Working directory */ - cwd?: string; -} +// Re-export for backward compatibility +export type { ExecOptions, ExecResult } from "../exec.js"; /** * UI context for hooks to request interactive UI from the harness. @@ -372,26 +352,26 @@ export type HookHandler = (event: E, ctx: HookEventContext) => Prom /** * Options passed to custom message renderers. */ -export interface CustomMessageRenderOptions { +/** + * Message type for hooks to send. Creates CustomMessageEntry in the session. + */ +export type HookMessage = Pick, "customType" | "content" | "display" | "details">; + +export interface HookMessageRenderOptions { /** Whether the view is expanded */ expanded: boolean; } /** - * Renderer for custom message entries. - * Hooks register these to provide custom TUI rendering for their CustomMessageEntry types. + * Renderer for hook messages. + * Hooks register these to provide custom TUI rendering for their message types. */ -export type CustomMessageRenderer = ( - entry: CustomMessageEntry, - options: CustomMessageRenderOptions, +export type HookMessageRenderer = ( + message: HookMessage, + options: HookMessageRenderOptions, theme: Theme, ) => Component | null; -/** - * Message type for hooks to send. Creates CustomMessageEntry in the session. - */ -export type HookMessage = Pick, "customType" | "content" | "display" | "details">; - /** * Context passed to hook command handlers. */ @@ -483,7 +463,7 @@ export interface HookAPI { * The renderer is called when rendering the entry in the TUI. * Return null to use the default renderer. */ - registerCustomMessageRenderer(customType: string, renderer: CustomMessageRenderer): void; + registerMessageRenderer(customType: string, renderer: HookMessageRenderer): void; /** * Register a custom slash command. diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 1c7ab14f..072cb009 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -340,7 +340,7 @@ function createFactoryFromLoadedHook(loaded: LoadedHook): HookFactory { function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; factory: HookFactory }>): LoadedHook[] { return definitions.map((def) => { const handlers = new Map Promise>>(); - const customMessageRenderers = new Map(); + const messageRenderers = new Map(); const commands = new Map(); let sendMessageHandler: (message: any, triggerTurn?: boolean) => void = () => {}; let appendEntryHandler: (customType: string, data?: any) => void = () => {}; @@ -357,8 +357,8 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa appendEntry: (customType: string, data?: any) => { appendEntryHandler(customType, data); }, - registerCustomMessageRenderer: (customType: string, renderer: any) => { - customMessageRenderers.set(customType, renderer); + registerMessageRenderer: (customType: string, renderer: any) => { + messageRenderers.set(customType, renderer); }, registerCommand: (name: string, options: any) => { commands.set(name, { name, ...options }); @@ -371,7 +371,7 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa path: def.path ?? "", resolvedPath: def.path ?? "", handlers, - customMessageRenderers, + messageRenderers, commands, setSendMessageHandler: (handler: (message: any, triggerTurn?: boolean) => void) => { sendMessageHandler = handler; diff --git a/packages/coding-agent/src/modes/interactive/components/custom-message.ts b/packages/coding-agent/src/modes/interactive/components/custom-message.ts index 5e952c32..5e2d3410 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-message.ts @@ -1,6 +1,6 @@ import type { TextContent } from "@mariozechner/pi-ai"; import { Box, Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui"; -import type { CustomMessageRenderer } from "../../../core/hooks/types.js"; +import type { HookMessage, HookMessageRenderer } from "../../../core/hooks/types.js"; import type { CustomMessageEntry } from "../../../core/session-manager.js"; import { getMarkdownTheme, theme } from "../theme/theme.js"; @@ -9,21 +9,49 @@ import { getMarkdownTheme, theme } from "../theme/theme.js"; * Uses distinct styling to differentiate from user messages. */ export class CustomMessageComponent extends Container { - constructor(entry: CustomMessageEntry, customRenderer?: CustomMessageRenderer) { + private entry: CustomMessageEntry; + private customRenderer?: HookMessageRenderer; + private box: Box; + private _expanded = false; + + constructor(entry: CustomMessageEntry, customRenderer?: HookMessageRenderer) { super(); + this.entry = entry; + this.customRenderer = customRenderer; this.addChild(new Spacer(1)); // Create box with purple background - const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); + this.box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); + this.addChild(this.box); + + this.rebuild(); + } + + setExpanded(expanded: boolean): void { + if (this._expanded !== expanded) { + this._expanded = expanded; + this.rebuild(); + } + } + + private rebuild(): void { + this.box.clear(); + + // Convert entry to HookMessage for renderer + const message: HookMessage = { + customType: this.entry.customType, + content: this.entry.content, + display: this.entry.display, + details: this.entry.details, + }; // Try custom renderer first - if (customRenderer) { + if (this.customRenderer) { try { - const component = customRenderer(entry, { expanded: false }, theme); + const component = this.customRenderer(message, { expanded: this._expanded }, theme); if (component) { - box.addChild(component); - this.addChild(box); + this.box.addChild(component); return; } } catch { @@ -32,27 +60,25 @@ export class CustomMessageComponent extends Container { } // Default rendering: label + content - const label = theme.fg("customMessageLabel", `\x1b[1m[${entry.customType}]\x1b[22m`); - box.addChild(new Text(label, 0, 0)); - box.addChild(new Spacer(1)); + const label = theme.fg("customMessageLabel", `\x1b[1m[${this.entry.customType}]\x1b[22m`); + this.box.addChild(new Text(label, 0, 0)); + this.box.addChild(new Spacer(1)); // Extract text content let text: string; - if (typeof entry.content === "string") { - text = entry.content; + if (typeof this.entry.content === "string") { + text = this.entry.content; } else { - text = entry.content + text = this.entry.content .filter((c): c is TextContent => c.type === "text") .map((c) => c.text) .join("\n"); } - box.addChild( + this.box.addChild( new Markdown(text, 0, 0, getMarkdownTheme(), { color: (text: string) => theme.fg("customMessageText", text), }), ); - - this.addChild(box); } } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 23efe7c9..1c910d37 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -1028,7 +1028,7 @@ export class InteractiveMode { parentId: null, timestamp: new Date().toISOString(), }; - const renderer = this.session.hookRunner?.getCustomMessageRenderer(message.customType); + const renderer = this.session.hookRunner?.getMessageRenderer(message.customType); this.chatContainer.addChild(new CustomMessageComponent(entry, renderer)); } } else if (message.role === "user") { @@ -1077,7 +1077,7 @@ export class InteractiveMode { // Check if this is a custom_message entry if (entry?.type === "custom_message") { if (entry.display) { - const renderer = this.session.hookRunner?.getCustomMessageRenderer(entry.customType); + const renderer = this.session.hookRunner?.getMessageRenderer(entry.customType); this.chatContainer.addChild(new CustomMessageComponent(entry, renderer)); } continue; @@ -1271,6 +1271,8 @@ export class InteractiveMode { child.setExpanded(this.toolOutputExpanded); } else if (child instanceof BashExecutionComponent) { child.setExpanded(this.toolOutputExpanded); + } else if (child instanceof CustomMessageComponent) { + child.setExpanded(this.toolOutputExpanded); } } this.ui.requestRender(); diff --git a/packages/coding-agent/test/compaction-hooks.test.ts b/packages/coding-agent/test/compaction-hooks.test.ts index 66d21b3d..ab23364b 100644 --- a/packages/coding-agent/test/compaction-hooks.test.ts +++ b/packages/coding-agent/test/compaction-hooks.test.ts @@ -63,7 +63,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { path: "test-hook", resolvedPath: "/test/test-hook.ts", handlers, - customMessageRenderers: new Map(), + messageRenderers: new Map(), commands: new Map(), setSendMessageHandler: () => {}, setAppendEntryHandler: () => {}, @@ -240,7 +240,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { ], ], ]), - customMessageRenderers: new Map(), + messageRenderers: new Map(), commands: new Map(), setSendMessageHandler: () => {}, setAppendEntryHandler: () => {}, @@ -286,7 +286,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { ], ], ]), - customMessageRenderers: new Map(), + messageRenderers: new Map(), commands: new Map(), setSendMessageHandler: () => {}, setAppendEntryHandler: () => {}, @@ -311,7 +311,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { ], ], ]), - customMessageRenderers: new Map(), + messageRenderers: new Map(), commands: new Map(), setSendMessageHandler: () => {}, setAppendEntryHandler: () => {},