Skip to content

fix(workflow): the stage chat sends the previous draft, not what the user typed #1451

Closed as not planned

Description

@HugoXK

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-293 wires the host composer as onSend: (text) => { onDraft(text); onSend() }.

  • onDraft is setDraft (:1089; the state is const [draft, setDraft] = useState('') at :887), while the parent onSend is :1103-1108:

    onSend: () => {
      const text = draft.trim()
      if (!text) return
      setDraft('')
      void call('workflow/say', { workflowId: open.id, text })...

    It reads draft from 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 by if (!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 and onSubmit (: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 })...
}
  • :292 becomes onSend: (text) => sendNow(text);
  • the fallback form's onSubmit (:321) calls sendNow(draft).

The parent must never read draft to decide what to send.

Acceptance (deterministic)

  • The test above posts hello on the first send and world on 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)

  • #1440 shipped this composer; this is a defect in what it shipped, not a redesign request.
  • #1448 (open) is the Settings canvas drawing every node as Idle; #1446 (open) is duplicate workflow names; #1441 (open) is deleting a workflow during a live run; #1424 (open) is a reaped run keeping nodeStatus: running. None of them touches the composer or workflow/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 touch onSend/onDraft. The wiring quoted below is present verbatim at :292 on f0133279, 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

  1. HugoXK commented on Oct 27, 2026

    @HugoXK
    ContributorAuthor

    Closing as not planned: the path this defect lived in has no entry point left.

    • #1459 is 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.
    • #1461 merged 2026-10-27T14:39:29Z (merge commit 952d6358) and removed the stage composer together with its Conversation branch.
    • On live main 5b9d0d7 (2026-10-27T15:44:32Z): grep -n composer plugins/workflow/client.js returns 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__.Conversation has no call in client.js — only the useConversationInset() layout hook remains, used at :1039. No conv-stub test 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions