# Optimistic Chat Message Rendering Design ## Goal Render a submitted user message in the active chat during the same client interaction, before `/api/chat` returns response headers or model output. Keep that user message visible when generation fails or is stopped, and reconcile the optimistic state with the authoritative conversation and message IDs returned by the server without duplicates. ## Scope The change is limited to the existing `corp-ui` chat hook and composer boundary. The persistent `/api/chat` contract, database schema, Gateway, FinOps, authentication, and model protocol remain unchanged. Optimistic rendering covers both text and already-uploaded attachment metadata. Attachment upload itself remains a prerequisite for submission. ## Client Data Flow 1. `ChatComposer` passes the trimmed text and the complete metadata for every uploaded attachment to `useChat.sendMessage`. 2. `sendMessage` validates the turn, captures the current authoritative conversation ID when one exists, and creates stable client-only IDs for the optimistic conversation and user message. 3. Before starting `authFetch`, the hook commits the user message to React state. For a new chat it also creates and activates an optimistic conversation. For an existing chat it appends the message and moves that conversation to the top without changing its authoritative ID. 4. The request body remains unchanged. It contains attachment IDs only and omits `conversationId` for a new optimistic chat; client-only IDs never cross the API boundary. 5. When `/api/chat` returns `X-Conversation-Id` and `X-User-Message-Id`, the hook atomically replaces the client-only IDs with server IDs. It updates the active conversation ID only when the optimistic conversation is still active. Existing messages and attachment metadata are retained. 6. The assistant message is appended after response headers are available. Stream chunks then update only that assistant message as they do today. React state updates remain immutable and use stable message IDs, consistent with React 19 list reconciliation guidance. Existing synchronous refs are updated in the same commit helper so async response callbacks operate on the latest optimistic state rather than a stale render snapshot. ## Errors, Cancellation, and Retry - A non-2xx response carrying the server conversation/message headers first reconciles the optimistic user message, then appends the authoritative assistant error. Existing retry behavior remains available. - A network failure before any authoritative headers leaves the optimistic user message in place and appends a client-only assistant error. Retry is disabled for that client-only error because the client cannot safely know whether the server persisted the turn. A reload restores authoritative history; the user can also submit a new turn manually. - Cancelling after the assistant stream starts preserves the user message and marks the assistant message `stopped`, matching existing behavior. - A `401` continues to invoke the authentication boundary. No client-only ID is sent to another user session. - Empty, over-limit, quota-exhausted, or already-generating submissions do not create optimistic state. ## Attachment Behavior `ChatComposer` retains the full upload response (`id`, `name`, `contentType`, and `size`) until submit. `useChat` renders that metadata immediately on the optimistic user message while deriving the unchanged list of attachment IDs for `/api/chat`. Reconciliation changes only the message ID; attachment IDs and metadata stay stable. ## Testing Hook tests use a deliberately unresolved `/api/chat` promise and assert that: - the new user message and new conversation are visible before the promise resolves; - an existing conversation receives the user message immediately; - server headers reconcile IDs without creating a second user message; - an error response retains the user message and appends one assistant error; - cancellation retains the user message and marks the assistant response stopped; - attachment metadata appears in optimistic state while the request sends only attachment IDs; - invalid submissions create neither state nor a network request. Composer tests bind the updated attachment metadata callback. Existing chat, quota, model preference, conversation switching, retry, lint, typecheck, and production build suites remain required regression checks. ## Deployment Verification After the minikube rollout, an authenticated browser or equivalent delayed request test must observe the user bubble before the model request completes. The completed response must contain exactly one user bubble and one assistant response, and the resulting server conversation must remain available after reload.