Repository navigation
feat(cli): add /btw for one-shot side questions - #268
Merged
Merged
Conversation
Ask a question without disturbing the main conversation. `/btw <question>` opens an overlay, answers it, and leaves the session untouched: nothing is appended to the transcript, the session file, or the model's context. The answer comes from `modelRegistry.complete()`, which talks to the same provider with the same credentials but owns no agent and no session. That is what keeps the exchange out of the conversation, and it also means a main turn in flight is unaffected, since the side question never enters the agent loop. Tools are unreachable for the same reason: `complete()` has no tool loop. The question is sent against a copy of the current branch's messages, with a trailing assistant message holding unanswered tool calls dropped -- that is exactly the state a streaming main turn leaves behind, and providers that validate message order reject it. Prior side questions are kept in an in-memory `BtwHistory` (last 20) and replayed only into the next side question, so follow-ups have context while main turns stay blind to them. It is deliberately not persisted: writing it down would put the exchanges back into the session the command exists to keep them out of. Panel chords: esc close, c copy, x clear history, shift+left/right browse earlier exchanges, up/down scroll. The waiting spinner reuses pi-tui's Loader as a frame clock only, the way the bash card does, and its interval is released on both finish() and dispose().
lit26
requested review from
MelodyVAR,
Uking-xxx,
ZouR-Ma and
liuedcson
as code owners
October 19, 2026 14:02
ZouR-Ma
approved these changes
Oct 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
/btw <question>asks a quick question without disturbing the main conversation. An overlay opens, answers, and closes onesc— nothing is appended to the transcript, the session file, or the model's context.Why this shape
The obvious implementations both fail:
interactive-mode.tsrenders every one of those unconditionally. The answer appears twice: once in the overlay, once below it.So the side question never enters the agent loop at all. It goes through
modelRegistry.complete(), which talks to the same provider with the same credentials but owns no agent and no session. There is nothing to append and nothing to rewind, a main turn in flight is untouched, and tools are unreachable becausecomplete()has no tool loop.Details worth a second look
buildContextEntries→convertToLlm), with a trailing assistant message holding unanswered tool calls dropped. That is exactly the state a streaming main turn leaves behind, and providers that validate message order reject it.BtwHistory, capped at 20, and are replayed only into the next side question. Main turns never see them. Deliberately not persisted: writing it down would put the exchanges back into the session this command exists to keep them out of.Loaderas a frame clock only — the frame is read out of its paint callback and the panel draws its own row, matchingview/transcript/bash-execution.ts. The interval is released on bothfinish()anddispose(); the overlay host already callsdispose()on close.Panel
escclose ·ccopy ·xclear history ·shift+←/→browse earlier exchanges ·↑/↓scroll. Shows(+N earlier /btw)when history is non-empty.Tests
Three cases in
test/step-slash-commands.test.ts(30 passing in that file):sendMessageis never called, the dangling tool call is stripped from the fork, the answer reaches the panel, the next side question carries the previous answer, andxclears itLoaderclock, andfinish()+dispose()stop requesting renders (no leaked timer per/btw)pnpm checkpasses (biome + all 16 repo invariant checks +tsgo --noEmit).Known gaps
complete()resolves with the finished message, so the panel shows the spinner and then the whole answer. Streaming would needstream()exposed on theModelRegistryfacade.showExtensionCustomcallscomponent.dispose()on close) rather than observed end to end.