[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,458 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import type { RefObject } from "react";
import type { Terminal as XTerm } from "@xterm/xterm";
import {
getVisibleTerminalLineTimestampRows,
onTerminalLineTimestampsChange,
} from "./runtime/terminalLineTimestamps";
import type { TerminalTimestampGutterRow } from "./runtime/terminalLineTimestamps";
import { getTerminalOutputPressure } from "./runtime/terminalOutputPressure";
export const TERMINAL_TIMESTAMP_GUTTER_MIN_WIDTH = 56;
export const TERMINAL_TIMESTAMP_GUTTER_HORIZONTAL_PADDING = 16;
export const TERMINAL_TIMESTAMP_SAMPLE_LABEL = "88:88:88";
/** Cap gutter paint rate while large-output pressure is active (rAF still coalesces). */
export const TERMINAL_TIMESTAMP_GUTTER_FLOOD_MIN_INTERVAL_MS = 100;
const GUTTER_ROW_CLASS =
"absolute left-0 right-0 px-2 text-right tabular-nums whitespace-nowrap";
type TerminalTimestampGutterProps = {
termRef: RefObject<XTerm | null>;
containerRef: RefObject<HTMLDivElement | null>;
enabled: boolean;
top: string;
left?: number;
bottom?: number;
sessionId: string;
color: string;
fontFamily: string;
fontSize: number;
fontWeight: string | number;
width: number;
onWidthChange?: (width: number) => void;
};
type DisposableLike = {
dispose: () => void;
};
type TerminalTimestampTypography = {
fontFamily?: string;
fontSize?: number;
fontWeight?: string | number;
};
const getTerminalScreen = (container: HTMLElement): HTMLElement => (
container.querySelector<HTMLElement>(".xterm-screen") ?? container
);
const clearElement = (element: HTMLElement) => {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
};
const applyGutterRowStyles = (
item: HTMLElement,
{
row,
label,
screenTop,
cellHeight,
color,
fontFamily,
fontSize,
fontWeight,
}: {
row: number;
label: string;
screenTop: number;
cellHeight: number;
color: string;
fontFamily: string;
fontSize: number;
fontWeight: string | number;
},
) => {
if (item.textContent !== label) {
item.textContent = label;
}
item.style.top = `${screenTop + row * cellHeight}px`;
item.style.height = `${cellHeight}px`;
item.style.lineHeight = `${cellHeight}px`;
item.style.color = color;
item.style.fontFamily = fontFamily;
item.style.fontSize = `${fontSize}px`;
item.style.fontWeight = String(fontWeight);
item.style.fontVariantNumeric = "tabular-nums";
item.style.display = "";
};
/**
* Reuse a fixed pool of row divs instead of clear+create on every paint.
* Returns the number of visible nodes kept after the update.
*/
export const syncTerminalTimestampGutterRows = (
gutter: HTMLElement,
rows: readonly TerminalTimestampGutterRow[],
layout: {
screenTop: number;
cellHeight: number;
color: string;
fontFamily: string;
fontSize: number;
fontWeight: string | number;
},
): number => {
const existing = gutter.children;
let index = 0;
for (; index < rows.length; index += 1) {
const { row, label } = rows[index];
let item = existing[index] as HTMLElement | undefined;
if (!item) {
item = document.createElement("div");
item.className = GUTTER_ROW_CLASS;
gutter.appendChild(item);
}
applyGutterRowStyles(item, {
row,
label,
screenTop: layout.screenTop,
cellHeight: layout.cellHeight,
color: layout.color,
fontFamily: layout.fontFamily,
fontSize: layout.fontSize,
fontWeight: layout.fontWeight,
});
}
// Hide surplus pooled nodes (keep them for the next paint).
for (; index < existing.length; index += 1) {
(existing[index] as HTMLElement).style.display = "none";
}
return rows.length;
};
export const resolveTerminalTimestampGutterColor = (
colors: Partial<Record<"brightCyan" | "brightYellow" | "brightMagenta" | "foreground", string>>,
): string => (
colors.brightCyan
|| colors.brightYellow
|| colors.brightMagenta
|| colors.foreground
|| "currentColor"
);
const normalizeTerminalTimestampFontSize = (fontSize?: number): number => (
Number.isFinite(fontSize) && fontSize && fontSize > 0 ? fontSize : 12
);
export const getTerminalTimestampTypography = ({
fontFamily,
fontSize,
fontWeight,
}: TerminalTimestampTypography) => ({
fontFamily: fontFamily || "monospace",
fontSize: normalizeTerminalTimestampFontSize(fontSize),
fontWeight: fontWeight ?? 400,
});
const estimateTerminalTimestampTextWidth = (
fontSize: number,
label = TERMINAL_TIMESTAMP_SAMPLE_LABEL,
): number => (
normalizeTerminalTimestampFontSize(fontSize) * label.length * 0.62
);
export const resolveTerminalTimestampGutterWidth = ({
measuredTextWidth,
fontSize,
label = TERMINAL_TIMESTAMP_SAMPLE_LABEL,
}: {
measuredTextWidth?: number;
fontSize?: number;
label?: string;
}): number => {
const textWidth =
Number.isFinite(measuredTextWidth) && measuredTextWidth !== undefined && measuredTextWidth > 0
? measuredTextWidth
: estimateTerminalTimestampTextWidth(normalizeTerminalTimestampFontSize(fontSize), label);
return Math.ceil(Math.max(
TERMINAL_TIMESTAMP_GUTTER_MIN_WIDTH,
textWidth + TERMINAL_TIMESTAMP_GUTTER_HORIZONTAL_PADDING,
));
};
export const resolveTerminalTimestampGutterRenderSignature = ({
screenTop,
cellHeight,
color,
fontFamily,
fontSize,
fontWeight,
rows,
}: {
screenTop: number;
cellHeight: number;
color: string;
fontFamily: string;
fontSize: number;
fontWeight: string | number;
rows: readonly TerminalTimestampGutterRow[];
}): string => {
let signature = `${screenTop}|${cellHeight}|${color}|${fontFamily}|${fontSize}|${fontWeight}`;
for (const { row, label } of rows) {
signature += `|${row}:${label}`;
}
return signature;
};
export function TerminalTimestampGutter({
termRef,
containerRef,
enabled,
top,
left = 0,
bottom = 0,
sessionId,
color,
fontFamily,
fontSize,
fontWeight,
width,
onWidthChange,
}: TerminalTimestampGutterProps) {
const gutterRef = useRef<HTMLDivElement>(null);
const typography = getTerminalTimestampTypography({ fontFamily, fontSize, fontWeight });
useLayoutEffect(() => {
if (!enabled || !onWidthChange) return;
const gutter = gutterRef.current;
if (!gutter) return;
let disposed = false;
const measure = () => {
if (disposed) return;
const probe = document.createElement("span");
probe.textContent = TERMINAL_TIMESTAMP_SAMPLE_LABEL;
probe.style.position = "absolute";
probe.style.visibility = "hidden";
probe.style.pointerEvents = "none";
probe.style.whiteSpace = "nowrap";
probe.style.fontFamily = typography.fontFamily;
probe.style.fontSize = `${typography.fontSize}px`;
probe.style.fontWeight = String(typography.fontWeight);
probe.style.fontVariantNumeric = "tabular-nums";
gutter.appendChild(probe);
const measuredTextWidth = probe.getBoundingClientRect().width;
probe.remove();
onWidthChange(resolveTerminalTimestampGutterWidth({
measuredTextWidth,
fontSize: typography.fontSize,
}));
};
measure();
const fonts = (document as Document & { fonts?: { ready?: Promise<unknown> } }).fonts;
void fonts?.ready?.then(measure);
return () => {
disposed = true;
};
}, [enabled, onWidthChange, sessionId, typography.fontFamily, typography.fontSize, typography.fontWeight]);
useEffect(() => {
const gutter = gutterRef.current;
if (!gutter) return;
let disposed = false;
let rafId: number | null = null;
let retryTimer: ReturnType<typeof setTimeout> | null = null;
let floodThrottleTimer: ReturnType<typeof setTimeout> | null = null;
let disposables: DisposableLike[] = [];
let resizeObserver: ResizeObserver | null = null;
let lastRenderSignature = "";
let lastFloodRenderAt = 0;
const clearGutter = () => {
lastRenderSignature = "";
clearElement(gutter);
};
const render = () => {
rafId = null;
// Always advance the flood clock when a render attempt runs — even if the
// signature is unchanged. Otherwise same-second labels during sustained
// output leave lastFloodRenderAt stale and the throttle stops limiting work.
lastFloodRenderAt = performance.now();
const term = termRef.current;
const container = containerRef.current;
if (!enabled || !term || !container) {
clearGutter();
return;
}
const screen = getTerminalScreen(container);
const rows = Math.max(1, term.rows || 1);
const cellHeight = screen.clientHeight / rows;
if (!Number.isFinite(cellHeight) || cellHeight <= 0) {
clearGutter();
return;
}
const screenRect = screen.getBoundingClientRect();
const gutterRect = gutter.getBoundingClientRect();
const screenTop = screenRect.top - gutterRect.top;
const visibleRows = getVisibleTerminalLineTimestampRows(term);
const signature = resolveTerminalTimestampGutterRenderSignature({
screenTop,
cellHeight,
color,
fontFamily: typography.fontFamily,
fontSize: typography.fontSize,
fontWeight: typography.fontWeight,
rows: visibleRows,
});
if (signature === lastRenderSignature) return;
lastRenderSignature = signature;
syncTerminalTimestampGutterRows(gutter, visibleRows, {
screenTop,
cellHeight,
color,
fontFamily: typography.fontFamily,
fontSize: typography.fontSize,
fontWeight: typography.fontWeight,
});
};
const queueRafRender = () => {
if (disposed || rafId !== null) return;
if (typeof requestAnimationFrame === "function") {
rafId = requestAnimationFrame(render);
} else {
render();
}
};
/**
* immediate: user scroll/resize — paint ASAP.
* normal: output/render pressure — throttle while flood pressure is active.
*/
const scheduleRender = (priority: "immediate" | "normal" = "normal") => {
if (disposed) return;
if (priority === "normal") {
const term = termRef.current;
if (term) {
const pressure = getTerminalOutputPressure(term);
if (pressure.largeOutput || pressure.longLine) {
const now = performance.now();
const elapsed = now - lastFloodRenderAt;
if (elapsed < TERMINAL_TIMESTAMP_GUTTER_FLOOD_MIN_INTERVAL_MS) {
if (floodThrottleTimer === null) {
floodThrottleTimer = setTimeout(() => {
floodThrottleTimer = null;
queueRafRender();
}, TERMINAL_TIMESTAMP_GUTTER_FLOOD_MIN_INTERVAL_MS - elapsed);
}
return;
}
}
}
} else if (floodThrottleTimer !== null) {
clearTimeout(floodThrottleTimer);
floodThrottleTimer = null;
}
queueRafRender();
};
const attach = () => {
if (disposed) return;
const term = termRef.current;
const container = containerRef.current;
if (!enabled || !term || !container) {
clearGutter();
if (enabled) {
retryTimer = setTimeout(attach, 50);
}
return;
}
disposables = [
// xterm fires onScroll for output-driven scrolling too (not only user
// wheel/trackpad). Throttle only while large-output pressure is active
// (time-bounded). Do not use longLine here — it sticks until the next
// data chunk and would lag genuine user scrolling after output stops.
term.onScroll?.(() => {
const pressure = getTerminalOutputPressure(term);
scheduleRender(pressure.largeOutput ? "normal" : "immediate");
}),
term.onRender?.(() => scheduleRender("normal")),
term.onResize?.(() => scheduleRender("immediate")),
].filter(Boolean) as DisposableLike[];
disposables.push({
dispose: onTerminalLineTimestampsChange(term, () => scheduleRender("normal")),
});
if (typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(() => scheduleRender("immediate"));
resizeObserver.observe(container);
resizeObserver.observe(getTerminalScreen(container));
}
scheduleRender("immediate");
};
attach();
return () => {
disposed = true;
if (rafId !== null && typeof cancelAnimationFrame === "function") {
cancelAnimationFrame(rafId);
}
if (retryTimer) {
clearTimeout(retryTimer);
}
if (floodThrottleTimer !== null) {
clearTimeout(floodThrottleTimer);
}
for (const disposable of disposables) {
disposable.dispose();
}
resizeObserver?.disconnect();
clearElement(gutter);
};
}, [
color,
containerRef,
enabled,
bottom,
left,
sessionId,
termRef,
top,
typography.fontFamily,
typography.fontSize,
typography.fontWeight,
]);
if (!enabled) return null;
return (
<div
ref={gutterRef}
aria-hidden="true"
className="pointer-events-none absolute z-[1] overflow-hidden select-none text-[color:var(--terminal-ui-fg)]"
style={{
top,
bottom,
left,
width,
backgroundColor: "var(--terminal-ui-bg)",
boxShadow: "inset -0.5px 0 0 color-mix(in srgb, var(--terminal-ui-fg) 8%, transparent)",
}}
data-section="terminal-timestamp-gutter"
/>
);
}