Skip to content

Port Pi's fork-from-an-earlier-message flow whole - #2

Merged
navbytes merged 3 commits into
mainfrom
claude/pi-context-tree-workflow-sjuutd
Sep 4, 2026
Merged

Port Pi's fork-from-an-earlier-message flow whole#2
navbytes merged 3 commits into
mainfrom
claude/pi-context-tree-workflow-sjuutd

Conversation

@navbytes

@navbytes navbytes commented Sep 4, 2026

Copy link
Copy Markdown
Owner

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#showTreeSelectoragent-session.ts#navigateTreecompaction/branch-summarization.ts) rather than from the docs, so the semantics match rather than the wording.

What changed

before now
dialog on yes/no confirm, then a separate summary picker one picker; its title says what will do, so the choice is the confirmation
esc in the picker jumped anyway, silently without a summary back on the same row, nothing forked
cancel the custom prompt silently meant "no summary" back to the three choices (Pi's loop)
what gets summarized the whole current session only the turns the move abandons
order of operations fork first, then summarize abort stream → draft → move → inject
┌ Fork & prefill this turn? ───────────────────────────────────────────────┐
│ No summary                              start clean · nothing carried over│
│ Summarize everything below this point   carry the 3 turns · ~14k over…   │
│ Summarize with a custom prompt          the same, with your own focus    │
└──────────────────────────────────────────────────────────────────────────┘

A 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.fork re-ids the copied prefix.

So core/tree.ts#spineOf reduces a session to its ordered sessionID:messageID path from the root — an ancestor's copied prefix keeps the ancestor's own ids — and core/actions.ts#abandonedTail takes the deepest entry present in both spines as the common ancestor; everything after it in the current session is the tail. A fork plan cuts the target spine before its boundary, because session.fork copies 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. buildSpineMap is refactored onto spineOf so 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 its branch_summary entry at the new leaf. Drafting first is what makes esc meaningful: 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.

summarizeInto splits into draftBranchSummary + injectBranchSummary, and the transcript serialization the merge drafter already uses (core/decision.ts#transcriptText) is reused for it.

Tests

  • 11 new unit tests: the tail computation across trunk / branch / sibling / ancestor jumps against the existing fixture, and the dialog copy.
  • A new pty e2e against the real OpenCode 1.18 TUI: all three options on screen in Pi's order, esc forks nothing, exactly one branch.opened{kind:"redo"}, summary.recorded, and the fork's next model request carries the injected summary (a noReply injection only reaches the provider on the following turn).
  • Full unit suite, typecheck and build green.

Not verified here: the server half of the e2e suite cannot run in this container — a bare opencode serve in an empty directory with no plugin installed never binds. That half is untouched by this change; worth a local bun run test:e2e before release.

Changelog

v0.2.2 was already tagged from 960105a, 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

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
@navbytes
navbytes merged commit d387c19 into main Sep 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants