Skip to content

feat: artifact and project management UI in web dashboard#954

Merged
Aureliolo merged 10 commits intomainfrom
feat/artifact-project-ui
Apr 1, 2026
Merged

feat: artifact and project management UI in web dashboard#954
Aureliolo merged 10 commits intomainfrom
feat/artifact-project-ui

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Add full browse/view/manage UI for artifacts and projects in the web dashboard
  • Backend: 2 new WS channels (artifacts, projects), 5 event types, best-effort publish on create/delete/upload mutations
  • Frontend: typed API endpoints, Zustand stores with polling + WebSocket bindings, 4 data hooks, route constants, sidebar navigation
  • 3 new shared UI components: MetadataGrid (detail page metadata), ProjectStatusBadge (semantic status colors), ContentTypeBadge (MIME type badge) -- all with Storybook stories
  • Artifacts: list page with search/type/agent/task filters, detail page with metadata grid, inline content preview (text via CodeMirror, images via blob URL), download, and delete with confirmation
  • Projects: list page with search/status filter and create button, create drawer (6 fields including TagInput for team), detail page with header, team section (Avatar grid with lead badge), and linked task list
  • Tests: 2 store tests, 4 page tests, 2 MSW handler files, test factories
  • Documentation: updated page-structure.md, brand-and-ux.md, web/CLAUDE.md with new pages, components, routes, WS channels

Test plan

  • uv run ruff check src/ tests/ -- all passed
  • uv run mypy src/ tests/ -- all passed
  • npm --prefix web run type-check -- all passed
  • npm --prefix web run lint -- zero warnings
  • npm --prefix web run test -- 194 files, 2326 tests passed
  • npm --prefix web run build -- production build succeeds
  • Visual verification at http://localhost:3000: list pages load with skeletons, filters work, detail views render, create form submits, download triggers, delete confirms

Pre-reviewed by 8 agents (code-reviewer, frontend-reviewer, api-contract-drift, silent-failure-hunter, issue-resolution-verifier, design-token-audit, test-quality-reviewer, docs-consistency), 17 findings addressed.

Closes #946

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6df9eab7-4c3e-413d-8196-b6cc3972a742

📥 Commits

Reviewing files that changed from the base of the PR and between 4ddb009 and ec3de9e.

📒 Files selected for processing (1)
  • web/src/pages/artifacts/ArtifactMetadata.tsx
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones, including StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, and TagInput
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors; NEVER hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens) for typography; NEVER set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders; NEVER hardcode values
Do NOT recreate status dots inline -- use
Do NOT build card-with-header layouts from scratch -- use
Do NOT create metric displays with text-metric font-bold -- use
Do NOT render initials circles manually -- use
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT use hardcoded Framer Motion transition durations -- use presets from @/lib/motion

web/src/**/*.{ts,tsx}: Apply eslint-web (zero warnings) via pre-commit hooks for the web dashboard (conditional on web/src/**/*.{ts,tsx})
Always reuse existing components from web/src/components/ui/ before creating new ones in the React dashboard
Never hardcode hex colors, font-famil...

Files:

  • web/src/pages/artifacts/ArtifactMetadata.tsx
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Web dashboard: see `web/CLAUDE.md` for commands, design system, and component inventory
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones, including StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, and TagInput

Applied to files:

  • web/src/pages/artifacts/ArtifactMetadata.tsx
🔇 Additional comments (2)
web/src/pages/artifacts/ArtifactMetadata.tsx (2)

4-8: Good design-system composition and styling discipline.

This implementation cleanly reuses shared UI primitives and semantic utility classes, with no hardcoded hex/font-family/pixel styling in the changed surface.

As per coding guidelines, "Always reuse existing components from web/src/components/ui/ before creating new ones in the React dashboard."

Also applies to: 65-97


75-96: Delete in-flight guard is correctly implemented.

Using disabled={deleting} on the Delete trigger and loading={deleting} on ConfirmDialog correctly prevents repeated destructive actions while deletion is pending.


Walkthrough

Adds backend WebSocket channels and event types for artifacts and projects and a request-scoped helper to publish WS events. Extends Artifact and CreateArtifactRequest models with project association, content type, and size. Controllers publish artifact/project WS events on create/delete/upload flows. Frontend: registers Projects and Artifacts routes and pages; adds many UI components (MetadataGrid, ProjectStatusBadge, ContentTypeBadge, cards, filters, skeletons), hooks (including reusable useDetailData, useArtifactsData, useProjectsData, detail hooks), Zustand stores, API endpoints, MSW mocks, file utilities (formatFileSize, downloadArtifactFile), and comprehensive tests. Sidebar, router routes, and shared API/types updated.

Suggested labels

autorelease: tagged

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.73% which is insufficient. The required threshold is 40.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: artifact and project management UI in web dashboard' accurately describes the primary change: adding UI for browsing and managing artifacts and projects.
Description check ✅ Passed The description provides a comprehensive overview of changes across backend, frontend, components, and tests, directly relating to the artifact/project UI implementation.
Linked Issues check ✅ Passed The PR fully addresses objectives from #946: projects list/detail/create UI, artifacts list/detail/download/delete UI, design system compliance, filters, WebSocket integration, and documentation updates.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives: WebSocket channels for artifacts/projects, new UI components (MetadataGrid, ProjectStatusBadge, ContentTypeBadge), hooks, stores, pages, routes, filters, and supporting utilities are all required for the artifact/project management feature.

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


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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA ec3de9e.
Ensure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.

Scanned Files

None

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the Projects and Artifacts dashboard modules, encompassing backend WebSocket event triggers, frontend API services, Zustand state management, and a suite of new UI components and pages. The changes include comprehensive unit tests and design documentation updates. Review feedback identified critical Python 3 syntax errors in exception handling within the controllers and suggested improvements for the file size formatting utility and artifact content preview logic to ensure robust error handling and accurate syntax highlighting.

event.model_dump_json(),
channels=[CHANNEL_ARTIFACTS],
)
except MemoryError, RecursionError:
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.

high

This line uses Python 2 syntax for catching multiple exceptions, which results in a SyntaxError in Python 3. Multiple exceptions must be enclosed in a tuple.

Suggested change
except MemoryError, RecursionError:
except (MemoryError, RecursionError):

event.model_dump_json(),
channels=[CHANNEL_PROJECTS],
)
except MemoryError, RecursionError:
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.

high

This line uses Python 2 syntax for catching multiple exceptions, which results in a SyntaxError in Python 3. Multiple exceptions must be enclosed in a tuple.

Suggested change
except MemoryError, RecursionError:
except (MemoryError, RecursionError):

if (!Number.isFinite(bytes) || bytes < 0) return '--'
if (bytes === 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
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.

medium

The exponent calculation can result in -1 if bytes is between 0 and 1 (e.g., 0.5), which would cause an out-of-bounds access on the units array. Clamping the exponent to a minimum of 0 ensures robustness for small fractional values.

Suggested change
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
const exponent = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1))

Comment on lines +16 to +21
function getLanguage(contentType: string): 'json' | 'yaml' {
if (contentType === 'application/json') return 'json'
if (contentType === 'application/x-yaml' || contentType === 'text/yaml') return 'yaml'
// Default to JSON for syntax highlighting even for plain text
return 'json'
}
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.

medium

Defaulting to json syntax highlighting for all non-YAML text files (like markdown or plain text) will cause the editor to display numerous syntax errors. It is better to return undefined for unsupported types to disable highlighting or add support for more languages like markdown.

Suggested change
function getLanguage(contentType: string): 'json' | 'yaml' {
if (contentType === 'application/json') return 'json'
if (contentType === 'application/x-yaml' || contentType === 'text/yaml') return 'yaml'
// Default to JSON for syntax highlighting even for plain text
return 'json'
}
function getLanguage(contentType: string): string | undefined {
const lower = contentType.toLowerCase()
if (lower === 'application/json') return 'json'
if (lower === 'application/x-yaml' || lower === 'text/yaml') return 'yaml'
if (lower === 'text/markdown') return 'markdown'
return undefined
}

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 65.38462% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.00%. Comparing base (146b782) to head (ec3de9e).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/synthorg/api/controllers/artifacts.py 38.88% 11 Missing ⚠️
src/synthorg/api/channels.py 63.15% 6 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #954      +/-   ##
==========================================
- Coverage   92.02%   92.00%   -0.02%     
==========================================
  Files         612      612              
  Lines       32984    33021      +37     
  Branches     3169     3170       +1     
==========================================
+ Hits        30352    30380      +28     
- Misses       2089     2097       +8     
- Partials      543      544       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 30

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/api/test_channels.py`:
- Around line 45-46: The test is reaching into internals
(plugin._arbitrary_channels_allowed and plugin._channels) which is fragile;
either assert the same behavior via a public API if available (e.g.,
plugin.is_channel_allowed(...) or plugin.allowed_channels property) or, if no
public accessor exists, keep the assertions but add a brief comment above the
asserts referencing why accessing _arbitrary_channels_allowed and _channels is
necessary for security verification and that they are intentionally relied upon
(include ALL_CHANNELS as the expected constant); update tests to prefer a public
method first and only fall back to the private attributes with the explanatory
comment.

In `@web/src/__tests__/pages/ArtifactDetailPage.test.tsx`:
- Around line 49-82: Add property-based tests using fast-check to cover state
transitions for ArtifactDetailPage by replacing or augmenting the example-based
tests that mutate hookReturn; use fc.assert and fc.property to generate
combinations of relevant state fields (e.g., artifact present vs null, loading
true/false, error string vs null) and call renderPage for each generated state,
then assert UI invariants like presence of 'artifact-detail-skeleton' when
loading & artifact null, 'Artifact not found.' when artifact null & not loading,
presence of 'artifact-metadata' and 'artifact-content-preview' when artifact is
set, and error banner text when error is set; reference the existing
helpers/variables hookReturn, defaultHookReturn, and renderPage to build the
generated states and reuse testIds/text checks (artifact-detail-skeleton,
artifact-metadata, artifact-content-preview, 'Artifact not found.', and error
string).

In `@web/src/__tests__/pages/ArtifactsPage.test.tsx`:
- Around line 46-86: Add property-based tests using fast-check in
web/src/__tests__/pages/ArtifactsPage.test.tsx to cover core invariants: use
fc.assert + fc.property to generate arbitrary valid permutations of the hook
state (use existing defaultHookReturn as a base and vary fields like loading,
error (string | undefined), totalArtifacts, artifacts, filteredArtifacts,
wsConnected) and call the existing renderPage helper for each generated state to
assert invariants such as "page heading 'Artifacts' is always present", "no
crash when error is any string and alert shows text when error defined", and
"skeleton is only shown when loading && totalArtifacts === 0". Reuse hookReturn
and renderPage names to inject the generated states and keep each property test
focused on one invariant (stable render, error-handling, loading transition).

In `@web/src/__tests__/pages/ProjectDetailPage.test.tsx`:
- Around line 52-86: Add a property-based test using fast-check that asserts
ProjectDetailPage behavior across generated combinations of the hook state (use
fc.assert(fc.property(...))). Generate booleans/nullable values for the relevant
pieces from this test file (loading, project nullable, and error string) and for
each generated state set hookReturn accordingly (referencing defaultHookReturn
and hookReturn) then call renderPage and assert the same invariants already
covered (skeleton when loading && project null, "Project not found." when
!loading && project null, error banner when error set, and presence of
header/team/task when project present). Ensure the new test imports fc and uses
fc.assert(fc.property(...)) so it follows the repository guideline for
web/src/**/*.test.{ts,tsx}.

In `@web/src/__tests__/stores/artifacts.test.ts`:
- Around line 104-112: The test currently calls
useArtifactsStore.getState().updateFromWsEvent({} as never) which hides the
expected event shape; replace the synthetic never payload with a realistic event
object that matches the handler's expected structure (e.g., include properties
like type/action/resource/id/timestamp or whatever updateFromWsEvent reads) so
the test documents the contract and will fail if the handler begins relying on
payload fields; keep listArtifacts mocked as before and call updateFromWsEvent
with that realistic event to ensure listArtifacts is still invoked.
- Around line 57-77: Add a test to verify partial-failure behavior in
fetchArtifactDetail: mock getArtifact to resolve with an artifact (use
makeArtifact) and mock getArtifactContentText to reject (e.g., new
Error('Content unavailable')), then call
useArtifactsStore.getState().fetchArtifactDetail('artifact-001') and assert that
useArtifactsStore.getState().selectedArtifact equals the artifact while
contentPreview is cleared/nullable (e.g., null) and detailError is not set to
the content error (or matches the expected fallback behavior); target the
fetchArtifactDetail flow and the mocks for getArtifact and
getArtifactContentText when adding this case.
- Around line 114-124: Replace or augment the existing unit tests for the filter
setters with property-based tests using fast-check: write
fc.assert(fc.property(fc.string(), (s) => {
useArtifactsStore.getState().setSearchQuery(s); return
useArtifactsStore.getState().searchQuery === s })) and similarly for
setTypeFilter (useArtifactsStore.getState().setTypeFilter) to verify arbitrary
strings (including empty, unicode, long) round-trip correctly; use fc.assert +
fc.property and import fast-check as fc, keeping the test descriptions around
"sets search query" and "sets type filter".

In `@web/src/__tests__/stores/projects.test.ts`:
- Around line 120-128: The test calls
useProjectsStore.getState().updateFromWsEvent which fires fetchProjects()
asynchronously, so the assertion on mocked listProjects may race; update the
test to wait for the async call to complete (e.g., use waitFor from
`@testing-library/react` or a small await flush-promises/nextTick) before
asserting listProjects was called. Locate the test around the updateFromWsEvent
spec and change the expect(listProjects).toHaveBeenCalled() to run inside a
waitFor or after awaiting a promise resolution to ensure fetchProjects has
executed.

In `@web/src/components/ui/metadata-grid.stories.tsx`:
- Around line 13-57: Add three exported Story objects—Hover, Loading, and
Error—next to the existing Default, TwoColumns, FourColumns, WithMonoValues, and
Empty exports; each should provide args that represent the required UI states
(e.g., Hover: include a representative items array and a hover/hovered boolean
arg to simulate the hover state; Loading: set isLoading: true and optionally no
items; Error: set an error or errorMessage string arg and optionally items
empty) and ensure the arg names match the actual MetadataGrid component props
(adjust hover/isLoading/error keys to the component's real prop names).

In `@web/src/components/ui/metadata-grid.tsx`:
- Around line 24-41: The component currently renders label/value pairs with
generic div/span markup which loses semantic relationship for screen readers;
change the outer container to a <dl> instead of the wrapping div, render each
pair as a wrapper element (e.g., a div) containing a <dt> for item.label and a
<dd> for item.value, preserve the existing styling by applying the same
className, colClass and per-item item.valueClassName to the corresponding
elements (keep the key on the pair using item.label), and ensure the
small-uppercase label classes are moved to the <dt> and the 'text-foreground'
plus item.valueClassName are applied to the <dd> so visual styling remains
identical while gaining proper metadata semantics.

In `@web/src/components/ui/project-status-badge.tsx`:
- Around line 34-40: Replace the inline status dot and label with the shared
StatusBadge component: remove the <span data-slot="status-dot" ... /> and the
conditional label span and instead render <StatusBadge> (pass the current status
prop and the label or showLabel flag as props so it displays the dot and label
consistently). Ensure any usage of DOT_COLOR_CLASSES and the local "status-dot"
slot is removed and that the component uses the shared StatusBadge API so
styling and behavior remain centralized.

In `@web/src/hooks/useArtifactDetailData.ts`:
- Around line 1-98: Both useArtifactDetailData and useProjectDetailData
duplicate the same polling, websocket debounce, bindings creation, and lifecycle
logic; extract a reusable hook (e.g., useDetailData or makeUseDetailData
factory) that accepts unique pieces (id, selectors/getters like
useArtifactsStore.getState().fetchArtifactDetail or
useProjectsStore.getState().fetchProjectDetail, store selectors for selected
item/content/loading/error, and DETAIL_CHANNELS) and internally re-uses
usePolling, useWebSocket, WS_DEBOUNCE_MS, DETAIL_POLL_INTERVAL,
artifactIdRef/pollFn/bindings creation and cleanup; then simplify
useArtifactDetailData and useProjectDetailData to call the generic hook with
their specific fetch function, selectors, and channel list.

In `@web/src/hooks/useProjectDetailData.ts`:
- Line 30: The parameter for useProjectDetailData is declared as projectId:
string but the function treats it as optional; update the signature to make
optionality explicit (e.g., change to projectId?: string or projectId: string |
null) and update any internal null/undefined checks to match (keep the existing
if (!projectId) guard). Also update call sites to pass possibly undefined/null
values or narrow before calling, and adjust any related types (e.g.,
UseProjectDetailDataReturn usages) if they rely on a non-optional projectId.

In `@web/src/mocks/handlers/artifacts.ts`:
- Around line 61-67: The GET artifact handler returns apiSuccess(null) with a
404 status which contradicts the HTTP status; update the
http.get('/api/v1/artifacts/:id') handler so when mockArtifacts.find(...) yields
no artifact it returns the project's error envelope (e.g., apiError or the
existing error helper) containing a clear "not found" message instead of
apiSuccess(null), still using HttpResponse.json(..., { status: 404 }); modify
the handler around the artifact lookup (mockArtifacts, params.id) to use that
error helper so the body’s success flag matches the 404 status.

In `@web/src/pages/artifacts/ArtifactCard.tsx`:
- Around line 15-37: ArtifactCard is building a custom "card-with-header" UI but
the codebase requires using the shared SectionCard primitive; replace the outer
Link/div composition in the ArtifactCard component with SectionCard (importing
SectionCard at top), move the artifact path into SectionCard's header area (or
header prop) and place the current body content (StatPill, ContentTypeBadge,
size/created at, and "by {artifact.created_by}") into SectionCard's children,
keeping the Link navigation (either make the SectionCard clickable or wrap the
internal content in the existing Link) and preserve use of formatLabel,
ContentTypeBadge, StatPill, formatFileSize and formatRelativeTime; remove the
handmade card CSS classes and ensure imports are updated (remove unused styles)
so the component relies on SectionCard for layout and styling.

In `@web/src/pages/artifacts/ArtifactContentPreview.tsx`:
- Around line 17-24: getLanguage currently maps only JSON/YAML and returns
'json' for everything else which mislabels types like text/markdown or
text/html; update the getLanguage(contentType: string) function to add mappings
for common types (e.g., 'text/markdown' -> 'markdown' or 'markdown' mode name
used by your editor, 'text/html' -> 'html', 'text/css' -> 'css', etc.) and use a
neutral fallback such as 'text' (or CodeMirror's equivalent) instead of
defaulting to 'json' so non-JSON content doesn't get JSON highlighting; ensure
you reference the same function name getLanguage and the contentType parameter
when locating where to add the extra content-type checks.
- Around line 75-77: The handler handleDownload is declared after the
component's early return so it's inaccessible in the "no content" branch and is
re-created on every render; move the function definition above the early returns
or wrap it in useCallback and define it before any return so it can be
referenced from both branches without being recreated each render — implement
useCallback(() => downloadArtifactFile(artifact.id,
artifact.path.split('/').pop() ?? artifact.id), [artifact.id, artifact.path])
(or equivalent) and replace the current handleDownload reference with this
memoized function.

In `@web/src/pages/artifacts/ArtifactFilters.tsx`:
- Around line 29-95: Replace the raw HTML inputs/selects in ArtifactFilters.tsx
with the dashboard's shared primitives InputField and SelectField (or add
compact variants there) so styling and behavior are centralized: for the search
box use InputField bound to searchQuery and setSearchQuery; for type/content
selects use SelectField bound to typeFilter/setTypeFilter and
contentTypeFilter/setContentTypeFilter and render options from
ARTIFACT_TYPE_VALUES and CONTENT_TYPE_OPTIONS (keep formatLabel for labels); for
createdBy/task/project filters use InputField bound to
createdByFilter/setCreatedByFilter, taskIdFilter/setTaskIdFilter, and
projectIdFilter/setProjectIdFilter. Ensure prop mapping matches the primitives'
API (value, onChange handlers that call the existing setters, placeholder,
aria-label) and remove the duplicated className styling so the shared components
control appearance.

In `@web/src/pages/artifacts/ArtifactGridView.tsx`:
- Line 23: The class on StaggerGroup uses hardcoded pixel breakpoints
(max-[1279px], max-[767px]); replace them with standard Tailwind breakpoint
tokens and a mobile-first pattern (e.g., set base grid-cols-1 then
sm:grid-cols-2 and lg:grid-cols-3 or whichever mapping fits the design) so
layout uses sm/md/lg/xl tokens instead of pixel utilities; update the className
on StaggerGroup to remove max-[1279px]:grid-cols-2 and max-[767px]:grid-cols-1
and use the appropriate tokenized classes (for example: "grid grid-cols-1
sm:grid-cols-2 lg:grid-cols-3 gap-grid-gap") to comply with the tokenized layout
rules.

In `@web/src/pages/projects/ProjectCard.tsx`:
- Around line 15-45: The file uses a bespoke Link-wrapped card shell in
ProjectCard; replace that with the shared SectionCard component so the layout
uses the canonical card-with-header pattern. Swap the outer Link block for
SectionCard (use SectionCard's prop that renders as a Link or wraps navigation),
move the project title + ProjectStatusBadge into the SectionCard header, put
project.description and the StatPill row into the card body, and move the
members/deadline row into the card footer; keep existing child components
(ProjectStatusBadge, StatPill, formatCurrency, formatRelativeTime) and the
routes/encoding used for navigation so behavior remains identical. Ensure
accessibility and existing classes/spacing are preserved via SectionCard props
rather than rebuilding the card markup manually.

In `@web/src/pages/projects/ProjectCreateDrawer.tsx`:
- Around line 100-108: The visible "Team Members" label is not associated with
the TagInput, causing loss of accessibility; remove the standalone <label> and
instead pass the label/aria-label prop into the TagInput (or provide an id prop
and ensure TagInput attaches it to its underlying input) so that TagInput
(component TagInput) receives the accessible label for form.team updates via
updateField; update the JSX to stop rendering the sibling label and supply the
text "Team Members" to TagInput (or set aria-labelledby/aria-label inside
TagInput) so assistive tech announces the field on focus.
- Around line 45-63: The budget validation in handleSubmit currently allows
non-finite values (e.g., Infinity from Number('1e309')) and the error text says
"positive" while 0 is accepted; update the validation to parse the budget into a
numeric value (const parsed = Number(form.budget)), require
Number.isFinite(parsed) and parsed >= 0, and set next.budget to a clearer
message like "Budget must be a non-negative finite number" when it fails; only
pass Number(form.budget) into createProject after this check so Infinity/NaN
never get serialized, and keep references to handleSubmit, form.budget,
setErrors, and useProjectsStore.getState().createProject to locate changes.

In `@web/src/pages/projects/ProjectFilters.tsx`:
- Around line 13-41: Replace the raw <input> and <select> with the shared
InputField and SelectField primitives: use InputField for the search box wired
to searchQuery and setSearchQuery (preserve placeholder and aria-label), and use
SelectField for the status dropdown wired to statusFilter and setStatusFilter
(preserve empty "All statuses" option and aria-label). For SelectField, provide
options derived from PROJECT_STATUS_VALUES mapped to { value: s, label:
formatLabel(s) } plus an empty value for clearing; ensure the onChange handler
converts empty string to null and validates values against
PROJECT_STATUS_VALUES/ProjectStatus before calling setStatusFilter. Keep
existing className/styling props and accessibility attributes when replacing the
components.

In `@web/src/pages/projects/ProjectGridView.tsx`:
- Line 23: The StaggerGroup element's className uses hardcoded pixel breakpoints
(max-[1279px], max-[767px]) — update the responsive classes to use standard
Tailwind breakpoint tokens (e.g., xl:, lg:, md:, sm:) so the grid uses
grid-cols-3 at large screens and falls back to grid-cols-2 / grid-cols-1 at the
recommended breakpoints; locate the StaggerGroup in ProjectGridView.tsx and
replace the custom max-[...] tokens with the appropriate Tailwind tokens to
match the intended breakpoints while preserving the grid and gap classes.

In `@web/src/pages/projects/ProjectTaskList.tsx`:
- Around line 29-41: The mapped JSX for each task is too large inline; extract
it into a new Presentational component named ProjectTaskRow that accepts a task
prop (type matching the task shape). Move the Link, TaskStatusIndicator, title
span, and the conditional assigned_to span into ProjectTaskRow and keep the same
className, ROUTES.TASK_DETAIL.replace(':taskId', encodeURIComponent(task.id))
usage and key semantics (ensure key remains on the mapped element or on the
returned <ProjectTaskRow /> in the map). Replace tasks.map((task) => ( ... ))
with tasks.map((task) => <ProjectTaskRow key={task.id} task={task} />) so
behavior and conditional rendering are preserved.

In `@web/src/pages/projects/ProjectTeamSection.tsx`:
- Around line 29-43: Extract the JSX inside project.team.map in
ProjectTeamSection.tsx into a new shared component (e.g., TeamMemberRow) to
comply with the rule against complex JSX in map callbacks: create a
TeamMemberRow component that accepts props (agentId: string, isLead: boolean)
and renders the Link using ROUTES.AGENT_DETAIL, Avatar, the agentId span, and
the conditional "Lead" badge based on isLead; then replace the inline map with
project.team.map(agentId => <TeamMemberRow key={agentId} agentId={agentId}
isLead={agentId === project.lead} />).

In `@web/src/stores/artifacts.ts`:
- Around line 1-9: Update the isPreviewableText function to treat YAML as
previewable by including its MIME type(s) — add checks for "application/yaml"
(and optionally "application/x-yaml") alongside the existing "text/*" and
"application/json" checks so YAML content can render inline previews (modify the
isPreviewableText helper).
- Around line 76-106: The artifact preview check in fetchArtifactDetail relies
on the truthiness of artifact.size_bytes; make it explicit by guarding against
null/undefined before comparing (e.g., check artifact.size_bytes != null &&
artifact.size_bytes > 0) so isPreviewableText and getArtifactContentText only
run when size_bytes is a valid number; update the condition that currently reads
artifact.content_type && artifact.size_bytes > 0 to explicitly test for
non-null/undefined size_bytes and then > 0 to improve readability and
correctness.

In `@web/src/stores/projects.ts`:
- Around line 63-96: The outer try/catch around Promise.allSettled in
fetchProjectDetail is unnecessary because Promise.allSettled never rejects;
remove the try/catch and simplify the flow to rely on projectResult.status and
tasksResult.status for error handling: keep the Promise.allSettled([...]) call,
check projectResult and tasksResult as you already do, ensure you call set({
detailLoading: false, detailError: ... }) on all early returns (when project is
null) and at the end when setting selectedProject/projectTasks, and use
getErrorMessage(projectResult.reason || tasksResult.reason) where appropriate;
reference fetchProjectDetail, projectResult, tasksResult, getErrorMessage and
the existing set(...) calls when making these edits.

In `@web/src/utils/download.ts`:
- Around line 11-20: The current flow creates an object URL with
URL.createObjectURL(blob) and a temporary anchor element (created via
document.createElement('a')) but only cleans them up on the happy path; wrap the
DOM append/click sequence in a try/finally so cleanup always runs: after
obtaining blob from downloadArtifactContent(artifactId) create url and element,
then in a try block append the anchor and call a.click(), and in the finally
block remove the anchor from document.body if it was appended and call
URL.revokeObjectURL(url); reference the downloadArtifactContent,
URL.createObjectURL, URL.revokeObjectURL and the temporary anchor element to
locate where to add the finally cleanup.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75fa963a-eaca-47dc-8bdc-5f4ce93d8cfa

📥 Commits

Reviewing files that changed from the base of the PR and between 020c610 and 87d5b82.

📒 Files selected for processing (62)
  • docs/design/brand-and-ux.md
  • docs/design/page-structure.md
  • src/synthorg/api/channels.py
  • src/synthorg/api/controllers/artifacts.py
  • src/synthorg/api/controllers/projects.py
  • src/synthorg/api/dto.py
  • src/synthorg/api/ws_models.py
  • src/synthorg/core/artifact.py
  • src/synthorg/core/project.py
  • tests/unit/api/test_channels.py
  • web/CLAUDE.md
  • web/src/__tests__/helpers/factories.ts
  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
  • web/src/__tests__/pages/ProjectsPage.test.tsx
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/__tests__/stores/projects.test.ts
  • web/src/api/endpoints/artifacts.ts
  • web/src/api/endpoints/projects.ts
  • web/src/api/types.ts
  • web/src/components/layout/Sidebar.tsx
  • web/src/components/ui/content-type-badge.stories.tsx
  • web/src/components/ui/content-type-badge.tsx
  • web/src/components/ui/metadata-grid.stories.tsx
  • web/src/components/ui/metadata-grid.tsx
  • web/src/components/ui/project-status-badge.stories.tsx
  • web/src/components/ui/project-status-badge.tsx
  • web/src/hooks/useArtifactDetailData.ts
  • web/src/hooks/useArtifactsData.ts
  • web/src/hooks/useProjectDetailData.ts
  • web/src/hooks/useProjectsData.ts
  • web/src/mocks/handlers/artifacts.ts
  • web/src/mocks/handlers/index.ts
  • web/src/mocks/handlers/projects.ts
  • web/src/pages/ArtifactDetailPage.tsx
  • web/src/pages/ArtifactsPage.tsx
  • web/src/pages/ProjectDetailPage.tsx
  • web/src/pages/ProjectsPage.tsx
  • web/src/pages/artifacts/ArtifactCard.tsx
  • web/src/pages/artifacts/ArtifactContentPreview.tsx
  • web/src/pages/artifacts/ArtifactDetailSkeleton.tsx
  • web/src/pages/artifacts/ArtifactFilters.tsx
  • web/src/pages/artifacts/ArtifactGridView.tsx
  • web/src/pages/artifacts/ArtifactMetadata.tsx
  • web/src/pages/artifacts/ArtifactsSkeleton.tsx
  • web/src/pages/projects/ProjectCard.tsx
  • web/src/pages/projects/ProjectCreateDrawer.stories.tsx
  • web/src/pages/projects/ProjectCreateDrawer.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
  • web/src/pages/projects/ProjectFilters.tsx
  • web/src/pages/projects/ProjectGridView.tsx
  • web/src/pages/projects/ProjectHeader.tsx
  • web/src/pages/projects/ProjectTaskList.tsx
  • web/src/pages/projects/ProjectTeamSection.tsx
  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/router/index.tsx
  • web/src/router/routes.ts
  • web/src/stores/artifacts.ts
  • web/src/stores/projects.ts
  • web/src/utils/download.ts
  • web/src/utils/format.ts

Aureliolo and others added 5 commits April 1, 2026 00:12
Add full browse/view/manage UI for artifacts and projects:

Backend: WS channels (artifacts, projects), 5 event types, publish
on create/delete/upload mutations in artifact and project controllers.

Frontend: typed API endpoints, Zustand stores with polling + WebSocket
bindings, data hooks, route constants, sidebar navigation.

New shared components: MetadataGrid (detail page metadata display),
ProjectStatusBadge (semantic status colors), ContentTypeBadge (MIME
type display) -- all with Storybook stories.

Artifacts: list page with search/type filter, detail page with
metadata grid, inline content preview (text via CodeMirror, images
via blob URL), download, and delete with confirmation.

Projects: list page with search/status filter and create button,
create drawer (6 fields including TagInput for team), detail page
with header, team section (Avatar grid with lead badge), and linked
task list.

Tests: store tests, page render tests, MSW handlers, test factories.

Closes #946

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move Request import from TYPE_CHECKING guard to runtime import in
artifact and project controllers. PEP 649 lazy annotations still
require runtime availability for Litestar's dependency injection
to resolve handler parameters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-reviewed by 8 agents, 17 findings addressed:

- Add exc_info=True to WS publish error logging (artifacts, projects)
- Fix blob URL memory leak in ArtifactContentPreview (ref-based cleanup)
- Surface image preview errors to user instead of silent catch
- Add partial failure reporting for content preview fetch in store
- Add agent/task filter inputs to ArtifactFilters UI
- Add INFO log after project creation (PERSISTENCE_PROJECT_SAVED)
- Optimistically add project to list on create for immediate UI update
- Fix Artifact.created_at nullability (string | null) to match backend
- Fix CreateProjectRequest.lead nullability (string | null) to match backend
- Fix hardcoded p-3 to p-card in ProjectCreateDrawer error banner
- Update page-structure.md: 9 WS channels, remove deferred references,
  add Projects/Artifacts to nav hierarchy, URL routing, WS subscription map
- Update web/CLAUDE.md: add 3 new components to reuse table, update
  hooks/stores descriptions
- Update brand-and-ux.md: add components, formatFileSize, types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Security: remove image/svg+xml from safe content types (XSS risk),
  exclude SVG from image preview rendering
- Backend: add project_id field to Artifact model and CreateArtifactRequest,
  extract shared publish_ws_event helper into channels.py, extract
  _save_metadata_with_rollback helper for upload_content, fix
  ProjectStatusFilter to use NotBlankStr, fix lead null coercion in
  WS payload, add Final annotation to ALL_CHANNELS
- Frontend: add content type and project filters to artifacts list page,
  extract shared downloadArtifactFile utility, add image loading skeleton,
  include error context in partial failure messages, remove fire-and-forget
  background refresh in createProject
- Types: fix WsEvent.payload to dict[str, object], add reserved comment
  to PROJECT_STATUS_CHANGED
- Docs: add Projects and Artifacts page description sections to
  page-structure.md, fix controller count 20->22, fix budget docstring
  USD->EUR, add missing Artifact docstring attributes
- Tests: strengthen test_create_channels_plugin assertions, add
  deleteArtifact error propagation test, add updateFromWsEvent tests,
  add createProject optimistic state verification
- Comments: fix CRUD docstrings to match actual operations, add ESLint
  disable justifications

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Semantics: MetadataGrid uses dl/dt/dd instead of generic div/span
- Accessibility: ProjectCreateDrawer team field uses role="group" with
  aria-label, budget validation rejects Infinity/NaN
- Tests: add fast-check property-based tests to ArtifactDetailPage,
  ArtifactsPage, and ProjectDetailPage; add partial-failure test and
  property-based filter tests to artifacts store; fix updateFromWsEvent
  race in projects store with waitFor; use realistic WsEvent payloads
  instead of {} as never
- Hooks: extract shared useDetailData factory hook to deduplicate
  useArtifactDetailData and useProjectDetailData; make projectId
  parameter optional (string | undefined)
- Components: extract ProjectTaskRow and TeamMemberRow from map callbacks
  in ProjectTaskList and ProjectTeamSection
- Layout: replace hardcoded pixel breakpoints (max-[1279px], max-[767px])
  with standard Tailwind tokens (sm:grid-cols-2 xl:grid-cols-3) in both
  grid views
- Stores: add YAML to isPreviewableText, explicit size_bytes null check,
  remove unnecessary try/catch around Promise.allSettled in projects store
- Fixes: download utility uses try/finally for DOM cleanup, MSW artifact
  404 handler uses apiError instead of apiSuccess(null),
  handleDownload moved before early returns with useCallback,
  test_channels private attr access documented with comment
- Stories: add SingleItem and ManyItems stories to MetadataGrid

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

♻️ Duplicate comments (2)
web/src/components/ui/metadata-grid.stories.tsx (1)

13-80: 🧹 Nitpick | 🔵 Trivial

Add missing Hover, Loading, and Error story states.

The story file covers Default, variants, and Empty, but is missing three required states per coding guidelines. Even for purely presentational components, these stories help document all possible visual states.

🧩 Example additions
 export const Empty: Story = {
   args: { items: [] },
 }
+
+export const Hover: Story = {
+  parameters: { pseudo: { hover: true } },
+  args: Default.args,
+}
+
+export const Loading: Story = {
+  args: {
+    items: [
+      { label: 'Type', value: '—' },
+      { label: 'Status', value: '—' },
+    ],
+  },
+}
+
+export const Error: Story = {
+  args: {
+    items: [{ label: 'Error', value: 'Failed to load metadata' }],
+  },
+}

As per coding guidelines, "Storybook stories (.stories.tsx files) must include all component states: default, hover, loading, error, empty."

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

In `@web/src/components/ui/metadata-grid.stories.tsx` around lines 13 - 80, Add
three stories named Hover, Loading, and Error alongside the existing exports
(Default, TwoColumns, FourColumns, WithMonoValues, SingleItem, ManyItems,
Empty). For Loading export a Story that sets a loading prop (e.g., args: {
isLoading: true } or items: undefined) to show the spinner/placeholder state;
for Error export a Story with an error prop (e.g., args: { error: 'Failed to
load' }) to render the component's error UI; for Hover export a Story that
provides normal args (reuse Default.items) and a play function that simulates a
hover over the first metadata item (use testing-library userEvent.hover on the
component's root or first label) so the hover visual state is captured. Ensure
the new exports are named exactly Hover, Loading, and Error to match storybook
expectations.
web/src/pages/projects/ProjectCreateDrawer.tsx (1)

103-111: ⚠️ Potential issue | 🟠 Major

Associate “Team Members” with the actual TagInput control, not only the group.

The visible text is not programmatically bound to the input rendered by TagInput in this usage. Please pass an explicit input label (label / aria-label / aria-labelledby, depending on TagInput API) so assistive tech announces the field name on focus.

#!/bin/bash
# Verify TagInput supports explicit labeling props and how they map to the underlying input.
fd -i 'tag-input.tsx' web/src/components/ui --exec cat
rg -n -C3 'interface .*TagInput|type .*TagInput|label|aria-label|aria-labelledby|htmlFor|id=' web/src/components/ui/tag-input.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/projects/ProjectCreateDrawer.tsx` around lines 103 - 111, The
visible "Team Members" text is not programmatically bound to the TagInput
control, so assistive tech won't announce it; update the TagInput usage in
ProjectCreateDrawer (the TagInput rendering that uses value={form.team} and
onChange={updateField}) to pass an explicit accessible label—prefer
aria-label="Team Members" (or label/aria-labelledby if TagInput's API expects
those) so the input is announced on focus; if TagInput requires an id/label
pairing, add a matching id to the underlying input via TagInput props and
reference it with aria-labelledby.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/synthorg/api/channels.py`:
- Around line 101-103: The except Exception branch in
src/synthorg/api/channels.py currently re-raises without logging; update the
generic except block (adjacent to the existing "except MemoryError,
RecursionError: raise" clause) to log a structured error before re-raising —
call logger.error or logger.exception with contextual fields (e.g., channel id,
message/payload, function name) and include the traceback (exc_info=True) so
observability is preserved, then re-raise the exception; do not change the
MemoryError/RecursionError behavior.

In `@src/synthorg/api/controllers/artifacts.py`:
- Around line 92-127: The helper _save_metadata_with_rollback currently types
repo and storage as Any; define small Protocols (e.g., ArtifactRepository with
async def save(self, artifact: Artifact) -> None and StorageBackend with async
def delete(self, artifact_id: str) -> None) or use existing repository/storage
interfaces, import typing.Protocol, then change the function signature to repo:
ArtifactRepository and storage: StorageBackend (and add any necessary
imports/exports); update tests/type hints as needed so repo.save and
storage.delete are properly typed.

In `@src/synthorg/api/controllers/projects.py`:
- Around line 156-166: After saving the project (repo.save(project)), make the
WebSocket publish a best-effort operation by wrapping the call to
publish_ws_event(...) in a try/except that catches Exception, logs the error
(including context: project.id and event type) and does not re-raise so the HTTP
response remains successful; locate the publish_ws_event call and modify it
accordingly (e.g., try: publish_ws_event(...) except Exception as e:
logger.error(...)) to ensure failures in publish_ws_event do not break the API
flow.

In `@web/src/components/ui/metadata-grid.tsx`:
- Line 34: The dt element in metadata-grid.tsx uses a raw utility class
"text-[10px]" which bypasses the project's typography tokens; replace that
ad-hoc size with the appropriate design-token class used across shared UI (i.e.,
swap "text-[10px]" in the dt className to the project tokenized size class used
for small/caption text in shared components—use the existing token (e.g., the
project's small/caption token) so the metadata-grid component conforms to the
tokenized typography system.

In `@web/src/pages/projects/ProjectDetailSkeleton.tsx`:
- Line 8: The className currently hardcodes a pixel breakpoint ("grid
grid-cols-2 gap-grid-gap max-[1023px]:grid-cols-1"); replace the arbitrary
max-[1023px] utility with a standard Tailwind breakpoint by making the default
single-column and enabling two columns at the lg breakpoint (e.g., change to
"grid grid-cols-1 gap-grid-gap lg:grid-cols-2") so the layout uses the
design-system breakpoints; update the JSX in ProjectDetailSkeleton's div
containing the grid accordingly.

In `@web/src/pages/projects/ProjectHeader.tsx`:
- Around line 18-20: The Budget field in ProjectHeader currently treats zero as
missing by using project.budget > 0; change the condition to check for
undefined/null instead so a legitimate 0 displays: update the rendering logic
that uses project.budget (and the value expression that calls formatCurrency) to
show formatCurrency(project.budget) when project.budget is defined (or non-null)
and only show '--' when project.budget is null/undefined, leaving formatCurrency
and the label 'Budget' intact.

In `@web/src/pages/projects/ProjectsSkeleton.tsx`:
- Line 11: The responsive grid in ProjectsSkeleton.tsx uses hardcoded pixel
max-queries (max-[1279px], max-[767px]); replace them with standard Tailwind
breakpoints by switching the class on the grid div (the <div className="grid
...">) to use breakpoint modifiers such as "grid-cols-1 md:grid-cols-2
xl:grid-cols-3 gap-grid-gap" (or equivalent order you prefer) so the layout uses
md and xl breakpoints instead of the custom max-[] values.

In `@web/src/stores/artifacts.ts`:
- Around line 138-141: The updateFromWsEvent handler currently declares an
unused event parameter; update the signature for updateFromWsEvent to either
remove the unused event parameter or rename it to _event to indicate intentional
non-use, and ensure any references to
useArtifactsStore.getState().fetchArtifacts() remain unchanged; apply the same
convention as used in the projects store so the pattern is consistent across
stores.

In `@web/src/stores/projects.ts`:
- Around line 119-121: The implementation of updateFromWsEvent currently ignores
the declared event: WsEvent parameter; either consume the event to decide when
to refetch (e.g., inspect event.type or event.resource and call
useProjectsStore.getState().fetchProjects() only for relevant event types) or
update the declaration/implementation to remove the unused parameter and/or add
a clear comment explaining that all events intentionally trigger a full refetch;
make the change in the updateFromWsEvent function to keep signature and behavior
consistent with the interface and to avoid unused-parameter warnings.

---

Duplicate comments:
In `@web/src/components/ui/metadata-grid.stories.tsx`:
- Around line 13-80: Add three stories named Hover, Loading, and Error alongside
the existing exports (Default, TwoColumns, FourColumns, WithMonoValues,
SingleItem, ManyItems, Empty). For Loading export a Story that sets a loading
prop (e.g., args: { isLoading: true } or items: undefined) to show the
spinner/placeholder state; for Error export a Story with an error prop (e.g.,
args: { error: 'Failed to load' }) to render the component's error UI; for Hover
export a Story that provides normal args (reuse Default.items) and a play
function that simulates a hover over the first metadata item (use
testing-library userEvent.hover on the component's root or first label) so the
hover visual state is captured. Ensure the new exports are named exactly Hover,
Loading, and Error to match storybook expectations.

In `@web/src/pages/projects/ProjectCreateDrawer.tsx`:
- Around line 103-111: The visible "Team Members" text is not programmatically
bound to the TagInput control, so assistive tech won't announce it; update the
TagInput usage in ProjectCreateDrawer (the TagInput rendering that uses
value={form.team} and onChange={updateField}) to pass an explicit accessible
label—prefer aria-label="Team Members" (or label/aria-labelledby if TagInput's
API expects those) so the input is announced on focus; if TagInput requires an
id/label pairing, add a matching id to the underlying input via TagInput props
and reference it with aria-labelledby.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b0ec860-3a5c-4c1e-9fb2-9aafc6318bd6

📥 Commits

Reviewing files that changed from the base of the PR and between 87d5b82 and 5440f22.

📒 Files selected for processing (63)
  • docs/design/brand-and-ux.md
  • docs/design/page-structure.md
  • src/synthorg/api/channels.py
  • src/synthorg/api/controllers/artifacts.py
  • src/synthorg/api/controllers/projects.py
  • src/synthorg/api/dto.py
  • src/synthorg/api/ws_models.py
  • src/synthorg/core/artifact.py
  • src/synthorg/core/project.py
  • tests/unit/api/test_channels.py
  • web/CLAUDE.md
  • web/src/__tests__/helpers/factories.ts
  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
  • web/src/__tests__/pages/ProjectsPage.test.tsx
  • web/src/__tests__/stores/artifacts.test.ts
  • web/src/__tests__/stores/projects.test.ts
  • web/src/api/endpoints/artifacts.ts
  • web/src/api/endpoints/projects.ts
  • web/src/api/types.ts
  • web/src/components/layout/Sidebar.tsx
  • web/src/components/ui/content-type-badge.stories.tsx
  • web/src/components/ui/content-type-badge.tsx
  • web/src/components/ui/metadata-grid.stories.tsx
  • web/src/components/ui/metadata-grid.tsx
  • web/src/components/ui/project-status-badge.stories.tsx
  • web/src/components/ui/project-status-badge.tsx
  • web/src/hooks/useArtifactDetailData.ts
  • web/src/hooks/useArtifactsData.ts
  • web/src/hooks/useDetailData.ts
  • web/src/hooks/useProjectDetailData.ts
  • web/src/hooks/useProjectsData.ts
  • web/src/mocks/handlers/artifacts.ts
  • web/src/mocks/handlers/index.ts
  • web/src/mocks/handlers/projects.ts
  • web/src/pages/ArtifactDetailPage.tsx
  • web/src/pages/ArtifactsPage.tsx
  • web/src/pages/ProjectDetailPage.tsx
  • web/src/pages/ProjectsPage.tsx
  • web/src/pages/artifacts/ArtifactCard.tsx
  • web/src/pages/artifacts/ArtifactContentPreview.tsx
  • web/src/pages/artifacts/ArtifactDetailSkeleton.tsx
  • web/src/pages/artifacts/ArtifactFilters.tsx
  • web/src/pages/artifacts/ArtifactGridView.tsx
  • web/src/pages/artifacts/ArtifactMetadata.tsx
  • web/src/pages/artifacts/ArtifactsSkeleton.tsx
  • web/src/pages/projects/ProjectCard.tsx
  • web/src/pages/projects/ProjectCreateDrawer.stories.tsx
  • web/src/pages/projects/ProjectCreateDrawer.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
  • web/src/pages/projects/ProjectFilters.tsx
  • web/src/pages/projects/ProjectGridView.tsx
  • web/src/pages/projects/ProjectHeader.tsx
  • web/src/pages/projects/ProjectTaskList.tsx
  • web/src/pages/projects/ProjectTeamSection.tsx
  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/router/index.tsx
  • web/src/router/routes.ts
  • web/src/stores/artifacts.ts
  • web/src/stores/projects.ts
  • web/src/utils/download.ts
  • web/src/utils/format.ts

Comment on lines +101 to +103
except MemoryError, RecursionError:
raise
except Exception:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Log fatal publish failures before re-raising.

This branch re-raises without context, which drops observability for the most severe WS publish failures. Add a structured logger.error(...) before raise.

As per coding guidelines, "All error paths must log at WARNING or ERROR with context before raising."

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

In `@src/synthorg/api/channels.py` around lines 101 - 103, The except Exception
branch in src/synthorg/api/channels.py currently re-raises without logging;
update the generic except block (adjacent to the existing "except MemoryError,
RecursionError: raise" clause) to log a structured error before re-raising —
call logger.error or logger.exception with contextual fields (e.g., channel id,
message/payload, function name) and include the traceback (exc_info=True) so
observability is preserved, then re-raise the exception; do not change the
MemoryError/RecursionError behavior.

Comment on lines +92 to +127
async def _save_metadata_with_rollback(
repo: Any,
storage: Any,
artifact_id: str,
updated: Artifact,
) -> None:
"""Save updated artifact metadata, rolling back storage on failure.

Args:
repo: Artifact persistence repository.
storage: Artifact content storage backend.
artifact_id: Artifact identifier.
updated: Updated artifact model.

Raises:
PersistenceError: If the metadata save fails (after rollback attempt).
"""
try:
await repo.save(updated)
except PersistenceError as exc:
logger.warning(
PERSISTENCE_ARTIFACT_SAVE_FAILED,
artifact_id=artifact_id,
error=str(exc),
note="metadata save failed, rolling back content",
)
try:
await storage.delete(artifact_id)
except Exception as cleanup_exc:
logger.warning(
PERSISTENCE_ARTIFACT_STORAGE_ROLLBACK_FAILED,
artifact_id=artifact_id,
error=str(cleanup_exc),
)
raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding type hints for repo and storage parameters.

The _save_metadata_with_rollback helper uses Any for repo and storage parameters. Adding proper type hints would improve type safety and IDE support:

 async def _save_metadata_with_rollback(
-    repo: Any,
-    storage: Any,
+    repo: ArtifactRepository,
+    storage: ArtifactStorage,
     artifact_id: str,
     updated: Artifact,
 ) -> None:

This assumes these types exist in the codebase. If not, protocol types could be defined for the required methods (save, delete, store).

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

In `@src/synthorg/api/controllers/artifacts.py` around lines 92 - 127, The helper
_save_metadata_with_rollback currently types repo and storage as Any; define
small Protocols (e.g., ArtifactRepository with async def save(self, artifact:
Artifact) -> None and StorageBackend with async def delete(self, artifact_id:
str) -> None) or use existing repository/storage interfaces, import
typing.Protocol, then change the function signature to repo: ArtifactRepository
and storage: StorageBackend (and add any necessary imports/exports); update
tests/type hints as needed so repo.save and storage.delete are properly typed.

Comment on lines +156 to +166
publish_ws_event(
request,
WsEventType.PROJECT_CREATED,
CHANNEL_PROJECTS,
{
"project_id": project.id,
"name": project.name,
"status": project.status.value,
"lead": project.lead,
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard WebSocket publish so project creation remains best-effort.

At Line [156], an exception from publish_ws_event(...) would fail the HTTP request after repo.save(project) already succeeded, causing false client failure and risky retries.

Suggested fix
         await repo.save(project)
         logger.info(PERSISTENCE_PROJECT_SAVED, project_id=project.id)
-        publish_ws_event(
-            request,
-            WsEventType.PROJECT_CREATED,
-            CHANNEL_PROJECTS,
-            {
-                "project_id": project.id,
-                "name": project.name,
-                "status": project.status.value,
-                "lead": project.lead,
-            },
-        )
+        try:
+            publish_ws_event(
+                request,
+                WsEventType.PROJECT_CREATED,
+                CHANNEL_PROJECTS,
+                {
+                    "project_id": project.id,
+                    "name": project.name,
+                    "status": project.status.value,
+                    "lead": project.lead,
+                },
+            )
+        except Exception as exc:
+            logger.warning(
+                "api.projects.ws_publish_failed",
+                project_id=project.id,
+                event=WsEventType.PROJECT_CREATED.value,
+                error=str(exc),
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/synthorg/api/controllers/projects.py` around lines 156 - 166, After
saving the project (repo.save(project)), make the WebSocket publish a
best-effort operation by wrapping the call to publish_ws_event(...) in a
try/except that catches Exception, logs the error (including context: project.id
and event type) and does not re-raise so the HTTP response remains successful;
locate the publish_ws_event call and modify it accordingly (e.g., try:
publish_ws_event(...) except Exception as e: logger.error(...)) to ensure
failures in publish_ws_event do not break the API flow.

>
{items.map((item) => (
<div key={item.label}>
<dt className="text-[10px] uppercase tracking-wide text-text-muted">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid arbitrary pixel typography in shared UI component.

text-[10px] bypasses the tokenized typography system for web/src/components/ui/*. Use a tokenized text size class instead.

♻️ Suggested change
-          <dt className="text-[10px] uppercase tracking-wide text-text-muted">
+          <dt className="text-xs uppercase tracking-wide text-text-muted">

As per coding guidelines, "When creating new shared components ... use design tokens exclusively."

📝 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
<dt className="text-[10px] uppercase tracking-wide text-text-muted">
<dt className="text-xs uppercase tracking-wide text-text-muted">
{item.label}
</dt>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/metadata-grid.tsx` at line 34, The dt element in
metadata-grid.tsx uses a raw utility class "text-[10px]" which bypasses the
project's typography tokens; replace that ad-hoc size with the appropriate
design-token class used across shared UI (i.e., swap "text-[10px]" in the dt
className to the project tokenized size class used for small/caption text in
shared components—use the existing token (e.g., the project's small/caption
token) so the metadata-grid component conforms to the tokenized typography
system.

- Replace hardcoded pixel breakpoints in ProjectDetailSkeleton
  and ProjectsSkeleton with standard Tailwind tokens (lg:, sm:, xl:)
- Fix ProjectHeader budget display: show formatCurrency(0) for zero
  budget instead of '--' (zero is a valid budget value)
- Document intentional unused event parameter in updateFromWsEvent
  for both artifacts and projects stores with explanatory comment
- Fix property-based test error string generators to use fc.lorem()
  instead of fc.string() to avoid whitespace edge cases in queryByText

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 05:36
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 1, 2026 05:37 — with GitHub Actions Inactive
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds full artifacts/projects management UX to the web dashboard and wires corresponding backend WebSocket channels/events to keep UI data fresh (polling + WS-triggered refetch), along with supporting shared UI components, mocks, and tests.

Changes:

  • Backend: add artifacts/projects WS channels + event types and best-effort publishing on create/delete/upload mutations.
  • Frontend: add routes/pages, typed API endpoints + Zustand stores/hooks for artifacts/projects list/detail UIs (with polling + WS bindings).
  • UI/tooling: add shared badges/metadata grid components (with Storybook), MSW handlers, and Vitest coverage for new stores/pages.

Reviewed changes

Copilot reviewed 63 out of 63 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
web/src/utils/format.ts Adds formatFileSize() for artifact size display.
web/src/utils/download.ts Adds helper to download artifact content as a file with toast-on-error.
web/src/stores/projects.ts New Zustand store for projects list/detail state and actions.
web/src/stores/artifacts.ts New Zustand store for artifacts list/detail state + text preview + delete.
web/src/router/routes.ts Adds route constants for projects/artifacts list + detail.
web/src/router/index.tsx Registers new lazy-loaded routes for projects/artifacts pages.
web/src/pages/ProjectsPage.tsx Projects list page shell (counts, errors, WS warning, create drawer).
web/src/pages/projects/ProjectTeamSection.tsx Team section UI for project detail page.
web/src/pages/projects/ProjectTaskList.tsx Linked task list UI for project detail page.
web/src/pages/projects/ProjectsSkeleton.tsx Loading skeleton for projects list page.
web/src/pages/projects/ProjectHeader.tsx Project detail header using MetadataGrid + status badge.
web/src/pages/projects/ProjectGridView.tsx Project card grid + empty state.
web/src/pages/projects/ProjectFilters.tsx Client-side filters for projects list (search + status).
web/src/pages/projects/ProjectDetailSkeleton.tsx Loading skeleton for project detail page.
web/src/pages/projects/ProjectCreateDrawer.tsx Create project drawer + validation + submit wiring.
web/src/pages/projects/ProjectCreateDrawer.stories.tsx Storybook stories for create project drawer.
web/src/pages/projects/ProjectCard.tsx Project list card UI.
web/src/pages/ProjectDetailPage.tsx Project detail page shell with error/WS banners and sections.
web/src/pages/ArtifactsPage.tsx Artifacts list page shell (counts, errors, WS warning).
web/src/pages/artifacts/ArtifactsSkeleton.tsx Loading skeleton for artifacts list page.
web/src/pages/artifacts/ArtifactMetadata.tsx Artifact detail metadata + download/delete actions.
web/src/pages/artifacts/ArtifactGridView.tsx Artifact card grid + empty state.
web/src/pages/artifacts/ArtifactFilters.tsx Client-side filters for artifacts list.
web/src/pages/artifacts/ArtifactDetailSkeleton.tsx Loading skeleton for artifact detail page.
web/src/pages/artifacts/ArtifactContentPreview.tsx Inline content preview (text via CodeMirror, images via blob URL).
web/src/pages/artifacts/ArtifactCard.tsx Artifact list card UI (size, content type, created time).
web/src/pages/ArtifactDetailPage.tsx Artifact detail page shell with error/WS banners and sections.
web/src/mocks/handlers/projects.ts Adds MSW handlers for project list + get-by-id.
web/src/mocks/handlers/index.ts Exports new artifacts/projects MSW handler groups.
web/src/mocks/handlers/artifacts.ts Adds MSW handlers for artifact list + get-by-id.
web/src/hooks/useProjectsData.ts Projects list data hook (initial fetch, polling, WS refetch debounce, filtering).
web/src/hooks/useProjectDetailData.ts Project detail data hook using shared useDetailData.
web/src/hooks/useDetailData.ts Shared detail hook for polling + WS debounce + cleanup.
web/src/hooks/useArtifactsData.ts Artifacts list data hook (initial fetch, polling, WS refetch debounce, filtering).
web/src/hooks/useArtifactDetailData.ts Artifact detail data hook using shared useDetailData.
web/src/components/ui/project-status-badge.tsx New semantic status-dot badge for project status.
web/src/components/ui/project-status-badge.stories.tsx Storybook stories for ProjectStatusBadge.
web/src/components/ui/metadata-grid.tsx New reusable key/value metadata grid component.
web/src/components/ui/metadata-grid.stories.tsx Storybook stories for MetadataGrid.
web/src/components/ui/content-type-badge.tsx New MIME-type badge for artifacts.
web/src/components/ui/content-type-badge.stories.tsx Storybook stories for ContentTypeBadge.
web/src/components/layout/Sidebar.tsx Adds sidebar nav entries for Projects and Artifacts.
web/src/api/types.ts Adds Artifact/Project types, enums/value lists, and WS channels/event types.
web/src/api/endpoints/projects.ts Implements typed listProjects, getProject, createProject.
web/src/api/endpoints/artifacts.ts Implements typed list/get/create/delete + content download/text endpoints.
web/src/tests/stores/projects.test.ts Store unit tests for projects state/actions/WS update behavior.
web/src/tests/stores/artifacts.test.ts Store unit tests for artifacts state/actions/preview/delete/WS update behavior.
web/src/tests/pages/ProjectsPage.test.tsx Page tests for Projects list page rendering states.
web/src/tests/pages/ProjectDetailPage.test.tsx Page tests for Project detail page rendering states + invariants.
web/src/tests/pages/ArtifactsPage.test.tsx Page tests for Artifacts list page rendering states + invariants.
web/src/tests/pages/ArtifactDetailPage.test.tsx Page tests for Artifact detail page rendering states + invariants.
web/src/tests/helpers/factories.ts Adds artifact/project factories for tests.
web/CLAUDE.md Updates web architecture/component inventory docs for new domains/components.
tests/unit/api/test_channels.py Updates channel test coverage for new WS channels and plugin config assertions.
src/synthorg/core/project.py Updates project budget documentation strings.
src/synthorg/core/artifact.py Adds project_id field and expands artifact attribute docs.
src/synthorg/api/ws_models.py Extends WS event types; adjusts payload typing.
src/synthorg/api/dto.py Extends CreateArtifactRequest with project_id; adds CreateProjectRequest validation.
src/synthorg/api/controllers/projects.py Publishes project.created WS events on project creation.
src/synthorg/api/controllers/artifacts.py Publishes artifact WS events on create/delete/upload; refactors rollback helper; adds project_id wiring.
src/synthorg/api/channels.py Adds artifacts/projects channels + a shared best-effort publish_ws_event() helper.
docs/design/page-structure.md Documents new pages/routes and WS channel mapping for artifacts/projects.
docs/design/brand-and-ux.md Documents new shared UI components and formatFileSize() utility.

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

event.model_dump_json(),
channels=[channel],
)
except MemoryError, RecursionError:
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

except MemoryError, RecursionError: is invalid Python 3 syntax and will raise a SyntaxError at import time. Use except (MemoryError, RecursionError): (or separate except blocks) to preserve the intended re-raise behavior.

Suggested change
except MemoryError, RecursionError:
except (MemoryError, RecursionError):

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +125
<InputField
label="Deadline"
type="date"
value={form.deadline}
onChange={(e) => updateField('deadline', e.target.value)}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The form uses <input type="date">, which produces a YYYY-MM-DD string, but the backend CreateProjectRequest validates deadline with datetime.fromisoformat() (expects a full ISO datetime). This will cause project creation to fail whenever a deadline is set; convert the date to a valid ISO 8601 datetime (e.g., append a time/offset) or switch the input to collect a full ISO datetime.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +51
http.get('/api/v1/projects/:id', ({ params }) => {
const project = mockProjects.find((p) => p.id === params.id)
if (!project) {
return HttpResponse.json(apiSuccess(null), { status: 404 })
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The 404 branch returns apiSuccess(null) with success: true, which contradicts the error status and can break client-side error handling/unwrap semantics. Return apiError('Project not found') (with success: false) for consistency with other handlers (e.g. artifacts) and the ApiResponse contract.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +39
{ label: 'Path', value: artifact.path, valueClassName: 'font-mono text-xs break-all' },
{ label: 'Task', value: artifact.task_id, valueClassName: 'font-mono text-xs' },
{ label: 'Created By', value: artifact.created_by },
{ label: 'Created', value: formatDate(artifact.created_at) },
]
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

artifact.project_id is part of the artifact model but isn't shown anywhere in the metadata grid. The PR description/docs call out a project link/field on the artifact detail page; consider adding a “Project” row (and optionally linking to the project detail route when present) so users can see the association.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +13
contentType.startsWith('text/') ||
contentType === 'application/json' ||
contentType === 'application/yaml' ||
contentType === 'application/x-yaml'
)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

isPreviewableText() treats application/yaml as previewable, but the CodeMirror language selection (getLanguage()) only recognizes application/x-yaml / text/yaml. This means YAML content labeled as application/yaml will fall back to JSON mode; either include application/yaml in getLanguage() or remove it from the previewable set to keep behavior consistent.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/stores/artifacts.ts`:
- Around line 81-83: In fetchArtifactDetail, clear stale detail state when a new
fetch begins and when it fails: set selectedArtifact and contentPreview to null
alongside setting detailLoading: true and detailError: null at the start of the
function (before awaiting getArtifact(id)), and likewise clear
selectedArtifact/contentPreview when handling the error path before setting
detailError; keep using the existing _detailRequestId guard so only responses
matching the current request restore state.
- Around line 51-52: Replace the current string-based _detailRequestId
stale-guard with a monotonic request token: make _detailRequestId a numeric
counter (or generate a unique monotonic token) and, in each method that fetches
artifact details (the functions that currently set/read _detailRequestId and
compare with artifact id at the stale-guard sites), increment/assign the token
before the async request, capture it in a local variable, and after the await
compare that local token to the shared _detailRequestId before mutating state;
update every occurrence that currently uses artifact id for staleness (the sites
referencing _detailRequestId at the lines noted) so all overlapping requests for
the same artifact are guarded by the monotonic token check.
- Around line 113-119: deleteArtifact currently removes the artifact from
artifacts and decrements totalArtifacts but leaves selectedArtifact intact;
update the deleteArtifact flow (the deleteArtifact function) to also clear or
unset selectedArtifact when the deleted id matches state.selectedArtifact?.id
(e.g., set selectedArtifact to null/undefined) so the store does not retain
deleted detail data after the artifacts and totalArtifacts update.

In `@web/src/stores/projects.ts`:
- Around line 53-60: The fetchProjects() handler applies every async response
and conflicts with the optimistic-create prepend in lines around 97-99, causing
duplicates or lost items; introduce a request token (e.g., _listRequestToken)
incremented on each fetch and capture its value in the fetchProjects call, then
only apply result.data to the store if the captured token equals the current
_listRequestToken, and update createProject/optimistic prepend logic to either
use the same token check or re-run a safe merge only when the tokens match so
out-of-order fetches do not overwrite the optimistic state.
- Around line 36-37: fetchProjectDetail currently uses a single _detailRequestId
string and only compares project id, allowing stale slower responses to
overwrite newer data and never clearing selectedProject; change
fetchProjectDetail to generate a unique request token (e.g., a UUID or
incremented counter assigned to _detailRequestId) at the start of the load,
store that token locally in the async scope, and on every state update only
apply results if the stored token matches _detailRequestId (not just project
id); additionally clear selectedProject (set to null/undefined) when beginning a
new load and also on error/early-return so ProjectDetailPage doesn't render
stale data; update all similar blocks (lines around 63-78 and 109-116) that use
_detailRequestId to follow the same token-check-and-clear pattern.
- Around line 56-57: The code currently hard-caps project list and linked tasks
(see listProjects call that passes limit: 200 and the linked-tasks truncation at
50), which hides data; update the logic to remove fixed limits and implement
proper pagination/incremental loading: modify the listProjects usage (and any
getProjectDetails/getLinkedTasks helper) to accept page/limit parameters or to
fetch until totalProjects is reached (using result.total) instead of a single
fixed 200, and change the linked tasks path (the function that truncates to 50)
to request paged task results and expose a “show more” or load-more mechanism in
the store so the UI can load additional pages on demand. Ensure you use the
existing result.total/totalProjects and the store methods (set, listProjects,
getLinkedTasks/getProjectDetails) to drive incremental fetches rather than
hard-coded constants.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6ef5cf78-772d-4c64-85f1-e16a1bfe2084

📥 Commits

Reviewing files that changed from the base of the PR and between 5440f22 and 990f140.

📒 Files selected for processing (8)
  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
  • web/src/pages/projects/ProjectHeader.tsx
  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/stores/artifacts.ts
  • web/src/stores/projects.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Agent
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones, including StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, and TagInput
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors; NEVER hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens) for typography; NEVER set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders; NEVER hardcode values
Do NOT recreate status dots inline -- use
Do NOT build card-with-header layouts from scratch -- use
Do NOT create metric displays with text-metric font-bold -- use
Do NOT render initials circles manually -- use
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT use hardcoded Framer Motion transition durations -- use presets from @/lib/motion

web/src/**/*.{ts,tsx}: Apply eslint-web (zero warnings) via pre-commit hooks for the web dashboard (conditional on web/src/**/*.{ts,tsx})
Always reuse existing components from web/src/components/ui/ before creating new ones in the React dashboard
Never hardcode hex colors, font-famil...

Files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
  • web/src/pages/projects/ProjectHeader.tsx
  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
  • web/src/stores/artifacts.ts
  • web/src/stores/projects.ts
🧠 Learnings (21)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Web dashboard: see `web/CLAUDE.md` for commands, design system, and component inventory
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared components, place in web/src/components/ui/ with descriptive kebab-case filename, create a .stories.tsx file with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively, and import cn from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones, including StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, and TagInput

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
  • web/src/pages/projects/ProjectHeader.tsx
  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
📚 Learning: 2026-03-31T22:07:03.782Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T22:07:03.782Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones in the React dashboard

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT build card-with-header layouts from scratch -- use <SectionCard>

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
  • web/src/pages/projects/ProjectHeader.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors; NEVER hardcode hex values in .tsx/.ts files

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
📚 Learning: 2026-03-31T22:07:03.782Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T22:07:03.782Z
Learning: Applies to web/src/**/*.{ts,tsx} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in the web dashboard — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to web/src/**/*.{ts,tsx} : NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders; NEVER hardcode values

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard

Applied to files:

  • web/src/pages/projects/ProjectsSkeleton.tsx
  • web/src/pages/projects/ProjectDetailSkeleton.tsx
  • web/src/pages/projects/ProjectHeader.tsx
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.stories.tsx : Use storybook/test instead of storybook/test for Storybook 10 imports

Applied to files:

  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: A PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files and catches: hardcoded hex colors and rgba values, hardcoded font-family declarations, hardcoded Framer Motion transition durations, new components without Storybook stories, duplicate patterns, and complex .map() blocks. Fix all violations before proceeding.

Applied to files:

  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/.storybook/preview.{ts,tsx} : In Storybook preview.tsx, set parameters.a11y.test to 'error', 'todo', or 'off' to enforce WCAG compliance on all stories (replace old .element and .manual patterns)

Applied to files:

  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to tests/**/*.py : NEVER skip, dismiss, or ignore flaky tests — always fix them fully and fundamentally. For timing-sensitive tests, mock `time.monotonic()` and `asyncio.sleep()` to make them deterministic instead of widening timing margins

Applied to files:

  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
  • web/src/__tests__/pages/ArtifactsPage.test.tsx
📚 Learning: 2026-03-31T22:07:03.782Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T22:07:03.782Z
Learning: Applies to tests/**/*.py : Never skip, dismiss, or ignore flaky tests. Always fix them fully and fundamentally. For timing-sensitive tests, mock `time.monotonic()` and `asyncio.sleep()` to make them deterministic. For indefinite blocking tasks, use `asyncio.Event().wait()` instead of `asyncio.sleep(large_number)`

Applied to files:

  • web/src/__tests__/pages/ArtifactDetailPage.test.tsx
📚 Learning: 2026-03-31T22:07:03.782Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T22:07:03.782Z
Learning: Applies to web/src/**/*.{ts,tsx} : The PostToolUse hook (`scripts/check_web_design_system.py`) enforces design system rules (component reuse, design token usage) on every Edit/Write to `web/src/`

Applied to files:

  • web/src/__tests__/pages/ProjectDetailPage.test.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/stores/projects.ts
🔇 Additional comments (6)
web/src/pages/projects/ProjectsSkeleton.tsx (1)

1-18: Looks good — skeleton implementation is clean and guideline-aligned.

Component reuse, responsive grid tokens, and map rendering are all in good shape.

web/src/pages/projects/ProjectDetailSkeleton.tsx (1)

1-13: Design-system compliant skeleton implementation.

Good use of shared Skeleton/SkeletonCard primitives and tokenized responsive layout classes.

As per coding guidelines, "Always reuse existing components from web/src/components/ui/ before creating new ones" and "Use density-aware tokens ... or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing."

web/src/pages/projects/ProjectHeader.tsx (1)

11-49: Header composition and metadata rendering look solid.

This correctly uses shared components and preserves valid zero-budget display behavior.

As per coding guidelines, "Do NOT build card-with-header layouts from scratch -- use " and "Always reuse existing components from web/src/components/ui/ before creating new ones."

web/src/__tests__/pages/ArtifactDetailPage.test.tsx (1)

84-130: Property-based state-transition coverage is well implemented.

The randomized invariants are aligned with the page’s conditional render contract and complement the example-based cases well.

Based on learnings, "Dashboard testing: Vitest unit tests organized by feature under web/src/__tests__/. Use fast-check for property-based testing (fc.assert + fc.property)."

web/src/__tests__/pages/ArtifactsPage.test.tsx (1)

88-142: Strong invariant testing for page state behavior.

The property tests are focused and correctly validate skeleton/error behavior across generated state permutations.

Based on learnings, "Dashboard testing: Vitest unit tests organized by feature under web/src/__tests__/. Use fast-check for property-based testing (fc.assert + fc.property)."

web/src/__tests__/pages/ProjectDetailPage.test.tsx (1)

88-134: Property-based transition checks are correctly scoped and useful.

Nice addition of invariant-based checks for loading/not-found/error branches on top of scenario tests.

Based on learnings, "Dashboard testing: Vitest unit tests organized by feature under web/src/__tests__/. Use fast-check for property-based testing (fc.assert + fc.property)."

Comment on lines +56 to +57
const result = await listProjects({ limit: 200 })
set({ projects: result.data, totalProjects: result.total, listLoading: false })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These fixed limits silently hide projects and tasks.

Line 56 hard-caps the dashboard to 200 loaded projects even though the store’s search/status/lead filters are local, so larger workspaces become partially unsearchable. Line 69 similarly truncates linked tasks at 50 with no pagination or “show more” signal, which means the project detail view can look complete while omitting data.

Also applies to: 67-69

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

In `@web/src/stores/projects.ts` around lines 56 - 57, The code currently
hard-caps project list and linked tasks (see listProjects call that passes
limit: 200 and the linked-tasks truncation at 50), which hides data; update the
logic to remove fixed limits and implement proper pagination/incremental
loading: modify the listProjects usage (and any getProjectDetails/getLinkedTasks
helper) to accept page/limit parameters or to fetch until totalProjects is
reached (using result.total) instead of a single fixed 200, and change the
linked tasks path (the function that truncates to 50) to request paged task
results and expose a “show more” or load-more mechanism in the store so the UI
can load additional pages on demand. Ensure you use the existing
result.total/totalProjects and the store methods (set, listProjects,
getLinkedTasks/getProjectDetails) to drive incremental fetches rather than
hard-coded constants.

- Replace string-based _detailRequestId with monotonic counter token
  for more robust stale-response guards (handles same-ID refetch)
- Clear selectedArtifact/contentPreview at fetch start and on error
  to prevent stale data flash during navigation
- Clear selectedProject/projectTasks at fetch start and on error
- deleteArtifact clears selectedArtifact when the deleted ID matches
  the currently viewed artifact
- Extract isStaleRequest() helper to avoid false-positive timing
  attack lint warnings on token comparison

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
web/src/stores/projects.ts (3)

96-105: ⚠️ Potential issue | 🟡 Minor

Optimistic update can cause temporary duplicate entries.

If a WS-triggered fetchProjects() resolves between the API call and the optimistic set(), or if the server already includes the new project in a concurrent response, the project may appear twice until the next reconciliation.

🛠️ Proposed fix: deduplicate on insert
   createProject: async (data: CreateProjectRequest) => {
     const project = await createProjectApi(data)
-    // Optimistically add to local state for immediate UI update
-    set((state) => ({
-      projects: [project, ...state.projects],
-      totalProjects: state.totalProjects + 1,
-    }))
+    // Add to local state, avoiding duplicates if a refetch already included it
+    set((state) => {
+      if (state.projects.some((p) => p.id === project.id)) return state
+      return {
+        projects: [project, ...state.projects],
+        totalProjects: state.totalProjects + 1,
+      }
+    })
     // Polling and WS events will reconcile with server state.
     return project
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/projects.ts` around lines 96 - 105, The optimistic insert in
createProject (which calls createProjectApi and then set(...)) can produce
duplicates if a WS or fetchProjects() already added the same project; fix by
deduplicating before inserting: when updating state in createProject's set
callback, filter out any existing project with the same unique id (e.g.,
project.id) from state.projects and then prepend the new project, and adjust
totalProjects only if the id was not already present; this ensures
createProject, the set handler, and interactions with fetchProjects()/WS events
won't produce temporary duplicates.

55-63: ⚠️ Potential issue | 🟠 Major

List fetch race condition remains unaddressed.

Concurrent fetchProjects() calls (from polling + WS events) can race, and the slower response overwrites newer data. Unlike fetchProjectDetail, this action has no request token guard.

Additionally, the hard limit of 200 projects means larger workspaces will have data silently hidden from client-side search/filters.

🛠️ Proposed fix: add list request token
+let _listRequestToken = 0

   fetchProjects: async () => {
+    const token = ++_listRequestToken
     set({ listLoading: true, listError: null })
     try {
       const result = await listProjects({ limit: 200 })
+      if (_listRequestToken !== token) return
       set({ projects: result.data, totalProjects: result.total, listLoading: false })
     } catch (err) {
+      if (_listRequestToken !== token) return
       set({ listLoading: false, listError: getErrorMessage(err) })
     }
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/projects.ts` around lines 55 - 63, fetchProjects can race when
called concurrently (polling + WS) and the slower response can overwrite newer
data; also the hard-coded limit of 200 hides projects for large workspaces. Fix
by adding a request token guard: when fetchProjects starts, generate and save a
new token into state (e.g., state key listRequestToken), capture it in the local
scope, call listProjects (avoid forcing limit=200 — pass a configurable limit or
omit to enable server-side paging), and on success only set
projects/totalProjects/listLoading if the saved token still matches the captured
token; ensure errors also check/reset listLoading only for the active token and
set listError using getErrorMessage(err). Use the existing fetchProjects
function and state keys (listRequestToken, projects, totalProjects, listLoading,
listError) to implement this guard.

111-119: ⚠️ Potential issue | 🔴 Critical

Resetting token to 0 breaks stale-request detection.

Setting _detailRequestToken = 0 instead of incrementing it allows token collisions after navigation. Per useDetailData (relevant snippet #4), clearDetail() is called on unmount. Race scenario:

  1. User views Project A → fetchProjectDetail("A"): token = 1
  2. User navigates to Project B → cleanup calls clearDetail(): token = 0
  3. fetchProjectDetail("B") called: token = ++0 = 1 (same as A's token!)
  4. Project A's response arrives first: isStaleRequest(1) checks 1 !== 1 → false → stale data applied
  5. User sees Project A data on Project B's page until B's response overwrites it
🐛 Proposed fix: increment instead of reset
   clearDetail: () => {
-    _detailRequestToken = 0
+    _detailRequestToken += 1
     set({
       selectedProject: null,
       projectTasks: [],
       detailLoading: false,
       detailError: null,
     })
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/projects.ts` around lines 111 - 119, clearDetail currently
resets _detailRequestToken to 0 which allows token reuse and stale-response
collisions; instead increment the token so any prior requests become stale. In
the clearDetail function (referencing clearDetail and _detailRequestToken),
replace the assignment _detailRequestToken = 0 with an increment (e.g.,
++_detailRequestToken or _detailRequestToken++) so fetchProjectDetail /
isStaleRequest logic (and the cleanup in useDetailData) will correctly mark
earlier responses as stale.
web/src/stores/artifacts.ts (1)

132-133: ⚠️ Potential issue | 🟠 Major

Regression: clearDetail() makes detail request tokens reusable again.

Line 133 resets _detailRequestToken to 0. Since useArtifactDetailData calls clearDetail() on unmount, a fast leave/re-enter can reuse token 1 and let an older fetchArtifactDetail() response pass isStaleRequest(), briefly restoring the wrong artifact. Keep the counter monotonic here.

♻️ Minimal fix
   clearDetail: () => {
-    _detailRequestToken = 0
+    _detailRequestToken++
     set({
       selectedArtifact: null,
       contentPreview: null,
       detailLoading: false,
       detailError: null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/artifacts.ts` around lines 132 - 133, clearDetail() currently
resets _detailRequestToken to 0 which makes tokens reusable and allows stale
fetchArtifactDetail() responses to pass isStaleRequest() after a fast
unmount/remount; instead make the token monotonic by bumping it (e.g. replace
the reset with an increment such as ++_detailRequestToken) so every
clearDetail() produces a newer token and older responses are always considered
stale; update clearDetail in the artifacts store (referencing clearDetail and
_detailRequestToken) — no other behavioral changes to useArtifactDetailData,
fetchArtifactDetail, or isStaleRequest are needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/stores/artifacts.ts`:
- Around line 73-77: The fetchArtifacts function is hard-capping results by
calling listArtifacts({ limit: 200 }), preventing filters/pagination from
reaching the backend; update fetchArtifacts to stop using a fixed limit and
instead accept and forward active store filters and pagination parameters (e.g.,
page, pageSize, cursor) to listArtifacts (or implement server-side pagination
loop if you truly need all rows), so that listArtifacts receives the current
filters from the store and returns the correct subset and totalArtifacts; ensure
set calls update listLoading/listError appropriately while using the returned
paging metadata to populate artifacts and totalArtifacts.

---

Duplicate comments:
In `@web/src/stores/artifacts.ts`:
- Around line 132-133: clearDetail() currently resets _detailRequestToken to 0
which makes tokens reusable and allows stale fetchArtifactDetail() responses to
pass isStaleRequest() after a fast unmount/remount; instead make the token
monotonic by bumping it (e.g. replace the reset with an increment such as
++_detailRequestToken) so every clearDetail() produces a newer token and older
responses are always considered stale; update clearDetail in the artifacts store
(referencing clearDetail and _detailRequestToken) — no other behavioral changes
to useArtifactDetailData, fetchArtifactDetail, or isStaleRequest are needed.

In `@web/src/stores/projects.ts`:
- Around line 96-105: The optimistic insert in createProject (which calls
createProjectApi and then set(...)) can produce duplicates if a WS or
fetchProjects() already added the same project; fix by deduplicating before
inserting: when updating state in createProject's set callback, filter out any
existing project with the same unique id (e.g., project.id) from state.projects
and then prepend the new project, and adjust totalProjects only if the id was
not already present; this ensures createProject, the set handler, and
interactions with fetchProjects()/WS events won't produce temporary duplicates.
- Around line 55-63: fetchProjects can race when called concurrently (polling +
WS) and the slower response can overwrite newer data; also the hard-coded limit
of 200 hides projects for large workspaces. Fix by adding a request token guard:
when fetchProjects starts, generate and save a new token into state (e.g., state
key listRequestToken), capture it in the local scope, call listProjects (avoid
forcing limit=200 — pass a configurable limit or omit to enable server-side
paging), and on success only set projects/totalProjects/listLoading if the saved
token still matches the captured token; ensure errors also check/reset
listLoading only for the active token and set listError using
getErrorMessage(err). Use the existing fetchProjects function and state keys
(listRequestToken, projects, totalProjects, listLoading, listError) to implement
this guard.
- Around line 111-119: clearDetail currently resets _detailRequestToken to 0
which allows token reuse and stale-response collisions; instead increment the
token so any prior requests become stale. In the clearDetail function
(referencing clearDetail and _detailRequestToken), replace the assignment
_detailRequestToken = 0 with an increment (e.g., ++_detailRequestToken or
_detailRequestToken++) so fetchProjectDetail / isStaleRequest logic (and the
cleanup in useDetailData) will correctly mark earlier responses as stale.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d591c668-a141-4911-aa9e-a5d3fdf9948a

📥 Commits

Reviewing files that changed from the base of the PR and between 990f140 and 6cab2b1.

📒 Files selected for processing (2)
  • web/src/stores/artifacts.ts
  • web/src/stores/projects.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones, including StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, and TagInput
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors; NEVER hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens) for typography; NEVER set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders; NEVER hardcode values
Do NOT recreate status dots inline -- use
Do NOT build card-with-header layouts from scratch -- use
Do NOT create metric displays with text-metric font-bold -- use
Do NOT render initials circles manually -- use
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT use hardcoded Framer Motion transition durations -- use presets from @/lib/motion

web/src/**/*.{ts,tsx}: Apply eslint-web (zero warnings) via pre-commit hooks for the web dashboard (conditional on web/src/**/*.{ts,tsx})
Always reuse existing components from web/src/components/ui/ before creating new ones in the React dashboard
Never hardcode hex colors, font-famil...

Files:

  • web/src/stores/artifacts.ts
  • web/src/stores/projects.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Keep functions under 50 lines and files under 800 lines

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Line length: 88 characters (enforced by ruff)

Applied to files:

  • web/src/stores/projects.ts
🔇 Additional comments (4)
web/src/stores/projects.ts (4)

1-34: LGTM!

The imports and interface are well-structured. Good use of readonly arrays for immutability and clear separation between list, detail, and filter state.


36-38: LGTM!

Good use of a monotonic counter with an extracted helper function to centralize stale-request detection.


65-94: LGTM!

Good implementation with proper stale-request guarding, state clearing at fetch start, and graceful handling of partial failures with informative error messages. The Promise.allSettled usage correctly handles independent fetch failures.

Note: The 50-task limit (line 71) may hide data for projects with many tasks. Consider adding pagination or a "show more" indicator in a future iteration.


121-125: LGTM!

The comment clearly documents why the event payload is intentionally ignored. Given the 30s polling interval, a full refetch is a reasonable simplification.

Comment on lines +73 to +77
fetchArtifacts: async () => {
set({ listLoading: true, listError: null })
try {
const result = await listArtifacts({ limit: 200 })
set({ artifacts: result.data, totalArtifacts: result.total, listLoading: false })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stop truncating the artifact list at 200 rows.

Line 76 always calls listArtifacts({ limit: 200 }), so none of the store filters ever reach the API and the dashboard only searches/manages the first 200 artifacts loaded into memory. Once result.total > 200, matching artifacts after page 1 become impossible to find even though totalArtifacts still reports them. Please either paginate this store or send the active filters to the backend instead of hard-capping an unfiltered snapshot.

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

In `@web/src/stores/artifacts.ts` around lines 73 - 77, The fetchArtifacts
function is hard-capping results by calling listArtifacts({ limit: 200 }),
preventing filters/pagination from reaching the backend; update fetchArtifacts
to stop using a fixed limit and instead accept and forward active store filters
and pagination parameters (e.g., page, pageSize, cursor) to listArtifacts (or
implement server-side pagination loop if you truly need all rows), so that
listArtifacts receives the current filters from the store and returns the
correct subset and totalArtifacts; ensure set calls update listLoading/listError
appropriately while using the returned paging metadata to populate artifacts and
totalArtifacts.

- clearDetail increments token instead of resetting to 0, preventing
  stale-response collision on fast unmount/remount cycles
- createProject deduplicates by ID before optimistic prepend to
  prevent transient duplicates when concurrent fetch returns same project
- fetchProjects uses list request token guard so slower concurrent
  responses (polling + WS) do not overwrite newer data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 06:18
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 1, 2026 06:19 — with GitHub Actions Inactive
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 63 out of 63 changed files in this pull request and generated 5 comments.


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

Comment on lines +103 to +113
createProject: async (data: CreateProjectRequest) => {
const project = await createProjectApi(data)
// Optimistically add to local state for immediate UI update.
// Filter by ID first to prevent duplicates if a concurrent fetch already added it.
set((state) => {
const filtered = state.projects.filter((p) => p.id !== project.id)
return {
projects: [project, ...filtered],
totalProjects: filtered.length + 1,
}
})
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

totalProjects is being set to filtered.length + 1 after createProject(). This can corrupt the count when the list is paginated/limited (e.g., limit: 200) or already contains more/less items than totalProjects. Consider incrementing from the previous state.totalProjects (while still de-duping the list) or reusing the server-reported total when available.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +43
function handleDownload() {
downloadArtifactFile(artifact.id, artifact.path.split('/').pop() ?? artifact.id)
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Filename fallback uses artifact.path.split('/').pop() ?? artifact.id. If path ends with /, .pop() returns an empty string, and ?? will keep it, producing a blank download filename. Use || (or explicitly check for a non-empty string) so empty names fall back to artifact.id.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +24
function getLanguage(contentType: string): 'json' | 'yaml' {
const lower = contentType.toLowerCase()
if (lower === 'application/json') return 'json'
if (lower === 'application/x-yaml' || lower === 'text/yaml') return 'yaml'
// Falls back to JSON mode for non-JSON/non-YAML text types.
// Plain text may show minor syntax coloring.
return 'json'
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

getLanguage() treats application/json as JSON and only recognizes YAML for application/x-yaml or text/yaml, but the store’s preview logic also allows application/yaml. Add application/yaml here (and consider text/x-yaml) so YAML previews consistently get YAML mode instead of falling back to JSON highlighting.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +39
const handleDownload = useCallback(() => {
downloadArtifactFile(artifact.id, artifact.path.split('/').pop() ?? artifact.id)
}, [artifact.id, artifact.path])
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Same empty-filename issue as in ArtifactMetadata: artifact.path.split('/').pop() ?? artifact.id will produce an empty download name if the path ends with /. Prefer || or a non-empty-string check for the fallback.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +52
http.get('/api/v1/projects/:id', ({ params }) => {
const project = mockProjects.find((p) => p.id === params.id)
if (!project) {
return HttpResponse.json(apiSuccess(null), { status: 404 })
}
return HttpResponse.json(apiSuccess(project))
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

On 404 this handler returns apiSuccess(null) (success=true, data=null). The client unwrap() treats data: null as an error but will surface an unhelpful "Unknown API error" message because error is null. Return apiError('Project not found') (like the artifacts handler) so Storybook and tests get a meaningful error string.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
web/src/stores/projects.ts (1)

63-63: ⚠️ Potential issue | 🟠 Major

Remove fixed fetch caps that silently truncate visible data.

Line 63 (limit: 200) and Line 78 (limit: 50) can make list/detail views appear complete while omitting projects/tasks. With local filtering and no load-more in this store, users can’t discover truncated items.

Also applies to: 78-78

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

In `@web/src/stores/projects.ts` at line 63, The store is hard-capping fetches
(e.g., listProjects({ limit: 200 }) and listProjects({ limit: 50 })) which
silently truncates visible data; update the calls to listProjects used in this
store (e.g., in the load/list and detail-loading functions) to remove fixed
limits and instead implement proper pagination or full retrieval: either accept
a configurable limit/offset param passed into the store, or loop using the API
cursor/next-token until all items are fetched (or document a clear default and
expose a "load more" API). Replace the literal limit: 200 and limit: 50 usages
with this pagination logic or a parameterized call so no items are silently
omitted.
web/src/stores/artifacts.ts (2)

117-122: ⚠️ Potential issue | 🟡 Minor

Clear the detail error when deleting the selected artifact.

If the current artifact already had a preview warning in detailError, Lines 120-121 null the artifact but leave that message behind. The detail page will then show the stale preview error instead of its default “Artifact not found.” state.

♻️ Suggested cleanup
     set((state) => ({
       artifacts: state.artifacts.filter((a) => a.id !== id),
       totalArtifacts: Math.max(0, state.totalArtifacts - 1),
       selectedArtifact: state.selectedArtifact?.id === id ? null : state.selectedArtifact,
       contentPreview: state.selectedArtifact?.id === id ? null : state.contentPreview,
+      detailLoading: state.selectedArtifact?.id === id ? false : state.detailLoading,
+      detailError: state.selectedArtifact?.id === id ? null : state.detailError,
     }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/artifacts.ts` around lines 117 - 122, When removing an
artifact in the set updater, also clear any stale detail error for the deleted
selected item: if state.selectedArtifact?.id === id then set selectedArtifact to
null, contentPreview to null, and detailError to null so the detail page doesn't
show the old preview warning; update the set call handling
artifacts/totalArtifacts to include detailError: state.selectedArtifact?.id ===
id ? null : state.detailError (referencing set, artifacts, totalArtifacts,
selectedArtifact, contentPreview, and detailError).

73-77: ⚠️ Potential issue | 🟠 Major

Stop refreshing an unfiltered 200-row snapshot.

fetchArtifacts() still ignores every filter in the store and always requests listArtifacts({ limit: 200 }). Once the workspace has more than 200 artifacts, the dashboard can only search/filter/manage the first page while totalArtifacts still reports the larger total.

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

In `@web/src/stores/artifacts.ts` around lines 73 - 77, fetchArtifacts currently
ignores the store's filtering/pagination and always calls listArtifacts({ limit:
200 }), causing the UI to only show the first snapshot; update fetchArtifacts to
read the store's current filters/pagination (e.g., filter criteria, page/offset,
limit) and pass them into listArtifacts (for example listArtifacts({ limit,
offset, ...filters })) before setting artifacts and totalArtifacts so results
match the active filters; ensure you reference the store's getters (used in this
file) to obtain those values and still set listLoading/listError appropriately
in the try/catch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/stores/artifacts.ts`:
- Around line 51-53: fetchArtifacts and updateFromWsEvent can race: ensure each
async list-refresh path uses a request token snapshot and skips applying results
if stale. Increment _detailRequestToken when starting a new list fetch, capture
it in a local variable, and call isStaleRequest(token) before mutating state
(including listLoading, listError, and the artifacts list) so an older response
cannot overwrite a newer snapshot; apply the same token-checked guard in
updateFromWsEvent and any other list-update code paths referenced around lines
73-80.
- Around line 87-107: The selectedArtifact and loading state are currently set
only after getArtifactContentText completes, which blocks metadata rendering;
change the sequencing in the function that calls getArtifact(id),
isPreviewableText(), getArtifactContentText(id), and set(...) so that after
awaiting getArtifact(id) you immediately call set({ selectedArtifact: artifact,
detailLoading: false }) (and set contentPreview:null/detailError as appropriate)
to render metadata, then kick off the preview fetch asynchronously (e.g., start
getArtifactContentText in a detached async block or Promise) that still checks
isStaleRequest(token) before applying results; when the preview resolves or
errors, update contentPreview and detailError via set(...) and ensure
partialErrors use getErrorMessage(err) and stale-request checks (refer to
getArtifact, getArtifactContentText, isPreviewableText, selectedArtifact,
contentPreview, detailLoading, detailError, set).

In `@web/src/stores/projects.ts`:
- Around line 107-112: The patch recomputes totalProjects from the local slice
(filtered.length + 1) which can undercount when the store holds a partial
dataset; inside the set((state) => { ... }) updater, preserve and increment the
existing state.totalProjects instead of deriving it from the filtered array—e.g.
set totalProjects to (state.totalProjects ?? filtered.length) + 1 to safely
fallback when totalProjects is missing; update the set callback that manipulates
projects and totalProjects accordingly.

---

Duplicate comments:
In `@web/src/stores/artifacts.ts`:
- Around line 117-122: When removing an artifact in the set updater, also clear
any stale detail error for the deleted selected item: if
state.selectedArtifact?.id === id then set selectedArtifact to null,
contentPreview to null, and detailError to null so the detail page doesn't show
the old preview warning; update the set call handling artifacts/totalArtifacts
to include detailError: state.selectedArtifact?.id === id ? null :
state.detailError (referencing set, artifacts, totalArtifacts, selectedArtifact,
contentPreview, and detailError).
- Around line 73-77: fetchArtifacts currently ignores the store's
filtering/pagination and always calls listArtifacts({ limit: 200 }), causing the
UI to only show the first snapshot; update fetchArtifacts to read the store's
current filters/pagination (e.g., filter criteria, page/offset, limit) and pass
them into listArtifacts (for example listArtifacts({ limit, offset, ...filters
})) before setting artifacts and totalArtifacts so results match the active
filters; ensure you reference the store's getters (used in this file) to obtain
those values and still set listLoading/listError appropriately in the try/catch.

In `@web/src/stores/projects.ts`:
- Line 63: The store is hard-capping fetches (e.g., listProjects({ limit: 200 })
and listProjects({ limit: 50 })) which silently truncates visible data; update
the calls to listProjects used in this store (e.g., in the load/list and
detail-loading functions) to remove fixed limits and instead implement proper
pagination or full retrieval: either accept a configurable limit/offset param
passed into the store, or loop using the API cursor/next-token until all items
are fetched (or document a clear default and expose a "load more" API). Replace
the literal limit: 200 and limit: 50 usages with this pagination logic or a
parameterized call so no items are silently omitted.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4c1d5f30-c52d-495a-9922-93b4d4e5d6fb

📥 Commits

Reviewing files that changed from the base of the PR and between 6cab2b1 and 1b011b6.

📒 Files selected for processing (2)
  • web/src/stores/artifacts.ts
  • web/src/stores/projects.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Agent
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Build Sandbox
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones, including StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, and TagInput
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors; NEVER hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens) for typography; NEVER set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders; NEVER hardcode values
Do NOT recreate status dots inline -- use
Do NOT build card-with-header layouts from scratch -- use
Do NOT create metric displays with text-metric font-bold -- use
Do NOT render initials circles manually -- use
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT use hardcoded Framer Motion transition durations -- use presets from @/lib/motion

web/src/**/*.{ts,tsx}: Apply eslint-web (zero warnings) via pre-commit hooks for the web dashboard (conditional on web/src/**/*.{ts,tsx})
Always reuse existing components from web/src/components/ui/ before creating new ones in the React dashboard
Never hardcode hex colors, font-famil...

Files:

  • web/src/stores/projects.ts
  • web/src/stores/artifacts.ts
🧠 Learnings (5)
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Keep functions under 50 lines and files under 800 lines

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Line length: 88 characters (enforced by ruff)

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.stories.tsx : Use storybook/actions instead of storybook/addon-actions for Storybook 10 imports

Applied to files:

  • web/src/stores/artifacts.ts
🔇 Additional comments (1)
web/src/stores/projects.ts (1)

36-43: Good stale-response protection for concurrent async loads.

The request-token guards for list/detail fetches and token invalidation in clearDetail correctly prevent out-of-order responses from clobbering newer state.

Also applies to: 59-69, 72-82, 122-130

Comment on lines +51 to +53
let _detailRequestToken = 0
/** True when a newer detail request has superseded this one. */
function isStaleRequest(token: number): boolean { return _detailRequestToken !== token }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard the list refresh path against stale responses.

fetchArtifacts() can run from polling and updateFromWsEvent() concurrently. If the older request resolves last, it overwrites the newer snapshot and can also drop listLoading/listError for the wrong request.

🛡️ Suggested stale-response guard
+let _listRequestToken = 0
 let _detailRequestToken = 0
 /** True when a newer detail request has superseded this one. */
 function isStaleRequest(token: number): boolean { return _detailRequestToken !== token }

   fetchArtifacts: async () => {
+    const token = ++_listRequestToken
     set({ listLoading: true, listError: null })
     try {
       const result = await listArtifacts({ limit: 200 })
+      if (token !== _listRequestToken) return
       set({ artifacts: result.data, totalArtifacts: result.total, listLoading: false })
     } catch (err) {
+      if (token !== _listRequestToken) return
       set({ listLoading: false, listError: getErrorMessage(err) })
     }
   },

Also applies to: 73-80

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

In `@web/src/stores/artifacts.ts` around lines 51 - 53, fetchArtifacts and
updateFromWsEvent can race: ensure each async list-refresh path uses a request
token snapshot and skips applying results if stale. Increment
_detailRequestToken when starting a new list fetch, capture it in a local
variable, and call isStaleRequest(token) before mutating state (including
listLoading, listError, and the artifacts list) so an older response cannot
overwrite a newer snapshot; apply the same token-checked guard in
updateFromWsEvent and any other list-update code paths referenced around lines
73-80.

Comment on lines +107 to +112
set((state) => {
const filtered = state.projects.filter((p) => p.id !== project.id)
return {
projects: [project, ...filtered],
totalProjects: filtered.length + 1,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

totalProjects is recomputed from local slice, which can corrupt counts.

Line 111 uses filtered.length + 1, but filtered is only the currently loaded list. If the store holds a partial dataset, this can drop totalProjects to an incorrect value after create.

Proposed fix
   set((state) => {
-    const filtered = state.projects.filter((p) => p.id !== project.id)
-    return {
-      projects: [project, ...filtered],
-      totalProjects: filtered.length + 1,
-    }
+    const exists = state.projects.some((p) => p.id === project.id)
+    if (exists) {
+      return { projects: state.projects }
+    }
+    return {
+      projects: [project, ...state.projects],
+      totalProjects: state.totalProjects + 1,
+    }
   })
📝 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
set((state) => {
const filtered = state.projects.filter((p) => p.id !== project.id)
return {
projects: [project, ...filtered],
totalProjects: filtered.length + 1,
}
set((state) => {
const exists = state.projects.some((p) => p.id === project.id)
if (exists) {
return { projects: state.projects }
}
return {
projects: [project, ...state.projects],
totalProjects: state.totalProjects + 1,
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/projects.ts` around lines 107 - 112, The patch recomputes
totalProjects from the local slice (filtered.length + 1) which can undercount
when the store holds a partial dataset; inside the set((state) => { ... })
updater, preserve and increment the existing state.totalProjects instead of
deriving it from the filtered array—e.g. set totalProjects to
(state.totalProjects ?? filtered.length) + 1 to safely fallback when
totalProjects is missing; update the set callback that manipulates projects and
totalProjects accordingly.

- Add list request token guard to artifacts store fetchArtifacts to
  prevent concurrent polling/WS responses from overwriting newer data
- Render artifact metadata immediately after getArtifact resolves,
  then fetch content preview asynchronously in the background so the
  detail page is not blocked by preview loading
- Clear detailError alongside selectedArtifact/contentPreview when
  deleting the currently viewed artifact
- Fix projects MSW handler 404 to use apiError instead of apiSuccess
- Add project_id to ArtifactMetadata grid display
- Add application/yaml to getLanguage() for consistent YAML
  syntax highlighting (matches isPreviewableText)
- Fix empty filename fallback: use || instead of ?? so paths
  ending with / fall back to artifact.id
- Fix createProject totalProjects: preserve server-reported total
  and only increment when project ID was not already present

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
web/src/stores/projects.ts (1)

63-63: ⚠️ Potential issue | 🟠 Major

Remove fixed fetch caps that silently truncate projects/tasks.

Line 63 (limit: 200) and Line 78 (limit: 50) can hide valid data while the UI filters and detail view read from local store state. This makes larger workspaces partially unsearchable and project detail tasks potentially incomplete with no load-more path.

Please switch to explicit pagination/incremental loading (or full paginated accumulation) instead of hard caps.

Also applies to: 78-78

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

In `@web/src/stores/projects.ts` at line 63, The call using a hard cap
(listProjects({ limit: 200 }) and the similar listProjects({ limit: 50 }))
truncates results; replace these fixed limits with proper pagination: update the
code that calls listProjects to either (A) perform incremental accumulation by
repeatedly calling listProjects with the API's cursor/offset until no more pages
and merge into the local store, or (B) expose a loadMore pagination API/state so
the UI requests additional pages on demand; locate the occurrences of
listProjects(...) in this file (and any helper like
fetchProjects/fetchProjectTasks if present) and remove the hard-coded limits,
wiring in the cursor/offset params and accumulation or loadMore behavior instead
of truncating results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/artifacts/ArtifactMetadata.tsx`:
- Around line 64-98: The Delete button remains clickable while a deletion is in
progress; update the Delete Button (the Button instance that calls
setDeleteOpen(true)) to reflect the deleting state by adding disabled={deleting}
(and/or aria-disabled={deleting} or a loading prop if available) and keep its
onClick no-op when deleting, so that while deleting is true the button cannot
open the ConfirmDialog again; adjust the Button near the Trash2 icon and ensure
the deleting prop is used consistently with the ConfirmDialog and handleDelete.

In `@web/src/stores/artifacts.ts`:
- Around line 148-152: The WebSocket handler updateFromWsEvent is calling
fetchArtifacts() fire-and-forget so network failures are swallowed; make
updateFromWsEvent async, await useArtifactsStore.getState().fetchArtifacts(),
and wrap the await in a try/catch that handles errors (e.g., set the store's
listError or call the app's error/notification helper and console.error) so a
failed refetch updates state or surfaces a notification instead of silently
leaving stale data.
- Around line 77-88: fetchArtifacts currently calls listArtifacts({ limit: 200
}) and ignores store filter state, causing only the first 200 artifacts to be
fetched; update fetchArtifacts to read the active filter fields from the store
(e.g., type, created_by, task_id, content_type, project_id) and pass them into
listArtifacts alongside limit (or implement paginated requests that include
these filters) so the back-end applies filtering correctly; ensure you still use
_listRequestToken and isStaleListRequest as before and set
listLoading/listError/artifacts/totalArtifacts from the API response.

---

Duplicate comments:
In `@web/src/stores/projects.ts`:
- Line 63: The call using a hard cap (listProjects({ limit: 200 }) and the
similar listProjects({ limit: 50 })) truncates results; replace these fixed
limits with proper pagination: update the code that calls listProjects to either
(A) perform incremental accumulation by repeatedly calling listProjects with the
API's cursor/offset until no more pages and merge into the local store, or (B)
expose a loadMore pagination API/state so the UI requests additional pages on
demand; locate the occurrences of listProjects(...) in this file (and any helper
like fetchProjects/fetchProjectTasks if present) and remove the hard-coded
limits, wiring in the cursor/offset params and accumulation or loadMore behavior
instead of truncating results.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7a419ade-5ee9-48dd-a8c3-b80e812e76a5

📥 Commits

Reviewing files that changed from the base of the PR and between 1b011b6 and 4ddb009.

📒 Files selected for processing (5)
  • web/src/mocks/handlers/projects.ts
  • web/src/pages/artifacts/ArtifactContentPreview.tsx
  • web/src/pages/artifacts/ArtifactMetadata.tsx
  • web/src/stores/artifacts.ts
  • web/src/stores/projects.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones, including StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, and TagInput
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) for colors; NEVER hardcode hex values in .tsx/.ts files
Use font-sans or font-mono (maps to Geist tokens) for typography; NEVER set fontFamily directly
Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders; NEVER hardcode values
Do NOT recreate status dots inline -- use
Do NOT build card-with-header layouts from scratch -- use
Do NOT create metric displays with text-metric font-bold -- use
Do NOT render initials circles manually -- use
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Do NOT use rgba() with hardcoded values -- use design token variables
Do NOT use hardcoded Framer Motion transition durations -- use presets from @/lib/motion

web/src/**/*.{ts,tsx}: Apply eslint-web (zero warnings) via pre-commit hooks for the web dashboard (conditional on web/src/**/*.{ts,tsx})
Always reuse existing components from web/src/components/ui/ before creating new ones in the React dashboard
Never hardcode hex colors, font-famil...

Files:

  • web/src/mocks/handlers/projects.ts
  • web/src/pages/artifacts/ArtifactContentPreview.tsx
  • web/src/pages/artifacts/ArtifactMetadata.tsx
  • web/src/stores/projects.ts
  • web/src/stores/artifacts.ts
🧠 Learnings (5)
📚 Learning: 2026-03-31T14:41:06.345Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-03-31T14:41:06.345Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones, including StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, and TagInput

Applied to files:

  • web/src/pages/artifacts/ArtifactMetadata.tsx
📚 Learning: 2026-03-15T21:49:53.264Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:49:53.264Z
Learning: Fix everything valid — never skip when review agents find valid issues (including pre-existing issues in surrounding code, suggestions, and findings adjacent to the PR's changes). No deferring, no 'out of scope' skipping.

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Fix all valid issues found by review agents (including pre-existing issues in surrounding code, suggestions, and adjacent findings) — never skip or defer

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Keep functions under 50 lines and files under 800 lines

Applied to files:

  • web/src/stores/projects.ts
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Line length: 88 characters (enforced by ruff)

Applied to files:

  • web/src/stores/projects.ts
🔇 Additional comments (10)
web/src/mocks/handlers/projects.ts (1)

1-54: LGTM!

The MSW handlers are well-structured and follow established patterns:

  • Mock data correctly implements the Project interface with good variety (nullable fields tested).
  • Pagination metadata dynamically derives total from mock data length.
  • Both handlers use the apiSuccess/apiError helpers for consistent API envelope.
  • The detail handler correctly returns 404 with proper status code for missing projects.
  • Type safety is maintained throughout (string ID comparison is correct per Project.id type).
web/src/stores/projects.ts (1)

60-68: Stale-response protection is well implemented here.

The monotonic request tokens plus clearDetail invalidation correctly prevent older async responses from overwriting newer state.

Also applies to: 73-81, 123-130

web/src/pages/artifacts/ArtifactContentPreview.tsx (3)

17-24: getLanguage fallback to 'json' for non-JSON/YAML text types may cause incorrect highlighting.

This was noted in a prior review but remains unaddressed. For text/plain, text/markdown, text/html, etc., the function returns 'json', which applies JSON syntax highlighting to non-JSON content. Consider adding more content-type mappings or a neutral fallback if CodeMirror supports one.


1-39: LGTM — Component structure and hook usage are correct.

The component properly:

  • Uses useCallback for handleDownload before early returns (addressing prior feedback)
  • Manages blob URL lifecycle with proper cleanup in useEffect
  • Correctly excludes SVG to prevent XSS risks
  • Reuses existing UI components (SectionCard, Skeleton, EmptyState, LazyCodeMirrorEditor)

42-65: Blob URL lifecycle management is well-implemented.

The useEffect correctly:

  • Sets revoked flag to prevent state updates after unmount
  • Resets imageSrc and imageError on cleanup
  • Revokes the object URL via imageSrcRef to prevent memory leaks
  • Dependencies array includes all relevant values
web/src/stores/artifacts.ts (3)

51-57: Stale-response protection is well-implemented.

The monotonic request token pattern correctly handles race conditions for both list and detail fetches. Each async operation captures a token at start and checks isStaleDetailRequest/isStaleListRequest before applying state updates.


90-115: Detail fetch correctly renders metadata before preview loads.

The implementation:

  1. Clears stale state at start (line 92)
  2. Sets selectedArtifact and detailLoading: false immediately after metadata arrives (line 98)
  3. Fetches content preview in background with proper stale-request guards
  4. Partial failures set detailError while keeping metadata visible

This addresses prior feedback about blocking the detail page on preview downloads.


117-129: Delete correctly clears selected artifact state when deleting the viewed artifact.

The implementation checks isSelected and conditionally clears selectedArtifact, contentPreview, and detailError. This addresses prior feedback about stale detail data after deletion.

web/src/pages/artifacts/ArtifactMetadata.tsx (2)

21-44: LGTM — Component follows design system and coding guidelines.

The implementation correctly:

  • Reuses existing UI components (SectionCard, MetadataGrid, ContentTypeBadge, Button, ConfirmDialog)
  • Uses semantic Tailwind classes (text-danger, hover:bg-danger/10, text-muted-foreground, font-mono)
  • Properly formats metadata values with appropriate utilities

Based on learnings: "Always reuse existing components from web/src/components/ui/ before creating new ones."


46-62: Delete handler correctly manages async flow and error states.

The implementation:

  1. Sets deleting: true before async operation
  2. Properly catches errors and shows toast with getErrorMessage
  3. Uses finally to reset state regardless of outcome
  4. Navigates away only on success

Comment on lines +77 to +88
fetchArtifacts: async () => {
const token = ++_listRequestToken
set({ listLoading: true, listError: null })
try {
const result = await listArtifacts({ limit: 200 })
if (isStaleListRequest(token)) return
set({ artifacts: result.data, totalArtifacts: result.total, listLoading: false })
} catch (err) {
if (isStaleListRequest(token)) return
set({ listLoading: false, listError: getErrorMessage(err) })
}
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the listArtifacts API endpoint supports filter parameters
rg -n "listArtifacts" --type=ts -C3

Repository: Aureliolo/synthorg

Length of output: 4750


🏁 Script executed:

# Check ArtifactFilters type definition
rg -n "ArtifactFilters" --type=ts -A5

Repository: Aureliolo/synthorg

Length of output: 3182


🏁 Script executed:

# Check the full store implementation for filter state
cat -n web/src/stores/artifacts.ts | head -100

Repository: Aureliolo/synthorg

Length of output: 4092


🏁 Script executed:

# Check how filters are managed in the UI or other parts
rg -n "searchQuery|typeFilter|createdByFilter" web/src --type=ts -B2 -A2

Repository: Aureliolo/synthorg

Length of output: 26914


🏁 Script executed:

# Check the full useArtifactsData implementation to understand filtering flow
cat -n web/src/hooks/useArtifactsData.ts

Repository: Aureliolo/synthorg

Length of output: 4180


🏁 Script executed:

# Check if there's pagination logic elsewhere or if offset is used
rg -n "offset|pagination" web/src/stores/artifacts.ts web/src/hooks/useArtifactsData.ts --type=ts

Repository: Aureliolo/synthorg

Length of output: 44


Store does not forward active filters to the backend API — only first 200 artifacts are fetched.

fetchArtifacts calls listArtifacts({ limit: 200 }) without passing filter parameters (type, created_by, task_id, content_type, project_id). Although the API supports these filters, they are applied only client-side in useArtifactsData after fetch, limiting search results to whatever is in the 200-artifact set. Users cannot access matching artifacts beyond position 200 even though totalArtifacts indicates they exist on the backend.

Modify fetchArtifacts to forward active store filters to the API call, or implement pagination with filters if backend constraints require it.

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

In `@web/src/stores/artifacts.ts` around lines 77 - 88, fetchArtifacts currently
calls listArtifacts({ limit: 200 }) and ignores store filter state, causing only
the first 200 artifacts to be fetched; update fetchArtifacts to read the active
filter fields from the store (e.g., type, created_by, task_id, content_type,
project_id) and pass them into listArtifacts alongside limit (or implement
paginated requests that include these filters) so the back-end applies filtering
correctly; ensure you still use _listRequestToken and isStaleListRequest as
before and set listLoading/listError/artifacts/totalArtifacts from the API
response.

Comment on lines +148 to +152
// Event payload ignored -- all events trigger a full refetch.
// Incremental updates are not worth the complexity given 30s polling.
updateFromWsEvent: () => {
useArtifactsStore.getState().fetchArtifacts()
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider awaiting fetchArtifacts() or handling errors in WS event handler.

updateFromWsEvent calls fetchArtifacts() without awaiting it or catching errors. While this is fire-and-forget by design (per the comment), any fetch failure will silently set listError without notification. If the WS event indicates new data but the refetch fails, the user sees stale data with no indication of the problem.

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

In `@web/src/stores/artifacts.ts` around lines 148 - 152, The WebSocket handler
updateFromWsEvent is calling fetchArtifacts() fire-and-forget so network
failures are swallowed; make updateFromWsEvent async, await
useArtifactsStore.getState().fetchArtifacts(), and wrap the await in a try/catch
that handles errors (e.g., set the store's listError or call the app's
error/notification helper and console.error) so a failed refetch updates state
or surfaces a notification instead of silently leaving stale data.

Prevents reopening the ConfirmDialog while a delete operation
is in progress by adding disabled={deleting} to the trigger button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 1, 2026 06:38 — with GitHub Actions Inactive
@Aureliolo Aureliolo merged commit 00a0430 into main Apr 1, 2026
32 of 34 checks passed
@Aureliolo Aureliolo deleted the feat/artifact-project-ui branch April 1, 2026 06:53
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 1, 2026 06:54 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Apr 1, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.5.4](v0.5.3...v0.5.4)
(2026-04-01)


### Features

* artifact and project management UI in web dashboard
([#954](#954))
([00a0430](00a0430))
* embed MkDocs build output in React web dashboard at /docs
([#948](#948))
([f229fc2](f229fc2))
* personality preset discovery API and user-defined preset CRUD
([#952](#952))
([497848a](497848a))
* support multi-provider model resolution with budget-based selection
([#953](#953))
([146b782](146b782))
* support per-agent memory retention overrides
([#209](#209))
([#951](#951))
([020c610](020c610))


### Documentation

* write user guides and tutorials
([#949](#949))
([1367225](1367225))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.

feat: artifact and project management UI in web dashboard

2 participants