@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +994 -130
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +189 -10
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +62 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +31 -964
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -8,13 +8,14 @@ import { fetchMessages } from '@/lib/chats'
|
|
|
8
8
|
import { toast } from 'sonner'
|
|
9
9
|
import { Skeleton } from '@/components/shared/skeleton'
|
|
10
10
|
import { EmptyState } from '@/components/shared/empty-state'
|
|
11
|
+
import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
|
|
11
12
|
|
|
12
13
|
interface Props {
|
|
13
14
|
inSidebar?: boolean
|
|
14
15
|
onSelect?: () => void
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
type SessionFilter = 'all' | 'active' | '
|
|
18
|
+
type SessionFilter = 'all' | 'active' | 'unread'
|
|
18
19
|
type SortMode = 'lastActive' | 'name' | 'messages'
|
|
19
20
|
|
|
20
21
|
export function ChatList({ inSidebar, onSelect }: Props) {
|
|
@@ -24,15 +25,19 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
24
25
|
const setCurrentSession = useAppStore((s) => s.setCurrentSession)
|
|
25
26
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
26
27
|
const loadConnectors = useAppStore((s) => s.loadConnectors)
|
|
27
|
-
const
|
|
28
|
+
const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
28
29
|
const clearSessions = useAppStore((s) => s.clearSessions)
|
|
29
30
|
const togglePinSession = useAppStore((s) => s.togglePinSession)
|
|
30
31
|
const markChatRead = useAppStore((s) => s.markChatRead)
|
|
32
|
+
const lastReadTimestamps = useAppStore((s) => s.lastReadTimestamps)
|
|
33
|
+
const agents = useAppStore((s) => s.agents)
|
|
34
|
+
const connectors = useAppStore((s) => s.connectors)
|
|
31
35
|
const setMessages = useChatStore((s) => s.setMessages)
|
|
32
36
|
const [search, setSearch] = useState('')
|
|
33
37
|
const [typeFilter, setTypeFilter] = useState<SessionFilter>('all')
|
|
34
38
|
const [sortMode, setSortMode] = useState<SortMode>('lastActive')
|
|
35
39
|
const [loaded, setLoaded] = useState(Object.keys(sessions).length > 0)
|
|
40
|
+
const [bulkMenuOpen, setBulkMenuOpen] = useState(false)
|
|
36
41
|
|
|
37
42
|
useEffect(() => {
|
|
38
43
|
if (Object.keys(sessions).length > 0 && !loaded) setLoaded(true)
|
|
@@ -56,10 +61,32 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
56
61
|
const filtered = useMemo(() => {
|
|
57
62
|
return allUserSessions
|
|
58
63
|
.filter((s) => {
|
|
59
|
-
|
|
64
|
+
const unreadCount = (s.messages || []).filter(
|
|
65
|
+
(m) => m.role === 'assistant' && (m.time || 0) > (lastReadTimestamps[s.id] || 0),
|
|
66
|
+
).length
|
|
67
|
+
if (search) {
|
|
68
|
+
const agent = s.agentId ? agents[s.agentId] : null
|
|
69
|
+
const connector = Object.values(connectors).find((item) => item.chatroomId == null && item.agentId === s.agentId && item.isEnabled !== false)
|
|
70
|
+
const lastMessage = s.messages?.[s.messages.length - 1]
|
|
71
|
+
const haystack = [
|
|
72
|
+
s.name,
|
|
73
|
+
agent?.name,
|
|
74
|
+
s.provider,
|
|
75
|
+
s.model,
|
|
76
|
+
s.cwd,
|
|
77
|
+
connector?.name,
|
|
78
|
+
connector?.platform,
|
|
79
|
+
lastMessage?.text,
|
|
80
|
+
lastMessage?.source?.senderName,
|
|
81
|
+
lastMessage?.source?.platform,
|
|
82
|
+
]
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join(' ')
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
if (!haystack.includes(search.toLowerCase())) return false
|
|
87
|
+
}
|
|
60
88
|
if (typeFilter === 'active' && !s.active) return false
|
|
61
|
-
if (typeFilter === '
|
|
62
|
-
if (typeFilter === 'orchestrated' && s.sessionType !== 'orchestrated') return false
|
|
89
|
+
if (typeFilter === 'unread' && unreadCount === 0) return false
|
|
63
90
|
return true
|
|
64
91
|
})
|
|
65
92
|
.sort((a, b) => {
|
|
@@ -71,7 +98,7 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
71
98
|
if (sortMode === 'messages') return (b.messages?.length || 0) - (a.messages?.length || 0)
|
|
72
99
|
return (b.lastActiveAt || 0) - (a.lastActiveAt || 0)
|
|
73
100
|
})
|
|
74
|
-
}, [allUserSessions, search,
|
|
101
|
+
}, [agents, allUserSessions, connectors, lastReadTimestamps, search, sortMode, typeFilter])
|
|
75
102
|
|
|
76
103
|
const handleSelect = async (id: string) => {
|
|
77
104
|
setCurrentSession(id)
|
|
@@ -115,8 +142,8 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
115
142
|
</svg>
|
|
116
143
|
}
|
|
117
144
|
title="No chats yet"
|
|
118
|
-
subtitle="Create
|
|
119
|
-
action={!inSidebar ? { label: '+ New
|
|
145
|
+
subtitle="Create an agent to open its persistent thread"
|
|
146
|
+
action={!inSidebar ? { label: '+ New Agent', onClick: () => setAgentSheetOpen(true) } : undefined}
|
|
120
147
|
/>
|
|
121
148
|
)
|
|
122
149
|
}
|
|
@@ -125,7 +152,7 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
125
152
|
<div className="flex-1 flex flex-col overflow-y-auto">
|
|
126
153
|
{/* Filter tabs — always visible when sessions exist */}
|
|
127
154
|
<div className="flex items-center gap-1 px-4 pt-2 pb-1 shrink-0">
|
|
128
|
-
{(['all', 'active', '
|
|
155
|
+
{(['all', 'active', 'unread'] as SessionFilter[]).map((f) => (
|
|
129
156
|
<button
|
|
130
157
|
key={f}
|
|
131
158
|
onClick={() => setTypeFilter(f)}
|
|
@@ -133,25 +160,34 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
133
160
|
${typeFilter === f ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
134
161
|
style={{ fontFamily: 'inherit' }}
|
|
135
162
|
>
|
|
136
|
-
{f === 'all' ? 'All' : f === 'active' ? 'Active' :
|
|
163
|
+
{f === 'all' ? 'All' : f === 'active' ? 'Active' : 'Unread'}
|
|
137
164
|
</button>
|
|
138
165
|
))}
|
|
139
166
|
{filtered.length > 0 && (
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
<div className="ml-auto relative">
|
|
168
|
+
<button
|
|
169
|
+
onClick={() => setBulkMenuOpen((open) => !open)}
|
|
170
|
+
className="p-1.5 rounded-[8px] text-text-3/70 hover:text-text-2 hover:bg-white/[0.04]
|
|
171
|
+
cursor-pointer transition-all bg-transparent border-none"
|
|
172
|
+
title="More actions"
|
|
173
|
+
>
|
|
174
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
175
|
+
<circle cx="5" cy="12" r="1.75" />
|
|
176
|
+
<circle cx="12" cy="12" r="1.75" />
|
|
177
|
+
<circle cx="19" cy="12" r="1.75" />
|
|
178
|
+
</svg>
|
|
179
|
+
</button>
|
|
180
|
+
<Dropdown open={bulkMenuOpen} onClose={() => setBulkMenuOpen(false)}>
|
|
181
|
+
<DropdownItem onClick={async () => {
|
|
182
|
+
setBulkMenuOpen(false)
|
|
183
|
+
if (!window.confirm(`Delete ${filtered.length} chat${filtered.length === 1 ? '' : 's'}?`)) return
|
|
184
|
+
await clearSessions(filtered.map((s) => s.id))
|
|
185
|
+
toast.success(`${filtered.length} chat${filtered.length === 1 ? '' : 's'} deleted`)
|
|
186
|
+
}}>
|
|
187
|
+
Clear filtered chats
|
|
188
|
+
</DropdownItem>
|
|
189
|
+
</Dropdown>
|
|
190
|
+
</div>
|
|
155
191
|
)}
|
|
156
192
|
</div>
|
|
157
193
|
|
|
@@ -209,7 +245,7 @@ export function ChatList({ inSidebar, onSelect }: Props) {
|
|
|
209
245
|
) : (
|
|
210
246
|
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
|
|
211
247
|
<p className="text-[13px] text-text-3/50">
|
|
212
|
-
No {typeFilter === '
|
|
248
|
+
No {typeFilter === 'active' ? 'active' : typeFilter} chats{search ? ` matching "${search}"` : ''}
|
|
213
249
|
</p>
|
|
214
250
|
</div>
|
|
215
251
|
)}
|
|
@@ -68,7 +68,7 @@ export function CheckpointTimeline({ sessionId }: Props) {
|
|
|
68
68
|
return (
|
|
69
69
|
<div className="p-8 text-center">
|
|
70
70
|
<p className="text-text-3 text-[13px]">No checkpoints found for this chat.</p>
|
|
71
|
-
<p className="text-[11px] text-text-3/50 mt-1">
|
|
71
|
+
<p className="text-[11px] text-text-3/50 mt-1">Checkpoint restore is only available when a backend created checkpoints for this chat.</p>
|
|
72
72
|
</div>
|
|
73
73
|
)
|
|
74
74
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_HEARTBEAT_SHOW_ALERTS, DEFAULT_HEARTBEAT_SHOW_OK } from '@/lib/heartbeat-defaults'
|
|
4
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
4
5
|
import type { Message } from '@/types'
|
|
5
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
7
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
8
|
import { api } from '@/lib/api-client'
|
|
9
|
+
import { shouldHidePersistedStreamingAssistantMessage } from '@/lib/chat-streaming-state'
|
|
8
10
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
11
|
import { MessageBubble } from './message-bubble'
|
|
10
12
|
import { StreamingBubble } from './streaming-bubble'
|
|
@@ -49,9 +51,10 @@ interface Props {
|
|
|
49
51
|
messages: Message[]
|
|
50
52
|
streaming: boolean
|
|
51
53
|
connectorFilter?: string | null
|
|
54
|
+
loading?: boolean
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
export function MessageList({ messages, streaming, connectorFilter = null }: Props) {
|
|
57
|
+
export function MessageList({ messages, streaming, connectorFilter = null, loading = false }: Props) {
|
|
55
58
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
56
59
|
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
|
|
57
60
|
const snapUntilRef = useRef(0)
|
|
@@ -78,8 +81,8 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
78
81
|
|| (session?.provider === 'claude-cli' ? undefined : session?.model || session?.provider)
|
|
79
82
|
|| undefined
|
|
80
83
|
|
|
81
|
-
const showOk = appSettings.heartbeatShowOk ??
|
|
82
|
-
const showAlerts = appSettings.heartbeatShowAlerts ??
|
|
84
|
+
const showOk = appSettings.heartbeatShowOk ?? DEFAULT_HEARTBEAT_SHOW_OK
|
|
85
|
+
const showAlerts = appSettings.heartbeatShowAlerts ?? DEFAULT_HEARTBEAT_SHOW_ALERTS
|
|
83
86
|
|
|
84
87
|
// Gateway disconnect overlay for openclaw agents
|
|
85
88
|
const isOpenClaw = agent?.provider === 'openclaw'
|
|
@@ -156,6 +159,10 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
156
159
|
const [searchQuery, setSearchQuery] = useState('')
|
|
157
160
|
const [searchIdx, setSearchIdx] = useState(0)
|
|
158
161
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
162
|
+
const openSearch = useCallback(() => {
|
|
163
|
+
setSearchOpen(true)
|
|
164
|
+
setTimeout(() => searchInputRef.current?.focus(), 50)
|
|
165
|
+
}, [])
|
|
159
166
|
|
|
160
167
|
const isHeartbeatMessage = (msg: Message) =>
|
|
161
168
|
msg.role === 'assistant' && (msg.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(msg.text || '') || /^\s*NO_MESSAGE\b/i.test(msg.text || ''))
|
|
@@ -164,6 +171,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
164
171
|
|
|
165
172
|
const displayedMessages: Message[] = []
|
|
166
173
|
for (const msg of messages) {
|
|
174
|
+
if (shouldHidePersistedStreamingAssistantMessage(msg, { localStreaming: streaming, displayText })) continue
|
|
167
175
|
const isHeartbeat = isHeartbeatMessage(msg)
|
|
168
176
|
|
|
169
177
|
// Visibility filtering based on settings
|
|
@@ -190,11 +198,13 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
// Search matches
|
|
193
|
-
const searchMatches =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
const searchMatches = useMemo(() => {
|
|
202
|
+
const normalizedQuery = searchQuery.trim().toLowerCase()
|
|
203
|
+
if (!normalizedQuery) return []
|
|
204
|
+
return filteredMessages
|
|
205
|
+
.map((msg, i) => ({ msg, i }))
|
|
206
|
+
.filter(({ msg }) => msg.text.toLowerCase().includes(normalizedQuery))
|
|
207
|
+
}, [filteredMessages, searchQuery])
|
|
198
208
|
|
|
199
209
|
// Track whether user is at/near bottom so we know whether to auto-scroll on new content
|
|
200
210
|
const wasAtBottomRef = useRef(true)
|
|
@@ -309,6 +319,15 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
309
319
|
return () => window.removeEventListener('swarmclaw:scroll-to-message', handler)
|
|
310
320
|
}, [])
|
|
311
321
|
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!searchQuery || !searchMatches.length) return
|
|
324
|
+
const currentMatch = searchMatches[searchIdx]
|
|
325
|
+
if (!currentMatch) return
|
|
326
|
+
const el = scrollRef.current?.querySelector(`[data-message-index="${currentMatch.i}"]`) as HTMLElement | null
|
|
327
|
+
if (!el) return
|
|
328
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
329
|
+
}, [searchIdx, searchMatches, searchQuery])
|
|
330
|
+
|
|
312
331
|
// Ctrl+F search toggle
|
|
313
332
|
useEffect(() => {
|
|
314
333
|
const handler = (e: KeyboardEvent) => {
|
|
@@ -332,9 +351,81 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
332
351
|
|
|
333
352
|
return (
|
|
334
353
|
<div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden">
|
|
354
|
+
<div className="shrink-0 px-4 md:px-12 lg:px-16 pt-3">
|
|
355
|
+
<div className="flex flex-wrap items-center gap-2 rounded-[14px] border border-white/[0.06] bg-surface/55 px-3 py-2 backdrop-blur-sm">
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
onClick={() => {
|
|
359
|
+
if (searchOpen) {
|
|
360
|
+
setSearchOpen(false)
|
|
361
|
+
setSearchQuery('')
|
|
362
|
+
setSearchIdx(0)
|
|
363
|
+
} else {
|
|
364
|
+
openSearch()
|
|
365
|
+
}
|
|
366
|
+
}}
|
|
367
|
+
className={`inline-flex items-center gap-1.5 rounded-[9px] border px-2.5 py-1.5 text-[11px] font-600 transition-colors cursor-pointer ${
|
|
368
|
+
searchOpen
|
|
369
|
+
? 'border-accent-bright/25 bg-accent-soft/60 text-accent-bright'
|
|
370
|
+
: 'border-white/[0.06] bg-white/[0.03] text-text-3 hover:text-text-2 hover:bg-white/[0.06]'
|
|
371
|
+
}`}
|
|
372
|
+
>
|
|
373
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
374
|
+
<circle cx="11" cy="11" r="8" />
|
|
375
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
376
|
+
</svg>
|
|
377
|
+
Find
|
|
378
|
+
<span className="hidden sm:inline text-text-3/50">Cmd/Ctrl+F</span>
|
|
379
|
+
</button>
|
|
380
|
+
<button
|
|
381
|
+
type="button"
|
|
382
|
+
onClick={() => setBookmarkFilter((v) => !v)}
|
|
383
|
+
className={`inline-flex items-center gap-1.5 rounded-[9px] border px-2.5 py-1.5 text-[11px] font-600 transition-colors cursor-pointer ${
|
|
384
|
+
bookmarkFilter
|
|
385
|
+
? 'border-amber-400/25 bg-amber-500/10 text-amber-300'
|
|
386
|
+
: 'border-white/[0.06] bg-white/[0.03] text-text-3 hover:text-text-2 hover:bg-white/[0.06]'
|
|
387
|
+
}`}
|
|
388
|
+
>
|
|
389
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill={bookmarkFilter ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
390
|
+
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
391
|
+
</svg>
|
|
392
|
+
{bookmarkFilter ? 'Bookmarked' : 'Bookmarks'}
|
|
393
|
+
</button>
|
|
394
|
+
{(searchQuery || bookmarkFilter) && (
|
|
395
|
+
<button
|
|
396
|
+
type="button"
|
|
397
|
+
onClick={() => {
|
|
398
|
+
setSearchOpen(false)
|
|
399
|
+
setSearchQuery('')
|
|
400
|
+
setSearchIdx(0)
|
|
401
|
+
setBookmarkFilter(false)
|
|
402
|
+
}}
|
|
403
|
+
className="inline-flex items-center gap-1.5 rounded-[9px] border border-white/[0.06] bg-transparent px-2.5 py-1.5 text-[11px] font-600 text-text-3 hover:text-text-2 hover:bg-white/[0.04] cursor-pointer transition-colors"
|
|
404
|
+
>
|
|
405
|
+
Reset filters
|
|
406
|
+
</button>
|
|
407
|
+
)}
|
|
408
|
+
<div className="ml-auto flex items-center gap-2 text-[11px] text-text-3/60">
|
|
409
|
+
{searchQuery ? (
|
|
410
|
+
<span className="tabular-nums">
|
|
411
|
+
{searchMatches.length > 0 ? `${searchIdx + 1}/${searchMatches.length}` : '0 results'}
|
|
412
|
+
</span>
|
|
413
|
+
) : (
|
|
414
|
+
<span>{filteredMessages.length} message{filteredMessages.length === 1 ? '' : 's'}</span>
|
|
415
|
+
)}
|
|
416
|
+
{loading && (
|
|
417
|
+
<span className="inline-flex items-center gap-1.5 rounded-full bg-white/[0.04] px-2 py-1 text-text-3/70">
|
|
418
|
+
<span className="w-2 h-2 rounded-full bg-accent-bright animate-pulse" />
|
|
419
|
+
Loading thread
|
|
420
|
+
</span>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
335
426
|
{/* In-thread search bar */}
|
|
336
427
|
{searchOpen && (
|
|
337
|
-
<div className="
|
|
428
|
+
<div className="shrink-0 z-20 flex items-center gap-2 px-4 md:px-12 lg:px-16 py-2 bg-surface/95 backdrop-blur-sm border-b border-white/[0.06]">
|
|
338
429
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3 shrink-0">
|
|
339
430
|
<circle cx="11" cy="11" r="8" />
|
|
340
431
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
@@ -399,7 +490,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
399
490
|
<div
|
|
400
491
|
ref={scrollRef}
|
|
401
492
|
onScroll={updateScrollState}
|
|
402
|
-
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-4 md:px-12 lg:px-16 pt-
|
|
493
|
+
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-4 md:px-12 lg:px-16 pt-4 pb-[120px] md:pb-10 fade-up"
|
|
403
494
|
>
|
|
404
495
|
<div className="flex flex-col gap-6 relative">
|
|
405
496
|
{/* Chat spine — vertical line for assistant messages */}
|
|
@@ -440,13 +531,48 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
440
531
|
</div>
|
|
441
532
|
)}
|
|
442
533
|
{filteredMessages.length === 0 && !streaming && (
|
|
443
|
-
|
|
444
|
-
<
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
534
|
+
searchQuery.trim() || bookmarkFilter || connectorFilter ? (
|
|
535
|
+
<div className="flex flex-col items-center justify-center gap-3 py-20 text-center">
|
|
536
|
+
<div className="w-12 h-12 rounded-full bg-white/[0.04] border border-white/[0.06] flex items-center justify-center">
|
|
537
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" className="text-text-3/70">
|
|
538
|
+
<circle cx="11" cy="11" r="8" />
|
|
539
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
540
|
+
</svg>
|
|
541
|
+
</div>
|
|
542
|
+
<span className="font-display text-[16px] font-600 text-text-2">
|
|
543
|
+
{bookmarkFilter ? 'No bookmarked messages here' : 'No messages match these filters'}
|
|
544
|
+
</span>
|
|
545
|
+
<span className="text-[13px] text-text-3/60 max-w-[360px]">
|
|
546
|
+
{searchQuery.trim()
|
|
547
|
+
? `Nothing in this thread matches "${searchQuery.trim()}".`
|
|
548
|
+
: connectorFilter
|
|
549
|
+
? 'Try another source filter or reset the thread filters.'
|
|
550
|
+
: 'Try another keyword or turn off bookmarks-only mode.'}
|
|
551
|
+
</span>
|
|
552
|
+
{(searchQuery.trim() || bookmarkFilter) && (
|
|
553
|
+
<button
|
|
554
|
+
type="button"
|
|
555
|
+
onClick={() => {
|
|
556
|
+
setSearchOpen(false)
|
|
557
|
+
setSearchQuery('')
|
|
558
|
+
setSearchIdx(0)
|
|
559
|
+
setBookmarkFilter(false)
|
|
560
|
+
}}
|
|
561
|
+
className="rounded-[10px] border border-white/[0.06] bg-white/[0.03] px-3 py-2 text-[12px] font-600 text-text-2 hover:bg-white/[0.06] cursor-pointer transition-colors"
|
|
562
|
+
>
|
|
563
|
+
Clear thread filters
|
|
564
|
+
</button>
|
|
565
|
+
)}
|
|
566
|
+
</div>
|
|
567
|
+
) : (
|
|
568
|
+
<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' }}>
|
|
569
|
+
<AgentAvatar seed={agent?.avatarSeed || null} avatarUrl={agent?.avatarUrl} name={agent?.name || 'Agent'} size={48} />
|
|
570
|
+
<span className="font-display text-[16px] font-600 text-text-2">{agent?.name || 'Assistant'}</span>
|
|
571
|
+
<span className="text-[14px] text-text-3/60">
|
|
572
|
+
{INTRO_GREETINGS[stableHash(agent?.id || session?.id || '') % INTRO_GREETINGS.length]}
|
|
573
|
+
</span>
|
|
574
|
+
</div>
|
|
575
|
+
)
|
|
450
576
|
)}
|
|
451
577
|
{filteredMessages.map((msg, i) => {
|
|
452
578
|
// Context-clear divider — render a visual separator instead of a bubble
|
|
@@ -514,7 +640,7 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
|
|
|
514
640
|
|
|
515
641
|
return (
|
|
516
642
|
<div
|
|
517
|
-
key={`${sessionId}-${msg.
|
|
643
|
+
key={`${sessionId}-${msg.role}-${originalIndex >= 0 ? originalIndex : i}`}
|
|
518
644
|
data-message-index={i}
|
|
519
645
|
style={{
|
|
520
646
|
animation: `${msg.role === 'user' ? 'msg-in-right' : 'msg-in-left'} 0.4s var(--ease-spring) both`,
|
|
@@ -29,6 +29,16 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
29
29
|
const removePendingFile = useChatroomStore((s) => s.removePendingFile)
|
|
30
30
|
const replyingTo = useChatroomStore((s) => s.replyingTo)
|
|
31
31
|
const setReplyingTo = useChatroomStore((s) => s.setReplyingTo)
|
|
32
|
+
const streaming = useChatroomStore((s) => s.streaming)
|
|
33
|
+
const queuedMessages = useChatroomStore((s) => s.queuedMessages)
|
|
34
|
+
const removeQueuedMessage = useChatroomStore((s) => s.removeQueuedMessage)
|
|
35
|
+
|
|
36
|
+
const resizeTextarea = useCallback(() => {
|
|
37
|
+
const node = inputRef.current
|
|
38
|
+
if (!node) return
|
|
39
|
+
node.style.height = 'auto'
|
|
40
|
+
node.style.height = `${Math.min(node.scrollHeight, 160)}px`
|
|
41
|
+
}, [])
|
|
32
42
|
|
|
33
43
|
// Draft persistence: restore on chatroom change
|
|
34
44
|
const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
@@ -38,6 +48,10 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
38
48
|
setText(draft || '')
|
|
39
49
|
}, [chatroomId])
|
|
40
50
|
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
resizeTextarea()
|
|
53
|
+
}, [resizeTextarea, text, chatroomId])
|
|
54
|
+
|
|
41
55
|
// Debounced save to localStorage
|
|
42
56
|
useEffect(() => {
|
|
43
57
|
if (!chatroomId) return
|
|
@@ -83,6 +97,7 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
83
97
|
|
|
84
98
|
const handleChange = useCallback((value: string) => {
|
|
85
99
|
setText(value)
|
|
100
|
+
resizeTextarea()
|
|
86
101
|
const cursorPos = inputRef.current?.selectionStart || value.length
|
|
87
102
|
const beforeCursor = value.slice(0, cursorPos)
|
|
88
103
|
const mentionMatch = beforeCursor.match(/@(\S*)$/)
|
|
@@ -95,7 +110,7 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
95
110
|
setMentionFilter('')
|
|
96
111
|
setSelectedIndex(0)
|
|
97
112
|
}
|
|
98
|
-
}, [])
|
|
113
|
+
}, [resizeTextarea])
|
|
99
114
|
|
|
100
115
|
const insertMention = useCallback((name: string) => {
|
|
101
116
|
const cursorPos = inputRef.current?.selectionStart || text.length
|
|
@@ -138,13 +153,23 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
138
153
|
return parts.length > 0 ? parts : null
|
|
139
154
|
}, [text])
|
|
140
155
|
|
|
141
|
-
const mentionDropdownVisible = showMentions
|
|
156
|
+
const mentionDropdownVisible = showMentions
|
|
142
157
|
const mentionItems = mentionDropdownVisible
|
|
143
158
|
? ['all', ...filteredAgents.map((a) => a.name)]
|
|
144
159
|
: []
|
|
160
|
+
const visibleQueuedMessages = queuedMessages.filter((item) => item.chatroomId === chatroomId)
|
|
161
|
+
|
|
162
|
+
const handleSendCurrent = useCallback(() => {
|
|
163
|
+
if ((!text.trim() && !pendingFiles.length) || disabled) return
|
|
164
|
+
onSend(text)
|
|
165
|
+
setText('')
|
|
166
|
+
resizeTextarea()
|
|
167
|
+
if (chatroomId) safeStorageRemove(`sc_draft_cr_${chatroomId}`)
|
|
168
|
+
setShowMentions(false)
|
|
169
|
+
}, [chatroomId, disabled, onSend, pendingFiles.length, resizeTextarea, text])
|
|
145
170
|
|
|
146
171
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
147
|
-
if (mentionDropdownVisible) {
|
|
172
|
+
if (mentionDropdownVisible && mentionItems.length > 0) {
|
|
148
173
|
if (e.key === 'ArrowDown') {
|
|
149
174
|
e.preventDefault()
|
|
150
175
|
setSelectedIndex((i) => (i + 1) % mentionItems.length)
|
|
@@ -165,12 +190,7 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
165
190
|
|
|
166
191
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
167
192
|
e.preventDefault()
|
|
168
|
-
|
|
169
|
-
onSend(text)
|
|
170
|
-
setText('')
|
|
171
|
-
if (chatroomId) safeStorageRemove(`sc_draft_cr_${chatroomId}`)
|
|
172
|
-
setShowMentions(false)
|
|
173
|
-
}
|
|
193
|
+
handleSendCurrent()
|
|
174
194
|
}
|
|
175
195
|
if (e.key === 'Escape') {
|
|
176
196
|
if (replyingTo) {
|
|
@@ -195,21 +215,68 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
195
215
|
<span className="text-[13px] text-text">all</span>
|
|
196
216
|
<span className="text-[11px] text-text-3 ml-auto">Mention all agents</span>
|
|
197
217
|
</button>
|
|
198
|
-
{filteredAgents.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
{filteredAgents.length > 0 ? (
|
|
219
|
+
filteredAgents.map((agent, i) => (
|
|
220
|
+
<button
|
|
221
|
+
key={agent.id}
|
|
222
|
+
onClick={() => insertMention(agent.name)}
|
|
223
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-left transition-all cursor-pointer ${
|
|
224
|
+
selectedIndex === i + 1 ? 'bg-white/[0.08]' : 'hover:bg-white/[0.06]'
|
|
225
|
+
}`}
|
|
226
|
+
>
|
|
227
|
+
<AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={20} />
|
|
228
|
+
<span className="text-[13px] text-text">{agent.name}</span>
|
|
229
|
+
</button>
|
|
230
|
+
))
|
|
231
|
+
) : (
|
|
232
|
+
<div className="px-3 py-3 text-[12px] text-text-3">
|
|
233
|
+
No agents match <span className="text-text">@{mentionFilter}</span>.
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{visibleQueuedMessages.length > 0 && (
|
|
240
|
+
<div className="mb-2 flex flex-wrap items-center gap-1.5">
|
|
241
|
+
<span className="label-mono text-amber-400/70">Queued</span>
|
|
242
|
+
{visibleQueuedMessages.map((item) => (
|
|
243
|
+
<span key={item.id} className="inline-flex items-center gap-1.5 rounded-[8px] border border-amber-500/15 bg-amber-500/10 px-2.5 py-1 text-[11px] text-amber-300">
|
|
244
|
+
<span className="truncate max-w-[180px]">
|
|
245
|
+
{item.text.trim() || `Attachment${item.pendingFiles.length > 1 ? 's' : ''}`}
|
|
246
|
+
</span>
|
|
247
|
+
{item.pendingFiles.length > 0 && (
|
|
248
|
+
<span className="rounded-full bg-amber-500/10 px-1.5 py-0.5 text-[10px]">
|
|
249
|
+
+{item.pendingFiles.length} file{item.pendingFiles.length === 1 ? '' : 's'}
|
|
250
|
+
</span>
|
|
251
|
+
)}
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onClick={() => removeQueuedMessage(item.id)}
|
|
255
|
+
className="border-none bg-transparent p-0 text-amber-300/70 hover:text-amber-200 cursor-pointer"
|
|
256
|
+
>
|
|
257
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
258
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
259
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
260
|
+
</svg>
|
|
261
|
+
</button>
|
|
262
|
+
</span>
|
|
209
263
|
))}
|
|
210
264
|
</div>
|
|
211
265
|
)}
|
|
212
266
|
|
|
267
|
+
{visibleQueuedMessages.length === 0 && !disabled && (
|
|
268
|
+
<div className="mb-2 flex items-center justify-between gap-2 rounded-[10px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
|
|
269
|
+
<span className="text-[11px] text-text-3">
|
|
270
|
+
{streaming
|
|
271
|
+
? 'Current round is still running. Press send to queue the next message.'
|
|
272
|
+
: agents.length > 0
|
|
273
|
+
? 'Use @AgentName or @all to direct the next reply.'
|
|
274
|
+
: 'Start the next round here.'}
|
|
275
|
+
</span>
|
|
276
|
+
<span className="text-[10px] text-text-3/50">Enter sends · Shift+Enter newline</span>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
|
|
213
280
|
{/* Reply preview banner */}
|
|
214
281
|
{replyingTo && (
|
|
215
282
|
<div className="flex items-center gap-2 mb-2 px-2 py-1.5 rounded-[8px] bg-white/[0.04] border border-white/[0.06]">
|
|
@@ -268,12 +335,12 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
268
335
|
</svg>
|
|
269
336
|
</button>
|
|
270
337
|
|
|
271
|
-
<div className="flex-1 relative rounded-[
|
|
338
|
+
<div className="flex-1 relative rounded-[10px] bg-white/[0.06] border border-white/[0.08] focus-within:border-accent-bright/40">
|
|
272
339
|
{/* Highlight mirror — renders @mentions with accent background behind the transparent textarea */}
|
|
273
340
|
<div
|
|
274
341
|
aria-hidden
|
|
275
342
|
className="absolute inset-0 px-3 py-2 text-[13px] leading-[1.5] break-words whitespace-pre-wrap pointer-events-none overflow-hidden"
|
|
276
|
-
style={{ minHeight: '
|
|
343
|
+
style={{ minHeight: '44px', color: 'transparent' }}
|
|
277
344
|
>
|
|
278
345
|
{highlightedSegments}
|
|
279
346
|
</div>
|
|
@@ -286,21 +353,17 @@ export function ChatroomInput({ agents, onSend, disabled }: Props) {
|
|
|
286
353
|
placeholder="Type a message... Use @ to mention agents"
|
|
287
354
|
disabled={disabled}
|
|
288
355
|
rows={1}
|
|
289
|
-
className="w-full resize-none
|
|
290
|
-
style={{ minHeight: '
|
|
356
|
+
className="relative w-full resize-none rounded-[10px] border-none bg-transparent px-3 py-2.5 text-[13px] text-text placeholder:text-text-3 focus:outline-none max-h-[160px] disabled:opacity-50"
|
|
357
|
+
style={{ minHeight: '44px' }}
|
|
291
358
|
/>
|
|
292
359
|
</div>
|
|
293
360
|
<button
|
|
294
|
-
onClick={
|
|
295
|
-
if ((text.trim() || pendingFiles.length) && !disabled) {
|
|
296
|
-
onSend(text)
|
|
297
|
-
setText('')
|
|
298
|
-
if (chatroomId) safeStorageRemove(`sc_draft_cr_${chatroomId}`)
|
|
299
|
-
setShowMentions(false)
|
|
300
|
-
}
|
|
301
|
-
}}
|
|
361
|
+
onClick={handleSendCurrent}
|
|
302
362
|
disabled={(!text.trim() && !pendingFiles.length) || disabled}
|
|
303
|
-
className=
|
|
363
|
+
className={`shrink-0 w-10 h-10 rounded-[10px] flex items-center justify-center transition-all disabled:opacity-30 cursor-pointer ${
|
|
364
|
+
streaming ? 'bg-amber-500/20 hover:bg-amber-500/30' : 'bg-accent-bright hover:bg-accent-bright/90'
|
|
365
|
+
}`}
|
|
366
|
+
title="Send or queue message"
|
|
304
367
|
>
|
|
305
368
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
306
369
|
<line x1="22" y1="2" x2="11" y2="13" />
|