[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,304 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
COPY_ON_SELECT_USER_GESTURE_RELEASE_MS,
createCopyOnSelectUserGestureTracker,
pulseCopyOnSelectUserCommand,
shouldWriteCopyOnSelect,
subscribeCopyOnSelectUserCommand,
subscribeCopyOnSelectUserGesture,
} from "./copyOnSelect.ts";
test("copy-on-select writes only after a user selection gesture", () => {
assert.equal(shouldWriteCopyOnSelect({
hasText: true,
copyOnSelect: true,
isRestoringSelection: false,
isUserSelection: true,
}), true);
});
test("copy-on-select skips SearchAddon and other programmatic selections", () => {
assert.equal(shouldWriteCopyOnSelect({
hasText: true,
copyOnSelect: true,
isRestoringSelection: false,
isUserSelection: false,
}), false);
});
test("copy-on-select still skips restore and attach snapshots", () => {
assert.equal(shouldWriteCopyOnSelect({
allowCopy: false,
hasText: true,
copyOnSelect: true,
isRestoringSelection: false,
isUserSelection: true,
}), false);
assert.equal(shouldWriteCopyOnSelect({
hasText: true,
copyOnSelect: true,
isRestoringSelection: true,
isUserSelection: true,
}), false);
assert.equal(shouldWriteCopyOnSelect({
hasText: false,
copyOnSelect: true,
isRestoringSelection: false,
isUserSelection: true,
}), false);
assert.equal(shouldWriteCopyOnSelect({
hasText: true,
copyOnSelect: false,
isRestoringSelection: false,
isUserSelection: true,
}), false);
});
test("user gesture stays armed until shortly after pointer-up", () => {
assert.ok(COPY_ON_SELECT_USER_GESTURE_RELEASE_MS < 200);
const scheduled: Array<{ cb: () => void; ms: number }> = [];
const tracker = createCopyOnSelectUserGestureTracker({
setTimeoutFn: ((cb: () => void, ms?: number) => {
scheduled.push({ cb, ms: ms ?? 0 });
return scheduled.length as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout,
clearTimeoutFn: (() => {}) as typeof clearTimeout,
});
assert.equal(tracker.isActive(), false);
tracker.mark();
assert.equal(tracker.isActive(), true);
tracker.release();
assert.equal(tracker.isActive(), true);
assert.equal(scheduled.at(-1)?.ms, COPY_ON_SELECT_USER_GESTURE_RELEASE_MS);
// A later SearchAddon revival (200ms) must not still look like a drag.
scheduled.at(-1)?.cb();
assert.equal(tracker.isActive(), false);
tracker.dispose();
});
test("marking again cancels a pending release so a new drag can copy", () => {
const cleared: number[] = [];
let nextId = 1;
const tracker = createCopyOnSelectUserGestureTracker({
setTimeoutFn: ((cb: () => void) => {
const id = nextId;
nextId += 1;
void cb;
return id as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout,
clearTimeoutFn: ((id: ReturnType<typeof setTimeout>) => {
cleared.push(id as unknown as number);
}) as typeof clearTimeout,
});
tracker.mark();
tracker.release();
tracker.mark();
assert.deepEqual(cleared, [1]);
assert.equal(tracker.isActive(), true);
tracker.dispose();
});
const listenerKey = (
type: string,
options?: boolean | AddEventListenerOptions,
): string => {
const capture = options === true || (
typeof options === "object" && options?.capture === true
);
return `${type}:${capture ? "capture" : "bubble"}`;
};
const createEventTargetStub = () => {
const listeners = new Map<string, Set<EventListener>>();
return {
addEventListener(
type: string,
listener: EventListener,
options?: boolean | AddEventListenerOptions,
) {
const key = listenerKey(type, options);
const set = listeners.get(key) ?? new Set();
set.add(listener);
listeners.set(key, set);
},
removeEventListener(
type: string,
listener: EventListener,
options?: boolean | AddEventListenerOptions,
) {
listeners.get(listenerKey(type, options))?.delete(listener);
},
dispatch(type: string, event: Event, phase: "capture" | "bubble") {
for (const listener of listeners.get(`${type}:${phase}`) ?? []) {
listener(event);
}
},
};
};
const eventOn = (type: string, target: EventTarget): Event => {
const event = new Event(type);
Object.defineProperty(event, "target", { value: target });
return event;
};
test("pointer listeners mark on capture down and release on document up", () => {
const el = createEventTargetStub();
const root = createEventTargetStub();
const view = createEventTargetStub();
let marked = 0;
let pulsed = 0;
let released = 0;
const unsubscribe = subscribeCopyOnSelectUserGesture(
{ element: el },
{
mark: () => {
marked += 1;
},
release: () => {
released += 1;
},
pulse: () => {
pulsed += 1;
},
},
root,
view,
);
root.dispatch("mousedown", eventOn("mousedown", el), "capture");
root.dispatch("mouseup", eventOn("mouseup", el), "bubble");
root.dispatch("contextmenu", eventOn("contextmenu", el), "capture");
root.dispatch("touchcancel", eventOn("touchcancel", el), "bubble");
view.dispatch("blur", eventOn("blur", el), "bubble");
assert.equal(marked, 1);
assert.equal(released, 3);
assert.equal(pulsed, 1);
unsubscribe();
root.dispatch("mousedown", eventOn("mousedown", el), "capture");
root.dispatch("mouseup", eventOn("mouseup", el), "bubble");
root.dispatch("touchcancel", eventOn("touchcancel", el), "bubble");
view.dispatch("blur", eventOn("blur", el), "bubble");
assert.equal(marked, 1);
assert.equal(released, 3);
assert.equal(pulsed, 1);
});
test("late contextmenu after mouseup is a one-shot and then releases", () => {
const scheduled: Array<() => void> = [];
const tracker = createCopyOnSelectUserGestureTracker({
setTimeoutFn: ((cb: () => void) => {
scheduled.push(cb);
return scheduled.length as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout,
clearTimeoutFn: (() => {}) as typeof clearTimeout,
});
tracker.mark();
tracker.release();
// Windows: contextmenu after mouseup used to cancel this timer and stick.
tracker.pulse();
assert.equal(tracker.isActive(), true);
scheduled.at(-1)?.();
assert.equal(tracker.isActive(), false);
tracker.dispose();
});
test("document-capture contextmenu still counts when the terminal never sees the bubble", () => {
const el = createEventTargetStub();
const outside = createEventTargetStub();
const root = createEventTargetStub();
let pulsed = 0;
subscribeCopyOnSelectUserGesture(
{ element: el },
{
mark: () => {},
release: () => {},
pulse: () => {
pulsed += 1;
},
},
root,
);
// tmux/vim capture handler on the container stops the event before
// term.element bubble listeners would run.
root.dispatch("contextmenu", eventOn("contextmenu", el), "capture");
root.dispatch("contextmenu", eventOn("contextmenu", outside), "capture");
assert.equal(pulsed, 1);
});
test("user-invoked Select All pulses only the selected terminal", () => {
let pulsedA = 0;
let pulsedB = 0;
const terminalA = { id: "a" };
const terminalB = { id: "b" };
const unsubscribeA = subscribeCopyOnSelectUserCommand(terminalA, () => {
pulsedA += 1;
});
const unsubscribeB = subscribeCopyOnSelectUserCommand(terminalB, () => {
pulsedB += 1;
});
pulseCopyOnSelectUserCommand(terminalA);
assert.equal(pulsedA, 1);
assert.equal(pulsedB, 0);
unsubscribeA();
unsubscribeB();
pulseCopyOnSelectUserCommand(terminalA);
assert.equal(pulsedA, 1);
assert.equal(pulsedB, 0);
});
test("issue 3007: search match then later revival does not copy", () => {
const scheduled: Array<() => void> = [];
const tracker = createCopyOnSelectUserGestureTracker({
setTimeoutFn: ((cb: () => void) => {
scheduled.push(cb);
return scheduled.length as unknown as ReturnType<typeof setTimeout>;
}) as typeof setTimeout,
clearTimeoutFn: (() => {}) as typeof clearTimeout,
});
// Typing in the search bar selects the match with no terminal pointer.
assert.equal(shouldWriteCopyOnSelect({
hasText: true,
copyOnSelect: true,
isRestoringSelection: false,
isUserSelection: tracker.isActive(),
}), false);
// User drag-selects a docker image id in the buffer.
tracker.mark();
assert.equal(shouldWriteCopyOnSelect({
hasText: true,
copyOnSelect: true,
isRestoringSelection: false,
isUserSelection: tracker.isActive(),
}), true);
tracker.release();
scheduled.at(-1)?.();
// Opening the snippet dialog resizes the terminal; SearchAddon re-selects
// the search term. Clipboard must stay on the image id.
assert.equal(shouldWriteCopyOnSelect({
hasText: true,
copyOnSelect: true,
isRestoringSelection: false,
isUserSelection: tracker.isActive(),
}), false);
tracker.dispose();
});