[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,470 @@
import type { Terminal as XTerm } from "@xterm/xterm";
import { MAX_TERMINAL_WRITE_QUEUE_DRAIN_BYTES } from "./terminalFlowConstants";
export const MAX_WRITE_QUEUE_ITEMS = 32;
export const MAX_WRITE_QUEUE_BYTES = 512 * 1024;
/**
* Soft wall-clock budget per event-loop turn of continuous write-queue drain.
* After this, yield with setTimeout(0) so input/Ctrl-C can run — similar to
* keeping the UI responsive without Tabby-style 4KB choppiness.
*/
export const WRITE_QUEUE_TURN_BUDGET_MS = 10;
export type TerminalWriteQueueOptions = {
onDropped?: (bytes: number) => void;
dropBytes?: number;
deferStart?: boolean;
yieldAfter?: boolean;
maxDrainBytes?: number;
};
type QueuedWrite = {
bytes: number;
dropBytes: number;
steps: QueuedWriteStep[];
nextIndex: number;
cancelled: boolean;
yieldAfter: boolean;
maxDrainBytes: number;
};
type QueuedWriteStep = {
bytes: number;
dropBytes: number;
write: (done: () => void) => void;
yieldAfter: boolean;
};
type TerminalWriteQueue = {
writing: boolean;
active?: QueuedWrite;
pending: QueuedWrite[];
pendingBytes: number;
drainBytes: number;
floodMode: boolean;
turnStartedAt: number;
onDropped?: (bytes: number) => void;
drainTimer?: ReturnType<typeof setTimeout>;
stepTimer?: ReturnType<typeof setTimeout>;
stepContinuation?: () => void;
};
const terminalWriteQueues = new WeakMap<XTerm, TerminalWriteQueue>();
const terminalWriteQueueDropHandlers = new WeakMap<XTerm, (bytes: number) => void>();
const getOrCreateQueue = (term: XTerm): TerminalWriteQueue => {
let queue = terminalWriteQueues.get(term);
if (!queue) {
queue = {
writing: false,
pending: [],
pendingBytes: 0,
drainBytes: 0,
floodMode: false,
turnStartedAt: 0,
onDropped: terminalWriteQueueDropHandlers.get(term),
};
terminalWriteQueues.set(term, queue);
}
return queue;
};
const beginQueueTurn = (queue: TerminalWriteQueue): void => {
if (queue.turnStartedAt <= 0) {
queue.turnStartedAt = performance.now();
}
};
const endQueueTurn = (queue: TerminalWriteQueue): void => {
queue.turnStartedAt = 0;
};
const isQueueTurnBudgetExceeded = (queue: TerminalWriteQueue): boolean => {
if (queue.turnStartedAt <= 0) return false;
return performance.now() - queue.turnStartedAt >= WRITE_QUEUE_TURN_BUDGET_MS;
};
const scheduleQueueDrain = (
term: XTerm,
queue: TerminalWriteQueue,
deferred: boolean,
): void => {
if (queue.drainTimer) return;
if (!deferred) {
scheduleNextTerminalWrite(term, queue);
return;
}
endQueueTurn(queue);
queue.drainTimer = setTimeout(() => {
queue.drainTimer = undefined;
scheduleNextTerminalWrite(term, queue);
}, 0);
};
const scheduleNextTerminalWrite = (term: XTerm, queue: TerminalWriteQueue) => {
const next = queue.pending.shift();
if (!next) {
queue.writing = false;
queue.drainBytes = 0;
queue.floodMode = false;
endQueueTurn(queue);
if (terminalWriteQueues.get(term) === queue) {
terminalWriteQueues.delete(term);
}
return;
}
beginQueueTurn(queue);
if (
queue.drainBytes > 0
&& queue.drainBytes + next.bytes > next.maxDrainBytes
) {
queue.drainBytes = 0;
queue.pending.unshift(next);
scheduleQueueDrain(term, queue, true);
return;
}
// Time-budget yield: continuous sync drains stay smooth like Tabby, but never
// pin the main thread for multi-frame bulk parses.
if (queue.drainBytes > 0 && isQueueTurnBudgetExceeded(queue)) {
queue.pending.unshift(next);
scheduleQueueDrain(term, queue, true);
return;
}
queue.pendingBytes -= next.bytes;
if (queue.pendingBytes < 0) queue.pendingBytes = 0;
queue.writing = true;
queue.active = next;
runQueuedWrite(next, () => {
if (queue.active !== next) {
return;
}
queue.drainBytes += next.bytes;
if (queue.active === next) {
queue.active = undefined;
}
if (next.yieldAfter) {
queue.drainBytes = 0;
}
const shouldDefer = next.yieldAfter || isQueueTurnBudgetExceeded(queue);
if (shouldDefer && !next.yieldAfter) {
// Byte drain continues next turn; reset counter so a fresh budget applies.
queue.drainBytes = 0;
}
scheduleQueueDrain(term, queue, shouldDefer);
}, (continuation) => {
endQueueTurn(queue);
const timer = setTimeout(() => {
if (terminalWriteQueues.get(term) !== queue || queue.stepTimer !== timer) {
return;
}
queue.stepTimer = undefined;
queue.stepContinuation = undefined;
continuation();
}, 0);
queue.stepTimer = timer;
queue.stepContinuation = continuation;
});
};
const resolveMaxDrainBytes = (value?: number): number => (
Number.isFinite(value) && Number(value) > 0
? Number(value)
: MAX_TERMINAL_WRITE_QUEUE_DRAIN_BYTES
);
/** Max synchronous recursive step hops inside one merged flood item. */
const MAX_SYNC_MERGED_STEPS_BEFORE_YIELD = 32;
const runQueuedWrite = (
item: QueuedWrite,
done: () => void,
deferStep: (continuation: () => void) => void,
): void => {
let index = 0;
let completed = false;
let currentDrainBytes = 0;
let syncStepsSinceYield = 0;
let turnStartedAt = performance.now();
const deferAndResume = (): void => {
syncStepsSinceYield = 0;
currentDrainBytes = 0;
deferStep(() => {
turnStartedAt = performance.now();
runNext();
});
};
const runNext = (): void => {
if (completed) {
return;
}
if (item.cancelled) {
completed = true;
done();
return;
}
const step = item.steps[index];
if (!step) {
completed = true;
done();
return;
}
if (
currentDrainBytes > 0
&& currentDrainBytes + step.bytes > item.maxDrainBytes
) {
deferAndResume();
return;
}
index += 1;
item.nextIndex = index;
let callbackCalledSynchronously = false;
let insideWrite = true;
const continueAfterStep = (): void => {
currentDrainBytes += step.bytes;
// Inter-step yields honor per-step flags only. item.yieldAfter applies
// after the whole item finishes (see scheduleNextTerminalWrite), so that
// flood merges preserve writeLargeTerminalBatch drain-budget pacing
// instead of forcing a timer after every shard.
if (step.yieldAfter && index < item.steps.length) {
deferAndResume();
return;
}
// Flood-merged items can hold thousands of tiny sync steps. Bound stack
// depth and wall-clock time so the main thread stays under the turn budget.
if (index < item.steps.length) {
syncStepsSinceYield += 1;
if (
syncStepsSinceYield >= MAX_SYNC_MERGED_STEPS_BEFORE_YIELD
|| performance.now() - turnStartedAt >= WRITE_QUEUE_TURN_BUDGET_MS
) {
deferAndResume();
return;
}
}
runNext();
};
step.write(() => {
if (insideWrite) {
callbackCalledSynchronously = true;
return;
}
continueAfterStep();
});
insideWrite = false;
if (callbackCalledSynchronously) {
continueAfterStep();
}
};
runNext();
};
const runDeferredQueueStepNow = (queue: TerminalWriteQueue): boolean => {
const continuation = queue.stepContinuation;
if (!continuation) return false;
if (queue.stepTimer !== undefined) {
clearTimeout(queue.stepTimer);
}
queue.stepTimer = undefined;
queue.stepContinuation = undefined;
continuation();
return true;
};
const runDeferredQueueDrainNow = (
term: XTerm,
queue: TerminalWriteQueue,
): boolean => {
if (queue.drainTimer === undefined) return false;
clearTimeout(queue.drainTimer);
queue.drainTimer = undefined;
scheduleNextTerminalWrite(term, queue);
return true;
};
const mergePendingWrites = (queue: TerminalWriteQueue): void => {
if (queue.pending.length <= 1) return;
const steps: QueuedWriteStep[] = [];
let bytes = 0;
let dropBytes = 0;
// Keep a post-item yield if any source item asked for one; inter-step yields
// come only from per-step yieldAfter / maxDrainBytes (not a forced every-step
// pause). writeLargeTerminalBatch sets yieldAfter only every drain budget.
let yieldAfterItem = false;
for (const item of queue.pending) {
bytes += item.bytes;
dropBytes += item.dropBytes;
if (item.yieldAfter) {
yieldAfterItem = true;
}
steps.push(...item.steps.slice(item.nextIndex));
}
queue.pending = [{
bytes,
dropBytes,
steps,
nextIndex: 0,
cancelled: false,
yieldAfter: yieldAfterItem,
maxDrainBytes: Math.min(...queue.pending.map((item) => item.maxDrainBytes)),
}];
queue.pendingBytes = bytes;
queue.floodMode = true;
};
const updateFloodMode = (
queue: TerminalWriteQueue,
nextBytes: number,
): void => {
if (
queue.floodMode
|| queue.pending.length >= MAX_WRITE_QUEUE_ITEMS
|| queue.pendingBytes + nextBytes > MAX_WRITE_QUEUE_BYTES
) {
queue.floodMode = true;
}
};
export const setTerminalWriteQueueDropHandler = (
term: XTerm,
onDropped?: (bytes: number) => void,
): void => {
if (onDropped) {
terminalWriteQueueDropHandlers.set(term, onDropped);
} else {
terminalWriteQueueDropHandlers.delete(term);
}
const queue = terminalWriteQueues.get(term);
if (queue && onDropped) {
queue.onDropped = onDropped;
}
};
export const getTerminalWriteQueueDepth = (term: XTerm): number =>
terminalWriteQueues.get(term)?.pending.length ?? 0;
export const hasPendingTerminalWriteQueueWork = (term: XTerm): boolean => {
const queue = terminalWriteQueues.get(term);
if (!queue) return false;
return (
queue.writing
|| queue.active !== undefined
|| queue.pending.length > 0
|| queue.pendingBytes > 0
|| queue.drainTimer !== undefined
|| queue.stepTimer !== undefined
|| queue.stepContinuation !== undefined
);
};
export const isTerminalWriteQueueInFloodMode = (term: XTerm): boolean =>
terminalWriteQueues.get(term)?.floodMode ?? false;
export const flushTerminalWriteQueueBypassingTimers = (term: XTerm): boolean => {
let flushed = false;
for (let guard = 0; guard < 4096; guard += 1) {
const queue = terminalWriteQueues.get(term);
if (!queue) return flushed;
if (runDeferredQueueStepNow(queue)) {
flushed = true;
continue;
}
if (runDeferredQueueDrainNow(term, queue)) {
flushed = true;
continue;
}
if (!queue.writing && queue.pending.length > 0) {
scheduleNextTerminalWrite(term, queue);
flushed = true;
continue;
}
return flushed;
}
return flushed;
};
export const enqueueTerminalWrite = (
term: XTerm,
bytes: number,
write: (done: () => void) => void,
options: TerminalWriteQueueOptions = {},
): void => {
const queue = getOrCreateQueue(term);
const dropBytes = Number.isFinite(options.dropBytes)
? Math.max(0, Number(options.dropBytes))
: bytes;
if (options.onDropped) {
queue.onDropped = options.onDropped;
} else if (!queue.onDropped) {
queue.onDropped = terminalWriteQueueDropHandlers.get(term);
}
updateFloodMode(queue, bytes);
queue.pending.push({
bytes,
dropBytes,
steps: [{ bytes, dropBytes, write, yieldAfter: Boolean(options.yieldAfter) }],
nextIndex: 0,
cancelled: false,
yieldAfter: Boolean(options.yieldAfter),
maxDrainBytes: resolveMaxDrainBytes(options.maxDrainBytes),
});
queue.pendingBytes += bytes;
if (
queue.floodMode
|| queue.pending.length >= MAX_WRITE_QUEUE_ITEMS
|| queue.pendingBytes > MAX_WRITE_QUEUE_BYTES
) {
mergePendingWrites(queue);
}
if (!queue.writing) {
scheduleQueueDrain(term, queue, Boolean(options.deferStart));
}
};
/** Drop queued output frames without writing them to xterm. */
export const abortTerminalWriteQueue = (
term: XTerm,
onDropped?: (bytes: number) => void,
): void => {
const queue = terminalWriteQueues.get(term);
if (!queue) return;
let droppedBytes = queue.pending.reduce((sum, item) => sum + item.dropBytes, 0);
if (queue.active) {
queue.active.cancelled = true;
droppedBytes += queue.active.steps
.slice(queue.active.nextIndex)
.reduce((sum, step) => sum + step.dropBytes, 0);
}
queue.pending = [];
queue.pendingBytes = 0;
queue.writing = false;
queue.floodMode = false;
queue.turnStartedAt = 0;
queue.active = undefined;
if (queue.drainTimer) {
clearTimeout(queue.drainTimer);
queue.drainTimer = undefined;
}
if (queue.stepTimer !== undefined) {
clearTimeout(queue.stepTimer);
queue.stepTimer = undefined;
}
queue.stepContinuation = undefined;
terminalWriteQueues.delete(term);
if (droppedBytes > 0) {
(onDropped ?? queue.onDropped)?.(droppedBytes);
}
};