mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
40d80fe535
* perf: comprehensive performance optimization across UI and state management - Replace Array.find/includes with Map/Set lookups for O(1) access in hot paths - Add requestAnimationFrame throttling to all mousemove resize handlers - Remove redundant forceUpdate + useSyncExternalStore double subscription - Extract terminal search decoration config to module-level constant - Pause server stats polling and resize handlers for hidden terminals - Add timer cleanup for useEffect/useLayoutEffect with setTimeout - Use useEffectEvent to stabilize effect callbacks and reduce effect re-runs - Use useDeferredValue for QuickSwitcher search input - Batch activeTabStore notifications with microtask coalescing - Memoize sessionLogConfig and activityTrackedSessions to prevent child re-renders - Use ref pattern for stable onTerminalDataCapture callback - Skip TerminalLayer pre-warming when no sessions or workspaces exist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: flush final resize value before canceling RAF Apply the last computed size synchronously on mouseup/cleanup before canceling the pending requestAnimationFrame. This prevents the final drag delta from being dropped when mouseup fires before the queued frame executes. Addresses review feedback from codex on all 3 RAF-throttled resize handlers: split resize, side panel resize, and SFTP column resize. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: initialize lastClientXRef on resize start to prevent click-collapse Without initialization, a click on the resize handle without dragging would use lastClientXRef=0, computing a large negative diff and collapsing the column to minimum width. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert useDeferredValue for QuickSwitcher search useDeferredValue can lag behind the actual input, causing quickResults to reflect a stale query when the user types fast and presses Enter. This is a correctness regression - the selected item may not match the user's intent. The host list is typically small (<200), so synchronous filtering is fast enough without deferral. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore runtime activity guard to prevent stale badge on tab switch The pre-filtered activityTrackedSessions reduces subscriptions for disconnected sessions, but removing the runtime shouldMarkSessionActivity check introduced a race: between tab switch and effect re-subscription, old listeners could mark the newly-focused session as unread. Restore the activeTabIdRef.current guard inside the callback as a safety net. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: defer initialConnectDoneRef flag until auto-connect executes Moving the flag inside the setTimeout callback prevents it from being set when the timer is canceled by cleanup. Previously, if the effect re-ran before the setTimeout(0) fired, the timer was cleared but the ref was already true, permanently skipping the initial local connect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: capture resizingRef fields before setState updater Destructure field/startX/startWidth from the ref upfront so the functional updater closure never reads resizingRef.current after it may have been cleared by handleResizeEnd. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove activeTabId from activityTrackedSessions to stabilize subscriptions Depending on activeTabId caused subscriptions to tear down and recreate on every tab switch, resetting the ChunkedEscapeFilter mid-sequence and producing false unread badges. The runtime guard via activeTabIdRef already handles the active-tab check, so pre-filtering only needs to exclude disconnected sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: fetch server stats immediately when tab becomes visible again Use hasFetchedRef to distinguish first connect (2s delay for connection stabilization) from tab resume (immediate fetch). Prevents showing stale CPU/memory data for 2 seconds after switching back to a terminal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore cold-start prewarm and reset network stats on tab resume 1. Revert shouldPrewarm guard - TerminalLayer should always prewarm after 1.2s regardless of session/workspace count, as the purpose is to hide lazy-load latency before the user opens their first terminal. 2. Reset netRxSpeed/netTxSpeed to 0 when resuming a hidden terminal tab. The backend computes network throughput as a delta from the previous sample, so the first fetch after a long hidden interval would show artificially low throughput averaged over the gap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reset hasFetchedRef on disconnect and preserve built-in theme precedence 1. Clear hasFetchedRef when connection drops so reconnects get the 2s stabilization delay before first stats fetch. 2. Reverse theme merge order in themeById Map so built-in themes are written last and take precedence over custom themes with duplicate IDs, matching the original find() semantics and other resolution sites (customThemeStore.getThemeById, Terminal.tsx). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: also clear per-interface network speeds on tab resume Reset rxSpeed/txSpeed on each netInterfaces entry in addition to the aggregate values, so the network hovercard doesn't show stale throughput while waiting for the first fresh poll after resume. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reset capture ref on retry and skip warmup for established connections 1. Reset terminalDataCapturedRef in handleRetry() so log capture works for retried sessions (retry doesn't change sessionId, so the effect that resets the ref never re-runs). 2. Track connection start time to skip the 2s warmup delay when a tab becomes visible for a connection that was already established while hidden. Only apply the warmup for truly fresh connections (<2s old). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prevent overlapping stats requests and track connection time while hidden 1. Add fetchInFlightRef guard to prevent concurrent getServerStats requests that could race and corrupt baseline CPU/network data. 2. Move connectedAtRef initialization before the isVisible check so connections that complete while the tab is hidden record their start time. This ensures the warmup delay is correctly skipped when the tab becomes visible for an already-stable connection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reset fetchInFlightRef on disconnect to unblock reconnect stats A pending getServerStats request from a previous connection could keep fetchInFlightRef set, causing the reconnected session's initial fetch to be skipped until the old request timed out. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clear fetchInFlightRef when tab becomes hidden Ensures the resume fetch isn't blocked by an in-flight request from the previous visible cycle. Any stale response from the old request will be quickly overwritten by the fresh immediate fetch on resume. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use generation counter to invalidate stale stats responses Replace fetchInFlightRef with a generation counter that increments on each fetch. Stale responses from before a hide/show cycle are discarded by comparing the captured generation against the current value, fully preventing pre-hide requests from overwriting zeroed network stats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: increment fetch generation on effect setup to invalidate in-flight requests The generation was only incremented inside fetchStats, but the resume setTimeout hadn't fired yet when old responses arrived. Incrementing at effect setup time ensures any pre-hide in-flight request is immediately stale, preventing it from overwriting zeroed network stats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
79 lines
2.4 KiB
TypeScript
79 lines
2.4 KiB
TypeScript
import { useCallback,useSyncExternalStore } from 'react';
|
|
|
|
// Simple store for active tab that allows fine-grained subscriptions
|
|
type Listener = () => void;
|
|
|
|
class ActiveTabStore {
|
|
private activeTabId: string = 'vault';
|
|
private listeners = new Set<Listener>();
|
|
private pendingNotify = false;
|
|
|
|
getActiveTabId = () => this.activeTabId;
|
|
|
|
setActiveTabId = (id: string) => {
|
|
if (this.activeTabId !== id) {
|
|
this.activeTabId = id;
|
|
// Defer listener notification to avoid "setState during render" if called from a render phase
|
|
if (this.pendingNotify) return;
|
|
this.pendingNotify = true;
|
|
Promise.resolve().then(() => {
|
|
this.pendingNotify = false;
|
|
this.listeners.forEach(listener => listener());
|
|
});
|
|
}
|
|
};
|
|
|
|
subscribe = (listener: Listener) => {
|
|
this.listeners.add(listener);
|
|
return () => this.listeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
export const activeTabStore = new ActiveTabStore();
|
|
|
|
// Hook to read active tab ID - only re-renders when activeTabId changes
|
|
export const useActiveTabId = () => {
|
|
return useSyncExternalStore(
|
|
activeTabStore.subscribe,
|
|
activeTabStore.getActiveTabId
|
|
);
|
|
};
|
|
|
|
// Hook to get setter - never causes re-render
|
|
export const useSetActiveTabId = () => {
|
|
return activeTabStore.setActiveTabId;
|
|
};
|
|
|
|
// Check if a specific tab is active - only re-renders when this specific tab's active state changes
|
|
export const useIsTabActive = (tabId: string) => {
|
|
const getSnapshot = useCallback(() => activeTabStore.getActiveTabId() === tabId, [tabId]);
|
|
return useSyncExternalStore(activeTabStore.subscribe, getSnapshot);
|
|
};
|
|
|
|
// Stable snapshot functions - defined once outside components
|
|
const getIsVaultActive = () => activeTabStore.getActiveTabId() === 'vault';
|
|
const getIsSftpActive = () => activeTabStore.getActiveTabId() === 'sftp';
|
|
|
|
// Check if vault is active
|
|
export const useIsVaultActive = () => {
|
|
return useSyncExternalStore(
|
|
activeTabStore.subscribe,
|
|
getIsVaultActive
|
|
);
|
|
};
|
|
|
|
// Check if sftp is active
|
|
export const useIsSftpActive = () => {
|
|
return useSyncExternalStore(
|
|
activeTabStore.subscribe,
|
|
getIsSftpActive
|
|
);
|
|
};
|
|
|
|
// Check if terminal layer should be visible
|
|
export const useIsTerminalLayerVisible = (draggingSessionId: string | null) => {
|
|
const activeTabId = useActiveTabId();
|
|
const isTerminalTab = activeTabId !== 'vault' && activeTabId !== 'sftp';
|
|
return isTerminalTab || !!draggingSessionId;
|
|
};
|