@swarmclawai/swarmclaw 0.7.6 → 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 +19 -10
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +13 -1
- package/src/app/api/connectors/[id]/route.ts +20 -2
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +139 -0
- 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/cli/index.js +40 -0
- package/src/cli/index.test.js +68 -0
- package/src/cli/spec.js +60 -0
- package/src/components/agents/agent-sheet.tsx +281 -33
- package/src/components/auth/setup-wizard.tsx +75 -2
- 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/gateways/gateway-sheet.tsx +140 -8
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +221 -17
- 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/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- 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 +33 -3
- 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/openclaw-deploy.test.ts +8 -0
- package/src/lib/server/openclaw-deploy.ts +679 -19
- package/src/lib/server/orchestrator-lg.ts +1 -0
- package/src/lib/server/orchestrator.ts +11 -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 +278 -8
- 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/setup-defaults.ts +2 -2
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +104 -0
|
@@ -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">
|
|
@@ -12,6 +12,25 @@ const PROJECT_COLORS = [
|
|
|
12
12
|
]
|
|
13
13
|
|
|
14
14
|
const inputClass = 'w-full px-3 py-2.5 rounded-lg bg-white/[0.06] border border-white/[0.06] text-[13px] text-text-1 placeholder:text-text-3/40 focus:outline-none focus:border-accent/40 transition-colors'
|
|
15
|
+
const sectionTitleClass = 'block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2'
|
|
16
|
+
|
|
17
|
+
function listToText(values?: string[]) {
|
|
18
|
+
return Array.isArray(values) ? values.join('\n') : ''
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function textToList(value: string) {
|
|
22
|
+
return value
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map((entry) => entry.trim())
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseOptionalInteger(value: string) {
|
|
29
|
+
const trimmed = value.trim()
|
|
30
|
+
if (!trimmed) return undefined
|
|
31
|
+
const parsed = Number.parseInt(trimmed, 10)
|
|
32
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
33
|
+
}
|
|
15
34
|
|
|
16
35
|
export function ProjectSheet() {
|
|
17
36
|
const open = useAppStore((s) => s.projectSheetOpen)
|
|
@@ -24,6 +43,15 @@ export function ProjectSheet() {
|
|
|
24
43
|
const [name, setName] = useState('')
|
|
25
44
|
const [description, setDescription] = useState('')
|
|
26
45
|
const [color, setColor] = useState<string | undefined>(undefined)
|
|
46
|
+
const [objective, setObjective] = useState('')
|
|
47
|
+
const [audience, setAudience] = useState('')
|
|
48
|
+
const [prioritiesText, setPrioritiesText] = useState('')
|
|
49
|
+
const [openObjectivesText, setOpenObjectivesText] = useState('')
|
|
50
|
+
const [capabilityHintsText, setCapabilityHintsText] = useState('')
|
|
51
|
+
const [credentialRequirementsText, setCredentialRequirementsText] = useState('')
|
|
52
|
+
const [successMetricsText, setSuccessMetricsText] = useState('')
|
|
53
|
+
const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
|
|
54
|
+
const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('')
|
|
27
55
|
|
|
28
56
|
const editing = editingId ? projects[editingId] : null
|
|
29
57
|
|
|
@@ -33,10 +61,28 @@ export function ProjectSheet() {
|
|
|
33
61
|
setName(editing.name)
|
|
34
62
|
setDescription(editing.description)
|
|
35
63
|
setColor(editing.color)
|
|
64
|
+
setObjective(editing.objective || '')
|
|
65
|
+
setAudience(editing.audience || '')
|
|
66
|
+
setPrioritiesText(listToText(editing.priorities))
|
|
67
|
+
setOpenObjectivesText(listToText(editing.openObjectives))
|
|
68
|
+
setCapabilityHintsText(listToText(editing.capabilityHints))
|
|
69
|
+
setCredentialRequirementsText(listToText(editing.credentialRequirements))
|
|
70
|
+
setSuccessMetricsText(listToText(editing.successMetrics))
|
|
71
|
+
setHeartbeatPrompt(editing.heartbeatPrompt || '')
|
|
72
|
+
setHeartbeatIntervalSec(editing.heartbeatIntervalSec ? String(editing.heartbeatIntervalSec) : '')
|
|
36
73
|
} else {
|
|
37
74
|
setName('')
|
|
38
75
|
setDescription('')
|
|
39
76
|
setColor(PROJECT_COLORS[0])
|
|
77
|
+
setObjective('')
|
|
78
|
+
setAudience('')
|
|
79
|
+
setPrioritiesText('')
|
|
80
|
+
setOpenObjectivesText('')
|
|
81
|
+
setCapabilityHintsText('')
|
|
82
|
+
setCredentialRequirementsText('')
|
|
83
|
+
setSuccessMetricsText('')
|
|
84
|
+
setHeartbeatPrompt('')
|
|
85
|
+
setHeartbeatIntervalSec('')
|
|
40
86
|
}
|
|
41
87
|
}
|
|
42
88
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -52,6 +98,15 @@ export function ProjectSheet() {
|
|
|
52
98
|
name: name.trim() || 'Unnamed Project',
|
|
53
99
|
description,
|
|
54
100
|
color,
|
|
101
|
+
objective: objective.trim() || undefined,
|
|
102
|
+
audience: audience.trim() || undefined,
|
|
103
|
+
priorities: textToList(prioritiesText),
|
|
104
|
+
openObjectives: textToList(openObjectivesText),
|
|
105
|
+
capabilityHints: textToList(capabilityHintsText),
|
|
106
|
+
credentialRequirements: textToList(credentialRequirementsText),
|
|
107
|
+
successMetrics: textToList(successMetricsText),
|
|
108
|
+
heartbeatPrompt: heartbeatPrompt.trim() || undefined,
|
|
109
|
+
heartbeatIntervalSec: parseOptionalInteger(heartbeatIntervalSec),
|
|
55
110
|
}
|
|
56
111
|
if (editing) {
|
|
57
112
|
await updateProject(editing.id, data)
|
|
@@ -72,10 +127,10 @@ export function ProjectSheet() {
|
|
|
72
127
|
}
|
|
73
128
|
|
|
74
129
|
return (
|
|
75
|
-
<BottomSheet open={open} onClose={onClose}>
|
|
130
|
+
<BottomSheet open={open} onClose={onClose} wide>
|
|
76
131
|
<h2 className="font-display text-[18px] font-700 text-text mb-6">{editing ? 'Edit Project' : 'New Project'}</h2>
|
|
77
132
|
<div className="mb-6">
|
|
78
|
-
<label className=
|
|
133
|
+
<label className={sectionTitleClass}>Name</label>
|
|
79
134
|
<input
|
|
80
135
|
type="text"
|
|
81
136
|
value={name}
|
|
@@ -88,7 +143,7 @@ export function ProjectSheet() {
|
|
|
88
143
|
</div>
|
|
89
144
|
|
|
90
145
|
<div className="mb-6">
|
|
91
|
-
<label className=
|
|
146
|
+
<label className={sectionTitleClass}>Description</label>
|
|
92
147
|
<textarea
|
|
93
148
|
value={description}
|
|
94
149
|
onChange={(e) => setDescription(e.target.value)}
|
|
@@ -99,8 +154,125 @@ export function ProjectSheet() {
|
|
|
99
154
|
/>
|
|
100
155
|
</div>
|
|
101
156
|
|
|
157
|
+
<div className="grid gap-6 sm:grid-cols-2 mb-6">
|
|
158
|
+
<div>
|
|
159
|
+
<label className={sectionTitleClass}>Objective</label>
|
|
160
|
+
<textarea
|
|
161
|
+
value={objective}
|
|
162
|
+
onChange={(e) => setObjective(e.target.value)}
|
|
163
|
+
placeholder="What durable outcome is this project driving?"
|
|
164
|
+
className={inputClass + ' min-h-[88px] resize-y'}
|
|
165
|
+
style={{ fontFamily: 'inherit' }}
|
|
166
|
+
rows={4}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
<div>
|
|
170
|
+
<label className={sectionTitleClass}>Audience</label>
|
|
171
|
+
<textarea
|
|
172
|
+
value={audience}
|
|
173
|
+
onChange={(e) => setAudience(e.target.value)}
|
|
174
|
+
placeholder="Who is this project for?"
|
|
175
|
+
className={inputClass + ' min-h-[88px] resize-y'}
|
|
176
|
+
style={{ fontFamily: 'inherit' }}
|
|
177
|
+
rows={4}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="grid gap-6 sm:grid-cols-2 mb-6">
|
|
183
|
+
<div>
|
|
184
|
+
<label className={sectionTitleClass}>Pilot Priorities</label>
|
|
185
|
+
<textarea
|
|
186
|
+
value={prioritiesText}
|
|
187
|
+
onChange={(e) => setPrioritiesText(e.target.value)}
|
|
188
|
+
placeholder={'One per line\nResearch the market\nBuild the pilot'}
|
|
189
|
+
className={inputClass + ' min-h-[110px] resize-y'}
|
|
190
|
+
style={{ fontFamily: 'inherit' }}
|
|
191
|
+
rows={5}
|
|
192
|
+
/>
|
|
193
|
+
<p className="mt-2 text-[11px] text-text-3/45">One priority per line.</p>
|
|
194
|
+
</div>
|
|
195
|
+
<div>
|
|
196
|
+
<label className={sectionTitleClass}>Open Objectives</label>
|
|
197
|
+
<textarea
|
|
198
|
+
value={openObjectivesText}
|
|
199
|
+
onChange={(e) => setOpenObjectivesText(e.target.value)}
|
|
200
|
+
placeholder={'One per line\nDraft the research brief\nPrepare the rollout checklist'}
|
|
201
|
+
className={inputClass + ' min-h-[110px] resize-y'}
|
|
202
|
+
style={{ fontFamily: 'inherit' }}
|
|
203
|
+
rows={5}
|
|
204
|
+
/>
|
|
205
|
+
<p className="mt-2 text-[11px] text-text-3/45">Use this for durable next outcomes, not one-off chat prompts.</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div className="grid gap-6 sm:grid-cols-2 mb-6">
|
|
210
|
+
<div>
|
|
211
|
+
<label className={sectionTitleClass}>Capability Hints</label>
|
|
212
|
+
<textarea
|
|
213
|
+
value={capabilityHintsText}
|
|
214
|
+
onChange={(e) => setCapabilityHintsText(e.target.value)}
|
|
215
|
+
placeholder={'One per line\nResearch\nWeb browsing\nInbox automation'}
|
|
216
|
+
className={inputClass + ' min-h-[110px] resize-y'}
|
|
217
|
+
style={{ fontFamily: 'inherit' }}
|
|
218
|
+
rows={5}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
<div>
|
|
222
|
+
<label className={sectionTitleClass}>Credential Requirements</label>
|
|
223
|
+
<textarea
|
|
224
|
+
value={credentialRequirementsText}
|
|
225
|
+
onChange={(e) => setCredentialRequirementsText(e.target.value)}
|
|
226
|
+
placeholder={'One per line\nGmail app password\nCRM API token'}
|
|
227
|
+
className={inputClass + ' min-h-[110px] resize-y'}
|
|
228
|
+
style={{ fontFamily: 'inherit' }}
|
|
229
|
+
rows={5}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div className="grid gap-6 sm:grid-cols-2 mb-6">
|
|
235
|
+
<div>
|
|
236
|
+
<label className={sectionTitleClass}>Success Metrics</label>
|
|
237
|
+
<textarea
|
|
238
|
+
value={successMetricsText}
|
|
239
|
+
onChange={(e) => setSuccessMetricsText(e.target.value)}
|
|
240
|
+
placeholder={'One per line\nReduce response time below 10 minutes\nIncrease qualified replies'}
|
|
241
|
+
className={inputClass + ' min-h-[96px] resize-y'}
|
|
242
|
+
style={{ fontFamily: 'inherit' }}
|
|
243
|
+
rows={4}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="grid gap-4">
|
|
247
|
+
<div>
|
|
248
|
+
<label className={sectionTitleClass}>Heartbeat Prompt</label>
|
|
249
|
+
<textarea
|
|
250
|
+
value={heartbeatPrompt}
|
|
251
|
+
onChange={(e) => setHeartbeatPrompt(e.target.value)}
|
|
252
|
+
placeholder="What should the project heartbeat ask the agent to review?"
|
|
253
|
+
className={inputClass + ' min-h-[72px] resize-y'}
|
|
254
|
+
style={{ fontFamily: 'inherit' }}
|
|
255
|
+
rows={3}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
<div>
|
|
259
|
+
<label className={sectionTitleClass}>Heartbeat Interval (seconds)</label>
|
|
260
|
+
<input
|
|
261
|
+
type="number"
|
|
262
|
+
min={0}
|
|
263
|
+
step={60}
|
|
264
|
+
value={heartbeatIntervalSec}
|
|
265
|
+
onChange={(e) => setHeartbeatIntervalSec(e.target.value)}
|
|
266
|
+
placeholder="1800"
|
|
267
|
+
className={inputClass}
|
|
268
|
+
style={{ fontFamily: 'inherit' }}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
102
274
|
<div className="mb-8">
|
|
103
|
-
<label className=
|
|
275
|
+
<label className={sectionTitleClass}>Color</label>
|
|
104
276
|
<div className="flex items-center gap-2">
|
|
105
277
|
{PROJECT_COLORS.map((c) => (
|
|
106
278
|
<button
|