@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.
Files changed (86) hide show
  1. package/README.md +19 -10
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +16 -0
  4. package/src/app/api/agents/route.ts +2 -0
  5. package/src/app/api/chats/[id]/route.ts +21 -1
  6. package/src/app/api/chats/route.ts +13 -1
  7. package/src/app/api/connectors/[id]/route.ts +20 -2
  8. package/src/app/api/connectors/route.ts +12 -8
  9. package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
  10. package/src/app/api/external-agents/[id]/route.ts +38 -6
  11. package/src/app/api/external-agents/route.ts +17 -1
  12. package/src/app/api/gateways/[id]/health/route.ts +8 -0
  13. package/src/app/api/gateways/[id]/route.ts +53 -1
  14. package/src/app/api/gateways/route.ts +53 -0
  15. package/src/app/api/openclaw/deploy/route.ts +139 -0
  16. package/src/app/api/projects/[id]/route.ts +6 -2
  17. package/src/app/api/projects/route.ts +4 -3
  18. package/src/app/api/secrets/[id]/route.ts +1 -0
  19. package/src/app/api/secrets/route.ts +2 -1
  20. package/src/app/api/settings/route.ts +2 -0
  21. package/src/cli/index.js +40 -0
  22. package/src/cli/index.test.js +68 -0
  23. package/src/cli/spec.js +60 -0
  24. package/src/components/agents/agent-sheet.tsx +281 -33
  25. package/src/components/auth/setup-wizard.tsx +75 -2
  26. package/src/components/chat/chat-area.tsx +36 -19
  27. package/src/components/chat/chat-header.tsx +4 -0
  28. package/src/components/chat/delegation-banner.test.ts +14 -1
  29. package/src/components/chat/delegation-banner.tsx +1 -1
  30. package/src/components/gateways/gateway-sheet.tsx +140 -8
  31. package/src/components/layout/app-layout.tsx +40 -23
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
  33. package/src/components/projects/project-detail.tsx +217 -0
  34. package/src/components/projects/project-sheet.tsx +176 -4
  35. package/src/components/providers/provider-list.tsx +221 -17
  36. package/src/components/shared/settings/section-capability-policy.tsx +38 -0
  37. package/src/components/shared/settings/section-voice.tsx +11 -3
  38. package/src/components/tasks/approvals-panel.tsx +177 -18
  39. package/src/components/tasks/task-board.tsx +137 -23
  40. package/src/components/tasks/task-card.tsx +29 -0
  41. package/src/components/tasks/task-sheet.tsx +16 -4
  42. package/src/lib/server/agent-runtime-config.ts +142 -7
  43. package/src/lib/server/agent-thread-session.ts +9 -1
  44. package/src/lib/server/capability-router.test.ts +22 -0
  45. package/src/lib/server/capability-router.ts +54 -18
  46. package/src/lib/server/chat-execution.ts +33 -3
  47. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  48. package/src/lib/server/connectors/manager.ts +99 -74
  49. package/src/lib/server/daemon-state.ts +83 -46
  50. package/src/lib/server/elevenlabs.test.ts +59 -1
  51. package/src/lib/server/heartbeat-service.ts +5 -1
  52. package/src/lib/server/main-agent-loop.test.ts +260 -0
  53. package/src/lib/server/main-agent-loop.ts +559 -14
  54. package/src/lib/server/openclaw-deploy.test.ts +8 -0
  55. package/src/lib/server/openclaw-deploy.ts +679 -19
  56. package/src/lib/server/orchestrator-lg.ts +1 -0
  57. package/src/lib/server/orchestrator.ts +11 -0
  58. package/src/lib/server/plugins.ts +6 -1
  59. package/src/lib/server/project-context.ts +162 -0
  60. package/src/lib/server/project-utils.ts +150 -0
  61. package/src/lib/server/queue-followups.test.ts +147 -2
  62. package/src/lib/server/queue.ts +278 -8
  63. package/src/lib/server/session-run-manager.ts +31 -0
  64. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  65. package/src/lib/server/session-tools/connector.ts +26 -1
  66. package/src/lib/server/session-tools/context.ts +5 -0
  67. package/src/lib/server/session-tools/crud.ts +265 -76
  68. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  69. package/src/lib/server/session-tools/delegate.ts +38 -2
  70. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  71. package/src/lib/server/session-tools/memory.ts +14 -2
  72. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  73. package/src/lib/server/session-tools/platform.ts +60 -19
  74. package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
  75. package/src/lib/server/session-tools/web.ts +153 -6
  76. package/src/lib/server/stream-agent-chat.test.ts +27 -2
  77. package/src/lib/server/stream-agent-chat.ts +104 -30
  78. package/src/lib/server/tool-aliases.ts +2 -0
  79. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  80. package/src/lib/server/tool-capability-policy.ts +29 -1
  81. package/src/lib/server/tool-planning.test.ts +44 -0
  82. package/src/lib/server/tool-planning.ts +269 -0
  83. package/src/lib/setup-defaults.ts +2 -2
  84. package/src/lib/tool-definitions.ts +2 -1
  85. package/src/lib/validation/schemas.ts +9 -0
  86. 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="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Name</label>
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="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Description</label>
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="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Color</label>
275
+ <label className={sectionTitleClass}>Color</label>
104
276
  <div className="flex items-center gap-2">
105
277
  {PROJECT_COLORS.map((c) => (
106
278
  <button