Use this skill to keep the template's web chat feeling like a polished dashboard surface: stable, quiet, fast to first paint, and predictable under streaming state.
Product Shape
Build the actual chat surface, not a marketing page. The first screen should be a usable composer, sidebar, or chat route.
Keep the aesthetic restrained and work-focused: dense but readable spacing, quiet borders, predictable controls, and minimal decoration.
Prefer existing shadcn/Tailwind primitives and local components over new abstractions.
Use icons for compact commands, with accessible labels/tooltips. Avoid explanatory in-app text for behavior that good controls can imply.
Cache Components And PPR Basics
Keep cacheComponents: true enabled in next.config.ts. Treat the chat route as a static shell with dynamic data streamed into Suspense boundaries.
Keep the shell cheap and deterministic. Do not make app/(chat)/layout.tsx wait on auth, database, chat history, or per-chat data before rendering the shell.
Pass cheap initial state into AgentChatShell: empty history, initialNextCursor: null, viewer: null, and getInitialSetupStatus().
Load expensive or request-bound data inside hidden Suspense children: ResolvedChatBootstrap for setup/viewer/sidebar history and ExistingChat for active chat data.
Use sync components to hand resolved server data to the client shell. AgentChatBootstrapSync should update setup status, viewer, and history. AgentChatRouteSync should update the active chat for the current route.
Keep Suspense fallbacks null for these sync boundaries when the visible shell already has stable placeholders. Do not add loading panels that push the chat frame around.
Do not call notFound() until the dynamic boundary has enough real viewer/setup/chat data to prove the chat is inaccessible. The outer shell should still render while that decision is loading.
Keep root and session routes visually distinct but structurally compatible. The root page can center the composer; the session page can pin it near the bottom. Both should live inside the same shell.
Treat PPR/static shell work as a latency feature and a correctness feature. The goal is first paint without auth/database blocking, then precise data reconciliation after bootstrap.
Static Shell And Hydration
Preserve the shell-first strategy. AgentChatShell should paint the sidebar frame, top controls, and route body before auth, setup, or chat history finishes loading.
Keep bootstrap data out of the blocking route path. Use hidden Suspense sync components to hydrate setup status, viewer, sidebar history, and active chat data.
Preserve theme pre-hydration. Theme should be resolved before React paints so dark/light mode does not flash or mismatch.
Preserve auth display pre-hydration. The eve_logged_in cookie is a display hint only; the head script sets data-eve-auth-display so CSS can hide the wrong signed-in/signed-out branch before viewer data resolves.
Never treat the auth hint as authority. Server actions, chat loading, Eve auth, and rate limits must use the real Better Auth session.
Keep suppressHydrationWarning only where it protects known pre-hydration document mutations such as theme/auth display hints.
Same Route, Signed In Or Out
Render one shared route shell for signed-in and signed-out states. Do not fork the page into separate auth routes just to show different controls.
While bootstrap is loading, render both possible placeholders inside AuthDisplayLoggedIn and AuthDisplayLoggedOut; let the document dataset hide the wrong branch.
For signed-out users, show the Sign in affordance immediately. For signed-in users, show a neutral user-menu placeholder instead of flashing Sign in.
After bootstrap resolves, render from real viewer state: viewer ? <UserMenu /> : <SidebarSignInButton />.
When a signed-out user submits text, save the draft to sessionStorage, open the auth modal, and restore the draft after sign-in.
Optimistic Messaging And Row Identity
Show an optimistic user bubble immediately on send, but keep persisted Eve events as the source of truth.
Clear the optimistic user bubble only when the reduced event log contains the real latest user message.
Keep live chat state centralized where possible. Avoid rendering from multiple competing sources in the same frame.
Prefer stable row identities for streaming responses. A "thinking" or "streaming reply" row should become the assistant row without remounting into a different component path.
Use a separate thinking presence only when there is no meaningful assistant text yet or a turn is still finalizing. Fade it out rather than abruptly removing it.
Avoid letting final snapshot writes fight live state. During streaming, prefer the live source; after settle, reconcile with the canonical snapshot.
Composer And Sidebar
Keep ChatComposer controlled by the page: value, onChange, onSubmit, disabled, disabledReason, isBusy, and isPreparing.
Enter should submit. Shift+Enter should insert a newline. Disabled states should expose actionable tooltip text.
Busy state should show Stop. Preparing state should show a spinner. Empty or over-limit input should disable send without moving layout.
Root and session pages can place the composer differently, but they should share the same component and control contract.
Persist desktop sidebar open state in a cookie and write a pre-hydration document hint to avoid open-then-collapse flicker.
Keep sidebar history cursor-paginated and optimistic. Use touchChat, removeChat, and title updates for sidebar state; do not make sidebar state the transcript source of truth.
Keep mobile sidebar overlay behavior separate from desktop persistent state.
Message Rendering
Render user messages as compact right-aligned bubbles. Render assistant text as full-width left-aligned prose.
Use Streamdown for assistant markdown. Keep code blocks, lists, and inline code readable in both themes.
Group related dynamic tool parts into compact rows. Expand details only when there is useful input, output, error, or HITL control.
Render reasoning as a collapsible block. Label active reasoning as "Thinking..." and settled reasoning as "Reasoning".
Render connection authorization as a structured card, not prose embedded in assistant text.
Keep text inside controls and cards from overflowing at mobile widths. Prefer stable dimensions for buttons, tool rows, counters, and composer controls.
Styling Rules
Use the existing token system: background, foreground, muted, border, card, accent, and local opacity patterns.
Keep cards modest. Do not nest cards inside cards. Use cards for messages, modals, repeated items, or framed controls, not whole-page decoration.
Keep radius consistent with the template. Avoid large pill shapes unless the local component already uses them.
Avoid large decorative gradients, orbs, blobs, and marketing-style hero layouts in the chat app.
Use lucide icons when an icon exists. Add aria-label for icon-only buttons and tooltip text where meaning is not obvious.
Avoid layout shifts from loading, hover, or stream state. Reserve stable space for composer controls, sidebar rows, auth placeholders, and thinking/tool rows.
Verification
Run pnpm typecheck and pnpm build after UI or app-router changes.
Manually check root signed-out, root signed-in, auth modal draft restore, first-message navigation, follow-up optimistic send, streaming assistant row, Connect/Skip auth cards, theme first paint, desktop sidebar cookie restore, and mobile sidebar overlay when touched.
Use screenshots or browser checks for visual changes across desktop and mobile. Watch specifically for auth flicker, theme flash, composer jumps, sidebar jumps, and message remount flicker.
How to use it
Copy the folder
Take vercel-labs/dashboard-chat-best-practices from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
Check the name does not clash
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.