feat(ui): ports, pids and ahead-counts on the surfaces that lacked them #107

Merged
lz merged 1 commit from feat/ports-and-probes into main 2026-09-04 17:25:08 +02:00
Owner

Seventh and last in the #96 stack — targets #106, not main. Closes the remaining mock-fidelity gaps.

Two facts folded into an exec that already runs

checkSessionHealth already pays one docker exec per session per 10s tick for the claude walk. The pid is already in hand inside that loop, and the git ahead-count is a local command in the same shell — so both ride along. One exec, not three. I had previously argued against this on cost; I was pricing it as three separate execs, which was wrong.

claude's version is per-container and only changes at boot, so it lives on worker health, probed once and keyed on container id (a recreate re-probes), evicted at removeWorker.

Verified against a real /proc, not just parsed:

cwd with a live claude   ->  pid 24724 · ahead 0
worktree, no upstream    ->  ahead 27          (the origin/HEAD fallback)
nonexistent cwd          ->  (nothing)         both facts absent

"ahead 0" and "could not measure" are distinct at the wire level — level emits the literal line, unmeasurable emits nothing, which the parser turns into null. The chip is absent when unknown; it never asserts ahead 0 for a branch nothing could count.

The self-match negative control matters here, because this is the bug class that survived 1092 unit tests and needed dind to find. Pointed at its own shell's cwd:

NEW shape:  (no pid line — immune at the argv0 gate)
OLD shape:  OLD MATCHED pid 674623      ← the shipped bug, reproducing live

Surfaces

  • Hover card — ports row (session: per-port chips; workspace: total plus how many await approval, the only thing on the card that says an agent is blocked). The agent row now shows the live probe rather than the row's status, plus claude <version> · pid <n>, and an ahead N chip beside the branch. The comment that said a live probe was impossible here is gone — #106 made it false.
  • Session settings — ports inline: :5173 → host:41703 with an approved/pending chip, Copy, and an inline approve for pending, keeping "Open ports panel →" as the last row. Copy button on the worktree path.
  • Workspace settings — provider chip beside Repository, resolved from connection_id through a small store, degrading to no chip when unknown rather than to a guess.

Browser-verified end to end: :3001 awaiting approval [approve] → click → :3001 172.30.0.4:3001 approved [Copy], with the server agreeing (p2 :3001 approved host=3001).

Gates

119 files / 1164 tests passed
typecheck  4895 FILES  0 ERRORS  0 WARNINGS
lint       clean

Not in this PR

Editable session name / workspace label. The spec asks for both and the mock shows inputs; they are deliberately deferred past the release candidate so the trial exercises what already exists. Note the recorded reason for deferring the label is now stale: it was that the label is baked into every running claude's --remote-control argv, but #100 replaced the argv-grep health probe with a CWD match, so remoteControlId is used only at launch. Session-name rename is still genuinely hard — it is simultaneously the tmux window name, the git branch and the worktree directory.

Seventh and last in the #96 stack — **targets #106**, not `main`. Closes the remaining mock-fidelity gaps. ## Two facts folded into an exec that already runs `checkSessionHealth` already pays one `docker exec` per session per 10s tick for the claude walk. The **pid** is already in hand inside that loop, and the **git ahead-count** is a local command in the same shell — so both ride along. One exec, not three. I had previously argued against this on cost; I was pricing it as three separate execs, which was wrong. claude's **version** is per-container and only changes at boot, so it lives on *worker* health, probed once and keyed on container id (a recreate re-probes), evicted at `removeWorker`. Verified against a real `/proc`, not just parsed: ``` cwd with a live claude -> pid 24724 · ahead 0 worktree, no upstream -> ahead 27 (the origin/HEAD fallback) nonexistent cwd -> (nothing) both facts absent ``` **"ahead 0" and "could not measure" are distinct at the wire level** — level emits the literal line, unmeasurable emits nothing, which the parser turns into `null`. The chip is absent when unknown; it never asserts `ahead 0` for a branch nothing could count. The self-match negative control matters here, because this is the bug class that survived 1092 unit tests and needed dind to find. Pointed at its own shell's cwd: ``` NEW shape: (no pid line — immune at the argv0 gate) OLD shape: OLD MATCHED pid 674623 ← the shipped bug, reproducing live ``` ## Surfaces - **Hover card** — ports row (session: per-port chips; workspace: total plus how many await approval, the only thing on the card that says an agent is *blocked*). The agent row now shows the **live probe** rather than the row's status, plus `claude <version> · pid <n>`, and an `ahead N` chip beside the branch. The comment that said a live probe was impossible here is gone — #106 made it false. - **Session settings** — ports inline: `:5173 → host:41703` with an approved/pending chip, Copy, and an inline **approve** for pending, keeping "Open ports panel →" as the last row. Copy button on the worktree path. - **Workspace settings** — provider chip beside Repository, resolved from `connection_id` through a small store, degrading to no chip when unknown rather than to a guess. Browser-verified end to end: `:3001 awaiting approval [approve]` → click → `:3001 172.30.0.4:3001 approved [Copy]`, with the server agreeing (`p2 :3001 approved host=3001`). ## Gates ``` 119 files / 1164 tests passed typecheck 4895 FILES 0 ERRORS 0 WARNINGS lint clean ``` ## Not in this PR Editable session name / workspace label. The spec asks for both and the mock shows inputs; they are deliberately deferred past the release candidate so the trial exercises what already exists. Note the recorded reason for deferring the **label** is now stale: it was that the label is baked into every running claude's `--remote-control` argv, but #100 replaced the argv-grep health probe with a CWD match, so `remoteControlId` is used only at launch. Session-*name* rename is still genuinely hard — it is simultaneously the tmux window name, the git branch and the worktree directory.
feat(ui): ports, pids and ahead-counts on the surfaces that lacked them
All checks were successful
ci / nexus (pull_request) Successful in 6m51s
ci / images (pull_request) Successful in 8m17s
pr-image-cleanup / delete-pr-images (pull_request) Successful in 11s
204fbbfa5a
The per-session health poll already paid one docker exec per session per
tick to ask "is claude up". Two more facts ride inside that same script
rather than adding execs: the pid of the matched process (the walk
already has it — the directory it read the cmdline from), and how far
the worktree's branch is ahead.

The ahead-count has two sources and no third: the branch's own upstream,
then origin/HEAD for a branch that has never been pushed, which is most
session branches. When neither resolves the answer is ABSENT, not zero —
a branch level with its upstream and one we failed to measure are
different facts, so only the measured one gets a chip.

claude's version is NOT on that poll. It is fixed for the life of a
container (the entrypoint's boot-time update writes it), so it sits on
worker health behind a per-container cache and costs one exec per
container instead of one per session per 10s.

probeClaudeCwd is untouched and is not reimplemented on top of the new
probe: its other callers include the 100ms waitForClaude loop, which
must not run git. The new script keeps that function's process walk
byte-for-byte, including the shape that makes it immune to matching its
own /bin/sh — verified by running both shapes through `sh -c` here, where
the old argv-grep form matches its own cmdline and the new one does not.

Then the three surfaces that could not show any of it:

- The rail's hover card gets a ports row (a session names each port, a
  workspace states the total and how many are waiting on the operator —
  the only thing on that card that says an agent is blocked), an ahead
  chip, and a live agent row. Its comment saying a live probe was
  impossible here is now false and is gone.
- The session settings pane lists its ports inline with an approve
  button, instead of only linking to the ports panel; the branch gets
  the ahead chip and the agent status gets `claude <v> · pid <n>`.
- The workspace pane resolves its connection to a provider chip.

Three copies of "write to the clipboard and toast" become one helper,
and the preview-URL rule (advertise host vs the operator's own hostname)
stops being a second copy in the settings pane.
lz changed target branch from feat/store-consolidation to main 2026-09-04 17:25:00 +02:00
lz merged commit 29f80fc1be into main 2026-09-04 17:25:08 +02:00
Sign in to join this conversation.
No reviewers
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!107
No description provided.