Port Pi's fork-from-an-earlier-message flow whole - #2
Merged
Conversation
Pressing enter on a row above where you are now opened a yes/no confirm and
then, separately, "Summarize the branch you are leaving?". Pi asks one
question, and that question is the confirmation. It also summarizes something
narrower than we did.
- One dialog, Pi's three answers in Pi's order: No summary / Summarize
everything below this point / Summarize with a custom prompt. The title says
what enter will do, and the option lines say how much you are leaving
("carry the 3 turns · ~14k over as one summary"). A switch has no picked
point, so its middle answer reads "Summarize what you are leaving".
- The summary now covers what the jump abandons rather than the whole session.
Pi collects the entries from the old leaf back to the common ancestor with
the target; core/tree.ts#spineOf reduces both sides to their ordered
sessionID:messageID path (an ancestor's copied prefix keeps the ancestor's
own ids), the deepest shared entry is the common ancestor, and everything
after it in the current session is the tail — core/actions.ts#abandonedTail,
unit tested against the trunk/branch/sibling fixture.
- esc in the picker really cancels: it puts you back on the same row with
nothing forked, where before it moved you anyway without a summary.
Cancelling the custom-prompt editor loops back to the three choices instead
of quietly meaning "no summary".
- Nothing moves until the summary exists. executeJump follows navigateTree's
order — abort a streaming reply, draft, then fork/switch, then inject at the
destination — so esc while it drafts aborts the helper session's reply and
the whole jump. A summary that fails outright still lets the move through,
with a notice.
- A jump with nothing below the selected point, and jumpSummary: "never",
skip the question and show the plain confirm.
summarizeInto splits into draftBranchSummary + injectBranchSummary so the
draft can be cancelled before anything is written, and the transcript
serialization the merge drafter already uses is reused for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
…that proves the summary landed pyte is optional, so the per-screen snapshots runTuiScreens returns are empty wherever it is not installed; the other pty tests already match against the flat ANSI-stripped capture, so this one does too. The summary is injected with noReply, which means it reaches the provider only on the next turn: the test now sends the fork's pre-filled prompt, so the model request that follows is the proof the summary is really in the fork's context. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
v0.2.2 was already tagged from the commit this branch forks from, so the "(unreleased)" heading was stale and the new bullets belonged above it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
This was referenced Sep 4, 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.
Pressing
⏎on a row above where you are now opened a yes/no confirm and then, separately, "Summarize the branch you are leaving?". Pi asks one question, and that question is the confirmation. It also summarizes something narrower than we did.Read from Pi's source (
interactive-mode.ts#showTreeSelector→agent-session.ts#navigateTree→compaction/branch-summarization.ts) rather than from the docs, so the semantics match rather than the wording.What changed
⏎⏎will do, so the choice is the confirmationescin the pickerA switch to another branch has no picked point, so its middle answer reads "Summarize what you are leaving".
jumpSummary: "never", and a jump with nothing below the selected point, degrade to the plain confirm."Everything below that point"
The substantive piece. Pi collects the entries from the old leaf back to the common ancestor with the target and summarizes those; OpenCode has no such tree, since each branch is its own session and
session.forkre-ids the copied prefix.So
core/tree.ts#spineOfreduces a session to its orderedsessionID:messageIDpath from the root — an ancestor's copied prefix keeps the ancestor's own ids — andcore/actions.ts#abandonedTailtakes the deepest entry present in both spines as the common ancestor; everything after it in the current session is the tail. Aforkplan cuts the target spine before its boundary, becausesession.forkcopies messages strictly before it.Redoing trunk turn 2 summarizes turns 2–3. Switching from a branch to a sibling summarizes the branch's own turns and not the shared trunk.
buildSpineMapis refactored ontospineOfso the walk has one definition.Order of operations
Matching
navigateTree: abort a streaming reply first (so the summary covers the reply as it actually ended) → draft the summary while nothing has moved yet → fork or switch → inject at the destination, the way Pi attaches itsbranch_summaryentry at the new leaf. Drafting first is what makesescmeaningful: it aborts the helper session's in-flight reply and the whole jump. A summary that fails (rather than being aborted) still lets the move through, with a notice — the alternative is stranding you on the session you asked to leave.summarizeIntosplits intodraftBranchSummary+injectBranchSummary, and the transcript serialization the merge drafter already uses (core/decision.ts#transcriptText) is reused for it.Tests
escforks nothing, exactly onebranch.opened{kind:"redo"},summary.recorded, and the fork's next model request carries the injected summary (anoReplyinjection only reaches the provider on the following turn).Not verified here: the
serverhalf of the e2e suite cannot run in this container — a bareopencode servein an empty directory with no plugin installed never binds. That half is untouched by this change; worth a localbun run test:e2ebefore release.Changelog
v0.2.2was already tagged from960105a, the commit this branch forks from, so its(unreleased)heading was stale. This adds a## 0.2.3 (unreleased)section above it and dates## 0.2.2 — 2026-09-03.🤖 Generated with Claude Code
https://claude.ai/code/session_01BrwAnTfTmJSmFwVn3DDFxr
Generated by Claude Code