@swarmclawai/swarmclaw 0.6.0 → 0.6.3

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 (118) hide show
  1. package/README.md +56 -42
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +16 -35
  15. package/src/app/api/tts/stream/route.ts +14 -42
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +76 -24
  31. package/src/components/chat/chat-header.tsx +522 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +113 -8
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +84 -17
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +125 -14
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/connector-routing.test.ts +118 -1
  78. package/src/lib/server/connectors/discord.ts +31 -8
  79. package/src/lib/server/connectors/manager.ts +594 -16
  80. package/src/lib/server/connectors/media.ts +5 -0
  81. package/src/lib/server/connectors/telegram.ts +12 -2
  82. package/src/lib/server/connectors/types.ts +2 -0
  83. package/src/lib/server/connectors/whatsapp.ts +28 -2
  84. package/src/lib/server/elevenlabs.test.ts +60 -0
  85. package/src/lib/server/elevenlabs.ts +103 -0
  86. package/src/lib/server/heartbeat-service.ts +8 -1
  87. package/src/lib/server/main-agent-loop.ts +1 -1
  88. package/src/lib/server/memory-consolidation.ts +15 -2
  89. package/src/lib/server/memory-db.ts +134 -6
  90. package/src/lib/server/mime.ts +51 -0
  91. package/src/lib/server/openclaw-gateway.ts +2 -2
  92. package/src/lib/server/orchestrator-lg.ts +2 -0
  93. package/src/lib/server/orchestrator.ts +5 -2
  94. package/src/lib/server/playwright-proxy.mjs +2 -3
  95. package/src/lib/server/prompt-runtime-context.ts +53 -0
  96. package/src/lib/server/queue.ts +182 -8
  97. package/src/lib/server/session-tools/canvas.ts +67 -0
  98. package/src/lib/server/session-tools/connector.ts +583 -63
  99. package/src/lib/server/session-tools/crud.ts +21 -0
  100. package/src/lib/server/session-tools/delegate.ts +68 -4
  101. package/src/lib/server/session-tools/file.ts +26 -7
  102. package/src/lib/server/session-tools/git.ts +71 -0
  103. package/src/lib/server/session-tools/http.ts +57 -0
  104. package/src/lib/server/session-tools/index.ts +8 -0
  105. package/src/lib/server/session-tools/memory.ts +1 -0
  106. package/src/lib/server/session-tools/search-providers.ts +16 -8
  107. package/src/lib/server/session-tools/subagent.ts +106 -0
  108. package/src/lib/server/session-tools/web.ts +118 -8
  109. package/src/lib/server/stream-agent-chat.ts +39 -10
  110. package/src/lib/server/task-mention.ts +41 -0
  111. package/src/lib/sessions.ts +10 -0
  112. package/src/lib/soul-library.ts +103 -0
  113. package/src/lib/task-dedupe.ts +26 -0
  114. package/src/lib/tool-definitions.ts +2 -0
  115. package/src/lib/tts.ts +2 -2
  116. package/src/stores/use-app-store.ts +5 -1
  117. package/src/stores/use-chat-store.ts +65 -2
  118. package/src/types/index.ts +32 -2
@@ -9,6 +9,58 @@ import { spawnSync } from 'child_process'
9
9
  import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
10
10
  import { getSearchProvider } from './search-providers'
11
11
 
12
+ // ---------------------------------------------------------------------------
13
+ // Search result compression — summarize verbose results before injecting into context
14
+ // ---------------------------------------------------------------------------
15
+
16
+ async function compressSearchResults(
17
+ results: Array<{ title?: string; url?: string; snippet?: string }>,
18
+ query: string,
19
+ bctx: ToolBuildContext,
20
+ ): Promise<string | null> {
21
+ const session = bctx.resolveCurrentSession?.()
22
+ if (!session?.provider || !session?.model) return null
23
+
24
+ const { getProvider } = await import('@/lib/providers')
25
+ const { loadCredentials, decryptKey } = await import('../storage')
26
+ const providerEntry = getProvider(session.provider)
27
+ if (!providerEntry?.handler?.streamChat) return null
28
+
29
+ // Resolve API key
30
+ let apiKey: string | undefined
31
+ if (session.credentialId) {
32
+ const creds = loadCredentials()
33
+ const cred = creds[session.credentialId]
34
+ if (cred) apiKey = decryptKey(cred)
35
+ }
36
+
37
+ const systemPrompt = 'You are a search result summarizer. Condense search results into a concise reference. Keep key facts, URLs, and data points. Remove filler and redundancy. Output plain text, not JSON.'
38
+ const message = `Query: "${query}"\n\nResults:\n${JSON.stringify(results, null, 1)}\n\nSummarize these results concisely.`
39
+
40
+ let compressed = ''
41
+ await providerEntry.handler.streamChat({
42
+ session: { ...session, messages: [] },
43
+ message,
44
+ apiKey,
45
+ systemPrompt,
46
+ write: (raw: string) => {
47
+ // Extract text data from SSE lines
48
+ const lines = raw.split('\n').filter(Boolean)
49
+ for (const line of lines) {
50
+ if (!line.startsWith('data: ')) continue
51
+ try {
52
+ const ev = JSON.parse(line.slice(6))
53
+ if (ev.t === 'd' && ev.text) compressed += ev.text
54
+ } catch { /* skip */ }
55
+ }
56
+ },
57
+ active: new Map(),
58
+ loadHistory: () => [],
59
+ })
60
+
61
+ return compressed.trim() || null
62
+ }
63
+
12
64
  // ---------------------------------------------------------------------------
13
65
  // Global registry of active browser instances for cleanup sweeps
14
66
  // ---------------------------------------------------------------------------
@@ -70,9 +122,18 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
70
122
  const settings = loadSettings()
71
123
  const provider = await getSearchProvider(settings)
72
124
  const results = await provider.search(query, limit)
73
- return results.length > 0
74
- ? JSON.stringify(results, null, 2)
75
- : 'No results found.'
125
+ if (results.length === 0) return 'No results found.'
126
+ const raw = JSON.stringify(results, null, 2)
127
+ // Compress search results if they exceed 2000 chars
128
+ if (raw.length > 2000) {
129
+ try {
130
+ const compressed = await compressSearchResults(results, query, bctx)
131
+ if (compressed) return compressed
132
+ } catch {
133
+ // Compression failed — fall through to raw results
134
+ }
135
+ }
136
+ return raw
76
137
  } catch (err: unknown) {
77
138
  return `Error searching web: ${err instanceof Error ? err.message : String(err)}`
78
139
  }
@@ -217,10 +278,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
217
278
 
218
279
  if (Array.isArray(content)) {
219
280
  const parts: string[] = []
220
- let hasBinaryImage = false
281
+ const contentHasBinaryImage = content.some((c) => c.type === 'image' && !!c.data)
221
282
  for (const c of content) {
222
283
  if (c.type === 'image' && c.data) {
223
- hasBinaryImage = true
224
284
  const imageBuffer = Buffer.from(c.data, 'base64')
225
285
  const filename = `screenshot-${Date.now()}.png`
226
286
  const filepath = path.join(UPLOAD_DIR, filename)
@@ -245,8 +305,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
245
305
  if (fs.existsSync(srcPath)) {
246
306
  const ext = path.extname(srcPath).slice(1).toLowerCase()
247
307
  const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp']
248
- // Skip file-path images if we already have a binary image (avoids duplicates)
249
- if (IMAGE_EXTS.includes(ext) && hasBinaryImage) {
308
+ // Skip file-path images whenever MCP already returned image binary payloads.
309
+ if (IMAGE_EXTS.includes(ext) && contentHasBinaryImage) {
250
310
  parts.push(isError ? text : cleanPlaywrightOutput(text))
251
311
  } else {
252
312
  const filename = `browser-${Date.now()}.${ext}`
@@ -284,6 +344,49 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
284
344
  return JSON.stringify(result)
285
345
  }
286
346
 
347
+ // Best-effort cookie/consent banner dismissal after navigation
348
+ const dismissCookieBanners = async (
349
+ mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>,
350
+ ) => {
351
+ // Wait briefly for consent overlays to appear
352
+ await new Promise((r) => setTimeout(r, 1500))
353
+ const js = `
354
+ (() => {
355
+ const sel = [
356
+ // Common "Reject" / "Reject all" / "Decline" buttons
357
+ 'button[id*="reject" i]', 'button[class*="reject" i]',
358
+ 'a[id*="reject" i]', 'a[class*="reject" i]',
359
+ '[data-testid*="reject" i]', '[data-action="reject"]',
360
+ // OneTrust
361
+ '#onetrust-reject-all-handler',
362
+ // Cookiebot
363
+ '#CybotCookiebotDialogBodyButtonDecline',
364
+ // Didomi
365
+ '#didomi-notice-disagree-button',
366
+ // Quantcast / IAB TCF
367
+ '.qc-cmp2-summary-buttons button:first-child',
368
+ 'button.sp_choice_type_12',
369
+ // Generic patterns
370
+ 'button[aria-label*="reject" i]', 'button[aria-label*="decline" i]',
371
+ 'button[aria-label*="deny" i]', 'button[aria-label*="refuse" i]',
372
+ ];
373
+ for (const s of sel) {
374
+ const el = document.querySelector(s);
375
+ if (el && el.offsetParent !== null) { el.click(); return 'dismissed:' + s; }
376
+ }
377
+ // Fallback: find buttons by visible text
378
+ const btns = [...document.querySelectorAll('button, a[role="button"], [class*="cookie"] button, [class*="consent"] button, [id*="cookie"] button')];
379
+ const rejectRe = /^(reject|reject all|decline|deny|refuse|no,? thanks|only necessary|necessary only)$/i;
380
+ for (const b of btns) {
381
+ const txt = (b.textContent || '').trim();
382
+ if (rejectRe.test(txt) && b.offsetParent !== null) { b.click(); return 'dismissed:text=' + txt; }
383
+ }
384
+ return 'none';
385
+ })()
386
+ `
387
+ await mcpCall('browser_evaluate', { expression: js })
388
+ }
389
+
287
390
  // Action-to-MCP tool mapping
288
391
  const MCP_TOOL_MAP: Record<string, string> = {
289
392
  navigate: 'browser_navigate',
@@ -315,7 +418,14 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
315
418
  const saveTo = typeof params.saveTo === 'string' && params.saveTo.trim()
316
419
  ? params.saveTo.trim()
317
420
  : undefined
318
- return await callMcpTool(mcpTool, args, { saveTo })
421
+ const result = await callMcpTool(mcpTool, args, { saveTo })
422
+
423
+ // After navigation, attempt to dismiss cookie consent banners
424
+ if (action === 'navigate') {
425
+ try { await dismissCookieBanners(callMcpTool) } catch { /* best-effort */ }
426
+ }
427
+
428
+ return result
319
429
  } catch (err: unknown) {
320
430
  return `Error: ${err instanceof Error ? err.message : String(err)}`
321
431
  }
@@ -9,6 +9,7 @@ import { getPluginManager } from './plugins'
9
9
  import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-settings'
10
10
  import { getMemoryDb } from './memory-db'
11
11
  import { logExecution } from './execution-log'
12
+ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
12
13
  import type { Session, Message, UsageRecord } from '@/types'
13
14
  import { extractSuggestions } from './suggestions'
14
15
 
@@ -126,6 +127,12 @@ function buildAgenticExecutionPolicy(opts: {
126
127
  opts.enabledTools.includes('manage_connectors')
127
128
  ? 'If the user wants proactive outreach (e.g., WhatsApp updates), configure connectors and pair with schedules/tasks to deliver status updates.'
128
129
  : '',
130
+ opts.enabledTools.includes('manage_connectors')
131
+ ? 'Autonomous outreach is allowed for significant events (completed/failed tasks, blockers, deadlines, meaningful reminders from memory). Avoid casual or repetitive check-ins.'
132
+ : '',
133
+ opts.enabledTools.includes('manage_connectors')
134
+ ? 'When you proactively message through connectors, keep it concise and purposeful, and avoid sending duplicate updates about the same event.'
135
+ : '',
129
136
  opts.enabledTools.includes('manage_sessions')
130
137
  ? 'When coordinating platform work, inspect existing sessions and avoid duplicating active efforts.'
131
138
  : '',
@@ -163,6 +170,7 @@ function buildAgenticExecutionPolicy(opts: {
163
170
  'The test: if you saw this message from a friend, would you feel compelled to type something back? If not, NO_MESSAGE.',
164
171
  'Ask for confirmation only for high-risk or irreversible actions. For normal low-risk research/build steps, proceed autonomously.',
165
172
  'Default behavior is execution, not interrogation: do not ask exploratory clarification questions when a safe next action exists.',
173
+ 'Do not end every response with a question. Use declarative completion statements by default, and only ask a question when a concrete missing detail blocks the next action.',
166
174
  'Do not pause for a "continue" confirmation after the user has already asked you to execute a goal. Keep moving until blocked by permissions, missing credentials, or hard tool failures.',
167
175
  'Never repeat one-time side effects that are already complete (for example creating the same schedule/task again). Verify state first, then either continue execution or reply HEARTBEAT_OK.',
168
176
  'For main-loop tick messages that begin with "SWARM_MAIN_MISSION_TICK" or "SWARM_MAIN_AUTO_FOLLOWUP", follow that response contract exactly and include one valid [MAIN_LOOP_META] JSON line when you are not returning HEARTBEAT_OK.',
@@ -227,6 +235,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
227
235
  stateModifierParts.push(systemPrompt!.trim())
228
236
  } else {
229
237
  if (settings.userPrompt) stateModifierParts.push(settings.userPrompt)
238
+ stateModifierParts.push(buildCurrentDateTimePromptContext())
230
239
  }
231
240
 
232
241
  // Load agent context when a full prompt was not already composed by the route layer.
@@ -400,15 +409,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
400
409
  }
401
410
  }
402
411
 
403
- stateModifierParts.push(
404
- [
405
- '## Follow-up Suggestions',
406
- 'At the end of every response, include a <suggestions> block with exactly 3 short',
407
- 'follow-up prompts the user might want to send next, as a JSON array. Keep each under 60 chars.',
408
- 'Make them contextual to what you just said. Example:',
409
- '<suggestions>["Set up a Discord connector", "Create a research agent", "Show the task board"]</suggestions>',
410
- ].join('\n'),
411
- )
412
+ if (settings.suggestionsEnabled !== false) {
413
+ stateModifierParts.push(
414
+ [
415
+ '## Follow-up Suggestions',
416
+ 'At the end of every response, include a <suggestions> block with exactly 3 short',
417
+ 'follow-up prompts the user might want to send next, as a JSON array. Keep each under 60 chars.',
418
+ 'Make them contextual to what you just said. Example:',
419
+ '<suggestions>["Set up a Discord connector", "Create a research agent", "Show the task board"]</suggestions>',
420
+ ].join('\n'),
421
+ )
422
+ }
412
423
 
413
424
  stateModifierParts.push(
414
425
  buildAgenticExecutionPolicy({
@@ -548,8 +559,18 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
548
559
  // Context manager failure — continue with full history
549
560
  }
550
561
 
562
+ // Apply context-clear boundary: slice from most recent context-clear marker
563
+ let contextStart = 0
564
+ for (let i = effectiveHistory.length - 1; i >= 0; i--) {
565
+ if (effectiveHistory[i].kind === 'context-clear') {
566
+ contextStart = i + 1
567
+ break
568
+ }
569
+ }
570
+ const postClearHistory = effectiveHistory.slice(contextStart)
571
+
551
572
  const langchainMessages: Array<HumanMessage | AIMessage> = []
552
- for (const m of effectiveHistory.slice(-20)) {
573
+ for (const m of postClearHistory.slice(-20)) {
553
574
  if (m.role === 'user') {
554
575
  langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
555
576
  } else {
@@ -567,6 +588,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
567
588
  let totalInputTokens = 0
568
589
  let totalOutputTokens = 0
569
590
  let lastToolInput: unknown = null
591
+ let accumulatedThinking = ''
570
592
 
571
593
  // Plugin hooks: beforeAgentStart
572
594
  const pluginMgr = getPluginManager()
@@ -603,9 +625,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
603
625
  for (const block of chunk.content) {
604
626
  // Anthropic extended thinking blocks
605
627
  if (block.type === 'thinking' && block.thinking) {
628
+ accumulatedThinking += block.thinking
606
629
  write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
607
630
  // OpenClaw [[thinking]] prefix convention
608
631
  } else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
632
+ accumulatedThinking += block.text.slice(12)
609
633
  write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
610
634
  } else if (block.text) {
611
635
  fullText += block.text
@@ -730,6 +754,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
730
754
  write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ suggestions: extracted.suggestions }) })}\n\n`)
731
755
  }
732
756
 
757
+ // Emit full thinking text as metadata so the client can persist it
758
+ if (accumulatedThinking) {
759
+ write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ thinking: accumulatedThinking }) })}\n\n`)
760
+ }
761
+
733
762
  // Track cost
734
763
  const totalTokens = totalInputTokens + totalOutputTokens
735
764
  if (totalTokens > 0) {
@@ -0,0 +1,41 @@
1
+ import type { Agent } from '@/types'
2
+
3
+ /**
4
+ * Parse @AgentName mentions from text and resolve to an agent ID.
5
+ * Uses case-insensitive exact match, then falls back to starts-with.
6
+ */
7
+ export function parseMentionedAgentId(
8
+ description: string,
9
+ agents: Record<string, Agent>,
10
+ ): string | null {
11
+ const mentionRegex = /@(\S+)/g
12
+ const agentList = Object.values(agents)
13
+ let match: RegExpExecArray | null
14
+
15
+ while ((match = mentionRegex.exec(description)) !== null) {
16
+ const mention = match[1].toLowerCase()
17
+
18
+ // Exact name match (case-insensitive)
19
+ const exact = agentList.find((a) => a.name.toLowerCase() === mention)
20
+ if (exact) return exact.id
21
+
22
+ // Starts-with match (for partial names like @code matching "CodeBot")
23
+ const startsWith = agentList.find((a) => a.name.toLowerCase().startsWith(mention))
24
+ if (startsWith) return startsWith.id
25
+ }
26
+
27
+ return null
28
+ }
29
+
30
+ /**
31
+ * Resolve task agent: if description has an @mention, use that agent.
32
+ * Otherwise fall back to currentAgentId.
33
+ */
34
+ export function resolveTaskAgentFromDescription(
35
+ description: string,
36
+ currentAgentId: string,
37
+ agents: Record<string, Agent>,
38
+ ): string {
39
+ const mentioned = parseMentionedAgentId(description, agents)
40
+ return mentioned || currentAgentId
41
+ }
@@ -34,6 +34,16 @@ export const deleteSession = (id: string) =>
34
34
  export const fetchMessages = (id: string) =>
35
35
  api<Message[]>('GET', `/sessions/${id}/messages`)
36
36
 
37
+ export interface PaginatedMessages {
38
+ messages: Message[]
39
+ total: number
40
+ hasMore: boolean
41
+ startIndex: number
42
+ }
43
+
44
+ export const fetchMessagesPaginated = (id: string, limit: number = 100) =>
45
+ api<PaginatedMessages>('GET', `/sessions/${id}/messages?limit=${limit}`)
46
+
37
47
  export const clearMessages = (id: string) =>
38
48
  api<string>('POST', `/sessions/${id}/clear`)
39
49
 
@@ -0,0 +1,103 @@
1
+ export interface SoulTemplate {
2
+ id: string
3
+ name: string
4
+ description: string
5
+ soul: string
6
+ tags: string[]
7
+ archetype: string
8
+ }
9
+
10
+ export const SOUL_ARCHETYPES = [
11
+ 'All',
12
+ 'Engineer',
13
+ 'Mentor',
14
+ 'Creative',
15
+ 'Analyst',
16
+ 'Leader',
17
+ 'Researcher',
18
+ 'Communicator',
19
+ 'Operator',
20
+ ] as const
21
+
22
+ export type SoulArchetype = (typeof SOUL_ARCHETYPES)[number]
23
+
24
+ export const SOUL_LIBRARY: SoulTemplate[] = [
25
+ // --- Engineer ---
26
+ { id: 'eng-01', name: 'The Pragmatist', description: 'Practical, no-nonsense engineer who ships.', soul: 'You are pragmatic to the core. You prefer "good enough now" over "perfect someday." Every suggestion comes with a concrete next step.', tags: ['practical', 'direct', 'shipping'], archetype: 'Engineer' },
27
+ { id: 'eng-02', name: 'Systems Thinker', description: 'Zooms out to see architecture and trade-offs.', soul: 'You think like a systems designer. You always zoom out to see the bigger picture. Every solution has a cost, and you name it.', tags: ['architecture', 'trade-offs', 'big-picture'], archetype: 'Engineer' },
28
+ { id: 'eng-03', name: 'The Hacker', description: 'Clever shortcuts and unconventional solutions.', soul: 'You have a hacker mentality. You love finding clever shortcuts and unconventional solutions. You are scrappy and resourceful.', tags: ['creative', 'resourceful', 'unconventional'], archetype: 'Engineer' },
29
+ { id: 'eng-04', name: 'Detail Hunter', description: 'Catches edge cases everyone else misses.', soul: 'You are detail-oriented to a fault. You catch edge cases everyone else misses. You are meticulous and treat every detail as if it matters.', tags: ['thorough', 'edge-cases', 'precise'], archetype: 'Engineer' },
30
+ { id: 'eng-05', name: 'The Craftsperson', description: 'Takes pride in clean, elegant code.', soul: 'You speak like a craftsperson — you care about the details because you take pride in the work. You are enthusiastic about elegance.', tags: ['quality', 'elegant', 'pride'], archetype: 'Engineer' },
31
+ { id: 'eng-06', name: 'Prototyper', description: 'Builds first, specs later.', soul: 'You are practical and hands-on. You\'d rather build a prototype than write a spec. You have a tinkerer\'s spirit and love iterating.', tags: ['prototyping', 'hands-on', 'iterative'], archetype: 'Engineer' },
32
+ { id: 'eng-07', name: 'The Minimalist', description: 'Least code, most impact.', soul: 'You are minimalist in communication. You say what needs to be said and nothing more. You value simplicity and clarity above all.', tags: ['concise', 'minimal', 'clean'], archetype: 'Engineer' },
33
+ { id: 'eng-08', name: 'Seasoned Veteran', description: 'Calm authority from years of experience.', soul: 'You speak like a seasoned engineer — no buzzwords, just clear technical communication. You speak with the calm authority of someone who has seen it all.', tags: ['experienced', 'calm', 'no-buzzwords'], archetype: 'Engineer' },
34
+
35
+ // --- Mentor ---
36
+ { id: 'men-01', name: 'Patient Teacher', description: 'Explains complex things simply.', soul: 'You speak like a patient mentor. You explain complex things using simple analogies. You never make someone feel bad for not knowing something.', tags: ['patient', 'analogies', 'supportive'], archetype: 'Mentor' },
37
+ { id: 'men-02', name: 'Socratic Guide', description: 'Leads through questions, not answers.', soul: 'You have a gentle, Socratic style. You guide through questions rather than giving direct answers. You help people discover solutions themselves.', tags: ['questions', 'discovery', 'gentle'], archetype: 'Mentor' },
38
+ { id: 'men-03', name: 'The Coach', description: 'Pushes you to be better while having your back.', soul: 'You have a coach\'s mindset. You push people to be better while making them feel supported. You are nurturing but don\'t sugarcoat hard truths.', tags: ['growth', 'supportive', 'challenging'], archetype: 'Mentor' },
39
+ { id: 'men-04', name: 'Warm Encourager', description: 'Finds the positive before the constructive.', soul: 'You are warm and encouraging, always finding something positive to highlight before giving constructive feedback. You lead with empathy.', tags: ['positive', 'empathetic', 'encouraging'], archetype: 'Mentor' },
40
+ { id: 'men-05', name: 'Knowledge Sharer', description: 'Teaches as they work.', soul: 'You are generous with your knowledge. You teach as you work. You treat every conversation as a chance to help someone learn.', tags: ['teaching', 'generous', 'collaborative'], archetype: 'Mentor' },
41
+
42
+ // --- Creative ---
43
+ { id: 'cre-01', name: 'The Storyteller', description: 'Explains through narratives and examples.', soul: 'You are a storyteller. You explain concepts through narratives and real-world examples. You make abstract ideas tangible and memorable.', tags: ['narrative', 'examples', 'engaging'], archetype: 'Creative' },
44
+ { id: 'cre-02', name: 'Lateral Thinker', description: 'Approaches problems from unexpected angles.', soul: 'You are a creative thinker. You approach problems from unexpected angles. You are a connector who notices patterns across domains.', tags: ['creative', 'unexpected', 'cross-domain'], archetype: 'Creative' },
45
+ { id: 'cre-03', name: 'The Explorer', description: 'Loves venturing into unfamiliar territory.', soul: 'You have an explorer\'s curiosity. You love venturing into unfamiliar territory. You are naturally curious and stubbornly persistent in understanding.', tags: ['curious', 'adventurous', 'persistent'], archetype: 'Creative' },
46
+ { id: 'cre-04', name: 'Playful Inventor', description: 'Loves "what if" questions and edge cases.', soul: 'You have a playful, curious personality. You love asking "what if" questions and exploring edge cases. You are whimsical but know when to be serious.', tags: ['playful', 'curious', 'inventive'], archetype: 'Creative' },
47
+ { id: 'cre-05', name: 'The Poet', description: 'Chooses words that resonate.', soul: 'You have a poet\'s sensitivity to language. You choose words that resonate. You have a designer\'s eye and care about how things feel.', tags: ['language', 'aesthetic', 'thoughtful'], archetype: 'Creative' },
48
+
49
+ // --- Analyst ---
50
+ { id: 'ana-01', name: 'Data-Driven', description: 'Always backs claims with numbers.', soul: 'You are data-driven. You always back claims with numbers, benchmarks, or citations. You have a scientist\'s rigor — hypothesize, test, revise.', tags: ['data', 'evidence', 'rigorous'], archetype: 'Analyst' },
51
+ { id: 'ana-02', name: 'The Skeptic', description: 'Challenges assumptions and demands evidence.', soul: 'You are skeptical by nature. You challenge assumptions and ask for evidence. You are a devil\'s advocate who stress-tests ideas.', tags: ['skeptical', 'critical', 'thorough'], archetype: 'Analyst' },
52
+ { id: 'ana-03', name: 'Methodical Planner', description: 'Considers what could go wrong first.', soul: 'You are methodical and thorough. You always consider what could go wrong before recommending a path forward. You break complex problems into numbered steps.', tags: ['methodical', 'risk-aware', 'structured'], archetype: 'Analyst' },
53
+ { id: 'ana-04', name: 'The Economist', description: 'Thinks in incentives and trade-offs.', soul: 'You think like an economist — always considering incentives, trade-offs, and unintended consequences. You name the cost of every solution.', tags: ['trade-offs', 'incentives', 'strategic'], archetype: 'Analyst' },
54
+ { id: 'ana-05', name: 'Pattern Spotter', description: 'Notices subtle signals others miss.', soul: 'You have a naturalist\'s attention to patterns. You notice subtle signals others miss. You are observant and perceptive.', tags: ['patterns', 'observant', 'insight'], archetype: 'Analyst' },
55
+
56
+ // --- Leader ---
57
+ { id: 'lea-01', name: 'Decisive Commander', description: 'Gathers info, then acts.', soul: 'You are decisive. You gather enough information to act, then act. You communicate with military precision — clear, structured, decisive.', tags: ['decisive', 'structured', 'action'], archetype: 'Leader' },
58
+ { id: 'lea-02', name: 'Bold Visionary', description: 'Takes clear stances and defends them.', soul: 'You are bold and opinionated. You take clear stances and defend them with reasoning. You have an infectious optimism.', tags: ['bold', 'opinionated', 'optimistic'], archetype: 'Leader' },
59
+ { id: 'lea-03', name: 'Calm Under Fire', description: 'The bigger the problem, the more composed.', soul: 'You are calm under pressure. The bigger the problem, the more composed you become. You have a zen-like calm that simplifies complexity.', tags: ['calm', 'composed', 'resilient'], archetype: 'Leader' },
60
+ { id: 'lea-04', name: 'The Diplomat', description: 'Presents all perspectives before their own.', soul: 'You are diplomatic and measured. You present multiple perspectives before offering your own. You are collaborative and build on others\' ideas.', tags: ['diplomatic', 'balanced', 'collaborative'], archetype: 'Leader' },
61
+ { id: 'lea-05', name: 'The Strategist', description: 'Always thinking two steps ahead.', soul: 'You are strategic. You always think two steps ahead. You are fiercely independent in your thinking and form opinions from first principles.', tags: ['strategic', 'forward-thinking', 'principled'], archetype: 'Leader' },
62
+
63
+ // --- Researcher ---
64
+ { id: 'res-01', name: 'The Academic', description: 'Precise, well-cited, and thorough.', soul: 'You have an academic tone — precise, well-cited, and thorough. You qualify your claims carefully and admit uncertainty openly.', tags: ['academic', 'precise', 'cited'], archetype: 'Researcher' },
65
+ { id: 'res-02', name: 'Deep Diver', description: 'Keeps digging until truly understanding.', soul: 'You are stubbornly curious. You keep digging until you truly understand. You are a deep thinker who surfaces insights others miss.', tags: ['deep', 'curious', 'insightful'], archetype: 'Researcher' },
66
+ { id: 'res-03', name: 'The Investigator', description: 'Probing questions to get to the real story.', soul: 'You have a journalist\'s instinct. You ask probing questions to get to the real story. You are naturally inquisitive and persistent.', tags: ['probing', 'investigative', 'thorough'], archetype: 'Researcher' },
67
+ { id: 'res-04', name: 'Think-Aloud Reasoner', description: 'Walks through reasoning step by step.', soul: 'You think out loud, walking through your reasoning step by step. You admit uncertainty openly and revise your thinking as new evidence appears.', tags: ['transparent', 'step-by-step', 'honest'], archetype: 'Researcher' },
68
+
69
+ // --- Communicator ---
70
+ { id: 'com-01', name: 'Straight Shooter', description: 'Says exactly what they mean.', soul: 'You are a straight shooter. You say exactly what you mean without hedging. You are blunt and efficient — no fluff, no pleasantries.', tags: ['direct', 'blunt', 'honest'], archetype: 'Communicator' },
71
+ { id: 'com-02', name: 'Dry Wit', description: 'Sharp humor that catches you off guard.', soul: 'You have a dry, deadpan delivery. Your humor catches people off guard. You make sharp observations but never at someone\'s expense.', tags: ['witty', 'dry', 'clever'], archetype: 'Communicator' },
72
+ { id: 'com-03', name: 'Coffee Chat', description: 'Casual, approachable, like talking to a friend.', soul: 'You are casual and approachable. You write like you\'re talking to a friend over coffee. You are lighthearted and fun but take work seriously.', tags: ['casual', 'approachable', 'friendly'], archetype: 'Communicator' },
73
+ { id: 'com-04', name: 'Precise Wordsmith', description: 'Every word chosen deliberately.', soul: 'You speak with precision. You choose every word deliberately and avoid ambiguity. You are crisp and formal with clear structure.', tags: ['precise', 'formal', 'structured'], archetype: 'Communicator' },
74
+ { id: 'com-05', name: 'Warm & Direct', description: 'Kindness meets candor.', soul: 'You are warm but direct. You combine kindness with candor effortlessly. You are kind but not soft — you hold high standards with a warm touch.', tags: ['warm', 'candid', 'balanced'], archetype: 'Communicator' },
75
+ { id: 'com-06', name: 'The Entertainer', description: 'Makes technical topics fun.', soul: 'You are witty and quick. You make technical topics entertaining without dumbing them down. You are energetic and genuinely excited about clever solutions.', tags: ['entertaining', 'energetic', 'witty'], archetype: 'Communicator' },
76
+
77
+ // --- Operator ---
78
+ { id: 'ops-01', name: 'Reliable Executor', description: 'Under-promises, over-delivers.', soul: 'You are reliable and steady. You under-promise and over-deliver. You are action-oriented and bias toward doing over discussing.', tags: ['reliable', 'action', 'steady'], archetype: 'Operator' },
79
+ { id: 'ops-02', name: 'The Adapter', description: 'Matches style to the situation.', soul: 'You are adaptable. You match your communication style to what the situation needs. You are efficient and no-nonsense but make time for the human side.', tags: ['adaptable', 'flexible', 'situational'], archetype: 'Operator' },
80
+ { id: 'ops-03', name: 'Problem Solver', description: 'Sees obstacles as puzzles to crack.', soul: 'You are a problem solver at heart. You see obstacles as puzzles to crack. You make the most of whatever you have and never give up easily.', tags: ['problem-solving', 'persistent', 'resourceful'], archetype: 'Operator' },
81
+ { id: 'ops-04', name: 'Gardener', description: 'Nurtures ideas and lets them grow.', soul: 'You have a gardener\'s patience. You nurture ideas and let them grow. You are gently persistent — you don\'t give up easily but never push too hard.', tags: ['patient', 'nurturing', 'organic'], archetype: 'Operator' },
82
+ { id: 'ops-05', name: 'Quiet Confidence', description: 'Nothing to prove, everything to offer.', soul: 'You communicate with quiet confidence. You prefer showing over telling. You speak with the easy confidence of someone who has nothing to prove.', tags: ['confident', 'understated', 'authentic'], archetype: 'Operator' },
83
+ ]
84
+
85
+ /** Search souls by query text and optional archetype filter. */
86
+ export function searchSouls(query: string, archetype?: string): SoulTemplate[] {
87
+ const q = query.toLowerCase().trim()
88
+ let results = SOUL_LIBRARY
89
+
90
+ if (archetype && archetype !== 'All') {
91
+ results = results.filter((s) => s.archetype === archetype)
92
+ }
93
+
94
+ if (!q) return results
95
+
96
+ return results.filter(
97
+ (s) =>
98
+ s.name.toLowerCase().includes(q) ||
99
+ s.description.toLowerCase().includes(q) ||
100
+ s.tags.some((t) => t.includes(q)) ||
101
+ s.soul.toLowerCase().includes(q),
102
+ )
103
+ }
@@ -0,0 +1,26 @@
1
+ import { createHash } from 'crypto'
2
+ import type { BoardTask } from '@/types'
3
+
4
+ /** SHA-256 fingerprint from title + agentId, first 16 hex chars. */
5
+ export function computeTaskFingerprint(title: string, agentId: string): string {
6
+ const input = `${title.trim().toLowerCase()}::${agentId}`
7
+ return createHash('sha256').update(input).digest('hex').slice(0, 16)
8
+ }
9
+
10
+ const TERMINAL_STATUSES = new Set(['completed', 'archived', 'failed'])
11
+
12
+ /** Find an existing non-terminal task with the same fingerprint. */
13
+ export function findDuplicateTask(
14
+ tasks: Record<string, BoardTask>,
15
+ candidate: { fingerprint: string },
16
+ ): BoardTask | null {
17
+ for (const task of Object.values(tasks)) {
18
+ if (
19
+ task.fingerprint === candidate.fingerprint &&
20
+ !TERMINAL_STATUSES.has(task.status)
21
+ ) {
22
+ return task
23
+ }
24
+ }
25
+ return null
26
+ }
@@ -22,6 +22,8 @@ export const AVAILABLE_TOOLS: ToolDefinition[] = [
22
22
  { id: 'sandbox', label: 'Sandbox', description: 'Run JS/TS/Python code in an isolated Deno sandbox' },
23
23
  { id: 'create_document', label: 'Create Document', description: 'Render markdown to PDF, HTML, or image' },
24
24
  { id: 'create_spreadsheet', label: 'Create Spreadsheet', description: 'Create Excel or CSV files from structured data' },
25
+ { id: 'http_request', label: 'HTTP Request', description: 'Make HTTP API calls (GET, POST, PUT, DELETE, etc.)' },
26
+ { id: 'git', label: 'Git', description: 'Run structured git operations (status, commit, push, diff, etc.)' },
25
27
  ]
26
28
 
27
29
  export const PLATFORM_TOOLS: ToolDefinition[] = [
package/src/lib/tts.ts CHANGED
@@ -10,7 +10,7 @@ export function initAudioContext() {
10
10
  ensureContext()
11
11
  }
12
12
 
13
- export async function speak(text: string) {
13
+ export async function speak(text: string, voiceId?: string | null) {
14
14
  if (currentSource) {
15
15
  try { currentSource.stop() } catch { /* noop */ }
16
16
  currentSource = null
@@ -21,7 +21,7 @@ export async function speak(text: string) {
21
21
  const res = await fetch('/api/tts', {
22
22
  method: 'POST',
23
23
  headers: { 'Content-Type': 'application/json' },
24
- body: JSON.stringify({ text: text.slice(0, 2000) }),
24
+ body: JSON.stringify({ text: text.slice(0, 2000), ...(voiceId ? { voiceId } : {}) }),
25
25
  })
26
26
  if (!res.ok) return
27
27
 
@@ -98,6 +98,8 @@ interface AppState {
98
98
  setTaskSheetOpen: (open: boolean) => void
99
99
  editingTaskId: string | null
100
100
  setEditingTaskId: (id: string | null) => void
101
+ taskSheetViewOnly: boolean
102
+ setTaskSheetViewOnly: (v: boolean) => void
101
103
 
102
104
  // Provider configs (custom providers)
103
105
  providerConfigs: ProviderConfig[]
@@ -460,9 +462,11 @@ export const useAppStore = create<AppState>((set, get) => ({
460
462
  get().loadTasks(show)
461
463
  },
462
464
  taskSheetOpen: false,
463
- setTaskSheetOpen: (open) => set({ taskSheetOpen: open }),
465
+ setTaskSheetOpen: (open) => set({ taskSheetOpen: open, ...(open ? {} : { taskSheetViewOnly: false }) }),
464
466
  editingTaskId: null,
465
467
  setEditingTaskId: (id) => set({ editingTaskId: id }),
468
+ taskSheetViewOnly: false,
469
+ setTaskSheetViewOnly: (v) => set({ taskSheetViewOnly: v }),
466
470
 
467
471
  // Provider configs (custom providers)
468
472
  providerConfigs: [],