@swarmclawai/swarmclaw 0.6.7 → 0.7.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.
Files changed (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState, useCallback } from 'react'
3
+ import { useState, useCallback, useEffect } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
6
  import { updateTask, archiveTask } from '@/lib/tasks'
@@ -20,9 +20,10 @@ interface TaskCardProps {
20
20
  selectionMode?: boolean
21
21
  selected?: boolean
22
22
  onToggleSelect?: (id: string) => void
23
+ index?: number
23
24
  }
24
25
 
25
- export function TaskCard({ task, selectionMode, selected, onToggleSelect }: TaskCardProps) {
26
+ export function TaskCard({ task, selectionMode, selected, onToggleSelect, index = 0 }: TaskCardProps) {
26
27
  const agents = useAppStore((s) => s.agents)
27
28
  const projects = useAppStore((s) => s.projects)
28
29
  const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
@@ -32,6 +33,15 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
32
33
  const setActiveView = useAppStore((s) => s.setActiveView)
33
34
  const [dragging, setDragging] = useState(false)
34
35
  const [confirmArchive, setConfirmArchive] = useState(false)
36
+ const [allowDrag, setAllowDrag] = useState(false)
37
+
38
+ useEffect(() => {
39
+ if (typeof window === 'undefined') return
40
+ const isCoarsePointer = typeof window.matchMedia === 'function'
41
+ ? window.matchMedia('(pointer: coarse)').matches
42
+ : 'ontouchstart' in window
43
+ setAllowDrag(!isCoarsePointer)
44
+ }, [])
35
45
 
36
46
  const tasks = useAppStore((s) => s.tasks)
37
47
  const agent = agents[task.agentId]
@@ -85,9 +95,9 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
85
95
 
86
96
  return (
87
97
  <div
88
- draggable={!selectionMode}
89
- onDragStart={selectionMode ? undefined : handleDragStart}
90
- onDragEnd={selectionMode ? undefined : handleDragEnd}
98
+ draggable={!selectionMode && allowDrag}
99
+ onDragStart={selectionMode || !allowDrag ? undefined : handleDragStart}
100
+ onDragEnd={selectionMode || !allowDrag ? undefined : handleDragEnd}
91
101
  onClick={(e) => {
92
102
  if (selectionMode && onToggleSelect) {
93
103
  e.stopPropagation()
@@ -98,9 +108,13 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
98
108
  }
99
109
  }}
100
110
  className={`py-3 px-4 rounded-[14px] border border-l-[3px] ${borderColor} bg-surface hover:bg-surface-2 transition-all group
101
- ${selectionMode ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing'}
111
+ ${selectionMode || !allowDrag ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing'} touch-pan-y
102
112
  ${dragging ? 'opacity-40 scale-[0.97]' : ''}
103
- ${selected ? 'border-accent-bright/40 bg-accent-bright/[0.04] ring-1 ring-accent-bright/20' : 'border-white/[0.06]'}`}
113
+ ${selected ? 'border-accent-bright/40 bg-accent-bright/[0.04] ring-1 ring-accent-bright/20 shadow-lg' : 'border-white/[0.06] hover:border-white/[0.12] hover:scale-[1.01] hover:shadow-md'}`}
114
+ style={{
115
+ animation: 'spring-in 0.5s var(--ease-spring) both',
116
+ animationDelay: `${Math.min(index * 0.05, 0.4)}s`
117
+ }}
104
118
  >
105
119
  <div className="flex items-start gap-3 mb-3">
106
120
  {/* Selection checkbox */}
@@ -68,7 +68,7 @@ export function TaskColumn({ status, tasks, onDrop, selectionMode, selectedIds,
68
68
 
69
69
  return (
70
70
  <div
71
- className={`flex-1 min-w-[240px] max-w-[320px] flex flex-col rounded-[16px] transition-colors duration-150 ${
71
+ className={`flex-1 min-w-[240px] max-w-[320px] min-h-0 flex flex-col rounded-[16px] transition-colors duration-150 ${
72
72
  dragOver ? 'bg-accent-bright/[0.04] ring-1 ring-accent-bright/20' : ''
73
73
  }`}
74
74
  onDragOver={handleDragOver}
@@ -109,11 +109,12 @@ export function TaskColumn({ status, tasks, onDrop, selectionMode, selectedIds,
109
109
  </div>
110
110
  )}
111
111
 
112
- <div className="flex flex-col gap-3 flex-1 overflow-y-auto pr-1 px-1 pb-2">
113
- {tasks.map((task) => (
112
+ <div className="flex flex-col gap-3 flex-1 min-h-0 overflow-y-auto overscroll-y-contain touch-pan-y pr-1 px-1 pb-2">
113
+ {tasks.map((task, idx) => (
114
114
  <TaskCard
115
115
  key={task.id}
116
116
  task={task}
117
+ index={idx}
117
118
  selectionMode={selectionMode}
118
119
  selected={selectedIds?.has(task.id)}
119
120
  onToggleSelect={onToggleSelect}
@@ -62,7 +62,7 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
62
62
  }
63
63
 
64
64
  return (
65
- <div className="flex-1 flex flex-col overflow-y-auto">
65
+ <div className="flex-1 min-h-0 flex flex-col overflow-y-auto overscroll-y-contain touch-pan-y">
66
66
  {/* Search + clear */}
67
67
  {sorted.length > 0 && (
68
68
  <div className="px-3 py-2 shrink-0 flex flex-col gap-2">
@@ -10,7 +10,7 @@ import { AgentPickerList } from '@/components/shared/agent-picker-list'
10
10
  import { DirBrowser } from '@/components/shared/dir-browser'
11
11
  import { SheetFooter } from '@/components/shared/sheet-footer'
12
12
  import { inputClass } from '@/components/shared/form-styles'
13
- import type { BoardTask, TaskComment } from '@/types'
13
+ import type { BoardTask, TaskComment, TaskQualityGateConfig } from '@/types'
14
14
  import { SectionLabel } from '@/components/shared/section-label'
15
15
  import { AgentAvatar } from '@/components/agents/agent-avatar'
16
16
 
@@ -22,6 +22,16 @@ function fmtTime(ts: number) {
22
22
  return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
23
23
  }
24
24
 
25
+ function normalizeGateNumber(value: unknown, fallback: number, min: number, max: number): number {
26
+ const parsed = typeof value === 'number'
27
+ ? value
28
+ : typeof value === 'string'
29
+ ? Number.parseInt(value, 10)
30
+ : Number.NaN
31
+ if (!Number.isFinite(parsed)) return fallback
32
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
33
+ }
34
+
25
35
  export function TaskSheet() {
26
36
  const open = useAppStore((s) => s.taskSheetOpen)
27
37
  const setOpen = useAppStore((s) => s.setTaskSheetOpen)
@@ -59,6 +69,12 @@ export function TaskSheet() {
59
69
  const [dueAt, setDueAt] = useState<string>('')
60
70
  const [customFields, setCustomFields] = useState<Record<string, string | number | boolean>>({})
61
71
  const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'critical' | ''>('')
72
+ const [qualityGateEnabled, setQualityGateEnabled] = useState(true)
73
+ const [qualityGateMinResultChars, setQualityGateMinResultChars] = useState(80)
74
+ const [qualityGateMinEvidenceItems, setQualityGateMinEvidenceItems] = useState(2)
75
+ const [qualityGateRequireVerification, setQualityGateRequireVerification] = useState(false)
76
+ const [qualityGateRequireArtifact, setQualityGateRequireArtifact] = useState(false)
77
+ const [qualityGateRequireReport, setQualityGateRequireReport] = useState(false)
62
78
 
63
79
  const editing = editingId ? tasks[editingId] : null
64
80
  const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
@@ -68,6 +84,12 @@ export function TaskSheet() {
68
84
  loadAgents()
69
85
  loadProjects()
70
86
  loadSettings()
87
+ const defaultGateEnabled = appSettings.taskQualityGateEnabled ?? true
88
+ const defaultGateMinResult = normalizeGateNumber(appSettings.taskQualityGateMinResultChars, 80, 10, 2000)
89
+ const defaultGateMinEvidence = normalizeGateNumber(appSettings.taskQualityGateMinEvidenceItems, 2, 0, 8)
90
+ const defaultGateRequireVerification = appSettings.taskQualityGateRequireVerification ?? false
91
+ const defaultGateRequireArtifact = appSettings.taskQualityGateRequireArtifact ?? false
92
+ const defaultGateRequireReport = appSettings.taskQualityGateRequireReport ?? false
71
93
  if (editing) {
72
94
  setTitle(editing.title)
73
95
  setDescription(editing.description)
@@ -83,6 +105,13 @@ export function TaskSheet() {
83
105
  setDueAt(editing.dueAt ? new Date(editing.dueAt).toISOString().slice(0, 10) : '')
84
106
  setCustomFields(editing.customFields || {})
85
107
  setPriority(editing.priority || '')
108
+ const gate = (editing.qualityGate || null) as TaskQualityGateConfig | null
109
+ setQualityGateEnabled(gate?.enabled ?? defaultGateEnabled)
110
+ setQualityGateMinResultChars(normalizeGateNumber(gate?.minResultChars, defaultGateMinResult, 10, 2000))
111
+ setQualityGateMinEvidenceItems(normalizeGateNumber(gate?.minEvidenceItems, defaultGateMinEvidence, 0, 8))
112
+ setQualityGateRequireVerification(gate?.requireVerification ?? defaultGateRequireVerification)
113
+ setQualityGateRequireArtifact(gate?.requireArtifact ?? defaultGateRequireArtifact)
114
+ setQualityGateRequireReport(gate?.requireReport ?? defaultGateRequireReport)
86
115
  } else {
87
116
  setTitle('')
88
117
  setDescription('')
@@ -98,6 +127,12 @@ export function TaskSheet() {
98
127
  setDueAt('')
99
128
  setCustomFields({})
100
129
  setPriority('')
130
+ setQualityGateEnabled(defaultGateEnabled)
131
+ setQualityGateMinResultChars(defaultGateMinResult)
132
+ setQualityGateMinEvidenceItems(defaultGateMinEvidence)
133
+ setQualityGateRequireVerification(defaultGateRequireVerification)
134
+ setQualityGateRequireArtifact(defaultGateRequireArtifact)
135
+ setQualityGateRequireReport(defaultGateRequireReport)
101
136
  }
102
137
  }
103
138
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -116,6 +151,17 @@ export function TaskSheet() {
116
151
  }
117
152
 
118
153
  const handleSave = async () => {
154
+ const qualityGate: TaskQualityGateConfig | null = qualityGateEnabled
155
+ ? {
156
+ enabled: true,
157
+ minResultChars: qualityGateMinResultChars,
158
+ minEvidenceItems: qualityGateMinEvidenceItems,
159
+ requireVerification: qualityGateRequireVerification,
160
+ requireArtifact: qualityGateRequireArtifact,
161
+ requireReport: qualityGateRequireReport,
162
+ }
163
+ : null
164
+
119
165
  // projectId uses null (not undefined) so the API can distinguish "clear" from "not sent"
120
166
  // projectId uses null (not undefined) so the API can distinguish "clear" from "not sent"
121
167
  const payload = {
@@ -124,6 +170,7 @@ export function TaskSheet() {
124
170
  tags, blockedBy, dueAt: dueAt ? new Date(dueAt).getTime() : null,
125
171
  customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
126
172
  priority: priority || undefined,
173
+ qualityGate,
127
174
  } as Partial<BoardTask> & { title: string; description: string; agentId: string }
128
175
  try {
129
176
  if (editing) {
@@ -363,6 +410,19 @@ export function TaskSheet() {
363
410
  </div>
364
411
  )}
365
412
 
413
+ {editing.qualityGate?.enabled && (
414
+ <div className="mb-8">
415
+ <SectionLabel>Quality Gate</SectionLabel>
416
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface space-y-1.5 text-[12px] text-text-2">
417
+ <p>Min result chars: {editing.qualityGate.minResultChars ?? 80}</p>
418
+ <p>Min evidence signals: {editing.qualityGate.minEvidenceItems ?? 2}</p>
419
+ <p>Verification required: {(editing.qualityGate.requireVerification ?? false) ? 'Yes' : 'No'}</p>
420
+ <p>Artifact required: {(editing.qualityGate.requireArtifact ?? false) ? 'Yes' : 'No'}</p>
421
+ <p>Task report required: {(editing.qualityGate.requireReport ?? false) ? 'Yes' : 'No'}</p>
422
+ </div>
423
+ </div>
424
+ )}
425
+
366
426
  {/* Images (thumbnails only, no remove/upload) */}
367
427
  {editing.images && editing.images.length > 0 && (
368
428
  <div className="mb-8">
@@ -797,6 +857,75 @@ export function TaskSheet() {
797
857
  />
798
858
  </div>
799
859
 
860
+ <div className="mb-8">
861
+ <SectionLabel>Quality Gate</SectionLabel>
862
+ <p className="text-[12px] text-text-3 mb-3">
863
+ Checks that must pass before this task can be marked completed.
864
+ </p>
865
+ <div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface">
866
+ <button
867
+ onClick={() => setQualityGateEnabled((prev) => !prev)}
868
+ className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${qualityGateEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
869
+ >
870
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${qualityGateEnabled ? 'translate-x-[18px]' : ''}`} />
871
+ </button>
872
+ <span className="ml-2 text-[12px] text-text-2">{qualityGateEnabled ? 'Enabled' : 'Disabled'}</span>
873
+
874
+ {qualityGateEnabled && (
875
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4">
876
+ <div>
877
+ <label className="block text-[11px] text-text-3 mb-1.5">Min Result Chars</label>
878
+ <input
879
+ type="number"
880
+ min={10}
881
+ max={2000}
882
+ value={qualityGateMinResultChars}
883
+ onChange={(e) => setQualityGateMinResultChars(normalizeGateNumber(e.target.value, 80, 10, 2000))}
884
+ className={inputClass}
885
+ style={{ fontFamily: 'inherit' }}
886
+ />
887
+ </div>
888
+ <div>
889
+ <label className="block text-[11px] text-text-3 mb-1.5">Min Evidence Signals</label>
890
+ <input
891
+ type="number"
892
+ min={0}
893
+ max={8}
894
+ value={qualityGateMinEvidenceItems}
895
+ onChange={(e) => setQualityGateMinEvidenceItems(normalizeGateNumber(e.target.value, 2, 0, 8))}
896
+ className={inputClass}
897
+ style={{ fontFamily: 'inherit' }}
898
+ />
899
+ </div>
900
+ <label className="flex items-center gap-2 text-[12px] text-text-2">
901
+ <input
902
+ type="checkbox"
903
+ checked={qualityGateRequireVerification}
904
+ onChange={(e) => setQualityGateRequireVerification(e.target.checked)}
905
+ />
906
+ Require verification evidence (tests/lint/build)
907
+ </label>
908
+ <label className="flex items-center gap-2 text-[12px] text-text-2">
909
+ <input
910
+ type="checkbox"
911
+ checked={qualityGateRequireArtifact}
912
+ onChange={(e) => setQualityGateRequireArtifact(e.target.checked)}
913
+ />
914
+ Require artifact evidence (upload URL or task artifacts)
915
+ </label>
916
+ <label className="flex items-center gap-2 text-[12px] text-text-2 md:col-span-2">
917
+ <input
918
+ type="checkbox"
919
+ checked={qualityGateRequireReport}
920
+ onChange={(e) => setQualityGateRequireReport(e.target.checked)}
921
+ />
922
+ Require generated task report
923
+ </label>
924
+ </div>
925
+ )}
926
+ </div>
927
+ </div>
928
+
800
929
  {/* Custom Fields */}
801
930
  {appSettings.taskCustomFieldDefs && appSettings.taskCustomFieldDefs.length > 0 && (
802
931
  <div className="mb-8">
@@ -64,6 +64,7 @@ function DialogContent({
64
64
  "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
65
65
  className
66
66
  )}
67
+ style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}
67
68
  {...props}
68
69
  >
69
70
  {children}
@@ -71,6 +71,7 @@ function SheetContent({
71
71
  "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
72
72
  className
73
73
  )}
74
+ style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}
74
75
  {...props}
75
76
  >
76
77
  {children}
@@ -9,6 +9,7 @@ import { useAppStore } from '@/stores/use-app-store'
9
9
  import { useWs } from '@/hooks/use-ws'
10
10
  import { api } from '@/lib/api-client'
11
11
  import type { BoardTask } from '@/types'
12
+ import { HintTip } from '@/components/shared/hint-tip'
12
13
 
13
14
  type Range = '24h' | '7d' | '30d'
14
15
 
@@ -66,11 +67,9 @@ function formatDuration(ms: number): string {
66
67
 
67
68
  function formatBucketLabel(bucket: string, range: Range): string {
68
69
  if (range === '24h') {
69
- // "2026-03-01T14" → "14:00"
70
70
  const hour = bucket.split('T')[1]
71
71
  return hour ? `${hour}:00` : bucket
72
72
  }
73
- // "2026-03-01" → "Mar 1"
74
73
  const parts = bucket.split('-')
75
74
  if (parts.length === 3) {
76
75
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
@@ -118,11 +117,8 @@ export function MetricsDashboard() {
118
117
  try {
119
118
  const res = await api<UsageResponse>('GET', `/usage?range=${range}`)
120
119
  setData(res)
121
- } catch {
122
- // ignore
123
- } finally {
124
- setLoading(false)
125
- }
120
+ } catch { /* ignore */ }
121
+ setLoading(false)
126
122
  }, [range])
127
123
 
128
124
  useEffect(() => {
@@ -135,7 +131,6 @@ export function MetricsDashboard() {
135
131
  // eslint-disable-next-line react-hooks/exhaustive-deps
136
132
  }, [])
137
133
 
138
- // --- Task metrics ---
139
134
  const [taskMetrics, setTaskMetrics] = useState<{
140
135
  wip: number; completedCount: number; avgCycleMs: number
141
136
  velocity: { bucket: string; count: number }[]
@@ -156,7 +151,6 @@ export function MetricsDashboard() {
156
151
 
157
152
  const completionRate = computeCompletionRate(tasks)
158
153
 
159
- // Prepare chart data
160
154
  const timeSeriesFormatted = (data?.timeSeries ?? []).map((pt) => ({
161
155
  ...pt,
162
156
  label: formatBucketLabel(pt.bucket, range),
@@ -190,13 +184,13 @@ export function MetricsDashboard() {
190
184
 
191
185
  return (
192
186
  <div className="flex-1 flex flex-col h-full overflow-y-auto">
193
- <div className="px-8 pt-6 pb-4 shrink-0">
187
+ <div className="px-8 pt-6 pb-4 shrink-0" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
194
188
  <h1 className="font-display text-[28px] font-700 tracking-[-0.03em]">Usage</h1>
195
189
  <p className="text-[13px] text-text-3 mt-1">Token usage, cost tracking &amp; agent performance</p>
196
190
  </div>
197
191
 
198
192
  {/* Range tabs */}
199
- <div className="px-8 pb-4 shrink-0">
193
+ <div className="px-8 pb-4 shrink-0" style={{ animation: 'fade-up 0.5s var(--ease-spring) 0.05s both' }}>
200
194
  <div className="flex gap-1 bg-surface-2 rounded-[10px] p-1 w-fit">
201
195
  {RANGES.map((r) => (
202
196
  <button
@@ -207,6 +201,7 @@ export function MetricsDashboard() {
207
201
  ? 'bg-accent-soft text-accent-bright'
208
202
  : 'text-text-3 hover:text-text-2'
209
203
  }`}
204
+ style={range === r ? { animation: 'spring-in 0.3s var(--ease-spring)' } : undefined}
210
205
  >
211
206
  {RANGE_LABELS[r]}
212
207
  </button>
@@ -225,31 +220,33 @@ export function MetricsDashboard() {
225
220
  <div className="px-8 pb-8 space-y-6">
226
221
  {/* Stats cards */}
227
222
  <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
228
- <StatCard label="Total Tokens" value={formatTokens(data?.totalTokens ?? 0)} />
229
- <StatCard label="Total Cost" value={formatCost(data?.totalCost ?? 0)} />
230
- <StatCard label="Requests" value={String(data?.records.length ?? 0)} />
231
- <StatCard label="Completion Rate" value={`${completionRate}%`} />
223
+ <StatCard label="Total Tokens" value={formatTokens(data?.totalTokens ?? 0)} index={0} />
224
+ <StatCard label="Total Cost" value={formatCost(data?.totalCost ?? 0)} index={1} />
225
+ <StatCard label="Requests" value={String(data?.records.length ?? 0)} index={2} />
226
+ <StatCard label="Completion Rate" value={`${completionRate}%`} index={3} />
232
227
  </div>
233
228
 
234
229
  {/* Token usage over time */}
235
- <ChartCard title="Token Usage Over Time">
236
- {timeSeriesFormatted.length > 0 ? (
237
- <ResponsiveContainer width="100%" height={280}>
238
- <LineChart data={timeSeriesFormatted} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
239
- <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
240
- <XAxis dataKey="label" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
241
- <YAxis tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={formatTokens} />
242
- <Tooltip {...tooltipStyle} formatter={(value: number | undefined) => [formatTokens(value ?? 0), 'Tokens']} />
243
- <Line type="monotone" dataKey="tokens" stroke="#818CF8" strokeWidth={2} dot={false} activeDot={{ r: 4, fill: '#818CF8' }} />
244
- </LineChart>
245
- </ResponsiveContainer>
246
- ) : (
247
- <EmptyChart />
248
- )}
249
- </ChartCard>
230
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both' }}>
231
+ <ChartCard title="Token Usage Over Time">
232
+ {timeSeriesFormatted.length > 0 ? (
233
+ <ResponsiveContainer width="100%" height={280}>
234
+ <LineChart data={timeSeriesFormatted} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
235
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
236
+ <XAxis dataKey="label" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
237
+ <YAxis tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={formatTokens} />
238
+ <Tooltip {...tooltipStyle} formatter={(value: number | undefined) => [formatTokens(value ?? 0), 'Tokens']} />
239
+ <Line type="monotone" dataKey="tokens" stroke="#818CF8" strokeWidth={2} dot={false} activeDot={{ r: 4, fill: '#818CF8' }} />
240
+ </LineChart>
241
+ </ResponsiveContainer>
242
+ ) : (
243
+ <EmptyChart />
244
+ )}
245
+ </ChartCard>
246
+ </div>
250
247
 
251
248
  {/* Cost by provider + cost by agent */}
252
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
249
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.25s both' }}>
253
250
  <ChartCard title="Cost by Provider">
254
251
  {providerData.length > 0 ? (
255
252
  <ResponsiveContainer width="100%" height={280}>
@@ -293,16 +290,16 @@ export function MetricsDashboard() {
293
290
 
294
291
  {/* Task KPIs */}
295
292
  {taskMetrics && (
296
- <>
293
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
297
294
  <h3 className="font-display text-[16px] font-700 text-text mt-2">Task Performance</h3>
298
- <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
299
- <StatCard label="Tasks Completed" value={String(taskMetrics.completedCount)} />
300
- <StatCard label="Avg Cycle Time" value={formatDuration(taskMetrics.avgCycleMs)} />
301
- <StatCard label="WIP" value={String(taskMetrics.wip)} />
302
- <StatCard label="Completion Rate" value={`${completionRate}%`} />
295
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
296
+ <StatCard label="Tasks Completed" value={String(taskMetrics.completedCount)} index={0} />
297
+ <StatCard label="Avg Cycle Time" value={formatDuration(taskMetrics.avgCycleMs)} index={1} />
298
+ <StatCard label="WIP" value={String(taskMetrics.wip)} index={2} />
299
+ <StatCard label="Completion Rate" value={`${completionRate}%`} index={3} />
303
300
  </div>
304
301
 
305
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
302
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
306
303
  <ChartCard title="Task Velocity">
307
304
  {taskMetrics.velocity.length > 0 ? (
308
305
  <ResponsiveContainer width="100%" height={280}>
@@ -345,20 +342,44 @@ export function MetricsDashboard() {
345
342
  )}
346
343
  </ChartCard>
347
344
  </div>
348
- </>
345
+ </div>
349
346
  )}
350
347
 
348
+ {/* Latency by Provider */}
349
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.35s both' }}>
350
+ <ChartCard title="Average Latency by Provider (ms)">
351
+ {providerData.some(p => (data?.providerHealth?.[p.name]?.avgLatencyMs ?? 0) > 0) ? (
352
+ <ResponsiveContainer width="100%" height={280}>
353
+ <BarChart data={providerData.map(p => ({ ...p, latency: Math.round(data?.providerHealth?.[p.name]?.avgLatencyMs || 0) }))} layout="vertical" margin={{ top: 5, right: 30, bottom: 5, left: 40 }}>
354
+ <CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" horizontal={false} />
355
+ <XAxis type="number" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
356
+ <YAxis dataKey="name" type="category" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} width={80} />
357
+ <Tooltip {...tooltipStyle} cursor={{ fill: 'rgba(255,255,255,0.04)' }} />
358
+ <Bar dataKey="latency" radius={[0, 4, 4, 0]}>
359
+ {providerData.map((_entry, index) => (
360
+ <Cell key={`cell-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
361
+ ))}
362
+ </Bar>
363
+ </BarChart>
364
+ </ResponsiveContainer>
365
+ ) : (
366
+ <EmptyChart />
367
+ )}
368
+ </ChartCard>
369
+ </div>
370
+
351
371
  {/* Provider Health */}
352
372
  {data?.providerHealth && Object.keys(data.providerHealth).length > 0 && (
353
- <div>
354
- <h3 className="font-display text-[14px] font-600 text-text-2 mb-3">Provider Health</h3>
373
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
374
+ <h3 className="font-display text-[14px] font-600 text-text-2 mb-3 flex items-center gap-2">Provider Health <HintTip text="API reliability and performance across your configured providers" /></h3>
355
375
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
356
376
  {Object.entries(data.providerHealth)
357
377
  .sort(([, a], [, b]) => b.totalRequests - a.totalRequests)
358
- .map(([name, h]) => (
378
+ .map(([name, h], idx) => (
359
379
  <div
360
380
  key={name}
361
- className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] flex flex-col gap-3"
381
+ className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] flex flex-col gap-3 hover:bg-surface transition-all hover:scale-[1.02]"
382
+ style={{ animation: 'spring-in 0.5s var(--ease-spring) both', animationDelay: `${0.45 + idx * 0.03}s` }}
362
383
  >
363
384
  <div className="flex items-center justify-between">
364
385
  <p className="text-[14px] font-600 text-text">{name}</p>
@@ -367,13 +388,13 @@ export function MetricsDashboard() {
367
388
  <div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[12px]">
368
389
  <span className="text-text-3">Requests</span>
369
390
  <span className="text-text font-500 text-right">{h.totalRequests}</span>
370
- <span className="text-text-3">Error Rate</span>
391
+ <span className="text-text-3 flex items-center gap-1">Error Rate <HintTip text="Percentage of API calls that failed" /></span>
371
392
  <span className={`font-500 text-right ${errorRateColor(h.errorRate)}`}>
372
393
  {(h.errorRate * 100).toFixed(1)}%
373
394
  </span>
374
395
  {h.avgLatencyMs > 0 && (
375
396
  <>
376
- <span className="text-text-3">Avg Latency</span>
397
+ <span className="text-text-3 flex items-center gap-1">Avg Latency <HintTip text="Average response time from the provider" /></span>
377
398
  <span className="text-text font-500 text-right">{Math.round(h.avgLatencyMs)}ms</span>
378
399
  </>
379
400
  )}
@@ -401,9 +422,12 @@ export function MetricsDashboard() {
401
422
  )
402
423
  }
403
424
 
404
- function StatCard({ label, value }: { label: string; value: string }) {
425
+ function StatCard({ label, value, index = 0 }: { label: string; value: string; index?: number }) {
405
426
  return (
406
- <div className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04]">
427
+ <div
428
+ className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] hover:bg-surface transition-all hover:scale-[1.02]"
429
+ style={{ animation: 'spring-in 0.6s var(--ease-spring) both', animationDelay: `${0.1 + index * 0.05}s` }}
430
+ >
407
431
  <p className="text-[11px] font-500 text-text-3 uppercase tracking-[0.05em] mb-1">{label}</p>
408
432
  <p className="text-[22px] font-display font-700 tracking-[-0.02em] text-text">{value}</p>
409
433
  </div>
@@ -412,7 +436,7 @@ function StatCard({ label, value }: { label: string; value: string }) {
412
436
 
413
437
  function ChartCard({ title, children }: { title: string; children: React.ReactNode }) {
414
438
  return (
415
- <div className="bg-surface-2 rounded-[12px] p-5 border border-white/[0.04]">
439
+ <div className="bg-surface-2 rounded-[12px] p-5 border border-white/[0.04] hover:border-white/[0.1] transition-colors">
416
440
  <h3 className="font-display text-[14px] font-600 text-text-2 mb-4">{title}</h3>
417
441
  {children}
418
442
  </div>