@swarmclawai/swarmclaw 1.5.69 → 1.5.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/package.json +3 -3
- package/scripts/run-next-build.mjs +29 -0
- package/src/app/api/providers/[id]/route.test.ts +20 -0
- package/src/components/layout/dashboard-shell.tsx +15 -1
- package/src/components/layout/sidebar-rail.tsx +4 -4
- package/src/features/providers/queries.ts +0 -1
- package/src/lib/app/view-constants.test.ts +21 -0
- package/src/lib/app/view-constants.ts +25 -0
- package/src/lib/providers/openai.test.ts +54 -0
- package/src/lib/providers/openai.ts +11 -7
- package/src/lib/server/session-tools/web-browser-config.test.ts +5 -0
- package/src/lib/server/session-tools/web-utils.ts +3 -0
package/README.md
CHANGED
|
@@ -399,6 +399,25 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.5.71 Highlights
|
|
403
|
+
|
|
404
|
+
Fast-follow release for [#60](https://github.com/swarmclawai/swarmclaw/pull/60) by [@borislavnnikolov](https://github.com/borislavnnikolov). Thanks Borislav!
|
|
405
|
+
|
|
406
|
+
- **Browser MCP works from standalone builds again.** The Next standalone output now includes the Playwright MCP runtime packages required by packaged SwarmClaw builds.
|
|
407
|
+
- **Host browser launches use cached Chromium.** Local Playwright MCP startup now selects Chromium explicitly instead of depending on a system Chrome install.
|
|
408
|
+
- **Standalone repair is more robust.** The build repair step now fills partially traced Playwright MCP package directories, and the packaging and browser startup paths are covered by regression tests.
|
|
409
|
+
|
|
410
|
+
### v1.5.70 Highlights
|
|
411
|
+
|
|
412
|
+
Fast-follow release for [#56](https://github.com/swarmclawai/swarmclaw/pull/56) by [@latentwill](https://github.com/latentwill). Thanks latentwill!
|
|
413
|
+
|
|
414
|
+
Also includes fixes for [#57](https://github.com/swarmclawai/swarmclaw/issues/57) and [#58](https://github.com/swarmclawai/swarmclaw/issues/58) reported by [@zantak](https://github.com/zantak). Thanks zantak!
|
|
415
|
+
|
|
416
|
+
- **Builtin provider saves work again.** Saving a builtin provider no longer sends the strict-schema rejected `type` field, and the provider update route is now covered by the runtime test script.
|
|
417
|
+
- **Knowledge sources appear on direct visits.** Panel-backed routes such as Knowledge now auto-open their source/sidebar panel on desktop route changes, while mobile keeps the drawer closed by default.
|
|
418
|
+
- **Reasoning content stays out of the reply body.** OpenAI-compatible `reasoning_content` and `reasoning` stream deltas now flow into the existing collapsed Thinking panel instead of being appended before the visible answer.
|
|
419
|
+
- **macOS install guidance remains explicit.** Ad-hoc signed macOS desktop builds still document the quarantine workaround until Developer ID signing and notarization are available. Thanks [@yagudaev](https://github.com/yagudaev) for confirming the current workaround on Apple Silicon.
|
|
420
|
+
|
|
402
421
|
### v1.5.69 Highlights
|
|
403
422
|
|
|
404
423
|
Fast-follow release for [#55](https://github.com/swarmclawai/swarmclaw/pull/55) by [@borislavnnikolov](https://github.com/borislavnnikolov). Thanks Borislav!
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.71",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"cli": "node ./bin/swarmclaw.js",
|
|
87
87
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
88
88
|
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
|
|
89
|
-
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
90
|
-
"test:runtime": "tsx --test src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
|
|
89
|
+
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
90
|
+
"test:runtime": "tsx --test src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/app/view-constants.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
|
|
91
91
|
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
|
|
92
92
|
"test:e2e": "tsx .workbench/browser-e2e/run.ts",
|
|
93
93
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -35,6 +35,11 @@ export const REQUIRED_NEXT_METADATA_FILES = [
|
|
|
35
35
|
'get-metadata-route.js',
|
|
36
36
|
'is-metadata-route.js',
|
|
37
37
|
]
|
|
38
|
+
export const REQUIRED_STANDALONE_BROWSER_PACKAGES = [
|
|
39
|
+
'@playwright/mcp',
|
|
40
|
+
'playwright',
|
|
41
|
+
'playwright-core',
|
|
42
|
+
]
|
|
38
43
|
|
|
39
44
|
function parsePositiveInteger(value) {
|
|
40
45
|
const parsed = Number.parseInt(String(value ?? '').trim(), 10)
|
|
@@ -187,6 +192,27 @@ export function repairStandaloneCssTreeData(cwd = process.cwd()) {
|
|
|
187
192
|
return repaired
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
export function repairStandaloneBrowserMcpRuntime(cwd = process.cwd()) {
|
|
196
|
+
const standaloneDir = path.join(cwd, '.next', 'standalone')
|
|
197
|
+
if (!fs.existsSync(standaloneDir)) return false
|
|
198
|
+
|
|
199
|
+
let repaired = false
|
|
200
|
+
const standaloneNodeModules = path.join(standaloneDir, 'node_modules')
|
|
201
|
+
for (const packageName of REQUIRED_STANDALONE_BROWSER_PACKAGES) {
|
|
202
|
+
const sourceDir = path.join(cwd, 'node_modules', ...packageName.split('/'))
|
|
203
|
+
const targetDir = path.join(standaloneNodeModules, ...packageName.split('/'))
|
|
204
|
+
if (!fs.existsSync(sourceDir)) {
|
|
205
|
+
throw new Error(`Missing required browser MCP runtime package under ${sourceDir}.`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true })
|
|
209
|
+
fs.cpSync(sourceDir, targetDir, { recursive: true, force: true })
|
|
210
|
+
repaired = true
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return repaired
|
|
214
|
+
}
|
|
215
|
+
|
|
190
216
|
export function repairStandaloneNextMetadata(cwd = process.cwd()) {
|
|
191
217
|
const standaloneDir = path.join(cwd, '.next', 'standalone')
|
|
192
218
|
if (!fs.existsSync(standaloneDir)) return false
|
|
@@ -248,6 +274,9 @@ function main() {
|
|
|
248
274
|
if (result.status === 0 && repairStandaloneCssTreeData(process.cwd())) {
|
|
249
275
|
console.error('Copied css-tree/data/ into standalone build output.')
|
|
250
276
|
}
|
|
277
|
+
if (result.status === 0 && repairStandaloneBrowserMcpRuntime(process.cwd())) {
|
|
278
|
+
console.error('Copied Playwright MCP runtime packages into standalone build output.')
|
|
279
|
+
}
|
|
251
280
|
process.exit(result.status)
|
|
252
281
|
}
|
|
253
282
|
if (result.signal) {
|
|
@@ -47,3 +47,23 @@ test('provider route upserts builtin override records for enablement changes', (
|
|
|
47
47
|
assert.equal(output.responsePayload.type, 'builtin')
|
|
48
48
|
assert.equal(output.responsePayload.isEnabled, false)
|
|
49
49
|
})
|
|
50
|
+
|
|
51
|
+
test('provider route rejects unknown fields per ProviderUpdateSchema.strict()', () => {
|
|
52
|
+
const output = runWithTempDataDir<{ status: number }>(`
|
|
53
|
+
const routeMod = await import('./src/app/api/providers/[id]/route')
|
|
54
|
+
const route = routeMod.default || routeMod
|
|
55
|
+
|
|
56
|
+
const response = await route.PUT(
|
|
57
|
+
new Request('http://local/api/providers/openai', {
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
headers: { 'content-type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ type: 'builtin', isEnabled: true }),
|
|
61
|
+
}),
|
|
62
|
+
{ params: Promise.resolve({ id: 'openai' }) },
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
console.log(JSON.stringify({ status: response.status }))
|
|
66
|
+
`, { prefix: 'swarmclaw-provider-route-strict-test-' })
|
|
67
|
+
|
|
68
|
+
assert.equal(output.status, 400)
|
|
69
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useCallback } from 'react'
|
|
3
|
+
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
4
4
|
import { useRouter, usePathname } from 'next/navigation'
|
|
5
5
|
import { initAudioContext } from '@/lib/tts'
|
|
6
6
|
import { clearStoredAccessKey } from '@/lib/app/api-client'
|
|
@@ -13,6 +13,7 @@ import { useSwipe } from '@/hooks/use-swipe'
|
|
|
13
13
|
import { useWs } from '@/hooks/use-ws'
|
|
14
14
|
import { api } from '@/lib/app/api-client'
|
|
15
15
|
import { pathToView, useNavigate } from '@/lib/app/navigation'
|
|
16
|
+
import { shouldAutoOpenPanelSidebar } from '@/lib/app/view-constants'
|
|
16
17
|
|
|
17
18
|
import { FullScreenLoader } from '@/components/ui/full-screen-loader'
|
|
18
19
|
import { SidebarRail } from '@/components/layout/sidebar-rail'
|
|
@@ -45,6 +46,7 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
45
46
|
|
|
46
47
|
const [bootTimedOut, setBootTimedOut] = useState(false)
|
|
47
48
|
const [profileSheetOpen, setProfileSheetOpen] = useState(false)
|
|
49
|
+
const lastAutoOpenedPanelPathRef = useRef<string | null>(null)
|
|
48
50
|
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
|
|
49
51
|
const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
|
|
50
52
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
@@ -165,6 +167,18 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
|
|
|
165
167
|
}
|
|
166
168
|
}, [pathname, isViewEnabled, router, isAuthPage])
|
|
167
169
|
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (isAuthPage) return
|
|
172
|
+
const currentView = pathToView(pathname)
|
|
173
|
+
if (!shouldAutoOpenPanelSidebar(currentView, isDesktop)) {
|
|
174
|
+
lastAutoOpenedPanelPathRef.current = null
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
if (lastAutoOpenedPanelPathRef.current === pathname) return
|
|
178
|
+
lastAutoOpenedPanelPathRef.current = pathname
|
|
179
|
+
setSidebarOpen(true)
|
|
180
|
+
}, [isAuthPage, isDesktop, pathname, setSidebarOpen])
|
|
181
|
+
|
|
168
182
|
// Extension sidebar items
|
|
169
183
|
const refreshExtensionState = useCallback(() => {
|
|
170
184
|
void loadExtensions()
|
|
@@ -9,7 +9,7 @@ import { DaemonIndicator } from '@/components/layout/daemon-indicator'
|
|
|
9
9
|
import { NotificationCenter } from '@/components/shared/notification-center'
|
|
10
10
|
import { NavItem, RailTooltip } from '@/components/layout/nav-item'
|
|
11
11
|
import { useWs } from '@/hooks/use-ws'
|
|
12
|
-
import { FULL_WIDTH_VIEWS } from '@/lib/app/view-constants'
|
|
12
|
+
import { FULL_WIDTH_VIEWS, isPanelSidebarView } from '@/lib/app/view-constants'
|
|
13
13
|
import { pathToView, useNavigate } from '@/lib/app/navigation'
|
|
14
14
|
import { safeStorageGet, safeStorageSet } from '@/lib/app/safe-storage'
|
|
15
15
|
import type { AppView } from '@/types'
|
|
@@ -80,9 +80,9 @@ export function SidebarRail({
|
|
|
80
80
|
setSidebarOpen(false)
|
|
81
81
|
return
|
|
82
82
|
}
|
|
83
|
-
if (
|
|
84
|
-
setSidebarOpen(
|
|
85
|
-
} else if (
|
|
83
|
+
if (isPanelSidebarView(view)) {
|
|
84
|
+
setSidebarOpen(!(activeView === view && sidebarOpen))
|
|
85
|
+
} else if (FULL_WIDTH_VIEWS.has(view)) {
|
|
86
86
|
setSidebarOpen(false)
|
|
87
87
|
} else {
|
|
88
88
|
setSidebarOpen(true)
|
|
@@ -85,7 +85,6 @@ export function useSaveBuiltinProviderMutation() {
|
|
|
85
85
|
mutationFn: async ({ id, models, isEnabled, baseUrl }: SaveBuiltinProviderInput) => {
|
|
86
86
|
await api('PUT', `/providers/${id}/models`, { models })
|
|
87
87
|
return api('PUT', `/providers/${id}`, {
|
|
88
|
-
type: 'builtin',
|
|
89
88
|
isEnabled,
|
|
90
89
|
...(baseUrl ? { baseUrl } : {}),
|
|
91
90
|
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { isPanelSidebarView, shouldAutoOpenPanelSidebar } from './view-constants'
|
|
5
|
+
|
|
6
|
+
describe('panel sidebar route helpers', () => {
|
|
7
|
+
it('treats knowledge as a panel-backed view', () => {
|
|
8
|
+
assert.equal(isPanelSidebarView('knowledge'), true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('auto-opens panel-backed views only on desktop', () => {
|
|
12
|
+
assert.equal(shouldAutoOpenPanelSidebar('knowledge', true), true)
|
|
13
|
+
assert.equal(shouldAutoOpenPanelSidebar('knowledge', false), false)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('does not auto-open full-width views without panel layouts', () => {
|
|
17
|
+
assert.equal(shouldAutoOpenPanelSidebar('home', true), false)
|
|
18
|
+
assert.equal(shouldAutoOpenPanelSidebar('settings', true), false)
|
|
19
|
+
assert.equal(shouldAutoOpenPanelSidebar(null, true), false)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -245,3 +245,28 @@ export const FULL_WIDTH_VIEWS = new Set<AppView>([
|
|
|
245
245
|
'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'extensions',
|
|
246
246
|
'usage', 'runs', 'autonomy', 'logs', 'settings', 'activity', 'projects', 'swarmfeed', 'marketplace', 'missions',
|
|
247
247
|
])
|
|
248
|
+
|
|
249
|
+
export const PANEL_SIDEBAR_VIEWS = new Set<AppView>([
|
|
250
|
+
'agents',
|
|
251
|
+
'connectors',
|
|
252
|
+
'extensions',
|
|
253
|
+
'knowledge',
|
|
254
|
+
'logs',
|
|
255
|
+
'mcp_servers',
|
|
256
|
+
'memory',
|
|
257
|
+
'providers',
|
|
258
|
+
'runs',
|
|
259
|
+
'schedules',
|
|
260
|
+
'secrets',
|
|
261
|
+
'skills',
|
|
262
|
+
'tasks',
|
|
263
|
+
'webhooks',
|
|
264
|
+
])
|
|
265
|
+
|
|
266
|
+
export function isPanelSidebarView(view: AppView | null | undefined): boolean {
|
|
267
|
+
return Boolean(view && PANEL_SIDEBAR_VIEWS.has(view))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function shouldAutoOpenPanelSidebar(view: AppView | null | undefined, isDesktop: boolean): boolean {
|
|
271
|
+
return isDesktop && isPanelSidebarView(view)
|
|
272
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { streamOpenAiChat } from './openai'
|
|
5
|
+
|
|
6
|
+
function sseChunk(data: unknown) {
|
|
7
|
+
return `data: ${JSON.stringify(data)}\n\n`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseSseEvents(frames: string[]) {
|
|
11
|
+
return frames
|
|
12
|
+
.flatMap((frame) => frame.trim().split('\n\n'))
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((frame) => JSON.parse(frame.replace(/^data: /, '')) as { t: string; text?: string })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('OpenAI-compatible reasoning deltas stream as thinking instead of visible text', async () => {
|
|
18
|
+
const originalFetch = globalThis.fetch
|
|
19
|
+
const encoded = new TextEncoder()
|
|
20
|
+
const frames = [
|
|
21
|
+
sseChunk({ choices: [{ delta: { reasoning_content: 'internal reasoning ' } }] }),
|
|
22
|
+
sseChunk({ choices: [{ delta: { content: 'visible answer' } }] }),
|
|
23
|
+
'data: [DONE]\n\n',
|
|
24
|
+
]
|
|
25
|
+
const writes: string[] = []
|
|
26
|
+
|
|
27
|
+
globalThis.fetch = async () => new Response(new ReadableStream({
|
|
28
|
+
start(controller) {
|
|
29
|
+
for (const frame of frames) controller.enqueue(encoded.encode(frame))
|
|
30
|
+
controller.close()
|
|
31
|
+
},
|
|
32
|
+
}), {
|
|
33
|
+
status: 200,
|
|
34
|
+
headers: { 'content-type': 'text/event-stream' },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await streamOpenAiChat({
|
|
39
|
+
session: { id: 'session-1', provider: 'openai', model: 'test-model' },
|
|
40
|
+
message: 'hello',
|
|
41
|
+
write: (data) => writes.push(data),
|
|
42
|
+
active: new Map(),
|
|
43
|
+
loadHistory: () => [],
|
|
44
|
+
} as Parameters<typeof streamOpenAiChat>[0])
|
|
45
|
+
|
|
46
|
+
assert.equal(result, 'visible answer')
|
|
47
|
+
assert.deepEqual(parseSseEvents(writes), [
|
|
48
|
+
{ t: 'thinking', text: 'internal reasoning ' },
|
|
49
|
+
{ t: 'd', text: 'visible answer' },
|
|
50
|
+
])
|
|
51
|
+
} finally {
|
|
52
|
+
globalThis.fetch = originalFetch
|
|
53
|
+
}
|
|
54
|
+
})
|
|
@@ -166,13 +166,17 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
166
166
|
try {
|
|
167
167
|
const parsed = JSON.parse(data)
|
|
168
168
|
const choice = parsed.choices?.[0]?.delta
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
writeSSE(write, '
|
|
169
|
+
const contentDelta = typeof choice?.content === 'string' ? choice.content : ''
|
|
170
|
+
const reasoningDelta =
|
|
171
|
+
typeof choice?.reasoning_content === 'string' ? choice.reasoning_content
|
|
172
|
+
: typeof choice?.reasoning === 'string' ? choice.reasoning
|
|
173
|
+
: ''
|
|
174
|
+
if (reasoningDelta) {
|
|
175
|
+
writeSSE(write, 'thinking', reasoningDelta)
|
|
176
|
+
}
|
|
177
|
+
if (contentDelta) {
|
|
178
|
+
fullResponse += contentDelta
|
|
179
|
+
writeSSE(write, 'd', contentDelta)
|
|
176
180
|
}
|
|
177
181
|
// Extract usage from the final chunk (stream_options: include_usage)
|
|
178
182
|
if (usageEnabled && parsed.usage && onUsage) {
|
|
@@ -18,9 +18,12 @@ describe('browser tool connection config', () => {
|
|
|
18
18
|
const params = buildBrowserStdioServerParams('/tmp/swarmclaw-browser-profile')
|
|
19
19
|
|
|
20
20
|
assert.equal(params.command, process.execPath)
|
|
21
|
+
assert.equal(params.args.includes('--browser'), true)
|
|
22
|
+
assert.equal(params.args[params.args.indexOf('--browser') + 1], 'chromium')
|
|
21
23
|
assert.equal(params.args.includes('--headless'), true)
|
|
22
24
|
assert.equal(params.args.includes('--shared-browser-context'), false)
|
|
23
25
|
assert.equal(params.args.includes('/tmp/swarmclaw-browser-profile'), true)
|
|
26
|
+
assert.equal((params.env as Record<string, string | undefined>).PLAYWRIGHT_MCP_BROWSER, 'chromium')
|
|
24
27
|
assert.equal((params.env as Record<string, string | undefined>).PLAYWRIGHT_MCP_USER_DATA_DIR, '/tmp/swarmclaw-browser-profile')
|
|
25
28
|
assert.equal(params.env.PLAYWRIGHT_MCP_OUTPUT_MODE, 'file')
|
|
26
29
|
assert.equal(params.env.PLAYWRIGHT_MCP_TIMEOUT_ACTION, '60000')
|
|
@@ -39,10 +42,12 @@ describe('browser tool connection config', () => {
|
|
|
39
42
|
assert.equal(params.args.includes('--cdp-header'), true)
|
|
40
43
|
assert.equal(params.args.includes('Authorization: Bearer token-123'), true)
|
|
41
44
|
assert.equal(params.args.includes('--allow-unrestricted-file-access'), true)
|
|
45
|
+
assert.equal(params.args.includes('--browser'), false)
|
|
42
46
|
assert.equal(params.args.includes('--user-data-dir'), false)
|
|
43
47
|
const env = params.env as Record<string, string | undefined>
|
|
44
48
|
assert.equal(env.PLAYWRIGHT_MCP_CDP_ENDPOINT, 'http://127.0.0.1:44001')
|
|
45
49
|
assert.equal(params.env.PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS, '1')
|
|
50
|
+
assert.equal(env.PLAYWRIGHT_MCP_BROWSER, undefined)
|
|
46
51
|
assert.equal(env.PLAYWRIGHT_MCP_USER_DATA_DIR, undefined)
|
|
47
52
|
})
|
|
48
53
|
|
|
@@ -102,6 +102,7 @@ export function buildBrowserStdioServerParams(
|
|
|
102
102
|
allowUnrestrictedFileAccess?: boolean
|
|
103
103
|
},
|
|
104
104
|
) {
|
|
105
|
+
const browserName = 'chromium'
|
|
105
106
|
const cliCandidates = [
|
|
106
107
|
path.join(process.cwd(), 'node_modules', '@playwright', 'mcp', 'cli.js'),
|
|
107
108
|
path.join(process.cwd(), '[project]', 'node_modules', '@playwright', 'mcp', 'cli.js'),
|
|
@@ -134,6 +135,7 @@ export function buildBrowserStdioServerParams(
|
|
|
134
135
|
}
|
|
135
136
|
} else {
|
|
136
137
|
args.push(
|
|
138
|
+
'--browser', browserName,
|
|
137
139
|
'--headless',
|
|
138
140
|
'--user-data-dir', profileDir,
|
|
139
141
|
)
|
|
@@ -145,6 +147,7 @@ export function buildBrowserStdioServerParams(
|
|
|
145
147
|
env: {
|
|
146
148
|
...env,
|
|
147
149
|
...(cdpEndpoint ? { PLAYWRIGHT_MCP_CDP_ENDPOINT: cdpEndpoint } : {
|
|
150
|
+
PLAYWRIGHT_MCP_BROWSER: browserName,
|
|
148
151
|
PLAYWRIGHT_MCP_USER_DATA_DIR: profileDir,
|
|
149
152
|
PLAYWRIGHT_MCP_HEADLESS: '1',
|
|
150
153
|
}),
|