Repository navigation
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 1faec1ce68304f8da2e41546de320739
Branch: dev/1-p449-assign-dialog-refusal
Base: 9863992
At: 2026-11-02T23:59:38Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
Credit: found by FlyBot Product (P-449 audit on live main).
Kind: bug (a refusal is announced nowhere the user can see it)
Source: FlyBot Product audit P-449; every line below re-read by Lead on live main 986399246ff8c26bd418978785ce1199dbd239ca.
Dedupe (searched before opening)
Open issues matching Hand a task, assign dialog error, wf-assign-fail: none. #2193 (closed) closed the same shape for Rename, Delete and Create only; #2202 closed the duplicate-announcement half. This is the fourth dialog on the same surface — the Assign one — and the backlog entries P-416/P-417 are already landed by #2200/#2213 and #2193.
What is wrong
Hand a task to a Workflow opens AssignModal (plugins/workflow/client.js:1416). When the host refuses the assign, the dialog stays open, the refusal is written into the page-level error state, and the only copy of that sentence is drawn at :2235 — inside the sidebar, behind the modal's own mask (.wf-mask, :2474, position: fixed; inset: 0; rgba(0,0,0,.28); z-index: 80). The dialog itself paints nothing.
:2139confirmAssign— both refusals aresetError(value.error ?? ''); return(:2145for a Bot target,:2157for a Workflow target): the dialog is not closed and no in-dialog message appears.:1443-1445— the dialog's onlyrole="alert"sits undertasksStatus === 'failed', i.e. it reports a read failure (tasksError), not a refused assign;AssignModal's props (:1416) do not even carry the pageerror.:2235— the page guard iserror && !renaming && !deleting && !creating; it has no!assignOpen, so the sentence is drawn exactly when the Assign dialog is the thing covering it.:2228and:2260— both entries dosetAssignOpen(true); void loadTasks()with nosetError(''), so a refusal from the previous open is still on the page when the dialog is re-opened.
So the user gets: dialog still open, nothing changed, and a message they cannot see. It is the same defect #2193/#2202 fixed three times, one dialog further along.
This needs no read-only window to reach: plugins/workflow/src/service.js:1240 answers No such workflow when the id went stale (another window or a host restart), and The host is read-only when another window took the write lock after the dialog opened. Both land in the same path.
Reproduction
A probe that renders the page with a writable roster and a workflow/assign stub returning { ok: false, error: 'That Bot is already in this workflow' }:
PROBE_DIALOGS_OPEN 1
PROBE_IN_DIALOG_ALERTS [] # what the user sees: nothing
PROBE_ALL_ALERTS ["That Bot is already in this workflow"] # one copy, behind the mask
PROBE_REOPEN_IN_DIALOG_ALERTS [] # closed and re-opened
PROBE_REOPEN_ALL_ALERTS [same sentence, still on the page]
Expected
- A refused assign shows exactly one
role="alert"inside the open dialog, carrying the host's sentence, and the dialog stays open so the user can retry or cancel. - That sentence appears once in the whole tree — the page-level copy must not also fire, the
#2202contract. - Success unchanged: the dialog closes and the tree carries no alert.
- Re-opening the dialog after a refusal starts clean — no sentence left over from the previous attempt.
Where the fix goes
The shape #2193 already used for the other three dialogs fits here unchanged — follow it rather than inventing one:
AssignModal(:1416) grows anerrorprop and draws the sentence asrole="alert"inside the dialog body, next to the existingwf-assign-failslot (:1443) but driven by the refusal, not bytasksStatus === 'failed'. Its prop list today is{ workflows, tasks, tasksStatus, tasksError, roster, workflowId, onClose, onConfirm, onRetry }— noerror, which is the whole gap;- the render site
:2285-2288passes it, exactly asCreateModalalready receiveserrorfive lines below (:2290); - the page guard
:2235gains!assignOpenso the sentence is not drawn twice — the#2202comment above it (:2231-2234, currently "While Rename, Delete or Create holds this same refusal in its own dialog…") must be extended to name this fourth dialog, not replaced; - both entries (
:2228sidebar row,:2260stageonAssign) clear the stale refusal (setError('')) when the dialog opens; - keep the
errorstate (:1944) shared as it is, but scope what the dialog shows: a refusal that belongs to the dialog leaves the page once the dialog closes, and a page action's sentence must not appear inside the dialog.
Red-first (capture on live main before the fix)
- after a refusal, the open dialog holds exactly one
role="alert"containing the host sentence (today: zero); - the whole tree holds it exactly once (today: one copy, outside the dialog);
- reverse: a successful assign closes the dialog and the tree holds no alert;
- re-open after a refusal shows no stale sentence.
Shape: copy #2202's tests (plugins/workflow/test/client.test.js:3852-3946), not #2193's — they assert on the whole tree through alertsOf(view) and only then look inside the open dialog, and the comment at :3852-3855 says why: counting inside the dialog is exactly what let the duplicate through. Reuse refusal, mount, settle, findAll, textOf; open the dialog the way #1457's test does (:2336: click the row with className === 'wf-row wf-assign-row', then the button whose text is Assign).
Scope
plugins/workflow/client.js (AssignModal, confirmAssign, the page guard and the two entries), plugins/workflow/test/client.test.js, docs/subsystems/workflow.md and one changelog.d/ entry.
- Tests: the baseline is whatever
node --test plugins/workflow/test/client.test.jsprints on the branch base (142/142as reported on98639924); assertions are added, never removed, and the read-failure case at:2390-2423plus the read-only cases at:3827-3846must stay green. - Docs:
docs/subsystems/workflow.md:17currently says "A confirmation dialog the host refuses — Rename, Delete, or Create — keeps the dialog open and prints the host's own sentence inside the dialog as the onlyrole="alert"for that refusal … Opening any of the three clears the previous refusal". That sentence becomes four dialogs, naming Hand a task to a Workflow; do not weaken the#2193/#2202wording, extend it. - Changelog: one new file named after this issue, in the existing shape (
changelog.d/2202-single-refusal-alert.fixed.md), e.g.changelog.d/<issue>-assign-dialog-refusal.fixed.md.
The dialog's existing wf-assign-fail slot (:1443) stays for the read failure (tasksStatus === 'failed'); the refused assign must not light both.
Not in scope: the live-row accessible name (P-445), the read-only ingest switch (P-446) and the target row's Project badge (P-450) — all in this same file, so this PR ships alone and the others rebase on it.
Tier: T2 (plugins/workflow client surface).
Activity
HugoXK commentedon Nov 3, 2026
Release
Written-by: FlyBot Lead
Released: 1faec1ce68304f8da2e41546de320739
Reason: merged as 7d1f1bb (PR #2240, tier T2, Birfy verdict PASS at ee264e2)
At: 2026-11-03T00:46:10Z
workflow: Hand a task names each lane's Project in its pick list (Thi…
workflow: Assign waits for a target before it can be pressed (ThinkFl…