mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +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
@@ -16,6 +16,7 @@ import {
|
||||
formatPaneSnapshot,
|
||||
parsePaneRows,
|
||||
derivePaneExits,
|
||||
hasObservablePaneSession,
|
||||
resolveActivePaneTarget,
|
||||
} from '../src/tmux-manager.js';
|
||||
import { execSync, exec } from 'node:child_process';
|
||||
@@ -1120,3 +1121,57 @@ describe('TmuxManager pane-exit bookkeeping', () => {
|
||||
expect(manager.getPaneExit('codeman-aaaa')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasObservablePaneSession', () => {
|
||||
// The pane-exit watcher is always-on, so a tick with nothing to observe is
|
||||
// the normal case on an instance running only remote or Docker work. This
|
||||
// predicate is what keeps that tick from exec'ing tmux to find out.
|
||||
const base = {
|
||||
sessionId: 's1',
|
||||
muxName: 'codeman-aaaa',
|
||||
pid: 100,
|
||||
createdAt: 0,
|
||||
workingDir: '/tmp',
|
||||
mode: 'claude' as const,
|
||||
attached: true,
|
||||
};
|
||||
|
||||
it('says no for an empty manager', () => {
|
||||
expect(hasObservablePaneSession([])).toBe(false);
|
||||
});
|
||||
|
||||
it('says yes for a local session, which is the whole reason the watcher runs', () => {
|
||||
expect(hasObservablePaneSession([base])).toBe(true);
|
||||
});
|
||||
|
||||
it('says no for a remote session, whose local pane holds the ssh client', () => {
|
||||
expect(hasObservablePaneSession([{ ...base, remote: { host: 'box', user: 'me' } }])).toBe(false);
|
||||
});
|
||||
|
||||
it('says no for a Docker case, whose local pane holds a `docker exec`', () => {
|
||||
expect(hasObservablePaneSession([{ ...base, docker: { containerName: 'c1' } }])).toBe(false);
|
||||
});
|
||||
|
||||
it('says no for a record rebuilt from the socket, which carries no provenance', () => {
|
||||
// `reconcileSessions()` gives it a synthetic id that matches no state.json
|
||||
// entry, so a remote session rediscovered that way looks local. Session
|
||||
// forces UNKNOWN for it, so reading tmux for it buys nothing.
|
||||
expect(hasObservablePaneSession([{ ...base, discovered: true }])).toBe(false);
|
||||
});
|
||||
|
||||
it('says yes when one local session sits among sessions that cannot answer', () => {
|
||||
// The read is one batched call for the whole socket, so a single local
|
||||
// session is enough to make the tick worth paying for.
|
||||
expect(
|
||||
hasObservablePaneSession([
|
||||
{ ...base, sessionId: 's1', remote: { host: 'box', user: 'me' } },
|
||||
{ ...base, sessionId: 's2', discovered: true },
|
||||
{ ...base, sessionId: 's3' },
|
||||
])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// The predicate has to agree with `Session.paneExitApplies`, which is where
|
||||
// the rule is enforced; that pairing is pinned in session-pane-exit.test.ts,
|
||||
// where a real Session can answer for itself.
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user