From dccdf91b8c1c088d3ccbb88999c1f656764a1941 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 1 Jan 2026 21:58:01 +0100 Subject: [PATCH] Add ctx.ui.theme getter for styling status text with theme colors - Add theme property to HookUIContext interface - Implement in interactive, RPC, and no-op contexts - Add status-line.ts example hook - Document styling with theme colors in hooks.md --- packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/docs/hooks.md | 20 +++++++++- .../coding-agent/examples/hooks/README.md | 1 + .../examples/hooks/status-line.ts | 38 +++++++++++++++++++ .../src/core/custom-tools/loader.ts | 4 ++ .../src/core/export-html/template.js | 7 +++- .../coding-agent/src/core/hooks/runner.ts | 4 ++ packages/coding-agent/src/core/hooks/types.ts | 10 +++++ .../src/modes/interactive/interactive-mode.ts | 3 ++ .../coding-agent/src/modes/rpc/rpc-mode.ts | 5 +++ .../test/compaction-hooks.test.ts | 4 ++ 11 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/examples/hooks/status-line.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0969d284..ad19512f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -36,6 +36,7 @@ The hooks API has been restructured with more granular events and better session - New `pi.registerMessageRenderer(customType, renderer)` for custom TUI rendering - New `ctx.ui.custom(component)` for full TUI component rendering with keyboard focus - New `ctx.ui.setStatus(key, text)` for persistent status text in footer (multiple hooks can set their own) +- New `ctx.ui.theme` getter for styling text with theme colors - `ctx.exec()` moved to `pi.exec()` - `ctx.sessionFile` → `ctx.sessionManager.getSessionFile()` - New `ctx.modelRegistry` and `ctx.model` for API key resolution @@ -191,6 +192,7 @@ Total color count increased from 46 to 50. See [docs/theme.md](docs/theme.md) fo ### Added - `ctx.ui.setStatus(key, text)` for hooks to display persistent status text in the footer ([#385](https://github.com/badlogic/pi-mono/pull/385) by [@prateekmedia](https://github.com/prateekmedia)) +- `ctx.ui.theme` getter for styling status text and other output with theme colors - `/share` command to upload session as a secret GitHub gist and get a shareable URL via shittycodingagent.ai ([#380](https://github.com/badlogic/pi-mono/issues/380)) - HTML export now includes a tree visualization sidebar for navigating session branches ([#375](https://github.com/badlogic/pi-mono/issues/375)) - HTML export supports keyboard shortcuts: Ctrl+T to toggle thinking blocks, Ctrl+O to toggle tool outputs diff --git a/packages/coding-agent/docs/hooks.md b/packages/coding-agent/docs/hooks.md index 1a9a0570..2f61c3b6 100644 --- a/packages/coding-agent/docs/hooks.md +++ b/packages/coding-agent/docs/hooks.md @@ -438,7 +438,25 @@ const currentText = ctx.ui.getEditorText(); - Multiple hooks can set their own status using unique keys - Statuses are displayed on a single line in the footer, sorted alphabetically by key - Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width -- ANSI escape codes for styling are preserved +- Use `ctx.ui.theme` to style status text with theme colors (see below) + +**Styling with theme colors:** + +Use `ctx.ui.theme` to apply consistent colors that respect the user's theme: + +```typescript +const theme = ctx.ui.theme; + +// Foreground colors +ctx.ui.setStatus("my-hook", theme.fg("success", "✓") + theme.fg("dim", " Ready")); +ctx.ui.setStatus("my-hook", theme.fg("error", "✗") + theme.fg("dim", " Failed")); +ctx.ui.setStatus("my-hook", theme.fg("accent", "●") + theme.fg("dim", " Working...")); + +// Available fg colors: accent, success, error, warning, muted, dim, text, and more +// See docs/theme.md for the full list of theme colors +``` + +See [examples/hooks/status-line.ts](../examples/hooks/status-line.ts) for a complete example. **Custom components:** diff --git a/packages/coding-agent/examples/hooks/README.md b/packages/coding-agent/examples/hooks/README.md index cab8d80d..8661f6a4 100644 --- a/packages/coding-agent/examples/hooks/README.md +++ b/packages/coding-agent/examples/hooks/README.md @@ -26,6 +26,7 @@ cp permission-gate.ts ~/.pi/agent/hooks/ | `custom-compaction.ts` | Custom compaction that summarizes entire conversation | | `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` | | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence | +| `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors | ## Writing Hooks diff --git a/packages/coding-agent/examples/hooks/status-line.ts b/packages/coding-agent/examples/hooks/status-line.ts new file mode 100644 index 00000000..41da3df9 --- /dev/null +++ b/packages/coding-agent/examples/hooks/status-line.ts @@ -0,0 +1,38 @@ +/** + * Status Line Hook + * + * Demonstrates ctx.ui.setStatus() for displaying persistent status text in the footer. + * Shows turn progress with themed colors. + */ + +import type { HookAPI } from "@mariozechner/pi-coding-agent"; + +export default function (pi: HookAPI) { + let turnCount = 0; + + pi.on("session_start", async (_event, ctx) => { + const theme = ctx.ui.theme; + ctx.ui.setStatus("status-demo", theme.fg("dim", "Ready")); + }); + + pi.on("turn_start", async (_event, ctx) => { + turnCount++; + const theme = ctx.ui.theme; + const spinner = theme.fg("accent", "●"); + const text = theme.fg("dim", ` Turn ${turnCount}...`); + ctx.ui.setStatus("status-demo", spinner + text); + }); + + pi.on("turn_end", async (_event, ctx) => { + const theme = ctx.ui.theme; + const check = theme.fg("success", "✓"); + const text = theme.fg("dim", ` Turn ${turnCount} complete`); + ctx.ui.setStatus("status-demo", check + text); + }); + + pi.on("session_new", async (_event, ctx) => { + turnCount = 0; + const theme = ctx.ui.theme; + ctx.ui.setStatus("status-demo", theme.fg("dim", "Ready")); + }); +} diff --git a/packages/coding-agent/src/core/custom-tools/loader.ts b/packages/coding-agent/src/core/custom-tools/loader.ts index dd223e18..ee81d603 100644 --- a/packages/coding-agent/src/core/custom-tools/loader.ts +++ b/packages/coding-agent/src/core/custom-tools/loader.ts @@ -14,6 +14,7 @@ import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { createJiti } from "jiti"; import { getAgentDir, isBunBinary } from "../../config.js"; +import { theme } from "../../modes/interactive/theme/theme.js"; import type { ExecOptions } from "../exec.js"; import { execCommand } from "../exec.js"; import type { HookUIContext } from "../hooks/types.js"; @@ -94,6 +95,9 @@ function createNoOpUIContext(): HookUIContext { custom: async () => undefined as never, setEditorText: () => {}, getEditorText: () => "", + get theme() { + return theme; + }, }; } diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index 431ae495..33c5edb5 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -1074,7 +1074,12 @@ highlighted = escapeHtml(code); } } else { - highlighted = escapeHtml(code); + // Auto-detect language if not specified + try { + highlighted = hljs.highlightAuto(code).value; + } catch { + highlighted = escapeHtml(code); + } } return `
${highlighted}
`; } diff --git a/packages/coding-agent/src/core/hooks/runner.ts b/packages/coding-agent/src/core/hooks/runner.ts index 7a0782f4..12624098 100644 --- a/packages/coding-agent/src/core/hooks/runner.ts +++ b/packages/coding-agent/src/core/hooks/runner.ts @@ -4,6 +4,7 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { Model } from "@mariozechner/pi-ai"; +import { theme } from "../../modes/interactive/theme/theme.js"; import type { ModelRegistry } from "../model-registry.js"; import type { SessionManager } from "../session-manager.js"; import type { AppendEntryHandler, LoadedHook, SendMessageHandler } from "./loader.js"; @@ -43,6 +44,9 @@ const noOpUIContext: HookUIContext = { custom: async () => undefined as never, setEditorText: () => {}, getEditorText: () => "", + get theme() { + return theme; + }, }; /** diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 693ceff0..3328d860 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -112,6 +112,16 @@ export interface HookUIContext { * @returns Current editor text */ getEditorText(): string; + + /** + * Get the current theme for styling text with ANSI codes. + * Use theme.fg() and theme.bg() to style status text. + * + * @example + * const theme = ctx.ui.theme; + * ctx.ui.setStatus("my-hook", theme.fg("success", "✓") + " Ready"); + */ + readonly theme: Theme; } /** diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 00b03a00..a3d260d2 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -375,6 +375,9 @@ export class InteractiveMode { custom: (factory) => this.showHookCustom(factory), setEditorText: (text) => this.editor.setText(text), getEditorText: () => this.editor.getText(), + get theme() { + return theme; + }, }; this.setToolUIContext(uiContext, true); diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index ba274421..ff1ebec7 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -15,6 +15,7 @@ import * as crypto from "node:crypto"; import * as readline from "readline"; import type { AgentSession } from "../../core/agent-session.js"; import type { HookUIContext } from "../../core/hooks/index.js"; +import { theme } from "../interactive/theme/theme.js"; import type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types.js"; // Re-export types for consumers @@ -150,6 +151,10 @@ export async function runRpcMode(session: AgentSession): Promise { // Host should track editor state locally if needed return ""; }, + + get theme() { + return theme; + }, }); // Set up hooks with RPC-based UI context diff --git a/packages/coding-agent/test/compaction-hooks.test.ts b/packages/coding-agent/test/compaction-hooks.test.ts index 09d47a90..d5bcbb15 100644 --- a/packages/coding-agent/test/compaction-hooks.test.ts +++ b/packages/coding-agent/test/compaction-hooks.test.ts @@ -21,6 +21,7 @@ import { ModelRegistry } from "../src/core/model-registry.js"; import { SessionManager } from "../src/core/session-manager.js"; import { SettingsManager } from "../src/core/settings-manager.js"; import { codingTools } from "../src/core/tools/index.js"; +import { theme } from "../src/modes/interactive/theme/theme.js"; const API_KEY = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY; @@ -112,6 +113,9 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { custom: async () => undefined as never, setEditorText: () => {}, getEditorText: () => "", + get theme() { + return theme; + }, }, hasUI: false, });