Skip to content

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
AgentSystemLabs:mainfrom
kaminskypavel:feat/move-session-between-worktrees
Open

kaminskypavel wants to merge 1 commit into
AgentSystemLabs:mainfrom
kaminskypavel:feat/move-session-between-worktrees

Conversation

@kaminskypavel

Copy link
Copy Markdown
Contributor

A session can now move to another worktree, or into another project, without losing its conversation. Press m on 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

A session card dragged from one worktree band to another in a live nebula, then moved to another project

Full-quality MP4

✨ What you get

🚚 Move from the keyboard

  • The MOVE PICKER: m on a session card
    • lists the project's other checkouts, root first, then every other project's as project ▸ branch
    • letters narrow the list (type a project's name to jump to its checkouts); Enter moves
    • also on the card's right-click menu as Move to…; rebindable as move_session
  • Why m: ^m is the same byte as Enter in terminals without the kitty protocol, and ⇧M is Memory usage
    • the m_opens_no_menu guard keeps its point: m opens the picker, never a context menu

🖱️ Move by dragging

  • Drop on a band: drag a card onto another worktree's band
    • its rule, its cards, or the blank grid under it all count, so an empty band takes a drop too
    • the rule reads release to move here while the card is over it
  • Drop on a project tab: the session moves into that project's root
    • the tab lights while the card is over it
  • A drop asks first: Move 'name' to branch? It restarts there…, Enter moves, Esc keeps it
    • Settings › Sessions › Confirm drag move (confirm_drag_move, on by default) turns the question off
    • the picker never asks: a pick and Enter are already deliberate
  • What stayed the same: a plain click on a card still selects it, a double click still enters it

🧠 The session carries on

  • Same conversation: the CLI is killed and resumed in the target checkout with its stored session id
    • idle: moved straight away
    • mid-turn: the card moves now, the process follows when that turn ends (no "continue" notice is injected)
    • not running: boots in the target on its next launch
  • The grid follows: a session moved into another project opens that project's tab with the card selected

📸 Screenshots

m: the MOVE PICKER Other projects, narrowed by typing demo
The MOVE PICKER over the grid listing main (root) The MOVE PICKER filtered to demo's checkouts: demo ▸ feature-x, demo ▸ main (root), demo ▸ wheel-one-line
A drop asks first Blank grid under an empty band is that band
The Move session confirm after a card was dropped on the root band's rule The Move session confirm after a card was dropped in the blank space under an empty worktree band
Moved into another project: the grid follows to demo
The demo project's grid, its tab opened, the moved card selected under feature-x

Captured 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
Loading

⚠️ Risk

🎯 Attack surface

  • ClientRequest::MoveAgent { id, worktree }. Reached by any client on the DAEMON's unix socket. Held by: the socket's 0700 mode (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 worktree relocation path that already ships; the new request only adds a user-initiated entry to it.

Level Why
🔒 Security & production Low one new same-user ClientRequest; spawns only into registered worktrees, with the same resume and hook install as any spawn
⚡ Performance Low nothing on a hot path; a drag recomputes bands() per motion report while the button is held, as clicks already do
🧩 Fit with the codebase Low follows existing patterns: pending_moves, relocate_into, the WORKTREE PICKER's menu, ConfirmDialog, a Sessions setting with added_on

Rollback: git revert undoes all of it. No PROTOCOL VERSION bump, no store migration; a config written with confirm_drag_move keeps the key harmlessly.

🔧 Technical overview

  • Mechanism. Daemon::move_agent moves 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 a PendingMove { target, notice: false }, which complete_pending_move drains on the turn end; with no notice the held Stop is released, so the row finishes normally. pending_moves used to hold a bare Worktree; it now carries notice, true for nebula 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.
  • Files. crates/nebula-daemon/src/registry.rs: move_agent, PendingMove, relocate_into's notice; 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_selection switches 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.
  • Not done. Terminal cards do not move. The picker has no "+ new worktree" row (nebula worktree from inside the session covers that). A PR or issue session moved to another project still carries its original PR / issue scope rule.
  • Gate. cargo test green for nebula-core (33), nebula-daemon (306), nebula-tui (1243) and help_cli. Two pre-existing timing tests (hook_past_the_timeout_is_killed_with_what_it_started, a branch_switch ordering test) flaked once under load and pass on rerun. rustfmt and clippy were not available in this toolchain, so make ci's fmt and lint steps did not run: please run them before merging.

📝 Notes

  • Based on upstream/main at v0.42.0 (0f98b95); no conflicts.
  • No upgrade step: no PROTOCOL VERSION bump.

🤖 Generated with Claude Code

…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
kaminskypavel marked this pull request as ready for review October 2, 2026 18:00
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.

1 participant