FN-6130: make compound engineering theme colors dark-mode aware

Align compound engineering dashboard colors with the active theme tokens.

- replace hardcoded and legacy compound engineering CSS colors with dashboard theme tokens
- switch dimmed text treatments from opacity to explicit theme-aware text tokens for light and dark mode
- add regression coverage for token usage in compound engineering styles
- keep branch authority checks compatible with the optional base SHA argument

Files changed:
 packages/engine/src/branch-conflicts.ts            |   1 +
 .../src/dashboard/CompoundEngineeringView.css      | 106 +++++++++++----------
 .../src/dashboard/__tests__/theme-tokens.test.ts   |  97 +++++++++++++++++++
 3 files changed, 153 insertions(+), 51 deletions(-)

Fusion-Task-Id: FN-6130

Fusion-Task-Lineage: 00adc217-0649-4b4f-9b9b-92c551f02926
This commit is contained in:
gsxdsm
2026-06-09 14:56:26 -07:00
parent e78203e7fb
commit 7f08845ed5
3 changed files with 153 additions and 51 deletions

View File

@@ -377,6 +377,7 @@ export async function isBranchAuthoritativeForTask(
repoDir: string,
branch: string,
taskId: string,
_baseSha?: string,
): Promise<{ ok: true } | { ok: false; reason: string }> {
try {
await revParse(repoDir, `refs/heads/${branch}`);

View File

@@ -1,5 +1,6 @@
.ce-view {
display: flex;
color: var(--text);
flex: 1 1 auto;
flex-direction: column;
gap: 1rem;
@@ -25,7 +26,7 @@
.ce-view-summary {
font-size: 0.8rem;
opacity: 0.7;
color: var(--text-muted);
}
.ce-loading,
@@ -35,7 +36,7 @@
}
.ce-view-error {
color: var(--color-danger, #d23);
color: var(--color-error);
}
.ce-empty {
@@ -52,7 +53,7 @@
}
.ce-empty-hint {
opacity: 0.7;
color: var(--text-muted);
font-size: 0.85rem;
}
@@ -63,7 +64,7 @@
}
.ce-group {
border: 1px solid var(--color-border, rgba(128, 128, 128, 0.25));
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.75rem 1rem;
display: flex;
@@ -72,7 +73,7 @@
}
.ce-group[data-empty="true"] {
opacity: 0.65;
color: var(--text-muted);
}
.ce-group-header {
@@ -88,13 +89,13 @@
.ce-group-count {
font-size: 0.75rem;
opacity: 0.6;
color: var(--text-dim);
}
.ce-group-empty {
margin: 0;
font-size: 0.8rem;
opacity: 0.6;
color: var(--text-dim);
}
.ce-artifact-list {
@@ -132,14 +133,14 @@
.ce-artifact-path {
font-size: 0.7rem;
opacity: 0.55;
color: var(--text-dim);
}
.ce-artifact-open {
appearance: none;
background: none;
border: none;
color: var(--color-primary, #2563eb);
color: var(--color-info);
cursor: pointer;
font: inherit;
padding: 0;
@@ -148,11 +149,11 @@
.ce-artifact-open:hover,
.ce-artifact-open:focus-visible {
color: var(--color-primary-hover, #1d4ed8);
color: var(--color-info);
}
.ce-artifact-error .ce-artifact-error-msg {
color: var(--color-danger, #d23);
color: var(--color-error);
font-size: 0.78rem;
}
@@ -224,7 +225,7 @@
}
.ce-flow-status {
font-size: 0.72rem;
opacity: 0.7;
color: var(--text-muted);
text-transform: capitalize;
}
.ce-flow-close {
@@ -248,14 +249,14 @@
.ce-flow-turn-role {
font-size: 0.65rem;
text-transform: uppercase;
opacity: 0.55;
color: var(--text-dim);
}
.ce-flow-turn-agent .ce-flow-turn-text {
white-space: pre-wrap;
}
.ce-flow-thinking {
font-style: italic;
opacity: 0.7;
color: var(--text-muted);
}
.ce-flow-question {
display: flex;
@@ -268,7 +269,7 @@
}
.ce-flow-question-desc {
font-size: 0.8rem;
opacity: 0.75;
color: var(--text-muted);
margin: 0;
}
.ce-flow-text {
@@ -308,7 +309,7 @@
}
.ce-flow-option-desc {
font-size: 0.72rem;
opacity: 0.7;
color: var(--text-muted);
}
.ce-flow-checkbox {
display: flex;
@@ -317,26 +318,26 @@
cursor: pointer;
}
.ce-flow-error {
color: var(--color-danger, #d23);
color: var(--color-error);
font-size: 0.82rem;
}
/* Degraded chat fallback (R8/AE1) — must read as visibly distinct. */
.ce-flow-degraded {
border: 1px dashed var(--color-warning, #c80);
border: 1px dashed var(--color-warning);
border-radius: 6px;
padding: 0.6rem;
background: color-mix(in srgb, var(--color-warning, #c80) 8%, transparent);
background: color-mix(in srgb, var(--color-warning) 8%, transparent);
}
.ce-flow-degraded-banner {
margin: 0 0 0.4rem;
font-size: 0.78rem;
font-weight: 600;
color: var(--color-warning, #a60);
color: var(--color-warning);
}
.ce-flow-degraded-options {
font-size: 0.78rem;
opacity: 0.8;
color: var(--text-muted);
margin: 0 0 0.4rem;
padding-left: 1.1rem;
}
@@ -359,8 +360,8 @@
gap: 0.5rem;
}
.ce-session-row.is-active .ce-session-open {
border-color: var(--color-accent, #36c);
background: color-mix(in srgb, var(--color-accent, #36c) 8%, transparent);
border-color: var(--todo);
background: color-mix(in srgb, var(--todo) 8%, transparent);
}
.ce-session-open {
flex: 1;
@@ -369,7 +370,7 @@
gap: 0.6rem;
text-align: left;
padding: 0.4rem 0.6rem;
border: 1px solid var(--color-border, #ddd);
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
cursor: pointer;
@@ -384,26 +385,26 @@
.ce-session-status {
font-size: 0.74rem;
text-transform: capitalize;
opacity: 0.8;
color: var(--text-muted);
}
.ce-session-status-awaiting_input {
color: var(--color-warning, #a60);
color: var(--color-warning);
font-weight: 600;
opacity: 1;
}
.ce-session-status-error,
.ce-session-status-interrupted {
color: var(--color-danger, #d23);
opacity: 1;
color: var(--color-error);
}
.ce-session-status-completed {
color: var(--color-success, #2a7);
opacity: 1;
color: var(--color-success);
}
.ce-session-updated {
margin-left: auto;
font-size: 0.72rem;
opacity: 0.6;
color: var(--text-dim);
}
/* ── Q&A transcript bubbles ────────────────────────────────────────────── */
@@ -425,18 +426,18 @@
max-width: 85%;
padding: 0.45rem 0.65rem;
border-radius: 10px;
background: color-mix(in srgb, var(--color-border, #ddd) 30%, transparent);
background: color-mix(in srgb, var(--border) 30%, transparent);
}
.ce-flow-turn-user {
align-self: flex-end;
background: color-mix(in srgb, var(--color-accent, #36c) 12%, transparent);
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
.ce-flow-turn-role {
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
opacity: 0.55;
color: var(--text-dim);
}
.ce-flow-turn-text {
white-space: pre-wrap;
@@ -444,23 +445,26 @@
font-size: 0.86rem;
}
.ce-flow-turn-question {
border-left: 3px solid var(--color-accent, #36c);
border-left: 3px solid var(--todo);
}
.ce-flow-turn-answer.is-steering {
border-left: 3px solid var(--color-warning, #c80);
border-left: 3px solid var(--color-warning);
}
.ce-flow-turn-comment {
font-size: 0.78rem;
font-style: italic;
opacity: 0.85;
border-top: 1px dashed color-mix(in srgb, var(--color-border, #ddd) 60%, transparent);
color: var(--text-muted);
border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
padding-top: 0.25rem;
}
.ce-flow-turn-done {
align-self: center;
background: color-mix(in srgb, var(--color-success, #2a7) 10%, transparent);
background: color-mix(in srgb, var(--color-success) 10%, transparent);
font-size: 0.8rem;
}
.ce-flow-artifact-path {
color: var(--text-dim);
}
.ce-flow-turn-activity {
background: transparent;
padding: 0;
@@ -471,7 +475,7 @@
.ce-flow-activity-details summary {
cursor: pointer;
font-size: 0.78rem;
opacity: 0.7;
color: var(--text-muted);
}
.ce-flow-activity {
display: flex;
@@ -479,9 +483,9 @@
gap: 0.25rem;
margin: 0.3rem 0 0;
padding: 0.5rem 0.6rem;
border: 1px solid color-mix(in srgb, var(--color-border, #ddd) 70%, transparent);
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--color-border, #ddd) 12%, transparent);
background: color-mix(in srgb, var(--border) 12%, transparent);
max-height: 16rem;
overflow-y: auto;
}
@@ -493,7 +497,7 @@
font-family: var(--font-mono, ui-monospace, monospace);
}
.ce-activity-thinking {
opacity: 0.6;
color: var(--text-dim);
font-style: italic;
}
.ce-activity-tool {
@@ -501,13 +505,13 @@
font-family: var(--font-mono, ui-monospace, monospace);
}
.ce-activity-tool.is-running .ce-activity-tool-marker {
color: var(--color-accent, #36c);
color: var(--todo);
}
.ce-activity-tool.is-done .ce-activity-tool-marker {
color: var(--color-success, #2a7);
color: var(--color-success);
}
.ce-activity-tool.is-error .ce-activity-tool-marker {
color: var(--color-danger, #d23);
color: var(--color-error);
}
/* ── Live working pane ──────────────────────────────────────────────────── */
@@ -520,13 +524,13 @@
gap: 0.45rem;
margin: 0 0 0.3rem;
font-size: 0.82rem;
opacity: 0.85;
color: var(--text-muted);
}
.ce-flow-pulse {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-accent, #36c);
background: var(--todo);
animation: ce-pulse 1.2s ease-in-out infinite;
}
@keyframes ce-pulse {
@@ -538,12 +542,12 @@
.ce-flow-guidance {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px dashed color-mix(in srgb, var(--color-border, #ddd) 70%, transparent);
border-top: 1px dashed color-mix(in srgb, var(--border) 70%, transparent);
}
.ce-flow-guidance-label {
display: block;
font-size: 0.74rem;
opacity: 0.65;
color: var(--text-dim);
margin-bottom: 0.3rem;
}
.ce-flow-guidance-row {

View File

@@ -0,0 +1,97 @@
import { describe, expect, it } from "vitest";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const cssPath = path.resolve(__dirname, "../CompoundEngineeringView.css");
const css = fs.readFileSync(cssPath, "utf-8");
function selectorBlocks(selector: string): string[] {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`${escaped}\\s*\\{[^}]*\\}`, "g");
return css.match(pattern) ?? [];
}
describe("CompoundEngineeringView theme tokens", () => {
it("does not use hardcoded legacy color fallbacks", () => {
const forbiddenPatterns = [
"#d23",
"#36c",
"#ddd",
"#c80",
"#a60",
"#2a7",
"#2563eb",
"#1d4ed8",
"rgba(128, 128, 128",
];
for (const pattern of forbiddenPatterns) {
expect(css, `expected CSS not to contain ${pattern}`).not.toContain(pattern);
}
});
it("does not reference non-existent dashboard color tokens", () => {
const forbiddenTokens = [
"--color-danger",
"--color-accent",
"--color-border",
"--color-primary",
"--color-primary-hover",
];
for (const token of forbiddenTokens) {
expect(css, `expected CSS not to reference ${token}`).not.toContain(token);
}
});
it("keeps color-mix expressions token-based", () => {
const colorMixCalls = css.match(/color-mix\([^)]*(?:\)[^)]*)?\)/g) ?? [];
expect(colorMixCalls.length).toBeGreaterThan(0);
for (const call of colorMixCalls) {
expect(call, `expected ${call} to reference a CSS token`).toContain("var(--");
expect(call, `expected ${call} not to contain hardcoded hex`).not.toMatch(/#[0-9a-fA-F]{3,8}/);
expect(call, `expected ${call} not to contain rgba()`).not.toMatch(/rgba\(/);
}
});
it("sets root view text color from the theme text token", () => {
const [viewBlock] = selectorBlocks(".ce-view");
expect(viewBlock).toBeDefined();
expect(viewBlock).toMatch(/color:\s*var\(--text\)\s*;/);
});
it("does not use opacity to dim text selectors", () => {
const textDimmingSelectors = [
".ce-view-summary",
".ce-empty-hint",
'.ce-group[data-empty="true"]',
".ce-group-count",
".ce-group-empty",
".ce-artifact-path",
".ce-flow-turn-role",
".ce-flow-option-desc",
".ce-flow-status",
".ce-session-status",
".ce-session-updated",
".ce-flow-guidance-label",
".ce-flow-thinking",
".ce-flow-question-desc",
".ce-flow-working-label",
".ce-flow-degraded-options",
".ce-flow-activity-details summary",
".ce-activity-thinking",
".ce-flow-turn-comment",
];
for (const selector of textDimmingSelectors) {
const blocks = selectorBlocks(selector);
expect(blocks, `expected to find selector ${selector}`).not.toHaveLength(0);
for (const block of blocks) {
expect(block, `expected ${selector} not to use opacity for text dimming`).not.toMatch(/opacity\s*:/);
}
}
});
});