Skip to content

Conversation

@batmn-dev
Copy link
Owner

@batmn-dev batmn-dev commented Feb 9, 2026

Summary

A large branch covering four main workstreams: migrating the component library from Radix to Base UI, adding Model Context Protocol (MCP) tool support, fixing multi-chat message reliability, and curating the model catalog.


1. Base UI Migration (Radix → Base UI)

Replaced all 21 @radix-ui/react-* dependencies with a single @base-ui/react package. 35 UI components in components/ui/ were migrated:

  • API mapping: ContentPopup, OverlayBackdrop, ViewportList, ScrollUpButtonScrollUpArrow, LabelGroupLabel (select)
  • Positioning: Replaced Radix's position="popper" with explicit <Positioner> wrapper components using side/align/sideOffset props
  • Data attributes: data-[state=open]data-[open], data-[state=closed]data-[closed] throughout CSS classes
  • CSS variables: --radix-*-transform-origin--transform-origin, --radix-select-content-available-height--available-height, etc.
  • Composition: Created lib/as-child-adapter.ts to bridge Radix's asChild boolean prop to Base UI's render prop pattern (with full test suite)
  • Components migrated: accordion, alert-dialog, aspect-ratio, avatar, badge, breadcrumb, button, chain-of-thought, checkbox, collapsible, collapsible-section, context-menu, dialog, dropdown-menu, form, hover-card, label, menubar, message, navigation-menu, popover, progress, radio-group, scroll-area, select, separator, sheet, sidebar, slider, steps, switch, tabs, toggle, toggle-group, tooltip

2. MCP (Model Context Protocol) Integration

Full end-to-end MCP support, feature-flagged behind ENABLE_MCP=true:

Database layer (Convex):

  • 3 new tables: mcpServers, mcpToolApprovals, mcpToolCallLog
  • mcpServers.ts — CRUD with SSRF validation, server limit enforcement, encrypted auth storage
  • mcpToolApprovals.ts — Per-tool granular approval/toggle with bulk sync
  • mcpToolCallLog.ts — Audit trail with truncated previews and paginated queries
  • messages.ts — Server-side idempotency guard (deduplicates by messageGroupId + role + model)

API routes (3 new):

  • app/api/mcp-servers/route.ts — Server CRUD
  • app/api/mcp-servers/status/route.ts — Health check
  • app/api/mcp-servers/test/route.ts — Connection test

Core library (lib/mcp/):

  • load-tools.ts — Orchestrates connecting to user's enabled MCP servers, loading tools with namespacing, and returning a ToolSet for streamText()
  • url-validation.ts — SSRF defense: blocks private IPv4/IPv6 ranges, localhost, CGNAT, link-local, IPv4-mapped IPv6, .local hostnames
  • circuit-breaker.ts — Resilience pattern for MCP connections (failure threshold → open → half-open → recovery)
  • truncation.ts — Output sanitization for tool results

Chat route integration (app/api/chat/route.ts):

  • Loads MCP tools for authenticated users with ENABLE_MCP=true
  • Injects tools into streamText() with configurable maxSteps
  • PostHog events: mcp_tool_load (per-request) and mcp_tool_call (per-invocation)
  • after() cleanup ensures MCP client connections are always closed

Settings UI (3 new components):

  • mcp-server-form.tsx — Add/edit form with transport (SSE/HTTP) and auth type (none/bearer/header) options
  • mcp-servers.tsx — Server list with enable/disable, test connection, delete
  • mcp-tool-approvals.tsx — Granular per-tool approval toggles

Tests (4 new test files, 1442 lines):

  • circuit-breaker.test.ts, load-tools.test.ts, truncation.test.ts, url-validation.test.ts

3. Multi-Chat Reliability Fixes

Fixed duplicate message groups and lost history in multi-model chat:

  • Submission registry (SubmissionRecord): Lifecycle-managed tracking of multi-model submissions — entries are only cleaned up when ALL models complete streaming, preventing premature bridge removal that caused duplicate groups
  • Per-model groupId tracking: Replaced single shared messageGroupIdRef with per-model Map<string, string>, fixing stale closure bugs when submissions overlap
  • UUID-based grouping: Messages now grouped by messageGroupId (UUID) instead of text content, with text-content fallback for backward compatibility
  • Server-side idempotency: convex/messages.ts add mutation checks for existing message with same messageGroupId + role + model before inserting
  • Navigation cleanup: Stops active streams and resets all state (submission registry, group refs, hook messages) when navigating between chats
  • Post-persistence cleanup: Each model's useChat hook messages are cleared after cacheAndAddMessage, preventing stale live messages from producing duplicate groups

4. Model Catalog Curation

Added: GPT-5.1 Instant, GPT-5.1 Thinking, Gemini 3 Pro (renamed from preview), Codestral, o4-mini
Removed: Claude 3 Haiku, Claude Opus 4, Gemini 1.5 Flash/Pro/8B, Gemini 2.0 Flash/Flash-Lite, GPT-4o Mini, GPT-4.1 Mini/Nano, GPT-4.5 Preview, GPT-5, GPT-5 Mini, o1, o3-mini, all Llama/Groq models (llama.ts deleted)
Updated: OpenRouter Gemini model IDs to GA (removed -preview suffixes), removed preview tags from stable models

5. UI/UX Improvements

  • Sidebar: Simplified sidebar-list.tsx, improved collapsible chevron rotation
  • User menu: Removed app info dialog (deleted app-info-content.tsx + app-info-trigger.tsx), updated settings icon
  • File upload: Centralized accept types into lib/file-handling.ts

6. Tooling & Infrastructure

  • Added Vitest (^4.0.18) with vitest.config.ts and bun run test script
  • Added bun run dev:clean script (clears .next cache before dev)
  • Pinned @ai-sdk/mcp to exact 1.0.18 (was ^1.0.18)

116 files changed · ~10,600 additions · ~1,560 deletions · 25 commits

Test plan

  • Base UI components: Verify dialog, dropdown, popover, select, tooltip, tabs, collapsible, sidebar all render and animate correctly
  • Base UI interactions: Test asChild pass-through (e.g., tooltip wrapping icon buttons), keyboard navigation, focus management
  • MCP integration: Add an MCP server in settings, verify connection test, confirm tools appear in chat
  • MCP security: Verify SSRF validation blocks localhost, 10.x.x.x, 192.168.x.x, ::1, and fe80:: URLs
  • Multi-chat: Send messages to 3+ models simultaneously, verify no duplicate groups, messages persist after navigation
  • Multi-chat idempotency: Rapid-fire submissions don't produce duplicate messages in Convex
  • Model catalog: Verify new models (GPT-5.1, Gemini 3 Pro) appear, deprecated models are gone
  • Unit tests: bun run test — all tests pass (as-child-adapter, circuit-breaker, load-tools, truncation, url-validation)
  • Lint & types: bun run lint and bun run typecheck pass cleanly

batmn-dev and others added 6 commits February 9, 2026 17:58
- Add @base-ui/react dependency
- Create asChild → render prop adapter utility with tests
- Add research on Radix-to-Base-UI CSS variable mapping
- Add Base UI migration plan
- Add create-plan skill template

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace @radix-ui/react-* with @base-ui/react/* across 22 component
files. Updates include Radix Slot/asChild to Base UI render prop via
adaptAsChild adapter, Content→Popup, Overlay→Backdrop renames,
data-[state=open/closed]→data-[open/closed] attribute migration,
Positioner wrappers for tooltip/popover/hover-card/dropdown-menu,
and --radix-* → --transform-origin CSS variable updates. Backward-
compatible prop aliases preserved where needed.

Co-authored-by: Cursor <cursoragent@cursor.com>
…tives

Migrate accordion, context-menu, menubar, navigation-menu, radio-group,
scroll-area, select, slider, and toggle-group to @base-ui/react. Updates
include Radix data-[state=open/closed] → Base UI data-[open/closed],
Positioner/Popup patterns, and renamed sub-components (e.g. SubTrigger →
SubmenuTrigger, ItemIndicator → CheckboxItemIndicator).

Co-authored-by: Cursor <cursoragent@cursor.com>
- Replace @radix-ui/react-collapsible with @base-ui/react/collapsible
- Replace Radix Slot with Base UI useRender + adaptSlotAsChild in form and sidebar
- Update data attributes from data-[state=open] to data-[open] pattern
- Update CSS variable from --radix-collapsible-content-height to --collapsible-panel-height

Co-authored-by: Cursor <cursoragent@cursor.com>
…gration config

Remove 26 @radix-ui/* packages from dependencies, switch Shadcn style to
"base-vega" for future Base UI component generation, update CSS variable
naming (--radix-popper-anchor-width → --anchor-width), and document the
Base UI migration in components/CLAUDE.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
Update all documentation references from Radix to Base UI and fix
collapsible data attributes (data-[state=open] → data-[open]) in
chain-of-thought, steps, and tool components. Mark migration plan
as completed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link

vercel bot commented Feb 9, 2026

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

Project Deployment Actions Updated (UTC)
not-a-wrapper Ready Ready Preview, Comment Feb 10, 2026 0:06am

Remove deprecated prop aliases (delayDuration, skipDelayDuration, position)
from tooltip, select, and related components. Update all call sites to use
native Base UI props directly. Simplify scroll viewport ref in chat preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

5 issues found across 55 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="components/ui/breadcrumb.tsx">

<violation number="1" location="components/ui/breadcrumb.tsx:45">
P2: `useRender` is a hook, so this module must be a Client Component. Without a `"use client"` directive, Next.js treats this file as a Server Component and will error when `useRender` is called. Add `"use client"` at the top of the file (or refactor to avoid hooks).</violation>
</file>

<file name="components/ui/select.tsx">

<violation number="1" location="components/ui/select.tsx:75">
P2: `alignItemWithTrigger` passed by callers is ignored and forwarded to `SelectPrimitive.Popup` because the local constant is hard-coded from `position` and the prop is never destructured. This prevents callers from overriding alignment and may pass an unsupported prop to Popup. Destructure `alignItemWithTrigger` from the props and default it to the Radix-compat behavior before passing the remaining props to Popup.</violation>
</file>

<file name="components/ui/form.tsx">

<violation number="1" location="components/ui/form.tsx:109">
P2: FormControl’s props type allows any ReactNode, but the implementation requires exactly one ReactElement via React.Children.only. This mismatch can cause runtime exceptions when a caller passes null, text, or multiple children. Tighten the prop type (or add a runtime guard) so invalid children are caught at compile time.</violation>
</file>

<file name="components/ui/context-menu.tsx">

<violation number="1" location="components/ui/context-menu.tsx:102">
P2: Wrap the submenu Positioner/Popup in `ContextMenuPrimitive.Portal` as per Base UI’s nested menu pattern; otherwise submenu content can be clipped or layered incorrectly because it isn’t portaled.</violation>
</file>

<file name="components/ui/popover.tsx">

<violation number="1" location="components/ui/popover.tsx:67">
P2: PopoverAnchor is now just a plain div, so it no longer wires into the popover context. Any usage expecting anchor positioning will break silently. Consider removing PopoverAnchor entirely or implementing it using a Base UI-supported anchor mechanism so it still affects positioning.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@greptile-apps
Copy link

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

This PR completes a large Radix → Base UI migration by replacing Radix primitives with Base UI primitives across the components/ui/* wrapper layer and updating state/slot-driven Tailwind selectors (e.g., data-[state=open]data-[open], Radix CSS vars → Base UI shared vars).

The new lib/as-child-adapter.ts provides a compatibility shim so existing call sites can keep using Radix-style asChild while wrappers translate to Base UI’s render prop.

Merge blockers found are concentrated in wrapper/call-site integration: the header UserMenu currently renders an empty dropdown trigger, the asChild shim throws on non-element children when asChild=true (runtime crash path), and submenu slot styling is broken due to hardcoded data-slot in DropdownMenuContent.

Confidence Score: 2/5

  • This PR has a few user-facing runtime issues that should be fixed before merge.
  • Base UI migration is broadly consistent, but there are confirmed integration bugs: the header user menu trigger renders with no children, the asChild compatibility shim can throw at runtime for non-element children when asChild=true, and dropdown submenu slot attributes are misapplied which breaks slot-scoped styling/testing selectors.
  • app/components/layout/user-menu.tsx, lib/as-child-adapter.ts, components/ui/dropdown-menu.tsx

Important Files Changed

Filename Overview
app/components/layout/user-menu.tsx Migrates user menu to new dropdown/tooltip wrappers; header variant now renders an empty DropdownMenuTrigger (bug).
app/globals.css Global styles updated for Base UI data attributes; no obvious issues found.
components/common/multi-model-selector/base.tsx Continues using dropdown/menu wrappers; no direct issues found with new Base UI API usage.
components/ui/button.tsx Button migrated to Base UI Button + asChild shim; inherits adaptAsChild runtime throwing behavior if misused.
components/ui/dialog.tsx Dialog migrated to Base UI; uses asChild shim for trigger; no definite issues found.
components/ui/dropdown-menu.tsx Dropdown menu migrated to Base UI Menu; submenu content slot currently mismatched due to hardcoded data-slot in DropdownMenuContent.
components/ui/select.tsx Select migrated to Base UI Select; uses new Positioner/Popup structure; no definite issues found.
components/ui/sheet.tsx Sheet migrated using Base UI Dialog; trigger lacks asChild support which may break existing usages expecting asChild.
components/ui/tooltip.tsx Tooltip migrated to Base UI Tooltip; provider delay compat retained; no definite issues found.
lib/as-child-adapter.ts Introduces asChild compatibility shim; adaptAsChild currently throws for non-element children when asChild=true (runtime crash risk).
package.json Dependencies updated to remove Radix direct deps and include Base UI; no obvious issues in manifest itself.

Sequence Diagram

sequenceDiagram
  participant App as app/components (call sites)
  participant Wrappers as components/ui/* wrappers
  participant Adapter as lib/as-child-adapter
  participant BaseUI as @base-ui/react primitives

  App->>Wrappers: Render <DropdownMenu>/<Trigger>/<Content>
  Wrappers->>Adapter: adaptAsChild(asChild, children)
  Adapter-->>Wrappers: { render?, children }
  Wrappers->>BaseUI: <Trigger render={render}>{children}</Trigger>
  Wrappers->>BaseUI: <Positioner><Popup ... /></Positioner>
  Note over Adapter,Wrappers: If asChild=true + non-element children,
  Note over Adapter,Wrappers: Children.only(...) throws (runtime crash).
  Note over App,Wrappers: If Trigger used without children,
  Note over App,Wrappers: trigger renders empty (header user-menu bug).
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

11 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 257 to 276
function DropdownMenuSubContent({
className,
side = "right",
sideOffset = 0,
align = "start",
alignOffset = -3,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuPrimitive.SubContent
<DropdownMenuContent
data-slot="dropdown-menu-sub-content"
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
"min-w-[8rem] origin-(--transform-origin)",
className
)}
{...props}
Copy link

Choose a reason for hiding this comment

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

Submenu gets wrong slot
DropdownMenuSubContent passes data-slot="dropdown-menu-sub-content" into DropdownMenuContent, but DropdownMenuContent hardcodes data-slot="dropdown-menu-content" on the actual MenuPrimitive.Popup. As a result, submenu popups will be styled like the main menu content (and any data-slot-scoped CSS for submenus won't match).

To keep slot-based styling/test selectors correct, ensure the popup rendered for the submenu carries the dropdown-menu-sub-content slot (not the parent wrapper component).

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/ui/dropdown-menu.tsx
Line: 257:276

Comment:
**Submenu gets wrong slot**
`DropdownMenuSubContent` passes `data-slot="dropdown-menu-sub-content"` into `DropdownMenuContent`, but `DropdownMenuContent` hardcodes `data-slot="dropdown-menu-content"` on the actual `MenuPrimitive.Popup`. As a result, submenu popups will be styled like the main menu content (and any `data-slot`-scoped CSS for submenus won't match).

To keep slot-based styling/test selectors correct, ensure the popup rendered for the submenu carries the `dropdown-menu-sub-content` slot (not the parent wrapper component).

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link

greptile-apps bot commented Feb 9, 2026

Additional Comments (1)

app/components/layout/user-menu.tsx
Dropdown trigger missing children
DropdownMenuTrigger in components/ui/dropdown-menu.tsx destructures children and passes it through to adaptAsChild. In the header variant here, <DropdownMenuTrigger> is used without any children, so adaptAsChild(false, undefined) returns { children: undefined } and the trigger renders no content.

This results in an empty/unclickable menu trigger in the header. Wrap the Avatar in the trigger (or use asChild and pass the Avatar as the child) so the trigger has content.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/components/layout/user-menu.tsx
Line: 151:159

Comment:
**Dropdown trigger missing children**
`DropdownMenuTrigger` in `components/ui/dropdown-menu.tsx` destructures `children` and passes it through to `adaptAsChild`. In the header variant here, `<DropdownMenuTrigger>` is used without any children, so `adaptAsChild(false, undefined)` returns `{ children: undefined }` and the trigger renders no content.

This results in an empty/unclickable menu trigger in the header. Wrap the `Avatar` in the trigger (or use `asChild` and pass the `Avatar` as the child) so the trigger has content.

How can I resolve this? If you propose a fix, please make it concise.

@batmn-dev batmn-dev changed the title feat: Base UI migration, MCP integration, multi-chat fixes, and model catalog updates feat: migrate UI to Base UI, add MCP integration, fix multi-chat reliability, curate model catalog Feb 9, 2026
- Replace vaul (Radix-based) with vaul-base (Base UI port) for drawer
- Import vaul-base/style.css in globals.css
- Simplify adaptAsChild/adaptSlotAsChild: remove Children.only, graceful
  fallback for non-element children instead of throwing
- Add asChild support to DrawerTrigger and DrawerClose via adapter
- Wrap ContextMenu sub-content in Portal for proper layering
- Map HoverCard openDelay to Base UI's delay prop
- Add missing data-[closed]:animate-out to menubar content
- Remove unused PopoverAnchor export
- Remove stale data-[open] classes from dialog/sheet close buttons
- Tighten FormControl children type to ReactElement
- Add "use client" directive to breadcrumb
- Document third-party deps (cmdk, vaul-base) in components/CLAUDE.md

Co-authored-by: Cursor <cursoragent@cursor.com>
HoverCard: bridge Radix-style openDelay/closeDelay from Root to Base UI's
Trigger via context, restoring delay behavior for source.tsx consumer.

Menubar: add missing data-[closed]:animate-out on MenubarContent so exit
animations play, and wrap MenubarSubContent in Portal for proper z-index.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="app/globals.css">

<violation number="1" location="app/globals.css:3">
P2: Avoid importing CSS via a relative path into node_modules; it’s brittle and can break module resolution in different build setups. Use the package import instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

vaul-base's package.json exports field doesn't include ./style.css,
causing Turbopack's enhanced-resolve to block the import and crash
the dev server. Copied the CSS locally as a workaround.

Co-authored-by: Cursor <cursoragent@cursor.com>
@batmn-dev batmn-dev merged commit 80fe803 into main Feb 10, 2026
6 checks passed
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.

1 participant