{"_id":"@cargo-cult/pi-web-ui","_rev":"5-50feed2a1fe42ff3cb061c3e21a21094","name":"@cargo-cult/pi-web-ui","dist-tags":{"latest":"0.47.0"},"versions":{"0.40.1":{"name":"@cargo-cult/pi-web-ui","version":"0.40.1","keywords":["ai","chat","ui","components","llm","web-components","mini-lit"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-web-ui@0.40.1","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"dist":{"shasum":"37c76d70d2490a4ac7d09b5c2f50316deb250042","tarball":"https://registry.npmjs.org/@cargo-cult/pi-web-ui/-/pi-web-ui-0.40.1.tgz","fileCount":371,"integrity":"sha512-xyNG3GRusUemqwcmdFXGu587pBbgfhD4svVv0scESq1xB6nTFIbPfW+4Hkmn6eziHeLMS+rXPXxOm8HsEYgUqg==","signatures":[{"sig":"MEYCIQDXRiirU+JhPBwY59cKbV87cYK6weJDZE7tgnnFwLnGUwIhANoj8lPhDcXo0dZDYBNHc0BuamGtrPOVtGjTfiKwijTz","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-web-ui@0.40.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1708669},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":"./dist/index.js","./app.css":"./dist/app.css"},"gitHead":"4d64ff64d64172c9c7ac87db756bf6044503396f","scripts":{"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\"","build":"tsgo -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify","check":"biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit","clean":"rm -rf dist","dev:tsc":"concurrently --names \"build\" --prefix-colors \"cyan\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\""},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/web-ui"},"_npmVersion":"10.9.4","description":"Reusable web UI components for AI chat interfaces powered by @cargo-cult/pi-ai","directories":{},"_nodeVersion":"22.21.1","dependencies":{"xlsx":"https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz","jszip":"^3.10.1","lucide":"^0.544.0","ollama":"^0.6.0","pdfjs-dist":"5.4.394","docx-preview":"^0.3.7","@lmstudio/sdk":"^1.5.0","@cargo-cult/pi-ai":"^0.40.1","@cargo-cult/pi-tui":"^0.40.1"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.7.3","concurrently":"^9.2.1","@tailwindcss/cli":"^4.0.0-beta.14","@mariozechner/mini-lit":"^0.2.0"},"peerDependencies":{"lit":"^3.3.1","@mariozechner/mini-lit":"^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-web-ui_0.40.1_1767984146516_0.2827287982374287","host":"s3://npm-registry-packages-npm-production"}},"0.42.1":{"name":"@cargo-cult/pi-web-ui","version":"0.42.1","keywords":["ai","chat","ui","components","llm","web-components","mini-lit"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-web-ui@0.42.1","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"dist":{"shasum":"f191c7e3075ac81b5706f69ff39b5d8feb13280d","tarball":"https://registry.npmjs.org/@cargo-cult/pi-web-ui/-/pi-web-ui-0.42.1.tgz","fileCount":371,"integrity":"sha512-1xSiNkj6oeNKqVpHnoSPPgSS/bYH9GYPqGINRk1y/7ahNPH9GyzIxIJlQnjp+9dO1Y+SQcro7b1b8cw4nDQVOA==","signatures":[{"sig":"MEUCIQCuASSRyvL7Ezy6lcO3q07yfq1D8MshHP1l+iLoN+hsugIgEKn5I0dEs4GjwC8C0AO1aichjWPxclt/TshzKeSKhyY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-web-ui@0.42.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1708747},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":"./dist/index.js","./app.css":"./dist/app.css"},"gitHead":"5198ee434057587f815953b57e154b8f71e1db3c","scripts":{"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\"","build":"tsgo -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify","check":"biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit","clean":"rm -rf dist","dev:tsc":"concurrently --names \"build\" --prefix-colors \"cyan\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\""},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/web-ui"},"_npmVersion":"10.9.4","description":"Reusable web UI components for AI chat interfaces powered by @cargo-cult/pi-ai","directories":{},"_nodeVersion":"22.21.1","dependencies":{"xlsx":"https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz","jszip":"^3.10.1","lucide":"^0.544.0","ollama":"^0.6.0","pdfjs-dist":"5.4.394","docx-preview":"^0.3.7","@lmstudio/sdk":"^1.5.0","@cargo-cult/pi-ai":"^0.42.1","@cargo-cult/pi-tui":"^0.42.1"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.7.3","concurrently":"^9.2.1","@tailwindcss/cli":"^4.0.0-beta.14","@mariozechner/mini-lit":"^0.2.0"},"peerDependencies":{"lit":"^3.3.1","@mariozechner/mini-lit":"^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-web-ui_0.42.1_1767986281690_0.3659009274542071","host":"s3://npm-registry-packages-npm-production"}},"0.45.3":{"name":"@cargo-cult/pi-web-ui","version":"0.45.3","keywords":["ai","chat","ui","components","llm","web-components","mini-lit"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-web-ui@0.45.3","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"dist":{"shasum":"1590639950230cae7faa142bae98951dcda44dc4","tarball":"https://registry.npmjs.org/@cargo-cult/pi-web-ui/-/pi-web-ui-0.45.3.tgz","fileCount":371,"integrity":"sha512-5fPGDj+GyI9yCxyFJ5RSvqMxJ5KbjHi+j++pEwjX4pxpePEiU9oDuPmyb55Q8R03JiS/AX0h1Ia1qp0R1HSKsQ==","signatures":[{"sig":"MEUCIQD/lUASNrasP1Ss8JYgLupd/qUifv1Fs0tFu/wb/Tg5tgIgPS1Hr9U5NsnaPFlnvC27c7CDU7CwHbB0+mC0gLHLo4I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-web-ui@0.45.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1709171},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":"./dist/index.js","./app.css":"./dist/app.css"},"gitHead":"c565ce93e848a6a833849cebebec6178585abed7","scripts":{"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\"","build":"tsgo -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify","check":"biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit","clean":"rm -rf dist","dev:tsc":"concurrently --names \"build\" --prefix-colors \"cyan\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\""},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/web-ui"},"_npmVersion":"10.9.4","description":"Reusable web UI components for AI chat interfaces powered by @cargo-cult/pi-ai","directories":{},"_nodeVersion":"22.21.1","dependencies":{"xlsx":"https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz","jszip":"^3.10.1","lucide":"^0.544.0","ollama":"^0.6.0","pdfjs-dist":"5.4.394","docx-preview":"^0.3.7","@lmstudio/sdk":"^1.5.0","@cargo-cult/pi-ai":"^0.45.3","@cargo-cult/pi-tui":"^0.45.3"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.7.3","concurrently":"^9.2.1","@tailwindcss/cli":"^4.0.0-beta.14","@mariozechner/mini-lit":"^0.2.0"},"peerDependencies":{"lit":"^3.3.1","@mariozechner/mini-lit":"^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-web-ui_0.45.3_1768326684669_0.024061147448688258","host":"s3://npm-registry-packages-npm-production"}},"0.45.7":{"name":"@cargo-cult/pi-web-ui","version":"0.45.7","keywords":["ai","chat","ui","components","llm","web-components","mini-lit"],"author":{"name":"Mario Zechner"},"license":"MIT","_id":"@cargo-cult/pi-web-ui@0.45.7","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"homepage":"https://github.com/TUstudents/pi-mono#readme","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"dist":{"shasum":"5c1a2cdd09ceea52daa90d18df964fe481f82c61","tarball":"https://registry.npmjs.org/@cargo-cult/pi-web-ui/-/pi-web-ui-0.45.7.tgz","fileCount":371,"integrity":"sha512-9463+HNDZs/TNIUVRJgbSDzUKxXcdUlIaQJEkYR2ozSX6rdcOjZBA8o9nM65ZQljuUo6FJ9lg7FuQM4sVCAEyQ==","signatures":[{"sig":"MEUCIQDIdhcIHmZ7rAkCELnYeviSH8fhDTz/yZsPn34I+8/F1gIgWujbD955BERCpoaYhec8lbfQAITA3w1XyE643mg6eP0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-web-ui@0.45.7","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1709275},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","exports":{".":"./dist/index.js","./app.css":"./dist/app.css"},"gitHead":"0854b993f8933023232e238f1e8eb14c6768817f","scripts":{"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\"","build":"tsgo -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify","check":"biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit","clean":"rm -rf dist","dev:tsc":"concurrently --names \"build\" --prefix-colors \"cyan\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\""},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"repository":{"url":"git+https://github.com/TUstudents/pi-mono.git","type":"git","directory":"packages/web-ui"},"_npmVersion":"10.9.4","description":"Reusable web UI components for AI chat interfaces powered by @cargo-cult/pi-ai","directories":{},"_nodeVersion":"22.21.1","dependencies":{"xlsx":"https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz","jszip":"^3.10.1","lucide":"^0.544.0","ollama":"^0.6.0","pdfjs-dist":"5.4.394","docx-preview":"^0.3.7","@lmstudio/sdk":"^1.5.0","@cargo-cult/pi-ai":"^0.45.7","@cargo-cult/pi-tui":"^0.45.7"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.7.3","concurrently":"^9.2.1","@tailwindcss/cli":"^4.0.0-beta.14","@mariozechner/mini-lit":"^0.2.0"},"peerDependencies":{"lit":"^3.3.1","@mariozechner/mini-lit":"^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/pi-web-ui_0.45.7_1768473936506_0.5760298393268073","host":"s3://npm-registry-packages-npm-production"}},"0.47.0":{"name":"@cargo-cult/pi-web-ui","version":"0.47.0","description":"Reusable web UI components for AI chat interfaces powered by @cargo-cult/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":"tsgo -p tsconfig.build.json && tailwindcss -i ./src/app.css -o ./dist/app.css --minify","dev":"concurrently --names \"build,example\" --prefix-colors \"cyan,green\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\" \"npm run dev --prefix example\"","dev:tsc":"concurrently --names \"build\" --prefix-colors \"cyan\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\" \"tailwindcss -i ./src/app.css -o ./dist/app.css --watch\"","check":"biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit"},"dependencies":{"@lmstudio/sdk":"^1.5.0","@cargo-cult/pi-ai":"^0.47.0","@cargo-cult/pi-tui":"^0.47.0","docx-preview":"^0.3.7","jszip":"^3.10.1","lucide":"^0.544.0","ollama":"^0.6.0","pdfjs-dist":"5.4.394","xlsx":"https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"},"peerDependencies":{"@mariozechner/mini-lit":"^0.2.0","lit":"^3.3.1"},"devDependencies":{"@mariozechner/mini-lit":"^0.2.0","@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":{"name":"Mario Zechner"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/TUstudents/pi-mono.git","directory":"packages/web-ui"},"_id":"@cargo-cult/pi-web-ui@0.47.0","gitHead":"639988d4bcbe25693775ce39cd905382eb58b7d4","bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"homepage":"https://github.com/TUstudents/pi-mono#readme","_nodeVersion":"22.21.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-gxlBfMuSOuf13B2GA5hY+2fw4zmMniYt+uKlp0k0z/QysJ/U0qoHSUVyCwp15wEOTq/2TedYEQw5jB6y+xDqhg==","shasum":"e56a4d0992e823b1ccd5fe6fe75689fbcf54d432","tarball":"https://registry.npmjs.org/@cargo-cult/pi-web-ui/-/pi-web-ui-0.47.0.tgz","fileCount":371,"unpackedSize":1709327,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@cargo-cult%2fpi-web-ui@0.47.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCEuVs6Th/+Aop3CZi9Y8XLh89MbtMjmTCRSB6DyKgKJwIgQxvJVCJFZ/Y1S80CL5mE2WaoaUtGjxsWCWpejXxYC6Y="}]},"_npmUser":{"name":"tustudents","email":"python@atoms.eu"},"directories":{},"maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/pi-web-ui_0.47.0_1768544544831_0.13197492172582237"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-09T18:42:26.398Z","modified":"2026-01-16T06:22:25.298Z","0.40.1":"2026-01-09T18:42:26.708Z","0.42.1":"2026-01-09T19:18:01.867Z","0.45.3":"2026-01-13T17:51:24.863Z","0.45.7":"2026-01-15T10:45:36.645Z","0.47.0":"2026-01-16T06:22:25.011Z"},"bugs":{"url":"https://github.com/TUstudents/pi-mono/issues"},"author":{"name":"Mario Zechner"},"license":"MIT","homepage":"https://github.com/TUstudents/pi-mono#readme","keywords":["ai","chat","ui","components","llm","web-components","mini-lit"],"repository":{"type":"git","url":"git+https://github.com/TUstudents/pi-mono.git","directory":"packages/web-ui"},"description":"Reusable web UI components for AI chat interfaces powered by @cargo-cult/pi-ai","maintainers":[{"name":"tustudents","email":"python@atoms.eu"}],"readme":"# @cargo-cult/pi-web-ui\n\nReusable web UI components for building AI chat interfaces powered by [@cargo-cult/pi-ai](../ai) and [@cargo-cult/pi-agent-core](../agent).\n\nBuilt with [mini-lit](https://github.com/badlogic/mini-lit) web components and Tailwind CSS v4.\n\n## Features\n\n- **Chat UI**: Complete interface with message history, streaming, and tool execution\n- **Tools**: JavaScript REPL, document extraction, and artifacts (HTML, SVG, Markdown, etc.)\n- **Attachments**: PDF, DOCX, XLSX, PPTX, images with preview and text extraction\n- **Artifacts**: Interactive HTML, SVG, Markdown with sandboxed execution\n- **Storage**: IndexedDB-backed storage for sessions, API keys, and settings\n- **CORS Proxy**: Automatic proxy handling for browser environments\n- **Custom Providers**: Support for Ollama, LM Studio, vLLM, and OpenAI-compatible APIs\n\n## Installation\n\n```bash\nnpm install @cargo-cult/pi-web-ui @cargo-cult/pi-agent-core @cargo-cult/pi-ai\n```\n\n## Quick Start\n\nSee the [example](./example) directory for a complete working application.\n\n```typescript\nimport { Agent } from '@cargo-cult/pi-agent-core';\nimport { getModel } from '@cargo-cult/pi-ai';\nimport {\n  ChatPanel,\n  AppStorage,\n  IndexedDBStorageBackend,\n  ProviderKeysStore,\n  SessionsStore,\n  SettingsStore,\n  setAppStorage,\n  defaultConvertToLlm,\n  ApiKeyPromptDialog,\n} from '@cargo-cult/pi-web-ui';\nimport '@cargo-cult/pi-web-ui/app.css';\n\n// Set up storage\nconst settings = new SettingsStore();\nconst providerKeys = new ProviderKeysStore();\nconst sessions = new SessionsStore();\n\nconst backend = new IndexedDBStorageBackend({\n  dbName: 'my-app',\n  version: 1,\n  stores: [\n    settings.getConfig(),\n    providerKeys.getConfig(),\n    sessions.getConfig(),\n    SessionsStore.getMetadataConfig(),\n  ],\n});\n\nsettings.setBackend(backend);\nproviderKeys.setBackend(backend);\nsessions.setBackend(backend);\n\nconst storage = new AppStorage(settings, providerKeys, sessions, undefined, backend);\nsetAppStorage(storage);\n\n// Create agent\nconst agent = new Agent({\n  initialState: {\n    systemPrompt: 'You are a helpful assistant.',\n    model: getModel('anthropic', 'claude-sonnet-4-5-20250929'),\n    thinkingLevel: 'off',\n    messages: [],\n    tools: [],\n  },\n  convertToLlm: defaultConvertToLlm,\n});\n\n// Create chat panel\nconst chatPanel = new ChatPanel();\nawait chatPanel.setAgent(agent, {\n  onApiKeyRequired: (provider) => ApiKeyPromptDialog.prompt(provider),\n});\n\ndocument.body.appendChild(chatPanel);\n```\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────┐\n│                    ChatPanel                         │\n│  ┌─────────────────────┐  ┌─────────────────────┐   │\n│  │   AgentInterface    │  │   ArtifactsPanel    │   │\n│  │  (messages, input)  │  │  (HTML, SVG, MD)    │   │\n│  └─────────────────────┘  └─────────────────────┘   │\n└─────────────────────────────────────────────────────┘\n                          │\n                          ▼\n┌─────────────────────────────────────────────────────┐\n│              Agent (from pi-agent-core)              │\n│  - State management (messages, model, tools)         │\n│  - Event emission (agent_start, message_update, ...) │\n│  - Tool execution                                    │\n└─────────────────────────────────────────────────────┘\n                          │\n                          ▼\n┌─────────────────────────────────────────────────────┐\n│                   AppStorage                         │\n│  ┌──────────┐ ┌──────────┐ ┌──────────┐            │\n│  │ Settings │ │ Provider │ │ Sessions │            │\n│  │  Store   │ │Keys Store│ │  Store   │            │\n│  └──────────┘ └──────────┘ └──────────┘            │\n│                     │                               │\n│              IndexedDBStorageBackend                │\n└─────────────────────────────────────────────────────┘\n```\n\n## Components\n\n### ChatPanel\n\nHigh-level chat interface with built-in artifacts panel.\n\n```typescript\nconst chatPanel = new ChatPanel();\nawait chatPanel.setAgent(agent, {\n  // Prompt for API key when needed\n  onApiKeyRequired: async (provider) => ApiKeyPromptDialog.prompt(provider),\n\n  // Hook before sending messages\n  onBeforeSend: async () => { /* save draft, etc. */ },\n\n  // Handle cost display click\n  onCostClick: () => { /* show cost breakdown */ },\n\n  // Custom sandbox URL for browser extensions\n  sandboxUrlProvider: () => chrome.runtime.getURL('sandbox.html'),\n\n  // Add custom tools\n  toolsFactory: (agent, agentInterface, artifactsPanel, runtimeProvidersFactory) => {\n    const replTool = createJavaScriptReplTool();\n    replTool.runtimeProvidersFactory = runtimeProvidersFactory;\n    return [replTool];\n  },\n});\n```\n\n### AgentInterface\n\nLower-level chat interface for custom layouts.\n\n```typescript\nconst chat = document.createElement('agent-interface') as AgentInterface;\nchat.session = agent;\nchat.enableAttachments = true;\nchat.enableModelSelector = true;\nchat.enableThinkingSelector = true;\nchat.onApiKeyRequired = async (provider) => { /* ... */ };\nchat.onBeforeSend = async () => { /* ... */ };\n```\n\nProperties:\n- `session`: Agent instance\n- `enableAttachments`: Show attachment button (default: true)\n- `enableModelSelector`: Show model selector (default: true)\n- `enableThinkingSelector`: Show thinking level selector (default: true)\n- `showThemeToggle`: Show theme toggle (default: false)\n\n### Agent (from pi-agent-core)\n\n```typescript\nimport { Agent } from '@cargo-cult/pi-agent-core';\n\nconst agent = new Agent({\n  initialState: {\n    model: getModel('anthropic', 'claude-sonnet-4-5-20250929'),\n    systemPrompt: 'You are helpful.',\n    thinkingLevel: 'off',\n    messages: [],\n    tools: [],\n  },\n  convertToLlm: defaultConvertToLlm,\n});\n\n// Events\nagent.subscribe((event) => {\n  switch (event.type) {\n    case 'agent_start': // Agent loop started\n    case 'agent_end':   // Agent loop finished\n    case 'turn_start':  // LLM call started\n    case 'turn_end':    // LLM call finished\n    case 'message_start':\n    case 'message_update': // Streaming update\n    case 'message_end':\n      break;\n  }\n});\n\n// Send message\nawait agent.prompt('Hello!');\nawait agent.prompt({ role: 'user-with-attachments', content: 'Check this', attachments, timestamp: Date.now() });\n\n// Control\nagent.abort();\nagent.setModel(newModel);\nagent.setThinkingLevel('medium');\nagent.setTools([...]);\nagent.queueMessage(customMessage);\n```\n\n## Message Types\n\n### UserMessageWithAttachments\n\nUser message with file attachments:\n\n```typescript\nconst message: UserMessageWithAttachments = {\n  role: 'user-with-attachments',\n  content: 'Analyze this document',\n  attachments: [pdfAttachment],\n  timestamp: Date.now(),\n};\n\n// Type guard\nif (isUserMessageWithAttachments(msg)) {\n  console.log(msg.attachments);\n}\n```\n\n### ArtifactMessage\n\nFor session persistence of artifacts:\n\n```typescript\nconst artifact: ArtifactMessage = {\n  role: 'artifact',\n  action: 'create', // or 'update', 'delete'\n  filename: 'chart.html',\n  content: '<div>...</div>',\n  timestamp: new Date().toISOString(),\n};\n\n// Type guard\nif (isArtifactMessage(msg)) {\n  console.log(msg.filename);\n}\n```\n\n### Custom Message Types\n\nExtend via declaration merging:\n\n```typescript\ninterface SystemNotification {\n  role: 'system-notification';\n  message: string;\n  level: 'info' | 'warning' | 'error';\n  timestamp: string;\n}\n\ndeclare module '@cargo-cult/pi-agent-core' {\n  interface CustomAgentMessages {\n    'system-notification': SystemNotification;\n  }\n}\n\n// Register renderer\nregisterMessageRenderer('system-notification', {\n  render: (msg) => html`<div class=\"alert\">${msg.message}</div>`,\n});\n\n// Extend convertToLlm\nfunction myConvertToLlm(messages: AgentMessage[]): Message[] {\n  const processed = messages.map((m) => {\n    if (m.role === 'system-notification') {\n      return { role: 'user', content: `<system>${m.message}</system>`, timestamp: Date.now() };\n    }\n    return m;\n  });\n  return defaultConvertToLlm(processed);\n}\n```\n\n## Message Transformer\n\n`convertToLlm` transforms app messages to LLM-compatible format:\n\n```typescript\nimport { defaultConvertToLlm, convertAttachments } from '@cargo-cult/pi-web-ui';\n\n// defaultConvertToLlm handles:\n// - UserMessageWithAttachments → user message with image/text content blocks\n// - ArtifactMessage → filtered out (UI-only)\n// - Standard messages (user, assistant, toolResult) → passed through\n```\n\n## Tools\n\n### JavaScript REPL\n\nExecute JavaScript in a sandboxed browser environment:\n\n```typescript\nimport { createJavaScriptReplTool } from '@cargo-cult/pi-web-ui';\n\nconst replTool = createJavaScriptReplTool();\n\n// Configure runtime providers for artifact/attachment access\nreplTool.runtimeProvidersFactory = () => [\n  new AttachmentsRuntimeProvider(attachments),\n  new ArtifactsRuntimeProvider(artifactsPanel, agent, true), // read-write\n];\n\nagent.setTools([replTool]);\n```\n\n### Extract Document\n\nExtract text from documents at URLs:\n\n```typescript\nimport { createExtractDocumentTool } from '@cargo-cult/pi-web-ui';\n\nconst extractTool = createExtractDocumentTool();\nextractTool.corsProxyUrl = 'https://corsproxy.io/?';\n\nagent.setTools([extractTool]);\n```\n\n### Artifacts Tool\n\nBuilt into ArtifactsPanel, supports: HTML, SVG, Markdown, text, JSON, images, PDF, DOCX, XLSX.\n\n```typescript\nconst artifactsPanel = new ArtifactsPanel();\nartifactsPanel.agent = agent;\n\n// The tool is available as artifactsPanel.tool\nagent.setTools([artifactsPanel.tool]);\n```\n\n### Custom Tool Renderers\n\n```typescript\nimport { registerToolRenderer, type ToolRenderer } from '@cargo-cult/pi-web-ui';\n\nconst myRenderer: ToolRenderer = {\n  render(params, result, isStreaming) {\n    return {\n      content: html`<div>...</div>`,\n      isCustom: false, // true = no card wrapper\n    };\n  },\n};\n\nregisterToolRenderer('my_tool', myRenderer);\n```\n\n## Storage\n\n### Setup\n\n```typescript\nimport {\n  AppStorage,\n  IndexedDBStorageBackend,\n  SettingsStore,\n  ProviderKeysStore,\n  SessionsStore,\n  CustomProvidersStore,\n  setAppStorage,\n  getAppStorage,\n} from '@cargo-cult/pi-web-ui';\n\n// Create stores\nconst settings = new SettingsStore();\nconst providerKeys = new ProviderKeysStore();\nconst sessions = new SessionsStore();\nconst customProviders = new CustomProvidersStore();\n\n// Create backend with all store configs\nconst backend = new IndexedDBStorageBackend({\n  dbName: 'my-app',\n  version: 1,\n  stores: [\n    settings.getConfig(),\n    providerKeys.getConfig(),\n    sessions.getConfig(),\n    SessionsStore.getMetadataConfig(),\n    customProviders.getConfig(),\n  ],\n});\n\n// Wire stores to backend\nsettings.setBackend(backend);\nproviderKeys.setBackend(backend);\nsessions.setBackend(backend);\ncustomProviders.setBackend(backend);\n\n// Create and set global storage\nconst storage = new AppStorage(settings, providerKeys, sessions, customProviders, backend);\nsetAppStorage(storage);\n```\n\n### SettingsStore\n\nKey-value settings:\n\n```typescript\nawait storage.settings.set('proxy.enabled', true);\nawait storage.settings.set('proxy.url', 'https://proxy.example.com');\nconst enabled = await storage.settings.get<boolean>('proxy.enabled');\n```\n\n### ProviderKeysStore\n\nAPI keys by provider:\n\n```typescript\nawait storage.providerKeys.set('anthropic', 'sk-ant-...');\nconst key = await storage.providerKeys.get('anthropic');\nconst providers = await storage.providerKeys.list();\n```\n\n### SessionsStore\n\nChat sessions with metadata:\n\n```typescript\n// Save session\nawait storage.sessions.save(sessionData, metadata);\n\n// Load session\nconst data = await storage.sessions.get(sessionId);\nconst metadata = await storage.sessions.getMetadata(sessionId);\n\n// List sessions (sorted by lastModified)\nconst allMetadata = await storage.sessions.getAllMetadata();\n\n// Update title\nawait storage.sessions.updateTitle(sessionId, 'New Title');\n\n// Delete\nawait storage.sessions.delete(sessionId);\n```\n\n### CustomProvidersStore\n\nCustom LLM providers:\n\n```typescript\nconst provider: CustomProvider = {\n  id: crypto.randomUUID(),\n  name: 'My Ollama',\n  type: 'ollama',\n  baseUrl: 'http://localhost:11434',\n};\n\nawait storage.customProviders.set(provider);\nconst all = await storage.customProviders.getAll();\n```\n\n## Attachments\n\nLoad and process files:\n\n```typescript\nimport { loadAttachment, type Attachment } from '@cargo-cult/pi-web-ui';\n\n// From File input\nconst file = inputElement.files[0];\nconst attachment = await loadAttachment(file);\n\n// From URL\nconst attachment = await loadAttachment('https://example.com/doc.pdf');\n\n// From ArrayBuffer\nconst attachment = await loadAttachment(arrayBuffer, 'document.pdf');\n\n// Attachment structure\ninterface Attachment {\n  id: string;\n  type: 'image' | 'document';\n  fileName: string;\n  mimeType: string;\n  size: number;\n  content: string;        // base64 encoded\n  extractedText?: string; // For documents\n  preview?: string;       // base64 preview image\n}\n```\n\nSupported formats: PDF, DOCX, XLSX, PPTX, images, text files.\n\n## CORS Proxy\n\nFor browser environments with CORS restrictions:\n\n```typescript\nimport { createStreamFn, shouldUseProxyForProvider, isCorsError } from '@cargo-cult/pi-web-ui';\n\n// AgentInterface auto-configures proxy from settings\n// For manual setup:\nagent.streamFn = createStreamFn(async () => {\n  const enabled = await storage.settings.get<boolean>('proxy.enabled');\n  return enabled ? await storage.settings.get<string>('proxy.url') : undefined;\n});\n\n// Providers requiring proxy:\n// - zai: always\n// - anthropic: only OAuth tokens (sk-ant-oat-*)\n```\n\n## Dialogs\n\n### SettingsDialog\n\n```typescript\nimport { SettingsDialog, ProvidersModelsTab, ProxyTab, ApiKeysTab } from '@cargo-cult/pi-web-ui';\n\nSettingsDialog.open([\n  new ProvidersModelsTab(), // Custom providers + model list\n  new ProxyTab(),           // CORS proxy settings\n  new ApiKeysTab(),         // API keys per provider\n]);\n```\n\n### SessionListDialog\n\n```typescript\nimport { SessionListDialog } from '@cargo-cult/pi-web-ui';\n\nSessionListDialog.open(\n  async (sessionId) => { /* load session */ },\n  (deletedId) => { /* handle deletion */ },\n);\n```\n\n### ApiKeyPromptDialog\n\n```typescript\nimport { ApiKeyPromptDialog } from '@cargo-cult/pi-web-ui';\n\nconst success = await ApiKeyPromptDialog.prompt('anthropic');\n```\n\n### ModelSelector\n\n```typescript\nimport { ModelSelector } from '@cargo-cult/pi-web-ui';\n\nModelSelector.open(currentModel, (selectedModel) => {\n  agent.setModel(selectedModel);\n});\n```\n\n## Styling\n\nImport the pre-built CSS:\n\n```typescript\nimport '@cargo-cult/pi-web-ui/app.css';\n```\n\nOr use Tailwind with custom config:\n\n```css\n@import '@mariozechner/mini-lit/themes/claude.css';\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n## Internationalization\n\n```typescript\nimport { i18n, setLanguage, translations } from '@cargo-cult/pi-web-ui';\n\n// Add translations\ntranslations.de = {\n  'Loading...': 'Laden...',\n  'No sessions yet': 'Noch keine Sitzungen',\n};\n\nsetLanguage('de');\nconsole.log(i18n('Loading...')); // \"Laden...\"\n```\n\n## Examples\n\n- [example/](./example) - Complete web app with sessions, artifacts, custom messages\n- [sitegeist](https://sitegeist.ai) - Browser extension using pi-web-ui\n\n## Known Issues\n\n- **PersistentStorageDialog**: Currently broken\n\n## License\n\nMIT\n","readmeFilename":"README.md"}