Updates to prompts

This commit is contained in:
Mario Zechner 2025-10-17 22:44:03 +02:00
parent ffc9be8867
commit 2a7ccf0fcb
15 changed files with 911 additions and 271 deletions

55
package-lock.json generated
View file

@ -2132,6 +2132,12 @@
"@types/node": "*"
}
},
"node_modules/@types/mime-types": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
"integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
"license": "MIT"
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
@ -3793,6 +3799,27 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@ -5222,7 +5249,8 @@
"version": "0.5.44",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-ai": "^0.5.44"
"@mariozechner/pi-ai": "^0.5.44",
"@mariozechner/pi-tui": "^0.5.44"
},
"devDependencies": {
"@types/node": "^24.3.0",
@ -5385,7 +5413,6 @@
"dependencies": {
"@mariozechner/pi-agent": "^0.5.44",
"@mariozechner/pi-ai": "^0.5.44",
"@mariozechner/pi-tui": "^0.5.44",
"chalk": "^5.5.0",
"glob": "^11.0.3"
},
@ -5435,7 +5462,7 @@
"version": "0.5.44",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-agent-old": "^0.5.44",
"@mariozechner/pi-agent": "^0.5.44",
"chalk": "^5.5.0"
},
"bin": {
@ -5492,10 +5519,6 @@
"node": ">=20.0.0"
}
},
"packages/tui/node_modules/@types/mime-types": {
"version": "2.1.4",
"license": "MIT"
},
"packages/tui/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
@ -5518,29 +5541,13 @@
"node": ">= 18"
}
},
"packages/tui/node_modules/mime-db": {
"version": "1.54.0",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"packages/tui/node_modules/mime-types": {
"version": "3.0.1",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"packages/web-ui": {
"name": "@mariozechner/pi-web-ui",
"version": "0.5.44",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-ai": "^0.5.43",
"@mariozechner/pi-tui": "^0.5.44",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lit": "^3.3.1",

View file

@ -18,7 +18,8 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@mariozechner/pi-ai": "^0.5.44"
"@mariozechner/pi-ai": "^0.5.44",
"@mariozechner/pi-tui": "^0.5.44"
},
"keywords": [
"ai",

View file

@ -0,0 +1,170 @@
# Debug Mode Guide
## Enabling Debug Output
Debug logs are written to files in `/tmp/` to avoid interfering with TUI rendering.
There are three ways to enable debug output:
1. **CLI flag**: `--debug` or `-d`
```bash
coding-agent --debug --script "Hello"
```
This will print log file locations:
```
[TUI] Debug logging to: /tmp/tui-debug-1234567890.log
[RENDERER] Debug logging to: /tmp/agent-debug-1234567890.log
```
2. **Environment variables**:
```bash
TUI_DEBUG=1 AGENT_DEBUG=1 coding-agent
```
3. **Individual components**:
```bash
TUI_DEBUG=1 coding-agent # Only TUI debug
AGENT_DEBUG=1 coding-agent # Only agent/renderer debug
```
## Viewing Debug Logs
Debug logs are written to `/tmp/` with timestamps:
- `/tmp/tui-debug-<timestamp>.log` - TUI rendering events
- `/tmp/agent-debug-<timestamp>.log` - Agent/renderer events
To tail the logs while the agent runs:
```bash
# In one terminal
coding-agent --debug --script "Hello"
# In another terminal (use the path printed above)
tail -f /tmp/tui-debug-*.log
tail -f /tmp/agent-debug-*.log
```
## Scripted Messages for Testing
Use `--script` to replay messages automatically in interactive mode:
```bash
# Single scripted message
coding-agent --debug --script "What files are in this directory?"
# Multiple scripted messages
coding-agent --debug --script "Hello" --script "List the files" --script "Read package.json"
```
The agent will:
1. Type the message into the editor
2. Submit it
3. Wait for the agent to complete its response
4. Move to the next message
5. Exit after all messages are processed
## Debug Output Reference
### TUI Debug Messages
**`[TUI DEBUG]`** - Low-level terminal UI rendering events
- **`requestRender() called but TUI not started`** - Render requested before TUI initialization (usually benign)
- **`Render queued`** - A render has been scheduled for next tick
- **`Executing queued render`** - About to perform the actual render
- **`renderToScreen() called: resize=X, termWidth=Y, termHeight=Z`** - Starting render cycle
- **`Reset for resize`** - Terminal was resized, clearing buffers
- **`Collected N render commands, total lines: M`** - Gathered all component output (N components, M total lines)
- **`Performing initial render`** - First render (full screen write)
- **`Performing line-based render`** - Differential render (only changed lines)
- **`Render complete. Total renders: X, avg lines redrawn: Y`** - Render finished with performance stats
### Renderer Debug Messages
**`[RENDERER DEBUG]`** - Agent renderer (TuiRenderer) events
- **`handleStateUpdate: isStreaming=X, messages=N, pendingToolCalls=M`** - Agent state changed
- `isStreaming=true` - Agent is currently responding
- `messages=N` - Total messages in conversation
- `pendingToolCalls=M` - Number of tool calls waiting to execute
- **`Adding N new stable messages`** - N messages were finalized and added to chat history
- **`Streaming message role=X`** - Currently streaming a message with role X (user/assistant/toolResult)
- **`Starting loading animation`** - Spinner started because agent is thinking
- **`Creating streaming component`** - Creating UI component to show live message updates
- **`Streaming stopped`** - Agent finished responding
- **`Requesting render`** - Asking TUI to redraw the screen
- **`simulateInput: "text"`** - Scripted message being typed
- **`Triggering onInputCallback`** - Submitting the scripted message
### Script Debug Messages
**`[SCRIPT]`** - Scripted message playback
- **`Sending message N/M: text`** - Sending message N out of M total
- **`All N messages completed. Exiting.`** - Finished all scripted messages
**`[AGENT]`** - Agent execution
- **`Completed response to: "text"`** - Agent finished processing this message
## Interpreting Debug Output
### Normal Message Flow
```
[RENDERER DEBUG] handleStateUpdate: isStreaming=false, messages=0, pendingToolCalls=0
[SCRIPT] Sending message 1/1: Hello
[RENDERER DEBUG] simulateInput: "Hello"
[RENDERER DEBUG] Triggering onInputCallback
[RENDERER DEBUG] handleStateUpdate: isStreaming=true, messages=1, pendingToolCalls=0
[RENDERER DEBUG] Streaming message role=user
[RENDERER DEBUG] Starting loading animation
[RENDERER DEBUG] Requesting render
[TUI DEBUG] Render queued
[TUI DEBUG] Executing queued render
[TUI DEBUG] renderToScreen() called: resize=false, termWidth=120, termHeight=40
[TUI DEBUG] Collected 4 render commands, total lines: 8
[TUI DEBUG] Performing line-based render
[TUI DEBUG] Render complete. Total renders: 5, avg lines redrawn: 12.4
[RENDERER DEBUG] handleStateUpdate: isStreaming=true, messages=1, pendingToolCalls=0
[RENDERER DEBUG] Streaming message role=assistant
...
[RENDERER DEBUG] handleStateUpdate: isStreaming=false, messages=2, pendingToolCalls=0
[RENDERER DEBUG] Streaming stopped
[RENDERER DEBUG] Adding 1 new stable messages
[AGENT] Completed response to: "Hello"
```
### What to Look For
**Rendering Issues:**
- If `Render queued` appears but no `Executing queued render` → render loop broken
- If `total lines` is 0 or unexpectedly small → components not rendering
- If `avg lines redrawn` is huge → too many full redraws (performance issue)
- If no `[TUI DEBUG]` messages → TUI debug not enabled or TUI not starting
**Message Flow Issues:**
- If messages increase but no "Adding N new stable messages" → renderer not detecting changes
- If `isStreaming=true` never becomes `false` → agent hanging
- If `pendingToolCalls` stays > 0 → tool execution stuck
- If `Streaming stopped` never appears → streaming never completes
**Scripted Message Issues:**
- If `simulateInput` appears but no `Triggering onInputCallback` → callback not registered yet
- If `Sending message` appears but no `Completed response` → agent not responding
- If no `[SCRIPT]` messages → script messages not being processed
## Example Debug Session
```bash
# Test basic rendering with a simple scripted message
coding-agent --debug --script "Hello"
# Test multi-turn conversation
coding-agent --debug --script "Hi" --script "What files are here?" --script "Thanks"
# Test tool execution
coding-agent --debug --script "List all TypeScript files"
```
Look for the flow: script → simulateInput → handleStateUpdate → render → completed

View file

@ -22,7 +22,6 @@
"dependencies": {
"@mariozechner/pi-agent": "^0.5.44",
"@mariozechner/pi-ai": "^0.5.44",
"@mariozechner/pi-tui": "^0.5.44",
"chalk": "^5.5.0",
"glob": "^11.0.3"
},

View file

@ -3,6 +3,7 @@ import { getModel } from "@mariozechner/pi-ai";
import chalk from "chalk";
import { SessionManager } from "./session-manager.js";
import { codingTools } from "./tools/index.js";
import { TuiRenderer } from "./tui-renderer.js";
interface Args {
provider?: string;
@ -57,6 +58,9 @@ ${chalk.bold("Options:")}
--help, -h Show this help
${chalk.bold("Examples:")}
# Interactive mode (no messages = interactive TUI)
coding-agent
# Single message
coding-agent "List all .ts files in src/"
@ -101,6 +105,57 @@ Guidelines:
Current directory: ${process.cwd()}`;
async function runInteractiveMode(agent: Agent, _sessionManager: SessionManager): Promise<void> {
const renderer = new TuiRenderer();
// Initialize TUI
await renderer.init();
// Set interrupt callback
renderer.setInterruptCallback(() => {
agent.abort();
});
// Subscribe to agent state updates
agent.subscribe(async (event) => {
if (event.type === "state-update") {
await renderer.handleStateUpdate(event.state);
}
});
// Interactive loop
while (true) {
const userInput = await renderer.getUserInput();
// Process the message - agent.prompt will add user message and trigger state updates
try {
await agent.prompt(userInput);
} catch (error: any) {
// Error handling - errors should be in agent state
console.error("Error:", error.message);
}
}
}
async function runSingleShotMode(agent: Agent, sessionManager: SessionManager, messages: string[]): Promise<void> {
for (const message of messages) {
console.log(chalk.blue(`\n> ${message}\n`));
await agent.prompt(message);
// Print response
const lastMessage = agent.state.messages[agent.state.messages.length - 1];
if (lastMessage.role === "assistant") {
for (const content of lastMessage.content) {
if (content.type === "text") {
console.log(content.text);
}
}
}
}
console.log(chalk.dim(`\nSession saved to: ${sessionManager.getSessionFile()}`));
}
export async function main(args: string[]) {
const parsed = parseArgs(args);
@ -183,27 +238,12 @@ export async function main(args: string[]) {
sessionManager.saveEvent(event);
});
// Process messages
if (parsed.messages.length === 0) {
console.log(chalk.yellow("No messages provided. Use --help for usage information."));
console.log(chalk.dim(`Session saved to: ${sessionManager.getSessionFile()}`));
return;
// Determine mode: interactive if no messages provided
const isInteractive = parsed.messages.length === 0;
if (isInteractive) {
await runInteractiveMode(agent, sessionManager);
} else {
await runSingleShotMode(agent, sessionManager, parsed.messages);
}
for (const message of parsed.messages) {
console.log(chalk.blue(`\n> ${message}\n`));
await agent.prompt(message);
// Print response
const lastMessage = agent.state.messages[agent.state.messages.length - 1];
if (lastMessage.role === "assistant") {
for (const content of lastMessage.content) {
if (content.type === "text") {
console.log(content.text);
}
}
}
}
console.log(chalk.dim(`\nSession saved to: ${sessionManager.getSessionFile()}`));
}

View file

@ -0,0 +1,308 @@
import type { AgentState } from "@mariozechner/pi-agent";
import type { AssistantMessage, Message } from "@mariozechner/pi-ai";
import {
CombinedAutocompleteProvider,
Container,
LoadingAnimation,
MarkdownComponent,
TextComponent,
TextEditor,
TUI,
WhitespaceComponent,
} from "@mariozechner/pi-tui";
import chalk from "chalk";
/**
* Component that renders a streaming message with live updates
*/
class StreamingMessageComponent extends Container {
private textComponent: MarkdownComponent | null = null;
private toolCallsContainer: Container | null = null;
private currentContent = "";
private currentToolCalls: any[] = [];
updateContent(message: Message | null) {
if (!message) {
this.clear();
return;
}
if (message.role === "assistant") {
const assistantMsg = message as AssistantMessage;
// Update text content
const textContent = assistantMsg.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("");
if (textContent !== this.currentContent) {
this.currentContent = textContent;
if (this.textComponent) {
this.removeChild(this.textComponent);
}
if (textContent) {
this.textComponent = new MarkdownComponent(textContent);
this.addChild(this.textComponent);
}
}
// Update tool calls
const toolCalls = assistantMsg.content.filter((c) => c.type === "toolCall");
if (JSON.stringify(toolCalls) !== JSON.stringify(this.currentToolCalls)) {
this.currentToolCalls = toolCalls;
if (this.toolCallsContainer) {
this.removeChild(this.toolCallsContainer);
}
if (toolCalls.length > 0) {
this.toolCallsContainer = new Container();
for (const toolCall of toolCalls) {
const argsStr =
typeof toolCall.arguments === "string" ? toolCall.arguments : JSON.stringify(toolCall.arguments);
this.toolCallsContainer.addChild(
new TextComponent(chalk.yellow(`[tool] ${toolCall.name}(${argsStr})`)),
);
}
this.addChild(this.toolCallsContainer);
}
}
}
}
}
/**
* TUI renderer for the coding agent
*/
export class TuiRenderer {
private ui: TUI;
private chatContainer: Container;
private statusContainer: Container;
private editor: TextEditor;
private isInitialized = false;
private onInputCallback?: (text: string) => void;
private loadingAnimation: LoadingAnimation | null = null;
private onInterruptCallback?: () => void;
private lastSigintTime = 0;
// Message tracking
private lastStableMessageCount = 0;
private streamingComponent: StreamingMessageComponent | null = null;
constructor() {
this.ui = new TUI();
this.chatContainer = new Container();
this.statusContainer = new Container();
this.editor = new TextEditor();
// Setup autocomplete for file paths and slash commands
const autocompleteProvider = new CombinedAutocompleteProvider([], process.cwd());
this.editor.setAutocompleteProvider(autocompleteProvider);
}
async init(): Promise<void> {
if (this.isInitialized) return;
// Add header with instructions
const header = new TextComponent(
chalk.blueBright(">> coding-agent interactive <<") +
"\n" +
chalk.dim("Press Escape to interrupt while processing") +
"\n" +
chalk.dim("Press CTRL+C to clear the text editor") +
"\n" +
chalk.dim("Press CTRL+C twice quickly to exit"),
{ bottom: 1 },
);
// Setup UI layout
this.ui.addChild(header);
this.ui.addChild(this.chatContainer);
this.ui.addChild(this.statusContainer);
this.ui.addChild(new WhitespaceComponent(1));
this.ui.addChild(this.editor);
this.ui.setFocus(this.editor);
// Set up global key handler for Escape and Ctrl+C
this.ui.onGlobalKeyPress = (data: string): boolean => {
// Intercept Escape key when processing
if (data === "\x1b" && this.loadingAnimation) {
if (this.onInterruptCallback) {
this.onInterruptCallback();
}
return false;
}
// Handle Ctrl+C (raw mode sends \x03)
if (data === "\x03") {
const now = Date.now();
const timeSinceLastCtrlC = now - this.lastSigintTime;
if (timeSinceLastCtrlC < 500) {
// Second Ctrl+C within 500ms - exit
this.stop();
process.exit(0);
} else {
// First Ctrl+C - clear the editor
this.clearEditor();
this.lastSigintTime = now;
}
return false;
}
return true;
};
// Handle editor submission
this.editor.onSubmit = (text: string) => {
text = text.trim();
if (!text) return;
if (this.onInputCallback) {
this.onInputCallback(text);
}
};
// Start the UI
this.ui.start();
this.isInitialized = true;
}
async handleStateUpdate(state: AgentState): Promise<void> {
if (!this.isInitialized) {
await this.init();
}
// Count stable messages (exclude the streaming one if streaming)
const stableMessageCount = state.isStreaming ? state.messages.length - 1 : state.messages.length;
// Add any NEW stable messages
if (stableMessageCount > this.lastStableMessageCount) {
for (let i = this.lastStableMessageCount; i < stableMessageCount; i++) {
const message = state.messages[i];
this.addMessageToChat(message);
}
this.lastStableMessageCount = stableMessageCount;
}
// Handle streaming message
if (state.isStreaming) {
const streamingMessage = state.messages[state.messages.length - 1];
// Show loading animation if we just started streaming
if (!this.loadingAnimation) {
this.editor.disableSubmit = true;
this.statusContainer.clear();
this.loadingAnimation = new LoadingAnimation(this.ui);
this.statusContainer.addChild(this.loadingAnimation);
}
// Create or update streaming component
if (!this.streamingComponent) {
this.streamingComponent = new StreamingMessageComponent();
this.chatContainer.addChild(this.streamingComponent);
}
this.streamingComponent.updateContent(streamingMessage);
} else {
// Streaming stopped
if (this.loadingAnimation) {
this.loadingAnimation.stop();
this.loadingAnimation = null;
this.statusContainer.clear();
}
if (this.streamingComponent) {
this.chatContainer.removeChild(this.streamingComponent);
this.streamingComponent = null;
}
this.editor.disableSubmit = false;
}
this.ui.requestRender();
}
private addMessageToChat(message: Message): void {
if (message.role === "user") {
this.chatContainer.addChild(new TextComponent(chalk.green("[user]")));
const userMsg = message as any;
const textContent = userMsg.content?.map((c: any) => c.text || "").join("") || message.content || "";
this.chatContainer.addChild(new TextComponent(textContent, { bottom: 1 }));
} else if (message.role === "assistant") {
this.chatContainer.addChild(new TextComponent(chalk.hex("#FFA500")("[assistant]")));
const assistantMsg = message as AssistantMessage;
// Render text content
const textContent = assistantMsg.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("");
if (textContent) {
this.chatContainer.addChild(new MarkdownComponent(textContent));
}
// Render tool calls
const toolCalls = assistantMsg.content.filter((c) => c.type === "toolCall");
for (const toolCall of toolCalls) {
const argsStr =
typeof toolCall.arguments === "string" ? toolCall.arguments : JSON.stringify(toolCall.arguments);
this.chatContainer.addChild(new TextComponent(chalk.yellow(`[tool] ${toolCall.name}(${argsStr})`)));
}
this.chatContainer.addChild(new WhitespaceComponent(1));
} else if (message.role === "toolResult") {
const toolResultMsg = message as any;
const output = toolResultMsg.result?.output || toolResultMsg.result || "";
// Truncate long outputs
const lines = output.split("\n");
const maxLines = 10;
const truncated = lines.length > maxLines;
const toShow = truncated ? lines.slice(0, maxLines) : lines;
for (const line of toShow) {
this.chatContainer.addChild(new TextComponent(chalk.gray(line)));
}
if (truncated) {
this.chatContainer.addChild(new TextComponent(chalk.dim(`... (${lines.length - maxLines} more lines)`)));
}
this.chatContainer.addChild(new WhitespaceComponent(1));
}
}
async getUserInput(): Promise<string> {
return new Promise((resolve) => {
this.onInputCallback = (text: string) => {
this.onInputCallback = undefined;
resolve(text);
};
});
}
setInterruptCallback(callback: () => void): void {
this.onInterruptCallback = callback;
}
clearEditor(): void {
this.editor.setText("");
this.statusContainer.clear();
const hint = new TextComponent(chalk.dim("Press Ctrl+C again to exit"));
this.statusContainer.addChild(hint);
this.ui.requestRender();
setTimeout(() => {
this.statusContainer.clear();
this.ui.requestRender();
}, 500);
}
stop(): void {
if (this.loadingAnimation) {
this.loadingAnimation.stop();
this.loadingAnimation = null;
}
if (this.isInitialized) {
this.ui.stop();
this.isInitialized = false;
}
}
}

View file

@ -34,7 +34,7 @@
"node": ">=20.0.0"
},
"dependencies": {
"@mariozechner/pi-agent-old": "^0.5.44",
"@mariozechner/pi-agent": "^0.5.44",
"chalk": "^5.5.0"
},
"devDependencies": {}

View file

@ -1,49 +1,50 @@
{
"name": "@mariozechner/pi-web-ui",
"version": "0.5.44",
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./app.css": "./dist/app.css"
},
"scripts": {
"clean": "rm -rf dist",
"build": "tsc -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify",
"dev": "concurrently --names \"build,example\" --prefix-colors \"cyan,green\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\" \"npm run dev --prefix example\"",
"typecheck": "tsc --noEmit && cd example && tsc --noEmit",
"check": "npm run typecheck"
},
"dependencies": {
"@mariozechner/pi-ai": "^0.5.43",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lit": "^3.3.1",
"lucide": "^0.544.0",
"ollama": "^0.6.0",
"pdfjs-dist": "^5.4.296",
"xlsx": "^0.18.5"
},
"peerDependencies": {
"@mariozechner/mini-lit": "^0.1.9"
},
"devDependencies": {
"@mariozechner/mini-lit": "^0.1.9",
"@tailwindcss/cli": "^4.0.0-beta.14",
"concurrently": "^9.2.1",
"typescript": "^5.7.3"
},
"keywords": [
"ai",
"chat",
"ui",
"components",
"llm",
"web-components",
"mini-lit"
],
"author": "Mario Zechner",
"license": "MIT"
"name": "@mariozechner/pi-web-ui",
"version": "0.5.44",
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./app.css": "./dist/app.css"
},
"scripts": {
"clean": "rm -rf dist",
"build": "tsc -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify",
"dev": "concurrently --names \"build,example\" --prefix-colors \"cyan,green\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\" \"npm run dev --prefix example\"",
"typecheck": "tsc --noEmit && cd example && tsc --noEmit",
"check": "npm run typecheck"
},
"dependencies": {
"@mariozechner/pi-ai": "^0.5.43",
"@mariozechner/pi-tui": "^0.5.44",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lit": "^3.3.1",
"lucide": "^0.544.0",
"ollama": "^0.6.0",
"pdfjs-dist": "^5.4.296",
"xlsx": "^0.18.5"
},
"peerDependencies": {
"@mariozechner/mini-lit": "^0.1.9"
},
"devDependencies": {
"@mariozechner/mini-lit": "^0.1.9",
"@tailwindcss/cli": "^4.0.0-beta.14",
"concurrently": "^9.2.1",
"typescript": "^5.7.3"
},
"keywords": [
"ai",
"chat",
"ui",
"components",
"llm",
"web-components",
"mini-lit"
],
"author": "Mario Zechner",
"license": "MIT"
}

View file

@ -0,0 +1,88 @@
#!/usr/bin/env tsx
/**
* Count tokens in system prompts using Anthropic's token counter API
*/
import * as prompts from "../src/prompts/prompts.js";
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (!ANTHROPIC_API_KEY) {
console.error("Error: ANTHROPIC_API_KEY environment variable not set");
process.exit(1);
}
interface TokenCountResponse {
input_tokens: number;
}
async function countTokens(text: string): Promise<number> {
const response = await fetch("https://api.anthropic.com/v1/messages/count_tokens", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-3-5-sonnet-20241022",
messages: [
{
role: "user",
content: text,
},
],
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`API error: ${response.status} ${error}`);
}
const data = (await response.json()) as TokenCountResponse;
return data.input_tokens;
}
async function main() {
console.log("Counting tokens in prompts...\n");
const promptsToCount: Array<{ name: string; content: string }> = [
{
name: "ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW",
content: prompts.ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW,
},
{
name: "ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO",
content: prompts.ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
},
{
name: "ATTACHMENTS_RUNTIME_DESCRIPTION",
content: prompts.ATTACHMENTS_RUNTIME_DESCRIPTION,
},
{
name: "JAVASCRIPT_REPL_TOOL_DESCRIPTION (without runtime providers)",
content: prompts.JAVASCRIPT_REPL_TOOL_DESCRIPTION([]),
},
{
name: "ARTIFACTS_TOOL_DESCRIPTION (without runtime providers)",
content: prompts.ARTIFACTS_TOOL_DESCRIPTION([]),
},
];
let total = 0;
for (const prompt of promptsToCount) {
try {
const tokens = await countTokens(prompt.content);
total += tokens;
console.log(`${prompt.name}: ${tokens.toLocaleString()} tokens`);
} catch (error) {
console.error(`Error counting tokens for ${prompt.name}:`, error);
}
}
console.log(`\nTotal: ${total.toLocaleString()} tokens`);
}
main();

View file

@ -82,13 +82,14 @@ export class ChatPanel extends LitElement {
// Set up artifacts panel
this.artifactsPanel = new ArtifactsPanel();
this.artifactsPanel.agent = agent; // Pass agent for HTML artifact runtime providers
if (config?.sandboxUrlProvider) {
this.artifactsPanel.sandboxUrlProvider = config.sandboxUrlProvider;
}
// Register the standalone tool renderer (not the panel itself)
registerToolRenderer("artifacts", new ArtifactsToolRenderer(this.artifactsPanel));
// Runtime providers factory for attachments + artifacts access
// Runtime providers factory for REPL tools (read-write access)
const runtimeProvidersFactory = () => {
const attachments: Attachment[] = [];
for (const message of this.agent!.state.messages) {
@ -105,12 +106,11 @@ export class ChatPanel extends LitElement {
providers.push(new AttachmentsRuntimeProvider(attachments));
}
// Add artifacts provider (always available)
providers.push(new ArtifactsRuntimeProvider(this.artifactsPanel!, this.agent!));
// Add artifacts provider with read-write access (for REPL)
providers.push(new ArtifactsRuntimeProvider(this.artifactsPanel!, this.agent!, true));
return providers;
};
this.artifactsPanel.runtimeProvidersFactory = runtimeProvidersFactory;
this.artifactsPanel.onArtifactsChange = () => {
const count = this.artifactsPanel?.artifacts?.size ?? 0;

View file

@ -1,4 +1,7 @@
import { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION } from "../../prompts/prompts.js";
import {
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW,
} from "../../prompts/prompts.js";
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
// Define minimal interface for ArtifactsPanel to avoid circular dependencies
@ -24,6 +27,7 @@ export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider {
constructor(
private artifactsPanel: ArtifactsPanelLike,
private agent?: AgentLike,
private readWrite: boolean = true,
) {}
getData(): Record<string, any> {
@ -210,6 +214,6 @@ export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider {
}
getDescription(): string {
return ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION;
return this.readWrite ? ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW : ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO;
}
}

View file

@ -55,7 +55,8 @@ export { SessionListDialog } from "./dialogs/SessionListDialog.js";
export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js";
// Prompts
export {
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION,
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW,
ATTACHMENTS_RUNTIME_DESCRIPTION,
} from "./prompts/prompts.js";
// Storage

View file

@ -56,8 +56,9 @@ console.log('Sum:', sum, 'Average:', avg);
- Chart.js: Set options: { responsive: false, animation: false }
- Three.js: renderer.setSize(800, 600) with matching aspect ratio
## Library functions
You can use the following functions in your code:
## Helper Functions (Automatically Available)
These functions are injected into the execution environment and available globally:
${runtimeProviderDescriptions.join("\n\n")}
`;
@ -66,92 +67,78 @@ ${runtimeProviderDescriptions.join("\n\n")}
// Artifacts Tool
// ============================================================================
export const ARTIFACTS_TOOL_DESCRIPTION = (
runtimeProviderDescriptions: string[],
) => `Creates and manages file artifacts. Each artifact is a file with a filename and content.
export const ARTIFACTS_TOOL_DESCRIPTION = (runtimeProviderDescriptions: string[]) => `# Artifacts
CRITICAL - ARTIFACT UPDATE WORKFLOW:
1. Creating new file? Use 'create'
2. Changing specific section(s)? Use 'update' (PREFERRED - token efficient)
3. Complete structural overhaul? Use 'rewrite' (last resort only)
Create and manage persistent files that live alongside the conversation.
NEVER regenerate entire documents to change small sections
ALWAYS use 'update' for targeted edits (adding sources, fixing sections, appending to lists)
## When to Use - Artifacts Tool vs REPL
Commands:
1. create: Create a new file
- filename: Name with extension (required, e.g., 'summary.md', 'index.html')
- title: Display name for the tab (optional, defaults to filename)
- content: File content (required)
- Use for: Brand new files only
**Use artifacts tool when YOU are the author:**
- Writing research summaries, analysis, ideas, documentation
- Creating markdown notes for user to read
- Building HTML applications/visualizations that present data
- Creating HTML artifacts that render charts from programmatically generated data
2. update: Update part of an existing file (PREFERRED for edits)
- filename: File to update (required)
- old_str: Exact string to replace (required, can be multi-line)
- new_str: Replacement string (required)
- Use for: Adding sources, fixing typos, updating sections, appending content
- Token efficient - only transmits the changed portion
- Example: Adding source link to a section
**Use repl + artifact storage functions when CODE processes data:**
- Scraping workflows that extract and store data
- Processing CSV/Excel files programmatically
- Data transformation pipelines
- Binary file generation requiring libraries (PDF, DOCX)
3. rewrite: Completely replace a file's content (LAST RESORT)
- filename: File to rewrite (required)
- content: New content (required)
- Use ONLY when: Complete structural overhaul needed
- DO NOT use for: Adding one line, fixing one section, appending content
**Pattern: REPL generates data Artifacts tool creates HTML that visualizes it**
Example: repl scrapes products stores products.json you author dashboard.html that reads products.json and renders Chart.js visualizations
4. get: Retrieve the full content of a file
- filename: File to retrieve (required)
- Returns the complete file content
## Input
- { action: "create", filename: "notes.md", content: "..." } - Create new file
- { action: "update", filename: "notes.md", old_str: "...", new_str: "..." } - Update part of file (PREFERRED)
- { action: "rewrite", filename: "notes.md", content: "..." } - Replace entire file (LAST RESORT)
- { action: "get", filename: "data.json" } - Retrieve file content
- { action: "delete", filename: "old.csv" } - Delete file
- { action: "htmlArtifactLogs", filename: "app.html" } - Get console logs from HTML artifact
5. delete: Delete a file
- filename: File to delete (required)
## Returns
Depends on action:
- create/update/rewrite/delete: Success status or error
- get: File content
- htmlArtifactLogs: Console logs and errors
6. logs: Get console logs and errors (HTML files only)
- filename: HTML file to get logs for (required)
## Supported File Types
Text-based files you author: .md, .txt, .html, .js, .css, .json, .csv, .svg
Binary files requiring libraries (use repl): .pdf, .docx
ANTI-PATTERNS TO AVOID:
Using 'get' + modifying content + 'rewrite' to change one section
Using createOrUpdateArtifact() in code for manual edits YOU make
Use 'update' command for surgical, targeted modifications
## Critical - Prefer Update Over Rewrite
NEVER: get entire file + rewrite to change small sections
ALWAYS: update for targeted edits (token efficient)
Ask: Can I describe the change as old_str new_str? Use update.
For text/html artifacts:
- Must be a single self-contained file
- External scripts: Use CDNs like https://esm.sh, https://unpkg.com, or https://cdnjs.cloudflare.com
- Preferred: Use https://esm.sh for npm packages (e.g., https://esm.sh/three for Three.js)
- For ES modules, use: <script type="module">import * as THREE from 'https://esm.sh/three';</script>
- For Three.js specifically: import from 'https://esm.sh/three' or 'https://esm.sh/three@0.160.0'
- For addons: import from 'https://esm.sh/three/examples/jsm/controls/OrbitControls.js'
- No localStorage/sessionStorage - use in-memory variables only
- CSS should be included inline
- CRITICAL REMINDER FOR HTML ARTIFACTS:
- ALWAYS set a background color inline in <style> or directly on body element
- Failure to set a background color is a COMPLIANCE ERROR
- Background color MUST be explicitly defined to ensure visibility and proper rendering
- Can embed base64 images directly in img tags
- Ensure the layout is responsive as the iframe might be resized
- Note: Network errors (404s) for external scripts may not be captured in logs due to browser security
---
For application/vnd.ant.code artifacts:
- Include the language parameter for syntax highlighting
- Supports all major programming languages
## HTML Artifacts
For text/markdown:
- Standard markdown syntax
- Will be rendered with full formatting
- Can include base64 images using markdown syntax
Interactive HTML applications that can visualize data from other artifacts.
For image/svg+xml:
- Complete SVG markup
- Will be rendered inline
- Can embed raster images as base64 in SVG
### Data Access
- Can read artifacts created by repl and user attachments
- Use to build dashboards, visualizations, interactive tools
- See Helper Functions section below for available functions
CRITICAL REMINDER FOR ALL ARTIFACTS:
- Prefer to update existing files rather than creating new ones
- Keep filenames consistent and descriptive
- Use appropriate file extensions
- Ensure HTML artifacts have a defined background color
### Requirements
- Self-contained single file
- Import ES modules from esm.sh: <script type="module">import X from 'https://esm.sh/pkg';</script>
- Use Tailwind CDN: <script src="https://cdn.tailwindcss.com"></script>
- Can embed images from any domain: <img src="https://example.com/image.jpg">
- MUST set background color explicitly (avoid transparent)
- Inline CSS or Tailwind utility classes
- No localStorage/sessionStorage
The following functions are available inside your code in HTML artifacts:
### Styling
- Use Tailwind utility classes for clean, functional designs
- Ensure responsive layout (iframe may be resized)
- Avoid purple gradients, AI aesthetic clichés, and emojis
### Helper Functions (Automatically Available)
These functions are injected into HTML artifact sandbox:
${runtimeProviderDescriptions.join("\n\n")}
`;
@ -160,52 +147,83 @@ ${runtimeProviderDescriptions.join("\n\n")}
// Artifacts Runtime Provider
// ============================================================================
export const ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION = `
### Artifacts
export const ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW = `
### Artifacts Storage
Programmatically create, read, update, and delete artifact files from your code.
Create, read, update, and delete files in artifacts storage.
#### When to Use
- Persist data or state between REPL calls
- ONLY when writing code that programmatically generates/transforms data
- Examples: Web scraping results, processed CSV data, generated charts saved as JSON
- The artifact content is CREATED BY THE CODE, not by you directly
- Store intermediate results between tool calls
- Save generated files (images, CSVs, processed data) for user to view and download
#### Do NOT Use For
- Summaries or notes YOU write (use artifacts tool instead)
- Content YOU author directly (use artifacts tool instead)
- Content you author directly, like summaries of content you read (use artifacts tool instead)
#### Functions
- await listArtifacts() - Get list of all artifact filenames, returns string[]
* Example: const files = await listArtifacts(); // ['data.json', 'notes.md']
- await getArtifact(filename) - Read artifact content, returns string or object
* Auto-parses .json files to objects
* Example: const data = await getArtifact('data.json'); // Returns parsed object
- await createOrUpdateArtifact(filename, content, mimeType?) - Create/update artifact FROM CODE
* ONLY use when the content is generated programmatically by your code
* Auto-stringifies objects for .json files
* Example: await createOrUpdateArtifact('scraped-data.json', results)
* Example: await createOrUpdateArtifact('chart.png', base64ImageData, 'image/png')
- await deleteArtifact(filename) - Delete an artifact
* Example: await deleteArtifact('temp.json')
- listArtifacts() - List all artifact filenames, returns Promise<string[]>
- getArtifact(filename) - Read artifact content, returns Promise<string | object>. JSON files auto-parse to objects, binary files return base64 string
- createOrUpdateArtifact(filename, content, mimeType?) - Create or update artifact, returns Promise<void>. JSON files auto-stringify objects, binary requires base64 string with mimeType
- deleteArtifact(filename) - Delete artifact, returns Promise<void>
#### Example
Scraping data and saving it:
JSON workflow:
\`\`\`javascript
const response = await fetch('https://api.example.com/data');
const data = await response.json();
await createOrUpdateArtifact('api-results.json', data);
// Fetch and save
const response = await fetch('https://api.example.com/products');
const products = await response.json();
await createOrUpdateArtifact('products.json', products);
// Later: read and filter
const all = await getArtifact('products.json');
const cheap = all.filter(p => p.price < 100);
await createOrUpdateArtifact('cheap.json', cheap);
\`\`\`
Binary data (convert to base64 first):
Binary file (image):
\`\`\`javascript
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
const arrayBuffer = await blob.arrayBuffer();
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
await createOrUpdateArtifact('image.png', base64);
const canvas = document.createElement('canvas');
canvas.width = 800; canvas.height = 600;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'blue';
ctx.fillRect(0, 0, 800, 600);
// Remove data:image/png;base64, prefix
const base64 = canvas.toDataURL().split(',')[1];
await createOrUpdateArtifact('chart.png', base64, 'image/png');
\`\`\`
`;
export const ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO = `
### Artifacts Storage
Read files from artifacts storage.
#### When to Use
- Read artifacts created by REPL or artifacts tool
- Access data from other HTML artifacts
- Load configuration or data files
#### Do NOT Use For
- Creating new artifacts (not available in HTML artifacts)
- Modifying artifacts (read-only access)
#### Functions
- listArtifacts() - List all artifact filenames, returns Promise<string[]>
- getArtifact(filename) - Read artifact content, returns Promise<string | object>. JSON files auto-parse to objects, binary files return base64 string
#### Example
JSON data:
\`\`\`javascript
const products = await getArtifact('products.json');
const html = products.map(p => \`<div>\${p.name}: $\${p.price}</div>\`).join('');
document.body.innerHTML = html;
\`\`\`
Binary image:
\`\`\`javascript
const base64 = await getArtifact('chart.png');
const img = document.createElement('img');
img.src = 'data:image/png;base64,' + base64;
document.body.appendChild(img);
\`\`\`
`;
@ -216,49 +234,33 @@ await createOrUpdateArtifact('image.png', base64);
export const ATTACHMENTS_RUNTIME_DESCRIPTION = `
### User Attachments
Read files that the user has uploaded to the conversation.
Read files the user uploaded to the conversation.
#### When to Use
- When you need to read or process files the user has uploaded to the conversation
- Examples: CSV data files, JSON datasets, Excel spreadsheets, images, PDFs
#### Do NOT Use For
- Creating new files (use createOrUpdateArtifact instead)
- Modifying existing files (read first, then create artifact with modified version)
- Process user-uploaded files (CSV, JSON, Excel, images, PDFs)
#### Functions
- listAttachments() - List all attachments, returns array of {id, fileName, mimeType, size}
* Example: const files = listAttachments(); // [{id: '...', fileName: 'data.xlsx', mimeType: '...', size: 12345}]
- readTextAttachment(attachmentId) - Read attachment as text, returns string
* Use for: CSV, JSON, TXT, XML, and other text-based files
* Example: const csvContent = readTextAttachment(files[0].id);
* Example: const json = JSON.parse(readTextAttachment(jsonFile.id));
- readBinaryAttachment(attachmentId) - Read attachment as binary data, returns Uint8Array
* Use for: Excel (.xlsx), images, PDFs, and other binary files
* Example: const xlsxBytes = readBinaryAttachment(files[0].id);
* Example: const XLSX = await import('https://esm.run/xlsx'); const workbook = XLSX.read(xlsxBytes);
- readTextAttachment(id) - Read attachment as text, returns string
- readBinaryAttachment(id) - Read attachment as binary data, returns Uint8Array
#### Example
Processing CSV attachment:
CSV file:
\`\`\`javascript
const files = listAttachments();
const csvFile = files.find(f => f.fileName.endsWith('.csv'));
const csvData = readTextAttachment(csvFile.id);
const rows = csvData.split('\\n').map(row => row.split(','));
console.log(\`Found \${rows.length} rows\`);
\`\`\`
Processing Excel attachment:
Excel file:
\`\`\`javascript
const XLSX = await import('https://esm.run/xlsx');
const files = listAttachments();
const excelFile = files.find(f => f.fileName.endsWith('.xlsx'));
const bytes = readBinaryAttachment(excelFile.id);
const xlsxFile = files.find(f => f.fileName.endsWith('.xlsx'));
const bytes = readBinaryAttachment(xlsxFile.id);
const workbook = XLSX.read(bytes);
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet);
const data = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]);
\`\`\`
`;
@ -266,22 +268,15 @@ const jsonData = XLSX.utils.sheet_to_json(firstSheet);
// Extract Document Tool
// ============================================================================
export const EXTRACT_DOCUMENT_DESCRIPTION = `Extract plain text from documents on the web (PDF, DOCX, XLSX, PPTX).
export const EXTRACT_DOCUMENT_DESCRIPTION = `# Extract Document
## Purpose
Use this when the user wants you to read a document at a URL.
Extract plain text from documents on the web (PDF, DOCX, XLSX, PPTX).
## Parameters
- url: URL of the document (PDF, DOCX, XLSX, or PPTX only)
## When to Use
User wants you to read a document at a URL.
## Input
- { url: "https://example.com/document.pdf" } - URL to PDF, DOCX, XLSX, or PPTX
## Returns
Structured plain text with page/sheet/slide delimiters in XML-like format:
- PDFs: <pdf filename="..."><page number="1">text</page>...</pdf>
- Word: <docx filename="..."><page number="1">text</page></docx>
- Excel: <excel filename="..."><sheet name="Sheet1" index="1">CSV data</sheet>...</excel>
- PowerPoint: <pptx filename="..."><slide number="1">text</slide>...<notes>...</notes></pptx>
## Important Notes
- Maximum file size: 50MB
- CORS restrictions may block some URLs - if this happens, the error will guide you to help the user configure a CORS proxy
- Format is automatically detected from file extension and Content-Type header`;
Structured plain text with page/sheet/slide delimiters.`;

View file

@ -6,9 +6,17 @@ import { html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, type Ref, ref } from "lit/directives/ref.js";
import { X } from "lucide";
import type { Agent } from "../../agent/agent.js";
import type { ArtifactMessage } from "../../components/Messages.js";
import { ArtifactsRuntimeProvider } from "../../components/sandbox/ArtifactsRuntimeProvider.js";
import { AttachmentsRuntimeProvider } from "../../components/sandbox/AttachmentsRuntimeProvider.js";
import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.js";
import { ARTIFACTS_TOOL_DESCRIPTION } from "../../prompts/prompts.js";
import {
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
ARTIFACTS_TOOL_DESCRIPTION,
ATTACHMENTS_RUNTIME_DESCRIPTION,
} from "../../prompts/prompts.js";
import type { Attachment } from "../../utils/attachment-utils.js";
import { i18n } from "../../utils/i18n.js";
import type { ArtifactElement } from "./ArtifactElement.js";
import { DocxArtifact } from "./DocxArtifact.js";
@ -50,8 +58,8 @@ export class ArtifactsPanel extends LitElement {
private artifactElements = new Map<string, ArtifactElement>();
private contentRef: Ref<HTMLDivElement> = createRef();
// External factory for runtime providers (decouples panel from AgentInterface)
@property({ attribute: false }) runtimeProvidersFactory?: () => SandboxRuntimeProvider[];
// Agent reference (needed to get attachments for HTML artifacts)
@property({ attribute: false }) agent?: Agent;
// Sandbox URL provider for browser extensions (optional)
@property({ attribute: false }) sandboxUrlProvider?: () => string;
// Callbacks
@ -68,6 +76,29 @@ export class ArtifactsPanel extends LitElement {
return this._artifacts;
}
// Get runtime providers for HTML artifacts (read-only: attachments + artifacts)
private getHtmlArtifactRuntimeProviders(): SandboxRuntimeProvider[] {
const providers: SandboxRuntimeProvider[] = [];
// Get attachments from agent messages
if (this.agent) {
const attachments: Attachment[] = [];
for (const message of this.agent.state.messages) {
if (message.role === "user" && message.attachments) {
attachments.push(...message.attachments);
}
}
if (attachments.length > 0) {
providers.push(new AttachmentsRuntimeProvider(attachments));
}
}
// Add read-only artifacts provider
providers.push(new ArtifactsRuntimeProvider(this, this.agent, false));
return providers;
}
protected override createRenderRoot(): HTMLElement | DocumentFragment {
return this; // light DOM for shared styles
}
@ -153,8 +184,7 @@ export class ArtifactsPanel extends LitElement {
const type = this.getFileType(filename);
if (type === "html") {
element = new HtmlArtifact();
const runtimeProviders = this.runtimeProvidersFactory?.() || [];
(element as HtmlArtifact).runtimeProviders = runtimeProviders;
(element as HtmlArtifact).runtimeProviders = this.getHtmlArtifactRuntimeProviders();
if (this.sandboxUrlProvider) {
(element as HtmlArtifact).sandboxUrlProvider = this.sandboxUrlProvider;
}
@ -198,8 +228,7 @@ export class ArtifactsPanel extends LitElement {
// Just update content
element.content = content;
if (element instanceof HtmlArtifact) {
const runtimeProviders = this.runtimeProvidersFactory?.() || [];
element.runtimeProviders = runtimeProviders;
element.runtimeProviders = this.getHtmlArtifactRuntimeProviders();
}
}
@ -240,16 +269,15 @@ export class ArtifactsPanel extends LitElement {
// Build the AgentTool (no details payload; return only output strings)
public get tool(): AgentTool<typeof artifactsParamsSchema, undefined> {
const self = this;
return {
label: "Artifacts",
name: "artifacts",
get description() {
const runtimeProviderDescriptions =
self
.runtimeProvidersFactory?.()
.map((d) => d.getDescription())
.filter((d) => d.trim().length > 0) || [];
// HTML artifacts have read-only access to attachments and artifacts
const runtimeProviderDescriptions = [
ATTACHMENTS_RUNTIME_DESCRIPTION,
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
];
return ARTIFACTS_TOOL_DESCRIPTION(runtimeProviderDescriptions);
},
parameters: artifactsParamsSchema,
@ -282,7 +310,6 @@ export class ArtifactsPanel extends LitElement {
// 2) Build an ordered list of successful artifact operations
const operations: Array<ArtifactsParams> = [];
for (const m of messages) {
// Handle artifact messages (from programmatic operations like browser_javascript)
if ((m as any).role === "artifact") {
const artifactMsg = m as ArtifactMessage;
switch (artifactMsg.action) {

View file

@ -16,14 +16,12 @@ const tui = JSON.parse(readFileSync(join(packagesDir, 'tui/package.json'), 'utf8
const agent = JSON.parse(readFileSync(join(packagesDir, 'agent/package.json'), 'utf8'));
const pods = JSON.parse(readFileSync(join(packagesDir, 'pods/package.json'), 'utf8'));
const webUi = JSON.parse(readFileSync(join(packagesDir, 'web-ui/package.json'), 'utf8'));
const browserExtension = JSON.parse(readFileSync(join(packagesDir, 'browser-extension/package.json'), 'utf8'));
console.log('Current versions:');
console.log(` @mariozechner/pi-tui: ${tui.version}`);
console.log(` @mariozechner/pi-agent: ${agent.version}`);
console.log(` @mariozechner/pi: ${pods.version}`);
console.log(` @mariozechner/pi-web-ui: ${webUi.version}`);
console.log(` @mariozechner/pi-reader-extension: ${browserExtension.version}`);
// Update agent's dependency on tui
if (agent.dependencies['@mariozechner/pi-tui']) {
@ -41,12 +39,13 @@ if (pods.dependencies['@mariozechner/pi-agent']) {
console.log(`Updated pods' dependency on pi-agent: ${oldVersion} → ^${agent.version}`);
}
// Update browser-extension's dependency on web-ui
if (browserExtension.dependencies['@mariozechner/pi-web-ui']) {
const oldVersion = browserExtension.dependencies['@mariozechner/pi-web-ui'];
browserExtension.dependencies['@mariozechner/pi-web-ui'] = `^${webUi.version}`;
writeFileSync(join(packagesDir, 'browser-extension/package.json'), JSON.stringify(browserExtension, null, '\t') + '\n');
console.log(`Updated browser-extension's dependency on pi-web-ui: ${oldVersion} → ^${webUi.version}`);
// Update web-ui's dependency on tui
if (webUi.dependencies['@mariozechner/pi-tui']) {
const oldVersion = webUi.dependencies['@mariozechner/pi-tui'];
webUi.dependencies['@mariozechner/pi-tui'] = `^${tui.version}`;
writeFileSync(join(packagesDir, 'web-ui/package.json'), JSON.stringify(webUi, null, '\t') + '\n');
console.log(`Updated web-ui's dependency on pi-tui: ${oldVersion} → ^${tui.version}`);
}
console.log('\n✅ Version sync complete!');