@swarmclawai/swarmclaw 1.2.6 → 1.2.8

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 (112) hide show
  1. package/README.md +24 -17
  2. package/next.config.ts +1 -0
  3. package/package.json +3 -2
  4. package/scripts/easy-setup.mjs +1 -1
  5. package/scripts/postinstall.mjs +1 -1
  6. package/skills/swarmclaw.md +115 -0
  7. package/skills/tools/browser.md +131 -0
  8. package/skills/tools/execute.md +98 -0
  9. package/skills/tools/files.md +98 -0
  10. package/skills/tools/memory.md +104 -0
  11. package/skills/tools/platform.md +144 -0
  12. package/skills/tools/skills.md +83 -0
  13. package/src/app/api/chats/[id]/messages/route.ts +23 -19
  14. package/src/app/api/chats/messages-route.test.ts +105 -51
  15. package/src/app/api/mcp-servers/[id]/test/route.ts +3 -2
  16. package/src/app/api/openclaw/deploy/route.ts +2 -0
  17. package/src/app/api/setup/doctor/route.ts +4 -4
  18. package/src/components/agents/agent-chat-list.tsx +23 -1
  19. package/src/components/agents/inspector-panel.tsx +165 -48
  20. package/src/components/chat/chat-area.tsx +38 -9
  21. package/src/components/chat/message-list.tsx +33 -19
  22. package/src/components/gateways/gateway-sheet.tsx +5 -2
  23. package/src/lib/agent-execute-defaults.test.ts +24 -0
  24. package/src/lib/agent-execute-defaults.ts +62 -0
  25. package/src/lib/chat/queued-message-queue.test.ts +134 -1
  26. package/src/lib/chat/queued-message-queue.ts +77 -2
  27. package/src/lib/server/agents/agent-service.ts +5 -0
  28. package/src/lib/server/builtin-extensions.ts +1 -0
  29. package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +1 -1
  30. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +1 -0
  31. package/src/lib/server/chat-execution/chat-execution-utils.ts +2 -2
  32. package/src/lib/server/chat-execution/chat-turn-preparation.ts +79 -42
  33. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -0
  34. package/src/lib/server/chat-execution/continuation-evaluator.ts +8 -0
  35. package/src/lib/server/chat-execution/memory-mutation-tools.ts +1 -1
  36. package/src/lib/server/chat-execution/message-classifier.ts +11 -1
  37. package/src/lib/server/chat-execution/prompt-builder.test.ts +28 -0
  38. package/src/lib/server/chat-execution/prompt-builder.ts +14 -1
  39. package/src/lib/server/chat-execution/prompt-mode.test.ts +24 -0
  40. package/src/lib/server/chat-execution/prompt-mode.ts +5 -1
  41. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +6 -4
  42. package/src/lib/server/chat-execution/stream-agent-chat.ts +45 -16
  43. package/src/lib/server/chatrooms/chatroom-routing.test.ts +4 -0
  44. package/src/lib/server/connectors/discord.ts +2 -2
  45. package/src/lib/server/connectors/matrix.ts +3 -2
  46. package/src/lib/server/connectors/signal.ts +5 -4
  47. package/src/lib/server/connectors/slack.ts +10 -9
  48. package/src/lib/server/connectors/teams.ts +3 -2
  49. package/src/lib/server/connectors/telegram.ts +4 -4
  50. package/src/lib/server/connectors/whatsapp.ts +2 -2
  51. package/src/lib/server/daemon/controller.ts +7 -0
  52. package/src/lib/server/gateways/gateway-profile-service.ts +19 -1
  53. package/src/lib/server/messages/message-repository.test.ts +70 -0
  54. package/src/lib/server/messages/message-repository.ts +11 -6
  55. package/src/lib/server/openclaw/deploy.ts +32 -2
  56. package/src/lib/server/plugins-advanced.test.ts +1 -2
  57. package/src/lib/server/provider-health.ts +1 -1
  58. package/src/lib/server/runtime/process-manager.ts +13 -9
  59. package/src/lib/server/runtime/session-run-manager/queries.ts +15 -0
  60. package/src/lib/server/runtime/session-run-manager.test.ts +58 -0
  61. package/src/lib/server/sandbox/session-runtime.test.ts +18 -1
  62. package/src/lib/server/sandbox/session-runtime.ts +40 -28
  63. package/src/lib/server/session-tools/autonomy-tools.test.ts +7 -9
  64. package/src/lib/server/session-tools/context.ts +1 -1
  65. package/src/lib/server/session-tools/credential-env.ts +109 -0
  66. package/src/lib/server/session-tools/crud.ts +3 -3
  67. package/src/lib/server/session-tools/edit_file.ts +3 -2
  68. package/src/lib/server/session-tools/execute.test.ts +58 -0
  69. package/src/lib/server/session-tools/execute.ts +334 -0
  70. package/src/lib/server/session-tools/files-tool.ts +635 -0
  71. package/src/lib/server/session-tools/index.ts +14 -4
  72. package/src/lib/server/session-tools/memory-tool.ts +242 -0
  73. package/src/lib/server/session-tools/memory.ts +1 -1
  74. package/src/lib/server/session-tools/openclaw-nodes.ts +3 -2
  75. package/src/lib/server/session-tools/openclaw-workspace.ts +3 -2
  76. package/src/lib/server/session-tools/platform-tool.ts +617 -0
  77. package/src/lib/server/session-tools/session-info.ts +3 -2
  78. package/src/lib/server/session-tools/session-tools-wiring.test.ts +3 -4
  79. package/src/lib/server/session-tools/shell.ts +7 -122
  80. package/src/lib/server/session-tools/skills-tool.ts +396 -0
  81. package/src/lib/server/session-tools/web.ts +2 -2
  82. package/src/lib/server/storage-normalization.ts +2 -0
  83. package/src/lib/server/tool-aliases.ts +2 -1
  84. package/src/lib/server/tool-capability-policy-advanced.test.ts +9 -2
  85. package/src/lib/server/tool-capability-policy.test.ts +2 -1
  86. package/src/lib/server/tool-capability-policy.ts +60 -33
  87. package/src/lib/server/tool-planning.ts +11 -0
  88. package/src/lib/setup-defaults.ts +5 -0
  89. package/src/lib/tool-definitions.ts +1 -0
  90. package/src/lib/validation/schemas.test.ts +16 -0
  91. package/src/lib/validation/schemas.ts +16 -0
  92. package/src/stores/use-chat-store.test.ts +231 -0
  93. package/src/stores/use-chat-store.ts +62 -13
  94. package/src/types/agent.ts +348 -0
  95. package/src/types/app-settings.ts +175 -0
  96. package/src/types/approval.ts +27 -0
  97. package/src/types/connector.ts +187 -0
  98. package/src/types/extension.ts +386 -0
  99. package/src/types/index.ts +16 -3555
  100. package/src/types/message.ts +57 -0
  101. package/src/types/misc.ts +739 -0
  102. package/src/types/mission.ts +185 -0
  103. package/src/types/protocol.ts +422 -0
  104. package/src/types/provider.ts +52 -0
  105. package/src/types/run.ts +183 -0
  106. package/src/types/schedule.ts +59 -0
  107. package/src/types/session.ts +265 -0
  108. package/src/types/skill.ts +157 -0
  109. package/src/types/task.ts +140 -0
  110. package/src/types/working-state.ts +211 -0
  111. package/src/views/settings/section-heartbeat.tsx +2 -2
  112. package/src/lib/server/session-tools/sandbox.ts +0 -281
@@ -0,0 +1,144 @@
1
+ # Platform Tool
2
+
3
+ Interact with the SwarmClaw platform: manage tasks, communicate with humans and other agents, access projects, and participate in chatrooms.
4
+
5
+ ## Action Groups
6
+
7
+ ### Tasks
8
+
9
+ | Action | Description | Key Parameters |
10
+ |--------|-------------|----------------|
11
+ | `tasks.create` | Create a new task | `title`, `description`, `priority` |
12
+ | `tasks.update` | Update task fields | `id`, fields to update |
13
+ | `tasks.list` | List tasks with filters | `status`, `assignee`, `priority` |
14
+ | `tasks.get` | Get task details | `id` |
15
+ | `tasks.complete` | Mark task as done | `id`, `result` (optional summary) |
16
+
17
+ #### Create a task
18
+
19
+ ```json
20
+ {
21
+ "action": "tasks.create",
22
+ "title": "Review PR #55",
23
+ "description": "Check for type safety issues and test coverage",
24
+ "priority": "high"
25
+ }
26
+ ```
27
+
28
+ #### List open tasks
29
+
30
+ ```json
31
+ { "action": "tasks.list", "status": "open" }
32
+ ```
33
+
34
+ ### Communication
35
+
36
+ | Action | Description | Key Parameters |
37
+ |--------|-------------|----------------|
38
+ | `communicate.ask_human` | Block and wait for human input | `question`, `context` |
39
+ | `communicate.send_message` | Send to a connector channel | `connector`, `channel`, `message` |
40
+ | `communicate.delegate` | Route work to another agent | `agentId`, `message` |
41
+ | `communicate.spawn` | Create a subagent for parallel work | `agentId`, `message`, `mode` |
42
+
43
+ #### Ask human (blocks execution)
44
+
45
+ ```json
46
+ {
47
+ "action": "communicate.ask_human",
48
+ "question": "Should I proceed with the database migration?",
49
+ "context": "This will add 3 new columns to the users table and backfill existing rows."
50
+ }
51
+ ```
52
+
53
+ **Important:** `ask_human` blocks the agent loop until the human responds. Use it when you genuinely need input before continuing. Do not use it for status updates (use `send_message` instead).
54
+
55
+ #### Send a message to Discord/Slack/Telegram
56
+
57
+ ```json
58
+ {
59
+ "action": "communicate.send_message",
60
+ "connector": "discord",
61
+ "channel": "#general",
62
+ "message": "Deployment complete. All tests passing."
63
+ }
64
+ ```
65
+
66
+ #### Delegate to another agent
67
+
68
+ ```json
69
+ {
70
+ "action": "communicate.delegate",
71
+ "agentId": "agent_research",
72
+ "message": "Find the top 5 competitors in the AI coding assistant space and summarize their pricing."
73
+ }
74
+ ```
75
+
76
+ #### Spawn a subagent
77
+
78
+ ```json
79
+ {
80
+ "action": "communicate.spawn",
81
+ "agentId": "agent_coder",
82
+ "message": "Implement the dark mode toggle component",
83
+ "mode": "run"
84
+ }
85
+ ```
86
+
87
+ Modes:
88
+ - `run` -- fire and forget, subagent runs independently
89
+ - `session` -- creates a persistent session you can check on later
90
+
91
+ ### Projects
92
+
93
+ | Action | Description | Key Parameters |
94
+ |--------|-------------|----------------|
95
+ | `projects.list` | List all projects | (none) |
96
+ | `projects.get` | Get project details | `id` |
97
+
98
+ ```json
99
+ { "action": "projects.list" }
100
+ ```
101
+
102
+ ### Chatrooms
103
+
104
+ | Action | Description | Key Parameters |
105
+ |--------|-------------|----------------|
106
+ | `chatrooms.send` | Send a message to a chatroom | `chatroomId`, `message` |
107
+ | `chatrooms.list` | List available chatrooms | (none) |
108
+ | `chatrooms.history` | Get recent messages | `chatroomId`, `limit` |
109
+
110
+ ```json
111
+ {
112
+ "action": "chatrooms.send",
113
+ "chatroomId": "room_design",
114
+ "message": "The mockups are ready for review."
115
+ }
116
+ ```
117
+
118
+ ### Agents
119
+
120
+ | Action | Description | Key Parameters |
121
+ |--------|-------------|----------------|
122
+ | `agents.list` | List all agents | (none) |
123
+ | `agents.get` | Get agent details | `id` |
124
+
125
+ ```json
126
+ { "action": "agents.list" }
127
+ ```
128
+
129
+ ## Communication Decision Guide
130
+
131
+ | Situation | Action |
132
+ |-----------|--------|
133
+ | Need human approval before proceeding | `communicate.ask_human` |
134
+ | Sharing a status update with the team | `communicate.send_message` |
135
+ | Task is outside your expertise | `communicate.delegate` |
136
+ | Task can run in parallel with your work | `communicate.spawn` |
137
+ | Collaborating with agents in a shared space | `chatrooms.send` |
138
+
139
+ ## Tips
140
+
141
+ - Use `ask_human` sparingly. Only block when you truly cannot proceed without input.
142
+ - When delegating, provide enough context that the target agent can work independently.
143
+ - Check `tasks.list` before creating duplicates.
144
+ - Use `agents.list` to discover available agents and their capabilities before delegating.
@@ -0,0 +1,83 @@
1
+ # Skills Tool
2
+
3
+ Discover and load skill files that teach you how to use tools, APIs, and workflows.
4
+
5
+ ## Actions
6
+
7
+ | Action | Description | Key Parameters |
8
+ |--------|-------------|----------------|
9
+ | `list` | Browse all available skills | (none) |
10
+ | `read` | Load a skill by name | `name` |
11
+ | `search` | Find skills by keyword | `query` |
12
+
13
+ ## List Available Skills
14
+
15
+ ```json
16
+ { "action": "list" }
17
+ ```
18
+
19
+ Returns all discoverable skill files with names and short descriptions.
20
+
21
+ ## Read a Skill
22
+
23
+ ```json
24
+ { "action": "read", "name": "tools/files" }
25
+ ```
26
+
27
+ Loads the full content of a skill file. Skill names use path-style notation:
28
+
29
+ - `tools/files` -- the files tool documentation
30
+ - `tools/memory` -- the memory tool documentation
31
+ - `swarmclaw` -- the platform overview skill
32
+ - `github` -- GitHub CLI operations
33
+
34
+ Name matching is flexible: partial matches work if unambiguous.
35
+
36
+ ## Search Skills
37
+
38
+ ```json
39
+ { "action": "search", "query": "browser screenshot" }
40
+ ```
41
+
42
+ Searches skill file names, descriptions, and content for keyword matches. Returns ranked results.
43
+
44
+ ## Skill File Locations
45
+
46
+ | Directory | Source | Description |
47
+ |-----------|--------|-------------|
48
+ | `skills/` | Built-in | Shipped with SwarmClaw, checked into the repo |
49
+ | `data/skills/` | User-created | Added at runtime, not version-controlled |
50
+
51
+ ## Skill File Format
52
+
53
+ Skills are markdown files (`.md`). They can be:
54
+
55
+ - **Flat files**: `skills/swarmclaw.md`
56
+ - **Directory-based**: `skills/github/SKILL.md`
57
+ - **Nested**: `skills/tools/files.md`
58
+
59
+ Optional YAML frontmatter can declare metadata:
60
+
61
+ ```yaml
62
+ ---
63
+ name: my-skill
64
+ description: What this skill teaches
65
+ metadata:
66
+ openclaw:
67
+ requires:
68
+ bins: ["gh"]
69
+ ---
70
+ ```
71
+
72
+ ## When to Use Skills
73
+
74
+ - **Before using an unfamiliar tool**: Load its skill to understand parameters, patterns, and best practices.
75
+ - **When stuck on a task**: Search for skills related to what you're trying to accomplish.
76
+ - **At the start of a session**: List skills to understand what documentation is available.
77
+
78
+ ## Tips
79
+
80
+ - Skills are read-only reference material. They don't execute anything.
81
+ - Load the relevant tool skill before attempting complex operations with that tool.
82
+ - If a skill doesn't exist for what you need, you can still use the tool directly -- skills are guides, not gates.
83
+ - User-created skills in `data/skills/` take the same format as built-in ones.
@@ -3,7 +3,13 @@ import { notFound } from '@/lib/server/collection-helpers'
3
3
  import { materializeStreamingAssistantArtifacts } from '@/lib/chat/chat-streaming-state'
4
4
  import { appendSessionNote } from '@/lib/server/session-note'
5
5
  import { getSessionRunState } from '@/lib/server/runtime/session-run-manager'
6
- import { getSession, saveSession } from '@/lib/server/sessions/session-repository'
6
+ import { getSession } from '@/lib/server/sessions/session-repository'
7
+ import {
8
+ appendMessage,
9
+ getMessages,
10
+ replaceAllMessages,
11
+ replaceMessageAt,
12
+ } from '@/lib/server/messages/message-repository'
7
13
  import type { Message } from '@/types'
8
14
  import { safeParseBody } from '@/lib/server/safe-parse-body'
9
15
 
@@ -11,22 +17,21 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
11
17
  const { id } = await params
12
18
  const session = getSession(id)
13
19
  if (!session) return notFound()
14
- session.messages = Array.isArray(session.messages) ? session.messages : []
15
20
 
16
21
  // Use persisted fields plus the run ledger. Process-local execution state is
17
22
  // intentionally excluded here so stale registry entries do not block cleanup.
18
23
  const sessionClaimsActive = session.active === true
19
24
  || (typeof session.currentRunId === 'string' && session.currentRunId.trim().length > 0)
20
25
  || !!getSessionRunState(id).runningRunId
21
- if (!sessionClaimsActive && materializeStreamingAssistantArtifacts(session.messages)) {
22
- saveSession(id, session)
26
+ const allMessages = getMessages(id)
27
+ if (!sessionClaimsActive && materializeStreamingAssistantArtifacts(allMessages)) {
28
+ replaceAllMessages(id, allMessages)
23
29
  }
24
30
 
25
31
  const url = new URL(req.url)
26
32
  const limitParam = url.searchParams.get('limit')
27
33
  const beforeParam = url.searchParams.get('before')
28
34
 
29
- const allMessages = Array.isArray(session.messages) ? session.messages : []
30
35
  const total = allMessages.length
31
36
 
32
37
  // If no limit param, return all messages (backward compatible)
@@ -64,13 +69,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
64
69
  const session = getSession(id)
65
70
  if (!session) return notFound()
66
71
 
67
- session.messages.push({
72
+ appendMessage(id, {
68
73
  role: 'user',
69
74
  text: '',
70
75
  kind: 'context-clear',
71
76
  time: Date.now(),
72
77
  })
73
- saveSession(id, session)
74
78
  return NextResponse.json({ ok: true })
75
79
  }
76
80
 
@@ -96,37 +100,37 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
96
100
  const { id } = await params
97
101
  const { data: body, error } = await safeParseBody<{ messageIndex: number; bookmarked: boolean }>(req)
98
102
  if (error) return error
99
- const session = getSession(id)
100
- if (!session) return notFound()
103
+ if (!getSession(id)) return notFound()
101
104
 
102
105
  const { messageIndex, bookmarked } = body
103
- if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
106
+ const messages = getMessages(id)
107
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= messages.length) {
104
108
  return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
105
109
  }
106
110
 
107
- session.messages[messageIndex].bookmarked = bookmarked
108
- saveSession(id, session)
109
- return NextResponse.json(session.messages[messageIndex])
111
+ const updated = { ...messages[messageIndex], bookmarked }
112
+ replaceMessageAt(id, messageIndex, updated)
113
+ return NextResponse.json(updated)
110
114
  }
111
115
 
112
116
  export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
113
117
  const { id } = await params
114
118
  const { data: body, error } = await safeParseBody<{ messageIndex: number }>(req)
115
119
  if (error) return error
116
- const session = getSession(id)
117
- if (!session) return notFound()
120
+ if (!getSession(id)) return notFound()
118
121
 
119
122
  const { messageIndex } = body
120
- if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= session.messages.length) {
123
+ const messages = getMessages(id)
124
+ if (typeof messageIndex !== 'number' || messageIndex < 0 || messageIndex >= messages.length) {
121
125
  return NextResponse.json({ error: 'Invalid message index' }, { status: 400 })
122
126
  }
123
127
 
124
128
  // Only allow deleting context-clear markers (safety guard)
125
- if (session.messages[messageIndex].kind !== 'context-clear') {
129
+ if (messages[messageIndex].kind !== 'context-clear') {
126
130
  return NextResponse.json({ error: 'Only context-clear markers can be removed' }, { status: 400 })
127
131
  }
128
132
 
129
- session.messages.splice(messageIndex, 1)
130
- saveSession(id, session)
133
+ messages.splice(messageIndex, 1)
134
+ replaceAllMessages(id, messages)
131
135
  return NextResponse.json({ ok: true })
132
136
  }
@@ -3,65 +3,119 @@ import test from 'node:test'
3
3
 
4
4
  import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
5
5
 
6
- test('chat messages route materializes stale streaming artifacts even if runtime memory is stale', () => {
6
+ test('messages route serves and mutates repo-backed transcript history', () => {
7
7
  const output = runWithTempDataDir<{
8
- status: number
9
- returnedStreaming: boolean | null
10
- returnedText: string | null
11
- persistedStreaming: boolean | null
12
- persistedText: string | null
13
- }>(`
14
- const storageMod = await import('./src/lib/server/storage')
15
- const routeMod = await import('./src/app/api/chats/[id]/messages/route')
16
- const runtimeStateMod = await import('./src/lib/server/runtime/runtime-state')
17
- const storage = storageMod.default || storageMod
18
- const route = routeMod.default || routeMod
19
- const runtimeState = runtimeStateMod.default || runtimeStateMod
8
+ fullCount: number
9
+ paginatedTexts: string[]
10
+ paginatedStartIndex: number
11
+ paginatedTotal: number
12
+ bookmarkPersisted: boolean
13
+ contextClearCountAfterPost: number
14
+ finalKinds: Array<string | null>
15
+ finalBookmarked: boolean
16
+ blobMessageCount: number
17
+ }>(`
18
+ const storageMod = await import('./src/lib/server/storage')
19
+ const repoMod = await import('@/lib/server/messages/message-repository')
20
+ const routeMod = await import('./src/app/api/chats/[id]/messages/route')
21
+ const storage = storageMod.default || storageMod
22
+ const repo = repoMod.default || repoMod
23
+ const route = routeMod.default || routeMod
20
24
 
21
- storage.upsertStoredItem('sessions', 'session-stale', {
22
- id: 'session-stale',
23
- name: 'Stale session',
24
- provider: 'ollama',
25
- model: 'test-model',
26
- createdAt: 1,
27
- updatedAt: 1,
28
- active: false,
29
- currentRunId: null,
30
- messages: [
31
- { role: 'user', text: 'hello', time: 1 },
32
- {
33
- role: 'assistant',
34
- text: 'partial reply',
35
- time: 2,
36
- streaming: true,
37
- toolEvents: [{ name: 'http_request', input: '{}', output: '{"ok":true}' }],
38
- },
39
- ],
25
+ const now = Date.now()
26
+ storage.saveSessions({
27
+ sess_1: {
28
+ id: 'sess_1',
29
+ name: 'Repo-backed session',
30
+ cwd: process.env.WORKSPACE_DIR,
31
+ user: 'tester',
32
+ provider: 'openai',
33
+ model: 'gpt-5',
34
+ claudeSessionId: null,
35
+ codexThreadId: null,
36
+ opencodeSessionId: null,
37
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
38
+ messages: [],
39
+ createdAt: now,
40
+ lastActiveAt: now,
41
+ },
40
42
  })
41
43
 
42
- runtimeState.registerActiveSessionProcess('session-stale', { kill() {} })
44
+ repo.appendMessage('sess_1', { role: 'user', text: 'hello', time: now })
45
+ repo.appendMessage('sess_1', { role: 'user', text: '', kind: 'context-clear', time: now + 1 })
46
+ repo.appendMessage('sess_1', { role: 'assistant', text: 'welcome back', time: now + 2 })
47
+ storage.patchSession('sess_1', (current) => {
48
+ if (!current) return null
49
+ current.messages = [{ role: 'assistant', text: 'stale blob only', time: now - 10 }]
50
+ return current
51
+ })
52
+
53
+ const fullResponse = await route.GET(
54
+ new Request('http://local/api/chats/sess_1/messages'),
55
+ { params: Promise.resolve({ id: 'sess_1' }) },
56
+ )
57
+ const fullMessages = await fullResponse.json()
43
58
 
44
- const response = await route.GET(
45
- new Request('http://local/api/chats/session-stale/messages'),
46
- { params: Promise.resolve({ id: 'session-stale' }) },
59
+ const paginatedResponse = await route.GET(
60
+ new Request('http://local/api/chats/sess_1/messages?limit=2'),
61
+ { params: Promise.resolve({ id: 'sess_1' }) },
47
62
  )
48
- const payload = await response.json()
49
- const persisted = storage.loadSession('session-stale')
50
- const returned = Array.isArray(payload) ? payload[payload.length - 1] : null
51
- const saved = Array.isArray(persisted?.messages) ? persisted.messages[persisted.messages.length - 1] : null
63
+ const paginated = await paginatedResponse.json()
64
+
65
+ const bookmarkResponse = await route.PUT(
66
+ new Request('http://local/api/chats/sess_1/messages', {
67
+ method: 'PUT',
68
+ headers: { 'content-type': 'application/json' },
69
+ body: JSON.stringify({ messageIndex: 2, bookmarked: true }),
70
+ }),
71
+ { params: Promise.resolve({ id: 'sess_1' }) },
72
+ )
73
+ const bookmarked = await bookmarkResponse.json()
74
+
75
+ await route.POST(
76
+ new Request('http://local/api/chats/sess_1/messages', {
77
+ method: 'POST',
78
+ headers: { 'content-type': 'application/json' },
79
+ body: JSON.stringify({ kind: 'context-clear' }),
80
+ }),
81
+ { params: Promise.resolve({ id: 'sess_1' }) },
82
+ )
83
+
84
+ const afterPost = repo.getMessages('sess_1')
85
+ const contextClearCountAfterPost = afterPost.filter((message) => message.kind === 'context-clear').length
86
+
87
+ await route.DELETE(
88
+ new Request('http://local/api/chats/sess_1/messages', {
89
+ method: 'DELETE',
90
+ headers: { 'content-type': 'application/json' },
91
+ body: JSON.stringify({ messageIndex: 1 }),
92
+ }),
93
+ { params: Promise.resolve({ id: 'sess_1' }) },
94
+ )
95
+
96
+ const finalMessages = repo.getMessages('sess_1')
97
+ const sessions = storage.loadSessions()
52
98
 
53
99
  console.log(JSON.stringify({
54
- status: response.status,
55
- returnedStreaming: returned?.streaming === true,
56
- returnedText: returned?.text || null,
57
- persistedStreaming: saved?.streaming === true,
58
- persistedText: saved?.text || null,
100
+ fullCount: fullMessages.length,
101
+ paginatedTexts: paginated.messages.map((message) => message.text),
102
+ paginatedStartIndex: paginated.startIndex,
103
+ paginatedTotal: paginated.total,
104
+ bookmarkPersisted: bookmarked.bookmarked === true,
105
+ contextClearCountAfterPost,
106
+ finalKinds: finalMessages.map((message) => message.kind || null),
107
+ finalBookmarked: finalMessages[1]?.bookmarked === true,
108
+ blobMessageCount: Array.isArray(sessions.sess_1.messages) ? sessions.sess_1.messages.length : -1,
59
109
  }))
60
- `, { prefix: 'swarmclaw-chat-messages-route-' })
110
+ `, { prefix: 'swarmclaw-messages-route-' })
61
111
 
62
- assert.equal(output.status, 200)
63
- assert.equal(output.returnedStreaming, false)
64
- assert.equal(output.persistedStreaming, false)
65
- assert.equal(output.returnedText, 'partial reply')
66
- assert.equal(output.persistedText, 'partial reply')
112
+ assert.equal(output.fullCount, 3)
113
+ assert.deepEqual(output.paginatedTexts, ['', 'welcome back'])
114
+ assert.equal(output.paginatedStartIndex, 1)
115
+ assert.equal(output.paginatedTotal, 3)
116
+ assert.equal(output.bookmarkPersisted, true)
117
+ assert.equal(output.contextClearCountAfterPost, 2)
118
+ assert.deepEqual(output.finalKinds, [null, null, 'context-clear'])
119
+ assert.equal(output.finalBookmarked, true)
120
+ assert.equal(output.blobMessageCount, 1)
67
121
  })
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
2
2
  import { loadMcpServers } from '@/lib/server/storage'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { connectMcpServer, mcpToolsToLangChain, disconnectMcpServer } from '@/lib/server/mcp-client'
5
+ import { errorMessage } from '@/lib/shared-utils'
5
6
 
6
7
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
7
8
  const { id } = await params
@@ -15,9 +16,9 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
15
16
  const toolNames = tools.map((t: any) => t.name)
16
17
  await disconnectMcpServer(client, transport)
17
18
  return NextResponse.json({ ok: true, tools: toolNames })
18
- } catch (err: any) {
19
+ } catch (err: unknown) {
19
20
  return NextResponse.json(
20
- { ok: false, error: err.message || 'Connection failed' },
21
+ { ok: false, error: errorMessage(err) || 'Connection failed' },
21
22
  { status: 500 }
22
23
  )
23
24
  }
@@ -129,6 +129,7 @@ export async function POST(req: Request) {
129
129
  locals: result.locals,
130
130
  localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
131
131
  token: result.token,
132
+ gatewayProfileId: result.gatewayProfileId,
132
133
  })
133
134
  }
134
135
 
@@ -156,6 +157,7 @@ export async function POST(req: Request) {
156
157
  locals: result.locals,
157
158
  localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
158
159
  token: result.token,
160
+ gatewayProfileId: result.gatewayProfileId,
159
161
  })
160
162
  }
161
163
 
@@ -217,14 +217,14 @@ export async function GET(req: Request) {
217
217
  pushCheck(
218
218
  checks,
219
219
  'docker',
220
- 'Docker (sandbox runtime)',
220
+ 'Docker (browser sandbox runtime)',
221
221
  docker.available ? 'pass' : 'warn',
222
222
  docker.available
223
- ? `Docker ${docker.version || ''} is available for container sandbox execution.`.trim()
224
- : 'Docker is not available. SwarmClaw will fall back to host execution until Docker Desktop is installed.',
223
+ ? `Docker ${docker.version || ''} is available for sandbox browser execution.`.trim()
224
+ : 'Docker is not available. SwarmClaw will use the host Playwright runtime unless Docker Desktop is installed.',
225
225
  )
226
226
  if (!docker.available) {
227
- actions.push('Install Docker Desktop if you want shell, browser, and code execution to stay inside containers by default.')
227
+ actions.push('Install Docker Desktop if you want Playwright browser sessions to use the sandbox browser runtime.')
228
228
  }
229
229
 
230
230
  const gitRootCheck = run('git', ['rev-parse', '--is-inside-work-tree'], 4_000)
@@ -193,9 +193,11 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
193
193
  // eslint-disable-next-line react-hooks/exhaustive-deps
194
194
  }, [filteredAgents.map((a) => a.id).join(',')])
195
195
 
196
+ const [enableAgentTarget, setEnableAgentTarget] = useState<Agent | null>(null)
197
+
196
198
  const handleSelect = async (agent: Agent) => {
197
199
  if (agent.disabled === true && !agent.threadSessionId) {
198
- toast.error(`${agent.name} is disabled. Re-enable it to start a new chat.`)
200
+ setEnableAgentTarget(agent)
199
201
  return
200
202
  }
201
203
  navigateTo('agents', agent.id)
@@ -547,6 +549,26 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
547
549
  onConfirm={() => { setConfirmBulkDelete(false); handleBulkDelete() }}
548
550
  onCancel={() => setConfirmBulkDelete(false)}
549
551
  />
552
+ <ConfirmDialog
553
+ open={!!enableAgentTarget}
554
+ title={`Enable ${enableAgentTarget?.name ?? 'Agent'}?`}
555
+ message={`${enableAgentTarget?.name ?? 'This agent'} is currently disabled. Enable it to start a new chat.`}
556
+ confirmLabel="Enable"
557
+ onConfirm={async () => {
558
+ if (!enableAgentTarget) return
559
+ try {
560
+ await api('PUT', `/agents/${enableAgentTarget.id}`, { disabled: false })
561
+ await loadAgents()
562
+ const agent = enableAgentTarget
563
+ setEnableAgentTarget(null)
564
+ handleSelect(agent)
565
+ } catch {
566
+ toast.error('Failed to enable agent')
567
+ setEnableAgentTarget(null)
568
+ }
569
+ }}
570
+ onCancel={() => setEnableAgentTarget(null)}
571
+ />
550
572
  </div>
551
573
  )
552
574
  }