Skip to content

feat: add ccsession top — live monitor for active sessions #112

Description

@sorafujitani

Problem

Running several agent sessions in parallel (Claude Code, Codex, ...) is now a normal workflow, but there is no way to see which sessions are active right now and what each one is doing. ccsession already scans every transcript on disk; it only lacks an activity-oriented view.

This issue adds ccsession top: a live, auto-refreshing picker showing recently-active sessions, with the preview pane following the highlighted session's tail. Enter resumes the session, exactly like the default picker.

UX spec

ccsession top                 # live view, default window 15m, refresh every 2s
ccsession top --window 1h     # show sessions active within the last hour
ccsession top --interval 5s   # refresh cadence

Row format (TSV, same column conventions as ccsession list):

STATUS   SOURCE   LAST      DIR          LABEL
RUNNING  claude   12s ago   ccsession    fix preview highlight ...
IDLE     codex    4m ago    myapp        add login endpoint ...
  • RUNNING (green): activity within the last 60 seconds.
  • IDLE (dim): activity within --window but older than 60 seconds.
  • Sessions with no activity inside --window are not listed.
  • The list reloads automatically every --interval without the user pressing anything; the highlighted line and query are preserved across reloads (fzf reload keeps them).
  • Preview pane: same ccsession preview output, with --preview-window set to follow so a streaming session visibly scrolls.
  • Enter: resume the highlighted session (same resume path as the default picker).
  • The three matcher-mode keybindings from the default picker are NOT needed here (fuzzy only). Grep/dir modes are out of scope.

Implementation plan

1. Activity timestamp on session.Session

  • Add FileMTime time.Time to session.Session (internal/session/types.go).
  • Populate it in ParseSessionTail (internal/session/parse.go): readTail already calls f.Stat() (parse.go:170) — capture fi.ModTime() there and thread it back, do not add a second os.Stat.
  • Backends that are not one-file-per-session (opencode) leave it zero. Define the effective activity time as:
func (s *Session) ActiveAt() time.Time {
    if !s.FileMTime.IsZero() { return s.FileMTime }
    return s.LastTime
}
  • FileMTime is preferred over LastTime because the JSONL is appended to continuously while an agent streams, so mtime moves even when the parsed tail timestamp lags.

2. list --active <duration> filter and status column

  • Add to list.Options (internal/list/list.go): Active time.Duration (zero = disabled, current behavior unchanged).
  • When Active > 0:
    • drop sessions with now.Sub(s.ActiveAt()) > opts.Active;
    • prepend a status column to formatLine output: RUNNING if now.Sub(ActiveAt()) < 60*time.Second, else IDLE. Color: green for RUNNING, faint for IDLE, following the existing color conventions in internal/ansi.
  • Wire a --active <duration> flag into cmdList in cmd/ccsession/main.go (parse with time.ParseDuration).
  • Note the column indices shift when the status column is present; top passes its own fzf --with-nth/--nth, so the default picker script is untouched.

3. top subcommand

  • New case "top" in the subcommand switch in cmd/ccsession/main.go (main.go:184).
  • New file cmd/ccsession/top.go (or extend main.go following existing style) that:
    1. Generates a random API key (crypto/rand, hex) and a temp file path for the port handshake (use os.CreateTemp).
    2. Launches fzf via exec.Command with:
      • FZF_API_KEY=<key> and FZF_DEFAULT_COMMAND unset; input provided by --bind 'start:reload(<CCSESSION_BIN> list --active <window> --color=always)';
      • --listen (no port argument — fzf picks a free one);
      • --bind 'start:+execute-silent(echo $FZF_PORT > <portfile>)' — fzf exports FZF_PORT to processes it spawns, this is the documented way to discover the port from outside;
      • --preview '<CCSESSION_BIN> preview {N}' with --preview-window including follow;
      • --bind 'enter:become(<CCSESSION_BIN> resume {N})' mirroring how the default script resumes;
      • reuse the CCSESSION_BIN self-invocation pattern from defaultScriptTmpl; CCSESSION_SOURCE inherits via os.Environ() like today, so top works with any backend including all.
    3. In a goroutine, polls the port file until non-empty (with a ~5s timeout), then every --interval sends POST http://127.0.0.1:<port>/ with header x-api-key: <key> and body reload(<CCSESSION_BIN> list --active <window> --color=always).
    4. Stops the ticker and cleans up the temp file when the fzf process exits. top returns fzf's exit status like the default picker does (130 = cancel is not an error).
  • fzf --listen requires fzf >= 0.42 and FZF_PORT/api-key semantics stabilized well before 0.58, which the README already requires — no new version constraint.

4. Help text

  • Add top to the usage text in cmd/ccsession/main.go and to the README feature list + usage section, including the --window/--interval flags.

Edge cases

  • No active sessions: fzf shows an empty list; the reload loop keeps running so sessions appear as they become active. Do not exit.
  • --interval < 1s: reject with a clear error (avoid hammering scans).
  • Port file never appears (very old fzf, listen failure): kill fzf, print an actionable error mentioning the fzf version requirement.
  • resume for opencode/grok/codex works through the existing ResumeSpec path; nothing top-specific.
  • NO_COLOR / non-tty behavior must match the existing picker (colors are forced with --color=always only inside the fzf pipeline).

Testing

  • Unit: status classification (RUNNING/IDLE/excluded) as a table-driven test over synthetic ActiveAt values; --active filtering in internal/list with fake sessions.
  • Unit: ParseSessionTail populates FileMTime (extend internal/session/parse_test.go, os.Chtimes a fixture like TestParseSessionTail_FallsBackToModTimeWhenNoTimestamp does).
  • The fzf/HTTP loop is thin glue; keep it small and manual-test it. Extract the reload-POST body/URL construction into a pure function and unit-test that.

Non-goals

  • Grep/dir matcher modes inside top.
  • Status inference from transcript content (e.g. "waiting for user" vs "streaming") — mtime recency only for now; content-based status can layer on later.
  • Watching for new sessions via fsnotify — periodic rescan is enough.

Acceptance criteria

  • ccsession top shows only sessions active within the window, refreshes automatically, preview follows the highlighted session, Enter resumes it.
  • ccsession list --active 15m works standalone and emits the status column.
  • Default picker (ccsession with no args) behavior and its column layout are completely unchanged.
  • gofmt -l . clean, go vet ./... clean, golangci-lint run clean, new tests pass with go test ./....

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestimpact: highHigh user-visible impact

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions