@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 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.69",
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 (FULL_WIDTH_VIEWS.has(view)) {
84
- setSidebarOpen(false)
85
- } else if (activeView === view && sidebarOpen) {
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 delta = choice?.content
170
- // Thinking/reasoning models (kimi-k2, etc.) put output in reasoning fields
171
- || choice?.reasoning_content
172
- || choice?.reasoning
173
- if (delta) {
174
- fullResponse += delta
175
- writeSSE(write, 'd', delta)
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) {