For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Render an accessible assistant loading placeholder immediately after every newly submitted user turn and reconcile it with the authoritative model response.
Architecture: useChat will create a client user message and a client assistant placeholder in one optimistic state update. Response reconciliation will replace both client IDs with server IDs and update the existing assistant object instead of appending a second message. ChatMessage will render an empty streaming assistant as an accessible animated-dot indicator.
Tech Stack: React 19, Next.js 16 App Router, TypeScript, Tailwind CSS, Vitest, Testing Library, minikube/Kubernetes.
Модель формирует ответ./api/chat resolves.Files:
hooks/use-chat.tshooks/use-chat.test.tsxInterfaces:
Consumes: existing ChatMessage, Conversation, authFetch, and client-prefixed optimistic IDs.
Produces: OptimisticTurn.assistantMessageId: string and conversations containing [user, assistant(streaming)] immediately after submission.
Step 1: Write failing optimistic lifecycle tests
Extend the deferred-response test to assert the immediate two-message state:
expect(result.current.activeConversation?.messages).toMatchObject([
{ role: "user", content: "Покажи сразу", status: "complete" },
{ role: "assistant", content: "", status: "streaming" },
]);
After resolving the response, retain the exact authoritative IDs assertion:
expect(result.current.activeConversation?.messages.map(({ id }) => id)).toEqual([
"user-1",
"assistant-1",
]);
expect(result.current.activeConversation?.messages).toHaveLength(2);
Strengthen the pre-header error test to capture the client assistant ID before rejection and assert the same ID becomes error rather than a third message being appended.
Run:
npm test -- hooks/use-chat.test.tsx
Expected: FAIL because the immediate state contains only the user message, and the pre-header error creates a new assistant object after rejection.
Add assistantMessageId to OptimisticTurn, create an empty streaming assistant alongside the user message, and append both in the same commitConversations update:
const assistantMessage: ChatMessage = {
id: optimistic.assistantMessageId,
role: "assistant",
content: "",
createdAt: optimistic.createdAt,
status: "streaming",
};
messages: [...conversation.messages, userMessage, assistantMessage]
In commitResponseMessage, replace the optimistic assistant in place:
messages: conversation.messages.map((message) => {
if (message.id === optimistic?.userMessageId) {
return { ...message, id: userMessageId as string };
}
if (message.id === optimistic?.assistantMessageId) {
return assistantMessage;
}
return message;
})
For pre-header errors or stops, call updateMessage with optimistic.assistantMessageId; do not construct or append a second assistant message.
Run:
npm test -- hooks/use-chat.test.tsx
Expected: all hook tests PASS, including immediate placeholder, reconciliation, attachments, errors, stops, switching, and preference lifecycle.
git add hooks/use-chat.ts hooks/use-chat.test.tsx
git commit -m "feat: add optimistic assistant placeholder"
Files:
components/chat/chat-message.tsxcomponents/chat/chat-message.test.tsxInterfaces:
Consumes: an assistant ChatMessage with status: "streaming" and empty content.
Produces: a visible three-dot pending indicator with accessible status text, hidden as soon as content exists.
Step 1: Write failing rendering tests
Render an empty streaming assistant and assert:
expect(screen.getByRole("status", { name: "Модель формирует ответ" })).toBeVisible();
expect(screen.getAllByTestId("response-placeholder-dot")).toHaveLength(3);
Render a streaming assistant with content: "Первый фрагмент" and assert the label is absent and the text is visible.
Run:
npm test -- components/chat/chat-message.test.tsx
Expected: FAIL because the current empty streaming state renders the text Думаю… and no status element or dots.
Replace the empty-streaming fallback with:
{message.status === "streaming" && !message.content ? (
<span
aria-label="Модель формирует ответ"
className="inline-flex items-center gap-1 py-2"
role="status"
>
{[0, 1, 2].map((index) => (
<span
aria-hidden="true"
className="size-1.5 animate-bounce rounded-full bg-muted-foreground motion-reduce:animate-none"
data-testid="response-placeholder-dot"
key={index}
style={{ animationDelay: `${index * 120}ms` }}
/>
))}
</span>
) : (
<p className="whitespace-pre-wrap text-sm leading-7">{message.content}</p>
)}
Keep copy, retry, stopped, attachment, and download behavior unchanged.
Run:
npm test -- components/chat/chat-message.test.tsx components/chat/chat-app.test.tsx hooks/use-chat.test.tsx
Expected: all focused tests PASS.
git add components/chat/chat-message.tsx components/chat/chat-message.test.tsx
git commit -m "feat: show model response placeholder"
Files:
corp-ui workspaceaegida-services/ai-control-chat-uiInterfaces:
Consumes: the committed corp-ui image.
Produces: a Ready minikube pod serving the placeholder behavior through https://chat.aegida.test:8443.
Step 1: Run full local verification
npm test
npm run lint
npx tsc --noEmit
npm run build
git diff --check
Expected: all commands exit 0. If Turbopack cannot bind its sandbox worker port, rerun only npm run build with the approved local-worker permission.
CHAT_UI_IMAGE="ai-control-chat-ui:minikube-$(git rev-parse --short=7 HEAD)"
minikube image build -t "$CHAT_UI_IMAGE" .
kubectl -n aegida-services set image deployment/ai-control-chat-ui app="$CHAT_UI_IMAGE"
kubectl -n aegida-services rollout status deployment/ai-control-chat-ui --timeout=240s
Expected: rollout succeeds with one Ready replica and zero restarts.
curl -ksS --max-time 10 -w '\nHTTP %{http_code}\n' https://chat.aegida.test:8443/api/health
In the in-app browser, submit a unique short prompt and immediately verify the user article, Модель формирует ответ status, and stop button are simultaneously visible. Wait for completion and verify the status disappears, a single assistant response remains, and the conversation persists after reload.
Report the image tag and digest, pod Ready/restart state, health HTTP status, exact test count, and browser assertions. Leave the verified application tab open for the user.