[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
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:
211
components/terminal/autocomplete/terminalAutocompletePrompt.ts
Normal file
211
components/terminal/autocomplete/terminalAutocompletePrompt.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { isSensitiveTerminalChallenge } from "../../../domain/terminalPromptSecurity";
|
||||
import {
|
||||
isNonPromptLine,
|
||||
reconcilePromptWithExternalCommand,
|
||||
type PromptDetectionResult,
|
||||
} from "./promptDetector";
|
||||
import { computeLivePreviewWrite } from "./livePreviewSequence";
|
||||
|
||||
const THEMED_PROMPT_MARKERS = /[❯❮→➜➤⟩»›]/;
|
||||
|
||||
function hasStandardShellPromptTerminator(promptText: string): boolean {
|
||||
return /[$#%>]$/.test(promptText.trimEnd());
|
||||
}
|
||||
|
||||
function isSingleThemedPromptTerminator(promptText: string): boolean {
|
||||
const trimmed = promptText.trim();
|
||||
if (trimmed.length !== 1) return false;
|
||||
const code = trimmed.charCodeAt(0);
|
||||
return THEMED_PROMPT_MARKERS.test(trimmed) || (code >= 0xE000 && code <= 0xF8FF);
|
||||
}
|
||||
|
||||
function isThemedPromptPathToken(token: string): boolean {
|
||||
return (
|
||||
token === "~" ||
|
||||
token.startsWith("~/") ||
|
||||
token.startsWith("/") ||
|
||||
/^[A-Za-z]:[\\/]/.test(token) ||
|
||||
token.includes("\\")
|
||||
);
|
||||
}
|
||||
|
||||
function hasThemedPromptDecorationInInput(prompt: PromptDetectionResult): boolean {
|
||||
const hasThemedPromptMarker =
|
||||
THEMED_PROMPT_MARKERS.test(prompt.promptText) ||
|
||||
Array.from(prompt.promptText).some((ch) => {
|
||||
const code = ch.charCodeAt(0);
|
||||
return code >= 0xE000 && code <= 0xF8FF;
|
||||
});
|
||||
if (hasThemedPromptMarker && hasStandardShellPromptTerminator(prompt.promptText)) {
|
||||
return false;
|
||||
}
|
||||
if (hasThemedPromptMarker && isSingleThemedPromptTerminator(prompt.promptText)) {
|
||||
const firstToken = prompt.userInput.trimStart().match(/^\S+/)?.[0] ?? "";
|
||||
return (
|
||||
(prompt.userInput.startsWith(" ") || isThemedPromptPathToken(firstToken)) &&
|
||||
/\S+\s+\S/.test(prompt.userInput)
|
||||
);
|
||||
}
|
||||
return hasThemedPromptMarker && /\S+\s+\S/.test(prompt.userInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Command-line text used for autocomplete matching (popup / ghost).
|
||||
*
|
||||
* Enter recording keeps a stricter echo-alignment policy so short lagging
|
||||
* prefixes are not committed as history. Autocomplete can safely prefer the
|
||||
* reliable keystroke buffer when it is ahead of the remote shell echo —
|
||||
* otherwise high-latency SSH drops local history/fig matches until the user
|
||||
* pauses and the echo catches up (#2830).
|
||||
*/
|
||||
export function resolveAutocompleteQueryInput(
|
||||
prompt: PromptDetectionResult,
|
||||
typedBuffer: string,
|
||||
typedBufferReliable: boolean,
|
||||
): string | null {
|
||||
if (!prompt.isAtPrompt) return null;
|
||||
|
||||
// Prefer the keystroke buffer when it is reliably aligned with the remote
|
||||
// echo as a shared prefix in either direction:
|
||||
// - buffer ahead of echo (typing faster than SSH echo)
|
||||
// - echo ahead of buffer (partial/full backspace while echo still lags)
|
||||
// Without the second case, a lagging echo of deleted characters would keep
|
||||
// driving completions/accept (e.g. typed `gi` + echo `git` → accept
|
||||
// ` status` → remote `gi status`). An unreliable empty buffer is different:
|
||||
// history recall / cursor moves clear the buffer without meaning the line
|
||||
// is empty, so fall through to prompt.userInput there.
|
||||
if (
|
||||
typedBufferReliable &&
|
||||
(typedBuffer.startsWith(prompt.userInput) ||
|
||||
prompt.userInput.startsWith(typedBuffer))
|
||||
) {
|
||||
return typedBuffer;
|
||||
}
|
||||
|
||||
return prompt.userInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an in-flight completion result still belongs to the active query.
|
||||
*
|
||||
* Live preview rewrites the typed buffer to the highlighted candidate, so a
|
||||
* naive `currentInput === queryInput` check would drop late path listings
|
||||
* while a preview row remains selected.
|
||||
*/
|
||||
export function isSameAutocompleteQuery(options: {
|
||||
queryInput: string;
|
||||
currentInput: string | null;
|
||||
previewActive: boolean;
|
||||
previewBaseline: string;
|
||||
}): boolean {
|
||||
if (options.currentInput === null) return false;
|
||||
if (options.currentInput === options.queryInput) return true;
|
||||
return options.previewActive && options.previewBaseline === options.queryInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether fetchSuggestions must refuse to query/render for an already-known
|
||||
* sensitive line (host latch or auth-challenge prompt text).
|
||||
*
|
||||
* This is *not* a substitute for the empty-echo / `allowExternalProviders:
|
||||
* false` wait in useTerminalAutocomplete: `read -s -p '$ '` still looks like
|
||||
* a normal shell PS1 until echo validates, so that path stays fail-closed
|
||||
* separately (#2814).
|
||||
*/
|
||||
export function shouldBlockAutocompleteForSensitivePrompt(options: {
|
||||
sensitiveInputActive: boolean;
|
||||
promptText: string;
|
||||
}): boolean {
|
||||
if (options.sensitiveInputActive) return true;
|
||||
return isSensitiveTerminalChallenge(options.promptText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keystrokes that rewrite the remote command line to `candidate`.
|
||||
*
|
||||
* Must use the same echo-lag-aware baseline as suggestion matching: the remote
|
||||
* shell already has the typed buffer, even when local echo still shows a short
|
||||
* prefix. Using lagging `prompt.userInput` here would append a duplicate tail
|
||||
* (e.g. typed `systemctl` + echo `s` + accept → send `ystemctl …`).
|
||||
*/
|
||||
export function computeAutocompleteAcceptWrite(options: {
|
||||
prompt: PromptDetectionResult;
|
||||
typedBuffer: string;
|
||||
typedBufferReliable: boolean;
|
||||
candidate: string;
|
||||
os: string;
|
||||
execute?: boolean;
|
||||
allowLineReplacement?: boolean;
|
||||
}): string | null {
|
||||
const currentLine = resolveAutocompleteQueryInput(
|
||||
options.prompt,
|
||||
options.typedBuffer,
|
||||
options.typedBufferReliable,
|
||||
);
|
||||
if (currentLine === null) return null;
|
||||
|
||||
const allowLineReplacement = options.allowLineReplacement !== false;
|
||||
if (
|
||||
!options.candidate.startsWith(currentLine) &&
|
||||
!allowLineReplacement
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = computeLivePreviewWrite({
|
||||
currentLine,
|
||||
candidate: options.candidate,
|
||||
os: options.os,
|
||||
promptText: options.prompt.promptText,
|
||||
});
|
||||
if (!options.execute) return body;
|
||||
return body ? `${body}\r` : "\r";
|
||||
}
|
||||
|
||||
export function getCommandToRecordOnEnter(
|
||||
livePrompt: PromptDetectionResult,
|
||||
alignedTyped: string | null,
|
||||
typedBuffer: string,
|
||||
typedBufferReliable: boolean,
|
||||
): string | null {
|
||||
if (!livePrompt.isAtPrompt) return null;
|
||||
const alignedCommand = alignedTyped?.trim();
|
||||
if (alignedCommand) return alignedCommand;
|
||||
|
||||
const reliableTypedCommand = typedBufferReliable ? typedBuffer.trim() : "";
|
||||
if (reliableTypedCommand) {
|
||||
const reconciledPrompt = reconcilePromptWithExternalCommand(
|
||||
livePrompt,
|
||||
reliableTypedCommand,
|
||||
);
|
||||
if (reconciledPrompt) return reliableTypedCommand;
|
||||
}
|
||||
|
||||
const liveCommand = livePrompt.userInput.trim();
|
||||
if (!liveCommand && reliableTypedCommand) {
|
||||
return isNonPromptLine(`${livePrompt.promptText}${reliableTypedCommand}`)
|
||||
? null
|
||||
: reliableTypedCommand;
|
||||
}
|
||||
if (!liveCommand) return null;
|
||||
if (!typedBufferReliable && hasThemedPromptDecorationInInput(livePrompt)) return null;
|
||||
|
||||
const liveInputMayIncludePromptDecoration =
|
||||
typedBufferReliable &&
|
||||
typedBuffer.trim().length > 0 &&
|
||||
liveCommand !== typedBuffer.trim() &&
|
||||
liveCommand.endsWith(typedBuffer.trim());
|
||||
if (liveInputMayIncludePromptDecoration) return null;
|
||||
|
||||
const liveInputMayBeLagging =
|
||||
typedBufferReliable &&
|
||||
typedBuffer.trim().length > 0 &&
|
||||
typedBuffer.length > livePrompt.userInput.length &&
|
||||
typedBuffer.startsWith(livePrompt.userInput);
|
||||
if (liveInputMayBeLagging) return null;
|
||||
|
||||
if (typedBufferReliable && hasThemedPromptDecorationInInput(livePrompt)) return null;
|
||||
|
||||
return liveCommand;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user