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-miner-ui/src/routes/portfolio.tsx renders the local portfolio-queue dashboard as three components: PortfolioQueueView (status-count cards + per-repo table), PortfolioQueueActionsSection (the release/requeue table), and PortfolioPage (the Card shell that composes both, polling fetchPortfolioQueue via usePolledFetch/DEFAULT_POLL_INTERVAL_MS from apps/loopover-miner-ui/src/lib/use-polled-fetch.ts).
Today every loading/empty/error state in this file is a hand-written literal <p>, not a shared component:
PortfolioQueueView's result === null branch renders "Loading local portfolio queue…" as plain muted text.
Its error branch renders role="alert" text: Could not read the local portfolio queue: {result.error}.
Its empty branch renders "No queued work yet — the cards fill in once the miner enqueues its first portfolio item.".
PortfolioQueueActionsSection repeats the same three-branch pattern independently: "Loading actionable queue items…", its own role="alert" error text, and "No in-progress or completed items to release or requeue right now.".
There is no skeleton placeholder anywhere in this app. @loopover/ui-kit already ships a working Skeleton primitive at packages/loopover-ui-kit/src/components/skeleton.tsx (<div className={cn("animate-pulse rounded-md bg-primary/10", className)} />), exported at the stable subpath @loopover/ui-kit/components/skeleton — apps/loopover-miner-ui/package.json already depends on "@loopover/ui-kit": ">=0.1.0 <2.0.0", and portfolio.tsx already imports Button/Card/Table straight from @loopover/ui-kit/components/* — but nothing under apps/loopover-miner-ui/src imports Skeleton today.
StateBoundary/LoadingState/ErrorState/EmptyState/Spinner exist today only app-locally at apps/loopover-ui/src/components/site/state-views.tsx. The #6244 UI-primitives audit (apps/loopover-ui/src/chat-ui-primitives-audit.md, landed via PR #6474) confirmed these are reusable as-is for chat, but that file itself imports lucide-react, sonner's toast, and apps/loopover-ui-local @/lib/utils / @/lib/api/request — none of which apps/loopover-miner-ui pulls in today. apps/loopover-miner-ui cannot import a sibling app's src/ directly; it only ever consumes shared code through @loopover/ui-kit (confirmed workspace pattern: both apps/loopover-ui and apps/loopover-miner-ui depend on the package, never on each other's source). So this route redesign has a real, load-bearing prerequisite: state-views.tsx's primitives need to be ported into @loopover/ui-kit proper first, by a separate issue.
Requirements
⚠️ Read this before starting. This issue is blocked on the issue that ports state-views.tsx's LoadingState/EmptyState/ErrorState/StateBoundary primitives out of apps/loopover-ui/src/components/site/state-views.tsx and into @loopover/ui-kit proper. Do not fork or copy state-views.tsx into apps/loopover-miner-ui as a local one-off shim to unblock yourself, and do not add an import that reaches into apps/loopover-ui/src from this app — that is not how this workspace is wired (apps/loopover-miner-ui only ever depends on @loopover/ui-kit, never on a sibling app's source tree). Confirm the port has landed and import the real component from @loopover/ui-kit before opening a PR for this issue.
Only these files may change: apps/loopover-miner-ui/src/routes/portfolio.tsx, apps/loopover-miner-ui/src/portfolio-queue.test.tsx, and apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx. Do not touch run-history.tsx, ledgers.tsx, index.tsx, or __root.tsx — those routes are separate, independently-scoped redesign issues using the same pattern.
Do not modify apps/loopover-miner-ui/src/lib/portfolio-queue.ts, apps/loopover-miner-ui/src/lib/portfolio-queue-actions.ts, or apps/loopover-miner-ui/src/lib/use-polled-fetch.ts. The fetchers, the POST /api/portfolio-queue/{release,requeue} wiring, and the poll cadence (DEFAULT_POLL_INTERVAL_MS) stay byte-for-byte as they are.
The Button elements in PortfolioQueueActionsSection (Release/Requeue) and their onClick/disabled={pending} wiring to onRelease/onRequeue → releaseItem/requeueItem must keep the exact same call sites and behavior. This restyle only changes the surrounding container/loading/empty/error chrome around those buttons — the buttons and the endpoints they hit are explicitly out of scope. A future issue wires chat's action-dispatch through these same buttons/endpoints; changing them here would break that plan.
Replace PortfolioQueueView's result === null branch and PortfolioQueueActionsSection's result === null branch with the ported StateBoundary/LoadingState component wrapping Skeleton placeholders shaped like the real content: 3 skeleton cards for the dl status cards and skeleton rows for the per-repo table (in PortfolioQueueView); skeleton rows shaped like the queue-actions table (in PortfolioQueueActionsSection).
Replace the hand-written error/empty <p> blocks in both components with the ported ErrorState/EmptyState, preserving the exact current copy and role="alert" semantics on errors — do not reword the user-visible strings (Could not read the local portfolio queue: {error}, Could not read actionable queue items: {error}, Queue action failed: {error}, No queued work yet…, No in-progress or completed items to release or requeue right now.).
Update, don't delete, the two existing loading-state test assertions that will break under the new markup: apps/loopover-miner-ui/src/portfolio-queue.test.tsx's "renders the loading state before the first result arrives" test (currently expect(screen.getByText(/Loading local portfolio queue/i)).toBeTruthy()) and the equivalently-named test in apps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx. Every other existing test in both files — including the release/requeue wiring assertions, the pending-disable test, and the #6090 regression test (failing release renders the error without a false re-fetch) — must keep passing with their existing behavioral assertions unchanged.
Deliverables
PortfolioQueueView's loading branch renders StateBoundary/LoadingState + Skeleton placeholders shaped like the 3 status cards and per-repo table
PortfolioQueueActionsSection's loading branch renders StateBoundary/LoadingState + Skeleton placeholders shaped like the queue-actions table
Both components' error/empty branches use the ported ErrorState/EmptyState, with unchanged copy and role="alert" on errors
lib/portfolio-queue.ts, lib/portfolio-queue-actions.ts, lib/use-polled-fetch.ts, and all Button/onClick wiring left untouched
portfolio-queue.test.tsx and portfolio-queue-actions.test.tsx updated so their loading-state assertions match the new markup, with all other existing assertions still passing unmodified
Test Coverage Requirements
apps/loopover-miner-ui sits under apps/**, which codecov.yml's ignore: list explicitly excludes (Codecov only collects coverage over src/**, packages/loopover-engine/src/**, and packages/loopover-miner/lib/**), so this PR is not scored by codecov/patch. It is still gated by this app's own local test script (apps/loopover-miner-ui/package.json's "test": "vitest run --coverage") as part of the whole-repo npm run test:ci local gate, which must stay green.
Update the two named loading-state test assertions to check for the new skeleton/StateBoundary output (e.g. by role/test-id on the skeleton container) instead of the removed literal text.
Keep every other existing assertion in portfolio-queue.test.tsx and portfolio-queue-actions.test.tsx passing unchanged, in particular: the release/requeue POST-wiring tests, the pending-disable test, and the #6090 regression test — these prove the buttons/actions genuinely weren't touched.
No new invariant tests are required beyond the above — this is a rendering-only change with no new branches/logic to cover.
Expected Outcome
The portfolio route shows content-shaped skeleton placeholders during its poll-cadence loading window instead of a flat "Loading…" sentence, using the same StateBoundary/Skeleton primitives the rest of the miner-ui redesign is standardizing on — while the release/requeue actions behave identically to today: same buttons, same POST /api/portfolio-queue/{release,requeue} endpoints, same disabled-while-pending behavior, verified by the untouched wiring tests. No new attack surface is introduced: no fetcher, endpoint, or button semantic changes.
Links & Resources
apps/loopover-miner-ui/src/routes/portfolio.tsx — the file being restyled
The issue that ports state-views.tsx's primitives into @loopover/ui-kit — hard prerequisite; do not start this issue until that one has landed
The analogous redesign issues for the run-history and ledgers routes — same StateBoundary/Skeleton pattern applied to those routes, independently scoped
The portfolio release/requeue chat action-dispatch issue — later work that calls through these exact same, unmodified buttons and endpoints; this issue's job is to keep that surface visually restyled but behaviorally frozen underneath
Context
apps/loopover-miner-ui/src/routes/portfolio.tsxrenders the local portfolio-queue dashboard as three components:PortfolioQueueView(status-count cards + per-repo table),PortfolioQueueActionsSection(the release/requeue table), andPortfolioPage(theCardshell that composes both, pollingfetchPortfolioQueueviausePolledFetch/DEFAULT_POLL_INTERVAL_MSfromapps/loopover-miner-ui/src/lib/use-polled-fetch.ts).Today every loading/empty/error state in this file is a hand-written literal
<p>, not a shared component:PortfolioQueueView'sresult === nullbranch renders"Loading local portfolio queue…"as plain muted text.role="alert"text:Could not read the local portfolio queue: {result.error}."No queued work yet — the cards fill in once the miner enqueues its first portfolio item.".PortfolioQueueActionsSectionrepeats the same three-branch pattern independently:"Loading actionable queue items…", its ownrole="alert"error text, and"No in-progress or completed items to release or requeue right now.".There is no skeleton placeholder anywhere in this app.
@loopover/ui-kitalready ships a workingSkeletonprimitive atpackages/loopover-ui-kit/src/components/skeleton.tsx(<div className={cn("animate-pulse rounded-md bg-primary/10", className)} />), exported at the stable subpath@loopover/ui-kit/components/skeleton—apps/loopover-miner-ui/package.jsonalready depends on"@loopover/ui-kit": ">=0.1.0 <2.0.0", andportfolio.tsxalready importsButton/Card/Tablestraight from@loopover/ui-kit/components/*— but nothing underapps/loopover-miner-ui/srcimportsSkeletontoday.StateBoundary/LoadingState/ErrorState/EmptyState/Spinnerexist today only app-locally atapps/loopover-ui/src/components/site/state-views.tsx. The #6244 UI-primitives audit (apps/loopover-ui/src/chat-ui-primitives-audit.md, landed via PR #6474) confirmed these are reusable as-is for chat, but that file itself importslucide-react,sonner'stoast, andapps/loopover-ui-local@/lib/utils/@/lib/api/request— none of whichapps/loopover-miner-uipulls in today.apps/loopover-miner-uicannot import a sibling app'ssrc/directly; it only ever consumes shared code through@loopover/ui-kit(confirmed workspace pattern: bothapps/loopover-uiandapps/loopover-miner-uidepend on the package, never on each other's source). So this route redesign has a real, load-bearing prerequisite:state-views.tsx's primitives need to be ported into@loopover/ui-kitproper first, by a separate issue.Requirements
apps/loopover-miner-ui/src/routes/portfolio.tsx,apps/loopover-miner-ui/src/portfolio-queue.test.tsx, andapps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx. Do not touchrun-history.tsx,ledgers.tsx,index.tsx, or__root.tsx— those routes are separate, independently-scoped redesign issues using the same pattern.apps/loopover-miner-ui/src/lib/portfolio-queue.ts,apps/loopover-miner-ui/src/lib/portfolio-queue-actions.ts, orapps/loopover-miner-ui/src/lib/use-polled-fetch.ts. The fetchers, thePOST /api/portfolio-queue/{release,requeue}wiring, and the poll cadence (DEFAULT_POLL_INTERVAL_MS) stay byte-for-byte as they are.Buttonelements inPortfolioQueueActionsSection(Release/Requeue) and theironClick/disabled={pending}wiring toonRelease/onRequeue→releaseItem/requeueItemmust keep the exact same call sites and behavior. This restyle only changes the surrounding container/loading/empty/error chrome around those buttons — the buttons and the endpoints they hit are explicitly out of scope. A future issue wires chat's action-dispatch through these same buttons/endpoints; changing them here would break that plan.PortfolioQueueView'sresult === nullbranch andPortfolioQueueActionsSection'sresult === nullbranch with the portedStateBoundary/LoadingStatecomponent wrappingSkeletonplaceholders shaped like the real content: 3 skeleton cards for thedlstatus cards and skeleton rows for the per-repo table (inPortfolioQueueView); skeleton rows shaped like the queue-actions table (inPortfolioQueueActionsSection).<p>blocks in both components with the portedErrorState/EmptyState, preserving the exact current copy androle="alert"semantics on errors — do not reword the user-visible strings (Could not read the local portfolio queue: {error},Could not read actionable queue items: {error},Queue action failed: {error},No queued work yet…,No in-progress or completed items to release or requeue right now.).apps/loopover-miner-ui/src/portfolio-queue.test.tsx's"renders the loading state before the first result arrives"test (currentlyexpect(screen.getByText(/Loading local portfolio queue/i)).toBeTruthy()) and the equivalently-named test inapps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx. Every other existing test in both files — including the release/requeue wiring assertions, the pending-disable test, and the#6090regression test (failing release renders the error without a false re-fetch) — must keep passing with their existing behavioral assertions unchanged.Deliverables
PortfolioQueueView's loading branch rendersStateBoundary/LoadingState+Skeletonplaceholders shaped like the 3 status cards and per-repo tablePortfolioQueueActionsSection's loading branch rendersStateBoundary/LoadingState+Skeletonplaceholders shaped like the queue-actions tableErrorState/EmptyState, with unchanged copy androle="alert"on errorslib/portfolio-queue.ts,lib/portfolio-queue-actions.ts,lib/use-polled-fetch.ts, and all Button/onClick wiring left untouchedportfolio-queue.test.tsxandportfolio-queue-actions.test.tsxupdated so their loading-state assertions match the new markup, with all other existing assertions still passing unmodifiedTest Coverage Requirements
apps/loopover-miner-uisits underapps/**, whichcodecov.yml'signore:list explicitly excludes (Codecov only collects coverage oversrc/**,packages/loopover-engine/src/**, andpackages/loopover-miner/lib/**), so this PR is not scored bycodecov/patch. It is still gated by this app's own local test script (apps/loopover-miner-ui/package.json's"test": "vitest run --coverage") as part of the whole-reponpm run test:cilocal gate, which must stay green.StateBoundaryoutput (e.g. by role/test-id on the skeleton container) instead of the removed literal text.portfolio-queue.test.tsxandportfolio-queue-actions.test.tsxpassing unchanged, in particular: the release/requeue POST-wiring tests, the pending-disable test, and the#6090regression test — these prove the buttons/actions genuinely weren't touched.Expected Outcome
The portfolio route shows content-shaped skeleton placeholders during its poll-cadence loading window instead of a flat "Loading…" sentence, using the same
StateBoundary/Skeletonprimitives the rest of the miner-ui redesign is standardizing on — while the release/requeue actions behave identically to today: same buttons, samePOST /api/portfolio-queue/{release,requeue}endpoints, same disabled-while-pending behavior, verified by the untouched wiring tests. No new attack surface is introduced: no fetcher, endpoint, or button semantic changes.Links & Resources
apps/loopover-miner-ui/src/routes/portfolio.tsx— the file being restyledapps/loopover-miner-ui/src/lib/portfolio-queue-actions.ts— release/requeue wiring, untouchedapps/loopover-miner-ui/src/lib/portfolio-queue.ts— summary fetcher, untouchedapps/loopover-miner-ui/src/lib/use-polled-fetch.ts— poll cadence, untouchedapps/loopover-miner-ui/src/portfolio-queue.test.tsxandapps/loopover-miner-ui/src/portfolio-queue-actions.test.tsx— existing tests to updatepackages/loopover-ui-kit/src/components/skeleton.tsx— theSkeletonprimitive, already available today via@loopover/ui-kit/components/skeletonapps/loopover-ui/src/components/site/state-views.tsx— theLoadingState/EmptyState/ErrorState/StateBoundarysource to be portedapps/loopover-ui/src/chat-ui-primitives-audit.md(landed via PR docs(ui): audit ui-kit for existing chat-adjacent UI primitives #6474) — confirms these primitives are reusable as-is once portedstate-views.tsx's primitives into@loopover/ui-kit— hard prerequisite; do not start this issue until that one has landedStateBoundary/Skeletonpattern applied to those routes, independently scoped