fix: luminance-aware chrome theming for light themes; serialized settings writes; buffered log flush

- Derive tab-bar/chrome palette by background luminance: light themes keep
  a near-background bar with black-tinted tab overlays instead of a muddy
  gray strip; dark themes unchanged
- Theme dockview tabs via the group-scoped --dv-*-tab-* vars its own rules
  consume (they outspecify our .dv-tab rules and leaked abyss navy onto
  light tabs); bump inactive-tab hover specificity to match
- Convert settings dialog + highlight editor hardcoded white text/border
  tints to color-mix over --chrome-fg so panes stay readable on light themes
- commands.ts: per-file log write buffer with a single drain loop per file
  (burst output coalesces into one appendFile per IO tick, chain no longer
  grows per logWrite); stop-tail rides the same buffer
- settingsStore: serialize all writers through mutateSettings() queue that
  re-reads latest state per mutation (tray close-action vs settings UI full
  saves no longer clobber each other)
- tests: burst ordering + stop-tail case for the log buffer
This commit is contained in:
Bill committed 2026-09-17 09:11:05 +08:00
1 parent 9e453e272c
commit b3744b4705
9 files changed
+218 -99

No files matched your search

+8
View File
@@ -5,6 +5,14 @@
--chrome-border: #2d2d2d;
--chrome-accent: #3b8eea;
--chrome-hover: #2a2a2a;
--chrome-bg-deep: #101418;
/* Tab-bar overlays; overwritten per theme by applyChromeTheme(). */
--chrome-tab-bg: rgba(255, 255, 255, 0.04);
--chrome-tab-bg-hover: rgba(255, 255, 255, 0.07);
--chrome-tab-ring: rgba(255, 255, 255, 0.1);
--chrome-tab-ring-hover: rgba(255, 255, 255, 0.2);
--chrome-tab-fg: #98a2ab;
--chrome-tab-fg-hover: #d0d0d0;
}
html,
+17 -14
View File
@@ -1,5 +1,8 @@
/* ============================================================
Highlight settings tab + rule editor (settings/highlight.css)
Text/border tints derive from --chrome-fg via color-mix so the
panel follows the active terminal theme (white tints on dark
themes, black tints on light themes).
============================================================ */
.hl-tab {
@@ -21,15 +24,15 @@
}
.hl-table .ant-table-thead > tr > th {
background: rgba(255, 255, 255, 0.04);
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 4%, transparent);
border-bottom: 1px solid var(--chrome-border);
color: rgba(255, 255, 255, 0.85);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 85%, transparent);
font-size: 12px;
}
.hl-table .ant-table-tbody > tr > td {
background: transparent;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
border-bottom: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 6%, transparent);
}
.hl-table .ant-table-tbody > tr:hover > td {
@@ -48,7 +51,7 @@
.hl-pattern-text {
font-family: 'Cascadia Mono', Consolas, 'JetBrains Mono', monospace;
font-size: 12px;
color: rgba(255, 255, 255, 0.88);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 88%, transparent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -71,17 +74,17 @@
.hl-cell-priority {
font-family: 'Cascadia Mono', Consolas, 'JetBrains Mono', monospace;
font-size: 12px;
color: rgba(255, 255, 255, 0.88);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 88%, transparent);
}
.hl-cell-note {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 70%, transparent);
}
.hl-cell-empty {
font-size: 12px;
color: rgba(255, 255, 255, 0.3);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 30%, transparent);
}
.hl-preview-text {
@@ -114,7 +117,7 @@
.hl-editor-label {
flex: 0 0 56px;
color: rgba(255, 255, 255, 0.88);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 88%, transparent);
padding-top: 5px;
}
@@ -129,7 +132,7 @@
.hl-editor-hint {
align-self: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.45);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 45%, transparent);
}
.hl-editor-error {
@@ -149,7 +152,7 @@
.hl-editor-sub-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 60%, transparent);
}
.hl-editor-inline {
@@ -170,7 +173,7 @@
width: 40px;
height: 26px;
padding: 0;
border: 1px solid rgba(255, 255, 255, 0.2);
border: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 20%, transparent);
border-radius: 4px;
background: transparent;
cursor: pointer;
@@ -197,7 +200,7 @@
.hl-palette-tag {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 50%, transparent);
margin-right: 2px;
}
@@ -205,7 +208,7 @@
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.3);
border: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 30%, transparent);
cursor: pointer;
display: inline-block;
transition: transform 0.12s ease, box-shadow 0.12s ease;
@@ -218,4 +221,4 @@
.hl-swatch-selected {
box-shadow: 0 0 0 2px var(--chrome-accent) inset;
border-color: var(--chrome-accent);
}
}
+22 -19
View File
@@ -1,5 +1,8 @@
/* ============================================================
Settings dialog + theme editor styles (settings/**, theme/**)
Text/border tints derive from --chrome-fg via color-mix so the
dialog follows the active terminal theme (white tints on dark
themes, black tints on light themes).
============================================================ */
/* ---------- settings dialog scaffolding ---------- */
@@ -106,7 +109,7 @@
}
.settings-row:hover {
background: rgba(255, 255, 255, 0.03);
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 3%, transparent);
}
.settings-row + .settings-row {
@@ -115,7 +118,7 @@
.settings-row-label {
flex: 0 0 46%;
color: rgba(255, 255, 255, 0.88);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 88%, transparent);
font-size: 13px;
line-height: 1.4;
}
@@ -125,7 +128,7 @@
margin-top: 2px;
font-size: 12px;
font-weight: 400;
color: rgba(255, 255, 255, 0.45);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 45%, transparent);
}
.settings-row-control {
@@ -156,7 +159,7 @@
.settings-block-hint {
font-weight: 400;
font-size: 12px;
color: rgba(255, 255, 255, 0.45);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 45%, transparent);
}
.settings-radio-group {
@@ -192,7 +195,7 @@
.settings-preview-title {
display: block;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 50%, transparent);
margin-bottom: 8px;
}
@@ -247,14 +250,14 @@
border-radius: 8px;
overflow: hidden;
cursor: pointer;
border: 2px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border: 2px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 12%, transparent);
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 4%, transparent);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.theme-card:hover {
border-color: var(--chrome-accent);
background: rgba(255, 255, 255, 0.07);
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 7%, transparent);
}
.theme-card-selected {
@@ -280,7 +283,7 @@
.theme-card-name {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.88);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 88%, transparent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -293,7 +296,7 @@
padding: 2px 5px;
border-radius: 4px;
background: rgba(59, 142, 234, 0.2);
color: #6fb3ff;
color: color-mix(in srgb, var(--chrome-accent, #3b8eea) 55%, var(--chrome-fg, #cccccc));
}
.theme-card-dots {
@@ -306,7 +309,7 @@
width: 14px;
height: 14px;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.25);
border: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 25%, transparent);
display: inline-block;
}
@@ -336,12 +339,12 @@
.theme-card-new-icon {
font-size: 26px;
line-height: 1;
color: rgba(255, 255, 255, 0.55);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 55%, transparent);
}
.theme-card-new-text {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 60%, transparent);
}
/* ---------- theme editor ---------- */
@@ -359,7 +362,7 @@
.theme-editor-label {
flex: 0 0 56px;
color: rgba(255, 255, 255, 0.88);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 88%, transparent);
}
.theme-editor-colors {
@@ -379,7 +382,7 @@
.theme-editor-color-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 70%, transparent);
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
@@ -391,7 +394,7 @@
width: 40px;
height: 26px;
padding: 0;
border: 1px solid rgba(255, 255, 255, 0.2);
border: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 20%, transparent);
border-radius: 4px;
background: transparent;
cursor: pointer;
@@ -408,7 +411,7 @@
.theme-editor-divider {
grid-column: 1 / -1;
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-top: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 10%, transparent);
margin: 2px 0;
}
@@ -419,7 +422,7 @@
.theme-editor-preview-label {
display: block;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 50%, transparent);
margin-bottom: 6px;
}
@@ -495,7 +498,7 @@
}
.about-changelog {
border-top: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 8%, transparent);
padding-top: 12px;
}
+45 -6
View File
@@ -7,6 +7,17 @@ function parseHex(color: string): [number, number, number] | null {
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
}
/** WCAG relative luminance (0=black, 1=white); null for non-hex input. */
function luminance(color: string): number | null {
const c = parseHex(color)
if (!c) return null
const lin = c.map((v) => {
const s = v / 255
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4
})
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
}
function toHex(v: number): string {
return Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')
}
@@ -19,6 +30,10 @@ export function mix(a: string, b: string, t: number): string | null {
return `#${toHex(ca[0] + (cb[0] - ca[0]) * t)}${toHex(ca[1] + (cb[1] - ca[1]) * t)}${toHex(ca[2] + (cb[2] - ca[2]) * t)}`
}
/** Whether the active terminal theme has a light background. Tab bars derived
* from it flip their overlay direction (white-on-dark vs black-on-light). */
let chromeIsLight = false
/** Derive the app chrome palette (sidebar, tab bars, dividers) from the active
* terminal theme so the whole window follows theme switches instead of only
* the terminal canvas. Values land on :root CSS variables; anything the theme
@@ -30,13 +45,36 @@ export function applyChromeTheme(colors: ThemeColors): void {
}
set('--chrome-bg', colors.background)
set('--chrome-fg', colors.foreground)
set('--chrome-bg-deep', mix(colors.background, '#000000', 0.35))
set('--chrome-hover', mix(colors.background, colors.foreground, 0.08))
set('--chrome-border', mix(colors.background, colors.foreground, 0.16))
chromeIsLight = (luminance(colors.background) ?? 0) > 0.5
if (chromeIsLight) {
// Light chrome: keep the bar close to the background (a big mix toward
// black turns pale themes into a muddy gray strip) and overlay tabs with
// translucent black instead of the translucent white used on dark themes.
set('--chrome-bg-deep', mix(colors.background, '#000000', 0.08))
set('--chrome-hover', mix(colors.background, '#000000', 0.06))
set('--chrome-border', mix(colors.background, '#000000', 0.2))
set('--chrome-tab-bg', 'rgba(0, 0, 0, 0.045)')
set('--chrome-tab-bg-hover', 'rgba(0, 0, 0, 0.09)')
set('--chrome-tab-ring', 'rgba(0, 0, 0, 0.18)')
set('--chrome-tab-ring-hover', 'rgba(0, 0, 0, 0.32)')
set('--chrome-tab-fg', mix(colors.background, '#000000', 0.68))
set('--chrome-tab-fg-hover', mix(colors.background, '#000000', 0.88))
} else {
set('--chrome-bg-deep', mix(colors.background, '#000000', 0.35))
set('--chrome-hover', mix(colors.background, colors.foreground, 0.08))
set('--chrome-border', mix(colors.background, colors.foreground, 0.16))
set('--chrome-tab-bg', 'rgba(255, 255, 255, 0.04)')
set('--chrome-tab-bg-hover', 'rgba(255, 255, 255, 0.07)')
set('--chrome-tab-ring', 'rgba(255, 255, 255, 0.1)')
set('--chrome-tab-ring-hover', 'rgba(255, 255, 255, 0.2)')
set('--chrome-tab-fg', '#98a2ab')
set('--chrome-tab-fg-hover', '#d0d0d0')
}
}
/** Tab accent: base color + a lightened foreground variant for text on dark
* tinted backgrounds. Consumed via var(--tab-accent*) in workspace.css. */
/** Tab accent: base color + a foreground variant that stays readable on the
* bar (lightened on dark chrome, darkened on light chrome). Consumed via
* var(--tab-accent*) in workspace.css. */
export function applyTabAccent(color: string): void {
const style = document.documentElement.style
// Settings hydrated from an older main process may lack the key; clear the
@@ -47,5 +85,6 @@ export function applyTabAccent(color: string): void {
return
}
style.setProperty('--tab-accent', color)
style.setProperty('--tab-accent-fg', mix(color, '#ffffff', 0.18) ?? color)
const fg = chromeIsLight ? mix(color, '#000000', 0.35) : mix(color, '#ffffff', 0.18)
style.setProperty('--tab-accent-fg', fg ?? color)
}
+32 -18
View File
@@ -35,14 +35,14 @@
border: none;
border-radius: 5px;
background: transparent;
color: #9a9a9a;
color: var(--chrome-tab-fg, #9a9a9a);
font-size: 15px;
cursor: pointer;
}
.workspace-rail-btn:hover {
background: var(--chrome-hover);
color: #ffffff;
color: var(--chrome-fg, #ffffff);
}
.workspace-rail-divider {
@@ -54,7 +54,7 @@
/* ---- M6.1 rail active state (mode entries) ---- */
.workspace-rail-btn.is-active {
color: var(--dv-active-tab-foreground-color, #e8e8e8);
color: var(--tab-accent-fg, #e8e8e8);
box-shadow: inset 2px 0 0 var(--tab-accent, #3fb950);
background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent);
}
@@ -179,6 +179,18 @@
/* Pane split dividers: soft slate band at rest, accent blue on hover/drag. */
--dv-sash-color: rgba(148, 163, 184, 0.28);
--dv-active-sash-color: rgba(59, 142, 234, 0.75);
/* Tab surfaces. Dockview paints tabs through group-scoped rules that
outspecify our .dv-tab rules, so theme the tabs via the vars those rules
consume — otherwise the abyss defaults (dark navy) leak onto light themes. */
--dv-activegroup-visiblepanel-tab-background-color: color-mix(
in srgb, var(--tab-accent, #3fb950) 10%, transparent);
--dv-activegroup-visiblepanel-tab-color: var(--tab-accent-fg, #3fd06a);
--dv-activegroup-hiddenpanel-tab-background-color: var(--chrome-tab-bg, rgba(255, 255, 255, 0.04));
--dv-activegroup-hiddenpanel-tab-color: var(--chrome-tab-fg, #98a2ab);
--dv-inactivegroup-visiblepanel-tab-background-color: var(--chrome-tab-bg, rgba(255, 255, 255, 0.04));
--dv-inactivegroup-visiblepanel-tab-color: var(--chrome-tab-fg, #98a2ab);
--dv-inactivegroup-hiddenpanel-tab-background-color: var(--chrome-tab-bg, rgba(255, 255, 255, 0.04));
--dv-inactivegroup-hiddenpanel-tab-color: var(--chrome-tab-fg, #98a2ab);
}
/* ---- tab polish: low rectangular tabs, quiet inactive, green active ---- */
@@ -191,16 +203,18 @@
border-radius: 3px;
padding: 0 8px;
margin-right: 4px;
background: rgba(255, 255, 255, 0.04);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.10);
color: #98a2ab;
background: var(--chrome-tab-bg, rgba(255, 255, 255, 0.04));
box-shadow: inset 0 0 0 1px var(--chrome-tab-ring, rgba(255, 255, 255, 0.1));
color: var(--chrome-tab-fg, #98a2ab);
transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
.workspace-dockview .dv-tab.dv-inactive-tab:hover {
background: rgba(255, 255, 255, 0.07);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.20);
color: #d0d0d0;
/* Group-scoped chain matches dockview's own specificity so the hover tint
wins over its group-level background rules. */
.workspace-dockview .dv-groupview .dv-tabs-container .dv-tab.dv-inactive-tab:hover {
background: var(--chrome-tab-bg-hover, rgba(255, 255, 255, 0.07));
box-shadow: inset 0 0 0 1px var(--chrome-tab-ring-hover, rgba(255, 255, 255, 0.2));
color: var(--chrome-tab-fg-hover, #d0d0d0);
}
/* XTerminal-style active tab: green outline + tinted bg + green title. */
@@ -229,7 +243,7 @@
width: 15px;
height: 15px;
border-radius: 2px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
box-shadow: inset 0 0 0 1px var(--chrome-tab-ring-hover, rgba(255, 255, 255, 0.16));
opacity: 0.8;
transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
@@ -432,9 +446,9 @@ body.ssh-resizing .ssh-bottom-divider {
padding: 0;
border: none;
border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
box-shadow: inset 0 0 0 1px var(--chrome-tab-ring, rgba(255, 255, 255, 0.16));
background: transparent;
color: #98a2ab;
color: var(--chrome-tab-fg, #98a2ab);
font-size: 10px;
cursor: pointer;
transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
@@ -464,18 +478,18 @@ body.ssh-resizing .ssh-bottom-divider {
padding: 0;
border: none;
border-radius: 6px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
box-shadow: inset 0 0 0 1px var(--chrome-tab-ring, rgba(255, 255, 255, 0.16));
background: transparent;
color: #98a2ab;
color: var(--chrome-tab-fg, #98a2ab);
font-size: 10px;
cursor: pointer;
transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
}
.workspace-tabbar-btn2:hover {
background: rgba(255, 255, 255, 0.07);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.24);
color: #ffffff;
background: var(--chrome-tab-bg-hover, rgba(255, 255, 255, 0.07));
box-shadow: inset 0 0 0 1px var(--chrome-tab-ring-hover, rgba(255, 255, 255, 0.24));
color: var(--chrome-tab-fg-hover, #ffffff);
}
.workspace-tabbar-btn2.is-active {