[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,160 @@
import type { Terminal as XTerm } from "@xterm/xterm";
import {
handleRemoteClipboardImageUpload,
type RemoteClipboardImageBridge,
type RemoteClipboardImageUploadResult,
} from "./clipboardImagePaste";
import { extractRootPathsFromClipboardFiles } from "./terminalHelpers";
import { pasteTextIntoTerminal } from "./runtime/terminalUserPaste";
import { logger } from "../../lib/logger";
/** ASCII Ctrl+V - forwarded so nested TUIs can run their own image-paste bindings. */
export const LOCAL_CLIPBOARD_IMAGE_CTRL_V = "\u0016";
type ClipboardFileBridge = Pick<
Partial<NetcattyBridge>,
"readClipboardFiles" | "hasClipboardImage"
>;
type TerminalClipboardPasteOptions = {
bridge?: ClipboardFileBridge;
/**
* When true, a clipboard image is uploaded to the remote host (SFTP) and
* the remote path is pasted, instead of falling back to a text paste.
* Callers should only enable this for connections that support remote
* image paste; the upload no-ops unless a session id is provided.
*/
autoUploadClipboardImage?: boolean;
clipboardImageBridge?: RemoteClipboardImageBridge;
getRemoteCwd?: () => Promise<string | null | undefined>;
isLocalConnection: boolean;
isSensitiveInput?: () => boolean;
onClipboardImageUploadResult?: (result: RemoteClipboardImageUploadResult) => void;
onPasteData?: (data: string) => boolean | void;
readClipboardText: () => Promise<string>;
scrollOnPaste?: boolean;
scrollToBottomAfterProgrammaticInput?: (data: string) => void;
sessionId: string | null | undefined;
terminalBackend: {
writeToSession: (sessionId: string, data: string, options?: { automated?: boolean; sensitive?: boolean }) => void;
};
term: Pick<XTerm, "paste" | "scrollToBottom"> & Partial<Pick<XTerm, "focus">>;
};
export async function handleTerminalClipboardPaste({
bridge,
autoUploadClipboardImage = false,
clipboardImageBridge,
getRemoteCwd,
isLocalConnection,
isSensitiveInput,
onClipboardImageUploadResult,
onPasteData,
readClipboardText,
scrollOnPaste = false,
scrollToBottomAfterProgrammaticInput,
sessionId,
terminalBackend,
term,
}: TerminalClipboardPasteOptions): Promise<void> {
// Image-first: when enabled and a remote session is active, a clipboard
// image triggers the SFTP upload flow. "no-image" means the clipboard
// holds text/files instead, so we silently continue to the normal paste.
if (autoUploadClipboardImage && sessionId && !isLocalConnection) {
try {
const result = await handleRemoteClipboardImageUpload({
bridge: clipboardImageBridge,
getRemoteCwd: getRemoteCwd ?? (async () => undefined),
isSensitiveInput,
sessionId,
terminalBackend,
term,
scrollToBottomAfterProgrammaticInput,
});
if (result.ok === true) {
onClipboardImageUploadResult?.(result);
return;
}
if (result.reason !== "no-image" && result.reason !== "unsupported") {
onClipboardImageUploadResult?.(result);
return;
}
} catch {
// The clipboard image was already read successfully above, so any throw
// here means the upload itself failed (e.g. SFTP cannot be opened for
// the session). Surface the same error as the context-menu action
// instead of silently pasting unrelated clipboard content.
onClipboardImageUploadResult?.({ ok: false, reason: "upload-failed" });
return;
}
}
const readClipboardFiles = bridge?.readClipboardFiles;
if (isLocalConnection && readClipboardFiles) {
try {
const files = await readClipboardFiles();
if (files.length > 0 && sessionId) {
const paths = extractRootPathsFromClipboardFiles(files);
if (paths.length > 0) {
const pathsText = paths.join(" ");
terminalBackend.writeToSession(sessionId, pathsText, {
sensitive: isSensitiveInput?.() === true,
});
scrollToBottomAfterProgrammaticInput?.(pathsText);
term.focus?.();
return;
}
}
} catch {
// Fall through to text paste.
}
}
let text = "";
try {
text = await readClipboardText();
} catch (error) {
// Text read failed (permissions / image-only clipboard quirks). Treat as
// empty so local image probe can still forward Ctrl+V.
logger.warn("Failed to read clipboard text for terminal paste", error);
}
// Prefer real text paste. Whitespace-only is deferred until after the local
// image probe so screenshot clipboards that also carry blank text/plain can
// still forward Ctrl+V for nested TUIs.
if (text.trim() && sessionId) {
pasteTextIntoTerminal(term, text, {
scrollOnPaste,
onPasteData,
});
return;
}
// Local image-only clipboard: Electron's Edit>Paste turns Ctrl+V into a
// paste event, so TUI apps (Claude Code chat:imagePaste, etc.) never see
// the chord. Forward raw Ctrl+V; the app can then read the OS clipboard
// via xclip/wl-paste. Skip remote sessions - the image is not on the host.
if (isLocalConnection && sessionId && bridge?.hasClipboardImage) {
try {
if (await bridge.hasClipboardImage()) {
terminalBackend.writeToSession(sessionId, LOCAL_CLIPBOARD_IMAGE_CTRL_V, {
sensitive: isSensitiveInput?.() === true,
});
scrollToBottomAfterProgrammaticInput?.(LOCAL_CLIPBOARD_IMAGE_CTRL_V);
term.focus?.();
return;
}
} catch {
// Clipboard probe failed; fall through to whitespace text paste if any.
}
}
// Preserve intentional whitespace-only pastes (indent / newline) when no
// local clipboard image is present.
if (text && sessionId) {
pasteTextIntoTerminal(term, text, {
scrollOnPaste,
onPasteData,
});
}
}