@swarmclawai/swarmclaw 1.3.4 → 1.3.6
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 +20 -76
- package/package.json +3 -2
- package/skills/swarmclaw.md +17 -0
- package/src/app/api/agents/[id]/dream/route.ts +45 -0
- package/src/app/api/knowledge/[id]/route.ts +48 -49
- package/src/app/api/knowledge/hygiene/route.ts +13 -0
- package/src/app/api/knowledge/route.ts +70 -42
- package/src/app/api/knowledge/sources/[id]/archive/route.ts +15 -0
- package/src/app/api/knowledge/sources/[id]/restore/route.ts +10 -0
- package/src/app/api/knowledge/sources/[id]/route.ts +1 -0
- package/src/app/api/knowledge/sources/[id]/supersede/route.ts +26 -0
- package/src/app/api/knowledge/sources/[id]/sync/route.ts +17 -0
- package/src/app/api/knowledge/sources/route.ts +1 -0
- package/src/app/api/knowledge/upload/route.ts +3 -51
- package/src/app/api/memory/dream/[id]/route.ts +19 -0
- package/src/app/api/memory/dream/route.ts +34 -0
- package/src/app/knowledge/layout.tsx +1 -1
- package/src/app/knowledge/page.tsx +2 -22
- package/src/app/protocols/page.tsx +21 -2
- package/src/cli/index.js +16 -0
- package/src/cli/spec.js +5 -0
- package/src/components/agents/agent-sheet.tsx +65 -0
- package/src/components/chat/message-bubble.tsx +10 -0
- package/src/components/knowledge/grounding-panel.tsx +99 -0
- package/src/components/knowledge/knowledge-detail.tsx +402 -0
- package/src/components/knowledge/knowledge-list.tsx +351 -126
- package/src/components/knowledge/knowledge-sheet.tsx +208 -119
- package/src/components/memory/dream-history.tsx +155 -0
- package/src/components/memory/memory-card.tsx +7 -0
- package/src/components/memory/memory-detail.tsx +46 -0
- package/src/components/runs/run-list.tsx +23 -0
- package/src/lib/server/api-routes.test.ts +43 -2
- package/src/lib/server/chat-execution/chat-execution-disabled.test.ts +14 -31
- package/src/lib/server/chat-execution/chat-execution-eval-history.test.ts +11 -34
- package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +108 -0
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +35 -36
- package/src/lib/server/chat-execution/chat-execution-types.ts +8 -1
- package/src/lib/server/chat-execution/chat-execution.ts +1 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +21 -1
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +6 -1
- package/src/lib/server/chat-execution/post-stream-finalization.ts +15 -3
- package/src/lib/server/chat-execution/prompt-sections.ts +29 -3
- package/src/lib/server/chat-execution/stream-agent-chat.ts +6 -1
- package/src/lib/server/execution-engine/task-attempt.ts +8 -2
- package/src/lib/server/knowledge-import.ts +159 -0
- package/src/lib/server/knowledge-sources.test.ts +261 -0
- package/src/lib/server/knowledge-sources.ts +1284 -0
- package/src/lib/server/memory/dream-cycles.ts +49 -0
- package/src/lib/server/memory/dream-idle-callback.ts +38 -0
- package/src/lib/server/memory/dream-service.ts +315 -0
- package/src/lib/server/memory/memory-db.ts +37 -2
- package/src/lib/server/protocols/protocol-agent-turn.ts +7 -0
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +19 -6
- package/src/lib/server/protocols/protocol-service.test.ts +99 -0
- package/src/lib/server/protocols/protocol-step-helpers.ts +7 -1
- package/src/lib/server/protocols/protocol-step-processors.ts +16 -3
- package/src/lib/server/protocols/protocol-types.ts +4 -0
- package/src/lib/server/runtime/daemon-state/core.ts +6 -1
- package/src/lib/server/runtime/run-ledger.test.ts +120 -0
- package/src/lib/server/runtime/run-ledger.ts +27 -1
- package/src/lib/server/runtime/session-run-manager/drain.ts +5 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +19 -2
- package/src/lib/server/storage-normalization.ts +5 -0
- package/src/lib/server/storage.ts +15 -0
- package/src/lib/server/test-utils/run-with-temp-data-dir.ts +15 -2
- package/src/stores/slices/ui-slice.ts +4 -0
- package/src/types/agent.ts +7 -0
- package/src/types/dream.ts +45 -0
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +3 -0
- package/src/types/misc.ts +131 -0
- package/src/types/protocol.ts +4 -0
- package/src/types/run.ts +4 -1
|
@@ -8,105 +8,211 @@ import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
|
8
8
|
import { EmptyState } from '@/components/shared/empty-state'
|
|
9
9
|
import { PageLoader } from '@/components/ui/page-loader'
|
|
10
10
|
import { SearchInput } from '@/components/ui/search-input'
|
|
11
|
-
import type {
|
|
11
|
+
import type { KnowledgeHygieneSummary, KnowledgeSearchHit, KnowledgeSourceSummary } from '@/types'
|
|
12
|
+
import { toast } from 'sonner'
|
|
12
13
|
|
|
13
14
|
export function KnowledgeList() {
|
|
14
15
|
const [search, setSearch] = useState('')
|
|
15
|
-
const [
|
|
16
|
+
const [sources, setSources] = useState<KnowledgeSourceSummary[]>([])
|
|
17
|
+
const [hits, setHits] = useState<KnowledgeSearchHit[]>([])
|
|
16
18
|
const [loaded, setLoaded] = useState(false)
|
|
17
19
|
const [error, setError] = useState<string | null>(null)
|
|
18
20
|
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
21
|
+
const [includeArchived, setIncludeArchived] = useState(false)
|
|
22
|
+
const [hygiene, setHygiene] = useState<KnowledgeHygieneSummary | null>(null)
|
|
23
|
+
const [maintaining, setMaintaining] = useState(false)
|
|
19
24
|
const searchRef = useRef(search)
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
25
|
+
|
|
26
|
+
const agents = useAppStore((state) => state.agents)
|
|
27
|
+
const loadAgents = useAppStore((state) => state.loadAgents)
|
|
28
|
+
const refreshKey = useAppStore((state) => state.knowledgeRefreshKey)
|
|
29
|
+
const openKnowledgeSheet = useAppStore((state) => state.setKnowledgeSheetOpen)
|
|
30
|
+
const setEditingKnowledgeId = useAppStore((state) => state.setEditingKnowledgeId)
|
|
31
|
+
const selectedKnowledgeSourceId = useAppStore((state) => state.selectedKnowledgeSourceId)
|
|
32
|
+
const setSelectedKnowledgeSourceId = useAppStore((state) => state.setSelectedKnowledgeSourceId)
|
|
33
|
+
const triggerKnowledgeRefresh = useAppStore((state) => state.triggerKnowledgeRefresh)
|
|
24
34
|
|
|
25
35
|
const openSheet = useCallback((id?: string) => {
|
|
26
36
|
setEditingKnowledgeId(id ?? null)
|
|
27
|
-
|
|
28
|
-
}, [
|
|
37
|
+
openKnowledgeSheet(true)
|
|
38
|
+
}, [openKnowledgeSheet, setEditingKnowledgeId])
|
|
29
39
|
|
|
30
40
|
const load = useCallback(async (query: string, tag?: string | null) => {
|
|
31
41
|
try {
|
|
32
42
|
const params = new URLSearchParams()
|
|
33
|
-
if (query) params.set('q', query)
|
|
34
43
|
if (tag) params.set('tags', tag)
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
44
|
+
if (includeArchived) params.set('includeArchived', 'true')
|
|
45
|
+
const currentSelectedId = useAppStore.getState().selectedKnowledgeSourceId
|
|
46
|
+
|
|
47
|
+
if (query.trim()) {
|
|
48
|
+
params.set('q', query.trim())
|
|
49
|
+
const results = await api<KnowledgeSearchHit[]>('GET', `/knowledge?${params.toString()}`)
|
|
50
|
+
const nextHits = Array.isArray(results) ? results : []
|
|
51
|
+
setHits(nextHits)
|
|
52
|
+
setSources([])
|
|
53
|
+
if (!currentSelectedId || !nextHits.some((hit) => hit.sourceId === currentSelectedId)) {
|
|
54
|
+
setSelectedKnowledgeSourceId(nextHits[0]?.sourceId || null)
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const qs = params.toString()
|
|
58
|
+
const results = await api<KnowledgeSourceSummary[]>('GET', `/knowledge/sources${qs ? `?${qs}` : ''}`)
|
|
59
|
+
const nextSources = Array.isArray(results) ? results : []
|
|
60
|
+
setSources(nextSources)
|
|
61
|
+
setHits([])
|
|
62
|
+
if (!currentSelectedId || !nextSources.some((source) => source.id === currentSelectedId)) {
|
|
63
|
+
setSelectedKnowledgeSourceId(nextSources[0]?.id || null)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
38
66
|
setError(null)
|
|
39
67
|
} catch {
|
|
40
|
-
setError('Unable to load knowledge
|
|
68
|
+
setError('Unable to load knowledge sources.')
|
|
41
69
|
}
|
|
42
70
|
setLoaded(true)
|
|
71
|
+
}, [includeArchived, setSelectedKnowledgeSourceId])
|
|
72
|
+
|
|
73
|
+
const loadHygiene = useCallback(async () => {
|
|
74
|
+
try {
|
|
75
|
+
const summary = await api<KnowledgeHygieneSummary>('GET', '/knowledge/hygiene')
|
|
76
|
+
setHygiene(summary)
|
|
77
|
+
} catch {
|
|
78
|
+
setHygiene(null)
|
|
79
|
+
}
|
|
43
80
|
}, [])
|
|
44
81
|
|
|
45
|
-
useEffect(() => {
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
searchRef.current = search
|
|
84
|
+
}, [search])
|
|
46
85
|
|
|
47
|
-
// Initial load
|
|
48
86
|
useEffect(() => {
|
|
49
87
|
loadAgents()
|
|
50
|
-
|
|
88
|
+
}, [loadAgents])
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
void load(searchRef.current, activeTag)
|
|
93
|
+
}, 0)
|
|
51
94
|
return () => clearTimeout(timer)
|
|
52
|
-
|
|
53
|
-
|
|
95
|
+
}, [activeTag, load, refreshKey])
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
void loadHygiene()
|
|
99
|
+
}, [loadHygiene, refreshKey])
|
|
54
100
|
|
|
55
|
-
// Debounced search
|
|
56
101
|
useEffect(() => {
|
|
57
|
-
const timer = setTimeout(() => {
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
void load(search, activeTag)
|
|
104
|
+
}, 250)
|
|
58
105
|
return () => clearTimeout(timer)
|
|
59
|
-
}, [
|
|
106
|
+
}, [activeTag, includeArchived, load, search])
|
|
60
107
|
|
|
61
108
|
const uniqueTags = useMemo(() => {
|
|
62
109
|
const tags = new Set<string>()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
110
|
+
const items = search.trim() ? hits : sources
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
for (const tag of item.tags) tags.add(tag)
|
|
66
113
|
}
|
|
67
|
-
return Array.from(tags).sort()
|
|
68
|
-
}, [
|
|
114
|
+
return Array.from(tags).sort((left, right) => left.localeCompare(right))
|
|
115
|
+
}, [hits, search, sources])
|
|
69
116
|
|
|
70
|
-
const handleDelete = async (id: string) => {
|
|
117
|
+
const handleDelete = useCallback(async (id: string) => {
|
|
71
118
|
try {
|
|
72
|
-
await api('DELETE', `/knowledge/${id}`)
|
|
73
|
-
|
|
119
|
+
await api('DELETE', `/knowledge/sources/${id}`)
|
|
120
|
+
if (selectedKnowledgeSourceId === id) {
|
|
121
|
+
setSelectedKnowledgeSourceId(null)
|
|
122
|
+
}
|
|
123
|
+
triggerKnowledgeRefresh()
|
|
74
124
|
} catch {
|
|
75
|
-
//
|
|
125
|
+
// Best-effort delete; caller can retry from refreshed list.
|
|
76
126
|
}
|
|
77
|
-
}
|
|
127
|
+
}, [selectedKnowledgeSourceId, setSelectedKnowledgeSourceId, triggerKnowledgeRefresh])
|
|
128
|
+
|
|
129
|
+
const runMaintenance = useCallback(async () => {
|
|
130
|
+
setMaintaining(true)
|
|
131
|
+
try {
|
|
132
|
+
await api('POST', '/knowledge/hygiene')
|
|
133
|
+
triggerKnowledgeRefresh()
|
|
134
|
+
void loadHygiene()
|
|
135
|
+
toast.success('Knowledge maintenance completed')
|
|
136
|
+
} catch {
|
|
137
|
+
toast.error('Knowledge maintenance failed')
|
|
138
|
+
} finally {
|
|
139
|
+
setMaintaining(false)
|
|
140
|
+
}
|
|
141
|
+
}, [loadHygiene, triggerKnowledgeRefresh])
|
|
78
142
|
|
|
79
|
-
const formatDate = (
|
|
80
|
-
|
|
143
|
+
const formatDate = (timestamp?: number | null) => {
|
|
144
|
+
if (!timestamp) return 'Not indexed'
|
|
145
|
+
return new Date(timestamp).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
81
146
|
}
|
|
82
147
|
|
|
148
|
+
const scopedAgentsFor = (agentIds: string[]) => agentIds.map((id) => agents[id]).filter(Boolean)
|
|
149
|
+
|
|
83
150
|
if (!loaded) {
|
|
84
151
|
return <PageLoader label="Loading knowledge..." />
|
|
85
152
|
}
|
|
86
153
|
|
|
154
|
+
const showingHits = search.trim().length > 0
|
|
155
|
+
const items = showingHits ? hits : sources
|
|
156
|
+
|
|
87
157
|
return (
|
|
88
158
|
<div className="flex-1 flex flex-col overflow-y-auto">
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
159
|
+
<div className="px-5 py-2 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
|
|
160
|
+
<SearchInput
|
|
161
|
+
size="sm"
|
|
162
|
+
value={search}
|
|
163
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
164
|
+
onClear={() => setSearch('')}
|
|
165
|
+
placeholder="Search knowledge..."
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{hygiene && (
|
|
170
|
+
<div className="px-5 pb-2 shrink-0">
|
|
171
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-white/[0.03] p-3">
|
|
172
|
+
<div className="flex items-center justify-between gap-3">
|
|
173
|
+
<div>
|
|
174
|
+
<div className="text-[10px] font-700 uppercase tracking-[0.12em] text-text-3/55">Hygiene</div>
|
|
175
|
+
<div className="mt-1 flex flex-wrap gap-2 text-[11px] text-text-2/80">
|
|
176
|
+
<span>stale {hygiene.counts.stale}</span>
|
|
177
|
+
<span>duplicates {hygiene.counts.duplicate}</span>
|
|
178
|
+
<span>broken {hygiene.counts.broken}</span>
|
|
179
|
+
<span>archived {hygiene.counts.archived}</span>
|
|
180
|
+
<span>superseded {hygiene.counts.superseded}</span>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<button
|
|
184
|
+
onClick={() => { void runMaintenance() }}
|
|
185
|
+
disabled={maintaining}
|
|
186
|
+
className="rounded-[9px] border border-white/[0.08] bg-white/[0.04] px-2.5 py-1.5 text-[11px] font-600 text-text-2 transition-all cursor-pointer disabled:opacity-50"
|
|
187
|
+
>
|
|
188
|
+
{maintaining ? 'Running…' : 'Maintain'}
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="mt-3 flex items-center justify-between gap-3">
|
|
192
|
+
<div className="text-[10px] text-text-3/55">
|
|
193
|
+
Last scan {new Date(hygiene.scannedAt).toLocaleTimeString()}
|
|
194
|
+
</div>
|
|
195
|
+
<button
|
|
196
|
+
onClick={() => setIncludeArchived((current) => !current)}
|
|
197
|
+
className={`rounded-[8px] px-2 py-1 text-[10px] font-700 uppercase tracking-[0.08em] cursor-pointer ${
|
|
198
|
+
includeArchived ? 'bg-amber-500/12 text-amber-200' : 'bg-white/[0.04] text-text-3/75'
|
|
199
|
+
}`}
|
|
200
|
+
>
|
|
201
|
+
{includeArchived ? 'Showing archived' : 'Hide archived'}
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
99
205
|
</div>
|
|
100
206
|
)}
|
|
101
207
|
|
|
102
|
-
{/* Tag filters */}
|
|
103
208
|
{uniqueTags.length > 0 && (
|
|
104
209
|
<div className="px-5 pb-1.5 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.05s both' }}>
|
|
105
210
|
<div className="flex gap-1 flex-wrap">
|
|
106
211
|
<button
|
|
107
212
|
onClick={() => setActiveTag(null)}
|
|
108
|
-
className={`px-2 py-0.5 rounded-[6px] text-[9px] font-600 cursor-pointer transition-all uppercase tracking-wider
|
|
109
|
-
|
|
213
|
+
className={`px-2 py-0.5 rounded-[6px] text-[9px] font-600 cursor-pointer transition-all uppercase tracking-wider ${
|
|
214
|
+
!activeTag ? 'bg-white/[0.06] text-text-2' : 'bg-transparent text-text-3/70 hover:text-text-3'
|
|
215
|
+
}`}
|
|
110
216
|
style={{ fontFamily: 'inherit' }}
|
|
111
217
|
>
|
|
112
218
|
all
|
|
@@ -115,8 +221,9 @@ export function KnowledgeList() {
|
|
|
115
221
|
<button
|
|
116
222
|
key={tag}
|
|
117
223
|
onClick={() => setActiveTag(activeTag === tag ? null : tag)}
|
|
118
|
-
className={`px-2 py-0.5 rounded-[6px] text-[9px] font-600 cursor-pointer transition-all uppercase tracking-wider
|
|
119
|
-
|
|
224
|
+
className={`px-2 py-0.5 rounded-[6px] text-[9px] font-600 cursor-pointer transition-all uppercase tracking-wider ${
|
|
225
|
+
activeTag === tag ? 'bg-white/[0.06] text-text-2' : 'bg-transparent text-text-3/70 hover:text-text-3'
|
|
226
|
+
}`}
|
|
120
227
|
style={{ fontFamily: 'inherit' }}
|
|
121
228
|
>
|
|
122
229
|
{tag}
|
|
@@ -126,84 +233,202 @@ export function KnowledgeList() {
|
|
|
126
233
|
</div>
|
|
127
234
|
)}
|
|
128
235
|
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
236
|
+
{items.length > 0 ? (
|
|
237
|
+
<div className="grid grid-cols-1 gap-3 px-5 pb-6">
|
|
238
|
+
{showingHits
|
|
239
|
+
? hits.map((hit, idx) => {
|
|
240
|
+
const scopedAgents = scopedAgentsFor(hit.agentIds)
|
|
241
|
+
const active = selectedKnowledgeSourceId === hit.sourceId
|
|
242
|
+
return (
|
|
243
|
+
<div
|
|
244
|
+
key={hit.id}
|
|
245
|
+
onClick={() => setSelectedKnowledgeSourceId(hit.sourceId)}
|
|
246
|
+
className={`p-3 rounded-[12px] border transition-all relative group cursor-pointer ${
|
|
247
|
+
active
|
|
248
|
+
? 'border-accent-bright/25 bg-accent-soft/10'
|
|
249
|
+
: 'border-white/[0.04] bg-transparent hover:bg-surface-2 hover:border-white/[0.1]'
|
|
250
|
+
}`}
|
|
251
|
+
style={{
|
|
252
|
+
animation: 'spring-in 0.5s var(--ease-spring) both',
|
|
253
|
+
animationDelay: `${0.08 + idx * 0.02}s`,
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<div className="flex items-start justify-between gap-2 mb-1.5">
|
|
257
|
+
<div className="min-w-0">
|
|
258
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
259
|
+
<span className="font-display text-[13px] font-600 text-text truncate">{hit.sourceTitle}</span>
|
|
260
|
+
<Badge variant="secondary" className="text-[9px] px-1.5 py-0 uppercase">{hit.sourceKind}</Badge>
|
|
261
|
+
</div>
|
|
262
|
+
<p className="text-[10px] text-text-3/55">
|
|
263
|
+
Chunk {hit.chunkIndex + 1} of {hit.chunkCount}
|
|
264
|
+
{hit.sectionLabel ? ` • ${hit.sectionLabel}` : ''}
|
|
265
|
+
</p>
|
|
266
|
+
{hit.whyMatched && (
|
|
267
|
+
<p className="mt-1 text-[10px] text-sky-200/70">{hit.whyMatched}</p>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
<button
|
|
271
|
+
onClick={(event) => {
|
|
272
|
+
event.stopPropagation()
|
|
273
|
+
openSheet(hit.sourceId)
|
|
274
|
+
}}
|
|
275
|
+
className="text-text-3/40 hover:text-accent-bright transition-colors p-0.5 cursor-pointer"
|
|
276
|
+
title="Edit"
|
|
277
|
+
>
|
|
278
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
279
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
280
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
281
|
+
</svg>
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<p className="text-[11px] text-text-2/80 line-clamp-4">{hit.snippet}</p>
|
|
286
|
+
|
|
287
|
+
<div className="flex items-center gap-2 mt-2.5 flex-wrap">
|
|
288
|
+
{hit.tags.map((tag) => (
|
|
289
|
+
<Badge key={`${hit.id}-${tag}`} variant="secondary" className="text-[9px] px-1.5 py-0">{tag}</Badge>
|
|
290
|
+
))}
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div className="flex items-center gap-2 mt-2.5">
|
|
294
|
+
<span className={`text-[10px] font-600 ${hit.scope === 'global' ? 'text-emerald-400' : 'text-amber-400'}`}>
|
|
295
|
+
{hit.scope === 'global' ? 'Global' : `${hit.agentIds.length} agent(s)`}
|
|
296
|
+
</span>
|
|
297
|
+
{scopedAgents.length > 0 && (
|
|
298
|
+
<div className="flex items-center -space-x-1.5">
|
|
299
|
+
{scopedAgents.slice(0, 5).map((agent) => (
|
|
300
|
+
<AgentAvatar
|
|
301
|
+
key={agent.id}
|
|
302
|
+
seed={agent.avatarSeed}
|
|
303
|
+
avatarUrl={agent.avatarUrl}
|
|
304
|
+
name={agent.name}
|
|
305
|
+
size={16}
|
|
306
|
+
className="ring-1 ring-surface"
|
|
307
|
+
/>
|
|
308
|
+
))}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
183
312
|
</div>
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
313
|
+
)
|
|
314
|
+
})
|
|
315
|
+
: sources.map((source, idx) => {
|
|
316
|
+
const scopedAgents = scopedAgentsFor(source.agentIds)
|
|
317
|
+
const active = selectedKnowledgeSourceId === source.id
|
|
318
|
+
return (
|
|
319
|
+
<div
|
|
320
|
+
key={source.id}
|
|
321
|
+
onClick={() => setSelectedKnowledgeSourceId(source.id)}
|
|
322
|
+
className={`p-3 rounded-[12px] border transition-all relative group cursor-pointer ${
|
|
323
|
+
active
|
|
324
|
+
? 'border-accent-bright/25 bg-accent-soft/10'
|
|
325
|
+
: 'border-white/[0.04] bg-transparent hover:bg-surface-2 hover:border-white/[0.1]'
|
|
326
|
+
}`}
|
|
327
|
+
style={{
|
|
328
|
+
animation: 'spring-in 0.5s var(--ease-spring) both',
|
|
329
|
+
animationDelay: `${0.08 + idx * 0.02}s`,
|
|
330
|
+
}}
|
|
331
|
+
>
|
|
332
|
+
<div className="flex items-start justify-between gap-2 mb-1">
|
|
333
|
+
<div className="min-w-0">
|
|
334
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
335
|
+
<span className="font-display text-[13px] font-600 text-text truncate">{source.title}</span>
|
|
336
|
+
<Badge variant="secondary" className="text-[9px] px-1.5 py-0 uppercase">{source.kind}</Badge>
|
|
337
|
+
{source.archivedAt ? (
|
|
338
|
+
<Badge variant="secondary" className="text-[9px] px-1.5 py-0 uppercase text-amber-200">archived</Badge>
|
|
339
|
+
) : source.supersededBySourceId ? (
|
|
340
|
+
<Badge variant="secondary" className="text-[9px] px-1.5 py-0 uppercase text-text-3">superseded</Badge>
|
|
341
|
+
) : null}
|
|
342
|
+
</div>
|
|
343
|
+
<p className="text-[10px] text-text-3/55">
|
|
344
|
+
{source.chunkCount} chunk{source.chunkCount === 1 ? '' : 's'}
|
|
345
|
+
{' • '}
|
|
346
|
+
{formatDate(source.lastIndexedAt)}
|
|
347
|
+
</p>
|
|
197
348
|
</div>
|
|
198
|
-
|
|
199
|
-
|
|
349
|
+
|
|
350
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
351
|
+
<button
|
|
352
|
+
onClick={(event) => {
|
|
353
|
+
event.stopPropagation()
|
|
354
|
+
openSheet(source.id)
|
|
355
|
+
}}
|
|
356
|
+
className="text-text-3/40 hover:text-accent-bright transition-colors p-0.5 cursor-pointer"
|
|
357
|
+
title="Edit"
|
|
358
|
+
>
|
|
359
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
360
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
361
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
362
|
+
</svg>
|
|
363
|
+
</button>
|
|
364
|
+
<button
|
|
365
|
+
onClick={(event) => {
|
|
366
|
+
event.stopPropagation()
|
|
367
|
+
void handleDelete(source.id)
|
|
368
|
+
}}
|
|
369
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5 cursor-pointer"
|
|
370
|
+
title="Delete"
|
|
371
|
+
>
|
|
372
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
373
|
+
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
374
|
+
</svg>
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
{source.topSnippet && (
|
|
380
|
+
<p className="text-[11px] text-text-3/70 line-clamp-3 mb-2">{source.topSnippet}</p>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
384
|
+
<span className={`text-[10px] font-600 ${
|
|
385
|
+
source.syncStatus === 'error'
|
|
386
|
+
? 'text-red-300'
|
|
387
|
+
: source.stale
|
|
388
|
+
? 'text-amber-300'
|
|
389
|
+
: 'text-emerald-300'
|
|
390
|
+
}`}
|
|
391
|
+
>
|
|
392
|
+
{source.syncStatus === 'error' ? 'Sync error' : source.stale ? 'Stale' : 'Ready'}
|
|
393
|
+
</span>
|
|
394
|
+
<span className={`text-[10px] font-600 ${source.scope === 'global' ? 'text-emerald-400' : 'text-amber-400'}`}>
|
|
395
|
+
{source.scope === 'global' ? 'Global' : `${source.agentIds.length} agent(s)`}
|
|
396
|
+
</span>
|
|
397
|
+
{source.sourceLabel && (
|
|
398
|
+
<span className="text-[10px] text-text-3/55 truncate">{source.sourceLabel}</span>
|
|
200
399
|
)}
|
|
201
400
|
</div>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
401
|
+
|
|
402
|
+
{source.tags.length > 0 && (
|
|
403
|
+
<div className="flex items-center gap-1 mt-2 flex-wrap">
|
|
404
|
+
{source.tags.map((tag) => (
|
|
405
|
+
<Badge key={`${source.id}-${tag}`} variant="secondary" className="text-[9px] px-1.5 py-0">{tag}</Badge>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{scopedAgents.length > 0 && (
|
|
411
|
+
<div className="flex items-center gap-1.5 mt-2">
|
|
412
|
+
<div className="flex items-center -space-x-1.5">
|
|
413
|
+
{scopedAgents.slice(0, 5).map((agent) => (
|
|
414
|
+
<AgentAvatar
|
|
415
|
+
key={agent.id}
|
|
416
|
+
seed={agent.avatarSeed}
|
|
417
|
+
avatarUrl={agent.avatarUrl}
|
|
418
|
+
name={agent.name}
|
|
419
|
+
size={16}
|
|
420
|
+
className="ring-1 ring-surface"
|
|
421
|
+
/>
|
|
422
|
+
))}
|
|
423
|
+
</div>
|
|
424
|
+
{scopedAgents.length > 5 && (
|
|
425
|
+
<span className="text-[10px] font-600 text-text-3/60">+{scopedAgents.length - 5}</span>
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
)
|
|
431
|
+
})}
|
|
207
432
|
</div>
|
|
208
433
|
) : error ? (
|
|
209
434
|
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
|
|
@@ -217,7 +442,7 @@ export function KnowledgeList() {
|
|
|
217
442
|
Retry
|
|
218
443
|
</button>
|
|
219
444
|
</div>
|
|
220
|
-
) :
|
|
445
|
+
) : (
|
|
221
446
|
<EmptyState
|
|
222
447
|
icon={
|
|
223
448
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
|
|
@@ -225,11 +450,11 @@ export function KnowledgeList() {
|
|
|
225
450
|
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
|
226
451
|
</svg>
|
|
227
452
|
}
|
|
228
|
-
title=
|
|
229
|
-
subtitle=
|
|
453
|
+
title={showingHits ? 'No matching knowledge chunks' : 'No knowledge sources yet'}
|
|
454
|
+
subtitle={showingHits ? 'Try a broader query or clear filters' : 'Add a manual note, upload a file, or import a URL'}
|
|
230
455
|
action={{ label: '+ Add Knowledge', onClick: () => openSheet() }}
|
|
231
456
|
/>
|
|
232
|
-
)
|
|
457
|
+
)}
|
|
233
458
|
</div>
|
|
234
459
|
)
|
|
235
460
|
}
|