@swarmclawai/swarmclaw 1.3.4 → 1.3.5

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 (69) hide show
  1. package/README.md +15 -76
  2. package/package.json +1 -1
  3. package/skills/swarmclaw.md +17 -0
  4. package/src/app/api/agents/[id]/dream/route.ts +45 -0
  5. package/src/app/api/knowledge/[id]/route.ts +48 -49
  6. package/src/app/api/knowledge/hygiene/route.ts +13 -0
  7. package/src/app/api/knowledge/route.ts +70 -42
  8. package/src/app/api/knowledge/sources/[id]/archive/route.ts +15 -0
  9. package/src/app/api/knowledge/sources/[id]/restore/route.ts +10 -0
  10. package/src/app/api/knowledge/sources/[id]/route.ts +1 -0
  11. package/src/app/api/knowledge/sources/[id]/supersede/route.ts +26 -0
  12. package/src/app/api/knowledge/sources/[id]/sync/route.ts +17 -0
  13. package/src/app/api/knowledge/sources/route.ts +1 -0
  14. package/src/app/api/knowledge/upload/route.ts +3 -51
  15. package/src/app/api/memory/dream/[id]/route.ts +19 -0
  16. package/src/app/api/memory/dream/route.ts +34 -0
  17. package/src/app/knowledge/layout.tsx +1 -1
  18. package/src/app/knowledge/page.tsx +2 -22
  19. package/src/app/protocols/page.tsx +21 -2
  20. package/src/cli/index.js +16 -0
  21. package/src/cli/spec.js +5 -0
  22. package/src/components/agents/agent-sheet.tsx +65 -0
  23. package/src/components/chat/message-bubble.tsx +10 -0
  24. package/src/components/knowledge/grounding-panel.tsx +99 -0
  25. package/src/components/knowledge/knowledge-detail.tsx +402 -0
  26. package/src/components/knowledge/knowledge-list.tsx +351 -126
  27. package/src/components/knowledge/knowledge-sheet.tsx +208 -119
  28. package/src/components/memory/dream-history.tsx +155 -0
  29. package/src/components/memory/memory-card.tsx +7 -0
  30. package/src/components/memory/memory-detail.tsx +46 -0
  31. package/src/components/runs/run-list.tsx +23 -0
  32. package/src/lib/server/api-routes.test.ts +43 -2
  33. package/src/lib/server/chat-execution/chat-execution-grounding.test.ts +127 -0
  34. package/src/lib/server/chat-execution/chat-execution-types.ts +8 -1
  35. package/src/lib/server/chat-execution/chat-execution.ts +1 -0
  36. package/src/lib/server/chat-execution/chat-turn-finalization.ts +21 -1
  37. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +6 -1
  38. package/src/lib/server/chat-execution/post-stream-finalization.ts +15 -3
  39. package/src/lib/server/chat-execution/prompt-sections.ts +29 -3
  40. package/src/lib/server/chat-execution/stream-agent-chat.ts +6 -1
  41. package/src/lib/server/execution-engine/task-attempt.ts +8 -2
  42. package/src/lib/server/knowledge-import.ts +159 -0
  43. package/src/lib/server/knowledge-sources.test.ts +215 -0
  44. package/src/lib/server/knowledge-sources.ts +1266 -0
  45. package/src/lib/server/memory/dream-cycles.ts +49 -0
  46. package/src/lib/server/memory/dream-idle-callback.ts +38 -0
  47. package/src/lib/server/memory/dream-service.ts +315 -0
  48. package/src/lib/server/memory/memory-db.ts +37 -2
  49. package/src/lib/server/protocols/protocol-agent-turn.ts +7 -0
  50. package/src/lib/server/protocols/protocol-run-lifecycle.ts +19 -6
  51. package/src/lib/server/protocols/protocol-service.test.ts +99 -0
  52. package/src/lib/server/protocols/protocol-step-helpers.ts +7 -1
  53. package/src/lib/server/protocols/protocol-step-processors.ts +16 -3
  54. package/src/lib/server/protocols/protocol-types.ts +4 -0
  55. package/src/lib/server/runtime/daemon-state/core.ts +6 -1
  56. package/src/lib/server/runtime/run-ledger.test.ts +120 -0
  57. package/src/lib/server/runtime/run-ledger.ts +27 -1
  58. package/src/lib/server/runtime/session-run-manager/drain.ts +5 -0
  59. package/src/lib/server/runtime/session-run-manager/state.ts +19 -2
  60. package/src/lib/server/storage-normalization.ts +5 -0
  61. package/src/lib/server/storage.ts +15 -0
  62. package/src/stores/slices/ui-slice.ts +4 -0
  63. package/src/types/agent.ts +7 -0
  64. package/src/types/dream.ts +45 -0
  65. package/src/types/index.ts +1 -0
  66. package/src/types/message.ts +3 -0
  67. package/src/types/misc.ts +131 -0
  68. package/src/types/protocol.ts +4 -0
  69. package/src/types/run.ts +4 -1
@@ -0,0 +1,34 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { log } from '@/lib/server/logger'
3
+
4
+ const TAG = 'api-dream'
5
+
6
+ export async function GET(req: Request) {
7
+ try {
8
+ const { searchParams } = new URL(req.url)
9
+ const agentId = searchParams.get('agentId') ?? undefined
10
+ const limit = Math.max(1, Math.min(500, Number(searchParams.get('limit')) || 50))
11
+ const { listDreamCycles } = await import('@/lib/server/memory/dream-cycles')
12
+ const cycles = listDreamCycles(agentId, limit)
13
+ return NextResponse.json({ ok: true, cycles })
14
+ } catch (err: unknown) {
15
+ log.error(TAG, 'GET failed:', err)
16
+ return NextResponse.json({ ok: false, error: String((err as Error)?.message || err) }, { status: 500 })
17
+ }
18
+ }
19
+
20
+ export async function POST(req: Request) {
21
+ try {
22
+ const body = await req.json().catch(() => ({})) as Record<string, unknown>
23
+ const agentId = typeof body.agentId === 'string' ? body.agentId : undefined
24
+ if (!agentId) {
25
+ return NextResponse.json({ ok: false, error: 'agentId is required' }, { status: 400 })
26
+ }
27
+ const { executeDreamCycle } = await import('@/lib/server/memory/dream-service')
28
+ const cycle = await executeDreamCycle(agentId, 'manual')
29
+ return NextResponse.json({ ok: true, cycle })
30
+ } catch (err: unknown) {
31
+ log.error(TAG, 'POST failed:', err)
32
+ return NextResponse.json({ ok: false, error: String((err as Error)?.message || err) }, { status: 500 })
33
+ }
34
+ }
@@ -10,7 +10,7 @@ export default function KnowledgeLayout({ children }: { children: React.ReactNod
10
10
  <>
11
11
  <SidebarPanelShell
12
12
  title="Knowledge"
13
- createLabel="Knowledge Entry"
13
+ createLabel="New Source"
14
14
  onNew={() => useAppStore.getState().setKnowledgeSheetOpen(true)}
15
15
  >
16
16
  <KnowledgeList />
@@ -1,27 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { useAppStore } from '@/stores/use-app-store'
4
- import { KnowledgeList } from '@/components/knowledge/knowledge-list'
3
+ import { KnowledgeDetail } from '@/components/knowledge/knowledge-detail'
5
4
 
6
5
  export default function KnowledgePage() {
7
- return (
8
- <div className="flex-1 flex flex-col h-full">
9
- <div className="flex items-center px-6 pt-5 pb-3 shrink-0">
10
- <h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] capitalize flex-1">
11
- Knowledge
12
- </h2>
13
- <button
14
- onClick={() => useAppStore.getState().setKnowledgeSheetOpen(true)}
15
- className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-bright/15 transition-all cursor-pointer"
16
- style={{ fontFamily: 'inherit' }}
17
- >
18
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
19
- <line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
20
- </svg>
21
- Knowledge Entry
22
- </button>
23
- </div>
24
- <KnowledgeList />
25
- </div>
26
- )
6
+ return <KnowledgeDetail />
27
7
  }
@@ -16,6 +16,7 @@ import {
16
16
  import { useTasksQuery } from '@/features/tasks/queries'
17
17
  import { MainContent } from '@/components/layout/main-content'
18
18
  import { StructuredSessionLauncher } from '@/components/protocols/structured-session-launcher'
19
+ import { GroundingPanel } from '@/components/knowledge/grounding-panel'
19
20
  import { timeAgo } from '@/lib/time-format'
20
21
  import type {
21
22
  BoardTask,
@@ -164,8 +165,8 @@ export default function ProtocolsPage() {
164
165
  const runActionMutation = useProtocolRunActionMutation()
165
166
  const upsertTemplateMutation = useUpsertProtocolTemplateMutation()
166
167
  const deleteTemplateMutation = useDeleteProtocolTemplateMutation()
167
- const templates = templatesQuery.data ?? []
168
- const runs = runsQuery.data ?? []
168
+ const templates = useMemo(() => templatesQuery.data ?? [], [templatesQuery.data])
169
+ const runs = useMemo(() => runsQuery.data ?? [], [runsQuery.data])
169
170
  const detail = detailQuery.data ?? null
170
171
  const agents = agentsQuery.data ?? {}
171
172
  const chatrooms = chatroomsQuery.data ?? {}
@@ -203,6 +204,14 @@ export default function ProtocolsPage() {
203
204
  const selectedTemplate = useMemo(() => templates.find((template) => template.id === form.templateId) || null, [form.templateId, templates])
204
205
  const customTemplates = useMemo(() => templates.filter((template) => !template.builtIn), [templates])
205
206
  const builtInTemplates = useMemo(() => templates.filter((template) => template.builtIn), [templates])
207
+ const artifactCitationMap = useMemo(() => {
208
+ const map = new Map<string, ProtocolRunEvent['citations']>()
209
+ for (const event of detail?.events || []) {
210
+ if (!event.artifactId || !event.citations?.length) continue
211
+ map.set(event.artifactId, event.citations)
212
+ }
213
+ return map
214
+ }, [detail?.events])
206
215
  const filteredRuns = useMemo(() => {
207
216
  const search = runSearch.trim().toLowerCase()
208
217
  return runs.filter((run) => {
@@ -1167,6 +1176,11 @@ export default function ProtocolsPage() {
1167
1176
  <span className="text-[11px] text-text-3/60">{artifact.kind.replace(/_/g, ' ')}</span>
1168
1177
  </div>
1169
1178
  <div className="mt-2 whitespace-pre-wrap text-[13px] leading-relaxed text-text-2">{artifact.content}</div>
1179
+ {artifactCitationMap.get(artifact.id)?.length ? (
1180
+ <div className="mt-3">
1181
+ <GroundingPanel citations={artifactCitationMap.get(artifact.id)} compact />
1182
+ </div>
1183
+ ) : null}
1170
1184
  </div>
1171
1185
  ))}
1172
1186
  </div>
@@ -1220,6 +1234,11 @@ export default function ProtocolsPage() {
1220
1234
  <div className="text-[11px] text-text-3/55">{timeAgo(event.createdAt)}</div>
1221
1235
  </div>
1222
1236
  <div className="mt-2 text-[13px] leading-relaxed text-text-2">{event.summary}</div>
1237
+ {event.citations?.length ? (
1238
+ <div className="mt-3">
1239
+ <GroundingPanel citations={event.citations} compact />
1240
+ </div>
1241
+ ) : null}
1223
1242
  </div>
1224
1243
  ))}
1225
1244
  </div>
package/src/cli/index.js CHANGED
@@ -26,6 +26,8 @@ const COMMAND_GROUPS = [
26
26
  cmd('clone', 'POST', '/agents/:id/clone', 'Clone an agent'),
27
27
  cmd('bulk-update', 'PATCH', '/agents/bulk', 'Bulk update agents', { expectsJsonBody: true }),
28
28
  cmd('status', 'GET', '/agents/:id/status', 'Get live status for an agent'),
29
+ cmd('dream', 'GET', '/agents/:id/dream', 'Get agent dream config and recent cycles'),
30
+ cmd('dream-update', 'PATCH', '/agents/:id/dream', 'Update agent dream config', { expectsJsonBody: true }),
29
31
  ],
30
32
  },
31
33
  {
@@ -268,6 +270,17 @@ const COMMAND_GROUPS = [
268
270
  requestType: 'upload',
269
271
  inputPositional: 'filePath',
270
272
  }),
273
+ cmd('hygiene', 'GET', '/knowledge/hygiene', 'Get knowledge hygiene summary'),
274
+ cmd('hygiene-run', 'POST', '/knowledge/hygiene', 'Run knowledge hygiene maintenance'),
275
+ cmd('sources', 'GET', '/knowledge/sources', 'List knowledge sources'),
276
+ cmd('source-create', 'POST', '/knowledge/sources', 'Create knowledge source', { expectsJsonBody: true }),
277
+ cmd('source-get', 'GET', '/knowledge/sources/:id', 'Get knowledge source detail'),
278
+ cmd('source-update', 'PUT', '/knowledge/sources/:id', 'Update knowledge source', { expectsJsonBody: true }),
279
+ cmd('source-delete', 'DELETE', '/knowledge/sources/:id', 'Delete knowledge source'),
280
+ cmd('source-archive', 'POST', '/knowledge/sources/:id/archive', 'Archive knowledge source', { expectsJsonBody: true }),
281
+ cmd('source-restore', 'POST', '/knowledge/sources/:id/restore', 'Restore archived knowledge source'),
282
+ cmd('source-supersede', 'POST', '/knowledge/sources/:id/supersede', 'Mark source as superseded', { expectsJsonBody: true }),
283
+ cmd('source-sync', 'POST', '/knowledge/sources/:id/sync', 'Re-sync file/URL knowledge source'),
271
284
  ],
272
285
  },
273
286
  {
@@ -290,6 +303,9 @@ const COMMAND_GROUPS = [
290
303
  cmd('maintenance', 'GET', '/memory/maintenance', 'Analyze memory dedupe/prune candidates'),
291
304
  cmd('maintenance-run', 'POST', '/memory/maintenance', 'Run memory dedupe/prune maintenance', { expectsJsonBody: true }),
292
305
  cmd('graph', 'GET', '/memory/graph', 'Get memory graph (nodes and links) for visualization'),
306
+ cmd('dream', 'GET', '/memory/dream', 'List dream cycles'),
307
+ cmd('dream-trigger', 'POST', '/memory/dream', 'Trigger a dream cycle', { expectsJsonBody: true }),
308
+ cmd('dream-get', 'GET', '/memory/dream/:id', 'Get dream cycle by id'),
293
309
  ],
294
310
  },
295
311
  {
package/src/cli/spec.js CHANGED
@@ -12,6 +12,8 @@ const COMMAND_GROUPS = {
12
12
  restore: { description: 'Restore a trashed agent', method: 'POST', path: '/agents/trash' },
13
13
  purge: { description: 'Permanently delete a trashed agent', method: 'DELETE', path: '/agents/trash' },
14
14
  status: { description: 'Get live status for an agent', method: 'GET', path: '/agents/:id/status', params: ['id'] },
15
+ dream: { description: 'Get agent dream config and recent cycles', method: 'GET', path: '/agents/:id/dream', params: ['id'] },
16
+ 'dream-update': { description: 'Update agent dream config', method: 'PATCH', path: '/agents/:id/dream', params: ['id'] },
15
17
  },
16
18
  },
17
19
  activity: {
@@ -218,6 +220,9 @@ const COMMAND_GROUPS = {
218
220
  delete: { description: 'Delete memory entry', method: 'DELETE', path: '/memory/:id', params: ['id'] },
219
221
  maintenance: { description: 'Analyze memory dedupe/prune candidates', method: 'GET', path: '/memory/maintenance' },
220
222
  'maintenance-run': { description: 'Run memory dedupe/prune maintenance', method: 'POST', path: '/memory/maintenance' },
223
+ dream: { description: 'List dream cycles', method: 'GET', path: '/memory/dream' },
224
+ 'dream-trigger': { description: 'Trigger a dream cycle', method: 'POST', path: '/memory/dream' },
225
+ 'dream-get': { description: 'Get dream cycle by id', method: 'GET', path: '/memory/dream/:id', params: ['id'] },
221
226
  },
222
227
  },
223
228
  'memory-images': {
@@ -236,6 +236,9 @@ export function AgentSheet() {
236
236
  const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
237
237
  const [heartbeatModel, setHeartbeatModel] = useState('')
238
238
  const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
239
+ const [dreamEnabled, setDreamEnabled] = useState(false)
240
+ const [dreamCooldownMinutes, setDreamCooldownMinutes] = useState('360')
241
+ const [dreamTier2Enabled, setDreamTier2Enabled] = useState(true)
239
242
  const [orchestratorEnabled, setOrchestratorEnabled] = useState(false)
240
243
  const [orchestratorMission, setOrchestratorMission] = useState('')
241
244
  const [orchestratorWakeInterval, setOrchestratorWakeInterval] = useState('5m')
@@ -437,6 +440,9 @@ export function AgentSheet() {
437
440
  setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
438
441
  setHeartbeatModel(editing.heartbeatModel || '')
439
442
  setHeartbeatPrompt(editing.heartbeatPrompt || '')
443
+ setDreamEnabled(editing.dreamEnabled || false)
444
+ setDreamCooldownMinutes(editing.dreamConfig?.cooldownMinutes != null ? String(editing.dreamConfig.cooldownMinutes) : '360')
445
+ setDreamTier2Enabled(editing.dreamConfig?.tier2Enabled !== false)
440
446
  setOrchestratorEnabled(editing.orchestratorEnabled || false)
441
447
  setOrchestratorMission(editing.orchestratorMission || '')
442
448
  setOrchestratorWakeInterval(typeof editing.orchestratorWakeInterval === 'string' ? editing.orchestratorWakeInterval : typeof editing.orchestratorWakeInterval === 'number' ? `${editing.orchestratorWakeInterval}s` : '5m')
@@ -516,6 +522,9 @@ export function AgentSheet() {
516
522
  setHeartbeatIntervalSec(parseDurationToSec(src.heartbeatInterval, src.heartbeatIntervalSec))
517
523
  setHeartbeatModel(src.heartbeatModel || '')
518
524
  setHeartbeatPrompt(src.heartbeatPrompt || '')
525
+ setDreamEnabled(src.dreamEnabled || false)
526
+ setDreamCooldownMinutes(src.dreamConfig?.cooldownMinutes != null ? String(src.dreamConfig.cooldownMinutes) : '360')
527
+ setDreamTier2Enabled(src.dreamConfig?.tier2Enabled !== false)
519
528
  setOrchestratorEnabled(src.orchestratorEnabled || false)
520
529
  setOrchestratorMission(src.orchestratorMission || '')
521
530
  setOrchestratorWakeInterval(typeof src.orchestratorWakeInterval === 'string' ? src.orchestratorWakeInterval : typeof src.orchestratorWakeInterval === 'number' ? `${src.orchestratorWakeInterval}s` : '5m')
@@ -794,6 +803,10 @@ export function AgentSheet() {
794
803
  heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
795
804
  heartbeatModel: heartbeatModel.trim() || null,
796
805
  heartbeatPrompt: heartbeatPrompt.trim() || null,
806
+ dreamEnabled,
807
+ dreamConfig: dreamEnabled
808
+ ? { cooldownMinutes: Number(dreamCooldownMinutes) || 360, tier2Enabled: dreamTier2Enabled }
809
+ : null,
797
810
  orchestratorEnabled,
798
811
  orchestratorMission: orchestratorMission.trim() || undefined,
799
812
  orchestratorWakeInterval: orchestratorWakeInterval.trim() || null,
@@ -818,6 +831,8 @@ export function AgentSheet() {
818
831
  data.heartbeatIntervalSec = null
819
832
  data.heartbeatModel = null
820
833
  data.heartbeatPrompt = null
834
+ data.dreamEnabled = false
835
+ data.dreamConfig = null
821
836
  }
822
837
  const savedAgent = editing
823
838
  ? await updateAgent(editing.id, data)
@@ -1007,6 +1022,7 @@ export function AgentSheet() {
1007
1022
  if (skills.length > 0 || skillIds.length > 0 || mcpServerIds.length > 0 || mcpDisabledTools.length > 0) badges.push('Skills & MCP')
1008
1023
  if (toolsDifferFromDefault || filesystemScope === 'machine') badges.push('Tools')
1009
1024
  if (budgetEnabled) badges.push('Budget')
1025
+ if (dreamEnabled) badges.push('Dreaming')
1010
1026
  if (disabled) badges.push('Disabled')
1011
1027
  if (autoRecovery) badges.push('Recovery')
1012
1028
  if (projectId) badges.push('Project')
@@ -1018,6 +1034,7 @@ export function AgentSheet() {
1018
1034
  autoRecovery,
1019
1035
  budgetEnabled,
1020
1036
  disabled,
1037
+ dreamEnabled,
1021
1038
  fallbackCredentialIds.length,
1022
1039
  filesystemScope,
1023
1040
  identityPersonaLabel,
@@ -1905,6 +1922,54 @@ export function AgentSheet() {
1905
1922
  <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${heartbeatEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
1906
1923
  </button>
1907
1924
  </div>
1925
+ <div className="flex items-center justify-between gap-4 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4 mt-3">
1926
+ <div className="min-w-0">
1927
+ <div className="flex items-center gap-2">
1928
+ <p className="text-[14px] font-600 text-text">Dreaming</p>
1929
+ <HintTip text="When enabled, this agent consolidates and optimizes its memories during idle periods" />
1930
+ </div>
1931
+ <p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
1932
+ Consolidate, decay, and reflect on memories when the agent is idle.
1933
+ </p>
1934
+ </div>
1935
+ <button
1936
+ type="button"
1937
+ onClick={() => setDreamEnabled((current) => !current)}
1938
+ className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${dreamEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
1939
+ aria-pressed={dreamEnabled}
1940
+ >
1941
+ <span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${dreamEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
1942
+ </button>
1943
+ </div>
1944
+ {dreamEnabled && (
1945
+ <div className="mt-3 rounded-[14px] border border-white/[0.04] bg-white/[0.01] px-4 py-4 space-y-3">
1946
+ <div>
1947
+ <label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
1948
+ Cooldown (minutes) <HintTip text="Minimum minutes between dream cycles" />
1949
+ </label>
1950
+ <input
1951
+ type="number"
1952
+ value={dreamCooldownMinutes}
1953
+ onChange={(e) => setDreamCooldownMinutes(e.target.value)}
1954
+ min={1}
1955
+ placeholder="360"
1956
+ className={inputClass}
1957
+ style={{ fontFamily: 'inherit' }}
1958
+ />
1959
+ </div>
1960
+ <label className="flex items-center gap-3 cursor-pointer">
1961
+ <div
1962
+ onClick={() => setDreamTier2Enabled((current) => !current)}
1963
+ className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${dreamTier2Enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
1964
+ >
1965
+ <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${dreamTier2Enabled ? 'left-[22px]' : 'left-0.5'}`} />
1966
+ </div>
1967
+ <span className="flex items-center gap-2 text-[13px] text-text-2">
1968
+ Tier 2 Reflection <HintTip text="Use the agent's LLM to reflect on memories and produce consolidated insights" />
1969
+ </span>
1970
+ </label>
1971
+ </div>
1972
+ )}
1908
1973
  </SectionCard>
1909
1974
  )}
1910
1975
 
@@ -25,6 +25,7 @@ import { ConnectorPlatformIcon, getConnectorPlatformLabel } from '@/components/s
25
25
  import { copyTextToClipboard } from '@/lib/clipboard'
26
26
  import { formatMessageTimestamp } from '@/lib/chat/chat-display'
27
27
  import { stripAllInternalMetadata } from '@/lib/strip-internal-metadata'
28
+ import { GroundingPanel } from '@/components/knowledge/grounding-panel'
28
29
 
29
30
  /** Parse delegation-source metadata prefix from system messages */
30
31
  const DELEGATION_SOURCE_RE = /^\[delegation-source:([^:]*):([^:]*):([^\]]*)\]/
@@ -974,6 +975,15 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
974
975
  </div>
975
976
  ) : null}
976
977
 
978
+ {!isUser && (message.citations?.length || message.retrievalTrace?.hits?.length) ? (
979
+ <div className="mt-2 max-w-[85%] md:max-w-[72%]">
980
+ <GroundingPanel
981
+ citations={message.citations}
982
+ retrievalTrace={message.retrievalTrace}
983
+ />
984
+ </div>
985
+ ) : null}
986
+
977
987
  {/* Bookmark indicator */}
978
988
  {message.bookmarked && (
979
989
  <div className={`flex items-center gap-1 mt-1 px-1 ${isUser ? 'justify-end' : ''}`}>
@@ -0,0 +1,99 @@
1
+ 'use client'
2
+
3
+ import type { KnowledgeCitation, KnowledgeRetrievalTrace } from '@/types'
4
+
5
+ function dedupeCitations(citations: KnowledgeCitation[]): KnowledgeCitation[] {
6
+ const seen = new Set<string>()
7
+ const out: KnowledgeCitation[] = []
8
+ for (const citation of citations) {
9
+ const key = `${citation.sourceId}:${citation.chunkId}`
10
+ if (seen.has(key)) continue
11
+ seen.add(key)
12
+ out.push(citation)
13
+ }
14
+ return out
15
+ }
16
+
17
+ export function GroundingPanel(props: {
18
+ citations?: KnowledgeCitation[]
19
+ retrievalTrace?: KnowledgeRetrievalTrace | null
20
+ title?: string
21
+ compact?: boolean
22
+ className?: string
23
+ }) {
24
+ const explicit = Array.isArray(props.citations) ? dedupeCitations(props.citations) : []
25
+ const fallback = props.retrievalTrace?.hits ? dedupeCitations(props.retrievalTrace.hits) : []
26
+ const items = explicit.length > 0 ? explicit : fallback
27
+ if (items.length === 0) return null
28
+
29
+ const title = props.title || 'Grounding'
30
+ const compact = props.compact === true
31
+ const selected = explicit.length > 0
32
+
33
+ return (
34
+ <details className={`group rounded-[12px] border border-sky-400/15 bg-sky-400/[0.04] ${props.className || ''}`}>
35
+ <summary className="flex cursor-pointer list-none items-center gap-2 px-3.5 py-2.5 select-none [&::-webkit-details-marker]:hidden">
36
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0 text-sky-300/70 transition-transform group-open:rotate-90">
37
+ <polyline points="9 18 15 12 9 6" />
38
+ </svg>
39
+ <span className="text-[11px] font-600 uppercase tracking-[0.05em] text-sky-200/80">{title}</span>
40
+ <span className="text-[10px] font-mono text-text-3/50">
41
+ {selected ? `${explicit.length} citation${explicit.length === 1 ? '' : 's'}` : `${fallback.length} retrieved`}
42
+ </span>
43
+ {props.retrievalTrace?.selectorStatus && (
44
+ <span className="ml-auto text-[10px] text-text-3/45">
45
+ {props.retrievalTrace.selectorStatus === 'selected'
46
+ ? 'selected'
47
+ : props.retrievalTrace.selectorStatus === 'no_match'
48
+ ? 'candidates'
49
+ : 'retrieved'}
50
+ </span>
51
+ )}
52
+ </summary>
53
+
54
+ <div className="space-y-2 px-3.5 pb-3 pt-1">
55
+ {props.retrievalTrace?.query && (
56
+ <div className="rounded-[10px] border border-white/[0.06] bg-black/20 px-3 py-2 text-[11px] text-text-3/75">
57
+ Query: <span className="text-text-2">{props.retrievalTrace.query}</span>
58
+ </div>
59
+ )}
60
+
61
+ {items.map((citation) => (
62
+ <div key={`${citation.sourceId}:${citation.chunkId}`} className="rounded-[10px] border border-white/[0.06] bg-black/15 px-3 py-2.5">
63
+ <div className="flex items-start justify-between gap-3">
64
+ <div className="min-w-0">
65
+ <div className="text-[12px] font-600 text-text-2">{citation.sourceTitle}</div>
66
+ <div className="mt-0.5 text-[10px] text-text-3/60">
67
+ Chunk {citation.chunkIndex + 1} of {citation.chunkCount}
68
+ {citation.sectionLabel ? ` • ${citation.sectionLabel}` : ''}
69
+ </div>
70
+ </div>
71
+ <div className="shrink-0 text-[10px] font-mono text-text-3/55">
72
+ {citation.score.toFixed(2)}
73
+ </div>
74
+ </div>
75
+
76
+ {citation.whyMatched && (
77
+ <div className="mt-2 text-[11px] text-sky-100/78">{citation.whyMatched}</div>
78
+ )}
79
+
80
+ <div className={`mt-2 whitespace-pre-wrap break-words text-text-2/85 ${compact ? 'text-[11px]' : 'text-[12px]'}`}>
81
+ {citation.snippet}
82
+ </div>
83
+
84
+ {(citation.sourceLabel || citation.sourceUrl) && (
85
+ <div className="mt-2 flex flex-wrap items-center gap-2 text-[10px] text-text-3/55">
86
+ {citation.sourceLabel && <span>{citation.sourceLabel}</span>}
87
+ {citation.sourceUrl && (
88
+ <a href={citation.sourceUrl} target="_blank" rel="noreferrer" className="text-sky-300 hover:underline">
89
+ open source
90
+ </a>
91
+ )}
92
+ </div>
93
+ )}
94
+ </div>
95
+ ))}
96
+ </div>
97
+ </details>
98
+ )
99
+ }