Skip to content

feat: App sidebar, AI chat, and community chat redesign#264

Merged
stickerdaniel merged 68 commits intomainfrom
fix/app-sidebar-and-chat
Mar 29, 2026
Merged

feat: App sidebar, AI chat, and community chat redesign#264
stickerdaniel merged 68 commits intomainfrom
fix/app-sidebar-and-chat

Conversation

@stickerdaniel
Copy link
Copy Markdown
Owner

@stickerdaniel stickerdaniel commented Mar 26, 2026

Summary

  • App sidebar: New collapsible sidebar with AI chat thread listing, relative timestamps, "Show more" pagination, and ⌘⇧O new chat shortcut
  • AI chat: Thread management with warm pre-creation, metered billing (Autumn), auto-focus input, streaming with optimistic updates
  • Community chat redesign: Complete UI rewrite with optimistic updates, WhatsApp-style bubbles with inline timestamps and avatars, PromptInput with auto-resize
  • Billing: Metered features for both chats (3 free msgs/month, Pro unlimited/30 AI), client-side gating with server-side tracking via scheduled actions
  • Auth fixes: Safari password manager compatibility (autocomplete=username), form action attributes
  • E2E: Local Convex backend support, stable data-testid selectors

Test plan

  • Community chat: send message → appears instantly (optimistic), avatars + timestamps visible
  • AI chat: send message → streaming response, billing tracked
  • Free user: 3 msgs then upgrade banner shows
  • ⌘⇧O opens new AI chat thread with input focused
  • Sidebar: threads show relative time, "Show more" loads additional threads smoothly
  • E2E tests pass against local Convex backend

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 26, 2026

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

Project Deployment Actions Updated (UTC)
saas-starter Ready Ready Preview, Comment Mar 29, 2026 0:19am

Copy link
Copy Markdown
Owner Author

stickerdaniel commented Mar 26, 2026

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Mar 26, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​lucide/​svelte@​1.7.0 ⏵ 0.577.01001009796100 +20

View full report

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Mar 26, 2026

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.

View full report

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 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/dashboard page 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.

Comment on lines +15 to +52
/**
* 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.');
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +42
/**
* 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'
});
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment thread src/lib/components/nav-user.svelte Outdated
Comment on lines 27 to 30
<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>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +50
async function handleUpgrade() {
haptic.trigger('light');
const result = await upgradeOperation.execute({
productId: 'pro',
successUrl: page.url.href + '?upgraded=true'
});
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +25
<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>
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +42
$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 });
});
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +26
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();

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +194 to +213
// 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++;
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread e2e/signout.spec.ts Outdated
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();
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
await page.getByText('Log out').click();
await page.getByTestId('logout-menu-item').click();

Copilot uses AI. Check for mistakes.
? thread.lastMessage.length > 30
? thread.lastMessage.slice(0, 30) + '...'
: thread.lastMessage
: 'New conversation',
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
: 'New conversation',
: 'ai_chat.thread.no_messages',

Copilot uses AI. Check for mistakes.
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.
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
@stickerdaniel stickerdaniel changed the title fix(sidebar): Remove Dashboard, Docs, and Start nav items feat: App sidebar, AI chat, and community chat redesign Mar 28, 2026
- 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.
Copy link
Copy Markdown
Owner Author

stickerdaniel commented Mar 29, 2026

Merge activity

  • Mar 29, 12:28 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Mar 29, 12:28 AM UTC: @stickerdaniel merged this pull request with Graphite.

@stickerdaniel stickerdaniel merged commit 02c035d into main Mar 29, 2026
8 checks passed
@stickerdaniel stickerdaniel deleted the fix/app-sidebar-and-chat branch March 29, 2026 00:28
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.

2 participants