mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
c6baebd514
* Support password in ssh deep links with ephemeral connect ssh://user:password@host URLs now connect directly using the URL credentials without saving the host to the vault, covering bastion one-time password flows. Password-less links keep the existing match-or-prefill behavior. Fixes #1890 Co-authored-by: Cursor <cursoragent@cursor.com> * Keep ephemeral deep-link hosts out of vault update paths Terminal-layer host updates (e.g. the SFTP follow-CWD toggle) map over the merged terminal host list, which could persist an ephemeral host and its plaintext password into the vault. Route terminal-layer host updates through a splitter that applies ephemeral entries to in-memory state only and forwards just the vault hosts to updateHosts. Co-authored-by: Cursor <cursoragent@cursor.com> * Exclude ephemeral deep-link sessions from session restore Sessions backed by an in-memory ephemeral host cannot survive a relaunch (the one-time password only lives in memory), so persisting them would leak the ephemeral host metadata into restore storage and restore a tab that can never reconnect. Mark ephemeral hosts and their sessions, and filter them out of the session-restore payload. Co-authored-by: Cursor <cursoragent@cursor.com> * Treat ephemeral hosts as unsaved in appearance classification The terminal theme/font panel and pane appearance resolution decided "saved vault host" via hostMap membership. Ephemeral deep-link hosts are now in that map, so theme/font changes were routed to the persisted-host updater and silently dropped. Classify via isSavedVaultHost (ephemeral flag aware) so those changes fall back to the session/global setting path as before. Co-authored-by: Cursor <cursoragent@cursor.com> * Reuse saved host settings for password deep links and keep ephemeral marker on clones Password deep links now run saved-host matching first: a uniquely matched vault host contributes its non-credential settings (proxy, jump chain, charset, group, ...) to the ephemeral host while the URL credentials override vault identities and key references. Session clones (split/copy/copy-to-new-window) now carry the ephemeralHost marker so cloned tabs stay excluded from session restore. Co-authored-by: Cursor <cursoragent@cursor.com> * Block group credential inheritance on password deep-link ephemeral hosts buildSshDeepLinkEphemeralHostFromSaved now takes the group-resolved effective host and clears group/identityFilePaths in addition to identity references, so a later resolveEffectiveHost pass cannot re-inherit group default credentials (identity, key, password) over the one-time URL password. Non-credential group defaults (proxy, jump chain, charset, ...) are preserved via the effective host. Co-authored-by: Cursor <cursoragent@cursor.com> * Route ephemeral-host font zoom through the per-session font-size path Ctrl+zoom on a non-workspace ephemeral deep-link session hit the saved-host update path, where the vault updater silently dropped the change (ephemeral ids are not in the persisted hosts array). Zoom and the theme panel font-size slider/reset now store the size on the session (rendered via applySessionFontSizeToHost) for ephemeral hosts, matching the workspace-pane behavior. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix ephemeral session font zoom routing * Prevent ephemeral deep-link sudo autofill --------- Co-authored-by: Cursor <cursoragent@cursor.com>
312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
import { startTransition, useCallback, useEffect, useMemo } from 'react';
|
|
|
|
import type { ResolvedAppearance, TerminalAppearanceHostScope } from '../../domain/terminalAppearanceRuntime';
|
|
import {
|
|
clearHostFontFamilyOverride,
|
|
clearHostFontSizeOverride,
|
|
clearHostFontWeightOverride,
|
|
clearHostThemeOverride,
|
|
hasHostFontFamilyOverride,
|
|
hasHostFontSizeOverride,
|
|
hasHostFontWeightOverride,
|
|
hasHostThemeOverride,
|
|
resolveHostTerminalFontFamilyId,
|
|
resolveHostTerminalFontSize,
|
|
resolveHostTerminalFontWeight,
|
|
resolveHostTerminalThemeId,
|
|
} from '../../domain/terminalAppearance';
|
|
import { isSavedVaultHost } from '../../domain/ephemeralHosts';
|
|
import { isSameResolvedTerminalFont } from '../../infrastructure/config/fonts';
|
|
import type { Host, TerminalSession, TerminalTheme, Workspace } from '../../types';
|
|
import { getScopedTopTabsThemeId } from '../terminalTopTabsTheme';
|
|
import type { SidePanelTab } from './TerminalLayerSupport';
|
|
|
|
const navigatorPlatform = typeof navigator !== 'undefined' ? navigator.platform : '';
|
|
|
|
interface UseTerminalThemePanelStateOptions {
|
|
accentMode: 'theme' | 'custom';
|
|
activeSession: TerminalSession | undefined;
|
|
activeSidePanelTab: SidePanelTab | null;
|
|
activeWorkspace: Workspace | undefined;
|
|
clearIntent: () => void;
|
|
customAccent: string;
|
|
followAppTerminalTheme: boolean;
|
|
focusedSessionId: string | undefined;
|
|
fontSize: number;
|
|
hostMap: Map<string, Host>;
|
|
isSidePanelOpenForCurrentTab: boolean;
|
|
isVisible: boolean;
|
|
onUpdateHost: (host: Host) => void;
|
|
onUpdateTerminalFontFamilyId?: (fontFamilyId: string) => void;
|
|
onUpdateTerminalFontSize?: (fontSize: number) => void;
|
|
onUpdateTerminalFontWeight?: (fontWeight: number) => void;
|
|
onUpdateTerminalThemeId?: (themeId: string) => void;
|
|
onUpdateSessionFontSize?: (sessionId: string, fontSize: number) => void;
|
|
onClearSessionFontSizeOverride?: (sessionId: string) => void;
|
|
pickTheme: (themeId: string) => void;
|
|
resolveFocusedAppearance: (hostScope: TerminalAppearanceHostScope) => ResolvedAppearance;
|
|
sessionHostsMap: Map<string, Host>;
|
|
terminalFontFamilyId: string;
|
|
terminalSettings?: { fontWeight?: number };
|
|
terminalTheme: TerminalTheme;
|
|
}
|
|
|
|
export function useTerminalThemePanelState({
|
|
activeSession,
|
|
activeSidePanelTab,
|
|
activeWorkspace,
|
|
clearIntent,
|
|
followAppTerminalTheme,
|
|
focusedSessionId,
|
|
fontSize,
|
|
hostMap,
|
|
isSidePanelOpenForCurrentTab,
|
|
isVisible,
|
|
onUpdateHost,
|
|
onUpdateTerminalFontFamilyId,
|
|
onUpdateTerminalFontSize,
|
|
onUpdateTerminalFontWeight,
|
|
onUpdateTerminalThemeId,
|
|
onUpdateSessionFontSize,
|
|
onClearSessionFontSizeOverride,
|
|
pickTheme,
|
|
resolveFocusedAppearance,
|
|
sessionHostsMap,
|
|
terminalFontFamilyId,
|
|
terminalSettings,
|
|
terminalTheme,
|
|
}: UseTerminalThemePanelStateOptions) {
|
|
useEffect(() => {
|
|
if (isSidePanelOpenForCurrentTab) {
|
|
if (!followAppTerminalTheme && activeSidePanelTab !== 'theme') {
|
|
clearIntent();
|
|
}
|
|
return;
|
|
}
|
|
clearIntent();
|
|
}, [activeSidePanelTab, clearIntent, followAppTerminalTheme, isSidePanelOpenForCurrentTab]);
|
|
|
|
const focusedHost = useMemo((): Host | null => {
|
|
if (activeWorkspace && focusedSessionId) {
|
|
return sessionHostsMap.get(focusedSessionId) ?? null;
|
|
}
|
|
if (activeSession) {
|
|
return sessionHostsMap.get(activeSession.id) ?? null;
|
|
}
|
|
return null;
|
|
}, [activeWorkspace, focusedSessionId, activeSession, sessionHostsMap]);
|
|
|
|
const isFocusedHostLocal = useMemo(() => {
|
|
return focusedHost?.protocol === 'local' || !!focusedHost?.id?.startsWith('local-');
|
|
}, [focusedHost]);
|
|
|
|
const isFocusedHostEphemeral = useMemo(() => {
|
|
if (isFocusedHostLocal) return true;
|
|
if (!focusedHost) return true;
|
|
return !isSavedVaultHost(hostMap.get(focusedHost.id));
|
|
}, [focusedHost, isFocusedHostLocal, hostMap]);
|
|
|
|
const rawFocusedHost = useMemo(() => {
|
|
if (!focusedHost) return null;
|
|
return hostMap.get(focusedHost.id) ?? null;
|
|
}, [focusedHost, hostMap]);
|
|
|
|
const focusedHostScope = useMemo((): TerminalAppearanceHostScope => ({
|
|
host: focusedHost,
|
|
isEphemeral: isFocusedHostEphemeral,
|
|
}), [focusedHost, isFocusedHostEphemeral]);
|
|
|
|
const focusedAppearance = useMemo(
|
|
() => resolveFocusedAppearance(focusedHostScope),
|
|
[resolveFocusedAppearance, focusedHostScope],
|
|
);
|
|
|
|
const previewTargetSessionId = activeWorkspace?.focusedSessionId ?? activeSession?.id ?? null;
|
|
|
|
const focusedThemeId = resolveHostTerminalThemeId(focusedHost, terminalTheme.id);
|
|
const focusedFontFamilyId = resolveHostTerminalFontFamilyId(focusedHost, terminalFontFamilyId);
|
|
const focusedFontSize = resolveHostTerminalFontSize(focusedHost, fontSize);
|
|
const focusedThemeOverridden = hasHostThemeOverride(focusedHost);
|
|
const focusedFontFamilyOverridden = hasHostFontFamilyOverride(focusedHost);
|
|
const focusedFontSizeOverridden = hasHostFontSizeOverride(focusedHost);
|
|
const focusedFontWeight = resolveHostTerminalFontWeight(focusedHost, terminalSettings?.fontWeight ?? 400);
|
|
const focusedFontWeightOverridden = hasHostFontWeightOverride(focusedHost);
|
|
const visibleFocusedThemeId = followAppTerminalTheme ? terminalTheme.id : focusedThemeId;
|
|
const listSelectedThemeId = followAppTerminalTheme
|
|
? terminalTheme.id
|
|
: focusedAppearance.themeId;
|
|
const previewedOrVisibleThemeId = listSelectedThemeId;
|
|
const resolvedPreviewTheme = focusedAppearance.theme;
|
|
|
|
const activeTopTabsThemeId = useMemo(
|
|
() => getScopedTopTabsThemeId({
|
|
activeSidePanelTab,
|
|
activeThemePreviewId: null,
|
|
activeWorkspace,
|
|
followAppTerminalTheme,
|
|
isVisible,
|
|
previewTargetSessionId,
|
|
previewedOrVisibleThemeId,
|
|
resolveSessionThemeId: (sessionId) => {
|
|
const host = sessionHostsMap.get(sessionId) ?? null;
|
|
const isEphemeral = !host || !isSavedVaultHost(hostMap.get(host.id));
|
|
return resolveFocusedAppearance({ host, isEphemeral }).themeId;
|
|
},
|
|
}),
|
|
[
|
|
activeSidePanelTab,
|
|
activeWorkspace,
|
|
followAppTerminalTheme,
|
|
hostMap,
|
|
isVisible,
|
|
previewTargetSessionId,
|
|
previewedOrVisibleThemeId,
|
|
resolveFocusedAppearance,
|
|
sessionHostsMap,
|
|
],
|
|
);
|
|
|
|
const handleThemeChangeForFocusedSession = useCallback((themeId: string) => {
|
|
if (themeId === listSelectedThemeId) return;
|
|
if (!focusedHost && !followAppTerminalTheme) return;
|
|
|
|
if (followAppTerminalTheme) {
|
|
pickTheme(themeId);
|
|
return;
|
|
}
|
|
|
|
pickTheme(themeId, {
|
|
followApp: false,
|
|
scopeHostId: rawFocusedHost?.id ?? focusedHost?.id ?? null,
|
|
});
|
|
if (isFocusedHostEphemeral) {
|
|
onUpdateTerminalThemeId?.(themeId);
|
|
} else if (rawFocusedHost) {
|
|
onUpdateHost({ ...rawFocusedHost, theme: themeId, themeOverride: true });
|
|
}
|
|
}, [
|
|
focusedHost,
|
|
followAppTerminalTheme,
|
|
isFocusedHostEphemeral,
|
|
listSelectedThemeId,
|
|
onUpdateHost,
|
|
onUpdateTerminalThemeId,
|
|
pickTheme,
|
|
rawFocusedHost,
|
|
]);
|
|
|
|
const handleThemeResetForFocusedSession = useCallback(() => {
|
|
clearIntent();
|
|
if (!focusedHost || isFocusedHostEphemeral || !rawFocusedHost) return;
|
|
onUpdateHost(clearHostThemeOverride(rawFocusedHost));
|
|
}, [clearIntent, focusedHost, isFocusedHostEphemeral, onUpdateHost, rawFocusedHost]);
|
|
|
|
const handleFontFamilyChangeForFocusedSession = useCallback((fontFamilyId: string) => {
|
|
if (!focusedHost || isSameResolvedTerminalFont(fontFamilyId, focusedFontFamilyId, navigatorPlatform)) return;
|
|
startTransition(() => {
|
|
if (isFocusedHostEphemeral) {
|
|
onUpdateTerminalFontFamilyId?.(fontFamilyId);
|
|
return;
|
|
}
|
|
if (rawFocusedHost) {
|
|
onUpdateHost({ ...rawFocusedHost, fontFamily: fontFamilyId, fontFamilyOverride: true });
|
|
}
|
|
});
|
|
}, [focusedHost, focusedFontFamilyId, isFocusedHostEphemeral, onUpdateTerminalFontFamilyId, onUpdateHost, rawFocusedHost]);
|
|
|
|
const handleFontFamilyResetForFocusedSession = useCallback(() => {
|
|
if (!focusedHost || isFocusedHostEphemeral || !rawFocusedHost) return;
|
|
onUpdateHost(clearHostFontFamilyOverride(rawFocusedHost));
|
|
}, [focusedHost, isFocusedHostEphemeral, onUpdateHost, rawFocusedHost]);
|
|
|
|
const handleFontSizeChangeForFocusedSession = useCallback((newFontSize: number) => {
|
|
if (!focusedHost || newFontSize === focusedFontSize) return;
|
|
startTransition(() => {
|
|
if (activeWorkspace && focusedSessionId) {
|
|
onUpdateSessionFontSize?.(focusedSessionId, newFontSize);
|
|
return;
|
|
}
|
|
if (isFocusedHostEphemeral) {
|
|
// Ephemeral hosts cannot persist host-level overrides; keep the
|
|
// change per-session (same path Ctrl+zoom uses) when possible.
|
|
const targetSessionId = focusedSessionId ?? activeSession?.id;
|
|
if (targetSessionId) {
|
|
onUpdateSessionFontSize?.(targetSessionId, newFontSize);
|
|
return;
|
|
}
|
|
onUpdateTerminalFontSize?.(newFontSize);
|
|
return;
|
|
}
|
|
if (rawFocusedHost) {
|
|
onUpdateHost({ ...rawFocusedHost, fontSize: newFontSize, fontSizeOverride: true });
|
|
}
|
|
});
|
|
}, [activeSession, activeWorkspace, focusedHost, focusedFontSize, focusedSessionId, isFocusedHostEphemeral, onUpdateSessionFontSize, onUpdateTerminalFontSize, onUpdateHost, rawFocusedHost]);
|
|
|
|
const handleFontSizeResetForFocusedSession = useCallback(() => {
|
|
if (!focusedHost) return;
|
|
if (activeWorkspace && focusedSessionId) {
|
|
onClearSessionFontSizeOverride?.(focusedSessionId);
|
|
return;
|
|
}
|
|
if (isFocusedHostEphemeral) {
|
|
const targetSessionId = focusedSessionId ?? activeSession?.id;
|
|
if (targetSessionId) onClearSessionFontSizeOverride?.(targetSessionId);
|
|
return;
|
|
}
|
|
if (!rawFocusedHost) return;
|
|
onUpdateHost(clearHostFontSizeOverride(rawFocusedHost));
|
|
}, [activeSession, activeWorkspace, focusedHost, focusedSessionId, isFocusedHostEphemeral, onClearSessionFontSizeOverride, onUpdateHost, rawFocusedHost]);
|
|
|
|
const handleFontWeightChangeForFocusedSession = useCallback((newFontWeight: number) => {
|
|
if (!focusedHost || newFontWeight === focusedFontWeight) return;
|
|
startTransition(() => {
|
|
if (isFocusedHostEphemeral) {
|
|
onUpdateTerminalFontWeight?.(newFontWeight);
|
|
return;
|
|
}
|
|
const rawHost = hostMap.get(focusedHost.id);
|
|
if (rawHost) {
|
|
onUpdateHost({ ...rawHost, fontWeight: newFontWeight, fontWeightOverride: true });
|
|
}
|
|
});
|
|
}, [focusedHost, focusedFontWeight, isFocusedHostEphemeral, onUpdateTerminalFontWeight, onUpdateHost, hostMap]);
|
|
|
|
const handleFontWeightResetForFocusedSession = useCallback(() => {
|
|
if (!focusedHost || isFocusedHostEphemeral) return;
|
|
const rawHost = hostMap.get(focusedHost.id);
|
|
if (rawHost) {
|
|
onUpdateHost(clearHostFontWeightOverride(rawHost));
|
|
}
|
|
}, [focusedHost, isFocusedHostEphemeral, onUpdateHost, hostMap]);
|
|
|
|
const composeBarThemeColors = useMemo(() => {
|
|
if (!activeWorkspace || !focusedSessionId) return terminalTheme.colors;
|
|
return resolvedPreviewTheme.colors;
|
|
}, [activeWorkspace, focusedSessionId, resolvedPreviewTheme.colors, terminalTheme.colors]);
|
|
|
|
return {
|
|
activeTopTabsThemeId,
|
|
composeBarThemeColors,
|
|
focusedFontFamilyId,
|
|
focusedFontFamilyOverridden,
|
|
focusedFontSize,
|
|
focusedFontSizeOverridden,
|
|
focusedFontWeight,
|
|
focusedFontWeightOverridden,
|
|
focusedThemeOverridden,
|
|
handleFontFamilyChangeForFocusedSession,
|
|
handleFontFamilyResetForFocusedSession,
|
|
handleFontSizeChangeForFocusedSession,
|
|
handleFontSizeResetForFocusedSession,
|
|
handleFontWeightChangeForFocusedSession,
|
|
handleFontWeightResetForFocusedSession,
|
|
handleThemeChangeForFocusedSession,
|
|
handleThemeResetForFocusedSession,
|
|
previewedOrVisibleThemeId,
|
|
previewTargetSessionId,
|
|
resolvedPreviewTheme,
|
|
visibleFocusedThemeId,
|
|
};
|
|
}
|