@elvinw/pi-qq

/qq — quick side questions for pi that don't enter the conversation context (Claude Code's /btw, for pi)

Packages

Package details

extension

Install @elvinw/pi-qq from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@elvinw/pi-qq
Package
@elvinw/pi-qq
Version
0.5.2
Published
Oct 15, 2026
Downloads
732/mo · 454/wk
Author
elvinw
License
MIT
Types
extension
Size
66.8 KB
Dependencies
0 dependencies · 0 peers

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

pi-qq — /qq quick side question

Ask a one-off "by the way" question without letting the Q&A enter the ongoing context — the same idea as Claude Code's /btw.

True deletion: dismissed threads are really gone

Mainline pi's session is append-only: dismissing a /qq thread can at best move the leaf back, leaving the Q&A as a dead branch in the file (visible in /tree). This extension closes that gap. At load time it additively installs pruneBranches(keepLeafId?) on SessionManager.prototype — keeping only the root→leaf path, rebuilding pi's own index, and rewriting the session file. Never overriding an existing method, and only when the running pi version is in the verified allowlist (config.patchTargetPiVersions, currently 1.1.0 and 1.2.0 — the patch touches private-by-convention internals, so a version is added only after confirming SessionManager is byte-identical).

Dismiss and /nvm then drop the thread through a ladder — the first tier that works wins:

  1. prune (pin matches the running pi): the thread is physically removed from the session file — no dead branch, same session id. (Other orphan branches are tidied too.)
  2. rewind (any other pi): the baseline — the thread is orphaned in the tree (visible in /tree). There is deliberately no fork tier: a fork would create a duplicate session file in /resume on every dismiss. A one-time notice makes the degradation visible.

Transient failure paths (no answer, timeouts) still use a plain rewind: they discard one small failed question and must not churn session files.

Good to know: the prune tier rewrites your session files in place (the thread's nodes are removed from the JSONL). The patch itself is in-memory only and additive — it refuses to install on any pi other than the exact pinned one, and uninstalling the extension leaves pi completely untouched. Still, if you run valuable sessions, keep your usual ~/.pi backups.

Commands

Command Tool policy during the question
/qq <question> prompt asks the model to use no tools
/qqro <question> prompt asks the model to use only read-only tools (read, grep, find, ls)
/ro Side thread only: toggle the follow-up policy between no-tools and read-only
/sub Side thread only: start a sub-thread from the current position. It stays part of the SAME side session (a context-invisible boundary marker shows it in /tree and on resume); /nvm and /merge always act on the whole session. (Named /sub because pi's built-in /fork — fork the session — wins the bare /fork exact match.)
/merge Side thread only: keep everything — initial /qq + every follow-up + every sub-thread — as conversation. The side session becomes mainline, the mode ends, and the next message continues right after the last answer.
/nvm Side thread only: never mind — exit the mode and rewind the whole side session (initial /qq + follow-ups + sub-threads) to its first question, as if it never happened

The side thread is visible everywhere:

  • Input box: while the thread is open, the editor's top border becomes a "qq side thread — … /ro read-only · /sub sub-thread · /merge to main · /nvm back" title (live: the sub-thread depth updates on /sub) and the footer status shows the live tool policy and depth.
  • Command list: hidden == not dispatchable. Inside the thread /qq and /qqro disappear from the list and do not run either (plain text is the question now). /ro, /sub, /merge and /nvm are not registered commands at all — an input-level handler owns them while the thread is open, and outside it they pass through to the model as ordinary messages, exactly like any unknown slash text. A mistyped mode command (/nvm', /subx, …) is a typo, not a command: inside the thread it runs as a follow-up question (the box IS the side conversation), with a note telling you the exact spelling.

Neither command touches the active tool set: the side request is a byte-identical prefix extension of your last turn, so on llama.cpp/vLLM it costs zero extra prefill (the server's prefix cache only grows, and the cache is still intact for the next main turn). Tool policy is two-layer:

  1. the prompt asks the model to comply (no tools / read-only only), and
  2. a hard guardrail blocks any out-of-policy tool call before it executes — the model receives the block reason as a tool error and falls back to answering in text. A rogue write/bash can never run during a side question.

How it works

The Q&A takes over the main transcript like a normal turn and stays there until you decide its fate. The first side answer ends in a keypress choice:

  • f → open the side thread: the input box becomes your side conversation. Plain text is the next side question (answered with the whole thread in context, no /qq prefix needed). /ro toggles the follow-up policy; /sub starts a sub-thread (a boundary within the same session — it does not change what /nvm and /merge span); /merge keeps the whole session as conversation. Once in the thread there is no keypress prompt after a follow-up answer — it simply stays on the branch and you keep typing the next follow-up. The session ends when you run /nvm (drop it), /merge (keep it), or when a normal (non-/qq) message lands via another path (rpc driver, genuinely unknown slash text, …) — that message absorbs the thread: it can never be rewound away again. (A mistyped mode command is NOT such a message — see near-miss above.)
  • exit mid-thread → the thread survives in the session file. When pi next starts on that session, the extension detects that the conversation ends in side Q&A and re-opens the side thread: an answered one re- enters the mode (type to follow up, /nvm drops it); one that was still streaming only restores the thread so /nvm can still drop it. A merged thread (its merge left a context-invisible close marker) and an absorbed one are correctly NOT re-opened.
  • m → merge (first answer only): the side Q&A becomes part of the conversation and the next message continues right after the answer. (In the thread, /merge is the command-level equivalent — same effect, any time.)
  • any other key → dismiss (first answer only): the whole side thread (every question and answer in it) is rewound away and the conversation continues as if it never happened. Printable keys pass through, so you can start typing your next prompt immediately.

Mechanically, a pi session is an append-only tree of entries and the model context is always built from the root→leaf path. /qq exploits that:

  1. Remembers the current leaf (homeLeaf), appends a marker entry, and sends the question (with its tool-policy note) — the Q&A lands on a temporary side branch.
  2. Waits for the run to fully settle (agent_settled).
  3. First answer: puts up "f follow-up · m merges · any other key dismisses" (footer status + notification) and waits for your keypress. Follow-up answer (side thread open): no keypress at all — the answer stays and the input box is already yours.
  4. On dismiss//nvm, drops the thread at its root — the leaf before the thread's first question, so a whole follow-up thread vanishes at once — via the first available tier: prune (exact pi match) → leaf rewind (any other pi). On merge, it simply doesn't rewind.

After a dismiss the exchange is excluded from all future model requests and compaction. With the self-patch active it is also physically removed from the session file; on any other pi it remains as a dead branch, visible in /tree (you can re-attach it later by navigating to it).

Compaction safety

A side question must never compact the main conversation. If the session is near its token limit, the side turn itself can trip auto-compaction — and a compaction entry stranded on the main line would truncate the real history out of context (its summary would have been written from side-branch context). So during a side turn pi-qq:

  • cancels auto-compaction (session_before_compact → cancel),
  • waits for true idle (no run, no compaction) before rewinding — navigateTree refuses while the session is busy,
  • notifies when a rewind fails instead of failing silently (the Q&A stays visible so you can see it; re-run the command to dismiss again),
  • warns up front when the context is ≥ 90% of the window (the side turn may overflow; /compact first is the clean move).

Cost note: the side request still sends the full current context (the model needs it to answer "which of the two approaches…"). What /qq saves is the recurring cost — the exchange never rides along in every subsequent turn.

Install

pi install npm:@elvinw/pi-qq

Works on any pi: on pi 1.1.0 or 1.2.0, dismissed threads are physically pruned from the session file (see above); on other pi versions the extension automatically degrades to the plain dead-branch behavior.

or from a local clone:

pi --extension ./pi-qq

or place this directory in ~/.pi/agent/extensions/ (personal) or .pi/extensions/ (project), then /reload.

No npm install is needed to run it: the only runtime import (CustomEditor, for the mode's input-box title) is provided by pi itself when it loads the extension.

Development

npm install        # pulls the published pi-coding-agent 1.2.0 types + tsc
npm run typecheck
npm test           # 57 scenario tests (node:test, no extra deps)

The pi-coding-agent dev-dependency is exactly pinned (no ^) because the self-patch — and the tests around it — target a verified pi version (1.2.0, the newest in the allowlist); it also exists for local typechecking against the published API. node_modules/ is git-ignored.

Tests

test/ runs the extension against a mock pi environment (the extension imports pi's types only — no runtime dependency — so a mock ExtensionAPI plus an in-memory session tree exercise the real logic, no pi process needed). The scenario matrix covers:

  • lifecycle — send → settle → PENDING → dismiss/merge, key semantics (case, pass-through vs consume), status line, prompt notes
  • threads — follow-up chains, context inheritance, thread absorption by a normal message, orphaning via /tree
  • side-thread mode — plain-text follow-ups, no keypress gate after a follow-up answer (the next plain text is the next follow-up), /ro toggling (and non-reset across follow-ups), /nvm (with and without a thread, in-flight guard), auto-exit on /compact//tree//new effects, recursion guard, slash pass-through, in-flight guard, full e2e loop, near-miss (a mistyped mode command like /nvm' runs as a follow-up question, thread intact, and passes through as an ordinary message outside the thread)
  • sub-threads & merge — /sub appends a context-invisible boundary (depth shown live in the status and the editor title) and the session continues; /nvm drops the WHOLE session (initial /qq + sub-threads); /merge keeps the whole chain in order + close marker and ends the mode; both pass through as ordinary messages outside the thread; both are refused while a question is in flight; resume re-enters at the right depth and the walker treats fork boundaries as transparent (unit-tested, incl. merged/absorbed-after-sub and unanswered-after-sub)
  • mode surface — autocomplete hides /qq+/qqro inside the thread and hidden == not dispatched (/ro, /sub, /merge and /nvm are input-level, not commands, and pass through as ordinary messages outside the thread); the editor border carries the live mode title in mode only
  • failures — no answer, settle timeout, failed question mid-thread, stuck-busy rewind
  • queue — busy-on-call, re-settle loop, queue timeout, in-flight guard
  • invariants — zero leaked subscriptions on every path (including mode exit), compaction cancellation, tool guardrail (block/allow matrix), session swap, empty session, preflight warning
  • resume — pi exited mid-thread: answered thread re-enters the mode (follow-up continues, /nvm drops the whole session, incl. sub-threads), merged/absorbed threads are not re-opened, unanswered (mid-stream) restore only the thread for /nvm, plain sessions untouched
  • true deletion — installSessionPrune on the exact pinned pi version (idempotent, version-mismatch refusal) and a real SessionManager integration test (prune keeps root→target, re-pins the leaf, rewrites the file, reopens cleanly); the drop ladder on dismiss and /nvm (prune / rewind tiers — never fork)

Safety

  • Tool policy is two-layer while the tool set itself stays untouched (deliberate, for cache preservation): the prompt asks the model to comply, and a tool_call guardrail blocks any out-of-policy call before it executes — the model sees the block reason as a tool error and answers in text instead. Nothing can write files or run commands during /qq.
  • If an answer is aborted or errors before producing text, only that question is rewound away immediately (nothing to read); the rest of an in-progress thread survives.
  • Exitting pi mid-thread never loses the thread: it is re-detected from the session file on the next start (see exit mid-thread above). Merge leaves a context-invisible close marker so a resumed session can tell a merged thread from an open one.
  • /new, /fork, or quit mid-question: the wait bails out; the side branch is just an orphan branch (harmless). Inside an open side thread these commands (and /compact, /tree navigation) end the mode automatically — the thread is absorbed or orphaned and plain text goes back to the main conversation.
  • In side-thread mode, plain text submitted while a question is still streaming is dropped with a warning (strictly serial — one question at a time, single-slot llama.cpp safe).
  • Double /qq//qqro is rejected while one is running or queued.
  • If the agent is running, the question is queued and runs automatically when the flow settles (aborting the flow unblocks it). Still strictly serial — compatible with single-slot llama.cpp (-np 1). A 2h wait cap drops the question rather than wedging the command.