Merge a7eb6c58ab1d6a65607f57a870c44c601b24953c into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Liu Ricardo 2026-03-20 20:13:24 -07:00 committed by GitHub
commit 6330af45b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 143 additions and 17 deletions

View File

@ -31,7 +31,42 @@ describe("chat markdown rendering", () => {
await app.updateComplete;
const strong = app.querySelector(".sidebar-markdown strong");
expect(strong?.textContent).toBe("world");
const strongNodes = Array.from(app.querySelectorAll(".sidebar-markdown strong"));
expect(strongNodes.map((node) => node.textContent)).toContain("world");
});
it("shows tool call request parameters in the sidebar", async () => {
const app = mountApp("/chat");
await app.updateComplete;
const timestamp = Date.now();
app.chatMessages = [
{
role: "assistant",
content: [
{
type: "toolcall",
name: "sessions_spawn",
arguments: { agentId: "research", prompt: "hello" },
},
],
timestamp,
},
];
await app.updateComplete;
const toolCard = app.querySelector<HTMLElement>(".chat-tool-card");
expect(toolCard).not.toBeNull();
toolCard?.click();
await app.updateComplete;
const sidebar = app.querySelector(".sidebar-markdown");
expect(sidebar?.textContent).toContain("Arguments");
expect(sidebar?.textContent).toContain("agentId");
expect(sidebar?.textContent).toContain("research");
expect(sidebar?.textContent).toContain("prompt");
expect(sidebar?.textContent).toContain("hello");
});
});

View File

@ -5,7 +5,7 @@ import type { ToolCard } from "../types/chat-types.ts";
import { TOOL_INLINE_THRESHOLD } from "./constants.ts";
import { extractTextCached } from "./message-extract.ts";
import { isToolResultMessage } from "./message-normalizer.ts";
import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers.ts";
import { buildToolSidebarContent, getTruncatedPreview } from "./tool-helpers.ts";
export function extractToolCards(message: unknown): ToolCard[] {
const m = message as Record<string, unknown>;
@ -56,14 +56,14 @@ export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content:
const canClick = Boolean(onOpenSidebar);
const handleClick = canClick
? () => {
if (hasText) {
onOpenSidebar!(formatToolOutputForSidebar(card.text!));
return;
}
const info = `## ${display.label}\n\n${
detail ? `**Command:** \`${detail}\`\n\n` : ""
}*No output tool completed successfully.*`;
onOpenSidebar!(info);
onOpenSidebar!(
buildToolSidebarContent({
title: display.label,
detail,
args: card.args,
output: card.text,
}),
);
}
: undefined;
@ -95,11 +95,7 @@ export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content:
<span class="chat-tool-card__icon">${icons[display.icon]}</span>
<span>${display.label}</span>
</div>
${
canClick
? html`<span class="chat-tool-card__action">${hasText ? "View" : ""} ${icons.check}</span>`
: nothing
}
${canClick ? html`<span class="chat-tool-card__action">View ${icons.check}</span>` : nothing}
${isEmpty && !canClick ? html`<span class="chat-tool-card__status">${icons.check}</span>` : nothing}
</div>
${detail ? html`<div class="chat-tool-card__detail">${detail}</div>` : nothing}

View File

@ -1,5 +1,10 @@
import { describe, it, expect } from "vitest";
import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers.ts";
import {
buildToolSidebarContent,
formatToolOutputForSidebar,
formatToolPayloadForSidebar,
getTruncatedPreview,
} from "./tool-helpers.ts";
describe("tool-helpers", () => {
describe("formatToolOutputForSidebar", () => {
@ -138,4 +143,54 @@ describe("tool-helpers", () => {
expect(result.endsWith("…")).toBe(true);
});
});
describe("formatToolPayloadForSidebar", () => {
it("formats structured arguments as JSON", () => {
const result = formatToolPayloadForSidebar({ agentId: "research", count: 2 });
expect(result).toBe(`\`\`\`json
{
"agentId": "research",
"count": 2
}
\`\`\``);
});
it("returns null for undefined arguments", () => {
expect(formatToolPayloadForSidebar(undefined)).toBeNull();
});
it("returns null for null arguments", () => {
expect(formatToolPayloadForSidebar(null)).toBeNull();
});
});
describe("buildToolSidebarContent", () => {
it("includes arguments and output when both are present", () => {
const result = buildToolSidebarContent({
title: "Session Spawn",
detail: "sessions_spawn research",
args: { agentId: "research" },
output: '{"status":"ok"}',
});
expect(result).toContain("## Session Spawn");
expect(result).toContain("**Command:** `sessions_spawn research`");
expect(result).toContain("**Arguments**");
expect(result).toContain('"agentId": "research"');
expect(result).toContain("**Output**");
expect(result).toContain('"status": "ok"');
});
it("shows completion text when output is absent", () => {
const result = buildToolSidebarContent({
title: "Session Spawn",
args: { agentId: "research" },
});
expect(result).toContain("**Arguments**");
expect(result).toContain('"agentId": "research"');
expect(result).toContain("*No output - tool completed successfully.*");
});
});
});

View File

@ -22,6 +22,46 @@ export function formatToolOutputForSidebar(text: string): string {
return text;
}
/**
* Format tool-call input/arguments for display in the sidebar.
* Uses JSON code blocks for structured values and preserves plain text input.
*/
export function formatToolPayloadForSidebar(value: unknown): string | null {
if (value === undefined || value === null) {
return null;
}
if (typeof value === "string") {
return formatToolOutputForSidebar(value);
}
try {
return "```json\n" + JSON.stringify(value, null, 2) + "\n```";
} catch {
return String(value);
}
}
export function buildToolSidebarContent(params: {
title: string;
detail?: string;
args?: unknown;
output?: string;
}): string {
const sections = [`## ${params.title}`];
if (params.detail) {
sections.push(`**Command:** \`${params.detail}\``);
}
const args = formatToolPayloadForSidebar(params.args);
if (args !== null) {
sections.push(`**Arguments**\n${args}`);
}
if (typeof params.output === "string") {
sections.push(`**Output**\n${formatToolOutputForSidebar(params.output)}`);
} else {
sections.push("*No output - tool completed successfully.*");
}
return sections.join("\n\n");
}
/**
* Get a truncated preview of tool output text.
* Truncates to first N lines or first N characters, whichever is shorter.