feat(coding-agent): expose output padding to custom renderers (#7045)

Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
Alexey Zaytsev 2026-07-25 03:29:34 -03:00 • committed by GitHub
parent 2a2b0a39f0
commit 518855dd50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 78 additions and 7 deletions

View file

@ -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)).

View file

@ -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);
});
```

View file

@ -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;
});

View file

@ -1129,6 +1129,8 @@ export interface SessionBeforeTreeResult {
export interface MessageRenderOptions {
expanded: boolean;
/** Horizontal padding configured by the outputPad setting. */
outputPad: number;
}
export interface EntryRenderOptions {

View file

@ -16,16 +16,19 @@ export class CustomMessageComponent extends Container {
private customComponent?: Component;
private markdownTheme: MarkdownTheme;
private _expanded = false;
private outputPad: number;
constructor(
message: CustomMessage<unknown>,
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;

View file

@ -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);
}
}

View file

@ -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);
});
});