Context
packages/loopover-ui-kit/src/components/state-views.tsx's StateBoundary component has a
useEffect (lines 267–280) whose own leading comment states the intent explicitly:
// When this boundary flips into the error state, surface a toast with Retry.
useEffect(() => {
if (isError && errorLabel) {
onFailureNotify?.({ label: errorLabel, kind: errorKind ?? "network", message: ..., retry: onRetry });
}
}, [isError, errorLabel, errorKind, resolvedErrorDescription, onRetry, onFailureNotify]);
"Flips into the error state" describes an edge-triggered notification — fire once, on the
false→true transition. The implementation is level-triggered: it has no memory of the previous
isError value, so it re-fires on any render where isError is true and any of the six listed
deps changed identity — including onRetry and onFailureNotify, which are ordinary function props.
Every real call site goes through the app-level wrapper at
apps/loopover-ui/src/components/site/state-views.tsx:
export function StateBoundary(props: ComponentProps<typeof UiKitStateBoundary>) {
return <UiKitStateBoundary onFailureNotify={(args) => notifyApiFailure(args)} {...props} />;
}
This wrapper is itself a plain function component re-invoked on every parent render, and it
constructs a brand-new arrow function for onFailureNotify each time (deliberately, per its own
comment, to keep notifyApiFailure lazily-referenced) — so onFailureNotify's identity is
guaranteed to change on every single render of any component using errorLabel, regardless of
whether the underlying error state actually changed. Six call sites currently pass errorLabel
(owner-panel.tsx, digest-panel.tsx, commands-panel.tsx, app.operator.tsx, app.runs.tsx,
app.analytics.tsx) — any unrelated re-render of these panels while an error is showing (a sibling
state update, a timer tick, an unrelated prop change) re-invokes notifyApiFailure.
The correct, edge-triggered pattern already exists elsewhere in this same codebase:
apps/loopover-ui/src/lib/mcp-version-badge.tsx (its wasError ref) tracks the previous error
value explicitly and only calls notifyApiFailure/notifyApiRecovered on an actual transition. That
is the precedent to mirror.
notifyApiFailure (apps/loopover-ui/src/lib/api/request.ts) does have its own dedupe-by-id and a
repeat counter, so this does not spam stacked toasts — but it does mean the toast content churns
("still failing (Nx in a row)") and notifyApiFailure executes far more often than the "flips into"
comment describes, on every affected re-render rather than on the actual state transition. No
existing test (apps/loopover-ui/src/components/site/state-views.test.tsx) asserts a call count
across re-renders — every existing assertion only checks that notifyApiFailure was called with the
right arguments at least once, so this gap has no regression coverage today.
Requirements
- In
packages/loopover-ui-kit/src/components/state-views.tsx's StateBoundary, change the
onFailureNotify effect to be genuinely edge-triggered: track whether the boundary was already in
the error state on the previous render (a useRef<boolean>, mirroring the wasError pattern in
apps/loopover-ui/src/lib/mcp-version-badge.tsx) and only call onFailureNotify on the
false→true transition, not on every render where isError is already true.
- The transition must still correctly re-fire if the boundary leaves and re-enters the error state
(e.g. error → ready → error again) — this is a "false→true edge" fix, not a "call at most once
ever" fix.
apps/loopover-ui/src/components/site/state-views.tsx's wrapper's inline
onFailureNotify={(args) => notifyApiFailure(args)} may stay exactly as-is once the ui-kit side is
edge-triggered — do not "fix" it by trying to memoize the wrapper's arrow function instead;
the fix belongs in the boundary's own edge-detection, not in papering over one unstable caller.
- Do not change
onRetry's own click-triggered call path (the Retry button dispatch) — only the
passive onFailureNotify effect is in scope.
Deliverables
Test Coverage Requirements
packages/loopover-ui-kit is not part of vitest.config.ts's coverage.include list and is not
measured by Codecov's patch gate — the acceptance signal for a PR here is a green local test run, not
a Codecov percentage. The regression test itself is still mandatory per this repo's own testing
discipline (a fix without a regression test is incomplete), it just isn't Codecov-gated. If this fix's
test happens to be added to apps/loopover-ui/src/components/site/state-views.test.tsx instead (also
apps/**, also outside coverage.include), the same applies there.
Expected Outcome
A component using errorLabel that stays in the error state across several unrelated re-renders
triggers exactly one failure notification per actual error-state entry, matching the effect's own
"flips into the error state" documented intent — not one notification per re-render.
Links & Resources
packages/loopover-ui-kit/src/components/state-views.tsx:267-280
apps/loopover-ui/src/components/site/state-views.tsx:23-25
apps/loopover-ui/src/lib/mcp-version-badge.tsx:17-33 (the edge-triggered pattern to mirror)
apps/loopover-ui/src/components/site/state-views.test.tsx (where to add the regression test)
Context
packages/loopover-ui-kit/src/components/state-views.tsx'sStateBoundarycomponent has auseEffect(lines 267–280) whose own leading comment states the intent explicitly:"Flips into the error state" describes an edge-triggered notification — fire once, on the
false→true transition. The implementation is level-triggered: it has no memory of the previous
isErrorvalue, so it re-fires on any render whereisErroris true and any of the six listeddeps changed identity — including
onRetryandonFailureNotify, which are ordinary function props.Every real call site goes through the app-level wrapper at
apps/loopover-ui/src/components/site/state-views.tsx:This wrapper is itself a plain function component re-invoked on every parent render, and it
constructs a brand-new arrow function for
onFailureNotifyeach time (deliberately, per its owncomment, to keep
notifyApiFailurelazily-referenced) — soonFailureNotify's identity isguaranteed to change on every single render of any component using
errorLabel, regardless ofwhether the underlying error state actually changed. Six call sites currently pass
errorLabel(
owner-panel.tsx,digest-panel.tsx,commands-panel.tsx,app.operator.tsx,app.runs.tsx,app.analytics.tsx) — any unrelated re-render of these panels while an error is showing (a siblingstate update, a timer tick, an unrelated prop change) re-invokes
notifyApiFailure.The correct, edge-triggered pattern already exists elsewhere in this same codebase:
apps/loopover-ui/src/lib/mcp-version-badge.tsx(itswasErrorref) tracks the previous errorvalue explicitly and only calls
notifyApiFailure/notifyApiRecoveredon an actual transition. Thatis the precedent to mirror.
notifyApiFailure(apps/loopover-ui/src/lib/api/request.ts) does have its own dedupe-by-idand arepeat counter, so this does not spam stacked toasts — but it does mean the toast content churns
("still failing (Nx in a row)") and
notifyApiFailureexecutes far more often than the "flips into"comment describes, on every affected re-render rather than on the actual state transition. No
existing test (
apps/loopover-ui/src/components/site/state-views.test.tsx) asserts a call countacross re-renders — every existing assertion only checks that
notifyApiFailurewas called with theright arguments at least once, so this gap has no regression coverage today.
Requirements
packages/loopover-ui-kit/src/components/state-views.tsx'sStateBoundary, change theonFailureNotifyeffect to be genuinely edge-triggered: track whether the boundary was already inthe error state on the previous render (a
useRef<boolean>, mirroring thewasErrorpattern inapps/loopover-ui/src/lib/mcp-version-badge.tsx) and only callonFailureNotifyon thefalse→true transition, not on every render where
isErroris alreadytrue.(e.g. error → ready → error again) — this is a "false→true edge" fix, not a "call at most once
ever" fix.
apps/loopover-ui/src/components/site/state-views.tsx's wrapper's inlineonFailureNotify={(args) => notifyApiFailure(args)}may stay exactly as-is once the ui-kit side isedge-triggered — do not "fix" it by trying to memoize the wrapper's arrow function instead;
the fix belongs in the boundary's own edge-detection, not in papering over one unstable caller.
onRetry's own click-triggered call path (the Retry button dispatch) — only thepassive
onFailureNotifyeffect is in scope.Deliverables
StateBoundaryinpackages/loopover-ui-kit/src/components/state-views.tsxonly invokesonFailureNotifyon a realfalse → truetransition ofisError(witherrorLabelpresent),never on a same-state re-render.
apps/loopover-ui/src/components/site/state-views.test.tsxthat:renders
StateBoundarywithisError errorLabel="X", re-renders it several times withisErrorstill
truebut a differentonRetry/onFailureNotifyidentity each time (simulating the realwrapper), and asserts the notify callback was invoked exactly once — then transitions
isErrortofalseand back totrueand asserts it fires again exactly once for the secondtransition.
Test Coverage Requirements
packages/loopover-ui-kitis not part ofvitest.config.ts'scoverage.includelist and is notmeasured by Codecov's
patchgate — the acceptance signal for a PR here is a green local test run, nota Codecov percentage. The regression test itself is still mandatory per this repo's own testing
discipline (a fix without a regression test is incomplete), it just isn't Codecov-gated. If this fix's
test happens to be added to
apps/loopover-ui/src/components/site/state-views.test.tsxinstead (alsoapps/**, also outsidecoverage.include), the same applies there.Expected Outcome
A component using
errorLabelthat stays in the error state across several unrelated re-renderstriggers exactly one failure notification per actual error-state entry, matching the effect's own
"flips into the error state" documented intent — not one notification per re-render.
Links & Resources
packages/loopover-ui-kit/src/components/state-views.tsx:267-280apps/loopover-ui/src/components/site/state-views.tsx:23-25apps/loopover-ui/src/lib/mcp-version-badge.tsx:17-33(the edge-triggered pattern to mirror)apps/loopover-ui/src/components/site/state-views.test.tsx(where to add the regression test)