@swarmclawai/swarmclaw 0.7.7 → 0.7.8
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 +10 -9
- package/package.json +1 -1
- package/src/app/api/chats/route.ts +1 -0
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/components/agents/agent-sheet.tsx +184 -14
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-header.tsx +4 -0
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/shared/settings/section-capability-policy.tsx +38 -0
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/tasks/approvals-panel.tsx +177 -18
- package/src/components/tasks/task-board.tsx +137 -23
- package/src/components/tasks/task-card.tsx +29 -0
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/lib/server/capability-router.test.ts +22 -0
- package/src/lib/server/capability-router.ts +54 -18
- package/src/lib/server/chat-execution.ts +25 -1
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.ts +99 -74
- package/src/lib/server/daemon-state.ts +83 -46
- package/src/lib/server/elevenlabs.test.ts +59 -1
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins.ts +6 -1
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-followups.test.ts +147 -2
- package/src/lib/server/queue.ts +234 -7
- package/src/lib/server/session-run-manager.ts +31 -0
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.ts +26 -1
- package/src/lib/server/session-tools/context.ts +5 -0
- package/src/lib/server/session-tools/crud.ts +265 -76
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +38 -2
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.ts +14 -2
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
- package/src/lib/server/session-tools/web.ts +153 -6
- package/src/lib/server/stream-agent-chat.test.ts +27 -2
- package/src/lib/server/stream-agent-chat.ts +104 -30
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +269 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/types/index.ts +39 -0
|
@@ -136,10 +136,12 @@ export function ChatArea() {
|
|
|
136
136
|
if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
|
|
137
137
|
setMessagesLoading(false)
|
|
138
138
|
})
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
|
|
140
|
+
const sessionAtLoad = useAppStore.getState().sessions[requestedSessionId]
|
|
141
|
+
if (sessionAtLoad?.active) {
|
|
141
142
|
useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamText: '' })
|
|
142
143
|
}
|
|
144
|
+
|
|
143
145
|
// Refresh active state from server so returning to a session restores typing indicator.
|
|
144
146
|
loadSessions().then(() => {
|
|
145
147
|
if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
|
|
@@ -148,6 +150,7 @@ export function ChatArea() {
|
|
|
148
150
|
useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamText: '' })
|
|
149
151
|
}
|
|
150
152
|
}).catch((err) => console.error('Failed to refresh messages:', err))
|
|
153
|
+
|
|
151
154
|
devServer(requestedSessionId, 'status').then((r) => {
|
|
152
155
|
if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
|
|
153
156
|
setDevServer(r.running ? r : null)
|
|
@@ -155,23 +158,31 @@ export function ChatArea() {
|
|
|
155
158
|
if (cancelled || useAppStore.getState().currentSessionId !== requestedSessionId) return
|
|
156
159
|
setDevServer(null)
|
|
157
160
|
})
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
} else {
|
|
161
|
+
|
|
162
|
+
return () => {
|
|
163
|
+
cancelled = true
|
|
164
|
+
}
|
|
165
|
+
}, [loadSessions, sessionId, setDevServer, setMessages])
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!sessionId) return
|
|
169
|
+
let cancelled = false
|
|
170
|
+
if (!sessionHasBrowserPlugin) {
|
|
169
171
|
setBrowserActive(false)
|
|
172
|
+
return
|
|
170
173
|
}
|
|
174
|
+
checkBrowser(sessionId).then((r) => {
|
|
175
|
+
if (cancelled || useAppStore.getState().currentSessionId !== sessionId) return
|
|
176
|
+
setBrowserActive(r.active)
|
|
177
|
+
}).catch((err) => {
|
|
178
|
+
if (cancelled || useAppStore.getState().currentSessionId !== sessionId) return
|
|
179
|
+
console.error('Browser check failed:', err)
|
|
180
|
+
setBrowserActive(false)
|
|
181
|
+
})
|
|
171
182
|
return () => {
|
|
172
183
|
cancelled = true
|
|
173
184
|
}
|
|
174
|
-
}, [
|
|
185
|
+
}, [sessionHasBrowserPlugin, sessionId])
|
|
175
186
|
|
|
176
187
|
// Auto-poll messages for sessions that are actively running on the server
|
|
177
188
|
const isServerActive = session?.active === true
|
|
@@ -216,10 +227,16 @@ export function ChatArea() {
|
|
|
216
227
|
shouldPollMessages ? 2000 : undefined,
|
|
217
228
|
)
|
|
218
229
|
|
|
219
|
-
//
|
|
230
|
+
// Keep the local typing indicator aligned with the server's active state
|
|
220
231
|
useEffect(() => {
|
|
221
232
|
if (!sessionId) return
|
|
222
233
|
const state = useChatStore.getState()
|
|
234
|
+
if (isServerActive) {
|
|
235
|
+
if (!state.streaming && !state.streamText) {
|
|
236
|
+
useChatStore.setState({ streaming: true, streamingSessionId: sessionId, streamText: '' })
|
|
237
|
+
}
|
|
238
|
+
return
|
|
239
|
+
}
|
|
223
240
|
if (
|
|
224
241
|
!isServerActive
|
|
225
242
|
&& state.streaming
|
|
@@ -230,7 +247,7 @@ export function ChatArea() {
|
|
|
230
247
|
fetchMessages(sessionId).then(setMessages).catch(() => {})
|
|
231
248
|
useChatStore.setState({ streaming: false, streamingSessionId: null, streamText: '' })
|
|
232
249
|
}
|
|
233
|
-
}, [isServerActive, sessionId])
|
|
250
|
+
}, [isServerActive, sessionId, setMessages])
|
|
234
251
|
|
|
235
252
|
// Poll browser status while session has browser tools
|
|
236
253
|
const hasBrowserTool = session?.plugins?.includes('browser')
|
|
@@ -255,7 +272,7 @@ export function ChatArea() {
|
|
|
255
272
|
if (!sessionId) return
|
|
256
273
|
await devServer(sessionId, 'stop')
|
|
257
274
|
setDevServer(null)
|
|
258
|
-
}, [sessionId])
|
|
275
|
+
}, [sessionId, setDevServer])
|
|
259
276
|
|
|
260
277
|
const handleClear = useCallback(async () => {
|
|
261
278
|
setConfirmClear(false)
|
|
@@ -263,7 +280,7 @@ export function ChatArea() {
|
|
|
263
280
|
await clearMessages(sessionId)
|
|
264
281
|
setMessages([])
|
|
265
282
|
loadSessions()
|
|
266
|
-
}, [sessionId])
|
|
283
|
+
}, [loadSessions, sessionId, setMessages])
|
|
267
284
|
|
|
268
285
|
const handleDelete = useCallback(async () => {
|
|
269
286
|
setConfirmDelete(false)
|
|
@@ -271,7 +288,7 @@ export function ChatArea() {
|
|
|
271
288
|
await deleteChat(sessionId)
|
|
272
289
|
removeSessionFromStore(sessionId)
|
|
273
290
|
setCurrentSession(null)
|
|
274
|
-
}, [sessionId])
|
|
291
|
+
}, [removeSessionFromStore, sessionId, setCurrentSession])
|
|
275
292
|
|
|
276
293
|
const handlePrompt = useCallback((text: string) => {
|
|
277
294
|
sendMessage(text)
|
|
@@ -280,12 +280,16 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
280
280
|
const fromDelegateOpenCode = session.delegateResumeIds?.opencode
|
|
281
281
|
? { label: 'OpenCode', id: session.delegateResumeIds.opencode, command: `opencode run \"<task>\" --session ${session.delegateResumeIds.opencode}` }
|
|
282
282
|
: null
|
|
283
|
+
const fromDelegateGemini = session.delegateResumeIds?.gemini
|
|
284
|
+
? { label: 'Gemini', id: session.delegateResumeIds.gemini, command: `gemini --resume ${session.delegateResumeIds.gemini} --prompt \"<task>\"` }
|
|
285
|
+
: null
|
|
283
286
|
return fromSessionClaude
|
|
284
287
|
|| fromSessionCodex
|
|
285
288
|
|| fromSessionOpenCode
|
|
286
289
|
|| fromDelegateClaude
|
|
287
290
|
|| fromDelegateCodex
|
|
288
291
|
|| fromDelegateOpenCode
|
|
292
|
+
|| fromDelegateGemini
|
|
289
293
|
|| null
|
|
290
294
|
}, [session.claudeSessionId, session.codexThreadId, session.opencodeSessionId, session.delegateResumeIds])
|
|
291
295
|
|
|
@@ -23,5 +23,18 @@ describe('parseTaskCompletion', () => {
|
|
|
23
23
|
assert.equal(parsed?.reportPath, 'data/task-reports/abc12345.md')
|
|
24
24
|
assert.equal(parsed?.workingDir, '/tmp/work')
|
|
25
25
|
})
|
|
26
|
-
})
|
|
27
26
|
|
|
27
|
+
it('captures Gemini resume lines from task completion payloads', () => {
|
|
28
|
+
const text = [
|
|
29
|
+
'Task completed: **[Ship follow-up](#task:task-gemini)**',
|
|
30
|
+
'',
|
|
31
|
+
'Gemini session: `gemini-session-7`',
|
|
32
|
+
'',
|
|
33
|
+
'All done.',
|
|
34
|
+
].join('\n')
|
|
35
|
+
const parsed = parseTaskCompletion(text)
|
|
36
|
+
|
|
37
|
+
assert.ok(parsed)
|
|
38
|
+
assert.equal(parsed?.resumeInfo, 'Gemini session: `gemini-session-7`')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -169,7 +169,7 @@ export function parseTaskCompletion(text: string): TaskCompletionInfo | null {
|
|
|
169
169
|
}
|
|
170
170
|
} else if (section.startsWith('Task report: ')) {
|
|
171
171
|
reportPath = section.replace('Task report: ', '').replace(/^`|`$/g, '')
|
|
172
|
-
} else if (/^(Claude session|Codex thread|OpenCode session|CLI session):/.test(section)) {
|
|
172
|
+
} else if (/^(Claude session|Codex thread|OpenCode session|Gemini session|CLI session):/.test(section)) {
|
|
173
173
|
resumeInfo = section
|
|
174
174
|
} else if (section.trim()) {
|
|
175
175
|
resultParts.push(section)
|
|
@@ -141,6 +141,7 @@ export function AppLayout() {
|
|
|
141
141
|
const execApprovals = useApprovalStore((s) => s.approvals)
|
|
142
142
|
const loadExecApprovals = useApprovalStore((s) => s.loadApprovals)
|
|
143
143
|
const pruneExecApprovals = useApprovalStore((s) => s.pruneExpired)
|
|
144
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
144
145
|
const isDesktop = useMediaQuery('(min-width: 768px)')
|
|
145
146
|
const hasSelectedSession = !!(currentSessionId && sessions[currentSessionId])
|
|
146
147
|
|
|
@@ -168,11 +169,23 @@ export function AppLayout() {
|
|
|
168
169
|
pruneExecApprovals()
|
|
169
170
|
}, 10000)
|
|
170
171
|
|
|
171
|
-
const appSettings = useAppStore((s) => s.appSettings)
|
|
172
172
|
const [agentViewMode, setAgentViewMode] = useState<'chat' | 'config'>('chat')
|
|
173
173
|
const [profileSheetOpen, setProfileSheetOpen] = useState(false)
|
|
174
174
|
const [canvasDismissedFor, setCanvasDismissedFor] = useState<string | null>(null)
|
|
175
175
|
|
|
176
|
+
const isViewEnabled = useCallback((view: AppView) => {
|
|
177
|
+
if (view === 'projects') return appSettings.projectManagementEnabled !== false
|
|
178
|
+
if (view === 'tasks') return appSettings.taskManagementEnabled !== false
|
|
179
|
+
if (view === 'chatrooms') return plugins['chatroom']?.enabled !== false
|
|
180
|
+
if (view === 'schedules') return plugins['schedule']?.enabled !== false
|
|
181
|
+
if (view === 'memory') return plugins['memory']?.enabled !== false
|
|
182
|
+
if (view === 'connectors') return plugins['connectors']?.enabled !== false
|
|
183
|
+
if (view === 'webhooks') return plugins['http']?.enabled !== false
|
|
184
|
+
if (view === 'wallets') return plugins['wallet']?.enabled !== false
|
|
185
|
+
if (view === 'logs') return plugins['monitor']?.enabled !== false
|
|
186
|
+
return true
|
|
187
|
+
}, [appSettings.projectManagementEnabled, appSettings.taskManagementEnabled, plugins])
|
|
188
|
+
|
|
176
189
|
const handleShortcutKey = useCallback((e: KeyboardEvent) => {
|
|
177
190
|
const mod = e.metaKey || e.ctrlKey
|
|
178
191
|
// Cmd+N / Ctrl+N — jump to the default agent shortcut
|
|
@@ -188,8 +201,10 @@ export function AppLayout() {
|
|
|
188
201
|
}
|
|
189
202
|
// Cmd+Shift+T / Ctrl+Shift+T — jump to tasks
|
|
190
203
|
if (mod && e.shiftKey && e.key.toLowerCase() === 't') {
|
|
204
|
+
const state = useAppStore.getState()
|
|
205
|
+
if (state.appSettings.taskManagementEnabled === false) return
|
|
191
206
|
e.preventDefault()
|
|
192
|
-
|
|
207
|
+
state.setActiveView('tasks')
|
|
193
208
|
}
|
|
194
209
|
}, [])
|
|
195
210
|
|
|
@@ -222,6 +237,13 @@ export function AppLayout() {
|
|
|
222
237
|
}
|
|
223
238
|
}, [appSettings.themeHue])
|
|
224
239
|
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (!isViewEnabled(activeView)) {
|
|
242
|
+
setActiveView('home')
|
|
243
|
+
setSidebarOpen(false)
|
|
244
|
+
}
|
|
245
|
+
}, [activeView, isViewEnabled, setActiveView, setSidebarOpen])
|
|
246
|
+
|
|
225
247
|
const [pluginSidebarItems, setPluginSidebarItems] = useState<Array<{ id: string; label: string; href: string }>>([])
|
|
226
248
|
|
|
227
249
|
const refreshPluginState = useCallback(() => {
|
|
@@ -235,17 +257,6 @@ export function AppLayout() {
|
|
|
235
257
|
|
|
236
258
|
useWs('plugins', refreshPluginState)
|
|
237
259
|
|
|
238
|
-
const isViewEnabled = useCallback((view: AppView) => {
|
|
239
|
-
if (view === 'chatrooms') return plugins['chatroom']?.enabled !== false
|
|
240
|
-
if (view === 'schedules') return plugins['schedule']?.enabled !== false
|
|
241
|
-
if (view === 'memory') return plugins['memory']?.enabled !== false
|
|
242
|
-
if (view === 'connectors') return plugins['connectors']?.enabled !== false
|
|
243
|
-
if (view === 'webhooks') return plugins['http']?.enabled !== false
|
|
244
|
-
if (view === 'wallets') return plugins['wallet']?.enabled !== false
|
|
245
|
-
if (view === 'logs') return plugins['monitor']?.enabled !== false
|
|
246
|
-
return true
|
|
247
|
-
}, [plugins])
|
|
248
|
-
|
|
249
260
|
const [railExpanded, setRailExpanded] = useState(() => {
|
|
250
261
|
const stored = safeStorageGet(RAIL_EXPANDED_KEY)
|
|
251
262
|
return stored === null ? true : stored === 'true'
|
|
@@ -262,6 +273,7 @@ export function AppLayout() {
|
|
|
262
273
|
}
|
|
263
274
|
|
|
264
275
|
const openNewSheet = () => {
|
|
276
|
+
if (!isViewEnabled(activeView)) return
|
|
265
277
|
if (activeView === 'agents') setAgentSheetOpen(true)
|
|
266
278
|
else if (activeView === 'schedules') setScheduleSheetOpen(true)
|
|
267
279
|
else if (activeView === 'tasks') setTaskSheetOpen(true)
|
|
@@ -278,6 +290,7 @@ export function AppLayout() {
|
|
|
278
290
|
}
|
|
279
291
|
|
|
280
292
|
const handleNavClick = (view: AppView) => {
|
|
293
|
+
if (!isViewEnabled(view)) return
|
|
281
294
|
if (FULL_WIDTH_VIEWS.has(view)) {
|
|
282
295
|
setActiveView(view)
|
|
283
296
|
setSidebarOpen(false)
|
|
@@ -491,11 +504,13 @@ export function AppLayout() {
|
|
|
491
504
|
</svg>
|
|
492
505
|
</NavItem>
|
|
493
506
|
)}
|
|
494
|
-
|
|
495
|
-
<
|
|
496
|
-
<
|
|
497
|
-
|
|
498
|
-
|
|
507
|
+
{isViewEnabled('projects') && (
|
|
508
|
+
<NavItem view="projects" label="Projects" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('projects')}>
|
|
509
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
510
|
+
<path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" /><path d="M14 2v7h7" />
|
|
511
|
+
</svg>
|
|
512
|
+
</NavItem>
|
|
513
|
+
)}
|
|
499
514
|
</div>
|
|
500
515
|
|
|
501
516
|
<div className={`flex flex-col gap-0.5 ${railExpanded ? '' : 'items-center'}`}>
|
|
@@ -504,11 +519,13 @@ export function AppLayout() {
|
|
|
504
519
|
) : (
|
|
505
520
|
<div className="my-1 h-px w-6 bg-white/[0.06]" />
|
|
506
521
|
)}
|
|
507
|
-
|
|
508
|
-
<
|
|
509
|
-
<
|
|
510
|
-
|
|
511
|
-
|
|
522
|
+
{isViewEnabled('tasks') && (
|
|
523
|
+
<NavItem view="tasks" label="Tasks" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('tasks')} badge={pendingApprovalCount}>
|
|
524
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
525
|
+
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" /><rect x="9" y="3" width="6" height="4" rx="1" /><path d="M9 14l2 2 4-4" />
|
|
526
|
+
</svg>
|
|
527
|
+
</NavItem>
|
|
528
|
+
)}
|
|
512
529
|
<NavItem view="approvals" label="Approvals" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('approvals')} badge={pendingApprovalCount}>
|
|
513
530
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
514
531
|
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
|
|
@@ -16,6 +16,13 @@ function relativeDate(ts: number): string {
|
|
|
16
16
|
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function formatHeartbeatInterval(intervalSec?: number | null): string {
|
|
20
|
+
if (!intervalSec || intervalSec <= 0) return 'Manual'
|
|
21
|
+
if (intervalSec % 3600 === 0) return `${intervalSec / 3600}h`
|
|
22
|
+
if (intervalSec % 60 === 0) return `${intervalSec / 60}m`
|
|
23
|
+
return `${intervalSec}s`
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
const STATUS_STYLES: Record<string, string> = {
|
|
20
27
|
backlog: 'bg-white/[0.06] text-text-3',
|
|
21
28
|
queued: 'bg-amber-500/15 text-amber-400',
|
|
@@ -92,6 +99,8 @@ export function ProjectDetail() {
|
|
|
92
99
|
const tasks = useAppStore((s) => s.tasks) as Record<string, BoardTask>
|
|
93
100
|
const schedules = useAppStore((s) => s.schedules) as Record<string, Schedule>
|
|
94
101
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
102
|
+
const secrets = useAppStore((s) => s.secrets)
|
|
103
|
+
const loadSecrets = useAppStore((s) => s.loadSecrets)
|
|
95
104
|
const setEditingProjectId = useAppStore((s) => s.setEditingProjectId)
|
|
96
105
|
const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
|
|
97
106
|
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
@@ -100,6 +109,8 @@ export function ProjectDetail() {
|
|
|
100
109
|
const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
|
|
101
110
|
const setEditingScheduleId = useAppStore((s) => s.setEditingScheduleId)
|
|
102
111
|
const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
|
|
112
|
+
const setEditingSecretId = useAppStore((s) => s.setEditingSecretId)
|
|
113
|
+
const setSecretSheetOpen = useAppStore((s) => s.setSecretSheetOpen)
|
|
103
114
|
|
|
104
115
|
const [assignPickerOpen, setAssignPickerOpen] = useState(false)
|
|
105
116
|
const [now, setNow] = useState(() => Date.now())
|
|
@@ -109,6 +120,11 @@ export function ProjectDetail() {
|
|
|
109
120
|
return () => window.clearInterval(intervalId)
|
|
110
121
|
}, [])
|
|
111
122
|
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!activeProjectFilter) return
|
|
125
|
+
void loadSecrets()
|
|
126
|
+
}, [activeProjectFilter, loadSecrets])
|
|
127
|
+
|
|
112
128
|
const project = activeProjectFilter ? projects[activeProjectFilter] : null
|
|
113
129
|
|
|
114
130
|
const projectAgents = useMemo(
|
|
@@ -128,6 +144,11 @@ export function ProjectDetail() {
|
|
|
128
144
|
[schedules, activeProjectFilter],
|
|
129
145
|
)
|
|
130
146
|
|
|
147
|
+
const projectSecrets = useMemo(
|
|
148
|
+
() => Object.values(secrets).filter((secret) => secret.projectId === activeProjectFilter),
|
|
149
|
+
[secrets, activeProjectFilter],
|
|
150
|
+
)
|
|
151
|
+
|
|
131
152
|
const completedTasks = projectTasks.filter((t) => t.status === 'completed').length
|
|
132
153
|
const totalTasks = projectTasks.length
|
|
133
154
|
const progressPct = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
|
|
@@ -188,6 +209,23 @@ export function ProjectDetail() {
|
|
|
188
209
|
[now, projectSchedules],
|
|
189
210
|
)
|
|
190
211
|
|
|
212
|
+
const capabilityHints = Array.isArray(project?.capabilityHints) ? project.capabilityHints : []
|
|
213
|
+
const priorities = Array.isArray(project?.priorities) ? project.priorities : []
|
|
214
|
+
const openObjectives = Array.isArray(project?.openObjectives) ? project.openObjectives : []
|
|
215
|
+
const credentialRequirements = Array.isArray(project?.credentialRequirements) ? project.credentialRequirements : []
|
|
216
|
+
const successMetrics = Array.isArray(project?.successMetrics) ? project.successMetrics : []
|
|
217
|
+
const hasOperatingContext = Boolean(
|
|
218
|
+
project?.objective
|
|
219
|
+
|| project?.audience
|
|
220
|
+
|| priorities.length
|
|
221
|
+
|| openObjectives.length
|
|
222
|
+
|| capabilityHints.length
|
|
223
|
+
|| credentialRequirements.length
|
|
224
|
+
|| successMetrics.length
|
|
225
|
+
|| project?.heartbeatPrompt
|
|
226
|
+
|| project?.heartbeatIntervalSec,
|
|
227
|
+
)
|
|
228
|
+
|
|
191
229
|
const busiestAgent = useMemo(() => {
|
|
192
230
|
return projectAgents
|
|
193
231
|
.map((agent) => ({
|
|
@@ -305,6 +343,185 @@ export function ProjectDetail() {
|
|
|
305
343
|
</div>
|
|
306
344
|
</div>
|
|
307
345
|
|
|
346
|
+
<div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_280px] gap-4 mb-8">
|
|
347
|
+
<div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] px-5 py-5">
|
|
348
|
+
<div className="flex items-center justify-between gap-4 mb-4">
|
|
349
|
+
<div>
|
|
350
|
+
<h2 className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">Project Operating System</h2>
|
|
351
|
+
<p className="text-[12px] text-text-3/60 mt-1">Define what this project is trying to achieve, how agents should operate, and what long-lived context matters.</p>
|
|
352
|
+
</div>
|
|
353
|
+
<button
|
|
354
|
+
onClick={() => { setEditingProjectId(project.id); setProjectSheetOpen(true) }}
|
|
355
|
+
className="shrink-0 px-3 py-2 rounded-[10px] bg-white/[0.04] text-[12px] font-600 text-text-2 hover:bg-white/[0.08] transition-all cursor-pointer border-none"
|
|
356
|
+
style={{ fontFamily: 'inherit' }}
|
|
357
|
+
>
|
|
358
|
+
Configure
|
|
359
|
+
</button>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{hasOperatingContext ? (
|
|
363
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
364
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-surface/60 px-4 py-3.5">
|
|
365
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/50 mb-2">Mission</div>
|
|
366
|
+
<div className="space-y-3">
|
|
367
|
+
<div>
|
|
368
|
+
<div className="text-[11px] font-600 text-text-3/50 mb-1">Objective</div>
|
|
369
|
+
<p className="text-[13px] text-text leading-relaxed">{project.objective || 'Add a durable objective for this project.'}</p>
|
|
370
|
+
</div>
|
|
371
|
+
<div>
|
|
372
|
+
<div className="text-[11px] font-600 text-text-3/50 mb-1">Audience</div>
|
|
373
|
+
<p className="text-[13px] text-text-2/80 leading-relaxed">{project.audience || 'Set who this project is for so agents can answer from that context.'}</p>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-surface/60 px-4 py-3.5">
|
|
379
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/50 mb-2">Execution Focus</div>
|
|
380
|
+
<div className="space-y-3">
|
|
381
|
+
<div>
|
|
382
|
+
<div className="text-[11px] font-600 text-text-3/50 mb-1">Pilot priorities</div>
|
|
383
|
+
{priorities.length > 0 ? (
|
|
384
|
+
<div className="flex flex-wrap gap-1.5">
|
|
385
|
+
{priorities.map((priority) => (
|
|
386
|
+
<span key={priority} className="rounded-full bg-accent-soft px-2.5 py-1 text-[11px] font-600 text-accent-bright">
|
|
387
|
+
{priority}
|
|
388
|
+
</span>
|
|
389
|
+
))}
|
|
390
|
+
</div>
|
|
391
|
+
) : (
|
|
392
|
+
<p className="text-[12px] text-text-3/50">No priorities captured yet.</p>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
<div>
|
|
396
|
+
<div className="text-[11px] font-600 text-text-3/50 mb-1">Open objectives</div>
|
|
397
|
+
{openObjectives.length > 0 ? (
|
|
398
|
+
<div className="space-y-1.5">
|
|
399
|
+
{openObjectives.map((objective) => (
|
|
400
|
+
<div key={objective} className="flex items-start gap-2 text-[12px] text-text-2">
|
|
401
|
+
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-emerald-400" />
|
|
402
|
+
<span>{objective}</span>
|
|
403
|
+
</div>
|
|
404
|
+
))}
|
|
405
|
+
</div>
|
|
406
|
+
) : (
|
|
407
|
+
<p className="text-[12px] text-text-3/50">No open objectives captured yet.</p>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-surface/60 px-4 py-3.5">
|
|
414
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/50 mb-2">Operating Modes</div>
|
|
415
|
+
{capabilityHints.length > 0 ? (
|
|
416
|
+
<div className="flex flex-wrap gap-1.5">
|
|
417
|
+
{capabilityHints.map((hint) => (
|
|
418
|
+
<span key={hint} className="rounded-full bg-white/[0.06] px-2.5 py-1 text-[11px] font-600 text-text-2">
|
|
419
|
+
{hint}
|
|
420
|
+
</span>
|
|
421
|
+
))}
|
|
422
|
+
</div>
|
|
423
|
+
) : (
|
|
424
|
+
<p className="text-[12px] text-text-3/50">No capability hints yet. Add things like research, build, browsing, inbox ops, or credential bootstrapping.</p>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-surface/60 px-4 py-3.5">
|
|
429
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-text-3/50 mb-2">Success Metrics</div>
|
|
430
|
+
{successMetrics.length > 0 ? (
|
|
431
|
+
<div className="space-y-1.5">
|
|
432
|
+
{successMetrics.map((metric) => (
|
|
433
|
+
<div key={metric} className="flex items-start gap-2 text-[12px] text-text-2">
|
|
434
|
+
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-sky-400" />
|
|
435
|
+
<span>{metric}</span>
|
|
436
|
+
</div>
|
|
437
|
+
))}
|
|
438
|
+
</div>
|
|
439
|
+
) : (
|
|
440
|
+
<p className="text-[12px] text-text-3/50">Define success metrics if this project has open-ended goals like revenue, outreach, or inbox response quality.</p>
|
|
441
|
+
)}
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
) : (
|
|
445
|
+
<div className="rounded-[12px] border border-dashed border-white/[0.08] px-5 py-6 text-center">
|
|
446
|
+
<p className="text-[13px] font-600 text-text-2">This project still needs operating context.</p>
|
|
447
|
+
<p className="mt-1 text-[12px] text-text-3/55">Add objective, open objectives, capability hints, credential needs, and heartbeat settings so agents can treat the project as a durable operating system.</p>
|
|
448
|
+
</div>
|
|
449
|
+
)}
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] px-5 py-5">
|
|
453
|
+
<div className="flex items-center justify-between mb-3">
|
|
454
|
+
<h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Ops Readiness</h3>
|
|
455
|
+
<span className="text-[11px] text-text-3/40">{projectSecrets.length} secret{projectSecrets.length === 1 ? '' : 's'}</span>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<div className="grid grid-cols-2 gap-2 mb-4">
|
|
459
|
+
{[
|
|
460
|
+
{ label: 'Linked secrets', value: projectSecrets.length, tone: 'text-emerald-400' },
|
|
461
|
+
{ label: 'Credential reqs', value: credentialRequirements.length, tone: 'text-amber-400' },
|
|
462
|
+
{ label: 'Heartbeat', value: project.heartbeatIntervalSec ? formatHeartbeatInterval(project.heartbeatIntervalSec) : 'Off', tone: 'text-sky-400' },
|
|
463
|
+
{ label: 'Schedules', value: projectSchedules.length, tone: 'text-text-2' },
|
|
464
|
+
].map((item) => (
|
|
465
|
+
<div key={item.label} className="rounded-[12px] border border-white/[0.06] bg-surface/60 px-3 py-3">
|
|
466
|
+
<div className={`text-[18px] font-display font-700 tracking-[-0.02em] ${item.tone}`}>{item.value}</div>
|
|
467
|
+
<div className="mt-1 text-[10px] font-600 uppercase tracking-[0.08em] text-text-3/45">{item.label}</div>
|
|
468
|
+
</div>
|
|
469
|
+
))}
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
<div className="space-y-3">
|
|
473
|
+
<div>
|
|
474
|
+
<div className="text-[11px] font-600 text-text-3/50 mb-1">Credential requirements</div>
|
|
475
|
+
{credentialRequirements.length > 0 ? (
|
|
476
|
+
<div className="space-y-1.5">
|
|
477
|
+
{credentialRequirements.map((item) => (
|
|
478
|
+
<div key={item} className="flex items-start gap-2 text-[12px] text-text-2">
|
|
479
|
+
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-amber-400" />
|
|
480
|
+
<span>{item}</span>
|
|
481
|
+
</div>
|
|
482
|
+
))}
|
|
483
|
+
</div>
|
|
484
|
+
) : (
|
|
485
|
+
<p className="text-[12px] text-text-3/50">No credentials requested yet.</p>
|
|
486
|
+
)}
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<div>
|
|
490
|
+
<div className="text-[11px] font-600 text-text-3/50 mb-1">Heartbeat</div>
|
|
491
|
+
{project.heartbeatPrompt || project.heartbeatIntervalSec ? (
|
|
492
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-surface/60 px-3 py-3">
|
|
493
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.08em] text-sky-400">
|
|
494
|
+
Every {formatHeartbeatInterval(project.heartbeatIntervalSec)}
|
|
495
|
+
</div>
|
|
496
|
+
<p className="mt-1 text-[12px] text-text-2 leading-relaxed">
|
|
497
|
+
{project.heartbeatPrompt || 'No heartbeat prompt configured.'}
|
|
498
|
+
</p>
|
|
499
|
+
</div>
|
|
500
|
+
) : (
|
|
501
|
+
<p className="text-[12px] text-text-3/50">No project heartbeat configured.</p>
|
|
502
|
+
)}
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<div className="flex flex-wrap gap-2 pt-1">
|
|
506
|
+
<button
|
|
507
|
+
onClick={() => { setEditingSecretId(null); setSecretSheetOpen(true) }}
|
|
508
|
+
className="px-3 py-2 rounded-[10px] bg-accent-soft text-[12px] font-600 text-accent-bright hover:bg-accent-bright/15 transition-all cursor-pointer border-none"
|
|
509
|
+
style={{ fontFamily: 'inherit' }}
|
|
510
|
+
>
|
|
511
|
+
Add project secret
|
|
512
|
+
</button>
|
|
513
|
+
<button
|
|
514
|
+
onClick={() => { setEditingScheduleId(null); setScheduleSheetOpen(true) }}
|
|
515
|
+
className="px-3 py-2 rounded-[10px] bg-white/[0.04] text-[12px] font-600 text-text-2 hover:bg-white/[0.08] transition-all cursor-pointer border-none"
|
|
516
|
+
style={{ fontFamily: 'inherit' }}
|
|
517
|
+
>
|
|
518
|
+
Add heartbeat schedule
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
308
525
|
<div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_280px] gap-4 mb-8">
|
|
309
526
|
<div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] px-5 py-5">
|
|
310
527
|
<div className="flex items-center justify-between gap-4 mb-4">
|