Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1495.fixed.md
Original file line number Diff line number Diff line change
@@ -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)).
2 changes: 1 addition & 1 deletion docs/subsystems/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion plugins/workflow/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))}
Expand Down
55 changes: 55 additions & 0 deletions plugins/workflow/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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/)
Expand Down
Loading