aegida-console / docs / superpowers / plans / 2026-08-09-chat-response-placeholder.md
2026-08-09-chat-response-placeholder.md
Raw

Chat Response Placeholder Implementation Plan

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.

Global Constraints

  • No API, database, Gateway, FinOps, model-adapter, or persisted-message schema changes.
  • The accessible pending label is exactly Модель формирует ответ.
  • The placeholder must exist before /api/chat resolves.
  • Authoritative response reconciliation must leave exactly one user message and one assistant message.
  • Pre-header errors and stops must update the placeholder in place and remain non-retryable.
  • No new runtime dependencies.

Task 1: Optimistic assistant lifecycle

Files:

  • Modify: hooks/use-chat.ts
  • Test: hooks/use-chat.test.tsx

Interfaces:

  • 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.

  • Step 2: Run the hook tests and verify RED

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.

  • Step 3: Implement the optimistic assistant and reconciliation

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.

  • Step 4: Run hook tests and verify GREEN

Run:

npm test -- hooks/use-chat.test.tsx

Expected: all hook tests PASS, including immediate placeholder, reconciliation, attachments, errors, stops, switching, and preference lifecycle.

  • Step 5: Commit the hook lifecycle
git add hooks/use-chat.ts hooks/use-chat.test.tsx
git commit -m "feat: add optimistic assistant placeholder"

Task 2: Accessible animated pending state

Files:

  • Modify: components/chat/chat-message.tsx
  • Test: components/chat/chat-message.test.tsx

Interfaces:

  • 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.

  • Step 2: Run the component test and verify RED

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.

  • Step 3: Implement the loading indicator

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.

  • Step 4: Run focused chat tests and verify GREEN

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.

  • Step 5: Commit the rendering change
git add components/chat/chat-message.tsx components/chat/chat-message.test.tsx
git commit -m "feat: show model response placeholder"

Task 3: Verification and minikube rollout

Files:

  • Verify only: entire corp-ui workspace
  • Deploy: Kubernetes Deployment aegida-services/ai-control-chat-ui

Interfaces:

  • 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.

  • Step 2: Build and deploy the minikube image
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.

  • Step 3: Verify health and browser behavior
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.

  • Step 4: Record final evidence

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.