mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
a859af6c62
* feat(terminal): suggest Network Device Mode via a session-header tip When a connected host is classified as a network device (auto-detected vendor banner/os-release, or a manual distro-icon override) but Network Device Mode is off, show a non-blocking one-line tip at the top of the session. The user can enable the mode (persisted to the host) or dismiss it; we only suggest once per host. Driven by the host's effective distro so it also previews without real hardware: set a host's device icon to a network vendor and reconnect. - domain: shouldSuggestNetworkDeviceMode (exact vendor match, once-per-host) - application/state: networkDeviceModeSuggestion persistence boundary - TerminalView: header tip + reserve top offset so it never covers output - i18n: terminal.networkDevice.tip.* for en/zh-CN/zh-TW/ru Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): gate network device mode tip to plain SSH sessions Codex review: the suggestion tip used the host's effective distro (incl. manual icon override), so a Mosh/ET/serial/telnet session with a network-vendor classification would still offer it and persist a host-level deviceType:'network'. The Host Details toggle is exposed only for plain SSH hosts, so this could set a hidden mode that surprises later SSH reconnects. Add an effectiveProtocol gate to shouldSuggestNetworkDeviceMode and pass effectiveTerminalProtocol from TerminalView. (#2367) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): make network device tip gate effective and popup/multi-pane safe Addresses Codex re-review on the tip: - The effectiveProtocol gate was dead: ctx never carried effectiveTerminalProtocol, so it was always undefined (unrestricted). Derive it from the host via resolveEffectiveTerminalProtocol instead. - Suppress the tip when host updates are unavailable (detached popup has no onUpdateHost) so Enable can't silently no-op; plumb canUpdateHost through ctx. - Sync dismiss/enable across panes/windows for the same host via an in-process subscription in the suggestion persistence module. (#2367) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): make network device tip once-per-host, ephemeral- and multi-window-safe Addresses Codex re-review round 3 on the tip: - Exclude ephemeral deep-link/quick-connect hosts (isSavedVaultHost): they are not in the persisted vault array, so Enable would silently no-op even though canUpdateHost is true. - Track first display separately from user interaction: record "shown" silently on first display so simply closing the session no longer re-nags on every reconnect, while the showing instance keeps the tip until the user acts. - Propagate resolve/dismiss across separate BrowserWindows (#/session-window) via a native storage-event listener that diffs newly handled host ids. (#2367) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): don't touch localStorage at import; keep network tip clear of speed dial Addresses Codex re-review round 4: - P1: networkDeviceModeSuggestion read localStorage at module scope (knownHandledIds initializer), which threw ReferenceError under the Node test runner and broke `npm test`. Initialize it lazily inside the existing browser-only window guard instead. - P2: when the host info bar is hidden and search is closed the full-width tip strip overlapped the compact speed-dial toggle (top-right) and swallowed its clicks. Reserve right-side clearance for the toggle via a pure resolveNetworkDeviceTipRightInset helper (with unit test). (#2367) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): drop network tip once the host is classified elsewhere Addresses Codex re-review round 5: while the tip is latched open, enabling Network Device Mode from Host Details or a synced pane updates the host to deviceType:'network', but the eligibility effect's early return kept the tip (and its now-redundant Enable action) visible. Clear the latch whenever host.deviceType becomes 'network'. (#2367) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): persist only the deviceType override, not the effective host Addresses Codex re-review round 5 (P1): the tip's Enable spread the *effective* session host (group defaults / proxy profile already materialized) into onUpdateHost, so mergeTerminalHostUpdate froze every inherited field (credentials, appearance, proxyConfig) as host-level overrides and later group/profile changes stopped propagating. Send a sparse { id, deviceType: 'network' } update instead. (#2367) Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(terminal): move network device tip lifecycle into a state hook; per-host keys Addresses Codex re-review round 6: - P1 (architecture): TerminalView no longer imports the persistence API or owns the handled-state subscription/eligibility/writes. Extract useNetworkDeviceModeSuggestion under application/state; the component consumes { visible, enable, dismiss } and keeps only view glue (sparse host update + toast), per AGENTS.md's layer boundaries. - P2 (cross-window clobber): persist one key per host instead of a single shared array. localStorage has no atomic read-modify-write, so concurrent appends from two windows could drop a marker; independent keys never clobber, and the storage event recovers the host id directly from the key. (#2367) Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
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 };
|
|
}
|