Repository navigation
Description
Written-by: FlyBot Product
What happens
#1440 moved the Workflow stage's composer onto the host Conversation component. On that path, what gets posted is the previous draft, not what the user typed: typing hello and pressing send posts nothing at all, and the next send posts hello. The fallback composer in the same file (plugins/workflow/client.js:321) does not have this problem, so it only shows where __flybot__.Conversation exists — latent until core publishes the conversation surface.
Reproduction (deterministic, plugin's own harness)
plugins/workflow/test/client.test.js already has everything the repro needs: the react stub, mount, findAll, textOf, the delivery() graph and settle(). The only new thing is the host stub, and one test:
globalThis.__flybot__ = {
Conversation: (props) => react.createElement('div', { className: 'conv-stub', onSend: props.onSend }, 'conv'),
ConversationHeader: (props) => react.createElement('div', { className: 'conv-head' }, props.title),
}
const calls = []
const call = async (name, body) => {
calls.push({ name, body })
if (name === 'workflow/state') return { workflows: [delivery()], templates: [] }
return { ok: true, workflows: [delivery()] }
}
const view = mount(client.Section, { roster: { bots: [] }, call, actions: {} })
await settle()
findAll(view.tree, node => node.type === 'button' && textOf(node).includes('发布交付流水线'))[0].props.onClick()
await settle()
const conv = () => findAll(view.tree, node => node.props?.className === 'conv-stub')[0]
conv().props.onSend('hello')
await settle()
assert.deepEqual(calls.filter(item => item.name === 'workflow/say').map(item => item.body), [{ workflowId: 'delivery', text: 'hello' }])Today it fails, and the two lines that say so are:
PROBE D say calls after typing "hello": []
PROBE D say calls after typing "world": [{"workflowId":"delivery","text":"hello"}]
(the assertion fails with first send should post "hello", calls were []).
Root cause
-
plugins/workflow/client.js:290-293wires the host composer asonSend: (text) => { onDraft(text); onSend() }. -
onDraftissetDraft(:1089; the state isconst [draft, setDraft] = useState('')at:887), while the parentonSendis:1103-1108:onSend: () => { const text = draft.trim() if (!text) return setDraft('') void call('workflow/say', { workflowId: open.id, text })...
It reads
draftfrom this render's closure.onDraft(text)only schedules a re-render — React does not change state inside the running handler — so the parent always reads the value from before this batch: empty on the first send (swallowed byif (!text) return), the previously sent text on every send after that. -
The fallback form avoids it only by accident of event order:
onChange(:324) commits the draft on one event andonSubmit(:321) runs on the next, after the re-render.
Expected
The stage sends the text it is handed. One send function taking the text as a parameter:
const sendNow = (text) => {
const trimmed = text.trim()
if (!trimmed) return
setDraft('')
void call('workflow/say', { workflowId: open.id, text: trimmed })...
}:292becomesonSend: (text) => sendNow(text);- the fallback form's
onSubmit(:321) callssendNow(draft).
The parent must never read draft to decide what to send.
Acceptance (deterministic)
- The test above posts
helloon the first send andworldon the second. - With no
__flybot__.Conversation(fallback form) the typed text is still what gets posted.
Scope
S: one call site and the send function in plugins/workflow/client.js (:292, :1103-1108, the form submit at :321). No host change, no endpoint change, no storage change.
Reachability
The running instance is checkout e37f1f8, which predates the Workflows surface. This defect is not yet visible to users; it arrives with the next milestone update.
Ownership
plugins/workflow/** belongs to the workflow module. FlyBot reports this and does not implement it or open a PR for it.
Not a duplicate (as of my read)
#1440shipped this composer; this is a defect in what it shipped, not a redesign request.#1448(open) is the Settings canvas drawing every node asIdle;#1446(open) is duplicate workflow names;#1441(open) is deleting a workflow during a live run;#1424(open) is a reaped run keepingnodeStatus: running. None of them touches the composer orworkflow/say.#1450(closed, fixed by#1452) was the stage canvas overlaying the left sidebar; a different surface.#1452(merged,f0133279) restyled the stage and now hides ephemeral node lines; it did not touchonSend/onDraft. The wiring quoted below is present verbatim at:292onf0133279, so this bug was not fixed by it.- Search
stage chat→#1450,#1438(closed),#1375(closed),#1272;composer workflow→#659,#1059,#641(all closed, unrelated);draft workflow send→ 0.
Activity
HugoXK commentedon Oct 27, 2026
Closing as not planned: the path this defect lived in has no entry point left.
#1459is closed (2026-10-27T14:16:29Z). Its body carries the owner's decision:Workflow does not need messaging. Tasks are handed to the graph; pause/resume/progress only.#1461merged 2026-10-27T14:39:29Z (merge commit952d6358) and removed the stage composer together with itsConversationbranch.5b9d0d7(2026-10-27T15:44:32Z):grep -n composer plugins/workflow/client.jsreturns only the comment at:380(no message composer (#1459)) and the dead CSS at:1364-1366(.wf-composer-wrap/.wf-composer); no render path references the class.__flybot__.Conversationhas no call inclient.js— only theuseConversationInset()layout hook remains, used at:1039. Noconv-stubtest exists in the tree.No separate cleanup issue: the
.wf-composer*dead CSS is recorded in the backlog only and is not part of#1475.