mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
fix(tmux): gate the pane-exit read, and mute the dot on the rich rail too
Four changes the maintainer asked for on Ark0N/Codeman#446 before merging. The pane-exit watcher stays always-on, but a tick now costs nothing when there is nothing to observe. `hasObservablePaneSession()` skips the tmux exec while every session on the manager is one of the shapes `Session.paneExitApplies` already forces to UNKNOWN: a remote SSH session (its local pane holds the ssh client), a docker case (a `docker exec` into the container's own tmux), and a record rebuilt from the socket (no provenance at all). The timer is untouched. Skipping retracts nothing, for the same reason a failed read does not: the map still holds the last real reading, and every path that puts a new command in a pane calls `clearPaneExit()` itself. The two copies of that rule are pinned against each other in `test/session-pane-exit.test.ts`, because drift between them is silent in both directions. `DEFAULT_PANE_EXIT_INTERVAL_MS` was already a constant beside the stats and remote-reconnect intervals; its comment now says why the watcher owns its own cadence and why the number is what it is. The never-default-an-absent-status rule is written where `PaneExit` is declared. It names `status ?? 0` as the thing never to write, and says that an agent the OOM killer took would otherwise read as a user typing `/exit` — which is what absent-stays-absent keeps a later clean-exit sweep away from. Nothing fails when somebody adds that `??`, which is why the sentence is there rather than a test. Checking the dot's specificity found a second fight, and it was losing. On the tab strip the alert rules win as intended: a session that exits with a permission dialog pending still renders red, and yellow for an idle alert. On the rich vertical tab rail they did not — that rail's own `tab-state-*` dot rules are (0,9,1) against the strip's mute at (0,5,0), so an exited session there kept a full green dot AND the working halo beside a badge reading "exited". The rail twin matches that specificity exactly and therefore must stay below those rules in source order; it clears the halo as well, which the strip's rule never had to think about. `test/session-pane-exit-ui.test.ts` now resolves the real stylesheet in jsdom rather than matching selector text: postcss collects every rule that paints `.tab-status`, a real engine decides, and the tests read back the answer. Two mutations were run against it to prove it has teeth — dropping the hand-written alert exclusions fails three cases, and moving the rail twin above the state rules fails one. Refs Ark0N/Codeman#446. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c67c130caa
commit
90fd0a5a15
+43
-1
@@ -153,7 +153,13 @@ const GRACEFUL_SHUTDOWN_WAIT_MS = 100;
|
||||
/** Default stats collection interval (2 seconds) */
|
||||
const DEFAULT_STATS_INTERVAL_MS = 2000;
|
||||
|
||||
/** How often the pane-exit watcher re-reads every pane on the socket. */
|
||||
/**
|
||||
* How often the pane-exit watcher re-reads every pane on the socket. The
|
||||
* watcher owns this cadence: it does NOT ride `startStatsCollection()`, whose
|
||||
* lifetime a browser panel controls (see {@link TmuxManager.startPaneExitWatcher}).
|
||||
* Matched to the stats cadence above because both cost one batched tmux read,
|
||||
* and kept well under EXEC_TIMEOUT_MS so a normal read finishes inside a tick.
|
||||
*/
|
||||
const DEFAULT_PANE_EXIT_INTERVAL_MS = 2000;
|
||||
|
||||
/** Default remote-reconnect watcher poll interval (5 seconds) — COD-108 */
|
||||
@@ -359,6 +365,33 @@ export function derivePaneExits(rows: PaneRow[], now: number): Map<string, PaneE
|
||||
return exits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Could any of these tmux sessions ever produce a pane-exit answer? Exported
|
||||
* for unit testing.
|
||||
*
|
||||
* Mirrors `Session.paneExitApplies`, which is where the rule is enforced. A
|
||||
* remote session's local pane holds the ssh client, a docker case's holds a
|
||||
* `docker exec` into the container's own tmux, and a record rebuilt from the
|
||||
* socket carries no provenance at all, so the session end forces all three to
|
||||
* UNKNOWN whatever tmux reports. A tick that sees only those has nothing to
|
||||
* learn, and `refreshPaneExits()` skips its tmux read rather than paying for
|
||||
* the answer.
|
||||
*
|
||||
* ⚠ This gates the READ, never the watcher. The watcher is always-on by
|
||||
* design (see {@link TmuxManager.startPaneExitWatcher}), so it keeps ticking
|
||||
* with nothing to observe and picks the read straight back up as soon as one
|
||||
* local session exists.
|
||||
*/
|
||||
export function hasObservablePaneSession(sessions: Iterable<MuxSession>): boolean {
|
||||
for (const session of sessions) {
|
||||
if (session.remote) continue;
|
||||
if (session.docker) continue;
|
||||
if (session.discovered === true) continue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a target pane id from `tmux list-panes -F '#{pane_id}:#{pane_active}'`.
|
||||
* Prefers the active pane and falls back to the first valid pane.
|
||||
@@ -3055,9 +3088,18 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
* Two guards keep a slow read from undoing a fast one. A read already in
|
||||
* flight suppresses the next poll, and a read that started before a
|
||||
* {@link clearPaneExit} is discarded when it lands.
|
||||
*
|
||||
* A third guard skips the read entirely while no session on this manager
|
||||
* could produce an answer ({@link hasObservablePaneSession}). Skipping
|
||||
* retracts nothing, for the same reason a failed read does not: the map
|
||||
* still holds what the last real read saw, and every path that puts a new
|
||||
* command in a pane calls {@link clearPaneExit} itself.
|
||||
*/
|
||||
async refreshPaneExits(now: number = Date.now()): Promise<void> {
|
||||
if (IS_TEST_MODE) return;
|
||||
// Nothing on this socket could answer, so do not exec tmux to find that
|
||||
// out. See `hasObservablePaneSession`: the watcher above still ticks.
|
||||
if (!hasObservablePaneSession(this.sessions.values())) return;
|
||||
if (this.paneExitReadInFlight) return;
|
||||
|
||||
const generation = this.paneExitGeneration;
|
||||
|
||||
Reference in New Issue
Block a user