# Agent Sessions (prototype) Track live Codex + Claude Code agent sessions — local Mac first, other Macs on the tailnet second — and surface them in the CodexBar menu with click-to-focus of the owning terminal window. ## Why in CodexBar CodexBar already parses `~/.claude/projects` JSONL (cost scanner) and ships a bundled CLI on macOS + Linux. Sessions reuse both: the local scanner feeds the menu UI, and the same scanner exposed as `codexbar sessions --json` is what remote Macs run over SSH. No daemon, no new app. ## Data model (CodexBarCore) ```swift public struct AgentSession: Codable, Sendable, Identifiable { public enum Provider: String, Codable, Sendable { case codex, claude } public enum Source: String, Codable, Sendable { case cli, desktopApp, ide, unknown } public enum State: String, Codable, Sendable { case active, idle } public var id: String // session UUID when resolvable, else "pid:" public var provider: Provider public var source: Source public var state: State public var pid: Int32? // nil for file-only (e.g. Codex desktop) sessions public var cwd: String? public var projectName: String? // last path component of cwd public var startedAt: Date? public var lastActivityAt: Date? // transcript mtime public var transcriptPath: String? public var host: String // local hostname, or remote host label } ``` `active` = last activity ≤ 120 s ago. `idle` = live process (or recent file) with older activity. Constants live in one `SessionScanConfig` struct (activeWindow 120 s, fileOnlyWindow 30 min) so thresholds are tunable/testable. ## Local scanner (CodexBarCore, no new deps) `LocalAgentSessionScanner` combines two signals: 1. **Process scan** — parse `ps -axo pid=,ppid=,lstart=,command=`. - Claude: command basename `claude` (skip obvious non-agent helpers). Source: path contains `Application Support/Claude/claude-code` → `.desktopApp`, else `.cli`. Deduplicate the wrapper/child pair (desktop spawns `disclaimer` parent + `claude` child with same argv; keep the child). - Codex: basename `codex` with no `app-server` argument → `.cli` (TUI or `exec`). `codex app-server` marks the desktop app as present but is not itself a session. - cwd per pid via one batched `lsof -a -d cwd -Fn -p ` call (parse `p`/`n` records). Failure → cwd nil, session still listed. 2. **Transcript correlation** - Claude: cwd → `~/.claude/projects//` (escape: every non-alphanumeric ASCII → `-`), newest `*.jsonl` by mtime → session id (filename UUID), lastActivityAt (mtime). Also reuse `ClaudeDesktopProjectsLocator` roots so desktop local-agent-mode sessions resolve. - Codex: enumerate `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` for today + yesterday (`$CODEX_HOME` respected). Read only the first line (`session_meta`: `session_id`, `cwd`, `originator`, `source`). File with mtime ≤ fileOnlyWindow and no matching live pid → file-only session, source from `originator` (`codex_exec`/`exec` → `.cli`; ide-ish originators → `.ide`; desktop → `.desktopApp`). Live `codex` pids match to rollouts by cwd (newest wins); unmatched live pid still listed with nil transcript. - Never read more than the first line of any JSONL; never load whole transcripts. Scanner is `Sendable`, pure functions where possible; ps/lsof output parsing lives in dedicated parser types fed by strings so tests use fixtures. ## CLI (CodexBarCLI) - `codexbar sessions` — table; `--json` — `[AgentSession]` (stable field names above; ISO-8601 dates). - `codexbar sessions focus ` — macOS only: focus the session's terminal window (see Focus). Exit 1 if id unknown, 2 if focus failed. - Follows existing `CLI*Command.swift` conventions. Works on Linux for listing (ps/proc paths guarded), focus is Darwin-only. ## Remote hosts (CodexBarCore + app) `RemoteSessionFetcher`: - Host list = manual entries (settings, ssh destinations like `steipete@clawmac`) ∪ automatic Tailscale discovery (no-op when tailscale is absent): run `tailscale status --json` (PATH, then `/Applications/Tailscale.app/Contents/MacOS/Tailscale`), take online peers with `"OS": "macOS"|"linux"`, use first `DNSName` label as host. Local host excluded. - Fetch per host (parallel, 5 s budget): `ssh -o BatchMode=yes -o ConnectTimeout=3 sh -lc 'codexbar sessions --json'` with fallback to the bundled app CLI path (resolve the canonical bundled location from `Scripts/package_app.sh` and hardcode it as fallback: `… || sessions --json`). Host errors are non-fatal: host shown as unreachable, others still render. - Remote focus: fire-and-forget `ssh sh -lc 'codexbar sessions focus '`. - Refresh: local scan every 30 s while the status item exists (cheap), remote every 60 s and immediately on menu open; both skipped when the feature is off. Reuse existing refresh loop plumbing rather than new timers if it fits. ## Menu UI (CodexBar app) - New menu section **Agent Sessions (N)** (N = total, all hosts) above the settings/footer area, built through the existing `MenuDescriptor`-style seam so it's testable headless. - Local sessions first, then one group per remote host (`clawmac — 2`, unreachable hosts greyed with a tooltip). Row: state dot (● active / ○ idle), provider glyph, `projectName — provider · source · 12m`. - Click local row → `SessionWindowFocuser`. Click remote row → remote focus ssh call. - Settings: "Sessions" group — a single enable toggle (default on) plus a manual hosts text field (comma-separated); Tailscale discovery is always on while the feature is enabled. Persist in `SettingsStore` like neighboring prefs. ## Focus (macOS, app + CLI shared in Core or app-adjacent target) `SessionWindowFocuser`: 1. pid → walk ppid chain to the nearest ancestor whose `NSRunningApplication.bundleIdentifier` is a known terminal/editor host: Ghostty, iTerm2, Apple Terminal, Warp, WezTerm, kitty, Alacritty, VS Code, Cursor, Zed, Claude desktop (`com.anthropic.claudefordesktop`). Fallback: the app owning the pid. 2. Activate the app, then AX (`AXUIElementCreateApplication` → `AXWindows`): raise the window whose title contains projectName or the cwd tail; fallback to frontmost window of that app. Requires Accessibility permission — call `AXIsProcessTrustedWithOptions` with prompt on first use; degrade gracefully (activate app only) when untrusted. 3. File-only sessions (no pid): Claude desktop → activate Claude.app; Codex desktop → activate Codex.app; otherwise no-op with log. tmux pane / terminal-tab precision is out of scope for the prototype. ## Tests (Tests/CodexBarTests) Fixture-driven, no live processes, no Keychain/AX: - ps output parser: desktop `disclaimer`+`claude` dedupe, codex vs `codex app-server`, weird argv. - lsof `-Fn` parser. - Claude cwd escaping → project dir mapping; newest-jsonl selection (temp dirs). - Codex rollout first-line parse → AgentSession (fixture JSONL), file-only window cutoff. - Tailscale status JSON → host list (fixture; offline/iOS peers excluded). - Sessions JSON round-trip (CLI output schema stability). - Menu section descriptor: counts, grouping, unreachable-host rendering. ## Non-goals (prototype) Claude.ai chat sessions; Codex cloud tasks; historical session browsing/analytics; "waiting on permission" state; tmux pane/tab focus; Bonjour/mDNS; persistent remote daemon or push transport; widget changes. No new SPM dependencies. ## Proof `make check` clean; `make test` (or focused `swift test --filter` covering the new tests) green; `swift run CodexBarCLI sessions --json` produces plausible output on this Mac.