From 2e9c021e68fce1bc531c8f71ccf28800a79fab72 Mon Sep 17 00:00:00 2001 From: shixu Date: Mon, 29 Jun 2026 10:30:42 +0800 Subject: [PATCH] Address terminal encoding review feedback --- application/state/useStoredString.test.ts | 4 ++++ application/state/useStoredString.ts | 9 +++++++++ components/Terminal.tsx | 5 ++--- components/terminal/autocomplete/snippetCompleter.ts | 11 +++++++++-- components/terminal/useTerminalEffects.ts | 2 +- components/terminalLayer/useTerminalLayerEffects.ts | 10 +++++++++- domain/terminalEncodingPreference.test.ts | 4 ++++ 7 files changed, 38 insertions(+), 7 deletions(-) diff --git a/application/state/useStoredString.test.ts b/application/state/useStoredString.test.ts index b434af8de..0dba2a5ee 100644 --- a/application/state/useStoredString.test.ts +++ b/application/state/useStoredString.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { createStoredStringSyncHandlers, + readOptionalStoredStringValue, readStoredStringValue, resolveStoredStringUpdate, } from "./useStoredString.ts"; @@ -85,12 +86,15 @@ test("stored string helpers read fallback and resolve updater-style toggles", (t const storageKey = "netcatty:test-mode"; assert.equal(readStoredStringValue(storageKey, "edit", isTestMode), "edit"); + assert.equal(readOptionalStoredStringValue(storageKey, isTestMode), null); env.storage.setItem(storageKey, "preview"); assert.equal(readStoredStringValue(storageKey, "edit", isTestMode), "preview"); + assert.equal(readOptionalStoredStringValue(storageKey, isTestMode), "preview"); env.storage.setItem(storageKey, "invalid"); assert.equal(readStoredStringValue(storageKey, "edit", isTestMode), "edit"); + assert.equal(readOptionalStoredStringValue(storageKey, isTestMode), null); assert.equal(resolveStoredStringUpdate("edit", "preview"), "preview"); assert.equal( diff --git a/application/state/useStoredString.ts b/application/state/useStoredString.ts index e0aba108e..b637a9d9c 100644 --- a/application/state/useStoredString.ts +++ b/application/state/useStoredString.ts @@ -18,6 +18,15 @@ export const readStoredStringValue = ( return isAllowedValue(stored) ? stored : fallback; }; +export const readOptionalStoredStringValue = ( + storageKey: string, + isAllowedValue: (value: string | null) => value is T, +): T | null => { + if (!canUseLocalStorage()) return null; + const stored = localStorageAdapter.readString(storageKey); + return isAllowedValue(stored) ? stored : null; +}; + export const resolveStoredStringUpdate = ( currentValue: T, nextValue: T | ((currentValue: T) => T), diff --git a/components/Terminal.tsx b/components/Terminal.tsx index 461f62953..0c3b01612 100644 --- a/components/Terminal.tsx +++ b/components/Terminal.tsx @@ -44,7 +44,7 @@ import { supportsZmodemTerminalDragDrop } from "../lib/zmodemDragDrop"; import { resolveHostAuth } from "../domain/sshAuth"; import { useTerminalBackend } from "../application/state/useTerminalBackend"; import { useStoredBoolean } from "../application/state/useStoredBoolean"; -import { readStoredStringValue, useStoredString } from "../application/state/useStoredString"; +import { readOptionalStoredStringValue, useStoredString } from "../application/state/useStoredString"; import { useSessionLogBackend } from "../application/state/useSessionLogBackend"; import { useTerminalLayoutSuppressActive } from "../application/state/terminalLayoutSuppressStore"; // SFTPModal removed - SFTP is now handled by SftpSidePanel in TerminalLayer @@ -597,9 +597,8 @@ const TerminalComponent: React.FC = ({ const [terminalEncoding, setTerminalEncoding] = useState(() => { return resolveInitialTerminalEncoding( host?.charset, - readStoredStringValue( + readOptionalStoredStringValue( terminalEncodingStorageKey, - 'utf-8', isTerminalEncodingPreference, ), ); diff --git a/components/terminal/autocomplete/snippetCompleter.ts b/components/terminal/autocomplete/snippetCompleter.ts index c58040b43..d1b805a8c 100644 --- a/components/terminal/autocomplete/snippetCompleter.ts +++ b/components/terminal/autocomplete/snippetCompleter.ts @@ -7,11 +7,18 @@ */ import type { Snippet } from "../../../domain/models"; import type { CompletionSuggestion } from "./completionEngine"; -import { snippetAppliesToHost } from "../../../domain/snippetTargets"; const SNIPPET_BASE_SCORE = 2000; // Above history (1000+freq) per "snippet > history". const SNIPPET_PREFIX_BONUS = 100; +function snippetAvailableForAutocomplete(snippet: Snippet, hostId?: string): boolean { + if (snippet.targetsAllHosts) return true; + if (snippet.targets && snippet.targets.length > 0) { + return hostId !== undefined && snippet.targets.includes(hostId); + } + return true; +} + export function getSnippetSuggestions( input: string, snippets: Snippet[], @@ -22,7 +29,7 @@ export function getSnippetSuggestions( const out: CompletionSuggestion[] = []; for (const snippet of snippets) { - if (!snippetAppliesToHost(snippet, options.hostId)) continue; + if (!snippetAvailableForAutocomplete(snippet, options.hostId)) continue; const label = (snippet.label || "").toLowerCase(); const firstLine = (snippet.command || "").split("\n")[0].trim().toLowerCase(); diff --git a/components/terminal/useTerminalEffects.ts b/components/terminal/useTerminalEffects.ts index 508a1efa9..6f1c17f03 100644 --- a/components/terminal/useTerminalEffects.ts +++ b/components/terminal/useTerminalEffects.ts @@ -1343,7 +1343,7 @@ export function useTerminalEffects(ctx: TerminalEffectsContext) { const handleWindowFocus = () => { if (!shouldRecoverOnAppResume()) return; - const term = termRef.current; + let term = termRef.current; if (term) { cancelScheduledUnfocusedRepaint(term); forceTerminalRepaintBypassingAnimationFrame(term); diff --git a/components/terminalLayer/useTerminalLayerEffects.ts b/components/terminalLayer/useTerminalLayerEffects.ts index cffc7c758..8416eee3b 100644 --- a/components/terminalLayer/useTerminalLayerEffects.ts +++ b/components/terminalLayer/useTerminalLayerEffects.ts @@ -8,7 +8,7 @@ import { getTerminalSidePanelShellWidth } from './TerminalLayerSidePanelSection' type TerminalLayerEffectsContext = Record; export function useTerminalLayerEffects(ctx: TerminalLayerEffectsContext) { - const { activeSidePanelTab, activeTabId, activeTabIdRef, activeWorkspace, activityTrackedSessions, cancelAnimationFrame, ChunkedEscapeFilter, clearTimeout, document, dropHint, filterTabsMap, focusedSessionId, getSessionActivityIdsToClear, handleToggleAiFromTopBar, handleToggleScriptsSidePanel, handleToggleSidePanel, hasNotifiableTerminalOutput, isComposeBarOpen, isFocusMode, isTerminalLayerVisible, lastSidePanelTabRef, Map, onSessionData, onSplitSessionRef, onToggleBroadcastRef, onToggleWorkspaceViewModeRef, prevFocusedSessionIdRef, refocusActiveTerminalSession, requestAnimationFrame, ResizeObserver, sessionActivityStore, sessions, Set, setAiMountedTabIds, setDropHint, setNotesMountedTabIds, setScriptsMountedTabIds, setSystemMountedTabIds, setSftpHostForTab, setSftpInitialLocationForTab, setSftpPendingUploadsForTab, setSidePanelOpenTabs, setThemeMountedTabIds, setTimeout, setupMcpApprovalBridge, setWorkspaceArea, sidePanelPosition, sidePanelWidth, sftpActiveHost, sftpHostForTab, shouldMarkSessionActivity, sidePanelOpenTabs, splitHorizontalHandlersRef, splitVerticalHandlersRef, terminalRendererCwdBySessionRef, toggleScriptsSidePanelRef, toggleSidePanelRef, validAIScopeTargetIds, validSessionActivityIds, window, workspaceBroadcastHandlersRef, workspaceFocusHandlersRef, workspaceInnerRef, workspaces } = ctx; + const { activeSidePanelTab, activeTabId, activeTabIdRef, activeWorkspace, activityTrackedSessions, cancelAnimationFrame, ChunkedEscapeFilter, clearTimeout, clearTopTabsPreviewVars, document, dropHint, filterTabsMap, focusedSessionId, getSessionActivityIdsToClear, handleToggleAiFromTopBar, handleToggleScriptsSidePanel, handleToggleSidePanel, hasNotifiableTerminalOutput, isComposeBarOpen, isFocusMode, isTerminalLayerVisible, lastSidePanelTabRef, Map, onSessionData, onSplitSessionRef, onToggleBroadcastRef, onToggleWorkspaceViewModeRef, prevFocusedSessionIdRef, refocusActiveTerminalSession, requestAnimationFrame, ResizeObserver, sessionActivityStore, sessions, Set, setAiMountedTabIds, setDropHint, setNotesMountedTabIds, setScriptsMountedTabIds, setSystemMountedTabIds, setSftpHostForTab, setSftpInitialLocationForTab, setSftpPendingUploadsForTab, setSidePanelOpenTabs, setThemeMountedTabIds, setTimeout, setupMcpApprovalBridge, setWorkspaceArea, sidePanelPosition, sidePanelWidth, sftpActiveHost, sftpHostForTab, shouldMarkSessionActivity, sidePanelOpenTabs, splitHorizontalHandlersRef, splitVerticalHandlersRef, terminalRendererCwdBySessionRef, toggleScriptsSidePanelRef, toggleSidePanelRef, validAIScopeTargetIds, validSessionActivityIds, window, workspaceBroadcastHandlersRef, workspaceFocusHandlersRef, workspaceInnerRef, workspaces } = ctx; const activeWorkspaceId = activeWorkspace?.id; const activeWorkspaceViewMode = activeWorkspace?.viewMode; @@ -60,6 +60,14 @@ export function useTerminalLayerEffects(ctx: TerminalLayerEffectsContext) { }); }, [remeasureWorkspaceArea, requestAnimationFrame]); + useEffect(() => { + if (!isTerminalLayerVisible) { + if (typeof clearTopTabsPreviewVars === 'function') { + clearTopTabsPreviewVars(); + } + } + }, [clearTopTabsPreviewVars, isTerminalLayerVisible]); + useEffect(() => { const liveSessionIds = new Set(sessions.map((session) => session.id)); for (const sessionId of terminalRendererCwdBySessionRef.current.keys()) { diff --git a/domain/terminalEncodingPreference.test.ts b/domain/terminalEncodingPreference.test.ts index 3cdc6a3ab..858bdcd53 100644 --- a/domain/terminalEncodingPreference.test.ts +++ b/domain/terminalEncodingPreference.test.ts @@ -24,6 +24,10 @@ test("per-host remembered terminal encoding wins for supported or empty host cha assert.equal(resolveInitialTerminalEncoding(undefined, "gb18030"), "gb18030"); }); +test("missing per-host remembered encoding preserves configured GB18030 charset", () => { + assert.equal(resolveInitialTerminalEncoding("GB18030", null), "gb18030"); +}); + test("per-host remembered UTF-8 can override a supported GB18030 host charset", () => { assert.equal(resolveInitialTerminalEncoding("GB18030", "utf-8"), "utf-8"); });