[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,918 @@
import test from "node:test";
import assert from "node:assert/strict";
import { GhostTextAddon } from "./autocomplete/GhostTextAddon.ts";
type RenderListener = () => void;
type ResizeListener = () => void;
class FakeElement {
public readonly style: Record<string, string> = {};
public textContent = "";
public className = "";
public children: FakeElement[] = [];
appendChild(child: FakeElement): FakeElement {
this.children.push(child);
return child;
}
insertBefore(child: FakeElement, referenceNode: FakeElement | null): FakeElement {
if (!referenceNode) {
this.children.push(child);
return child;
}
const index = this.children.indexOf(referenceNode);
if (index < 0) {
this.children.push(child);
return child;
}
this.children.splice(index, 0, child);
return child;
}
remove(): void {
// No-op for tests.
}
querySelector(selector: string): FakeElement | null {
if (selector === ".xterm-screen") {
return this.children.find((child) => child.className === "xterm-screen") ?? null;
}
return null;
}
}
function installFakeDocument(): () => void {
const previousDocument = globalThis.document;
const fakeDocument = {
createElement() {
return new FakeElement();
},
} as unknown as Document;
Object.defineProperty(globalThis, "document", {
configurable: true,
value: fakeDocument,
});
return () => {
if (previousDocument === undefined) {
delete (globalThis as { document?: Document }).document;
return;
}
Object.defineProperty(globalThis, "document", {
configurable: true,
value: previousDocument,
});
};
}
function createFakeTerm() {
const renderListeners: RenderListener[] = [];
const resizeListeners: ResizeListener[] = [];
const element = new FakeElement();
const screen = new FakeElement();
screen.className = "xterm-screen";
element.appendChild(screen);
const term = {
element,
cols: 80,
rows: 24,
options: {
fontSize: 14,
fontFamily: "monospace",
},
buffer: {
active: {
cursorX: 2,
cursorY: 0,
},
},
_core: {
_renderService: {
dimensions: {
css: {
cell: {
width: 9,
height: 18,
},
},
},
},
},
onRender(listener: RenderListener) {
renderListeners.push(listener);
return {
dispose() {
const index = renderListeners.indexOf(listener);
if (index >= 0) renderListeners.splice(index, 1);
},
};
},
onResize(listener: ResizeListener) {
resizeListeners.push(listener);
return {
dispose() {
const index = resizeListeners.indexOf(listener);
if (index >= 0) resizeListeners.splice(index, 1);
},
};
},
};
return {
term,
ghostElement: () => screen.children[0]?.children[0] ?? null,
fireRender() {
for (const listener of [...renderListeners]) listener();
},
};
}
test("shifts ghost to predicted cursor column as matching input is typed", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("docker", "do");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.style.display, "block");
assert.equal(ghost.textContent, "cker");
// show() anchored at cursorX=2, cell width=9 → left=18.
assert.equal(ghost.style.left, "18px");
addon.adjustToInput("doc");
// After one matching char, the ghost predicts the cursor has moved
// to column 3 and trims "c" from the tail so the next char starts
// where the echo will land. Not waiting for xterm's render keeps
// ghost + real input aligned across SSH echo latency.
assert.equal(ghost.style.display, "block");
assert.equal(ghost.textContent, "ker");
assert.equal(ghost.style.left, "27px");
assert.equal(addon.getGhostText(), "ker");
} finally {
restoreDocument();
}
});
test("walks the anchor column backwards on backspace so the ghost re-aligns", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("docker", "do");
const ghost = ghostElement();
assert.ok(ghost);
addon.adjustToInput("doc");
assert.equal(ghost.textContent, "ker");
assert.equal(ghost.style.left, "27px");
// Backspace below the anchor input — the ghost should shift *left*,
// not stay pinned at the show-time anchor column. Pinning would
// leave a visual gap between the real cursor and the ghost.
addon.adjustToInput("d");
assert.equal(ghost.textContent, "ocker");
// anchor was cursorX=2 captured at show(); "d" is 1 char below
// anchorInputLength=2 → predicted cursor column = 1.
assert.equal(ghost.style.left, "9px");
// Backspace past the anchor back to empty: left is clamped at 0.
addon.adjustToInput("");
assert.equal(ghost.textContent, "docker");
assert.equal(ghost.style.left, "0px");
} finally {
restoreDocument();
}
});
test("advances the anchor by two cells when a CJK glyph is typed", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
// Suggestion starts with a CJK char so the prefix-match survives
// the next keystroke.
addon.show("你好世界", "");
const ghost = ghostElement();
assert.ok(ghost);
// show() anchored at cursorX=2. Input length 0 → delta 0 → left=18.
assert.equal(ghost.style.left, "18px");
addon.adjustToInput("你");
// One CJK char = 2 cells. Predicted col = 2 + 2 = 4 → left 36px.
assert.equal(ghost.textContent, "好世界");
assert.equal(ghost.style.left, "36px");
} finally {
restoreDocument();
}
});
test("wraps the ghost to the next row when the predicted column crosses cols", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
// Shrink the terminal to 10 cols to keep the math obvious. Anchor at
// col 8 with 5 ASCII chars to type → predicted col = 13, which should
// wrap to col 3 of row 1.
term.cols = 10;
term.buffer.active.cursorX = 8;
addon.activate(term as never);
addon.show("abcdefghij", "ab");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.style.top, "0px");
addon.adjustToInput("abcde");
// Predicted col = 8 + (5-2) = 11 → wraps to col 1 on next row.
// cellWidth=9, cellHeight=18.
assert.equal(ghost.textContent, "fghij");
assert.equal(ghost.style.left, "9px");
assert.equal(ghost.style.top, "18px");
} finally {
restoreDocument();
}
});
test("self-heals a stale anchor on render while no adjustToInput has fired", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
// show() captures cursorX=2 — simulate this firing during the
// keystroke→echo gap by later advancing the live cursor and
// verifying the ghost anchor snaps to the echoed position.
addon.show("docker", "do");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.style.left, "18px");
term.buffer.active.cursorX = 5;
fireRender();
// Input hasn't moved from the show-time baseline, so updatePosition
// re-reads live cursor: new left = 5 * 9 = 45px.
assert.equal(ghost.style.left, "45px");
} finally {
restoreDocument();
}
});
test("self-heal adopts live X/Y when echo wraps instead of double-counting", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
// Prompt ends at col 8; four pending cells predict X=12 on a 10-col
// terminal (wraps to col 2 / row 1). When echo lands there, Math.max
// on X alone would keep 12 and paint the ghost on row 2.
term.cols = 10;
term.buffer.active.cursorX = 8;
term.buffer.active.cursorY = 0;
term.buffer.active.baseY = 0;
term.buffer.active.getLine = () => ({
translateToString: () => "$ ",
});
addon.activate(term as never);
addon.show("abcdefghij", "abcd");
const ghost = ghostElement();
assert.ok(ghost);
// Predicted wrap: col 2 on row 1 → left 18px, top 18px.
assert.equal(ghost.style.left, "18px");
assert.equal(ghost.style.top, "18px");
term.buffer.active.cursorX = 2;
term.buffer.active.cursorY = 1;
term.buffer.active.getLine = () => ({
isWrapped: true,
translateToString: () => "abcd",
});
fireRender();
assert.equal(ghost.style.left, "18px");
assert.equal(ghost.style.top, "18px");
} finally {
restoreDocument();
}
});
test("self-heal adopts live X when a bottom-row wrap scrolls instead of changing Y", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
// Bottom-row prediction: prompt at col 8 + 4 pending cells → X=12.
// Echo scrolls the buffer so Y stays on the last row while live X
// becomes the normalized wrap column (2). Math.max would keep 12 and
// paint the ghost one row below the visible screen.
term.cols = 10;
term.rows = 24;
term.buffer.active.cursorX = 8;
term.buffer.active.cursorY = 23;
term.buffer.active.baseY = 0;
term.buffer.active.getLine = () => ({
translateToString: () => "$ ",
});
addon.activate(term as never);
addon.show("abcdefghij", "abcd");
const ghost = ghostElement();
assert.ok(ghost);
// Pre-echo: col 2 on predicted row 24 → top 24*18.
assert.equal(ghost.style.left, "18px");
assert.equal(ghost.style.top, "432px");
term.buffer.active.cursorX = 2;
term.buffer.active.cursorY = 23;
term.buffer.active.getLine = () => ({
isWrapped: true,
translateToString: () => "abcd",
});
fireRender();
assert.equal(ghost.style.left, "18px");
assert.equal(ghost.style.top, "414px");
} finally {
restoreDocument();
}
});
test("anchors ghost after wide pre-echo input when the line has not echoed yet", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
term.buffer.active.baseY = 0;
term.buffer.active.getLine = () => ({
translateToString: () => "$ ",
});
addon.activate(term as never);
// Live cursor still at the prompt; typed "部署" is only in the keystroke buffer.
addon.show("部署脚本", "部署");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.textContent, "脚本");
// cursorX=2 + 4 wide cells → column 6 → left 54px.
assert.equal(ghost.style.left, "54px");
} finally {
restoreDocument();
}
});
test("anchors ghost using only the unechoed suffix after a partial shell echo", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
// Shell has echoed "$ doc"; buffered input is still the full "docker".
term.buffer.active.cursorX = 5;
term.buffer.active.baseY = 0;
term.buffer.active.getLine = () => ({
translateToString: () => "$ doc",
});
addon.activate(term as never);
addon.show("docker compose", "docker");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.textContent, " compose");
// Unechoed "ker" is 3 cells → column 8 → left 72px (not 5+6=11).
assert.equal(ghost.style.left, "72px");
} finally {
restoreDocument();
}
});
test("anchors ghost by cell columns when the prompt is a multi-code-unit emoji", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
// Family ZWJ emoji is many UTF-16 units but only 2 terminal cells.
// A UTF-16 slice(0, cursorX) would stop inside the emoji and treat the
// fully-echoed "docker" as unechoed, shifting the ghost right.
const emoji = "👨‍👩‍👧‍👦";
const cells: Array<{ chars: string; width: number }> = [
{ chars: emoji, width: 2 },
{ chars: " ", width: 1 },
{ chars: "$", width: 1 },
{ chars: " ", width: 1 },
...Array.from("docker", (ch) => ({ chars: ch, width: 1 })),
];
const totalCols = cells.reduce((sum, cell) => sum + cell.width, 0);
term.buffer.active.cursorX = totalCols;
term.buffer.active.baseY = 0;
term.buffer.active.getLine = () => ({
translateToString: (
_trimRight?: boolean,
startColumn = 0,
endColumn = totalCols,
) => {
let col = 0;
let text = "";
for (const cell of cells) {
if (col >= endColumn) break;
if (col >= startColumn) text += cell.chars;
col += cell.width;
}
return text;
},
});
addon.activate(term as never);
addon.show("docker compose", "docker");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.textContent, " compose");
// Fully echoed → stay at live cursor (11 cells → 99px), not 11+6.
assert.equal(ghost.style.left, "99px");
} finally {
restoreDocument();
}
});
test("does not overshoot the ghost when the echoed command already wrapped", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
// cols=10, prompt "$ " + "docker com" wraps; cursor sits on the
// continuation row after a fully-echoed "docker compose".
// Physical rows: "$ docker c" | "ompose|"
term.cols = 10;
term.buffer.active.baseY = 0;
term.buffer.active.cursorY = 1;
term.buffer.active.cursorX = 6;
const lines: Record<number, { isWrapped?: boolean; text: string }> = {
0: { text: "$ docker c" },
1: { isWrapped: true, text: "ompose" },
};
term.buffer.active.getLine = (y: number) => {
const row = lines[y];
if (!row) return undefined;
return {
isWrapped: row.isWrapped,
translateToString: (
_trimRight?: boolean,
startColumn?: number,
endColumn?: number,
) => {
if (startColumn !== undefined && endColumn !== undefined) {
return row.text.padEnd(endColumn).slice(startColumn, endColumn);
}
return row.text;
},
};
};
addon.activate(term as never);
addon.show("docker compose up", "docker compose");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.textContent, " up");
// Fully echoed across the wrap → anchor stays at live cursor (col 6),
// not liveX + full input width.
assert.equal(ghost.style.left, "54px");
assert.equal(ghost.style.top, "18px");
} finally {
restoreDocument();
}
});
test("wraps the ghost to the previous row when deletion crosses a row boundary", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
term.cols = 10;
term.buffer.active.cursorX = 1;
term.buffer.active.cursorY = 1;
addon.activate(term as never);
// Anchored at row 1 col 1 with 5 chars already typed.
addon.show("abcdefghij", "abcde");
const ghost = ghostElement();
assert.ok(ghost);
// Backspace back to 2 chars — delta = -3 across a row boundary.
addon.adjustToInput("ab");
// targetCol = 1 - 3 = -2 → col = 8 (wrapped) on row 0.
assert.equal(ghost.textContent, "cdefghij");
assert.equal(ghost.style.left, "72px");
assert.equal(ghost.style.top, "0px");
} finally {
restoreDocument();
}
});
test("hides ghost immediately when input no longer matches suggestion", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("docker", "do");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.style.display, "block");
addon.adjustToInput("dox");
assert.equal(ghost.style.display, "none");
assert.equal(ghost.textContent, "");
assert.equal(addon.isActive(), false);
} finally {
restoreDocument();
}
});
test("applyKeystroke: printable char trims ghost tail when buffer is unreliable (issue #906)", () => {
// Repro for issue #906: after Tab passes to shell and the typed-buffer
// is flagged unreliable, the ghost addon's currentInput is the only
// source of truth for what the user has typed since the last show().
// Without applyKeystroke, line 798's reliability gate prevents
// adjustToInput from firing and the ghost retains its show-time tail
// — when the next keystroke advances the cursor, the stale tail
// overlaps the just-typed glyph (e.g., typing 't' after 'systemctl s'
// makes the screen read 'systemctl sttop firewalld').
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("systemctl stop firewalld", "systemctl s");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.textContent, "top firewalld");
addon.applyKeystroke("t");
// Ghost tail must shrink by exactly one char so when the shell
// echoes 't', the next visible glyph after the cursor is 'o', not
// 't' (which would render as 'sttop').
assert.equal(ghost.textContent, "op firewalld");
assert.equal(addon.isActive(), true);
} finally {
restoreDocument();
}
});
test("applyKeystroke: backspace re-grows ghost tail by one char", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("docker", "doc");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.textContent, "ker");
addon.applyKeystroke("\x7f");
assert.equal(ghost.textContent, "cker");
} finally {
restoreDocument();
}
});
test("applyKeystroke: Ctrl+W word-erases trailing word from currentInput", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
// Mid-suggestion: user has typed two words; Ctrl+W should drop the
// tail word and let the ghost regrow to cover what was erased.
addon.show("git commit -m wip", "git com");
const ghost = ghostElement();
assert.ok(ghost);
assert.equal(ghost.textContent, "mit -m wip");
addon.applyKeystroke("\x17");
// The same /\s*\S+\s*$/ regex used by handleInput consumes the
// leading whitespace too, so "git com" → "git"; the ghost regrows
// to cover the now-uncovered leading space + remainder.
assert.equal(ghost.textContent, " commit -m wip");
} finally {
restoreDocument();
}
});
test("applyKeystroke: hides ghost when next char diverges from suggestion", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("docker", "do");
const ghost = ghostElement();
assert.ok(ghost);
// 'x' breaks the prefix invariant — ghost must hide immediately so
// a → -accept after this point can't pull a stale tail onto a line
// that no longer matches the suggestion.
addon.applyKeystroke("x");
assert.equal(ghost.style.display, "none");
assert.equal(addon.isActive(), false);
} finally {
restoreDocument();
}
});
test("applyKeystroke: ignores non-typing data (escape sequences, control codes)", () => {
// Escape sequences and other control codes are routed through
// clearState() in handleInput, not propagated to the ghost — but we
// want applyKeystroke to be a safe no-op if accidentally called with
// them (defense in depth).
const restoreDocument = installFakeDocument();
const { term, ghostElement } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("docker", "do");
const ghost = ghostElement();
assert.ok(ghost);
const tailBefore = ghost.textContent;
addon.applyKeystroke("\x1b[A"); // up-arrow escape sequence
addon.applyKeystroke("\x01"); // Ctrl+A
addon.applyKeystroke(""); // empty
assert.equal(ghost.textContent, tailBefore);
assert.equal(addon.isActive(), true);
} finally {
restoreDocument();
}
});
test("hides the ghost on render when the device echoed untracked input (#1013)", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
// We believe only "network in" is typed; suggestion is the full command.
addon.show("network interface show", "network in");
assert.equal(addon.isActive(), true);
// The real line shows MORE than we tracked: a bastion host echoed the
// next char ("t") that our client-side buffer never recorded.
const line = "ecOS# network int";
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = line.length;
active.getLine = () => ({ translateToString: () => line });
fireRender();
assert.equal(addon.isActive(), false);
assert.equal(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});
test("hides the ghost when TopsecOS-style backspace leaves stale text after the cursor (#1060)", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
// The user deleted back from "system..." to "syst", so the tracked
// input is shorter and the ghost regrows to "em license show".
addon.show("system license show", "syst");
assert.equal(addon.isActive(), true);
// TopsecOS devices shown in #1060 leave the old suffix visible after
// the cursor while processing Backspace, so the real buffer looks like:
// TopsecOS# syst|em license show
const beforeCursor = "TopsecOS# syst";
const line = `${beforeCursor}em license show`;
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = beforeCursor.length;
active.getLine = () => ({ translateToString: () => line });
fireRender();
assert.equal(addon.isActive(), false);
assert.equal(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});
test("hides the ghost when only the deleted prefix remains after the cursor (#1060)", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("system license show", "syst");
assert.equal(addon.isActive(), true);
const beforeCursor = "TopsecOS# syst";
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = beforeCursor.length;
active.getLine = () => ({ translateToString: () => `${beforeCursor}em` });
fireRender();
assert.equal(addon.isActive(), false);
assert.equal(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});
test("hides the ghost when TopsecOS-style backspace leaves stale text after empty input (#1060)", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("system license show", "");
assert.equal(addon.isActive(), true);
const beforeCursor = "TopsecOS# ";
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = beforeCursor.length;
active.getLine = () => ({ translateToString: () => `${beforeCursor}system license show` });
fireRender();
assert.equal(addon.isActive(), false);
assert.equal(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});
test("hides the ghost when stale text is followed by right-side status text (#1060)", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("system license show", "syst");
assert.equal(addon.isActive(), true);
const beforeCursor = "TopsecOS# syst";
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = beforeCursor.length;
active.getLine = () => ({ translateToString: () => `${beforeCursor}em 12:34 ok` });
fireRender();
assert.equal(addon.isActive(), false);
assert.equal(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});
test("hides the ghost when a stale argument suffix starts with a space (#1060)", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("system license show", "system");
assert.equal(addon.isActive(), true);
const beforeCursor = "TopsecOS# system";
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = beforeCursor.length;
active.getLine = () => ({ translateToString: () => `${beforeCursor} license show` });
fireRender();
assert.equal(addon.isActive(), false);
assert.equal(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});
test("keeps the ghost when unrelated right-side prompt text is visible", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("system license show", "syst");
assert.equal(addon.isActive(), true);
const beforeCursor = "host# syst";
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = beforeCursor.length;
active.getLine = () => ({ translateToString: () => `${beforeCursor} 12:34 ok` });
fireRender();
assert.equal(addon.isActive(), true);
assert.notEqual(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});
test("keeps the ghost when only right-side spacing overlaps a space-prefixed suffix", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("system license show", "system");
assert.equal(addon.isActive(), true);
const beforeCursor = "host# system";
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = beforeCursor.length;
active.getLine = () => ({ translateToString: () => `${beforeCursor} 12:34 ok` });
fireRender();
assert.equal(addon.isActive(), true);
assert.notEqual(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});
test("keeps the ghost when adjacent right-side text only shares the first character", () => {
const restoreDocument = installFakeDocument();
const { term, ghostElement, fireRender } = createFakeTerm();
const addon = new GhostTextAddon();
try {
addon.activate(term as never);
addon.show("system license show", "syst");
assert.equal(addon.isActive(), true);
const beforeCursor = "host# syst";
const active = term.buffer.active as Record<string, unknown>;
active.baseY = 0;
active.cursorX = beforeCursor.length;
active.getLine = () => ({ translateToString: () => `${beforeCursor}error` });
fireRender();
assert.equal(addon.isActive(), true);
assert.notEqual(ghostElement()?.style.display, "none");
} finally {
addon.dispose();
restoreDocument();
}
});