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

Loading
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