From 09fd6f3411ad0ed19746f6f645686650fd2bf6ce Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Sat, 13 Jun 2026 20:03:07 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84=20style(chat):=20carousel=20the=20?= =?UTF-8?q?OpStatusTray=20generating=20phrase=20every=204s=20(#15775)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generating status phrase was picked once per operation and stayed frozen for the whole run. Rotate it like a carousel — advancing to the next phrase every 4s with a subtle fade — so a long-running task feels alive instead of stuck on one line. - add pickRotatingStatusPhrase: seed keeps the starting phrase stable per operation, step advances the carousel; reuses the existing 1s elapsed ticker so no extra timer is needed - fade/slide the phrase on each switch via a keyed wrapper span (keeps the shiny-text shimmer animation intact) Co-authored-by: Claude Opus 4.8 --- .../Conversation/ChatInput/OpStatusTray.tsx | 32 ++++++++++++++-- .../ChatInput/OpStatusTray/logic.test.ts | 37 ++++++++++++++++++- .../ChatInput/OpStatusTray/logic.ts | 16 ++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/features/Conversation/ChatInput/OpStatusTray.tsx b/src/features/Conversation/ChatInput/OpStatusTray.tsx index e03c5506cd..ad9d013012 100644 --- a/src/features/Conversation/ChatInput/OpStatusTray.tsx +++ b/src/features/Conversation/ChatInput/OpStatusTray.tsx @@ -21,7 +21,11 @@ import { import { contextSelectors, dataSelectors, useConversationStore } from '../store'; import { type ActivityKey, resolveOperationActivity } from '../utils/operationActivity'; -import { parseStatusPhrases, pickStableStatusPhrase } from './OpStatusTray/logic'; +import { parseStatusPhrases, pickRotatingStatusPhrase } from './OpStatusTray/logic'; + +// Cycle the generating phrase like a carousel so a long-running task doesn't +// stare back with the same line the whole time. +const STATUS_PHRASE_ROTATION_MS = 4000; const styles = createStaticStyles(({ css, cssVar }) => ({ container: css` @@ -110,6 +114,22 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ text-overflow: ellipsis; white-space: nowrap; `, + statusPhrase: css` + @keyframes op-status-tray-phrase-enter { + from { + transform: translateY(3px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } + } + + display: inline-block; + animation: op-status-tray-phrase-enter 0.4s ease; + `, timerValue: css` flex: none; color: ${cssVar.colorTextTertiary}; @@ -290,10 +310,12 @@ const OpStatusTray = memo(({ topAttached }) => { returnObjects: true, }), ); + const rotationStep = Math.floor(elapsed / STATUS_PHRASE_ROTATION_MS); const randomGeneratingStatus = - pickStableStatusPhrase( + pickRotatingStatusPhrase( generatingPhrases, operationState.statusSeed ?? String(operationState.startTime), + rotationStep, ) ?? t('chat:opStatusTray.status.generating'); const statusText = operationState.activity === 'generating' @@ -372,7 +394,11 @@ const OpStatusTray = memo(({ topAttached }) => { > - {statusText}... + + + {statusText}... + + {formatElapsedClockTime(elapsed)} diff --git a/src/features/Conversation/ChatInput/OpStatusTray/logic.test.ts b/src/features/Conversation/ChatInput/OpStatusTray/logic.test.ts index efc1cc62f5..df7ba657b3 100644 --- a/src/features/Conversation/ChatInput/OpStatusTray/logic.test.ts +++ b/src/features/Conversation/ChatInput/OpStatusTray/logic.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { parseStatusPhrases, pickStableStatusPhrase } from './logic'; +import { parseStatusPhrases, pickRotatingStatusPhrase, pickStableStatusPhrase } from './logic'; describe('OpStatusTray logic', () => { describe('status phrases', () => { @@ -24,5 +24,40 @@ describe('OpStatusTray logic', () => { ); expect(phrases).toContain(pickStableStatusPhrase(phrases, 'op-123')); }); + + it('starts the rotation from the stable phrase and advances by step', () => { + const phrases = ['Working', 'Thinking', 'Flambéing']; + + const start = pickStableStatusPhrase(phrases, 'op-123'); + expect(pickRotatingStatusPhrase(phrases, 'op-123', 0)).toBe(start); + + const startIndex = phrases.indexOf(start!); + expect(pickRotatingStatusPhrase(phrases, 'op-123', 1)).toBe( + phrases[(startIndex + 1) % phrases.length], + ); + }); + + it('wraps around the phrase list as the step grows', () => { + const phrases = ['Working', 'Thinking', 'Flambéing']; + + expect(pickRotatingStatusPhrase(phrases, 'op-123', 0)).toBe( + pickRotatingStatusPhrase(phrases, 'op-123', phrases.length), + ); + }); + + it('tolerates non-finite or negative steps', () => { + const phrases = ['Working', 'Thinking', 'Flambéing']; + + expect(pickRotatingStatusPhrase(phrases, 'op-123', Number.NaN)).toBe( + pickRotatingStatusPhrase(phrases, 'op-123', 0), + ); + expect(pickRotatingStatusPhrase(phrases, 'op-123', -5)).toBe( + pickRotatingStatusPhrase(phrases, 'op-123', 0), + ); + }); + + it('returns undefined when there are no phrases', () => { + expect(pickRotatingStatusPhrase([], 'op-123', 3)).toBeUndefined(); + }); }); }); diff --git a/src/features/Conversation/ChatInput/OpStatusTray/logic.ts b/src/features/Conversation/ChatInput/OpStatusTray/logic.ts index 9fbb266aee..6c842154b4 100644 --- a/src/features/Conversation/ChatInput/OpStatusTray/logic.ts +++ b/src/features/Conversation/ChatInput/OpStatusTray/logic.ts @@ -27,3 +27,19 @@ export const pickStableStatusPhrase = (phrases: string[], seed: string): string if (phrases.length === 0) return undefined; return phrases[hashString(seed) % phrases.length]; }; + +/** + * Cycle through phrases over time so the status text reads like a carousel. + * `step` advances once per rotation tick; the seed keeps the starting phrase + * stable per operation so two concurrent operations don't sync up. + */ +export const pickRotatingStatusPhrase = ( + phrases: string[], + seed: string, + step: number, +): string | undefined => { + if (phrases.length === 0) return undefined; + const start = hashString(seed) % phrases.length; + const safeStep = Number.isFinite(step) ? Math.max(0, Math.floor(step)) : 0; + return phrases[(start + safeStep) % phrases.length]; +};