mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +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);
|
||||
if (!component) {
|
||||
return undefined;
|
||||
}
|
||||
const lines = component.render(width);
|
||||
return ansiLinesToHtml(lines);
|
||||
} catch {
|
||||
|
|
@ -79,6 +82,9 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
|
|||
|
||||
// Always render expanded, client-side will apply truncation
|
||||
const component = toolDef.renderResult(agentToolResult, { expanded: true, isPartial: false }, theme);
|
||||
if (!component) {
|
||||
return undefined;
|
||||
}
|
||||
const lines = component.render(width);
|
||||
return ansiLinesToHtml(lines);
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -356,10 +356,14 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
|||
): Promise<AgentToolResult<TDetails>>;
|
||||
|
||||
/** 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 */
|
||||
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();
|
||||
// Incremental syntax highlighting cache for write tool call args
|
||||
private writeHighlightCache?: WriteHighlightCache;
|
||||
// When true, this component intentionally renders no lines
|
||||
private hideComponent = false;
|
||||
|
||||
constructor(
|
||||
toolName: string,
|
||||
|
|
@ -354,6 +356,13 @@ export class ToolExecutionComponent extends Container {
|
|||
this.updateDisplay();
|
||||
}
|
||||
|
||||
override render(width: number): string[] {
|
||||
if (this.hideComponent) {
|
||||
return [];
|
||||
}
|
||||
return super.render(width);
|
||||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
// Set background based on state
|
||||
const bgFn = this.isPartial
|
||||
|
|
@ -362,8 +371,12 @@ export class ToolExecutionComponent extends Container {
|
|||
? (text: string) => theme.bg("toolErrorBg", 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)
|
||||
if (this.shouldUseBuiltInRenderer()) {
|
||||
if (useBuiltInRenderer) {
|
||||
if (this.toolName === "bash") {
|
||||
// Bash uses Box with visual line truncation
|
||||
this.contentBox.setBgFn(bgFn);
|
||||
|
|
@ -383,16 +396,19 @@ export class ToolExecutionComponent extends Container {
|
|||
if (this.toolDefinition.renderCall) {
|
||||
try {
|
||||
const callComponent = this.toolDefinition.renderCall(this.args, theme);
|
||||
if (callComponent) {
|
||||
if (callComponent !== undefined) {
|
||||
this.contentBox.addChild(callComponent);
|
||||
customRendererHasContent = true;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to default on error
|
||||
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
|
||||
customRendererHasContent = true;
|
||||
}
|
||||
} else {
|
||||
// No custom renderCall, show tool name
|
||||
this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0));
|
||||
customRendererHasContent = true;
|
||||
}
|
||||
|
||||
// Render result component if we have a result
|
||||
|
|
@ -403,14 +419,16 @@ export class ToolExecutionComponent extends Container {
|
|||
{ expanded: this.expanded, isPartial: this.isPartial },
|
||||
theme,
|
||||
);
|
||||
if (resultComponent) {
|
||||
if (resultComponent !== undefined) {
|
||||
this.contentBox.addChild(resultComponent);
|
||||
customRendererHasContent = true;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to showing raw output on error
|
||||
const output = this.getTextOutput();
|
||||
if (output) {
|
||||
this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0));
|
||||
customRendererHasContent = true;
|
||||
}
|
||||
}
|
||||
} else if (this.result) {
|
||||
|
|
@ -418,8 +436,13 @@ export class ToolExecutionComponent extends Container {
|
|||
const output = this.getTextOutput();
|
||||
if (output) {
|
||||
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)
|
||||
|
|
@ -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) {
|
||||
if (content.type === "toolCall") {
|
||||
if (!this.pendingTools.has(content.id)) {
|
||||
this.chatContainer.addChild(new Text("", 0, 0));
|
||||
const component = new ToolExecutionComponent(
|
||||
content.name,
|
||||
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