You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
console: a sticky error toast stays on screen next to the later success toast — a multi-step form's refused submit and its eventual success are both shown at once #7252
Provenance: linked card filed by the objectstack triage seat (session session_019kDRpB7D2XzVzkaLp57T5D, R+89) from objectstack-ai/objectstack#14311 (maintainer-side dogfood of the objectui pin bump, objectstack PR #14295), per the multi-repo rule that side observations land in the repo whose code fixes them.
Step 3 Create → POST /api/v1/data/showcase_project → 400 VALIDATION_FAILED; the console shows a banner plus a sticky error toast ("Invalid project status transition.").
Back to step 2, change the status, Create → 201; a success toast appears ("Your new project is ready…").
The earlier error toast is still on screen beside the success toast, so the final screen shows both a failure and a success for the same form.
Expected
A subsequent successful submit of the same form dismisses (or supersedes) the sticky error toast raised by its earlier refusal; a form never ends on a screen that asserts both outcomes.
Claim: PM loop round 6 (domain:ui seat)
Session: session_01NRRumy89BYdW9ogbcdHTho
Branch: claude/issue-7252-form-error-toast-supersede
Worktree: objectui-issue-7252
Domain: domain:ui
File surface: apps/console/src/components/FormPage.tsx (the submit path's toast.error at lines 1743 and 1769 and its success toast) and, only if the wizard raises its own toasts, packages/plugin-form/src/WizardForm.tsx; tests beside the touched code; .changeset/ for any released package touched (apps/console publishes nothing; plugin-form does). ⛔ Not the state-machine refusal itself, its field location or localisation (objectstack#14311, domain:engine); ⛔ not deriving allowed initial values from stateMachine (a console feature, not filed).
Container & model: S, model: opus, mode:subagent
Clause-②: no
Serial constraints cleared: none — no open objectui PR in this seat's lane touches FormPage.tsx or WizardForm.tsx; the dogfood session's PR #7288 touches packages/components/src/renderers/layout/containers.tsx only.
Premise re-verified on origin/mainebc05b4d6 before dispatch: FormPage.tsx:1743 and :1769 still raise toast.error(...) on a refused submit; the only toast.dismiss() call in the tree is DraftChangesPanel.tsx:442, so nothing on the form path dismisses or supersedes an earlier error toast when a later submit succeeds. How the error toast becomes sticky (a duration option, the Toaster's defaults, or an Infinity on the error path) is the dev's first measurement, not assumed here.
Dev claim (os-dev, dispatched by the domain:ui PM seat)
Session: session_01NRRumy89BYdW9ogbcdHTho
Branch: claude/issue-7252-form-error-toast-supersede (empty branch pushed as the write-path probe — push returned [new branch], write path is green)
Worktree: /home/user/objectui-issue-7252
Base sha: ebc05b4d6 (origin/main at dispatch time)
File surface I expect to touch (to be confirmed once I have read the submit path): apps/console/src/components/FormPage.tsx, tests beside it, and packages/plugin-form/src/WizardForm.tsx plus a patch changeset only if that released package turns out to raise the toasts.
Out of scope per the card: the state-machine refusal itself and its localisation (objectstack-ai/objectstack#14311), and deriving allowed initial values from stateMachine.
{
"issue": 7252,
"status": "done",
"branch": "claude/issue-7252-form-error-toast-supersede",
"pr": "https://github.com/objectstack-ai/objectui/pull/7342",
"premise_still_valid": true,
"summary": "The card's defect and its Expected contract both hold and are implemented: a refused submit's toast is now superseded by the outcome of the next submit, so a form never ends on a screen asserting both outcomes. Two of the dispatch order's ZONE 2 assumptions were falsified on origin/main ebc05b4d6 and the fix moved accordingly. (1) The two toasts are NOT both on FormPage's submit path. FormPage renders a flat form and toasts the literal 'Submitted'; the reported wizard runs ObjectForm to WizardForm, whose handleStepSubmit rethrows a rejected write into the catch of the SDUI form renderer in packages/components/src/renderers/form/form.tsx, which raises the refusal (banner plus toast), while WizardForm raises the success. Two modules, which is why neither could supersede the other and why the fix lands in the renderer. (2) The refusal toast is NOT sticky by configuration: ConsoleToaster sets duration 4000 for every severity, the components Toaster sets none (sonner 2.0.8 default TOAST_LIFETIME is also 4000), and duration Infinity appears on no form path in the repo. Durations were therefore left untouched and only supersession was fixed, which reproduces inside any lifetime. Every outcome toast a form raises now travels under one stable per-form id (React.useId): the renderer publishes its three outcome toasts under it and retires the previous attempt's toast in the same place it already cleared the previous attempt's in-form banner; the console's FormPage shares one id between its confirmation and its submit failure. The objectui#4190 redirect-destination refusal is deliberately excluded and keeps its own toast, since the write succeeded there and that refusal must stay readable beside the confirmation it qualifies.",
"files_changed": [
"packages/components/src/renderers/form/form.tsx",
"apps/console/src/components/FormPage.tsx",
"packages/plugin-form/src/WizardForm.outcomeToastSupersede.test.tsx (new)",
"apps/console/src/components/FormPage.outcomeToast.test.tsx (new)",
".changeset/7252-form-outcome-toast-supersede.md (new, patch for components and console)"
],
"tests": "All runs through os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-7252; verdicts quoted from the lock's own VERDICT line, never a bare exit code. PRE-FIX RED, measured twice. First, an early run of the new wizard pin came back red with an EMPTY toast registry while the in-form banner rendered the refusal: 'os-verify-lock: VERDICT command-exit 1', 'Test Files 1 failed (1)', 'Tests 2 failed (2)', 'AssertionError: expected [] to deeply equal [ { type: error } ]' — that was the mock target, not the fix (plugin-form does not depend on sonner, so vi.mock('sonner') from that package intercepts nothing); retargeting the mock at @object-ui/components/ui/sonner, the single module both raisers reach, turned it green. Second, the ablation below is the real pre-fix red. GREEN AFTER FIX: 'pnpm exec vitest run packages/plugin-form/' -> 'Test Files 82 passed (82)', 'Tests 823 passed | 8 skipped (831)', VERDICT command-exit 0. 'pnpm exec vitest run packages/components/src/renderers/form/' -> 'Test Files 57 passed (57)', 'Tests 386 passed (386)', VERDICT command-exit 0. 'pnpm exec vitest run apps/console/src/' -> 'Test Files 86 passed (86)', 'Tests 950 passed (950)', VERDICT command-exit 0. TYPECHECK: the first attempt failed with TS2307 'Cannot find module @object-ui/core' etc., i.e. a missing dependency closure, not a real red; after building the closures (12 then 35 projects, both VERDICT command-exit 0), 'pnpm --workspace-concurrency=2 --filter @object-ui/components --filter @object-ui/console --filter @object-ui/plugin-form run type-check' -> VERDICT command-exit 0 with all three script names echoed (Scope: 3 of 47), so this was not a zero-match silent pass. NOT-MEASURED check cleared: tsc --listFilesOnly on packages/plugin-form/tsconfig.test.json and on apps/console/tsconfig.json each resolve the new test file (1 hit apiece), so the green typecheck really covers them. GATE UNION re-run AFTER the final commit, at HEAD dbed63dd3, each exit code captured after redirection and each verdict quoted from the gate itself: check-changeset-presence '4 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major 'No changeset declares a major bump.'; check-changeset-fixed 'All workspace packages are in the changeset fixed group.'; check-changeset-overwrite 'No pre-existing changeset was modified or deleted.'; check:control-bytes 'check-control-bytes: OK (scanned 6027 tracked text file(s); skipped 85 binary).'; check:vi-mock-specifiers 'check-vi-mock-specifiers: OK (4143 tracked source file(s) ...)'; check:vi-mock-inherit 'check-vi-mock-inherit: OK (4143 tracked source file(s) ...)'; check:phantom-deps 'Every in-scope import is declared by the package that publishes it.' — all EXIT=0. ABLATION, run AFTER the commit, two legs, each with an EXIT/INT/TERM restore trap on absolute paths. No rebuild is owed and no dist is involved: the root vitest.config.mts aliases @object-ui/components to packages/components/src (line 415), so the pins resolve the edited source directly — which the red legs themselves confirm, since mutating src alone turned them red. Leg A (drop the id and the dismissal from the renderer): mutation proved on disk by grep on the text removed, not by the editor's exit code — 'id: outcomeToastId' 0 occurrences, 'toast.dismiss(outcomeToastId)' 0, injected 'toast.error(errorMessage);' 1 — then 'Test Files 1 failed (1)', 'Tests 2 failed (2)', 'AssertionError: expected [ { type: error }, ...(1) ] to deeply equal [ { type: success } ]', i.e. literally the card's two-outcomes-at-once screen. Restored with git checkout HEAD -- ABSOLUTE_PATH; git diff HEAD empty and git hash-object back to the HEAD blob 5d01245f79d3418ff41c07d30e9f4eb15476f85b. Leg B (drop the shared id from FormPage): mutation proved the same way ('id: outcomeToastId' 0, injected 'toast.error(msg);' 1), then 'Tests 1 failed | 1 passed (2)' with the same AssertionError — the predicted narrow change detector, since the surviving test is the redirect-refusal guard that never depended on the id. Restored; blob back to 2de9ac81934deafcf6db36de4bc3ad32057c18a0 and git status clean.",
"mcp_calls": "9 — issue_read/get, issue_read/get_comments, add_issue_comment (claim), create_pull_request, list_pull_requests, pull_request_read/get, update_pull_request, add_issue_comment (this report), issue_read/get_comments (read-back). No search_issues was needed: the one out-of-scope finding is handed to the PM rather than filed, so no dedupe search was owed.",
"open_questions": [],
"out_of_scope_findings": [
"NOT FILED — handed to the PM, per the dispatch order's rule that concrete defects beyond a dedupe-checked finding go to you rather than being filed by me. packages/plugin-form/src/MasterDetailForm.tsx carries the SAME defect class this card is about and is NOT fixed by this PR: handleSaved raises toast.success (line 831) and handleError raises toast.error (line 850) as two separate callbacks on one save path, with no shared id and no dismissal, so a refused save followed by a successful one leaves both on screen. It is a different renderer with its own submit path, so it was out of the claimed file surface and out of the bounded in-place exemption. Severity note for triage: it is masked in the console, because that host supplies onSuccess and the built-in success toast is then deliberately skipped; it bites hosts that do not supply onSuccess (SDUI / embedded). Fix shape is identical to this PR's — one stable per-form id across the two callbacks."
],
"deviations": [
"BOUNDED IN-PLACE FIX, declared: apps/console/src/components/FormPage.tsx was fixed alongside the renderer. All four conditions hold — same defect class as the card; mechanical, with the correct shape pinned by the renderer fix landing in the same PR; no other claim on that file (the dispatch order cleared serial constraints); same gate family, no new verification surface. It is also inside the file surface the PM's claim comment declared. Named with evidence in the PR body, as the exemption requires.",
"MOCK TARGET, measured not assumed: the wizard pin mocks @object-ui/components/ui/sonner rather than bare 'sonner'. plugin-form does not depend on sonner, so vi.mock('sonner') from that package registers against an id nothing resolves to and intercepts nothing — measured as a red run with an empty registry. The wrapper module is the single module both raisers reach, so one mock covers the renderer and WizardForm; a green run is itself the proof the mock is in the graph.",
"NARROWED LINT, declared with all three evidence items: repo-wide eslint . is CI's run; locally it was narrowed to the four changed ts/tsx files -> 0 errors, 98 warnings, every warning a pre-existing pattern in those files and no package here sets --max-warnings. (1) Population read from eslint.config.js itself: **/*.{ts,tsx} minus **/dist, **/.next, **/node_modules, **/public, **/.source. (2) File count 4, read from --format json. (3) eslint.config.js configures NO type-aware linting — no parserOptions.project, no projectService — so every rule is single-file and this diff cannot move any untouched file's verdict.",
"The remaining repo-wide gate farm was left to CI per the contract; the derived family above is the half that was owed locally.",
"The changeset names @object-ui/components and @object-ui/console as patch. plugin-form gained only a test file (no src change) and all three sit in the changeset fixed group, so they bump together; check-changeset-presence is green and is the authority here.",
"CONFLICT/REBASE CHECK (added to the PR body after origin/main advanced to 2956d7af8, 11 commits past the fork point ebc05b4d6): neither edited file moved on main. The only files that moved under packages/components/src, packages/plugin-form/src and apps/console/src are MetadataHmrReloader.tsx, MetadataHmrReloader.test.tsx, page-header-title-width.test.tsx and renderers/layout/containers.tsx; git merge-tree against current origin/main shows zero conflict markers and the PR reports mergeable_state clean. Not rebased or merged — the merge queue rebuilds.",
"CI convergence was not waited on, per the maintainer's 2026-08-10 ruling that this belongs to the PM. The draft PR is open at dbed63dd3 and gate status at report time is in_progress."
]
}
PM review: ACCEPT — PR #7342 (head dbed63dd3, fork ebc05b4d6). Flipping to ready + auto-merge now; the dev's worktree is gone, so this is a content read of the fetched branch, the report, and CI — not a local re-run.
Verified:
Diff is exactly the reported 5 files (+483 / −6). packages/components/src/renderers/form/form.tsx: one React.useId()-scoped outcomeToastId, the three outcome toast.error(...) calls carry { id: outcomeToastId }, and toast.dismiss(outcomeToastId) sits beside the existing setSubmitError(null) at the top of handleSubmit. apps/console/src/components/FormPage.tsx: the same id shared between toast.success('Submitted') and the catch's toast.error(msg); the objectui#4190 redirect refusal keeps its own toast, as the comment in the diff says. No duration change anywhere — the dev measured every form toast path at 4000 ms (or sonner's default), so supersession is the whole fix.
Two ZONE 2 assumptions falsified, both accepted: the two toasts are raised by two modules (renderer refusal vs. WizardForm success), which is why the fix lands in the renderer; and the refusal toast is not sticky by configuration. The bounded in-place fix on FormPage.tsx is inside the claimed file surface.
Pins: WizardForm.outcomeToastSupersede.test.tsx mocks @object-ui/components/ui/sonner (the one module both raisers reach — the bare sonner mock was measured inert from plugin-form); FormPage.outcomeToast.test.tsx. Ablation per file (drop the id + dismissal → expected [ { type: error }, … ] to deeply equal [ { type: success } ], the card's screen) with restores proven by blob hash.
Verification: plugin-form 82 files / 823 passed, components form renderers 57 / 386, console 86 / 950; type-check green across the three packages with the script names echoed; changeset @object-ui/components + @object-ui/console patch, presence / no-major / fixed-group / overwrite gates green; narrowed lint 0 errors.
Governed check: check-governed-merges.mjs --test over the 5 paths → NOT governed, exit 0.
CI: 30 check runs on dbed63dd3, every non-skipped one completed: success (Type Check 09:07:00Z, shards 1–4 by 09:13:04Z, Lint, Bundle Analysis, Doc Snippet Type Check, README Export Check). origin/main moved 11 commits since the fork; the dev's read-only merge-tree showed no conflicts and none of the moved files is in this diff.
Out-of-scope finding handed over by the dev — filed as a separate card in this round (see the next comment for the number): packages/plugin-form/src/MasterDetailForm.tsx raises toast.success (handleSaved, :831) and toast.error (handleError, :850) on one save path with no shared id and no dismissal — the same defect class, masked in the console because that host supplies onSuccess, live for SDUI / embedded hosts.
The handed-over MasterDetailForm finding is filed as #7345 (bug, pm:queue, p3, domain:ui; fix shape = PR #7342's). PR #7342 is ready + auto-merge (SQUASH) as of this comment; landing note follows the merge event with a content probe on origin/main.
Landed. PR #7342 merged through the queue at 12:10Z as 4704aa4bb on main (queue build ref pr-7342-35d768896…, chained after #7341, confirmed positively before the merge).
Verified by content on origin/main after git fetch, not by PR head sha:
packages/components/src/renderers/form/form.tsx:1155 declares outcomeToastId; the three outcome toast.error calls (:1938, :2029, :2099) carry it and :1977 dismisses it at the top of each attempt.
apps/console/src/components/FormPage.tsx:1597 declares the same id; :1701 (toast.success('Submitted')) and :1786 (toast.error(msg)) share it.
Activity
os-litant commentedon Sep 2, 2026
Claim: PM loop round 6 (domain:ui seat)
Session:
session_01NRRumy89BYdW9ogbcdHThoBranch:
claude/issue-7252-form-error-toast-supersedeWorktree:
objectui-issue-7252Domain:
domain:uiFile surface:
apps/console/src/components/FormPage.tsx(the submit path'stoast.errorat lines 1743 and 1769 and its success toast) and, only if the wizard raises its own toasts,packages/plugin-form/src/WizardForm.tsx; tests beside the touched code;.changeset/for any released package touched (apps/consolepublishes nothing;plugin-formdoes). ⛔ Not the state-machine refusal itself, its field location or localisation (objectstack#14311,domain:engine); ⛔ not deriving allowed initial values fromstateMachine(a console feature, not filed).Container & model: S, model: opus, mode:subagent
Clause-②: no
Serial constraints cleared: none — no open objectui PR in this seat's lane touches
FormPage.tsxorWizardForm.tsx; the dogfood session's PR #7288 touchespackages/components/src/renderers/layout/containers.tsxonly.Premise re-verified on
origin/mainebc05b4d6before dispatch:FormPage.tsx:1743and:1769still raisetoast.error(...)on a refused submit; the onlytoast.dismiss()call in the tree isDraftChangesPanel.tsx:442, so nothing on the form path dismisses or supersedes an earlier error toast when a later submit succeeds. How the error toast becomes sticky (adurationoption, the Toaster's defaults, or anInfinityon the error path) is the dev's first measurement, not assumed here.Generated by Claude Code
Generated by Claude Code
os-litant commentedon Sep 2, 2026
Dev claim (os-dev, dispatched by the
domain:uiPM seat)Session:
session_01NRRumy89BYdW9ogbcdHThoBranch:
claude/issue-7252-form-error-toast-supersede(empty branch pushed as the write-path probe — push returned[new branch], write path is green)Worktree:
/home/user/objectui-issue-7252Base sha:
ebc05b4d6(origin/mainat dispatch time)File surface I expect to touch (to be confirmed once I have read the submit path):
apps/console/src/components/FormPage.tsx, tests beside it, andpackages/plugin-form/src/WizardForm.tsxplus apatchchangeset only if that released package turns out to raise the toasts.Out of scope per the card: the state-machine refusal itself and its localisation (objectstack-ai/objectstack#14311), and deriving allowed initial values from
stateMachine.Generated by Claude Code
Generated by Claude Code
os-litant commentedon Sep 2, 2026
os-dev-report
{ "issue": 7252, "status": "done", "branch": "claude/issue-7252-form-error-toast-supersede", "pr": "https://github.com/objectstack-ai/objectui/pull/7342", "premise_still_valid": true, "summary": "The card's defect and its Expected contract both hold and are implemented: a refused submit's toast is now superseded by the outcome of the next submit, so a form never ends on a screen asserting both outcomes. Two of the dispatch order's ZONE 2 assumptions were falsified on origin/main ebc05b4d6 and the fix moved accordingly. (1) The two toasts are NOT both on FormPage's submit path. FormPage renders a flat form and toasts the literal 'Submitted'; the reported wizard runs ObjectForm to WizardForm, whose handleStepSubmit rethrows a rejected write into the catch of the SDUI form renderer in packages/components/src/renderers/form/form.tsx, which raises the refusal (banner plus toast), while WizardForm raises the success. Two modules, which is why neither could supersede the other and why the fix lands in the renderer. (2) The refusal toast is NOT sticky by configuration: ConsoleToaster sets duration 4000 for every severity, the components Toaster sets none (sonner 2.0.8 default TOAST_LIFETIME is also 4000), and duration Infinity appears on no form path in the repo. Durations were therefore left untouched and only supersession was fixed, which reproduces inside any lifetime. Every outcome toast a form raises now travels under one stable per-form id (React.useId): the renderer publishes its three outcome toasts under it and retires the previous attempt's toast in the same place it already cleared the previous attempt's in-form banner; the console's FormPage shares one id between its confirmation and its submit failure. The objectui#4190 redirect-destination refusal is deliberately excluded and keeps its own toast, since the write succeeded there and that refusal must stay readable beside the confirmation it qualifies.", "files_changed": [ "packages/components/src/renderers/form/form.tsx", "apps/console/src/components/FormPage.tsx", "packages/plugin-form/src/WizardForm.outcomeToastSupersede.test.tsx (new)", "apps/console/src/components/FormPage.outcomeToast.test.tsx (new)", ".changeset/7252-form-outcome-toast-supersede.md (new, patch for components and console)" ], "tests": "All runs through os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-7252; verdicts quoted from the lock's own VERDICT line, never a bare exit code. PRE-FIX RED, measured twice. First, an early run of the new wizard pin came back red with an EMPTY toast registry while the in-form banner rendered the refusal: 'os-verify-lock: VERDICT command-exit 1', 'Test Files 1 failed (1)', 'Tests 2 failed (2)', 'AssertionError: expected [] to deeply equal [ { type: error } ]' — that was the mock target, not the fix (plugin-form does not depend on sonner, so vi.mock('sonner') from that package intercepts nothing); retargeting the mock at @object-ui/components/ui/sonner, the single module both raisers reach, turned it green. Second, the ablation below is the real pre-fix red. GREEN AFTER FIX: 'pnpm exec vitest run packages/plugin-form/' -> 'Test Files 82 passed (82)', 'Tests 823 passed | 8 skipped (831)', VERDICT command-exit 0. 'pnpm exec vitest run packages/components/src/renderers/form/' -> 'Test Files 57 passed (57)', 'Tests 386 passed (386)', VERDICT command-exit 0. 'pnpm exec vitest run apps/console/src/' -> 'Test Files 86 passed (86)', 'Tests 950 passed (950)', VERDICT command-exit 0. TYPECHECK: the first attempt failed with TS2307 'Cannot find module @object-ui/core' etc., i.e. a missing dependency closure, not a real red; after building the closures (12 then 35 projects, both VERDICT command-exit 0), 'pnpm --workspace-concurrency=2 --filter @object-ui/components --filter @object-ui/console --filter @object-ui/plugin-form run type-check' -> VERDICT command-exit 0 with all three script names echoed (Scope: 3 of 47), so this was not a zero-match silent pass. NOT-MEASURED check cleared: tsc --listFilesOnly on packages/plugin-form/tsconfig.test.json and on apps/console/tsconfig.json each resolve the new test file (1 hit apiece), so the green typecheck really covers them. GATE UNION re-run AFTER the final commit, at HEAD dbed63dd3, each exit code captured after redirection and each verdict quoted from the gate itself: check-changeset-presence '4 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major 'No changeset declares a major bump.'; check-changeset-fixed 'All workspace packages are in the changeset fixed group.'; check-changeset-overwrite 'No pre-existing changeset was modified or deleted.'; check:control-bytes 'check-control-bytes: OK (scanned 6027 tracked text file(s); skipped 85 binary).'; check:vi-mock-specifiers 'check-vi-mock-specifiers: OK (4143 tracked source file(s) ...)'; check:vi-mock-inherit 'check-vi-mock-inherit: OK (4143 tracked source file(s) ...)'; check:phantom-deps 'Every in-scope import is declared by the package that publishes it.' — all EXIT=0. ABLATION, run AFTER the commit, two legs, each with an EXIT/INT/TERM restore trap on absolute paths. No rebuild is owed and no dist is involved: the root vitest.config.mts aliases @object-ui/components to packages/components/src (line 415), so the pins resolve the edited source directly — which the red legs themselves confirm, since mutating src alone turned them red. Leg A (drop the id and the dismissal from the renderer): mutation proved on disk by grep on the text removed, not by the editor's exit code — 'id: outcomeToastId' 0 occurrences, 'toast.dismiss(outcomeToastId)' 0, injected 'toast.error(errorMessage);' 1 — then 'Test Files 1 failed (1)', 'Tests 2 failed (2)', 'AssertionError: expected [ { type: error }, ...(1) ] to deeply equal [ { type: success } ]', i.e. literally the card's two-outcomes-at-once screen. Restored with git checkout HEAD -- ABSOLUTE_PATH; git diff HEAD empty and git hash-object back to the HEAD blob 5d01245f79d3418ff41c07d30e9f4eb15476f85b. Leg B (drop the shared id from FormPage): mutation proved the same way ('id: outcomeToastId' 0, injected 'toast.error(msg);' 1), then 'Tests 1 failed | 1 passed (2)' with the same AssertionError — the predicted narrow change detector, since the surviving test is the redirect-refusal guard that never depended on the id. Restored; blob back to 2de9ac81934deafcf6db36de4bc3ad32057c18a0 and git status clean.", "mcp_calls": "9 — issue_read/get, issue_read/get_comments, add_issue_comment (claim), create_pull_request, list_pull_requests, pull_request_read/get, update_pull_request, add_issue_comment (this report), issue_read/get_comments (read-back). No search_issues was needed: the one out-of-scope finding is handed to the PM rather than filed, so no dedupe search was owed.", "open_questions": [], "out_of_scope_findings": [ "NOT FILED — handed to the PM, per the dispatch order's rule that concrete defects beyond a dedupe-checked finding go to you rather than being filed by me. packages/plugin-form/src/MasterDetailForm.tsx carries the SAME defect class this card is about and is NOT fixed by this PR: handleSaved raises toast.success (line 831) and handleError raises toast.error (line 850) as two separate callbacks on one save path, with no shared id and no dismissal, so a refused save followed by a successful one leaves both on screen. It is a different renderer with its own submit path, so it was out of the claimed file surface and out of the bounded in-place exemption. Severity note for triage: it is masked in the console, because that host supplies onSuccess and the built-in success toast is then deliberately skipped; it bites hosts that do not supply onSuccess (SDUI / embedded). Fix shape is identical to this PR's — one stable per-form id across the two callbacks." ], "deviations": [ "BOUNDED IN-PLACE FIX, declared: apps/console/src/components/FormPage.tsx was fixed alongside the renderer. All four conditions hold — same defect class as the card; mechanical, with the correct shape pinned by the renderer fix landing in the same PR; no other claim on that file (the dispatch order cleared serial constraints); same gate family, no new verification surface. It is also inside the file surface the PM's claim comment declared. Named with evidence in the PR body, as the exemption requires.", "MOCK TARGET, measured not assumed: the wizard pin mocks @object-ui/components/ui/sonner rather than bare 'sonner'. plugin-form does not depend on sonner, so vi.mock('sonner') from that package registers against an id nothing resolves to and intercepts nothing — measured as a red run with an empty registry. The wrapper module is the single module both raisers reach, so one mock covers the renderer and WizardForm; a green run is itself the proof the mock is in the graph.", "NARROWED LINT, declared with all three evidence items: repo-wide eslint . is CI's run; locally it was narrowed to the four changed ts/tsx files -> 0 errors, 98 warnings, every warning a pre-existing pattern in those files and no package here sets --max-warnings. (1) Population read from eslint.config.js itself: **/*.{ts,tsx} minus **/dist, **/.next, **/node_modules, **/public, **/.source. (2) File count 4, read from --format json. (3) eslint.config.js configures NO type-aware linting — no parserOptions.project, no projectService — so every rule is single-file and this diff cannot move any untouched file's verdict.", "The remaining repo-wide gate farm was left to CI per the contract; the derived family above is the half that was owed locally.", "The changeset names @object-ui/components and @object-ui/console as patch. plugin-form gained only a test file (no src change) and all three sit in the changeset fixed group, so they bump together; check-changeset-presence is green and is the authority here.", "CONFLICT/REBASE CHECK (added to the PR body after origin/main advanced to 2956d7af8, 11 commits past the fork point ebc05b4d6): neither edited file moved on main. The only files that moved under packages/components/src, packages/plugin-form/src and apps/console/src are MetadataHmrReloader.tsx, MetadataHmrReloader.test.tsx, page-header-title-width.test.tsx and renderers/layout/containers.tsx; git merge-tree against current origin/main shows zero conflict markers and the PR reports mergeable_state clean. Not rebased or merged — the merge queue rebuilds.", "CI convergence was not waited on, per the maintainer's 2026-08-10 ruling that this belongs to the PM. The draft PR is open at dbed63dd3 and gate status at report time is in_progress." ] }Generated by Claude Code
Generated by Claude Code
os-litant commentedon Sep 2, 2026
PM review: ACCEPT — PR #7342 (head
dbed63dd3, forkebc05b4d6). Flipping to ready + auto-merge now; the dev's worktree is gone, so this is a content read of the fetched branch, the report, and CI — not a local re-run.Verified:
packages/components/src/renderers/form/form.tsx: oneReact.useId()-scopedoutcomeToastId, the three outcometoast.error(...)calls carry{ id: outcomeToastId }, andtoast.dismiss(outcomeToastId)sits beside the existingsetSubmitError(null)at the top ofhandleSubmit.apps/console/src/components/FormPage.tsx: the same id shared betweentoast.success('Submitted')and the catch'stoast.error(msg); the objectui#4190 redirect refusal keeps its own toast, as the comment in the diff says. No duration change anywhere — the dev measured every form toast path at 4000 ms (or sonner's default), so supersession is the whole fix.WizardFormsuccess), which is why the fix lands in the renderer; and the refusal toast is not sticky by configuration. The bounded in-place fix onFormPage.tsxis inside the claimed file surface.WizardForm.outcomeToastSupersede.test.tsxmocks@object-ui/components/ui/sonner(the one module both raisers reach — the baresonnermock was measured inert fromplugin-form);FormPage.outcomeToast.test.tsx. Ablation per file (drop the id + dismissal →expected [ { type: error }, … ] to deeply equal [ { type: success } ], the card's screen) with restores proven by blob hash.@object-ui/components+@object-ui/consolepatch, presence / no-major / fixed-group / overwrite gates green; narrowed lint 0 errors.check-governed-merges.mjs --testover the 5 paths → NOT governed, exit 0.dbed63dd3, every non-skipped onecompleted: success(Type Check 09:07:00Z, shards 1–4 by 09:13:04Z, Lint, Bundle Analysis, Doc Snippet Type Check, README Export Check).origin/mainmoved 11 commits since the fork; the dev's read-only merge-tree showed no conflicts and none of the moved files is in this diff.Out-of-scope finding handed over by the dev — filed as a separate card in this round (see the next comment for the number):
packages/plugin-form/src/MasterDetailForm.tsxraisestoast.success(handleSaved, :831) andtoast.error(handleError, :850) on one save path with no shared id and no dismissal — the same defect class, masked in the console because that host suppliesonSuccess, live for SDUI / embedded hosts.Generated by Claude Code
os-litant commentedon Sep 2, 2026
The handed-over
MasterDetailFormfinding is filed as #7345 (bug,pm:queue, p3,domain:ui; fix shape = PR #7342's). PR #7342 is ready + auto-merge (SQUASH) as of this comment; landing note follows the merge event with a content probe onorigin/main.Generated by Claude Code
os-litant commentedon Sep 2, 2026
Landed. PR #7342 merged through the queue at 12:10Z as
4704aa4bbonmain(queue build refpr-7342-35d768896…, chained after #7341, confirmed positively before the merge).Verified by content on
origin/mainaftergit fetch, not by PR head sha:packages/components/src/renderers/form/form.tsx:1155declaresoutcomeToastId; the three outcometoast.errorcalls (:1938,:2029,:2099) carry it and:1977dismisses it at the top of each attempt.apps/console/src/components/FormPage.tsx:1597declares the same id;:1701(toast.success('Submitted')) and:1786(toast.error(msg)) share it.packages/plugin-form/src/MasterDetailForm.tsx:831/:850still raise their two toasts without an id (0 hits foroutcomeToastId) — that is plugin-form:MasterDetailFormraises its save-success and save-error toasts under separate auto ids, so a refused save followed by a successful one shows both outcomes at once — the #7252 defect class, outside PR #7342's surface #7345, deliberately outside this PR.main:WizardForm.outcomeToastSupersede.test.tsx,FormPage.outcomeToast.test.tsx.Card closed by the PR's
Fixesline;pm:dispatchedstripped in the same round (labels nowbug,priority:p3,domain:ui).Generated by Claude Code