Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev

This commit is contained in:
2026-05-24 17:58:49 +00:00
3 changed files with 50 additions and 23 deletions
+12 -6
View File
@@ -145,21 +145,27 @@ class TerminalSession:
# Only resize if dimensions actually changed # Only resize if dimensions actually changed
if cols == self._cols and rows == self._rows: if cols == self._cols and rows == self._rows:
logger.info(f"resize() skipped for session {self.session_id}: already {cols}x{rows}")
return return
self._cols = cols self._cols = cols
self._rows = rows self._rows = rows
logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}") logger.info(f"resize() called for session {self.session_id}: {cols}x{rows}")
# Set host PTY size
self._set_terminal_size(cols, rows) self._set_terminal_size(cols, rows)
# Docker exec doesn't forward PTY resize to the container process, # Docker exec doesn't forward PTY resize to the container process,
# so we need to explicitly set the size inside the container shell. # so we need to explicitly set the size inside the container shell.
# Only do this on the first resize to avoid interfering with user input. # Send on every resize so the container shell always matches the frontend.
if not getattr(self, '_stty_sent', False): # Use stty -echo to hide command output, then re-enable echo
self._stty_sent = True # Add small delay to ensure shell is ready to receive commands
stty_cmd = f"stty cols {cols} rows {rows}\n".encode() await asyncio.sleep(0.1)
await self.write_input(stty_cmd) stty_cmd = (
logger.info(f"Sent stty command to container for session {self.session_id}: {cols}x{rows}") f"stty -echo; stty cols {cols} rows {rows}; stty echo\n"
).encode()
await self.write_input(stty_cmd)
logger.info(f"Sent stty resize to container for session {self.session_id}: {cols}x{rows}")
async def reset(self) -> None: async def reset(self) -> None:
"""Reset the session by killing the process and clearing state.""" """Reset the session by killing the process and clearing state."""
+32 -5
View File
@@ -216,17 +216,29 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
// Fit terminal and notify backend // Fit terminal and notify backend
const fitTerminal = () => { const fitTerminal = () => {
if (!fitAddonRef.current || !termRef.current) return; if (!fitAddonRef.current || !termRef.current) {
console.log('[Terminal] fitTerminal: refs not ready');
return;
}
const oldCols = termRef.current.cols; const oldCols = termRef.current.cols;
const oldRows = termRef.current.rows; const oldRows = termRef.current.rows;
console.log('[Terminal] fitTerminal called, container dims:', container?.offsetWidth, container?.offsetHeight);
fitAddonRef.current.fit(); fitAddonRef.current.fit();
const { cols, rows } = termRef.current; const { cols, rows } = termRef.current;
// Force refresh if dimensions changed console.log('[Terminal] fitTerminal result:', cols, rows, '(was:', oldCols, oldRows + ')');
if (cols !== oldCols || rows !== oldRows) {
termRef.current.refresh(0, rows - 1); // Always refresh on initial load or when dimensions change
} requestAnimationFrame(() => {
termRef.current?.refresh(0, rows - 1);
});
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
console.log('[Terminal] Sending resize:', cols, rows);
ws.send(JSON.stringify({ type: "resize", cols, rows })); ws.send(JSON.stringify({ type: "resize", cols, rows }));
} else {
console.log('[Terminal] WebSocket not open, state:', ws.readyState);
} }
}; };
@@ -284,6 +296,19 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
}); });
resizeObserver.observe(container); resizeObserver.observe(container);
// Also listen for window resize as fallback (ResizeObserver might miss some cases)
let windowResizeTimeout: ReturnType<typeof setTimeout>;
const handleWindowResize = () => {
clearTimeout(windowResizeTimeout);
windowResizeTimeout = setTimeout(() => {
requestAnimationFrame(() => {
if (!container.isConnected) return;
fitTerminal();
});
}, 250);
};
window.addEventListener("resize", handleWindowResize);
// Refit after mobile header auto-hides (3s delay + 0.3s transition) // Refit after mobile header auto-hides (3s delay + 0.3s transition)
const headerHideTimeout = setTimeout(() => { const headerHideTimeout = setTimeout(() => {
fitTerminal(); fitTerminal();
@@ -316,8 +341,10 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
return () => { return () => {
clearTimeout(resizeTimeout); clearTimeout(resizeTimeout);
clearTimeout(windowResizeTimeout);
clearTimeout(headerHideTimeout); clearTimeout(headerHideTimeout);
resizeObserver.disconnect(); resizeObserver.disconnect();
window.removeEventListener("resize", handleWindowResize);
document.removeEventListener("visibilitychange", handleVisibilityChange); document.removeEventListener("visibilitychange", handleVisibilityChange);
if (ws) { if (ws) {
ws.close(); ws.close();
+6 -12
View File
@@ -3220,21 +3220,15 @@ a.nav-item,
position: relative; position: relative;
} }
/* xterm fills container */ /* xterm fills container - reset position for mobile */
.terminal-wrapper.mobile .terminal-container .xterm { .terminal-wrapper.mobile .terminal-container .xterm {
position: relative !important;
top: auto !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
width: 100%; width: 100%;
height: 100%; height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
}
.terminal-wrapper.mobile .terminal-container .xterm-viewport {
flex: 1;
width: 100% !important;
height: 100% !important;
max-height: 100% !important;
overflow-y: auto !important;
} }
/* Special Keys Strip */ /* Special Keys Strip */