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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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.
Expand Down Expand Up @@ -61,9 +61,9 @@ From the project root:
| -- | -- |
| `fleetcom` | Connect to the daemon, autostarting it when necessary, and open the dashboard |
| `fleetcom <session>` | Load a saved session, then open the dashboard |
| `fleetcom --foreground` | Run in-process without a daemon; jobs stop when the client quits |
| `fleetcom --scrollback <lines>` | 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 <lines>` | 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.
Expand All @@ -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

Expand All @@ -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

Expand Down
22 changes: 11 additions & 11 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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`.
12 changes: 7 additions & 5 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
| -- | -- |
| `fleetcom` | Connect to the daemon (autostarting it if needed) and open the dashboard |
| `fleetcom <session>` | 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 <lines>` | 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 |

Expand All @@ -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

Expand Down Expand Up @@ -81,7 +81,7 @@ Tasks retain 2,000 lines of scrollback by default. `--scrollback <lines>` 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

Expand Down Expand Up @@ -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/<rule>` (a recognized agent status line, tagged with the matcher that extracted it).

## Attached

The task owns the terminal, and its status bar reads `[attached] <command> Ctrl-\ background`, or `[attached] <name> · <command>` for a named task. `Ctrl-\` returns to the dashboard; every other key (control chords included) goes to the child.
Expand Down
4 changes: 2 additions & 2 deletions docs/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions docs/sessions.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 <name>`.

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.
5 changes: 4 additions & 1 deletion src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,10 @@ fn connect_or_autostart_in(dir: &Path) -> io::Result<UnixStream> {
}
Err(io::Error::new(
ErrorKind::TimedOut,
"daemon did not come up",
format!(
"daemon did not come up; check {}",
dir.join("daemon.log").display()
),
))
}

Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ fleetcom - a fleet-view supervisor for arbitrary shell commands
Usage:
fleetcom [<session>] connect to the daemon (autostarting it),
optionally loading a saved session
fleetcom --foreground [<session>] run without a daemon; jobs die on quit
fleetcom --kill kill the daemon and every job it owns
fleetcom --foreground [<session>] 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)

Expand Down
Loading
Loading