refactor: codebase audit cleanup — dead code, dedup, module splits
Multi-agent audit + aggressive cleanup across server/web/coder/booterm, delivered behind a DEFER discipline so none of the in-flight files were touched. Removes dead code/deps/columns, dedups server + coder helpers, and splits the oversized modules (tools.ts, opencode-server.ts, sentinel-summaries, turn.ts, TerminalPane.tsx) behind stable contracts. Adds 78 parity/unit tests (server 587, coder 323); fixes two latent bugs (ChatPane queue keys, FileViewerOverlay blank-line parity). Intended tag: v2.7.12-audit-cleanup.
This commit is contained in:
179
apps/web/src/hooks/terminal/useTerminalFit.ts
Normal file
179
apps/web/src/hooks/terminal/useTerminalFit.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
|
||||
// useTerminalFit — terminal measurement + the refit triggers that are purely
|
||||
// about sizing (fonts.ready rAF, 150ms settle timeout, ResizeObserver,
|
||||
// visualViewport resize). The two refit triggers that are entangled with the
|
||||
// connection lifecycle — the init rAF (fit → /start → connect) and the
|
||||
// visibilitychange handler (reconnect-or-refit) — live in useTerminalSocket
|
||||
// and call this hook's `fit()`; that keeps each of those single-rAF / if-else
|
||||
// sequences intact rather than splitting one listener across two hooks.
|
||||
//
|
||||
// Exposes a stable `fit()` (the FitAddon-bypass measurement, with the iOS
|
||||
// keyboard clip) and `getSize()`/`setSize()` over the read-latest size mirror.
|
||||
|
||||
// xterm 5 ships no public dimensions API — `_core._renderService.dimensions`
|
||||
// is internal but stable across the package versions we ship. This is the ONE
|
||||
// place that reaches into the private cell metrics; everything else funnels
|
||||
// through `cellSize` or `readCellMetrics`.
|
||||
function readCellMetrics(term: Terminal): { w: number; h: number } | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dims = (term as any)._core?._renderService?.dimensions?.css?.cell;
|
||||
if (dims && dims.width > 0 && dims.height > 0) {
|
||||
return { w: dims.width, h: dims.height };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Cell metrics with a getBoundingClientRect fallback for callers (touch
|
||||
// point→cell mapping) that need an estimate even before xterm has measured.
|
||||
// `fit()` deliberately does NOT use this fallback — it bails until real
|
||||
// metrics exist so it never resizes against a fallback estimate.
|
||||
export function cellSize(term: Terminal, container: HTMLElement): { w: number; h: number } {
|
||||
const m = readCellMetrics(term);
|
||||
if (m) return m;
|
||||
const rect = container.getBoundingClientRect();
|
||||
return { w: rect.width / Math.max(term.cols, 1), h: rect.height / Math.max(term.rows, 1) };
|
||||
}
|
||||
|
||||
interface FitDeps {
|
||||
termRef: React.MutableRefObject<Terminal | null>;
|
||||
containerRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
sessionId: string;
|
||||
paneId: string;
|
||||
}
|
||||
|
||||
export interface TerminalFit {
|
||||
fit: () => void;
|
||||
getSize: () => { cols: number; rows: number };
|
||||
setSize: (cols: number, rows: number) => void;
|
||||
}
|
||||
|
||||
export function useTerminalFit({ termRef, containerRef, sessionId, paneId }: FitDeps): TerminalFit {
|
||||
// Last size pushed through term.onResize. Mirrors xterm's authoritative
|
||||
// cols/rows; consulted on every WS open so the resize frame carries the
|
||||
// currently-measured size, not a stale lastSize from before the latest fit.
|
||||
const lastSizeRef = useRef<{ cols: number; rows: number }>({ cols: 80, rows: 24 });
|
||||
|
||||
const getSize = useCallback(() => lastSizeRef.current, []);
|
||||
const setSize = useCallback((cols: number, rows: number) => {
|
||||
lastSizeRef.current = { cols, rows };
|
||||
}, []);
|
||||
|
||||
// Bypass FitAddon's proposeDimensions(), which subtracts the native
|
||||
// scrollbar's reserved width even after CSS hides it (boolab fix). We
|
||||
// compute cols/rows directly from host.clientWidth/Height ÷ cell metrics.
|
||||
// Falls through (no-op) if cell metrics aren't measured yet — a later
|
||||
// refit (fonts.ready / setTimeout / ResizeObserver) will catch it.
|
||||
//
|
||||
// v1.10.8d: row count uses the VISIBLE height (clipped against the
|
||||
// visualViewport) rather than raw clientHeight. h-dvh on the root only
|
||||
// shrinks for the URL bar — not the iOS keyboard — so without the clip
|
||||
// we allocate rows for the area hidden behind the keyboard, and bash's
|
||||
// cursor (pushed down by MOTD on SSH login) ends up below the fold.
|
||||
const fit = useCallback((): void => {
|
||||
const t = termRef.current;
|
||||
if (!t) return;
|
||||
const host = containerRef.current;
|
||||
if (!host) return;
|
||||
if (!t.element || !(t.element as HTMLElement).offsetParent) return;
|
||||
const m = readCellMetrics(t);
|
||||
if (!m) return;
|
||||
const cellW = m.w;
|
||||
const cellH = m.h;
|
||||
let visibleHeight = host.clientHeight;
|
||||
const vp2 = typeof window !== 'undefined' ? window.visualViewport : null;
|
||||
if (vp2) {
|
||||
const rect = host.getBoundingClientRect();
|
||||
// visualViewport.height is the layout viewport minus the keyboard.
|
||||
// rect.bottom is in layout-viewport coordinates. clip to whichever
|
||||
// is smaller. offsetTop/pageTop adjustments aren't needed at zoom=1.
|
||||
const visibleBottom = Math.min(rect.bottom, vp2.height);
|
||||
const clipped = Math.max(0, visibleBottom - rect.top);
|
||||
if (clipped > 0) visibleHeight = clipped;
|
||||
}
|
||||
const cols = Math.max(2, Math.floor(host.clientWidth / cellW));
|
||||
const rows = Math.max(1, Math.floor(visibleHeight / cellH));
|
||||
if (cols !== t.cols || rows !== t.rows) {
|
||||
try {
|
||||
t.resize(cols, rows);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}, [termRef, containerRef]);
|
||||
|
||||
// Pure-fit refit triggers. Re-run on session/pane change so a recreated
|
||||
// terminal gets the same fonts.ready / settle / ResizeObserver / vpResize
|
||||
// coverage the pre-split single effect gave it.
|
||||
useEffect(() => {
|
||||
const term = termRef.current;
|
||||
const ctr = containerRef.current;
|
||||
if (!term || !ctr) return;
|
||||
|
||||
let disposed = false;
|
||||
|
||||
// Boolab pattern: term measures cell metrics on open() against whatever
|
||||
// font is registered at that instant; if the font hasn't loaded yet we
|
||||
// get a fallback measurement and the wrong cols. Refit when
|
||||
// document.fonts.ready resolves.
|
||||
let fontRaf: number | null = null;
|
||||
if (typeof document !== 'undefined' && document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready
|
||||
.then(() => {
|
||||
if (disposed) return;
|
||||
fontRaf = requestAnimationFrame(() => fit());
|
||||
})
|
||||
.catch(() => {
|
||||
/* ignore */
|
||||
});
|
||||
}
|
||||
|
||||
// Second-pass refit: mobile browsers can settle layout slightly late,
|
||||
// causing the rAF fit to measure a stale clientWidth.
|
||||
const delayedFit = setTimeout(() => fit(), 150);
|
||||
|
||||
// ResizeObserver: refit on any container size change. No debounce —
|
||||
// term.onResize itself is the throttle (no-op when cols/rows unchanged).
|
||||
const ro = new ResizeObserver(() => {
|
||||
fit();
|
||||
});
|
||||
ro.observe(ctr);
|
||||
|
||||
// v1.10.8d: visualViewport.resize fires when the iOS keyboard opens or
|
||||
// closes — the layout viewport stays full-height (so our h-dvh root
|
||||
// doesn't shrink), but the visual viewport contracts above the keyboard.
|
||||
// Refit so xterm's row count matches the visible area, then scroll to
|
||||
// bottom so the cursor stays above the keyboard fold (boolab pattern).
|
||||
let onVpResize: (() => void) | null = null;
|
||||
const vp = typeof window !== 'undefined' ? window.visualViewport : null;
|
||||
if (vp) {
|
||||
onVpResize = (): void => {
|
||||
if (disposed) return;
|
||||
fit();
|
||||
const t = termRef.current;
|
||||
if (t) {
|
||||
try {
|
||||
t.scrollToBottom();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
};
|
||||
vp.addEventListener('resize', onVpResize);
|
||||
}
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (fontRaf !== null) cancelAnimationFrame(fontRaf);
|
||||
clearTimeout(delayedFit);
|
||||
ro.disconnect();
|
||||
if (vp && onVpResize) vp.removeEventListener('resize', onVpResize);
|
||||
};
|
||||
// sessionId/paneId drive re-run on terminal recreation (term is rebuilt by
|
||||
// the orchestrator on those deps); fit is stable.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId, paneId, fit]);
|
||||
|
||||
return { fit, getSize, setSize };
|
||||
}
|
||||
Reference in New Issue
Block a user