mac: bundle web chat assets

This commit is contained in:
Peter Steinberger
2025-12-06 05:01:28 +01:00
parent 15cdeeddaf
commit 42d843297d
315 changed files with 16618 additions and 20 deletions

View File

@@ -0,0 +1,39 @@
import { LitElement } from "lit";
import "./MessageEditor.js";
import "./MessageList.js";
import "./Messages.js";
import type { Agent } from "../agent/agent.js";
import "./StreamingMessageContainer.js";
import type { Attachment } from "../utils/attachment-utils.js";
export declare class AgentInterface extends LitElement {
session?: Agent;
enableAttachments: boolean;
enableModelSelector: boolean;
enableThinkingSelector: boolean;
showThemeToggle: boolean;
onApiKeyRequired?: (provider: string) => Promise<boolean>;
onBeforeSend?: () => void | Promise<void>;
onBeforeToolCall?: (toolName: string, args: any) => boolean | Promise<boolean>;
onCostClick?: () => void;
private _messageEditor;
private _streamingContainer;
private _autoScroll;
private _lastScrollTop;
private _lastClientHeight;
private _scrollContainer?;
private _resizeObserver?;
private _unsubscribeSession?;
setInput(text: string, attachments?: Attachment[]): void;
setAutoScroll(enabled: boolean): void;
protected createRenderRoot(): HTMLElement | DocumentFragment;
willUpdate(changedProperties: Map<string, any>): void;
connectedCallback(): Promise<void>;
disconnectedCallback(): void;
private setupSessionSubscription;
private _handleScroll;
sendMessage(input: string, attachments?: Attachment[]): Promise<void>;
private renderMessages;
private renderStats;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=AgentInterface.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"AgentInterface.d.ts","sourceRoot":"","sources":["../../src/components/AgentInterface.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAIvC,OAAO,oBAAoB,CAAC;AAC5B,OAAO,kBAAkB,CAAC;AAC1B,OAAO,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,KAAK,EAAc,MAAM,mBAAmB,CAAC;AAE3D,OAAO,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAK/D,qBACa,cAAe,SAAQ,UAAU;IAEb,OAAO,CAAC,EAAE,KAAK,CAAC;IACnB,iBAAiB,UAAQ;IACzB,mBAAmB,UAAQ;IAC3B,sBAAsB,UAAQ;IAC9B,eAAe,UAAS;IAErB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1D,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1C,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAGhC,OAAO,CAAC,cAAc,CAAiB;IAC1B,OAAO,CAAC,mBAAmB,CAA6B;IAE9F,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,gBAAgB,CAAC,CAAc;IACvC,OAAO,CAAC,eAAe,CAAC,CAAiB;IACzC,OAAO,CAAC,mBAAmB,CAAC,CAAa;IAElC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE;IAWjD,aAAa,CAAC,OAAO,EAAE,OAAO;cAIlB,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,UAAU,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC;IASxC,iBAAiB;IAkCvB,oBAAoB;IAmB7B,OAAO,CAAC,wBAAwB;IAqBhC,OAAO,CAAC,aAAa,CAwBnB;IAEW,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE;IAsClE,OAAO,CAAC,cAAc;IAmCtB,OAAO,CAAC,WAAW;IAgDV,MAAM;CA4Cf"}

View File

@@ -0,0 +1,330 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators.js";
import { ModelSelector } from "../dialogs/ModelSelector.js";
import "./MessageEditor.js";
import "./MessageList.js";
import "./Messages.js"; // Import for side effects to register the custom elements
import { getAppStorage } from "../storage/app-storage.js";
import "./StreamingMessageContainer.js";
import { formatUsage } from "../utils/format.js";
import { i18n } from "../utils/i18n.js";
let AgentInterface = class AgentInterface extends LitElement {
constructor() {
super(...arguments);
this.enableAttachments = true;
this.enableModelSelector = true;
this.enableThinkingSelector = true;
this.showThemeToggle = false;
this._autoScroll = true;
this._lastScrollTop = 0;
this._lastClientHeight = 0;
this._handleScroll = (_ev) => {
if (!this._scrollContainer)
return;
const currentScrollTop = this._scrollContainer.scrollTop;
const scrollHeight = this._scrollContainer.scrollHeight;
const clientHeight = this._scrollContainer.clientHeight;
const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight;
// Ignore relayout due to message editor getting pushed up by stats
if (clientHeight < this._lastClientHeight) {
this._lastClientHeight = clientHeight;
return;
}
// Only disable auto-scroll if user scrolled UP or is far from bottom
if (currentScrollTop !== 0 && currentScrollTop < this._lastScrollTop && distanceFromBottom > 50) {
this._autoScroll = false;
}
else if (distanceFromBottom < 10) {
// Re-enable if very close to bottom
this._autoScroll = true;
}
this._lastScrollTop = currentScrollTop;
this._lastClientHeight = clientHeight;
};
}
setInput(text, attachments) {
const update = () => {
if (!this._messageEditor)
requestAnimationFrame(update);
else {
this._messageEditor.value = text;
this._messageEditor.attachments = attachments || [];
}
};
update();
}
setAutoScroll(enabled) {
this._autoScroll = enabled;
}
createRenderRoot() {
return this;
}
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
// Re-subscribe when session property changes
if (changedProperties.has("session")) {
this.setupSessionSubscription();
}
}
async connectedCallback() {
super.connectedCallback();
this.style.display = "flex";
this.style.flexDirection = "column";
this.style.height = "100%";
this.style.minHeight = "0";
// Wait for first render to get scroll container
await this.updateComplete;
this._scrollContainer = this.querySelector(".overflow-y-auto");
if (this._scrollContainer) {
// Set up ResizeObserver to detect content changes
this._resizeObserver = new ResizeObserver(() => {
if (this._autoScroll && this._scrollContainer) {
this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight;
}
});
// Observe the content container inside the scroll container
const contentContainer = this._scrollContainer.querySelector(".max-w-3xl");
if (contentContainer) {
this._resizeObserver.observe(contentContainer);
}
// Set up scroll listener with better detection
this._scrollContainer.addEventListener("scroll", this._handleScroll);
}
// Subscribe to external session if provided
this.setupSessionSubscription();
}
disconnectedCallback() {
super.disconnectedCallback();
// Clean up observers and listeners
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = undefined;
}
if (this._scrollContainer) {
this._scrollContainer.removeEventListener("scroll", this._handleScroll);
}
if (this._unsubscribeSession) {
this._unsubscribeSession();
this._unsubscribeSession = undefined;
}
}
setupSessionSubscription() {
if (this._unsubscribeSession) {
this._unsubscribeSession();
this._unsubscribeSession = undefined;
}
if (!this.session)
return;
this._unsubscribeSession = this.session.subscribe(async (ev) => {
if (ev.type === "state-update") {
if (this._streamingContainer) {
this._streamingContainer.isStreaming = ev.state.isStreaming;
this._streamingContainer.setMessage(ev.state.streamMessage, !ev.state.isStreaming);
}
this.requestUpdate();
}
else if (ev.type === "error-no-model") {
// TODO show some UI feedback
}
else if (ev.type === "error-no-api-key") {
// Handled by onApiKeyRequired callback
}
});
}
async sendMessage(input, attachments) {
if ((!input.trim() && attachments?.length === 0) || this.session?.state.isStreaming)
return;
const session = this.session;
if (!session)
throw new Error("No session set on AgentInterface");
if (!session.state.model)
throw new Error("No model set on AgentInterface");
// Check if API key exists for the provider (only needed in direct mode)
const provider = session.state.model.provider;
const apiKey = await getAppStorage().providerKeys.get(provider);
// If no API key, prompt for it
if (!apiKey) {
if (!this.onApiKeyRequired) {
console.error("No API key configured and no onApiKeyRequired handler set");
return;
}
const success = await this.onApiKeyRequired(provider);
// If still no API key, abort the send
if (!success) {
return;
}
}
// Call onBeforeSend hook before sending
if (this.onBeforeSend) {
await this.onBeforeSend();
}
// Only clear editor after we know we can send
this._messageEditor.value = "";
this._messageEditor.attachments = [];
this._autoScroll = true; // Enable auto-scroll when sending a message
await this.session?.prompt(input, attachments);
}
renderMessages() {
if (!this.session)
return html `<div class="p-4 text-center text-muted-foreground">${i18n("No session available")}</div>`;
const state = this.session.state;
// Build a map of tool results to allow inline rendering in assistant messages
const toolResultsById = new Map();
for (const message of state.messages) {
if (message.role === "toolResult") {
toolResultsById.set(message.toolCallId, message);
}
}
return html `
<div class="flex flex-col gap-3">
<!-- Stable messages list - won't re-render during streaming -->
<message-list
.messages=${this.session.state.messages}
.tools=${state.tools}
.pendingToolCalls=${this.session ? this.session.state.pendingToolCalls : new Set()}
.isStreaming=${state.isStreaming}
.onCostClick=${this.onCostClick}
></message-list>
<!-- Streaming message container - manages its own updates -->
<streaming-message-container
class="${state.isStreaming ? "" : "hidden"}"
.tools=${state.tools}
.isStreaming=${state.isStreaming}
.pendingToolCalls=${state.pendingToolCalls}
.toolResultsById=${toolResultsById}
.onCostClick=${this.onCostClick}
></streaming-message-container>
</div>
`;
}
renderStats() {
if (!this.session)
return html `<div class="text-xs h-5"></div>`;
const state = this.session.state;
const totals = state.messages
.filter((m) => m.role === "assistant")
.reduce((acc, msg) => {
const usage = msg.usage;
if (usage) {
acc.input += usage.input;
acc.output += usage.output;
acc.cacheRead += usage.cacheRead;
acc.cacheWrite += usage.cacheWrite;
acc.cost.total += usage.cost.total;
}
return acc;
}, {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
});
const hasTotals = totals.input || totals.output || totals.cacheRead || totals.cacheWrite;
const totalsText = hasTotals ? formatUsage(totals) : "";
return html `
<div class="text-xs text-muted-foreground flex justify-between items-center h-5">
<div class="flex items-center gap-1">
${this.showThemeToggle ? html `<theme-toggle></theme-toggle>` : html ``}
</div>
<div class="flex ml-auto items-center gap-3">
${totalsText
? this.onCostClick
? html `<span class="cursor-pointer hover:text-foreground transition-colors" @click=${this.onCostClick}>${totalsText}</span>`
: html `<span>${totalsText}</span>`
: ""}
</div>
</div>
`;
}
render() {
if (!this.session)
return html `<div class="p-4 text-center text-muted-foreground">${i18n("No session set")}</div>`;
const session = this.session;
const state = this.session.state;
return html `
<div class="flex flex-col h-full bg-background text-foreground">
<!-- Messages Area -->
<div class="flex-1 overflow-y-auto">
<div class="max-w-3xl mx-auto p-4 pb-0">${this.renderMessages()}</div>
</div>
<!-- Input Area -->
<div class="shrink-0">
<div class="max-w-3xl mx-auto px-2">
<message-editor
.isStreaming=${state.isStreaming}
.currentModel=${state.model}
.thinkingLevel=${state.thinkingLevel}
.showAttachmentButton=${this.enableAttachments}
.showModelSelector=${this.enableModelSelector}
.showThinkingSelector=${this.enableThinkingSelector}
.onSend=${(input, attachments) => {
this.sendMessage(input, attachments);
}}
.onAbort=${() => session.abort()}
.onModelSelect=${() => {
ModelSelector.open(state.model, (model) => session.setModel(model));
}}
.onThinkingChange=${this.enableThinkingSelector
? (level) => {
session.setThinkingLevel(level);
}
: undefined}
></message-editor>
${this.renderStats()}
</div>
</div>
</div>
`;
}
};
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "session", void 0);
__decorate([
property({ type: Boolean })
], AgentInterface.prototype, "enableAttachments", void 0);
__decorate([
property({ type: Boolean })
], AgentInterface.prototype, "enableModelSelector", void 0);
__decorate([
property({ type: Boolean })
], AgentInterface.prototype, "enableThinkingSelector", void 0);
__decorate([
property({ type: Boolean })
], AgentInterface.prototype, "showThemeToggle", void 0);
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "onApiKeyRequired", void 0);
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "onBeforeSend", void 0);
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "onBeforeToolCall", void 0);
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "onCostClick", void 0);
__decorate([
query("message-editor")
], AgentInterface.prototype, "_messageEditor", void 0);
__decorate([
query("streaming-message-container")
], AgentInterface.prototype, "_streamingContainer", void 0);
AgentInterface = __decorate([
customElement("agent-interface")
], AgentInterface);
export { AgentInterface };
// Register custom element with guard
if (!customElements.get("agent-interface")) {
customElements.define("agent-interface", AgentInterface);
}
//# sourceMappingURL=AgentInterface.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
import { LitElement } from "lit";
import type { Attachment } from "../utils/attachment-utils.js";
export declare class AttachmentTile extends LitElement {
attachment: Attachment;
showDelete: boolean;
onDelete?: () => void;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private handleClick;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=AttachmentTile.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"AttachmentTile.d.ts","sourceRoot":"","sources":["../../src/components/AttachmentTile.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAKjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAG/D,qBACa,cAAe,SAAQ,UAAU;IACjB,UAAU,EAAG,UAAU,CAAC;IACvB,UAAU,UAAS;IACpC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;cAEf,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAMlC,OAAO,CAAC,WAAW,CAEjB;IAEO,MAAM;CAmFf"}

View File

@@ -0,0 +1,115 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit/dist/icons.js";
import { LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { html } from "lit/html.js";
import { FileSpreadsheet, FileText, X } from "lucide";
import { AttachmentOverlay } from "../dialogs/AttachmentOverlay.js";
import { i18n } from "../utils/i18n.js";
let AttachmentTile = class AttachmentTile extends LitElement {
constructor() {
super(...arguments);
this.showDelete = false;
this.handleClick = () => {
AttachmentOverlay.open(this.attachment);
};
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
this.classList.add("max-h-16");
}
render() {
const hasPreview = !!this.attachment.preview;
const isImage = this.attachment.type === "image";
const isPdf = this.attachment.mimeType === "application/pdf";
const isDocx = this.attachment.mimeType?.includes("wordprocessingml") ||
this.attachment.fileName.toLowerCase().endsWith(".docx");
const isPptx = this.attachment.mimeType?.includes("presentationml") ||
this.attachment.fileName.toLowerCase().endsWith(".pptx");
const isExcel = this.attachment.mimeType?.includes("spreadsheetml") ||
this.attachment.fileName.toLowerCase().endsWith(".xlsx") ||
this.attachment.fileName.toLowerCase().endsWith(".xls");
// Choose the appropriate icon
const getDocumentIcon = () => {
if (isExcel)
return icon(FileSpreadsheet, "md");
return icon(FileText, "md");
};
return html `
<div class="relative group inline-block">
${hasPreview
? html `
<div class="relative">
<img
src="data:${isImage ? this.attachment.mimeType : "image/png"};base64,${this.attachment.preview}"
class="w-16 h-16 object-cover rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity"
alt="${this.attachment.fileName}"
title="${this.attachment.fileName}"
@click=${this.handleClick}
/>
${isPdf
? html `
<!-- PDF badge overlay -->
<div class="absolute bottom-0 left-0 right-0 bg-background/90 px-1 py-0.5 rounded-b-lg">
<div class="text-[10px] text-muted-foreground text-center font-medium">${i18n("PDF")}</div>
</div>
`
: ""}
</div>
`
: html `
<!-- Fallback: document icon + filename -->
<div
class="w-16 h-16 rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity bg-muted text-muted-foreground flex flex-col items-center justify-center p-2"
@click=${this.handleClick}
title="${this.attachment.fileName}"
>
${getDocumentIcon()}
<div class="text-[10px] text-center truncate w-full">
${this.attachment.fileName.length > 10
? this.attachment.fileName.substring(0, 8) + "..."
: this.attachment.fileName}
</div>
</div>
`}
${this.showDelete
? html `
<button
@click=${(e) => {
e.stopPropagation();
this.onDelete?.();
}}
class="absolute -top-1 -right-1 w-5 h-5 bg-background hover:bg-muted text-muted-foreground hover:text-foreground rounded-full flex items-center justify-center opacity-100 hover:opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100 transition-opacity border border-input shadow-sm"
title="${i18n("Remove")}"
>
${icon(X, "xs")}
</button>
`
: ""}
</div>
`;
}
};
__decorate([
property({ type: Object })
], AttachmentTile.prototype, "attachment", void 0);
__decorate([
property({ type: Boolean })
], AttachmentTile.prototype, "showDelete", void 0);
__decorate([
property()
], AttachmentTile.prototype, "onDelete", void 0);
AttachmentTile = __decorate([
customElement("attachment-tile")
], AttachmentTile);
export { AttachmentTile };
//# sourceMappingURL=AttachmentTile.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"AttachmentTile.js","sourceRoot":"","sources":["../../src/components/AttachmentTile.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,12 @@
import { LitElement } from "lit";
export declare class ConsoleBlock extends LitElement {
content: string;
variant: "default" | "error";
private copied;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private copy;
updated(): void;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=ConsoleBlock.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ConsoleBlock.d.ts","sourceRoot":"","sources":["../../src/components/ConsoleBlock.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAMjC,qBAAa,YAAa,SAAQ,UAAU;IAC/B,OAAO,EAAE,MAAM,CAAM;IACrB,OAAO,EAAE,SAAS,GAAG,OAAO,CAAa;IAC5C,OAAO,CAAC,MAAM,CAAS;cAEb,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;YAKpB,IAAI;IAYT,OAAO;IAQP,MAAM;CAyBf"}

View File

@@ -0,0 +1,84 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit";
import { LitElement } from "lit";
import { property, state } from "lit/decorators.js";
import { html } from "lit/html.js";
import { Check, Copy } from "lucide";
import { i18n } from "../utils/i18n.js";
export class ConsoleBlock extends LitElement {
constructor() {
super(...arguments);
this.content = "";
this.variant = "default";
this.copied = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
async copy() {
try {
await navigator.clipboard.writeText(this.content || "");
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 1500);
}
catch (e) {
console.error("Copy failed", e);
}
}
updated() {
// Auto-scroll to bottom on content changes
const container = this.querySelector(".console-scroll");
if (container) {
container.scrollTop = container.scrollHeight;
}
}
render() {
const isError = this.variant === "error";
const textClass = isError ? "text-destructive" : "text-foreground";
return html `
<div class="border border-border rounded-lg overflow-hidden">
<div class="flex items-center justify-between px-3 py-1.5 bg-muted border-b border-border">
<span class="text-xs text-muted-foreground font-mono">${i18n("console")}</span>
<button
@click=${() => this.copy()}
class="flex items-center gap-1 px-2 py-0.5 text-xs rounded hover:bg-accent text-muted-foreground hover:text-accent-foreground transition-colors"
title="${i18n("Copy output")}"
>
${this.copied ? icon(Check, "sm") : icon(Copy, "sm")}
${this.copied ? html `<span>${i18n("Copied!")}</span>` : ""}
</button>
</div>
<div class="console-scroll overflow-auto max-h-64">
<pre class="!bg-background !border-0 !rounded-none m-0 p-3 text-xs ${textClass} font-mono whitespace-pre-wrap">
${this.content || ""}</pre
>
</div>
</div>
`;
}
}
__decorate([
property()
], ConsoleBlock.prototype, "content", void 0);
__decorate([
property()
], ConsoleBlock.prototype, "variant", void 0);
__decorate([
state()
], ConsoleBlock.prototype, "copied", void 0);
// Register custom element
if (!customElements.get("console-block")) {
customElements.define("console-block", ConsoleBlock);
}
//# sourceMappingURL=ConsoleBlock.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ConsoleBlock.js","sourceRoot":"","sources":["../../src/components/ConsoleBlock.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAExC,MAAM,OAAO,YAAa,SAAQ,UAAU;IAA5C;;QACa,YAAO,GAAW,EAAE,CAAC;QACrB,YAAO,GAAwB,SAAS,CAAC;QACpC,WAAM,GAAG,KAAK,CAAC;IAwDjC,CAAC;IAtDmB,gBAAgB;QAClC,OAAO,IAAI,CAAC;IACb,CAAC;IAEQ,iBAAiB;QACzB,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;IAC9B,CAAC;IAEO,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC;YACJ,MAAM,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;YACxD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,UAAU,CAAC,GAAG,EAAE;gBACf,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;YACrB,CAAC,EAAE,IAAI,CAAC,CAAC;QACV,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;QACjC,CAAC;IACF,CAAC;IAEQ,OAAO;QACf,2CAA2C;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,iBAAiB,CAAuB,CAAC;QAC9E,IAAI,SAAS,EAAE,CAAC;YACf,SAAS,CAAC,SAAS,GAAG,SAAS,CAAC,YAAY,CAAC;QAC9C,CAAC;IACF,CAAC;IAEQ,MAAM;QACd,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC;QACzC,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,iBAAiB,CAAC;QAEnE,OAAO,IAAI,CAAA;;;6DAGgD,IAAI,CAAC,SAAS,CAAC;;eAE7D,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;;eAEjB,IAAI,CAAC,aAAa,CAAC;;QAE1B,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC;QAClD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,SAAS,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;;;;0EAIU,SAAS;EACjF,IAAI,CAAC,OAAO,IAAI,EAAE;;;;GAIjB,CAAC;IACH,CAAC;CACD;AA1DY;IAAX,QAAQ,EAAE;6CAAsB;AACrB;IAAX,QAAQ,EAAE;6CAA0C;AACpC;IAAhB,KAAK,EAAE;4CAAwB;AA0DjC,0BAA0B;AAC1B,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;IAC1C,cAAc,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;AACtD,CAAC"}

View File

@@ -0,0 +1,17 @@
import { LitElement, type TemplateResult } from "lit";
import type { CustomProvider } from "../storage/stores/custom-providers-store.js";
export declare class CustomProviderCard extends LitElement {
provider: CustomProvider;
isAutoDiscovery: boolean;
status?: {
modelCount: number;
status: "connected" | "disconnected" | "checking";
};
onRefresh?: (provider: CustomProvider) => void;
onEdit?: (provider: CustomProvider) => void;
onDelete?: (provider: CustomProvider) => void;
protected createRenderRoot(): this;
private renderStatus;
render(): TemplateResult;
}
//# sourceMappingURL=CustomProviderCard.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CustomProviderCard.d.ts","sourceRoot":"","sources":["../../src/components/CustomProviderCard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6CAA6C,CAAC;AAElF,qBACa,kBAAmB,SAAQ,UAAU;IACrB,QAAQ,EAAG,cAAc,CAAC;IACzB,eAAe,UAAS;IACzB,MAAM,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,UAAU,CAAA;KAAE,CAAC;IACnG,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAC5C,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAE1D,SAAS,CAAC,gBAAgB;IAI1B,OAAO,CAAC,YAAY;IAgCpB,MAAM,IAAI,cAAc;CAgDxB"}

View File

@@ -0,0 +1,110 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { i18n } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
let CustomProviderCard = class CustomProviderCard extends LitElement {
constructor() {
super(...arguments);
this.isAutoDiscovery = false;
}
createRenderRoot() {
return this;
}
renderStatus() {
if (!this.isAutoDiscovery) {
return html `
<div class="text-xs text-muted-foreground mt-1">
${i18n("Models")}: ${this.provider.models?.length || 0}
</div>
`;
}
if (!this.status)
return html ``;
const statusIcon = this.status.status === "connected"
? html `<span class="text-green-500">●</span>`
: this.status.status === "checking"
? html `<span class="text-yellow-500">●</span>`
: html `<span class="text-red-500">●</span>`;
const statusText = this.status.status === "connected"
? `${this.status.modelCount} ${i18n("models")}`
: this.status.status === "checking"
? i18n("Checking...")
: i18n("Disconnected");
return html `
<div class="text-xs text-muted-foreground mt-1 flex items-center gap-1">
${statusIcon} ${statusText}
</div>
`;
}
render() {
return html `
<div class="border border-border rounded-lg p-4 space-y-2">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="font-medium text-sm text-foreground">${this.provider.name}</div>
<div class="text-xs text-muted-foreground mt-1">
<span class="capitalize">${this.provider.type}</span>
${this.provider.baseUrl ? html `${this.provider.baseUrl}` : ""}
</div>
${this.renderStatus()}
</div>
<div class="flex gap-2">
${this.isAutoDiscovery && this.onRefresh
? Button({
onClick: () => this.onRefresh?.(this.provider),
variant: "ghost",
size: "sm",
children: i18n("Refresh"),
})
: ""}
${this.onEdit
? Button({
onClick: () => this.onEdit?.(this.provider),
variant: "ghost",
size: "sm",
children: i18n("Edit"),
})
: ""}
${this.onDelete
? Button({
onClick: () => this.onDelete?.(this.provider),
variant: "ghost",
size: "sm",
children: i18n("Delete"),
})
: ""}
</div>
</div>
</div>
`;
}
};
__decorate([
property({ type: Object })
], CustomProviderCard.prototype, "provider", void 0);
__decorate([
property({ type: Boolean })
], CustomProviderCard.prototype, "isAutoDiscovery", void 0);
__decorate([
property({ type: Object })
], CustomProviderCard.prototype, "status", void 0);
__decorate([
property()
], CustomProviderCard.prototype, "onRefresh", void 0);
__decorate([
property()
], CustomProviderCard.prototype, "onEdit", void 0);
__decorate([
property()
], CustomProviderCard.prototype, "onDelete", void 0);
CustomProviderCard = __decorate([
customElement("custom-provider-card")
], CustomProviderCard);
export { CustomProviderCard };
//# sourceMappingURL=CustomProviderCard.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CustomProviderCard.js","sourceRoot":"","sources":["../../src/components/CustomProviderCard.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,uCAAuC,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,UAAU,EAAuB,MAAM,KAAK,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAIrD,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,UAAU;IAA3C;;QAEuB,oBAAe,GAAG,KAAK,CAAC;IA0FtD,CAAC;IApFU,gBAAgB;QACzB,OAAO,IAAI,CAAC;IACb,CAAC;IAEO,YAAY;QACnB,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAA;;OAEP,IAAI,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;;IAEvD,CAAC;QACH,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAA,EAAE,CAAC;QAEhC,MAAM,UAAU,GACf,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW;YACjC,CAAC,CAAC,IAAI,CAAA,uCAAuC;YAC7C,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU;gBAClC,CAAC,CAAC,IAAI,CAAA,wCAAwC;gBAC9C,CAAC,CAAC,IAAI,CAAA,qCAAqC,CAAC;QAE/C,MAAM,UAAU,GACf,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW;YACjC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE;YAC/C,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU;gBAClC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC;gBACrB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAE1B,OAAO,IAAI,CAAA;;MAEP,UAAU,IAAI,UAAU;;GAE3B,CAAC;IACH,CAAC;IAED,MAAM;QACL,OAAO,IAAI,CAAA;;;;yDAI4C,IAAI,CAAC,QAAQ,CAAC,IAAI;;kCAEzC,IAAI,CAAC,QAAQ,CAAC,IAAI;SAC3C,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE;;QAE/D,IAAI,CAAC,YAAY,EAAE;;;QAIpB,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,SAAS;YACrC,CAAC,CAAC,MAAM,CAAC;gBACP,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAC9C,OAAO,EAAE,OAAO;gBAChB,IAAI,EAAE,IAAI;gBACV,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC;aACzB,CAAC;YACH,CAAC,CAAC,EACJ;QAEC,IAAI,CAAC,MAAM;YACV,CAAC,CAAC,MAAM,CAAC;gBACP,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAC3C,OAAO,EAAE,OAAO;gBAChB,IAAI,EAAE,IAAI;gBACV,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;aACtB,CAAC;YACH,CAAC,CAAC,EACJ;QAEC,IAAI,CAAC,QAAQ;YACZ,CAAC,CAAC,MAAM,CAAC;gBACP,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAC7C,OAAO,EAAE,OAAO;gBAChB,IAAI,EAAE,IAAI;gBACV,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC;aACxB,CAAC;YACH,CAAC,CAAC,EACJ;;;;GAIH,CAAC;IACH,CAAC;CACD,CAAA;AA3F4B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;oDAA2B;AACzB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;2DAAyB;AACzB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;kDAAoF;AACnG;IAAX,QAAQ,EAAE;qDAAgD;AAC/C;IAAX,QAAQ,EAAE;kDAA6C;AAC5C;IAAX,QAAQ,EAAE;oDAA+C;AAN9C,kBAAkB;IAD9B,aAAa,CAAC,sBAAsB,CAAC;GACzB,kBAAkB,CA4F9B"}

View File

@@ -0,0 +1,15 @@
import { LitElement, type TemplateResult } from "lit";
/**
* Reusable expandable section component for tool renderers.
* Captures children in connectedCallback and re-renders them in the details area.
*/
export declare class ExpandableSection extends LitElement {
summary: string;
defaultExpanded: boolean;
private expanded;
private capturedChildren;
protected createRenderRoot(): this;
connectedCallback(): void;
render(): TemplateResult;
}
//# sourceMappingURL=ExpandableSection.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExpandableSection.d.ts","sourceRoot":"","sources":["../../src/components/ExpandableSection.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAI5D;;;GAGG;AACH,qBACa,iBAAkB,SAAQ,UAAU;IACpC,OAAO,EAAG,MAAM,CAAC;IACA,eAAe,UAAS;IAC5C,OAAO,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,gBAAgB,CAAc;IAEtC,SAAS,CAAC,gBAAgB;IAIjB,iBAAiB;IASjB,MAAM,IAAI,cAAc;CAgBjC"}

View File

@@ -0,0 +1,63 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ChevronDown, ChevronRight } from "lucide";
/**
* Reusable expandable section component for tool renderers.
* Captures children in connectedCallback and re-renders them in the details area.
*/
let ExpandableSection = class ExpandableSection extends LitElement {
constructor() {
super(...arguments);
this.defaultExpanded = false;
this.expanded = false;
this.capturedChildren = [];
}
createRenderRoot() {
return this; // light DOM
}
connectedCallback() {
super.connectedCallback();
// Capture children before first render
this.capturedChildren = Array.from(this.childNodes);
// Clear children (we'll re-insert them in render)
this.innerHTML = "";
this.expanded = this.defaultExpanded;
}
render() {
return html `
<div>
<button
@click=${() => {
this.expanded = !this.expanded;
}}
class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full text-left"
>
${icon(this.expanded ? ChevronDown : ChevronRight, "sm")}
<span>${this.summary}</span>
</button>
${this.expanded ? html `<div class="mt-2">${this.capturedChildren}</div>` : ""}
</div>
`;
}
};
__decorate([
property()
], ExpandableSection.prototype, "summary", void 0);
__decorate([
property({ type: Boolean })
], ExpandableSection.prototype, "defaultExpanded", void 0);
__decorate([
state()
], ExpandableSection.prototype, "expanded", void 0);
ExpandableSection = __decorate([
customElement("expandable-section")
], ExpandableSection);
export { ExpandableSection };
//# sourceMappingURL=ExpandableSection.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExpandableSection.js","sourceRoot":"","sources":["../../src/components/ExpandableSection.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAuB,MAAM,KAAK,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEnD;;;GAGG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAkB,SAAQ,UAAU;IAA1C;;QAEuB,oBAAe,GAAG,KAAK,CAAC;QACpC,aAAQ,GAAG,KAAK,CAAC;QAC1B,qBAAgB,GAAW,EAAE,CAAC;IA+BvC,CAAC;IA7BU,gBAAgB;QACzB,OAAO,IAAI,CAAC,CAAC,YAAY;IAC1B,CAAC;IAEQ,iBAAiB;QACzB,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,uCAAuC;QACvC,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpD,kDAAkD;QAClD,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC;IACtC,CAAC;IAEQ,MAAM;QACd,OAAO,IAAI,CAAA;;;cAGC,GAAG,EAAE;YACb,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;QAChC,CAAC;;;OAGC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,EAAE,IAAI,CAAC;aAChD,IAAI,CAAC,OAAO;;MAEnB,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA,qBAAqB,IAAI,CAAC,gBAAgB,QAAQ,CAAC,CAAC,CAAC,EAAE;;GAE9E,CAAC;IACH,CAAC;CACD,CAAA;AAlCY;IAAX,QAAQ,EAAE;kDAAkB;AACA;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;0DAAyB;AACpC;IAAhB,KAAK,EAAE;mDAA0B;AAHtB,iBAAiB;IAD7B,aAAa,CAAC,oBAAoB,CAAC;GACvB,iBAAiB,CAmC7B"}

View File

@@ -0,0 +1,26 @@
import { type BaseComponentProps } from "@mariozechner/mini-lit/dist/mini.js";
import { type Ref } from "lit/directives/ref.js";
export type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "search";
export type InputSize = "sm" | "md" | "lg";
export interface InputProps extends BaseComponentProps {
type?: InputType;
size?: InputSize;
value?: string;
placeholder?: string;
label?: string;
error?: string;
disabled?: boolean;
required?: boolean;
name?: string;
autocomplete?: string;
min?: number;
max?: number;
step?: number;
inputRef?: Ref<HTMLInputElement>;
onInput?: (e: Event) => void;
onChange?: (e: Event) => void;
onKeyDown?: (e: KeyboardEvent) => void;
onKeyUp?: (e: KeyboardEvent) => void;
}
export declare const Input: import("@mariozechner/mini-lit/dist/mini.js").Component<InputProps>;
//# sourceMappingURL=Input.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../src/components/Input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,kBAAkB,EAAM,MAAM,qCAAqC,CAAC;AAElF,OAAO,EAAE,KAAK,GAAG,EAAO,MAAM,uBAAuB,CAAC;AAGtD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;AAC5F,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,MAAM,WAAW,UAAW,SAAQ,kBAAkB;IACrD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IAC7B,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;CACrC;AAED,eAAO,MAAM,KAAK,qEAmFjB,CAAC"}

View File

@@ -0,0 +1,57 @@
import { fc } from "@mariozechner/mini-lit/dist/mini.js";
import { html } from "lit";
import { ref } from "lit/directives/ref.js";
import { i18n } from "../utils/i18n.js";
export const Input = fc(({ type = "text", size = "md", value = "", placeholder = "", label = "", error = "", disabled = false, required = false, name = "", autocomplete = "", min, max, step, inputRef, onInput, onChange, onKeyDown, onKeyUp, className = "", }) => {
const sizeClasses = {
sm: "h-8 px-3 py-1 text-sm",
md: "h-9 px-3 py-1 text-sm md:text-sm",
lg: "h-10 px-4 py-1 text-base",
};
const baseClasses = "flex w-full min-w-0 rounded-md border bg-transparent text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium";
const interactionClasses = "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground";
const focusClasses = "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]";
const darkClasses = "dark:bg-input/30";
const stateClasses = error
? "border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40"
: "border-input";
const disabledClasses = "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50";
const handleInput = (e) => {
onInput?.(e);
};
const handleChange = (e) => {
onChange?.(e);
};
return html `
<div class="flex flex-col gap-1.5 ${className}">
${label
? html `
<label class="text-sm font-medium text-foreground">
${label} ${required ? html `<span class="text-destructive">${i18n("*")}</span>` : ""}
</label>
`
: ""}
<input
type="${type}"
class="${baseClasses} ${sizeClasses[size]} ${interactionClasses} ${focusClasses} ${darkClasses} ${stateClasses} ${disabledClasses}"
.value=${value}
placeholder="${placeholder}"
?disabled=${disabled}
?required=${required}
?aria-invalid=${!!error}
name="${name}"
autocomplete="${autocomplete}"
min="${min ?? ""}"
max="${max ?? ""}"
step="${step ?? ""}"
@input=${handleInput}
@change=${handleChange}
@keydown=${onKeyDown}
@keyup=${onKeyUp}
${inputRef ? ref(inputRef) : ""}
/>
${error ? html `<span class="text-sm text-destructive">${error}</span>` : ""}
</div>
`;
});
//# sourceMappingURL=Input.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"Input.js","sourceRoot":"","sources":["../../src/components/Input.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,EAAE,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAY,GAAG,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AA0BxC,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CACtB,CAAC,EACA,IAAI,GAAG,MAAM,EACb,IAAI,GAAG,IAAI,EACX,KAAK,GAAG,EAAE,EACV,WAAW,GAAG,EAAE,EAChB,KAAK,GAAG,EAAE,EACV,KAAK,GAAG,EAAE,EACV,QAAQ,GAAG,KAAK,EAChB,QAAQ,GAAG,KAAK,EAChB,IAAI,GAAG,EAAE,EACT,YAAY,GAAG,EAAE,EACjB,GAAG,EACH,GAAG,EACH,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAS,GAAG,EAAE,GACd,EAAE,EAAE;IACJ,MAAM,WAAW,GAAG;QACnB,EAAE,EAAE,uBAAuB;QAC3B,EAAE,EAAE,kCAAkC;QACtC,EAAE,EAAE,0BAA0B;KAC9B,CAAC;IAEF,MAAM,WAAW,GAChB,qNAAqN,CAAC;IACvN,MAAM,kBAAkB,GACvB,0FAA0F,CAAC;IAC5F,MAAM,YAAY,GAAG,+EAA+E,CAAC;IACrG,MAAM,WAAW,GAAG,kBAAkB,CAAC;IACvC,MAAM,YAAY,GAAG,KAAK;QACzB,CAAC,CAAC,2FAA2F;QAC7F,CAAC,CAAC,cAAc,CAAC;IAClB,MAAM,eAAe,GAAG,8EAA8E,CAAC;IAEvG,MAAM,WAAW,GAAG,CAAC,CAAQ,EAAE,EAAE;QAChC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IACd,CAAC,CAAC;IAEF,MAAM,YAAY,GAAG,CAAC,CAAQ,EAAE,EAAE;QACjC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;IACf,CAAC,CAAC;IAEF,OAAO,IAAI,CAAA;uCAC0B,SAAS;MAE3C,KAAK;QACJ,CAAC,CAAC,IAAI,CAAA;;UAEF,KAAK,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA,kCAAkC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;;OAEpF;QACD,CAAC,CAAC,EACJ;;aAES,IAAI;cACH,WAAW,IACnB,WAAW,CAAC,IAAI,CACjB,IAAI,kBAAkB,IAAI,YAAY,IAAI,WAAW,IAAI,YAAY,IAAI,eAAe;cAC/E,KAAK;oBACC,WAAW;iBACd,QAAQ;iBACR,QAAQ;qBACJ,CAAC,CAAC,KAAK;aACf,IAAI;qBACI,YAAY;YACrB,GAAG,IAAI,EAAE;YACT,GAAG,IAAI,EAAE;aACR,IAAI,IAAI,EAAE;cACT,WAAW;eACV,YAAY;gBACX,SAAS;cACX,OAAO;OACd,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;;MAE9B,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,0CAA0C,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE;;GAE5E,CAAC;AACH,CAAC,CACD,CAAC"}

View File

@@ -0,0 +1,43 @@
import type { Model } from "@mariozechner/pi-ai";
import { LitElement } from "lit";
import { type Attachment } from "../utils/attachment-utils.js";
import "./AttachmentTile.js";
export declare class MessageEditor extends LitElement {
private _value;
private textareaRef;
get value(): string;
set value(val: string);
isStreaming: boolean;
currentModel?: Model<any>;
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high";
showAttachmentButton: boolean;
showModelSelector: boolean;
showThinkingSelector: boolean;
onInput?: (value: string) => void;
onSend?: (input: string, attachments: Attachment[]) => void;
onAbort?: () => void;
onModelSelect?: () => void;
onThinkingChange?: (level: "off" | "minimal" | "low" | "medium" | "high") => void;
onFilesChange?: (files: Attachment[]) => void;
attachments: Attachment[];
maxFiles: number;
maxFileSize: number;
acceptedTypes: string;
processingFiles: boolean;
isDragging: boolean;
private fileInputRef;
protected createRenderRoot(): HTMLElement | DocumentFragment;
private handleTextareaInput;
private handleKeyDown;
private handlePaste;
private handleSend;
private handleAttachmentClick;
private handleFilesSelected;
private removeFile;
private handleDragOver;
private handleDragLeave;
private handleDrop;
firstUpdated(): void;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=MessageEditor.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"MessageEditor.d.ts","sourceRoot":"","sources":["../../src/components/MessageEditor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAIvC,OAAO,EAAE,KAAK,UAAU,EAAkB,MAAM,8BAA8B,CAAC;AAE/E,OAAO,qBAAqB,CAAC;AAE7B,qBACa,aAAc,SAAQ,UAAU;IAC5C,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,WAAW,CAAoC;IAEvD,IACI,KAAK,IAIM,MAAM,CAFpB;IAED,IAAI,KAAK,CAAC,GAAG,EAAE,MAAM,EAIpB;IAEW,WAAW,UAAS;IACpB,YAAY,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,aAAa,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAS;IACrE,oBAAoB,UAAQ;IAC5B,iBAAiB,UAAQ;IACzB,oBAAoB,UAAQ;IAC5B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,KAAK,IAAI,CAAC;IAClF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC9C,WAAW,EAAE,UAAU,EAAE,CAAM;IAC/B,QAAQ,SAAM;IACd,WAAW,SAAoB;IAC/B,aAAa,SACqF;IAErG,eAAe,UAAS;IACxB,UAAU,UAAS;IAC5B,OAAO,CAAC,YAAY,CAAiC;cAElC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAIrE,OAAO,CAAC,mBAAmB,CAIzB;IAEF,OAAO,CAAC,aAAa,CAUnB;IAEF,OAAO,CAAC,WAAW,CAgDjB;IAEF,OAAO,CAAC,UAAU,CAEhB;IAEF,OAAO,CAAC,qBAAqB,CAE3B;YAEY,mBAAmB;IAmCjC,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,cAAc,CAMpB;IAEF,OAAO,CAAC,eAAe,CAUrB;IAEF,OAAO,CAAC,UAAU,CAkChB;IAEO,YAAY;IAOZ,MAAM;CAqKf"}

View File

@@ -0,0 +1,414 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { Select } from "@mariozechner/mini-lit/dist/Select.js";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { Brain, Loader2, Paperclip, Send, Sparkles, Square } from "lucide";
import { loadAttachment } from "../utils/attachment-utils.js";
import { i18n } from "../utils/i18n.js";
import "./AttachmentTile.js";
let MessageEditor = class MessageEditor extends LitElement {
constructor() {
super(...arguments);
this._value = "";
this.textareaRef = createRef();
this.isStreaming = false;
this.thinkingLevel = "off";
this.showAttachmentButton = true;
this.showModelSelector = true;
this.showThinkingSelector = true;
this.attachments = [];
this.maxFiles = 10;
this.maxFileSize = 20 * 1024 * 1024; // 20MB
this.acceptedTypes = "image/*,application/pdf,.docx,.pptx,.xlsx,.xls,.txt,.md,.json,.xml,.html,.css,.js,.ts,.jsx,.tsx,.yml,.yaml";
this.processingFiles = false;
this.isDragging = false;
this.fileInputRef = createRef();
this.handleTextareaInput = (e) => {
const textarea = e.target;
this.value = textarea.value;
this.onInput?.(this.value);
};
this.handleKeyDown = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (!this.isStreaming && !this.processingFiles && (this.value.trim() || this.attachments.length > 0)) {
this.handleSend();
}
}
else if (e.key === "Escape" && this.isStreaming) {
e.preventDefault();
this.onAbort?.();
}
};
this.handlePaste = async (e) => {
const items = e.clipboardData?.items;
if (!items)
return;
const imageFiles = [];
// Check for image items in clipboard
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
imageFiles.push(file);
}
}
}
// If we found images, process them
if (imageFiles.length > 0) {
e.preventDefault(); // Prevent default paste behavior
if (imageFiles.length + this.attachments.length > this.maxFiles) {
alert(`Maximum ${this.maxFiles} files allowed`);
return;
}
this.processingFiles = true;
const newAttachments = [];
for (const file of imageFiles) {
try {
if (file.size > this.maxFileSize) {
alert(`Image exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
continue;
}
const attachment = await loadAttachment(file);
newAttachments.push(attachment);
}
catch (error) {
console.error("Error processing pasted image:", error);
alert(`Failed to process pasted image: ${String(error)}`);
}
}
this.attachments = [...this.attachments, ...newAttachments];
this.onFilesChange?.(this.attachments);
this.processingFiles = false;
}
};
this.handleSend = () => {
this.onSend?.(this.value, this.attachments);
};
this.handleAttachmentClick = () => {
this.fileInputRef.value?.click();
};
this.handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
if (!this.isDragging) {
this.isDragging = true;
}
};
this.handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
// Only set isDragging to false if we're leaving the entire component
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
this.isDragging = false;
}
};
this.handleDrop = async (e) => {
e.preventDefault();
e.stopPropagation();
this.isDragging = false;
const files = Array.from(e.dataTransfer?.files || []);
if (files.length === 0)
return;
if (files.length + this.attachments.length > this.maxFiles) {
alert(`Maximum ${this.maxFiles} files allowed`);
return;
}
this.processingFiles = true;
const newAttachments = [];
for (const file of files) {
try {
if (file.size > this.maxFileSize) {
alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
continue;
}
const attachment = await loadAttachment(file);
newAttachments.push(attachment);
}
catch (error) {
console.error(`Error processing ${file.name}:`, error);
alert(`Failed to process ${file.name}: ${String(error)}`);
}
}
this.attachments = [...this.attachments, ...newAttachments];
this.onFilesChange?.(this.attachments);
this.processingFiles = false;
};
}
get value() {
return this._value;
}
set value(val) {
const oldValue = this._value;
this._value = val;
this.requestUpdate("value", oldValue);
}
createRenderRoot() {
return this;
}
async handleFilesSelected(e) {
const input = e.target;
const files = Array.from(input.files || []);
if (files.length === 0)
return;
if (files.length + this.attachments.length > this.maxFiles) {
alert(`Maximum ${this.maxFiles} files allowed`);
input.value = "";
return;
}
this.processingFiles = true;
const newAttachments = [];
for (const file of files) {
try {
if (file.size > this.maxFileSize) {
alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
continue;
}
const attachment = await loadAttachment(file);
newAttachments.push(attachment);
}
catch (error) {
console.error(`Error processing ${file.name}:`, error);
alert(`Failed to process ${file.name}: ${String(error)}`);
}
}
this.attachments = [...this.attachments, ...newAttachments];
this.onFilesChange?.(this.attachments);
this.processingFiles = false;
input.value = ""; // Reset input
}
removeFile(fileId) {
this.attachments = this.attachments.filter((f) => f.id !== fileId);
this.onFilesChange?.(this.attachments);
}
firstUpdated() {
const textarea = this.textareaRef.value;
if (textarea) {
textarea.focus();
}
}
render() {
// Check if current model supports thinking/reasoning
const model = this.currentModel;
const supportsThinking = model?.reasoning === true; // Models with reasoning:true support thinking
return html `
<div
class="bg-card rounded-xl border shadow-sm relative ${this.isDragging ? "border-primary border-2 bg-primary/5" : "border-border"}"
@dragover=${this.handleDragOver}
@dragleave=${this.handleDragLeave}
@drop=${this.handleDrop}
>
<!-- Drag overlay -->
${this.isDragging
? html `
<div class="absolute inset-0 bg-primary/10 rounded-xl pointer-events-none z-10 flex items-center justify-center">
<div class="text-primary font-medium">${i18n("Drop files here")}</div>
</div>
`
: ""}
<!-- Attachments -->
${this.attachments.length > 0
? html `
<div class="px-4 pt-3 pb-2 flex flex-wrap gap-2">
${this.attachments.map((attachment) => html `
<attachment-tile
.attachment=${attachment}
.showDelete=${true}
.onDelete=${() => this.removeFile(attachment.id)}
></attachment-tile>
`)}
</div>
`
: ""}
<textarea
class="w-full bg-transparent p-4 text-foreground placeholder-muted-foreground outline-none resize-none overflow-y-auto"
placeholder=${i18n("Type a message...")}
rows="1"
style="max-height: 200px; field-sizing: content; min-height: 1lh; height: auto;"
.value=${this.value}
@input=${this.handleTextareaInput}
@keydown=${this.handleKeyDown}
@paste=${this.handlePaste}
${ref(this.textareaRef)}
></textarea>
<!-- Hidden file input -->
<input
type="file"
${ref(this.fileInputRef)}
@change=${this.handleFilesSelected}
accept=${this.acceptedTypes}
multiple
style="display: none;"
/>
<!-- Button Row -->
<div class="px-2 pb-2 flex items-center justify-between">
<!-- Left side - attachment and thinking selector -->
<div class="flex gap-2 items-center">
${this.showAttachmentButton
? this.processingFiles
? html `
<div class="h-8 w-8 flex items-center justify-center">
${icon(Loader2, "sm", "animate-spin text-muted-foreground")}
</div>
`
: html `
${Button({
variant: "ghost",
size: "icon",
className: "h-8 w-8",
onClick: this.handleAttachmentClick,
children: icon(Paperclip, "sm"),
})}
`
: ""}
${supportsThinking && this.showThinkingSelector
? html `
${Select({
value: this.thinkingLevel,
placeholder: i18n("Off"),
options: [
{ value: "off", label: i18n("Off"), icon: icon(Brain, "sm") },
{ value: "minimal", label: i18n("Minimal"), icon: icon(Brain, "sm") },
{ value: "low", label: i18n("Low"), icon: icon(Brain, "sm") },
{ value: "medium", label: i18n("Medium"), icon: icon(Brain, "sm") },
{ value: "high", label: i18n("High"), icon: icon(Brain, "sm") },
],
onChange: (value) => {
this.onThinkingChange?.(value);
},
width: "80px",
size: "sm",
variant: "ghost",
fitContent: true,
})}
`
: ""}
</div>
<!-- Model selector and send on the right -->
<div class="flex gap-2 items-center">
${this.showModelSelector && this.currentModel
? html `
${Button({
variant: "ghost",
size: "sm",
onClick: () => {
// Focus textarea before opening model selector so focus returns there
this.textareaRef.value?.focus();
// Wait for next frame to ensure focus takes effect before dialog captures it
requestAnimationFrame(() => {
this.onModelSelect?.();
});
},
children: html `
${icon(Sparkles, "sm")}
<span class="ml-1">${this.currentModel.id}</span>
`,
className: "h-8 text-xs truncate",
})}
`
: ""}
${this.isStreaming
? html `
${Button({
variant: "ghost",
size: "icon",
onClick: this.onAbort,
children: icon(Square, "sm"),
className: "h-8 w-8",
})}
`
: html `
${Button({
variant: "ghost",
size: "icon",
onClick: this.handleSend,
disabled: (!this.value.trim() && this.attachments.length === 0) || this.processingFiles,
children: html `<div style="transform: rotate(-45deg)">${icon(Send, "sm")}</div>`,
className: "h-8 w-8",
})}
`}
</div>
</div>
</div>
`;
}
};
__decorate([
property()
], MessageEditor.prototype, "value", null);
__decorate([
property()
], MessageEditor.prototype, "isStreaming", void 0);
__decorate([
property()
], MessageEditor.prototype, "currentModel", void 0);
__decorate([
property()
], MessageEditor.prototype, "thinkingLevel", void 0);
__decorate([
property()
], MessageEditor.prototype, "showAttachmentButton", void 0);
__decorate([
property()
], MessageEditor.prototype, "showModelSelector", void 0);
__decorate([
property()
], MessageEditor.prototype, "showThinkingSelector", void 0);
__decorate([
property()
], MessageEditor.prototype, "onInput", void 0);
__decorate([
property()
], MessageEditor.prototype, "onSend", void 0);
__decorate([
property()
], MessageEditor.prototype, "onAbort", void 0);
__decorate([
property()
], MessageEditor.prototype, "onModelSelect", void 0);
__decorate([
property()
], MessageEditor.prototype, "onThinkingChange", void 0);
__decorate([
property()
], MessageEditor.prototype, "onFilesChange", void 0);
__decorate([
property()
], MessageEditor.prototype, "attachments", void 0);
__decorate([
property()
], MessageEditor.prototype, "maxFiles", void 0);
__decorate([
property()
], MessageEditor.prototype, "maxFileSize", void 0);
__decorate([
property()
], MessageEditor.prototype, "acceptedTypes", void 0);
__decorate([
state()
], MessageEditor.prototype, "processingFiles", void 0);
__decorate([
state()
], MessageEditor.prototype, "isDragging", void 0);
MessageEditor = __decorate([
customElement("message-editor")
], MessageEditor);
export { MessageEditor };
//# sourceMappingURL=MessageEditor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
import type { AgentTool } from "@mariozechner/pi-ai";
import { LitElement, type TemplateResult } from "lit";
import type { AppMessage } from "./Messages.js";
export declare class MessageList extends LitElement {
messages: AppMessage[];
tools: AgentTool[];
pendingToolCalls?: Set<string>;
isStreaming: boolean;
onCostClick?: () => void;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private buildRenderItems;
render(): TemplateResult<1>;
}
//# sourceMappingURL=MessageList.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../src/components/MessageList.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,SAAS,EAGT,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAG5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,qBAAa,WAAY,SAAQ,UAAU;IACf,QAAQ,EAAE,UAAU,EAAE,CAAM;IAC5B,KAAK,EAAE,SAAS,EAAE,CAAM;IACvB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,WAAW,EAAE,OAAO,CAAS;IAC1B,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;cAEtC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKlC,OAAO,CAAC,gBAAgB;IAuDf,MAAM;CAUf"}

View File

@@ -0,0 +1,104 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, LitElement } from "lit";
import { property } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { renderMessage } from "./message-renderer-registry.js";
export class MessageList extends LitElement {
constructor() {
super(...arguments);
this.messages = [];
this.tools = [];
this.isStreaming = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
buildRenderItems() {
// Map tool results by call id for quick lookup
const resultByCallId = new Map();
for (const message of this.messages) {
if (message.role === "toolResult") {
resultByCallId.set(message.toolCallId, message);
}
}
const items = [];
let index = 0;
for (const msg of this.messages) {
// Skip artifact messages - they're for session persistence only, not UI display
if (msg.role === "artifact") {
continue;
}
// Try custom renderer first
const customTemplate = renderMessage(msg);
if (customTemplate) {
items.push({ key: `msg:${index}`, template: customTemplate });
index++;
continue;
}
// Fall back to built-in renderers
if (msg.role === "user") {
items.push({
key: `msg:${index}`,
template: html `<user-message .message=${msg}></user-message>`,
});
index++;
}
else if (msg.role === "assistant") {
const amsg = msg;
items.push({
key: `msg:${index}`,
template: html `<assistant-message
.message=${amsg}
.tools=${this.tools}
.isStreaming=${false}
.pendingToolCalls=${this.pendingToolCalls}
.toolResultsById=${resultByCallId}
.hideToolCalls=${false}
.onCostClick=${this.onCostClick}
></assistant-message>`,
});
index++;
}
else {
// Skip standalone toolResult messages; they are rendered via paired tool-message above
// Skip unknown roles
}
}
return items;
}
render() {
const items = this.buildRenderItems();
return html `<div class="flex flex-col gap-3">
${repeat(items, (it) => it.key, (it) => it.template)}
</div>`;
}
}
__decorate([
property({ type: Array })
], MessageList.prototype, "messages", void 0);
__decorate([
property({ type: Array })
], MessageList.prototype, "tools", void 0);
__decorate([
property({ type: Object })
], MessageList.prototype, "pendingToolCalls", void 0);
__decorate([
property({ type: Boolean })
], MessageList.prototype, "isStreaming", void 0);
__decorate([
property({ attribute: false })
], MessageList.prototype, "onCostClick", void 0);
// Register custom element
if (!customElements.get("message-list")) {
customElements.define("message-list", MessageList);
}
//# sourceMappingURL=MessageList.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"MessageList.js","sourceRoot":"","sources":["../../src/components/MessageList.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,64 @@
import type { AgentTool, AssistantMessage as AssistantMessageType, ToolCall, ToolResultMessage as ToolResultMessageType, UserMessage as UserMessageType } from "@mariozechner/pi-ai";
import { LitElement, type TemplateResult } from "lit";
import type { Attachment } from "../utils/attachment-utils.js";
import "./ThinkingBlock.js";
export type UserMessageWithAttachments = UserMessageType & {
attachments?: Attachment[];
};
export interface ArtifactMessage {
role: "artifact";
action: "create" | "update" | "delete";
filename: string;
content?: string;
title?: string;
timestamp: string;
}
type BaseMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType | ArtifactMessage;
export interface CustomMessages {
}
export type AppMessage = BaseMessage | CustomMessages[keyof CustomMessages];
export declare class UserMessage extends LitElement {
message: UserMessageWithAttachments;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
render(): TemplateResult<1>;
}
export declare class AssistantMessage extends LitElement {
message: AssistantMessageType;
tools?: AgentTool<any>[];
pendingToolCalls?: Set<string>;
hideToolCalls: boolean;
toolResultsById?: Map<string, ToolResultMessageType>;
isStreaming: boolean;
onCostClick?: () => void;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
render(): TemplateResult<1>;
}
export declare class ToolMessageDebugView extends LitElement {
callArgs: any;
result?: ToolResultMessageType;
hasResult: boolean;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private pretty;
render(): TemplateResult<1>;
}
export declare class ToolMessage extends LitElement {
toolCall: ToolCall;
tool?: AgentTool<any>;
result?: ToolResultMessageType;
pending: boolean;
aborted: boolean;
isStreaming: boolean;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
render(): TemplateResult;
}
export declare class AbortedMessage extends LitElement {
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
protected render(): unknown;
}
export {};
//# sourceMappingURL=Messages.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"Messages.d.ts","sourceRoot":"","sources":["../../src/components/Messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,SAAS,EACT,gBAAgB,IAAI,oBAAoB,EACxC,QAAQ,EACR,iBAAiB,IAAI,qBAAqB,EAC1C,WAAW,IAAI,eAAe,EAC9B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAG5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAG/D,OAAO,oBAAoB,CAAC;AAE5B,MAAM,MAAM,0BAA0B,GAAG,eAAe,GAAG;IAAE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC;AAG1F,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CAClB;AAGD,KAAK,WAAW,GAAG,oBAAoB,GAAG,0BAA0B,GAAG,qBAAqB,GAAG,eAAe,CAAC;AAS/G,MAAM,WAAW,cAAc;CAE9B;AAGD,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,cAAc,CAAC,MAAM,cAAc,CAAC,CAAC;AAE5E,qBACa,WAAY,SAAQ,UAAU;IACd,OAAO,EAAG,0BAA0B,CAAC;cAE9C,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKzB,MAAM;CAyBf;AAED,qBACa,gBAAiB,SAAQ,UAAU;IACnB,OAAO,EAAG,oBAAoB,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;IACxB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,UAAS;IACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IACpD,WAAW,EAAE,OAAO,CAAS;IAC1B,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;cAEtC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKzB,MAAM;CA2Df;AAED,qBACa,oBAAqB,SAAQ,UAAU;IACvB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAS;cAErC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKlC,OAAO,CAAC,MAAM;IAYL,MAAM;CA2Bf;AAED,qBACa,WAAY,SAAQ,UAAU;IACd,QAAQ,EAAG,QAAQ,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IACtB,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC9B,OAAO,EAAE,OAAO,CAAS;IACzB,OAAO,EAAE,OAAO,CAAS;IACzB,WAAW,EAAE,OAAO,CAAS;cAEvC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKzB,MAAM;CAiCf;AAED,qBACa,cAAe,SAAQ,UAAU;cAC1B,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;cAKf,MAAM,IAAI,OAAO;CAGpC"}

View File

@@ -0,0 +1,275 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { renderTool } from "../tools/index.js";
import { formatUsage } from "../utils/format.js";
import { i18n } from "../utils/i18n.js";
import "./ThinkingBlock.js";
let UserMessage = class UserMessage extends LitElement {
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
render() {
const content = typeof this.message.content === "string"
? this.message.content
: this.message.content.find((c) => c.type === "text")?.text || "";
return html `
<div class="flex justify-start mx-4">
<div class="user-message-container py-2 px-4 rounded-xl">
<markdown-block .content=${content}></markdown-block>
${this.message.attachments && this.message.attachments.length > 0
? html `
<div class="mt-3 flex flex-wrap gap-2">
${this.message.attachments.map((attachment) => html ` <attachment-tile .attachment=${attachment}></attachment-tile> `)}
</div>
`
: ""}
</div>
</div>
`;
}
};
__decorate([
property({ type: Object })
], UserMessage.prototype, "message", void 0);
UserMessage = __decorate([
customElement("user-message")
], UserMessage);
export { UserMessage };
let AssistantMessage = class AssistantMessage extends LitElement {
constructor() {
super(...arguments);
this.hideToolCalls = false;
this.isStreaming = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
render() {
// Render content in the order it appears
const orderedParts = [];
for (const chunk of this.message.content) {
if (chunk.type === "text" && chunk.text.trim() !== "") {
orderedParts.push(html `<markdown-block .content=${chunk.text}></markdown-block>`);
}
else if (chunk.type === "thinking" && chunk.thinking.trim() !== "") {
orderedParts.push(html `<thinking-block .content=${chunk.thinking} .isStreaming=${this.isStreaming}></thinking-block>`);
}
else if (chunk.type === "toolCall") {
if (!this.hideToolCalls) {
const tool = this.tools?.find((t) => t.name === chunk.name);
const pending = this.pendingToolCalls?.has(chunk.id) ?? false;
const result = this.toolResultsById?.get(chunk.id);
// A tool call is aborted if the message was aborted and there's no result for this tool call
const aborted = this.message.stopReason === "aborted" && !result;
orderedParts.push(html `<tool-message
.tool=${tool}
.toolCall=${chunk}
.result=${result}
.pending=${pending}
.aborted=${aborted}
.isStreaming=${this.isStreaming}
></tool-message>`);
}
}
}
return html `
<div>
${orderedParts.length ? html ` <div class="px-4 flex flex-col gap-3">${orderedParts}</div> ` : ""}
${this.message.usage && !this.isStreaming
? this.onCostClick
? html ` <div class="px-4 mt-2 text-xs text-muted-foreground cursor-pointer hover:text-foreground transition-colors" @click=${this.onCostClick}>${formatUsage(this.message.usage)}</div> `
: html ` <div class="px-4 mt-2 text-xs text-muted-foreground">${formatUsage(this.message.usage)}</div> `
: ""}
${this.message.stopReason === "error" && this.message.errorMessage
? html `
<div class="mx-4 mt-3 p-3 bg-destructive/10 text-destructive rounded-lg text-sm overflow-hidden">
<strong>${i18n("Error:")}</strong> ${this.message.errorMessage}
</div>
`
: ""}
${this.message.stopReason === "aborted"
? html `<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`
: ""}
</div>
`;
}
};
__decorate([
property({ type: Object })
], AssistantMessage.prototype, "message", void 0);
__decorate([
property({ type: Array })
], AssistantMessage.prototype, "tools", void 0);
__decorate([
property({ type: Object })
], AssistantMessage.prototype, "pendingToolCalls", void 0);
__decorate([
property({ type: Boolean })
], AssistantMessage.prototype, "hideToolCalls", void 0);
__decorate([
property({ type: Object })
], AssistantMessage.prototype, "toolResultsById", void 0);
__decorate([
property({ type: Boolean })
], AssistantMessage.prototype, "isStreaming", void 0);
__decorate([
property({ attribute: false })
], AssistantMessage.prototype, "onCostClick", void 0);
AssistantMessage = __decorate([
customElement("assistant-message")
], AssistantMessage);
export { AssistantMessage };
let ToolMessageDebugView = class ToolMessageDebugView extends LitElement {
constructor() {
super(...arguments);
this.hasResult = false;
}
createRenderRoot() {
return this; // light DOM for shared styles
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
pretty(value) {
try {
if (typeof value === "string") {
const maybeJson = JSON.parse(value);
return { content: JSON.stringify(maybeJson, null, 2), isJson: true };
}
return { content: JSON.stringify(value, null, 2), isJson: true };
}
catch {
return { content: typeof value === "string" ? value : String(value), isJson: false };
}
}
render() {
const textOutput = this.result?.content
?.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n") || "";
const output = this.pretty(textOutput);
const details = this.pretty(this.result?.details);
return html `
<div class="mt-3 flex flex-col gap-2">
<div>
<div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Call")}</div>
<code-block .code=${this.pretty(this.callArgs).content} language="json"></code-block>
</div>
<div>
<div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Result")}</div>
${this.hasResult
? html `<code-block .code=${output.content} language="${output.isJson ? "json" : "text"}"></code-block>
<code-block .code=${details.content} language="${details.isJson ? "json" : "text"}"></code-block>`
: html `<div class="text-xs text-muted-foreground">${i18n("(no result)")}</div>`}
</div>
</div>
`;
}
};
__decorate([
property({ type: Object })
], ToolMessageDebugView.prototype, "callArgs", void 0);
__decorate([
property({ type: Object })
], ToolMessageDebugView.prototype, "result", void 0);
__decorate([
property({ type: Boolean })
], ToolMessageDebugView.prototype, "hasResult", void 0);
ToolMessageDebugView = __decorate([
customElement("tool-message-debug")
], ToolMessageDebugView);
export { ToolMessageDebugView };
let ToolMessage = class ToolMessage extends LitElement {
constructor() {
super(...arguments);
this.pending = false;
this.aborted = false;
this.isStreaming = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
render() {
const toolName = this.tool?.name || this.toolCall.name;
// Render tool content (renderer handles errors and styling)
const result = this.aborted
? {
role: "toolResult",
isError: true,
content: [],
toolCallId: this.toolCall.id,
toolName: this.toolCall.name,
timestamp: Date.now(),
}
: this.result;
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">
${renderResult.content}
</div>
`;
}
};
__decorate([
property({ type: Object })
], ToolMessage.prototype, "toolCall", void 0);
__decorate([
property({ type: Object })
], ToolMessage.prototype, "tool", void 0);
__decorate([
property({ type: Object })
], ToolMessage.prototype, "result", void 0);
__decorate([
property({ type: Boolean })
], ToolMessage.prototype, "pending", void 0);
__decorate([
property({ type: Boolean })
], ToolMessage.prototype, "aborted", void 0);
__decorate([
property({ type: Boolean })
], ToolMessage.prototype, "isStreaming", void 0);
ToolMessage = __decorate([
customElement("tool-message")
], ToolMessage);
export { ToolMessage };
let AbortedMessage = class AbortedMessage extends LitElement {
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
render() {
return html `<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`;
}
};
AbortedMessage = __decorate([
customElement("aborted-message")
], AbortedMessage);
export { AbortedMessage };
//# sourceMappingURL=Messages.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
import { LitElement } from "lit";
export declare class ProviderKeyInput extends LitElement {
provider: string;
private keyInput;
private testing;
private failed;
private hasKey;
private inputChanged;
protected createRenderRoot(): this;
connectedCallback(): Promise<void>;
private checkKeyStatus;
private testApiKey;
private saveKey;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=ProviderKeyInput.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ProviderKeyInput.d.ts","sourceRoot":"","sources":["../../src/components/ProviderKeyInput.ts"],"names":[],"mappings":"AAIA,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAkBvC,qBACa,gBAAiB,SAAQ,UAAU;IACnC,QAAQ,SAAM;IACjB,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAS;IAEtC,SAAS,CAAC,gBAAgB;IAIX,iBAAiB;YAKlB,cAAc;YASd,UAAU;YAgCV,OAAO;IAiCrB,MAAM;CAqCN"}

View File

@@ -0,0 +1,170 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { i18n } from "@mariozechner/mini-lit";
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { complete, getModel } from "@mariozechner/pi-ai";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { getAppStorage } from "../storage/app-storage.js";
import { applyProxyIfNeeded } from "../utils/proxy-utils.js";
import { Input } from "./Input.js";
// Test models for each provider
const TEST_MODELS = {
anthropic: "claude-3-5-haiku-20241022",
openai: "gpt-4o-mini",
google: "gemini-2.5-flash",
groq: "openai/gpt-oss-20b",
openrouter: "z-ai/glm-4.6",
cerebras: "gpt-oss-120b",
xai: "grok-4-fast-non-reasoning",
zai: "glm-4.5-air",
};
let ProviderKeyInput = class ProviderKeyInput extends LitElement {
constructor() {
super(...arguments);
this.provider = "";
this.keyInput = "";
this.testing = false;
this.failed = false;
this.hasKey = false;
this.inputChanged = false;
}
createRenderRoot() {
return this;
}
async connectedCallback() {
super.connectedCallback();
await this.checkKeyStatus();
}
async checkKeyStatus() {
try {
const key = await getAppStorage().providerKeys.get(this.provider);
this.hasKey = !!key;
}
catch (error) {
console.error("Failed to check key status:", error);
}
}
async testApiKey(provider, apiKey) {
try {
const modelId = TEST_MODELS[provider];
// Returning true here for Ollama and friends. Can' know which model to use for testing
if (!modelId)
return true;
let model = getModel(provider, modelId);
if (!model)
return false;
// Get proxy URL from settings (if available)
const proxyEnabled = await getAppStorage().settings.get("proxy.enabled");
const proxyUrl = await getAppStorage().settings.get("proxy.url");
// Apply proxy only if this provider/key combination requires it
model = applyProxyIfNeeded(model, apiKey, proxyEnabled ? proxyUrl || undefined : undefined);
const context = {
messages: [{ role: "user", content: "Reply with: ok", timestamp: Date.now() }],
};
const result = await complete(model, context, {
apiKey,
maxTokens: 200,
});
return result.stopReason === "stop";
}
catch (error) {
console.error(`API key test failed for ${provider}:`, error);
return false;
}
}
async saveKey() {
if (!this.keyInput)
return;
this.testing = true;
this.failed = false;
const success = await this.testApiKey(this.provider, this.keyInput);
this.testing = false;
if (success) {
try {
await getAppStorage().providerKeys.set(this.provider, this.keyInput);
this.hasKey = true;
this.inputChanged = false;
this.requestUpdate();
}
catch (error) {
console.error("Failed to save API key:", error);
this.failed = true;
setTimeout(() => {
this.failed = false;
this.requestUpdate();
}, 5000);
}
}
else {
this.failed = true;
setTimeout(() => {
this.failed = false;
this.requestUpdate();
}, 5000);
}
}
render() {
return html `
<div class="space-y-3">
<div class="flex items-center gap-2">
<span class="text-sm font-medium capitalize text-foreground">${this.provider}</span>
${this.testing
? Badge({ children: i18n("Testing..."), variant: "secondary" })
: this.hasKey
? html `<span class="text-green-600 dark:text-green-400"></span>`
: ""}
${this.failed ? Badge({ children: i18n("✗ Invalid"), variant: "destructive" }) : ""}
</div>
<div class="flex items-center gap-2">
${Input({
type: "password",
placeholder: this.hasKey ? "••••••••••••" : i18n("Enter API key"),
value: this.keyInput,
onInput: (e) => {
this.keyInput = e.target.value;
this.inputChanged = true;
this.requestUpdate();
},
className: "flex-1",
})}
${Button({
onClick: () => this.saveKey(),
variant: "default",
size: "sm",
disabled: !this.keyInput || this.testing || (this.hasKey && !this.inputChanged),
children: i18n("Save"),
})}
</div>
</div>
`;
}
};
__decorate([
property()
], ProviderKeyInput.prototype, "provider", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "keyInput", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "testing", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "failed", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "hasKey", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "inputChanged", void 0);
ProviderKeyInput = __decorate([
customElement("provider-key-input")
], ProviderKeyInput);
export { ProviderKeyInput };
//# sourceMappingURL=ProviderKeyInput.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ProviderKeyInput.js","sourceRoot":"","sources":["../../src/components/ProviderKeyInput.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,85 @@
import { LitElement } from "lit";
import { type MessageConsumer } from "./sandbox/RuntimeMessageRouter.js";
import type { SandboxRuntimeProvider } from "./sandbox/SandboxRuntimeProvider.js";
export interface SandboxFile {
fileName: string;
content: string | Uint8Array;
mimeType: string;
}
export interface SandboxResult {
success: boolean;
console: Array<{
type: string;
text: string;
}>;
files?: SandboxFile[];
error?: {
message: string;
stack: string;
};
returnValue?: any;
}
/**
* Function that returns the URL to the sandbox HTML file.
* Used in browser extensions to load sandbox.html via chrome.runtime.getURL().
*/
export type SandboxUrlProvider = () => string;
/**
* Configuration for prepareHtmlDocument
*/
export interface PrepareHtmlOptions {
/** True if this is an HTML artifact (inject into existing HTML), false if REPL (wrap in HTML) */
isHtmlArtifact: boolean;
/** True if this is a standalone download (no runtime bridge, no navigation interceptor) */
isStandalone?: boolean;
}
export declare class SandboxIframe extends LitElement {
private iframe?;
/**
* Optional: Provide a function that returns the sandbox HTML URL.
* If provided, the iframe will use this URL instead of srcdoc.
* This is required for browser extensions with strict CSP.
*/
sandboxUrlProvider?: SandboxUrlProvider;
createRenderRoot(): this;
connectedCallback(): void;
disconnectedCallback(): void;
/**
* Load HTML content into sandbox and keep it displayed (for HTML artifacts)
* @param sandboxId Unique ID
* @param htmlContent Full HTML content
* @param providers Runtime providers to inject
* @param consumers Message consumers to register (optional)
*/
loadContent(sandboxId: string, htmlContent: string, providers?: SandboxRuntimeProvider[], consumers?: MessageConsumer[]): void;
private loadViaSandboxUrl;
private loadViaSrcdoc;
/**
* Execute code in sandbox
* @param sandboxId Unique ID for this execution
* @param code User code (plain JS for REPL, or full HTML for artifacts)
* @param providers Runtime providers to inject
* @param consumers Additional message consumers (optional, execute has its own internal consumer)
* @param signal Abort signal
* @returns Promise resolving to execution result
*/
execute(sandboxId: string, code: string, providers?: SandboxRuntimeProvider[], consumers?: MessageConsumer[], signal?: AbortSignal, isHtmlArtifact?: boolean): Promise<SandboxResult>;
/**
* 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;
/**
* Prepare complete HTML document with runtime + user code
* PUBLIC so HtmlArtifact can use it for download button
*/
prepareHtmlDocument(sandboxId: string, userCode: string, providers?: SandboxRuntimeProvider[], options?: PrepareHtmlOptions): string;
/**
* Generate runtime script from providers
* @param sandboxId Unique sandbox ID
* @param providers Runtime providers
* @param isStandalone If true, skip runtime bridge and navigation interceptor (for standalone downloads)
*/
private getRuntimeScript;
}
//# sourceMappingURL=SandboxedIframe.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SandboxedIframe.d.ts","sourceRoot":"","sources":["../../src/components/SandboxedIframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAIjC,OAAO,EAAE,KAAK,eAAe,EAA0B,MAAM,mCAAmC,CAAC;AACjG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,GAAG,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,MAAM,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,iGAAiG;IACjG,cAAc,EAAE,OAAO,CAAC;IACxB,2FAA2F;IAC3F,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAWD,qBACa,aAAc,SAAQ,UAAU;IAC5C,OAAO,CAAC,MAAM,CAAC,CAAoB;IAEnC;;;;OAIG;IAC6B,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IAExE,gBAAgB;IAIP,iBAAiB;IAIjB,oBAAoB;IAQ7B;;;;;;OAMG;IACI,WAAW,CACjB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,SAAS,GAAE,sBAAsB,EAAO,EACxC,SAAS,GAAE,eAAe,EAAO,GAC/B,IAAI;IAkDP,OAAO,CAAC,iBAAiB;IAuEzB,OAAO,CAAC,aAAa;IAyBrB;;;;;;;;OAQG;IACU,OAAO,CACnB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,sBAAsB,EAAO,EACxC,SAAS,GAAE,eAAe,EAAO,EACjC,MAAM,CAAC,EAAE,WAAW,EACpB,cAAc,GAAE,OAAe,GAC7B,OAAO,CAAC,aAAa,CAAC;IAmKzB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAiBpB;;;OAGG;IACI,mBAAmB,CACzB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,sBAAsB,EAAO,EACxC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,MAAM;IAmFT;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;CAgGxB"}

View File

@@ -0,0 +1,511 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ConsoleRuntimeProvider } from "./sandbox/ConsoleRuntimeProvider.js";
import { RuntimeMessageBridge } from "./sandbox/RuntimeMessageBridge.js";
import { RUNTIME_MESSAGE_ROUTER } from "./sandbox/RuntimeMessageRouter.js";
/**
* 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) {
return code.replace(/<\/script/gi, "<\\/script");
}
let SandboxIframe = class SandboxIframe extends LitElement {
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
}
disconnectedCallback() {
super.disconnectedCallback();
// Note: We don't unregister the sandbox here for loadContent() mode
// because the caller (HtmlArtifact) owns the sandbox lifecycle.
// For execute() mode, the sandbox is unregistered in the cleanup function.
this.iframe?.remove();
}
/**
* Load HTML content into sandbox and keep it displayed (for HTML artifacts)
* @param sandboxId Unique ID
* @param htmlContent Full HTML content
* @param providers Runtime providers to inject
* @param consumers Message consumers to register (optional)
*/
loadContent(sandboxId, htmlContent, providers = [], consumers = []) {
// Unregister previous sandbox if exists
try {
RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
}
catch {
// Sandbox might not exist, that's ok
}
providers = [new ConsoleRuntimeProvider(), ...providers];
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
// loadContent is always used for HTML artifacts (not standalone)
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, {
isHtmlArtifact: true,
isStandalone: false,
});
// 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();
if (this.sandboxUrlProvider) {
// Browser extension mode: use sandbox.html with postMessage
this.loadViaSandboxUrl(sandboxId, completeHtml);
}
else {
// Web mode: use srcdoc
this.loadViaSrcdoc(sandboxId, completeHtml);
}
}
loadViaSandboxUrl(sandboxId, completeHtml) {
// Create iframe pointing to sandbox URL
this.iframe = document.createElement("iframe");
this.iframe.sandbox.add("allow-scripts");
this.iframe.sandbox.add("allow-modals");
this.iframe.style.width = "100%";
this.iframe.style.height = "100%";
this.iframe.style.border = "none";
this.iframe.src = this.sandboxUrlProvider();
// Update router with iframe reference BEFORE appending to DOM
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
// Listen for open-external-url messages from iframe
const externalUrlHandler = (e) => {
if (e.data.type === "open-external-url" && e.source === this.iframe?.contentWindow) {
// Use chrome.tabs API to open in new tab
const chromeAPI = globalThis.chrome;
if (chromeAPI?.tabs) {
chromeAPI.tabs.create({ url: e.data.url });
}
else {
// Fallback for non-extension context
window.open(e.data.url, "_blank");
}
}
};
window.addEventListener("message", externalUrlHandler);
// Listen for sandbox-ready and sandbox-error messages directly
const readyHandler = (e) => {
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({
type: "sandbox-load",
sandboxId,
code: completeHtml,
}, "*");
}
};
const errorHandler = (e) => {
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);
}
loadViaSrcdoc(sandboxId, completeHtml) {
// Create iframe with srcdoc
this.iframe = document.createElement("iframe");
this.iframe.sandbox.add("allow-scripts");
this.iframe.sandbox.add("allow-modals");
this.iframe.style.width = "100%";
this.iframe.style.height = "100%";
this.iframe.style.border = "none";
this.iframe.srcdoc = completeHtml;
// Update router with iframe reference BEFORE appending to DOM
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
// Listen for open-external-url messages from iframe
const externalUrlHandler = (e) => {
if (e.data.type === "open-external-url" && e.source === this.iframe?.contentWindow) {
// Fallback for non-extension context
window.open(e.data.url, "_blank");
}
};
window.addEventListener("message", externalUrlHandler);
this.appendChild(this.iframe);
}
/**
* Execute code in sandbox
* @param sandboxId Unique ID for this execution
* @param code User code (plain JS for REPL, or full HTML for artifacts)
* @param providers Runtime providers to inject
* @param consumers Additional message consumers (optional, execute has its own internal consumer)
* @param signal Abort signal
* @returns Promise resolving to execution result
*/
async execute(sandboxId, code, providers = [], consumers = [], signal, isHtmlArtifact = false) {
if (signal?.aborted) {
throw new Error("Execution aborted");
}
const consoleProvider = new ConsoleRuntimeProvider();
providers = [consoleProvider, ...providers];
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
// Notify providers that execution is starting
for (const provider of providers) {
provider.onExecutionStart?.(sandboxId, signal);
}
const files = [];
let completed = false;
return new Promise((resolve, reject) => {
// 4. Create execution consumer for lifecycle messages
const executionConsumer = {
async handleMessage(message) {
if (message.type === "file-returned") {
files.push({
fileName: message.fileName,
content: message.content,
mimeType: message.mimeType,
});
}
else if (message.type === "execution-complete") {
completed = true;
cleanup();
resolve({
success: true,
console: consoleProvider.getLogs(),
files,
returnValue: message.returnValue,
});
}
else if (message.type === "execution-error") {
completed = true;
cleanup();
resolve({ success: false, console: consoleProvider.getLogs(), error: message.error, files });
}
},
};
RUNTIME_MESSAGE_ROUTER.addConsumer(sandboxId, executionConsumer);
const cleanup = () => {
// Notify providers that execution has ended
for (const provider of providers) {
provider.onExecutionEnd?.(sandboxId);
}
RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
signal?.removeEventListener("abort", abortHandler);
clearTimeout(timeoutId);
this.iframe?.remove();
this.iframe = undefined;
};
// Abort handler
const abortHandler = () => {
if (!completed) {
completed = true;
cleanup();
reject(new Error("Execution aborted"));
}
};
if (signal) {
signal.addEventListener("abort", abortHandler);
}
// Timeout handler (30 seconds)
const timeoutId = setTimeout(() => {
if (!completed) {
completed = true;
cleanup();
resolve({
success: false,
console: consoleProvider.getLogs(),
error: { message: "Execution timeout (120s)", stack: "" },
files,
});
}
}, 120000);
// 4. Prepare HTML and create iframe
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, {
isHtmlArtifact,
isStandalone: false,
});
// 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");
this.iframe.sandbox.add("allow-scripts", "allow-modals");
this.iframe.style.cssText = "width: 100%; height: 100%; border: none;";
this.iframe.src = this.sandboxUrlProvider();
// Update router with iframe reference BEFORE appending to DOM
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
// Listen for sandbox-ready and sandbox-error messages
const readyHandler = (e) => {
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({
type: "sandbox-load",
sandboxId,
code: completeHtml,
}, "*");
}
};
const errorHandler = (e) => {
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 {
// Web mode: use srcdoc
this.iframe = document.createElement("iframe");
this.iframe.sandbox.add("allow-scripts", "allow-modals");
this.iframe.style.cssText = "width: 100%; height: 100%; border: none; display: none;";
this.iframe.srcdoc = completeHtml;
// Update router with iframe reference BEFORE appending to DOM
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
this.appendChild(this.iframe);
}
});
}
/**
* 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
*/
validateHtml(html) {
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) {
return error.message || "Unknown validation error";
}
}
/**
* Prepare complete HTML document with runtime + user code
* PUBLIC so HtmlArtifact can use it for download button
*/
prepareHtmlDocument(sandboxId, userCode, providers = [], options) {
// Default options
const opts = {
isHtmlArtifact: false,
isStandalone: false,
...options,
};
// Runtime script that will be injected
const runtime = this.getRuntimeScript(sandboxId, providers, opts.isStandalone || false);
// Only check for HTML tags if explicitly marked as HTML artifact
// For javascript_repl, userCode is JavaScript that may contain HTML in string literals
if (opts.isHtmlArtifact) {
// HTML Artifact - inject runtime into existing HTML
const headMatch = userCode.match(/<head[^>]*>/i);
if (headMatch) {
const index = headMatch.index + headMatch[0].length;
return userCode.slice(0, index) + runtime + userCode.slice(index);
}
const htmlMatch = userCode.match(/<html[^>]*>/i);
if (htmlMatch) {
const index = htmlMatch.index + htmlMatch[0].length;
return userCode.slice(0, index) + runtime + userCode.slice(index);
}
// Fallback: prepend runtime
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>
${runtime}
</head>
<body>
<script type="module">
(async () => {
try {
// Wrap user code in async function to capture return value
const userCodeFunc = async () => {
${escapedUserCode}
};
const returnValue = await userCodeFunc();
// Call completion callbacks before complete()
if (window.__completionCallbacks && window.__completionCallbacks.length > 0) {
try {
await Promise.all(window.__completionCallbacks.map(cb => cb(true)));
} catch (e) {
console.error('Completion callback error:', e);
}
}
await window.complete(null, returnValue);
} catch (error) {
// Call completion callbacks before complete() (error path)
if (window.__completionCallbacks && window.__completionCallbacks.length > 0) {
try {
await Promise.all(window.__completionCallbacks.map(cb => cb(false)));
} catch (e) {
console.error('Completion callback error:', e);
}
}
await window.complete({
message: error?.message || String(error),
stack: error?.stack || new Error().stack
});
}
})();
</script>
</body>
</html>`;
}
}
/**
* Generate runtime script from providers
* @param sandboxId Unique sandbox ID
* @param providers Runtime providers
* @param isStandalone If true, skip runtime bridge and navigation interceptor (for standalone downloads)
*/
getRuntimeScript(sandboxId, providers = [], isStandalone = false) {
// Collect all data from providers
const allData = {};
for (const provider of providers) {
Object.assign(allData, provider.getData());
}
// Generate bridge code (skip if standalone)
const bridgeCode = isStandalone
? ""
: RuntimeMessageBridge.generateBridgeCode({
context: "sandbox-iframe",
sandboxId,
});
// Collect all runtime functions - pass sandboxId as string literal
const runtimeFunctions = [];
for (const provider of providers) {
runtimeFunctions.push(`(${provider.getRuntime().toString()})(${JSON.stringify(sandboxId)});`);
}
// Build script with HTML escaping
// Escape </script> to prevent premature tag closure in HTML parser
const dataInjection = Object.entries(allData)
.map(([key, value]) => {
const jsonStr = JSON.stringify(value).replace(/<\/script/gi, "<\\/script");
return `window.${key} = ${jsonStr};`;
})
.join("\n");
// TODO the font-size is needed, as chrome seems to inject a stylesheet into iframes
// found in an extension context like sidepanel, settin body { font-size: 75% }. It's
// definitely not our code doing that.
// See https://stackoverflow.com/questions/71480433/chrome-is-injecting-some-stylesheet-in-popup-ui-which-reduces-the-font-size-to-7
// Navigation interceptor (only if NOT standalone)
const navigationInterceptor = isStandalone
? ""
: `
// Navigation interceptor: prevent all navigation and open externally
(function() {
// Intercept link clicks
document.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (link && link.href) {
// Check if it's an external link (not javascript: or #hash)
if (link.href.startsWith('http://') || link.href.startsWith('https://')) {
e.preventDefault();
e.stopPropagation();
window.parent.postMessage({ type: 'open-external-url', url: link.href }, '*');
}
}
}, true);
// Intercept form submissions
document.addEventListener('submit', function(e) {
const form = e.target;
if (form && form.action) {
e.preventDefault();
e.stopPropagation();
window.parent.postMessage({ type: 'open-external-url', url: form.action }, '*');
}
}, true);
// Prevent window.location changes (only if not already redefined)
try {
const originalLocation = window.location;
Object.defineProperty(window, 'location', {
get: function() { return originalLocation; },
set: function(url) {
window.parent.postMessage({ type: 'open-external-url', url: url.toString() }, '*');
}
});
} catch (e) {
// Already defined, skip
}
})();
`;
return `<style>
html, body {
font-size: initial;
}
</style>
<script>
window.sandboxId = ${JSON.stringify(sandboxId)};
${dataInjection}
${bridgeCode}
${runtimeFunctions.join("\n")}
${navigationInterceptor}
</script>`;
}
};
__decorate([
property({ attribute: false })
], SandboxIframe.prototype, "sandboxUrlProvider", void 0);
SandboxIframe = __decorate([
customElement("sandbox-iframe")
], SandboxIframe);
export { SandboxIframe };
//# sourceMappingURL=SandboxedIframe.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
import type { AgentTool, Message, ToolResultMessage } from "@mariozechner/pi-ai";
import { LitElement } from "lit";
export declare class StreamingMessageContainer extends LitElement {
tools: AgentTool[];
isStreaming: boolean;
pendingToolCalls?: Set<string>;
toolResultsById?: Map<string, ToolResultMessage>;
onCostClick?: () => void;
private _message;
private _pendingMessage;
private _updateScheduled;
private _immediateUpdate;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
setMessage(message: Message | null, immediate?: boolean): void;
render(): import("lit-html").TemplateResult<1> | undefined;
}
//# sourceMappingURL=StreamingMessageContainer.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"StreamingMessageContainer.d.ts","sourceRoot":"","sources":["../../src/components/StreamingMessageContainer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAGvC,qBAAa,yBAA0B,SAAQ,UAAU;IAC7B,KAAK,EAAE,SAAS,EAAE,CAAM;IACtB,WAAW,UAAS;IACrB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEhD,OAAO,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,gBAAgB,CAAS;cAEd,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAM3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,EAAE,SAAS,UAAQ;IAmCnD,MAAM;CAmCf"}

View File

@@ -0,0 +1,117 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, LitElement } from "lit";
import { property, state } from "lit/decorators.js";
export class StreamingMessageContainer extends LitElement {
constructor() {
super(...arguments);
this.tools = [];
this.isStreaming = false;
this._message = null;
this._pendingMessage = null;
this._updateScheduled = false;
this._immediateUpdate = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
// Public method to update the message with batching for performance
setMessage(message, immediate = false) {
// Store the latest message
this._pendingMessage = message;
// If this is an immediate update (like clearing), apply it right away
if (immediate || message === null) {
this._immediateUpdate = true;
this._message = message;
this.requestUpdate();
// Cancel any pending updates since we're clearing
this._pendingMessage = null;
this._updateScheduled = false;
return;
}
// Otherwise batch updates for performance during streaming
if (!this._updateScheduled) {
this._updateScheduled = true;
requestAnimationFrame(async () => {
// Only apply the update if we haven't been cleared
if (!this._immediateUpdate && this._pendingMessage !== null) {
// Deep clone the message to ensure Lit detects changes in nested properties
// (like toolCall.arguments being mutated during streaming)
this._message = JSON.parse(JSON.stringify(this._pendingMessage));
this.requestUpdate();
}
// Reset for next batch
this._pendingMessage = null;
this._updateScheduled = false;
this._immediateUpdate = false;
});
}
}
render() {
// Show loading indicator if loading but no message yet
if (!this._message) {
if (this.isStreaming)
return html `<div class="flex flex-col gap-3 mb-3">
<span class="mx-4 inline-block w-2 h-4 bg-muted-foreground animate-pulse"></span>
</div>`;
return html ``; // Empty until a message is set
}
const msg = this._message;
if (msg.role === "toolResult") {
// Skip standalone tool result in streaming; the stable list will render paired tool-message
return html ``;
}
else if (msg.role === "user") {
// Skip standalone tool result in streaming; the stable list will render it immediiately
return html ``;
}
else if (msg.role === "assistant") {
// Assistant message - render inline tool messages during streaming
return html `
<div class="flex flex-col gap-3 mb-3">
<assistant-message
.message=${msg}
.tools=${this.tools}
.isStreaming=${this.isStreaming}
.pendingToolCalls=${this.pendingToolCalls}
.toolResultsById=${this.toolResultsById}
.hideToolCalls=${false}
.onCostClick=${this.onCostClick}
></assistant-message>
${this.isStreaming ? html `<span class="mx-4 inline-block w-2 h-4 bg-muted-foreground animate-pulse"></span>` : ""}
</div>
`;
}
}
}
__decorate([
property({ type: Array })
], StreamingMessageContainer.prototype, "tools", void 0);
__decorate([
property({ type: Boolean })
], StreamingMessageContainer.prototype, "isStreaming", void 0);
__decorate([
property({ type: Object })
], StreamingMessageContainer.prototype, "pendingToolCalls", void 0);
__decorate([
property({ type: Object })
], StreamingMessageContainer.prototype, "toolResultsById", void 0);
__decorate([
property({ attribute: false })
], StreamingMessageContainer.prototype, "onCostClick", void 0);
__decorate([
state()
], StreamingMessageContainer.prototype, "_message", void 0);
// Register custom element
if (!customElements.get("streaming-message-container")) {
customElements.define("streaming-message-container", StreamingMessageContainer);
}
//# sourceMappingURL=StreamingMessageContainer.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"StreamingMessageContainer.js","sourceRoot":"","sources":["../../src/components/StreamingMessageContainer.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,11 @@
import { LitElement } from "lit";
export declare class ThinkingBlock extends LitElement {
content: string;
isStreaming: boolean;
private isExpanded;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private toggleExpanded;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=ThinkingBlock.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ThinkingBlock.d.ts","sourceRoot":"","sources":["../../src/components/ThinkingBlock.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAIvC,qBACa,aAAc,SAAQ,UAAU;IAChC,OAAO,EAAG,MAAM,CAAC;IACA,WAAW,UAAS;IACxC,OAAO,CAAC,UAAU,CAAS;cAEjB,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKlC,OAAO,CAAC,cAAc;IAIb,MAAM;CAkBf"}

View File

@@ -0,0 +1,58 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ChevronRight } from "lucide";
let ThinkingBlock = class ThinkingBlock extends LitElement {
constructor() {
super(...arguments);
this.isStreaming = false;
this.isExpanded = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
toggleExpanded() {
this.isExpanded = !this.isExpanded;
}
render() {
const shimmerClasses = this.isStreaming
? "animate-shimmer bg-gradient-to-r from-muted-foreground via-foreground to-muted-foreground bg-[length:200%_100%] bg-clip-text text-transparent"
: "";
return html `
<div class="thinking-block">
<div
class="thinking-header cursor-pointer select-none flex items-center gap-2 py-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
@click=${this.toggleExpanded}
>
<span class="transition-transform inline-block ${this.isExpanded ? "rotate-90" : ""}">${icon(ChevronRight, "sm")}</span>
<span class="${shimmerClasses}">Thinking...</span>
</div>
${this.isExpanded ? html `<markdown-block .content=${this.content} .isThinking=${true}></markdown-block>` : ""}
</div>
`;
}
};
__decorate([
property()
], ThinkingBlock.prototype, "content", void 0);
__decorate([
property({ type: Boolean })
], ThinkingBlock.prototype, "isStreaming", void 0);
__decorate([
state()
], ThinkingBlock.prototype, "isExpanded", void 0);
ThinkingBlock = __decorate([
customElement("thinking-block")
], ThinkingBlock);
export { ThinkingBlock };
//# sourceMappingURL=ThinkingBlock.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ThinkingBlock.js","sourceRoot":"","sources":["../../src/components/ThinkingBlock.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAG/B,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QAEuB,gBAAW,GAAG,KAAK,CAAC;QAChC,eAAU,GAAG,KAAK,CAAC;IAiCrC,CAAC;IA/BmB,gBAAgB;QAClC,OAAO,IAAI,CAAC;IACb,CAAC;IAEQ,iBAAiB;QACzB,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;IAC9B,CAAC;IAEO,cAAc;QACrB,IAAI,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC;IACpC,CAAC;IAEQ,MAAM;QACd,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW;YACtC,CAAC,CAAC,+IAA+I;YACjJ,CAAC,CAAC,EAAE,CAAC;QAEN,OAAO,IAAI,CAAA;;;;cAIC,IAAI,CAAC,cAAc;;sDAEqB,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC;oBACjG,cAAc;;MAE5B,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA,4BAA4B,IAAI,CAAC,OAAO,gBAAgB,IAAI,oBAAoB,CAAC,CAAC,CAAC,EAAE;;GAE9G,CAAC;IACH,CAAC;CACD,CAAA;AAnCY;IAAX,QAAQ,EAAE;8CAAkB;AACA;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;kDAAqB;AAChC;IAAhB,KAAK,EAAE;iDAA4B;AAHxB,aAAa;IADzB,aAAa,CAAC,gBAAgB,CAAC;GACnB,aAAa,CAoCzB"}

View File

@@ -0,0 +1,12 @@
import type { TemplateResult } from "lit";
import type { AppMessage } from "./Messages.js";
export type MessageRole = AppMessage["role"];
export interface MessageRenderer<TMessage extends AppMessage = AppMessage> {
render(message: TMessage): TemplateResult;
}
export declare function registerMessageRenderer<TRole extends MessageRole>(role: TRole, renderer: MessageRenderer<Extract<AppMessage, {
role: TRole;
}>>): void;
export declare function getMessageRenderer(role: MessageRole): MessageRenderer | undefined;
export declare function renderMessage(message: AppMessage): TemplateResult | undefined;
//# sourceMappingURL=message-renderer-registry.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"message-renderer-registry.d.ts","sourceRoot":"","sources":["../../src/components/message-renderer-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;AAG7C,MAAM,WAAW,eAAe,CAAC,QAAQ,SAAS,UAAU,GAAG,UAAU;IACxE,MAAM,CAAC,OAAO,EAAE,QAAQ,GAAG,cAAc,CAAC;CAC1C;AAKD,wBAAgB,uBAAuB,CAAC,KAAK,SAAS,WAAW,EAChE,IAAI,EAAE,KAAK,EACX,QAAQ,EAAE,eAAe,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC,GAC7D,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,CAEjF;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,cAAc,GAAG,SAAS,CAE7E"}

View File

@@ -0,0 +1,12 @@
// Registry of custom message renderers by role
const messageRenderers = new Map();
export function registerMessageRenderer(role, renderer) {
messageRenderers.set(role, renderer);
}
export function getMessageRenderer(role) {
return messageRenderers.get(role);
}
export function renderMessage(message) {
return messageRenderers.get(message.role)?.render(message);
}
//# sourceMappingURL=message-renderer-registry.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"message-renderer-registry.js","sourceRoot":"","sources":["../../src/components/message-renderer-registry.ts"],"names":[],"mappings":"AAWA,+CAA+C;AAC/C,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAqC,CAAC;AAEtE,MAAM,UAAU,uBAAuB,CACtC,IAAW,EACX,QAA+D;IAE/D,gBAAgB,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAiB;IACnD,OAAO,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAmB;IAChD,OAAO,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;AAC5D,CAAC"}

View File

@@ -0,0 +1,35 @@
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
interface ArtifactsPanelLike {
artifacts: Map<string, {
content: string;
}>;
tool: {
execute(toolCallId: string, args: {
command: string;
filename: string;
content?: string;
}): Promise<any>;
};
}
interface AgentLike {
appendMessage(message: any): void;
}
/**
* Artifacts Runtime Provider
*
* Provides programmatic access to session artifacts from sandboxed code.
* Allows code to create, read, update, and delete artifacts dynamically.
* Supports both online (extension) and offline (downloaded HTML) modes.
*/
export declare class ArtifactsRuntimeProvider implements SandboxRuntimeProvider {
private artifactsPanel;
private agent?;
private readWrite;
constructor(artifactsPanel: ArtifactsPanelLike, agent?: AgentLike | undefined, readWrite?: boolean);
getData(): Record<string, any>;
getRuntime(): (sandboxId: string) => void;
handleMessage(message: any, respond: (response: any) => void): Promise<void>;
getDescription(): string;
}
export {};
//# sourceMappingURL=ArtifactsRuntimeProvider.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ArtifactsRuntimeProvider.d.ts","sourceRoot":"","sources":["../../../src/components/sandbox/ArtifactsRuntimeProvider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAG1E,UAAU,kBAAkB;IAC3B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5C,IAAI,EAAE;QACL,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;KACzG,CAAC;CACF;AAED,UAAU,SAAS;IAClB,aAAa,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC;CAClC;AAED;;;;;;GAMG;AACH,qBAAa,wBAAyB,YAAW,sBAAsB;IAErE,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,KAAK,CAAC;IACd,OAAO,CAAC,SAAS;gBAFT,cAAc,EAAE,kBAAkB,EAClC,KAAK,CAAC,EAAE,SAAS,YAAA,EACjB,SAAS,GAAE,OAAc;IAGlC,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAS9B,UAAU,IAAI,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI;IAgGnC,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA8ElF,cAAc,IAAI,MAAM;CAGxB"}

View File

@@ -0,0 +1,189 @@
import { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO, ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW, } from "../../prompts/prompts.js";
/**
* Artifacts Runtime Provider
*
* Provides programmatic access to session artifacts from sandboxed code.
* Allows code to create, read, update, and delete artifacts dynamically.
* Supports both online (extension) and offline (downloaded HTML) modes.
*/
export class ArtifactsRuntimeProvider {
constructor(artifactsPanel, agent, readWrite = true) {
this.artifactsPanel = artifactsPanel;
this.agent = agent;
this.readWrite = readWrite;
}
getData() {
// Inject artifact snapshot for offline mode
const snapshot = {};
this.artifactsPanel.artifacts.forEach((artifact, filename) => {
snapshot[filename] = artifact.content;
});
return { artifacts: snapshot };
}
getRuntime() {
// This function will be stringified, so no external references!
return (_sandboxId) => {
// Auto-parse/stringify for .json files
const isJsonFile = (filename) => filename.endsWith(".json");
window.listArtifacts = async () => {
// Online: ask extension
if (window.sendRuntimeMessage) {
const response = await window.sendRuntimeMessage({
type: "artifact-operation",
action: "list",
});
if (!response.success)
throw new Error(response.error);
return response.result;
}
// Offline: return snapshot keys
else {
return Object.keys(window.artifacts || {});
}
};
window.getArtifact = async (filename) => {
let content;
// Online: ask extension
if (window.sendRuntimeMessage) {
const response = await window.sendRuntimeMessage({
type: "artifact-operation",
action: "get",
filename,
});
if (!response.success)
throw new Error(response.error);
content = response.result;
}
// Offline: read snapshot
else {
if (!window.artifacts?.[filename]) {
throw new Error(`Artifact not found (offline mode): ${filename}`);
}
content = window.artifacts[filename];
}
// Auto-parse .json files
if (isJsonFile(filename)) {
try {
return JSON.parse(content);
}
catch (e) {
throw new Error(`Failed to parse JSON from ${filename}: ${e}`);
}
}
return content;
};
window.createOrUpdateArtifact = async (filename, content, mimeType) => {
if (!window.sendRuntimeMessage) {
throw new Error("Cannot create/update artifacts in offline mode (read-only)");
}
let finalContent = content;
// Auto-stringify .json files
if (isJsonFile(filename) && typeof content !== "string") {
finalContent = JSON.stringify(content, null, 2);
}
else if (typeof content !== "string") {
finalContent = JSON.stringify(content, null, 2);
}
const response = await window.sendRuntimeMessage({
type: "artifact-operation",
action: "createOrUpdate",
filename,
content: finalContent,
mimeType,
});
if (!response.success)
throw new Error(response.error);
};
window.deleteArtifact = async (filename) => {
if (!window.sendRuntimeMessage) {
throw new Error("Cannot delete artifacts in offline mode (read-only)");
}
const response = await window.sendRuntimeMessage({
type: "artifact-operation",
action: "delete",
filename,
});
if (!response.success)
throw new Error(response.error);
};
};
}
async handleMessage(message, respond) {
if (message.type !== "artifact-operation") {
return;
}
const { action, filename, content, mimeType } = message;
try {
switch (action) {
case "list": {
const filenames = Array.from(this.artifactsPanel.artifacts.keys());
respond({ success: true, result: filenames });
break;
}
case "get": {
const artifact = this.artifactsPanel.artifacts.get(filename);
if (!artifact) {
respond({ success: false, error: `Artifact not found: ${filename}` });
}
else {
respond({ success: true, result: artifact.content });
}
break;
}
case "createOrUpdate": {
try {
const exists = this.artifactsPanel.artifacts.has(filename);
const command = exists ? "rewrite" : "create";
const action = exists ? "update" : "create";
await this.artifactsPanel.tool.execute("", {
command,
filename,
content,
});
this.agent?.appendMessage({
role: "artifact",
action,
filename,
content,
...(action === "create" && { title: filename }),
timestamp: new Date().toISOString(),
});
respond({ success: true });
}
catch (err) {
respond({ success: false, error: err.message });
}
break;
}
case "delete": {
try {
await this.artifactsPanel.tool.execute("", {
command: "delete",
filename,
});
this.agent?.appendMessage({
role: "artifact",
action: "delete",
filename,
timestamp: new Date().toISOString(),
});
respond({ success: true });
}
catch (err) {
respond({ success: false, error: err.message });
}
break;
}
default:
respond({ success: false, error: `Unknown artifact action: ${action}` });
}
}
catch (error) {
respond({ success: false, error: error.message });
}
}
getDescription() {
return this.readWrite ? ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW : ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO;
}
}
//# sourceMappingURL=ArtifactsRuntimeProvider.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,17 @@
import type { Attachment } from "../../utils/attachment-utils.js";
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
/**
* Attachments Runtime Provider
*
* OPTIONAL provider that provides file access APIs to sandboxed code.
* Only needed when attachments are present.
* Attachments are read-only snapshot data - no messaging needed.
*/
export declare class AttachmentsRuntimeProvider implements SandboxRuntimeProvider {
private attachments;
constructor(attachments: Attachment[]);
getData(): Record<string, any>;
getRuntime(): (sandboxId: string) => void;
getDescription(): string;
}
//# sourceMappingURL=AttachmentsRuntimeProvider.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"AttachmentsRuntimeProvider.d.ts","sourceRoot":"","sources":["../../../src/components/sandbox/AttachmentsRuntimeProvider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE1E;;;;;;GAMG;AACH,qBAAa,0BAA2B,YAAW,sBAAsB;IAC5D,OAAO,CAAC,WAAW;gBAAX,WAAW,EAAE,UAAU,EAAE;IAE7C,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAa9B,UAAU,IAAI,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI;IAmCzC,cAAc,IAAI,MAAM;CAGxB"}

View File

@@ -0,0 +1,64 @@
import { ATTACHMENTS_RUNTIME_DESCRIPTION } from "../../prompts/prompts.js";
/**
* Attachments Runtime Provider
*
* OPTIONAL provider that provides file access APIs to sandboxed code.
* Only needed when attachments are present.
* Attachments are read-only snapshot data - no messaging needed.
*/
export class AttachmentsRuntimeProvider {
constructor(attachments) {
this.attachments = attachments;
}
getData() {
const attachmentsData = this.attachments.map((a) => ({
id: a.id,
fileName: a.fileName,
mimeType: a.mimeType,
size: a.size,
content: a.content,
extractedText: a.extractedText,
}));
return { attachments: attachmentsData };
}
getRuntime() {
// This function will be stringified, so no external references!
// These functions read directly from window.attachments
// Works both online AND offline (no messaging needed!)
return (_sandboxId) => {
window.listAttachments = () => (window.attachments || []).map((a) => ({
id: a.id,
fileName: a.fileName,
mimeType: a.mimeType,
size: a.size,
}));
window.readTextAttachment = (attachmentId) => {
const a = (window.attachments || []).find((x) => x.id === attachmentId);
if (!a)
throw new Error("Attachment not found: " + attachmentId);
if (a.extractedText)
return a.extractedText;
try {
return atob(a.content);
}
catch {
throw new Error("Failed to decode text content for: " + attachmentId);
}
};
window.readBinaryAttachment = (attachmentId) => {
const a = (window.attachments || []).find((x) => x.id === attachmentId);
if (!a)
throw new Error("Attachment not found: " + attachmentId);
const bin = atob(a.content);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++)
bytes[i] = bin.charCodeAt(i);
return bytes;
};
};
}
getDescription() {
return ATTACHMENTS_RUNTIME_DESCRIPTION;
}
}
//# sourceMappingURL=AttachmentsRuntimeProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"AttachmentsRuntimeProvider.js","sourceRoot":"","sources":["../../../src/components/sandbox/AttachmentsRuntimeProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAC;AAI3E;;;;;;GAMG;AACH,MAAM,OAAO,0BAA0B;IACtC,YAAoB,WAAyB;QAAzB,gBAAW,GAAX,WAAW,CAAc;IAAG,CAAC;IAEjD,OAAO;QACN,MAAM,eAAe,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACpD,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,aAAa,EAAE,CAAC,CAAC,aAAa;SAC9B,CAAC,CAAC,CAAC;QAEJ,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC;IACzC,CAAC;IAED,UAAU;QACT,gEAAgE;QAChE,wDAAwD;QACxD,uDAAuD;QACvD,OAAO,CAAC,UAAkB,EAAE,EAAE;YAC5B,MAAc,CAAC,eAAe,GAAG,GAAG,EAAE,CACtC,CAAE,MAAc,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;gBACpD,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,IAAI,EAAE,CAAC,CAAC,IAAI;aACZ,CAAC,CAAC,CAAC;YAEJ,MAAc,CAAC,kBAAkB,GAAG,CAAC,YAAoB,EAAE,EAAE;gBAC7D,MAAM,CAAC,GAAG,CAAE,MAAc,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,YAAY,CAAC,CAAC;gBACtF,IAAI,CAAC,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,YAAY,CAAC,CAAC;gBACjE,IAAI,CAAC,CAAC,aAAa;oBAAE,OAAO,CAAC,CAAC,aAAa,CAAC;gBAC5C,IAAI,CAAC;oBACJ,OAAO,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;gBACxB,CAAC;gBAAC,MAAM,CAAC;oBACR,MAAM,IAAI,KAAK,CAAC,qCAAqC,GAAG,YAAY,CAAC,CAAC;gBACvE,CAAC;YACF,CAAC,CAAC;YAED,MAAc,CAAC,oBAAoB,GAAG,CAAC,YAAoB,EAAE,EAAE;gBAC/D,MAAM,CAAC,GAAG,CAAE,MAAc,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,YAAY,CAAC,CAAC;gBACtF,IAAI,CAAC,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,YAAY,CAAC,CAAC;gBACjE,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;gBAC5B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;oBAAE,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAClE,OAAO,KAAK,CAAC;YACd,CAAC,CAAC;QACH,CAAC,CAAC;IACH,CAAC;IAED,cAAc;QACb,OAAO,+BAA+B,CAAC;IACxC,CAAC;CACD"}

View File

@@ -0,0 +1,42 @@
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
export interface ConsoleLog {
type: "log" | "warn" | "error" | "info";
text: string;
args?: unknown[];
}
/**
* Console Runtime Provider
*
* REQUIRED provider that should always be included first.
* Provides console capture, error handling, and execution lifecycle management.
* Collects console output for retrieval by caller.
*/
export declare class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
private logs;
private completionError;
private completed;
getData(): Record<string, any>;
getDescription(): string;
getRuntime(): (sandboxId: string) => void;
handleMessage(message: any, respond: (response: any) => void): Promise<void>;
/**
* Get collected console logs
*/
getLogs(): ConsoleLog[];
/**
* Get completion status
*/
isCompleted(): boolean;
/**
* Get completion error if any
*/
getCompletionError(): {
message: string;
stack: string;
} | null;
/**
* Reset state for reuse
*/
reset(): void;
}
//# sourceMappingURL=ConsoleRuntimeProvider.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ConsoleRuntimeProvider.d.ts","sourceRoot":"","sources":["../../../src/components/sandbox/ConsoleRuntimeProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE1E,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;CACjB;AAED;;;;;;GAMG;AACH,qBAAa,sBAAuB,YAAW,sBAAsB;IACpE,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,eAAe,CAAmD;IAC1E,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAK9B,cAAc,IAAI,MAAM;IAIxB,UAAU,IAAI,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI;IA4GnC,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBlF;;OAEG;IACH,OAAO,IAAI,UAAU,EAAE;IAIvB;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,kBAAkB,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAI/D;;OAEG;IACH,KAAK,IAAI,IAAI;CAKb"}

View File

@@ -0,0 +1,161 @@
/**
* Console Runtime Provider
*
* REQUIRED provider that should always be included first.
* Provides console capture, error handling, and execution lifecycle management.
* Collects console output for retrieval by caller.
*/
export class ConsoleRuntimeProvider {
constructor() {
this.logs = [];
this.completionError = null;
this.completed = false;
}
getData() {
// No data needed
return {};
}
getDescription() {
return "";
}
getRuntime() {
return (_sandboxId) => {
// Store truly original console methods on first wrap only
// This prevents accumulation of wrapper functions across multiple executions
if (!window.__originalConsole) {
window.__originalConsole = {
log: console.log.bind(console),
error: console.error.bind(console),
warn: console.warn.bind(console),
info: console.info.bind(console),
};
}
// Always use the truly original console, not the current (possibly wrapped) one
const originalConsole = window.__originalConsole;
// Track pending send promises to wait for them in onCompleted
const pendingSends = [];
["log", "error", "warn", "info"].forEach((method) => {
console[method] = (...args) => {
const text = args
.map((arg) => {
try {
return typeof arg === "object" ? JSON.stringify(arg) : String(arg);
}
catch {
return String(arg);
}
})
.join(" ");
// Always log locally too (using truly original console)
originalConsole[method].apply(console, args);
// Send immediately and track the promise (only in extension context)
if (window.sendRuntimeMessage) {
const sendPromise = window
.sendRuntimeMessage({
type: "console",
method,
text,
args,
})
.catch(() => { });
pendingSends.push(sendPromise);
}
};
});
// Register completion callback to wait for all pending sends
if (window.onCompleted) {
window.onCompleted(async (_success) => {
// Wait for all pending console sends to complete
if (pendingSends.length > 0) {
await Promise.all(pendingSends);
}
});
}
// Track errors for HTML artifacts
let lastError = null;
// Error handlers - track errors but don't log them
// (they'll be shown via execution-error message)
window.addEventListener("error", (e) => {
const text = (e.error?.stack || e.message || String(e)) + " at line " + (e.lineno || "?") + ":" + (e.colno || "?");
lastError = {
message: e.error?.message || e.message || String(e),
stack: e.error?.stack || text,
};
});
window.addEventListener("unhandledrejection", (e) => {
const text = "Unhandled promise rejection: " + (e.reason?.message || e.reason || "Unknown error");
lastError = {
message: e.reason?.message || String(e.reason) || "Unhandled promise rejection",
stack: e.reason?.stack || text,
};
});
// Expose complete() method for user code to call
let completionSent = false;
window.complete = async (error, returnValue) => {
if (completionSent)
return;
completionSent = true;
const finalError = error || lastError;
if (window.sendRuntimeMessage) {
if (finalError) {
await window.sendRuntimeMessage({
type: "execution-error",
error: finalError,
});
}
else {
await window.sendRuntimeMessage({
type: "execution-complete",
returnValue,
});
}
}
};
};
}
async handleMessage(message, respond) {
if (message.type === "console") {
// Collect console output
this.logs.push({
type: message.method === "error"
? "error"
: message.method === "warn"
? "warn"
: message.method === "info"
? "info"
: "log",
text: message.text,
args: message.args,
});
// Acknowledge receipt
respond({ success: true });
}
}
/**
* Get collected console logs
*/
getLogs() {
return this.logs;
}
/**
* Get completion status
*/
isCompleted() {
return this.completed;
}
/**
* Get completion error if any
*/
getCompletionError() {
return this.completionError;
}
/**
* Reset state for reuse
*/
reset() {
this.logs = [];
this.completionError = null;
this.completed = false;
}
}
//# sourceMappingURL=ConsoleRuntimeProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ConsoleRuntimeProvider.js","sourceRoot":"","sources":["../../../src/components/sandbox/ConsoleRuntimeProvider.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,30 @@
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
export interface DownloadableFile {
fileName: string;
content: string | Uint8Array;
mimeType: string;
}
/**
* File Download Runtime Provider
*
* Provides returnDownloadableFile() for creating user downloads.
* Files returned this way are NOT accessible to the LLM later (one-time download).
* Works both online (sends to extension) and offline (triggers browser download directly).
* Collects files for retrieval by caller.
*/
export declare class FileDownloadRuntimeProvider implements SandboxRuntimeProvider {
private files;
getData(): Record<string, any>;
getRuntime(): (sandboxId: string) => void;
handleMessage(message: any, respond: (response: any) => void): Promise<void>;
/**
* Get collected files
*/
getFiles(): DownloadableFile[];
/**
* Reset state for reuse
*/
reset(): void;
getDescription(): string;
}
//# sourceMappingURL=FileDownloadRuntimeProvider.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FileDownloadRuntimeProvider.d.ts","sourceRoot":"","sources":["../../../src/components/sandbox/FileDownloadRuntimeProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,qBAAa,2BAA4B,YAAW,sBAAsB;IACzE,OAAO,CAAC,KAAK,CAA0B;IAEvC,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAK9B,UAAU,IAAI,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI;IAuDnC,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAalF;;OAEG;IACH,QAAQ,IAAI,gBAAgB,EAAE;IAI9B;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb,cAAc,IAAI,MAAM;CAGxB"}

View File

@@ -0,0 +1,97 @@
/**
* File Download Runtime Provider
*
* Provides returnDownloadableFile() for creating user downloads.
* Files returned this way are NOT accessible to the LLM later (one-time download).
* Works both online (sends to extension) and offline (triggers browser download directly).
* Collects files for retrieval by caller.
*/
export class FileDownloadRuntimeProvider {
constructor() {
this.files = [];
}
getData() {
// No data needed
return {};
}
getRuntime() {
return (_sandboxId) => {
window.returnDownloadableFile = async (fileName, content, mimeType) => {
let finalContent, finalMimeType;
if (content instanceof Blob) {
const arrayBuffer = await content.arrayBuffer();
finalContent = new Uint8Array(arrayBuffer);
finalMimeType = mimeType || content.type || "application/octet-stream";
if (!mimeType && !content.type) {
throw new Error("returnDownloadableFile: MIME type is required for Blob content. Please provide a mimeType parameter (e.g., 'image/png').");
}
}
else if (content instanceof Uint8Array) {
finalContent = content;
if (!mimeType) {
throw new Error("returnDownloadableFile: MIME type is required for Uint8Array content. Please provide a mimeType parameter (e.g., 'image/png').");
}
finalMimeType = mimeType;
}
else if (typeof content === "string") {
finalContent = content;
finalMimeType = mimeType || "text/plain";
}
else {
finalContent = JSON.stringify(content, null, 2);
finalMimeType = mimeType || "application/json";
}
// Send to extension if in extension context (online mode)
if (window.sendRuntimeMessage) {
const response = await window.sendRuntimeMessage({
type: "file-returned",
fileName,
content: finalContent,
mimeType: finalMimeType,
});
if (response.error)
throw new Error(response.error);
}
else {
// Offline mode: trigger browser download directly
const blob = new Blob([finalContent instanceof Uint8Array ? finalContent : finalContent], {
type: finalMimeType,
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
}
};
};
}
async handleMessage(message, respond) {
if (message.type === "file-returned") {
// Collect file for caller
this.files.push({
fileName: message.fileName,
content: message.content,
mimeType: message.mimeType,
});
respond({ success: true });
}
}
/**
* Get collected files
*/
getFiles() {
return this.files;
}
/**
* Reset state for reuse
*/
reset() {
this.files = [];
}
getDescription() {
return "returnDownloadableFile(filename, content, mimeType?) - Create downloadable file for user (one-time download, not accessible later)";
}
}
//# sourceMappingURL=FileDownloadRuntimeProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"FileDownloadRuntimeProvider.js","sourceRoot":"","sources":["../../../src/components/sandbox/FileDownloadRuntimeProvider.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,19 @@
/**
* Generates sendRuntimeMessage() function for injection into execution contexts.
* Provides unified messaging API that works in both sandbox iframe and user script contexts.
*/
export type MessageType = "request-response" | "fire-and-forget";
export interface RuntimeMessageBridgeOptions {
context: "sandbox-iframe" | "user-script";
sandboxId: string;
}
export declare class RuntimeMessageBridge {
/**
* Generate sendRuntimeMessage() function as injectable string.
* Returns the function source code to be injected into target context.
*/
static generateBridgeCode(options: RuntimeMessageBridgeOptions): string;
private static generateSandboxBridge;
private static generateUserScriptBridge;
}
//# sourceMappingURL=RuntimeMessageBridge.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"RuntimeMessageBridge.d.ts","sourceRoot":"","sources":["../../../src/components/sandbox/RuntimeMessageBridge.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,WAAW,GAAG,kBAAkB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,2BAA2B;IAC3C,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;CAClB;AAGD,qBAAa,oBAAoB;IAChC;;;OAGG;IACH,MAAM,CAAC,kBAAkB,CAAC,OAAO,EAAE,2BAA2B,GAAG,MAAM;IAQvE,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAwCpC,OAAO,CAAC,MAAM,CAAC,wBAAwB;CAevC"}

View File

@@ -0,0 +1,74 @@
/**
* Generates sendRuntimeMessage() function for injection into execution contexts.
* Provides unified messaging API that works in both sandbox iframe and user script contexts.
*/
// biome-ignore lint/complexity/noStaticOnlyClass: fine
export class RuntimeMessageBridge {
/**
* Generate sendRuntimeMessage() function as injectable string.
* Returns the function source code to be injected into target context.
*/
static generateBridgeCode(options) {
if (options.context === "sandbox-iframe") {
return RuntimeMessageBridge.generateSandboxBridge(options.sandboxId);
}
else {
return RuntimeMessageBridge.generateUserScriptBridge(options.sandboxId);
}
}
static generateSandboxBridge(sandboxId) {
// Returns stringified function that uses window.parent.postMessage
return `
window.__completionCallbacks = [];
window.sendRuntimeMessage = async (message) => {
const messageId = 'msg_' + Date.now() + '_' + Math.random().toString(36).substring(2, 9);
return new Promise((resolve, reject) => {
const handler = (e) => {
if (e.data.type === 'runtime-response' && e.data.messageId === messageId) {
window.removeEventListener('message', handler);
if (e.data.success) {
resolve(e.data);
} else {
reject(new Error(e.data.error || 'Operation failed'));
}
}
};
window.addEventListener('message', handler);
window.parent.postMessage({
...message,
sandboxId: ${JSON.stringify(sandboxId)},
messageId: messageId
}, '*');
// Timeout after 30s
setTimeout(() => {
window.removeEventListener('message', handler);
reject(new Error('Runtime message timeout'));
}, 30000);
});
};
window.onCompleted = (callback) => {
window.__completionCallbacks.push(callback);
};
`.trim();
}
static generateUserScriptBridge(sandboxId) {
// Returns stringified function that uses chrome.runtime.sendMessage
return `
window.__completionCallbacks = [];
window.sendRuntimeMessage = async (message) => {
return await chrome.runtime.sendMessage({
...message,
sandboxId: ${JSON.stringify(sandboxId)}
});
};
window.onCompleted = (callback) => {
window.__completionCallbacks.push(callback);
};
`.trim();
}
}
//# sourceMappingURL=RuntimeMessageBridge.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"RuntimeMessageBridge.js","sourceRoot":"","sources":["../../../src/components/sandbox/RuntimeMessageBridge.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,uDAAuD;AACvD,MAAM,OAAO,oBAAoB;IAChC;;;OAGG;IACH,MAAM,CAAC,kBAAkB,CAAC,OAAoC;QAC7D,IAAI,OAAO,CAAC,OAAO,KAAK,gBAAgB,EAAE,CAAC;YAC1C,OAAO,oBAAoB,CAAC,qBAAqB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACtE,CAAC;aAAM,CAAC;YACP,OAAO,oBAAoB,CAAC,wBAAwB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACzE,CAAC;IACF,CAAC;IAEO,MAAM,CAAC,qBAAqB,CAAC,SAAiB;QACrD,mEAAmE;QACnE,OAAO;;;;;;;;;;;;;;;;;;;;;yBAqBgB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;;;;;;;;;;;;;;CAcjD,CAAC,IAAI,EAAE,CAAC;IACR,CAAC;IAEO,MAAM,CAAC,wBAAwB,CAAC,SAAiB;QACxD,oEAAoE;QACpE,OAAO;;;;;qBAKY,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;;;;;;CAM7C,CAAC,IAAI,EAAE,CAAC;IACR,CAAC;CACD"}

View File

@@ -0,0 +1,65 @@
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
/**
* Message consumer interface - components that want to receive messages from sandboxes
*/
export interface MessageConsumer {
/**
* Handle a message from a sandbox.
* All consumers receive all messages - decide internally what to handle.
*/
handleMessage(message: any): Promise<void>;
}
/**
* Centralized message router for all runtime communication.
*
* This singleton replaces all individual window.addEventListener("message") calls
* with a single global listener that routes messages to the appropriate handlers.
* Also handles user script messages from chrome.runtime.onUserScriptMessage.
*
* Benefits:
* - Single global listener instead of multiple independent listeners
* - Automatic cleanup when sandboxes are destroyed
* - Support for bidirectional communication (providers) and broadcasting (consumers)
* - Works with both sandbox iframes and user scripts
* - Clear lifecycle management
*/
export declare class RuntimeMessageRouter {
private sandboxes;
private messageListener;
private userScriptMessageListener;
/**
* Register a new sandbox with its runtime providers.
* Call this BEFORE creating the iframe (for sandbox contexts) or executing user script.
*/
registerSandbox(sandboxId: string, providers: SandboxRuntimeProvider[], consumers: MessageConsumer[]): void;
/**
* Update the iframe reference for a sandbox.
* Call this AFTER creating the iframe.
* This is needed so providers can send responses back to the sandbox.
*/
setSandboxIframe(sandboxId: string, iframe: HTMLIFrameElement): void;
/**
* Unregister a sandbox and remove all its consumers.
* Call this when the sandbox is destroyed.
*/
unregisterSandbox(sandboxId: string): void;
/**
* Add a message consumer for a sandbox.
* Consumers receive broadcast messages (console, execution-complete, etc.)
*/
addConsumer(sandboxId: string, consumer: MessageConsumer): void;
/**
* Remove a message consumer from a sandbox.
*/
removeConsumer(sandboxId: string, consumer: MessageConsumer): void;
/**
* Setup the global message listeners (called automatically)
*/
private setupListener;
}
/**
* Global singleton instance.
* Import this from wherever you need to interact with the message router.
*/
export declare const RUNTIME_MESSAGE_ROUTER: RuntimeMessageRouter;
//# sourceMappingURL=RuntimeMessageRouter.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"RuntimeMessageRouter.d.ts","sourceRoot":"","sources":["../../../src/components/sandbox/RuntimeMessageRouter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAK1E;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAYD;;;;;;;;;;;;;GAaG;AACH,qBAAa,oBAAoB;IAChC,OAAO,CAAC,SAAS,CAAqC;IACtD,OAAO,CAAC,eAAe,CAA4C;IACnE,OAAO,CAAC,yBAAyB,CAElB;IAEf;;;OAGG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,sBAAsB,EAAE,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,IAAI;IAY3G;;;;OAIG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAOpE;;;OAGG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAmB1C;;;OAGG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI;IAO/D;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI;IAOlE;;OAEG;IACH,OAAO,CAAC,aAAa;CAuFrB;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,sBAA6B,CAAC"}

View File

@@ -0,0 +1,168 @@
/**
* Centralized message router for all runtime communication.
*
* This singleton replaces all individual window.addEventListener("message") calls
* with a single global listener that routes messages to the appropriate handlers.
* Also handles user script messages from chrome.runtime.onUserScriptMessage.
*
* Benefits:
* - Single global listener instead of multiple independent listeners
* - Automatic cleanup when sandboxes are destroyed
* - Support for bidirectional communication (providers) and broadcasting (consumers)
* - Works with both sandbox iframes and user scripts
* - Clear lifecycle management
*/
export class RuntimeMessageRouter {
constructor() {
this.sandboxes = new Map();
this.messageListener = null;
this.userScriptMessageListener = null;
}
/**
* Register a new sandbox with its runtime providers.
* Call this BEFORE creating the iframe (for sandbox contexts) or executing user script.
*/
registerSandbox(sandboxId, providers, consumers) {
this.sandboxes.set(sandboxId, {
sandboxId,
iframe: null, // Will be set via setSandboxIframe() for sandbox contexts
providers,
consumers: new Set(consumers),
});
// Setup global listener if not already done
this.setupListener();
}
/**
* Update the iframe reference for a sandbox.
* Call this AFTER creating the iframe.
* This is needed so providers can send responses back to the sandbox.
*/
setSandboxIframe(sandboxId, iframe) {
const context = this.sandboxes.get(sandboxId);
if (context) {
context.iframe = iframe;
}
}
/**
* Unregister a sandbox and remove all its consumers.
* Call this when the sandbox is destroyed.
*/
unregisterSandbox(sandboxId) {
this.sandboxes.delete(sandboxId);
// If no more sandboxes, remove global listeners
if (this.sandboxes.size === 0) {
// Remove iframe listener
if (this.messageListener) {
window.removeEventListener("message", this.messageListener);
this.messageListener = null;
}
// Remove user script listener
if (this.userScriptMessageListener && typeof chrome !== "undefined" && chrome.runtime?.onUserScriptMessage) {
chrome.runtime.onUserScriptMessage.removeListener(this.userScriptMessageListener);
this.userScriptMessageListener = null;
}
}
}
/**
* Add a message consumer for a sandbox.
* Consumers receive broadcast messages (console, execution-complete, etc.)
*/
addConsumer(sandboxId, consumer) {
const context = this.sandboxes.get(sandboxId);
if (context) {
context.consumers.add(consumer);
}
}
/**
* Remove a message consumer from a sandbox.
*/
removeConsumer(sandboxId, consumer) {
const context = this.sandboxes.get(sandboxId);
if (context) {
context.consumers.delete(consumer);
}
}
/**
* Setup the global message listeners (called automatically)
*/
setupListener() {
// Setup sandbox iframe listener
if (!this.messageListener) {
this.messageListener = async (e) => {
const { sandboxId, messageId } = e.data;
if (!sandboxId)
return;
const context = this.sandboxes.get(sandboxId);
if (!context) {
return;
}
// Create respond() function for bidirectional communication
const respond = (response) => {
context.iframe?.contentWindow?.postMessage({
type: "runtime-response",
messageId,
sandboxId,
...response,
}, "*");
};
// 1. Try provider handlers first (for bidirectional comm)
for (const provider of context.providers) {
if (provider.handleMessage) {
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)
for (const consumer of context.consumers) {
await consumer.handleMessage(e.data);
// Don't stop - let all consumers see the message
}
};
window.addEventListener("message", this.messageListener);
}
// Setup user script message listener
if (!this.userScriptMessageListener) {
// Guard: check if we're in extension context
if (typeof chrome === "undefined" || !chrome.runtime?.onUserScriptMessage) {
return;
}
this.userScriptMessageListener = (message, _sender, sendResponse) => {
const { sandboxId } = message;
if (!sandboxId)
return false;
const context = this.sandboxes.get(sandboxId);
if (!context)
return false;
const respond = (response) => {
sendResponse({
...response,
sandboxId,
});
};
// Route to providers (async)
(async () => {
// 1. Try provider handlers first (for bidirectional comm)
for (const provider of context.providers) {
if (provider.handleMessage) {
await provider.handleMessage(message, respond);
// Don't stop - let consumers also handle the message
}
}
// 2. Broadcast to consumers (one-way messages or lifecycle events)
for (const consumer of context.consumers) {
await consumer.handleMessage(message);
// Don't stop - let all consumers see the message
}
})();
return true; // Indicates async response
};
chrome.runtime.onUserScriptMessage.addListener(this.userScriptMessageListener);
}
}
}
/**
* Global singleton instance.
* Import this from wherever you need to interact with the message router.
*/
export const RUNTIME_MESSAGE_ROUTER = new RuntimeMessageRouter();
//# sourceMappingURL=RuntimeMessageRouter.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"RuntimeMessageRouter.js","sourceRoot":"","sources":["../../../src/components/sandbox/RuntimeMessageRouter.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,48 @@
/**
* Interface for providing runtime capabilities to sandboxed iframes.
* Each provider injects data and runtime functions into the sandbox context.
*/
export interface SandboxRuntimeProvider {
/**
* Returns data to inject into window scope.
* Keys become window properties (e.g., { attachments: [...] } -> window.attachments)
*/
getData(): Record<string, any>;
/**
* Returns a runtime function that will be stringified and executed in the sandbox.
* The function receives sandboxId and has access to data from getData() via window.
*
* IMPORTANT: This function will be converted to string via .toString() and injected
* into the sandbox, so it cannot reference external variables or imports.
*/
getRuntime(): (sandboxId: string) => void;
/**
* Optional message handler for bidirectional communication.
* All providers receive all messages - decide internally what to handle.
*
* @param message - The message from the sandbox
* @param respond - Function to send a response back to the sandbox
*/
handleMessage?(message: any, respond: (response: any) => void): Promise<void>;
/**
* Optional documentation describing what globals/functions this provider injects.
* This will be appended to tool descriptions dynamically so the LLM knows what's available.
*/
getDescription(): string;
/**
* Optional lifecycle callback invoked when sandbox execution starts.
* Providers can use this to track abort signals for cancellation of async operations.
*
* @param sandboxId - The unique identifier for this sandbox execution
* @param signal - Optional AbortSignal that will be triggered if execution is cancelled
*/
onExecutionStart?(sandboxId: string, signal?: AbortSignal): void;
/**
* Optional lifecycle callback invoked when sandbox execution ends (success, error, or abort).
* Providers can use this to clean up any resources associated with the sandbox.
*
* @param sandboxId - The unique identifier for this sandbox execution
*/
onExecutionEnd?(sandboxId: string): void;
}
//# sourceMappingURL=SandboxRuntimeProvider.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SandboxRuntimeProvider.d.ts","sourceRoot":"","sources":["../../../src/components/sandbox/SandboxRuntimeProvider.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACtC;;;OAGG;IACH,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAE/B;;;;;;OAMG;IACH,UAAU,IAAI,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAE1C;;;;;;OAMG;IACH,aAAa,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9E;;;OAGG;IACH,cAAc,IAAI,MAAM,CAAC;IAEzB;;;;;;OAMG;IACH,gBAAgB,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAEjE;;;;;OAKG;IACH,cAAc,CAAC,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC"}

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=SandboxRuntimeProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"SandboxRuntimeProvider.js","sourceRoot":"","sources":["../../../src/components/sandbox/SandboxRuntimeProvider.ts"],"names":[],"mappings":""}