@elvinw/pi-qq
v0.5.2
Published
/qq — quick side questions for pi that don't enter the conversation context (Claude Code's /btw, for pi)
Maintainers
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:
- 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.)
- 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/resumeon 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
~/.pibackups.
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
/qqand/qqrodisappear from the list and do not run either (plain text is the question now)./ro,/sub,/mergeand/nvmare 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:
- the prompt asks the model to comply (no tools / read-only only), and
- 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/bashcan 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
/qqprefix needed)./rotoggles the follow-up policy;/substarts a sub-thread (a boundary within the same session — it does not change what/nvmand/mergespan);/mergekeeps 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,
/nvmdrops it); one that was still streaming only restores the thread so/nvmcan 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,
/mergeis 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:
- 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. - Waits for the run to fully settle (
agent_settled). - 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.
- 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 —
navigateTreerefuses 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;
/compactfirst 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-qqWorks 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-qqor 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),
/rotoggling (and non-reset across follow-ups),/nvm(with and without a thread, in-flight guard), auto-exit on/compact//tree//neweffects, 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 —
/subappends a context-invisible boundary (depth shown live in the status and the editor title) and the session continues;/nvmdrops the WHOLE session (initial/qq+ sub-threads);/mergekeeps 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+/qqroinside the thread and hidden == not dispatched (/ro,/sub,/mergeand/nvmare 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,
/nvmdrops 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 —
installSessionPruneon the exact pinned pi version (idempotent, version-mismatch refusal) and a realSessionManagerintegration 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_callguardrail 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,/treenavigation) 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//qqrois 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.
