@oh-my-pi/pi-web-ui 1.337.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +609 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package.json +24 -0
- package/example/src/app.css +1 -0
- package/example/src/custom-messages.ts +99 -0
- package/example/src/main.ts +420 -0
- package/example/tsconfig.json +23 -0
- package/example/vite.config.ts +6 -0
- package/package.json +57 -0
- package/scripts/count-prompt-tokens.ts +88 -0
- package/src/ChatPanel.ts +218 -0
- package/src/app.css +68 -0
- package/src/components/AgentInterface.ts +390 -0
- package/src/components/AttachmentTile.ts +107 -0
- package/src/components/ConsoleBlock.ts +74 -0
- package/src/components/CustomProviderCard.ts +96 -0
- package/src/components/ExpandableSection.ts +46 -0
- package/src/components/Input.ts +113 -0
- package/src/components/MessageEditor.ts +404 -0
- package/src/components/MessageList.ts +97 -0
- package/src/components/Messages.ts +384 -0
- package/src/components/ProviderKeyInput.ts +152 -0
- package/src/components/SandboxedIframe.ts +626 -0
- package/src/components/StreamingMessageContainer.ts +107 -0
- package/src/components/ThinkingBlock.ts +45 -0
- package/src/components/message-renderer-registry.ts +28 -0
- package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
- package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/src/dialogs/AttachmentOverlay.ts +640 -0
- package/src/dialogs/CustomProviderDialog.ts +274 -0
- package/src/dialogs/ModelSelector.ts +314 -0
- package/src/dialogs/PersistentStorageDialog.ts +146 -0
- package/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/src/dialogs/SessionListDialog.ts +157 -0
- package/src/dialogs/SettingsDialog.ts +216 -0
- package/src/index.ts +115 -0
- package/src/prompts/prompts.ts +282 -0
- package/src/storage/app-storage.ts +60 -0
- package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/src/storage/store.ts +33 -0
- package/src/storage/stores/custom-providers-store.ts +62 -0
- package/src/storage/stores/provider-keys-store.ts +33 -0
- package/src/storage/stores/sessions-store.ts +136 -0
- package/src/storage/stores/settings-store.ts +34 -0
- package/src/storage/types.ts +206 -0
- package/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/src/tools/artifacts/Console.ts +102 -0
- package/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/src/tools/artifacts/GenericArtifact.ts +118 -0
- package/src/tools/artifacts/HtmlArtifact.ts +203 -0
- package/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
- package/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/src/tools/artifacts/SvgArtifact.ts +82 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
- package/src/tools/artifacts/artifacts.ts +713 -0
- package/src/tools/artifacts/index.ts +7 -0
- package/src/tools/extract-document.ts +271 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/javascript-repl.ts +316 -0
- package/src/tools/renderer-registry.ts +127 -0
- package/src/tools/renderers/BashRenderer.ts +52 -0
- package/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/src/tools/renderers/DefaultRenderer.ts +95 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/src/tools/types.ts +15 -0
- package/src/utils/attachment-utils.ts +472 -0
- package/src/utils/auth-token.ts +22 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/i18n.ts +653 -0
- package/src/utils/model-discovery.ts +277 -0
- package/src/utils/proxy-utils.ts +134 -0
- package/src/utils/test-sessions.ts +2357 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { html, LitElement } from "lit";
|
|
4
|
+
import { property, state } from "lit/decorators.js";
|
|
5
|
+
|
|
6
|
+
export class StreamingMessageContainer extends LitElement {
|
|
7
|
+
@property({ type: Array }) tools: AgentTool[] = [];
|
|
8
|
+
@property({ type: Boolean }) isStreaming = false;
|
|
9
|
+
@property({ type: Object }) pendingToolCalls?: Set<string>;
|
|
10
|
+
@property({ type: Object }) toolResultsById?: Map<string, ToolResultMessage>;
|
|
11
|
+
@property({ attribute: false }) onCostClick?: () => void;
|
|
12
|
+
|
|
13
|
+
@state() private _message: AgentMessage | null = null;
|
|
14
|
+
private _pendingMessage: AgentMessage | null = null;
|
|
15
|
+
private _updateScheduled = false;
|
|
16
|
+
private _immediateUpdate = false;
|
|
17
|
+
|
|
18
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override connectedCallback(): void {
|
|
23
|
+
super.connectedCallback();
|
|
24
|
+
this.style.display = "block";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Public method to update the message with batching for performance
|
|
28
|
+
public setMessage(message: AgentMessage | null, immediate = false) {
|
|
29
|
+
// Store the latest message
|
|
30
|
+
this._pendingMessage = message;
|
|
31
|
+
|
|
32
|
+
// If this is an immediate update (like clearing), apply it right away
|
|
33
|
+
if (immediate || message === null) {
|
|
34
|
+
this._immediateUpdate = true;
|
|
35
|
+
this._message = message;
|
|
36
|
+
this.requestUpdate();
|
|
37
|
+
// Cancel any pending updates since we're clearing
|
|
38
|
+
this._pendingMessage = null;
|
|
39
|
+
this._updateScheduled = false;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Otherwise batch updates for performance during streaming
|
|
44
|
+
if (!this._updateScheduled) {
|
|
45
|
+
this._updateScheduled = true;
|
|
46
|
+
|
|
47
|
+
requestAnimationFrame(async () => {
|
|
48
|
+
// Only apply the update if we haven't been cleared
|
|
49
|
+
if (!this._immediateUpdate && this._pendingMessage !== null) {
|
|
50
|
+
// Deep clone the message to ensure Lit detects changes in nested properties
|
|
51
|
+
// (like toolCall.arguments being mutated during streaming)
|
|
52
|
+
this._message = JSON.parse(JSON.stringify(this._pendingMessage));
|
|
53
|
+
this.requestUpdate();
|
|
54
|
+
}
|
|
55
|
+
// Reset for next batch
|
|
56
|
+
this._pendingMessage = null;
|
|
57
|
+
this._updateScheduled = false;
|
|
58
|
+
this._immediateUpdate = false;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override render() {
|
|
64
|
+
// Show loading indicator if loading but no message yet
|
|
65
|
+
if (!this._message) {
|
|
66
|
+
if (this.isStreaming)
|
|
67
|
+
return html`<div class="flex flex-col gap-3 mb-3">
|
|
68
|
+
<span class="mx-4 inline-block w-2 h-4 bg-muted-foreground animate-pulse"></span>
|
|
69
|
+
</div>`;
|
|
70
|
+
return html``; // Empty until a message is set
|
|
71
|
+
}
|
|
72
|
+
const msg = this._message;
|
|
73
|
+
|
|
74
|
+
if (msg.role === "toolResult") {
|
|
75
|
+
// Skip standalone tool result in streaming; the stable list will render paired tool-message
|
|
76
|
+
return html``;
|
|
77
|
+
} else if (msg.role === "user" || msg.role === "user-with-attachments") {
|
|
78
|
+
// Skip standalone tool result in streaming; the stable list will render it immediiately
|
|
79
|
+
return html``;
|
|
80
|
+
} else if (msg.role === "assistant") {
|
|
81
|
+
// Assistant message - render inline tool messages during streaming
|
|
82
|
+
return html`
|
|
83
|
+
<div class="flex flex-col gap-3 mb-3">
|
|
84
|
+
<assistant-message
|
|
85
|
+
.message=${msg}
|
|
86
|
+
.tools=${this.tools}
|
|
87
|
+
.isStreaming=${this.isStreaming}
|
|
88
|
+
.pendingToolCalls=${this.pendingToolCalls}
|
|
89
|
+
.toolResultsById=${this.toolResultsById}
|
|
90
|
+
.hideToolCalls=${false}
|
|
91
|
+
.onCostClick=${this.onCostClick}
|
|
92
|
+
></assistant-message>
|
|
93
|
+
${
|
|
94
|
+
this.isStreaming
|
|
95
|
+
? html`<span class="mx-4 inline-block w-2 h-4 bg-muted-foreground animate-pulse"></span>`
|
|
96
|
+
: ""
|
|
97
|
+
}
|
|
98
|
+
</div>
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Register custom element
|
|
105
|
+
if (!customElements.get("streaming-message-container")) {
|
|
106
|
+
customElements.define("streaming-message-container", StreamingMessageContainer);
|
|
107
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { icon } from "@mariozechner/mini-lit";
|
|
2
|
+
import { html, LitElement } from "lit";
|
|
3
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
4
|
+
import { ChevronRight } from "lucide";
|
|
5
|
+
|
|
6
|
+
@customElement("thinking-block")
|
|
7
|
+
export class ThinkingBlock extends LitElement {
|
|
8
|
+
@property() content!: string;
|
|
9
|
+
@property({ type: Boolean }) isStreaming = false;
|
|
10
|
+
@state() private isExpanded = false;
|
|
11
|
+
|
|
12
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override connectedCallback(): void {
|
|
17
|
+
super.connectedCallback();
|
|
18
|
+
this.style.display = "block";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private toggleExpanded() {
|
|
22
|
+
this.isExpanded = !this.isExpanded;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override render() {
|
|
26
|
+
const shimmerClasses = this.isStreaming
|
|
27
|
+
? "animate-shimmer bg-gradient-to-r from-muted-foreground via-foreground to-muted-foreground bg-[length:200%_100%] bg-clip-text text-transparent"
|
|
28
|
+
: "";
|
|
29
|
+
|
|
30
|
+
return html`
|
|
31
|
+
<div class="thinking-block">
|
|
32
|
+
<div
|
|
33
|
+
class="thinking-header cursor-pointer select-none flex items-center gap-2 py-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
34
|
+
@click=${this.toggleExpanded}
|
|
35
|
+
>
|
|
36
|
+
<span class="transition-transform inline-block ${this.isExpanded ? "rotate-90" : ""}"
|
|
37
|
+
>${icon(ChevronRight, "sm")}</span
|
|
38
|
+
>
|
|
39
|
+
<span class="${shimmerClasses}">Thinking...</span>
|
|
40
|
+
</div>
|
|
41
|
+
${this.isExpanded ? html`<markdown-block .content=${this.content} .isThinking=${true}></markdown-block>` : ""}
|
|
42
|
+
</div>
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { TemplateResult } from "lit";
|
|
3
|
+
|
|
4
|
+
// Extract role type from AppMessage union
|
|
5
|
+
export type MessageRole = AgentMessage["role"];
|
|
6
|
+
|
|
7
|
+
// Generic message renderer typed to specific message type
|
|
8
|
+
export interface MessageRenderer<TMessage extends AgentMessage = AgentMessage> {
|
|
9
|
+
render(message: TMessage): TemplateResult;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Registry of custom message renderers by role
|
|
13
|
+
const messageRenderers = new Map<MessageRole, MessageRenderer<any>>();
|
|
14
|
+
|
|
15
|
+
export function registerMessageRenderer<TRole extends MessageRole>(
|
|
16
|
+
role: TRole,
|
|
17
|
+
renderer: MessageRenderer<Extract<AgentMessage, { role: TRole }>>,
|
|
18
|
+
): void {
|
|
19
|
+
messageRenderers.set(role, renderer);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getMessageRenderer(role: MessageRole): MessageRenderer | undefined {
|
|
23
|
+
return messageRenderers.get(role);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderMessage(message: AgentMessage): TemplateResult | undefined {
|
|
27
|
+
return messageRenderers.get(message.role)?.render(message);
|
|
28
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
|
|
3
|
+
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW,
|
|
4
|
+
} from "../../prompts/prompts.js";
|
|
5
|
+
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
|
|
6
|
+
|
|
7
|
+
// Define minimal interface for ArtifactsPanel to avoid circular dependencies
|
|
8
|
+
interface ArtifactsPanelLike {
|
|
9
|
+
artifacts: Map<string, { content: string }>;
|
|
10
|
+
tool: {
|
|
11
|
+
execute(toolCallId: string, args: { command: string; filename: string; content?: string }): Promise<any>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AgentLike {
|
|
16
|
+
appendMessage(message: any): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Artifacts Runtime Provider
|
|
21
|
+
*
|
|
22
|
+
* Provides programmatic access to session artifacts from sandboxed code.
|
|
23
|
+
* Allows code to create, read, update, and delete artifacts dynamically.
|
|
24
|
+
* Supports both online (extension) and offline (downloaded HTML) modes.
|
|
25
|
+
*/
|
|
26
|
+
export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider {
|
|
27
|
+
constructor(
|
|
28
|
+
private artifactsPanel: ArtifactsPanelLike,
|
|
29
|
+
private agent?: AgentLike,
|
|
30
|
+
private readWrite: boolean = true,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
getData(): Record<string, any> {
|
|
34
|
+
// Inject artifact snapshot for offline mode
|
|
35
|
+
const snapshot: Record<string, string> = {};
|
|
36
|
+
this.artifactsPanel.artifacts.forEach((artifact, filename) => {
|
|
37
|
+
snapshot[filename] = artifact.content;
|
|
38
|
+
});
|
|
39
|
+
return { artifacts: snapshot };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getRuntime(): (sandboxId: string) => void {
|
|
43
|
+
// This function will be stringified, so no external references!
|
|
44
|
+
return (_sandboxId: string) => {
|
|
45
|
+
// Auto-parse/stringify for .json files
|
|
46
|
+
const isJsonFile = (filename: string) => filename.endsWith(".json");
|
|
47
|
+
|
|
48
|
+
(window as any).listArtifacts = async (): Promise<string[]> => {
|
|
49
|
+
// Online: ask extension
|
|
50
|
+
if ((window as any).sendRuntimeMessage) {
|
|
51
|
+
const response = await (window as any).sendRuntimeMessage({
|
|
52
|
+
type: "artifact-operation",
|
|
53
|
+
action: "list",
|
|
54
|
+
});
|
|
55
|
+
if (!response.success) throw new Error(response.error);
|
|
56
|
+
return response.result;
|
|
57
|
+
}
|
|
58
|
+
// Offline: return snapshot keys
|
|
59
|
+
else {
|
|
60
|
+
return Object.keys((window as any).artifacts || {});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
(window as any).getArtifact = async (filename: string): Promise<any> => {
|
|
65
|
+
let content: string;
|
|
66
|
+
|
|
67
|
+
// Online: ask extension
|
|
68
|
+
if ((window as any).sendRuntimeMessage) {
|
|
69
|
+
const response = await (window as any).sendRuntimeMessage({
|
|
70
|
+
type: "artifact-operation",
|
|
71
|
+
action: "get",
|
|
72
|
+
filename,
|
|
73
|
+
});
|
|
74
|
+
if (!response.success) throw new Error(response.error);
|
|
75
|
+
content = response.result;
|
|
76
|
+
}
|
|
77
|
+
// Offline: read snapshot
|
|
78
|
+
else {
|
|
79
|
+
if (!(window as any).artifacts?.[filename]) {
|
|
80
|
+
throw new Error(`Artifact not found (offline mode): ${filename}`);
|
|
81
|
+
}
|
|
82
|
+
content = (window as any).artifacts[filename];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Auto-parse .json files
|
|
86
|
+
if (isJsonFile(filename)) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(content);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
throw new Error(`Failed to parse JSON from ${filename}: ${e}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return content;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
(window as any).createOrUpdateArtifact = async (
|
|
97
|
+
filename: string,
|
|
98
|
+
content: any,
|
|
99
|
+
mimeType?: string,
|
|
100
|
+
): Promise<void> => {
|
|
101
|
+
if (!(window as any).sendRuntimeMessage) {
|
|
102
|
+
throw new Error("Cannot create/update artifacts in offline mode (read-only)");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let finalContent = content;
|
|
106
|
+
// Auto-stringify .json files
|
|
107
|
+
if (isJsonFile(filename) && typeof content !== "string") {
|
|
108
|
+
finalContent = JSON.stringify(content, null, 2);
|
|
109
|
+
} else if (typeof content !== "string") {
|
|
110
|
+
finalContent = JSON.stringify(content, null, 2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const response = await (window as any).sendRuntimeMessage({
|
|
114
|
+
type: "artifact-operation",
|
|
115
|
+
action: "createOrUpdate",
|
|
116
|
+
filename,
|
|
117
|
+
content: finalContent,
|
|
118
|
+
mimeType,
|
|
119
|
+
});
|
|
120
|
+
if (!response.success) throw new Error(response.error);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
(window as any).deleteArtifact = async (filename: string): Promise<void> => {
|
|
124
|
+
if (!(window as any).sendRuntimeMessage) {
|
|
125
|
+
throw new Error("Cannot delete artifacts in offline mode (read-only)");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const response = await (window as any).sendRuntimeMessage({
|
|
129
|
+
type: "artifact-operation",
|
|
130
|
+
action: "delete",
|
|
131
|
+
filename,
|
|
132
|
+
});
|
|
133
|
+
if (!response.success) throw new Error(response.error);
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async handleMessage(message: any, respond: (response: any) => void): Promise<void> {
|
|
139
|
+
if (message.type !== "artifact-operation") {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { action, filename, content } = message;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
switch (action) {
|
|
147
|
+
case "list": {
|
|
148
|
+
const filenames = Array.from(this.artifactsPanel.artifacts.keys());
|
|
149
|
+
respond({ success: true, result: filenames });
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case "get": {
|
|
154
|
+
const artifact = this.artifactsPanel.artifacts.get(filename);
|
|
155
|
+
if (!artifact) {
|
|
156
|
+
respond({ success: false, error: `Artifact not found: ${filename}` });
|
|
157
|
+
} else {
|
|
158
|
+
respond({ success: true, result: artifact.content });
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case "createOrUpdate": {
|
|
164
|
+
try {
|
|
165
|
+
const exists = this.artifactsPanel.artifacts.has(filename);
|
|
166
|
+
const command = exists ? "rewrite" : "create";
|
|
167
|
+
const action = exists ? "update" : "create";
|
|
168
|
+
|
|
169
|
+
await this.artifactsPanel.tool.execute("", {
|
|
170
|
+
command,
|
|
171
|
+
filename,
|
|
172
|
+
content,
|
|
173
|
+
});
|
|
174
|
+
this.agent?.appendMessage({
|
|
175
|
+
role: "artifact",
|
|
176
|
+
action,
|
|
177
|
+
filename,
|
|
178
|
+
content,
|
|
179
|
+
...(action === "create" && { title: filename }),
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
});
|
|
182
|
+
respond({ success: true });
|
|
183
|
+
} catch (err: any) {
|
|
184
|
+
respond({ success: false, error: err.message });
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case "delete": {
|
|
190
|
+
try {
|
|
191
|
+
await this.artifactsPanel.tool.execute("", {
|
|
192
|
+
command: "delete",
|
|
193
|
+
filename,
|
|
194
|
+
});
|
|
195
|
+
this.agent?.appendMessage({
|
|
196
|
+
role: "artifact",
|
|
197
|
+
action: "delete",
|
|
198
|
+
filename,
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
});
|
|
201
|
+
respond({ success: true });
|
|
202
|
+
} catch (err: any) {
|
|
203
|
+
respond({ success: false, error: err.message });
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
default:
|
|
209
|
+
respond({ success: false, error: `Unknown artifact action: ${action}` });
|
|
210
|
+
}
|
|
211
|
+
} catch (error: any) {
|
|
212
|
+
respond({ success: false, error: error.message });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getDescription(): string {
|
|
217
|
+
return this.readWrite ? ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW : ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ATTACHMENTS_RUNTIME_DESCRIPTION } from "../../prompts/prompts.js";
|
|
2
|
+
import type { Attachment } from "../../utils/attachment-utils.js";
|
|
3
|
+
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Attachments Runtime Provider
|
|
7
|
+
*
|
|
8
|
+
* OPTIONAL provider that provides file access APIs to sandboxed code.
|
|
9
|
+
* Only needed when attachments are present.
|
|
10
|
+
* Attachments are read-only snapshot data - no messaging needed.
|
|
11
|
+
*/
|
|
12
|
+
export class AttachmentsRuntimeProvider implements SandboxRuntimeProvider {
|
|
13
|
+
constructor(private attachments: Attachment[]) {}
|
|
14
|
+
|
|
15
|
+
getData(): Record<string, any> {
|
|
16
|
+
const attachmentsData = this.attachments.map((a) => ({
|
|
17
|
+
id: a.id,
|
|
18
|
+
fileName: a.fileName,
|
|
19
|
+
mimeType: a.mimeType,
|
|
20
|
+
size: a.size,
|
|
21
|
+
content: a.content,
|
|
22
|
+
extractedText: a.extractedText,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
return { attachments: attachmentsData };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getRuntime(): (sandboxId: string) => void {
|
|
29
|
+
// This function will be stringified, so no external references!
|
|
30
|
+
// These functions read directly from window.attachments
|
|
31
|
+
// Works both online AND offline (no messaging needed!)
|
|
32
|
+
return (_sandboxId: string) => {
|
|
33
|
+
(window as any).listAttachments = () =>
|
|
34
|
+
((window as any).attachments || []).map((a: any) => ({
|
|
35
|
+
id: a.id,
|
|
36
|
+
fileName: a.fileName,
|
|
37
|
+
mimeType: a.mimeType,
|
|
38
|
+
size: a.size,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
(window as any).readTextAttachment = (attachmentId: string) => {
|
|
42
|
+
const a = ((window as any).attachments || []).find((x: any) => x.id === attachmentId);
|
|
43
|
+
if (!a) throw new Error(`Attachment not found: ${attachmentId}`);
|
|
44
|
+
if (a.extractedText) return a.extractedText;
|
|
45
|
+
try {
|
|
46
|
+
return atob(a.content);
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Error(`Failed to decode text content for: ${attachmentId}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
(window as any).readBinaryAttachment = (attachmentId: string) => {
|
|
53
|
+
const a = ((window as any).attachments || []).find((x: any) => x.id === attachmentId);
|
|
54
|
+
if (!a) throw new Error(`Attachment not found: ${attachmentId}`);
|
|
55
|
+
const bin = atob(a.content);
|
|
56
|
+
const bytes = new Uint8Array(bin.length);
|
|
57
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
58
|
+
return bytes;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getDescription(): string {
|
|
64
|
+
return ATTACHMENTS_RUNTIME_DESCRIPTION;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
|
|
2
|
+
|
|
3
|
+
export interface ConsoleLog {
|
|
4
|
+
type: "log" | "warn" | "error" | "info";
|
|
5
|
+
text: string;
|
|
6
|
+
args?: unknown[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Console Runtime Provider
|
|
11
|
+
*
|
|
12
|
+
* REQUIRED provider that should always be included first.
|
|
13
|
+
* Provides console capture, error handling, and execution lifecycle management.
|
|
14
|
+
* Collects console output for retrieval by caller.
|
|
15
|
+
*/
|
|
16
|
+
export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
|
|
17
|
+
private logs: ConsoleLog[] = [];
|
|
18
|
+
private completionError: { message: string; stack: string } | null = null;
|
|
19
|
+
private completed = false;
|
|
20
|
+
|
|
21
|
+
getData(): Record<string, any> {
|
|
22
|
+
// No data needed
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getDescription(): string {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getRuntime(): (sandboxId: string) => void {
|
|
31
|
+
return (_sandboxId: string) => {
|
|
32
|
+
// Store truly original console methods on first wrap only
|
|
33
|
+
// This prevents accumulation of wrapper functions across multiple executions
|
|
34
|
+
if (!(window as any).__originalConsole) {
|
|
35
|
+
(window as any).__originalConsole = {
|
|
36
|
+
log: console.log.bind(console),
|
|
37
|
+
error: console.error.bind(console),
|
|
38
|
+
warn: console.warn.bind(console),
|
|
39
|
+
info: console.info.bind(console),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Always use the truly original console, not the current (possibly wrapped) one
|
|
44
|
+
const originalConsole = (window as any).__originalConsole;
|
|
45
|
+
|
|
46
|
+
// Track pending send promises to wait for them in onCompleted
|
|
47
|
+
const pendingSends: Promise<any>[] = [];
|
|
48
|
+
|
|
49
|
+
["log", "error", "warn", "info"].forEach((method) => {
|
|
50
|
+
(console as any)[method] = (...args: any[]) => {
|
|
51
|
+
const text = args
|
|
52
|
+
.map((arg) => {
|
|
53
|
+
try {
|
|
54
|
+
return typeof arg === "object" ? JSON.stringify(arg) : String(arg);
|
|
55
|
+
} catch {
|
|
56
|
+
return String(arg);
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.join(" ");
|
|
60
|
+
|
|
61
|
+
// Always log locally too (using truly original console)
|
|
62
|
+
(originalConsole as any)[method].apply(console, args);
|
|
63
|
+
|
|
64
|
+
// Send immediately and track the promise (only in extension context)
|
|
65
|
+
if ((window as any).sendRuntimeMessage) {
|
|
66
|
+
const sendPromise = (window as any)
|
|
67
|
+
.sendRuntimeMessage({
|
|
68
|
+
type: "console",
|
|
69
|
+
method,
|
|
70
|
+
text,
|
|
71
|
+
args,
|
|
72
|
+
})
|
|
73
|
+
.catch(() => {});
|
|
74
|
+
pendingSends.push(sendPromise);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Register completion callback to wait for all pending sends
|
|
80
|
+
if ((window as any).onCompleted) {
|
|
81
|
+
(window as any).onCompleted(async (_success: boolean) => {
|
|
82
|
+
// Wait for all pending console sends to complete
|
|
83
|
+
if (pendingSends.length > 0) {
|
|
84
|
+
await Promise.all(pendingSends);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Track errors for HTML artifacts
|
|
90
|
+
let lastError: { message: string; stack: string } | null = null;
|
|
91
|
+
|
|
92
|
+
// Error handlers - track errors but don't log them
|
|
93
|
+
// (they'll be shown via execution-error message)
|
|
94
|
+
window.addEventListener("error", (e) => {
|
|
95
|
+
const text = `${e.error?.stack || e.message || String(e)} at line ${e.lineno || "?"}:${e.colno || "?"}`;
|
|
96
|
+
|
|
97
|
+
lastError = {
|
|
98
|
+
message: e.error?.message || e.message || String(e),
|
|
99
|
+
stack: e.error?.stack || text,
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
104
|
+
const text = `Unhandled promise rejection: ${e.reason?.message || e.reason || "Unknown error"}`;
|
|
105
|
+
|
|
106
|
+
lastError = {
|
|
107
|
+
message: e.reason?.message || String(e.reason) || "Unhandled promise rejection",
|
|
108
|
+
stack: e.reason?.stack || text,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Expose complete() method for user code to call
|
|
113
|
+
let completionSent = false;
|
|
114
|
+
(window as any).complete = async (error?: { message: string; stack: string }, returnValue?: any) => {
|
|
115
|
+
if (completionSent) return;
|
|
116
|
+
completionSent = true;
|
|
117
|
+
|
|
118
|
+
const finalError = error || lastError;
|
|
119
|
+
|
|
120
|
+
if ((window as any).sendRuntimeMessage) {
|
|
121
|
+
if (finalError) {
|
|
122
|
+
await (window as any).sendRuntimeMessage({
|
|
123
|
+
type: "execution-error",
|
|
124
|
+
error: finalError,
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
await (window as any).sendRuntimeMessage({
|
|
128
|
+
type: "execution-complete",
|
|
129
|
+
returnValue,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async handleMessage(message: any, respond: (response: any) => void): Promise<void> {
|
|
138
|
+
if (message.type === "console") {
|
|
139
|
+
// Collect console output
|
|
140
|
+
this.logs.push({
|
|
141
|
+
type:
|
|
142
|
+
message.method === "error"
|
|
143
|
+
? "error"
|
|
144
|
+
: message.method === "warn"
|
|
145
|
+
? "warn"
|
|
146
|
+
: message.method === "info"
|
|
147
|
+
? "info"
|
|
148
|
+
: "log",
|
|
149
|
+
text: message.text,
|
|
150
|
+
args: message.args,
|
|
151
|
+
});
|
|
152
|
+
// Acknowledge receipt
|
|
153
|
+
respond({ success: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get collected console logs
|
|
159
|
+
*/
|
|
160
|
+
getLogs(): ConsoleLog[] {
|
|
161
|
+
return this.logs;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get completion status
|
|
166
|
+
*/
|
|
167
|
+
isCompleted(): boolean {
|
|
168
|
+
return this.completed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get completion error if any
|
|
173
|
+
*/
|
|
174
|
+
getCompletionError(): { message: string; stack: string } | null {
|
|
175
|
+
return this.completionError;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Reset state for reuse
|
|
180
|
+
*/
|
|
181
|
+
reset(): void {
|
|
182
|
+
this.logs = [];
|
|
183
|
+
this.completionError = null;
|
|
184
|
+
this.completed = false;
|
|
185
|
+
}
|
|
186
|
+
}
|