diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a7c2251db..e5cd1676a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -17,6 +17,8 @@ ### Added +- Exposed `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL` to commands run by built-in and factory-created bash tools. +- Exposed the `outputPad` setting to custom message renderers. - Added inherited `Tool.constrainedSampling` with strict JSON Schema (`prefer`/`require`) and OpenAI Lark/regex grammar variants across OpenAI, Anthropic, Amazon Bedrock, Google Gemini, and Mistral. See [Constrained Sampling for Tools](../ai/README.md#constrained-sampling-for-tools). - Added inherited `supportsGrammarTools` and `supportsStrictTools` compatibility flags, expanded `supportsStrictMode` coverage, and generated model capability metadata to gate constrained sampling. - Added inherited Kimi Code subscription OAuth login for the Kimi For Coding provider, including device authorization and automatic token refresh ([#6935](https://github.com/earendil-works/pi/pull/6935) by [@zaycruz](https://github.com/zaycruz)). diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 1622ca6fa..aeb5e866a 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -2791,7 +2791,7 @@ Register a custom renderer for messages with your `customType`. Use message rend import { Text } from "@earendil-works/pi-tui"; pi.registerMessageRenderer("my-extension", (message, options, theme) => { - const { expanded } = options; + const { expanded, outputPad } = options; let text = theme.fg("accent", `[${message.customType}] `); text += message.content; @@ -2799,7 +2799,7 @@ pi.registerMessageRenderer("my-extension", (message, options, theme) => { text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2)); } - return new Text(text, 0, 0); + return new Text(text, outputPad, 0); }); ``` diff --git a/packages/coding-agent/examples/extensions/message-renderer.ts b/packages/coding-agent/examples/extensions/message-renderer.ts index 9183d1ec6..953941ee2 100644 --- a/packages/coding-agent/examples/extensions/message-renderer.ts +++ b/packages/coding-agent/examples/extensions/message-renderer.ts @@ -12,7 +12,7 @@ import { Box, Text } from "@earendil-works/pi-tui"; export default function (pi: ExtensionAPI) { // Register custom renderer for "status-update" messages - pi.registerMessageRenderer("status-update", (message, { expanded }, theme) => { + pi.registerMessageRenderer("status-update", (message, { expanded, outputPad }, theme) => { const details = message.details as { level: string; timestamp: number } | undefined; const level = details?.level ?? "info"; @@ -29,7 +29,7 @@ export default function (pi: ExtensionAPI) { } // Use Box with customMessageBg for consistent styling - const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); + const box = new Box(outputPad, 1, (t) => theme.bg("customMessageBg", t)); box.addChild(new Text(text, 0, 0)); return box; }); diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 22b386ba8..c8989a59c 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -1129,6 +1129,8 @@ export interface SessionBeforeTreeResult { export interface MessageRenderOptions { expanded: boolean; + /** Horizontal padding configured by the outputPad setting. */ + outputPad: number; } export interface EntryRenderOptions { 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 a8fffc6f3..b5ee92658 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-message.ts @@ -16,16 +16,19 @@ export class CustomMessageComponent extends Container { private customComponent?: Component; private markdownTheme: MarkdownTheme; private _expanded = false; + private outputPad: number; constructor( message: CustomMessage, customRenderer?: MessageRenderer, markdownTheme: MarkdownTheme = getMarkdownTheme(), + outputPad = 1, ) { super(); this.message = message; this.customRenderer = customRenderer; this.markdownTheme = markdownTheme; + this.outputPad = outputPad; this.addChild(new Spacer(1)); @@ -42,6 +45,13 @@ export class CustomMessageComponent extends Container { } } + setOutputPad(outputPad: number): void { + if (this.outputPad !== outputPad) { + this.outputPad = outputPad; + this.rebuild(); + } + } + override invalidate(): void { super.invalidate(); this.rebuild(); @@ -58,7 +68,11 @@ export class CustomMessageComponent extends Container { // Try custom renderer first - it handles its own styling if (this.customRenderer) { try { - const component = this.customRenderer(this.message, { expanded: this._expanded }, theme); + const component = this.customRenderer( + this.message, + { expanded: this._expanded, outputPad: this.outputPad }, + theme, + ); if (component) { // Custom renderer provides its own styled component this.customComponent = component; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index de2872dbd..30cd984f7 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3242,7 +3242,12 @@ export class InteractiveMode { case "custom": { if (message.display) { const renderer = this.session.extensionRunner.getMessageRenderer(message.customType); - const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings()); + const component = new CustomMessageComponent( + message, + renderer, + this.getMarkdownThemeWithSettings(), + this.outputPad, + ); component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component); } @@ -4257,7 +4262,11 @@ export class InteractiveMode { this.outputPad = padding; if (this.streamingComponent || this.session.isStreaming) { for (const child of this.chatContainer.children) { - if (child instanceof AssistantMessageComponent || child instanceof UserMessageComponent) { + if ( + child instanceof AssistantMessageComponent || + child instanceof CustomMessageComponent || + child instanceof UserMessageComponent + ) { child.setOutputPad(padding); } } diff --git a/packages/coding-agent/test/custom-message.test.ts b/packages/coding-agent/test/custom-message.test.ts new file mode 100644 index 000000000..cdc598010 --- /dev/null +++ b/packages/coding-agent/test/custom-message.test.ts @@ -0,0 +1,44 @@ +import { Text } from "@earendil-works/pi-tui"; +import { describe, expect, test } from "vitest"; +import type { MessageRenderer, MessageRenderOptions } from "../src/core/extensions/types.ts"; +import type { CustomMessage } from "../src/core/messages.ts"; +import { CustomMessageComponent } from "../src/modes/interactive/components/custom-message.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; +import { stripAnsi } from "../src/utils/ansi.ts"; + +describe("CustomMessageComponent", () => { + test("provides output padding to custom renderers and updates it", () => { + initTheme("dark"); + const optionsSeen: MessageRenderOptions[] = []; + const renderer: MessageRenderer = (_message, options) => { + optionsSeen.push(options); + return new Text("custom", options.outputPad, 0); + }; + const message: CustomMessage = { + role: "custom", + customType: "test", + content: "custom", + display: true, + timestamp: Date.now(), + }; + const component = new CustomMessageComponent(message, renderer, undefined, 1); + + expect(optionsSeen).toEqual([{ expanded: false, outputPad: 1 }]); + expect( + component + .render(40) + .map(stripAnsi) + .some((line) => line.startsWith(" custom")), + ).toBe(true); + + component.setOutputPad(0); + + expect(optionsSeen.at(-1)).toEqual({ expanded: false, outputPad: 0 }); + expect( + component + .render(40) + .map(stripAnsi) + .some((line) => line.startsWith("custom")), + ).toBe(true); + }); +});