From f646a29d1ab75a6cc0bf5b81cf912e6ea135a832 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 8 Oct 2025 13:38:45 +0200 Subject: [PATCH] Add collapsible tool renderers with animated expand/collapse - Add renderCollapsibleHeader() to renderer-registry - Places chevron on right, spinner on left - Toggles between ChevronRight (collapsed) and ChevronDown (expanded) - Uses max-h-0/max-h-[2000px] with transition-all for smooth animation - Dynamically adds/removes mt-3 to avoid margin when collapsed - Update javascript-repl renderer to use collapsible sections - Code and console output hidden by default - Only file attachments remain visible - 300ms smooth animation on expand/collapse - Export renderCollapsibleHeader from web-ui index --- packages/web-ui/src/index.ts | 2 +- packages/web-ui/src/tools/javascript-repl.ts | 60 ++++++++++-------- .../web-ui/src/tools/renderer-registry.ts | 62 ++++++++++++++++++- 3 files changed, 95 insertions(+), 29 deletions(-) diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index f91bd6c4..c3068a47 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -69,7 +69,7 @@ export { TextArtifact } from "./tools/artifacts/TextArtifact.js"; // Tools export { getToolRenderer, registerToolRenderer, renderTool } from "./tools/index.js"; export { createJavaScriptReplTool, javascriptReplTool } from "./tools/javascript-repl.js"; -export { renderHeader } from "./tools/renderer-registry.js"; +export { renderCollapsibleHeader, renderHeader } from "./tools/renderer-registry.js"; export { BashRenderer } from "./tools/renderers/BashRenderer.js"; export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.js"; // Tool renderers diff --git a/packages/web-ui/src/tools/javascript-repl.ts b/packages/web-ui/src/tools/javascript-repl.ts index 02efc3b5..b5329493 100644 --- a/packages/web-ui/src/tools/javascript-repl.ts +++ b/packages/web-ui/src/tools/javascript-repl.ts @@ -1,11 +1,12 @@ import { html, i18n, type TemplateResult } from "@mariozechner/mini-lit"; import type { AgentTool, ToolResultMessage } from "@mariozechner/pi-ai"; import { type Static, Type } from "@sinclair/typebox"; +import { createRef, ref } from "lit/directives/ref.js"; import { Code } from "lucide"; import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js"; import type { Attachment } from "../utils/attachment-utils.js"; -import { registerToolRenderer, renderHeader } from "./renderer-registry.js"; +import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js"; import type { ToolRenderer } from "./types.js"; // Execute JavaScript code with attachments using SandboxedIframe @@ -93,10 +94,25 @@ export type JavaScriptReplToolResult = { }; const javascriptReplSchema = Type.Object({ - title: Type.String({ description: "Brief title describing what the code snippet tries to achieve" }), + title: Type.String({ + description: + "Brief title describing what the code snippet tries to achieve in active form, e.g. 'Calculating sum'", + }), code: Type.String({ description: "JavaScript code to execute" }), }); +export type JavaScriptReplParams = Static; + +interface JavaScriptReplResult { + output?: string; + files?: Array<{ + fileName: string; + mimeType: string; + size: number; + contentBase64: string; + }>; +} + export function createJavaScriptReplTool(): AgentTool & { attachmentsProvider?: () => Attachment[]; sandboxUrlProvider?: () => string; @@ -230,22 +246,6 @@ Global variables: // Export a default instance for backward compatibility export const javascriptReplTool = createJavaScriptReplTool(); -// JavaScript REPL renderer with streaming support - -interface JavaScriptReplParams { - code: string; -} - -interface JavaScriptReplResult { - output?: string; - files?: Array<{ - fileName: string; - mimeType: string; - size: number; - contentBase64: string; - }>; -} - export const javascriptReplRenderer: ToolRenderer = { render( params: JavaScriptReplParams | undefined, @@ -255,6 +255,10 @@ export const javascriptReplRenderer: ToolRenderer(); + const codeChevronRef = createRef(); + // With result: show params + result if (result && params) { const output = result.output || ""; @@ -290,13 +294,15 @@ export const javascriptReplRenderer: ToolRenderer - ${renderHeader(state, Code, i18n("Executing JavaScript"))} - - ${output ? html`` : ""} +
+ ${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)} +
+ + ${output ? html`` : ""} +
${ attachments.length - ? html`
+ ? html`
${attachments.map((att) => html``)}
` : "" @@ -308,9 +314,11 @@ export const javascriptReplRenderer: ToolRenderer - ${renderHeader(state, Code, i18n("Executing JavaScript"))} - ${params.code ? html`` : ""} +
+ ${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)} +
+ ${params.code ? html`` : ""} +
`; } diff --git a/packages/web-ui/src/tools/renderer-registry.ts b/packages/web-ui/src/tools/renderer-registry.ts index df28a2b9..9d753610 100644 --- a/packages/web-ui/src/tools/renderer-registry.ts +++ b/packages/web-ui/src/tools/renderer-registry.ts @@ -1,5 +1,7 @@ -import { html, icon, type TemplateResult } from "@mariozechner/mini-lit"; -import { Loader } from "lucide"; +import { html, icon, iconDOM, type TemplateResult } from "@mariozechner/mini-lit"; +import type { Ref } from "lit/directives/ref.js"; +import { ref } from "lit/directives/ref.js"; +import { ChevronDown, ChevronRight, Loader } from "lucide"; import type { ToolRenderer } from "./types.js"; // Registry of tool renderers @@ -54,3 +56,59 @@ export function renderHeader(state: "inprogress" | "complete" | "error", toolIco `; } } + +/** + * Helper to render a collapsible header for tool renderers + * Same as renderHeader but with a chevron button that toggles visibility of content + */ +export function renderCollapsibleHeader( + state: "inprogress" | "complete" | "error", + toolIcon: any, + text: string, + contentRef: Ref, + chevronRef: Ref, + defaultExpanded = false, +): TemplateResult { + const statusIcon = (iconComponent: any, color: string) => + html`${icon(iconComponent, "sm")}`; + + const toggleContent = (e: Event) => { + e.preventDefault(); + const content = contentRef.value; + const chevron = chevronRef.value; + if (content && chevron) { + const isCollapsed = content.classList.contains("max-h-0"); + if (isCollapsed) { + content.classList.remove("max-h-0"); + content.classList.add("max-h-[2000px]", "mt-3"); + chevron.innerHTML = iconDOM(ChevronDown, "sm").outerHTML; + } else { + content.classList.remove("max-h-[2000px]", "mt-3"); + content.classList.add("max-h-0"); + chevron.innerHTML = iconDOM(ChevronRight, "sm").outerHTML; + } + } + }; + + const toolIconColor = + state === "complete" + ? "text-green-600 dark:text-green-500" + : state === "error" + ? "text-destructive" + : "text-foreground"; + + return html` +
+
+ ${state === "inprogress" ? statusIcon(Loader, "text-foreground animate-spin") : ""} + ${statusIcon(toolIcon, toolIconColor)} + ${text} +
+ +
+ `; +}