From 31437c13e6f363389171878b494f78eddecf820c Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 3 Sep 2026 12:10:10 -0700 Subject: [PATCH] [eric] chat: the pre-reply cue is a quiet breathing mark (LDRS dot-pulse, MIT, lifted as CSS), with a word beside it only when a real step label exists Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01C9zwUaHucUgrdxvK8FvjYT --- .../src/app/pages/AgentChat/AgentChat.tsx | 49 ++++++++++--------- .../pages/AgentChat/bubbles/ThinkingMark.tsx | 35 +++++++++++++ .../AgentChat/bubbles/thinkingMark.test.ts | 23 +++++++++ 3 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 frontend/src/app/pages/AgentChat/bubbles/ThinkingMark.tsx create mode 100644 frontend/src/app/pages/AgentChat/bubbles/thinkingMark.test.ts diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index 596a3ec3..e70ef876 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -57,9 +57,9 @@ import StreamingBubble from './bubbles/StreamingBubble'; import WelcomeQuickReplies from './WelcomeQuickReplies'; import InlineSurfaceEmbeds from './shell/InlineSurfaceEmbeds'; import { useWelcomeGreeting } from './useWelcomeGreeting'; -import { THINKING_LABELS } from './thinkingLabels'; import MessageBubble from './bubbles/MessageBubble'; import BurstRevealBubble from './bubbles/BurstRevealBubble'; +import ThinkingMark from './bubbles/ThinkingMark'; import { estimateRenderedTextHeight, RECHECK_VISIBILITY_EVENT } from './bubbles/markdownMeasure'; import CompactionMarker from './bubbles/CompactionMarker'; import MessageActionBar from './shell/MessageActionBar'; @@ -180,32 +180,33 @@ const ThinkingBubble: React.FC<{ label?: string | null }> = ({ label }) => { const c = useClaudeTokens(); const shimmerBase = c.text.tertiary; const shimmerHighlight = c.text.primary; - // Aux-LLM label wins; otherwise the pill stays plain "Thinking". The whimsical verbs read as personality - // in the per-message thinking bubble (MessageBubble), but as a vague, confusing status on a working card. - const display = label ? `${label}…` : `${THINKING_LABELS[0].live}…`; - // A quiet shimmer LINE, not a bordered card: status shares one visual language with the - // per-message thinking row, so only real content gets bubbles (the ChatGPT/Claude pattern). + // The mark alone until there is something real to say (the Anthropic pattern): a word like "Thinking" is + // a promise the harness cannot keep on a slow lane, and it read as a status that never changed. The + // aux-written step label ("Reading files") is real, so it rides beside the mark when it exists. return ( - - - {display} - + + + {label && ( + + {label}… + + )} ); diff --git a/frontend/src/app/pages/AgentChat/bubbles/ThinkingMark.tsx b/frontend/src/app/pages/AgentChat/bubbles/ThinkingMark.tsx new file mode 100644 index 00000000..8d6acb18 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/bubbles/ThinkingMark.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Box from '@mui/material/Box'; + +// The quiet mark that holds the floor before a reply: three dots breathing in turn, the Claude/ChatGPT +// pattern. Lifted as plain CSS from LDRS dot-pulse (MIT, (c) 2022 Griffin Johnston, github.com/GriffinJohnston/ldrs) so no dependency rides along. +const keyframes = ` +@keyframes osw-thinking-mark { + 0%, 100% { transform: scale(0); opacity: 0.35; } + 50% { transform: scale(1); opacity: 1; } +} +`; + +interface Props { + size?: number; + color: string; +} + +export default function ThinkingMark({ size = 6, color }: Props) { + const dot = { + width: size, + height: size, + borderRadius: '50%', + bgcolor: color, + transform: 'scale(0)', + }; + const speed = '1.3s'; + return ( + + + + + + + ); +} diff --git a/frontend/src/app/pages/AgentChat/bubbles/thinkingMark.test.ts b/frontend/src/app/pages/AgentChat/bubbles/thinkingMark.test.ts new file mode 100644 index 00000000..21581ae9 --- /dev/null +++ b/frontend/src/app/pages/AgentChat/bubbles/thinkingMark.test.ts @@ -0,0 +1,23 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +// Eric (2026-09-03): "why not something like what anthropic does where they just have a loading thing of +// some sort until the actual thinking / agent response appears". The pre-reply cue is the breathing mark +// alone; a word appears beside it only when the aux-written step label is real. +test('the pre-reply cue is the mark, with no default word', () => { + const chat = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/AgentChat/AgentChat.tsx'), 'utf8'); + const start = chat.indexOf('const ThinkingBubble: React.FC'); + const bubble = chat.slice(start, chat.indexOf('interface QueuedMessage', start)); + assert.match(bubble, / { + const mark = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/AgentChat/bubbles/ThinkingMark.tsx'), 'utf8'); + assert.match(mark, /MIT, \(c\) 2022 Griffin Johnston/); + assert.ok(!/from 'ldrs'/.test(mark), 'no runtime dependency on the library'); + assert.match(mark, /@keyframes osw-thinking-mark/); +});