diff --git a/package-lock.json b/package-lock.json index d1e94ce9..da0c16f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/agent/package.json b/packages/agent/package.json index 81397279..7c757e33 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -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", diff --git a/packages/coding-agent/DEBUG.md b/packages/coding-agent/DEBUG.md new file mode 100644 index 00000000..e90126f0 --- /dev/null +++ b/packages/coding-agent/DEBUG.md @@ -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-.log` - TUI rendering events +- `/tmp/agent-debug-.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 diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 7b2194e4..70415d4f 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -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" }, diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 25832182..4984d5d1 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -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 { + 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 { + 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()}`)); } diff --git a/packages/coding-agent/src/tui-renderer.ts b/packages/coding-agent/src/tui-renderer.ts new file mode 100644 index 00000000..62616bb7 --- /dev/null +++ b/packages/coding-agent/src/tui-renderer.ts @@ -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 { + 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 { + 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 { + 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; + } + } +} diff --git a/packages/pods/package.json b/packages/pods/package.json index 4774d29f..531dc469 100644 --- a/packages/pods/package.json +++ b/packages/pods/package.json @@ -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": {} diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 921d5420..ea96f254 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -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" } diff --git a/packages/web-ui/scripts/count-prompt-tokens.ts b/packages/web-ui/scripts/count-prompt-tokens.ts new file mode 100644 index 00000000..04c01bb0 --- /dev/null +++ b/packages/web-ui/scripts/count-prompt-tokens.ts @@ -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 { + 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(); diff --git a/packages/web-ui/src/ChatPanel.ts b/packages/web-ui/src/ChatPanel.ts index 97afb995..9999868c 100644 --- a/packages/web-ui/src/ChatPanel.ts +++ b/packages/web-ui/src/ChatPanel.ts @@ -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; diff --git a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts index 3fc4a5e5..ad59dfa1 100644 --- a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts @@ -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 { @@ -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; } } diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index 579f323e..eccb7c11 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -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 diff --git a/packages/web-ui/src/prompts/prompts.ts b/packages/web-ui/src/prompts/prompts.ts index 73873aba..c11e0c48 100644 --- a/packages/web-ui/src/prompts/prompts.ts +++ b/packages/web-ui/src/prompts/prompts.ts @@ -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: -- 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