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.
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.
ChatComposer passes the trimmed text and the complete metadata for every
uploaded attachment to useChat.sendMessage.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.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.conversationId for a new optimistic chat; client-only IDs never
cross the API boundary./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.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.
stopped, matching existing behavior.401 continues to invoke the authentication boundary. No client-only ID
is sent to another user session.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.
Hook tests use a deliberately unresolved /api/chat promise and assert that:
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.
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.