Files
NetMesh/application/state/useNetworkDeviceModeSuggestion.ts

98 lines
3.7 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useState } from 'react';
import { getEffectiveHostDistro, shouldSuggestNetworkDeviceMode } from '../../domain/host';
import { isSavedVaultHost } from '../../domain/ephemeralHosts';
import { resolveEffectiveTerminalProtocol } from '../../domain/terminalProtocol';
import type { Host } from '../../domain/models';
import {
isNetworkDeviceSuggestionHandled,
markNetworkDeviceSuggestionShown,
resolveNetworkDeviceSuggestion,
subscribeNetworkDeviceSuggestionHandled,
} from './networkDeviceModeSuggestion';
export interface NetworkDeviceModeSuggestion {
/** Whether the one-line tip should be rendered for this session. */
visible: boolean;
/** User accepted: persist + hide, then run the caller's enable side effects. */
enable: () => void;
/** User dismissed: persist + hide. */
dismiss: () => void;
}
/**
* Owns the lifecycle of the "enable Network Device Mode" suggestion for a single
* terminal session: eligibility, the once-per-host persistence boundary, and
* cross-pane/window synchronization. Components consume `visible`/`enable`/
* `dismiss` and keep their own view/side-effect glue (host update, toast).
*
* `visible` is a latch rather than a live predicate: once we decide to show the
* tip we record the display (which also suppresses reconnects and other panes)
* and keep it visible until the user acts, the host is classified elsewhere, or
* another surface resolves it.
*/
export function useNetworkDeviceModeSuggestion({
host,
connected,
canUpdateHost,
onEnable,
}: {
host: Host;
connected: boolean;
canUpdateHost: boolean;
onEnable: () => void;
}): NetworkDeviceModeSuggestion {
const [visible, setVisible] = useState(false);
// Reset on host change and hide when the suggestion is resolved elsewhere
// (another pane or window) for this host. Declared before the eligibility
// effect so, on mount, the reset (a no-op false->false) runs first and cannot
// clobber the latch the eligibility effect sets below.
useEffect(() => {
setVisible(false);
return subscribeNetworkDeviceSuggestionHandled((changedHostId) => {
if (changedHostId === host.id) setVisible(false);
});
}, [host.id]);
useEffect(() => {
if (visible) return;
// Enabling writes back through onEnable, which only lands for vault hosts.
// Skip when updates can't persist: the detached popup (no host updates) and
// ephemeral deep-link hosts (not in the vault array).
if (!connected || !canUpdateHost || !isSavedVaultHost(host)) return;
const eligible = shouldSuggestNetworkDeviceMode({
host,
detectedDistro: getEffectiveHostDistro(host),
alreadyHandled: isNetworkDeviceSuggestionHandled(host.id),
// Match the Host Details toggle: only plain SSH (not Mosh/ET/serial/etc.).
effectiveProtocol: resolveEffectiveTerminalProtocol(host),
});
if (!eligible) return;
// Record the display silently so reconnects and later-mounting panes stay
// quiet, but this instance keeps the tip until the user acts.
markNetworkDeviceSuggestionShown(host.id);
setVisible(true);
}, [visible, connected, canUpdateHost, host]);
useEffect(() => {
// The mode may be enabled from another surface (Host Details, a synced
// pane) while the tip is latched open; drop it as soon as the host is
// already classified so we don't keep offering an enabled action.
if (host.deviceType === 'network') setVisible(false);
}, [host.deviceType]);
const dismiss = useCallback(() => {
resolveNetworkDeviceSuggestion(host.id);
setVisible(false);
}, [host.id]);
const enable = useCallback(() => {
resolveNetworkDeviceSuggestion(host.id);
setVisible(false);
onEnable();
}, [host.id, onEnable]);
return { visible, enable, dismiss };
}