mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 10:05:14 +00:00
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.
This commit is contained in:
parent
3db2a6fe2c
commit
b129154cc8
23 changed files with 423 additions and 180 deletions
|
|
@ -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<AgentEvent, AgentContext["messages"]> {
|
||||
|
|
@ -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<any>[] = (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<AgentEvent, AgentContext["messages"]>,
|
||||
streamFn?: typeof streamSimple,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<TApp = Message> {
|
||||
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<any>;
|
||||
preprocessor?: (messages: AgentContext["messages"], abortSignal?: AbortSignal) => Promise<AgentContext["messages"]>;
|
||||
getQueuedMessages?: <T>() => Promise<QueuedMessage<T>[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TApi extends Api>(model: Model<TApi>, options: Opti
|
|||
};
|
||||
|
||||
// Create the prompt config
|
||||
const config: PromptConfig = {
|
||||
const config: AgentLoopConfig = {
|
||||
model,
|
||||
...options,
|
||||
};
|
||||
|
|
@ -167,7 +167,7 @@ async function abortTest<TApi extends Api>(model: Model<TApi>, options: OptionsF
|
|||
};
|
||||
|
||||
// Create the prompt config
|
||||
const config: PromptConfig = {
|
||||
const config: AgentLoopConfig = {
|
||||
model,
|
||||
...options,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<AgentState>;
|
||||
|
|
@ -74,6 +76,7 @@ export class Agent {
|
|||
private transport: AgentTransport;
|
||||
private debugListener?: (entry: DebugLogEntry) => void;
|
||||
private messageTransformer: (messages: AppMessage[]) => Message[] | Promise<Message[]>;
|
||||
private messageQueue: Array<QueuedMessage<AppMessage>> = [];
|
||||
|
||||
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 <T>() => {
|
||||
// Return queued messages (they'll be added to state via message_end event)
|
||||
const queued = this.messageQueue.slice();
|
||||
this.messageQueue = [];
|
||||
return queued as QueuedMessage<T>[];
|
||||
},
|
||||
};
|
||||
|
||||
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<string>() });
|
||||
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<AgentState>): void {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<any>[];
|
||||
model: Model<any>;
|
||||
reasoning?: "low" | "medium" | "high";
|
||||
getQueuedMessages?: <T>() => Promise<QueuedMessage<T>[]>;
|
||||
}
|
||||
|
||||
// Events yielded by transports must match the @mariozechner/pi-ai prompt() events.
|
||||
|
|
|
|||
|
|
@ -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<boolean>;
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,16 +231,22 @@ export class ToolMessage extends LitElement {
|
|||
const result: ToolResultMessageType<any> | 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`
|
||||
<div class="p-2.5 border border-border rounded-md bg-card text-card-foreground shadow-xs">
|
||||
${toolContent}
|
||||
${renderResult.content}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <script> tags
|
||||
* @returns Escaped code safe for injection
|
||||
*/
|
||||
function escapeScriptContent(code: string): string {
|
||||
return code.replace(/<\/script/gi, "<\\/script");
|
||||
}
|
||||
|
||||
@customElement("sandbox-iframe")
|
||||
export class SandboxIframe extends LitElement {
|
||||
private iframe?: HTMLIFrameElement;
|
||||
|
|
@ -79,6 +88,26 @@ export class SandboxIframe extends LitElement {
|
|||
// loadContent is always used for HTML artifacts
|
||||
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, true);
|
||||
|
||||
// Validate HTML before loading
|
||||
const validationError = this.validateHtml(completeHtml);
|
||||
if (validationError) {
|
||||
console.error("HTML validation failed:", validationError);
|
||||
// Show error in iframe instead of crashing
|
||||
this.iframe?.remove();
|
||||
this.iframe = document.createElement("iframe");
|
||||
this.iframe.style.cssText = "width: 100%; height: 100%; border: none;";
|
||||
this.iframe.srcdoc = `
|
||||
<html>
|
||||
<body style="font-family: monospace; padding: 20px; background: #fff; color: #000;">
|
||||
<h3 style="color: #c00;">HTML Validation Error</h3>
|
||||
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap;">${validationError}</pre>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
this.appendChild(this.iframe);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove previous iframe if exists
|
||||
this.iframe?.remove();
|
||||
|
||||
|
|
@ -104,10 +133,11 @@ export class SandboxIframe extends LitElement {
|
|||
// Update router with iframe reference BEFORE appending to DOM
|
||||
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
|
||||
|
||||
// Listen for sandbox-ready message directly
|
||||
// Listen for sandbox-ready and sandbox-error messages directly
|
||||
const readyHandler = (e: MessageEvent) => {
|
||||
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
||||
window.removeEventListener("message", readyHandler);
|
||||
window.removeEventListener("message", errorHandler);
|
||||
|
||||
// Send content to sandbox
|
||||
this.iframe?.contentWindow?.postMessage(
|
||||
|
|
@ -121,7 +151,27 @@ export class SandboxIframe extends LitElement {
|
|||
}
|
||||
};
|
||||
|
||||
const errorHandler = (e: MessageEvent) => {
|
||||
if (e.data.type === "sandbox-error" && e.source === this.iframe?.contentWindow) {
|
||||
window.removeEventListener("message", readyHandler);
|
||||
window.removeEventListener("message", errorHandler);
|
||||
|
||||
// The sandbox.js already sent us the error via postMessage.
|
||||
// We need to convert it to an execution-error message that the execute() consumer will handle.
|
||||
// Simulate receiving an execution-error from the sandbox
|
||||
window.postMessage(
|
||||
{
|
||||
sandboxId: sandboxId,
|
||||
type: "execution-error",
|
||||
error: { message: e.data.error, stack: e.data.stack },
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", readyHandler);
|
||||
window.addEventListener("message", errorHandler);
|
||||
|
||||
this.appendChild(this.iframe);
|
||||
}
|
||||
|
|
@ -228,15 +278,22 @@ export class SandboxIframe extends LitElement {
|
|||
resolve({
|
||||
success: false,
|
||||
console: consoleProvider.getLogs(),
|
||||
error: { message: "Execution timeout (30s)", stack: "" },
|
||||
error: { message: "Execution timeout (120s)", stack: "" },
|
||||
files,
|
||||
});
|
||||
}
|
||||
}, 30000);
|
||||
}, 120000);
|
||||
|
||||
// 4. Prepare HTML and create iframe
|
||||
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, isHtmlArtifact);
|
||||
|
||||
// 5. Validate HTML before sending to sandbox
|
||||
const validationError = this.validateHtml(completeHtml);
|
||||
if (validationError) {
|
||||
reject(new Error(`HTML validation failed: ${validationError}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sandboxUrlProvider) {
|
||||
// Browser extension mode: wait for sandbox-ready
|
||||
this.iframe = document.createElement("iframe");
|
||||
|
|
@ -247,10 +304,11 @@ export class SandboxIframe extends LitElement {
|
|||
// Update router with iframe reference BEFORE appending to DOM
|
||||
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
|
||||
|
||||
// Listen for sandbox-ready message directly
|
||||
// Listen for sandbox-ready and sandbox-error messages
|
||||
const readyHandler = (e: MessageEvent) => {
|
||||
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
||||
window.removeEventListener("message", readyHandler);
|
||||
window.removeEventListener("message", errorHandler);
|
||||
|
||||
// Send content to sandbox
|
||||
this.iframe?.contentWindow?.postMessage(
|
||||
|
|
@ -264,7 +322,25 @@ export class SandboxIframe extends LitElement {
|
|||
}
|
||||
};
|
||||
|
||||
const errorHandler = (e: MessageEvent) => {
|
||||
if (e.data.type === "sandbox-error" && e.source === this.iframe?.contentWindow) {
|
||||
window.removeEventListener("message", readyHandler);
|
||||
window.removeEventListener("message", errorHandler);
|
||||
|
||||
// Convert sandbox-error to execution-error for the execution consumer
|
||||
window.postMessage(
|
||||
{
|
||||
sandboxId: sandboxId,
|
||||
type: "execution-error",
|
||||
error: { message: e.data.error, stack: e.data.stack },
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", readyHandler);
|
||||
window.addEventListener("message", errorHandler);
|
||||
|
||||
this.appendChild(this.iframe);
|
||||
} else {
|
||||
|
|
@ -282,6 +358,27 @@ export class SandboxIframe extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate HTML using DOMParser - returns error message if invalid, null if valid
|
||||
* Note: JavaScript syntax validation is done in sandbox.js to avoid CSP restrictions
|
||||
*/
|
||||
private validateHtml(html: string): string | null {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
|
||||
// Check for parser errors
|
||||
const parserError = doc.querySelector("parsererror");
|
||||
if (parserError) {
|
||||
return parserError.textContent || "Unknown parse error";
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
return error.message || "Unknown validation error";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare complete HTML document with runtime + user code
|
||||
* PUBLIC so HtmlArtifact can use it for download button
|
||||
|
|
@ -315,6 +412,9 @@ export class SandboxIframe extends LitElement {
|
|||
return runtime + userCode;
|
||||
} else {
|
||||
// REPL - wrap code in HTML with runtime and call complete() when done
|
||||
// Escape </script> in user code to prevent premature tag closure
|
||||
const escapedUserCode = escapeScriptContent(userCode);
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ArtifactsParams, unde
|
|||
params: ArtifactsParams | undefined,
|
||||
result: ToolResultMessage<undefined> | 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<ArtifactsParams, unde
|
|||
|
||||
const isHtml = filename?.endsWith(".html");
|
||||
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
||||
|
|
@ -113,16 +114,21 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// For other errors, just show error message
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, headerText)}
|
||||
<div class="text-sm text-destructive">${result.output || i18n("An error occurred")}</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Full params + result
|
||||
|
|
@ -136,27 +142,33 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
// GET command: show code block with file content
|
||||
if (command === "get") {
|
||||
const fileContent = result.output || i18n("(no output)");
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||
<code-block .code=${fileContent} language=${getLanguageFromFilename(filename)}></code-block>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// LOGS command: show console block
|
||||
if (command === "logs") {
|
||||
const logs = result.output || i18n("(no output)");
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||
<console-block .content=${logs}></console-block>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// CREATE/UPDATE/REWRITE: always show code block, + console block for .html files
|
||||
|
|
@ -165,7 +177,8 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
const isHtml = filename?.endsWith(".html");
|
||||
const logs = result.output || "";
|
||||
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
||||
|
|
@ -173,13 +186,16 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
${isHtml && logs ? html`<console-block .content=${logs}></console-block>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (command === "update") {
|
||||
const isHtml = filename?.endsWith(".html");
|
||||
const logs = result.output || "";
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
||||
|
|
@ -187,15 +203,20 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
${isHtml && logs ? html`<console-block .content=${logs}></console-block>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// For DELETE, just show header
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Params only (streaming or waiting for result)
|
||||
|
|
@ -204,7 +225,7 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
|
||||
// If no command yet
|
||||
if (!command) {
|
||||
return renderHeader(state, FileCode2, i18n("Preparing artifact..."));
|
||||
return { content: renderHeader(state, FileCode2, i18n("Preparing artifact...")), isCustom: false };
|
||||
}
|
||||
|
||||
const labels = getCommandLabels(command);
|
||||
|
|
@ -214,7 +235,8 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
switch (command) {
|
||||
case "create":
|
||||
case "rewrite":
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||
|
|
@ -225,10 +247,13 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
|
||||
case "update":
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||
|
|
@ -239,27 +264,35 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
|
||||
case "get":
|
||||
case "logs":
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300"></div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
|
||||
default:
|
||||
return html`
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// No params or result yet
|
||||
return renderHeader(state, FileCode2, i18n("Preparing artifact..."));
|
||||
return { content: renderHeader(state, FileCode2, i18n("Preparing artifact...")), isCustom: false };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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<JavaScriptReplParams, JavaScri
|
|||
params: JavaScriptReplParams | undefined,
|
||||
result: ToolResultMessage<JavaScriptReplResult> | 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<JavaScriptReplParams, JavaScri
|
|||
};
|
||||
});
|
||||
|
||||
return html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
||||
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
||||
<code-block .code=${params.code || ""} language="javascript"></code-block>
|
||||
${output ? html`<console-block .content=${output} .variant=${result.isError ? "error" : "default"}></console-block>` : ""}
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
||||
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
||||
<code-block .code=${params.code || ""} language="javascript"></code-block>
|
||||
${output ? html`<console-block .content=${output} .variant=${result.isError ? "error" : "default"}></console-block>` : ""}
|
||||
</div>
|
||||
${
|
||||
attachments.length
|
||||
? html`<div class="flex flex-wrap gap-2 mt-3">
|
||||
${attachments.map((att) => html`<attachment-tile .attachment=${att}></attachment-tile>`)}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
${
|
||||
attachments.length
|
||||
? html`<div class="flex flex-wrap gap-2 mt-3">
|
||||
${attachments.map((att) => html`<attachment-tile .attachment=${att}></attachment-tile>`)}
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Just params (streaming or waiting for result)
|
||||
if (params) {
|
||||
return html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
||||
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||
${params.code ? html`<code-block .code=${params.code} language="javascript"></code-block>` : ""}
|
||||
return {
|
||||
content: html`
|
||||
<div>
|
||||
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
||||
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||
${params.code ? html`<code-block .code=${params.code} language="javascript"></code-block>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// No params or result yet
|
||||
return renderHeader(state, Code, i18n("Preparing JavaScript..."));
|
||||
return { content: renderHeader(state, Code, i18n("Preparing JavaScript...")), isCustom: false };
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<BashParams, undefined> {
|
||||
render(params: BashParams | undefined, result: ToolResultMessage<undefined> | undefined): TemplateResult {
|
||||
render(params: BashParams | undefined, result: ToolResultMessage<undefined> | 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`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, SquareTerminal, i18n("Running command..."))}
|
||||
<console-block .content=${combined} .variant=${result.isError ? "error" : "default"}></console-block>
|
||||
</div>
|
||||
`;
|
||||
return {
|
||||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, SquareTerminal, i18n("Running command..."))}
|
||||
<console-block .content=${combined} .variant=${result.isError ? "error" : "default"}></console-block>
|
||||
</div>
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Just params (streaming or waiting)
|
||||
if (params?.command) {
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, SquareTerminal, i18n("Running command..."))}
|
||||
<console-block .content=${`> ${params.command}`}></console-block>
|
||||
</div>
|
||||
`;
|
||||
return {
|
||||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, SquareTerminal, i18n("Running command..."))}
|
||||
<console-block .content=${`> ${params.command}`}></console-block>
|
||||
</div>
|
||||
`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// No params yet
|
||||
return renderHeader(state, SquareTerminal, i18n("Waiting for command..."));
|
||||
return { content: renderHeader(state, SquareTerminal, i18n("Waiting for command...")), isCustom: false };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CalculateParams, undefined> {
|
||||
render(params: CalculateParams | undefined, result: ToolResultMessage<undefined> | undefined): TemplateResult {
|
||||
render(params: CalculateParams | undefined, result: ToolResultMessage<undefined> | undefined): ToolRenderResult {
|
||||
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||
|
||||
// Full params + full result
|
||||
|
|
@ -20,29 +20,35 @@ export class CalculateRenderer implements ToolRenderer<CalculateParams, undefine
|
|||
|
||||
// Error: show expression in header, error below
|
||||
if (result.isError) {
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, Calculator, params.expression)}
|
||||
<div class="text-sm text-destructive">${output}</div>
|
||||
</div>
|
||||
`;
|
||||
return {
|
||||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, Calculator, params.expression)}
|
||||
<div class="text-sm text-destructive">${output}</div>
|
||||
</div>
|
||||
`,
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`<div class="text-sm text-muted-foreground whitespace-pre-wrap font-mono">${text}</div>`;
|
||||
return {
|
||||
content: html`<div class="text-sm text-muted-foreground whitespace-pre-wrap font-mono">${text}</div>`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Show params
|
||||
|
|
@ -25,13 +28,19 @@ export class DefaultRenderer implements ToolRenderer {
|
|||
}
|
||||
|
||||
if (isStreaming && (!text || text === "{}" || text === "null")) {
|
||||
return html`<div class="text-sm text-muted-foreground">${i18n("Preparing tool parameters...")}</div>`;
|
||||
return {
|
||||
content: html`<div class="text-sm text-muted-foreground">${i18n("Preparing tool parameters...")}</div>`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
|
||||
return html`<console-block .content=${text}></console-block>`;
|
||||
return { content: html`<console-block .content=${text}></console-block>`, isCustom: false };
|
||||
}
|
||||
|
||||
// No params or result yet
|
||||
return html`<div class="text-sm text-muted-foreground">${i18n("Preparing tool...")}</div>`;
|
||||
return {
|
||||
content: html`<div class="text-sm text-muted-foreground">${i18n("Preparing tool...")}</div>`,
|
||||
isCustom: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GetCurrentTimeParams, undefined> {
|
||||
render(params: GetCurrentTimeParams | undefined, result: ToolResultMessage<undefined> | undefined): TemplateResult {
|
||||
render(
|
||||
params: GetCurrentTimeParams | undefined,
|
||||
result: ToolResultMessage<undefined> | undefined,
|
||||
): ToolRenderResult {
|
||||
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||
|
||||
// Full params + full result
|
||||
|
|
@ -23,16 +26,19 @@ export class GetCurrentTimeRenderer implements ToolRenderer<GetCurrentTimeParams
|
|||
|
||||
// Error: show header, error below
|
||||
if (result.isError) {
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, Clock, headerText)}
|
||||
<div class="text-sm text-destructive">${output}</div>
|
||||
</div>
|
||||
`;
|
||||
return {
|
||||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, Clock, headerText)}
|
||||
<div class="text-sm text-destructive">${output}</div>
|
||||
</div>
|
||||
`,
|
||||
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<GetCurrentTimeParams
|
|||
|
||||
// Error: show header, error below
|
||||
if (result.isError) {
|
||||
return html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, Clock, i18n("Getting current date and time"))}
|
||||
<div class="text-sm text-destructive">${output}</div>
|
||||
</div>
|
||||
`;
|
||||
return {
|
||||
content: html`
|
||||
<div class="space-y-3">
|
||||
${renderHeader(state, Clock, i18n("Getting current date and time"))}
|
||||
<div class="text-sm text-destructive">${output}</div>
|
||||
</div>
|
||||
`,
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TParams = any, TDetails = any> {
|
||||
render(
|
||||
params: TParams | undefined,
|
||||
result: ToolResultMessage<TDetails> | undefined,
|
||||
isStreaming?: boolean,
|
||||
): TemplateResult;
|
||||
): ToolRenderResult;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue