Merge pull request #397 from irisitymichaelgrundberg/fix/detached-session-owns-its-pane-size

fix(terminal): let a detached session's own window own its pane size
This commit is contained in:
Ark0N
2026-09-10 02:58:54 +02:00
committed by GitHub
3 changed files with 227 additions and 2 deletions
+20 -1
View File
@@ -1332,7 +1332,11 @@ class CodemanApp {
this._redock(id);
}
/** Clear all dashboard-side detached state/timers for a session. */
/** Clear all dashboard-side detached state/timers for a session, and take its
* sizing back: the popup owned the pane while it was open, so the dashboard's
* record of it is stale and the session it is showing needs re-measuring.
* ⚠️ Not idempotent — each call re-asserts, so a path that redocks twice for
* one close sends two SIGWINCHs. */
_redock(id) {
const t = this._detachWatchTimers.get(id);
if (t) { clearInterval(t); this._detachWatchTimers.delete(id); }
@@ -1340,6 +1344,21 @@ class CodemanApp {
this._detachOrphanStrikes.delete(id);
this.detachedWindows.delete(id);
this._markDetached(id, false);
// While the popup owned this session the dashboard sent no resizes, so
// `_lastResizeDims` — one value for the whole window — no longer describes
// the PTY, which the popup has been sizing. Clearing it makes the next
// sendResize report truthfully, on every redock path rather than only the
// active one: `selectSession` reads that answer to decide whether to wait
// for the TUI's redraw, and a false "unchanged" makes it fetch the frame
// before the redraw lands.
this._lastResizeDims = null;
// Sizing comes back with the session. `force` buys a guaranteed repaint for
// the case where popup and dashboard happened to agree on a size; the server
// already resizes on its own comparison against the real pane whenever the
// two differ.
if (this.sessions.has(id) && id === this.activeSessionId) {
this.sendResize(id, { force: true })?.catch?.(() => {});
}
}
/** Defer a channel-driven redock briefly. A popup *reload* emits 'redocked'
+23 -1
View File
@@ -940,7 +940,10 @@ Object.assign(CodemanApp.prototype, {
// causes Ink to re-render at the new row count, garbling terminal output.
// Local fit() still runs so xterm knows the viewport size for scrolling.
const keyboardUp = typeof KeyboardHandler !== 'undefined' && KeyboardHandler.keyboardVisible;
if (this.activeSessionId && !keyboardUp) {
// Same yield as sendResize: never resize a PTY whose session is showing
// in its own window. Dragging the dashboard's border must not reshape it.
const detachedElsewhere = !this.isSoloWindow && this.detachedSessions?.has(this.activeSessionId);
if (this.activeSessionId && !keyboardUp && !detachedElsewhere) {
const dims = this.fitAddon.proposeDimensions();
// Enforce minimum dimensions to prevent layout issues
const cols = dims ? Math.max(dims.cols, MIN_COLS) : MIN_COLS;
@@ -3848,6 +3851,14 @@ Object.assign(CodemanApp.prototype, {
return;
}
// The pane belongs to the popup showing it, so this window has nothing to
// restore. Say so rather than reporting a size that was never sent — the
// same button in that window does the job.
if (!this.isSoloWindow && this.detachedSessions?.has(this.activeSessionId)) {
this.showToast('This session is sized by its own window', 'warning');
return;
}
const dims = this.getTerminalDimensions();
if (!dims) {
this.showToast('Could not determine terminal size', 'error');
@@ -4900,6 +4911,17 @@ Object.assign(CodemanApp.prototype, {
// Fit terminal to container before reading dimensions — ensures local
// terminal size matches what we report to the server PTY.
if (this.fitAddon) this.fitAddon.fit();
// One PTY cannot hold two sizes. A detached session is owned by its own
// window, and the dashboard's terminal is narrower than that window because
// the session rail takes width the popup does not have — so both sizing it
// makes the CLI draw frames that fit neither, which garbles the popup. The
// dashboard yields; the solo window sizes what it alone displays.
// (_maybeRefetchFullHistory already stands aside for the same reason.)
// ⚠️ AFTER the fit, never before: the local reflow keeps the dashboard's own
// xterm right, and only the SERVER write is the dashboard's to withhold —
// the mobile-keyboard guard below draws exactly this line. tab-rail-resize
// performs its one settle-time refit through this call and has no fallback.
if (!this.isSoloWindow && this.detachedSessions?.has(sessionId)) return false;
const dims = this.getTerminalDimensions();
if (!dims) return false;
// Did the dimensions actually change since the last resize we sent? Callers