commits
## Character Roster Expansion & Chat UX Improvements
### New Characters
Adds six new characters alongside expanded and renamed existing ones, bringing the total roster to nine. Each character now has a `description` field shown as a subtitle in the character selector:
- **Yuki** (formerly unnamed Kitsune) — sly, flirty fox girl
- **Momo** (formerly unnamed Neko) — bratty, needy cat girl
- **Hana** — bouncy, loyal puppy girl
- **Suzu** — soft, cuddly bunny girl
- **Natsuki** — sharp, flustered tsundere
- **Yuri** — shy, intense dandere
- **Sayori** — sunny, clingy deredere
- **Rei** — cool, quiet kuudere
- **Monika** — polished, teasing onee-san
Character prompts are now generated via a shared `buildPrompt` helper that enforces consistent rules and response style guidelines across all characters, replacing the previous ad-hoc prompt strings.
### Starter Prompts
Replaces the static fox girl image and attribution on the new chat screen with a grid of four randomly selected conversation starters. Selecting a starter pre-fills the chat composer input. A pool of 50 prompts is defined in `lib/prompts.ts` with a Fisher-Yates shuffle for random selection.
### Model List Updates
Adds `gpt-5.5` (now first in the list) and `gpt-5.4-nano`, and reorders `gpt-5.4-mini` after `gpt-5.4`.
### Component Refactors
- `CharacterSelect` and `ModelSelect` now accept their data as props rather than fetching it internally via hooks, making them easier to control from the parent `ChatForm`.
- `ChatComposer` and `ChatForm` accept optional controlled `text`/`onTextChange` props, enabling the starter prompt selection to pre-fill the input.
- A new `Grid` layout component is added with responsive column and gap variants.
- The initial character selection on a new chat is now randomized rather than always defaulting to the first character.
- The logo link in the sidebar header now points to `/chats` instead of `/`.
Replace `expectApiResponse` with `createApiError` for more explicit error handling across all API hooks.
Previously, `expectApiResponse` wrapped the entire fetch call in a try/catch, obscuring network errors under a generic fallback message and hiding the response from the caller. The new `createApiError` function accepts an already-resolved `Response` and returns a constructed `ApiError`, leaving the fetch call and `response.ok` check inline at each call site.
The error message construction has also been improved: if the response body contains an error message that differs from the fallback, both are combined as `"<fallback>: <response message>"` rather than discarding one in favor of the other.
## Improve API error handling and query retry behavior
Improves error messages returned from the chats API to be more descriptive, replacing generic "Not found" and "Invalid character" messages with context-specific ones like "Chat not found" and "Character not found".
Adds a `shouldRetryApiError` utility that prevents React Query from retrying requests that fail with 4xx client errors, since these are deterministic failures that won't resolve on retry. Only 5xx server errors and network-level failures will be retried, up to a maximum of 2 attempts.
Removes the special-case 404 handling in `useChat` that was silently swallowing not-found errors and returning `undefined`, allowing the error to propagate normally instead.
Enables `richColors` on the Sonner toast component so that error and success toasts are visually distinct.
## Add AI rate limiting and improve API error handling
### Rate Limiting
Adds per-user rate limiting for AI endpoints using Upstash Redis and `@upstash/ratelimit`. Users are limited to **10 requests per minute** and **120 requests per hour**. When a limit is exceeded, the API returns a `429` response with a human-readable retry message (e.g. "Rate limit exceeded. Please try again in 2 minutes.") along with standard `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. Rate limiting is enforced before inserting user messages, so messages are only persisted when the request is allowed through.
Also fixes a bug where the title generation condition checked `messages.length === 1` instead of `messages.length === 0`, and corrects the message list passed to `streamChat` to include the new user message that was previously being omitted.
### API Error Handling
Introduces a shared `ApiError` class and `expectApiResponse` helper that extracts error messages from API responses (preferring the `message` field from JSON bodies) and throws typed errors. All hooks now use this helper instead of inline `!response.ok` checks.
The `QueryClient` is configured with global `QueryCache` and `MutationCache` error handlers that display `ApiError` messages as toast notifications via `sonner`, giving users visible feedback when API calls fail, including rate limit errors.
### Environment
Adds `.env.example` files for both `apps/api` and `apps/web` documenting the required environment variables, including the new `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` variables. The `.gitignore` is updated to allow `.env.example` files to be committed.
Replaces the `Button` component wrapping a `Link` in the chat sidebar header with a plain `Link` styled using `buttonVariants()`, removing the need for a render prop pattern.
Replaces the landing page with an automatic redirect to `/chats`, removing the welcome page UI and the now-unused `Page` component.
### TL;DR
Refactored the chat layout to use a sticky, absolutely-positioned composer that overlays the message list, and introduced a reusable `Card` component.
### What changed?
- Introduced a new `ChatComposer` component that wraps `ChatForm` inside a `Card`, positioned absolutely at the bottom of the chat view. `ChatForm` no longer accepts a `className` prop and has a more compact, borderless textarea style.
- Added a `Card` UI component with sub-components (`CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`) supporting `default` and `sm` size variants.
- `ChatMessages` now wraps its content in a `Container` with bottom padding to prevent messages from being hidden behind the fixed composer.
- The `Chat` component and the chats index route now use `ChatComposer` instead of `ChatForm` directly, and the layout uses `relative`/`absolute` positioning to anchor the composer to the bottom.
- `Container` now renders as a flex column by default.
- The `SidebarInset` in the chats route layout no longer wraps content in a `Container`, allowing each route to manage its own layout and sizing. The inset height is set to `calc(100dvh - 1rem)`.
- The send button becomes full-width on mobile, and the controls row wraps to a column on smaller screens.
### How to test?
1. Open the chat list page (`/chats`) and verify the composer is pinned to the bottom with the heading and mascot centered above it.
2. Open an existing chat and confirm messages scroll independently while the composer remains fixed at the bottom.
3. Resize the browser to a mobile viewport and confirm the send button spans full width and the controls stack vertically.
4. Verify the `Card` component renders correctly with both `default` and `sm` sizes.
### Why make this change?
The previous layout pushed the chat form inline within the page flow, causing inconsistent spacing and scroll behavior. Pinning the composer absolutely at the bottom provides a more conventional chat UI where the input is always visible and the message list scrolls freely beneath it.
### TL;DR
Adds the ability for users to edit their last message in a chat, which re-submits the conversation and streams a new assistant response.
### What changed?
- Added a new `POST /:id/messages/edit-last` API endpoint that updates the last user message and streams a regenerated assistant response.
- Added a `useEditLastMessage` hook that handles optimistic updates, streams the new assistant response chunk-by-chunk into the query cache, and invalidates relevant queries on completion.
- Introduced `ChatMessageBubble` and `ChatMessageEditForm` as dedicated components, extracted from `ChatMessage` to support the inline editing UI.
- Added an `EditMessageButton` component with a pencil icon that appears alongside the copy and regenerate actions on the last editable user message.
- `ChatMessages` now tracks which message is being edited via `editingMessageId` state, determines the last editable user message, and wires up edit/cancel/save handlers.
- Edit and regenerate actions are mutually disabled while either operation is pending.
- `CopyButton` and `RegenerateMessageButton` updated to use `icon-sm` size.
### How to test?
1. Start a chat and send at least one message to receive an assistant reply.
2. Hover over your last user message — an edit (pencil) icon should appear alongside the copy button.
3. Click the edit icon to open the inline edit form with your original message pre-filled.
4. Modify the text and click **Save** (or press Enter). The user message should update and the assistant response should stream in fresh.
5. Alternatively, click **Cancel** to discard changes and return to the original message.
6. Verify that the edit button is disabled while a regeneration is in progress, and vice versa.
### Why make this change?
Users previously had no way to correct or revise a message after sending it. This feature allows iterating on a conversation without starting over, improving the overall chat experience.
### TL;DR
Adds the ability to regenerate the last assistant message in a chat.
### What changed?
- Added a `POST /:id/messages/regenerate` API endpoint that streams a regenerated response for the last assistant message in a chat, replacing its content in the database once streaming completes.
- Added a `RegenerateMessageButton` component (using a `RotateCcwIcon`) that appears in the message actions row for the last assistant message.
- Added a `useRegenerateLastMessage` hook that handles optimistically clearing the last assistant message text, streaming the new response chunk-by-chunk into the query cache, and invalidating relevant queries on completion or error.
- The regenerate button is only shown on the most recent assistant message that has non-empty text, and is disabled while a regeneration is in progress.
### How to test?
1. Open an existing chat that has at least one assistant response.
2. Hover over the last assistant message to reveal the action buttons.
3. Click the regenerate (↺) button and confirm the message clears and streams a new response.
4. Verify the regenerated content persists after the stream completes.
5. Confirm the button is disabled while regeneration is in progress and re-enables afterward.
### Why make this change?
Users may want to get a different response from the assistant without having to resend their message. This feature provides a straightforward way to retry the last assistant message in a chat.
### TL;DR
Tightened up the chat UI with minor spacing and sizing adjustments.
### What changed?
- Added `gap="xs"` to the chat messages container so messages have smaller spacing between them.
- Reduced the copy button size from `icon` to `icon-xs` for a more compact appearance.
### How to test?
1. Open a chat conversation and verify that messages are evenly spaced with a small gap between them.
2. Hover over a message and confirm the copy button appears smaller and still functions correctly when clicked.
### Why make this change?
The chat message list had too much spacing between messages, and the copy button was visually oversized relative to the surrounding UI elements. These tweaks improve the overall visual polish and consistency of the chat interface.
### TL;DR
Adds a copy-to-clipboard button that appears on hover/focus for each chat message.
### What changed?
- Added a `CopyButton` component that writes text to the clipboard using the Clipboard API. After copying, it briefly shows a checkmark icon for 1.5 seconds before reverting to the copy icon.
- Added a `ChatMessageActions` component that renders the `CopyButton` and is hidden by default, becoming visible when the message is hovered or focused.
- Updated `ChatMessage` to wrap the message bubble in a `Stack` with a `group` class, enabling the hover/focus visibility behavior for `ChatMessageActions`. Actions are only rendered when the message has content and is not in the typing state.
### How to test?
1. Open a chat and send or receive a message.
2. Hover over a message — a copy button should appear below it.
3. Click the copy button and verify the message text is copied to the clipboard.
4. Confirm the icon switches to a checkmark briefly before reverting back to the copy icon.
5. Verify the button does not appear while the assistant typing indicator is active.
### Why make this change?
Users need a convenient way to copy message content without manually selecting text, improving the overall usability of the chat interface.
### TL;DR
Disable chat form inputs when the user is not authenticated or auth state hasn't loaded yet.
### What changed?
The chat form now checks Clerk's authentication state (`isLoaded` and `isSignedIn`) before allowing interaction. The message textarea, send button, character selector, and model selector are all disabled when auth is unavailable (i.e., auth hasn't loaded or the user is not signed in).
### How to test?
1. Open the chat page while signed out and verify that the message input, character selector, and model selector are all disabled.
2. Sign in and confirm that all controls become interactive.
3. Verify that sending a message works as expected when authenticated.
### Why make this change?
Preventing interaction with the chat form before authentication is confirmed avoids potential errors from unauthenticated API calls. This ensures users cannot attempt to send messages or change chat settings before a valid session is established.
### TL;DR
Allow unauthenticated users to access the chats route, showing a "Sign In" button in the sidebar footer instead of redirecting them away.
### What changed?
The `/chats` route no longer redirects unauthenticated users to the sign-in page. Instead, a "Sign In" button is displayed in the sidebar footer when the user is signed out, using Clerk's `SignedOut` and `SignInButton` components. The `SignedIn` guard around the user button remains intact.
### How to test?
1. Open the app while signed out and navigate to `/chats`.
2. Verify you are no longer redirected to the sign-in page.
3. Confirm the sidebar footer displays a "Sign In" button.
4. Click the "Sign In" button and verify it triggers the Clerk sign-in flow.
5. Sign in and confirm the sidebar footer switches to showing the user button.
### Why make this change?
Redirecting unauthenticated users away from the chats route creates a poor experience for users who may want to explore the app before signing in. Surfacing a "Sign In" prompt directly in the sidebar is a softer, more welcoming approach that keeps users in context.
- Add Hono API routes for characters, chats, and streaming messages
- Add Drizzle SQLite schema, db config, validation, ids, and seed script
- Seed Kitsune and Neko characters with system prompts
- Use character prompt as the system prompt for streamed chat responses
- Replace shared package types with Hono client inferred frontend types
- Connect frontend chat/character hooks to the API client
- Add message sending hook with chat creation, navigation, optimistic
messages, and streaming
updates
- Add typing indicator for pending assistant responses
- Update character select to display character names while using ids
internally
- Remove unused shared package workspace
The API port is now hardcoded to 3001 in the `api` app.
The `web` app's proxy target is also updated to reflect this change.
This commit adds the `@types/bun` dependency to the `apps/api`
package.json.
It also removes the explicit Bun type declaration from
`apps/api/src/index.ts` as it is no longer needed.
The logo component has been updated to use an icon instead of text. The
welcome message on the home page now incorporates the new logo
component.
Introduces avatars to chat messages and displays character icons in the
character selection dropdown. Also includes styling updates for the chat
form and character select components.
## Character Roster Expansion & Chat UX Improvements
### New Characters
Adds six new characters alongside expanded and renamed existing ones, bringing the total roster to nine. Each character now has a `description` field shown as a subtitle in the character selector:
- **Yuki** (formerly unnamed Kitsune) — sly, flirty fox girl
- **Momo** (formerly unnamed Neko) — bratty, needy cat girl
- **Hana** — bouncy, loyal puppy girl
- **Suzu** — soft, cuddly bunny girl
- **Natsuki** — sharp, flustered tsundere
- **Yuri** — shy, intense dandere
- **Sayori** — sunny, clingy deredere
- **Rei** — cool, quiet kuudere
- **Monika** — polished, teasing onee-san
Character prompts are now generated via a shared `buildPrompt` helper that enforces consistent rules and response style guidelines across all characters, replacing the previous ad-hoc prompt strings.
### Starter Prompts
Replaces the static fox girl image and attribution on the new chat screen with a grid of four randomly selected conversation starters. Selecting a starter pre-fills the chat composer input. A pool of 50 prompts is defined in `lib/prompts.ts` with a Fisher-Yates shuffle for random selection.
### Model List Updates
Adds `gpt-5.5` (now first in the list) and `gpt-5.4-nano`, and reorders `gpt-5.4-mini` after `gpt-5.4`.
### Component Refactors
- `CharacterSelect` and `ModelSelect` now accept their data as props rather than fetching it internally via hooks, making them easier to control from the parent `ChatForm`.
- `ChatComposer` and `ChatForm` accept optional controlled `text`/`onTextChange` props, enabling the starter prompt selection to pre-fill the input.
- A new `Grid` layout component is added with responsive column and gap variants.
- The initial character selection on a new chat is now randomized rather than always defaulting to the first character.
- The logo link in the sidebar header now points to `/chats` instead of `/`.
Replace `expectApiResponse` with `createApiError` for more explicit error handling across all API hooks.
Previously, `expectApiResponse` wrapped the entire fetch call in a try/catch, obscuring network errors under a generic fallback message and hiding the response from the caller. The new `createApiError` function accepts an already-resolved `Response` and returns a constructed `ApiError`, leaving the fetch call and `response.ok` check inline at each call site.
The error message construction has also been improved: if the response body contains an error message that differs from the fallback, both are combined as `"<fallback>: <response message>"` rather than discarding one in favor of the other.
## Improve API error handling and query retry behavior
Improves error messages returned from the chats API to be more descriptive, replacing generic "Not found" and "Invalid character" messages with context-specific ones like "Chat not found" and "Character not found".
Adds a `shouldRetryApiError` utility that prevents React Query from retrying requests that fail with 4xx client errors, since these are deterministic failures that won't resolve on retry. Only 5xx server errors and network-level failures will be retried, up to a maximum of 2 attempts.
Removes the special-case 404 handling in `useChat` that was silently swallowing not-found errors and returning `undefined`, allowing the error to propagate normally instead.
Enables `richColors` on the Sonner toast component so that error and success toasts are visually distinct.
## Add AI rate limiting and improve API error handling
### Rate Limiting
Adds per-user rate limiting for AI endpoints using Upstash Redis and `@upstash/ratelimit`. Users are limited to **10 requests per minute** and **120 requests per hour**. When a limit is exceeded, the API returns a `429` response with a human-readable retry message (e.g. "Rate limit exceeded. Please try again in 2 minutes.") along with standard `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. Rate limiting is enforced before inserting user messages, so messages are only persisted when the request is allowed through.
Also fixes a bug where the title generation condition checked `messages.length === 1` instead of `messages.length === 0`, and corrects the message list passed to `streamChat` to include the new user message that was previously being omitted.
### API Error Handling
Introduces a shared `ApiError` class and `expectApiResponse` helper that extracts error messages from API responses (preferring the `message` field from JSON bodies) and throws typed errors. All hooks now use this helper instead of inline `!response.ok` checks.
The `QueryClient` is configured with global `QueryCache` and `MutationCache` error handlers that display `ApiError` messages as toast notifications via `sonner`, giving users visible feedback when API calls fail, including rate limit errors.
### Environment
Adds `.env.example` files for both `apps/api` and `apps/web` documenting the required environment variables, including the new `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` variables. The `.gitignore` is updated to allow `.env.example` files to be committed.
### TL;DR
Refactored the chat layout to use a sticky, absolutely-positioned composer that overlays the message list, and introduced a reusable `Card` component.
### What changed?
- Introduced a new `ChatComposer` component that wraps `ChatForm` inside a `Card`, positioned absolutely at the bottom of the chat view. `ChatForm` no longer accepts a `className` prop and has a more compact, borderless textarea style.
- Added a `Card` UI component with sub-components (`CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`) supporting `default` and `sm` size variants.
- `ChatMessages` now wraps its content in a `Container` with bottom padding to prevent messages from being hidden behind the fixed composer.
- The `Chat` component and the chats index route now use `ChatComposer` instead of `ChatForm` directly, and the layout uses `relative`/`absolute` positioning to anchor the composer to the bottom.
- `Container` now renders as a flex column by default.
- The `SidebarInset` in the chats route layout no longer wraps content in a `Container`, allowing each route to manage its own layout and sizing. The inset height is set to `calc(100dvh - 1rem)`.
- The send button becomes full-width on mobile, and the controls row wraps to a column on smaller screens.
### How to test?
1. Open the chat list page (`/chats`) and verify the composer is pinned to the bottom with the heading and mascot centered above it.
2. Open an existing chat and confirm messages scroll independently while the composer remains fixed at the bottom.
3. Resize the browser to a mobile viewport and confirm the send button spans full width and the controls stack vertically.
4. Verify the `Card` component renders correctly with both `default` and `sm` sizes.
### Why make this change?
The previous layout pushed the chat form inline within the page flow, causing inconsistent spacing and scroll behavior. Pinning the composer absolutely at the bottom provides a more conventional chat UI where the input is always visible and the message list scrolls freely beneath it.
### TL;DR
Adds the ability for users to edit their last message in a chat, which re-submits the conversation and streams a new assistant response.
### What changed?
- Added a new `POST /:id/messages/edit-last` API endpoint that updates the last user message and streams a regenerated assistant response.
- Added a `useEditLastMessage` hook that handles optimistic updates, streams the new assistant response chunk-by-chunk into the query cache, and invalidates relevant queries on completion.
- Introduced `ChatMessageBubble` and `ChatMessageEditForm` as dedicated components, extracted from `ChatMessage` to support the inline editing UI.
- Added an `EditMessageButton` component with a pencil icon that appears alongside the copy and regenerate actions on the last editable user message.
- `ChatMessages` now tracks which message is being edited via `editingMessageId` state, determines the last editable user message, and wires up edit/cancel/save handlers.
- Edit and regenerate actions are mutually disabled while either operation is pending.
- `CopyButton` and `RegenerateMessageButton` updated to use `icon-sm` size.
### How to test?
1. Start a chat and send at least one message to receive an assistant reply.
2. Hover over your last user message — an edit (pencil) icon should appear alongside the copy button.
3. Click the edit icon to open the inline edit form with your original message pre-filled.
4. Modify the text and click **Save** (or press Enter). The user message should update and the assistant response should stream in fresh.
5. Alternatively, click **Cancel** to discard changes and return to the original message.
6. Verify that the edit button is disabled while a regeneration is in progress, and vice versa.
### Why make this change?
Users previously had no way to correct or revise a message after sending it. This feature allows iterating on a conversation without starting over, improving the overall chat experience.
### TL;DR
Adds the ability to regenerate the last assistant message in a chat.
### What changed?
- Added a `POST /:id/messages/regenerate` API endpoint that streams a regenerated response for the last assistant message in a chat, replacing its content in the database once streaming completes.
- Added a `RegenerateMessageButton` component (using a `RotateCcwIcon`) that appears in the message actions row for the last assistant message.
- Added a `useRegenerateLastMessage` hook that handles optimistically clearing the last assistant message text, streaming the new response chunk-by-chunk into the query cache, and invalidating relevant queries on completion or error.
- The regenerate button is only shown on the most recent assistant message that has non-empty text, and is disabled while a regeneration is in progress.
### How to test?
1. Open an existing chat that has at least one assistant response.
2. Hover over the last assistant message to reveal the action buttons.
3. Click the regenerate (↺) button and confirm the message clears and streams a new response.
4. Verify the regenerated content persists after the stream completes.
5. Confirm the button is disabled while regeneration is in progress and re-enables afterward.
### Why make this change?
Users may want to get a different response from the assistant without having to resend their message. This feature provides a straightforward way to retry the last assistant message in a chat.
### TL;DR
Tightened up the chat UI with minor spacing and sizing adjustments.
### What changed?
- Added `gap="xs"` to the chat messages container so messages have smaller spacing between them.
- Reduced the copy button size from `icon` to `icon-xs` for a more compact appearance.
### How to test?
1. Open a chat conversation and verify that messages are evenly spaced with a small gap between them.
2. Hover over a message and confirm the copy button appears smaller and still functions correctly when clicked.
### Why make this change?
The chat message list had too much spacing between messages, and the copy button was visually oversized relative to the surrounding UI elements. These tweaks improve the overall visual polish and consistency of the chat interface.
### TL;DR
Adds a copy-to-clipboard button that appears on hover/focus for each chat message.
### What changed?
- Added a `CopyButton` component that writes text to the clipboard using the Clipboard API. After copying, it briefly shows a checkmark icon for 1.5 seconds before reverting to the copy icon.
- Added a `ChatMessageActions` component that renders the `CopyButton` and is hidden by default, becoming visible when the message is hovered or focused.
- Updated `ChatMessage` to wrap the message bubble in a `Stack` with a `group` class, enabling the hover/focus visibility behavior for `ChatMessageActions`. Actions are only rendered when the message has content and is not in the typing state.
### How to test?
1. Open a chat and send or receive a message.
2. Hover over a message — a copy button should appear below it.
3. Click the copy button and verify the message text is copied to the clipboard.
4. Confirm the icon switches to a checkmark briefly before reverting back to the copy icon.
5. Verify the button does not appear while the assistant typing indicator is active.
### Why make this change?
Users need a convenient way to copy message content without manually selecting text, improving the overall usability of the chat interface.
### TL;DR
Disable chat form inputs when the user is not authenticated or auth state hasn't loaded yet.
### What changed?
The chat form now checks Clerk's authentication state (`isLoaded` and `isSignedIn`) before allowing interaction. The message textarea, send button, character selector, and model selector are all disabled when auth is unavailable (i.e., auth hasn't loaded or the user is not signed in).
### How to test?
1. Open the chat page while signed out and verify that the message input, character selector, and model selector are all disabled.
2. Sign in and confirm that all controls become interactive.
3. Verify that sending a message works as expected when authenticated.
### Why make this change?
Preventing interaction with the chat form before authentication is confirmed avoids potential errors from unauthenticated API calls. This ensures users cannot attempt to send messages or change chat settings before a valid session is established.
### TL;DR
Allow unauthenticated users to access the chats route, showing a "Sign In" button in the sidebar footer instead of redirecting them away.
### What changed?
The `/chats` route no longer redirects unauthenticated users to the sign-in page. Instead, a "Sign In" button is displayed in the sidebar footer when the user is signed out, using Clerk's `SignedOut` and `SignInButton` components. The `SignedIn` guard around the user button remains intact.
### How to test?
1. Open the app while signed out and navigate to `/chats`.
2. Verify you are no longer redirected to the sign-in page.
3. Confirm the sidebar footer displays a "Sign In" button.
4. Click the "Sign In" button and verify it triggers the Clerk sign-in flow.
5. Sign in and confirm the sidebar footer switches to showing the user button.
### Why make this change?
Redirecting unauthenticated users away from the chats route creates a poor experience for users who may want to explore the app before signing in. Surfacing a "Sign In" prompt directly in the sidebar is a softer, more welcoming approach that keeps users in context.
- Add Hono API routes for characters, chats, and streaming messages
- Add Drizzle SQLite schema, db config, validation, ids, and seed script
- Seed Kitsune and Neko characters with system prompts
- Use character prompt as the system prompt for streamed chat responses
- Replace shared package types with Hono client inferred frontend types
- Connect frontend chat/character hooks to the API client
- Add message sending hook with chat creation, navigation, optimistic
messages, and streaming
updates
- Add typing indicator for pending assistant responses
- Update character select to display character names while using ids
internally
- Remove unused shared package workspace