@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.
Files changed (109) hide show
  1. package/README.md +15 -2
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +3 -2
  15. package/src/app/api/tts/stream/route.ts +3 -2
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +46 -22
  31. package/src/components/chat/chat-header.tsx +455 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +180 -7
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +68 -16
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +51 -11
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/manager.ts +218 -7
  78. package/src/lib/server/heartbeat-service.ts +8 -1
  79. package/src/lib/server/main-agent-loop.ts +1 -1
  80. package/src/lib/server/memory-consolidation.ts +15 -2
  81. package/src/lib/server/memory-db.ts +134 -6
  82. package/src/lib/server/mime.ts +51 -0
  83. package/src/lib/server/openclaw-gateway.ts +2 -2
  84. package/src/lib/server/orchestrator-lg.ts +2 -0
  85. package/src/lib/server/orchestrator.ts +5 -2
  86. package/src/lib/server/playwright-proxy.mjs +2 -3
  87. package/src/lib/server/prompt-runtime-context.ts +53 -0
  88. package/src/lib/server/queue.ts +52 -7
  89. package/src/lib/server/session-tools/canvas.ts +67 -0
  90. package/src/lib/server/session-tools/connector.ts +83 -9
  91. package/src/lib/server/session-tools/crud.ts +21 -0
  92. package/src/lib/server/session-tools/delegate.ts +68 -4
  93. package/src/lib/server/session-tools/git.ts +71 -0
  94. package/src/lib/server/session-tools/http.ts +57 -0
  95. package/src/lib/server/session-tools/index.ts +8 -0
  96. package/src/lib/server/session-tools/memory.ts +1 -0
  97. package/src/lib/server/session-tools/search-providers.ts +16 -8
  98. package/src/lib/server/session-tools/subagent.ts +106 -0
  99. package/src/lib/server/session-tools/web.ts +115 -4
  100. package/src/lib/server/stream-agent-chat.ts +32 -10
  101. package/src/lib/server/task-mention.ts +41 -0
  102. package/src/lib/sessions.ts +10 -0
  103. package/src/lib/soul-library.ts +103 -0
  104. package/src/lib/task-dedupe.ts +26 -0
  105. package/src/lib/tool-definitions.ts +2 -0
  106. package/src/lib/tts.ts +2 -2
  107. package/src/stores/use-app-store.ts +5 -1
  108. package/src/stores/use-chat-store.ts +65 -2
  109. package/src/types/index.ts +32 -2
@@ -3,6 +3,8 @@
3
3
  import type { SettingsSectionProps } from './types'
4
4
 
5
5
  export function VoiceSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
6
+ const enabled = appSettings.elevenLabsEnabled ?? false
7
+
6
8
  return (
7
9
  <div className="mb-10">
8
10
  <h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
@@ -12,30 +14,49 @@ export function VoiceSection({ appSettings, patchSettings, inputClass }: Setting
12
14
  Configure voice playback (TTS) and speech-to-text input.
13
15
  </p>
14
16
  <div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
15
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
16
- <div>
17
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">ElevenLabs API Key</label>
18
- <input
19
- type="password"
20
- value={appSettings.elevenLabsApiKey || ''}
21
- onChange={(e) => patchSettings({ elevenLabsApiKey: e.target.value || null })}
22
- placeholder="sk_..."
23
- className={inputClass}
24
- style={{ fontFamily: 'inherit' }}
25
- />
26
- </div>
17
+ {/* ElevenLabs toggle */}
18
+ <div className="flex items-center justify-between mb-5">
27
19
  <div>
28
- <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">ElevenLabs Voice ID</label>
29
- <input
30
- type="text"
31
- value={appSettings.elevenLabsVoiceId || ''}
32
- onChange={(e) => patchSettings({ elevenLabsVoiceId: e.target.value || null })}
33
- placeholder="JBFqnCBsd6RMkjVDRZzb"
34
- className={inputClass}
35
- style={{ fontFamily: 'inherit' }}
36
- />
20
+ <label className="font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em]">ElevenLabs TTS</label>
21
+ <p className="text-[11px] text-text-3/60 mt-0.5">Enable text-to-speech for agent responses</p>
37
22
  </div>
23
+ <button
24
+ type="button"
25
+ onClick={() => patchSettings({ elevenLabsEnabled: !enabled })}
26
+ className={`relative w-10 h-[22px] rounded-full transition-colors cursor-pointer border-none ${enabled ? 'bg-accent-bright' : 'bg-surface-3'}`}
27
+ >
28
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform ${enabled ? 'translate-x-[18px]' : ''}`} />
29
+ </button>
38
30
  </div>
31
+
32
+ {enabled && (
33
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
34
+ <div>
35
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">API Key</label>
36
+ <input
37
+ type="password"
38
+ value={appSettings.elevenLabsApiKey || ''}
39
+ onChange={(e) => patchSettings({ elevenLabsApiKey: e.target.value || null })}
40
+ placeholder="sk_..."
41
+ className={inputClass}
42
+ style={{ fontFamily: 'inherit' }}
43
+ />
44
+ </div>
45
+ <div>
46
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Default Voice ID</label>
47
+ <input
48
+ type="text"
49
+ value={appSettings.elevenLabsVoiceId || ''}
50
+ onChange={(e) => patchSettings({ elevenLabsVoiceId: e.target.value || null })}
51
+ placeholder="JBFqnCBsd6RMkjVDRZzb"
52
+ className={inputClass}
53
+ style={{ fontFamily: 'inherit' }}
54
+ />
55
+ <p className="text-[11px] text-text-3/60 mt-1.5">Fallback voice when an agent has no override set.</p>
56
+ </div>
57
+ </div>
58
+ )}
59
+
39
60
  <div>
40
61
  <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Speech Recognition Language</label>
41
62
  <input
@@ -26,8 +26,8 @@ export function WebSearchSection({ appSettings, patchSettings, inputClass }: Set
26
26
  <option value="google">Google (scraping, no key required)</option>
27
27
  <option value="bing">Bing (scraping, no key required)</option>
28
28
  <option value="searxng">SearXNG (self-hosted, no key required)</option>
29
- <option value="tavily">Tavily (requires API key in Secrets)</option>
30
- <option value="brave">Brave Search (requires API key in Secrets)</option>
29
+ <option value="tavily">Tavily (API key required)</option>
30
+ <option value="brave">Brave Search (API key required)</option>
31
31
  </select>
32
32
  </div>
33
33
 
@@ -45,10 +45,34 @@ export function WebSearchSection({ appSettings, patchSettings, inputClass }: Set
45
45
  </div>
46
46
  )}
47
47
 
48
- {(provider === 'tavily' || provider === 'brave') && (
49
- <p className="text-[11px] text-text-3/70">
50
- Add a secret named &quot;{provider}&quot; or &quot;{provider}_api_key&quot; in the Secrets section below.
51
- </p>
48
+ {provider === 'tavily' && (
49
+ <div>
50
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Tavily API Key</label>
51
+ <input
52
+ type="password"
53
+ value={appSettings.tavilyApiKey || ''}
54
+ onChange={(e) => patchSettings({ tavilyApiKey: e.target.value || null })}
55
+ placeholder="tvly-..."
56
+ className={inputClass}
57
+ style={{ fontFamily: 'inherit' }}
58
+ />
59
+ <p className="text-[11px] text-text-3/60 mt-2">Get your API key from <a href="https://tavily.com" target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">tavily.com</a></p>
60
+ </div>
61
+ )}
62
+
63
+ {provider === 'brave' && (
64
+ <div>
65
+ <label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Brave Search API Key</label>
66
+ <input
67
+ type="password"
68
+ value={appSettings.braveApiKey || ''}
69
+ onChange={(e) => patchSettings({ braveApiKey: e.target.value || null })}
70
+ placeholder="BSA..."
71
+ className={inputClass}
72
+ style={{ fontFamily: 'inherit' }}
73
+ />
74
+ <p className="text-[11px] text-text-3/60 mt-2">Get your API key from <a href="https://brave.com/search/api/" target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">brave.com/search/api</a></p>
75
+ </div>
52
76
  )}
53
77
  </div>
54
78
  </div>
@@ -8,6 +8,7 @@ import { ThemeSection } from './section-theme'
8
8
  import { OrchestratorSection } from './section-orchestrator'
9
9
  import { RuntimeLoopSection } from './section-runtime-loop'
10
10
  import { CapabilityPolicySection } from './section-capability-policy'
11
+ import { StorageSection } from './section-storage'
11
12
  import { VoiceSection } from './section-voice'
12
13
  import { WebSearchSection } from './section-web-search'
13
14
  import { HeartbeatSection } from './section-heartbeat'
@@ -28,7 +29,7 @@ const TABS: Tab[] = [
28
29
  {
29
30
  id: 'general',
30
31
  label: 'General',
31
- keywords: ['preferences', 'user', 'language', 'default', 'capability', 'policy', 'permissions', 'tools'],
32
+ keywords: ['preferences', 'user', 'language', 'default', 'capability', 'policy', 'permissions', 'tools', 'storage', 'uploads', 'disk', 'files', 'cleanup'],
32
33
  icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></svg>,
33
34
  },
34
35
  {
@@ -180,6 +181,7 @@ export function SettingsPage() {
180
181
  <>
181
182
  <UserPreferencesSection {...sectionProps} />
182
183
  <CapabilityPolicySection {...sectionProps} />
184
+ <StorageSection {...sectionProps} />
183
185
  </>
184
186
  )}
185
187
 
@@ -0,0 +1,259 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
5
+
6
+ interface UploadFile {
7
+ name: string
8
+ size: number
9
+ modified: number
10
+ category: string
11
+ url: string
12
+ }
13
+
14
+ type SortField = 'modified' | 'size' | 'name'
15
+
16
+ interface Props {
17
+ files: UploadFile[]
18
+ onDelete: (filenames: string[]) => void
19
+ }
20
+
21
+ function formatBytes(bytes: number): string {
22
+ if (bytes === 0) return '0 B'
23
+ const units = ['B', 'KB', 'MB', 'GB']
24
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
25
+ const value = bytes / Math.pow(1024, i)
26
+ return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[i]}`
27
+ }
28
+
29
+ function formatDate(ms: number): string {
30
+ const d = new Date(ms)
31
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
32
+ }
33
+
34
+ const CATEGORY_ICONS: Record<string, string> = {
35
+ image: '\u{1F5BC}',
36
+ video: '\u{1F3AC}',
37
+ audio: '\u{1F3B5}',
38
+ document: '\u{1F4C4}',
39
+ archive: '\u{1F4E6}',
40
+ other: '\u{1F4CE}',
41
+ }
42
+
43
+ const CATEGORY_LABELS: Record<string, string> = {
44
+ image: 'Images',
45
+ video: 'Videos',
46
+ audio: 'Audio',
47
+ document: 'Docs',
48
+ archive: 'Archives',
49
+ other: 'Other',
50
+ }
51
+
52
+ export function StorageBrowser({ files, onDelete }: Props) {
53
+ const [selected, setSelected] = useState<Set<string>>(new Set())
54
+ const [sortBy, setSortBy] = useState<SortField>('modified')
55
+ const [filterCategory, setFilterCategory] = useState<string | null>(null)
56
+ const [confirmDelete, setConfirmDelete] = useState<string[] | null>(null)
57
+
58
+ const categories = useMemo(() => {
59
+ const cats = new Set<string>()
60
+ for (const f of files) cats.add(f.category)
61
+ return Array.from(cats).sort()
62
+ }, [files])
63
+
64
+ const filtered = useMemo(() => {
65
+ let list = filterCategory ? files.filter((f) => f.category === filterCategory) : files
66
+ list = [...list].sort((a, b) => {
67
+ if (sortBy === 'modified') return b.modified - a.modified
68
+ if (sortBy === 'size') return b.size - a.size
69
+ return a.name.localeCompare(b.name)
70
+ })
71
+ return list
72
+ }, [files, filterCategory, sortBy])
73
+
74
+ const totalSize = useMemo(() => files.reduce((s, f) => s + f.size, 0), [files])
75
+
76
+ const toggleSelect = (name: string) => {
77
+ setSelected((prev) => {
78
+ const next = new Set(prev)
79
+ if (next.has(name)) next.delete(name)
80
+ else next.add(name)
81
+ return next
82
+ })
83
+ }
84
+
85
+ const toggleSelectAll = () => {
86
+ if (selected.size === filtered.length) {
87
+ setSelected(new Set())
88
+ } else {
89
+ setSelected(new Set(filtered.map((f) => f.name)))
90
+ }
91
+ }
92
+
93
+ const handleDeleteSelected = () => {
94
+ const names = Array.from(selected)
95
+ if (names.length > 0) setConfirmDelete(names)
96
+ }
97
+
98
+ const executeDelete = () => {
99
+ if (confirmDelete) {
100
+ onDelete(confirmDelete)
101
+ setSelected((prev) => {
102
+ const next = new Set(prev)
103
+ for (const name of confirmDelete) next.delete(name)
104
+ return next
105
+ })
106
+ setConfirmDelete(null)
107
+ }
108
+ }
109
+
110
+ return (
111
+ <div>
112
+ {/* Header */}
113
+ <div className="flex items-center justify-between mb-5">
114
+ <div>
115
+ <h3 className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">File Browser</h3>
116
+ <p className="text-[12px] text-text-3 mt-0.5">
117
+ {files.length} file{files.length !== 1 ? 's' : ''} &middot; {formatBytes(totalSize)}
118
+ </p>
119
+ </div>
120
+ <select
121
+ value={sortBy}
122
+ onChange={(e) => setSortBy(e.target.value as SortField)}
123
+ className="px-3 py-1.5 rounded-[10px] border border-white/[0.08] bg-bg text-text text-[12px] outline-none cursor-pointer"
124
+ style={{ fontFamily: 'inherit' }}
125
+ >
126
+ <option value="modified">Newest first</option>
127
+ <option value="size">Largest first</option>
128
+ <option value="name">Name A-Z</option>
129
+ </select>
130
+ </div>
131
+
132
+ {/* Category filters */}
133
+ <div className="flex gap-1.5 mb-4 flex-wrap">
134
+ <button
135
+ onClick={() => setFilterCategory(null)}
136
+ className={`px-3 py-1 rounded-full text-[11px] font-600 cursor-pointer transition-all border
137
+ ${!filterCategory
138
+ ? 'bg-accent-soft border-accent-bright/30 text-accent-bright'
139
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.04]'}`}
140
+ style={{ fontFamily: 'inherit' }}
141
+ >
142
+ All
143
+ </button>
144
+ {categories.map((cat) => (
145
+ <button
146
+ key={cat}
147
+ onClick={() => setFilterCategory(filterCategory === cat ? null : cat)}
148
+ className={`px-3 py-1 rounded-full text-[11px] font-600 cursor-pointer transition-all border
149
+ ${filterCategory === cat
150
+ ? 'bg-accent-soft border-accent-bright/30 text-accent-bright'
151
+ : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.04]'}`}
152
+ style={{ fontFamily: 'inherit' }}
153
+ >
154
+ {CATEGORY_ICONS[cat] || ''} {CATEGORY_LABELS[cat] || cat}
155
+ </button>
156
+ ))}
157
+ </div>
158
+
159
+ {/* Select all */}
160
+ {filtered.length > 0 && (
161
+ <div className="flex items-center gap-2 mb-3">
162
+ <button
163
+ onClick={toggleSelectAll}
164
+ className="text-[11px] text-accent-bright hover:underline cursor-pointer bg-transparent border-none"
165
+ style={{ fontFamily: 'inherit' }}
166
+ >
167
+ {selected.size === filtered.length ? 'Deselect all' : 'Select all'}
168
+ </button>
169
+ {selected.size > 0 && (
170
+ <span className="text-[11px] text-text-3">
171
+ {selected.size} selected
172
+ </span>
173
+ )}
174
+ </div>
175
+ )}
176
+
177
+ {/* File grid */}
178
+ {filtered.length === 0 ? (
179
+ <div className="py-12 text-center text-[13px] text-text-3/60">
180
+ {files.length === 0 ? 'No uploaded files.' : 'No files match this filter.'}
181
+ </div>
182
+ ) : (
183
+ <div className="grid grid-cols-2 sm:grid-cols-3 gap-2 max-h-[400px] overflow-y-auto pr-1">
184
+ {filtered.map((file) => (
185
+ <div
186
+ key={file.name}
187
+ onClick={() => toggleSelect(file.name)}
188
+ className={`relative p-3 rounded-[14px] border cursor-pointer transition-all
189
+ ${selected.has(file.name)
190
+ ? 'border-accent-bright/40 bg-accent-soft/30'
191
+ : 'border-white/[0.06] bg-surface hover:border-white/[0.12]'}`}
192
+ >
193
+ {/* Checkbox */}
194
+ <div className={`absolute top-2 right-2 w-4 h-4 rounded-[5px] border transition-all flex items-center justify-center
195
+ ${selected.has(file.name)
196
+ ? 'border-accent-bright bg-accent-bright'
197
+ : 'border-white/[0.15] bg-transparent'}`}
198
+ >
199
+ {selected.has(file.name) && (
200
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
201
+ <polyline points="20 6 9 17 4 12" />
202
+ </svg>
203
+ )}
204
+ </div>
205
+
206
+ {/* Thumbnail / icon */}
207
+ <div className="w-full aspect-square rounded-[10px] bg-white/[0.03] mb-2 flex items-center justify-center overflow-hidden">
208
+ {file.category === 'image' ? (
209
+ // eslint-disable-next-line @next/next/no-img-element
210
+ <img
211
+ src={file.url}
212
+ alt={file.name}
213
+ className="w-full h-full object-cover rounded-[10px]"
214
+ loading="lazy"
215
+ />
216
+ ) : (
217
+ <span className="text-[28px]">{CATEGORY_ICONS[file.category] || CATEGORY_ICONS.other}</span>
218
+ )}
219
+ </div>
220
+
221
+ {/* Meta */}
222
+ <p className="text-[11px] font-600 text-text truncate" title={file.name}>{file.name}</p>
223
+ <p className="text-[10px] text-text-3/60 mt-0.5">
224
+ {formatBytes(file.size)} &middot; {formatDate(file.modified)}
225
+ </p>
226
+ </div>
227
+ ))}
228
+ </div>
229
+ )}
230
+
231
+ {/* Bulk delete footer */}
232
+ {selected.size > 0 && (
233
+ <div className="mt-4 pt-4 border-t border-white/[0.06] flex items-center justify-between">
234
+ <span className="text-[12px] text-text-3">
235
+ {selected.size} file{selected.size !== 1 ? 's' : ''} selected
236
+ </span>
237
+ <button
238
+ onClick={handleDeleteSelected}
239
+ className="px-4 py-2 rounded-[10px] bg-danger text-white text-[12px] font-600 cursor-pointer
240
+ hover:brightness-110 active:scale-[0.97] transition-all border-none"
241
+ style={{ fontFamily: 'inherit' }}
242
+ >
243
+ Delete Selected
244
+ </button>
245
+ </div>
246
+ )}
247
+
248
+ <ConfirmDialog
249
+ open={!!confirmDelete}
250
+ title="Delete Files"
251
+ message={`Permanently delete ${confirmDelete?.length ?? 0} file${(confirmDelete?.length ?? 0) !== 1 ? 's' : ''}? This cannot be undone.`}
252
+ confirmLabel="Delete"
253
+ danger
254
+ onConfirm={executeDelete}
255
+ onCancel={() => setConfirmDelete(null)}
256
+ />
257
+ </div>
258
+ )
259
+ }
@@ -34,6 +34,14 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
34
34
  const agent = agents[task.agentId]
35
35
  const project = task.projectId ? projects[task.projectId] : null
36
36
 
37
+ const priorityConfig = {
38
+ critical: { label: 'Critical', cls: 'bg-red-500/10 text-red-400' },
39
+ high: { label: 'High', cls: 'bg-orange-500/10 text-orange-400' },
40
+ medium: { label: 'Med', cls: 'bg-amber-500/10 text-amber-400' },
41
+ low: { label: 'Low', cls: 'bg-sky-500/10 text-sky-400' },
42
+ } as const
43
+ const prio = task.priority && priorityConfig[task.priority]
44
+
37
45
  const isBlocked = Array.isArray(task.blockedBy) && task.blockedBy.length > 0
38
46
  const isOverdue = task.dueAt && task.dueAt < Date.now() && task.status !== 'completed' && task.status !== 'archived'
39
47
  const borderColor = isBlocked ? 'border-l-rose-500'
@@ -86,7 +94,7 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
86
94
  setTaskSheetOpen(true)
87
95
  }
88
96
  }}
89
- className={`p-4 rounded-[14px] border border-l-[3px] ${borderColor} bg-surface hover:bg-surface-2 transition-all group
97
+ className={`py-3 px-4 rounded-[14px] border border-l-[3px] ${borderColor} bg-surface hover:bg-surface-2 transition-all group
90
98
  ${selectionMode ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing'}
91
99
  ${dragging ? 'opacity-40 scale-[0.97]' : ''}
92
100
  ${selected ? 'border-accent-bright/40 bg-accent-bright/[0.04] ring-1 ring-accent-bright/20' : 'border-white/[0.06]'}`}
@@ -114,6 +122,11 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
114
122
  </svg>
115
123
  )}
116
124
  <h4 className="flex-1 text-[14px] font-600 text-text leading-[1.4] line-clamp-2">{task.title}</h4>
125
+ {prio && (
126
+ <span className={`px-1.5 py-0.5 rounded-[5px] text-[10px] font-600 shrink-0 ${prio.cls}`}>
127
+ {prio.label}
128
+ </span>
129
+ )}
117
130
  {isBlocked && (
118
131
  <span className="px-1.5 py-0.5 rounded-[5px] bg-rose-500/10 text-rose-400 text-[10px] font-600 shrink-0">
119
132
  {task.blockedBy?.length}