From 79ccf47655f401123ea3c9f6c598c0d0f03601eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=A4=A7=E7=8C=AB?= <16399091+binaricat@users.noreply.github.com> Date: Sun, 14 Jun 2026 09:54:13 +0800 Subject: [PATCH] fix editor tab theme toggle (#1467) --- App.tsx | 2 +- application/app/AppHostTreeLayer.tsx | 35 +++++++- application/app/AppView.tsx | 9 +- application/app/activeChromeTheme.test.ts | 22 ++++- application/app/activeChromeTheme.ts | 11 ++- application/app/workTabSurface.test.ts | 101 +++++++++++++++++++++- application/app/workTabSurface.ts | 28 +++++- 7 files changed, 193 insertions(+), 15 deletions(-) diff --git a/App.tsx b/App.tsx index 1cee37d7..b3ef6528 100755 --- a/App.tsx +++ b/App.tsx @@ -990,7 +990,7 @@ function App({ settings }: { settings: SettingsState }) { logViews={logViews} t={t} /> - + ); } diff --git a/application/app/AppHostTreeLayer.tsx b/application/app/AppHostTreeLayer.tsx index 976d0b89..79bd93ca 100644 --- a/application/app/AppHostTreeLayer.tsx +++ b/application/app/AppHostTreeLayer.tsx @@ -8,6 +8,7 @@ import type { GroupConfig, Host, TerminalSession, TerminalTheme, Workspace } fro import { isHostTreeWorkTabSurface, resolveWorkTabActiveHostId, + resolveWorkTabHostTreeTheme, } from './workTabSurface'; interface AppHostTreeLayerProps { @@ -20,7 +21,12 @@ interface AppHostTreeLayerProps { editorTabs: readonly EditorTab[]; logViews: readonly LogView[]; orderedTabs: readonly string[]; - resolvedPreviewTheme: TerminalTheme; + accentMode: 'theme' | 'custom'; + currentTerminalTheme: TerminalTheme; + customAccent: string; + followAppTerminalTheme: boolean; + hostById: ReadonlyMap; + themeById: ReadonlyMap; onConnect: (host: Host) => void; onCreateLocalTerminal?: () => void; } @@ -43,7 +49,12 @@ export const AppHostTreeLayer: React.FC = ({ editorTabs, logViews, orderedTabs, - resolvedPreviewTheme, + accentMode, + currentTerminalTheme, + customAccent, + followAppTerminalTheme, + hostById, + themeById, onConnect, onCreateLocalTerminal, }) => { @@ -67,6 +78,24 @@ export const AppHostTreeLayer: React.FC = ({ workspaces, }), [activeTabId, editorTabs, sessions, workspaces]); + const hostTreeTheme = useMemo(() => resolveWorkTabHostTreeTheme({ + activeHostId, + accentMode, + currentTerminalTheme, + customAccent, + followAppTerminalTheme, + hostById, + themeById, + }), [ + activeHostId, + accentMode, + currentTerminalTheme, + customAccent, + followAppTerminalTheme, + hostById, + themeById, + ]); + return (
= ({ hosts={hosts} customGroups={customGroups} groupConfigs={groupConfigs} - resolvedPreviewTheme={resolvedPreviewTheme} + resolvedPreviewTheme={hostTreeTheme} activeHostId={activeHostId} onConnect={onConnect} onCreateLocalTerminal={onCreateLocalTerminal} diff --git a/application/app/AppView.tsx b/application/app/AppView.tsx index f505f9f4..d19b8cd4 100644 --- a/application/app/AppView.tsx +++ b/application/app/AppView.tsx @@ -48,7 +48,7 @@ export function AppView({ ctx }: { ctx: AppViewContext }) { setNavigateToSection, setProtocolSelectHost, setQuickSearch, setSessionRenameValue, setTerminalFontFamilyId, setTerminalFontSize, setTerminalThemeId, updateSessionFontSize, clearSessionFontSizeOverride, setWorkspaceFocusedSession, setWorkspaceRenameValue, settings, sftpAutoOpenSidebar, sftpFollowTerminalCwd, setSftpFollowTerminalCwd, sftpAutoSync, sftpDefaultViewMode, sftpDoubleClickBehavior, sftpShowHiddenFiles, sftpUseCompressedUpload, shellHistory, snippetPackages, snippets, splitSessionWithCurrentShell, startSessionRename, - startWorkspaceRename, submitSessionRename, submitWorkspaceRename, t, terminalFontFamilyId, terminalFontSize, terminalSettings, terminalThemeId, + startWorkspaceRename, submitSessionRename, submitWorkspaceRename, t, terminalFontFamilyId, terminalFontSize, terminalSettings, terminalThemeId, themeById, toggleBroadcast, toggleConnectionLogSaved, toggleScriptsSidePanelRef, toggleSidePanelRef, toggleWorkspaceViewMode, unmanageSource, updateConnectionLog, updateCustomGroups, updateGroupConfigs, updateHostDistro, updateHosts, updateIdentities, updateKeys, updateKnownHosts, updateManagedSources, updateProxyProfiles, updateSnippetPackages, updateSnippets, updateSplitSizes, updateTerminalSetting, workspaceRenameTarget, workspaceRenameValue, workspaces, @@ -153,7 +153,12 @@ export function AppView({ ctx }: { ctx: AppViewContext }) { editorTabs={editorTabs} logViews={logViews} orderedTabs={orderedTabsWithEditors} - resolvedPreviewTheme={currentTerminalTheme} + accentMode={accentMode} + currentTerminalTheme={currentTerminalTheme} + customAccent={customAccent} + followAppTerminalTheme={followAppTerminalTheme} + hostById={hostById} + themeById={themeById} onConnect={handleConnectToHost} onCreateLocalTerminal={handleCreateLocalTerminal} /> diff --git a/application/app/activeChromeTheme.test.ts b/application/app/activeChromeTheme.test.ts index 5f77c6be..198b1ef7 100644 --- a/application/app/activeChromeTheme.test.ts +++ b/application/app/activeChromeTheme.test.ts @@ -39,7 +39,7 @@ const baseInput = { workspaceById: new Map(), }; -test("editor tabs use the theme from their owning host", () => { +test("editor tabs use the owning host terminal theme when follow-app terminal theme is off", () => { const editorTab = { id: "editor-1", hostId: "host-1", @@ -58,6 +58,26 @@ test("editor tabs use the theme from their owning host", () => { assert.equal(resolved?.id, hostTheme.id); }); +test("editor tabs use the followed terminal theme when follow-app terminal theme is on", () => { + const editorTab = { + id: "editor-1", + hostId: "host-1", + sessionId: "sftp-1", + }; + + const resolved = resolveActiveChromeTheme({ + ...baseInput, + activeTabId: toEditorTabId(editorTab.id), + editorTabs: [editorTab as unknown as EditorTab], + followAppTerminalTheme: true, + hostById: new Map([ + ["host-1", { id: "host-1", theme: hostTheme.id } as unknown as Host], + ]), + }); + + assert.equal(resolved?.id, currentTheme.id); +}); + test("log tabs use the saved log theme when available", () => { const resolved = resolveActiveChromeTheme({ ...baseInput, diff --git a/application/app/activeChromeTheme.ts b/application/app/activeChromeTheme.ts index 75a3a4b8..a4690910 100644 --- a/application/app/activeChromeTheme.ts +++ b/application/app/activeChromeTheme.ts @@ -54,22 +54,21 @@ export function resolveActiveChromeTheme({ }: ResolveActiveChromeThemeInput): TerminalTheme | null { if (activeTabId === "vault" || activeTabId === "sftp") return null; - const resolveSessionTheme = (session: TerminalSession): TerminalTheme => { + const resolveHostTheme = (hostId: string): TerminalTheme => { if (followAppTerminalTheme) return currentTerminalTheme; - const host = hostById.get(session.hostId) ?? null; + const host = hostById.get(hostId) ?? null; const themeId = resolveHostTerminalThemeId(host, currentTerminalTheme.id); const baseTheme = themeById.get(themeId) ?? currentTerminalTheme; return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent); }; + const resolveSessionTheme = (session: TerminalSession): TerminalTheme => resolveHostTheme(session.hostId); + if (isEditorTabId(activeTabId)) { const editorTabId = fromEditorTabId(activeTabId); const editorTab = editorTabs.find((tab) => tab.id === editorTabId); if (!editorTab) return null; - const host = hostById.get(editorTab.hostId) ?? null; - const themeId = resolveHostTerminalThemeId(host, currentTerminalTheme.id); - const baseTheme = themeById.get(themeId) ?? currentTerminalTheme; - return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent); + return resolveHostTheme(editorTab.hostId); } const logView = logViews.find((item) => item.id === activeTabId); diff --git a/application/app/workTabSurface.test.ts b/application/app/workTabSurface.test.ts index 87f1d7ce..14d1a1fd 100644 --- a/application/app/workTabSurface.test.ts +++ b/application/app/workTabSurface.test.ts @@ -8,9 +8,38 @@ import { isTerminalContentTabSurface, reorderWorkTabIds, resolveWorkTabActiveHostId, + resolveWorkTabHostTreeTheme, } from './workTabSurface'; import type { EditorTab } from '../state/editorTabStore'; -import type { TerminalSession, Workspace } from '../../types'; +import type { Host, TerminalSession, TerminalTheme, Workspace } from '../../types'; + +const makeTheme = (id: string, type: TerminalTheme['type'], background: string): TerminalTheme => ({ + id, + name: id, + type, + colors: { + background, + foreground: type === 'dark' ? '#ffffff' : '#000000', + cursor: '#888888', + selection: '#555555', + black: '#000000', + red: '#ff0000', + green: '#00ff00', + yellow: '#ffff00', + blue: '#0000ff', + magenta: '#ff00ff', + cyan: '#00ffff', + white: '#ffffff', + brightBlack: '#444444', + brightRed: '#ff5555', + brightGreen: '#55ff55', + brightYellow: '#ffff55', + brightBlue: '#5555ff', + brightMagenta: '#ff55ff', + brightCyan: '#55ffff', + brightWhite: '#ffffff', + }, +}); test('work tab order keeps custom positions and appends new tabs', () => { assert.deepEqual( @@ -104,3 +133,73 @@ test('shared host tree resolves active host ids across work tab types', () => { assert.equal(resolveWorkTabActiveHostId({ activeTabId: 'editor:file-1', sessions, workspaces, editorTabs }), 'host-3'); assert.equal(resolveWorkTabActiveHostId({ activeTabId: 'log-1', sessions, workspaces, editorTabs }), null); }); + +test('shared host tree uses the active host theme when follow-app terminal theme is off', () => { + const currentTheme = makeTheme('app-dark', 'dark', '#111111'); + const hostTheme = makeTheme('host-light', 'light', '#fafafa'); + const host = { + id: 'host-1', + label: 'Host', + hostname: 'host.local', + username: 'root', + tags: [], + os: 'linux', + theme: hostTheme.id, + themeOverride: true, + } as Host; + + const resolved = resolveWorkTabHostTreeTheme({ + activeHostId: host.id, + accentMode: 'theme', + currentTerminalTheme: currentTheme, + customAccent: '#8b5cf6', + followAppTerminalTheme: false, + hostById: new Map([[host.id, host]]), + themeById: new Map([[currentTheme.id, currentTheme], [hostTheme.id, hostTheme]]), + }); + + assert.equal(resolved.id, hostTheme.id); +}); + +test('shared host tree uses the followed terminal theme when follow-app terminal theme is on', () => { + const currentTheme = makeTheme('app-light', 'light', '#ffffff'); + const hostTheme = makeTheme('host-dark', 'dark', '#050505'); + const host = { + id: 'host-1', + label: 'Host', + hostname: 'host.local', + username: 'root', + tags: [], + os: 'linux', + theme: hostTheme.id, + themeOverride: true, + } as Host; + + const resolved = resolveWorkTabHostTreeTheme({ + activeHostId: host.id, + accentMode: 'theme', + currentTerminalTheme: currentTheme, + customAccent: '#8b5cf6', + followAppTerminalTheme: true, + hostById: new Map([[host.id, host]]), + themeById: new Map([[currentTheme.id, currentTheme], [hostTheme.id, hostTheme]]), + }); + + assert.equal(resolved.id, currentTheme.id); +}); + +test('shared host tree falls back to the current terminal theme without an active host', () => { + const currentTheme = makeTheme('app-dark', 'dark', '#111111'); + + const resolved = resolveWorkTabHostTreeTheme({ + activeHostId: null, + accentMode: 'theme', + currentTerminalTheme: currentTheme, + customAccent: '#8b5cf6', + followAppTerminalTheme: false, + hostById: new Map(), + themeById: new Map([[currentTheme.id, currentTheme]]), + }); + + assert.equal(resolved.id, currentTheme.id); +}); diff --git a/application/app/workTabSurface.ts b/application/app/workTabSurface.ts index ea274c95..45f5b346 100644 --- a/application/app/workTabSurface.ts +++ b/application/app/workTabSurface.ts @@ -2,8 +2,9 @@ import { fromEditorTabId, isEditorTabId, } from '../state/activeTabStore'; +import { applyCustomAccentToTerminalTheme, resolveHostTerminalThemeId } from '../../domain/terminalAppearance'; import type { EditorTab } from '../state/editorTabStore'; -import type { TerminalSession, Workspace } from '../../types'; +import type { Host, TerminalSession, TerminalTheme, Workspace } from '../../types'; function uniqueTabIds(tabIds: readonly string[]): string[] { const seen = new Set(); @@ -125,3 +126,28 @@ export function resolveWorkTabActiveHostId({ return null; } + +export function resolveWorkTabHostTreeTheme({ + activeHostId, + accentMode, + currentTerminalTheme, + customAccent, + followAppTerminalTheme, + hostById, + themeById, +}: { + activeHostId: string | null; + accentMode: 'theme' | 'custom'; + currentTerminalTheme: TerminalTheme; + customAccent: string; + followAppTerminalTheme: boolean; + hostById: ReadonlyMap; + themeById: ReadonlyMap; +}): TerminalTheme { + if (!activeHostId || followAppTerminalTheme) return currentTerminalTheme; + + const host = hostById.get(activeHostId) ?? null; + const themeId = resolveHostTerminalThemeId(host, currentTerminalTheme.id); + const baseTheme = themeById.get(themeId) ?? currentTerminalTheme; + return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent); +}