From a86ed669a20c591f0801932e98dd00b651091412 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 12 Aug 2026 14:46:15 -0700 Subject: [PATCH 1/4] docs(protocol): one idle window drives parked and lifecycle --- src/protocol.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/protocol.rs b/src/protocol.rs index f948850..0a04227 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -319,8 +319,8 @@ impl Preview { /// A read-only snapshot of one task: everything a dashboard row needs, with no /// handle into the live process. Time is pre-reduced to the `*_ago` durations /// and `lifecycle`/`parked` are pre-computed by the core (it owns the clock -/// and both idle windows), so nothing here depends on a process-local -/// `Instant` that a socket peer could not interpret. +/// and the one idle window both fields share), so nothing here depends on a +/// process-local `Instant` that a socket peer could not interpret. #[derive(Debug, Clone, PartialEq)] pub struct TaskView { pub id: u64, @@ -332,8 +332,9 @@ pub struct TaskView { /// Custom display name; `None` means unnamed. pub name: Option, pub lifecycle: Lifecycle, - /// Quiet past the placement window, a much longer edge than `lifecycle`'s - /// idle threshold; `false` once finished. + /// Quiet past the core's idle window while live; `false` once finished. + /// Same threshold as `Lifecycle::Idle`: the glyph reads `lifecycle`, + /// state-section placement reads this field. pub parked: bool, /// The dashboard preview, resolved by the core at snapshot time. pub preview: Preview, @@ -932,6 +933,8 @@ pub fn decode_event(kind: u8, payload: &[u8]) -> Option { // A frame from a daemon predating `parked` derives it // from the idle lifecycle: skew degrades to the // pre-`parked` signal, never to a dropped frame. + // Current cores compute `parked` from that same window, + // so the fallback matches a modern frame. let parked = if tv["parked"].is_null() { lifecycle == Lifecycle::Idle } else { From 390e81158aca702b1971ab4187a4fcb3fa54da2f Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 12 Aug 2026 14:46:16 -0700 Subject: [PATCH 2/4] refactor(harness): one registry row for capture and summary --- src/harness/mod.rs | 38 ++++++++++++++++++++++++++++-------- src/harness/summary.rs | 11 +++++------ src/harness/summary_tests.rs | 9 +++++++++ 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/harness/mod.rs b/src/harness/mod.rs index 625c9bb..382334c 100644 --- a/src/harness/mod.rs +++ b/src/harness/mod.rs @@ -102,14 +102,33 @@ pub trait Harness: Sync { } } -/// Harness registry in detection order. -pub static HARNESSES: &[&dyn Harness] = &[&Claude, &Codex, &Grok]; +/// One registered agent CLI: capture harness and display adapter. +struct Agent { + harness: &'static dyn Harness, + summary: &'static dyn crate::preview::SummaryAdapter, +} + +/// Registered CLIs in detection order. +static AGENTS: &[Agent] = &[ + Agent { + harness: &Claude, + summary: &summary::ClaudeSummary, + }, + Agent { + harness: &Codex, + summary: &summary::CodexSummary, + }, + Agent { + harness: &Grok, + summary: &summary::GrokSummary, + }, +]; /// Return the first harness that recognizes `cmd`. pub fn detect(cmd: &str) -> Option<(&'static dyn Harness, Invocation)> { - HARNESSES + AGENTS .iter() - .find_map(|h| h.detect(cmd).map(|inv| (*h, inv))) + .find_map(|a| a.harness.detect(cmd).map(|inv| (a.harness, inv))) } /// Classification of an accepted agent-CLI command. @@ -380,7 +399,8 @@ mod tests { /// including path-qualified programs and quoted IDs. #[test] fn every_harness_detects_the_two_authored_shapes() { - for &h in HARNESSES { + for a in AGENTS { + let h = a.harness; let (prog, sel) = h.shape(); assert_eq!(h.detect(prog), Some(Invocation::Bare), "{prog}"); assert_eq!( @@ -403,7 +423,8 @@ mod tests { /// unchanged. #[test] fn every_harness_regenerates_the_canonical_resume_form() { - for &h in HARNESSES { + for a in AGENTS { + let h = a.harness; let (prog, sel) = h.shape(); let canonical = format!("{prog} {sel} '{ID}'"); assert_eq!(h.resume_command(prog, ID), canonical, "{prog}"); @@ -432,7 +453,8 @@ mod tests { /// opacity cases stay in each harness's own test module. #[test] fn every_harness_keeps_shared_shell_syntax_opaque() { - for &h in HARNESSES { + for a in AGENTS { + let h = a.harness; let (prog, sel) = h.shape(); let opaque = [ format!("{prog} 'fix the tests'"), @@ -456,7 +478,7 @@ mod tests { ); } // Another tool's program word never matches. - for other in HARNESSES.iter().map(|o| o.shape().0) { + for other in AGENTS.iter().map(|o| o.harness.shape().0) { if other != prog { assert_eq!(h.detect(other), None, "{other:?} is not {prog}"); } diff --git a/src/harness/summary.rs b/src/harness/summary.rs index 9f34c53..04be348 100644 --- a/src/harness/summary.rs +++ b/src/harness/summary.rs @@ -35,12 +35,11 @@ use crate::preview::SummaryAdapter; /// independent of session-capture instrumentation. pub fn select(command: &str) -> Option<&'static dyn SummaryAdapter> { let first = command.split_whitespace().next()?; - match Path::new(first).file_name()?.to_str()? { - "claude" => Some(&ClaudeSummary), - "codex" => Some(&CodexSummary), - "grok" => Some(&GrokSummary), - _ => None, - } + let name = Path::new(first).file_name()?.to_str()?; + super::AGENTS + .iter() + .find(|a| a.harness.shape().0 == name) + .map(|a| a.summary) } /// Whether `row` is a full-width horizontal rule: nothing but `─`, long diff --git a/src/harness/summary_tests.rs b/src/harness/summary_tests.rs index 8abae7a..153f4be 100644 --- a/src/harness/summary_tests.rs +++ b/src/harness/summary_tests.rs @@ -66,6 +66,15 @@ fn select_matches_first_word_basenames_only() { } } +/// Every registered program word selects a dashboard adapter. +#[test] +fn select_covers_every_registered_shape() { + for a in crate::harness::AGENTS { + let name = a.harness.shape().0; + assert!(select(name).is_some(), "{name} must select an adapter"); + } +} + /// Each program word routes to its own CLI's matchers: the selected /// adapter fires that CLI's rule on that CLI's screen shape. #[test] From a3bf23206ba5e68744aa5660b9b63c88bf25605c Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 12 Aug 2026 14:46:16 -0700 Subject: [PATCH 3/4] refactor(path): host the shared runtime-dir env key --- src/daemon.rs | 18 ++++++++---------- src/harness/assets.rs | 6 ++++-- src/path.rs | 5 +++++ src/supervisor.rs | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 1ddef12..a7bb80f 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -45,6 +45,7 @@ use nix::{ use crate::{ core::{LoopExit, Wake, run_loop}, frame::{MAX_FRAME, SEND_TIMEOUT, read_frame, write_frame}, + path::FLEETCOM_RUNTIME_DIR, protocol::{ Command, Event, LaunchContext, PROTOCOL_VERSION, decode_command, decode_event, decode_hello, encode_command, encode_event, encode_hello, hello_version, @@ -61,15 +62,11 @@ const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5); /// in microseconds. const HELLO_PROBE: Duration = Duration::from_secs(1); -/// Env var overriding the per-user runtime directory (socket, lock, and the -/// capture-asset root the supervisor derives from it). -pub const FLEETCOM_RUNTIME_DIR: &str = "FLEETCOM_RUNTIME_DIR"; - -/// Per-user directory holding the socket. `FLEETCOM_RUNTIME_DIR` overrides it -/// (tests point it at an isolated temp dir); else `$XDG_RUNTIME_DIR/fleetcom` -/// (per-user on Linux); else `$TMPDIR/fleetcom-$uid`, the macOS path, where -/// `$TMPDIR` is already per-user and the uid suffix covers a shared `/tmp` on an -/// XDG-less Linux. +/// Socket/lock directory. `FLEETCOM_RUNTIME_DIR` overrides it (tests point it +/// at an isolated temp dir); else `$XDG_RUNTIME_DIR/fleetcom` (per-user on +/// Linux); else `$TMPDIR/fleetcom-$uid`, the macOS path, where `$TMPDIR` is +/// already per-user and the uid suffix covers a shared `/tmp` on an XDG-less +/// Linux. fn runtime_dir() -> PathBuf { resolve_runtime_dir( std::env::var(FLEETCOM_RUNTIME_DIR).ok(), @@ -79,7 +76,8 @@ fn runtime_dir() -> PathBuf { ) } -/// Resolve the runtime directory from explicit inputs. +/// Socket/lock resolver: override verbatim, else nonempty +/// `$XDG_RUNTIME_DIR/fleetcom`, else `$TMPDIR/fleetcom-$uid`. fn resolve_runtime_dir( override_dir: Option, xdg: Option, diff --git a/src/harness/assets.rs b/src/harness/assets.rs index 5a68eb0..bd37513 100644 --- a/src/harness/assets.rs +++ b/src/harness/assets.rs @@ -72,8 +72,10 @@ fn claude_settings_json() -> String { .dump() } -/// Resolve the capture root from an explicit runtime directory, the platform -/// runtime directory, or the platform cache directory, in that order. +/// Capture-root resolver. An explicit override is what the supervisor passes +/// when `FLEETCOM_RUNTIME_DIR` is set; otherwise `dirs::runtime_dir()/fleetcom`, +/// then `dirs::cache_dir()/fleetcom/run`. Those fallbacks are not +/// `daemon::resolve_runtime_dir`. pub fn runtime_root(override_dir: Option<&Path>) -> Option { if let Some(dir) = override_dir { return Some(dir.to_path_buf()); diff --git a/src/path.rs b/src/path.rs index e77bbd3..591ff1c 100644 --- a/src/path.rs +++ b/src/path.rs @@ -2,6 +2,11 @@ use std::path::{Component, Path, PathBuf}; +/// Shared env key for the runtime directory. Both the socket/lock resolver +/// (`resolve_runtime_dir`) and the capture-root resolver (`runtime_root`) +/// honor it; their fallbacks differ. +pub const FLEETCOM_RUNTIME_DIR: &str = "FLEETCOM_RUNTIME_DIR"; + /// Shorten a path for display: `$HOME` collapses to `~`. Everything else stays /// absolute, so two directories never render as the same label. pub fn abbreviate(path: &Path) -> String { diff --git a/src/supervisor.rs b/src/supervisor.rs index ca5a639..e35cf92 100644 --- a/src/supervisor.rs +++ b/src/supervisor.rs @@ -720,7 +720,7 @@ impl Supervisor { /// and installation failure disables instrumentation for the spawn. fn ensure_capture_assets(&mut self) -> Option<&assets::CaptureAssets> { let root = self - .launch_env_path(crate::daemon::FLEETCOM_RUNTIME_DIR) + .launch_env_path(path::FLEETCOM_RUNTIME_DIR) .or_else(|| { assets::runtime_root(None).map(|base| { let key = self From ec71b5159f9cb56851355c11a93a9bf23584ec5f Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 12 Aug 2026 14:46:16 -0700 Subject: [PATCH 4/4] refactor(app): one match for session-picker keys --- src/app.rs | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/app.rs b/src/app.rs index db2559d..8ae21a5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1287,36 +1287,36 @@ impl App { } return; } - match self.session_page { - SessionPage::Saved => match k.code { - KeyCode::Esc => self.mode = Mode::Dashboard, - KeyCode::Up => self.session_sel = self.session_sel.saturating_sub(1), - KeyCode::Down => { - self.session_sel = step_down(self.session_sel, self.session_names.len()) - } - KeyCode::Enter => { - if let Some(name) = self.session_names.get(self.session_sel).cloned() { - self.load_session(&name); + match k.code { + KeyCode::Esc => self.mode = Mode::Dashboard, + KeyCode::Up | KeyCode::Down => { + let (sel, len) = match self.session_page { + SessionPage::Saved => (&mut self.session_sel, self.session_names.len()), + SessionPage::Recovery => (&mut self.recovery_sel, self.session_recovery.len()), + }; + *sel = if k.code == KeyCode::Up { + sel.saturating_sub(1) + } else { + step_down(*sel, len) + }; + } + KeyCode::Enter => { + match self.session_page { + SessionPage::Saved => { + if let Some(name) = self.session_names.get(self.session_sel).cloned() { + self.load_session(&name); + } } - self.mode = Mode::Dashboard; - } - _ => {} - }, - SessionPage::Recovery => match k.code { - KeyCode::Esc => self.mode = Mode::Dashboard, - KeyCode::Up => self.recovery_sel = self.recovery_sel.saturating_sub(1), - KeyCode::Down => { - self.recovery_sel = step_down(self.recovery_sel, self.session_recovery.len()) - } - KeyCode::Enter => { - if let Some(e) = self.session_recovery.get(self.recovery_sel) { - let stem = e.stem.clone(); - self.transport.send(Command::LoadRecovery { stem }); + SessionPage::Recovery => { + if let Some(e) = self.session_recovery.get(self.recovery_sel) { + let stem = e.stem.clone(); + self.transport.send(Command::LoadRecovery { stem }); + } } - self.mode = Mode::Dashboard; } - _ => {} - }, + self.mode = Mode::Dashboard; + } + _ => {} } }