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/1459.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Workflow stage: fix flowchart wire alignment, compact sidebar thumbnails, a breathing border on the running node, no message composer on the stage, and stop ephemeral node replies from appearing in persistent Bot chats (e.g. Chief). Progress stays on the workflow row and graph. ([#1459](https://github.com/ThinkFlowLab/FlyBot/issues/1459))
14 changes: 8 additions & 6 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4242,11 +4242,13 @@ button.bt-set-row:hover{background:var(--bt-hover)}
.bt-wf-prompt{display:flex;flex-direction:column;gap:4px;font-size:12px;min-width:0}
.bt-wf-prompt textarea{width:100%;box-sizing:border-box;border-radius:8px;border:1px solid var(--bt-line);background:var(--bt-card);color:inherit;font:inherit;font-size:12px;padding:6px 8px;resize:vertical}
.bt-wf-plane{position:relative;width:0;margin:16px auto 0}
.bt-wf-wires{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}
.bt-wf-wires{position:absolute;left:calc(-1 * var(--bt-wf-wire-span,480px));top:0;width:calc(2 * var(--bt-wf-wire-span,480px));overflow:visible;pointer-events:none}
.bt-wf-wires path{fill:none;stroke:var(--bt-ink-3);stroke-width:1.6}
.bt-wf-wires marker path{fill:var(--bt-ink-3);stroke:none}
.bt-wf-node{position:absolute;width:168px;box-sizing:border-box;border:1px solid var(--bt-line);background:var(--bt-card);border-radius:12px;padding:8px 9px;display:flex;align-items:center;gap:8px}
.bt-wf-node.running{border-color:var(--bt-accent);box-shadow:0 0 0 3px rgba(77,107,254,.12)}
@keyframes bt-wf-node-live{0%,100%{border-color:var(--bt-accent);box-shadow:0 0 0 2px rgba(77,107,254,.18),0 0 0 0 rgba(77,107,254,.2)}50%{border-color:rgba(77,107,254,.85);box-shadow:0 0 0 3px rgba(77,107,254,.35),0 0 14px 2px rgba(77,107,254,.22)}}
.bt-wf-node.running{border-color:var(--bt-accent);animation:bt-wf-node-live 1.5s ease-in-out infinite}
@media (prefers-reduced-motion:reduce){.bt-wf-node.running{animation:none;box-shadow:0 0 0 3px rgba(77,107,254,.2)}}
.bt-wf-node.done{border-color:rgba(22,163,74,.35)}
.bt-wf-node.failed{border-color:#e5484d}
.bt-wf-node.queued{opacity:.78}
Expand Down Expand Up @@ -14450,9 +14452,10 @@ function hitNode(event) {
return (nested ?? direct)?.dataset?.node || "";
}
var WIRE_SPAN = 480;
var NODE_WIRE_OUT = 56;
function wirePath(from, to) {
const x1 = from.x + WIRE_SPAN;
const y1 = from.y + 48;
const y1 = from.y + NODE_WIRE_OUT;
const x2 = to.x + WIRE_SPAN;
const y2 = to.y;
const mid = (y1 + y2) / 2;
Expand Down Expand Up @@ -14520,16 +14523,15 @@ function GraphCanvas({ graph, editing, roster, onCommit }) {
))) : null,
(0, import_react34.createElement)(
"div",
{ className: "bt-wf-plane", "data-plane": "1", style: { height } },
{ className: "bt-wf-plane", "data-plane": "1", style: { height, "--bt-wf-wire-span": `${WIRE_SPAN}px` } },
(0, import_react34.createElement)(
"svg",
{
className: "bt-wf-wires",
"aria-hidden": true,
width: WIRE_SPAN * 2,
height,
viewBox: `0 0 ${WIRE_SPAN * 2} ${height}`,
style: { left: -WIRE_SPAN, width: WIRE_SPAN * 2, height }
viewBox: `0 0 ${WIRE_SPAN * 2} ${height}`
},
(0, import_react34.createElement)(
"defs",
Expand Down
6 changes: 4 additions & 2 deletions core/src/client/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -547,11 +547,13 @@ button.bt-set-row:hover{background:var(--bt-hover)}
.bt-wf-prompt{display:flex;flex-direction:column;gap:4px;font-size:12px;min-width:0}
.bt-wf-prompt textarea{width:100%;box-sizing:border-box;border-radius:8px;border:1px solid var(--bt-line);background:var(--bt-card);color:inherit;font:inherit;font-size:12px;padding:6px 8px;resize:vertical}
.bt-wf-plane{position:relative;width:0;margin:16px auto 0}
.bt-wf-wires{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}
.bt-wf-wires{position:absolute;left:calc(-1 * var(--bt-wf-wire-span,480px));top:0;width:calc(2 * var(--bt-wf-wire-span,480px));overflow:visible;pointer-events:none}
.bt-wf-wires path{fill:none;stroke:var(--bt-ink-3);stroke-width:1.6}
.bt-wf-wires marker path{fill:var(--bt-ink-3);stroke:none}
.bt-wf-node{position:absolute;width:168px;box-sizing:border-box;border:1px solid var(--bt-line);background:var(--bt-card);border-radius:12px;padding:8px 9px;display:flex;align-items:center;gap:8px}
.bt-wf-node.running{border-color:var(--bt-accent);box-shadow:0 0 0 3px rgba(77,107,254,.12)}
@keyframes bt-wf-node-live{0%,100%{border-color:var(--bt-accent);box-shadow:0 0 0 2px rgba(77,107,254,.18),0 0 0 0 rgba(77,107,254,.2)}50%{border-color:rgba(77,107,254,.85);box-shadow:0 0 0 3px rgba(77,107,254,.35),0 0 14px 2px rgba(77,107,254,.22)}}
.bt-wf-node.running{border-color:var(--bt-accent);animation:bt-wf-node-live 1.5s ease-in-out infinite}
@media (prefers-reduced-motion:reduce){.bt-wf-node.running{animation:none;box-shadow:0 0 0 3px rgba(77,107,254,.2)}}
.bt-wf-node.done{border-color:rgba(22,163,74,.35)}
.bt-wf-node.failed{border-color:#e5484d}
.bt-wf-node.queued{opacity:.78}
Expand Down
6 changes: 3 additions & 3 deletions core/src/client/workflows.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,11 @@ function hitNode(event) {
// be negative. The wire box is shifted by SPAN so those points stay inside the
// SVG viewport; overflow:visible is not reliable for a stroke on the edge.
const WIRE_SPAN = 480
const NODE_WIRE_OUT = 56

function wirePath(from, to) {
const x1 = from.x + WIRE_SPAN
const y1 = from.y + 48
const y1 = from.y + NODE_WIRE_OUT
const x2 = to.x + WIRE_SPAN
const y2 = to.y
const mid = (y1 + y2) / 2
Expand Down Expand Up @@ -177,11 +178,10 @@ function GraphCanvas({ graph, editing, roster, onCommit }) {
onCommit?.({ nodes: nodes.map((item) => (item.id === node.id ? { ...item, prompt } : item)), edges })
},
})))) : null,
h('div', { className: 'bt-wf-plane', 'data-plane': '1', style: { height } },
h('div', { className: 'bt-wf-plane', 'data-plane': '1', style: { height, '--bt-wf-wire-span': `${WIRE_SPAN}px` } },
h('svg', {
className: 'bt-wf-wires', 'aria-hidden': true,
width: WIRE_SPAN * 2, height, viewBox: `0 0 ${WIRE_SPAN * 2} ${height}`,
style: { left: -WIRE_SPAN, width: WIRE_SPAN * 2, height },
},
h('defs', null,
h('marker', {
Expand Down
1 change: 1 addition & 0 deletions docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second
### New-chat menu

- The + menu is a popover anchored next to the sidebar **+** (#195, #173): its `.bt-pane` carries `data-menu` and stays see-through, the menu is a 280px card at the top left with no title row, and a press on the pane outside the card (or Escape) closes it. New chat, New group chat, New Bot and plugin rows open their full pane as before.
- Settings → **Workflows** flowchart wire plane (#1459): `GraphCanvas` in `workflows.js` places nodes on a zero-width center line (`.bt-wf-plane`). Connector paths are drawn in an SVG shifted by `WIRE_SPAN` (480): the plane sets `--bt-wf-wire-span` on its inline style and `.bt-wf-wires` in `styles.js` uses `left:calc(-1 * var(--bt-wf-wire-span))` and `width:calc(2 * var(--bt-wf-wire-span))`, so coordinates `node.x + WIRE_SPAN` line up with each card's center instead of fanning to a corner. The `@flybot/workflow` stage drawer uses the same anchoring (`--wf-wire-span` on `.wf-plane`, matching rules on `.wf-wires`). `NODE_WIRE_OUT` is the bottom port offset for the cubic segment. Tests: `plugins/workflow/test/client.test.js` (`wirePath` regression); Settings: `core/test/workflows-page.test.js`.
- Settings → **Workflows** (`src/client/workflows.js`, #1364) sits right after **Scheduled tasks**, beside the other automation page, so the page list's existing order and the plugin-slot rule (a `flybot.settings.page` entry is listed after Connectors) are unchanged. It is not a second row in the account menu: the menu keeps its one name per destination. The page reads `@flybot/workflow` through `call('workflow/state')`: **Factory presets** are the template catalog (a drawable graph, so a pipeline can be opened before it is copied) and **Yours** are the workflows saved on this machine whose ids are not those templates. Each user-owned workflow row shows rename (✎) and delete (×) action buttons; factory presets show no actions and refuse both operations (#1434). Rename opens a modal with a name field and calls `workflow/rename`; delete confirms ("Are you sure you want to delete '{name}'? This cannot be undone.") and calls `workflow/delete`, which removes the workflow from state, releases ephemeral Bots if a run exists, and cleans logs/runs/routes. Opening one draws the flowchart; a saved workflow can be dragged (move a node, draw an edge, drop a local Bot, remove a node) and each node's **system prompt** can be typed in a field beside the canvas — that prompt is what the node's run Bot receives — and the picture is saved with `workflow/graph` (#1393). **Use this** copies a factory template with `workflow/create`. A read that rejects or never returns settles as an error with **Retry**; an empty store says there are no workflows. When that read is `bot/not-found`, or the host says `Unknown FlyBot endpoint workflow/…`, the page says the Workflow plugin is not installed or not enabled (a default install includes `@flybot/workflow`) in the interface language, and **Retry** stays (#1381). Any other failure still shows its own reason. The page does not teach `workflows/<id>.json`. The definition engine that still stores those files stays on the host and is not this page. Tests: `core/test/workflows-page.test.js`.
- The sidebar footer button (`ConnectApps` in `sidebar.js`, label **Connect apps** / 连接应用) opens Settings > Connectors through `actions.openConnectors()`, the same call and the same words as the account menu's **Connect apps** row. The shell's plugin panel stays reachable in the Bot surface through the account menu's **Plugins** row (`actions.selectPanel('plugins')`, 插件), without switching to classic Agent mode.
- One name per destination in the account menu and command palette (#392, #393, #394): **Settings** opens the Settings dialog (General), since **Bot settings** / **Edit Bot** open one Bot; **Scheduled tasks** opens Settings → Scheduled tasks, the name Settings and a Bot's details use (DSH's own bundle stays "Automation tasks" in the Plugins hints); and **Switch to classic Agent mode** is the only way to classic Agent mode, so Settings → General is Appearance only.
Expand Down
6 changes: 3 additions & 3 deletions docs/subsystems/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ Everything in `plugins/workflow/`: the graph rules, the run store, the matcher,

| File | Role |
| --- | --- |
| `plugins/workflow/index.js` | Plugin entry (`flybot-workflow`). Endpoints, `run_workflow`, `define_workflow`, the board listener, and the reply middleware that reports a step result. |
| `plugins/workflow/index.js` | Plugin entry (`flybot-workflow`). Endpoints, `run_workflow`, `define_workflow`, the board listener, and the reply middleware that ingests a step result and denies delivery to persistent Bot chats. |
| `plugins/workflow/src/graph.js` | Nodes, edges, positions, and named inputs and outputs. A node starts when its forward predecessors are settled and a taken edge reaches it. Same-rank nodes run together. A forward cycle is refused. A loop edge is the only cycle. A `when` edge chooses a branch; the other is skipped. A required input with no upstream provider is refused before a run starts. |
| `plugins/workflow/src/service.js` | The store, lanes (active runs, FIFO queue, terminal history), `maxConcurrent`, `rename`, `delete`, `saveGraph`, `define`, `runNamed`, `ingest`, `stepResult`. `rename` updates a workflow's name and optional description; `delete` removes the workflow, releases ephemeral Bots, and cleans logs/runs/routes. Factory template ids (e.g. `delivery`) are normal saved rows and may be deleted. A spawn hook makes one run Bot per node. Disposing the plugin releases every run Bot first and drops those ids from the saved record only once the release has finished, handing the release back as a promise the host awaits, so a shutdown that is cut short cannot leave hidden Bots that no state names (#1387). A released run Bot also gives its runner name back to the allocator instead of keeping it for the lifetime of the process. A dispatch that does not land is not marked handed. |
| `plugins/workflow/src/ephemeral.js` | Builds each node's run Bot from its prompt, role, model and tools, and releases the ids the host returns. `permissionFor` picks `danger-full-access` when the node names a shell, otherwise `workspace-write`. |
| `plugins/workflow/src/match.js` | Which open card matches a Workflow or a local Bot. |
| `plugins/workflow/src/template.js` | 发布交付流水线 (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), chat stage (normal conversation UI), assign dialog (scrollable with sticky footer), flowchart drawer (collapsed by default, per-node inspector), and Settings → Workflow graphs (canvas-first with inspector panel). The product UI starts only live runs (assign, ingest, composer). The stage shows the queue and refreshes state while one is active. 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. |
| `plugins/workflow/client.js` | Sidebar section (with mini-flowcharts), workflow stage (view/control: progress strip and flowchart drawer — no message composer), assign dialog (scrollable with sticky footer), flowchart drawer (collapsed by default, per-node inspector), 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 one is 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. |

## Entry points

Expand All @@ -46,7 +46,7 @@ Everything in `plugins/workflow/`: the graph rules, the run store, the matcher,

1. Friends' Bots are not nodes. A `remote` Bot is refused by create, define and the matcher.
2. The canvas and the run share one graph: nodes, edges and positions. A drag commits `workflow/graph`. Status highlighting reads that same graph. Replacing edges does not recompute who is already running.
3. **A live run advances only when its ephemeral Bot replies, following the graph's edges in order.** That reply is the chat line. Nodes start only when their forward predecessors finish and edges reach them. There is no scripted or seeded run path: `workflow/state` exposes only `live === true` runs. A run does not create a schedule. `scheduleLabel` is display text. When the host can create Bots, starting a live run (`assign`, `run_workflow`, `ingest`) spawns one ephemeral Bot per node from that node's prompt, role, model and tools, sends it the step from the Main Bot, and removes every instance when the run finishes, fails for good, or is reset. A `failOnce` retry keeps them. They are hidden and left out of the sidebar. Loading the plugin does not spawn Bots for the default workflows. Loading is also the one moment a run the host left behind is noticed: a run still marked `running` is abandoned, so loading releases its instances and records the run as `failed`. Nothing resumes it. A run that already ended keeps its own status (#1388). A dispatch that does not land is not marked handed. The chat names the code, and the next hand-off tries that node again. It is not sent twice in the same hand-off. A reply that says `could not answer your message` or `model call failed` fails that node and does not start its successor. A 429 or rate-limit mention in any other reply does not.
3. **A live run advances only when its ephemeral Bot replies, following the graph's edges in order.** That reply advances the run and may appear on the workflow stage log; it is not delivered into a persistent sidebar Bot conversation (for example Chief). The `workflow-step` send middleware denies those replies after `stepResult` runs. Nodes start only when their forward predecessors finish and edges reach them. There is no scripted or seeded run path: `workflow/state` exposes only `live === true` runs. A run does not create a schedule. `scheduleLabel` is display text. When the host can create Bots, starting a live run (`assign`, `run_workflow`, `ingest`) spawns one ephemeral Bot per node from that node's prompt, role, model and tools, sends it the step from the Main Bot, and removes every instance when the run finishes, fails for good, or is reset. A `failOnce` retry keeps them. They are hidden and left out of the sidebar. Loading the plugin does not spawn Bots for the default workflows. Loading is also the one moment a run the host left behind is noticed: a run still marked `running` is abandoned, so loading releases its instances and records the run as `failed`. Nothing resumes it. A run that already ended keeps its own status (#1388). A dispatch that does not land is not marked handed. The chat names the code, and the next hand-off tries that node again. It is not sent twice in the same hand-off. A reply that says `could not answer your message` or `model call failed` fails that node and does not start its successor. A 429 or rate-limit mention in any other reply does not.
4. Each workflow keeps a FIFO queue and up to `maxConcurrent` live runs (default 2, an integer from 1 to 8 on the record or via `workflow/concurrency`). The host runs at most 6 live runs at once. A task that does not fit is enqueued. When a run finishes, every workflow with a free slot is drained, oldest enqueue first, until the host cap. A new assign on a workflow that already has a queue waits behind it. A task does not replace a running run or release that run's Bots. Migrating an old store keeps only runs that were still `running` with ephemeral Bot ids; other legacy scripted rows are dropped from active state (finished tasks may remain in lane history). History records a finished or failed live run. Lowering the cap does not stop a run that has already started. Assigning `{ botId }` claims the card for that local Bot and does not enqueue. A remote Bot is refused.
5. A node declares up to eight inputs and eight outputs. A required input is an output of the same name on an ancestor, or `task`, `title`, or `brief` on a root. A start with no provider is refused, and Bots spawned for that attempt are released. On a Bot-backed run a successor stays queued until its required inputs are present. If every forward predecessor is done, failed or skipped and an input is still missing, that node fails and the run stops. The dispatch text carries `Input <name>: <value>`. The Bot reports `OUTPUT <name>: <value>`; a single declared output may be the reply body. Nodes start in edge order.
6. Each node is a real ephemeral Bot for that run, then it is removed. A permanent sidebar Bot (`botId`) is optional and is not required for the step to run. `tools` on the node is what that Bot may use: `github` grants `mcp__github__*` (pull requests, reviews, comments); `bash` or `shell` sets the permission to `danger-full-access` and allows a full-access shell; any other step gets `workspace-write`. A hidden run Bot cannot show an approval card, so a granted tool is allowed and a connector it did not name is refused. The host stores this on the run Bot as `stepTools` and `permission`.
Expand Down
Loading
Loading