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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 1.0.87

- Feat: saved session lists and a live-process view, on the Session Buddy model ([#145](https://github.com/grimmerk/codev/issues/145), [#94](https://github.com/grimmerk/codev/issues/94))
- **`● N live` chip** next to the search box scopes the list to sessions with a running process; the total memory they hold shows beside it. A **`stats`** toggle (off by default, remembered) adds each row's **memory and uptime** for the "which one to close first" moment — off by default because on most rows those figures track the message count closely enough to be noise. Pid and terminal device live in the tooltip. Measured while building it: 36 `claude` processes held 4.66GB while the terminal app itself held 368MB
- The live view is built by joining `ps` against `~/.claude/sessions/`, not by trusting the registration files: a session that is running but never registered shows up marked **`⚠ unregistered`** (invisible to every other view), and a registration whose process is gone is counted as stale in the chip's tooltip instead of being shown as a ghost. The same join also tells a saved-list member whether it is running, so clicking a running session that has no history row yet (a fresh `/branch` child) switches to it instead of resuming a second copy
- **`save list…`** captures what is on screen — the live set, the pinned set, or a search result — as a named list, stored in `~/.config/codev/session-lists.json`
- **`🗂 N` chip** shows the saved lists; click one to view its members in the order they were captured and resume any of them. Lists can be renamed (`✎` on the row or in the list header) and deleted (`✕`, confirmed with a second click). The default name is today's `MMDD` — a label, not an identity — and becomes `MMDD-2`, `MMDD-3` on a second save that day. A member whose transcript is gone still reads as the session it was, because the list stored its title, branch and last messages
- Each member carries the **recap line** Claude Code writes into the transcript (`away_summary` — "where we are, what's next"), shown on the row in place of the last reply. Measured: 65 of 66 non-trivial sessions have one. A recap that predates the session's last activity by more than 30 minutes is marked `⏱`, because its "next step" may already be done
- **Search matches the session id** (both search paths, one shared rule), so the id a terminal status line shows finds the session — the one field that stays unique when several sessions share a name ([#142](https://github.com/grimmerk/codev/issues/142)). The rule is a **prefix of at least four hex characters**, never a substring — `de` or `cafe` would otherwise match nearly every session through its id. A row that matched on its id shows an `id 4ed7505a` marker, since the id is not otherwise on screen
- A session with **two running processes** (a resumed copy, or a `/branch` parent and child) shows both in the live scope, the second marked `⚠ 2nd process`, so the chip's count and the list agree and the memory total adds every process
- The marks and lists stores are now written with owner-only permissions (`0600`); the lists store carries conversation snippets
- **A scope survives resuming from it.** Open a session from a saved list or from the live scope, come back, and you are still in that list / that scope — a scope is a place to work through several sessions. Only the search box is cleared on return, as before
- Deliberately absent: an "open all" button. Reopening 22 browser tabs is cheap; resuming 22 sessions is ~3GB of processes, which is the problem this feature exists to relieve
- Under the hood: the marks store and the new lists store share one atomic-JSON-store module (`src/atomic-json-store.ts`) — the read-authority invariant PR #137 spent four review rounds on now has exactly one implementation. 53 new unit tests (lists normalize / transitions / file roundtrip / normalizer fixed point / untrusted-file inspection, `ps` parsing and the live join, list-view scopes, session-id prefix search) — 138 total

## 1.0.86

- Feat: session rows are readable again when titles are long
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Press `⌃+⌘+R` or click the menu bar icon to launch the Quick Switcher. Searc

CodeV can list, search, and resume Claude Code sessions. Press `⌃+⌘+R` to open the Quick Switcher, then `Tab` to toggle to Sessions mode. Live status dots show session state: working (orange pulse), idle (green), needs attention (orange blink).

Search covers **every session and every user prompt you ever typed** (not just the ~100 most recent sessions shown in the list) plus titles, branches, PR links, and last AI replies. When a match sits in the middle of a conversation, the row shows a `⌕ #N …` snippet with the surrounding context. Closed one-shot sessions (≤2 messages, untitled, no PR) fold into an expandable "minor sessions" row to keep the list scannable.
Search covers **every session and every user prompt you ever typed** (not just the ~100 most recent sessions shown in the list) plus titles, branches, PR links, last AI replies, and the **session id** (a prefix of four or more hex characters — type the id your terminal status line shows to find that exact session, then `⌘D` to pin it; the row shows an `id 4ed7505a` marker since the id is not otherwise on screen). When a match sits in the middle of a conversation, the row shows an amber `match #N` snippet with the surrounding context, and every capped line — title, first/last message, branch, last reply — **moves its window to the match** so you can see *why* the row is there. Long titles are shortened **from the middle** (`head … tail`), so a title written as an `A -> B > C` chain keeps its newest step; hover for the full title. Closed one-shot sessions (≤2 messages, untitled, no PR) fold into an expandable "minor sessions" row to keep the list scannable.

**Pin** the sessions you keep coming back to (hover 📌 on a row, or `⌘D` on the selected row): they **move into** a **📌 Pinned** zone at the top, ordered by recency like the rest of the list — works even for old sessions found via deep search. **Hide** one-offs you never want in the main flow (hover ⊘, or `⇧⌘D`): they move into the minor-sessions fold, stay searchable, and can be unhidden from inside the fold (they carry a persistent ⊘ marker there). Pins and hides live in `~/.config/codev/session-marks.json`, shared across accounts.

Expand All @@ -35,6 +35,18 @@ The `📌 Pinned (N)` header carries two independent toggles:

Keyboard semantics worth knowing: the shortcuts act on the **selected row** (the one with the blue left border — hovering selects), and require an explicit selection. `⌘D` = pin/unpin toggle; `⇧⌘D` = hide (on a pinned row this unpins *and* folds in one step — pin and hide are mutually exclusive). When the last pin is removed the header disappears entirely (that's normal, not a collapse). A pinned session is never folded away as a "minor session", whatever its message count.

#### Live view and saved session lists

Two chips beside the search box, on the [Session Buddy](https://sessionbuddy.com/) model — save what is open, put the windows down, come back to the set later:

| Chip | What it does |
|---|---|
| `● N live` | **Scope the list to sessions with a running process**, with the memory they hold beside it. Built by joining `ps` against Claude Code's own `~/.claude/sessions/` registrations, so a session that is running but never registered still shows (marked `⚠ unregistered`), a registration whose process is gone is never shown as a ghost, and a session running under **two** processes (a resumed copy, a `/branch` parent and child) shows both (`⚠ 2nd process`). A **`stats`** toggle (off by default, remembered) adds each row's memory and uptime for the "which one do I close first" moment. |
| `save list…` | Appears whenever the list is scoped (`● live`, `only`, or a search): **saves exactly what is on screen as a named list**. The default name is today's `MMDD`, then `MMDD-2`, `MMDD-3` — a label, not an identity. |
| `🗂 N` | Shows the saved lists. Click one to view its members **in the order they were captured** and resume any of them; `✎` renames, `✕` (then `delete?`) deletes. |

A saved member stores what you recognise a session by — title, branch, pin state at capture, the last messages, and the **recap** line Claude Code writes into the transcript (the `※ recap:` "where we are, what's next" line), which replaces the last-reply line on the member's row; a recap much older than the session's last activity is marked `⏱`, since its "next step" may already be done. A member whose transcript is gone still reads as the session it was. Opening a session from a list or from the live scope **leaves you in that scope** when you come back (only the search box is cleared), so you can work through a set one session at a time. There is deliberately no "open all": 22 sessions is a few GB of processes, which is the very thing a saved list exists to relieve. Lists live in `~/.config/codev/session-lists.json`; a file that cannot be trusted as written is reported at load, never rewritten.

**Simple rule**: when running multiple sessions in the same project directory at the same time, give each running session a name. Closed sessions don't need names — they won't cause issues.

- **Best**: start with a name — `claude -n "my task"` (or `claude --name "my task"`)
Expand Down
121 changes: 115 additions & 6 deletions docs/session-finding-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,110 @@ listing / `history.jsonl` tail; run it either side of an action and diff. It is
"resume sometimes seems confused" into the numbers above, and it applies to any future
question about session identity (`/fork` is the open one — see #142's comment).

### 4.8 Saved session lists + the live view (issues #145, #94)

Status and open questions live in **#144**; the reasoning is here.

**The model is Session Buddy, not favourites.** Pins (§4.4) are "this matters long-term";
a saved list is "this is what I had open on Tuesday" — a named snapshot of the running
set, put down so the windows can be closed and picked up later. The two are different
things and both stay.

**Why the live view is a prerequisite, not a nicety.** "Save what is open" needs a correct
answer to "what is open", and `~/.claude/sessions/<pid>.json` alone does not give one:
measured 2026-09-05 across 33 real sessions, one was running with no registration and
one was registered with a dead process. Saving from the registrations would omit a
session and store a ghost. So the live report joins `ps` (ground truth for "running";
knows nothing about sessions) against the registrations (knows the session; can be stale
or missing), and a process with no registration is shown as its own row marked
`⚠ unregistered`. The join is what makes the count trustworthy; the same measurement also
showed that four of five "unregistered claude processes" were the daemon and its pty
helpers, which is why the filter is "session process **and** (registered **or** attached to
a tty)" rather than "any `claude` binary".

**Why it lives in the Sessions tab as scopes, not as a new tab.** Row rendering, search,
pins, status dots and resume-on-click all already live there; a separate screen would
either duplicate them or force a refactor of the biggest file in the app. Vertical space
is the scarce resource, so the two new entry points are chips in the search row (which has
spare width), and a scope replaces the list rather than adding to it. Scopes rank: a list
being viewed beats live, live beats pinned-only — encoded in `session-list-view.ts` so a
stale flag can never blank the list.

**A scope survives resuming from it; the search box does not.** Opening a session clears the
query on the next show (a query is a way of finding one session), but it leaves a saved list or
the live scope in place: a scope is a place to work *through* several sessions — resume one,
come back, resume the next — and being dropped out of it on every return was the first
complaint in live testing. The scope's header / active chip keeps it visible; `✕` or the chip
leaves it.

**What a member stores is the feature.** A list of bare sessionIds is useless for recall.
Each member captures title, branch, pin state (a snapshot — never updated later), the last
user and assistant messages, and the **recap** Claude Code writes into the transcript
(`"type":"system","subtype":"away_summary"`), every text field capped so a 30-session list
is a few tens of KB. The recap is preferred over the last assistant turn because it is
written to answer exactly the question a snapshot answers, and it is reliable enough to
lead with: 65 of 66 non-trivial sessions carry one (the misses are ≤29-line stubs that never
reached the three turns it needs). It is not unconditional — it can be switched off in
`/config`, needs the terminal to have been unfocused, and **never repeats back-to-back, so it
can predate the session's last turn** — which is why the row shows the last message as a
fallback and marks a recap `⏱` when it is more than 30 minutes older than the session's last
activity: its final sentence is usually "next: …", and acting on a stale one is the failure
mode.

**Deliberately absent: "open all".** In a browser, restoring 22 tabs is cheap. Here, 22
sessions is ~3GB of processes — the very problem the feature exists to relieve. Restore is
per-row (the existing click-to-resume), and a whole-set restore, if it ever comes, has to
show the projected cost first.

**Drift across `/branch` is shared with pins.** A list's members are keyed by sessionId
(each carrying the captured title, branch, pin state, messages and recap), so §4.7 applies
unchanged: after a branch, the member's key points at the ancestor. That is one more consumer of
the stable-task-identity decision in #142 (C1), and an argument for making it rather than
routing around it.

**Store.** `~/.config/codev/session-lists.json`, beside the marks store, on the same
authoritative-read / atomic-write / directory-watch machinery — extracted into
`src/atomic-json-store.ts` so the read-authority invariant PR #137 spent four rounds on has
exactly one implementation. **That invariant has a corollary the first live test paid for:
the store's normalizer must be a fixed point of itself.** A cap that landed on a space wrote
a trailing blank that the next read trimmed away, so the file the app had just written read
back as "normalization would change this" — non-authoritative — and every later write was
refused, silently. Two rules follow: normalize → serialize → normalize must be byte-stable
(tested), and a refused write must be shown, never swallowed.

**The `ps` join is also what makes "is it running" right for rows the registration-based
detection cannot see.** A session with no history row yet (a `/branch` child before its
first prompt) is invisible to `detectActiveSessions`, so a row for it reads as not running and
a click *resumes* it — a second process for the same id. Saved-list members and pin
placeholders now take their running state from the join as well, and viewing a list refreshes
it. The general fix — feeding the join into active detection itself — is #142 C0 territory.

**What the row shows.** By default, nothing extra: the live scope is a "running sessions
only" browse, and the one figure that says whether there is a problem — the total memory —
sits beside the search box. Per-row memory and uptime are behind a `stats` toggle (off by
default, remembered), because on most rows they track the message count closely enough to be
noise (user verdict after two rounds); they earn their width at the "which one do I close
first" moment, which is occasional. The tty is never on the row: a person cannot act on a tty
name, and it stays in the tooltip and in the data, where the future window-switching (#142 C0)
needs it.

**What the main list deliberately does not show.** A running process whose session has no
`history.jsonl` line — a `/branch` child before its first prompt (measured: the `/branch` prompt
itself is recorded under the *parent*) — has no row in the main list, and never did before
this feature either. The live scope synthesizes a row for it (named after its cwd, `⚠
unregistered` if it also lacks a registration); the main list does not. Decided 2026-09-05 to
keep it that way for now: a synthetic main-list row would be nearly blank (`codev · … msgs ·
dot`) until `forkedFrom` is read, and the honest presentation is the generation chain — the
child under its parent's lineage, with the parent's title — which is #142 C2/C3. The precise
repro and the interim option are in #149; the workaround today is the live scope.

**An untrusted store is reported, not repaired.** When the lists file exists but its
normalization is not a no-op (hand-edited, or a hypothetical future format change), the UI
says so with what the file holds — "N lists / M sessions inside; fix or remove it" — instead
of silently showing an empty list, which read as "my list was deleted" in a live test. It is
never rewritten from the UI: that would be an exception to the read-authority rule for a case
no released build produces, and a real format change is a versioned migration's job.

## 5. Batch 2 — structural investments

### 5.1 C4: preview / detail (v1 card → v2 pane)
Expand Down Expand Up @@ -367,8 +471,11 @@ question about session identity (`/fork` is the open one — see #142's comment)
- Multi-account: one DB with an `account` column; scan sources via the existing
`getScannableAccounts()`.
- Duplicate-content note: normal resumes append to the same file (§4.4) — no cross-file
duplication; only explicit `--fork-session` creates ancestor/descendant double-matches —
rare, v1 ignores.
duplication. **But `/branch` copies the transcript into a new file on every use, and it is
a daily action (23.9% of transcripts, §4.7)** — so ancestor/descendant double-matches are
common, not rare, and an index must dedupe them. The copied lines carry `forkedFrom`, so
"skip lines whose `forkedFrom.sessionId` is already indexed" is exact. (This note used to
say only `--fork-session` creates duplicates; that was wrong.)
- Expired sessions (transcript already cleaned up): the index keeps the text → results get an
"expired" badge (readable, not resumable).

Expand Down Expand Up @@ -397,10 +504,12 @@ question about session identity (`/fork` is the open one — see #142's comment)
the same file (fact 4) → titles persist naturally; pins keyed by sessionId persist the
same way.
4. **Resume semantics (verified on 2.1.207 via `--help`)**: `--resume` / `--continue`
**reuse the sessionId and continue the same file by default**; only `--fork-session`
creates a new id/file. ⚠️ Old Claude Code versions forked by default — stale web posts and
old experience still claim that; don't trust them (this plan's first draft got it wrong
until the user challenged it).
**reuse the sessionId and continue the same file by default**; `--fork-session` creates a
new id/file, **and so does `/branch`** — the same process keeps writing to a copied
transcript under a new id (§4.7, measured on 2.1.260). ⚠️ Old Claude Code versions forked
by default — stale web posts and old experience still claim that; don't trust them (this
plan's first draft got it wrong until the user challenged it). Anything about `/fork` must
carry a version: its meaning changed at 2.1.161 and again at 2.1.212 (issue #142).
5. **history.jsonl: one line = one complete user prompt** (`display` untruncated, longest
measured 9,224 chars); a session spans many lines; the accumulator keeps first/last and,
since PR #132, all prompts in a main-side map.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "CodeV",
"productName": "CodeV",
"version": "1.0.86",
"version": "1.0.87",
"description": "Quick switcher for VS Code, Cursor, and Claude Code sessions",
"repository": {
"type": "git",
Expand Down
Loading
Loading