diff --git a/README.md b/README.md index f2f08ac..608cdd5 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Managing several long-lived commands across terminal panes is pesky, especially - Run each command in its own PTY and group tasks by state, working directory, or named group. - Keep tasks running under a daemon across client disconnects. -- Save and reload task recipes: directories, commands, group assignments, display names, tags, and the flagship. +- Save and reload task recipes: directories, commands, group assignments, display names, tags, the flagship, and which tasks had finished. - Rerun a completed task in place, keeping its identity, group, and name. - Launch `claude`, `codex`, `grok`, and `omp` as managed agents and resume their conversations on rerun and reload. - Recover the current task set from automatic snapshots. diff --git a/docs/README.md b/docs/README.md index fdc5180..a741b7f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # fleetcom Documentation -The daemon supervises live processes; session files store repeatable launch recipes. Reconnecting returns you to the daemon's running tasks, while loading a session starts new processes from its recipe. Use this guide for storage paths, lifecycle details, and a complete first run. +Use the daemon to supervise live processes and session files to save repeatable launch recipes, including whether each task had finished. Reconnect to access the daemon's running tasks. Load a session to start new processes for its live entries and restore finished tasks as stopped rows. Use this guide for storage paths, lifecycle details, and a complete first run. ## Index @@ -150,7 +150,7 @@ Press `w`, type a name, and press `Enter` to save the fleet as a [session](sessi | `/daemon.lock` | `0666 & ~umask` when new; otherwise unchanged | the owning daemon's PID, trustworthy only while its `flock` is held | | `/daemon.log` | `0666 & ~umask` when new; otherwise unchanged | stderr from the autostarted daemon | | [session directory](#config-directory-sessions) | `0700` | saved recipes | -| `/.json` | `0600` | directories, commands, groups, display names, tags, the flagship | +| `/.json` | `0600` | directories, commands, groups, display names, tags, the flagship, stopped flags | | `/recovery/` | `0700` | [automatic snapshots](sessions.md#recovery) | | `/recovery/.json` | `0600` | one automatic session recipe | | `/-/` | `0700` | [agent hook, notifier, and extension-module assets plus per-run capture payloads](agent-resume.md#capture-state-and-isolation) | @@ -165,7 +165,7 @@ The runtime directory is validated before its contents are used. The path must b Full command lines are persisted in recipes, including any embedded secrets. A token passed as an argument is written to its session file and to every recovery snapshot that captures the task. -The client environment is not persisted. Each client's environment and working directory are sent during the connection handshake and retained in daemon memory. Use session and recovery files to preserve only directories, commands, group assignments, display names, tags, and the flagship. +The client environment is not persisted. Each client's environment and working directory are sent during the connection handshake and retained in daemon memory. Use session and recovery files to preserve only directories, commands, group assignments, display names, tags, the flagship, and whether each task had finished. ### Captured IDs diff --git a/docs/commands.md b/docs/commands.md index b1ba61e..256a87a 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -34,8 +34,8 @@ Choose the operating mode with launch arguments. Use keys to control the dashboa | `Ctrl-]` | Attach to the flagship | | `g` | Assign the selected task to a group (through the group picker) | | `R` | Rename the selected task: a display name shown in place of the command | -| `r` | Rerun a finished task; resume the conversation for a managed agent | -| `X` | Kill a running task (`TERM`, then `KILL` after 2 s), or remove a finished one | +| `r` | Rerun a finished task or start a stopped one; resume the conversation for a managed agent | +| `X` | Kill a running task (`TERM`, then `KILL` after 2 s), or remove a finished or stopped one | | `w` | Save the current tasks as a session | | `o` | Load a saved session or a recovery snapshot (through the [session picker](#the-o-session-picker)) | | `?` | Open the [controls overlay](#the--controls-overlay) | @@ -53,6 +53,7 @@ Choose the operating mode with launch arguments. Use keys to control the dashboa | `∙` | Idle: running, but quiet | | `✓` | Completed, exit 0 | | `✗` | Completed, non-zero exit | +| ` ` | Stopped: restored from a [session](sessions.md) and not yet started | | `◆` | Tagged "in use" | | `⚑` | The [flagship](#the-flagship) | @@ -108,13 +109,13 @@ Press `Esc`, `Enter`, `q`, or `End`, or scroll to the bottom, to return to live #### Destroying tasks -Press uppercase `X` to kill a running task or remove a finished one. On removal, remaining processes in the task's process group are also terminated: `TERM`, then `KILL` after two seconds. Lowercase `x` and `Ctrl-X` are unbound. +Press uppercase `X` to kill a running task or remove a finished or stopped one. On removal of a finished task, remaining processes in its process group are also terminated: `TERM`, then `KILL` after two seconds. A stopped task has no process group to sweep. Lowercase `x` and `Ctrl-X` are unbound. #### Rerunning tasks -Press `r` to rerun a finished task. Running tasks cannot be rerun without first being killed and are left untouched. +Press `r` to rerun a finished task or start a stopped one: a task restored from a [session](sessions.md) that had finished when the file was written. Until you start a stopped task, there is no process or output to view in attach or peek. Kill a running task before rerunning it; pressing `r` while it is running has no effect. -On rerun, the replacement is started in the same directory with the requesting client's environment. For a literal task, the exact command text is run again. For a managed agent, the binary is resolved on the current `PATH`, and the captured conversation is resumed when an ID is known; see [agent session resume](agent-resume.md). +On rerun, start the replacement in the same directory with the requesting client's environment. For a literal task, run the exact command text again. For a managed agent, resolve the binary on the current `PATH` and resume the captured conversation when an ID is known; see [agent session resume](agent-resume.md). For a stopped managed agent, resume its stored session ID. Binary lookup is deferred until you press `r`, so you can load the session without the agent installed. On rerun, the task's ID, `◆` tag, group, name, and spawn order are preserved; its clock and screen are reset. Under lifecycle sorting, the restarted task may be listed in another section. You can also rerun inside peek without closing the overlay. @@ -128,9 +129,9 @@ Press uppercase `Q` to stop the daemon and terminate each task's process group: #### Grouping and tagging -Press `s` to cycle through state, dir, and custom grouping. In the header strip `by state · dir · custom`, the active mode is bold and the rest dim. +Press `s` to cycle through state, dir, and custom grouping. In the header strip `by state · dir · custom`, the active mode is bold and the rest dim. To its left, read the counts as `N running · N idle · N done`, followed by ` · N stopped` only while at least one stopped task is listed. -- By state: In use / Running / Idle / Completed. After 10 s without output, a running task is grouped under Idle. Completed tasks are grouped in one section, with exit status marked `✓`/`✗`. +- By state: In use / Running / Idle / Completed. After 10 s without output, a running task is grouped under Idle. Completed tasks are grouped in one section, with exit status marked `✓`/`✗`. Stopped tasks are placed in Completed as well, with a blank glyph and a blank age cell: no process has run under the row, so there is no launch, output, or exit time to measure from. - By dir: one section per working directory; the invocation directory first, then the remaining labels sorted without regard to case. - By custom group: one section per group name, sorted without regard to case, with Unassigned last. Fresh spawns remain unassigned unless they inherit a group, and the Unassigned section exists only while it has a member. @@ -138,7 +139,7 @@ Dashboard section labels and within-section directory tiebreaks use the same cas Groups belong to task state: an assignment survives client detach and rerun (`r`), and switching grouping modes does not modify it. Press `g` to reassign the selected task through the [group picker](#the-g-group-picker). -Press `m` to toggle the "in use" tag, marked `◆`. In state mode, tagged tasks form the In use section at the top. In custom mode, tagged tasks are placed at the top of their existing groups rather than in a global section. Within a dir or custom section, tasks are sorted as tagged, live, then completed; each class is then sorted by directory and spawn order. Idle state does not affect row order in these modes, so quiet tasks are kept in place and marked `∙`. In state mode, quiet tasks are moved from Running to Idle. +Press `m` to toggle the "in use" tag, marked `◆`. In state mode, tagged tasks form the In use section at the top. In custom mode, tagged tasks are placed at the top of their existing groups rather than in a global section. Within a dir or custom section, tasks are sorted as tagged, live, then completed or stopped; each class is then sorted by directory and spawn order. Idle state does not affect row order in these modes, so quiet tasks are kept in place and marked `∙`. In state mode, quiet tasks are moved from Running to Idle. Press `M` to select the next tagged task in dashboard order, wrapping after the last. With no tagged tasks, the selection is unchanged; with one, that task is selected. @@ -146,7 +147,7 @@ In custom mode only, a new command is assigned the selected task's group, throug #### The flagship -Press `]` to mark the selected task as the flagship, shown as `⚑`; press it again to clear the mark. You can mark only one task at a time. Select another task and press `]` to move the mark. Finished tasks cannot be marked. Sorting is unchanged. +Press `]` to mark the selected task as the flagship, shown as `⚑`; press it again to clear the mark. You can mark only one task at a time. Select another task and press `]` to move the mark. Finished and stopped tasks cannot be marked. Sorting is unchanged. Press `Ctrl-]` from the dashboard, peek, or an attached task to attach to the flagship, and again to go back: @@ -155,7 +156,7 @@ Press `Ctrl-]` from the dashboard, peek, or an attached task to attach to the fl After attaching to the flagship with `Enter`, press `Ctrl-]` to return to the dashboard. -You keep tags set with `m` after task completion, but lose the flagship mark on completion or removal. Save and load [sessions](sessions.md) to preserve and restore both marks. You can also recover both marks from automatic snapshots. When loading with a current flagship, you keep that mark. +You keep tags set with `m` after task completion, but lose the flagship mark on completion or removal. Save and load [sessions](sessions.md) to preserve and restore both marks. You can also recover both marks from automatic snapshots. When loading with a current flagship, you keep that mark. A stored flagship on a stopped entry is not restored. #### Renaming @@ -229,7 +230,7 @@ Bindings are arranged in two columns. At limited height, group headers are omitt ## Peek -Peek shows the selected task's live screen (the last screenful) in a centered box over the dashboard. Use `↑`/`↓` (or `k`/`j`) to switch tasks, `Enter` to attach, `r` to rerun a finished task, or `Space`, `Esc`, or `q` to close. +Use peek to view the selected task's live screen (the last screenful) in a centered box over the dashboard. Use `↑`/`↓` (or `k`/`j`) to switch tasks, `Enter` to attach, `r` to rerun a finished task or start a stopped one, or `Space`, `Esc`, or `q` to close. Read the source of the row's dashboard preview in the footer's `preview:` segment: `floor` (the last non-blank row of the live screen), `marker` (a full-screen program with no usable title), `title` (the child's window title), or `anchor/` (a recognized agent status, tagged with the matcher that produced it). On the alternate screen, any current title is rendered; recognized title formats are normalized first. On the primary screen, only a retained title recognized by the task's agent adapter is rendered. Otherwise, `floor` is used. Most rules name a screen matcher, such as `claude:spinner` or `codex:approval-menu`. The `claude:registry-approval` and `claude:registry-waiting` rules instead come from Claude's on-disk session status. diff --git a/docs/sessions.md b/docs/sessions.md index b27ada6..fc542f6 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -1,6 +1,6 @@ # Sessions -Save a session to preserve the task set's working directories, commands or managed agents with their session IDs, group assignments, display names, "in use" tags, and flagship mark. You cannot preserve client state (the attached or peeked task, selection, grouping and sort modes, terminal size) or process state (lifecycle, output, timings) in a session. Each time you load a session, you start new processes with the saved settings. To keep live processes running across client disconnects, use the [daemon](README.md#storage-paths). +Save a session to preserve the task set's working directories, commands or managed agents with their session IDs, group assignments, display names, "in use" tags, flagship mark, and whether each task had finished. You cannot preserve client state (the attached or peeked task, selection, grouping and sort modes, terminal size) or process state (output, timings, exit status) in a session. Each time you load a session, you start new processes for the tasks that were live; a task that had finished is restored as a stopped row, with no process until you press `r`. To keep live processes running across client disconnects, use the [daemon](README.md#storage-paths). ## Storage @@ -20,10 +20,10 @@ The filename is derived from the session name: leading and trailing whitespace i A session file is a JSON object with three fields: `version` for the format version, currently 2; `name` for the typed session name, trimmed but not sanitized; and `dirs` for an ordered list of entries per working directory. Use one of two entry forms, according to the task's launch mode: -- A **literal** entry is a typed command. Use a bare string without labels or marks, or an object with `cmd` and optional `group`, `name`, `tagged`, and `flagship` fields. Load a literal entry to run the stored text unchanged through your shell: even `"claude"` is a literal command. -- A **managed** entry is an object with `agent`, the registered program word (`claude`, `codex`, `grok`, or `omp`), plus optional `resume`, `group`, `name`, `tagged`, and `flagship` fields. Save tasks launched from the Agent page in this form. Load a managed entry with `resume` to reopen the stored session ID, or without it to start a fresh conversation. See [Agent session resume](agent-resume.md). +- A **literal** entry is a typed command. Use a bare string without labels or marks, or an object with `cmd` and optional `group`, `name`, `tagged`, `flagship`, and `stopped` fields. Load a literal entry to run the stored text unchanged through your shell: even `"claude"` is a literal command. +- A **managed** entry is an object with `agent`, the registered program word (`claude`, `codex`, `grok`, or `omp`), plus optional `resume`, `group`, `name`, `tagged`, `flagship`, and `stopped` fields. Save tasks launched from the Agent page in this form. Load a managed entry with `resume` to reopen the stored session ID, or without it to start a fresh conversation. See [Agent session resume](agent-resume.md). -Use the optional booleans `tagged` and `flagship` on either object form. Set `tagged` to `true` to tag a task ["in use"](commands.md#grouping-and-tagging), or `flagship` to `true` to mark the [flagship](commands.md#the-flagship). To leave either mark unset, use `false`, `null`, or omit the field. On save, include each field only when true. Mark at most one entry per file as the flagship; with a second, you cannot load the file. +Use the optional booleans `tagged`, `flagship`, and `stopped` on either object form. Set `tagged` to `true` to tag a task ["in use"](commands.md#grouping-and-tagging), `flagship` to `true` to mark the [flagship](commands.md#the-flagship), or `stopped` to `true` to restore the task as a row with no process. To leave any of them unset, use `false`, `null`, or omit the field. On save, include each field only when true; `stopped` is true for every task that had finished or had not yet been started. Mark at most one entry per file as the flagship; with a second, you cannot load the file. ```json { @@ -33,6 +33,7 @@ Use the optional booleans `tagged` and `flagship` on either object form. Set `ta "~/work/api": [ "cargo watch -x test", { "cmd": "cargo run", "group": "api", "name": "api server", "tagged": true }, + { "cmd": "cargo build --release", "stopped": true }, { "agent": "claude", "resume": "c8c4a5cc-0b32-4ba0-a6b4-6ed08c218e0d", "group": "agents", "flagship": true }, { "agent": "omp" } ], @@ -62,19 +63,23 @@ On load, expect validation of the entire recipe before any commands are started. - For each entry, use a command string or an object with a string `cmd` or a string `agent`, never both. - For `agent`, use a supported program word. For `resume`, use a lowercase `8-4-4-4-12` UUID, `null`, or omit the field. - For the optional entry `group` and `name` fields and the wrapped session's `name`, use strings, `null`, or omit the fields. -- For `tagged` and `flagship`, use booleans, `null`, or omit the fields. Set `flagship` to `true` on at most one entry; otherwise, both entry locations are reported in the error. +- For `tagged`, `flagship`, and `stopped`, use booleans, `null`, or omit the fields. Set `flagship` to `true` on at most one entry; otherwise, both entry locations are reported in the error. You can include unknown fields in wrapped metadata and entry objects; these are ignored. You can also use empty maps, arrays, and strings. With invalid JSON or any malformed field, you cannot load any part of the recipe; your existing tasks and recipe file remain intact. For schema errors, expect the quoted directory key, entry number (starting at 1), and offending field where applicable. -After validation, group and display names are stripped of control characters and surrounding whitespace and limited to 64 characters. `Unassigned` is treated as no group but is a legal display name. Missing directories, entries past the task limit, and literal commands past the length limit are skipped and counted in the load status. A managed entry whose agent is not found on the connected client's `PATH` is reported on the status line and counted as a failed spawn; the remaining entries are still loaded. Spawn failures are reported separately; tasks already started from a structurally valid recipe are left running. +After validation, group and display names are stripped of control characters and surrounding whitespace and limited to 64 characters. `Unassigned` is treated as no group but is a legal display name. Missing directories, entries past the task limit, and literal commands past the length limit are skipped and counted in the load status. For a live managed entry without an agent binary on the connected client's `PATH`, expect a diagnostic on the status line and a failed-spawn count. The remaining entries are still loaded. Spawn failures are reported separately; tasks already started from a structurally valid recipe are left running. -Use the string form for commands with no group, no name, and no mark. String and object entries can appear in the same directory array. +A stopped entry is restored as a row with no process: lifecycle `stopped`, a blank glyph, a blank age cell, and an empty screen under attach and peek. The same directory, task-limit, and length rules apply. Load a stopped managed entry by agent word alone; binary lookup on the connected client's `PATH` is deferred until you press `r`, so you can load the file without the agent installed. Press `r` to start the row: run the stored text for a literal entry, or resume the stored session ID for a managed entry. See [Rerunning tasks](commands.md#rerunning-tasks). + +Read each outcome on the status line, with the stopped count before the skipped and failed counts: `loaded 'api': 1 task(s), 1 stopped, 1 skipped (missing dir, task limit, or command too long)`. With nothing spawned and at least one other count, expect no `0 task(s)` clause: `loaded 'api': 1 stopped`. After loading a recovery snapshot, expect `loaded recovery snapshot; save to name it, 1 stopped`. + +Use the string form for a live command with no group, no name, and no mark. String and object entries can appear in the same directory array. ## Recovery The current task set is automatically snapshotted under `recovery/` inside the session directory. A separate file is written for each daemon (or `--foreground` core), named for its start time and process ID. After each write, the new file and files whose process IDs are still live are protected; among the remaining files, the nine newest names are retained. A shared recovery directory can therefore contain more than ten snapshots while multiple writers are live. -Expect a snapshot pass two seconds after the last command that can change a saved recipe, including marking with `m` or `]`. After a burst of commands, count the delay from the last command. You also get a pass after the flagship mark is cleared on task exit, without a command. For a nonempty recipe, expect a write when its content or destination changed, or when the expected snapshot file is missing. For changes made without a command, such as a newly captured session ID for a managed task, expect checks every 60 seconds. +Expect a snapshot pass two seconds after the last command that can change a saved recipe, including marking with `m` or `]`. After a burst of commands, count the delay from the last command. You also get a pass after a task exits, without a command, to persist the entry's `stopped` flag and clear any flagship mark on that task. For a nonempty recipe, expect a write when its content or destination changed, or when the expected snapshot file is missing. For changes made without a command, such as a newly captured session ID for a managed task, expect checks every 60 seconds. - No snapshot is written for an empty fleet. After removing every task, the previous snapshot is retained. - Snapshots are left in place after quitting, disconnecting, or `fleetcom --kill`. @@ -87,8 +92,8 @@ As with saved recipes, full command lines are persisted in recovery files, inclu ## Saving and loading -- Save: press `w` in the dashboard, type a name, and press `Enter` to write `.json` with the session name and each task's directory, command or managed agent and session ID, optional group and name, "in use" tag, and flagship mark. +- Save: press `w` in the dashboard, type a name, and press `Enter` to write `.json` with the session name and each task's directory, command or managed agent and session ID, optional group and name, "in use" tag, flagship mark, and stopped flag. - Load in-app: `o`, pick from the list, `Enter`. - Load at launch: `fleetcom `. -Load a session to start new processes from the stored commands. To retain live process state, use the daemon; you cannot store that state in session files. When you load, you keep your existing tasks and marks and recover the tags on every loaded task. You recover a stored flagship only when no task is currently marked. See [Agent session resume](agent-resume.md) for preserving supported agent conversations across relaunch. +Load a session to start new processes from the live entries and to restore the stopped entries as rows with no process; press `r` on a stopped row to start it. To retain live process state, use the daemon; you cannot store that state in session files. When you load, you keep your existing tasks and marks and recover the tags on every loaded task. You recover a stored flagship only when no task is currently marked, and never on a stopped entry. See [Agent session resume](agent-resume.md) for preserving supported agent conversations across relaunch. diff --git a/src/app.rs b/src/app.rs index 2379d2b..2276786 100644 --- a/src/app.rs +++ b/src/app.rs @@ -364,12 +364,12 @@ fn step_down(sel: usize, len: usize) -> usize { fn state_section(v: &TaskView) -> (u8, &'static str) { if v.tagged { (0, "In use") + } else if v.lifecycle.finished() { + (3, "Completed") + } else if v.lifecycle == Lifecycle::Idle { + (2, "Idle") } else { - match v.lifecycle { - Lifecycle::Active => (1, "Running"), - Lifecycle::Idle => (2, "Idle"), - Lifecycle::Ok | Lifecycle::Failed => (3, "Completed"), - } + (1, "Running") } } @@ -379,7 +379,7 @@ fn state_section(v: &TaskView) -> (u8, &'static str) { fn row_rank(v: &TaskView) -> u8 { if v.tagged { 0 - } else if matches!(v.lifecycle, Lifecycle::Ok | Lifecycle::Failed) { + } else if v.lifecycle.finished() { 2 } else { 1 @@ -1327,7 +1327,7 @@ impl App { // next daemon tick, before inclusion in a snapshot. KeyCode::Char(']') => { if let Some(i) = self.selected_task() - && !matches!(self.views[i].lifecycle, Lifecycle::Ok | Lifecycle::Failed) + && !self.views[i].lifecycle.finished() { let v = &self.views[i]; let id = (!v.flagship).then_some(v.id); @@ -1961,7 +1961,7 @@ impl App { /// Restart the selected task only when the local snapshot marks it finished. fn rerun_selected(&mut self) { if let Some(i) = self.selected_task() - && matches!(self.views[i].lifecycle, Lifecycle::Ok | Lifecycle::Failed) + && self.views[i].lifecycle.finished() { self.transport.send(Command::Restart { id: self.views[i].id, @@ -1974,8 +1974,7 @@ impl App { return; }; let id = self.views[i].id; - let finished = matches!(self.views[i].lifecycle, Lifecycle::Ok | Lifecycle::Failed); - if finished { + if self.views[i].lifecycle.finished() { // Drop it, landing selection on the neighbor (not the top). The // removal reflects next tick, so pick the neighbor id from the // *current* order now and pin selection to it. diff --git a/src/app_readme_tests.rs b/src/app_readme_tests.rs index d51bc55..e2ebb00 100644 --- a/src/app_readme_tests.rs +++ b/src/app_readme_tests.rs @@ -144,7 +144,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { "Scope small fixes for dashboard and CLI", "claude:action-row", ), - started_ago: mins(2), + started_ago: Some(mins(2)), quiet_ago: Some(secs(3)), finished_ago: None, }, @@ -162,7 +162,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { "Fable 5.1 (high) · Inferring… · thinking with high effort", "claude:spinner", ), - started_ago: mins(5), + started_ago: Some(mins(5)), quiet_ago: Some(secs(8)), finished_ago: None, }, @@ -177,7 +177,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: Some("Grok Language".to_string()), lifecycle: Lifecycle::Active, preview: working("Grok 4.7 (xhigh) · Responding…", "grok:spinner"), - started_ago: mins(12), + started_ago: Some(mins(12)), quiet_ago: Some(secs(4)), finished_ago: None, }, @@ -192,7 +192,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: Some("Codex Language".to_string()), lifecycle: Lifecycle::Active, preview: anchor(CODEX_LANGUAGE, "codex:working"), - started_ago: mins(18), + started_ago: Some(mins(18)), quiet_ago: Some(secs(2)), finished_ago: None, }, @@ -207,7 +207,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: Some("Codex Review".to_string()), lifecycle: Lifecycle::Active, preview: anchor(CODEX_REVIEW, "codex:working"), - started_ago: mins(24), + started_ago: Some(mins(24)), quiet_ago: Some(secs(6)), finished_ago: None, }, @@ -222,7 +222,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: None, lifecycle: Lifecycle::Ok, preview: frozen(FLEETCOM_TESTS), - started_ago: mins(2), + started_ago: Some(mins(2)), quiet_ago: None, finished_ago: Some(secs(12)), }, @@ -237,7 +237,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: None, lifecycle: Lifecycle::Failed, preview: frozen(FLEETCOM_CLIPPY), - started_ago: mins(5), + started_ago: Some(mins(5)), quiet_ago: None, finished_ago: Some(mins(3)), }, @@ -252,7 +252,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: Some("claude agents".to_string()), lifecycle: Lifecycle::Active, preview: title("2 awaiting input · claude agents"), - started_ago: mins(63), + started_ago: Some(mins(63)), quiet_ago: Some(secs(9)), finished_ago: None, }, @@ -267,7 +267,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: Some("Zellij".to_string()), lifecycle: Lifecycle::Active, preview: title("Desktop ¦ Utility"), - started_ago: mins(126), + started_ago: Some(mins(126)), quiet_ago: Some(secs(4)), finished_ago: None, }, @@ -282,7 +282,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: None, lifecycle: Lifecycle::Idle, preview: floor(">>>"), - started_ago: mins(48), + started_ago: Some(mins(48)), quiet_ago: Some(mins(41)), finished_ago: None, }, @@ -297,7 +297,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: None, lifecycle: Lifecycle::Ok, preview: frozen("Already up-to-date."), - started_ago: mins(14), + started_ago: Some(mins(14)), quiet_ago: None, finished_ago: Some(mins(13)), }, @@ -312,7 +312,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { name: Some("Game Infra Review".to_string()), lifecycle: Lifecycle::Active, preview: title("Turret Game Codebase Organization and Ex… - grok"), - started_ago: mins(8), + started_ago: Some(mins(8)), quiet_ago: Some(secs(5)), finished_ago: None, }, @@ -329,7 +329,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { // omp has no anchor tier: the primary-screen title tier renders // the conversation label omp announces, with no model prefix. preview: title("Tuning missile damage falloff"), - started_ago: mins(33), + started_ago: Some(mins(33)), quiet_ago: Some(secs(7)), finished_ago: None, }, @@ -346,7 +346,7 @@ fn live_fleet(dirs: &Dirs) -> Vec { // At its prompt: the primary-screen title tier renders the // conversation label omp announces as `π >