@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.
- package/README.md +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- 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
|
|
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
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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 = !!(
|
|
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-[
|
|
503
|
+
className="absolute -inset-[4px] rounded-full"
|
|
490
504
|
style={{
|
|
491
|
-
background: '
|
|
492
|
-
animation: '
|
|
493
|
-
filter: 'blur(
|
|
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
|
-
? '
|
|
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 ? '
|
|
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 +
|
|
517
|
-
<div className="flex
|
|
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:
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
{
|
|
309
|
-
</
|
|
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-
|
|
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
|
|
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}
|