Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion apps/server/src/terminal/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ interface TerminalSubprocessInspector {
): Effect.Effect<TerminalSubprocessInspectResult, TerminalSubprocessCheckError>;
}

// Resize can fail permanently (e.g. a Bun build without `terminal.resize`). The frontend
// command swallows the error, so warn once per process to keep the cause visible in logs
// without spamming on every resize.
const resizeFailureWarned = new WeakSet<PtyAdapter.PtyProcess>();

const resizePtyProcess = (
session: TerminalSessionState,
process: PtyAdapter.PtyProcess,
Expand All @@ -217,7 +222,20 @@ const resizePtyProcess = (
rows,
cause,
}),
});
}).pipe(
Effect.tapError((error) =>
Effect.gen(function* () {
if (resizeFailureWarned.has(process)) return;
resizeFailureWarned.add(process);
yield* Effect.logWarning("terminal resize failed; size changes will not propagate", {
threadId: session.threadId,
terminalId: session.terminalId,
terminalPid: process.pid,
cause: error.cause,
});
}),
),
);

export interface ShellCandidate {
shell: string;
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/components/ThreadTerminalDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,37 @@ export function TerminalViewport({
window.cancelAnimationFrame(frame);
};
}, [drawerHeight, environmentId, resizeEpoch, terminalId, threadId]);

// CSS/flex-driven container resizes (split-pane drag, sidebar collapse) change the xterm
// size without firing window.resize, so resizeEpoch never advances. Observe the container
// directly and refit. rAF-coalesced to one fit per frame, matching the effect above.
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let frame: number | null = null;
const observer = new ResizeObserver(() => {
if (frame !== null) return;
frame = window.requestAnimationFrame(() => {
frame = null;
const terminal = terminalRef.current;
const fitAddon = fitAddonRef.current;
if (!terminal || !fitAddon) return;
const wasAtBottom = terminal.buffer.active.viewportY >= terminal.buffer.active.baseY;
fitTerminalSafely(fitAddon);
if (wasAtBottom) {
terminal.scrollToBottom();
}
void resizeTerminal(terminal.cols, terminal.rows);
Comment on lines +813 to +817

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid resizing hidden terminals to minimum geometry

When switching away from a thread, retained terminal drawers are kept mounted but wrapped in className="hidden" in PersistentThreadTerminalDrawer; that display:none size change also reaches this observer. Because the callback still calls fit() and sends the resulting size to the server, background terminals can be resized to xterm's minimum/zero-container geometry and receive a SIGWINCH while hidden, which can break full-screen/background processes and corrupt wrapping until the thread is shown again. Skip the resize when the observed container size is 0 or pause the observer while the drawer is not visible.

Useful? React with 👍 / 👎.

});
});
observer.observe(container);
return () => {
if (frame !== null) {
window.cancelAnimationFrame(frame);
}
observer.disconnect();
};
}, [environmentId, terminalId, threadId]);
return (
<div
ref={containerRef}
Expand Down
Loading