cargo-port 0.1.2

A TUI for inspecting and managing Rust projects
# Running list: CPU smoothing + sub-process outline

Status: complete 2026-06-04 — all four phases shipped
Authored: 2026-06-03 — follow-on to `docs/sub_panes_and_running.md` (complete)
Revised: 2026-06-04 — Phase 4 widened the outline from same-target wrapper
nesting to **full descendant subtrees**: every process a tracked instance
spawns (tracked or not — `cargo`, `rustc`, wrappers) joins the outline.

## Goal

Two refinements to the Running sub-pane, both driven by watching `cargo mend`
runs in the live list:

1. **Steady CPU readings.** The CPU column oscillates (4% ↔ 23% for
   cargo-port) because both the sampler and the sampled process run ~1 s
   cycles: `process.cpu_usage()` is the average over the window between two
   poller refreshes, and how much of a work burst lands inside a given window
   varies with phase alignment. Show a moving average over the last N polls
   instead of the instantaneous sample.
2. **Sub-process outline.** One `cargo mend` run is several OS processes (an
   orchestrator plus one `RUSTC_WORKSPACE_WRAPPER` re-invocation per workspace
   target, joined through an untracked `cargo` intermediate). Today they render
   as N identical sibling rows. Nest children under their parent with the
   project list's expand/collapse idiom, collapsed by default, with the
   collapsed parent row showing the subtree's aggregate CPU/MEM.

## Data sources

- **ppid.** sysinfo populates `Process::parent()` from `libc::proc_bsdinfo`
  during the base per-process fetch — the same call that provides
  `start_time()` for the kill guard. It is present on every refresh regardless
  of `ProcessRefreshKind` flags, so the tree costs no new syscalls.
- **Start-time ordering as the reuse guard.** A parent precedes its child, so
  any hop in the ancestor walk whose parent started *after* its child is a
  reused PID — treat the row as top-level.

## Target behaviour

### CPU column

The CPU cell shows the mean of the instance's last `N = 5` poll samples (at
the 1 s poll cadence: the average over the last 5 s). A new instance averages
over however many samples exist, so its first reading is the raw sample, not
a zero-diluted mean. Memory stays instantaneous — it is a level, not a rate,
and does not alias.

### Outline

*(As revised by Phase 4.)* The outline shows every process a tracked
instance spawned. A process joins when its ancestor chain — walked through
`parent()` links, start-time-validated per hop against PID reuse,
depth-capped — reaches a tracked instance; its outline parent is its direct
OS parent. Top level is what was started independently. The `cargo` group's
count-prefix invariant holds because segment membership follows the subtree
root: installed roots sort first and their subtrees stay contiguous.

Rows render in tree order — children directly under their parent,
depth-indented with a `└` marker:

```
├─ Running (6) ──────────────────────────────────────────────────────┤
│ Target          Profile  PID     CPU   MEM       Path              │
│ ▼ cargo (6)                                                        │
│ cargo-port      cargo     4821    6%   312 MiB   ~/rust/cargo-port │
│ ▶ cargo-mend (4) cargo    5120   64%   2.1 GiB   ~/rust/cargo-mend │
│ my_app          debug     6233   12%   201 MiB   ~/rust/my_app     │
```

Expanded (`▼` on the parent), the parent row shows its **own** metrics and
each child shows its own:

```
│ ▼ cargo-mend (4) cargo    5120    2%   90 MiB    ~/rust/cargo-mend │
│   └ cargo-mend   cargo    5121   34%   801 MiB   ~/rust/cargo-mend │
│   └ cargo-mend   cargo    5122   28%   773 MiB   ~/rust/cargo-mend │
```

Behaviour rules:

- **Collapsed by default**, like the `cargo` group. Collapsed parent rows show
  the subtree-aggregate CPU/MEM and a `(N)` child count; expanded parents show
  their own metrics.
- **Toggle idiom matches the `cargo` group**: `Enter` on a parent row toggles;
  `Right` expands a collapsed parent; `Left` collapses — directly on the
  parent, or from a child row by handing the highlight to the parent.
- **Kill stays per-instance.** `K` on a parent row (collapsed or not) kills
  that one PID; hidden children are not killed implicitly.
- **The divider count is all instances**, hidden or not — same convention as
  the collapsed `cargo` group.
- **Orphaning is visible by design.** When a parent exits while children
  live, the OS reparents them to launchd: a tracked orphan pops out to top
  level; an untracked orphan's chain no longer reaches a tracked instance,
  so its row leaves the outline on the next poll.
- **Cross-target nesting is in** *(Phase 4 revision — previously deferred)*:
  a tracked target spawned by another tracked target nests under it; top
  level means started independently.

## Data model

- `RunningTargetsPoller` gains `cpu_history: HashMap<u32, VecDeque<f32>>`
  (capped at `CPU_SMOOTHING_WINDOW_POLLS = 5`), maintained exactly like
  `first_seen`: fed during the poll loop, retained against live PIDs, evicted
  by `drop_instances`.
- `RunningInstance` gains `parent_pid: Option<u32>` — the direct OS parent
  when the instance descends from another tracked instance (Phase 4
  semantics; Phases 2–3 shipped a same-key, same-profile restriction since
  deleted), resolved after the snapshot rebuild from the sysinfo table the
  poller already holds. The walk is a pure function over a
  `pid -> (parent, start_time)` lookup so tests fixture it with a map
  instead of a live process table.
- `RunningRow` gains `parent_pid` and a tree `depth`; `build_running_rows`
  reorders its sorted output into tree order (children after their parent,
  depth-first). Top-level rows keep today's ordering exactly — a row whose
  parent is absent from the list is top-level.
- `TargetsPane` gains `expanded_parents: HashSet<u32>` (empty = all
  collapsed); `build_running_list` skips descendants of collapsed parents.
  Stale PIDs are retained out each frame so a reused PID starts collapsed.

## Phases

Each phase ends with `cargo build && cargo +nightly fmt`, `cargo nextest run`,
`cargo mend`, and `cargo install --path .`.

### Phase 1 — CPU moving average ✅ complete (2026-06-03)

Poller-only: the history map, the mean fed into `RunningInstance.cpu_percent`,
retention + eviction, unit tests (mean, window cap, eviction). No render
change — the CPU cell already formats `cpu_percent`.

Shipped in `src/tui/running_targets.rs`: `CPU_SMOOTHING_WINDOW_POLLS`,
`cpu_history` on the poller, `smoothed_cpu` (a free function so the tick
loop's disjoint field borrows hold). The installed-bin branch feeds one
sample per OS process before the multi-project attribution loop.

### Phase 2 — parent resolution + tree-ordered rows ✅ complete (2026-06-03)

`parent_pid` on instance and row, the ancestor walk (pure, fixtured tests:
through-an-intermediate, reuse-rejection via start-time, depth cap,
self-parent), tree ordering with `depth`, and the indented `└` rendering.
Children are always visible in this phase — the outline ships expanded.

Shipped: `ParentLink` + `nearest_tracked_ancestor` + `resolve_parent_links`
(`running_targets.rs`), `tree_ordered`/`append_subtree`/`indented_name`
(`running_subpane.rs`). `tree_ordered` drains stranded rows defensively even
though the start-time-validated walk makes a parent-link cycle unreachable.

### Phase 3 — expand/collapse + aggregate metrics ✅ complete (2026-06-03)

`expanded_parents` on `TargetsPane`, collapsed-by-default list filtering, the
`▶/▼ name (N)` parent row with subtree-aggregate CPU/MEM while collapsed, the
three toggle paths (`Enter`/`Right`/`Left`) wired beside the `cargo` group's
in `actions.rs`, cursor handoff on collapse-from-child, and the PID-anchor
interaction (an anchor hidden by a collapse falls to the parent row).

Shipped: `visible_indices`/`outline_subtree_len`/`outline_name`/
`displayed_metrics` (`running_subpane.rs`), the `expanded_parents` accessors
on `TargetsPane`, and `toggle_running_parent`/`expand_running_parent`/
`collapse_running_parent` (`actions.rs`). Deviations from the plan text:

- The `(N)` subtree count renders in **both** states (the expanded mock above
  already showed it), counting the whole subtree, not direct children only.
- `Left` runs innermost-first: outline parent before the `cargo` group, so a
  wrapper row collapses its orchestrator before a second `Left` folds the
  group.
- `TargetsPane::new()` lost `const``HashSet::new()` is not const; the only
  caller (`Panes::new`) is non-const.
- No anchor self-heal case remained: expansion state changes only through the
  toggle paths, and the collapse-from-child path hands the highlight (and
  anchor) to the parent explicitly.

### Phase 4 — full descendant subtrees ✅ complete (2026-06-04)

Phases 2–3 nested only same-target, same-profile wrappers; the revision shows
**everything a tracked instance spawned**. The model becomes uniform:

- **Shown set** = tracked instances ∪ every process whose ancestor chain
  reaches one. Top level is what was started independently; a tracked target
  spawned *by* another tracked target nests under it (the lint-runtime's
  `cargo mend` runs nest under cargo-port; a terminal-launched `cargo mend`
  is its own top-level root).
- **Outline parent = direct OS parent.** "Descendant of tracked" is closed
  upward, so a descendant's direct parent is always itself shown — no
  nearest-shown walk needed beyond the reaches-tracked membership check
  (`shown_parent` = `nearest_tracked_ancestor(pid)?` then `links(pid).parent`).
  The same-profile restriction is deleted.
- **`ChildProcess`** (`running_targets.rs`): name (exe file name, free with
  our `with_exe` refresh), smoothed CPU, memory, `first_seen`, `create_time`,
  direct parent. Collected in a second tick pass over the process table;
  `first_seen`/`cpu_history` retention and `drop_instances` cover child PIDs.
- **`RunningRowKind`** (`running_subpane.rs`): `Target { profile,
  display_path }` or `Child` — child rows render blank Profile/Path cells,
  and the kill confirm labels them `name (process)`. `K` on any row kills
  that one PID; hidden children are never killed implicitly.
- **`cargo` segment membership moved to the subtree root**
  (`cargo_segment_len`): the header folds installed roots *with everything
  they spawned*, and its count is the folded row count, not the installed
  instance count. The divider's `Running (N)` likewise counts all shown rows.
- Collapsed-by-default now does the heavy lifting: during a mend run the
  whole compile reads as one `▶ cargo-port (N)` row whose aggregate CPU/MEM
  is the entire subtree.
- Orphaning stays visible by design: when an intermediate (`cargo`) exits,
  the OS reparents its children to launchd, their chains stop reaching a
  tracked PID, and the rows leave the outline on the next poll.