@swarmclawai/swarmclaw 0.6.0 → 0.6.2
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 +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react'
|
|
4
|
+
import ReactMarkdown from 'react-markdown'
|
|
5
|
+
import remarkGfm from 'remark-gfm'
|
|
4
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
7
|
import { createTask, updateTask, archiveTask, unarchiveTask } from '@/lib/tasks'
|
|
6
8
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
@@ -10,6 +12,7 @@ import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
|
10
12
|
import { inputClass } from '@/components/shared/form-styles'
|
|
11
13
|
import type { BoardTask, TaskComment } from '@/types'
|
|
12
14
|
import { SectionLabel } from '@/components/shared/section-label'
|
|
15
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
13
16
|
|
|
14
17
|
function fmtTime(ts: number) {
|
|
15
18
|
const d = new Date(ts)
|
|
@@ -33,6 +36,9 @@ export function TaskSheet() {
|
|
|
33
36
|
const loadProjects = useAppStore((s) => s.loadProjects)
|
|
34
37
|
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
35
38
|
|
|
39
|
+
const viewOnly = useAppStore((s) => s.taskSheetViewOnly)
|
|
40
|
+
const setViewOnly = useAppStore((s) => s.setTaskSheetViewOnly)
|
|
41
|
+
|
|
36
42
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
37
43
|
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
38
44
|
|
|
@@ -50,6 +56,7 @@ export function TaskSheet() {
|
|
|
50
56
|
const [blockedBy, setBlockedBy] = useState<string[]>([])
|
|
51
57
|
const [dueAt, setDueAt] = useState<string>('')
|
|
52
58
|
const [customFields, setCustomFields] = useState<Record<string, string | number | boolean>>({})
|
|
59
|
+
const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'critical' | ''>('')
|
|
53
60
|
|
|
54
61
|
const editing = editingId ? tasks[editingId] : null
|
|
55
62
|
const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
|
|
@@ -71,6 +78,7 @@ export function TaskSheet() {
|
|
|
71
78
|
setBlockedBy(editing.blockedBy || [])
|
|
72
79
|
setDueAt(editing.dueAt ? new Date(editing.dueAt).toISOString().slice(0, 10) : '')
|
|
73
80
|
setCustomFields(editing.customFields || {})
|
|
81
|
+
setPriority(editing.priority || '')
|
|
74
82
|
} else {
|
|
75
83
|
setTitle('')
|
|
76
84
|
setDescription('')
|
|
@@ -83,6 +91,7 @@ export function TaskSheet() {
|
|
|
83
91
|
setBlockedBy([])
|
|
84
92
|
setDueAt('')
|
|
85
93
|
setCustomFields({})
|
|
94
|
+
setPriority('')
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -108,6 +117,7 @@ export function TaskSheet() {
|
|
|
108
117
|
cwd: cwd || undefined, file: file || undefined,
|
|
109
118
|
tags, blockedBy, dueAt: dueAt ? new Date(dueAt).getTime() : null,
|
|
110
119
|
customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
|
|
120
|
+
priority: priority || undefined,
|
|
111
121
|
} as Partial<BoardTask> & { title: string; description: string; agentId: string }
|
|
112
122
|
if (editing) {
|
|
113
123
|
await updateTask(editing.id, payload)
|
|
@@ -130,7 +140,9 @@ export function TaskSheet() {
|
|
|
130
140
|
})
|
|
131
141
|
const data = await res.json()
|
|
132
142
|
if (data.url) setImages((prev) => [...prev, data.url])
|
|
133
|
-
} catch
|
|
143
|
+
} catch (err: unknown) {
|
|
144
|
+
console.error('Image upload failed:', err instanceof Error ? err.message : String(err))
|
|
145
|
+
}
|
|
134
146
|
setUploading(false)
|
|
135
147
|
e.target.value = ''
|
|
136
148
|
}
|
|
@@ -173,9 +185,296 @@ export function TaskSheet() {
|
|
|
173
185
|
setCommentText('')
|
|
174
186
|
}
|
|
175
187
|
|
|
188
|
+
const PRIORITY_STYLES: Record<string, string> = {
|
|
189
|
+
low: 'bg-sky-500/10 border-sky-500/20 text-sky-400',
|
|
190
|
+
medium: 'bg-amber-500/10 border-amber-500/20 text-amber-400',
|
|
191
|
+
high: 'bg-orange-500/10 border-orange-500/20 text-orange-400',
|
|
192
|
+
critical: 'bg-red-500/10 border-red-500/20 text-red-400',
|
|
193
|
+
}
|
|
194
|
+
const STATUS_STYLES: Record<string, string> = {
|
|
195
|
+
backlog: 'bg-white/[0.06] text-text-3',
|
|
196
|
+
queued: 'bg-amber-500/10 text-amber-400',
|
|
197
|
+
'in-progress': 'bg-sky-500/10 text-sky-400',
|
|
198
|
+
completed: 'bg-emerald-500/10 text-emerald-400',
|
|
199
|
+
failed: 'bg-red-500/10 text-red-400',
|
|
200
|
+
archived: 'bg-white/[0.04] text-text-3/60',
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const taskAgent = editing ? agents[editing.agentId] : null
|
|
204
|
+
const taskProject = editing?.projectId ? projects[editing.projectId] : null
|
|
205
|
+
|
|
206
|
+
/* ───── View-only mode ───── */
|
|
207
|
+
if (viewOnly && editing) {
|
|
208
|
+
return (
|
|
209
|
+
<BottomSheet open={open} onClose={onClose}>
|
|
210
|
+
{/* Header: title + badges + timestamps */}
|
|
211
|
+
<div className="mb-8">
|
|
212
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-3">
|
|
213
|
+
{editing.title}
|
|
214
|
+
</h2>
|
|
215
|
+
<div className="flex flex-wrap items-center gap-2 mb-3">
|
|
216
|
+
<span className={`px-2.5 py-1 rounded-[8px] text-[12px] font-600 border border-transparent ${STATUS_STYLES[editing.status] || 'bg-white/[0.06] text-text-3'}`}>
|
|
217
|
+
{editing.status}
|
|
218
|
+
</span>
|
|
219
|
+
{editing.priority && (
|
|
220
|
+
<span className={`px-2.5 py-1 rounded-[8px] text-[12px] font-600 border ${PRIORITY_STYLES[editing.priority] || ''}`}>
|
|
221
|
+
{editing.priority}
|
|
222
|
+
</span>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[12px] text-text-3">
|
|
226
|
+
<span>Created {fmtTime(editing.createdAt)}</span>
|
|
227
|
+
{editing.startedAt && <span>Started {fmtTime(editing.startedAt)}</span>}
|
|
228
|
+
{editing.completedAt && <span>Completed {fmtTime(editing.completedAt)}</span>}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Description */}
|
|
233
|
+
{editing.description && (
|
|
234
|
+
<div className="mb-8">
|
|
235
|
+
<SectionLabel>Description</SectionLabel>
|
|
236
|
+
<div className="msg-content text-[14px] leading-[1.7] text-text-2 break-words p-4 rounded-[14px] border border-white/[0.06] bg-surface">
|
|
237
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{editing.description}</ReactMarkdown>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* Agent */}
|
|
243
|
+
{taskAgent && (
|
|
244
|
+
<div className="mb-8">
|
|
245
|
+
<SectionLabel>Agent</SectionLabel>
|
|
246
|
+
<div className="flex items-center gap-2.5 px-4 py-3 rounded-[14px] border border-white/[0.06] bg-surface">
|
|
247
|
+
<AgentAvatar seed={taskAgent.avatarSeed || null} name={taskAgent.name} size={24} />
|
|
248
|
+
<span className="text-[14px] font-600 text-text">{taskAgent.name}</span>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Project */}
|
|
254
|
+
{taskProject && (
|
|
255
|
+
<div className="mb-8">
|
|
256
|
+
<SectionLabel>Project</SectionLabel>
|
|
257
|
+
<span className="inline-flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface text-[13px] font-600 text-text-2">
|
|
258
|
+
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: taskProject.color || '#6366F1' }} />
|
|
259
|
+
{taskProject.name}
|
|
260
|
+
</span>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{/* Directory / File */}
|
|
265
|
+
{(editing.cwd || editing.file) && (
|
|
266
|
+
<div className="mb-8">
|
|
267
|
+
<SectionLabel>{editing.file ? 'File' : 'Directory'}</SectionLabel>
|
|
268
|
+
<code className="block px-4 py-3 rounded-[14px] border border-white/[0.06] bg-surface text-[13px] text-text-2 font-mono break-all">
|
|
269
|
+
{editing.file || editing.cwd}
|
|
270
|
+
</code>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
{/* Tags */}
|
|
275
|
+
{editing.tags && editing.tags.length > 0 && (
|
|
276
|
+
<div className="mb-8">
|
|
277
|
+
<SectionLabel>Tags</SectionLabel>
|
|
278
|
+
<div className="flex flex-wrap gap-1.5">
|
|
279
|
+
{editing.tags.map((tag) => (
|
|
280
|
+
<span key={tag} className="px-2.5 py-1 rounded-[8px] bg-indigo-500/10 text-indigo-400 text-[12px] font-600">
|
|
281
|
+
{tag}
|
|
282
|
+
</span>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{/* Blocked By */}
|
|
289
|
+
{editing.blockedBy && editing.blockedBy.length > 0 && (
|
|
290
|
+
<div className="mb-8">
|
|
291
|
+
<SectionLabel>Blocked By</SectionLabel>
|
|
292
|
+
<div className="flex flex-wrap gap-1.5">
|
|
293
|
+
{editing.blockedBy.map((bid) => {
|
|
294
|
+
const bt = tasks[bid]
|
|
295
|
+
return (
|
|
296
|
+
<span key={bid} className="px-2.5 py-1 rounded-[8px] bg-white/[0.04] text-text-3 text-[12px] font-600">
|
|
297
|
+
{bt ? bt.title : bid}
|
|
298
|
+
</span>
|
|
299
|
+
)
|
|
300
|
+
})}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
{/* Blocks */}
|
|
306
|
+
{editing.blocks && editing.blocks.length > 0 && (
|
|
307
|
+
<div className="mb-8">
|
|
308
|
+
<SectionLabel>Blocks</SectionLabel>
|
|
309
|
+
<div className="flex flex-wrap gap-1.5">
|
|
310
|
+
{editing.blocks.map((bid) => {
|
|
311
|
+
const bt = tasks[bid]
|
|
312
|
+
return bt ? (
|
|
313
|
+
<span key={bid} className="px-2.5 py-1 rounded-[8px] bg-white/[0.04] text-text-3 text-[12px] font-600">{bt.title}</span>
|
|
314
|
+
) : null
|
|
315
|
+
})}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
|
|
320
|
+
{/* Due Date */}
|
|
321
|
+
{editing.dueAt && (
|
|
322
|
+
<div className="mb-8">
|
|
323
|
+
<SectionLabel>Due Date</SectionLabel>
|
|
324
|
+
<span className="text-[14px] text-text-2">{new Date(editing.dueAt).toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })}</span>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
|
|
328
|
+
{/* Custom Fields */}
|
|
329
|
+
{editing.customFields && Object.keys(editing.customFields).length > 0 && (
|
|
330
|
+
<div className="mb-8">
|
|
331
|
+
<SectionLabel>Custom Fields</SectionLabel>
|
|
332
|
+
<div className="space-y-2">
|
|
333
|
+
{Object.entries(editing.customFields).map(([key, val]) => {
|
|
334
|
+
const def = appSettings.taskCustomFieldDefs?.find((d) => d.key === key)
|
|
335
|
+
return (
|
|
336
|
+
<div key={key} className="flex items-baseline gap-2">
|
|
337
|
+
<span className="text-[12px] font-600 text-text-3">{def?.label || key}:</span>
|
|
338
|
+
<span className="text-[13px] text-text-2">{String(val)}</span>
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
})}
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
|
|
346
|
+
{/* Images (thumbnails only, no remove/upload) */}
|
|
347
|
+
{editing.images && editing.images.length > 0 && (
|
|
348
|
+
<div className="mb-8">
|
|
349
|
+
<SectionLabel>Images</SectionLabel>
|
|
350
|
+
<div className="flex gap-2 flex-wrap">
|
|
351
|
+
{editing.images.map((url, i) => (
|
|
352
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
353
|
+
<img key={i} src={url} alt="" className="w-20 h-20 rounded-[10px] object-cover border border-white/[0.08]" />
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
{/* Result */}
|
|
360
|
+
{editing.result && (
|
|
361
|
+
<div className="mb-8">
|
|
362
|
+
<SectionLabel>Result</SectionLabel>
|
|
363
|
+
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface text-[13px] text-text-2 whitespace-pre-wrap max-h-[200px] overflow-y-auto">
|
|
364
|
+
{editing.result}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
{/* CLI Sessions */}
|
|
370
|
+
{(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.cliResumeId) && (
|
|
371
|
+
<div className="mb-8">
|
|
372
|
+
<SectionLabel>CLI Sessions</SectionLabel>
|
|
373
|
+
<div className="flex flex-wrap gap-2">
|
|
374
|
+
{editing.claudeResumeId && (
|
|
375
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
376
|
+
<span className="text-[11px] font-600 text-amber-400">Claude</span>
|
|
377
|
+
<code className="text-[11px] text-text-3 font-mono">{editing.claudeResumeId}</code>
|
|
378
|
+
</div>
|
|
379
|
+
)}
|
|
380
|
+
{editing.codexResumeId && (
|
|
381
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
382
|
+
<span className="text-[11px] font-600 text-emerald-400">Codex</span>
|
|
383
|
+
<code className="text-[11px] text-text-3 font-mono">{editing.codexResumeId}</code>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
{editing.opencodeResumeId && (
|
|
387
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
388
|
+
<span className="text-[11px] font-600 text-sky-400">OpenCode</span>
|
|
389
|
+
<code className="text-[11px] text-text-3 font-mono">{editing.opencodeResumeId}</code>
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
{!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId) && editing.cliResumeId && (
|
|
393
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
394
|
+
<span className="text-[11px] font-600 text-text-2">{editing.cliProvider || 'CLI'}</span>
|
|
395
|
+
<code className="text-[11px] text-text-3 font-mono">{editing.cliResumeId}</code>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{/* Error */}
|
|
403
|
+
{editing.error && (
|
|
404
|
+
<div className="mb-8">
|
|
405
|
+
<label className="block font-display text-[12px] font-600 text-red-400 uppercase tracking-[0.08em] mb-3">Error</label>
|
|
406
|
+
<div className="p-4 rounded-[14px] border border-red-500/10 bg-red-500/[0.03] text-[13px] text-red-400/80 whitespace-pre-wrap">
|
|
407
|
+
{editing.error}
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
{/* Comments (with input — adding comments from view mode is useful) */}
|
|
413
|
+
<div className="mb-8">
|
|
414
|
+
<SectionLabel>Comments {editing.comments?.length ? `(${editing.comments.length})` : ''}</SectionLabel>
|
|
415
|
+
|
|
416
|
+
{editing.comments && editing.comments.length > 0 && (
|
|
417
|
+
<div className="space-y-3 mb-4 max-h-[300px] overflow-y-auto">
|
|
418
|
+
{editing.comments.map((c) => (
|
|
419
|
+
<div key={c.id} className="p-3.5 rounded-[12px] border border-white/[0.06] bg-surface">
|
|
420
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
421
|
+
<span className={`text-[12px] font-600 ${c.agentId ? 'text-accent-bright' : 'text-text-2'}`}>
|
|
422
|
+
{c.author}
|
|
423
|
+
</span>
|
|
424
|
+
<span className="text-[10px] text-text-3/50 font-mono">{fmtTime(c.createdAt)}</span>
|
|
425
|
+
</div>
|
|
426
|
+
<p className="text-[13px] text-text-2 leading-[1.5] whitespace-pre-wrap">{c.text}</p>
|
|
427
|
+
</div>
|
|
428
|
+
))}
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
|
|
432
|
+
<div className="flex gap-2">
|
|
433
|
+
<input
|
|
434
|
+
type="text"
|
|
435
|
+
value={commentText}
|
|
436
|
+
onChange={(e) => setCommentText(e.target.value)}
|
|
437
|
+
placeholder="Add a comment..."
|
|
438
|
+
className={`${inputClass} flex-1`}
|
|
439
|
+
style={{ fontFamily: 'inherit' }}
|
|
440
|
+
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAddComment() } }}
|
|
441
|
+
/>
|
|
442
|
+
<button
|
|
443
|
+
onClick={handleAddComment}
|
|
444
|
+
disabled={!commentText.trim()}
|
|
445
|
+
className="px-4 py-3 rounded-[14px] border-none bg-accent-soft text-accent-bright text-[13px] font-600 cursor-pointer disabled:opacity-30 hover:brightness-110 transition-all shrink-0"
|
|
446
|
+
style={{ fontFamily: 'inherit' }}
|
|
447
|
+
>
|
|
448
|
+
Post
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{/* Footer: Edit + Close */}
|
|
454
|
+
<div className="flex gap-3 pt-2 border-t border-white/[0.04]">
|
|
455
|
+
<button
|
|
456
|
+
onClick={onClose}
|
|
457
|
+
className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
|
|
458
|
+
style={{ fontFamily: 'inherit' }}
|
|
459
|
+
>
|
|
460
|
+
Close
|
|
461
|
+
</button>
|
|
462
|
+
<button
|
|
463
|
+
onClick={() => setViewOnly(false)}
|
|
464
|
+
className="flex-1 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
|
|
465
|
+
style={{ fontFamily: 'inherit' }}
|
|
466
|
+
>
|
|
467
|
+
Edit
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
</BottomSheet>
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* ───── Edit / Create mode ───── */
|
|
176
475
|
return (
|
|
177
476
|
<BottomSheet open={open} onClose={onClose}>
|
|
178
|
-
<div className="mb-
|
|
477
|
+
<div className="mb-8">
|
|
179
478
|
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
180
479
|
{editing ? 'Edit Task' : 'New Task'}
|
|
181
480
|
</h2>
|
|
@@ -201,13 +500,38 @@ export function TaskSheet() {
|
|
|
201
500
|
<textarea
|
|
202
501
|
value={description}
|
|
203
502
|
onChange={(e) => setDescription(e.target.value)}
|
|
204
|
-
placeholder="Detailed task instructions
|
|
503
|
+
placeholder="Detailed task instructions... Use @AgentName to auto-assign"
|
|
205
504
|
rows={4}
|
|
206
505
|
className={`${inputClass} resize-y min-h-[100px]`}
|
|
207
506
|
style={{ fontFamily: 'inherit' }}
|
|
208
507
|
/>
|
|
209
508
|
</div>
|
|
210
509
|
|
|
510
|
+
{/* Priority */}
|
|
511
|
+
<div className="mb-8">
|
|
512
|
+
<SectionLabel>Priority <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
|
|
513
|
+
<div className="flex flex-wrap gap-2">
|
|
514
|
+
{([['', 'None', 'bg-surface border-white/[0.06] text-text-2'],
|
|
515
|
+
['low', 'Low', 'bg-sky-500/10 border-sky-500/20 text-sky-400'],
|
|
516
|
+
['medium', 'Medium', 'bg-amber-500/10 border-amber-500/20 text-amber-400'],
|
|
517
|
+
['high', 'High', 'bg-orange-500/10 border-orange-500/20 text-orange-400'],
|
|
518
|
+
['critical', 'Critical', 'bg-red-500/10 border-red-500/20 text-red-400'],
|
|
519
|
+
] as const).map(([val, label, cls]) => (
|
|
520
|
+
<button
|
|
521
|
+
key={val}
|
|
522
|
+
onClick={() => setPriority(val as typeof priority)}
|
|
523
|
+
className={`px-4 py-3 rounded-[12px] text-[14px] font-600 cursor-pointer transition-all border
|
|
524
|
+
${priority === val
|
|
525
|
+
? `${cls} ring-1 ring-current`
|
|
526
|
+
: 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
|
|
527
|
+
style={{ fontFamily: 'inherit' }}
|
|
528
|
+
>
|
|
529
|
+
{label}
|
|
530
|
+
</button>
|
|
531
|
+
))}
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
|
|
211
535
|
{/* Images */}
|
|
212
536
|
<div className="mb-8">
|
|
213
537
|
<SectionLabel>Images <span className="normal-case tracking-normal font-normal text-text-3">(optional — reference designs, mockups, etc.)</span></SectionLabel>
|
|
@@ -341,6 +665,7 @@ export function TaskSheet() {
|
|
|
341
665
|
<SectionLabel>Blocked By <span className="normal-case tracking-normal font-normal text-text-3">(tasks that must complete first)</span></SectionLabel>
|
|
342
666
|
<select
|
|
343
667
|
multiple
|
|
668
|
+
aria-label="Assign agents"
|
|
344
669
|
value={blockedBy}
|
|
345
670
|
onChange={(e) => setBlockedBy(Array.from(e.target.selectedOptions, (o) => o.value))}
|
|
346
671
|
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[13px] outline-none min-h-[80px] focus-glow"
|
|
@@ -57,6 +57,13 @@ function formatCost(n: number): string {
|
|
|
57
57
|
return `$${n.toFixed(4)}`
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function formatDuration(ms: number): string {
|
|
61
|
+
if (!ms) return '—'
|
|
62
|
+
if (ms < 60_000) return `${Math.round(ms / 1000)}s`
|
|
63
|
+
if (ms < 3600_000) return `${Math.round(ms / 60_000)}m`
|
|
64
|
+
return `${(ms / 3600_000).toFixed(1)}h`
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
function formatBucketLabel(bucket: string, range: Range): string {
|
|
61
68
|
if (range === '24h') {
|
|
62
69
|
// "2026-03-01T14" → "14:00"
|
|
@@ -128,7 +135,24 @@ export function MetricsDashboard() {
|
|
|
128
135
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
136
|
}, [])
|
|
130
137
|
|
|
138
|
+
// --- Task metrics ---
|
|
139
|
+
const [taskMetrics, setTaskMetrics] = useState<{
|
|
140
|
+
wip: number; completedCount: number; avgCycleMs: number
|
|
141
|
+
velocity: { bucket: string; count: number }[]
|
|
142
|
+
byAgent: { agentName: string; completed: number; failed: number }[]
|
|
143
|
+
} | null>(null)
|
|
144
|
+
|
|
145
|
+
const loadTaskMetrics = useCallback(async () => {
|
|
146
|
+
try {
|
|
147
|
+
const res = await api<typeof taskMetrics>('GET', `/tasks/metrics?range=${range}`)
|
|
148
|
+
setTaskMetrics(res)
|
|
149
|
+
} catch { /* ignore */ }
|
|
150
|
+
}, [range])
|
|
151
|
+
|
|
152
|
+
useEffect(() => { loadTaskMetrics() }, [loadTaskMetrics])
|
|
153
|
+
|
|
131
154
|
useWs('usage', loadData, 30_000)
|
|
155
|
+
useWs('tasks', loadTaskMetrics, 15_000)
|
|
132
156
|
|
|
133
157
|
const completionRate = computeCompletionRate(tasks)
|
|
134
158
|
|
|
@@ -154,20 +178,20 @@ export function MetricsDashboard() {
|
|
|
154
178
|
|
|
155
179
|
const tooltipStyle = {
|
|
156
180
|
contentStyle: {
|
|
157
|
-
background: '
|
|
181
|
+
background: 'var(--color-surface)',
|
|
158
182
|
border: '1px solid rgba(255,255,255,0.08)',
|
|
159
183
|
borderRadius: 8,
|
|
160
184
|
fontSize: 12,
|
|
161
|
-
color: '
|
|
185
|
+
color: 'var(--color-text)',
|
|
162
186
|
},
|
|
163
|
-
itemStyle: { color: '
|
|
164
|
-
labelStyle: { color: '
|
|
187
|
+
itemStyle: { color: 'var(--color-text)' },
|
|
188
|
+
labelStyle: { color: 'var(--color-text-2)' },
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
return (
|
|
168
192
|
<div className="flex-1 flex flex-col h-full overflow-y-auto">
|
|
169
193
|
<div className="px-8 pt-6 pb-4 shrink-0">
|
|
170
|
-
<h1 className="font-display text-[28px] font-
|
|
194
|
+
<h1 className="font-display text-[28px] font-700 tracking-[-0.03em]">Usage</h1>
|
|
171
195
|
<p className="text-[13px] text-text-3 mt-1">Token usage, cost tracking & agent performance</p>
|
|
172
196
|
</div>
|
|
173
197
|
|
|
@@ -192,7 +216,10 @@ export function MetricsDashboard() {
|
|
|
192
216
|
|
|
193
217
|
{loading && !data ? (
|
|
194
218
|
<div className="flex-1 flex items-center justify-center">
|
|
195
|
-
<
|
|
219
|
+
<div className="flex items-center gap-3">
|
|
220
|
+
<span className="w-5 h-5 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin" />
|
|
221
|
+
<span className="text-[14px] text-text-3">Loading metrics...</span>
|
|
222
|
+
</div>
|
|
196
223
|
</div>
|
|
197
224
|
) : (
|
|
198
225
|
<div className="px-8 pb-8 space-y-6">
|
|
@@ -276,6 +303,63 @@ export function MetricsDashboard() {
|
|
|
276
303
|
</ChartCard>
|
|
277
304
|
</div>
|
|
278
305
|
|
|
306
|
+
{/* Task KPIs */}
|
|
307
|
+
{taskMetrics && (
|
|
308
|
+
<>
|
|
309
|
+
<h3 className="font-display text-[16px] font-700 text-text mt-2">Task Performance</h3>
|
|
310
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
311
|
+
<StatCard label="Tasks Completed" value={String(taskMetrics.completedCount)} />
|
|
312
|
+
<StatCard label="Avg Cycle Time" value={formatDuration(taskMetrics.avgCycleMs)} />
|
|
313
|
+
<StatCard label="WIP" value={String(taskMetrics.wip)} />
|
|
314
|
+
<StatCard label="Completion Rate" value={`${completionRate}%`} />
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
318
|
+
<ChartCard title="Task Velocity">
|
|
319
|
+
{taskMetrics.velocity.length > 0 ? (
|
|
320
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
321
|
+
<BarChart data={taskMetrics.velocity.map((v) => ({ ...v, label: formatBucketLabel(v.bucket, range) }))} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
|
|
322
|
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
|
|
323
|
+
<XAxis dataKey="label" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
|
|
324
|
+
<YAxis tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} allowDecimals={false} />
|
|
325
|
+
<Tooltip {...tooltipStyle} formatter={(value: number | undefined) => [value ?? 0, 'Completed']} />
|
|
326
|
+
<Bar dataKey="count" fill="#34D399" radius={[4, 4, 0, 0]} />
|
|
327
|
+
</BarChart>
|
|
328
|
+
</ResponsiveContainer>
|
|
329
|
+
) : (
|
|
330
|
+
<EmptyChart />
|
|
331
|
+
)}
|
|
332
|
+
</ChartCard>
|
|
333
|
+
|
|
334
|
+
<ChartCard title="Tasks by Agent">
|
|
335
|
+
{taskMetrics.byAgent.length > 0 ? (
|
|
336
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
337
|
+
<BarChart
|
|
338
|
+
data={taskMetrics.byAgent.slice(0, 8).map((a) => ({
|
|
339
|
+
name: a.agentName.length > 12 ? a.agentName.slice(0, 12) + '…' : a.agentName,
|
|
340
|
+
completed: a.completed,
|
|
341
|
+
failed: a.failed,
|
|
342
|
+
}))}
|
|
343
|
+
margin={{ top: 5, right: 20, bottom: 5, left: 0 }}
|
|
344
|
+
>
|
|
345
|
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
|
|
346
|
+
<XAxis dataKey="name" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
|
|
347
|
+
<YAxis tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} allowDecimals={false} />
|
|
348
|
+
<Tooltip {...tooltipStyle} />
|
|
349
|
+
<Bar dataKey="completed" fill="#34D399" radius={[4, 4, 0, 0]} stackId="a" name="Completed" />
|
|
350
|
+
<Bar dataKey="failed" fill="#F87171" radius={[4, 4, 0, 0]} stackId="a" name="Failed" />
|
|
351
|
+
<Legend verticalAlign="bottom" iconType="circle" iconSize={8}
|
|
352
|
+
formatter={(value: string) => <span style={{ color: '#a0a0b0', fontSize: 11 }}>{value}</span>} />
|
|
353
|
+
</BarChart>
|
|
354
|
+
</ResponsiveContainer>
|
|
355
|
+
) : (
|
|
356
|
+
<EmptyChart />
|
|
357
|
+
)}
|
|
358
|
+
</ChartCard>
|
|
359
|
+
</div>
|
|
360
|
+
</>
|
|
361
|
+
)}
|
|
362
|
+
|
|
279
363
|
{/* Provider Health */}
|
|
280
364
|
{data?.providerHealth && Object.keys(data.providerHealth).length > 0 && (
|
|
281
365
|
<div>
|
|
@@ -48,7 +48,7 @@ interface UseContinuousSpeechOptions {
|
|
|
48
48
|
|
|
49
49
|
export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
50
50
|
const { lang, silenceDelayMs = 800, onUtterance } = options
|
|
51
|
-
const [state,
|
|
51
|
+
const [state, _setState] = useState<ContinuousSpeechState>('idle')
|
|
52
52
|
const [transcript, setTranscript] = useState('')
|
|
53
53
|
const [interimText, setInterimText] = useState('')
|
|
54
54
|
|
|
@@ -56,6 +56,12 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
56
56
|
const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
57
57
|
const activeRef = useRef(false)
|
|
58
58
|
const accumulatedRef = useRef('')
|
|
59
|
+
const stateRef = useRef<ContinuousSpeechState>('idle')
|
|
60
|
+
|
|
61
|
+
const setState = useCallback((next: ContinuousSpeechState) => {
|
|
62
|
+
stateRef.current = next
|
|
63
|
+
_setState(next)
|
|
64
|
+
}, [])
|
|
59
65
|
|
|
60
66
|
const clearSilenceTimer = () => {
|
|
61
67
|
if (silenceTimerRef.current) {
|
|
@@ -122,7 +128,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
122
128
|
|
|
123
129
|
recog.onend = () => {
|
|
124
130
|
// Auto-restart if still active (browser may stop recognition periodically)
|
|
125
|
-
if (activeRef.current &&
|
|
131
|
+
if (activeRef.current && stateRef.current !== 'waitingForResponse') {
|
|
126
132
|
try { recog.start() } catch { /* noop */ }
|
|
127
133
|
}
|
|
128
134
|
}
|
|
@@ -156,7 +162,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
156
162
|
setTranscript('')
|
|
157
163
|
setInterimText('')
|
|
158
164
|
accumulatedRef.current = ''
|
|
159
|
-
}, [])
|
|
165
|
+
}, [setState])
|
|
160
166
|
|
|
161
167
|
const pause = useCallback(() => {
|
|
162
168
|
clearSilenceTimer()
|
|
@@ -172,7 +178,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
172
178
|
setInterimText('')
|
|
173
179
|
setState('listening')
|
|
174
180
|
startRecognition()
|
|
175
|
-
}, [startRecognition])
|
|
181
|
+
}, [startRecognition, setState])
|
|
176
182
|
|
|
177
183
|
const supported = typeof window !== 'undefined' &&
|
|
178
184
|
!!((window as unknown as WindowWithSpeechRecognition).SpeechRecognition || (window as unknown as WindowWithSpeechRecognition).webkitSpeechRecognition)
|