mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 01:01:42 +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 type { AssistantMessage, Context, Message, ToolResultMessage, UserMessage } from "../types.js";
|
||||||
import { EventStream } from "../utils/event-stream.js";
|
import { EventStream } from "../utils/event-stream.js";
|
||||||
import { validateToolArguments } from "../utils/validation.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
|
// Main prompt function - returns a stream of events
|
||||||
export function agentLoop(
|
export function agentLoop(
|
||||||
prompt: UserMessage,
|
prompt: UserMessage,
|
||||||
context: AgentContext,
|
context: AgentContext,
|
||||||
config: PromptConfig,
|
config: AgentLoopConfig,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
streamFn?: typeof streamSimple,
|
streamFn?: typeof streamSimple,
|
||||||
): EventStream<AgentEvent, AgentContext["messages"]> {
|
): EventStream<AgentEvent, AgentContext["messages"]> {
|
||||||
|
|
@ -36,15 +36,33 @@ export function agentLoop(
|
||||||
messages,
|
messages,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep looping while we have tool calls
|
// Keep looping while we have tool calls or queued messages
|
||||||
let hasMoreToolCalls = true;
|
let hasMoreToolCalls = true;
|
||||||
let firstTurn = true;
|
let firstTurn = true;
|
||||||
while (hasMoreToolCalls) {
|
let queuedMessages: QueuedMessage<any>[] = (await config.getQueuedMessages?.()) || [];
|
||||||
|
|
||||||
|
while (hasMoreToolCalls || queuedMessages.length > 0) {
|
||||||
if (!firstTurn) {
|
if (!firstTurn) {
|
||||||
stream.push({ type: "turn_start" });
|
stream.push({ type: "turn_start" });
|
||||||
} else {
|
} else {
|
||||||
firstTurn = false;
|
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
|
// Stream assistant response
|
||||||
const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
|
const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
|
||||||
newMessages.push(message);
|
newMessages.push(message);
|
||||||
|
|
@ -69,6 +87,9 @@ export function agentLoop(
|
||||||
newMessages.push(...toolResults);
|
newMessages.push(...toolResults);
|
||||||
}
|
}
|
||||||
stream.push({ type: "turn_end", message, toolResults: 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.push({ type: "agent_end", messages: newMessages });
|
||||||
stream.end(newMessages);
|
stream.end(newMessages);
|
||||||
|
|
@ -80,7 +101,7 @@ export function agentLoop(
|
||||||
// Helper functions
|
// Helper functions
|
||||||
async function streamAssistantResponse(
|
async function streamAssistantResponse(
|
||||||
context: AgentContext,
|
context: AgentContext,
|
||||||
config: PromptConfig,
|
config: AgentLoopConfig,
|
||||||
signal: AbortSignal | undefined,
|
signal: AbortSignal | undefined,
|
||||||
stream: EventStream<AgentEvent, AgentContext["messages"]>,
|
stream: EventStream<AgentEvent, AgentContext["messages"]>,
|
||||||
streamFn?: typeof streamSimple,
|
streamFn?: typeof streamSimple,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export { agentLoop } from "./agent-loop.js";
|
export { agentLoop } from "./agent-loop.js";
|
||||||
export * from "./tools/index.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
|
// contained in messages, which can be appended to the context
|
||||||
| { type: "agent_end"; messages: AgentContext["messages"] };
|
| { type: "agent_end"; messages: AgentContext["messages"] };
|
||||||
|
|
||||||
// Configuration for prompt execution
|
// Queued message with optional LLM representation
|
||||||
export interface PromptConfig extends SimpleStreamOptions {
|
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>;
|
model: Model<any>;
|
||||||
preprocessor?: (messages: AgentContext["messages"], abortSignal?: AbortSignal) => Promise<AgentContext["messages"]>;
|
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 { describe, expect, it } from "vitest";
|
||||||
import { agentLoop } from "../src/agent/agent-loop.js";
|
import { agentLoop } from "../src/agent/agent-loop.js";
|
||||||
import { calculateTool } from "../src/agent/tools/calculate.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 { getModel } from "../src/models.js";
|
||||||
import type { Api, Message, Model, OptionsForApi, UserMessage } from "../src/types.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
|
// Create the prompt config
|
||||||
const config: PromptConfig = {
|
const config: AgentLoopConfig = {
|
||||||
model,
|
model,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
@ -167,7 +167,7 @@ async function abortTest<TApi extends Api>(model: Model<TApi>, options: OptionsF
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the prompt config
|
// Create the prompt config
|
||||||
const config: PromptConfig = {
|
const config: AgentLoopConfig = {
|
||||||
model,
|
model,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Context } from "@mariozechner/pi-ai";
|
import type { Context, QueuedMessage } from "@mariozechner/pi-ai";
|
||||||
import {
|
import {
|
||||||
type AgentTool,
|
type AgentTool,
|
||||||
type AssistantMessage as AssistantMessageType,
|
type AssistantMessage as AssistantMessageType,
|
||||||
|
|
@ -47,7 +47,9 @@ export interface AgentState {
|
||||||
export type AgentEvent =
|
export type AgentEvent =
|
||||||
| { type: "state-update"; state: AgentState }
|
| { type: "state-update"; state: AgentState }
|
||||||
| { type: "error-no-model" }
|
| { 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 {
|
export interface AgentOptions {
|
||||||
initialState?: Partial<AgentState>;
|
initialState?: Partial<AgentState>;
|
||||||
|
|
@ -74,6 +76,7 @@ export class Agent {
|
||||||
private transport: AgentTransport;
|
private transport: AgentTransport;
|
||||||
private debugListener?: (entry: DebugLogEntry) => void;
|
private debugListener?: (entry: DebugLogEntry) => void;
|
||||||
private messageTransformer: (messages: AppMessage[]) => Message[] | Promise<Message[]>;
|
private messageTransformer: (messages: AppMessage[]) => Message[] | Promise<Message[]>;
|
||||||
|
private messageQueue: Array<QueuedMessage<AppMessage>> = [];
|
||||||
|
|
||||||
constructor(opts: AgentOptions) {
|
constructor(opts: AgentOptions) {
|
||||||
this._state = { ...this._state, ...opts.initialState };
|
this._state = { ...this._state, ...opts.initialState };
|
||||||
|
|
@ -111,6 +114,14 @@ export class Agent {
|
||||||
appendMessage(m: AppMessage) {
|
appendMessage(m: AppMessage) {
|
||||||
this.patch({ messages: [...this._state.messages, m] });
|
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() {
|
clearMessages() {
|
||||||
this.patch({ messages: [] });
|
this.patch({ messages: [] });
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +130,11 @@ export class Agent {
|
||||||
this.abortController?.abort();
|
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[]) {
|
async prompt(input: string, attachments?: Attachment[]) {
|
||||||
const model = this._state.model;
|
const model = this._state.model;
|
||||||
if (!model) {
|
if (!model) {
|
||||||
|
|
@ -150,6 +166,7 @@ export class Agent {
|
||||||
|
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
this.patch({ isStreaming: true, streamMessage: null, error: undefined });
|
this.patch({ isStreaming: true, streamMessage: null, error: undefined });
|
||||||
|
this.emit({ type: "started" });
|
||||||
|
|
||||||
const reasoning =
|
const reasoning =
|
||||||
this._state.thinkingLevel === "off"
|
this._state.thinkingLevel === "off"
|
||||||
|
|
@ -162,6 +179,12 @@ export class Agent {
|
||||||
tools: this._state.tools,
|
tools: this._state.tools,
|
||||||
model,
|
model,
|
||||||
reasoning,
|
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 {
|
try {
|
||||||
|
|
@ -169,9 +192,12 @@ export class Agent {
|
||||||
let turnDebug: DebugLogEntry | null = null;
|
let turnDebug: DebugLogEntry | null = null;
|
||||||
let turnStart = 0;
|
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);
|
const llmMessages = await this.messageTransformer(this._state.messages);
|
||||||
|
|
||||||
|
console.log("transformed messages:", llmMessages);
|
||||||
for await (const ev of this.transport.run(
|
for await (const ev of this.transport.run(
|
||||||
llmMessages,
|
llmMessages,
|
||||||
userMessage as Message,
|
userMessage as Message,
|
||||||
|
|
@ -292,11 +318,9 @@ export class Agent {
|
||||||
} finally {
|
} finally {
|
||||||
this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set<string>() });
|
this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set<string>() });
|
||||||
this.abortController = undefined;
|
this.abortController = undefined;
|
||||||
|
this.emit({ type: "completed" });
|
||||||
}
|
}
|
||||||
{
|
this.logState("final state:");
|
||||||
const { systemPrompt, model, messages } = this._state;
|
|
||||||
console.log("final state:", { systemPrompt, model, messages });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private patch(p: Partial<AgentState>): void {
|
private patch(p: Partial<AgentState>): void {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import type {
|
import type {
|
||||||
AgentContext,
|
AgentContext,
|
||||||
|
AgentLoopConfig,
|
||||||
Api,
|
Api,
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
AssistantMessageEvent,
|
AssistantMessageEvent,
|
||||||
Context,
|
Context,
|
||||||
Message,
|
Message,
|
||||||
Model,
|
Model,
|
||||||
PromptConfig,
|
|
||||||
SimpleStreamOptions,
|
SimpleStreamOptions,
|
||||||
ToolCall,
|
ToolCall,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
|
|
@ -348,9 +348,10 @@ export class AppTransport implements AgentTransport {
|
||||||
tools: cfg.tools,
|
tools: cfg.tools,
|
||||||
};
|
};
|
||||||
|
|
||||||
const pc: PromptConfig = {
|
const pc: AgentLoopConfig = {
|
||||||
model: cfg.model,
|
model: cfg.model,
|
||||||
reasoning: cfg.reasoning,
|
reasoning: cfg.reasoning,
|
||||||
|
getQueuedMessages: cfg.getQueuedMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Yield events from the upstream agentLoop iterator
|
// 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 { getAppStorage } from "../../storage/app-storage.js";
|
||||||
import type { AgentRunConfig, AgentTransport } from "./types.js";
|
import type { AgentRunConfig, AgentTransport } from "./types.js";
|
||||||
|
|
||||||
|
|
@ -34,10 +40,11 @@ export class ProviderTransport implements AgentTransport {
|
||||||
tools: cfg.tools,
|
tools: cfg.tools,
|
||||||
};
|
};
|
||||||
|
|
||||||
const pc: PromptConfig = {
|
const pc: AgentLoopConfig = {
|
||||||
model,
|
model,
|
||||||
reasoning: cfg.reasoning,
|
reasoning: cfg.reasoning,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
getQueuedMessages: cfg.getQueuedMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Yield events from agentLoop
|
// 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.
|
// The minimal configuration needed to run a turn.
|
||||||
export interface AgentRunConfig {
|
export interface AgentRunConfig {
|
||||||
|
|
@ -6,6 +6,7 @@ export interface AgentRunConfig {
|
||||||
tools: AgentTool<any>[];
|
tools: AgentTool<any>[];
|
||||||
model: Model<any>;
|
model: Model<any>;
|
||||||
reasoning?: "low" | "medium" | "high";
|
reasoning?: "low" | "medium" | "high";
|
||||||
|
getQueuedMessages?: <T>() => Promise<QueuedMessage<T>[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events yielded by transports must match the @mariozechner/pi-ai prompt() events.
|
// 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 {
|
export class AgentInterface extends LitElement {
|
||||||
// Optional external session: when provided, this component becomes a view over the session
|
// Optional external session: when provided, this component becomes a view over the session
|
||||||
@property({ attribute: false }) session?: Agent;
|
@property({ attribute: false }) session?: Agent;
|
||||||
@property() enableAttachments = true;
|
@property({ type: Boolean }) enableAttachments = true;
|
||||||
@property() enableModelSelector = true;
|
@property({ type: Boolean }) enableModelSelector = true;
|
||||||
@property() enableThinkingSelector = true;
|
@property({ type: Boolean }) enableThinkingSelector = true;
|
||||||
@property() showThemeToggle = false;
|
@property({ type: Boolean }) showThemeToggle = false;
|
||||||
// Optional custom API key prompt handler - if not provided, uses default dialog
|
// Optional custom API key prompt handler - if not provided, uses default dialog
|
||||||
@property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
|
@property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
|
||||||
// Optional callback called before sending a message
|
// Optional callback called before sending a message
|
||||||
|
|
@ -52,6 +52,10 @@ export class AgentInterface extends LitElement {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setAutoScroll(enabled: boolean) {
|
||||||
|
this._autoScroll = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,16 +231,22 @@ export class ToolMessage extends LitElement {
|
||||||
const result: ToolResultMessageType<any> | undefined = this.aborted
|
const result: ToolResultMessageType<any> | undefined = this.aborted
|
||||||
? { role: "toolResult", isError: true, output: "", toolCallId: this.toolCall.id, toolName: this.toolCall.name }
|
? { role: "toolResult", isError: true, output: "", toolCallId: this.toolCall.id, toolName: this.toolCall.name }
|
||||||
: this.result;
|
: this.result;
|
||||||
const toolContent = renderTool(
|
const renderResult = renderTool(
|
||||||
toolName,
|
toolName,
|
||||||
this.toolCall.arguments,
|
this.toolCall.arguments,
|
||||||
result,
|
result,
|
||||||
!this.aborted && (this.isStreaming || this.pending),
|
!this.aborted && (this.isStreaming || this.pending),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle custom rendering (no card wrapper)
|
||||||
|
if (renderResult.isCustom) {
|
||||||
|
return renderResult.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: wrap in card
|
||||||
return html`
|
return html`
|
||||||
<div class="p-2.5 border border-border rounded-md bg-card text-card-foreground shadow-xs">
|
<div class="p-2.5 border border-border rounded-md bg-card text-card-foreground shadow-xs">
|
||||||
${toolContent}
|
${renderResult.content}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,15 @@ export interface SandboxResult {
|
||||||
*/
|
*/
|
||||||
export type SandboxUrlProvider = () => string;
|
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")
|
@customElement("sandbox-iframe")
|
||||||
export class SandboxIframe extends LitElement {
|
export class SandboxIframe extends LitElement {
|
||||||
private iframe?: HTMLIFrameElement;
|
private iframe?: HTMLIFrameElement;
|
||||||
|
|
@ -79,6 +88,26 @@ export class SandboxIframe extends LitElement {
|
||||||
// loadContent is always used for HTML artifacts
|
// loadContent is always used for HTML artifacts
|
||||||
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, true);
|
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
|
// Remove previous iframe if exists
|
||||||
this.iframe?.remove();
|
this.iframe?.remove();
|
||||||
|
|
||||||
|
|
@ -104,10 +133,11 @@ export class SandboxIframe extends LitElement {
|
||||||
// Update router with iframe reference BEFORE appending to DOM
|
// Update router with iframe reference BEFORE appending to DOM
|
||||||
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
|
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) => {
|
const readyHandler = (e: MessageEvent) => {
|
||||||
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
||||||
window.removeEventListener("message", readyHandler);
|
window.removeEventListener("message", readyHandler);
|
||||||
|
window.removeEventListener("message", errorHandler);
|
||||||
|
|
||||||
// Send content to sandbox
|
// Send content to sandbox
|
||||||
this.iframe?.contentWindow?.postMessage(
|
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", readyHandler);
|
||||||
|
window.addEventListener("message", errorHandler);
|
||||||
|
|
||||||
this.appendChild(this.iframe);
|
this.appendChild(this.iframe);
|
||||||
}
|
}
|
||||||
|
|
@ -228,15 +278,22 @@ export class SandboxIframe extends LitElement {
|
||||||
resolve({
|
resolve({
|
||||||
success: false,
|
success: false,
|
||||||
console: consoleProvider.getLogs(),
|
console: consoleProvider.getLogs(),
|
||||||
error: { message: "Execution timeout (30s)", stack: "" },
|
error: { message: "Execution timeout (120s)", stack: "" },
|
||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 120000);
|
||||||
|
|
||||||
// 4. Prepare HTML and create iframe
|
// 4. Prepare HTML and create iframe
|
||||||
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, isHtmlArtifact);
|
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) {
|
if (this.sandboxUrlProvider) {
|
||||||
// Browser extension mode: wait for sandbox-ready
|
// Browser extension mode: wait for sandbox-ready
|
||||||
this.iframe = document.createElement("iframe");
|
this.iframe = document.createElement("iframe");
|
||||||
|
|
@ -247,10 +304,11 @@ export class SandboxIframe extends LitElement {
|
||||||
// Update router with iframe reference BEFORE appending to DOM
|
// Update router with iframe reference BEFORE appending to DOM
|
||||||
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
|
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) => {
|
const readyHandler = (e: MessageEvent) => {
|
||||||
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
|
||||||
window.removeEventListener("message", readyHandler);
|
window.removeEventListener("message", readyHandler);
|
||||||
|
window.removeEventListener("message", errorHandler);
|
||||||
|
|
||||||
// Send content to sandbox
|
// Send content to sandbox
|
||||||
this.iframe?.contentWindow?.postMessage(
|
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", readyHandler);
|
||||||
|
window.addEventListener("message", errorHandler);
|
||||||
|
|
||||||
this.appendChild(this.iframe);
|
this.appendChild(this.iframe);
|
||||||
} else {
|
} 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
|
* Prepare complete HTML document with runtime + user code
|
||||||
* PUBLIC so HtmlArtifact can use it for download button
|
* PUBLIC so HtmlArtifact can use it for download button
|
||||||
|
|
@ -315,6 +412,9 @@ export class SandboxIframe extends LitElement {
|
||||||
return runtime + userCode;
|
return runtime + userCode;
|
||||||
} else {
|
} else {
|
||||||
// REPL - wrap code in HTML with runtime and call complete() when done
|
// 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>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -326,7 +426,7 @@ export class SandboxIframe extends LitElement {
|
||||||
try {
|
try {
|
||||||
// Wrap user code in async function to capture return value
|
// Wrap user code in async function to capture return value
|
||||||
const userCodeFunc = async () => {
|
const userCodeFunc = async () => {
|
||||||
${userCode}
|
${escapedUserCode}
|
||||||
};
|
};
|
||||||
|
|
||||||
const returnValue = await userCodeFunc();
|
const returnValue = await userCodeFunc();
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ export class RuntimeMessageRouter {
|
||||||
|
|
||||||
// Setup global listener if not already done
|
// Setup global listener if not already done
|
||||||
this.setupListener();
|
this.setupListener();
|
||||||
console.log(`Registered sandbox: ${sandboxId}, providers: ${providers.length}, consumers: ${consumers.length}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -72,7 +71,6 @@ export class RuntimeMessageRouter {
|
||||||
if (context) {
|
if (context) {
|
||||||
context.iframe = iframe;
|
context.iframe = iframe;
|
||||||
}
|
}
|
||||||
console.log("Set iframe for sandbox:", sandboxId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -96,7 +94,6 @@ export class RuntimeMessageRouter {
|
||||||
this.userScriptMessageListener = null;
|
this.userScriptMessageListener = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("Unregistered sandbox:", sandboxId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -108,7 +105,6 @@ export class RuntimeMessageRouter {
|
||||||
if (context) {
|
if (context) {
|
||||||
context.consumers.add(consumer);
|
context.consumers.add(consumer);
|
||||||
}
|
}
|
||||||
console.log("Added consumer for sandbox:", sandboxId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -119,7 +115,6 @@ export class RuntimeMessageRouter {
|
||||||
if (context) {
|
if (context) {
|
||||||
context.consumers.delete(consumer);
|
context.consumers.delete(consumer);
|
||||||
}
|
}
|
||||||
console.log("Removed consumer for sandbox:", sandboxId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -132,18 +127,8 @@ export class RuntimeMessageRouter {
|
||||||
const { sandboxId, messageId } = e.data;
|
const { sandboxId, messageId } = e.data;
|
||||||
if (!sandboxId) return;
|
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);
|
const context = this.sandboxes.get(sandboxId);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
console.log("[ROUTER] No context found for sandbox:", sandboxId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,19 +146,15 @@ export class RuntimeMessageRouter {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Try provider handlers first (for bidirectional comm)
|
// 1. Try provider handlers first (for bidirectional comm)
|
||||||
console.log("[ROUTER] Broadcasting to", context.providers.length, "providers");
|
|
||||||
for (const provider of context.providers) {
|
for (const provider of context.providers) {
|
||||||
if (provider.handleMessage) {
|
if (provider.handleMessage) {
|
||||||
console.log("[ROUTER] Calling provider.handleMessage for", provider.constructor.name);
|
|
||||||
await provider.handleMessage(e.data, respond);
|
await provider.handleMessage(e.data, respond);
|
||||||
// Don't stop - let consumers also handle the message
|
// Don't stop - let consumers also handle the message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Broadcast to consumers (one-way messages or lifecycle events)
|
// 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) {
|
for (const consumer of context.consumers) {
|
||||||
console.log("[ROUTER] Calling consumer.handleMessage");
|
|
||||||
await consumer.handleMessage(e.data);
|
await consumer.handleMessage(e.data);
|
||||||
// Don't stop - let all consumers see the message
|
// Don't stop - let all consumers see the message
|
||||||
}
|
}
|
||||||
|
|
@ -186,7 +167,6 @@ export class RuntimeMessageRouter {
|
||||||
if (!this.userScriptMessageListener) {
|
if (!this.userScriptMessageListener) {
|
||||||
// Guard: check if we're in extension context
|
// Guard: check if we're in extension context
|
||||||
if (typeof chrome === "undefined" || !chrome.runtime?.onUserScriptMessage) {
|
if (typeof chrome === "undefined" || !chrome.runtime?.onUserScriptMessage) {
|
||||||
console.log("[RuntimeMessageRouter] User script API not available (not in extension context)");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,8 +177,6 @@ export class RuntimeMessageRouter {
|
||||||
const context = this.sandboxes.get(sandboxId);
|
const context = this.sandboxes.get(sandboxId);
|
||||||
if (!context) return false;
|
if (!context) return false;
|
||||||
|
|
||||||
console.log("Router received user script message for sandbox:", sandboxId, message);
|
|
||||||
|
|
||||||
const respond = (response: any) => {
|
const respond = (response: any) => {
|
||||||
sendResponse({
|
sendResponse({
|
||||||
...response,
|
...response,
|
||||||
|
|
@ -227,7 +205,6 @@ export class RuntimeMessageRouter {
|
||||||
};
|
};
|
||||||
|
|
||||||
chrome.runtime.onUserScriptMessage.addListener(this.userScriptMessageListener);
|
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
|
// Tool renderers
|
||||||
export { DefaultRenderer } from "./tools/renderers/DefaultRenderer.js";
|
export { DefaultRenderer } from "./tools/renderers/DefaultRenderer.js";
|
||||||
export { GetCurrentTimeRenderer } from "./tools/renderers/GetCurrentTimeRenderer.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";
|
export type { Attachment } from "./utils/attachment-utils.js";
|
||||||
// Utils
|
// Utils
|
||||||
export { loadAttachment } from "./utils/attachment-utils.js";
|
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
|
## When to Use
|
||||||
- Quick calculations or data transformations
|
- Quick calculations or data transformations
|
||||||
- Testing JavaScript code snippets
|
- Testing JavaScript code snippets in isolation
|
||||||
- Processing data with libraries (XLSX, CSV, etc.)
|
- Processing data with libraries (XLSX, CSV, etc.)
|
||||||
- Creating visualizations (charts, graphs)
|
- 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;
|
- Chart.js: const Chart = (await import('https://esm.run/chart.js/auto')).default;
|
||||||
- Three.js: const THREE = await import('https://esm.run/three');
|
- Three.js: const THREE = await import('https://esm.run/three');
|
||||||
|
|
||||||
## Important Notes
|
## Persistence between tool calls
|
||||||
- Graphics: Use fixed dimensions (800x600), NOT window.innerWidth/Height
|
- Objects stored on global scope do not persist between calls.
|
||||||
- Chart.js: Set options: { responsive: false, animation: false }
|
- Use artifacts as a key-value JSON object store:
|
||||||
- Three.js: renderer.setSize(800, 600) with matching aspect ratio
|
- Use createOrUpdateArtifact(filename, content) to persist data between calls. JSON objects are auto-stringified.
|
||||||
- Output: All console.log() calls are captured and displayed
|
- 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
|
## Example
|
||||||
const data = [10, 20, 15, 25];
|
const data = [10, 20, 15, 25];
|
||||||
const sum = data.reduce((a, b) => a + b, 0);
|
const sum = data.reduce((a, b) => a + b, 0);
|
||||||
const avg = sum / data.length;
|
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
|
// Artifacts Tool
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { FileCode2 } from "lucide";
|
||||||
import "../../components/ConsoleBlock.js";
|
import "../../components/ConsoleBlock.js";
|
||||||
import { i18n } from "../../utils/i18n.js";
|
import { i18n } from "../../utils/i18n.js";
|
||||||
import { renderCollapsibleHeader, renderHeader } from "../renderer-registry.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 { ArtifactPill } from "./ArtifactPill.js";
|
||||||
import type { ArtifactsPanel, ArtifactsParams } from "./artifacts.js";
|
import type { ArtifactsPanel, ArtifactsParams } from "./artifacts.js";
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
||||||
params: ArtifactsParams | undefined,
|
params: ArtifactsParams | undefined,
|
||||||
result: ToolResultMessage<undefined> | undefined,
|
result: ToolResultMessage<undefined> | undefined,
|
||||||
isStreaming?: boolean,
|
isStreaming?: boolean,
|
||||||
): TemplateResult {
|
): ToolRenderResult {
|
||||||
const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete";
|
const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete";
|
||||||
|
|
||||||
// Create refs for collapsible sections
|
// Create refs for collapsible sections
|
||||||
|
|
@ -101,7 +101,8 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
||||||
|
|
||||||
const isHtml = filename?.endsWith(".html");
|
const isHtml = filename?.endsWith(".html");
|
||||||
|
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
${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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other errors, just show error message
|
// For other errors, just show error message
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
${renderHeader(state, FileCode2, headerText)}
|
${renderHeader(state, FileCode2, headerText)}
|
||||||
<div class="text-sm text-destructive">${result.output || i18n("An error occurred")}</div>
|
<div class="text-sm text-destructive">${result.output || i18n("An error occurred")}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full params + result
|
// Full params + result
|
||||||
|
|
@ -136,27 +142,33 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
||||||
// GET command: show code block with file content
|
// GET command: show code block with file content
|
||||||
if (command === "get") {
|
if (command === "get") {
|
||||||
const fileContent = result.output || i18n("(no output)");
|
const fileContent = result.output || i18n("(no output)");
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||||
<code-block .code=${fileContent} language=${getLanguageFromFilename(filename)}></code-block>
|
<code-block .code=${fileContent} language=${getLanguageFromFilename(filename)}></code-block>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOGS command: show console block
|
// LOGS command: show console block
|
||||||
if (command === "logs") {
|
if (command === "logs") {
|
||||||
const logs = result.output || i18n("(no output)");
|
const logs = result.output || i18n("(no output)");
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
||||||
<console-block .content=${logs}></console-block>
|
<console-block .content=${logs}></console-block>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// CREATE/UPDATE/REWRITE: always show code block, + console block for .html files
|
// 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 isHtml = filename?.endsWith(".html");
|
||||||
const logs = result.output || "";
|
const logs = result.output || "";
|
||||||
|
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
${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">
|
<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>` : ""}
|
${isHtml && logs ? html`<console-block .content=${logs}></console-block>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command === "update") {
|
if (command === "update") {
|
||||||
const isHtml = filename?.endsWith(".html");
|
const isHtml = filename?.endsWith(".html");
|
||||||
const logs = result.output || "";
|
const logs = result.output || "";
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
${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">
|
<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>` : ""}
|
${isHtml && logs ? html`<console-block .content=${logs}></console-block>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// For DELETE, just show header
|
// For DELETE, just show header
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}
|
${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Params only (streaming or waiting for result)
|
// Params only (streaming or waiting for result)
|
||||||
|
|
@ -204,7 +225,7 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
||||||
|
|
||||||
// If no command yet
|
// If no command yet
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return renderHeader(state, FileCode2, i18n("Preparing artifact..."));
|
return { content: renderHeader(state, FileCode2, i18n("Preparing artifact...")), isCustom: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const labels = getCommandLabels(command);
|
const labels = getCommandLabels(command);
|
||||||
|
|
@ -214,7 +235,8 @@ export class ArtifactsToolRenderer implements ToolRenderer<ArtifactsParams, unde
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "create":
|
case "create":
|
||||||
case "rewrite":
|
case "rewrite":
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
|
|
||||||
case "update":
|
case "update":
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
|
|
||||||
case "get":
|
case "get":
|
||||||
case "logs":
|
case "logs":
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
${renderCollapsibleHeader(state, FileCode2, renderHeaderWithPill(headerText, filename), contentRef, chevronRef, false)}
|
||||||
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300"></div>
|
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return html`
|
return {
|
||||||
|
content: html`
|
||||||
<div>
|
<div>
|
||||||
${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}
|
${renderHeader(state, FileCode2, renderHeaderWithPill(headerText, filename))}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No params or result yet
|
// 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}`;
|
let result = `Created file ${params.filename}`;
|
||||||
if (this.getFileType(params.filename) === "html" && !options.skipWait) {
|
if (this.getFileType(params.filename) === "html" && !options.skipWait) {
|
||||||
const logs = await this.waitForHtmlExecution(params.filename);
|
const logs = await this.waitForHtmlExecution(params.filename);
|
||||||
result += logs;
|
result += `\n${logs}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -486,7 +486,7 @@ export class ArtifactsPanel extends LitElement {
|
||||||
let result = "";
|
let result = "";
|
||||||
if (this.getFileType(params.filename) === "html" && !options.skipWait) {
|
if (this.getFileType(params.filename) === "html" && !options.skipWait) {
|
||||||
const logs = await this.waitForHtmlExecution(params.filename);
|
const logs = await this.waitForHtmlExecution(params.filename);
|
||||||
result += logs;
|
result += `\n${logs}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { TemplateResult } from "@mariozechner/mini-lit";
|
|
||||||
import type { ToolResultMessage } from "@mariozechner/pi-ai";
|
import type { ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
|
import "./javascript-repl.js"; // Auto-registers the renderer
|
||||||
import { getToolRenderer, registerToolRenderer } from "./renderer-registry.js";
|
import { getToolRenderer, registerToolRenderer } from "./renderer-registry.js";
|
||||||
import { BashRenderer } from "./renderers/BashRenderer.js";
|
import { BashRenderer } from "./renderers/BashRenderer.js";
|
||||||
import { DefaultRenderer } from "./renderers/DefaultRenderer.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
|
// Register all built-in tool renderers
|
||||||
registerToolRenderer("bash", new BashRenderer());
|
registerToolRenderer("bash", new BashRenderer());
|
||||||
|
|
@ -18,7 +18,7 @@ export function renderTool(
|
||||||
params: any | undefined,
|
params: any | undefined,
|
||||||
result: ToolResultMessage | undefined,
|
result: ToolResultMessage | undefined,
|
||||||
isStreaming?: boolean,
|
isStreaming?: boolean,
|
||||||
): TemplateResult {
|
): ToolRenderResult {
|
||||||
const renderer = getToolRenderer(toolName);
|
const renderer = getToolRenderer(toolName);
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
return renderer.render(params, result, isStreaming);
|
return renderer.render(params, result, isStreaming);
|
||||||
|
|
@ -26,4 +26,4 @@ export function renderTool(
|
||||||
return defaultRenderer.render(params, result, isStreaming);
|
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 { AgentTool, ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
import { type Static, Type } from "@sinclair/typebox";
|
import { type Static, Type } from "@sinclair/typebox";
|
||||||
import { createRef, ref } from "lit/directives/ref.js";
|
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 { JAVASCRIPT_REPL_DESCRIPTION } from "../prompts/tool-prompts.js";
|
||||||
import type { Attachment } from "../utils/attachment-utils.js";
|
import type { Attachment } from "../utils/attachment-utils.js";
|
||||||
import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.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
|
// Execute JavaScript code with attachments using SandboxedIframe
|
||||||
export async function executeJavaScript(
|
export async function executeJavaScript(
|
||||||
|
|
@ -194,7 +194,7 @@ export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScri
|
||||||
params: JavaScriptReplParams | undefined,
|
params: JavaScriptReplParams | undefined,
|
||||||
result: ToolResultMessage<JavaScriptReplResult> | undefined,
|
result: ToolResultMessage<JavaScriptReplResult> | undefined,
|
||||||
isStreaming?: boolean,
|
isStreaming?: boolean,
|
||||||
): TemplateResult {
|
): ToolRenderResult {
|
||||||
// Determine status
|
// Determine status
|
||||||
const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete";
|
const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete";
|
||||||
|
|
||||||
|
|
@ -236,38 +236,44 @@ export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScri
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return html`
|
return {
|
||||||
<div>
|
content: html`
|
||||||
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
<div>
|
||||||
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
||||||
<code-block .code=${params.code || ""} language="javascript"></code-block>
|
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
||||||
${output ? html`<console-block .content=${output} .variant=${result.isError ? "error" : "default"}></console-block>` : ""}
|
<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>
|
</div>
|
||||||
${
|
`,
|
||||||
attachments.length
|
isCustom: false,
|
||||||
? html`<div class="flex flex-wrap gap-2 mt-3">
|
};
|
||||||
${attachments.map((att) => html`<attachment-tile .attachment=${att}></attachment-tile>`)}
|
|
||||||
</div>`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just params (streaming or waiting for result)
|
// Just params (streaming or waiting for result)
|
||||||
if (params) {
|
if (params) {
|
||||||
return html`
|
return {
|
||||||
<div>
|
content: html`
|
||||||
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
<div>
|
||||||
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
${renderCollapsibleHeader(state, Code, params.title ? params.title : i18n("Executing JavaScript"), codeContentRef, codeChevronRef, false)}
|
||||||
${params.code ? html`<code-block .code=${params.code} language="javascript"></code-block>` : ""}
|
<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>
|
||||||
</div>
|
`,
|
||||||
`;
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// No params or result yet
|
// 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 type { ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
import { SquareTerminal } from "lucide";
|
import { SquareTerminal } from "lucide";
|
||||||
import { i18n } from "../../utils/i18n.js";
|
import { i18n } from "../../utils/i18n.js";
|
||||||
import { renderHeader } from "../renderer-registry.js";
|
import { renderHeader } from "../renderer-registry.js";
|
||||||
import type { ToolRenderer } from "../types.js";
|
import type { ToolRenderer, ToolRenderResult } from "../types.js";
|
||||||
|
|
||||||
interface BashParams {
|
interface BashParams {
|
||||||
command: string;
|
command: string;
|
||||||
|
|
@ -11,32 +11,38 @@ interface BashParams {
|
||||||
|
|
||||||
// Bash tool has undefined details (only uses output)
|
// Bash tool has undefined details (only uses output)
|
||||||
export class BashRenderer implements ToolRenderer<BashParams, undefined> {
|
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";
|
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||||
|
|
||||||
// With result: show command + output
|
// With result: show command + output
|
||||||
if (result && params?.command) {
|
if (result && params?.command) {
|
||||||
const output = result.output || "";
|
const output = result.output || "";
|
||||||
const combined = output ? `> ${params.command}\n\n${output}` : `> ${params.command}`;
|
const combined = output ? `> ${params.command}\n\n${output}` : `> ${params.command}`;
|
||||||
return html`
|
return {
|
||||||
<div class="space-y-3">
|
content: html`
|
||||||
${renderHeader(state, SquareTerminal, i18n("Running command..."))}
|
<div class="space-y-3">
|
||||||
<console-block .content=${combined} .variant=${result.isError ? "error" : "default"}></console-block>
|
${renderHeader(state, SquareTerminal, i18n("Running command..."))}
|
||||||
</div>
|
<console-block .content=${combined} .variant=${result.isError ? "error" : "default"}></console-block>
|
||||||
`;
|
</div>
|
||||||
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just params (streaming or waiting)
|
// Just params (streaming or waiting)
|
||||||
if (params?.command) {
|
if (params?.command) {
|
||||||
return html`
|
return {
|
||||||
<div class="space-y-3">
|
content: html`
|
||||||
${renderHeader(state, SquareTerminal, i18n("Running command..."))}
|
<div class="space-y-3">
|
||||||
<console-block .content=${`> ${params.command}`}></console-block>
|
${renderHeader(state, SquareTerminal, i18n("Running command..."))}
|
||||||
</div>
|
<console-block .content=${`> ${params.command}`}></console-block>
|
||||||
`;
|
</div>
|
||||||
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// No params yet
|
// 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 type { ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
import { Calculator } from "lucide";
|
import { Calculator } from "lucide";
|
||||||
import { i18n } from "../../utils/i18n.js";
|
import { i18n } from "../../utils/i18n.js";
|
||||||
import { renderHeader } from "../renderer-registry.js";
|
import { renderHeader } from "../renderer-registry.js";
|
||||||
import type { ToolRenderer } from "../types.js";
|
import type { ToolRenderer, ToolRenderResult } from "../types.js";
|
||||||
|
|
||||||
interface CalculateParams {
|
interface CalculateParams {
|
||||||
expression: string;
|
expression: string;
|
||||||
|
|
@ -11,7 +11,7 @@ interface CalculateParams {
|
||||||
|
|
||||||
// Calculate tool has undefined details (only uses output)
|
// Calculate tool has undefined details (only uses output)
|
||||||
export class CalculateRenderer implements ToolRenderer<CalculateParams, undefined> {
|
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";
|
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||||
|
|
||||||
// Full params + full result
|
// Full params + full result
|
||||||
|
|
@ -20,29 +20,35 @@ export class CalculateRenderer implements ToolRenderer<CalculateParams, undefine
|
||||||
|
|
||||||
// Error: show expression in header, error below
|
// Error: show expression in header, error below
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
return html`
|
return {
|
||||||
<div class="space-y-3">
|
content: html`
|
||||||
${renderHeader(state, Calculator, params.expression)}
|
<div class="space-y-3">
|
||||||
<div class="text-sm text-destructive">${output}</div>
|
${renderHeader(state, Calculator, params.expression)}
|
||||||
</div>
|
<div class="text-sm text-destructive">${output}</div>
|
||||||
`;
|
</div>
|
||||||
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success: show expression = result in header
|
// 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
|
// Full params, no result: just show header with expression in it
|
||||||
if (params?.expression) {
|
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
|
// Partial params (empty expression), no result
|
||||||
if (params && !params.expression) {
|
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
|
// 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 type { ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
import { i18n } from "../../utils/i18n.js";
|
import { i18n } from "../../utils/i18n.js";
|
||||||
import type { ToolRenderer } from "../types.js";
|
import type { ToolRenderer, ToolRenderResult } from "../types.js";
|
||||||
|
|
||||||
export class DefaultRenderer implements ToolRenderer {
|
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
|
// Show result if available
|
||||||
if (result) {
|
if (result) {
|
||||||
const text = result.output || i18n("(no output)");
|
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
|
// Show params
|
||||||
|
|
@ -25,13 +28,19 @@ export class DefaultRenderer implements ToolRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStreaming && (!text || text === "{}" || text === "null")) {
|
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
|
// 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 type { ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
import { Clock } from "lucide";
|
import { Clock } from "lucide";
|
||||||
import { i18n } from "../../utils/i18n.js";
|
import { i18n } from "../../utils/i18n.js";
|
||||||
import { renderHeader } from "../renderer-registry.js";
|
import { renderHeader } from "../renderer-registry.js";
|
||||||
import type { ToolRenderer } from "../types.js";
|
import type { ToolRenderer, ToolRenderResult } from "../types.js";
|
||||||
|
|
||||||
interface GetCurrentTimeParams {
|
interface GetCurrentTimeParams {
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
|
|
@ -11,7 +11,10 @@ interface GetCurrentTimeParams {
|
||||||
|
|
||||||
// GetCurrentTime tool has undefined details (only uses output)
|
// GetCurrentTime tool has undefined details (only uses output)
|
||||||
export class GetCurrentTimeRenderer implements ToolRenderer<GetCurrentTimeParams, undefined> {
|
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";
|
const state = result ? (result.isError ? "error" : "complete") : "inprogress";
|
||||||
|
|
||||||
// Full params + full result
|
// Full params + full result
|
||||||
|
|
@ -23,16 +26,19 @@ export class GetCurrentTimeRenderer implements ToolRenderer<GetCurrentTimeParams
|
||||||
|
|
||||||
// Error: show header, error below
|
// Error: show header, error below
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
return html`
|
return {
|
||||||
<div class="space-y-3">
|
content: html`
|
||||||
${renderHeader(state, Clock, headerText)}
|
<div class="space-y-3">
|
||||||
<div class="text-sm text-destructive">${output}</div>
|
${renderHeader(state, Clock, headerText)}
|
||||||
</div>
|
<div class="text-sm text-destructive">${output}</div>
|
||||||
`;
|
</div>
|
||||||
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success: show time in header
|
// Success: show time in header
|
||||||
return renderHeader(state, Clock, `${headerText}: ${output}`);
|
return { content: renderHeader(state, Clock, `${headerText}: ${output}`), isCustom: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full result, no params
|
// Full result, no params
|
||||||
|
|
@ -41,29 +47,38 @@ export class GetCurrentTimeRenderer implements ToolRenderer<GetCurrentTimeParams
|
||||||
|
|
||||||
// Error: show header, error below
|
// Error: show header, error below
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
return html`
|
return {
|
||||||
<div class="space-y-3">
|
content: html`
|
||||||
${renderHeader(state, Clock, i18n("Getting current date and time"))}
|
<div class="space-y-3">
|
||||||
<div class="text-sm text-destructive">${output}</div>
|
${renderHeader(state, Clock, i18n("Getting current date and time"))}
|
||||||
</div>
|
<div class="text-sm text-destructive">${output}</div>
|
||||||
`;
|
</div>
|
||||||
|
`,
|
||||||
|
isCustom: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success: show time in header
|
// 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
|
// Full params, no result: show timezone info in header
|
||||||
if (params?.timezone) {
|
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
|
// Partial params (no timezone) or empty params, no result
|
||||||
if (params) {
|
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
|
// 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 { ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
import type { TemplateResult } from "lit";
|
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> {
|
export interface ToolRenderer<TParams = any, TDetails = any> {
|
||||||
render(
|
render(
|
||||||
params: TParams | undefined,
|
params: TParams | undefined,
|
||||||
result: ToolResultMessage<TDetails> | undefined,
|
result: ToolResultMessage<TDetails> | undefined,
|
||||||
isStreaming?: boolean,
|
isStreaming?: boolean,
|
||||||
): TemplateResult;
|
): ToolRenderResult;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue