diff --git a/src/renderer/src/ChatTimeline.test.tsx b/src/renderer/src/ChatTimeline.test.tsx index e679992e..32962af6 100644 --- a/src/renderer/src/ChatTimeline.test.tsx +++ b/src/renderer/src/ChatTimeline.test.tsx @@ -560,17 +560,21 @@ describe('ChatTimeline', () => { messageStartIndex: 0, ...callbacks, retryContent: '', totalMessageCount: 1 } const message: Message = { - id: 'status-message', role: 'assistant', content: '已有内容', + id: 'status-message', role: 'assistant', content: '', createdAt: Date.now(), state: 'streaming' } const { rerender } = render() expect(screen.getByRole('status', { name: '等待后续进展' })).toBeVisible() + expect(screen.getByRole('status', { name: '等待后续进展' })).toHaveClass('message__status--active') expect(document.querySelector('.message__status-dot--active')).toBeNull() + rerender() + expect(screen.getByRole('status', { name: '等待后续进展' })).not.toHaveClass('message__status--active') const tool = { callId: 'read-1', name: 'read', summary: 'Read file' } rerender() expect(screen.getByRole('status', { name: '工具待执行:read' })).toBeVisible() + expect(screen.getByRole('status', { name: '工具待执行:read' })).toHaveClass('message__status--active') rerender() diff --git a/src/renderer/src/ChatTimeline.tsx b/src/renderer/src/ChatTimeline.tsx index b78f8526..7fda3d78 100644 --- a/src/renderer/src/ChatTimeline.tsx +++ b/src/renderer/src/ChatTimeline.tsx @@ -754,6 +754,10 @@ function ChatMessageRowView({ ? t('chat.status.waitingForSubagent') : t('chat.status.waitingForProgress')) : message.status + const isWaitingForResponse = message.role === 'assistant' && + message.state === 'streaming' && + !message.content.trim() && + !message.blocks?.some(block => block.type === 'text' && block.content.trim()) const roleCaption = message.role !== 'assistant' ? undefined @@ -1232,9 +1236,11 @@ function ChatMessageRowView({ : 'polite' } className={ - failed - ? 'message__status message__status--error' - : 'message__status' + [ + 'message__status', + failed && 'message__status--error', + isWaitingForResponse && 'message__status--active' + ].filter(Boolean).join(' ') } role={ message.state !== 'error' && (message.knowledgeRetrieval || compressionMarkers.length > 0) @@ -1248,7 +1254,7 @@ function ChatMessageRowView({ aria-hidden="true" className="message__status-dot" /> - {statusText} + {statusText} )} {canRetry && Boolean(retryContent?.trim()) && ( diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index dc93cb39..86abb23d 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -7343,6 +7343,31 @@ button > svg { color: var(--danger); } +.message__status--active { + color: var(--text-muted); +} + +.message__status-label { + min-width: 0; +} + +.message__status--active .message__status-label { + background: linear-gradient( + 105deg, + var(--text-muted) 0%, + var(--text-muted) 38%, + var(--text-primary) 50%, + var(--text-muted) 62%, + var(--text-muted) 100% + ); + background-size: 320% 100%; + background-position: 140% 0; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: message-status-shimmer 2.2s linear infinite; +} + .context-compression-event { display: flex; width: 100%; @@ -18209,6 +18234,18 @@ textarea.field-control { } } +@keyframes message-status-shimmer { + 0%, + 12% { + background-position: 140% 0; + } + + 88%, + 100% { + background-position: -40% 0; + } +} + @keyframes composer-recording-pulse { 0%, 100% {