Skip to content

feat: web ui polish - #125

Merged
pedronauck merged 5 commits into
mainfrom
feat/web-ui-polish
Apr 24, 2026
Merged

pedronauck merged 5 commits into
mainfrom
feat/web-ui-polish

Conversation

@pedronauck

@pedronauck pedronauck commented Apr 24, 2026 •

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Live run event feed, in-memory event store, and hook for realtime run monitoring; new UI primitives: Alert, EmptyState, Markdown, Metric, Skeleton, RunEventFeed.
    • Workflow filter in runs list; toast notifications added.
  • Bug Fixes

    • Improved security headers middleware.
    • Enhanced static asset caching with ETag/304 handling.
  • Improvements

    • Replaced many loading/error texts with skeletons and Alert/EmptyState components.
    • Button loading state, status badge pulse, accessibility and visual/token refinements.

compozybot and others added 3 commits April 23, 2026 15:57
Ships the CRITICAL + HIGH + selected MEDIUM fixes from the
2026-04-23 daemon web UI audit.

Phase 1 — Foundation:
- Drop `--spacing: 0.2rem` override in tokens.css (restores Tailwind scale).
- Raise `--card` contrast; darken `--background` for stronger surface layering.
- Point `--ring` at brand primary; unify sidebar primary with brand.
- Add `Alert`, `Skeleton`/`SkeletonRow`/`SkeletonText`, `Metric`, `Markdown`
  primitives to `@compozy/ui`.
- Mount `<Toaster>` (sonner) at the router root.

Phase 2 — Live runs:
- `run-event-store` + `use-run-event-feed` hook backing a bounded ring buffer
  via `useSyncExternalStore`.
- `RunEventFeed` renders per-event rows with kind tones, timestamps, and
  payload summaries.
- Route wires `onEvent` into the store; terminal run kinds invalidate the
  snapshot and runs list queries.

Phase 3 — Content polish:
- Swap `<pre>{markdown}</pre>` with hardened `<Markdown>` (react-markdown +
  rehype-sanitize + remark-gfm) in reviews, workflow memory, spec and task
  detail views.
- Replace inline alert `<p>` banners with `<Alert>` across dashboard, runs
  list, run detail, reviews and memory views.
- Runs list shows a `SkeletonRow` loading state, computed duration column
  and a workflow-slug filter derived from the current page.
- Dashboard replaces the scalar active-run count with a live
  `ActiveRunsCard` + queue chips; scalars become `<Metric>` tiles.
- `useDashboard` + `useRuns` refetch every 3s (foreground only).

Phase 4 — Go hardening:
- `securityHeadersMiddleware` installs CSP, X-Frame-Options, nosniff,
  Referrer-Policy, Permissions-Policy, and Cross-Origin-* defaults on every
  response.
- Static handler stamps strong SHA-256 ETags, serves `assets/*` with
  `Cache-Control: public, max-age=31536000, immutable`, and `no-cache` for
  everything else. Honors `If-None-Match` for 304s.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Stabilize run event feed lifecycle and terminal stream shutdown behavior.
Reset reused workflow/runs UI state correctly and restore accessible loading feedback.
Preserve exported spacing tokens while keeping the updated UI token palette and regression coverage.
- retune dashboard tokens, typography, surface depth, status badges, and shared empty/loading states

- polish dashboard and runs with health diagnostics, denser run rows, and a timeline-style event feed

- upgrade workflow, spec, memory, and review views with semantic states and document-reader surfaces
@pedronauck pedronauck self-assigned this Apr 24, 2026
@coderabbitai

coderabbitai Bot commented Apr 24, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds HTTP security headers middleware; implements asset ETag caching and conditional responses; expands UI design system with new primitives (Alert, Skeleton, EmptyState, Metric, Markdown) and token/font changes; introduces in-memory run event store, hooks, and UI feed; refactors many routes to use new components and polling behavior.

Changes

Cohort / File(s) Summary
Security & HTTP
internal/api/httpapi/security_headers.go, internal/api/httpapi/server.go, internal/api/httpapi/security_headers_test.go
New Gin middleware applying CSP and other security headers; integrated into server middleware chain; unit test verifying tight script-src CSP.
Static assets
internal/api/httpapi/static.go
Per-asset SHA-256 ETag computation/caching with concurrency protection; If-None-Match handling and 304 short-circuiting; immutable Cache-Control for /assets/ paths and no-cache elsewhere.
Run event feed infra
web/src/systems/runs/lib/event-store.ts, web/src/systems/runs/hooks/use-run-event-feed.ts, web/src/systems/runs/hooks/use-run-event-feed.test.tsx, web/src/systems/runs/hooks/use-run-stream.ts, web/src/systems/runs/hooks/use-run-stream.test.tsx
Adds in-memory RunEventStore (normalize/append/reset/subscribe), isTerminalKind helper, useRunEventFeed hook exposing append/reset, and stream behavior updates (manual close suppression, active guard); tests added for isolation and manual-close behavior.
Run feed UI & exports
web/src/systems/runs/components/run-event-feed.tsx, web/src/systems/runs/index.ts
New RunEventFeed component showing capped, reverse-chronological events with badges, timestamps, summaries and expandable JSON; module exports for feed and event-store utilities/types.
Polling behavior
web/src/systems/runs/hooks/use-runs.ts, web/src/systems/dashboard/hooks/use-dashboard.ts
Enables 3s refetch polling with background refetch disabled for runs and dashboard queries.
Design system: new primitives
packages/ui/src/components/alert.tsx, packages/ui/src/components/empty-state.tsx, packages/ui/src/components/metric.tsx, packages/ui/src/components/markdown.tsx, packages/ui/src/components/skeleton.tsx, packages/ui/src/index.ts
Introduces Alert, EmptyState, Metric, Markdown (GFM + sanitization), Skeleton variants; exports added from UI entrypoint.
Design system: tokens & fonts
packages/ui/src/tokens.css, packages/ui/src/styles.css
Replaces Playfair with self-hosted Nippo/Disket Mono fonts; revises color, surface, border, shadow tokens; adds skeleton shimmer and surface keyframes; updates eyebrow utility and global background gradient.
Component updates
packages/ui/src/components/button.tsx, packages/ui/src/components/app-shell.tsx, packages/ui/src/components/status-badge.tsx, packages/ui/src/components/section-heading.tsx, packages/ui/src/components/surface-card.tsx
Button: loading state, spinner, aria-busy, icon-only sizing; AppShell and SurfaceCard: layout/spacing/radius/shadow adjustments; StatusBadge: showDot/pulse props; SectionHeading: typography and min-width fixes.
Stories & tests (UI)
packages/ui/src/components/stories/surface-card.stories.tsx, packages/ui/tests/primitives.test.tsx, packages/ui/tests/tokens.test.ts
Story styling updates; tests extended to cover new primitives and token expectations (fonts, spacing, colors); button loading/alert/markdown/metric/skeleton assertions added.
App integration & routes
web/src/main.tsx, web/src/routes/_app/..., web/src/systems/... (many files, see summary)
Wires Sonner Toaster; replaces inline loading/error/empty UI across many routes with Skeleton/Alert/EmptyState/Markdown; adds RunEventFeed usage, live event handling, updated back-button styling, and various UI refactors across dashboard, runs, memory, reviews, workflows, spec, task board, workspace flows.
Tests & tooling
web/src/test/setup.ts, various new/updated tests under web/src/...
Mocks window.scrollTo in tests; many new/updated tests for onboarding accessibility, degraded health, run event feed behavior, rerender harnesses, and component integrations.
Daemon constants
internal/daemon/query_service.go
Replaces hard-coded lane/status strings with shared constants (e.g., runStatusPending, runStatusCompleted) used in ordering and title mapping.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client App
    participant Stream as useRunStream
    participant EventHook as useRunEventFeed
    participant Store as RunEventStore
    participant UI as RunEventFeed Component

    Client->>Stream: open subscription for runId
    Stream->>Stream: receive server event (raw)
    Stream->>EventHook: onEvent(raw)
    EventHook->>Store: append(eventId, raw)
    Store->>Store: normalize & cache event
    Store->>EventHook: notify subscribers / update snapshot
    EventHook->>UI: provide events snapshot
    UI->>UI: re-render feed list
    alt terminal event
        Stream->>EventHook: notify terminal
        EventHook->>Stream: close stream
        Stream->>Client: stop updates
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.01% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: web ui polish' is vague and generic, using a non-descriptive term ('polish') that fails to convey the specific changes made in this substantial changeset. Consider a more specific title that reflects the primary changes, such as 'feat: redesign dashboard and UI with new component system' or 'feat: introduce Alert, Skeleton, EmptyState, and Metric components with design tokens'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-ui-polish

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/src/systems/runs/components/runs-list-view.tsx (1)

167-176: ⚠️ Potential issue | 🟡 Minor

Prevent list rendering while isLoading is true.

Right now, rows can render at the same time as the loading skeleton when stale runs exist. Guard this block with !isLoading to avoid mixed loading/content states.

💡 Proposed fix
-      {visibleRuns.length > 0 ? (
+      {!isLoading && visibleRuns.length > 0 ? (
         <ul
           className="overflow-hidden rounded-[var(--radius-xl)] border border-border-subtle bg-card shadow-[var(--shadow-sm)]"
           data-testid="runs-list-items"
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/systems/runs/components/runs-list-view.tsx` around lines 167 - 176,
The runs list is rendering while loading, causing skeleton and rows to appear
together; wrap the existing block that checks visibleRuns.length and maps to
<RunRow> with a guard that ensures !isLoading (e.g., change the condition to
!isLoading && visibleRuns.length > 0) so the <ul data-testid="runs-list-items">
and its RunRow children are only rendered when loading is false.
web/src/systems/spec/components/workflow-spec-view.tsx (1)

79-91: ⚠️ Potential issue | 🟡 Minor

Complete the tabs keyboard behavior or keep these as plain buttons.

The new ARIA wiring makes this a tabs widget, but the interaction model is still missing: there is no roving tabIndex and no ArrowLeft/ArrowRight/Home/End handling. Keyboard users can still reach each button with Tab, but screen readers now get tab semantics without the expected tab behavior.

Also applies to: 108-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/systems/spec/components/workflow-spec-view.tsx` around lines 79 - 91,
The tab elements in workflow-spec-view.tsx were given ARIA tab semantics but
lack the expected keyboard model; either implement proper roving tabindex and
arrow/Home/End handling for the tab widget or remove tab semantics and keep them
as plain buttons. To fix: inside the WorkflowSpecView component (where the
mapped tab buttons are rendered) choose one approach — (A) implement a
roving-tabindex by tracking focused/active tab key in state (e.g., activeKey),
render each tab with role="tab", aria-selected={activeKey===tab.key},
tabIndex={activeKey===tab.key?0:-1}, keep a refs array for tabs, and add
onKeyDown on each tab to handle ArrowLeft/ArrowRight/Home/End to move focus and
update activeKey (and call whatever selection handler is used), or (B) remove
aria-controls/role/tab-specific attributes (aria-selected, role="tab") and keep
them as regular buttons so keyboard behavior remains standard; update id
attributes and data-testid accordingly to match the chosen approach.
🧹 Nitpick comments (8)
internal/api/httpapi/static.go (1)

122-136: Optional optimization: prevent duplicate hashing on concurrent cache misses.

Two simultaneous misses for the same asset can both compute SHA-256 before either writes the cache entry. A second check under write lock avoids redundant work.

♻️ Proposed refinement
 func (h *staticHandler) etagFor(assetPath string, data []byte) string {
 	h.etagMu.RLock()
 	if tag, ok := h.etags[assetPath]; ok {
 		h.etagMu.RUnlock()
 		return tag
 	}
 	h.etagMu.RUnlock()
 
 	sum := sha256.Sum256(data)
 	tag := fmt.Sprintf("%q", hex.EncodeToString(sum[:16]))
 
 	h.etagMu.Lock()
+	if existing, ok := h.etags[assetPath]; ok {
+		h.etagMu.Unlock()
+		return existing
+	}
 	h.etags[assetPath] = tag
 	h.etagMu.Unlock()
 	return tag
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/static.go` around lines 122 - 136, In etagFor, avoid
duplicated SHA-256 work on concurrent misses by performing a second existence
check after acquiring the write lock: keep the initial read lock check using
etagMu and etags; on miss, release the read lock and acquire etagMu.Lock(), then
check etags[assetPath] again and return it if present; only if still missing
compute sha256.Sum256(data), format the tag (hex.EncodeToString on sum[:16]),
store it in etags[assetPath], unlock etagMu, and return the tag—this implements
double-checked locking in etagFor to prevent redundant hashing.
packages/ui/src/components/metric.tsx (1)

5-10: ReactNode slots don't match the <p> wrappers.

label, value, and hint accept arbitrary JSX, but these slots are rendered inside <p> tags. Passing block content here will produce invalid nesting. Either narrow the props to text-like content or switch those wrappers to <div>/<span> so the public API matches what the component can safely render.

Also applies to: 30-35

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/metric.tsx` around lines 5 - 10, The MetricProps
declare label, value, and hint as ReactNode but the Metric component renders
them inside <p> elements, which causes invalid HTML if callers pass block-level
JSX; update the component so the public API and rendering match: either restrict
MetricProps.label/value/hint to text-like types (e.g., string | number |
React.ReactText) and validate/convert inputs, or change the JSX wrappers used in
the Metric component from <p> to inline/container-safe elements (e.g., <div> or
<span>) so arbitrary ReactNode content is valid; apply the same change for the
other occurrences referenced (the other label/value/hint usages around the
component render). Ensure you update the MetricProps type or the rendered tags
consistently and adjust any tests/styles relying on <p> behavior.
packages/ui/src/components/status-badge.tsx (1)

68-71: Respect reduced-motion for the pulse affordance.

The dot animation currently runs for everyone. In a shared status primitive, gate it with motion-safe: so users who prefer reduced motion still get the status color without the animation.

Suggested change
         <span
           aria-hidden="true"
           className={cn(
             "size-1.5 rounded-full bg-current",
             pulse &&
-              "animate-pulse shadow-[0_0_0_3px_color-mix(in_srgb,currentColor_18%,transparent)]"
+              "motion-safe:animate-pulse shadow-[0_0_0_3px_color-mix(in_srgb,currentColor_18%,transparent)]"
           )}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/status-badge.tsx` around lines 68 - 71, The status
dot currently always applies the pulse animation when pulse is true; update the
class composition in the StatusBadge component (the className usage where
"size-1.5 rounded-full bg-current" and the pulse conditional is applied) to gate
the animation with the Tailwind motion-safe prefix (e.g. use
"motion-safe:animate-pulse" and motion-safe for the shadow) so users with
reduced-motion preferences see the static dot while still receiving the color;
adjust the conditional that references pulse to only append the
motion-safe-prefixed animation/shadow classes when pulse is true.
web/src/systems/app-shell/components/workspace-onboarding.tsx (1)

50-51: Use unique IDs for assistive-text bindings

Line 50 and Line 51 use hard-coded IDs, which can collide if this component is rendered multiple times on one page. Prefer useId() so aria-describedby targets stay unique.

♻️ Suggested change
-import { useState, type ReactElement, type FormEvent } from "react";
+import { useId, useState, type ReactElement, type FormEvent } from "react";
@@
-  const inputDescriptionId = "workspace-onboarding-input-help";
-  const inputErrorId = "workspace-onboarding-input-error";
+  const baseId = useId();
+  const inputDescriptionId = `${baseId}-workspace-onboarding-input-help`;
+  const inputErrorId = `${baseId}-workspace-onboarding-input-error`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/systems/app-shell/components/workspace-onboarding.tsx` around lines
50 - 51, The component defines hard-coded IDs inputDescriptionId and
inputErrorId which can collide when multiple WorkspaceOnboarding instances
render; replace these with React's useId() to generate unique IDs (e.g., const
descriptionId = useId(); const errorId = useId()) and update all usages
(aria-describedby and any label/assistive-text references) to reference
descriptionId and errorId instead of the string literals so each instance gets
unique assistive-text bindings.
packages/ui/tests/primitives.test.tsx (1)

153-158: Tighten the disabled assertion

Line 157 checks a broad "disabled" substring. A more specific button-attribute assertion makes this test less brittle.

♻️ Suggested change
-    expect(html).toContain("disabled");
+    expect(html).toMatch(/<button[^>]*\sdisabled(\s|>|=)/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/tests/primitives.test.tsx` around lines 153 - 158, The test
"renders loading buttons as busy and disabled" uses renderToStaticMarkup(<Button
loading>...) and currently asserts a broad "disabled" substring; replace that
brittle check with a specific button-attribute check such as asserting the
rendered HTML contains the opening button tag with the disabled attribute (e.g.,
expect(html).toContain('<button disabled')) so the test verifies the disabled
attribute is present on the actual button element rendered by the Button
component.
packages/ui/src/tokens.css (1)

14-15: Prefer WOFF2 for shipped mono fonts

Using .ttf for web delivery is heavier; WOFF2 variants would reduce font payload and improve first render performance.

Also applies to: 22-23

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/tokens.css` around lines 14 - 15, Replace the .ttf font
delivery with WOFF2 in the `@font-face` src declarations: update the src entries
that currently reference "./assets/fonts/Disket-Mono-Regular.ttf" (and the
similar src at lines 22-23) to point to corresponding .woff2 files in
assets/fonts, keep a lightweight fallback chain if desired (e.g., woff2 then
woff), and ensure the new .woff2 files are added to the repo and referenced by
the same `@font-face` name so Disket-Mono loads via the smaller WOFF2 payload.
web/src/systems/workflows/components/workflow-inventory-view.tsx (1)

234-253: Extract duplicated workflow action-link classes into one constant.

Lines 234, 243, and 252 duplicate the same class string. Centralizing it will reduce drift and simplify future style changes.

♻️ Proposed refactor
 function WorkflowRow({
   workflow,
   onSync,
   onStartRun,
   onArchive,
   pendingSync,
   pendingStart,
   pendingArchive,
 }: {
@@
 }): ReactElement {
+  const workflowActionLinkClassName =
+    "inline-flex items-center justify-center gap-2 rounded-[var(--radius-md)] border border-border bg-[color:var(--surface-inset)] px-3 py-1.5 text-sm text-foreground transition-colors hover:border-border-strong hover:bg-surface-hover";
+
   return (
     <li>
       <SurfaceCard data-interactive="true" data-testid={`workflow-row-${workflow.slug}`}>
@@
           <Link
-            className="inline-flex items-center justify-center gap-2 rounded-[var(--radius-md)] border border-border bg-[color:var(--surface-inset)] px-3 py-1.5 text-sm text-foreground transition-colors hover:border-border-strong hover:bg-surface-hover"
+            className={workflowActionLinkClassName}
             data-testid={`workflow-view-board-${workflow.slug}`}
@@
           <Link
-            className="inline-flex items-center justify-center gap-2 rounded-[var(--radius-md)] border border-border bg-[color:var(--surface-inset)] px-3 py-1.5 text-sm text-foreground transition-colors hover:border-border-strong hover:bg-surface-hover"
+            className={workflowActionLinkClassName}
             data-testid={`workflow-view-spec-${workflow.slug}`}
@@
           <Link
-            className="inline-flex items-center justify-center gap-2 rounded-[var(--radius-md)] border border-border bg-[color:var(--surface-inset)] px-3 py-1.5 text-sm text-foreground transition-colors hover:border-border-strong hover:bg-surface-hover"
+            className={workflowActionLinkClassName}
             data-testid={`workflow-view-memory-${workflow.slug}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/systems/workflows/components/workflow-inventory-view.tsx` around
lines 234 - 253, Extract the repeated class string used on the three Link
components into a single constant (e.g., WORKFLOW_ACTION_LINK_CLASS) declared at
the top of the module and replace the className prop on the Link elements that
render the task board, spec, and memory buttons (the Links with data-testid
values workflow-view-board-{workflow.slug}, workflow-view-spec-{workflow.slug},
and workflow-view-memory-{workflow.slug}) to reference that constant; keep the
constant local to the file and ensure it preserves the exact class value so
behavior and tests remain unchanged.
web/src/systems/spec/components/workflow-spec-view.test.tsx (1)

162-182: Add a test for the “active tab disappears” fallback too.

This covers the workflow.slug reset path, but the new validity guard in workflow-spec-view.tsx is a separate branch. Please also rerender the same workflow after removing the active document and assert that the view falls back to the first present tab; that is the easier regression to miss here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/systems/spec/components/workflow-spec-view.test.tsx` around lines 162
- 182, Add a second assertion to cover the "active tab disappears" branch in
workflow-spec-view.tsx by rerendering the same workflow (keep workflow.id/slug
unchanged) but remove the currently active document(s) (e.g., set the previously
active collection to undefined/empty in the call to view.rerender after clicking
the ADR tab) and then assert the UI falls back to the first present tab (for
example expect the PRD or Techspec tab content to be displayed). Locate the test
using renderSpec in workflow-spec-view.test.tsx and update the test flow after
userEvent.click(screen.getByTestId("workflow-spec-tab-adrs")) to perform the
rerender-without-active-documents and the fallback assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/httpapi/security_headers.go`:
- Line 18: The CSP currently allows inline scripts via "script-src 'self'
'unsafe-inline'" which weakens XSS protection; locate the constant or function
that builds the Content-Security-Policy header (e.g., the variable/content
string named contentSecurityPolicy or the function that returns security headers
in security_headers.go) and remove 'unsafe-inline' from the script-src directive
so it reads only "script-src 'self'". If your app requires specific inline
scripts, replace unsafe-inline with per-response nonces or explicit script
hashes and ensure the code that injects headers (e.g., BuildSecurityHeaders or
GetSecurityHeaders) generates and inserts those nonces/hashes into both the CSP
and the script tags where used.

In `@packages/ui/src/components/alert.tsx`:
- Around line 42-44: The component currently sets resolvedRole = role ??
(variant === "error" ? "alert" : "status"), which makes non-error banners live
regions; change this so only error variant gets a live region: compute
resolvedRole = role ?? (variant === "error" ? "alert" : undefined) (or omit the
role attribute when undefined) so that provided role still wins, "error" results
in role="alert", and other variants do not default to role="status".

In `@packages/ui/src/components/markdown.tsx`:
- Line 21: The hover rule currently brightens links when the markdown container
is hovered because the class uses hover:[&_a]:brightness-110; update the
selector to target the anchor's own hover state by replacing
hover:[&_a]:brightness-110 with [&_a:hover]:brightness-110 in the class list
inside the Markdown component (look for the string array in
packages/ui/src/components/markdown.tsx that contains
"[&_a]:text-[color:var(--primary)] [&_a]:underline [&_a]:underline-offset-2
hover:[&_a]:brightness-110").

In `@packages/ui/src/components/skeleton.tsx`:
- Around line 28-29: Normalize the lines count before rendering in the Skeleton
component: compute a local variable (e.g., normalizedLines = Math.max(1, lines))
and use that for Array.from length and for the last-line check (compare index
=== normalizedLines - 1) instead of comparing against the raw lines prop so the
last-line width class is applied when lines <= 0.

In `@web/src/systems/reviews/components/review-detail-view.tsx`:
- Line 248: The StatusBadge is using resolveStatusTone (which handles issue
states) but must use the runs status resolver used for daemon runs; replace the
call to resolveStatusTone(run.status) with the runs resolver used in
task-detail-view.tsx (import that resolver the same way task-detail-view.tsx
does, e.g., resolveRunsStatusTone or the exact function name exported there) so
StatusBadge receives the correct tone for run.status; update the import at the
top and swap the function used in the JSX (StatusBadge tone={...}) accordingly.

---

Outside diff comments:
In `@web/src/systems/runs/components/runs-list-view.tsx`:
- Around line 167-176: The runs list is rendering while loading, causing
skeleton and rows to appear together; wrap the existing block that checks
visibleRuns.length and maps to <RunRow> with a guard that ensures !isLoading
(e.g., change the condition to !isLoading && visibleRuns.length > 0) so the <ul
data-testid="runs-list-items"> and its RunRow children are only rendered when
loading is false.

In `@web/src/systems/spec/components/workflow-spec-view.tsx`:
- Around line 79-91: The tab elements in workflow-spec-view.tsx were given ARIA
tab semantics but lack the expected keyboard model; either implement proper
roving tabindex and arrow/Home/End handling for the tab widget or remove tab
semantics and keep them as plain buttons. To fix: inside the WorkflowSpecView
component (where the mapped tab buttons are rendered) choose one approach — (A)
implement a roving-tabindex by tracking focused/active tab key in state (e.g.,
activeKey), render each tab with role="tab",
aria-selected={activeKey===tab.key}, tabIndex={activeKey===tab.key?0:-1}, keep a
refs array for tabs, and add onKeyDown on each tab to handle
ArrowLeft/ArrowRight/Home/End to move focus and update activeKey (and call
whatever selection handler is used), or (B) remove
aria-controls/role/tab-specific attributes (aria-selected, role="tab") and keep
them as regular buttons so keyboard behavior remains standard; update id
attributes and data-testid accordingly to match the chosen approach.

---

Nitpick comments:
In `@internal/api/httpapi/static.go`:
- Around line 122-136: In etagFor, avoid duplicated SHA-256 work on concurrent
misses by performing a second existence check after acquiring the write lock:
keep the initial read lock check using etagMu and etags; on miss, release the
read lock and acquire etagMu.Lock(), then check etags[assetPath] again and
return it if present; only if still missing compute sha256.Sum256(data), format
the tag (hex.EncodeToString on sum[:16]), store it in etags[assetPath], unlock
etagMu, and return the tag—this implements double-checked locking in etagFor to
prevent redundant hashing.

In `@packages/ui/src/components/metric.tsx`:
- Around line 5-10: The MetricProps declare label, value, and hint as ReactNode
but the Metric component renders them inside <p> elements, which causes invalid
HTML if callers pass block-level JSX; update the component so the public API and
rendering match: either restrict MetricProps.label/value/hint to text-like types
(e.g., string | number | React.ReactText) and validate/convert inputs, or change
the JSX wrappers used in the Metric component from <p> to inline/container-safe
elements (e.g., <div> or <span>) so arbitrary ReactNode content is valid; apply
the same change for the other occurrences referenced (the other label/value/hint
usages around the component render). Ensure you update the MetricProps type or
the rendered tags consistently and adjust any tests/styles relying on <p>
behavior.

In `@packages/ui/src/components/status-badge.tsx`:
- Around line 68-71: The status dot currently always applies the pulse animation
when pulse is true; update the class composition in the StatusBadge component
(the className usage where "size-1.5 rounded-full bg-current" and the pulse
conditional is applied) to gate the animation with the Tailwind motion-safe
prefix (e.g. use "motion-safe:animate-pulse" and motion-safe for the shadow) so
users with reduced-motion preferences see the static dot while still receiving
the color; adjust the conditional that references pulse to only append the
motion-safe-prefixed animation/shadow classes when pulse is true.

In `@packages/ui/src/tokens.css`:
- Around line 14-15: Replace the .ttf font delivery with WOFF2 in the `@font-face`
src declarations: update the src entries that currently reference
"./assets/fonts/Disket-Mono-Regular.ttf" (and the similar src at lines 22-23) to
point to corresponding .woff2 files in assets/fonts, keep a lightweight fallback
chain if desired (e.g., woff2 then woff), and ensure the new .woff2 files are
added to the repo and referenced by the same `@font-face` name so Disket-Mono
loads via the smaller WOFF2 payload.

In `@packages/ui/tests/primitives.test.tsx`:
- Around line 153-158: The test "renders loading buttons as busy and disabled"
uses renderToStaticMarkup(<Button loading>...) and currently asserts a broad
"disabled" substring; replace that brittle check with a specific
button-attribute check such as asserting the rendered HTML contains the opening
button tag with the disabled attribute (e.g., expect(html).toContain('<button
disabled')) so the test verifies the disabled attribute is present on the actual
button element rendered by the Button component.

In `@web/src/systems/app-shell/components/workspace-onboarding.tsx`:
- Around line 50-51: The component defines hard-coded IDs inputDescriptionId and
inputErrorId which can collide when multiple WorkspaceOnboarding instances
render; replace these with React's useId() to generate unique IDs (e.g., const
descriptionId = useId(); const errorId = useId()) and update all usages
(aria-describedby and any label/assistive-text references) to reference
descriptionId and errorId instead of the string literals so each instance gets
unique assistive-text bindings.

In `@web/src/systems/spec/components/workflow-spec-view.test.tsx`:
- Around line 162-182: Add a second assertion to cover the "active tab
disappears" branch in workflow-spec-view.tsx by rerendering the same workflow
(keep workflow.id/slug unchanged) but remove the currently active document(s)
(e.g., set the previously active collection to undefined/empty in the call to
view.rerender after clicking the ADR tab) and then assert the UI falls back to
the first present tab (for example expect the PRD or Techspec tab content to be
displayed). Locate the test using renderSpec in workflow-spec-view.test.tsx and
update the test flow after
userEvent.click(screen.getByTestId("workflow-spec-tab-adrs")) to perform the
rerender-without-active-documents and the fallback assertion.

In `@web/src/systems/workflows/components/workflow-inventory-view.tsx`:
- Around line 234-253: Extract the repeated class string used on the three Link
components into a single constant (e.g., WORKFLOW_ACTION_LINK_CLASS) declared at
the top of the module and replace the className prop on the Link elements that
render the task board, spec, and memory buttons (the Links with data-testid
values workflow-view-board-{workflow.slug}, workflow-view-spec-{workflow.slug},
and workflow-view-memory-{workflow.slug}) to reference that constant; keep the
constant local to the file and ensure it preserves the exact class value so
behavior and tests remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a276fbc0-572c-4a43-8a07-b1cc929fd153

📥 Commits

Reviewing files that changed from the base of the PR and between 9c359f1 and 968d669.

⛔ Files ignored due to path filters (3)
  • .compozy/tasks/daemon-web-ui/audit_2026-04-23.md is excluded by !**/*.md
  • bun.lock is excluded by !**/*.lock
  • packages/ui/package.json is excluded by !**/*.json
📒 Files selected for processing (54)
  • internal/api/httpapi/security_headers.go
  • internal/api/httpapi/server.go
  • internal/api/httpapi/static.go
  • packages/ui/src/components/alert.tsx
  • packages/ui/src/components/app-shell.tsx
  • packages/ui/src/components/button.tsx
  • packages/ui/src/components/empty-state.tsx
  • packages/ui/src/components/markdown.tsx
  • packages/ui/src/components/metric.tsx
  • packages/ui/src/components/section-heading.tsx
  • packages/ui/src/components/skeleton.tsx
  • packages/ui/src/components/status-badge.tsx
  • packages/ui/src/components/stories/surface-card.stories.tsx
  • packages/ui/src/components/surface-card.tsx
  • packages/ui/src/index.ts
  • packages/ui/src/tokens.css
  • packages/ui/tests/primitives.test.tsx
  • packages/ui/tests/tokens.test.ts
  • web/src/main.tsx
  • web/src/routes/_app/index.tsx
  • web/src/routes/_app/memory_.$slug.tsx
  • web/src/routes/_app/reviews_.$slug.$round.$issueId.tsx
  • web/src/routes/_app/runs_.$runId.tsx
  • web/src/routes/_app/workflows_.$slug.spec.tsx
  • web/src/routes/_app/workflows_.$slug.tasks.tsx
  • web/src/routes/_app/workflows_.$slug.tasks_.$taskId.tsx
  • web/src/styles.css
  • web/src/systems/app-shell/components/app-shell-layout.tsx
  • web/src/systems/app-shell/components/workspace-onboarding.tsx
  • web/src/systems/app-shell/components/workspace-picker.tsx
  • web/src/systems/dashboard/components/dashboard-view.test.tsx
  • web/src/systems/dashboard/components/dashboard-view.tsx
  • web/src/systems/dashboard/hooks/use-dashboard.ts
  • web/src/systems/memory/components/memory-index-view.tsx
  • web/src/systems/memory/components/workflow-memory-view.tsx
  • web/src/systems/reviews/components/review-detail-view.tsx
  • web/src/systems/reviews/components/reviews-index-view.tsx
  • web/src/systems/runs/components/run-detail-view.tsx
  • web/src/systems/runs/components/run-event-feed.tsx
  • web/src/systems/runs/components/runs-list-view.test.tsx
  • web/src/systems/runs/components/runs-list-view.tsx
  • web/src/systems/runs/hooks/use-run-event-feed.test.tsx
  • web/src/systems/runs/hooks/use-run-event-feed.ts
  • web/src/systems/runs/hooks/use-run-stream.test.tsx
  • web/src/systems/runs/hooks/use-run-stream.ts
  • web/src/systems/runs/hooks/use-runs.ts
  • web/src/systems/runs/index.ts
  • web/src/systems/runs/lib/event-store.ts
  • web/src/systems/spec/components/workflow-spec-view.test.tsx
  • web/src/systems/spec/components/workflow-spec-view.tsx
  • web/src/systems/workflows/components/task-board-view.tsx
  • web/src/systems/workflows/components/task-detail-view.tsx
  • web/src/systems/workflows/components/workflow-inventory-view.tsx
  • web/src/test/setup.ts

Comment thread internal/api/httpapi/security_headers.go Outdated
Comment thread packages/ui/src/components/alert.tsx Outdated
Comment thread packages/ui/src/components/markdown.tsx Outdated
Comment thread packages/ui/src/components/skeleton.tsx Outdated
Comment thread web/src/systems/reviews/components/review-detail-view.tsx Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/daemon/query_service.go (1)

739-739: Use constant for retrying in lane ordering too.

Line 739 still hard-codes "retrying" while neighboring statuses use constants. Swapping to runStatusRetrying keeps a single source of truth.

♻️ Suggested diff
-	for _, status := range []string{runStatusPending, "running", "retrying", runStatusCompleted, "failed", "canceled"} {
+	for _, status := range []string{runStatusPending, "running", runStatusRetrying, runStatusCompleted, "failed", "canceled"} {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/daemon/query_service.go` at line 739, The loop building the status
list uses a string literal "retrying" instead of the constant; update the slice
in the for loop that iterates over []string{runStatusPending, "running",
"retrying", runStatusCompleted, "failed", "canceled"} to use runStatusRetrying
so all statuses come from the same constants (e.g., replace the "retrying"
literal with runStatusRetrying in the status-slice used for lane ordering).
internal/api/httpapi/security_headers_test.go (1)

27-33: Make CSP assertion directive-aware instead of exact-substring based.

The current check can miss unsafe-inline if token order changes (or if extra sources are inserted). Assert against the parsed script-src directive itself.

🔍 Suggested hardening
 	csp := recorder.Header().Get("Content-Security-Policy")
-	if !strings.Contains(csp, "script-src 'self';") {
-		t.Fatalf("Content-Security-Policy = %q, want script-src 'self';", csp)
-	}
-	if strings.Contains(csp, "script-src 'self' 'unsafe-inline';") {
-		t.Fatalf("Content-Security-Policy = %q, want no unsafe-inline script directive", csp)
-	}
+	var scriptSrc string
+	for _, d := range strings.Split(csp, ";") {
+		d = strings.TrimSpace(d)
+		if strings.HasPrefix(d, "script-src ") {
+			scriptSrc = d
+			break
+		}
+	}
+	if scriptSrc == "" || !strings.Contains(scriptSrc, "'self'") {
+		t.Fatalf("Content-Security-Policy = %q, want script-src to include 'self'", csp)
+	}
+	if strings.Contains(scriptSrc, "'unsafe-inline'") {
+		t.Fatalf("Content-Security-Policy = %q, want script-src without unsafe-inline", csp)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/security_headers_test.go` around lines 27 - 33, The test
currently checks the CSP header via substring matching; instead parse the
Content-Security-Policy header from
recorder.Header().Get("Content-Security-Policy"), split it into directives by
';', locate the "script-src" directive, split that directive into source tokens,
and then assert that the token list contains "'self'" and does NOT contain
"'unsafe-inline'". Update the test around the csp variable to extract the
script-src directive, normalize/trimming whitespace, and perform membership
checks on the parsed tokens rather than substring matches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/api/httpapi/security_headers_test.go`:
- Around line 12-16: The test TestSecurityHeadersMiddlewareSetsTightScriptCSP
should not call t.Parallel() at the top while mutating global Gin state; wrap
the assertions in a t.Run("Should set tight script CSP", func(t *testing.T) {
... }) subtest, call gin.SetMode(gin.TestMode) inside that subtest before
invoking t.Parallel(), and then run the rest of the test there; update
references to TestSecurityHeadersMiddlewareSetsTightScriptCSP, gin.SetMode,
t.Run, and t.Parallel accordingly so the global mode is set before enabling
parallel execution for the subtest.

In `@packages/ui/src/components/alert.tsx`:
- Around line 54-59: The current truthy checks in the Alert component suppress
valid ReactNode values like 0; replace the boolean guards around title and
children with nullish checks (e.g., use title != null and children != null) so
the <p> for title and the children wrapper render when title/children are valid
non-null/undefined ReactNodes. Update the conditional expressions in the JSX
where title and children are referenced in packages/ui/src/components/alert.tsx
to use these nullish checks (refer to the title and children render blocks) to
ensure values like 0 are not omitted.

---

Nitpick comments:
In `@internal/api/httpapi/security_headers_test.go`:
- Around line 27-33: The test currently checks the CSP header via substring
matching; instead parse the Content-Security-Policy header from
recorder.Header().Get("Content-Security-Policy"), split it into directives by
';', locate the "script-src" directive, split that directive into source tokens,
and then assert that the token list contains "'self'" and does NOT contain
"'unsafe-inline'". Update the test around the csp variable to extract the
script-src directive, normalize/trimming whitespace, and perform membership
checks on the parsed tokens rather than substring matches.

In `@internal/daemon/query_service.go`:
- Line 739: The loop building the status list uses a string literal "retrying"
instead of the constant; update the slice in the for loop that iterates over
[]string{runStatusPending, "running", "retrying", runStatusCompleted, "failed",
"canceled"} to use runStatusRetrying so all statuses come from the same
constants (e.g., replace the "retrying" literal with runStatusRetrying in the
status-slice used for lane ordering).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6670f1fc-8922-4ce0-aade-28427f74ee1a

📥 Commits

Reviewing files that changed from the base of the PR and between 968d669 and b5c40d6.

⛔ Files ignored due to path filters (14)
  • .compozy/tasks/web-polish/reviews-001/_meta.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_001.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_002.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_003.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_004.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_005.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_006.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_007.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_008.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_009.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_010.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_011.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_012.md is excluded by !**/*.md
  • .compozy/tasks/web-polish/reviews-001/issue_013.md is excluded by !**/*.md
📒 Files selected for processing (14)
  • internal/api/httpapi/security_headers.go
  • internal/api/httpapi/security_headers_test.go
  • internal/daemon/query_service.go
  • packages/ui/src/components/alert.tsx
  • packages/ui/src/components/markdown.tsx
  • packages/ui/src/components/metric.tsx
  • packages/ui/src/components/skeleton.tsx
  • packages/ui/src/components/status-badge.tsx
  • packages/ui/tests/primitives.test.tsx
  • web/src/systems/app-shell/components/workspace-onboarding.test.tsx
  • web/src/systems/app-shell/components/workspace-onboarding.tsx
  • web/src/systems/reviews/components/review-detail-view.test.tsx
  • web/src/systems/reviews/components/review-detail-view.tsx
  • web/src/systems/spec/components/workflow-spec-view.test.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/ui/src/components/metric.tsx
  • packages/ui/src/components/status-badge.tsx
  • packages/ui/src/components/markdown.tsx
  • packages/ui/tests/primitives.test.tsx
  • web/src/systems/reviews/components/review-detail-view.tsx
  • web/src/systems/app-shell/components/workspace-onboarding.tsx

Comment on lines +12 to +16
func TestSecurityHeadersMiddlewareSetsTightScriptCSP(t *testing.T) {
t.Parallel()

gin.SetMode(gin.TestMode)

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.

⚠️ Potential issue | 🟠 Major

Avoid parallel execution while mutating global Gin mode, and wrap in a t.Run("Should...") subtest.

Line 13 marks the test parallel, but Line 15 mutates process-global state (gin.SetMode), which can race with other tests. Also, this test should follow the required subtest pattern.

✅ Suggested rewrite
 func TestSecurityHeadersMiddlewareSetsTightScriptCSP(t *testing.T) {
-	t.Parallel()
-
 	gin.SetMode(gin.TestMode)
-
-	engine := gin.New()
-	engine.Use(securityHeadersMiddleware())
-	engine.GET("/", func(c *gin.Context) {
-		c.Status(http.StatusNoContent)
-	})
-
-	request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
-	recorder := httptest.NewRecorder()
-	engine.ServeHTTP(recorder, request)
-
-	csp := recorder.Header().Get("Content-Security-Policy")
-	if !strings.Contains(csp, "script-src 'self';") {
-		t.Fatalf("Content-Security-Policy = %q, want script-src 'self';", csp)
-	}
-	if strings.Contains(csp, "script-src 'self' 'unsafe-inline';") {
-		t.Fatalf("Content-Security-Policy = %q, want no unsafe-inline script directive", csp)
-	}
+	t.Run("Should set tight script CSP", func(t *testing.T) {
+		engine := gin.New()
+		engine.Use(securityHeadersMiddleware())
+		engine.GET("/", func(c *gin.Context) {
+			c.Status(http.StatusNoContent)
+		})
+
+		request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
+		recorder := httptest.NewRecorder()
+		engine.ServeHTTP(recorder, request)
+
+		csp := recorder.Header().Get("Content-Security-Policy")
+		if !strings.Contains(csp, "script-src 'self';") {
+			t.Fatalf("Content-Security-Policy = %q, want script-src 'self';", csp)
+		}
+		if strings.Contains(csp, "script-src 'self' 'unsafe-inline';") {
+			t.Fatalf("Content-Security-Policy = %q, want no unsafe-inline script directive", csp)
+		}
+	})
 }

As per coding guidelines, MUST use t.Run("Should...") pattern for ALL test cases and Use t.Parallel() for independent subtests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/httpapi/security_headers_test.go` around lines 12 - 16, The test
TestSecurityHeadersMiddlewareSetsTightScriptCSP should not call t.Parallel() at
the top while mutating global Gin state; wrap the assertions in a t.Run("Should
set tight script CSP", func(t *testing.T) { ... }) subtest, call
gin.SetMode(gin.TestMode) inside that subtest before invoking t.Parallel(), and
then run the rest of the test there; update references to
TestSecurityHeadersMiddlewareSetsTightScriptCSP, gin.SetMode, t.Run, and
t.Parallel accordingly so the global mode is set before enabling parallel
execution for the subtest.

Comment on lines +54 to +59
{title ? <p className="font-semibold leading-5 text-current">{title}</p> : null}
{children ? (
<div className="text-sm leading-5 text-current [&_a]:underline [&_a]:underline-offset-2">
{children}
</div>
) : null}

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.

⚠️ Potential issue | 🟡 Minor

Use nullish checks for ReactNode rendering.

At Line 54 and Line 55, truthy checks can suppress valid values like 0. Prefer null/undefined checks so all valid nodes render.

💡 Proposed fix
-        {title ? <p className="font-semibold leading-5 text-current">{title}</p> : null}
-        {children ? (
+        {title != null ? <p className="font-semibold leading-5 text-current">{title}</p> : null}
+        {children != null ? (
           <div className="text-sm leading-5 text-current [&_a]:underline [&_a]:underline-offset-2">
             {children}
           </div>
         ) : null}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{title ? <p className="font-semibold leading-5 text-current">{title}</p> : null}
{children ? (
<div className="text-sm leading-5 text-current [&_a]:underline [&_a]:underline-offset-2">
{children}
</div>
) : null}
{title != null ? <p className="font-semibold leading-5 text-current">{title}</p> : null}
{children != null ? (
<div className="text-sm leading-5 text-current [&_a]:underline [&_a]:underline-offset-2">
{children}
</div>
) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/alert.tsx` around lines 54 - 59, The current
truthy checks in the Alert component suppress valid ReactNode values like 0;
replace the boolean guards around title and children with nullish checks (e.g.,
use title != null and children != null) so the <p> for title and the children
wrapper render when title/children are valid non-null/undefined ReactNodes.
Update the conditional expressions in the JSX where title and children are
referenced in packages/ui/src/components/alert.tsx to use these nullish checks
(refer to the title and children render blocks) to ensure values like 0 are not
omitted.

@pedronauck
pedronauck merged commit 4c756d1 into main Apr 24, 2026
8 of 9 checks passed
@pedronauck
pedronauck deleted the feat/web-ui-polish branch April 24, 2026 02:10
This was referenced Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants