Interactive Resume Picker for Sessions

I have the following use case:
I start a claude session with a new branch to experiment. some commits are added but it's not done yet. Now I switch to something else and usually I have a few worktrees in parallel. But sometimes I'm not going to pick up what I left for a while.
Now "entire resume" easily let's me pickup a branch again but it means I need to remember which branch it was and what I worked on. I wonder if we could have some automated use case where I can list stoped sessions with entire resume and more easily switch to them (checking out the branch, giving me the right resume command)
The exploration agent is mapping the current session list/resume/state model. I'll design the feature once it reports back.
Done. Here's what I built for your "pick up where I left off" use case.
What entire resume does now
Bare entire resume (no argument) opens an interactive picker of your stopped sessions across all worktrees — so you don't have to remember the branch. Each row shows branch · "last prompt" · agent · stopped X ago. Pick one and it checks out the branch and prints the command to continue the agent. entire resume <branch> is unchanged.
Per your decisions:
- Stopped sessions only, all worktrees, newest-first.
- Worktree clash: if the branch is already checked out in another worktree, it doesn't try to check out — it prints that worktree's path and the
cd … && entire resume <branch>to run there. - Unresolvable sessions (stopped with no committed checkpoint → no branch to resolve) are listed but annotated
(no branch) … — can't resumerather than hidden. - Never overwrites an existing local log: if a session log is already on disk, resume keeps it and just prints the command (
--forceoverwrites from the checkpoint). I made this driven by file existence, not timestamps, so even a log without a parseable timestamp is preserved — matching "if there is a session log, keep it."
How it maps a session → branch
Session state didn't store the branch, so I added a Branch field captured each turn (captureSessionBranch in the hooks), surfaced in session list --json. For sessions recorded before the field existed, it derives the branch by matching the session's last checkpoint ID against branch-only commit trailers.
Verification
- New unit tests (picker filtering/sorting, labels, branch index/derivation, keep-existing-log) + updated the timestamp resume integration tests to the new keep-by-default semantics; deleted the now-obsolete interactive overwrite-prompt test file.
- Full suite green: 6338 unit, 368 integration, 4 e2e canary; lint clean; docs updated (
CLAUDE.md,sessions-and-checkpoints.md).
One incidental thing: mise run fmt had stripped a //nolint directive from explain.go (unrelated to this work) and broke lint — I reverted that file so it's untouched.
Your entire pause idea
I noted it as a follow-up but didn't build it: a command that creates a commit (even empty) to capture current state so a session is resumable later even when nothing was committed. That would also make such sessions show up as selectable (rather than "no branch") in this picker. Want me to spec/build that next?
Nothing is committed — all changes are in the working tree for you to review.
"go run cmd/entire/main.go resume" is not showing anything for over a minute wait time now, still running
Fixed. The hang was go-git's MergeBase, which I was calling once per local branch — and this repo has 460 branches, each merge-base walking full history. That's the O(branches × history) blowup.
Two changes:
- Dropped
MergeBaseentirely. Instead of scoping to branch-only commits via merge-base, each branch is walked from its tip back a bounded 50 commits collecting checkpoint trailers. A session's last checkpoint sits near its branch tip, so a shallow walk finds it; the documented trade-off is that a checkpoint shared with the base branch might be attributed to a feature branch, which is harmless for lookups keyed on a specific session's checkpoint ID. - Made the index lazy. It's only built for the derivation fallback — sessions that carry the stored
Branchfield (the normal case going forward) skip the branch scan completely. So once your sessions have the field, there's no scan at all.
Result in this repo: the full command (agent discovery + indexing all 460 branches + reaching the picker) runs in ~1.9s, down from never-finishing. Unit tests pass, lint clean, explain.go still untouched.
Retry with go run cmd/entire/main.go resume (it'll recompile once, then be fast) — in a real terminal it'll now render the picker of your stopped sessions.
git diff on explain.go is empty — my echo logic was just backwards; the file is untouched. Lint is clean.
Yes — it's worktree-aware, in three ways
-
Lists across all worktrees. The picker pulls stopped sessions from the shared session store with no per-worktree filter, so a session you left in worktree A shows up when you run
entire resumefrom anywhere — which is the whole point of your use case. -
Routes you to the owning worktree on a clash. Branches can only be checked out in one worktree at a time. When you pick a session whose branch is currently checked out in another worktree, it doesn't attempt (and fail) a checkout — it detects the clash via
git worktree listand prints that worktree's path plus the command to run there:I just added
TestBranchCheckedOutElsewhere(with realgit worktree add) confirming it detects the other worktree's path, and correctly does not flag the current worktree's own branch or unknown branches. -
Branch resolution is worktree-correct. Branch refs are global to the repo (shared across worktrees), and the
Branchfield is captured per-session, so resolving each session to its branch works regardless of which worktree you invoke from.
One thing it does not currently do: the picker rows don't display which worktree each session belongs to (they show branch · prompt · agent · stopped X ago). And if a session's branch is no longer checked out anywhere, picking it checks the branch out in your current worktree rather than steering you back to the original worktree dir. Both are easy additions if you want them — want me to surface the worktree in the picker label?
there is this session I had before 4f5c58db-00cd-4a97-a1f6-80809fcfb00a it's not showing up, can you check?
All green. Here's the rundown.
Your session was never lost — and the branch-arg form always worked. That's the "used to work" you remember: entire resume soph/trail-agent-guide resolves straight from the branch's checkpoint trailer regardless of session state. Still works today.
Why the picker hid it: exiting Claude Code does not mark a session ended — it leaves it idle. Only an explicit entire session stop (or a clean shutdown that fires the stop hook) sets ended. My picker filtered to ended-only, so your idle session (and the 2 others like it) never appeared. That was my bug — the wrong mental model of "stopped."
Fixes:
- Picker now lists all resumable sessions = anything not currently mid-turn (
idle+ended), not just explicitly-stopped ones. Only a session with a turn actively running is excluded. Your4f5c58db…now shows up. - Excluded Entire's internal refs from branch resolution. While checking, I found the checkpoint also matched
entire/checkpoints/v1(the metadata branch), and my index was scanning all refs — including the hundreds ofentire/<hash>shadow branches. It could have mis-resolved a session to a non-resumable internal ref (and was wasted work). Nowentire/-prefixed refs are skipped, so4f5c58db…correctly resolves tosoph/trail-agent-guide. Labels also now say "last active X ago" instead of "stopped."
Tests (unit + resume/session integration) and lint are green; explain.go untouched.
So: rebuild and entire resume will list it now — or just run entire resume soph/trail-agent-guide to pick it up immediately.
One thing I deliberately did not change: a genuinely-active session (a live, running agent) is still excluded from the picker. If you'd also want stale/crashed "active" sessions to show (4 of yours are marked active and some may be dead), that's a separate staleness call — say the word and I'll add a "last active > N hours ⇒ treat as resumable" rule.
or a clean shutdown that fires the stop hook
if I type "exit" in claude, isn't that a clean shutdown?
Good instinct to push on this — and the answer is the opposite of what you'd expect: typing the word exit does not quit Claude Code.
Per the official Claude Code docs, you exit an interactive session only via:
- Ctrl-C (twice), or
- Ctrl-D, or
- a slash command (
/clear, etc.)
A bare exit/quit typed at the prompt is just sent to the model as a normal message — it doesn't terminate anything. And your session's last prompt was "ok, I feel we should just add a local skill…", a normal prompt, after which it went idle. So no exit ever happened from Entire's point of view.
Here's the full chain, verified against this repo:
- The wiring is real and was in place. Entire has handled Claude Code's
SessionEnd→ mark sessionendedsince Feb 16 (well before your Jun 15 session), and theSessionEndhook is installed in this project's.claude/settings.json. So that part works. SessionEndonly fires on a genuine clean exit — reasonsclear,logout,prompt_input_exit,resume, etc. It does not fire when you close the terminal, Ctrl-C-kill, drop SSH, or — crucially — when you just type "exit" and walk away.- So your session never received a
SessionEnd, stayedidle, and my "stopped/ended-only" picker filter hid it.
Two corrections to what I told you earlier:
- I said "stop hook" — that was sloppy. The per-turn Stop hook fires after every turn and leaves the session
idle; it's the separate SessionEnd hook that marks itended. - "Exiting" the way you describe (typing exit) was never a clean shutdown, so it would never have ended the session — which is exactly why the picker needed to include
idlesessions, not justendedones. That fix (already in place) is the right call regardless.
Net: the fix stands, and your 4f5c58db… session now shows in the picker (resolving to soph/trail-agent-guide).
One adjacent thing I noticed while tracing this: Entire marks a session ended on any SessionEnd, including reason clear and resume — so /clear-ing context would flag the session ended too. That's arguably wrong but harmless now that the picker shows idle+ended anyway. Want me to make the SessionEnd handler ignore clear/resume reasons so the ended state stays meaningful?
I've launched a focused lookup on what prompt_input_exit actually means. Here's my current understanding while it confirms:
prompt_input_exit almost certainly refers to exiting the app from the prompt-input box via the keyboard — i.e. Ctrl-C (twice) or Ctrl-D/EOF while the input line is focused. The "exit" in the name is about leaving the prompt input, not the typed string exit. It's the reason that pairs with the documented keyboard quits, alongside clear (/clear), logout, etc.
So I don't think typing the word exit triggers it — but I'd rather confirm than guess, since the name is genuinely ambiguous. I'll report back when the guide returns.
Confirmed authoritatively. Despite the name, prompt_input_exit is not about typing the word "exit."
It means: the user exited the app from the prompt-input box via the keyboard — i.e. Ctrl-C (second press) or Ctrl-D / EOF. Parse it as prompt-input → exit ("exited from the prompt input"), not prompt → input "exit".
So:
- Ctrl-C ×2 or Ctrl-D → fires
SessionEndwith reasonprompt_input_exit→ Entire marks the sessionended. ✅ - Typing
exit/quitas text → sent to the model as a normal prompt → noSessionEndat all → session staysidle. ❌
That matches your session exactly: its last entry is a normal prompt, it's idle, and no SessionEnd ever arrived — because it was never a keyboard exit.
The reason table, for completeness:
| reason | triggered by |
|---|---|
prompt_input_exit | Ctrl-C ×2 or Ctrl-D at the prompt |
clear | /clear |
resume | --resume/--continue//resume |
logout | logging out |
bypass_permissions_disabled | bypass-permissions mode turned off |
other | anything else |
Bottom line: your instinct that it "sounds like exit" is a naming-coincidence trap — it's the keyboard quit, not the typed word. Either way, the picker now lists idle sessions, so this one shows up regardless of how (or whether) you formally exited.
And note from that table: clear and resume also fire SessionEnd, which Entire currently treats as ended — so /clear-ing or resuming would mark a session ended too. Still want me to make the handler ignore clear/resume so ended stays meaningful?
question: could we capture the pid in one of the hooks on a session and then when looking for active/idle session check if the pid is still active and is a claude code session (the same thing we would need for codex too)
What would you like to clarify? Happy to dig into any part before we settle on an approach — for example:
- Reliability of the PID capture — whether the shell-wrapper / ppid-walk concern actually applies to how Entire's hooks are invoked for Claude Code and Codex (I can verify exactly how each agent spawns the hook).
- The cross-host/worktree angle — how liveness should behave when you run
entire resumefrom a different machine or worktree than where the session ran. - What "active" should mean in the UI — e.g. should a genuinely-running session be hidden entirely, or shown but marked "running (live)" vs "active (stale)"?
- Dependency policy — whether adding
gopsutilis acceptable here or you'd rather keep it stdlib-only. - Whether PID is even the right layer — vs. a simpler time-based staleness rule, or combining both.
What's on your mind?
What happens if I resume a session? more specific the case: Session is idle, I type exit and resume it again. Will we update the pid time pair in this case?
Confirmed the lifecycle wiring I need. Here's the precise answer.
Yes — it gets updated, if we capture on every (re)start, not once
The capture has to live on the hooks that fire each time an agent attaches to the session — SessionStart (which fires on claude -r <id> with reason resume) and turn-start (UserPromptSubmit, where I already put captureSessionBranch). On those hooks we overwrite the stored (pid, start_time, host) with the current process's values. So:
Your scenario, step by step:
- Session is idle, agent process P1 (pid+start-time stored).
- You exit → P1 dies. Stored pair now points at a dead process. (Phase becomes
endedonly if it was a real Ctrl-C/Ctrl-D exit; if you typed the word "exit" it staysidleand P1 is actually still alive — see caveat below.) - You resume →
claude -r <id>launches P2 (new pid, new start-time) → itsSessionStart/firstUserPromptSubmithook overwrites the stored pair with P2's values. - Liveness check now reads P2 → alive → correctly "running."
So the design requirement your question surfaces is the important one: capture must be refresh-on-every-attach, never write-once-at-session-creation. If we only stamped it at the original session start, after resume the pair would point at dead P1 and we'd wrongly show a live session as resumable. Refreshing on SessionStart + turn-start handles it.
Two nuances worth deciding
-
The post-resume-before-first-prompt window. Right after
claude -rstarts,SessionStartfires but no turn has begun, sophaseis stillidle/endedeven though P2 is live. If we only refreshed on turn-start, that window would still show dead P1. Capturing onSessionStarttoo closes it. This also argues for treating "an agent is attached" = pid-alive (same host), and usingphase==activeonly for "mid-turn" — pid-liveness is the better attach signal than phase. -
Your literal "type exit" case. Since typing
exitdoesn't actually quit Claude Code, P1 stays alive and the stored pair stays valid — so no update happens and nothing's stale. The pair only needs refreshing when a genuinely new process attaches (real exit + resume, or resuming from another terminal — where you could briefly have two live processes for one session id; we'd just track the most recent).
Net: the (pid, start-time) pair stays correct across resume as long as capture is wired to session-start + turn-start. Want me to fold that capture-point requirement into the design (and pick a liveness approach), or keep exploring edge cases first?
I think we should pause on this and first commit what we have and maybe create a PR and then we can work on this approach on a new branch.
The full gate (fmt + lint + test:ci, including integration and the e2e canary) is running in the background — it takes a few minutes. I'll commit and open the PR once it's green. While it runs, here's the commit/PR I'll create so you can redirect me if needed:
- Branch:
soph/resume-session-picker - Commit/PR title:
Add interactive resume picker for stopped/idle sessions - Scope: bare
entire resumepicker (all worktrees, idle+ended, worktree-clash routing),Branchfield on session state + checkpoint-trailer derivation fallback (internalentire/refs excluded), keep-existing-local-log-by-default on resume, plus tests and docs. - Not included (intentionally deferred to a later branch): the PID/start-time liveness detection.
I'll report back when the checks finish.
Re-running the full test:ci to /tmp/resume-testci.log in the background (~12 min). I'll inspect the complete log when it finishes — if it's green I'll commit and open the PR; if it fails again I'll have the full failure detail to diagnose. Standing by for the completion notification.
Done. Everything is locked in.
- Branch:
soph/resume-session-picker(pushed) - Commit:
605ece819— Add interactive resume picker for stopped/idle sessions - PR: https://github.com/entireio/cli/pull/1445
- Pre-merge gate:
mise run check(fmt + lint + unit + integration + e2e canary) green. The one earlierFAILwas a flaky test — the clean re-run confirmed it.
The PID/process-liveness work is explicitly carved out as a follow-up in the PR body, so it's clear that's intentionally not here.
When you're ready to pick up the liveness approach, just say so and I'll branch off main (or off this branch if you want it stacked) and start there — we left off with the design settled around capturing (pid, start_time, host) on session-start + turn-start, treating pid-liveness (not phase) as the "agent attached" signal, and the open choice between gopsutil vs stdlib-only.
I wonder now if this is confusing when you are used to the prior behaviour. Like before it worked on local and remote sessions (by using the branch) but now it's a local only picker. But I don't think we have a good way of doing a remote picker? like how even to figure out which branches are still relevant
Good instinct, and worth untangling — because I think the worry is partly a framing issue and partly a genuine hard problem.
What actually changed (and what didn't)
The remote capability isn't lost. entire resume <branch> is 100% intact — including the "branch isn't local? fetch it from origin and resume" path. The picker is purely additive on top of that; before this PR there was no picker at all, so no one loses the remote-branch flow. The only new expectation gap is: someone might assume the new no-arg picker also surfaces remote sessions, and it doesn't.
Why the picker is local-only
It's driven by session-state files in .git/entire-sessions/ — which are per-machine. So it can only know about sessions that ran on this machine. That's actually a feature for the "what was I doing" use case (it includes idle/uncommitted sessions with rich live state — phase, last-interaction, tokens — none of which exist remotely), but it's intrinsically local.
The remote picker — your relevance instinct is the crux
It's technically doable: enumerate origin branches (cheaply via already-fetched refs/remotes/origin/*, or with a network ls-remote), find each branch's checkpoint trailer, read session metadata (prompt/agent/time) from the pushed entire/checkpoints/v1 metadata. That's the same data resume <branch> already consumes.
But "which branches are still relevant" is exactly the wall — and this repo (460 branches) is the perfect cautionary example. A raw list of every branch-with-a-checkpoint is useless. You'd need filters, each imperfect:
- Not merged into main (work still outstanding) — best signal, but merge-detection against remote may need a fetch.
- Recent — sort by latest checkpoint date; cut off old ones.
- Authored by you — filter by committer/session, since a shared remote has everyone's branches.
- Has an Entire checkpoint — drops plain branches.
Even with all of those, it's heuristic, needs network round-trips, and reads metadata per branch. It's a meaningfully bigger, fuzzier feature than the local picker — fundamentally "branch-oriented" rather than "session-oriented."
My recommendation
- Now (tiny, in this PR if you want): add a one-line footer to the picker so the scope is explicit, e.g. "Showing sessions from this machine. To resume a branch from origin:
entire resume <branch>." That removes the confusion for someone used to the old branch flow, at near-zero cost. - Later (separate feature): an opt-in
entire resume --remote/--allthat does the branch-oriented listing with the relevance filters above — keeping the default fast and local. I'd group it with or after the PID work, since both are "make the session list smarter" efforts.
Want me to add the footer hint to PR #1445 before it merges? And should I write up the remote/branch-picker (with the relevance heuristics) as a tracked follow-up alongside the PID note?
Done — footer added and pushed to PR #1445.
The picker now shows, below the title:
(huh's Select has no dedicated footer slot, so this renders as the description directly under the title — the reliably-visible spot.) Build/lint/tests green, explain.go untouched, pushed as commit 5910581e8.
So #1445 now has the picker + the scope-clarifying hint. Still queued as separate follow-ups whenever you want them: the PID/process-liveness detection, and the opt-in remote/branch picker (--remote/--all with relevance filters).
High: The picker can resume the wrong session. In cmd/entire/cli/resume_picker.go:98, the selected row is a specific SessionState, but cmd/entire/cli/resume_picker.go:115 passes only chosen.branch into runResume. resumeFromCurrentBranch then searches branch history for the latest checkpoint in cmd/entire/cli/resume.go:164, ignoring chosen.state.SessionID and LastCheckpointID. So two stopped sessions on the same branch both resume the same latest branch checkpoint; an idle session with a stored branch but no committed checkpoint is selectable and then either fails with “No Entire checkpoint found” or resumes an unrelated older checkpoint. The picker should either resume from the selected state/checkpoint ID, or list branches rather than sessions. Add coverage for two stopped sessions on one branch and a Branch-only idle session with empty LastCheckpointID.
Fixed and pushed to PR #1445 (commit 32bed47a2). The reviewer's finding was spot-on; here's what changed.
The bug
The picker let you select a specific session, but on selection it only passed chosen.branch to runResume, which re-derived the latest checkpoint on that branch via findBranchCheckpoints — completely ignoring which session you picked. So:
- Two sessions on one branch → both resumed the branch's latest checkpoint.
- An idle session with a stored branch but no committed checkpoint was selectable, then either failed with "No Entire checkpoint found" or resumed unrelated older work.
The fix
- Carry the checkpoint ID through the picker.
resumableSessionnow holdscheckpointID(the session'sLastCheckpointID). A row is selectable only if it has both a branch and a committed checkpoint — otherwise it's shown disabled with the reason (no branch/no committed checkpoint). That makes the branch-only idle session non-selectable instead of misleading. - Resume the selected session specifically. New
resumeSessionOnBranch→resumeByCheckpointIDswitches to the branch and restores that checkpoint's session(s), rather than searching branch history. The checkout logic is shared withrunResumevia an extractedswitchToBranchForResume(no duplication).
Coverage added (the two cases the reviewer named)
- Two sessions on one branch →
TestResolveResumableBranches_TwoSessionsSameBranchasserts each item carries its own distinct checkpoint, plusTestResumeByCheckpointID_ResumesRequestedSessionproves resuming the older session restores only it (session-a) and not the newer session-b. - Branch-only idle session, empty
LastCheckpointID→TestResumableSession_RequiresCheckpoint+ the updatedbuildResumeOptionstest assert it's not selectable.
Lint clean, explain.go untouched, unit + resume integration tests green.
One judgment call worth flagging: I made the picker resume log-only at the branch tip (checkout branch + restore the chosen session's transcript) rather than resetting the working tree to the selected checkpoint's commit. That matches "continue this conversation" and avoids surprise resets, but it does mean if you pick an older session on a branch that has since moved on, your code is at the branch tip, not that checkpoint. If you'd rather it reset to the selected checkpoint's commit, that's a small follow-up — let me know.
Yes — exactly. The picker is session-oriented: one row per session, so two sessions on the same branch show as two rows. After the fix, that's now correct (selecting each resumes its own session), where before it was the bug (both pointed at the branch's latest).
The rows are disambiguated by the other fields, since the branch repeats:
Same branch, different prompt + last-active time. So you can tell them apart and resume the specific conversation you want.
Whether that's good or noisy depends on how you work a branch:
- If you typically have one session per branch, you'll see one row per branch — clean.
- If you re-open an agent on the same branch across several sittings, each is a distinct session → multiple rows for that branch. Could feel repetitive.
If the repetition bothers you, the alternative is to collapse to one row per branch (showing the most-recently-active session, resuming that). Trade-off: you'd lose the ability to pick an older session on a branch from the list — though entire resume <branch> would still land on the latest anyway.
My lean is to keep it per-session (it's the more capable model and matches "resume the conversation I left," including older ones), but I can add collapsing — or a --by-branch view — if you'd prefer the tidier list. Want either, or leave it per-session?
Good news on feasibility: the repo already leans on bubbletea heavily (search_tui.go, activity_tui.go, dispatch_tui.go, recap_tui.go, review/tui_model.go), with bubbletea/v2 + bubbles/v2 as deps — and there are reusable renderers in sessions.go: writeSessionInfoText (summary) and writeSessionTranscript (the conversation). So the building blocks are all here.
The one constraint
The current picker is a huh.NewSelect, and huh's Select has no clean hook for a per-row secondary action like "press v." So a v-to-view key means replacing the huh picker with a small custom bubbletea model — which is exactly what the other *_tui.go files do, so there's a well-worn pattern to copy.
What it'd look like
A bubbletea list with keybindings:
↑/↓navigate,Enterresume,vview,q/esccancel.vopens a preview of the highlighted session. Two depths to choose from:- Quick summary — reuse
writeSessionInfoText: prompt, agent/model, branch, worktree, last-active, tokens, files touched, checkpoint. Cheap (data's already in memory), shows in a side panel or toggled pane. - Full transcript — reuse
writeSessionTranscriptin a scrollablebubbles/viewport. Richer ("what was I actually doing"), but reads the transcript file and is more UI.
- Quick summary — reuse
My lean: do both in layers — a summary panel always visible for the highlighted row (so you get context just by scrolling, no keypress), and v to expand into the scrollable transcript. The summary-on-highlight alone might even remove the need for a keypress in most cases.
Caveats / cost
- Accessibility: the current code respects
ACCESSIBLE=1viaNewAccessibleForm. A custom TUI needs a deliberate accessible fallback (numbered list + "enter a number to view" prompt), which huh gave us for free. That's the main added cost. - Scope: this is a bigger change than #1445 (swap huh → custom model + preview + accessible path), so it's a follow-up branch, not part of the current PR.
So: definitely worth doing and well-supported here. The decision I'd want before building: should v show the quick summary, the full scrollable transcript, or the layered approach (summary on highlight + v for transcript)? I'd recommend layered. Want me to write this up as a tracked follow-up alongside the PID and remote-picker notes?
Here's what I found — and it changes the recommendation slightly.
How activity does its "list"
activity is not a selectable list — it's a read-only scrolling dashboard:
- A bubbletea model (
activityModel,AltScreen) that loads data async (fetchDatacmd →activityDataMsg/activityErrMsg), showing aspinnerwhile loading. - Layout = a static header (
renderHeader: stat cards + charts) printed above a singleviewport.Model. The "commit list" is just rendered to a string (renderCommitListN) and stuffed intoviewport.SetContent— there's no cursor, no per-row selection, no Enter. - Navigation is scroll only (
↑/↓,j/k,home/end,g/G,q), with a footer help line + scroll-percent. OnWindowSizeMsgit recomputes the viewport height (height − header − footer).
So activity gives us the scrollable-content pattern (great for a transcript preview pane), but it has no selection model — wrong template for a picker.
The right template: search_tui.go
This one is exactly your idea already built:
- A hand-rolled selectable list:
cursor, highlightedselectedrow style,selectedResult(), pagination (resultsPerPage). - Mode switching:
modeBrowse(the list) ↔modeDetail(a full-screen detail view in its owndetailVP viewport.Model) ↔modeSearch. Selecting a row and viewing its detail is precisely thev-to-view flow we discussed — it's done here as a mode transition into a viewport.
So for the resume picker
Reuse search_tui's structure, not activity's:
cursor+selectedResult()for navigation, Enter → resume the selected session.v→ switch to amodeDetailwith adetailVPviewport (theactivityviewport pattern) rendering the session via the existingwriteSessionInfoText(summary) and/orwriteSessionTranscript(full conversation).escfrom detail → back to the list;q→ cancel.
Net: search_tui.go is the blueprint (selection + detail-mode), activity_tui.go is the reference for the scrollable detail pane itself. Both already exist in the repo, so building a custom resume picker with v-to-view is well-supported.
Want me to write this up as the tracked follow-up (custom picker TUI modeled on search_tui, with v → session view), alongside the PID and remote-picker notes?
can you actually create a draft trail for each of them? "entire trail create --help" should give you all you need
All three draft trails are created and visible. Done.
| # | Branch | Trail | Status |
|---|---|---|---|
| 583 | soph/session-liveness-pid | Session liveness: detect crashed 'active' sessions via PID + start-time | draft |
| 586 | soph/resume-remote-picker | Resume picker: opt-in remote/branch view (entire resume --remote) | draft |
| 587 | soph/resume-picker-tui-view | Resume picker: custom TUI with 'v' to preview a session | draft |
Each captures the design we worked out (including the footguns/decisions — e.g. ppid-walk + PID-reuse + cross-host fallback for liveness; the relevance-filter problem for the remote picker; search_tui.go as the template + accessibility caveat for the TUI). Each got its own branch off main, pushed to origin, all as draft. I stayed on soph/resume-session-picker (the current PR #1445 branch) — no checkout switch.
Note: trail create pushes the new branch to origin, so those three branches now exist on the remote (empty, just based on main). They'll surface under entire trail list --status draft. Nothing was merged or opened as a PR.
So the state now:
- PR #1445 (
soph/resume-session-picker): the picker feature + footer + the resume-the-selected-session fix — ready for review. - 3 draft trails: the follow-ups, queued and documented.
on the current work:
-
Medium: cmd/entire/cli/resume_picker.go:128 still drops the selected checkpoint in the “branch checked out in another worktree” path. It tells the user to run entire resume <branch>, which resumes the branch’s latest checkpoint, not the selected session. This reintroduces the old bug for the worktree-clash case when multiple sessions share a branch. The guidance should preserve the selected-session flow, likely by telling the user to cd there and run the picker again, or by adding a checkpoint/session-specific resume entrypoint.
-
Medium: cmd/entire/cli/resume_picker.go:128 prints the branch unquoted in a copy-paste shell command. Git accepts branch names like x;echo-pwn and x&echo-pwn, so the suggested command can execute unintended shell tokens if copied. Quote/shell-escape the branch argument too, not just otherPath.
Both findings fixed and pushed to PR #1445 (commit 607d7060b).
What changed in the worktree-clash path
Finding 1 — wrong session. The clash path used to print cd <path> && entire resume <branch>, which resumes the branch's latest checkpoint — the exact bug we just fixed everywhere else, sneaking back in for the clash case. Now it tells the user to re-run the picker in that worktree (cd <path> && entire session resume). The branch is already checked out there, so the picker lists the session, the clash no longer triggers (it's the current worktree), and selecting it resumes the specific session by its checkpoint — selected-session flow preserved. I also name the session by its prompt in the message so it's easy to re-pick.
Finding 2 — shell injection. The branch is no longer in the executable command at all (it only appears in non-executable %q prose), so a branch like x;echo pwn can't inject. The worktree path — the one value that is in the command — is now shellQuoted (POSIX single-quote escaping, same as the strategy package's helper), so $(...), ;, spaces, and apostrophes in a path stay inert.
Coverage
TestShellQuote— including$(echo pwn),x;echo pwn, and apostrophe escaping.TestWorktreeClashMessage— three subtests proving: (a) it points at the picker, neverentire (session) resume <branch>; (b) a malicious branch name never reaches the command line; (c) a path with$(...)is single-quoted and an apostrophe is escaped.
Message building was factored into worktreeClashMessage to make it unit-testable. Lint clean, explain.go untouched, resume unit tests green.
One thing I deliberately did not do: add a public session/checkpoint-specific resume entrypoint (the reviewer's alternative). Pointing at the picker is the smaller, surface-free fix and fully preserves correctness. If you'd rather have a direct entire session resume --session <id> (also useful for scripting), that's a clean follow-up — say the word and I'll spin up a trail for it.
[P2] cmd/entire/cli/resume_picker.go:250: the legacy branch-resolution fallback skips the default branch, but still walks default-branch history through every non-default branch. For pre-PR session states with Branch == "" and a LastCheckpointID on main/ master, the picker will either show “no branch” or mis-map that checkpoint to some feature branch that contains the commit. Selecting it then checks out the wrong branch before restoring the exact checkpoint. This contradicts the documented fallback for old sessions. I’d add a test for a legacy session checkpoint on main, and either index the default branch explicitly or only attribute a checkpoint to feature branches when it is actually branch-only.
All 8 threads resolved, 0 unresolved. Done.
Summary
Addressed every Copilot/Cursor comment on PR #1445 with code fixes, pushed as 9f95b6cad, then replied to each thread (citing the commit + what changed) and resolved all of them.
| # | Bot | Sev | Issue | Fix |
|---|---|---|---|---|
| 1 | cursor | High | Bare resume had no TTY gate — opened a form that can't render in CI/pipes | Gate on interactive.CanPromptInteractively(); bail with guidance |
| 2 | cursor | Med | Picker used form.Run(), ignoring command context | form.RunWithContext(ctx) (already used NewAccessibleForm) |
| 3 | cursor | Med | Worktree-clash false positive when WorktreeRoot fails | Return ("", false) when current root is unknown |
| 4 | cursor | Low | Ctrl+C wrapped as noisy "selection failed" | context.Canceled + ErrUserAborted exit cleanly |
| 5 | Copilot | — | captureSessionBranch left stale branch on detached HEAD | Clear state.Branch on detached HEAD; added TestCaptureSessionBranch |
| 6 | Copilot | — | resumeSingleSession doc comment outdated | Rewrote to keep-by-default semantics |
| 7 | Copilot | — | Single-session message differed from multi ("Keeping existing") | Aligned wording |
| 8 | Copilot | — | Branch iterator not closed | defer iter.Close() (thread was already auto-resolved) |
All green: build, lint (0 issues), unit + resume integration tests; explain.go untouched.
PR #1445 state now
Commits on soph/resume-session-picker:
- Picker feature
- Local-scope footer hint
- Resume selected session (not branch-latest)
- Worktree-clash guidance + shell-injection fix
- Default-branch checkpoint attribution (legacy fallback)
- Copilot/Cursor comment fixes
All review threads resolved. The three deferred items (PID liveness, remote picker, v-to-view TUI) remain as draft trails. Worth noting: the bots review per-commit, so new commits may draw a fresh round of comments — ping me and I'll work through those too.
Checked and cleared. Here's what entire trail finding surfaced and what I did.
What it is
entire trail finding is the dashboard of agent-native findings attached to a trail (separate from GitHub PR review comments) — reviewers/agents file findings with severity, confidence, file:line, and a suggested fix; you show/resolve/dismiss/apply them.
What was open
Trail #581 (this branch) had 1 open medium finding (confidence 0.85) on branchCheckedOutElsewhere: the git worktree list --porcelain parser could, in theory, mis-pair a branch line with a stale worktree path when a detached-HEAD worktree (no branch line) is in the output.
The specific mechanism it described wasn't actually live (curPath is overwritten at each worktree line, so a branch line already pairs with its own block) — but the defensive suggestion was sound, so I implemented it rather than dismissing:
- Factored the parsing into
parseWorktreeForBranch, now block-scoped: reset the path at each blank-line block boundary, and only match abranchline when a worktree line was seen in the same block. A detached worktree can no longer pair a branch with a stale path or return an empty path. - Dropped the now-dead
rawRoot != ""guard (the empty case returns earlier). - Added
TestParseWorktreeForBranchcovering a detached worktree.
Committed/pushed as eed087fe2, lint clean, explain.go untouched, then resolved the finding with a note. Dashboard now shows 0 open findings (1 resolved).
So the trail's findings are clean, and PR #1445's review threads are all resolved too. Want me to also update the trail's status (it's currently open) or post a recap, or move on to one of the draft follow-ups?