diff --git a/docs/quick-pane-v1-design.md b/docs/quick-pane-v1-design.md
new file mode 100644
index 0000000..fea70e9
--- /dev/null
+++ b/docs/quick-pane-v1-design.md
@@ -0,0 +1,66 @@
+# Quick Pane v1 Design
+
+## Goal
+
+Quick Pane is a small native window opened by a desktop/window-manager hotkey. It accepts one short request and returns either a useful answer or an explicit escalation within 60 seconds.
+
+## Scope
+
+- Cold-launchable, independent Tauri window.
+- Compact request input, quick-model picker, and workspace picker.
+- Configured default workspace, quick model, follow-up model, supplemental prompt, time limit, and read-only tool limit.
+- Ephemeral Quick Pane UI state and best-effort runtime cleanup.
+- Advisory read-only/no-subagent instructions, inline permission/clarification prompts, and explicit normal-session handoff.
+
+## Non-goals
+
+- Persisted quick history.
+- Named profiles.
+- App-managed WM binding writes.
+- Trusted prevention of mutable tools or direct edits.
+- Trusted prevention of subagents.
+- A memory-only Pi runtime; Pi may write temporary session data while running.
+
+## Invocation
+
+Desktop/WM configuration owns the hotkey so it works while Pi GUI is absent. Settings displays and copies the required command; it does not register or modify system bindings.
+
+The app creates or focuses a dedicated small Quick Pane window. `Esc` closes it and discards quick-only state.
+
+## Request lifecycle
+
+1. Pane chooses configured default workspace and quick model, with per-request overrides.
+2. It starts an isolated Pi RPC runtime in the chosen workspace.
+3. Immutable prompt text instructs one request, read-only inspection, zero subagents, configured wall-clock budget, and configured tool-call budget. Current Pi RPC cannot trusted-enforce those instructions; event-derived caps and cleanup are best effort.
+4. Supplemental user instructions append beneath those prompt guardrails.
+5. Pane streams answer, tool status, and inline extension requests.
+6. On success it displays final answer. On a cap it preserves partial answer, explains the cap, and offers handoff.
+
+## Escalation
+
+Mutations, subagents, multi-step work, and likely-over-budget work preflight-escalate. The quick agent explains why. The user clicks **Continue in full session** to open a new normal session with:
+
+- original request;
+- escalation note or partial findings;
+- selected workspace; and
+- configured follow-up model.
+
+No full session opens automatically.
+
+## Permissions and privacy
+
+Quick Pane surfaces Pi extension `select`, `confirm`, `input`, and `editor` requests inline. User explicitly answers every prompt. Existing policy remains authoritative: ordinary workspace reads may be allowed, external-directory reads may ask, and sensitive paths may deny.
+
+Quick Pane UI request, output, and transcript stay in memory only. Closing pane removes them and triggers best-effort quick-runtime cleanup. Pi may write temporary session data while the child runs. A deliberate handoff persists only in newly created normal-session history.
+
+## Failure handling
+
+Unavailable models, startup failures, and crashes render inline actionable errors: retry, pick another quick model, or open normal session. No silent fallback model is used.
+
+## Settings validation
+
+Settings reject invalid model, workspace, custom prompt, or numeric limit. Failed save keeps previously stored configuration and marks invalid fields.
+
+## Acceptance
+
+From cold or hot desktop state, the hotkey produces an answer or explicit escalation within 60 seconds. Automated component, bridge, persistence, and native tests cover behavior; a manual cold-launch/hotkey smoke test verifies desktop integration.
diff --git a/docs/quick-pane-v1-implementation-plan.md b/docs/quick-pane-v1-implementation-plan.md
new file mode 100644
index 0000000..3a67ff1
--- /dev/null
+++ b/docs/quick-pane-v1-implementation-plan.md
@@ -0,0 +1,43 @@
+# Quick Pane v1 Implementation Plan
+
+## 1. Define persisted configuration
+
+- Add Quick Pane settings types, defaults, validation, serialization, and migration-safe native storage.
+- Fields: default workspace, quick model, follow-up model, supplemental prompt, maximum seconds, maximum read-only tool calls.
+- Preserve current configuration when validation or save fails.
+
+## 2. Add native window and launch contract
+
+- Extend Tauri configuration and Rust window management with a dedicated Quick Pane window label.
+- Add command-line action used by external WM hotkey to create/show/focus pane, including cold launch.
+- Keep `--toggle` and ordinary `--new` behavior unchanged.
+
+## 3. Build Quick Pane React surface
+
+- Add isolated state reducer/component for request, selected model/workspace, progress, final answer, partial answer, escalation, extension UI, and errors.
+- Keep pane transcript in component memory only and request best-effort runtime/session cleanup on close.
+- Add Settings controls and copyable WM launch command.
+
+## 4. Add constrained bridge runtime mode
+
+- Extend runtime creation with Quick Pane metadata and ephemeral session lifecycle.
+- Build immutable advisory quick-agent instructions around supplemental custom prompt.
+- Use observed tool events and wall-clock cancellation for best-effort caps; document that current Pi RPC cannot trusted-enforce read-only/no-subagent policy or memory-only runtime sessions.
+- Instruct preflight escalation for mutation, subagent, multi-step, or likely-over-budget requests.
+
+## 5. Route interaction and handoff
+
+- Reuse existing extension request/response protocol for inline Quick Pane permission and clarification dialogs.
+- On explicit continuation, create normal runtime using workspace and follow-up model.
+- Prefill it with original request plus escalation note/partial findings.
+
+## 6. Test and document
+
+- Add unit tests for settings validation/persistence and guardrail classification.
+- Add component tests for pane states, picker behavior, inline prompts, error/retry, and cleanup.
+- Add Rust/bridge tests for window command routing, budget cancellation, ephemeral cleanup, and handoff payload.
+- Add manual WM hotkey/cold-launch instructions and smoke checklist.
+
+## Sequencing
+
+Land configuration/runtime contract first, then native window and UI, then handoff and test coverage. Preserve existing normal-session paths throughout.
diff --git a/docs/quick-pane-v1-test-plan.md b/docs/quick-pane-v1-test-plan.md
new file mode 100644
index 0000000..d75c015
--- /dev/null
+++ b/docs/quick-pane-v1-test-plan.md
@@ -0,0 +1,42 @@
+# Quick Pane v1 Test Plan and QA Checklist
+
+## Automated coverage
+
+### Settings
+
+- Valid defaults load when no Quick Pane configuration exists.
+- Valid configuration round-trips through native storage.
+- Invalid workspace, model, prompt, time, and tool values show field errors and retain prior saved state.
+- Quick model picker affects quick request only; follow-up model remains configured default.
+
+### Runtime guardrails
+
+- Quick runtime receives immutable advisory read-only/no-subagent prompt text plus supplemental prompt.
+- Mutation, subagent, multi-step, and likely-over-budget requests are instructed to preflight-escalate.
+- Runtime applies best-effort configured 60-second/6-tool caps from observed events and exposes reason.
+- Partial result survives a cap; pane transcript is removed and runtime cleanup requested after pane close.
+- Tests/documentation state current limitations: Pi RPC cannot trusted-enforce prompt policy or guarantee memory-only temporary sessions.
+
+### UI and bridge
+
+- Quick Pane opens/focuses through dedicated native command without changing normal `--new` or `--toggle` behavior.
+- Workspace and quick-model picker values reach runtime creation.
+- Permission and clarification extension requests render inline and send correct response payloads.
+- Retry, model change, and normal-session actions appear for startup/model/crash errors.
+- Handoff creates normal session with original request, escalation note, workspace, and follow-up model.
+
+## Manual smoke checklist
+
+1. Configure default workspace, models, prompt, and 60-second/6-tool limits.
+2. Add desktop/WM hotkey using Settings launch command.
+3. From no running GUI process, invoke hotkey and verify Quick Pane opens/focuses.
+4. Submit short answer-only request; verify result within 60 seconds.
+5. Submit read-only inspection request; answer any external-read permission prompt inline.
+6. Submit mutation or multi-step request; verify reason and **Continue in full session**.
+7. Continue; verify normal session receives handoff and follow-up model.
+8. Close Quick Pane; reopen and verify quick transcript is absent.
+9. Select unavailable model or induce startup failure; verify inline recovery options.
+
+## Exit criteria
+
+All targeted automated tests pass. Manual cold-launch/hotkey smoke completes. No regressions in normal sessions, `--toggle`, or `--new` behavior.
diff --git a/src/bridge/agent-registry.js b/src/bridge/agent-registry.js
index 539f32d..e8fa161 100644
--- a/src/bridge/agent-registry.js
+++ b/src/bridge/agent-registry.js
@@ -309,7 +309,9 @@ export function createAgentRegistry({
version: 2,
migrated: true,
directories: [...managedWorktrees].sort(),
- runtimes: [...runtimesByRuntimeId.values()].map((runtime) => ({
+ runtimes: [...runtimesByRuntimeId.values()]
+ .filter((runtime) => !runtime.ephemeral)
+ .map((runtime) => ({
runtimeId: runtime.runtimeId,
worktreePath: runtime.worktreePath,
...(runtime.sessionPath ? { sessionPath: runtime.sessionPath } : {}),
@@ -326,7 +328,7 @@ export function createAgentRegistry({
}
function addWorktreeRuntime(runtime) {
- managedWorktrees.add(runtime.worktreePath);
+ if (!runtime.ephemeral) managedWorktrees.add(runtime.worktreePath);
let runtimes = runtimesByWorktreePath.get(runtime.worktreePath);
if (!runtimes) {
runtimes = new Set();
@@ -573,6 +575,7 @@ export function createAgentRegistry({
sessionPath,
sessionId,
legacyDefault = false,
+ ephemeral = false,
openedAt = new Date().toISOString(),
}) {
return {
@@ -583,6 +586,7 @@ export function createAgentRegistry({
sessionPath,
sessionId,
legacyDefault,
+ ephemeral,
openedAt,
state: "starting",
stateVersion: 0,
@@ -719,13 +723,16 @@ export function createAgentRegistry({
sessionPath,
sessionId,
legacyDefault = false,
+ ephemeral = false,
openedAt,
},
{ restore = false } = {},
) {
if (stopping) throw new Error("agent registry is stopping");
const canonicalPath = await realpath(worktreePath);
- const sessionDir = sessionDirectoryFor(sessionRoot, canonicalPath);
+ const sessionDir = ephemeral
+ ? path.join(sessionRoot, "quick", runtimeId)
+ : sessionDirectoryFor(sessionRoot, canonicalPath);
await mkdir(sessionDir, { recursive: true, mode: SESSION_DIRECTORY_MODE });
await chmod(sessionDir, SESSION_DIRECTORY_MODE);
let ownedPath;
@@ -757,6 +764,7 @@ export function createAgentRegistry({
sessionPath: ownedPath,
sessionId,
legacyDefault,
+ ephemeral,
openedAt,
});
if (stopping) throw new Error("agent registry is stopping");
@@ -778,7 +786,7 @@ export function createAgentRegistry({
});
try {
await launch(runtime);
- await persistWorkspace();
+ if (!runtime.ephemeral) await persistWorkspace();
publishWorkspace("runtime_opened", runtime, {
runtime: publicRuntime(runtime),
});
@@ -974,7 +982,7 @@ export function createAgentRegistry({
// Remove desired-open/UI intent now, but retain the session lease until
// the old child can no longer write to its JSONL file.
unindexRuntime(runtime, { releaseSessionLease: false });
- await persistWorkspace();
+ if (!runtime.ephemeral) await persistWorkspace();
runtime.stopped = true;
runtime.supervisor?.stop();
let stopConfirmed = true;
@@ -1002,6 +1010,8 @@ export function createAgentRegistry({
}
}
if (stopConfirmed) releaseClosedSessionLease(runtime);
+ if (runtime.ephemeral && stopConfirmed)
+ await rm(runtime.sessionDir, { recursive: true, force: true }).catch(() => {});
runtime.state = "stopped";
publishWorkspace("runtime_closed", runtime, {
runtimeId: runtime.runtimeId,
@@ -1206,10 +1216,18 @@ export function createAgentRegistry({
async createSessionRuntime(worktreePath) {
return openRuntime({ worktreePath });
},
+ async createQuickRuntime(worktreePath) {
+ return openRuntime({ worktreePath, ephemeral: true });
+ },
async openSessionRuntime(worktreePath, sessionPath) {
return openRuntime({ worktreePath, sessionPath });
},
closeSessionRuntime: closeRuntime,
+ async closeQuickRuntime(runtimeId) {
+ const runtime = getRuntime(runtimeId);
+ if (!runtime.ephemeral) throw new Error("runtime is not a quick runtime");
+ return closeRuntime(runtimeId);
+ },
listAgents() {
return [...runtimesByAgentId.values()]
.map(publicAgent)
diff --git a/src/bridge/service.js b/src/bridge/service.js
index f41eaaa..3907d92 100644
--- a/src/bridge/service.js
+++ b/src/bridge/service.js
@@ -46,6 +46,12 @@ export async function startBridgeService({
request.payload.worktreePath,
),
};
+ case "create_quick_runtime":
+ return {
+ runtime: await registry.createQuickRuntime(
+ request.payload.worktreePath,
+ ),
+ };
case "open_session_runtime":
return {
runtime: await registry.openSessionRuntime(
@@ -55,6 +61,8 @@ export async function startBridgeService({
};
case "close_session_runtime":
return registry.closeSessionRuntime(request.payload.runtimeId);
+ case "close_quick_runtime":
+ return registry.closeQuickRuntime(request.payload.runtimeId);
case "list_directory_sessions":
return {
sessions: await registry.listDirectorySessions(
diff --git a/src/protocol/index.js b/src/protocol/index.js
index 3151bda..37ee92e 100644
--- a/src/protocol/index.js
+++ b/src/protocol/index.js
@@ -12,8 +12,10 @@ const requestOperations = new Map([
["get_workspace", { agent: false, payload: "none" }],
["get_workspace_summary", { agent: false, payload: "none" }],
["create_session_runtime", { agent: false, payload: "worktree" }],
+ ["create_quick_runtime", { agent: false, payload: "worktree" }],
["open_session_runtime", { agent: false, payload: "worktreeSession" }],
["close_session_runtime", { agent: false, payload: "runtime" }],
+ ["close_quick_runtime", { agent: false, payload: "runtime" }],
["list_directory_sessions", { agent: false, payload: "worktree" }],
["get_session_runtime_snapshot", { agent: false, payload: "runtime" }],
["subscribe_workspace", { agent: false, payload: "cursor" }],
diff --git a/test/protocol.test.js b/test/protocol.test.js
index 2fc2a83..e4cb00b 100644
--- a/test/protocol.test.js
+++ b/test/protocol.test.js
@@ -115,8 +115,10 @@ test("accepts additive multi-session runtime operations and numeric workspace cu
{ op: "get_workspace" },
{ op: "get_workspace_summary" },
{ op: "create_session_runtime", payload: { worktreePath } },
+ { op: "create_quick_runtime", payload: { worktreePath } },
{ op: "open_session_runtime", payload: { worktreePath, sessionPath } },
{ op: "close_session_runtime", payload: { runtimeId: "runtime-1" } },
+ { op: "close_quick_runtime", payload: { runtimeId: "runtime-1" } },
{ op: "list_directory_sessions", payload: { worktreePath } },
{
op: "get_session_runtime_snapshot",
diff --git a/test/quick-pane-ui.test.js b/test/quick-pane-ui.test.js
new file mode 100644
index 0000000..b87bdbe
--- /dev/null
+++ b/test/quick-pane-ui.test.js
@@ -0,0 +1,33 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+
+test("Quick Pane polls snapshots, renders answers, and gates handoff", async () => {
+ const source = await readFile(
+ new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
+ "utf8",
+ );
+ assert.match(source, /get_session_runtime_snapshot/);
+ assert.match(source, /setInterval\(\(\) => void refresh\(\), 800\)/);
+ assert.match(
+ source,
+ /
\{answer\}<\/pre>/,
+ );
+ assert.match(
+ source,
+ /\{\(escalation \|\| error\) && \(\s*