mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 09:47:44 +02:00
[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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9zwUaHucUgrdxvK8FvjYT
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b43faecdac
commit
31437c13e6
@@ -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 (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', my: 0.75 }}>
|
||||
<style>{thinkingShimmerKeyframes}</style>
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75, py: 0.5, px: 1, ml: -1 }}>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: 500,
|
||||
background: `linear-gradient(90deg, ${shimmerBase} 0%, ${shimmerBase} 40%, ${shimmerHighlight} 50%, ${shimmerBase} 60%, ${shimmerBase} 100%)`,
|
||||
backgroundSize: '200% 100%',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
color: 'transparent',
|
||||
animation: 'thinking-shimmer 2s linear infinite',
|
||||
transition: 'opacity 0.25s',
|
||||
}}
|
||||
>
|
||||
{display}
|
||||
</Box>
|
||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 1, py: 0.5, px: 1, ml: -1, minHeight: 28 }}>
|
||||
<ThinkingMark color={c.text.tertiary} />
|
||||
{label && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: 500,
|
||||
background: `linear-gradient(90deg, ${shimmerBase} 0%, ${shimmerBase} 40%, ${shimmerHighlight} 50%, ${shimmerBase} 60%, ${shimmerBase} 100%)`,
|
||||
backgroundSize: '200% 100%',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
color: 'transparent',
|
||||
animation: 'thinking-shimmer 2s linear infinite',
|
||||
transition: 'opacity 0.25s',
|
||||
}}
|
||||
>
|
||||
{label}…
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Box aria-label="Waiting for a reply" role="img" sx={{ display: 'inline-flex', alignItems: 'center', gap: `${size * 0.9}px`, height: size * 2, flexShrink: 0 }}>
|
||||
<style>{keyframes}</style>
|
||||
<Box sx={{ ...dot, animation: `osw-thinking-mark ${speed} ease-in-out calc(${speed} * -0.375) infinite` }} />
|
||||
<Box sx={{ ...dot, animation: `osw-thinking-mark ${speed} ease-in-out calc(${speed} * -0.25) infinite both` }} />
|
||||
<Box sx={{ ...dot, animation: `osw-thinking-mark ${speed} ease-in-out calc(${speed} * -0.125) infinite` }} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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, /<ThinkingMark color=/);
|
||||
assert.ok(!bubble.includes('THINKING_LABELS'), 'a default "Thinking" word is back');
|
||||
assert.match(bubble, /\{label && \(/, 'the shimmer label must be conditional on a real label');
|
||||
});
|
||||
|
||||
test('the mark is a no-dependency lift and says where it came from', () => {
|
||||
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/);
|
||||
});
|
||||
Reference in New Issue
Block a user