@swarmclawai/swarmclaw 0.5.2 → 0.6.0
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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback } from 'react'
|
|
4
4
|
import { TaskCard } from './task-card'
|
|
5
|
+
import { createTask } from '@/lib/tasks'
|
|
6
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
7
|
import type { BoardTask, BoardTaskStatus } from '@/types'
|
|
6
8
|
|
|
7
9
|
const COLUMN_CONFIG: Record<BoardTaskStatus, { label: string; color: string; dot: string }> = {
|
|
@@ -17,11 +19,18 @@ interface Props {
|
|
|
17
19
|
status: BoardTaskStatus
|
|
18
20
|
tasks: BoardTask[]
|
|
19
21
|
onDrop: (taskId: string, newStatus: BoardTaskStatus) => void
|
|
22
|
+
selectionMode?: boolean
|
|
23
|
+
selectedIds?: Set<string>
|
|
24
|
+
onToggleSelect?: (id: string) => void
|
|
25
|
+
onSelectAll?: () => void
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
export function TaskColumn({ status, tasks, onDrop }: Props) {
|
|
28
|
+
export function TaskColumn({ status, tasks, onDrop, selectionMode, selectedIds, onToggleSelect, onSelectAll }: Props) {
|
|
23
29
|
const config = COLUMN_CONFIG[status]
|
|
24
30
|
const [dragOver, setDragOver] = useState(false)
|
|
31
|
+
const [quickAddValue, setQuickAddValue] = useState('')
|
|
32
|
+
const [adding, setAdding] = useState(false)
|
|
33
|
+
const loadTasks = useAppStore((s) => s.loadTasks)
|
|
25
34
|
|
|
26
35
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
27
36
|
e.preventDefault()
|
|
@@ -42,6 +51,21 @@ export function TaskColumn({ status, tasks, onDrop }: Props) {
|
|
|
42
51
|
}
|
|
43
52
|
}, [onDrop, status])
|
|
44
53
|
|
|
54
|
+
const handleQuickAdd = async () => {
|
|
55
|
+
const title = quickAddValue.trim()
|
|
56
|
+
if (!title || adding) return
|
|
57
|
+
setAdding(true)
|
|
58
|
+
try {
|
|
59
|
+
await createTask({ title, description: '', agentId: '', status })
|
|
60
|
+
await loadTasks()
|
|
61
|
+
setQuickAddValue('')
|
|
62
|
+
} finally {
|
|
63
|
+
setAdding(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const selectedCount = tasks.filter((t) => selectedIds?.has(t.id)).length
|
|
68
|
+
|
|
45
69
|
return (
|
|
46
70
|
<div
|
|
47
71
|
className={`flex-1 min-w-[240px] max-w-[320px] flex flex-col rounded-[16px] transition-colors duration-150 ${
|
|
@@ -51,14 +75,49 @@ export function TaskColumn({ status, tasks, onDrop }: Props) {
|
|
|
51
75
|
onDragLeave={handleDragLeave}
|
|
52
76
|
onDrop={handleDrop}
|
|
53
77
|
>
|
|
54
|
-
<div className="flex items-center gap-2.5 px-2 mb-
|
|
78
|
+
<div className="flex items-center gap-2.5 px-2 mb-3">
|
|
55
79
|
<div className={`w-2 h-2 rounded-full ${config.dot}`} />
|
|
56
80
|
<span className={`font-display text-[13px] font-600 ${config.color}`}>{config.label}</span>
|
|
57
81
|
<span className="text-[12px] text-text-3 ml-auto">{tasks.length}</span>
|
|
82
|
+
{selectionMode && tasks.length > 0 && (
|
|
83
|
+
<button
|
|
84
|
+
onClick={onSelectAll}
|
|
85
|
+
className={`text-[10px] font-600 px-1.5 py-0.5 rounded-[5px] cursor-pointer border-none transition-colors
|
|
86
|
+
${selectedCount === tasks.length && selectedCount > 0
|
|
87
|
+
? 'bg-accent-bright/20 text-accent-bright'
|
|
88
|
+
: 'bg-white/[0.04] text-text-3 hover:bg-white/[0.08]'}`}
|
|
89
|
+
style={{ fontFamily: 'inherit' }}
|
|
90
|
+
>
|
|
91
|
+
{selectedCount === tasks.length && selectedCount > 0 ? 'All' : 'Select all'}
|
|
92
|
+
</button>
|
|
93
|
+
)}
|
|
58
94
|
</div>
|
|
95
|
+
|
|
96
|
+
{/* Quick add input */}
|
|
97
|
+
{(status === 'backlog' || status === 'queued') && (
|
|
98
|
+
<div className="px-1 mb-2">
|
|
99
|
+
<input
|
|
100
|
+
type="text"
|
|
101
|
+
value={quickAddValue}
|
|
102
|
+
onChange={(e) => setQuickAddValue(e.target.value)}
|
|
103
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleQuickAdd() }}
|
|
104
|
+
placeholder={`+ Add to ${config.label.toLowerCase()}...`}
|
|
105
|
+
className="w-full px-3 py-2 rounded-[10px] bg-white/[0.02] border border-dashed border-white/[0.08] text-[12px] text-text placeholder:text-text-3/30 outline-none focus:border-white/[0.15] focus:bg-white/[0.03] transition-colors"
|
|
106
|
+
style={{ fontFamily: 'inherit' }}
|
|
107
|
+
disabled={adding}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
59
112
|
<div className="flex flex-col gap-3 flex-1 overflow-y-auto pr-1 px-1 pb-2">
|
|
60
113
|
{tasks.map((task) => (
|
|
61
|
-
<TaskCard
|
|
114
|
+
<TaskCard
|
|
115
|
+
key={task.id}
|
|
116
|
+
task={task}
|
|
117
|
+
selectionMode={selectionMode}
|
|
118
|
+
selected={selectedIds?.has(task.id)}
|
|
119
|
+
onToggleSelect={onToggleSelect}
|
|
120
|
+
/>
|
|
62
121
|
))}
|
|
63
122
|
{tasks.length === 0 && (
|
|
64
123
|
<div className={`text-[12px] text-text-3/50 text-center py-8 rounded-[12px] border border-dashed transition-colors ${
|
|
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { useWs } from '@/hooks/use-ws'
|
|
6
6
|
import { api } from '@/lib/api-client'
|
|
7
7
|
import type { BoardTaskStatus } from '@/types'
|
|
8
|
+
import { EmptyState } from '@/components/shared/empty-state'
|
|
8
9
|
|
|
9
10
|
const STATUS_DOT: Record<BoardTaskStatus, string> = {
|
|
10
11
|
backlog: 'bg-white/20',
|
|
@@ -88,9 +89,16 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
88
89
|
)}
|
|
89
90
|
|
|
90
91
|
{filtered.length === 0 && (
|
|
91
|
-
<
|
|
92
|
-
{
|
|
93
|
-
|
|
92
|
+
<EmptyState
|
|
93
|
+
icon={
|
|
94
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" className="text-accent-bright">
|
|
95
|
+
<rect x="3" y="3" width="18" height="18" rx="2" fill="currentColor" opacity="0.2" />
|
|
96
|
+
<path d="M9 11l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
97
|
+
</svg>
|
|
98
|
+
}
|
|
99
|
+
title={sorted.length === 0 ? 'No tasks yet' : 'No matching tasks'}
|
|
100
|
+
subtitle={sorted.length === 0 ? 'Create tasks and assign agents to run them' : 'Try adjusting your search'}
|
|
101
|
+
/>
|
|
94
102
|
)}
|
|
95
103
|
{filtered.map((task) => {
|
|
96
104
|
const agent = agents[task.agentId]
|
|
@@ -101,7 +109,7 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
101
109
|
setEditingTaskId(task.id)
|
|
102
110
|
setTaskSheetOpen(true)
|
|
103
111
|
}}
|
|
104
|
-
className="w-full text-left
|
|
112
|
+
className="w-full text-left py-3.5 px-4 rounded-[14px] border border-transparent bg-transparent cursor-pointer hover:bg-white/[0.03] transition-all"
|
|
105
113
|
style={{ fontFamily: 'inherit' }}
|
|
106
114
|
>
|
|
107
115
|
<div className="flex items-center gap-2.5">
|
|
@@ -4,8 +4,12 @@ import { useEffect, useState } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { createTask, updateTask, archiveTask, unarchiveTask } from '@/lib/tasks'
|
|
6
6
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
|
+
import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
7
8
|
import { DirBrowser } from '@/components/shared/dir-browser'
|
|
9
|
+
import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
10
|
+
import { inputClass } from '@/components/shared/form-styles'
|
|
8
11
|
import type { BoardTask, TaskComment } from '@/types'
|
|
12
|
+
import { SectionLabel } from '@/components/shared/section-label'
|
|
9
13
|
|
|
10
14
|
function fmtTime(ts: number) {
|
|
11
15
|
const d = new Date(ts)
|
|
@@ -25,6 +29,10 @@ export function TaskSheet() {
|
|
|
25
29
|
const agents = useAppStore((s) => s.agents)
|
|
26
30
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
27
31
|
|
|
32
|
+
const projects = useAppStore((s) => s.projects)
|
|
33
|
+
const loadProjects = useAppStore((s) => s.loadProjects)
|
|
34
|
+
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
35
|
+
|
|
28
36
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
29
37
|
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
30
38
|
|
|
@@ -36,6 +44,7 @@ export function TaskSheet() {
|
|
|
36
44
|
const [uploading, setUploading] = useState(false)
|
|
37
45
|
const [cwd, setCwd] = useState('')
|
|
38
46
|
const [file, setFile] = useState<string | null>(null)
|
|
47
|
+
const [projectId, setProjectId] = useState('')
|
|
39
48
|
const [tags, setTags] = useState<string[]>([])
|
|
40
49
|
const [tagInput, setTagInput] = useState('')
|
|
41
50
|
const [blockedBy, setBlockedBy] = useState<string[]>([])
|
|
@@ -43,16 +52,18 @@ export function TaskSheet() {
|
|
|
43
52
|
const [customFields, setCustomFields] = useState<Record<string, string | number | boolean>>({})
|
|
44
53
|
|
|
45
54
|
const editing = editingId ? tasks[editingId] : null
|
|
46
|
-
const agentList = Object.values(agents)
|
|
55
|
+
const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
|
|
47
56
|
|
|
48
57
|
useEffect(() => {
|
|
49
58
|
if (open) {
|
|
50
59
|
loadAgents()
|
|
60
|
+
loadProjects()
|
|
51
61
|
loadSettings()
|
|
52
62
|
if (editing) {
|
|
53
63
|
setTitle(editing.title)
|
|
54
64
|
setDescription(editing.description)
|
|
55
65
|
setAgentId(editing.agentId)
|
|
66
|
+
setProjectId(editing.projectId || '')
|
|
56
67
|
setImages(editing.images || [])
|
|
57
68
|
setCwd(editing.cwd || '')
|
|
58
69
|
setFile(editing.file || null)
|
|
@@ -64,6 +75,7 @@ export function TaskSheet() {
|
|
|
64
75
|
setTitle('')
|
|
65
76
|
setDescription('')
|
|
66
77
|
setAgentId(agentList[0]?.id || '')
|
|
78
|
+
setProjectId(activeProjectFilter || '')
|
|
67
79
|
setImages([])
|
|
68
80
|
setCwd('')
|
|
69
81
|
setFile(null)
|
|
@@ -89,12 +101,14 @@ export function TaskSheet() {
|
|
|
89
101
|
}
|
|
90
102
|
|
|
91
103
|
const handleSave = async () => {
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
// projectId uses null (not undefined) so the API can distinguish "clear" from "not sent"
|
|
105
|
+
// projectId uses null (not undefined) so the API can distinguish "clear" from "not sent"
|
|
106
|
+
const payload = {
|
|
107
|
+
title: title.trim() || 'Untitled Task', description, agentId, projectId: projectId || null, images,
|
|
94
108
|
cwd: cwd || undefined, file: file || undefined,
|
|
95
109
|
tags, blockedBy, dueAt: dueAt ? new Date(dueAt).getTime() : null,
|
|
96
110
|
customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
|
|
97
|
-
}
|
|
111
|
+
} as Partial<BoardTask> & { title: string; description: string; agentId: string }
|
|
98
112
|
if (editing) {
|
|
99
113
|
await updateTask(editing.id, payload)
|
|
100
114
|
} else {
|
|
@@ -159,8 +173,6 @@ export function TaskSheet() {
|
|
|
159
173
|
setCommentText('')
|
|
160
174
|
}
|
|
161
175
|
|
|
162
|
-
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
163
|
-
|
|
164
176
|
return (
|
|
165
177
|
<BottomSheet open={open} onClose={onClose}>
|
|
166
178
|
<div className="mb-10">
|
|
@@ -173,7 +185,7 @@ export function TaskSheet() {
|
|
|
173
185
|
</div>
|
|
174
186
|
|
|
175
187
|
<div className="mb-8">
|
|
176
|
-
<
|
|
188
|
+
<SectionLabel>Title</SectionLabel>
|
|
177
189
|
<input
|
|
178
190
|
type="text"
|
|
179
191
|
value={title}
|
|
@@ -185,7 +197,7 @@ export function TaskSheet() {
|
|
|
185
197
|
</div>
|
|
186
198
|
|
|
187
199
|
<div className="mb-8">
|
|
188
|
-
<
|
|
200
|
+
<SectionLabel>Description</SectionLabel>
|
|
189
201
|
<textarea
|
|
190
202
|
value={description}
|
|
191
203
|
onChange={(e) => setDescription(e.target.value)}
|
|
@@ -198,9 +210,7 @@ export function TaskSheet() {
|
|
|
198
210
|
|
|
199
211
|
{/* Images */}
|
|
200
212
|
<div className="mb-8">
|
|
201
|
-
<
|
|
202
|
-
Images <span className="normal-case tracking-normal font-normal text-text-3">(optional — reference designs, mockups, etc.)</span>
|
|
203
|
-
</label>
|
|
213
|
+
<SectionLabel>Images <span className="normal-case tracking-normal font-normal text-text-3">(optional — reference designs, mockups, etc.)</span></SectionLabel>
|
|
204
214
|
{images.length > 0 && (
|
|
205
215
|
<div className="flex gap-2 flex-wrap mb-3">
|
|
206
216
|
{images.map((url, i) => (
|
|
@@ -229,33 +239,48 @@ export function TaskSheet() {
|
|
|
229
239
|
</div>
|
|
230
240
|
|
|
231
241
|
<div className="mb-8">
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
242
|
+
<SectionLabel>Agent</SectionLabel>
|
|
243
|
+
<AgentPickerList
|
|
244
|
+
agents={agentList}
|
|
245
|
+
selected={agentId}
|
|
246
|
+
onSelect={(id) => setAgentId(id)}
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Project (optional) */}
|
|
251
|
+
<div className="mb-8">
|
|
252
|
+
<SectionLabel>Project <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
|
|
253
|
+
<div className="flex flex-wrap gap-2">
|
|
254
|
+
<button
|
|
255
|
+
onClick={() => setProjectId('')}
|
|
256
|
+
className={`px-4 py-3 rounded-[12px] text-[14px] font-600 cursor-pointer transition-all border
|
|
257
|
+
${!projectId
|
|
258
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
259
|
+
: 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
|
|
260
|
+
style={{ fontFamily: 'inherit' }}
|
|
261
|
+
>
|
|
262
|
+
None
|
|
263
|
+
</button>
|
|
264
|
+
{Object.values(projects).map((p) => (
|
|
265
|
+
<button
|
|
266
|
+
key={p.id}
|
|
267
|
+
onClick={() => setProjectId(p.id)}
|
|
268
|
+
className={`px-4 py-3 rounded-[12px] text-[14px] font-600 cursor-pointer transition-all border flex items-center gap-2
|
|
269
|
+
${projectId === p.id
|
|
270
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
271
|
+
: 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
|
|
272
|
+
style={{ fontFamily: 'inherit' }}
|
|
273
|
+
>
|
|
274
|
+
<span className="w-2.5 h-2.5 rounded-full shrink-0" style={{ backgroundColor: p.color || '#6366F1' }} />
|
|
275
|
+
{p.name}
|
|
276
|
+
</button>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
252
279
|
</div>
|
|
253
280
|
|
|
254
281
|
{/* Directory (optional) */}
|
|
255
282
|
<div className="mb-8">
|
|
256
|
-
<
|
|
257
|
-
Directory <span className="normal-case tracking-normal font-normal text-text-3">(optional — project to work in)</span>
|
|
258
|
-
</label>
|
|
283
|
+
<SectionLabel>Directory <span className="normal-case tracking-normal font-normal text-text-3">(optional — project to work in)</span></SectionLabel>
|
|
259
284
|
<DirBrowser
|
|
260
285
|
value={cwd || null}
|
|
261
286
|
file={file}
|
|
@@ -273,9 +298,7 @@ export function TaskSheet() {
|
|
|
273
298
|
|
|
274
299
|
{/* Tags */}
|
|
275
300
|
<div className="mb-8">
|
|
276
|
-
<
|
|
277
|
-
Tags <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
278
|
-
</label>
|
|
301
|
+
<SectionLabel>Tags <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
|
|
279
302
|
{tags.length > 0 && (
|
|
280
303
|
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
281
304
|
{tags.map((tag) => (
|
|
@@ -315,9 +338,7 @@ export function TaskSheet() {
|
|
|
315
338
|
|
|
316
339
|
{/* Dependencies */}
|
|
317
340
|
<div className="mb-8">
|
|
318
|
-
<
|
|
319
|
-
Blocked By <span className="normal-case tracking-normal font-normal text-text-3">(tasks that must complete first)</span>
|
|
320
|
-
</label>
|
|
341
|
+
<SectionLabel>Blocked By <span className="normal-case tracking-normal font-normal text-text-3">(tasks that must complete first)</span></SectionLabel>
|
|
321
342
|
<select
|
|
322
343
|
multiple
|
|
323
344
|
value={blockedBy}
|
|
@@ -346,9 +367,7 @@ export function TaskSheet() {
|
|
|
346
367
|
|
|
347
368
|
{/* Due Date */}
|
|
348
369
|
<div className="mb-8">
|
|
349
|
-
<
|
|
350
|
-
Due Date <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
351
|
-
</label>
|
|
370
|
+
<SectionLabel>Due Date <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span></SectionLabel>
|
|
352
371
|
<input
|
|
353
372
|
type="date"
|
|
354
373
|
value={dueAt}
|
|
@@ -361,7 +380,7 @@ export function TaskSheet() {
|
|
|
361
380
|
{/* Custom Fields */}
|
|
362
381
|
{appSettings.taskCustomFieldDefs && appSettings.taskCustomFieldDefs.length > 0 && (
|
|
363
382
|
<div className="mb-8">
|
|
364
|
-
<
|
|
383
|
+
<SectionLabel>Custom Fields</SectionLabel>
|
|
365
384
|
<div className="space-y-4">
|
|
366
385
|
{appSettings.taskCustomFieldDefs.map((def) => (
|
|
367
386
|
<div key={def.key}>
|
|
@@ -396,7 +415,7 @@ export function TaskSheet() {
|
|
|
396
415
|
|
|
397
416
|
{editing?.result && (
|
|
398
417
|
<div className="mb-8">
|
|
399
|
-
<
|
|
418
|
+
<SectionLabel>Result</SectionLabel>
|
|
400
419
|
<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">
|
|
401
420
|
{editing.result}
|
|
402
421
|
</div>
|
|
@@ -405,7 +424,7 @@ export function TaskSheet() {
|
|
|
405
424
|
|
|
406
425
|
{editing && (editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.cliResumeId) && (
|
|
407
426
|
<div className="mb-8">
|
|
408
|
-
<
|
|
427
|
+
<SectionLabel>CLI Sessions</SectionLabel>
|
|
409
428
|
<div className="flex flex-wrap gap-2">
|
|
410
429
|
{editing.claudeResumeId && (
|
|
411
430
|
<div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
|
|
@@ -447,9 +466,7 @@ export function TaskSheet() {
|
|
|
447
466
|
{/* Comments */}
|
|
448
467
|
{editing && (
|
|
449
468
|
<div className="mb-8">
|
|
450
|
-
<
|
|
451
|
-
Comments {editing.comments?.length ? `(${editing.comments.length})` : ''}
|
|
452
|
-
</label>
|
|
469
|
+
<SectionLabel>Comments {editing.comments?.length ? `(${editing.comments.length})` : ''}</SectionLabel>
|
|
453
470
|
|
|
454
471
|
{editing.comments && editing.comments.length > 0 && (
|
|
455
472
|
<div className="space-y-3 mb-4 max-h-[300px] overflow-y-auto">
|
|
@@ -489,29 +506,29 @@ export function TaskSheet() {
|
|
|
489
506
|
</div>
|
|
490
507
|
)}
|
|
491
508
|
|
|
492
|
-
<
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
509
|
+
<SheetFooter
|
|
510
|
+
onCancel={onClose}
|
|
511
|
+
onSave={handleSave}
|
|
512
|
+
saveLabel={editing ? 'Save' : 'Create'}
|
|
513
|
+
saveDisabled={!title.trim() || !agentId}
|
|
514
|
+
left={<>
|
|
515
|
+
{editing && editing.status !== 'archived' && (
|
|
516
|
+
<button onClick={handleArchive} className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-3 text-[15px] font-600 cursor-pointer hover:bg-white/[0.04] transition-all" style={{ fontFamily: 'inherit' }}>
|
|
517
|
+
Archive
|
|
518
|
+
</button>
|
|
519
|
+
)}
|
|
520
|
+
{editing && editing.status === 'archived' && (
|
|
521
|
+
<button onClick={handleUnarchive} className="py-3.5 px-6 rounded-[14px] border border-accent-bright/20 bg-transparent text-accent-bright text-[15px] font-600 cursor-pointer hover:bg-accent-bright/10 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
522
|
+
Unarchive
|
|
523
|
+
</button>
|
|
524
|
+
)}
|
|
525
|
+
{editing && editing.status === 'backlog' && (
|
|
526
|
+
<button onClick={handleQueue} className="py-3.5 px-6 rounded-[14px] border border-amber-500/20 bg-transparent text-amber-400 text-[15px] font-600 cursor-pointer hover:bg-amber-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
527
|
+
Queue
|
|
528
|
+
</button>
|
|
529
|
+
)}
|
|
530
|
+
</>}
|
|
531
|
+
/>
|
|
515
532
|
</BottomSheet>
|
|
516
533
|
)
|
|
517
534
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
function HoverCard({
|
|
9
|
+
openDelay = 300,
|
|
10
|
+
closeDelay = 150,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
13
|
+
return (
|
|
14
|
+
<HoverCardPrimitive.Root
|
|
15
|
+
data-slot="hover-card"
|
|
16
|
+
openDelay={openDelay}
|
|
17
|
+
closeDelay={closeDelay}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function HoverCardTrigger({
|
|
24
|
+
...props
|
|
25
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
26
|
+
return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function HoverCardContent({
|
|
30
|
+
className,
|
|
31
|
+
sideOffset = 8,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
35
|
+
return (
|
|
36
|
+
<HoverCardPrimitive.Portal>
|
|
37
|
+
<HoverCardPrimitive.Content
|
|
38
|
+
data-slot="hover-card-content"
|
|
39
|
+
sideOffset={sideOffset}
|
|
40
|
+
className={cn(
|
|
41
|
+
"rounded-[12px] w-[260px] z-50 p-3 border border-white/[0.08] shadow-xl backdrop-blur-xl bg-[rgba(16,16,28,0.95)] animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin)",
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</HoverCardPrimitive.Content>
|
|
48
|
+
</HoverCardPrimitive.Portal>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
@@ -18,6 +18,16 @@ interface TimePoint {
|
|
|
18
18
|
cost: number
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
interface ProviderHealthEntry {
|
|
22
|
+
totalRequests: number
|
|
23
|
+
successCount: number
|
|
24
|
+
errorCount: number
|
|
25
|
+
errorRate: number
|
|
26
|
+
avgLatencyMs: number
|
|
27
|
+
lastUsed: number
|
|
28
|
+
models: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
interface UsageResponse {
|
|
22
32
|
records: unknown[]
|
|
23
33
|
totalTokens: number
|
|
@@ -25,6 +35,7 @@ interface UsageResponse {
|
|
|
25
35
|
byAgent: Record<string, { tokens: number; cost: number }>
|
|
26
36
|
byProvider: Record<string, { tokens: number; cost: number }>
|
|
27
37
|
timeSeries: TimePoint[]
|
|
38
|
+
providerHealth?: Record<string, ProviderHealthEntry>
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
const RANGES: Range[] = ['24h', '7d', '30d']
|
|
@@ -62,6 +73,25 @@ function formatBucketLabel(bucket: string, range: Range): string {
|
|
|
62
73
|
return bucket
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
function formatRelativeTime(ts: number): string {
|
|
77
|
+
if (!ts) return 'Never'
|
|
78
|
+
const diff = Date.now() - ts
|
|
79
|
+
const seconds = Math.floor(diff / 1000)
|
|
80
|
+
if (seconds < 60) return 'Just now'
|
|
81
|
+
const minutes = Math.floor(seconds / 60)
|
|
82
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
83
|
+
const hours = Math.floor(minutes / 60)
|
|
84
|
+
if (hours < 24) return `${hours}h ago`
|
|
85
|
+
const days = Math.floor(hours / 24)
|
|
86
|
+
return `${days}d ago`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function errorRateColor(rate: number): string {
|
|
90
|
+
if (rate < 0.05) return 'text-emerald-400'
|
|
91
|
+
if (rate < 0.2) return 'text-amber-400'
|
|
92
|
+
return 'text-red-400'
|
|
93
|
+
}
|
|
94
|
+
|
|
65
95
|
function computeCompletionRate(tasks: Record<string, BoardTask>): number {
|
|
66
96
|
const all = Object.values(tasks)
|
|
67
97
|
const eligible = all.filter((t) => t.status !== 'backlog' && t.status !== 'archived')
|
|
@@ -245,6 +275,54 @@ export function MetricsDashboard() {
|
|
|
245
275
|
)}
|
|
246
276
|
</ChartCard>
|
|
247
277
|
</div>
|
|
278
|
+
|
|
279
|
+
{/* Provider Health */}
|
|
280
|
+
{data?.providerHealth && Object.keys(data.providerHealth).length > 0 && (
|
|
281
|
+
<div>
|
|
282
|
+
<h3 className="font-display text-[14px] font-600 text-text-2 mb-3">Provider Health</h3>
|
|
283
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
284
|
+
{Object.entries(data.providerHealth)
|
|
285
|
+
.sort(([, a], [, b]) => b.totalRequests - a.totalRequests)
|
|
286
|
+
.map(([name, h]) => (
|
|
287
|
+
<div
|
|
288
|
+
key={name}
|
|
289
|
+
className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] flex flex-col gap-3"
|
|
290
|
+
>
|
|
291
|
+
<div className="flex items-center justify-between">
|
|
292
|
+
<p className="text-[14px] font-600 text-text">{name}</p>
|
|
293
|
+
<span className="text-[11px] text-text-3">{formatRelativeTime(h.lastUsed)}</span>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[12px]">
|
|
296
|
+
<span className="text-text-3">Requests</span>
|
|
297
|
+
<span className="text-text font-500 text-right">{h.totalRequests}</span>
|
|
298
|
+
<span className="text-text-3">Error Rate</span>
|
|
299
|
+
<span className={`font-500 text-right ${errorRateColor(h.errorRate)}`}>
|
|
300
|
+
{(h.errorRate * 100).toFixed(1)}%
|
|
301
|
+
</span>
|
|
302
|
+
{h.avgLatencyMs > 0 && (
|
|
303
|
+
<>
|
|
304
|
+
<span className="text-text-3">Avg Latency</span>
|
|
305
|
+
<span className="text-text font-500 text-right">{Math.round(h.avgLatencyMs)}ms</span>
|
|
306
|
+
</>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
{h.models.length > 0 && (
|
|
310
|
+
<div className="flex flex-wrap gap-1.5 pt-1">
|
|
311
|
+
{h.models.map((m) => (
|
|
312
|
+
<span
|
|
313
|
+
key={m}
|
|
314
|
+
className="px-2 py-0.5 rounded-[6px] bg-white/[0.06] text-[11px] text-text-3 font-500"
|
|
315
|
+
>
|
|
316
|
+
{m}
|
|
317
|
+
</span>
|
|
318
|
+
))}
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
))}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
248
326
|
</div>
|
|
249
327
|
)}
|
|
250
328
|
</div>
|
|
@@ -98,7 +98,7 @@ export function UsageList() {
|
|
|
98
98
|
</div>
|
|
99
99
|
<div className="mt-2 h-1 rounded-full bg-white/[0.04] overflow-hidden">
|
|
100
100
|
<div
|
|
101
|
-
className="h-full rounded-full bg-
|
|
101
|
+
className="h-full rounded-full bg-accent-bright/60"
|
|
102
102
|
style={{ width: `${Math.max(pct, 1)}%` }}
|
|
103
103
|
/>
|
|
104
104
|
</div>
|
|
@@ -389,7 +389,7 @@ export function WebhookSheet() {
|
|
|
389
389
|
<button
|
|
390
390
|
onClick={handleSave}
|
|
391
391
|
disabled={saving}
|
|
392
|
-
className="px-8 py-3 rounded-[14px] border-none bg-
|
|
392
|
+
className="px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
393
393
|
style={{ fontFamily: 'inherit' }}
|
|
394
394
|
>
|
|
395
395
|
{saving ? 'Saving...' : editing ? 'Update' : 'Create'}
|