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
apps/loopover-ui/src/components/site/state-views.tsx is the app's shared loading/empty/error primitive set. It exports five things: Spinner, LoadingState, EmptyState, StateActionButton, ErrorState, StateBoundary (a private Shell helper and NETWORK_ERROR_KINDS constant back them internally), plus a sixth, unrelated export usePreviewDataState. StateBoundary is the composite: given isLoading/isError/isEmpty, it renders LoadingState, ErrorState, or EmptyState and otherwise passes through children — every route/panel in the app that needs a consistent async-state surface reaches for it instead of hand-rolling its own branches.
#6244's audit (apps/loopover-ui/src/chat-ui-primitives-audit.md, landed via PR #6474, merged 2026-07-16) confirmed these four — LoadingState/EmptyState/ErrorState/StateBoundary — are reusable as-is for the miner-dashboard chat rail's message list, which needs its own async-state surface rather than inventing a second one. That audit also confirmed the outer shell/building-block primitives (this file, plus scroll-area.tsx, avatar.tsx, textarea.tsx/input.tsx/button.tsx) are the only reusable pieces for chat — the message list, composer, streaming renderer, and typing indicator all still need to be built new, in later issues. Per the design synthesis for the miner-dashboard redesign, this port is the one hard prerequisite that has to land before the message-list issue can start, since chat is meant to consume this exact primitive.
@loopover/ui-kit (packages/loopover-ui-kit/) is the shared package already consumed by both apps/loopover-ui and apps/loopover-miner-ui as a real npm workspace dependency — not a fork. apps/loopover-miner-ui already imports ui-kit components directly (e.g. apps/loopover-miner-ui/src/routes/portfolio.tsx:4-6 imports Button/Card/Table from @loopover/ui-kit/components/*), while apps/loopover-ui re-exports through thin local shims under @/components/ui/* — e.g. apps/loopover-ui/src/components/ui/skeleton.tsx, whose entire body is export * from "@loopover/ui-kit/components/skeleton";, backed by packages/loopover-ui-kit/src/components/skeleton.tsx. That's the precedent this move follows, though (see Requirements) state-views.tsx needs one adaptation a pure skeleton.tsx-style shim doesn't.
The one real wrinkle:state-views.tsx currently imports notifyApiFailure and type ApiFailureKind from apps/loopover-ui/src/lib/api/request.ts (request.ts:15 defines ApiFailureKind, request.ts:109 defines notifyApiFailure — an app-local API-status/toast singleton wired to that file's own beginRequest/endRequest/reportApiFailure tracking, used to surface a deduped "Retry" toast when StateBoundary's errorLabel prop is set). apps/loopover-miner-ui/src has no equivalent module at all (confirmed: no notifyApiFailure/ApiFailureKind anywhere in that app), and packages/loopover-ui-kit must never import from an app (that's backwards — apps depend on ui-kit, not the reverse; also, none of apps/loopover-ui's @/... path aliases resolve inside packages/loopover-ui-kit, which has no aliases configured in packages/loopover-ui-kit/tsconfig.json). This is the one place the port needs real (small, additive) adaptation rather than a byte-for-byte copy — spelled out exactly in Requirements below.
Also confirmed while researching this move: Spinner and StateActionButton (two of the file's other exports) have real call sites today outside state-views.tsx itself — audit-feed.tsx, dead-letter-queue-panel.tsx, refresh-meta.tsx, app-panels/rees-analyzer-field-group.tsx, routes/app.operator.tsx, and routes/app.analytics.tsx all import one or both directly. The back-compat re-export has to cover all six of the file's current exports, not just the four named in this issue's title, or those six files break.
Requirements
⚠️ Read this before starting. This issue touches exactly two files: packages/loopover-ui-kit/src/components/state-views.tsx (new) and apps/loopover-ui/src/components/site/state-views.tsx (rewritten in place into a back-compat
wrapper — never deleted). A PR that (a) copies the component into ui-kit but leaves the app's
original file untouched or deletes it outright instead of turning it into the wrapper described
below, (b) drops or renames any of the file's six existing exports (Spinner, LoadingState, EmptyState, StateActionButton, ErrorState, StateBoundary) from the app's existing @/components/site/state-views import path, or (c) has the ui-kit copy import anything from apps/loopover-ui (including @/lib/api/request, @/lib/utils, or any other @/... alias — none
of those resolve inside packages/loopover-ui-kit) does NOT satisfy this issue and will be closed.
Create packages/loopover-ui-kit/src/components/state-views.tsx containing Spinner, the
private Shell helper, StateActionButton, LoadingState, EmptyState, ErrorState, and StateBoundary, ported from apps/loopover-ui/src/components/site/state-views.tsx verbatim
except for the two adaptations in Requirements 3 and 4.
The ported file's cn import must be import { cn } from "../utils"; — the same relative
pattern every other file in packages/loopover-ui-kit/src/components/*.tsx already uses (e.g. skeleton.tsx:1, card.tsx:3, badge.tsx:4), not @/lib/utils.
Define export type ApiFailureKind = "timeout" | "network" | "http"; locally inside the ported
file. Do not import ApiFailureKind from apps/loopover-ui/src/lib/api/request.ts — this union
is structurally identical to that file's own ApiFailureKind (request.ts:15), so this is a
zero-logic duplication of a 3-value type alias, not a redesign.
Replace the ported StateBoundary's hardcoded notifyApiFailure(...) call (currently imported
from apps/loopover-ui/src/lib/api/request.ts:109) with a new optional prop — onFailureNotify?: (args: { label: string; kind: ApiFailureKind; message: string; retry?: () => void }) => void
— invoked inside the exact same if (isError && errorLabel) effect the current code already has,
with the same { label, kind: errorKind ?? "network", message, retry: onRetry } argument shape.
When onFailureNotify is not supplied, the effect must no-op. This is the only place any new logic
is added — everything else in Requirement 1 is a literal move.
Rewrite apps/loopover-ui/src/components/site/state-views.tsx into a back-compat wrapper that:
re-exports Spinner, LoadingState, EmptyState, StateActionButton, and ErrorState unchanged
from @loopover/ui-kit/components/state-views; re-exports a StateBoundary that forwards every
prop through to the ui-kit version but defaults onFailureNotify to the app's real notifyApiFailure (from @/lib/api/request) whenever the caller doesn't supply one of its own —
so every existing app call site's runtime behavior stays byte-identical to before this move; and
keeps usePreviewDataState defined locally, unmoved (Requirement 7).
Do not change any of these existing import paths — they must keep resolving exactly as they do
today, with zero edits required in the files themselves: apps/loopover-ui/src/components/site/audit-feed.tsx, dead-letter-queue-panel.tsx, refresh-meta.tsx, app-panels/rees-analyzer-field-group.tsx, routes/app.operator.tsx, and routes/app.analytics.tsx (all import Spinner and/or StateActionButton directly from @/components/site/state-views today).
usePreviewDataState — the file's fifth export, unrelated to the four primitives named in this
issue's title — has zero call sites anywhere in the app today (confirmed by repo-wide search).
Leave it exactly where it is, in apps/loopover-ui/src/components/site/state-views.tsx, unmoved
and unmodified. Do not delete it and do not port it into ui-kit as part of this issue.
Do not modify apps/loopover-ui/src/lib/api/request.ts in this PR.
Do not touch apps/loopover-miner-ui in this PR. Wiring the miner dashboard's header/routes or
its chat message list to actually consume @loopover/ui-kit's StateBoundary happens in the
separate issues that depend on this one, not here.
This move produces zero visible/rendered output change in apps/loopover-ui — every prop and
default the app's callers see stays the same. State that explicitly in the PR's ## Summary; the ## UI Evidence screenshot table in the PR template does not apply here since there is no
before/after to capture.
Deliverables
packages/loopover-ui-kit/src/components/state-views.tsx — new file, per Requirements 1-4
apps/loopover-ui/src/components/site/state-views.tsx — rewritten into the back-compat wrapper
described in Requirement 5
No other source files changed (Requirements 6, 8, 9)
Test Coverage Requirements
packages/loopover-ui-kit/src/** is not in Codecov's measured paths — the root codecov.yml
comment and vitest.config.ts's coverage.include both list only src/**, packages/loopover-engine/src/**, packages/loopover-miner/lib/**, and one named analyzer file; packages/loopover-ui-kit/** was never included, the same way apps/** is explicitly excluded. This
move is not gated by the 99% Codecov patch requirement, in either the source or destination file — do
not expect a codecov/patch check to appear for this diff.
The real regression backstop is apps/loopover-ui's own existing suite: apps/loopover-ui/src/components/site/state-views.test.tsx (13 it(...) cases covering ErrorState's network-vs-http default copy, StateBoundary's loading/error/empty/skeleton branches,
retry/refresh actions, and — critically — two assertions that mock @/lib/api/request and check notifyApiFailure is called with the right kind). Every one of those assertions must still pass
after this move, run via npm --prefix apps/loopover-ui run test (or the root npm run test:ci,
which runs it as part of the full suite) — the two notifyApiFailure-mock assertions specifically
verify that Requirement 4's onFailureNotify wiring reaches the app's real notifier by default. If
the new prop name forces a mechanical update to how the test invokes StateBoundary, make only the
minimal edit needed to keep every existing assertion intent-identical — do not weaken or remove any
of the 13 cases.
packages/loopover-ui-kit has no test runner configured today (no vitest config, no *.test.tsx
anywhere in the package, and its package.json only defines build/typecheck scripts) — do not
add one as part of this issue; that's a separate scope decision this port doesn't imply. Instead:
packages/loopover-ui-kit's own npm run typecheck (tsc -p tsconfig.json --noEmit) and npm run build must both pass clean on the new file.
The root npm run typecheck must pass clean across both changed files.
Run the full local gate per house rule 3 before opening the PR: npm run test:ci and npm audit --audit-level=moderate.
Expected Outcome
@loopover/ui-kit ships LoadingState/EmptyState/ErrorState/StateBoundary (plus Spinner
and StateActionButton) as real package-level primitives with zero hard dependency on apps/loopover-ui's API-status plumbing.
apps/loopover-ui's visible behavior and every existing call site's runtime output are unchanged.
apps/loopover-miner-ui (or any future consumer) can import StateBoundary and friends directly
from @loopover/ui-kit/components/state-views without inventing its own copy or stubbing out notifyApiFailure.
The miner-dashboard chat message-list issue is unblocked — per the design synthesis it's required
to consume this exact primitive rather than build a second one — and so are the visual/ui-kit
adoption issues for the miner dashboard's header and four routes, which reuse StateBoundary
straight from @loopover/ui-kit instead of reaching into apps/loopover-ui.
App-local dependency being decoupled: apps/loopover-ui/src/lib/api/request.ts (ApiFailureKind
at line 15, notifyApiFailure at line 109) — read, not edited, by this issue
Existing external call sites of Spinner/StateActionButton that must keep working unmodified: apps/loopover-ui/src/components/site/audit-feed.tsx, dead-letter-queue-panel.tsx, refresh-meta.tsx, app-panels/rees-analyzer-field-group.tsx, routes/app.operator.tsx, routes/app.analytics.tsx
Downstream, dependent work (not in scope here): the miner-dashboard chat message-list issue, and
the visual/ui-kit adoption issues that restyle the miner dashboard's header and its four routes
(index/run-history/portfolio/ledgers) using StateBoundary/Skeleton/chart/pagination
primitives from @loopover/ui-kit
Context
apps/loopover-ui/src/components/site/state-views.tsxis the app's shared loading/empty/error primitive set. It exports five things:Spinner,LoadingState,EmptyState,StateActionButton,ErrorState,StateBoundary(a privateShellhelper andNETWORK_ERROR_KINDSconstant back them internally), plus a sixth, unrelated exportusePreviewDataState.StateBoundaryis the composite: givenisLoading/isError/isEmpty, it rendersLoadingState,ErrorState, orEmptyStateand otherwise passes throughchildren— every route/panel in the app that needs a consistent async-state surface reaches for it instead of hand-rolling its own branches.#6244's audit (
apps/loopover-ui/src/chat-ui-primitives-audit.md, landed via PR #6474, merged 2026-07-16) confirmed these four —LoadingState/EmptyState/ErrorState/StateBoundary— are reusable as-is for the miner-dashboard chat rail's message list, which needs its own async-state surface rather than inventing a second one. That audit also confirmed the outer shell/building-block primitives (this file, plusscroll-area.tsx,avatar.tsx,textarea.tsx/input.tsx/button.tsx) are the only reusable pieces for chat — the message list, composer, streaming renderer, and typing indicator all still need to be built new, in later issues. Per the design synthesis for the miner-dashboard redesign, this port is the one hard prerequisite that has to land before the message-list issue can start, since chat is meant to consume this exact primitive.@loopover/ui-kit(packages/loopover-ui-kit/) is the shared package already consumed by bothapps/loopover-uiandapps/loopover-miner-uias a real npm workspace dependency — not a fork.apps/loopover-miner-uialready imports ui-kit components directly (e.g.apps/loopover-miner-ui/src/routes/portfolio.tsx:4-6importsButton/Card/Tablefrom@loopover/ui-kit/components/*), whileapps/loopover-uire-exports through thin local shims under@/components/ui/*— e.g.apps/loopover-ui/src/components/ui/skeleton.tsx, whose entire body isexport * from "@loopover/ui-kit/components/skeleton";, backed bypackages/loopover-ui-kit/src/components/skeleton.tsx. That's the precedent this move follows, though (see Requirements)state-views.tsxneeds one adaptation a pureskeleton.tsx-style shim doesn't.The one real wrinkle:
state-views.tsxcurrently importsnotifyApiFailureandtype ApiFailureKindfromapps/loopover-ui/src/lib/api/request.ts(request.ts:15definesApiFailureKind,request.ts:109definesnotifyApiFailure— an app-local API-status/toast singleton wired to that file's ownbeginRequest/endRequest/reportApiFailuretracking, used to surface a deduped "Retry" toast whenStateBoundary'serrorLabelprop is set).apps/loopover-miner-ui/srchas no equivalent module at all (confirmed: nonotifyApiFailure/ApiFailureKindanywhere in that app), andpackages/loopover-ui-kitmust never import from an app (that's backwards — apps depend on ui-kit, not the reverse; also, none ofapps/loopover-ui's@/...path aliases resolve insidepackages/loopover-ui-kit, which has no aliases configured inpackages/loopover-ui-kit/tsconfig.json). This is the one place the port needs real (small, additive) adaptation rather than a byte-for-byte copy — spelled out exactly in Requirements below.Also confirmed while researching this move:
SpinnerandStateActionButton(two of the file's other exports) have real call sites today outsidestate-views.tsxitself —audit-feed.tsx,dead-letter-queue-panel.tsx,refresh-meta.tsx,app-panels/rees-analyzer-field-group.tsx,routes/app.operator.tsx, androutes/app.analytics.tsxall import one or both directly. The back-compat re-export has to cover all six of the file's current exports, not just the four named in this issue's title, or those six files break.Requirements
packages/loopover-ui-kit/src/components/state-views.tsxcontainingSpinner, theprivate
Shellhelper,StateActionButton,LoadingState,EmptyState,ErrorState, andStateBoundary, ported fromapps/loopover-ui/src/components/site/state-views.tsxverbatimexcept for the two adaptations in Requirements 3 and 4.
cnimport must beimport { cn } from "../utils";— the same relativepattern every other file in
packages/loopover-ui-kit/src/components/*.tsxalready uses (e.g.skeleton.tsx:1,card.tsx:3,badge.tsx:4), not@/lib/utils.export type ApiFailureKind = "timeout" | "network" | "http";locally inside the portedfile. Do not import
ApiFailureKindfromapps/loopover-ui/src/lib/api/request.ts— this unionis structurally identical to that file's own
ApiFailureKind(request.ts:15), so this is azero-logic duplication of a 3-value type alias, not a redesign.
StateBoundary's hardcodednotifyApiFailure(...)call (currently importedfrom
apps/loopover-ui/src/lib/api/request.ts:109) with a new optional prop —onFailureNotify?: (args: { label: string; kind: ApiFailureKind; message: string; retry?: () => void }) => void— invoked inside the exact same
if (isError && errorLabel)effect the current code already has,with the same
{ label, kind: errorKind ?? "network", message, retry: onRetry }argument shape.When
onFailureNotifyis not supplied, the effect must no-op. This is the only place any new logicis added — everything else in Requirement 1 is a literal move.
apps/loopover-ui/src/components/site/state-views.tsxinto a back-compat wrapper that:re-exports
Spinner,LoadingState,EmptyState,StateActionButton, andErrorStateunchangedfrom
@loopover/ui-kit/components/state-views; re-exports aStateBoundarythat forwards everyprop through to the ui-kit version but defaults
onFailureNotifyto the app's realnotifyApiFailure(from@/lib/api/request) whenever the caller doesn't supply one of its own —so every existing app call site's runtime behavior stays byte-identical to before this move; and
keeps
usePreviewDataStatedefined locally, unmoved (Requirement 7).today, with zero edits required in the files themselves:
apps/loopover-ui/src/components/site/audit-feed.tsx,dead-letter-queue-panel.tsx,refresh-meta.tsx,app-panels/rees-analyzer-field-group.tsx,routes/app.operator.tsx, androutes/app.analytics.tsx(all importSpinnerand/orStateActionButtondirectly from@/components/site/state-viewstoday).usePreviewDataState— the file's fifth export, unrelated to the four primitives named in thisissue's title — has zero call sites anywhere in the app today (confirmed by repo-wide search).
Leave it exactly where it is, in
apps/loopover-ui/src/components/site/state-views.tsx, unmovedand unmodified. Do not delete it and do not port it into ui-kit as part of this issue.
apps/loopover-ui/src/lib/api/request.tsin this PR.apps/loopover-miner-uiin this PR. Wiring the miner dashboard's header/routes orits chat message list to actually consume
@loopover/ui-kit'sStateBoundaryhappens in theseparate issues that depend on this one, not here.
apps/loopover-ui— every prop anddefault the app's callers see stays the same. State that explicitly in the PR's
## Summary; the## UI Evidencescreenshot table in the PR template does not apply here since there is nobefore/after to capture.
Deliverables
packages/loopover-ui-kit/src/components/state-views.tsx— new file, per Requirements 1-4apps/loopover-ui/src/components/site/state-views.tsx— rewritten into the back-compat wrapperdescribed in Requirement 5
Test Coverage Requirements
packages/loopover-ui-kit/src/**is not in Codecov's measured paths — the rootcodecov.ymlcomment and
vitest.config.ts'scoverage.includeboth list onlysrc/**,packages/loopover-engine/src/**,packages/loopover-miner/lib/**, and one named analyzer file;packages/loopover-ui-kit/**was never included, the same wayapps/**is explicitly excluded. Thismove is not gated by the 99% Codecov patch requirement, in either the source or destination file — do
not expect a
codecov/patchcheck to appear for this diff.The real regression backstop is
apps/loopover-ui's own existing suite:apps/loopover-ui/src/components/site/state-views.test.tsx(13it(...)cases coveringErrorState's network-vs-http default copy,StateBoundary's loading/error/empty/skeleton branches,retry/refresh actions, and — critically — two assertions that mock
@/lib/api/requestand checknotifyApiFailureis called with the rightkind). Every one of those assertions must still passafter this move, run via
npm --prefix apps/loopover-ui run test(or the rootnpm run test:ci,which runs it as part of the full suite) — the two
notifyApiFailure-mock assertions specificallyverify that Requirement 4's
onFailureNotifywiring reaches the app's real notifier by default. Ifthe new prop name forces a mechanical update to how the test invokes
StateBoundary, make only theminimal edit needed to keep every existing assertion intent-identical — do not weaken or remove any
of the 13 cases.
packages/loopover-ui-kithas no test runner configured today (no vitest config, no*.test.tsxanywhere in the package, and its
package.jsononly definesbuild/typecheckscripts) — do notadd one as part of this issue; that's a separate scope decision this port doesn't imply. Instead:
packages/loopover-ui-kit's ownnpm run typecheck(tsc -p tsconfig.json --noEmit) andnpm run buildmust both pass clean on the new file.npm run typecheckmust pass clean across both changed files.npm run test:ciandnpm audit --audit-level=moderate.Expected Outcome
@loopover/ui-kitshipsLoadingState/EmptyState/ErrorState/StateBoundary(plusSpinnerand
StateActionButton) as real package-level primitives with zero hard dependency onapps/loopover-ui's API-status plumbing.apps/loopover-ui's visible behavior and every existing call site's runtime output are unchanged.apps/loopover-miner-ui(or any future consumer) can importStateBoundaryand friends directlyfrom
@loopover/ui-kit/components/state-viewswithout inventing its own copy or stubbing outnotifyApiFailure.to consume this exact primitive rather than build a second one — and so are the visual/ui-kit
adoption issues for the miner dashboard's header and four routes, which reuse
StateBoundarystraight from
@loopover/ui-kitinstead of reaching intoapps/loopover-ui.Links & Resources
apps/loopover-ui/src/components/site/state-views.tsxpackages/loopover-ui-kit/src/components/state-views.tsx(new)apps/loopover-ui/src/lib/api/request.ts(ApiFailureKindat line 15,
notifyApiFailureat line 109) — read, not edited, by this issueapps/loopover-ui/src/components/ui/skeleton.tsx+packages/loopover-ui-kit/src/components/skeleton.tsxcnimport precedent:packages/loopover-ui-kit/src/components/skeleton.tsx:1,card.tsx:3,badge.tsx:4Spinner/StateActionButtonthat must keep working unmodified:apps/loopover-ui/src/components/site/audit-feed.tsx,dead-letter-queue-panel.tsx,refresh-meta.tsx,app-panels/rees-analyzer-field-group.tsx,routes/app.operator.tsx,routes/app.analytics.tsxapps/loopover-ui/src/chat-ui-primitives-audit.md, landed via PRdocs(ui): audit ui-kit for existing chat-adjacent UI primitives #6474 (
docs(ui): audit ui-kit for existing chat-adjacent UI primitives), closing the audit issuethat requested it
the visual/ui-kit adoption issues that restyle the miner dashboard's header and its four routes
(
index/run-history/portfolio/ledgers) usingStateBoundary/Skeleton/chart/paginationprimitives from
@loopover/ui-kit