mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 03:00:44 +00:00
Updates to prompts
This commit is contained in:
parent
ffc9be8867
commit
2a7ccf0fcb
15 changed files with 911 additions and 271 deletions
55
package-lock.json
generated
55
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
170
packages/coding-agent/DEBUG.md
Normal file
170
packages/coding-agent/DEBUG.md
Normal 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
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()}`));
|
||||
}
|
||||
|
|
|
|||
308
packages/coding-agent/src/tui-renderer.ts
Normal file
308
packages/coding-agent/src/tui-renderer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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": {}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
88
packages/web-ui/scripts/count-prompt-tokens.ts
Normal file
88
packages/web-ui/scripts/count-prompt-tokens.ts
Normal 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();
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.`;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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!');
|
||||
Loading…
Add table
Add a link
Reference in a new issue