@swarmclawai/swarmclaw 1.9.34 → 1.9.37
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 +50 -0
- package/electron-dist/main.js +218 -0
- package/package.json +5 -3
- package/src/app/api/openclaw/history/route.ts +11 -6
- package/src/app/api/preview-server/route.ts +20 -12
- package/src/app/api/search/route.test.ts +63 -0
- package/src/app/api/search/route.ts +3 -2
- package/src/app/api/settings/route.ts +5 -1
- package/src/app/api/settings/settings-route.test.ts +38 -0
- package/src/app/api/usage/live/route.ts +2 -2
- package/src/app/globals.css +158 -0
- package/src/app/layout.tsx +12 -9
- package/src/app/protocols/builder/[templateId]/page.tsx +5 -5
- package/src/cli/binary.test.js +4 -1
- package/src/components/layout/dashboard-shell.tsx +9 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +106 -15
- package/src/components/providers/theme-provider.tsx +16 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +5 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +4 -4
- package/src/features/protocols/builder/utils/builder-template-access.test.ts +30 -0
- package/src/features/protocols/builder/utils/builder-template-access.ts +5 -0
- package/src/lib/server/messages/message-repository.test.ts +122 -0
- package/src/lib/server/messages/message-repository.ts +67 -11
- package/src/lib/server/runtime/devserver-launch.ts +7 -4
- package/src/lib/theme-mode.ts +5 -0
- package/src/types/app-settings.ts +2 -0
- package/src/views/settings/section-theme.tsx +41 -1
|
@@ -68,3 +68,125 @@ test('appendMessage notifies both generic and per-session message topics', () =>
|
|
|
68
68
|
assert.deepEqual(output.genericTopics, ['messages'])
|
|
69
69
|
assert.deepEqual(output.sessionTopics, ['messages:sess-notify'])
|
|
70
70
|
})
|
|
71
|
+
|
|
72
|
+
test('lazy migration compacts legacy session message blobs after table persistence', () => {
|
|
73
|
+
const output = runWithTempDataDir<{
|
|
74
|
+
returnedTexts: string[]
|
|
75
|
+
secondReadTexts: string[]
|
|
76
|
+
blobMessageCount: number
|
|
77
|
+
messageCount: number
|
|
78
|
+
lastMessageText: string | null
|
|
79
|
+
}>(`
|
|
80
|
+
const storageMod = await import('@/lib/server/storage')
|
|
81
|
+
const repoMod = await import('@/lib/server/messages/message-repository')
|
|
82
|
+
const storage = storageMod.default || storageMod
|
|
83
|
+
const repo = repoMod.default || repoMod
|
|
84
|
+
|
|
85
|
+
storage.saveSessions({
|
|
86
|
+
'sess-legacy-blob': {
|
|
87
|
+
id: 'sess-legacy-blob',
|
|
88
|
+
name: 'Legacy blob session',
|
|
89
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
90
|
+
user: 'tester',
|
|
91
|
+
provider: 'openai',
|
|
92
|
+
model: 'gpt-5',
|
|
93
|
+
claudeSessionId: null,
|
|
94
|
+
codexThreadId: null,
|
|
95
|
+
opencodeSessionId: null,
|
|
96
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
97
|
+
messages: [
|
|
98
|
+
{ role: 'user', text: 'first legacy prompt', time: 1 },
|
|
99
|
+
{ role: 'assistant', text: 'first legacy reply', time: 2 },
|
|
100
|
+
{ role: 'user', text: 'second legacy prompt', time: 3 },
|
|
101
|
+
],
|
|
102
|
+
createdAt: Date.now(),
|
|
103
|
+
lastActiveAt: Date.now(),
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const returned = repo.getMessages('sess-legacy-blob')
|
|
108
|
+
const secondRead = repo.getMessages('sess-legacy-blob')
|
|
109
|
+
const stored = storage.loadSessions()['sess-legacy-blob']
|
|
110
|
+
|
|
111
|
+
console.log(JSON.stringify({
|
|
112
|
+
returnedTexts: returned.map((message) => message.text),
|
|
113
|
+
secondReadTexts: secondRead.map((message) => message.text),
|
|
114
|
+
blobMessageCount: Array.isArray(stored.messages) ? stored.messages.length : -1,
|
|
115
|
+
messageCount: stored.messageCount,
|
|
116
|
+
lastMessageText: stored.lastMessageSummary?.text || null,
|
|
117
|
+
}))
|
|
118
|
+
`, { prefix: 'swarmclaw-message-repo-compact-' })
|
|
119
|
+
|
|
120
|
+
assert.deepEqual(output.returnedTexts, [
|
|
121
|
+
'first legacy prompt',
|
|
122
|
+
'first legacy reply',
|
|
123
|
+
'second legacy prompt',
|
|
124
|
+
])
|
|
125
|
+
assert.deepEqual(output.secondReadTexts, output.returnedTexts)
|
|
126
|
+
assert.equal(output.blobMessageCount, 0)
|
|
127
|
+
assert.equal(output.messageCount, 3)
|
|
128
|
+
assert.equal(output.lastMessageText, 'second legacy prompt')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('bulk migration reports compaction for table-backed legacy blobs', () => {
|
|
132
|
+
const output = runWithTempDataDir<{
|
|
133
|
+
result: {
|
|
134
|
+
migrated: number
|
|
135
|
+
compacted: number
|
|
136
|
+
skipped: number
|
|
137
|
+
total: number
|
|
138
|
+
}
|
|
139
|
+
blobMessageCount: number
|
|
140
|
+
messageCount: number
|
|
141
|
+
}>(`
|
|
142
|
+
const storageMod = await import('@/lib/server/storage')
|
|
143
|
+
const repoMod = await import('@/lib/server/messages/message-repository')
|
|
144
|
+
const storage = storageMod.default || storageMod
|
|
145
|
+
const repo = repoMod.default || repoMod
|
|
146
|
+
|
|
147
|
+
const messages = [
|
|
148
|
+
{ role: 'user', text: 'stale blob prompt', time: 10 },
|
|
149
|
+
{ role: 'assistant', text: 'stale blob reply', time: 20 },
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
storage.saveSessions({
|
|
153
|
+
'sess-table-backed-blob': {
|
|
154
|
+
id: 'sess-table-backed-blob',
|
|
155
|
+
name: 'Table backed blob session',
|
|
156
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
157
|
+
user: 'tester',
|
|
158
|
+
provider: 'openai',
|
|
159
|
+
model: 'gpt-5',
|
|
160
|
+
claudeSessionId: null,
|
|
161
|
+
codexThreadId: null,
|
|
162
|
+
opencodeSessionId: null,
|
|
163
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
164
|
+
messages,
|
|
165
|
+
createdAt: Date.now(),
|
|
166
|
+
lastActiveAt: Date.now(),
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const db = storage.getDb()
|
|
171
|
+
const insert = db.prepare('INSERT INTO session_messages (session_id, seq, data) VALUES (?, ?, ?)')
|
|
172
|
+
messages.forEach((message, index) => {
|
|
173
|
+
insert.run('sess-table-backed-blob', index, JSON.stringify(message))
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const result = repo.migrateAllSessions()
|
|
177
|
+
const stored = storage.loadSessions()['sess-table-backed-blob']
|
|
178
|
+
|
|
179
|
+
console.log(JSON.stringify({
|
|
180
|
+
result,
|
|
181
|
+
blobMessageCount: Array.isArray(stored.messages) ? stored.messages.length : -1,
|
|
182
|
+
messageCount: stored.messageCount,
|
|
183
|
+
}))
|
|
184
|
+
`, { prefix: 'swarmclaw-message-repo-migrate-report-' })
|
|
185
|
+
|
|
186
|
+
assert.equal(output.result.migrated, 0)
|
|
187
|
+
assert.equal(output.result.compacted, 1)
|
|
188
|
+
assert.equal(output.result.skipped, 1)
|
|
189
|
+
assert.equal(output.result.total, 1)
|
|
190
|
+
assert.equal(output.blobMessageCount, 0)
|
|
191
|
+
assert.equal(output.messageCount, 2)
|
|
192
|
+
})
|
|
@@ -86,6 +86,38 @@ function summarizeForMeta(message: Message): Message {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function getLastAssistantAt(messages: Message[]): number | null {
|
|
90
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
91
|
+
if (messages[i].role === 'assistant' && typeof messages[i].time === 'number') {
|
|
92
|
+
return messages[i].time
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function compactDeprecatedBlobMessages(
|
|
99
|
+
sessionId: string,
|
|
100
|
+
persistedCount: number,
|
|
101
|
+
lastMsg: Message | null,
|
|
102
|
+
lastAssistantAt: number | null,
|
|
103
|
+
): boolean {
|
|
104
|
+
let compacted = false
|
|
105
|
+
patchSession(sessionId, (current) => {
|
|
106
|
+
if (!current) return null
|
|
107
|
+
const blobCount = Array.isArray(current.messages) ? current.messages.length : 0
|
|
108
|
+
if (blobCount === 0) return current
|
|
109
|
+
if (persistedCount < blobCount) return current
|
|
110
|
+
|
|
111
|
+
current.messages = []
|
|
112
|
+
current.messageCount = persistedCount
|
|
113
|
+
current.lastMessageSummary = lastMsg ? summarizeForMeta(lastMsg) : null
|
|
114
|
+
if (lastAssistantAt !== null) current.lastAssistantAt = lastAssistantAt
|
|
115
|
+
compacted = true
|
|
116
|
+
return current
|
|
117
|
+
})
|
|
118
|
+
return compacted
|
|
119
|
+
}
|
|
120
|
+
|
|
89
121
|
// ---------------------------------------------------------------------------
|
|
90
122
|
// Session metadata sync — keeps messageCount / lastMessageSummary on the blob
|
|
91
123
|
// ---------------------------------------------------------------------------
|
|
@@ -139,18 +171,14 @@ function lazyMigrateSession(sessionId: string): Message[] | null {
|
|
|
139
171
|
ins.run(sessionId, i, JSON.stringify(messages[i]))
|
|
140
172
|
}
|
|
141
173
|
|
|
142
|
-
// Compute metadata
|
|
174
|
+
// Compute metadata before compacting deprecated blob storage.
|
|
143
175
|
const lastMsg = messages[messages.length - 1]
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (messages[i].role === 'assistant' && typeof messages[i].time === 'number') {
|
|
147
|
-
lastAssistantAt = messages[i].time
|
|
148
|
-
break
|
|
149
|
-
}
|
|
150
|
-
}
|
|
176
|
+
const lastAssistantAt = getLastAssistantAt(messages)
|
|
177
|
+
const persistedCount = rowCount(sessionId)
|
|
151
178
|
|
|
152
179
|
patchSession(sessionId, (current) => {
|
|
153
180
|
if (!current) return null
|
|
181
|
+
current.messages = persistedCount >= messages.length ? [] : current.messages
|
|
154
182
|
current.messageCount = messages.length
|
|
155
183
|
current.lastMessageSummary = lastMsg ? summarizeForMeta(lastMsg) : null
|
|
156
184
|
if (lastAssistantAt !== null && typeof current.lastAssistantAt !== 'number') {
|
|
@@ -183,6 +211,7 @@ export function getMessages(sessionId: string): Message[] {
|
|
|
183
211
|
const m = parseMsg(row.data)
|
|
184
212
|
if (m) out.push(m)
|
|
185
213
|
}
|
|
214
|
+
compactDeprecatedBlobMessages(sessionId, out.length, out[out.length - 1] || null, getLastAssistantAt(out))
|
|
186
215
|
return out
|
|
187
216
|
}, { sessionId })
|
|
188
217
|
}
|
|
@@ -338,10 +367,16 @@ export function deleteSessionMessages(sessionId: string): void {
|
|
|
338
367
|
// Bulk migration (for CLI / admin endpoint)
|
|
339
368
|
// ---------------------------------------------------------------------------
|
|
340
369
|
|
|
341
|
-
export function migrateAllSessions(): {
|
|
370
|
+
export function migrateAllSessions(): {
|
|
371
|
+
migrated: number
|
|
372
|
+
compacted: number
|
|
373
|
+
skipped: number
|
|
374
|
+
total: number
|
|
375
|
+
} {
|
|
342
376
|
const db = getDb()
|
|
343
377
|
const rows = db.prepare('SELECT id, data FROM sessions').all() as Array<{ id: string; data: string }>
|
|
344
378
|
let migrated = 0
|
|
379
|
+
let compacted = 0
|
|
345
380
|
let skipped = 0
|
|
346
381
|
|
|
347
382
|
for (const row of rows) {
|
|
@@ -351,16 +386,37 @@ export function migrateAllSessions(): { migrated: number; skipped: number; total
|
|
|
351
386
|
skipped++
|
|
352
387
|
continue
|
|
353
388
|
}
|
|
354
|
-
|
|
389
|
+
|
|
390
|
+
const persistedCount = rowCount(row.id)
|
|
391
|
+
if (persistedCount > 0) {
|
|
392
|
+
const persistedRows = stmts().selectAll.all(row.id) as Array<{ data: string }>
|
|
393
|
+
const persistedMessages: Message[] = []
|
|
394
|
+
for (const persistedRow of persistedRows) {
|
|
395
|
+
const message = parseMsg(persistedRow.data)
|
|
396
|
+
if (message) persistedMessages.push(message)
|
|
397
|
+
}
|
|
398
|
+
if (compactDeprecatedBlobMessages(
|
|
399
|
+
row.id,
|
|
400
|
+
persistedCount,
|
|
401
|
+
persistedMessages[persistedMessages.length - 1] || null,
|
|
402
|
+
getLastAssistantAt(persistedMessages),
|
|
403
|
+
)) {
|
|
404
|
+
compacted++
|
|
405
|
+
}
|
|
355
406
|
skipped++
|
|
356
407
|
continue
|
|
357
408
|
}
|
|
409
|
+
|
|
358
410
|
lazyMigrateSession(row.id)
|
|
359
411
|
migrated++
|
|
412
|
+
const stored = loadSession(row.id)
|
|
413
|
+
if (!Array.isArray(stored?.messages) || stored.messages.length === 0) {
|
|
414
|
+
compacted++
|
|
415
|
+
}
|
|
360
416
|
} catch {
|
|
361
417
|
skipped++
|
|
362
418
|
}
|
|
363
419
|
}
|
|
364
420
|
|
|
365
|
-
return { migrated, skipped, total: rows.length }
|
|
421
|
+
return { migrated, compacted, skipped, total: rows.length }
|
|
366
422
|
}
|
|
@@ -23,10 +23,10 @@ const NEXT_CONFIG_FILES = [
|
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
function readPackageJson(dir: string): PackageJsonLike | null {
|
|
26
|
-
const pkgPath = path.join(dir, 'package.json')
|
|
27
|
-
if (!fs.existsSync(pkgPath)) return null
|
|
26
|
+
const pkgPath = path.join(/*turbopackIgnore: true*/ dir, 'package.json')
|
|
27
|
+
if (!fs.existsSync(/*turbopackIgnore: true*/ pkgPath)) return null
|
|
28
28
|
try {
|
|
29
|
-
const parsed: unknown = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
29
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(/*turbopackIgnore: true*/ pkgPath, 'utf8'))
|
|
30
30
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
31
31
|
? parsed as PackageJsonLike
|
|
32
32
|
: null
|
|
@@ -46,7 +46,10 @@ function hasNextScript(pkg: PackageJsonLike): boolean {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function hasNextConfig(dir: string): boolean {
|
|
49
|
-
return NEXT_CONFIG_FILES.some((file) =>
|
|
49
|
+
return NEXT_CONFIG_FILES.some((file) => {
|
|
50
|
+
const configPath = path.join(/*turbopackIgnore: true*/ dir, file)
|
|
51
|
+
return fs.existsSync(/*turbopackIgnore: true*/ configPath)
|
|
52
|
+
})
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
function classifyPackageRoot(dir: string, pkg: PackageJsonLike): FrameworkKind {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionResetMode } from './session'
|
|
2
2
|
import type { ExtensionManagedLocalFolderDeclaration } from './extension'
|
|
3
|
+
import type { ThemeMode } from '@/lib/theme-mode'
|
|
3
4
|
|
|
4
5
|
// --- App Settings ---
|
|
5
6
|
export type LoopMode = 'bounded' | 'ongoing'
|
|
@@ -134,6 +135,7 @@ export interface AppSettings {
|
|
|
134
135
|
defaultAgentId?: string | null
|
|
135
136
|
// Theme
|
|
136
137
|
themeHue?: string
|
|
138
|
+
themeMode?: ThemeMode
|
|
137
139
|
// Web search provider
|
|
138
140
|
webSearchProvider?: 'duckduckgo' | 'google' | 'bing' | 'searxng' | 'tavily' | 'brave' | 'exa'
|
|
139
141
|
searxngUrl?: string
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
|
+
import { Monitor, Moon, Sun } from 'lucide-react'
|
|
5
|
+
import { useTheme } from 'next-themes'
|
|
4
6
|
import { toast } from 'sonner'
|
|
7
|
+
import { normalizeThemeMode, type ThemeMode } from '@/lib/theme-mode'
|
|
5
8
|
import type { SettingsSectionProps } from './types'
|
|
6
9
|
|
|
7
10
|
const PRESETS = [
|
|
@@ -13,12 +16,26 @@ const PRESETS = [
|
|
|
13
16
|
{ label: 'Rose', color: '#2e1a24' },
|
|
14
17
|
]
|
|
15
18
|
|
|
19
|
+
const THEME_MODES: Array<{ id: ThemeMode; label: string; Icon: typeof Sun }> = [
|
|
20
|
+
{ id: 'light', label: 'Light', Icon: Sun },
|
|
21
|
+
{ id: 'dark', label: 'Dark', Icon: Moon },
|
|
22
|
+
{ id: 'system', label: 'System', Icon: Monitor },
|
|
23
|
+
]
|
|
24
|
+
|
|
16
25
|
export function ThemeSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
26
|
+
const { setTheme } = useTheme()
|
|
17
27
|
const currentHue = appSettings.themeHue || PRESETS[0].color
|
|
28
|
+
const currentMode = normalizeThemeMode(appSettings.themeMode)
|
|
18
29
|
const [customHex, setCustomHex] = useState(
|
|
19
30
|
PRESETS.some((p) => p.color === currentHue) ? '' : currentHue,
|
|
20
31
|
)
|
|
21
32
|
|
|
33
|
+
const applyMode = (mode: ThemeMode) => {
|
|
34
|
+
setTheme(mode)
|
|
35
|
+
patchSettings({ themeMode: mode })
|
|
36
|
+
toast.success('Theme updated')
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
const applyHue = (color: string) => {
|
|
23
40
|
patchSettings({ themeHue: color })
|
|
24
41
|
document.documentElement.style.setProperty('--neutral-tint', color)
|
|
@@ -38,9 +55,32 @@ export function ThemeSection({ appSettings, patchSettings, inputClass }: Setting
|
|
|
38
55
|
Theme
|
|
39
56
|
</h3>
|
|
40
57
|
<p className="text-[12px] text-text-3 mb-5">
|
|
41
|
-
|
|
58
|
+
Choose a color scheme and shift the UI palette with a preset or custom hex color.
|
|
42
59
|
</p>
|
|
43
60
|
|
|
61
|
+
<div className="inline-grid grid-cols-3 rounded-[8px] border border-white/[0.08] bg-white/[0.03] p-1 mb-5">
|
|
62
|
+
{THEME_MODES.map(({ id, label, Icon }) => {
|
|
63
|
+
const isActive = currentMode === id
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
key={id}
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => applyMode(id)}
|
|
69
|
+
aria-pressed={isActive}
|
|
70
|
+
className={`h-9 px-3 rounded-[6px] flex items-center justify-center gap-2 text-[12px] font-600 transition-colors ${
|
|
71
|
+
isActive
|
|
72
|
+
? 'bg-accent text-white'
|
|
73
|
+
: 'text-text-3 hover:text-text hover:bg-white/[0.05]'
|
|
74
|
+
}`}
|
|
75
|
+
title={label}
|
|
76
|
+
>
|
|
77
|
+
<Icon className="w-4 h-4" aria-hidden="true" />
|
|
78
|
+
<span>{label}</span>
|
|
79
|
+
</button>
|
|
80
|
+
)
|
|
81
|
+
})}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
44
84
|
{/* Preset swatches */}
|
|
45
85
|
<div className="flex flex-wrap gap-3 mb-4">
|
|
46
86
|
{PRESETS.map((preset) => {
|