Skip to content

Commit 4d85a87

Browse files
committed
Fix panel sidebars and reasoning streams for v1.5.70
1 parent dfe14fb commit 4d85a87

9 files changed

Lines changed: 146 additions & 17 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,17 @@ Operational docs: https://swarmclaw.ai/docs/observability
399399

400400
## Releases
401401

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+
402413
### v1.5.69 Highlights
403414

404415
Fast-follow release for [#55](https://github.com/swarmclawai/swarmclaw/pull/55) by [@borislavnnikolov](https://github.com/borislavnnikolov). Thanks Borislav!

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@swarmclawai/swarmclaw",
3-
"version": "1.5.69",
3+
"version": "1.5.70",
44
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
55
"main": "electron-dist/main.js",
66
"license": "MIT",
@@ -86,8 +86,8 @@
8686
"cli": "node ./bin/swarmclaw.js",
8787
"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",
8888
"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/providers/[id]/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",
9191
"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",
9292
"test:e2e": "tsx .workbench/browser-e2e/run.ts",
9393
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",

src/components/layout/dashboard-shell.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useEffect, useState, useCallback } from 'react'
3+
import { useEffect, useRef, useState, useCallback } from 'react'
44
import { useRouter, usePathname } from 'next/navigation'
55
import { initAudioContext } from '@/lib/tts'
66
import { clearStoredAccessKey } from '@/lib/app/api-client'
@@ -13,6 +13,7 @@ import { useSwipe } from '@/hooks/use-swipe'
1313
import { useWs } from '@/hooks/use-ws'
1414
import { api } from '@/lib/app/api-client'
1515
import { pathToView, useNavigate } from '@/lib/app/navigation'
16+
import { shouldAutoOpenPanelSidebar } from '@/lib/app/view-constants'
1617

1718
import { FullScreenLoader } from '@/components/ui/full-screen-loader'
1819
import { SidebarRail } from '@/components/layout/sidebar-rail'
@@ -45,6 +46,7 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
4546

4647
const [bootTimedOut, setBootTimedOut] = useState(false)
4748
const [profileSheetOpen, setProfileSheetOpen] = useState(false)
49+
const lastAutoOpenedPanelPathRef = useRef<string | null>(null)
4850
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
4951
const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
5052
const appSettings = useAppStore((s) => s.appSettings)
@@ -165,6 +167,18 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
165167
}
166168
}, [pathname, isViewEnabled, router, isAuthPage])
167169

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+
168182
// Extension sidebar items
169183
const refreshExtensionState = useCallback(() => {
170184
void loadExtensions()

src/components/layout/sidebar-rail.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { DaemonIndicator } from '@/components/layout/daemon-indicator'
99
import { NotificationCenter } from '@/components/shared/notification-center'
1010
import { NavItem, RailTooltip } from '@/components/layout/nav-item'
1111
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'
1313
import { pathToView, useNavigate } from '@/lib/app/navigation'
1414
import { safeStorageGet, safeStorageSet } from '@/lib/app/safe-storage'
1515
import type { AppView } from '@/types'
@@ -80,9 +80,9 @@ export function SidebarRail({
8080
setSidebarOpen(false)
8181
return
8282
}
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)) {
8686
setSidebarOpen(false)
8787
} else {
8888
setSidebarOpen(true)

src/lib/app/view-constants.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
})

src/lib/app/view-constants.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,28 @@ export const FULL_WIDTH_VIEWS = new Set<AppView>([
245245
'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'extensions',
246246
'usage', 'runs', 'autonomy', 'logs', 'settings', 'activity', 'projects', 'swarmfeed', 'marketplace', 'missions',
247247
])
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+
}

src/lib/providers/openai.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
})

src/lib/providers/openai.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,17 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
166166
try {
167167
const parsed = JSON.parse(data)
168168
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)
176180
}
177181
// Extract usage from the final chunk (stream_options: include_usage)
178182
if (usageEnabled && parsed.usage && onUsage) {

0 commit comments

Comments
 (0)