@swarmclawai/swarmclaw 1.2.0 → 1.2.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 (241) hide show
  1. package/README.md +19 -0
  2. package/package.json +5 -2
  3. package/skills/coding-agent/SKILL.md +111 -0
  4. package/skills/github/SKILL.md +140 -0
  5. package/skills/nano-banana-pro/SKILL.md +62 -0
  6. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  7. package/skills/nano-pdf/SKILL.md +53 -0
  8. package/skills/openai-image-gen/SKILL.md +78 -0
  9. package/skills/openai-image-gen/scripts/gen.py +328 -0
  10. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  11. package/skills/skill-creator/SKILL.md +147 -0
  12. package/skills/skill-creator/scripts/init_skill.py +378 -0
  13. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  14. package/skills/summarize/SKILL.md +77 -0
  15. package/src/app/api/auth/route.ts +20 -5
  16. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  17. package/src/app/api/chats/[id]/devserver/route.ts +17 -20
  18. package/src/app/api/chats/[id]/messages/route.ts +15 -11
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/credentials/[id]/route.ts +4 -1
  24. package/src/app/api/extensions/marketplace/route.ts +5 -2
  25. package/src/app/api/ip/route.ts +2 -2
  26. package/src/app/api/memory/maintenance/route.ts +5 -2
  27. package/src/app/api/preview-server/route.ts +15 -12
  28. package/src/app/api/projects/[id]/route.ts +7 -46
  29. package/src/app/api/system/status/route.ts +11 -0
  30. package/src/app/api/upload/route.ts +4 -1
  31. package/src/cli/index.js +7 -0
  32. package/src/cli/spec.js +1 -0
  33. package/src/components/agents/agent-files-editor.tsx +44 -32
  34. package/src/components/agents/personality-builder.tsx +13 -7
  35. package/src/components/agents/trash-list.tsx +1 -1
  36. package/src/components/chat/chat-area.tsx +45 -23
  37. package/src/components/chat/message-bubble.test.ts +35 -0
  38. package/src/components/chat/message-bubble.tsx +20 -9
  39. package/src/components/chat/message-list.tsx +62 -42
  40. package/src/components/chat/swarm-status-card.tsx +10 -3
  41. package/src/components/input/chat-input.tsx +34 -14
  42. package/src/components/layout/daemon-indicator.tsx +7 -8
  43. package/src/components/layout/update-banner.tsx +8 -13
  44. package/src/components/logs/log-list.tsx +1 -1
  45. package/src/components/memory/memory-card.tsx +3 -1
  46. package/src/components/org-chart/org-chart-view.tsx +4 -0
  47. package/src/components/projects/project-list.tsx +4 -2
  48. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  49. package/src/components/secrets/secret-sheet.tsx +1 -1
  50. package/src/components/secrets/secrets-list.tsx +1 -1
  51. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  52. package/src/components/shared/dir-browser.tsx +22 -18
  53. package/src/components/skills/skill-sheet.tsx +2 -3
  54. package/src/components/tasks/task-list.tsx +1 -1
  55. package/src/components/tasks/task-sheet.tsx +1 -1
  56. package/src/hooks/use-openclaw-gateway.ts +46 -27
  57. package/src/instrumentation.ts +10 -7
  58. package/src/lib/chat/assistant-render-id.ts +3 -0
  59. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  60. package/src/lib/chat/chat-streaming-state.ts +20 -8
  61. package/src/lib/chat/chat.ts +18 -2
  62. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  63. package/src/lib/chat/queued-message-queue.ts +11 -2
  64. package/src/lib/providers/anthropic.ts +6 -3
  65. package/src/lib/providers/claude-cli.ts +9 -3
  66. package/src/lib/providers/cli-utils.test.ts +124 -0
  67. package/src/lib/providers/cli-utils.ts +15 -0
  68. package/src/lib/providers/codex-cli.ts +9 -3
  69. package/src/lib/providers/gemini-cli.ts +6 -2
  70. package/src/lib/providers/index.ts +4 -1
  71. package/src/lib/providers/ollama.ts +5 -2
  72. package/src/lib/providers/openai.ts +8 -5
  73. package/src/lib/providers/opencode-cli.ts +6 -2
  74. package/src/lib/server/activity/activity-log.ts +21 -0
  75. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  76. package/src/lib/server/agents/agent-cascade.ts +79 -59
  77. package/src/lib/server/agents/agent-registry.ts +23 -4
  78. package/src/lib/server/agents/agent-repository.ts +90 -0
  79. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  80. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  81. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  82. package/src/lib/server/agents/guardian.ts +2 -2
  83. package/src/lib/server/agents/main-agent-loop.ts +14 -6
  84. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  85. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  86. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  87. package/src/lib/server/agents/task-session.ts +3 -4
  88. package/src/lib/server/approvals/approval-repository.ts +30 -0
  89. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  90. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  91. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  92. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  93. package/src/lib/server/chat-execution/chat-execution.ts +84 -1914
  94. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  95. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  96. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  97. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  98. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  99. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  100. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  101. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  102. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  103. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  104. package/src/lib/server/chat-execution/post-stream-finalization.ts +5 -2
  105. package/src/lib/server/chat-execution/prompt-builder.ts +22 -1
  106. package/src/lib/server/chat-execution/prompt-sections.ts +55 -13
  107. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  108. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  109. package/src/lib/server/chat-execution/stream-agent-chat.ts +58 -25
  110. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  111. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  112. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  113. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  114. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  115. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  116. package/src/lib/server/connectors/connector-repository.ts +58 -0
  117. package/src/lib/server/connectors/discord.ts +10 -7
  118. package/src/lib/server/connectors/email.ts +17 -14
  119. package/src/lib/server/connectors/googlechat.ts +7 -4
  120. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  121. package/src/lib/server/connectors/matrix.ts +6 -3
  122. package/src/lib/server/connectors/openclaw.ts +20 -17
  123. package/src/lib/server/connectors/outbox.ts +4 -1
  124. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  125. package/src/lib/server/connectors/runtime-state.ts +19 -0
  126. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  127. package/src/lib/server/connectors/signal.ts +9 -6
  128. package/src/lib/server/connectors/slack.ts +13 -10
  129. package/src/lib/server/connectors/teams.ts +8 -5
  130. package/src/lib/server/connectors/telegram.ts +15 -12
  131. package/src/lib/server/connectors/whatsapp.ts +32 -29
  132. package/src/lib/server/credentials/credential-repository.ts +7 -0
  133. package/src/lib/server/embeddings.ts +4 -1
  134. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  135. package/src/lib/server/link-understanding.ts +4 -1
  136. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  137. package/src/lib/server/memory/memory-abstract.ts +59 -0
  138. package/src/lib/server/memory/memory-db.ts +40 -14
  139. package/src/lib/server/missions/mission-repository.ts +74 -0
  140. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  141. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  142. package/src/lib/server/missions/mission-service/context.ts +4 -0
  143. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  144. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  145. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  146. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  147. package/src/lib/server/missions/mission-service.test.ts +9 -2
  148. package/src/lib/server/missions/mission-service.ts +6 -2263
  149. package/src/lib/server/openclaw/gateway.ts +8 -5
  150. package/src/lib/server/persistence/repository-utils.ts +154 -0
  151. package/src/lib/server/persistence/storage-context.ts +51 -0
  152. package/src/lib/server/persistence/transaction.ts +1 -0
  153. package/src/lib/server/project-utils.ts +13 -0
  154. package/src/lib/server/projects/project-repository.ts +36 -0
  155. package/src/lib/server/projects/project-service.ts +79 -0
  156. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  157. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  158. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  159. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  160. package/src/lib/server/provider-health.ts +18 -0
  161. package/src/lib/server/query-expansion.ts +4 -1
  162. package/src/lib/server/runtime/alert-dispatch.ts +8 -7
  163. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  164. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  165. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  166. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  167. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  168. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  169. package/src/lib/server/runtime/daemon-state.ts +3 -1331
  170. package/src/lib/server/runtime/estop-repository.ts +4 -0
  171. package/src/lib/server/runtime/estop.ts +3 -1
  172. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  173. package/src/lib/server/runtime/heartbeat-service.ts +78 -34
  174. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  175. package/src/lib/server/runtime/idle-window.ts +6 -3
  176. package/src/lib/server/runtime/network.ts +11 -0
  177. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  178. package/src/lib/server/runtime/perf.ts +4 -1
  179. package/src/lib/server/runtime/process-manager.ts +7 -4
  180. package/src/lib/server/runtime/queue/claims.ts +4 -0
  181. package/src/lib/server/runtime/queue/core.ts +2079 -0
  182. package/src/lib/server/runtime/queue/execution.ts +7 -0
  183. package/src/lib/server/runtime/queue/followups.ts +4 -0
  184. package/src/lib/server/runtime/queue/queries.ts +12 -0
  185. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  186. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  187. package/src/lib/server/runtime/queue-repository.ts +17 -0
  188. package/src/lib/server/runtime/queue.ts +5 -2058
  189. package/src/lib/server/runtime/run-ledger.ts +6 -5
  190. package/src/lib/server/runtime/run-repository.ts +73 -0
  191. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  192. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  193. package/src/lib/server/runtime/runtime-state.ts +99 -0
  194. package/src/lib/server/runtime/scheduler.ts +13 -8
  195. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  196. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  197. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  198. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  199. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  200. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  201. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  202. package/src/lib/server/runtime/session-run-manager.ts +72 -1374
  203. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  204. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  205. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  206. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  207. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  208. package/src/lib/server/session-tools/context.ts +14 -0
  209. package/src/lib/server/session-tools/discovery.ts +9 -6
  210. package/src/lib/server/session-tools/index.ts +3 -1
  211. package/src/lib/server/session-tools/platform.ts +1 -1
  212. package/src/lib/server/session-tools/subagent.ts +23 -2
  213. package/src/lib/server/session-tools/wallet.ts +4 -1
  214. package/src/lib/server/sessions/session-repository.ts +85 -0
  215. package/src/lib/server/settings/settings-repository.ts +25 -0
  216. package/src/lib/server/skills/clawhub-client.ts +4 -1
  217. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  218. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  219. package/src/lib/server/skills/skill-discovery.ts +2 -2
  220. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  221. package/src/lib/server/skills/skill-repository.ts +14 -0
  222. package/src/lib/server/solana.ts +6 -0
  223. package/src/lib/server/storage-auth.ts +5 -5
  224. package/src/lib/server/storage-normalization.ts +4 -0
  225. package/src/lib/server/storage.ts +32 -32
  226. package/src/lib/server/tasks/task-followups.ts +4 -1
  227. package/src/lib/server/tasks/task-repository.ts +54 -0
  228. package/src/lib/server/tool-loop-detection.ts +8 -3
  229. package/src/lib/server/tool-planning.ts +226 -0
  230. package/src/lib/server/tool-retry.ts +4 -3
  231. package/src/lib/server/usage/usage-repository.ts +30 -0
  232. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  233. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  234. package/src/lib/server/ws-hub.ts +5 -2
  235. package/src/lib/strip-internal-metadata.test.ts +78 -37
  236. package/src/lib/strip-internal-metadata.ts +20 -6
  237. package/src/stores/use-approval-store.ts +7 -1
  238. package/src/stores/use-chat-store.test.ts +54 -0
  239. package/src/stores/use-chat-store.ts +26 -6
  240. package/src/types/index.ts +6 -0
  241. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -0,0 +1,11 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getDaemonHealthSummary } from '@/lib/server/runtime/daemon-state'
3
+ import packageJson from '../../../../../package.json'
4
+
5
+ export async function GET() {
6
+ const summary = getDaemonHealthSummary()
7
+ return NextResponse.json({
8
+ ...summary,
9
+ version: packageJson.version,
10
+ })
11
+ }
@@ -3,6 +3,9 @@ import fs from 'fs'
3
3
  import path from 'path'
4
4
  import { genId } from '@/lib/id'
5
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
6
+ import { log } from '@/lib/server/logger'
7
+
8
+ const TAG = 'api-upload'
6
9
 
7
10
  export async function POST(req: Request) {
8
11
  const filename = req.headers.get('x-filename') || 'image.png'
@@ -12,7 +15,7 @@ export async function POST(req: Request) {
12
15
 
13
16
  if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
14
17
  fs.writeFileSync(filePath, buf)
15
- console.log(`[upload] saved ${buf.length} bytes to ${filePath}`)
18
+ log.info(TAG, `saved ${buf.length} bytes to ${filePath}`)
16
19
 
17
20
  return NextResponse.json({ path: filePath, size: buf.length, url: `/api/uploads/${name}` })
18
21
  }
package/src/cli/index.js CHANGED
@@ -730,6 +730,13 @@ const COMMAND_GROUPS = [
730
730
  cmd('delete-many', 'DELETE', '/uploads', 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', { expectsJsonBody: true }),
731
731
  ],
732
732
  },
733
+ {
734
+ name: 'system-status',
735
+ description: 'Lightweight system health summary',
736
+ commands: [
737
+ cmd('get', 'GET', '/system/status', 'Get system health summary (safe for external monitors)'),
738
+ ],
739
+ },
733
740
  {
734
741
  name: 'usage',
735
742
  description: 'Usage and cost summary',
package/src/cli/spec.js CHANGED
@@ -496,6 +496,7 @@ const COMMAND_GROUPS = {
496
496
  description: 'System and version endpoints',
497
497
  commands: {
498
498
  ip: { description: 'Get local bind IP/port', method: 'GET', path: '/ip' },
499
+ status: { description: 'Get lightweight system health summary (safe for external monitors)', method: 'GET', path: '/system/status' },
499
500
  usage: { description: 'Get usage summary', method: 'GET', path: '/usage' },
500
501
  version: { description: 'Get local/remote git version info', method: 'GET', path: '/version' },
501
502
  update: { description: 'Update to latest stable release tag (fallback: main)', method: 'POST', path: '/version/update' },
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useEffect, useState } from 'react'
3
+ import { useEffect, useState } from 'react'
4
4
  import { api } from '@/lib/app/api-client'
5
5
  import { errorMessage } from '@/lib/shared-utils'
6
6
  import { PersonalityBuilder } from './personality-builder'
@@ -8,6 +8,14 @@ import { PersonalityBuilder } from './personality-builder'
8
8
  const FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md', 'MEMORY.md', 'AGENTS.md'] as const
9
9
  const GUIDED_FILES = new Set(['SOUL.md', 'IDENTITY.md', 'USER.md'])
10
10
 
11
+ function makeInitialFiles(): Record<string, FileState> {
12
+ const initial: Record<string, FileState> = {}
13
+ for (const f of FILES) {
14
+ initial[f] = { content: '', original: '', loading: true, saving: false }
15
+ }
16
+ return initial
17
+ }
18
+
11
19
  interface FileState {
12
20
  content: string
13
21
  original: string
@@ -22,45 +30,49 @@ interface Props {
22
30
 
23
31
  export function AgentFilesEditor({ agentId }: Props) {
24
32
  const [activeTab, setActiveTab] = useState<string>(FILES[0])
25
- const [files, setFiles] = useState<Record<string, FileState>>({})
33
+ const [files, setFiles] = useState<Record<string, FileState>>(makeInitialFiles)
26
34
  const [guidedMode, setGuidedMode] = useState(false)
27
35
 
28
- const loadFiles = useCallback(async () => {
29
- const initial: Record<string, FileState> = {}
30
- for (const f of FILES) {
31
- initial[f] = { content: '', original: '', loading: true, saving: false }
32
- }
33
- setFiles(initial)
36
+ // Reset to loading state when agentId changes
37
+ const [prevAgentId, setPrevAgentId] = useState(agentId)
38
+ if (agentId !== prevAgentId) {
39
+ setPrevAgentId(agentId)
40
+ setFiles(makeInitialFiles())
41
+ }
34
42
 
35
- try {
36
- const result = await api<Record<string, { content: string; error?: string }>>('GET', `/openclaw/agent-files?agentId=${agentId}`)
37
- setFiles((prev) => {
38
- const next = { ...prev }
39
- for (const [name, data] of Object.entries(result)) {
40
- next[name] = {
41
- content: data.content,
42
- original: data.content,
43
- loading: false,
44
- saving: false,
45
- error: data.error,
43
+ useEffect(() => {
44
+ let cancelled = false
45
+ api<Record<string, { content: string; error?: string }>>('GET', `/openclaw/agent-files?agentId=${agentId}`)
46
+ .then((result) => {
47
+ if (cancelled) return
48
+ setFiles((prev) => {
49
+ const next = { ...prev }
50
+ for (const [name, data] of Object.entries(result)) {
51
+ next[name] = {
52
+ content: data.content,
53
+ original: data.content,
54
+ loading: false,
55
+ saving: false,
56
+ error: data.error,
57
+ }
46
58
  }
47
- }
48
- return next
59
+ return next
60
+ })
49
61
  })
50
- } catch (err: unknown) {
51
- const message = errorMessage(err)
52
- setFiles((prev) => {
53
- const next = { ...prev }
54
- for (const f of FILES) {
55
- next[f] = { ...next[f], loading: false, error: message }
56
- }
57
- return next
62
+ .catch((err: unknown) => {
63
+ if (cancelled) return
64
+ const message = errorMessage(err)
65
+ setFiles((prev) => {
66
+ const next = { ...prev }
67
+ for (const f of FILES) {
68
+ next[f] = { ...next[f], loading: false, error: message }
69
+ }
70
+ return next
71
+ })
58
72
  })
59
- }
73
+ return () => { cancelled = true }
60
74
  }, [agentId])
61
75
 
62
- useEffect(() => { loadFiles() }, [loadFiles])
63
-
64
76
  const handleContentChange = (filename: string, content: string) => {
65
77
  setFiles((prev) => ({
66
78
  ...prev,
@@ -1,8 +1,7 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useMemo, useState } from 'react'
3
+ import { useMemo, useState } from 'react'
4
4
  import type { PersonalityDraft } from '@/types'
5
- import { api } from '@/lib/app/api-client'
6
5
  import {
7
6
  parseIdentityMd, serializeIdentityMd,
8
7
  parseUserMd, serializeUserMd,
@@ -19,12 +18,12 @@ interface Props {
19
18
  const inputClass = 'w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text outline-none placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors'
20
19
  const labelClass = 'block text-[11px] font-600 uppercase tracking-wider text-text-3/50 mb-1'
21
20
 
22
- export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSave }: Props) {
21
+ export function PersonalityBuilder({ fileType, content, onSave }: Props) {
23
22
  const [draft, setDraft] = useState<Record<string, string>>({})
24
23
  const [initialDraft, setInitialDraft] = useState<Record<string, string>>({})
25
24
  const [saveState, setSaveState] = useState<'idle' | 'saved'>('idle')
26
25
 
27
- useEffect(() => {
26
+ const parsedContent = useMemo(() => {
28
27
  let parsed: Record<string, string> = {}
29
28
  if (fileType === 'IDENTITY.md') {
30
29
  const p = parseIdentityMd(content)
@@ -36,11 +35,18 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
36
35
  const p = parseSoulMd(content)
37
36
  parsed = { coreTruths: p.coreTruths || '', boundaries: p.boundaries || '', vibe: p.vibe || '', continuity: p.continuity || '' }
38
37
  }
39
- setDraft(parsed)
40
- setInitialDraft(parsed)
41
- setSaveState('idle')
38
+ return parsed
42
39
  }, [content, fileType])
43
40
 
41
+ // Reset form when content/fileType changes (render-time state adjustment)
42
+ const [syncKey, setSyncKey] = useState({ content, fileType })
43
+ if (content !== syncKey.content || fileType !== syncKey.fileType) {
44
+ setSyncKey({ content, fileType })
45
+ setDraft(parsedContent)
46
+ setInitialDraft(parsedContent)
47
+ setSaveState('idle')
48
+ }
49
+
44
50
  const isDirty = useMemo(() => {
45
51
  return Object.keys(draft).some((k) => draft[k] !== (initialDraft[k] ?? ''))
46
52
  }, [draft, initialDraft])
@@ -12,7 +12,7 @@ export function TrashList() {
12
12
  const loadAgents = useAppStore((s) => s.loadAgents)
13
13
  const [confirmPermanent, setConfirmPermanent] = useState<Agent | null>(null)
14
14
 
15
- useEffect(() => { loadTrashedAgents() }, [])
15
+ useEffect(() => { loadTrashedAgents() }, [loadTrashedAgents])
16
16
 
17
17
  const handleRestore = async (id: string) => {
18
18
  await api('POST', '/agents/trash', { id })
@@ -27,6 +27,7 @@ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
27
27
  import { speak } from '@/lib/tts'
28
28
  import { api } from '@/lib/app/api-client'
29
29
  import { messagesDiffer } from '@/lib/chat/chat-streaming-state'
30
+ import { createAssistantRenderId } from '@/lib/chat/assistant-render-id'
30
31
  import { getSessionLastMessage } from '@/lib/chat/session-summary'
31
32
  import { getEnabledCapabilityIds, getEnabledToolIds } from '@/lib/capability-selection'
32
33
 
@@ -71,6 +72,36 @@ export function ChatArea() {
71
72
  const setPreviewContent = useChatStore((s) => s.setPreviewContent)
72
73
  const isDesktop = useMediaQuery('(min-width: 768px)')
73
74
 
75
+ const markSessionLocallyIdle = useCallback((targetSessionId: string) => {
76
+ const appState = useAppStore.getState()
77
+ const existing = appState.sessions[targetSessionId]
78
+ if (!existing) return
79
+ appState.updateSessionInStore({
80
+ ...existing,
81
+ active: false,
82
+ currentRunId: null,
83
+ })
84
+ }, [])
85
+
86
+ const startServerStreamingPlaceholder = useCallback((targetSessionId: string, phase: 'queued' | 'thinking' | 'connecting' = 'thinking') => {
87
+ useChatStore.setState((state) => {
88
+ const sameServerStream = state.streaming
89
+ && state.streamSource === 'server'
90
+ && state.streamingSessionId === targetSessionId
91
+
92
+ return {
93
+ streaming: true,
94
+ streamingSessionId: targetSessionId,
95
+ streamSource: 'server',
96
+ streamPhase: sameServerStream ? state.streamPhase : phase,
97
+ streamText: '',
98
+ displayText: '',
99
+ assistantRenderId: sameServerStream && state.assistantRenderId ? state.assistantRenderId : createAssistantRenderId(),
100
+ thinkingStartTime: sameServerStream && state.thinkingStartTime > 0 ? state.thinkingStartTime : Date.now(),
101
+ }
102
+ })
103
+ }, [])
104
+
74
105
  const currentAgent = useAppStore((s) => {
75
106
  const agentId = session?.agentId
76
107
  return agentId ? s.agents[agentId] ?? null : null
@@ -178,14 +209,12 @@ export function ChatArea() {
178
209
  })
179
210
 
180
211
  const sessionAtLoad = useAppStore.getState().sessions[requestedSessionId]
181
- if (sessionAtLoad?.active) {
182
- useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamSource: 'server', streamText: '' })
183
- }
212
+ if (sessionAtLoad?.active) startServerStreamingPlaceholder(requestedSessionId)
184
213
 
185
214
  return () => {
186
215
  cancelled = true
187
216
  }
188
- }, [loadQueuedMessages, refreshSession, sessionId, setDevServer, setMessages])
217
+ }, [loadQueuedMessages, refreshSession, sessionId, setDevServer, setMessages, startServerStreamingPlaceholder])
189
218
 
190
219
  useEffect(() => {
191
220
  if (!sessionId || messagesLoading) return
@@ -195,9 +224,7 @@ export function ChatArea() {
195
224
  void refreshSession(requestedSessionId).then(() => {
196
225
  if (cancelled || selectActiveSessionId(useAppStore.getState()) !== requestedSessionId) return
197
226
  const refreshed = useAppStore.getState().sessions[requestedSessionId]
198
- if (refreshed?.active) {
199
- useChatStore.setState({ streaming: true, streamingSessionId: requestedSessionId, streamSource: 'server', streamText: '' })
200
- }
227
+ if (refreshed?.active) startServerStreamingPlaceholder(requestedSessionId)
201
228
  }).catch((err) => console.error('Failed to refresh session:', err))
202
229
 
203
230
  void devServer(requestedSessionId, 'status').then((r) => {
@@ -213,7 +240,7 @@ export function ChatArea() {
213
240
  cancelled = true
214
241
  window.clearTimeout(timer)
215
242
  }
216
- }, [messagesLoading, refreshSession, sessionId, setDevServer])
243
+ }, [messagesLoading, refreshSession, sessionId, setDevServer, startServerStreamingPlaceholder])
217
244
 
218
245
  useEffect(() => {
219
246
  if (!sessionId || messagesLoading) return
@@ -292,19 +319,12 @@ export function ChatArea() {
292
319
  && !chatState.streaming
293
320
  && chatState.streamingSessionId !== sessionId
294
321
  ) {
295
- useChatStore.setState({
296
- streaming: true,
297
- streamingSessionId: sessionId,
298
- streamSource: 'server',
299
- streamPhase: 'thinking',
300
- streamText: '',
301
- thinkingStartTime: Date.now(),
302
- })
322
+ startServerStreamingPlaceholder(sessionId)
303
323
  }
304
324
  } catch (err) {
305
325
  console.error('Failed to refresh queue:', err)
306
326
  }
307
- }, [loadQueuedMessages, sessionId])
327
+ }, [loadQueuedMessages, sessionId, startServerStreamingPlaceholder])
308
328
 
309
329
  // Subscribe to WS messages for this session — always subscribe when session exists,
310
330
  // only enable fallback polling when actively needed
@@ -316,7 +336,7 @@ export function ChatArea() {
316
336
  useWs(
317
337
  sessionId ? 'runs' : '',
318
338
  refreshQueue,
319
- sessionId && (isServerActive || queuedCount > 0) ? 10_000 : undefined,
339
+ sessionId && (isServerActive || queuedCount > 0) ? 2_500 : undefined,
320
340
  )
321
341
 
322
342
  // Listen for stream-end signal from the server — clears streaming state
@@ -325,11 +345,12 @@ export function ChatArea() {
325
345
  if (!sessionId) return
326
346
  const state = useChatStore.getState()
327
347
  if (state.streamSource === 'server' && state.streamingSessionId === sessionId) {
328
- useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
348
+ markSessionLocallyIdle(sessionId)
349
+ useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', assistantRenderId: null, streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
329
350
  void refreshMessages()
330
351
  void refreshSession(sessionId)
331
352
  }
332
- }, [sessionId, refreshMessages, refreshSession])
353
+ }, [markSessionLocallyIdle, sessionId, refreshMessages, refreshSession])
333
354
  useWs(sessionId ? `stream-end:${sessionId}` : '', handleStreamEnd)
334
355
 
335
356
  // Keep the local typing indicator aligned with the server's active state
@@ -338,7 +359,7 @@ export function ChatArea() {
338
359
  const state = useChatStore.getState()
339
360
  if (isServerActive) {
340
361
  if (!state.streaming && !state.streamText) {
341
- useChatStore.setState({ streaming: true, streamingSessionId: sessionId, streamSource: 'server', streamText: '' })
362
+ startServerStreamingPlaceholder(sessionId)
342
363
  }
343
364
  return
344
365
  }
@@ -349,9 +370,10 @@ export function ChatArea() {
349
370
  ) {
350
371
  // Server finished — clear all streaming state and fetch final messages
351
372
  fetchMessages(sessionId).then(setMessages).catch(() => {})
352
- useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
373
+ markSessionLocallyIdle(sessionId)
374
+ useChatStore.setState({ streaming: false, streamingSessionId: null, streamSource: null, streamText: '', displayText: '', assistantRenderId: null, streamPhase: 'thinking', streamToolName: '', thinkingText: '', thinkingStartTime: 0 })
353
375
  }
354
- }, [isServerActive, sessionId, setMessages])
376
+ }, [isServerActive, markSessionLocallyIdle, sessionId, setMessages, startServerStreamingPlaceholder])
355
377
 
356
378
  // Poll browser status while session has browser tools
357
379
  const hasBrowserTool = getEnabledToolIds(session).includes('browser')
@@ -37,6 +37,41 @@ describe('MessageBubble', () => {
37
37
  assert.doesNotMatch(html, /streaming-cursor/)
38
38
  })
39
39
 
40
+ it('falls back to persisted streaming content when the live stream payload is temporarily empty', async () => {
41
+ const messageBubbleModule = await import('./message-bubble') as Record<string, unknown>
42
+ const MessageBubble = (
43
+ messageBubbleModule.MessageBubble
44
+ || (messageBubbleModule.default as { MessageBubble?: unknown } | undefined)?.MessageBubble
45
+ || (messageBubbleModule['module.exports'] as { MessageBubble?: unknown } | undefined)?.MessageBubble
46
+ ) as typeof import('./message-bubble').MessageBubble | undefined
47
+ assert.ok(MessageBubble)
48
+
49
+ const html = renderToStaticMarkup(
50
+ React.createElement(MessageBubble, {
51
+ message: {
52
+ role: 'assistant',
53
+ text: 'Recovered persisted partial text',
54
+ time: Date.now(),
55
+ kind: 'chat',
56
+ streaming: true,
57
+ },
58
+ assistantName: 'Hal2k-3',
59
+ agentName: 'Hal2k-3',
60
+ liveStream: {
61
+ active: true,
62
+ phase: 'responding',
63
+ toolName: '',
64
+ text: '',
65
+ thinking: '',
66
+ toolEvents: [],
67
+ },
68
+ }),
69
+ )
70
+
71
+ assert.match(html, /Recovered persisted partial text/)
72
+ assert.match(html, /streaming-cursor/)
73
+ })
74
+
40
75
  it('renders upload-linked screenshots inline without duplicating them at the bottom', async () => {
41
76
  const messageBubbleModule = await import('./message-bubble') as Record<string, unknown>
42
77
  const MessageBubble = (
@@ -367,12 +367,14 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
367
367
  const toolEvents = message.toolEvents ?? emptyToolEvents
368
368
  const toolEventsForMedia = useMemo(
369
369
  () => (liveStreamActive
370
- ? liveToolEvents.map((event) => ({
371
- name: event.name,
372
- input: event.input,
373
- output: event.output,
374
- error: event.status === 'error' || undefined,
375
- }))
370
+ ? (liveToolEvents.length > 0
371
+ ? liveToolEvents.map((event) => ({
372
+ name: event.name,
373
+ input: event.input,
374
+ output: event.output,
375
+ error: event.status === 'error' || undefined,
376
+ }))
377
+ : toolEvents)
376
378
  : toolEvents),
377
379
  [liveStreamActive, liveToolEvents, toolEvents],
378
380
  )
@@ -383,7 +385,15 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
383
385
  )
384
386
  const displayToolEvents = useMemo(
385
387
  () => (liveStreamActive
386
- ? liveToolEvents.filter((ev) => ev.name !== 'send_file' || ev.status === 'error')
388
+ ? (liveToolEvents.length > 0
389
+ ? liveToolEvents.filter((ev) => ev.name !== 'send_file' || ev.status === 'error')
390
+ : persistedToolEvents.map((ev, i) => ({
391
+ id: ev.toolCallId || `${message.time}-${ev.name}-${i}`,
392
+ name: ev.name,
393
+ input: ev.input,
394
+ output: ev.output,
395
+ status: ev.error ? 'error' as const : 'done' as const,
396
+ })))
387
397
  : persistedToolEvents.map((ev, i) => ({
388
398
  id: ev.toolCallId || `${message.time}-${ev.name}-${i}`,
389
399
  name: ev.name,
@@ -418,11 +428,12 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
418
428
  const effectiveThinking = !isUser
419
429
  ? (liveStreamActive ? (liveStream?.thinking?.trim() ? liveStream.thinking : undefined) : message.thinking)
420
430
  : undefined
421
- const sourceText = liveStreamActive ? (liveStream?.text || '') : message.text
431
+
432
+ const sourceText = liveStreamActive ? (liveStream?.text || message.text || '') : message.text
422
433
  const connectorDeliveryTranscript = !isUser && message.kind === 'connector-delivery'
423
434
  ? (message.source?.deliveryTranscript?.trim() || '')
424
435
  : ''
425
- const copySourceText = connectorDeliveryTranscript || (liveStreamActive ? (liveStream?.text || '') : message.text)
436
+ const copySourceText = connectorDeliveryTranscript || (liveStreamActive ? (liveStream?.text || message.text || '') : message.text)
426
437
 
427
438
  // Extract ALL media from ALL tool events for inline display after the message text.
428
439
  // Covers send_file, browser screenshots, file tool outputs — everything.
@@ -100,13 +100,6 @@ const LiveStreamBubble = memo(function LiveStreamBubble(props: LiveStreamBubbleP
100
100
  return <MessageBubble {...props} liveStream={liveStream} />
101
101
  })
102
102
 
103
- interface LastMessageMomentOverlayProps {
104
- messages: Message[]
105
- sessionId: string | null
106
- agentId?: string | null
107
- streaming: boolean
108
- children: (momentOverlay: React.ReactNode, currentMoment: { kind: string } | null) => React.ReactNode
109
- }
110
103
 
111
104
  function useLastMessageMoment(messages: Message[], sessionId: string | null, agentId: string | null | undefined, streaming: boolean) {
112
105
  type MomentType = { kind: 'heartbeat' } | { kind: 'tool'; key: string; name: string; input: string }
@@ -117,31 +110,24 @@ function useLastMessageMoment(messages: Message[], sessionId: string | null, age
117
110
  setCurrentMoment({ kind: 'heartbeat' })
118
111
  })
119
112
 
120
- const prevToolKeyRef = useRef<string | null>(null)
121
- const seededMomentSessionRef = useRef<string | null>(null)
122
-
123
- useEffect(() => {
124
- if (!sessionId) {
125
- seededMomentSessionRef.current = null
126
- prevToolKeyRef.current = null
127
- setCurrentMoment(null)
128
- return
129
- }
130
-
131
- if (seededMomentSessionRef.current === sessionId) return
132
- seededMomentSessionRef.current = sessionId
133
- prevToolKeyRef.current = getLatestAssistantToolMoment(messages)?.key || null
134
- setCurrentMoment(null)
135
- }, [messages, sessionId])
136
-
137
- useEffect(() => {
138
- if (!sessionId || seededMomentSessionRef.current !== sessionId) return
113
+ const [trackedSessionId, setTrackedSessionId] = useState<string | null>(null)
114
+ const [trackedToolKey, setTrackedToolKey] = useState<string | null>(null)
115
+
116
+ // Render-time: respond to sessionId/messages changes (avoids set-state-in-effect)
117
+ if (sessionId !== trackedSessionId) {
118
+ setTrackedSessionId(sessionId)
119
+ const initialKey = sessionId
120
+ ? getLatestAssistantToolMoment(messages)?.key || null
121
+ : null
122
+ setTrackedToolKey(initialKey)
123
+ if (currentMoment !== null) setCurrentMoment(null)
124
+ } else if (sessionId) {
139
125
  const moment = getLatestAssistantToolMoment(messages)
140
- if (!moment) return
141
- if (moment.key === prevToolKeyRef.current) return
142
- prevToolKeyRef.current = moment.key
143
- setCurrentMoment({ kind: 'tool', key: moment.key, name: moment.name, input: moment.input })
144
- }, [messages, sessionId])
126
+ if (moment && moment.key !== trackedToolKey) {
127
+ setTrackedToolKey(moment.key)
128
+ setCurrentMoment({ kind: 'tool', key: moment.key, name: moment.name, input: moment.input })
129
+ }
130
+ }
145
131
 
146
132
  const momentOverlay = useMemo(() => {
147
133
  if (streaming || !currentMoment) return null
@@ -161,13 +147,6 @@ function useLastMessageMoment(messages: Message[], sessionId: string | null, age
161
147
  return { currentMoment, momentOverlay }
162
148
  }
163
149
 
164
- interface Props {
165
- messages: Message[]
166
- streaming: boolean
167
- connectorFilter?: string | null
168
- loading?: boolean
169
- }
170
-
171
150
  interface LiveThinkingLaneProps {
172
151
  show: boolean
173
152
  assistantName?: string
@@ -195,6 +174,13 @@ const LiveThinkingLane = memo(function LiveThinkingLane({
195
174
  )
196
175
  })
197
176
 
177
+ interface Props {
178
+ messages: Message[]
179
+ streaming: boolean
180
+ connectorFilter?: string | null
181
+ loading?: boolean
182
+ }
183
+
198
184
  export function MessageList({ messages, streaming, connectorFilter = null, loading = false }: Props) {
199
185
  const scrollRef = useRef<HTMLDivElement>(null)
200
186
  const [showScrollToBottom, setShowScrollToBottom] = useState(false)
@@ -202,6 +188,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
202
188
  const snapUntilRef = useRef(0)
203
189
  const prevSessionIdRef = useRef<string | null>(null)
204
190
  const assistantRenderId = useChatStore((s) => s.assistantRenderId)
191
+ const thinkingStartTime = useChatStore((s) => s.thinkingStartTime)
205
192
  const hasLiveArtifacts = useChatStore(selectHasLiveArtifacts)
206
193
  const setMessages = useChatStore((s) => s.setMessages)
207
194
  const retryLastMessage = useChatStore((s) => s.retryLastMessage)
@@ -232,7 +219,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
232
219
  const showGatewayOverlay = isOpenClaw && gatewayStatus === 'disconnected'
233
220
 
234
221
  // Moment overlay for last assistant message (heartbeat or tool events)
235
- const { currentMoment, momentOverlay: lastMomentOverlay } = useLastMessageMoment(messages, sessionId, agent?.id, streaming)
222
+ const { momentOverlay: lastMomentOverlay } = useLastMessageMoment(messages, sessionId, agent?.id, streaming)
236
223
 
237
224
  // Unread count tracking
238
225
  const unreadRef = useRef(0)
@@ -311,13 +298,46 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
311
298
  return dedupeMessagesForDisplay(displayedMessages)
312
299
  }, [messages, showAlerts, showOk])
313
300
 
301
+ const latestPersistedStreamingMessage = useMemo(() => {
302
+ for (let i = baseDisplayedMessages.length - 1; i >= 0; i -= 1) {
303
+ const candidate = baseDisplayedMessages[i]
304
+ if (candidate.role === 'assistant' && candidate.streaming === true) {
305
+ return candidate
306
+ }
307
+ }
308
+ return null
309
+ }, [baseDisplayedMessages])
310
+
311
+ const currentRunHasCompletedAssistant = useMemo(
312
+ () => (
313
+ streaming
314
+ && thinkingStartTime > 0
315
+ && baseDisplayedMessages.some((message) => (
316
+ message.role === 'assistant'
317
+ && message.streaming !== true
318
+ && message.kind !== 'system'
319
+ && message.kind !== 'heartbeat'
320
+ && typeof message.time === 'number'
321
+ && message.time >= thinkingStartTime
322
+ ))
323
+ ),
324
+ [baseDisplayedMessages, streaming, thinkingStartTime],
325
+ )
326
+
327
+ const showLiveStreamRow = streaming
328
+ && !!assistantRenderId
329
+ && !currentRunHasCompletedAssistant
330
+ && (hasLiveArtifacts || !!latestPersistedStreamingMessage)
331
+
314
332
  const streamingAwareMessages = useMemo(() => (
315
333
  buildStreamingAwareMessageList(baseDisplayedMessages, {
316
334
  localStreaming: streaming,
317
335
  hasLiveArtifacts,
318
336
  assistantRenderId,
337
+ showLiveRow: showLiveStreamRow,
338
+ syntheticAssistant: latestPersistedStreamingMessage,
319
339
  })
320
- ), [assistantRenderId, baseDisplayedMessages, hasLiveArtifacts, streaming])
340
+ ), [assistantRenderId, baseDisplayedMessages, hasLiveArtifacts, latestPersistedStreamingMessage, showLiveStreamRow, streaming])
321
341
 
322
342
  const filteredMessages = useMemo(() => {
323
343
  let nextMessages = bookmarkFilter
@@ -637,7 +657,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
637
657
  }, [searchOpen])
638
658
 
639
659
  return (
640
- <div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden" data-testid="message-list">
660
+ <div className="relative flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden isolate" data-testid="message-list">
641
661
  <div className="shrink-0 px-4 md:px-12 lg:px-16 pt-3">
642
662
  <div className="flex flex-wrap items-center gap-2 rounded-[14px] border border-white/[0.06] bg-surface/55 px-3 py-2 backdrop-blur-sm">
643
663
  <button
@@ -867,7 +887,7 @@ export function MessageList({ messages, streaming, connectorFilter = null, loadi
867
887
  {transcriptNodes}
868
888
  <ApprovalCards agentId={agent?.id} />
869
889
  <LiveThinkingLane
870
- show={streaming && !hasLiveArtifacts && !hasVisiblePersistedStreamingMessage}
890
+ show={streaming && !showLiveStreamRow && !hasLiveArtifacts && !hasVisiblePersistedStreamingMessage}
871
891
  assistantName={assistantName}
872
892
  agentAvatarSeed={agent?.avatarSeed}
873
893
  agentAvatarUrl={agent?.avatarUrl}