diff --git a/src/components/insights/coach-panel/message-thread.tsx b/src/components/insights/coach-panel/message-thread.tsx
index add50de79..54c1880c0 100644
--- a/src/components/insights/coach-panel/message-thread.tsx
+++ b/src/components/insights/coach-panel/message-thread.tsx
@@ -26,7 +26,6 @@ import { ABOUT_ME_FIELD_MAX_CHARS } from "@/lib/validations/about-me";
import { SourceChips } from "./source-chips";
import { ReminderSuggestionCard } from "./reminder-suggestion-card";
import { StreamedProse } from "./streamed-prose";
-import { ThinkingDisclosure } from "./thinking-disclosure";
import { MessageTokenFooter } from "./message-token-footer";
import type {
CoachConversationDetailDTO,
@@ -470,11 +469,8 @@ export function MessageThread({
providerType={streaming.inProgress ? "streaming" : null}
inProgress={streaming.inProgress}
errorCode={streaming.errorCode}
- // v1.18.9 — live word-fade + the elapsed-time thinking
- // disclosure + the just-landed token footer.
+ // v1.18.9 — live word-fade + the just-landed token footer.
streaming
- startedAt={streaming.startedAt}
- reasoning={streaming.reasoning}
usage={streaming.usage}
/>
@@ -513,17 +509,6 @@ interface ChatBubbleProps {
* settled plain text.
*/
streaming?: boolean;
- /**
- * v1.18.9 — send-anchored timestamp (epoch ms) for the streaming turn,
- * feeding the thinking disclosure's elapsed timer. Null on persisted
- * bubbles.
- */
- startedAt?: number | null;
- /**
- * v1.18.9 — reasoning-summary text from the additive `reasoning` SSE
- * frame, shown inside the thinking disclosure when present.
- */
- reasoning?: string;
/**
* v1.18.9 — per-turn token usage for the just-finished streaming bubble
* (from `done.usage`). Persisted bubbles read `tokensUsed` / `model`
@@ -548,8 +533,6 @@ function ChatBubble({
errorCode,
messageId,
streaming,
- startedAt,
- reasoning,
usage,
tokensUsed,
model,
@@ -678,28 +661,16 @@ function ChatBubble({
)}
- {/* v1.18.9 — the single thinking indicator (live elapsed-time line,
- then an auto-collapsed past-tense disclosure). Renders only for
- the live streaming turn — persisted bubbles pass no `startedAt`,
- so the disclosure returns null and history stays settled. This
- replaces the old
dots-plus-word combo: there is
- never a three-dots indicator AND a "Thinking" word at once. */}
- {streaming && startedAt != null && (
-
- )}
- {/* The prose bubble. Skipped while the live turn is still thinking
- with no prose (the disclosure carries the live state above), so
- the user never sees an empty bubble under the indicator. The
- `` fallback below only fires when there is no live
- disclosure (e.g. a defensive null `startedAt`). */}
- {(content ||
- safeError ||
- (inProgress && !(streaming && startedAt != null))) && (
+ {/* v1.19.1 (C3) — the live turn shows the classic typing animation
+ (three pulsing dots) while it is still thinking with no prose
+ yet, restoring the writing/typing indicator the maintainer
+ prefers over the "Denke nach / Nachgedacht" reasoning disclosure.
+ The dots render inside the prose bubble below; the disclosure is
+ retired. Persisted bubbles never stream, so history stays settled. */}
+ {/* The prose bubble. While the live turn is still thinking with no
+ prose the bubble carries the typing animation; once tokens land
+ it swaps to the streamed prose without a layout jump. */}
+ {(content || safeError || inProgress) && (
`), which showed
- * three bouncing dots AND the word "Thinking…" at the same time — the
- * exact pairing the maintainer disliked. This is a single, calm control:
- *
- * - While the turn is thinking (streaming, no prose yet): a live
- * "Thinking… N s" line with a small spinner. `role="status"` so a
- * screen reader announces "thinking" once, not on every tick.
- * - Once the first token lands OR the turn settles: it relabels to the
- * past tense ("Thought for N s") and auto-collapses into a quiet
- * `
` disclosure. Expanding reveals the reasoning summary when
- * a reasoning-capable provider supplied one (the additive `reasoning`
- * SSE frame), otherwise a one-line factual fallback — never fabricated
- * chain-of-thought.
- *
- * The elapsed time is computed from the send-anchored `startedAt`
- * timestamp: it ticks live while thinking and freezes the instant the
- * first token (or the settled state) arrives.
- */
-export interface ThinkingDisclosureProps {
- /**
- * Epoch-ms timestamp the turn started (the moment the send fired). Null
- * disables the elapsed timer (the component renders nothing).
- */
- startedAt: number | null;
- /** True until the turn settles (the `done`/`error` frame closed it). */
- inProgress: boolean;
- /**
- * Whether any assistant prose has streamed in yet. Drives the live →
- * past-tense relabel + auto-collapse: the moment the answer starts, the
- * "Thinking…" line freezes and folds away.
- */
- hasContent: boolean;
- /**
- * Optional reasoning-summary text from the additive `reasoning` SSE
- * frame, rendered inside the expanded disclosure when present.
- */
- reasoning?: string;
-}
-
-export function ThinkingDisclosure({
- startedAt,
- inProgress,
- hasContent,
- reasoning,
-}: ThinkingDisclosureProps) {
- const { t } = useTranslations();
- const panelId = useId();
- const [open, setOpen] = useState(false);
-
- // The turn is "thinking" only while it is in progress with no prose yet.
- const thinking = inProgress && hasContent === false && startedAt !== null;
-
- // `elapsed` ticks once a second while thinking; the interval stops the
- // moment `thinking` flips false, so the last value it set is the frozen
- // "thought for N s" figure — no render-time ref, no sync setState in the
- // effect body (the first tick lands on the interval, not inline).
- const [elapsed, setElapsed] = useState(0);
- useEffect(() => {
- if (!thinking || startedAt === null) return;
- const tick = () =>
- setElapsed(Math.max(0, Math.round((Date.now() - startedAt) / 1000)));
- tick();
- const handle = window.setInterval(tick, 1000);
- return () => window.clearInterval(handle);
- }, [thinking, startedAt]);
-
- if (startedAt === null) return null;
-
- // ── Live "thinking" line (single indicator, no dots-plus-word) ──
- if (thinking) {
- return (
-
-
- {t("insights.coach.thinking")}
-
- {t("insights.coach.thinkingElapsed", { seconds: elapsed })}
-
-
- );
- }
-
- // ── Settled: past-tense collapsible disclosure (auto-collapsed) ──
- const reasoningText = reasoning?.trim() ?? "";
- return (
- setOpen((e.target as HTMLDetailsElement).open)}
- className="group inline-block"
- >
-
-
-
- {t("insights.coach.thinkingDuration", { seconds: elapsed })}
-
-
-
- {reasoningText || t("insights.coach.thinkingDetail")}
-
-
- );
-}