fix(pages): route error states through ErrorNotice (batch pages-rest-1) - #8816
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of The Design-Verdict: PASS A documented audit batch executed per-row against a blocking rule, with every deviation, overlap, and grandfathered surface disclosed and justified in the diff. [DESIGN-REVIEWED] 6f02564 |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of All evidence reconciled. The blind reader correctly identified every primary control the PR adds (Retry, Ask the agent, dismiss, Back to Apps, the last-error chevron) and would dare to use them; the confusions it did report, plus the screenshot coverage, drive the verdict below. UX-Verdict: CONCERNS Every migrated notice names what failed and offers a way out, and the blind reader used them all correctly — but 8 of 13 migrated pages were never screenshotted. Watch
Evidence gaps
Suggestions
[UX-REVIEWED] 6f02564 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All hunks are read, the mandating rule is verified in First-Principles-Verdict: CONCERNS Every substantive item is mandated by the blocking What this change shipsIntent: bring 13 top-level pages into compliance with the blocking AUTOSDE rule
WatchThe description was truncated at 8000 bytes by the workflow, so "undeclared" could not be fully established for item 10; it is nonetheless unrelated to error surfacing. SubtractionsDrop [FIRST-PRINCIPLES-REVIEWED] 6f02564 |
GPT 5.6 Review — ✅ no blocking findings (all downgraded on adjudication)GPT 5.6 flagged blocking issues on This comment is updated in place on each push. Review detailsBLOCKING -- website/src/pages/ChannelPage.tsx:695 -- Failed reconciliation leaves rejected optimistic state visible Adjudication (Opus 4.8) — is blocking on each finding proportionate?The error IS already surfaced through LOW — requires both the mutation reject (line 704/713 catch) AND the reconciliation re-read reject (line 693-695 swallowed catch); consequence is a visible, self-correcting UI inconsistency: the error notice is shown via [ADJUDICATION] 6f02564 total=1 uphold=0 downgrade=1 False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsNo findings that block; one advisory. FINDING — website/src/pages/AppDetailPage.tsx:872 — for an installed app, [OPUS-REVIEWED] 6f02564 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
|
Round-1 dispositions (head GPT 5.6 — both blocking findings fixed in code.
UX Review — Watch fixed; both suggestions taken.
First Principles — both subtractions taken.
Focus Cue Gate: the ChatPage title-rename Design Review — PASS, nothing to answer. |
bf762b8 to
6c64881
Compare
|
Round-2 dispositions (head Frontend Tests (first real run, 28 red across 4 shards) — 24 are this PR's, fixed; 4 are inherited from main.
|
6c64881 to
ab8a1c9
Compare
|
Round-3 dispositions (head GPT 5.6 — blocking finding fixed at all five rows (max-two-buttons-per-row):
UX Review — suggestion taken: Design Review — both suggestions acknowledged, not taken here: (1) DevFleet's single latest-error slot is a documented trade-off — burst failures still reach the notification center, and a stacked list is a follow-up for the components batch; (2) folding the fallback into First Principles — rider declared: the two Frontend Tests: the 4 remaining reds on |
|
Round-9 disposition (head GPT 5.6 — the one fenced finding fixed. Everything else on |
cc890a7 to
eb5ebe2
Compare
|
Round-10 dispositions (head
|
eb5ebe2 to
ee3ee8f
Compare
|
Round-11 disposition (head GPT 5.6 — the one fenced finding fixed by taking its first suggested remedy. A refused dismiss / listen change no longer rolls the row back by hand; it re-reads the channel ( Everything else on |
ee3ee8f to
ccaf01c
Compare
|
Round-12 dispositions ( First Principles
UX
|
UX Review: - ChannelPage: a refused close keeps the channel in the list; a refused dismiss / listen change puts the agent row back, restoring only the state THAT request set so a newer, successful change is never overwritten by a stale rollback; a failed presets read no longer papers over a failed channel-list read; the "No channels yet" / "Create a channel" onboarding copy is withheld while the list read failed. - AgentsPage: a transport failure that takes every read down is named once (the roster notice with Retry); the cards whose failure reads identically stay quiet but still withhold their "no active sessions" / "no subagents" empty states. A card failing for a different reason still names it. - DevFleetPage: a rejected fleet read is the same unknown as a discovery error (counts show "—", not "Worktrees (0)" / "0 rows"); the DISK card shows "—" when /disk fails. - HooksPage: the expanded last_error row is a sticky-left block exactly as wide as the scroller, so it reads in full at any horizontal scroll instead of painting under the sticky Actions column. GPT (page-layout-pattern): every control this batch added to a page file renders through the Btn primitive, not a raw <button>. Capture harness: ChannelPage and AgentsPage scenes; HooksPage mounts a hook whose last run failed and the runner opens its last_error row and runs a failing Test; scene width fits the hooks table so the Status column and its expander are in frame. Tests: ChannelPageCoverage pins the reverted optimistic state, the kept channel on a refused close, and the notice (not the empty state) on a failed list read; HooksPage.stickyActions pins the measured scroller ref. GPT (round 9): AppDetailPage.load() drops the previous record before resolving the new name, so an in-session navigation to a genuinely missing app renders the not-found page instead of the stale app under the new URL. UX (round 10): a failed hook Test is reported once, by the titled notice beside the row (testMut leaves the page-top mutError chain); the AppPage and AgentsPage load-failure notices carry a what-failed title instead of the bare transport message; the HooksPage capture scene is tall enough to show the provider-hooks notice and its Retry. GPT (round 11): a refused dismiss / listen change reconciles the row from channelGet instead of a hand-rolled rollback, which could not tell the optimistic value from one a socket event or a newer request wrote. First Principles (round 12): the pin FAILURE notice no longer shares the 8-second timer with the pin STATUS line; only the answer times out.
|
Final head
Ready for human review. |
Problem / Motivation
The dashboard error-state audit (
research/error-states-audit.md) found 320 error surfaces bypassing the sharedErrorNoticeand 197 mutations/queries that fail silently — every one a violation of the blocking AUTOSDE ruleerrors-use-error-notice. The report recommends fixing by surface in batches of ≤25 files. This PR is §5.4, batch pages-rest-1: the 13 top-level pagesAgentsPage,AppDetailPage,AppPage,ArtifactDeployPage,ArtifactDetailPage,ArtifactPopoutFrame,ArtifactsPage,ChannelPage,ChatPage,ChatSidebar,ContextBreakdownPanel,DevFleetPage,HooksPage(48 violation rows + 45 silent-failure rows).No linked issue: driven by the audit report.
Why it matters
This is an AI agent app: an error the user cannot fix is usually one the agent can, so a hand-written red div is a dead end — it throws away the structured context (route, endpoint, HTTP status, backend
code) thatErrorNoticerecovers from the error journal and offers to the agent. Several of these sites were worse than a dead end: a rejectedgetApprendered as "App Not Found", a rejected file read opened a file tab whose content was the error string (so it looked saveable),alert()dialogs carried nothing to the agent, and ~30 Dev Fleet failures lived only in a 7-second toast.What changed (motivation → approach → change)
Every site follows the report's per-row
recommended_action.askAgentis on wherever the surface holds no unsaved draft (load/list failures, status panels, actions whose inputs are already persisted). It is off with a{/* No hand-off: <draft> */}comment next to an unsaved value: the ArtifactDeployPage new-profile form (npName/npAccount/npRole, conditional onshowNewProfile), the ArtifactDetailPage editor buffer (editedContent) and comment draft, the ChannelPage unsent message text, the ChatSidebar bulk-model selection, the open HookForm (!creating && !editing) and the ArtifactsPage folder-name inputs.ChatPage follows the reasoning sibling PR #8730 established and I verified in code: the composer draft is persisted per slot on every keystroke and slot switch, and an in-chat hand-off (
subscribeChatHandoff) opens a fresh slot without navigating away, so the hand-off is on for the pane-level notices, with that reasoning as a comment on the notice stack.Hand-written surfaces migrated →
ErrorNoticeAgentsPagerole="alert"div for a refused delete;text-warndiv on the Skills tab onlyAppDetailPagebg-danger/10div with a directly mountedAskAgentButton; bespoke Fix with AI button on the install-failed cardErrorNotice askAgent onDismiss; install-failed headline is an inline notice whose hand-off resolves a journal entry (recordError) carrying the redacted log tail the old button pasted by handArtifactDeployPageCardmixing success text with backend{error}bodies;color: var(--danger)span; literal#dc2626divErrorNotice(No hand-off while the new-profile form is open); verify-unreachable and pending-confirmation failures → noticesArtifactDetailPageborder-danger/40div forpublication.last_error; twotext-dangerspans inUpstreamSyncBannerversionQuery.error) is now a load failure with Back to Live, not "Not found."ArtifactsPagebg-danger/10banner with an "Error" headingErrorNotice onDismiss; the pure client-side pick hints (unsupported type / too large / empty) split into arole="status"hint, not the error surfaceChannelPageModal(AlertTriangletext-warn, OK button); two nativealert()sErrorNotice askAgent onDismiss(testId="channel-error"); inputs were already submitted, so a blocking modal bought nothingChatPagealert()for fork / plan-from-here / apply-plan; hand-written upload, sid and pin banners;unresumableResumenotice with no askAgent decision; toast-onlycreateSlotandpinModelToAgentfailures;'Unable to read file'/'Error reading file'written as the file tab's contentactionErrornotice (testId="action-error") plusupload-error/sid-error/pin-errornotices above the composer; the 20-file / 50 MB client hints stayrole="status";pinStatuskeeps only answers (pin limit, not found); read failures report through the notice and leave the tab cold (retry on next click) instead of masquerading as file text;askAgent+ reasoning onunresumableResumeChatSidebartext-mutedpreview failure;text-dangerspans for cleanup / bulk-model errors;role="status"seed box+N ↻, failure named in its title/aria-label) is left as is, the report's second option for that row: a text notice cannot sit on the session row's chip line without a third control (max-two-buttons-per-row) or a second line (session-row-fixed-height), anderrors-use-error-noticedoes not accept a row budget as a reason to omit the hand-off — three blocking rules with no compliant intersection, so the grandfathered chip stays untouched by this diff.DevFleetPagevar(--danger)spans (rebase, sync, provision, worktree detail, prune rows);role="alert"divs (fleet discovery, gateway restart);EmptyState subtitle={error}for backend-unavailable; page-localnotify(msg, {type:'error'})ToastHost as the only report for ~30 catch sitesnotify(type:'error')now lands in an in-pageactionErrorErrorNotice(testId="devfleet-action-error") instead of the transient toast — every existing site gains a persistent, selectable surface without touching them; success/info keep the toastHooksPagebg-danger/10banner with "Error" heading;last_erroronly in anInfoTiptooltip;text-dangerdiv +role="alert"box for test failures; provider-hooks failure dressed asEmptyStatehooks-error,hook-test-error);last_erroropens as an expandable row under the hook; provider failure = notice + RetrySilent failures surfaced
SubagentInfo.errorrendered per failed run; spawn-list / sessions-context / sessions-usage (.catch(()=>null)removed) / agents-installed (+Retry) / mcp-tools / kirocrew-agents+default-agent (explains why delete is unavailable, +Retry) / agent-detail queries renderisError;patchModelMut,spawnClearMut,spawnDeleteMut,setDefaultMutgainonError..catch(() => setX(null|[]))swallows replaced by an error state;getApponly treatsstatus === 404as "not installed" (isNotFoundError, duck-typed so mocked clients keep working); registry/system failures are held and shown while an installed app still renders.jget/jsend(and the two raw fetches) now throw on!res.okcarrying the backenderror/detail, so 4xx/5xx bodies become query errors; config/profiles/sites/webapp/pending queries renderisError(+Retry);sitesResp.profile_errorsrendered; every profile/recall/destroy/verify/loadPolicy mutation gainsonError.isErrorrendered; the seven comment mutations'onMutErrnow records the message (commentActionError) instead of only invalidating.isError(+Retry);pinMut,createFolderMut,updateFolderMuterrors join the banner chain;RemoteArtifactsSectionrenders a notice + Retry instead ofreturn null.channelsList,channelPresets,channelGet,channelDismissAgent,channelUpdateAgent,channelCloseswallows surfaced;channelPostfailure now keeps the text in the composer and reports it (No hand-off: the unsent message).steerMutation(console-only),renameSlot.catch(()=>{}),generateTitle, pin/unpin.catches →actionError/pinError.isError(+Retry); column, folder and the five create-chat mutations gainonError→board-error/folder-action-error/new-chat-errornotices.isErrorrendered. DevFleetPage: disk queryisErrorrendered inline.Deliberately not done
website/AUTOSDE.yamluntouched (settings-1 owns the rule text). Rule-gap variants found here, for that PR: (askAgent-off comment form) an omission whose reason is a ROW BUDGET (max-two-buttons-per-row / session-row-fixed-height) rather than a draft — ChatSidebar source-link chip line — has no sanctioned comment shape; (4.5) nativealert()— ChannelPage ×2, ChatPage ×3; (4.2) toast/notification as the only report — ChatPage restage/createSlot/pinModel, DevFleetPage ToastHost; (4.6/4.7).catch(() => setX(null|[]))and emptycatch {}presenting failure as empty/not-found — AgentsPage, AppDetailPage, AppPage, ChannelPage ×7; (4.8)res.oknever checked — ArtifactDeployPagejget/jsend+ two raw fetches; (4.4) tooltip-onlylast_error— HooksPageInfoTip; error string used as content (file-tab text, PageHeader subtitle) — ChatPage, AppDetailPage; bespoke hand-off button / directAskAgentButtonmount outside a boundary — AppDetailPage; failure visible only as a statusBadge— AgentsPage subagents;if (error) return nullhiding a section — ArtifactsPage;role="status"holding a failure — ChatSidebar seed, ChatPage pins; success and failure sharing one non-alert card — ArtifactDeployPage.ChatPage.tsx(the/sideslash failure routed into the existingrefusedPressnotice, plusaskAgenton that notice). This PR does not touch those four spots; a small overlap/trivial rebase is expected whichever lands second.readErroronPanelTab(hooks/usePanelTabs.ts) and a notice inFileTabBody(pages/chat/SidePanel.tsx) — outside this batch; the ChatPage side (query keepsok:false, no error-as-content) is done and the failure reports through the pane notice.focus-visible:accent underline added); GPT F1 — the ChatSidebar source-link failure notice stays on the chip line (zero-basisflex-1, truncated, full text intitle) so the session row gains no stacked line; GPT F2 — the DevFleet rebase failure notice is its own row-spanning block, not a third control in the action row; UX — DevFleet's page-level action error scrolls into view when set, AppPage's failure branch has Retry + Back to Apps; First Principles — the five file-local error→text helpers are replaced byutils/thunkError.errMessage(+ per-site fallback), andTrustAppModal.isNotFoundis replaced by the sharedisNotFoundError.+Nretry and may not grow a line — a row-budget reason, not a draft; flagged below as a rule-gap for the askAgent-off comment form), ChatSidebar cleanup dialog (notice above the Cancel/Archive pair), DevFleet sync and provision failures (notice on its ownbasis-fullline above Log / dismiss). UX: ChatPage clears the pane-level action error on slot switch. Declared rider: thefocus-visible:border-b focus-visible:border-accentclasses on ChatPage's title-renameInputare required by the (blocking) Focus Cue Gate, which fires because this diff touches that line'sonBlur(therenameSlotfailure path).ArtifactDeployPage's localfailFromreplaced by the sharedtoApiErrorfactory (api/apiError.ts), so a non-2xx reply is anApiErrorcarrying the status and the backend'serror/detailtext; the interimrequest_failed_statuskey is retired from all catalogs. GPT F2 taken — DevFleet keeps the latest action failure in module state (_lastActionError), seeds the page notice from it on mount and clears both on dismiss, so a background Pull+Build/provision failure that lands while the page is unmounted is still on the page when the user returns (test seam__resetDevFleetNoticesForTests). GPT F1 (hand-off on the ChatSidebar chip-line notice) NOT taken — see the rule-gap note; the adjudicator downgraded both findings as disproportionate. Frontend Tests round: translator-context orphan for the retiredchannelPage.ok, zh-CN "turn" = 轮次, HooksPage integration test on thehooks-errornotice, the pin-jump source contract onsetPinLoadError.steerMutationaround a receipt that never rejects and renders its own error row, so this PR'sonErrorthere is dropped and thecould_not_steerkey retired. Catalog conflicts resolved by key-level replay again (32 added / 16 removed keys onto main's catalogs).RemoteCrewPanel.test.tsx› "agent hand-off from the diagnosis note" (4 cases) fails on every merge ref that includes both fix(settings): route error states through ErrorNotice (batch settings-1) #8729 and fix(settings): route error states through ErrorNotice (batch settings-2) #8749 — the test helper looks for arole="status"box that fix(settings): route error states through ErrorNotice (batch settings-2) #8749 replaced withErrorNotice. Unblocker: test(settings): point the RemoteCrewPanel diagnosis helper at the ErrorNotice #8828 (one test helper line). Frontend Tests here will stay red until it lands or this branch is rebased over it.last_errorrow spans the scrolling columns with a sticky spacer under Actions. Evidence: ChannelPage + AgentsPage scenes and the HooksPage failed-hook row / failed Test are now in the frames. ChatPage and ChatSidebar are not in the harness: both need a live slot with transcript state to render their composer-adjacent notices, which the fixture-fetch harness cannot stage; their notices are pinned by the updated suites instead.last_errorrow is a sticky-left block exactly as wide as the table's scroller, so it reads in full at any horizontal scroll instead of painting under the sticky Actions column; the harness scene is wide enough for the Status column and its expander to be in frame. Failure-as-empty tension removed: ChannelPage withholds its onboarding copy while the list read failed; AgentsPage names a page-wide transport failure once (the roster notice + Retry) and keeps the identically-failed cards quiet while still withholding their empty states; DevFleet treats a rejected fleet read as the same unknown as a discovery error (—everywhere, no "Worktrees (0)" / "0 rows"). "Ask the agent" wording is a shared-component change — flagged for the components batch.a.listenMode === mode/a.state === 'done'guards), so two overlapping changes where the first fails and the second succeeds keep the server-confirmed value; GPT F2 (page-layout-pattern) — every control this batch added to a page file is aBtn, no raw<button>remains in the diff. Two suites updated to the round-7 contracts (failed list read → notice, not empty state; measured scroller ref).AppDetailPage.load()now drops the previous record (setApp(null)) before resolving the new name, so an in-session navigation from a loaded app to a genuinely missing one renders the not-found page instead of the stale app under the new URL. (The loading branch already replaced the page for the duration of a load, so nothing else changes.)hook-test-errornotice beside the row;testMutleaves the page-topmutErrorchain, since a second bare copy read as a page-wide outage. The AppPage and AgentsPage load-failure notices carry a what-failed title (Failed to load app/Failed to load agents, 2 new keys) instead of the bare transport message. The HooksPage capture scene now shows the provider-hooks notice + Retry. "Ask the agent" pre-click disclosure is a shared-component change — components batch.channelGet(the pattern clear-context already uses) instead of a hand-rolled rollback — a rollback cannot tell the optimistic value from one achannel_agent_leftsocket event or a newer request wrote, so it could resurrect a removed agent. If the re-read fails too the row is left as is; the notice already names the failure.Inputgainedfocus-visible:border-b focus-visible:border-accentbecause the Focus Cue gate flags a focusable control whose focus ring is suppressed (focus:!shadow-none) with no replacement cue — it is the gate's fix, not a design change.test_members_dm_thread.py::TestDenialAuditOffload::test_no_members_sel_audit_is_offloadedfails on every merge ref containing both fix(sel): warm singleton at startup; drop per-site audit hops (#8608) #8741 (inverted the members AST guard) and feat: four-layer member system prompt for crew DM threads #7235 (added twoto_thread-wrapped audits). Unblocker: fix(members): enqueue the rules read/write audits inline, not via to_thread #8844.ManageAgentsFooter(components/AgentDropdownList.tsx) still renders the default-agent failure as a hand-writtenrole="alert"div and only acceptserror?: boolean— a components-batch item.App.tsx'sagentSwitchNoticesurface likewise.i18n: 34 new keys in
en.manual.json(net after review rounds: 32 of the originals retired/kept plusappPage.retry,appPage.back_to_apps,appPage.failed_to_load_app,agentsPage.failed_to_load_agents) + 11 translations (zh-CN, hi, es, fr, bn, pt, ru, de, ja, ko, it),en-XAregenerated. Wherever possible the notice receives the same string the site already had so the journal message-match keeps working. 16 retired keys (fix_with_ai,dismiss_error,app_not_found_2,error/add_failed/update_failed/remove_failed,unable_to_read_file,error_reading_file,dismiss_provision_status, …) removed from all 13 catalogs so thedeadKeysratchet stays at baseline.Tests
Per the owner's standing instruction no test suite was run locally; verification is on CI. Existing suites updated to the new shape:
ChatPageMoreCoverage: fork / plan-from-here / apply-plan now assert theaction-errorErrorNotice(rolealert, real reason,alertSpynot called).AppDetailPageCoverage: load failure asserts Failed to load app + notice + Retry (not "App Not Found"); registry/system failure asserts the app renders and the notice namesregistry offline; install-failed hand-off asserts the staged prompt (consumeChatHandoff) carries the log tail and the endpoint;Dismisslabel.AppDetailPageAutoAction/Stars/ArtLocalFallback/TrustAppModal: "not installed" rejections now carrystatus: 404.ArtifactDeployPageCoverage: failure text is the backend reason verbatim /Request failed (NNN).ChannelPageCoverage+ChannelPage.clearContext: in-pagechannel-errornotice instead of a modal /window.alert.DevFleetPage+DevFleetPageCoverage: dismiss via theprovision-error-<name>notice.HooksPage.testFailure:hook-test-error/hooks-errornotices.Local gates run clean:
tsc -b,eslinton every touched file, the fulli18n-check.mjschain withI18N_BASE_REF=origin/main(added-lines 0, source-strings 0 badly shaped, key-refs 0 dangling, pseudolocale in sync),check-phantom-classes.mjs,check-theme-colors.mjs; no non-inclusive terms in the diff.Manual verification
Capture harness
website/capture/error-notice-pages-rest-1.{html,tsx}+website/scripts/capture-error-notice-pages-rest-1.mjsmounts the realHooksPage,AppPage,ChannelPage,AgentsPageandDevFleetPagewithfetchstubbed to reject — except the hooks list, which resolves with one hook whose last run failed so the runner can open itslast_errorrow and run a failing Test (both notices are in the after frame). The runner asserts the after frames render one "Ask the agent" per scene and none of the legacy markup, and that the before frame (same harness against a pristineorigin/mainworktree) renders zero hand-offs.Screenshots / video
Before (base) → after (this PR), dark:
After, light theme
Related Issues
Batch pages-rest-1 of the error-state sweep (
research/error-states-audit.md§5.4). Sibling batches: settings-1 (owns the AUTOSDE rule text), chat-1 (#8730 — shares the ChatPage/sidetouch noted above), chat-2, pages-rest-2, components.Pattern harvest
Rule candidate: semgrep
Pattern: a fetch helper (
jget/jsend-style) that returnsr.json()without checkingr.ok, and auseQuery/raw-promise.catch(() => setX(null | []))— both convert a transport failure into an empty / not-found state (audit §4.6–4.8). Recorded here for the settings-1 rule-text PR.Checklist