Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev
This commit is contained in:
@@ -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."""
|
||||||
|
|||||||
@@ -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
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user