diff --git a/package.json b/package.json index 4318a19a..715c5ff1 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ ], "scripts": { "clean": "npm run clean --workspaces", - "build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/pi-ai && npm run build -w @mariozechner/pi-web-ui && npm run build -w @mariozechner/pi-reader-extension && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/pi-proxy && npm run build -w @mariozechner/pi", - "dev": "concurrently --names \"ai,web-ui,browser-ext,tui,proxy\" --prefix-colors \"cyan,green,yellow,magenta,blue\" \"npm run dev -w @mariozechner/pi-ai\" \"npm run dev -w @mariozechner/pi-web-ui\" \"npm run dev -w @mariozechner/pi-reader-extension\" \"npm run dev -w @mariozechner/pi-tui\" \"npm run dev -w @mariozechner/pi-proxy\"", + "build": "npm run build -w @mariozechner/pi-tui && npm run build -w @mariozechner/pi-ai && npm run build -w @mariozechner/pi-web-ui && npm run build -w @mariozechner/pi-agent && npm run build -w @mariozechner/pi-proxy && npm run build -w @mariozechner/pi", + "dev": "concurrently --names \"ai,web-ui,tui,proxy\" --prefix-colors \"cyan,green,magenta,blue\" \"npm run dev -w @mariozechner/pi-ai\" \"npm run dev -w @mariozechner/pi-web-ui\" \"npm run dev -w @mariozechner/pi-tui\" \"npm run dev -w @mariozechner/pi-proxy\"", "check": "biome check --write . && npm run check --workspaces && tsc --noEmit", "test": "npm run test --workspaces --if-present", "version:patch": "npm version patch -ws --no-git-tag-version && node scripts/sync-versions.js", diff --git a/packages/browser-extension/README.md b/packages/browser-extension/README.md deleted file mode 100644 index 06fb7e3f..00000000 --- a/packages/browser-extension/README.md +++ /dev/null @@ -1,1188 +0,0 @@ -# Pi Reader Browser Extension - -A cross-browser extension that provides an AI-powered reading assistant in a side panel (Chrome/Edge) or sidebar (Firefox), built with mini-lit components and Tailwind CSS v4. - -## Browser Support - -- **Chrome/Edge** - Uses Side Panel API (Manifest V3) -- **Firefox** - Uses Sidebar Action API (Manifest V2) -- **Opera** - Sidebar support (untested but should work with Firefox manifest) - -## Architecture - -### High-Level Overview - -The extension is a full-featured AI chat interface that runs in your browser's side panel/sidebar. It can communicate with AI providers in two ways: - -1. **Direct Mode** (default) - Calls AI provider APIs directly from the browser using API keys stored locally -2. **Proxy Mode** - Routes requests through a proxy server using an auth token - -**Browser Adaptation:** -- **Chrome/Edge** - Side Panel API for dedicated panel UI, Manifest V3 -- **Firefox** - Sidebar Action API for sidebar UI, Manifest V2 -- **Page Content Access** - Uses `chrome.scripting.executeScript` to extract page text -- **Cross-browser APIs** - Uses `browser.*` (Firefox) and `chrome.*` (Chrome/Edge) via runtime detection - -### Core Architecture Layers - -``` -┌─────────────────────────────────────────────────────────────┐ -│ UI Layer (sidepanel.ts) │ -│ ├─ Header (theme toggle, settings) │ -│ └─ ChatPanel │ -│ └─ AgentInterface (main chat UI) │ -│ ├─ MessageList (stable messages) │ -│ ├─ StreamingMessageContainer (live updates) │ -│ └─ MessageEditor (input + attachments) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ State Layer (state/) │ -│ └─ AgentSession │ -│ ├─ Manages conversation state │ -│ ├─ Coordinates transport │ -│ └─ Handles tool execution │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ Transport Layer (state/transports/) │ -│ ├─ DirectTransport (uses KeyStore for API keys) │ -│ └─ ProxyTransport (uses auth token + proxy server) │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ AI Provider APIs / Proxy Server │ -│ (Anthropic, OpenAI, Google, etc.) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Directory Structure by Responsibility - -``` -src/ -├── UI Components (what users see) -│ ├── sidepanel.ts # App entry point, header -│ ├── ChatPanel.ts # Main chat container, creates AgentSession -│ ├── AgentInterface.ts # Complete chat UI (messages + input) -│ ├── MessageList.ts # Renders stable messages -│ ├── StreamingMessageContainer.ts # Handles streaming updates -│ ├── Messages.ts # Message components (user, assistant, tool) -│ ├── MessageEditor.ts # Input field with attachments -│ ├── ConsoleBlock.ts # Console-style output display -│ ├── AttachmentTile.ts # Attachment preview thumbnails -│ ├── AttachmentOverlay.ts # Full-screen attachment viewer -│ └── ModeToggle.ts # Toggle between document/text view -│ -├── Components (reusable utilities) -│ └── components/ -│ └── SandboxedIframe.ts # Sandboxed HTML renderer with console capture -│ -├── Dialogs (modal interactions) -│ ├── dialogs/ -│ │ ├── DialogBase.ts # Base class for all dialogs -│ │ ├── ModelSelector.ts # Select AI model -│ │ ├── ApiKeysDialog.ts # Manage API keys (for direct mode) -│ │ └── PromptDialog.ts # Simple text input dialog -│ -├── State Management (business logic) -│ ├── state/ -│ │ ├── agent-session.ts # Core state manager (pub/sub pattern) -│ │ ├── KeyStore.ts # Cross-browser API key storage -│ │ └── transports/ -│ │ ├── types.ts # Transport interface definitions -│ │ ├── DirectTransport.ts # Direct API calls -│ │ └── ProxyTransport.ts # Proxy server calls -│ -├── Tools (AI function calling) -│ ├── tools/ -│ │ ├── types.ts # ToolRenderer interface -│ │ ├── renderer-registry.ts # Global tool renderer registry -│ │ ├── index.ts # Tool exports and registration -│ │ ├── browser-javascript.ts # Execute JS in current tab -│ │ ├── renderers/ # Custom tool UI renderers -│ │ │ ├── DefaultRenderer.ts # Fallback for unknown tools -│ │ │ ├── CalculateRenderer.ts # Calculator tool UI -│ │ │ ├── GetCurrentTimeRenderer.ts -│ │ │ └── BashRenderer.ts # Bash command execution UI -│ │ └── artifacts/ # Artifact tools (HTML, Mermaid, etc.) -│ │ ├── ArtifactElement.ts # Base class for artifacts -│ │ ├── HtmlArtifact.ts # HTML artifact with sandboxed preview -│ │ └── MermaidArtifact.ts # Mermaid diagram rendering -│ -├── Utilities (shared helpers) -│ └── utils/ -│ ├── attachment-utils.ts # PDF, Office, image processing -│ ├── auth-token.ts # Proxy auth token management -│ ├── format.ts # Token usage, cost formatting -│ └── i18n.ts # Internationalization (EN + DE) -│ -└── Entry Points (browser integration) - ├── background.ts # Service worker (opens side panel) - ├── sidepanel.html # HTML entry point - ├── sandbox.html # Sandboxed page for artifact HTML - ├── sandbox.js # Sandbox environment setup - └── live-reload.ts # Hot reload during development -``` - ---- - -## Common Development Tasks - -### "I want to add a new AI tool" - -**Tools** are functions the AI can call (e.g., calculator, web search, code execution). Here's how to add one: - -#### 1. Define the Tool (use `@mariozechner/pi-ai`) - -Tools come from the `@mariozechner/pi-ai` package. Use existing tools or create custom ones: - -```typescript -// src/tools/my-custom-tool.ts -import type { AgentTool } from "@mariozechner/pi-ai"; - -export const myCustomTool: AgentTool = { - name: "my_custom_tool", - label: "My Custom Tool", - description: "Does something useful", - parameters: { - type: "object", - properties: { - input: { type: "string", description: "Input parameter" } - }, - required: ["input"] - }, - execute: async (params) => { - // Your tool logic here - const result = processInput(params.input); - return { - output: result, - details: { /* any structured data */ } - }; - } -}; -``` - -#### 2. Create a Custom Renderer (Optional) - -Renderers control how the tool appears in the chat. If you don't create one, `DefaultRenderer` will be used. - -```typescript -// src/tools/renderers/MyCustomRenderer.ts -import { html } from "@mariozechner/mini-lit"; -import type { ToolResultMessage } from "@mariozechner/pi-ai"; -import type { ToolRenderer } from "../types.js"; - -export class MyCustomRenderer implements ToolRenderer { - renderParams(params: any, isStreaming?: boolean) { - // Show tool call parameters (e.g., "Searching for: ") - - return html` -
- ${isStreaming ? "Processing..." : `Input: ${params.input}`} -
- `; - } - - renderResult(params: any, result: ToolResultMessage) { - // Show tool result (e.g., search results, calculation output) - if (result.isError) { - return html`
${result.output}
`; - } - return html` -
-
Result:
-
${result.output}
-
- `; - } -} -``` - -**Renderer Tips:** -- Use `ConsoleBlock` for command output (see `BashRenderer.ts`) -- Use `` for code/JSON (from `@mariozechner/mini-lit`) -- Use `` for markdown content -- Check `isStreaming` to show loading states - -#### 3. Register the Tool and Renderer - -```typescript -// src/tools/index.ts -import { myCustomTool } from "./my-custom-tool.js"; -import { MyCustomRenderer } from "./renderers/MyCustomRenderer.js"; -import { registerToolRenderer } from "./renderer-registry.js"; - -// Register the renderer -registerToolRenderer("my_custom_tool", new MyCustomRenderer()); - -// Export the tool so ChatPanel can use it -export { myCustomTool }; -``` - -#### 4. Add Tool to ChatPanel - -```typescript -// src/ChatPanel.ts -import { myCustomTool } from "./tools/index.js"; - -// In AgentSession constructor: -this.session = new AgentSession({ - initialState: { - tools: [calculateTool, getCurrentTimeTool, myCustomTool], // Add here - // ... - } -}); -``` - -**File Locations:** -- Tool definition: `src/tools/my-custom-tool.ts` -- Tool renderer: `src/tools/renderers/MyCustomRenderer.ts` -- Registration: `src/tools/index.ts` (register renderer) -- Integration: `src/ChatPanel.ts` (add to tools array) - ---- - -### "I want to change how messages are displayed" - -**Message components** control how conversations appear: - -- **User messages**: Edit `UserMessage` in [src/Messages.ts](src/Messages.ts) -- **Assistant messages**: Edit `AssistantMessage` in [src/Messages.ts](src/Messages.ts) -- **Tool call cards**: Edit `ToolMessage` in [src/Messages.ts](src/Messages.ts) -- **Markdown rendering**: Comes from `@mariozechner/mini-lit` (can't customize easily) -- **Code blocks**: Comes from `@mariozechner/mini-lit` (can't customize easily) - -**Example: Change user message styling** - -```typescript -// src/Messages.ts - in UserMessage component -render() { - return html` -
- - -
- `; -} -``` - ---- - -### "I want to add a new model provider" - -Models come from `@mariozechner/pi-ai`. The package supports: -- `anthropic` (Claude) -- `openai` (GPT) -- `google` (Gemini) -- `groq`, `cerebras`, `xai`, `openrouter`, etc. - -**To add a provider:** - -1. Ensure `@mariozechner/pi-ai` supports it (check package docs) -2. Add API key configuration in [src/dialogs/ApiKeysDialog.ts](src/dialogs/ApiKeysDialog.ts): - - Add provider to `PROVIDERS` array - - Add test model to `TEST_MODELS` object -3. Users can then select models via the model selector - -**No code changes needed** - the extension auto-discovers all models from `@mariozechner/pi-ai`. - ---- - -### "I want to modify the transport layer" - -**Transport** determines how requests reach AI providers: - -#### Direct Mode (Default) -- **File**: [src/state/transports/DirectTransport.ts](src/state/transports/DirectTransport.ts) -- **How it works**: Gets API keys from `KeyStore` → calls provider APIs directly -- **When to use**: Local development, no proxy server -- **Configuration**: API keys stored in Chrome local storage - -#### Proxy Mode -- **File**: [src/state/transports/ProxyTransport.ts](src/state/transports/ProxyTransport.ts) -- **How it works**: Gets auth token → sends request to proxy server → proxy calls providers -- **When to use**: Want to hide API keys, centralized auth, usage tracking -- **Configuration**: Auth token stored in localStorage, proxy URL hardcoded - -**Switch transport mode in ChatPanel:** - -```typescript -// src/ChatPanel.ts -this.session = new AgentSession({ - transportMode: "direct", // or "proxy" - authTokenProvider: async () => getAuthToken(), // Only needed for proxy - // ... -}); -``` - -**Proxy Server Requirements:** -- Must accept POST to `/api/stream` endpoint -- Request format: `{ model, context, options }` -- Response format: SSE stream with delta events -- See `ProxyTransport.ts` for expected event types - -**To add a new transport:** - -1. Create `src/state/transports/MyTransport.ts` -2. Implement `AgentTransport` interface: - ```typescript - async *run(userMessage, cfg, signal): AsyncIterable - ``` -3. Register in `ChatPanel.ts` constructor - ---- - -### "I want to change the system prompt" - -**System prompts** guide the AI's behavior. Change in [src/ChatPanel.ts](src/ChatPanel.ts): - -```typescript -// src/ChatPanel.ts -this.session = new AgentSession({ - initialState: { - systemPrompt: "You are a helpful AI assistant specialized in code review.", - // ... - } -}); -``` - -Or make it dynamic: - -```typescript -// Read from storage, settings dialog, etc. -const systemPrompt = await chrome.storage.local.get("system-prompt"); -``` - ---- - -### "I want to add attachment support for a new file type" - -**Attachment processing** happens in [src/utils/attachment-utils.ts](src/utils/attachment-utils.ts): - -1. **Add file type detection** in `loadAttachment()`: - ```typescript - if (mimeType === "application/my-format" || fileName.endsWith(".myext")) { - const { extractedText } = await processMyFormat(arrayBuffer, fileName); - return { id, type: "document", fileName, mimeType, content, extractedText }; - } - ``` - -2. **Add processor function**: - ```typescript - async function processMyFormat(buffer: ArrayBuffer, fileName: string) { - // Extract text from your format - const text = extractTextFromMyFormat(buffer); - return { extractedText: `\n${text}\n` }; - } - ``` - -3. **Update accepted types** in [src/MessageEditor.ts](src/MessageEditor.ts): - ```typescript - acceptedTypes = "image/*,application/pdf,.myext,..."; - ``` - -4. **Optional: Add preview support** in [src/AttachmentOverlay.ts](src/AttachmentOverlay.ts) - -**Supported formats:** -- Images: All image/* (preview support) -- PDF: Text extraction + thumbnail generation -- Office: DOCX, PPTX, XLSX (text extraction) -- Text: .txt, .md, .json, .xml, etc. - ---- - -### "I want to customize the UI theme" - -The extension uses the **Claude theme** from `@mariozechner/mini-lit`. Colors are defined via CSS variables: - -**Option 1: Override theme variables** -```css -/* src/app.css */ -@layer base { - :root { - --primary: 210 100% 50%; /* Custom blue */ - --radius: 0.5rem; - } -} -``` - -**Option 2: Use a different mini-lit theme** -```css -/* src/app.css */ -@import "@mariozechner/mini-lit/themes/default.css"; /* Instead of claude.css */ -``` - -**Available variables:** -- `--background`, `--foreground` - Base colors -- `--card`, `--card-foreground` - Card backgrounds -- `--primary`, `--primary-foreground` - Primary actions -- `--muted`, `--muted-foreground` - Secondary elements -- `--accent`, `--accent-foreground` - Hover states -- `--destructive` - Error/delete actions -- `--border`, `--input` - Border colors -- `--radius` - Border radius - ---- - -### "I want to add a new settings option" - -Settings currently managed via dialogs. To add persistent settings: - -#### 1. Create storage helpers - -```typescript -// src/utils/config.ts (create this file) -export async function getMySetting(): Promise { - const result = await chrome.storage.local.get("my-setting"); - return result["my-setting"] || "default-value"; -} - -export async function setMySetting(value: string): Promise { - await chrome.storage.local.set({ "my-setting": value }); -} -``` - -#### 2. Create or extend settings dialog - -```typescript -// src/dialogs/SettingsDialog.ts (create this file, similar to ApiKeysDialog) -// Add UI for your setting -// Call getMySetting() / setMySetting() on save -``` - -#### 3. Open from header - -```typescript -// src/sidepanel.ts - in settings button onClick -SettingsDialog.open(); -``` - -#### 4. Use in ChatPanel - -```typescript -// src/ChatPanel.ts -const mySetting = await getMySetting(); -this.session = new AgentSession({ - initialState: { /* use mySetting */ } -}); -``` - ---- - -### "I want to access the current page content" - -Page content extraction is in [src/sidepanel.ts](src/sidepanel.ts): - -```typescript -// Example: Get page text -const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); -const results = await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: () => document.body.innerText, -}); -const pageText = results[0].result; -``` - -**To use in chat:** -1. Extract page content in `ChatPanel` -2. Add to system prompt or first user message -3. Or create a tool that reads page content - -**Permissions required:** -- `activeTab` - Access current tab -- `scripting` - Execute scripts in pages -- Already configured in `manifest.*.json` - ---- - -## Transport Modes Explained - -### Direct Mode (Default) - -**Flow:** -``` -Browser Extension - → KeyStore (get API key) - → DirectTransport - → Provider API (Anthropic/OpenAI/etc.) - → Stream response back -``` - -**Pros:** -- No external dependencies -- Lower latency (direct connection) -- Works offline for API key management -- Full control over requests - -**Cons:** -- API keys stored in browser (secure, but local) -- Each user needs their own API keys -- CORS restrictions (some providers may not work) -- Can't track usage centrally - -**Setup:** -1. Open extension → Settings → Manage API Keys -2. Add keys for desired providers (Anthropic, OpenAI, etc.) -3. Select model and start chatting - -**Files involved:** -- [src/state/transports/DirectTransport.ts](src/state/transports/DirectTransport.ts) - Transport implementation -- [src/state/KeyStore.ts](src/state/KeyStore.ts) - Cross-browser API key storage -- [src/dialogs/ApiKeysDialog.ts](src/dialogs/ApiKeysDialog.ts) - API key UI - ---- - -### Proxy Mode - -**Flow:** -``` -Browser Extension - → Auth Token (from localStorage) - → ProxyTransport - → Proxy Server (https://genai.mariozechner.at or custom) - → Provider API - → Stream response back through proxy -``` - -**Pros:** -- No API keys in browser -- Centralized auth/usage tracking -- Can implement rate limiting, quotas -- Custom logic server-side -- No CORS issues - -**Cons:** -- Requires proxy server setup -- Additional network hop (latency) -- Dependency on proxy availability -- Need to manage auth tokens - -**Setup:** -1. Get auth token from proxy server admin -2. Extension prompts for token on first use -3. Token stored in localStorage -4. Start chatting (proxy handles provider APIs) - -**Proxy URL Configuration:** -Currently hardcoded in `ProxyTransport.ts`: -```typescript -const PROXY_URL = "https://genai.mariozechner.at"; -``` - -To make configurable: -1. Add storage helper in `utils/config.ts` -2. Add UI in SettingsDialog -3. Pass to ProxyTransport constructor - -**Proxy Server Requirements:** - -The proxy server must implement: - -**Endpoint:** `POST /api/stream` - -**Request:** -```typescript -{ - model: Model, // Provider + model ID - context: Context, // System prompt, messages, tools - options: { - temperature?: number, - maxTokens?: number, - reasoning?: string - } -} -``` - -**Response:** SSE (Server-Sent Events) stream - -**Event Types:** -```typescript -data: {"type":"start","partial":{...}} -data: {"type":"text_start","contentIndex":0} -data: {"type":"text_delta","contentIndex":0,"delta":"Hello"} -data: {"type":"text_end","contentIndex":0,"contentSignature":"..."} -data: {"type":"thinking_start","contentIndex":1} -data: {"type":"thinking_delta","contentIndex":1,"delta":"..."} -data: {"type":"toolcall_start","contentIndex":2,"id":"...","toolName":"..."} -data: {"type":"toolcall_delta","contentIndex":2,"delta":"..."} -data: {"type":"toolcall_end","contentIndex":2} -data: {"type":"done","reason":"stop","usage":{...}} -``` - -**Auth:** Bearer token in `Authorization` header - -**Error Handling:** -- Return 401 for invalid auth → extension clears token and re-prompts -- Return 4xx/5xx with JSON: `{"error":"message"}` - -**Reference Implementation:** -See [src/state/transports/ProxyTransport.ts](src/state/transports/ProxyTransport.ts) for full event parsing logic. - ---- - -### Switching Between Modes - -**At runtime** (in ChatPanel): -```typescript -const mode = await getTransportMode(); // "direct" or "proxy" -this.session = new AgentSession({ - transportMode: mode, - authTokenProvider: mode === "proxy" ? async () => getAuthToken() : undefined, - // ... -}); -``` - -**Storage helpers** (create these): -```typescript -// src/utils/config.ts -export type TransportMode = "direct" | "proxy"; - -export async function getTransportMode(): Promise { - const result = await chrome.storage.local.get("transport-mode"); - return (result["transport-mode"] as TransportMode) || "direct"; -} - -export async function setTransportMode(mode: TransportMode): Promise { - await chrome.storage.local.set({ "transport-mode": mode }); -} -``` - -**UI for switching** (create this): -```typescript -// src/dialogs/SettingsDialog.ts -// Radio buttons: ○ Direct (use API keys) / ○ Proxy (use auth token) -// On save: setTransportMode(), reload AgentSession -``` - ---- - -## Understanding mini-lit - -Before working on the UI, read these files to understand the component library: - -- `node_modules/@mariozechner/mini-lit/README.md` - Complete component documentation -- `node_modules/@mariozechner/mini-lit/llms.txt` - LLM-friendly component reference -- `node_modules/@mariozechner/mini-lit/dist/*.ts` - Source files for specific components - -Key concepts: -- **Functional Components** - Stateless functions that return `TemplateResult` (Button, Badge, etc.) -- **Custom Elements** - Stateful LitElement classes (``, ``, etc.) -- **Reactive State** - Use `createState()` for reactive UI updates -- **Claude Theme** - We use the Claude theme from mini-lit - -## Project Structure - -``` -packages/browser-extension/ -├── src/ -│ ├── app.css # Tailwind v4 entry point with Claude theme -│ ├── background.ts # Service worker for opening side panel -│ ├── sidepanel.html # Side panel HTML entry point -│ ├── sidepanel.ts # Main side panel app with hot reload -│ ├── sandbox.html # Sandboxed page for artifact HTML rendering -│ └── sandbox.js # Sandbox environment setup (console capture, helpers) -├── scripts/ -│ ├── build.mjs # esbuild bundler configuration -│ └── dev-server.mjs # WebSocket server for hot reloading -├── manifest.chrome.json # Chrome/Edge manifest (MV3) -├── manifest.firefox.json # Firefox manifest (MV2) -├── icon-*.png # Extension icons -├── dist-chrome/ # Chrome build (git-ignored) -└── dist-firefox/ # Firefox build (git-ignored) -``` - -## Development Setup - -### Prerequisites -1. Install dependencies from monorepo root: - ```bash - npm install - ``` - -2. Build the extension: - ```bash - # Build for both browsers - npm run build -w @mariozechner/pi-reader-extension - - # Or build for specific browser - npm run build:chrome -w @mariozechner/pi-reader-extension - npm run build:firefox -w @mariozechner/pi-reader-extension - ``` - -3. Load the extension: - - **Chrome/Edge:** - - Open `chrome://extensions/` or `edge://extensions/` - - Enable "Developer mode" - - Click "Load unpacked" - - Select `packages/browser-extension/dist-chrome/` - - **Firefox:** - - Open `about:debugging` - - Click "This Firefox" - - Click "Load Temporary Add-on" - - Select any file in `packages/browser-extension/dist-firefox/` - -### Development Workflow - -1. **Start the dev server** (from monorepo root): - ```bash - # For Chrome development - npm run dev -w @mariozechner/pi-reader-extension - - # For Firefox development - npm run dev:firefox -w @mariozechner/pi-reader-extension - ``` - - This runs three processes in parallel: - - **esbuild** - Watches and rebuilds TypeScript files - - **Tailwind CSS v4** - Watches and rebuilds styles - - **WebSocket server** - Watches dist/ and triggers extension reload - -2. **Automatic reloading**: - - Any change to source files triggers a rebuild - - The WebSocket server detects dist/ changes - - Side panel connects to `ws://localhost:8765` - - Extension auto-reloads via `chrome.runtime.reload()` - -3. **Open the side panel**: - - Click the extension icon in Chrome toolbar - - Or use Chrome's side panel button (top-right) - -## Key Files - -### [src/sidepanel.ts](src/sidepanel.ts) -Main application logic: -- Extracts page content via `chrome.scripting.executeScript` -- Manages chat UI with mini-lit components -- Handles WebSocket connection for hot reload -- Direct AI API calls (no background worker needed) - -### [src/app.css](src/app.css) -Tailwind v4 configuration: -- Imports Claude theme from mini-lit -- Uses `@source` directive to scan mini-lit components -- Compiled to `dist/app.css` during build - -### [scripts/build.mjs](scripts/build.mjs) -Build configuration: -- Uses esbuild for fast TypeScript bundling -- Copies static files (HTML, manifest, icons, sandbox files) -- Supports watch mode for development -- Browser-specific builds (Chrome MV3, Firefox MV2) - -### [scripts/dev-server.mjs](scripts/dev-server.mjs) -Hot reload server: -- WebSocket server on port 8765 -- Watches `dist/` directory for changes -- Sends reload messages to connected clients - -### [src/state/KeyStore.ts](src/state/KeyStore.ts) -Cross-browser API key storage: -- Detects browser environment (`browser.storage` vs `chrome.storage`) -- Stores API keys in local storage -- Used by DirectTransport for provider authentication - -### [src/components/SandboxedIframe.ts](src/components/SandboxedIframe.ts) -Reusable sandboxed HTML renderer: -- Creates sandboxed iframe with `allow-scripts` and `allow-modals` -- Injects runtime scripts using TypeScript `.toString()` pattern -- Captures console logs and errors via `postMessage` -- Provides attachment helper functions to sandboxed content -- Emits `@console` and `@execution-complete` events - -### [src/tools/artifacts/HtmlArtifact.ts](src/tools/artifacts/HtmlArtifact.ts) -HTML artifact renderer: -- Uses `SandboxedIframe` component for secure HTML preview -- Toggle between preview and code view -- Displays console logs and errors in collapsible panel -- Supports attachments (accessible via `listFiles()`, `readTextFile()`, etc.) - -### [src/sandbox.html](src/sandbox.html) and [src/sandbox.js](src/sandbox.js) -Sandboxed page for artifact HTML: -- Declared in manifest `sandbox.pages` array -- Has permissive CSP allowing external scripts and `eval()` -- Currently used as fallback (most functionality moved to `SandboxedIframe`) -- Provides helper functions for file access and console capture - -## Working with mini-lit Components - -### Basic Usage -Read `../../mini-lit/llms.txt` and `../../mini-lit/README.md` in full. If in doubt, find the component in `../../mini-lit/src/` and read its source file in full. - -### Tailwind Classes -All standard Tailwind utilities work, plus mini-lit's theme variables: -- `bg-background`, `text-foreground` - Theme-aware colors -- `bg-card`, `border-border` - Component backgrounds -- `text-muted-foreground` - Secondary text -- `bg-primary`, `text-primary-foreground` - Primary actions - -## Troubleshooting - -### Extension doesn't reload automatically -- Check WebSocket server is running (port 8765) -- Check console for connection errors -- Manually reload at `chrome://extensions/` - -### Side panel doesn't open -- Check manifest permissions -- Ensure background service worker is loaded -- Try clicking extension icon directly - -### Styles not updating -- Ensure Tailwind watcher is running -- Check `src/app.css` imports -- Clear Chrome extension cache - -## Building for Production - -```bash -npm run build -w @mariozechner/pi-reader-extension -``` - -This creates an optimized build in `dist/` without hot reload code. - ---- - -## Content Security Policy (CSP) Issues and Workarounds - -Browser extensions face strict Content Security Policy restrictions that affect dynamic code execution. This section documents these limitations and the solutions implemented in this extension. - -### Overview of CSP Restrictions - -**Content Security Policy** prevents unsafe operations like `eval()`, `new Function()`, and inline scripts to protect against XSS attacks. Browser extensions have even stricter CSP rules than regular web pages. - -### CSP in Extension Pages (Side Panel, Popup, Options) - -**Problem:** Extension pages (like our side panel) cannot use `eval()` or `new Function()` due to manifest CSP restrictions. - -**Chrome Manifest V3:** -```json -"content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'self'" -} -``` -- `'unsafe-eval'` is **explicitly forbidden** in MV3 extension pages -- Attempting to add it causes extension load failure: `"Insecure CSP value "'unsafe-eval'" in directive 'script-src'"` - -**Firefox Manifest V2:** -```json -"content_security_policy": "script-src 'self' 'wasm-unsafe-eval' ...; object-src 'self'" -``` -- `'unsafe-eval'` is **forbidden** in Firefox MV2 `script-src` -- Only `'wasm-unsafe-eval'` is allowed (for WebAssembly) - -**Impact on Tool Parameter Validation:** - -The `@mariozechner/pi-ai` package uses AJV (Another JSON Schema Validator) to validate tool parameters. AJV compiles JSON schemas into validation functions using `new Function()`, which violates extension CSP. - -**Solution:** Detect browser extension environment and disable AJV validation: - -```typescript -// @packages/ai/src/utils/validation.ts -const isBrowserExtension = typeof globalThis !== "undefined" && - (globalThis as any).chrome?.runtime?.id !== undefined; - -let ajv: any = null; -if (!isBrowserExtension) { - try { - ajv = new Ajv({ allErrors: true, strict: false }); - addFormats(ajv); - } catch (e) { - console.warn("AJV validation disabled due to CSP restrictions"); - } -} - -export function validateToolArguments(tool: Tool, toolCall: ToolCall): any { - // Skip validation in browser extension (CSP prevents AJV from working) - if (!ajv || isBrowserExtension) { - return toolCall.arguments; // Trust the LLM - } - // ... normal validation -} -``` - -**Call chain:** -1. `@packages/ai/src/utils/validation.ts` - Validation logic -2. `@packages/ai/src/agent/agent-loop.ts` - Calls `validateToolArguments()` in `executeToolCalls()` -3. `@packages/browser-extension/src/state/transports/DirectTransport.ts` - Uses agent loop -4. `@packages/browser-extension/src/state/agent-session.ts` - Coordinates transport - -**Result:** Tool parameter validation is **disabled in browser extensions**. We trust the LLM to generate valid parameters. - ---- - -### CSP in Sandboxed Pages (HTML Artifacts) - -**Problem:** HTML artifacts need to render user-generated HTML with external scripts (e.g., Chart.js, D3.js) and execute dynamic code. - -**Solution:** Use sandboxed pages with permissive CSP. - -#### How Sandboxed Pages Work - -**Chrome Manifest V3:** -```json -{ - "sandbox": { - "pages": ["sandbox.html"] - }, - "content_security_policy": { - "sandbox": "sandbox allow-scripts allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:; ..." - } -} -``` - -**Firefox Manifest V2:** -- MV2 doesn't support `sandbox.pages` with external script hosts in CSP -- We switched to MV2 to whitelist CDN hosts in main CSP: -```json -{ - "content_security_policy": "script-src 'self' 'wasm-unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com https://cdn.skypack.dev; ..." -} -``` - -#### SandboxedIframe Component - -The [src/components/SandboxedIframe.ts](src/components/SandboxedIframe.ts) component provides a reusable way to render HTML artifacts: - -**Key implementation details:** - -1. **Runtime Script Injection:** Instead of relying on `sandbox.html`, we inject runtime scripts directly into the HTML using TypeScript `.toString()`: - -```typescript -private injectRuntimeScripts(htmlContent: string): string { - // Define runtime function in TypeScript with proper typing - const runtimeFunction = function (artifactId: string, attachments: any[]) { - // Console capture - window.__artifactLogs = []; - const originalConsole = { log: console.log, error: console.error, /* ... */ }; - - ['log', 'error', 'warn', 'info'].forEach((method) => { - console[method] = function (...args: any[]) { - const text = args.map(arg => /* stringify */).join(' '); - window.__artifactLogs.push({ type: method === 'error' ? 'error' : 'log', text }); - window.parent.postMessage({ type: 'console', method, text, artifactId }, '*'); - originalConsole[method].apply(console, args); - }; - }); - - // Error handlers - window.addEventListener('error', (e: ErrorEvent) => { /* ... */ }); - window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => { /* ... */ }); - - // Attachment helpers - window.listFiles = () => attachments.map(/* ... */); - window.readTextFile = (id) => { /* ... */ }; - window.readBinaryFile = (id) => { /* ... */ }; - }; - - // Convert function to string and inject - const runtimeScript = ` - - `; - - // Inject at start of or beginning of HTML - return htmlContent.replace(/]*>/i, (m) => `${m}${runtimeScript}`) || runtimeScript + htmlContent; -} -``` - -2. **Sandbox Attributes:** The iframe uses: - - `sandbox="allow-scripts allow-modals"` - **NOT** `allow-same-origin` - - Removing `allow-same-origin` prevents sandboxed content from bypassing the sandbox - - `postMessage` still works without `allow-same-origin` - -3. **Communication:** Parent window listens for messages from iframe: - - `{type: "console", method, text, artifactId}` - Console logs - - `{type: "execution-complete", logs, artifactId}` - Final logs after page load - -4. **Usage in HtmlArtifact:** - -```typescript -// src/tools/artifacts/HtmlArtifact.ts -render() { - return html` - - `; -} -``` - -**Files involved:** -- [src/components/SandboxedIframe.ts](src/components/SandboxedIframe.ts) - Reusable sandboxed iframe component -- [src/tools/artifacts/HtmlArtifact.ts](src/tools/artifacts/HtmlArtifact.ts) - Uses SandboxedIframe -- [src/sandbox.html](src/sandbox.html) - Fallback sandboxed page (mostly unused now) -- [src/sandbox.js](src/sandbox.js) - Sandbox environment (mostly unused now) -- [manifest.chrome.json](manifest.chrome.json) - Chrome MV3 sandbox CSP -- [manifest.firefox.json](manifest.firefox.json) - Firefox MV2 CDN whitelist - ---- - -### CSP in Injected Tab Scripts (browser-javascript Tool) - -**Problem:** The `browser-javascript` tool executes AI-generated JavaScript in the current tab. Many sites have strict CSP that blocks `eval()` and `new Function()`. - -**Example - Gmail's CSP:** -``` -script-src 'report-sample' 'nonce-...' 'unsafe-inline' 'strict-dynamic' https: http:; -require-trusted-types-for 'script'; -``` - -Gmail uses **Trusted Types** (`require-trusted-types-for 'script'`) which blocks all string-to-code conversions, including: -- `eval(code)` -- `new Function(code)` -- `setTimeout(code)` (with string argument) -- Setting `innerHTML`, `outerHTML`, ` - - diff --git a/packages/browser-extension/static/sandbox.js b/packages/browser-extension/static/sandbox.js deleted file mode 100644 index 93088917..00000000 --- a/packages/browser-extension/static/sandbox.js +++ /dev/null @@ -1,12 +0,0 @@ -// Minimal sandbox.js - just listens for sandbox-load and writes the content -window.addEventListener("message", (event) => { - if (event.data.type === "sandbox-load") { - // Write the complete HTML (which includes runtime + user code) - document.open(); - document.write(event.data.code); - document.close(); - } -}); - -// Signal ready to parent -window.parent.postMessage({ type: "sandbox-ready" }, "*"); diff --git a/packages/browser-extension/static/sidepanel.html b/packages/browser-extension/static/sidepanel.html deleted file mode 100644 index 7d93924c..00000000 --- a/packages/browser-extension/static/sidepanel.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - pi-ai - - - - - - \ No newline at end of file diff --git a/packages/browser-extension/tsconfig.build.json b/packages/browser-extension/tsconfig.build.json deleted file mode 100644 index 722d8e0c..00000000 --- a/packages/browser-extension/tsconfig.build.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./dist", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "Bundler", - "target": "ES2022", - "types": ["chrome"] - }, - "include": ["src/**/*.ts"], - "exclude": ["dist", "node_modules"] -} diff --git a/packages/browser-extension/tsconfig.json b/packages/browser-extension/tsconfig.json deleted file mode 100644 index 5c295044..00000000 --- a/packages/browser-extension/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.build.json", - "compilerOptions": { - "noEmit": true - } -} diff --git a/pi-mono.code-workspace b/pi-mono.code-workspace index f8d507ed..19a651d7 100644 --- a/pi-mono.code-workspace +++ b/pi-mono.code-workspace @@ -7,7 +7,7 @@ "path": "../mini-lit" }, { - "path": "../genai-workshop-new" + "path": "../sitegeist" } ], "settings": {}