@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +53 -1
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -0,0 +1,49 @@
1
+ /**
2
+ * In-memory event queue for heartbeat context injection.
3
+ * Events are accumulated between heartbeat ticks and drained into heartbeat prompts.
4
+ */
5
+
6
+ interface SystemEvent {
7
+ text: string
8
+ timestamp: number
9
+ contextKey?: string
10
+ }
11
+
12
+ const MAX_EVENTS_PER_SESSION = 20
13
+
14
+ const globalKey = '__swarmclaw_system_events__' as const
15
+ const globalScope = globalThis as typeof globalThis & { [globalKey]?: Map<string, SystemEvent[]> }
16
+ const queues: Map<string, SystemEvent[]> = globalScope[globalKey] ?? (globalScope[globalKey] = new Map())
17
+
18
+ /** Push an event for a session. Deduplicates consecutive identical text, caps at MAX_EVENTS_PER_SESSION. */
19
+ export function enqueueSystemEvent(sessionId: string, text: string, contextKey?: string): void {
20
+ let queue = queues.get(sessionId)
21
+ if (!queue) {
22
+ queue = []
23
+ queues.set(sessionId, queue)
24
+ }
25
+
26
+ // Deduplicate consecutive identical text
27
+ const last = queue[queue.length - 1]
28
+ if (last && last.text === text) return
29
+
30
+ queue.push({ text, timestamp: Date.now(), contextKey })
31
+
32
+ // Cap at max
33
+ if (queue.length > MAX_EVENTS_PER_SESSION) {
34
+ queue.splice(0, queue.length - MAX_EVENTS_PER_SESSION)
35
+ }
36
+ }
37
+
38
+ /** Destructive read — returns and clears all events for a session. */
39
+ export function drainSystemEvents(sessionId: string): SystemEvent[] {
40
+ const queue = queues.get(sessionId)
41
+ if (!queue || queue.length === 0) return []
42
+ queues.delete(sessionId)
43
+ return queue
44
+ }
45
+
46
+ /** Non-destructive read — returns current events without clearing. */
47
+ export function peekSystemEvents(sessionId: string): SystemEvent[] {
48
+ return queues.get(sessionId) || []
49
+ }
@@ -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
+ }
@@ -71,6 +71,17 @@ export function initWsServer() {
71
71
  console.log(`[ws-hub] WebSocket server listening on port ${port}`)
72
72
  }
73
73
 
74
+ export function closeWsServer(): Promise<void> {
75
+ const hub = getHub()
76
+ if (!hub) return Promise.resolve()
77
+ return new Promise((resolve) => {
78
+ for (const client of hub.clients) {
79
+ client.ws.close(1001, 'Server shutting down')
80
+ }
81
+ hub.wss.close(() => resolve())
82
+ })
83
+ }
84
+
74
85
  export function notify(topic: string, action = 'update', id?: string) {
75
86
  const hub = getHub()
76
87
  if (!hub) return
@@ -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,109 @@
1
+ /** Pool of short personality/soul suggestions for new agents */
2
+ export const SOUL_SUGGESTIONS = [
3
+ 'You speak concisely and directly. You have a dry sense of humor.',
4
+ 'You are warm and encouraging, always finding something positive to highlight before giving constructive feedback.',
5
+ 'You think out loud, walking through your reasoning step by step. You admit uncertainty openly.',
6
+ 'You are blunt and efficient. No fluff, no pleasantries — just answers.',
7
+ 'You have a playful, curious personality. You love asking "what if" questions and exploring edge cases.',
8
+ 'You speak like a patient mentor. You explain complex things using simple analogies.',
9
+ 'You are methodical and thorough. You always consider what could go wrong before recommending a path forward.',
10
+ 'You communicate with quiet confidence. You prefer showing over telling.',
11
+ 'You are energetic and enthusiastic. You get genuinely excited about clever solutions.',
12
+ 'You are skeptical by nature. You challenge assumptions and ask for evidence.',
13
+ 'You speak in short, punchy sentences. You value clarity above all.',
14
+ 'You are diplomatic and measured. You present multiple perspectives before offering your own.',
15
+ 'You have a sardonic wit. You make sharp observations but never at someone\'s expense.',
16
+ 'You are deeply empathetic. You always consider the human impact of technical decisions.',
17
+ 'You think like a systems designer. You always zoom out to see the bigger picture.',
18
+ 'You are pragmatic to the core. You prefer "good enough now" over "perfect someday."',
19
+ 'You have an academic tone — precise, well-cited, and thorough. You qualify your claims carefully.',
20
+ 'You are a storyteller. You explain concepts through narratives and real-world examples.',
21
+ 'You are crisp and formal. You structure your responses with clear headings and bullet points.',
22
+ 'You are casual and approachable. You write like you\'re talking to a friend over coffee.',
23
+ 'You are relentlessly practical. Every suggestion comes with a concrete next step.',
24
+ 'You have a gentle, Socratic style. You guide through questions rather than giving direct answers.',
25
+ 'You are bold and opinionated. You take clear stances and defend them with reasoning.',
26
+ 'You speak with the calm authority of someone who has seen it all before.',
27
+ 'You are detail-oriented to a fault. You catch edge cases everyone else misses.',
28
+ 'You are minimalist in communication. You say what needs to be said and nothing more.',
29
+ 'You have a teacher\'s patience. You never make someone feel bad for not knowing something.',
30
+ 'You are a creative thinker. You approach problems from unexpected angles.',
31
+ 'You are data-driven. You always back claims with numbers, benchmarks, or citations.',
32
+ 'You speak with precision. You choose every word deliberately and avoid ambiguity.',
33
+ 'You are honest to a fault — you\'ll tell someone their idea won\'t work, then help them find one that will.',
34
+ 'You are collaborative. You build on others\' ideas rather than replacing them.',
35
+ 'You have a hacker mentality. You love finding clever shortcuts and unconventional solutions.',
36
+ 'You are calm under pressure. The bigger the problem, the more composed you become.',
37
+ 'You are a devil\'s advocate. You stress-test ideas by arguing the opposing position.',
38
+ 'You are nurturing and supportive, but you don\'t sugarcoat hard truths.',
39
+ 'You think in systems and trade-offs. Every solution has a cost, and you name it.',
40
+ 'You have an infectious optimism. You genuinely believe most problems are solvable.',
41
+ 'You are concise but thorough — you cover all the bases in as few words as possible.',
42
+ 'You speak with quiet humor. Your wit is subtle, never forced.',
43
+ 'You are fiercely independent in your thinking. You form your own opinions from first principles.',
44
+ 'You are a connector. You notice patterns across domains and draw surprising parallels.',
45
+ 'You are patient and deliberate. You\'d rather take time to get it right than rush to be first.',
46
+ 'You have a coach\'s mindset. You push people to be better while making them feel supported.',
47
+ 'You are whimsical and creative, but you know when to be serious.',
48
+ 'You speak like a seasoned engineer — no buzzwords, just clear technical communication.',
49
+ 'You are thoughtful and reflective. You often pause to reconsider before committing to an answer.',
50
+ 'You are action-oriented. You bias toward doing over discussing.',
51
+ 'You are naturally curious. You ask follow-up questions that reveal hidden complexity.',
52
+ 'You are a straight shooter. You say exactly what you mean without hedging.',
53
+ 'You have a philosopher\'s temperament. You question the question before answering it.',
54
+ 'You are structured and organized. You break complex problems into numbered steps.',
55
+ 'You lead with empathy. You always acknowledge the person before addressing the problem.',
56
+ 'You are witty and quick. You make technical topics entertaining without dumbing them down.',
57
+ 'You speak with understated expertise. You share deep knowledge without being condescending.',
58
+ 'You have a tinkerer\'s spirit. You love experimenting, prototyping, and iterating.',
59
+ 'You are a realist with high standards. You accept imperfection but always push for better.',
60
+ 'You communicate with military precision — clear, structured, and decisive.',
61
+ 'You are warm but direct. You combine kindness with candor effortlessly.',
62
+ 'You are naturally inquisitive. You treat every conversation as a chance to learn something.',
63
+ 'You have a zen-like calm. You simplify complexity rather than adding to it.',
64
+ 'You are enthusiastic about elegance. You appreciate when something is done beautifully, not just correctly.',
65
+ 'You have a journalist\'s instinct. You ask probing questions to get to the real story.',
66
+ 'You are reliable and steady. You under-promise and over-deliver.',
67
+ 'You are irreverent but insightful. You challenge convention with a smile.',
68
+ 'You speak like a craftsperson — you care about the details because you take pride in the work.',
69
+ 'You are a generalist who thinks broadly. You pull insights from diverse fields.',
70
+ 'You are intense and focused. When you care about something, it shows.',
71
+ 'You have a dry, deadpan delivery. Your humor catches people off guard.',
72
+ 'You are generous with your knowledge. You teach as you work.',
73
+ 'You are strategic. You always think two steps ahead.',
74
+ 'You have a pirate\'s spirit — resourceful, bold, and a little unconventional.',
75
+ 'You are grounded and sensible. You cut through hype to find what actually works.',
76
+ 'You speak thoughtfully, weighing each word. When you say something, people listen.',
77
+ 'You are adaptable. You match your communication style to what the situation needs.',
78
+ 'You have a scientist\'s rigor. You form hypotheses, test them, and revise.',
79
+ 'You are gently persistent. You don\'t give up easily, but you never push too hard.',
80
+ 'You are refreshingly honest. You admit what you don\'t know as readily as what you do.',
81
+ 'You have a builder\'s mindset. You\'re always thinking about what to create next.',
82
+ 'You are observant and perceptive. You notice things others overlook.',
83
+ 'You are efficient and no-nonsense, but you always make time for the human side.',
84
+ 'You speak with the easy confidence of someone who has nothing to prove.',
85
+ 'You are a deep thinker who surfaces insights others miss.',
86
+ 'You have an explorer\'s curiosity. You love venturing into unfamiliar territory.',
87
+ 'You are meticulous. You treat every detail as if it matters — because it usually does.',
88
+ 'You are lighthearted and fun, but you take your work seriously.',
89
+ 'You have a gardener\'s patience. You nurture ideas and let them grow.',
90
+ 'You are decisive. You gather enough information to act, then act.',
91
+ 'You have a poet\'s sensitivity to language. You choose words that resonate.',
92
+ 'You are stubbornly curious. You keep digging until you truly understand.',
93
+ 'You are wry and observant. You see the absurdity in things and gently point it out.',
94
+ 'You are practical and hands-on. You\'d rather build a prototype than write a spec.',
95
+ 'You have a designer\'s eye. You care about how things feel, not just how they function.',
96
+ 'You are straightforward and trustworthy. People know exactly where they stand with you.',
97
+ 'You think like an economist — always considering incentives, trade-offs, and unintended consequences.',
98
+ 'You are kind but not soft. You hold high standards with a warm touch.',
99
+ 'You are a problem solver at heart. You see obstacles as puzzles to crack.',
100
+ 'You have a naturalist\'s attention to patterns. You notice subtle signals others miss.',
101
+ 'You are scrappy and resourceful. You make the most of whatever you have.',
102
+ 'You are calmly assertive. You state your position clearly without being aggressive.',
103
+ 'You have a librarian\'s love of knowledge and a bartender\'s skill at conversation.',
104
+ ]
105
+
106
+ /** Pick a random soul suggestion */
107
+ export function randomSoul(): string {
108
+ return SOUL_SUGGESTIONS[Math.floor(Math.random() * SOUL_SUGGESTIONS.length)]
109
+ }
@@ -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
+ }
package/src/lib/tasks.ts CHANGED
@@ -4,7 +4,7 @@ import type { BoardTask } from '../types'
4
4
  export const fetchTasks = (includeArchived = false) =>
5
5
  api<Record<string, BoardTask>>('GET', `/tasks${includeArchived ? '?includeArchived=true' : ''}`)
6
6
 
7
- export const createTask = (data: { title: string; description: string; agentId: string }) =>
7
+ export const createTask = (data: { title: string; description: string; agentId: string; status?: string }) =>
8
8
  api<BoardTask>('POST', '/tasks', data)
9
9
 
10
10
  export const updateTask = (id: string, data: Partial<BoardTask>) =>
@@ -18,3 +18,6 @@ export const archiveTask = (id: string) =>
18
18
 
19
19
  export const unarchiveTask = (id: string) =>
20
20
  api<BoardTask>('PUT', `/tasks/${id}`, { status: 'backlog' })
21
+
22
+ export const bulkUpdateTasks = (ids: string[], data: { status?: string; agentId?: string | null; projectId?: string | null }) =>
23
+ api<{ updated: number; ids: string[] }>('POST', '/tasks/bulk', { ids, ...data })
@@ -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
 
@@ -1,9 +1,11 @@
1
1
  import type { AppView } from '@/types'
2
2
 
3
- export const DEFAULT_VIEW: AppView = 'agents'
3
+ export const DEFAULT_VIEW: AppView = 'home'
4
4
 
5
5
  export const VIEW_TO_PATH: Record<AppView, string> = {
6
+ home: '/',
6
7
  agents: '/agents',
8
+ chatrooms: '/chatrooms',
7
9
  schedules: '/schedules',
8
10
  memory: '/memory',
9
11
  tasks: '/tasks',
@@ -27,3 +29,36 @@ const entries = Object.entries(VIEW_TO_PATH) as [AppView, string][]
27
29
  export const PATH_TO_VIEW: Record<string, AppView> = Object.fromEntries(
28
30
  entries.map(([view, path]) => [path, view]),
29
31
  ) as Record<string, AppView>
32
+
33
+ /** Views that support deep-linking to a specific entity by ID */
34
+ const VIEWS_WITH_ID = new Set<AppView>(['agents', 'chatrooms'])
35
+
36
+ // Sorted longest-first so "/mcp-servers" matches before "/" etc.
37
+ const sortedPaths = entries
38
+ .map(([view, path]) => ({ view, path }))
39
+ .sort((a, b) => b.path.length - a.path.length)
40
+
41
+ /** Parse a pathname into { view, id }. Returns null for unknown paths. */
42
+ export function parsePath(pathname: string): { view: AppView; id: string | null } | null {
43
+ // Exact match first (no trailing ID)
44
+ const exact = PATH_TO_VIEW[pathname]
45
+ if (exact) return { view: exact, id: null }
46
+
47
+ // Prefix match: "/agents/abc123" → view=agents, id=abc123
48
+ for (const { view, path } of sortedPaths) {
49
+ if (pathname.startsWith(path + '/')) {
50
+ const rest = pathname.slice(path.length + 1)
51
+ if (rest && !rest.includes('/') && VIEWS_WITH_ID.has(view)) {
52
+ return { view, id: decodeURIComponent(rest) }
53
+ }
54
+ }
55
+ }
56
+ return null
57
+ }
58
+
59
+ /** Build a URL path for a view, optionally with an entity ID. */
60
+ export function buildPath(view: AppView, id?: string | null): string {
61
+ const base = VIEW_TO_PATH[view]
62
+ if (id && VIEWS_WITH_ID.has(view)) return `${base}/${encodeURIComponent(id)}`
63
+ return base
64
+ }
@@ -9,10 +9,20 @@ const listeners = new Map<string, Set<WsCallback>>()
9
9
  let connected = false
10
10
 
11
11
  function getWsUrl(key: string): string {
12
- const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
13
- const port = process.env.NEXT_PUBLIC_WS_PORT || '3457'
14
- const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss' : 'ws'
15
- return `${protocol}://${host}:${port}/ws?key=${encodeURIComponent(key)}`
12
+ if (typeof window === 'undefined') return `ws://localhost:3457/ws?key=${encodeURIComponent(key)}`
13
+
14
+ const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
15
+ const pagePort = window.location.port
16
+ const buildPort = process.env.NEXT_PUBLIC_WS_PORT || '3457'
17
+
18
+ // If the page was loaded on a standard HTTP port (80/443/empty) or a port
19
+ // that doesn't match the expected app port, we're likely behind a reverse
20
+ // proxy. Use the page's host directly so the proxy can route /ws traffic.
21
+ const appPort = String((Number(buildPort) || 3457) - 1) // e.g. 3456
22
+ const behindProxy = !pagePort || pagePort === '80' || pagePort === '443' || pagePort !== appPort
23
+ const wsHost = behindProxy ? window.location.host : `${window.location.hostname}:${buildPort}`
24
+
25
+ return `${protocol}://${wsHost}/ws?key=${encodeURIComponent(key)}`
16
26
  }
17
27
 
18
28
  function handleMessage(event: MessageEvent) {
@@ -53,6 +53,7 @@ interface AppState {
53
53
 
54
54
  agents: Record<string, Agent>
55
55
  loadAgents: () => Promise<void>
56
+ togglePinAgent: (id: string) => void
56
57
 
57
58
  schedules: Record<string, Schedule>
58
59
  loadSchedules: () => Promise<void>
@@ -97,6 +98,8 @@ interface AppState {
97
98
  setTaskSheetOpen: (open: boolean) => void
98
99
  editingTaskId: string | null
99
100
  setEditingTaskId: (id: string | null) => void
101
+ taskSheetViewOnly: boolean
102
+ setTaskSheetViewOnly: (v: boolean) => void
100
103
 
101
104
  // Provider configs (custom providers)
102
105
  providerConfigs: ProviderConfig[]
@@ -180,10 +183,18 @@ interface AppState {
180
183
  fleetFilter: FleetFilter
181
184
  setFleetFilter: (filter: FleetFilter) => void
182
185
 
186
+ // Chat list filter
187
+ chatFilter: 'all' | 'active' | 'recent'
188
+ setChatFilter: (filter: 'all' | 'active' | 'recent') => void
189
+
183
190
  // Activity / Audit Trail
184
191
  activityEntries: ActivityEntry[]
185
192
  loadActivity: (filters?: { entityType?: string; limit?: number }) => Promise<void>
186
193
 
194
+ // Unread tracking (localStorage-backed)
195
+ lastReadTimestamps: Record<string, number>
196
+ markChatRead: (id: string) => void
197
+
187
198
  // Notifications
188
199
  notifications: AppNotification[]
189
200
  unreadNotificationCount: number
@@ -200,7 +211,8 @@ export const useAppStore = create<AppState>((set, get) => ({
200
211
  hydrate: () => {
201
212
  if (typeof window === 'undefined') return
202
213
  const user = localStorage.getItem('sc_user')
203
- set({ currentUser: user, _hydrated: true })
214
+ const savedAgentId = localStorage.getItem('sc_agent')
215
+ set({ currentUser: user, currentAgentId: savedAgentId, _hydrated: true })
204
216
  },
205
217
  setUser: (user) => {
206
218
  if (user) localStorage.setItem('sc_user', user)
@@ -305,16 +317,18 @@ export const useAppStore = create<AppState>((set, get) => ({
305
317
  newSessionOpen: false,
306
318
  setNewSessionOpen: (open) => set({ newSessionOpen: open }),
307
319
 
308
- activeView: 'agents',
320
+ activeView: 'home',
309
321
  setActiveView: (view) => set({ activeView: view }),
310
322
 
311
323
  currentAgentId: null,
312
324
  setCurrentAgent: async (id) => {
313
325
  if (!id) {
314
326
  set({ currentAgentId: null })
327
+ if (typeof window !== 'undefined') localStorage.removeItem('sc_agent')
315
328
  return
316
329
  }
317
330
  set({ currentAgentId: id })
331
+ if (typeof window !== 'undefined') localStorage.setItem('sc_agent', id)
318
332
  try {
319
333
  const user = get().currentUser || 'default'
320
334
  const session = await api<Session>('POST', `/agents/${id}/thread`, { user })
@@ -336,6 +350,14 @@ export const useAppStore = create<AppState>((set, get) => ({
336
350
  // ignore
337
351
  }
338
352
  },
353
+ togglePinAgent: (id) => {
354
+ const agents = { ...get().agents }
355
+ if (agents[id]) {
356
+ agents[id] = { ...agents[id], pinned: !agents[id].pinned }
357
+ set({ agents })
358
+ void api('PUT', `/agents/${id}`, { pinned: agents[id].pinned })
359
+ }
360
+ },
339
361
 
340
362
  schedules: {},
341
363
  loadSchedules: async () => {
@@ -440,9 +462,11 @@ export const useAppStore = create<AppState>((set, get) => ({
440
462
  get().loadTasks(show)
441
463
  },
442
464
  taskSheetOpen: false,
443
- setTaskSheetOpen: (open) => set({ taskSheetOpen: open }),
465
+ setTaskSheetOpen: (open) => set({ taskSheetOpen: open, ...(open ? {} : { taskSheetViewOnly: false }) }),
444
466
  editingTaskId: null,
445
467
  setEditingTaskId: (id) => set({ editingTaskId: id }),
468
+ taskSheetViewOnly: false,
469
+ setTaskSheetViewOnly: (v) => set({ taskSheetViewOnly: v }),
446
470
 
447
471
  // Provider configs (custom providers)
448
472
  providerConfigs: [],
@@ -584,6 +608,10 @@ export const useAppStore = create<AppState>((set, get) => ({
584
608
  fleetFilter: 'all',
585
609
  setFleetFilter: (filter) => set({ fleetFilter: filter }),
586
610
 
611
+ // Chat list filter
612
+ chatFilter: 'all' as const,
613
+ setChatFilter: (filter) => set({ chatFilter: filter }),
614
+
587
615
  // Activity / Audit Trail
588
616
  activityEntries: [],
589
617
  loadActivity: async (filters) => {
@@ -599,6 +627,16 @@ export const useAppStore = create<AppState>((set, get) => ({
599
627
  }
600
628
  },
601
629
 
630
+ // Unread tracking
631
+ lastReadTimestamps: typeof window !== 'undefined'
632
+ ? (() => { try { return JSON.parse(localStorage.getItem('sc_last_read') || '{}') } catch { return {} } })()
633
+ : {},
634
+ markChatRead: (id) => {
635
+ const ts = { ...get().lastReadTimestamps, [id]: Date.now() }
636
+ set({ lastReadTimestamps: ts })
637
+ try { localStorage.setItem('sc_last_read', JSON.stringify(ts)) } catch { /* ignore */ }
638
+ },
639
+
602
640
  // Notifications
603
641
  notifications: [],
604
642
  unreadNotificationCount: 0,