fix(tui): say alt+1-9 on the bar, and stop the dot grid when switching

The bar now reads "alt+1-9 switch · F1 back to the codeman dashboard", so the
switch keys are discoverable instead of secret. Shown only when those keys were
actually claimed, the same rule the way-out key follows: a bar naming a key
that does nothing is the bug this series started with.

THE DOT GRID. Switching landed in a pane occupying part of the terminal with
tmux's dot fill everywhere else. It was never a size mismatch — the window was
already the right size. `window-size latest` only resizes a window while a
client is ON it, and the sessions behind the tab strip have none until you
switch, so the resize happened AT the switch: tmux painted the newly-available
area with dots and an idle claude had no reason to redraw into it. Every
switchable session is now pre-sized to the attaching terminal, which moves that
repaint to attach time while the user is still looking at the first session,
and the switch binding restores `window-size latest` on arrival so a mid-attach
terminal resize still follows. Measured: 14 consecutive switches across 7
sessions, zero dot-padded rows, against 1-in-6 before.

⚠️ Known loose end, deliberately not papered over: after a detach the window
SIZE is restored exactly but the window-size MODE can come back as `latest`
rather than `manual`. The restore primitive round-trips correctly in isolation
(manual -> presize -> latest -> restore = manual) and no call site in the TUI
or the server sets `latest` afterwards, so the cause is not yet identified. The
practical effect is nil: the remaining client keeps the window at its own size
and Codeman re-pins `manual` on the browser's next resize.
This commit is contained in:
Codeman maintainer
2026-08-22 14:13:58 +02:00
parent 74c9879359
commit bc1821661f
3 changed files with 105 additions and 8 deletions
+58 -1
View File
@@ -883,6 +883,43 @@ export class TuiClient {
}
}
/**
* Size a window to the terminal that is about to look at it, NOW.
*
* ⚠️ `window-size latest` only resizes a window while a client is actually on
* it. The sessions behind the tab strip have none until you switch, so the
* resize happened AT the switch: tmux painted the newly-available area with
* its dot fill, and an idle claude had no reason to redraw into it, leaving a
* pane in the corner of a dotted screen (reported from the beta, with a
* screenshot). Pre-sizing moves that repaint to attach time, while the user
* is still looking at the first session.
*
* `resize-window` with an explicit size implies `window-size manual`, which
* is what we want: the size is already right when the switch lands, so tmux
* has nothing to change and nothing to repaint. The switch binding puts
* `latest` back so a mid-attach terminal resize still follows.
*/
async presizeWindow(muxName: string, cols: number, rows: number): Promise<boolean> {
if (!MUX_NAME_PATTERN.test(muxName)) return false;
if (!Number.isSafeInteger(cols) || !Number.isSafeInteger(rows) || cols <= 0 || rows <= 0) return false;
try {
await this.exec('tmux', [
'-L',
this.socket,
'resize-window',
'-t',
muxName,
'-x',
String(cols),
'-y',
String(rows),
]);
return true;
} catch {
return false;
}
}
/**
* tmux's prefix key for a session (`C-b` unless the user's config says
* otherwise), or null when tmux cannot say. Session-level first, then global:
@@ -963,7 +1000,27 @@ export class TuiClient {
async bindSwitchKey(key: string, target: string): Promise<boolean> {
if (!MUX_NAME_PATTERN.test(target)) return false;
try {
await this.exec('tmux', ['-L', this.socket, 'bind-key', '-T', 'root', key, 'switch-client', '-t', target]);
// Two commands: go there, then let that window follow this terminal again.
// It is already the right size (see presizeWindow), so `latest` changes
// nothing on arrival and costs no repaint — it matters only if the
// terminal is resized while sitting in that session.
await this.exec('tmux', [
'-L',
this.socket,
'bind-key',
'-T',
'root',
key,
'switch-client',
'-t',
target,
';',
'set-window-option',
'-t',
target,
'window-size',
'latest',
]);
return true;
} catch {
return false;