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": "*" "@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": { "node_modules/@types/minimatch": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
@ -3793,6 +3799,27 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/mimic-response": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@ -5222,7 +5249,8 @@
"version": "0.5.44", "version": "0.5.44",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mariozechner/pi-ai": "^0.5.44" "@mariozechner/pi-ai": "^0.5.44",
"@mariozechner/pi-tui": "^0.5.44"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
@ -5385,7 +5413,6 @@
"dependencies": { "dependencies": {
"@mariozechner/pi-agent": "^0.5.44", "@mariozechner/pi-agent": "^0.5.44",
"@mariozechner/pi-ai": "^0.5.44", "@mariozechner/pi-ai": "^0.5.44",
"@mariozechner/pi-tui": "^0.5.44",
"chalk": "^5.5.0", "chalk": "^5.5.0",
"glob": "^11.0.3" "glob": "^11.0.3"
}, },
@ -5435,7 +5462,7 @@
"version": "0.5.44", "version": "0.5.44",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mariozechner/pi-agent-old": "^0.5.44", "@mariozechner/pi-agent": "^0.5.44",
"chalk": "^5.5.0" "chalk": "^5.5.0"
}, },
"bin": { "bin": {
@ -5492,10 +5519,6 @@
"node": ">=20.0.0" "node": ">=20.0.0"
} }
}, },
"packages/tui/node_modules/@types/mime-types": {
"version": "2.1.4",
"license": "MIT"
},
"packages/tui/node_modules/chalk": { "packages/tui/node_modules/chalk": {
"version": "5.6.2", "version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
@ -5518,29 +5541,13 @@
"node": ">= 18" "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": { "packages/web-ui": {
"name": "@mariozechner/pi-web-ui", "name": "@mariozechner/pi-web-ui",
"version": "0.5.44", "version": "0.5.44",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mariozechner/pi-ai": "^0.5.43", "@mariozechner/pi-ai": "^0.5.43",
"@mariozechner/pi-tui": "^0.5.44",
"docx-preview": "^0.3.7", "docx-preview": "^0.3.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lit": "^3.3.1", "lit": "^3.3.1",

View file

@ -18,7 +18,8 @@
"prepublishOnly": "npm run clean && npm run build" "prepublishOnly": "npm run clean && npm run build"
}, },
"dependencies": { "dependencies": {
"@mariozechner/pi-ai": "^0.5.44" "@mariozechner/pi-ai": "^0.5.44",
"@mariozechner/pi-tui": "^0.5.44"
}, },
"keywords": [ "keywords": [
"ai", "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": { "dependencies": {
"@mariozechner/pi-agent": "^0.5.44", "@mariozechner/pi-agent": "^0.5.44",
"@mariozechner/pi-ai": "^0.5.44", "@mariozechner/pi-ai": "^0.5.44",
"@mariozechner/pi-tui": "^0.5.44",
"chalk": "^5.5.0", "chalk": "^5.5.0",
"glob": "^11.0.3" "glob": "^11.0.3"
}, },

View file

@ -3,6 +3,7 @@ import { getModel } from "@mariozechner/pi-ai";
import chalk from "chalk"; import chalk from "chalk";
import { SessionManager } from "./session-manager.js"; import { SessionManager } from "./session-manager.js";
import { codingTools } from "./tools/index.js"; import { codingTools } from "./tools/index.js";
import { TuiRenderer } from "./tui-renderer.js";
interface Args { interface Args {
provider?: string; provider?: string;
@ -57,6 +58,9 @@ ${chalk.bold("Options:")}
--help, -h Show this help --help, -h Show this help
${chalk.bold("Examples:")} ${chalk.bold("Examples:")}
# Interactive mode (no messages = interactive TUI)
coding-agent
# Single message # Single message
coding-agent "List all .ts files in src/" coding-agent "List all .ts files in src/"
@ -101,6 +105,57 @@ Guidelines:
Current directory: ${process.cwd()}`; 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[]) { export async function main(args: string[]) {
const parsed = parseArgs(args); const parsed = parseArgs(args);
@ -183,27 +238,12 @@ export async function main(args: string[]) {
sessionManager.saveEvent(event); sessionManager.saveEvent(event);
}); });
// Process messages // Determine mode: interactive if no messages provided
if (parsed.messages.length === 0) { const isInteractive = 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()}`)); if (isInteractive) {
return; 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" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@mariozechner/pi-agent-old": "^0.5.44", "@mariozechner/pi-agent": "^0.5.44",
"chalk": "^5.5.0" "chalk": "^5.5.0"
}, },
"devDependencies": {} "devDependencies": {}

View file

@ -1,49 +1,50 @@
{ {
"name": "@mariozechner/pi-web-ui", "name": "@mariozechner/pi-web-ui",
"version": "0.5.44", "version": "0.5.44",
"description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai", "description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",
"./app.css": "./dist/app.css" "./app.css": "./dist/app.css"
}, },
"scripts": { "scripts": {
"clean": "rm -rf dist", "clean": "rm -rf dist",
"build": "tsc -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify", "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\"", "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", "typecheck": "tsc --noEmit && cd example && tsc --noEmit",
"check": "npm run typecheck" "check": "npm run typecheck"
}, },
"dependencies": { "dependencies": {
"@mariozechner/pi-ai": "^0.5.43", "@mariozechner/pi-ai": "^0.5.43",
"docx-preview": "^0.3.7", "@mariozechner/pi-tui": "^0.5.44",
"jszip": "^3.10.1", "docx-preview": "^0.3.7",
"lit": "^3.3.1", "jszip": "^3.10.1",
"lucide": "^0.544.0", "lit": "^3.3.1",
"ollama": "^0.6.0", "lucide": "^0.544.0",
"pdfjs-dist": "^5.4.296", "ollama": "^0.6.0",
"xlsx": "^0.18.5" "pdfjs-dist": "^5.4.296",
}, "xlsx": "^0.18.5"
"peerDependencies": { },
"@mariozechner/mini-lit": "^0.1.9" "peerDependencies": {
}, "@mariozechner/mini-lit": "^0.1.9"
"devDependencies": { },
"@mariozechner/mini-lit": "^0.1.9", "devDependencies": {
"@tailwindcss/cli": "^4.0.0-beta.14", "@mariozechner/mini-lit": "^0.1.9",
"concurrently": "^9.2.1", "@tailwindcss/cli": "^4.0.0-beta.14",
"typescript": "^5.7.3" "concurrently": "^9.2.1",
}, "typescript": "^5.7.3"
"keywords": [ },
"ai", "keywords": [
"chat", "ai",
"ui", "chat",
"components", "ui",
"llm", "components",
"web-components", "llm",
"mini-lit" "web-components",
], "mini-lit"
"author": "Mario Zechner", ],
"license": "MIT" "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 // Set up artifacts panel
this.artifactsPanel = new ArtifactsPanel(); this.artifactsPanel = new ArtifactsPanel();
this.artifactsPanel.agent = agent; // Pass agent for HTML artifact runtime providers
if (config?.sandboxUrlProvider) { if (config?.sandboxUrlProvider) {
this.artifactsPanel.sandboxUrlProvider = config.sandboxUrlProvider; this.artifactsPanel.sandboxUrlProvider = config.sandboxUrlProvider;
} }
// Register the standalone tool renderer (not the panel itself) // Register the standalone tool renderer (not the panel itself)
registerToolRenderer("artifacts", new ArtifactsToolRenderer(this.artifactsPanel)); 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 runtimeProvidersFactory = () => {
const attachments: Attachment[] = []; const attachments: Attachment[] = [];
for (const message of this.agent!.state.messages) { for (const message of this.agent!.state.messages) {
@ -105,12 +106,11 @@ export class ChatPanel extends LitElement {
providers.push(new AttachmentsRuntimeProvider(attachments)); providers.push(new AttachmentsRuntimeProvider(attachments));
} }
// Add artifacts provider (always available) // Add artifacts provider with read-write access (for REPL)
providers.push(new ArtifactsRuntimeProvider(this.artifactsPanel!, this.agent!)); providers.push(new ArtifactsRuntimeProvider(this.artifactsPanel!, this.agent!, true));
return providers; return providers;
}; };
this.artifactsPanel.runtimeProvidersFactory = runtimeProvidersFactory;
this.artifactsPanel.onArtifactsChange = () => { this.artifactsPanel.onArtifactsChange = () => {
const count = this.artifactsPanel?.artifacts?.size ?? 0; 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"; import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
// Define minimal interface for ArtifactsPanel to avoid circular dependencies // Define minimal interface for ArtifactsPanel to avoid circular dependencies
@ -24,6 +27,7 @@ export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider {
constructor( constructor(
private artifactsPanel: ArtifactsPanelLike, private artifactsPanel: ArtifactsPanelLike,
private agent?: AgentLike, private agent?: AgentLike,
private readWrite: boolean = true,
) {} ) {}
getData(): Record<string, any> { getData(): Record<string, any> {
@ -210,6 +214,6 @@ export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider {
} }
getDescription(): string { 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"; export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js";
// Prompts // Prompts
export { export {
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION, ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW,
ATTACHMENTS_RUNTIME_DESCRIPTION, ATTACHMENTS_RUNTIME_DESCRIPTION,
} from "./prompts/prompts.js"; } from "./prompts/prompts.js";
// Storage // Storage

View file

@ -56,8 +56,9 @@ console.log('Sum:', sum, 'Average:', avg);
- Chart.js: Set options: { responsive: false, animation: false } - Chart.js: Set options: { responsive: false, animation: false }
- Three.js: renderer.setSize(800, 600) with matching aspect ratio - Three.js: renderer.setSize(800, 600) with matching aspect ratio
## Library functions ## Helper Functions (Automatically Available)
You can use the following functions in your code:
These functions are injected into the execution environment and available globally:
${runtimeProviderDescriptions.join("\n\n")} ${runtimeProviderDescriptions.join("\n\n")}
`; `;
@ -66,92 +67,78 @@ ${runtimeProviderDescriptions.join("\n\n")}
// Artifacts Tool // Artifacts Tool
// ============================================================================ // ============================================================================
export const ARTIFACTS_TOOL_DESCRIPTION = ( export const ARTIFACTS_TOOL_DESCRIPTION = (runtimeProviderDescriptions: string[]) => `# Artifacts
runtimeProviderDescriptions: string[],
) => `Creates and manages file artifacts. Each artifact is a file with a filename and content.
CRITICAL - ARTIFACT UPDATE WORKFLOW: Create and manage persistent files that live alongside the conversation.
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)
NEVER regenerate entire documents to change small sections ## When to Use - Artifacts Tool vs REPL
ALWAYS use 'update' for targeted edits (adding sources, fixing sections, appending to lists)
Commands: **Use artifacts tool when YOU are the author:**
1. create: Create a new file - Writing research summaries, analysis, ideas, documentation
- filename: Name with extension (required, e.g., 'summary.md', 'index.html') - Creating markdown notes for user to read
- title: Display name for the tab (optional, defaults to filename) - Building HTML applications/visualizations that present data
- content: File content (required) - Creating HTML artifacts that render charts from programmatically generated data
- Use for: Brand new files only
2. update: Update part of an existing file (PREFERRED for edits) **Use repl + artifact storage functions when CODE processes data:**
- filename: File to update (required) - Scraping workflows that extract and store data
- old_str: Exact string to replace (required, can be multi-line) - Processing CSV/Excel files programmatically
- new_str: Replacement string (required) - Data transformation pipelines
- Use for: Adding sources, fixing typos, updating sections, appending content - Binary file generation requiring libraries (PDF, DOCX)
- Token efficient - only transmits the changed portion
- Example: Adding source link to a section
3. rewrite: Completely replace a file's content (LAST RESORT) **Pattern: REPL generates data Artifacts tool creates HTML that visualizes it**
- filename: File to rewrite (required) Example: repl scrapes products stores products.json you author dashboard.html that reads products.json and renders Chart.js visualizations
- content: New content (required)
- Use ONLY when: Complete structural overhaul needed
- DO NOT use for: Adding one line, fixing one section, appending content
4. get: Retrieve the full content of a file ## Input
- filename: File to retrieve (required) - { action: "create", filename: "notes.md", content: "..." } - Create new file
- Returns the complete file content - { 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 ## Returns
- filename: File to delete (required) 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) ## Supported File Types
- filename: HTML file to get logs for (required) 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: ## Critical - Prefer Update Over Rewrite
Using 'get' + modifying content + 'rewrite' to change one section NEVER: get entire file + rewrite to change small sections
Using createOrUpdateArtifact() in code for manual edits YOU make ALWAYS: update for targeted edits (token efficient)
Use 'update' command for surgical, targeted modifications 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: ## HTML Artifacts
- Include the language parameter for syntax highlighting
- Supports all major programming languages
For text/markdown: Interactive HTML applications that can visualize data from other artifacts.
- Standard markdown syntax
- Will be rendered with full formatting
- Can include base64 images using markdown syntax
For image/svg+xml: ### Data Access
- Complete SVG markup - Can read artifacts created by repl and user attachments
- Will be rendered inline - Use to build dashboards, visualizations, interactive tools
- Can embed raster images as base64 in SVG - See Helper Functions section below for available functions
CRITICAL REMINDER FOR ALL ARTIFACTS: ### Requirements
- Prefer to update existing files rather than creating new ones - Self-contained single file
- Keep filenames consistent and descriptive - Import ES modules from esm.sh: <script type="module">import X from 'https://esm.sh/pkg';</script>
- Use appropriate file extensions - Use Tailwind CDN: <script src="https://cdn.tailwindcss.com"></script>
- Ensure HTML artifacts have a defined background color - 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")} ${runtimeProviderDescriptions.join("\n\n")}
`; `;
@ -160,52 +147,83 @@ ${runtimeProviderDescriptions.join("\n\n")}
// Artifacts Runtime Provider // Artifacts Runtime Provider
// ============================================================================ // ============================================================================
export const ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION = ` export const ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RW = `
### Artifacts ### 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 #### When to Use
- Persist data or state between REPL calls - Store intermediate results between tool calls
- ONLY when writing code that programmatically generates/transforms data - Save generated files (images, CSVs, processed data) for user to view and download
- Examples: Web scraping results, processed CSV data, generated charts saved as JSON
- The artifact content is CREATED BY THE CODE, not by you directly
#### Do NOT Use For #### Do NOT Use For
- Summaries or notes YOU write (use artifacts tool instead) - Content you author directly, like summaries of content you read (use artifacts tool instead)
- Content YOU author directly (use artifacts tool instead)
#### Functions #### Functions
- await listArtifacts() - Get list of all artifact filenames, returns string[] - listArtifacts() - List all artifact filenames, returns Promise<string[]>
* Example: const files = await listArtifacts(); // ['data.json', 'notes.md'] - 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
- await getArtifact(filename) - Read artifact content, returns string or object - deleteArtifact(filename) - Delete artifact, returns Promise<void>
* 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')
#### Example #### Example
Scraping data and saving it: JSON workflow:
\`\`\`javascript \`\`\`javascript
const response = await fetch('https://api.example.com/data'); // Fetch and save
const data = await response.json(); const response = await fetch('https://api.example.com/products');
await createOrUpdateArtifact('api-results.json', data); 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 \`\`\`javascript
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png')); const canvas = document.createElement('canvas');
const arrayBuffer = await blob.arrayBuffer(); canvas.width = 800; canvas.height = 600;
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); const ctx = canvas.getContext('2d');
await createOrUpdateArtifact('image.png', base64); 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 = ` export const ATTACHMENTS_RUNTIME_DESCRIPTION = `
### User Attachments ### User Attachments
Read files that the user has uploaded to the conversation. Read files the user uploaded to the conversation.
#### When to Use #### When to Use
- When you need to read or process files the user has uploaded to the conversation - Process user-uploaded files (CSV, JSON, Excel, images, PDFs)
- 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)
#### Functions #### Functions
- listAttachments() - List all attachments, returns array of {id, fileName, mimeType, size} - listAttachments() - List all attachments, returns array of {id, fileName, mimeType, size}
* Example: const files = listAttachments(); // [{id: '...', fileName: 'data.xlsx', mimeType: '...', size: 12345}] - readTextAttachment(id) - Read attachment as text, returns string
- readBinaryAttachment(id) - Read attachment as binary data, returns Uint8Array
- 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);
#### Example #### Example
Processing CSV attachment: CSV file:
\`\`\`javascript \`\`\`javascript
const files = listAttachments(); const files = listAttachments();
const csvFile = files.find(f => f.fileName.endsWith('.csv')); const csvFile = files.find(f => f.fileName.endsWith('.csv'));
const csvData = readTextAttachment(csvFile.id); const csvData = readTextAttachment(csvFile.id);
const rows = csvData.split('\\n').map(row => row.split(',')); const rows = csvData.split('\\n').map(row => row.split(','));
console.log(\`Found \${rows.length} rows\`);
\`\`\` \`\`\`
Processing Excel attachment: Excel file:
\`\`\`javascript \`\`\`javascript
const XLSX = await import('https://esm.run/xlsx'); const XLSX = await import('https://esm.run/xlsx');
const files = listAttachments(); const files = listAttachments();
const excelFile = files.find(f => f.fileName.endsWith('.xlsx')); const xlsxFile = files.find(f => f.fileName.endsWith('.xlsx'));
const bytes = readBinaryAttachment(excelFile.id); const bytes = readBinaryAttachment(xlsxFile.id);
const workbook = XLSX.read(bytes); const workbook = XLSX.read(bytes);
const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; const data = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]);
const jsonData = XLSX.utils.sheet_to_json(firstSheet);
\`\`\` \`\`\`
`; `;
@ -266,22 +268,15 @@ const jsonData = XLSX.utils.sheet_to_json(firstSheet);
// Extract Document Tool // 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 Extract plain text from documents on the web (PDF, DOCX, XLSX, PPTX).
Use this when the user wants you to read a document at a URL.
## Parameters ## When to Use
- url: URL of the document (PDF, DOCX, XLSX, or PPTX only) 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 ## Returns
Structured plain text with page/sheet/slide delimiters in XML-like format: Structured plain text with page/sheet/slide delimiters.`;
- 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`;

View file

@ -6,9 +6,17 @@ import { html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { createRef, type Ref, ref } from "lit/directives/ref.js"; import { createRef, type Ref, ref } from "lit/directives/ref.js";
import { X } from "lucide"; import { X } from "lucide";
import type { Agent } from "../../agent/agent.js";
import type { ArtifactMessage } from "../../components/Messages.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 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 { i18n } from "../../utils/i18n.js";
import type { ArtifactElement } from "./ArtifactElement.js"; import type { ArtifactElement } from "./ArtifactElement.js";
import { DocxArtifact } from "./DocxArtifact.js"; import { DocxArtifact } from "./DocxArtifact.js";
@ -50,8 +58,8 @@ export class ArtifactsPanel extends LitElement {
private artifactElements = new Map<string, ArtifactElement>(); private artifactElements = new Map<string, ArtifactElement>();
private contentRef: Ref<HTMLDivElement> = createRef(); private contentRef: Ref<HTMLDivElement> = createRef();
// External factory for runtime providers (decouples panel from AgentInterface) // Agent reference (needed to get attachments for HTML artifacts)
@property({ attribute: false }) runtimeProvidersFactory?: () => SandboxRuntimeProvider[]; @property({ attribute: false }) agent?: Agent;
// Sandbox URL provider for browser extensions (optional) // Sandbox URL provider for browser extensions (optional)
@property({ attribute: false }) sandboxUrlProvider?: () => string; @property({ attribute: false }) sandboxUrlProvider?: () => string;
// Callbacks // Callbacks
@ -68,6 +76,29 @@ export class ArtifactsPanel extends LitElement {
return this._artifacts; 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 { protected override createRenderRoot(): HTMLElement | DocumentFragment {
return this; // light DOM for shared styles return this; // light DOM for shared styles
} }
@ -153,8 +184,7 @@ export class ArtifactsPanel extends LitElement {
const type = this.getFileType(filename); const type = this.getFileType(filename);
if (type === "html") { if (type === "html") {
element = new HtmlArtifact(); element = new HtmlArtifact();
const runtimeProviders = this.runtimeProvidersFactory?.() || []; (element as HtmlArtifact).runtimeProviders = this.getHtmlArtifactRuntimeProviders();
(element as HtmlArtifact).runtimeProviders = runtimeProviders;
if (this.sandboxUrlProvider) { if (this.sandboxUrlProvider) {
(element as HtmlArtifact).sandboxUrlProvider = this.sandboxUrlProvider; (element as HtmlArtifact).sandboxUrlProvider = this.sandboxUrlProvider;
} }
@ -198,8 +228,7 @@ export class ArtifactsPanel extends LitElement {
// Just update content // Just update content
element.content = content; element.content = content;
if (element instanceof HtmlArtifact) { if (element instanceof HtmlArtifact) {
const runtimeProviders = this.runtimeProvidersFactory?.() || []; element.runtimeProviders = this.getHtmlArtifactRuntimeProviders();
element.runtimeProviders = runtimeProviders;
} }
} }
@ -240,16 +269,15 @@ export class ArtifactsPanel extends LitElement {
// Build the AgentTool (no details payload; return only output strings) // Build the AgentTool (no details payload; return only output strings)
public get tool(): AgentTool<typeof artifactsParamsSchema, undefined> { public get tool(): AgentTool<typeof artifactsParamsSchema, undefined> {
const self = this;
return { return {
label: "Artifacts", label: "Artifacts",
name: "artifacts", name: "artifacts",
get description() { get description() {
const runtimeProviderDescriptions = // HTML artifacts have read-only access to attachments and artifacts
self const runtimeProviderDescriptions = [
.runtimeProvidersFactory?.() ATTACHMENTS_RUNTIME_DESCRIPTION,
.map((d) => d.getDescription()) ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION_RO,
.filter((d) => d.trim().length > 0) || []; ];
return ARTIFACTS_TOOL_DESCRIPTION(runtimeProviderDescriptions); return ARTIFACTS_TOOL_DESCRIPTION(runtimeProviderDescriptions);
}, },
parameters: artifactsParamsSchema, parameters: artifactsParamsSchema,
@ -282,7 +310,6 @@ export class ArtifactsPanel extends LitElement {
// 2) Build an ordered list of successful artifact operations // 2) Build an ordered list of successful artifact operations
const operations: Array<ArtifactsParams> = []; const operations: Array<ArtifactsParams> = [];
for (const m of messages) { for (const m of messages) {
// Handle artifact messages (from programmatic operations like browser_javascript)
if ((m as any).role === "artifact") { if ((m as any).role === "artifact") {
const artifactMsg = m as ArtifactMessage; const artifactMsg = m as ArtifactMessage;
switch (artifactMsg.action) { 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 agent = JSON.parse(readFileSync(join(packagesDir, 'agent/package.json'), 'utf8'));
const pods = JSON.parse(readFileSync(join(packagesDir, 'pods/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 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('Current versions:');
console.log(` @mariozechner/pi-tui: ${tui.version}`); console.log(` @mariozechner/pi-tui: ${tui.version}`);
console.log(` @mariozechner/pi-agent: ${agent.version}`); console.log(` @mariozechner/pi-agent: ${agent.version}`);
console.log(` @mariozechner/pi: ${pods.version}`); console.log(` @mariozechner/pi: ${pods.version}`);
console.log(` @mariozechner/pi-web-ui: ${webUi.version}`); console.log(` @mariozechner/pi-web-ui: ${webUi.version}`);
console.log(` @mariozechner/pi-reader-extension: ${browserExtension.version}`);
// Update agent's dependency on tui // Update agent's dependency on tui
if (agent.dependencies['@mariozechner/pi-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}`); 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']) { // Update web-ui's dependency on tui
const oldVersion = browserExtension.dependencies['@mariozechner/pi-web-ui']; if (webUi.dependencies['@mariozechner/pi-tui']) {
browserExtension.dependencies['@mariozechner/pi-web-ui'] = `^${webUi.version}`; const oldVersion = webUi.dependencies['@mariozechner/pi-tui'];
writeFileSync(join(packagesDir, 'browser-extension/package.json'), JSON.stringify(browserExtension, null, '\t') + '\n'); webUi.dependencies['@mariozechner/pi-tui'] = `^${tui.version}`;
console.log(`Updated browser-extension's dependency on pi-web-ui: ${oldVersion} → ^${webUi.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!'); console.log('\n✅ Version sync complete!');