@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.
- package/README.md +24 -6
- package/package.json +1 -1
- package/src/app/api/agents/route.ts +1 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/route.ts +5 -0
- package/src/app/api/tasks/route.ts +2 -0
- package/src/app/api/usage/route.ts +9 -2
- package/src/cli/index.js +24 -0
- package/src/components/agents/agent-sheet.tsx +27 -6
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/message-list.tsx +19 -3
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/connectors/connector-sheet.tsx +8 -1
- package/src/components/home/home-view.tsx +39 -15
- package/src/components/layout/app-layout.tsx +18 -2
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +9 -2
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -4
- package/src/components/tasks/approvals-panel.tsx +120 -0
- package/src/components/usage/metrics-dashboard.tsx +25 -3
- package/src/lib/server/chat-execution.ts +96 -12
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/daemon-state.ts +70 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/main-agent-loop.ts +114 -15
- package/src/lib/server/memory-db.ts +18 -7
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +3 -0
- package/src/lib/server/plugins.ts +44 -22
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +27 -0
- package/src/lib/server/session-run-manager.ts +21 -1
- package/src/lib/server/session-tools/http.ts +19 -9
- package/src/lib/server/session-tools/index.ts +34 -0
- package/src/lib/server/session-tools/memory.ts +39 -11
- package/src/lib/server/session-tools/schedule.ts +43 -0
- package/src/lib/server/session-tools/web.ts +35 -11
- package/src/lib/server/storage.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +57 -8
- package/src/lib/server/tool-capability-policy.ts +1 -0
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/view-routes.ts +1 -0
- 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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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(() =>
|
|
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
|
-
<
|
|
28
|
-
|
|
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
|
-
{
|
|
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=
|
|
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=
|
|
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
|
-
<
|
|
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">
|
|
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
|
-
|
|
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
|
}
|