Skip to content

feat(visual-systems): add per-project visual system configuration#247

Merged
mikkisguy merged 7 commits into
mainfrom
25-visual-system-config
Jun 21, 2026
Merged

feat(visual-systems): add per-project visual system configuration#247
mikkisguy merged 7 commits into
mainfrom
25-visual-system-config

Conversation

@mikkisguy

@mikkisguy mikkisguy commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Closes #25

What

Adds per-project visual system configuration: GET/PUT endpoints,
service layer, and a frontend dialog for editing the config that
controls how generated Ren'Py visual filenames are produced
(template tokens, group prefixes, padding, shared jump prefix,
placeholder base URL).

How

Backend (Fastify + Drizzle)

  • GET /api/projects/:projectId/visual-system returns the config;
    auto-creates a default row on first read.
  • PUT /api/projects/:projectId/visual-system PATCHes a subset
    of fields. Sentinels ("", {}) clear optional fields back
    to NULL.
  • New Zod visualSystemConfigSchema with http/https restriction
    on placeholderBaseUrl and explicit clearable semantics for
    defaultGroupType, placeholderBaseUrl, and groupPrefixes.
  • New VisualSystemsService (default-on-first-read + PATCH
    upsert) enforces requireProjectOwnership.
  • Integration tests: 18 cases covering auth, validation,
    clearing, groupPrefixes JSONB round-trip, default creation,
    non-http protocol rejection, unknown-field rejection.

Frontend (React + TanStack Query)

  • API client, useVisualSystem hook (optimistic update with
    rollback), and VisualSystemDialog with a live preview that
    feeds the current form into generateVisualName().
  • New query keys under visualSystemKeys.

DB mapping

  • DB column scene_padding is mapped to the shared API field
    labelPadding in the service layer (legacy column kept for
    backward compat with the existing schema/migration).

How verified

  • pnpm typecheck — clean
  • pnpm lint — clean
  • pnpm test:unit — 1606 tests pass (no regressions)
  • pnpm test:integration — 410 tests pass (18 new for visual
    systems, no regressions)

@oracle verdict

Verdict: ready (after one round of fixes).

Round 1 review surfaced 3 must-fix + 2 should-fix + 1 nit.
All addressed:

  • Dialog now always sends all clearable fields (no more
    un-clearable optional values).
  • defaultGroupType schema accepts "" as clear sentinel.
  • placeholderBaseUrl schema restricts to http/https via
    custom .refine().
  • Service's defensive in-memory fallback now logs a warning
    before returning defaults.
  • 3 new integration tests cover the clearing paths and the
    non-http protocol rejection.

Round 2 review returned "Verdict: ready".

Summary by CodeRabbit

Release Notes

  • New Features
    • Added per-project Visual System configuration (GET/PUT) with automatic defaults, partial updates, and clear/normalize behavior.
    • Added Visual System editing inside the unified Project Settings modal, with live preview and validation.
  • UI Improvements
    • Unified project settings entry with tabbed navigation; added reusable tab UI controls.
    • Added optional 1/2-column layouts for character and route lists.
    • Improved dialog accessibility and centering.
  • Tests
    • Added Visual System GET/PUT integration tests covering auth/ownership and validation/clearing rules.
    • Updated CharacterDialog close-button test selector.

Implements GitHub issue #25. Visual system config controls how
generated Ren'Py visual filenames are produced (template tokens,
group prefixes, padding, shared jump prefix, placeholder base URL).

Backend
- GET /api/projects/:projectId/visual-system returns the config,
  auto-creating a default row on first read.
- PUT /api/projects/:projectId/visual-system PATCHes a subset of
  fields. Sentinels ("", {}) clear optional fields back to NULL.
- New Zod schema 'visualSystemConfigSchema' with http/https
  restriction on placeholderBaseUrl and clearable semantics for
  defaultGroupType, placeholderBaseUrl, and groupPrefixes.
- New 'VisualSystemsService' with default-on-first-read and
  PATCH upsert. Enforces requireProjectOwnership.
- Integration tests: 18 cases covering auth, validation,
  clearing, groupPrefixes JSONB round-trip, default creation.

Frontend
- New API client, TanStack Query hook (optimistic update with
  rollback), and VisualSystemDialog with a live preview that
  feeds the current form into generateVisualName().
- New query keys under visualSystemKeys.

Closes #25
@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
branchforge-docs Ready Ready Preview, Comment Jun 21, 2026 4:01pm

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Implements end-to-end visual system configuration for the BranchForge project: a Zod validation schema with empty-string clearing semantics, a VisualSystemsService with idempotent default-row creation and ownership enforcement, Fastify GET/PUT routes, a full integration test suite, a React VisualSystemDialog backed by a TanStack Query hook with optimistic updates, and a new Tabs UI primitive. Additionally, refactors the settings dialogs to consolidate route and character configuration into a unified ProjectSettingsDialog with tabbed navigation.

Changes

Visual System Configuration and Settings UI Consolidation

Layer / File(s) Summary
Validation schema and defaults
apps/backend/src/lib/validation.ts
Introduces visualSystemConfigSchema with strict/partial PATCH semantics, empty-string sentinel handling for clearing optional fields, URL protocol enforcement, and exports VISUAL_SYSTEM_CONFIG_DEFAULTS plus the inferred VisualSystemConfigInput type.
VisualSystemsService: mapping, read, write
apps/backend/src/services/visual-systems.service.ts
Implements DB-to-wire shape mapping (scenePaddinglabelPadding), idempotent default-row creation on GET via onConflictDoNothing, clearable-field normalization to null, partial-payload upsert on PUT with conflict targeting, and singleton export.
Fastify routes and server registration
apps/backend/src/routes/visual-systems.routes.ts, apps/backend/src/index.ts
Defines GET and PUT handlers with authenticate/validateParams/validateBody middleware at /projects/:projectId/visual-system and registers visualSystemsRoutes in the Fastify startup sequence.
Integration tests: auth, validation, defaults, patching
apps/backend/src/routes/__tests__/visual-systems.routes.integration.test.ts
Provides test harness with DB seeding, session helpers, and comprehensive coverage for GET/PUT auth/ownership/validation, default-row creation, legacy field mapping, selective patching, JSON persistence, and clearing semantics.
Frontend API client, query keys, and hook
apps/frontend/src/lib/api/visual-systems.ts, apps/frontend/src/lib/query-keys.ts, apps/frontend/src/hooks/useVisualSystem.ts
Adds visualSystemsApi with GET/PUT methods, visualSystemKeys query factory, and useVisualSystem hook with 5-minute stale query, optimistic cache merge, snapshot/rollback on error, toast notifications, and cache invalidation on settle.
Form state helpers and JSON parser
apps/frontend/src/components/visual-system.helpers.ts
Introduces VisualSystemFormState interface, INITIAL_VISUAL_SYSTEM_FORM defaults, config-to-form state mapping, and parseGroupPrefixes() with JSON parsing and structure validation.
VisualSystemDialog with validation and preview
apps/frontend/src/components/VisualSystemDialog.tsx
Implements the complete dialog form with config hydration, per-field validation (required fields, padding range, URL protocol), live preview via generateVisualName(), and save handler using groupPrefixes: parsed.value ?? {} for clearing semantics.
Tabs UI primitive
apps/frontend/src/components/ui/tabs.tsx
Implements a lightweight custom Tabs component (no Radix dependency) with Tabs, TabsList, TabsTrigger, and TabsPanel supporting controlled/uncontrolled state, ARIA accessibility, tab registration, keyboard navigation, and focus management.
Character and route settings content extraction
apps/frontend/src/components/CharacterSettingsContent.tsx, apps/frontend/src/components/RouteSettingsContent.tsx
Extracts loading/error/empty/list UI and edit-state orchestration into dedicated reusable components; extends list components with optional columns?: 1 | 2 layout prop.
Simplify character and route dialogs
apps/frontend/src/components/CharacterDialog.tsx, apps/frontend/src/components/ide-shared/RouteSettingsDialog.tsx
Refactors both dialogs into thin DialogShell wrappers delegating content rendering to extracted CharacterSettingsContent and RouteSettingsContent, removing all inlined edit-state logic.
ProjectSettingsDialog with tabbed navigation
apps/frontend/src/components/ProjectSettingsDialog.tsx
Creates a unified modal with Characters, Routes, and Visual System tabs; delegates first two to extracted content components and inlines visual system form with useVisualSystem and group-prefix JSON parsing.
LeftSidebar navigation consolidation
apps/frontend/src/components/ide-shared/LeftSidebar.tsx
Replaces separate routes and characters modal keys with unified projectSettings key; adds "Project Settings" navigation entry; refactors navigation/theme UI into subcomponents with encapsulated click-outside dismissal; wires modal dispatcher.
Dialog element improvements
apps/frontend/src/components/ui/dialog.tsx
Updates native Dialog centering from m-auto to explicit top/left + translate positioning and adds support for aria-label and aria-labelledby props.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ProjectSettingsDialog as ProjectSettingsDialog<br/>(Visual System tab)
  participant useVisualSystem
  participant visualSystemsApi
  participant FastifyRoute as Fastify Route<br/>(PUT /visual-system)
  participant VisualSystemsService
  participant DB as visualSystems table

  User->>ProjectSettingsDialog: opens dialog, Visual System tab
  ProjectSettingsDialog->>useVisualSystem: config, isLoading, updateConfig
  useVisualSystem->>visualSystemsApi: getVisualSystemConfig(projectId)
  visualSystemsApi->>FastifyRoute: GET /projects/:projectId/visual-system
  FastifyRoute->>VisualSystemsService: getVisualSystemConfig(projectId, userId)
  VisualSystemsService->>DB: insert defaults onConflictDoNothing
  VisualSystemsService->>DB: select row
  DB-->>VisualSystemsService: row or null
  VisualSystemsService-->>FastifyRoute: VisualSystemConfigResult
  FastifyRoute-->>visualSystemsApi: 200 VisualSystemConfig
  visualSystemsApi-->>useVisualSystem: config
  useVisualSystem-->>ProjectSettingsDialog: hydrated config

  User->>ProjectSettingsDialog: edits fields and clicks Save
  ProjectSettingsDialog->>ProjectSettingsDialog: trim fields, parseGroupPrefixes
  ProjectSettingsDialog->>useVisualSystem: updateConfig(patch)
  useVisualSystem->>useVisualSystem: optimistic cache merge
  useVisualSystem->>visualSystemsApi: updateVisualSystemConfig(projectId, body)
  visualSystemsApi->>FastifyRoute: PUT /projects/:projectId/visual-system
  FastifyRoute->>VisualSystemsService: updateVisualSystemConfig(projectId, userId, input)
  VisualSystemsService->>VisualSystemsService: normalize clearable fields to null
  VisualSystemsService->>DB: upsert onConflictDoUpdate
  DB-->>VisualSystemsService: updated row
  VisualSystemsService-->>FastifyRoute: VisualSystemConfigResult
  FastifyRoute-->>visualSystemsApi: 200 VisualSystemConfig
  visualSystemsApi-->>useVisualSystem: updated config
  useVisualSystem->>useVisualSystem: show success toast, invalidate cache
  useVisualSystem-->>ProjectSettingsDialog: updated config
  ProjectSettingsDialog->>User: closes dialog
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly Related PRs

  • mikkisguy/branchforge#129: Both PRs modify the shared apps/frontend/src/components/ui/dialog.tsx Dialog component to improve interaction and accessibility behavior (main PR adds aria-label/aria-labelledby support and styling improvements).
  • mikkisguy/branchforge#132: The main PR's refactor of apps/frontend/src/components/ide-shared/LeftSidebar.tsx (swapping individual dialogs for a unified ProjectSettingsDialog modal state machine) is code-level related to this PR's modal state consolidation.
  • mikkisguy/branchforge#178: Both PRs refactor CharacterDialog to delegate content rendering, with the main PR completing the refactor by introducing CharacterSettingsContent extraction.

Poem

🐇 Hop hop, the visual names align,
A schema strict with padding just right,
The service seeds defaults so fine,
Optimistic cache jumps through the night—
The dialog blooms with preview bright,
Tabs consolidate the settings view,
Every field validated true! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Most changes directly support the visual system feature (#25). However, CharacterDialog/CharacterList refactoring, RouteList column support, Tabs component implementation, ProjectSettingsDialog unification, and LeftSidebar restructuring appear to be out-of-scope UI improvements unrelated to visual system configuration objectives. Isolate visual system-specific changes into a separate PR, or document why the refactoring (dialog consolidation, Tabs component, layout improvements) is necessary for visual system feature completion and move that documentation to the PR description.
Docstring Coverage ⚠️ Warning Docstring coverage is 52.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding per-project visual system configuration capability. It reflects the core objective of implementing visual system CRUD functionality.
Linked Issues check ✅ Passed The PR implementation comprehensively addresses all acceptance criteria from issue #25: GET endpoint retrieves config with auto-default creation, PUT endpoint handles partial updates with field clearing, frontend UI with VisualSystemDialog and useVisualSystem hook is provided, and generateVisualName() integration is implemented with live preview.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 25-visual-system-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

React Doctor found 1 issue in 1 file · 1 warning · score 95 / 100 (Great) · vs main

1 warning

src/components/ui/tabs.tsx

  • ⚠️ L257 Data passed to parent via effect no-pass-data-to-parent

Reviewed by React Doctor for commit cdf1c85. See inline comments for fixes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/src/lib/validation.ts`:
- Around line 847-851: The jumpPrefixShared field validation requires a minimum
length of 1 character but the default configuration returns an empty string for
this field, creating a validation conflict that breaks config round-tripping.
Fix the jumpPrefixShared validation schema to allow empty strings by removing
the min(1) constraint, changing it to .optional().default(''), or updating the
default configuration to provide a non-empty value that satisfies the validation
rules.

In `@apps/backend/src/routes/__tests__/visual-systems.routes.integration.test.ts`:
- Around line 314-606: Add a regression test within the "PUT
/projects/:projectId/visual-system" describe block that explicitly verifies the
PUT endpoint accepts an empty string for jumpPrefixShared and properly handles
it. The test should follow the pattern of existing tests like "treats empty
placeholderBaseUrl as null" and "clears defaultGroupType when set to an empty
string" by first seeding a visual system row with a non-empty jumpPrefixShared
value, then sending a PUT request with jumpPrefixShared set to an empty string,
and finally asserting both the response and the database state to confirm the
value is properly stored and round-tripped.

In `@apps/frontend/src/components/VisualSystemDialog.tsx`:
- Around line 126-136: The validation loop iterating through groupPrefixes
entries checks if keys and values are empty strings using length checks, but
doesn't account for whitespace-only strings which pass locally but fail
server-side validation after trimming. In the validation block starting with the
for loop over Object.entries, trim both the key and value before checking their
lengths. Replace the condition checking k.length === 0 with k.trim().length ===
0, and the condition checking v.length === 0 with v.trim().length === 0, to
ensure whitespace-only entries are caught during client-side validation and
match the server-side behavior.
- Around line 164-170: The URL protocol validation in the placeholderBaseUrl
validation block uses startsWith("http") which is too permissive and allows
invalid protocols that would fail backend validation. Update the condition
checking url.protocol to explicitly validate that the protocol is exactly
"http:" or "https:" rather than just checking if it starts with "http", ensuring
client-side validation matches the backend rules exactly.

In `@apps/frontend/src/lib/api/visual-systems.ts`:
- Line 9: The import statement for request in the visual-systems.ts file uses a
relative path `./client` instead of the frontend alias convention. Change the
import statement to use the `@/lib/api/client` path instead of the relative
`./client` path to maintain consistency with the frontend import conventions
established in this codebase.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c652edb8-63ea-4861-a982-99acd9e42d0f

📥 Commits

Reviewing files that changed from the base of the PR and between 59bc591 and f16055a.

📒 Files selected for processing (9)
  • apps/backend/src/index.ts
  • apps/backend/src/lib/validation.ts
  • apps/backend/src/routes/__tests__/visual-systems.routes.integration.test.ts
  • apps/backend/src/routes/visual-systems.routes.ts
  • apps/backend/src/services/visual-systems.service.ts
  • apps/frontend/src/components/VisualSystemDialog.tsx
  • apps/frontend/src/hooks/useVisualSystem.ts
  • apps/frontend/src/lib/api/visual-systems.ts
  • apps/frontend/src/lib/query-keys.ts

Comment thread apps/backend/src/lib/validation.ts Outdated
Comment thread apps/frontend/src/components/VisualSystemDialog.tsx Outdated
Comment thread apps/frontend/src/components/VisualSystemDialog.tsx
Comment thread apps/frontend/src/lib/api/visual-systems.ts
Four follow-up fixes from inline review of #247:

1. Backend: visualSystemConfigSchema.jumpPrefixShared now accepts the
   empty string (via .or(z.literal(''))) so the default value of ''
   in VISUAL_SYSTEM_CONFIG_DEFAULTS can be round-tripped through a
   PUT without a 400. The DB column is NOT NULL and the shared
   VisualSystemConfig type declares the field as required, so ''
   is the natural cleared/default value (not NULL).
2. Integration test: regression test seeds a non-empty
   jumpPrefixShared, PATCHes with '', and asserts both response and
   DB row read back ''.
3. Frontend: parseGroupPrefixes now trims keys/values before the
   empty-length check, matching the server's .trim().min(1)
   validation. A whitespace-only entry used to pass client-side but
   fail server-side.
4. Frontend: placeholderBaseUrl protocol check changed from
   startsWith('http') to exact === 'http:' || === 'https:' to match
   the server-side check. The startsWith form would let
   'httpa://example.com' and similar bogus schemes through.
Replaces the separate Routes / Characters sidebar entries in the
left sidebar with a single 'Settings' entry that opens a unified
tabbed modal. The tab order is Characters → Routes → Visual
System. Tabs are owned by the outer dialog; each tab manages its
own persistence. Inner edit dialogs (for Characters and Routes)
stack on top as dialog-over-dialog, matching the pattern the
user prefers over inline editing.

What changed
- New 'ProjectSettingsDialog' with a tab strip hosting
  Characters, Routes, and Visual System.
- New 'tabs' primitive (state-based, no Radix dep).
- New 'CharacterSettingsContent' and 'RouteSettingsContent'
  components extracted from the existing dialogs.
- New 'visual-system.helpers' shared by the standalone dialog and
  the tab (parseGroupPrefixes, toVisualSystemFormState,
  INITIAL_VISUAL_SYSTEM_FORM).
- 'VisualSystemFormContent' is now exported and used directly
  inside the visual tab (no nested dialog) so Cancel/Save work
  as expected.
- 'CharacterList' and 'RouteList' accept a 'columns' prop
  (default 1; pass 2 in the tab to keep the dialog frame
  height stable).
- 'ProjectSettingsDialog' uses a fixed h-[80vh] min-h-[500px]
  so the dialog frame is the same size across all three tabs;
  content scrolls inside.
- 'Dialog' primitive now uses top-1/2 left-1/2 transform for
  reliable centering. The previous m-auto approach didn't
  reliably center a fixed-height dialog (especially when nested
  inside another open dialog).
- Visual System form: Cancel button removed. Save persists;
  the dialog's X / outer Close button discards unsaved changes.
- LeftSidebar: Routes and Characters buttons replaced with one
  'Settings' button (SlidersHorizontal icon) that opens the
  unified modal.

Verified
- pnpm typecheck, lint clean.
- 815 frontend + 747 backend unit tests pass.
- Dialog visually centered in the viewport (was anchored to
  top before the transform fix).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/frontend/src/components/CharacterDialog.tsx`:
- Around line 48-54: The close button element in the CharacterDialog component
lacks an accessible name for screen readers since it only contains an icon (X
component). Add an aria-label attribute to the button element with an
appropriate label like "Close" to provide screen readers with a descriptive name
for the control, making the close button accessible to all users.

In `@apps/frontend/src/components/CharacterSettingsContent.tsx`:
- Around line 11-17: The imports for CharacterList and CharacterEditDialog.lazy
in the CharacterSettingsContent.tsx file are using relative import paths
(./CharacterList and ./CharacterEditDialog.lazy) instead of the required `@/`
alias convention. Replace these two relative imports with their `@/` equivalents:
change import { CharacterList } from "./CharacterList" to import { CharacterList
} from "`@/components/CharacterList`" and change import { CharacterEditDialog }
from "./CharacterEditDialog.lazy" to import { CharacterEditDialog } from
"`@/components/CharacterEditDialog.lazy`" to align with the project's import
conventions.

In `@apps/frontend/src/components/ProjectSettingsDialog.tsx`:
- Around line 204-212: The condition `if (isLoading || !config)` in the
ProjectSettingsDialog component shows the loader indefinitely when the config
fetch fails, leaving users without recovery options. Instead of checking only
for `!config`, also check for a query error state (from your data fetching hook)
and display an error message or retry UI when the fetch fails. Replace the
current condition with logic that distinguishes between the loading state (show
spinner) and the error state (show error UI with retry option), ensuring the
spinner only displays while actively fetching and not when the request has
failed.

In `@apps/frontend/src/components/ui/tabs.tsx`:
- Around line 153-163: The tab button component currently lacks keyboard
navigation support for arrow keys, preventing keyboard users from moving between
tabs. Add an onKeyDown event handler to the button element that detects
ArrowLeft and ArrowRight key presses and calls setValue with the appropriate
adjacent tab value. The handler should determine the next or previous tab value
based on the key pressed and update the active tab accordingly, allowing
keyboard users to navigate between tabs using arrow keys while maintaining the
existing onClick handler for mouse interactions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 24a91e03-10cf-4325-a4cc-0aa6d1e3bb67

📥 Commits

Reviewing files that changed from the base of the PR and between 1689ee3 and 6b61f73.

📒 Files selected for processing (12)
  • apps/frontend/src/components/CharacterDialog.tsx
  • apps/frontend/src/components/CharacterList.tsx
  • apps/frontend/src/components/CharacterSettingsContent.tsx
  • apps/frontend/src/components/ProjectSettingsDialog.tsx
  • apps/frontend/src/components/RouteList.tsx
  • apps/frontend/src/components/RouteSettingsContent.tsx
  • apps/frontend/src/components/VisualSystemDialog.tsx
  • apps/frontend/src/components/ide-shared/LeftSidebar.tsx
  • apps/frontend/src/components/ide-shared/RouteSettingsDialog.tsx
  • apps/frontend/src/components/ui/dialog.tsx
  • apps/frontend/src/components/ui/tabs.tsx
  • apps/frontend/src/components/visual-system.helpers.ts

Comment thread apps/frontend/src/components/CharacterDialog.tsx
Comment thread apps/frontend/src/components/CharacterSettingsContent.tsx
Comment thread apps/frontend/src/components/ProjectSettingsDialog.tsx
Comment thread apps/frontend/src/components/ui/tabs.tsx
- ProjectSettingsDialog: drop the useEffect that reset activeTab on
  open/defaultTab change. Use the render-time 'storing information
  from previous renders' pattern instead, so the user doesn't see a
  one-frame flash of the previous tab on reopen. Add aria-label to
  the Dialog.

- VisualSystemDialog: same render-time pattern replaces the
  useEffect that re-hydrated the form when initialConfig changed.
  Add aria-label to the Dialog and to the group-prefixes textarea
  (defensive — the wrapping <Label htmlFor> wasn't being detected
  by react-doctor's static analysis).

- dialog.tsx: add optional 'aria-label' and 'aria-labelledby' props
  on the native <dialog> so callers can give screen readers an
  accessible name (required by react-doctor/dialog-has-accessible-name).

- tabs.tsx: replace useContext with React 19's use() in the tabs
  context consumer (no-react19-deprecated-apis).

- LeftSidebar: extract ModeSwitcher, ProjectSelector, NavButtons,
  ThemeSwitcher, CollapseButton, and UserActions sub-components to
  shrink the main function from 477 to 220 lines (no-giant-component).
  Move each popover's click-outside handler into its sub-component
  and wrap onClose in useEffectEvent so the listener isn't re-bound
  on every parent render.

- The remaining react-doctor hits on the render-time 'previous value'
  trackers are false positives (the state IS read in the if-condition,
  not just in handlers) and the useRef alternative is blocked by the
  react-hooks/refs eslint rule. Suppressed inline with the
  react-doctor-disable-next-line pattern already used elsewhere in
  the codebase.

react-doctor: 82 -> 100/100 (no issues on changed files).
Three of the four inline findings were still valid; one was
skipped (relative imports — matches the established convention
for sibling files in this codebase, same reason it was rejected
for visual-systems.ts earlier).

1. CharacterDialog: add aria-label="Close character dialog" to
   the X close button so screen readers announce the control.
   Updated the 'Dialog Controls' test to use an exact-name match
   for the footer Close button (the X button now also matches
   'Close' in its accessible name).

2. useVisualSystem: expose isError on the hook return so callers
   can distinguish a failed fetch from a still-loading one. The
   visual tab in ProjectSettingsDialog now renders an error UI
   (icon + message + Retry button) before falling through to the
   spinner; previously a failed fetch left the spinner spinning
   forever.

3. tabs: add WAI-ARIA arrow-key navigation. The Tabs root
   tracks registered tab values via a registerTab callback (in
   insertion order); each TabsTrigger registers itself on mount
   and handles ArrowLeft / ArrowRight (wraps), Home, and End.
   Focus follows the active tab via data-tab-value lookup.

Plus a small sidebar cleanup: the Settings button now reads
'Project Settings' (was just 'Settings' — ambiguous next to the
user-level Settings gear), and the Flow button now reads
'Flow Graph' (matches its tooltip and icon).

Verified
- pnpm typecheck, lint clean.
- 815 frontend + 747 backend unit tests pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/frontend/src/components/ide-shared/LeftSidebar.tsx`:
- Around line 213-225: The useEffect hook that sets up the click-outside handler
has an early return condition that checks for both !isOpen and isCollapsed, but
since the popovers are only rendered when isCollapsed is true, this prevents the
click-outside dismissal handler from being registered in the only mode where
it's needed. Remove the isCollapsed check from the early return condition on
line 214 so that the handler is set up whenever isOpen is true, regardless of
collapsed state. Apply the same fix to the similar code block around lines
388-400.

In `@apps/frontend/src/components/ui/tabs.tsx`:
- Line 200: The tab registration and keyboard navigation logic in this component
has two issues: first, the useEffect that calls registerTab(value) registers all
tabs including disabled ones, which allows arrow key navigation to activate
disabled tabs via the logic in the arrow key handlers; second, the document
query on lines 233-235 is global and not scoped to the current tablist instance,
causing the wrong tab to be focused when multiple tablists exist with the same
value. To fix this, add a check in the useEffect to only register the tab if it
is not disabled, modify the arrow key navigation handlers (around lines 205-213
and 227-230) to skip over disabled tabs when cycling through tab values, and
scope the document query to the current tablist DOM element or container instead
of querying globally on document.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 727a1fe5-9b8b-4cf4-8221-56591b06eb3c

📥 Commits

Reviewing files that changed from the base of the PR and between 6b61f73 and 9d48051.

📒 Files selected for processing (8)
  • apps/frontend/src/components/CharacterDialog.tsx
  • apps/frontend/src/components/ProjectSettingsDialog.tsx
  • apps/frontend/src/components/VisualSystemDialog.tsx
  • apps/frontend/src/components/__tests__/CharacterDialog.test.tsx
  • apps/frontend/src/components/ide-shared/LeftSidebar.tsx
  • apps/frontend/src/components/ui/dialog.tsx
  • apps/frontend/src/components/ui/tabs.tsx
  • apps/frontend/src/hooks/useVisualSystem.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/frontend/src/components/tests/CharacterDialog.test.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/frontend/src/components/ProjectSettingsDialog.tsx
  • apps/frontend/src/hooks/useVisualSystem.ts
  • apps/frontend/src/components/CharacterDialog.tsx
  • apps/frontend/src/components/VisualSystemDialog.tsx

Comment thread apps/frontend/src/components/ide-shared/LeftSidebar.tsx Outdated
Comment thread apps/frontend/src/components/ui/tabs.tsx Outdated
…ollow-up

The CodeRabbit follow-up commit (9d48051) added WAI-ARIA arrow-key
navigation to the tabs primitive, which:
  - re-introduced useContext (reverting the React 19 use() switch)
  - added two useRef(new Map/Set) initializers
  - added a child->parent registerTab effect

The useRef new-Map/Set and registerTab-effect patterns trigger
react-doctor rules. Both are intentional:

  - useRef(new Map()) / useRef(new Set()): the rebuild cost is
    negligible for an empty Map/Set, and the alternative lazy-init
    pattern forces non-null assertions at every call site inside
    the closures that follow. Suppressed inline.

  - useEffect(registerTab): child->parent registration is the
    standard pattern for tab enumeration; React has no other
    mechanism for a parent to know about TabsTrigger children
    statically. Suppressed inline.

Also picks up a pre-existing working-tree fix in LeftSidebar: the
click-outside effect's early-return used the inverted condition
`if (!isOpen || isCollapsed) return;`, which registered the
listener only in the mode that has no popover to dismiss. The
popover is rendered only in the collapsed branch, so the listener
should be (and now is) gated on `isOpen` alone.

react-doctor: 90 -> 100/100 on changed files.
typecheck, lint, 815 frontend tests all green.
Comment thread apps/frontend/src/components/ui/tabs.tsx
@mikkisguy mikkisguy merged commit 1baf1a2 into main Jun 21, 2026
11 checks passed
@mikkisguy mikkisguy deleted the 25-visual-system-config branch June 21, 2026 16:01
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.

Implement visual system configuration

1 participant