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
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
type QueryRoot = {
|
|
querySelector: (selector: string) => unknown | null;
|
|
};
|
|
|
|
type QueryTarget = QueryRoot & {
|
|
querySelector: (selector: string) => QueryTarget | FocusableTarget | null;
|
|
};
|
|
|
|
type FocusableTarget = {
|
|
focus?: () => void;
|
|
};
|
|
|
|
/** Skip terminal refocus while a Radix dialog is open so deferred tmux actions do not steal modal focus. */
|
|
export const hasOpenAppDialog = (
|
|
doc: QueryRoot | null = typeof document !== "undefined" ? document : null,
|
|
): boolean => {
|
|
if (!doc) return false;
|
|
return doc.querySelector('[role="dialog"][data-state="open"]') !== null;
|
|
};
|
|
|
|
interface FocusTerminalSessionInputOptions {
|
|
document?: QueryTarget | null;
|
|
requestAnimationFrame?: (callback: () => void) => unknown;
|
|
setTimeout?: (callback: () => void, delay: number) => unknown;
|
|
retryDelays?: readonly number[];
|
|
}
|
|
|
|
const escapeAttributeValue = (value: string): string =>
|
|
value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
|
|
export const TERMINAL_SESSION_RESTORE_FOCUS_EVENT = "netcatty:terminal-session-restore-focus";
|
|
|
|
export type TerminalSessionRestoreFocusDetail = {
|
|
sessionId: string;
|
|
};
|
|
|
|
const dispatchTerminalSessionRestoreFocus = (sessionId: string): void => {
|
|
if (typeof window === "undefined") return;
|
|
window.dispatchEvent(new CustomEvent<TerminalSessionRestoreFocusDetail>(
|
|
TERMINAL_SESSION_RESTORE_FOCUS_EVENT,
|
|
{ detail: { sessionId } },
|
|
));
|
|
};
|
|
|
|
export const focusTerminalSessionInput = (
|
|
sessionId: string | null | undefined,
|
|
options: FocusTerminalSessionInputOptions = {},
|
|
): void => {
|
|
if (!sessionId) return;
|
|
|
|
const doc = options.document ?? (typeof document !== "undefined" ? document : null);
|
|
if (!doc) return;
|
|
|
|
const raf = options.requestAnimationFrame
|
|
?? (typeof requestAnimationFrame !== "undefined"
|
|
? requestAnimationFrame
|
|
: (callback: () => void) => {
|
|
callback();
|
|
return undefined;
|
|
});
|
|
const scheduleTimeout = options.setTimeout
|
|
?? (typeof setTimeout !== "undefined"
|
|
? setTimeout
|
|
: (callback: () => void) => {
|
|
callback();
|
|
return undefined;
|
|
});
|
|
const retryDelays = options.retryDelays ?? [50];
|
|
const paneSelector = `[data-session-id="${escapeAttributeValue(sessionId)}"]`;
|
|
|
|
const focusTarget = () => {
|
|
if (hasOpenAppDialog(doc)) {
|
|
dispatchTerminalSessionRestoreFocus(sessionId);
|
|
return;
|
|
}
|
|
|
|
const pane = doc.querySelector(paneSelector) as QueryTarget | null;
|
|
const textarea = pane?.querySelector("textarea.xterm-helper-textarea") as FocusableTarget | null;
|
|
textarea?.focus?.();
|
|
dispatchTerminalSessionRestoreFocus(sessionId);
|
|
};
|
|
|
|
raf(() => {
|
|
focusTarget();
|
|
retryDelays.forEach((delay) => {
|
|
scheduleTimeout(focusTarget, delay);
|
|
});
|
|
});
|
|
};
|