[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:
250
components/terminal/hooks/useTerminalContextActions.ts
Normal file
250
components/terminal/hooks/useTerminalContextActions.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import type { Terminal as XTerm } from "@xterm/xterm";
|
||||
import { useCallback } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import { netcattyBridge } from "../../../infrastructure/services/netcattyBridge";
|
||||
import { logger } from "../../../lib/logger";
|
||||
import { pasteTextIntoTerminal } from "../runtime/terminalUserPaste";
|
||||
import { clearTerminalViewportAndSyncPty } from "../clearTerminalViewport";
|
||||
import {
|
||||
handleRemoteClipboardImageUpload,
|
||||
type RemoteClipboardImageUploadResult,
|
||||
} from "../clipboardImagePaste";
|
||||
import { handleTerminalClipboardPaste } from "../terminalClipboardPaste";
|
||||
import { pulseCopyOnSelectUserCommand } from "../copyOnSelect";
|
||||
import { getTerminalSelectionForClipboard } from "../normalizeTerminalSelection";
|
||||
import {
|
||||
getHistoryPreviewSelectionFromRoot,
|
||||
requestHistoryPreviewHide,
|
||||
selectHistoryPreviewAll,
|
||||
findHistoryPreviewOverlay,
|
||||
} from "../runtime/terminalHistoryScrollOverride";
|
||||
|
||||
type BroadcastPasteRefs = {
|
||||
sourceSessionId: string;
|
||||
sessionRef: RefObject<string | null>;
|
||||
isBroadcastEnabledRef?: RefObject<boolean | undefined>;
|
||||
onBroadcastInputRef?: RefObject<((data: string, sourceSessionId: string) => void) | undefined>;
|
||||
passwordPromptActiveRef?: RefObject<boolean | undefined>;
|
||||
};
|
||||
|
||||
export const broadcastTerminalPasteData = (
|
||||
data: string,
|
||||
{
|
||||
sourceSessionId,
|
||||
sessionRef,
|
||||
isBroadcastEnabledRef,
|
||||
onBroadcastInputRef,
|
||||
passwordPromptActiveRef,
|
||||
}: BroadcastPasteRefs,
|
||||
): boolean => {
|
||||
if (
|
||||
passwordPromptActiveRef?.current !== true
|
||||
&& sessionRef.current
|
||||
&& isBroadcastEnabledRef?.current
|
||||
&& onBroadcastInputRef?.current
|
||||
) {
|
||||
onBroadcastInputRef.current(data, sourceSessionId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useTerminalContextActions = ({
|
||||
termRef,
|
||||
sourceSessionId,
|
||||
sessionRef,
|
||||
onHasSelectionChange,
|
||||
scrollOnPasteRef,
|
||||
isBroadcastEnabledRef,
|
||||
onBroadcastInputRef,
|
||||
passwordPromptActiveRef,
|
||||
isLocalConnection,
|
||||
supportsRemoteImagePaste,
|
||||
autoUploadClipboardImageOnPasteRef,
|
||||
clearWipesScrollbackRef,
|
||||
normalizeTextOnCopyRef,
|
||||
terminalBackend,
|
||||
getRemoteCwd,
|
||||
scrollToBottomAfterProgrammaticInput,
|
||||
onClipboardImageUploadResult,
|
||||
}: {
|
||||
termRef: RefObject<XTerm | null>;
|
||||
sourceSessionId: string;
|
||||
sessionRef: RefObject<string | null>;
|
||||
onHasSelectionChange?: (hasSelection: boolean) => void;
|
||||
scrollOnPasteRef?: RefObject<boolean>;
|
||||
isBroadcastEnabledRef?: RefObject<boolean | undefined>;
|
||||
onBroadcastInputRef?: RefObject<((data: string, sourceSessionId: string) => void) | undefined>;
|
||||
passwordPromptActiveRef?: RefObject<boolean | undefined>;
|
||||
isLocalConnection: boolean;
|
||||
supportsRemoteImagePaste: boolean;
|
||||
/** When true, paste auto-uploads a clipboard image (remote sessions only). */
|
||||
autoUploadClipboardImageOnPasteRef?: RefObject<boolean | undefined>;
|
||||
clearWipesScrollbackRef?: RefObject<boolean | undefined>;
|
||||
/** When false, copy uses raw getSelection(). Default true when unset. */
|
||||
normalizeTextOnCopyRef?: RefObject<boolean | undefined>;
|
||||
terminalBackend: {
|
||||
writeToSession: (sessionId: string, data: string, options?: { automated?: boolean }) => void;
|
||||
clearSessionPtyBuffer?: (sessionId: string) => void;
|
||||
};
|
||||
getRemoteCwd?: () => Promise<string | null | undefined>;
|
||||
scrollToBottomAfterProgrammaticInput?: (data: string) => void;
|
||||
onClipboardImageUploadResult?: (result: RemoteClipboardImageUploadResult) => void;
|
||||
}) => {
|
||||
const broadcastUserPasteData = useCallback((data: string) => {
|
||||
return broadcastTerminalPasteData(data, {
|
||||
sourceSessionId,
|
||||
sessionRef,
|
||||
isBroadcastEnabledRef,
|
||||
onBroadcastInputRef,
|
||||
passwordPromptActiveRef,
|
||||
});
|
||||
}, [isBroadcastEnabledRef, onBroadcastInputRef, passwordPromptActiveRef, sessionRef, sourceSessionId]);
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
const term = termRef.current;
|
||||
if (!term) return;
|
||||
const selection = getHistoryPreviewSelectionFromRoot(term.element?.parentElement)
|
||||
|| getTerminalSelectionForClipboard(
|
||||
term,
|
||||
normalizeTextOnCopyRef?.current ?? true,
|
||||
);
|
||||
if (selection) {
|
||||
navigator.clipboard.writeText(selection);
|
||||
}
|
||||
}, [normalizeTextOnCopyRef, termRef]);
|
||||
|
||||
const onPaste = useCallback(async () => {
|
||||
const term = termRef.current;
|
||||
if (!term) return;
|
||||
requestHistoryPreviewHide(term.element?.parentElement);
|
||||
term.focus();
|
||||
try {
|
||||
const bridge = netcattyBridge.get();
|
||||
await handleTerminalClipboardPaste({
|
||||
bridge,
|
||||
autoUploadClipboardImage:
|
||||
supportsRemoteImagePaste && autoUploadClipboardImageOnPasteRef?.current === true,
|
||||
clipboardImageBridge: bridge ?? undefined,
|
||||
getRemoteCwd,
|
||||
isLocalConnection,
|
||||
isSensitiveInput: () => passwordPromptActiveRef?.current === true,
|
||||
onClipboardImageUploadResult,
|
||||
readClipboardText: () => navigator.clipboard.readText(),
|
||||
scrollOnPaste: scrollOnPasteRef?.current ?? false,
|
||||
onPasteData: broadcastUserPasteData,
|
||||
sessionId: sessionRef.current,
|
||||
scrollToBottomAfterProgrammaticInput,
|
||||
terminalBackend,
|
||||
term,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn("Failed to paste from clipboard", err);
|
||||
}
|
||||
}, [
|
||||
autoUploadClipboardImageOnPasteRef,
|
||||
broadcastUserPasteData,
|
||||
getRemoteCwd,
|
||||
isLocalConnection,
|
||||
onClipboardImageUploadResult,
|
||||
passwordPromptActiveRef,
|
||||
sessionRef,
|
||||
supportsRemoteImagePaste,
|
||||
termRef,
|
||||
scrollOnPasteRef,
|
||||
scrollToBottomAfterProgrammaticInput,
|
||||
terminalBackend,
|
||||
]);
|
||||
|
||||
const onUploadClipboardImage = useCallback(async () => {
|
||||
const term = termRef.current;
|
||||
if (!term) return;
|
||||
try {
|
||||
const bridge = netcattyBridge.get();
|
||||
const result = await handleRemoteClipboardImageUpload({
|
||||
bridge,
|
||||
getRemoteCwd: getRemoteCwd ?? (async () => undefined),
|
||||
isSensitiveInput: () => passwordPromptActiveRef?.current === true,
|
||||
sessionId: supportsRemoteImagePaste ? sessionRef.current : null,
|
||||
terminalBackend,
|
||||
term,
|
||||
scrollToBottomAfterProgrammaticInput,
|
||||
});
|
||||
onClipboardImageUploadResult?.(result);
|
||||
} catch (err) {
|
||||
logger.warn("Failed to upload clipboard image", err);
|
||||
onClipboardImageUploadResult?.({ ok: false, reason: "upload-failed" });
|
||||
}
|
||||
}, [
|
||||
getRemoteCwd,
|
||||
passwordPromptActiveRef,
|
||||
onClipboardImageUploadResult,
|
||||
scrollToBottomAfterProgrammaticInput,
|
||||
sessionRef,
|
||||
supportsRemoteImagePaste,
|
||||
termRef,
|
||||
terminalBackend,
|
||||
]);
|
||||
|
||||
const onPasteSelection = useCallback(() => {
|
||||
const term = termRef.current;
|
||||
if (!term) return;
|
||||
const selection = getHistoryPreviewSelectionFromRoot(term.element?.parentElement)
|
||||
|| getTerminalSelectionForClipboard(
|
||||
term,
|
||||
normalizeTextOnCopyRef?.current ?? true,
|
||||
);
|
||||
if (!selection || !sessionRef.current) return;
|
||||
requestHistoryPreviewHide(term.element?.parentElement);
|
||||
term.focus();
|
||||
pasteTextIntoTerminal(term, selection, {
|
||||
scrollOnPaste: scrollOnPasteRef?.current ?? false,
|
||||
onPasteData: broadcastUserPasteData,
|
||||
});
|
||||
}, [broadcastUserPasteData, normalizeTextOnCopyRef, sessionRef, termRef, scrollOnPasteRef]);
|
||||
|
||||
const onSelectAll = useCallback(() => {
|
||||
const term = termRef.current;
|
||||
if (!term) return;
|
||||
pulseCopyOnSelectUserCommand(term);
|
||||
const previewOverlay = findHistoryPreviewOverlay(term.element?.parentElement);
|
||||
if (previewOverlay && selectHistoryPreviewAll(previewOverlay)) {
|
||||
onHasSelectionChange?.(true);
|
||||
return;
|
||||
}
|
||||
term.selectAll();
|
||||
onHasSelectionChange?.(true);
|
||||
}, [onHasSelectionChange, termRef]);
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
const term = termRef.current;
|
||||
if (!term) return;
|
||||
clearTerminalViewportAndSyncPty(term, {
|
||||
wipeScrollback: clearWipesScrollbackRef?.current ?? true,
|
||||
syncPty: () => {
|
||||
const id = sessionRef.current;
|
||||
if (id) {
|
||||
terminalBackend.clearSessionPtyBuffer?.(id);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [clearWipesScrollbackRef, sessionRef, termRef, terminalBackend]);
|
||||
|
||||
const onSelectWord = useCallback(() => {
|
||||
const term = termRef.current;
|
||||
if (!term) return;
|
||||
pulseCopyOnSelectUserCommand(term);
|
||||
term.selectAll();
|
||||
onHasSelectionChange?.(true);
|
||||
}, [onHasSelectionChange, termRef]);
|
||||
|
||||
return {
|
||||
onCopy,
|
||||
onPaste,
|
||||
onUploadClipboardImage: supportsRemoteImagePaste ? onUploadClipboardImage : undefined,
|
||||
onPasteSelection,
|
||||
onSelectAll,
|
||||
onClear,
|
||||
onSelectWord,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user