@swarmclawai/swarmclaw 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -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
|
+
}
|
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 })
|
package/src/lib/view-routes.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { AppView } from '@/types'
|
|
2
2
|
|
|
3
|
-
export const DEFAULT_VIEW: AppView = '
|
|
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
|
+
}
|
package/src/lib/ws-client.ts
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
const protocol =
|
|
15
|
-
|
|
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) {
|
package/src/proxy.ts
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import type { NextRequest } from 'next/server'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/* ------------------------------------------------------------------ */
|
|
5
|
+
/* Rate-limit state — HMR-safe via globalThis */
|
|
6
|
+
/* ------------------------------------------------------------------ */
|
|
7
|
+
|
|
8
|
+
interface RateLimitEntry {
|
|
9
|
+
count: number
|
|
10
|
+
lockedUntil: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rateLimitMap = (
|
|
14
|
+
(globalThis as Record<string, unknown>).__swarmclaw_rate_limit__ ??= new Map()
|
|
15
|
+
) as Map<string, RateLimitEntry>
|
|
16
|
+
|
|
17
|
+
const MAX_ATTEMPTS = 5
|
|
18
|
+
const LOCKOUT_MS = 15 * 60 * 1000 // 15 minutes
|
|
19
|
+
const PRUNE_THRESHOLD = 1000
|
|
20
|
+
|
|
21
|
+
/** Prune expired entries when the map grows too large. */
|
|
22
|
+
function pruneRateLimitMap() {
|
|
23
|
+
if (rateLimitMap.size <= PRUNE_THRESHOLD) return
|
|
24
|
+
const now = Date.now()
|
|
25
|
+
rateLimitMap.forEach((entry, ip) => {
|
|
26
|
+
if (entry.lockedUntil < now && entry.count < MAX_ATTEMPTS) {
|
|
27
|
+
rateLimitMap.delete(ip)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Extract client IP from the request. */
|
|
33
|
+
function getClientIp(request: NextRequest): string {
|
|
34
|
+
const forwarded = request.headers.get('x-forwarded-for')
|
|
35
|
+
if (forwarded) {
|
|
36
|
+
const first = forwarded.split(',')[0]?.trim()
|
|
37
|
+
if (first) return first
|
|
38
|
+
}
|
|
39
|
+
return (request as unknown as { ip?: string }).ip ?? 'unknown'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ------------------------------------------------------------------ */
|
|
43
|
+
/* Proxy */
|
|
44
|
+
/* ------------------------------------------------------------------ */
|
|
45
|
+
|
|
46
|
+
/** Access key auth proxy with brute-force rate limiting.
|
|
5
47
|
* Checks X-Access-Key header or ?key= param on all /api/ routes except /api/auth.
|
|
6
48
|
* The key is validated against the ACCESS_KEY env var.
|
|
49
|
+
* After 5 failed attempts from a single IP the client is locked out for 15 minutes.
|
|
7
50
|
*/
|
|
8
51
|
export function proxy(request: NextRequest) {
|
|
9
52
|
const { pathname } = request.nextUrl
|
|
@@ -29,13 +72,47 @@ export function proxy(request: NextRequest) {
|
|
|
29
72
|
return NextResponse.next()
|
|
30
73
|
}
|
|
31
74
|
|
|
75
|
+
// --- Rate-limit housekeeping ---
|
|
76
|
+
pruneRateLimitMap()
|
|
77
|
+
|
|
78
|
+
const clientIp = getClientIp(request)
|
|
79
|
+
const entry = rateLimitMap.get(clientIp)
|
|
80
|
+
|
|
81
|
+
// Check lockout before even validating the key
|
|
82
|
+
if (entry && entry.lockedUntil > Date.now()) {
|
|
83
|
+
const retryAfter = Math.ceil((entry.lockedUntil - Date.now()) / 1000)
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{ error: 'Too many failed attempts. Try again later.', retryAfter },
|
|
86
|
+
{ status: 429, headers: { 'Retry-After': String(retryAfter) } },
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
32
90
|
const providedKey =
|
|
33
91
|
request.headers.get('x-access-key')
|
|
34
92
|
|| request.nextUrl.searchParams.get('key')
|
|
35
93
|
|| ''
|
|
36
94
|
|
|
37
95
|
if (providedKey !== accessKey) {
|
|
38
|
-
|
|
96
|
+
// Record the failed attempt
|
|
97
|
+
const current = rateLimitMap.get(clientIp) ?? { count: 0, lockedUntil: 0 }
|
|
98
|
+
current.count += 1
|
|
99
|
+
|
|
100
|
+
if (current.count >= MAX_ATTEMPTS) {
|
|
101
|
+
current.lockedUntil = Date.now() + LOCKOUT_MS
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
rateLimitMap.set(clientIp, current)
|
|
105
|
+
|
|
106
|
+
const remaining = Math.max(0, MAX_ATTEMPTS - current.count)
|
|
107
|
+
return NextResponse.json(
|
|
108
|
+
{ error: 'Unauthorized' },
|
|
109
|
+
{ status: 401, headers: { 'X-RateLimit-Remaining': String(remaining) } },
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Successful auth — clear any prior failed-attempt tracking for this IP
|
|
114
|
+
if (entry) {
|
|
115
|
+
rateLimitMap.delete(clientIp)
|
|
39
116
|
}
|
|
40
117
|
|
|
41
118
|
return NextResponse.next()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { create } from 'zustand'
|
|
4
|
-
import type { Sessions, Session, NetworkInfo, Directory, ProviderInfo, Credentials, Agent, Schedule, AppView, BoardTask, AppSettings, OrchestratorSecret, ProviderConfig, Skill, Connector, Webhook, McpServerConfig, PluginMeta, Project, FleetFilter, ActivityEntry } from '../types'
|
|
4
|
+
import type { Sessions, Session, NetworkInfo, Directory, ProviderInfo, Credentials, Agent, Schedule, AppView, BoardTask, AppSettings, OrchestratorSecret, ProviderConfig, Skill, Connector, Webhook, McpServerConfig, PluginMeta, Project, FleetFilter, ActivityEntry, AppNotification } from '../types'
|
|
5
5
|
import { fetchSessions, fetchDirs, fetchProviders, fetchCredentials } from '../lib/sessions'
|
|
6
6
|
import { fetchAgents } from '../lib/agents'
|
|
7
7
|
import { fetchSchedules } from '../lib/schedules'
|
|
@@ -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>
|
|
@@ -180,10 +181,26 @@ interface AppState {
|
|
|
180
181
|
fleetFilter: FleetFilter
|
|
181
182
|
setFleetFilter: (filter: FleetFilter) => void
|
|
182
183
|
|
|
184
|
+
// Chat list filter
|
|
185
|
+
chatFilter: 'all' | 'active' | 'recent'
|
|
186
|
+
setChatFilter: (filter: 'all' | 'active' | 'recent') => void
|
|
187
|
+
|
|
183
188
|
// Activity / Audit Trail
|
|
184
189
|
activityEntries: ActivityEntry[]
|
|
185
190
|
loadActivity: (filters?: { entityType?: string; limit?: number }) => Promise<void>
|
|
186
191
|
|
|
192
|
+
// Unread tracking (localStorage-backed)
|
|
193
|
+
lastReadTimestamps: Record<string, number>
|
|
194
|
+
markChatRead: (id: string) => void
|
|
195
|
+
|
|
196
|
+
// Notifications
|
|
197
|
+
notifications: AppNotification[]
|
|
198
|
+
unreadNotificationCount: number
|
|
199
|
+
loadNotifications: () => Promise<void>
|
|
200
|
+
markNotificationRead: (id: string) => Promise<void>
|
|
201
|
+
markAllNotificationsRead: () => Promise<void>
|
|
202
|
+
clearReadNotifications: () => Promise<void>
|
|
203
|
+
|
|
187
204
|
}
|
|
188
205
|
|
|
189
206
|
export const useAppStore = create<AppState>((set, get) => ({
|
|
@@ -192,7 +209,8 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
192
209
|
hydrate: () => {
|
|
193
210
|
if (typeof window === 'undefined') return
|
|
194
211
|
const user = localStorage.getItem('sc_user')
|
|
195
|
-
|
|
212
|
+
const savedAgentId = localStorage.getItem('sc_agent')
|
|
213
|
+
set({ currentUser: user, currentAgentId: savedAgentId, _hydrated: true })
|
|
196
214
|
},
|
|
197
215
|
setUser: (user) => {
|
|
198
216
|
if (user) localStorage.setItem('sc_user', user)
|
|
@@ -297,16 +315,18 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
297
315
|
newSessionOpen: false,
|
|
298
316
|
setNewSessionOpen: (open) => set({ newSessionOpen: open }),
|
|
299
317
|
|
|
300
|
-
activeView: '
|
|
318
|
+
activeView: 'home',
|
|
301
319
|
setActiveView: (view) => set({ activeView: view }),
|
|
302
320
|
|
|
303
321
|
currentAgentId: null,
|
|
304
322
|
setCurrentAgent: async (id) => {
|
|
305
323
|
if (!id) {
|
|
306
324
|
set({ currentAgentId: null })
|
|
325
|
+
if (typeof window !== 'undefined') localStorage.removeItem('sc_agent')
|
|
307
326
|
return
|
|
308
327
|
}
|
|
309
328
|
set({ currentAgentId: id })
|
|
329
|
+
if (typeof window !== 'undefined') localStorage.setItem('sc_agent', id)
|
|
310
330
|
try {
|
|
311
331
|
const user = get().currentUser || 'default'
|
|
312
332
|
const session = await api<Session>('POST', `/agents/${id}/thread`, { user })
|
|
@@ -328,6 +348,14 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
328
348
|
// ignore
|
|
329
349
|
}
|
|
330
350
|
},
|
|
351
|
+
togglePinAgent: (id) => {
|
|
352
|
+
const agents = { ...get().agents }
|
|
353
|
+
if (agents[id]) {
|
|
354
|
+
agents[id] = { ...agents[id], pinned: !agents[id].pinned }
|
|
355
|
+
set({ agents })
|
|
356
|
+
void api('PUT', `/agents/${id}`, { pinned: agents[id].pinned })
|
|
357
|
+
}
|
|
358
|
+
},
|
|
331
359
|
|
|
332
360
|
schedules: {},
|
|
333
361
|
loadSchedules: async () => {
|
|
@@ -576,6 +604,10 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
576
604
|
fleetFilter: 'all',
|
|
577
605
|
setFleetFilter: (filter) => set({ fleetFilter: filter }),
|
|
578
606
|
|
|
607
|
+
// Chat list filter
|
|
608
|
+
chatFilter: 'all' as const,
|
|
609
|
+
setChatFilter: (filter) => set({ chatFilter: filter }),
|
|
610
|
+
|
|
579
611
|
// Activity / Audit Trail
|
|
580
612
|
activityEntries: [],
|
|
581
613
|
loadActivity: async (filters) => {
|
|
@@ -591,4 +623,63 @@ export const useAppStore = create<AppState>((set, get) => ({
|
|
|
591
623
|
}
|
|
592
624
|
},
|
|
593
625
|
|
|
626
|
+
// Unread tracking
|
|
627
|
+
lastReadTimestamps: typeof window !== 'undefined'
|
|
628
|
+
? (() => { try { return JSON.parse(localStorage.getItem('sc_last_read') || '{}') } catch { return {} } })()
|
|
629
|
+
: {},
|
|
630
|
+
markChatRead: (id) => {
|
|
631
|
+
const ts = { ...get().lastReadTimestamps, [id]: Date.now() }
|
|
632
|
+
set({ lastReadTimestamps: ts })
|
|
633
|
+
try { localStorage.setItem('sc_last_read', JSON.stringify(ts)) } catch { /* ignore */ }
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
// Notifications
|
|
637
|
+
notifications: [],
|
|
638
|
+
unreadNotificationCount: 0,
|
|
639
|
+
loadNotifications: async () => {
|
|
640
|
+
try {
|
|
641
|
+
const notifications = await api<AppNotification[]>('GET', '/notifications')
|
|
642
|
+
set({
|
|
643
|
+
notifications,
|
|
644
|
+
unreadNotificationCount: notifications.filter((n) => !n.read).length,
|
|
645
|
+
})
|
|
646
|
+
} catch {
|
|
647
|
+
// ignore
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
markNotificationRead: async (id) => {
|
|
651
|
+
const notifications = get().notifications.map((n) =>
|
|
652
|
+
n.id === id ? { ...n, read: true } : n,
|
|
653
|
+
)
|
|
654
|
+
set({
|
|
655
|
+
notifications,
|
|
656
|
+
unreadNotificationCount: notifications.filter((n) => !n.read).length,
|
|
657
|
+
})
|
|
658
|
+
try {
|
|
659
|
+
await api('PUT', `/notifications/${id}`, { read: true })
|
|
660
|
+
} catch {
|
|
661
|
+
// ignore
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
markAllNotificationsRead: async () => {
|
|
665
|
+
const notifications = get().notifications.map((n) => ({ ...n, read: true }))
|
|
666
|
+
set({ notifications, unreadNotificationCount: 0 })
|
|
667
|
+
try {
|
|
668
|
+
await Promise.all(
|
|
669
|
+
get().notifications.filter((n) => !n.read).map((n) => api('PUT', `/notifications/${n.id}`, { read: true })),
|
|
670
|
+
)
|
|
671
|
+
} catch {
|
|
672
|
+
// ignore
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
clearReadNotifications: async () => {
|
|
676
|
+
const notifications = get().notifications.filter((n) => !n.read)
|
|
677
|
+
set({ notifications, unreadNotificationCount: notifications.length })
|
|
678
|
+
try {
|
|
679
|
+
await api('DELETE', '/notifications')
|
|
680
|
+
} catch {
|
|
681
|
+
// ignore
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
|
|
594
685
|
}))
|
|
@@ -68,6 +68,10 @@ interface ChatState {
|
|
|
68
68
|
pendingImage: PendingFile | null
|
|
69
69
|
setPendingImage: (img: PendingFile | null) => void
|
|
70
70
|
|
|
71
|
+
// Reply-to
|
|
72
|
+
replyingTo: { message: Message; index: number } | null
|
|
73
|
+
setReplyingTo: (reply: { message: Message; index: number } | null) => void
|
|
74
|
+
|
|
71
75
|
devServer: DevServerStatus | null
|
|
72
76
|
setDevServer: (ds: DevServerStatus | null) => void
|
|
73
77
|
|
|
@@ -83,12 +87,22 @@ interface ChatState {
|
|
|
83
87
|
sendHeartbeat: (sessionId: string) => Promise<void>
|
|
84
88
|
stopStreaming: () => void
|
|
85
89
|
|
|
90
|
+
// Thinking/reasoning text during streaming
|
|
91
|
+
thinkingText: string
|
|
92
|
+
thinkingStartTime: number
|
|
93
|
+
|
|
86
94
|
// Rich trace blocks during streaming (F13)
|
|
87
95
|
streamTraces: ChatTraceBlock[]
|
|
88
96
|
|
|
89
97
|
// Voice conversation
|
|
90
98
|
voiceConversationActive: boolean
|
|
91
99
|
onStreamEvent: ((event: { t: string; text?: string }) => void) | null
|
|
100
|
+
|
|
101
|
+
// Message queue (send while streaming)
|
|
102
|
+
queuedMessages: string[]
|
|
103
|
+
addQueuedMessage: (text: string) => void
|
|
104
|
+
removeQueuedMessage: (index: number) => void
|
|
105
|
+
shiftQueuedMessage: () => string | undefined
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
// Module-level cadence interval (not in state to avoid re-renders)
|
|
@@ -128,9 +142,21 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
128
142
|
setSoundEnabled(next)
|
|
129
143
|
set({ soundEnabled: next })
|
|
130
144
|
},
|
|
145
|
+
thinkingText: '',
|
|
146
|
+
thinkingStartTime: 0,
|
|
131
147
|
streamTraces: [],
|
|
132
148
|
voiceConversationActive: false,
|
|
133
149
|
onStreamEvent: null,
|
|
150
|
+
queuedMessages: [],
|
|
151
|
+
addQueuedMessage: (text) => set((s) => ({ queuedMessages: [...s.queuedMessages, text] })),
|
|
152
|
+
removeQueuedMessage: (index) => set((s) => ({ queuedMessages: s.queuedMessages.filter((_, i) => i !== index) })),
|
|
153
|
+
shiftQueuedMessage: () => {
|
|
154
|
+
const q = get().queuedMessages
|
|
155
|
+
if (!q.length) return undefined
|
|
156
|
+
const next = q[0]
|
|
157
|
+
set({ queuedMessages: q.slice(1) })
|
|
158
|
+
return next
|
|
159
|
+
},
|
|
134
160
|
|
|
135
161
|
pendingFiles: [],
|
|
136
162
|
addPendingFile: (f) => set((s) => ({ pendingFiles: [...s.pendingFiles, f] })),
|
|
@@ -141,6 +167,10 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
141
167
|
get pendingImage() { const files = get().pendingFiles; return files.length ? files[0] : null },
|
|
142
168
|
setPendingImage: (img) => set({ pendingFiles: img ? [img] : [] }),
|
|
143
169
|
|
|
170
|
+
// Reply-to
|
|
171
|
+
replyingTo: null,
|
|
172
|
+
setReplyingTo: (reply) => set({ replyingTo: reply }),
|
|
173
|
+
|
|
144
174
|
previewContent: null,
|
|
145
175
|
setPreviewContent: (content) => set({ previewContent: content }),
|
|
146
176
|
|
|
@@ -150,7 +180,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
150
180
|
setDebugOpen: (open) => set({ debugOpen: open }),
|
|
151
181
|
|
|
152
182
|
sendMessage: async (text: string) => {
|
|
153
|
-
const { pendingFiles } = get()
|
|
183
|
+
const { pendingFiles, replyingTo } = get()
|
|
154
184
|
if ((!text.trim() && !pendingFiles.length) || get().streaming) return
|
|
155
185
|
const sessionId = useAppStore.getState().currentSessionId
|
|
156
186
|
if (!sessionId) return
|
|
@@ -162,6 +192,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
162
192
|
const attachedFiles = pendingFiles.length > 1
|
|
163
193
|
? pendingFiles.map((f) => f.path)
|
|
164
194
|
: undefined
|
|
195
|
+
const replyToId = replyingTo?.message?.replyToId ? undefined : replyingTo?.message ? `msg-${replyingTo.index}` : undefined
|
|
165
196
|
|
|
166
197
|
const userMsg: Message = {
|
|
167
198
|
role: 'user',
|
|
@@ -170,6 +201,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
170
201
|
imagePath,
|
|
171
202
|
imageUrl,
|
|
172
203
|
attachedFiles,
|
|
204
|
+
...(replyToId ? { replyToId } : {}),
|
|
173
205
|
}
|
|
174
206
|
clearCadence()
|
|
175
207
|
set((s) => ({
|
|
@@ -180,8 +212,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
180
212
|
streamToolName: '',
|
|
181
213
|
displayText: '',
|
|
182
214
|
agentStatus: null,
|
|
215
|
+
thinkingText: '',
|
|
216
|
+
thinkingStartTime: Date.now(),
|
|
183
217
|
messages: [...s.messages, userMsg],
|
|
184
218
|
pendingFiles: [],
|
|
219
|
+
replyingTo: null,
|
|
185
220
|
toolEvents: [],
|
|
186
221
|
lastUsage: null,
|
|
187
222
|
}))
|
|
@@ -296,6 +331,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
296
331
|
set({ streamText: fullText })
|
|
297
332
|
if (get().soundEnabled) playError()
|
|
298
333
|
}
|
|
334
|
+
} else if (event.t === 'thinking') {
|
|
335
|
+
set((s) => ({ thinkingText: s.thinkingText + (event.text || '') }))
|
|
299
336
|
} else if (event.t === 'status') {
|
|
300
337
|
try {
|
|
301
338
|
const parsed = JSON.parse(event.text || '{}')
|
|
@@ -306,7 +343,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
306
343
|
} else if (event.t === 'done') {
|
|
307
344
|
// done
|
|
308
345
|
}
|
|
309
|
-
}, attachedFiles)
|
|
346
|
+
}, attachedFiles, { replyToId })
|
|
310
347
|
|
|
311
348
|
clearCadence()
|
|
312
349
|
if (get().soundEnabled && soundFiredStart) playStreamEnd()
|
|
@@ -333,13 +370,21 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|
|
333
370
|
displayText: '',
|
|
334
371
|
streamPhase: 'thinking' as const,
|
|
335
372
|
streamToolName: '',
|
|
373
|
+
thinkingText: '',
|
|
374
|
+
thinkingStartTime: 0,
|
|
336
375
|
}))
|
|
337
376
|
if (get().ttsEnabled && !get().voiceConversationActive) speak(fullText)
|
|
338
377
|
} else {
|
|
339
|
-
set({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking' as const, streamToolName: '' })
|
|
378
|
+
set({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking' as const, streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
|
|
340
379
|
}
|
|
341
380
|
|
|
342
381
|
useAppStore.getState().loadSessions()
|
|
382
|
+
|
|
383
|
+
// Auto-dequeue: if there are queued messages, send the next one
|
|
384
|
+
const nextQueued = get().shiftQueuedMessage()
|
|
385
|
+
if (nextQueued) {
|
|
386
|
+
setTimeout(() => get().sendMessage(nextQueued), 100)
|
|
387
|
+
}
|
|
343
388
|
},
|
|
344
389
|
|
|
345
390
|
editAndResend: async (messageIndex: number, newText: string) => {
|