@swarmclawai/swarmclaw 0.6.7 → 0.6.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 (73) hide show
  1. package/README.md +24 -6
  2. package/package.json +1 -1
  3. package/src/app/api/agents/route.ts +1 -0
  4. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  5. package/src/app/api/eval/run/route.ts +37 -0
  6. package/src/app/api/eval/scenarios/route.ts +24 -0
  7. package/src/app/api/eval/suite/route.ts +29 -0
  8. package/src/app/api/memory/graph/route.ts +46 -0
  9. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  10. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  11. package/src/app/api/souls/[id]/route.ts +65 -0
  12. package/src/app/api/souls/route.ts +70 -0
  13. package/src/app/api/tasks/[id]/route.ts +5 -0
  14. package/src/app/api/tasks/route.ts +2 -0
  15. package/src/app/api/usage/route.ts +9 -2
  16. package/src/cli/index.js +24 -0
  17. package/src/components/agents/agent-sheet.tsx +27 -6
  18. package/src/components/agents/soul-library-picker.tsx +84 -13
  19. package/src/components/chat/activity-moment.tsx +2 -0
  20. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  21. package/src/components/chat/message-list.tsx +19 -3
  22. package/src/components/chat/session-debug-panel.tsx +106 -84
  23. package/src/components/chat/task-approval-card.tsx +78 -0
  24. package/src/components/chat/tool-call-bubble.tsx +3 -0
  25. package/src/components/connectors/connector-sheet.tsx +8 -1
  26. package/src/components/home/home-view.tsx +39 -15
  27. package/src/components/layout/app-layout.tsx +18 -2
  28. package/src/components/memory/memory-browser.tsx +73 -45
  29. package/src/components/memory/memory-graph-view.tsx +203 -0
  30. package/src/components/plugins/plugin-list.tsx +1 -1
  31. package/src/components/schedules/schedule-sheet.tsx +9 -2
  32. package/src/components/shared/hint-tip.tsx +31 -0
  33. package/src/components/shared/settings/section-runtime-loop.tsx +5 -4
  34. package/src/components/tasks/approvals-panel.tsx +120 -0
  35. package/src/components/usage/metrics-dashboard.tsx +25 -3
  36. package/src/lib/server/chat-execution.ts +96 -12
  37. package/src/lib/server/chatroom-helpers.ts +63 -5
  38. package/src/lib/server/chatroom-orchestration.ts +74 -0
  39. package/src/lib/server/context-manager.ts +132 -50
  40. package/src/lib/server/daemon-state.ts +70 -1
  41. package/src/lib/server/eval/runner.ts +126 -0
  42. package/src/lib/server/eval/scenarios.ts +218 -0
  43. package/src/lib/server/eval/scorer.ts +96 -0
  44. package/src/lib/server/eval/store.ts +37 -0
  45. package/src/lib/server/eval/types.ts +48 -0
  46. package/src/lib/server/execution-log.ts +12 -8
  47. package/src/lib/server/guardian.ts +34 -0
  48. package/src/lib/server/heartbeat-service.ts +53 -1
  49. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  50. package/src/lib/server/link-understanding.ts +55 -0
  51. package/src/lib/server/main-agent-loop.ts +114 -15
  52. package/src/lib/server/memory-db.ts +18 -7
  53. package/src/lib/server/mmr.ts +73 -0
  54. package/src/lib/server/orchestrator-lg.ts +3 -0
  55. package/src/lib/server/plugins.ts +44 -22
  56. package/src/lib/server/query-expansion.ts +57 -0
  57. package/src/lib/server/queue.ts +27 -0
  58. package/src/lib/server/session-run-manager.ts +21 -1
  59. package/src/lib/server/session-tools/http.ts +19 -9
  60. package/src/lib/server/session-tools/index.ts +34 -0
  61. package/src/lib/server/session-tools/memory.ts +39 -11
  62. package/src/lib/server/session-tools/schedule.ts +43 -0
  63. package/src/lib/server/session-tools/web.ts +35 -11
  64. package/src/lib/server/storage.ts +12 -0
  65. package/src/lib/server/stream-agent-chat.ts +57 -8
  66. package/src/lib/server/tool-capability-policy.ts +1 -0
  67. package/src/lib/server/tool-retry.ts +62 -0
  68. package/src/lib/server/transcript-repair.ts +72 -0
  69. package/src/lib/setup-defaults.ts +1 -0
  70. package/src/lib/tool-definitions.ts +1 -0
  71. package/src/lib/validation/schemas.ts +1 -0
  72. package/src/lib/view-routes.ts +1 -0
  73. package/src/types/index.ts +34 -3
@@ -16,6 +16,7 @@ import { AgentPickerList } from '@/components/shared/agent-picker-list'
16
16
  import { randomSoul } from '@/lib/soul-suggestions'
17
17
  import { SectionLabel } from '@/components/shared/section-label'
18
18
  import { SoulLibraryPicker } from './soul-library-picker'
19
+ import { HintTip } from '@/components/shared/hint-tip'
19
20
 
20
21
  const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
21
22
 
@@ -109,6 +110,7 @@ export function AgentSheet() {
109
110
  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
110
111
  const [uploading, setUploading] = useState(false)
111
112
  const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
113
+ const [autoRecovery, setAutoRecovery] = useState(false)
112
114
  const [voiceId, setVoiceId] = useState('')
113
115
  const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
114
116
  const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
@@ -193,6 +195,7 @@ export function AgentSheet() {
193
195
  setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
194
196
  setAvatarUrl(editing.avatarUrl || null)
195
197
  setThinkingLevel(editing.thinkingLevel || '')
198
+ setAutoRecovery(editing.autoRecovery || false)
196
199
  setVoiceId(editing.elevenLabsVoiceId || '')
197
200
  setHeartbeatEnabled(editing.heartbeatEnabled || false)
198
201
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
@@ -235,6 +238,7 @@ export function AgentSheet() {
235
238
  setProjectId(undefined)
236
239
  setAvatarSeed('')
237
240
  setThinkingLevel('')
241
+ setAutoRecovery(false)
238
242
  setVoiceId('')
239
243
  setHeartbeatEnabled(false)
240
244
  setHeartbeatIntervalSec('')
@@ -334,6 +338,7 @@ export function AgentSheet() {
334
338
  avatarSeed: avatarSeed.trim() || undefined,
335
339
  avatarUrl: avatarUrl || null,
336
340
  thinkingLevel: thinkingLevel || undefined,
341
+ autoRecovery,
337
342
  elevenLabsVoiceId: voiceId.trim() || null,
338
343
  heartbeatEnabled,
339
344
  heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
@@ -653,8 +658,9 @@ export function AgentSheet() {
653
658
 
654
659
  {/* Thinking Level */}
655
660
  <div className="mb-8">
656
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
661
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
657
662
  Thinking Level <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
663
+ <HintTip text="Higher levels produce more thoughtful responses but cost more tokens" />
658
664
  </label>
659
665
  <select
660
666
  value={thinkingLevel}
@@ -671,6 +677,20 @@ export function AgentSheet() {
671
677
  <p className="text-[11px] text-text-3/70 mt-1.5">Controls reasoning depth. Anthropic models use extended thinking; OpenAI o-series uses reasoning_effort. Others get system prompt guidance.</p>
672
678
  </div>
673
679
 
680
+ {/* Auto-Recovery */}
681
+ <div className="mb-8">
682
+ <div className="flex items-center justify-between mb-1.5">
683
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Guardian Auto-Recovery <HintTip text="Automatically resets the agent's workspace if it gets into a broken state" /></label>
684
+ <div
685
+ onClick={() => setAutoRecovery(!autoRecovery)}
686
+ className={`w-9 h-5 rounded-full transition-all relative cursor-pointer ${autoRecovery ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
687
+ >
688
+ <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all ${autoRecovery ? 'left-[18px]' : 'left-0.5'}`} />
689
+ </div>
690
+ </div>
691
+ <p className="text-[11px] text-text-3/70">If this agent critically fails a task that modifies the workspace, SwarmClaw Guardian will automatically perform a <code className="text-[10px] bg-white/[0.05] px-1 rounded">git reset --hard</code> to restore the last known good state.</p>
692
+ </div>
693
+
674
694
  {/* ElevenLabs Voice ID */}
675
695
  {appSettings.elevenLabsEnabled && (
676
696
  <div className="mb-8">
@@ -692,7 +712,7 @@ export function AgentSheet() {
692
712
  {/* Heartbeat Configuration */}
693
713
  <div className="mb-8">
694
714
  <div className="flex items-center justify-between mb-3">
695
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Heartbeat</label>
715
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Heartbeat <HintTip text="Periodically runs a background prompt to keep the agent active and aware" /></label>
696
716
  <button
697
717
  type="button"
698
718
  onClick={() => setHeartbeatEnabled(!heartbeatEnabled)}
@@ -704,7 +724,7 @@ export function AgentSheet() {
704
724
  {heartbeatEnabled && (
705
725
  <div className="space-y-4 mt-3">
706
726
  <div>
707
- <label className="block text-[12px] text-text-3/70 mb-1.5">Interval</label>
727
+ <label className="flex items-center gap-1.5 text-[12px] text-text-3/70 mb-1.5">Interval <HintTip text="Minutes between each heartbeat check" /></label>
708
728
  <select
709
729
  value={heartbeatIntervalSec}
710
730
  onChange={(e) => setHeartbeatIntervalSec(e.target.value)}
@@ -746,7 +766,7 @@ export function AgentSheet() {
746
766
  {/* Monthly Budget */}
747
767
  <div className="mb-8">
748
768
  <div className="flex items-center justify-between mb-3">
749
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Monthly Budget</label>
769
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Monthly Budget <HintTip text="Set a spending limit for this agent's API usage" /></label>
750
770
  <button
751
771
  type="button"
752
772
  onClick={() => setBudgetEnabled(!budgetEnabled)}
@@ -774,7 +794,7 @@ export function AgentSheet() {
774
794
  </div>
775
795
  </div>
776
796
  <div>
777
- <label className="block text-[12px] text-text-3/70 mb-1.5">When exceeded</label>
797
+ <label className="flex items-center gap-1.5 text-[12px] text-text-3/70 mb-1.5">When exceeded <HintTip text="Warn shows an alert but keeps running; Block stops the agent from making API calls" /></label>
778
798
  <div className="flex gap-2">
779
799
  <button
780
800
  type="button"
@@ -835,6 +855,7 @@ export function AgentSheet() {
835
855
  <div className="mb-8">
836
856
  <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
837
857
  Soul / Personality <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
858
+ <HintTip text="The agent's voice and tone — how it talks, not what it knows" />
838
859
  {soul !== soulInitial && soulSaveState === 'idle' && (
839
860
  <span className="inline-flex items-center gap-1 normal-case tracking-normal text-[10px] text-amber-400 font-600">
840
861
  <span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
@@ -889,7 +910,7 @@ export function AgentSheet() {
889
910
  {provider !== 'openclaw' && (
890
911
  <div className="mb-8">
891
912
  <div className="flex items-center gap-2 mb-3">
892
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">System Prompt</label>
913
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">System Prompt <HintTip text="Instructions that tell the agent what it can do, what tools to use, and how to behave" /></label>
893
914
  <button onClick={() => promptFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
894
915
  <input ref={promptFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSystemPrompt)} className="hidden" />
895
916
  </div>
@@ -1,8 +1,9 @@
1
1
  'use client'
2
2
 
3
- import { useState, useMemo } from 'react'
3
+ import { useState, useMemo, useEffect } from 'react'
4
4
  import { BottomSheet } from '@/components/shared/bottom-sheet'
5
5
  import { SOUL_LIBRARY, SOUL_ARCHETYPES, searchSouls, type SoulTemplate } from '@/lib/soul-library'
6
+ import { api } from '@/lib/api-client'
6
7
 
7
8
  interface SoulLibraryPickerProps {
8
9
  open: boolean
@@ -13,8 +14,48 @@ interface SoulLibraryPickerProps {
13
14
  export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPickerProps) {
14
15
  const [query, setQuery] = useState('')
15
16
  const [archetype, setArchetype] = useState('All')
17
+ const [source, setSource] = useState<'library' | 'forge'>('library')
18
+ const [customSouls, setCustomSouls] = useState<SoulTemplate[]>([])
19
+ const [loading, setLoading] = useState(false)
16
20
 
17
- const results = useMemo(() => searchSouls(query, archetype), [query, archetype])
21
+ const results = useMemo(() => {
22
+ if (source === 'library') {
23
+ return searchSouls(query, archetype)
24
+ } else {
25
+ let filtered = customSouls
26
+ if (archetype && archetype !== 'All') {
27
+ filtered = filtered.filter(s => s.archetype === archetype)
28
+ }
29
+ if (query) {
30
+ const q = query.toLowerCase()
31
+ filtered = filtered.filter(s =>
32
+ s.name.toLowerCase().includes(q) ||
33
+ s.soul.toLowerCase().includes(q) ||
34
+ s.tags.some(t => t.toLowerCase().includes(q))
35
+ )
36
+ }
37
+ return filtered
38
+ }
39
+ }, [query, archetype, source, customSouls])
40
+
41
+ useEffect(() => {
42
+ if (open && source === 'forge') {
43
+ const load = async () => {
44
+ setLoading(true)
45
+ try {
46
+ const res = await api<SoulTemplate[]>('GET', '/souls')
47
+ // Filter out the built-in ones from the API result since we show them in 'library' tab
48
+ const libraryIds = new Set(SOUL_LIBRARY.map(s => s.id))
49
+ setCustomSouls(res.filter(s => !libraryIds.has(s.id)))
50
+ } catch (err) {
51
+ console.error('Failed to load custom souls', err)
52
+ } finally {
53
+ setLoading(false)
54
+ }
55
+ }
56
+ load()
57
+ }
58
+ }, [open, source])
18
59
 
19
60
  const handleSelect = (template: SoulTemplate) => {
20
61
  onSelect(template.soul)
@@ -23,9 +64,25 @@ export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPicker
23
64
 
24
65
  return (
25
66
  <BottomSheet open={open} onClose={onClose}>
26
- <div className="mb-6">
27
- <h2 className="font-display text-[24px] font-700 tracking-[-0.03em] mb-1">Soul Library</h2>
28
- <p className="text-[13px] text-text-3">Browse personality templates for your agent</p>
67
+ <div className="mb-6 flex items-center justify-between">
68
+ <div>
69
+ <h2 className="font-display text-[24px] font-700 tracking-[-0.03em] mb-1">Soul Library</h2>
70
+ <p className="text-[13px] text-text-3">Browse personality templates for your agent</p>
71
+ </div>
72
+ <div className="flex bg-white/[0.04] p-1 rounded-[12px] border border-white/[0.04]">
73
+ <button
74
+ onClick={() => setSource('library')}
75
+ className={`px-3 py-1.5 rounded-[10px] text-[12px] font-600 transition-all ${source === 'library' ? 'bg-white/[0.08] text-text shadow-sm' : 'text-text-3 hover:text-text-2'}`}
76
+ >
77
+ Verified
78
+ </button>
79
+ <button
80
+ onClick={() => setSource('forge')}
81
+ className={`px-3 py-1.5 rounded-[10px] text-[12px] font-600 transition-all ${source === 'forge' ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:text-text-2'}`}
82
+ >
83
+ SwarmForge
84
+ </button>
85
+ </div>
29
86
  </div>
30
87
 
31
88
  {/* Search */}
@@ -34,7 +91,7 @@ export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPicker
34
91
  type="text"
35
92
  value={query}
36
93
  onChange={(e) => setQuery(e.target.value)}
37
- placeholder="Search personalities..."
94
+ placeholder={source === 'library' ? "Search verified personalities..." : "Search SwarmForge / Custom..."}
38
95
  className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none focus-glow"
39
96
  style={{ fontFamily: 'inherit' }}
40
97
  />
@@ -59,31 +116,45 @@ export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPicker
59
116
 
60
117
  {/* Results grid */}
61
118
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-[60vh] overflow-y-auto pb-4">
62
- {results.map((template) => (
119
+ {loading ? (
120
+ <div className="col-span-2 py-12 flex flex-col items-center gap-3">
121
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-accent-bright" />
122
+ <p className="text-[13px] text-text-3">Stoking the forge...</p>
123
+ </div>
124
+ ) : results.map((template) => (
63
125
  <button
64
126
  key={template.id}
65
127
  onClick={() => handleSelect(template)}
66
- className="text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 hover:border-accent-bright/20 transition-all cursor-pointer group"
128
+ className={`text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer group
129
+ ${source === 'forge' ? 'hover:border-accent-bright/20' : 'hover:border-white/[0.12]'}`}
67
130
  style={{ fontFamily: 'inherit' }}
68
131
  >
69
132
  <div className="flex items-start gap-2 mb-2">
70
- <h4 className="text-[14px] font-600 text-text group-hover:text-accent-bright transition-colors">
133
+ <h4 className={`text-[14px] font-600 text-text transition-colors ${source === 'forge' ? 'group-hover:text-accent-bright' : ''}`}>
71
134
  {template.name}
72
135
  </h4>
73
136
  <span className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] text-text-3 text-[10px] font-600 shrink-0">
74
137
  {template.archetype}
75
138
  </span>
76
139
  </div>
77
- <p className="text-[12px] text-text-3 mb-2">{template.description}</p>
140
+ <p className="text-[12px] text-text-3 mb-2 line-clamp-2">{template.description}</p>
78
141
  <p className="text-[11px] text-text-3/60 line-clamp-2 italic">{template.soul}</p>
79
142
  </button>
80
143
  ))}
81
- {results.length === 0 && (
82
- <p className="text-[13px] text-text-3 col-span-2 text-center py-8">No personalities match your search</p>
144
+ {!loading && results.length === 0 && (
145
+ <div className="col-span-2 text-center py-12">
146
+ <div className="w-12 h-12 rounded-full bg-white/[0.03] flex items-center justify-center mx-auto mb-3">
147
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40"><path d="M12 2a4 4 0 0 1 4 4v2a4 4 0 0 1-8 0V6a4 4 0 0 1 4-4Z"/><path d="M16 14H8a4 4 0 0 0-4 4v2h16v-2a4 4 0 0 0-4-4Z"/></svg>
148
+ </div>
149
+ <p className="text-[14px] font-600 text-text-2">No personalities match</p>
150
+ <p className="text-[12px] text-text-3/50 mt-1">{source === 'forge' ? 'Be the first to forge a custom soul in this category!' : 'Try a different search term.'}</p>
151
+ </div>
83
152
  )}
84
153
  </div>
85
154
 
86
- <p className="text-[11px] text-text-3/50 mt-4 text-center">{SOUL_LIBRARY.length} personalities available</p>
155
+ <p className="text-[11px] text-text-3/50 mt-4 text-center">
156
+ {source === 'library' ? `${SOUL_LIBRARY.length} verified templates` : `${customSouls.length} custom souls in your forge`}
157
+ </p>
87
158
  </BottomSheet>
88
159
  )
89
160
  }
@@ -7,6 +7,7 @@ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain
7
7
  memory_tool: { label: 'Committed to memory', color: '#A855F7', icon: 'brain' },
8
8
  manage_tasks: { label: 'Created a task', color: '#EC4899', icon: 'clipboard' },
9
9
  manage_schedules: { label: 'Scheduled something', color: '#EC4899', icon: 'clipboard' },
10
+ schedule_wake: { label: 'Set a reminder', color: '#F59E0B', icon: 'clipboard' },
10
11
  manage_agents: { label: 'Created an agent', color: '#EC4899', icon: 'clipboard' },
11
12
  delegate_to_claude_code: { label: 'Delegated to Claude Code', color: '#38BDF8', icon: 'delegate' },
12
13
  delegate_to_codex_cli: { label: 'Delegated to Codex', color: '#38BDF8', icon: 'delegate' },
@@ -24,6 +25,7 @@ function extractSnippet(toolName: string, toolInput: string): string | null {
24
25
  if ((toolName === 'memory' || toolName === 'memory_tool') && parsed.key) return parsed.key
25
26
  if (toolName === 'manage_tasks' && parsed.title) return parsed.title
26
27
  if (toolName === 'manage_schedules' && parsed.name) return parsed.name
28
+ if (toolName === 'schedule_wake' && parsed.message) return parsed.message
27
29
  if (toolName === 'manage_agents' && parsed.name) return parsed.name
28
30
  if (toolName === 'delegate_to_agent' && (parsed.agentName || parsed.agentId)) return parsed.agentName || parsed.agentId
29
31
  if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
@@ -0,0 +1,112 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { toast } from 'sonner'
7
+
8
+ interface Checkpoint {
9
+ checkpointId: string
10
+ parentCheckpointId?: string
11
+ metadata: Record<string, unknown>
12
+ createdAt: number
13
+ values?: Record<string, unknown>
14
+ }
15
+
16
+ interface Props {
17
+ sessionId: string
18
+ }
19
+
20
+ export function CheckpointTimeline({ sessionId }: Props) {
21
+ const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([])
22
+ const [loading, setLoading] = useState(true)
23
+ const [restoringId, setRestoringId] = useState<string | null>(null)
24
+ const loadSessions = useAppStore((s) => s.loadSessions)
25
+
26
+ const load = async () => {
27
+ setLoading(true)
28
+ try {
29
+ const data = await api<Checkpoint[]>('GET', `/sessions/${sessionId}/checkpoints`)
30
+ setCheckpoints(data)
31
+ } catch (err) {
32
+ console.error('Failed to load checkpoints', err)
33
+ } finally {
34
+ setLoading(false)
35
+ }
36
+ }
37
+
38
+ useEffect(() => {
39
+ load()
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ }, [sessionId])
42
+
43
+ const handleRestore = async (checkpoint: Checkpoint) => {
44
+ if (!confirm('Restore session to this point? This will delete all subsequent history.')) return
45
+
46
+ setRestoringId(checkpoint.checkpointId)
47
+ try {
48
+ await api('POST', `/sessions/${sessionId}/restore`, {
49
+ checkpointId: checkpoint.checkpointId,
50
+ timestamp: checkpoint.createdAt
51
+ })
52
+ toast.success('Session restored successfully')
53
+ await loadSessions()
54
+ await load()
55
+ } catch (err) {
56
+ toast.error('Failed to restore session')
57
+ console.error(err)
58
+ } finally {
59
+ setRestoringId(null)
60
+ }
61
+ }
62
+
63
+ if (loading) {
64
+ return <div className="p-8 text-center text-text-3 text-[13px]">Retrieving history...</div>
65
+ }
66
+
67
+ if (checkpoints.length === 0) {
68
+ return (
69
+ <div className="p-8 text-center">
70
+ <p className="text-text-3 text-[13px]">No checkpoints found for this session.</p>
71
+ <p className="text-[11px] text-text-3/50 mt-1">Only LangGraph-orchestrated sessions support time travel.</p>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ return (
77
+ <div className="flex flex-col gap-3 p-5">
78
+ {checkpoints.map((cp, i) => (
79
+ <div
80
+ key={cp.checkpointId}
81
+ className="group relative flex flex-col gap-2 p-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.04] transition-all"
82
+ >
83
+ <div className="flex items-center justify-between">
84
+ <div className="flex flex-col">
85
+ <span className="text-[11px] font-700 text-accent-bright uppercase tracking-wider">
86
+ {i === 0 ? 'Current State' : `Point ${checkpoints.length - i}`}
87
+ </span>
88
+ <span className="text-[10px] text-text-3 font-mono">
89
+ {new Date(cp.createdAt).toLocaleString()}
90
+ </span>
91
+ </div>
92
+ {i > 0 && (
93
+ <button
94
+ onClick={() => handleRestore(cp)}
95
+ disabled={!!restoringId}
96
+ className="px-3 py-1 rounded-[6px] bg-accent-soft text-accent-bright text-[11px] font-600 border-none cursor-pointer hover:brightness-110 disabled:opacity-50"
97
+ >
98
+ {restoringId === cp.checkpointId ? 'Restoring...' : 'Restore here'}
99
+ </button>
100
+ )}
101
+ </div>
102
+
103
+ {cp.values && Array.isArray(cp.values.messages) && cp.values.messages.length > 0 && (
104
+ <div className="mt-1 p-2 rounded-[8px] bg-black/20 text-[11px] text-text-3 line-clamp-2 italic">
105
+ Last message: {String((cp.values.messages[cp.values.messages.length - 1] as Record<string, unknown>)?.content ?? 'Empty state')}
106
+ </div>
107
+ )}
108
+ </div>
109
+ ))}
110
+ </div>
111
+ )
112
+ }
@@ -11,6 +11,7 @@ import { StreamingBubble } from './streaming-bubble'
11
11
  import { ThinkingIndicator } from './thinking-indicator'
12
12
  import { SuggestionsBar } from './suggestions-bar'
13
13
  import { ExecApprovalCard } from './exec-approval-card'
14
+ import { TaskApprovalCard } from './task-approval-card'
14
15
  import { HeartbeatMoment, ActivityMoment, isNotableTool } from './activity-moment'
15
16
  import { useApprovalStore } from '@/stores/use-approval-store'
16
17
  import { useWs } from '@/hooks/use-ws'
@@ -573,13 +574,28 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
573
574
 
574
575
  function ApprovalCards({ agentId }: { agentId?: string | null }) {
575
576
  const approvals = useApprovalStore((s) => s.approvals)
577
+ const tasks = useAppStore((s) => s.tasks)
578
+ const sessionId = useAppStore((s) => s.currentSessionId)
579
+
576
580
  const cards = Object.values(approvals).filter((a) => !agentId || a.agentId === agentId)
577
- if (!cards.length) return null
581
+
582
+ // Find tasks associated with this session that need approval
583
+ const pendingTasks = Object.values(tasks).filter((t) => {
584
+ if (!t.pendingApproval) return false
585
+ // Show if matches the current session OR the current agent
586
+ return t.sessionId === sessionId || (agentId && t.agentId === agentId)
587
+ })
588
+
589
+ if (!cards.length && !pendingTasks.length) return null
590
+
578
591
  return (
579
- <>
592
+ <div className="flex flex-col gap-2">
580
593
  {cards.map((a) => (
581
594
  <ExecApprovalCard key={a.id} approval={a} />
582
595
  ))}
583
- </>
596
+ {pendingTasks.map((t) => (
597
+ <TaskApprovalCard key={t.id} task={t} />
598
+ ))}
599
+ </div>
584
600
  )
585
601
  }