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
54 changes: 27 additions & 27 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
_ => {}
}
}

Expand Down
18 changes: 8 additions & 10 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand All @@ -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<String>,
xdg: Option<String>,
Expand Down
6 changes: 4 additions & 2 deletions src/harness/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
if let Some(dir) = override_dir {
return Some(dir.to_path_buf());
Expand Down
38 changes: 30 additions & 8 deletions src/harness/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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!(
Expand All @@ -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}");
Expand Down Expand Up @@ -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'"),
Expand All @@ -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}");
}
Expand Down
11 changes: 5 additions & 6 deletions src/harness/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/harness/summary_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
5 changes: 5 additions & 0 deletions src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 7 additions & 4 deletions src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -332,8 +332,9 @@ pub struct TaskView {
/// Custom display name; `None` means unnamed.
pub name: Option<String>,
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,
Expand Down Expand Up @@ -932,6 +933,8 @@ pub fn decode_event(kind: u8, payload: &[u8]) -> Option<Event> {
// 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 {
Expand Down
2 changes: 1 addition & 1 deletion src/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down