@swarmclawai/swarmclaw 0.6.4 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +62 -30
  2. package/package.json +10 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +34 -2
  8. package/src/app/api/chatrooms/route.ts +26 -3
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  14. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  15. package/src/app/api/sessions/route.ts +11 -2
  16. package/src/app/api/tasks/[id]/route.ts +18 -13
  17. package/src/app/api/tasks/route.ts +44 -1
  18. package/src/app/api/usage/route.ts +16 -7
  19. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  20. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  21. package/src/app/api/wallets/[id]/route.ts +118 -0
  22. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  23. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  24. package/src/app/api/wallets/route.ts +74 -0
  25. package/src/app/globals.css +8 -0
  26. package/src/cli/index.js +20 -0
  27. package/src/cli/index.ts +223 -39
  28. package/src/cli/spec.js +14 -0
  29. package/src/components/agents/agent-avatar.tsx +15 -1
  30. package/src/components/agents/agent-card.tsx +38 -6
  31. package/src/components/agents/agent-chat-list.tsx +79 -3
  32. package/src/components/agents/agent-sheet.tsx +191 -26
  33. package/src/components/auth/setup-wizard.tsx +268 -353
  34. package/src/components/chat/chat-area.tsx +24 -9
  35. package/src/components/chat/chat-header.tsx +48 -19
  36. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  37. package/src/components/chat/delegation-banner.test.ts +27 -0
  38. package/src/components/chat/delegation-banner.tsx +109 -23
  39. package/src/components/chat/message-bubble.tsx +17 -16
  40. package/src/components/chat/message-list.tsx +6 -5
  41. package/src/components/chat/streaming-bubble.tsx +3 -2
  42. package/src/components/chat/thinking-indicator.tsx +3 -2
  43. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  44. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  45. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  46. package/src/components/chatrooms/chatroom-message.tsx +165 -23
  47. package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
  48. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  49. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  50. package/src/components/connectors/connector-health.tsx +120 -0
  51. package/src/components/connectors/connector-list.tsx +1 -1
  52. package/src/components/connectors/connector-sheet.tsx +9 -0
  53. package/src/components/home/home-view.tsx +25 -3
  54. package/src/components/input/chat-input.tsx +8 -1
  55. package/src/components/knowledge/knowledge-list.tsx +1 -1
  56. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  57. package/src/components/layout/app-layout.tsx +35 -4
  58. package/src/components/memory/memory-agent-list.tsx +1 -1
  59. package/src/components/memory/memory-browser.tsx +1 -0
  60. package/src/components/memory/memory-card.tsx +3 -2
  61. package/src/components/memory/memory-detail.tsx +3 -3
  62. package/src/components/memory/memory-sheet.tsx +2 -2
  63. package/src/components/projects/project-detail.tsx +4 -4
  64. package/src/components/schedules/schedule-list.tsx +55 -9
  65. package/src/components/schedules/schedule-sheet.tsx +134 -23
  66. package/src/components/secrets/secret-sheet.tsx +1 -1
  67. package/src/components/secrets/secrets-list.tsx +1 -1
  68. package/src/components/sessions/session-card.tsx +1 -1
  69. package/src/components/shared/agent-picker-list.tsx +1 -1
  70. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  71. package/src/components/shared/command-palette.tsx +237 -0
  72. package/src/components/shared/connector-platform-icon.tsx +1 -0
  73. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  74. package/src/components/skills/skill-list.tsx +1 -1
  75. package/src/components/skills/skill-sheet.tsx +1 -1
  76. package/src/components/tasks/task-board.tsx +3 -3
  77. package/src/components/tasks/task-card.tsx +22 -2
  78. package/src/components/tasks/task-sheet.tsx +112 -17
  79. package/src/components/usage/metrics-dashboard.tsx +13 -25
  80. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  81. package/src/components/wallets/wallet-panel.tsx +616 -0
  82. package/src/components/wallets/wallet-section.tsx +100 -0
  83. package/src/hooks/use-swipe.ts +49 -0
  84. package/src/lib/providers/anthropic.ts +16 -2
  85. package/src/lib/providers/claude-cli.ts +7 -1
  86. package/src/lib/providers/index.ts +7 -0
  87. package/src/lib/providers/ollama.ts +16 -2
  88. package/src/lib/providers/openai.ts +7 -2
  89. package/src/lib/providers/openclaw.ts +6 -1
  90. package/src/lib/providers/provider-defaults.ts +7 -0
  91. package/src/lib/schedule-templates.ts +115 -0
  92. package/src/lib/server/agent-registry.ts +2 -2
  93. package/src/lib/server/alert-dispatch.ts +64 -0
  94. package/src/lib/server/chat-execution.ts +76 -4
  95. package/src/lib/server/chatroom-health.ts +60 -0
  96. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  97. package/src/lib/server/chatroom-helpers.ts +86 -12
  98. package/src/lib/server/chatroom-routing.ts +65 -0
  99. package/src/lib/server/connectors/discord.ts +3 -0
  100. package/src/lib/server/connectors/email.ts +267 -0
  101. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  102. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  103. package/src/lib/server/connectors/manager.ts +239 -5
  104. package/src/lib/server/connectors/openclaw.ts +3 -0
  105. package/src/lib/server/connectors/slack.ts +6 -0
  106. package/src/lib/server/connectors/telegram.ts +18 -0
  107. package/src/lib/server/connectors/types.ts +2 -0
  108. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  109. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  110. package/src/lib/server/connectors/whatsapp.ts +17 -5
  111. package/src/lib/server/cost.ts +70 -0
  112. package/src/lib/server/create-notification.ts +2 -0
  113. package/src/lib/server/daemon-state.ts +124 -0
  114. package/src/lib/server/dag-validation.ts +115 -0
  115. package/src/lib/server/memory-db.ts +12 -7
  116. package/src/lib/server/openclaw-doctor.ts +48 -0
  117. package/src/lib/server/orchestrator-lg.ts +12 -2
  118. package/src/lib/server/orchestrator.ts +6 -1
  119. package/src/lib/server/queue-followups.test.ts +224 -0
  120. package/src/lib/server/queue.ts +238 -24
  121. package/src/lib/server/scheduler.ts +3 -0
  122. package/src/lib/server/session-run-manager.ts +22 -1
  123. package/src/lib/server/session-tools/chatroom.ts +11 -2
  124. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  125. package/src/lib/server/session-tools/index.ts +8 -2
  126. package/src/lib/server/session-tools/memory.ts +23 -4
  127. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  128. package/src/lib/server/session-tools/shell.ts +1 -1
  129. package/src/lib/server/session-tools/wallet.ts +124 -0
  130. package/src/lib/server/session-tools/web.ts +2 -2
  131. package/src/lib/server/solana.ts +122 -0
  132. package/src/lib/server/storage.ts +158 -6
  133. package/src/lib/server/stream-agent-chat.ts +126 -63
  134. package/src/lib/server/task-mention.test.ts +41 -0
  135. package/src/lib/server/task-mention.ts +3 -2
  136. package/src/lib/setup-defaults.ts +277 -0
  137. package/src/lib/tool-definitions.ts +1 -0
  138. package/src/lib/validation/schemas.ts +69 -0
  139. package/src/lib/view-routes.ts +1 -0
  140. package/src/stores/use-app-store.ts +15 -3
  141. package/src/stores/use-chatroom-store.ts +52 -2
  142. package/src/types/index.ts +98 -2
  143. package/tsconfig.json +2 -1
@@ -146,8 +146,8 @@ export function ChatArea() {
146
146
  if (!sessionId) return
147
147
  try {
148
148
  const msgs = await fetchMessages(sessionId)
149
- if (msgs.length > messagesLenRef.current) {
150
- const newMsgs = msgs.slice(messagesLenRef.current)
149
+ if (msgs.length !== messagesLenRef.current) {
150
+ const newMsgs = msgs.length > messagesLenRef.current ? msgs.slice(messagesLenRef.current) : []
151
151
  setMessages(msgs)
152
152
  if (ttsEnabledRef.current && typeof document !== 'undefined' && document.visibilityState === 'visible') {
153
153
  const latestAssistant = [...newMsgs].reverse().find((m) => {
@@ -324,17 +324,32 @@ export function ChatArea() {
324
324
  <DevServerBar status={devServerStatus} onStop={handleStopDevServer} />
325
325
 
326
326
  {messagesLoading && !messages.length ? (
327
- <div className="flex-1 flex items-center justify-center">
328
- <div className="flex flex-col items-center gap-3" style={{ animation: 'fade-in 0.2s ease' }}>
329
- <div className="relative w-10 h-10">
330
- <div className="absolute inset-0 rounded-full border-2 border-white/[0.06]" />
331
- <div className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent-bright animate-spin" />
327
+ <div className="flex-1 flex flex-col gap-5 px-4 md:px-12 lg:px-16 py-8" style={{ animation: 'fade-in 0.2s ease' }}>
328
+ {/* Skeleton message bubbles */}
329
+ <div className="flex gap-3 max-w-[70%]">
330
+ <div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
331
+ <div className="flex-1 space-y-2">
332
+ <div className="h-3 w-24 rounded bg-white/[0.06] animate-pulse" />
333
+ <div className="h-16 rounded-[12px] bg-white/[0.04] animate-pulse" />
334
+ </div>
335
+ </div>
336
+ <div className="flex gap-3 max-w-[60%] self-end flex-row-reverse">
337
+ <div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
338
+ <div className="flex-1 space-y-2">
339
+ <div className="h-3 w-16 rounded bg-white/[0.06] animate-pulse ml-auto" />
340
+ <div className="h-10 rounded-[12px] bg-white/[0.04] animate-pulse" />
341
+ </div>
342
+ </div>
343
+ <div className="flex gap-3 max-w-[65%]">
344
+ <div className="w-8 h-8 rounded-full bg-white/[0.06] animate-pulse shrink-0" />
345
+ <div className="flex-1 space-y-2">
346
+ <div className="h-3 w-20 rounded bg-white/[0.06] animate-pulse" />
347
+ <div className="h-24 rounded-[12px] bg-white/[0.04] animate-pulse" />
332
348
  </div>
333
- <span className="text-[13px] text-text-3/50 font-500">Loading messages...</span>
334
349
  </div>
335
350
  </div>
336
351
  ) : isEmpty ? (
337
- <div className="flex-1 flex flex-col items-center justify-center px-6 pb-4 relative">
352
+ <div className="flex-1 flex flex-col items-center justify-center px-6 pb-[120px] md:pb-4 relative">
338
353
  {/* Atmospheric background glow */}
339
354
  <div className="absolute inset-0 pointer-events-none overflow-hidden">
340
355
  <div className="absolute top-[20%] left-[50%] -translate-x-1/2 w-[500px] h-[300px]"
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useMemo, useRef } from 'react'
3
+ import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
4
4
  import type { Session } from '@/types'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
@@ -17,6 +17,7 @@ import { AgentAvatar } from '@/components/agents/agent-avatar'
17
17
  import { ModelCombobox } from '@/components/shared/model-combobox'
18
18
  import { toast } from 'sonner'
19
19
  import type { ProviderType } from '@/types'
20
+ import { useWs } from '@/hooks/use-ws'
20
21
 
21
22
  function shortPath(p: string): string {
22
23
  return (p || '').replace(/^\/Users\/\w+/, '~')
@@ -106,6 +107,19 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
106
107
  const [renameError, setRenameError] = useState('')
107
108
  const renameInputRef = useRef<HTMLInputElement>(null)
108
109
  const renameContainerRef = useRef<HTMLSpanElement>(null)
110
+ const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
111
+ const [walletBalance, setWalletBalance] = useState<number | null>(null)
112
+
113
+ const fetchWalletBalance = useCallback(async () => {
114
+ if (!agent?.walletId) { setWalletBalance(null); return }
115
+ try {
116
+ const data = await api<{ balanceSol?: number }>('GET', `/wallets/${agent.walletId}`)
117
+ setWalletBalance(data.balanceSol ?? null)
118
+ } catch { setWalletBalance(null) }
119
+ }, [agent?.walletId])
120
+
121
+ useEffect(() => { fetchWalletBalance() }, [fetchWalletBalance])
122
+ useWs('wallets', fetchWalletBalance)
109
123
 
110
124
  // Find linked task for this session
111
125
  const linkedTask = useMemo(() => {
@@ -458,7 +472,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
458
472
  const hasToolToggles = ((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)
459
473
  const hasMemoryLink = !!(agent && session.tools?.includes('memory'))
460
474
  const hasSourceFilter = !!hasMultipleSources
461
- const hasContextBar = !!(hasToolToggles || isMainSession || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
475
+ const hasContextBar = !!(isMainSession || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
462
476
 
463
477
  return (
464
478
  <header
@@ -486,26 +500,26 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
486
500
  <div className="relative shrink-0">
487
501
  {streaming && (
488
502
  <div
489
- className="absolute -inset-[3px] rounded-full opacity-40"
503
+ className="absolute -inset-[4px] rounded-full"
490
504
  style={{
491
- background: 'conic-gradient(from 0deg, var(--color-accent-bright), transparent 120deg, transparent 240deg, var(--color-accent-bright))',
492
- animation: 'spin 2.5s linear infinite',
493
- filter: 'blur(3px)',
505
+ background: 'radial-gradient(circle, var(--color-accent-bright), transparent 70%)',
506
+ animation: 'pulse-glow 2s ease-in-out infinite',
507
+ filter: 'blur(5px)',
494
508
  }}
495
509
  />
496
510
  )}
497
511
  <div
498
- className="relative rounded-full"
512
+ className="relative rounded-full transition-transform duration-500"
499
513
  style={{
500
514
  padding: 2,
501
515
  background: streaming
502
- ? 'conic-gradient(from 0deg, var(--color-accent-bright), transparent 120deg, transparent 240deg, var(--color-accent-bright))'
516
+ ? 'linear-gradient(135deg, var(--color-accent-bright), var(--color-accent))'
503
517
  : 'linear-gradient(135deg, rgba(255,255,255,0.10), rgba(255,255,255,0.03))',
504
- animation: streaming ? 'spin 2.5s linear infinite' : undefined,
518
+ animation: streaming ? 'avatar-pulse 2s ease-in-out infinite' : undefined,
505
519
  }}
506
520
  >
507
521
  <div className="rounded-full bg-bg">
508
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={hasContextBar ? 44 : 34} />
522
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={(hasContextBar || hasToolToggles) ? 44 : 34} />
509
523
  </div>
510
524
  </div>
511
525
  </div>
@@ -513,8 +527,9 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
513
527
 
514
528
  {/* Identity + metadata — fills center */}
515
529
  <div className="flex-1 min-w-0 flex items-center gap-3">
516
- {/* Name + inline badges */}
517
- <div className="flex items-center gap-2 min-w-0 shrink">
530
+ {/* Name (row 1) + tools (row 2) */}
531
+ <div className="flex flex-col gap-0.5 min-w-0 shrink">
532
+ <div className="flex items-center gap-2 min-w-0">
518
533
  {renaming && agent ? (
519
534
  <span ref={renameContainerRef} className="inline-flex items-center gap-2">
520
535
  <input
@@ -585,10 +600,28 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
585
600
  <span className="shrink-0 w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
586
601
  )}
587
602
  </div>
603
+ {hasToolToggles && <ChatToolToggles session={session} />}
604
+ </div>
588
605
 
589
- {/* Metadata tray: model · usage · path · status */}
606
+ {/* Metadata tray: wallet · model · path · status */}
590
607
  <div className="flex items-center gap-1.5 min-w-0 overflow-hidden">
591
608
  <span className="text-text-3/10 text-[10px] select-none shrink-0">/</span>
609
+ {walletBalance !== null && (
610
+ <>
611
+ <button
612
+ type="button"
613
+ onClick={() => { setWalletPanelAgentId(agent!.id); setActiveView('wallets') }}
614
+ className="inline-flex items-center gap-1 shrink-0 bg-transparent border-none p-0.5 rounded-[4px] cursor-pointer text-[11px] text-text-3/45 font-mono hover:text-text-3/70 hover:bg-white/[0.04] transition-colors"
615
+ title="View wallet"
616
+ >
617
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
618
+ <rect x="2" y="6" width="20" height="14" rx="2" /><path d="M22 10H18a2 2 0 0 0 0 4h4" />
619
+ </svg>
620
+ {walletBalance.toFixed(3)} SOL
621
+ </button>
622
+ <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
623
+ </>
624
+ )}
592
625
  {modelName && (
593
626
  <div className="relative shrink-0" ref={modelSwitcherRef}>
594
627
  <button
@@ -727,7 +760,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
727
760
  </button>
728
761
  {hbDropdownOpen && (
729
762
  <div className="absolute top-full right-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[80px]">
730
- {[1800, 3600, 7200, 21600, 43200].map((sec) => (
763
+ {[...(typeof window !== 'undefined' && window.location.hostname === 'localhost' ? [10, 15, 30, 60] : []), 1800, 3600, 7200, 21600, 43200].map((sec) => (
731
764
  <button
732
765
  key={sec}
733
766
  onClick={() => handleSelectHeartbeatInterval(sec)}
@@ -809,11 +842,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
809
842
 
810
843
  {/* Context bar: tools, mission controls, links */}
811
844
  {hasContextBar && (
812
- <div className="flex items-center gap-1.5 px-3.5 pb-1.5 overflow-x-auto scrollbar-none">
813
- {hasToolToggles && <ChatToolToggles session={session} />}
814
- {hasToolToggles && (hasMemoryLink || isMainSession || linkedTask || resumeHandle || isOpenClawAgent || browserActive) && (
815
- <div className="w-px h-4 bg-white/[0.05] shrink-0" />
816
- )}
845
+ <div className="flex items-center gap-1.5 px-3.5 pb-1.5 flex-wrap">
817
846
  {isMainSession && (
818
847
  <>
819
848
  <button
@@ -76,7 +76,7 @@ export function ChatToolToggles({ session }: Props) {
76
76
  {group.tools.map((tool) => {
77
77
  const enabled = sessionTools.includes(tool.id)
78
78
  return (
79
- <label key={tool.id} className="flex items-center gap-2.5 py-1.5 cursor-pointer">
79
+ <label key={tool.id} className="flex items-center gap-2.5 py-1.5 cursor-pointer" title={tool.description}>
80
80
  <div
81
81
  onClick={() => toggleTool(tool.id)}
82
82
  className={`w-8 h-[18px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
@@ -0,0 +1,27 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { parseTaskCompletion } from './delegation-banner'
4
+
5
+ describe('parseTaskCompletion', () => {
6
+ it('extracts output files and report path from completion payload', () => {
7
+ const text = [
8
+ 'Task completed: **[Build docs](#task:abc12345)**',
9
+ '',
10
+ 'Working directory: `/tmp/work`',
11
+ '',
12
+ 'Output files:',
13
+ '- `docs/guide.md`',
14
+ '- `docs/faq.md`',
15
+ '',
16
+ 'Task report: `data/task-reports/abc12345.md`',
17
+ '',
18
+ 'Done.',
19
+ ].join('\n')
20
+ const parsed = parseTaskCompletion(text)
21
+ assert.ok(parsed)
22
+ assert.deepEqual(parsed?.outputFiles, ['docs/guide.md', 'docs/faq.md'])
23
+ assert.equal(parsed?.reportPath, 'data/task-reports/abc12345.md')
24
+ assert.equal(parsed?.workingDir, '/tmp/work')
25
+ })
26
+ })
27
+
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import ReactMarkdown from 'react-markdown'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { AgentAvatar } from '@/components/agents/agent-avatar'
6
6
  import { api } from '@/lib/api-client'
@@ -10,6 +10,7 @@ type DelegationStatus = 'delegating' | 'checking' | 'completed' | 'failed'
10
10
  interface DelegationBannerProps {
11
11
  agentName: string
12
12
  agentAvatarSeed: string | null
13
+ agentAvatarUrl?: string | null
13
14
  taskPreview: string
14
15
  taskId: string | null
15
16
  status: DelegationStatus
@@ -60,7 +61,7 @@ function statusText(status: DelegationStatus, name: string): string {
60
61
  }
61
62
  }
62
63
 
63
- export function DelegationBanner({ agentName, agentAvatarSeed, taskPreview, taskId, status }: DelegationBannerProps) {
64
+ export function DelegationBanner({ agentName, agentAvatarSeed, agentAvatarUrl, taskPreview, taskId, status }: DelegationBannerProps) {
64
65
  const cfg = STATUS_CONFIG[status]
65
66
 
66
67
  const handleTaskClick = () => {
@@ -83,7 +84,7 @@ export function DelegationBanner({ agentName, agentAvatarSeed, taskPreview, task
83
84
  }}
84
85
  >
85
86
  <div className="shrink-0" style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both' }}>
86
- <AgentAvatar seed={agentAvatarSeed} name={agentName} size={24} />
87
+ <AgentAvatar seed={agentAvatarSeed} avatarUrl={agentAvatarUrl} name={agentName} size={24} />
87
88
  </div>
88
89
  <StatusIcon status={status} color={cfg.color} />
89
90
  <div className="flex flex-col gap-0.5 min-w-0 flex-1">
@@ -120,6 +121,8 @@ export interface TaskCompletionInfo {
120
121
  /** The agent that executed the task (present on delegated results) */
121
122
  executorName: string | null
122
123
  workingDir: string | null
124
+ reportPath: string | null
125
+ outputFiles: string[]
123
126
  resumeInfo: string | null
124
127
  resultBody: string
125
128
  imageUrl?: string
@@ -140,12 +143,32 @@ export function parseTaskCompletion(text: string): TaskCompletionInfo | null {
140
143
  const sections = bodyStart === -1 ? [] : text.slice(bodyStart + 2).split('\n\n')
141
144
 
142
145
  let workingDir: string | null = null
146
+ let reportPath: string | null = null
147
+ const outputFiles: string[] = []
143
148
  let resumeInfo: string | null = null
144
149
  const resultParts: string[] = []
145
150
 
146
151
  for (const section of sections) {
147
152
  if (section.startsWith('Working directory: ')) {
148
153
  workingDir = section.replace('Working directory: ', '').replace(/^`|`$/g, '')
154
+ } else if (section.startsWith('Output files:')) {
155
+ const fromBackticks = [...section.matchAll(/`([^`\n]+)`/g)].map((m) => (m[1] || '').trim()).filter(Boolean)
156
+ if (fromBackticks.length > 0) {
157
+ for (const fileRef of fromBackticks) {
158
+ if (!outputFiles.includes(fileRef)) outputFiles.push(fileRef)
159
+ }
160
+ } else {
161
+ const fromBullets = section
162
+ .split('\n')
163
+ .slice(1)
164
+ .map((line) => line.replace(/^\s*-\s*/, '').trim())
165
+ .filter(Boolean)
166
+ for (const fileRef of fromBullets) {
167
+ if (!outputFiles.includes(fileRef)) outputFiles.push(fileRef)
168
+ }
169
+ }
170
+ } else if (section.startsWith('Task report: ')) {
171
+ reportPath = section.replace('Task report: ', '').replace(/^`|`$/g, '')
149
172
  } else if (/^(Claude session|Codex thread|OpenCode session|CLI session):/.test(section)) {
150
173
  resumeInfo = section
151
174
  } else if (section.trim()) {
@@ -153,12 +176,21 @@ export function parseTaskCompletion(text: string): TaskCompletionInfo | null {
153
176
  }
154
177
  }
155
178
 
156
- return { status, taskTitle, taskId, executorName, workingDir, resumeInfo, resultBody: resultParts.join('\n\n') }
179
+ return {
180
+ status,
181
+ taskTitle,
182
+ taskId,
183
+ executorName,
184
+ workingDir,
185
+ reportPath,
186
+ outputFiles,
187
+ resumeInfo,
188
+ resultBody: resultParts.join('\n\n'),
189
+ }
157
190
  }
158
191
 
159
192
  export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
160
193
  const isSuccess = info.status === 'completed'
161
- const [expanded, setExpanded] = useState(false)
162
194
 
163
195
  const handleTaskClick = () => {
164
196
  if (!info.taskId) return
@@ -170,10 +202,6 @@ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
170
202
  })
171
203
  }
172
204
 
173
- // Truncate result for preview
174
- const resultPreview = info.resultBody.length > 200 ? info.resultBody.slice(0, 200) + '...' : info.resultBody
175
- const hasLongResult = info.resultBody.length > 200
176
-
177
205
  return (
178
206
  <div
179
207
  className="rounded-[14px] overflow-hidden"
@@ -278,6 +306,47 @@ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
278
306
  </div>
279
307
  )}
280
308
 
309
+ {info.outputFiles.length > 0 && (
310
+ <div className="flex flex-col gap-1">
311
+ <span className="text-[11px] text-text-3/55">Output files</span>
312
+ <div className="flex flex-wrap gap-1.5">
313
+ {info.outputFiles.map((fileRef) => {
314
+ const openPath = info.workingDir && !fileRef.startsWith('/') && !fileRef.startsWith('~/')
315
+ ? `${info.workingDir.replace(/\/$/, '')}/${fileRef}`
316
+ : fileRef
317
+ return (
318
+ <button
319
+ key={fileRef}
320
+ type="button"
321
+ onClick={() => { api('POST', '/files/open', { path: openPath }).catch(() => {}) }}
322
+ className="px-2 py-1 rounded-[7px] text-[10px] font-mono bg-white/[0.03] border border-white/[0.08] text-text-3/70 hover:text-text-3 cursor-pointer max-w-full truncate"
323
+ title={`Open ${openPath}`}
324
+ >
325
+ {fileRef}
326
+ </button>
327
+ )
328
+ })}
329
+ </div>
330
+ </div>
331
+ )}
332
+
333
+ {info.reportPath && (
334
+ <div className="flex items-center gap-2">
335
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0 text-text-3/40">
336
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
337
+ <polyline points="14 2 14 8 20 8" />
338
+ </svg>
339
+ <button
340
+ type="button"
341
+ onClick={() => { api('POST', '/files/open', { path: info.reportPath }).catch(() => {}) }}
342
+ className="text-[11px] text-text-3/60 hover:text-text-3 font-mono truncate bg-transparent border-none p-0 cursor-pointer transition-colors"
343
+ title="Open task report"
344
+ >
345
+ {info.reportPath}
346
+ </button>
347
+ </div>
348
+ )}
349
+
281
350
  {/* Image artifact */}
282
351
  {info.imageUrl && (
283
352
  <div className="mt-0.5">
@@ -295,19 +364,35 @@ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
295
364
  {/* Result body */}
296
365
  {info.resultBody && (
297
366
  <div className="mt-0.5">
298
- <div className="rounded-[10px] bg-white/[0.02] border border-white/[0.04] px-3 py-2.5">
299
- <pre className="text-[12px] leading-[1.6] text-text-3/80 whitespace-pre-wrap break-words m-0 font-mono">
300
- {expanded ? info.resultBody : resultPreview}
301
- </pre>
302
- {hasLongResult && (
303
- <button
304
- type="button"
305
- onClick={() => setExpanded((v) => !v)}
306
- className="mt-2 text-[11px] font-600 text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer p-0 transition-colors"
367
+ <div className="rounded-[10px] bg-white/[0.02] border border-white/[0.04] px-3 py-2.5 max-h-[260px] overflow-y-auto">
368
+ <div className="text-[12px] leading-[1.6] text-text-3/80 break-words">
369
+ <ReactMarkdown
370
+ components={{
371
+ a: ({ href, children }) => (
372
+ <a
373
+ href={href}
374
+ target="_blank"
375
+ rel="noreferrer"
376
+ className="text-emerald-300 hover:text-emerald-200 underline decoration-emerald-300/40"
377
+ >
378
+ {children}
379
+ </a>
380
+ ),
381
+ img: ({ src, alt }) => (
382
+ // eslint-disable-next-line @next/next/no-img-element
383
+ <img
384
+ src={src || ''}
385
+ alt={alt || 'Task artifact'}
386
+ loading="lazy"
387
+ className="max-w-full rounded-[8px] border border-white/[0.08] my-2"
388
+ />
389
+ ),
390
+ p: ({ children }) => <p className="m-0 mb-2">{children}</p>,
391
+ }}
307
392
  >
308
- {expanded ? 'Show less' : 'Show full result'}
309
- </button>
310
- )}
393
+ {info.resultBody}
394
+ </ReactMarkdown>
395
+ </div>
311
396
  </div>
312
397
  </div>
313
398
  )}
@@ -321,12 +406,13 @@ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
321
406
  interface DelegationSourceBannerProps {
322
407
  delegatorName: string
323
408
  delegatorAvatarSeed: string | null
409
+ delegatorAvatarUrl?: string | null
324
410
  taskTitle: string
325
411
  taskId: string | null
326
412
  description: string
327
413
  }
328
414
 
329
- export function DelegationSourceBanner({ delegatorName, delegatorAvatarSeed, taskTitle, taskId, description }: DelegationSourceBannerProps) {
415
+ export function DelegationSourceBanner({ delegatorName, delegatorAvatarSeed, delegatorAvatarUrl, taskTitle, taskId, description }: DelegationSourceBannerProps) {
330
416
  const handleTaskClick = () => {
331
417
  if (!taskId) return
332
418
  const store = useAppStore.getState()
@@ -343,7 +429,7 @@ export function DelegationSourceBanner({ delegatorName, delegatorAvatarSeed, tas
343
429
  style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1)' }}
344
430
  >
345
431
  <div className="shrink-0 mt-0.5" style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both' }}>
346
- <AgentAvatar seed={delegatorAvatarSeed} name={delegatorName} size={24} />
432
+ <AgentAvatar seed={delegatorAvatarSeed} avatarUrl={delegatorAvatarUrl} name={delegatorName} size={24} />
347
433
  </div>
348
434
  <div className="flex flex-col gap-1 min-w-0 flex-1">
349
435
  <span className="text-[12px] font-600 text-indigo-400">
@@ -136,6 +136,7 @@ interface Props {
136
136
  message: Message
137
137
  assistantName?: string
138
138
  agentAvatarSeed?: string
139
+ agentAvatarUrl?: string | null
139
140
  agentName?: string
140
141
  isLast?: boolean
141
142
  onRetry?: () => void
@@ -147,7 +148,7 @@ interface Props {
147
148
  momentOverlay?: React.ReactNode
148
149
  }
149
150
 
150
- export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork, onTransferToAgent, momentOverlay }: Props) {
151
+ export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentAvatarUrl, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork, onTransferToAgent, momentOverlay }: Props) {
151
152
  const isUser = message.role === 'user'
152
153
  const isHeartbeat = !isUser && (message.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(message.text || ''))
153
154
  const currentUser = useAppStore((s) => s.currentUser)
@@ -231,7 +232,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
231
232
  {!isUser && (
232
233
  <div className="absolute left-[4px] top-0">
233
234
  <div style={momentOverlay ? { animation: 'avatar-moment-pulse 0.6s ease' } : undefined}>
234
- {agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" />}
235
+ {agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" />}
235
236
  </div>
236
237
  {momentOverlay}
237
238
  </div>
@@ -515,7 +516,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
515
516
  )}
516
517
  </div>
517
518
  ) : (
518
- <div className={`msg-content text-[15px] break-words ${isUser ? 'leading-[1.6] text-white/95' : 'leading-[1.7] text-text'}`}>
519
+ <div className={`msg-content text-[15px] md:text-[14px] break-words ${isUser ? 'leading-[1.6] text-white/95' : 'leading-[1.7] text-text'}`}>
519
520
  <ReactMarkdown
520
521
  remarkPlugins={[remarkGfm]}
521
522
  rehypePlugins={[rehypeHighlight]}
@@ -659,12 +660,12 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
659
660
  )}
660
661
 
661
662
  {/* Action buttons */}
662
- <div className={`flex items-center gap-1 mt-1.5 px-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
663
+ <div className={`flex items-center gap-1 mt-1.5 px-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
663
664
  <button
664
665
  onClick={handleCopy}
665
666
  aria-label="Copy message"
666
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
667
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
667
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
668
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
668
669
  style={{ fontFamily: 'inherit' }}
669
670
  >
670
671
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -677,8 +678,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
677
678
  <button
678
679
  onClick={() => onToggleBookmark(messageIndex)}
679
680
  aria-label={message.bookmarked ? 'Remove bookmark' : 'Bookmark message'}
680
- className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
681
- text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all ${message.bookmarked ? 'text-amber-400' : ''}`}
681
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
682
+ text-[11px] font-500 cursor-pointer hover:bg-white/[0.04] transition-all justify-center md:justify-start ${message.bookmarked ? 'text-amber-400' : ''}`}
682
683
  style={{ fontFamily: 'inherit' }}
683
684
  >
684
685
  <svg width="12" height="12" viewBox="0 0 24 24" fill={message.bookmarked ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -691,8 +692,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
691
692
  <button
692
693
  onClick={() => { setEditText(message.text); setEditing(true) }}
693
694
  aria-label="Edit and resend"
694
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
695
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
695
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
696
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
696
697
  style={{ fontFamily: 'inherit' }}
697
698
  >
698
699
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -706,8 +707,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
706
707
  <button
707
708
  onClick={() => onFork(messageIndex)}
708
709
  aria-label="Fork conversation from here"
709
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
710
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
710
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
711
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
711
712
  style={{ fontFamily: 'inherit' }}
712
713
  >
713
714
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -724,8 +725,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
724
725
  <button
725
726
  onClick={onRetry}
726
727
  aria-label="Retry message"
727
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
728
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
728
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
729
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
729
730
  style={{ fontFamily: 'inherit' }}
730
731
  >
731
732
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -740,8 +741,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
740
741
  <button
741
742
  onClick={() => setTransferPickerOpen(!transferPickerOpen)}
742
743
  aria-label="Transfer to another agent"
743
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border-none bg-transparent
744
- text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all"
744
+ className="flex items-center gap-1.5 px-2.5 py-1.5 min-h-[44px] min-w-[44px] md:min-h-0 md:min-w-0 rounded-[8px] border-none bg-transparent
745
+ text-[11px] font-500 text-text-3 cursor-pointer hover:text-text-2 hover:bg-white/[0.04] transition-all justify-center md:justify-start"
745
746
  style={{ fontFamily: 'inherit' }}
746
747
  >
747
748
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -397,7 +397,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
397
397
  <div
398
398
  ref={scrollRef}
399
399
  onScroll={updateScrollState}
400
- className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-6 md:px-12 lg:px-16 pt-6 pb-10 fade-up"
400
+ className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-4 md:px-12 lg:px-16 pt-6 pb-[120px] md:pb-10 fade-up"
401
401
  >
402
402
  <div className="flex flex-col gap-6 relative">
403
403
  {/* Chat spine — vertical line for assistant messages */}
@@ -439,7 +439,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
439
439
  )}
440
440
  {filteredMessages.length === 0 && !streaming && (
441
441
  <div className="flex flex-col items-center justify-center gap-3 py-20 text-center" style={{ animation: 'fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) both' }}>
442
- <AgentAvatar seed={agent?.avatarSeed || null} name={agent?.name || 'Agent'} size={48} />
442
+ <AgentAvatar seed={agent?.avatarSeed || null} avatarUrl={agent?.avatarUrl} name={agent?.name || 'Agent'} size={48} />
443
443
  <span className="font-display text-[16px] font-600 text-text-2">{agent?.name || 'Assistant'}</span>
444
444
  <span className="text-[14px] text-text-3/60">
445
445
  {INTRO_GREETINGS[stableHash(agent?.id || session?.id || '') % INTRO_GREETINGS.length]}
@@ -526,6 +526,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
526
526
  message={msg}
527
527
  assistantName={assistantName}
528
528
  agentAvatarSeed={agent?.avatarSeed}
529
+ agentAvatarUrl={agent?.avatarUrl}
529
530
  agentName={agent?.name}
530
531
  isLast={isLastAssistant}
531
532
  onRetry={isLastAssistant ? retryLastMessage : undefined}
@@ -540,9 +541,9 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
540
541
  )
541
542
  })}
542
543
  <ApprovalCards agentId={agent?.id} />
543
- {streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
544
- {streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentName={agent?.name} />}
545
- {appSettings.suggestionsEnabled !== false && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
544
+ {streaming && !displayText && <ThinkingIndicator assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
545
+ {streaming && displayText && <StreamingBubble text={displayText} assistantName={assistantName} agentAvatarSeed={agent?.avatarSeed} agentAvatarUrl={agent?.avatarUrl} agentName={agent?.name} />}
546
+ {appSettings.suggestionsEnabled === true && !streaming && filteredMessages.length > 0 && filteredMessages[filteredMessages.length - 1]?.role === 'assistant' && (
546
547
  <SuggestionsBar lastMessage={filteredMessages[filteredMessages.length - 1]} onSend={sendMessage} />
547
548
  )}
548
549
  </div>
@@ -104,10 +104,11 @@ interface Props {
104
104
  text: string
105
105
  assistantName?: string
106
106
  agentAvatarSeed?: string
107
+ agentAvatarUrl?: string | null
107
108
  agentName?: string
108
109
  }
109
110
 
110
- export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentName }: Props) {
111
+ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAvatarUrl, agentName }: Props) {
111
112
  const toolEvents = useChatStore((s) => s.toolEvents)
112
113
  const streamPhase = useChatStore((s) => s.streamPhase)
113
114
  const streamToolName = useChatStore((s) => s.streamToolName)
@@ -137,7 +138,7 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentNam
137
138
  style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}
138
139
  >
139
140
  <div className="absolute left-[4px] top-0 relative">
140
- {agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
141
+ {agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
141
142
  {currentMoment && (
142
143
  <ActivityMoment
143
144
  key={currentMoment.id}