A custom tool for pi-agent that opens an interactive form to gather user responses to clarification questions. On macOS, uses Glimpse to render in a native WKWebView window; falls back to a browser tab on other platforms. The launcher setting can also open forms in an Orca browser tab.
interview-demo-sped-up.mp4
pi install npm:pi-interviewRestart pi to load the extension.
Requirements:
- pi v0.82.1 or later
- For native macOS window:
pi install npm:glimpseui(optional, falls back to browser if not installed)
- Question Types: Single-select, multi-select, text input, image upload/camera capture, 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
⌘+←/⌘+→(Ctrloff 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
- Launcher Selection: Open forms in Glimpse, an Orca tab, or a browser
- Image Support: Drag & drop anywhere on a question, file picker, browser camera capture, 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 usingAsk - Tool Discoverability (pi v0.59+): Registers a
promptSnippetsointerviewremains eligible for inclusion in pi's defaultAvailable toolsprompt section - Themes: Built-in default + optional light/dark + custom theme CSS
┌─────────┐ ┌──────────────────────────────────────────┐ ┌─────────┐
│ Agent │ │ Glimpse / Browser Form │ │ Agent │
│ invokes ├─────►│ ├─────►│receives │
│interview│ │ answer → answer → attach img → answer │ │responses│
└─────────┘ │ ↑ │ └─────────┘
│ └── auto-save, timeout resets ───────┤
└──────────────────────────────────────────┘
Lifecycle:
- Agent calls
interview()→ local server starts → Glimpse window opens (macOS) or browser tab (elsewhere) - User answers at their own pace; each change auto-saves and resets the timeout
- Session ends via:
- Submit (
⌘+Enter) → responses returned to agent - Timeout → warning overlay, option to stay or close
- Escape × 2 → quick cancel
- Submit (
- Window closes automatically; agent receives responses (or
nullif 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.
Remote and Moshi sessions: When the session looks remote (ssh/mosh env, or an active remote login on the host), the tool skips or supplements the local window and prints the form URL with access hints: a Moshi tip when the moshi-hook gateway is running (tap the preview button in the terminal title bar and pick the interview server), and an exact ssh -L command for plain SSH (mosh can't forward ports). The server binds low ports (8377+, scanning forward on collision) and answers tokenless loopback opens with a landing page that hops to the form, so Moshi's browser preview reaches it in one tap. Requests with a non-loopback Host header are rejected.
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.
The interview tool is invoked by pi-agent, not imported directly:
// 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
async: false // optional, return immediately and deliver the answer later
});With async: true the tool opens the form and returns right away with an interview ID, so the agent keeps working and background results keep flowing. When the user submits, cancels, or the form times out, the same answer text arrives as a message starting with Interview <id> finished. and starts a new turn if the agent is idle. Outstanding async interviews close when the session ends.
{
"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"
}
]
}| 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" |
media |
object or object[] | Media content: image, chart, mermaid, table, or html |
Questions and options can include content blocks for code snippets, diffs, and Markdown.
Question-level code content (displayed above options):
{
"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:
{
"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"):
{
"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"):
{
"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: trueshows raw source.- Any other
lang: renders as raw source;showSourceis not allowed.
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.
{
"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]" }
}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: "<div>...</div>", caption? |
Raw HTML content |
All media types support position: "above" (default), "below", or "side" (two-column layout).
{
"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 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
| 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) |
Settings in ~/.pi/agent/settings.json:
{
"interview": {
"timeout": 600,
"port": 19847,
"snapshotDir": "~/.pi/interview-snapshots/",
"autoSaveOnSubmit": true,
"generateModel": "anthropic/claude-haiku-4-5",
"launcher": "browser",
"browser": "Firefox",
"glimpseFloating": false,
"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.
Launcher: launcher selects where the form opens. Omit it for the default behavior: Glimpse on macOS when glimpseui is installed and the session is local, otherwise a browser tab.
"glimpse": native macOS Glimpse window. Requires a local macOS session withglimpseuiinstalled, and reports why the window could not open instead of using a browser."browser": browser tab, even when Glimpse is installed"orca": browser tab in the current Orca-managed worktree, or in Orca's focused worktree when the working directory is not inside one. Orca focuses the tab when that worktree is visible; otherwise the tab is staged in its tab bar. TheorcaCLI must be available onPATH.
Browser: browser names the application used for browser tabs (e.g. "Firefox", "Brave Browser"). It applies when the form opens in a browser: launcher: "browser", or launcher omitted with Glimpse unavailable. It has no effect under "glimpse" or "orca".
Glimpse window: glimpseFloating keeps the native macOS Glimpse window above other windows when true (default: false). It does not affect browser fallback behavior.
Theme notes:
mode:dark(default),light, orauto(follows OS unless overridden)name: built-in themes aredefaultandtuftelightPath/darkPath: optional CSS file paths (absolute or relative to cwd)toggleHotkey: optional; when set, toggles light/dark and persists per browser profile
The interview form supports light/dark themes with automatic OS detection and user override.
| Theme | Description |
|---|---|
default |
Monospace, IDE-inspired aesthetic |
tufte |
Serif fonts (Instrument Serif), book-like feel |
dark(default): Dark background, light textlight: Light background, dark textauto: Follows OS preference, user can toggle and override persists in localStorage
Create custom CSS files that override the default variables:
: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:
{
"interview": {
"theme": {
"mode": "auto",
"lightPath": "~/my-themes/light.css",
"darkPath": "~/my-themes/dark.css",
"toggleHotkey": "mod+shift+l"
}
}
}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.
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
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
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:
interview({ questions: "~/.pi/interview-recovery/2026-01-02_093000_myproject_main_65bec3f4.json" })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: truein settings) - Automatically saves after successful submission
- Folder name includes
-submittedsuffix
Reviving a Saved Interview:
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:
{
"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)
- Max 12 images total per submission
- Max 5MB per image
- Max 4096x4096 pixels per image
- Allowed types: PNG, JPG, GIF, WebP
