[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:
160
components/terminal/terminalClipboardPaste.ts
Normal file
160
components/terminal/terminalClipboardPaste.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user