Skip to content

feat: Settings page with search, dependency indicators, grouped rendering (#784)#902

Merged
Aureliolo merged 10 commits intomainfrom
feat/settings-page
Mar 28, 2026
Merged

feat: Settings page with search, dependency indicators, grouped rendering (#784)#902
Aureliolo merged 10 commits intomainfrom
feat/settings-page

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Summary

  • Implement full Settings page replacing placeholder stubs with search, collapsible namespace sections, dependency indicators, floating save bar, advanced mode toggle, GUI/Code editor toggle, source/restart badges
  • Expand Zustand settings store with full schema/entries state, batch save with per-key tracking, WebSocket system channel integration
  • Create useSettingsData hook following established useDashboardData pattern (polling + WS + store selectors)
  • Add 14 page-scoped sub-components: NamespaceSection, SettingRow, SettingField (type-aware renderer), SourceBadge, RestartBadge, DependencyIndicator, SearchInput, AdvancedModeBanner, FloatingSaveBar, CodeEditorPanel (JSON/YAML), SettingsSkeleton, shared utils
  • Add dependency map (budget auto-downgrade, backup enabled, security output scanning) with controller-disabled visual state
  • Add Storybook stories for SourceBadge and RestartBadge
  • Update docs/design/page-structure.md to match implementation (collapsible sections layout, _schema endpoint paths)
  • Unit tests for hook (9 tests) and page component (11 tests)

Review coverage

Pre-reviewed by 4 agents (frontend-reviewer, issue-resolution-verifier, docs-consistency, silent-failure-hunter). 14 findings addressed:

  • Extracted duplicated utilities to shared utils.ts
  • Fixed silent error swallowing in refreshEntries (errors now propagate to usePolling)
  • Added nested validation in parseText for namespace values
  • Separated parse/save error handling in CodeEditorPanel
  • Improved batch save to track failed keys and clear only succeeded entries
  • Fixed resetSetting two-step partial failure handling
  • Removed unused namespace prop from NamespaceSection
  • Updated design spec docs to match implementation

Test plan

  • npm --prefix web run lint -- zero warnings
  • npm --prefix web run type-check -- passes
  • npm --prefix web run test -- 1640 tests pass
  • Navigate to /settings -- verify 7 namespace sections with correct icons
  • Click chevron on a section -- verify collapse/expand
  • Toggle "Advanced" -- verify warning dialog, then advanced settings appear with banner
  • Type in search box -- verify real-time filtering
  • Change a setting value -- verify floating save bar appears
  • Click Save -- verify API call, bar disappears, toast
  • Toggle to Code view -- verify JSON/YAML display
  • Navigate to /settings/budget -- verify single-namespace filtered view

Closes #784

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Walkthrough

Replaces the placeholder Settings UI with a full implementation: collapsible, per-namespace sections with grouped rendering and fuzzy search; GUI and Code edit modes (JSON/YAML edited in Code); advanced-mode toggle with a one-time session-confirmation and persisted preference; dependency indicators driven by frontend SETTING_DEPENDENCIES / SETTING_DEPENDED_BY maps with dependents rendered muted when controllers are disabled; dirty-value tracking with batch save/reset and a floating save bar; source/restart badges; new hooks useSettingsData and useSettingsDirtyState; Zustand store now exposes schema/entries/loading/saving actions and WS updates; settings schema endpoints renamed to GET /settings/_schema and GET /settings/_schema/{ns}.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.32% which is insufficient. The required threshold is 40.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main feature additions: Settings page with search, dependency indicators, and grouped rendering.
Description check ✅ Passed The description is comprehensive and clearly relates to the changeset, detailing implementation scope, review findings, and test plan.
Linked Issues check ✅ Passed The PR implementation fully addresses all objectives from issue #784: fuzzy search, grouped namespace sections, dependency indicators, floating save bar, advanced mode with confirmation/banner, dual GUI/Code edit modes, source/restart badges, and unit tests.
Out of Scope Changes check ✅ Passed All code changes are scoped to Settings page implementation and supporting infrastructure; no unrelated modifications detected. Documentation updates appropriately reflect implementation.

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


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

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 15:14 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 28, 2026

Dependency Review

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

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 1488571.
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 settings management system, featuring a main settings page with GUI and Code (JSON/YAML) views, namespace-specific pages, and real-time updates via WebSockets and polling. The review feedback identifies a bug in numeric slider handling for zero values, a potential ReDoS vulnerability in client-side regex validation, and state synchronization issues in the code editor's save logic. Additionally, suggestions were provided to reduce code duplication between pages, simplify redundant loop checks, and refactor repetitive error handling within the Zustand store for better maintainability.

Comment on lines +127 to +132
try {
await onSave(changes)
setDirty(false)
} catch (err) {
setParseError(err instanceof Error ? err.message : 'Failed to save settings')
}
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

There's an issue with the error handling and state update in handleSave:

  1. The try...catch block around await onSave(changes) will likely never catch an error. The onSave prop is implemented as handleCodeSave in SettingsPage.tsx, which uses saveSettingsBatch. saveSettingsBatch uses Promise.allSettled and does not throw; it returns a set of failed keys.
  2. setDirty(false) is called immediately after onSave. If some settings fail to save (i.e., failedKeys.size > 0), the component will still mark the state as not dirty. This is misleading for the user as their unsaved changes will seem to disappear from the UI without being saved.

I suggest modifying the onSave prop to return the set of failed keys and updating the logic here to handle it correctly. For example:

SettingsPage.tsx

const handleCodeSave = useCallback(
  async (changes: Map<string, string>) => {
    const failedKeys = await saveSettingsBatch(changes, updateSetting);
    // ... toast logic ...
    return failedKeys;
  },
  [updateSetting]
);

CodeEditorPanel.tsx

// props
onSave: (changes: Map<string, string>) => Promise<Set<string>>;

// handleSave
const failedKeys = await onSave(changes);
if (failedKeys.size === 0) {
  setDirty(false);
} else {
  setParseError(`${failedKeys.size} setting(s) failed to save. Please review and try again.`);
}

}
if (definition.validator_pattern) {
try {
if (!new RegExp(definition.validator_pattern).test(raw)) // eslint-disable-line security/detect-non-literal-regexp -- pattern from trusted backend schema
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.

security-high high

Using new RegExp() with a pattern sourced from the backend (definition.validator_pattern) can introduce a Regular Expression Denial of Service (ReDoS) vulnerability. If a malicious or poorly-formed regex pattern is provided by the API, it could cause the client's browser to hang.

While the eslint-disable comment indicates awareness and trust in the backend, for enhanced security it's best to avoid constructing regex from external inputs on the client. If this validation is necessary, consider performing it on the backend. Given the potential impact, this is a security risk.

definition.min_value != null &&
definition.max_value != null
) {
const numValue = Number(value) || definition.min_value
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

There is a bug in how the numValue for the slider is calculated. The expression Number(value) || definition.min_value will incorrectly handle a valid setting value of 0. If value is '0', Number(value) becomes 0, and the expression evaluates to definition.min_value, which might be different.

This will cause the UI to display an incorrect value and potentially save it if the user interacts with the slider.

Suggested change
const numValue = Number(value) || definition.min_value
const parsedValue = Number(value)
const numValue = Number.isNaN(parsedValue) ? definition.min_value : parsedValue

Comment on lines +43 to +47
useEffect(() => {
polling.start()
return () => polling.stop()
// eslint-disable-next-line @eslint-react/exhaustive-deps
}, [])
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 useEffect hook for starting and stopping polling has an empty dependency array, suppressed by an eslint-disable comment. While the comment states that polling.start and polling.stop are stable, it's a best practice to include all dependencies to prevent future bugs if the usePolling hook changes. This also improves code clarity and adheres to the rules of hooks.

  useEffect(() => {
    polling.start()
    return () => polling.stop()
  }, [polling.start, polling.stop])

Comment on lines +108 to +127
const handleSave = useCallback(async () => {
const failedKeys = await saveSettingsBatch(dirtyValues, updateSetting)
if (failedKeys.size === 0) {
setDirtyValues(new Map())
useToastStore.getState().add({ variant: 'success', title: 'Settings saved' })
} else {
// Clear only succeeded entries from dirty values
setDirtyValues((prev) => {
const next = new Map<string, string>()
for (const [k, v] of prev) {
if (failedKeys.has(k)) next.set(k, v)
}
return next
})
useToastStore.getState().add({
variant: 'error',
title: `${failedKeys.size} setting(s) failed to save`,
})
}
}, [dirtyValues, updateSetting])
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 handleSave function is nearly identical to the one in SettingsNamespacePage.tsx. This code duplication can make future maintenance more difficult, as changes would need to be applied in two places. Consider extracting this logic into a shared utility function.

Comment on lines +60 to +65
for (let i = 0; i < results.length; i++) {
const result = results[i]
const key = keys[i]
if (result && key && result.status === 'rejected') {
failedKeys.add(key)
}
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 loop for collecting failed keys can be simplified. The checks for result && key are redundant because results and keys arrays will always have the same length, and their elements will be defined.

  for (let i = 0; i < results.length; i++) {
    if (results[i].status === 'rejected') {
      failedKeys.add(keys[i])
    }
  }

Comment on lines +142 to +159
// Reset succeeded -- refetch entries to get the resolved default.
// A refetch failure here means the reset applied but the UI is stale.
try {
const entries = await settingsApi.getAllSettings()
set((state) => {
const newSaving = new Set(state.savingKeys)
newSaving.delete(compositeKey)
return { entries, savingKeys: newSaving }
})
} catch {
// Reset applied but refetch failed -- clear saving state, entries will
// be refreshed by the next poll cycle.
set((state) => {
const newSaving = new Set(state.savingKeys)
newSaving.delete(compositeKey)
return { savingKeys: newSaving }
})
}
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 error handling logic for refetching entries after a successful resetSetting API call is a bit repetitive. The code to update savingKeys is duplicated in both the try and catch blocks. This can be refactored for better readability and maintainability.

    // Reset succeeded -- refetch entries to get the resolved default.
    let refreshedEntries: SettingEntry[] | undefined;
    try {
      refreshedEntries = await settingsApi.getAllSettings();
    } catch (error) {
      // A refetch failure here means the reset applied but the UI is stale.
      // It will be refreshed by the next poll cycle.
      console.warn('Setting reset succeeded, but entry refresh failed:', getErrorMessage(error));
    } finally {
      set((state) => {
        const newSaving = new Set(state.savingKeys)
        newSaving.delete(compositeKey)
        const newState: Partial<SettingsState> = { savingKeys: newSaving }
        if (refreshedEntries) {
          newState.entries = refreshedEntries
        }
        return newState
      })
    }

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

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

Inline comments:
In `@web/src/pages/settings/CodeEditorPanel.tsx`:
- Around line 144-165: The two toggle buttons in CodeEditorPanel are building
conditional className strings with template literals; import cn from
'@/lib/utils' and replace those template literals with cn(...) calls so class
merging follows the repo convention: update the JSON and YAML button className
props to use cn(...) combining the shared classes and the conditional classes
based on the format variable, and ensure the component imports cn and continues
to call handleFormatChange('json') / handleFormatChange('yaml') as before.
- Around line 109-123: The diff currently only iterates parsed and misses
deletions in original (variables: parsed, original, changes), which lets pure
deletes clear dirty without persisting removals; update the save logic in
CodeEditorPanel.tsx to also iterate original namespaces/keys and detect keys
absent from parsed, then either add entries to changes that express a
reset/delete (e.g., map `${ns}/${key}` to a deletion marker/null) or
short-circuit and reject the save with an error prompting deletion support;
ensure setDirty(false) only runs after deletions are handled and keep the same
change-key format so downstream apply/save code can handle resets.

In `@web/src/pages/settings/FloatingSaveBar.tsx`:
- Around line 34-36: The save error message rendered from saveError in
FloatingSaveBar.tsx isn't in a live region; update the element that renders
{saveError} (the span currently with className="text-xs text-danger") to be
announced by screen readers by adding an appropriate live-region attribute and
role (e.g., aria-live="assertive" and role="alert"), keeping the same
conditional rendering and visual styling so the message is voiced immediately
when saveError appears.

In `@web/src/pages/settings/RestartBadge.tsx`:
- Around line 10-19: The page-local RestartBadge span in RestartBadge.tsx (the
element using cn and the RotateCcw icon) should be replaced with the shared
badge primitive (e.g., import and use the shared StatusBadge/Badge component
from the ui components) so we reuse existing styling and behavior; update
RestartBadge to render the shared component, pass through className and title
props, include the RotateCcw icon as its leading child, and remove the hardcoded
span markup so the badge matches other UI badges.

In `@web/src/pages/settings/SearchInput.tsx`:
- Around line 40-43: The clear handler (handleClear) must cancel any pending
debounce before resetting state to avoid a stale callback re-applying the old
query; update handleClear to first cancel the debounce (e.g., call
debouncedOnChange.cancel() if you're using lodash.debounce or
clearTimeout(debounceTimerRef.current) if you store a timer ref), then call
setLocal('') and onChange(''); reference the existing handleClear function and
the debounced function/timer (debouncedOnChange or debounceTimerRef) to locate
where to add the cancel call.

In `@web/src/pages/settings/SettingField.tsx`:
- Around line 130-145: The array textarea is being serialized on every keystroke
which trims blank lines and breaks typing; change the isArraySetting branch in
SettingField (where parseArrayValue and serializeArrayValue are used) to use a
local draft state for the textarea value, update that draft on every onChange of
the InputField (and clear validationError via setValidationError), and only call
serializeArrayValue and the external onChange when the field is committed (e.g.,
onBlur or onSave). Keep parseArrayValue for initial draft population and ensure
disabled and error props still flow to InputField.
- Around line 115-116: The calculation for numValue uses the || operator which
treats 0 as falsy and replaces a valid "0" with definition.min_value; change the
logic that sets numValue (the variable assigned from Number(value) ||
definition.min_value) to explicitly check for missing/empty input (e.g., value
=== '' || value == null) and only then use definition.min_value, otherwise use
Number(value) — keep the step logic (step = definition.type === 'int' ? 1 : 0.1)
unchanged.

In `@web/src/pages/settings/SourceBadge.tsx`:
- Around line 23-33: The SourceBadge component duplicates inline badge markup
and styling; replace the span with the shared badge primitive (e.g., the shared
Badge/StatusBadge component from the ui library) instead of rolling custom
markup: import the shared badge component into SourceBadge, pass through
className, map SOURCE_STYLES[source] to the badge's variant/class prop, and
render SOURCE_LABELS[source] as the badge children; remove the duplicated inline
styles and ensure SourceBadge continues to accept SourceBadgeProps so
theming/spacing from the shared component is used consistently.

In `@web/src/pages/settings/utils.ts`:
- Around line 5-13: matchesSetting currently uses plain substring checks which
don't support typo-tolerant fuzzy matching; replace the includes checks in
matchesSetting(entry: SettingEntry, query: string) with a fuzzy-match approach
(for example, integrate a lightweight fuzzy-matching helper or a library like
Fuse.js) and run the fuzzy matcher against def.key, def.description,
def.namespace, and def.group; ensure the new logic normalizes case (lowercase)
and returns true if any field scores above the chosen threshold or matches per
the fuzzy helper, keeping the function signature and using SettingEntry and
entry.definition to locate the fields to test.

In `@web/src/pages/SettingsNamespacePage.tsx`:
- Around line 62-68: handleValueChange currently always sets the compositeKey in
dirtyValues which leaves reverted fields marked dirty and causes redundant
saves; update the useCallback for handleValueChange to compare the new value
against the persisted value from entries (e.g., entries.get(compositeKey) or
however persisted values are stored) and if they match, remove the key from the
copied Map (next.delete(compositeKey)), otherwise set it (next.set(compositeKey,
value)), then return next; keep the Map copy pattern currently used to avoid
mutating state directly.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 129-141: The code path for saving from Code mode doesn't reconcile
GUI drafts so handleCodeSave should update the in-memory GUI drafts/state after
a successful save: after await saveSettingsBatch(changes, updateSetting)
returns, for each key in changes that succeeded (i.e., not in failedKeys) update
or remove that key from dirtyValues and update entries/codeEntries accordingly
so the GUI reflects the just-saved values; locate handleCodeSave,
saveSettingsBatch, updateSetting, codeEntries, entries and dirtyValues and
ensure successful keys either overwrite dirtyValues or clear their draft entry
so GUI → Code → GUI shows the saved value (also add a regression test for the
GUI→Code→GUI flow).
- Around line 108-127: handleSave currently replaces the entire live dirtyValues
map after the async batch finishes, which wipes drafts added while the batch was
in flight; fix by snapshotting the keys you attempted before awaiting (e.g.,
const attemptedKeys = new Set(dirtyValues.keys())) and then use setDirtyValues
functional updaters that only remove or keep keys from that attemptedKeys set:
on full success, remove only attemptedKeys from the existing prev map; on
partial failure, remove attemptedKeys \ failedKeys and keep both failedKeys and
any keys in prev that were not in attemptedKeys (so new drafts added during the
request are preserved). Reference: handleSave, saveSettingsBatch,
setDirtyValues, dirtyValues, failedKeys.
- Around line 96-102: handleValueChange currently always adds compositeKey to
dirtyValues; change its updater to compare the new value against the persisted
value and remove the draft when they match. In the setDirtyValues callback for
handleValueChange, create a copy of prev (new Map(prev)), look up the current
persisted value for that compositeKey from the component's persisted state
(e.g., the settings/persistedValues source you use elsewhere), and if value ===
persistedValue call next.delete(compositeKey) else next.set(compositeKey,
value); then return next. This keeps the save bar accurate and avoids no-op
writes.

In `@web/src/stores/settings.ts`:
- Around line 162-166: The WebSocket handler updateFromWsEvent currently calls
the async get().refreshEntries() and drops the returned promise, causing
potential unhandled rejections; update the call so the promise is observed and
failures are handled (e.g., attach a .catch handler that logs the error or
otherwise handles it) when event.channel === 'system' inside updateFromWsEvent
to ensure any GET failure from refreshEntries() is caught and dealt with.

In `@web/src/utils/constants.ts`:
- Around line 118-122: The reverse-map construction using Object.fromEntries
silently overwrites duplicates; change the SETTING_DEPENDED_BY build to iterate
SETTING_DEPENDENCIES entries and guard against duplicate dependent keys by
tracking seen deps and throwing a clear error (or assert) when a dep is already
mapped to a different controller; i.e., replace the Object.fromEntries approach
with a loop/reduce that checks each dep and fails fast in development if a
conflict is detected so you can locate the problematic key and controllers.
🪄 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: 23bf3224-0560-44bd-9c80-232cb02a7407

📥 Commits

Reviewing files that changed from the base of the PR and between 937974e and 86025cb.

📒 Files selected for processing (22)
  • docs/design/page-structure.md
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/utils.ts
  • web/src/stores/settings.ts
  • web/src/utils/constants.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 (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/utils/constants.ts
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/utils.ts
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/stores/settings.ts
  • web/src/pages/SettingsPage.tsx
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/SettingsPage.tsx
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/utils/constants.ts
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/utils.ts
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/stores/settings.ts
  • web/src/pages/SettingsPage.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/utils/constants.ts
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/utils.ts
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/stores/settings.ts
  • web/src/pages/SettingsPage.tsx
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/SettingsPage.tsx
docs/design/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

When an implementation deviates from the spec (better approach found, scope evolved, etc.), update the relevant docs/design/ page to reflect the new reality

Files:

  • docs/design/page-structure.md
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Documentation must be written in Markdown and built with Zensical (config: mkdocs.yml); design spec lives in docs/design/ (11 pages linked from docs/DESIGN_SPEC.md)

Files:

  • docs/design/page-structure.md
🧠 Learnings (13)
📓 Common learnings
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SourceBadge.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export component props as a TypeScript interface

Applied to files:

  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SettingField.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__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • web/src/utils/constants.ts
  • docs/design/page-structure.md
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in components

Applied to files:

  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SourceBadge.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to docs/design/**/*.md : When an implementation deviates from the spec (better approach found, scope evolved, etc.), update the relevant `docs/design/` page to reflect the new reality

Applied to files:

  • docs/design/page-structure.md
📚 Learning: 2026-03-18T08:23:08.912Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T08:23:08.912Z
Learning: When approved deviations occur, update the relevant `docs/design/` page to reflect the new reality.

Applied to files:

  • docs/design/page-structure.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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • docs/design/page-structure.md
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/stores/settings.ts
  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.

Applied to files:

  • docs/design/page-structure.md
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Do NOT recreate complex (>8 line) JSX inside `.map()` blocks; extract to a shared component in `web/src/components/ui/`

Applied to files:

  • web/src/pages/settings/SearchInput.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design token variables exclusively for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); never hardcode shadow or border values

Applied to files:

  • web/src/pages/settings/SourceBadge.tsx
🔇 Additional comments (3)
docs/design/page-structure.md (1)

110-116: Design-spec sync is correct.

The updated Settings IA text and renamed schema endpoints are aligned with the implemented page behavior and API surface.

web/src/pages/settings/NamespaceSection.tsx (1)

1-1: Import ReactNode explicitly here.

icon: React.ReactNode relies on the React namespace being in scope, but this module only imports useState. In the current React/TypeScript setup that usually type-checks only if the type is imported explicitly.

Also applies to: 8-10

web/src/pages/settings/CodeEditorPanel.tsx (1)

1-1: Import ChangeEvent instead of using React.ChangeEvent.

This module never imports the React namespace, so the annotation currently depends on a global that is usually unavailable with the automatic JSX runtime.

Also applies to: 91-95

Comment on lines +10 to +19
<span
className={cn(
'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium bg-warning/10 text-warning',
className,
)}
title="Changes to this setting require a restart to take effect"
>
<RotateCcw className="size-2.5" aria-hidden />
Restart
</span>
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

Reuse shared badge primitives instead of introducing a page-local badge implementation.

This badge is implemented as a custom <span> even though it fits the shared status/badge UI pattern and should come from web/src/components/ui/ for consistency and reuse.

As per coding guidelines: “ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard).”

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

In `@web/src/pages/settings/RestartBadge.tsx` around lines 10 - 19, The page-local
RestartBadge span in RestartBadge.tsx (the element using cn and the RotateCcw
icon) should be replaced with the shared badge primitive (e.g., import and use
the shared StatusBadge/Badge component from the ui components) so we reuse
existing styling and behavior; update RestartBadge to render the shared
component, pass through className and title props, include the RotateCcw icon as
its leading child, and remove the hardcoded span markup so the badge matches
other UI badges.

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

🤖 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__/hooks/useSettingsData.test.ts`:
- Around line 116-123: The test accesses vi.mocked(useWebSocket).mock.calls[0]!
without asserting the mock was called first, which can yield an unclear runtime
error; add an explicit guard such as expect(useWebSocket).toHaveBeenCalled() or
expect(vi.mocked(useWebSocket).mock.calls.length).toBeGreaterThan(0) before
reading mock.calls, then assign callArgs from mock.calls[0][0] (or mock.calls[0]
depending on shape) and proceed to check callArgs.bindings — reference the
useWebSocket mock and the callArgs variable in useSettingsData.test.ts when
making this change.

In `@web/src/__tests__/pages/SettingsPage.test.tsx`:
- Around line 101-104: Add a short explanatory comment above the vi.mock
declaration that documents why the dynamic key pattern is used (i.e., why
useSettingsData is returned via {[hookName]: ...} instead of a direct property)
and reference the linting/bundling issue or ticket that necessitates it; ensure
the comment mentions the hook name useSettingsData and the helper
getSettingsData so future maintainers understand the rationale and can safely
refactor to return { useSettingsData: () => getSettingsData() } if that
constraint is resolved.

In `@web/src/pages/settings/NamespaceSection.tsx`:
- Around line 52-57: The long className string on the button (the element using
setCollapsed, isOpen and contentId) exceeds the 88-character limit; split or
refactor it so no line is longer than 88 chars — e.g., break the className into
multiple concatenated strings or use a utility like clsx/array join to list
classes on separate lines (preserving the same classes: "flex w-full
items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-card-hover")
and keep the JSX attributes (onClick, aria-expanded, aria-controls) on their own
lines for readability.

In `@web/src/pages/settings/SearchInput.tsx`:
- Around line 17-22: The current prop synchronization updates local state during
render using prevValueRef and setLocal; move this logic into a useEffect to
preserve render purity: create or reuse prevValueRef and inside a useEffect that
depends on value compare value to prevValueRef.current, update
prevValueRef.current and call setLocal(value) there (instead of performing those
updates during render) so SearchInput's local state syncs when the prop changes
without causing render-time side effects.

In `@web/src/pages/settings/SettingField.tsx`:
- Around line 96-103: The long single-line RegExp test with the inline
eslint-disable comment in SettingField.tsx exceeds the 88-char limit; split the
expression and the comment across multiple lines so the eslint-disable-line
security/detect-non-literal-regexp comment remains attached to the RegExp
construction but the .test(raw) call is on the next line (e.g., assign new
RegExp(definition.validator_pattern) to a local variable or break after the
RegExp call) and preserve the try/catch and warning using the same symbols
(definition.validator_pattern, new RegExp(...), .test(raw), and the
console.warn) so behavior is unchanged while satisfying line-length rules.

In `@web/src/pages/SettingsNamespacePage.tsx`:
- Around line 154-159: The error display JSX in SettingsNamespacePage renders a
div with a very long className string (the block that checks the error variable
and returns the div with AlertTriangle), which exceeds the 88-char line limit;
fix by breaking the className into multiple concatenated strings or use a
classnames/style utility (e.g., classNames(...)) so each source line stays under
88 chars, keeping the same classes ("flex items-center gap-2 rounded-lg",
"border border-danger/30 bg-danger/5", "px-4 py-2 text-sm text-danger") and
preserving the conditional rendering of error and the AlertTriangle element.
- Around line 161-166: The long className string on the warning div in
SettingsNamespacePage.tsx exceeds the 88-char limit; refactor the JSX to use the
cn() (or classNames) helper to break the classes across multiple lines and keep
the string under the limit—locate the conditional rendering block using
wsConnected, loading, and wsSetupError (the div containing WifiOff) and replace
the inline long className with cn('flex', 'items-center', 'gap-2', 'rounded-lg',
'border', 'border-warning/30', 'bg-warning/5', 'px-4', 'py-2', 'text-sm',
'text-warning') split across lines for readability.

In `@web/src/stores/settings.ts`:
- Around line 61-63: The console.warn call in the catch block for fetching
currency settings is over the 88-char limit; modify the catch (error) handler so
it breaks the long message into shorter parts—for example call
console.warn('[settings] Failed to fetch currency setting, keeping default:',
getErrorMessage(error)) using multiple arguments or split the string into two
shorter strings or a template split across concatenation; update the catch block
where getErrorMessage and console.warn are used to ensure the line length is
under 88 characters.
🪄 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: a488c98f-2486-4b42-be1e-f081b5fb3e54

📥 Commits

Reviewing files that changed from the base of the PR and between 86025cb and 2c7163a.

📒 Files selected for processing (23)
  • CLAUDE.md
  • docs/design/page-structure.md
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/utils.ts
  • web/src/stores/settings.ts
  • web/src/utils/constants.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 Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Build Web
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Documentation must be written in Markdown and built with Zensical (config: mkdocs.yml); design spec lives in docs/design/ (11 pages linked from docs/DESIGN_SPEC.md)

Files:

  • CLAUDE.md
  • docs/design/page-structure.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/utils/constants.ts
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/utils.ts
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/SettingField.tsx
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/utils/constants.ts
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/utils.ts
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/utils/constants.ts
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/utils.ts
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/SettingField.tsx
docs/design/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

When an implementation deviates from the spec (better approach found, scope evolved, etc.), update the relevant docs/design/ page to reflect the new reality

Files:

  • docs/design/page-structure.md
🧠 Learnings (20)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/pages/**/*.tsx : Lazy-load page components and place page-scoped sub-components in `pages/<page-name>/` subdirectories (e.g., `pages/tasks/`, `pages/org-edit/`)

Applied to files:

  • CLAUDE.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:

  • 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
📚 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-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.

Applied to files:

  • CLAUDE.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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Do NOT recreate complex (>8 line) JSX inside `.map()` blocks; extract to a shared component in `web/src/components/ui/`

Applied to files:

  • CLAUDE.md
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SourceBadge.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SourceBadge.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export component props as a TypeScript interface

Applied to files:

  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/NamespaceSection.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • web/src/utils/constants.ts
  • docs/design/page-structure.md
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in components

Applied to files:

  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/CodeEditorPanel.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__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design token variables exclusively for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); never hardcode shadow or border values

Applied to files:

  • web/src/pages/settings/SourceBadge.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to docs/design/**/*.md : When an implementation deviates from the spec (better approach found, scope evolved, etc.), update the relevant `docs/design/` page to reflect the new reality

Applied to files:

  • docs/design/page-structure.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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • docs/design/page-structure.md
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.

Applied to files:

  • docs/design/page-structure.md
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`) or CSS variables (`var(--so-accent)`); NEVER hardcode hex values or rgba() in .tsx files

Applied to files:

  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{ts,tsx} : Use camelCase for variable names and function identifiers

Applied to files:

  • web/src/pages/settings/CodeEditorPanel.tsx
🔇 Additional comments (32)
CLAUDE.md (1)

219-219: Looks good.

The settings/ example in the page-scoped subdirectory note is clear and aligns with the new Settings page structure.

web/src/pages/settings/SettingsSkeleton.tsx (1)

1-16: Solid loading skeleton implementation.

Good reuse of SkeletonCard plus accessible loading semantics for screen readers.

web/src/pages/settings/AdvancedModeBanner.tsx (1)

4-23: Banner component is clean and well-structured.

Prop typing, warning copy, and disable action wiring all look correct.

web/src/utils/constants.ts (1)

111-132: Dependency constants are robustly implemented.

The reverse-map duplicate check is a strong safety guard, and the polling interval constant is clear.

web/src/pages/settings/SourceBadge.tsx (1)

4-35: Nice typed mapping for source variants.

Using Record<SettingSource, string> for styles/labels gives good compile-time exhaustiveness.

web/src/pages/settings/FloatingSaveBar.tsx (1)

21-56: Well-executed save bar behavior.

Conditional rendering, loading state handling, and live error announcement are all implemented correctly.

web/src/pages/settings/SourceBadge.stories.tsx (1)

13-28: Story coverage is good here.

You covered each source variant plus a consolidated “all sources” state, which is useful for visual validation.

web/src/pages/settings/CodeEditorPanel.tsx (2)

63-71: Render-time state sync pattern is acceptable but unconventional.

The sync logic that updates text when entries changes runs during render rather than in a useEffect. This works because it only updates state when the reference changes and the component isn't dirty, but it's an unconventional pattern that may surprise other developers.

This pattern is valid in React 18+ and avoids an extra render cycle compared to useEffect, so no change is strictly required. Consider adding a brief comment explaining the intent if you keep it.


139-147: LGTM on the diff logic.

The change detection correctly compares stringified values against the original entries and builds the changes map. The JSON.stringify fallback for non-string values (line 142) aligns with the SettingEntry.value type being string.

web/src/pages/settings/RestartBadge.stories.tsx (1)

1-13: LGTM.

The Storybook story is appropriately minimal for this stateless badge component. Since RestartBadge has no loading, error, or empty states, a single Default story provides sufficient coverage.

web/src/__tests__/hooks/useSettingsData.test.ts (1)

51-142: LGTM on test coverage.

The test suite comprehensively covers the hook's core responsibilities: initial fetch, state derivation (loading, entries, error, saving), WebSocket channel binding, and polling lifecycle. The mock isolation pattern with resetStore() is clean.

web/src/pages/settings/DependencyIndicator.tsx (1)

10-24: LGTM.

Clean implementation with proper accessibility support (screen-reader-only text duplicating the tooltip) and an early return for empty dependents. The component follows project conventions.

docs/design/page-structure.md (1)

108-122: LGTM.

Documentation accurately reflects the implementation: collapsible namespace sections, advanced mode UX flow with session/localStorage persistence, frontend-maintained dependency map, and the renamed _schema endpoints. The relevant code snippets confirm the API client already uses the correct /settings/_schema path.

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

123-200: LGTM on test coverage.

The test suite provides solid coverage of UI rendering states: loading skeleton, namespace sections, error/warning banners, search controls, toggles, advanced mode filtering, and empty state. The beforeEach cleanup of storage ensures proper test isolation.

Consider adding interaction tests (clicking toggles, save/discard) in a follow-up to complete the coverage.

web/src/pages/SettingsPage.tsx (4)

96-117: LGTM — Draft cleanup logic correctly implemented.

The persistedValues memo and the updated handleValueChange properly remove drafts when the user reverts to the persisted value, keeping the save bar accurate and avoiding no-op writes.


123-150: LGTM — Concurrent edit handling correctly implemented.

The handleSave now snapshots dirtyValues into pending before the async call and only removes keys from dirtyValues that both succeeded and still match the pending value. This preserves any edits made while the save was in flight.


152-184: LGTM — Code/GUI mode synchronization correctly implemented.

handleCodeSave reconciles dirtyValues after code-mode saves, and codeEntries overlays dirty GUI values so Code mode sees unsaved GUI edits. This prevents the stale-draft bug identified in prior review.

Also applies to: 210-221


265-268: Verify FloatingSaveBar is hidden in code mode.

The FloatingSaveBar is rendered only in GUI mode (inside the else branch starting at line 269), which is correct since CodeEditorPanel has its own save controls. Just confirming this is intentional.

web/src/pages/settings/RestartBadge.tsx (1)

8-21: No rewrite needed. The existing page-scoped RestartBadge component is appropriately implemented. The shared badge components (StatusBadge, ProviderHealthBadge) serve as status indicators with colored dots, not icon-based warning badges. Since no suitable shared component exists for this specific icon + text + warning pattern, the current page-scoped implementation in web/src/pages/settings/ is correct.

web/src/pages/settings/SettingRow.tsx (1)

1-94: LGTM!

Well-structured component with proper separation of concerns. The composite key pattern (namespace/key) is consistently used for dependency lookups against SETTING_DEPENDENCIES and SETTING_DEPENDED_BY. The conditional rendering for env-locked, controller-disabled, and security-sensitive states provides clear user feedback.

web/src/hooks/useSettingsData.ts (1)

1-78: LGTM!

The hook correctly orchestrates initial fetch, polling, and WebSocket subscriptions following the established patterns from useDashboardData. The useCallback/useMemo usage properly stabilizes references, and accessing the store via getState() in handlers avoids stale closure issues.

web/src/pages/SettingsNamespacePage.tsx (1)

24-202: Overall implementation looks solid.

The page correctly handles namespace validation, dirty-value tracking (with the fix for reverted values), batch saving with partial failure handling, and appropriate loading/error/disconnected states. The integration with useSettingsData and the utility functions is clean.

web/src/pages/settings/NamespaceSection.tsx (1)

8-101: Well-structured collapsible section component.

The component properly groups entries by definition.group, handles the collapse/expand state with forceOpen override, and wraps each SettingRow in an ErrorBoundary for graceful error handling. The ARIA attributes (aria-expanded, aria-controls) provide good accessibility.

web/src/pages/settings/utils.ts (3)

4-28: Fuzzy search implementation looks good.

The subsequence matching algorithm correctly implements the PR requirement for fuzzy search across settings. It handles empty queries gracefully and searches across key, description, namespace, and group fields.


34-48: Controller-disabled logic correctly prioritizes dirty values.

The function properly checks dirtyValues first before falling back to persisted entries, ensuring the UI reflects unsaved changes immediately. Returning false when the controller entry is missing is a safe default.


66-88: Batch save correctly handles partial failures.

Using Promise.allSettled ensures all saves are attempted regardless of individual failures, and the returned failedKeys set allows the caller to provide appropriate user feedback.

web/src/pages/settings/SettingField.tsx (3)

36-71: Array setting now uses local draft correctly.

The ArraySettingField component maintains a local draft state and only serializes on blur, addressing the previous concern about keystroke-by-keystroke serialization disrupting user input.


144-164: Slider correctly preserves zero values.

Using Number.isNaN(parsedValue) instead of the falsy-check pattern ensures that "0" renders as 0, not the minimum value.


1-219: Comprehensive field rendering implementation.

The component handles all setting types (bool, enum, numeric with slider, array, JSON, and text variants) with appropriate validation and error display. The conditional rendering branches are well-organized and the validation logic is thorough.

web/src/stores/settings.ts (3)

160-166: WebSocket event handler now properly handles the promise.

The void prefix with .catch() correctly handles the async refresh and logs any failures without leaving an unhandled rejection.


66-91: Robust parallel fetch with partial failure handling.

Using Promise.allSettled ensures both requests complete before processing, and preserving existing state on partial failures provides a good user experience when only one endpoint fails.


126-158: Reset flow handles edge cases gracefully.

The implementation properly tracks saving state, refetches entries to get the resolved default after reset, and gracefully handles refetch failures by logging a warning and relying on the next poll cycle to update the UI.

Comment on lines +17 to +22
// Sync external value changes (e.g. clearing from parent)
const prevValueRef = useRef(value)
if (value !== prevValueRef.current) {
prevValueRef.current = value
setLocal(value)
}
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:

find web/src/pages/settings -name "SearchInput.tsx" -type f

Repository: Aureliolo/synthorg

Length of output: 100


🏁 Script executed:

cat -n web/src/pages/settings/SearchInput.tsx

Repository: Aureliolo/synthorg

Length of output: 2750


Move state update to effect for prop synchronization.

Lines 19–22 update state during render, which breaks render purity and causes unnecessary re-renders in Strict Mode. Sync external value changes using useEffect instead.

🛠️ Proposed fix
 export function SearchInput({ value, onChange, className }: SearchInputProps) {
   const [local, setLocal] = useState(value)
   const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
 
-  // Sync external value changes (e.g. clearing from parent)
-  const prevValueRef = useRef(value)
-  if (value !== prevValueRef.current) {
-    prevValueRef.current = value
-    setLocal(value)
-  }
+  // Sync external value changes (e.g. clearing from parent)
+  useEffect(() => {
+    setLocal(value)
+  }, [value])
📝 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
// Sync external value changes (e.g. clearing from parent)
const prevValueRef = useRef(value)
if (value !== prevValueRef.current) {
prevValueRef.current = value
setLocal(value)
}
export function SearchInput({ value, onChange, className }: SearchInputProps) {
const [local, setLocal] = useState(value)
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Sync external value changes (e.g. clearing from parent)
useEffect(() => {
setLocal(value)
}, [value])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/SearchInput.tsx` around lines 17 - 22, The current
prop synchronization updates local state during render using prevValueRef and
setLocal; move this logic into a useEffect to preserve render purity: create or
reuse prevValueRef and inside a useEffect that depends on value compare value to
prevValueRef.current, update prevValueRef.current and call setLocal(value) there
(instead of performing those updates during render) so SearchInput's local state
syncs when the prop changes without causing render-time side effects.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 16:00 — 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: 6

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

Inline comments:
In `@web/src/pages/settings/NamespaceSection.tsx`:
- Around line 45-60: The button's onClick currently toggles collapsed even when
forceOpen is true, causing hidden state to replay later; change the click
handler on the section header so it only calls setCollapsed when forceOpen is
false (i.e., guard the toggle with if (!forceOpen) ...), referencing the
collapsed, setCollapsed, forceOpen and isOpen variables so the section cannot be
mutated while force-opened.

In `@web/src/pages/settings/SettingField.tsx`:
- Around line 80-94: The numeric validation currently coerces raw to Number (in
the branches checking definition.type === 'int' and === 'float'), but Number('')
=== 0 so empty input incorrectly passes; update both branches to first treat
blank/whitespace-only raw as invalid (e.g., if raw.trim() === '' return an
appropriate message like 'Required' or 'Must be a number' for floats and 'Must
be an integer' for ints) before calling Number(raw), then keep the existing
integer/NaN/min/max checks; target the checks around the raw -> Number(raw)
conversion in SettingField.tsx.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 265-270: When Advanced mode is disabled via the AdvancedModeBanner
onDisable callback (currently calling setAdvancedMode(false) and
localStorage.setItem(SETTINGS_ADVANCED_KEY, 'false')), clear or resolve any
unsaved advanced-only drafts so they don't remain hidden but still count for
submission; specifically, filter the dirtyValues state to remove entries whose
keys are advanced-only (use the existing predicate or list that identifies
advanced settings, e.g., isAdvancedKey or SETTINGS_ADVANCED_KEYS) and update the
dirtyValues (and any derived state used by the floating save bar) before or
after calling setAdvancedMode so the hidden drafts are no longer shown or
submittable.
- Around line 210-223: codeEntries is built from entries but never filtered by
searchQuery, so SearchInput appears but has no effect in Code mode; either apply
the same search filtering to codeEntries or hide SearchInput when viewMode ===
'code'. Update the code that builds codeEntries (the map/filter block
referencing entries, HIDDEN_SETTINGS, advancedMode, NAMESPACE_ORDER) to also
filter by the current searchQuery (matching against definition.key,
definition.namespace and any displayed label), or alternatively conditionally
render the SearchInput when viewMode !== 'code' (the component named
SearchInput) so the control is hidden in code view; make the same change for the
other code-mode list logic mentioned (the corresponding block handling the
code-mode entries).

In `@web/src/stores/settings.ts`:
- Around line 96-100: refreshEntries currently overwrites state with whichever
settingsApi.getAllSettings() resolves last, clobbering newer local changes from
updateSetting/resetSetting; fix it by making refreshEntries ignore stale
responses—e.g., generate and store a unique requestId or timestamp in the
settings store before awaiting settingsApi.getAllSettings(), then after the
await compare that id/timestamp to the current store value and only call set({
entries }) if they match; alternatively merge the incoming entries with existing
getState().entries by only replacing entries whose remote updatedAt is newer
than the local one—refer to refreshEntries, settingsApi.getAllSettings,
updateSetting, resetSetting, and set to implement this guard.
- Around line 102-159: When updating or resetting the budget/currency setting,
also update the store's standalone currency field so useSettingsStore((s) =>
s.currency) stays in sync: in updateSetting (after receiving updated) derive the
new currency from the updated setting (same logic used in fetchCurrency/refresh)
and include currency in the set(...) return object; in resetSetting, when
refreshedEntries is available derive the currency from refreshedEntries and
include it in the final set(...) update; reuse the same derivation helper (e.g.,
fetchCurrency or the internal deriveCurrency logic) rather than duplicating
parsing logic.
🪄 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: 43112271-6f7b-4184-813d-be0a97762385

📥 Commits

Reviewing files that changed from the base of the PR and between 2c7163a and c66c715.

📒 Files selected for processing (7)
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.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 Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
  • web/src/pages/SettingsPage.tsx
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
  • web/src/pages/SettingsPage.tsx
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
  • web/src/pages/SettingsPage.tsx
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/SettingsPage.tsx
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/SettingsPage.tsx
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export component props as a TypeScript interface

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Do NOT recreate complex (>8 line) JSX inside `.map()` blocks; extract to a shared component in `web/src/components/ui/`

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{py,ts,tsx,go} : Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`) or CSS variables (`var(--so-accent)`); NEVER hardcode hex values or rgba() in .tsx files

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in components

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/stores/settings.ts
📚 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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/stores/settings.ts
  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx

@Aureliolo Aureliolo force-pushed the feat/settings-page branch from c66c715 to dd478d1 Compare March 28, 2026 16:16
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 16: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: 4

♻️ Duplicate comments (6)
web/src/pages/settings/SearchInput.tsx (1)

18-22: ⚠️ Potential issue | 🟠 Major

Move prop synchronization out of render.

Line 19 to Line 22 updates state during render (setLocal(value)), which can trigger unnecessary re-renders and violates render purity.

🛠️ Proposed fix
 export function SearchInput({ value, onChange, className }: SearchInputProps) {
   const [local, setLocal] = useState(value)
   const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

-  // Sync external value changes (e.g. clearing from parent)
-  const prevValueRef = useRef(value)
-  if (value !== prevValueRef.current) {
-    prevValueRef.current = value
-    setLocal(value)
-  }
+  // Sync external value changes (e.g. clearing from parent)
+  useEffect(() => {
+    setLocal(value)
+  }, [value])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/SearchInput.tsx` around lines 18 - 22, The component
currently mutates prevValueRef and calls setLocal(value) during render (see
prevValueRef, useRef, setLocal, value), which violates render purity; move the
prop-to-state synchronization into a useEffect that watches value and updates
prevValueRef.current and calls setLocal(value) there so state updates happen
after render and avoid unnecessary re-renders.
web/src/pages/settings/RestartBadge.tsx (1)

10-19: 🛠️ Refactor suggestion | 🟠 Major

Use a shared UI badge primitive instead of custom page-local badge markup.

Line 10 introduces a custom badge <span> pattern that should be standardized through web/src/components/ui/ for consistency and reuse.

As per coding guidelines: “ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard).”

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

In `@web/src/pages/settings/RestartBadge.tsx` around lines 10 - 19, Replace the
custom span badge in RestartBadge.tsx with the shared UI badge primitive from
web/src/components/ui (e.g., use the existing StatusBadge/Badge component)
instead of the inline <span> markup; pass through the className prop and the
title "Changes to this setting require a restart to take effect", render the
RotateCcw icon as the badge leading icon, and ensure the text "Restart" is the
badge label so styling and accessibility come from the shared component rather
than page-local CSS.
web/src/stores/settings.ts (2)

99-104: ⚠️ Potential issue | 🟠 Major

Keep currency in sync when entries are replaced.

deriveCurrency() is only used in the save/reset paths. Both the initial
fetchSettingsData() set and the polling/WebSocket refreshEntries() set can
replace entries with a new budget/currency value while leaving
useSettingsStore((s) => s.currency) stale.

🛠 Proposed fix
       set({
         schema,
         entries,
+        currency: deriveCurrency(entries) ?? get().currency,
         loading: false,
         error: errors.length > 0 ? errors.join('; ') : null,
       })
...
-    set({ entries })
+    set((state) => ({
+      entries,
+      currency: deriveCurrency(entries) ?? state.currency,
+    }))

Also applies to: 110-117

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

In `@web/src/stores/settings.ts` around lines 99 - 104, The store update that
replaces entries (in both fetchSettingsData() and refreshEntries()) fails to
update the derived currency field; call deriveCurrency(entries) and include its
result in the set(...) payload whenever you replace entries so
useSettingsStore((s) => s.currency) stays in sync—specifically, update the set
call that currently sets { schema, entries, loading: false, error: ... } to also
set currency: deriveCurrency(entries); apply the same change to the analogous
set in the polling/WebSocket refreshEntries() path and any other paths that
replace entries.

110-117: ⚠️ Potential issue | 🟠 Major

refreshEntries() can still commit an older snapshot after a newer one.

The savingKeys guard only blocks in-flight saves. Two overlapping poll or
WebSocket refreshes can still resolve out of order, and the slower older
response will win because nothing ties set({ entries }) to the newest request.

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

In `@web/src/stores/settings.ts` around lines 110 - 117, refreshEntries can race:
two overlapping calls may resolve out-of-order and an older response can
overwrite a newer one; fix by attaching a monotonic request identifier (e.g.,
requestId or timestamp) stored in the settings store and checking it before
committing entries. Specifically, increment or set a latestRequestId in the
store when refreshEntries starts, capture that id locally, call
settingsApi.getAllSettings(), and only call set({ entries }) if the captured id
matches the store's latestRequestId and savingKeys is still clear; reference
refreshEntries, settingsApi.getAllSettings, get(), set(), savingKeys, and
entries to locate where to add the id management and the pre-commit check.
web/src/pages/SettingsPage.tsx (2)

159-167: ⚠️ Potential issue | 🟠 Major

Code saves can leave an older GUI draft on top of the saved value.

If dirtyValues already contains a GUI draft and Code mode saves a different
value, next.get(key) === value is false, so the stale GUI draft survives.
Switching back to GUI then shows the old draft instead of the just-saved code
value.

🛠 Proposed fix
         setDirtyValues((prev) => {
           const next = new Map(prev)
-          for (const [key, value] of changes) {
-            if (!failedKeys.has(key) && next.get(key) === value) {
-              next.delete(key)
-            }
+          for (const key of changes.keys()) {
+            if (!failedKeys.has(key)) {
+              next.delete(key)
+            }
           }
           return next
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SettingsPage.tsx` around lines 159 - 167, The current
setDirtyValues updater only removes a draft when the draft equals the saved
value, leaving stale GUI drafts when Code mode saved a different value; update
the logic in the setDirtyValues callback (the block that iterates over changes,
failedKeys, and next) to remove any entry for a key that was part of changes and
not in failedKeys (i.e., if !failedKeys.has(key) then next.delete(key)), so
successful saves always clear corresponding dirtyValues entries regardless of
their previous draft value.

187-197: ⚠️ Potential issue | 🟠 Major

The header Advanced toggle still hides unsaved advanced drafts.

Only AdvancedModeBanner.onDisable prunes advanced-only keys from
dirtyValues. Disabling Advanced from the header toggle leaves those drafts
hidden but still counted and submittable via the save bar.

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

In `@web/src/pages/SettingsPage.tsx` around lines 187 - 197, handleAdvancedToggle
currently disables advanced mode without pruning advanced-only drafts, leaving
advanced keys in dirtyValues (so they remain counted and can be submitted);
mirror the cleanup logic from AdvancedModeBanner.onDisable by removing
advanced-only keys from dirtyValues when setAdvancedMode(false) is called in
handleAdvancedToggle, then persist the SETTINGS_ADVANCED_KEY and respect the
SETTINGS_ADVANCED_WARNED_KEY flow (use setShowAdvancedWarning and return early
only for the warning path), ensuring the same helper or function that
AdvancedModeBanner.onDisable uses to prune dirtyValues is invoked here so both
disable paths behave identically.
🤖 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__/pages/SettingsPage.test.tsx`:
- Around line 186-190: Add a complementary test in SettingsPage.test.tsx that
enables advanced mode before rendering: set
localStorage.setItem('settings_advanced_mode','true'), call renderSettings(),
and assert the advanced-level setting appears with expect(screen.getByText('Api
Prefix')).toBeInTheDocument(); ensure you clear or reset the localStorage flag
after the test so it doesn't affect other tests.

In `@web/src/pages/settings/CodeEditorPanel.tsx`:
- Around line 194-200: The textarea in CodeEditorPanel (the JSX using
value={text}, onChange={handleChange}, aria-label={`${format.toUpperCase()}
editor`}) should include a rows attribute to give assistive tech a hint about
expected content height; update that textarea element to add an appropriate rows
value (e.g., rows={10} or another sensible default for your content) while
keeping the existing min-h-96 class and other props.

In `@web/src/pages/settings/NamespaceSection.tsx`:
- Around line 84-96: The map body that builds compositeKey and returns an
ErrorBoundary wrapping SettingRow is too large; extract that JSX into a small
reusable component (e.g., NamespaceSettingRow or SettingRowWrapper) that accepts
the entry and the existing context helpers (dirtyValues, onValueChange,
savingKeys, controllerDisabledMap) and internally computes compositeKey =
`${entry.definition.namespace}/${entry.definition.key}` and renders
<ErrorBoundary level="component"><SettingRow .../></ErrorBoundary> passing
dirtyValue, onChange (calling onValueChange with compositeKey), saving (checking
savingKeys.has(compositeKey)) and controllerDisabled
(controllerDisabledMap.get(compositeKey)); then replace the map body with a
simple call to the new component (e.g., groupEntries.map(entry =>
<NamespaceSettingRow entry={entry} />) ) so the loop only selects data and the
complex JSX lives in the extracted component.

In `@web/src/pages/settings/utils.ts`:
- Around line 71-76: The compositeKey parsing in the keys.map callback can
silently produce wrong ns/key when no '/' exists; add a defensive guard in that
callback (where slashIdx, ns, key are computed) to check if slashIdx > -1 and if
not either throw a clear error or skip that key (and optionally log) before
calling updateSetting; ensure the logic around updateSetting,
dirtyValues.get(compositeKey), and the returned promise handles the
early-return/skip consistently so malformed composite keys do not produce
corrupted slices.

---

Duplicate comments:
In `@web/src/pages/settings/RestartBadge.tsx`:
- Around line 10-19: Replace the custom span badge in RestartBadge.tsx with the
shared UI badge primitive from web/src/components/ui (e.g., use the existing
StatusBadge/Badge component) instead of the inline <span> markup; pass through
the className prop and the title "Changes to this setting require a restart to
take effect", render the RotateCcw icon as the badge leading icon, and ensure
the text "Restart" is the badge label so styling and accessibility come from the
shared component rather than page-local CSS.

In `@web/src/pages/settings/SearchInput.tsx`:
- Around line 18-22: The component currently mutates prevValueRef and calls
setLocal(value) during render (see prevValueRef, useRef, setLocal, value), which
violates render purity; move the prop-to-state synchronization into a useEffect
that watches value and updates prevValueRef.current and calls setLocal(value)
there so state updates happen after render and avoid unnecessary re-renders.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 159-167: The current setDirtyValues updater only removes a draft
when the draft equals the saved value, leaving stale GUI drafts when Code mode
saved a different value; update the logic in the setDirtyValues callback (the
block that iterates over changes, failedKeys, and next) to remove any entry for
a key that was part of changes and not in failedKeys (i.e., if
!failedKeys.has(key) then next.delete(key)), so successful saves always clear
corresponding dirtyValues entries regardless of their previous draft value.
- Around line 187-197: handleAdvancedToggle currently disables advanced mode
without pruning advanced-only drafts, leaving advanced keys in dirtyValues (so
they remain counted and can be submitted); mirror the cleanup logic from
AdvancedModeBanner.onDisable by removing advanced-only keys from dirtyValues
when setAdvancedMode(false) is called in handleAdvancedToggle, then persist the
SETTINGS_ADVANCED_KEY and respect the SETTINGS_ADVANCED_WARNED_KEY flow (use
setShowAdvancedWarning and return early only for the warning path), ensuring the
same helper or function that AdvancedModeBanner.onDisable uses to prune
dirtyValues is invoked here so both disable paths behave identically.

In `@web/src/stores/settings.ts`:
- Around line 99-104: The store update that replaces entries (in both
fetchSettingsData() and refreshEntries()) fails to update the derived currency
field; call deriveCurrency(entries) and include its result in the set(...)
payload whenever you replace entries so useSettingsStore((s) => s.currency)
stays in sync—specifically, update the set call that currently sets { schema,
entries, loading: false, error: ... } to also set currency:
deriveCurrency(entries); apply the same change to the analogous set in the
polling/WebSocket refreshEntries() path and any other paths that replace
entries.
- Around line 110-117: refreshEntries can race: two overlapping calls may
resolve out-of-order and an older response can overwrite a newer one; fix by
attaching a monotonic request identifier (e.g., requestId or timestamp) stored
in the settings store and checking it before committing entries. Specifically,
increment or set a latestRequestId in the store when refreshEntries starts,
capture that id locally, call settingsApi.getAllSettings(), and only call set({
entries }) if the captured id matches the store's latestRequestId and savingKeys
is still clear; reference refreshEntries, settingsApi.getAllSettings, get(),
set(), savingKeys, and entries to locate where to add the id management and the
pre-commit check.
🪄 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: b14ec3ba-01ca-4d9d-8c97-15cbd25f9e67

📥 Commits

Reviewing files that changed from the base of the PR and between c66c715 and dd478d1.

📒 Files selected for processing (23)
  • CLAUDE.md
  • docs/design/page-structure.md
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/utils.ts
  • web/src/stores/settings.ts
  • web/src/utils/constants.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 (7)
**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Documentation must be written in Markdown and built with Zensical (config: mkdocs.yml); design spec lives in docs/design/ (11 pages linked from docs/DESIGN_SPEC.md)

Files:

  • CLAUDE.md
  • docs/design/page-structure.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/utils/constants.ts
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/stores/settings.ts
  • web/src/pages/settings/utils.ts
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/utils/constants.ts
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/stores/settings.ts
  • web/src/pages/settings/utils.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/utils/constants.ts
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/stores/settings.ts
  • web/src/pages/settings/utils.ts
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
docs/design/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

When an implementation deviates from the spec (better approach found, scope evolved, etc.), update the relevant docs/design/ page to reflect the new reality

Files:

  • docs/design/page-structure.md
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/pages/**/*.tsx : Lazy-load page components and place page-scoped sub-components in `pages/<page-name>/` subdirectories (e.g., `pages/tasks/`, `pages/org-edit/`)

Applied to files:

  • CLAUDE.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:

  • 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
📚 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-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.

Applied to files:

  • CLAUDE.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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Do NOT recreate complex (>8 line) JSX inside `.map()` blocks; extract to a shared component in `web/src/components/ui/`

Applied to files:

  • CLAUDE.md
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : New shared components must have a corresponding `.stories.tsx` Storybook file with all component states (default, hover, loading, error, empty)

Applied to files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/NamespaceSection.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export component props as a TypeScript interface

Applied to files:

  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • web/src/utils/constants.ts
  • docs/design/page-structure.md
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design token variables exclusively for shadows and borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); never hardcode shadow or border values

Applied to files:

  • web/src/pages/settings/SourceBadge.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`) or CSS variables (`var(--so-accent)`); NEVER hardcode hex values or rgba() in .tsx files

Applied to files:

  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in components

Applied to files:

  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
📚 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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • docs/design/page-structure.md
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to docs/design/**/*.md : When an implementation deviates from the spec (better approach found, scope evolved, etc.), update the relevant `docs/design/` page to reflect the new reality

Applied to files:

  • docs/design/page-structure.md
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.

Applied to files:

  • docs/design/page-structure.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__/pages/SettingsPage.test.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{py,ts,tsx,go} : Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{ts,tsx} : Use camelCase for variable names and function identifiers

Applied to files:

  • web/src/pages/settings/CodeEditorPanel.tsx
🔇 Additional comments (13)
CLAUDE.md (1)

219-219: Docs update is consistent with the new Settings page structure.

The added settings/ example under web/src/pages/ is clear and aligned with the implementation.

web/src/pages/settings/SettingsSkeleton.tsx (1)

3-15: Skeleton structure and accessibility look good.

This is a clean loading state, and the live-region semantics are appropriate.

web/src/pages/settings/RestartBadge.stories.tsx (1)

4-13: Story setup is clean and correctly typed.

The meta object and StoryObj<typeof meta> usage are solid.

web/src/pages/settings/AdvancedModeBanner.tsx (1)

8-21: Banner implementation looks good.

Props are cleanly typed, and reuse of shared Button keeps consistency.

web/src/utils/constants.ts (1)

117-129: Good hardening on reverse dependency map construction.

The explicit duplicate check prevents silent controller overwrites and makes misconfigurations fail fast.

web/src/pages/settings/FloatingSaveBar.tsx (1)

34-38: Accessibility handling for save errors is correctly implemented.

The alert live region is in place and should announce failures promptly.

web/src/pages/settings/SettingField.tsx (1)

1-227: LGTM — well-structured field component with proper validation.

The implementation correctly handles:

  • Empty numeric field validation (lines 81, 90) preventing Number('') coercion issues
  • Slider zero preservation using Number.isNaN check (line 159)
  • Array settings with local draft state to avoid re-serialization on every keystroke
  • Type-appropriate controls (toggle, select, slider, textarea, input) based on definition
web/src/pages/settings/CodeEditorPanel.tsx (1)

63-71: Sync logic may cause stale state on rapid entry updates.

The pattern if (entries !== prevEntriesRef.current) compares by reference. If entries is a new array reference with identical content (e.g., from store normalization), this triggers unnecessary re-serialization. However, since it only applies when !dirty, the user experience impact is minimal—just redundant computation.

web/src/pages/settings/DependencyIndicator.tsx (1)

1-24: LGTM — clean, accessible dependency indicator.

Good accessibility pattern with aria-hidden on the icon and sr-only span for screen readers. The title attribute provides a basic tooltip for mouse users.

web/src/__tests__/hooks/useSettingsData.test.ts (1)

1-144: LGTM — solid test coverage for the settings data hook.

Tests properly cover:

  • Initial data fetch on mount
  • Store state propagation (loading, entries, error, saving)
  • WebSocket channel bindings with proper guard assertion before accessing mock calls
  • Polling lifecycle (start on mount, stop on unmount)
docs/design/page-structure.md (1)

110-121: LGTM — documentation accurately reflects implementation.

Good updates:

  • Clarified GUI/Code edit modes with JSON/YAML in Code mode
  • Documented sessionStorage deduplication and localStorage persistence for advanced mode
  • Called out that dependency indicators are frontend-driven via SETTING_DEPENDENCIES
  • Scoped backup namespace to config-only with CRUD deferred
  • Updated API paths to _schema prefix

As per coding guidelines, "When an implementation deviates from the spec, update the relevant docs/design/ page to reflect the new reality."

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

1-203: LGTM — comprehensive page component tests.

Good coverage of:

  • Loading/skeleton states with data presence checks
  • Namespace section rendering
  • Error and WebSocket disconnect states
  • UI controls (search, Advanced toggle, Code toggle)
  • Advanced setting filtering
  • Empty state handling
  • Custom wsSetupError message display

The dynamic mock key pattern is now documented (lines 101-103).

web/src/pages/settings/SourceBadge.tsx (1)

23-35: The SourceBadge component correctly uses semantic Tailwind classes (bg-accent/10, text-accent, etc.) and follows all styling guidelines. While StatusBadge and ProviderHealthBadge exist in web/src/components/ui/, they implement a different visual pattern (status indicators with colored dots) that doesn't match SourceBadge's use case (filled rectangular badges). Composing SourceBadge with these components would require significant modifications or wrapper logic that defeats the benefit of component reuse. This is an appropriate page-scoped component.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 16:37 — 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: 4

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

113-123: ⚠️ Potential issue | 🟠 Major

refreshEntries() can still apply an older snapshot after a write.

The savingKeys checks only tell you whether a save is active at the beginning and end of the request. If a write starts and finishes while getAllSettings() is in flight, this older response still passes both checks and can revert the just-saved entry until the next refresh. Track a request/mutation version and ignore any response that predates the latest write.

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

In `@web/src/stores/settings.ts` around lines 113 - 123, refreshEntries can apply
an older snapshot if a write starts and completes while getAllSettings() is in
flight; add a monotonic request/mutation version to the store (e.g.,
writeVersion or requestId) that you increment whenever a save begins (the same
place that mutates savingKeys) and capture the current version before calling
settingsApi.getAllSettings(); after the await, compare the captured version to
the store's latest version and only apply set({ entries, currency }) via
set(...) (and call deriveCurrency(entries)) if the captured version matches the
latest so stale responses are ignored.
web/src/pages/settings/utils.ts (1)

8-28: ⚠️ Potential issue | 🟠 Major

Normalize separator characters before fuzzy-matching setting names.

The page renders names like Api Prefix, but this matcher sees the raw key api_prefix. A query like api prefix therefore misses the exact setting name users can see. Normalize _/- to spaces, or fuzzy-match the same humanized label that the row component renders.

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

In `@web/src/pages/settings/utils.ts` around lines 8 - 28, The matcher misses
visible names like "Api Prefix" because keys like entry.definition.key contain
underscores/dashes; update fuzzyMatch (or pre-normalize inside matchesSetting)
to replace '_' and '-' with spaces on both the haystack and the query (e.g.,
normalize entry.definition.key, description, namespace, group and the query
string) so comparisons use the humanized label form users see; reference
fuzzyMatch and matchesSetting (and entry.definition.key) when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/pages/settings/CodeEditorPanel.tsx`:
- Around line 17-25: Before constructing the `changes` payload, build a Map of
the original SettingEntry objects keyed by `${namespace}/${key}` (use the same
fields accessed in entriesToObject: entry.definition.namespace and
entry.definition.key). In the save/parsing path that currently produces
`changes` (the code that parses the editor buffer and maps into
updates/deletes), validate each parsed namespace/key against that Map and reject
the save if a parsed key is not present in the Map (new key) or if the mapped
entry has source === 'env' (read-only); surface a clear error to the user and
abort building/sending `changes`. Apply this validation in the same save handler
(where `changes` is assembled) and also to the deletion code path so all
additions/updates/deletes are checked.
- Around line 155-157: The editor must be frozen during an in-flight save to
prevent mid-request edits from being marked clean and lost; introduce a saving
state (e.g., useState<boolean> saving) and set saving=true before calling await
onSave(changes) and saving=false after to disable the textarea and format
toggles while saving; additionally, avoid clearing dirty unconditionally after
the await — either capture a saveToken/saveVersion before the await and only
call setDirty(false) if the current edit version matches that token, or compare
the current edits/content to the saved changes before clearing dirty; update
references to setDirty, onSave, and failedKeys accordingly and ensure UI
controls read the new saving state to become disabled during the save.

In `@web/src/pages/settings/NamespaceSection.tsx`:
- Around line 8-20: Extract the inline props object for the NamespaceSettingRow
component into a named interface (e.g., NamespaceSettingRowProps) and use it in
the function signature; specifically create an interface that declares entry:
SettingEntry, dirtyValues: ReadonlyMap<string, string>, onValueChange: (ck:
string, v: string) => void, savingKeys: ReadonlySet<string>, and
controllerDisabledMap: ReadonlyMap<string, boolean>, then change the
NamespaceSettingRow declaration to accept props: NamespaceSettingRowProps.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 243-250: The Code toggle currently unmounts CodeEditorPanel and
loses unsaved local edits; add a guard by tracking the editor's dirty state in
the parent and preventing immediate viewMode changes when dirty. Expose a
callback or ref from CodeEditorPanel (e.g., onDirtyChange or getIsDirty) and
store it in parent state (e.g., editorIsDirty), then update the ToggleField
onChange handler (the anonymous function that calls setViewMode) to check
editorIsDirty: if not dirty, proceed as before; if dirty, present a
confirmation/save/discard flow (modal or browser confirm) and only call
setViewMode('gui') when the user explicitly confirms discard or after saving.
Apply the same guarded logic for the other toggle usage mentioned (the other
ToggleField code path).

---

Duplicate comments:
In `@web/src/pages/settings/utils.ts`:
- Around line 8-28: The matcher misses visible names like "Api Prefix" because
keys like entry.definition.key contain underscores/dashes; update fuzzyMatch (or
pre-normalize inside matchesSetting) to replace '_' and '-' with spaces on both
the haystack and the query (e.g., normalize entry.definition.key, description,
namespace, group and the query string) so comparisons use the humanized label
form users see; reference fuzzyMatch and matchesSetting (and
entry.definition.key) when making the change.

In `@web/src/stores/settings.ts`:
- Around line 113-123: refreshEntries can apply an older snapshot if a write
starts and completes while getAllSettings() is in flight; add a monotonic
request/mutation version to the store (e.g., writeVersion or requestId) that you
increment whenever a save begins (the same place that mutates savingKeys) and
capture the current version before calling settingsApi.getAllSettings(); after
the await, compare the captured version to the store's latest version and only
apply set({ entries, currency }) via set(...) (and call deriveCurrency(entries))
if the captured version matches the latest so stale responses are ignored.
🪄 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: 1275e716-3c6a-4335-9378-30bd262864fe

📥 Commits

Reviewing files that changed from the base of the PR and between dd478d1 and 828a575.

📒 Files selected for processing (6)
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/utils.ts
  • web/src/stores/settings.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 Backend
  • GitHub Check: Build Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Dashboard Test
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript
Use camelCase for variable names and function identifiers

Files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/utils.ts
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/stores/settings.ts
web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.tsx: ALWAYS reuse existing shared components from web/src/components/ui/ before creating new ones (e.g., StatusBadge, MetricCard, AgentCard, DeptHealthBar, SectionCard)
Use Tailwind semantic classes (text-foreground, bg-card, text-accent, text-success) or CSS variables (var(--so-accent)); NEVER hardcode hex values or rgba() in .tsx files
Use font-sans or font-mono for typography (maps to Geist tokens); NEVER set fontFamily directly in CSS
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; NEVER hardcode pixel values for layout spacing
Do NOT recreate complex (>8 line) JSX inside .map() blocks; extract to a shared component in web/src/components/ui/

Files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
**/*.{py,ts,tsx,go}

📄 CodeRabbit inference engine (CLAUDE.md)

Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/utils.ts
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/stores/settings.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Import cn from @/lib/utils for conditional class merging in components

Files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/utils.ts
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/stores/settings.ts
web/src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Lazy-load page components and place page-scoped sub-components in pages/<page-name>/ subdirectories (e.g., pages/tasks/, pages/org-edit/)

Files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
📚 Learning: 2026-03-20T08:28:32.845Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T08:28:32.845Z
Learning: Applies to web/src/__tests__/**/*.{ts,js} : Dashboard testing: Vitest unit tests organized by feature under `web/src/__tests__/`. Use fast-check for property-based testing (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Do NOT recreate complex (>8 line) JSX inside `.map()` blocks; extract to a shared component in `web/src/components/ui/`

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{py,ts,tsx,go} : Lines must not exceed 88 characters (enforced by ruff for Python, configured in web/.eslintrc for TypeScript)

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in components

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : ALWAYS reuse existing shared components from `web/src/components/ui/` before creating new ones (e.g., `StatusBadge`, `MetricCard`, `AgentCard`, `DeptHealthBar`, `SectionCard`)

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to web/src/**/*.tsx : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`) or CSS variables (`var(--so-accent)`); NEVER hardcode hex values or rgba() in .tsx files

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
📚 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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • web/src/pages/SettingsPage.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-28T14:27:45.936Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T14:27:45.936Z
Learning: Applies to **/*.{ts,tsx} : Use camelCase for variable names and function identifiers

Applied to files:

  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • web/src/stores/settings.ts

@Aureliolo Aureliolo force-pushed the feat/settings-page branch from 828a575 to ba9f9f6 Compare March 28, 2026 16:56
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 16:57 — 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: 12

♻️ Duplicate comments (2)
web/src/pages/settings/SearchInput.tsx (1)

17-22: ⚠️ Potential issue | 🟠 Major

Move state sync to useEffect to preserve render purity.

Calling setLocal(value) during render breaks render purity and causes extra re-renders in Strict Mode. The previous review flagged this but it remains unaddressed.

🛠️ Proposed fix
-  // Sync external value changes (e.g. clearing from parent)
-  const prevValueRef = useRef(value)
-  if (value !== prevValueRef.current) {
-    prevValueRef.current = value
-    setLocal(value)
-  }
+  // Sync external value changes (e.g. clearing from parent)
+  useEffect(() => {
+    setLocal(value)
+  }, [value])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/SearchInput.tsx` around lines 17 - 22, The code is
mutating state during render by comparing value to prevValueRef.current and
calling setLocal(value); move this synchronization into a useEffect: create a
useEffect that runs when value changes, compare value to prevValueRef.current
inside that effect, update prevValueRef.current and call setLocal(value) there
(leave the initial useRef(prevValueRef) and setLocal state as-is), ensuring
render purity and avoiding setState during render in the SearchInput component.
web/src/pages/settings/RestartBadge.tsx (1)

8-21: 🧹 Nitpick | 🔵 Trivial

Consider reusing shared badge primitives.

This component duplicates the badge pattern that likely exists in web/src/components/ui/. If a shared Badge or StatusBadge component exists, consider composing it here to maintain consistency and reduce duplication.

The current implementation is well-structured and uses proper design tokens (bg-warning/10, text-warning), so this is a low-priority refactor.

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

In `@web/src/pages/settings/RestartBadge.tsx` around lines 8 - 21, The
RestartBadge component duplicates a shared badge pattern; refactor RestartBadge
to reuse the existing shared Badge (or StatusBadge) primitive instead of
rendering raw span markup: locate the RestartBadge function and replace the
span/RotateCcw usage with the shared Badge component, passing through className
and title and rendering <RotateCcw /> and the "Restart" label as children,
preserving the cn(...) composed classes (bg-warning/10, text-warning) and
aria-hidden prop so styling and accessibility remain identical.
🤖 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__/hooks/useSettingsData.test.ts`:
- Around line 35-49: The test helper resetStore is replacing store action
functions with mocks; instead, leave the real actions in place and spy on them
so tests are less coupled to store internals. Modify resetStore to set only data
fields (schema, entries, loading, error, savingKeys, saveError) and do not
override fetchSettingsData, refreshEntries, updateSetting, resetSetting,
updateFromWsEvent; in your tests create spies like
jest.spyOn(useSettingsStore.getState(), 'fetchSettingsData') (and similarly for
refreshEntries, updateSetting, resetSetting, updateFromWsEvent) to assert calls,
and ensure you restore spies in afterEach with mockRestore to avoid cross-test
pollution.

In `@web/src/hooks/useSettingsData.ts`:
- Around line 33-48: The initial mount triggers fetchSettingsData() which calls
getAllSettings() and then pollingStart() immediately invokes refreshEntries()
(which calls getAllSettings() again), causing duplicate concurrent calls; modify
useSettingsData.ts so that either pollingStart() is deferred until
fetchSettingsData() completes or the first poll tick is suppressed by updating
usePolling or its usage: await useSettingsStore.getState().fetchSettingsData()
before starting pollingStart(), or pass an option to usePolling to skip the
immediate invocation so refreshEntries() does not run on first start; ensure
references are to useSettingsStore.getState().fetchSettingsData(),
pollingStart()/pollingStop(), usePolling, refreshEntries(), and getAllSettings()
so the change is applied in the correct places.

In `@web/src/pages/settings/CodeEditorPanel.tsx`:
- Around line 117-198: The handleSave function is too long—extract the
validation/diff logic into smaller functions to stay under 50 lines: add a
detectRemovedKeys(original, parsed) that returns the removedKeys array (use
entriesToObject and parsed from parseText like current logic) and a
validateChanges(parsed, original, entryLookup) that returns { changes:
Map<string,string>, unknownKeys: string[], envKeys: string[] } implementing the
loop that builds changes/unknownKeys/envKeys and stringifies non-string values;
then simplify handleSave to call parseText, original=entriesToObject(entries),
removedKeys = detectRemovedKeys(original, parsed), validateChanges(...), and
keep the existing onSave/updateDirty flow unchanged (refer to symbols:
handleSave, parseText, entriesToObject, detectRemovedKeys, validateChanges,
entryLookup, onSave, updateDirty).

In `@web/src/pages/settings/NamespaceSection.tsx`:
- Around line 24-35: Remove the redundant key prop on the ErrorBoundary inside
the NamespaceSettingRow component: the reconciliation key is set by the parent
when mapping over entries (the caller that uses ck as the list key), so delete
key={ck} from the ErrorBoundary in NamespaceSection.tsx (inside the
NamespaceSettingRow render that uses variables entry, ck, dirtyValues,
onValueChange, savingKeys, controllerDisabledMap) to avoid the unnecessary prop.

In `@web/src/pages/settings/RestartBadge.stories.tsx`:
- Around line 4-8: The Storybook meta for RestartBadge is missing the
accessibility test configuration; update the meta object (the constant named
"meta" for component RestartBadge in RestartBadge.stories.tsx) to include a
parameters.a11y.test entry that enforces the project's WCAG level (for example
set parameters.a11y.test to the required WCAG rule set such as wcag2aa or the
configured test object), ensuring the story runs accessibility checks.

In `@web/src/pages/settings/SettingField.tsx`:
- Around line 73-115: The SettingField component is too large and should be
split: extract type-specific rendering and validation into smaller components
(e.g., IntSetting, FloatSetting, ArraySetting, GenericSetting) and keep the
top-level SettingField as a thin orchestrator that computes compositeKey and
isArraySetting (SIMPLE_ARRAY_SETTINGS) and passes definition, value, onChange,
disabled and the validate callback (or move validate into each type component if
preferred). Move the integer/float validation branches out of validate if you
split validation per type, preserve the RegExp logic (validator_pattern) where
appropriate, and ensure each new component implements the same contract used by
SettingField (props names and handlers) so the main function stays small and
only selects which child to render based on definition.type and isArraySetting.
- Around line 160-161: The slider uses a hardcoded step (step = definition.type
=== 'int' ? 1 : 0.1) which is inappropriate for many float ranges; update the
step calculation in SettingField (the step variable) to derive a dynamic step
for float types using the numeric range (e.g., compute step = (max - min) / N
with a sensible N like 100, clamp to a minimum precision and optionally snap to
a power-of-ten or decimal increment) while keeping step = 1 for ints; use
definition.min and definition.max (falling back to reasonable defaults if
missing) so the slider resolution scales with the range.

In `@web/src/pages/settings/SettingRow.tsx`:
- Around line 22-24: The formatKey function can produce leading/trailing or
multiple spaces and wrong output for empty strings; update formatKey to first
normalize the key by replacing one-or-more underscores with a single space, trim
the result, return an empty string if the normalized key is empty, then
title-case each word (capitalizing only the first character of each word) to
avoid odd characters or extra spaces — locate and change the function named
formatKey in SettingRow.tsx to implement these steps.

In `@web/src/pages/settings/SourceBadge.stories.tsx`:
- Around line 4-8: The Storybook meta object for SourceBadge is missing the
accessibility enforcement flag; update the existing meta constant (meta) for the
SourceBadge story to include parameters.a11y.test set to true (or the
project-standard test config) so WCAG checks run; modify the meta object near
the definition of component: SourceBadge to add or extend parameters with an
a11y property containing test: true.

In `@web/src/pages/SettingsNamespacePage.tsx`:
- Around line 42-44: The code unconditionally casts `namespace` to
`SettingNamespace` in `ns` even though `validNamespace` (computed against
`NAMESPACE_ORDER`) may be false; change the pattern to narrow the type at
runtime instead of always asserting: compute `validNamespace` as you do, then
only derive `ns` when `validNamespace` is true (e.g., set `ns` to `undefined` or
a typed value otherwise, or create `if (!validNamespace) { /* early return or
fallback */ }`), and keep `advancedMode` and `SETTINGS_ADVANCED_KEY` usage
unchanged; reference `validNamespace`, `NAMESPACE_ORDER`, `namespace`, and `ns`
when making this change so the cast is only performed after the runtime check.
- Around line 74-117: Extract the duplicated dirty-state logic from
SettingsNamespacePage (handleValueChange, handleDiscard, handleSave, dirtyValues
state and persistedValues derivation) into a shared custom hook named
useSettingsDirtyState that accepts entries and updateSetting and returns {
dirtyValues, handleValueChange, handleDiscard, handleSave }; update
SettingsNamespacePage (and SettingsPage) to call useSettingsDirtyState(entries,
updateSetting) and remove the local implementations, ensuring you preserve the
exact behavior (using saveSettingsBatch, useToastStore, and comparing
persistedValues by compositeKey) and keep the same function names so callers
remain unchanged.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 250-257: Replace the blocking window.confirm call in the onChange
handler with the existing ConfirmDialog flow: introduce a new boolean state
(e.g., showCodeDiscardWarning) and, in the onChange handler for the toggle, if
(!v && codeDirty) set that state true and return; otherwise call setViewMode as
before. Add a ConfirmDialog component wired to showCodeDiscardWarning with
onOpenChange to close it, and onConfirm should call setCodeDirty(false),
setViewMode('gui') and close the dialog; ensure handlers reference the existing
setCodeDirty and setViewMode functions and the ConfirmDialog props
(open/onOpenChange/title/description/confirmLabel/onConfirm).

---

Duplicate comments:
In `@web/src/pages/settings/RestartBadge.tsx`:
- Around line 8-21: The RestartBadge component duplicates a shared badge
pattern; refactor RestartBadge to reuse the existing shared Badge (or
StatusBadge) primitive instead of rendering raw span markup: locate the
RestartBadge function and replace the span/RotateCcw usage with the shared Badge
component, passing through className and title and rendering <RotateCcw /> and
the "Restart" label as children, preserving the cn(...) composed classes
(bg-warning/10, text-warning) and aria-hidden prop so styling and accessibility
remain identical.

In `@web/src/pages/settings/SearchInput.tsx`:
- Around line 17-22: The code is mutating state during render by comparing value
to prevValueRef.current and calling setLocal(value); move this synchronization
into a useEffect: create a useEffect that runs when value changes, compare value
to prevValueRef.current inside that effect, update prevValueRef.current and call
setLocal(value) there (leave the initial useRef(prevValueRef) and setLocal state
as-is), ensuring render purity and avoiding setState during render in the
SearchInput component.
🪄 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: b7c888d1-8c82-4280-b64c-b53c2c56a441

📥 Commits

Reviewing files that changed from the base of the PR and between 828a575 and ba9f9f6.

📒 Files selected for processing (23)
  • CLAUDE.md
  • docs/design/page-structure.md
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/utils.ts
  • web/src/stores/settings.ts
  • web/src/utils/constants.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 Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{ts,tsx}: Use design tokens and semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) -- NEVER hardcode hex values or rgba in .tsx/.ts files
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/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 text-metric classes -- use <MetricCard>
Do NOT render initials circles manually -- use <Avatar>; do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Use TypeScript 6.0 features: remove deprecated baseUrl, accept that esModuleInterop is always true, explicitly list types if needed, use "bundler" or "nodenext" for moduleResolution
Never use real vendor names (OpenAI, Claude, GPT, Anthropic) in web dashboard code -- use generic names: example-provider, example-large-001, etc.
Keep functions under 50 lines and files under 800 lines
Handle errors explicitly, never silently swallow exceptions

Files:

  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/utils/constants.ts
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/pages/settings/utils.ts
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/stores/settings.ts
web/src/**/*.stories.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use Storybook 10 ESM-only imports: storybook/test (not @storybook/test), storybook/actions (not @storybook/addon-actions); use definePreview from @storybook/react-vite; set parameters.a11y.test for WCAG compliance enforcement

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
docs/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Design spec pages in docs/design/ are mandatory references before implementing features -- ALWAYS read the relevant design page

Files:

  • docs/design/page-structure.md
docs/design/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

When approved deviations from the design spec occur during implementation, update the relevant docs/design/ page to reflect the new reality

Files:

  • docs/design/page-structure.md
🧠 Learnings (19)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
📚 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:

  • 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
📚 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-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/components/ui/**/*.{tsx} : Create a `.stories.tsx` file alongside each new shared component with all states (default, hover, loading, error, empty) for Storybook documentation

Applied to files:

  • CLAUDE.md
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Always reuse existing shared components from `web/src/components/ui/` before creating new ones; export props as TypeScript interfaces; use design tokens exclusively; import `cn` from `@/lib/utils` for conditional class merging

Applied to files:

  • CLAUDE.md
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.

Applied to files:

  • CLAUDE.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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.stories.tsx : Use Storybook 10 ESM-only imports: `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`); use `definePreview` from `storybook/react-vite`; set `parameters.a11y.test` for WCAG compliance enforcement

Applied to files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
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 text-metric classes -- use `<MetricCard>`

Applied to files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT render initials circles manually -- use `<Avatar>`; do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • web/src/utils/constants.ts
  • web/src/pages/settings/utils.ts
  • docs/design/page-structure.md
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/stores/settings.ts
📚 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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • docs/design/page-structure.md
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.

Applied to files:

  • docs/design/page-structure.md
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use design tokens and semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) -- NEVER hardcode hex values or rgba in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) -- NEVER hardcode values

Applied to files:

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

Applied to files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Keep functions under 50 lines and files under 800 lines

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
🔇 Additional comments (31)
web/src/pages/settings/AdvancedModeBanner.tsx (1)

1-23: LGTM!

Clean implementation that properly uses design tokens (text-warning, border-warning/30, bg-warning/5), reuses the shared Button component, exports the props interface, and includes appropriate accessibility attributes (role="status", aria-hidden on the decorative icon).

web/src/pages/settings/SourceBadge.tsx (1)

1-35: LGTM!

The component correctly uses design tokens (bg-accent/10, text-warning, bg-success/10, etc.), exports the props interface, and uses cn() for class merging. As a page-scoped component in pages/settings/, the specialized badge styling (uppercase, small font, tracking-wider) is appropriate for this specific use case.

web/src/pages/settings/SearchInput.tsx (1)

49-70: LGTM on the JSX structure and accessibility.

The input properly uses semantic classes (border-border, bg-surface, text-foreground, focus:border-accent), includes aria-label="Search settings" for accessibility, and the clear button has aria-label="Clear search".

web/src/pages/settings/FloatingSaveBar.tsx (1)

1-57: LGTM!

Well-implemented floating save bar with:

  • Proper accessibility: error messages use role="alert" and aria-live="assertive" for screen reader announcements
  • Design token compliance: semantic classes (border-border, bg-surface, text-text-secondary, text-danger)
  • Reuses shared Button component
  • Appropriate loading state with disabled buttons and spinner
CLAUDE.md (1)

219-219: LGTM!

Documentation correctly updated to reflect the new settings/ page-scoped subdirectory alongside existing examples.

web/src/pages/settings/SettingsSkeleton.tsx (1)

1-16: LGTM!

Clean skeleton implementation that:

  • Reuses the shared SkeletonCard component
  • Uses semantic bg-border class for placeholder blocks
  • Includes proper accessibility attributes (role="status", aria-live="polite", aria-label="Loading settings")
web/src/utils/constants.ts (1)

105-132: LGTM!

The dependency mapping implementation is clean:

  • SETTING_DEPENDENCIES provides a clear controller→dependents mapping
  • The reverse lookup SETTING_DEPENDED_BY includes proper duplicate detection with a descriptive error message
  • The composite key format (namespace/key) is consistent with usage in SettingRow.tsx
  • 60-second polling interval is reasonable for settings that change infrequently
web/src/pages/settings/DependencyIndicator.tsx (1)

1-24: LGTM!

Clean implementation with good accessibility patterns:

  • Early return for empty array prevents unnecessary rendering
  • sr-only span provides screen reader support
  • aria-hidden on decorative icon is correct
  • Tooltip text is user-friendly ("Controls: ...")
web/src/__tests__/hooks/useSettingsData.test.ts (1)

116-125: LGTM!

The guard assertion on line 121 (expect(mock).toHaveBeenCalled()) was correctly added before accessing mock.calls[0]. This prevents confusing errors if useWebSocket wasn't called.

docs/design/page-structure.md (1)

110-120: LGTM!

Documentation updates accurately reflect the implementation:

  • Collapsible namespace sections instead of two-column grid
  • Advanced mode confirmation flow with session deduplication
  • Frontend-maintained dependency map (not backend schema)
  • Backup CRUD scope clarification
  • Correct API paths (/settings/_schema)

Good practice updating the design spec to match reality per coding guidelines.

web/src/__tests__/pages/SettingsPage.test.tsx (2)

109-114: Verify store mock consistency with hook mock.

The store mock returns an empty savingKeys set (line 112), and the hook mock returns saving: false (line 90). This is consistent since saving is derived from savingKeys.size > 0 in the real hook.

If future tests need to verify saving state behavior, ensure both mocks are updated together to maintain consistency.


192-196: LGTM!

The complementary test for advanced mode visibility was added. It correctly sets settings_show_advanced in localStorage before rendering and verifies the advanced-level setting appears.

web/src/pages/settings/utils.ts (2)

4-32: LGTM!

The fuzzy search implementation is clean and effective:

  • normalize handles underscores and hyphens as word separators
  • fuzzyMatch uses subsequence matching (e.g., "prt" matches "server_port")
  • matchesSetting returns true for empty queries and checks multiple fields

This addresses the PR objective for "fuzzy search across all settings (searching name, description, and namespace)."


69-96: LGTM!

The batch save implementation handles errors properly:

  • Validates composite key format with slashIdx < 1 (catches missing slash and empty namespace)
  • Uses Promise.allSettled for parallel saves without short-circuiting on failure
  • Returns failed keys for upstream error handling
  • Logs errors with context for debugging
web/src/pages/settings/NamespaceSection.tsx (1)

64-130: LGTM!

The NamespaceSection component is well-structured:

  • Proper accessibility with aria-expanded and aria-controls
  • forceOpen guard prevents hidden state mutation during search
  • Group headers conditionally render only when multiple groups exist
  • Clean separation with extracted NamespaceSettingRow
  • Uses design tokens (bg-card, border-border, text-foreground, etc.)
web/src/pages/SettingsPage.tsx (3)

51-61: Well-structured data hook consumption.

The destructuring of useSettingsData() is clean and appropriate. The separate access to storeSavingKeys from the store for granular per-key tracking (line 63) is a good pattern that enables per-row saving indicators while keeping the hook's aggregate saving boolean for the floating bar.


125-152: Batch save correctly handles concurrent edits.

The implementation properly snapshots dirtyValues before the async operation and only removes keys from the live state if they haven't been modified during the save. This correctly addresses the race condition where a user might edit another field while a batch save is in flight.


109-119: ⚠️ Potential issue | 🟡 Minor

handleValueChange closure captures stale persistedValues.

The callback depends on persistedValues, but persistedValues is derived from entries. When entries updates (e.g., after a WebSocket refresh), persistedValues changes, but handleValueChange still holds the old closure until the next render. This can cause incorrect dirty tracking if a user edits a field right after a background refresh.

This is a minor edge case since the comparison will eventually correct itself, but adding entries to the dependency array or using a ref pattern would make it more robust.

🛠️ Proposed fix using a ref for immediate access
+  const persistedValuesRef = useRef(persistedValues)
+  persistedValuesRef.current = persistedValues

-  const handleValueChange = useCallback((compositeKey: string, value: string) => {
+  const handleValueChange = useCallback((compositeKey: string, value: string) => {
     setDirtyValues((prev) => {
       const next = new Map(prev)
-      if (persistedValues.get(compositeKey) === value) {
+      if (persistedValuesRef.current.get(compositeKey) === value) {
         next.delete(compositeKey)
       } else {
         next.set(compositeKey, value)
       }
       return next
     })
-  }, [persistedValues])
+  }, [])
			> Likely an incorrect or invalid review comment.
web/src/hooks/useSettingsData.ts (2)

1-8: Imports and constants are well-organized.

The use of satisfies readonly WsChannel[] ensures type safety while allowing the array to be narrowly typed. This pattern correctly validates that 'system' is a valid WsChannel at compile time.


66-77: Clean return interface with derived state.

The hook correctly derives saving from savingKeys.size > 0, providing a simple boolean for consumers while the store maintains granular per-key tracking. The return object exposes all necessary state and actions for the settings UI.

web/src/pages/settings/SettingRow.tsx (1)

42-76: Well-structured row layout with appropriate visual feedback.

The component correctly handles multiple states (env-locked, saving, controller-disabled, security-sensitive) with clear visual indicators and helper text. The use of design tokens (text-warning, text-danger, text-text-muted) follows the coding guidelines.

web/src/pages/SettingsNamespacePage.tsx (1)

155-175: Error and warning banners correctly use design tokens.

The implementation uses cn() for class merging and semantic tokens (border-danger/30, bg-danger/5, text-danger) as per coding guidelines. This addresses the past review comments about line length.

web/src/pages/settings/SettingField.tsx (2)

36-71: Array field correctly uses local draft pattern.

The ArraySettingField component properly maintains a local draft that only serializes on blur, preventing the keystroke-triggered re-render issue flagged in past reviews. The ref-based sync pattern handles external value updates correctly.


78-97: Numeric validation correctly handles empty strings.

The validation now checks for empty/whitespace strings before attempting numeric coercion, addressing the past review about Number('') becoming 0. Both int and float paths return appropriate error messages.

web/src/pages/settings/CodeEditorPanel.tsx (3)

129-149: Removed key detection correctly implemented.

The save handler properly detects when keys present in the original entries are missing from the parsed editor content and rejects the save with a clear error message. This addresses the past review about silent deletion bypass.


186-198: Save correctly guards against mid-save edits.

The implementation captures textBeforeSave before the async onSave call and only clears dirty if the text hasn't changed during the save operation. This prevents losing edits made while a save is in flight.


41-43: The suggested schema change will not improve type handling—both YAML.CORE_SCHEMA and YAML.JSON_SCHEMA are functionally identical in js-yaml and support the same type coercion behavior.

The code already handles type coercion explicitly on lines 162–163 by converting non-string values via JSON.stringify() before comparison. Switching schemas would not prevent type coercion or preserve original string types, as both schemas parse YAML identically in the js-yaml library. No change needed.

			> Likely an incorrect or invalid review comment.
web/src/stores/settings.ts (4)

162-203: resetSetting finally block may cause inconsistent state on failure.

The finally block at line 185 always removes the key from savingKeys, even if the initial resetSetting API call failed (caught at line 170). This is correct for cleanup, but the logic in the finally block also conditionally updates entries if refreshedEntries exists—which it won't if we took the error path at line 176 (throw).

Actually, upon closer inspection, if the error is thrown at line 176, the finally block still runs but refreshedEntries is undefined, so only savingKeys is cleaned up. This is correct behavior.


113-124: Refresh guard prevents overwriting in-flight saves.

The double-check pattern (before and after the async fetch) correctly prevents stale data from overwriting recently saved values. This addresses the past review about race conditions between refresh and save operations.


83-111: fetchSettingsData handles partial failures gracefully.

Using Promise.allSettled ensures that a schema fetch failure doesn't block entries loading (and vice versa). The error aggregation provides clear feedback about which request failed. The pattern of preserving existing state on failure (get().schema/get().entries) maintains UI stability.


9-21: deriveCurrency validates format before updating.

The helper correctly validates the currency value against the ISO 4217 pattern (^[A-Z]{3}$) before returning it. Returning undefined for invalid values prevents corrupting the store with malformed currency codes.

Comment on lines +35 to +49
function resetStore() {
useSettingsStore.setState({
schema: [],
entries: [],
loading: false,
error: null,
savingKeys: new Set(),
saveError: null,
fetchSettingsData: mockFetchSettingsData,
refreshEntries: mockRefreshEntries,
updateSetting: mockUpdateSetting,
resetSetting: mockResetSetting,
updateFromWsEvent: mockUpdateFromWsEvent,
})
}
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 testing store action calls rather than replacing them.

The resetStore function replaces store actions with mocks. While this works, it tightly couples tests to store internals. An alternative approach would be to spy on the actual store actions without replacing them, making tests more resilient to store refactoring.

However, the current approach is pragmatic for unit testing the hook's integration with the store.

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

In `@web/src/__tests__/hooks/useSettingsData.test.ts` around lines 35 - 49, The
test helper resetStore is replacing store action functions with mocks; instead,
leave the real actions in place and spy on them so tests are less coupled to
store internals. Modify resetStore to set only data fields (schema, entries,
loading, error, savingKeys, saveError) and do not override fetchSettingsData,
refreshEntries, updateSetting, resetSetting, updateFromWsEvent; in your tests
create spies like jest.spyOn(useSettingsStore.getState(), 'fetchSettingsData')
(and similarly for refreshEntries, updateSetting, resetSetting,
updateFromWsEvent) to assert calls, and ensure you restore spies in afterEach
with mockRestore to avoid cross-test pollution.

Comment on lines +4 to +8
const meta = {
title: 'Settings/RestartBadge',
component: RestartBadge,
parameters: { layout: 'centered' },
} satisfies Meta<typeof RestartBadge>
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

Add parameters.a11y.test for WCAG compliance enforcement.

Per coding guidelines, Storybook stories should set parameters.a11y.test to enforce accessibility compliance.

♿ Proposed fix
 const meta = {
   title: 'Settings/RestartBadge',
   component: RestartBadge,
-  parameters: { layout: 'centered' },
+  parameters: { layout: 'centered', a11y: { test: 'error' } },
 } satisfies Meta<typeof RestartBadge>

As per coding guidelines: "set parameters.a11y.test for WCAG compliance enforcement."

📝 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 meta = {
title: 'Settings/RestartBadge',
component: RestartBadge,
parameters: { layout: 'centered' },
} satisfies Meta<typeof RestartBadge>
const meta = {
title: 'Settings/RestartBadge',
component: RestartBadge,
parameters: { layout: 'centered', a11y: { test: 'error' } },
} satisfies Meta<typeof RestartBadge>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/RestartBadge.stories.tsx` around lines 4 - 8, The
Storybook meta for RestartBadge is missing the accessibility test configuration;
update the meta object (the constant named "meta" for component RestartBadge in
RestartBadge.stories.tsx) to include a parameters.a11y.test entry that enforces
the project's WCAG level (for example set parameters.a11y.test to the required
WCAG rule set such as wcag2aa or the configured test object), ensuring the story
runs accessibility checks.

Comment on lines +22 to +24
function formatKey(key: string): string {
return key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
}
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

formatKey may produce unexpected output for edge cases.

The regex-based title casing works for typical snake_case keys but may produce odd results for:

  • Keys starting with underscores (e.g., _internal_key Internal Key with leading space)
  • Keys with consecutive underscores (max__retriesMax Retries with double space)
  • Empty strings

Consider adding basic normalization:

♻️ More robust formatting
 function formatKey(key: string): string {
-  return key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
+  return key
+    .replace(/_+/g, ' ')
+    .trim()
+    .replace(/\b\w/g, (c) => c.toUpperCase())
 }
📝 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
function formatKey(key: string): string {
return key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
}
function formatKey(key: string): string {
return key
.replace(/_+/g, ' ')
.trim()
.replace(/\b\w/g, (c) => c.toUpperCase())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/SettingRow.tsx` around lines 22 - 24, The formatKey
function can produce leading/trailing or multiple spaces and wrong output for
empty strings; update formatKey to first normalize the key by replacing
one-or-more underscores with a single space, trim the result, return an empty
string if the normalized key is empty, then title-case each word (capitalizing
only the first character of each word) to avoid odd characters or extra spaces —
locate and change the function named formatKey in SettingRow.tsx to implement
these steps.

Comment on lines +4 to +8
const meta = {
title: 'Settings/SourceBadge',
component: SourceBadge,
parameters: { layout: 'centered' },
} satisfies Meta<typeof SourceBadge>
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

Add parameters.a11y.test for WCAG compliance enforcement.

Per coding guidelines, Storybook stories should set parameters.a11y.test to enforce accessibility compliance.

♿ Proposed fix
 const meta = {
   title: 'Settings/SourceBadge',
   component: SourceBadge,
-  parameters: { layout: 'centered' },
+  parameters: { layout: 'centered', a11y: { test: 'error' } },
 } satisfies Meta<typeof SourceBadge>

As per coding guidelines: "set parameters.a11y.test for WCAG compliance enforcement."

📝 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 meta = {
title: 'Settings/SourceBadge',
component: SourceBadge,
parameters: { layout: 'centered' },
} satisfies Meta<typeof SourceBadge>
const meta = {
title: 'Settings/SourceBadge',
component: SourceBadge,
parameters: { layout: 'centered', a11y: { test: 'error' } },
} satisfies Meta<typeof SourceBadge>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/SourceBadge.stories.tsx` around lines 4 - 8, The
Storybook meta object for SourceBadge is missing the accessibility enforcement
flag; update the existing meta constant (meta) for the SourceBadge story to
include parameters.a11y.test set to true (or the project-standard test config)
so WCAG checks run; modify the meta object near the definition of component:
SourceBadge to add or extend parameters with an a11y property containing test:
true.

Comment on lines +42 to +44
const [advancedMode] = useState(() => localStorage.getItem(SETTINGS_ADVANCED_KEY) === 'true')
const validNamespace = NAMESPACE_ORDER.includes(namespace as SettingNamespace)
const ns = namespace as SettingNamespace
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

Type assertion without runtime validation could cause issues.

Line 43 validates namespace against NAMESPACE_ORDER, but line 44 casts unconditionally. If validNamespace is false, ns is still assigned, which is safe because it's only used after the guard. However, the pattern could be cleaner:

♻️ Cleaner type narrowing
-  const validNamespace = NAMESPACE_ORDER.includes(namespace as SettingNamespace)
-  const ns = namespace as SettingNamespace
+  const ns = NAMESPACE_ORDER.find((n) => n === namespace)
+  const validNamespace = ns !== undefined

   // Later usage is safe because ns is only used after validNamespace check
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/SettingsNamespacePage.tsx` around lines 42 - 44, The code
unconditionally casts `namespace` to `SettingNamespace` in `ns` even though
`validNamespace` (computed against `NAMESPACE_ORDER`) may be false; change the
pattern to narrow the type at runtime instead of always asserting: compute
`validNamespace` as you do, then only derive `ns` when `validNamespace` is true
(e.g., set `ns` to `undefined` or a typed value otherwise, or create `if
(!validNamespace) { /* early return or fallback */ }`), and keep `advancedMode`
and `SETTINGS_ADVANCED_KEY` usage unchanged; reference `validNamespace`,
`NAMESPACE_ORDER`, `namespace`, and `ns` when making this change so the cast is
only performed after the runtime check.

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 17:16 — 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: 5

🤖 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/hooks/useSettingsData.ts`:
- Around line 40-47: The effect currently calls
useSettingsStore.getState().fetchSettingsData().then(...) so if
fetchSettingsData() rejects the .then never runs and pollingStart() is never
called; update the effect to handle rejections (use .catch or .finally) so
pollingStart() is invoked regardless of fetchSettingsData() outcome while still
respecting the cancelled flag and calling pollingStop() on cleanup; reference
the existing symbols useSettingsStore.getState().fetchSettingsData(),
pollingStart, pollingStop and the cancelled boolean to ensure the check remains
before invoking pollingStart().

In `@web/src/hooks/useSettingsDirtyState.ts`:
- Around line 59-101: The handleSave callback can capture a stale dirtyValues
reference when invoked rapidly; to fix this, add a ref (e.g., isSavingRef or
inFlightSavesRef) and use it inside handleSave to prevent concurrent saves (or
debounce repeated calls): check and set isSavingRef.current at the start of
handleSave, return early if a save is in-flight, then clear the ref when the
async saveSettingsBatch(pending, updateSetting) completes or fails; keep the
existing pending = new Map(dirtyValues) and setDirtyValues functional updater
but gate execution with the ref to avoid overlapping saves.

In `@web/src/pages/settings/CodeEditorPanel.tsx`:
- Around line 130-138: The sync-from-entries logic that compares prevEntriesRef
to entries should be moved out of render into a useEffect to avoid running
synchronously during render; wrap the comparison and update in a useEffect that
depends on [entries, dirty, format] and inside it update prevEntriesRef.current,
call setText(serializeEntries(entries, format)) and setParseError(null) only
when entries changed and !dirty, ensuring you reference the same prevEntriesRef,
serializeEntries, setText, setParseError, entries, dirty and format symbols so
the behavior remains the same but runs after render.

In `@web/src/pages/settings/NamespaceSection.tsx`:
- Line 77: The generated contentId using const contentId =
`ns-${displayName.replace(/\s+/g, '-').toLowerCase()}-content` is fragile
because it only replaces spaces and can produce invalid or problematic HTML IDs
for values with special characters or Unicode; update the sanitization logic for
displayName used when creating contentId (and any matching aria-controls usage)
to: normalize Unicode (e.g., NFKD), strip diacritics, convert to lower-case,
replace any sequence of non-alphanumeric characters with a single hyphen,
collapse multiple hyphens, and trim leading/trailing hyphens so the resulting ID
contains only safe characters (letters, numbers, hyphens/underscores) and is
stable for accessibility references like contentId and aria-controls.

In `@web/src/pages/SettingsNamespacePage.tsx`:
- Around line 25-160: The SettingsNamespacePage component is too long; extract
small render helpers/components to reduce its body under 50 lines: create a
Header component (props: displayName, onBack, searchQuery, setSearchQuery) for
the top bar, an ErrorBanner component (props: error) and a WarningBanner
component (props: wsSetupError) for the two banner blocks, and a ContentSection
component (props: filteredEntries, displayName, dirtyValues, handleValueChange,
storeSavingKeys, controllerDisabledMap, handleSave, handleDiscard, saving,
saveError) that renders the EmptyState/NamespaceSection and FloatingSaveBar;
replace the inline JSX in SettingsNamespacePage with these helpers and pass the
existing symbols (useSettingsData outputs, NAMESPACE_DISPLAY_NAMES,
filteredEntries, dirtyValues, handleValueChange, handleSave, handleDiscard,
storeSavingKeys, controllerDisabledMap, wsSetupError, error,
searchQuery/setSearchQuery) so behavior stays identical.
🪄 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: ba64fb00-ed1f-425e-afcb-0688f35f36a6

📥 Commits

Reviewing files that changed from the base of the PR and between ba9f9f6 and 9593824.

📒 Files selected for processing (6)
  • web/src/hooks/useSettingsData.ts
  • web/src/hooks/useSettingsDirtyState.ts
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/NamespaceSection.tsx
📜 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 Web
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{ts,tsx}: Use design tokens and semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) -- NEVER hardcode hex values or rgba in .tsx/.ts files
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/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 text-metric classes -- use <MetricCard>
Do NOT render initials circles manually -- use <Avatar>; do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Use TypeScript 6.0 features: remove deprecated baseUrl, accept that esModuleInterop is always true, explicitly list types if needed, use "bundler" or "nodenext" for moduleResolution
Never use real vendor names (OpenAI, Claude, GPT, Anthropic) in web dashboard code -- use generic names: example-provider, example-large-001, etc.
Keep functions under 50 lines and files under 800 lines
Handle errors explicitly, never silently swallow exceptions

Files:

  • web/src/hooks/useSettingsDirtyState.ts
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/SettingsNamespacePage.tsx
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Always reuse existing shared components from `web/src/components/ui/` before creating new ones; export props as TypeScript interfaces; use design tokens exclusively; import `cn` from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Keep functions under 50 lines and files under 800 lines

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
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 text-metric classes -- use `<MetricCard>`

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use design tokens and semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) -- NEVER hardcode hex values or rgba in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT render initials circles manually -- use `<Avatar>`; do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

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

Applied to files:

  • web/src/pages/settings/CodeEditorPanel.tsx
📚 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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • web/src/pages/SettingsPage.tsx
  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
🔇 Additional comments (15)
web/src/hooks/useSettingsDirtyState.ts (1)

1-15: LGTM!

Clean hook extraction that properly addresses the duplicate dirty-state logic concern from the prior review. The interface is well-typed with ReadonlyMap for persistedValues, and the hook correctly handles:

  • Removing keys when values revert to persisted state
  • Preserving concurrent edits during save via snapshot + functional updater
  • Per-key failure tracking
web/src/hooks/useSettingsData.ts (1)

49-63: LGTM!

WebSocket bindings are correctly memoized with stable references, and the handler uses getState() to avoid stale closure issues. The fix for the duplicate fetch concern from the prior review (sequencing initial fetch before polling) is properly implemented.

web/src/pages/settings/NamespaceSection.tsx (1)

64-130: LGTM!

The component properly addresses all prior review feedback:

  • Props extracted to named interface
  • Complex JSX extracted to NamespaceSettingRow
  • Redundant key removed from ErrorBoundary
  • Toggle guarded when forceOpen is true
  • ClassNames split using cn()

The grouping logic and accessibility attributes (aria-expanded, aria-controls) are well implemented.

web/src/pages/SettingsPage.tsx (3)

76-82: LGTM!

Excellent integration of the useSettingsDirtyState hook. The page properly delegates dirty state management while adding page-specific logic like handleCodeSave and pruneAdvancedDrafts.


179-191: LGTM!

The codeEntries computation properly overlays GUI drafts onto the base entries, ensuring Code mode reflects unsaved GUI changes. Combined with handleCodeSave clearing successfully saved keys from dirtyValues, this creates proper bidirectional synchronization between GUI and Code modes.


252-255: 🧹 Nitpick | 🔵 Trivial

Line length exceeds typical limits but is acceptable for component instantiation.

The CodeEditorPanel line spans ~115 characters. While it exceeds 88 characters, splitting component props across multiple lines is a stylistic choice. Consider whether your linter enforces this.

♻️ Optional: Split props across lines
         <ErrorBoundary level="section">
-          <CodeEditorPanel entries={codeEntries} onSave={handleCodeSave} saving={saving} onDirtyChange={setCodeDirty} />
+          <CodeEditorPanel
+            entries={codeEntries}
+            onSave={handleCodeSave}
+            saving={saving}
+            onDirtyChange={setCodeDirty}
+          />
         </ErrorBoundary>
⛔ Skipped due to learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Keep functions under 50 lines and files under 800 lines
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).
web/src/pages/settings/CodeEditorPanel.tsx (3)

89-109: LGTM!

The parseText function properly validates:

  1. Size limit (MAX_EDITOR_BYTES) to prevent DoS
  2. Top-level must be an object (not array/primitive)
  3. Each namespace value must be an object

Using YAML.CORE_SCHEMA is the correct choice for settings data as it avoids YAML's unsafe type coercion (e.g., !!python/object).


59-87: LGTM!

The extracted buildChanges function properly addresses prior review concerns:

  • Unknown keys are collected and rejected
  • Env-sourced settings are collected and rejected
  • Only changed values are included in the changes map
  • Non-string values are JSON-stringified

This enforces the edit constraints and prevents bypassing the GUI's hidden/advanced filtering through the code editor.


196-203: LGTM!

The save flow correctly handles concurrent edits by capturing textBeforeSave and only clearing dirty state if the text hasn't changed during the async operation. Combined with the disabled={saving} on UI controls, this prevents the "edits lost during save" issue from the prior review.

web/src/pages/SettingsNamespacePage.tsx (6)

49-50: Type assertion without runtime guard is still present.

The unconditional cast namespace as SettingNamespace at line 50 happens before the validNamespace guard is used. While safe because ns is only used after the guard, this was previously flagged. The cleaner pattern using find() would provide true type narrowing.


52-62: LGTM!

The filtering logic is well-structured with correct dependency array. The composite key pattern (namespace/key) aligns with the PR's conventions, and the filtering order (namespace → hidden → advanced → search) is efficient.


69-71: LGTM!

The loading check loading && entries.length === 0 is a good pattern—it shows the skeleton only on initial load while allowing stale data to remain visible during background refreshes.


105-125: LGTM!

The error and warning banners now use cn() to split class names across lines, addressing the previous line-length concerns. The semantic color tokens (border-danger/30, bg-danger/5, etc.) follow the design system guidelines.


138-149: LGTM!

Good use of ErrorBoundary with level="section" to isolate failures. The forceOpen prop appropriately keeps the single namespace expanded on this dedicated page.


152-158: LGTM!

The FloatingSaveBar is correctly wired with dirty state count, save status, handlers, and error state from the appropriate hooks.

Comment on lines +130 to +138
// Sync from entries when not dirty
const prevEntriesRef = useRef<typeof entries | undefined>(undefined)
if (entries !== prevEntriesRef.current) {
prevEntriesRef.current = entries
if (!dirty) {
setText(serializeEntries(entries, format))
setParseError(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.

🧹 Nitpick | 🔵 Trivial

Ref-based sync pattern runs during render which may cause issues.

The sync logic on lines 132-138 executes during render (not in an effect), comparing object references directly. While this works because entries array reference changes when data updates, this pattern:

  1. Runs synchronously during render which can cause subtle issues
  2. May miss updates if the same array is mutated in place (though unlikely with React state)

This is a common pattern in React for "deriving state from props" but could be more explicit with useEffect or a controlled component approach.

♻️ Alternative using useEffect
-  const prevEntriesRef = useRef<typeof entries | undefined>(undefined)
-  if (entries !== prevEntriesRef.current) {
-    prevEntriesRef.current = entries
-    if (!dirty) {
-      setText(serializeEntries(entries, format))
-      setParseError(null)
-    }
-  }
+  const prevEntriesRef = useRef<typeof entries | undefined>(undefined)
+  useEffect(() => {
+    if (entries !== prevEntriesRef.current) {
+      prevEntriesRef.current = entries
+      if (!dirty) {
+        setText(serializeEntries(entries, format))
+        setParseError(null)
+      }
+    }
+  }, [entries, dirty, format])

Note: The current pattern is intentional to avoid effect timing issues and is valid per React docs on "adjusting state during render."

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

In `@web/src/pages/settings/CodeEditorPanel.tsx` around lines 130 - 138, The
sync-from-entries logic that compares prevEntriesRef to entries should be moved
out of render into a useEffect to avoid running synchronously during render;
wrap the comparison and update in a useEffect that depends on [entries, dirty,
format] and inside it update prevEntriesRef.current, call
setText(serializeEntries(entries, format)) and setParseError(null) only when
entries changed and !dirty, ensuring you reference the same prevEntriesRef,
serializeEntries, setText, setParseError, entries, dirty and format symbols so
the behavior remains the same but runs after render.

const [collapsed, setCollapsed] = useState(false)
const isOpen = forceOpen || !collapsed
const groups = groupByGroup(entries)
const contentId = `ns-${displayName.replace(/\s+/g, '-').toLowerCase()}-content`
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 more robust ID sanitization for contentId.

The current regex only replaces spaces. If displayName contains special characters (e.g., &, <, or Unicode), the generated ID may be invalid or cause accessibility issues with aria-controls.

♻️ More robust sanitization
-  const contentId = `ns-${displayName.replace(/\s+/g, '-').toLowerCase()}-content`
+  const contentId = `ns-${displayName.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-content`
📝 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 contentId = `ns-${displayName.replace(/\s+/g, '-').toLowerCase()}-content`
const contentId = `ns-${displayName.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}-content`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/NamespaceSection.tsx` at line 77, The generated
contentId using const contentId = `ns-${displayName.replace(/\s+/g,
'-').toLowerCase()}-content` is fragile because it only replaces spaces and can
produce invalid or problematic HTML IDs for values with special characters or
Unicode; update the sanitization logic for displayName used when creating
contentId (and any matching aria-controls usage) to: normalize Unicode (e.g.,
NFKD), strip diacritics, convert to lower-case, replace any sequence of
non-alphanumeric characters with a single hyphen, collapse multiple hyphens, and
trim leading/trailing hyphens so the resulting ID contains only safe characters
(letters, numbers, hyphens/underscores) and is stable for accessibility
references like contentId and aria-controls.

Comment on lines 25 to +160
export default function SettingsNamespacePage() {
return <PlaceholderPage title="Settings Namespace" />
const { namespace } = useParams<{ namespace: string }>()
const {
entries,
loading,
error,
saving,
saveError,
wsConnected,
wsSetupError,
updateSetting,
} = useSettingsData()

const storeSavingKeys = useSettingsStore((s) => s.savingKeys)

const [searchQuery, setSearchQuery] = useState('')
const [advancedMode] = useState(() => localStorage.getItem(SETTINGS_ADVANCED_KEY) === 'true')

const {
dirtyValues,
handleValueChange,
handleDiscard,
handleSave,
} = useSettingsDirtyState(entries, updateSetting)
const validNamespace = NAMESPACE_ORDER.includes(namespace as SettingNamespace)
const ns = namespace as SettingNamespace

const filteredEntries = useMemo(() => {
if (!validNamespace) return []
return entries.filter((e) => {
if (e.definition.namespace !== ns) return false
const compositeKey = `${e.definition.namespace}/${e.definition.key}`
if (HIDDEN_SETTINGS.has(compositeKey)) return false
if (!advancedMode && e.definition.level === 'advanced') return false
if (searchQuery && !matchesSetting(e, searchQuery)) return false
return true
})
}, [entries, ns, validNamespace, advancedMode, searchQuery])

const controllerDisabledMap = useMemo(
() => buildControllerDisabledMap(entries, dirtyValues),
[entries, dirtyValues],
)

if (loading && entries.length === 0) {
return <SettingsSkeleton />
}

if (!validNamespace) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button asChild variant="ghost" size="icon">
<Link to={ROUTES.SETTINGS}><ArrowLeft className="size-4" /></Link>
</Button>
<h1 className="text-lg font-semibold text-foreground">Settings</h1>
</div>
<EmptyState
icon={Settings}
title="Unknown namespace"
description={`"${namespace}" is not a valid settings namespace.`}
/>
</div>
)
}

const displayName = NAMESPACE_DISPLAY_NAMES[ns]

return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4">
<Button asChild variant="ghost" size="icon">
<Link to={ROUTES.SETTINGS}><ArrowLeft className="size-4" /></Link>
</Button>
<h1 className="text-lg font-semibold text-foreground">{displayName} Settings</h1>
</div>
<SearchInput value={searchQuery} onChange={setSearchQuery} className="w-64" />
</div>

{error && (
<div className={cn(
'flex items-center gap-2 rounded-lg',
'border border-danger/30 bg-danger/5',
'px-4 py-2 text-sm text-danger',
)}>
<AlertTriangle className="size-4 shrink-0" />
{error}
</div>
)}

{!wsConnected && !loading && (
<div className={cn(
'flex items-center gap-2 rounded-lg',
'border border-warning/30 bg-warning/5',
'px-4 py-2 text-sm text-warning',
)}>
<WifiOff className="size-4 shrink-0" />
{wsSetupError ?? 'Real-time updates disconnected. Data may be stale.'}
</div>
)}

{filteredEntries.length === 0 ? (
<EmptyState
icon={Settings}
title={searchQuery ? 'No matching settings' : 'No settings available'}
description={
searchQuery
? 'Try a different search term or clear the filter.'
: `No ${displayName.toLowerCase()} settings are available.`
}
/>
) : (
<ErrorBoundary level="section">
<NamespaceSection
displayName={displayName}
icon={<Settings className="size-4" />}
entries={filteredEntries}
dirtyValues={dirtyValues}
onValueChange={handleValueChange}
savingKeys={storeSavingKeys}
controllerDisabledMap={controllerDisabledMap}
forceOpen
/>
</ErrorBoundary>
)}

<FloatingSaveBar
dirtyCount={dirtyValues.size}
saving={saving}
onSave={handleSave}
onDiscard={handleDiscard}
saveError={saveError}
/>
</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 extracting render helpers to reduce component length.

The component body spans ~135 lines, exceeding the 50-line guideline. While the logic is well-organized via hooks, the JSX render could be split into helper components or render functions for the header, error/warning banners, and content sections.

♻️ Optional extraction example
// Extract banner rendering
function ErrorBanner({ error }: { error: string }) {
  return (
    <div className={cn(
      'flex items-center gap-2 rounded-lg',
      'border border-danger/30 bg-danger/5',
      'px-4 py-2 text-sm text-danger',
    )}>
      <AlertTriangle className="size-4 shrink-0" />
      {error}
    </div>
  )
}

// Then in component:
{error && <ErrorBanner error={error} />}

As per coding guidelines: "Keep functions under 50 lines and files under 800 lines"

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

In `@web/src/pages/SettingsNamespacePage.tsx` around lines 25 - 160, The
SettingsNamespacePage component is too long; extract small render
helpers/components to reduce its body under 50 lines: create a Header component
(props: displayName, onBack, searchQuery, setSearchQuery) for the top bar, an
ErrorBanner component (props: error) and a WarningBanner component (props:
wsSetupError) for the two banner blocks, and a ContentSection component (props:
filteredEntries, displayName, dirtyValues, handleValueChange, storeSavingKeys,
controllerDisabledMap, handleSave, handleDiscard, saving, saveError) that
renders the EmptyState/NamespaceSection and FloatingSaveBar; replace the inline
JSX in SettingsNamespacePage with these helpers and pass the existing symbols
(useSettingsData outputs, NAMESPACE_DISPLAY_NAMES, filteredEntries, dirtyValues,
handleValueChange, handleSave, handleDiscard, storeSavingKeys,
controllerDisabledMap, wsSetupError, error, searchQuery/setSearchQuery) so
behavior stays identical.

Aureliolo and others added 9 commits March 28, 2026 18:28
…ring (#784)

Implement full Settings page replacing placeholder stubs. Includes fuzzy
search across all settings, collapsible namespace sections with group
sub-headings, dependency indicators, floating save bar, advanced mode
toggle with warning dialog, GUI/Code editor toggle (JSON/YAML), source
badges (DB/env/YAML/default), restart-required badges, and env-sourced
read-only enforcement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract shared utilities (matchesSetting, isControllerDisabled,
buildControllerDisabledMap, saveSettingsBatch) to utils.ts, eliminating
duplication across SettingsPage and SettingsNamespacePage. Fix silent
error swallowing in refreshEntries (let errors propagate to usePolling),
add nested validation in CodeEditorPanel parseText, surface format
conversion errors, separate parse/save error handling, improve batch
save to track failed keys and only clear succeeded entries. Remove unused
namespace prop from NamespaceSection, log invalid regex patterns in
SettingField, fix resetSetting two-step partial failure handling.
Update page-structure.md to match actual implementation (collapsible
sections layout, _schema endpoint paths).

Pre-reviewed by 4 agents, 14 findings addressed.

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

Bugs fixed:
- Slider treats valid "0" as falsy, showing min_value instead
- SearchInput clear button doesn't cancel pending debounce
- handleSave wipes drafts added during in-flight batch save
- CodeEditorPanel setDirty(false) called even on partial save failure
- Array textarea serializes on every keystroke, breaking typing
- Code editor diff misses deleted keys (now rejects with guidance)

Error handling:
- Add logging to resetSetting bare catch (was completely silent)
- Handle unhandled promise rejection in updateFromWsEvent
- Log individual rejection reasons in saveSettingsBatch
- Add top-level try-catch to handleSave/handleCodeSave
- Add console.warn to parseArrayValue empty catch
- Refactor resetSetting with finally block (DRY savingKeys cleanup)

State management:
- handleValueChange removes key when reverted to persisted value
- handleCodeSave reconciles GUI dirtyValues after code-mode save
- codeEntries overlays dirty values so Code mode sees GUI edits
- Snapshot dirty keys before async batch to preserve concurrent edits
- Use Zustand selectors for updateSetting/resetSetting in hook

Accessibility:
- Add aria-live="assertive" to FloatingSaveBar save error
- Add aria-controls to NamespaceSection collapse button

Security:
- Add 64 KiB input size guard before YAML/JSON parse
- Fix compositeKey.split('/') to handle keys containing slashes

Search:
- Replace substring matching with fuzzy subsequence matching

Code quality:
- Fix parseText type to Record<string, Record<string, unknown>>
- Use cn() for CodeEditorPanel format toggle buttons
- Add duplicate guard to SETTING_DEPENDED_BY reverse-map
- Fix isControllerDisabled JSDoc to match implementation
- Remove redundant JSX section divider comments
- Remove redundant SettingField branch comments
- onSave now returns Set<string> for partial-failure feedback

Tests:
- Fix mock updateSetting return type (was undefined, now SettingEntry)
- Add wsSetupError passthrough test
- Use toHaveBeenCalledOnce() for polling assertion
- Add polling stop on unmount test

Docs:
- Add settings/ to CLAUDE.md pages subdirectory examples
- Document advanced-mode dialog, dependency map, backup CRUD deferral

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add assertion guard before reading mock.calls in WS test
- Add explanatory comment for dynamic mock key pattern
- Break long className strings with cn() in NamespaceSection,
  SettingsPage, and SettingsNamespacePage banner divs
- Split long RegExp + eslint-disable in SettingField
- Break long console.warn in settings store fetchCurrency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Guard NamespaceSection toggle when forceOpen is true so hidden
  collapsed state doesn't replay when search clears
- Reject empty string in int/float validation (Number('') === 0
  was silently passing)
- Clear advanced-only drafts from dirtyValues when disabling
  advanced mode so hidden settings don't count toward save bar
- Hide SearchInput in Code view mode (was visible but inert)
- Skip refreshEntries while saves are in progress to prevent
  stale GET responses from overwriting fresh PUT results
- Sync standalone currency field when budget/currency is updated
  or reset via updateSetting/resetSetting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add test verifying advanced settings appear in advanced mode
- Add rows={20} to code editor textarea for assistive tech
- Extract NamespaceSettingRow from >8-line map body (CLAUDE.md rule)
- Add defensive guard for malformed composite keys (missing slash)
- Fix handleCodeSave to unconditionally clear succeeded GUI drafts
  (was keeping stale drafts when code-mode saved a different value)
- Extract pruneAdvancedDrafts and use in both handleAdvancedToggle
  and AdvancedModeBanner.onDisable for consistent behavior
- Sync currency field in fetchSettingsData and refreshEntries via
  deriveCurrency helper (was only synced in fetchCurrency)

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

- Validate parsed keys in code editor: reject unknown keys (not in
  schema) and env-sourced keys (read-only) before saving
- Freeze textarea and format toggles during save to prevent mid-save
  edits; only clear dirty if text unchanged after save completes
- Guard Code toggle: confirm discard when code editor has unsaved
  changes (onDirtyChange callback from CodeEditorPanel)
- Extract NamespaceSettingRowProps interface from inline object type
- Normalize underscores/dashes to spaces in fuzzy matcher so
  "api prefix" matches key "api_prefix"
- Add defensive guard for malformed composite keys (slashIdx < 1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sequence initial fetch before polling to avoid duplicate
  getAllSettings calls on mount (useSettingsData)
- Extract detectRemovedKeys and buildChanges from handleSave to
  bring function under 50-line limit (CodeEditorPanel)
- Remove redundant key prop on ErrorBoundary inside
  NamespaceSettingRow (parent map already sets key)
- Extract useSettingsDirtyState hook for shared dirty-state logic
  (persistedValues, handleValueChange, handleDiscard, handleSave)
  used by both SettingsPage and SettingsNamespacePage
- Replace window.confirm with ConfirmDialog for code editor
  discard confirmation (consistent with advanced mode dialog)
- Normalize underscores/dashes to spaces in fuzzy matcher

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use .finally() instead of .then() so polling starts even if
  the initial fetch rejects (was a dead-end on failure)
- Add isSavingRef guard in useSettingsDirtyState to prevent
  overlapping saveSettingsBatch calls from double-clicks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aureliolo Aureliolo force-pushed the feat/settings-page branch from 9593824 to c26c912 Compare March 28, 2026 17:28
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 17:29 — 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: 13

♻️ Duplicate comments (7)
web/src/pages/settings/RestartBadge.stories.tsx (1)

4-8: ⚠️ Potential issue | 🟡 Minor

Enable the Storybook a11y gate for this story.

This meta still lacks parameters.a11y.test, so the badge story is outside the repo's WCAG enforcement.

Suggested fix
 const meta = {
   title: 'Settings/RestartBadge',
   component: RestartBadge,
-  parameters: { layout: 'centered' },
+  parameters: { layout: 'centered', a11y: { test: 'error' } },
 } satisfies Meta<typeof RestartBadge>

As per coding guidelines: "set parameters.a11y.test for WCAG compliance enforcement".

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

In `@web/src/pages/settings/RestartBadge.stories.tsx` around lines 4 - 8, The
story meta for RestartBadge is missing the Storybook a11y gate; update the
exported meta object (the meta constant in RestartBadge.stories.tsx) to include
parameters.a11y.test (e.g., set parameters.a11y.test: true or the repo's
required test config) so the story is covered by the WCAG enforcement; ensure
you add it alongside the existing parameters key without removing other
properties like layout or component.
web/src/stores/settings.ts (1)

113-123: ⚠️ Potential issue | 🟠 Major

savingKeys doesn't prevent refresh-vs-refresh races.

This guard only protects against local saves. Polling and WebSocket-triggered refreshEntries() calls can still resolve out of order, and the older snapshot will overwrite the newer one if it finishes last. Keep a refresh request id/timestamp and ignore stale responses before calling set.

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

In `@web/src/stores/settings.ts` around lines 113 - 123, The refreshEntries
function can still suffer from out-of-order responses; add a monotonic request
id or timestamp (e.g., lastRefreshId or lastRefreshAt) stored in the settings
module/state and increment/set it immediately before calling
settingsApi.getAllSettings(), capture the current id into a local variable, then
after the await compare the local id to the stored lastRefreshId/lastRefreshAt
and abort (return) if they differ (stale response); keep the existing savingKeys
checks and the deriveCurrency/set logic but only apply set when the response is
the latest. Ensure the unique symbols referenced are refreshEntries,
settingsApi.getAllSettings, savingKeys, deriveCurrency, set, and the new
lastRefreshId/lastRefreshAt you add.
web/src/pages/settings/SourceBadge.stories.tsx (1)

4-8: ⚠️ Potential issue | 🟡 Minor

Enable the Storybook a11y gate on this meta config.

Without parameters.a11y.test, this story bypasses the repo's WCAG enforcement.

♿ Suggested fix
 const meta = {
   title: 'Settings/SourceBadge',
   component: SourceBadge,
-  parameters: { layout: 'centered' },
+  parameters: {
+    layout: 'centered',
+    a11y: { test: 'error' },
+  },
 } satisfies Meta<typeof SourceBadge>

As per coding guidelines: "set parameters.a11y.test for WCAG compliance enforcement."

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

In `@web/src/pages/settings/SourceBadge.stories.tsx` around lines 4 - 8, The
Storybook meta for SourceBadge is missing the a11y gate; update the meta object
(meta) to include parameters.a11y.test with a WCAG test configuration (e.g., a
boolean or the recommended test options) so Storybook runs the accessibility
check—modify the existing parameters field on the meta object (parameters) to
add an a11y.test entry to enforce the repo's WCAG checks.
web/src/pages/settings/SearchInput.tsx (1)

17-28: ⚠️ Potential issue | 🟠 Major

Don't sync value by calling setLocal during render.

This forces a render-time state update, and any queued debounce is still left alive, so an external clear/reset can be overwritten by the stale callback a moment later. Move the prop sync into an effect and cancel timerRef.current before applying the new value.

♻️ Suggested fix
-  // Sync external value changes (e.g. clearing from parent)
-  const prevValueRef = useRef(value)
-  if (value !== prevValueRef.current) {
-    prevValueRef.current = value
-    setLocal(value)
-  }
+  useEffect(() => {
+    if (timerRef.current) {
+      clearTimeout(timerRef.current)
+      timerRef.current = null
+    }
+    setLocal(value)
+  }, [value])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/SearchInput.tsx` around lines 17 - 28, Move the
prop-to-local sync out of render and into an effect: instead of comparing value
to prevValueRef.current and calling setLocal during render, create a useEffect
that runs when value changes, clears any pending debounce via timerRef.current
(if set), updates prevValueRef.current, and then calls setLocal(value); ensure
you still use the existing prevValueRef, timerRef, setLocal and value
identifiers so the stale debounced callback cannot overwrite the externally
provided value.
web/src/hooks/useSettingsData.ts (1)

41-45: ⚠️ Potential issue | 🟠 Major

Catch the initial load before finally().

finally() does not absorb a rejection. If fetchSettingsData() fails, this fire-and-forget chain still produces an unhandled promise rejection even though polling starts afterward.

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

In `@web/src/hooks/useSettingsData.ts` around lines 41 - 45, The current
fire-and-forget call to useSettingsStore.getState().fetchSettingsData() uses
.finally() but doesn't handle rejections, causing unhandled promise rejections;
change the call to handle errors before finally (e.g., await or attach
.catch(...) to fetchSettingsData() or wrap it in an async IIFE with
try/catch/finally) so that any rejection is caught and pollingStart() (the
existing finally work) still runs only when not cancelled; update the useEffect
invocation that references cancelled, fetchSettingsData, and pollingStart
accordingly.
web/src/pages/settings/CodeEditorPanel.tsx (1)

197-200: ⚠️ Potential issue | 🟠 Major

This post-save dirty check never detects newer edits.

text and textBeforeSave are captured from the same render, so this comparison is always true after the await. If the user edits before saving disables the editor, a successful save still clears dirty and can hide newer unsaved text. Compare against a ref/current edit version instead of the closed-over state.

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

In `@web/src/pages/settings/CodeEditorPanel.tsx` around lines 197 - 200, The
post-save dirty check uses closed-over render state (text and textBeforeSave) so
the equality check after awaiting onSave always matches; change it to compare
the latest editor value (e.g., a ref or editor API) instead of the captured
text: when awaiting onSave(changes) completes, read the current edit value from
the shared ref (e.g., latestTextRef.current) or editor.getValue() and call
updateDirty(false) only if failedKeys.size === 0 and that current value equals
the pre-save snapshot; update usages of textBeforeSave/text to use that
ref/current value and ensure latestTextRef is kept in sync on every edit.
web/src/pages/SettingsPage.tsx (1)

155-166: ⚠️ Potential issue | 🟠 Major

Disabling Advanced mode can strand a dirty code buffer in an invalid state.

Both disable paths only prune dirtyValues. In code view, CodeEditorPanel keeps its own dirty text and refuses to resync while dirty, so advanced keys can remain in the textarea after Advanced mode is off; the next save then fails as unknown keys because entryLookup no longer contains them. Reuse the existing discard/reset flow here and funnel both disable paths through the same handler.

Also applies to: 242-248

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

In `@web/src/pages/SettingsPage.tsx` around lines 155 - 166, handleAdvancedToggle
currently only calls pruneAdvancedDrafts when turning advanced mode off, which
leaves CodeEditorPanel's internal dirty text in place and causes unknown-key
save failures; update handleAdvancedToggle to funnel both disable paths through
the same discard/reset flow used by the UI (the handler wired to the
"Discard/Reset" action for advanced entries) instead of only pruning
dirtyValues, so that CodeEditorPanel's dirty buffer is cleared/resynced
(reference handleAdvancedToggle, pruneAdvancedDrafts, CodeEditorPanel and the
existing discard/reset handler) before calling setAdvancedMode(false) and
persisting SETTINGS_ADVANCED_KEY.
🤖 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/hooks/useSettingsData.ts`:
- Line 8: Change the broad forwarding of all 'system' events so only
settings-related system events trigger a refresh: narrow SETTINGS_CHANNELS
handling in useSettingsData by adding a predicate when reading WS events (before
calling updateFromWsEvent) that checks the event type/subject is the settings
event (e.g., event.type === 'settings' or event.event === 'settings'), and
likewise update the store logic around refreshEntries() to only run when
event.channel === 'system' && the event indicates settings change; modify the
code paths in updateFromWsEvent() and where refreshEntries() is invoked to use
this predicate so unrelated system startup/shutdown/error events are ignored.

In `@web/src/pages/settings/CodeEditorPanel.tsx`:
- Around line 111-264: CodeEditorPanel is too large and mixes UI with
parsing/validation/save logic; refactor by extracting the toolbar (format
buttons) into a CodeEditorToolbar component, the footer (Save/Reset buttons +
status) into a CodeEditorFooter component, and the parsing/diff/save
orchestration into a hook (e.g., useCodeEditor or useCodeEditorState) that
exposes text, format, parseError, dirty, handlers (handleChange,
handleFormatChange, handleSave, handleReset). Move usages of serializeEntries,
parseText, entriesToObject, detectRemovedKeys, and buildChanges into that hook
so CodeEditorPanel only composes CodeEditorToolbar, the textarea, and
CodeEditorFooter and stays under 50 lines; ensure the hook accepts entries,
onSave, onDirtyChange and returns the same behaviors and state used in the
current component (including entryLookup logic and updateDirty).
- Around line 89-92: In parseText, the size check uses text.length (UTF-16 code
units) instead of byte length; replace that check with the UTF-8 byte length
computed by new TextEncoder().encode(text).length and keep using
MAX_EDITOR_BYTES for the limit and the existing error message (e.g., `Input too
large (max ${MAX_EDITOR_BYTES / 1024} KiB)`) so the guard correctly prevents
inputs exceeding MAX_EDITOR_BYTES bytes.

In `@web/src/pages/settings/DependencyIndicator.tsx`:
- Around line 15-22: The DependencyIndicator component currently renders a
non-focusable <span> with the tooltip only in the title attribute, which makes
it inaccessible to keyboard/screen-reader users; update DependencyIndicator to
use a focusable trigger (e.g., switch the outer element to a <button> or add
tabIndex={0} and role="button" to the container) and expose the tooltip via
accessible APIs (create an id for the tooltip, render the tooltip content either
as a visible helper element or as an ARIA tooltip/popup and reference it with
aria-describedby on the trigger), ensure the Link2 icon keeps aria-hidden and
keep a sr-only or visible label tied to the tooltip id so keyboard and
screen-reader users get the same explanatory text as mouse users.

In `@web/src/pages/settings/FloatingSaveBar.tsx`:
- Around line 28-36: The floating bar can be forced wide by long saveError text;
in FloatingSaveBar.tsx update the sticky container (the element with className
including "w-fit") to allow wrapping (add "flex-wrap" and remove or replace
"w-fit" so the bar can grow vertically), and constrain the error message span
that renders saveError (the span with role="alert") by adding a max-width (e.g.,
"max-w-[40ch]" or "max-w-xs") plus word-wrapping ("break-words" or
"whitespace-normal") or truncation ("truncate") so long API/validation text
won't push the Save/Discard buttons off-screen; keep dirtyCount rendering
unchanged.

In `@web/src/pages/settings/NamespaceSection.tsx`:
- Around line 105-125: The JSX passed into the groups.entries() map inside
NamespaceSection is too large; extract the rendered group block into a new
helper component (e.g., NamespaceGroup) and replace the inline callback with a
simple map that returns <NamespaceGroup key={group} group={group}
groupEntries={groupEntries} ...props>. Implement NamespaceGroup to accept props
(group, groupEntries, dirtyValues, onValueChange, savingKeys,
controllerDisabledMap) and render the header conditional on groups.size > 1 and
the inner groupEntries.map producing NamespaceSettingRow (using the same key
generation `${entry.definition.namespace}/${entry.definition.key}`) so the
iteration logic in NamespaceSection is only data selection and the JSX stays
under the 8-line guideline.

In `@web/src/pages/settings/SettingField.tsx`:
- Around line 50-55: The draft state (draft, setDraft) is being overwritten
whenever the incoming prop value changes because prevValueRef logic always calls
setDraft(parseArrayValue(value)); change this to track a local dirty flag (e.g.
isDirtyRef or isDirty state) set to true when the user edits the textarea and
set to false on blur/save, and only resync from props when the draft is clean;
specifically update the block using prevValueRef and value to check both value
!== prevValueRef.current && !isDirtyRef.current before calling
setDraft(parseArrayValue(value)), keep parseArrayValue for parsing, and ensure
you clear isDirty when the value is committed (onBlur/onSave) so future external
updates will resync.

In `@web/src/pages/SettingsNamespacePage.tsx`:
- Around line 77-79: The icon-only "back" controls render an unnamed interactive
element; update both instances (the Button+Link containing <ArrowLeft .../> at
the top and the second occurrence around lines 97-99) to include an accessible
name by adding an aria-label (e.g., aria-label="Back to settings") to the
interactive element—place the aria-label on the Button (or the Link if Button
forwards aria props) so screen readers receive the label while preserving the
existing Button asChild, variant="ghost", size="icon" and the Link
to={ROUTES.SETTINGS} with the ArrowLeft icon.
- Around line 40-42: The SettingsNamespacePage currently snapshots
SETTINGS_ADVANCED_KEY into a read-only advancedMode state (const [advancedMode]
= useState(...)) so namespace deep links cannot reveal advanced-only settings;
change advancedMode to a controlled state (const [advancedMode, setAdvancedMode]
= useState(...)) and either render the same advanced-mode toggle/banner
component used on the main Settings page (so users can flip it and re-check the
namespace) or, alternatively, perform a redirect through the main /settings
route preserving the namespace query/params so the existing toggle UI is shown;
ensure updates write back to localStorage (SETTINGS_ADVANCED_KEY) and that the
component re-renders to show advanced entries (affecting the logic in the
settings list render paths referenced around the blocks handling empty-state
rendering).

In `@web/src/pages/SettingsPage.tsx`:
- Around line 43-51: The NAMESPACE_ICONS map currently uses Record<string,
React.ReactNode>, hiding missing namespaces; change its type to
Record<SettingNamespace, React.ReactNode> (or alternatively use
Partial<Record<SettingNamespace, React.ReactNode>> with a runtime fallback) and
ensure all nine SettingNamespace values are present in NAMESPACE_ICONS (or
explicitly handle missing keys when rendering), referencing the NAMESPACE_ICONS
constant and the SettingNamespace type so new namespaces will surface as compile
errors rather than silently falling back to the generic icon.
- Around line 53-323: SettingsPage is too large and contains complex JSX inside
a .map(); extract the namespace list item and the mode-specific rendering into
smaller components to follow the guideline. Create a NamespaceListItem component
that encapsulates the StaggerItem + ErrorBoundary + NamespaceSection rendering
(accept props: ns, entries from filteredByNamespace.get(ns), dirtyValues,
handleValueChange, storeSavingKeys, controllerDisabledMap, searchQuery) and
replace the inline map body with <NamespaceListItem key={ns} ... />; also
extract the code/gui conditional block into a ModePanel component that receives
codeEntries, handleCodeSave, saving, setCodeDirty, filteredByNamespace,
searchQuery, dirtyValues, handleValueChange, storeSavingKeys,
controllerDisabledMap, handleSave, handleDiscard, saveError and returns the
CodeEditorPanel or the EmptyState+StaggerGroup+FloatingSaveBar. Keep
SettingsPage responsible for state and pass down the necessary handlers (e.g.,
handleCodeSave, handleValueChange, handleSave, handleDiscard) and derived data
(filteredByNamespace, codeEntries, controllerDisabledMap) so each new component
stays under ~50 lines.

In `@web/src/stores/settings.ts`:
- Around line 120-123: The settings store leaves a stale error after a prior
fetch failure because fetchSettingsData() sets error but successful paths
(refreshEntries()/post-reset refetch) don't clear it; update the success
branches where you build the patch (e.g., in refreshEntries() and the post-reset
refetch code that currently does "const patch: Partial<SettingsState> = {
entries }" followed by deriving currency and set(patch)) to explicitly set
patch.error = null before calling set(patch) so the old error is cleared when
new data arrives (apply same change to the other similar block around the
192-199 region).

---

Duplicate comments:
In `@web/src/hooks/useSettingsData.ts`:
- Around line 41-45: The current fire-and-forget call to
useSettingsStore.getState().fetchSettingsData() uses .finally() but doesn't
handle rejections, causing unhandled promise rejections; change the call to
handle errors before finally (e.g., await or attach .catch(...) to
fetchSettingsData() or wrap it in an async IIFE with try/catch/finally) so that
any rejection is caught and pollingStart() (the existing finally work) still
runs only when not cancelled; update the useEffect invocation that references
cancelled, fetchSettingsData, and pollingStart accordingly.

In `@web/src/pages/settings/CodeEditorPanel.tsx`:
- Around line 197-200: The post-save dirty check uses closed-over render state
(text and textBeforeSave) so the equality check after awaiting onSave always
matches; change it to compare the latest editor value (e.g., a ref or editor
API) instead of the captured text: when awaiting onSave(changes) completes, read
the current edit value from the shared ref (e.g., latestTextRef.current) or
editor.getValue() and call updateDirty(false) only if failedKeys.size === 0 and
that current value equals the pre-save snapshot; update usages of
textBeforeSave/text to use that ref/current value and ensure latestTextRef is
kept in sync on every edit.

In `@web/src/pages/settings/RestartBadge.stories.tsx`:
- Around line 4-8: The story meta for RestartBadge is missing the Storybook a11y
gate; update the exported meta object (the meta constant in
RestartBadge.stories.tsx) to include parameters.a11y.test (e.g., set
parameters.a11y.test: true or the repo's required test config) so the story is
covered by the WCAG enforcement; ensure you add it alongside the existing
parameters key without removing other properties like layout or component.

In `@web/src/pages/settings/SearchInput.tsx`:
- Around line 17-28: Move the prop-to-local sync out of render and into an
effect: instead of comparing value to prevValueRef.current and calling setLocal
during render, create a useEffect that runs when value changes, clears any
pending debounce via timerRef.current (if set), updates prevValueRef.current,
and then calls setLocal(value); ensure you still use the existing prevValueRef,
timerRef, setLocal and value identifiers so the stale debounced callback cannot
overwrite the externally provided value.

In `@web/src/pages/settings/SourceBadge.stories.tsx`:
- Around line 4-8: The Storybook meta for SourceBadge is missing the a11y gate;
update the meta object (meta) to include parameters.a11y.test with a WCAG test
configuration (e.g., a boolean or the recommended test options) so Storybook
runs the accessibility check—modify the existing parameters field on the meta
object (parameters) to add an a11y.test entry to enforce the repo's WCAG checks.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 155-166: handleAdvancedToggle currently only calls
pruneAdvancedDrafts when turning advanced mode off, which leaves
CodeEditorPanel's internal dirty text in place and causes unknown-key save
failures; update handleAdvancedToggle to funnel both disable paths through the
same discard/reset flow used by the UI (the handler wired to the "Discard/Reset"
action for advanced entries) instead of only pruning dirtyValues, so that
CodeEditorPanel's dirty buffer is cleared/resynced (reference
handleAdvancedToggle, pruneAdvancedDrafts, CodeEditorPanel and the existing
discard/reset handler) before calling setAdvancedMode(false) and persisting
SETTINGS_ADVANCED_KEY.

In `@web/src/stores/settings.ts`:
- Around line 113-123: The refreshEntries function can still suffer from
out-of-order responses; add a monotonic request id or timestamp (e.g.,
lastRefreshId or lastRefreshAt) stored in the settings module/state and
increment/set it immediately before calling settingsApi.getAllSettings(),
capture the current id into a local variable, then after the await compare the
local id to the stored lastRefreshId/lastRefreshAt and abort (return) if they
differ (stale response); keep the existing savingKeys checks and the
deriveCurrency/set logic but only apply set when the response is the latest.
Ensure the unique symbols referenced are refreshEntries,
settingsApi.getAllSettings, savingKeys, deriveCurrency, set, and the new
lastRefreshId/lastRefreshAt you add.
🪄 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: 77e88540-35cc-4978-a598-2f37f07e9722

📥 Commits

Reviewing files that changed from the base of the PR and between 9593824 and c26c912.

📒 Files selected for processing (24)
  • CLAUDE.md
  • docs/design/page-structure.md
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/hooks/useSettingsDirtyState.ts
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/utils.ts
  • web/src/stores/settings.ts
  • web/src/utils/constants.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 Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{ts,tsx}: Use design tokens and semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) -- NEVER hardcode hex values or rgba in .tsx/.ts files
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/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 text-metric classes -- use <MetricCard>
Do NOT render initials circles manually -- use <Avatar>; do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Use TypeScript 6.0 features: remove deprecated baseUrl, accept that esModuleInterop is always true, explicitly list types if needed, use "bundler" or "nodenext" for moduleResolution
Never use real vendor names (OpenAI, Claude, GPT, Anthropic) in web dashboard code -- use generic names: example-provider, example-large-001, etc.
Keep functions under 50 lines and files under 800 lines
Handle errors explicitly, never silently swallow exceptions

Files:

  • web/src/pages/settings/SettingsSkeleton.tsx
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/SettingRow.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/hooks/useSettingsData.ts
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/utils/constants.ts
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/hooks/useSettingsDirtyState.ts
  • web/src/pages/settings/utils.ts
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/stores/settings.ts
web/src/**/*.stories.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use Storybook 10 ESM-only imports: storybook/test (not @storybook/test), storybook/actions (not @storybook/addon-actions); use definePreview from @storybook/react-vite; set parameters.a11y.test for WCAG compliance enforcement

Files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
docs/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

Design spec pages in docs/design/ are mandatory references before implementing features -- ALWAYS read the relevant design page

Files:

  • docs/design/page-structure.md
docs/design/**/*.md

📄 CodeRabbit inference engine (CLAUDE.md)

When approved deviations from the design spec occur during implementation, update the relevant docs/design/ page to reflect the new reality

Files:

  • docs/design/page-structure.md
🧠 Learnings (21)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
📚 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:

  • 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
📚 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-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/components/ui/**/*.{tsx} : Create a `.stories.tsx` file alongside each new shared component with all states (default, hover, loading, error, empty) for Storybook documentation

Applied to files:

  • CLAUDE.md
  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Always reuse existing shared components from `web/src/components/ui/` before creating new ones; export props as TypeScript interfaces; use design tokens exclusively; import `cn` from `@/lib/utils` for conditional class merging

Applied to files:

  • CLAUDE.md
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SearchInput.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.

Applied to files:

  • CLAUDE.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 docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.stories.tsx : Use Storybook 10 ESM-only imports: `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`); use `definePreview` from `storybook/react-vite`; set `parameters.a11y.test` for WCAG compliance enforcement

Applied to files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/__tests__/hooks/useSettingsData.test.ts
  • web/src/__tests__/pages/SettingsPage.test.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
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 text-metric classes -- use `<MetricCard>`

Applied to files:

  • web/src/pages/settings/RestartBadge.stories.tsx
  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/RestartBadge.tsx
  • web/src/pages/settings/SourceBadge.stories.tsx
  • web/src/pages/settings/AdvancedModeBanner.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use design tokens and semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) -- NEVER hardcode hex values or rgba in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/settings/SourceBadge.tsx
  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) -- NEVER hardcode values

Applied to files:

  • web/src/pages/settings/SourceBadge.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT render initials circles manually -- use `<Avatar>`; do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

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

Applied to files:

  • web/src/__tests__/pages/SettingsPage.test.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • docs/design/page-structure.md
  • web/src/utils/constants.ts
  • web/src/pages/settings/utils.ts
  • web/src/stores/settings.ts
📚 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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • docs/design/page-structure.md
  • web/src/stores/settings.ts
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.

Applied to files:

  • docs/design/page-structure.md
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Keep functions under 50 lines and files under 800 lines

Applied to files:

  • web/src/pages/settings/NamespaceSection.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/settings/CodeEditorPanel.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use TypeScript 6.0 features: remove deprecated `baseUrl`, accept that `esModuleInterop` is always true, explicitly list `types` if needed, use `"bundler"` or `"nodenext"` for `moduleResolution`

Applied to files:

  • web/src/pages/settings/utils.ts
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).

Applied to files:

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

Applied to files:

  • web/src/pages/settings/CodeEditorPanel.tsx
🔇 Additional comments (4)
CLAUDE.md (1)

219-219: Documentation update is aligned with the new Settings page structure.

Including settings/ in the pages/<page-name>/ examples correctly reflects the current frontend organization and improves discoverability.

web/src/pages/settings/AdvancedModeBanner.tsx (3)

1-2: Imports correctly use shared UI primitives.

Good reuse of the shared Button component instead of custom button markup.

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


4-6: Props contract is clean and explicit.

AdvancedModeBannerProps is minimal and correctly exported as a TypeScript interface.

Based on learnings: export props as TypeScript interfaces.


10-21: Banner markup and styling are implementation-ready.

Line 12 uses semantic/tokenized utility classes (no hardcoded color values), and Line 18 wires the disable action directly via onDisable.

As per coding guidelines: Use design tokens and semantic classes — NEVER hardcode hex values or rgba in .tsx/.ts files.

import type { SettingDefinition, SettingEntry, SettingNamespace, WsChannel } from '@/api/types'
import { SETTINGS_POLL_INTERVAL } from '@/utils/constants'

const SETTINGS_CHANNELS = ['system'] as const satisfies readonly WsChannel[]
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

Filter the system binding down to settings events.

This hook forwards every system event into updateFromWsEvent(), and the store currently refreshes on event.channel === 'system' alone. That means unrelated startup/shutdown/error traffic will also trigger refreshEntries(), which is noisy and can race in-flight saves.

Also applies to: 50-58

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

In `@web/src/hooks/useSettingsData.ts` at line 8, Change the broad forwarding of
all 'system' events so only settings-related system events trigger a refresh:
narrow SETTINGS_CHANNELS handling in useSettingsData by adding a predicate when
reading WS events (before calling updateFromWsEvent) that checks the event
type/subject is the settings event (e.g., event.type === 'settings' or
event.event === 'settings'), and likewise update the store logic around
refreshEntries() to only run when event.channel === 'system' && the event
indicates settings change; modify the code paths in updateFromWsEvent() and
where refreshEntries() is invoked to use this predicate so unrelated system
startup/shutdown/error events are ignored.

Comment on lines +89 to +92
function parseText(text: string, format: CodeFormat): ParsedSettings {
if (text.length > MAX_EDITOR_BYTES) {
throw new Error(`Input too large (max ${MAX_EDITOR_BYTES / 1024} KiB)`)
}
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:

# Find and read the CodeEditorPanel.tsx file
find . -type f -name "CodeEditorPanel.tsx" | head -20

Repository: Aureliolo/synthorg

Length of output: 106


🏁 Script executed:

# Also search for files in the web/src/pages/settings directory
ls -la web/src/pages/settings/ 2>/dev/null | head -30

Repository: Aureliolo/synthorg

Length of output: 1143


🏁 Script executed:

# Check if the file exists and read it
cat -n web/src/pages/settings/CodeEditorPanel.tsx 2>/dev/null | head -150

Repository: Aureliolo/synthorg

Length of output: 5901


🏁 Script executed:

# Check for other byte/size checks in the CodeEditorPanel.tsx file
rg "length|encode|bytes|size" web/src/pages/settings/CodeEditorPanel.tsx -n

Repository: Aureliolo/synthorg

Length of output: 577


🏁 Script executed:

# Search for TextEncoder usage elsewhere in the codebase to see if it's already used
rg "TextEncoder" web/src --type tsx --type ts

Repository: Aureliolo/synthorg

Length of output: 89


🏁 Script executed:

# Check the utils.ts file in the same directory for relevant utilities
cat -n web/src/pages/settings/utils.ts

Repository: Aureliolo/synthorg

Length of output: 4099


🏁 Script executed:

# Check the tsconfig to understand the target environment
cat -n tsconfig.json 2>/dev/null | head -30

Repository: Aureliolo/synthorg

Length of output: 44


Use TextEncoder().encode(text).length to measure actual bytes instead of UTF-16 code units.

text.length counts UTF-16 code units, not bytes. Multibyte JSON/YAML can exceed the 64 KiB limit while still passing this guard. Since MAX_EDITOR_BYTES represents a byte limit, use new TextEncoder().encode(text).length to accurately check the UTF-8 byte size.

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

In `@web/src/pages/settings/CodeEditorPanel.tsx` around lines 89 - 92, In
parseText, the size check uses text.length (UTF-16 code units) instead of byte
length; replace that check with the UTF-8 byte length computed by new
TextEncoder().encode(text).length and keep using MAX_EDITOR_BYTES for the limit
and the existing error message (e.g., `Input too large (max ${MAX_EDITOR_BYTES /
1024} KiB)`) so the guard correctly prevents inputs exceeding MAX_EDITOR_BYTES
bytes.

Comment on lines +111 to +264
export function CodeEditorPanel({ entries, onSave, saving, onDirtyChange }: CodeEditorPanelProps) {
const [format, setFormat] = useState<CodeFormat>('json')
const [text, setText] = useState('')
const [parseError, setParseError] = useState<string | null>(null)
const [dirty, setDirty] = useState(false)

const entryLookup = useMemo(() => {
const map = new Map<string, SettingEntry>()
for (const e of entries) {
map.set(`${e.definition.namespace}/${e.definition.key}`, e)
}
return map
}, [entries])

const updateDirty = useCallback((next: boolean) => {
setDirty(next)
onDirtyChange?.(next)
}, [onDirtyChange])

// Sync from entries when not dirty
const prevEntriesRef = useRef<typeof entries | undefined>(undefined)
if (entries !== prevEntriesRef.current) {
prevEntriesRef.current = entries
if (!dirty) {
setText(serializeEntries(entries, format))
setParseError(null)
}
}

const handleFormatChange = useCallback(
(newFormat: CodeFormat) => {
setFormat(newFormat)
if (!dirty) {
setText(serializeEntries(entries, newFormat))
} else {
// Try to convert existing text to new format
try {
const parsed = parseText(text, format)
if (newFormat === 'json') {
setText(JSON.stringify(parsed, null, 2))
} else {
setText(YAML.dump(parsed, { indent: 2, lineWidth: 120, noRefs: true, sortKeys: false }))
}
setParseError(null)
} catch {
setParseError(`Cannot convert to ${newFormat.toUpperCase()}: fix syntax errors first`)
setFormat(format) // revert format toggle
}
}
},
[dirty, entries, format, text],
)

const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setText(e.target.value)
updateDirty(true)
setParseError(null)
}, [updateDirty])

const handleSave = useCallback(async () => {
let parsed: ParsedSettings
try {
parsed = parseText(text, format)
} catch (err) {
setParseError(err instanceof Error ? err.message : `Failed to parse ${format.toUpperCase()}`)
return
}

const original = entriesToObject(entries)
const removed = detectRemovedKeys(original, parsed)
if (removed.length > 0) {
setParseError(`Cannot remove settings via code editor. Use GUI to reset. Removed: ${removed.join(', ')}`)
return
}

const { changes, unknownKeys, envKeys } = buildChanges(parsed, original, entryLookup)
if (unknownKeys.length > 0) {
setParseError(`Unknown setting(s): ${unknownKeys.join(', ')}`)
return
}
if (envKeys.length > 0) {
setParseError(`Cannot edit env-sourced setting(s): ${envKeys.join(', ')}`)
return
}
if (changes.size === 0) { updateDirty(false); return }

const textBeforeSave = text
const failedKeys = await onSave(changes)
if (failedKeys.size === 0) {
if (text === textBeforeSave) updateDirty(false)
} else {
setParseError(`${failedKeys.size} setting(s) failed to save.`)
}
}, [text, format, entries, entryLookup, onSave, updateDirty])

const handleReset = useCallback(() => {
setText(serializeEntries(entries, format))
updateDirty(false)
setParseError(null)
}, [entries, format, updateDirty])

return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => handleFormatChange('json')}
disabled={saving}
className={cn(
'rounded px-2.5 py-1 text-xs font-medium transition-colors',
format === 'json' ? 'bg-accent/10 text-accent' : 'text-text-muted hover:text-foreground',
)}
>
JSON
</button>
<button
type="button"
onClick={() => handleFormatChange('yaml')}
disabled={saving}
className={cn(
'rounded px-2.5 py-1 text-xs font-medium transition-colors',
format === 'yaml' ? 'bg-accent/10 text-accent' : 'text-text-muted hover:text-foreground',
)}
>
YAML
</button>
</div>

<textarea
value={text}
onChange={handleChange}
disabled={saving}
rows={20}
className="w-full min-h-96 rounded-lg border border-border bg-surface p-4 font-mono text-sm text-foreground outline-none focus:ring-2 focus:ring-accent resize-y disabled:opacity-60"
spellCheck={false}
aria-label={`${format.toUpperCase()} editor`}
/>

{parseError && (
<p className="text-xs text-danger" role="alert">{parseError}</p>
)}

<div className="flex items-center gap-3">
<Button onClick={handleSave} disabled={!dirty || saving}>
{saving ? 'Saving...' : `Save ${format.toUpperCase()}`}
</Button>
<Button variant="outline" onClick={handleReset} disabled={!dirty || saving}>
Reset
</Button>
{dirty && <span className="text-xs text-warning">Unsaved changes</span>}
</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

Break CodeEditorPanel into smaller units.

The component is still handling serialization, parsing, validation, diffing, save orchestration, dirty tracking, and rendering in one function. Pull the save/diff path and the toolbar/footer into smaller helpers/components so this stays testable and within the repo limit.

As per coding guidelines, "Keep functions under 50 lines and files under 800 lines".

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

In `@web/src/pages/settings/CodeEditorPanel.tsx` around lines 111 - 264,
CodeEditorPanel is too large and mixes UI with parsing/validation/save logic;
refactor by extracting the toolbar (format buttons) into a CodeEditorToolbar
component, the footer (Save/Reset buttons + status) into a CodeEditorFooter
component, and the parsing/diff/save orchestration into a hook (e.g.,
useCodeEditor or useCodeEditorState) that exposes text, format, parseError,
dirty, handlers (handleChange, handleFormatChange, handleSave, handleReset).
Move usages of serializeEntries, parseText, entriesToObject, detectRemovedKeys,
and buildChanges into that hook so CodeEditorPanel only composes
CodeEditorToolbar, the textarea, and CodeEditorFooter and stays under 50 lines;
ensure the hook accepts entries, onSave, onDirtyChange and returns the same
behaviors and state used in the current component (including entryLookup logic
and updateDirty).

Comment on lines +15 to +22
return (
<span
className={cn('inline-flex items-center text-text-muted', className)}
title={tooltip}
>
<Link2 className="size-3" aria-hidden />
<span className="sr-only">{tooltip}</span>
</span>
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

This dependency tooltip is mouse-only right now.

The dependent-setting list only lives in a title on a non-focusable <span>, so sighted keyboard users just get an unexplained icon. If this indicator is meant to explain why settings are controlled/disabled, switch to a focusable trigger with a real tooltip/popover or render visible helper text.

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

In `@web/src/pages/settings/DependencyIndicator.tsx` around lines 15 - 22, The
DependencyIndicator component currently renders a non-focusable <span> with the
tooltip only in the title attribute, which makes it inaccessible to
keyboard/screen-reader users; update DependencyIndicator to use a focusable
trigger (e.g., switch the outer element to a <button> or add tabIndex={0} and
role="button" to the container) and expose the tooltip via accessible APIs
(create an id for the tooltip, render the tooltip content either as a visible
helper element or as an ARIA tooltip/popup and reference it with
aria-describedby on the trigger), ensure the Link2 icon keeps aria-hidden and
keep a sr-only or visible label tied to the tooltip id so keyboard and
screen-reader users get the same explanatory text as mouse users.

Comment on lines +28 to +36
className="sticky bottom-4 z-10 mx-auto flex w-fit items-center gap-3 rounded-lg border border-border bg-surface px-4 py-2.5 shadow-lg"
>
<span className="text-sm text-text-secondary">
{dirtyCount} unsaved {dirtyCount === 1 ? 'change' : 'changes'}
</span>

{saveError && (
<span className="text-xs text-danger" role="alert" aria-live="assertive">
{saveError}
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

Keep long save errors from breaking the bar layout.

This is a single-row w-fit flex container, so a long API/validation message can force horizontal overflow and push the Save/Discard buttons off-screen on narrow viewports. Let the bar wrap or constrain the error text width here.

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

In `@web/src/pages/settings/FloatingSaveBar.tsx` around lines 28 - 36, The
floating bar can be forced wide by long saveError text; in FloatingSaveBar.tsx
update the sticky container (the element with className including "w-fit") to
allow wrapping (add "flex-wrap" and remove or replace "w-fit" so the bar can
grow vertically), and constrain the error message span that renders saveError
(the span with role="alert") by adding a max-width (e.g., "max-w-[40ch]" or
"max-w-xs") plus word-wrapping ("break-words" or "whitespace-normal") or
truncation ("truncate") so long API/validation text won't push the Save/Discard
buttons off-screen; keep dirtyCount rendering unchanged.

Comment on lines +43 to +51
const NAMESPACE_ICONS: Record<string, React.ReactNode> = {
api: <Globe className="size-4" />,
memory: <Brain className="size-4" />,
budget: <Wallet className="size-4" />,
security: <Shield className="size-4" />,
coordination: <Network className="size-4" />,
observability: <Eye className="size-4" />,
backup: <HardDrive className="size-4" />,
}
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

Make the namespace icon map exhaustive.

Record<string, React.ReactNode> hides missing namespaces at compile time, so omissions silently fall back to the generic gear. Type this as Record<SettingNamespace, React.ReactNode> (or an explicit typed partial with fallback) so namespace additions stay aligned with the page contract. Based on learnings, settings definitions span 9 namespaces.

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

In `@web/src/pages/SettingsPage.tsx` around lines 43 - 51, The NAMESPACE_ICONS map
currently uses Record<string, React.ReactNode>, hiding missing namespaces;
change its type to Record<SettingNamespace, React.ReactNode> (or alternatively
use Partial<Record<SettingNamespace, React.ReactNode>> with a runtime fallback)
and ensure all nine SettingNamespace values are present in NAMESPACE_ICONS (or
explicitly handle missing keys when rendering), referencing the NAMESPACE_ICONS
constant and the SettingNamespace type so new namespaces will surface as compile
errors rather than silently falling back to the generic icon.

Comment on lines 53 to 323
export default function SettingsPage() {
return <PlaceholderPage title="Settings" />
const {
entries,
loading,
error,
saving,
saveError,
wsConnected,
wsSetupError,
updateSetting,
} = useSettingsData()

const storeSavingKeys = useSettingsStore((s) => s.savingKeys)

const [searchQuery, setSearchQuery] = useState('')
const [advancedMode, setAdvancedMode] = useState(
() => localStorage.getItem(SETTINGS_ADVANCED_KEY) === 'true',
)
const [viewMode, setViewMode] = useState<ViewMode>('gui')
const [showAdvancedWarning, setShowAdvancedWarning] = useState(false)
const [codeDirty, setCodeDirty] = useState(false)
const [showCodeDiscardWarning, setShowCodeDiscardWarning] = useState(false)

const {
dirtyValues,
setDirtyValues,
handleValueChange,
handleDiscard,
handleSave,
} = useSettingsDirtyState(entries, updateSetting)

// Filter entries: exclude hidden, filter by level, filter by search
const filteredByNamespace = useMemo(() => {
const result = new Map<SettingNamespace, SettingEntry[]>()
for (const ns of NAMESPACE_ORDER) {
const nsEntries = entries.filter((e) => {
if (e.definition.namespace !== ns) return false
const compositeKey = `${e.definition.namespace}/${e.definition.key}`
if (HIDDEN_SETTINGS.has(compositeKey)) return false
if (!advancedMode && e.definition.level === 'advanced') return false
if (searchQuery && !matchesSetting(e, searchQuery)) return false
return true
})
if (nsEntries.length > 0) {
result.set(ns, nsEntries)
}
}
return result
}, [entries, advancedMode, searchQuery])

const controllerDisabledMap = useMemo(
() => buildControllerDisabledMap(entries, dirtyValues),
[entries, dirtyValues],
)

const handleCodeSave = useCallback(
async (changes: Map<string, string>): Promise<Set<string>> => {
try {
const failedKeys = await saveSettingsBatch(changes, updateSetting)

// Clear GUI drafts for keys successfully saved from code mode
setDirtyValues((prev) => {
const next = new Map(prev)
for (const key of changes.keys()) {
if (!failedKeys.has(key)) next.delete(key)
}
return next
})

if (failedKeys.size === 0) {
useToastStore.getState().add({ variant: 'success', title: 'Settings saved' })
} else {
useToastStore.getState().add({
variant: 'error',
title: `${failedKeys.size} setting(s) failed to save`,
})
}
return failedKeys
} catch (err) {
console.error('[settings] Unexpected error in handleCodeSave:', err)
useToastStore.getState().add({ variant: 'error', title: 'Failed to save settings' })
return new Set(changes.keys())
}
},
[updateSetting, setDirtyValues],
)

const pruneAdvancedDrafts = useCallback(() => {
setDirtyValues((prev) => {
const next = new Map(prev)
for (const ck of prev.keys()) {
const entry = entries.find(
(e) => `${e.definition.namespace}/${e.definition.key}` === ck,
)
if (entry?.definition.level === 'advanced') {
next.delete(ck)
}
}
return next
})
}, [entries, setDirtyValues])

const handleAdvancedToggle = useCallback((checked: boolean) => {
if (checked) {
const warned = sessionStorage.getItem(SETTINGS_ADVANCED_WARNED_KEY)
if (warned !== 'true') {
setShowAdvancedWarning(true)
return
}
}
if (!checked) pruneAdvancedDrafts()
setAdvancedMode(checked)
localStorage.setItem(SETTINGS_ADVANCED_KEY, String(checked))
}, [pruneAdvancedDrafts])

const confirmAdvancedMode = useCallback(() => {
sessionStorage.setItem(SETTINGS_ADVANCED_WARNED_KEY, 'true')
setAdvancedMode(true)
localStorage.setItem(SETTINGS_ADVANCED_KEY, 'true')
setShowAdvancedWarning(false)
}, [])

if (loading && entries.length === 0) {
return <SettingsSkeleton />
}

// Visible entries for code editor, overlaid with GUI drafts so Code mode sees unsaved GUI edits
const codeEntries = entries
.map((entry) => {
const ck = `${entry.definition.namespace}/${entry.definition.key}`
const dirtyValue = dirtyValues.get(ck)
return dirtyValue !== undefined ? { ...entry, value: dirtyValue } : entry
})
.filter((e) => {
const ck = `${e.definition.namespace}/${e.definition.key}`
if (HIDDEN_SETTINGS.has(ck)) return false
if (!advancedMode && e.definition.level === 'advanced') return false
return NAMESPACE_ORDER.includes(e.definition.namespace)
})

return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<h1 className="text-lg font-semibold text-foreground">Settings</h1>
<div className="flex items-center gap-4">
{viewMode !== 'code' && (
<SearchInput value={searchQuery} onChange={setSearchQuery} className="w-64" />
)}
<ToggleField
label="Code"
checked={viewMode === 'code'}
onChange={(v) => {
if (!v && codeDirty) {
setShowCodeDiscardWarning(true)
return
}
setViewMode(v ? 'code' : 'gui')
}}
/>
<ToggleField
label="Advanced"
checked={advancedMode}
onChange={handleAdvancedToggle}
/>
</div>
</div>

{error && (
<div className={cn(
'flex items-center gap-2 rounded-lg',
'border border-danger/30 bg-danger/5',
'px-4 py-2 text-sm text-danger',
)}>
<AlertTriangle className="size-4 shrink-0" />
{error}
</div>
)}

{!wsConnected && !loading && (
<div className={cn(
'flex items-center gap-2 rounded-lg',
'border border-warning/30 bg-warning/5',
'px-4 py-2 text-sm text-warning',
)}>
<WifiOff className="size-4 shrink-0" />
{wsSetupError ?? 'Real-time updates disconnected. Data may be stale.'}
</div>
)}

{advancedMode && (
<AdvancedModeBanner
onDisable={() => {
pruneAdvancedDrafts()
setAdvancedMode(false)
localStorage.setItem(SETTINGS_ADVANCED_KEY, 'false')
}}
/>
)}

{viewMode === 'code' ? (
<ErrorBoundary level="section">
<CodeEditorPanel entries={codeEntries} onSave={handleCodeSave} saving={saving} onDirtyChange={setCodeDirty} />
</ErrorBoundary>
) : (
<>
{filteredByNamespace.size === 0 && (
<EmptyState
icon={Settings}
title={searchQuery ? 'No matching settings' : 'No settings available'}
description={
searchQuery
? 'Try a different search term or clear the filter.'
: 'Settings will appear once the backend is configured.'
}
/>
)}

<StaggerGroup className="space-y-4">
{NAMESPACE_ORDER.filter((ns) => filteredByNamespace.has(ns)).map((ns) => (
<StaggerItem key={ns}>
<ErrorBoundary level="section">
<NamespaceSection
displayName={NAMESPACE_DISPLAY_NAMES[ns]}
icon={NAMESPACE_ICONS[ns] ?? <Settings className="size-4" />}
entries={filteredByNamespace.get(ns)!}
dirtyValues={dirtyValues}
onValueChange={handleValueChange}
savingKeys={storeSavingKeys}
controllerDisabledMap={controllerDisabledMap}
forceOpen={searchQuery.length > 0}
/>
</ErrorBoundary>
</StaggerItem>
))}
</StaggerGroup>

<FloatingSaveBar
dirtyCount={dirtyValues.size}
saving={saving}
onSave={handleSave}
onDiscard={handleDiscard}
saveError={saveError}
/>
</>
)}

<ConfirmDialog
open={showAdvancedWarning}
onOpenChange={setShowAdvancedWarning}
title="Enable Advanced Mode?"
description="Advanced settings control low-level system behavior. Misconfiguration may affect stability or security. Only change these if you know what you are doing."
confirmLabel="Enable"
onConfirm={confirmAdvancedMode}
/>

<ConfirmDialog
open={showCodeDiscardWarning}
onOpenChange={setShowCodeDiscardWarning}
title="Discard code editor changes?"
description="You have unsaved changes in the code editor. Switching to GUI mode will discard them."
confirmLabel="Discard"
variant="destructive"
onConfirm={() => {
setCodeDirty(false)
setShowCodeDiscardWarning(false)
setViewMode('gui')
}}
/>
</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

Split SettingsPage before it gets harder to change.

This component now owns page state, filtering, save orchestration, advanced/code-mode UX, banners, and a long JSX tree inside .map(). Extract the namespace list item and the mode-specific sections into smaller components/hooks.

As per coding guidelines, "Keep functions under 50 lines and files under 800 lines" and "Do NOT create complex (>8 line) JSX inside .map() -- 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/SettingsPage.tsx` around lines 53 - 323, SettingsPage is too
large and contains complex JSX inside a .map(); extract the namespace list item
and the mode-specific rendering into smaller components to follow the guideline.
Create a NamespaceListItem component that encapsulates the StaggerItem +
ErrorBoundary + NamespaceSection rendering (accept props: ns, entries from
filteredByNamespace.get(ns), dirtyValues, handleValueChange, storeSavingKeys,
controllerDisabledMap, searchQuery) and replace the inline map body with
<NamespaceListItem key={ns} ... />; also extract the code/gui conditional block
into a ModePanel component that receives codeEntries, handleCodeSave, saving,
setCodeDirty, filteredByNamespace, searchQuery, dirtyValues, handleValueChange,
storeSavingKeys, controllerDisabledMap, handleSave, handleDiscard, saveError and
returns the CodeEditorPanel or the EmptyState+StaggerGroup+FloatingSaveBar. Keep
SettingsPage responsible for state and pass down the necessary handlers (e.g.,
handleCodeSave, handleValueChange, handleSave, handleDiscard) and derived data
(filteredByNamespace, codeEntries, controllerDisabledMap) so each new component
stays under ~50 lines.

Comment on lines +105 to +106
const c = deriveCurrency(entries)
if (c) patch.currency = c
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

Derive currency authoritatively from each fresh entries snapshot.

These branches only write currency when deriveCurrency() returns a value, and the post-reset full refetch only derives it at all when the reset target is budget/currency. If the resolved currency disappears, becomes invalid, or changes during a full refetch, entries and the standalone currency field drift apart and the UI keeps formatting money with stale data. Write an explicit fallback on every fresh snapshot instead of skipping the assignment.

Based on learnings: "Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults)".

Also applies to: 121-122, 145-147, 195-197

- Make DependencyIndicator keyboard-focusable with tabIndex={0},
  role="note", and aria-label (was non-focusable span)
- Constrain FloatingSaveBar error text with max-w-[40ch] and
  break-words to prevent long errors pushing buttons off-screen
- Add aria-label="Back to settings" to icon-only back buttons
  in SettingsNamespacePage
- Narrow NAMESPACE_ICONS type from Record<string> to
  Partial<Record<SettingNamespace>> for compile-time safety
- Clear stale error on successful refreshEntries and post-reset
  refetch (error banner was persisting after data recovered)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 17:56 — 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.

♻️ Duplicate comments (7)
web/src/stores/settings.ts (2)

192-199: ⚠️ Potential issue | 🟠 Major

Resync currency from every successful post-reset refetch.

The GET at Line 181 returns authoritative entries, but Lines 196-199 only update the standalone currency field when the reset target itself was budget/currency. If another client changes currency before this refetch resolves, entries and useSettingsStore((s) => s.currency) drift apart. Derive currency from refreshedEntries on every successful refetch.

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

In `@web/src/stores/settings.ts` around lines 192 - 199, The code only updates
update.currency when the reset target is 'budget/currency'; instead, after any
successful refetch (when refreshedEntries is truthy) derive the standalone
currency from refreshedEntries by calling deriveCurrency(refreshedEntries) and
set update.currency to that value if present. Locate the block that assigns
update.entries = refreshedEntries and move or add a call to
deriveCurrency(refreshedEntries) there (remove the ns === 'budget' && key ===
'currency' guard) so update.currency stays in sync with refreshedEntries on
every successful refetch.

113-123: ⚠️ Potential issue | 🟠 Major

Ignore stale refreshEntries() snapshots.

Line 117 can fetch an older snapshot before a save starts, and Line 120 will still commit it after that save finishes because savingKeys is empty again by then. That lets poll/WebSocket refreshes briefly revert freshly saved values until the next fetch. Track request freshness before set(...).

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

In `@web/src/stores/settings.ts` around lines 113 - 123, refreshEntries can fetch
then overwrite a just-started save because it only checks get().savingKeys
before and after the await; fix by tracking request freshness: capture a
snapshot token before calling settingsApi.getAllSettings (e.g. read const
beforeToken = Array.from(get().savingKeys) or a savingVersion value) and after
the await verify the token is still current (compare against get().savingKeys or
savingVersion) and only call set(patch) if unchanged; update saving start/finish
logic to bump a savingVersion if you choose that approach so refreshEntries can
reliably detect intervening saves.
web/src/pages/SettingsPage.tsx (2)

43-51: ⚠️ Potential issue | 🟡 Minor

Make the namespace icon map exhaustive and shared.

company and providers are still missing here, so those namespaces fall back to the generic gear and the per-namespace icon requirement is only partially met. Type this as Record<SettingNamespace, React.ReactNode> and add the missing entries so omissions fail at compile time; SettingsNamespacePage.tsx can reuse the same map instead of hardcoding <Settings />. Based on learnings: "Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup)".

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

In `@web/src/pages/SettingsPage.tsx` around lines 43 - 51, The NAMESPACE_ICONS map
is incomplete and should be exhaustive and shared: change its type from
Partial<Record<SettingNamespace, React.ReactNode>> to Record<SettingNamespace,
React.ReactNode>, add the missing entries for "company" and "providers" (using
appropriate icons, e.g., Company/Building and Providers/Plug icons), and export
NAMESPACE_ICONS so SettingsNamespacePage.tsx can import and reuse it instead of
hardcoding <Settings />; this ensures omissions fail at compile time and
centralizes per-namespace icons used alongside the SettingNamespace definitions.

53-323: 🛠️ Refactor suggestion | 🟠 Major

Extract the namespace row and mode panels before this gets harder to change.

SettingsPage is still far beyond the 50-line limit and keeps a long JSX tree inside the .map() at Lines 271-286. Pull the namespace row and mode-specific panels into smaller components or hooks. As per coding guidelines: "Keep functions under 50 lines and files under 800 lines" and "Do NOT create complex (>8 line) JSX inside .map() -- 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/SettingsPage.tsx` around lines 53 - 323, SettingsPage is too
large and contains complex JSX inside the .map() and the viewMode conditional;
extract the namespace row and the two mode panels into small components to
comply with the 50-line rule and avoid complex JSX in .map(). Create a
NamespaceRow (or NamespaceItem) component that receives ns, filteredByNamespace,
NAMESPACE_DISPLAY_NAMES, NAMESPACE_ICONS, dirtyValues, handleValueChange,
storeSavingKeys, controllerDisabledMap, searchQuery and returns the
ErrorBoundary -> StaggerItem -> NamespaceSection wrapper currently inside
NAMESPACE_ORDER.filter(...).map(...); then create CodeModePanel and GuiModePanel
components to replace the viewMode === 'code' conditional block
(CodeEditorPanel/ErrorBoundary and the EmptyState + StaggerGroup +
FloatingSaveBar block), passing codeEntries, handleCodeSave, saving,
setCodeDirty, filteredByNamespace, dirtyValues, handleValueChange,
storeSavingKeys, controllerDisabledMap, handleSave, handleDiscard, saveError as
needed; finally, replace the inlined JSX with these components and memoize them
if necessary to keep SettingsPage concise and under 50 lines.
web/src/pages/settings/FloatingSaveBar.tsx (1)

28-36: ⚠️ Potential issue | 🟠 Major

Allow the save bar to wrap on narrow viewports.

Line 28 still forces a w-fit single-row bar, so the capped error text at Line 36 can combine with the dirty-count label and two buttons to overflow small screens and push the actions off-screen. Drop w-fit and let this container wrap.

♻️ Suggested fix
-          className="sticky bottom-4 z-10 mx-auto flex w-fit items-center gap-3 rounded-lg border border-border bg-surface px-4 py-2.5 shadow-lg"
+          className="sticky bottom-4 z-10 mx-auto flex max-w-full flex-wrap items-center gap-3 rounded-lg border border-border bg-surface px-4 py-2.5 shadow-lg"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/FloatingSaveBar.tsx` around lines 28 - 36, The
container in FloatingSaveBar.tsx forces a single-row layout via the "w-fit"
utility which prevents the save bar from wrapping on narrow viewports; remove
"w-fit" from the root className and enable wrapping by adding "flex-wrap" (and
optionally a responsive max-w like "max-w-full") so the dirtyCount label,
saveError text, and action buttons can break to multiple lines instead of
overflowing; update the className string on the root element that currently
contains "w-fit" to remove it and include "flex-wrap" (and "max-w-full" if
desired).
web/src/pages/SettingsNamespacePage.tsx (2)

25-160: 🛠️ Refactor suggestion | 🟠 Major

Split SettingsNamespacePage into smaller render helpers.

This function still owns filtering, header state, banners, empty-state branching, and section rendering in one large component. Extract the header/banner/content blocks so this route stays maintainable. As per coding guidelines: "Keep functions under 50 lines and files under 800 lines".

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

In `@web/src/pages/SettingsNamespacePage.tsx` around lines 25 - 160, The
SettingsNamespacePage component is too large—extract the header, banners/status,
and main content into small render helper functions (e.g., renderHeader,
renderBanners, renderContent) that live in the same file and accept the needed
props/closures (useParams-derived namespace/ns, displayName,
searchQuery/setSearchQuery, filteredEntries, error, wsConnected, wsSetupError,
loading, entries, dirtyValues, handleValueChange, handleDiscard, handleSave,
storeSavingKeys, controllerDisabledMap, saving, saveError); keep all existing
logic and JSX (SearchInput, EmptyState, NamespaceSection, FloatingSaveBar,
ErrorBoundary, buttons) but move them into these helpers so
SettingsNamespacePage becomes a thin wrapper that computes
filteredEntries/controllerDisabledMap and returns the composed helpers, keeping
function length under ~50 lines.

40-41: ⚠️ Potential issue | 🟠 Major

Namespace deep links still can't reveal advanced-only settings.

Line 41 snapshots advancedMode from localStorage, but this route never renders the toggle/banner that can change it. When a namespace only contains advanced entries, Lines 127-136 reduce the page to an unrecoverable empty state with no way to show the actual settings. Reuse the main Settings page's advanced-mode control here or redirect through /settings while preserving the namespace.

Also applies to: 127-136

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

In `@web/src/pages/SettingsNamespacePage.tsx` around lines 40 - 41, The page reads
advancedMode from localStorage but never exposes a way to change it, so
namespace deep links that only contain advanced settings become an unrecoverable
empty state; fix by making advancedMode writable and either (A) reuse the main
Settings page's advanced-mode control: change const [advancedMode] =
useState(...) to const [advancedMode, setAdvancedMode] = useState(...) and
render or import the same AdvancedToggle/Banner component (and synchronize
setAdvancedMode -> localStorage using useEffect) so users can reveal advanced
entries, or (B) if you prefer not to render the control here, detect when the
namespace contains only advanced entries and redirect to /settings while
preserving the namespace query param so the main Settings page (which has the
toggle) can show the entries; reference advancedMode, SETTINGS_ADVANCED_KEY,
SettingsNamespacePage and the AdvancedToggle/Banner control when implementing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@web/src/pages/settings/FloatingSaveBar.tsx`:
- Around line 28-36: The container in FloatingSaveBar.tsx forces a single-row
layout via the "w-fit" utility which prevents the save bar from wrapping on
narrow viewports; remove "w-fit" from the root className and enable wrapping by
adding "flex-wrap" (and optionally a responsive max-w like "max-w-full") so the
dirtyCount label, saveError text, and action buttons can break to multiple lines
instead of overflowing; update the className string on the root element that
currently contains "w-fit" to remove it and include "flex-wrap" (and
"max-w-full" if desired).

In `@web/src/pages/SettingsNamespacePage.tsx`:
- Around line 25-160: The SettingsNamespacePage component is too large—extract
the header, banners/status, and main content into small render helper functions
(e.g., renderHeader, renderBanners, renderContent) that live in the same file
and accept the needed props/closures (useParams-derived namespace/ns,
displayName, searchQuery/setSearchQuery, filteredEntries, error, wsConnected,
wsSetupError, loading, entries, dirtyValues, handleValueChange, handleDiscard,
handleSave, storeSavingKeys, controllerDisabledMap, saving, saveError); keep all
existing logic and JSX (SearchInput, EmptyState, NamespaceSection,
FloatingSaveBar, ErrorBoundary, buttons) but move them into these helpers so
SettingsNamespacePage becomes a thin wrapper that computes
filteredEntries/controllerDisabledMap and returns the composed helpers, keeping
function length under ~50 lines.
- Around line 40-41: The page reads advancedMode from localStorage but never
exposes a way to change it, so namespace deep links that only contain advanced
settings become an unrecoverable empty state; fix by making advancedMode
writable and either (A) reuse the main Settings page's advanced-mode control:
change const [advancedMode] = useState(...) to const [advancedMode,
setAdvancedMode] = useState(...) and render or import the same
AdvancedToggle/Banner component (and synchronize setAdvancedMode -> localStorage
using useEffect) so users can reveal advanced entries, or (B) if you prefer not
to render the control here, detect when the namespace contains only advanced
entries and redirect to /settings while preserving the namespace query param so
the main Settings page (which has the toggle) can show the entries; reference
advancedMode, SETTINGS_ADVANCED_KEY, SettingsNamespacePage and the
AdvancedToggle/Banner control when implementing.

In `@web/src/pages/SettingsPage.tsx`:
- Around line 43-51: The NAMESPACE_ICONS map is incomplete and should be
exhaustive and shared: change its type from Partial<Record<SettingNamespace,
React.ReactNode>> to Record<SettingNamespace, React.ReactNode>, add the missing
entries for "company" and "providers" (using appropriate icons, e.g.,
Company/Building and Providers/Plug icons), and export NAMESPACE_ICONS so
SettingsNamespacePage.tsx can import and reuse it instead of hardcoding
<Settings />; this ensures omissions fail at compile time and centralizes
per-namespace icons used alongside the SettingNamespace definitions.
- Around line 53-323: SettingsPage is too large and contains complex JSX inside
the .map() and the viewMode conditional; extract the namespace row and the two
mode panels into small components to comply with the 50-line rule and avoid
complex JSX in .map(). Create a NamespaceRow (or NamespaceItem) component that
receives ns, filteredByNamespace, NAMESPACE_DISPLAY_NAMES, NAMESPACE_ICONS,
dirtyValues, handleValueChange, storeSavingKeys, controllerDisabledMap,
searchQuery and returns the ErrorBoundary -> StaggerItem -> NamespaceSection
wrapper currently inside NAMESPACE_ORDER.filter(...).map(...); then create
CodeModePanel and GuiModePanel components to replace the viewMode === 'code'
conditional block (CodeEditorPanel/ErrorBoundary and the EmptyState +
StaggerGroup + FloatingSaveBar block), passing codeEntries, handleCodeSave,
saving, setCodeDirty, filteredByNamespace, dirtyValues, handleValueChange,
storeSavingKeys, controllerDisabledMap, handleSave, handleDiscard, saveError as
needed; finally, replace the inlined JSX with these components and memoize them
if necessary to keep SettingsPage concise and under 50 lines.

In `@web/src/stores/settings.ts`:
- Around line 192-199: The code only updates update.currency when the reset
target is 'budget/currency'; instead, after any successful refetch (when
refreshedEntries is truthy) derive the standalone currency from refreshedEntries
by calling deriveCurrency(refreshedEntries) and set update.currency to that
value if present. Locate the block that assigns update.entries =
refreshedEntries and move or add a call to deriveCurrency(refreshedEntries)
there (remove the ns === 'budget' && key === 'currency' guard) so
update.currency stays in sync with refreshedEntries on every successful refetch.
- Around line 113-123: refreshEntries can fetch then overwrite a just-started
save because it only checks get().savingKeys before and after the await; fix by
tracking request freshness: capture a snapshot token before calling
settingsApi.getAllSettings (e.g. read const beforeToken =
Array.from(get().savingKeys) or a savingVersion value) and after the await
verify the token is still current (compare against get().savingKeys or
savingVersion) and only call set(patch) if unchanged; update saving start/finish
logic to bump a savingVersion if you choose that approach so refreshEntries can
reliably detect intervening saves.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7ab557b2-a3a9-4645-bea6-0b5300aa9d75

📥 Commits

Reviewing files that changed from the base of the PR and between c26c912 and 1488571.

📒 Files selected for processing (5)
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/stores/settings.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 Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Build Backend
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (1)
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/src/**/*.{ts,tsx}: Use design tokens and semantic classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) -- NEVER hardcode hex values or rgba in .tsx/.ts files
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/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 text-metric classes -- use <MetricCard>
Do NOT render initials circles manually -- use <Avatar>; do NOT create complex (>8 line) JSX inside .map() -- extract to a shared component
Use TypeScript 6.0 features: remove deprecated baseUrl, accept that esModuleInterop is always true, explicitly list types if needed, use "bundler" or "nodenext" for moduleResolution
Never use real vendor names (OpenAI, Claude, GPT, Anthropic) in web dashboard code -- use generic names: example-provider, example-large-001, etc.
Keep functions under 50 lines and files under 800 lines
Handle errors explicitly, never silently swallow exceptions

Files:

  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/stores/settings.ts
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/settings/**/*.py : Settings use runtime-editable persistence with precedence: DB > env > YAML > code defaults. 8 namespaces with Fernet encryption for sensitive values.
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Always reuse existing shared components from `web/src/components/ui/` before creating new ones; export props as TypeScript interfaces; use design tokens exclusively; import `cn` from `@/lib/utils` for conditional class merging

Applied to files:

  • web/src/pages/settings/DependencyIndicator.tsx
  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Keep functions under 50 lines and files under 800 lines

Applied to files:

  • web/src/pages/settings/FloatingSaveBar.tsx
  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
  • web/src/stores/settings.ts
📚 Learning: 2026-03-21T11:08:01.542Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-21T11:08:01.542Z
Learning: Applies to **/*.{py,md} : Line length: 88 characters (ruff).

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
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 text-metric classes -- use `<MetricCard>`

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT render initials circles manually -- use `<Avatar>`; do NOT create complex (>8 line) JSX inside `.map()` -- extract to a shared component

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use design tokens and semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) -- NEVER hardcode hex values or rgba in `.tsx`/`.ts` files

Applied to files:

  • web/src/pages/SettingsNamespacePage.tsx
📚 Learning: 2026-03-28T16:54:20.164Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T16:54:20.164Z
Learning: Applies to web/src/**/*.{ts,tsx} : Never use real vendor names (OpenAI, Claude, GPT, Anthropic) in web dashboard code -- use generic names: `example-provider`, `example-large-001`, etc.

Applied to files:

  • web/src/pages/SettingsPage.tsx
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).

Applied to files:

  • web/src/stores/settings.ts
📚 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/settings/**/*.py : Settings package (settings/): runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge (JSON serialization for Pydantic/collections), ConfigResolver (typed accessors), validation, registry, change notifications via message bus, SettingsSubscriber protocol, SettingsChangeDispatcher (polls `#settings` channel, routes to subscribers, restart_required filtering)

Applied to files:

  • web/src/stores/settings.ts
🔇 Additional comments (2)
web/src/pages/settings/DependencyIndicator.tsx (2)

15-25: Accessibility concern from previous review has been addressed.

The component is now keyboard-focusable (tabIndex={0}), has an appropriate role="note" for supplementary content, and exposes the tooltip text via aria-label for screen readers. The aria-hidden on the icon prevents double-announcement. Good fix.


17-17: Confirm whether text-text-muted is intentionally defined or should be replaced.

The class text-text-muted is not defined in the Tailwind @theme inline configuration in web/src/styles/global.css. While --color-text-secondary is explicitly defined (generating text-text-secondary), there is no corresponding --color-text-muted. The codebase uses text-text-muted extensively (40+ occurrences), but the proper semantic class based on the theme would be text-muted-foreground (from --color-muted-foreground). The double "text" prefix contradicts the documented coding guidelines pattern (text-foreground, text-accent, etc.). Clarify whether this is an inherited class from shadcn/ui, a configuration error, or intentional — and update accordingly.

@Aureliolo Aureliolo merged commit a7b9870 into main Mar 28, 2026
32 checks passed
@Aureliolo Aureliolo deleted the feat/settings-page branch March 28, 2026 18:09
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview March 28, 2026 18:09 — 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

Development

Successfully merging this pull request may close these issues.

feat: Settings page (search, dependency indicators, grouped rendering)

1 participant