pi-interview

# Interview Tool A custom tool for pi-agent that opens an interactive form to gather user responses to clarification questions. On macOS, uses [Glimpse](https://github.com/hazat/glimpse) to render in a native WKWebView window; falls back to a browser tab on other platforms. https://github.com/user-attachments/assets/52285bd9-956e-4020-aca5-9fbd82916934 ## Installation ```bash pi install npm:pi-interview ``` Restart pi to load the extension. **Requirements:** - pi-agent v0.35.0 or later (extensions API) - For native macOS window: `pi install npm:glimpseui` (optional, falls back to browser if not installed) ## Features - **Question Types**: Single-select, multi-select, text input, image upload, and info panels - **Rich Media**: Embed images, Chart.js charts, Mermaid diagrams, tables, and HTML in questions - **Pre-selection**: Recommended options show a "Recommended" badge and are pre-checked on load - **Conviction & Weight**: Control recommendation strength (`conviction`) and visual prominence (`weight`) - **"Other" Option**: Single/multi select questions support custom text input - **Per-Question Attachments**: Attach images to any question via button or drag & drop - **Keyboard Navigation**: Full keyboard support with `⌘+←`/`⌘+→` (`Ctrl` off macOS), arrow keys, Tab, Enter - **Auto-save**: Responses saved to localStorage, restored on reload - **Session Timeout**: Configurable timeout with countdown badge, refreshes on activity - **Multi-Agent Support**: Queue detection prevents focus stealing when multiple agents run interviews - **Queue Toast Switcher**: Active interviews show a top-right toast with a dropdown to open queued sessions - **Session Recovery**: Abandoned/timed-out interviews save questions for later retry - **Save Snapshots**: Save interview state to HTML for later review or revival - **Session Status Bar**: Shows project path, git branch, and session ID for identification - **Image Support**: Drag & drop anywhere on question, file picker, or paste a path into the dedicated path field - **Path Normalization**: Handles shell-escaped paths (`\ `) and macOS screenshot filenames (narrow no-break space before AM/PM) - **Generate & Review Options**: Single/multi-select questions, including rich-option questions with inline content blocks, show "✦ Generate more" (appends new choices) and "↻ Review options" (reviews options and rewrites the question for clarity) buttons powered by an LLM - **Ask About an Option**: Single/multi options, including rich options with inline content blocks, can open an inline assistant panel with prompt chips, freeform follow-up questions, provider/model overrides under Advanced, and auto-saved option analysis - **Option Clarifications**: Single/multi options, including rich options with inline content blocks, can reveal a separate inline `Optional clarification...` field when selected, letting users attach a short note to a choice without using `Ask` - **Tool Discoverability (pi v0.59+)**: Registers a `promptSnippet` so `interview` remains eligible for inclusion in pi's default `Available tools` prompt section - **Themes**: Built-in default + optional light/dark + custom theme CSS ## How It Works ``` ┌─────────┐ ┌──────────────────────────────────────────┐ ┌─────────┐ │ Agent │ │ Glimpse / Browser Form │ │ Agent │ │ invokes ├─────►│ ├─────►│receives │ │interview│ │ answer → answer → attach img → answer │ │responses│ └─────────┘ │ ↑ │ └─────────┘ │ └── auto-save, timeout resets ───────┤ └──────────────────────────────────────────┘ ``` **Lifecycle:** 1. Agent calls `interview()` → local server starts → Glimpse window opens (macOS) or browser tab (elsewhere) 2. User answers at their own pace; each change auto-saves and resets the timeout 3. Session ends via: - **Submit** (`⌘+Enter`) → responses returned to agent - **Timeout** → warning overlay, option to stay or close - **Escape × 2** → quick cancel 4. Window closes automatically; agent receives responses (or `null` if cancelled) **Timeout behavior:** The countdown (visible in corner) resets on any activity - typing, clicking, or mouse movement. When it expires, an overlay appears giving the user a chance to continue. Progress is never lost thanks to localStorage auto-save. **Multi-agent behavior:** When multiple agents run interviews simultaneously, only the first auto-opens the window. Subsequent interviews are queued and shown as a URL in the tool output, preventing focus stealing. When you submit the active interview, the window automatically redirects to the next queued interview. Active interviews also surface a top-right toast with a dropdown to open queued sessions. A session status bar at the top of each form shows the project path, git branch, and session ID for easy identification. ## Usage The interview tool is invoked by pi-agent, not imported directly: ```javascript // Create a questions JSON file, then call the tool await interview({ questions: '/path/to/questions.json', timeout: 600, // optional, seconds (default: 600) verbose: false // optional, debug logging }); ``` ## Question Schema ```json { "title": "Project Setup", "description": "Review my suggestions and adjust as needed.", "questions": [ { "id": "context", "type": "info", "question": "Architecture context", "context": "This project needs SSR and edge deployment support." }, { "id": "framework", "type": "single", "question": "Which framework?", "options": ["React", "Vue", "Svelte"], "recommended": "React", "conviction": "strong", "weight": "critical" }, { "id": "features", "type": "multi", "question": "Which features?", "context": "Select all that apply", "options": ["Auth", "Database", "API"], "recommended": ["Auth", "Database"] }, { "id": "indent", "type": "single", "question": "Indent style?", "options": ["Tabs", "Spaces (2)", "Spaces (4)"], "recommended": "Spaces (2)", "weight": "minor" }, { "id": "notes", "type": "text", "question": "Additional requirements?" }, { "id": "mockup", "type": "image", "question": "Upload a design mockup" } ] } ``` ### Question Fields | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier | | `type` | string | `single`, `multi`, `text`, `image`, or `info` | | `question` | string | Question text | | `options` | string[] or object[] | Choices (required for single/multi). Can be strings or `{ label, content? }` objects | | `recommended` | string or string[] | Shows "Recommended" badge and pre-selects option(s) | | `conviction` | string | `"strong"` or `"slight"`. Slight opts out of pre-selection. Requires `recommended` | | `weight` | string | `"critical"` (prominent card) or `"minor"` (compact card) | | `context` | string | Help text shown below question | | `content` | object | Content block displayed below question text (`lang: "md"|"markdown"` previews Markdown by default) | | `media` | object or object[] | Media content: image, chart, mermaid, table, or html | ### Content Blocks Questions and options can include `content` blocks for code snippets, diffs, and Markdown. **Question-level code content** (displayed above options): ```json { "id": "review", "type": "single", "question": "Review this implementation", "content": { "source": "function add(a, b) {\n return a + b;\n}", "lang": "ts", "file": "src/math.ts", "lines": "10-12", "highlights": [2] }, "options": ["Approve", "Request changes"] } ``` **Options with content blocks**: ```json { "options": [ { "label": "Use async/await", "content": { "source": "const data = await fetch(url);", "lang": "ts" } }, { "label": "Use promises", "content": { "source": "fetch(url).then(data => ...);", "lang": "ts" } }, "Keep current implementation" ] } ``` **Diff display** (`lang: "diff"`): ```json { "content": { "source": "--- a/file.ts\n+++ b/file.ts\n@@ -1,3 +1,4 @@\n const x = 1;\n+const y = 2;\n const z = 3;", "lang": "diff", "file": "src/file.ts" } } ``` **Markdown preview by default** (`lang: "md"` or `"markdown"`): ```json { "content": { "source": "# Release notes\n\n- Added preview mode\n- Fixed wrapping", "lang": "md" } } ``` Set `showSource: true` on Markdown content to show raw Markdown instead of preview. | Content Field | Type | Description | |---------------|------|-------------| | `source` | string | Content text (required) | | `lang` | string | Language hint (e.g., `ts`, `diff`, `md`) | | `file` | string | File path shown in the header | | `lines` | string | Line range shown in the header (code content only) | | `highlights` | number[] | Line highlights (code content only) | | `title` | string | Optional title above content | | `showSource` | boolean | Markdown only: `true` forces raw source instead of preview | Rules: - `lang: "md"` or `"markdown"`: preview by default, `showSource: true` shows raw source. - Any other `lang`: renders as raw source; `showSource` is not allowed. ### Info Panels Use `type: "info"` for non-interactive context panels. They display a title, context text, and optional media but have no input — they're skipped during keyboard navigation and excluded from responses. ```json { "id": "overview", "type": "info", "question": "Architecture Overview", "context": "The system uses a microservices architecture with three main services.", "media": { "type": "mermaid", "mermaid": "graph LR\n A[API] --> B[Auth]\n A --> C[Data]" } } ``` ### Media Blocks Questions can embed media via the `media` field (single object or array). Supported types: | Type | Fields | Description | |------|--------|-------------| | `image` | `src`, `alt?`, `caption?` | Image (local path, URL, or data URI) | | `table` | `table: { headers, rows, highlights? }`, `caption?` | Data table with optional row highlighting | | `chart` | `chart: { type, data, options? }`, `caption?` | Chart.js chart (bar, line, pie, etc.) | | `mermaid` | `mermaid: "graph LR\n..."`, `caption?` | Mermaid diagram | | `html` | `html: "
...
"`, `caption?` | Raw HTML content | All media types support `position`: `"above"` (default), `"below"`, or `"side"` (two-column layout). ```json { "id": "db-choice", "type": "single", "question": "Which database?", "media": { "type": "table", "table": { "headers": ["Database", "Latency", "Cost"], "rows": [["PostgreSQL", "~5ms", "$50/mo"], ["DynamoDB", "~2ms", "$80/mo"]], "highlights": [0] }, "caption": "Benchmark results from staging" }, "options": ["PostgreSQL", "DynamoDB"], "recommended": "PostgreSQL" } ``` ### Conviction & Weight **Conviction** controls how strongly a recommendation is presented: - Omitted (default): shows "Recommended" badge, pre-selects the option - `"strong"`: same as default (use when very confident) - `"slight"`: shows "Recommended" badge but does NOT pre-select (use when unsure) **Weight** controls visual prominence: - `"critical"`: thick accent border, tinted background — for decisions that matter most - `"minor"`: compact card with smaller text and padding — for low-stakes preferences ## Keyboard Shortcuts | Key | Action | |-----|--------| | `↑` `↓` | Navigate options | | `⌘+←` `⌘+→` | Navigate between questions (`Ctrl` off macOS) | | `Tab` | Cycle through options | | `Enter` / `Space` | Select option | | `⌘+V` | Paste text in the focused input | | `⌘+Enter` | Submit form | | `Esc` | Show exit overlay (press twice to quit) | | `⌘+Shift+L` | Toggle theme (if enabled; appears in shortcuts bar) | ## Configuration Settings in `~/.pi/agent/settings.json`: ```json { "interview": { "timeout": 600, "port": 19847, "snapshotDir": "~/.pi/interview-snapshots/", "autoSaveOnSubmit": true, "generateModel": "anthropic/claude-haiku-4-5", "theme": { "mode": "auto", "name": "default", "lightPath": "/path/to/light.css", "darkPath": "/path/to/dark.css", "toggleHotkey": "mod+shift+l" } } } ``` **Timeout precedence**: params > settings > default (600s) **Snapshot settings:** - `snapshotDir`: Directory for saved interview snapshots (default: `~/.pi/interview-snapshots/`) - `autoSaveOnSubmit`: Automatically save snapshot on successful submit (default: `true`) **Port setting**: Set a fixed `port` (e.g., `19847`) to use a consistent port across sessions. **Generate model**: `generateModel` sets the model for the generate/review option actions (e.g., `"anthropic/claude-haiku-4-5"`). Defaults to the agent's current model, then falls back to a cheap available model. If an explicitly configured generate model fails at request time and the current session is using a different model, interview retries once with the current session model. **Theme notes:** - `mode`: `dark` (default), `light`, or `auto` (follows OS unless overridden) - `name`: built-in themes are `default` and `tufte` - `lightPath` / `darkPath`: optional CSS file paths (absolute or relative to cwd) - `toggleHotkey`: optional; when set, toggles light/dark and persists per browser profile ## Theming The interview form supports light/dark themes with automatic OS detection and user override. ### Built-in Themes | Theme | Description | |-------|-------------| | `default` | Monospace, IDE-inspired aesthetic | | `tufte` | Serif fonts (Instrument Serif), book-like feel | ### Theme Modes - **`dark`** (default): Dark background, light text - **`light`**: Light background, dark text - **`auto`**: Follows OS preference, user can toggle and override persists in localStorage ### Custom Themes Create custom CSS files that override the default variables: ```css :root { --bg-body: #f8f8f8; --bg-card: #ffffff; --bg-elevated: #f0f0f0; --bg-selected: #d0d0e0; --bg-hover: #e8e8e8; --fg: #1a1a1a; --fg-muted: #6c6c6c; --fg-dim: #8a8a8a; --accent: #5f8787; --accent-hover: #4a7272; --accent-muted: rgba(95, 135, 135, 0.15); --border: #5f87af; --border-muted: #b0b0b0; --border-focus: #8a8a9a; --border-active: #9090a0; --success: #87af87; --warning: #d7af5f; --error: #af5f5f; --focus-ring: rgba(95, 135, 175, 0.2); } ``` Then reference in settings or params: ```json { "interview": { "theme": { "mode": "auto", "lightPath": "~/my-themes/light.css", "darkPath": "~/my-themes/dark.css", "toggleHotkey": "mod+shift+l" } } } ``` ### Toggle Hotkey When `toggleHotkey` is set (e.g., `"mod+shift+l"`), users can switch between light/dark modes. The preference persists in the browser's localStorage across sessions. ## Response Format ```typescript interface Response { id: string; value: string | string[]; attachments?: string[]; // image paths attached to non-image questions } ``` Example: ``` - framework: React [attachments: /path/to/diagram.png] - features: Auth, Database - notes: Need SSO support - mockup: /tmp/uploaded-image.png ``` ## File Structure ``` interview/ ├── index.ts # Tool entry point, parameter schema ├── settings.ts # Shared settings module ├── server.ts # HTTP server, request handling ├── schema.ts # TypeScript interfaces for questions/responses └── form/ ├── index.html # Form template ├── styles.css # Base styles (dark tokens) ├── themes/ # Theme overrides (light/dark) └── script.js # Form logic, keyboard nav, image handling ``` ## Session Recovery If an interview times out or is abandoned (tab closed, lost connection), the questions are automatically saved to `~/.pi/interview-recovery/` for later retry. **Recovery files:** - Location: `~/.pi/interview-recovery/` - Format: `{date}_{time}_{project}_{branch}_{sessionId}.json` - Example: `2026-01-02_093000_myproject_main_65bec3f4.json` - Auto-cleanup: Files older than 7 days are deleted **To retry an abandoned interview:** ```javascript interview({ questions: "~/.pi/interview-recovery/2026-01-02_093000_myproject_main_65bec3f4.json" }) ``` ## Saving Interviews Save a snapshot of your interview at any time for later review or to resume. **Manual Save:** - Click the Save button (header or footer) - Saves to `~/.pi/interview-snapshots/` by default - Creates folder with `index.html` + `images/` subfolder **Auto-save on Submit:** - Enabled by default (`autoSaveOnSubmit: true` in settings) - Automatically saves after successful submission - Folder name includes `-submitted` suffix **Reviving a Saved Interview:** ```javascript interview({ questions: "~/.pi/interview-snapshots/project-setup-myapp-main-2026-01-20-141523/index.html" }) ``` The form opens with answers pre-populated. Edit and submit as normal. **Configuration:** ```json { "interview": { "snapshotDir": "~/.pi/interview-snapshots/", "autoSaveOnSubmit": true } } ``` **Snapshot Structure:** ``` ~/.pi/interview-snapshots/ {title}-{project}-{branch}-{timestamp}[-submitted]/ index.html # Human-readable + embedded JSON for revival images/ mockup.png # Uploaded images (relative paths in HTML) ``` ## Limits - Max 12 images total per submission - Max 5MB per image - Max 4096x4096 pixels per image - Allowed types: PNG, JPG, GIF, WebP