= ({
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);
+}