Skip to content

XS⚠️ ◾ Replace Loader2 icon usages with LoadingState component#986

Open
tomek-i wants to merge 1 commit into
mainfrom
708-loader2-to-loadingstate
Open

XS⚠️ ◾ Replace Loader2 icon usages with LoadingState component#986
tomek-i wants to merge 1 commit into
mainfrom
708-loader2-to-loadingstate

Conversation

@tomek-i

@tomek-i tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

Replaces every direct Loader2 (lucide-react) icon usage in the desktop UI with the existing reusable LoadingState component, so loading indicators are visually and behaviourally consistent across the app instead of each callsite hand-rolling its own spinner classes.

Closes #708

Key decisions

  • Extended LoadingState with optional className / inline props instead of introducing a second component. className forwards to the underlying Spinner (which already merges classes via cn/tailwind-merge and applies animate-spin by default), so callers migrating a bare Loader2 icon (e.g. mr-2 h-4 w-4) keep identical sizing/margin without re-specifying animate-spin. inline skips the default flex items-center justify-center py-8 block wrapper so the spinner can sit next to a label inside a button/badge instead of forcing block layout.
  • No-props behaviour is unchanged — every pre-existing bare <LoadingState /> caller (HomePage, ProjectsPage, ApprovalDialog, FinalResultPanel, PromptSelectionDialog) renders pixel-identical output; those files have zero diff.
  • WorkflowStepCard's STATUS_CONFIG icon table mapped in_progress to the Loader2 component reference, rendered generically alongside CheckCircle2/XCircle via <Icon className={...} />. Since LoadingState isn't a drop-in Lucide-icon-shaped component, StatusIcon now special-cases status === "in_progress" to render <LoadingState inline className={cn("size-5", config.iconClass, className)} /> before falling through to the generic icon render for the other statuses.

Files changed

  • src/ui/src/components/common/LoadingState.tsx — add optional className / inline props
  • src/ui/src/components/health-status/health-status.tsx
  • src/ui/src/components/settings/llm/LLMProviderForm.tsx
  • src/ui/src/components/settings/llm/OrchestratorBackendSetting.tsx
  • src/ui/src/components/video/UploadResult.tsx
  • src/ui/src/components/workflow/ShaveOutcomeView.tsx
  • src/ui/src/components/workflow/UndoStagePanel.tsx
  • src/ui/src/components/workflow/WorkflowStepCard.tsx

No remaining Loader2 usages in application code — grep -rn "Loader2" src/ui/src --include="*.tsx" --include="*.ts" returns nothing outside ui/spinner.tsx's own Loader2Icon import, which is the low-level primitive LoadingState already wraps and was intentionally left untouched.

Acceptance criteria

  • All UI places that previously used Loader2 now use LoadingState.
  • Visual appearance of loaders remains consistent — each replacement passes the original size/margin/colour classes through className, so rendered output matches the prior markup (animate-spin now applied by LoadingState/Spinner itself).
  • LoadingState is flexible enough to support prior sizing/margin/animation via props (className, inline) with no visual regressions.
  • Tests updated where applicable and CI passes — no existing test/snapshot referenced Loader2 by name; the one test coupled to the visual contract (WorkflowStepCard.test.tsx's .animate-spin querySelector assertion for the in_progress status) still passes since Spinner applies animate-spin by default.

Testing performed

  • npm run build — pass
  • npm rebuild better-sqlite3 --build-from-source && npx vitest run --exclude 'src/ui/**' — pass (68 files / 696 tests)
  • npm --prefix src/ui install && npm --prefix src/ui test — pass (35 files / 259 tests), including WorkflowStepCard.test.tsx, UploadResult.test.tsx, ShaveOutcomeView.test.tsx
  • npm run lint — pass (biome clean; one pre-existing informational note on an unrelated file with no diff in this PR)
  • npm run format — no fixes needed; git diff --exit-code clean

Adds optional className/inline props to LoadingState so it can replace
every direct Loader2 usage while preserving original sizing, margin,
and animation. Closes #708.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 07:12
@tomek-i tomek-i added the armada Eligible for the ARMADA fleet to pick up label Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR Metrics

Thanks for keeping your pull request small.
⚠️ Consider adding additional tests.

Lines
Product Code 45
Test Code -
Subtotal 45
Ignored Code -
Total 45

Metrics computed by PR Metrics. Add it to your Azure DevOps and GitHub PRs!

@github-actions github-actions Bot changed the title Replace Loader2 icon usages with LoadingState component XS⚠️ ◾ Replace Loader2 icon usages with LoadingState component Jul 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR standardizes loading indicators across the desktop UI by replacing direct Loader2 (lucide-react) usages with the shared LoadingState component, improving visual/behavioral consistency and reducing duplicated spinner styling.

Changes:

  • Extended LoadingState to support inline rendering and custom sizing/margin via inline + className.
  • Replaced inline Loader2 JSX usages in several UI components with LoadingState inline while preserving sizing/color classes.
  • Updated WorkflowStepCard status icon rendering to special-case the in_progress status using LoadingState instead of the previous icon table entry.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/ui/src/components/common/LoadingState.tsx Adds className + inline props to support both block and inline spinners.
src/ui/src/components/health-status/health-status.tsx Migrates “checking” spinner to LoadingState inline.
src/ui/src/components/settings/llm/LLMProviderForm.tsx Replaces submit-button loader icon with LoadingState inline.
src/ui/src/components/settings/llm/OrchestratorBackendSetting.tsx Replaces readiness re-check spinner with LoadingState inline.
src/ui/src/components/video/UploadResult.tsx Replaces uploading badge spinner with LoadingState inline.
src/ui/src/components/workflow/ShaveOutcomeView.tsx Replaces “still running” spinner with LoadingState inline.
src/ui/src/components/workflow/UndoStagePanel.tsx Replaces in-progress status spinner with LoadingState inline.
src/ui/src/components/workflow/WorkflowStepCard.tsx Uses LoadingState for in_progress status + retry button spinner.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Pre-release build is available for this PR:
https://github.com/SSWConsulting/SSW.YakShaver.Desktop/releases/tag/0.6.0-beta.986.1784617978

@tomek-i tomek-i added armada:reviewing Claimed by crows-nest; review->merge pipeline running and removed armada Eligible for the ARMADA fleet to pick up labels Jul 21, 2026
@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

🔭 crows-nest: ready-PR pipeline started — review → address → re-validate → gated merge.

@@ -16,7 +17,7 @@ const openUrl = (url: string | null) => {

const UploadingBadge = () => (
<span className="text-sm font-medium px-3 py-1.5 rounded-full bg-white/10 text-white/80 border border-white/20 flex items-center gap-2">

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Loading spinner now announces redundant "Loading" to screen readers

Spinner (src/ui/src/components/ui/spinner.tsx) unconditionally renders role="status" aria-label="Loading". Previously the bare Loader2 icons at these 7 migrated call sites had no ARIA semantics at all — now every one announces "Loading" via an implicit live region. Here specifically, the spinner sits directly beside the visible text "Uploading", so a screen reader will likely announce something like "Loading Uploading" — redundant rather than a clean improvement. Similar redundancy exists at LLMProviderForm.tsx (+"Save"), OrchestratorBackendSetting.tsx (+"Re-check"), ShaveOutcomeView.tsx (+"This shave is still running"), UndoStagePanel.tsx, and WorkflowStepCard.tsx's retry button. Consider an aria-hidden escape hatch on Spinner/LoadingState (or an optional label override) for inline usages where adjacent visible text already conveys the loading state, rather than every migrated site double-announcing by default.

flagged by: codex-rescue

}

return (
<div className="flex items-center justify-center py-8">

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] LoadingState conflates two visual roles behind a silent boolean default

LoadingState previously had one contract: full-block centered loader. It now has two — pass nothing → block wrapper with py-8 padding; pass inline → bare inline spinner. Because inline defaults to false with no compile-time signal distinguishing the two use cases, a caller who forgets inline at a new inline call site silently gets a broken block layout instead of a type error. This is exactly what's already happened at several pre-existing (untouched-by-this-PR) call sites in FinalResultPanel.tsx and ApprovalDialog.tsx — see the top-level summary comment. Not blocking for this diff since it doesn't itself introduce a wrong callsite, but it's the abstraction seam most likely to keep causing bugs as more call sites migrate. Consider two distinctly-named exports (e.g. LoadingState for block, an InlineSpinner/LoadingIcon for inline) instead of one component with a silently-defaulted mode flag.

flagged by: codex-rescue

in_progress: {
icon: Loader2,
iconClass: "animate-spin text-zinc-300",
icon: null,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] STATUS_CONFIG.in_progress.icon is now dead/unused

in_progress.icon is set to null and iconClass no longer includes animate-spin, but StatusIcon special-cases status === "in_progress" before ever reading config.icon (line ~92), so this field is now vestigial — it happens to share the same null value as not_started's icon, but for not_started that null is actually read and drives the neutral-circle fallback, while for in_progress it's never read at all (the early return bypasses it). A future maintainer skimming STATUS_CONFIG could reasonably assume icon: null still means "render the neutral circle" for in_progress", and removing the special case would silently turn the spinner into a plain circle. Consider dropping the iconkey from thein_progress` entry (or adding a short comment noting it's unused/kept only for object-shape uniformity).

flagged independently by both lenses: code-review + codex-rescue

return (
<div className="flex items-center justify-center py-8">
<Spinner />
<Spinner className={className} />

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Inline vs block branches duplicate the Spinner render

The two branches (if (inline) return <Spinner className={className} /> and the block-wrapped fallback) both just render <Spinner className={className} />, differing only in whether it's wrapped in the flex items-center justify-center py-8 div. Could be simplified to a single return with a conditional wrapper, e.g.:

const spinner = <Spinner className={className} />;
return inline ? spinner : <div className="flex items-center justify-center py-8">{spinner}</div>;

Purely stylistic — current form is also perfectly readable.

flagged by: code-review

@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

[major, out-of-diff — flagged by: codex-rescue] Pre-existing non-inline LoadingState usages left unmigrated in FinalResultPanel.tsx

src/ui/src/components/workflow/FinalResultPanel.tsx (not touched by this PR) has 4 call sites — lines 926, 941, 954, 991 — that render bare <LoadingState /> inside small <Button> elements next to short text/icon siblings (RotateCcw, Undo2, "Reprocess original shave", "Undo", "Run reprocess"). Because inline defaults to false, each of these renders the full flex items-center justify-center py-8 block wrapper inside a button — very likely a real, pre-existing visual bug (oversized button height from the block padding).

This PR adds exactly the inline prop that fixes this pattern, and its own doc comment ("Callers migrating a small inline Loader2 ... next to text ... should pass true") describes this exact scenario, yet the PR doesn't touch this file. Since the PR is scoped as the definitive Loader2→LoadingState migration and claims to close #708, this is worth a fast-follow: apply inline at these 4 sites (or file a follow-up issue) so the fix doesn't stop short of full internal consistency.

Not blocking for this PR (the bug predates it and this PR doesn't regress it), but flagged because it directly undercuts the PR's own "no visual regressions / consistent loaders" framing if left as-is.

@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

[major, out-of-diff — flagged by: codex-rescue] Pre-existing non-inline LoadingState usages left unmigrated in ApprovalDialog.tsx

src/ui/src/components/workflow/ApprovalDialog.tsx (not touched by this PR) has 4 call sites — lines 254, 275, 305, 321 — rendering bare <LoadingState /> inside <span className="flex items-center gap-2">...text</span> alongside labels like "Cancelling...", "Sending...", "Saving...", "Processing...". Without inline, this renders the block-level py-8 wrapper inline in a flex row, producing an oversized/misaligned spinner relative to the adjacent text — same class of bug as FinalResultPanel.tsx (see separate comment).

Same recommendation: apply inline at these sites (or file a fast-follow issue), since leaving both usage patterns (<LoadingState /> vs <LoadingState inline />) coexisting post-merge will confuse future contributors about which is "correct" for an inline-next-to-text context.

Not blocking for this PR — the bug predates it and isn't introduced or worsened by this diff.

@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

muster review — PR #986

Two independent lenses reviewed this diff in parallel: code-review (conventions/correctness) and codex-rescue (root-cause second opinion). Findings below are consolidated and deduped.

Summary

Severity Count
blocking 0
major 2
minor 2
nit 2

Bottom line: no blocking findings — review advisory only. The in-diff change itself (7 call sites migrated from bare Loader2 to LoadingState, plus the new className/inline props) is correct, in-scope, and free of visual regressions: animate-spin is correctly supplied once by Spinner rather than duplicated, cn/tailwind-merge resolves size-class conflicts as expected, the STATUS_CONFIG.icon: null change for in_progress is safe because StatusIcon special-cases that status before ever reading .icon, and no leftover Loader2 references remain outside ui/spinner.tsx's own primitive import (verified independently).

Where the lenses agreed vs. diverged

  • Agreement: both lenses independently flagged that STATUS_CONFIG.in_progress.icon is now dead/vestigial data (nit, WorkflowStepCard.tsx) — independent agreement, kept as one finding.
  • Divergence: the code-review lens read this as a clean, low-risk diff with only two nits. The codex-rescue lens, reading cold for root cause, went further and looked at whether the PR's own stated goal ("consistent loaders repo-wide", closes Replace Loader2 icon usages with LoadingState component for consistent loading UI #708) is actually achieved — and found it isn't fully: two files outside this PR's diff (FinalResultPanel.tsx, ApprovalDialog.tsx) already use bare, non-inline <LoadingState /> inside buttons/inline text rows, which (pre-existing, not introduced by this PR) very likely renders an oversized block-padding spinner in a tight inline context. This PR adds the exact inline prop that would fix those call sites but doesn't apply it there. Posted as top-level (not inline) comments since those files have no diff in this PR to anchor to.
  • The codex-rescue lens also raised an accessibility point the code-review lens didn't: Spinner unconditionally sets role="status" aria-label="Loading", which is new semantics for all 7 migrated call sites (previously bare Loader2 had none) — several sites already have adjacent visible loading text, risking redundant screen-reader announcements (e.g. "Loading Uploading").

Findings

  1. [major, out-of-diff] FinalResultPanel.tsx — 4 pre-existing bare <LoadingState /> sites inside buttons need inline to avoid block-padding blowup; not introduced/regressed by this PR, but undercuts its "no visual regressions, consistent loaders" claim if left as a fast-follow gap. (codex-rescue)
  2. [major, out-of-diff] ApprovalDialog.tsx — same pattern, 4 sites. (codex-rescue)
  3. [minor] LoadingState.tsx — the component now conflates two visual roles (block placeholder vs. inline icon) behind a silently-defaulted inline boolean; the two findings above are exactly the kind of drift this seam invites as more callers migrate. (codex-rescue)
  4. [minor] UploadResult.tsx (representative of all 7 migrated sites) — Spinner's unconditional role="status" aria-label="Loading" risks redundant screen-reader announcements next to existing visible loading text. (codex-rescue)
  5. [nit] WorkflowStepCard.tsxSTATUS_CONFIG.in_progress.icon: null is now dead/unread data, risking future maintainer confusion with not_started's (actually-read) icon: null. (both lenses — code-review + codex-rescue)
  6. [nit] LoadingState.tsx — inline/block branches both just render <Spinner className={className}/>, could collapse to one return with a conditional wrapper. Purely stylistic. (code-review)

Inline comments posted at the relevant lines for findings 3–6; findings 1–2 posted as top-level comments (no diff lines in this PR to anchor to).


Posted by ARMADA muster — two-lens review (code-review + codex-rescue), non-interactive.

@tomek-i tomek-i removed the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 21, 2026
@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

✅ crows-nest: reviewed — 6 findings (2 major, 2 minor, 2 nit), zero blocking. Advisory only — awaiting human merge (auto-merge off).

@tomek-i tomek-i added the armada Eligible for the ARMADA fleet to pick up label Jul 21, 2026
@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

🔭 crows-nest: reconciling stale claim — pipeline had already completed (reviewed, addressed, zero blocking findings) but the armada:reviewing label was never cleared. Reconciled to ready_awaiting_human; no re-review performed.

@tomek-i tomek-i added the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 21, 2026
@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

🔭 crows-nest: picked up for review by ARMADA muster (unified scheduler tick, label armada).

@@ -60,8 +60,8 @@ function OrchestratorBadge({ backend }: { backend: OrchestratorBackend }) {

const STATUS_CONFIG = {
in_progress: {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] STATUS_CONFIG.in_progress.icon is now dead/misleading data

StatusIcon special-cases status === "in_progress" with an early return (rendering <LoadingState inline .../>) before config.icon is ever read for that branch. STATUS_CONFIG.in_progress.icon was changed from Loader2 to null but the key is still present — unlike not_started.icon: null, which legitimately falls through to the generic !Icon branch, in_progress.icon is now unreachable/unused. A future maintainer changing STATUS_CONFIG.in_progress.icon (e.g. swapping the icon) would see no effect and have to discover the early-return special-case in StatusIcon to understand why.

Suggested fix: drop the icon key from the in_progress entry (or add a short comment noting it's superseded by StatusIcon's early return).

flagged by: both lenses independently (code-review + codex-rescue)

disabled={isCheckingReadiness}
>
{isCheckingReadiness && <Loader2 className="mr-1.5 h-3 w-3 animate-spin" />}
{isCheckingReadiness && <LoadingState inline className="mr-1.5 h-3 w-3" />}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Nested role="status" live region introduced at this callsite

SettingsWarningBanner (src/ui/src/components/settings/SettingsWarningBanner.tsx) renders its action slot inside an <output aria-live="polite">, which carries an implicit role="status". Previously the "Re-check" button's Loader2 icon carried no ARIA role, so there was no nesting. Now <LoadingState inline className="mr-1.5 h-3 w-3" /> renders Spinner, which sets an explicit role="status" aria-label="Loading" (src/ui/src/components/ui/spinner.tsx) — producing a role="status" element nested inside another role="status"/aria-live region. This is new at this one callsite (none of the other seven converted callsites sit inside an existing live region). Screen readers may announce the region twice or behave inconsistently with duplicated status roles.

Not blocking, but worth a look — consider suppressing Spinner's own role in this nested context, or confirm the double-announcement is an accepted trade-off.

flagged by: code-review lens

}

return (
<div className="flex items-center justify-center py-8">

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] New className/inline props on LoadingState have no direct test coverage

No test file exists for LoadingState.tsx. This PR adds two new, behavior-changing props — className (forwarded into Spinner's cn() merge) and inline (conditionally omits the block wrapper) — with no unit test verifying: (a) inline actually omits the flex items-center justify-center py-8 wrapper, (b) className reaches the rendered Spinner, (c) the no-args/default rendering is unchanged. Coverage relies entirely on incidental assertions in the 8 consuming components, most of which (HealthStatus, LLMProviderForm, OrchestratorBackendSetting, UndoStagePanel) have no test file at all, and none assert on the spinner's classes or wrapper presence.

Suggested fix: a small dedicated test rendering LoadingState with/without inline and asserting wrapper presence/absence + className passthrough would catch regressions (e.g. the tailwind-merge sizing issue flagged separately) before they reach a consumer.

flagged by: codex-rescue (fallback lens)

* previously rendered a bare `Loader2` icon inline can preserve their sizing/margin.
*/
className?: string;
/**

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Design note: this relocates rather than root-causes the loading-indicator inconsistency

Issue #708's presumed intent is that loading indicators are visually/behaviourally inconsistent across the app. This PR is a real improvement — it centralizes the icon and default animate-spin behaviour into Spinner/LoadingState — but by adding two open-ended escape hatches (className free-forms size/margin/colour per callsite, inline toggles layout), every callsite still hand-picks its own size (h-5 w-5 / h-4 w-4 / h-3 w-3 / w-3 h-3 / size-3.5 / size-5) and colour (text-white/50, text-purple-200, text-zinc-300, unstyled). The visual inconsistency is preserved, just expressed through className props on a shared component instead of raw Loader2 props.

Not blocking — worth naming as a follow-up if #708's real goal was visual consistency rather than code reuse: consider a size/variant prop enum instead of open className forwarding.

flagged by: codex-rescue (fallback lens)

@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Muster review — PR #986

Dual-lens review note (degraded): the codex:codex-rescue agent type is not available in this environment, so the second lens was run as a general-purpose subagent explicitly briefed to give a cold, adversarial, root-cause second opinion on the same diff (independent of the first lens's output). This is a best-effort substitute, not the true codex-rescue lens — flagging per muster's degrade-transparency requirement. Both lenses' claims were independently spot-checked (tailwind-merge behaviour verified with a real npm install tailwind-merge@3.3.1 + twMerge() call; the ARIA nesting and STATUS_CONFIG dead-code claims verified by reading the actual source).

Verdict: no blocking findings — review advisory only

Summary: 0 blocking · 1 major · 4 minor · 2 nit

The refactor itself is sound: mechanical Loader2LoadingState swaps across 8 files, all previously-hand-rolled sizing/margin/colour classes are preserved via the new className prop, inline is applied correctly at every converted callsite (never leaving a block-wrapped spinner where inline layout is needed), and a repo-wide grep confirms no remaining Loader2/Loader2Icon usage in application code outside spinner.tsx (the low-level primitive, intentionally untouched). No correctness bug or broken functionality was found.

Findings (worst first)

  1. [major] src/ui/src/components/ui/spinner.tsxSpinner's cn("size-4 animate-spin", className) merge does not reliably override size-4 with the h-*/w-* class pairs six of the eight migrated callsites now pass through LoadingState. Verified with tailwind-merge@3.3.1: twMerge("size-4 animate-spin", "h-5 w-5 text-white/50")"size-4 animate-spin h-5 w-5 text-white/50"size-4 is not dropped (tailwind-merge only dedupes size-* against other size-* classes, confirmed separately). Which class wins today depends on Tailwind's internal utility-emission order, not a contract cn guarantees. Not a proven visible bug right now, but a latent fragility newly exercised by this PR (the one pre-existing Spinner caller passes size-8, which merges cleanly). (posted as a PR-level finding — spinner.tsx is unchanged by this PR, so GitHub's review-comment API won't anchor an inline comment to it.)
  2. [minor] src/ui/src/components/workflow/WorkflowStepCard.tsx:62STATUS_CONFIG.in_progress.icon is now dead/misleading data; StatusIcon's early return for in_progress never reads it. Flagged independently by both lenses — strong signal. Posted inline.
  3. [minor] src/ui/src/components/settings/llm/OrchestratorBackendSetting.tsx:210 — the migrated LoadingState inline now nests an explicit role="status" inside SettingsWarningBanner's <output aria-live="polite"> (implicit role="status"), a new double-live-region at this one callsite. Posted inline.
  4. [minor] src/ui/src/components/common/LoadingState.tsx — the new className/inline props have no direct test coverage; regressions (e.g. finding ✨ Native Linux Support - Enable YakShaver Desktop App to run natively on Linux #1) would only surface incidentally via consumers, most of which have no tests either. Posted inline.
  5. [nit] src/ui/src/components/common/LoadingState.tsx — design note: the PR centralizes the icon/animation but preserves per-callsite size/colour inconsistency via open className forwarding rather than a constrained size/variant API. Worth a follow-up conversation, not blocking. Posted inline.
  6. [nit] src/ui/src/components/workflow/FinalResultPanel.tsx (untouched by this PR) — has pre-existing bare <LoadingState /> usages inside <Button> elements that could adopt the new inline prop for consistency with this PR's pattern. Out of scope here; noted as a possible follow-up. (posted as a PR-level finding — file isn't part of this diff.)

Lens agreement / divergence

The two lenses converged strongly on the STATUS_CONFIG.in_progress.icon dead-code point (independently raised by both) and otherwise covered complementary ground — the conventions lens focused on the ARIA-nesting and out-of-scope-consistency angles, the root-cause lens dug into the tailwind-merge class-precedence mechanics and test-coverage/design-framing questions. No outright disagreement between the two.

Bottom line

0 blocking, 1 major, 4 minor, 2 nit — not blocking merge, but the tailwind-merge sizing fragility (#1) is worth a look before or shortly after merge since it's the one finding with a plausible (if unproven) path to a visible regression.

@tomek-i tomek-i removed the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 21, 2026
@tomek-i

tomek-i commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

🔭 crows-nest: muster review complete — no blocking findings (7 non-blocking findings posted for follow-up). PR is green and mergeable but autoMerge is off in .armada/config.json, so this stops here: ready to merge, awaiting a human.

@tomek-i tomek-i added the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 22, 2026
@tomek-i

tomek-i commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

🔭 crows-nest: ready-PR pipeline started — review → address → re-validate → gated merge.

@@ -16,7 +17,7 @@ const openUrl = (url: string | null) => {

const UploadingBadge = () => (
<span className="text-sm font-medium px-3 py-1.5 rounded-full bg-white/10 text-white/80 border border-white/20 flex items-center gap-2">

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Migrated inline spinners now announce role="status"/aria-label="Loading" next to visible text

Both review lenses independently flagged this. Spinner (src/ui/src/components/ui/spinner.tsx) unconditionally renders role="status" aria-label="Loading" on the wrapped Loader2Icon. The prior bare <Loader2 .../> icons at every migrated callsite (this one, health-status.tsx, LLMProviderForm.tsx, OrchestratorBackendSetting.tsx, ShaveOutcomeView.tsx, UndoStagePanel.tsx, and WorkflowStepCard.tsx's retry button) were purely decorative with no ARIA semantics. After this change, each now introduces a new accessible live-region announcing "Loading" immediately beside text that already conveys the same state (e.g. "Uploading"), and if multiple LoadingState instances render at once (e.g. several WorkflowStepCards simultaneously in_progress), screen readers get several indistinguishable "Loading" announcements with no way to tell them apart.

This is arguably a net accessibility improvement over having no ARIA semantics at all, so it's not a regression worth blocking on — but it's a real, untested-for behavioral change the PR description doesn't call out (acceptance criteria only address visual/animate-spin parity). Worth a conscious decision: keep as-is (accept the duplication as an intentional improvement), or let LoadingState/Spinner accept an optional override/suppression for aria-label so inline decorative usages next to redundant text can opt out.

flagged by: code-review + codex-rescue

export function LoadingState({ className, inline = false }: LoadingStateProps = {}) {
if (inline) {
return <Spinner className={className} />;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] className + inline are two independent, uncoupled props — future misuse is possible

Nothing enforces that an inline-sized className (e.g. mr-2 h-4 w-4) is only ever paired with inline. A future caller could pass a small icon-sized className without inline and get an unwanted flex items-center justify-center py-8 block wrapper around a small icon, or pass inline with no className and get a bare unstyled spinner where block centering was actually wanted. All 8 current callsites in this PR pair them correctly, so this isn't an active bug — just a shape that relies on caller discipline rather than being structurally safe. A single prop (e.g. layout: "block" | "inline" combined with a size/variant prop) would make the misuse impossible by construction, if this component grows more callers over time.

flagged by: codex-rescue

@tomek-i

tomek-i commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

muster review — PR #986

Two independent lenses reviewed this diff in parallel: a conventions/correctness pass and a codex-rescue root-cause second opinion.

Verdict: no blocking or major findings — 2 nits. This is a clean, tightly-scoped refactor.

What both lenses verified independently

  • All 8 Loader2LoadingState callsites preserve identical visual output (sizing, margins, animate-spin) — Spinner's cn()/tailwind-merge correctly resolves the default size-4 animate-spin against each caller's classes.
  • WorkflowStepCard's STATUS_CONFIG.in_progress (icon: null, iconClass retained) is correctly consumed by the new early-return branch in StatusIcon — no stale/dead config, no interaction with the separate retry-button branch.
  • No stray Loader2 usages remain anywhere in src/ui outside ui/spinner.tsx (intentionally left as the low-level primitive).
  • Existing tests (WorkflowStepCard.test.tsx, UploadResult.test.tsx, ShaveOutcomeView.test.tsx) still pass against the new markup.
  • The 5 bare <LoadingState /> call sites (HomePage, ProjectsPage, ApprovalDialog, FinalResultPanel, PromptSelectionDialog) are confirmed zero-diff; the = {} default-parameter shape is safe with JSX's no-props call convention.
  • Scope is tight — no unrelated behavior changes.

Findings (posted inline)

Severity File Note
nit src/ui/src/components/video/UploadResult.tsx (representative of all 7 migrated inline callsites) Migrated spinners now carry role="status" aria-label="Loading" (from Spinner), which the prior bare Loader2 icons never had. This adds new, previously-absent live-region announcements next to text that already conveys loading state, and could produce duplicate/indistinguishable "Loading" announcements if multiple instances render at once (e.g. several WorkflowStepCards in_progress simultaneously). Likely a net accessibility improvement, but it's an unexamined side effect the PR's acceptance criteria (which only address visual parity) don't call out. Flagged independently by both lenses.
nit src/ui/src/components/common/LoadingState.tsx className + inline are two independent, uncoupled props with no structural guard against mismatched pairing (e.g. an inline-sized className without inline: true would force an unwanted block wrapper). Not an active bug today — all current callsites pair them correctly — but a single layout/size-shaped prop would make misuse impossible by construction if this component gains more callers.

One additional claim raised by the codex-rescue lens (that size-4 vs caller h-*/w-* merging in cn() was "accidental"/order-dependent rather than a guaranteed tailwind-merge contract) was checked against the pinned tailwind-merge@^3.3.1, which explicitly groups size-* and h-*/w-* as conflicting utilities by design — this is deterministic string-level dedup, not CSS-emission-order luck. That claim was not included as a finding.

Bottom line: 0 blocking, 0 major, 2 nits — no blocking issues, review advisory only. Looks mergeable as-is.


Posted by armada:muster — two-lens review (code-review + codex-rescue). This review does not approve, request changes, or merge; acting on findings is shipwright's job, gating the merge is crows-nest's.

@tomek-i tomek-i removed the armada:reviewing Claimed by crows-nest; review->merge pipeline running label Jul 22, 2026
@tomek-i

tomek-i commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

🔭 crows-nest: review complete — 0 blocking, 0 major, 2 advisory nits (both lenses agreed, no degradation). ✅ reviewed, no blocking findings — awaiting human merge (auto-merge off).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

armada Eligible for the ARMADA fleet to pick up

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace Loader2 icon usages with LoadingState component for consistent loading UI

2 participants