Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,21 @@ conflate them:
| intent | tmux | icon | home-screen section |
|---|---|---|---|
| open | running | 🟒 | sessions |
| open, parked | stopped | πŸ…ΏοΈ | sessions |
| open | stopped | 🟠 | sessions |
| closed / no record | stopped | ⚫ | inactive worktrees |

Within the *sessions* section rows are ordered running, parked, then stopped.

**Stop** ends the claude process and keeps the record open (the transcript is
untouched, so the conversation resumes; an in-flight task is interrupted).
**Terminate** ends it and deletes the record. Anything that kills a session
**Park** is the same operation with `SessionRecord.parked` set: a *flavour* of
stopped, not a third intent, so everything keyed off `is_stopped` (resume,
terminate, the sessions section) keeps working. It differs only in the icon and
in being skipped by **Restore N stopped sessions** β€” a session put down on
purpose should not come back with the ones a crash took away. `mark_open`
writes a fresh record, so launching or resuming un-parks; that is the only exit
short of terminating. **Terminate** ends it and deletes the record. Anything that kills a session
*outside* fujimoto β€” host restart, `tmux kill-session`, a closed window, `exit`
in the pane β€” leaves the record alone, so it comes back as stopped. That is the
whole restart-recovery mechanism: no boot-time detection, no heuristics.
Expand Down Expand Up @@ -236,14 +245,17 @@ as JSON in `~/.cache/fujimoto/sessions.json` (same graceful-degradation pattern
as `settings.py`: missing file, unreadable cache or corrupt JSON yield an empty
state, never an error):
- `SessionRecord` dataclass: `cwd`, `project`, `session_type`, `branch`,
`claude_session_id`, `last_seen`. Only `cwd` is required β€” every other field
defaults, so a record written by a different fujimoto version still loads.
`claude_session_id`, `parked`, `last_seen`. Only `cwd` is required β€” every
other field defaults, so a record written by a different fujimoto version
still loads (`parked` is coerced with `bool()` on load for exactly that
reason).
- Keyed by **tmux session name**, so worktree, direct and ad hoc sessions are
covered uniformly and a record outlives its worktree directory.
- `load_state()` / `save_state()`, `mark_open(...)` (every launch and
reconnect; a reconnect passing no id keeps the id recorded at first launch),
`mark_closed(name)`, `touch(name, claude_session_id=None)`,
`rename(old, new)`, `prune()`.
`mark_closed(name)`, `touch(name, claude_session_id=None, *, parked=None)`
(`parked` is tri-state: `None` leaves the flag alone, so a plain stop of a
parked session doesn't silently un-park it), `rename(old, new)`, `prune()`.
- **A record's presence means "open"; its absence means "closed"** β€” which is
also what a session fujimoto has never launched looks like. `mark_closed`
therefore just deletes the record, and there is no reconciliation pass and
Expand Down Expand Up @@ -407,7 +419,7 @@ responsible for running it off the event loop.
- `SessionApp` β€” main app class with CSS styling
- Module-level helpers: `_claude_state_label(state)`, `_relative_time(dt)`, `_get_claude_sessions(root, worktrees)`, `_is_fork_worktree(path)`, `_build_fork_system_prompt(project, working_dir, parent_worktree, base_branch)`, `_fit_snippet(snippet, max_width)` / `_render_snippet(snippet, max_width)` (search-result snippet rendering β€” see the `Content.assemble` gotcha), `_match_spans(text, matcher)` / `_highlight(text, spans)` / `_log_body(text, folds)` (log-viewer match highlighting, split so the regex half can run in a worker thread and only the `Content` half touches widgets), `_tool_collapsible(title, …)` (a `Collapsible` whose title is `Content`, never markup)
- Instance helpers: `_build_session_label(session, state_suffix)` β€” the single source of truth for session row text (including the 🍴 fork marker), used by `_show_home`'s render of direct *and* worktree rows and by `_poll_session_states` for in-place updates; `_build_claude_session_items(sessions, prefix)` β€” shared row rendering for the resume (`rp-*`), fork (`fp-*`) and log-viewer (`lp-*`) pickers; `_matching_worktree(path)` β€” the project worktree a path refers to, compared by `resolve()`; `_direct_session_cwd(tmux_name)` β€” where a `direct-N` row really runs and on what branch (memoized in `_direct_cwd_cache`); `_resume_target(project, cwd, session=None)` β€” the tmux name and session type a resume should use
- Views: home (sessions list), session actions submenu, terminate/stop prompt (`#terminate-prompt`, opened by a pending `close` from `Ctrl-A x`; Terminate / Stop / Cancel with Terminate highlighted so Enter matches the `confirm-before` it replaces, and Cancel re-attaching via `_launch_target` so it costs nothing), finish flow, confirm dialog, create form, branch select (3 options), branch picker (filterable list), fork title form, fork branch select, fork session picker, session log picker + read-only log viewer, conflict resolution, project switcher (with autocomplete filter), tmux install, error
- Views: home (sessions list), session actions submenu, terminate/park/stop prompt (`#terminate-prompt`, opened by a pending `close` from `Ctrl-A x`; Terminate / Park / Stop / Cancel with Terminate highlighted so Enter matches the `confirm-before` it replaces, and Cancel re-attaching via `_launch_target` so it costs nothing), finish flow, confirm dialog, create form, branch select (3 options), branch picker (filterable list), fork title form, fork branch select, fork session picker, session log picker + read-only log viewer, conflict resolution, project switcher (with autocomplete filter), tmux install, error
- Transcript search (`s`) β€” see the dedicated section below.
- Home screen name filter: `/` arms a filter box (`#home-search`) mounted above
`#home-list`. `action_search` reveals + focuses it; `Input.Changed` re-renders
Expand All @@ -430,7 +442,7 @@ responsible for running it off the event loop.
`_search_query`. Guarded on `_on_home`, so `r` is inert in other views.
- Home screen sections: actions ("Restore N stopped sessions" when any exist, "New worktree session", "New session in X", "Ad hoc session"), sessions β€” running 🟒 *and* stopped 🟠 together, since the circle colour carries the distinction (with Claude state indicators on the running ones), inactive worktrees, previous Claude sessions (resumable, capped at 5), switch project
- Worktree create flow: title β†’ branch select (default w/ fetch & rebase, current branch, another branch β†’ picker) β†’ create
- Session actions submenu (in order): for active sessions, Connect β†’ Fork session β†’ Resume previous session; for inactive worktrees, Resume previous session β†’ Fork session β†’ Launch (resume is the more common action when picking an idle worktree). Then: View session log (whenever the path has a previous Claude session), Open terminal, Open in VS Code, Rename, Stop session (active only), Terminate session (active or stopped), Finish (worktree only), Cancel. Stop and Terminate are two menu items rather than one item plus a prompt β€” a menu is already a choice β€” but both route into the single `_end_session(session, terminate=...)` handler, which is also what the `Ctrl-A x` prompt calls. Claude-session items show just "Resume" + View session log + Open terminal/VS Code + Cancel. "Fork session" is always inserted at index 1 (`items.insert(1, ...)`) so its position holds across both layouts.
- Session actions submenu (in order): for active sessions, Connect β†’ Fork session β†’ Resume previous session; for inactive worktrees, Resume previous session β†’ Fork session β†’ Launch (resume is the more common action when picking an idle worktree). Then: View session log (whenever the path has a previous Claude session), Open terminal, Open in VS Code, Rename, Park session and Stop session (active only), Terminate session (active or stopped), Finish (worktree only), Cancel. Stop and Terminate are two menu items rather than one item plus a prompt β€” a menu is already a choice β€” but both route into the single `_end_session(session, terminate=...)` handler, which is also what the `Ctrl-A x` prompt calls. Claude-session items show just "Resume" + View session log + Open terminal/VS Code + Cancel. "Fork session" is always inserted at index 1 (`items.insert(1, ...)`) so its position holds across both layouts.
- "Resume previous session" auto-launches the sole candidate when only one previous Claude session exists for the path, skipping the picker. Two or more sessions still show the picker.
- Fork flow: `sa-fork` β†’ `#fork-title-input` β†’ `#fork-branch-list` (parent branch (default) / parent's base / another branch β†’ the shared `_show_branch_picker`) β†’ conversation picker `#fork-picker` (`fp-{i}`, only when >1 candidate) β†’ the shared `_finalize_create` / `_do_create_and_launch`. Offered for worktree *and* direct sessions that have at least one previous Claude session.
- Session log viewer: `sa-viewlog` β†’ `_show_log_picker` β†’ `_show_session_log`. The
Expand Down Expand Up @@ -607,7 +619,7 @@ Three custom exception types, all caught in `main()`:
`uv tool install --force --reinstall --no-cache .`, and verify with
`fujimoto --version` (the `.devN` suffix moves) or by grepping the installed
`site-packages/fujimoto/cli.py` for something the change added.
- **Remembering sessions across a restart**: `session_state.py` records every session as open at launch β€” in `main()`, **before** `launch_claude_in_tmux` blocks on the attach, so a host that dies mid-session still has a record to restore from. `_init_git_info` loads the pruned state into `_open_sessions`; `_stopped_records()` derives the open-but-not-running set for the current project; `_build_home_items` renders those in the *sessions* section as 🟠 and excludes them from *inactive worktrees*. A **Restore N stopped sessions** row relaunches them all via `create_session` (detached, resuming each path's latest transcript, attaching to none) β€” deliberately without `_apply_worktree_config`, which would run N `init` blocks up front; config still runs when the user actually attaches. `_end_session(session, terminate=...)` is the single handler behind both menu items and both outcomes of the `Ctrl-A x` prompt; it tolerates a `kill_session` failure only when `session_exists` confirms the session is already gone (otherwise marking a live session closed would hide it). `_do_delete_worktree` and `on_rename_submitted` keep the store honest via `mark_closed` / `rename`.
- **Remembering sessions across a restart**: `session_state.py` records every session as open at launch β€” in `main()`, **before** `launch_claude_in_tmux` blocks on the attach, so a host that dies mid-session still has a record to restore from. `_init_git_info` loads the pruned state into `_open_sessions`; `_stopped_records()` derives the open-but-not-running set for the current project; `_build_home_items` renders those in the *sessions* section as 🟠 (πŸ…ΏοΈ when `parked`, sorted after the running rows and before the stopped ones) and excludes them from *inactive worktrees*. A **Restore N stopped sessions** row relaunches the unparked ones via `create_session` (detached, resuming each path's latest transcript, attaching to none) β€” deliberately without `_apply_worktree_config`, which would run N `init` blocks up front; config still runs when the user actually attaches. `_end_session(session, terminate=..., park=...)` is the single handler behind every menu item and every outcome of the `Ctrl-A x` prompt; it tolerates a `kill_session` failure only when `session_exists` confirms the session is already gone (otherwise marking a live session closed would hide it). `_do_delete_worktree` and `on_rename_submitted` keep the store honest via `mark_closed` / `rename`.
- **Session metadata**: `.fujimoto/meta.json` stored in worktree directory records the base branch for cherry-pick targeting, the `source_root` (main repo) for project-config source resolution, and β€” for forks β€” `forked_from_session_id` plus `forked_from_worktree`. The `.fujimoto/` directory contains a `.gitignore` with `*` so its contents are automatically ignored by git.
- **Project config (`.fujimoto.yaml`)**: An optional, committed per-project file (`project_config.py`) declaring files to copy/link into a worktree and init commands to run. Applied centrally in `main()`'s launch loop (parent process, **before** `tmux attach`) by `_apply_worktree_config(working_dir)`, for **every** worktree connection mode (new, reconnect-to-live, relaunch/resume) β€” so copy/link/init run on each connect, not just creation. `_do_create_and_launch` no longer applies config; it only creates the worktree and stores meta. Key mechanics:
- **Config is read from the source root (main clone), not the worktree.** `.fujimoto.yaml` is a local, uncommitted file in the main clone, so it isn't present in a worktree checkout β€” `_apply_worktree_config` calls `load_project_config(source_root)`. (`--create-config` writes it to the main clone's root.)
Expand Down
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,20 +319,24 @@ https://docs.pypi.org/trusted-publishers/
cannot be used to test key bindings β€” it injects into the pane and
bypasses key-table dispatch; to automate it, write the keys to an attached
client's pty instead (`pty.fork()`, then `os.write(fd, b"\x01f")`).
8. Test stop / terminate and restart recovery:
8. Test stop / park / terminate and restart recovery:
- Attach to a session and press `Ctrl-A s` β€” it should detach with no
prompt, and reappear on the home screen as 🟠 in the *sessions* section
- Select it β†’ **Launch** or **Resume previous session** β€” it goes back to 🟒
- Press `Ctrl-A x` with claude alone in the window β€” the TUI should reappear
on the terminate prompt; Enter terminates (⚫), `Stop` keeps it 🟠, and
`Cancel` drops you straight back into the session having killed nothing
on the terminate prompt; Enter terminates (⚫), `Stop` keeps it 🟠,
`Park` keeps it πŸ…ΏοΈ, and `Cancel` drops you straight back into the session
having killed nothing
- Press `Ctrl-A t` to split, then `Ctrl-A x` β€” it must fall back to the
original `kill pane? (y/n)` confirm and leave the session alone
- Simulate a restart with `tmux kill-server` while sessions are open, then
run `fujimoto`: every session should come back 🟠, with a
**Restore N stopped sessions** row that relaunches them all detached
- Terminate one through the menu, then `tmux kill-server` again β€” the
terminated one must stay ⚫ while the others return 🟠
- Park a session (menu item or the `Ctrl-A x` prompt): it renders πŸ…ΏοΈ above
the 🟠 rows, the **Restore** row's count drops by one and leaves it alone,
and launching it clears the flag back to 🟒
- Check `~/.cache/fujimoto/sessions.json` holds a record per open session
and nothing for terminated ones
As with `Ctrl-A f`, `tmux send-keys` cannot drive these bindings; write to an
Expand Down
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ via `FUJIMOTO_META_KEY`) to enter a one-shot "fujimoto mode", then:
| `f` | Fork this session β€” detaches back to the fujimoto TUI and opens the **Fork session** flow for this worktree, so you get the usual name prompt, base-branch menu and conversation picker. |
| `s` | Stop this session β€” closes claude but keeps the session in your list as 🟠, ready to resume. No prompt. |
| `d` | Detach the tmux session (returns you to the fujimoto TUI). |
| `x` | End this session. With a split open it kills just that pane (with confirmation), as before. With claude alone in the window it detaches to the fujimoto TUI and asks whether to **terminate** (default) or merely **stop** it. |
| `x` | End this session. With a split open it kills just that pane (with confirmation), as before. With claude alone in the window it detaches to the fujimoto TUI and asks whether to **terminate** (default), **park** πŸ…ΏοΈ or merely **stop** 🟠 it. |
| `[` | Enter copy mode (scrollback / selection). |
| `?` | Flash the binding cheatsheet in the status bar. |

Expand Down Expand Up @@ -269,6 +269,7 @@ fujimoto
───── sessions ─────
🟒 20260309-cleanup-ui (worktree)
🟒 direct-1 (direct @ main)
πŸ…ΏοΈ 20260305-spike-caching (worktree)
🟠 20260307-parser-rewrite (worktree)
🟠 direct-2 (direct @ main)
───── inactive worktrees ─────
Expand All @@ -289,6 +290,7 @@ what you had open instead:
| | Meaning |
|---|---|
| 🟒 | Running right now. |
| πŸ…ΏοΈ | **Parked** β€” stopped on purpose and set aside. Resume it whenever you like; **Restore** leaves it where it is. |
| 🟠 | **Stopped** β€” not running, but you never told fujimoto you were done with it. Resume it and carry on. |
| ⚫ | An inactive worktree: terminated through fujimoto, or never launched from it. |

Expand All @@ -298,20 +300,28 @@ host restart, `tmux kill-session` from another terminal, closing the window,
`exit` in the pane β€” leaves it stopped and resumable. There is no guessing
about why a session went away.

Sessions are listed running first, then parked, then stopped, so the ones
still in flight stay at the top.

**Restore** appears at the top of the home screen whenever something is
stopped. It relaunches every stopped session in the project at once, each
resuming its most recent conversation, and attaches to none of them β€” pick the
one you want to sit in from the list.
one you want to sit in from the list. Parked sessions are skipped: you put them
down deliberately, unlike the ones a restart took away.

**Stop** ends the claude process but keeps the session history: the transcript
is untouched and the conversation resumes where it left off. Any task claude
was part-way through is interrupted, so stop at a natural break.

**Park** does exactly what Stop does, but flags the session so it shows as πŸ…ΏοΈ
and sits out the bulk restore β€” for work you are consciously shelving rather
than pausing. Launching or resuming a parked session un-parks it.

#### Filtering sessions by name β€” `/`

Press `/` on the home screen to open a filter box, then type. Matching is live
and case-insensitive, against session names and branch names, and covers the
**sessions** (running and stopped), **inactive worktrees** and **previous claude
**sessions** (running, parked and stopped), **inactive worktrees** and **previous claude
sessions** lists at once. While a filter is active the action rows
(`+ New …`, settings, switch project) are hidden so only matches remain.

Expand Down Expand Up @@ -396,14 +406,14 @@ Select any session to see contextual options:

| Session State | Options |
|--------------|---------|
| Active worktree | Connect, Fork session, Resume previous session, View session log, Stop, Terminate, Finish |
| Active worktree | Connect, Fork session, Resume previous session, View session log, Park, Stop, Terminate, Finish |
| Stopped worktree | Resume previous session, Fork session, View session log, Launch, Terminate, Finish |
| Inactive worktree | Resume previous session, Fork session, View session log, Launch, Finish |
| Active direct | Connect, Fork session, Resume previous session, View session log, Stop, Terminate |
| Active direct | Connect, Fork session, Resume previous session, View session log, Park, Stop, Terminate |

**Stop** keeps the session in your list as 🟠; **Terminate** marks it done and
drops it to ⚫. Both close claude β€” the difference is only whether fujimoto
offers it back to you.
**Stop** keeps the session in your list as 🟠 and **Park** keeps it as πŸ…ΏοΈ;
**Terminate** marks it done and drops it to ⚫. All three close claude β€” the
difference is only whether fujimoto offers it back to you, and how eagerly.

All session types also offer **Open terminal**, **Open in VS Code** and
**Rename**.
Expand Down
Loading
Loading