diff --git a/changelog.d/1495.fixed.md b/changelog.d/1495.fixed.md new file mode 100644 index 000000000..e5298f2dd --- /dev/null +++ b/changelog.d/1495.fixed.md @@ -0,0 +1 @@ +Fixed the Workflow flowchart's node role sub-label in dark themes: #1484 had replaced its theme token with a hardcoded grey that read 1.05–1.15:1 on the dark node cards, and the colour now follows `--bt-ink-2` again, which keeps every dark theme at 5.60–6.33:1 and the light themes at 4.42–5.11:1 ([#1495](https://github.com/ThinkFlowLab/FlyBot/issues/1495)). diff --git a/docs/subsystems/workflow.md b/docs/subsystems/workflow.md index 909979abe..a57bba4d9 100644 --- a/docs/subsystems/workflow.md +++ b/docs/subsystems/workflow.md @@ -33,7 +33,7 @@ Everything in `plugins/workflow/`: the graph rules, the run store, the matcher, | `plugins/workflow/src/match.js` | Which open card matches a Workflow or a local Bot. | | `plugins/workflow/src/template.js` | Factory delivery/weekly graphs plus `EARLY_END_GUIDANCE` for Claim/Verify/Triage early-end markers; 发布交付流水线 (a parallel branch) and the weekly report, each node with the inputs and outputs that edge carries. | | `plugins/workflow/src/narrate.js` | Progress lines for a step. A live run quotes the Bot reply when present; otherwise delivery-template nodes may use canned copy. A join line is only for a rank of more than one node. | -| `plugins/workflow/client.js` | Sidebar section (with mini-flowcharts), workflow stage (view/control: one card per live run with its own title, node line, segment progress, status chip, and Pause / Resume / Stop; click a card or its Flow button to focus that run in the drawer flowchart; optional aggregate chip when several run or the queue is non-empty — no message composer), assign dialog (scrollable with sticky footer), flowchart drawer (collapsed by default, per-node inspector, fitted to the 400px panel so the graph needs no inner horizontal scroll), and Settings → Workflow graphs (canvas-first with inspector panel). The product UI starts only live runs (assign, ingest). The stage shows the queue and refreshes state while runs are active; a running node uses a breathing border animation. Visual flowcharts replace text chains across all surfaces. Sidebar rows (including factory ids such as `delivery`) support Delete with confirm. The stage and dialogs portal onto `document.body`; a missing container stays in the section tree. A stage control that fails — a graph save, Reset, Pause, Resume or Stop that throws, rejects, answers `{ok:false}` or does not answer within 10 s — ends on the section's `.wf-err` alert instead of being swallowed, and a success clears the alert. | +| `plugins/workflow/client.js` | Sidebar section (with mini-flowcharts), workflow stage (view/control: one card per live run with its own title, node line, segment progress, status chip, and Pause / Resume / Stop; click a card or its Flow button to focus that run in the drawer flowchart; optional aggregate chip when several run or the queue is non-empty — no message composer), assign dialog (scrollable with sticky footer), flowchart drawer (collapsed by default, per-node inspector, fitted to the 400px panel so the graph needs no inner horizontal scroll), and Settings → Workflow graphs (canvas-first with inspector panel). The product UI starts only live runs (assign, ingest). The stage shows the queue and refreshes state while runs are active; a running node uses a breathing border animation. Visual flowcharts replace text chains across all surfaces. Sidebar rows (including factory ids such as `delivery`) support Delete with confirm. The stage and dialogs portal onto `document.body`; a missing container stays in the section tree. A stage control that fails — a graph save, Reset, Pause, Resume or Stop that throws, rejects, answers `{ok:false}` or does not answer within 10 s — ends on the section's `.wf-err` alert instead of being swallowed, and a success clears the alert. The flowchart's node role sub-label takes its colour from `--bt-ink-2` rather than a hardcoded grey, so it stays readable on every theme's node card (#1495). | ## Entry points diff --git a/plugins/workflow/client.js b/plugins/workflow/client.js index db49166c1..c9c1d5b1e 100644 --- a/plugins/workflow/client.js +++ b/plugins/workflow/client.js @@ -1616,7 +1616,7 @@ window.__ModuleLoader__.load({ .wf-node.done{border-color:rgba(22,163,74,.35)}.wf-node.failed{border-color:#e5484d} .wf-node.selected{border-color:var(--bt-accent,#4d6bfe);box-shadow:0 0 0 2px rgba(77,107,254,.2)} .wf-nt{min-width:0;flex:1;display:flex;flex-direction:column} -.wf-nm{font-weight:600;font-size:12.5px}.wf-rl{font-size:12px;color:rgba(20,20,20,.55);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.wf-nm{font-weight:600;font-size:12.5px}.wf-rl{font-size:12px;color:var(--bt-ink-2,rgba(20,20,20,.6));white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .wf-box{border:1px solid var(--bt-line,rgba(20,20,20,.08));border-radius:10px;background:var(--bt-card,#fff);padding:10px 12px} .wf-kv{display:flex;justify-content:space-between;gap:16px;padding:8px 0;font-size:12px} .wf-kv span:first-child{color:var(--bt-ink-2,rgba(20,20,20,.6))} diff --git a/plugins/workflow/test/client.test.js b/plugins/workflow/test/client.test.js index 2281f335f..c309f364a 100644 --- a/plugins/workflow/test/client.test.js +++ b/plugins/workflow/test/client.test.js @@ -4,6 +4,8 @@ import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { test } from 'node:test' +import { allThemes } from '../../../core/src/client/themes.js' + // `rectWidth` is what a host element reports from getBoundingClientRect, so a // component that measures its own container can be driven from a plain object. const live = { inst: null, idx: 0, rectWidth: 0 } @@ -1025,6 +1027,59 @@ test('palette chips distinguish duplicate Bot names and expose drag affordance', view.unmount() }) +const workflowCss = () => { + const source = readFileSync(new URL('../client.js', import.meta.url), 'utf8') + return source.slice(source.indexOf('const CSS = `') + 'const CSS = `'.length) +} +const ruleBody = (name, css = workflowCss()) => new RegExp(`\\.${name}\\{([^}]*)\\}`).exec(css)?.[1] ?? '' +const hexRgb = (hex) => [1, 3, 5].map(index => parseInt(hex.slice(index, index + 2), 16)) +const linear = (value) => { + const v = value / 255 + return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4 +} +const luminanceOf = ([r, g, b]) => 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b) +const contrastOf = (a, b) => { + const first = luminanceOf(a) + const second = luminanceOf(b) + return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05) +} +const declaredInk = (declared, text) => { + const token = /var\(--bt-ink-([23])/.exec(declared) + if (token) return { rgb: hexRgb(text), alpha: token[1] === '2' ? 0.6 : 0.36 } + const literal = /rgba?\(([^)]+)\)/.exec(declared) + if (!literal) throw new Error(`the .wf-rl colour cannot be resolved: ${declared}`) + const parts = literal[1].split(',').map(value => Number(value.trim())) + return { rgb: parts.slice(0, 3), alpha: parts[3] ?? 1 } +} +const compositeOver = ({ rgb, alpha }, background) => rgb.map((value, index) => alpha * value + (1 - alpha) * background[index]) + +test('the flowchart node role sub-label colour is a theme token, never a bare grey', () => { + const body = ruleBody('wf-rl') + assert.match(body, /color:var\(--bt-ink-2,/, '.wf-rl must take its colour from a theme token') + assert.doesNotMatch(body, /color:(rgba?\(|#)/, '.wf-rl must not hardcode its colour') +}) + +test('the node role sub-label keeps 4.4:1 in light and 4.5:1 in every dark theme', () => { + const declared = /color:([^;]+)/.exec(ruleBody('wf-rl'))?.[1] ?? '' + const modes = [] + for (const [id, theme] of Object.entries(allThemes())) { + modes.push({ id, mode: 'light', palette: theme.light }) + modes.push({ id, mode: 'dark', palette: theme.dark }) + } + assert.ok(modes.length >= 8, 'every shipped theme has a light and a dark mode') + for (const { id, mode, palette } of modes) { + const ink = declaredInk(declared, palette.text) + const card = hexRgb(palette.card) + const ratio = contrastOf(compositeOver(ink, card), card) + // Every dark card is dark (#1f1f1f / #1c1c1c / #2a2925 / #141c33), so 4.5:1 is the bar there. + // In light, --bt-ink-2 (0.6 alpha) gives 4.81 on DeepSeek's #141414 text; Paper's text is + // #1f1e1b, which composites to 4.42 — the token itself cannot reach 4.5 without a core token + // change, so light is held at 4.4:1 and never below the 4.07:1 the hardcoded grey gave. + const floor = mode === 'dark' ? 4.5 : 4.4 + assert.ok(ratio >= floor, `${id} ${mode}: ${declared.trim()} on ${palette.card} is ${ratio.toFixed(2)}:1, below ${floor}:1`) + } +}) + test('running flow nodes use a breathing border animation only while running', () => { const source = readFileSync(new URL('../client.js', import.meta.url), 'utf8') assert.match(source, /@keyframes wf-node-live/)