[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,278 @@
import type { MutableRefObject, RefObject } from "react";
import type { Terminal as XTerm } from "@xterm/xterm";
import type { GhostTextAddon } from "./GhostTextAddon";
import type { AutocompleteSettings } from "./useTerminalAutocomplete";
import { getAlignedPrompt } from "./promptDetector";
import { recordCommand } from "./commandHistoryStore";
import { getCommandToRecordOnEnter } from "./terminalAutocompletePrompt";
interface TerminalAutocompleteInputContext {
settingsRef: MutableRefObject<AutocompleteSettings>;
lastKeystrokeRef: MutableRefObject<number>;
suppressNextEnterRecordRef: MutableRefObject<boolean>;
lastAcceptedCommandRef: MutableRefObject<string | null>;
typedInputBufferRef: MutableRefObject<string>;
typedBufferReliableRef: MutableRefObject<boolean>;
previewBaselineRef: MutableRefObject<string>;
previewActiveRef: MutableRefObject<boolean>;
termRef: RefObject<XTerm | null>;
hostIdRef: MutableRefObject<string>;
hostOsRef: MutableRefObject<"linux" | "windows" | "macos">;
ghostAddonRef: MutableRefObject<GhostTextAddon | null>;
debounceTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>;
clearState: () => void;
syncPopupToInput: (input: string | null) => void;
fetchSuggestions: () => void | Promise<void>;
}
export function handleTerminalAutocompleteInput(
data: string,
context: TerminalAutocompleteInputContext,
): void {
const {
settingsRef,
lastKeystrokeRef,
suppressNextEnterRecordRef,
lastAcceptedCommandRef,
typedInputBufferRef,
typedBufferReliableRef,
previewBaselineRef,
previewActiveRef,
termRef,
hostIdRef,
hostOsRef,
ghostAddonRef,
debounceTimerRef,
clearState,
syncPopupToInput,
fetchSuggestions,
} = context;
if (!settingsRef.current.enabled) {
return;
}
const now = Date.now();
const timeSinceLastKeystroke = now - lastKeystrokeRef.current;
lastKeystrokeRef.current = now;
// Command recording: Enter key
if (data === "\r" || data === "\n") {
// Skip recording if selectAndExecute already recorded this command
if (suppressNextEnterRecordRef.current) {
suppressNextEnterRecordRef.current = false;
} else {
// If user accepted a completion (Tab/→) and immediately pressed Enter,
// the buffer may not reflect the accepted text yet. Use the tracked value.
if (lastAcceptedCommandRef.current) {
recordCommand(lastAcceptedCommandRef.current, hostIdRef.current, hostOsRef.current);
} else {
// Require a live prompt before trusting either keystroke buffer
// or buffer-based detection — otherwise sudo password Enter
// would record the typed password as a command.
const typedBuffer = typedInputBufferRef.current;
const typedBufferReliable = typedBufferReliableRef.current;
const { prompt: livePrompt, alignedTyped } = getAlignedPrompt(
termRef.current,
typedBuffer,
typedBufferReliable,
);
const commandToRecord = getCommandToRecordOnEnter(
livePrompt,
alignedTyped,
typedBuffer,
typedBufferReliable,
);
if (commandToRecord) {
recordCommand(commandToRecord, hostIdRef.current, hostOsRef.current);
}
}
lastAcceptedCommandRef.current = null;
}
typedInputBufferRef.current = "";
typedBufferReliableRef.current = true;
clearState();
return;
}
// Ctrl+C, Ctrl+U — clear. These kill the zle line entirely, so the
// buffer is once again a true reflection of the (empty) line.
if (data === "\x03" || data === "\x15") {
typedInputBufferRef.current = "";
typedBufferReliableRef.current = true;
// Same rationale as the ctrl/escape early returns below: any
// previously-accepted suggestion is gone from the line too, so
// accept → Ctrl-C → type "foo" → Enter must not log the stale
// accepted command via the Enter fast path.
lastAcceptedCommandRef.current = null;
clearState();
return;
}
// Backspace / DEL: drop the last typed char so the buffer stays aligned
// with what the shell actually holds.
if (data === "\x7f" || data === "\b") {
typedInputBufferRef.current = typedInputBufferRef.current.slice(0, -1);
} else if (data === "\x17") {
// Ctrl+W: word-erase — kill the trailing whitespace + word.
typedInputBufferRef.current = typedInputBufferRef.current.replace(/\s*\S+\s*$/, "");
} else if (data.startsWith("\x1b[200~")) {
// Bracketed paste: "\x1b[200~...\x1b[201~". The inner bytes are
// literal input, so newlines stay on the zle line instead of
// executing each segment — meaning we must preserve the whole
// content in the buffer, not just the post-final-newline tail
// (Codex #814 P2).
//
// Reliability is *inherited*, not reset: if the buffer was
// already aligned with the line (reliable=true), appending this
// paste keeps it aligned; if the buffer was unreliable (e.g.
// after ↑ recalled a history command so line ≠ buffer), the
// paste only extends the tail but the head is still whatever
// the shell had, so the buffer stays unreliable. Without this,
// a paste-after-recall flow would flip reliability back on and
// Enter would record just the pasted suffix as the command
// (Codex #814 P1 follow-up).
const endIdx = data.indexOf("\x1b[201~");
const content = endIdx >= 0
? data.slice("\x1b[200~".length, endIdx)
: data.slice("\x1b[200~".length);
typedInputBufferRef.current += content;
// Paste extends the line past whatever was accepted, so the
// Enter fast-path must not record the pre-paste accepted
// command — mirrors the non-bracketed paste branch below.
lastAcceptedCommandRef.current = null;
clearState();
return;
} else if (data.startsWith("\x1b") && data !== "\x1b") {
// Cursor-movement / function keys — we lose track of where the
// cursor sits relative to our append-only buffer. Mark the
// buffer unreliable and drop it; detectPrompt takes over until
// the next Enter / Ctrl-C / Ctrl-U.
typedInputBufferRef.current = "";
typedBufferReliableRef.current = false;
} else if (data.length === 1 && data.charCodeAt(0) >= 32) {
typedInputBufferRef.current += data;
} else if (data.length > 1 && !data.startsWith("\x1b")) {
// Paste chunk. Any \r / \n inside executes the preceding text as
// a command in the shell, so keeping the pre-newline portion in
// our buffer would leave stale content that a later Enter could
// record (Codex #814 P2). Drop everything up to and including
// the last terminator and keep only the tail as new content.
// Intermediate executed lines aren't synthesized back into
// recordCommand here — the onCommandExecuted path in
// createXTermRuntime still captures them independently.
const lastCR = data.lastIndexOf("\r");
const lastLF = data.lastIndexOf("\n");
const nlIdx = Math.max(lastCR, lastLF);
if (nlIdx >= 0) {
typedInputBufferRef.current = data.slice(nlIdx + 1);
typedBufferReliableRef.current = true;
// The embedded newline flushed any previously-accepted
// suggestion too — clearing the cache here prevents the next
// Enter from falling into the lastAcceptedCommandRef fast path
// and recording that stale command.
lastAcceptedCommandRef.current = null;
clearState();
return;
}
typedInputBufferRef.current += data;
} else if (data.length === 1 && data.charCodeAt(0) < 32) {
// Any other single control char (Ctrl-A, Ctrl-E, Ctrl-B, Ctrl-F,
// Ctrl-R, Ctrl-P, Ctrl-N, ...) moves the cursor or swaps the
// line in ways this append-only buffer can't follow. Same story
// as escape sequences above — and hide the ghost too, so the
// unreliable-accept fallback doesn't pull a stale tail onto a
// recalled line (Codex #815 follow-up).
typedInputBufferRef.current = "";
typedBufferReliableRef.current = false;
// Null the fast-path accepted-command cache: accept-then-Ctrl-R
// should not let an old accepted command sneak back in via the
// Enter fast path after reverse-search picks a different one.
lastAcceptedCommandRef.current = null;
clearState();
return;
}
// Escape sequences (arrow keys, Home, End, etc.): clear stale suggestions
// since cursor position may have changed, making current suggestions invalid.
// Up/Down/Right/Tab are handled by handleKeyEvent; other sequences land here.
if (data.startsWith("\x1b") && data !== "\x1b") {
// Same fast-path reset as the single-byte ctrl-char branch above —
// accept-then-↑/↓ must not record the stale accepted command if
// the user then presses Enter on a different recalled line.
lastAcceptedCommandRef.current = null;
clearState();
return;
}
// User is typing more — invalidate accepted command fallback since the
// command is being edited further (e.g., accepted "git status" then added " --short")
lastAcceptedCommandRef.current = null;
// The previewed candidate is now edited, so the line is the user's own
// text. Drop preview-active so Escape dismisses the popup without
// reverting these edits back to the stale baseline (#1005).
previewActiveRef.current = false;
if (typedBufferReliableRef.current) {
previewBaselineRef.current = typedInputBufferRef.current;
}
// The popup must follow the edited line immediately, before the debounced
// provider refresh runs. Reconcile stale history rows against the current
// input; an unreliable append-only buffer cannot validate any old row.
if (settingsRef.current.showPopupMenu) {
const currentInput = typedBufferReliableRef.current
? typedInputBufferRef.current
: null;
syncPopupToInput(
currentInput !== null && currentInput.length >= settingsRef.current.minChars
? currentInput
: null,
);
}
// Re-align any visible ghost text to the freshly-updated buffer
// immediately. Without this the ghost keeps the tail it captured at
// show() time; a fast "type + press →" sequence then pastes the
// pre-update tail on top of the new input ("doc" + "cker ls" →
// "doccker ls"). Skip when the user has turned showGhostText off
// mid-session: otherwise a ghost that was active before the toggle
// would keep moving around under a setting the user just said to
// disable (Codex #815 P2).
//
// Reliable buffer: feed adjustToInput the full post-mutation buffer
// so multi-char pastes refresh the ghost as one batch. Unreliable
// buffer (post Tab / cursor-move / history recall): the buffer
// is just the suffix typed since unreliability began, so feeding
// it to adjustToInput would fail the prefix invariant and hide
// the ghost. Instead let the addon evolve its own currentInput
// off the keystroke directly (issue #906) — that input was seeded
// by the last show() with the live xterm reading, which is the
// only post-Tab source-of-truth we have.
if (settingsRef.current.showGhostText) {
if (typedBufferReliableRef.current) {
ghostAddonRef.current?.adjustToInput(typedInputBufferRef.current);
} else {
ghostAddonRef.current?.applyKeystroke(data);
}
}
// Fast typing suppression: if typing faster than threshold, skip this debounce cycle
const isFastTyping = timeSinceLastKeystroke < settingsRef.current.fastTypingThresholdMs;
// Debounced suggestion fetch
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (isFastTyping) {
// Still debounce, but with a longer delay to wait for typing to pause
debounceTimerRef.current = setTimeout(() => {
debounceTimerRef.current = null;
void fetchSuggestions();
}, settingsRef.current.debounceMs * 3);
} else {
debounceTimerRef.current = setTimeout(() => {
debounceTimerRef.current = null;
void fetchSuggestions();
}, settingsRef.current.debounceMs);
}
}