diff --git a/apps/web/src/components/__tests__/chat-interface.test.tsx b/apps/web/src/components/__tests__/chat-interface.test.tsx
index 2d66ff3d..5ac2b2a7 100644
--- a/apps/web/src/components/__tests__/chat-interface.test.tsx
+++ b/apps/web/src/components/__tests__/chat-interface.test.tsx
@@ -231,6 +231,7 @@ function makeDefaultState() {
chatId: null,
isResuming: false,
resumedContent: '',
+ resetChatError: vi.fn(),
}
}
diff --git a/apps/web/src/components/ai-elements/conversation.tsx b/apps/web/src/components/ai-elements/conversation.tsx
index 58c7289e..76e9b4ab 100644
--- a/apps/web/src/components/ai-elements/conversation.tsx
+++ b/apps/web/src/components/ai-elements/conversation.tsx
@@ -83,6 +83,7 @@ export const Conversation = ({ className, children, showScrollButton = false, ..
ref={scrollRef}
className="absolute inset-0 overflow-y-auto"
role="log"
+ aria-label="Chat messages"
>
{children}
{/* Anchor element for scroll-to-bottom */}
@@ -92,10 +93,11 @@ export const Conversation = ({ className, children, showScrollButton = false, ..
{showScrollButton && !isAtBottom && (
)}
diff --git a/apps/web/src/components/app-loading-placeholder.tsx b/apps/web/src/components/app-loading-placeholder.tsx
new file mode 100644
index 00000000..934d6545
--- /dev/null
+++ b/apps/web/src/components/app-loading-placeholder.tsx
@@ -0,0 +1,44 @@
+import { cn } from "@/lib/utils";
+
+type AppLoadingPlaceholderProps = {
+ /** Announced to screen readers */
+ message?: string;
+ className?: string;
+ /** Matches authenticated shell: sidebar stub + main pane */
+ variant?: "app-shell" | "simple";
+};
+
+/**
+ * Accessible loading placeholder for route-level and auth-gated suspense states.
+ */
+export function AppLoadingPlaceholder({
+ message = "Loading",
+ className,
+ variant = "simple",
+}: AppLoadingPlaceholderProps) {
+ if (variant === "app-shell") {
+ return (
+
+ );
+ }
+
+ return (
+
+ {message}
+
+ );
+}
diff --git a/apps/web/src/components/chat/chat-interface.tsx b/apps/web/src/components/chat/chat-interface.tsx
index c667641a..c68e87e7 100644
--- a/apps/web/src/components/chat/chat-interface.tsx
+++ b/apps/web/src/components/chat/chat-interface.tsx
@@ -51,6 +51,8 @@ export function ChatInterface({ chatId }: ChatInterfaceProps) {
error,
stop,
isNewChat,
+ isLoadingMessages,
+ resetChatError,
} = usePersistentChat({
chatId,
onChatCreated: (newChatId) => {
@@ -92,7 +94,9 @@ export function ChatInterface({ chatId }: ChatInterfaceProps) {
messages={messages}
isLoading={isLoading}
isNewChat={isNewChat}
- error={error ?? null}
+ isLoadingHistory={Boolean(chatId) && isLoadingMessages}
+ streamError={error ?? null}
+ onDismissStreamError={resetChatError}
stop={stop}
handleSubmit={handleSubmit}
onEditMessage={editMessage}
@@ -114,7 +118,9 @@ interface ChatInterfaceContentProps {
}>;
isLoading: boolean;
isNewChat: boolean;
- error: Error | null;
+ isLoadingHistory: boolean;
+ streamError: Error | null;
+ onDismissStreamError: () => void;
stop: () => void;
handleSubmit: (message: PromptInputMessage) => Promise;
onEditMessage: (messageId: string, newContent: string) => Promise;
@@ -128,7 +134,9 @@ const ChatInterfaceContent = memo(function ChatInterfaceContent({
messages,
isLoading,
isNewChat,
- error: _error,
+ isLoadingHistory,
+ streamError,
+ onDismissStreamError,
stop,
handleSubmit,
onEditMessage,
@@ -137,6 +145,8 @@ const ChatInterfaceContent = memo(function ChatInterfaceContent({
textareaRef,
}: ChatInterfaceContentProps) {
const controller = usePromptInputController();
+ const promptValueRef = useRef(controller.textInput.value);
+ promptValueRef.current = controller.textInput.value;
const [editingMessageId, setEditingMessageId] = useState(null);
const [isSavingEdit, setIsSavingEdit] = useState(false);
const savedDraftRef = useRef("");
@@ -201,14 +211,14 @@ const ChatInterfaceContent = memo(function ChatInterfaceContent({
const startEdit = useCallback(
(messageId: string, content: string) => {
- savedDraftRef.current = controller.textInput.value;
+ savedDraftRef.current = promptValueRef.current;
setEditingMessageId(messageId);
setInput(content);
setTimeout(() => {
textareaRef.current?.focus();
}, 0);
},
- [controller.textInput.value, setInput, textareaRef],
+ [setInput, textareaRef],
);
const cancelEdit = useCallback(() => {
@@ -260,6 +270,9 @@ const ChatInterfaceContent = memo(function ChatInterfaceContent({
messages={messages}
isLoading={isLoading}
isNewChat={isNewChat}
+ isLoadingHistory={isLoadingHistory}
+ streamError={streamError}
+ onDismissStreamError={onDismissStreamError}
onPromptSelect={onPromptSelect}
onRetryMessage={onRetryMessage}
onForkMessage={onForkMessage}
@@ -270,11 +283,16 @@ const ChatInterfaceContent = memo(function ChatInterfaceContent({
{editingMessageId && (
-
+
Editing message