feat: App sidebar, AI chat, and community chat redesign#264
feat: App sidebar, AI chat, and community chat redesign#264stickerdaniel merged 68 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
All alerts resolved. Learn more about Socket for GitHub. This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored. |
There was a problem hiding this comment.
Pull request overview
This PR removes legacy app navigation/pages (Dashboard/Docs/Start), redirects /app to Community Chat, and introduces a new Pro-gated AI Chat experience with sidebar thread management and various UI/E2E improvements across the app shell.
Changes:
- Remove the
/app/dashboardpage and sidebar/global-search entries; redirect/app→/app/community-chat. - Add AI Chat (routes + Convex backend tables/functions) including “warm thread” pre-creation and sidebar thread listing.
- Improve UI polish (chat layout/input/animations/sidebar press effects/header background) and update Playwright E2E to support local Convex discovery.
Reviewed changes
Copilot reviewed 53 out of 55 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Writes local Convex backend URL to .convex/.backend-url for E2E discovery during dev. |
| src/routes/sidebar-07/+page.svelte | Adds a new sidebar demo page/route using sample sidebar components. |
| src/routes/layout.css | Adds chip-in keyframe used by chat suggestion chip animation. |
| src/routes/[[lang]]/app/dashboard/+page.svelte | Removes dashboard page. |
| src/routes/[[lang]]/app/community-chat/+page.svelte | Refactors community chat layout to match app page structure; removes decorative icons/title component. |
| src/routes/[[lang]]/app/ai-chat/thread-chat.svelte | Adds the AI thread chat UI wrapper around shared chat UI components + Pro banner. |
| src/routes/[[lang]]/app/ai-chat/+page.svelte | Adds AI Chat page with viewer load + warm-thread resolution + upgrade flow. |
| src/routes/[[lang]]/app/ai-chat/+page.server.ts | Adds server load to fetch viewer for AI chat page. |
| src/routes/[[lang]]/app/+page.svelte | Updates /app SEO metadata to community chat. |
| src/routes/[[lang]]/app/+page.server.ts | Redirects /app to /app/community-chat. |
| src/routes/[[lang]]/app/+layout.svelte | Adds sidebar thread queries + warm-thread prewarming; passes search + fullControl to layout. |
| src/routes/[[lang]]/admin/support/thread-chat.svelte | Minor ChatInput class adjustment (removes p-0). |
| src/lib/convex/schema.ts | Adds aiChatThreads table + indexes for user/thread/warm-thread tracking. |
| src/lib/convex/crons.ts | Adds cron to delete stale warm AI chat thread mappings. |
| src/lib/convex/aiChat/threads.ts | Implements AI chat thread list/create/delete + warm-thread helpers + stale-warm cleanup mutation. |
| src/lib/convex/aiChat/rateLimit.ts | Adds per-user rate limiter config for AI chat messages/uploads. |
| src/lib/convex/aiChat/messages.ts | Adds AI chat send/list message endpoints + internal streaming action. |
| src/lib/convex/aiChat/files.ts | Adds AI chat upload URL + uploaded-file registration to agent. |
| src/lib/convex/aiChat/agent.ts | Defines AI Chat Agent configuration (OpenRouter model + instructions). |
| src/lib/convex/_generated/api.d.ts | Regenerates Convex API typings to include aiChat modules. |
| src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte | Adds press effect (active:translate-y-px). |
| src/lib/components/ui/sidebar/sidebar-menu-button.svelte | Adds press effect (active:translate-y-px). |
| src/lib/components/team-switcher.svelte | Adds sample team switcher component used by demo sidebar. |
| src/lib/components/prompt-kit/prompt-suggestion/prompt-suggestion.svelte | Adjusts suggestion chip padding. |
| src/lib/components/prompt-kit/chat-container/chat-container-root.svelte | Adds scrollbar-gutter: stable to reduce layout shift. |
| src/lib/components/nav-user.svelte | Refactors user menu UI (currently placeholders). |
| src/lib/components/nav-projects.svelte | Adds sample “Projects” sidebar section component for demo sidebar. |
| src/lib/components/nav-main.svelte | Adds sample collapsible “Platform” nav component for demo sidebar. |
| src/lib/components/global-search/search-routes.ts | Updates search routes for community chat + adds AI chat route. |
| src/lib/components/authenticated/types.ts | Extends nav item types to support collapsible + sub-items + disableNav. |
| src/lib/components/authenticated/configs/app-sidebar-config.ts | Updates app sidebar config: remove dashboard/docs/home; add AI chat w/ warm thread + sub-items. |
| src/lib/components/authenticated/authenticated-sidebar.svelte | Adds collapsible AI chat nav section with persisted open state + auto-animate. |
| src/lib/components/authenticated/authenticated-header.svelte | Adds bg-sidebar/30 header background. |
| src/lib/components/app/app-page-title.svelte | Removes unused AppPageTitle component. |
| src/lib/components/app-sidebar.svelte | Adds sample “AppSidebar” component with mock data (demo). |
| src/lib/chat/ui/ChatRoot.svelte | Adds svelte-ignore comments to clarify stable references. |
| src/lib/chat/ui/ChatMessages.svelte | Constrains message area to max-w-3xl and repositions scroll button responsively. |
| src/lib/chat/ui/ChatInput.svelte | Updates input container styling + adds animated suggestion chip entry + attachment spacing. |
| src/lib/chat/ui/ChatContext.svelte.ts | Removes client-side image processing; uploads original files and only extracts dimensions. |
| src/lib/chat/ui/ChatAttachments.svelte | Tweaks active press behavior and a11y notes for clickable attachments. |
| src/i18n/fr.json | Removes dashboard strings; adds AI chat strings + sidebar keys. |
| src/i18n/es.json | Removes dashboard strings; adds AI chat strings + sidebar keys. |
| src/i18n/en.json | Removes dashboard strings; adds AI chat strings + sidebar keys. |
| src/i18n/de.json | Removes dashboard strings; adds AI chat strings + sidebar keys. |
| package.json | Adds @formkit/auto-animate; updates @lucide/svelte version. |
| e2e/utils/convex-url.ts | Adds shared Convex URL resolver (env or .convex/.backend-url). |
| e2e/utils/auth.ts | Updates authenticated-shell selector used by tests. |
| e2e/support-migration.spec.ts | Uses resolveConvexUrl(); improves missing-URL error message. |
| e2e/signout.spec.ts | Updates selectors for user menu and logout. |
| e2e/global-teardown.ts | Uses resolveConvexUrl(). |
| e2e/global-setup.ts | Uses resolveConvexUrl(); improves missing-URL error message. |
| e2e/ai-chat.spec.ts | Adds E2E coverage for warm-thread navigation behavior. |
| e2e/admin-users-table.spec.ts | Uses resolveConvexUrl(); improves missing-URL error message. |
| bun.lock | Locks new dependency versions. |
| btca.config.jsonc | Updates btca resource config (removes searchPath entries; adds autoAnimate resource). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Send a user message and get AI response with streaming | ||
| * | ||
| * Pro-only enforcement is done client-side (page gates non-Pro users). | ||
| * Supports multimodal messages with file and image attachments. | ||
| */ | ||
| export const sendMessage = authedMutation({ | ||
| args: { | ||
| threadId: v.string(), | ||
| prompt: v.string(), | ||
| fileIds: v.optional(v.array(v.string())) | ||
| }, | ||
| handler: async (ctx, args) => { | ||
| if (args.prompt.length > 2000) { | ||
| throw new ConvexError('Message is too long (max 2000 characters)'); | ||
| } | ||
|
|
||
| const userId = ctx.user._id; | ||
|
|
||
| // Verify thread ownership | ||
| const record = await ctx.db | ||
| .query('aiChatThreads') | ||
| .withIndex('by_thread', (q) => q.eq('threadId', args.threadId)) | ||
| .first(); | ||
| if (!record || record.userId !== userId) { | ||
| throw new ConvexError('Thread not found'); | ||
| } | ||
|
|
||
| // Consume warm thread on first message (backend-driven, no client coordination needed) | ||
| if (record.isWarm) { | ||
| await ctx.db.patch(record._id, { isWarm: false }); | ||
| } | ||
|
|
||
| // Rate limit check | ||
| const rateLimitStatus = await aiChatRateLimiter.limit(ctx, 'aiChatMessage', { key: userId }); | ||
| if (!rateLimitStatus.ok) { | ||
| throw new ConvexError('Too many messages. Please wait a moment.'); | ||
| } |
There was a problem hiding this comment.
This mutation is effectively a Pro-only feature, but the backend enforcement is explicitly client-side only. Any authenticated user can still call this mutation directly and incur LLM cost. Please enforce Pro/entitlement on the server (e.g. via Autumn check/feature gating similar to src/lib/convex/messages.ts) before saving/scheduling the AI response.
| /** | ||
| * Generate a URL for uploading files to Convex storage | ||
| * | ||
| * Rate limited per authenticated user. | ||
| */ | ||
| export const generateUploadUrl = mutation({ | ||
| args: {}, | ||
| handler: async (ctx) => { | ||
| const user = await authComponent.getAuthUser(ctx); | ||
| if (!user) { | ||
| throw new ConvexError('Authentication required'); | ||
| } | ||
|
|
||
| const rateLimitStatus = await aiChatRateLimiter.limit(ctx, 'aiChatFileUpload', { | ||
| key: user._id | ||
| }); | ||
| if (!rateLimitStatus.ok) { | ||
| throw new ConvexError('Too many file uploads. Please try again later.'); | ||
| } | ||
|
|
||
| return await ctx.runMutation(components.convexFilesControl.upload.generateUploadUrl, { | ||
| provider: 'convex' | ||
| }); | ||
| } |
There was a problem hiding this comment.
File upload endpoints don’t enforce the Pro/entitlement requirement. If AI Chat is Pro-only, these should also verify Pro/allowed status on the server (otherwise non-Pro users can upload and store files / consume upload rate limits and resources).
| <Avatar.Root class="size-8 rounded-lg"> | ||
| <Avatar.Image src={user.avatar} alt={user.name} /> | ||
| <Avatar.Fallback class="rounded-lg">{initials}</Avatar.Fallback> | ||
| <Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback> | ||
| </Avatar.Root> |
There was a problem hiding this comment.
Avatar fallback is hard-coded to "CN". This will show incorrect initials for most users and breaks the previous behavior. Consider deriving initials from the provided user name (and keeping “?” as a safe fallback when name is empty).
| async function handleUpgrade() { | ||
| haptic.trigger('light'); | ||
| const result = await upgradeOperation.execute({ | ||
| productId: 'pro', | ||
| successUrl: page.url.href + '?upgraded=true' | ||
| }); |
There was a problem hiding this comment.
successUrl is built by string-concatenating page.url.href + '?upgraded=true'. If the current URL already has query params (e.g. ?thread=...), this will produce an invalid URL with multiple ?. Build this via new URL(page.url) and searchParams.set('upgraded','true') to preserve existing params.
| <Sidebar.Provider> | ||
| <AppSidebar /> | ||
| <Sidebar.Inset> | ||
| <header | ||
| class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12" | ||
| > | ||
| <div class="flex items-center gap-2 px-4"> | ||
| <Sidebar.Trigger class="-ms-1" /> | ||
| <Separator orientation="vertical" class="me-2 data-[orientation=vertical]:h-4" /> | ||
| <Breadcrumb.Root> | ||
| <Breadcrumb.List> | ||
| <Breadcrumb.Item class="hidden md:block"> | ||
| <Breadcrumb.Link href="##">Build Your Application</Breadcrumb.Link> | ||
| </Breadcrumb.Item> | ||
| <Breadcrumb.Separator class="hidden md:block" /> | ||
| <Breadcrumb.Item> | ||
| <Breadcrumb.Page>Data Fetching</Breadcrumb.Page> | ||
| </Breadcrumb.Item> |
There was a problem hiding this comment.
This introduces a new publicly routable /sidebar-07 page with placeholder content (e.g. href="##" and mock UI blocks). If this is only intended as a local/demo page from a UI template, it should be moved to a non-shipping area (or removed) so it doesn’t appear in production builds and get indexed.
| $effect(() => { | ||
| if (!threadId && !resolvingThread && viewer.data) { | ||
| resolvingThread = true; | ||
| client.mutation(api.aiChat.threads.getOrCreateWarmThread, {}).then((result) => { | ||
| const url = new URL(page.url); | ||
| url.searchParams.set('thread', result.threadId); | ||
| goto(resolve(url.pathname + url.search), { noScroll: true, replaceState: true }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
The warm-thread resolver sets resolvingThread = true but never resets it, and it doesn’t handle rejection from the mutation. If the mutation fails, this can leave the page stuck without retry. Handle errors (toast/log) and reset resolvingThread in a .finally() (or use an async IIFE with try/finally).
| export const listThreads = authedQuery({ | ||
| args: { | ||
| paginationOpts: v.optional(paginationOptsValidator) | ||
| }, | ||
| handler: async (ctx, _args) => { | ||
| const userId = ctx.user._id; | ||
|
|
||
| // Get user's AI chat thread records (exclude warm/pre-warmed threads) | ||
| const aiChatThreadRecords = await ctx.db | ||
| .query('aiChatThreads') | ||
| .withIndex('by_user', (q) => q.eq('userId', userId)) | ||
| .order('desc') | ||
| .collect(); | ||
|
|
There was a problem hiding this comment.
listThreads declares a paginationOpts argument and docs say the result is paginated, but the implementation ignores args and always collect()s all records + does per-thread queries. This can become slow for users with many threads. Either implement pagination using args.paginationOpts (and ideally avoid N+1 by batching), or remove the pagination arg/docs to match behavior.
| // Bounded: warm threads are rare (1 per user max), take(100) is safe | ||
| export const deleteStaleWarmThreads = internalMutation({ | ||
| args: {}, | ||
| returns: v.object({ deleted: v.number() }), | ||
| handler: async (ctx) => { | ||
| const cutoffTime = Date.now() - 7 * 24 * 60 * 60 * 1000; | ||
|
|
||
| // Bounded: at most 1 warm thread per user, take(100) is safe | ||
| const staleRecords = await ctx.db | ||
| .query('aiChatThreads') | ||
| .filter((q) => | ||
| q.and(q.eq(q.field('isWarm'), true), q.lt(q.field('_creationTime'), cutoffTime)) | ||
| ) | ||
| .take(100); | ||
|
|
||
| let deleted = 0; | ||
| for (const record of staleRecords) { | ||
| await ctx.db.delete(record._id); | ||
| deleted++; | ||
| } |
There was a problem hiding this comment.
deleteStaleWarmThreads only deletes the aiChatThreads mapping record, but the underlying agent thread created for the warm thread remains in the agent component tables. That means warm threads can still accumulate over time despite this cleanup job. Consider also archiving/deleting the corresponding agent thread (and/or deleting its messages/streams) when removing the mapping record.
| await page.locator('#user-menu-trigger').click(); | ||
| await page.locator('[data-testid="logout-button"]').click(); | ||
| await page.locator('[data-sidebar="menu-button"][data-size="lg"]').click(); | ||
| await page.getByText('Log out').click(); |
There was a problem hiding this comment.
The test clicks the logout item via visible text ("Log out"). If the UI is localized (and especially if NavUser returns to using Tolgee keys), this becomes brittle. Prefer a stable selector like data-testid on the logout menu item.
| await page.getByText('Log out').click(); | |
| await page.getByTestId('logout-menu-item').click(); |
| ? thread.lastMessage.length > 30 | ||
| ? thread.lastMessage.slice(0, 30) + '...' | ||
| : thread.lastMessage | ||
| : 'New conversation', |
There was a problem hiding this comment.
The fallback label "New conversation" is hard-coded English in the sidebar config. Since the rest of the app uses Tolgee keys, this should be localized (e.g. use a translation key like ai_chat.thread.no_messages for the fallback, or pass a flag/type and let the sidebar render the translated fallback).
| : 'New conversation', | |
| : 'ai_chat.thread.no_messages', |
Remove unused sidebar entries and the dashboard page. Redirect /app to /app/community-chat instead of /app/dashboard.
Use centered max-w-3xl container, inline heading with separator, and messages-square icon. Remove decorative icons and unused AppPageTitle component.
- Add AI chat feature with thread management and Convex backend - Add new app sidebar components (team switcher, nav) - Add chip-in CSS animation for chat suggestion chips - Add bg-sidebar/30 header background for visual hierarchy - Remove searchPath from btca config resources - Add autoAnimate btca resource
- Use bg-popover for chat input container - Align message padding (px-8) with input text offset - Align attachment preview with action buttons (mx-3 mt-3) - Use px-4 padding for suggestion chips - Remove extra pt-1 from input container
Vite writes backend URL to .convex/.backend-url so Playwright can auto-discover it. Adds shared resolveConvexUrl() utility used by all test files. Fixes broken #user-menu-trigger selector in auth utils. Adds AI Chat warm thread E2E tests.
Reverts shadcn demo nav-user that was accidentally committed, restoring Autumn billing, Pro badge, sign out, and initials.
6f190be to
cba3277
Compare
Restore #user-menu-trigger selector that was overwritten by shadcn demo code.
Fix successUrl corruption that caused "Thread not found" after upgrading to Pro. The URL now properly uses the URL API instead of naive string concatenation. Also adds server-side Pro checks in sendMessage and file upload mutations, fixes autoAnimate cleanup leak in sidebar, and localizes the "New conversation" fallback label.
Defense-in-depth check should only hard-block on definitive denial (allowed=false), not on API errors or missing data. The UI remains the primary gate for Pro enforcement.
Safari's heuristics scan for "username" in the name attribute to identify the credential field. Also adds action/method to signal login intent even though preventDefault() intercepts submission.
- Focus textarea when navigating to AI chat thread - Add global Cmd+N / Ctrl+N shortcut to start new chat - Show KBD badge next to AI Chat in sidebar
- App: ⌘1 Community Chat, ⌘2 AI Chat, ⌘; Admin, ⌘, Settings - Admin: ⌘1-4 nav items, ⌘; Back to App - Auto-focus chat input on mount and keyboard navigation - Show KBD badges on hover for all nav and footer items
⌘1-9 conflicts with browser tab switching. ⌘; requires Shift+, on German keyboards which conflicts with ⌘,. Use ⌘. instead.
Merge activity
|

Summary
⌘⇧Onew chat shortcutautocomplete=username), form action attributesdata-testidselectorsTest plan
⌘⇧Oopens new AI chat thread with input focused