Skip to content

feat: migrate web dashboard from Radix UI to Base UI, activate CSP nonce, rebuild org chart page, and fix agent routing#1083

Merged
Aureliolo merged 20 commits intomainfrom
feat/radix-to-base-ui-migration
Apr 6, 2026
Merged

feat: migrate web dashboard from Radix UI to Base UI, activate CSP nonce, rebuild org chart page, and fix agent routing#1083
Aureliolo merged 20 commits intomainfrom
feat/radix-to-base-ui-migration

Conversation

@Aureliolo
Copy link
Copy Markdown
Owner

Closes #1070. Subsumes #1064.

Replaces the previously closed PR #1074. Same branch, clean single-narrative summary of what actually landed.

Summary

Eight bundled changes that all touch the same frontend files (plus one backend semantics fix for the analytics overview):

  1. Radix UI to Base UI migration. radix-ui + cmdk fully removed (115 transitive packages); @base-ui/react@1.3.0 + cmdk-base@1.0.0 installed (8 packages). components.json switches from radix-nova to base-vega. 14 component files migrated: button.tsx (via a local <Slot> helper over @base-ui/react/merge-props), dialog.tsx, confirm-dialog.tsx, theme-toggle.tsx, command-palette.tsx (now cmdk-base hosted inside Base UI Dialog for focus trap + escape + portal), six page-level Dialog / AlertDialog files, OrgEditPage.tsx Tabs, and both Workflow Menu files. Data attributes rewritten (data-[state=open] to data-[open], data-[state=active] to data-[active], plus data-[starting-style] / data-[ending-style] for transition lifecycle).

  2. CSP nonce activation, end-to-end. nginx.conf generates a per-request nonce from $request_id and substitutes it into __CSP_NONCE__ via sub_filter. index.html exposes the nonce on <meta name="csp-nonce">. web/src/lib/csp.ts reads it (rejecting the un-substituted placeholder and logging warnings when missing/placeholder so production misconfig is visible). App.tsx wraps the tree in <CSPProvider nonce> (Base UI) + <MotionConfig nonce> (Framer Motion). security-headers.conf replaces style-src 'self' 'unsafe-inline' with CSP Level 3 split directives: style-src-elem 'self' 'nonce-$csp_nonce' (locks down <style> elements) + style-src-attr 'unsafe-inline' (required by Floating UI positioning, not a practical XSS vector: style attributes cannot execute JavaScript).

  3. Personality trimming WebSocket notification (folded in from feat: WebSocket notification when personality trimming activates #1064):

    • New engine.personality_trimming_notify setting (BOOLEAN, default true).
    • New WsEventType.PERSONALITY_TRIMMED enum member.
    • New personality_trim_notifier callback parameter on AgentEngine, invoked best-effort from _prepare_context (re-raises MemoryError / RecursionError / asyncio.CancelledError; logs prompt.personality.notify_failed and swallows everything else).
    • New synthorg.api.app.make_personality_trim_notifier factory that returns a ready-to-wire async callback bound to the live ChannelsPlugin. External engine runners (CLI workers, K8s jobs) call this factory and pass the result into AgentEngine.
    • Frontend: personality.trimmed added to WsEventType + WS_EVENT_TYPE_VALUES. New useGlobalNotifications hook mounted in AppLayout subscribes to the agents channel globally; agents store dispatches an info toast with agent_name (length-bounded) + before to after tokens when the event arrives, and logs a warning on malformed payloads.
  4. Org chart page full rebuild.

    • New synthetic Owner node above the root department, accepting an array-shaped payload for future multi-user ownership (feat: multi-user ownership + department-admin permissions, surfaced in the org chart #1082). Fixed w-[240px] / h-[90px] so layout centering matches rendered size.
    • Restructured hierarchy: Executive is the root department box with the CEO / CTO living inside it as regular agents (not standalone top-level nodes); all other departments hang below via a single cross-dept edge row. Matches real-world org charts and avoids the "edge cuts through the executive box" problem because inter-dept lines now originate from the root dept box's bottom edge.
    • Dagre is used for ordering only; a post-layout shift pass enforces an exact 48px gap between the owner row, the root department, and the non-root department row (the prior minlen approach was quantized into 50px rank jumps).
    • Custom <HierarchyEdge> SVG L-path replaces React Flow's getSmoothStepPath, which had a Z-shape fanout limitation that silently ignored centerY on cross-x edges. L-path routes cleanly with a single vertical bend.
    • Live particle flow: new useLiveEdgeActivity hook subscribes to the messages store and pulses particles along edges that carry traffic in the last 3s. Three modes via segmented control: Flow (always on), Live (traffic-driven, default), Off. SVG animateMotion with uniform Manhattan-distance speed so particles don't visually "slow down" on longer edges.
    • Department group node: dashed empty-state border with inline "+ Add agent" CTA, status dots (active / idle / error / offline) with tooltip legend, budget utilization bar, LEAD badge on department leads. Header height is computed from live toggle state so agent cards never overlap chrome.
    • Inline toolbar toggles replace the previous popover view menu: particle-flow segmented control + per-feature icon toggles for add-agent CTA, lead badges, budget bars, status dots, minimap. Status-dots toggle has a multi-line tooltip explaining the color legend.
    • Search overlay (Ctrl+F) with match dimming and keyboard nav across matches.
    • Collapse / expand per department with chevron.
    • Minimap togglable, default off, custom node colors; no labels (unreadable at minimap scale).
    • New persistence store useOrgChartPrefs (web/src/stores/org-chart-prefs.ts): particleFlowMode: 'live', add-agent + lead badge + budget bar visible, status dots + minimap hidden.
  5. Agent routing by id, not name. /agents/:agentName switched to /agents/:agentId. Names can contain arbitrary characters that broke backend lookup after URL encoding. AgentDetailPage resolves the agent from useCompanyStore by id (falling back to name for legacy config) and passes the resolved name to the data hook. Navigation sites updated: AgentGridView, OrgChartPage, ProjectTeamSection.

  6. Dynamic department filter. Department dropdown on the agents page now reads from useCompanyStore().config.departments instead of the hardcoded DEPARTMENT_NAME_VALUES enum, so it matches the user-configured department list (wizard, packs, manual edits). Uses display_name when provided, falls back to formatLabel(name).

  7. Coming-soon gates on missing backend CRUD. New shared constant in web/src/pages/org-edit/coming-soon.ts points at issue feat: implement backend CRUD endpoints for company, departments, and agents (all 9 write paths are missing) #1081. Nine mutation paths in the org-edit dialogs / drawers surface a banner explaining the limitation instead of hitting 405 responses.

  8. Analytics "active agents" runtime semantics. _resolve_agent_counts in analytics.py now computes "active" as agents currently executing a task (task status IN_PROGRESS, assigned_to in the employed set), not "agents with employment status ACTIVE". The previous semantics conflated HR lifecycle with runtime state and produced the surprising "4 active / 0 idle / 0 tasks" display on the overview dashboard. _assemble_overview now passes all_tasks into the helper so the computation reuses the already-fetched task list.

Reviewer attention items

  1. Combobox criterion (issue §4a): the issue text says "provider model search (currently a filtered list in ProviderFormModal)" should use Combobox. That filtered list does not exist in the current repo: the only filter in ProviderFormModal.tsx is a static 3-item auth-type filter, not a typeahead. The PR documents the keep-decision in web/CLAUDE.md -> Base UI Adoption Decisions ("Combobox, Autocomplete: Not adopted: no current typeahead call sites in the dashboard that would benefit. Re-evaluate when filterable selects become a feature requirement.").

  2. Bundle size direction: the issue anticipated a net reduction; the actual delta is a 3% increase (vendor-ui 150 kB to 230 kB, total dist 3.4M to 3.5M). The tradeoff is Base UI's more comprehensive primitive surface (Floating UI positioning, focus management, transition tracking) versus the narrow subset shadcn previously pulled from Radix. First-class CSP nonce support, broader component coverage, and active upstream maintenance justify the delta.

  3. Personality trimming end-to-end wiring: AgentEngine is not constructed anywhere under src/ today (only in tests): it is library code that external runners construct. This PR provides the make_personality_trim_notifier factory so external runners can wire the callback in one line, but the in-tree API layer does not itself construct engines. The frontend toast path is fully wired and will render when any engine host emits the event. Closing the in-tree wiring gap is a separate architectural decision, out of scope.

  4. style-src-attr 'unsafe-inline': retained for Floating UI's transient inline positioning styles (style="position: fixed; top: ...; left: ..."). style attributes cannot execute JavaScript and cannot carry CSP nonces per the CSS spec. This is the narrowest permissive the CSP spec allows. Documented in docs/security.md -> CSP Nonce Infrastructure.

Verification

All gates green:

  • uv run ruff check src/ tests/ -- clean
  • uv run ruff format --check src/ tests/ -- 1541 files already formatted
  • uv run mypy src/ tests/ -- clean (1541 source files)
  • uv run python -m pytest tests/ -n 8 -k analytics -- 59 passed
  • npm --prefix web run lint -- zero warnings
  • npm --prefix web run type-check -- clean
  • npm --prefix web run test -- 2406 passed (203 files)

Zero Radix references remain in tracked files.

Non-goals

Copilot AI review requested due to automatic review settings April 5, 2026 22:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: eb51466b-9a71-4924-bcb5-1f49c9fd32ff

📥 Commits

Reviewing files that changed from the base of the PR and between 2f4d019 and e87c502.

📒 Files selected for processing (1)
  • web/src/styles/design-tokens.css
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build 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,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,css}: Use Tailwind semantic color classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) instead of hardcoded hex values
Use font-sans or font-mono for typography (maps to Geist tokens), never set fontFamily directly
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing, never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders instead of hardcoded values
Do NOT use rgba() with hardcoded values -- use design token variables
A PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files and catches design system violations; fix all violations before proceeding

Files:

  • web/src/styles/design-tokens.css
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
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/).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to docker/{Dockerfile*,compose.yml} : Docker: Backend uses 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web uses nginxinc/nginx-unprivileged, Vue 3 SPA with PrimeVue + Tailwind CSS, SPA routing, API/WebSocket proxy to backend.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:28:28.895Z
Learning: Web dashboard: see `web/CLAUDE.md` for commands, design system, and component inventory
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `AgentCard` component for agent display (avatar, name, role, status, current task)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T18:57:46.655Z
Learning: Applies to web/package.json : Web dashboard Node.js 22+, TypeScript 6.0+, dependencies in `web/package.json`
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders instead of hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Do NOT use `rgba()` with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web code — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in component code -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use Tailwind semantic color classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) instead of hardcoded hex values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

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

Applied to files:

  • web/src/styles/design-tokens.css
🔇 Additional comments (5)
web/src/styles/design-tokens.css (5)

77-79: Shadow tokens are properly theme-derived.

Good shift to color-mix(...) over existing --so-* colors here. It keeps shadow styling theme-aware without reintroducing hardcoded channel literals.

As per coding guidelines: web/src/**/*.{ts,tsx,css} — “Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders instead of hardcoded values” and “Do NOT use rgba() with hardcoded values -- use design token variables”.


81-85: Nice extraction of motion and org-chart constants into tokens.

Pulling the dim timing and drag opacity into named tokens keeps later component styling from drifting back to ad hoc literals.


88-95: The minimap token surface matches its consumer cleanly.

These names line up with the exact --so-minimap-* variables used by web/src/pages/OrgChartPage.tsx, so the org-chart minimap stays fully theme-driven without inline fallbacks.


98-102: Overlay states are consistently tokenized.

This keeps hover, active, and selection layers aligned with the active theme and avoids the hardcoded RGBA maintenance issue that existed earlier in this file.

As per coding guidelines: web/src/**/*.{ts,tsx,css} — “Do NOT use rgba() with hardcoded values -- use design token variables”.


190-192: The neon-only glow override is scoped well.

Keeping the stronger neon treatment while still deriving it from --so-accent preserves the special-case look without breaking token consistency.

As per coding guidelines: web/src/**/*.{ts,tsx,css} — “Do NOT use rgba() with hardcoded values -- use design token variables”.


Walkthrough

Replaces Radix UI and cmdk with Base UI and cmdk-base across the web dashboard, docs, Storybook, and build config; updates shadcn style preset and web/package.json. Activates per-request CSP nonces via nginx sub_filter, an active <meta name="csp-nonce">, runtime nonce extraction in web/src/lib/csp.ts, and propagation into Base UI’s CSPProvider and Framer Motion MotionConfig. Adds end-to-end personality-trimming notifications: new ENGINE setting personality_trimming_notify, notifier types and factory, WsEventType.PERSONALITY_TRIMMED, engine wiring to publish events, frontend handling and toasts, accompanying unit tests, and related documentation updates.

Suggested labels

autorelease: tagged

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 5, 2026

@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 5, 2026 22:33 — with GitHub Actions Inactive
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 5, 2026

Dependency Review

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

Snapshot Warnings

⚠️: No snapshots were found for the head SHA e87c502.
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.

OpenSSF Scorecard

PackageVersionScoreDetails
npm/@babel/runtime 7.29.2 🟢 7.2
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 18 issue activity found in the last 90 days -- score normalized to 10
Code-Review🟢 9Found 27/30 approved changesets -- score normalized to 9
Security-Policy🟢 10security policy file detected
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions🟢 9detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 2badge detected: InProgress
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
Fuzzing⚠️ 0project is not fuzzed
npm/@base-ui/react 1.3.0 🟢 7.6
Details
CheckScoreReason
Dependency-Update-Tool🟢 10update tool detected
Maintained🟢 1030 commit(s) and 8 issue activity found in the last 90 days -- score normalized to 10
Security-Policy🟢 10security policy file detected
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 7Found 12/17 approved changesets -- score normalized to 7
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Pinned-Dependencies🟢 10all dependencies are pinned
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices🟢 5badge detected: Passing
SAST🟢 7SAST tool detected but not run on all commits
License🟢 10license file detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Branch-Protection🟢 4branch protection is not maximal on development and all release branches
Vulnerabilities⚠️ 034 existing vulnerabilities detected
CI-Tests🟢 1029 out of 29 merged PRs checked by a CI test -- score normalized to 10
Contributors🟢 10project has 19 contributing companies or organizations
npm/@base-ui/utils 0.2.6 🟢 7.6
Details
CheckScoreReason
Dependency-Update-Tool🟢 10update tool detected
Maintained🟢 1030 commit(s) and 8 issue activity found in the last 90 days -- score normalized to 10
Security-Policy🟢 10security policy file detected
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 7Found 12/17 approved changesets -- score normalized to 7
Token-Permissions🟢 10GitHub workflow tokens follow principle of least privilege
Pinned-Dependencies🟢 10all dependencies are pinned
Binary-Artifacts🟢 10no binaries found in the repo
CII-Best-Practices🟢 5badge detected: Passing
SAST🟢 7SAST tool detected but not run on all commits
License🟢 10license file detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Branch-Protection🟢 4branch protection is not maximal on development and all release branches
Vulnerabilities⚠️ 034 existing vulnerabilities detected
CI-Tests🟢 1029 out of 29 merged PRs checked by a CI test -- score normalized to 10
Contributors🟢 10project has 19 contributing companies or organizations
npm/cmdk-base 1.0.0 UnknownUnknown
npm/tabbable 6.4.0 🟢 5.9
Details
CheckScoreReason
Packaging⚠️ -1packaging workflow not detected
Code-Review⚠️ -1Found no human activity in the last 30 changesets
Maintained🟢 1030 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10
Security-Policy🟢 10security policy file detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies🟢 3dependency not pinned by hash detected -- score normalized to 3
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • web/package-lock.json

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 migrates the web dashboard's UI primitive layer from Radix UI to Base UI, updates the Content Security Policy (CSP) to use nonces for dynamic style injection, and introduces a personality trimming notification system for agents. Several Python files contain syntax errors where multiple exceptions are caught using the deprecated Python 2 comma-separated syntax instead of a tuple, which must be corrected for Python 3 compatibility.

channels=[CHANNEL_AGENTS],
),
)
except MemoryError, RecursionError:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This except syntax is for Python 2. For compatibility with Python 3, multiple exceptions should be specified as a tuple.

        except (MemoryError, RecursionError):

return 0, config_agent_count
try:
employed = await app_state.agent_registry.list_active()
except MemoryError, RecursionError:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This except syntax is for Python 2. For compatibility with Python 3, multiple exceptions should be specified as a tuple.

    except (MemoryError, RecursionError):

"engine",
"personality_trimming_notify",
)
except MemoryError, RecursionError:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This except syntax is for Python 2. For compatibility with Python 3, multiple exceptions should be specified as a tuple. This also occurs on line 1162.

            except (MemoryError, RecursionError):

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR modernizes the web dashboard’s UI primitive layer and security posture (Radix → Base UI + CSP nonce), while also delivering a substantial Org Chart UX rebuild, agent-detail routing hardening (id-based routes), and an engine→dashboard WebSocket notification for personality trimming (plus an analytics semantics fix).

Changes:

  • Migrate UI primitives from Radix UI/cmdk to Base UI/cmdk-base and update affected components/pages accordingly.
  • Activate end-to-end CSP nonce propagation (nginx sub_filter → meta tag → getCspNonce() → Base UI CSPProvider + Framer Motion MotionConfig) and update CSP directives.
  • Rebuild Org Chart rendering/UX (owner node, custom hierarchy edges + live particles, toolbar prefs persistence, search overlay, collapsible depts), add personality-trim WS toast path, switch agent routes to :agentId, and fix analytics “active agents” semantics.

Reviewed changes

Copilot reviewed 102 out of 103 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
web/vite.config.ts Update vendor chunk split list for Base UI/cmdk-base.
web/src/styles/global.css Update shadcn bridge comment to reflect Base UI.
web/src/stores/org-chart-prefs.ts Add persisted org chart UI prefs (particle mode, toggles).
web/src/stores/agents.ts Harden WS payload handling; add personality.trimmed toast dispatch + logging.
web/src/router/routes.ts Change agent detail route param from :agentName:agentId.
web/src/router/index.tsx Update router path to agents/:agentId.
web/src/pages/workflows/WorkflowTableView.tsx Migrate dropdown actions to Base UI Menu; improve delete confirm promise handling.
web/src/pages/workflows/WorkflowCard.tsx Migrate dropdown actions to Base UI Menu; allow async delete.
web/src/pages/workflow-editor/VersionDiffViewer.tsx Update dialog padding tokens and comment for Base UI.
web/src/pages/settings/SettingField.tsx Improve float display formatting (strip trailing .0+).
web/src/pages/providers/ProviderFormModal.tsx Migrate Dialog usage to Base UI patterns.
web/src/pages/providers/ModelPullDialog.tsx Migrate AlertDialog to Base UI; add two-phase cancel/close behavior.
web/src/pages/projects/ProjectTeamSection.tsx Update agent detail links to id-based routes.
web/src/pages/OrgEditPage.tsx Migrate Tabs to Base UI; add read-only “coming soon” gate banner for missing CRUD.
web/src/pages/org/OwnerNode.tsx Add synthetic “Owner” node type for org chart.
web/src/pages/org/OrgChartToolbar.tsx Add inline segmented controls/toggles wired to persisted org chart prefs.
web/src/pages/org/OrgChartSearchOverlay.tsx Add Ctrl/Cmd+F org chart search overlay UI.
web/src/pages/org/HierarchyEdge.tsx Replace smoothstep with custom orthogonal hierarchy edge + optional particles.
web/src/pages/org/DepartmentGroupNode.stories.tsx Update stories for new dept node data (budget/utilization/status dots).
web/src/pages/org/CeoNode.tsx Visual/interaction updates (hover-revealed handles, styling).
web/src/pages/org/AgentNode.tsx Add optional LEAD badge + hover-only handles; wire to org chart prefs.
web/src/pages/org-edit/YamlEditorPanel.tsx Disable Save while backend CRUD is missing; add tooltip pointer to #1081.
web/src/pages/org-edit/PackSelectionDialog.tsx Migrate Dialog to Base UI; align styling tokens.
web/src/pages/org-edit/GeneralTab.tsx Replace slider w/ numeric input; add comm-pattern enum select; disable save gate.
web/src/pages/org-edit/DepartmentEditDrawer.tsx Remove runtime meter from editor; disable Save/Delete with #1081 tooltip.
web/src/pages/org-edit/DepartmentCreateDialog.tsx Migrate Dialog to Base UI; disable Create with #1081 tooltip.
web/src/pages/org-edit/coming-soon.ts Centralize read-only gate copy/URLs for org-edit surfaces.
web/src/pages/org-edit/AgentsTab.tsx Disable add/drag in read-only mode; add #1081 tooltips.
web/src/pages/org-edit/AgentEditDrawer.tsx Disable Save/Delete with #1081 tooltip.
web/src/pages/org-edit/AgentCreateDialog.tsx Migrate Dialog to Base UI; disable Create with #1081 tooltip.
web/src/pages/dashboard/BudgetBurnChart.tsx Silence first-paint Recharts warning via initialDimension.
web/src/pages/budget/SpendBurnChart.tsx Silence first-paint Recharts warning via initialDimension.
web/src/pages/budget/CostBreakdownChart.tsx Silence first-paint Recharts warning via initialDimension.
web/src/pages/agents/AgentGridView.tsx Update agent detail links to use agent.id ?? agent.name.
web/src/pages/agents/AgentFilters.tsx Populate department filter from live company config instead of static enum.
web/src/pages/AgentDetailPage.tsx Resolve agent name from id-based route param via company config.
web/src/lib/csp.ts Add nonce reader logging + placeholder detection; cache semantics clarified.
web/src/hooks/useOrgChartData.ts Add owners + collapsed dept filtering + layout prefs + updated layout call.
web/src/hooks/useLiveEdgeActivity.ts New hook to track edge activity from messages store for “live” particles.
web/src/hooks/useGlobalNotifications.ts New global WS subscription hook for app-wide toast/event dispatch.
web/src/hooks/useCommandPalette.ts Loosen command action return typing; improve registration typing/docs; expose setOpen.
web/src/components/ui/theme-toggle.tsx Migrate Popover to Base UI.
web/src/components/ui/slot.tsx Add local Slot implementation using Base UI merge-props.
web/src/components/ui/dialog.tsx Migrate shared Dialog wrapper components to Base UI.
web/src/components/ui/confirm-dialog.tsx Migrate ConfirmDialog to Base UI AlertDialog; improve async confirm behavior.
web/src/components/ui/button.tsx Replace Radix Slot usage with local Slot.
web/src/components/ui/animated-presence.tsx Adjust route transition variants/mode to reduce flashing; ensure full-height wrapper.
web/src/components/layout/StatusBar.tsx Combine health+WS into single status surface; add idle agents metric; use HealthPopover.
web/src/components/layout/Sidebar.tsx Add disabled “Notifications” placeholder; route health status click into HealthPopover.
web/src/components/layout/AppLayout.tsx Mount global notifications hook; update spacing token usage.
web/src/App.tsx Wrap app with Base UI CSPProvider + Framer Motion MotionConfig nonce.
web/src/api/types.ts Add personality.trimmed WS event type constant/value.
web/src/tests/stores/agents.test.ts Add tests for personality.trimmed toast dispatch behavior.
web/src/tests/pages/org/OrgChartPage.test.tsx Extend ReactFlow mocks (MiniMap) for org chart tests.
web/src/tests/pages/org-edit/YamlEditorPanel.test.tsx Update tests for read-only gating of Save YAML.
web/src/tests/pages/org-edit/GeneralTab.test.tsx Update tests for read-only gating of Save Settings.
web/src/tests/pages/org-edit/DepartmentsTab.test.tsx Update tests to reflect removal of runtime meters from editor view.
web/src/tests/pages/org-edit/DepartmentEditDrawer.test.tsx Update tests for read-only Save/Delete gating and meter removal.
web/src/tests/pages/org-edit/DepartmentCreateDialog.test.tsx Update tests for disabled Create gating.
web/src/tests/pages/org-edit/AgentEditDrawer.test.tsx Update tests for disabled Save/Delete gating.
web/src/tests/pages/org-edit/AgentCreateDialog.test.tsx Update tests for disabled Create gating.
web/src/tests/pages/approvals/ApprovalDetailDrawer.test.tsx Reduce flakiness (fresh handler factory, scoped queries); update Base UI wording.
web/src/tests/pages/agents/AgentGridView.test.tsx Update test to assert id-based agent links.
web/src/tests/pages/agents/AgentFilters.test.tsx Seed company store config for dynamic department dropdown.
web/src/tests/pages/AgentDetailPage.test.tsx Update route param name to agentId.
web/src/tests/components/ui/confirm-dialog.test.tsx Adjust assertions for Base UI onOpenChange signature + focus guards.
web/src/tests/components/layout/StatusBar.test.tsx Update expectations for idle metric and removed brand duplication.
web/src/tests/App.test.tsx Stub global notifications hook; adjust timeouts for heavier initial imports.
web/security-headers.conf Update CSP to nonce-based style-src split directives (elem/attr).
web/package.json Add @base-ui/react; replace cmdk with cmdk-base; remove radix-ui dependency.
web/nginx.conf Enable per-request nonce generation + sub_filter injection; adjust gzip_static + headers logic.
web/index.html Add <meta name="csp-nonce" ...> placeholder for nginx substitution.
web/components.json Switch shadcn style from radix-nova → base-vega.
web/CLAUDE.md Update dashboard docs for Base UI adoption + CSP nonce flow + primitive decisions.
tests/unit/settings/test_engine_settings.py Add/adjust setting registry tests for personality_trimming_notify.
tests/unit/api/test_personality_trim_notifier_factory.py Add tests for WS notifier factory publishing behavior and error handling.
src/synthorg/settings/definitions/engine.py Register engine.personality_trimming_notify setting.
src/synthorg/observability/events/prompt.py Add prompt.personality.notify_failed event constant.
src/synthorg/api/ws_models.py Add WsEventType.PERSONALITY_TRIMMED.
src/synthorg/api/controllers/analytics.py Redefine “active agents” as agents executing IN_PROGRESS tasks; reuse fetched task list.
src/synthorg/api/app.py Add make_personality_trim_notifier factory bound to ChannelsPlugin.
docs/guides/deployment.md Update CSP documentation to nonce-based directives and deployment notes.
docs/design/ux-research.md Update references to cmdk-base + Base UI.
docs/design/ux-guidelines.md Update wording + command palette library reference.
docs/design/page-structure.md Update command palette library reference to cmdk-base.
docs/design/engine.md Document personality_trimming_notify and WS toast pathway.
docs/design/brand-and-ux.md Update ConfirmDialog/CommandPalette/ThemeToggle primitive references to Base UI/cmdk-base.
docs/architecture/tech-stack.md Update tech-stack references (Base UI, cmdk-base, CSP nonce).
CLAUDE.md Update top-level stack note (Base UI + cmdk-base).
Comments suppressed due to low confidence (1)

web/src/pages/AgentDetailPage.tsx:61

  • AgentDetailPage resolves the backend lookup name from company config, but falls back to an empty string when config isn’t loaded yet. That makes useAgentDetailData('') return no agent and the page immediately renders the “Agent not found.” error state, causing a misleading flash (and potentially permanent error if config fetch fails). Consider gating on company-store loading/config and showing the skeleton until resolution is possible, and/or falling back to agentId when configAgent is unavailable so you never pass an empty string into the hook.

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

Comment on lines +67 to +70
// Approximate path length (Manhattan distance) for uniform
// particle speed across edges of different lengths.
const approxLength = Math.abs(tx - sx) + Math.abs(ty - sy) + BEND_OFFSET
const durSec = Math.max(MIN_PARTICLE_DUR_SEC, approxLength / PARTICLE_PX_PER_SEC)
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

approxLength is intended to approximate the Manhattan length of the rendered L-shaped path, but the current formula adds BEND_OFFSET on top of |ty - sy|, effectively double-counting the bend distance for typical edges. This makes particle speed inconsistent across edges and generally slower than PARTICLE_PX_PER_SEC. Compute the length as |tx-sx| + |bendY-sy| + |ty-bendY| for the L path (and |ty-sy| for the straight vertical path) to match the actual geometry.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +164
if (collapsedDeptIds && collapsedDeptIds.size > 0) {
tree.nodes = tree.nodes
.filter((n) => !(n.parentId && collapsedDeptIds.has(n.parentId)))
.map((n) =>
n.type === 'department' && collapsedDeptIds.has(n.id)
? { ...n, data: { ...n.data, isCollapsed: true } }
: n,
)
tree.edges = tree.edges.filter((e) => {
// Drop edges pointing at or from filtered-out child agents.
const source = tree.nodes.find((n) => n.id === e.source)
const target = tree.nodes.find((n) => n.id === e.target)
return source !== undefined && target !== undefined
})
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

The collapsed-department edge filter does tree.nodes.find(...) twice per edge, which is O(E·N) and will scale poorly as the chart grows. You can make this O(E+N) by building a Set of remaining node IDs once (e.g. const nodeIds = new Set(tree.nodes.map(n => n.id))) and then filtering edges with nodeIds.has(e.source) && nodeIds.has(e.target).

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

codecov bot commented Apr 5, 2026

Codecov Report

❌ Patch coverage is 73.17073% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.54%. Comparing base (d341d37) to head (e87c502).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/synthorg/api/controllers/analytics.py 29.03% 20 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1083      +/-   ##
==========================================
- Coverage   89.57%   89.54%   -0.03%     
==========================================
  Files         750      750              
  Lines       43975    44045      +70     
  Branches     4417     4426       +9     
==========================================
+ Hits        39389    39441      +52     
- Misses       3797     3813      +16     
- Partials      789      791       +2     

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

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 23

Caution

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

⚠️ Outside diff range comments (2)
web/src/pages/dashboard/BudgetBurnChart.tsx (1)

74-91: ⚠️ Potential issue | 🟡 Minor

Missing currency prop in tooltip content.

ChartTooltipContent doesn't accept or use a currency parameter, so the tooltip will use the default currency formatting regardless of the currency prop passed to BudgetBurnChart. Compare with SpendBurnChart.tsx which correctly threads currency through to its tooltip.

Proposed fix
-function ChartTooltipContent({ active, payload, label }: {
+function ChartTooltipContent({ active, payload, label, currency }: {
   active?: boolean
   payload?: Array<{ value: number; dataKey: string }>
   label?: string
+  currency?: string
 }) {
   if (!active || !payload?.length) return null
   return (
     <div className="rounded-md border border-border bg-card px-3 py-2 text-xs shadow-md">
       <p className="mb-1 font-sans text-text-secondary">{label}</p>
       {payload.map((entry) => (
         <p key={entry.dataKey} className="font-mono text-foreground">
           {entry.dataKey === 'projected' ? 'Forecast: ' : 'Spend: '}
-          {formatCurrency(entry.value)}
+          {formatCurrency(entry.value, currency)}
         </p>
       ))}
     </div>
   )
 }

And update the usage:

-<Tooltip content={<ChartTooltipContent />} />
+<Tooltip content={<ChartTooltipContent currency={currency} />} />

Also applies to: 157-157

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

In `@web/src/pages/dashboard/BudgetBurnChart.tsx` around lines 74 - 91,
ChartTooltipContent currently ignores the chart's currency so tooltips always
use default formatting; update the ChartTooltipContent signature to accept a
currency prop (e.g., currency?: string), pass that currency into formatCurrency
when rendering each entry, and then thread the same currency from
BudgetBurnChart into the tooltip's content prop where ChartTooltipContent is
used so the tooltip shows the correct currency. Ensure you reference
ChartTooltipContent and formatCurrency and the BudgetBurnChart usage so the prop
name matches in both places.
web/src/pages/tasks/TaskCreateDialog.tsx (1)

216-276: 🧹 Nitpick | 🔵 Trivial

Consider extracting FormField or using InputField from components/ui.

The coding guidelines recommend reusing components from web/src/components/ui/. This file defines a local FormField component rather than using the existing InputField component. If FormField's accessibility pattern is valuable, consider extracting it to components/ui/form-field.tsx for reuse across other dialogs. Alternatively, the accessibility improvements could be incorporated into InputField itself.

As per coding guidelines: "Always reuse existing components from web/src/components/ui/ (... InputField, SelectField...) before creating new ones"

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

In `@web/src/pages/tasks/TaskCreateDialog.tsx` around lines 216 - 276,
TaskCreateDialog is using a locally defined FormField component instead of
reusing the existing InputField/SelectField in web/src/components/ui/; replace
or extract to follow the guideline. Fix by either: 1) refactor TaskCreateDialog
to use the existing InputField and SelectField components (replace
FormField-wrapped input/textarea/select usages with InputField/SelectField and
pass errors, label, required props) referencing the TaskCreateDialog component
and the FormField symbol to locate occurrences; or 2) if the FormField
accessibility pattern must be preserved, extract it into a shared
components/ui/form-field.tsx and import it in TaskCreateDialog and other dialogs
(move the FormField definition to the new file, export it, and update imports).
Ensure prop names (label, error, required) match the shared component API and
update usages for title, description, project, created_by, assigned_to,
estimated_complexity, type, priority, and budget_limit fields accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/security.md`:
- Around line 152-159: Update the "Browser support for directive splitting"
section to indicate the Firefox bug status may change by adding a short,
version-tracking note next to the Firefox link (e.g., "check bug status
periodically / update when landing in Firefox") and include an "as of <date>" or
similar timestamp; reference the section header "Browser support for directive
splitting" and the Firefox bug URL
(https://bugzilla.mozilla.org/show_bug.cgi?id=1529338) so reviewers can find and
update this note when Firefox adds support for style-src-attr.

In `@src/synthorg/api/app.py`:
- Around line 276-296: The warning handler in the except block around
channels_plugin.publish may raise KeyError by indexing payload directly; update
the except block (where logger.warning is called with
PROMPT_PERSONALITY_NOTIFY_FAILED) to avoid direct payload[...] access—either
extract required values from payload into local variables before the try or use
safe lookups like
payload.get("agent_id")/payload.get("agent_name")/payload.get("task_id")/payload.get("trim_tier")/payload.get("before_tokens")/payload.get("after_tokens")
with sensible defaults, then pass those locals into logger.warning so the
log-and-swallow behavior of the notifier is preserved.

In `@web/src/__tests__/pages/org-edit/DepartmentEditDrawer.test.tsx`:
- Around line 41-47: The test 'shows the agent count from the runtime health
payload' currently only asserts the meter was removed; update it to also assert
that the drawer renders the health.agent_count summary by calling renderDrawer()
then checking for the expected text produced from the payload (e.g.
expect(screen.getByText(/1 agent/ or /N agent/)) or a regex like /\\d+ agent/),
referencing the rendered output produced by renderDrawer and the
health.agent_count field so the test fails if the agent-count summary stops
being rendered.

In `@web/src/__tests__/pages/org-edit/GeneralTab.test.tsx`:
- Around line 53-67: The test currently never asserts that the change event
updated local state; add an assertion that the company name input actually
changed (e.g., assert screen.getByLabelText(/company name/i) has value 'Updated
Corp') so the form is truly dirty, and then restore the property-based sweep
using fast-check: wrap the multi-field mutation checks in fc.assert +
fc.property with the same arbitrary field generator pattern used in
web/src/__tests__/utils/*.property.test.ts to exercise GeneralTab's `#1081` gate
across many dirty-state combinations; keep mockOnUpdate and the existing Save
Settings disabled/assertions intact so the property test clicks the disabled
button and verifies mockOnUpdate is not called.

In `@web/src/components/layout/Sidebar.tsx`:
- Around line 371-378: Replace the inline sr-only span that announces WebSocket
status with the shared LiveRegion component: import LiveRegion and pass the same
computed message based on wsConnected and wsReconnectExhausted (the existing
conditional string values) to LiveRegion so it handles aria-live, role="status"
and debounce/accessibility behavior; remove the inline <span role="status"
aria-live="polite"> block and ensure LiveRegion is rendered in its place so
announcements use the centralized behavior.

In `@web/src/components/ui/animated-presence.tsx`:
- Around line 31-41: pageVariants currently hardcodes transition durations
(duration: 0.12 and duration: 0.06); refactor to use motion presets by adding
tweenCrossfadeEnter and tweenCrossfadeExit in "@/lib/motion" (export const
tweenCrossfadeEnter = { type: 'tween', duration: 0.12, ease: 'easeOut' } and
export const tweenCrossfadeExit = { type: 'tween', duration: 0.06, ease:
'easeIn' }) and then import those presets into this file and replace the inline
transition objects in pageVariants.animate and pageVariants.exit with the
imported tweenCrossfadeEnter and tweenCrossfadeExit respectively.

In `@web/src/components/ui/confirm-dialog.tsx`:
- Around line 1-4: The Confirm Dialog allows double submissions because loading
is parent-controlled; add an internal re-entry guard (e.g., an isConfirming
useRef or useState) inside the component that short-circuits the confirm handler
and disables the confirm button while a confirm is in-flight. Update the local
handler that calls the onConfirm prop (referenced as onConfirm) — name it
handleConfirm or similar — to check and set isConfirming before invoking
onConfirm, clear the flag only after the promise resolves/rejects, and use this
flag to disable the confirm button and show the Loader2 so rapid double-clicks
cannot trigger multiple onConfirm executions.

In `@web/src/components/ui/health-popover.tsx`:
- Around line 198-234: fetchHealth currently allows overlapping requests to race
and unconditionally write loadState, so an older/slower response can overwrite a
newer one; fix by adding a useRef (import useRef from React) e.g. latestFetchRef
to hold an incrementing request id or timestamp, increment it at the start of
fetchHealth and capture the id in the promise handlers, then only call
setLoadState/setNowMs inside .then and .catch if the captured id equals
latestFetchRef.current (ensuring "latest-response-wins"); update the fetchHealth
function (and add the useRef declaration alongside open/loadState/nowMs) and
keep all other logic unchanged.
- Around line 312-321: Remove the explicit aria-describedby={undefined} prop on
the Dialog.Popup component so Base UI can auto-wire the Dialog.Description;
locate the Dialog.Popup usage in the health-popover component and delete the
aria-describedby prop (or leave it unset) to restore screen-reader announcement
of the Dialog.Description content.

In `@web/src/components/ui/slot.tsx`:
- Around line 43-52: In Slot, calling Children.only(children) can throw for
non-singleton children; first check the children shape before invoking
Children.only — e.g., verify React.Children.count(children) === 1 and that the
child is a valid React element (use isValidElement/React.isValidElement) and
only then call Children.only; if the guard fails, call log.warn with the
existing message and return children as a graceful no-op; update the Slot
function to perform this pre-check around Children.only and isValidElement to
avoid render crashes.

In `@web/src/hooks/useOrgChartData.ts`:
- Around line 159-164: The edge filtering currently does O(edges × nodes) work
by calling tree.nodes.find(...) for each tree.edges entry; replace that with an
O(edges + nodes) approach by first building a Set of valid node IDs from
tree.nodes (e.g., collect n.id into a Set) and then use tree.edges.filter(e =>
idSet.has(e.source) && idSet.has(e.target)) so the checks for e.source and
e.target are O(1) each; update the logic around tree.edges and tree.nodes
accordingly.

In `@web/src/pages/AgentDetailPage.tsx`:
- Around line 26-29: The lookup fallback is dropping the raw route param which
prevents name-based routes from working; update the fallback for
resolvedAgentName to use agentId when configAgent?.name is undefined (i.e.,
change resolvedAgentName from configAgent?.name ?? '' to configAgent?.name ??
agentId) and apply the same change to the other occurrence mentioned (the
similar fallback at line 44), ensuring components like useAgentDetailData
receive the original route param as the fallback identifier.

In `@web/src/pages/org-edit/AgentEditDrawer.tsx`:
- Around line 172-199: The Delete and Save Button elements in AgentEditDrawer
(the Button with onClick={() => setDeleteOpen(true)} / Trash2 and the Button
that calls handleSave and renders Loader2) include an explicit
aria-disabled="true" while also using the native disabled prop; remove the
redundant aria-disabled="true" attributes from these Button instances (keep
disabled and title={ORG_EDIT_COMING_SOON_TOOLTIP}) so the native disabled
behavior/semantics are preserved without duplicate ARIA attributes.

In `@web/src/pages/org-edit/AgentsTab.tsx`:
- Around line 198-203: The disabled "Add Agent" Button uses aria-disabled and
title to explain why it’s unavailable, but native disabled buttons aren’t
focusable and title is unreliable; update the AgentsTab UI so the explanatory
copy (ORG_EDIT_COMING_SOON_TOOLTIP / "#1081" text) is visible outside the
disabled button — for example wrap the Button in a focusable element or render
visible helper text or a tooltip trigger next to the Button and remove reliance
on title on the disabled Button (affecting the Button usage around setCreateOpen
in AgentsTab and the other occurrence at lines ~227-232).

In `@web/src/pages/org-edit/DepartmentCreateDialog.tsx`:
- Around line 163-171: The Create Department Button is permanently disabled but
still has an onClick handler (onClick={handleSubmit}), which is misleading;
remove the onClick prop from the Button component in DepartmentCreateDialog
(retain disabled, aria-disabled, title, submitting/Loader2, and text) or if you
intend to re-enable later, replace the prop with a brief comment referencing
issue `#1081` and keep handleSubmit in scope. Ensure you update the Button JSX so
there is no unused interactive handler attached while the button remains
disabled.

In `@web/src/pages/org/DepartmentGroupNode.stories.tsx`:
- Around line 70-89: The OverBudget story declares budgetPercent: 20 but still
shows utilizationPercent: 95, so update the fixture so utilizationPercent
exceeds the budget threshold; in the OverBudget export (symbol: OverBudget)
adjust the data fields (budgetPercent and/or utilizationPercent) so
utilizationPercent > budgetPercent (for example set utilizationPercent to a
value >100 or at least greater than 20) to ensure the story truly represents an
over-budget state.

In `@web/src/pages/org/HierarchyEdge.tsx`:
- Around line 67-70: The particle duration is inflated for straight
vertical/horizontal edges because BEND_OFFSET is always added to approxLength;
change the approxLength calculation in HierarchyEdge (where approxLength,
BEND_OFFSET, durSec, MIN_PARTICLE_DUR_SEC, PARTICLE_PX_PER_SEC are used) to only
add BEND_OFFSET when the edge actually bends (i.e., both dx and dy are non-zero
or sx !== tx && sy !== ty). Compute approxLength = Math.abs(tx - sx) +
Math.abs(ty - sy) + (hasBend ? BEND_OFFSET : 0) and then compute durSec as
before to restore uniform particle speed on aligned edges.

In `@web/src/pages/org/layout.ts`:
- Around line 106-121: The fallback branch currently triggers when
leafNodes.length === 0 but synthetic owner nodes count as leaves, so owner-only
orgs skip the empty layout; change the condition to detect "no agent nodes"
instead (e.g., count only real agent/department leaf nodes, excluding synthetic
owner nodes) and use that to enter the empty-state layout; ensure the code that
maps nodes (nodes.map) still sets position, width (EMPTY_GROUP_MIN_WIDTH) and
height (EMPTY_GROUP_HEIGHT) for each empty department, and place owner nodes
separately before rendering empty departments so they don't overlap the first
empty box.
- Around line 311-449: The post-layout adjustment pass (steps labeled Step 4
onward) assumes top-to-bottom layout and breaks when direction === 'LR'; update
the routine that runs this pass to check the layout direction (e.g., a
LayoutDirection or `direction` parameter) and only perform the
vertical/centering logic when direction === 'TB', or alternatively implement
mirrored behavior for LR by swapping x/y or inverting x calculations (affecting
rootPopulated, allGroupResults sorting/centering logic, ownerBottomY computation
from positionedLeafMap, Step 4.5 root/row vertical shifts using
DESIRED_INTER_DEPT_GAP, and Step 5 owner centering using getNodeDim) so that LR
produces the correct left-to-right layout; ensure the gating or mirroring covers
the root centering, non-root shifts, and owner repositioning code paths
(references: rootPopulated, rootGroupIds, allGroupResults, positionedLeafMap,
getNodeDim, DESIRED_INTER_DEPT_GAP).

In `@web/src/pages/org/OrgChartSearchOverlay.tsx`:
- Around line 81-88: Replace the raw <button> in OrgChartSearchOverlay.tsx with
the shared Button component: add "import { Button } from
'@/components/ui/button'" at the top, then render <Button> with
onClick={onClose}, aria-label="Close search" and the appropriate variant/size or
className to match the existing styling (hover, rounded, padding, and text
color). Ensure the X icon remains as the child (e.g., <X className="size-3.5"
/>) and that the Button preserves accessibility and transition behavior
currently provided by the native button.

In `@web/src/pages/OrgChartPage.tsx`:
- Around line 605-612: Replace the hardcoded inline transition string in the
dimmed branch with a motion preset from "@/lib/motion": add or reuse a named
preset (e.g., searchDimTransition or OPACITY_DIM_TRANSITION) in "@/lib/motion",
import it into OrgChartPage.tsx, and assign that preset to next.style (or
reference a CSS custom property set from the preset) instead of using
'transition: "opacity 180ms ease"' in the block that sets next.style when dimmed
(the code that checks `if (dimmed) { next.style = { ...n.style, opacity: 0.25,
transition: 'opacity 180ms ease' } }`).
- Around line 776-804: The MiniMap props use hardcoded color literals; update
the MiniMap configuration (the MiniMap component and its props bgColor,
maskColor, maskStrokeColor, nodeColor, and nodeStrokeColor) to use design
tokens/CSS variables or Tailwind semantic variables (e.g., var(--so-...))
instead of literal values like "rgba(15, 18, 26, 0.92)", "#38bdf8", and
"#f59e0b"; for nodeColor and nodeStrokeColor adjust the returned values to
reference those variables (e.g., return strings using var(--so-accent) or
semantic token names) so all colors follow the project's design-token
convention.

---

Outside diff comments:
In `@web/src/pages/dashboard/BudgetBurnChart.tsx`:
- Around line 74-91: ChartTooltipContent currently ignores the chart's currency
so tooltips always use default formatting; update the ChartTooltipContent
signature to accept a currency prop (e.g., currency?: string), pass that
currency into formatCurrency when rendering each entry, and then thread the same
currency from BudgetBurnChart into the tooltip's content prop where
ChartTooltipContent is used so the tooltip shows the correct currency. Ensure
you reference ChartTooltipContent and formatCurrency and the BudgetBurnChart
usage so the prop name matches in both places.

In `@web/src/pages/tasks/TaskCreateDialog.tsx`:
- Around line 216-276: TaskCreateDialog is using a locally defined FormField
component instead of reusing the existing InputField/SelectField in
web/src/components/ui/; replace or extract to follow the guideline. Fix by
either: 1) refactor TaskCreateDialog to use the existing InputField and
SelectField components (replace FormField-wrapped input/textarea/select usages
with InputField/SelectField and pass errors, label, required props) referencing
the TaskCreateDialog component and the FormField symbol to locate occurrences;
or 2) if the FormField accessibility pattern must be preserved, extract it into
a shared components/ui/form-field.tsx and import it in TaskCreateDialog and
other dialogs (move the FormField definition to the new file, export it, and
update imports). Ensure prop names (label, error, required) match the shared
component API and update usages for title, description, project, created_by,
assigned_to, estimated_complexity, type, priority, and budget_limit fields
accordingly.
🪄 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: 560f79f3-adc0-4d7c-a386-97c848de2c86

📥 Commits

Reviewing files that changed from the base of the PR and between 174e2be and fa28743.

⛔ Files ignored due to path filters (1)
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (102)
  • CLAUDE.md
  • docs/architecture/tech-stack.md
  • docs/design/brand-and-ux.md
  • docs/design/engine.md
  • docs/design/page-structure.md
  • docs/design/ux-guidelines.md
  • docs/design/ux-research.md
  • docs/guides/deployment.md
  • docs/security.md
  • src/synthorg/api/app.py
  • src/synthorg/api/controllers/analytics.py
  • src/synthorg/api/ws_models.py
  • src/synthorg/engine/agent_engine.py
  • src/synthorg/observability/events/prompt.py
  • src/synthorg/settings/definitions/engine.py
  • tests/unit/api/test_personality_trim_notifier_factory.py
  • tests/unit/engine/test_personality_trim_notifier.py
  • tests/unit/settings/test_engine_settings.py
  • web/CLAUDE.md
  • web/components.json
  • web/index.html
  • web/nginx.conf
  • web/package.json
  • web/security-headers.conf
  • web/src/App.tsx
  • web/src/__tests__/App.test.tsx
  • web/src/__tests__/components/layout/StatusBar.test.tsx
  • web/src/__tests__/components/ui/confirm-dialog.test.tsx
  • web/src/__tests__/hooks/useGlobalNotifications.test.ts
  • web/src/__tests__/pages/AgentDetailPage.test.tsx
  • web/src/__tests__/pages/agents/AgentFilters.test.tsx
  • web/src/__tests__/pages/agents/AgentGridView.test.tsx
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/__tests__/pages/org-edit/AgentCreateDialog.test.tsx
  • web/src/__tests__/pages/org-edit/AgentEditDrawer.test.tsx
  • web/src/__tests__/pages/org-edit/DepartmentCreateDialog.test.tsx
  • web/src/__tests__/pages/org-edit/DepartmentEditDrawer.test.tsx
  • web/src/__tests__/pages/org-edit/DepartmentsTab.test.tsx
  • web/src/__tests__/pages/org-edit/GeneralTab.test.tsx
  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
  • web/src/__tests__/pages/org/OrgChartPage.test.tsx
  • web/src/__tests__/pages/org/build-org-tree.test.ts
  • web/src/__tests__/stores/agents.test.ts
  • web/src/api/types.ts
  • web/src/components/layout/AppLayout.tsx
  • web/src/components/layout/Sidebar.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/components/ui/animated-presence.tsx
  • web/src/components/ui/button.tsx
  • web/src/components/ui/command-palette.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/components/ui/dialog.tsx
  • web/src/components/ui/health-popover.tsx
  • web/src/components/ui/slot.tsx
  • web/src/components/ui/theme-toggle.tsx
  • web/src/hooks/useCommandPalette.ts
  • web/src/hooks/useGlobalNotifications.ts
  • web/src/hooks/useLiveEdgeActivity.ts
  • web/src/hooks/useOrgChartData.ts
  • web/src/lib/csp.ts
  • web/src/pages/AgentDetailPage.tsx
  • web/src/pages/OrgChartPage.tsx
  • web/src/pages/OrgEditPage.tsx
  • web/src/pages/agents/AgentFilters.tsx
  • web/src/pages/agents/AgentGridView.tsx
  • web/src/pages/budget/CostBreakdownChart.tsx
  • web/src/pages/budget/SpendBurnChart.tsx
  • web/src/pages/dashboard/BudgetBurnChart.tsx
  • web/src/pages/org-edit/AgentCreateDialog.tsx
  • web/src/pages/org-edit/AgentEditDrawer.tsx
  • web/src/pages/org-edit/AgentsTab.tsx
  • web/src/pages/org-edit/DepartmentCreateDialog.tsx
  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/org-edit/DepartmentsTab.tsx
  • web/src/pages/org-edit/GeneralTab.tsx
  • web/src/pages/org-edit/PackSelectionDialog.tsx
  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/pages/org-edit/coming-soon.ts
  • web/src/pages/org/AgentNode.tsx
  • web/src/pages/org/CeoNode.tsx
  • web/src/pages/org/DepartmentGroupNode.stories.tsx
  • web/src/pages/org/DepartmentGroupNode.tsx
  • web/src/pages/org/HierarchyEdge.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
  • web/src/pages/org/OrgChartToolbar.tsx
  • web/src/pages/org/OwnerNode.tsx
  • web/src/pages/org/build-org-tree.ts
  • web/src/pages/org/layout.ts
  • web/src/pages/projects/ProjectTeamSection.tsx
  • web/src/pages/providers/ModelPullDialog.tsx
  • web/src/pages/providers/ProviderFormModal.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/tasks/TaskCreateDialog.tsx
  • web/src/pages/workflow-editor/VersionDiffViewer.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/router/index.tsx
  • web/src/router/routes.ts
  • web/src/stores/agents.ts
  • web/src/stores/org-chart-prefs.ts
  • web/src/styles/global.css
  • web/vite.config.ts

Comment on lines +152 to +159
#### Browser support for directive splitting

- `style-src-elem`: Chrome 75+, Firefox 108+, Safari 15.4+ (partial, full at 26.2+), Edge 79+.
- `style-src-attr`: Chrome 75+, Edge 79+. Not supported in Firefox
([bug 1529338](https://bugzilla.mozilla.org/show_bug.cgi?id=1529338)) or Safari.

When a browser does not recognise `style-src-attr`, it falls back to `style-src` -- the
directive-splitting is backwards-compatible.
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

Minor: Firefox bug link could benefit from version tracking.

The documentation correctly notes Firefox doesn't support style-src-attr and links to the bug. Consider adding a note to check the bug status periodically, as Firefox support may land and the fallback behavior note would then be outdated.

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

In `@docs/security.md` around lines 152 - 159, Update the "Browser support for
directive splitting" section to indicate the Firefox bug status may change by
adding a short, version-tracking note next to the Firefox link (e.g., "check bug
status periodically / update when landing in Firefox") and include an "as of
<date>" or similar timestamp; reference the section header "Browser support for
directive splitting" and the Firefox bug URL
(https://bugzilla.mozilla.org/show_bug.cgi?id=1529338) so reviewers can find and
update this note when Firefox adds support for style-src-attr.

Comment on lines +53 to +67
it('disables Save Settings button with #1081 tooltip even when form is dirty', () => {
const config = makeCompanyConfig()
render(<GeneralTab config={config} onUpdate={mockOnUpdate} saving={false} />)
expect(screen.getByRole('button', { name: 'Save Settings' })).toBeDisabled()
})

it('save button disablement follows invariant across states', () => {
fc.assert(
fc.property(fc.boolean(), fc.string({ minLength: 1 }), (saving, nextName) => {
cleanup()
const config = makeCompanyConfig()
render(<GeneralTab config={config} onUpdate={mockOnUpdate} saving={saving} />)
if (nextName !== config.company_name) {
fireEvent.change(screen.getByLabelText(/company name/i), {
target: { value: nextName },
})
}
const button = screen.getByRole('button', { name: /save settings/i })
const isPristine = nextName === config.company_name
const shouldBeDisabled = isPristine || saving
if (shouldBeDisabled) {
expect(button).toBeDisabled()
} else {
expect(button).toBeEnabled()
}
}),
{ numRuns: 20 },
)
})

it('disables save button when saving', () => {
const config = makeCompanyConfig()
render(<GeneralTab config={config} onUpdate={mockOnUpdate} saving={true} />)
expect(screen.getByRole('button', { name: 'Save Settings' })).toBeDisabled()
// Make the form dirty. Before the backend gate, this would have
// enabled the Save button; with the gate in place it must stay
// disabled regardless of form state.
fireEvent.change(screen.getByLabelText(/company name/i), {
target: { value: 'Updated Corp' },
})
const saveButton = screen.getByRole('button', { name: /save settings/i })
expect(saveButton).toBeDisabled()
expect(saveButton.getAttribute('title') ?? '').toContain('1081')
// Clicking the disabled button must not call onUpdate.
fireEvent.click(saveButton)
expect(mockOnUpdate).not.toHaveBeenCalled()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- current test ---'
sed -n '1,220p' web/src/__tests__/pages/org-edit/GeneralTab.test.tsx

printf '\n%s\n' '--- fast-check usage in web tests ---'
rg -n -C2 'fc\.assert|fc\.property|from .fast-check.|from "fast-check"' web/src

Repository: Aureliolo/synthorg

Length of output: 50374


Assert the edited value first, and restore an equivalent fast-check sweep.

Save Settings starts disabled, so this test passes even if the change event stops updating local state. Add an assertion like expect(screen.getByLabelText(/company name/i)).toHaveValue('Updated Corp') to prove the form actually became dirty. Then restore a property-based sweep across multiple field mutations (using fc.assert + fc.property with an arbitrary field generator) to exercise the #1081 gate across dirty-state combinations instead of a single field, following patterns in web/src/__tests__/utils/*.property.test.ts.

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

In `@web/src/__tests__/pages/org-edit/GeneralTab.test.tsx` around lines 53 - 67,
The test currently never asserts that the change event updated local state; add
an assertion that the company name input actually changed (e.g., assert
screen.getByLabelText(/company name/i) has value 'Updated Corp') so the form is
truly dirty, and then restore the property-based sweep using fast-check: wrap
the multi-field mutation checks in fc.assert + fc.property with the same
arbitrary field generator pattern used in
web/src/__tests__/utils/*.property.test.ts to exercise GeneralTab's `#1081` gate
across many dirty-state combinations; keep mockOnUpdate and the existing Save
Settings disabled/assertions intact so the property test clicks the disabled
button and verifies mockOnUpdate is not called.

Comment on lines +371 to 378
{/* Screen reader live announcement for status changes */}
<span className="sr-only" role="status" aria-live="polite">
{wsConnected
? 'Connected'
? 'Connection status: connected'
: wsReconnectExhausted
? 'Disconnected'
: 'Reconnecting...'}
? 'Connection status: disconnected'
: 'Connection status: reconnecting'}
</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.

🧹 Nitpick | 🔵 Trivial

Prefer the shared LiveRegion for the WS status announcement.

This reconnect status can flap; the inline role="status" span skips the debounce/accessibility behavior the shared component already provides.

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

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

In `@web/src/components/layout/Sidebar.tsx` around lines 371 - 378, Replace the
inline sr-only span that announces WebSocket status with the shared LiveRegion
component: import LiveRegion and pass the same computed message based on
wsConnected and wsReconnectExhausted (the existing conditional string values) to
LiveRegion so it handles aria-live, role="status" and debounce/accessibility
behavior; remove the inline <span role="status" aria-live="polite"> block and
ensure LiveRegion is rendered in its place so announcements use the centralized
behavior.

Comment on lines +605 to +612
}
if (dimmed) {
next.style = { ...n.style, opacity: 0.25, transition: 'opacity 180ms ease' }
} else if (n.style && typeof (n.style as { opacity?: number }).opacity === 'number') {
const rest = { ...n.style } as Record<string, unknown>
delete rest['opacity']
next.style = rest
}
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

Hardcoded transition duration should use motion presets.

The inline style uses transition: 'opacity 180ms ease' which hardcodes the duration. Per coding guidelines, Framer Motion transition durations should use presets from @/lib/motion.

As per coding guidelines: "Do NOT hardcode Framer Motion transition durations -- use presets from @/lib/motion"

🎨 Suggested approach

Consider defining the search dim transition in @/lib/motion and importing it, or using a CSS custom property for the duration:

       if (dimmed) {
-        next.style = { ...n.style, opacity: 0.25, transition: 'opacity 180ms ease' }
+        next.style = { ...n.style, opacity: 0.25, transition: `opacity var(--so-transition-dim, 180ms) ease` }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/OrgChartPage.tsx` around lines 605 - 612, Replace the hardcoded
inline transition string in the dimmed branch with a motion preset from
"@/lib/motion": add or reuse a named preset (e.g., searchDimTransition or
OPACITY_DIM_TRANSITION) in "@/lib/motion", import it into OrgChartPage.tsx, and
assign that preset to next.style (or reference a CSS custom property set from
the preset) instead of using 'transition: "opacity 180ms ease"' in the block
that sets next.style when dimmed (the code that checks `if (dimmed) { next.style
= { ...n.style, opacity: 0.25, transition: 'opacity 180ms ease' } }`).

@Aureliolo Aureliolo force-pushed the feat/radix-to-base-ui-migration branch from fa28743 to b2ea28b Compare April 5, 2026 23:30
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 5, 2026 23:31 — with GitHub Actions Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 32

Caution

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

⚠️ Outside diff range comments (7)
docs/design/page-structure.md (1)

250-337: ⚠️ Potential issue | 🟡 Minor

Update remaining Agent route docs to agentId.

While this section was updated, the same document still uses agentName in route examples (Line [72], Line [74], Line [260], Line [337]). Please align those to agentId to match current routing.

📝 Proposed doc fix
- Click navigates to the Agent Detail page at `/agents/{agentName}`.
+ Click navigates to the Agent Detail page at `/agents/{agentId}`.

- **Agent Detail page** (`/agents/{agentName}`) -- single scrollable page with these sections:
+ **Agent Detail page** (`/agents/{agentId}`) -- single scrollable page with these sections:

- Navigates to a dedicated full page at `/agents/{agentName}`.
+ Navigates to a dedicated full page at `/agents/{agentId}`.

- | `/agents/:agentName` | Agent detail | Full page with scrollable sections |
+ | `/agents/:agentId` | Agent detail | Full page with scrollable sections |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/design/page-structure.md` around lines 250 - 337, Update the remaining
route references and descriptive text that still use `/agents/:agentName` to
`/agents/:agentId` across the document (specifically replace occurrences in the
URL Routing Map and any inline examples or prose such as the Agent Detail Page
trigger and route list), ensuring the Agent Detail Page heading and the routes
table entry (previously `/agents/:agentName`) and any mentions like "agentName"
are changed to "agentId" so all examples and links consistently reflect the
current routing.
web/src/pages/org-edit/AgentEditDrawer.tsx (1)

81-101: 🧹 Nitpick | 🔵 Trivial

Dead code: handleSave logic is unreachable while button is disabled.

While the coming-soon gate is in place, the handleSave callback (lines 81-101) and handleDelete callback (lines 103-115) cannot be invoked. Consider adding a guard comment or TODO noting these will become active when #1081 lands, similar to the test file comments.

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

In `@web/src/pages/org-edit/AgentEditDrawer.tsx` around lines 81 - 101, The
handleSave and handleDelete callbacks are currently unreachable because the UI
button is disabled behind the coming-soon gate; add a brief guard comment/TODO
above both function definitions (handleSave and handleDelete) stating these are
intentionally inactive until issue `#1081` lands and reference `#1081` so future
readers know why they appear unused, mirroring the explanatory comments in the
test files.
web/src/lib/motion.ts (1)

72-79: 🧹 Nitpick | 🔵 Trivial

Derive TRANSITION_SLOW_MS from tweenSlow.

Line 79 re-hardcodes the same slow timing already declared in tweenSlow. That reintroduces drift the next time someone tweaks the preset. Define one shared seconds constant and derive both exports from it.

♻️ Suggested change
+const TWEEN_SLOW_DURATION_S = 0.4;
+
 /** Slow tween: page transitions, large layout shifts. */
 export const tweenSlow: Transition = {
   type: "tween",
-  duration: 0.4,
+  duration: TWEEN_SLOW_DURATION_S,
   ease: [0.4, 0, 0.2, 1],
 };
 
 /** Slow tween duration in milliseconds (matches tweenSlow.duration). */
-export const TRANSITION_SLOW_MS = 400;
+export const TRANSITION_SLOW_MS = TWEEN_SLOW_DURATION_S * 1000;
As per coding guidelines, `web/src/**/*.{ts,tsx}`: Do NOT hardcode Framer Motion transition durations -- use presets from `@/lib/motion`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/lib/motion.ts` around lines 72 - 79, tweenSlow.duration is hardcoded
then duplicated as TRANSITION_SLOW_MS, risking drift; introduce a single shared
seconds constant (e.g., SLOW_SEC or SLOW_SECONDS) and derive both
tweenSlow.duration (in seconds) and TRANSITION_SLOW_MS (milliseconds via
multiplication by 1000) from that constant so updates only happen in one place;
update references to use the new constant and keep symbol names tweenSlow and
TRANSITION_SLOW_MS unchanged.
web/src/pages/org-edit/DepartmentEditDrawer.tsx (1)

55-87: 🧹 Nitpick | 🔵 Trivial

Unreachable code: save/delete handlers and ConfirmDialog.

With both Save and Delete buttons permanently disabled, the handleSave (lines 55-73) and handleDelete (lines 75-87) callbacks are unreachable. The ConfirmDialog (lines 196-205) can never open since the Delete button's onClick is blocked.

Consider either:

  1. Removing the dead code until backend CRUD is implemented
  2. Keeping it but adding a // TODO: re-enable when #1081 ships comment to clarify intent

Also applies to: 196-205

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

In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx` around lines 55 - 87, The
Save/Delete handlers and the ConfirmDialog are unreachable because the Save and
Delete buttons are permanently disabled; add clear TODO comments indicating
these are intentionally disabled until backend CRUD is implemented (reference
the issue number `#1081`) instead of removing code: place a "// TODO: re-enable
when `#1081` ships — handlers kept for future CRUD support" comment immediately
above the handleSave and handleDelete useCallback declarations and above the
ConfirmDialog JSX block to make intent explicit and avoid dead-code confusion;
alternatively, if you prefer removal, delete the handleSave, handleDelete
functions and the ConfirmDialog JSX and any associated state (setDeleting,
setDeleteOpen) to eliminate dead code.
web/src/pages/org-edit/DepartmentsTab.tsx (1)

211-226: ⚠️ Potential issue | 🟡 Minor

The new copy still points users at a disabled “Add Department” flow.

The empty state tells users to create their first department, and the under-allocation warning tells them to add a new one, but every visible Add Department control in this tab is intentionally disabled. Reword these paths toward Add Team Pack / editing existing departments until #1081 lands.

Also applies to: 267-276, 308-311

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

In `@web/src/pages/org-edit/DepartmentsTab.tsx` around lines 211 - 226, Update the
copy in DepartmentsTab.tsx so it doesn't direct users to a disabled "Add
Department" action: change the EmptyState title/description and any
under-allocation warning text that currently prompt creation to instead suggest
using "Add Team Pack" or editing existing departments; update the Button (the
one using onClick={() => setCreateOpen(true)} and ORG_EDIT_COMING_SOON_TOOLTIP)
tooltip/text so it reflects "Coming soon" rather than instructing users to
create a department; make the same copy adjustments for the other instances in
this file that render EmptyState/Button-like UI (the components using Building2,
Plus, EmptyState, and that setCreateOpen) so messaging consistently points to
Add Team Pack / edit flows until `#1081` is implemented.
web/src/hooks/useOrgChartData.ts (1)

101-114: ⚠️ Potential issue | 🟠 Major

Don’t start polling after unmount.

If fetchCompanyData() settles after cleanup runs, this continuation still calls polling.start(). That leaks an interval and can double background fetches on remounts / StrictMode.

♻️ Suggested fix
   useEffect(() => {
+    let cancelled = false
     const companyStore = useCompanyStore.getState()
     companyStore.fetchCompanyData().then(() => {
+      if (cancelled) return
       if (useCompanyStore.getState().config) {
         companyStore.fetchDepartmentHealths().catch((err: unknown) => {
           log.warn('fetchDepartmentHealths failed:', err)
         })
       }
       polling.start()
     }).catch((err: unknown) => {
+      if (cancelled) return
       log.warn('fetchCompanyData failed:', err)
       polling.start()
     })
-    return () => polling.stop()
+    return () => {
+      cancelled = true
+      polling.stop()
+    }
     // eslint-disable-next-line `@eslint-react/exhaustive-deps` -- mount-only effect; polling ref identity is stable
   }, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/hooks/useOrgChartData.ts` around lines 101 - 114, The effect may call
polling.start() after the component has unmounted causing leaked intervals;
modify the useEffect around useCompanyStore.getState().fetchCompanyData() and
its downstream fetchDepartmentHealths() so they check an "isMounted" flag (or an
AbortController) before calling polling.start(), and set that flag to false in
the cleanup (which still calls polling.stop()); specifically, in the promise
.then and .catch handlers for fetchCompanyData() and the .catch for
fetchDepartmentHealths(), guard polling.start() with if (isMounted) {
polling.start() } (or abort the chain) to prevent starting polling after
unmount.
src/synthorg/engine/agent_engine.py (1)

1047-1076: ⚠️ Potential issue | 🟠 Major

Don't re-enable trimming when only the override lookup fails.

These two ENGINE settings are read under one try. If personality_trimming_enabled resolves to False and only personality_max_tokens_override raises, this block falls back to the defaults and leaves trimming_enabled=True, which silently turns trimming back on. Read the flag and the override independently so a partial settings-store failure preserves the value that was already resolved.

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

In `@src/synthorg/engine/agent_engine.py` around lines 1047 - 1076, The code
currently reads both ENGINE settings inside one try/except so a failure fetching
personality_max_tokens_override can cause trimming_enabled to be reset; change
the logic in agent_engine.py to fetch the two settings independently: call
self._config_resolver.get_bool("engine","personality_trimming_enabled") inside
its own try/except and update trimming_enabled only on success, then call
get_int("engine","personality_max_tokens_override") inside a separate try/except
and update tokens_override only on success; keep MemoryError/RecursionError
re-raised and preserve the existing warning logging behavior (logger.warning
with EXECUTION_ENGINE_ERROR) when either lookup fails so a partial failure does
not overwrite trimming_enabled resolved by the other lookup.
♻️ Duplicate comments (3)
web/src/pages/org/HierarchyEdge.tsx (1)

71-76: ⚠️ Potential issue | 🟡 Minor

The particle duration is still overcounting bent edges.

For routed edges, |dx| + |dy| + BEND_OFFSET is longer than the path you actually render whenever targetY >= bendY, so particles move slower than PARTICLE_PX_PER_SEC. Compute the duration from the rendered segments instead.

🔧 Suggested fix
-  const approxLength = Math.abs(tx - sx) < 0.5
-    ? Math.abs(ty - sy)
-    : Math.abs(tx - sx) + Math.abs(ty - sy) + BEND_OFFSET
+  const approxLength = Math.abs(tx - sx) < 0.5
+    ? Math.abs(ty - sy)
+    : BEND_OFFSET + Math.abs(tx - sx) + Math.abs(ty - bendY)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org/HierarchyEdge.tsx` around lines 71 - 76, The duration
calculation overestimates routed edges by adding BEND_OFFSET to the full
Manhattan distance; instead compute the actual rendered segment lengths and
derive durSec from that. Replace the approxLength logic in HierarchyEdge
(currently using sx, sy, tx, ty, BEND_OFFSET) with a computed segmentsLength: if
the edge is routed (i.e., has a bend at bendX/bendY) sum the two rendered
segment lengths (|sx - bendX| + |sy - bendY| and |tx - bendX| + |ty - bendY|),
otherwise use the direct Manhattan length (|tx - sx| + |ty - sy|); then use
durSec = Math.max(MIN_PARTICLE_DUR_SEC, segmentsLength / PARTICLE_PX_PER_SEC).
Ensure you reference the same bend coordinates used when rendering the path so
particles match the visual path.
web/src/components/layout/Sidebar.tsx (1)

371-378: ⚠️ Potential issue | 🟡 Minor

Use the shared LiveRegion for the WS status announcement.

Lines 371-378 still recreate the live announcement inline. This reconnect state can flap, so the shared component's debounce/accessibility behavior is a better fit and keeps announcements consistent across the dashboard.

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

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

In `@web/src/components/layout/Sidebar.tsx` around lines 371 - 378, Replace the
inline screen-reader span in Sidebar.tsx with the shared LiveRegion component:
compute the same status message from wsConnected and wsReconnectExhausted (the
current ternary logic) and pass it as the LiveRegion children/prop so
announcements use the shared debounce/accessibility behavior; reference the
LiveRegion component from web/src/components/ui/LiveRegion and remove the inline
<span role="status" aria-live="polite"> block that uses wsConnected and
wsReconnectExhausted.
web/src/components/ui/confirm-dialog.tsx (1)

92-95: ⚠️ Potential issue | 🟠 Major

submittingRef.current in render won't disable the button during submission.

Refs don't trigger re-renders, so disabled={submittingRef.current || loading} will always read the initial false value when rendering. The guard at line 94 prevents double-invocation, but the button remains visually enabled and clickable during the async operation.

Either convert submittingRef to useState so the button re-renders as disabled, or combine with the existing loading prop by having callers always provide it (which they should for the spinner to show).

🐛 Proposed fix using useState
-import { useRef } from 'react'
+import { useState } from 'react'
 import { AlertDialog } from '@base-ui/react/alert-dialog'
...
 }: ConfirmDialogProps) {
-  const submittingRef = useRef(false)
+  const [submitting, setSubmitting] = useState(false)
+  const busy = loading || submitting

   return (
     <AlertDialog.Root
       open={open}
       onOpenChange={(nextOpen: boolean) => {
-        if (loading && !nextOpen) return
+        if (busy && !nextOpen) return
         onOpenChange(nextOpen)
       }}
     >
...
             <AlertDialog.Close
               render={
-                <Button variant="outline" disabled={loading}>
+                <Button variant="outline" disabled={busy}>
                   {cancelLabel}
                 </Button>
               }
             />
             <Button
               variant={variant === 'destructive' ? 'destructive' : 'default'}
               data-variant={variant}
-              disabled={submittingRef.current || loading}
+              disabled={busy}
               onClick={async () => {
-                if (submittingRef.current || loading) return
-                submittingRef.current = true
+                if (busy) return
+                setSubmitting(true)
                 try {
                   await onConfirm()
                   onOpenChange(false)
                 } catch (err) {
                   log.warn('ConfirmDialog onConfirm threw', { title }, err)
                 } finally {
-                  submittingRef.current = false
+                  setSubmitting(false)
                 }
               }}
             >
-              {loading && (
+              {busy && (
                 <Loader2 className="mr-2 size-4 animate-spin" aria-hidden="true" />
               )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ui/confirm-dialog.tsx` around lines 92 - 95, The button's
disabled check uses submittingRef.current which won't trigger re-renders;
replace this with a stateful flag or require callers to pass loading. Change
submittingRef to a useState boolean (e.g., [submitting, setSubmitting]) and
update the onClick handler in the ConfirmDialog component so it checks
submitting || loading, sets setSubmitting(true) before the async work and
setSubmitting(false) in a finally block; update disabled to disabled={submitting
|| loading} and remove reliance on submittingRef.current (or alternatively
enforce callers always provide the loading prop and remove submittingRef usage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/security.md`:
- Around line 110-111: The documentation's CSP snippet is incomplete: update the
CSP example in docs/security.md so it matches the deployed header in
web/security-headers.conf by including the style-src fallback alongside the
element/attribute directives; replace the current `style-src-elem 'self'
'nonce-$csp_nonce'; style-src-attr 'unsafe-inline'` fragment with the full
policy `style-src 'self' 'nonce-$csp_nonce' 'unsafe-inline'; style-src-elem
'self' 'nonce-$csp_nonce'; style-src-attr 'unsafe-inline'` so the docs and
deployed config are consistent.

In `@src/synthorg/api/controllers/analytics.py`:
- Around line 401-406: The code calls app_state.persistence.tasks.list_tasks()
which scans the entire task history and causes ACTIVE_AGENTS metrics to scale
with retained tasks; change this to a bounded query that only fetches current
IN_PROGRESS assignees or a dedicated aggregate/count API instead of listing
everything. Update the call site that feeds _resolve_agent_counts (the block
using all_tasks and active_count) to use a filtered method such as
tasks.list_tasks(status=TaskStatus.IN_PROGRESS) or a new
tasks.count_active_assignees()/tasks.aggregate_active_assignees() that returns
the current set/count of in-progress assignees, then pass that result into
_resolve_agent_counts (or refactor _resolve_agent_counts to accept the
aggregate) so the work is bounded and does not scan full history.
- Around line 273-277: The debug branches inside _resolve_agent_counts()
incorrectly emit the module-level ANALYTICS_OVERVIEW_QUERIED event even when
called from /analytics/trends; replace those with a helper-scoped event constant
(e.g., declare a local variable like event_name at top of _resolve_agent_counts
or pass an event param) and use that local event when calling logger.debug in
both fallback branches (the occurrences currently using
ANALYTICS_OVERVIEW_QUERIED around the two debug blocks at the same function,
including the similar block at lines ~295-299). Ensure the helper-scoped
constant follows the guideline to use event name constants from
synthorg.observability.events (or map the caller to the correct event) so trends
requests are logged with the trends event and overview requests with the
overview event.
- Around line 278-290: In resolve_agent_counts, don't return zero for
active_agents when the agent registry query fails; first compute the busy set
from all_tasks (e.g., derive busy_count from app_state.all_tasks or the function
that builds the busy set) before calling app_state.agent_registry.list_active(),
and on Exception (but after re-raising MemoryError/RecursionError) log the error
as you do and return busy_count, config_agent_count instead of 0,
config_agent_count so in-progress work is still reflected; keep the successful
path unchanged (combine employed with busy set as before) and only use the
registry result to adjust idle/employed numbers.

In `@src/synthorg/engine/agent_engine.py`:
- Around line 1100-1101: The code in _prepare_context() logs
PROMPT_PERSONALITY_TRIMMED then awaits
self._maybe_notify_personality_trim(trim_payload), which blocks the prompt-build
hot path and adds latency; instead fire-and-forget the notifier by scheduling it
with asyncio.create_task (or asyncio.ensure_future) and attach a done-callback
that logs exceptions so the notification runs in background and does not delay
the model call; apply the same change for the other occurrence around the
logger.info + await pair referenced in the comment (the second occurrence).

In `@web/CLAUDE.md`:
- Line 5: Update the docs to match the real implementation: change the
MotionConfig example to pass the actual nonce value instead of a boolean
shorthand. Specifically, in the documentation text that shows App.tsx wrapping
the app with <CSPProvider nonce={getCspNonce()}> and <MotionConfig nonce>,
replace the latter with <MotionConfig nonce={nonce}> (or the equivalent
expression used in App.tsx) so it mirrors how getCspNonce() is passed through;
reference MotionConfig, CSPProvider, App.tsx and getCspNonce() to locate the
example to edit.

In `@web/package.json`:
- Line 47: The dependency "cmdk-base" is pinned to "1.0.0" which is inconsistent
with other caret ranges; update the package.json dependency entry for cmdk-base
to use a caret range "^1.0.0" instead of "1.0.0" and then reinstall
(npm/yarn/pnpm) to update the lockfile so the change is reflected across the
project.

In `@web/security-headers.conf`:
- Line 27: The Content-Security-Policy header declared in the add_header
directive (the line starting with add_header Content-Security-Policy) is missing
the frame-ancestors directive; update that CSP string to include frame-ancestors
'none' so the CSP enforces the anti-framing/clickjacking policy alongside
X-Frame-Options (i.e., append "frame-ancestors 'none';" into the existing CSP
value).

In `@web/src/__tests__/pages/org-edit/DepartmentCreateDialog.test.tsx`:
- Around line 33-37: The test currently relies on the disabled button's title
attribute to surface the "#1081" reason; update the assertion to verify visible,
accessible helper text or a tooltip trigger instead: after calling
renderDialog() and locating the Create Department button (createButton via
getByRole), find the visible explanatory text or the tooltip trigger element
(e.g., query/getByText or getByRole('button'|'link') for the tooltip opener, or
follow aria-describedby from createButton) and assert that it contains "1081"
rather than checking createButton.getAttribute('title'). Ensure you still assert
the button is disabled via expect(createButton).toBeDisabled().

In `@web/src/components/layout/StatusBar.tsx`:
- Around line 37-57: The current resolveCombinedStatus logic lets healthStatus
=== 'unknown' short-circuit outage signals; reorder checks in
resolveCombinedStatus so wsReconnectExhausted and !wsConnected are evaluated
before the 'unknown' branch (i.e., check wsReconnectExhausted then !wsConnected,
then healthStatus === 'unknown'), and apply the same reorder to the
duplicate/related function at the other occurrence (lines 93-101) so a WebSocket
reconnect exhaustion or disconnected state produces an offline/reconnecting pill
even when healthStatus is still 'unknown'.

In `@web/src/hooks/useLiveEdgeActivity.ts`:
- Around line 33-37: The subscription callback in useMessagesStore.subscribe
rebuilds prevIds (the Set of prevState.messages.map(m => m.id)) on every update
which can be costly for large message arrays; change the approach in the
callback used in useLiveEdgeActivity.ts to avoid recreating the full Set each
time—either maintain a lastSeenIdRef (store the most recent message id) and
compute newMessages by comparing incoming messages against that ref, or keep an
incremental structure (e.g., a persistent Set stored outside the callback) that
you update when newMessages are found; modify the logic referencing
prevState.messages, prevIds, and newMessages to use lastSeenIdRef or the
incremental Set and update that reference when new messages are processed.

In `@web/src/hooks/useOrgChartData.ts`:
- Around line 146-163: The collapse pruning currently runs unconditionally and
removes agent nodes/edges even when viewMode === 'force'; change the condition
so the pruning of tree.nodes/tree.edges only runs for hierarchy views (e.g.,
when viewMode !== 'force'). Specifically update the block that uses
collapsedDeptIds to also check viewMode (or move it inside the existing viewMode
!== 'force' branch) so that only the hierarchy layout filters out child agents
of collapsed departments; keep injecting isCollapsed into department node data
where UI needs it but do not remove agent nodes/edges when in 'force' mode.

In `@web/src/pages/agents/AgentFilters.tsx`:
- Around line 69-82: Replace the raw HTML <select> in AgentFilters with the
project's SelectField component to enforce consistent styling and accessibility:
render SelectField with value={departmentFilter ?? ''}, onChange that extracts
e.target.value (cast to DepartmentName) and uses setDepartmentFilter(v &&
validDepartmentNames.has(v) ? v : null), pass the options mapped from
departmentOptions (label/value) and include a placeholder option "All
departments" or placeholder prop and aria-label="Filter by department"; apply
the same substitution and value/onChange/validation pattern to the other select
instances (the other department/team filter selects referenced in the comment)
so they all use SelectField and preserve the existing validDepartmentNames
check.

In `@web/src/pages/org-edit/AgentCreateDialog.tsx`:
- Around line 165-177: The primary action Button in AgentCreateDialog is
permanently disabled and only communicates the reason via the title attribute
(ORG_EDIT_COMING_SOON_TOOLTIP), which is invisible to keyboard users; update
AgentCreateDialog to surface the "coming soon" message visibly and prevent users
filling the form by either (A) rendering a short-circuit notice/Alert with the
ORG_EDIT_COMING_SOON_TOOLTIP text and not rendering the form when the feature
flag is off, or (B) keep the Button disabled but add visible helper copy (a
Text/Caption adjacent to the Button) that shows ORG_EDIT_COMING_SOON_TOOLTIP;
ensure handleSubmit cannot run (or the form isn't rendered) when the feature is
disabled so users don’t encounter a dead end.

In `@web/src/pages/org-edit/GeneralTab.tsx`:
- Around line 163-168: The Button in GeneralTab (the Button with
onClick={handleSave} and title={ORG_EDIT_COMING_SOON_TOOLTIP}) is disabled so
its title will never be exposed; move the explanatory text out of the disabled
control and into an accessible element: either render the
ORG_EDIT_COMING_SOON_TOOLTIP as visible helper text (e.g., a <p> or <span>
beneath or next to the Button) with an aria-describedby on the disabled Button,
or wrap the Button in a non-disabled Tooltip/trigger component that shows the
tooltip on hover/focus while leaving the inner Button disabled; ensure keyboard
users can focus the trigger or read the helper text (use aria-describedby with
the helper element if you choose visible text).
- Around line 141-148: The SelectField currently masks unknown backend values by
coercing form.communication_pattern to 'hybrid'; instead, preserve and surface
the raw value so enum drift is visible. Update the SelectField usage
(referencing SelectField, COMMUNICATION_PATTERN_OPTIONS, VALID_COMM_PATTERNS,
and form.communication_pattern) to pass the raw form.communication_pattern as
the value and, if it is not in VALID_COMM_PATTERNS, add or render an explicit
"Unsupported: <value>" option in the options list so the UI displays the actual
stored value rather than silently mapping it to 'hybrid'.

In `@web/src/pages/org-edit/YamlEditorPanel.tsx`:
- Around line 73-81: The YAML editor currently disables the Save button but
still allows edits and marks the form dirty; update YamlEditorPanel to operate
in a true read-only mode when org edit is gated: add a readOnly prop/state (or
derive from the same flag used to disable the Save
button/ORG_EDIT_COMING_SOON_TOOLTIP) and 1) pass it into the text area/editor
component used in YamlEditorPanel so the editor is non-editable (remove or
short-circuit the onChange handler when readOnly), 2) prevent toggling the dirty
state (stop calling setDirty or ignore changes) while readOnly is true, and 3)
suppress any unsaved-changes UI affordance (hide or disable uses of dirty) so
users cannot make or be prompted about unsavable edits; ensure handleSave and
saving remain unchanged but unreachable when readOnly.

In `@web/src/pages/org/AgentNode.tsx`:
- Around line 59-66: The badge in AgentNode.tsx uses a hardcoded utility class
text-[9px] which bypasses the design token scale; replace that token with the
tokenized size (use text-micro) in the span that renders the "Lead" badge (the
element with aria-label="Department lead") so the badge follows the dashboard
typography tokens and removes the hardcoded font-size.

In `@web/src/pages/org/build-org-tree.ts`:
- Around line 214-229: The utilization calculation in buildDeptData is using
health.department_cost_7d and deptBudgetCap (monthly) which mismatches windows
and yields null when companyMonthlyBudget is absent; instead, use the
backend-provided health.utilization_percent when present, otherwise convert
deptBudgetCap to a 7-day equivalent (deptBudgetCap * 7/30 or similar) before
computing utilization from health.department_cost_7d; update the logic around
deptBudgetCap, cost7d and utilizationPercent in buildDeptData to first prefer
health.utilization_percent, then fall back to computing Math.min(100,
Math.round((cost7d / weeklyCap) * 100)) only when both cost7d and a
weekly-equivalent cap exist.

In `@web/src/pages/org/CeoNode.tsx`:
- Around line 26-29: The CEO card's class list in CeoNode.tsx currently uses raw
utility shadows ('shadow-md shadow-accent/20' and 'hover:shadow-lg
hover:shadow-accent/30'); replace those with the dashboard shadow tokens/CSS
vars so the node uses the tokenized system (e.g., use the project's shadow token
classes or CSS variables such as var(--so-shadow-card) and
var(--so-shadow-card-hover) instead). Update the class string in the CeoNode
component to remove the bespoke shadow utilities and reference the existing
shadow token classes/vars used elsewhere in the dashboard.

In `@web/src/pages/org/DepartmentGroupNode.tsx`:
- Line 82: The aria-label on DepartmentGroupNode.tsx currently always uses
"agents" even when agentCount === 1; update the aria-label construction (the JSX
attribute aria-label in DepartmentGroupNode, using variables displayName and
agentCount) to pluralize correctly by using a conditional that renders "1 agent"
when agentCount === 1, "{N} agents" when agentCount > 1, and ", empty" when
agentCount === 0—keep the same descriptive wording but vary "agent"/"agents"
based on agentCount.
- Around line 174-194: DepartmentGroupNode is recreating per-agent status dots
inline (using STATUS_DOT_STYLES and rendering span for each dot in the
visibleDots map); replace that implementation with the shared StatusBadge
component (or add a compact/dotOnly prop to StatusBadge) and pass the agent's
runtimeStatus and agentId/label so styling and semantics are centralized.
Specifically, in the visibleDots.map block inside DepartmentGroupNode, stop
rendering raw spans and instead render <StatusBadge ...> (or extend StatusBadge
to accept mode="dot"/compact/dotOnly and className overrides) with appropriate
props for status, aria-label/title using agentId and
STATUS_DOT_STYLES[status].label, and keep the +{hiddenDotCount} indicator as-is.
- Around line 207-218: Remove the global pointer-events-none on the parent div
so the chip's title tooltip can be hovered; instead make the chip itself visibly
non-actionable and non-focusable by adding attributes/classes on the span with
title "Add agent -- coming soon (`#1081`)" (e.g., add aria-disabled="true",
tabIndex={-1}, and a visual cue like cursor-not-allowed or opacity) and ensure
you apply the same change for the other occurrence (lines 230-239) so the
tooltip is reachable while the chip remains non-interactive.

In `@web/src/pages/org/layout.ts`:
- Around line 109-121: The fast-path that runs when agentLeafNodes.length === 0
applies EMPTY_GROUP_MIN_WIDTH/EMPTY_GROUP_HEIGHT to every node, which wrongly
forces owner nodes into the empty-department size; update the nodes.map block
(the map over nodes returned when agentLeafNodes is empty) to detect owner nodes
(n.type === 'owner') and skip applying EMPTY_GROUP_MIN_WIDTH/EMPTY_GROUP_HEIGHT
and the forced style/width/height for those nodes—only apply the empty-group
positioning/sizing to non-owner nodes so owner nodes keep their original
size/style/width/height (but you may still set a position for layout if needed).

In `@web/src/pages/org/OrgChartSearchOverlay.tsx`:
- Around line 32-45: When handling focus in the useEffect that watches open,
capture the previously focused element before you call requestAnimationFrame and
focus the input (e.g., store document.activeElement in a prevFocused variable or
ref), then on the cleanup path (when cancelling the animation frame or when the
overlay closes) restore focus to that saved element if it still exists and is
focusable; update the effect that references inputRef, requestAnimationFrame,
and cancelAnimationFrame to save the previous focus before focusing and to
re-focus it in the returned cleanup callback.

In `@web/src/pages/OrgChartPage.tsx`:
- Line 42: VALID_NODE_TYPES currently includes 'owner' but
ContextMenuState['nodeType'] and NodeContextMenu don't support it; remove
'owner' from the VALID_NODE_TYPES set (const VALID_NODE_TYPES = new
Set(['agent', 'ceo', 'department'])) so runtime values never produce an
unsupported nodeType, or alternatively update ContextMenuState and
NodeContextMenu to accept 'owner' everywhere if you intend to support it; do not
rely on the existing type cast that hides the mismatch.
- Around line 223-225: Search is being built from the already-filtered
nodes/edges produced by useOrgChartData(viewMode, collapsedDepts), so agents
inside collapsed departments are omitted; change the search indexing to use the
full tree instead of the filtered nodes or to auto-expand matching departments
before filtering. Concretely, update useOrgChartData (or export a new selector
like getAllNodes/getFullTree) so callers can access the complete node list
(e.g., fullNodes/fullEdges) and use that for building the matcher/index, or add
logic in the search handler to expand departments that contain matches prior to
applying collapsedDepts filtering; apply the same change in the other search
usages mentioned (the block referenced around 553-571).

In `@web/src/pages/tasks/TaskCreateDialog.tsx`:
- Around line 318-324: The current clone in controlWithAria replaces any
existing aria-describedby on children with errorId which drops hint/help ids;
change the cloneElement call to read the existing aria-describedby from
children.props['aria-describedby'], append errorId (space-separated) if present,
and pass that combined string as the 'aria-describedby' prop (still setting
'aria-invalid': true) so existing assistive text is preserved; update
controlWithAria to build combinedDesc = existing ? `${existing} ${errorId}` :
errorId before cloning.
- Around line 299-341: This file adds a page-local FormField plus
INPUT_CLASSES/TEXTAREA_CLASSES duplicating shared field logic; remove the
FormField component and the INPUT_CLASSES/TEXTAREA_CLASSES constants and replace
their usages with the shared InputField and SelectField components (use
InputField's multiline/textarea mode for multi-line inputs and pass label,
required, error/hint props; use SelectField for dropdowns with placeholder/error
props). Ensure you stop cloning children for ARIA—use the shared components'
built-in aria-describedby/aria-invalid behavior and map any className overrides
into the shared components' className or size props so styling remains
consistent.

In `@web/src/pages/workflows/WorkflowCard.tsx`:
- Around line 13-14: Add local loading state and wire it into ConfirmDialog:
create a state like isDeleting via useState, implement a handleDelete function
that sets isDeleting true, awaits the async onDelete(id) call, and finally sets
isDeleting false, and pass loading={isDeleting} plus onConfirm={() =>
handleDelete(workflow.id)} to ConfirmDialog (keep using confirmDelete and
setConfirmDelete for open state). Ensure the updated onDelete invocation awaits
the promise so the spinner shows during the async delete.

In `@web/src/pages/workflows/WorkflowTableView.tsx`:
- Around line 81-120: The Menu.Trigger currently passes a raw JSX element which
prevents Base UI from attaching handlers/refs; update the Menu.Trigger render
prop to be a render function that accepts props and spreads them onto the button
(keep type="button", className, aria-label={`Actions for ${w.name}`} and the
MoreHorizontal icon); specifically change the Menu.Trigger usage so the render
callback forwards the incoming props to the button element to ensure proper
event/ref/state wiring.

In `@web/src/styles/global.css`:
- Around line 163-180: The CSS currently applies transform/transition to the
positioned container (.react-flow__node) causing React Flow's position updates
to animate; move the scale and transition rules off .react-flow__node and onto
its child wrapper (.react-flow__node > div) instead: remove transform and
transition from .react-flow__node (including the transition override in the
reduced-motion media query) and add the scale (from .react-flow__node.dragging)
and the transition to .react-flow__node > div so the visual scale and easing
affect only the content wrapper while the positioned container remains
unanimated.

---

Outside diff comments:
In `@docs/design/page-structure.md`:
- Around line 250-337: Update the remaining route references and descriptive
text that still use `/agents/:agentName` to `/agents/:agentId` across the
document (specifically replace occurrences in the URL Routing Map and any inline
examples or prose such as the Agent Detail Page trigger and route list),
ensuring the Agent Detail Page heading and the routes table entry (previously
`/agents/:agentName`) and any mentions like "agentName" are changed to "agentId"
so all examples and links consistently reflect the current routing.

In `@src/synthorg/engine/agent_engine.py`:
- Around line 1047-1076: The code currently reads both ENGINE settings inside
one try/except so a failure fetching personality_max_tokens_override can cause
trimming_enabled to be reset; change the logic in agent_engine.py to fetch the
two settings independently: call
self._config_resolver.get_bool("engine","personality_trimming_enabled") inside
its own try/except and update trimming_enabled only on success, then call
get_int("engine","personality_max_tokens_override") inside a separate try/except
and update tokens_override only on success; keep MemoryError/RecursionError
re-raised and preserve the existing warning logging behavior (logger.warning
with EXECUTION_ENGINE_ERROR) when either lookup fails so a partial failure does
not overwrite trimming_enabled resolved by the other lookup.

In `@web/src/hooks/useOrgChartData.ts`:
- Around line 101-114: The effect may call polling.start() after the component
has unmounted causing leaked intervals; modify the useEffect around
useCompanyStore.getState().fetchCompanyData() and its downstream
fetchDepartmentHealths() so they check an "isMounted" flag (or an
AbortController) before calling polling.start(), and set that flag to false in
the cleanup (which still calls polling.stop()); specifically, in the promise
.then and .catch handlers for fetchCompanyData() and the .catch for
fetchDepartmentHealths(), guard polling.start() with if (isMounted) {
polling.start() } (or abort the chain) to prevent starting polling after
unmount.

In `@web/src/lib/motion.ts`:
- Around line 72-79: tweenSlow.duration is hardcoded then duplicated as
TRANSITION_SLOW_MS, risking drift; introduce a single shared seconds constant
(e.g., SLOW_SEC or SLOW_SECONDS) and derive both tweenSlow.duration (in seconds)
and TRANSITION_SLOW_MS (milliseconds via multiplication by 1000) from that
constant so updates only happen in one place; update references to use the new
constant and keep symbol names tweenSlow and TRANSITION_SLOW_MS unchanged.

In `@web/src/pages/org-edit/AgentEditDrawer.tsx`:
- Around line 81-101: The handleSave and handleDelete callbacks are currently
unreachable because the UI button is disabled behind the coming-soon gate; add a
brief guard comment/TODO above both function definitions (handleSave and
handleDelete) stating these are intentionally inactive until issue `#1081` lands
and reference `#1081` so future readers know why they appear unused, mirroring the
explanatory comments in the test files.

In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx`:
- Around line 55-87: The Save/Delete handlers and the ConfirmDialog are
unreachable because the Save and Delete buttons are permanently disabled; add
clear TODO comments indicating these are intentionally disabled until backend
CRUD is implemented (reference the issue number `#1081`) instead of removing code:
place a "// TODO: re-enable when `#1081` ships — handlers kept for future CRUD
support" comment immediately above the handleSave and handleDelete useCallback
declarations and above the ConfirmDialog JSX block to make intent explicit and
avoid dead-code confusion; alternatively, if you prefer removal, delete the
handleSave, handleDelete functions and the ConfirmDialog JSX and any associated
state (setDeleting, setDeleteOpen) to eliminate dead code.

In `@web/src/pages/org-edit/DepartmentsTab.tsx`:
- Around line 211-226: Update the copy in DepartmentsTab.tsx so it doesn't
direct users to a disabled "Add Department" action: change the EmptyState
title/description and any under-allocation warning text that currently prompt
creation to instead suggest using "Add Team Pack" or editing existing
departments; update the Button (the one using onClick={() =>
setCreateOpen(true)} and ORG_EDIT_COMING_SOON_TOOLTIP) tooltip/text so it
reflects "Coming soon" rather than instructing users to create a department;
make the same copy adjustments for the other instances in this file that render
EmptyState/Button-like UI (the components using Building2, Plus, EmptyState, and
that setCreateOpen) so messaging consistently points to Add Team Pack / edit
flows until `#1081` is implemented.

---

Duplicate comments:
In `@web/src/components/layout/Sidebar.tsx`:
- Around line 371-378: Replace the inline screen-reader span in Sidebar.tsx with
the shared LiveRegion component: compute the same status message from
wsConnected and wsReconnectExhausted (the current ternary logic) and pass it as
the LiveRegion children/prop so announcements use the shared
debounce/accessibility behavior; reference the LiveRegion component from
web/src/components/ui/LiveRegion and remove the inline <span role="status"
aria-live="polite"> block that uses wsConnected and wsReconnectExhausted.

In `@web/src/components/ui/confirm-dialog.tsx`:
- Around line 92-95: The button's disabled check uses submittingRef.current
which won't trigger re-renders; replace this with a stateful flag or require
callers to pass loading. Change submittingRef to a useState boolean (e.g.,
[submitting, setSubmitting]) and update the onClick handler in the ConfirmDialog
component so it checks submitting || loading, sets setSubmitting(true) before
the async work and setSubmitting(false) in a finally block; update disabled to
disabled={submitting || loading} and remove reliance on submittingRef.current
(or alternatively enforce callers always provide the loading prop and remove
submittingRef usage).

In `@web/src/pages/org/HierarchyEdge.tsx`:
- Around line 71-76: The duration calculation overestimates routed edges by
adding BEND_OFFSET to the full Manhattan distance; instead compute the actual
rendered segment lengths and derive durSec from that. Replace the approxLength
logic in HierarchyEdge (currently using sx, sy, tx, ty, BEND_OFFSET) with a
computed segmentsLength: if the edge is routed (i.e., has a bend at bendX/bendY)
sum the two rendered segment lengths (|sx - bendX| + |sy - bendY| and |tx -
bendX| + |ty - bendY|), otherwise use the direct Manhattan length (|tx - sx| +
|ty - sy|); then use durSec = Math.max(MIN_PARTICLE_DUR_SEC, segmentsLength /
PARTICLE_PX_PER_SEC). Ensure you reference the same bend coordinates used when
rendering the path so particles match the visual path.
🪄 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: 43932c28-26e0-466d-9ce8-6545136a8c7f

📥 Commits

Reviewing files that changed from the base of the PR and between fa28743 and b2ea28b.

⛔ Files ignored due to path filters (1)
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (104)
  • CLAUDE.md
  • docs/architecture/tech-stack.md
  • docs/design/brand-and-ux.md
  • docs/design/engine.md
  • docs/design/page-structure.md
  • docs/design/ux-guidelines.md
  • docs/design/ux-research.md
  • docs/guides/deployment.md
  • docs/security.md
  • src/synthorg/api/app.py
  • src/synthorg/api/controllers/analytics.py
  • src/synthorg/api/ws_models.py
  • src/synthorg/engine/agent_engine.py
  • src/synthorg/observability/events/prompt.py
  • src/synthorg/settings/definitions/engine.py
  • tests/unit/api/test_personality_trim_notifier_factory.py
  • tests/unit/engine/test_personality_trim_notifier.py
  • tests/unit/settings/test_engine_settings.py
  • web/CLAUDE.md
  • web/components.json
  • web/index.html
  • web/nginx.conf
  • web/package.json
  • web/security-headers.conf
  • web/src/App.tsx
  • web/src/__tests__/App.test.tsx
  • web/src/__tests__/components/layout/StatusBar.test.tsx
  • web/src/__tests__/components/ui/confirm-dialog.test.tsx
  • web/src/__tests__/hooks/useGlobalNotifications.test.ts
  • web/src/__tests__/pages/AgentDetailPage.test.tsx
  • web/src/__tests__/pages/agents/AgentFilters.test.tsx
  • web/src/__tests__/pages/agents/AgentGridView.test.tsx
  • web/src/__tests__/pages/approvals/ApprovalDetailDrawer.test.tsx
  • web/src/__tests__/pages/org-edit/AgentCreateDialog.test.tsx
  • web/src/__tests__/pages/org-edit/AgentEditDrawer.test.tsx
  • web/src/__tests__/pages/org-edit/DepartmentCreateDialog.test.tsx
  • web/src/__tests__/pages/org-edit/DepartmentEditDrawer.test.tsx
  • web/src/__tests__/pages/org-edit/DepartmentsTab.test.tsx
  • web/src/__tests__/pages/org-edit/GeneralTab.test.tsx
  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
  • web/src/__tests__/pages/org/OrgChartPage.test.tsx
  • web/src/__tests__/pages/org/build-org-tree.test.ts
  • web/src/__tests__/stores/agents.test.ts
  • web/src/api/types.ts
  • web/src/components/layout/AppLayout.tsx
  • web/src/components/layout/Sidebar.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/components/ui/animated-presence.tsx
  • web/src/components/ui/button.tsx
  • web/src/components/ui/command-palette.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/components/ui/dialog.tsx
  • web/src/components/ui/health-popover.tsx
  • web/src/components/ui/slot.tsx
  • web/src/components/ui/theme-toggle.tsx
  • web/src/hooks/useCommandPalette.ts
  • web/src/hooks/useGlobalNotifications.ts
  • web/src/hooks/useLiveEdgeActivity.ts
  • web/src/hooks/useOrgChartData.ts
  • web/src/lib/csp.ts
  • web/src/lib/motion.ts
  • web/src/pages/AgentDetailPage.tsx
  • web/src/pages/OrgChartPage.tsx
  • web/src/pages/OrgEditPage.tsx
  • web/src/pages/agents/AgentFilters.tsx
  • web/src/pages/agents/AgentGridView.tsx
  • web/src/pages/budget/CostBreakdownChart.tsx
  • web/src/pages/budget/SpendBurnChart.tsx
  • web/src/pages/dashboard/BudgetBurnChart.tsx
  • web/src/pages/org-edit/AgentCreateDialog.tsx
  • web/src/pages/org-edit/AgentEditDrawer.tsx
  • web/src/pages/org-edit/AgentsTab.tsx
  • web/src/pages/org-edit/DepartmentCreateDialog.tsx
  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/org-edit/DepartmentsTab.tsx
  • web/src/pages/org-edit/GeneralTab.tsx
  • web/src/pages/org-edit/PackSelectionDialog.tsx
  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/pages/org-edit/coming-soon.ts
  • web/src/pages/org/AgentNode.tsx
  • web/src/pages/org/CeoNode.tsx
  • web/src/pages/org/DepartmentGroupNode.stories.tsx
  • web/src/pages/org/DepartmentGroupNode.tsx
  • web/src/pages/org/HierarchyEdge.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
  • web/src/pages/org/OrgChartToolbar.tsx
  • web/src/pages/org/OwnerNode.tsx
  • web/src/pages/org/build-org-tree.ts
  • web/src/pages/org/layout.ts
  • web/src/pages/projects/ProjectTeamSection.tsx
  • web/src/pages/providers/ModelPullDialog.tsx
  • web/src/pages/providers/ProviderFormModal.tsx
  • web/src/pages/settings/SettingField.tsx
  • web/src/pages/tasks/TaskCreateDialog.tsx
  • web/src/pages/workflow-editor/VersionDiffViewer.tsx
  • web/src/pages/workflows/WorkflowCard.tsx
  • web/src/pages/workflows/WorkflowTableView.tsx
  • web/src/router/index.tsx
  • web/src/router/routes.ts
  • web/src/stores/agents.ts
  • web/src/stores/org-chart-prefs.ts
  • web/src/styles/design-tokens.css
  • web/src/styles/global.css
  • web/vite.config.ts

Comment on lines +273 to +277
logger.debug(
ANALYTICS_OVERVIEW_QUERIED,
note="no agent registry -- all agents reported as idle",
config_agent_count=config_agent_count,
)
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

Use a helper-scoped event constant instead of ANALYTICS_OVERVIEW_QUERIED.

_resolve_agent_counts() is called from both /analytics/overview and /analytics/trends, but these debug branches always emit the overview event. A trends request that hits either fallback will now be logged as an overview query and skew observability.

As per coding guidelines "Use event name constants from domain-specific modules under synthorg.observability.events."

Also applies to: 295-299

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

In `@src/synthorg/api/controllers/analytics.py` around lines 273 - 277, The debug
branches inside _resolve_agent_counts() incorrectly emit the module-level
ANALYTICS_OVERVIEW_QUERIED event even when called from /analytics/trends;
replace those with a helper-scoped event constant (e.g., declare a local
variable like event_name at top of _resolve_agent_counts or pass an event param)
and use that local event when calling logger.debug in both fallback branches
(the occurrences currently using ANALYTICS_OVERVIEW_QUERIED around the two debug
blocks at the same function, including the similar block at lines ~295-299).
Ensure the helper-scoped constant follows the guideline to use event name
constants from synthorg.observability.events (or map the caller to the correct
event) so trends requests are logged with the trends event and overview requests
with the overview event.

Comment on lines +401 to +406
all_tasks = await app_state.persistence.tasks.list_tasks()
active_count, _ = await _resolve_agent_counts(
app_state,
0,
all_tasks=all_tasks,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid scanning the full task history for a flat-line snapshot.

list_tasks() makes metric=ACTIVE_AGENTS scale with total retained tasks, even though this branch only needs the current set of IN_PROGRESS assignees once and then repeats that single value across buckets. Please switch this to a filtered query or dedicated aggregate so the cost stays bounded as task history grows.

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

In `@src/synthorg/api/controllers/analytics.py` around lines 401 - 406, The code
calls app_state.persistence.tasks.list_tasks() which scans the entire task
history and causes ACTIVE_AGENTS metrics to scale with retained tasks; change
this to a bounded query that only fetches current IN_PROGRESS assignees or a
dedicated aggregate/count API instead of listing everything. Update the call
site that feeds _resolve_agent_counts (the block using all_tasks and
active_count) to use a filtered method such as
tasks.list_tasks(status=TaskStatus.IN_PROGRESS) or a new
tasks.count_active_assignees()/tasks.aggregate_active_assignees() that returns
the current set/count of in-progress assignees, then pass that result into
_resolve_agent_counts (or refactor _resolve_agent_counts to accept the
aggregate) so the work is bounded and does not scan full history.

Comment on lines +1100 to +1101
logger.info(PROMPT_PERSONALITY_TRIMMED, **trim_payload)
await self._maybe_notify_personality_trim(trim_payload)
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 the notifier off the prompt-build hot path.

_prepare_context() now waits for a best-effort notification before the first model call. The timeout bounds the failure mode, but every slow publish can still add up to 2 seconds of latency to a trimmed run. If this integration is non-critical, it should enqueue/fire-and-forget instead of awaiting external I/O here.

Also applies to: 1172-1174

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

In `@src/synthorg/engine/agent_engine.py` around lines 1100 - 1101, The code in
_prepare_context() logs PROMPT_PERSONALITY_TRIMMED then awaits
self._maybe_notify_personality_trim(trim_payload), which blocks the prompt-build
hot path and adds latency; instead fire-and-forget the notifier by scheduling it
with asyncio.create_task (or asyncio.ensure_future) and attach a done-callback
that logs exceptions so the notification runs in background and does not delay
the model call; apply the same change for the other occurrence around the
logger.info + await pair referenced in the comment (the second occurrence).

Comment on lines +299 to 341
const INPUT_CLASSES = cn('w-full h-8 rounded-md border border-border bg-surface px-2 text-body-sm text-foreground outline-none', FOCUS_RING)
const TEXTAREA_CLASSES = cn('w-full rounded-md border border-border bg-surface px-2 py-1.5 text-body-sm text-foreground outline-none resize-y', FOCUS_RING)

function FormField({ label, error, required, children }: { label: string; error?: string; required?: boolean; children: React.ReactNode }) {
// Accessibility:
// - The <label> wraps only the visible text and the form control so
// screen readers resolve label-to-input via implicit association
// without the error text leaking into the control's accessible name.
// - The error <p> is rendered as a sibling of the label (outside it)
// with a stable id, and the form control is cloned with an
// `aria-describedby` pointing at that id so AT announces the error
// as separate help text rather than as part of the label.
const errorId = useId()
// Inject aria-describedby / aria-invalid onto the wrapped form control
// when an error is present so AT announces the error as separate help
// text. cloneElement is the only way to do this for an arbitrary
// children prop without binding every call site to a specific input
// component; the wrapping `isValidElement` guard keeps the clone safe
// for the single-element case this FormField is actually used for.
const controlWithAria =
error && isValidElement<{ 'aria-describedby'?: string; 'aria-invalid'?: boolean }>(children)
? // eslint-disable-next-line @eslint-react/no-clone-element -- see comment above
cloneElement(children, {
'aria-describedby': errorId,
'aria-invalid': true,
})
: children
return (
<div>
<label className="mb-1 block text-[11px] font-semibold uppercase tracking-wider text-text-muted">
{label}{required && <span className="text-danger"> *</span>}
<div className="block">
<label className="block">
<span className="mb-1 block text-compact font-semibold uppercase tracking-wider text-text-muted">
{label}{required && <span className="text-danger"> *</span>}
</span>
{controlWithAria}
</label>
{children}
{error && <p className="mt-0.5 text-[10px] text-danger">{error}</p>}
{error && (
<p id={errorId} className="mt-0.5 text-micro text-danger">
{error}
</p>
)}
</div>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Please stop extending a page-local field stack here.

This FormField/INPUT_CLASSES path duplicates label, error, and ARIA behavior that the shared InputField and SelectField components are supposed to own, so every future form fix now has to be implemented twice. Migrate this dialog back to the shared field primitives instead of adding more page-specific infrastructure. As per coding guidelines "Use InputField component for labeled text input with error/hint display, optional multiline textarea mode" and "Use SelectField component for labeled select dropdown with error/hint and placeholder support".

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

In `@web/src/pages/tasks/TaskCreateDialog.tsx` around lines 299 - 341, This file
adds a page-local FormField plus INPUT_CLASSES/TEXTAREA_CLASSES duplicating
shared field logic; remove the FormField component and the
INPUT_CLASSES/TEXTAREA_CLASSES constants and replace their usages with the
shared InputField and SelectField components (use InputField's
multiline/textarea mode for multi-line inputs and pass label, required,
error/hint props; use SelectField for dropdowns with placeholder/error props).
Ensure you stop cloning children for ARIA—use the shared components' built-in
aria-describedby/aria-invalid behavior and map any className overrides into the
shared components' className or size props so styling remains consistent.

Comment on lines +318 to +324
const controlWithAria =
error && isValidElement<{ 'aria-describedby'?: string; 'aria-invalid'?: boolean }>(children)
? // eslint-disable-next-line @eslint-react/no-clone-element -- see comment above
cloneElement(children, {
'aria-describedby': errorId,
'aria-invalid': true,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Preserve existing aria-describedby when you attach the error id.

This overwrite drops any hint/help ids the control already had, so an error would hide the rest of the assistive text. Append errorId to the existing value instead of replacing it.

🔧 Suggested fix
-        cloneElement(children, {
-          'aria-describedby': errorId,
-          'aria-invalid': true,
-        })
+        cloneElement(children, {
+          'aria-describedby': [children.props['aria-describedby'], errorId].filter(Boolean).join(' '),
+          'aria-invalid': true,
+        })
📝 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 controlWithAria =
error && isValidElement<{ 'aria-describedby'?: string; 'aria-invalid'?: boolean }>(children)
? // eslint-disable-next-line @eslint-react/no-clone-element -- see comment above
cloneElement(children, {
'aria-describedby': errorId,
'aria-invalid': true,
})
const controlWithAria =
error && isValidElement<{ 'aria-describedby'?: string; 'aria-invalid'?: boolean }>(children)
? // eslint-disable-next-line `@eslint-react/no-clone-element` -- see comment above
cloneElement(children, {
'aria-describedby': [children.props['aria-describedby'], errorId].filter(Boolean).join(' '),
'aria-invalid': true,
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/tasks/TaskCreateDialog.tsx` around lines 318 - 324, The current
clone in controlWithAria replaces any existing aria-describedby on children with
errorId which drops hint/help ids; change the cloneElement call to read the
existing aria-describedby from children.props['aria-describedby'], append
errorId (space-separated) if present, and pass that combined string as the
'aria-describedby' prop (still setting 'aria-invalid': true) so existing
assistive text is preserved; update controlWithAria to build combinedDesc =
existing ? `${existing} ${errorId}` : errorId before cloning.

Comment on lines +13 to 14
onDelete: (id: string) => void | Promise<void>
onDuplicate: (id: string) => void
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

CONFIRM_DIALOG="$(fd -i 'confirm-dialog\.tsx$' web/src/components/ui | head -n1)"
test -n "$CONFIRM_DIALOG"

echo "=== $CONFIRM_DIALOG ==="
sed -n '1,260p' "$CONFIRM_DIALOG"

echo
echo "=== Async/loading-related logic ==="
rg -n -C3 'onConfirm|loading|disabled|await|Promise' "$CONFIRM_DIALOG"

Repository: Aureliolo/synthorg

Length of output: 6984


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the file location
FILE="$(fd -i 'workflowcard\.tsx$' web/src/pages/workflows | head -n1)"
test -n "$FILE"

echo "=== Lines 95-115 (ConfirmDialog usage context) ==="
sed -n '95,115p' "$FILE" | cat -n

echo
echo "=== Search for ConfirmDialog with context ==="
rg -n -B3 -A8 'ConfirmDialog' "$FILE" | head -60

Repository: Aureliolo/synthorg

Length of output: 1644


Pass loading prop to ConfirmDialog for consistent async feedback.

The shared ConfirmDialog component internally serializes async onConfirm calls with await and blocks dismissal during in-flight requests, so async deletes here are safe. However, the loading prop is missing (lines 100–108), which means the spinner won't render and parent state cannot sync external loading signals. Add a loading state to the component and wire it into ConfirmDialog:

const [isDeleting, setIsDeleting] = useState(false)

// Update onDelete signature if needed to report completion
const handleDelete = async (id: string) => {
  setIsDeleting(true)
  try {
    await onDelete(id)
  } finally {
    setIsDeleting(false)
  }
}

// Then:
<ConfirmDialog
  open={confirmDelete}
  onOpenChange={setConfirmDelete}
  onConfirm={() => handleDelete(workflow.id)}
  loading={isDeleting}
  // ... rest of props
/>

Per coding guidelines, use ConfirmDialog with loading state to provide clear async feedback.

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

In `@web/src/pages/workflows/WorkflowCard.tsx` around lines 13 - 14, Add local
loading state and wire it into ConfirmDialog: create a state like isDeleting via
useState, implement a handleDelete function that sets isDeleting true, awaits
the async onDelete(id) call, and finally sets isDeleting false, and pass
loading={isDeleting} plus onConfirm={() => handleDelete(workflow.id)} to
ConfirmDialog (keep using confirmDelete and setConfirmDelete for open state).
Ensure the updated onDelete invocation awaits the promise so the spinner shows
during the async delete.

Comment on lines +81 to +120
<Menu.Root>
<Menu.Trigger
render={
<button
type="button"
className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
aria-label={`Actions for ${w.name}`}
>
<Pencil className="size-3.5" />
Edit
</DropdownMenu.Item>
<DropdownMenu.Item
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground outline-none hover:bg-accent/10 focus:bg-accent/10"
onSelect={() => onDuplicate(w.id)}
>
<Copy className="size-3.5" />
Duplicate
</DropdownMenu.Item>
<DropdownMenu.Item
className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-danger outline-none hover:bg-danger/10 focus:bg-danger/10"
onSelect={() => setConfirmDeleteId(w.id)}
>
<Trash2 className="size-3.5" />
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
<MoreHorizontal className="size-4" />
</button>
}
/>
<Menu.Portal>
<Menu.Positioner align="end" sideOffset={4}>
<Menu.Popup className="z-50 min-w-36 rounded-lg border border-border bg-card py-1 shadow-[var(--so-shadow-card-hover)] transition-[opacity,translate,scale] duration-150 ease-out data-[closed]:opacity-0 data-[starting-style]:opacity-0 data-[ending-style]:opacity-0 data-[closed]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:scale-95">
<Menu.Item
className="flex w-full cursor-default items-center gap-2 px-3 py-1.5 text-sm text-foreground outline-none data-[highlighted]:bg-surface"
onClick={() => { void navigate(editorUrl) }}
>
<Pencil className="size-3.5" />
Edit
</Menu.Item>
<Menu.Item
className="flex w-full cursor-default items-center gap-2 px-3 py-1.5 text-sm text-foreground outline-none data-[highlighted]:bg-surface"
onClick={() => onDuplicate(w.id)}
>
<Copy className="size-3.5" />
Duplicate
</Menu.Item>
<Menu.Item
className="flex w-full cursor-default items-center gap-2 px-3 py-1.5 text-sm text-danger outline-none data-[highlighted]:bg-surface"
onClick={() => setConfirmDeleteId(w.id)}
>
<Trash2 className="size-3.5" />
Delete
</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
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

🌐 Web query:

What is the correct API for Menu.Trigger render prop and Menu.Positioner in @base-ui/react version 1.3?

💡 Result:

In @base-ui/react version 1.3.0, the correct API for Menu.Trigger and Menu.Positioner uses the render prop for customization. For Menu.Trigger (renders a by default): - Use render={} as a shorthand for render={(props, state) => <CustomComponent {...props} />}. - Or use the full render function: render={(props: HTMLProps, state: Menu.Trigger.State) => <CustomComponent {...props} />}. - State includes { open: boolean }. - Example: <Menu.Trigger render={}>Open menu</Menu.Trigger> For Menu.Positioner (renders a

by default): - Supports the same render prop: ReactElement | ((props: HTMLProps, state: Menu.Positioner.State) => ReactElement). - State includes { open: boolean, side: Side, align: Align, anchorHidden: boolean, nested: boolean, instant?: string }. - Additional positioning props: side, align, sideOffset, alignOffset, collisionBoundary, collisionPadding, arrowPadding, sticky. This render prop pattern is the standard composition API across Base UI components, allowing replacement of the default element or composition with custom components. The custom component must forward ref and spread props. Sources confirm v1.3.0 exists (released Mar 12, 2026) with Menu enhancements like content transitions, and no breaking changes to this API from v1.1.0 types.

Citations:


Fix Menu.Trigger render prop to properly forward props.

The render prop for Menu.Trigger should use a render function that forwards props to the button element, not a raw JSX element. Base UI's render prop pattern requires components to spread props for proper integration (event handlers, refs, state attributes).

Change from:

<Menu.Trigger
  render={
    <button
      type="button"
      className="..."
      aria-label={`Actions for ${w.name}`}
    >
      <MoreHorizontal className="size-4" />
    </button>
  }
/>

To:

<Menu.Trigger
  render={(props) => (
    <button
      {...props}
      type="button"
      className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
      aria-label={`Actions for ${w.name}`}
    >
      <MoreHorizontal className="size-4" />
    </button>
  )}
/>

The rest of the Menu structure and design tokens are correct. Menu.Positioner props (align="end", sideOffset={4}) align with v1.3.0 API.

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

In `@web/src/pages/workflows/WorkflowTableView.tsx` around lines 81 - 120, The
Menu.Trigger currently passes a raw JSX element which prevents Base UI from
attaching handlers/refs; update the Menu.Trigger render prop to be a render
function that accepts props and spreads them onto the button (keep
type="button", className, aria-label={`Actions for ${w.name}`} and the
MoreHorizontal icon); specifically change the Menu.Trigger usage so the render
callback forwards the incoming props to the button element to ensure proper
event/ref/state wiring.

Comment on lines +163 to +180
.react-flow__node.dragging {
opacity: var(--so-opacity-dragging, 0.6);
transform: scale(1.02);
z-index: 1000 !important;
}

.react-flow__node.dragging > div {
box-shadow: var(--so-shadow-card-hover);
}

.react-flow__node {
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

@media (prefers-reduced-motion: reduce) {
.react-flow__node {
transition: none;
}
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:

cat -n web/src/styles/global.css | sed -n '160,185p'

Repository: Aureliolo/synthorg

Length of output: 897


🌐 Web query:

React Flow node positioning transform behavior

💡 Result:

In React Flow (now @xyflow/react), node positioning and transform behavior works as follows: Node positions are defined in flow/canvas coordinates via the position: {x, y} property on each node object. These coordinates represent the location of the node's origin point on the infinite canvas. The nodeOrigin (default [0, 0]) determines which point of the node is placed at that position: [0,0] is top-left, [0.5, 0.5] is center, [1,1] is bottom-right. This is configurable per-node or globally via ReactFlow's nodeOrigin prop. The entire viewport (containing all nodes and edges) is transformed via a CSS transform on the .react-flowviewport element: transform: translate(tx px, ty px) scale(zoom). Here: - tx, ty are the translation (pan) values from the viewport state. - zoom (scale) is typically between minZoom (0.5) and maxZoom (2) by default. For rendering a node: 1. Node's internal position is adjusted by nodeOrigin: effective position = position - (node.width * originX, node.height * originY). 2. The node's DOM element is positioned absolutely at that effective flow position (in flow coordinates). 3. The viewport's CSS transform then translates and scales all child elements (nodes) to screen coordinates: screenX = (effectiveX * zoom) + tx, screenY = (effectiveY * zoom) + ty. Node dimensions (width/height) are measured automatically after render and used for layouting, collisions, etc., but do not directly affect positioning—control size via CSS on the node component. To convert between screen and flow coordinates (e.g., for placing nodes at mouse position): - Use screenToFlowPosition(screenPos) from useReactFlow: subtracts bounding rect, applies inverse transform (x' = (x - tx) / zoom, y' = (y - ty) / zoom), optionally snaps to grid. - Inverse: flowToScreenPosition(flowPos) or older project. When dragging nodes, positions update in flow coordinates (NodePositionChange events), independent of current viewport—dragged positions account for viewport via similar inverse transform. Sources confirm this via official docs on Node, ReactFlow props, NodeOrigin, and GitHub issues/discussions on coordinate projection (e.g., screenToFlowPosition implementation, viewport transform array [x,y,zoom]).

Citations:


Don't animate the React Flow positioning wrapper.

Line 174 transitions transform on .react-flow__node, which is the container that React Flow manipulates during positioning and interaction updates. Adding a transition here will cause those library-driven updates to ease in unexpectedly, and the scale effect on line 165 adds competing transform logic to the same element. Move the scale and transition to the child content wrapper (.react-flow__node > div) instead, keeping the positioned container free of animations.

♻️ Suggested change
 .react-flow__node.dragging {
   opacity: var(--so-opacity-dragging, 0.6);
-  transform: scale(1.02);
   z-index: 1000 !important;
 }
 
-.react-flow__node.dragging > div {
-  box-shadow: var(--so-shadow-card-hover);
-}
-
-.react-flow__node {
+.react-flow__node > div {
   transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
 }
+
+.react-flow__node.dragging > div {
+  transform: scale(1.02);
+  box-shadow: var(--so-shadow-card-hover);
+}
 
 `@media` (prefers-reduced-motion: reduce) {
-  .react-flow__node {
+  .react-flow__node > div {
     transition: none;
   }
 }
📝 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
.react-flow__node.dragging {
opacity: var(--so-opacity-dragging, 0.6);
transform: scale(1.02);
z-index: 1000 !important;
}
.react-flow__node.dragging > div {
box-shadow: var(--so-shadow-card-hover);
}
.react-flow__node {
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (prefers-reduced-motion: reduce) {
.react-flow__node {
transition: none;
}
.react-flow__node.dragging {
opacity: var(--so-opacity-dragging, 0.6);
z-index: 1000 !important;
}
.react-flow__node > div {
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.react-flow__node.dragging > div {
transform: scale(1.02);
box-shadow: var(--so-shadow-card-hover);
}
`@media` (prefers-reduced-motion: reduce) {
.react-flow__node > div {
transition: none;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/styles/global.css` around lines 163 - 180, The CSS currently applies
transform/transition to the positioned container (.react-flow__node) causing
React Flow's position updates to animate; move the scale and transition rules
off .react-flow__node and onto its child wrapper (.react-flow__node > div)
instead: remove transform and transition from .react-flow__node (including the
transition override in the reduced-motion media query) and add the scale (from
.react-flow__node.dragging) and the transition to .react-flow__node > div so the
visual scale and easing affect only the content wrapper while the positioned
container remains unanimated.

Aureliolo added 14 commits April 6, 2026 02:03
…once + personality trimming WS notification

Close #1070.

- Replace radix-ui + cmdk with @base-ui/react@1.3.0 + cmdk-base@1.0.0.
  14 component files migrated (button with local Slot helper; dialog,
  confirm-dialog, theme-toggle, command-palette wrappers; six page-level
  Dialog/AlertDialog files; OrgEditPage Tabs; two workflow Menu files).
  components.json switched from radix-nova to base-vega. 115 packages
  removed, 8 added.

- Activate end-to-end CSP nonce pipeline. App.tsx wraps <CSPProvider nonce>
  + <MotionConfig nonce>. nginx generates per-request nonces from
  $request_id and substitutes __CSP_NONCE__ in index.html via sub_filter.
  security-headers.conf replaces 'style-src unsafe-inline' with CSP Level 3
  split: style-src-elem 'self' 'nonce-$csp_nonce' + style-src-attr
  'unsafe-inline' (for Floating UI positioning, not a practical XSS vector).

- Fold in #1064: personality trimming WebSocket notification.
  - Add engine.personality_trimming_notify setting (BOOLEAN, default true).
  - Add WsEventType.PERSONALITY_TRIMMED enum member.
  - Add personality_trim_notifier callback param to AgentEngine, invoked
    best-effort from _prepare_context with the same payload as the
    PROMPT_PERSONALITY_TRIMMED log event.
  - Add frontend handler in agents store that dispatches a toast on
    personality.trimmed events.
  - New useGlobalNotifications hook mounted in AppLayout subscribes to the
    agents channel app-wide so the toast renders regardless of current page.
  - 5 new notifier tests + 4 new toast-handler tests.

- Rewrite docs/security.md: delete the Accepted Risk section entirely and
  replace with CSP Nonce Infrastructure (current-state only). Update
  web/CLAUDE.md, docs/design/{brand-and-ux,ux-research}.md,
  docs/architecture/tech-stack.md, root CLAUDE.md to reference Base UI /
  cmdk-base. Add Base UI Adoption Decisions table in web/CLAUDE.md
  documenting keep-custom decisions for Toast (Zustand integration),
  Drawer (Framer Motion + motion token enforcement), Meter (ProgressGauge
  already has proper a11y), SelectField (native mobile picker).

- Bundle size: vendor-ui 150 kB -> 230 kB (+80 kB / +28 kB gzip). Net +3%
  on total dist weight. The growth is Base UI's more comprehensive
  primitive surface (Floating UI, focus management, transition tracking);
  the tradeoff is first-class CSP nonce support and active upstream
  maintenance.

- Test results: 14213 backend tests (unit + integration) pass, 2413
  frontend tests pass, Storybook builds, lint + type-check clean on both
  sides. Zero Radix references in tracked files.
Implements all valid findings from the 10-agent pre-PR review swarm on PR
#1070. Details per area:

Backend / engine:
- agent_engine._maybe_notify_personality_trim: switch from payload.get() to
  direct subscript for guaranteed keys, expand log context with agent_name
  + trim_tier, add reason labels to distinguish setting-read vs. callback
  failure log sites.
- AgentEngine.__init__: add has_personality_trim_notifier to the debug log
  so misconfigured wiring is visible at startup.
- Rename PROMPT_PERSONALITY_TRIM_NOTIFY_FAILED to PROMPT_PERSONALITY_NOTIFY_FAILED
  to match the dot-hierarchy naming convention used by the rest of the
  prompt event constants (prompt.policy.validation_failed style).
- Update PersonalityTrimNotifier docstring to point at the new
  api.app.make_personality_trim_notifier factory.
- New api.app.make_personality_trim_notifier factory: returns a ready-to-wire
  async callback bound to the live ChannelsPlugin. External engine runners
  call this factory and pass the result into AgentEngine so personality
  trimming events reach the dashboard end-to-end.
- Tests: add test_notifier_fires_without_config_resolver (covers the
  config_resolver=None branch) and test_cancelled_error_propagates (asserts
  asyncio.CancelledError is never swallowed by the best-effort try/except).

Frontend:
- lib/csp.ts: rewrite the stale docstring to describe the active nonce flow;
  log.warn / log.error when the meta tag is missing or contains the
  un-substituted __CSP_NONCE__ placeholder so production CSP failures are
  visible instead of silent.
- stores/agents.ts: log.warn on malformed personality.trimmed payloads
  instead of silently falling back to a generic toast; length-bound
  agent_name to 64 chars as defence-in-depth against oversized values;
  log.warn on unknown agent.status_changed status values.
- hooks/useGlobalNotifications.ts: surface setupError and reconnectExhausted
  via warning / error toasts so a silent WebSocket death does not leave
  users wondering why notifications stopped. New test file covers bindings,
  dispatch, and error-toast paths.
- components/ui/slot.tsx: compose the Slot's own ref with any child ref via
  a composeRefs helper so neither is silently dropped; add a dev-only
  console.warn when asChild receives a non-element child; drop the
  incorrect mergeProps<'div'> generic; rewrite the docstring to reflect
  current state (only Button uses this helper, all other primitives use
  Base UI's native render prop).
- components/ui/command-palette.tsx: fix the entrance/exit animation -- in
  Tailwind v4, translate compiles to the dedicated CSS translate: property
  rather than transform: shorthand, so the transition list needs
  transition-[opacity,translate] instead of transition-[opacity,transform].
- components/ui/dialog.tsx: drop the no-op backdrop-filter from the
  backdrop's transition property list.
- Design tokens: replace p-6 with p-card on the 5 migrated Dialog /
  AlertDialog popups (confirm-dialog, AgentCreateDialog, DepartmentCreateDialog,
  PackSelectionDialog, TaskCreateDialog), replace p-4 with p-card on the
  theme-toggle popover popup, and replace gap-4 with gap-grid-gap on the
  3 grid-cols-2 blocks inside TaskCreateDialog -- matches the precedent set
  by ModelPullDialog and the density-aware spacing token rules.
- package.json: pin cmdk-base to exact 1.0.0 (not ^1.0.0) since it is a
  smaller community package without the security track record of major
  MUI deps.

Docs:
- guides/deployment.md: replace the stale style-src 'unsafe-inline' CSP
  string with the active CSP Level 3 split directives and link to
  security.md, so operators configuring reverse proxies copy the correct
  value.
- design/engine.md: document the new personality_trimming_notify setting,
  the personality.trimmed WebSocket event contract, the agents-channel
  dispatch, the dashboard toast rendering, and the wiring contract via
  make_personality_trim_notifier.
- design/ux-guidelines.md, design/page-structure.md: update the two
  remaining stale cmdk references to cmdk-base.
- web/CLAUDE.md: add a Base UI primitive recipe to the Creating New
  Components checklist (subpath imports, render prop, Portal / Backdrop /
  Popup / Positioner, data-attribute names for animations, Tailwind v4
  translate / scale caveat). Remove historical phrasing.

Verification: 14054 backend unit tests pass, 2419 frontend tests pass
(+6 new), ruff + mypy + eslint + tsc all clean.
Critical / infrastructure:
- nginx: disable gzip_static on index.html so sub_filter can substitute the
  CSP nonce placeholder (gzip_static serves precompressed .gz files verbatim
  and bypasses response body filters). Apply the same fix to the SPA fallback
  location and re-include the security-headers snippet + Cache-Control:
  no-cache there so deep-link routes carry the nonce + CSP.
- CSP: add style-src 'self' 'nonce-...' 'unsafe-inline' fallback alongside
  the Level 3 style-src-elem/style-src-attr split so browsers without CSP3
  support do not fall through to default-src and break Floating UI.
- nginx: scope per-request CSP emission to HTML responses only (drop the
  security-headers include from /assets/, add proxy_hide_header +
  pass-through on /api/ and /api/v1/ws so the backend's own CSP is not
  overlaid).
- nginx comment: stop overstating $request_id as 'cryptographically
  random'; describe the 128-bit pseudo-random nature and threat model.
- docs: drop remaining 'not Radix's Overlay + Content' migration note from
  web/CLAUDE.md; update docs/design/ux-research.md cmdk references to
  cmdk-base.

Backend (engine / api / tests):
- agent_engine: rename msg= kwarg to note= in the settings-read failure
  log (msg collides with structlog reserved key and could crash the
  fallback warning).
- agent_engine: _maybe_notify_personality_trim is now fail-open when the
  config resolver raises -- a transient settings failure must not silently
  disable notifications that the operator enabled. The warning log still
  fires with an explicit fail-open reason.
- Introduce PersonalityTrimPayload TypedDict; annotate the notifier alias,
  factory return, and call sites so mypy catches payload key drift.
- make_personality_trim_notifier: annotate return as PersonalityTrimNotifier,
  log failures with the full payload context under
  PROMPT_PERSONALITY_NOTIFY_FAILED, document that channels_plugin.publish
  is synchronous so the async signature does not mislead readers.
- Docstrings: clarify best-effort semantics, note MemoryError/RecursionError
  re-raise and CancelledError propagation, document the dual emission paths
  of PROMPT_PERSONALITY_NOTIFY_FAILED (setting read + notifier call).
- Tests: add test_personality_trim_notifier_factory (happy path, error
  swallow, reuse, BaseException propagation); parametrize the notifier
  matrix in test_personality_trim_notifier; add MemoryError/RecursionError
  propagation cases for both the notifier and the setting-read branch;
  replace brittle len() count assertion in test_engine_settings with a
  set-containment check.

Frontend (Base UI migration fixes):
- All 11 dialog/popover/menu files: transition-[opacity,transform,scale] ->
  transition-[opacity,translate,scale] (the 'transform' token is dead in
  Tailwind v4 which compiles translate-*/scale-* to dedicated CSS props).
- OrgChartPage: useMemo the useRegisterCommands array so the effect does
  not thrash command-palette subscribers on every render.
- OrgEditPage Tabs: add isTabValue type guard instead of 'as TabValue'
  casts; tighten handleTabChange parameter type.
- command-palette: surface action errors via a toast + always-on log in
  production (previously silent unless DEV); log localStorage read/write
  errors via the project logger.
- csp.ts: downgrade the '__CSP_NONCE__ placeholder present' log to debug
  in DEV so the Vite dev server does not spam false-positive errors on
  every page load; keep error-level log in production.
- slot.tsx: replace bare console.warn with createLogger('slot').
- useGlobalNotifications: reset dedupe refs when the WS successfully
  reconnects so flapping connections emit fresh toasts instead of silent
  forever after the first failure.
- agents store: log warnings on fetchAgents/fetchAgentDetail error paths
  so backend failures are correlatable from the frontend log stream.
- confirm-dialog: remove unnecessary e.preventDefault(); add log.warn on
  swallowed confirm errors; expose data-variant for testable styling.
- TaskCreateDialog: add the prevOpenRef reset-on-close pattern used by the
  other create dialogs; guard onOpenChange against closing during submit.
- Workflow delete dialogs: trust ConfirmDialog's own auto-close path
  instead of double-flipping state.
- ModelPullDialog: document the intentional two-phase close during pull.
- WorkflowTableView menu: unify styling with WorkflowCard (bg-card,
  cursor-default, data-[highlighted]:bg-surface).
- useCommandPalette: add explicit void return, readonly parameter, and
  document the memoization requirement.

Design tokens:
- AppLayout PageLoadingFallback: space-y-4/gap-4 -> space-y-section-gap /
  gap-grid-gap; main gutter p-6 -> p-card.
- DialogHeader: px-6 py-4 -> p-card (propagates to VersionDiffViewer).
- VersionDiffViewer summary + body: px-6 py-3/4 -> p-card.
- ProviderFormModal header + body: px-6 py-4/5 -> p-card.
- OrgEditPage tab panel gutter: pt-6 -> pt-section-gap.
- TaskCreateDialog: text-[13px]/[11px]/[10px] -> text-body-sm/compact/micro.

Frontend tests:
- useGlobalNotifications: brittle length check -> shape assertion; new
  dedupe-across-rerenders, ref-reset-on-reconnect, and unmount-cleanup
  tests; parametrize the setup-error / reconnect-exhausted toast cases.
- agents.test: new tests for .slice(0, 64) agent_name length bound, NaN/
  Infinity token rejection, and non-string agent_name fallback.
- confirm-dialog.test: assert data-variant instead of className substring.
- App.test: drop the weak getAllByText('SynthOrg') assertion.
- ApprovalDetailDrawer.test: per-test handler factory (resetAllMocks) so
  queued rejections do not leak; scope Approve/Reject confirm-button
  lookups to the alertdialog via within(); simplify focus-trap test to
  assert the application-level aria-modal contract instead of
  re-implementing Base UI's focus-trap internals.

Coverage:
- Pre-existing ruff/mypy clean on all changed Python modules; 14229 Python
  unit+integration tests pass (+16 new); 2425 web Vitest tests pass (+6
  new); ESLint zero warnings; TypeScript type-check clean; Storybook builds.
…latency

On CI with --detect-async-leaks + coverage enabled, the initial import of
AppLayout (which transitively pulls framer-motion, cmdk-base, Base UI
primitives, and every lazy page) takes 5-9s before the Suspense fallback
resolves. The previous 5000ms budget was exactly at the edge and flaked
intermittently as new transitively-lazy modules were added.

Raise the waitFor timeout to 8000ms and the outer test timeout to 15000ms
with headroom over observed CI timings. This is lazy module resolution,
not a time-based race -- no deterministic mock can short-circuit actual
Node import work, so a wider budget is the correct fix.
- agent_engine: bound the personality-trim notifier call with
  asyncio.timeout(2.0) so a slow or hung notifier cannot stall run().
  Timeout failures emit PROMPT_PERSONALITY_NOTIFY_FAILED with a distinct
  'notifier callback timed out (>2s)' reason. Docstring updated to list
  all three failure paths (setting read, timeout, callback raised).

- ModelPullDialog: close the modal after a cancel-during-pull by latching
  closeAfterCancelRef on cancel and dispatching onClose() from a useEffect
  once pullingModel clears. Uses a ref rather than state to avoid the
  @eslint-react/set-state-in-effect warning while still bridging the gap
  the earlier comment-only fix left open.

- web/CLAUDE.md: resolve the Select guidance contradiction -- the
  'Creating New Components' section no longer tells contributors to
  prefer Base UI Select. The authoritative list lives in the Adoption
  Decisions table below.

- dialog.tsx: DialogDescription's shared default was 'text-muted' (muted
  surface color) instead of 'text-muted-foreground' (readable muted
  text). Every consumer inheriting the default now gets the same
  contrast as ConfirmDialog.

- theme-toggle.tsx: replace hardcoded shadow-lg on the Popover Popup
  with the tokenized shadow-[var(--so-shadow-card-hover)], matching the
  rest of the dashboard.

- agents store personality.trimmed handler: suppress the toast entirely
  when every payload field is missing/invalid -- a 'An agent personality
  was trimmed' toast with no name or numbers provided no actionable
  information. The existing warn log retains the diagnostic signal.
  Corresponding test renamed from 'uses default label when agent_name is
  missing' to 'suppresses the toast when every payload field is missing'.

- useGlobalNotifications unmount test: the previous version flipped the
  mocked WS state after unmount but never re-rendered, so it could not
  actually verify teardown. Replaced with a baseline 'unmount does not
  throw and leaves the store untouched' assertion; deep cleanup coverage
  belongs to useWebSocket's own test suite (which owns the subscription).

Not fixed (rejected as invalid):
- TaskCreateDialog render-phase state reset: CodeRabbit flagged the
  if (!open && prevOpenRef.current) { setForm(...) } pattern as a React
  anti-pattern, but setting state during render based on a prop
  transition is an officially supported React idiom (see the React docs
  'Storing information from previous renders'). This same pattern lives
  in AgentCreateDialog, DepartmentCreateDialog, and PackSelectionDialog
  with passing tests + eslint. Changing it would diverge from three
  sibling dialogs for a non-issue.
- agent_engine: type PersonalityTrimPayload identifier fields (agent_id,
  agent_name, task_id) as NotBlankStr per CLAUDE.md identifier convention.
  Pydantic validators don't fire inside a TypedDict at runtime, but the
  alias documents intent and keeps the notifier contract consistent with
  the rest of the codebase.

- web/CLAUDE.md: resolve the Tooltip inconsistency -- the 'Creating New
  Components' and 'Base UI Adoption Decisions' sections no longer mention
  Tooltip as preferred Base UI, since the adoption table does not list it
  as adopted. Contributors are directed to reach for existing primitives
  first and add a row to the table if a real Tooltip requirement appears.

- stores/agents: null-guard WsEvent.payload before casting to
  Record<string, unknown>. The TypeScript 'as' cast is compile-time only
  -- a malformed broker sending null would throw on the next property
  access. The guard drops the event and logs the drop.

- stores/agents: sanitize attacker-controlled values in log.warn calls --
  'agent' (URL route param) in fetchAgentDetail, 'status' in the
  unknown-status branch of the agent.status_changed handler, and
  'event_type' in the new payload-null guard. Matches the
  'sanitizeForLog attacker-controlled fields' rule in web/CLAUDE.md.

- dialog.tsx: reuse the shared Button component inside DialogCloseButton
  via Base UI's render prop, instead of duplicating the icon-button
  styles. Keeps tokens, focus states, and hover semantics centralized.

- dialog.tsx: DialogDescription was text-muted (already fixed in prior
  commit, retained here).

- useGlobalNotifications.test: resolve the 'agents' binding by channel
  name via bindings.find(b => b.channel === 'agents') instead of by
  index, so adding unrelated subscriptions upstream cannot silently
  break the suite.
- api/app.py: the asyncio.timeout() guard on the notifier call was
  ineffective against a synchronous blocking publish because timeouts
  only cancel at await points. Wrap channels_plugin.publish() in
  asyncio.to_thread() so the timeout has a real cancel checkpoint and a
  pathological channels-plugin implementation cannot block the event
  loop. Docstring updated to explain the why.

- dialog.tsx DialogCloseButton: add explicit type="button" to the
  shared close control so a dialog containing a <form> cannot
  accidentally submit the form when the close icon is clicked.

- web/CLAUDE.md Adoption Decisions: resolve the 'via shadcn/ui' vs
  'used directly, no wrapper layer' contradiction. components.json is
  the shadcn style setting, but the adopted primitives are imported
  directly from @base-ui/react/* subpaths with no wrapper layer.
  Contributors are told to use the direct-import path.

- useGlobalNotifications.test: add a mirror of the setupError dedupe
  reset sequence for reconnectExhaustedRef so a flapping connection
  that exhausts reconnects twice emits two toasts (not one silent
  regression).

All 5 other items CodeRabbit re-posted in this round are duplicates of
findings already fixed in the prior commit (they were flagged at
line:null because CodeRabbit's re-scan matched against the diff span
rather than the resolved code).  The agents.ts line-246 null-cast
concern is resolved by the new null guard at line 232, which returns
before any cast on a null payload.
Valid items fixed:

- docs/security.md: correct the X-Frame-Options justification -- it
  prevents framing by a foreign origin, not same-page UI redress via an
  injected style attribute. Same-page redress is covered by different
  mitigations.

- api/app.py: clarify the best-effort docstring to distinguish ordinary
  Exception publish failures (swallowed) from MemoryError/RecursionError
  (re-raised) and asyncio.CancelledError (propagates as a BaseException).
  Matches the design page wording.

- agent_engine test matrix: replace the profile-default-dependent
  '(True, 0, True, True, 0)' no-trim row with an explicit
  '(False, 10, True, True, 0)' trimming-disabled row. Tightening the
  default personality budget in future work cannot silently flip this
  matrix row.

- agent_engine tests: add test_notifier_timeout_is_swallowed covering
  the new 2s TimeoutError branch. Patches asyncio.timeout down to 10ms
  and uses asyncio.Event().wait() for a cancellation-safe hang.

- useGlobalNotifications.test: convert the 'forwards personality.trimmed'
  test to assert delegation to the agents store (via a vi.spyOn on
  updateFromWsEvent) instead of the toast content. Toast shape lives in
  the agents store test suite; unrelated copy changes no longer cascade
  into this hook's tests.

- confirm-dialog: block Escape and backdrop dismissal while
  loading=true. Without this, callers that clear state on close (e.g.
  ApprovalDetailDrawer resetting its 'comment' state) drop the user's
  retry context mid-operation even though the component intends to stay
  open on failure.

- useGlobalNotifications.ts: wrap the setupError value in sanitizeForLog
  before embedding in structured log arguments. setupError originates
  from WebSocket transport errors which can surface messages derived
  from untrusted response bodies.

- DepartmentCreateDialog + PackSelectionDialog: replace hardcoded
  shadow-lg with the tokenized shadow-[var(--so-shadow-card-hover)],
  matching the other migrated dialogs in this PR.

- TaskCreateDialog FormField: bind label-to-input using the implicit
  label-association form (label element wraps the form control).
  Previously the label was a sibling with no htmlFor/id pair, so
  assistive tech could not associate it with any of the 9 form fields.

- WorkflowCard + WorkflowTableView: return the onDelete promise from
  the ConfirmDialog's onConfirm handler, and widen the onDelete prop
  type to 'void | Promise<void>' so ConfirmDialog can observe rejection
  and keep the dialog open on failure. The parent WorkflowsPage passes
  an async handleDelete that was previously not being awaited.

- stores/agents personality.trimmed handler: validate before_tokens /
  after_tokens as non-negative integers (not just finite numbers), so
  a malformed payload with negative or fractional values falls through
  to the generic fallback description instead of rendering nonsense.

- nginx.conf /api and /api/v1/ws locations: re-declare the non-CSP
  security headers (X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy, HSTS) alongside the CSP
  pass-through. Without this, nginx-generated error responses (502/504/
  timeout) on proxied paths were missing the full hardening set because
  defining any add_header in a location block disables server-level
  inheritance.

Rejected findings (with justification):

- web/package.json cmdk-base exact pin: intentional supply-chain posture
  for a single-maintainer package. Pin is deliberate, not a bug.

- fast-check refactor of the whole useGlobalNotifications / agents test
  suites: adopting property-based testing for these state machines is a
  larger architectural change that belongs in a follow-up PR. The
  current hand-picked cases cover the documented behavioural invariants.

- ModelPullDialog AlertDialog.Close render={<Button .../>} 'static
  element is wrong' finding: false positive. Base UI's render prop
  accepts ReactElement | render-function; the static-element form is
  the canonical pattern used in 10+ places across this PR, and all
  associated tests pass.
- docs/security.md: refine the $request_id claim to match nginx's
  conditional behavior. nginx uses CSPRNG (RAND_bytes) when built
  against OpenSSL and falls back to a seeded PRNG otherwise. Both
  provide 128-bit uniqueness, which is what the CSP nonce threat model
  requires; the new wording avoids overstating CSPRNG on non-OpenSSL
  builds.

- nginx.conf /assets/: remove the `always` flag from Cache-Control.
  With `always`, a transient 404 (e.g. during a bad deploy) gets
  pinned in the browser/CDN cache for a year with the immutable
  policy, requiring manual cache clearing. Without `always`, the
  header only applies to successful responses (which is what we want).
  X-Content-Type-Options keeps `always` because nosniff is safe to
  emit on error responses.

- useGlobalNotifications.test: add afterEach(vi.restoreAllMocks) so a
  failing test that bails before its inline mockRestore() call cannot
  leak a spy into subsequent tests.

- dialog.tsx, confirm-dialog.tsx, AgentCreateDialog.tsx,
  ModelPullDialog.tsx, command-palette.tsx, TaskCreateDialog.tsx:
  replace the remaining hardcoded `shadow-lg` on dialog/popup
  surfaces with the tokenized `shadow-[var(--so-shadow-card-hover)]`,
  completing the dialog-surface shadow migration across the PR.

- stores/agents personality.trimmed handler: reject contradictory
  payloads where after_tokens > before_tokens. Trimming must reduce
  token count, so a swapped payload falls through to the generic
  fallback description instead of rendering "trimmed: 120 → 600 tokens".

Rejected (false positives):

- Dialog.Close render prop "not documented": the `render` prop IS
  supported on Dialog.Close and AlertDialog.Close per the official
  Base UI docs and is used in 10+ places across this PR with passing
  tests. CoderRabbit's web query returned stale information.

- PackSelectionDialog render-phase state reset "move to useEffect":
  the `if (!open && prevOpenRef.current) { setState(...) }` pattern is
  officially supported by React (see "Storing information from
  previous renders" in the React docs). The same pattern is used in
  three sibling dialogs with passing tests + eslint.
… active-agent runtime semantics

Major web dashboard rebuild on top of the Radix-to-Base UI migration, plus one
backend semantics fix for the analytics overview.

## Org chart

- New synthetic Owner node sits above the root department and accepts multiple
  owners (array-shaped payload) for future multi-user support. Explicit
  w-[240px] / h-[90px] so layout centering matches rendered size.
- Restructured hierarchy: Executive is the root department with the CTO living
  inside it as a regular agent (not a standalone top-level node). All other
  departments hang below via a single cross-dept edge row.
- Dagre is now used for ordering only; a post-layout shift pass enforces an
  exact 48px gap between the owner row, the root department, and the non-root
  department row. Prior minlen approach was quantized into 50px rank jumps.
- Custom HierarchyEdge SVG L-path replaces React Flow's getSmoothStepPath,
  which had a Z-shape fanout limitation that silently ignored centerY on
  cross-x edges. L-path routes cleanly with a single vertical bend.
- Live particle flow: useLiveEdgeActivity subscribes to the messages store and
  pulses particles along edges that carry traffic in the last 3s. Three modes:
  Flow (always on), Live (traffic-driven, default), Off. Uses SVG animateMotion
  for uniform Manhattan-distance speed.
- Department group node: dashed empty-state border with inline "+ Add agent"
  CTA; status dots (active / idle / error / offline) with tooltip legend;
  budget utilization bar; LEAD badge on department leads. Header height is
  computed from live toggle state so agent cards never overlap chrome.
- Inline toolbar toggles replace the previous popover-based view menu: particle
  flow mode (segmented Flow / Live / Off), add-agent CTA, lead badges, budget
  bars, status dots, minimap. Status-dots toggle has a multi-line tooltip
  explaining the color legend.
- Search overlay (Ctrl+F) with match dimming, keyboard nav across matches,
  highlighted matches in the tree.
- Collapse / expand per department with chevron.
- Minimap: togglable, default off, custom node colors, no labels (unreadable
  at minimap scale).
- New persistence store useOrgChartPrefs (web/src/stores/org-chart-prefs.ts)
  with sensible defaults: particleFlowMode='live', add-agent + lead badge +
  budget bar visible, status dots + minimap hidden.

## Agent URLs and filters

- Agent detail routes switched from /agents/:agentName to /agents/:agentId.
  Names can contain arbitrary characters that broke backend lookup after URL
  encoding. AgentDetailPage resolves the agent from useCompanyStore by id
  (falling back to name for legacy config), and passes the resolved name to
  the data hook. All navigation sites updated: AgentGridView, OrgChartPage,
  ProjectTeamSection.
- Department filter on the agents page now pulls from
  useCompanyStore().config.departments instead of the hardcoded
  DEPARTMENT_NAME_VALUES enum. Users create their own departments via the
  setup wizard / packs; the filter needs to match what they actually have.
  Uses display_name when provided, falls back to formatLabel(name).

## Coming-soon gates

- New web/src/pages/org-edit/coming-soon.ts exposes a shared constant pointing
  at issue #1081 (missing backend CRUD endpoints). Nine mutation paths in the
  org-edit dialogs / drawers are gated behind this constant with a banner
  explaining the limitation, rather than surfacing raw 405 responses.

## Backend: active-agent semantics

- _resolve_agent_counts in analytics.py now computes "active" as agents
  currently executing a task (task status == IN_PROGRESS, assigned_to in the
  employed set), not "agents with employment status ACTIVE". The previous
  semantics conflated HR lifecycle with runtime state and produced the
  surprising "4 active / 0 idle / 0 tasks" display on the overview dashboard.
- Caller (_assemble_overview) now passes all_tasks into the helper so the
  computation shares the already-fetched task list.

## Misc

- New health-popover.tsx component (used by status bar).
- Tests updated for all structural changes (URL format, filter dependency on
  company store, build-org-tree structure with crossDeptKind markers).
…t, and Gemini

Rebase onto origin/main (resolve #1072 conflict in agent_engine.py).

Python backend:
- Fix trends active_agents always returning 0 (pass all_tasks)
- Update idle/active field descriptions for runtime-state semantics
- Add debug logs on _resolve_agent_counts fallback branches
- Use safe .get() in notifier failure path to prevent KeyError
- Rename note= to reason= for log consistency
- Narrow trim_tier to Literal[1, 2, 3]
- Extract _read_notify_enabled helper (50-line function limit)
- Remove redundant parentheses in logger kwarg

Frontend:
- Move unnonced <style> to global.css (CSP nonce compliance)
- Replace 9 hardcoded MiniMap colors with CSS variables
- Extract Framer Motion durations to @/lib/motion presets
- Add confirm-dialog double-click re-entry guard
- Fix health-popover race condition (latest-response-wins)
- Remove dead side/align props from HealthPopover
- Restore aria-describedby auto-wiring on health dialog
- Guard Slot Children.only() against invalid input
- Fall back to agentId URL param in AgentDetailPage
- Wrap useGlobalNotifications handler in try/catch
- Add debug log for unknown WS event types in agents store
- Replace raw button with shared Button in search overlay
- Fix approxLength double-counting BEND_OFFSET on straight edges
- Wire HierarchyEdge through React Flow generic (remove as cast)
- Fix owner-only orgs skipping empty-state layout
- Optimize O(E*N) edge filtering to O(E+N) with Set
- Narrow LayoutDirection to TB only (LR was unused)
- Fix design token violations (space-y, gap, transition durations)
- Fix OverBudget story budgetPercent (20 -> 120)
- Remove redundant aria-disabled from disabled buttons
- Remove onClick from permanently disabled Create Department button
- Reinstate agent-count assertion in DepartmentEditDrawer test
- Thread currency prop through BudgetBurnChart tooltip
- Update stale comments (smoothstep, dimensions, re-render)

Infrastructure:
- Add Strict-Transport-Security to /assets/ location block
- analytics: derive active agent count from tasks even without registry
- GeneralTab: preserve unknown communication_pattern values with indicator
- GeneralTab: add visible coming-soon description below disabled save button
- AgentCreateDialog: add visible coming-soon description below disabled create button
- security-headers: add frame-ancestors 'none' to CSP header
- OrgChartPage: search indexes full pre-collapse tree via allNodes
@Aureliolo Aureliolo force-pushed the feat/radix-to-base-ui-migration branch from b2ea28b to 1622523 Compare April 6, 2026 00:05
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 6, 2026 00:06 — 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: 16

Caution

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

⚠️ Outside diff range comments (1)
web/src/lib/motion.ts (1)

72-79: 🧹 Nitpick | 🔵 Trivial

Keep the slow timing single-sourced.

Lines 72-79 now encode the same duration twice (0.4 seconds and 400 ms). The next timing tweak can silently desync the motion preset from any timer-based caller.

♻️ Proposed refactor
+const TWEEN_SLOW_SECONDS = 0.4
+
 /** Slow tween: page transitions, large layout shifts. */
 export const tweenSlow: Transition = {
   type: "tween",
-  duration: 0.4,
+  duration: TWEEN_SLOW_SECONDS,
   ease: [0.4, 0, 0.2, 1],
 };
 
 /** Slow tween duration in milliseconds (matches tweenSlow.duration). */
-export const TRANSITION_SLOW_MS = 400;
+export const TRANSITION_SLOW_MS = TWEEN_SLOW_SECONDS * 1000;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/lib/motion.ts` around lines 72 - 79, The duration is duplicated
between tweenSlow.duration and TRANSITION_SLOW_MS causing potential drift; pick
one canonical source (either TRANSITION_SLOW_MS or tweenSlow.duration) and
derive the other from it (e.g., keep TRANSITION_SLOW_MS as the single source of
truth and set tweenSlow.duration = TRANSITION_SLOW_MS / 1000, or keep
tweenSlow.duration and compute TRANSITION_SLOW_MS =
Math.round(tweenSlow.duration * 1000)); update references to use the canonical
symbol (TRANSITION_SLOW_MS or tweenSlow) so future edits change only one value.
♻️ Duplicate comments (20)
src/synthorg/api/controllers/analytics.py (3)

296-307: ⚠️ Potential issue | 🟠 Major

Keep the active count when the registry lookup fails.

The exception fallback returns (0, config_agent_count) even when all_tasks already shows in-progress assignees. A transient registry error will therefore flatten both overview and ACTIVE_AGENTS trends to zero while work is still running.

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

In `@src/synthorg/api/controllers/analytics.py` around lines 296 - 307, When
app_state.agent_registry.list_active() fails the except block currently returns
(0, config_agent_count) which wipes ACTIVE_AGENTS despite in-progress tasks;
instead, catch the Exception and compute the active count from the existing
in-memory data (e.g., derive the active count from all_tasks/in-progress
assignees already computed earlier in resolve_agent_counts) and return
(derived_active_count, config_agent_count) so the first tuple value uses the
locally-known active agents rather than 0; modify the except handler around
app_state.agent_registry.list_active() (and references to employed) to log the
error but fall back to the computed local active count.

418-423: ⚠️ Potential issue | 🟠 Major

Don’t scan the full task history for a flat-line snapshot.

This branch only needs the current set of IN_PROGRESS assignees once, but list_tasks() scales with total retained task history. ACTIVE_AGENTS trends will get slower as history grows.

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

In `@src/synthorg/api/controllers/analytics.py` around lines 418 - 423, The code
is calling app_state.persistence.tasks.list_tasks() which returns the full task
history; instead, fetch only current IN_PROGRESS tasks so ACTIVE_AGENTS
computation scales properly—modify the call site that feeds
_resolve_agent_counts (keep _resolve_agent_counts signature) to pass a filtered
task list (e.g., use an existing list_active_tasks or call list_tasks with a
status/statuses filter like TaskStatus.IN_PROGRESS) so you only retrieve current
in-progress assignees rather than the entire history.

274-275: ⚠️ Potential issue | 🟡 Minor

Pass the caller’s analytics event into _resolve_agent_counts().

This helper is reused by /analytics/trends, but these fallback/debug branches still emit ANALYTICS_OVERVIEW_QUERIED. A trends request that hits any of them gets misclassified in observability.

Also applies to: 288-289, 312-313

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

In `@src/synthorg/api/controllers/analytics.py` around lines 274 - 275, The debug
branches are calling _resolve_agent_counts() without the caller’s analytics
event, causing logs to always emit ANALYTICS_OVERVIEW_QUERIED and misclassify
/analytics/trends; update each call site (the calls around lines emitting
ANALYTICS_OVERVIEW_QUERIED and similar at the other two spots) to pass the
current analytics event object (e.g., analytics_event or caller_event) into
_resolve_agent_counts(…) and then adjust _resolve_agent_counts to use that
passed-in event when emitting logs instead of hardcoding
ANALYTICS_OVERVIEW_QUERIED so the correct event name is logged for trends and
overview calls.
web/src/styles/global.css (1)

163-180: ⚠️ Potential issue | 🟠 Major

Keep React Flow’s positioned node wrapper free of transforms.

.react-flow__node is the element React Flow moves with its own transform. Applying both transform: scale(...) and transition: transform there makes drag/layout updates ease unexpectedly. Move the animation to the child content wrapper instead.

In React Flow / `@xyflow/react`, does `.react-flow__node` receive library-managed `transform` updates for positioning, and can adding custom CSS `transform` or `transition: transform` to that element interfere with drag or layout updates?
♻️ Suggested change
 .react-flow__node.dragging {
   opacity: var(--so-opacity-dragging, 0.6);
-  transform: scale(1.02);
   z-index: 1000 !important;
 }
 
+.react-flow__node > div {
+  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
 .react-flow__node.dragging > div {
+  transform: scale(1.02);
   box-shadow: var(--so-shadow-card-hover);
 }
 
-.react-flow__node {
-  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
 `@media` (prefers-reduced-motion: reduce) {
-  .react-flow__node {
+  .react-flow__node > div {
     transition: none;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/styles/global.css` around lines 163 - 180, The .react-flow__node
element is positioned by React Flow via its own transform so remove custom
transform and transition from that selector to avoid interfering with
library-managed positioning: delete transform: scale(...) from
.react-flow__node.dragging and remove transition: transform from
.react-flow__node; instead apply the scale transform and transition to the
node's content wrapper (the child div targeted by .react-flow__node.dragging >
div and .react-flow__node > div) so the visual hover/drag animation runs on the
inner element while the outer .react-flow__node remains transform-free; keep the
existing box-shadow on .react-flow__node.dragging > div.
web/src/pages/tasks/TaskCreateDialog.tsx (2)

318-324: ⚠️ Potential issue | 🟡 Minor

Append the error id instead of overwriting aria-describedby.

If a control already exposes hint text, this clone drops that id when an error appears. Merge the existing value with errorId so assistive tech keeps both descriptions.

🔧 Suggested change
   const controlWithAria =
     error && isValidElement<{ 'aria-describedby'?: string; 'aria-invalid'?: boolean }>(children)
       ? // eslint-disable-next-line `@eslint-react/no-clone-element` -- see comment above
         cloneElement(children, {
-          'aria-describedby': errorId,
+          'aria-describedby': [children.props['aria-describedby'], errorId]
+            .filter(Boolean)
+            .join(' '),
           'aria-invalid': true,
         })
       : children
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/tasks/TaskCreateDialog.tsx` around lines 318 - 324, The clone
currently overwrites any existing 'aria-describedby' when creating
controlWithAria; update the clone logic in TaskCreateDialog's controlWithAria
branch to read the existing aria-describedby from children (when isValidElement
and children.props['aria-describedby'] exists), merge it with errorId (append
space-separated and dedupe if present) and pass the merged string to
cloneElement along with 'aria-invalid': true, so existing hint IDs are preserved
and the errorId is added.

299-300: 🛠️ Refactor suggestion | 🟠 Major

Replace the local field stack with the shared form primitives.

FormField, INPUT_CLASSES, and TEXTAREA_CLASSES now duplicate label/error/ARIA behavior that InputField and SelectField are supposed to own, so every form fix has to be implemented twice. Please switch this dialog back to the shared field components instead of extending the page-local wrapper.

As per coding guidelines "Use InputField component for labeled text input with error/hint display, optional multiline textarea mode" and "Use SelectField component for labeled select dropdown with error/hint and placeholder support".

Also applies to: 303-339

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

In `@web/src/pages/tasks/TaskCreateDialog.tsx` around lines 299 - 300, The current
TaskCreateDialog uses a local FormField wrapper and duplicated
INPUT_CLASSES/TEXTAREA_CLASSES causing duplicated label/error/ARIA behavior;
replace these usages with the shared InputField and SelectField components:
remove/stop using FormField, INPUT_CLASSES and TEXTAREA_CLASSES and swap each
input usage to InputField (for single-line inputs) or InputField with
multiline/rows prop for textareas, and swap selects to SelectField, passing
through label, name, error/hint/placeholder and options, and wire up the same
form registration/control props (e.g., the existing name/field registration) so
ARIA, error display and hint behavior are delegated to InputField/SelectField.
web/src/pages/org/OrgChartSearchOverlay.tsx (1)

34-45: ⚠️ Potential issue | 🟠 Major

Restore focus when the search overlay closes.

Open moves focus into the input, but close just unmounts it. That leaves keyboard users with no predictable place to continue from.

🔧 Suggested change
   const inputRef = useRef<HTMLInputElement>(null)
+  const restoreFocusRef = useRef<HTMLElement | null>(null)
 
   useEffect(() => {
     if (open) {
+      restoreFocusRef.current =
+        document.activeElement instanceof HTMLElement ? document.activeElement : null
       // Defer focus to the next frame so the input is actually
       // mounted and visible before we try to focus it.
       const id = requestAnimationFrame(() => {
         inputRef.current?.focus()
         inputRef.current?.select()
       })
-      return () => cancelAnimationFrame(id)
+      return () => {
+        cancelAnimationFrame(id)
+        if (restoreFocusRef.current?.isConnected) {
+          restoreFocusRef.current.focus()
+        }
+      }
     }
     return undefined
   }, [open])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org/OrgChartSearchOverlay.tsx` around lines 34 - 45, The effect
that focuses the input when open=true doesn’t restore focus when the overlay
closes; update the logic in the useEffect that references open, inputRef,
requestAnimationFrame and cancelAnimationFrame to capture document.activeElement
(e.g., prevFocus) before moving focus, and on cleanup when open becomes false
(or in the returned cleanup) call prevFocus?.focus() (guarding for elements that
may be removed) so keyboard users regain their prior focus; ensure you still
cancel the animation frame id and clear the saved reference to avoid leaks.
web/src/pages/org-edit/YamlEditorPanel.tsx (1)

73-85: ⚠️ Potential issue | 🟠 Major

Make the YAML panel truly read-only while save is gated.

Line 76 permanently disables save, but edits are still accepted and dirty still surfaces at Line 85. This creates an unsavable dead-end flow.

Proposed fix
 export function YamlEditorPanel({ config, onSave, saving }: YamlEditorPanelProps) {
+  const readOnly = true // TODO(`#1081`): switch to feature flag when backend CRUD lands
   const [yamlText, setYamlText] = useState('')
   const [parseError, setParseError] = useState<string | null>(null)
   const [dirty, setDirty] = useState(false)
@@
   const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    if (readOnly) return
     setYamlText(e.target.value)
     setDirty(true)
     setParseError(null)
-  }, [])
+  }, [readOnly])
@@
       <textarea
         value={yamlText}
-        onChange={handleChange}
+        onChange={readOnly ? undefined : handleChange}
+        readOnly={readOnly}
+        aria-readonly={readOnly}
         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"
         spellCheck={false}
         aria-label="YAML editor"
       />
@@
         <Button
           onClick={handleSave}
           disabled
           aria-disabled="true"
           title={ORG_EDIT_COMING_SOON_TOOLTIP}
         >
           {saving ? 'Saving...' : 'Save YAML'}
         </Button>
-        <Button variant="outline" onClick={handleReset} disabled={!dirty || saving}>
+        <Button variant="outline" onClick={handleReset} disabled={readOnly || !dirty || saving}>
           Reset
         </Button>
-        {dirty && <span className="text-xs text-warning">Unsaved changes</span>}
+        {!readOnly && dirty && <span className="text-xs text-warning">Unsaved changes</span>}
+        {readOnly && <span className="text-xs text-text-muted">{ORG_EDIT_COMING_SOON_TOOLTIP}</span>}
       </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org-edit/YamlEditorPanel.tsx` around lines 73 - 85, The YAML
panel currently leaves the editor editable while the Save path is permanently
disabled; update YamlEditorPanel so when the Save button is disabled (save
permanently gated) the editor is read-only and the dirty state/unsaved indicator
is suppressed: wire the save-gated condition used for the Save Button (the
disabled prop/ORG_EDIT_COMING_SOON_TOOLTIP branch) into the editor component
(e.g., pass readOnly to the CodeMirror/textarea instance) and ensure functions
that set/compute dirty (references to dirty, handleSave, handleReset) no longer
mark the form dirty or show the "Unsaved changes" span when save is gated; also
disable or hide Reset when save is gated to avoid a dead-end flow.
web/src/__tests__/pages/org-edit/DepartmentCreateDialog.test.tsx (1)

33-40: ⚠️ Potential issue | 🟠 Major

Don’t encode a title-only disabled reason in test expectations.

Asserting only title on a disabled control reinforces an inaccessible UX. Prefer asserting visible helper text or an accessible tooltip/description target (aria-describedby) that includes 1081.

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

In `@web/src/__tests__/pages/org-edit/DepartmentCreateDialog.test.tsx` around
lines 33 - 40, Update the test to stop asserting a disabled reason via the title
attribute and instead verify an accessible visible helper or description
contains "1081": after calling renderDialog() and locating the Create Department
button (createButton via screen.getByRole), assert the button is disabled, then
find the visible helper text or the element referenced by the button's
aria-describedby (e.g., via screen.getByText(/1081/) or
screen.getById(aria-describedby value)) and assert it contains "1081"; keep the
click assertion that fireEvent.click(createButton) does not call mockOnCreate.
Use existing helpers like renderDialog and mockOnCreate to locate elements and
maintain behavior checks.
web/src/__tests__/pages/org-edit/GeneralTab.test.tsx (1)

53-67: ⚠️ Potential issue | 🟡 Minor

Assert the field actually changed, and restore the fast-check disablement sweep.

This test can still pass if change stops mutating local form state, because the button is always disabled. Add an explicit value assertion after fireEvent.change(...), then restore a property-based sweep for dirty-state combinations.

As per coding guidelines, "React/TypeScript code in web dashboard should use property-based testing with fast-check (fc.assert + fc.property)."

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

In `@web/src/__tests__/pages/org-edit/GeneralTab.test.tsx` around lines 53 - 67,
The test for GeneralTab can falsely pass because it never asserts the input
actually updated and it removed the property-based sweep; after
fireEvent.change(...) assert the company name input's value equals 'Updated
Corp' (use screen.getByLabelText(/company name/i).value) to ensure the change
mutated local form state, and reintroduce a fast-check property sweep (fc.assert
+ fc.property) that generates combinations of dirty-field values to confirm the
Save Settings button remains disabled and mockOnUpdate is never called across
generated dirty states; update the test that references GeneralTab,
mockOnUpdate, and the save button role/name to include these assertions and the
fc.property loop.
web/src/pages/org/CeoNode.tsx (1)

26-29: 🛠️ Refactor suggestion | 🟠 Major

Use dashboard shadow tokens instead of bespoke shadow utilities.

The current shadow classes bypass the shared tokenized shadow system.

♻️ Suggested change
-        'shadow-md shadow-accent/20 transition-all duration-200',
-        'hover:shadow-lg hover:shadow-accent/30',
+        'shadow-[var(--so-shadow-card)] transition-all duration-200',
+        'hover:shadow-[var(--so-shadow-card-hover)]',

As per coding guidelines: "Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders instead of hardcoded values".

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

In `@web/src/pages/org/CeoNode.tsx` around lines 26 - 29, The component
CeoNode.tsx uses bespoke shadow and border utilities ('shadow-md
shadow-accent/20', 'hover:shadow-lg hover:shadow-accent/30', and 'border-2
border-accent/40') which bypass the tokenized design system; update the class
string in CeoNode to use the shared tokens instead (replace the bespoke shadow
classes with the appropriate token variables such as var(--so-shadow-card) and
var(--so-shadow-card-hover) for normal and hover states, and swap the custom
border classes for the tokenized border class like border-border or
border-bright), preserving the existing layout/spacings (px-4 py-3, rounded-lg,
min/max-w) and hover transition utilities.
docs/security.md (1)

110-111: ⚠️ Potential issue | 🟡 Minor

Show the style-src fallback anywhere you describe the split policy.

Both snippets here omit style-src, even though the section later relies on that directive for backward compatibility. Please mirror the fallback in both places.

Suggested doc fix
-   `style-src-elem 'self' 'nonce-$csp_nonce'; style-src-attr 'unsafe-inline'`.
+   `style-src 'self' 'nonce-$csp_nonce' 'unsafe-inline'; style-src-elem 'self' 'nonce-$csp_nonce'; style-src-attr 'unsafe-inline'`.

Also applies to: 175-175

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

In `@docs/security.md` around lines 110 - 111, Add the missing style-src fallback
to the split CSP examples so they match the later compatibility section: in the
snippet that emits the CSP (web/security-headers.conf) and the other
split-policy example, add a style-src fallback directive that mirrors the later
section’s fallback alongside style-src-elem 'self' 'nonce-$csp_nonce' and
style-src-attr 'unsafe-inline' so older browsers get the same behavior as
described later.
web/src/pages/workflows/WorkflowCard.tsx (1)

100-108: ⚠️ Potential issue | 🟡 Minor

Pass delete loading through to ConfirmDialog.

onDelete is async-capable now, but the dialog still never receives loading, so the destructive action has no in-flight spinner/disabled feedback while the delete is running.

Suggested wiring
 export function WorkflowCard({ workflow, onDelete, onDuplicate }: WorkflowCardProps) {
   const [confirmDelete, setConfirmDelete] = useState(false)
+  const [isDeleting, setIsDeleting] = useState(false)
   const navigate = useNavigate()
+
+  const handleDelete = async () => {
+    setIsDeleting(true)
+    try {
+      await onDelete(workflow.id)
+    } finally {
+      setIsDeleting(false)
+    }
+  }
 
   return (
     <>
       ...
       <ConfirmDialog
         open={confirmDelete}
         onOpenChange={setConfirmDelete}
-        onConfirm={() => onDelete(workflow.id)}
+        onConfirm={handleDelete}
+        loading={isDeleting}
         title="Delete workflow"
         description={`Are you sure you want to delete "${workflow.name}"? This action cannot be undone.`}
         variant="destructive"

As per coding guidelines, "Use ConfirmDialog component for confirmation modal ... with loading state".

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

In `@web/src/pages/workflows/WorkflowCard.tsx` around lines 100 - 108, The
ConfirmDialog isn't receiving a loading prop so async deletes show no feedback;
add a local state (e.g., deleteLoading) in WorkflowCard, pass
loading={deleteLoading} to <ConfirmDialog>, and replace the inline onConfirm={()
=> onDelete(workflow.id)} with an async handler that sets deleteLoading = true,
awaits onDelete(workflow.id), then sets deleteLoading = false and closes the
dialog via setConfirmDelete(false); ensure you still handle errors/rejections so
the loading state is cleared.
web/src/pages/org/HierarchyEdge.tsx (1)

71-76: ⚠️ Potential issue | 🟡 Minor

Remove the extra bend distance from approxLength.

For the normal L-shaped path, the two vertical segments already add up to Math.abs(ty - sy). Adding BEND_OFFSET again makes particles run slower than the configured pixels-per-second on every bent edge.

📏 Suggested fix
-  const approxLength = Math.abs(tx - sx) < 0.5
-    ? Math.abs(ty - sy)
-    : Math.abs(tx - sx) + Math.abs(ty - sy) + BEND_OFFSET
+  const approxLength =
+    Math.abs(tx - sx) < 0.5
+      ? Math.abs(ty - sy)
+      : Math.abs(sy - bendY) + Math.abs(tx - sx) + Math.abs(ty - bendY)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org/HierarchyEdge.tsx` around lines 71 - 76, The approximate
path length calculation in approxLength currently adds BEND_OFFSET for L-shaped
edges, which double-counts the bend because the vertical segments already sum to
Math.abs(ty - sy); remove the extra BEND_OFFSET so approxLength is either
Math.abs(ty - sy) when Math.abs(tx - sx) < 0.5 or Math.abs(tx - sx) +
Math.abs(ty - sy) otherwise, allowing durSec (computed with MIN_PARTICLE_DUR_SEC
and PARTICLE_PX_PER_SEC) to reflect the intended pixels-per-second timing for
particles; update the expression using the existing variables (approxLength,
BEND_OFFSET, tx, sx, ty, sy, durSec) accordingly.
web/src/components/layout/Sidebar.tsx (1)

371-378: ⚠️ Potential issue | 🟡 Minor

Use the shared LiveRegion for the WS status announcement.

The inline sr-only span reintroduces the earlier accessibility regression: connection state can flap, and this skips the debounce/announcement behavior already centralized in LiveRegion.

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

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

In `@web/src/components/layout/Sidebar.tsx` around lines 371 - 378, Replace the
inline sr-only span in Sidebar.tsx with the shared LiveRegion component so WS
status announcements use the centralized debounce/announcement logic;
specifically, remove the span that reads based on
wsConnected/wsReconnectExhausted and render <LiveRegion> (from
web/src/components/ui/LiveRegion) passing the same computed status string (use
the wsConnected and wsReconnectExhausted values to produce "Connection status:
connected/disconnected/reconnecting") so announcements go through the existing
LiveRegion behavior instead of the ad-hoc span.
web/CLAUDE.md (1)

5-5: ⚠️ Potential issue | 🟡 Minor

Document the nonce value, not a boolean shorthand.

In JSX, <MotionConfig nonce> means nonce={true}. The docs should show the actual per-request nonce being passed through so they match App.tsx and the CSP flow.

✏️ Suggested wording
- `App.tsx` wraps the app in `<CSPProvider nonce={getCspNonce()}>` + `<MotionConfig nonce>` so every inline `<style>` tag injected by Base UI and Framer Motion carries the per-request CSP nonce. See `docs/security.md` → CSP Nonce Infrastructure for the full flow. Base UI's `render` prop is the polymorphism primitive used throughout the dashboard; the local `<Slot>` helper in `components/ui/slot.tsx` uses `@base-ui/react/merge-props` to support the `<Button asChild>` ergonomic (the only component that uses this helper -- all other primitives use Base UI's native `render` prop directly).
+ `App.tsx` wraps the app in `<CSPProvider nonce={getCspNonce()}>` + `<MotionConfig nonce={getCspNonce()}>` so every inline `<style>` tag injected by Base UI and Framer Motion carries the per-request CSP nonce. See `docs/security.md` → CSP Nonce Infrastructure for the full flow. Base UI's `render` prop is the polymorphism primitive used throughout the dashboard; the local `<Slot>` helper in `components/ui/slot.tsx` uses `@base-ui/react/merge-props` to support the `<Button asChild>` ergonomic (the only component that uses this helper -- all other primitives use Base UI's native `render` prop directly).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/CLAUDE.md` at line 5, Update the documentation example to pass the actual
per-request nonce value instead of the boolean shorthand; specifically, mirror
App.tsx by showing CSPProvider and MotionConfig receiving the real nonce (e.g.,
the value returned by getCspNonce()) rather than `<MotionConfig nonce>`; also
mention the Slot helper in components/ui/slot.tsx and that Base UI components
use the render prop, so the docs clearly show nonce={getCspNonce()} (or the
equivalent per-request nonce variable) being threaded through the CSPProvider
and MotionConfig to match the CSP nonce infrastructure.
web/src/components/ui/confirm-dialog.tsx (1)

42-55: ⚠️ Potential issue | 🟠 Major

Make the internal submit lock render-visible.

submittingRef.current blocks re-entry inside the handler, but useRef mutations do not rerender. Until the parent flips loading, Cancel/Escape/backdrop can still close the dialog, and the rendered disabled/spinner state stays stale.

🔒 Suggested fix
-import { useRef } from 'react'
+import { useState } from 'react'
@@
-  const submittingRef = useRef(false)
+  const [submitting, setSubmitting] = useState(false)
+  const busy = loading || submitting
@@
-        if (loading && !nextOpen) return
+        if (busy && !nextOpen) return
         onOpenChange(nextOpen)
       }}
@@
-                <Button variant="outline" disabled={loading}>
+                <Button variant="outline" disabled={busy}>
                   {cancelLabel}
                 </Button>
               }
             />
             <Button
               variant={variant === 'destructive' ? 'destructive' : 'default'}
               data-variant={variant}
-              disabled={submittingRef.current || loading}
+              disabled={busy}
               onClick={async () => {
-                if (submittingRef.current || loading) return
-                submittingRef.current = true
+                if (busy) return
+                setSubmitting(true)
                 try {
                   await onConfirm()
                   onOpenChange(false)
@@
-                } finally {
-                  submittingRef.current = false
-                }
+                } finally {
+                  setSubmitting(false)
+                }
               }}
             >
-              {loading && (
+              {busy && (
                 <Loader2 className="mr-2 size-4 animate-spin" aria-hidden="true" />
               )}

Also applies to: 82-110

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

In `@web/src/components/ui/confirm-dialog.tsx` around lines 42 - 55, The
submit-lock uses submittingRef (useRef) which doesn't trigger rerenders so the
visible disabled/spinner stays stale; replace or mirror submittingRef with a
stateful flag (e.g., isSubmitting via useState) and update it to true when the
confirm action starts and false when it finishes/errs, then use isSubmitting
(not submittingRef.current) everywhere the UI and the onOpenChange handler check
for an in-flight submit (references: submittingRef, isSubmitting, loading,
onOpenChange, AlertDialog.Root and the component's submit/confirm handlers);
ensure both the render (disabled buttons/spinner) and the Escape/backdrop
close-guard use the stateful flag so visibility and behavior remain consistent
(also apply the same change for the other submit-related logic around lines
82-110).
src/synthorg/engine/agent_engine.py (1)

1100-1101: ⚠️ Potential issue | 🟠 Major

Move trim notifications off the prompt-build path.

await self._maybe_notify_personality_trim(trim_payload) keeps non-critical notifier I/O on the critical path before the first provider call. Even with the 2s timeout below, every trimmed run can still pay that latency when the publisher is slow or unhealthy. Fire this in the background instead of awaiting it here.

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

In `@src/synthorg/engine/agent_engine.py` around lines 1100 - 1101, The call to
await self._maybe_notify_personality_trim(trim_payload) blocks the prompt-build
critical path; instead, fire the notifier in the background so slow publisher
I/O doesn't add latency. Replace the awaited call after
logger.info(PROMPT_PERSONALITY_TRIMMED, **trim_payload) with scheduling the
coroutine via asyncio.create_task (or asyncio.get_running_loop().create_task)
and attach a done callback that logs exceptions (or wrap the notifier to handle
its own exceptions/timeouts) so errors don't propagate to the prompt path; keep
the original _maybe_notify_personality_trim(trim_payload) function but ensure
any background task is non-blocking and any failures are logged.
web/src/pages/org/build-org-tree.ts (1)

214-229: ⚠️ Potential issue | 🟠 Major

Prefer the backend utilization metric here.

department_cost_7d / deptBudgetCap mixes a 7-day numerator with a monthly denominator, so utilizationPercent is understated and also drops to null whenever budget_monthly is absent even if health.utilization_percent is already populated. Prefer the backend metric first, then fall back to a 7-day-equivalent cap if you still need a local calculation. Please add a regression assertion for utilizationPercent alongside the fix.

📉 Suggested change
   const companyMonthlyBudget = typeof config.budget_monthly === 'number' ? config.budget_monthly : null
   const buildDeptData = (dept: Department): DepartmentGroupData => {
     const deptMembers = deptAgents.get(dept.name) ?? []
     const health = healthMap.get(dept.name)
     const activeCount = deptMembers.filter(
       (a) => resolveRuntimeStatus(a.id ?? a.name, a.status ?? 'active', runtimeStatuses) === 'active',
     ).length
     const budgetPercent = typeof dept.budget_percent === 'number' ? dept.budget_percent : null
-    const deptBudgetCap = budgetPercent !== null && companyMonthlyBudget !== null
-      ? (budgetPercent / 100) * companyMonthlyBudget
-      : null
+    const weeklyBudgetCap = budgetPercent !== null && companyMonthlyBudget !== null
+      ? ((budgetPercent / 100) * companyMonthlyBudget * 7) / 30
+      : null
     const cost7d = health?.department_cost_7d ?? null
-    const utilizationPercent = deptBudgetCap !== null && cost7d !== null && deptBudgetCap > 0
-      ? Math.min(100, Math.round((cost7d / deptBudgetCap) * 100))
-      : null
+    const utilizationPercent =
+      typeof health?.utilization_percent === 'number'
+        ? health.utilization_percent
+        : weeklyBudgetCap !== null && cost7d !== null && weeklyBudgetCap > 0
+          ? Math.min(100, Math.round((cost7d / weeklyBudgetCap) * 100))
+          : null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org/build-org-tree.ts` around lines 214 - 229, The
utilizationPercent logic in buildDeptData incorrectly computes from
department_cost_7d vs a monthly deptBudgetCap and returns null when
budget_monthly is missing; instead prefer the backend-provided
health.utilization_percent first, then only if that's absent compute a
7-day-equivalent local value (use health.department_cost_7d and convert
deptBudgetCap to a 7-day numerator or convert monthly cap to a 7-day cap) and
ensure you still handle nulls safely; update the computation in buildDeptData
(references: utilizationPercent, deptBudgetCap, companyMonthlyBudget,
health.department_cost_7d, health.utilization_percent) and add a unit/regression
assertion verifying utilizationPercent is populated from
health.utilization_percent when present and that the fallback calculation yields
a numeric percent when department_cost_7d and budget_monthly are available.
web/src/pages/org/layout.ts (1)

109-121: ⚠️ Potential issue | 🟡 Minor

Keep owner nodes out of the empty-department sizing path.

In the no-agent fast path, every node gets EMPTY_GROUP_MIN_WIDTH/EMPTY_GROUP_HEIGHT. Owner cards are explicitly sized elsewhere, so this doubles their height and skews hitboxes/edge anchoring in owner-only orgs. Only apply the empty-group dimensions to department nodes.

🧩 Suggested change
   if (agentLeafNodes.length === 0) {
     return nodes.map((n, i) => {
       const major = i % 3
       const minor = Math.floor(i / 3)
       const x = major * 260
       const y = minor * 180
+      const isDepartment = n.type === 'department'
       return {
         ...n,
         position: { x, y },
-        width: EMPTY_GROUP_MIN_WIDTH,
-        height: EMPTY_GROUP_HEIGHT,
-        style: { ...n.style, width: EMPTY_GROUP_MIN_WIDTH, height: EMPTY_GROUP_HEIGHT },
+        ...(isDepartment
+          ? {
+              width: EMPTY_GROUP_MIN_WIDTH,
+              height: EMPTY_GROUP_HEIGHT,
+              style: {
+                ...n.style,
+                width: EMPTY_GROUP_MIN_WIDTH,
+                height: EMPTY_GROUP_HEIGHT,
+              },
+            }
+          : {}),
       }
     })
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org/layout.ts` around lines 109 - 121, The current fast-path
(when agentLeafNodes.length === 0) applies
EMPTY_GROUP_MIN_WIDTH/EMPTY_GROUP_HEIGHT to every node in nodes.map, which
incorrectly resizes owner cards; update the mapping so that only department
nodes (n.type !== 'owner') receive position, width, height and style overrides
using EMPTY_GROUP_MIN_WIDTH and EMPTY_GROUP_HEIGHT, while owner nodes are
returned unchanged (or keep their existing position/size) — locate the nodes.map
block inside the agentLeafNodes === 0 branch and conditionally apply the
empty-group dimensions only for nodes where n.type !== 'owner'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/security.md`:
- Around line 98-106: Update the CSP nonce guidance to correct the safety claim:
state that web/nginx.conf's map $request_id $csp_nonce uses nginx's $request_id
which is cryptographically secure only when nginx is built against OpenSSL
(sourced from RAND_bytes), and explicitly note that the non-OpenSSL fallback
(ngx_random / seeded PRNG) is NOT cryptographically unpredictable and therefore
not suitable for CSP nonces; either assert only the OpenSSL/RAND_bytes path is
CSP-safe or add a clear warning that builds without OpenSSL should not rely on
$request_id for CSP nonces and recommend generating nonces with a CSPRNG
instead.

In `@src/synthorg/api/app.py`:
- Around line 277-283: The publish call is being executed off-thread via
asyncio.to_thread/functools.partial which risks race conditions; instead invoke
ChannelsPlugin.publish on the event loop thread: remove the asyncio.to_thread
and functools.partial wrappers and call
channels_plugin.publish(event.model_dump_json(), channels=[CHANNEL_AGENTS])
directly (or await it if publish is async), or alternatively enqueue the payload
to an async queue consumed on the event loop—update the code around
channels_plugin.publish, event.model_dump_json(), and CHANNEL_AGENTS
accordingly.

In `@web/src/components/ui/animated-presence.tsx`:
- Around line 74-92: The component currently forces 'h-full' on the motion.div
inside AnimatedPresence (see AnimatePresence usage, motion.div, key={routeKey},
variants and the className={cn('h-full', className)} line), coupling a layout
assumption into the primitive; remove the hardcoded 'h-full' from the className
merge so the component becomes layout-agnostic (change className={cn('h-full',
className)} to className={cn(className)} or equivalent) and update callers
(e.g., AppLayout) to pass className="h-full" explicitly where a full-height
wrapper is required; leave routeKey, variants, initial/animate/exit props
untouched.

In `@web/src/components/ui/command-palette.tsx`:
- Around line 164-170: The component currently uses arbitrary values like
"max-w-[640px]", "max-h-[320px]", "text-[10px]" and similar ad-hoc classes in
web/src/components/ui/command-palette.tsx; replace those hardcoded pixel
utilities with the design tokens or standard Tailwind scale classes (e.g., use
density-aware spacing tokens such as p-card, gap-section-gap, gap-grid-gap and
token-based max-width/height or standard Tailwind sizes like max-w-* / text-*)
so the primitive remains theme/density-aware; update all occurrences referenced
in the diff (the class strings containing max-w-[640px], max-h-[320px],
text-[10px], etc.) and keep the transition comment but ensure transition
properties use the proper token/scaled classes rather than arbitrary values.

In `@web/src/components/ui/confirm-dialog.tsx`:
- Around line 99-104: The catch block in ConfirmDialog's onConfirm handler logs
an attacker-controlled value `title` directly; wrap `title` with the repository
sanitizer before logging (use sanitizeForLog(title)) and pass the sanitized
value into the structured log object used in the log.warn call so the logged
object contains sanitizedTitle instead of the raw `title`.

In `@web/src/hooks/useCommandPalette.ts`:
- Around line 11-19: The action property's type `unknown | Promise<unknown>`
should be simplified to reflect intent: update the `action` signature in the
useCommandPalette hook/type from `() => unknown | Promise<unknown>` to a clearer
type such as `() => Promise<unknown>` (if you want to treat sync returns as
non-awaited values) or `() => void | Promise<void>` (if you only care about
success/rejection and not returned values); change the `action` declaration and
any callers/implementations to match the chosen signature and adjust any
await/handling logic accordingly (look for the `action` property in the
useCommandPalette hook/type and its usage sites).

In `@web/src/hooks/useGlobalNotifications.ts`:
- Around line 32-37: The catch block in the WebSocket handler logs an untrusted
field directly; wrap the attacker-controlled event type with sanitizeForLog()
before passing it into the structured object in the log. Update the handler's
catch that calls log.warn('updateFromWsEvent threw -- event dropped', {
event_type: String(event?.event_type) }, err) to instead pass { event_type:
sanitizeForLog(String(event?.event_type)) } so the call site around
useAgentsStore.getState().updateFromWsEvent is sanitized.

In `@web/src/hooks/useLiveEdgeActivity.ts`:
- Line 25: expiryMapRef currently keys by edge id which becomes stale when the
org-chart rebuilds; instead store TTLs keyed by the agent-pair string and, when
computing activeEdgeIds, map those pair keys through the latest
edgeIdByAgentPair to resolve current edge ids. Update the useRef declaration
(expiryMapRef) to be Map<string, number> keyed by the pair key, change any calls
that set/clear entries (in the handlers referenced around lines 42-52) to use
the pair key instead of edge id, and when materializing activeEdgeIds, iterate
expiryMapRef keys, use edgeIdByAgentPair.get(pairKey) to get the current edge
id, filter expired entries, and collect the resolved edge ids into
activeEdgeIds.

In `@web/src/pages/org-edit/DepartmentCreateDialog.tsx`:
- Around line 10-19: The props interface DepartmentCreateDialogProps declares
required properties existingNames and onCreate but the component signature for
DepartmentCreateDialog only destructures open and onOpenChange, causing a type
mismatch; to fix, update the interface to make existingNames and onCreate
optional (existingNames?: readonly string[]; onCreate?: (data:
CreateDepartmentRequest) => Promise<Department>) or alternatively include them
in the component's parameter destructuring (e.g., function
DepartmentCreateDialog({ open, onOpenChange, existingNames, onCreate }:
DepartmentCreateDialogProps)) so the declared contract matches the function
signature.

In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx`:
- Around line 168-186: The disabled Delete and Save buttons (Button components
using setDeleteOpen and handleSave) rely on
title={ORG_EDIT_COMING_SOON_TOOLTIP}, which inaccessible to
keyboard/screen-reader users; instead render the coming-soon message as visible
helper text or a focusable wrapper with an accessible description and reference
it via aria-describedby: create a nearby element (e.g., a small <span> or <div>)
containing ORG_EDIT_COMING_SOON_TOOLTIP with a stable id and either display it
inline under the buttons or attach aria-describedby="{that-id}" to a focusable
container (or to an enabled wrapper/button) so assistive tech can read the
reason while keeping the buttons disabled.

In `@web/src/pages/org-edit/GeneralTab.tsx`:
- Around line 117-139: The budget input currently coerces every keystroke by
converting e.target.value to Number and writing it back via
updateForm('budget_monthly'), which breaks intermediate states like "0." or
empty string; instead introduce a local string state (e.g., draftBudget) inside
the GeneralTab component initialized from form.budget_monthly, wire
InputField.value to draftBudget and update draftBudget on every onChange, and
only on blur/save (handle onBlur or submit) parse/validate the string (allow
empty -> 0 or enforce non-negative finite) and call updateForm('budget_monthly',
parsedNumber); ensure you still pass min/step props and keep parse/validation
logic currently used (Number.isFinite && >=0) when committing the value.

In `@web/src/pages/org/OrgChartToolbar.tsx`:
- Around line 46-76: The custom InlineToggle component in OrgChartToolbar lacks
accessibility and visible focus; replace the hand-rolled toggle/radiogroup logic
in InlineToggle (and similar code at lines 83-119) with the shared
SegmentedControl and Button primitives from web/src/components/ui so you get
built-in roving-tabindex/arrow-key behavior and focus styling; refactor
OrgChartToolbar to render SegmentedControl for mutually exclusive options and
use the Button-based toggle variant for individual toggles, wire their checked
state to the existing checked/onToggle props, and remove the custom
aria/keyboard handling so the shared components provide consistent keyboard
navigation and visible focus.

In `@web/src/pages/org/OwnerNode.tsx`:
- Around line 37-38: The component OwnerNode contains a one-off shadow utility
string 'shadow-sm shadow-warning/20' in its className; replace that custom
shadow with the shared shadow token (e.g. use the tokenized shadow variable
var(--so-shadow-card-hover) or the project's equivalent token) so the component
uses the dashboard shadow token instead of a hardcoded utility; update the
className in OwnerNode where 'shadow-sm shadow-warning/20' appears to remove the
hardcoded classes and apply the tokenized shadow token alongside existing
classes.

In `@web/src/pages/settings/SettingField.tsx`:
- Around line 110-115: The current useMemo for displayValue strips trailing “.0”
during editing causing input collapse; introduce an isEditing boolean state on
the component and set it true on focus and false on blur for the input element,
then update the useMemo (displayValue) to only apply the /^-?\d+\.0+$/
replacement when definition.type === 'float' AND isEditing is false, otherwise
return the raw value; ensure the input uses displayValue for rendering but
focus/blur handlers update isEditing so intermediate float keystrokes (e.g.,
typing 10.05) are preserved.

In `@web/src/stores/agents.ts`:
- Line 332: The log call in log.debug('WS event ignored: unhandled event_type',
{ event_type: String(event?.event_type) }) should use the existing
sanitizeForLog helper to normalize untrusted WS payloads for consistency;
replace the String(...) usage with sanitizeForLog(event?.event_type) so the
unhandled event_type is logged the same way as other handlers (refer to
sanitizeForLog and the log.debug call around event?.event_type).

In `@web/src/styles/design-tokens.css`:
- Line 90: Replace the hardcoded RGBA in the --so-minimap-node-dept token with a
theme-aware CSS variable: stop using rgba(56, 189, 248, 0.22) and instead derive
the color from a semantic token (e.g., use an existing department color variable
like --so-color-dept or create a new RGB token --so-color-dept-rgb) and apply
the alpha via rgba(var(--so-color-dept-rgb), 0.22) or color-mix(); update the
declaration of --so-minimap-node-dept to reference that semantic token so
minimap nodes follow theme changes (update any usages of --so-minimap-node-dept
accordingly).

---

Outside diff comments:
In `@web/src/lib/motion.ts`:
- Around line 72-79: The duration is duplicated between tweenSlow.duration and
TRANSITION_SLOW_MS causing potential drift; pick one canonical source (either
TRANSITION_SLOW_MS or tweenSlow.duration) and derive the other from it (e.g.,
keep TRANSITION_SLOW_MS as the single source of truth and set tweenSlow.duration
= TRANSITION_SLOW_MS / 1000, or keep tweenSlow.duration and compute
TRANSITION_SLOW_MS = Math.round(tweenSlow.duration * 1000)); update references
to use the canonical symbol (TRANSITION_SLOW_MS or tweenSlow) so future edits
change only one value.

---

Duplicate comments:
In `@docs/security.md`:
- Around line 110-111: Add the missing style-src fallback to the split CSP
examples so they match the later compatibility section: in the snippet that
emits the CSP (web/security-headers.conf) and the other split-policy example,
add a style-src fallback directive that mirrors the later section’s fallback
alongside style-src-elem 'self' 'nonce-$csp_nonce' and style-src-attr
'unsafe-inline' so older browsers get the same behavior as described later.

In `@src/synthorg/api/controllers/analytics.py`:
- Around line 296-307: When app_state.agent_registry.list_active() fails the
except block currently returns (0, config_agent_count) which wipes ACTIVE_AGENTS
despite in-progress tasks; instead, catch the Exception and compute the active
count from the existing in-memory data (e.g., derive the active count from
all_tasks/in-progress assignees already computed earlier in
resolve_agent_counts) and return (derived_active_count, config_agent_count) so
the first tuple value uses the locally-known active agents rather than 0; modify
the except handler around app_state.agent_registry.list_active() (and references
to employed) to log the error but fall back to the computed local active count.
- Around line 418-423: The code is calling
app_state.persistence.tasks.list_tasks() which returns the full task history;
instead, fetch only current IN_PROGRESS tasks so ACTIVE_AGENTS computation
scales properly—modify the call site that feeds _resolve_agent_counts (keep
_resolve_agent_counts signature) to pass a filtered task list (e.g., use an
existing list_active_tasks or call list_tasks with a status/statuses filter like
TaskStatus.IN_PROGRESS) so you only retrieve current in-progress assignees
rather than the entire history.
- Around line 274-275: The debug branches are calling _resolve_agent_counts()
without the caller’s analytics event, causing logs to always emit
ANALYTICS_OVERVIEW_QUERIED and misclassify /analytics/trends; update each call
site (the calls around lines emitting ANALYTICS_OVERVIEW_QUERIED and similar at
the other two spots) to pass the current analytics event object (e.g.,
analytics_event or caller_event) into _resolve_agent_counts(…) and then adjust
_resolve_agent_counts to use that passed-in event when emitting logs instead of
hardcoding ANALYTICS_OVERVIEW_QUERIED so the correct event name is logged for
trends and overview calls.

In `@src/synthorg/engine/agent_engine.py`:
- Around line 1100-1101: The call to await
self._maybe_notify_personality_trim(trim_payload) blocks the prompt-build
critical path; instead, fire the notifier in the background so slow publisher
I/O doesn't add latency. Replace the awaited call after
logger.info(PROMPT_PERSONALITY_TRIMMED, **trim_payload) with scheduling the
coroutine via asyncio.create_task (or asyncio.get_running_loop().create_task)
and attach a done callback that logs exceptions (or wrap the notifier to handle
its own exceptions/timeouts) so errors don't propagate to the prompt path; keep
the original _maybe_notify_personality_trim(trim_payload) function but ensure
any background task is non-blocking and any failures are logged.

In `@web/CLAUDE.md`:
- Line 5: Update the documentation example to pass the actual per-request nonce
value instead of the boolean shorthand; specifically, mirror App.tsx by showing
CSPProvider and MotionConfig receiving the real nonce (e.g., the value returned
by getCspNonce()) rather than `<MotionConfig nonce>`; also mention the Slot
helper in components/ui/slot.tsx and that Base UI components use the render
prop, so the docs clearly show nonce={getCspNonce()} (or the equivalent
per-request nonce variable) being threaded through the CSPProvider and
MotionConfig to match the CSP nonce infrastructure.

In `@web/src/__tests__/pages/org-edit/DepartmentCreateDialog.test.tsx`:
- Around line 33-40: Update the test to stop asserting a disabled reason via the
title attribute and instead verify an accessible visible helper or description
contains "1081": after calling renderDialog() and locating the Create Department
button (createButton via screen.getByRole), assert the button is disabled, then
find the visible helper text or the element referenced by the button's
aria-describedby (e.g., via screen.getByText(/1081/) or
screen.getById(aria-describedby value)) and assert it contains "1081"; keep the
click assertion that fireEvent.click(createButton) does not call mockOnCreate.
Use existing helpers like renderDialog and mockOnCreate to locate elements and
maintain behavior checks.

In `@web/src/__tests__/pages/org-edit/GeneralTab.test.tsx`:
- Around line 53-67: The test for GeneralTab can falsely pass because it never
asserts the input actually updated and it removed the property-based sweep;
after fireEvent.change(...) assert the company name input's value equals
'Updated Corp' (use screen.getByLabelText(/company name/i).value) to ensure the
change mutated local form state, and reintroduce a fast-check property sweep
(fc.assert + fc.property) that generates combinations of dirty-field values to
confirm the Save Settings button remains disabled and mockOnUpdate is never
called across generated dirty states; update the test that references
GeneralTab, mockOnUpdate, and the save button role/name to include these
assertions and the fc.property loop.

In `@web/src/components/layout/Sidebar.tsx`:
- Around line 371-378: Replace the inline sr-only span in Sidebar.tsx with the
shared LiveRegion component so WS status announcements use the centralized
debounce/announcement logic; specifically, remove the span that reads based on
wsConnected/wsReconnectExhausted and render <LiveRegion> (from
web/src/components/ui/LiveRegion) passing the same computed status string (use
the wsConnected and wsReconnectExhausted values to produce "Connection status:
connected/disconnected/reconnecting") so announcements go through the existing
LiveRegion behavior instead of the ad-hoc span.

In `@web/src/components/ui/confirm-dialog.tsx`:
- Around line 42-55: The submit-lock uses submittingRef (useRef) which doesn't
trigger rerenders so the visible disabled/spinner stays stale; replace or mirror
submittingRef with a stateful flag (e.g., isSubmitting via useState) and update
it to true when the confirm action starts and false when it finishes/errs, then
use isSubmitting (not submittingRef.current) everywhere the UI and the
onOpenChange handler check for an in-flight submit (references: submittingRef,
isSubmitting, loading, onOpenChange, AlertDialog.Root and the component's
submit/confirm handlers); ensure both the render (disabled buttons/spinner) and
the Escape/backdrop close-guard use the stateful flag so visibility and behavior
remain consistent (also apply the same change for the other submit-related logic
around lines 82-110).

In `@web/src/pages/org-edit/YamlEditorPanel.tsx`:
- Around line 73-85: The YAML panel currently leaves the editor editable while
the Save path is permanently disabled; update YamlEditorPanel so when the Save
button is disabled (save permanently gated) the editor is read-only and the
dirty state/unsaved indicator is suppressed: wire the save-gated condition used
for the Save Button (the disabled prop/ORG_EDIT_COMING_SOON_TOOLTIP branch) into
the editor component (e.g., pass readOnly to the CodeMirror/textarea instance)
and ensure functions that set/compute dirty (references to dirty, handleSave,
handleReset) no longer mark the form dirty or show the "Unsaved changes" span
when save is gated; also disable or hide Reset when save is gated to avoid a
dead-end flow.

In `@web/src/pages/org/build-org-tree.ts`:
- Around line 214-229: The utilizationPercent logic in buildDeptData incorrectly
computes from department_cost_7d vs a monthly deptBudgetCap and returns null
when budget_monthly is missing; instead prefer the backend-provided
health.utilization_percent first, then only if that's absent compute a
7-day-equivalent local value (use health.department_cost_7d and convert
deptBudgetCap to a 7-day numerator or convert monthly cap to a 7-day cap) and
ensure you still handle nulls safely; update the computation in buildDeptData
(references: utilizationPercent, deptBudgetCap, companyMonthlyBudget,
health.department_cost_7d, health.utilization_percent) and add a unit/regression
assertion verifying utilizationPercent is populated from
health.utilization_percent when present and that the fallback calculation yields
a numeric percent when department_cost_7d and budget_monthly are available.

In `@web/src/pages/org/CeoNode.tsx`:
- Around line 26-29: The component CeoNode.tsx uses bespoke shadow and border
utilities ('shadow-md shadow-accent/20', 'hover:shadow-lg
hover:shadow-accent/30', and 'border-2 border-accent/40') which bypass the
tokenized design system; update the class string in CeoNode to use the shared
tokens instead (replace the bespoke shadow classes with the appropriate token
variables such as var(--so-shadow-card) and var(--so-shadow-card-hover) for
normal and hover states, and swap the custom border classes for the tokenized
border class like border-border or border-bright), preserving the existing
layout/spacings (px-4 py-3, rounded-lg, min/max-w) and hover transition
utilities.

In `@web/src/pages/org/HierarchyEdge.tsx`:
- Around line 71-76: The approximate path length calculation in approxLength
currently adds BEND_OFFSET for L-shaped edges, which double-counts the bend
because the vertical segments already sum to Math.abs(ty - sy); remove the extra
BEND_OFFSET so approxLength is either Math.abs(ty - sy) when Math.abs(tx - sx) <
0.5 or Math.abs(tx - sx) + Math.abs(ty - sy) otherwise, allowing durSec
(computed with MIN_PARTICLE_DUR_SEC and PARTICLE_PX_PER_SEC) to reflect the
intended pixels-per-second timing for particles; update the expression using the
existing variables (approxLength, BEND_OFFSET, tx, sx, ty, sy, durSec)
accordingly.

In `@web/src/pages/org/layout.ts`:
- Around line 109-121: The current fast-path (when agentLeafNodes.length === 0)
applies EMPTY_GROUP_MIN_WIDTH/EMPTY_GROUP_HEIGHT to every node in nodes.map,
which incorrectly resizes owner cards; update the mapping so that only
department nodes (n.type !== 'owner') receive position, width, height and style
overrides using EMPTY_GROUP_MIN_WIDTH and EMPTY_GROUP_HEIGHT, while owner nodes
are returned unchanged (or keep their existing position/size) — locate the
nodes.map block inside the agentLeafNodes === 0 branch and conditionally apply
the empty-group dimensions only for nodes where n.type !== 'owner'.

In `@web/src/pages/org/OrgChartSearchOverlay.tsx`:
- Around line 34-45: The effect that focuses the input when open=true doesn’t
restore focus when the overlay closes; update the logic in the useEffect that
references open, inputRef, requestAnimationFrame and cancelAnimationFrame to
capture document.activeElement (e.g., prevFocus) before moving focus, and on
cleanup when open becomes false (or in the returned cleanup) call
prevFocus?.focus() (guarding for elements that may be removed) so keyboard users
regain their prior focus; ensure you still cancel the animation frame id and
clear the saved reference to avoid leaks.

In `@web/src/pages/tasks/TaskCreateDialog.tsx`:
- Around line 318-324: The clone currently overwrites any existing
'aria-describedby' when creating controlWithAria; update the clone logic in
TaskCreateDialog's controlWithAria branch to read the existing aria-describedby
from children (when isValidElement and children.props['aria-describedby']
exists), merge it with errorId (append space-separated and dedupe if present)
and pass the merged string to cloneElement along with 'aria-invalid': true, so
existing hint IDs are preserved and the errorId is added.
- Around line 299-300: The current TaskCreateDialog uses a local FormField
wrapper and duplicated INPUT_CLASSES/TEXTAREA_CLASSES causing duplicated
label/error/ARIA behavior; replace these usages with the shared InputField and
SelectField components: remove/stop using FormField, INPUT_CLASSES and
TEXTAREA_CLASSES and swap each input usage to InputField (for single-line
inputs) or InputField with multiline/rows prop for textareas, and swap selects
to SelectField, passing through label, name, error/hint/placeholder and options,
and wire up the same form registration/control props (e.g., the existing
name/field registration) so ARIA, error display and hint behavior are delegated
to InputField/SelectField.

In `@web/src/pages/workflows/WorkflowCard.tsx`:
- Around line 100-108: The ConfirmDialog isn't receiving a loading prop so async
deletes show no feedback; add a local state (e.g., deleteLoading) in
WorkflowCard, pass loading={deleteLoading} to <ConfirmDialog>, and replace the
inline onConfirm={() => onDelete(workflow.id)} with an async handler that sets
deleteLoading = true, awaits onDelete(workflow.id), then sets deleteLoading =
false and closes the dialog via setConfirmDelete(false); ensure you still handle
errors/rejections so the loading state is cleared.

In `@web/src/styles/global.css`:
- Around line 163-180: The .react-flow__node element is positioned by React Flow
via its own transform so remove custom transform and transition from that
selector to avoid interfering with library-managed positioning: delete
transform: scale(...) from .react-flow__node.dragging and remove transition:
transform from .react-flow__node; instead apply the scale transform and
transition to the node's content wrapper (the child div targeted by
.react-flow__node.dragging > div and .react-flow__node > div) so the visual
hover/drag animation runs on the inner element while the outer .react-flow__node
remains transform-free; keep the existing box-shadow on
.react-flow__node.dragging > div.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

Comment on lines +98 to +106
1. **Generation.** `web/nginx.conf` defines `map $request_id $csp_nonce { default $request_id; }`.
nginx's built-in `$request_id` is a 32-hex-character (128-bit) pseudo-random identifier
generated per request -- sufficient uniqueness for CSP nonce use. When nginx is built
against OpenSSL (the case for the Chainguard / nginx-unprivileged distros this project
ships), `$request_id` is sourced from `RAND_bytes` and is cryptographically random;
on builds without OpenSSL it falls back to a seeded PRNG, which still provides 128-bit
uniqueness but is not a CSPRNG. The CSP threat model only requires per-request
uniqueness against an attacker who does not observe prior responses, which both paths
satisfy.
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 | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For CSP nonces, is per-response uniqueness sufficient or must the nonce also be unpredictable/unguessable? How is nginx $request_id generated, and is it documented as suitable for CSP nonces?

💡 Result:

For CSP nonces, per-response uniqueness is not sufficient; the nonce must also be unpredictable and unguessable (cryptographically strong random value with at least 128 bits of entropy). This prevents attackers from guessing the nonce to inject malicious scripts, even if they can inspect responses for legitimate ones. Sources emphasize using cryptographically secure generators like OpenSSL's RAND_bytes or equivalent. Nginx's $request_id is generated per request as a 32-character hexadecimal string (128 bits). If nginx is compiled with OpenSSL support (NGX_OPENSSL), it uses RAND_bytes for 16 cryptographically strong random bytes. Otherwise, it falls back to ngx_random (pseudo-random). Official docs describe it as "unique request identifier generated from 16 random bytes, in hexadecimal" (nginx 1.11.0+), but do not explicitly state suitability for CSP nonces. However, community sources (e.g., Server Fault) confirm it is suitable when OpenSSL is enabled, as it meets CSP requirements. Verify compilation with nginx -V for OpenSSL.

Citations:


Fix the CSP nonce safety claim for non-OpenSSL builds.

The paragraph claims "both paths satisfy" the CSP threat model by requiring only per-request uniqueness. This is incorrect: CSP nonces require both uniqueness and cryptographic unpredictability. The non-OpenSSL fallback uses ngx_random, a seeded PRNG that does not meet the unpredictability requirement—only the OpenSSL path (using RAND_bytes) is suitable for CSP nonces.

Either document only the OpenSSL path as CSP-safe, or add an explicit note that the non-OpenSSL fallback does not satisfy CSP nonce requirements and should not be relied upon for this use case.

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

In `@docs/security.md` around lines 98 - 106, Update the CSP nonce guidance to
correct the safety claim: state that web/nginx.conf's map $request_id $csp_nonce
uses nginx's $request_id which is cryptographically secure only when nginx is
built against OpenSSL (sourced from RAND_bytes), and explicitly note that the
non-OpenSSL fallback (ngx_random / seeded PRNG) is NOT cryptographically
unpredictable and therefore not suitable for CSP nonces; either assert only the
OpenSSL/RAND_bytes path is CSP-safe or add a clear warning that builds without
OpenSSL should not rely on $request_id for CSP nonces and recommend generating
nonces with a CSPRNG instead.

Comment on lines +277 to +283
await asyncio.to_thread(
functools.partial(
channels_plugin.publish,
event.model_dump_json(),
channels=[CHANNEL_AGENTS],
),
)
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

🌐 Web query:

In Litestar 2.21.1, is ChannelsPlugin.publish()documented as safe to call from background threads such asasyncio.to_thread()?

💡 Result:

No, ChannelsPlugin.publish in Litestar 2.21.1 is not documented as safe to call from background threads such as asyncio.to_thread.

Citations:


Move ChannelsPlugin.publish() back to the event loop thread.

ChannelsPlugin.publish() in Litestar 2.21.1 is not documented as safe for off-thread calls. Wrapping it in asyncio.to_thread() creates a race condition that MagicMock tests won't catch. Either publish inline on the event loop or enqueue the message to a dedicated async queue that publishes without blocking.

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

In `@src/synthorg/api/app.py` around lines 277 - 283, The publish call is being
executed off-thread via asyncio.to_thread/functools.partial which risks race
conditions; instead invoke ChannelsPlugin.publish on the event loop thread:
remove the asyncio.to_thread and functools.partial wrappers and call
channels_plugin.publish(event.model_dump_json(), channels=[CHANNEL_AGENTS])
directly (or await it if publish is async), or alternatively enqueue the payload
to an async queue consumed on the event loop—update the code around
channels_plugin.publish, event.model_dump_json(), and CHANNEL_AGENTS
accordingly.

Comment on lines +74 to +92
// `h-full` (not `flex-1`) on the motion.div is load-bearing: this
// wrapper lives inside `<main>` which is a block element, so
// `flex-1` has no effect and leaves the motion.div with `height:
// auto` (content-based). Any descendant that relies on `h-full`
// (e.g. the Org Chart's React Flow container, which needs a
// concrete parent height to render at all) then collapses to zero.
// `h-full` resolves against `<main>`'s own concrete height (it is
// a flex-1 child of a `h-screen` column) and propagates the
// available height down to pages that want to fill it; pages that
// are shorter than the viewport still scroll naturally because
// `<main>` owns the `overflow-y-auto` above this wrapper.
<AnimatePresence mode="popLayout" initial={false}>
<motion.div
key={routeKey}
variants={variants}
initial="initial"
animate="animate"
exit="exit"
className={cn('flex-1', className)}
className={cn('h-full', className)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expected result: only the main route boundary uses AnimatedPresence.
rg -n -C3 '<AnimatedPresence\b' web/src

Repository: Aureliolo/synthorg

Length of output: 4302


🏁 Script executed:

#!/bin/bash
# Check WizardShell layout context around line 119
head -n 140 web/src/pages/setup/WizardShell.tsx | tail -n 35

Repository: Aureliolo/synthorg

Length of output: 1060


🏁 Script executed:

#!/bin/bash
# Check if any consumer passes className prop to override h-full
rg -n -B2 -A2 '<AnimatedPresence.*className=' web/src

Repository: Aureliolo/synthorg

Length of output: 567


🏁 Script executed:

#!/bin/bash
# Check the overall structure of WizardShell to understand layout hierarchy
cat -n web/src/pages/setup/WizardShell.tsx | head -n 150

Repository: Aureliolo/synthorg

Length of output: 5716


Consider making h-full explicit to the caller rather than a shared default.

Both production consumers (AppLayout and WizardShell) have flex-1 parents, so the h-full default works safely in both contexts. However, h-full is justified only in AppLayout—specifically to ensure descendant components relying on concrete parent height (like React Flow) don't collapse. WizardShell's flex-1 container doesn't have this constraint; content flows at natural height without the wrapper needing to fill available space.

Making h-full a shared default couples a layout assumption into the primitive for a single use case. Instead, apply h-full explicitly in AppLayout's usage (<AnimatedPresence className="h-full" />) and keep the component layout-agnostic. The className prop already supports this pattern.

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

In `@web/src/components/ui/animated-presence.tsx` around lines 74 - 92, The
component currently forces 'h-full' on the motion.div inside AnimatedPresence
(see AnimatePresence usage, motion.div, key={routeKey}, variants and the
className={cn('h-full', className)} line), coupling a layout assumption into the
primitive; remove the hardcoded 'h-full' from the className merge so the
component becomes layout-agnostic (change className={cn('h-full', className)} to
className={cn(className)} or equivalent) and update callers (e.g., AppLayout) to
pass className="h-full" explicitly where a full-height wrapper is required;
leave routeKey, variants, initial/animate/exit props untouched.

Comment on lines +164 to +170
'fixed left-1/2 top-[15vh] z-50 w-full max-w-[640px] max-[1023px]:max-w-[calc(100vw-2rem)] -translate-x-1/2',
'rounded-xl border border-border-bright bg-surface shadow-[var(--so-shadow-card-hover)]',
// Tailwind v4 uses dedicated `translate:`/`scale:` CSS properties
// rather than the legacy `transform:` shorthand, so the transition
// property list must name each one explicitly for the animation to
// play.
'transition-[opacity,translate] duration-150 ease-out',
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

Replace arbitrary sizing in this shared primitive.

max-w-[640px], max-h-[320px], text-[10px], and the other arbitrary layout values here reintroduce ad-hoc sizing into web/src/components/ui/. This layer should stay on tokens or standard Tailwind scales so density/theme changes remain consistent. As per coding guidelines, "Use design tokens exclusively in component code -- no hardcoded colors, fonts, or spacing" and "Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing, never hardcode pixel values for layout spacing."

Also applies to: 187-189, 193-194, 222-233, 263-266

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

In `@web/src/components/ui/command-palette.tsx` around lines 164 - 170, The
component currently uses arbitrary values like "max-w-[640px]", "max-h-[320px]",
"text-[10px]" and similar ad-hoc classes in
web/src/components/ui/command-palette.tsx; replace those hardcoded pixel
utilities with the design tokens or standard Tailwind scale classes (e.g., use
density-aware spacing tokens such as p-card, gap-section-gap, gap-grid-gap and
token-based max-width/height or standard Tailwind sizes like max-w-* / text-*)
so the primitive remains theme/density-aware; update all occurrences referenced
in the diff (the class strings containing max-w-[640px], max-h-[320px],
text-[10px], etc.) and keep the transition comment but ensure transition
properties use the proper token/scaled classes rather than arbitrary values.

Comment on lines +110 to +115
const displayValue = useMemo(() => {
if (definition.type === 'float' && /^-?\d+\.0+$/.test(value)) {
return value.replace(/\.0+$/, '')
}
return value
}, [definition.type, 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:

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

Repository: Aureliolo/synthorg

Length of output: 7237


🏁 Script executed:

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

Repository: Aureliolo/synthorg

Length of output: 1557


🏁 Script executed:

cat -n web/src/components/ui/input-field.tsx | sed -n '1,50p'

Repository: Aureliolo/synthorg

Length of output: 2024


Fix intermediate float value normalization during editing (prevents input collapse).

Line 183 binds a normalized displayValue that strips trailing .0 from float values. Combined with Lines 110-115, intermediate states like 10.0 trigger the normalization regex during typing, causing the input to collapse mid-flow when entering decimals (e.g., typing 10.05 through intermediate 10.0). Add an isEditing state to gate normalization—only apply it when the field is not focused, preserving the user's intermediate keystrokes.

Proposed fix
 export function SettingField({ definition, value, onChange, disabled }: SettingFieldProps) {
   const [validationError, setValidationError] = useState<string | null>(null)
+  const [isEditing, setIsEditing] = useState(false)
   const compositeKey = `${definition.namespace}/${definition.key}`
   const isArraySetting = SIMPLE_ARRAY_SETTINGS.has(compositeKey)
@@
   const displayValue = useMemo(() => {
-    if (definition.type === 'float' && /^-?\d+\.0+$/.test(value)) {
+    if (!isEditing && definition.type === 'float' && /^-?\d+\.0+$/.test(value)) {
       return value.replace(/\.0+$/, '')
     }
     return value
-  }, [definition.type, value])
+  }, [definition.type, value, isEditing])
@@
   return (
     <InputField
       label=""
       type={inputType}
       value={displayValue}
+      onFocus={() => setIsEditing(true)}
       onChange={(e) => {
         onChange(e.target.value)
         setValidationError(null)
       }}
-      onBlur={() => {
-        const err = validate(value)
+      onBlur={(e) => {
+        setIsEditing(false)
+        const err = validate(e.target.value)
         setValidationError(err)
       }}
       disabled={disabled}
       error={validationError}
     />
   )
 }
📝 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 displayValue = useMemo(() => {
if (definition.type === 'float' && /^-?\d+\.0+$/.test(value)) {
return value.replace(/\.0+$/, '')
}
return value
}, [definition.type, value])
export function SettingField({ definition, value, onChange, disabled }: SettingFieldProps) {
const [validationError, setValidationError] = useState<string | null>(null)
const [isEditing, setIsEditing] = useState(false)
const compositeKey = `${definition.namespace}/${definition.key}`
const isArraySetting = SIMPLE_ARRAY_SETTINGS.has(compositeKey)
const displayValue = useMemo(() => {
if (!isEditing && definition.type === 'float' && /^-?\d+\.0+$/.test(value)) {
return value.replace(/\.0+$/, '')
}
return value
}, [definition.type, value, isEditing])
return (
<InputField
label=""
type={inputType}
value={displayValue}
onFocus={() => setIsEditing(true)}
onChange={(e) => {
onChange(e.target.value)
setValidationError(null)
}}
onBlur={(e) => {
setIsEditing(false)
const err = validate(e.target.value)
setValidationError(err)
}}
disabled={disabled}
error={validationError}
/>
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/settings/SettingField.tsx` around lines 110 - 115, The current
useMemo for displayValue strips trailing “.0” during editing causing input
collapse; introduce an isEditing boolean state on the component and set it true
on focus and false on blur for the input element, then update the useMemo
(displayValue) to only apply the /^-?\d+\.0+$/ replacement when definition.type
=== 'float' AND isEditing is false, otherwise return the raw value; ensure the
input uses displayValue for rendering but focus/blur handlers update isEditing
so intermediate float keystrokes (e.g., typing 10.05) are preserved.

Round 2 (12 items):
- Replace text-[9px] with text-micro token (AgentNode lead badge)
- Replace bespoke shadows with --so-shadow-card token (CeoNode)
- Fix ARIA label plural: '1 agents' -> '1 agent' (DepartmentGroupNode)
- Replace pointer-events-none with cursor-not-allowed on add-agent CTA
- Restore focus to previous element on search overlay close
- Size owner nodes correctly in empty-state layout path
- Remove owner from VALID_NODE_TYPES (context menu mismatch)
- Use backend utilization_percent instead of mismatched 7d/monthly calc
- Guard collapsed-department filter to hierarchy view mode only
- Make YAML editor textarea readOnly while #1081 gated
- Show degraded status when health unknown + WS exhausted (StatusBar)
- Use scale: property instead of transform: for drag styles (global.css)

Round 3 (8 items):
- Sanitize title in ConfirmDialog log call
- Sanitize event_type in useGlobalNotifications catch
- Remove unused existingNames/onCreate from DepartmentCreateDialog props
- Use sanitizeForLog for unknown WS event type (agents store)
- Replace shadow-sm shadow-warning/20 with shadow token (OwnerNode)
- Make minimap dept color theme-aware via color-mix
- Add visible coming-soon description text (DepartmentEditDrawer)
- Replace hand-rolled particle toggle with shared SegmentedControl

Also: fix allNodes mock in OrgChartPage test
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

Caution

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

⚠️ Outside diff range comments (1)
web/src/pages/org-edit/DepartmentsTab.tsx (1)

222-225: ⚠️ Potential issue | 🟡 Minor

Update the gated-flow copy so it matches what users can actually do.

These messages still tell operators to create/add a department, but this screen disables department creation until #1081 lands. Reword them to point at Team Pack or budget rebalancing instead of an unavailable action.

Also applies to: 303-309

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

In `@web/src/pages/org-edit/DepartmentsTab.tsx` around lines 222 - 225, Update the
gated-flow copy in DepartmentsTab: find the EmptyState usage in
DepartmentsTab.tsx (the JSX element named EmptyState around the
title/description props and the other similar block at lines ~303-309) and
replace the title/description that tell operators to "Create your first
department" or "add a department" with copy that reflects available actions
(e.g., reference checking Team Pack or rebalancing budgets) — ensure the new
strings do not suggest department creation is possible until feature `#1081` lands
and keep tone consistent with other UI messages.
♻️ Duplicate comments (4)
web/src/components/layout/StatusBar.tsx (1)

37-64: ⚠️ Potential issue | 🟠 Major

checking... still masks startup connectivity failures.

If the first /health poll never succeeds, healthStatus stays unknown, so this branch still wins while the socket is disconnected but retrying. That leaves the pill stuck on “checking...” until reconnect exhaustion instead of surfacing the reconnecting/offline state after the first failed poll attempt.

💡 One way to preserve the initial grey state without hiding reconnects
 function resolveCombinedStatus(
   healthStatus: SystemStatus,
   wsConnected: boolean,
   wsReconnectExhausted: boolean,
+  healthCheckedOnce: boolean,
 ): { color: string; label: string } {
   if (healthStatus === 'down') {
     return { color: 'bg-danger', label: 'system down' }
   }
   if (healthStatus === 'degraded') {
     return { color: 'bg-warning', label: 'system degraded' }
   }
-  if (healthStatus === 'unknown') {
-    // If WebSocket reconnection is exhausted and we still have no
-    // health response, the backend is likely unreachable -- show an
-    // error state instead of "checking..." forever.
-    if (wsReconnectExhausted) {
-      return { color: 'bg-danger', label: 'unable to connect' }
-    }
+  if (healthStatus === 'unknown' && !healthCheckedOnce) {
     return { color: 'bg-muted-foreground', label: 'checking...' }
   }
   if (wsReconnectExhausted) {
-    return { color: 'bg-danger', label: 'live stream offline' }
+    return {
+      color: 'bg-danger',
+      label: healthStatus === 'unknown' ? 'unable to connect' : 'live stream offline',
+    }
   }
   if (!wsConnected) {
     return { color: 'bg-warning animate-pulse', label: 'reconnecting' }
   }
+  if (healthStatus === 'unknown') {
+    return { color: 'bg-muted-foreground', label: 'checking...' }
+  }
   return { color: 'bg-success', label: 'all systems normal' }
 }
 
 const [healthStatus, setHealthStatus] = useState<SystemStatus>('unknown')
+const [healthCheckedOnce, setHealthCheckedOnce] = useState(false)

 const pollHealth = useCallback(async () => {
   try {
     const health: HealthStatus = await getHealth()
     setHealthStatus(health.status)
   } catch {
     // Preserve last known state on transient failures; only real health
     // payloads should set 'degraded' or 'down'
+  } finally {
+    setHealthCheckedOnce(true)
   }
 }, [])

-const statusCfg = resolveCombinedStatus(healthStatus, wsConnected, wsReconnectExhausted)
+const statusCfg = resolveCombinedStatus(
+  healthStatus,
+  wsConnected,
+  wsReconnectExhausted,
+  healthCheckedOnce,
+)

Also applies to: 88-117

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

In `@web/src/components/layout/StatusBar.tsx` around lines 37 - 64, The pill stays
"checking..." even when the first health poll has failed because
resolveCombinedStatus only knows healthStatus but not whether a health poll has
ever completed; add a boolean parameter (e.g., hasPolledHealth or
healthAttempted) to resolveCombinedStatus and update its callers, then change
the unknown branch to surface connection state when a poll has already been
attempted: if healthStatus === 'unknown' && wsReconnectExhausted ->
{bg-danger,'unable to connect'}; else if healthStatus === 'unknown' &&
hasPolledHealth && !wsConnected -> {bg-warning animate-pulse,'reconnecting'};
otherwise keep {bg-muted-foreground,'checking...'} so initial grey remains but
failed polls show reconnecting/offline.
web/src/pages/org-edit/DepartmentsTab.tsx (1)

212-217: ⚠️ Potential issue | 🟠 Major

Don’t hide the #1081 explanation behind a disabled button title.

A disabled button’s title is not reliably exposed to keyboard, touch, or screen-reader users, so this can read as a silent dead end. Surface the reason in visible helper text or via a non-disabled tooltip trigger next to the button, and use the same pattern for the gated dialog CTA too.

Also applies to: 266-271

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

In `@web/src/pages/org-edit/DepartmentsTab.tsx` around lines 212 - 217, The
disabled primary Button currently hides the `#1081` explanation in its title
(Button with onClick={() => setCreateOpen(true)} and
title={ORG_EDIT_COMING_SOON_TOOLTIP}), which is inaccessible to
keyboard/screen-reader users; instead keep the button visually indicating the
gated state but remove reliance on the disabled title — add visible helper text
or an always-focusable tooltip trigger next to the Button that exposes
ORG_EDIT_COMING_SOON_TOOLTIP, or add an aria-describedby target element with
visible explanatory text; apply the same pattern to the other gated CTA
referenced around the create dialog (lines with the other Button at 266-271) so
both use a non-disabled accessible mechanism to surface the `#1081` explanation.
web/src/pages/org/DepartmentGroupNode.tsx (1)

207-219: ⚠️ Potential issue | 🟡 Minor

pointer-events-none still blocks the tooltip on the empty-state "Add agent" chip.

Line 207's wrapper has pointer-events-none, preventing users from hovering the title tooltip on line 213. The populated-state variant at line 231 correctly omits pointer-events-none. Apply the same fix here.

♻️ Suggested fix
       {isEmpty && (
-        <div className="pointer-events-none flex flex-1 flex-col items-center justify-center gap-2 pb-2 text-text-muted">
+        <div className="flex flex-1 flex-col items-center justify-center gap-2 pb-2 text-text-muted">
           <Users className="size-5" aria-hidden="true" />
           <span className="font-sans text-xs">No agents yet</span>
           {showAddAgentButton && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org/DepartmentGroupNode.tsx` around lines 207 - 219, The
empty-state wrapper div currently has the CSS class pointer-events-none which
blocks hover interactions for the "Add agent" span (the title tooltip) when
showAddAgentButton is true; update the DepartmentGroupNode empty-state rendering
so the wrapper does not include pointer-events-none (or conditionally omit it
when showAddAgentButton is true) so the Add agent span (the element with
title="Add agent -- coming soon (`#1081`)" and the Plus icon) can receive hover
events and show its tooltip.
web/src/pages/org/layout.ts (1)

109-123: ⚠️ Potential issue | 🟠 Major

Keep owner nodes on a dedicated row in the no-agent fast path.

When agentLeafNodes.length === 0, this branch still drops owners into the same grid as empty departments and resets them to 160x80. In an owner + empty-department org, that gives the owner card the wrong hitbox/edge anchors and turns the owner→department hierarchy into a side-by-side row instead of a top-level owner row.

🐛 Suggested fix
  const agentLeafNodes = leafNodes.filter((n) => n.type !== 'owner')
  if (agentLeafNodes.length === 0) {
-    return nodes.map((n, i) => {
-      const major = i % 3
-      const minor = Math.floor(i / 3)
-      const x = major * 260
-      const y = minor * 180
-      const w = n.type === 'owner' ? DEFAULT_NODE_WIDTH : EMPTY_GROUP_MIN_WIDTH
-      const h = n.type === 'owner' ? DEFAULT_NODE_HEIGHT : EMPTY_GROUP_HEIGHT
-      return {
-        ...n,
-        position: { x, y },
-        width: w,
-        height: h,
-        style: { ...n.style, width: w, height: h },
-      }
-    })
+    const owners = nodes.filter((n) => n.type === 'owner')
+    const departments = nodes.filter((n) => n.type !== 'owner')
+    const ownerRowHeight =
+      owners.length > 0
+        ? Math.max(...owners.map((n) => getNodeDim(n).h)) + DESIRED_INTER_DEPT_GAP
+        : 0
+
+    return [
+      ...owners.map((n, i) => {
+        const { w, h } = getNodeDim(n)
+        return {
+          ...n,
+          position: { x: i * (w + DEFAULT_GROUP_PADDING), y: 0 },
+          width: w,
+          height: h,
+          style: { ...n.style, width: w, height: h },
+        }
+      }),
+      ...departments.map((n, i) => {
+        const major = i % 3
+        const minor = Math.floor(i / 3)
+        return {
+          ...n,
+          position: {
+            x: major * (EMPTY_GROUP_MIN_WIDTH + DEFAULT_GROUP_PADDING),
+            y: ownerRowHeight + minor * (EMPTY_GROUP_HEIGHT + DEFAULT_GROUP_PADDING),
+          },
+          width: EMPTY_GROUP_MIN_WIDTH,
+          height: EMPTY_GROUP_HEIGHT,
+          style: {
+            ...n.style,
+            width: EMPTY_GROUP_MIN_WIDTH,
+            height: EMPTY_GROUP_HEIGHT,
+          },
+        }
+      }),
+    ]
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/org/layout.ts` around lines 109 - 123, When
agentLeafNodes.length === 0, split nodes into owners and non-owners and lay out
owners on a dedicated top row using DEFAULT_NODE_WIDTH and DEFAULT_NODE_HEIGHT
while laying out other (empty-department) nodes on the subsequent grid row(s)
using EMPTY_GROUP_MIN_WIDTH and EMPTY_GROUP_HEIGHT; update the mapping logic
(the nodes.map branch) to compute owner positions (x based on column, y = 0) and
non-owner positions with y offset (e.g., +180 per row), and ensure
style.width/style.height and width/height fields use the appropriate constants
(reference agentLeafNodes, leafNodes, DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT,
EMPTY_GROUP_MIN_WIDTH, EMPTY_GROUP_HEIGHT).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/layout/StatusBar.tsx`:
- Around line 154-157: Replace the local UI primitives Dot and StatusItem used
in the idle metric pill (where dataLoaded and idleAgents are rendered) with the
shared status/stat components from the design system (the ui/status or ui/stat
primitives) so the pill reuses the system-wide styling and accessibility; map
the existing props (color, children text like `${idleAgents} idle` or '--') to
the shared component API, ensure aria/text semantics remain the same, and remove
or stop importing the local Dot/StatusItem once all usages are migrated; apply
the same replacement for the other pill (the combined health trigger) that
currently extends the local primitives so both places use the shared ui
components.

In `@web/src/components/ui/confirm-dialog.tsx`:
- Around line 93-96: The button's disabled prop uses submittingRef.current which
doesn't trigger re-renders, causing a visual mismatch; replace the ref with
state: introduce a boolean state (e.g., isSubmitting via useState) and use
isSubmitting || loading for the disabled prop and early-return guard inside the
onClick handler, set isSubmitting = true at start of the async action and reset
to false in finally; update all references to submittingRef.current to
isSubmitting (and any assignments to use the setIsSubmitting setter) so the
button visually disables immediately when submitting starts.
- Around line 48-56: The dialog's onOpenChange guard only checks loading but not
submittingRef.current, allowing Escape/backdrop to close the dialog during the
window after submittingRef.current is set but before loading updates. Update the
onOpenChange handler to also check submittingRef.current (the same condition
used in the confirm click handler) and prevent closing when either
submittingRef.current or loading is truthy; reference the submittingRef ref and
the existing loading flag inside the onOpenChange closure so the close lock
behavior matches the confirm handler.

In `@web/src/hooks/useGlobalNotifications.ts`:
- Around line 47-63: The dedupe ref lastSetupErrorRef is never reset when the
transport clears, so repeated failed connect attempts that never reach connected
are suppressed; update the useEffect that watches setupError (and/or transport
state) to explicitly clear lastSetupErrorRef.current = null when setupError is
null (i.e., on transport reset/before a new connect), and keep the existing
branch that logs and toasts when setupError is non-null and different from
lastSetupErrorRef.current; reference the existing symbols lastSetupErrorRef,
setupError, sanitizeForLog, and useToastStore.add in your change.

In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx`:
- Around line 101-104: Extract the repeated expression health?.agent_count ?? 0
into a local constant (e.g., agentCount) inside DepartmentEditDrawer (or just
before the JSX return) and then use that constant in both places in the JSX: the
numeric display and the pluralization logic, replacing both occurrences of
health?.agent_count ?? 0 with agentCount to improve clarity and avoid double
evaluation.

In `@web/src/pages/org-edit/YamlEditorPanel.tsx`:
- Line 65: The aria-label on the YAML editor in YamlEditorPanel.tsx currently
contains an internal issue reference ("YAML editor (read-only until `#1081`)");
update the aria-label on the editor component to a clear, user-facing
description without the issue number (e.g., "YAML editor, read-only" or
"Read-only YAML editor") so screen readers get a descriptive label; locate the
aria-label attribute in the YamlEditorPanel component and replace the string
accordingly.
- Around line 59-66: Replace the plain <textarea> YAML editor with the app's
CodeMirror component by swapping it for LazyCodeMirrorEditor (or
CodeMirrorEditor if not lazy) and pass the same value (yamlText), onChange
handler (handleChange), readOnly prop, aria-label, and className/props needed;
ensure the editor is configured for YAML mode, lineNumbers, bracketMatching,
design-token dark theme, and supports readOnly natively (use
LazyCodeMirrorEditor with Suspense if needed) so the UI gains syntax
highlighting and editor features while preserving existing bindings.

In `@web/src/pages/org/OrgChartSearchOverlay.tsx`:
- Around line 72-76: The Escape key handler in OrgChartSearchOverlay's onKeyDown
currently calls onClose() but doesn't stop the event, causing OrgChartPage's
window-level Escape handler to run and double-close; update the onKeyDown
handler (in OrgChartSearchOverlay.tsx) to call e.stopPropagation() (before or
alongside e.preventDefault()) when e.key === 'Escape' so the event doesn't
bubble to the window listener that also triggers onClose() in OrgChartPage.
- Around line 67-81: Replace the raw <input> with the shared InputField
component: import InputField from web/src/components/ui/InputField and render
<InputField value={query} onChange={(v)=>onQueryChange(v)} onKeyDown={(e)=>{ if
(e.key==='Escape'){ e.preventDefault(); onClose(); } }} placeholder="Search
agents, roles, departments..." aria-label="Search org chart" ref={inputRef}
className="min-w-0 flex-1 bg-transparent text-sm text-foreground
placeholder:text-text-muted focus:outline-none" /> (or use the InputField's
supported ref/inputRef prop name if different); keep the controlled value
(query), event handlers (onQueryChange, onClose), and accessibility props, and
remove the original raw <input> so the page reuses the shared UI primitive.
- Line 62: The TSX in OrgChartSearchOverlay currently uses hardcoded utilities
'min-w-[320px] max-w-[480px]'; replace these with your design-token/scale
classes (e.g., use the project's Tailwind/design token equivalents such as
min-w-80 (20rem / 320px) and a matching token for 480px or the appropriate max-w
token like max-w-120 or a named token like max-w-lg) so layout widths come from
the spacing scale rather than arbitrary pixel values; update the class string in
OrgChartSearchOverlay to reference those token/scale class names.

In `@web/src/stores/agents.ts`:
- Around line 292-301: The monotonicity check currently allows equality
(afterValid <= beforeValid), which treats no-op trims as successful; update the
check in the monotonic variable to require a strict reduction (use afterValid <
beforeValid) so that before and after are only set when afterValid is strictly
less than beforeValid; adjust any downstream code that relies on the monotonic
flag if necessary (symbols to update: monotonic, beforeValid, afterValid,
before, after).

In `@web/src/styles/global.css`:
- Line 166: Add the missing CSS custom property --so-opacity-dragging to the
design system by declaring it in :root inside design-tokens.css (so themes can
override it); set a sensible default (matching the current fallback 0.6) and
include a short comment indicating it controls drag opacity for components that
use opacity: var(--so-opacity-dragging, 0.6) so Theme tokens can override it
later.

---

Outside diff comments:
In `@web/src/pages/org-edit/DepartmentsTab.tsx`:
- Around line 222-225: Update the gated-flow copy in DepartmentsTab: find the
EmptyState usage in DepartmentsTab.tsx (the JSX element named EmptyState around
the title/description props and the other similar block at lines ~303-309) and
replace the title/description that tell operators to "Create your first
department" or "add a department" with copy that reflects available actions
(e.g., reference checking Team Pack or rebalancing budgets) — ensure the new
strings do not suggest department creation is possible until feature `#1081` lands
and keep tone consistent with other UI messages.

---

Duplicate comments:
In `@web/src/components/layout/StatusBar.tsx`:
- Around line 37-64: The pill stays "checking..." even when the first health
poll has failed because resolveCombinedStatus only knows healthStatus but not
whether a health poll has ever completed; add a boolean parameter (e.g.,
hasPolledHealth or healthAttempted) to resolveCombinedStatus and update its
callers, then change the unknown branch to surface connection state when a poll
has already been attempted: if healthStatus === 'unknown' &&
wsReconnectExhausted -> {bg-danger,'unable to connect'}; else if healthStatus
=== 'unknown' && hasPolledHealth && !wsConnected -> {bg-warning
animate-pulse,'reconnecting'}; otherwise keep
{bg-muted-foreground,'checking...'} so initial grey remains but failed polls
show reconnecting/offline.

In `@web/src/pages/org-edit/DepartmentsTab.tsx`:
- Around line 212-217: The disabled primary Button currently hides the `#1081`
explanation in its title (Button with onClick={() => setCreateOpen(true)} and
title={ORG_EDIT_COMING_SOON_TOOLTIP}), which is inaccessible to
keyboard/screen-reader users; instead keep the button visually indicating the
gated state but remove reliance on the disabled title — add visible helper text
or an always-focusable tooltip trigger next to the Button that exposes
ORG_EDIT_COMING_SOON_TOOLTIP, or add an aria-describedby target element with
visible explanatory text; apply the same pattern to the other gated CTA
referenced around the create dialog (lines with the other Button at 266-271) so
both use a non-disabled accessible mechanism to surface the `#1081` explanation.

In `@web/src/pages/org/DepartmentGroupNode.tsx`:
- Around line 207-219: The empty-state wrapper div currently has the CSS class
pointer-events-none which blocks hover interactions for the "Add agent" span
(the title tooltip) when showAddAgentButton is true; update the
DepartmentGroupNode empty-state rendering so the wrapper does not include
pointer-events-none (or conditionally omit it when showAddAgentButton is true)
so the Add agent span (the element with title="Add agent -- coming soon (`#1081`)"
and the Plus icon) can receive hover events and show its tooltip.

In `@web/src/pages/org/layout.ts`:
- Around line 109-123: When agentLeafNodes.length === 0, split nodes into owners
and non-owners and lay out owners on a dedicated top row using
DEFAULT_NODE_WIDTH and DEFAULT_NODE_HEIGHT while laying out other
(empty-department) nodes on the subsequent grid row(s) using
EMPTY_GROUP_MIN_WIDTH and EMPTY_GROUP_HEIGHT; update the mapping logic (the
nodes.map branch) to compute owner positions (x based on column, y = 0) and
non-owner positions with y offset (e.g., +180 per row), and ensure
style.width/style.height and width/height fields use the appropriate constants
(reference agentLeafNodes, leafNodes, DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT,
EMPTY_GROUP_MIN_WIDTH, EMPTY_GROUP_HEIGHT).
🪄 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: 0ff18154-d80c-4d9b-be2f-217fcebfa625

📥 Commits

Reviewing files that changed from the base of the PR and between 1622523 and 0fcc30e.

📒 Files selected for processing (21)
  • web/src/__tests__/pages/org-edit/DepartmentCreateDialog.test.tsx
  • web/src/__tests__/pages/org/OrgChartPage.test.tsx
  • web/src/components/layout/StatusBar.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/hooks/useGlobalNotifications.ts
  • web/src/pages/org-edit/DepartmentCreateDialog.stories.tsx
  • web/src/pages/org-edit/DepartmentCreateDialog.tsx
  • web/src/pages/org-edit/DepartmentEditDrawer.tsx
  • web/src/pages/org-edit/DepartmentsTab.tsx
  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/pages/org/AgentNode.tsx
  • web/src/pages/org/CeoNode.tsx
  • web/src/pages/org/DepartmentGroupNode.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
  • web/src/pages/org/OrgChartToolbar.tsx
  • web/src/pages/org/OwnerNode.tsx
  • web/src/pages/org/build-org-tree.ts
  • web/src/pages/org/layout.ts
  • web/src/stores/agents.ts
  • web/src/styles/design-tokens.css
  • web/src/styles/global.css
💤 Files with no reviewable changes (1)
  • web/src/pages/org-edit/DepartmentCreateDialog.stories.tsx

Comment on lines +154 to +157
<StatusItem>
<Dot color="bg-muted-foreground" />
<span>{dataLoaded ? `${idleAgents} idle` : '--'}</span>
</StatusItem>
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 the shared status/stat primitives for these new pills.

The idle metric and combined health trigger both extend the local Dot/StatusItem path instead of the shared UI components, so this file drifts further from the design-system surface that already owns status presentation and accessibility.

As per coding guidelines, "TypeScript/React code must ALWAYS reuse existing components from web/src/components/ui/ before creating new ones."

Also applies to: 190-199

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

In `@web/src/components/layout/StatusBar.tsx` around lines 154 - 157, Replace the
local UI primitives Dot and StatusItem used in the idle metric pill (where
dataLoaded and idleAgents are rendered) with the shared status/stat components
from the design system (the ui/status or ui/stat primitives) so the pill reuses
the system-wide styling and accessibility; map the existing props (color,
children text like `${idleAgents} idle` or '--') to the shared component API,
ensure aria/text semantics remain the same, and remove or stop importing the
local Dot/StatusItem once all usages are migrated; apply the same replacement
for the other pill (the combined health trigger) that currently extends the
local primitives so both places use the shared ui components.

Comment on lines +47 to +63
const lastSetupErrorRef = useRef<string | null>(null)
useEffect(() => {
if (setupError && setupError !== lastSetupErrorRef.current) {
lastSetupErrorRef.current = setupError
// `setupError` originates from WebSocket transport errors, which can
// surface messages derived from untrusted response bodies; sanitize
// before embedding in the structured log.
log.warn('Global notifications WebSocket setup failed', {
setupError: sanitizeForLog(setupError),
})
useToastStore.getState().add({
variant: 'warning',
title: 'Live notifications unavailable',
description: 'You may miss real-time updates. Try refreshing the page.',
})
}
}, [setupError])
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

Reset the dedupe refs when the transport state clears.

web/src/hooks/useWebSocket.ts, Lines 65-77, clears setupError to null before each new connect() attempt, and web/src/stores/websocket.ts, Lines 133-146, resets reconnectExhausted to false at the start of doConnect(). Because this hook only re-arms its refs after connected flips true, a retry that fails again without ever reconnecting suppresses the next toast entirely.

Proposed fix
   const lastSetupErrorRef = useRef<string | null>(null)
   useEffect(() => {
-    if (setupError && setupError !== lastSetupErrorRef.current) {
-      lastSetupErrorRef.current = setupError
+    if (setupError === null) {
+      lastSetupErrorRef.current = null
+      return
+    }
+    if (setupError !== lastSetupErrorRef.current) {
+      lastSetupErrorRef.current = setupError
       // `setupError` originates from WebSocket transport errors, which can
       // surface messages derived from untrusted response bodies; sanitize
       // before embedding in the structured log.
       log.warn('Global notifications WebSocket setup failed', {
         setupError: sanitizeForLog(setupError),
@@
   const reconnectExhaustedRef = useRef(false)
   useEffect(() => {
-    if (reconnectExhausted && !reconnectExhaustedRef.current) {
-      reconnectExhaustedRef.current = true
+    if (!reconnectExhausted) {
+      reconnectExhaustedRef.current = false
+      return
+    }
+    if (!reconnectExhaustedRef.current) {
+      reconnectExhaustedRef.current = true
       log.error('Global notifications reconnect exhausted')
       useToastStore.getState().add({
         variant: 'error',
         title: 'Live notifications disconnected',

Also applies to: 67-88

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

In `@web/src/hooks/useGlobalNotifications.ts` around lines 47 - 63, The dedupe ref
lastSetupErrorRef is never reset when the transport clears, so repeated failed
connect attempts that never reach connected are suppressed; update the useEffect
that watches setupError (and/or transport state) to explicitly clear
lastSetupErrorRef.current = null when setupError is null (i.e., on transport
reset/before a new connect), and keep the existing branch that logs and toasts
when setupError is non-null and different from lastSetupErrorRef.current;
reference the existing symbols lastSetupErrorRef, setupError, sanitizeForLog,
and useToastStore.add in your change.

Comment on lines +101 to +104
<div className="inline-flex items-center gap-1.5 text-compact text-text-secondary">
<Users className="size-3.5" aria-hidden="true" />
{(health?.agent_count ?? 0)} agent{(health?.agent_count ?? 0) === 1 ? '' : 's'}
</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

Extract agent count to avoid repeated evaluation.

The expression health?.agent_count ?? 0 is evaluated twice for display and pluralization. Consider extracting it to a local constant for clarity.

♻️ Proposed refactor
+            const agentCount = health?.agent_count ?? 0
             <div className="inline-flex items-center gap-1.5 text-compact text-text-secondary">
               <Users className="size-3.5" aria-hidden="true" />
-              {(health?.agent_count ?? 0)} agent{(health?.agent_count ?? 0) === 1 ? '' : 's'}
+              {agentCount} agent{agentCount === 1 ? '' : 's'}
             </div>

Or inline with a render variable just before the JSX return.

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

In `@web/src/pages/org-edit/DepartmentEditDrawer.tsx` around lines 101 - 104,
Extract the repeated expression health?.agent_count ?? 0 into a local constant
(e.g., agentCount) inside DepartmentEditDrawer (or just before the JSX return)
and then use that constant in both places in the JSX: the numeric display and
the pluralization logic, replacing both occurrences of health?.agent_count ?? 0
with agentCount to improve clarity and avoid double evaluation.

className={cn(
'absolute left-1/2 top-4 z-10 -translate-x-1/2',
'flex items-center gap-2 rounded-lg border border-border bg-card/95 px-3 py-2 shadow-lg backdrop-blur',
'min-w-[320px] max-w-[480px]',
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

Replace hardcoded pixel width utilities with token/scale classes.

min-w-[320px] max-w-[480px] introduces hardcoded layout pixels in TSX. Use Tailwind scale classes (or design tokens) instead.

🔧 Proposed fix
-        'min-w-[320px] max-w-[480px]',
+        'min-w-80 max-w-lg',

As per coding guidelines "Use density-aware spacing tokens ... or standard Tailwind spacing, never hardcode pixel values for layout spacing."

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

In `@web/src/pages/org/OrgChartSearchOverlay.tsx` at line 62, The TSX in
OrgChartSearchOverlay currently uses hardcoded utilities 'min-w-[320px]
max-w-[480px]'; replace these with your design-token/scale classes (e.g., use
the project's Tailwind/design token equivalents such as min-w-80 (20rem / 320px)
and a matching token for 480px or the appropriate max-w token like max-w-120 or
a named token like max-w-lg) so layout widths come from the spacing scale rather
than arbitrary pixel values; update the class string in OrgChartSearchOverlay to
reference those token/scale class names.

Comment on lines +67 to +81
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => onQueryChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.preventDefault()
onClose()
}
}}
placeholder="Search agents, roles, departments..."
aria-label="Search org chart"
className="min-w-0 flex-1 bg-transparent text-sm text-foreground placeholder:text-text-muted focus:outline-none"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use shared InputField instead of a raw <input> in page code.

This page-level text input bypasses the shared UI primitive policy.

As per coding guidelines "Always reuse existing components from web/src/components/ui/ before creating new components" and "Use InputField component for labeled text input with error/hint display."

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

In `@web/src/pages/org/OrgChartSearchOverlay.tsx` around lines 67 - 81, Replace
the raw <input> with the shared InputField component: import InputField from
web/src/components/ui/InputField and render <InputField value={query}
onChange={(v)=>onQueryChange(v)} onKeyDown={(e)=>{ if (e.key==='Escape'){
e.preventDefault(); onClose(); } }} placeholder="Search agents, roles,
departments..." aria-label="Search org chart" ref={inputRef} className="min-w-0
flex-1 bg-transparent text-sm text-foreground placeholder:text-text-muted
focus:outline-none" /> (or use the InputField's supported ref/inputRef prop name
if different); keep the controlled value (query), event handlers (onQueryChange,
onClose), and accessibility props, and remove the original raw <input> so the
page reuses the shared UI primitive.

/* Use the dedicated CSS `scale` property instead of `transform` so we
do not overwrite React Flow's `transform: translate(...)` positioning. */
.react-flow__node.dragging {
opacity: var(--so-opacity-dragging, 0.6);
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

Define --so-opacity-dragging token in design-tokens.css.

This variable references a fallback value of 0.6, but the token itself isn't defined anywhere in the design system. This prevents theme customization of drag opacity. Add the token to :root in design-tokens.css for consistency.

🎨 Suggested addition to design-tokens.css
  /* -- Shadows -- */
  --so-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.25);
  --so-shadow-card-hover: 0 4px 24px rgba(56, 189, 248, 0.08);
  --so-shadow-health-glow: 0 0 8px rgba(56, 189, 248, 0.3);

+ /* -- Opacity -- */
+ --so-opacity-dragging: 0.6;
+
  /* -- Transitions -- */
📝 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
opacity: var(--so-opacity-dragging, 0.6);
/* -- Shadows -- */
--so-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.25);
--so-shadow-card-hover: 0 4px 24px rgba(56, 189, 248, 0.08);
--so-shadow-health-glow: 0 0 8px rgba(56, 189, 248, 0.3);
/* -- Opacity -- */
--so-opacity-dragging: 0.6;
/* -- Transitions -- */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/styles/global.css` at line 166, Add the missing CSS custom property
--so-opacity-dragging to the design system by declaring it in :root inside
design-tokens.css (so themes can override it); set a sensible default (matching
the current fallback 0.6) and include a short comment indicating it controls
drag opacity for components that use opacity: var(--so-opacity-dragging, 0.6) so
Theme tokens can override it later.

- Switch ConfirmDialog re-entry guard from useRef to useState (ref
  doesn't trigger re-render so button stayed visually enabled)
- Tighten trim-token monotonicity check from <= to < (suppress
  no-op '100 to 100' toasts)
- Define --so-opacity-dragging token in design-tokens.css
- Stop Escape propagation in search overlay to prevent double-close
- Remove internal issue number from YAML editor ARIA label
- Update YamlEditorPanel tests for read-only state
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
web/src/pages/org/OrgChartSearchOverlay.tsx (2)

67-82: ⚠️ Potential issue | 🟠 Major

Use shared InputField instead of raw <input> at Line 67.

This page-level raw input bypasses the shared UI primitive policy for text input fields.

As per coding guidelines "Always reuse existing components from web/src/components/ui/ before creating new components" and "Use InputField component for labeled text input with error/hint display, optional multiline textarea mode."

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

In `@web/src/pages/org/OrgChartSearchOverlay.tsx` around lines 67 - 82, Replace
the raw <input> used in the OrgChartSearchOverlay with the shared InputField
component: locate the input that uses inputRef, query, onQueryChange and onClose
and swap it to <InputField ... /> supplying the same props (value={query},
onChange={(v)=>onQueryChange(v)} or onChange handler shape expected by
InputField), forward the inputRef (use ref forwarding or InputField's inputRef
prop), preserve the onKeyDown handler (checking e.key === 'Escape' to call
onClose), and keep placeholder, aria-label and relevant styling/className so the
control looks and behaves the same while using the ui primitive.

62-62: ⚠️ Potential issue | 🟠 Major

Replace hardcoded width utilities at Line 62 with scale/token classes.

min-w-[320px] max-w-[480px] is still an arbitrary pixel utility and violates the layout token/scale rule.

♻️ Proposed change
-        'min-w-[320px] max-w-[480px]',
+        'min-w-80 max-w-lg',

As per coding guidelines "Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing, never hardcode pixel values for layout spacing."

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

In `@web/src/pages/org/OrgChartSearchOverlay.tsx` at line 62, In
OrgChartSearchOverlay.tsx replace the hardcoded utility string 'min-w-[320px]
max-w-[480px]' with your design system scale/token classes (remove arbitrary px
values); update the class on the overlay/container element in
OrgChartSearchOverlay to use the appropriate min/max width tokens from our token
scale (for example use the panel/preset tokens or theme spacing tokens such as a
panel-sm/panel-md or theme(spacing.*) equivalents) so it conforms to
density-aware spacing rules (use p-card/gap tokens or standard Tailwind spacing
tokens instead of fixed pixels).
🤖 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/org-edit/YamlEditorPanel.test.tsx`:
- Around line 37-42: The test in YamlEditorPanel.test.tsx uses
screen.getByText('Save YAML').closest('button')! which hides DOM failures;
change to use screen.getByRole('button', { name: /Save YAML/i }) to locate the
button directly (remove the non-null assertion), then assert it is disabled and
that its title contains '1081'—update the assertions around the YamlEditorPanel
render and mockOnSave usage accordingly.

In `@web/src/stores/agents.ts`:
- Around line 244-247: The current type-only checks allow empty or
whitespace-only WebSocket identifiers (e.g., payload.agent_id or payload.status)
which creates runtimeStatuses[''] and lets an empty agent_name bypass the "An
agent" fallback; update all relevant validation (the agent.status_changed
handler around the agentId/status checks and the similar blocks at 276-283 and
319-323) to treat blank strings as invalid by trimming and ensuring length > 0
(e.g., check typeof x === 'string' && x.trim().length > 0), and ensure any
lookup or toast label logic (where agent_name or agentId is used) falls back to
the "An agent" label when the trimmed id/name is empty.

In `@web/src/styles/design-tokens.css`:
- Around line 77-95: Several new CSS custom properties (e.g., --so-shadow-card,
--so-shadow-card-hover, --so-shadow-health-glow, --so-minimap-bg,
--so-minimap-mask, --so-minimap-border, --so-minimap-shadow,
--so-minimap-node-dept) use hardcoded rgba() values which violates the token
rule; replace those rgba() literals with token-derived values (either existing
tokens like var(--so-accent), var(--so-warning), var(--so-shadow-card) or new
tokens you add) and express opacity via color-mix() or token alpha variants so
the values remain themeable (for example, derive hover/health glow by mixing
var(--so-accent) with transparent or using an alpha token instead of raw
rgba()). Ensure every instance of rgba(...) in these new tokens is swapped for
var(...) or color-mix(...) referencing design tokens.

---

Duplicate comments:
In `@web/src/pages/org/OrgChartSearchOverlay.tsx`:
- Around line 67-82: Replace the raw <input> used in the OrgChartSearchOverlay
with the shared InputField component: locate the input that uses inputRef,
query, onQueryChange and onClose and swap it to <InputField ... /> supplying the
same props (value={query}, onChange={(v)=>onQueryChange(v)} or onChange handler
shape expected by InputField), forward the inputRef (use ref forwarding or
InputField's inputRef prop), preserve the onKeyDown handler (checking e.key ===
'Escape' to call onClose), and keep placeholder, aria-label and relevant
styling/className so the control looks and behaves the same while using the ui
primitive.
- Line 62: In OrgChartSearchOverlay.tsx replace the hardcoded utility string
'min-w-[320px] max-w-[480px]' with your design system scale/token classes
(remove arbitrary px values); update the class on the overlay/container element
in OrgChartSearchOverlay to use the appropriate min/max width tokens from our
token scale (for example use the panel/preset tokens or theme spacing tokens
such as a panel-sm/panel-md or theme(spacing.*) equivalents) so it conforms to
density-aware spacing rules (use p-card/gap tokens or standard Tailwind spacing
tokens instead of fixed pixels).
🪄 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: f0d5c2ab-8ca8-4f26-afee-c312df688c6c

📥 Commits

Reviewing files that changed from the base of the PR and between 0fcc30e and d1c9f80.

📒 Files selected for processing (6)
  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
  • web/src/stores/agents.ts
  • web/src/styles/design-tokens.css
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Dashboard Test
  • GitHub Check: Test (Python 3.14)
  • GitHub Check: Build Backend
  • GitHub Check: Build Sandbox
  • GitHub Check: Build Web
  • GitHub Check: Dependency Review
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (6)
web/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,js,jsx}: Always use createLogger from @/lib/logger instead of bare console.warn, console.error, or console.debug in application code
Use variable name log for logger instances (e.g., const log = createLogger('module-name'))
Use log.debug() for dev-only logging (stripped in production), log.warn() for warnings, and log.error() for errors
Pass dynamic/untrusted values as separate arguments to logger calls (not interpolated into the message string) so they go through sanitizeArg
Wrap attacker-controlled fields inside structured objects with sanitizeForLog() before embedding in logs

Files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
  • web/src/stores/agents.ts
web/src/pages/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/pages/**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones
Use StatusBadge component for agent/task/system status indicators (colored dot + optional built-in label toggle)
Use MetricCard component for numeric KPIs with sparkline, change badge, progress bar
Use Sparkline component for inline SVG trend lines with color? and animated? props (used inside MetricCard or standalone)
Use SectionCard component for titled card wrapper with icon and action slot
Use AgentCard component for agent display (avatar, name, role, status, current task)
Use DeptHealthBar component for department utilization (animated fill bar + health? + agentCount)
Use ProgressGauge component for circular or linear gauge for budget/utilization (variant? defaults to 'circular', max? defaults to 100)
Use StatPill component for compact inline label + value pair
Use Avatar component for circular initials avatar with optional borderColor? prop
Use Toast / ToastContainer component for success/error/warning/info notifications with auto-dismiss queue (mount ToastContainer once in AppLayout)
Use Skeleton / SkeletonCard / SkeletonMetric / SkeletonTable / SkeletonText components for loading placeholders matching component shapes (shimmer animation, respects prefers-reduced-motion)
Use EmptyState component for no-data / no-results placeholder with icon, title, description, optional action button
Use ErrorBoundary component for React error boundary with retry -- level prop: page / section / component
Use ConfirmDialog component for confirmation modal (Radix AlertDialog) with default / destructive variants and loading state
Use CommandPalette component for global Cmd+K search (cmdk + React Router) -- mount once in AppLayout, register commands via useCommandPalette hook
Use InlineEdit component for click-to-edit text with Enter/Escape, validation, optimistic save with rollback
Use...

Files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
web/src/**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,css}: Use Tailwind semantic color classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) instead of hardcoded hex values
Use font-sans or font-mono for typography (maps to Geist tokens), never set fontFamily directly
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing, never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders instead of hardcoded values
Do NOT use rgba() with hardcoded values -- use design token variables
A PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files and catches design system violations; fix all violations before proceeding

Files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
  • web/src/styles/design-tokens.css
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
  • web/src/stores/agents.ts
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Do NOT hardcode Framer Motion transition durations -- use presets from @/lib/motion
CSS side-effect imports in TypeScript files should use type declarations (/// <reference types="vite/client" /> covers this); TypeScript 6 defaults noUncheckedSideEffectImports to true

web/src/**/*.{ts,tsx}: TypeScript/React code must ALWAYS reuse existing components from web/src/components/ui/ before creating new ones.
NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in TypeScript/React code -- use design tokens and @/lib/motion presets.
TypeScript code in the web dashboard must follow design system rules. A PostToolUse hook (scripts/check_web_design_system.py) enforces these rules on every Edit/Write to web/src/.
Web dashboard TypeScript/TSX code must pass ESLint with zero warnings (enforced by pre-commit and pre-push hooks).

Files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
  • web/src/stores/agents.ts
web/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/**/*.{test,spec}.{ts,tsx}: TypeScript files in web dashboard must use --detect-async-leaks flag in tests to detect resource leaks.
React/TypeScript code in web dashboard should use property-based testing with fast-check (fc.assert + fc.property).

Files:

  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
web/src/components/ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/components/ui/**/*.{ts,tsx}: Place new shared components in web/src/components/ui/ with descriptive kebab-case filename
Export props as a TypeScript interface in new components
Use design tokens exclusively in component code -- no hardcoded colors, fonts, or spacing
Import cn from @/lib/utils for conditional class merging in component files

Files:

  • web/src/components/ui/confirm-dialog.tsx
🧠 Learnings (54)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
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/).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:28:28.895Z
Learning: Web dashboard: see `web/CLAUDE.md` for commands, design system, and component inventory
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T18:57:46.655Z
Learning: Applies to web/package.json : Web dashboard Node.js 22+, TypeScript 6.0+, dependencies in `web/package.json`
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to web/package.json : Web dashboard Node.js 20+; dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, ESLint, vue-tsc)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to docker/{Dockerfile*,compose.yml} : Docker: Backend uses 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web uses nginxinc/nginx-unprivileged, Vue 3 SPA with PrimeVue + Tailwind CSS, SPA routing, API/WebSocket proxy to backend.
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `CodeMirrorEditor` component for CodeMirror 6 editor with JSON/YAML modes, design-token dark theme, line numbers, bracket matching, `readOnly` support

Applied to files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `InlineEdit` component for click-to-edit text with Enter/Escape, validation, optimistic save with rollback

Applied to files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `LazyCodeMirrorEditor` component for Suspense-wrapped lazy-loaded `CodeMirrorEditor` (drop-in replacement, defers ~200KB+ CodeMirror bundle)

Applied to files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)

Applied to files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components

Applied to files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/components/ui/confirm-dialog.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `InputField` component for labeled text input with error/hint display, optional multiline textarea mode

Applied to files:

  • web/src/pages/org-edit/YamlEditorPanel.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.a11y.test: 'error' | 'todo' | 'off' for a11y testing (replaces old .element and .manual); set globally in preview.tsx to enforce WCAG compliance on all stories

Applied to files:

  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Use `parameters.a11y.test: 'error' | 'todo' | 'off'` for a11y testing configuration (replaces old `.element` and `.manual`)

Applied to files:

  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.stories.{ts,tsx} : Use `parameters.a11y.test: 'error' | 'todo' | 'off'` in Storybook stories for a11y testing (Storybook 10 change)

Applied to files:

  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
📚 Learning: 2026-04-05T22:36:24.545Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-05T22:36:24.545Z
Learning: Applies to web/**/*.{test,spec}.{ts,tsx} : React/TypeScript code in web dashboard should use property-based testing with fast-check (`fc.assert` + `fc.property`).

Applied to files:

  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.stories.{ts,tsx} : Use `storybook/test` instead of `storybook/test` and `storybook/actions` instead of `storybook/addon-actions` (Storybook 10 import path changes)

Applied to files:

  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.test.{ts,tsx} : Web dashboard: Use React Hypothesis (fast-check) for property-based testing with fc.assert + fc.property

Applied to files:

  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/.storybook/preview.tsx : Set `parameters.a11y.test` globally in `preview.tsx` to enforce WCAG compliance on all stories (Storybook 10)

Applied to files:

  • web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders instead of hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Do NOT use `rgba()` with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web code — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/styles/design-tokens.css
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in component code -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use Tailwind semantic color classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) instead of hardcoded hex values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

  • web/src/styles/design-tokens.css
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `ConfirmDialog` component for confirmation modal (Radix AlertDialog) with `default` / `destructive` variants and `loading` state

Applied to files:

  • web/src/components/ui/confirm-dialog.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `ThemeToggle` component for Radix Popover with 5-axis theme controls (color, density, typography, animation, sidebar), rendered in StatusBar

Applied to files:

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

Applied to files:

  • web/src/components/ui/confirm-dialog.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones

Applied to files:

  • web/src/components/ui/confirm-dialog.tsx
  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,js,jsx} : Wrap attacker-controlled fields inside structured objects with `sanitizeForLog()` before embedding in logs

Applied to files:

  • web/src/components/ui/confirm-dialog.tsx
  • web/src/stores/agents.ts
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,js,jsx} : Pass dynamic/untrusted values as separate arguments to logger calls (not interpolated into the message string) so they go through `sanitizeArg`

Applied to files:

  • web/src/components/ui/confirm-dialog.tsx
  • web/src/stores/agents.ts
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,js,jsx} : Always use `createLogger` from `@/lib/logger` instead of bare `console.warn`, `console.error`, or `console.debug` in application code

Applied to files:

  • web/src/components/ui/confirm-dialog.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Export props as a TypeScript interface in new components

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export props as a TypeScript interface for new components

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `Drawer` component for slide-in panel (`side` prop: left or right, default right) with overlay, spring animation, focus trap, Escape-to-close, optional header (`title`), `ariaLabel` for accessible name (one of `title` or `ariaLabel` required), and `contentClassName` override

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `MobileUnsupportedOverlay` component for full-screen overlay at `<768px` viewports directing users to desktop or CLI; self-manages visibility via `useBreakpoint`

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `CommandPalette` component for global Cmd+K search (cmdk + React Router) -- mount once in AppLayout, register commands via `useCommandPalette` hook

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `color?` and `animated?` props for Sparkline component (inline SVG trend lines)

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `AgentCard` component for agent display (avatar, name, role, status, current task)

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
  • web/src/stores/agents.ts
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,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

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-05T22:36:24.545Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-05T22:36:24.545Z
Learning: Applies to web/src/**/*.{ts,tsx} : NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in TypeScript/React code -- use design tokens and `@/lib/motion` presets.

Applied to files:

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

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-01T20:43:51.878Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `TagInput` component for chip-style multi-value input with add/remove, keyboard support (Enter to add, Backspace to remove), paste splitting

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `SelectField` component for labeled select dropdown with error/hint and placeholder support

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones; refer to design system inventory (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, etc.)

Applied to files:

  • web/src/pages/org/OrgChartSearchOverlay.tsx
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)

Applied to files:

  • web/src/stores/agents.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)

Applied to files:

  • web/src/stores/agents.ts
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `Toast` / `ToastContainer` component for success/error/warning/info notifications with auto-dismiss queue (mount `ToastContainer` once in AppLayout)

Applied to files:

  • web/src/stores/agents.ts
🔇 Additional comments (9)
web/src/pages/org/OrgChartSearchOverlay.tsx (1)

72-77: Escape handling is correctly contained now.

e.preventDefault() + e.stopPropagation() before onClose() is the right behavior here and avoids duplicate close side effects from parent/global handlers.

web/src/components/ui/confirm-dialog.tsx (1)

1-121: Well-implemented migration with past concerns addressed.

The Radix → Base UI migration is correctly executed:

  • Internal submitting state with useState (not useRef) provides immediate visual feedback when disabling buttons, addressing the prior concern about ref-based guards not triggering re-renders.
  • The busy derivation (loading || submitting) is used consistently in the onOpenChange guard (line 56), button disabled props (lines 86, 94), and the click handler guard (line 96).
  • sanitizeForLog(title) properly sanitizes the attacker-controllable field before logging.
  • Design tokens are used throughout (bg-surface, p-card, border-border-bright, shadow-[var(--so-shadow-card-hover)], etc.).
  • The AlertDialog.Close render prop pattern and data-attribute-based transitions follow Base UI conventions.
web/src/pages/org-edit/YamlEditorPanel.tsx (3)

59-66: LGTM on the readOnly attribute and improved aria-label.

The readOnly attribute correctly prevents user edits, and the aria-label no longer exposes the internal issue number—both address prior feedback.

However, the previous suggestion to replace <textarea> with LazyCodeMirrorEditor for YAML syntax highlighting, line numbers, and bracket matching remains unaddressed.


74-82: Disabled button accessibility concern remains.

The title attribute tooltip isn't accessible to keyboard users since focus is suppressed on disabled buttons. The prior suggestion to add visible helper text (e.g., aria-describedby with a visible <span>) still applies.


28-32: Dead code is acceptable given the gating context.

The handleChange callback and dirty state logic are unreachable while readOnly is set, but keeping them simplifies re-enabling when issue #1081 lands. No action required.

web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx (2)

23-29: Good test coverage for read-only state and accessible labeling.

The assertion at Line 28 that aria-label must not contain # is a valuable regression guard against exposing internal issue numbers to screen reader users.


5-8: Helpful context for future maintainers.

The comment clearly documents the gating reason and where to find the original tests when #1081 lands.

web/src/stores/agents.ts (2)

116-117: Good use of the shared logger path.

Both failure branches now stay on createLogger, and the route-derived agent identifier is sanitized before it is attached as structured context. As per coding guidelines "Always use createLogger from @/lib/logger instead of bare console.warn, console.error, or console.debug in application code" and "Wrap attacker-controlled fields inside structured objects with sanitizeForLog() before embedding in logs".

Also applies to: 165-168


232-241: Nice malformed-event drop path.

The upfront payload guard and the "no actionable fields, no toast" short-circuit keep bad broker messages out of both store state and the notification queue.

Also applies to: 311-316

Comment on lines +37 to 42
it('disables Save YAML button with #1081 tooltip', () => {
render(<YamlEditorPanel config={makeCompanyConfig()} onSave={mockOnSave} saving={false} />)
const textarea = screen.getByLabelText('YAML editor')
fireEvent.change(textarea, { target: { value: 'company_name: Updated\nagents: []\ndepartments: []\n' } })
fireEvent.click(screen.getByText('Save YAML'))
await waitFor(() => {
expect(mockOnSave).toHaveBeenCalledWith(expect.objectContaining({
company_name: 'Updated',
}))
})
})

it('resets textarea to original config on Reset click', () => {
const config = makeCompanyConfig()
render(<YamlEditorPanel config={config} onSave={mockOnSave} saving={false} />)
const textarea = screen.getByLabelText('YAML editor') as HTMLTextAreaElement
const original = textarea.value
fireEvent.change(textarea, { target: { value: 'company_name: Changed\n' } })
expect(textarea.value).toBe('company_name: Changed\n')
fireEvent.click(screen.getByText('Reset'))
expect(textarea.value).toBe(original)
})

it('disables buttons when saving', () => {
render(<YamlEditorPanel config={makeCompanyConfig()} onSave={mockOnSave} saving={true} />)
expect(screen.getByText('Saving...').closest('button')).toBeDisabled()
const saveButton = screen.getByText('Save YAML').closest('button')!
expect(saveButton).toBeDisabled()
expect(saveButton.getAttribute('title') ?? '').toContain('1081')
})
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

Prefer getByRole over non-null assertion chain.

Using .closest('button')! can obscure failures if the DOM structure changes. Use getByRole for a clearer assertion and better error messages on failure.

♻️ Suggested refactor
  it('disables Save YAML button with `#1081` tooltip', () => {
    render(<YamlEditorPanel config={makeCompanyConfig()} onSave={mockOnSave} saving={false} />)
-   const saveButton = screen.getByText('Save YAML').closest('button')!
+   const saveButton = screen.getByRole('button', { name: /save yaml/i })
    expect(saveButton).toBeDisabled()
    expect(saveButton.getAttribute('title') ?? '').toContain('1081')
  })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/__tests__/pages/org-edit/YamlEditorPanel.test.tsx` around lines 37 -
42, The test in YamlEditorPanel.test.tsx uses screen.getByText('Save
YAML').closest('button')! which hides DOM failures; change to use
screen.getByRole('button', { name: /Save YAML/i }) to locate the button directly
(remove the non-null assertion), then assert it is disabled and that its title
contains '1081'—update the assertions around the YamlEditorPanel render and
mockOnSave usage accordingly.

Comment on lines +244 to +247
const agentId = payload.agent_id
const status = payload.status
if (typeof agentId !== 'string' || typeof status !== 'string') {
log.warn('agent.status_changed payload missing required fields', {
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

Treat blank WebSocket identifiers as invalid.

These checks only validate typeof ... === 'string', so '' and whitespace-only values still pass. That can create runtimeStatuses[''] for agent.status_changed, and because Line 319 uses ??, an empty agent_name still bypasses the An agent fallback and renders a blank label in the toast.

♻️ Proposed fix
-      const agentId = payload.agent_id
+      const agentId =
+        typeof payload.agent_id === 'string' && payload.agent_id.trim().length > 0
+          ? payload.agent_id.trim()
+          : null
       const status = payload.status
-      if (typeof agentId !== 'string' || typeof status !== 'string') {
+      if (agentId === null || typeof status !== 'string') {
         log.warn('agent.status_changed payload missing required fields', {
-          hasAgentId: typeof agentId === 'string',
+          hasAgentId: agentId !== null,
           hasStatus: typeof status === 'string',
         })
         return
       }
...
-      const agentName =
-        typeof agentNameRaw === 'string'
+      const agentName =
+        typeof agentNameRaw === 'string' && agentNameRaw.trim().length > 0
           // Length-bound defence-in-depth: the backend payload is trusted but
           // bounding the visual blast radius of a malformed or oversized
           // agent_name is cheap insurance. React escapes text content, so
           // there is no XSS risk -- this is purely display hygiene.
-          ? agentNameRaw.slice(0, 64)
+          ? agentNameRaw.trim().slice(0, 64)
           : null

Also applies to: 276-283, 319-323

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

In `@web/src/stores/agents.ts` around lines 244 - 247, The current type-only
checks allow empty or whitespace-only WebSocket identifiers (e.g.,
payload.agent_id or payload.status) which creates runtimeStatuses[''] and lets
an empty agent_name bypass the "An agent" fallback; update all relevant
validation (the agent.status_changed handler around the agentId/status checks
and the similar blocks at 276-283 and 319-323) to treat blank strings as invalid
by trimming and ensuring length > 0 (e.g., check typeof x === 'string' &&
x.trim().length > 0), and ensure any lookup or toast label logic (where
agent_name or agentId is used) falls back to the "An agent" label when the
trimmed id/name is empty.

- Reject blank/whitespace-only WS identifiers in agents store
- Replace hardcoded rgba minimap tokens with color-mix from theme vars
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

276-283: ⚠️ Potential issue | 🟡 Minor

Blank agentName values still bypass the fallback.

The fix applied at lines 244-247 for agentId was not carried through here. An empty or whitespace-only agentNameRaw passes the typeof check, producing agentName = '' which bypasses the ?? 'An agent' fallback (nullish coalescing doesn't catch falsy strings). This results in toasts like " personality trimmed: 500 → 400 tokens" with a leading space or empty label.

♻️ Proposed fix
       const agentName =
-        typeof agentNameRaw === 'string'
+        typeof agentNameRaw === 'string' && agentNameRaw.trim().length > 0
           // Length-bound defence-in-depth: the backend payload is trusted but
           // bounding the visual blast radius of a malformed or oversized
           // agent_name is cheap insurance. React escapes text content, so
           // there is no XSS risk -- this is purely display hygiene.
-          ? agentNameRaw.slice(0, 64)
+          ? agentNameRaw.trim().slice(0, 64)
           : null

Also applies to: 319-323

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

In `@web/src/stores/agents.ts` around lines 276 - 283, agentName currently only
checks typeof agentNameRaw, so empty or whitespace strings slip through and
bypass the fallback; update the logic that builds agentName (similar to the
agentId fix at lines ~244-247) to trim agentNameRaw, treat empty/whitespace-only
results as null, and then slice to 64 chars if present (i.e., use
agentNameRaw?.trim() -> if non-empty then slice(0,64) else null); apply the same
change to the second occurrence around lines ~319-323 to ensure the "?? 'An
agent'" fallback is honored.
web/src/styles/design-tokens.css (1)

77-77: ⚠️ Potential issue | 🟠 Major

Replace hardcoded rgba() in --so-shadow-card with token-derived value.

The shadow token still uses a hardcoded RGBA value, violating the design-system rule. This was flagged in a previous review but remains unfixed.

🎨 Proposed fix
-  --so-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.25);
+  --so-shadow-card: 0 2px 8px color-mix(in srgb, var(--so-bg-base) 25%, transparent);

As per coding guidelines: web/src/**/*.{ts,tsx,css} must "Do NOT use rgba() with hardcoded values -- use design token variables" and "Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders instead of hardcoded values".

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

In `@web/src/styles/design-tokens.css` at line 77, The CSS variable
--so-shadow-card currently uses a hardcoded rgba() value; replace that rgba(...)
with the design token color variable (e.g., use the project shadow color token
such as var(--so-shadow-card-color) or the appropriate token like
var(--so-shadow-card-hover) if defined) so the shadow uses token-derived color
instead of hardcoded values; update the --so-shadow-card declaration to use the
token variable for the color portion and ensure the referenced token exists in
the token definitions.
🤖 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/styles/design-tokens.css`:
- Around line 88-92: The CSS uses an undefined token var(--so-bg) in the minimap
rules (--so-minimap-bg, --so-minimap-mask, --so-minimap-shadow), causing
color-mix() to fail; replace var(--so-bg) with the correct existing background
token (e.g., var(--so-bg-base) or whichever is semantically appropriate like
--so-bg-surface) in those three declarations so the color-mix() calls reference
a defined variable.

---

Duplicate comments:
In `@web/src/stores/agents.ts`:
- Around line 276-283: agentName currently only checks typeof agentNameRaw, so
empty or whitespace strings slip through and bypass the fallback; update the
logic that builds agentName (similar to the agentId fix at lines ~244-247) to
trim agentNameRaw, treat empty/whitespace-only results as null, and then slice
to 64 chars if present (i.e., use agentNameRaw?.trim() -> if non-empty then
slice(0,64) else null); apply the same change to the second occurrence around
lines ~319-323 to ensure the "?? 'An agent'" fallback is honored.

In `@web/src/styles/design-tokens.css`:
- Line 77: The CSS variable --so-shadow-card currently uses a hardcoded rgba()
value; replace that rgba(...) with the design token color variable (e.g., use
the project shadow color token such as var(--so-shadow-card-color) or the
appropriate token like var(--so-shadow-card-hover) if defined) so the shadow
uses token-derived color instead of hardcoded values; update the
--so-shadow-card declaration to use the token variable for the color portion and
ensure the referenced token exists in the token definitions.
🪄 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: 0fa34f79-2ae5-4576-ac1e-8df0097ad6dc

📥 Commits

Reviewing files that changed from the base of the PR and between d1c9f80 and b354ae8.

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

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,js,jsx}: Always use createLogger from @/lib/logger instead of bare console.warn, console.error, or console.debug in application code
Use variable name log for logger instances (e.g., const log = createLogger('module-name'))
Use log.debug() for dev-only logging (stripped in production), log.warn() for warnings, and log.error() for errors
Pass dynamic/untrusted values as separate arguments to logger calls (not interpolated into the message string) so they go through sanitizeArg
Wrap attacker-controlled fields inside structured objects with sanitizeForLog() before embedding in logs

Files:

  • web/src/stores/agents.ts
web/src/**/*.{ts,tsx,css}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,css}: Use Tailwind semantic color classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) instead of hardcoded hex values
Use font-sans or font-mono for typography (maps to Geist tokens), never set fontFamily directly
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing, never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders instead of hardcoded values
Do NOT use rgba() with hardcoded values -- use design token variables
A PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files and catches design system violations; fix all violations before proceeding

Files:

  • web/src/stores/agents.ts
  • web/src/styles/design-tokens.css
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx}: Do NOT hardcode Framer Motion transition durations -- use presets from @/lib/motion
CSS side-effect imports in TypeScript files should use type declarations (/// <reference types="vite/client" /> covers this); TypeScript 6 defaults noUncheckedSideEffectImports to true

web/src/**/*.{ts,tsx}: TypeScript/React code must ALWAYS reuse existing components from web/src/components/ui/ before creating new ones.
NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in TypeScript/React code -- use design tokens and @/lib/motion presets.
TypeScript code in the web dashboard must follow design system rules. A PostToolUse hook (scripts/check_web_design_system.py) enforces these rules on every Edit/Write to web/src/.
Web dashboard TypeScript/TSX code must pass ESLint with zero warnings (enforced by pre-commit and pre-push hooks).

Files:

  • web/src/stores/agents.ts
🧠 Learnings (21)
📓 Common learnings
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/).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `AgentCard` component for agent display (avatar, name, role, status, current task)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:28:28.895Z
Learning: Web dashboard: see `web/CLAUDE.md` for commands, design system, and component inventory
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T18:57:46.655Z
Learning: Applies to web/package.json : Web dashboard Node.js 22+, TypeScript 6.0+, dependencies in `web/package.json`
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to web/package.json : Web dashboard Node.js 20+; dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, ESLint, vue-tsc)
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)

Applied to files:

  • web/src/stores/agents.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)

Applied to files:

  • web/src/stores/agents.ts
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `AgentCard` component for agent display (avatar, name, role, status, current task)

Applied to files:

  • web/src/stores/agents.ts
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,js,jsx} : Wrap attacker-controlled fields inside structured objects with `sanitizeForLog()` before embedding in logs

Applied to files:

  • web/src/stores/agents.ts
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,js,jsx} : Pass dynamic/untrusted values as separate arguments to logger calls (not interpolated into the message string) so they go through `sanitizeArg`

Applied to files:

  • web/src/stores/agents.ts
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `Toast` / `ToastContainer` component for success/error/warning/info notifications with auto-dismiss queue (mount `ToastContainer` once in AppLayout)

Applied to files:

  • web/src/stores/agents.ts
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders instead of hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Do NOT use `rgba()` with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web code — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in component code -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use Tailwind semantic color classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) instead of hardcoded hex values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

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

Applied to files:

  • web/src/styles/design-tokens.css
🔇 Additional comments (9)
web/src/stores/agents.ts (7)

4-22: LGTM!

The imports and logger setup follow the coding guidelines. createLogger('agents') with the log variable name is correct, and importing sanitizeForLog enables proper sanitization of attacker-controlled values throughout the store.


116-117: LGTM!

Using log.warn with the error object as a separate argument allows proper sanitization and structured logging.


165-168: LGTM!

Good defensive logging—the comment correctly identifies that name originates from an attacker-controlled URL segment, and sanitizeForLog is appropriately applied before embedding in the structured log.


231-268: LGTM!

The agent.status_changed handler now properly rejects blank/whitespace-only identifiers with !agentId.trim() and !status.trim(), validates against VALID_RUNTIME_STATUSES, and sanitizes untrusted values before logging.


288-301: LGTM!

The token validation logic is solid: isTokenCount correctly rejects negative, fractional, Infinity, and NaN values. The monotonicity check now uses strict inequality (afterValid < beforeValid), ensuring that no-op "trims" with equal before/after counts fall through to the generic fallback description.


303-329: LGTM!

The fallback logic correctly suppresses uninformative toasts when all fields are invalid while still logging for diagnostics. The toast description gracefully degrades based on available data.


331-333: LGTM!

Debug logging for unhandled event types now correctly uses sanitizeForLog for the untrusted event_type field.

web/src/styles/design-tokens.css (2)

81-85: LGTM!

The transition duration and drag opacity tokens are correctly defined as primitive values.


93-95: LGTM!

The minimap node color tokens correctly reference existing design tokens and use color-mix() for transparency.

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 (1)
web/src/styles/design-tokens.css (1)

77-77: ⚠️ Potential issue | 🟠 Major

Replace hardcoded RGBA in --so-shadow-card with token-derived color.

--so-shadow-card still uses a hardcoded rgba(...), which violates the design-token rule for CSS in this repo.

🎨 Proposed fix
-  --so-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.25);
+  --so-shadow-card: 0 2px 8px color-mix(in srgb, var(--so-bg-base) 25%, transparent);

As per coding guidelines: web/src/**/*.{ts,tsx,css} must not use hardcoded rgba() values and should use token variables/color-mix with tokens.

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

In `@web/src/styles/design-tokens.css` at line 77, The CSS custom property
--so-shadow-card currently uses a hardcoded rgba value; replace it with a
token-derived color (e.g., use an existing color token via var(--token-name) or
color-mix() combining token(s) and alpha) so the shadow color comes from design
tokens rather than literal rgba(); update the --so-shadow-card definition to
reference the token (or a color-mix of tokens) and keep the same blur/offset
values.
🤖 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/styles/design-tokens.css`:
- Line 77: The CSS custom property --so-shadow-card currently uses a hardcoded
rgba value; replace it with a token-derived color (e.g., use an existing color
token via var(--token-name) or color-mix() combining token(s) and alpha) so the
shadow color comes from design tokens rather than literal rgba(); update the
--so-shadow-card definition to reference the token (or a color-mix of tokens)
and keep the same blur/offset values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e8087b52-402f-4d77-9889-c10658dd2a98

📥 Commits

Reviewing files that changed from the base of the PR and between b354ae8 and 6c3326e.

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

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,css}: Use Tailwind semantic color classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) instead of hardcoded hex values
Use font-sans or font-mono for typography (maps to Geist tokens), never set fontFamily directly
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing, never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders instead of hardcoded values
Do NOT use rgba() with hardcoded values -- use design token variables
A PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files and catches design system violations; fix all violations before proceeding

Files:

  • web/src/styles/design-tokens.css
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
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/).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:28:28.895Z
Learning: Web dashboard: see `web/CLAUDE.md` for commands, design system, and component inventory
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to docker/{Dockerfile*,compose.yml} : Docker: Backend uses 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web uses nginxinc/nginx-unprivileged, Vue 3 SPA with PrimeVue + Tailwind CSS, SPA routing, API/WebSocket proxy to backend.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T18:57:46.655Z
Learning: Applies to web/package.json : Web dashboard Node.js 22+, TypeScript 6.0+, dependencies in `web/package.json`
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to web/package.json : Web dashboard Node.js 20+; dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, ESLint, vue-tsc)
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders instead of hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Do NOT use `rgba()` with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web code — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in component code -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use Tailwind semantic color classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) instead of hardcoded hex values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

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

Applied to files:

  • web/src/styles/design-tokens.css
🔇 Additional comments (1)
web/src/styles/design-tokens.css (1)

81-95: Good tokenization for org-chart/minimap additions.

The new minimap/org-chart variables are now theme-aware via var(...) and color-mix(...), which is aligned with the design-system direction.

…theme vars

Shadows (--so-shadow-card, --so-shadow-card-hover, --so-shadow-health-glow)
and overlays (--so-overlay-hover, --so-overlay-active, --so-overlay-flash,
--so-overlay-selection, --so-overlay-selection-focused) now derive from
--so-bg-base, --so-accent, and white via color-mix(), making them
theme-portable.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/styles/design-tokens.css`:
- Around line 77-102: Theme classes (.theme-ice-station, .theme-stealth,
.theme-signal, .theme-neon) still override shadow/overlay tokens with hardcoded
rgba(...) values which breaks the new color-mix pattern; remove those per-theme
overrides for --so-shadow-card-hover, --so-shadow-health-glow and --so-overlay-*
(or rewrite them to use color-mix(in srgb, var(--so-accent) X%, transparent)
matching the :root pattern) so themed accents flow from the shared tokens
(--so-accent, --so-bg-base, --so-border) and you no longer need to update rgba
values when accent colors change.
🪄 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: 034c0cd7-3ace-4d1e-a6d4-ac02cf94b0f0

📥 Commits

Reviewing files that changed from the base of the PR and between 6c3326e and 2f4d019.

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

📄 CodeRabbit inference engine (web/CLAUDE.md)

web/src/**/*.{ts,tsx,css}: Use Tailwind semantic color classes (text-foreground, bg-card, text-accent, text-success, bg-danger, etc.) or CSS variables (var(--so-accent)) instead of hardcoded hex values
Use font-sans or font-mono for typography (maps to Geist tokens), never set fontFamily directly
Use density-aware spacing tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing, never hardcode pixel values for layout spacing
Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows and borders instead of hardcoded values
Do NOT use rgba() with hardcoded values -- use design token variables
A PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files and catches design system violations; fix all violations before proceeding

Files:

  • web/src/styles/design-tokens.css
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from web/src/components/ui/ (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast/ToastContainer, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem, Drawer, form fields, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor) before creating new components
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/StaggerItem) before creating new ones
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/).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to web/** : Web dashboard: Node.js 20+, dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, fast-check, ESLint, vue-tsc).
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:28:28.895Z
Learning: Web dashboard: see `web/CLAUDE.md` for commands, design system, and component inventory
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:17:43.675Z
Learning: Applies to docker/{Dockerfile*,compose.yml} : Docker: Backend uses 3-stage build (builder → setup → distroless runtime), Chainguard Python, non-root (UID 65532), CIS-hardened. Web uses nginxinc/nginx-unprivileged, Vue 3 SPA with PrimeVue + Tailwind CSS, SPA routing, API/WebSocket proxy to backend.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/pages/**/*.{ts,tsx} : Use `AgentCard` component for agent display (avatar, name, role, status, current task)
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : When creating new shared web components, place in web/src/components/ui/ with kebab-case filename, create .stories.tsx alongside with all states (default, hover, loading, error, empty), export props as TypeScript interface, use design tokens exclusively with no hardcoded colors/fonts/spacing, and import cn from `@/lib/utils` for conditional class merging
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T18:57:46.655Z
Learning: Applies to web/package.json : Web dashboard Node.js 22+, TypeScript 6.0+, dependencies in `web/package.json`
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to web/package.json : Web dashboard Node.js 20+; dependencies in web/package.json (Vue 3, PrimeVue, Tailwind CSS, Pinia, VueFlow, ECharts, Axios, vue-draggable-plus, Vitest, ESLint, vue-tsc)
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders instead of hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/src/**/*.{ts,tsx} : Web dashboard shadows/borders: use token variables (var(--so-shadow-card-hover), border-border, border-bright); never hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use token variables for shadows/borders (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`); no hardcoded values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (var(--so-shadow-card-hover), border-border, border-bright) for shadows/borders; never hardcode values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders. NEVER hardcode shadow or border values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Do NOT use `rgba()` with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Do not use rgba() with hardcoded values -- use design token variables

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web code — use design tokens and `@/lib/motion` presets

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Use design tokens exclusively in component code -- no hardcoded colors, fonts, or spacing

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-04T17:43:42.372Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-04T17:43:42.372Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Use Tailwind semantic color classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-accent)`) instead of hardcoded hex values

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`) or CSS variables (`var(--so-*)`) for colors. NEVER hardcode hex values or rgba() in `.tsx`/`.ts` files

Applied to files:

  • web/src/styles/design-tokens.css
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use semantic Tailwind classes (text-foreground, bg-card, text-accent, text-success, bg-danger) or CSS variables (var(--so-*)); never hardcode hex values in .tsx/.ts files

Applied to files:

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

Applied to files:

  • web/src/styles/design-tokens.css
🔇 Additional comments (4)
web/src/styles/design-tokens.css (4)

77-79: LGTM! Shadow tokens correctly use color-mix() with design token references.

The implementation properly derives shadow colors from --so-bg-base and --so-accent tokens, making shadows theme-portable. Past review concerns about hardcoded rgba() and undefined --so-bg have been addressed.


82-85: LGTM! Transition and opacity tokens are well-defined.

The new --so-transition-dim and --so-opacity-dragging tokens provide centralized control for org chart animations. The 0.6 opacity value is appropriately used with a fallback in global.css (line 166).


88-95: LGTM! Minimap tokens are theme-aware and correctly consumed.

All 8 minimap tokens use color-mix() with design token references or direct token assignments. The naming and spelling match exactly what's consumed in OrgChartPage.tsx. Past concerns about hardcoded rgba for --so-minimap-node-dept have been addressed.


98-102: LGTM! Overlay tokens use color-mix() appropriately.

The overlay tokens correctly derive colors from --so-accent with varying opacity levels via color-mix(). The use of the CSS keyword white for --so-overlay-hover is acceptable—it's not a hardcoded rgba() value, and theme classes don't override this token, indicating a consistent white-tint hover effect is intentional across all color palettes.

…-mix

Shadows and overlay tokens in :root use color-mix(var(--so-accent), %).
Theme classes already override --so-accent, so the per-theme shadow
and overlay lines were redundant. Neon retains explicit shadow overrides
for its stronger glow intensity (12% and 40% vs default 8% and 30%).
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 6, 2026 06:27 — with GitHub Actions Inactive
@Aureliolo Aureliolo merged commit ebc6921 into main Apr 6, 2026
32 of 34 checks passed
@Aureliolo Aureliolo deleted the feat/radix-to-base-ui-migration branch April 6, 2026 06:41
@Aureliolo Aureliolo temporarily deployed to cloudflare-preview April 6, 2026 06:41 — with GitHub Actions Inactive
Aureliolo added a commit that referenced this pull request Apr 6, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.6.2](v0.6.1...v0.6.2)
(2026-04-06)


### Features

* add issue analyzer script for priority/scope management
([#1084](#1084))
([1ccba27](1ccba27))
* config fixes and deferred improvements from PR
[#1058](#1058) review
([#1067](#1067))
([2cac2d3](2cac2d3)),
closes [#1061](#1061)
[#1060](#1060)
* cumulative risk-unit action budgets
([#806](#806)) and automated
reporting ([#245](#245))
([#1063](#1063))
([4689816](4689816))
* fine-tuning pipeline + CompositeBackend + workflow lifecycle
([#1065](#1065))
([85b05bc](85b05bc)),
closes [#1001](#1001)
[#850](#850)
[#1058](#1058)
* memory consolidation upgrades (LLM Merge, Search-and-Ask, diversity
penalty, distillation capture)
([#1071](#1071))
([174e2be](174e2be)),
closes [#704](#704)
* migrate web dashboard from Radix UI to Base UI, activate CSP nonce,
rebuild org chart page, and fix agent routing
([#1083](#1083))
([ebc6921](ebc6921))
* v0.7.0 engine foundations -- structured failure diagnosis + auditable
decisions ([#1072](#1072))
([d341d37](d341d37))
* workflow templates and versioning with diff and rollback
([#1069](#1069))
([7af94de](7af94de)),
closes [#1006](#1006)
[#1008](#1008)


### Documentation

* unify REST API docs under /docs/openapi/ and patch sitemap
([#1073](#1073))
([af19382](af19382))


### Maintenance

* bump hypothesis from 6.151.10 to 6.151.11 in the all group
([#1086](#1086))
([3176318](3176318))
* bump nginxinc/nginx-unprivileged from `f99cc61` to `601c823` in
/docker/web in the all group
([#1085](#1085))
([5eb99ac](5eb99ac))
* bump the all group in /web with 3 updates
([#1087](#1087))
([8deae44](8deae44))

---
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: migrate web dashboard from Radix UI to Base UI + activate CSP nonce infrastructure + personality trimming WS notification

2 participants