Session Management in AI Chat Applications
AI chat apps feel simple on the surface: a prompt box, a response stream, maybe some citations. Underneath, session management gets complicated fast:
- How do you persist conversation history?
- How much context do you send to the model?
- What happens when users refresh, switch devices, or lose connectivity?
- How do you keep the UI fast and accessible while handling all of this?
Good session management makes your AI app feel like a reliable collaborator, not a forgetful toy. The AI UX Kit includes patterns and hooks that help you get this right from day one.
🧠 What “Session” Means in AI Chat
A “session” is more than a single request/response:
- Conversation history: user + assistant messages across multiple turns.
- Context window: what subset of that history you send to the model.
- Metadata: user ID, model version, feature flags, experiments.
- Persistence: how long sessions live, how they’re resumed, and where they’re stored.
Your UI needs to:
- Render history efficiently.
- Stream new messages.
- Handle reconnects and errors.
- Keep everything keyboard- and screen-reader-friendly.
🧩 Core UI Pattern: Session Chat Hook
The AI UX Kit ships with session-aware chat hooks (e.g. use-session-chat) that centralize:
- Message state
- Loading and error states
- Basic persistence strategy
Here’s what a simplified usage pattern looks like:
import { useSessionChat } from "@/app/recipes/session/chat/hooks/use-session-chat"
import { PromptInput } from "@/components/ui/composites/prompt-input"
import { ResponseViewer } from "@/components/ui/composites/response-viewer"
export default function SessionChatPage() {
const {
messages,
isLoading,
error,
sendMessage,
retryLastMessage,
} = useSessionChat({
sessionId: "customer-support-session",
})
const handleSubmit = (value: string) => {
sendMessage({ content: value })
}
return (
<div className="flex flex-col h-screen">
<div className="flex-1 overflow-auto p-6 space-y-4">
{messages.map((message) => (
<div key={message.id} className={message.role === "user" ? "text-right" : "text-left"}>
{message.role === "assistant" ? (
<ResponseViewer content={message.content} />
) : (
<div className="inline-block bg-primary-50 rounded-lg px-4 py-2">
{message.content}
</div>
)}
</div>
))}
{error && (
<div className="mt-4 p-3 rounded-md bg-red-50 text-sm text-red-800 flex items-center justify-between">
<span>{error.message}</span>
<button
type="button"
onClick={retryLastMessage}
className="ml-4 text-xs underline"
>
Retry
</button>
</div>
)}
</div>
<div className="border-t p-4">
<PromptInput
onSubmit={(value) => handleSubmit(value)}
disabled={isLoading}
actionButton="send"
/>
</div>
</div>
)
}
The heavy lifting (state management, pending states, basic retries) lives in the hook, not scattered across components.
🗄️ Where to Store Session Data
Session data can live in multiple places:
- In-memory (React state): fast, but lost on refresh.
- Local storage: persists across reloads, but only on one device.
- Backend storage: durable and multi-device, but requires APIs.
The AI UX Kit is agnostic about where you store your data—it focuses on UI and UX—but its patterns make it easy to adapt.
Example: hydrating from persisted history:
const { messages, sendMessage, hydrateFromHistory } = useSessionChat({ sessionId })
React.useEffect(() => {
const saved = window.localStorage.getItem(`chat:${sessionId}`)
if (saved) {
hydrateFromHistory(JSON.parse(saved))
}
}, [sessionId, hydrateFromHistory])
React.useEffect(() => {
window.localStorage.setItem(`chat:${sessionId}`, JSON.stringify(messages))
}, [sessionId, messages])
Now sessions survive a refresh without any UI changes.
📏 Managing Context Windows
LLM context windows force you to decide how much history to send with each prompt.
Common strategies:
- Last N turns: send only the most recent messages (fast, simple).
- Summarized history: compress older messages into a running summary.
- Segmented sessions: split long conversations into chapters.
The UI needs to show the full history, even if the model only sees part of it.
Pseudocode for a last-N strategy:
function buildModelPayload(messages: ChatMessage[]) {
const MAX_TURNS = 12
const visibleHistory = messages.slice(-MAX_TURNS)
return visibleHistory.map((m) => ({
role: m.role,
content: m.content,
}))
}
You can implement this in your sendMessage handler, leaving the UI unchanged.
🔄 Handling Errors and Retries Gracefully
Network hiccups and model errors are inevitable. Bad UIs:
- Lose the user’s message.
- Show generic “Something went wrong” banners.
- Offer no path to retry.
Better pattern (supported by AI UX Kit recipes):
- Preserve the user’s message in the history.
- Show per-message error states.
- Allow “Retry” on that specific message.
// inside message rendering
{message.role === "assistant" && message.status === "error" && (
<button
type="button"
onClick={() => retryMessage(message.id)}
className="mt-2 text-xs text-red-700 underline"
>
Retry this response
</button>
)}
The recipes in the kit encode patterns like error-handling chat, so you’re not starting from an empty file.
♿ Accessibility in Session UIs
Session management isn't just about data—it's also about how the experience feels:
- Screen readers should announce new messages progressively.
- Keyboard users should be able to:
- Jump to the latest message,
- Re-open prompts,
- Interact with citations and feedback.
AI UX Kit's ResponseViewer and chat recipes are built with:
- Proper
aria-liveregions for streaming content. - Focusable elements for citations and controls.
- Predictable tab order across messages and input.
Example (simplified streaming component):
<ResponseViewer
content={streamedContent}
format="markdown"
showCitations
aria-live="polite"
aria-atomic={false}
/>
You don’t have to become an ARIA expert to get a reasonable default.
🔐 Multi-Session Interfaces
Many apps need multiple concurrent sessions:
- Conversations per project, customer, or case.
- “Tabs” for different experiments.
Pattern:
- Left sidebar lists sessions.
- Right panel shows the active session chat.
- All wired to the same session hook with different
sessionIds.
function SessionsLayout({ sessionId }: { sessionId: string }) {
const sessions = useUserSessions() // your data source
return (
<div className="flex h-screen">
<aside className="w-72 border-r bg-secondary-50">
{/* session list */}
{sessions.map((session) => (
<button
key={session.id}
className="w-full text-left px-4 py-2 hover:bg-secondary-100"
onClick={() => router.push(`/sessions/${session.id}`)}
>
<div className="text-sm font-medium">{session.title}</div>
<div className="text-xs text-secondary-600">
Updated {session.updatedAtRelative}
</div>
</button>
))}
</aside>
<main className="flex-1">
<SessionChatPage key={sessionId} />
</main>
</div>
)
}
This mirrors the structure of the AI UX Kit's session recipes while letting you plug in your own persistence.
✅ Session Management Checklist
Before shipping your AI chat experience, check:
-
History
- Users can see past messages after refresh.
- Old messages don’t break layout on mobile.
- Long histories still feel fast.
-
Context
- You have a clear strategy for what goes to the model.
- You don’t spam the model with the entire history on every call.
-
Resilience
- Network errors preserve the user’s message.
- Users can retry specific messages.
- Timeouts/exceptions show clear, actionable errors.
-
Accessibility
- New messages are announced to screen readers.
- Keyboard users can navigate messages and actions.
- Focus doesn’t jump unexpectedly during streaming.
-
Multi-session
- Sessions have stable IDs and URLs.
- Switching sessions is fast and predictable.
- Session titles or metadata make sense to users.
🚀 How the AI UX Kit Helps
Instead of hand-rolling all of this, you can:
- Use session chat recipes (
/recipes/session/chat) as a starting point. - Use prompt input, response viewer, and feedback components that already respect accessibility and tokenized styling.
- Adapt the included hooks (
use-session-chat,use-error-chat, etc.) to your persistence and model APIs.
Explore the AI UX Kit: View components
With solid session management in place, your AI chat stops feeling like a toy—and starts feeling like a product users can trust. For more on building complete AI interfaces, see Production-Ready AI Interfaces with Next.js.


