Skip to content

feat: Setup Wizard rebuild with template comparison, cost estimator, theme customization#879

Merged
Aureliolo merged 13 commits intomainfrom
feat/setup-wizard-rebuild
Mar 27, 2026
Merged

feat: Setup Wizard rebuild with template comparison, cost estimator, theme customization#879
Aureliolo merged 13 commits intomainfrom
feat/setup-wizard-rebuild

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • 7-step setup wizard: Account (conditional) -> Template -> Company -> Agents -> Providers -> Theme -> Complete
  • Template selection with category grouping and side-by-side comparison drawer (max 3)
  • Cost estimator with tier-based fallback pricing and usedFallback indicator
  • Currency selector (EUR default) with optional budget cap toggle
  • Template variable controls (sliders for numeric, toggles for boolean vars)
  • Agent customization: inline-edit names, model picker, randomize button, mini SVG org chart
  • Provider setup: auto-detect local providers first, then manual cloud add with connection testing
  • Theme step: 5-axis customization (palette, density, animation, sidebar, typography) with live mini-dashboard preview
  • Skip wizard option for power users (company name + account only)
  • 5 new shared UI components: Drawer, InputField, SelectField, SliderField, ToggleField (all with Storybook stories)
  • New useSetupWizardStore Zustand store with proper error handling (getErrorMessage + console.error)
  • 747 tests passing (13 new test files, including fast-check property tests)
  • Documentation updated: CLAUDE.md component table, brand-and-ux.md inventory, page-structure.md route table, README.md

Closes #783
Closes #238
Closes #726
Closes #728

Test plan

  • Type-check: npm --prefix web run type-check (passing)
  • Lint: npm --prefix web run lint (0 errors)
  • Tests: npm --prefix web run test (747 passing, 70 files)
  • Storybook: npm --prefix web run storybook -- verify new InputField, SelectField, SliderField, ToggleField, Drawer stories
  • Manual: Navigate to /setup/template and walk through all 7 steps
  • Manual: Test "Skip wizard" link on template step
  • Manual: Test invalid URL /setup/invalid redirects to first step
  • Manual: Test back/forward navigation and step locking

Review coverage

Pre-reviewed by 5 agents (frontend-reviewer, docs-consistency, issue-resolution-verifier, test-coverage-analyzer, silent-failure-hunter). 21 findings identified and fixed before PR creation.

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a7e115d-dcf3-4978-9fd6-26b168fadd29

📥 Commits

Reviewing files that changed from the base of the PR and between 7dab350 and aa342ef.

📒 Files selected for processing (7)
  • docs/design/brand-and-ux.md
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/template-categories.ts

Walkthrough

Adds a React-based multi-step Setup Wizard and extensive supporting code and docs. Introduces a new Zustand store for wizard state and flow, new setup pages and UI subcomponents (Account, Template, Company, Agents, Providers, Theme, Complete, plus many cards/drawers/forms and a MiniOrgChart), five shared UI components with stories and tests (Drawer, InputField, SelectField, SliderField, ToggleField), utilities (cost estimator, template categorization, provider status, password strength, currencies, setup validation), new API types for template variables, numerous unit/property tests, and documentation/README updates reflecting the revised flow.

Suggested labels

autorelease: tagged


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

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 12:13 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

Dependency Review

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

Snapshot Warnings

⚠️: No snapshots were found for the head SHA aa342ef.
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 implements a comprehensive multi-step setup wizard, including account creation, template selection, company configuration, and provider setup, supported by new reusable UI components and extensive testing. Feedback identifies several opportunities to improve code maintainability and robustness, such as replacing hardcoded strings and numeric thresholds with constants in the password strength and template selection logic. Additionally, the review highlights a functional gap in the 'Skip wizard' feature and suggests refining the company name validation to provide a clearer user experience.

Comment on lines +55 to +62
if (!companyResponse) {
return (
<EmptyState
title="Setup incomplete"
description="Go back and complete all previous steps before finishing."
/>
)
}
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

The 'Skip wizard' functionality appears to be incomplete. When a user clicks the 'Skip wizard' link, they are navigated to this step, but companyResponse is null, which currently shows an EmptyState.

To implement the described skip functionality, you should render the SkipWizardForm component here instead. This will allow users to perform the quick setup as intended.

Don't forget to import SkipWizardForm at the top of the file:

import { SkipWizardForm } from './SkipWizardForm'
  if (!companyResponse) {
    return <SkipWizardForm />
  }

setError('Username is required')
return
}
if (password.length < 12) {
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 minimum password length 12 is hardcoded here and also implicitly in the placeholder and hint props for the password InputField. It's a good practice to define this as a constant at the top of the file or in a shared constants file. This makes it easier to change and ensures consistency.

value={companyName}
onChange={(e) => setCompanyName(e.currentTarget.value)}
placeholder="Your organization name"
error={companyName.trim() === '' ? null : companyName.trim().length > 200 ? 'Max 200 characters' : null}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The validation logic for the company name is a bit complex and has a potential issue. If the user enters only whitespace, companyName.trim() === '' will be true, and no error will be shown, but the 'Apply Template' button will be disabled. This can be confusing.

It would be clearer to separate the validation concerns. You could show a length error immediately, but perhaps only show a 'required' error after an interaction (e.g., blur or submission attempt).

Consider simplifying this logic. For example, you could create a helper function or use a useMemo to calculate the error message for better readability.

Comment on lines +17 to +21
const COMPARISON_ROWS = [
{ label: 'Source', key: 'source' },
{ label: 'Tags', key: 'tags' },
{ label: 'Skill Patterns', key: 'skill_patterns' },
] as const
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

Using string literals for keys of TemplateInfoResponse is a bit fragile. If the TemplateInfoResponse type changes, these keys might become invalid without a compile-time error.

To improve type safety, you can explicitly type the key property to be a key of TemplateInfoResponse.

Suggested change
const COMPARISON_ROWS = [
{ label: 'Source', key: 'source' },
{ label: 'Tags', key: 'tags' },
{ label: 'Skill Patterns', key: 'skill_patterns' },
] as const
const COMPARISON_ROWS: { label: string; key: keyof TemplateInfoResponse }[] = [
{ label: 'Source', key: 'source' },
{ label: 'Tags', key: 'tags' },
{ label: 'Skill Patterns', key: 'skill_patterns' },
];

Comment on lines +52 to +55
const agentEstimate = template.tags.includes('solo') ? 1
: template.tags.includes('small-team') ? 3
: template.tags.includes('enterprise') || template.tags.includes('full-company') ? 12
: 5
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 strings 'solo', 'small-team', 'enterprise', and 'full-company' are used here to determine a rough agent count. To improve maintainability and avoid potential typos, it's a good practice to define these as constants and reuse them.

Comment on lines +13 to +22
if (password.length < 8) return { label: 'Weak', percent: 20, color: 'bg-danger' }
if (password.length < 12) return { label: 'Fair', percent: 40, color: 'bg-warning' }
const hasUpper = /[A-Z]/.test(password)
const hasLower = /[a-z]/.test(password)
const hasDigit = /\d/.test(password)
const hasSpecial = /[^A-Za-z0-9]/.test(password)
const variety = [hasUpper, hasLower, hasDigit, hasSpecial].filter(Boolean).length
if (variety >= 3 && password.length >= 16) return { label: 'Strong', percent: 100, color: 'bg-success' }
if (variety >= 3) return { label: 'Good', percent: 75, color: 'bg-accent' }
return { label: 'Fair', percent: 50, color: 'bg-warning' }
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 password length thresholds (8, 12, 16) and variety threshold (3) are hardcoded throughout this function. To improve readability and make these criteria easier to adjust in the future, consider defining them as named constants at the top of the file.

For example:

const WEAK_THRESHOLD = 8;
const FAIR_THRESHOLD = 12;
const STRONG_THRESHOLD = 16;
const VARIETY_THRESHOLD = 3;

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: 46

🤖 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/__tests__/components/ui/drawer.test.tsx`:
- Around line 87-97: The test 'calls onClose when overlay is clicked' silently
passes if overlay is missing because of the `if (overlay)` guard; replace the
conditional with an explicit assertion that the overlay exists (e.g., assert
`overlay` is not null/truthy) before attempting the click so the test fails when
`screen.getByRole('dialog').previousElementSibling` returns null; update the
test that references `overlay` in the Drawer test to assert existence then
perform `user.click(overlay)` and expect `handleClose` to have been called.

In `@web/src/__tests__/pages/setup/WizardProgress.test.tsx`:
- Around line 96-98: The test uses a non-null assertion on templateButton which
hides potential null failures; in WizardProgress.test.tsx replace the `!` usage
by explicitly asserting the element exists (e.g.,
expect(templateButton).toBeTruthy() or expect(templateButton).not.toBeNull())
before calling templateButton.querySelector('svg'), so locate the `buttons`
variable and the `templateButton` find call and add that existence assertion
prior to the SVG assertion.
- Around line 64-67: The test uses a non-null assertion (!) on templateButton
which can mask a missing element; instead assert the button exists before
interacting: find the button via screen.getAllByRole('button') and assign to
templateButton, then add an explicit expectation like
expect(templateButton).toBeDefined() or expect(templateButton).not.toBeNull()
(or use getByRole/getByText to get a guaranteed element) before calling await
user.click(templateButton) and finally assert handleClick was called with
'template' to surface failures clearly in the WizardProgress.test.tsx test
(variables: buttons, templateButton, handleClick, user).

In `@web/src/__tests__/stores/setup-wizard.test.ts`:
- Around line 235-266: Add unit tests covering provider-related store actions:
write tests that import the provider API helpers (e.g., createFromPreset from
'@/api/endpoints/providers'), mock their resolved and rejected results, and
exercise useSetupWizardStore methods createProviderFromPreset, probeAllPresets,
and testProviderConnection; assert that successful calls populate
useSetupWizardStore.getState().providers (check the new provider key) and that
failures set useSetupWizardStore.getState().providersError (or the specific
error field your store uses). Ensure you include both a success case
(mockResolvedValue) and an error case (mockRejectedValue/new Error(...)) for
createProviderFromPreset per the example in the review.

In `@web/src/__tests__/utils/password-strength.test.ts`:
- Around line 35-45: Replace the two example-based tests with property-based
assertions using fast-check: import fc and use
fc.assert(fc.property(fc.string(), pw => { const r = getPasswordStrength(pw);
return r.percent <= 100 })) and another fc.assert(fc.property(fc.string(), pw =>
{ return /^bg-/.test(getPasswordStrength(pw).color) })); ensure tests reference
getPasswordStrength and use fc.assert/fc.property so arbitrary strings are
validated instead of the fixed examples.

In `@web/src/__tests__/utils/setup-validation.property.test.ts`:
- Around line 45-50: The property test's regex used in fc.stringMatching (inside
the "company name with 1-200 non-whitespace chars + response is always valid"
test) requires at least two characters, so 1-character names are excluded;
update the regex to allow a single alphanumeric char while preserving the rule
that multi-char names start and end with an alphanumeric and may contain spaces
between (e.g. change the pattern to make the trailing alphanumeric optional for
length=1, such as using a non-capturing optional group), then run the test to
ensure 1–200 character names are generated.

In `@web/src/components/ui/drawer.tsx`:
- Around line 42-47: The current useEffect only sets initial focus and doesn't
implement a focus trap; update the Drawer component to add a keydown listener
while open that traps Tab/Shift+Tab and keeps focus inside panelRef (compute
focusable elements inside panelRef.current and cycle focus on Tab, reverse on
Shift+Tab), and ensure Escape behavior delegates to the existing close handler
(e.g., call the component's onClose/closeDrawer) so keyboard users cannot tab
out into background while aria-modal="true"; attach/remove the listener in the
same effect that watches open and clean up on unmount/close.
- Around line 84-86: The close button in the drawer component is missing an
explicit type, which can cause it to act as a submit button inside forms; update
the button element that calls onClose (the close action in the Drawer component)
to include type="button" so it will not trigger form submissions and maintains
expected behavior when nested in forms.
- Line 60: The overlay and panel are using hardcoded utility classes
("bg-black/40" on the overlay div and "shadow-xl" on the panel) which violates
the design-token rule; update the className values in the Drawer component so
they use the project's design tokens instead (e.g., replace bg-black/40 with the
semantic overlay token class or a CSS variable like var(--color-overlay) /
var(--overlay-alpha) and replace shadow-xl with the tokenized shadow class or
variable such as var(--shadow-card) or the design system's shadow utility),
ensuring the overlay div and the panel element that currently reference
"bg-black/40" and "shadow-xl" are changed to the corresponding token-based
classes/variables.

In `@web/src/components/ui/input-field.stories.tsx`:
- Around line 4-11: The Storybook meta for InputField currently lacks an
accessibility testing parameter; update the exported meta object (const meta) to
include parameters.a11y.test set to either 'error' | 'todo' | 'off' per project
policy (e.g., parameters: { a11y: { test: 'error' } }) so Storybook 10 will run
a11y checks for the InputField stories; ensure this change is applied on the
same meta object that satisfies Meta<typeof InputField> and keep the Story type
(StoryObj<typeof meta>) unchanged.

In `@web/src/components/ui/input-field.tsx`:
- Around line 4-12: The current InputFieldProps is a single interface extending
React.ComponentProps<'input'> which mis-types the textarea branch; change to a
discriminated union so props are correct for each element: define
InputFieldInputProps extends Omit<React.ComponentProps<'input'>, 'id' | 'ref'>
with multiline?: false and ref?: React.Ref<HTMLInputElement>, and
InputFieldTextareaProps extends Omit<React.ComponentProps<'textarea'>, 'id' |
'ref'> with multiline: true, rows?: number and ref?:
React.Ref<HTMLTextAreaElement>, then export type InputFieldProps =
(InputFieldInputProps | InputFieldTextareaProps) so TypeScript will only allow
input-only props when multiline is false and textarea-only props when multiline
is true; update the component signature to accept InputFieldProps and use a
combined ref type React.Ref<HTMLInputElement | HTMLTextAreaElement> or implement
forwardRef overloads so the actual ref matches the rendered element, and ensure
inside the render you spread the correct props into <input> vs <textarea> rather
than passing input props into the textarea.

In `@web/src/components/ui/select-field.stories.tsx`:
- Around line 5-10: The local currencies array duplicates the centralized
options; remove the local const currencies and import CURRENCY_OPTIONS from the
shared module (CURRENCY_OPTIONS) and use that in the story instead (replace
references to currencies with CURRENCY_OPTIONS); verify the shape matches any
prop types used by the SelectField story and adjust import/export if needed.

In `@web/src/components/ui/select-field.tsx`:
- Around line 51-53: The component currently always sets aria-describedby to
hintId even when hasError is true and the hint element isn't rendered; change
the logic in the SelectField component to compute a describedBy value that
prefers the errorId when hasError is true, otherwise uses hintId if hint exists,
and falls back to undefined; then pass aria-describedby={describedBy ||
undefined} (use the existing hasError, errorId, hint, and hintId symbols) so the
attribute only references elements that are actually rendered.

In `@web/src/components/ui/slider-field.stories.tsx`:
- Around line 5-12: Add Storybook a11y test configuration to the exported meta
object so accessibility checks run for this SliderField story: update the meta
constant (the object assigned to "meta" for SliderField) to include a parameters
key with an a11y.test value set to 'error' or 'todo' (per guideline) so the
story's accessibility testing is enforced; keep the rest of the meta (title,
component, tags) intact and leave Story/StoryObj usage unchanged.

In `@web/src/components/ui/toggle-field.tsx`:
- Line 47: The label currently always applies "cursor-pointer" and the active
text color; update the label rendering in the ToggleField component (the <label
htmlFor={id} ...>) to conditionally apply cursor and text classes based on the
disabled prop (e.g. use disabled ? "cursor-default text-muted-foreground" :
"cursor-pointer text-foreground"). Use the existing className composition
approach in this file (clsx/template string) so the label reflects non-clickable
affordance when disabled while still linking to id.
- Line 42: The thumb translation in toggle-field.tsx uses a hardcoded pixel
class ('translate-x-[18px]') — replace it with a design token or CSS variable to
follow token rules: update the ternary that sets the thumb class (the expression
using checked ? 'translate-x-[18px]' : 'translate-x-0.5') to use a tokenized
class like 'translate-x-4' or reference a CSS variable (e.g.,
translate-x-[var(--space-toggle-thumb)]) so spacing comes from design tokens
instead of hardcoded pixels.

In `@web/src/pages/setup/AgentModelPicker.tsx`:
- Around line 41-46: The current inline onChange handler in AgentModelPicker.tsx
uses val.split('::') which breaks if modelId contains '::'; change the parsing
to split only on the first '::' (e.g., find the first index of '::' on the input
val and extract provider as the substring before it and modelId as the substring
after it, or use a split limited to 2 parts) so provider and modelId are not
truncated before calling onChange(provider, modelId).

In `@web/src/pages/setup/AgentsStep.tsx`:
- Around line 24-28: The useEffect in AgentsStep calls fetchAgents repeatedly if
agents stays empty after a failed request; add a simple fetch guard state (e.g.,
agentsFetchAttempted or agentsError) inside the AgentsStep component and update
it when fetchAgents resolves or rejects, then change the useEffect condition to:
if agents.length === 0 && !agentsLoading && !agentsFetchAttempted (or
!agentsError) to avoid automatic retry loops; ensure fetchAgents sets the guard
on both success and failure and only reset that guard on an explicit user retry
action.

In `@web/src/pages/setup/CompanyStep.tsx`:
- Around line 118-122: The TemplateVariables component is being passed an empty
schema via variables={[]} which disables any template-driven controls; update
the call to pass the actual schema (e.g., the template variables definition)
instead of an empty array so controls can render and bind to templateVariables
and setTemplateVariable. Replace variables={[]} with the correct prop (the
variable schema or list used elsewhere in this module) so TemplateVariables
receives the real schema while keeping values={templateVariables} and
onChange={setTemplateVariable}.
- Around line 126-129: The Button's disabled prop currently only checks
companyName.trim() and companyLoading, which allows submission when other
validation rules fail; update the disabled condition to include the form-level
validation state (e.g., use isCompanyValid or !hasCompanyErrors) so the button
is disabled when any validation errors exist. Concretely, change the disabled
expression on the Button (the one using handleApplyTemplate) to something like:
disabled={companyLoading || !companyName.trim() || !isCompanyValid ||
Object.values(companyErrors || {}).some(Boolean)} or call validateCompany() and
use its result; reference the Button onClick handler handleApplyTemplate and the
form validation symbols (isCompanyValid, companyErrors, validateCompany) to
locate and wire up the check.
- Around line 161-163: The list uses the array index as a React key in the
agents.map render (key={i}), which is unstable; add a stable unique identifier
(e.g., an id string) to the SetupAgentSummary objects and use that (agent.id) as
the key instead of the index. Update the agent creation/initialization code
paths that produce SetupAgentSummary to assign a unique id (UUID or incremental
stable id) and change the JSX in CompanyStep (the agents.map render) to use
agent.id for reconciliation.

In `@web/src/pages/setup/CostEstimatePanel.tsx`:
- Around line 47-57: When the budget cap toggle (budgetCapEnabled) is turned on
but budgetCap is null the default shown by the SliderField
(Math.ceil(estimate.monthlyTotal * 2)) isn’t persisted; update the toggle
handler to initialize the value by calling onBudgetCapChange with that default.
Specifically, in the onBudgetCapEnabledChange logic (or its parent), detect when
enabled is true and budgetCap === null and estimate exists, then call
onBudgetCapChange(Math.ceil(estimate.monthlyTotal * 2)) so the SliderField value
is saved immediately; keep SliderField props (value, min, max, step,
formatValue) unchanged.

In `@web/src/pages/setup/MiniOrgChart.tsx`:
- Around line 83-94: The map over deptPositions uses index-based keys
(`key={`root-${i}`}`) which are unstable; change the key to a stable department
identifier from each position object (e.g., replace `root-${i}` with
`root-${pos.name}` or `root-${pos.id}` depending on your model) inside the
deptPositions.map in MiniOrgChart (the line element map) so keys remain stable
across reorders.
- Around line 119-154: Extract the agent rendering block inside
pos.dept.agents.map into a new presentational component (e.g., AgentNode) and
replace the inline JSX with a call to <AgentNode .../> passing agent, agentIdx,
pos.x, pos.y, deptIdx and constants (AVATAR_RADIUS, NODE_HEIGHT, V_GAP) or
compute positions beforehand; move the <title> element so it is a direct child
of the <circle> (for proper SVG tooltip association) and keep
getInitials(agent.name) usage in the new component; ensure keys remain
`${deptIdx}-${agentIdx}` when rendering the list.

In `@web/src/pages/setup/ProviderAddForm.tsx`:
- Around line 31-35: The catch currently swallows the thrown error in the
ProviderAddForm.tsx connection test (the try/catch around the async test that
calls setTesting and setError), which loses debugging info; change the catch to
capture the error (e.g., catch (err) or catch (e)), and include the error
details when handling the failure—for example log the error (console.error or a
logger) and/or append the err.message to setError in development mode while
still showing the user-friendly 'Connection test failed' message; ensure
setTesting(false) remains in the finally block.
- Around line 118-124: Replace the template-literal class concatenation for the
testResult block in the ProviderAddForm component with the project's cn utility:
import cn from '@/lib/utils' at the top, then call cn with the base classes
"rounded-md border px-3 py-2 text-sm" and pass conditional classes for success
vs danger based on testResult.success (e.g., 'border-success/30 bg-success/5
text-success' when true, otherwise 'border-danger/30 bg-danger/5 text-danger')
so class merging follows project conventions and preserves the existing styles
and conditional logic.
- Around line 49-53: The catch block in ProviderAddForm.tsx currently swallows
the error; change the catch to capture the exception (e.g., catch (err)) and
persist the details to a log (console.error or your app logger) while still
calling setError('Failed to create provider') and finally setAdding(false);
locate the catch in the ProviderAddForm component around the setError/setAdding
calls and add the error logging so debugging information is preserved without
changing the user-facing message.

In `@web/src/pages/setup/ProviderProbeResults.tsx`:
- Around line 32-63: Extract the JSX inside the localPresets.map in
ProviderProbeResults.tsx into a new functional component named ProbeResultItem;
create a ProbeResultItemProps interface with props (preset: ProviderPreset,
result?: ProbePresetResponse, probing: boolean, onAddPreset: (presetName:
string) => void), move the JSX logic (including detected calculation,
Loader2/Check/X icons, display_name, result.url and result.model_count, "Not
found" text, and Add button invoking onAddPreset) into that component, then
replace the map body with a concise <ProbeResultItem ... /> call passing preset,
probeResults[preset.name], probing, and onAddPreset.

In `@web/src/pages/setup/ProvidersStep.tsx`:
- Around line 32-36: The useEffect in ProvidersStep.tsx can re-trigger
fetchProviders on store updates if a previous fetch failed; update the effect
guard to also check providersError (e.g., only call fetchProviders when
providersCount === 0 && !providersLoading && !providersError) and add
providersError to the dependency array so the effect won't loop when an error
state exists; locate the useEffect that references providersCount,
providersLoading and fetchProviders and add providersError to both the condition
and deps.
- Around line 151-156: Replace the array index key used in the validation.errors
map with a stable identifier: in the JSX where validation.errors.map((err, i) =>
<li key={i}>{err}</li>), use the error string as the key (key={err}) to avoid
index-based keys; if duplicate error messages are possible, use a combined
stable key such as the error plus the index (e.g., `${err}-${i}`) so the <li>
elements render deterministically.
- Around line 39-43: The useEffect that triggers fetchPresets currently only
checks presets.length; extract the presetsLoading flag from the store and add it
to the effect condition and dependency array so it only calls fetchPresets when
presets.length === 0 and presetsLoading is false; update the dependency array to
include presetsLoading alongside presets.length and fetchPresets to mirror the
providers fetch effect and prevent redundant re-fetches if loading wasn't reset.

In `@web/src/pages/setup/SetupSummary.tsx`:
- Around line 79-81: The agent list uses the array index as the React key in the
agents.map call (key={i}), which can break identity; update the JSX returned by
agents.map to use a stable unique identifier instead (for example key={agent.id}
or key={`${agent.id ?? agent.name}`}) on the outer div used with Avatar to
ensure consistent row identity and avoid stale UI state.

In `@web/src/pages/setup/TemplateCategoryGroup.tsx`:
- Around line 34-49: The lookup for whether a template is in the compared list
uses comparedTemplates.includes(template.name) inside the templates.map in
TemplateCategoryGroup, which is O(n) per item; change the prop the parent
provides to be a Set<string> (e.g., comparedTemplateSet) and update this
component to use comparedTemplateSet.has(template.name) when rendering
TemplateCard (keep prop names consistent with onToggleCompare/onSelect
handlers), or accept both types by converting comparedTemplates to a Set at the
top of TemplateCategoryGroup before the map to ensure O(1) membership checks for
large lists.

In `@web/src/pages/setup/TemplateCompareDrawer.tsx`:
- Around line 55-71: The current rendering in TemplateCompareDrawer maps (value
as readonly string[]) when row.key === 'tags', which will throw if tags is
null/undefined; update the conditional to defensively check Array.isArray(value)
before mapping (e.g., const tags = Array.isArray(value) ? value : []; or render
fallback like '--' when value is null), then map tags to StatPill; ensure the
code paths that reference templates, row.key, value, and StatPill use the safe
tags variable rather than a raw assertion.

In `@web/src/pages/setup/TemplateStep.tsx`:
- Around line 28-32: The useEffect in TemplateStep currently re-triggers
fetchTemplates whenever templates is empty and templatesLoading is false,
causing a refetch loop if a previous fetch failed; update the condition to also
require no error (e.g., templates.length === 0 && !templatesLoading &&
!templatesError) and add templatesError to the dependency array so the effect
only retries when there's no error (or when templatesError changes), ensuring
fetchTemplates is not called continuously on failed loads.

In `@web/src/pages/setup/TemplateVariables.tsx`:
- Around line 48-51: The slider currently uses hardcoded bounds and a hardcoded
dollar prefix (see min/max/step and formatValue on the slider where variable is
referenced as v); update the slider to use template-provided constraints (e.g.,
v.min, v.max, v.step or v.range) with sensible fallbacks to the existing values
when those properties are absent, and make the prefix conditional (only apply
`$` in formatValue when the variable indicates currency, e.g., v.format ===
'currency' or v.is_currency) so non-monetary floats are not mislabeled.
- Around line 4-10: Update the TemplateVariable interface to add optional
numeric constraint properties (min?: number, max?: number, step?: number) so UI
components like the slider can use template-provided bounds instead of hardcoded
logic in TemplateVariables.tsx; update the slider rendering code to prefer
v.min/v.max/v.step when present and fall back to sensible defaults. Also remove
the hardcoded "$" currency formatting for floats in the float rendering logic
(or make it configurable via a format or currency property on TemplateVariable)
so the display format is driven by the template rather than assuming USD.

In `@web/src/pages/setup/WizardProgress.tsx`:
- Around line 42-97: The map block in WizardProgress.tsx is too large—extract
the JSX inside steps.map into a new presentational component (e.g.,
StepIndicator) that accepts props: step (StepConfig), index, isActive,
isComplete, isAccessible, isLast, and onStepClick; move all conditional
rendering and className logic (the button, circle/check, label and the trailing
divider) into StepIndicator and keep canNavigateTo, stepsCompleted and
currentStep checks in the parent, then replace the long inline map with a simple
<StepIndicator .../> invocation passing those computed props to preserve
behavior.
- Around line 50-53: In WizardProgress, remove the redundant guard inside the
button's onClick; rely on the button's disabled={!isAccessible} instead and call
the prop directly (e.g., onClick={() => onStepClick(step.key)}), or attach the
handler only when onStepClick exists to avoid undefined calls; update the button
that currently references isAccessible and onStepClick so the click logic is not
duplicated and behavior is driven by the disabled state.

In `@web/src/pages/setup/WizardShell.tsx`:
- Around line 91-97: Replace the raw <button> in WizardShell that performs the
skip action with the shared Button component from web/src/components/ui/Button;
keep the same props/behavior (type="button", onClick calling
navigate(`/setup/complete`)) and preserve the visual intent by mapping the
current classes/appearance to the Button's variant/size/intent API (e.g.,
text/ghost/sm) so the skip action remains visually subtle and accessible; update
the import to pull Button into WizardShell and remove the old element, ensuring
the onClick still references the navigate function used in the component.

In `@web/src/stores/setup-wizard.ts`:
- Around line 432-447: The createProviderFromPreset function currently catches
errors, logs them, and sets providersError but does not re-throw, preventing
callers like ProviderAddForm.handleAdd from seeing failures; update
createProviderFromPreset (the async function named createProviderFromPreset) so
that after setting providersError and logging the error it re-throws the caught
error (throw err) so awaiting callers can handle failures normally (preserve
existing set({ providersError: ... }) behavior, then re-throw).
- Around line 453-465: The probeAllPresets function currently probes each preset
sequentially which is slow; change it to run probePreset in parallel using
Promise.allSettled over presets.map(p => probePreset(p.name)) (or similar) so
each preset's result or error is captured, then assemble the results into the
same results: Record<string, ProbePresetResponse> shape (mapping by preset.name)
and update probeResults and probing flags the same way; ensure you still set
probing=true before starting and probing=false after collecting all settled
promises and record individual errors (console.error) for rejected promises
while populating successful values for fulfilled ones.

In `@web/src/utils/cost-estimator.ts`:
- Around line 82-87: The code uses a fallback lookup with TIER_FALLBACK_COSTS
and then applies non-null assertions (!) to assign
inputCostPer1k/outputCostPer1k; replace this by extracting a small helper or
type-guard so the value is statically non-null. Implement a function like
getTierFallbackCosts(tier): Cost that returns TIER_FALLBACK_COSTS[tier] ??
TIER_FALLBACK_COSTS.medium (or throws) and use it inside the else branch (where
usedFallback=true) to assign inputCostPer1k and outputCostPer1k without using !;
reference the TIER_FALLBACK_COSTS lookup, the fallback variable, and the
inputCostPer1k/outputCostPer1k assignments when making the change.

In `@web/src/utils/setup-validation.ts`:
- Around line 88-93: The loop uses an index and a non-null assertion on
input.agents[i]!; replace the indexed loop with a for...of over input.agents
(e.g., for (const agent of input.agents)) to remove the need for the non-null
assertion, then keep the existing check (agent.model_provider || agent.model_id)
and push the same error message into errors when missing; update the block
around the existing loop in setup-validation.ts where input.agents is iterated
and remove the `!` usage.
- Around line 53-72: In validateCompanyStep, trim companyDescription before
validating its length to match the companyName behavior: obtain a
trimmedDescription from input.companyDescription.trim() and use that for the
MAX_DESCRIPTION_LENGTH check (same function validateCompanyStep, variables
trimmedName and trimmedDescription, and constant MAX_DESCRIPTION_LENGTH) so
leading/trailing whitespace doesn't cause false validation failures; preserve
pushing the same error message and returning VALID or { valid: false, errors }
as before.

In `@web/src/utils/template-categories.ts`:
- Line 113: The current fallback label only capitalizes the first character of
the entire category string (return category.charAt(0).toUpperCase() +
category.slice(1)), which yields bad results for composite keys like
"small-team" or "big_team"; update the logic that formats the fallback label
(the code using the category variable in template-categories.ts) to normalize
separators (replace `-` and `_` with spaces), split on separators (e.g.,
/[-_\s]+/), title-case each word by making the first char uppercase and the rest
lowercase, then join the words with a single space and return that string so
"small-team" → "Small Team" and "big_team" → "Big Team".
🪄 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: 04cc8970-6787-4721-a2fa-ecca265b55c0

📥 Commits

Reviewing files that changed from the base of the PR and between 7d519d5 and 8ce8603.

📒 Files selected for processing (59)
  • CLAUDE.md
  • README.md
  • docs/design/brand-and-ux.md
  • docs/design/page-structure.md
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/components/ui/input-field.test.tsx
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/__tests__/components/ui/toggle-field.test.tsx
  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/utils/cost-estimator.property.test.ts
  • web/src/__tests__/utils/cost-estimator.test.ts
  • web/src/__tests__/utils/password-strength.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/__tests__/utils/setup-validation.test.ts
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/slider-field.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/SetupPage.tsx
  • web/src/pages/setup/AccountStep.tsx
  • web/src/pages/setup/AgentModelPicker.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/TemplateVariables.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/cost-estimator.ts
  • web/src/utils/currencies.ts
  • web/src/utils/password-strength.ts
  • web/src/utils/setup-validation.ts
  • web/src/utils/template-categories.ts

Comment on lines +64 to +67
const buttons = screen.getAllByRole('button')
const templateButton = buttons.find((b) => b.textContent?.includes('Template'))!
await user.click(templateButton)
expect(handleClick).toHaveBeenCalledWith('template')
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

Use explicit assertion instead of non-null assertion to surface test failures clearly.

Using ! to assert non-null can mask test failures if the button is not found. The test would throw a generic error rather than a clear assertion failure.

♻️ Proposed fix
-    const templateButton = buttons.find((b) => b.textContent?.includes('Template'))!
-    await user.click(templateButton)
+    const templateButton = buttons.find((b) => b.textContent?.includes('Template'))
+    expect(templateButton).toBeInTheDocument()
+    await user.click(templateButton!)
📝 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
const buttons = screen.getAllByRole('button')
const templateButton = buttons.find((b) => b.textContent?.includes('Template'))!
await user.click(templateButton)
expect(handleClick).toHaveBeenCalledWith('template')
const buttons = screen.getAllByRole('button')
const templateButton = buttons.find((b) => b.textContent?.includes('Template'))
expect(templateButton).toBeInTheDocument()
await user.click(templateButton!)
expect(handleClick).toHaveBeenCalledWith('template')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/pages/setup/WizardProgress.test.tsx` around lines 64 - 67,
The test uses a non-null assertion (!) on templateButton which can mask a
missing element; instead assert the button exists before interacting: find the
button via screen.getAllByRole('button') and assign to templateButton, then add
an explicit expectation like expect(templateButton).toBeDefined() or
expect(templateButton).not.toBeNull() (or use getByRole/getByText to get a
guaranteed element) before calling await user.click(templateButton) and finally
assert handleClick was called with 'template' to surface failures clearly in the
WizardProgress.test.tsx test (variables: buttons, templateButton, handleClick,
user).

Comment on lines +235 to +266
describe('agent actions', () => {
it('updates agent name via API', async () => {
const { updateAgentName } = await import('@/api/endpoints/setup')
const updatedAgent = {
name: 'New Name',
role: 'CEO',
department: 'executive',
level: 'c_suite' as SeniorityLevel,
model_provider: 'p',
model_id: 'm',
tier: 'large',
personality_preset: null,
}
vi.mocked(updateAgentName).mockResolvedValue(updatedAgent)

useSetupWizardStore.setState({
agents: [{
name: 'Old Name',
role: 'CEO',
department: 'executive',
level: 'c_suite' as SeniorityLevel,
model_provider: 'p',
model_id: 'm',
tier: 'large',
personality_preset: null,
}],
})

await useSetupWizardStore.getState().updateAgentName(0, 'New Name')
expect(useSetupWizardStore.getState().agents[0]?.name).toBe('New Name')
})
})
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 tests for provider actions.

The test suite covers template, company, agent, and theme actions well, but provider-related actions (createProviderFromPreset, probeAllPresets, testProviderConnection) lack test coverage. Given the error handling issue flagged in the store, testing the error path for createProviderFromPreset would be valuable.

🧪 Example test additions
describe('provider actions', () => {
  it('creates provider from preset', async () => {
    const { createFromPreset } = await import('@/api/endpoints/providers')
    vi.mocked(createFromPreset).mockResolvedValue({
      name: 'my-openai',
      // ... provider config
    })

    await useSetupWizardStore.getState().createProviderFromPreset('openai', 'my-openai', 'sk-xxx')
    expect(useSetupWizardStore.getState().providers['my-openai']).toBeDefined()
  })

  it('sets error on provider creation failure', async () => {
    const { createFromPreset } = await import('@/api/endpoints/providers')
    vi.mocked(createFromPreset).mockRejectedValue(new Error('Invalid API key'))

    await useSetupWizardStore.getState().createProviderFromPreset('openai', 'my-openai', 'bad-key')
    expect(useSetupWizardStore.getState().providersError).toBeTruthy()
  })
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/stores/setup-wizard.test.ts` around lines 235 - 266, Add
unit tests covering provider-related store actions: write tests that import the
provider API helpers (e.g., createFromPreset from '@/api/endpoints/providers'),
mock their resolved and rejected results, and exercise useSetupWizardStore
methods createProviderFromPreset, probeAllPresets, and testProviderConnection;
assert that successful calls populate useSetupWizardStore.getState().providers
(check the new provider key) and that failures set
useSetupWizardStore.getState().providersError (or the specific error field your
store uses). Ensure you include both a success case (mockResolvedValue) and an
error case (mockRejectedValue/new Error(...)) for createProviderFromPreset per
the example in the review.

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.

Review continued from previous batch...

Comment on lines +4 to +10
interface TemplateVariable {
readonly name: string
readonly description: string
readonly var_type: string
readonly default: string | number | boolean | null
readonly required: boolean
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if TemplateVariable or similar type in API types includes min/max/step
rg -n 'min|max|step' --type ts -C3 web/src/api/types.ts | head -60
# Also check template-related types
ast-grep --pattern 'interface $_ {
  $$$
  var_type$_
  $$$
}'

Repository: Aureliolo/synthorg

Length of output: 1736


🏁 Script executed:

# Search for TemplateVariable definition
rg -n 'interface TemplateVariable|type TemplateVariable' --type ts

Repository: Aureliolo/synthorg

Length of output: 221


🏁 Script executed:

# Look for template-related types more broadly
rg -n 'Template' web/src --type ts | grep -E '(interface|type)' | head -20

Repository: Aureliolo/synthorg

Length of output: 1212


🏁 Script executed:

# Read the TemplateVariables.tsx file to see actual code
wc -l web/src/pages/setup/TemplateVariables.tsx

Repository: Aureliolo/synthorg

Length of output: 106


🏁 Script executed:

# Read the complete TemplateVariables.tsx file
cat -n web/src/pages/setup/TemplateVariables.tsx

Repository: Aureliolo/synthorg

Length of output: 2657


🏁 Script executed:

# Read TemplateInfoResponse definition from api/types.ts
sed -n '1034,1100p' web/src/api/types.ts

Repository: Aureliolo/synthorg

Length of output: 1449


🏁 Script executed:

# Find where TemplateVariables component is used
rg -n 'TemplateVariables' --type ts web/src --type tsx | grep -v 'interface\|export'

Repository: Aureliolo/synthorg

Length of output: 89


🏁 Script executed:

# Search for where template variables data comes from
rg -n 'variables.*:' web/src/pages/setup --type ts --type tsx -C2 | head -40

Repository: Aureliolo/synthorg

Length of output: 89


🏁 Script executed:

# Find where TemplateVariables component is used
rg -n 'TemplateVariables' --type ts web/src | grep -v 'interface\|export function'

Repository: Aureliolo/synthorg

Length of output: 221


🏁 Script executed:

# Check setup-validation.ts for template-related types and how variables are structured
cat -n web/src/utils/setup-validation.ts

Repository: Aureliolo/synthorg

Length of output: 4975


🏁 Script executed:

# Search for any backend response or mock data with variables
rg -n 'variables' web/src/pages/setup --type ts -C3 | head -50

Repository: Aureliolo/synthorg

Length of output: 2198


🏁 Script executed:

# Check if TemplateInfoResponse or any template API includes variables field
rg -n 'variables' web/src/api/types.ts -C5

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Look for any TODO or comments about template variables in setup
rg -n 'TODO|FIXME|variables' web/src/pages/setup/CompanyStep.tsx -C2

Repository: Aureliolo/synthorg

Length of output: 258


🏁 Script executed:

# Check the full CompanyStep.tsx to understand how template data flows
sed -n '1,50p' web/src/pages/setup/CompanyStep.tsx

Repository: Aureliolo/synthorg

Length of output: 2610


TemplateVariable interface is missing constraint properties needed for slider configuration.

The interface should include optional min, max, and step properties so that slider bounds can be driven by template-defined constraints instead of hardcoded values (lines 48–50: min: v.var_type === 'int' ? 1 : 0, etc.). Additionally, the $ currency format assumed for floats (line 51) is not always appropriate; this should be configurable or removed.

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

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 4 - 10, Update the
TemplateVariable interface to add optional numeric constraint properties (min?:
number, max?: number, step?: number) so UI components like the slider can use
template-provided bounds instead of hardcoded logic in TemplateVariables.tsx;
update the slider rendering code to prefer v.min/v.max/v.step when present and
fall back to sensible defaults. Also remove the hardcoded "$" currency
formatting for floats in the float rendering logic (or make it configurable via
a format or currency property on TemplateVariable) so the display format is
driven by the template rather than assuming USD.

@Aureliolo Aureliolo force-pushed the feat/setup-wizard-rebuild branch from 8ce8603 to 2189284 Compare March 27, 2026 12:47
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 12:48 — with GitHub Actions Inactive
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: 28

♻️ Duplicate comments (3)
web/src/pages/setup/TemplateVariables.tsx (1)

4-10: ⚠️ Potential issue | 🟠 Major

Unsupported variable shapes disappear from the UI.

The local type already allows string defaults, but the renderer returns null for every non-bool/int/float variable and hardcodes the numeric bounds. Required string/enum variables and any template-specific min/max/step metadata cannot flow through this component right now, which also leaves no path for an enum-like model_tier_profile variable.

Also applies to: 41-57

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

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 4 - 10, The renderer
currently drops unsupported TemplateVariable shapes by returning null for
non-bool/int/float types and hardcodes numeric bounds; update the component that
consumes TemplateVariable (look for usage of var_type, default, required) to:
preserve and pass through all template-specific metadata (min, max, step, and
any enum values), add explicit rendering branches for string and enum-like
variables (e.g., model_tier_profile) instead of returning null, and remove
hardcoded numeric bounds so numeric widgets use the variable's min/max/step when
present (fall back to sensible defaults only when metadata is absent); ensure
required and default are respected for all types.
web/src/components/ui/drawer.tsx (1)

42-66: ⚠️ Potential issue | 🟠 Major

Complete the dialog focus lifecycle.

This effect moves focus into the panel, but the trap only wraps when the active element is already the first/last tabbable node, and it never restores the opener on close. The first Tab from the panel can escape the drawer, and closing it can dump focus onto body while aria-modal="true" is set.

🔧 Proposed fix
 export function Drawer({ open, onClose, title, children, className }: DrawerProps) {
   const panelRef = useRef<HTMLDivElement>(null)
+  const returnFocusRef = useRef<HTMLElement | null>(null)

   // Move initial focus to panel and trap Tab cycling within it
   useEffect(() => {
     if (!open || !panelRef.current) return
+    returnFocusRef.current = document.activeElement as HTMLElement | null
     panelRef.current.focus()

     const panel = panelRef.current
     const handleTab = (e: KeyboardEvent) => {
       if (e.key !== 'Tab') return
       const focusable = panel.querySelectorAll<HTMLElement>(
         'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
       )
-      if (focusable.length === 0) return
+      const active = document.activeElement as HTMLElement | null
+      if (focusable.length === 0) {
+        e.preventDefault()
+        panel.focus()
+        return
+      }
       const first = focusable[0]!
       const last = focusable[focusable.length - 1]!
-      if (e.shiftKey && document.activeElement === first) {
+      if (!active || !panel.contains(active)) {
+        e.preventDefault()
+        const next = e.shiftKey ? last : first
+        next.focus()
+      } else if (e.shiftKey && active === first) {
         e.preventDefault()
         last.focus()
-      } else if (!e.shiftKey && document.activeElement === last) {
+      } else if (!e.shiftKey && active === last) {
         e.preventDefault()
         first.focus()
       }
     }
     document.addEventListener('keydown', handleTab)
-    return () => document.removeEventListener('keydown', handleTab)
+    return () => {
+      document.removeEventListener('keydown', handleTab)
+      returnFocusRef.current?.focus()
+    }
   }, [open])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/drawer.tsx` around lines 42 - 66, When opening the
drawer, save the previously focused element (e.g., const opener =
document.activeElement stored in an openerRef) and on cleanup (when useEffect
unmounts/close) restore focus to that openerRef.focus(); enhance the handleTab
logic inside the useEffect so it always traps Tab keystrokes even if the
activeElement isn't exactly the first/last tabbable: compute the next/prev
tabbable target from the current activeElement within panelRef.current and if
Tab would move focus outside the panel preventDefault() and redirect focus to
first/last accordingly; ensure openerRef is created outside useEffect and
referenced in the cleanup to restore focus when open becomes false.
web/src/__tests__/stores/setup-wizard.test.ts (1)

18-24: 🧹 Nitpick | 🔵 Trivial

Provider store actions are still uncovered.

createFromPreset, probePreset, and testConnection are mocked here, but the suite still never exercises createProviderFromPreset, probeAllPresets, or testProviderConnection. Please add success and failure cases for those async branches before relying on the new Providers step.

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

In `@web/src/__tests__/stores/setup-wizard.test.ts` around lines 18 - 24, The
tests currently mock createFromPreset, probePreset, and testConnection but never
exercise the async store actions createProviderFromPreset, probeAllPresets, or
testProviderConnection; update the test suite to add both success and failure
cases for each of these async branches by configuring the mocked functions
(createFromPreset, probePreset, testConnection) to return resolved values for
success tests and rejected errors for failure tests, then call the corresponding
store methods (createProviderFromPreset, probeAllPresets,
testProviderConnection) and assert the expected state changes, dispatched
mutations/actions, and error handling/log messages for each path.
🤖 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/__tests__/components/ui/slider-field.test.tsx`:
- Around line 4-61: Add a test that verifies SliderField calls the provided
onChange handler with the new numeric value when the slider is changed: in the
SliderField test suite create a test (e.g., "calls onChange with new value on
input") that renders <SliderField ... onChange={handleChange} />, obtains the
slider via screen.getByRole('slider'), fires a change event with a new string
value (e.g., '10') and asserts handleChange was called with the numeric 10;
reference the SliderField component and the onChange prop and use a spy (vi.fn
or jest.fn) for handleChange.

In `@web/src/__tests__/utils/template-categories.test.ts`:
- Around line 75-87: The test is incorrectly deriving expectedOrder from the
result of categorizeTemplates; change it to compute expectedOrder from the
fixture templates instead. Use the templates array and CATEGORY_ORDER to produce
expectedOrder (e.g., filter CATEGORY_ORDER by whether any template in templates
has a tag matching the category) and then assert that
[...categorizeTemplates(templates).keys()] equals that expectedOrder; update
references in the test around makeTemplate, templates, CATEGORY_ORDER and
categorizeTemplates accordingly.

In `@web/src/components/ui/select-field.tsx`:
- Around line 42-53: The label's asterisk is only visual because the native
<select> element isn't receiving the required prop; update the SelectField
component so the <select> element includes required={required} (and also set
aria-required={required} for assistive tech) alongside the existing id, value,
onChange, disabled, aria-invalid, aria-errormessage and aria-describedby props
(see the select element and the required, label, id, value, onChange, hasError,
errorId, hintId symbols).

In `@web/src/pages/setup/AgentsStep.tsx`:
- Around line 115-128: The current StaggerItem key uses agent.name which can
collide during inline edits; update the key in the agents.map rendering (where
StaggerItem and SetupAgentCard are used) to a more stable identifier—prefer
agent.id if the Agent object has one, otherwise use a compound key such as
`${agent.name}-${index}` or `agent-${index}` to guarantee uniqueness and avoid
duplicate-key rendering issues during editing.

In `@web/src/pages/setup/CompanyStep.tsx`:
- Around line 96-114: The preview stays stale because changing inputs doesn't
clear the applied preview state; update the setters so that setCompanyName,
setCompanyDescription, and setTemplateVariable also clear the applied preview
and agents state (reset companyResponse to null/undefined and agents to an empty
array) whenever they're called; specifically modify the onChange handlers for
Company Name and Description and the function that updates template variables to
call setCompanyResponse(null) and setAgents([]) after updating their respective
state so the apply action/preview is invalidated and reapply becomes required.

In `@web/src/pages/setup/CostEstimatePanel.tsx`:
- Around line 100-115: The agent list is keyed by the display name
(key={agent.name}), which can collide; update the mapping to use a stable id
(e.g., carry entry.agentIndex into DepartmentGroup and use that as the key) and
extract the mapped JSX into a small shared component to keep the .map() body
short. Specifically, replace key={agent.name} in the departmentGroups.map block
with a stable identifier (entry.agentIndex or agent.id) passed down into
DepartmentGroup, create a compact AgentRow component (rendering the agent name,
modelId and formatCurrency(agent.monthlyCost, currency)), and use <AgentRow
key={stableId} .../> inside the map to satisfy the complexity guideline.
- Around line 71-78: The CostEstimatePanel is using raw <div> markup for a
card-with-header; replace the outer container and header with the shared
SectionCard component: wrap the existing inner content (the block that contains
the <h3> and <p>) inside <SectionCard> and move the heading text ("Cost
Estimate") into SectionCard's header/title prop or header slot, removing the
outer container classes (the rounded-lg border bg-card p-4 wrapper) and the
separate header div; keep the existing content (including the tokens line using
estimate.assumptions.dailyTokensPerAgent.toLocaleString()) unchanged inside the
SectionCard to preserve layout and data references.

In `@web/src/pages/setup/MiniOrgChart.tsx`:
- Around line 161-175: The current AgentNode list uses agent.name as the React
key which can collide if names are not unique; update the agents.map to use a
truly unique identifier (e.g., agent.id, agent.uuid, or fallback to
`${agent.name}-${agentIdx}`) when rendering AgentNode so keys are stable and
unique; look for the agents.map block and the AgentNode component usage (agent,
agentX, agentY, deptX, deptY) and replace key={agent.name} with a unique key
derived from an immutable id field or a deterministic fallback combining name
and agentIdx.

In `@web/src/pages/setup/ProviderAddForm.tsx`:
- Around line 33-36: In ProviderAddForm's connection test catch block, don't
clear the user's apiKey on test failure; instead preserve it so users can retry
without re-typing. Modify the catch in the test connection logic (where
setError(getErrorMessage(err)) is called) to remove the setApiKey('') call and
only set the error (or optionally set a non-destructive state like
setTestStatus('failed')), keeping the existing apiKey value managed by the
component.

In `@web/src/pages/setup/ProviderProbeResults.tsx`:
- Around line 44-46: The prop type for probeResults on ProviderProbeResultsProps
is declared as a full Record<string, ProbePresetResponse> but the store produces
sparse entries (some presets absent); change the type to Partial<Record<string,
ProbePresetResponse>> so undefined entries are allowed at runtime—update the
ProviderProbeResultsProps interface (probeResults) and any usages that rely on
its type (e.g., where ProbeResultItem is passed probeResults[preset.name]) to
match ProbeResultItem's result: ProbePresetResponse | undefined.

In `@web/src/pages/setup/ProvidersStep.tsx`:
- Around line 147-149: The StatusBadge currently sets status solely from
config.has_api_key which marks non-API auth providers as errors; replace that
with a status derived from config.auth_type and the corresponding credential
flag (e.g. if config.auth_type === 'api_key' use config.has_api_key, if 'oauth'
use config.has_oauth, if 'custom_header' use config.has_custom_header, if
'local' treat as configured), map the boolean to 'idle' when true and 'error'
when false, and pass that computed status into the StatusBadge used in
ProvidersStep (look for the StatusBadge component and references to
config.has_api_key and config.auth_type).
- Around line 139-153: Extract the JSX inside the providerEntries.map into a
small presentational component (e.g., ProviderRow) and replace the inline JSX
with <ProviderRow key={name} name={name} config={config} /> so the map remains
declarative; move the div with className "flex items-center justify-between..."
plus the inner spans and StatusBadge into ProviderRow, accept props {name,
config}, preserve the StatusBadge usage and its status prop (config.has_api_key
? 'idle' : 'error') and ensure any future auth-mode-specific badges/actions are
handled inside ProviderRow to keep .map() under 8 lines.
- Around line 107-133: Presets loading/error state is not exposed to components,
so ProviderProbeResults and ProviderAddForm cannot distinguish "no presets" from
"failed to load"; update the render to surface explicit loading/error UI or pass
the flags down: thread presetsLoading and presetsError into ProviderProbeResults
and ProviderAddForm (or replace their render with a retry panel that calls
fetchPresets()) so those components can show loading, show the error with a
retry action (invoke fetchPresets), and avoid treating an empty presets array as
a successful no-presets state; use the existing symbols presetsLoading,
presetsError, fetchPresets, ProviderProbeResults, ProviderAddForm,
handleAddPreset, handleAddCloud, and testProviderConnection to implement this.

In `@web/src/pages/setup/SetupSummary.tsx`:
- Around line 96-105: The StatusBadge should not derive health from
config.has_api_key; update the rendering in SetupSummary so StatusBadge uses a
real probe/test state or the provider's auth type instead of has_api_key: read a
health/status field (e.g., config.health, config.status, or a probe result like
config.probed?.state) or check auth type/local flag (e.g., config.auth_type ===
'none' or config.is_local) to decide 'idle' vs 'error', and fall back to a
neutral state when unknown; change the StatusBadge call (the component
referenced as StatusBadge in the map over providers) to use that authoritative
value rather than config.has_api_key.
- Around line 16-18: The component props currently include costEstimate,
currency and budgetCap but not the toggle state, so add a new boolean prop
budgetCapEnabled to the SetupSummary props/interface and pass it down from the
parent; then update the rendering condition that currently uses {budgetCap !==
null} to require the toggle be on by changing it to {budgetCapEnabled &&
budgetCap !== null && (...)} so the budget-cap row is hidden when the toggle is
off. Ensure you update any usages of the SetupSummary component to supply the
new budgetCapEnabled prop and adjust types accordingly.

In `@web/src/pages/setup/SkipWizardForm.tsx`:
- Around line 27-40: The current flow reads errors from
useSetupWizardStore.getState() after awaiting submitCompany() and
wizardCompleteSetup(), which is fragile; update the store actions submitCompany
and wizardCompleteSetup to return either an error string (or null) or throw on
failure, then replace the post-call reads of
useSetupWizardStore.getState().companyError and .completionError with direct
handling of the returned value (or try/catch) and call setError(error) when
non-null/exception occurs; ensure setCompanyNameStore remains before calling
submitCompany and adjust all call sites to rely on the actions' return/throw
rather than synchronous store state mutation.

In `@web/src/pages/setup/TemplateCard.tsx`:
- Around line 31-36: The current TemplateCard component is hand-rolling a card
container and using an arbitrary shadow value; replace the outer div with the
shared card primitive (SectionCard) from web/src/components/ui and move all
layout classes into its className prop, removing the custom shadow-[...] token;
when selected, apply the accent border class ('border-accent') and the shared
shadow token (use var(--so-shadow-card-hover) or the project token name like
'shadow-card-hover') instead of the arbitrary value, and keep the default
'border-border' for unselected state while preserving existing padding and hover
classes (e.g., 'hover:bg-card-hover') via SectionCard's className.

In `@web/src/pages/setup/TemplateCompareDrawer.tsx`:
- Around line 115-149: The map body inside TemplateCompareDrawer.tsx that
iterates COMPARISON_ROWS currently contains too much inline JSX (row diffing,
per-cell rendering, and the Tags branch); extract that into a new presentational
component (e.g., ComparisonRow) which receives props {row, templates} and
performs the isDifferent check using valuesAreEqual(row.getValue, templates) and
renders cells via a small ComparisonCell subcomponent (handles display
formatting, array join, and the Tags branch that uses StatPill). Replace the
inline COMPARISON_ROWS.map callback with a simple <ComparisonRow key={row.label}
row={row} templates={templates} /> to keep TemplateCompareDrawer concise and
make the cell logic unit-testable.

In `@web/src/pages/setup/TemplateStep.tsx`:
- Around line 94-101: The small-tier count is computed incorrectly causing solo
templates to be double-counted; inside the estimateTemplateCost call replace the
current small count calculation (which uses TIER_RATIO_SMALL_COMPLEMENT) with
the remainder after allocating large and medium tiers: compute largeCount =
Math.max(1, Math.floor(agentEstimate * TIER_RATIO_LARGE)) and mediumCount =
Math.floor(agentEstimate * TIER_RATIO_MEDIUM) then set small count to
Math.max(0, agentEstimate - largeCount - mediumCount) so the buckets
(large/medium/small) sum to agentEstimate; update the objects passed to
estimateTemplateCost accordingly (refer to agentEstimate, TIER_RATIO_LARGE,
TIER_RATIO_MEDIUM, and estimateTemplateCost).

In `@web/src/pages/setup/TemplateVariables.tsx`:
- Around line 21-28: The wrapper in TemplateVariables is a custom card/header
div; replace it with the shared SectionCard to keep header spacing/theming
consistent: import SectionCard into the TemplateVariables component, remove the
outer <div className="space-y-4 rounded-lg border border-border bg-card p-4">
and its header block, and render the existing header (<h3> and <p>) and body
content inside SectionCard's header/body slots (or children) so SectionCard
controls layout and styling; keep the TemplateVariables component name and inner
content unchanged.

In `@web/src/pages/setup/ThemePreview.tsx`:
- Around line 29-38: Replace the duplicated agent markup in ThemePreview with
the shared AgentCard component: remove the custom div block that uses Avatar,
StatusBadge and text and render AgentCard (from web/src/components/ui/AgentCard)
with the equivalent props (name "Akira Tanaka", size/variant, status "idle" and
subtitle "CEO, executive") so the visual output remains the same but reuses the
existing component and its styling/props APIs.
- Around line 19-20: ThemePreview is only exposing settings.density and
settings.animation as data attributes, so the live preview ignores the palette,
sidebar and typography axes; update the same wrapper element that currently has
data-density and data-animation to also include data-palette={settings.palette},
data-sidebar={settings.sidebar} and data-typography={settings.typography} (or
equivalent keys used in the settings object) so the component and CSS can react
to these values; locate the element in ThemePreview.tsx that sets
data-density/data-animation and add the three corresponding data-* attributes
referencing settings.palette, settings.sidebar and settings.typography.

In `@web/src/pages/setup/WizardNavigation.tsx`:
- Around line 24-26: currentIdx can be -1 when currentStep is not found in
stepOrder, causing isFirst/isLast to be incorrect; update the logic around
currentIdx (the computation using stepOrder.indexOf(currentStep)) to handle -1
defensively—either normalize currentIdx to a safe value (e.g., 0 or
stepOrder.length-1) or early-handle the mismatch by logging/throwing, and then
compute isFirst and isLast from the validated currentIdx so navigation cannot
enable back/next for a non-existent step.

In `@web/src/pages/setup/WizardShell.tsx`:
- Line 98: The skip button currently navigates to `/setup/complete` (see
onClick={() => navigate(`/setup/complete`)}) which renders CompleteStep that
requires full wizard data; change routing so that the skip flow uses
SkipWizardForm instead: update the STEP_COMPONENTS mapping to return
SkipWizardForm for the step key used by the skip action (or add a separate
route/condition that detects a skipped flag and renders SkipWizardForm instead
of CompleteStep), and ensure any navigation/props passed when clicking the skip
button indicate the skipped flow so SkipWizardForm receives only the company
input it expects.

In `@web/src/pages/setup/WizardSkeleton.tsx`:
- Line 9: The skeleton currently hardcodes six placeholders in WizardSkeleton
(the Array.from({ length: 6 }, ...) call) which can be out of sync when the
wizard can have 7 steps; change that hardcoded 6 to derive the count from the
actual wizard step source (e.g., use wizardSteps.length or the prop/selector
that builds the steps—replace Array.from({ length: 6 }...) with Array.from({
length: wizardSteps.length }) or Array.from({ length: getWizardSteps().length })
so the skeleton mirrors the real step count, including the conditional account
step.

In `@web/src/stores/setup-wizard.ts`:
- Around line 490-499: The completeSetup handler swallows errors instead of
re-throwing them, unlike createProviderFromPreset and testProviderConnection, so
callers can't react to failures; update async completeSetup() (in the
setup-wizard store) to, after set({ completionError: getErrorMessage(err),
completing: false }) inside the catch block, re-throw the original error (or
throw err) so callers receive the failure, preserving the existing local state
updates and logging first.

In `@web/src/utils/currencies.ts`:
- Around line 9-44: The module imports SelectOption from the UI layer which
inverts dependencies; remove the import and instead declare a minimal local type
alias (e.g. type SelectOption = { value: string; label: string }) or type the
export as readonly { value: string; label: string }[] and keep the
CURRENCY_OPTIONS and DEFAULT_CURRENCY exports unchanged; update the file so it
no longer references or imports '@/components/ui/select-field' and ensure
CURRENCY_OPTIONS retains its readonly typing and values.

In `@web/src/utils/template-categories.ts`:
- Around line 9-37: The code currently round-trips via display labels
(CATEGORY_LABELS and LABEL_TO_CATEGORY), causing label edits to break grouping;
replace LABEL_TO_CATEGORY with a direct TAG_TO_CATEGORY map that maps raw tags
(e.g., "solo", "mvp", "agency") to a canonical category key (e.g.,
'startup','consultancy'), keep CATEGORY_LABELS as the canonicalCategory ->
display label map, and update all usages (including grouping logic and
getCategoryLabel()) to: 1) resolve a tag to canonicalCategory via
TAG_TO_CATEGORY, and 2) resolve canonicalCategory to display text via
CATEGORY_LABELS[canonicalCategory] (falling back to 'Other'/'other' as needed);
also remove any code that inferred category by reverse-label lookup and replace
with direct TAG_TO_CATEGORY lookups in the functions that currently use
LABEL_TO_CATEGORY (references: CATEGORY_LABELS, LABEL_TO_CATEGORY,
getCategoryLabel).

---

Duplicate comments:
In `@web/src/__tests__/stores/setup-wizard.test.ts`:
- Around line 18-24: The tests currently mock createFromPreset, probePreset, and
testConnection but never exercise the async store actions
createProviderFromPreset, probeAllPresets, or testProviderConnection; update the
test suite to add both success and failure cases for each of these async
branches by configuring the mocked functions (createFromPreset, probePreset,
testConnection) to return resolved values for success tests and rejected errors
for failure tests, then call the corresponding store methods
(createProviderFromPreset, probeAllPresets, testProviderConnection) and assert
the expected state changes, dispatched mutations/actions, and error handling/log
messages for each path.

In `@web/src/components/ui/drawer.tsx`:
- Around line 42-66: When opening the drawer, save the previously focused
element (e.g., const opener = document.activeElement stored in an openerRef) and
on cleanup (when useEffect unmounts/close) restore focus to that
openerRef.focus(); enhance the handleTab logic inside the useEffect so it always
traps Tab keystrokes even if the activeElement isn't exactly the first/last
tabbable: compute the next/prev tabbable target from the current activeElement
within panelRef.current and if Tab would move focus outside the panel
preventDefault() and redirect focus to first/last accordingly; ensure openerRef
is created outside useEffect and referenced in the cleanup to restore focus when
open becomes false.

In `@web/src/pages/setup/TemplateVariables.tsx`:
- Around line 4-10: The renderer currently drops unsupported TemplateVariable
shapes by returning null for non-bool/int/float types and hardcodes numeric
bounds; update the component that consumes TemplateVariable (look for usage of
var_type, default, required) to: preserve and pass through all template-specific
metadata (min, max, step, and any enum values), add explicit rendering branches
for string and enum-like variables (e.g., model_tier_profile) instead of
returning null, and remove hardcoded numeric bounds so numeric widgets use the
variable's min/max/step when present (fall back to sensible defaults only when
metadata is absent); ensure required and default are respected for all types.
🪄 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: 9f050a6b-0f3c-44ce-a961-d27078384de4

📥 Commits

Reviewing files that changed from the base of the PR and between 8ce8603 and 2189284.

📒 Files selected for processing (62)
  • CLAUDE.md
  • README.md
  • docs/design/brand-and-ux.md
  • docs/design/organization.md
  • docs/design/page-structure.md
  • docs/user_guide.md
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/components/ui/input-field.test.tsx
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/__tests__/components/ui/toggle-field.test.tsx
  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/utils/cost-estimator.property.test.ts
  • web/src/__tests__/utils/cost-estimator.test.ts
  • web/src/__tests__/utils/password-strength.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/__tests__/utils/setup-validation.test.ts
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/api/types.ts
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/slider-field.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/SetupPage.tsx
  • web/src/pages/setup/AccountStep.tsx
  • web/src/pages/setup/AgentModelPicker.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/TemplateVariables.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/cost-estimator.ts
  • web/src/utils/currencies.ts
  • web/src/utils/password-strength.ts
  • web/src/utils/setup-validation.ts
  • web/src/utils/template-categories.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: Dashboard Test
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Markdown trailing-whitespace removal and end-of-file fixing enforced by pre-commit hooks

Files:

  • README.md
  • docs/design/page-structure.md
  • CLAUDE.md
  • docs/design/organization.md
  • docs/user_guide.md
  • docs/design/brand-and-ux.md
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones. Check the required component inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, TaskStatusIndicator, PriorityBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)) for colors; never hardcode hex values
Use font-sans or font-mono for typography (maps to Geist tokens); never set fontFamily directly
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables for shadows and borders (var(--so-shadow-card-hover), border-border, border-bright)
Do NOT recreate status dots inline -- use <StatusBadge>; do NOT build card-with-header layouts from scratch -- use <SectionCard>; do NOT create metric displays inline -- use <MetricCard>; do NOT render initials circles manually -- use <Avatar>; do NOT create complex (>8 line) JSX inside .map() -- extract to shared component
TypeScript 6.0: remove baseUrl (deprecated, will stop working in TS 7); esModuleInterop always true (cannot be set false); types defaults to [] (must explicitly list needed types); DOM.Iterable merged into DOM; moduleResolution: 'classic' and 'node10' removed (use 'bundler' or 'nodenext'); strict defaults to true
Storybook 10 is ESM-only; @storybook/addon-essentials, @storybook/addon-interactions, @storybook/test, @storybook/blocks no longer published (features built into core storybook); @storybook/addon-docs must be installed separately if using tags: ['autodocs']

Files:

  • web/src/pages/SetupPage.tsx
  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/api/types.ts
  • web/src/__tests__/components/ui/toggle-field.test.tsx
  • web/src/utils/currencies.ts
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/__tests__/components/ui/input-field.test.tsx
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/__tests__/utils/cost-estimator.property.test.ts
  • web/src/__tests__/utils/password-strength.test.ts
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/utils/template-categories.ts
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/utils/password-strength.ts
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/pages/setup/AgentModelPicker.tsx
  • web/src/__tests__/utils/cost-estimator.test.ts
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/slider-field.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateVariables.tsx
  • web/src/__tests__/utils/setup-validation.test.ts
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/AccountStep.tsx
  • web/src/utils/setup-validation.ts
  • web/src/pages/setup/WizardShell.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/pages/setup/CostEstimatePanel.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/utils/cost-estimator.ts
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
docs/design/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Always read the relevant docs/design/ page before implementing any feature or planning any issue; DESIGN_SPEC.md is a pointer file to 11 design pages

Files:

  • docs/design/page-structure.md
  • docs/design/organization.md
  • docs/design/brand-and-ux.md
web/src/components/ui/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

When creating new shared UI components, place in web/src/components/ui/ with kebab-case filename, create adjacent .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively (no hardcoded colors/fonts/spacing), import cn from @/lib/utils for conditional class merging

Files:

  • web/src/components/ui/toggle-field.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/slider-field.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/components/ui/select-field.tsx
web/src/**/*.stories.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Storybook 10: use definePreview from @storybook/react-vite for type-safe config; import from storybook/test (not @storybook/test) and storybook/actions (not @storybook/addon-actions); use parameters.backgrounds.options (object keyed by name) + initialGlobals.backgrounds.value; use parameters.a11y.test: 'error' | 'todo' | 'off' for accessibility testing

Files:

  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/select-field.stories.tsx
🧠 Learnings (21)
📚 Learning: 2026-03-19T07:12:14.508Z
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/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...

Applied to files:

  • README.md
  • CLAUDE.md
  • docs/design/organization.md
📚 Learning: 2026-03-21T12:54:22.557Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T12:54:22.557Z
Learning: Version bumping (pre-1.0): `fix:` = patch, `feat:` = patch, `feat!:` = minor, `BREAKING CHANGE` trailer = minor. Update version in `pyproject.toml` (`[tool.commitizen].version`) and `src/synthorg/__init__.py` (`__version__`)

Applied to files:

  • README.md
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/synthorg/api/**/*.py : Litestar API must include setup wizard, auth/, auto-wiring, and lifecycle management

Applied to files:

  • README.md
  • docs/design/organization.md
  • docs/user_guide.md
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to docker/Dockerfile.sandbox : Docker sandbox: `synthorg-sandbox` — Python 3.14 + Node.js + git, non-root (UID 10001), agent code execution sandbox

Applied to files:

  • README.md
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT recreate status dots inline -- use `<StatusBadge>`; do NOT build card-with-header layouts from scratch -- use `<SectionCard>`; do NOT create metric displays inline -- use `<MetricCard>`; do NOT render initials circles manually -- use `<Avatar>`; do NOT create complex (>8 line) JSX inside `.map()` -- extract to shared component

Applied to files:

  • web/src/pages/setup/WizardSkeleton.tsx
  • CLAUDE.md
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/components/ui/drawer.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/components/ui/drawer.stories.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/AccountStep.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Check the required component inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, TaskStatusIndicator, PriorityBadge)

Applied to files:

  • web/src/pages/setup/WizardSkeleton.tsx
  • CLAUDE.md
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/components/ui/drawer.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/components/ui/drawer.stories.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : When creating new shared UI components, place in `web/src/components/ui/` with kebab-case filename, create adjacent `.stories.tsx` with all states, export props as TypeScript interface, use design tokens exclusively (no hardcoded colors/fonts/spacing), import `cn` from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/__tests__/components/ui/toggle-field.test.tsx
  • CLAUDE.md
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/__tests__/components/ui/input-field.test.tsx
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/input-field.stories.tsx
  • docs/design/brand-and-ux.md
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/components/ui/drawer.stories.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateVariables.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/AccountStep.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/TemplateCategoryGroup.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__/components/ui/toggle-field.test.tsx
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/__tests__/components/ui/input-field.test.tsx
  • web/src/__tests__/utils/cost-estimator.property.test.ts
  • web/src/__tests__/utils/password-strength.test.ts
  • web/src/__tests__/utils/cost-estimator.test.ts
  • web/src/__tests__/utils/setup-validation.test.ts
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: use `definePreview` from `storybook/react-vite` for type-safe config; import from `storybook/test` (not `storybook/test`) and `storybook/actions` (not `storybook/addon-actions`); use `parameters.backgrounds.options` (object keyed by name) + `initialGlobals.backgrounds.value`; use `parameters.a11y.test: 'error' | 'todo' | 'off'` for accessibility testing

Applied to files:

  • web/src/__tests__/components/ui/toggle-field.test.tsx
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/__tests__/utils/setup-validation.test.ts
  • web/src/__tests__/stores/setup-wizard.test.ts
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : TypeScript 6.0: remove `baseUrl` (deprecated, will stop working in TS 7); `esModuleInterop` always true (cannot be set false); `types` defaults to `[]` (must explicitly list needed types); `DOM.Iterable` merged into `DOM`; `moduleResolution: 'classic'` and `'node10'` removed (use `'bundler'` or `'nodenext'`); `strict` defaults to true

Applied to files:

  • web/src/utils/currencies.ts
  • web/src/pages/setup/TemplateVariables.tsx
  • web/src/components/ui/select-field.tsx
📚 Learning: 2026-03-15T18:17:43.675Z
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).

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to web/package.json : Web dashboard Node.js 20+; dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, ESLint, vue-tsc)

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-15T21:20:09.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:20:09.993Z
Learning: Applies to web/src/components/** : Vue components organized by feature (agents/, approvals/, budget/, common/, dashboard/, layout/, messages/, org-chart/, tasks/).

Applied to files:

  • CLAUDE.md
  • web/src/pages/setup/MiniOrgChart.tsx
  • docs/design/brand-and-ux.md
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to docs/** : Docs source in docs/ (Markdown, built with Zensical); design spec in docs/design/ (7 pages: index, agents, organization, communication, engine, memory, operations)

Applied to files:

  • CLAUDE.md
📚 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 src/synthorg/templates/**/*.py : Templates: pre-built company templates, personality presets, and builder.

Applied to files:

  • docs/design/organization.md
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Storybook 10 is ESM-only; storybook/addon-essentials, storybook/addon-interactions, storybook/test, storybook/blocks no longer published (features built into core `storybook`); storybook/addon-docs must be installed separately if using `tags: ['autodocs']`

Applied to files:

  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/input-field.stories.tsx
  • docs/design/brand-and-ux.md
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/select-field.stories.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware spacing 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/components/ui/toggle-field.tsx
  • web/src/components/ui/drawer.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/TemplateCard.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`)

Applied to files:

  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/TemplateVariables.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors; never hardcode hex values

Applied to files:

  • web/src/components/ui/toggle-field.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to tests/**/*.py : Use `test-provider`, `test-small-001`, etc. as vendor-agnostic placeholders in tests; never use real vendor names

Applied to files:

  • web/src/__tests__/stores/setup-wizard.test.ts
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to tests/**/*.py : Tests must use test-provider, test-small-001, etc. for vendor-agnostic test data.

Applied to files:

  • web/src/__tests__/stores/setup-wizard.test.ts
🪛 GitHub Check: Dashboard Lint
web/src/pages/setup/AccountStep.tsx

[warning] 34-34:
Potential timing attack, left side: true

web/src/__tests__/utils/setup-validation.property.test.ts

[warning] 48-48:
Unsafe Regular Expression

Comment on lines +96 to +114
<InputField
label="Company Name"
required
value={companyName}
onChange={(e) => setCompanyName(e.currentTarget.value)}
placeholder="Your organization name"
error={companyName.trim() === '' ? null : companyName.trim().length > 200 ? 'Max 200 characters' : null}
/>

<InputField
label="Description"
multiline
rows={3}
value={companyDescription}
onChange={(e) => setCompanyDescription(e.currentTarget.value)}
placeholder="Describe your organization (optional)"
hint="Max 1000 characters"
error={companyDescription.length > 1000 ? 'Max 1000 characters' : null}
/>
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
# Inspect whether company/template input setters invalidate the applied preview.
rg -n -C3 "setCompanyName|setCompanyDescription|setTemplateVariable|submitCompany|companyResponse|agents" web/src/stores/setup-wizard.ts

Repository: Aureliolo/synthorg

Length of output: 4977


Invalidate the applied preview when company or template inputs change.

The setters setCompanyName(), setCompanyDescription(), and setTemplateVariable() do not clear companyResponse or agents. After users submit the template, these fields remain editable, but the apply action disappears (lines 144–153). This leaves the preview stale if any input is changed after applying. Either reset companyResponse when these setters are called, or provide a reapply action visible after the initial apply.

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

In `@web/src/pages/setup/CompanyStep.tsx` around lines 96 - 114, The preview stays
stale because changing inputs doesn't clear the applied preview state; update
the setters so that setCompanyName, setCompanyDescription, and
setTemplateVariable also clear the applied preview and agents state (reset
companyResponse to null/undefined and agents to an empty array) whenever they're
called; specifically modify the onChange handlers for Company Name and
Description and the function that updates template variables to call
setCompanyResponse(null) and setAgents([]) after updating their respective state
so the apply action/preview is invalidated and reapply becomes required.

Comment on lines +71 to +78
return (
<div className="space-y-4 rounded-lg border border-border bg-card p-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold text-foreground">Cost Estimate</h3>
<p className="text-xs text-muted-foreground">
Based on {estimate.assumptions.dailyTokensPerAgent.toLocaleString()} tokens/agent/day
</p>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this panel shell.

This is a card-with-header layout built from raw <div>s. Swap the outer container/header to SectionCard and keep the existing content inside it. As per coding guidelines, "do NOT build card-with-header layouts from scratch -- use <SectionCard>".

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

In `@web/src/pages/setup/CostEstimatePanel.tsx` around lines 71 - 78, The
CostEstimatePanel is using raw <div> markup for a card-with-header; replace the
outer container and header with the shared SectionCard component: wrap the
existing inner content (the block that contains the <h3> and <p>) inside
<SectionCard> and move the heading text ("Cost Estimate") into SectionCard's
header/title prop or header slot, removing the outer container classes (the
rounded-lg border bg-card p-4 wrapper) and the separate header div; keep the
existing content (including the tokens line using
estimate.assumptions.dailyTokensPerAgent.toLocaleString()) unchanged inside the
SectionCard to preserve layout and data references.

<div className="w-full max-w-4xl space-y-8 px-4">
{/* Progress bar skeleton */}
<div className="flex items-center justify-center gap-4">
{Array.from({ length: 6 }, (_, i) => (
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 | 🟡 Minor

Align skeleton step placeholders with the current wizard step count.

Line 9 hardcodes 6 placeholders, but the rebuilt wizard can present 7 steps (including conditional account). This can render an inconsistent loading state.

♻️ Proposed fix
 export function WizardSkeleton() {
+  const stepCount = 7
   return (
@@
-          {Array.from({ length: 6 }, (_, i) => (
+          {Array.from({ length: stepCount }, (_, i) => (
             <div key={i} className="flex flex-col items-center gap-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
{Array.from({ length: 6 }, (_, i) => (
export function WizardSkeleton() {
const stepCount = 7
return (
{Array.from({ length: stepCount }, (_, i) => (
<div key={i} className="flex flex-col items-center gap-1">
{/* skeleton step content */}
</div>
))}
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/WizardSkeleton.tsx` at line 9, The skeleton currently
hardcodes six placeholders in WizardSkeleton (the Array.from({ length: 6 }, ...)
call) which can be out of sync when the wizard can have 7 steps; change that
hardcoded 6 to derive the count from the actual wizard step source (e.g., use
wizardSteps.length or the prop/selector that builds the steps—replace
Array.from({ length: 6 }...) with Array.from({ length: wizardSteps.length }) or
Array.from({ length: getWizardSteps().length }) so the skeleton mirrors the real
step count, including the conditional account step.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 13:18 — with GitHub Actions Inactive
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: 7

♻️ Duplicate comments (7)
web/src/pages/setup/WizardSkeleton.tsx (1)

9-9: ⚠️ Potential issue | 🟡 Minor

Make the skeleton step count derive from the actual wizard step model.

Line 9 hardcodes 7, which can desync from conditional/dynamic step flow. Please derive this from the same source used by the wizard routing/step builder.

♻️ Proposed fix
-export function WizardSkeleton() {
+interface WizardSkeletonProps {
+  stepCount?: number
+}
+
+export function WizardSkeleton({ stepCount = 7 }: WizardSkeletonProps) {
   return (
@@
-          {Array.from({ length: 7 }, (_, i) => (
+          {Array.from({ length: stepCount }, (_, i) => (
             <div key={i} className="flex flex-col items-center gap-1">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/WizardSkeleton.tsx` at line 9, The skeleton currently
hardcodes Array.from({ length: 7 }) in WizardSkeleton.tsx which can drift from
the actual wizard flow; change it to derive the count from the same step model
used by the wizard (e.g., use the steps/stepModel/wizardSteps array or the
getWizardSteps() result that the routing/step builder uses) and replace the 7
with steps.length (or a computedSteps.length fallback) so the skeleton always
matches the real number of steps; ensure WizardSkeleton consumes or imports that
same source and provide a sensible fallback if the model is undefined.
web/src/utils/currencies.ts (2)

41-42: 🧹 Nitpick | 🔵 Trivial

Add CurrencyCode type for compile-time safety.

Deriving a union type from CURRENCY_OPTIONS ensures DEFAULT_CURRENCY and other currency code references are validated at compile time, catching typos and invalid values early.

♻️ Proposed refactor to add derived type
+export type CurrencyCode = (typeof CURRENCY_OPTIONS)[number]['value']
+
 /** Default currency code (matches backend DEFAULT_CURRENCY). */
-export const DEFAULT_CURRENCY = 'EUR'
+export const DEFAULT_CURRENCY: CurrencyCode = 'EUR'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/currencies.ts` around lines 41 - 42, Add a derived CurrencyCode
union type from the existing CURRENCY_OPTIONS so DEFAULT_CURRENCY and other
currency variables are validated at compile time; derive CurrencyCode as keyof
or value-union of CURRENCY_OPTIONS (referencing CURRENCY_OPTIONS) and annotate
DEFAULT_CURRENCY (and any related variables/functions that accept currency
codes) with that CurrencyCode type to ensure compiler catches invalid currency
strings.

9-39: 🧹 Nitpick | 🔵 Trivial

Extract CurrencyOption interface for better reusability.

The inline type { value: string; label: string } works but limits reusability. Extracting a named interface allows consumers to import the type directly and enables deriving a CurrencyCode union type from the array.

♻️ Proposed refactor to extract interface
+export interface CurrencyOption {
+  readonly value: string
+  readonly label: string
+}
+
 /** ISO 4217 currency codes with display labels, ordered by usage frequency. */
-export const CURRENCY_OPTIONS: readonly { value: string; label: string }[] = [
+export const CURRENCY_OPTIONS: readonly CurrencyOption[] = [
   { value: 'EUR', label: 'EUR - Euro' },
   { value: 'USD', label: 'USD - US Dollar' },
   { value: 'GBP', label: 'GBP - British Pound' },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/currencies.ts` around lines 9 - 39, Extract a named interface
for the currency option and derive a union type from CURRENCY_OPTIONS: define an
exported interface CurrencyOption { value: string; label: string } (or use
literal types for value if desired), change the CURRENCY_OPTIONS declaration to
use readonly CurrencyOption[], and export a CurrencyCode = typeof
CURRENCY_OPTIONS[number]['value'] union type so consumers can import
CurrencyOption and CurrencyCode; update any references to the inline type to use
CurrencyOption and ensure CURRENCY_OPTIONS remains readonly.
web/src/pages/setup/CostEstimatePanel.tsx (2)

87-94: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this panel shell.

The outer container uses a card-with-header layout built from raw <div> elements. Per coding guidelines, card-with-header layouts should use <SectionCard> instead of being built from scratch.

♻️ Proposed refactor
+import { SectionCard } from '@/components/ui/section-card'
 import { MetricCard } from '@/components/ui/metric-card'

Then replace the outer wrapper:

-  return (
-    <div className="space-y-4 rounded-lg border border-border bg-card p-4">
-      <div className="space-y-1">
-        <h3 className="text-sm font-semibold text-foreground">Cost Estimate</h3>
-        <p className="text-xs text-muted-foreground">
-          Based on {estimate.assumptions.dailyTokensPerAgent.toLocaleString()} tokens/agent/day
-        </p>
-      </div>
+  return (
+    <SectionCard
+      title="Cost Estimate"
+      description={`Based on ${estimate.assumptions.dailyTokensPerAgent.toLocaleString()} tokens/agent/day`}
+    >
       {/* ... rest of content ... */}
-    </div>
+    </SectionCard>
   )

As per coding guidelines: "do NOT build card-with-header layouts from scratch -- use <SectionCard>".

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

In `@web/src/pages/setup/CostEstimatePanel.tsx` around lines 87 - 94, The outer
wrapper in CostEstimatePanel is building a card-with-header manually; replace
that raw <div> structure with the SectionCard component: remove the top-level
div with classes "space-y-4 rounded-lg border border-border bg-card p-4" and the
inner header block, and instead render SectionCard (using its header/children
API) to host the title "Cost Estimate" and the description that references
estimate.assumptions.dailyTokensPerAgent.toLocaleString(); ensure the same
content and spacing are preserved by placing the existing body into
SectionCard's children and using SectionCard's header slot for the h3 and
paragraph.

116-128: 🧹 Nitpick | 🔵 Trivial

Consider extracting DepartmentGroupSection for cleaner composition.

While AgentRow was extracted (addressing the key collision issue), the department group rendering block at ~12 lines is on the edge of the guideline limit. Extracting it would improve readability and testability.

♻️ Optional extraction
function DepartmentGroupSection({ 
  group, 
  currency 
}: { 
  group: DepartmentGroup; 
  currency: string 
}) {
  return (
    <div className="space-y-1">
      <div className="flex items-center justify-between text-xs font-medium text-foreground">
        <span>{group.department}</span>
        <span className="text-muted-foreground">{formatCurrency(group.subtotal, currency)}</span>
      </div>
      <ul className="space-y-0.5 pl-3">
        {group.agents.map((agent) => (
          <AgentRow key={agent.agentIndex} agent={agent} currency={currency} />
        ))}
      </ul>
    </div>
  )
}

Then simplify the map:

{departmentGroups.map((group) => (
  <DepartmentGroupSection key={group.department} group={group} currency={currency} />
))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/CostEstimatePanel.tsx` around lines 116 - 128, Extract
the department group rendering into a new presentational component (e.g.,
DepartmentGroupSection) to improve readability and testability: create
DepartmentGroupSection which accepts props { group: DepartmentGroup; currency:
string } and renders the outer div, header that uses
formatCurrency(group.subtotal, currency), and the ul that maps group.agents to
AgentRow (preserve key={agent.agentIndex}). Replace the inline map over
departmentGroups with departmentGroups.map(group => <DepartmentGroupSection
key={group.department} group={group} currency={currency} />). Ensure
imports/exports and props typing are added and no existing keys change.
web/src/pages/setup/ThemePreview.tsx (1)

31-41: 🛠️ Refactor suggestion | 🟠 Major

Use the shared AgentCard for this preview block.

This hand-rolled card duplicates a design-system pattern that already exists in the required UI inventory, which makes the preview easier to drift from the rest of the app.

As per coding guidelines, "Always reuse existing components from web/src/components/ui/ before creating new ones. Check the required component inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, TaskStatusIndicator, PriorityBadge)".

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

In `@web/src/pages/setup/ThemePreview.tsx` around lines 31 - 41, Replace the
hand-rolled agent card markup with the shared AgentCard component: import and
use AgentCard in ThemePreview instead of the current div containing Avatar and
StatusBadge, passing the same props (name "Akira Tanaka", role "CEO, executive",
and status "idle") so the preview reuses the design-system implementation;
remove the duplicated div block and any direct uses of Avatar/StatusBadge inside
this preview, ensuring visual props match the original text sizes/labels via
AgentCard's API.
web/src/pages/setup/ProvidersStep.tsx (1)

138-164: ⚠️ Potential issue | 🟠 Major

Pass preset loading/error state into the preset-backed children.

presetsLoading and presetsError are read here, but Lines 151-164 still render ProviderProbeResults and ProviderAddForm as if presets={[]} were a successful load. That makes loading and failure indistinguishable from a real empty result, and after presetsError the step has no in-place retry path. Either gate these sections behind an explicit retry state or thread the flags plus fetchPresets down.

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

In `@web/src/pages/setup/ProvidersStep.tsx` around lines 138 - 164, The UI
currently renders ProviderProbeResults and ProviderAddForm even while presets
are loading or have errored; pass the loading/error state and a retry handler
down or gate rendering: propagate presetsLoading and presetsError (and
fetchPresets or the existing fetchPresets-equivalent) into ProviderProbeResults
and ProviderAddForm props (or wrap their JSX in a conditional that shows a
loading spinner when presetsLoading and an inline retry button that calls
fetchPresets when presetsError), so update the ProviderProbeResults and
ProviderAddForm invocations to include presetsLoading, presetsError, and a retry
callback (e.g., fetchPresets) or hide them until presetsLoading is false and
presetsError is null.
🤖 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/components/ui/select-field.tsx`:
- Line 52: The aria-required attribute currently renders when false; update the
SelectField (or the element rendering aria-required) to use the idiomatic
pattern aria-required={required || undefined} so the attribute is omitted when
required is falsy, matching the existing aria-errormessage and aria-describedby
conditional patterns and keeping behavior consistent.

In `@web/src/pages/setup/CompleteStep.tsx`:
- Around line 40-54: The await wizardCompleteSetup() call can throw and bypass
the subsequent completionError check, so wrap the wizardCompleteSetup invocation
in a try/catch inside handleComplete; in the catch block read
useSetupWizardStore.getState().completionError (or use the caught error) and
return early to keep the dialog open (do not proceed to useSetupStore.setState,
useToastStore.getState().add, setConfirmOpen(false), or navigate('/')); leave
the success path unchanged and reference handleComplete, wizardCompleteSetup,
useSetupWizardStore.getState().completionError, useSetupStore.setState,
useToastStore.getState().add, setConfirmOpen, and navigate.

In `@web/src/pages/setup/ProviderAddForm.tsx`:
- Around line 26-46: handleTest currently only calls onTest(providerName.trim())
and ignores selectedPreset and apiKey, so it doesn't validate the draft config;
update handleTest to pass the preset and apiKey (e.g., call
onTest(selectedPreset, providerName.trim(), apiKey || undefined)) and ensure it
early-returns only when providerName is empty or no preset is selected,
mirroring the checks in handleAdd; also propagate the result to setTestResult
and errors to setError as before so the UI validates credentials entered in the
form rather than only saved providers.
- Around line 26-39: The testResult is not cleared when the draft changes or
before a new test, causing stale success state to persist; modify handleTest to
call setTestResult(null) before invoking onTest (e.g., right after
setTesting(true) or setError(null)), and also clear testResult in the
draft-change handlers (the functions that update providerName, preset selection,
and API key—look for the onChange handlers and methods around lines 76-81,
91-99, 122-133) so any name/preset/key edits reset testResult to null.

In `@web/src/pages/setup/SetupSummary.tsx`:
- Around line 13-20: Extract the getProviderStatus function (which maps
ProviderConfig.auth_type to AgentRuntimeStatus) into a shared setup utility
module (e.g., export a function from a new setup utils file) and replace the
duplicate implementations in SetupSummary and ProvidersStep by importing and
using that exported getProviderStatus; ensure the utility exports types if
needed (ProviderConfig, AgentRuntimeStatus) or imports them, update both callers
to use the shared function, and delete the local copies so there is a single
source of truth for auth_type → status logic.
- Around line 91-103: The agent list rendering uses an unstable key and violates
the inline .map() length rule; add an optional id field to the SetupAgentSummary
type in web/src/api/types.ts (or choose a safe fallback) and extract the JSX
block in SetupSummary.tsx into a new component (e.g., SetupAgentRow) that
accepts a SetupAgentSummary prop and renders Avatar, name/role and StatPill for
department/tier; in the callers use a stable key like key={agent.id ??
`agent-${index}`} (or agent.id when present) and replace the inline map body
with a single-line call to <SetupAgentRow /> for each agent.

---

Duplicate comments:
In `@web/src/pages/setup/CostEstimatePanel.tsx`:
- Around line 87-94: The outer wrapper in CostEstimatePanel is building a
card-with-header manually; replace that raw <div> structure with the SectionCard
component: remove the top-level div with classes "space-y-4 rounded-lg border
border-border bg-card p-4" and the inner header block, and instead render
SectionCard (using its header/children API) to host the title "Cost Estimate"
and the description that references
estimate.assumptions.dailyTokensPerAgent.toLocaleString(); ensure the same
content and spacing are preserved by placing the existing body into
SectionCard's children and using SectionCard's header slot for the h3 and
paragraph.
- Around line 116-128: Extract the department group rendering into a new
presentational component (e.g., DepartmentGroupSection) to improve readability
and testability: create DepartmentGroupSection which accepts props { group:
DepartmentGroup; currency: string } and renders the outer div, header that uses
formatCurrency(group.subtotal, currency), and the ul that maps group.agents to
AgentRow (preserve key={agent.agentIndex}). Replace the inline map over
departmentGroups with departmentGroups.map(group => <DepartmentGroupSection
key={group.department} group={group} currency={currency} />). Ensure
imports/exports and props typing are added and no existing keys change.

In `@web/src/pages/setup/ProvidersStep.tsx`:
- Around line 138-164: The UI currently renders ProviderProbeResults and
ProviderAddForm even while presets are loading or have errored; pass the
loading/error state and a retry handler down or gate rendering: propagate
presetsLoading and presetsError (and fetchPresets or the existing
fetchPresets-equivalent) into ProviderProbeResults and ProviderAddForm props (or
wrap their JSX in a conditional that shows a loading spinner when presetsLoading
and an inline retry button that calls fetchPresets when presetsError), so update
the ProviderProbeResults and ProviderAddForm invocations to include
presetsLoading, presetsError, and a retry callback (e.g., fetchPresets) or hide
them until presetsLoading is false and presetsError is null.

In `@web/src/pages/setup/ThemePreview.tsx`:
- Around line 31-41: Replace the hand-rolled agent card markup with the shared
AgentCard component: import and use AgentCard in ThemePreview instead of the
current div containing Avatar and StatusBadge, passing the same props (name
"Akira Tanaka", role "CEO, executive", and status "idle") so the preview reuses
the design-system implementation; remove the duplicated div block and any direct
uses of Avatar/StatusBadge inside this preview, ensuring visual props match the
original text sizes/labels via AgentCard's API.

In `@web/src/pages/setup/WizardSkeleton.tsx`:
- Line 9: The skeleton currently hardcodes Array.from({ length: 7 }) in
WizardSkeleton.tsx which can drift from the actual wizard flow; change it to
derive the count from the same step model used by the wizard (e.g., use the
steps/stepModel/wizardSteps array or the getWizardSteps() result that the
routing/step builder uses) and replace the 7 with steps.length (or a
computedSteps.length fallback) so the skeleton always matches the real number of
steps; ensure WizardSkeleton consumes or imports that same source and provide a
sensible fallback if the model is undefined.

In `@web/src/utils/currencies.ts`:
- Around line 41-42: Add a derived CurrencyCode union type from the existing
CURRENCY_OPTIONS so DEFAULT_CURRENCY and other currency variables are validated
at compile time; derive CurrencyCode as keyof or value-union of CURRENCY_OPTIONS
(referencing CURRENCY_OPTIONS) and annotate DEFAULT_CURRENCY (and any related
variables/functions that accept currency codes) with that CurrencyCode type to
ensure compiler catches invalid currency strings.
- Around line 9-39: Extract a named interface for the currency option and derive
a union type from CURRENCY_OPTIONS: define an exported interface CurrencyOption
{ value: string; label: string } (or use literal types for value if desired),
change the CURRENCY_OPTIONS declaration to use readonly CurrencyOption[], and
export a CurrencyCode = typeof CURRENCY_OPTIONS[number]['value'] union type so
consumers can import CurrencyOption and CurrencyCode; update any references to
the inline type to use CurrencyOption and ensure CURRENCY_OPTIONS remains
readonly.
🪄 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: 0d29a8fd-90c3-48b1-8907-ac05a2472ad3

📥 Commits

Reviewing files that changed from the base of the PR and between 2189284 and 12cf5f2.

📒 Files selected for processing (17)
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/currencies.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 Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Dashboard Test
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (2)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones. Check the required component inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, TaskStatusIndicator, PriorityBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)) for colors; never hardcode hex values
Use font-sans or font-mono for typography (maps to Geist tokens); never set fontFamily directly
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables for shadows and borders (var(--so-shadow-card-hover), border-border, border-bright)
Do NOT recreate status dots inline -- use <StatusBadge>; do NOT build card-with-header layouts from scratch -- use <SectionCard>; do NOT create metric displays inline -- use <MetricCard>; do NOT render initials circles manually -- use <Avatar>; do NOT create complex (>8 line) JSX inside .map() -- extract to shared component
TypeScript 6.0: remove baseUrl (deprecated, will stop working in TS 7); esModuleInterop always true (cannot be set false); types defaults to [] (must explicitly list needed types); DOM.Iterable merged into DOM; moduleResolution: 'classic' and 'node10' removed (use 'bundler' or 'nodenext'); strict defaults to true
Storybook 10 is ESM-only; @storybook/addon-essentials, @storybook/addon-interactions, @storybook/test, @storybook/blocks no longer published (features built into core storybook); @storybook/addon-docs must be installed separately if using tags: ['autodocs']

Files:

  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/utils/currencies.ts
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
web/src/components/ui/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

When creating new shared UI components, place in web/src/components/ui/ with kebab-case filename, create adjacent .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively (no hardcoded colors/fonts/spacing), import cn from @/lib/utils for conditional class merging

Files:

  • web/src/components/ui/select-field.tsx
  • web/src/components/ui/drawer.tsx
🧠 Learnings (11)
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT recreate status dots inline -- use `<StatusBadge>`; do NOT build card-with-header layouts from scratch -- use `<SectionCard>`; do NOT create metric displays inline -- use `<MetricCard>`; do NOT render initials circles manually -- use `<Avatar>`; do NOT create complex (>8 line) JSX inside `.map()` -- extract to shared component

Applied to files:

  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Check the required component inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, TaskStatusIndicator, PriorityBadge)

Applied to files:

  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/utils/currencies.ts
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : When creating new shared UI components, place in `web/src/components/ui/` with kebab-case filename, create adjacent `.stories.tsx` with all states, export props as TypeScript interface, use design tokens exclusively (no hardcoded colors/fonts/spacing), import `cn` from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/utils/currencies.ts
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/components/ui/drawer.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__/components/ui/slider-field.test.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: use `definePreview` from `storybook/react-vite` for type-safe config; import from `storybook/test` (not `storybook/test`) and `storybook/actions` (not `storybook/addon-actions`); use `parameters.backgrounds.options` (object keyed by name) + `initialGlobals.backgrounds.value`; use `parameters.a11y.test: 'error' | 'todo' | 'off'` for accessibility testing

Applied to files:

  • web/src/pages/setup/ThemePreview.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`)

Applied to files:

  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/components/ui/drawer.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware spacing 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/setup/ThemePreview.tsx
  • web/src/components/ui/drawer.tsx
📚 Learning: 2026-03-15T21:20:09.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:20:09.993Z
Learning: Applies to web/src/components/** : Vue components organized by feature (agents/, approvals/, budget/, common/, dashboard/, layout/, messages/, org-chart/, tasks/).

Applied to files:

  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to web/**/*.vue : Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in dashboard code — use generic names: `example-provider`, `example-large-001`, etc.

Applied to files:

  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors; never hardcode hex values

Applied to files:

  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/components/ui/drawer.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : TypeScript 6.0: remove `baseUrl` (deprecated, will stop working in TS 7); `esModuleInterop` always true (cannot be set false); `types` defaults to `[]` (must explicitly list needed types); `DOM.Iterable` merged into `DOM`; `moduleResolution: 'classic'` and `'node10'` removed (use `'bundler'` or `'nodenext'`); `strict` defaults to true

Applied to files:

  • web/src/components/ui/select-field.tsx
🪛 GitHub Check: Dashboard Lint
web/src/pages/setup/MiniOrgChart.tsx

[warning] 167-167:
Do not use item index in the array as its key

web/src/pages/setup/AgentsStep.tsx

[warning] 117-117:
Do not use item index in the array as its key

🔇 Additional comments (11)
web/src/pages/setup/WizardNavigation.tsx (2)

24-27: Defensive step index handling is correctly implemented.

The rawIdx === -1 fallback prevents invalid index-derived navigation state and resolves the prior edge-case concern.


30-49: Navigation button state handling looks solid.

Back/Next rendering and disabled/loading behavior are clean and consistent, and the component correctly reuses the shared Button UI primitive.

web/src/pages/setup/WizardSkeleton.tsx (1)

1-8: Good reuse of shared skeleton primitives and semantic utility classes.

This file correctly uses shared UI skeleton components and avoids forbidden inline patterns in this implementation.
As per coding guidelines: "Always reuse existing components from web/src/components/ui/ before creating new ones."

Also applies to: 10-33

web/src/__tests__/components/ui/slider-field.test.tsx (1)

1-70: LGTM! Comprehensive test coverage for SliderField.

The test suite covers all key behaviors: label rendering, value display (plain and formatted), ARIA attributes (aria-valuemin, aria-valuemax, aria-valuenow), disabled state, aria-live accessibility, and the onChange handler interaction. The tests align well with the component implementation shown in the context snippets.

web/src/pages/setup/CostEstimatePanel.tsx (3)

139-156: Budget cap initialization correctly addressed.

The toggle handler now properly initializes the budget cap value when enabling (calling onBudgetCapChange before onBudgetCapEnabledChange), ensuring the displayed default is persisted immediately. This addresses the previous review feedback.


55-81: Clean department grouping implementation.

The useMemo logic handles edge cases well: empty estimates/agents return early, missing agents fall back to "Agent {index}", and missing departments default to "Unassigned". The dependency array [estimate, agents] is correct, and alphabetical sorting provides consistent ordering.


159-164: Good: usedFallback indicator provides transparency.

The conditional disclaimer (line 161-163) clearly communicates when tier-based fallback pricing was used, aligning with the PR objective to surface the usedFallback indicator to users.

web/src/pages/setup/ThemePreview.tsx (1)

17-23: All five theme axes are exposed now.

Surfacing every setting as a data-* attribute makes the preview CSS-addressable without extra state plumbing.

web/src/pages/setup/AgentsStep.tsx (1)

25-29: Good fetch guard on initial load.

Including !agentsError here prevents automatic re-fetch loops after a failed request.

web/src/pages/setup/TemplateCompareDrawer.tsx (1)

94-128: Nice extraction of the comparison row renderer.

Moving the per-row logic into ComparisonRowEntry keeps the drawer body readable and isolates the tags-cell branch from the outer layout.

web/src/pages/setup/TemplateStep.tsx (1)

97-104: Nice fix on the tier remainder math.

Deriving smallCount from the remainder keeps the three fallback buckets aligned with agentEstimate, including solo templates.

Comment on lines +26 to +46
const handleTest = useCallback(async () => {
if (!providerName.trim()) return
setTesting(true)
setError(null)
try {
const result = await onTest(providerName.trim())
setTestResult(result)
} catch (err) {
console.error('ProviderAddForm: test connection failed:', err)
setError(getErrorMessage(err))
} finally {
setTesting(false)
}
}, [providerName, onTest])

const handleAdd = useCallback(async () => {
if (!selectedPreset || !providerName.trim()) return
setAdding(true)
setError(null)
try {
await onAdd(selectedPreset, providerName.trim(), apiKey || undefined)
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

Test Connection is not testing the draft provider config.

handleTest() ignores both selectedPreset and apiKey, so it can only call onTest(name) for an already-saved provider. On first add, the button never validates the credentials the user just entered, and the create path still allows the request to go out with apiKey omitted even though this form is scoped to auth-backed presets.

Also applies to: 95-116

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

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 26 - 46, handleTest
currently only calls onTest(providerName.trim()) and ignores selectedPreset and
apiKey, so it doesn't validate the draft config; update handleTest to pass the
preset and apiKey (e.g., call onTest(selectedPreset, providerName.trim(), apiKey
|| undefined)) and ensure it early-returns only when providerName is empty or no
preset is selected, mirroring the checks in handleAdd; also propagate the result
to setTestResult and errors to setError as before so the UI validates
credentials entered in the form rather than only saved providers.

Comment on lines +26 to +39
const handleTest = useCallback(async () => {
if (!providerName.trim()) return
setTesting(true)
setError(null)
try {
const result = await onTest(providerName.trim())
setTestResult(result)
} catch (err) {
console.error('ProviderAddForm: test connection failed:', err)
setError(getErrorMessage(err))
} finally {
setTesting(false)
}
}, [providerName, onTest])
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 | 🟡 Minor

Reset testResult when the draft changes.

A successful result survives provider-name/API-key edits and even failed retests, so the green status panel can describe a different config than the one currently on screen. Clear it before a retest and on preset/name/key changes.

Also applies to: 76-81, 91-99, 122-133

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

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 26 - 39, The testResult
is not cleared when the draft changes or before a new test, causing stale
success state to persist; modify handleTest to call setTestResult(null) before
invoking onTest (e.g., right after setTesting(true) or setError(null)), and also
clear testResult in the draft-change handlers (the functions that update
providerName, preset selection, and API key—look for the onChange handlers and
methods around lines 76-81, 91-99, 122-133) so any name/preset/key edits reset
testResult to null.

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 (2)
web/src/pages/setup/CostEstimatePanel.tsx (1)

104-110: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for the panel shell.

Line 104 through Line 110 still implement a card-with-header using raw <div> markup; this should be migrated to the shared <SectionCard> wrapper.

As per coding guidelines, "do NOT build card-with-header layouts from scratch -- use <SectionCard>".

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

In `@web/src/pages/setup/CostEstimatePanel.tsx` around lines 104 - 110, The
CostEstimatePanel is manually rendering a card shell; replace the outer card div
in the CostEstimatePanel component with the shared SectionCard component and
move the header/title and description into SectionCard's expected structure (use
SectionCard as the wrapper and put the <h3> and <p> inside its header/title slot
or children), preserving the inner content that references
estimate.assumptions.dailyTokensPerAgent.toLocaleString() and any
styling/classes currently applied to the header and body so visual appearance
remains unchanged.
web/src/pages/setup/SetupSummary.tsx (1)

98-100: ⚠️ Potential issue | 🟡 Minor

Replace index-based key in agent list with a stable identity key.

key={agent-${index}} can break row identity when the list changes and is already flagged by lint.

🔧 Proposed fix
-          {agents.map((agent, index) => (
-            <SetupAgentRow key={`agent-${index}`} agent={agent} />
+          {agents.map((agent) => (
+            <SetupAgentRow
+              key={`${agent.name}-${agent.role}-${agent.department}-${agent.tier}`}
+              agent={agent}
+            />
           ))}
#!/bin/bash
# Verify index-based keys in setup pages
rg -nP --type=tsx 'key=\{\s*`?agent-\$\{index\}`?\s*\}|key=\{\s*index\s*\}' web/src/pages/setup
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/SetupSummary.tsx` around lines 98 - 100, The list is
using an index-based React key for SetupAgentRow which can break identity when
agents change; update the map to use a stable unique property from each agent
(e.g., agent.id or another guaranteed-unique field) as the key instead of
`agent-${index}` so that the SetupAgentRow components preserve identity across
reorders/updates; locate the map where `agents.map((agent, index) =>
<SetupAgentRow key={`agent-${index}`} agent={agent} />)` and replace the key
expression with the chosen stable identifier (fall back to a deterministic
composite like `${agent.id}-${agent.name}` only if a single unique id is not
available).
🤖 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/components/ui/select-field.tsx`:
- Around line 41-45: The SelectField UI component is using hardcoded utility
classes (gap-1.5, ml-0.5, px-3, py-2, text-sm, text-xs); update the JSX in
web/src/components/ui/select-field.tsx (label container, required span,
input/select wrapper, and helper text) to use the project's design-token
classes/variables instead of these Tailwind utility values—replace spacing
classes with the token spacing classes and font-size classes with token
typography classes from your design token module (use the same token names used
across other ui primitives) so the SelectField component and its elements
consistently consume design tokens.

In `@web/src/pages/setup/ProvidersStep.tsx`:
- Line 1: The effect currently re-triggers probeAllPresets() indefinitely when
probeResultsCount stays 0; add a guard to stop repeated auto-probes by tracking
attempts or an "exhausted" flag (e.g., a new state or useRef like
probeAttemptCount or probesExhausted) and returning early from the useEffect
when probesExhausted is true or probeAttemptCount has reached 1 (or a small
max). Specifically, update the effect that calls probeAllPresets() to (1)
increment the attempt counter or set probesExhausted when probeAllPresets
resolves with zero successful results, and (2) check that counter/flag at the
top of the useEffect and early-return so you no longer call probeAllPresets()
again when probeResultsCount === 0.

In `@web/src/utils/currencies.ts`:
- Around line 10-42: CURRENCY_OPTIONS is explicitly annotated as readonly {
value: string; label: string }[] which widens the element types to string and
causes CurrencyCode to become string; remove that explicit type annotation on
CURRENCY_OPTIONS and instead export the array with literal preservation using
"as const" combined with a "satisfies readonly { value: string; label: string
}[]" constraint (keep the export name CURRENCY_OPTIONS). This preserves literal
currency codes so CurrencyCode = (typeof CURRENCY_OPTIONS)[number]['value']
becomes the intended union of ISO code literals while still validating each item
shape.

---

Duplicate comments:
In `@web/src/pages/setup/CostEstimatePanel.tsx`:
- Around line 104-110: The CostEstimatePanel is manually rendering a card shell;
replace the outer card div in the CostEstimatePanel component with the shared
SectionCard component and move the header/title and description into
SectionCard's expected structure (use SectionCard as the wrapper and put the
<h3> and <p> inside its header/title slot or children), preserving the inner
content that references
estimate.assumptions.dailyTokensPerAgent.toLocaleString() and any
styling/classes currently applied to the header and body so visual appearance
remains unchanged.

In `@web/src/pages/setup/SetupSummary.tsx`:
- Around line 98-100: The list is using an index-based React key for
SetupAgentRow which can break identity when agents change; update the map to use
a stable unique property from each agent (e.g., agent.id or another
guaranteed-unique field) as the key instead of `agent-${index}` so that the
SetupAgentRow components preserve identity across reorders/updates; locate the
map where `agents.map((agent, index) => <SetupAgentRow key={`agent-${index}`}
agent={agent} />)` and replace the key expression with the chosen stable
identifier (fall back to a deterministic composite like
`${agent.id}-${agent.name}` only if a single unique id is not available).
🪄 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: 59dbb855-cbca-4519-a3d8-00eb52440d99

📥 Commits

Reviewing files that changed from the base of the PR and between 12cf5f2 and d31527d.

📒 Files selected for processing (7)
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/utils/currencies.ts
  • web/src/utils/provider-status.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 Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Dashboard Test
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (2)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{tsx,ts}: Always reuse existing components from web/src/components/ui/ before creating new ones. Check the required component inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, TaskStatusIndicator, PriorityBadge)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)) for colors; never hardcode hex values
Use font-sans or font-mono for typography (maps to Geist tokens); never set fontFamily directly
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Use token variables for shadows and borders (var(--so-shadow-card-hover), border-border, border-bright)
Do NOT recreate status dots inline -- use <StatusBadge>; do NOT build card-with-header layouts from scratch -- use <SectionCard>; do NOT create metric displays inline -- use <MetricCard>; do NOT render initials circles manually -- use <Avatar>; do NOT create complex (>8 line) JSX inside .map() -- extract to shared component
TypeScript 6.0: remove baseUrl (deprecated, will stop working in TS 7); esModuleInterop always true (cannot be set false); types defaults to [] (must explicitly list needed types); DOM.Iterable merged into DOM; moduleResolution: 'classic' and 'node10' removed (use 'bundler' or 'nodenext'); strict defaults to true
Storybook 10 is ESM-only; @storybook/addon-essentials, @storybook/addon-interactions, @storybook/test, @storybook/blocks no longer published (features built into core storybook); @storybook/addon-docs must be installed separately if using tags: ['autodocs']

Files:

  • web/src/utils/currencies.ts
  • web/src/utils/provider-status.ts
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
web/src/components/ui/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

When creating new shared UI components, place in web/src/components/ui/ with kebab-case filename, create adjacent .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively (no hardcoded colors/fonts/spacing), import cn from @/lib/utils for conditional class merging

Files:

  • web/src/components/ui/select-field.tsx
🧠 Learnings (4)
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : TypeScript 6.0: remove `baseUrl` (deprecated, will stop working in TS 7); `esModuleInterop` always true (cannot be set false); `types` defaults to `[]` (must explicitly list needed types); `DOM.Iterable` merged into `DOM`; `moduleResolution: 'classic'` and `'node10'` removed (use `'bundler'` or `'nodenext'`); `strict` defaults to true

Applied to files:

  • web/src/utils/currencies.ts
  • web/src/components/ui/select-field.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : When creating new shared UI components, place in `web/src/components/ui/` with kebab-case filename, create adjacent `.stories.tsx` with all states, export props as TypeScript interface, use design tokens exclusively (no hardcoded colors/fonts/spacing), import `cn` from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/utils/currencies.ts
  • web/src/components/ui/select-field.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT recreate status dots inline -- use `<StatusBadge>`; do NOT build card-with-header layouts from scratch -- use `<SectionCard>`; do NOT create metric displays inline -- use `<MetricCard>`; do NOT render initials circles manually -- use `<Avatar>`; do NOT create complex (>8 line) JSX inside `.map()` -- extract to shared component

Applied to files:

  • web/src/pages/setup/SetupSummary.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
📚 Learning: 2026-03-27T12:41:55.315Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:41:55.315Z
Learning: Applies to web/src/**/*.{tsx,ts} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Check the required component inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, TaskStatusIndicator, PriorityBadge)

Applied to files:

  • web/src/pages/setup/SetupSummary.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
🪛 GitHub Check: Dashboard Lint
web/src/pages/setup/SetupSummary.tsx

[warning] 99-99:
Do not use item index in the array as its key

🔇 Additional comments (6)
web/src/pages/setup/CostEstimatePanel.tsx (2)

145-150: Budget-cap default initialization is correctly persisted now.

Good fix: enabling the toggle with budgetCap === null now immediately persists Math.ceil(estimate.monthlyTotal * 2) before enabling state.


43-57: Breakdown row extraction and stable keys look solid.

The mapped breakdown JSX is cleanly extracted and keyed with a stable identifier (agentIndex), which avoids prior collision risks.

Also applies to: 132-134

web/src/components/ui/select-field.tsx (1)

46-55: Accessibility wiring looks correct and complete.

required, aria-required, aria-invalid, aria-errormessage, and conditional aria-describedby are now coherently applied, and the rendered hint/error nodes match the referenced ARIA attributes.

web/src/utils/provider-status.ts (1)

5-10: Good centralization of provider credential status mapping.

This helper cleanly consolidates auth-type-specific status derivation and removes duplicated logic in the setup UI.

web/src/pages/setup/CompleteStep.tsx (1)

40-46: Completion failure flow is correctly guarded now.

Catching the rejected wizardCompleteSetup() call and returning early preserves the dialog/error state and prevents false success navigation.

web/src/pages/setup/SetupSummary.tsx (1)

1-7: Nice adherence to shared UI and map extraction guidelines.

Using SectionCard/MetricCard/StatusBadge/Avatar and extracting SetupAgentRow keeps the summary maintainable.

As per coding guidelines, "Always reuse existing components from web/src/components/ui/" and "do NOT create complex (>8 line) JSX inside .map() -- extract to shared component".

Also applies to: 96-100

Comment on lines +41 to +45
<div className="flex flex-col gap-1.5">
<label htmlFor={id} className="text-sm font-medium text-foreground">
{label}
{required && <span className="ml-0.5 text-danger">*</span>}
</label>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use design-token classes for spacing/typography in this shared UI primitive.

This component still uses fixed utility spacing/size classes (gap-1.5, ml-0.5, px-3, py-2, text-sm, text-xs). For web/src/components/ui/*, please switch these to project design-token classes/variables to keep primitives fully token-driven.

As per coding guidelines: web/src/components/ui/*.{tsx,ts} must “use design tokens exclusively (no hardcoded colors/fonts/spacing)”.

Also applies to: 57-59, 74-77

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

In `@web/src/components/ui/select-field.tsx` around lines 41 - 45, The SelectField
UI component is using hardcoded utility classes (gap-1.5, ml-0.5, px-3, py-2,
text-sm, text-xs); update the JSX in web/src/components/ui/select-field.tsx
(label container, required span, input/select wrapper, and helper text) to use
the project's design-token classes/variables instead of these Tailwind utility
values—replace spacing classes with the token spacing classes and font-size
classes with token typography classes from your design token module (use the
same token names used across other ui primitives) so the SelectField component
and its elements consistently consume design tokens.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 14:00 — with GitHub Actions Inactive
@Aureliolo Aureliolo force-pushed the feat/setup-wizard-rebuild branch from 90ecdf7 to 0e36bd7 Compare March 27, 2026 14:30
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 14:31 — with GitHub Actions Inactive
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: 10

♻️ Duplicate comments (18)
web/src/pages/setup/TemplateCard.tsx (1)

31-36: ⚠️ Potential issue | 🟠 Major

Use shared card primitive and tokenized shadow for selected state.

This container still hand-rolls a card and uses a custom shadow expression. Please switch to SectionCard and a shared shadow tokenized style for selection state.

🔧 Proposed direction
 import { cn } from '@/lib/utils'
 import { Button } from '@/components/ui/button'
 import { StatPill } from '@/components/ui/stat-pill'
+import { SectionCard } from '@/components/ui/section-card'
@@
-    <div
+    <SectionCard
       className={cn(
-        'flex flex-col gap-3 rounded-lg border bg-card p-4 transition-colors',
-        selected ? 'border-accent shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)]' : 'border-border',
+        'flex flex-col gap-3 p-4 transition-colors',
+        selected ? 'border-accent shadow-[var(--so-shadow-card-hover)]' : 'border-border',
         'hover:bg-card-hover',
       )}
     >
@@
-    </div>
+    </SectionCard>

As per coding guidelines, "ALWAYS reuse existing components from web/src/components/ui/" and "Use token variables for shadows and borders ... NEVER hardcode values."

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

In `@web/src/pages/setup/TemplateCard.tsx` around lines 31 - 36, TemplateCard
currently hand-rolls a card div with inline classes and a hardcoded shadow
expression; replace that element with the shared SectionCard component and use
the tokenized selected shadow class instead of the custom expression. In
TemplateCard.tsx, swap the outer div (the one building className with cn(...,
selected ? 'border-accent shadow-[0_0_12px_color-mix(...)' : 'border-border',
...)) to render SectionCard, pass through the same children and props, map the
selected branch to the shared token class (e.g., 'ring-selected' or whatever
tokenized shadow token your design system exposes) and use the standard border
token classes rather than the hardcoded 'border-border'/'border-accent' strings
so the card fully uses web/src/components/ui/SectionCard and tokenized
shadow/border styles.
web/src/pages/setup/WizardSkeleton.tsx (1)

9-14: 🧹 Nitpick | 🔵 Trivial

Avoid hardcoded skeleton step count.

The placeholder count is still fixed at 7, which can drift from the actual wizard step source over time. Derive it from shared step metadata (or pass it in) to keep loading UI in sync.

♻️ Proposed refactor
-export function WizardSkeleton() {
+export function WizardSkeleton({ stepCount }: { stepCount: number }) {
   return (
@@
-          {Array.from({ length: 7 }, (_, i) => (
+          {Array.from({ length: stepCount }, (_, i) => (
             <div key={i} className="flex flex-col items-center gap-1">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/WizardSkeleton.tsx` around lines 9 - 14, The skeleton
currently renders a hardcoded 7 placeholders in WizardSkeleton.tsx; replace the
magic number by deriving the count from the real wizard metadata or a prop so
the loading UI stays in sync. Update WizardSkeleton to accept a prop like
stepCount (or import the shared steps array/constant used by the wizard and use
steps.length) and replace Array.from({ length: 7 }) with that dynamic value;
then update any callers to pass the stepCount or ensure the shared steps
constant is available to import. Ensure the Skeleton rendering loop keying and
classes remain unchanged.
web/src/components/ui/select-field.tsx (2)

53-55: ⚠️ Potential issue | 🟡 Minor

Keep the rendered error message attached to the field.

When hasError is true, the hint disappears and aria-describedby is cleared even though the error text is rendered. Point aria-describedby at errorId in the error state so the field stays explicitly associated with the visible message.

🔧 Suggested fix
   const errorId = `${id}-error`
   const hintId = `${id}-hint`
   const hasError = !!error
+  const describedBy = hasError ? errorId : hint ? hintId : undefined
 ...
-        aria-describedby={hint && !hasError ? hintId : undefined}
+        aria-describedby={describedBy}

Also applies to: 76-77

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

In `@web/src/components/ui/select-field.tsx` around lines 53 - 55, The field
currently clears aria-describedby when hasError is true even though an error
message (errorId) is rendered; update the SelectField component so
aria-describedby points to errorId when hasError is true, otherwise it should
point to hintId when hint exists (or undefined if neither); apply the same
change to the other identical block referenced (lines with aria attributes at
the second occurrence) and ensure you reference the hasError, errorId, hintId
symbols in the SelectField JSX where aria-describedby is set.

41-45: 🛠️ Refactor suggestion | 🟠 Major

Replace fixed spacing/type utilities in this shared field.

gap-1.5, ml-0.5, px-3, py-2, text-sm, and text-xs are still hardcoded in a web/src/components/ui/* primitive. This component should consume the same spacing/typography tokens as the rest of the design system.

Based on learnings, web/src/components/ui/*.tsx components must "use design tokens exclusively -- no hardcoded colors, fonts, or spacing".

Also applies to: 56-59, 73-77

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

In `@web/src/components/ui/select-field.tsx` around lines 41 - 45, The SelectField
component in web/src/components/ui/select-field.tsx uses hardcoded utility
classes (gap-1.5, ml-0.5, px-3, py-2, text-sm, text-xs) that must be replaced
with the design system’s spacing and typography tokens; update the wrapper,
label, select/input and helper/error text to consume the shared token values
(e.g., spacing/token variables and typography/token classes) instead of literal
utilities, import the tokens or token-aware class helper used across other ui
primitives, and swap each occurrence of gap-1.5, ml-0.5, px-3, py-2, text-sm and
text-xs with the appropriate token references so the component (SelectField)
conforms to the token-based styling convention.
web/src/components/ui/drawer.tsx (2)

47-65: ⚠️ Potential issue | 🟠 Major

Keep focus trapped when the drawer container itself has focus.

Line 47 initially focuses panel. From that state, Shift+Tab will not hit either boundary branch, so focus can jump back into the page behind the modal. Treat panel or any outside-panel active element as a wrap condition.

🔧 Suggested fix
     const handleTab = (e: KeyboardEvent) => {
       if (e.key !== 'Tab') return
       const focusable = panel.querySelectorAll<HTMLElement>(
         'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
       )
-      if (focusable.length === 0) return
+      if (focusable.length === 0) {
+        e.preventDefault()
+        panel.focus()
+        return
+      }
       const first = focusable[0]!
       const last = focusable[focusable.length - 1]!
-      if (e.shiftKey && document.activeElement === first) {
+      const active = document.activeElement
+      if (active === panel || !panel.contains(active)) {
+        e.preventDefault()
+        if (e.shiftKey) {
+          last.focus()
+        } else {
+          first.focus()
+        }
+      } else if (e.shiftKey && active === first) {
         e.preventDefault()
         last.focus()
-      } else if (!e.shiftKey && document.activeElement === last) {
+      } else if (!e.shiftKey && active === last) {
         e.preventDefault()
         first.focus()
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/drawer.tsx` around lines 47 - 65, The focus trap fails
when the drawer container (panel) itself is focused because handleTab only
checks the first/last focusable elements; update the handleTab function (used
with panelRef and panel) to also treat document.activeElement === panel or
document.activeElement not contained within panel as a wrap condition: if
Shift+Tab and (document.activeElement === first || document.activeElement ===
panel || !panel.contains(document.activeElement)) then preventDefault and focus
last; similarly, if Tab and (document.activeElement === last ||
document.activeElement === panel || !panel.contains(document.activeElement))
then preventDefault and focus first. Ensure you reference panelRef.current as
panel and keep the existing early returns for empty focusable.

104-126: 🛠️ Refactor suggestion | 🟠 Major

Finish tokenizing this shared drawer primitive.

w-[40vw], px-4 py-3, and p-4 keep the component tied to raw utilities instead of the design system. Please move the remaining sizing/padding choices to semantic/token classes or expose size variants on Drawer.

Based on learnings, web/src/components/ui/*.tsx components must "use design tokens exclusively -- no hardcoded colors, fonts, or spacing".

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

In `@web/src/components/ui/drawer.tsx` around lines 104 - 126, The Drawer
component still uses raw utility classes (w-[40vw], px-4 py-3, p-4) which
violate the design-token rule; update the root container and inner
header/content elements in the Drawer (the element that composes className with
'fixed inset-y-0 right-0 ...', the header div with 'px-4 py-3', and the content
div with 'p-4') to use semantic/token classes or expose and consume size/padding
variants on the Drawer API (e.g., size="sm|md|lg" or padding token props) so
spacing and width come from design tokens rather than hardcoded utilities;
ensure the new variant names map to token classes and keep existing className
merging behavior.
web/src/pages/setup/ProviderProbeResults.tsx (1)

44-46: ⚠️ Potential issue | 🟡 Minor

Type probeResults as sparse state.

Line 75 already reads missing entries as possible, but this prop says every string key is populated. That forces callers to overstate the runtime shape and hides absent probe results from the type system.

🔧 Suggested fix
 export interface ProviderProbeResultsProps {
   presets: readonly ProviderPreset[]
-  probeResults: Readonly<Record<string, ProbePresetResponse>>
+  probeResults: Readonly<Partial<Record<string, ProbePresetResponse>>>
   probing: boolean
   onAddPreset: (presetName: string) => void
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/ProviderProbeResults.tsx` around lines 44 - 46, The prop
type ProviderProbeResultsProps currently declares probeResults as
Readonly<Record<string, ProbePresetResponse>> which asserts every key exists;
change it to a sparse type so missing entries are representable (e.g.
Readonly<Partial<Record<string, ProbePresetResponse>>> or
Readonly<Record<string, ProbePresetResponse | undefined>>). Update the
ProviderProbeResultsProps declaration (probeResults) and ensure any access sites
(see code that reads probeResults at line ~75) handle undefined accordingly.
web/src/pages/setup/ThemePreview.tsx (1)

32-41: ⚠️ Potential issue | 🟡 Minor

Use the shared AgentCard component instead of custom markup.

This block recreates the agent card pattern manually, violating the design system guideline to reuse existing components. However, note that AgentCard requires role and department as separate props—the current "CEO, executive" string will need to be split accordingly. Also, AgentCard does not accept a size prop; the Avatar is internally sized at md. Refactor to use AgentCard with properly separated role and department values.

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

In `@web/src/pages/setup/ThemePreview.tsx` around lines 32 - 41, Replace the
custom markup block with the shared AgentCard component: remove the manual
Avatar and surrounding div, and render <AgentCard name="Akira Tanaka"
status="idle" role="CEO" department="executive" /> (or the equivalent JSX for
AgentCard in this codebase); do not pass a size prop (AgentCard/Avatar sizes are
handled internally) and ensure the status prop supplies the current state so the
internal StatusBadge renders with its label. This updates ThemePreview to reuse
AgentCard and splits the combined "CEO, executive" string into role="CEO" and
department="executive".
web/src/__tests__/utils/template-categories.test.ts (1)

75-88: ⚠️ Potential issue | 🟡 Minor

Test derives expected value from the result under test.

The test computes expectedOrder using result.has(c), which means if categorizeTemplates incorrectly drops a category, the test still passes. Derive the expected categories from the fixture templates instead.

🔧 Proposed fix
   it('returns categories in CATEGORY_ORDER', () => {
     const templates = [
       makeTemplate({ name: 'r', tags: ['research'] }),
       makeTemplate({ name: 's', tags: ['startup'] }),
       makeTemplate({ name: 'd', tags: ['dev-shop'] }),
       makeTemplate({ name: 'e', tags: ['enterprise'] }),
     ]
     const result = categorizeTemplates(templates)
     const keys = [...result.keys()]

-    // Verify order matches CATEGORY_ORDER for present categories
-    const expectedOrder = CATEGORY_ORDER.filter((c) => result.has(c))
+    // Expected categories based on fixture template tags
+    const fixtureCategories = new Set(['research', 'startup', 'dev-shop', 'enterprise'])
+    const expectedOrder = CATEGORY_ORDER.filter((c) => fixtureCategories.has(c))
     expect(keys).toEqual(expectedOrder)
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/utils/template-categories.test.ts` around lines 75 - 88,
The test currently derives expectedOrder from the function under test (using
result.has), which masks failures; change it to compute expectedOrder from the
fixture templates instead: build a set of categories present in the templates
array (using the tags on makeTemplate fixtures) and then filter CATEGORY_ORDER
by that set to produce expectedOrder, then assert keys equals that
expectedOrder; update the test that references categorizeTemplates,
CATEGORY_ORDER, makeTemplate, templates, and result accordingly.
web/src/pages/setup/WizardShell.tsx (1)

93-104: ⚠️ Potential issue | 🟠 Major

Skip button is broken — shows "Setup incomplete" error instead of skip form.

When users click skip on the account/template steps, the button navigates to /setup/complete, loading CompleteStep. Since the user hasn't completed the wizard, companyResponse is null, so CompleteStep returns an EmptyState with "Setup incomplete" error.

SkipWizardForm exists and implements the proper skip flow (company name input → submit → complete), but it's never rendered. The skip button should navigate to a route that displays SkipWizardForm instead.

🔧 Suggested fix

Add a skip route to STEP_COMPONENTS mapping and update the skip button:

const STEP_COMPONENTS: Record<WizardStep, React.ComponentType> = {
  account: AccountStep,
  template: TemplateStep,
  company: CompanyStep,
  agents: AgentsStep,
  providers: ProvidersStep,
  theme: ThemeStep,
  complete: CompleteStep,
+  skip: SkipWizardForm,
}
-            onClick={() => navigate(`/setup/complete`)}
+            onClick={() => navigate(`/setup/skip`)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/WizardShell.tsx` around lines 93 - 104, The skip button
currently navigates to `/setup/complete` which renders CompleteStep and fails
because companyResponse is null; update STEP_COMPONENTS to include a 'skip'
entry that maps to SkipWizardForm (reference STEP_COMPONENTS and SkipWizardForm)
and change the Button onClick in WizardShell (the Skip wizard button) to
navigate to `/setup/skip` so the SkipWizardForm is rendered instead of
CompleteStep.
web/src/pages/setup/SetupSummary.tsx (1)

96-102: 🧹 Nitpick | 🔵 Trivial

Same index-based key concern as AgentsStep.

Line 99 uses key={`agent-${index}`} which has the same stability concern noted in AgentsStep.tsx. For consistency, apply the same fix here if a stable identifier becomes available.

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

In `@web/src/pages/setup/SetupSummary.tsx` around lines 96 - 102, The agent list
in SetupSummary uses an unstable index-based key (key={`agent-${index}`}) when
rendering SetupAgentRow in the agents.map callback; replace the index key with a
stable unique identifier from the agent object (e.g., agent.id, agent.uuid, or a
unique agent.name) when available to avoid re-render issues—update the map in
SetupSummary (where agents.map produces <SetupAgentRow ... />) to use that
stable property as the key and fall back to a deterministic composite (like
`${agent.name}-${agent.createdAt}`) only if no single stable id exists.
web/src/utils/template-categories.ts (1)

54-62: 🧹 Nitpick | 🔵 Trivial

Category resolution round-trips through display labels, creating fragile coupling.

getTemplateCategory maps tag → CATEGORY_LABELS[tag] → LABEL_TO_CATEGORY[label]. If a display label changes (e.g., 'Startup''Startups') without updating LABEL_TO_CATEGORY, templates silently fall back to 'other'. Consider a direct TAG_TO_CATEGORY map that maps tags to canonical category keys, keeping CATEGORY_LABELS solely for display.

♻️ Proposed simplification
+/** Direct mapping from tag to canonical category key */
+const TAG_TO_CATEGORY: Readonly<Record<string, string>> = {
+  startup: 'startup',
+  solo: 'startup',
+  'small-team': 'startup',
+  mvp: 'startup',
+  'dev-shop': 'dev-shop',
+  'data-team': 'dev-shop',
+  product: 'product',
+  agile: 'product',
+  enterprise: 'enterprise',
+  'full-company': 'enterprise',
+  consultancy: 'consultancy',
+  agency: 'consultancy',
+  research: 'research',
+}
+
+/** Display labels for canonical category keys */
+const CATEGORY_DISPLAY_LABELS: Readonly<Record<string, string>> = {
+  startup: 'Startup',
+  'dev-shop': 'Development',
+  product: 'Product',
+  enterprise: 'Enterprise',
+  consultancy: 'Professional Services',
+  research: 'Research',
+  other: 'Other',
+}

 function getTemplateCategory(template: TemplateInfoResponse): string {
   for (const tag of template.tags) {
-    const label = CATEGORY_LABELS[tag]
-    if (label) {
-      return LABEL_TO_CATEGORY[label] ?? 'other'
-    }
+    const category = TAG_TO_CATEGORY[tag]
+    if (category) return category
   }
   return 'other'
 }

 export function getCategoryLabel(category: string): string {
-  for (const [label, key] of Object.entries(LABEL_TO_CATEGORY)) {
-    if (key === category) return label
-  }
+  const label = CATEGORY_DISPLAY_LABELS[category]
+  if (label) return label
   // Title-case fallback...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/template-categories.ts` around lines 54 - 62,
getTemplateCategory currently resolves categories by mapping tag →
CATEGORY_LABELS[tag] → LABEL_TO_CATEGORY[label], which breaks if display labels
change; introduce a new TAG_TO_CATEGORY map that directly maps each tag to its
canonical category key and update getTemplateCategory to return
TAG_TO_CATEGORY[tag] ?? 'other' (leaving CATEGORY_LABELS in place for UI-only
display labels); update any tests or usages that relied on the old indirection
and document TAG_TO_CATEGORY alongside CATEGORY_LABELS.
web/src/pages/setup/TemplateVariables.tsx (2)

4-10: 🧹 Nitpick | 🔵 Trivial

Consider extending TemplateVariable interface with constraint properties.

The interface lacks optional min, max, and step properties, forcing hardcoded slider bounds (lines 48-50). Templates may define specific valid ranges that should be respected.

♻️ Suggested interface extension
 interface TemplateVariable {
   readonly name: string
   readonly description: string
   readonly var_type: string
   readonly default: string | number | boolean | null
   readonly required: boolean
+  readonly min?: number
+  readonly max?: number
+  readonly step?: number
 }

Then update the slider to use these when available:

             <SliderField
               key={v.name}
               label={v.description || v.name}
               value={numValue}
-              min={v.var_type === 'int' ? 1 : 0}
-              max={v.var_type === 'int' ? 50 : 1000}
-              step={v.var_type === 'int' ? 1 : 10}
+              min={v.min ?? (v.var_type === 'int' ? 1 : 0)}
+              max={v.max ?? (v.var_type === 'int' ? 50 : 1000)}
+              step={v.step ?? (v.var_type === 'int' ? 1 : 10)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 4 - 10, Extend the
TemplateVariable interface by adding optional numeric constraint properties
(min?: number, max?: number, step?: number) and update any slider usage in
TemplateVariables.tsx to read and prefer these properties (e.g., use
variable.min/variable.max/variable.step) when rendering bounds and increments,
falling back to the current hardcoded defaults if they are undefined; ensure the
TemplateVariable type is exported/used wherever template variables are typed so
callers can supply these constraints.

21-28: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this card-with-header layout.

This hand-rolled card structure duplicates the pattern that SectionCard provides. Per coding guidelines: "do NOT build card-with-header layouts from scratch -- use <SectionCard>".

♻️ Proposed refactor
+import { SectionCard } from '@/components/ui/section-card'
 import { SliderField } from '@/components/ui/slider-field'
 import { ToggleField } from '@/components/ui/toggle-field'
...
   return (
-    <div className="space-y-4 rounded-lg border border-border bg-card p-4">
-      <div className="space-y-1">
-        <h3 className="text-sm font-semibold text-foreground">Template Variables</h3>
-        <p className="text-xs text-muted-foreground">
-          Customize how the template generates your company structure.
-        </p>
-      </div>
+    <SectionCard
+      title="Template Variables"
+      description="Customize how the template generates your company structure."
+    >
       {variables.map((v) => {
         // ... existing control rendering
       })}
-    </div>
+    </SectionCard>
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 21 - 28, The card
header/layout in the TemplateVariables component is hand-rolled; replace the
outer div and its header markup with the shared SectionCard component: import
SectionCard, remove the manual container (the div with classes "space-y-4
rounded-lg border border-border bg-card p-4") and the inner h3/paragraph header,
and wrap the existing body content with <SectionCard> while supplying the
"Template Variables" heading (and the brief description text) via SectionCard's
title/description props or children per the SectionCard API so the shared
component handles the card-with-header layout and styles.
web/src/pages/setup/ProviderAddForm.tsx (1)

26-39: ⚠️ Potential issue | 🟠 Major

handleTest doesn't validate the draft credentials.

The test handler only passes providerName to onTest(), ignoring selectedPreset and apiKey. For a new cloud provider being added, this means the "Test Connection" button cannot actually validate the credentials the user just entered—it would only work for already-configured providers.

Additionally, testResult persists across input changes, so a successful test can display for a completely different configuration than what's currently on screen.

🐛 Proposed fix
+  // Clear stale test result when inputs change
+  const clearTestResult = useCallback(() => {
+    setTestResult(null)
+  }, [])

   const handleTest = useCallback(async () => {
-    if (!providerName.trim()) return
+    if (!selectedPreset || !providerName.trim()) return
     setTesting(true)
     setError(null)
+    setTestResult(null)
     try {
-      const result = await onTest(providerName.trim())
+      const result = await onTest(providerName.trim(), apiKey || undefined)
       setTestResult(result)
     } catch (err) {
       console.error('ProviderAddForm: test connection failed:', err)
       setError(getErrorMessage(err))
     } finally {
       setTesting(false)
     }
-  }, [providerName, onTest])
+  }, [selectedPreset, providerName, apiKey, onTest])

Also clear testResult when inputs change:

         onChange={(val) => {
           setSelectedPreset(val)
+          setTestResult(null)
           if (!providerName) {
             setProviderName(val)
           }
         }}
-            onChange={(e) => setProviderName(e.currentTarget.value)}
+            onChange={(e) => {
+              setProviderName(e.currentTarget.value)
+              setTestResult(null)
+            }}
-            onChange={(e) => setApiKey(e.currentTarget.value)}
+            onChange={(e) => {
+              setApiKey(e.currentTarget.value)
+              setTestResult(null)
+            }}

Note: This also requires updating ProviderAddFormProps.onTest signature to accept the API key:

-  onTest: (name: string) => Promise<TestConnectionResponse>
+  onTest: (name: string, apiKey?: string) => Promise<TestConnectionResponse>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 26 - 39, handleTest
currently only passes providerName to onTest and doesn't validate or include the
draft credentials (selectedPreset and apiKey), and testResult is left stale when
inputs change; update handleTest to validate that providerName.trim() and the
current apiKey/selectedPreset are present and call onTest with both
providerName.trim() and the apiKey (update the ProviderAddFormProps.onTest
signature accordingly), and clear testResult via setTestResult(null) whenever
providerName, selectedPreset, or apiKey change (e.g., in their change handlers
or a useEffect watching those values) so test results always reflect the current
inputs.
web/src/pages/setup/CostEstimatePanel.tsx (1)

103-110: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this panel shell.

This is still a hand-built card-with-header layout. Wrap the existing content in the shared SectionCard instead of open-coding the outer container/header.

As per coding guidelines, "Do NOT build card-with-header layouts from scratch -- use <SectionCard>" and "ALWAYS reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.) before creating new ones".

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

In `@web/src/pages/setup/CostEstimatePanel.tsx` around lines 103 - 110, Replace
the manually coded card/container in the CostEstimatePanel component with the
shared SectionCard: remove the outer <div className="space-y-4 rounded-lg border
border-border bg-card p-4"> wrapper and instead wrap the existing header and
content inside <SectionCard> (import SectionCard from the shared UI components),
moving the <h3> and description into the SectionCard header/body as appropriate;
ensure you keep the existing text and use SectionCard's props/classes for
spacing so no duplicated styling remains.
web/src/pages/setup/CompanyStep.tsx (1)

182-184: ⚠️ Potential issue | 🟡 Minor

agent.name is still not a safe React key.

Inline rename/randomize makes duplicate display names plausible, and React will then reconcile the wrong preview row. Carry a stable id through SetupAgentSummary and use that here instead.

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

In `@web/src/pages/setup/CompanyStep.tsx` around lines 182 - 184, The list key is
unsafe because agents.map currently uses agent.name as the React key; change it
to use a stable unique identifier (e.g., agent.id) carried through from
SetupAgentSummary into the agent objects so React can reliably reconcile items.
Update the mapping in CompanyStep.tsx (the agents.map(...) expression and its
<li key=...>) to use the stable id property instead of agent.name, and ensure
SetupAgentSummary produces/populates that id on each agent object passed back to
this component.
web/src/stores/setup-wizard.ts (1)

286-309: ⚠️ Potential issue | 🔴 Critical

Template edits still leave stale generated state, and the variable controls never reach the API.

After the first successful apply, selectTemplate(), setTemplateVariable(), setCompanyName(), and setCompanyDescription() keep companyResponse/agents intact, so CompanyStep can stay "complete" while showing a preview for old inputs. Then submitCompany() only posts template_name, so the new sliders/toggles do not affect backend generation at all. Reset the generated state whenever any upstream template/company input changes, and include the template-variable payload in the create-company request.

Also applies to: 313-346

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

In `@web/src/stores/setup-wizard.ts` around lines 286 - 309, The store currently
lets stale generated state persist when upstream inputs change and also omits
template variables from the create-company API; update selectTemplate,
setTemplateVariable, setCompanyName, and setCompanyDescription to reset
generated state (clear companyResponse and agents and mark company as not
submitted) whenever they modify inputs, and ensure clearComparison similarly
clears generated state if it affects generation; then modify submitCompany to
include the current templateVariables (from state.templateVariables) in the
request payload sent to the create-company API so slider/toggle values are
posted to the backend.
🤖 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/__tests__/utils/setup-validation.property.test.ts`:
- Around line 45-61: The regex in the test's property generator is flagged for
nested quantifiers; replace the complex pattern used in the fc.stringMatching
call with a simpler, non-backtracking-safe pattern (e.g. start-with alnum then
explicit bounded repetition like /^[A-Za-z0-9][A-Za-z0-9 ]{0,199}$/) and keep or
remove the additional .filter length check as needed; update the generator in
the test that calls validateCompanyStep (and uses makeCompanyResponse) to use
the simpler pattern so the property-based test remains equivalent but avoids
backtracking issues.

In `@web/src/pages/setup/CompanyStep.tsx`:
- Around line 61-70: The memo for costEstimate is hardcoding provider models as
[] which forces fallback pricing; read the configured provider models from the
setup store (e.g. via the setup store selector like useSetupStore(state =>
state.providerModels) or setupStore.providerModels) and pass that array into
estimateMonthlyCost instead of []; also add that providerModels value to the
useMemo dependency list so costEstimate recalculates when provider/model pricing
changes (refer to costEstimate, agents, and estimateMonthlyCost).

In `@web/src/pages/setup/ProviderAddForm.tsx`:
- Around line 60-67: Replace the hand-rolled card-with-header markup in
ProviderAddForm with the shared SectionCard component: remove the outer div with
classes "space-y-4 rounded-lg border border-border bg-card p-4" and the inner
header block, and instead render SectionCard (import it if missing) passing the
header content ("Add Cloud Provider" title and subtitle) into the SectionCard's
header slot/props and the remaining form body as children; ensure you keep the
same text nodes ("Add Cloud Provider" and the subtitle) and CSS intent by using
SectionCard's API so the layout matches other usages like TemplateVariables.tsx.

In `@web/src/pages/setup/ProviderProbeResults.tsx`:
- Around line 61-80: Replace the manual card shell in the ProviderProbeResults
component with the shared SectionCard component: remove the outer div with
classes "space-y-3 rounded-lg border border-border bg-card p-4" and move the
header (h3 + p) into SectionCard's header/title/description slots (or as its
first children if SectionCard doesn't expose slots), then render the
localPresets.map(...) list inside SectionCard; update references to probing,
localPresets, ProbeResultItem, and onAddPreset so they remain unchanged, and
delete the duplicated spacing/header CSS now provided by SectionCard to keep
layout consistent with other setup screens.

In `@web/src/pages/setup/TemplateCompareDrawer.tsx`:
- Around line 44-67: The deriveCategory function duplicates category mapping
logic; move this mapping and iteration into a shared utility (e.g., add
deriveCategoryFromTags in `@/utils/template-categories.ts`), export it, and
replace the local deriveCategory implementation in TemplateCompareDrawer.tsx to
call deriveCategoryFromTags(template.tags) and then getCategoryLabel as needed.
Ensure the new utility contains the categoryTags map and returns the canonical
category key or 'other' so callers (like getCategoryLabel) can consistently map
to labels, and update imports/exports accordingly.

In `@web/src/pages/setup/TemplateCostBadge.tsx`:
- Around line 6-13: The prop type for TemplateCostBadge uses a raw string for
currency and duplicates the default; change the currency prop type from string
to the shared CurrencyCode type and use the shared DEFAULT_CURRENCY constant as
the default value in the TemplateCostBadge parameter list (update the import to
pull in CurrencyCode and DEFAULT_CURRENCY), and apply the same change where the
currency prop/type is declared or used in TemplateCostBadge and related lines
(e.g., any other occurrences around the component exported symbols) to enforce
valid currency codes at compile time.

In `@web/src/pages/setup/TemplateStep.tsx`:
- Around line 64-84: The current recommendedTemplates logic uses only
providerCount to flip from smallTags to largeTags, causing
TAG_ENTERPRISE/TAG_FULL_COMPANY templates to be recommended even if configured
providers don't support those tiers; update recommendedTemplates to inspect
provider model metadata instead: derive a boolean (e.g., hasEnterpriseTier) by
scanning providers' model/tier fields and only include largeTags
(TAG_ENTERPRISE, TAG_FULL_COMPANY) when that boolean is true, otherwise continue
to recommend smallTags or suppress enterprise recommendations; alternatively, if
provider model info is not yet loaded, return an empty recommended set or skip
enterprise tags until model metadata is available to avoid misleading badges.

In `@web/src/stores/setup-wizard.ts`:
- Around line 251-259: The canNavigateTo function currently treats targetIdx <=
0 as navigable which incorrectly allows steps not present in stepOrder (index
-1) to be accessed; update canNavigateTo (using stepOrder and stepsCompleted) to
first check if stepOrder.indexOf(step) === -1 and return false for missing
steps, then allow the first step (targetIdx === 0) and otherwise run the
existing loop that checks prior steps' completion.

In `@web/src/utils/cost-estimator.ts`:
- Around line 56-60: AgentCostInput currently has model_provider and model_id
but the cost lookup is keyed only by model_id, causing collisions when different
providers use the same id; update the map key construction and lookup to include
both model_provider and model_id (e.g., composite key `${provider}:${modelId}`)
wherever the lookup is built (the code that iterates configured models and
currently uses model.id) and wherever costs are read (the code that reads
agent.model_id), update typing/variable names as needed to use
agent.model_provider + agent.model_id, and add a regression test that creates
two providers with the same model_id and asserts the estimator charges the
correct provider-specific price.

In `@web/src/utils/provider-status.ts`:
- Around line 5-11: The getProviderStatus function uses an if-chain with a
fallback that can mask new AuthType additions; replace it with an exhaustive
switch on config.auth_type inside getProviderStatus (switch over 'none',
'api_key', 'oauth', 'custom_header') returning 'idle' or 'error' per case, and
include a default case that throws or uses a never-assertion to force
compile-time errors if AuthType is extended; reference ProviderConfig and
getProviderStatus to locate and update the logic.

---

Duplicate comments:
In `@web/src/__tests__/utils/template-categories.test.ts`:
- Around line 75-88: The test currently derives expectedOrder from the function
under test (using result.has), which masks failures; change it to compute
expectedOrder from the fixture templates instead: build a set of categories
present in the templates array (using the tags on makeTemplate fixtures) and
then filter CATEGORY_ORDER by that set to produce expectedOrder, then assert
keys equals that expectedOrder; update the test that references
categorizeTemplates, CATEGORY_ORDER, makeTemplate, templates, and result
accordingly.

In `@web/src/components/ui/drawer.tsx`:
- Around line 47-65: The focus trap fails when the drawer container (panel)
itself is focused because handleTab only checks the first/last focusable
elements; update the handleTab function (used with panelRef and panel) to also
treat document.activeElement === panel or document.activeElement not contained
within panel as a wrap condition: if Shift+Tab and (document.activeElement ===
first || document.activeElement === panel ||
!panel.contains(document.activeElement)) then preventDefault and focus last;
similarly, if Tab and (document.activeElement === last || document.activeElement
=== panel || !panel.contains(document.activeElement)) then preventDefault and
focus first. Ensure you reference panelRef.current as panel and keep the
existing early returns for empty focusable.
- Around line 104-126: The Drawer component still uses raw utility classes
(w-[40vw], px-4 py-3, p-4) which violate the design-token rule; update the root
container and inner header/content elements in the Drawer (the element that
composes className with 'fixed inset-y-0 right-0 ...', the header div with 'px-4
py-3', and the content div with 'p-4') to use semantic/token classes or expose
and consume size/padding variants on the Drawer API (e.g., size="sm|md|lg" or
padding token props) so spacing and width come from design tokens rather than
hardcoded utilities; ensure the new variant names map to token classes and keep
existing className merging behavior.

In `@web/src/components/ui/select-field.tsx`:
- Around line 53-55: The field currently clears aria-describedby when hasError
is true even though an error message (errorId) is rendered; update the
SelectField component so aria-describedby points to errorId when hasError is
true, otherwise it should point to hintId when hint exists (or undefined if
neither); apply the same change to the other identical block referenced (lines
with aria attributes at the second occurrence) and ensure you reference the
hasError, errorId, hintId symbols in the SelectField JSX where aria-describedby
is set.
- Around line 41-45: The SelectField component in
web/src/components/ui/select-field.tsx uses hardcoded utility classes (gap-1.5,
ml-0.5, px-3, py-2, text-sm, text-xs) that must be replaced with the design
system’s spacing and typography tokens; update the wrapper, label, select/input
and helper/error text to consume the shared token values (e.g., spacing/token
variables and typography/token classes) instead of literal utilities, import the
tokens or token-aware class helper used across other ui primitives, and swap
each occurrence of gap-1.5, ml-0.5, px-3, py-2, text-sm and text-xs with the
appropriate token references so the component (SelectField) conforms to the
token-based styling convention.

In `@web/src/pages/setup/CompanyStep.tsx`:
- Around line 182-184: The list key is unsafe because agents.map currently uses
agent.name as the React key; change it to use a stable unique identifier (e.g.,
agent.id) carried through from SetupAgentSummary into the agent objects so React
can reliably reconcile items. Update the mapping in CompanyStep.tsx (the
agents.map(...) expression and its <li key=...>) to use the stable id property
instead of agent.name, and ensure SetupAgentSummary produces/populates that id
on each agent object passed back to this component.

In `@web/src/pages/setup/CostEstimatePanel.tsx`:
- Around line 103-110: Replace the manually coded card/container in the
CostEstimatePanel component with the shared SectionCard: remove the outer <div
className="space-y-4 rounded-lg border border-border bg-card p-4"> wrapper and
instead wrap the existing header and content inside <SectionCard> (import
SectionCard from the shared UI components), moving the <h3> and description into
the SectionCard header/body as appropriate; ensure you keep the existing text
and use SectionCard's props/classes for spacing so no duplicated styling
remains.

In `@web/src/pages/setup/ProviderAddForm.tsx`:
- Around line 26-39: handleTest currently only passes providerName to onTest and
doesn't validate or include the draft credentials (selectedPreset and apiKey),
and testResult is left stale when inputs change; update handleTest to validate
that providerName.trim() and the current apiKey/selectedPreset are present and
call onTest with both providerName.trim() and the apiKey (update the
ProviderAddFormProps.onTest signature accordingly), and clear testResult via
setTestResult(null) whenever providerName, selectedPreset, or apiKey change
(e.g., in their change handlers or a useEffect watching those values) so test
results always reflect the current inputs.

In `@web/src/pages/setup/ProviderProbeResults.tsx`:
- Around line 44-46: The prop type ProviderProbeResultsProps currently declares
probeResults as Readonly<Record<string, ProbePresetResponse>> which asserts
every key exists; change it to a sparse type so missing entries are
representable (e.g. Readonly<Partial<Record<string, ProbePresetResponse>>> or
Readonly<Record<string, ProbePresetResponse | undefined>>). Update the
ProviderProbeResultsProps declaration (probeResults) and ensure any access sites
(see code that reads probeResults at line ~75) handle undefined accordingly.

In `@web/src/pages/setup/SetupSummary.tsx`:
- Around line 96-102: The agent list in SetupSummary uses an unstable
index-based key (key={`agent-${index}`}) when rendering SetupAgentRow in the
agents.map callback; replace the index key with a stable unique identifier from
the agent object (e.g., agent.id, agent.uuid, or a unique agent.name) when
available to avoid re-render issues—update the map in SetupSummary (where
agents.map produces <SetupAgentRow ... />) to use that stable property as the
key and fall back to a deterministic composite (like
`${agent.name}-${agent.createdAt}`) only if no single stable id exists.

In `@web/src/pages/setup/TemplateCard.tsx`:
- Around line 31-36: TemplateCard currently hand-rolls a card div with inline
classes and a hardcoded shadow expression; replace that element with the shared
SectionCard component and use the tokenized selected shadow class instead of the
custom expression. In TemplateCard.tsx, swap the outer div (the one building
className with cn(..., selected ? 'border-accent
shadow-[0_0_12px_color-mix(...)' : 'border-border', ...)) to render SectionCard,
pass through the same children and props, map the selected branch to the shared
token class (e.g., 'ring-selected' or whatever tokenized shadow token your
design system exposes) and use the standard border token classes rather than the
hardcoded 'border-border'/'border-accent' strings so the card fully uses
web/src/components/ui/SectionCard and tokenized shadow/border styles.

In `@web/src/pages/setup/TemplateVariables.tsx`:
- Around line 4-10: Extend the TemplateVariable interface by adding optional
numeric constraint properties (min?: number, max?: number, step?: number) and
update any slider usage in TemplateVariables.tsx to read and prefer these
properties (e.g., use variable.min/variable.max/variable.step) when rendering
bounds and increments, falling back to the current hardcoded defaults if they
are undefined; ensure the TemplateVariable type is exported/used wherever
template variables are typed so callers can supply these constraints.
- Around line 21-28: The card header/layout in the TemplateVariables component
is hand-rolled; replace the outer div and its header markup with the shared
SectionCard component: import SectionCard, remove the manual container (the div
with classes "space-y-4 rounded-lg border border-border bg-card p-4") and the
inner h3/paragraph header, and wrap the existing body content with <SectionCard>
while supplying the "Template Variables" heading (and the brief description
text) via SectionCard's title/description props or children per the SectionCard
API so the shared component handles the card-with-header layout and styles.

In `@web/src/pages/setup/ThemePreview.tsx`:
- Around line 32-41: Replace the custom markup block with the shared AgentCard
component: remove the manual Avatar and surrounding div, and render <AgentCard
name="Akira Tanaka" status="idle" role="CEO" department="executive" /> (or the
equivalent JSX for AgentCard in this codebase); do not pass a size prop
(AgentCard/Avatar sizes are handled internally) and ensure the status prop
supplies the current state so the internal StatusBadge renders with its label.
This updates ThemePreview to reuse AgentCard and splits the combined "CEO,
executive" string into role="CEO" and department="executive".

In `@web/src/pages/setup/WizardShell.tsx`:
- Around line 93-104: The skip button currently navigates to `/setup/complete`
which renders CompleteStep and fails because companyResponse is null; update
STEP_COMPONENTS to include a 'skip' entry that maps to SkipWizardForm (reference
STEP_COMPONENTS and SkipWizardForm) and change the Button onClick in WizardShell
(the Skip wizard button) to navigate to `/setup/skip` so the SkipWizardForm is
rendered instead of CompleteStep.

In `@web/src/pages/setup/WizardSkeleton.tsx`:
- Around line 9-14: The skeleton currently renders a hardcoded 7 placeholders in
WizardSkeleton.tsx; replace the magic number by deriving the count from the real
wizard metadata or a prop so the loading UI stays in sync. Update WizardSkeleton
to accept a prop like stepCount (or import the shared steps array/constant used
by the wizard and use steps.length) and replace Array.from({ length: 7 }) with
that dynamic value; then update any callers to pass the stepCount or ensure the
shared steps constant is available to import. Ensure the Skeleton rendering loop
keying and classes remain unchanged.

In `@web/src/stores/setup-wizard.ts`:
- Around line 286-309: The store currently lets stale generated state persist
when upstream inputs change and also omits template variables from the
create-company API; update selectTemplate, setTemplateVariable, setCompanyName,
and setCompanyDescription to reset generated state (clear companyResponse and
agents and mark company as not submitted) whenever they modify inputs, and
ensure clearComparison similarly clears generated state if it affects
generation; then modify submitCompany to include the current templateVariables
(from state.templateVariables) in the request payload sent to the create-company
API so slider/toggle values are posted to the backend.

In `@web/src/utils/template-categories.ts`:
- Around line 54-62: getTemplateCategory currently resolves categories by
mapping tag → CATEGORY_LABELS[tag] → LABEL_TO_CATEGORY[label], which breaks if
display labels change; introduce a new TAG_TO_CATEGORY map that directly maps
each tag to its canonical category key and update getTemplateCategory to return
TAG_TO_CATEGORY[tag] ?? 'other' (leaving CATEGORY_LABELS in place for UI-only
display labels); update any tests or usages that relied on the old indirection
and document TAG_TO_CATEGORY alongside CATEGORY_LABELS.
🪄 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: 904f03ed-04c7-43b0-a50a-3e2dc290e30d

📥 Commits

Reviewing files that changed from the base of the PR and between d31527d and 0e36bd7.

📒 Files selected for processing (63)
  • CLAUDE.md
  • README.md
  • docs/design/brand-and-ux.md
  • docs/design/organization.md
  • docs/design/page-structure.md
  • docs/user_guide.md
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/components/ui/input-field.test.tsx
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/__tests__/components/ui/toggle-field.test.tsx
  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/utils/cost-estimator.property.test.ts
  • web/src/__tests__/utils/cost-estimator.test.ts
  • web/src/__tests__/utils/password-strength.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/__tests__/utils/setup-validation.test.ts
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/api/types.ts
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/slider-field.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/SetupPage.tsx
  • web/src/pages/setup/AccountStep.tsx
  • web/src/pages/setup/AgentModelPicker.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/TemplateVariables.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/cost-estimator.ts
  • web/src/utils/currencies.ts
  • web/src/utils/password-strength.ts
  • web/src/utils/provider-status.ts
  • web/src/utils/setup-validation.ts
  • web/src/utils/template-categories.ts

Comment on lines +61 to +70
const costEstimate = useMemo(() => {
if (agents.length === 0) return null
return estimateMonthlyCost(
agents.map((a) => ({
model_provider: a.model_provider,
model_id: a.model_id,
tier: a.tier,
})),
[], // No provider models yet
)
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

Don't hardcode fallback pricing here.

Passing [] on Line 69 means estimateMonthlyCost() can never use configured provider/model pricing, so revisiting this step after Providers still shows fallback totals and keeps usedFallback permanently true. Read the provider models from the setup store and include them in this memo's dependencies.

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

In `@web/src/pages/setup/CompanyStep.tsx` around lines 61 - 70, The memo for
costEstimate is hardcoding provider models as [] which forces fallback pricing;
read the configured provider models from the setup store (e.g. via the setup
store selector like useSetupStore(state => state.providerModels) or
setupStore.providerModels) and pass that array into estimateMonthlyCost instead
of []; also add that providerModels value to the useMemo dependency list so
costEstimate recalculates when provider/model pricing changes (refer to
costEstimate, agents, and estimateMonthlyCost).

Comment on lines +60 to +67
return (
<div className="space-y-4 rounded-lg border border-border bg-card p-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold text-foreground">Add Cloud Provider</h3>
<p className="text-xs text-muted-foreground">
Connect a cloud LLM provider with your API key.
</p>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this card-with-header layout.

Similar to TemplateVariables.tsx, this hand-rolled card structure should use the shared SectionCard component for consistency. As per coding guidelines: "do NOT build card-with-header layouts from scratch -- use <SectionCard>".

♻️ Proposed refactor
+import { SectionCard } from '@/components/ui/section-card'
...
   return (
-    <div className="space-y-4 rounded-lg border border-border bg-card p-4">
-      <div className="space-y-1">
-        <h3 className="text-sm font-semibold text-foreground">Add Cloud Provider</h3>
-        <p className="text-xs text-muted-foreground">
-          Connect a cloud LLM provider with your API key.
-        </p>
-      </div>
+    <SectionCard
+      title="Add Cloud Provider"
+      description="Connect a cloud LLM provider with your API key."
+    >
       <SelectField ... />
       ...
-    </div>
+    </SectionCard>
   )
📝 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
return (
<div className="space-y-4 rounded-lg border border-border bg-card p-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold text-foreground">Add Cloud Provider</h3>
<p className="text-xs text-muted-foreground">
Connect a cloud LLM provider with your API key.
</p>
</div>
return (
<SectionCard
title="Add Cloud Provider"
description="Connect a cloud LLM provider with your API key."
>
<SelectField ... />
...
</SectionCard>
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 60 - 67, Replace the
hand-rolled card-with-header markup in ProviderAddForm with the shared
SectionCard component: remove the outer div with classes "space-y-4 rounded-lg
border border-border bg-card p-4" and the inner header block, and instead render
SectionCard (import it if missing) passing the header content ("Add Cloud
Provider" title and subtitle) into the SectionCard's header slot/props and the
remaining form body as children; ensure you keep the same text nodes ("Add Cloud
Provider" and the subtitle) and CSS intent by using SectionCard's API so the
layout matches other usages like TemplateVariables.tsx.

Comment on lines +61 to +80
return (
<div className="space-y-3 rounded-lg border border-border bg-card p-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold text-foreground">
{probing ? 'Detecting local providers...' : 'Auto-detected Providers'}
</h3>
<p className="text-xs text-muted-foreground">
Checking for locally running LLM providers.
</p>
</div>
{localPresets.map((preset) => (
<ProbeResultItem
key={preset.name}
preset={preset}
result={probeResults[preset.name]}
probing={probing}
onAddPreset={onAddPreset}
/>
))}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this card shell.

Lines 61-80 rebuild the standard card + header pattern inline. Reusing SectionCard keeps spacing and header treatment consistent across setup screens and avoids another one-off container.

As per coding guidelines, web/src/**/*.{ts,tsx} must "Do NOT build card-with-header layouts from scratch -- use <SectionCard>" and "ALWAYS reuse existing components from web/src/components/ui/ before creating new ones".

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

In `@web/src/pages/setup/ProviderProbeResults.tsx` around lines 61 - 80, Replace
the manual card shell in the ProviderProbeResults component with the shared
SectionCard component: remove the outer div with classes "space-y-3 rounded-lg
border border-border bg-card p-4" and move the header (h3 + p) into
SectionCard's header/title/description slots (or as its first children if
SectionCard doesn't expose slots), then render the localPresets.map(...) list
inside SectionCard; update references to probing, localPresets, ProbeResultItem,
and onAddPreset so they remain unchanged, and delete the duplicated
spacing/header CSS now provided by SectionCard to keep layout consistent with
other setup screens.

Comment on lines +64 to +84
const recommendedTemplates = useMemo(() => {
const recommended = new Set<string>()
const providerCount = Object.keys(providers).length
const smallTags = new Set([TAG_SOLO, TAG_SMALL_TEAM, 'startup', 'mvp'])
const largeTags = new Set([TAG_ENTERPRISE, TAG_FULL_COMPANY])

for (const template of templates) {
if (providerCount === 0) {
// No providers configured yet -- recommend small/simple templates
if (template.tags.some((tag) => smallTags.has(tag))) {
recommended.add(template.name)
}
} else {
// Providers configured -- recommend larger templates
if (template.tags.some((tag) => largeTags.has(tag))) {
recommended.add(template.name)
}
}
}
return recommended
}, [templates, providers])
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

Recommended is based on provider presence, not provider/model tiers.

Once providerCount > 0, every enterprise/full-company template becomes recommended regardless of what models are actually configured. That makes the badge misleading for users who only have economy/local tiers available. Derive the set from provider model metadata, or suppress the badge until those tiers are known.

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

In `@web/src/pages/setup/TemplateStep.tsx` around lines 64 - 84, The current
recommendedTemplates logic uses only providerCount to flip from smallTags to
largeTags, causing TAG_ENTERPRISE/TAG_FULL_COMPANY templates to be recommended
even if configured providers don't support those tiers; update
recommendedTemplates to inspect provider model metadata instead: derive a
boolean (e.g., hasEnterpriseTier) by scanning providers' model/tier fields and
only include largeTags (TAG_ENTERPRISE, TAG_FULL_COMPANY) when that boolean is
true, otherwise continue to recommend smallTags or suppress enterprise
recommendations; alternatively, if provider model info is not yet loaded, return
an empty recommended set or skip enterprise tags until model metadata is
available to avoid misleading badges.

Comment on lines +56 to +60
interface AgentCostInput {
readonly model_provider: string
readonly model_id: string
readonly tier: string
}
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

Key pricing by provider, not just model id.

AgentCostInput includes model_provider, but Lines 76-79 build the lookup with model.id alone and Line 86 reads it back with agent.model_id only. If two configured providers expose the same model id, the later insert wins and the estimate can bill an agent against the wrong provider's pricing.

🔧 One way to fix this
+interface PricedModel {
+  readonly provider: string
+  readonly model: ProviderModelConfig
+}
+
 export function estimateMonthlyCost(
   agents: readonly AgentCostInput[],
-  models: readonly ProviderModelConfig[],
+  models: readonly PricedModel[],
   options?: CostEstimateOptions,
 ): CostEstimate {
   const dailyTokens = options?.dailyTokensPerAgent ?? DEFAULT_DAILY_TOKENS_PER_AGENT
   const inputRatio = options?.inputOutputRatio ?? DEFAULT_INPUT_OUTPUT_RATIO

   const modelMap = new Map<string, ProviderModelConfig>()
-  for (const model of models) {
-    modelMap.set(model.id, model)
+  for (const { provider, model } of models) {
+    modelMap.set(`${provider}::${model.id}`, model)
   }
 ...
-    const model = modelMap.get(agent.model_id)
+    const model = modelMap.get(`${agent.model_provider}::${agent.model_id}`)

Please add a regression test with duplicate model ids across providers as part of the fix.

Also applies to: 68-70, 76-86

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

In `@web/src/utils/cost-estimator.ts` around lines 56 - 60, AgentCostInput
currently has model_provider and model_id but the cost lookup is keyed only by
model_id, causing collisions when different providers use the same id; update
the map key construction and lookup to include both model_provider and model_id
(e.g., composite key `${provider}:${modelId}`) wherever the lookup is built (the
code that iterates configured models and currently uses model.id) and wherever
costs are read (the code that reads agent.model_id), update typing/variable
names as needed to use agent.model_provider + agent.model_id, and add a
regression test that creates two providers with the same model_id and asserts
the estimator charges the correct provider-specific price.

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: 5

♻️ Duplicate comments (1)
web/src/stores/setup-wizard.ts (1)

287-289: ⚠️ Potential issue | 🟠 Major

Template edits still don't invalidate or persist generated company data.

selectTemplate() and setTemplateVariable() only update local fields, and Lines 338-342 still submit company_name, description, and template_name only. That leaves the new template-variable controls ineffective and allows companyResponse / agents to stay stale after template edits until another successful submit. Please include the variable payload in the company-generation request and clear the derived company state when template inputs change.

Also applies to: 306-309, 334-342

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

In `@web/src/stores/setup-wizard.ts` around lines 287 - 289, selectTemplate and
setTemplateVariable currently only update local fields but don't clear derived
company state or include template variables in the generation request; update
both selectTemplate and setTemplateVariable to reset/clear derived state (clear
companyResponse and agents) whenever template inputs change, and modify the
company-generation submit function (the code that currently posts company_name,
description, template_name) to include the template variables payload (e.g.,
template_variables or variables object) so the backend receives and uses them
when generating company/agents.
🤖 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/__tests__/utils/setup-validation.property.test.ts`:
- Around line 124-129: The property test currently uses a generator named
`template` from fc.oneof(fc.constant(null), fc.string(...)) but only cares
whether it's null or not; change the generator to a boolean (e.g., fc.boolean())
and update the callback to use that boolean to decide `companyResponse` (when
true call `makeCompanyResponse()`, when false set null) before calling
`validateCompanyStep`; update variable name from `template` to something like
`hasResponse` to improve clarity. This affects the generator and the creation of
the `companyResponse` passed into `validateCompanyStep`.
- Around line 45-50: The property-based test for company names in the test case
'company name with 1-200 non-whitespace chars + response is always valid'
currently uses a generator with /^[A-Za-z0-9][A-Za-z0-9 ]{0,199}$/ which
restricts allowed characters; update the generator to mirror validateCompanyStep
behavior by generating any string that, when trimmed, is non-empty and length <=
200 (e.g., fc.string().filter(s => s.trim().length > 0 && s.trim().length <=
200) or an equivalent trimming-aware generator) so the test covers the same
domain as validateCompanyStep and references the test name and
validateCompanyStep to locate the change.

In `@web/src/components/ui/drawer.tsx`:
- Around line 106-129: Replace the hardcoded layout/spacing utilities in the
Drawer component render (the container class string that contains w-[40vw] and
min-w-80, the header div with px-4 py-3, the close button with p-1, and the
content div with p-4) with the project’s design-token spacing/size classes
(e.g., density-aware tokens such as p-card, gap-section-gap, gap-grid-gap or
token-backed width/min-width utilities) so the shared component uses token-based
sizing; update the container, header, button, and content className occurrences
in web/src/components/ui/drawer.tsx (the JSX elements rendering the outer div,
the header div, the close button, and the content div that reference title,
onClose and children) to the corresponding design-token classes and remove the
arbitrary/explicit pixel or vw utilities.

In `@web/src/pages/setup/ProviderProbeResults.tsx`:
- Around line 16-23: Replace the inline probe-status markup in
ProviderProbeResults (the conditional rendering that currently uses Loader2,
Check, and X around lines rendering the probe indicator) with the shared
StatusBadge component from web/src/components/ui; map probing state to a
loading/pending variant, detected to success, and fallback to error/neutral as
appropriate, and ensure you pass any needed props (e.g., size or className) and
maintain existing accessibility/tooltip text currently associated with the
inline icons so the visual and semantic behavior stays the same.

In `@web/src/stores/setup-wizard.ts`:
- Around line 461-479: probeAllPresets currently drops rejected probe results so
the UI can't distinguish network/backend errors from genuine "not found" misses;
modify probeAllPresets to record a per-preset failure sentinel into probeResults
instead of omitting the key: when an entry from Promise.allSettled is rejected,
set results[presetName] to a distinct ProbePresetResponse-like value (e.g., {
status: 'error', error: <reason> } or a union tag) so ProviderProbeResults can
render errors separately; ensure the sentinel shape is compatible with the
ProbePresetResponse type (or extend it) and update any consumers that read
probeResults accordingly (functions: probeAllPresets, probePreset, and the
probeResults state).

---

Duplicate comments:
In `@web/src/stores/setup-wizard.ts`:
- Around line 287-289: selectTemplate and setTemplateVariable currently only
update local fields but don't clear derived company state or include template
variables in the generation request; update both selectTemplate and
setTemplateVariable to reset/clear derived state (clear companyResponse and
agents) whenever template inputs change, and modify the company-generation
submit function (the code that currently posts company_name, description,
template_name) to include the template variables payload (e.g.,
template_variables or variables object) so the backend receives and uses them
when generating company/agents.
🪄 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: bd495efe-d3bc-4a9c-bd1b-4df228a8dd17

📥 Commits

Reviewing files that changed from the base of the PR and between 0e36bd7 and ce9164b.

📒 Files selected for processing (6)
  • web/src/__tests__/stores/agents.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/provider-status.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: Deploy Preview
  • GitHub Check: Dashboard Test
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (2)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{ts,tsx}: ALWAYS reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.) before creating new ones
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, etc.) or CSS variables (var(--so-*)) for colors; NEVER hardcode hex or rgba values
Use font-sans or font-mono for typography; NEVER set fontFamily directly in component code
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Use token variables for shadows and borders (var(--so-shadow-card-hover), border-border, border-bright); NEVER hardcode values
Do NOT recreate status dots inline -- use <StatusBadge>; do NOT build card-with-header layouts from scratch -- use <SectionCard>; do NOT create metric displays with hardcoded styles -- use <MetricCard>
Do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component in web/src/components/ui/
Use TypeScript 6.0+ strict mode with proper tsconfig.json configuration: remove deprecated baseUrl, enable paths for imports, explicitly list types, use moduleResolution: 'bundler', keep strict: true
Set parameters.a11y.test: 'error' | 'todo' | 'off' in Storybook stories to enforce WCAG compliance (set globally in preview.tsx)

Files:

  • web/src/__tests__/stores/agents.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/provider-status.ts
web/src/components/ui/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/components/ui/*.tsx: Create new shared components in web/src/components/ui/ with a descriptive kebab-case filename, a corresponding .stories.tsx Storybook file, and a TypeScript interface for props
All new shared components must use design tokens exclusively -- no hardcoded colors, fonts, or spacing -- and import cn from @/lib/utils for conditional class merging

Files:

  • web/src/components/ui/drawer.tsx
🧠 Learnings (9)
📚 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__/utils/setup-validation.property.test.ts
📚 Learning: 2026-03-27T14:30:49.093Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T14:30:49.093Z
Learning: Applies to web/src/components/ui/*.tsx : Create new shared components in `web/src/components/ui/` with a descriptive kebab-case filename, a corresponding `.stories.tsx` Storybook file, and a TypeScript interface for props

Applied to files:

  • web/src/components/ui/drawer.tsx
📚 Learning: 2026-03-27T14:30:49.093Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T14:30:49.093Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); NEVER hardcode values

Applied to files:

  • web/src/components/ui/drawer.tsx
📚 Learning: 2026-03-27T14:30:49.093Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T14:30:49.093Z
Learning: Applies to web/src/components/ui/*.tsx : All new shared components must use design tokens exclusively -- no hardcoded colors, fonts, or spacing -- and import `cn` from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/components/ui/drawer.tsx
📚 Learning: 2026-03-27T14:30:49.093Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T14:30:49.093Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, etc.) or CSS variables (`var(--so-*)`) for colors; NEVER hardcode hex or rgba values

Applied to files:

  • web/src/components/ui/drawer.tsx
📚 Learning: 2026-03-27T14:30:49.093Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T14:30:49.093Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use density-aware spacing 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/components/ui/drawer.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
📚 Learning: 2026-03-27T14:30:49.093Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T14:30:49.093Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT recreate status dots inline -- use `<StatusBadge>`; do NOT build card-with-header layouts from scratch -- use `<SectionCard>`; do NOT create metric displays with hardcoded styles -- use `<MetricCard>`

Applied to files:

  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
📚 Learning: 2026-03-27T14:30:49.093Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T14:30:49.093Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.) before creating new ones

Applied to files:

  • web/src/components/ui/drawer.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
📚 Learning: 2026-03-27T14:30:49.093Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T14:30:49.093Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component in `web/src/components/ui/`

Applied to files:

  • web/src/pages/setup/ProviderProbeResults.tsx
🔇 Additional comments (7)
web/src/__tests__/stores/agents.test.ts (1)

99-101: Type-aligned fixture defaults look correct.

These defaults now match the current Task contract (coordination_topology required + optional updated_at), so this is a solid fix for test-data correctness.

web/src/pages/setup/ProviderProbeResults.tsx (3)

62-70: Replace the custom card shell with SectionCard.

This repeats the previously flagged pattern: Lines 62-70 build a card-with-header layout inline. Please switch to the shared SectionCard wrapper for consistent structure and spacing.

As per coding guidelines: "Do NOT build card-with-header layouts from scratch -- use <SectionCard>".


44-47: probeResults typing now correctly models sparse probe state.

Using Partial<Record<...>> here is the right contract for runtime behavior and keeps probeResults[preset.name] type-safe.


71-79: Nice cleanup of the .map() body.

The list rendering is now concise and easier to maintain with ProbeResultItem.

web/src/components/ui/drawer.tsx (1)

43-77: Focus trap and focus-restore behavior look correct.

The keyboard flow is solid here: initial focus, Tab/Shift+Tab cycling, Escape close, and opener focus restoration are all implemented cleanly.

web/src/utils/provider-status.ts (1)

5-18: Exhaustive auth-type mapping looks good.

This keeps provider badge state centralized and covers every current AuthType variant in one place.

web/src/stores/setup-wizard.ts (1)

251-259: Good fix for the omitted-step navigation hole.

The targetIdx === -1 guard closes the invalid-step bypass without weakening the prerequisite-step checks.

Comment on lines +106 to +129
'fixed inset-y-0 right-0 z-50 flex w-[40vw] min-w-80 max-w-xl flex-col',
'border-l border-border bg-card shadow-[var(--so-shadow-card-hover)]',
className,
)}
>
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h2 className="text-sm font-semibold text-foreground">{title}</h2>
<button
type="button"
onClick={onClose}
aria-label="Close"
className={cn(
'rounded-md p-1 text-muted-foreground transition-colors',
'hover:bg-card-hover hover:text-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent',
)}
>
<X className="size-4" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{children}
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

Use design-token sizing/spacing for this shared component instead of hardcoded layout values.

In a shared web/src/components/ui/*.tsx component, w-[40vw], min-w-80, px-4, py-3, p-1, and p-4 should be replaced with design-token-based sizing/spacing utilities (or token-backed CSS vars) to keep density/theming consistent.

♻️ Proposed tokenized direction
 className={cn(
-  'fixed inset-y-0 right-0 z-50 flex w-[40vw] min-w-80 max-w-xl flex-col',
+  'fixed inset-y-0 right-0 z-50 flex w-[var(--so-drawer-width)] min-w-[var(--so-drawer-min-width)] max-w-[var(--so-drawer-max-width)] flex-col',
   'border-l border-border bg-card shadow-[var(--so-shadow-card-hover)]',
   className,
 )}
...
-<div className="flex items-center justify-between border-b border-border px-4 py-3">
+<div className="flex items-center justify-between border-b border-border p-card">
...
-  'rounded-md p-1 text-muted-foreground transition-colors',
+  'rounded-md p-card text-muted-foreground transition-colors',
...
-<div className="flex-1 overflow-y-auto p-4">
+<div className="flex-1 overflow-y-auto p-card">

As per coding guidelines, "All new shared components must use design tokens exclusively -- no hardcoded colors, fonts, or spacing" and "Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing".

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

In `@web/src/components/ui/drawer.tsx` around lines 106 - 129, Replace the
hardcoded layout/spacing utilities in the Drawer component render (the container
class string that contains w-[40vw] and min-w-80, the header div with px-4 py-3,
the close button with p-1, and the content div with p-4) with the project’s
design-token spacing/size classes (e.g., density-aware tokens such as p-card,
gap-section-gap, gap-grid-gap or token-backed width/min-width utilities) so the
shared component uses token-based sizing; update the container, header, button,
and content className occurrences in web/src/components/ui/drawer.tsx (the JSX
elements rendering the outer div, the header div, the close button, and the
content div that reference title, onClose and children) to the corresponding
design-token classes and remove the arbitrary/explicit pixel or vw utilities.

Comment on lines +16 to +23
<div className="flex items-center gap-3 text-sm">
{probing && !result ? (
<Loader2 className="size-4 animate-spin text-muted-foreground" />
) : detected ? (
<Check className="size-4 text-success" />
) : (
<X className="size-4 text-muted-foreground" />
)}
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

Use shared StatusBadge instead of inline probe-status indicators.

Lines 16-23 and Line 35 render probe status states manually (Loader2/Check/X). This should be expressed via the shared status component to keep consistency and avoid one-off status UIs.

As per coding guidelines: "Do NOT recreate status dots inline -- use <StatusBadge>; ... ALWAYS reuse existing components from web/src/components/ui/ ...".

Also applies to: 35-39

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

In `@web/src/pages/setup/ProviderProbeResults.tsx` around lines 16 - 23, Replace
the inline probe-status markup in ProviderProbeResults (the conditional
rendering that currently uses Loader2, Check, and X around lines rendering the
probe indicator) with the shared StatusBadge component from
web/src/components/ui; map probing state to a loading/pending variant, detected
to success, and fallback to error/neutral as appropriate, and ensure you pass
any needed props (e.g., size or className) and maintain existing
accessibility/tooltip text currently associated with the inline icons so the
visual and semantic behavior stays the same.

Comment on lines +461 to +479
async probeAllPresets() {
const { presets } = get()
set({ probing: true })
const entries = await Promise.allSettled(
presets.map(async (preset) => {
const result = await probePreset(preset.name)
return [preset.name, result] as const
}),
)
const results: Record<string, ProbePresetResponse> = {}
for (const entry of entries) {
if (entry.status === 'fulfilled') {
results[entry.value[0]] = entry.value[1]
} else {
console.error('setup-wizard: probe failed for preset:', entry.reason)
}
}
set({ probeResults: results, probing: 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

Don't collapse probe failures into “Not found”.

Rejected probes are logged and then dropped from probeResults. In web/src/pages/setup/ProviderProbeResults.tsx:12-41, a missing entry after probing renders the same X/“Not found” state as a genuine miss, so backend/network failures get reported as provider absence. Store a per-preset failure state, or a sentinel result, so the UI can render an actual error separately.

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

In `@web/src/stores/setup-wizard.ts` around lines 461 - 479, probeAllPresets
currently drops rejected probe results so the UI can't distinguish
network/backend errors from genuine "not found" misses; modify probeAllPresets
to record a per-preset failure sentinel into probeResults instead of omitting
the key: when an entry from Promise.allSettled is rejected, set
results[presetName] to a distinct ProbePresetResponse-like value (e.g., {
status: 'error', error: <reason> } or a union tag) so ProviderProbeResults can
render errors separately; ensure the sentinel shape is compatible with the
ProbePresetResponse type (or extend it) and update any consumers that read
probeResults accordingly (functions: probeAllPresets, probePreset, and the
probeResults state).

@Aureliolo Aureliolo force-pushed the feat/setup-wizard-rebuild branch from ce9164b to dfe0fd1 Compare March 27, 2026 15:13
@Aureliolo Aureliolo had a problem deploying to cloudflare-preview March 27, 2026 15:14 — with GitHub Actions Failure
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: 20

Caution

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

⚠️ Outside diff range comments (1)
web/src/api/types.ts (1)

1132-1136: ⚠️ Potential issue | 🟠 Major

The setup request still can't serialize template-variable overrides.

The wizard now collects per-template values, but SetupCompanyRequest has nowhere to send them. Until this contract includes a template_variables-style payload, those controls cannot affect generation or persistence.

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

In `@web/src/api/types.ts` around lines 1132 - 1136, SetupCompanyRequest is
missing a field to carry per-template variable overrides; add a new property
(e.g., template_variables: Record<string, string> | null or a typed interface if
you have a known schema) to the SetupCompanyRequest interface so the wizard can
serialize and send collected per-template values; update any
consumers/serializers that construct SetupCompanyRequest to populate this new
template_variables field and ensure backend contract accepts it.
♻️ Duplicate comments (15)
web/src/pages/setup/TemplateCostBadge.tsx (1)

6-12: 🧹 Nitpick | 🔵 Trivial

Use shared CurrencyCode type and DEFAULT_CURRENCY constant.

The currency prop uses a raw string type which allows invalid currency codes. Using a typed constant improves compile-time safety.

♻️ Proposed refactor
 import { cn } from '@/lib/utils'
 import { formatCurrency } from '@/utils/format'
+import { DEFAULT_CURRENCY, type CurrencyCode } from '@/utils/currencies'

 export interface TemplateCostBadgeProps {
   monthlyCost: number
-  currency?: string
+  currency?: CurrencyCode
   className?: string
 }

 export function TemplateCostBadge({
   monthlyCost,
-  currency = 'EUR',
+  currency = DEFAULT_CURRENCY,
   className,
 }: TemplateCostBadgeProps) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/TemplateCostBadge.tsx` around lines 6 - 12, The currency
prop on TemplateCostBadge is typed as string; change it to use the shared
CurrencyCode type and DEFAULT_CURRENCY constant to enforce valid codes and a
consistent default. Import CurrencyCode and DEFAULT_CURRENCY, update the
component prop type (the currency field in the props interface) from string to
CurrencyCode, and set the default value for currency to DEFAULT_CURRENCY in the
TemplateCostBadge parameter destructuring.
web/src/stores/setup-wizard.ts (2)

287-289: ⚠️ Potential issue | 🟠 Major

Invalidate generated company data when the template changes.

Changing selectedTemplate or templateVariables leaves companyResponse and agents intact, so later steps can keep rendering the previous generated org until another successful submit. Clear downstream derived state and invalidate dependent steps on either edit.

Also applies to: 306-309

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

In `@web/src/stores/setup-wizard.ts` around lines 287 - 289, When changing the
template, clear downstream derived state so stale generated org data isn't
reused: update the selectTemplate function to, in addition to set({
selectedTemplate: name }), also clear companyResponse and agents (e.g. set
companyResponse: null and agents: []) and any dependent step flags; apply the
same invalidation in the templateVariables setter (the code around the
templateVariables update at the 306-309 region) so edits to either
selectedTemplate or templateVariables always invalidate companyResponse and
agents and force re-generation.

461-478: ⚠️ Potential issue | 🟠 Major

Don't collapse probe failures into “not found”.

Rejected probes are omitted from probeResults, and the UI then falls back to the same missing-entry state as a genuine miss. Persist an explicit error state per preset so backend/network failures render separately.

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

In `@web/src/stores/setup-wizard.ts` around lines 461 - 478, probeAllPresets
currently omits entries that reject so probeResults treats failures as "not
found"; update probeAllPresets to record an explicit error state per preset
instead of skipping it: when mapping/pulling Promise.allSettled results from
probePreset, set results[presetName] to either the successful
ProbePresetResponse or a sentinel error object (e.g., { error: <reason> } or {
status: 'error', message: String(reason) }) so the UI can distinguish
network/backend failures from genuine misses; ensure keys come from the original
preset name (refer to probeAllPresets, probePreset, presets, and probeResults)
and set probing back to false after populating probeResults.
web/src/pages/setup/ProviderProbeResults.tsx (2)

16-23: 🛠️ Refactor suggestion | 🟠 Major

Use StatusBadge for the probe state.

This still hand-rolls pending/success/miss indicators with raw icons. Swapping to the shared status component keeps probe-state UI consistent across the wizard.

Based on learnings: Applies to web/src/**/*.tsx: Do NOT recreate status dots inline—use <StatusBadge>.

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

In `@web/src/pages/setup/ProviderProbeResults.tsx` around lines 16 - 23, The
component ProviderProbeResults currently renders raw icons (Loader2, Check, X)
based on probing/result/detected; replace that inline icon logic with the shared
<StatusBadge> component, mapping probing -> state="pending" (show spinner),
detected -> state="success", else -> state="miss", and pass any tooltip/aria
text previously shown; also add/remove imports so StatusBadge is imported and
Loader2/Check/X are removed from this file. Ensure you update the render path
that currently checks probing && !result, detected, else to render <StatusBadge
state={...} /> accordingly.

62-80: 🛠️ Refactor suggestion | 🟠 Major

Wrap this shell in SectionCard.

The outer container is another custom card-with-header layout. Reusing SectionCard avoids one-off spacing and header treatment in setup screens.

Based on learnings: Applies to web/src/**/*.tsx: Do NOT build card-with-header layouts from scratch—use <SectionCard>.

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

In `@web/src/pages/setup/ProviderProbeResults.tsx` around lines 62 - 80, Replace
the manual card markup in ProviderProbeResults (the div with classes "space-y-3
rounded-lg border border-border bg-card p-4" and its header/p elements) with the
shared SectionCard component: move the header text and description into
SectionCard's title/subtitle props or children, keep the mapping over
localPresets to render ProbeResultItem (passing preset,
probeResults[preset.name], probing, onAddPreset) as SectionCard body content,
and remove the duplicated spacing/styling so the layout uses SectionCard's
standardized header and card styles.
web/src/components/ui/select-field.tsx (1)

41-79: 🛠️ Refactor suggestion | 🟠 Major

Tokenize the spacing and typography in this primitive.

gap-1.5, ml-0.5, px-3, py-2, text-sm, and text-xs bake fixed sizing into a shared UI component. This one should stay design-token driven so density/theme changes propagate uniformly.

Based on learnings: Applies to web/src/components/ui/**/*.tsx: use design tokens exclusively.

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

In `@web/src/components/ui/select-field.tsx` around lines 41 - 79, The component
currently uses hardcoded Tailwind utility classes (gap-1.5, ml-0.5, px-3, py-2,
text-sm, text-xs) inside the select-field JSX; replace those fixed utilities
with the design token classes/variables used across the UI (e.g., token-based
spacing, density and typography tokens) so spacing/typography respond to
theme/density changes—update the container div, label span, select className,
hint p and error p to use the token equivalents instead of the listed utilities
and apply the same token-driven replacements to other files under
web/src/components/ui/**/*.tsx as noted in the duplicate comment. Ensure aria
and behaviour code is unchanged and only className strings are swapped to token
names.
web/src/pages/setup/ThemePreview.tsx (1)

32-41: 🛠️ Refactor suggestion | 🟠 Major

Reuse AgentCard instead of custom agent-row markup.

This recreates a shared pattern that should come from the design-system component to avoid drift.

As per coding guidelines: “ALWAYS reuse existing components from web/src/components/ui/AgentCard …”.

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

In `@web/src/pages/setup/ThemePreview.tsx` around lines 32 - 41, Replace the
custom agent row markup in ThemePreview (the div with Avatar, name, StatusBadge
and title) with the shared AgentCard component from web/src/components/ui so we
reuse the design-system pattern; locate the block that renders Avatar, "Akira
Tanaka", StatusBadge(status="idle", label) and the subtitle "CEO, executive" and
render <AgentCard> (or the exported AgentCard component) passing the
corresponding props (name, subtitle/title, status) and any size/className needed
to match styling so StatusBadge and Avatar usage are removed and the shared
component is used consistently.
web/src/__tests__/pages/setup/WizardProgress.test.tsx (1)

64-67: ⚠️ Potential issue | 🟡 Minor

Avoid non-null assertion (!) in step button lookup tests.

Use getByRole(..., { name: /template/i }) (or assert defined before use) so failures report the missing element explicitly instead of relying on !.

♻️ Suggested cleanup
-    const buttons = screen.getAllByRole('button')
-    const templateButton = buttons.find((b) => b.textContent?.includes('Template'))!
-    expect(templateButton).toBeInTheDocument()
+    const templateButton = screen.getByRole('button', { name: /template/i })
     await user.click(templateButton)

Also applies to: 97-100

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

In `@web/src/__tests__/pages/setup/WizardProgress.test.tsx` around lines 64 - 67,
Replace the brittle buttons.find(... ) with a role lookup that reports failures:
use screen.getByRole('button', { name: /template/i }) to obtain the template
button (or call screen.getAllByRole then assert the found element is defined
before using it) and then pass that element to await user.click(...); update the
lookup at the occurrences using the templateButton variable (also lines ~97-100)
so tests fail with a clear missing-element message instead of relying on the
non-null assertion.
web/src/pages/setup/SkipWizardForm.tsx (1)

29-39: 🛠️ Refactor suggestion | 🟠 Major

Make async error handling explicit instead of reading store state after awaits.

The flow still depends on synchronous store-state mutation timing (getState().companyError / completionError) after async actions. Prefer returning an error (or throwing) from submitCompany and completeSetup and handling that directly in this function.

♻️ Suggested control-flow refactor
-      await submitCompany()
-      const companyErr = useSetupWizardStore.getState().companyError
+      const companyErr = await submitCompany()
       if (companyErr) {
         setError(companyErr)
         return
       }
-      await wizardCompleteSetup()
-      const completionErr = useSetupWizardStore.getState().completionError
+      const completionErr = await wizardCompleteSetup()
       if (completionErr) {
         setError(completionErr)
         return
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/SkipWizardForm.tsx` around lines 29 - 39, The current
flow reads useSetupWizardStore.getState().companyError and completionError after
awaiting submitCompany() and wizardCompleteSetup(), which relies on timing of
store mutations; instead change submitCompany and wizardCompleteSetup to
propagate failures (either throw an Error or return an Error result) and update
this file to handle those returns directly: call await submitCompany() inside a
try/catch (or check the returned result) and call setError(err) on
caught/returned errors; do the same for await wizardCompleteSetup(), and remove
the post-await store reads for companyError/completionError — keeping
useSetupWizardStore for state only, not for immediate error control flow.
web/src/pages/setup/WizardShell.tsx (1)

93-99: ⚠️ Potential issue | 🟠 Major

Skip action currently lands users in a non-skip completion path.

On Line 98, navigate('/setup/complete') does not mark a skip flow, so users can reach CompleteStep without the minimal data it expects. This can produce the “Setup incomplete” dead-end instead of the intended quick setup flow. Please set an explicit skip flag/mode before navigation and route/render the dedicated skip form for that mode.

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

In `@web/src/pages/setup/WizardShell.tsx` around lines 93 - 99, The skip button
currently calls navigate('/setup/complete') without indicating a skip flow;
change the onClick for the Button (the navigate call used when currentStep is
'account' or 'template') to pass an explicit skip indicator (e.g., a query param
like ?skipped=true or navigation state { skipped: true }) and update the
CompleteStep component to detect that skip flag (via location.search or
location.state) and render the dedicated skip form/flow instead of the normal
completion path.
web/src/pages/setup/TemplateCard.tsx (1)

31-36: 🛠️ Refactor suggestion | 🟠 Major

Use shared card primitive and tokenized shadow instead of custom card shell.

This block still hand-rolls the card container and uses an arbitrary shadow-[...] value. Please switch to the shared card primitive (e.g., SectionCard) and apply the standard shadow token class/variable for selected state.

As per coding guidelines, "ALWAYS reuse existing components from web/src/components/ui/..." and "Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders; never hardcode values in React code".

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

In `@web/src/pages/setup/TemplateCard.tsx` around lines 31 - 36, The current
TemplateCard component hand-rolls the card container and uses a hardcoded shadow
utility; replace the outer div with the shared SectionCard primitive
(SectionCard) and move the conditional selected styling to use the tokenized
shadow class/variable (e.g., the standard token such as
var(--so-shadow-card-selected) or the project's shadow utility class) instead of
the custom shadow-[...] value; keep the existing cn usage and preserve
selected-driven border logic (use 'border-accent' when selected and
'border-border' otherwise) and any children/props passed through TemplateCard so
behavior is unchanged.
web/src/pages/setup/CostEstimatePanel.tsx (1)

104-110: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for the panel container/header.

This is still a custom card-with-header shell; please wrap this panel in the shared SectionCard and keep inner content as children.

♻️ Suggested refactor
+import { SectionCard } from '@/components/ui/section-card'
...
-  return (
-    <div className="space-y-4 rounded-lg border border-border bg-card p-4">
-      <div className="space-y-1">
-        <h3 className="text-sm font-semibold text-foreground">Cost Estimate</h3>
-        <p className="text-xs text-muted-foreground">
-          Based on {estimate.assumptions.dailyTokensPerAgent.toLocaleString()} tokens/agent/day
-        </p>
-      </div>
+  return (
+    <SectionCard title="Cost Estimate">
+      <div className="space-y-4">
+        <p className="text-xs text-muted-foreground">
+          Based on {estimate.assumptions.dailyTokensPerAgent.toLocaleString()} tokens/agent/day
+        </p>
...
-    </div>
+      </div>
+    </SectionCard>
   )

As per coding guidelines, “do NOT build card-with-header layouts from scratch—use <SectionCard>.”

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

In `@web/src/pages/setup/CostEstimatePanel.tsx` around lines 104 - 110, The
CostEstimatePanel currently builds its own card/header div; replace that outer
container with the shared SectionCard component so the panel uses the standard
card-with-header layout. Locate the CostEstimatePanel component (the JSX
rendering the div with classes "space-y-4 rounded-lg border border-border
bg-card p-4" and the inner header h3 and p showing
estimate.assumptions.dailyTokensPerAgent) and wrap those inner children with
<SectionCard> instead of the custom div, preserving the inner content (the h3
and p) as children and moving any needed spacing/styling into SectionCard props
or inner elements so visual behavior remains the same.
web/src/pages/setup/TemplateStep.tsx (1)

64-84: ⚠️ Potential issue | 🟠 Major

Recommended logic is still based on provider count, not provider/model capability.

This can mark enterprise templates as recommended even when configured providers cannot support those tiers.

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

In `@web/src/pages/setup/TemplateStep.tsx` around lines 64 - 84, The current
recommendedTemplates logic uses providerCount to decide small vs large
recommendations; change it to detect actual provider/model capability instead:
compute a boolean (e.g., hasEnterpriseCapableProvider) by iterating providers
and checking their capability fields or supported model tiers (inspect each
provider object for properties like supportedModels, tier, supportsEnterprise,
maxModelSize, or similar) and use that boolean when deciding whether to
recommend large tags (TAG_ENTERPRISE, TAG_FULL_COMPANY) or small tags (TAG_SOLO,
TAG_SMALL_TEAM, 'startup', 'mvp'); update the useMemo to reference this
capability check (still using templates and providers as dependencies) and
ensure the check covers multiple providers and model variants so enterprise
templates are only recommended when an enterprise-capable provider/model exists.
web/src/utils/cost-estimator.ts (1)

76-79: ⚠️ Potential issue | 🟠 Major

Model pricing lookup can collide across providers.

modelMap is keyed only by model.id, but agents are identified by model_provider + model_id. If two providers expose the same model id, one overwrites the other and estimates can be wrong.

Also applies to: 86-87

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

In `@web/src/utils/cost-estimator.ts` around lines 76 - 79, The modelMap is keyed
only by model.id which causes collisions when different providers expose the
same id; change the map key to a composite key using provider and id (e.g.,
`${model.provider}:${model.id}`) when populating the map (refer to modelMap and
ProviderModelConfig population) and update every lookup that uses
modelMap.get(model.id) (locations around the estimation logic noted at 86-87) to
use the same composite key so each provider/model pair is unique.
web/src/__tests__/stores/setup-wizard.test.ts (1)

18-24: 🧹 Nitpick | 🔵 Trivial

Add missing tests for provider store actions.

Provider APIs are mocked, but createProviderFromPreset, probeAllPresets, and testProviderConnection branches (success/failure) are not covered in this suite.

If useful, I can draft a focused describe('provider actions') test block covering success/error paths for all three methods.

Also applies to: 236-299

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

In `@web/src/__tests__/stores/setup-wizard.test.ts` around lines 18 - 24, Add a
focused test block that covers the missing provider-store action branches: write
tests for createProviderFromPreset, probeAllPresets, and testProviderConnection
including both success and failure paths; use the existing mocks for
createFromPreset, probePreset, and testConnection (replace vi.fn() return values
to simulate resolved values and rejections) and assert the store actions
(createProviderFromPreset, probeAllPresets, testProviderConnection) dispatch the
right mutations/state changes and error handling branches accordingly (e.g.,
verify success updates and failure sets/records errors or returns expected
rejected 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 `@README.md`:
- Line 110: The README's startup flow description assumes the setup wizard
always begins with template selection; update the sentence to mention the
conditional account creation step when needsAdmin is true, e.g., state that on
fresh installs the flow may first prompt for account/admin creation if
needsAdmin is true before the setup wizard guides through choosing a company
template, naming the company, customizing agents, configuring LLM providers,
theme preferences, and reviewing the organization. Reference the conditional
flag needsAdmin in the text so readers know the account creation step can
precede template selection.

In `@web/src/api/types.ts`:
- Around line 1114-1120: The TemplateVariable interface (TemplateVariable) is
too generic: using var_type and default cannot describe slider bounds or enum
choices needed by new controls; update TemplateVariable to a typed union or
extend it with machine-readable metadata (e.g., numeric fields min, max, step
for numeric/slider types and an enum_values array or options for choice types)
and tighten var_type to discriminants (e.g., "string" | "number" | "boolean" |
"enum" | "slider") so render/validation code can switch on the discriminant and
read min/max/step/enum_values rather than guessing from name or default.

In `@web/src/components/ui/drawer.stories.tsx`:
- Around line 1-77: Add separate Storybook entries for the Drawer component to
cover loading, error and empty states: create new stories (e.g., Loading, Error,
Empty) alongside Default/Interactive/ScrollableContent that set Drawer props
(open, onClose, title) and supply appropriate children like Skeleton loaders for
Loading, an Alert or error message for Error, and the EmptyState component for
Empty; reference the Drawer component and existing story objects (Default,
Interactive, ScrollableContent) when adding these new Story objects so they
follow the same args pattern and rendering approach used in the file.

In `@web/src/components/ui/input-field.stories.tsx`:
- Around line 16-42: Add three canonical stories named Hover, Loading, and Empty
alongside the existing Default/Required/WithError stories: create Hover by
reusing Default args (label/placeholder) and simulate a hover state (either by
setting a supported prop such as className: 'pseudo-hover' or state/variant prop
your InputField accepts, or by adding a story decorator/parameters that applies
:hover styles), create Loading by adding a loading: true (or an equivalent prop
your component exposes) to show the spinner/disabled input state, and create
Empty by ensuring value: '' with appropriate placeholder to represent no
content; reference the existing story identifiers Default, WithError and
Required when copying args so the new stories match component variants.

In `@web/src/components/ui/input-field.tsx`:
- Around line 10-24: The InputField component currently declares a ref in the
InputProps and TextareaProps types but is a plain function so React won't pass
the JSX ref through; wrap InputField with React.forwardRef and update its
signature to accept (props: InputFieldProps, ref: React.Ref<HTMLInputElement |
HTMLTextAreaElement>) so the ref is forwarded to the underlying input/textarea,
or alternatively remove the ref from InputProps/TextareaProps; update the
internal usage to attach the forwarded ref to the rendered <input> or <textarea>
element and adjust type narrowing based on multiline to ensure correct element
ref typing for both input and textarea cases.

In `@web/src/components/ui/slider-field.stories.tsx`:
- Around line 17-54: Add four new Story exports named Hover, Loading, Error, and
Empty alongside the existing Default/WithFormat/Disabled/Interactive; each
should reference the SliderField component (or reuse InteractiveSlider) and
provide the proper args to represent the state: Hover should set args to a
normal value and include a story-level play or args to simulate hover (or set a
"hover" prop if the component supports it), Loading should pass a loading flag
or props that render the loading skeleton (e.g., loading: true), Error should
pass an error message prop (e.g., error: "…") to render validation state, and
Empty should pass value: undefined or null (or an empty flag) so the component
shows its empty state; ensure the new exports use the same prop shape as Default
(label, min, max, onChange) and reference SliderField / InteractiveSlider so
Storybook displays the canonical states.

In `@web/src/pages/setup/AccountStep.tsx`:
- Around line 51-113: Wrap the input fields and submit Button inside a semantic
<form> and wire its onSubmit to the existing handleSubmit so Enter key presses
submit; update handleSubmit (function handleSubmit) to accept the event and call
event.preventDefault() at the top (preserving existing logic), and change the
Button to type="submit" (keep disabled={loading}). Ensure you keep the same
error/display logic around InputField, password strength, and confirmPassword so
behavior is unchanged aside from adding form submission semantics.

In `@web/src/pages/setup/AgentModelPicker.tsx`:
- Line 34: The computed currentValue (`const currentValue =
`${currentProvider}::${currentModelId}``) can produce invalid strings when
`currentProvider` or `currentModelId` is empty; update the logic in
AgentModelPicker so that `currentValue` is set to an empty string when either
`currentProvider` or `currentModelId` is falsy (e.g., `''` or undefined) and
only compute the `${provider}::${modelId}` string when both values are present,
so the select/placeholder displays correctly.

In `@web/src/pages/setup/CompanyStep.tsx`:
- Around line 182-186: The list rendering uses agent.name as the React key which
may not be unique; update the map in CompanyStep (the agents.map(...) JSX) to
use a stable unique identifier (e.g., agent.id) for the key instead of
agent.name, and if SetupAgentSummary or the agent objects don't yet include id,
add/propagate an id field to the agent objects so you can reference
key={agent.id} for stable keys.
- Around line 95-134: The company details block (in CompanyStep.tsx wrapping
InputField and SelectField for companyName, companyDescription, currency and
model_tier_profile) and the agent preview card (lines ~179-189) use hand-rolled
divs for a card-with-header layout; replace those divs with the shared
SectionCard component so the header, padding, border and background follow the
app standard—move any header text/hint into SectionCard props, keep the existing
children (InputField/SelectField and the agent preview contents) and preserve
handlers (setCompanyName, setCompanyDescription, setCurrency,
setTemplateVariable) and error/hint logic when migrating.

In `@web/src/pages/setup/ProvidersStep.tsx`:
- Line 55: Rename the useRef variable probeAttempted to probeAttemptedRef and
update all its usages (e.g., where you read/write probeAttempted.current around
the probe logic at the current usage sites near the existing checks on lines
73–74) to use probeAttemptedRef.current; ensure the declaration const
probeAttemptedRef = useRef(false) replaces the old declaration and that any
references elsewhere in ProvidersStep.tsx are updated to the new identifier.

In `@web/src/pages/setup/SkipWizardForm.tsx`:
- Around line 56-84: The component is recreating a card-with-header layout
manually; replace the surrounding markup in SkipWizardForm with the shared
SectionCard component: wrap the title, description, InputField, error block, and
Button inside <SectionCard> (preserving props/state: companyName,
setCompanyName, error, loading, handleSubmit) and remove the custom rounded-lg
border bg-card container and its header markup so the shared SectionCard
provides the consistent header/body styling; ensure the InputField and Button
remain unchanged and that the error alert stays inside the SectionCard body.

In `@web/src/pages/setup/TemplateCompareDrawer.tsx`:
- Around line 113-118: The current TemplateCompareDrawer rendering uses StatPill
with label="" to display standalone tags (row.label === 'Tags'), which is
semantically awkward; replace the StatPill usage with the design-system
Tag/Badge component (e.g., Tag or Badge) so each tag is rendered as a standalone
tag element, import the Tag/Badge component at top of the file, map value.map to
render <Tag key={tag}>{tag}</Tag> (preserve any necessary className or size
props like "text-compact"), and remove the label="" usage from StatPill; if no
Tag/Badge exists, wrap the text in a lightweight span with the appropriate tag
CSS class to reflect the intended visual semantics.

In `@web/src/pages/setup/TemplateStep.tsx`:
- Around line 185-199: The mapped JSX for rendering category groups in
TemplateStep.tsx is too large for inline .map(); extract it into a new
presentational component (e.g., TemplateCategoryRow or CategoryRow) that accepts
props: category, templates (categoryTemplates), estimatedCosts, currency,
selectedTemplate, comparedTemplates, compareDisabled, recommendedTemplates,
onSelect, and onToggleCompare. Replace the current .map() body with a simple
call that returns the new component and pass through the existing handlers
(handleSelect, handleToggleCompare) and derived values (compareDisabled using
comparedTemplates.length >= MAX_COMPARE) so the TemplateStep.tsx .map() becomes
a single-line render. Ensure prop names match TemplateCategoryGroup where
semantics are identical or adapt as needed.

In `@web/src/pages/setup/ThemeStep.tsx`:
- Around line 55-78: The Theme step is missing the typography axis: add a new
TYPOGRAPHY_OPTIONS constant (e.g., similar shape to
PALETTE_OPTIONS/DENSITY_OPTIONS with value/label/description entries) and add
the corresponding control in the ThemeStep UI that calls
setThemeSetting('typography', selectedValue) and uses the same state/props
pattern as the existing controls (palette, density, animation, sidebar) so the
ThemeSettings/preview receives the typography selection; update any
mapping/initial value logic used for the other axes to include typography as
well to ensure default/selected values are handled consistently.

In `@web/src/pages/setup/WizardNavigation.tsx`:
- Around line 31-45: In the WizardNavigation component, both footer Button
elements (the Back button and the conditional Next button) are missing explicit
type attributes and default to "submit" inside forms; update the Back Button and
the Next Button in WizardNavigation.tsx to include type="button" so
onBack/onNext navigation handlers don't trigger unintended form submissions.

In `@web/src/stores/setup-wizard.ts`:
- Around line 451-458: The testProviderConnection function leaves providersError
set on failure and never clears it on retry; before calling testConnection (or
immediately when starting a retry) clear the stale error by calling set({
providersError: null }) so a subsequent successful test won't leave an old error
in the UI; keep the existing catch logic (set({ providersError:
getErrorMessage(err) }) and rethrow) but ensure providersError is reset either
just before await testConnection(name) or in the try branch after a success to
guarantee the UI reflects the current state.
- Around line 367-406: The catch blocks in updateAgentName and
randomizeAgentName currently swallow API errors by only setting agentsError,
preventing callers from detecting failures; update those functions (matching
updateAgentModel's intended behavior) to re-throw the caught error after calling
set({ agentsError: getErrorMessage(err) }) so callers can roll back or keep UI
in sync—i.e., inside updateAgentName and randomizeAgentName catch(err) handlers,
still set agentsError and then throw err (or return Promise.reject(err)) to
propagate the failure.

In `@web/src/utils/cost-estimator.ts`:
- Around line 73-75: Validate and clamp the estimator inputs at function entry:
ensure dailyTokensPerAgent/dailyTokens is >= 0 (or throw) and ensure
inputOutputRatio/inputRatio is within [0,1]; replace use-as-is with the
validated/clamped values before any cost math in functions that use
dailyTokensPerAgent and inputOutputRatio (refer to the variables dailyTokens and
inputRatio in this file), and apply the same validation/clamping to the other
occurrences mentioned around the 101-105 region so all cost calculations use
safe bounded inputs.

In `@web/src/utils/template-categories.ts`:
- Around line 107-117: The getCategoryLabel function currently does an O(n)
reverse lookup over LABEL_TO_CATEGORY; replace that with a direct map lookup
using a new or existing CATEGORY_TO_LABEL (or rename/ensure CATEGORY_LABELS) so
you can return CATEGORY_LABELS[category] (or CATEGORY_TO_LABEL.get(category)) if
present, otherwise keep the existing fallback title-casing logic; update
getCategoryLabel to first check the direct map (using CATEGORY_LABELS or
CATEGORY_TO_LABEL) and only perform the split/title-case fallback when the
lookup returns undefined.

---

Outside diff comments:
In `@web/src/api/types.ts`:
- Around line 1132-1136: SetupCompanyRequest is missing a field to carry
per-template variable overrides; add a new property (e.g., template_variables:
Record<string, string> | null or a typed interface if you have a known schema)
to the SetupCompanyRequest interface so the wizard can serialize and send
collected per-template values; update any consumers/serializers that construct
SetupCompanyRequest to populate this new template_variables field and ensure
backend contract accepts it.

---

Duplicate comments:
In `@web/src/__tests__/pages/setup/WizardProgress.test.tsx`:
- Around line 64-67: Replace the brittle buttons.find(... ) with a role lookup
that reports failures: use screen.getByRole('button', { name: /template/i }) to
obtain the template button (or call screen.getAllByRole then assert the found
element is defined before using it) and then pass that element to await
user.click(...); update the lookup at the occurrences using the templateButton
variable (also lines ~97-100) so tests fail with a clear missing-element message
instead of relying on the non-null assertion.

In `@web/src/__tests__/stores/setup-wizard.test.ts`:
- Around line 18-24: Add a focused test block that covers the missing
provider-store action branches: write tests for createProviderFromPreset,
probeAllPresets, and testProviderConnection including both success and failure
paths; use the existing mocks for createFromPreset, probePreset, and
testConnection (replace vi.fn() return values to simulate resolved values and
rejections) and assert the store actions (createProviderFromPreset,
probeAllPresets, testProviderConnection) dispatch the right mutations/state
changes and error handling branches accordingly (e.g., verify success updates
and failure sets/records errors or returns expected rejected results).

In `@web/src/components/ui/select-field.tsx`:
- Around line 41-79: The component currently uses hardcoded Tailwind utility
classes (gap-1.5, ml-0.5, px-3, py-2, text-sm, text-xs) inside the select-field
JSX; replace those fixed utilities with the design token classes/variables used
across the UI (e.g., token-based spacing, density and typography tokens) so
spacing/typography respond to theme/density changes—update the container div,
label span, select className, hint p and error p to use the token equivalents
instead of the listed utilities and apply the same token-driven replacements to
other files under web/src/components/ui/**/*.tsx as noted in the duplicate
comment. Ensure aria and behaviour code is unchanged and only className strings
are swapped to token names.

In `@web/src/pages/setup/CostEstimatePanel.tsx`:
- Around line 104-110: The CostEstimatePanel currently builds its own
card/header div; replace that outer container with the shared SectionCard
component so the panel uses the standard card-with-header layout. Locate the
CostEstimatePanel component (the JSX rendering the div with classes "space-y-4
rounded-lg border border-border bg-card p-4" and the inner header h3 and p
showing estimate.assumptions.dailyTokensPerAgent) and wrap those inner children
with <SectionCard> instead of the custom div, preserving the inner content (the
h3 and p) as children and moving any needed spacing/styling into SectionCard
props or inner elements so visual behavior remains the same.

In `@web/src/pages/setup/ProviderProbeResults.tsx`:
- Around line 16-23: The component ProviderProbeResults currently renders raw
icons (Loader2, Check, X) based on probing/result/detected; replace that inline
icon logic with the shared <StatusBadge> component, mapping probing ->
state="pending" (show spinner), detected -> state="success", else ->
state="miss", and pass any tooltip/aria text previously shown; also add/remove
imports so StatusBadge is imported and Loader2/Check/X are removed from this
file. Ensure you update the render path that currently checks probing &&
!result, detected, else to render <StatusBadge state={...} /> accordingly.
- Around line 62-80: Replace the manual card markup in ProviderProbeResults (the
div with classes "space-y-3 rounded-lg border border-border bg-card p-4" and its
header/p elements) with the shared SectionCard component: move the header text
and description into SectionCard's title/subtitle props or children, keep the
mapping over localPresets to render ProbeResultItem (passing preset,
probeResults[preset.name], probing, onAddPreset) as SectionCard body content,
and remove the duplicated spacing/styling so the layout uses SectionCard's
standardized header and card styles.

In `@web/src/pages/setup/SkipWizardForm.tsx`:
- Around line 29-39: The current flow reads
useSetupWizardStore.getState().companyError and completionError after awaiting
submitCompany() and wizardCompleteSetup(), which relies on timing of store
mutations; instead change submitCompany and wizardCompleteSetup to propagate
failures (either throw an Error or return an Error result) and update this file
to handle those returns directly: call await submitCompany() inside a try/catch
(or check the returned result) and call setError(err) on caught/returned errors;
do the same for await wizardCompleteSetup(), and remove the post-await store
reads for companyError/completionError — keeping useSetupWizardStore for state
only, not for immediate error control flow.

In `@web/src/pages/setup/TemplateCard.tsx`:
- Around line 31-36: The current TemplateCard component hand-rolls the card
container and uses a hardcoded shadow utility; replace the outer div with the
shared SectionCard primitive (SectionCard) and move the conditional selected
styling to use the tokenized shadow class/variable (e.g., the standard token
such as var(--so-shadow-card-selected) or the project's shadow utility class)
instead of the custom shadow-[...] value; keep the existing cn usage and
preserve selected-driven border logic (use 'border-accent' when selected and
'border-border' otherwise) and any children/props passed through TemplateCard so
behavior is unchanged.

In `@web/src/pages/setup/TemplateCostBadge.tsx`:
- Around line 6-12: The currency prop on TemplateCostBadge is typed as string;
change it to use the shared CurrencyCode type and DEFAULT_CURRENCY constant to
enforce valid codes and a consistent default. Import CurrencyCode and
DEFAULT_CURRENCY, update the component prop type (the currency field in the
props interface) from string to CurrencyCode, and set the default value for
currency to DEFAULT_CURRENCY in the TemplateCostBadge parameter destructuring.

In `@web/src/pages/setup/TemplateStep.tsx`:
- Around line 64-84: The current recommendedTemplates logic uses providerCount
to decide small vs large recommendations; change it to detect actual
provider/model capability instead: compute a boolean (e.g.,
hasEnterpriseCapableProvider) by iterating providers and checking their
capability fields or supported model tiers (inspect each provider object for
properties like supportedModels, tier, supportsEnterprise, maxModelSize, or
similar) and use that boolean when deciding whether to recommend large tags
(TAG_ENTERPRISE, TAG_FULL_COMPANY) or small tags (TAG_SOLO, TAG_SMALL_TEAM,
'startup', 'mvp'); update the useMemo to reference this capability check (still
using templates and providers as dependencies) and ensure the check covers
multiple providers and model variants so enterprise templates are only
recommended when an enterprise-capable provider/model exists.

In `@web/src/pages/setup/ThemePreview.tsx`:
- Around line 32-41: Replace the custom agent row markup in ThemePreview (the
div with Avatar, name, StatusBadge and title) with the shared AgentCard
component from web/src/components/ui so we reuse the design-system pattern;
locate the block that renders Avatar, "Akira Tanaka", StatusBadge(status="idle",
label) and the subtitle "CEO, executive" and render <AgentCard> (or the exported
AgentCard component) passing the corresponding props (name, subtitle/title,
status) and any size/className needed to match styling so StatusBadge and Avatar
usage are removed and the shared component is used consistently.

In `@web/src/pages/setup/WizardShell.tsx`:
- Around line 93-99: The skip button currently calls navigate('/setup/complete')
without indicating a skip flow; change the onClick for the Button (the navigate
call used when currentStep is 'account' or 'template') to pass an explicit skip
indicator (e.g., a query param like ?skipped=true or navigation state { skipped:
true }) and update the CompleteStep component to detect that skip flag (via
location.search or location.state) and render the dedicated skip form/flow
instead of the normal completion path.

In `@web/src/stores/setup-wizard.ts`:
- Around line 287-289: When changing the template, clear downstream derived
state so stale generated org data isn't reused: update the selectTemplate
function to, in addition to set({ selectedTemplate: name }), also clear
companyResponse and agents (e.g. set companyResponse: null and agents: []) and
any dependent step flags; apply the same invalidation in the templateVariables
setter (the code around the templateVariables update at the 306-309 region) so
edits to either selectedTemplate or templateVariables always invalidate
companyResponse and agents and force re-generation.
- Around line 461-478: probeAllPresets currently omits entries that reject so
probeResults treats failures as "not found"; update probeAllPresets to record an
explicit error state per preset instead of skipping it: when mapping/pulling
Promise.allSettled results from probePreset, set results[presetName] to either
the successful ProbePresetResponse or a sentinel error object (e.g., { error:
<reason> } or { status: 'error', message: String(reason) }) so the UI can
distinguish network/backend failures from genuine misses; ensure keys come from
the original preset name (refer to probeAllPresets, probePreset, presets, and
probeResults) and set probing back to false after populating probeResults.

In `@web/src/utils/cost-estimator.ts`:
- Around line 76-79: The modelMap is keyed only by model.id which causes
collisions when different providers expose the same id; change the map key to a
composite key using provider and id (e.g., `${model.provider}:${model.id}`) when
populating the map (refer to modelMap and ProviderModelConfig population) and
update every lookup that uses modelMap.get(model.id) (locations around the
estimation logic noted at 86-87) to use the same composite key so each
provider/model pair is unique.
🪄 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: e16c7521-2b09-4504-9e45-ecf67bced7d2

📥 Commits

Reviewing files that changed from the base of the PR and between ce9164b and dfe0fd1.

📒 Files selected for processing (63)
  • CLAUDE.md
  • README.md
  • docs/design/brand-and-ux.md
  • docs/design/organization.md
  • docs/design/page-structure.md
  • docs/user_guide.md
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/components/ui/input-field.test.tsx
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/__tests__/components/ui/toggle-field.test.tsx
  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/utils/cost-estimator.property.test.ts
  • web/src/__tests__/utils/cost-estimator.test.ts
  • web/src/__tests__/utils/password-strength.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/__tests__/utils/setup-validation.test.ts
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/api/types.ts
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/slider-field.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/SetupPage.tsx
  • web/src/pages/setup/AccountStep.tsx
  • web/src/pages/setup/AgentModelPicker.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/TemplateVariables.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/cost-estimator.ts
  • web/src/utils/currencies.ts
  • web/src/utils/password-strength.ts
  • web/src/utils/provider-status.ts
  • web/src/utils/setup-validation.ts
  • web/src/utils/template-categories.ts

Comment on lines +1114 to +1120
export interface TemplateVariable {
readonly name: string
readonly description: string
readonly var_type: string
readonly default: string | number | boolean | null
readonly required: boolean
}
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

TemplateVariable doesn't carry enough shape for the new controls.

A free-form var_type plus default isn't enough to render or validate sliders safely, and it can't describe enum-style variables like model_tier_profile without hardcoded guesses. Model this as a typed union or add machine-readable bounds/options such as min, max, step, and enum_values.

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

In `@web/src/api/types.ts` around lines 1114 - 1120, The TemplateVariable
interface (TemplateVariable) is too generic: using var_type and default cannot
describe slider bounds or enum choices needed by new controls; update
TemplateVariable to a typed union or extend it with machine-readable metadata
(e.g., numeric fields min, max, step for numeric/slider types and an enum_values
array or options for choice types) and tighten var_type to discriminants (e.g.,
"string" | "number" | "boolean" | "enum" | "slider") so render/validation code
can switch on the discriminant and read min/max/step/enum_values rather than
guessing from name or default.

Comment on lines +1 to +77
import { useState } from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import { Drawer } from './drawer'
import { Button } from './button'

const meta = {
title: 'UI/Drawer',
component: Drawer,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
a11y: { test: 'error' },
},
} satisfies Meta<typeof Drawer>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
open: true,
onClose: () => {},
title: 'Compare Templates',
children: (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Compare templates side by side to find the best fit for your organization.
</p>
<div className="grid grid-cols-2 gap-4">
<div className="rounded-md border border-border p-3">
<h3 className="text-sm font-semibold text-foreground">Tech Startup</h3>
<p className="text-xs text-muted-foreground">5 agents, 3 departments</p>
</div>
<div className="rounded-md border border-border p-3">
<h3 className="text-sm font-semibold text-foreground">Solo Founder</h3>
<p className="text-xs text-muted-foreground">1 agent, 1 department</p>
</div>
</div>
</div>
),
},
}

function InteractiveDrawer() {
const [open, setOpen] = useState(false)
return (
<div className="p-8">
<Button onClick={() => setOpen(true)}>Open Drawer</Button>
<Drawer open={open} onClose={() => setOpen(false)} title="Compare Templates">
<p className="text-sm text-muted-foreground">Drawer content goes here.</p>
</Drawer>
</div>
)
}

export const Interactive: Story = {
args: { open: false, onClose: () => {}, title: 'Drawer', children: null },
render: () => <InteractiveDrawer />,
}

export const ScrollableContent: Story = {
args: {
open: true,
onClose: () => {},
title: 'Long Content',
children: (
<div className="space-y-4">
{Array.from({ length: 20 }, (_, i) => (
<div key={i} className="rounded-md border border-border p-3">
<h3 className="text-sm font-semibold text-foreground">Item {i + 1}</h3>
<p className="text-xs text-muted-foreground">Some content for item {i + 1}</p>
</div>
))}
</div>
),
},
}
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 loading/error/empty content stories.

The current stories demonstrate the Drawer's core functionality well (open/closed, interactive toggle, scrollable content). Per coding guidelines, stories should include all component states. While Drawer is a container, consider adding stories showing:

  • Loading: Drawer with skeleton loaders as children
  • Error: Drawer with an error message/alert as children
  • Empty: Drawer with EmptyState component as children
♻️ Optional: Example additional stories
export const Loading: Story = {
  args: {
    open: true,
    onClose: () => {},
    title: 'Loading Content',
    children: (
      <div className="space-y-4">
        <Skeleton className="h-4 w-3/4" />
        <Skeleton className="h-4 w-1/2" />
        <Skeleton className="h-20" />
      </div>
    ),
  },
}

export const Empty: Story = {
  args: {
    open: true,
    onClose: () => {},
    title: 'No Results',
    children: (
      <EmptyState
        icon={SearchX}
        title="No templates found"
        description="Try adjusting your search criteria."
      />
    ),
  },
}

As per coding guidelines: "All Storybook stories 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/drawer.stories.tsx` around lines 1 - 77, Add separate
Storybook entries for the Drawer component to cover loading, error and empty
states: create new stories (e.g., Loading, Error, Empty) alongside
Default/Interactive/ScrollableContent that set Drawer props (open, onClose,
title) and supply appropriate children like Skeleton loaders for Loading, an
Alert or error message for Error, and the EmptyState component for Empty;
reference the Drawer component and existing story objects (Default, Interactive,
ScrollableContent) when adding these new Story objects so they follow the same
args pattern and rendering approach used in the file.

Comment on lines +16 to +42
export const Default: Story = {
args: { label: 'Company Name', placeholder: 'Enter company name' },
}

export const Required: Story = {
args: { label: 'Company Name', required: true, placeholder: 'Required field' },
}

export const WithError: Story = {
args: { label: 'Company Name', error: 'Company name is required', required: true },
}

export const WithHint: Story = {
args: { label: 'Description', hint: 'Max 1000 characters', placeholder: 'Optional description' },
}

export const Disabled: Story = {
args: { label: 'Company Name', disabled: true, value: 'Acme Corp' },
}

export const Multiline: Story = {
args: { label: 'Description', multiline: true, rows: 4, placeholder: 'Describe your company...' },
}

export const Password: Story = {
args: { label: 'Password', type: 'password', required: true, placeholder: 'Enter password' },
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add the missing canonical Storybook states (hover, loading, empty).

Current stories cover several variants but not the full required state set for this component story file.

As per coding guidelines: "All Storybook stories must include all component states (default, hover, loading, error, empty) alongside the component in .stories.tsx file."

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

In `@web/src/components/ui/input-field.stories.tsx` around lines 16 - 42, Add
three canonical stories named Hover, Loading, and Empty alongside the existing
Default/Required/WithError stories: create Hover by reusing Default args
(label/placeholder) and simulate a hover state (either by setting a supported
prop such as className: 'pseudo-hover' or state/variant prop your InputField
accepts, or by adding a story decorator/parameters that applies :hover styles),
create Loading by adding a loading: true (or an equivalent prop your component
exposes) to show the spinner/disabled input state, and create Empty by ensuring
value: '' with appropriate placeholder to represent no content; reference the
existing story identifiers Default, WithError and Required when copying args so
the new stories match component variants.

Comment on lines +10 to +24
interface InputProps extends BaseFieldProps, Omit<React.ComponentProps<'input'>, 'id'> {
multiline?: false
ref?: React.Ref<HTMLInputElement>
}

interface TextareaProps extends BaseFieldProps, Omit<React.ComponentProps<'textarea'>, 'id'> {
multiline: true
ref?: React.Ref<HTMLTextAreaElement>
}

export type InputFieldProps = InputProps | TextareaProps

export function InputField({
label, error, hint, multiline, className, ref, ...props
}: InputFieldProps) {
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
# Verify InputField ref implementation and call sites before applying fix.
rg -n "export function InputField|forwardRef\\(" web/src/components/ui/input-field.tsx
rg -nP "<InputField[^>]*\\bref=" --type tsx web/src

Repository: Aureliolo/synthorg

Length of output: 138


🏁 Script executed:

cat -n web/src/components/ui/input-field.tsx

Repository: Aureliolo/synthorg

Length of output: 2900


🏁 Script executed:

rg -n "ref=" web/src -A 2 -B 2 | grep -i inputfield -A 5 -B 5

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

rg -n "InputField" web/src | head -20

Repository: Aureliolo/synthorg

Length of output: 1604


🏁 Script executed:

rg -n "ref=" web/src/pages/setup/SkipWizardForm.tsx -A 5 -B 2 | grep -i inputfield -A 5 -B 5

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

rg -A 10 "multiline" web/src/pages/setup/SkipWizardForm.tsx

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

cat -n web/src/pages/setup/SkipWizardForm.tsx | sed -n '60,75p'

Repository: Aureliolo/synthorg

Length of output: 740


🏁 Script executed:

rg "InputField[^>]*ref=" web/src --type-add 'tsx:*.tsx'

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

rg -n "InputField" web/src/pages web/src/components web/src/__tests__ | grep -v "import\|export\|component:" | head -30

Repository: Aureliolo/synthorg

Length of output: 2072


Migrate to React.forwardRef to support the exposed ref prop.

Plain function components don't receive the ref JSX attribute via props—React extracts it separately. The current type signatures expose ref in InputProps and TextareaProps, but at runtime the ref will be undefined since the component isn't wrapped in React.forwardRef. Either wrap the component with React.forwardRef (using appropriate input/textarea ref typing) or remove ref from the public API types.

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

In `@web/src/components/ui/input-field.tsx` around lines 10 - 24, The InputField
component currently declares a ref in the InputProps and TextareaProps types but
is a plain function so React won't pass the JSX ref through; wrap InputField
with React.forwardRef and update its signature to accept (props:
InputFieldProps, ref: React.Ref<HTMLInputElement | HTMLTextAreaElement>) so the
ref is forwarded to the underlying input/textarea, or alternatively remove the
ref from InputProps/TextareaProps; update the internal usage to attach the
forwarded ref to the rendered <input> or <textarea> element and adjust type
narrowing based on multiline to ensure correct element ref typing for both input
and textarea cases.

Comment on lines +367 to +406
async updateAgentModel(index, provider, modelId) {
set({ agentsError: null })
try {
const updated = await apiUpdateAgentModel(index, {
model_provider: provider,
model_id: modelId,
})
set((s) => ({
agents: s.agents.map((a, i) => i === index ? updated : a),
}))
} catch (err) {
console.error('setup-wizard: updateAgentModel failed:', err)
set({ agentsError: getErrorMessage(err) })
}
},

async updateAgentName(index, name) {
set({ agentsError: null })
try {
const updated = await apiUpdateAgentName(index, { name })
set((s) => ({
agents: s.agents.map((a, i) => i === index ? updated : a),
}))
} catch (err) {
console.error('setup-wizard: updateAgentName failed:', err)
set({ agentsError: getErrorMessage(err) })
}
},

async randomizeAgentName(index) {
set({ agentsError: null })
try {
const updated = await apiRandomizeAgentName(index)
set((s) => ({
agents: s.agents.map((a, i) => i === index ? updated : a),
}))
} catch (err) {
console.error('setup-wizard: randomizeAgentName failed:', err)
set({ agentsError: 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

Re-throw agent mutation failures.

These actions set agentsError but resolve normally, so callers can't roll back inline name edits or keep model-picker/randomize UI in sync when the API fails.

💡 Proposed fix
   async updateAgentModel(index, provider, modelId) {
     set({ agentsError: null })
     try {
       const updated = await apiUpdateAgentModel(index, {
         model_provider: provider,
         model_id: modelId,
       })
       set((s) => ({
         agents: s.agents.map((a, i) => i === index ? updated : a),
       }))
     } catch (err) {
       console.error('setup-wizard: updateAgentModel failed:', err)
       set({ agentsError: getErrorMessage(err) })
+      throw err
     }
   },

Apply the same catch-path change to updateAgentName and randomizeAgentName.

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

In `@web/src/stores/setup-wizard.ts` around lines 367 - 406, The catch blocks in
updateAgentName and randomizeAgentName currently swallow API errors by only
setting agentsError, preventing callers from detecting failures; update those
functions (matching updateAgentModel's intended behavior) to re-throw the caught
error after calling set({ agentsError: getErrorMessage(err) }) so callers can
roll back or keep UI in sync—i.e., inside updateAgentName and randomizeAgentName
catch(err) handlers, still set agentsError and then throw err (or return
Promise.reject(err)) to propagate the failure.

Comment on lines +73 to +75
const dailyTokens = options?.dailyTokensPerAgent ?? DEFAULT_DAILY_TOKENS_PER_AGENT
const inputRatio = options?.inputOutputRatio ?? DEFAULT_INPUT_OUTPUT_RATIO

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 | 🟡 Minor

Validate estimator options before cost math.

dailyTokensPerAgent and inputOutputRatio are used as-is. Invalid values (negative tokens, ratio outside [0,1]) produce nonsensical costs. Add explicit bounds checks (or clamp) at function entry.

Also applies to: 101-105

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

In `@web/src/utils/cost-estimator.ts` around lines 73 - 75, Validate and clamp the
estimator inputs at function entry: ensure dailyTokensPerAgent/dailyTokens is >=
0 (or throw) and ensure inputOutputRatio/inputRatio is within [0,1]; replace
use-as-is with the validated/clamped values before any cost math in functions
that use dailyTokensPerAgent and inputOutputRatio (refer to the variables
dailyTokens and inputRatio in this file), and apply the same validation/clamping
to the other occurrences mentioned around the 101-105 region so all cost
calculations use safe bounded inputs.

@Aureliolo Aureliolo force-pushed the feat/setup-wizard-rebuild branch from dfe0fd1 to 34e20d7 Compare March 27, 2026 15:34
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 15:35 — with GitHub Actions Inactive
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: 11

♻️ Duplicate comments (23)
web/src/pages/setup/TemplateCostBadge.tsx (1)

4-14: 🧹 Nitpick | 🔵 Trivial

Use CurrencyCode type and DEFAULT_CURRENCY constant for type safety.

The currency prop is typed as string, allowing invalid currency codes at compile time. The currencies.ts utility already exports CurrencyCode (a union of valid codes) and DEFAULT_CURRENCY which should be used here to ensure consistency and type safety.

🔧 Proposed fix
 import { cn } from '@/lib/utils'
 import { formatCurrency } from '@/utils/format'
+import { DEFAULT_CURRENCY, type CurrencyCode } from '@/utils/currencies'

 export interface TemplateCostBadgeProps {
   monthlyCost: number
-  currency?: string
+  currency?: CurrencyCode
   className?: string
 }

 export function TemplateCostBadge({
   monthlyCost,
-  currency = 'EUR',
+  currency = DEFAULT_CURRENCY,
   className,
 }: TemplateCostBadgeProps) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/TemplateCostBadge.tsx` around lines 4 - 14, The currency
prop in TemplateCostBadgeProps and its default in TemplateCostBadge should use
the CurrencyCode type and DEFAULT_CURRENCY constant from currencies.ts for
compile-time safety; update the interface to declare currency?: CurrencyCode and
change the default value in the TemplateCostBadge parameter list to
DEFAULT_CURRENCY (importing CurrencyCode and DEFAULT_CURRENCY), ensuring
references to monthlyCost and className remain unchanged.
web/src/components/ui/slider-field.stories.tsx (1)

17-54: 🛠️ Refactor suggestion | 🟠 Major

Add the missing canonical story states (Hover, Loading, Error, Empty).

SliderField stories currently cover default/format/disabled/interactive only, so the required canonical state matrix is still incomplete.

As per coding guidelines: “Create .stories.tsx file with all 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/slider-field.stories.tsx` around lines 17 - 54, Add the
missing canonical story states for SliderField by creating new Story exports
named Hover, Loading, Error, and Empty alongside the existing
Default/WithFormat/Disabled/Interactive; each should provide appropriate args or
a render to simulate the state (e.g., Hover with a focused/hover prop or
storybook play to hover, Loading with a loading prop or skeleton state, Error
with an error message prop, Empty with value undefined/null) so the SliderField
component and InteractiveSlider usage are covered; reference the existing Story
type and props (label, value, min, max, step, formatValue, onChange, disabled)
when constructing these stories to mirror the pattern used by
Default/WithFormat/Disabled/Interactive.
web/src/components/ui/select-field.tsx (1)

41-45: 🛠️ Refactor suggestion | 🟠 Major

Replace fixed utility sizing/spacing classes with project design tokens.

SelectField still uses non-token utility sizing/spacing/typography classes in this shared primitive. Please align it with the tokenized class patterns used by the rest of the UI foundation.

As per coding guidelines: “When creating new shared React components… Use design tokens exclusively.”

Also applies to: 57-59, 74-77

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

In `@web/src/components/ui/select-field.tsx` around lines 41 - 45, The SelectField
component uses hardcoded utility classes (e.g., the wrapper div, label element,
required span, and other occurrences in the select/input and helper text)
instead of project design tokens; update SelectField to replace fixed
sizing/spacing/typography Tailwind utilities with the project’s token class
names (use the tokenized equivalents for container spacing, label typography,
required indicator spacing/color, select sizing, and helper text) so all
className values in the SelectField component (including the wrapper div, label,
required span, select/input, and helper text) use the design-token class
patterns used across the UI foundation.
web/src/components/ui/input-field.tsx (1)

10-24: ⚠️ Potential issue | 🟠 Major

ref API is broken without React.forwardRef.

InputField exposes and consumes a ref prop, but as a plain function component it will not receive JSX refs at runtime (Line 23). This makes ref forwarding effectively non-functional.

🔧 Suggested fix
-import { useId } from 'react'
+import { forwardRef, useId } from 'react'
@@
-export function InputField({
-  label, error, hint, multiline, className, ref, ...props
-}: InputFieldProps) {
+export const InputField = forwardRef<HTMLInputElement | HTMLTextAreaElement, InputFieldProps>(function InputField({
+  label, error, hint, multiline, className, ...props
+}, ref) {
@@
-}
+})
#!/bin/bash
# Verify whether InputField is using forwardRef and whether any call sites pass a JSX ref.
rg -n "export function InputField|forwardRef\\(" web/src/components/ui/input-field.tsx
rg -nP "<InputField[^>]*\\bref=" --type tsx web/src

Also applies to: 46-59

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

In `@web/src/components/ui/input-field.tsx` around lines 10 - 24, InputField’s ref
handling is broken because it’s a plain function component that declares a ref
prop; change it to use React.forwardRef so JSX refs work: convert the export
function InputField(...) to export const InputField =
React.forwardRef<HTMLInputElement | HTMLTextAreaElement,
InputFieldProps>(function InputField(props, ref) { ... }), remove/refactor the
explicit ref?: fields if necessary to accept the forwarded ref type (ensure
InputProps and TextareaProps use React.Ref<HTMLInputElement> |
React.Ref<HTMLTextAreaElement> types only if still needed), update the parameter
destructuring to take ref as the second arg (e.g., ({ label, error, hint,
multiline, className, ...props }, ref)), and set InputField.displayName =
'InputField' for nicer DevTools names; ensure internal uses of ref forward it to
the underlying <input> or <textarea> element depending on multiline.
web/src/components/ui/input-field.stories.tsx (1)

16-42: 🛠️ Refactor suggestion | 🟠 Major

Complete canonical Storybook state coverage (Hover, Loading, Empty).

You already have default/error variants, but this story file still misses the full required state set for shared UI components.

As per coding guidelines: “Create .stories.tsx file with all 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/input-field.stories.tsx` around lines 16 - 42, Add the
missing canonical stories: create Hover, Loading and Empty exports alongside the
existing Default, Required, WithError, WithHint, Disabled, Multiline, and
Password stories; for Hover, reuse Default args and either add a play() that
calls userEvent.hover on the input or apply a story decorator that forces the
:hover pseudo-state so the hover visuals are visible; for Loading, supply the
component's loading prop (e.g., isLoading/loading) and args that show a
spinner/disabled input and a relevant placeholder to demonstrate the loading UI;
for Empty, supply args with an empty value (value: '') and appropriate
placeholder/hint to show the empty state; name the new exports Hover, Loading,
and Empty so they appear in Storybook.
README.md (1)

110-110: ⚠️ Potential issue | 🟡 Minor

Missing mention of the conditional account creation step.

Per the PR's step flow (Account → Template → Company → Agents → Providers → Theme → Complete), fresh installs may first prompt for admin account creation when needsAdmin is true. The current description assumes template selection is always first.

📝 Suggested clarification
-Open [http://localhost:3000](http://localhost:3000) after `synthorg start` -- on a fresh install, the **setup wizard** guides you through choosing a company template, naming your company, customizing agents, configuring LLM providers, setting theme preferences, and reviewing the organization before launch.
+Open [http://localhost:3000](http://localhost:3000) after `synthorg start` -- on a fresh install, the **setup wizard** guides you through account creation (if needed), choosing a company template, naming your company, customizing agents, configuring LLM providers, setting theme preferences, and reviewing the organization before launch.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 110, Update the README’s setup wizard description to note
the conditional initial admin account creation step: mention that on fresh
installs, if the needsAdmin flag is true the wizard first prompts to create an
admin account (Account → Template → Company → Agents → Providers → Theme →
Complete), otherwise it proceeds directly to template selection; reference the
"setup wizard" and the "needsAdmin" condition in the text so readers understand
the alternate flow.
web/src/pages/setup/SkipWizardForm.tsx (1)

27-40: 🧹 Nitpick | 🔵 Trivial

Error handling relies on synchronous store state after async calls.

The pattern of calling await submitCompany() then reading getState().companyError assumes errors are set synchronously before the promise resolves. This works if store actions follow that convention but becomes fragile if actions are refactored.

Consider having store actions return the error directly:

♻️ Alternative approach
-      await submitCompany()
-      const companyErr = useSetupWizardStore.getState().companyError
-      if (companyErr) {
-        setError(companyErr)
-        return
-      }
+      const companyErr = await submitCompany()
+      if (companyErr) {
+        setError(companyErr)
+        return
+      }

This requires submitCompany() to return string | null instead of relying on store state.

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

In `@web/src/pages/setup/SkipWizardForm.tsx` around lines 27 - 40, The current
code reads errors from useSetupWizardStore.getState() after awaiting
submitCompany() and wizardCompleteSetup(), which is fragile; change
submitCompany() and wizardCompleteSetup() to return their error (string|null) or
throw on failure, then update this component to capture those return values
(e.g., const companyErr = await submitCompany()) and call setError(companyErr)
if present instead of reading companyError/completionError from the store; keep
setCompanyNameStore and setError usage the same but stop relying on synchronous
store state changes after the awaits.
web/src/pages/setup/ThemePreview.tsx (1)

31-41: 🛠️ Refactor suggestion | 🟠 Major

Replace manual agent card markup with AgentCard component.

This block manually recreates the agent display pattern that AgentCard already provides. Using the shared component ensures design consistency and reduces maintenance drift.

♻️ Proposed refactor using AgentCard
+import { AgentCard } from '@/components/ui/agent-card'
 // ... other imports

       {/* Agent card mock */}
-      <div className="flex items-center gap-3 rounded-md border border-border bg-card p-3">
-        <Avatar name="Akira Tanaka" size="sm" />
-        <div className="flex-1">
-          <div className="flex items-center gap-2">
-            <span className="text-sm font-medium text-foreground">Akira Tanaka</span>
-            <StatusBadge status="idle" label />
-          </div>
-          <span className="text-xs text-muted-foreground">CEO, executive</span>
-        </div>
-      </div>
+      <AgentCard
+        name="Akira Tanaka"
+        role="CEO"
+        department="executive"
+        status="idle"
+        size="sm"
+      />

As per coding guidelines: "Use AgentCard component from @/components/ui/agent-card for agent display: avatar, name, role, status, current task."

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

In `@web/src/pages/setup/ThemePreview.tsx` around lines 31 - 41, Replace the
manual agent card markup in ThemePreview (the div block that uses Avatar and
StatusBadge) with the shared AgentCard component from
"@/components/ui/agent-card": import AgentCard at the top, remove the manual
markup (the div container, Avatar, name span, StatusBadge, and role span) and
render <AgentCard> with appropriate props (e.g., name="Akira Tanaka", role="CEO,
executive", status="idle" and size or variant if supported) so the avatar, name,
role, status and current task are provided by the single source component and
design consistency is preserved.
web/src/api/types.ts (1)

1114-1120: ⚠️ Potential issue | 🟠 Major

Fix TemplateVariable to use discriminated union with per-variable bounds metadata.

var_type: string is too generic and forces hardcoded bounds: lines 48–50 in TemplateVariables.tsx hard-code min=1, max=50 for int and min=0, max=1000 for float. Different variables need different ranges, and there's no way to support enum types. Additionally, the TemplateVariable interface is duplicated locally in TemplateVariables.tsx—import from types instead.

♻️ Proposed fix
 export interface TemplateVariable {
   readonly name: string
   readonly description: string
-  readonly var_type: string
+  readonly var_type: 'string' | 'bool' | 'int' | 'float' | 'enum'
   readonly default: string | number | boolean | null
   readonly required: boolean
+  readonly min?: number
+  readonly max?: number
+  readonly step?: number
+  readonly enum_values?: readonly string[]
 }

Remove the duplicate TemplateVariable interface from web/src/pages/setup/TemplateVariables.tsx and import from @/api/types instead.

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

In `@web/src/api/types.ts` around lines 1114 - 1120, The TemplateVariable
interface currently uses a generic var_type string and lacks per-variable
bounds/enums; change it to a discriminated union in TemplateVariable (exported
from web/src/api/types.ts) with specific variants for types like { var_type:
"int"; min?: number; max?: number }, { var_type: "float"; min?: number; max?:
number; step?: number }, { var_type: "string"; minLength?: number; maxLength?:
number }, and { var_type: "enum"; options: string[] } (including shared fields
name, description, default, required), then remove the duplicate
TemplateVariable definition from TemplateVariables.tsx and import the updated
TemplateVariable from `@/api/types` so the TemplateVariables component can read
per-variable bounds and enums instead of hardcoding ranges.
web/src/__tests__/utils/template-categories.test.ts (1)

85-87: ⚠️ Potential issue | 🟡 Minor

Expected order is derived from the SUT output, so this can mask regressions.

On Line 86, CATEGORY_ORDER.filter((c) => result.has(c)) uses categorizeTemplates() output to build the expected value. Build expected categories from templates instead.

🔧 Proposed fix
-    const expectedOrder = CATEGORY_ORDER.filter((c) => result.has(c))
+    const expectedOrder = CATEGORY_ORDER.filter((c) =>
+      templates.some((t) => t.tags.includes(c)),
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/utils/template-categories.test.ts` around lines 85 - 87,
The test builds expectedOrder from the SUT output (result) which can hide
regressions; change it to derive expectedOrder from the input templates instead:
use CATEGORY_ORDER but filter by whether any item in the original templates
array maps to that category (i.e. check templates for presence of that category)
rather than checking result.has(c). Update the line that computes expectedOrder
in the test (near variables CATEGORY_ORDER, templates, result, keys, and
function categorizeTemplates) so expectedOrder reflects the input data, then
assert keys === expectedOrder.
web/src/pages/setup/TemplateCard.tsx (1)

31-36: 🛠️ Refactor suggestion | 🟠 Major

Replace arbitrary shadow value with design token.

Line 34 uses a hardcoded arbitrary shadow shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)]. While it references --so-accent, the shadow definition itself bypasses the design system's shadow tokens.

Use the shared shadow token (e.g., shadow-card-hover or var(--so-shadow-card-hover)) or define a new semantic token if the accent glow is a distinct visual pattern needed elsewhere.

♻️ Proposed fix using existing token
       className={cn(
         'flex flex-col gap-3 rounded-lg border bg-card p-4 transition-colors',
-        selected ? 'border-accent shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)]' : 'border-border',
+        selected ? 'border-accent shadow-card-hover' : 'border-border',
         'hover:bg-card-hover',
       )}

If the accent-tinted glow is intentional design, add a semantic token like --so-shadow-card-selected in your theme CSS and use shadow-card-selected here.

As per coding guidelines: "Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders in React components. NEVER hardcode shadow or border values."

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

In `@web/src/pages/setup/TemplateCard.tsx` around lines 31 - 36, The selected
state in TemplateCard.tsx currently injects a hardcoded arbitrary shadow string
inside the className (the ternary branch that adds 'border-accent
shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)]'); replace
that hardcoded shadow with the design token (e.g., use the existing token class
like 'shadow-card-hover' or a new semantic token such as 'shadow-card-selected'
that maps to var(--so-shadow-card-selected)/var(--so-shadow-card-hover)); update
the ternary to only reference the token class (keep 'border-accent' as-is) and
if introducing a new token add the corresponding CSS variable in the theme file
(e.g., --so-shadow-card-selected) so the component uses tokenized shadows
instead of an arbitrary value.
web/src/utils/template-categories.ts (1)

9-76: 🛠️ Refactor suggestion | 🟠 Major

Category assignment still round-trips through display labels.

The getTemplateCategory function (Lines 68-76) converts tag → CATEGORY_LABELS[tag] → LABEL_TO_CATEGORY[label] → canonical key. This makes category assignment dependent on presentation copy—renaming a label (e.g., "Development" → "Dev Teams") would silently miscategorize matching templates as "other".

A direct TAG_TO_CATEGORY map would decouple grouping from display labels:

♻️ Proposed refactor
+/** Maps raw template tags to canonical category keys */
+const TAG_TO_CATEGORY: Readonly<Record<string, string>> = {
+  startup: 'startup',
+  solo: 'startup',
+  'small-team': 'startup',
+  mvp: 'startup',
+  'dev-shop': 'dev-shop',
+  'data-team': 'dev-shop',
+  product: 'product',
+  agile: 'product',
+  enterprise: 'enterprise',
+  'full-company': 'enterprise',
+  consultancy: 'consultancy',
+  agency: 'consultancy',
+  research: 'research',
+}

 function getTemplateCategory(template: TemplateInfoResponse): string {
   for (const tag of template.tags) {
-    const label = CATEGORY_LABELS[tag]
-    if (label) {
-      return LABEL_TO_CATEGORY[label] ?? 'other'
+    const category = TAG_TO_CATEGORY[tag]
+    if (category) {
+      return category
     }
   }
   return 'other'
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/template-categories.ts` around lines 9 - 76,
getTemplateCategory currently maps tags by looking up CATEGORY_LABELS[tag] then
reversing via LABEL_TO_CATEGORY, which ties grouping to display text; add a
direct mapping (e.g., TAG_TO_CATEGORY) from tag key → canonical category and
update getTemplateCategory to use that map (falling back to 'other') instead of
round-tripping through CATEGORY_LABELS and LABEL_TO_CATEGORY; keep
CATEGORY_LABELS and LABEL_TO_CATEGORY for UI label lookups but replace the
lookup in getTemplateCategory to reference TAG_TO_CATEGORY to decouple grouping
from presentation.
web/src/pages/setup/WizardShell.tsx (1)

93-104: ⚠️ Potential issue | 🔴 Critical

Skip wizard routing still navigates to CompleteStep instead of SkipWizardForm.

The "Skip wizard (advanced)" button navigates to /setup/complete (Line 98), which renders CompleteStep. As noted in the past review, CompleteStep requires full wizard data (company, agents, providers) that won't exist when users skip from early steps. The SkipWizardForm component exists for this exact use case but isn't being used.

Consider either:

  1. Adding a separate route/condition for the skip flow that renders SkipWizardForm
  2. Using a query parameter or store flag to indicate skip mode, then conditionally render SkipWizardForm in the complete step
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/WizardShell.tsx` around lines 93 - 104, The "Skip wizard
(advanced)" button in WizardShell currently navigates to /setup/complete and
triggers CompleteStep which expects full wizard data; change the skip flow to
render SkipWizardForm instead by updating the navigate target or complete-step
render logic: either (A) modify the onClick in WizardShell's Button (the
navigate(...) call) to route to a distinct skip path (e.g., /setup/skip) that
you add and map to SkipWizardForm, or (B) append a query flag (e.g., ?skip=true)
in the navigate call and update CompleteStep's rendering logic to detect that
flag (or a store flag) and render SkipWizardForm when skip mode is present;
reference WizardShell, CompleteStep, SkipWizardForm, and the navigate(...) call
when making the change.
web/src/pages/setup/SetupSummary.tsx (1)

98-100: ⚠️ Potential issue | 🟡 Minor

Avoid index-based keys for agent rows.

Using index as part of the key can mis-map row identity when list order/content changes.

Suggested fix
-          {agents.map((agent, index) => (
-            <SetupAgentRow key={`agent-${index}`} agent={agent} />
+          {agents.map((agent) => (
+            <SetupAgentRow
+              key={`${agent.name}-${agent.role}-${agent.department}-${agent.model_provider}-${agent.model_id}`}
+              agent={agent}
+            />
           ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/SetupSummary.tsx` around lines 98 - 100, The current
mapping in SetupSummary uses the array index for keys which can break identity:
change the key on the SetupAgentRow rendered in the agents.map to a stable
unique identifier from each agent (e.g., agent.id, agent.uuid, or another
immutable property such as agent.email or name) instead of `index`; if agents
may lack an id, add/derive a stable unique field on the agent objects before
rendering and use that property as the key so SetupAgentRow preserves identity
across reorderings.
web/src/pages/setup/TemplateVariables.tsx (1)

22-28: 🛠️ Refactor suggestion | 🟠 Major

Replace the custom wrapper with SectionCard.

The component is still implementing a card header/body shell manually.

As per coding guidelines: "Use SectionCard component from @/components/ui/section-card for titled card wrappers with icon and action slots" and "Do NOT build card-with-header layouts from scratch in React -- use <SectionCard> component."

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

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 22 - 28, The
component currently builds a manual card wrapper (the outer div with class
"space-y-4 rounded-lg border border-border bg-card p-4" and the inner header
block with h3/p) instead of using the shared SectionCard; replace that outer
wrapper and header/body markup with the SectionCard component imported from
"@/components/ui/section-card", moving the title ("Template Variables") into
SectionCard's title slot/prop and the descriptive paragraph and the rest of the
content into its body; preserve any styling and functionality (icons/actions
slots if needed) and remove the duplicated card/header markup so
TemplateVariables uses SectionCard as the single wrapper component.
web/src/pages/setup/ProviderAddForm.tsx (3)

60-67: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this card-with-header block.

This wrapper duplicates the shared card shell instead of reusing the setup UI primitive.

As per coding guidelines: "Use SectionCard component from @/components/ui/section-card for titled card wrappers with icon and action slots" and "Do NOT build card-with-header layouts from scratch in React -- use <SectionCard> component."

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

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 60 - 67, The
card-with-header in ProviderAddForm is built manually; replace the outer div and
internal header block with the shared SectionCard component to avoid
duplication. Import SectionCard from "@/components/ui/section-card" and wrap the
form contents with <SectionCard title="Add Cloud Provider" subtitle="Connect a
cloud LLM provider with your API key."> (or use the component's icon/action
slots if needed), removing the duplicated classes on the current div and header;
keep the inner form markup (fields/buttons) unchanged and ensure ProviderAddForm
uses SectionCard's props for title/subtitle/icon/actions instead of custom
header markup.

26-39: ⚠️ Potential issue | 🟠 Major

Test Connection/Create Provider still don’t validate the full draft config.

The test path only sends providerName, and create still allows submitting cloud providers without a key. This can report a misleading “tested” state and allow invalid submissions.

Suggested direction
 export interface ProviderAddFormProps {
   presets: readonly ProviderPreset[]
   onAdd: (presetName: string, name: string, apiKey?: string) => Promise<void>
-  onTest: (name: string) => Promise<TestConnectionResponse>
+  onTest: (presetName: string, name: string, apiKey?: string) => Promise<TestConnectionResponse>
 }
...
-    if (!providerName.trim()) return
+    if (!selectedPreset || !providerName.trim() || !apiKey.trim()) return
...
-      const result = await onTest(providerName.trim())
+      const result = await onTest(selectedPreset, providerName.trim(), apiKey.trim())
...
-              disabled={testing || !providerName.trim()}
+              disabled={testing || !selectedPreset || !providerName.trim() || !apiKey.trim()}
...
-              disabled={adding || !providerName.trim() || !selectedPreset}
+              disabled={adding || !providerName.trim() || !selectedPreset || !apiKey.trim()}

Also applies to: 41-47, 113-117

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

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 26 - 39, handleTest
currently only sends providerName to onTest which doesn't validate the full
draft config and lets Create Provider submit incomplete providers; update
handleTest to pass the entire draft config (not just providerName) to onTest
(e.g., pass draftConfig or the form state used to build providers) and ensure
setTestResult/setError handle the returned validation result; additionally add
validation in the create/submit handler (the function that creates providers) to
reject or block submission when required fields like the provider key are
missing (perform the same full-config validation before calling the create API
and surface errors via setError).

26-39: ⚠️ Potential issue | 🟡 Minor

Clear stale testResult on input changes and before retesting.

A prior success/failure can remain visible after preset/name/key changes, so the status panel can describe an outdated draft.

Also applies to: 76-81, 91-100, 122-133

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

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 26 - 39, The testResult
state is not cleared when inputs change or when starting a new test, causing
stale results to show; update ProviderAddForm to call setTestResult(null)
whenever relevant inputs change (e.g., in onChange handlers for preset/name/key
fields) and at the start of handleTest before calling onTest (add
setTestResult(null) just after setTesting(true) and setError(null)). Also apply
the same clear-before-retest change for the other test handlers referenced (the
blocks around the other test functions) so every test run and every input change
resets testResult via setTestResult(null).
web/src/pages/setup/AccountStep.tsx (1)

60-114: ⚠️ Potential issue | 🟠 Major

Use a semantic <form onSubmit> for submission.

Submission is still click-driven. Wrapping these controls in a form and setting the button to type="submit" restores expected Enter-key behavior.

Suggested fix
-      <div className="space-y-4 rounded-lg border border-border bg-card p-6">
+      <form
+        className="space-y-4 rounded-lg border border-border bg-card p-6"
+        onSubmit={(e) => {
+          e.preventDefault()
+          void handleSubmit()
+        }}
+      >
...
-        <Button onClick={handleSubmit} disabled={loading} className="w-full">
+        <Button type="submit" disabled={loading} className="w-full">
           {loading ? 'Creating Account...' : 'Create Account'}
         </Button>
-      </div>
+      </form>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/AccountStep.tsx` around lines 60 - 114, Wrap the input
fields and submit Button in a semantic <form> with an onSubmit handler that
calls the existing handleSubmit (ensure you prevent default inside handleSubmit
or call e.preventDefault() in the new onSubmit wrapper), change the Button to
type="submit" so Enter triggers submission, and keep existing disabled={loading}
and validation/error props (references: handleSubmit, password, confirmPassword,
username, Button, InputField) so behavior and validation remain unchanged.
web/src/pages/setup/ProviderProbeResults.tsx (2)

62-80: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for the probe results container.

This is a hand-rolled card-with-header wrapper; switching to SectionCard keeps setup screens consistent.

As per coding guidelines: "Use SectionCard component from @/components/ui/section-card for titled card wrappers with icon and action slots" and "Do NOT build card-with-header layouts from scratch in React -- use <SectionCard> component."

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

In `@web/src/pages/setup/ProviderProbeResults.tsx` around lines 62 - 80, The probe
results UI in ProviderProbeResults.tsx is using a custom div wrapper instead of
the shared SectionCard; replace the hand-rolled card (the outer div that
contains the header and maps localPresets to ProbeResultItem) with the
SectionCard component from "@/components/ui/section-card", pass the title
("Auto-detected Providers" / "Detecting local providers..." using probing) and
any icon/action slots as needed, keep the mapped children (localPresets.map ->
ProbeResultItem with props preset, result, probing, onAddPreset) intact, and add
the SectionCard import at the top of the file.

16-23: 🛠️ Refactor suggestion | 🟠 Major

Replace inline probe-state icons with StatusBadge.

The status UI is hand-built (Loader2/Check/X) instead of using the shared status component.

As per coding guidelines: "Use StatusBadge component from @/components/ui/status-badge for agent/task/system status indicators" and "Do NOT recreate status dots inline in React -- use <StatusBadge> component."

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

In `@web/src/pages/setup/ProviderProbeResults.tsx` around lines 16 - 23, Replace
the ad-hoc icons (Loader2/Check/X) in ProviderProbeResults.tsx with the shared
StatusBadge component: import StatusBadge from "@/components/ui/status-badge"
and render it instead of the inline icons, mapping the current state variables
(probing, result, detected) to the appropriate StatusBadge state (e.g., loading
when probing && !result, success when detected, error/neutral otherwise). Remove
the Loader2/Check/X JSX and ensure the surrounding className/layout is preserved
while passing the correct status/variant props to StatusBadge.
web/src/__tests__/stores/setup-wizard.test.ts (1)

236-267: 🧹 Nitpick | 🔵 Trivial

Consider adding provider action tests.

While agent action tests are present, provider-related actions (createProviderFromPreset, probeAllPresets, testProviderConnection) lack test coverage. Testing the error path for createProviderFromPreset would be particularly valuable given its re-throw behavior.

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

In `@web/src/__tests__/stores/setup-wizard.test.ts` around lines 236 - 267, Add
unit tests covering provider actions: write tests for createProviderFromPreset
(including the error path to ensure it re-throws and state is unchanged),
probeAllPresets (happy/error paths verifying returned probe results and state
changes), and testProviderConnection (success and failure behaviors). In the
test file where agent tests live (setup-wizard.test.ts) import and mock the API
functions createProviderFromPreset, probeAllPresets, and testProviderConnection,
use useSetupWizardStore.setState to seed initial providers, call the
corresponding store methods (createProviderFromPreset, probeAllPresets,
testProviderConnection) and assert expected state mutations and that errors are
propagated when mocks reject. Ensure each test verifies both the API invocation
parameters and resulting store state or thrown error.
web/src/pages/setup/ProvidersStep.tsx (1)

55-55: ⚠️ Potential issue | 🟡 Minor

Rename probeAttempted to probeAttemptedRef.

Static analysis flags this: ref variables should be named ref or end with Ref per project conventions. All other useRef variables in the codebase follow this pattern (validatedRef, prevValueRef, etc.).

🔧 Proposed fix
-  const probeAttempted = useRef(false)
+  const probeAttemptedRef = useRef(false)

Also update usages at lines 73-74:

-    if (presets.length > 0 && probeResultsCount === 0 && !probing && !probeAttempted.current) {
-      probeAttempted.current = true
+    if (presets.length > 0 && probeResultsCount === 0 && !probing && !probeAttemptedRef.current) {
+      probeAttemptedRef.current = true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/ProvidersStep.tsx` at line 55, Rename the ref variable
probeAttempted to probeAttemptedRef to follow the project's naming convention
for useRef variables; update the declaration (const probeAttempted =
useRef(false)) to const probeAttemptedRef = useRef(false) and replace all
subsequent usages of probeAttempted (e.g., checks and assignments like
probeAttempted.current = true and if (probeAttempted.current) ...) with
probeAttemptedRef so the code consistently references the renamed ref.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/organization.md`:
- Around line 336-347: Update the Theme step description to include the missing
fifth axis, typography: change the phrase "Theme (set UI preferences for
palette, density, animation, and sidebar)" to "Theme (set UI preferences for
palette, density, typography, animation, and sidebar)" so the doc matches the
five-axis customization spec; ensure any nearby references to "five-axis" or
"Theme" wording in the same paragraph (e.g., the sentence beginning "When a
template is selected..." if it references theme axes) are consistent with adding
typography.

In `@web/src/components/ui/input-field.tsx`:
- Around line 30-37: The input primitive builds its class list with hardcoded
Tailwind utility sizes (e.g., 'rounded-md', 'px-3', 'py-2', 'text-sm') which
violates the design-token rule; update the inputClasses construction in
web/src/components/ui/input-field.tsx (and the analogous class lists at the
other occurrences noted) to use the project's design-token class names used by
other ui primitives (replace border-radius, padding, font-size, spacing and
color utilities with the corresponding token classes), keep the conditional
hasError -> 'border-danger' behavior but swap the static 'border-border' and
color tokens to their tokenized equivalents, and ensure className is still
appended so callers can extend styles.

In `@web/src/components/ui/select-field.stories.tsx`:
- Around line 20-60: Add the missing canonical stories by exporting three new
Story objects: Hover, Loading, and Empty. Create Hover as a story that mirrors
Default but sets a prop or state to simulate hover (e.g., a className or
Storybook play interaction) so reviewers can see hover styling; create Loading
as a story that passes loading: true (and optionally clears value) to
SelectField to show the skeleton/spinner state; create Empty as a story that
passes options: [] and value: '' (and placeholder: 'No options' or similar) to
show the empty-state UI. Reference the existing exports (Default,
WithPlaceholder, WithError, Disabled, Interactive) and the InteractiveSelect
function to match patterns used in the file when adding these new exports.

In `@web/src/components/ui/toggle-field.stories.tsx`:
- Around line 50-53: The Storybook story `Interactive` declares `args` but its
`render` function ignores them, so controls don't work; update `Interactive`'s
`render` to accept the story `args` and pass them into `InteractiveToggle`
(e.g., have `render` accept an `args` parameter and render `<InteractiveToggle
{...args} />`) so the declared `args` (label, checked, onChange) actually
control the rendered component.
- Around line 17-53: The stories are missing the required state variants (hover,
loading, error, empty); add Story exports named Hover, Loading, Error, and Empty
alongside the existing Default/Checked/WithDescription/Disabled/Interactive and
implement them by passing appropriate args or using a render/play function for
the ToggleField (referencing ToggleField and InteractiveToggle) to simulate
those states—e.g., Hover should set any props or use storybook play to apply
hover pseudo-state, Loading should pass a loading prop or wrap the component to
show its loading UI, Error should pass an error prop/message, and Empty should
render the component with no label/description or an empty state prop—ensuring
each new Story uses the same Story type and follows the existing pattern of
args/render so the file contains default, hover, loading, error, and empty
variants.

In `@web/src/pages/setup/SetupSummary.tsx`:
- Around line 116-118: Replace the plain paragraph used when there are no
providers with the shared EmptyState component: import EmptyState from
"@/components/ui/empty-state" and swap the conditional block that checks
Object.keys(providers).length === 0 to render <EmptyState /> configured with an
appropriate icon, a title like "No providers configured", a short description
(e.g., "Add a provider to get started"), and an optional action button that
navigates to the provider setup flow; update any props or classNames to match
other EmptyState usages in the codebase (keep the conditional on
Object.keys(providers).length === 0 and remove the existing <p
className="text-sm text-muted-foreground">).
- Around line 13-26: Replace the custom SetupAgentRow markup with the shared
AgentCard component: remove usage of Avatar, StatPill and the manual div layout
in the SetupAgentRow function and instead render <AgentCard /> (imported from
"@/components/ui/agent-card") passing the agent object or its fields (name,
role, status, currentTask) as props; if department and tier are not supported by
AgentCard, pass them via the component's extra props/children or extend
AgentCard usage to display them consistently. Keep the component signature
SetupAgentRow({ agent }: { agent: SetupAgentSummary }) and ensure you import
AgentCard and stop duplicating presentation logic so the shared UI building
block drives avatar, name, role, status and current task rendering.

In `@web/src/pages/setup/TemplateCompareDrawer.tsx`:
- Around line 69-75: COMPARISON_ROWS is missing the required "autonomy" and
"communication-pattern" dimensions for side-by-side template diffs; update the
COMPARISON_ROWS array to include two additional ComparisonRow entries (e.g.,
with label 'Autonomy' and label 'Communication Pattern') and implement their
getValue callbacks to return the template's autonomy and communication-pattern
fields (similar style to existing entries that call deriveCategory and
estimateAgentCount) so differences appear in the TemplateCompareDrawer UI;
reference the COMPARISON_ROWS constant and ensure field names used match the
template object (e.g., t.autonomy and t.communication_pattern or
t.communicationPattern).
- Around line 113-121: The Tags branch currently treats any array (including
empty arrays) as truthy and renders nothing; update the conditional in
TemplateCompareDrawer.tsx so that when row.label === 'Tags' and
Array.isArray(value) you also check value.length > 0 before mapping to StatPill,
and fall back to display || '--' when the array is empty; reference the
row.label === 'Tags' check, the value array, and the StatPill rendering to
locate and adjust the logic.

In `@web/src/pages/setup/TemplateVariables.tsx`:
- Around line 29-58: The JSX inside variables.map in TemplateVariables.tsx
contains branching UI logic and should be extracted into a small presenter
component (e.g., TemplateVariableControl) to keep the map callback simple;
create a new functional component TemplateVariableControl that accepts props
(variable: TemplateVariable, value, onChange) and implements the existing branch
logic for bool (ToggleField), int/float (SliderField) and default null, then
replace the inline map body with variables.map(v => <TemplateVariableControl
key={v.name} variable={v} value={values[v.name] ?? v.default}
onChange={onChange} />) so the map remains data wiring only and the rendering
logic lives in the new component.

In `@web/src/stores/setup-wizard.ts`:
- Around line 334-352: In submitCompany validate required fields before calling
createCompany: trim companyName and if it is empty set companyError (e.g.,
"Company name is required") and set companyLoading to false and return early;
likewise if selectedTemplate is null/empty set companyError (e.g., "Template
selection is required"), set companyLoading to false and return early; update
the handlers in submitCompany so these guards run before calling createCompany
and avoid making the API request when inputs are invalid.

---

Duplicate comments:
In `@README.md`:
- Line 110: Update the README’s setup wizard description to note the conditional
initial admin account creation step: mention that on fresh installs, if the
needsAdmin flag is true the wizard first prompts to create an admin account
(Account → Template → Company → Agents → Providers → Theme → Complete),
otherwise it proceeds directly to template selection; reference the "setup
wizard" and the "needsAdmin" condition in the text so readers understand the
alternate flow.

In `@web/src/__tests__/stores/setup-wizard.test.ts`:
- Around line 236-267: Add unit tests covering provider actions: write tests for
createProviderFromPreset (including the error path to ensure it re-throws and
state is unchanged), probeAllPresets (happy/error paths verifying returned probe
results and state changes), and testProviderConnection (success and failure
behaviors). In the test file where agent tests live (setup-wizard.test.ts)
import and mock the API functions createProviderFromPreset, probeAllPresets, and
testProviderConnection, use useSetupWizardStore.setState to seed initial
providers, call the corresponding store methods (createProviderFromPreset,
probeAllPresets, testProviderConnection) and assert expected state mutations and
that errors are propagated when mocks reject. Ensure each test verifies both the
API invocation parameters and resulting store state or thrown error.

In `@web/src/__tests__/utils/template-categories.test.ts`:
- Around line 85-87: The test builds expectedOrder from the SUT output (result)
which can hide regressions; change it to derive expectedOrder from the input
templates instead: use CATEGORY_ORDER but filter by whether any item in the
original templates array maps to that category (i.e. check templates for
presence of that category) rather than checking result.has(c). Update the line
that computes expectedOrder in the test (near variables CATEGORY_ORDER,
templates, result, keys, and function categorizeTemplates) so expectedOrder
reflects the input data, then assert keys === expectedOrder.

In `@web/src/api/types.ts`:
- Around line 1114-1120: The TemplateVariable interface currently uses a generic
var_type string and lacks per-variable bounds/enums; change it to a
discriminated union in TemplateVariable (exported from web/src/api/types.ts)
with specific variants for types like { var_type: "int"; min?: number; max?:
number }, { var_type: "float"; min?: number; max?: number; step?: number }, {
var_type: "string"; minLength?: number; maxLength?: number }, and { var_type:
"enum"; options: string[] } (including shared fields name, description, default,
required), then remove the duplicate TemplateVariable definition from
TemplateVariables.tsx and import the updated TemplateVariable from `@/api/types`
so the TemplateVariables component can read per-variable bounds and enums
instead of hardcoding ranges.

In `@web/src/components/ui/input-field.stories.tsx`:
- Around line 16-42: Add the missing canonical stories: create Hover, Loading
and Empty exports alongside the existing Default, Required, WithError, WithHint,
Disabled, Multiline, and Password stories; for Hover, reuse Default args and
either add a play() that calls userEvent.hover on the input or apply a story
decorator that forces the :hover pseudo-state so the hover visuals are visible;
for Loading, supply the component's loading prop (e.g., isLoading/loading) and
args that show a spinner/disabled input and a relevant placeholder to
demonstrate the loading UI; for Empty, supply args with an empty value (value:
'') and appropriate placeholder/hint to show the empty state; name the new
exports Hover, Loading, and Empty so they appear in Storybook.

In `@web/src/components/ui/input-field.tsx`:
- Around line 10-24: InputField’s ref handling is broken because it’s a plain
function component that declares a ref prop; change it to use React.forwardRef
so JSX refs work: convert the export function InputField(...) to export const
InputField = React.forwardRef<HTMLInputElement | HTMLTextAreaElement,
InputFieldProps>(function InputField(props, ref) { ... }), remove/refactor the
explicit ref?: fields if necessary to accept the forwarded ref type (ensure
InputProps and TextareaProps use React.Ref<HTMLInputElement> |
React.Ref<HTMLTextAreaElement> types only if still needed), update the parameter
destructuring to take ref as the second arg (e.g., ({ label, error, hint,
multiline, className, ...props }, ref)), and set InputField.displayName =
'InputField' for nicer DevTools names; ensure internal uses of ref forward it to
the underlying <input> or <textarea> element depending on multiline.

In `@web/src/components/ui/select-field.tsx`:
- Around line 41-45: The SelectField component uses hardcoded utility classes
(e.g., the wrapper div, label element, required span, and other occurrences in
the select/input and helper text) instead of project design tokens; update
SelectField to replace fixed sizing/spacing/typography Tailwind utilities with
the project’s token class names (use the tokenized equivalents for container
spacing, label typography, required indicator spacing/color, select sizing, and
helper text) so all className values in the SelectField component (including the
wrapper div, label, required span, select/input, and helper text) use the
design-token class patterns used across the UI foundation.

In `@web/src/components/ui/slider-field.stories.tsx`:
- Around line 17-54: Add the missing canonical story states for SliderField by
creating new Story exports named Hover, Loading, Error, and Empty alongside the
existing Default/WithFormat/Disabled/Interactive; each should provide
appropriate args or a render to simulate the state (e.g., Hover with a
focused/hover prop or storybook play to hover, Loading with a loading prop or
skeleton state, Error with an error message prop, Empty with value
undefined/null) so the SliderField component and InteractiveSlider usage are
covered; reference the existing Story type and props (label, value, min, max,
step, formatValue, onChange, disabled) when constructing these stories to mirror
the pattern used by Default/WithFormat/Disabled/Interactive.

In `@web/src/pages/setup/AccountStep.tsx`:
- Around line 60-114: Wrap the input fields and submit Button in a semantic
<form> with an onSubmit handler that calls the existing handleSubmit (ensure you
prevent default inside handleSubmit or call e.preventDefault() in the new
onSubmit wrapper), change the Button to type="submit" so Enter triggers
submission, and keep existing disabled={loading} and validation/error props
(references: handleSubmit, password, confirmPassword, username, Button,
InputField) so behavior and validation remain unchanged.

In `@web/src/pages/setup/ProviderAddForm.tsx`:
- Around line 60-67: The card-with-header in ProviderAddForm is built manually;
replace the outer div and internal header block with the shared SectionCard
component to avoid duplication. Import SectionCard from
"@/components/ui/section-card" and wrap the form contents with <SectionCard
title="Add Cloud Provider" subtitle="Connect a cloud LLM provider with your API
key."> (or use the component's icon/action slots if needed), removing the
duplicated classes on the current div and header; keep the inner form markup
(fields/buttons) unchanged and ensure ProviderAddForm uses SectionCard's props
for title/subtitle/icon/actions instead of custom header markup.
- Around line 26-39: handleTest currently only sends providerName to onTest
which doesn't validate the full draft config and lets Create Provider submit
incomplete providers; update handleTest to pass the entire draft config (not
just providerName) to onTest (e.g., pass draftConfig or the form state used to
build providers) and ensure setTestResult/setError handle the returned
validation result; additionally add validation in the create/submit handler (the
function that creates providers) to reject or block submission when required
fields like the provider key are missing (perform the same full-config
validation before calling the create API and surface errors via setError).
- Around line 26-39: The testResult state is not cleared when inputs change or
when starting a new test, causing stale results to show; update ProviderAddForm
to call setTestResult(null) whenever relevant inputs change (e.g., in onChange
handlers for preset/name/key fields) and at the start of handleTest before
calling onTest (add setTestResult(null) just after setTesting(true) and
setError(null)). Also apply the same clear-before-retest change for the other
test handlers referenced (the blocks around the other test functions) so every
test run and every input change resets testResult via setTestResult(null).

In `@web/src/pages/setup/ProviderProbeResults.tsx`:
- Around line 62-80: The probe results UI in ProviderProbeResults.tsx is using a
custom div wrapper instead of the shared SectionCard; replace the hand-rolled
card (the outer div that contains the header and maps localPresets to
ProbeResultItem) with the SectionCard component from
"@/components/ui/section-card", pass the title ("Auto-detected Providers" /
"Detecting local providers..." using probing) and any icon/action slots as
needed, keep the mapped children (localPresets.map -> ProbeResultItem with props
preset, result, probing, onAddPreset) intact, and add the SectionCard import at
the top of the file.
- Around line 16-23: Replace the ad-hoc icons (Loader2/Check/X) in
ProviderProbeResults.tsx with the shared StatusBadge component: import
StatusBadge from "@/components/ui/status-badge" and render it instead of the
inline icons, mapping the current state variables (probing, result, detected) to
the appropriate StatusBadge state (e.g., loading when probing && !result,
success when detected, error/neutral otherwise). Remove the Loader2/Check/X JSX
and ensure the surrounding className/layout is preserved while passing the
correct status/variant props to StatusBadge.

In `@web/src/pages/setup/ProvidersStep.tsx`:
- Line 55: Rename the ref variable probeAttempted to probeAttemptedRef to follow
the project's naming convention for useRef variables; update the declaration
(const probeAttempted = useRef(false)) to const probeAttemptedRef =
useRef(false) and replace all subsequent usages of probeAttempted (e.g., checks
and assignments like probeAttempted.current = true and if
(probeAttempted.current) ...) with probeAttemptedRef so the code consistently
references the renamed ref.

In `@web/src/pages/setup/SetupSummary.tsx`:
- Around line 98-100: The current mapping in SetupSummary uses the array index
for keys which can break identity: change the key on the SetupAgentRow rendered
in the agents.map to a stable unique identifier from each agent (e.g., agent.id,
agent.uuid, or another immutable property such as agent.email or name) instead
of `index`; if agents may lack an id, add/derive a stable unique field on the
agent objects before rendering and use that property as the key so SetupAgentRow
preserves identity across reorderings.

In `@web/src/pages/setup/SkipWizardForm.tsx`:
- Around line 27-40: The current code reads errors from
useSetupWizardStore.getState() after awaiting submitCompany() and
wizardCompleteSetup(), which is fragile; change submitCompany() and
wizardCompleteSetup() to return their error (string|null) or throw on failure,
then update this component to capture those return values (e.g., const
companyErr = await submitCompany()) and call setError(companyErr) if present
instead of reading companyError/completionError from the store; keep
setCompanyNameStore and setError usage the same but stop relying on synchronous
store state changes after the awaits.

In `@web/src/pages/setup/TemplateCard.tsx`:
- Around line 31-36: The selected state in TemplateCard.tsx currently injects a
hardcoded arbitrary shadow string inside the className (the ternary branch that
adds 'border-accent
shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)]'); replace
that hardcoded shadow with the design token (e.g., use the existing token class
like 'shadow-card-hover' or a new semantic token such as 'shadow-card-selected'
that maps to var(--so-shadow-card-selected)/var(--so-shadow-card-hover)); update
the ternary to only reference the token class (keep 'border-accent' as-is) and
if introducing a new token add the corresponding CSS variable in the theme file
(e.g., --so-shadow-card-selected) so the component uses tokenized shadows
instead of an arbitrary value.

In `@web/src/pages/setup/TemplateCostBadge.tsx`:
- Around line 4-14: The currency prop in TemplateCostBadgeProps and its default
in TemplateCostBadge should use the CurrencyCode type and DEFAULT_CURRENCY
constant from currencies.ts for compile-time safety; update the interface to
declare currency?: CurrencyCode and change the default value in the
TemplateCostBadge parameter list to DEFAULT_CURRENCY (importing CurrencyCode and
DEFAULT_CURRENCY), ensuring references to monthlyCost and className remain
unchanged.

In `@web/src/pages/setup/TemplateVariables.tsx`:
- Around line 22-28: The component currently builds a manual card wrapper (the
outer div with class "space-y-4 rounded-lg border border-border bg-card p-4" and
the inner header block with h3/p) instead of using the shared SectionCard;
replace that outer wrapper and header/body markup with the SectionCard component
imported from "@/components/ui/section-card", moving the title ("Template
Variables") into SectionCard's title slot/prop and the descriptive paragraph and
the rest of the content into its body; preserve any styling and functionality
(icons/actions slots if needed) and remove the duplicated card/header markup so
TemplateVariables uses SectionCard as the single wrapper component.

In `@web/src/pages/setup/ThemePreview.tsx`:
- Around line 31-41: Replace the manual agent card markup in ThemePreview (the
div block that uses Avatar and StatusBadge) with the shared AgentCard component
from "@/components/ui/agent-card": import AgentCard at the top, remove the
manual markup (the div container, Avatar, name span, StatusBadge, and role span)
and render <AgentCard> with appropriate props (e.g., name="Akira Tanaka",
role="CEO, executive", status="idle" and size or variant if supported) so the
avatar, name, role, status and current task are provided by the single source
component and design consistency is preserved.

In `@web/src/pages/setup/WizardShell.tsx`:
- Around line 93-104: The "Skip wizard (advanced)" button in WizardShell
currently navigates to /setup/complete and triggers CompleteStep which expects
full wizard data; change the skip flow to render SkipWizardForm instead by
updating the navigate target or complete-step render logic: either (A) modify
the onClick in WizardShell's Button (the navigate(...) call) to route to a
distinct skip path (e.g., /setup/skip) that you add and map to SkipWizardForm,
or (B) append a query flag (e.g., ?skip=true) in the navigate call and update
CompleteStep's rendering logic to detect that flag (or a store flag) and render
SkipWizardForm when skip mode is present; reference WizardShell, CompleteStep,
SkipWizardForm, and the navigate(...) call when making the change.

In `@web/src/utils/template-categories.ts`:
- Around line 9-76: getTemplateCategory currently maps tags by looking up
CATEGORY_LABELS[tag] then reversing via LABEL_TO_CATEGORY, which ties grouping
to display text; add a direct mapping (e.g., TAG_TO_CATEGORY) from tag key →
canonical category and update getTemplateCategory to use that map (falling back
to 'other') instead of round-tripping through CATEGORY_LABELS and
LABEL_TO_CATEGORY; keep CATEGORY_LABELS and LABEL_TO_CATEGORY for UI label
lookups but replace the lookup in getTemplateCategory to reference
TAG_TO_CATEGORY to decouple grouping from presentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

Comment on lines +30 to +37
const inputClasses = cn(
'w-full rounded-md border bg-surface px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent',
'disabled:opacity-60 disabled:cursor-not-allowed',
hasError ? 'border-danger' : 'border-border',
className,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use project design tokens for spacing/typography in this shared primitive.

This component still relies on fixed utility sizing/spacing classes; please align these class names to the same tokenized patterns used across other web/src/components/ui/* primitives.

As per coding guidelines: “When creating new shared React components… Use design tokens exclusively.”

Also applies to: 40-43, 67-70

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

In `@web/src/components/ui/input-field.tsx` around lines 30 - 37, The input
primitive builds its class list with hardcoded Tailwind utility sizes (e.g.,
'rounded-md', 'px-3', 'py-2', 'text-sm') which violates the design-token rule;
update the inputClasses construction in web/src/components/ui/input-field.tsx
(and the analogous class lists at the other occurrences noted) to use the
project's design-token class names used by other ui primitives (replace
border-radius, padding, font-size, spacing and color utilities with the
corresponding token classes), keep the conditional hasError -> 'border-danger'
behavior but swap the static 'border-border' and color tokens to their tokenized
equivalents, and ensure className is still appended so callers can extend
styles.

Comment on lines +17 to +53
export const Default: Story = {
args: { label: 'Enable feature', checked: false, onChange: () => {} },
}

export const Checked: Story = {
args: { label: 'Enable feature', checked: true, onChange: () => {} },
}

export const WithDescription: Story = {
args: {
label: 'Set a budget limit',
description: 'Budget enforcement prevents agents from exceeding this limit.',
checked: false,
onChange: () => {},
},
}

export const Disabled: Story = {
args: { label: 'Enable feature', checked: true, disabled: true, onChange: () => {} },
}

function InteractiveToggle() {
const [checked, setChecked] = useState(false)
return (
<ToggleField
label="Set a budget limit"
description="Budget enforcement prevents agents from exceeding this limit."
checked={checked}
onChange={setChecked}
/>
)
}

export const Interactive: Story = {
args: { label: 'Budget limit', checked: false, onChange: () => {} },
render: () => <InteractiveToggle />,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add the required Storybook state variants for this shared component.

This story set is missing the required state coverage (hover, loading, error, empty) defined for new shared UI components.

As per coding guidelines, “When creating new shared React components ... Create .stories.tsx file with all 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/toggle-field.stories.tsx` around lines 17 - 53, The
stories are missing the required state variants (hover, loading, error, empty);
add Story exports named Hover, Loading, Error, and Empty alongside the existing
Default/Checked/WithDescription/Disabled/Interactive and implement them by
passing appropriate args or using a render/play function for the ToggleField
(referencing ToggleField and InteractiveToggle) to simulate those states—e.g.,
Hover should set any props or use storybook play to apply hover pseudo-state,
Loading should pass a loading prop or wrap the component to show its loading UI,
Error should pass an error prop/message, and Empty should render the component
with no label/description or an empty state prop—ensuring each new Story uses
the same Story type and follows the existing pattern of args/render so the file
contains default, hover, loading, error, and empty variants.

Comment on lines +50 to +53
export const Interactive: Story = {
args: { label: 'Budget limit', checked: false, onChange: () => {} },
render: () => <InteractiveToggle />,
}
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

Interactive declares args, but render ignores them.

Right now Storybook controls don’t affect this story because render always returns <InteractiveToggle /> with hardcoded props.

♻️ Proposed fix
-function InteractiveToggle() {
-  const [checked, setChecked] = useState(false)
+function InteractiveToggle({
+  initialChecked,
+  label,
+  description,
+}: {
+  initialChecked: boolean
+  label: string
+  description: string
+}) {
+  const [checked, setChecked] = useState(initialChecked)
   return (
     <ToggleField
-      label="Set a budget limit"
-      description="Budget enforcement prevents agents from exceeding this limit."
+      label={label}
+      description={description}
       checked={checked}
       onChange={setChecked}
     />
   )
 }
 
 export const Interactive: Story = {
-  args: { label: 'Budget limit', checked: false, onChange: () => {} },
-  render: () => <InteractiveToggle />,
+  args: {
+    label: 'Set a budget limit',
+    description: 'Budget enforcement prevents agents from exceeding this limit.',
+    checked: false,
+  },
+  render: (args) => (
+    <InteractiveToggle
+      initialChecked={args.checked}
+      label={args.label}
+      description={args.description ?? ''}
+    />
+  ),
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/toggle-field.stories.tsx` around lines 50 - 53, The
Storybook story `Interactive` declares `args` but its `render` function ignores
them, so controls don't work; update `Interactive`'s `render` to accept the
story `args` and pass them into `InteractiveToggle` (e.g., have `render` accept
an `args` parameter and render `<InteractiveToggle {...args} />`) so the
declared `args` (label, checked, onChange) actually control the rendered
component.

Comment on lines +13 to +26
function SetupAgentRow({ agent }: { agent: SetupAgentSummary }) {
return (
<div className="flex items-center gap-3 rounded-md border border-border p-2">
<Avatar name={agent.name} size="sm" />
<div className="flex-1">
<span className="text-sm font-medium text-foreground">{agent.name}</span>
<span className="ml-2 text-xs text-muted-foreground">{agent.role}</span>
</div>
<div className="flex items-center gap-2">
<StatPill label="Dept" value={agent.department} />
<StatPill label="Tier" value={agent.tier} />
</div>
</div>
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use shared AgentCard instead of a custom agent row.

This duplicates agent presentation logic that should come from the shared UI building block.

As per coding guidelines: "Use AgentCard component from @/components/ui/agent-card for agent display: avatar, name, role, status, current task" and "ALWAYS reuse existing components from web/src/components/ui/ before creating new ones."

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

In `@web/src/pages/setup/SetupSummary.tsx` around lines 13 - 26, Replace the
custom SetupAgentRow markup with the shared AgentCard component: remove usage of
Avatar, StatPill and the manual div layout in the SetupAgentRow function and
instead render <AgentCard /> (imported from "@/components/ui/agent-card")
passing the agent object or its fields (name, role, status, currentTask) as
props; if department and tier are not supported by AgentCard, pass them via the
component's extra props/children or extend AgentCard usage to display them
consistently. Keep the component signature SetupAgentRow({ agent }: { agent:
SetupAgentSummary }) and ensure you import AgentCard and stop duplicating
presentation logic so the shared UI building block drives avatar, name, role,
status and current task rendering.

Comment on lines +116 to +118
{Object.keys(providers).length === 0 && (
<p className="text-sm text-muted-foreground">No providers configured yet.</p>
)}
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

Use EmptyState for the no-provider placeholder.

A plain paragraph bypasses the shared no-data pattern used across the app.

As per coding guidelines: "Use EmptyState component from @/components/ui/empty-state for no-data/no-results placeholders with icon, title, description, optional action button."

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

In `@web/src/pages/setup/SetupSummary.tsx` around lines 116 - 118, Replace the
plain paragraph used when there are no providers with the shared EmptyState
component: import EmptyState from "@/components/ui/empty-state" and swap the
conditional block that checks Object.keys(providers).length === 0 to render
<EmptyState /> configured with an appropriate icon, a title like "No providers
configured", a short description (e.g., "Add a provider to get started"), and an
optional action button that navigates to the provider setup flow; update any
props or classNames to match other EmptyState usages in the codebase (keep the
conditional on Object.keys(providers).length === 0 and remove the existing <p
className="text-sm text-muted-foreground">).

Comment on lines +69 to +75
const COMPARISON_ROWS: readonly ComparisonRow[] = [
{ label: 'Category', getValue: (t) => deriveCategory(t) },
{ label: 'Estimated Agents', getValue: (t) => String(estimateAgentCount(t)) },
{ label: 'Source', getValue: (t) => t.source },
{ label: 'Tags', getValue: (t) => t.tags },
{ label: 'Skill Patterns', getValue: (t) => t.skill_patterns.map((sp) => String(sp)) },
]
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

Comparison matrix is missing required dimensions from the linked objective.

COMPARISON_ROWS omits autonomy and communication-pattern comparisons, which are explicitly required for template side-by-side diffing. Please add both rows so differences are visible in the drawer.

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

In `@web/src/pages/setup/TemplateCompareDrawer.tsx` around lines 69 - 75,
COMPARISON_ROWS is missing the required "autonomy" and "communication-pattern"
dimensions for side-by-side template diffs; update the COMPARISON_ROWS array to
include two additional ComparisonRow entries (e.g., with label 'Autonomy' and
label 'Communication Pattern') and implement their getValue callbacks to return
the template's autonomy and communication-pattern fields (similar style to
existing entries that call deriveCategory and estimateAgentCount) so differences
appear in the TemplateCompareDrawer UI; reference the COMPARISON_ROWS constant
and ensure field names used match the template object (e.g., t.autonomy and
t.communication_pattern or t.communicationPattern).

Comment on lines +29 to +58
{variables.map((v) => {
const currentValue = values[v.name] ?? v.default
if (v.var_type === 'bool') {
return (
<ToggleField
key={v.name}
label={v.description || v.name}
checked={currentValue === true}
onChange={(checked) => onChange(v.name, checked)}
/>
)
}
if (v.var_type === 'int' || v.var_type === 'float') {
const numValue = typeof currentValue === 'number' ? currentValue : Number(currentValue) || 0
return (
<SliderField
key={v.name}
label={v.description || v.name}
value={numValue}
min={v.var_type === 'int' ? 1 : 0}
max={v.var_type === 'int' ? 50 : 1000}
step={v.var_type === 'int' ? 1 : 10}
formatValue={undefined}
onChange={(val) => onChange(v.name, val)}
/>
)
}
// String and other types: not rendered as slider/toggle
return null
})}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract variable control rendering out of the .map() callback.

The callback now carries branching UI logic; move it into a small presenter (e.g., TemplateVariableControl) and keep the map as data wiring only.

As per coding guidelines: "Do NOT create complex (>8 line) JSX inside .map() in React -- extract to a shared component."

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

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 29 - 58, The JSX
inside variables.map in TemplateVariables.tsx contains branching UI logic and
should be extracted into a small presenter component (e.g.,
TemplateVariableControl) to keep the map callback simple; create a new
functional component TemplateVariableControl that accepts props (variable:
TemplateVariable, value, onChange) and implements the existing branch logic for
bool (ToggleField), int/float (SliderField) and default null, then replace the
inline map body with variables.map(v => <TemplateVariableControl key={v.name}
variable={v} value={values[v.name] ?? v.default} onChange={onChange} />) so the
map remains data wiring only and the rendering logic lives in the new component.

Comment on lines +334 to +352
async submitCompany() {
const { companyName, companyDescription, selectedTemplate } = get()
set({ companyLoading: true, companyError: null })
try {
const response = await createCompany({
company_name: companyName.trim(),
description: companyDescription.trim() || null,
template_name: selectedTemplate,
})
set({
companyResponse: response,
agents: [...response.agents],
companyLoading: false,
})
} catch (err) {
console.error('setup-wizard: submitCompany failed:', err)
set({ companyError: getErrorMessage(err), companyLoading: 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.

🧹 Nitpick | 🔵 Trivial

Consider validating required fields before API call.

submitCompany doesn't validate companyName or selectedTemplate before making the API call. If companyName.trim() is empty or selectedTemplate is null, this may cause avoidable API errors. Consider adding guards or early returns.

💡 Proposed guard
   async submitCompany() {
     const { companyName, companyDescription, selectedTemplate } = get()
+    if (!companyName.trim() || !selectedTemplate) {
+      set({ companyError: 'Company name and template are required' })
+      return
+    }
     set({ companyLoading: true, companyError: null })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/setup-wizard.ts` around lines 334 - 352, In submitCompany
validate required fields before calling createCompany: trim companyName and if
it is empty set companyError (e.g., "Company name is required") and set
companyLoading to false and return early; likewise if selectedTemplate is
null/empty set companyError (e.g., "Template selection is required"), set
companyLoading to false and return early; update the handlers in submitCompany
so these guards run before calling createCompany and avoid making the API
request when inputs are invalid.

Aureliolo and others added 5 commits March 27, 2026 17:05
…and theme customization

7-step wizard flow: Account (conditional) -> Template -> Company -> Agents ->
Providers -> Theme -> Complete. Includes template comparison drawer, cost
estimation, currency selector (EUR default), budget cap, mini SVG org chart,
auto-detect local providers, 5-axis theme customization with live preview,
and skip-wizard quick setup for power users.

New shared components: InputField, SelectField, SliderField, ToggleField,
Drawer. 134 new tests (unit + property-based with fast-check). All 736
tests pass, type-check clean, lint clean.

Closes #238, closes #726, closes #728

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- All 11 store catch blocks now use getErrorMessage + console.error
- Fix infinite re-render in ProvidersStep (object ref in useEffect deps)
- Fix SkipWizardForm silent error swallowing (check store error after await)
- Fix WizardShell invalid URL step fallback (redirect to first step)
- Add usedFallback indicator to CostEstimate + surface in UI
- Extract getPasswordStrength to utility with tests
- Separate presetsError/presetsLoading from providersError
- Clear agentsError before mutations, use .map() for immutable updates
- Add aria-label to template compare checkboxes
- Fix CompleteStep dialog not closing on error
- Update CLAUDE.md, brand-and-ux.md, page-structure.md, README.md

Pre-reviewed by 5 agents, 21 findings addressed

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

- Store: re-throw in createProviderFromPreset, add error handling to
  testProviderConnection, parallelize probeAllPresets with Promise.allSettled
- Drawer: implement focus trap, replace hardcoded styles with design tokens,
  add type="button" on close button
- ProviderAddForm: capture error details with getErrorMessage, use cn()
- ToggleField: replace hardcoded pixel translation, fix disabled label cursor
- TemplateCard: fix invalid CSS shadow syntax with color-mix()
- TemplateVariables: remove hardcoded $ prefix (EUR default)
- AgentModelPicker: split on first :: only to handle model IDs with ::
- AgentsStep: add error guard to useEffect, show error state before empty state
- ProvidersStep: display presetsError, add error/loading guards to useEffects
- TemplateStep: add error guard to useEffect, extract tag constants
- CompanyStep: pass template variables from selected template, wire validation
  to button disabled, add model tier profile selector
- CostEstimatePanel: persist budget cap default, add per-agent breakdown
- TemplateCompareDrawer: add category/agent count rows, difference highlighting,
  type key as keyof, null-safe tags rendering
- WizardShell: replace raw button with Button component, add type guard
- WizardProgress: extract StepIndicator component, simplify onClick
- ProviderProbeResults: extract ProbeResultItem component
- MiniOrgChart: extract AgentNode, move title into circle, stable keys
- InputField: discriminated union for multiline/input type safety
- SelectField: fix aria-describedby when error replaces hint
- TemplateCard: add Recommended badge with provider-tier matching
- setup-validation: trim description, for..of loop, validation wiring
- cost-estimator: extract getTierCosts helper, eliminate non-null assertions
- template-categories: fix title-case fallback for composite keys
- password-strength: extract threshold constants
- AccountStep: extract MIN_PASSWORD_LENGTH constant
- Stories: add a11y test config, import shared currencies
- Tests: fix silent pass in drawer overlay, explicit assertions in
  WizardProgress, precise error assertion in store test, vendor name fix,
  property tests for password strength, regex fix for 1-char names
- Docs: update user_guide, organization, page-structure, brand-and-ux

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TemplateStep: fix tier count math bug (solo templates double-counted)
- ProviderAddForm: preserve apiKey on test failure instead of clearing
- AgentsStep/MiniOrgChart: use compound keys to prevent collisions on edit
- CostEstimatePanel: stable keys via agentIndex, extract AgentRow component
- select-field: pass required/aria-required to native select element
- WizardNavigation: guard against indexOf returning -1
- completeSetup: re-throw errors matching createProviderFromPreset pattern
- currencies.ts: remove UI layer import, use inline type
- ProvidersStep: auth-type-aware StatusBadge, extract ProviderRow component
- ThemePreview: expose all 5 theme axes as data attributes
- WizardSkeleton: change hardcoded 6 to 7 (max step count)
- TemplateCompareDrawer: extract ComparisonRowEntry component
- Drawer: save and restore focus to opener element on close
- slider-field.test: add onChange handler test
- SetupSummary: add budgetCapEnabled prop, gate budget row display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- select-field: omit aria-required when not required (use || undefined)
- CompleteStep: wrap wizardCompleteSetup in try/catch since store now
  re-throws errors (prevented unhandled throw skipping error display)
- Extract getProviderStatus to shared util (was duplicated in
  ProvidersStep and SetupSummary)
- currencies.ts: derive CurrencyCode union type for compile-time safety
- ProvidersStep: gate ProviderProbeResults/ProviderAddForm on presets
  loading/error state with skeleton and retry button
- CostEstimatePanel: extract DepartmentGroupSection component
- SetupSummary: extract SetupAgentRow component with stable keys

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aureliolo and others added 5 commits March 27, 2026 17:05
- currencies.ts: use 'as const satisfies' to preserve literal types
  so CurrencyCode is a union of ISO codes, not string
- ProvidersStep: add probeAttempted ref to prevent infinite probe
  loop when all probes fail and probeResultsCount stays 0
- Install @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities to
  fix TaskBoardPage type-check and test failures from main

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- canNavigateTo: return false for steps not in stepOrder (index -1
  was incorrectly treated as <= 0 which returns true)
- provider-status: replace if-chain with exhaustive switch on AuthType
  with never-assertion default for compile-time safety
- Drawer: handle Tab when panel itself is focused (active element
  outside focusable children now wraps correctly)
- setup-validation property test: simplify regex to avoid backtracking
  (cleared ESLint security/detect-unsafe-regex warning)
- ProviderProbeResults: type probeResults as Partial<Record> since
  failed probes produce sparse entries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Company name generator: use fc.string().filter() instead of regex
  to test the full domain (special chars, unicode) matching actual
  validateCompanyStep behavior
- Template/response generator: use fc.boolean() instead of
  fc.oneof(null, string) for clarity since only null-vs-present matters

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

- AgentModelPicker: produce empty string when provider or modelId is
  missing instead of "::modelId" or "provider::"
- WizardNavigation: add explicit type="button" on Back and Next buttons
  to prevent form submission if wrapped in a form context
- testProviderConnection: clear providersError before retry so stale
  errors don't persist after a successful test
- getCategoryLabel: O(1) direct map lookup via CATEGORY_TO_LABEL
  instead of O(n) reverse scan over LABEL_TO_CATEGORY entries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- docs/design/organization.md: include typography in the 5-axis theme
  description (was listing only 4 axes)
- TemplateCompareDrawer: render '--' fallback when tags array is empty
  instead of a blank cell

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aureliolo Aureliolo force-pushed the feat/setup-wizard-rebuild branch from 34e20d7 to 4760dac Compare March 27, 2026 16:06
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 16:07 — with GitHub Actions Inactive
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: 8

♻️ Duplicate comments (32)
web/src/utils/cost-estimator.ts (1)

73-74: 🧹 Nitpick | 🔵 Trivial

Consider validating dailyTokensPerAgent and inputOutputRatio bounds.

Negative values or ratios outside [0, 1] would produce incorrect or negative cost estimates. A defensive clamp or validation at function entry would prevent silent miscalculations.

♻️ Proposed validation
 export function estimateMonthlyCost(
   agents: readonly AgentCostInput[],
   models: readonly ProviderModelConfig[],
   options?: CostEstimateOptions,
 ): CostEstimate {
-  const dailyTokens = options?.dailyTokensPerAgent ?? DEFAULT_DAILY_TOKENS_PER_AGENT
-  const inputRatio = options?.inputOutputRatio ?? DEFAULT_INPUT_OUTPUT_RATIO
+  const dailyTokens = Math.max(0, options?.dailyTokensPerAgent ?? DEFAULT_DAILY_TOKENS_PER_AGENT)
+  const inputRatio = Math.max(0, Math.min(1, options?.inputOutputRatio ?? DEFAULT_INPUT_OUTPUT_RATIO))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/utils/cost-estimator.ts` around lines 73 - 74, Validate and clamp the
parsed inputs right after they are read: ensure dailyTokensPerAgent is
non-negative (e.g., dailyTokensPerAgent = Math.max(0, dailyTokensPerAgent)) and
ensure inputOutputRatio is constrained to [0,1] (e.g., inputOutputRatio =
Math.min(1, Math.max(0, inputOutputRatio))). Update the variables referenced in
this file (dailyTokensPerAgent, inputOutputRatio,
DEFAULT_DAILY_TOKENS_PER_AGENT, DEFAULT_INPUT_OUTPUT_RATIO) at function entry so
subsequent cost calculations never see negative tokens or out-of-range ratios
and add/update tests or a clear error/log if you prefer strict validation
instead of clamping.
web/src/api/types.ts (1)

1114-1120: ⚠️ Potential issue | 🟠 Major

TemplateVariable lacks machine-readable bounds for sliders and enum options.

The interface uses a free-form var_type: string which cannot safely describe slider bounds (min, max, step) or enum choices. The TemplateVariables.tsx component checks for 'bool', 'int', 'float' but has no metadata for valid ranges, which could lead to:

  • Sliders with unbounded or default ranges that don't match the template's actual constraints
  • No way to render model_tier_profile (economy/balanced/premium) as a dropdown without hardcoding

Consider extending to a discriminated union or adding optional fields:

export interface TemplateVariable {
  readonly name: string
  readonly description: string
  readonly var_type: 'string' | 'int' | 'float' | 'bool' | 'enum'
  readonly default: string | number | boolean | null
  readonly required: boolean
  // Numeric bounds (for int/float)
  readonly min?: number
  readonly max?: number
  readonly step?: number
  // Enum options (for enum type)
  readonly options?: readonly string[]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/api/types.ts` around lines 1114 - 1120, Update the TemplateVariable
shape to make var_type a discriminant and include machine-readable metadata for
sliders and enums: replace the free-form var_type string in the TemplateVariable
interface with a union of allowed types (e.g.,
'string'|'int'|'float'|'bool'|'enum') and add optional numeric bounds (min, max,
step) and an options array for enums; then adjust TemplateVariables.tsx to read
min/max/step when rendering int/float sliders and to render enum dropdowns from
options instead of hardcoding values, using TemplateVariable as the single
source of truth.
README.md (1)

110-110: ⚠️ Potential issue | 🟡 Minor

Mention the conditional account step here.

The setup wizard flow description omits the account creation step that occurs on fresh installs when needsAdmin is true. The documented flow in page-structure.md shows: account (conditional) → template → company → agents → providers → theme → complete.

Consider updating to: "...on a fresh install, the setup wizard may first prompt for admin account creation, then guides you through choosing a company template..."

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

In `@README.md` at line 110, Update the README's setup-wizard description to
mention the conditional admin account step: change the sentence that starts "on
a fresh install, the **setup wizard** guides you..." to note that when
needsAdmin is true the wizard may first prompt for admin/account creation before
proceeding to template → company → agents → providers → theme → complete;
reference the conditional flag name needsAdmin and the page-structure.md flow as
the canonical order when editing the sentence.
web/src/pages/setup/TemplateCostBadge.tsx (1)

4-14: 🧹 Nitpick | 🔵 Trivial

Use shared CurrencyCode type and DEFAULT_CURRENCY constant for type safety.

The currency prop uses a raw string type, which allows invalid currency codes at compile time. The project has a CurrencyCode type and DEFAULT_CURRENCY constant in @/utils/currencies that should be used here.

♻️ Proposed fix
 import { cn } from '@/lib/utils'
 import { formatCurrency } from '@/utils/format'
+import { DEFAULT_CURRENCY, type CurrencyCode } from '@/utils/currencies'

 export interface TemplateCostBadgeProps {
   monthlyCost: number
-  currency?: string
+  currency?: CurrencyCode
   className?: string
 }

 export function TemplateCostBadge({
   monthlyCost,
-  currency = 'EUR',
+  currency = DEFAULT_CURRENCY,
   className,
 }: TemplateCostBadgeProps) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/TemplateCostBadge.tsx` around lines 4 - 14, Update
TemplateCostBadgeProps and the TemplateCostBadge component to use the shared
CurrencyCode type and DEFAULT_CURRENCY constant: replace the currency prop's
type from string to CurrencyCode and import DEFAULT_CURRENCY from
"@/utils/currencies", then set the default currency value in the component
signature to DEFAULT_CURRENCY (keep monthlyCost and className unchanged). Ensure
imports for CurrencyCode and DEFAULT_CURRENCY are added and any references to
the currency prop throughout TemplateCostBadge remain compatible with the new
CurrencyCode type.
web/src/components/ui/toggle-field.stories.tsx (2)

50-53: 🧹 Nitpick | 🔵 Trivial

Interactive story declares args but render ignores them.

The args object defines label, checked, and onChange, but the render function returns <InteractiveToggle /> with hardcoded props, making Storybook controls non-functional for this story.

♻️ Proposed fix
-function InteractiveToggle() {
-  const [checked, setChecked] = useState(false)
+function InteractiveToggle({
+  initialChecked = false,
+  label,
+  description,
+}: {
+  initialChecked?: boolean
+  label: string
+  description?: string
+}) {
+  const [checked, setChecked] = useState(initialChecked)
   return (
     <ToggleField
-      label="Set a budget limit"
-      description="Budget enforcement prevents agents from exceeding this limit."
+      label={label}
+      description={description}
       checked={checked}
       onChange={setChecked}
     />
   )
 }

 export const Interactive: Story = {
-  args: { label: 'Budget limit', checked: false, onChange: () => {} },
-  render: () => <InteractiveToggle />,
+  args: {
+    label: 'Set a budget limit',
+    description: 'Budget enforcement prevents agents from exceeding this limit.',
+    checked: false,
+    onChange: () => {},
+  },
+  render: (args) => (
+    <InteractiveToggle
+      initialChecked={args.checked}
+      label={args.label}
+      description={args.description}
+    />
+  ),
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/toggle-field.stories.tsx` around lines 50 - 53, The
Interactive story defines args (label, checked, onChange) but its render
function ignores them; update the Story object so render receives and forwards
story args to InteractiveToggle (use the args parameter in the render function
and pass label, checked, onChange to the InteractiveToggle component) so
Storybook controls affect the component (reference: Interactive story, args,
render, InteractiveToggle).

17-36: 🛠️ Refactor suggestion | 🟠 Major

Add the required Storybook state variants.

The coding guidelines require stories for all states: default, hover, loading, error, empty. This file has Default, Checked, WithDescription, Disabled, and Interactive, but is missing the required hover, loading, error, and empty variants.

If ToggleField doesn't support loading/error props, consider whether those states should be added to the component, or document why they're not applicable.

As per coding guidelines: "When creating new shared React components ... Create .stories.tsx file with all 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/toggle-field.stories.tsx` around lines 17 - 36, The
story file is missing required state variants (hover, loading, error, empty);
add Story exports named Hover, Loading, Error, and Empty alongside Default,
Checked, WithDescription, Disabled (and Interactive if present) that pass the
appropriate props to ToggleField (e.g., for Hover simulate hovered state via a
decorator or arg that the Storybook docs use, for Loading and Error
add/loading/error props if ToggleField supports them, and for Empty pass an
empty or null label/description), or if ToggleField does not support
loading/error states, update the ToggleField component to accept and render
loading/error props (e.g., isLoading, error) and then add the corresponding
stories that exercise those states so the file contains Default, Hover, Loading,
Error, Empty, Checked, WithDescription, Disabled, Interactive as required by the
guidelines.
web/src/__tests__/utils/template-categories.test.ts (1)

75-88: ⚠️ Potential issue | 🟡 Minor

Derive expected order from fixture, not from the result.

The test computes expectedOrder from result.has(c), which uses the function under test to determine the expected outcome. If categorizeTemplates erroneously drops a category, this test would still pass. Build the expected set from the fixture templates instead.

🔧 Proposed fix
   it('returns categories in CATEGORY_ORDER', () => {
     const templates = [
       makeTemplate({ name: 'r', tags: ['research'] }),
       makeTemplate({ name: 's', tags: ['startup'] }),
       makeTemplate({ name: 'd', tags: ['dev-shop'] }),
       makeTemplate({ name: 'e', tags: ['enterprise'] }),
     ]
     const result = categorizeTemplates(templates)
     const keys = [...result.keys()]

-    // Verify order matches CATEGORY_ORDER for present categories
-    const expectedOrder = CATEGORY_ORDER.filter((c) => result.has(c))
+    // Expected categories based on fixture template tags
+    const fixtureCategories = ['research', 'startup', 'dev-shop', 'enterprise']
+    const expectedOrder = CATEGORY_ORDER.filter((c) => fixtureCategories.includes(c))
     expect(keys).toEqual(expectedOrder)
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/utils/template-categories.test.ts` around lines 75 - 88,
The test uses the function under test (result.has) to compute expectedOrder,
which masks regressions; change the test so expectedOrder is derived from the
fixture instead: inspect the templates array (created via makeTemplate) to
collect the set of present category keys from their tags (or map tags to
category keys), then compute expectedOrder = CATEGORY_ORDER.filter(c =>
templatesContainCategory(c)) and assert keys === expectedOrder; update the test
that references categorizeTemplates, CATEGORY_ORDER, makeTemplate, templates,
and expectedOrder accordingly.
web/src/pages/setup/ThemePreview.tsx (1)

31-41: 🛠️ Refactor suggestion | 🟠 Major

Reuse AgentCard for the mock agent row.

This block is duplicating the design-system agent presentation inline. The first time AgentCard styling changes, this preview will drift. As per coding guidelines: "Use AgentCard component from @/components/ui/agent-card for agent display: avatar, name, role, status, current task."

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

In `@web/src/pages/setup/ThemePreview.tsx` around lines 31 - 41, The ThemePreview
component contains an inline mock agent UI that duplicates Avatar, StatusBadge
and text instead of using the shared AgentCard component; replace the inline
block inside ThemePreview with the AgentCard from "@/components/ui/agent-card",
passing the same props (name "Akira Tanaka", role/title "CEO, executive", status
"idle" and any size props) so the preview reuses AgentCard's avatar, name, role
and status rendering and stays consistent when AgentCard styling changes.
web/src/components/ui/slider-field.stories.tsx (1)

17-54: 🛠️ Refactor suggestion | 🟠 Major

Complete the canonical state coverage for SliderField.

Default, WithFormat, Disabled, and Interactive are useful, but the required Hover, Loading, Error, and Empty stories are still missing for this shared component. If one of those states is only expressed by a wrapper, document it with a wrapper story rather than skipping it. As per coding guidelines: "When creating new shared React components, place in web/src/components/ui/ with descriptive kebab-case filename. Create .stories.tsx file with all 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/slider-field.stories.tsx` around lines 17 - 54, Add the
missing canonical stories for SliderField by creating and exporting Hover,
Loading, Error, and Empty Story objects in this file alongside
Default/WithFormat/Disabled/Interactive; implement Hover as a story that renders
<SliderField ... /> inside a wrapper that simulates :hover (or documents the
wrapper usage), add Loading to pass a loading prop or wrapper to SliderField to
show skeleton/spinner, add Error to pass an error message/prop (or error state)
to SliderField to display validation feedback, and add Empty to render the
component with no value/placeholder state; reference the existing SliderField
component, the InteractiveSlider helper, and the existing story exports
(Default, WithFormat, Disabled, Interactive) so the new stories follow the same
args/render pattern and are exported from the file.
docs/user_guide.md (1)

76-76: ⚠️ Potential issue | 🟡 Minor

Theme-step docs still describe only four customization axes.

The Theme step now also exposes typography, and sidebar mode is more accurate than sidebar position for rail/collapsible/hidden/compact options.

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

In `@docs/user_guide.md` at line 76, Update the Theme step description to reflect
the current customization axes: replace "sidebar position" with "sidebar mode"
and add "typography" to the list so it reads something like: Theme -- set UI
preferences including color palette, layout density, animation level,
typography, and sidebar mode (rail/collapsible/hidden/compact); update the
"Theme" text in docs/user_guide.md accordingly.
web/src/pages/setup/ThemeStep.tsx (1)

55-78: ⚠️ Potential issue | 🟠 Major

Expose the typography axis in the Theme step.

themeSettings and ThemePreview already include typography, but this screen only renders controls for palette, density, animation, and sidebar. Users currently cannot configure the fifth theme axis promised by this rebuild.

Also applies to: 99-129

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

In `@web/src/pages/setup/ThemeStep.tsx` around lines 55 - 78, The ThemeStep UI is
missing controls for the typography axis even though themeSettings and
ThemePreview support typography; add a TYPOGRAPHY_OPTIONS array (matching shape
of PALETTE_OPTIONS/DENSITY_OPTIONS etc.) and render a corresponding control in
the ThemeStep component so the typography selection is persisted to
themeSettings and passed to ThemePreview; update any handlers that read/write
selected values (the same functions managing palette/density/animation/sidebar)
to include the typography key so changes are stored and previewed correctly.
web/src/pages/setup/SkipWizardForm.tsx (1)

56-84: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this titled container.

The title/description block plus the custom bordered wrapper is reimplementing a shared section pattern inline. Swapping this to SectionCard keeps the setup pages consistent and avoids one-off card styling drift. As per coding guidelines: "Do NOT build card-with-header layouts from scratch in React -- use <SectionCard> component."

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

In `@web/src/pages/setup/SkipWizardForm.tsx` around lines 56 - 84, Replace the
manual title/description and custom bordered wrapper in SkipWizardForm with the
shared SectionCard component: remove the outer div that renders the h2/p and the
inner rounded border container and instead render <SectionCard title="Quick
Setup" description="Skip the wizard and configure everything later in
Settings."> and place the existing InputField (bound to
companyName/setCompanyName), the error alert block, and the Button (calling
handleSubmit, using loading/disabled state) as children of SectionCard so visual
behavior and bindings remain unchanged.
web/src/components/ui/input-field.stories.tsx (1)

16-42: 🛠️ Refactor suggestion | 🟠 Major

Add the missing canonical stories for InputField.

This file covers default/error/disabled variants, but the shared-component contract still requires explicit Hover, Loading, and Empty stories. If Default is intended to be the empty state, give it a value and add a separate empty-state story. As per coding guidelines: "When creating new shared React components, place in web/src/components/ui/ with descriptive kebab-case filename. Create .stories.tsx file with all 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/input-field.stories.tsx` around lines 16 - 42, Add the
missing canonical stories: create explicit Hover, Loading, and Empty Story
objects alongside the existing
Default/Required/WithError/WithHint/Disabled/Multiline/Password stories; if
Default should represent a non-empty field, give Default a value (e.g., value:
'Acme Corp') and add Empty with no value/placeholder to represent the empty
state; implement Loading by setting the component's loading prop (e.g., loading
or isLoading) in the Loading story args and implement Hover by adding a Hover
story that simulates the hover state via Storybook parameters/controls/play
function (or story-level pseudo-state settings) so the shared-component contract
(default, hover, loading, error, empty) is satisfied.
web/src/pages/setup/WizardShell.tsx (1)

93-99: ⚠️ Potential issue | 🔴 Critical

Skip flow still routes into the normal guarded wizard.

This button only changes the URL to /setup/complete; it never switches the shell into a skip-specific path. From account or template, that means the route either gets bounced back by canNavigateTo() or lands in the full completion step without the required wizard state, so the advertised advanced shortcut is still broken.

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

In `@web/src/pages/setup/WizardShell.tsx` around lines 93 - 99, The skip button in
WizardShell currently only calls navigate(`/setup/complete`) from the
account/template steps, so canNavigateTo still blocks or the wizard lacks
required state; update the Button's onClick to perform the skip flow setup
before navigating — e.g., call a new handler (handleSkip or similar) that sets
the wizard state (mark onboarding as skipped or set currentStep to 'complete' /
set a skipped flag) or pass a skip indicator in navigation state/query that
canNavigateTo recognizes, then call navigate; reference the Button onClick,
navigate, currentStep and canNavigateTo in your changes.
web/src/utils/template-categories.ts (1)

9-23: 🛠️ Refactor suggestion | 🟠 Major

Keep canonical category resolution independent from display labels.

getTemplateCategory() still resolves tag -> display label -> canonical key. Any label-only copy change here silently starts classifying matching templates as "other". A direct TAG_TO_CATEGORY map removes that coupling and gets rid of the duplicated reverse maps.

♻️ Suggested direction
-const CATEGORY_LABELS: Readonly<Record<string, string>> = {
-  startup: 'Startup',
-  solo: 'Startup',
-  'small-team': 'Startup',
-  mvp: 'Startup',
-  'dev-shop': 'Development',
-  'data-team': 'Development',
-  product: 'Product',
-  agile: 'Product',
-  enterprise: 'Enterprise',
-  'full-company': 'Enterprise',
-  consultancy: 'Professional Services',
-  agency: 'Professional Services',
-  research: 'Research',
-}
+const TAG_TO_CATEGORY: Readonly<Record<string, string>> = {
+  startup: 'startup',
+  solo: 'startup',
+  'small-team': 'startup',
+  mvp: 'startup',
+  'dev-shop': 'dev-shop',
+  'data-team': 'dev-shop',
+  product: 'product',
+  agile: 'product',
+  enterprise: 'enterprise',
+  'full-company': 'enterprise',
+  consultancy: 'consultancy',
+  agency: 'consultancy',
+  research: 'research',
+}
@@
 function getTemplateCategory(template: TemplateInfoResponse): string {
   for (const tag of template.tags) {
-    const label = CATEGORY_LABELS[tag]
-    if (label) {
-      return LABEL_TO_CATEGORY[label] ?? 'other'
-    }
+    const category = TAG_TO_CATEGORY[tag]
+    if (category) return category
   }
   return 'other'
 }

Also applies to: 29-51, 68-73

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

In `@web/src/utils/template-categories.ts` around lines 9 - 23, The code currently
couples display labels to canonical category resolution via CATEGORY_LABELS
which causes label edits to break classification; create a separate
TAG_TO_CATEGORY mapping that maps each tag (e.g., 'startup', 'solo', 'mvp',
'dev-shop', etc.) directly to a canonical category key (e.g., 'startup',
'development', 'product', 'enterprise', 'professional-services', 'research'),
keep CATEGORY_LABELS only for display names, and update getTemplateCategory() to
consult TAG_TO_CATEGORY first (falling back to 'other') instead of deriving
category from display labels; also remove any duplicated reverse maps that
reconstruct tag->category from labels to avoid future coupling.
web/src/pages/setup/TemplateVariables.tsx (3)

29-58: 🧹 Nitpick | 🔵 Trivial

Extract the variable control rendering into a dedicated component.

The .map() callback contains >8 lines of branching JSX logic. Extract this into a TemplateVariableControl component to keep the map as data wiring only.

As per coding guidelines: "Do NOT create complex (>8 line) JSX inside .map() in React -- extract to a shared component."

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

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 29 - 58, Extract the
JSX branching inside the variables.map in TemplateVariables.tsx into a new
presentational component named TemplateVariableControl that accepts props
{variable, value, onChange}; move the bool branch to render ToggleField and the
int/float branch to render SliderField inside that component (preserving prop
names like label, checked/value, min/max/step and calling
onChange(variable.name, ...)). Replace the inline map body with a simple
variables.map(v => <TemplateVariableControl key={v.name} variable={v}
value={values[v.name] ?? v.default} onChange={onChange} />) so the map only
wires data and the complex JSX lives in TemplateVariableControl.

48-50: ⚠️ Potential issue | 🟡 Minor

Slider bounds are hardcoded and ignore template-specific constraints.

The fixed min/max/step values (1/50/1 for int, 0/1000/10 for float) don't respect any template-provided constraints. If a template variable specifies a valid range (e.g., team size 3-15), these bounds won't enforce it.

Consider extending the TemplateVariable interface to include optional min, max, and step properties, then use them with fallbacks.

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

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 48 - 50, The slider
bounds currently hardcode min/max/step based only on v.var_type; update the
TemplateVariable type/interface to add optional numeric properties min, max, and
step, then change the Slider props (where min={...} max={...} step={...}) to
prefer v.min, v.max, v.step respectively with sensible fallbacks based on
v.var_type (e.g., for ints default to 1/50/1 and for floats default to
0/1000/10) so template-provided constraints are honored; adjust any consuming
code (e.g., where TemplateVariable is constructed or validated) to accept the
new optional fields.

21-28: 🛠️ Refactor suggestion | 🟠 Major

Reuse SectionCard for this card-with-header wrapper.

This hand-rolled bordered card with header/description pattern should use the shared SectionCard component for consistent spacing and theming.

As per coding guidelines: "Do NOT build card-with-header layouts from scratch in React -- use <SectionCard> component."

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

In `@web/src/pages/setup/TemplateVariables.tsx` around lines 21 - 28, Replace the
hand-rolled card wrapper in the TemplateVariables component with the shared
SectionCard component: remove the outer div with classes "space-y-4 rounded-lg
border border-border bg-card p-4" and the inner header block, and instead wrap
the header and description content inside <SectionCard> (or pass
title/description props if SectionCard supports them). Update imports to include
SectionCard and remove any redundant classNames used for spacing/theming so the
component relies on SectionCard for consistent layout and styling.
web/src/pages/setup/TemplateCard.tsx (1)

31-36: 🛠️ Refactor suggestion | 🟠 Major

Replace the arbitrary shadow value with a design token.

Line 34 uses shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)] which bypasses the project's shadow token system. Use the shared shadow token variable instead.

As per coding guidelines: "Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders. NEVER hardcode shadow or border values."

♻️ Proposed fix
       className={cn(
         'flex flex-col gap-3 rounded-lg border bg-card p-4 transition-colors',
-        selected ? 'border-accent shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)]' : 'border-border',
+        selected ? 'border-accent shadow-card-hover' : 'border-border',
         'hover:bg-card-hover',
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/TemplateCard.tsx` around lines 31 - 36, The component's
className in TemplateCard uses a hardcoded arbitrary shadow token
`shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)]`; replace
that with the shared design token (e.g. `var(--so-shadow-card-hover)`) so the
selected branch uses the project shadow token rather than a hardcoded
value—update the ternary that builds className (the block referencing selected)
to include the shadow token class name instead of the arbitrary shadow string
and ensure it follows the same pattern as other token classes like
`border-border`.
web/src/pages/setup/AccountStep.tsx (1)

60-114: ⚠️ Potential issue | 🟠 Major

Wrap inputs in a semantic <form> for keyboard submission.

The current implementation uses a <div> with onClick on the button (line 111), which means pressing Enter in an input field won't submit the form. This hurts keyboard accessibility and expected form behavior.

♻️ Proposed fix
-      <div className="space-y-4 rounded-lg border border-border bg-card p-6">
+      <form
+        className="space-y-4 rounded-lg border border-border bg-card p-6"
+        onSubmit={(e) => {
+          e.preventDefault()
+          void handleSubmit()
+        }}
+      >
         <InputField
           label="Username"
           ...
         />
         ...
-        <Button onClick={handleSubmit} disabled={loading} className="w-full">
+        <Button type="submit" disabled={loading} className="w-full">
           {loading ? 'Creating Account...' : 'Create Account'}
         </Button>
-      </div>
+      </form>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/AccountStep.tsx` around lines 60 - 114, Replace the outer
non-semantic div with a proper <form> in AccountStep.tsx so Enter key submits;
hook the form's onSubmit to the existing handleSubmit (or update handleSubmit to
accept an event and call event.preventDefault()), change the Button to
type="submit" instead of relying on onClick, and keep the InputField, error
display and loading logic as-is so keyboard users can submit and accessibility
is preserved.
web/src/pages/setup/SetupSummary.tsx (1)

98-100: ⚠️ Potential issue | 🟡 Minor

Use a stable key instead of array index.

Line 99 uses key={\agent-${index}`}which can cause reconciliation issues. IfSetupAgentSummarygains anid` field, prefer that. Otherwise, use a composite key combining stable agent properties.

♻️ Suggested fix
           {agents.map((agent, index) => (
-            <SetupAgentRow key={`agent-${index}`} agent={agent} />
+            <SetupAgentRow 
+              key={agent.id ?? `${agent.name}-${agent.role}-${agent.department}-${index}`} 
+              agent={agent} 
+            />
           ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/SetupSummary.tsx` around lines 98 - 100, The map in
SetupSummary rendering SetupAgentRow uses an unstable key `agent-${index}`;
change it to a stable identifier by using a unique property on the agent (e.g.,
`agent.id`) or, if `id` is not available, a composite of stable fields (e.g.,
`agent.name` + `agent.type`) as the key in the SetupAgentRow component render to
avoid reconciliation issues; update the JSX where `agents.map((agent, index) =>
<SetupAgentRow key={...} agent={agent} />)` is defined to use the stable key
and, if adding `id`, ensure the agent type/interface and any upstream data that
constructs agents expose that `id`.
web/src/pages/setup/CompanyStep.tsx (1)

61-71: ⚠️ Potential issue | 🟠 Major

Hardcoded empty array forces fallback pricing.

Line 69 passes [] for provider models to estimateMonthlyCost(), which means the cost estimate will always use fallback pricing and usedFallback will be permanently true. After users configure providers in a later step, revisiting this step still shows inaccurate estimates.

Extract provider models from the store and include them in the dependencies:

♻️ Suggested fix
  const costEstimate = useMemo(() => {
    if (agents.length === 0) return null
    return estimateMonthlyCost(
      agents.map((a) => ({
        model_provider: a.model_provider,
        model_id: a.model_id,
        tier: a.tier,
      })),
-      [], // No provider models yet
+      Object.values(providers).flatMap((p) => [...p.models]),
     )
-  }, [agents])
+  }, [agents, providers])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/CompanyStep.tsx` around lines 61 - 71, The costEstimate
memo currently passes a hardcoded empty array to estimateMonthlyCost causing
fallback pricing; update the memo to pull the current provider models from the
store (e.g. use the existing provider/models selector or prop such as
providerModels or providersFromStore) and pass that array instead of [] into
estimateMonthlyCost, and add that provider models variable to the useMemo
dependency array so the estimate updates when providers are configured; modify
the const costEstimate block and its dependency list accordingly (referencing
costEstimate, estimateMonthlyCost, agents, and the store selector/variable you
use for provider models).
web/src/pages/setup/ProvidersStep.tsx (1)

55-55: ⚠️ Potential issue | 🟡 Minor

Rename probeAttempted to probeAttemptedRef.

The ref variable violates the naming convention used throughout the codebase. All useRef variables should end with Ref (e.g., validatedRef, disposedRef).

🛠️ Proposed fix
-  const probeAttempted = useRef(false)
+  const probeAttemptedRef = useRef(false)

And update usages at lines 73-74:

-    if (presets.length > 0 && probeResultsCount === 0 && !probing && !probeAttempted.current) {
-      probeAttempted.current = true
+    if (presets.length > 0 && probeResultsCount === 0 && !probing && !probeAttemptedRef.current) {
+      probeAttemptedRef.current = true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/ProvidersStep.tsx` at line 55, Rename the ref variable
probeAttempted to probeAttemptedRef to follow the codebase naming convention for
useRef variables; update its declaration (const probeAttempted = useRef(false))
to use the new identifier and then replace all usages of probeAttempted (e.g.,
reads/writes like probeAttempted.current) with probeAttemptedRef throughout the
ProvidersStep component so references remain correct.
web/src/pages/setup/ProviderAddForm.tsx (3)

76-81: ⚠️ Potential issue | 🟡 Minor

Stale testResult persists after draft changes.

When the user changes the preset, provider name, or API key, the previous testResult remains displayed. This can mislead users into thinking their new configuration was tested successfully.

🛠️ Proposed fix: Clear testResult on input changes
         onChange={(val) => {
           setSelectedPreset(val)
+          setTestResult(null)
           if (!providerName) {
             setProviderName(val)
           }
         }}

Apply similar changes to the providerName and apiKey onChange handlers.

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

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 76 - 81, The UI keeps
showing a stale testResult after users change inputs; update the onChange
handlers for setSelectedPreset (the existing preset handler), setProviderName
(provider name input), and setApiKey (API key input) to also clear the
testResult state (e.g., call the testResult setter with a neutral value)
whenever those inputs change so previous test outcomes are removed immediately;
ensure you reference testResult's setter alongside setSelectedPreset,
setProviderName, and setApiKey in each relevant onChange handler.

60-67: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this card-with-header layout.

This hand-rolled card structure should use the shared SectionCard component for consistency. As per coding guidelines: "do NOT build card-with-header layouts from scratch -- use <SectionCard>".

♻️ Proposed refactor
+import { SectionCard } from '@/components/ui/section-card'
...
   return (
-    <div className="space-y-4 rounded-lg border border-border bg-card p-4">
-      <div className="space-y-1">
-        <h3 className="text-sm font-semibold text-foreground">Add Cloud Provider</h3>
-        <p className="text-xs text-muted-foreground">
-          Connect a cloud LLM provider with your API key.
-        </p>
-      </div>
+    <SectionCard
+      title="Add Cloud Provider"
+      description="Connect a cloud LLM provider with your API key."
+    >
       <SelectField ... />
       ...
-    </div>
+    </SectionCard>
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 60 - 67,
ProviderAddForm currently renders a custom card div for the header block instead
of using the shared SectionCard; replace the outer wrapper and header markup in
ProviderAddForm with the SectionCard component (use SectionCard's title/heading
and description props or its standard children pattern) so the structure and
styling are consistent, remove duplicated spacing/border/bg classes applied to
the hand-rolled div, and keep the existing inner form fields as SectionCard
children to preserve functionality.

26-39: ⚠️ Potential issue | 🟠 Major

Test Connection doesn't validate the draft credentials.

handleTest() calls onTest(providerName.trim()) which tests an existing provider by name, but the user is entering new credentials (preset + API key) that haven't been saved yet. The test button won't validate whether the API key they just typed is correct.

Consider updating the onTest prop signature to accept the draft config, or disable the test button until after provider creation.

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

In `@web/src/pages/setup/ProviderAddForm.tsx` around lines 26 - 39, handleTest
currently only passes providerName.trim() to onTest so it tests saved providers,
not the draft credentials the user typed; update the implementation and prop
contract so onTest accepts the draft config (e.g., pass both providerName.trim()
and the draft credential object/preset/API key from state) and then call onTest
with those draft values inside handleTest (or alternatively disable the Test
button until after creation if you prefer not to change the prop); update the
onTest prop type/signature and any callers to accept the new payload and use the
draft values when validating the connection (refer to handleTest and onTest to
locate where to change).
web/src/stores/setup-wizard.ts (3)

462-480: ⚠️ Potential issue | 🟠 Major

Probe failures are indistinguishable from "not found" results.

Rejected probes are logged but dropped from probeResults. The UI (ProviderProbeResults) renders a missing entry the same as a genuine miss, so network/backend failures appear as "provider not available" rather than errors.

Consider storing a per-preset failure sentinel so the UI can render errors separately:

🛠️ Proposed approach
   async probeAllPresets() {
     const { presets } = get()
     set({ probing: true })
     const entries = await Promise.allSettled(
       presets.map(async (preset) => {
         const result = await probePreset(preset.name)
         return [preset.name, result] as const
       }),
     )
-    const results: Record<string, ProbePresetResponse> = {}
+    const results: Record<string, ProbePresetResponse | { status: 'error'; error: string }> = {}
     for (const entry of entries) {
       if (entry.status === 'fulfilled') {
         results[entry.value[0]] = entry.value[1]
       } else {
         console.error('setup-wizard: probe failed for preset:', entry.reason)
+        results[entry.value?.[0] ?? 'unknown'] = { 
+          status: 'error', 
+          error: getErrorMessage(entry.reason) 
+        }
       }
     }
     set({ probeResults: results, probing: false })
   },

Note: The type of probeResults would need to be updated to include the error sentinel.

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

In `@web/src/stores/setup-wizard.ts` around lines 462 - 480, The probeAllPresets
function drops rejected probe results so the UI (ProviderProbeResults) cannot
distinguish network/backend errors from genuine "not found" misses; update
probeAllPresets to record a per-preset failure sentinel into the probeResults
map when an entry from Promise.allSettled is rejected (use the preset name from
the rejected entry) instead of only logging and dropping it, and update the
probeResults type to allow a failure sentinel value so ProviderProbeResults can
render errors separately; reference probeAllPresets, probePreset, probeResults,
and ProviderProbeResults when making the type and state changes.

334-352: 🧹 Nitpick | 🔵 Trivial

Consider validating required fields before API call.

submitCompany doesn't validate companyName or selectedTemplate before making the API call. If these are empty/null, the API will fail, which could be caught earlier with client-side validation.

💡 Proposed guard
   async submitCompany() {
     const { companyName, companyDescription, selectedTemplate } = get()
+    if (!companyName.trim() || !selectedTemplate) {
+      set({ companyError: 'Company name and template are required' })
+      return
+    }
     set({ companyLoading: true, companyError: null })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/setup-wizard.ts` around lines 334 - 352, submitCompany
currently calls createCompany without validating required inputs; add
client-side guards inside submitCompany to check that companyName.trim() is
non-empty and selectedTemplate is set, and if validation fails set({
companyError: "..." , companyLoading: false }) and return early to avoid the API
call. Ensure you trim companyName once, validate selectedTemplate, and keep the
existing error-setting pattern (use getErrorMessage or a specific message) so
agents/companyResponse aren't updated on invalid input.

383-406: ⚠️ Potential issue | 🟠 Major

Agent mutation failures are not re-thrown.

updateAgentName and randomizeAgentName catch errors, log them, and set agentsError, but don't re-throw. This prevents callers from rolling back inline edits or keeping UI state in sync when the API fails.

🐛 Proposed fix
   async updateAgentName(index, name) {
     set({ agentsError: null })
     try {
       const updated = await apiUpdateAgentName(index, { name })
       set((s) => ({
         agents: s.agents.map((a, i) => i === index ? updated : a),
       }))
     } catch (err) {
       console.error('setup-wizard: updateAgentName failed:', err)
       set({ agentsError: getErrorMessage(err) })
+      throw err
     }
   },

   async randomizeAgentName(index) {
     set({ agentsError: null })
     try {
       const updated = await apiRandomizeAgentName(index)
       set((s) => ({
         agents: s.agents.map((a, i) => i === index ? updated : a),
       }))
     } catch (err) {
       console.error('setup-wizard: randomizeAgentName failed:', err)
       set({ agentsError: getErrorMessage(err) })
+      throw err
     }
   },

Apply the same pattern to updateAgentModel (lines 367-381).

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

In `@web/src/stores/setup-wizard.ts` around lines 383 - 406, The catch blocks in
updateAgentName and randomizeAgentName swallow errors (they log and set
agentsError but do not re-throw), which prevents callers from reacting to
failures; update the catch handlers in those functions and in updateAgentModel
to re-throw the caught error after calling set({ agentsError:
getErrorMessage(err) }) and logging, so callers can perform rollback or UI
updates—i.e., inside the catch for updateAgentName, randomizeAgentName, and
updateAgentModel, add throw err (or re-throw the original error) as the final
statement after set(...) and console.error(...).
web/src/__tests__/stores/setup-wizard.test.ts (1)

236-267: 🧹 Nitpick | 🔵 Trivial

Consider adding tests for provider actions.

The test suite covers template, company, agent, and theme actions well, but provider-related actions (createProviderFromPreset, probeAllPresets, testProviderConnection) lack test coverage. Given the error handling and re-throw behavior in the store, testing both success and error paths would be valuable.

🧪 Example test additions
describe('provider actions', () => {
  it('creates provider from preset', async () => {
    const { createFromPreset } = await import('@/api/endpoints/providers')
    vi.mocked(createFromPreset).mockResolvedValue({
      name: 'my-openai',
      driver: 'openai',
      models: ['gpt-4'],
      auth_type: 'api_key',
      has_api_key: true,
    })

    await useSetupWizardStore.getState().createProviderFromPreset('openai', 'my-openai', 'sk-xxx')
    expect(useSetupWizardStore.getState().providers['my-openai']).toBeDefined()
  })

  it('re-throws on provider creation failure', async () => {
    const { createFromPreset } = await import('@/api/endpoints/providers')
    vi.mocked(createFromPreset).mockRejectedValue(new Error('Invalid API key'))

    await expect(
      useSetupWizardStore.getState().createProviderFromPreset('openai', 'my-openai', 'bad-key')
    ).rejects.toThrow('Invalid API key')
    expect(useSetupWizardStore.getState().providersError).toBe('Invalid API key')
  })
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/stores/setup-wizard.test.ts` around lines 236 - 267, Add
unit tests covering provider-related store actions: import the provider API
mocks (e.g., createFromPreset, probeAllPresets, testProviderConnection from
'@/api/endpoints/providers'), mock successful responses and failures, then
exercise the corresponding store methods on useSetupWizardStore
(createProviderFromPreset, probeAllPresets, testProviderConnection) asserting
success-state updates (e.g., providers['name'] exists, probe results stored) and
error handling (providersError set and errors re-thrown). Ensure you include
both a success case (mockResolvedValue) and an error case (mockRejectedValue)
for createProviderFromPreset and also tests for
probeAllPresets/testProviderConnection to validate their happy and failure
paths.
web/src/pages/setup/TemplateStep.tsx (1)

64-84: ⚠️ Potential issue | 🟠 Major

Recommended badge logic uses provider presence, not provider/model tiers.

When providerCount > 0, all enterprise/full-company templates become recommended regardless of what model tiers are actually available. This could mislead users who only have economy-tier or local models configured.

Consider deriving recommendations from provider model metadata, or suppressing enterprise recommendations until model tiers are known.

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

In `@web/src/pages/setup/TemplateStep.tsx` around lines 64 - 84, The current
recommendedTemplates use providerCount to decide recommending large templates;
update the logic in the recommendedTemplates useMemo to inspect provider
model-tier metadata instead of just Object.keys(providers).length: iterate
providers (from providers) and determine whether any configured provider exposes
enterprise/full-company tiers (e.g., check provider.tiers, provider.modelTiers,
or a similar field) and set a flag like hasEnterpriseSupport; then in the
templates loop use that flag (hasEnterpriseSupport) to decide whether to add
templates with TAG_ENTERPRISE/TAG_FULL_COMPANY, otherwise fall back to
recommending smallTags only; update references to templates, providers,
TAG_ENTERPRISE, TAG_FULL_COMPANY, TAG_SOLO, TAG_SMALL_TEAM accordingly.
web/src/pages/setup/CostEstimatePanel.tsx (1)

103-110: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this panel shell.

This is a card-with-header layout built from raw <div>s. As per coding guidelines: "do NOT build card-with-header layouts from scratch -- use <SectionCard>".

♻️ Proposed refactor
+import { SectionCard } from '@/components/ui/section-card'
...
   return (
-    <div className="space-y-4 rounded-lg border border-border bg-card p-4">
-      <div className="space-y-1">
-        <h3 className="text-sm font-semibold text-foreground">Cost Estimate</h3>
-        <p className="text-xs text-muted-foreground">
-          Based on {estimate.assumptions.dailyTokensPerAgent.toLocaleString()} tokens/agent/day
-        </p>
-      </div>
+    <SectionCard
+      title="Cost Estimate"
+      description={`Based on ${estimate.assumptions.dailyTokensPerAgent.toLocaleString()} tokens/agent/day`}
+    >
       <MetricCard ... />
       ...
-    </div>
+    </SectionCard>
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/setup/CostEstimatePanel.tsx` around lines 103 - 110, The
CostEstimatePanel is building a card-with-header using raw divs; replace the
outer wrapper with the shared SectionCard component instead. Import SectionCard
and wrap the existing contents so the panel header ("Cost Estimate" and the
assumptions paragraph) is passed as the card header (use SectionCard's
title/children/header slot per project convention) and keep the token display
and remaining children inside the SectionCard body; update references to
estimate.assumptions.dailyTokensPerAgent as before and remove the old rounded-lg
border/bg-card wrapper and its classNames.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 92742017-30cd-439c-bedf-ee28c60424ea

📥 Commits

Reviewing files that changed from the base of the PR and between 34e20d7 and 4760dac.

📒 Files selected for processing (63)
  • CLAUDE.md
  • README.md
  • docs/design/brand-and-ux.md
  • docs/design/organization.md
  • docs/design/page-structure.md
  • docs/user_guide.md
  • web/src/__tests__/components/ui/drawer.test.tsx
  • web/src/__tests__/components/ui/input-field.test.tsx
  • web/src/__tests__/components/ui/select-field.test.tsx
  • web/src/__tests__/components/ui/slider-field.test.tsx
  • web/src/__tests__/components/ui/toggle-field.test.tsx
  • web/src/__tests__/pages/setup/WizardProgress.test.tsx
  • web/src/__tests__/stores/setup-wizard.test.ts
  • web/src/__tests__/utils/cost-estimator.property.test.ts
  • web/src/__tests__/utils/cost-estimator.test.ts
  • web/src/__tests__/utils/password-strength.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/__tests__/utils/setup-validation.test.ts
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/api/types.ts
  • web/src/components/ui/drawer.stories.tsx
  • web/src/components/ui/drawer.tsx
  • web/src/components/ui/input-field.stories.tsx
  • web/src/components/ui/input-field.tsx
  • web/src/components/ui/select-field.stories.tsx
  • web/src/components/ui/select-field.tsx
  • web/src/components/ui/slider-field.stories.tsx
  • web/src/components/ui/slider-field.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/components/ui/toggle-field.tsx
  • web/src/pages/SetupPage.tsx
  • web/src/pages/setup/AccountStep.tsx
  • web/src/pages/setup/AgentModelPicker.tsx
  • web/src/pages/setup/AgentsStep.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/CostEstimatePanel.tsx
  • web/src/pages/setup/MiniOrgChart.tsx
  • web/src/pages/setup/ProviderAddForm.tsx
  • web/src/pages/setup/ProviderProbeResults.tsx
  • web/src/pages/setup/ProvidersStep.tsx
  • web/src/pages/setup/SetupAgentCard.tsx
  • web/src/pages/setup/SetupSummary.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/TemplateStep.tsx
  • web/src/pages/setup/TemplateVariables.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/ThemeStep.tsx
  • web/src/pages/setup/WizardNavigation.tsx
  • web/src/pages/setup/WizardProgress.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/WizardSkeleton.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/cost-estimator.ts
  • web/src/utils/currencies.ts
  • web/src/utils/password-strength.ts
  • web/src/utils/provider-status.ts
  • web/src/utils/setup-validation.ts
  • web/src/utils/template-categories.ts

Comment on lines +20 to +60
export const Default: Story = {
args: { label: 'Currency', options: currencies, value: 'EUR', onChange: () => {} },
}

export const WithPlaceholder: Story = {
args: {
label: 'Provider',
options: [
{ value: 'openai', label: 'OpenAI-compatible' },
{ value: 'ollama', label: 'Ollama (local)' },
],
value: '',
onChange: () => {},
placeholder: 'Select a provider...',
},
}

export const WithError: Story = {
args: {
label: 'Currency',
options: currencies,
value: '',
onChange: () => {},
error: 'Please select a currency',
required: true,
},
}

export const Disabled: Story = {
args: { label: 'Currency', options: currencies, value: 'EUR', onChange: () => {}, disabled: true },
}

function InteractiveSelect() {
const [value, setValue] = useState('EUR')
return <SelectField label="Currency" options={currencies} value={value} onChange={setValue} />
}

export const Interactive: Story = {
args: { label: 'Currency', options: currencies, value: 'EUR', onChange: () => {} },
render: () => <InteractiveSelect />,
}
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 Empty story for completeness.

The coding guidelines recommend stories for "default, hover, loading, error, empty" states. While SelectField doesn't have intrinsic loading/hover states (it's a native <select> wrapper), an Empty story with options: [] could document the edge case behavior.

♻️ Optional: Add Empty story
 export const Disabled: Story = {
   args: { label: 'Currency', options: currencies, value: 'EUR', onChange: () => {}, disabled: true },
 }

+export const Empty: Story = {
+  args: {
+    label: 'Currency',
+    options: [],
+    value: '',
+    onChange: () => {},
+    placeholder: 'No options available',
+  },
+}
+
 function InteractiveSelect() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/select-field.stories.tsx` around lines 20 - 60, Add an
"Empty" story to demonstrate the SelectField behavior with no options: create a
new exported story (e.g., export const Empty: Story) alongside
Default/WithPlaceholder/etc. that passes SelectField the unique props options:
[] and a suitable value (''), plus onChange and an optional placeholder or error
state to show intended UX; reference the SelectField component and the
InteractiveSelect helper if you want an interactive variant. Ensure the story
uses the same Story type and argument shape as the other stories (label,
options, value, onChange, placeholder/error as needed) so it appears in
Storybook and documents the empty-options edge case.

Comment on lines +115 to +128
<StaggerGroup className="space-y-3">
{agents.map((agent, index) => (
<StaggerItem key={`agent-${index}`}>
<SetupAgentCard
agent={agent}
index={index}
providers={providers}
onNameChange={handleNameChange}
onModelChange={handleModelChange}
onRandomizeName={handleRandomizeName}
/>
</StaggerItem>
))}
</StaggerGroup>
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

Index-based key persists despite static analysis warning.

Line 117 uses key={\agent-${index}`}which the linter flags. While agents don't currently have stable IDs, using index alone risks incorrect DOM reconciliation if the list is reordered or items are removed during inline editing. Consider adding anidfield toSetupAgentSummaryin@/api/types`, or use a composite key that includes more stable fields:

♻️ Suggested composite key
         {agents.map((agent, index) => (
-          <StaggerItem key={`agent-${index}`}>
+          <StaggerItem key={`${agent.name}-${agent.role}-${agent.department}-${index}`}>
             <SetupAgentCard
🧰 Tools
🪛 GitHub Check: Dashboard Lint

[warning] 117-117:
Do not use item index in the array as its key

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

In `@web/src/pages/setup/AgentsStep.tsx` around lines 115 - 128, The list uses
index-based keys in the agents.map render (key={`agent-${index}`}) which can
break reconciliation; update the data model (SetupAgentSummary in `@/api/types`)
to include a stable id for agents, populate that id when agents are created
(e.g., UUID), and change the key on the StaggerItem to use that stable id (e.g.,
key={agent.id}); alternatively, if you cannot change the type right now, use a
composite stable key derived from agent properties (e.g., name+model) in the
render and add a TODO to replace with a proper id in SetupAgentSummary and the
agent creation logic.

Comment on lines +28 to +41
setCompanyNameStore(trimmed)
await submitCompany()
const companyErr = useSetupWizardStore.getState().companyError
if (companyErr) {
setError(companyErr)
return
}
await wizardCompleteSetup()
const completionErr = useSetupWizardStore.getState().completionError
if (completionErr) {
setError(completionErr)
return
}
useSetupStore.setState({ setupComplete: true })
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
set -euo pipefail

echo "== SkipWizardForm flow =="
rg -n -C3 '\bsubmitCompany\b|\bcompleteSetup\b|companyError|completionError' web/src/pages/setup/SkipWizardForm.tsx

echo
echo "== Wizard store actions =="
fd 'setup-wizard' web/src -e ts -e tsx -x rg -n -C6 '\bsubmitCompany\b|\bcompleteSetup\b|companyError|completionError|agents|providers|setupComplete|skip' {}

echo
echo "== Completion validators and skip-mode hooks =="
rg -n -C4 'validate.*(agent|provider|complete)|at least one agent|at least one provider|skip wizard|skipWizard|setupComplete' web/src src/synthorg docs

Repository: Aureliolo/synthorg

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== completeSetup endpoint (full implementation) ==="
rg -n -A 50 'async def complete_setup' src/synthorg/api/controllers/setup.py 2>/dev/null | head -80

echo
echo "=== Check if setup completion validates providers ==="
rg -n 'has_providers|provider.*require|setupComplete.*bypass|skipWizard' src/synthorg/api/controllers/ docs/ 2>/dev/null | head -30

echo
echo "=== Check if submitCompany seeds default provider ==="
rg -n -B5 -A10 'def createCompany|async def create_company' src/synthorg/api/controllers/setup.py 2>/dev/null | head -40

echo
echo "=== Check SetupStatusResponse structure ==="
rg -n -A15 'class SetupStatusResponse' src/synthorg/api/controllers/setup_models.py 2>/dev/null

Repository: Aureliolo/synthorg

Length of output: 5584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for default/auto-create provider logic ==="
rg -n 'default.*provider|auto.*provider|seed.*provider|create.*default' src/synthorg/api/controllers/setup.py web/src/pages/setup/ 2>/dev/null | head -20

echo
echo "=== Check if skip wizard has bypass logic ==="
rg -n 'skip.*wizard|skipWizard|bypass.*provider|force.*complete' src/synthorg/api/controllers/setup.py src/synthorg/config/ 2>/dev/null | head -20

echo
echo "=== What actually populates providers in app_state? ==="
rg -n 'provider_registry|has_provider_registry' src/synthorg/ --type py | head -30

Repository: Aureliolo/synthorg

Length of output: 115


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full SkipWizardForm.tsx file ==="
cat web/src/pages/setup/SkipWizardForm.tsx

echo
echo "=== Where is SkipWizardForm used? ==="
rg -n 'SkipWizardForm' web/src --type ts --type tsx

echo
echo "=== Check setup wizard layout/page structure ==="
fd -e tsx -e ts 'setup.*wizard\|wizard.*setup' web/src/pages/setup/ -exec basename {} \;

Repository: Aureliolo/synthorg

Length of output: 3112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Where is SkipWizardForm used in routing? ==="
rg -n 'SkipWizardForm' web/src -A2 -B2 2>/dev/null

echo
echo "=== Setup page/route structure ==="
fd '\.tsx$' web/src/pages/setup/ -exec basename {} \;

echo
echo "=== Check if there's provider setup before SkipWizardForm ==="
fd -name '*.tsx' -path '*/setup/*' web/src/pages/ | xargs rg -l 'provider|Provider' 2>/dev/null

echo
echo "=== Read setup index/layout if it exists ==="
if [ -f web/src/pages/setup/index.tsx ]; then
  cat web/src/pages/setup/index.tsx
elif [ -f web/src/pages/setup/Setup.tsx ]; then
  head -100 web/src/pages/setup/Setup.tsx
fi

Repository: Aureliolo/synthorg

Length of output: 15770


Quick setup flow will fail at completion due to missing provider.

The backend's completeSetup() endpoint validates that a company, at least one agent, and at least one provider exist before allowing setup to finish (src/synthorg/api/controllers/setup.py:724-727). This quick path auto-populates agents via the template in submitCompany(), but provides no mechanism to create or configure a provider. The call to wizardCompleteSetup() will fail with "At least one provider must be configured before completing setup."

Either seed a default provider during submitCompany(), add an explicit provider configuration step to this form, or document that this is not a complete setup flow.

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

In `@web/src/pages/setup/SkipWizardForm.tsx` around lines 28 - 41, The quick-setup
path fails because wizardCompleteSetup() requires at least one provider but
submitCompany() only seeds agents; modify the flow to create a default provider
before calling wizardCompleteSetup(): after setCompanyNameStore(trimmed) and
await submitCompany(), call or implement a createDefaultProvider() (or invoke
the existing provider-creation API) and await it, then check
useSetupWizardStore.getState().providerError (or similar) and
setError(providerError) and return on failure, before proceeding to await
wizardCompleteSetup() and the existing completionError check; keep existing
symbols submitCompany(), wizardCompleteSetup(),
useSetupWizardStore.getState().companyError/completionError,
setCompanyNameStore, and useSetupStore.setState to locate and integrate the
change.

Comment on lines +76 to +81
const handleNext = useCallback(() => {
const idx = stepOrder.indexOf(currentStep)
if (idx < stepOrder.length - 1) {
navigate(`/setup/${stepOrder[idx + 1]}`)
}
}, [currentStep, stepOrder, navigate])
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

Disable Next when the next step is still locked.

handleNext() always pushes the next URL, and WizardNavigation is rendered without nextDisabled. When canNavigateTo(nextStep) is false, users can still click Next and then get redirected back by the URL-sync effect, which is a broken step-locking UX.

💡 Suggested fix
 export function WizardShell() {
   const navigate = useNavigate()
   const { step: urlStep } = useParams<{ step?: string }>()
@@
   const canNavigateTo = useSetupWizardStore((s) => s.canNavigateTo)
+  const currentIdx = stepOrder.indexOf(currentStep)
+  const nextStep = currentIdx >= 0 ? stepOrder[currentIdx + 1] : undefined
@@
   const handleNext = useCallback(() => {
-    const idx = stepOrder.indexOf(currentStep)
-    if (idx < stepOrder.length - 1) {
-      navigate(`/setup/${stepOrder[idx + 1]}`)
+    if (nextStep && canNavigateTo(nextStep)) {
+      navigate(`/setup/${nextStep}`)
     }
-  }, [currentStep, stepOrder, navigate])
+  }, [nextStep, canNavigateTo, navigate])
@@
         <div className="mt-8">
           <WizardNavigation
             stepOrder={stepOrder}
             currentStep={currentStep}
             onBack={handleBack}
             onNext={handleNext}
+            nextDisabled={nextStep ? !canNavigateTo(nextStep) : true}
           />
         </div>

Also applies to: 126-131

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

In `@web/src/pages/setup/WizardShell.tsx` around lines 76 - 81, handleNext
currently always navigates to the next step regardless of lock state causing a
broken UX; update handleNext (and the analogous handler around lines 126-131) to
check canNavigateTo(nextStep) before calling navigate and only push the next URL
when that returns true, and also pass a computed nextDisabled prop into
WizardNavigation (e.g., nextDisabled = !canNavigateTo(nextStep)) so the UI
disables the Next button when the next step is locked; reference the
stepOrder/currentStep calculation to derive nextStep and use
canNavigateTo(nextStep) to gate both navigation and the nextDisabled prop.

Comment on lines +76 to +86
const modelMap = new Map<string, ProviderModelConfig>()
for (const model of models) {
modelMap.set(model.id, model)
}

const breakdown: AgentCostBreakdown[] = []
let usedFallback = false

for (let i = 0; i < agents.length; i++) {
const agent = agents[i]!
const model = modelMap.get(agent.model_id)
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 ProviderModelConfig includes a provider field for the composite key
ast-grep --pattern 'interface ProviderModelConfig {
  $$$
}'

Repository: Aureliolo/synthorg

Length of output: 455


🏁 Script executed:

# Check the function signature and full context around the problematic code
sed -n '65,90p' web/src/utils/cost-estimator.ts

Repository: Aureliolo/synthorg

Length of output: 907


🏁 Script executed:

# Find AgentCostInput definition and verify if it has model_provider field
rg -A 10 'interface AgentCostInput' web/src

Repository: Aureliolo/synthorg

Length of output: 740


🏁 Script executed:

# Check where the models parameter comes from and what it contains
sed -n '50,75p' web/src/utils/cost-estimator.ts

Repository: Aureliolo/synthorg

Length of output: 842


🏁 Script executed:

# Find where estimateMonthlyCost is called to understand data flow
rg 'estimateMonthlyCost\(' web/src --type ts --type tsx -B 3 -A 3

Repository: Aureliolo/synthorg

Length of output: 89


🏁 Script executed:

# Find where estimateMonthlyCost is called
rg 'estimateMonthlyCost\(' web/src -A 3 -B 3

Repository: Aureliolo/synthorg

Length of output: 11806


🏁 Script executed:

# Check if there are any tests that verify collision scenarios
rg -A 10 "same.*model.*id\|collision\|duplicate.*model" web/src/__tests__/utils/cost-estimator.test.ts

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Check the full context of how models are assembled in the real call sites
sed -n '1,50p' web/src/pages/setup/CompanyStep.tsx | grep -A 20 "costEstimate\|estimateMonthlyCost"

Repository: Aureliolo/synthorg

Length of output: 1282


🏁 Script executed:

# Verify ProviderModelConfig definition one more time to be certain
sed -n '609,620p' web/src/api/types.ts

Repository: Aureliolo/synthorg

Length of output: 342


🏁 Script executed:

# Check where models are passed from in CompanyStep and CompleteStep
rg -B 5 "estimateMonthlyCost" web/src/pages/setup/CompanyStep.tsx | tail -20

Repository: Aureliolo/synthorg

Length of output: 634


🏁 Script executed:

# Look for where the models array is constructed or fetched
rg "ProviderModelConfig\[\]\|models.*=" web/src/pages/setup/CompanyStep.tsx -A 2 -B 2

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Get the full estimateMonthlyCost call in CompanyStep.tsx with more lines
sed -n '220,270p' web/src/pages/setup/CompanyStep.tsx

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Look for the availableModels or models variable in setup wizard store
rg "availableModels\|getModels\|models:" web/src/stores/setup-wizard.ts -B 2 -A 2

Repository: Aureliolo/synthorg

Length of output: 44


🏁 Script executed:

# Find the exact line where estimateMonthlyCost is called
grep -n "estimateMonthlyCost" web/src/pages/setup/CompanyStep.tsx

Repository: Aureliolo/synthorg

Length of output: 160


🏁 Script executed:

# Get context around that line
sed -n '175,235p' web/src/pages/setup/CompanyStep.tsx

Repository: Aureliolo/synthorg

Length of output: 1062


🏁 Script executed:

# Search for setup-wizard store definition
fd setup-wizard web/src/stores

Repository: Aureliolo/synthorg

Length of output: 92


🏁 Script executed:

# Get the full estimateMonthlyCost call from line 63
sed -n '60,75p' web/src/pages/setup/CompanyStep.tsx

Repository: Aureliolo/synthorg

Length of output: 498


🏁 Script executed:

# Check the setup-wizard store to understand the data structure
head -100 web/src/stores/setup-wizard.ts

Repository: Aureliolo/synthorg

Length of output: 2515


🏁 Script executed:

# Final check: Verify models are indeed always empty in actual calls
rg "estimateMonthlyCost" web/src/pages/setup -A 8 | grep -A 8 "models\|providedModels"

Repository: Aureliolo/synthorg

Length of output: 845


🏁 Script executed:

# Check CompleteStep more carefully - it DOES pass real models
sed -n '35,75p' web/src/pages/setup/CompleteStep.tsx

Repository: Aureliolo/synthorg

Length of output: 1241


🏁 Script executed:

# Check what ProviderConfig looks like - does it include provider identifier?
rg -A 15 "interface ProviderConfig" web/src/api/types.ts

Repository: Aureliolo/synthorg

Length of output: 485


🏁 Script executed:

# Verify the structure of providers object in CompleteStep more completely
sed -n '1,50p' web/src/pages/setup/CompleteStep.tsx

Repository: Aureliolo/synthorg

Length of output: 2140


🏁 Script executed:

# Final verification: confirm ProviderConfig has NO provider name field
rg -B 5 "interface ProviderConfig" web/src/api/types.ts | head -20

Repository: Aureliolo/synthorg

Length of output: 184


Model lookup ignores model_provider, risking price collisions when multiple providers are configured.

AgentCostInput includes model_provider (line 57), but models are flattened without provider context in CompleteStep.tsx (line 40: Object.values(providers).flatMap((p) => [...p.models])), and the model map is keyed only by model.id (line 78). If two configured providers expose models with the same ID but different pricing, the estimate will use whichever model was inserted last.

Note: The proposed composite-key fix references model.provider, which does not exist on ProviderModelConfig. The correct approach requires preserving provider context when flattening models, then constructing the composite key from both the provider name and model ID.

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

In `@web/src/utils/cost-estimator.ts` around lines 76 - 86, The model lookup
currently keys modelMap only by model.id, causing collisions when different
providers expose the same model ID; update the flattening and map construction
so provider context is preserved (change the flatten in CompleteStep.tsx to
include provider name with each ProviderModelConfig entry) and then build
modelMap in cost-estimator.ts using a composite key like
`${provider}:${model.id}`; finally update the lookup that uses
agent.model_provider and agent.model_id (in the loop referencing modelMap and
AgentCostInput) to query the composite key so pricing is resolved per provider.

Aureliolo and others added 2 commits March 27, 2026 17:30
…Next disabled

- docs/design/brand-and-ux.md: add missing InputField props (placeholder,
  required, disabled, type, value, onChange) to component inventory
- setup-validation property test: use Object.create(null) for provider
  record to prevent __proto__ key poisoning from fc.string()
- SkipWizardForm: simplify to try/catch since store actions now re-throw
  (removed dead post-await store reads), use getErrorMessage, wrap in
  <form> for Enter key submission support
- WizardShell: pass nextDisabled={!stepsCompleted[currentStep]} to
  WizardNavigation so Next is disabled until the current step completes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- WizardShell: skip button marks all steps complete before navigating
  so canNavigateTo('complete') returns true (was broken -- redirected back)
- CompleteStep: render SkipWizardForm when companyResponse is null
  instead of dead-end EmptyState
- TemplateCostBadge: use CurrencyCode type + DEFAULT_CURRENCY constant;
  threaded CurrencyCode through TemplateCard, TemplateCategoryGroup,
  TemplateCompareDrawer, and setup-wizard store
- template-categories: replace tag->label->category two-hop with direct
  TAG_TO_CATEGORY map, eliminating coupling where label renames would
  silently break classification; remove unused CATEGORY_LABELS/LABEL_TO_CATEGORY
- ThemePreview: replace hand-rolled agent markup with AgentCard component
- toggle-field.stories: forward Interactive story args to render function
- template-categories.test: derive expectedOrder from fixture tags
  instead of function under test
- README: mention conditional account creation step
- user_guide: add typography to theme step description

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 16:43 — with GitHub Actions Inactive
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: 7

♻️ Duplicate comments (7)
web/src/pages/setup/SkipWizardForm.tsx (2)

29-33: ⚠️ Potential issue | 🟠 Major

Abort quick setup when company creation fails.

submitCompany() stores companyError instead of rejecting, so await submitCompany() can return after a failed POST and still fall through to wizardCompleteSetup(). Check the store error before continuing, or make the action return/throw failure explicitly.

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

In `@web/src/pages/setup/SkipWizardForm.tsx` around lines 29 - 33, The code
currently proceeds to wizardCompleteSetup even if submitCompany failed because
submitCompany stores companyError instead of throwing; update the handler around
setCompanyNameStore/submitCompany/wizardCompleteSetup to check for failure after
awaiting submitCompany (e.g., read useSetupStore.getState().companyError or the
appropriate companyError selector) and abort (return or throw) if an error
exists, or alternatively change submitCompany to explicitly return a failure
value or throw on HTTP error so the caller can stop before calling
wizardCompleteSetup and useSetupStore.setState({ setupComplete: true }). Ensure
you reference setCompanyNameStore, submitCompany, wizardCompleteSetup and
companyError when making the change.

47-75: 🛠️ Refactor suggestion | 🟠 Major

Use SectionCard for this titled form block.

This is hand-rolling a card/header pattern that already exists in the shared UI layer. As per coding guidelines: "Do NOT build card-with-header layouts from scratch in React -- use <SectionCard> component."

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

In `@web/src/pages/setup/SkipWizardForm.tsx` around lines 47 - 75, Replace the
hand-rolled card/header in SkipWizardForm with the shared SectionCard component:
import SectionCard into web/src/pages/setup/SkipWizardForm.tsx, move the "Quick
Setup" heading and description into SectionCard's title/description props (or
children according to the SectionCard API), and wrap the existing <form
onSubmit={handleSubmit} ...> plus the InputField, error block, and Button inside
SectionCard so the layout uses the shared card/header pattern instead of the
custom divs; keep existing props and state usages (companyName, setCompanyName,
handleSubmit, loading, error) unchanged.
web/src/stores/setup-wizard.ts (2)

288-290: ⚠️ Potential issue | 🟠 Major

Keep the generated company state tied to the editable form.

selectTemplate(), setTemplateVariable(), setCompanyName(), and setCompanyDescription() only mutate local state, while submitCompany() still posts just template_name. That means template-variable/model-tier edits never reach /setup/company, and once companyResponse is set the stored preview/agents can drift from the visible form with no re-apply path.

Also applies to: 307-310, 315-343

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

In `@web/src/stores/setup-wizard.ts` around lines 288 - 290, The form setters
(selectTemplate, setTemplateVariable, setCompanyName, setCompanyDescription)
currently only change local fields but never update the composed company object
that submitCompany posts, causing template variables/model_tier edits to be
omitted and companyResponse to drift; fix by maintaining a single canonical
draft company state (e.g., generatedCompany or companyDraft) that those setters
update (include template_name, template_variables, model_tier, name,
description), and change submitCompany to POST that full draft object instead of
only template_name; also, when companyResponse is received, sync it back into
the same draft/edit state (or replace/merge) so preview/agents always reflect
the editable form (references: selectTemplate, setTemplateVariable,
setCompanyName, setCompanyDescription, submitCompany).

368-407: ⚠️ Potential issue | 🟠 Major

Re-throw failed agent mutations.

These actions only set agentsError and resolve normally. Callers driving inline edits and model changes can't detect the failure, so optimistic UI can stay committed after the API rejects.

Minimal fix
   async updateAgentModel(index, provider, modelId) {
     set({ agentsError: null })
     try {
       const updated = await apiUpdateAgentModel(index, {
         model_provider: provider,
         model_id: modelId,
       })
       set((s) => ({
         agents: s.agents.map((a, i) => i === index ? updated : a),
       }))
     } catch (err) {
       console.error('setup-wizard: updateAgentModel failed:', err)
       set({ agentsError: getErrorMessage(err) })
+      throw err
     }
   },

Apply the same catch-path change to updateAgentName() and randomizeAgentName().

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

In `@web/src/stores/setup-wizard.ts` around lines 368 - 407, The catch blocks in
updateAgentName and randomizeAgentName currently only log and set agentsError so
callers can’t detect API failures; match updateAgentModel’s behavior by
re-throwing the caught error after setting agentsError so callers driving inline
edits can handle failures (i.e., in updateAgentName and randomizeAgentName
catch(err) blocks call set({ agentsError: getErrorMessage(err) }) and then throw
err).
web/src/pages/setup/CompanyStep.tsx (1)

62-72: ⚠️ Potential issue | 🟠 Major

Use configured provider models here instead of forcing fallback pricing.

Passing [] guarantees fallback totals in this step, even after Providers have been configured and the user navigates back here. That makes the estimate disagree with CompleteStep, which already flattens providers into real model pricing.

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

In `@web/src/pages/setup/CompanyStep.tsx` around lines 62 - 72, The cost
estimation currently forces fallback pricing by passing an empty array as the
second argument to estimateMonthlyCost; change it to pass the configured
provider models instead so estimates here match CompleteStep. Locate the
costEstimate computation (const costEstimate = useMemo(...)) and replace the
hardcoded [] with the flattened list of provider model entries used elsewhere
(e.g., the same providers -> flatMap(p => p.models) or the existing
providersModels/flattenedProviders variable) so estimateMonthlyCost receives the
real provider model data rather than an empty array.
web/src/pages/setup/TemplateCompareDrawer.tsx (1)

45-68: 🧹 Nitpick | 🔵 Trivial

Category derivation logic duplicates mapping that should be centralized.

The deriveCategory function duplicates tag-to-category mapping that likely exists or should exist in @/utils/template-categories.ts. This could drift from the canonical source. Consider adding deriveCategoryFromTags to that utility module.

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

In `@web/src/pages/setup/TemplateCompareDrawer.tsx` around lines 45 - 68, The
deriveCategory function in TemplateCompareDrawer.tsx duplicates the tag→category
mapping; replace it by extracting the mapping logic into the shared utility
module (e.g., add a deriveCategoryFromTags(tags: string[]): string in
`@/utils/template-categories.ts`) and call that from deriveCategory (or remove
deriveCategory and import deriveCategoryFromTags directly). Specifically, move
the categoryTags map and the loop logic into deriveCategoryFromTags, export it,
then update the TemplateCompareDrawer function to call
deriveCategoryFromTags(template.tags) and pass the result to getCategoryLabel as
needed so there is a single canonical mapping location.
web/src/pages/setup/TemplateCard.tsx (1)

32-37: 🛠️ Refactor suggestion | 🟠 Major

Arbitrary shadow value bypasses design tokens.

Line 35 uses an arbitrary Tailwind shadow shadow-[0_0_12px_color-mix(...)]. While it references var(--so-accent), this still bypasses the standard shadow tokens. Consider using the shared shadow token:

-        selected ? 'border-accent shadow-[0_0_12px_color-mix(in_srgb,var(--so-accent)_15%,transparent)]' : 'border-border',
+        selected ? 'border-accent shadow-card-hover' : 'border-border',

If a custom accent glow is needed, add it as a design token (e.g., --so-shadow-accent-glow) in the theme layer. As per coding guidelines: "Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders in React components. NEVER hardcode shadow or border values."

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

In `@web/src/pages/setup/TemplateCard.tsx` around lines 32 - 37, The component
TemplateCard.tsx currently applies an arbitrary Tailwind shadow inside the
className conditional (the selected branch uses
shadow-[0_0_12px_color-mix(...)]), which bypasses design tokens; replace that
hardcoded shadow with the theme token (e.g., use var(--so-shadow-card-hover) or
a new token like var(--so-shadow-accent-glow)) and update the className in the
cn call that references selected so it uses the token-based utility instead of
the arbitrary value; if a new accent glow is required, add
--so-shadow-accent-glow to the theme layer and reference it from the className
to comply with the token usage guidelines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/design/brand-and-ux.md`:
- Line 227: Update the docs entry for SliderField to match the actual
SliderFieldProps in slider-field.tsx: add the missing optional props `disabled?`
and `className?` to the props column (or schema) so the component inventory
accurately reflects the public API defined by the SliderFieldProps interface in
slider-field.tsx.
- Line 226: Update the SelectField entry in the component inventory to match the
actual SelectFieldProps by adding the missing public props: include `required?`,
`disabled?`, and `className?` alongside the existing `label`, `options`,
`value`, `onChange`, `error?`, `hint?`, and `placeholder?`; reference the
SelectField component in select-field.tsx and the SelectFieldProps interface to
ensure the documented schema exactly matches the implemented prop names and
optionality.

In `@web/src/__tests__/utils/setup-validation.property.test.ts`:
- Around line 63-76: The test for empty-or-whitespace company names only matches
literal spaces; update the property in
web/src/__tests__/utils/setup-validation.property.test.ts so it generates all
whitespace characters instead of just spaces (the assertion targets
validateCompanyStep which uses trim()). Replace the fc.stringMatching(/^
{0,50}$/) generator with one that uses a whitespace-aware pattern (e.g.,
/^\s{0,50}$/) or an equivalent generator that includes
tabs/newlines/non-breaking spaces so the property truly verifies all
whitespace-only names are invalid.

In `@web/src/pages/setup/TemplateCard.tsx`:
- Around line 69-73: In TemplateCard, replace the semantic misuse of StatPill
(StatPill with label="") when rendering template.tags by using the design
system's Tag/Badge component (or a simple semantic <span role="status"> styled
as a tag if none exists) instead: update the map over template.tags to render
Tag/Badge components with key={tag} and the tag text as children, remove the
empty label prop and related StatPill import, and ensure you import the
Tag/Badge component (or add the small styled span) so tags are rendered with
proper semantics and styling.

In `@web/src/pages/setup/ThemePreview.tsx`:
- Around line 16-23: The preview div emits data-density but not the CSS density
class, so the density control has no visual effect; update ThemePreview.tsx to
add the corresponding density class to the container (e.g., map settings.density
values to "density-dense", "density-medium" (for "balanced"), and
"density-sparse") and include that class in the div's className while keeping
the existing data-density attribute; locate the div that currently uses
data-density={settings.density} and add the computed density class (or a small
helper like densityClass) so the design tokens
(.density-dense/.density-medium/.density-sparse) apply.

In `@web/src/pages/setup/WizardShell.tsx`:
- Around line 63-68: handleStepClick currently navigates unconditionally which
causes a flash when clicking locked steps; update handleStepClick in WizardShell
to first check the step's lock state (e.g., call the existing step-accessor such
as isStepLocked / isStepAccessible / canNavigateToStep or compute from
WizardStep/steps state) and return early if the step is locked, only calling
navigate(`/setup/${step}`) when the step is unlocked; ensure the same guard is
used for any onStepClick wiring so clicking a locked step does nothing and
avoids the brief redirect.

In `@web/src/stores/setup-wizard.ts`:
- Around line 434-447: The console.error in createProviderFromPreset is printing
the raw error which may include the api_key; replace that raw-error logging with
a safe/redacted message (for example log a fixed string plus
getErrorMessage(err) or a redacted object) so secrets aren't exposed—update the
error branch in createProviderFromPreset to stop passing err directly to
console.error and instead log only getErrorMessage(err) or a redacted error
summary and still call set({ providersError: getErrorMessage(err) }).

---

Duplicate comments:
In `@web/src/pages/setup/CompanyStep.tsx`:
- Around line 62-72: The cost estimation currently forces fallback pricing by
passing an empty array as the second argument to estimateMonthlyCost; change it
to pass the configured provider models instead so estimates here match
CompleteStep. Locate the costEstimate computation (const costEstimate =
useMemo(...)) and replace the hardcoded [] with the flattened list of provider
model entries used elsewhere (e.g., the same providers -> flatMap(p => p.models)
or the existing providersModels/flattenedProviders variable) so
estimateMonthlyCost receives the real provider model data rather than an empty
array.

In `@web/src/pages/setup/SkipWizardForm.tsx`:
- Around line 29-33: The code currently proceeds to wizardCompleteSetup even if
submitCompany failed because submitCompany stores companyError instead of
throwing; update the handler around
setCompanyNameStore/submitCompany/wizardCompleteSetup to check for failure after
awaiting submitCompany (e.g., read useSetupStore.getState().companyError or the
appropriate companyError selector) and abort (return or throw) if an error
exists, or alternatively change submitCompany to explicitly return a failure
value or throw on HTTP error so the caller can stop before calling
wizardCompleteSetup and useSetupStore.setState({ setupComplete: true }). Ensure
you reference setCompanyNameStore, submitCompany, wizardCompleteSetup and
companyError when making the change.
- Around line 47-75: Replace the hand-rolled card/header in SkipWizardForm with
the shared SectionCard component: import SectionCard into
web/src/pages/setup/SkipWizardForm.tsx, move the "Quick Setup" heading and
description into SectionCard's title/description props (or children according to
the SectionCard API), and wrap the existing <form onSubmit={handleSubmit} ...>
plus the InputField, error block, and Button inside SectionCard so the layout
uses the shared card/header pattern instead of the custom divs; keep existing
props and state usages (companyName, setCompanyName, handleSubmit, loading,
error) unchanged.

In `@web/src/pages/setup/TemplateCard.tsx`:
- Around line 32-37: The component TemplateCard.tsx currently applies an
arbitrary Tailwind shadow inside the className conditional (the selected branch
uses shadow-[0_0_12px_color-mix(...)]), which bypasses design tokens; replace
that hardcoded shadow with the theme token (e.g., use
var(--so-shadow-card-hover) or a new token like var(--so-shadow-accent-glow))
and update the className in the cn call that references selected so it uses the
token-based utility instead of the arbitrary value; if a new accent glow is
required, add --so-shadow-accent-glow to the theme layer and reference it from
the className to comply with the token usage guidelines.

In `@web/src/pages/setup/TemplateCompareDrawer.tsx`:
- Around line 45-68: The deriveCategory function in TemplateCompareDrawer.tsx
duplicates the tag→category mapping; replace it by extracting the mapping logic
into the shared utility module (e.g., add a deriveCategoryFromTags(tags:
string[]): string in `@/utils/template-categories.ts`) and call that from
deriveCategory (or remove deriveCategory and import deriveCategoryFromTags
directly). Specifically, move the categoryTags map and the loop logic into
deriveCategoryFromTags, export it, then update the TemplateCompareDrawer
function to call deriveCategoryFromTags(template.tags) and pass the result to
getCategoryLabel as needed so there is a single canonical mapping location.

In `@web/src/stores/setup-wizard.ts`:
- Around line 288-290: The form setters (selectTemplate, setTemplateVariable,
setCompanyName, setCompanyDescription) currently only change local fields but
never update the composed company object that submitCompany posts, causing
template variables/model_tier edits to be omitted and companyResponse to drift;
fix by maintaining a single canonical draft company state (e.g.,
generatedCompany or companyDraft) that those setters update (include
template_name, template_variables, model_tier, name, description), and change
submitCompany to POST that full draft object instead of only template_name;
also, when companyResponse is received, sync it back into the same draft/edit
state (or replace/merge) so preview/agents always reflect the editable form
(references: selectTemplate, setTemplateVariable, setCompanyName,
setCompanyDescription, submitCompany).
- Around line 368-407: The catch blocks in updateAgentName and
randomizeAgentName currently only log and set agentsError so callers can’t
detect API failures; match updateAgentModel’s behavior by re-throwing the caught
error after setting agentsError so callers driving inline edits can handle
failures (i.e., in updateAgentName and randomizeAgentName catch(err) blocks call
set({ agentsError: getErrorMessage(err) }) and then throw err).
🪄 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: 2d5a86b9-25a2-408d-a47d-ffd5a25b46cb

📥 Commits

Reviewing files that changed from the base of the PR and between 4760dac and 7dab350.

📒 Files selected for processing (17)
  • README.md
  • docs/design/brand-and-ux.md
  • docs/user_guide.md
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/CompanyStep.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/utils/template-categories.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). (5)
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Dashboard Test
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (3)
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{tsx,ts}: ALWAYS reuse existing components from web/src/components/ui/ before creating new ones. These are the shared building blocks for every page.
Use StatusBadge component from @/components/ui/status-badge for agent/task/system status indicators.
Use MetricCard component from @/components/ui/metric-card for numeric KPIs with sparkline and change badge.
Use SectionCard component from @/components/ui/section-card for titled card wrappers with icon and action slots.
Use AgentCard component from @/components/ui/agent-card for agent display: avatar, name, role, status, current task.
Use DeptHealthBar component from @/components/ui/dept-health-bar for department health display with animated fill bar.
Use ProgressGauge component from @/components/ui/progress-gauge for circular gauges of budget/utilization with optional max prop.
Use Avatar component from @/components/ui/avatar for circular initials avatars with optional borderColor prop.
Use Toast and ToastContainer components from @/components/ui/toast for notifications with auto-dismiss queue.
Use Skeleton, SkeletonCard, SkeletonMetric, SkeletonTable, SkeletonText from @/components/ui/skeleton for loading placeholders that match component shapes.
Use EmptyState component from @/components/ui/empty-state for no-data/no-results placeholders with icon, title, description, optional action button.
Use ErrorBoundary component from @/components/ui/error-boundary as React error boundary with retry. Use level prop: page / section / component.
Use ConfirmDialog component from @/components/ui/confirm-dialog for confirmation modals with default / destructive variants and loading state.
Use CommandPalette component from @/components/ui/command-palette for global Cmd+K search. Mount once in AppLayout and register commands via useCommandPalette hook.
Use InlineEdit component from @/components/ui/inline-edit for click-to-edit text ...

Files:

  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/utils/template-categories.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/CompleteStep.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/stores/setup-wizard.ts
  • web/src/pages/setup/CompanyStep.tsx
web/src/components/ui/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

When creating new shared React components, place in web/src/components/ui/ with descriptive kebab-case filename. Create .stories.tsx file with all states (default, hover, loading, error, empty). Export props as TypeScript interface. Use design tokens exclusively.

Files:

  • web/src/components/ui/toggle-field.stories.tsx
web/**/*.stories.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/**/*.stories.tsx: Storybook 10 is ESM-only -- all CommonJS support removed.
In Storybook 10, import from storybook/test (not @storybook/test) and storybook/actions (not @storybook/addon-actions).
In Storybook 10, use parameters.backgrounds.options (object keyed by name) + initialGlobals.backgrounds.value for background configuration.

Files:

  • web/src/components/ui/toggle-field.stories.tsx
🧠 Learnings (37)
📚 Learning: 2026-03-21T12:54:22.557Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T12:54:22.557Z
Learning: Version bumping (pre-1.0): `fix:` = patch, `feat:` = patch, `feat!:` = minor, `BREAKING CHANGE` trailer = minor. Update version in `pyproject.toml` (`[tool.commitizen].version`) and `src/synthorg/__init__.py` (`__version__`)

Applied to files:

  • README.md
📚 Learning: 2026-03-19T07:12:14.508Z
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/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...

Applied to files:

  • README.md
📚 Learning: 2026-03-26T15:18:16.848Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-26T15:18:16.848Z
Learning: Applies to src/synthorg/api/**/*.py : Litestar API must include setup wizard, auth/, auto-wiring, and lifecycle management

Applied to files:

  • README.md
  • docs/user_guide.md
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to docker/Dockerfile.sandbox : Docker sandbox: `synthorg-sandbox` — Python 3.14 + Node.js + git, non-root (UID 10001), agent code execution sandbox

Applied to files:

  • README.md
📚 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__/utils/template-categories.test.ts
  • web/src/__tests__/utils/setup-validation.property.test.ts
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/**/*.stories.tsx : In Storybook 10, import from `storybook/test` (not `storybook/test`) and `storybook/actions` (not `storybook/addon-actions`).

Applied to files:

  • web/src/__tests__/utils/template-categories.test.ts
  • web/src/components/ui/toggle-field.stories.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `InlineEdit` component from `@/components/ui/inline-edit` for click-to-edit text with Enter/Escape, validation, optimistic save with rollback.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `ConfirmDialog` component from `@/components/ui/confirm-dialog` for confirmation modals with `default` / `destructive` variants and `loading` state.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/CompleteStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT build card-with-header layouts from scratch in React -- use `<SectionCard>` component.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `SectionCard` component from `@/components/ui/section-card` for titled card wrappers with icon and action slots.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `AgentCard` component from `@/components/ui/agent-card` for agent display: avatar, name, role, status, current task.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `StaggerGroup` and `StaggerItem` components from `@/components/ui/stagger-group` for card entrance stagger with configurable delay.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create metric displays with `text-metric font-bold` in React -- use `<MetricCard>` component.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones. These are the shared building blocks for every page.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware spacing tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing in React components.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows/borders in React components. NEVER hardcode shadow or border values.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic color classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-accent)`). NEVER hardcode hex values in `.tsx`/`.ts` files.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/TemplateCard.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `MetricCard` component from `@/components/ui/metric-card` for numeric KPIs with sparkline and change badge.

Applied to files:

  • web/src/pages/setup/SkipWizardForm.tsx
  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/TemplateCard.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `StatusBadge` component from `@/components/ui/status-badge` for agent/task/system status indicators.

Applied to files:

  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `ProgressGauge` component from `@/components/ui/progress-gauge` for circular gauges of budget/utilization with optional `max` prop.

Applied to files:

  • web/src/pages/setup/TemplateCostBadge.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT recreate status dots inline in React -- use `<StatusBadge>` component.

Applied to files:

  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/WizardShell.tsx
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/components/ui/**/*.{tsx,ts} : When creating new shared React components, place in `web/src/components/ui/` with descriptive kebab-case filename. Create `.stories.tsx` file with all states (default, hover, loading, error, empty). Export props as TypeScript interface. Use design tokens exclusively.

Applied to files:

  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT use `rgba()` with hardcoded values in React -- use design token variables.

Applied to files:

  • web/src/pages/setup/TemplateCostBadge.tsx
  • web/src/pages/setup/ThemePreview.tsx
  • web/src/pages/setup/TemplateCard.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `DeptHealthBar` component from `@/components/ui/dept-health-bar` for department health display with animated fill bar.

Applied to files:

  • web/src/pages/setup/ThemePreview.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `Avatar` component from `@/components/ui/avatar` for circular initials avatars with optional `borderColor` prop.

Applied to files:

  • web/src/pages/setup/ThemePreview.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/.storybook/preview.tsx : In Storybook 10, use `parameters.a11y.test: 'error' | 'todo' | 'off'` for accessibility testing. Set globally in `preview.tsx` to enforce WCAG compliance on all stories.

Applied to files:

  • web/src/pages/setup/ThemePreview.tsx
  • web/src/components/ui/toggle-field.stories.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT render initials circles manually in React -- use `<Avatar>` component.

Applied to files:

  • web/src/pages/setup/ThemePreview.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `AnimatedPresence` component from `@/components/ui/animated-presence` for page transition wrapper (Framer Motion AnimatePresence keyed by route).

Applied to files:

  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do NOT create complex (>8 line) JSX inside `.map()` in React -- extract to a shared component.

Applied to files:

  • web/src/pages/setup/WizardShell.tsx
  • web/src/pages/setup/TemplateCategoryGroup.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
  • web/src/pages/setup/CompanyStep.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `Skeleton`, `SkeletonCard`, `SkeletonMetric`, `SkeletonTable`, `SkeletonText` from `@/components/ui/skeleton` for loading placeholders that match component shapes.

Applied to files:

  • web/src/pages/setup/WizardShell.tsx
  • docs/design/brand-and-ux.md
  • web/src/pages/setup/TemplateCard.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to tests/**/*.py : Use `test-provider`, `test-small-001`, etc. in tests instead of real vendor names.

Applied to files:

  • web/src/__tests__/utils/setup-validation.property.test.ts
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/**/*.stories.tsx : Storybook 10 is ESM-only -- all CommonJS support removed.

Applied to files:

  • web/src/components/ui/toggle-field.stories.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/**/*.stories.tsx : In Storybook 10, use `parameters.backgrounds.options` (object keyed by name) + `initialGlobals.backgrounds.value` for background configuration.

Applied to files:

  • web/src/components/ui/toggle-field.stories.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/.storybook/** : In Storybook 10, use `defineMain` from `storybook/react-vite/node` and `definePreview` from `storybook/react-vite` with explicit `framework` field.

Applied to files:

  • web/src/components/ui/toggle-field.stories.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `EmptyState` component from `@/components/ui/empty-state` for no-data/no-results placeholders with icon, title, description, optional action button.

Applied to files:

  • web/src/components/ui/toggle-field.stories.tsx
  • web/src/pages/setup/TemplateCompareDrawer.tsx
📚 Learning: 2026-03-27T15:31:25.847Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `TaskStatusIndicator` component from `@/components/ui/task-status-indicator` for task status dots with optional label and pulse animation.

Applied to files:

  • web/src/components/ui/toggle-field.stories.tsx
📚 Learning: 2026-03-19T11:33:01.580Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T11:33:01.580Z
Learning: Applies to web/**/*.vue : Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in dashboard code — use generic names: `example-provider`, `example-large-001`, etc.

Applied to files:

  • web/src/pages/setup/CompanyStep.tsx
🔇 Additional comments (19)
web/src/components/ui/toggle-field.stories.tsx (4)

17-36: Missing required state variants (hover, loading, error, empty).

The shared UI component guidelines require stories for all states: default, hover, loading, error, and empty. This file only covers default, checked, with-description, and disabled.

As per coding guidelines, "When creating new shared React components ... Create .stories.tsx file with all states (default, hover, loading, error, empty)."


43-46: Interactive story now partially forwards args.

The render function now passes label and description from args, which is an improvement. However, the checked arg is still not used to set the initial state of InteractiveToggle, so that Storybook control remains non-functional.


5-15: Meta configuration looks good.

The satisfies Meta<typeof ToggleField> pattern provides proper type inference, autodocs tag enables auto-generated documentation, and a11y: { test: 'error' } enforces accessibility compliance at the story level.


2-2: ⚠️ Potential issue | 🟠 Major

Change import path to match configured framework.

The configured Storybook framework is @storybook/react-vite (in web/.storybook/main.ts), so Meta and StoryObj should be imported from @storybook/react-vite, not @storybook/react:

import type { Meta, StoryObj } from '@storybook/react-vite'
⛔ Skipped due to learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/.storybook/** : In Storybook 10, use `defineMain` from `storybook/react-vite/node` and `definePreview` from `storybook/react-vite` with explicit `framework` field.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/**/*.stories.tsx : In Storybook 10, import from `storybook/test` (not `storybook/test`) and `storybook/actions` (not `storybook/addon-actions`).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/**/*.stories.tsx : Storybook 10 is ESM-only -- all CommonJS support removed.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T15:31:25.847Z
Learning: Applies to web/**/*.stories.tsx : In Storybook 10, use `parameters.backgrounds.options` (object keyed by name) + `initialGlobals.backgrounds.value` for background configuration.
docs/design/brand-and-ux.md (1)

225-225: Past concern resolved: InputField props are now documented.

The props table now includes the core input API (placeholder?, required?, disabled?, type?, value, onChange) alongside the presentation props, addressing the prior review comment. This accurately reflects how consumers use InputField in the codebase.

README.md (1)

110-110: Past concern resolved: conditional account step is now mentioned.

The text now includes "(if needed)" for the admin account creation step, accurately reflecting that this step only appears when needsAdmin is true. The flow description appropriately balances completeness with README-level brevity.

docs/user_guide.md (1)

71-79: LGTM: wizard documentation is accurate and complete.

The seven-step wizard flow is thoroughly documented with implementation-accurate details:

  • The conditional account step correctly notes it "only appears when no admin account exists yet"
  • Each step's capabilities (template categorization/comparison, provider auto-detection, theme customization) match the implementation
  • The completion gating validation (company + agents + providers required, all from DATABASE source not defaults) precisely reflects the backend enforcement in complete_setup endpoint

The sequential completion model and revisit capability are clearly explained.

web/src/pages/setup/WizardShell.tsx (4)

1-27: LGTM! Well-structured imports and step component registry.

The imports correctly use shared UI components (Button, ErrorBoundary, AnimatedPresence) and the STEP_COMPONENTS registry cleanly maps wizard steps to their components. The type guard isWizardStep properly validates URL parameters.


44-61: URL sync logic handles edge cases well.

The effect correctly:

  • Redirects missing/invalid steps to stepOrder[0]
  • Enforces step locking via canNavigateTo
  • Falls back to first incomplete step when navigation is blocked

94-109: Skip wizard flow correctly marks steps complete before navigation.

This addresses the previous routing concern—marking all steps complete ensures canNavigateTo returns true for the complete step, and CompleteStep can render properly with minimal data expectations.


124-128: Good use of ErrorBoundary and AnimatedPresence.

The step content is wrapped in ErrorBoundary at page level and AnimatedPresence is keyed by currentStep for route-based transitions. This follows the design system guidelines.

web/src/pages/setup/TemplateCostBadge.tsx (1)

1-29: LGTM! Clean, accessible cost badge implementation.

The component correctly uses the shared CurrencyCode type and DEFAULT_CURRENCY constant (addressing the previous review). The aria-label provides good accessibility, and styling uses semantic design tokens exclusively.

web/src/pages/setup/TemplateCard.tsx (1)

76-83: LGTM! Select button implementation.

The button correctly toggles between default and outline variants based on selection state, and uses the shared Button component.

web/src/pages/setup/TemplateCategoryGroup.tsx (2)

1-18: LGTM! Well-typed props interface.

The interface uses proper readonly types (readonly TemplateInfoResponse[], ReadonlyMap, ReadonlySet) for immutability guarantees, and CurrencyCode for type-safe currency handling.


37-53: LGTM! Clean category group rendering.

Good use of StaggerGroup/StaggerItem for card entrance animations per coding guidelines. The .map() body stays concise by delegating rendering to TemplateCard. The gap-grid-gap token is correctly used for spacing.

The comparedTemplates.includes() lookup is O(n), but with the max-3 compare limit from PR objectives, this is negligible.

web/src/pages/setup/TemplateCompareDrawer.tsx (4)

78-88: LGTM! Clean equality comparison helper.

The valuesAreEqual function properly handles both string and array values with consistent normalization via join(',').


95-129: Well-extracted comparison row component.

Good refactor addressing the previous review—the comparison row logic is now in ComparisonRowEntry, keeping the main component clean. The empty tags fallback (line 114-121) is properly handled.


131-175: LGTM! Drawer structure and action buttons.

The drawer correctly:

  • Returns null for fewer than 2 templates
  • Uses the shared Drawer component
  • Provides clear Select/Remove actions per template
  • Uses semantic border/spacing tokens

70-76: Remove this comment - the concerns are based on incorrect assumptions about the codebase.

SkillPattern is a string literal union ('tool_wrapper' | 'generator' | ... | 'pipeline'), so String(sp) correctly produces the string value, not [object Object]. Additionally, autonomy_level and communication_style are fields on PersonalityConfig (agent configuration), not on TemplateInfoResponse. Templates describe available skill patterns, not individual agent autonomy/communication settings. The objective #726 reference cannot be verified in the codebase.

			> Likely an incorrect or invalid review comment.

Comment on lines +69 to +73
<div className="flex flex-wrap gap-1">
{template.tags.map((tag) => (
<StatPill key={tag} label="" value={tag} className="text-compact" />
))}
</div>
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

StatPill with empty label for standalone tags.

Using StatPill with label="" renders value-only pills. This works but is semantically awkward. If the design system has a dedicated Tag or Badge component, consider using that instead for cleaner semantics.

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

In `@web/src/pages/setup/TemplateCard.tsx` around lines 69 - 73, In TemplateCard,
replace the semantic misuse of StatPill (StatPill with label="") when rendering
template.tags by using the design system's Tag/Badge component (or a simple
semantic <span role="status"> styled as a tag if none exists) instead: update
the map over template.tags to render Tag/Badge components with key={tag} and the
tag text as children, remove the empty label prop and related StatPill import,
and ensure you import the Tag/Badge component (or add the small styled span) so
tags are rendered with proper semantics and styling.

… mapping

- WizardShell: guard handleStepClick with canNavigateTo to prevent
  flash when clicking locked steps in progress bar
- ThemePreview: apply density CSS class (density-dense/medium/sparse)
  so the density control actually affects the preview layout
- setup-wizard store: redact raw error in createProviderFromPreset
  console.error to prevent API key leakage in logs
- TemplateCompareDrawer: replace duplicated categoryTags map with
  shared deriveCategoryFromTags from template-categories.ts
- template-categories: export deriveCategoryFromTags for reuse
- brand-and-ux.md: add missing props to SelectField and SliderField
  inventory rows (required, disabled, className)
- setup-validation property test: use \s instead of space-only regex
  so whitespace test covers tabs, newlines, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aureliolo Aureliolo merged commit ae8b50b into main Mar 27, 2026
27 of 29 checks passed
@Aureliolo Aureliolo deleted the feat/setup-wizard-rebuild branch March 27, 2026 17:06
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 27, 2026 17:06 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Mar 30, 2026
🤖 I have created a release *beep* *boop*
---
#MAJOR CHANGES; We got a somewhat working webui :)

##
[0.5.0](v0.4.9...v0.5.0)
(2026-03-30)


### Features

* add analytics trends and budget forecast API endpoints
([#798](#798))
([16b61f5](16b61f5))
* add department policies to default templates
([#852](#852))
([7a41548](7a41548))
* add remaining activity event types (task_started, tool_used,
delegation, cost_incurred)
([#832](#832))
([4252fac](4252fac))
* agent performance, activity, and history API endpoints
([#811](#811))
([9b75c1d](9b75c1d))
* Agent Profiles and Detail pages (biography, career, performance)
([#874](#874))
([62d7880](62d7880))
* app shell, Storybook, and CI/CD pipeline
([#819](#819))
([d4dde90](d4dde90))
* Approvals page with risk grouping, urgency indicators, batch actions
([#889](#889))
([4e9673d](4e9673d))
* Budget Panel page (P&L dashboard, breakdown charts, forecast)
([#890](#890))
([b63b0f1](b63b0f1))
* build infrastructure layer (API client, auth, WebSocket)
([#815](#815))
([9f01d3e](9f01d3e))
* CLI global options infrastructure, UI modes, exit codes, env vars
([#891](#891))
([fef4fc5](fef4fc5))
* CodeMirror editor and theme preferences toggle
([#905](#905),
[#807](#807))
([#909](#909))
([41fbedc](41fbedc))
* Company page (department/agent management)
([#888](#888))
([cfb88b0](cfb88b0))
* comprehensive hint coverage across all CLI commands
([#900](#900))
([937974e](937974e))
* config system extensions, per-command flags for
init/start/stop/status/logs
([#895](#895))
([32f83fe](32f83fe))
* configurable currency system replacing hardcoded USD
([#854](#854))
([b372551](b372551))
* Dashboard page (metric cards, activity feed, budget burn)
([#861](#861))
([7d519d5](7d519d5))
* department health, provider status, and activity feed endpoints
([#818](#818))
([6d5f196](6d5f196))
* design tokens and core UI components
([#833](#833))
([ed887f2](ed887f2))
* extend approval, meeting, and budget API responses
([#834](#834))
([31472bf](31472bf))
* frontend polish -- real-time UX, accessibility, responsive,
performance ([#790](#790),
[#792](#792),
[#791](#791),
[#793](#793))
([#917](#917))
([f04a537](f04a537))
* implement human roles and access control levels
([#856](#856))
([d6d8a06](d6d8a06))
* implement semantic conflict detection in workspace merge
([#860](#860))
([d97283b](d97283b))
* interaction components and animation patterns
([#853](#853))
([82d4b01](82d4b01))
* Login page + first-run bootstrap + Company page
([#789](#789),
[#888](#888))
([#896](#896))
([8758e8d](8758e8d))
* Meetings page with timeline viz, token bars, contribution formatting
([#788](#788))
([#904](#904))
([b207f46](b207f46))
* Messages page with threading, channel badges, sender indicators
([#787](#787))
([#903](#903))
([28293ad](28293ad))
* Org Chart force-directed view and drag-drop reassignment
([#872](#872),
[#873](#873))
([#912](#912))
([a68a938](a68a938))
* Org Chart page (living nodes, status, CRUD, department health)
([#870](#870))
([0acbdae](0acbdae))
* per-command flags for remaining commands, auto-behavior wiring,
help/discoverability
([#897](#897))
([3f7afa2](3f7afa2))
* Providers page with backend rework -- health, CRUD, subscription auth
([#893](#893))
([9f8dd98](9f8dd98))
* scaffold React + Vite + TypeScript + Tailwind project
([#799](#799))
([bd151aa](bd151aa))
* Settings page with search, dependency indicators, grouped rendering
([#784](#784))
([#902](#902))
([a7b9870](a7b9870))
* Setup Wizard rebuild with template comparison, cost estimator, theme
customization ([#879](#879))
([ae8b50b](ae8b50b))
* setup wizard UX -- template filters, card metadata, provider form
reuse ([#910](#910))
([7f04676](7f04676))
* setup wizard UX overhaul -- mode choice, step reorder, provider fixes
([#907](#907))
([ee964c4](ee964c4))
* structured ModelRequirement in template agent configs
([#795](#795))
([7433548](7433548))
* Task Board page (rich Kanban, filtering, dependency viz)
([#871](#871))
([04a19b0](04a19b0))


### Bug Fixes

* align frontend types with backend and debounce WS refetches
([#916](#916))
([134c11b](134c11b))
* auto-cleanup targets newly pulled images instead of old ones
([#884](#884))
([50e6591](50e6591))
* correct wipe backup-skip flow and harden error handling
([#808](#808))
([c05860f](c05860f))
* improve provider setup in wizard, subscription auth, dashboard bugs
([#914](#914))
([87bf8e6](87bf8e6))
* improve update channel detection and add config get command
([#814](#814))
([6b137f0](6b137f0))
* resolve all ESLint warnings, add zero-warnings enforcement
([#899](#899))
([079b46a](079b46a))
* subscription auth uses api_key, base URL optional for cloud providers
([#915](#915))
([f0098dd](f0098dd))


### Refactoring

* semantic analyzer cleanup -- shared filtering, concurrency, extraction
([#908](#908))
([81372bf](81372bf))


### Documentation

* brand identity and UX design system from
[#765](#765) exploration
([#804](#804))
([389a9f4](389a9f4))
* page structure and information architecture for v0.5.0 dashboard
([#809](#809))
([f8d6d4a](f8d6d4a))
* write UX design guidelines with WCAG-verified color system
([#816](#816))
([4a4594e](4a4594e))


### Tests

* add unit tests for agent hooks and page components
([#875](#875))
([#901](#901))
([1d81546](1d81546))


### CI/CD

* bump actions/deploy-pages from 4.0.5 to 5.0.0 in the major group
([#831](#831))
([01c19de](01c19de))
* bump astral-sh/setup-uv from 7.6.0 to 8.0.0 in
/.github/actions/setup-python-uv in the all group
([#920](#920))
([5f6ba54](5f6ba54))
* bump codecov/codecov-action from 5.5.3 to 6.0.0 in the major group
([#868](#868))
([f22a181](f22a181))
* bump github/codeql-action from 4.34.1 to 4.35.0 in the all group
([#883](#883))
([87a4890](87a4890))
* bump sigstore/cosign-installer from 4.1.0 to 4.1.1 in the
minor-and-patch group
([#830](#830))
([7a69050](7a69050))
* bump the all group with 3 updates
([#923](#923))
([ff27c8e](ff27c8e))
* bump wrangler from 4.76.0 to 4.77.0 in /.github in the minor-and-patch
group ([#822](#822))
([07d43eb](07d43eb))
* bump wrangler from 4.77.0 to 4.78.0 in /.github in the all group
([#882](#882))
([f84118d](f84118d))


### Maintenance

* add design system enforcement hook and component inventory
([#846](#846))
([15abc43](15abc43))
* add dev-only auth bypass for frontend testing
([#885](#885))
([6cdcd8a](6cdcd8a))
* add pre-push rebase check hook
([#855](#855))
([b637a04](b637a04))
* backend hardening -- eviction/size-caps and model validation
([#911](#911))
([81253d9](81253d9))
* bump axios from 1.13.6 to 1.14.0 in /web in the all group across 1
directory ([#922](#922))
([b1b0232](b1b0232))
* bump brace-expansion from 5.0.4 to 5.0.5 in /web
([#862](#862))
([ba4a565](ba4a565))
* bump eslint-plugin-react-refresh from 0.4.26 to 0.5.2 in /web
([#801](#801))
([7574bb5](7574bb5))
* bump faker from 40.11.0 to 40.11.1 in the minor-and-patch group
([#803](#803))
([14d322e](14d322e))
* bump https://github.com/astral-sh/ruff-pre-commit from v0.15.7 to
0.15.8 ([#864](#864))
([f52901e](f52901e))
* bump nginxinc/nginx-unprivileged from `6582a34` to `f99cc61` in
/docker/web in the all group
([#919](#919))
([df85e4f](df85e4f))
* bump nginxinc/nginx-unprivileged from `ccbac1a` to `6582a34` in
/docker/web ([#800](#800))
([f4e9450](f4e9450))
* bump node from `44bcbf4` to `71be405` in /docker/sandbox
([#827](#827))
([91bec67](91bec67))
* bump node from `5209bca` to `cf38e1f` in /docker/web
([#863](#863))
([66d6043](66d6043))
* bump picomatch in /site
([#842](#842))
([5f20bcc](5f20bcc))
* bump recharts 2-&gt;3 and @types/node 22-&gt;25 in /web
([#802](#802))
([a908800](a908800))
* Bump requests from 2.32.5 to 2.33.0
([#843](#843))
([41daf69](41daf69))
* bump smol-toml from 1.6.0 to 1.6.1 in /site
([#826](#826))
([3e5dbe4](3e5dbe4))
* bump the all group with 3 updates
([#921](#921))
([7bace0b](7bace0b))
* bump the minor-and-patch group across 1 directory with 2 updates
([#829](#829))
([93e611f](93e611f))
* bump the minor-and-patch group across 1 directory with 3 updates
([#841](#841))
([7010c8e](7010c8e))
* bump the minor-and-patch group across 1 directory with 3 updates
([#869](#869))
([548cee5](548cee5))
* bump the minor-and-patch group in /site with 2 updates
([#865](#865))
([9558101](9558101))
* bump the minor-and-patch group with 2 updates
([#867](#867))
([4830706](4830706))
* consolidate Dependabot groups to 1 PR per ecosystem
([06d2556](06d2556))
* consolidate Dependabot groups to 1 PR per ecosystem
([#881](#881))
([06d2556](06d2556))
* improve worktree skill with full dep sync and status enhancements
([#906](#906))
([772c625](772c625))
* remove Vue remnants and document framework decision
([#851](#851))
([bf2adf6](bf2adf6))
* update web dependencies and fix brace-expansion CVE
([#880](#880))
([a7a0ed6](a7a0ed6))
* upgrade to Storybook 10 and TypeScript 6
([#845](#845))
([52d95f2](52d95f2))

---
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

1 participant