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% {