From b129154cc838eb960d3ef9dd5fbb93b737c42bf4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 11 Oct 2025 04:40:42 +0200 Subject: [PATCH] Add ToolRenderResult interface for custom tool rendering - Changed ToolRenderer return type from TemplateResult to ToolRenderResult - ToolRenderResult = { content: TemplateResult, isCustom: boolean } - isCustom: true = no card wrapper, false = wrap in card - Updated all existing tool renderers to return new format - Updated Messages.ts to handle custom rendering This enables tools to render without default card chrome when needed. --- packages/ai/src/agent/agent-loop.ts | 31 ++++- packages/ai/src/agent/index.ts | 2 +- packages/ai/src/agent/types.ts | 11 +- packages/ai/test/agent.test.ts | 6 +- packages/web-ui/src/agent/agent.ts | 38 ++++-- .../src/agent/transports/AppTransport.ts | 5 +- .../src/agent/transports/ProviderTransport.ts | 11 +- packages/web-ui/src/agent/transports/types.ts | 3 +- .../web-ui/src/components/AgentInterface.ts | 12 +- packages/web-ui/src/components/Messages.ts | 10 +- .../web-ui/src/components/SandboxedIframe.ts | 110 +++++++++++++++++- .../sandbox/RuntimeMessageRouter.ts | 23 ---- packages/web-ui/src/index.ts | 2 +- packages/web-ui/src/prompts/tool-prompts.ts | 29 +++-- .../artifacts/artifacts-tool-renderer.ts | 85 +++++++++----- .../web-ui/src/tools/artifacts/artifacts.ts | 4 +- packages/web-ui/src/tools/index.ts | 8 +- packages/web-ui/src/tools/javascript-repl.ts | 58 ++++----- .../src/tools/renderers/BashRenderer.ts | 38 +++--- .../src/tools/renderers/CalculateRenderer.ts | 32 ++--- .../src/tools/renderers/DefaultRenderer.ts | 23 ++-- .../tools/renderers/GetCurrentTimeRenderer.ts | 55 +++++---- packages/web-ui/src/tools/types.ts | 7 +- 23 files changed, 423 insertions(+), 180 deletions(-) diff --git a/packages/ai/src/agent/agent-loop.ts b/packages/ai/src/agent/agent-loop.ts index a929c0fe..d51876f1 100644 --- a/packages/ai/src/agent/agent-loop.ts +++ b/packages/ai/src/agent/agent-loop.ts @@ -2,13 +2,13 @@ import { streamSimple } from "../stream.js"; import type { AssistantMessage, Context, Message, ToolResultMessage, UserMessage } from "../types.js"; import { EventStream } from "../utils/event-stream.js"; import { validateToolArguments } from "../utils/validation.js"; -import type { AgentContext, AgentEvent, AgentTool, AgentToolResult, PromptConfig } from "./types.js"; +import type { AgentContext, AgentEvent, AgentLoopConfig, AgentTool, AgentToolResult, QueuedMessage } from "./types.js"; // Main prompt function - returns a stream of events export function agentLoop( prompt: UserMessage, context: AgentContext, - config: PromptConfig, + config: AgentLoopConfig, signal?: AbortSignal, streamFn?: typeof streamSimple, ): EventStream { @@ -36,15 +36,33 @@ export function agentLoop( messages, }; - // Keep looping while we have tool calls + // Keep looping while we have tool calls or queued messages let hasMoreToolCalls = true; let firstTurn = true; - while (hasMoreToolCalls) { + let queuedMessages: QueuedMessage[] = (await config.getQueuedMessages?.()) || []; + + while (hasMoreToolCalls || queuedMessages.length > 0) { if (!firstTurn) { stream.push({ type: "turn_start" }); } else { firstTurn = false; } + + // Process queued messages first (inject before next assistant response) + if (queuedMessages.length > 0) { + for (const { original, llm } of queuedMessages) { + stream.push({ type: "message_start", message: original }); + stream.push({ type: "message_end", message: original }); + if (llm) { + currentContext.messages.push(llm); + newMessages.push(llm); + } + } + queuedMessages = []; + } + + console.log("agent-loop: ", [...currentContext.messages]); + // Stream assistant response const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn); newMessages.push(message); @@ -69,6 +87,9 @@ export function agentLoop( newMessages.push(...toolResults); } stream.push({ type: "turn_end", message, toolResults: toolResults }); + + // Get queued messages after turn completes + queuedMessages = (await config.getQueuedMessages?.()) || []; } stream.push({ type: "agent_end", messages: newMessages }); stream.end(newMessages); @@ -80,7 +101,7 @@ export function agentLoop( // Helper functions async function streamAssistantResponse( context: AgentContext, - config: PromptConfig, + config: AgentLoopConfig, signal: AbortSignal | undefined, stream: EventStream, streamFn?: typeof streamSimple, diff --git a/packages/ai/src/agent/index.ts b/packages/ai/src/agent/index.ts index 22cb56da..9ecb854d 100644 --- a/packages/ai/src/agent/index.ts +++ b/packages/ai/src/agent/index.ts @@ -1,3 +1,3 @@ export { agentLoop } from "./agent-loop.js"; export * from "./tools/index.js"; -export type { AgentContext, AgentEvent, AgentTool, PromptConfig } from "./types.js"; +export type { AgentContext, AgentEvent, AgentLoopConfig, AgentTool, QueuedMessage } from "./types.js"; diff --git a/packages/ai/src/agent/types.ts b/packages/ai/src/agent/types.ts index da8b7c5c..b52d5317 100644 --- a/packages/ai/src/agent/types.ts +++ b/packages/ai/src/agent/types.ts @@ -62,8 +62,15 @@ export type AgentEvent = // contained in messages, which can be appended to the context | { type: "agent_end"; messages: AgentContext["messages"] }; -// Configuration for prompt execution -export interface PromptConfig extends SimpleStreamOptions { +// Queued message with optional LLM representation +export interface QueuedMessage { + original: TApp; // Original message for UI events + llm?: Message; // Optional transformed message for loop context (undefined if filtered) +} + +// Configuration for agent loop execution +export interface AgentLoopConfig extends SimpleStreamOptions { model: Model; preprocessor?: (messages: AgentContext["messages"], abortSignal?: AbortSignal) => Promise; + getQueuedMessages?: () => Promise[]>; } diff --git a/packages/ai/test/agent.test.ts b/packages/ai/test/agent.test.ts index 452b00cf..e98468fe 100644 --- a/packages/ai/test/agent.test.ts +++ b/packages/ai/test/agent.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { agentLoop } from "../src/agent/agent-loop.js"; import { calculateTool } from "../src/agent/tools/calculate.js"; -import type { AgentContext, AgentEvent, PromptConfig } from "../src/agent/types.js"; +import type { AgentContext, AgentEvent, AgentLoopConfig } from "../src/agent/types.js"; import { getModel } from "../src/models.js"; import type { Api, Message, Model, OptionsForApi, UserMessage } from "../src/types.js"; @@ -15,7 +15,7 @@ async function calculateTest(model: Model, options: Opti }; // Create the prompt config - const config: PromptConfig = { + const config: AgentLoopConfig = { model, ...options, }; @@ -167,7 +167,7 @@ async function abortTest(model: Model, options: OptionsF }; // Create the prompt config - const config: PromptConfig = { + const config: AgentLoopConfig = { model, ...options, }; diff --git a/packages/web-ui/src/agent/agent.ts b/packages/web-ui/src/agent/agent.ts index abf98eba..6230b70a 100644 --- a/packages/web-ui/src/agent/agent.ts +++ b/packages/web-ui/src/agent/agent.ts @@ -1,4 +1,4 @@ -import type { Context } from "@mariozechner/pi-ai"; +import type { Context, QueuedMessage } from "@mariozechner/pi-ai"; import { type AgentTool, type AssistantMessage as AssistantMessageType, @@ -47,7 +47,9 @@ export interface AgentState { export type AgentEvent = | { type: "state-update"; state: AgentState } | { type: "error-no-model" } - | { type: "error-no-api-key"; provider: string }; + | { type: "error-no-api-key"; provider: string } + | { type: "started" } + | { type: "completed" }; export interface AgentOptions { initialState?: Partial; @@ -74,6 +76,7 @@ export class Agent { private transport: AgentTransport; private debugListener?: (entry: DebugLogEntry) => void; private messageTransformer: (messages: AppMessage[]) => Message[] | Promise; + private messageQueue: Array> = []; constructor(opts: AgentOptions) { this._state = { ...this._state, ...opts.initialState }; @@ -111,6 +114,14 @@ export class Agent { appendMessage(m: AppMessage) { this.patch({ messages: [...this._state.messages, m] }); } + async queueMessage(m: AppMessage) { + // Transform message and queue it for injection at next turn + const transformed = await this.messageTransformer([m]); + this.messageQueue.push({ + original: m, + llm: transformed[0], // undefined if filtered out + }); + } clearMessages() { this.patch({ messages: [] }); } @@ -119,6 +130,11 @@ export class Agent { this.abortController?.abort(); } + private logState(message: string) { + const { systemPrompt, model, messages } = this._state; + console.log(message, { systemPrompt, model, messages }); + } + async prompt(input: string, attachments?: Attachment[]) { const model = this._state.model; if (!model) { @@ -150,6 +166,7 @@ export class Agent { this.abortController = new AbortController(); this.patch({ isStreaming: true, streamMessage: null, error: undefined }); + this.emit({ type: "started" }); const reasoning = this._state.thinkingLevel === "off" @@ -162,6 +179,12 @@ export class Agent { tools: this._state.tools, model, reasoning, + getQueuedMessages: async () => { + // Return queued messages (they'll be added to state via message_end event) + const queued = this.messageQueue.slice(); + this.messageQueue = []; + return queued as QueuedMessage[]; + }, }; try { @@ -169,9 +192,12 @@ export class Agent { let turnDebug: DebugLogEntry | null = null; let turnStart = 0; - // Transform app messages to LLM-compatible messages + this.logState("prompt started, current state:"); + + // Transform app messages to LLM-compatible messages (initial set) const llmMessages = await this.messageTransformer(this._state.messages); + console.log("transformed messages:", llmMessages); for await (const ev of this.transport.run( llmMessages, userMessage as Message, @@ -292,11 +318,9 @@ export class Agent { } finally { this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set() }); this.abortController = undefined; + this.emit({ type: "completed" }); } - { - const { systemPrompt, model, messages } = this._state; - console.log("final state:", { systemPrompt, model, messages }); - } + this.logState("final state:"); } private patch(p: Partial): void { diff --git a/packages/web-ui/src/agent/transports/AppTransport.ts b/packages/web-ui/src/agent/transports/AppTransport.ts index 1400f03b..e07889f7 100644 --- a/packages/web-ui/src/agent/transports/AppTransport.ts +++ b/packages/web-ui/src/agent/transports/AppTransport.ts @@ -1,12 +1,12 @@ import type { AgentContext, + AgentLoopConfig, Api, AssistantMessage, AssistantMessageEvent, Context, Message, Model, - PromptConfig, SimpleStreamOptions, ToolCall, UserMessage, @@ -348,9 +348,10 @@ export class AppTransport implements AgentTransport { tools: cfg.tools, }; - const pc: PromptConfig = { + const pc: AgentLoopConfig = { model: cfg.model, reasoning: cfg.reasoning, + getQueuedMessages: cfg.getQueuedMessages, }; // Yield events from the upstream agentLoop iterator diff --git a/packages/web-ui/src/agent/transports/ProviderTransport.ts b/packages/web-ui/src/agent/transports/ProviderTransport.ts index 22776fb8..9682e275 100644 --- a/packages/web-ui/src/agent/transports/ProviderTransport.ts +++ b/packages/web-ui/src/agent/transports/ProviderTransport.ts @@ -1,4 +1,10 @@ -import { type AgentContext, agentLoop, type Message, type PromptConfig, type UserMessage } from "@mariozechner/pi-ai"; +import { + type AgentContext, + type AgentLoopConfig, + agentLoop, + type Message, + type UserMessage, +} from "@mariozechner/pi-ai"; import { getAppStorage } from "../../storage/app-storage.js"; import type { AgentRunConfig, AgentTransport } from "./types.js"; @@ -34,10 +40,11 @@ export class ProviderTransport implements AgentTransport { tools: cfg.tools, }; - const pc: PromptConfig = { + const pc: AgentLoopConfig = { model, reasoning: cfg.reasoning, apiKey, + getQueuedMessages: cfg.getQueuedMessages, }; // Yield events from agentLoop diff --git a/packages/web-ui/src/agent/transports/types.ts b/packages/web-ui/src/agent/transports/types.ts index 099a5480..afc099a8 100644 --- a/packages/web-ui/src/agent/transports/types.ts +++ b/packages/web-ui/src/agent/transports/types.ts @@ -1,4 +1,4 @@ -import type { AgentEvent, AgentTool, Message, Model } from "@mariozechner/pi-ai"; +import type { AgentEvent, AgentTool, Message, Model, QueuedMessage } from "@mariozechner/pi-ai"; // The minimal configuration needed to run a turn. export interface AgentRunConfig { @@ -6,6 +6,7 @@ export interface AgentRunConfig { tools: AgentTool[]; model: Model; reasoning?: "low" | "medium" | "high"; + getQueuedMessages?: () => Promise[]>; } // Events yielded by transports must match the @mariozechner/pi-ai prompt() events. diff --git a/packages/web-ui/src/components/AgentInterface.ts b/packages/web-ui/src/components/AgentInterface.ts index e6b7dd83..fdd78447 100644 --- a/packages/web-ui/src/components/AgentInterface.ts +++ b/packages/web-ui/src/components/AgentInterface.ts @@ -19,10 +19,10 @@ import type { StreamingMessageContainer } from "./StreamingMessageContainer.js"; export class AgentInterface extends LitElement { // Optional external session: when provided, this component becomes a view over the session @property({ attribute: false }) session?: Agent; - @property() enableAttachments = true; - @property() enableModelSelector = true; - @property() enableThinkingSelector = true; - @property() showThemeToggle = false; + @property({ type: Boolean }) enableAttachments = true; + @property({ type: Boolean }) enableModelSelector = true; + @property({ type: Boolean }) enableThinkingSelector = true; + @property({ type: Boolean }) showThemeToggle = false; // Optional custom API key prompt handler - if not provided, uses default dialog @property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise; // Optional callback called before sending a message @@ -52,6 +52,10 @@ export class AgentInterface extends LitElement { update(); } + public setAutoScroll(enabled: boolean) { + this._autoScroll = enabled; + } + protected override createRenderRoot(): HTMLElement | DocumentFragment { return this; } diff --git a/packages/web-ui/src/components/Messages.ts b/packages/web-ui/src/components/Messages.ts index 433faca3..59d64029 100644 --- a/packages/web-ui/src/components/Messages.ts +++ b/packages/web-ui/src/components/Messages.ts @@ -231,16 +231,22 @@ export class ToolMessage extends LitElement { const result: ToolResultMessageType | undefined = this.aborted ? { role: "toolResult", isError: true, output: "", toolCallId: this.toolCall.id, toolName: this.toolCall.name } : this.result; - const toolContent = renderTool( + const renderResult = renderTool( toolName, this.toolCall.arguments, result, !this.aborted && (this.isStreaming || this.pending), ); + // Handle custom rendering (no card wrapper) + if (renderResult.isCustom) { + return renderResult.content; + } + + // Default: wrap in card return html`
- ${toolContent} + ${renderResult.content}
`; } diff --git a/packages/web-ui/src/components/SandboxedIframe.ts b/packages/web-ui/src/components/SandboxedIframe.ts index bb913e7e..c694cc8b 100644 --- a/packages/web-ui/src/components/SandboxedIframe.ts +++ b/packages/web-ui/src/components/SandboxedIframe.ts @@ -25,6 +25,15 @@ export interface SandboxResult { */ export type SandboxUrlProvider = () => string; +/** + * Escape HTML special sequences in code to prevent premature tag closure + * @param code Code that will be injected into in user code to prevent premature tag closure + const escapedUserCode = escapeScriptContent(userCode); + return ` @@ -326,7 +426,7 @@ export class SandboxIframe extends LitElement { try { // Wrap user code in async function to capture return value const userCodeFunc = async () => { - ${userCode} + ${escapedUserCode} }; const returnValue = await userCodeFunc(); diff --git a/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts b/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts index fbc47b94..3ab84b04 100644 --- a/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts +++ b/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts @@ -59,7 +59,6 @@ export class RuntimeMessageRouter { // Setup global listener if not already done this.setupListener(); - console.log(`Registered sandbox: ${sandboxId}, providers: ${providers.length}, consumers: ${consumers.length}`); } /** @@ -72,7 +71,6 @@ export class RuntimeMessageRouter { if (context) { context.iframe = iframe; } - console.log("Set iframe for sandbox:", sandboxId); } /** @@ -96,7 +94,6 @@ export class RuntimeMessageRouter { this.userScriptMessageListener = null; } } - console.log("Unregistered sandbox:", sandboxId); } /** @@ -108,7 +105,6 @@ export class RuntimeMessageRouter { if (context) { context.consumers.add(consumer); } - console.log("Added consumer for sandbox:", sandboxId); } /** @@ -119,7 +115,6 @@ export class RuntimeMessageRouter { if (context) { context.consumers.delete(consumer); } - console.log("Removed consumer for sandbox:", sandboxId); } /** @@ -132,18 +127,8 @@ export class RuntimeMessageRouter { const { sandboxId, messageId } = e.data; if (!sandboxId) return; - console.log( - "[ROUTER] Received message for sandbox:", - sandboxId, - "type:", - e.data.type, - "full message:", - e.data, - ); - const context = this.sandboxes.get(sandboxId); if (!context) { - console.log("[ROUTER] No context found for sandbox:", sandboxId); return; } @@ -161,19 +146,15 @@ export class RuntimeMessageRouter { }; // 1. Try provider handlers first (for bidirectional comm) - console.log("[ROUTER] Broadcasting to", context.providers.length, "providers"); for (const provider of context.providers) { if (provider.handleMessage) { - console.log("[ROUTER] Calling provider.handleMessage for", provider.constructor.name); await provider.handleMessage(e.data, respond); // Don't stop - let consumers also handle the message } } // 2. Broadcast to consumers (one-way messages or lifecycle events) - console.log("[ROUTER] Broadcasting to", context.consumers.size, "consumers"); for (const consumer of context.consumers) { - console.log("[ROUTER] Calling consumer.handleMessage"); await consumer.handleMessage(e.data); // Don't stop - let all consumers see the message } @@ -186,7 +167,6 @@ export class RuntimeMessageRouter { if (!this.userScriptMessageListener) { // Guard: check if we're in extension context if (typeof chrome === "undefined" || !chrome.runtime?.onUserScriptMessage) { - console.log("[RuntimeMessageRouter] User script API not available (not in extension context)"); return; } @@ -197,8 +177,6 @@ export class RuntimeMessageRouter { const context = this.sandboxes.get(sandboxId); if (!context) return false; - console.log("Router received user script message for sandbox:", sandboxId, message); - const respond = (response: any) => { sendResponse({ ...response, @@ -227,7 +205,6 @@ export class RuntimeMessageRouter { }; chrome.runtime.onUserScriptMessage.addListener(this.userScriptMessageListener); - console.log("[RuntimeMessageRouter] Registered chrome.runtime.onUserScriptMessage listener"); } } } diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index cb8db7e0..8187e0e0 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -93,7 +93,7 @@ export { CalculateRenderer } from "./tools/renderers/CalculateRenderer.js"; // Tool renderers export { DefaultRenderer } from "./tools/renderers/DefaultRenderer.js"; export { GetCurrentTimeRenderer } from "./tools/renderers/GetCurrentTimeRenderer.js"; -export type { ToolRenderer } from "./tools/types.js"; +export type { ToolRenderer, ToolRenderResult } from "./tools/types.js"; export type { Attachment } from "./utils/attachment-utils.js"; // Utils export { loadAttachment } from "./utils/attachment-utils.js"; diff --git a/packages/web-ui/src/prompts/tool-prompts.ts b/packages/web-ui/src/prompts/tool-prompts.ts index ca252fa4..65bdc1a6 100644 --- a/packages/web-ui/src/prompts/tool-prompts.ts +++ b/packages/web-ui/src/prompts/tool-prompts.ts @@ -14,7 +14,7 @@ Execute JavaScript code in a sandboxed browser environment with full Web APIs. ## When to Use - Quick calculations or data transformations -- Testing JavaScript code snippets +- Testing JavaScript code snippets in isolation - Processing data with libraries (XLSX, CSV, etc.) - Creating visualizations (charts, graphs) @@ -29,17 +29,32 @@ Execute JavaScript code in a sandboxed browser environment with full Web APIs. - Chart.js: const Chart = (await import('https://esm.run/chart.js/auto')).default; - Three.js: const THREE = await import('https://esm.run/three'); -## Important Notes -- Graphics: Use fixed dimensions (800x600), NOT window.innerWidth/Height -- Chart.js: Set options: { responsive: false, animation: false } -- Three.js: renderer.setSize(800, 600) with matching aspect ratio -- Output: All console.log() calls are captured and displayed +## Persistence between tool calls +- Objects stored on global scope do not persist between calls. +- Use artifacts as a key-value JSON object store: + - Use createOrUpdateArtifact(filename, content) to persist data between calls. JSON objects are auto-stringified. + - Use listArtifacts() and getArtifact(filename) to read persisted data. JSON files are auto-parsed to objects. + - Prefer to use a single artifact throughout the session to store intermediate data (e.g. 'data.json'). + +## Input +- You have access to the user's attachments via listAttachments(), readTextAttachment(id), and readBinaryAttachment(id) +- You have access to previously created artifacts via listArtifacts() and getArtifact(filename) + +## Output +- All console.log() calls are captured for you to inspect. The user does not see these logs. +- Create artifacts for file results (images, JSON, CSV, etc.) which persiste throughout the + session and are accessible to you and the user. ## Example const data = [10, 20, 15, 25]; const sum = data.reduce((a, b) => a + b, 0); const avg = sum / data.length; -console.log('Sum:', sum, 'Average:', avg);`; +console.log('Sum:', sum, 'Average:', avg); + +## Important Notes +- Graphics: Use fixed dimensions (800x600), NOT window.innerWidth/Height +- Chart.js: Set options: { responsive: false, animation: false } +- Three.js: renderer.setSize(800, 600) with matching aspect ratio`; // ============================================================================ // Artifacts Tool diff --git a/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts b/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts index 69251d09..58469ef5 100644 --- a/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts +++ b/packages/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts @@ -6,7 +6,7 @@ import { FileCode2 } from "lucide"; import "../../components/ConsoleBlock.js"; import { i18n } from "../../utils/i18n.js"; import { renderCollapsibleHeader, renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer } from "../types.js"; +import type { ToolRenderer, ToolRenderResult } from "../types.js"; import { ArtifactPill } from "./ArtifactPill.js"; import type { ArtifactsPanel, ArtifactsParams } from "./artifacts.js"; @@ -54,7 +54,7 @@ export class ArtifactsToolRenderer implements ToolRenderer | undefined, isStreaming?: boolean, - ): TemplateResult { + ): ToolRenderResult { const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete"; // Create refs for collapsible sections @@ -101,7 +101,8 @@ export class ArtifactsToolRenderer implements ToolRenderer ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
@@ -113,16 +114,21 @@ export class ArtifactsToolRenderer implements ToolRenderer
- `; + `, + isCustom: false, + }; } // For other errors, just show error message - return html` + return { + content: html`
${renderHeader(state, FileCode2, headerText)}
${result.output || i18n("An error occurred")}
- `; + `, + isCustom: false, + }; } // Full params + result @@ -136,27 +142,33 @@ export class ArtifactsToolRenderer implements ToolRenderer ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
- `; + `, + isCustom: false, + }; } // LOGS command: show console block if (command === "logs") { const logs = result.output || i18n("(no output)"); - return html` + return { + content: html`
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
- `; + `, + isCustom: false, + }; } // CREATE/UPDATE/REWRITE: always show code block, + console block for .html files @@ -165,7 +177,8 @@ export class ArtifactsToolRenderer implements ToolRenderer ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
@@ -173,13 +186,16 @@ export class ArtifactsToolRenderer implements ToolRenderer` : ""}
- `; + `, + isCustom: false, + }; } if (command === "update") { const isHtml = filename?.endsWith(".html"); const logs = result.output || ""; - return html` + return { + content: html`
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
@@ -187,15 +203,20 @@ export class ArtifactsToolRenderer implements ToolRenderer` : ""}
- `; + `, + isCustom: false, + }; } // For DELETE, just show header - return html` + return { + content: html`
${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}
- `; + `, + isCustom: false, + }; } // Params only (streaming or waiting for result) @@ -204,7 +225,7 @@ export class ArtifactsToolRenderer implements ToolRenderer ${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
@@ -225,10 +247,13 @@ export class ArtifactsToolRenderer implements ToolRenderer
- `; + `, + isCustom: false, + }; case "update": - return html` + return { + content: html`
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
@@ -239,27 +264,35 @@ export class ArtifactsToolRenderer implements ToolRenderer
- `; + `, + isCustom: false, + }; case "get": case "logs": - return html` + return { + content: html`
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
- `; + `, + isCustom: false, + }; default: - return html` + return { + content: html`
${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}
- `; + `, + isCustom: false, + }; } } // No params or result yet - return renderHeader(state, FileCode2, i18n("Preparing artifact...")); + return { content: renderHeader(state, FileCode2, i18n("Preparing artifact...")), isCustom: false }; } } diff --git a/packages/web-ui/src/tools/artifacts/artifacts.ts b/packages/web-ui/src/tools/artifacts/artifacts.ts index 4ffee7d5..52682f93 100644 --- a/packages/web-ui/src/tools/artifacts/artifacts.ts +++ b/packages/web-ui/src/tools/artifacts/artifacts.ts @@ -408,7 +408,7 @@ export class ArtifactsPanel extends LitElement { let result = `Created file ${params.filename}`; if (this.getFileType(params.filename) === "html" && !options.skipWait) { const logs = await this.waitForHtmlExecution(params.filename); - result += logs; + result += `\n${logs}`; } return result; @@ -486,7 +486,7 @@ export class ArtifactsPanel extends LitElement { let result = ""; if (this.getFileType(params.filename) === "html" && !options.skipWait) { const logs = await this.waitForHtmlExecution(params.filename); - result += logs; + result += `\n${logs}`; } return result; diff --git a/packages/web-ui/src/tools/index.ts b/packages/web-ui/src/tools/index.ts index 107612c5..98035ff7 100644 --- a/packages/web-ui/src/tools/index.ts +++ b/packages/web-ui/src/tools/index.ts @@ -1,9 +1,9 @@ -import type { TemplateResult } from "@mariozechner/mini-lit"; import type { ToolResultMessage } from "@mariozechner/pi-ai"; +import "./javascript-repl.js"; // Auto-registers the renderer import { getToolRenderer, registerToolRenderer } from "./renderer-registry.js"; import { BashRenderer } from "./renderers/BashRenderer.js"; import { DefaultRenderer } from "./renderers/DefaultRenderer.js"; -import "./javascript-repl.js"; // Auto-registers the renderer +import type { ToolRenderResult } from "./types.js"; // Register all built-in tool renderers registerToolRenderer("bash", new BashRenderer()); @@ -18,7 +18,7 @@ export function renderTool( params: any | undefined, result: ToolResultMessage | undefined, isStreaming?: boolean, -): TemplateResult { +): ToolRenderResult { const renderer = getToolRenderer(toolName); if (renderer) { return renderer.render(params, result, isStreaming); @@ -26,4 +26,4 @@ export function renderTool( return defaultRenderer.render(params, result, isStreaming); } -export { registerToolRenderer, getToolRenderer }; +export { getToolRenderer, registerToolRenderer }; diff --git a/packages/web-ui/src/tools/javascript-repl.ts b/packages/web-ui/src/tools/javascript-repl.ts index 351bccf2..4b51b05a 100644 --- a/packages/web-ui/src/tools/javascript-repl.ts +++ b/packages/web-ui/src/tools/javascript-repl.ts @@ -1,4 +1,4 @@ -import { html, i18n, type TemplateResult } from "@mariozechner/mini-lit"; +import { html, i18n } 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"; @@ -8,7 +8,7 @@ import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntim import { JAVASCRIPT_REPL_DESCRIPTION } from "../prompts/tool-prompts.js"; import type { Attachment } from "../utils/attachment-utils.js"; import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js"; -import type { ToolRenderer } from "./types.js"; +import type { ToolRenderer, ToolRenderResult } from "./types.js"; // Execute JavaScript code with attachments using SandboxedIframe export async function executeJavaScript( @@ -194,7 +194,7 @@ export const javascriptReplRenderer: ToolRenderer | undefined, isStreaming?: boolean, - ): TemplateResult { + ): ToolRenderResult { // Determine status const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete"; @@ -236,38 +236,44 @@ export const javascriptReplRenderer: ToolRenderer - ${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)} -
- - ${output ? html`` : ""} + return { + content: html` +
+ ${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)} +
+ + ${output ? html`` : ""} +
+ ${ + attachments.length + ? html`
+ ${attachments.map((att) => html``)} +
` + : "" + }
- ${ - attachments.length - ? html`
- ${attachments.map((att) => html``)} -
` - : "" - } -
- `; + `, + isCustom: false, + }; } // Just params (streaming or waiting for result) if (params) { - return html` -
- ${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)} -
- ${params.code ? html`` : ""} + return { + content: html` +
+ ${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)} +
+ ${params.code ? html`` : ""} +
-
- `; + `, + isCustom: false, + }; } // No params or result yet - return renderHeader(state, Code, i18n("Preparing JavaScript...")); + return { content: renderHeader(state, Code, i18n("Preparing JavaScript...")), isCustom: false }; }, }; diff --git a/packages/web-ui/src/tools/renderers/BashRenderer.ts b/packages/web-ui/src/tools/renderers/BashRenderer.ts index 413e23c5..682792c5 100644 --- a/packages/web-ui/src/tools/renderers/BashRenderer.ts +++ b/packages/web-ui/src/tools/renderers/BashRenderer.ts @@ -1,9 +1,9 @@ -import { html, type TemplateResult } from "@mariozechner/mini-lit"; +import { html } from "@mariozechner/mini-lit"; import type { ToolResultMessage } from "@mariozechner/pi-ai"; import { SquareTerminal } from "lucide"; import { i18n } from "../../utils/i18n.js"; import { renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer } from "../types.js"; +import type { ToolRenderer, ToolRenderResult } from "../types.js"; interface BashParams { command: string; @@ -11,32 +11,38 @@ interface BashParams { // Bash tool has undefined details (only uses output) export class BashRenderer implements ToolRenderer { - render(params: BashParams | undefined, result: ToolResultMessage | undefined): TemplateResult { + render(params: BashParams | undefined, result: ToolResultMessage | undefined): ToolRenderResult { const state = result ? (result.isError ? "error" : "complete") : "inprogress"; // With result: show command + output if (result && params?.command) { const output = result.output || ""; const combined = output ? `> ${params.command}\n\n${output}` : `> ${params.command}`; - return html` -
- ${renderHeader(state, SquareTerminal, i18n("Running command..."))} - -
- `; + return { + content: html` +
+ ${renderHeader(state, SquareTerminal, i18n("Running command..."))} + +
+ `, + isCustom: false, + }; } // Just params (streaming or waiting) if (params?.command) { - return html` -
- ${renderHeader(state, SquareTerminal, i18n("Running command..."))} - ${params.command}`}> -
- `; + return { + content: html` +
+ ${renderHeader(state, SquareTerminal, i18n("Running command..."))} + ${params.command}`}> +
+ `, + isCustom: false, + }; } // No params yet - return renderHeader(state, SquareTerminal, i18n("Waiting for command...")); + return { content: renderHeader(state, SquareTerminal, i18n("Waiting for command...")), isCustom: false }; } } diff --git a/packages/web-ui/src/tools/renderers/CalculateRenderer.ts b/packages/web-ui/src/tools/renderers/CalculateRenderer.ts index cf630862..3c595cfb 100644 --- a/packages/web-ui/src/tools/renderers/CalculateRenderer.ts +++ b/packages/web-ui/src/tools/renderers/CalculateRenderer.ts @@ -1,9 +1,9 @@ -import { html, type TemplateResult } from "@mariozechner/mini-lit"; +import { html } from "@mariozechner/mini-lit"; import type { ToolResultMessage } from "@mariozechner/pi-ai"; import { Calculator } from "lucide"; import { i18n } from "../../utils/i18n.js"; import { renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer } from "../types.js"; +import type { ToolRenderer, ToolRenderResult } from "../types.js"; interface CalculateParams { expression: string; @@ -11,7 +11,7 @@ interface CalculateParams { // Calculate tool has undefined details (only uses output) export class CalculateRenderer implements ToolRenderer { - render(params: CalculateParams | undefined, result: ToolResultMessage | undefined): TemplateResult { + render(params: CalculateParams | undefined, result: ToolResultMessage | undefined): ToolRenderResult { const state = result ? (result.isError ? "error" : "complete") : "inprogress"; // Full params + full result @@ -20,29 +20,35 @@ export class CalculateRenderer implements ToolRenderer - ${renderHeader(state, Calculator, params.expression)} -
${output}
-
- `; + return { + content: html` +
+ ${renderHeader(state, Calculator, params.expression)} +
${output}
+
+ `, + isCustom: false, + }; } // Success: show expression = result in header - return renderHeader(state, Calculator, `${params.expression} = ${output}`); + return { content: renderHeader(state, Calculator, `${params.expression} = ${output}`), isCustom: false }; } // Full params, no result: just show header with expression in it if (params?.expression) { - return renderHeader(state, Calculator, `${i18n("Calculating")} ${params.expression}`); + return { + content: renderHeader(state, Calculator, `${i18n("Calculating")} ${params.expression}`), + isCustom: false, + }; } // Partial params (empty expression), no result if (params && !params.expression) { - return renderHeader(state, Calculator, i18n("Writing expression...")); + return { content: renderHeader(state, Calculator, i18n("Writing expression...")), isCustom: false }; } // No params, no result - return renderHeader(state, Calculator, i18n("Waiting for expression...")); + return { content: renderHeader(state, Calculator, i18n("Waiting for expression...")), isCustom: false }; } } diff --git a/packages/web-ui/src/tools/renderers/DefaultRenderer.ts b/packages/web-ui/src/tools/renderers/DefaultRenderer.ts index 8bed4dca..b2e4619a 100644 --- a/packages/web-ui/src/tools/renderers/DefaultRenderer.ts +++ b/packages/web-ui/src/tools/renderers/DefaultRenderer.ts @@ -1,14 +1,17 @@ -import { html, type TemplateResult } from "@mariozechner/mini-lit"; +import { html } from "@mariozechner/mini-lit"; import type { ToolResultMessage } from "@mariozechner/pi-ai"; import { i18n } from "../../utils/i18n.js"; -import type { ToolRenderer } from "../types.js"; +import type { ToolRenderer, ToolRenderResult } from "../types.js"; export class DefaultRenderer implements ToolRenderer { - render(params: any | undefined, result: ToolResultMessage | undefined, isStreaming?: boolean): TemplateResult { + render(params: any | undefined, result: ToolResultMessage | undefined, isStreaming?: boolean): ToolRenderResult { // Show result if available if (result) { const text = result.output || i18n("(no output)"); - return html`
${text}
`; + return { + content: html`
${text}
`, + isCustom: false, + }; } // Show params @@ -25,13 +28,19 @@ export class DefaultRenderer implements ToolRenderer { } if (isStreaming && (!text || text === "{}" || text === "null")) { - return html`
${i18n("Preparing tool parameters...")}
`; + return { + content: html`
${i18n("Preparing tool parameters...")}
`, + isCustom: false, + }; } - return html``; + return { content: html``, isCustom: false }; } // No params or result yet - return html`
${i18n("Preparing tool...")}
`; + return { + content: html`
${i18n("Preparing tool...")}
`, + isCustom: false, + }; } } diff --git a/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts b/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts index 20fd4bf1..ca954f7d 100644 --- a/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts +++ b/packages/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts @@ -1,9 +1,9 @@ -import { html, type TemplateResult } from "@mariozechner/mini-lit"; +import { html } from "@mariozechner/mini-lit"; import type { ToolResultMessage } from "@mariozechner/pi-ai"; import { Clock } from "lucide"; import { i18n } from "../../utils/i18n.js"; import { renderHeader } from "../renderer-registry.js"; -import type { ToolRenderer } from "../types.js"; +import type { ToolRenderer, ToolRenderResult } from "../types.js"; interface GetCurrentTimeParams { timezone?: string; @@ -11,7 +11,10 @@ interface GetCurrentTimeParams { // GetCurrentTime tool has undefined details (only uses output) export class GetCurrentTimeRenderer implements ToolRenderer { - render(params: GetCurrentTimeParams | undefined, result: ToolResultMessage | undefined): TemplateResult { + render( + params: GetCurrentTimeParams | undefined, + result: ToolResultMessage | undefined, + ): ToolRenderResult { const state = result ? (result.isError ? "error" : "complete") : "inprogress"; // Full params + full result @@ -23,16 +26,19 @@ export class GetCurrentTimeRenderer implements ToolRenderer - ${renderHeader(state, Clock, headerText)} -
${output}
-
- `; + return { + content: html` +
+ ${renderHeader(state, Clock, headerText)} +
${output}
+
+ `, + isCustom: false, + }; } // Success: show time in header - return renderHeader(state, Clock, `${headerText}: ${output}`); + return { content: renderHeader(state, Clock, `${headerText}: ${output}`), isCustom: false }; } // Full result, no params @@ -41,29 +47,38 @@ export class GetCurrentTimeRenderer implements ToolRenderer - ${renderHeader(state, Clock, i18n("Getting current date and time"))} -
${output}
- - `; + return { + content: html` +
+ ${renderHeader(state, Clock, i18n("Getting current date and time"))} +
${output}
+
+ `, + isCustom: false, + }; } // Success: show time in header - return renderHeader(state, Clock, `${i18n("Getting current date and time")}: ${output}`); + return { + content: renderHeader(state, Clock, `${i18n("Getting current date and time")}: ${output}`), + isCustom: false, + }; } // Full params, no result: show timezone info in header if (params?.timezone) { - return renderHeader(state, Clock, `${i18n("Getting current time in")} ${params.timezone}`); + return { + content: renderHeader(state, Clock, `${i18n("Getting current time in")} ${params.timezone}`), + isCustom: false, + }; } // Partial params (no timezone) or empty params, no result if (params) { - return renderHeader(state, Clock, i18n("Getting current date and time")); + return { content: renderHeader(state, Clock, i18n("Getting current date and time")), isCustom: false }; } // No params, no result - return renderHeader(state, Clock, i18n("Getting time...")); + return { content: renderHeader(state, Clock, i18n("Getting time...")), isCustom: false }; } } diff --git a/packages/web-ui/src/tools/types.ts b/packages/web-ui/src/tools/types.ts index c7d8ad29..9de5d1e6 100644 --- a/packages/web-ui/src/tools/types.ts +++ b/packages/web-ui/src/tools/types.ts @@ -1,10 +1,15 @@ import type { ToolResultMessage } from "@mariozechner/pi-ai"; import type { TemplateResult } from "lit"; +export interface ToolRenderResult { + content: TemplateResult; + isCustom: boolean; // true = no card wrapper, false = wrap in card +} + export interface ToolRenderer { render( params: TParams | undefined, result: ToolResultMessage | undefined, isStreaming?: boolean, - ): TemplateResult; + ): ToolRenderResult; }