Repository navigation
feat: web ui polish - #125
Conversation
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
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 | 🟡 MinorPrevent list rendering while
isLoadingis true.Right now, rows can render at the same time as the loading skeleton when stale runs exist. Guard this block with
!isLoadingto 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 | 🟡 MinorComplete 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
tabIndexand 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:ReactNodeslots don't match the<p>wrappers.
label,value, andhintaccept 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 bindingsLine 50 and Line 51 use hard-coded IDs, which can collide if this component is rendered multiple times on one page. Prefer
useId()soaria-describedbytargets 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 assertionLine 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 fontsUsing
.ttffor 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.slugreset path, but the new validity guard inworkflow-spec-view.tsxis 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
⛔ Files ignored due to path filters (3)
.compozy/tasks/daemon-web-ui/audit_2026-04-23.mdis excluded by!**/*.mdbun.lockis excluded by!**/*.lockpackages/ui/package.jsonis excluded by!**/*.json
📒 Files selected for processing (54)
internal/api/httpapi/security_headers.gointernal/api/httpapi/server.gointernal/api/httpapi/static.gopackages/ui/src/components/alert.tsxpackages/ui/src/components/app-shell.tsxpackages/ui/src/components/button.tsxpackages/ui/src/components/empty-state.tsxpackages/ui/src/components/markdown.tsxpackages/ui/src/components/metric.tsxpackages/ui/src/components/section-heading.tsxpackages/ui/src/components/skeleton.tsxpackages/ui/src/components/status-badge.tsxpackages/ui/src/components/stories/surface-card.stories.tsxpackages/ui/src/components/surface-card.tsxpackages/ui/src/index.tspackages/ui/src/tokens.csspackages/ui/tests/primitives.test.tsxpackages/ui/tests/tokens.test.tsweb/src/main.tsxweb/src/routes/_app/index.tsxweb/src/routes/_app/memory_.$slug.tsxweb/src/routes/_app/reviews_.$slug.$round.$issueId.tsxweb/src/routes/_app/runs_.$runId.tsxweb/src/routes/_app/workflows_.$slug.spec.tsxweb/src/routes/_app/workflows_.$slug.tasks.tsxweb/src/routes/_app/workflows_.$slug.tasks_.$taskId.tsxweb/src/styles.cssweb/src/systems/app-shell/components/app-shell-layout.tsxweb/src/systems/app-shell/components/workspace-onboarding.tsxweb/src/systems/app-shell/components/workspace-picker.tsxweb/src/systems/dashboard/components/dashboard-view.test.tsxweb/src/systems/dashboard/components/dashboard-view.tsxweb/src/systems/dashboard/hooks/use-dashboard.tsweb/src/systems/memory/components/memory-index-view.tsxweb/src/systems/memory/components/workflow-memory-view.tsxweb/src/systems/reviews/components/review-detail-view.tsxweb/src/systems/reviews/components/reviews-index-view.tsxweb/src/systems/runs/components/run-detail-view.tsxweb/src/systems/runs/components/run-event-feed.tsxweb/src/systems/runs/components/runs-list-view.test.tsxweb/src/systems/runs/components/runs-list-view.tsxweb/src/systems/runs/hooks/use-run-event-feed.test.tsxweb/src/systems/runs/hooks/use-run-event-feed.tsweb/src/systems/runs/hooks/use-run-stream.test.tsxweb/src/systems/runs/hooks/use-run-stream.tsweb/src/systems/runs/hooks/use-runs.tsweb/src/systems/runs/index.tsweb/src/systems/runs/lib/event-store.tsweb/src/systems/spec/components/workflow-spec-view.test.tsxweb/src/systems/spec/components/workflow-spec-view.tsxweb/src/systems/workflows/components/task-board-view.tsxweb/src/systems/workflows/components/task-detail-view.tsxweb/src/systems/workflows/components/workflow-inventory-view.tsxweb/src/test/setup.ts
There was a problem hiding this comment.
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 torunStatusRetryingkeeps 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-srcdirective 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
⛔ Files ignored due to path filters (14)
.compozy/tasks/web-polish/reviews-001/_meta.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_001.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_002.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_003.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_004.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_005.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_006.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_007.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_008.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_009.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_010.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_011.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_012.mdis excluded by!**/*.md.compozy/tasks/web-polish/reviews-001/issue_013.mdis excluded by!**/*.md
📒 Files selected for processing (14)
internal/api/httpapi/security_headers.gointernal/api/httpapi/security_headers_test.gointernal/daemon/query_service.gopackages/ui/src/components/alert.tsxpackages/ui/src/components/markdown.tsxpackages/ui/src/components/metric.tsxpackages/ui/src/components/skeleton.tsxpackages/ui/src/components/status-badge.tsxpackages/ui/tests/primitives.test.tsxweb/src/systems/app-shell/components/workspace-onboarding.test.tsxweb/src/systems/app-shell/components/workspace-onboarding.tsxweb/src/systems/reviews/components/review-detail-view.test.tsxweb/src/systems/reviews/components/review-detail-view.tsxweb/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
| func TestSecurityHeadersMiddlewareSetsTightScriptCSP(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| gin.SetMode(gin.TestMode) | ||
|
|
There was a problem hiding this comment.
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.
| {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} |
There was a problem hiding this comment.
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.
| {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.
Summary by CodeRabbit
New Features
Bug Fixes
Improvements