@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
@@ -46,13 +46,12 @@ export function ChatPreviewPanel({ content, onClose }: Props) {
46
46
 
47
47
  return (
48
48
  <div
49
- className="flex flex-col border-l border-white/[0.06] bg-bg shrink-0"
49
+ className="relative flex flex-col border-l border-white/[0.06] bg-bg shrink-0"
50
50
  style={{ width, minWidth: 300, maxWidth: '50%', animation: 'fade-in 0.25s ease' }}
51
51
  >
52
52
  {/* Resize handle */}
53
53
  <div
54
54
  className="absolute left-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-accent-bright/20 transition-colors z-10"
55
- style={{ position: 'relative', width: 4, minWidth: 4 }}
56
55
  onMouseDown={handleMouseDown}
57
56
  />
58
57
 
@@ -0,0 +1,371 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
6
+ import { api } from '@/lib/api-client'
7
+
8
+ type DelegationStatus = 'delegating' | 'checking' | 'completed' | 'failed'
9
+
10
+ interface DelegationBannerProps {
11
+ agentName: string
12
+ agentAvatarSeed: string | null
13
+ taskPreview: string
14
+ taskId: string | null
15
+ status: DelegationStatus
16
+ }
17
+
18
+ const STATUS_CONFIG: Record<DelegationStatus, { color: string; bg: string; border: string }> = {
19
+ delegating: { color: '#818CF8', bg: 'rgba(99,102,241,0.06)', border: 'rgba(99,102,241,0.12)' },
20
+ checking: { color: '#818CF8', bg: 'rgba(99,102,241,0.06)', border: 'rgba(99,102,241,0.12)' },
21
+ completed: { color: '#34D399', bg: 'rgba(52,211,153,0.06)', border: 'rgba(52,211,153,0.12)' },
22
+ failed: { color: '#F43F5E', bg: 'rgba(244,63,94,0.06)', border: 'rgba(244,63,94,0.12)' },
23
+ }
24
+
25
+ function StatusIcon({ status, color }: { status: DelegationStatus; color: string }) {
26
+ switch (status) {
27
+ case 'delegating':
28
+ return (
29
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round" className="shrink-0">
30
+ <path d="M5 12h14" />
31
+ <path d="M12 5l7 7-7 7" />
32
+ </svg>
33
+ )
34
+ case 'checking':
35
+ return (
36
+ <span className="w-3.5 h-3.5 shrink-0 rounded-full border-2 animate-spin" style={{ borderColor: color, borderTopColor: 'transparent' }} />
37
+ )
38
+ case 'completed':
39
+ return (
40
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round" className="shrink-0">
41
+ <polyline points="20 6 9 17 4 12" />
42
+ </svg>
43
+ )
44
+ case 'failed':
45
+ return (
46
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round" className="shrink-0">
47
+ <line x1="18" y1="6" x2="6" y2="18" />
48
+ <line x1="6" y1="6" x2="18" y2="18" />
49
+ </svg>
50
+ )
51
+ }
52
+ }
53
+
54
+ function statusText(status: DelegationStatus, name: string): string {
55
+ switch (status) {
56
+ case 'delegating': return `Delegated to ${name}`
57
+ case 'checking': return `Checking on ${name}...`
58
+ case 'completed': return `${name} completed`
59
+ case 'failed': return `${name} failed`
60
+ }
61
+ }
62
+
63
+ export function DelegationBanner({ agentName, agentAvatarSeed, taskPreview, taskId, status }: DelegationBannerProps) {
64
+ const cfg = STATUS_CONFIG[status]
65
+
66
+ const handleTaskClick = () => {
67
+ if (!taskId) return
68
+ const store = useAppStore.getState()
69
+ store.loadTasks(true).then(() => {
70
+ store.setTaskSheetViewOnly(true)
71
+ store.setEditingTaskId(taskId)
72
+ store.setTaskSheetOpen(true)
73
+ })
74
+ }
75
+
76
+ return (
77
+ <div
78
+ className="rounded-[12px] px-3.5 py-2.5 flex items-center gap-2.5"
79
+ style={{
80
+ background: cfg.bg,
81
+ border: `1px solid ${cfg.border}`,
82
+ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1)',
83
+ }}
84
+ >
85
+ <div className="shrink-0" style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both' }}>
86
+ <AgentAvatar seed={agentAvatarSeed} name={agentName} size={24} />
87
+ </div>
88
+ <StatusIcon status={status} color={cfg.color} />
89
+ <div className="flex flex-col gap-0.5 min-w-0 flex-1">
90
+ <span className="text-[12px] font-600" style={{ color: cfg.color }}>
91
+ {statusText(status, agentName)}
92
+ </span>
93
+ {taskPreview && (
94
+ <span className="text-[11px] text-text-3 truncate">{taskPreview}</span>
95
+ )}
96
+ </div>
97
+ {taskId && (
98
+ <button
99
+ type="button"
100
+ onClick={handleTaskClick}
101
+ className="shrink-0 text-[10px] font-600 px-2 py-1 rounded-[6px] cursor-pointer border-none transition-colors"
102
+ style={{
103
+ color: cfg.color,
104
+ background: `${cfg.color}15`,
105
+ }}
106
+ >
107
+ View Task
108
+ </button>
109
+ )}
110
+ </div>
111
+ )
112
+ }
113
+
114
+ /* ---------- Task Completion Card ---------- */
115
+
116
+ export interface TaskCompletionInfo {
117
+ status: 'completed' | 'failed'
118
+ taskTitle: string
119
+ taskId: string | null
120
+ /** The agent that executed the task (present on delegated results) */
121
+ executorName: string | null
122
+ workingDir: string | null
123
+ resumeInfo: string | null
124
+ resultBody: string
125
+ imageUrl?: string
126
+ }
127
+
128
+ const TASK_COMPLETION_RE = /^(?:Delegated )?[Tt]ask (completed|failed): \*\*\[([^\]]+)\]\(#task:([^)]+)\)\*\*(?:\s*\(by ([^)]+)\))?/
129
+
130
+ export function parseTaskCompletion(text: string): TaskCompletionInfo | null {
131
+ const m = text.match(TASK_COMPLETION_RE)
132
+ if (!m) return null
133
+ const status = m[1] as 'completed' | 'failed'
134
+ const taskTitle = m[2]
135
+ const taskId = m[3] || null
136
+ const executorName = m[4] || null
137
+
138
+ // Parse the body sections (separated by double newlines)
139
+ const bodyStart = text.indexOf('\n\n')
140
+ const sections = bodyStart === -1 ? [] : text.slice(bodyStart + 2).split('\n\n')
141
+
142
+ let workingDir: string | null = null
143
+ let resumeInfo: string | null = null
144
+ const resultParts: string[] = []
145
+
146
+ for (const section of sections) {
147
+ if (section.startsWith('Working directory: ')) {
148
+ workingDir = section.replace('Working directory: ', '').replace(/^`|`$/g, '')
149
+ } else if (/^(Claude session|Codex thread|OpenCode session|CLI session):/.test(section)) {
150
+ resumeInfo = section
151
+ } else if (section.trim()) {
152
+ resultParts.push(section)
153
+ }
154
+ }
155
+
156
+ return { status, taskTitle, taskId, executorName, workingDir, resumeInfo, resultBody: resultParts.join('\n\n') }
157
+ }
158
+
159
+ export function TaskCompletionCard({ info }: { info: TaskCompletionInfo }) {
160
+ const isSuccess = info.status === 'completed'
161
+ const [expanded, setExpanded] = useState(false)
162
+
163
+ const handleTaskClick = () => {
164
+ if (!info.taskId) return
165
+ const store = useAppStore.getState()
166
+ store.loadTasks(true).then(() => {
167
+ store.setTaskSheetViewOnly(true)
168
+ store.setEditingTaskId(info.taskId!)
169
+ store.setTaskSheetOpen(true)
170
+ })
171
+ }
172
+
173
+ // Truncate result for preview
174
+ const resultPreview = info.resultBody.length > 200 ? info.resultBody.slice(0, 200) + '...' : info.resultBody
175
+ const hasLongResult = info.resultBody.length > 200
176
+
177
+ return (
178
+ <div
179
+ className="rounded-[14px] overflow-hidden"
180
+ style={{
181
+ background: isSuccess ? 'rgba(52,211,153,0.04)' : 'rgba(244,63,94,0.04)',
182
+ border: `1px solid ${isSuccess ? 'rgba(52,211,153,0.15)' : 'rgba(244,63,94,0.15)'}`,
183
+ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1)',
184
+ }}
185
+ >
186
+ {/* Status header bar */}
187
+ <div
188
+ className="flex items-center gap-2.5 px-4 py-2.5"
189
+ style={{
190
+ background: isSuccess ? 'rgba(52,211,153,0.06)' : 'rgba(244,63,94,0.06)',
191
+ borderBottom: `1px solid ${isSuccess ? 'rgba(52,211,153,0.08)' : 'rgba(244,63,94,0.08)'}`,
192
+ }}
193
+ >
194
+ {/* Status icon */}
195
+ {isSuccess ? (
196
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" className="shrink-0">
197
+ <circle cx="12" cy="12" r="10" stroke="#34D399" strokeWidth="2" />
198
+ <polyline points="8 12 11 15 16 9" stroke="#34D399" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
199
+ </svg>
200
+ ) : (
201
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" className="shrink-0">
202
+ <circle cx="12" cy="12" r="10" stroke="#F43F5E" strokeWidth="2" />
203
+ <line x1="15" y1="9" x2="9" y2="15" stroke="#F43F5E" strokeWidth="2.5" strokeLinecap="round" />
204
+ <line x1="9" y1="9" x2="15" y2="15" stroke="#F43F5E" strokeWidth="2.5" strokeLinecap="round" />
205
+ </svg>
206
+ )}
207
+
208
+ <div className="flex flex-col gap-0.5 min-w-0 flex-1">
209
+ <span className={`text-[12px] font-700 ${isSuccess ? 'text-emerald-400' : 'text-rose-400'}`}>
210
+ {info.executorName
211
+ ? `${info.executorName} — task ${info.status}`
212
+ : `Task ${info.status}`
213
+ }
214
+ </span>
215
+ </div>
216
+
217
+ {info.taskId && (
218
+ <button
219
+ type="button"
220
+ onClick={handleTaskClick}
221
+ className="shrink-0 text-[10px] font-600 px-2.5 py-1 rounded-[6px] cursor-pointer border-none transition-colors"
222
+ style={{
223
+ color: isSuccess ? '#34D399' : '#F43F5E',
224
+ background: isSuccess ? 'rgba(52,211,153,0.1)' : 'rgba(244,63,94,0.1)',
225
+ }}
226
+ >
227
+ View Task
228
+ </button>
229
+ )}
230
+ </div>
231
+
232
+ {/* Body content */}
233
+ <div className="px-4 py-3 flex flex-col gap-2.5">
234
+ {/* Task title */}
235
+ <div className="flex items-start gap-2">
236
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0 mt-0.5 text-text-3/50">
237
+ <path d="M9 11l3 3L22 4" />
238
+ <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
239
+ </svg>
240
+ {info.taskId ? (
241
+ <button
242
+ type="button"
243
+ onClick={handleTaskClick}
244
+ className={`text-[13px] font-600 ${isSuccess ? 'text-emerald-300 hover:text-emerald-200' : 'text-rose-300 hover:text-rose-200'} cursor-pointer bg-transparent border-none p-0 font-inherit text-left underline decoration-current/30 hover:decoration-current/60 transition-colors`}
245
+ >
246
+ {info.taskTitle}
247
+ </button>
248
+ ) : (
249
+ <span className="text-[13px] font-600 text-text-2">{info.taskTitle}</span>
250
+ )}
251
+ </div>
252
+
253
+ {/* Working directory */}
254
+ {info.workingDir && (
255
+ <div className="flex items-center gap-2">
256
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0 text-text-3/40">
257
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
258
+ </svg>
259
+ <button
260
+ type="button"
261
+ onClick={() => { api('POST', '/files/open', { path: info.workingDir }).catch(() => {}) }}
262
+ className="text-[11px] text-text-3/60 hover:text-text-3 font-mono truncate bg-transparent border-none p-0 cursor-pointer transition-colors"
263
+ title="Open folder"
264
+ >
265
+ {info.workingDir}
266
+ </button>
267
+ </div>
268
+ )}
269
+
270
+ {/* Resume info */}
271
+ {info.resumeInfo && (
272
+ <div className="flex items-center gap-2">
273
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0 text-text-3/40">
274
+ <polyline points="4 17 10 11 4 5" />
275
+ <line x1="12" y1="19" x2="20" y2="19" />
276
+ </svg>
277
+ <span className="text-[11px] text-text-3/60 font-mono truncate">{info.resumeInfo}</span>
278
+ </div>
279
+ )}
280
+
281
+ {/* Image artifact */}
282
+ {info.imageUrl && (
283
+ <div className="mt-0.5">
284
+ {/* eslint-disable-next-line @next/next/no-img-element */}
285
+ <img
286
+ src={info.imageUrl}
287
+ alt="Task result"
288
+ loading="lazy"
289
+ className="max-w-full rounded-[10px] border border-white/[0.06]"
290
+ onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
291
+ />
292
+ </div>
293
+ )}
294
+
295
+ {/* Result body */}
296
+ {info.resultBody && (
297
+ <div className="mt-0.5">
298
+ <div className="rounded-[10px] bg-white/[0.02] border border-white/[0.04] px-3 py-2.5">
299
+ <pre className="text-[12px] leading-[1.6] text-text-3/80 whitespace-pre-wrap break-words m-0 font-mono">
300
+ {expanded ? info.resultBody : resultPreview}
301
+ </pre>
302
+ {hasLongResult && (
303
+ <button
304
+ type="button"
305
+ onClick={() => setExpanded((v) => !v)}
306
+ className="mt-2 text-[11px] font-600 text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer p-0 transition-colors"
307
+ >
308
+ {expanded ? 'Show less' : 'Show full result'}
309
+ </button>
310
+ )}
311
+ </div>
312
+ </div>
313
+ )}
314
+ </div>
315
+ </div>
316
+ )
317
+ }
318
+
319
+ /* ---------- Delegation Source Banner ---------- */
320
+
321
+ interface DelegationSourceBannerProps {
322
+ delegatorName: string
323
+ delegatorAvatarSeed: string | null
324
+ taskTitle: string
325
+ taskId: string | null
326
+ description: string
327
+ }
328
+
329
+ export function DelegationSourceBanner({ delegatorName, delegatorAvatarSeed, taskTitle, taskId, description }: DelegationSourceBannerProps) {
330
+ const handleTaskClick = () => {
331
+ if (!taskId) return
332
+ const store = useAppStore.getState()
333
+ store.loadTasks(true).then(() => {
334
+ store.setTaskSheetViewOnly(true)
335
+ store.setEditingTaskId(taskId)
336
+ store.setTaskSheetOpen(true)
337
+ })
338
+ }
339
+
340
+ return (
341
+ <div
342
+ className="rounded-[12px] px-3.5 py-2.5 flex items-start gap-2.5 bg-indigo-500/[0.05] border border-indigo-500/[0.12]"
343
+ style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1)' }}
344
+ >
345
+ <div className="shrink-0 mt-0.5" style={{ animation: 'delegation-handoff-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both' }}>
346
+ <AgentAvatar seed={delegatorAvatarSeed} name={delegatorName} size={24} />
347
+ </div>
348
+ <div className="flex flex-col gap-1 min-w-0 flex-1">
349
+ <span className="text-[12px] font-600 text-indigo-400">
350
+ Delegated by {delegatorName}
351
+ </span>
352
+ {taskTitle && (
353
+ taskId ? (
354
+ <button
355
+ type="button"
356
+ onClick={handleTaskClick}
357
+ className="text-[12px] text-indigo-300 hover:text-indigo-200 underline cursor-pointer bg-transparent border-none p-0 font-inherit text-left truncate"
358
+ >
359
+ {taskTitle}
360
+ </button>
361
+ ) : (
362
+ <span className="text-[12px] text-text-2 truncate">{taskTitle}</span>
363
+ )
364
+ )}
365
+ {description && (
366
+ <span className="text-[11px] text-text-3 line-clamp-2">{description}</span>
367
+ )}
368
+ </div>
369
+ </div>
370
+ )
371
+ }
@@ -49,13 +49,34 @@ export function FilePathChip({ filePath }: { filePath: string }) {
49
49
  ? serverState.framework.charAt(0).toUpperCase() + serverState.framework.slice(1)
50
50
  : null
51
51
 
52
+ const isDir = !PREVIEWABLE_EXT.test(filePath) && !filePath.includes('.')
53
+
54
+ const handleReveal = () => {
55
+ api('POST', '/files/open', { path: filePath }).catch(() => { /* best-effort */ })
56
+ }
57
+
52
58
  return (
53
59
  <span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-[8px] bg-white/[0.06] border border-white/[0.08] font-mono text-[13px]">
54
60
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/50 shrink-0">
55
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
56
- <polyline points="14 2 14 8 20 8" />
61
+ {isDir ? (
62
+ <><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" /></>
63
+ ) : (
64
+ <><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /></>
65
+ )}
57
66
  </svg>
58
67
  <span className="text-sky-400">{filePath}</span>
68
+ <button
69
+ onClick={handleReveal}
70
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] bg-white/[0.06] hover:bg-white/[0.10] text-[10px] font-600 text-text-3 hover:text-text-2 border-none transition-colors cursor-pointer"
71
+ title={isDir ? 'Open folder' : 'Reveal in file manager'}
72
+ >
73
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
74
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
75
+ <polyline points="15 3 21 3 21 9" />
76
+ <line x1="10" y1="14" x2="21" y2="3" />
77
+ </svg>
78
+ {isDir ? 'Open' : 'Reveal'}
79
+ </button>
59
80
  {canPreview && !serverState.running && (
60
81
  <a
61
82
  href={serveUrl}