Move a session to another worktree or project with m or a drag, and it carries on the same conversation there - #118
Open
kaminskypavel wants to merge 1 commit into
Conversation
…rom a list, or drag the card onto another band or project tab, and the session resumes there on the same conversation - `m` on a card (or the card menu's **Move to…**) opens the MOVE PICKER: the project's other checkouts, root first, then every other project's as `project ▸ branch`. Letters narrow it; Enter moves the session. - A card dragged onto another band (its rule, its cards, or the blank grid under it, so an empty band takes a drop too) or onto another PROJECT TAB (that project's root) moves the session there. The band rule reads `release to move here` and the tab lights while the card is over it. A drop asks first; Settings > Sessions > Confirm drag move (`confirm_drag_move`, on by default) turns that off. The picker never asks. - The DAEMON's new `MoveAgent` request moves the row at once and reuses the `nebula worktree` relocation: an idle session is killed and resumed in the target straight away; one mid-turn follows when that turn ends (resumed silent, with no "continue" notice, since that turn never asked to move); a dead one boots there on its next launch. `pending_moves` now carries whether the respawn opens on the relocation notice. - A session moved into another project takes the grid there with it. - The `m_opens_no_menu` guard keeps its point: `m` opens the move picker, never a context menu. `^m` would be Enter in most terminals and `⇧M` is Memory usage, so the key is `m`, rebindable as `move_session`. - docs/keys.md and docs/configuration.md; screenshot scenes move-session*. Tests: move_agent_rehomes_a_dead_row_across_checkouts_and_projects, move_agent_relocates_an_idle_session_now_and_a_busy_one_at_turn_end, m_on_a_card_picks_a_checkout_to_move_the_session_to, dragging_a_card_onto_another_band_moves_its_session, a_drop_under_the_last_band_moves_there_without_asking_when_confirm_is_off, dragging_a_card_onto_a_project_tab_moves_it_into_that_project, confirm_drag_move_is_on_by_default_and_persists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kaminskypavel
marked this pull request as ready for review
October 2, 2026 18:00
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.
A session can now move to another worktree, or into another project, without losing its conversation. Press
mon a card and pick a checkout, or drag the card onto another band or project tab. The DAEMON restarts the CLI there, resumed on the same session id.Contents
🎬 Demo
Full-quality MP4
✨ What you get
🚚 Move from the keyboard
mon a session cardproject ▸ branchmove_sessionm:^mis the same byte as Enter in terminals without the kitty protocol, and⇧Mis Memory usagem_opens_no_menuguard keeps its point:mopens the picker, never a context menu🖱️ Move by dragging
release to move herewhile the card is over itMove 'name' to branch? It restarts there…, Enter moves, Esc keeps itSettings › Sessions › Confirm drag move(confirm_drag_move, on by default) turns the question off🧠 The session carries on
📸 Screenshots
m: the MOVE PICKERdemodemoCaptured with the SCREENSHOT HARNESS (
make shot SCENE=move-session…, scenes added in this PR).🧭 How it flows
flowchart LR K(["m + pick"]) --> MA DR(["drag + drop"]) --> C{"confirm_drag_move?"} C -->|"on: Enter"| MA C -->|off| MA MA["ClientRequest::MoveAgent"] --> D["Daemon::move_agent"] D --> ROW["store.set_agent_worktree + broadcast"] D -->|"idle + alive"| R["relocate_into (silent)"] D -->|"mid-turn"| P["pending_moves {target, notice: false}"] P -->|"Stop / idle / progress clear"| R R --> CLI["kill PTY, respawn resumed in target"] ROW --> TUI["TUI: select_when_seen, grid follows into project"] classDef changed fill:#fde68a,stroke:#b45309,color:#111 class MA,D,P,C,TUI changed🎯 Attack surface
ClientRequest::MoveAgent { id, worktree }. Reached by any client on the DAEMON's unix socket. Held by: the socket's0700mode (same user only); the target must be a worktree row already in the store, so no arbitrary path is spawned into; archived rows are refused. Not held: a same-user client can move any session, as it can already restart or delete one.Verdict: 🟢 Low risk. The move reuses the
nebula worktreerelocation path that already ships; the new request only adds a user-initiated entry to it.ClientRequest; spawns only into registered worktrees, with the same resume and hook install as any spawnbands()per motion report while the button is held, as clicks already dopending_moves,relocate_into, the WORKTREE PICKER's menu,ConfirmDialog, a Sessions setting withadded_onRollback:
git revertundoes all of it. No PROTOCOL VERSION bump, no store migration; a config written withconfirm_drag_movekeeps the key harmlessly.🔧 Technical overview
Daemon::move_agentmoves the row (set_agent_worktree+ broadcast) and then makes the process match it. Alive and idle:relocate_into(id, target, notice: false)kills and respawns now. Alive and mid-turn (Running/NeedsFeedback): it queues aPendingMove { target, notice: false }, whichcomplete_pending_movedrains on the turn end; with no notice the held Stop is released, so the row finishes normally.pending_movesused to hold a bareWorktree; it now carriesnotice,truefornebula worktree(unchanged behaviour). Cross-project moves need nothing extra: every spawn already reinstalls hooks and rebuilds the PR / issue scope from the target worktree, and the Claude resume already scans every transcript slug.crates/nebula-daemon/src/registry.rs:move_agent,PendingMove,relocate_into'snotice;crates/nebula-core/src/protocol.rs:MoveAgent;crates/nebula-tui/src/event_loop/launcher.rs: the picker,press_card/drag_card/drop_card,confirm_move,move_plan;crates/nebula-tui/src/event_loop.rs:land_pending_selectionswitches project for a moved row;crates/nebula-tui/src/ui/launcher_view.rs: the lit rule and tab;crates/nebula-tui/src/config.rs:confirm_drag_move.nebula worktreefrom inside the session covers that). A PR or issue session moved to another project still carries its original PR / issue scope rule.cargo testgreen fornebula-core(33),nebula-daemon(306),nebula-tui(1243) andhelp_cli. Two pre-existing timing tests (hook_past_the_timeout_is_killed_with_what_it_started, abranch_switchordering test) flaked once under load and pass on rerun.rustfmtandclippywere not available in this toolchain, somake ci's fmt and lint steps did not run: please run them before merging.📝 Notes
upstream/mainat v0.42.0 (0f98b95); no conflicts.🤖 Generated with Claude Code