@swarmclawai/swarmclaw 0.4.5 → 0.5.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.
Files changed (113) hide show
  1. package/README.md +8 -2
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +5 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +23 -5
  6. package/src/app/api/agents/trash/route.ts +44 -0
  7. package/src/app/api/connectors/[id]/route.ts +7 -4
  8. package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
  9. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  10. package/src/app/api/openclaw/approvals/route.ts +46 -0
  11. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  12. package/src/app/api/openclaw/cron/route.ts +52 -0
  13. package/src/app/api/openclaw/directory/route.ts +4 -3
  14. package/src/app/api/openclaw/discover/route.ts +3 -2
  15. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  16. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  17. package/src/app/api/openclaw/gateway/route.ts +72 -0
  18. package/src/app/api/openclaw/history/route.ts +109 -0
  19. package/src/app/api/openclaw/media/route.ts +53 -0
  20. package/src/app/api/openclaw/models/route.ts +12 -0
  21. package/src/app/api/openclaw/permissions/route.ts +39 -0
  22. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  23. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  24. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  25. package/src/app/api/openclaw/skills/route.ts +82 -0
  26. package/src/app/api/openclaw/sync/route.ts +3 -2
  27. package/src/app/api/projects/[id]/route.ts +1 -1
  28. package/src/app/api/projects/route.ts +1 -1
  29. package/src/app/api/secrets/[id]/route.ts +1 -1
  30. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  31. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  32. package/src/app/api/sessions/[id]/messages/route.ts +18 -1
  33. package/src/app/api/sessions/[id]/route.ts +12 -3
  34. package/src/app/api/sessions/route.ts +6 -2
  35. package/src/app/globals.css +14 -0
  36. package/src/app/layout.tsx +5 -20
  37. package/src/cli/index.js +33 -1
  38. package/src/cli/spec.js +40 -0
  39. package/src/components/agents/agent-avatar.tsx +45 -0
  40. package/src/components/agents/agent-card.tsx +19 -5
  41. package/src/components/agents/agent-chat-list.tsx +31 -24
  42. package/src/components/agents/agent-files-editor.tsx +185 -0
  43. package/src/components/agents/agent-list.tsx +82 -3
  44. package/src/components/agents/agent-sheet.tsx +31 -0
  45. package/src/components/agents/cron-job-form.tsx +137 -0
  46. package/src/components/agents/exec-config-panel.tsx +147 -0
  47. package/src/components/agents/inspector-panel.tsx +310 -0
  48. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  49. package/src/components/agents/permission-preset-selector.tsx +79 -0
  50. package/src/components/agents/personality-builder.tsx +111 -0
  51. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  52. package/src/components/agents/skill-install-dialog.tsx +102 -0
  53. package/src/components/agents/trash-list.tsx +109 -0
  54. package/src/components/chat/chat-area.tsx +14 -2
  55. package/src/components/chat/chat-header.tsx +168 -4
  56. package/src/components/chat/chat-preview-panel.tsx +113 -0
  57. package/src/components/chat/exec-approval-card.tsx +89 -0
  58. package/src/components/chat/message-bubble.tsx +218 -36
  59. package/src/components/chat/message-list.tsx +135 -31
  60. package/src/components/chat/streaming-bubble.tsx +59 -10
  61. package/src/components/chat/suggestions-bar.tsx +74 -0
  62. package/src/components/chat/thinking-indicator.tsx +20 -6
  63. package/src/components/chat/tool-call-bubble.tsx +89 -16
  64. package/src/components/chat/tool-request-banner.tsx +20 -2
  65. package/src/components/chat/trace-block.tsx +103 -0
  66. package/src/components/projects/project-list.tsx +1 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  68. package/src/components/shared/avatar.tsx +13 -2
  69. package/src/components/shared/settings/settings-page.tsx +1 -0
  70. package/src/components/tasks/task-board.tsx +1 -1
  71. package/src/components/tasks/task-sheet.tsx +12 -12
  72. package/src/hooks/use-continuous-speech.ts +42 -5
  73. package/src/hooks/use-openclaw-gateway.ts +63 -0
  74. package/src/lib/notification-sounds.ts +58 -0
  75. package/src/lib/personality-parser.ts +97 -0
  76. package/src/lib/providers/openclaw.ts +17 -2
  77. package/src/lib/runtime-loop.ts +2 -2
  78. package/src/lib/server/chat-execution.ts +44 -2
  79. package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
  80. package/src/lib/server/connectors/bluebubbles.ts +5 -2
  81. package/src/lib/server/connectors/googlechat.ts +14 -10
  82. package/src/lib/server/connectors/manager.ts +37 -15
  83. package/src/lib/server/connectors/openclaw.ts +1 -0
  84. package/src/lib/server/daemon-state.ts +11 -0
  85. package/src/lib/server/main-agent-loop.ts +2 -3
  86. package/src/lib/server/main-session.ts +21 -0
  87. package/src/lib/server/openclaw-config-sync.ts +107 -0
  88. package/src/lib/server/openclaw-exec-config.ts +52 -0
  89. package/src/lib/server/openclaw-gateway.ts +291 -0
  90. package/src/lib/server/openclaw-history-merge.ts +36 -0
  91. package/src/lib/server/openclaw-models.ts +56 -0
  92. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  93. package/src/lib/server/openclaw-sync.ts +11 -10
  94. package/src/lib/server/queue.ts +2 -1
  95. package/src/lib/server/session-tools/connector.ts +4 -4
  96. package/src/lib/server/session-tools/delegate.ts +19 -3
  97. package/src/lib/server/session-tools/file.ts +20 -20
  98. package/src/lib/server/session-tools/index.ts +2 -2
  99. package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
  100. package/src/lib/server/session-tools/sandbox.ts +2 -2
  101. package/src/lib/server/session-tools/search-providers.ts +13 -6
  102. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  103. package/src/lib/server/session-tools/shell.ts +1 -1
  104. package/src/lib/server/session-tools/web.ts +8 -8
  105. package/src/lib/server/storage.ts +62 -11
  106. package/src/lib/server/stream-agent-chat.ts +24 -4
  107. package/src/lib/server/suggestions.ts +20 -0
  108. package/src/lib/server/ws-hub.ts +14 -0
  109. package/src/stores/use-app-store.ts +53 -1
  110. package/src/stores/use-approval-store.ts +78 -0
  111. package/src/stores/use-chat-store.ts +153 -5
  112. package/src/types/index.ts +127 -1
  113. package/tsconfig.json +13 -4
@@ -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 } 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 } 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'
@@ -22,6 +22,7 @@ interface AppState {
22
22
  clearSessions: (ids: string[]) => Promise<void>
23
23
  togglePinSession: (id: string) => void
24
24
  updateSessionInStore: (session: Session) => void
25
+ forkSession: (sessionId: string, messageIndex: number) => Promise<string | null>
25
26
 
26
27
  sidebarOpen: boolean
27
28
  setSidebarOpen: (open: boolean) => void
@@ -161,6 +162,22 @@ interface AppState {
161
162
  activeProjectFilter: string | null
162
163
  setActiveProjectFilter: (id: string | null) => void
163
164
 
165
+ // Agent trash
166
+ trashedAgents: Record<string, Agent>
167
+ loadTrashedAgents: () => Promise<void>
168
+ showTrash: boolean
169
+ setShowTrash: (show: boolean) => void
170
+
171
+ // Inspector panel
172
+ inspectorOpen: boolean
173
+ setInspectorOpen: (open: boolean) => void
174
+ inspectorTab: 'overview' | 'files' | 'skills' | 'automations' | 'advanced'
175
+ setInspectorTab: (tab: 'overview' | 'files' | 'skills' | 'automations' | 'advanced') => void
176
+
177
+ // Fleet sidebar filter (F16)
178
+ fleetFilter: FleetFilter
179
+ setFleetFilter: (filter: FleetFilter) => void
180
+
164
181
  }
165
182
 
166
183
  export const useAppStore = create<AppState>((set, get) => ({
@@ -212,6 +229,18 @@ export const useAppStore = create<AppState>((set, get) => ({
212
229
  updateSessionInStore: (session) => {
213
230
  set({ sessions: { ...get().sessions, [session.id]: session } })
214
231
  },
232
+ forkSession: async (sessionId, messageIndex) => {
233
+ try {
234
+ const forked = await api<Session>('POST', `/sessions/${sessionId}/fork`, { messageIndex })
235
+ if (!forked?.id) return null
236
+ await get().loadSessions()
237
+ set({ currentSessionId: forked.id })
238
+ return forked.id
239
+ } catch (err: unknown) {
240
+ console.error('Fork failed:', err instanceof Error ? err.message : String(err))
241
+ return null
242
+ }
243
+ },
215
244
 
216
245
  sidebarOpen: false,
217
246
  setSidebarOpen: (open) => set({ sidebarOpen: open }),
@@ -492,4 +521,27 @@ export const useAppStore = create<AppState>((set, get) => ({
492
521
  activeProjectFilter: null,
493
522
  setActiveProjectFilter: (id) => set({ activeProjectFilter: id }),
494
523
 
524
+ // Agent trash
525
+ trashedAgents: {},
526
+ loadTrashedAgents: async () => {
527
+ try {
528
+ const trashedAgents = await api<Record<string, Agent>>('GET', '/agents/trash')
529
+ set({ trashedAgents })
530
+ } catch {
531
+ // ignore
532
+ }
533
+ },
534
+ showTrash: false,
535
+ setShowTrash: (show) => set({ showTrash: show }),
536
+
537
+ // Inspector panel
538
+ inspectorOpen: false,
539
+ setInspectorOpen: (open) => set({ inspectorOpen: open }),
540
+ inspectorTab: 'overview',
541
+ setInspectorTab: (tab) => set({ inspectorTab: tab }),
542
+
543
+ // Fleet sidebar filter
544
+ fleetFilter: 'all',
545
+ setFleetFilter: (filter) => set({ fleetFilter: filter }),
546
+
495
547
  }))
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import { create } from 'zustand'
4
+ import type { PendingExecApproval, ExecApprovalDecision } from '@/types'
5
+ import { api } from '@/lib/api-client'
6
+
7
+ interface ApprovalState {
8
+ approvals: Record<string, PendingExecApproval>
9
+ addApproval: (approval: PendingExecApproval) => void
10
+ removeApproval: (id: string) => void
11
+ resolveApproval: (id: string, decision: ExecApprovalDecision) => Promise<void>
12
+ pruneExpired: () => void
13
+ loadApprovals: () => Promise<void>
14
+ }
15
+
16
+ export const useApprovalStore = create<ApprovalState>((set) => ({
17
+ approvals: {},
18
+
19
+ addApproval: (approval) => {
20
+ set((s) => ({ approvals: { ...s.approvals, [approval.id]: approval } }))
21
+ },
22
+
23
+ removeApproval: (id) => {
24
+ set((s) => {
25
+ const next = { ...s.approvals }
26
+ delete next[id]
27
+ return { approvals: next }
28
+ })
29
+ },
30
+
31
+ resolveApproval: async (id, decision) => {
32
+ // Mark as resolving
33
+ set((s) => {
34
+ const approval = s.approvals[id]
35
+ if (!approval) return s
36
+ return { approvals: { ...s.approvals, [id]: { ...approval, resolving: true, error: undefined } } }
37
+ })
38
+
39
+ try {
40
+ await api('POST', '/openclaw/approvals', { id, decision })
41
+ // Remove on success
42
+ set((s) => {
43
+ const next = { ...s.approvals }
44
+ delete next[id]
45
+ return { approvals: next }
46
+ })
47
+ } catch (err: unknown) {
48
+ const message = err instanceof Error ? err.message : String(err)
49
+ set((s) => {
50
+ const approval = s.approvals[id]
51
+ if (!approval) return s
52
+ return { approvals: { ...s.approvals, [id]: { ...approval, resolving: false, error: message } } }
53
+ })
54
+ }
55
+ },
56
+
57
+ pruneExpired: () => {
58
+ const now = Date.now()
59
+ set((s) => {
60
+ const next: Record<string, PendingExecApproval> = {}
61
+ for (const [id, a] of Object.entries(s.approvals)) {
62
+ if (a.expiresAtMs > now) next[id] = a
63
+ }
64
+ return { approvals: next }
65
+ })
66
+ },
67
+
68
+ loadApprovals: async () => {
69
+ try {
70
+ const result = await api<PendingExecApproval[]>('GET', '/openclaw/approvals')
71
+ const approvals: Record<string, PendingExecApproval> = {}
72
+ for (const a of result) approvals[a.id] = a
73
+ set({ approvals })
74
+ } catch {
75
+ // ignore — gateway may be offline
76
+ }
77
+ },
78
+ }))
@@ -1,11 +1,12 @@
1
1
  'use client'
2
2
 
3
3
  import { create } from 'zustand'
4
- import type { Message, DevServerStatus, SSEEvent } from '../types'
4
+ import type { Message, DevServerStatus, SSEEvent, ChatTraceBlock } from '../types'
5
5
  import { streamChat } from '../lib/chat'
6
6
  import { speak } from '../lib/tts'
7
7
  import { getStoredAccessKey } from '../lib/api-client'
8
8
  import { useAppStore } from './use-app-store'
9
+ import { getSoundEnabled, setSoundEnabled, playStreamStart, playStreamEnd, playToolComplete, playError } from '../lib/notification-sounds'
9
10
 
10
11
  export interface PendingFile {
11
12
  file: File
@@ -33,6 +34,16 @@ interface ChatState {
33
34
  streamingSessionId: string | null
34
35
  streamText: string
35
36
 
37
+ // Task 1: Rich status indicator
38
+ streamPhase: 'thinking' | 'tool' | 'responding'
39
+ streamToolName: string
40
+
41
+ // Task 2: Typing cadence simulation
42
+ displayText: string
43
+
44
+ // Task 4: Live agent status bar
45
+ agentStatus: { goal?: string; status?: string; summary?: string; nextAction?: string } | null
46
+
36
47
  messages: Message[]
37
48
  setMessages: (msgs: Message[]) => void
38
49
 
@@ -44,6 +55,9 @@ interface ChatState {
44
55
  ttsEnabled: boolean
45
56
  toggleTts: () => void
46
57
 
58
+ soundEnabled: boolean
59
+ toggleSound: () => void
60
+
47
61
  // Multi-file attachment support
48
62
  pendingFiles: PendingFile[]
49
63
  addPendingFile: (f: PendingFile) => void
@@ -57,23 +71,50 @@ interface ChatState {
57
71
  devServer: DevServerStatus | null
58
72
  setDevServer: (ds: DevServerStatus | null) => void
59
73
 
74
+ previewContent: { type: 'browser' | 'image' | 'code' | 'html'; url?: string; content?: string; title?: string } | null
75
+ setPreviewContent: (content: { type: 'browser' | 'image' | 'code' | 'html'; url?: string; content?: string; title?: string } | null) => void
76
+
60
77
  debugOpen: boolean
61
78
  setDebugOpen: (open: boolean) => void
62
79
 
63
80
  sendMessage: (text: string) => Promise<void>
81
+ editAndResend: (messageIndex: number, newText: string) => Promise<void>
64
82
  retryLastMessage: () => Promise<void>
65
83
  sendHeartbeat: (sessionId: string) => Promise<void>
66
84
  stopStreaming: () => void
67
85
 
86
+ // Rich trace blocks during streaming (F13)
87
+ streamTraces: ChatTraceBlock[]
88
+
68
89
  // Voice conversation
69
90
  voiceConversationActive: boolean
70
91
  onStreamEvent: ((event: { t: string; text?: string }) => void) | null
71
92
  }
72
93
 
94
+ // Module-level cadence interval (not in state to avoid re-renders)
95
+ let _cadenceInterval: ReturnType<typeof setInterval> | null = null
96
+ let _cadenceBuffer = ''
97
+ let _cadencePos = 0
98
+
99
+ function clearCadence() {
100
+ if (_cadenceInterval) {
101
+ clearInterval(_cadenceInterval)
102
+ _cadenceInterval = null
103
+ }
104
+ _cadenceBuffer = ''
105
+ _cadencePos = 0
106
+ }
107
+
108
+ const CADENCE_THRESHOLD = 120
109
+
73
110
  export const useChatStore = create<ChatState>((set, get) => ({
74
111
  streaming: false,
75
112
  streamingSessionId: null,
76
113
  streamText: '',
114
+ streamPhase: 'thinking',
115
+ streamToolName: '',
116
+ displayText: '',
117
+ agentStatus: null,
77
118
  messages: [],
78
119
  setMessages: (msgs) => set({ messages: msgs, toolEvents: [] }),
79
120
  toolEvents: [],
@@ -81,6 +122,13 @@ export const useChatStore = create<ChatState>((set, get) => ({
81
122
  lastUsage: null,
82
123
  ttsEnabled: false,
83
124
  toggleTts: () => set((s) => ({ ttsEnabled: !s.ttsEnabled })),
125
+ soundEnabled: getSoundEnabled(),
126
+ toggleSound: () => {
127
+ const next = !get().soundEnabled
128
+ setSoundEnabled(next)
129
+ set({ soundEnabled: next })
130
+ },
131
+ streamTraces: [],
84
132
  voiceConversationActive: false,
85
133
  onStreamEvent: null,
86
134
 
@@ -93,6 +141,9 @@ export const useChatStore = create<ChatState>((set, get) => ({
93
141
  get pendingImage() { const files = get().pendingFiles; return files.length ? files[0] : null },
94
142
  setPendingImage: (img) => set({ pendingFiles: img ? [img] : [] }),
95
143
 
144
+ previewContent: null,
145
+ setPreviewContent: (content) => set({ previewContent: content }),
146
+
96
147
  devServer: null,
97
148
  setDevServer: (ds) => set({ devServer: ds }),
98
149
  debugOpen: false,
@@ -120,10 +171,15 @@ export const useChatStore = create<ChatState>((set, get) => ({
120
171
  imageUrl,
121
172
  attachedFiles,
122
173
  }
174
+ clearCadence()
123
175
  set((s) => ({
124
176
  streaming: true,
125
177
  streamingSessionId: sessionId,
126
178
  streamText: '',
179
+ streamPhase: 'thinking' as const,
180
+ streamToolName: '',
181
+ displayText: '',
182
+ agentStatus: null,
127
183
  messages: [...s.messages, userMsg],
128
184
  pendingFiles: [],
129
185
  toolEvents: [],
@@ -136,7 +192,9 @@ export const useChatStore = create<ChatState>((set, get) => ({
136
192
  }
137
193
 
138
194
  let fullText = ''
195
+ let suggestions: string[] | null = null
139
196
  let toolCallCounter = 0
197
+ let soundFiredStart = false
140
198
  const shouldIgnoreTransientError = (msg: string) =>
141
199
  /cancelled by steer mode|stopped by user/i.test(msg || '')
142
200
 
@@ -146,6 +204,42 @@ export const useChatStore = create<ChatState>((set, get) => ({
146
204
  if (event.t === 'd') {
147
205
  fullText += event.text || ''
148
206
  set({ streamText: fullText })
207
+
208
+ // Phase: first text data → 'responding'
209
+ if (get().streamPhase !== 'responding') {
210
+ set({ streamPhase: 'responding' })
211
+ }
212
+
213
+ // Sound: stream start
214
+ if (!soundFiredStart && get().soundEnabled) {
215
+ soundFiredStart = true
216
+ playStreamStart()
217
+ }
218
+
219
+ // Typing cadence: buffer first CADENCE_THRESHOLD chars, release word-by-word
220
+ if (fullText.length <= CADENCE_THRESHOLD) {
221
+ _cadenceBuffer = fullText
222
+ if (!_cadenceInterval) {
223
+ _cadenceInterval = setInterval(() => {
224
+ if (_cadencePos >= _cadenceBuffer.length) {
225
+ // Buffer fully released — check if we've passed threshold
226
+ if (get().streamText.length > CADENCE_THRESHOLD) {
227
+ clearCadence()
228
+ set({ displayText: get().streamText })
229
+ }
230
+ return
231
+ }
232
+ // Release ~2 chars per 16ms tick
233
+ const nextPos = Math.min(_cadencePos + 2, _cadenceBuffer.length)
234
+ _cadencePos = nextPos
235
+ set({ displayText: _cadenceBuffer.slice(0, _cadencePos) })
236
+ }, 16)
237
+ }
238
+ } else {
239
+ // Past threshold — sync displayText directly
240
+ if (_cadenceInterval) clearCadence()
241
+ set({ displayText: fullText })
242
+ }
149
243
  } else if (event.t === 'md') {
150
244
  // Parse metadata events (usage/run/queue). Ignore unknown keys.
151
245
  try {
@@ -153,6 +247,9 @@ export const useChatStore = create<ChatState>((set, get) => ({
153
247
  if (meta.usage) {
154
248
  set({ lastUsage: meta.usage })
155
249
  }
250
+ if (meta.suggestions) {
251
+ suggestions = meta.suggestions
252
+ }
156
253
  } catch {
157
254
  // Ignore non-JSON metadata payloads.
158
255
  }
@@ -162,6 +259,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
162
259
  } else if (event.t === 'tool_call') {
163
260
  const id = `tc-${++toolCallCounter}`
164
261
  set((s) => ({
262
+ streamPhase: 'tool' as const,
263
+ streamToolName: event.toolName || 'unknown',
165
264
  toolEvents: [...s.toolEvents, {
166
265
  id,
167
266
  name: event.toolName || 'unknown',
@@ -170,9 +269,9 @@ export const useChatStore = create<ChatState>((set, get) => ({
170
269
  }],
171
270
  }))
172
271
  } else if (event.t === 'tool_result') {
272
+ const soundOn = get().soundEnabled
173
273
  set((s) => {
174
274
  const events = [...s.toolEvents]
175
- // Find the last running event with matching name
176
275
  const idx = events.findLastIndex(
177
276
  (e) => e.name === event.toolName && e.status === 'running',
178
277
  )
@@ -183,6 +282,10 @@ export const useChatStore = create<ChatState>((set, get) => ({
183
282
  || output.includes('ETIMEDOUT')
184
283
  || output.includes('Error:')
185
284
  events[idx] = { ...events[idx], status: isError ? 'error' : 'done', output }
285
+ if (soundOn) {
286
+ if (isError) playError()
287
+ else playToolComplete()
288
+ }
186
289
  }
187
290
  return { toolEvents: events }
188
291
  })
@@ -191,12 +294,22 @@ export const useChatStore = create<ChatState>((set, get) => ({
191
294
  if (!shouldIgnoreTransientError(errText)) {
192
295
  fullText += '\n[Error: ' + errText + ']'
193
296
  set({ streamText: fullText })
297
+ if (get().soundEnabled) playError()
298
+ }
299
+ } else if (event.t === 'status') {
300
+ try {
301
+ const parsed = JSON.parse(event.text || '{}')
302
+ set({ agentStatus: parsed })
303
+ } catch {
304
+ // ignore malformed status
194
305
  }
195
306
  } else if (event.t === 'done') {
196
307
  // done
197
308
  }
198
309
  }, attachedFiles)
199
310
 
311
+ clearCadence()
312
+ if (get().soundEnabled && soundFiredStart) playStreamEnd()
200
313
  if (fullText.trim()) {
201
314
  const currentToolEvents = get().toolEvents
202
315
  const assistantMsg: Message = {
@@ -210,21 +323,55 @@ export const useChatStore = create<ChatState>((set, get) => ({
210
323
  output: e.output,
211
324
  error: e.status === 'error' || undefined,
212
325
  })) : undefined,
326
+ suggestions: suggestions || undefined,
213
327
  }
214
328
  set((s) => ({
215
329
  messages: [...s.messages, assistantMsg],
216
330
  streaming: false,
217
331
  streamingSessionId: null,
218
332
  streamText: '',
333
+ displayText: '',
334
+ streamPhase: 'thinking' as const,
335
+ streamToolName: '',
219
336
  }))
220
337
  if (get().ttsEnabled && !get().voiceConversationActive) speak(fullText)
221
338
  } else {
222
- set({ streaming: false, streamingSessionId: null, streamText: '' })
339
+ set({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking' as const, streamToolName: '' })
223
340
  }
224
341
 
225
342
  useAppStore.getState().loadSessions()
226
343
  },
227
344
 
345
+ editAndResend: async (messageIndex: number, newText: string) => {
346
+ if (get().streaming) return
347
+ const sessionId = useAppStore.getState().currentSessionId
348
+ if (!sessionId) return
349
+ try {
350
+ const key = getStoredAccessKey()
351
+ const res = await fetch(`/api/sessions/${sessionId}/edit-resend`, {
352
+ method: 'POST',
353
+ headers: {
354
+ 'Content-Type': 'application/json',
355
+ ...(key ? { 'X-Access-Key': key } : {}),
356
+ },
357
+ body: JSON.stringify({ messageIndex, newText }),
358
+ })
359
+ if (!res.ok) return
360
+ // Reload messages from server (truncated)
361
+ const msgsRes = await fetch(`/api/sessions/${sessionId}/messages`, {
362
+ headers: key ? { 'X-Access-Key': key } : undefined,
363
+ })
364
+ if (msgsRes.ok) {
365
+ const msgs = await msgsRes.json()
366
+ set({ messages: msgs })
367
+ }
368
+ // Re-send with the new text
369
+ await get().sendMessage(newText)
370
+ } catch {
371
+ // ignore
372
+ }
373
+ },
374
+
228
375
  retryLastMessage: async () => {
229
376
  if (get().streaming) return
230
377
  const sessionId = useAppStore.getState().currentSessionId
@@ -312,7 +459,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
312
459
  .filter((line) => !line.includes('[MAIN_LOOP_META]'))
313
460
  .join('\n')
314
461
  .trim()
315
- if (!trimmed || trimmed === 'HEARTBEAT_OK' || sawError) return
462
+ if (!trimmed || trimmed === 'HEARTBEAT_OK' || trimmed === 'NO_MESSAGE' || sawError) return
316
463
 
317
464
  const assistantMsg: Message = {
318
465
  role: 'assistant',
@@ -346,6 +493,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
346
493
  // ignore
347
494
  }
348
495
  }
349
- set({ streaming: false, streamingSessionId: null, streamText: '' })
496
+ clearCadence()
497
+ set({ streaming: false, streamingSessionId: null, streamText: '', displayText: '', streamPhase: 'thinking' as const, streamToolName: '' })
350
498
  },
351
499
  }))
@@ -15,6 +15,8 @@ export interface Message {
15
15
  toolEvents?: MessageToolEvent[]
16
16
  kind?: 'chat' | 'heartbeat' | 'system'
17
17
  suppressed?: boolean
18
+ bookmarked?: boolean
19
+ suggestions?: string[]
18
20
  }
19
21
 
20
22
  export type ProviderType = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'openai' | 'ollama' | 'anthropic' | 'openclaw' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks'
@@ -61,6 +63,7 @@ export interface Session {
61
63
  createdAt: number
62
64
  lastActiveAt: number
63
65
  active?: boolean
66
+ mainSession?: boolean
64
67
  sessionType?: SessionType
65
68
  agentId?: string | null
66
69
  parentSessionId?: string | null
@@ -109,6 +112,7 @@ export interface Session {
109
112
  file?: string | null
110
113
  queuedCount?: number
111
114
  currentRunId?: string | null
115
+ conversationTone?: string
112
116
  }
113
117
 
114
118
  export type Sessions = Record<string, Session>
@@ -178,7 +182,7 @@ export interface MarketplacePlugin {
178
182
  }
179
183
 
180
184
  export interface SSEEvent {
181
- t: 'd' | 'md' | 'r' | 'done' | 'err' | 'tool_call' | 'tool_result'
185
+ t: 'd' | 'md' | 'r' | 'done' | 'err' | 'tool_call' | 'tool_result' | 'status'
182
186
  text?: string
183
187
  toolName?: string
184
188
  toolInput?: string
@@ -248,6 +252,10 @@ export interface Agent {
248
252
  heartbeatNextAction?: string | null
249
253
  thinkingLevel?: 'minimal' | 'low' | 'medium' | 'high'
250
254
  projectId?: string
255
+ avatarSeed?: string
256
+ trashedAt?: number
257
+ openclawSkillMode?: SkillAllowlistMode
258
+ openclawAllowedSkills?: string[]
251
259
  createdAt: number
252
260
  updatedAt: number
253
261
  }
@@ -535,6 +543,8 @@ export interface Connector {
535
543
  authenticated?: boolean
536
544
  /** WhatsApp has stored credentials from previous pairing (runtime only) */
537
545
  hasCredentials?: boolean
546
+ /** Connector presence info (runtime only) */
547
+ presence?: { lastMessageAt?: number | null; channelId?: string | null }
538
548
  createdAt: number
539
549
  updatedAt: number
540
550
  }
@@ -651,3 +661,119 @@ export interface ClawHubSkill {
651
661
  url: string
652
662
  version: string
653
663
  }
664
+
665
+ // --- OpenClaw Execution Approvals ---
666
+
667
+ export interface PendingExecApproval {
668
+ id: string
669
+ agentId: string
670
+ sessionKey: string
671
+ command: string
672
+ cwd?: string
673
+ host?: string
674
+ security?: string
675
+ ask?: string
676
+ createdAtMs: number
677
+ expiresAtMs: number
678
+ resolving?: boolean
679
+ error?: string
680
+ }
681
+
682
+ export type ExecApprovalDecision = 'allow-once' | 'allow-always' | 'deny'
683
+
684
+ // --- OpenClaw Skills ---
685
+
686
+ export interface OpenClawSkillEntry {
687
+ name: string
688
+ description?: string
689
+ source: 'bundled' | 'managed' | 'personal' | 'workspace'
690
+ eligible: boolean
691
+ requirements?: string[]
692
+ missing?: string[]
693
+ disabled?: boolean
694
+ installOptions?: SkillInstallOption[]
695
+ skillRequirements?: SkillRequirements
696
+ configChecks?: { key: string; ok: boolean }[]
697
+ skillKey?: string
698
+ baseDir?: string
699
+ }
700
+
701
+ export type SkillAllowlistMode = 'all' | 'none' | 'selected'
702
+
703
+ // --- Fleet Sidebar Filters (F16) ---
704
+ export type FleetFilter = 'all' | 'running' | 'approvals'
705
+
706
+ // --- Exec Approval Config (F8) ---
707
+ export interface ExecApprovalConfig {
708
+ security: 'deny' | 'allowlist' | 'full'
709
+ askMode: 'off' | 'on-miss' | 'always'
710
+ patterns: string[]
711
+ }
712
+
713
+ export interface ExecApprovalSnapshot {
714
+ path: string
715
+ exists: boolean
716
+ hash: string
717
+ file: ExecApprovalConfig
718
+ }
719
+
720
+ // --- Permission Presets (F9) ---
721
+ export type PermissionPreset = 'conservative' | 'collaborative' | 'autonomous'
722
+
723
+ // --- Personality Builder (F10) ---
724
+ export interface PersonalityDraft {
725
+ identity: { name?: string; creature?: string; vibe?: string; emoji?: string }
726
+ user: { name?: string; callThem?: string; pronouns?: string; timezone?: string; notes?: string; context?: string }
727
+ soul: { coreTruths?: string; boundaries?: string; vibe?: string; continuity?: string }
728
+ }
729
+
730
+ // --- Skill Lifecycle (F11) ---
731
+ export interface SkillInstallOption {
732
+ kind: 'brew' | 'node' | 'go' | 'uv' | 'download'
733
+ label: string
734
+ bins?: string[]
735
+ }
736
+
737
+ export interface SkillRequirements {
738
+ bins?: string[]
739
+ anyBins?: string[][]
740
+ env?: string[]
741
+ config?: string[]
742
+ os?: string[]
743
+ }
744
+
745
+ // --- Cron Jobs (F12) ---
746
+ export interface GatewayCronJob {
747
+ id: string
748
+ name: string
749
+ agentId: string
750
+ enabled: boolean
751
+ schedule: { kind: 'at' | 'every' | 'cron'; value: string; timezone?: string }
752
+ payload: {
753
+ kind: 'systemEvent' | 'agentTurn'
754
+ text?: string
755
+ message?: string
756
+ model?: string
757
+ deliver?: { mode: 'none' | 'announce'; channel?: string }
758
+ }
759
+ sessionTarget: 'main' | 'isolated'
760
+ state?: { nextRun?: string; lastRun?: string; lastStatus?: string }
761
+ }
762
+
763
+ // --- Rich Chat Traces (F13) ---
764
+ export interface ChatTraceBlock {
765
+ type: 'thinking' | 'tool-call' | 'tool-result'
766
+ content: string
767
+ label?: string
768
+ collapsed?: boolean
769
+ }
770
+
771
+ // --- Chat History Sync (F18) ---
772
+ export interface GatewaySessionPreview {
773
+ sessionKey: string
774
+ epoch: number
775
+ messages: Array<{ role: string; content: string; ts: number }>
776
+ }
777
+
778
+ // --- Gateway Reload Mode (F21) ---
779
+ export type GatewayReloadMode = 'hot' | 'hybrid' | 'full'
package/tsconfig.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2017",
4
- "lib": ["dom", "dom.iterable", "esnext"],
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
5
9
  "allowJs": true,
6
10
  "skipLibCheck": true,
7
11
  "strict": true,
@@ -19,7 +23,9 @@
19
23
  }
20
24
  ],
21
25
  "paths": {
22
- "@/*": ["./src/*"]
26
+ "@/*": [
27
+ "./src/*"
28
+ ]
23
29
  }
24
30
  },
25
31
  "include": [
@@ -28,7 +34,10 @@
28
34
  "**/*.tsx",
29
35
  ".next/types/**/*.ts",
30
36
  ".next/dev/types/**/*.ts",
31
- "**/*.mts"
37
+ "**/*.mts",
38
+ ".next/dev/dev/types/**/*.ts"
32
39
  ],
33
- "exclude": ["node_modules"]
40
+ "exclude": [
41
+ "node_modules"
42
+ ]
34
43
  }