Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
291 lines
8.5 KiB
TypeScript
291 lines
8.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { toEditorTabId } from "../state/activeTabStore.ts";
|
|
import type { EditorTab } from "../state/editorTabStore.ts";
|
|
import type { LogView } from "../state/logViewState.ts";
|
|
import { isActiveChromeThemeResolvable, resolveActiveChromeTheme } from "./activeChromeTheme.ts";
|
|
import type { Host, TerminalSession, TerminalTheme, Workspace } from "../../types";
|
|
|
|
const theme = (id: string, type: "dark" | "light" = "dark"): TerminalTheme => ({
|
|
id,
|
|
name: id,
|
|
type,
|
|
colors: {
|
|
background: type === "dark" ? "#111111" : "#eeeeee",
|
|
foreground: type === "dark" ? "#eeeeee" : "#111111",
|
|
cursor: "#22aaff",
|
|
},
|
|
});
|
|
|
|
const currentTheme = theme("current");
|
|
const hostTheme = theme("host-theme");
|
|
const logTheme = theme("log-theme", "light");
|
|
|
|
const baseInput = {
|
|
accentMode: "theme" as const,
|
|
currentTerminalTheme: currentTheme,
|
|
customAccent: "221.2 83.2% 53.3%",
|
|
editorTabs: [],
|
|
followAppTerminalTheme: false,
|
|
hostById: new Map<string, Host>(),
|
|
logViews: [],
|
|
sessionById: new Map<string, TerminalSession>(),
|
|
themeById: new Map([
|
|
[currentTheme.id, currentTheme],
|
|
[hostTheme.id, hostTheme],
|
|
[logTheme.id, logTheme],
|
|
]),
|
|
workspaceById: new Map<string, Workspace>(),
|
|
};
|
|
|
|
test("editor tabs use the owning host terminal theme when follow-app terminal theme is off", () => {
|
|
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],
|
|
hostById: new Map([
|
|
["host-1", { id: "host-1", theme: hostTheme.id } as unknown as 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("follow-app chrome applies custom accent onto the published base theme", () => {
|
|
const editorTab = {
|
|
id: "editor-1",
|
|
hostId: "host-1",
|
|
sessionId: "sftp-1",
|
|
};
|
|
|
|
const resolved = resolveActiveChromeTheme({
|
|
...baseInput,
|
|
accentMode: "custom",
|
|
customAccent: "0 100% 50%",
|
|
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);
|
|
assert.notEqual(resolved?.colors.cursor, currentTheme.colors.cursor);
|
|
assert.notEqual(resolved, currentTheme);
|
|
});
|
|
|
|
test("log tabs use the saved log theme when available", () => {
|
|
const resolved = resolveActiveChromeTheme({
|
|
...baseInput,
|
|
activeTabId: "log-1",
|
|
logViews: [{
|
|
id: "log-1",
|
|
connectionLogId: "1",
|
|
log: { id: "1", themeId: logTheme.id },
|
|
} as unknown as LogView],
|
|
});
|
|
|
|
assert.equal(resolved?.id, logTheme.id);
|
|
});
|
|
|
|
test("root pages use the normal application theme", () => {
|
|
const resolved = resolveActiveChromeTheme({
|
|
...baseInput,
|
|
activeTabId: "vault",
|
|
});
|
|
|
|
assert.equal(resolved, null);
|
|
});
|
|
|
|
test("follow-app workspace split view always uses the global terminal theme", () => {
|
|
const workspace: Workspace = {
|
|
id: "ws-1",
|
|
name: "Workspace",
|
|
viewMode: "split",
|
|
focusedSessionId: "session-1",
|
|
root: {
|
|
type: "split",
|
|
direction: "horizontal",
|
|
sizes: [50, 50],
|
|
children: [
|
|
{ type: "session", sessionId: "session-1" },
|
|
{ type: "session", sessionId: "session-2" },
|
|
],
|
|
},
|
|
} as unknown as Workspace;
|
|
|
|
const hostA = { id: "host-a", theme: hostTheme.id, themeOverride: true } as unknown as Host;
|
|
const hostB = { id: "host-b", theme: logTheme.id, themeOverride: true } as unknown as Host;
|
|
|
|
const resolved = resolveActiveChromeTheme({
|
|
...baseInput,
|
|
activeTabId: "ws-1",
|
|
followAppTerminalTheme: true,
|
|
hostById: new Map([
|
|
["host-a", hostA],
|
|
["host-b", hostB],
|
|
]),
|
|
sessionById: new Map([
|
|
["session-1", { id: "session-1", hostId: "host-a" } as TerminalSession],
|
|
["session-2", { id: "session-2", hostId: "host-b" } as TerminalSession],
|
|
]),
|
|
workspaceById: new Map([["ws-1", workspace]]),
|
|
});
|
|
|
|
assert.equal(resolved?.id, currentTheme.id);
|
|
});
|
|
|
|
test("manual workspace split view uses the focused session theme when panes differ", () => {
|
|
const workspace: Workspace = {
|
|
id: "ws-1",
|
|
name: "Workspace",
|
|
viewMode: "split",
|
|
focusedSessionId: "session-2",
|
|
root: {
|
|
type: "split",
|
|
direction: "horizontal",
|
|
sizes: [50, 50],
|
|
children: [
|
|
{ type: "pane", sessionId: "session-1" },
|
|
{ type: "pane", sessionId: "session-2" },
|
|
],
|
|
},
|
|
} as unknown as Workspace;
|
|
|
|
const hostA = { id: "host-a", theme: hostTheme.id, themeOverride: true } as unknown as Host;
|
|
const hostB = { id: "host-b", theme: logTheme.id, themeOverride: true } as unknown as Host;
|
|
const focusedTheme = theme("focused-intent");
|
|
|
|
const resolved = resolveActiveChromeTheme({
|
|
...baseInput,
|
|
activeTabId: "ws-1",
|
|
hostById: new Map([
|
|
["host-a", hostA],
|
|
["host-b", hostB],
|
|
]),
|
|
sessionById: new Map([
|
|
["session-1", { id: "session-1", hostId: "host-a" } as TerminalSession],
|
|
["session-2", { id: "session-2", hostId: "host-b" } as TerminalSession],
|
|
]),
|
|
workspaceById: new Map([["ws-1", workspace]]),
|
|
resolveSessionAppearance: ({ host }) => (
|
|
host?.id === "host-b"
|
|
? { themeId: focusedTheme.id, theme: focusedTheme, source: "intent", appThemeUpdate: null }
|
|
: { themeId: hostTheme.id, theme: hostTheme, source: "host-override", appThemeUpdate: null }
|
|
),
|
|
});
|
|
|
|
assert.equal(resolved?.id, focusedTheme.id);
|
|
});
|
|
|
|
test("manual split workspace falls back to the first tree session theme when focus is stale", () => {
|
|
const workspace: Workspace = {
|
|
id: "ws-1",
|
|
name: "Workspace",
|
|
viewMode: "split",
|
|
focusedSessionId: "missing-session",
|
|
root: {
|
|
type: "split",
|
|
direction: "horizontal",
|
|
sizes: [50, 50],
|
|
children: [
|
|
{ type: "pane", sessionId: "session-1" },
|
|
{ type: "pane", sessionId: "session-2" },
|
|
],
|
|
},
|
|
} as unknown as Workspace;
|
|
|
|
const hostA = { id: "host-a", theme: hostTheme.id, themeOverride: true } as unknown as Host;
|
|
const hostB = { id: "host-b", theme: logTheme.id, themeOverride: true } as unknown as Host;
|
|
|
|
const resolved = resolveActiveChromeTheme({
|
|
...baseInput,
|
|
activeTabId: "ws-1",
|
|
hostById: new Map([
|
|
["host-a", hostA],
|
|
["host-b", hostB],
|
|
]),
|
|
sessionById: new Map([
|
|
["session-1", { id: "session-1", hostId: "host-a" } as TerminalSession],
|
|
["session-2", { id: "session-2", hostId: "host-b" } as TerminalSession],
|
|
]),
|
|
workspaceById: new Map([["ws-1", workspace]]),
|
|
});
|
|
|
|
assert.equal(resolved?.id, hostTheme.id);
|
|
});
|
|
|
|
test("manual mode prefers runtime session appearance over stale host theme ids", () => {
|
|
const intentTheme = theme("intent-theme");
|
|
const resolved = resolveActiveChromeTheme({
|
|
...baseInput,
|
|
activeTabId: "session-1",
|
|
hostById: new Map([
|
|
["host-1", { id: "host-1", theme: hostTheme.id, themeOverride: true } as unknown as Host],
|
|
]),
|
|
sessionById: new Map([
|
|
["session-1", { id: "session-1", hostId: "host-1" } as TerminalSession],
|
|
]),
|
|
resolveSessionAppearance: () => ({
|
|
themeId: intentTheme.id,
|
|
theme: intentTheme,
|
|
source: "intent",
|
|
appThemeUpdate: null,
|
|
}),
|
|
});
|
|
|
|
assert.equal(resolved?.id, intentTheme.id);
|
|
});
|
|
|
|
test("chrome theme sync waits until a newly opened session is present in deps", () => {
|
|
assert.equal(
|
|
isActiveChromeThemeResolvable({
|
|
activeTabId: "session-new",
|
|
editorTabs: [],
|
|
logViews: [],
|
|
sessionById: new Map(),
|
|
workspaceById: new Map(),
|
|
}),
|
|
false,
|
|
);
|
|
|
|
assert.equal(
|
|
isActiveChromeThemeResolvable({
|
|
activeTabId: "session-new",
|
|
editorTabs: [],
|
|
logViews: [],
|
|
sessionById: new Map([["session-new", { id: "session-new" } as TerminalSession]]),
|
|
workspaceById: new Map(),
|
|
}),
|
|
true,
|
|
);
|
|
});
|