@swarmclawai/swarmclaw 1.5.69 → 1.5.70
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 +11 -0
- package/package.json +3 -3
- 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/README.md
CHANGED
|
@@ -399,6 +399,17 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.5.70 Highlights
|
|
403
|
+
|
|
404
|
+
Fast-follow release for [#56](https://github.com/swarmclawai/swarmclaw/pull/56) by [@latentwill](https://github.com/latentwill). Thanks latentwill!
|
|
405
|
+
|
|
406
|
+
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!
|
|
407
|
+
|
|
408
|
+
- **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.
|
|
409
|
+
- **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.
|
|
410
|
+
- **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.
|
|
411
|
+
- **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.
|
|
412
|
+
|
|
402
413
|
### v1.5.69 Highlights
|
|
403
414
|
|
|
404
415
|
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.70",
|
|
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",
|
|
@@ -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) {
|