From 5cb7af1ddcc688052bebbc42e40cda0eec3272a9 Mon Sep 17 00:00:00 2001 From: Aliou Diallo Date: Sun, 4 Jan 2026 20:55:52 +0100 Subject: [PATCH] feat(hooks): expose setTitle to HookUIContext (#446) --- packages/coding-agent/CHANGELOG.md | 6 +++++- packages/coding-agent/docs/hooks.md | 7 +++++++ packages/coding-agent/src/core/custom-tools/loader.ts | 1 + packages/coding-agent/src/core/hooks/runner.ts | 1 + packages/coding-agent/src/core/hooks/types.ts | 7 +++++++ .../src/modes/interactive/interactive-mode.ts | 1 + packages/coding-agent/src/modes/rpc/rpc-mode.ts | 10 ++++++++++ packages/coding-agent/src/modes/rpc/rpc-types.ts | 1 + packages/coding-agent/test/compaction-hooks.test.ts | 1 + 9 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 40e942e3..6cec0dfa 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Hook API: `ctx.ui.setTitle(title)` allows hooks to set the terminal window/tab title ([#446](https://github.com/badlogic/pi-mono/pull/446) by [@aliou](https://github.com/aliou)) + ### Changed - Expanded keybinding documentation to list all 32 supported symbol keys with notes on ctrl+symbol behavior ([#450](https://github.com/badlogic/pi-mono/pull/450) by [@kaofelix](https://github.com/kaofelix)) @@ -1341,4 +1345,4 @@ Initial public release. - Git branch display in footer - Message queueing during streaming responses - OAuth integration for Gmail and Google Calendar access -- HTML export with syntax highlighting and collapsible sections \ No newline at end of file +- HTML export with syntax highlighting and collapsible sections diff --git a/packages/coding-agent/docs/hooks.md b/packages/coding-agent/docs/hooks.md index b7ece86e..23d585c1 100644 --- a/packages/coding-agent/docs/hooks.md +++ b/packages/coding-agent/docs/hooks.md @@ -437,6 +437,9 @@ ctx.ui.setWidget("my-todos", [ ]); ctx.ui.setWidget("my-todos", undefined); // Clear widget +// Set the terminal window/tab title +ctx.ui.setTitle("pi - my-project"); + // Set the core input editor text (pre-fill prompts, generated content) ctx.ui.setEditorText("Generated prompt text here..."); @@ -457,6 +460,10 @@ const currentText = ctx.ui.getEditorText(); - Supports ANSI styling via `ctx.ui.theme` (including `strikethrough`) - **Caution:** Keep widgets small (a few lines). Large widgets from multiple hooks can cause viewport overflow and TUI flicker. Max 10 lines total across all string widgets. +**Terminal title notes:** +- Uses OSC escape sequence (works in most modern terminals like iTerm2, Terminal.app, Windows Terminal) +- Useful for showing project name, current task, or session status in the terminal tab/window title + **Custom widget components:** For more complex widgets, pass a factory function to `setWidget()`: diff --git a/packages/coding-agent/src/core/custom-tools/loader.ts b/packages/coding-agent/src/core/custom-tools/loader.ts index 1f72c41e..a3a0d6ee 100644 --- a/packages/coding-agent/src/core/custom-tools/loader.ts +++ b/packages/coding-agent/src/core/custom-tools/loader.ts @@ -93,6 +93,7 @@ function createNoOpUIContext(): HookUIContext { notify: () => {}, setStatus: () => {}, setWidget: () => {}, + setTitle: () => {}, custom: async () => undefined as never, setEditorText: () => {}, getEditorText: () => "", diff --git a/packages/coding-agent/src/core/hooks/runner.ts b/packages/coding-agent/src/core/hooks/runner.ts index 1e31c296..57f55346 100644 --- a/packages/coding-agent/src/core/hooks/runner.ts +++ b/packages/coding-agent/src/core/hooks/runner.ts @@ -59,6 +59,7 @@ const noOpUIContext: HookUIContext = { notify: () => {}, setStatus: () => {}, setWidget: () => {}, + setTitle: () => {}, custom: async () => undefined as never, setEditorText: () => {}, getEditorText: () => "", diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 22d6428c..0a817b1b 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -120,6 +120,13 @@ export interface HookUIContext { */ setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void; + /** + * Set the terminal window/tab title. + * Uses OSC escape sequence (works in most modern terminals). + * @param title - Title text to display + */ + setTitle(title: string): void; + /** * Show a custom component with keyboard focus. * The factory receives TUI, theme, and a done() callback to close the component. diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index e944bcc9..1b331d43 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -685,6 +685,7 @@ export class InteractiveMode { notify: (message, type) => this.showHookNotify(message, type), setStatus: (key, text) => this.setHookStatus(key, text), setWidget: (key, content) => this.setHookWidget(key, content), + setTitle: (title) => this.ui.terminal.setTitle(title), custom: (factory) => this.showHookCustom(factory), setEditorText: (text) => this.editor.setText(text), getEditorText: () => this.editor.getText(), diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 1bc4ef10..8f158cee 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -145,6 +145,16 @@ export async function runRpcMode(session: AgentSession): Promise { // Component factories are not supported in RPC mode - would need TUI access }, + setTitle(title: string): void { + // Fire and forget - host can implement terminal title control + output({ + type: "hook_ui_request", + id: crypto.randomUUID(), + method: "setTitle", + title, + } as RpcHookUIRequest); + }, + async custom() { // Custom UI not supported in RPC mode return undefined as never; diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index 172b745e..8866b6c9 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -196,6 +196,7 @@ export type RpcHookUIRequest = widgetKey: string; widgetLines: string[] | undefined; } + | { type: "hook_ui_request"; id: string; method: "setTitle"; title: string } | { type: "hook_ui_request"; id: string; method: "set_editor_text"; text: string }; // ============================================================================ diff --git a/packages/coding-agent/test/compaction-hooks.test.ts b/packages/coding-agent/test/compaction-hooks.test.ts index 99f2c1ab..03e6ac26 100644 --- a/packages/coding-agent/test/compaction-hooks.test.ts +++ b/packages/coding-agent/test/compaction-hooks.test.ts @@ -121,6 +121,7 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { notify: () => {}, setStatus: () => {}, setWidget: () => {}, + setTitle: () => {}, custom: async () => undefined as never, setEditorText: () => {}, getEditorText: () => "",