From 8dc8d4843733c96ad290e978ef8e19a9d5a04a2b Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Mon, 20 Jul 2026 15:34:24 -0700 Subject: [PATCH 1/2] fix: update documentation for clarity and consistency; replace 'jobs' with 'tasks' and refine descriptions across multiple files --- README.md | 18 +++++++++--------- docs/README.md | 22 +++++++++++----------- docs/commands.md | 12 +++++++----- docs/how-it-works.md | 4 ++-- docs/sessions.md | 4 ++-- src/daemon.rs | 5 ++++- src/main.rs | 4 ++-- src/supervisor.rs | 4 ++-- src/ui.rs | 15 ++++++--------- 9 files changed, 45 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index c2cdb56..cc4f345 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Press `Space` for a quick peek at a task’s live screen without attaching to it ### Attach and interact -Press `Enter` to take control of a task, then `^\` to return to the fleet without interrupting it. +Press `Enter` to take control of a task, then `Ctrl-\` to return to the dashboard without interrupting it. ![`fleetcom` attach](docs/img/attach.png) @@ -27,7 +27,7 @@ Press `Enter` to take control of a task, then `^\` to return to the fleet withou Running several long-lived commands is pesky once they span terminal panes or need to survive a disconnect. `fleetcom`: - Runs each command in its own PTY and groups tasks by state, working directory, or named group. -- Delegates jobs to a daemon, so a disconnecting client stops nothing. +- Delegates tasks to a daemon, so a disconnecting client stops nothing. - Saves and reloads task recipes: directories, commands, group assignments, and display names. - Reruns a completed task in place, keeping its identity, group, and name. - Preserves `claude`, `codex`, and `grok` conversations, so saved or rerun tasks resume instead of starting fresh. @@ -61,9 +61,9 @@ From the project root: | -- | -- | | `fleetcom` | Connect to the daemon, autostarting it when necessary, and open the dashboard | | `fleetcom ` | Load a saved session, then open the dashboard | -| `fleetcom --foreground` | Run in-process without a daemon; jobs stop when the client quits | -| `fleetcom --scrollback ` | Set per-task scrollback (default 2000); read at supervisor start, so a running daemon keeps its value until `--kill` | -| `fleetcom --kill` | Stop the daemon and every job it owns | +| `fleetcom --foreground` | Run in-process without a daemon; tasks stop when the client quits | +| `fleetcom --scrollback ` | Set per-task scrollback (default 2,000); read at supervisor start, so a running daemon keeps its value until `--kill` | +| `fleetcom --kill` | Stop the daemon and every task it owns | | `fleetcom --help` / `--version` | Print usage or version information and exit | The first ordinary invocation starts the daemon when necessary. `--daemon` is an internal mode. @@ -77,10 +77,10 @@ The first ordinary invocation starts the daemon when necessary. `--daemon` is an | ↑ ↓ / `k` `j` | move the selection | | `Enter` | attach to the selected task | | `Space` | peek at the selected task | -| `n` | new command in the current directory | +| `n` | new command in the invocation directory | | `@` | new command in a directory you pick (with completion) | -| `q` | disconnect; leave the daemon and jobs running | -| `Q` | quit; kill the jobs and stop the daemon | +| `q` | disconnect; leave the daemon and tasks running | +| `Q` | quit; kill the tasks and stop the daemon | ### Attached @@ -93,7 +93,7 @@ The first ordinary invocation starts the daemon when necessary. `--daemon` is an ## How it works -Every task runs in its own pseudo-terminal, emulated with `alacritty_terminal`. The dashboard preview, peek overlay, and attached view all read the same emulated screen grid, so full-screen programs such as `vim` and `htop` retain one consistent terminal state across views. [`docs/how-it-works.md`](docs/how-it-works.md) documents the terminal emulation, input routing, and activity windows. +Every task runs in its own pseudo-terminal, emulated with `alacritty_terminal`. The dashboard preview, peek overlay, and attached view all read the same emulated screen grid, so full-screen programs such as `vim` and `htop` retain one consistent terminal state across views. [`docs/how-it-works.md`](docs/how-it-works.md) documents the terminal emulation, input routing, and activity grouping. ## Scope and tradeoffs diff --git a/docs/README.md b/docs/README.md index b1cc6e8..aa7a54d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ ## Index - [Commands](commands.md): every key and launch flag, including the routing mechanics -- [How it works](how-it-works.md): the PTY emulation, input routing, and activity windows +- [How it works](how-it-works.md): the PTY emulation, input routing, and activity grouping - [Sessions](sessions.md): the task recipe format and where it lives - [Agent session resume](../src/harness/agent-resume.md): when `fleetcom` can save resumable `claude`, `codex`, and `grok` commands - [Storage paths](#storage-paths): the socket, the lock, and the session paths @@ -62,7 +62,7 @@ Run `fleetcom`. The first invocation starts the daemon and opens an empty dashbo fleetcom 0 running · 0 idle · 0 done by state · dir · custom ❯ n run · @ dir · s sort · w save · o load - ↑↓ select · enter attach · space peek · n/@ new · s sort · m tag · g group · R rename · r rerun · X kill · q detach · Q quit + ↑↓ select · enter attach · space peek · m tag · g group · R rename · r rerun · X kill · q detach · Q quit ``` Press `n`, enter a command, and press `Enter`. The command runs in its own PTY and appears under Running. Repeat the process for a second command: @@ -75,7 +75,7 @@ Press `n`, enter a command, and press `Enter`. The command runs in its own PTY a ✻ npm run dev VITE v5.0 ready in 312 ms 4s ❯ n run · @ dir · s sort · w save · o load - ↑↓ select · enter attach · space peek · n/@ new · s sort · m tag · g group · R rename · r rerun · X kill · q detach · Q quit + ↑↓ select · enter attach · space peek · m tag · g group · R rename · r rerun · X kill · q detach · Q quit ``` Each row is `glyph · tag · command · latest output · age`. The age counts from the task's last meaningful edge: launch while running, last output once idle, exit once completed. `Space` peeks: a read-only box of the selected task's live screen, without leaving the dashboard: @@ -85,7 +85,7 @@ Each row is `glyph · tag · command · latest output · age`. The age counts fr │ running 3 tests │ │ test result: ok. 42 passed; 0 failed │ │ │ - └ space/esc close · enter attach ─────────────────────┘ + └ space/esc close · enter attach · preview: floor ────┘ ``` `Enter` attaches to the task. Keystrokes then go to its PTY, except for the reserved background chord shown in the status bar: @@ -134,13 +134,13 @@ In custom mode, a new command inherits the selected task's group. The spawn prom The attached status bar shows both: `[attached] api tests · cargo watch -x test`. Names are daemon state, survive detach and rerun, and persist in saved [sessions](sessions.md). -`w`, a name, and `Enter` save the fleet as a [session](sessions.md). `q` then disconnects while the daemon and both jobs continue running. A subsequent `fleetcom` invocation reconstructs the dashboard from the daemon's current task state. `Q` or `fleetcom --kill` stops the jobs (`TERM`, then `KILL` after a two-second grace period) and exits the daemon. +`w`, a name, and `Enter` save the fleet as a [session](sessions.md). `q` then disconnects while the daemon and both tasks continue running. A subsequent `fleetcom` invocation reconstructs the dashboard from the daemon's current task state. `Q` or `fleetcom --kill` stops the tasks (`TERM`, then `KILL` after a two-second grace period) and exits the daemon. ## Operational constraints ### The fleet dies with the daemon -Because the daemon holds each PTY master, daemon termination closes the terminals and the kernel sends `SIGHUP` to every task's process group. A clean shutdown sends `SIGTERM` before `SIGKILL`; a crash or direct `SIGKILL` provides no grace period. HUP-immune jobs (`nohup`, `trap '' HUP`) can survive, but the next daemon neither owns nor displays them. A panic while serving one client only drops that connection. +Because the daemon holds each PTY master, daemon termination closes the terminals and the kernel sends `SIGHUP` to every task's process group. A clean shutdown sends `SIGTERM` before `SIGKILL`; a crash or direct `SIGKILL` provides no grace period. HUP-immune processes (`nohup`, `trap '' HUP`) can survive, but the next daemon neither owns nor displays them. A panic while serving one client only drops that connection. ### Commands run through the client's non-interactive shell @@ -164,20 +164,20 @@ Each task's terminal keeps a scrollback history whose depth is resolved once, wh ### Client and daemon protocol versions must match -The daemon rejects a mismatch during the handshake. Stop an incompatible daemon with `fleetcom --kill`, which also terminates every running job, then start a new client +The daemon rejects a mismatch during the handshake. Stop an incompatible daemon with `fleetcom --kill`, which also terminates every running task, then start a new client. ### The daemon serves one client at a time -A second `fleetcom` prints a waiting notice, then attaches when the active client disconnects (`q`). `Ctrl-C` while waiting aborts without touching the daemon +A second `fleetcom` prints a waiting notice, then attaches when the active client disconnects (`q`). `Ctrl-C` while waiting aborts without touching the daemon. ### Shutdown is graceful-first -`X`, `Q`, `--kill`, and daemon shutdown signals send `SIGTERM` to each job's *process group*, then escalate to `SIGKILL` after two seconds. Removing one task (`X`) keeps an exited leader unreaped through escalation, reserving the process-group ID so background children remain signalable. During full shutdown (`Q`/`--kill`), checking whether a group is empty reaps its exited leader. A `TERM`-ignoring member that outlives the leader can then become unsafe to signal by group ID and survive daemon shutdown. A child created by `cmd &` in a non-interactive shell normally remains in its parent's group. A process that calls `setsid` or otherwise leaves the group is outside the sweep and must be terminated separately +`X`, `Q`, `--kill`, and daemon shutdown signals send `SIGTERM` to each task's *process group*, then escalate to `SIGKILL` after two seconds. Removing one task (`X`) keeps an exited leader unreaped through escalation, reserving the process-group ID so background children remain signalable. During full shutdown (`Q`/`--kill`), checking whether a group is empty reaps its exited leader. A `TERM`-ignoring member that outlives the leader can then become unsafe to signal by group ID and survive daemon shutdown. A child created by `cmd &` in a non-interactive shell normally remains in its parent's group. A process that calls `setsid` or otherwise leaves the group is outside the sweep and must be terminated separately. ### `--foreground` is ephemeral -It runs the core in-process with no daemon, so the jobs die when you quit and there is nothing to reattach to +It runs the core in-process with no daemon, so the tasks die when you quit and there is nothing to reattach to. ### Signalling the daemon is a clean shutdown -`SIGTERM`/`SIGINT`/`SIGHUP` to the daemon group-kill every job, remove the socket, and exit. This is the same teardown as `Q` or `fleetcom --kill` +`SIGTERM`/`SIGINT`/`SIGHUP` to the daemon group-kill every task, remove the socket, and exit. This is the same teardown as `Q` or `fleetcom --kill`. diff --git a/docs/commands.md b/docs/commands.md index 21e13a6..f6649ae 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -8,9 +8,9 @@ | -- | -- | | `fleetcom` | Connect to the daemon (autostarting it if needed) and open the dashboard | | `fleetcom ` | Load a saved [session](sessions.md) at startup, then open the dashboard | -| `fleetcom --foreground` | Run the core in-process, no daemon; jobs die when you quit | +| `fleetcom --foreground` | Run the core in-process, no daemon; tasks die when you quit | | `fleetcom --scrollback ` | Set per-task scrollback depth (default 2,000, max 100,000); see [Scrollback](#scrollback) | -| `fleetcom --kill` | Stop the daemon and kill every job it owns; works even while another client is attached (it signals the daemon rather than queueing behind the socket) | +| `fleetcom --kill` | Stop the daemon and kill every task it owns; works even while another client is attached (it signals the daemon rather than queueing behind the socket) | | `fleetcom --help` / `-h` | Print usage and exit | | `fleetcom --version` / `-V` | Print the version and exit | @@ -33,8 +33,8 @@ | `X` | Kill a running task (`TERM`, then `KILL` after 2 s), or remove a finished one | | `w` | Save the current tasks as a session | | `o` | Load a saved session | -| `q` (or `Ctrl-C`) | Disconnect; leave the daemon and jobs running | -| `Q` | Quit; kill the jobs and stop the daemon | +| `q` (or `Ctrl-C`) | Disconnect; leave the daemon and tasks running | +| `Q` | Quit; kill the tasks and stop the daemon | ### Status glyphs @@ -81,7 +81,7 @@ Tasks retain 2,000 lines of scrollback by default. `--scrollback ` or the #### Detach vs. quit -From the dashboard, `q` or `Ctrl-C` disconnects the client and leaves the daemon and its jobs running; the next `fleetcom` reattaches. `Q` stops the daemon after sending `TERM` to each job's process group, then `KILL` after a 2 s grace. Processes that have moved to another group are outside this sweep. A `TERM`-ignoring member can also survive if its leader exits during shutdown, because checking group emptiness releases the process-group ID reservation before escalation (see [Shutdown is graceful-first](README.md#shutdown-is-graceful-first)). Outside attached mode, `Ctrl-C` disconnects the client; while attached, it belongs to the child. In prompts and pickers, `Esc` cancels without disconnecting. +From the dashboard, `q` or `Ctrl-C` disconnects the client and leaves the daemon and its tasks running; the next `fleetcom` reattaches. `Q` stops the daemon after sending `TERM` to each task's process group, then `KILL` after a 2 s grace. Processes that have moved to another group are outside this sweep. A `TERM`-ignoring member can also survive if its leader exits during shutdown, because checking group emptiness releases the process-group ID reservation before escalation (see [Shutdown is graceful-first](README.md#shutdown-is-graceful-first)). Outside attached mode, `Ctrl-C` disconnects the client; while attached, it belongs to the child. In prompts and pickers, `Esc` cancels without disconnecting. #### Grouping and tagging @@ -133,6 +133,8 @@ The daemon normalizes every group name received from the picker or a [session](s A centered box over the dashboard showing the selected task's live screen (the last screenful). `↑`/`↓` (or `k`/`j`) switch which task you're peeking at; `Enter` attaches to it; `r` reruns it if it has finished; `Space`, `Esc`, or `q` closes. +The footer's `preview:` segment names the source of the row's dashboard preview: `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 line, tagged with the matcher that extracted it). + ## Attached The task owns the terminal, and its status bar reads `[attached] Ctrl-\ background`, or `[attached] · ` for a named task. `Ctrl-\` returns to the dashboard; every other key (control chords included) goes to the child. diff --git a/docs/how-it-works.md b/docs/how-it-works.md index de3b01d..601ba43 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -8,6 +8,6 @@ Every task runs in its own pseudo-terminal, emulated with `alacritty_terminal`. Attached input follows the terminal modes reported by the child. Modified Enter becomes `ESC CR` when the terminal reports the modifier. Paste receives bracketed-paste markers only when the child enables them. Mouse events go to children that request a mouse protocol. Full-screen children receive alternate-scroll input only while DECSET 1007 is enabled; otherwise `fleetcom` suppresses wheel events. Each task retains 2,000 lines of scrollback. For inline children, wheel-up or `Shift+PageUp` enters history; paging keys navigate it, while `Esc` or ordinary input returns to live output. [`commands.md`](commands.md) documents the exact routing rules. -## Grouping uses two activity windows +## Grouping follows one activity window -The dashboard groups tasks by state (In use / Running / Idle / Completed), working directory, or names assigned with `g`. Activity uses two separate windows. The row glyph changes from `✻` to `∙` after 600 ms without output, while state grouping moves the task to Idle after 10 s. A tool such as `top`, which prints every 1–2 s, can therefore alternate glyphs without moving sections. The glyph reports the short edge; the section reports the debounced state. +The dashboard groups tasks by state (In use / Running / Idle / Completed), working directory, or names assigned with `g`. One 10-second window drives both idle signals: after 10 s without output, the row glyph changes from `✻` to `∙` and the task moves to Idle in the same refresh. A tool such as `top`, which prints every 1–2 s, never crosses the window, so it stays `✻` under Running. The preview text holds separately: a status from a weaker source must persist for 600 ms before it replaces a stronger one, which absorbs repaint flicker without affecting grouping. diff --git a/docs/sessions.md b/docs/sessions.md index 15173d7..8b84f3c 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -1,6 +1,6 @@ # Sessions -A session is a launch recipe, not a process snapshot. It records commands, working directories, and each task's optional group assignment and display name. Loading always starts new processes. Live process continuity belongs to the [daemon](README.md#storage-paths), which keeps jobs running across client disconnects. +A session is a launch recipe, not a process snapshot. It records commands, working directories, and each task's optional group assignment and display name. Loading always starts new processes. Live process continuity belongs to the [daemon](README.md#storage-paths), which keeps tasks running across client disconnects. ## Storage @@ -68,4 +68,4 @@ A bare agent command does not identify its conversation, so saving it verbatim w - Load in-app: `o`, pick from the list, `Enter`. - Load at launch: `fleetcom `. -Loading always spawns new processes from the stored commands. Existing jobs remain daemon state and never enter the session file. [Agent session resume](../src/harness/agent-resume.md) documents when supported agent commands can preserve their conversations across that relaunch. +Loading always spawns new processes from the stored commands. Existing tasks remain daemon state and never enter the session file. [Agent session resume](../src/harness/agent-resume.md) documents when supported agent commands can preserve their conversations across that relaunch. diff --git a/src/daemon.rs b/src/daemon.rs index 90f6e25..a49dcc1 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -299,7 +299,10 @@ fn connect_or_autostart_in(dir: &Path) -> io::Result { } Err(io::Error::new( ErrorKind::TimedOut, - "daemon did not come up", + format!( + "daemon did not come up; check {}", + dir.join("daemon.log").display() + ), )) } diff --git a/src/main.rs b/src/main.rs index 76c0ea0..f73f7b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,8 +71,8 @@ fleetcom - a fleet-view supervisor for arbitrary shell commands Usage: fleetcom [] connect to the daemon (autostarting it), optionally loading a saved session - fleetcom --foreground [] run without a daemon; jobs die on quit - fleetcom --kill kill the daemon and every job it owns + fleetcom --foreground [] run without a daemon; tasks die on quit + fleetcom --kill kill the daemon and every task it owns fleetcom --daemon run the daemon (internal; the first fleetcom starts it automatically) diff --git a/src/supervisor.rs b/src/supervisor.rs index 69f7c48..bcf72fc 100644 --- a/src/supervisor.rs +++ b/src/supervisor.rs @@ -674,14 +674,14 @@ impl Supervisor { /// the best-known ID. fn rerun(&mut self, id: u64) { let Some(i) = self.index_of(id) else { - self.status(format!("rerun: no task {id}")); + self.status(format!("rerun failed: no task {id}")); return; }; // Latch a recent exit and scrape its drained terminal before choosing // the rerun command. scrape_now(&mut self.tasks[i]); if self.tasks[i].finished.is_none() { - self.status("rerun: task is still running"); + self.status("rerun failed: task is still running"); return; } let Some(launch) = self.launch_or_refuse() else { diff --git a/src/ui.rs b/src/ui.rs index f25e90d..86d2b6d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -638,18 +638,15 @@ fn render_disconnected(out: &mut impl Write, app: &App) -> io::Result<()> { put(out, y, "", cols)?; } - let hint = if app.daemon_backed { - "r reconnect q quit" + // A `--foreground` core has no daemon: naming one on this screen would + // contradict the mode, and there is no external process to reconnect to. + let (title, hint) = if app.daemon_backed { + ("⚠ daemon connection lost", "r reconnect q quit") } else { - "core stopped q quit" + ("⚠ core stopped", "q quit") }; let mid = rows / 2; - put( - out, - mid.saturating_sub(1), - ¢er("⚠ daemon connection lost", cols), - cols, - )?; + put(out, mid.saturating_sub(1), ¢er(title, cols), cols)?; dim(out, mid + 1, ¢er(hint, cols), cols)?; Ok(()) } From 7060a39610b9c9e3bed86faf3d9f83b3607a258f Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Mon, 20 Jul 2026 15:36:44 -0700 Subject: [PATCH 2/2] fix: refine version replace command --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb6d6aa..2cd47ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - run: cargo test --verbose - run: | export VERSION=${{ github.event.release.tag_name }} - sed -i "s/0.0.0/$VERSION/g" Cargo.toml + sed -i "s/^version = \"0.0.0\"$/version = \"$VERSION\"/" Cargo.toml cargo publish --allow-dirty env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}