mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 05:00:16 +00:00
merge: PR #1719 for local testing
This commit is contained in:
commit
7b7b967aef
5 changed files with 114 additions and 6 deletions
|
|
@ -49,6 +49,9 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
|
||||||
}
|
}
|
||||||
|
|
||||||
const component = toolDef.renderCall(args, theme);
|
const component = toolDef.renderCall(args, theme);
|
||||||
|
if (!component) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const lines = component.render(width);
|
const lines = component.render(width);
|
||||||
return ansiLinesToHtml(lines);
|
return ansiLinesToHtml(lines);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -79,6 +82,9 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
|
||||||
|
|
||||||
// Always render expanded, client-side will apply truncation
|
// Always render expanded, client-side will apply truncation
|
||||||
const component = toolDef.renderResult(agentToolResult, { expanded: true, isPartial: false }, theme);
|
const component = toolDef.renderResult(agentToolResult, { expanded: true, isPartial: false }, theme);
|
||||||
|
if (!component) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const lines = component.render(width);
|
const lines = component.render(width);
|
||||||
return ansiLinesToHtml(lines);
|
return ansiLinesToHtml(lines);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -356,10 +356,14 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
||||||
): Promise<AgentToolResult<TDetails>>;
|
): Promise<AgentToolResult<TDetails>>;
|
||||||
|
|
||||||
/** Custom rendering for tool call display */
|
/** Custom rendering for tool call display */
|
||||||
renderCall?: (args: Static<TParams>, theme: Theme) => Component;
|
renderCall?: (args: Static<TParams>, theme: Theme) => Component | undefined;
|
||||||
|
|
||||||
/** Custom rendering for tool result display */
|
/** Custom rendering for tool result display */
|
||||||
renderResult?: (result: AgentToolResult<TDetails>, options: ToolRenderResultOptions, theme: Theme) => Component;
|
renderResult?: (
|
||||||
|
result: AgentToolResult<TDetails>,
|
||||||
|
options: ToolRenderResultOptions,
|
||||||
|
theme: Theme,
|
||||||
|
) => Component | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ export class ToolExecutionComponent extends Container {
|
||||||
private convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
private convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
|
||||||
// Incremental syntax highlighting cache for write tool call args
|
// Incremental syntax highlighting cache for write tool call args
|
||||||
private writeHighlightCache?: WriteHighlightCache;
|
private writeHighlightCache?: WriteHighlightCache;
|
||||||
|
// When true, this component intentionally renders no lines
|
||||||
|
private hideComponent = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
toolName: string,
|
toolName: string,
|
||||||
|
|
@ -354,6 +356,13 @@ export class ToolExecutionComponent extends Container {
|
||||||
this.updateDisplay();
|
this.updateDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override render(width: number): string[] {
|
||||||
|
if (this.hideComponent) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return super.render(width);
|
||||||
|
}
|
||||||
|
|
||||||
private updateDisplay(): void {
|
private updateDisplay(): void {
|
||||||
// Set background based on state
|
// Set background based on state
|
||||||
const bgFn = this.isPartial
|
const bgFn = this.isPartial
|
||||||
|
|
@ -362,8 +371,12 @@ export class ToolExecutionComponent extends Container {
|
||||||
? (text: string) => theme.bg("toolErrorBg", text)
|
? (text: string) => theme.bg("toolErrorBg", text)
|
||||||
: (text: string) => theme.bg("toolSuccessBg", text);
|
: (text: string) => theme.bg("toolSuccessBg", text);
|
||||||
|
|
||||||
|
const useBuiltInRenderer = this.shouldUseBuiltInRenderer();
|
||||||
|
let customRendererHasContent = false;
|
||||||
|
this.hideComponent = false;
|
||||||
|
|
||||||
// Use built-in rendering for built-in tools (or overrides without custom renderers)
|
// Use built-in rendering for built-in tools (or overrides without custom renderers)
|
||||||
if (this.shouldUseBuiltInRenderer()) {
|
if (useBuiltInRenderer) {
|
||||||
if (this.toolName === "bash") {
|
if (this.toolName === "bash") {
|
||||||
// Bash uses Box with visual line truncation
|
// Bash uses Box with visual line truncation
|
||||||
this.contentBox.setBgFn(bgFn);
|
this.contentBox.setBgFn(bgFn);
|
||||||
|
|
@ -383,16 +396,19 @@ export class ToolExecutionComponent extends Container {
|
||||||
if (this.toolDefinition.renderCall) {
|
if (this.toolDefinition.renderCall) {
|
||||||
try {
|
try {
|
||||||
const callComponent = this.toolDefinition.renderCall(this.args, theme);
|
const callComponent = this.toolDefinition.renderCall(this.args, theme);
|
||||||
if (callComponent) {
|
if (callComponent !== undefined) {
|
||||||
this.contentBox.addChild(callComponent);
|
this.contentBox.addChild(callComponent);
|
||||||
|
customRendererHasContent = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to default on error
|
// Fall back to default on error
|
||||||
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
|
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
|
||||||
|
customRendererHasContent = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No custom renderCall, show tool name
|
// No custom renderCall, show tool name
|
||||||
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
|
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
|
||||||
|
customRendererHasContent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render result component if we have a result
|
// Render result component if we have a result
|
||||||
|
|
@ -403,14 +419,16 @@ export class ToolExecutionComponent extends Container {
|
||||||
{ expanded: this.expanded, isPartial: this.isPartial },
|
{ expanded: this.expanded, isPartial: this.isPartial },
|
||||||
theme,
|
theme,
|
||||||
);
|
);
|
||||||
if (resultComponent) {
|
if (resultComponent !== undefined) {
|
||||||
this.contentBox.addChild(resultComponent);
|
this.contentBox.addChild(resultComponent);
|
||||||
|
customRendererHasContent = true;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to showing raw output on error
|
// Fall back to showing raw output on error
|
||||||
const output = this.getTextOutput();
|
const output = this.getTextOutput();
|
||||||
if (output) {
|
if (output) {
|
||||||
this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
||||||
|
customRendererHasContent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.result) {
|
} else if (this.result) {
|
||||||
|
|
@ -418,8 +436,13 @@ export class ToolExecutionComponent extends Container {
|
||||||
const output = this.getTextOutput();
|
const output = this.getTextOutput();
|
||||||
if (output) {
|
if (output) {
|
||||||
this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
||||||
|
customRendererHasContent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown tool with no registered definition - show generic fallback
|
||||||
|
this.contentText.setCustomBgFn(bgFn);
|
||||||
|
this.contentText.setText(this.formatToolExecution());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle images (same for both custom and built-in)
|
// Handle images (same for both custom and built-in)
|
||||||
|
|
@ -463,6 +486,10 @@ export class ToolExecutionComponent extends Container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!useBuiltInRenderer && this.toolDefinition) {
|
||||||
|
this.hideComponent = !customRendererHasContent && this.imageComponents.length === 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2109,7 +2109,6 @@ export class InteractiveMode {
|
||||||
for (const content of this.streamingMessage.content) {
|
for (const content of this.streamingMessage.content) {
|
||||||
if (content.type === "toolCall") {
|
if (content.type === "toolCall") {
|
||||||
if (!this.pendingTools.has(content.id)) {
|
if (!this.pendingTools.has(content.id)) {
|
||||||
this.chatContainer.addChild(new Text("", 0, 0));
|
|
||||||
const component = new ToolExecutionComponent(
|
const component = new ToolExecutionComponent(
|
||||||
content.name,
|
content.name,
|
||||||
content.arguments,
|
content.arguments,
|
||||||
|
|
|
||||||
72
packages/coding-agent/test/tool-execution-component.test.ts
Normal file
72
packages/coding-agent/test/tool-execution-component.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { Text, type TUI } from "@mariozechner/pi-tui";
|
||||||
|
import { Type } from "@sinclair/typebox";
|
||||||
|
import stripAnsi from "strip-ansi";
|
||||||
|
import { beforeAll, describe, expect, test } from "vitest";
|
||||||
|
import type { ToolDefinition } from "../src/core/extensions/types.js";
|
||||||
|
import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.js";
|
||||||
|
import { initTheme } from "../src/modes/interactive/theme/theme.js";
|
||||||
|
|
||||||
|
function createBaseToolDefinition(): ToolDefinition {
|
||||||
|
return {
|
||||||
|
name: "custom_tool",
|
||||||
|
label: "custom_tool",
|
||||||
|
description: "custom tool",
|
||||||
|
parameters: Type.Any(),
|
||||||
|
execute: async () => ({
|
||||||
|
content: [{ type: "text", text: "ok" }],
|
||||||
|
details: {},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFakeTui(): TUI {
|
||||||
|
return {
|
||||||
|
requestRender: () => {},
|
||||||
|
} as unknown as TUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ToolExecutionComponent custom renderer suppression", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
initTheme("dark");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders no lines when custom renderers return undefined", () => {
|
||||||
|
const toolDefinition: ToolDefinition = {
|
||||||
|
...createBaseToolDefinition(),
|
||||||
|
renderCall: () => undefined,
|
||||||
|
renderResult: () => undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const component = new ToolExecutionComponent("custom_tool", {}, {}, toolDefinition, createFakeTui());
|
||||||
|
expect(component.render(120)).toEqual([]);
|
||||||
|
|
||||||
|
component.updateResult(
|
||||||
|
{
|
||||||
|
content: [{ type: "text", text: "hidden" }],
|
||||||
|
details: {},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component.render(120)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps built-in tool rendering visible", () => {
|
||||||
|
const component = new ToolExecutionComponent("read", { path: "README.md" }, {}, undefined, createFakeTui());
|
||||||
|
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||||
|
expect(rendered).toContain("read");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps custom tool rendering visible when renderer returns a component", () => {
|
||||||
|
const toolDefinition: ToolDefinition = {
|
||||||
|
...createBaseToolDefinition(),
|
||||||
|
renderCall: () => new Text("custom call", 0, 0),
|
||||||
|
renderResult: () => undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const component = new ToolExecutionComponent("custom_tool", {}, {}, toolDefinition, createFakeTui());
|
||||||
|
const rendered = stripAnsi(component.render(120).join("\n"));
|
||||||
|
expect(rendered).toContain("custom call");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue