From de022ceba7a96d980d2e0786b06b4e269feb3f21 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 29 Mar 2026 21:33:15 +0200 Subject: [PATCH] feat(coding-agent): add hidden thinking label api closes #2673 --- packages/coding-agent/CHANGELOG.md | 1 + .../examples/extensions/README.md | 1 + .../extensions/hidden-thinking-label.ts | 57 +++++++++++++++++++ .../src/core/extensions/runner.ts | 1 + .../coding-agent/src/core/extensions/types.ts | 3 + .../components/assistant-message.ts | 19 ++++++- .../src/modes/interactive/interactive-mode.ts | 19 +++++++ .../coding-agent/src/modes/rpc/rpc-mode.ts | 4 ++ 8 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/examples/extensions/hidden-thinking-label.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 429ece44..a4370516 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ - Added `ToolDefinition.prepareArguments` hook to prepare raw tool call arguments before schema validation, enabling compatibility shims for resumed sessions with outdated tool schemas - Built-in `edit` tool now uses `prepareArguments` to silently fold legacy top-level `oldText`/`newText` into `edits[]` when resuming old sessions +- Added `ctx.ui.setHiddenThinkingLabel()` so extensions can customize the collapsed thinking label in interactive mode, with a no-op in RPC mode and a runnable example extension in `examples/extensions/hidden-thinking-label.ts` ([#2673](https://github.com/badlogic/pi-mono/issues/2673)) ### Fixed diff --git a/packages/coding-agent/examples/extensions/README.md b/packages/coding-agent/examples/extensions/README.md index 6e9b35f9..e05ed521 100644 --- a/packages/coding-agent/examples/extensions/README.md +++ b/packages/coding-agent/examples/extensions/README.md @@ -52,6 +52,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/ | `qna.ts` | Extracts questions from last response into editor via `ctx.ui.setEditorText()` | | `status-line.ts` | Shows turn progress in footer via `ctx.ui.setStatus()` with themed colors | | `widget-placement.ts` | Shows widgets above and below the editor via `ctx.ui.setWidget()` placement | +| `hidden-thinking-label.ts` | Customizes the collapsed thinking label via `ctx.ui.setHiddenThinkingLabel()` | | `model-status.ts` | Shows model changes in status bar via `model_select` hook | | `snake.ts` | Snake game with custom UI, keyboard handling, and session persistence | | `send-user-message.ts` | Demonstrates `pi.sendUserMessage()` for sending user messages from extensions | diff --git a/packages/coding-agent/examples/extensions/hidden-thinking-label.ts b/packages/coding-agent/examples/extensions/hidden-thinking-label.ts new file mode 100644 index 00000000..1f7ab85b --- /dev/null +++ b/packages/coding-agent/examples/extensions/hidden-thinking-label.ts @@ -0,0 +1,57 @@ +/** + * Hidden Thinking Label Extension + * + * Demonstrates `ctx.ui.setHiddenThinkingLabel()` for customizing the label shown + * when thinking blocks are hidden. + * + * Usage: + * pi --extension examples/extensions/hidden-thinking-label.ts + * + * Test: + * 1. Load this extension + * 2. Hide thinking blocks with Ctrl+T + * 3. Ask for something that produces reasoning output + * 4. The collapsed thinking block label will show the custom text + * + * Commands: + * /thinking-label Set a custom hidden thinking label + * /thinking-label Reset to the default label + */ + +import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; + +const DEFAULT_LABEL = "Pondering..."; + +export default function (pi: ExtensionAPI) { + let label = DEFAULT_LABEL; + + const applyLabel = (ctx: ExtensionContext) => { + ctx.ui.setHiddenThinkingLabel(label); + }; + + pi.on("session_start", async (_event, ctx) => { + applyLabel(ctx); + }); + + pi.on("session_switch", async (_event, ctx) => { + applyLabel(ctx); + }); + + pi.registerCommand("thinking-label", { + description: "Set the hidden thinking label. Use without args to reset.", + handler: async (args, ctx) => { + const nextLabel = args.trim(); + + if (!nextLabel) { + label = DEFAULT_LABEL; + ctx.ui.setHiddenThinkingLabel(); + ctx.ui.notify(`Hidden thinking label reset to: ${DEFAULT_LABEL}`); + return; + } + + label = nextLabel; + ctx.ui.setHiddenThinkingLabel(label); + ctx.ui.notify(`Hidden thinking label set to: ${label}`); + }, + }); +} diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index a44b20ea..f2257faa 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -178,6 +178,7 @@ const noOpUIContext: ExtensionUIContext = { onTerminalInput: () => () => {}, setStatus: () => {}, setWorkingMessage: () => {}, + setHiddenThinkingLabel: () => {}, setWidget: () => {}, setFooter: () => {}, setHeader: () => {}, diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 6ede5f95..710d4a66 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -127,6 +127,9 @@ export interface ExtensionUIContext { /** Set the working/loading message shown during streaming. Call with no argument to restore default. */ setWorkingMessage(message?: string): void; + /** Set the label shown for hidden thinking blocks. Call with no argument to restore default. */ + setHiddenThinkingLabel(label?: string): void; + /** Set a widget to display above or below the editor. Accepts string array or component factory. */ setWidget(key: string, content: string[] | undefined, options?: ExtensionWidgetOptions): void; setWidget( diff --git a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts index ae5256c6..6d6893a9 100644 --- a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts @@ -9,17 +9,20 @@ export class AssistantMessageComponent extends Container { private contentContainer: Container; private hideThinkingBlock: boolean; private markdownTheme: MarkdownTheme; + private hiddenThinkingLabel: string; private lastMessage?: AssistantMessage; constructor( message?: AssistantMessage, hideThinkingBlock = false, markdownTheme: MarkdownTheme = getMarkdownTheme(), + hiddenThinkingLabel = "Thinking...", ) { super(); this.hideThinkingBlock = hideThinkingBlock; this.markdownTheme = markdownTheme; + this.hiddenThinkingLabel = hiddenThinkingLabel; // Container for text/thinking content this.contentContainer = new Container(); @@ -39,6 +42,16 @@ export class AssistantMessageComponent extends Container { setHideThinkingBlock(hide: boolean): void { this.hideThinkingBlock = hide; + if (this.lastMessage) { + this.updateContent(this.lastMessage); + } + } + + setHiddenThinkingLabel(label: string): void { + this.hiddenThinkingLabel = label; + if (this.lastMessage) { + this.updateContent(this.lastMessage); + } } updateContent(message: AssistantMessage): void { @@ -70,8 +83,10 @@ export class AssistantMessageComponent extends Container { .some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim())); if (this.hideThinkingBlock) { - // Show static "Thinking..." label when hidden - this.contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0)); + // Show static thinking label when hidden + this.contentContainer.addChild( + new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), 1, 0), + ); if (hasVisibleContentAfter) { this.contentContainer.addChild(new Spacer(1)); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 7c933e67..48188120 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -164,6 +164,8 @@ export class InteractiveMode { private loadingAnimation: Loader | undefined = undefined; private pendingWorkingMessage: string | undefined = undefined; private readonly defaultWorkingMessage = "Working..."; + private readonly defaultHiddenThinkingLabel = "Thinking..."; + private hiddenThinkingLabel = this.defaultHiddenThinkingLabel; private lastSigintTime = 0; private lastEscapeTime = 0; @@ -1325,6 +1327,19 @@ export class InteractiveMode { this.ui.requestRender(); } + private setHiddenThinkingLabel(label?: string): void { + this.hiddenThinkingLabel = label ?? this.defaultHiddenThinkingLabel; + for (const child of this.chatContainer.children) { + if (child instanceof AssistantMessageComponent) { + child.setHiddenThinkingLabel(this.hiddenThinkingLabel); + } + } + if (this.streamingComponent) { + this.streamingComponent.setHiddenThinkingLabel(this.hiddenThinkingLabel); + } + this.ui.requestRender(); + } + /** * Set an extension widget (string array or custom component). */ @@ -1405,6 +1420,7 @@ export class InteractiveMode { if (this.loadingAnimation) { this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`); } + this.setHiddenThinkingLabel(); } // Maximum total widget lines to prevent viewport overflow @@ -1557,6 +1573,7 @@ export class InteractiveMode { this.pendingWorkingMessage = message; } }, + setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label), setWidget: (key, content, options) => this.setExtensionWidget(key, content, options), setFooter: (factory) => this.setExtensionFooter(factory), setHeader: (factory) => this.setExtensionHeader(factory), @@ -2262,6 +2279,7 @@ export class InteractiveMode { undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), + this.hiddenThinkingLabel, ); this.streamingMessage = event.message; this.chatContainer.addChild(this.streamingComponent); @@ -2617,6 +2635,7 @@ export class InteractiveMode { message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), + this.hiddenThinkingLabel, ); this.chatContainer.addChild(assistantComponent); break; diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 2529689c..e36d4fae 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -167,6 +167,10 @@ export async function runRpcMode(session: AgentSession): Promise { // Working message not supported in RPC mode - requires TUI loader access }, + setHiddenThinkingLabel(_label?: string): void { + // Hidden thinking label not supported in RPC mode - requires TUI message rendering access + }, + setWidget(key: string, content: unknown, options?: ExtensionWidgetOptions): void { // Only support string arrays in RPC mode - factory functions are ignored if (content === undefined || Array.isArray(content)) {