@swarmclawai/swarmclaw 1.9.16 → 1.9.17
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
|
@@ -149,6 +149,14 @@ clawhub install swarmclaw
|
|
|
149
149
|
|
|
150
150
|
[Browse on ClawHub](https://clawhub.ai/skills/swarmclaw)
|
|
151
151
|
|
|
152
|
+
## v1.9.17 Highlights
|
|
153
|
+
|
|
154
|
+
Agent configuration history is now visible in the agent editor, so operators can review recent saved versions and restore prior settings without leaving the agent workflow.
|
|
155
|
+
|
|
156
|
+
- **Agent sheet history.** Advanced agent settings list recent saved versions with timestamp, actor, and provider/model snapshot.
|
|
157
|
+
- **One-click restore.** Restoring a prior version uses the existing config-version restore API, refreshes agent state, and closes the sheet to avoid stale form data.
|
|
158
|
+
- **Regression coverage.** New tests cover config-version list/restore routes and UI summary formatting.
|
|
159
|
+
|
|
152
160
|
## Hosted Deploys
|
|
153
161
|
|
|
154
162
|
SwarmClaw now ships provider-ready deploy files at the repo root:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.17",
|
|
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",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
89
89
|
"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",
|
|
90
90
|
"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/gateways/gateway-topology.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/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
91
|
-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts 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/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.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/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.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/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.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/tasks/task-workspace-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-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
91
|
+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/agent-planning-mode.test.ts src/lib/agent-config-history.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts 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/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.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/chat-execution/prompt-sections.planning-mode.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.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/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/runs/run-handoff.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.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/tasks/task-workspace-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-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/config-versions/config-versions-route.test.ts src/app/api/runs/run-handoff-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
|
|
92
92
|
"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",
|
|
93
93
|
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
|
|
94
94
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
|
|
5
|
+
|
|
6
|
+
test('config version routes list and restore prior agent settings', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
updateStatus: number
|
|
9
|
+
listStatus: number
|
|
10
|
+
restoreStatus: number
|
|
11
|
+
versionCount: number
|
|
12
|
+
versionName: string | null
|
|
13
|
+
restoredName: string | null
|
|
14
|
+
restoredModel: string | null
|
|
15
|
+
}>(`
|
|
16
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
17
|
+
const agentRouteMod = await import('./src/app/api/agents/[id]/route')
|
|
18
|
+
const configRouteMod = await import('./src/app/api/config-versions/route')
|
|
19
|
+
const restoreRouteMod = await import('./src/app/api/config-versions/restore/route')
|
|
20
|
+
const storage = storageMod.default || storageMod
|
|
21
|
+
const agentRoute = agentRouteMod.default || agentRouteMod
|
|
22
|
+
const configRoute = configRouteMod.default || configRouteMod
|
|
23
|
+
const restoreRoute = restoreRouteMod.default || restoreRouteMod
|
|
24
|
+
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
storage.saveAgents({
|
|
27
|
+
agent_history_1: {
|
|
28
|
+
id: 'agent_history_1',
|
|
29
|
+
name: 'Before Save',
|
|
30
|
+
description: 'Original',
|
|
31
|
+
systemPrompt: '',
|
|
32
|
+
provider: 'ollama',
|
|
33
|
+
model: 'qwen3.5',
|
|
34
|
+
credentialId: null,
|
|
35
|
+
fallbackCredentialIds: [],
|
|
36
|
+
apiEndpoint: null,
|
|
37
|
+
gatewayProfileId: null,
|
|
38
|
+
extensions: [],
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const params = { params: Promise.resolve({ id: 'agent_history_1' }) }
|
|
45
|
+
const updateResponse = await agentRoute.PUT(new Request('http://local/api/agents/agent_history_1', {
|
|
46
|
+
method: 'PUT',
|
|
47
|
+
headers: { 'content-type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ name: 'After Save', model: 'qwen3.6' }),
|
|
49
|
+
}), params)
|
|
50
|
+
|
|
51
|
+
const listResponse = await configRoute.GET(new Request('http://local/api/config-versions?entityKind=agent&entityId=agent_history_1'))
|
|
52
|
+
const listPayload = await listResponse.json()
|
|
53
|
+
const firstVersion = listPayload.versions?.[0] || null
|
|
54
|
+
|
|
55
|
+
const restoreResponse = await restoreRoute.POST(new Request('http://local/api/config-versions/restore', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ versionId: firstVersion?.id }),
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const restored = storage.loadAgents().agent_history_1
|
|
62
|
+
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
updateStatus: updateResponse.status,
|
|
65
|
+
listStatus: listResponse.status,
|
|
66
|
+
restoreStatus: restoreResponse.status,
|
|
67
|
+
versionCount: Array.isArray(listPayload.versions) ? listPayload.versions.length : 0,
|
|
68
|
+
versionName: firstVersion?.snapshot?.name || null,
|
|
69
|
+
restoredName: restored?.name || null,
|
|
70
|
+
restoredModel: restored?.model || null,
|
|
71
|
+
}))
|
|
72
|
+
`, { prefix: 'swarmclaw-config-versions-route-' })
|
|
73
|
+
|
|
74
|
+
assert.equal(output.updateStatus, 200)
|
|
75
|
+
assert.equal(output.listStatus, 200)
|
|
76
|
+
assert.equal(output.restoreStatus, 200)
|
|
77
|
+
assert.equal(output.versionCount, 1)
|
|
78
|
+
assert.equal(output.versionName, 'Before Save')
|
|
79
|
+
assert.equal(output.restoredName, 'Before Save')
|
|
80
|
+
assert.equal(output.restoredModel, 'qwen3.5')
|
|
81
|
+
})
|
|
@@ -27,10 +27,12 @@ import { resolveStoredOllamaMode } from '@/lib/ollama-mode'
|
|
|
27
27
|
import { errorMessage } from '@/lib/shared-utils'
|
|
28
28
|
import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
|
|
29
29
|
import { AGENT_PLANNING_MODE_OPTIONS, describeAgentPlanningMode, normalizeAgentPlanningMode, type AgentPlanningMode } from '@/lib/agent-planning-mode'
|
|
30
|
+
import { buildAgentConfigVersionSummary } from '@/lib/agent-config-history'
|
|
30
31
|
import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
|
|
31
32
|
import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
|
|
32
33
|
import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
|
|
33
34
|
import { AgentMarketplaceSettings } from '@/features/swarmdock/agent-marketplace-settings'
|
|
35
|
+
import type { ConfigVersion } from '@/types/config-version'
|
|
34
36
|
|
|
35
37
|
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
36
38
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
@@ -292,6 +294,10 @@ export function AgentSheet() {
|
|
|
292
294
|
const lastAutoSyncedModelsKeyRef = useRef<string | null>(null)
|
|
293
295
|
const skipAutoModelRef = useRef(false)
|
|
294
296
|
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false)
|
|
297
|
+
const [configVersions, setConfigVersions] = useState<ConfigVersion[]>([])
|
|
298
|
+
const [configVersionsLoading, setConfigVersionsLoading] = useState(false)
|
|
299
|
+
const [configVersionsError, setConfigVersionsError] = useState<string | null>(null)
|
|
300
|
+
const [restoringConfigVersionId, setRestoringConfigVersionId] = useState<string | null>(null)
|
|
295
301
|
|
|
296
302
|
const handleFileUpload = (setter: (v: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
297
303
|
const file = e.target.files?.[0]
|
|
@@ -361,6 +367,23 @@ export function AgentSheet() {
|
|
|
361
367
|
return { synced: !sameModels, models: result.models }
|
|
362
368
|
}, [agentSelectableProviders, loadProviders, openclawEnabled])
|
|
363
369
|
|
|
370
|
+
const loadAgentConfigVersions = useCallback(async (agentId: string) => {
|
|
371
|
+
setConfigVersionsLoading(true)
|
|
372
|
+
setConfigVersionsError(null)
|
|
373
|
+
try {
|
|
374
|
+
const response = await api<{ versions: ConfigVersion[] }>(
|
|
375
|
+
'GET',
|
|
376
|
+
`/config-versions?entityKind=agent&entityId=${encodeURIComponent(agentId)}`,
|
|
377
|
+
)
|
|
378
|
+
setConfigVersions(Array.isArray(response.versions) ? response.versions : [])
|
|
379
|
+
} catch (err) {
|
|
380
|
+
setConfigVersions([])
|
|
381
|
+
setConfigVersionsError(errorMessage(err))
|
|
382
|
+
} finally {
|
|
383
|
+
setConfigVersionsLoading(false)
|
|
384
|
+
}
|
|
385
|
+
}, [])
|
|
386
|
+
|
|
364
387
|
const providerNeedsKey = !editing && (
|
|
365
388
|
(currentProvider?.requiresApiKey && providerCredentials.length === 0 && !addingKey) ||
|
|
366
389
|
(provider === 'ollama' && ollamaMode === 'cloud' && providerCredentials.length === 0 && !addingKey)
|
|
@@ -640,6 +663,17 @@ export function AgentSheet() {
|
|
|
640
663
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
641
664
|
}, [open, editingId])
|
|
642
665
|
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
if (!open || !editingId) {
|
|
668
|
+
setConfigVersions([])
|
|
669
|
+
setConfigVersionsError(null)
|
|
670
|
+
setConfigVersionsLoading(false)
|
|
671
|
+
setRestoringConfigVersionId(null)
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
void loadAgentConfigVersions(editingId)
|
|
675
|
+
}, [editingId, loadAgentConfigVersions, open])
|
|
676
|
+
|
|
643
677
|
useEffect(() => {
|
|
644
678
|
if (skipAutoModelRef.current) {
|
|
645
679
|
skipAutoModelRef.current = false
|
|
@@ -891,6 +925,33 @@ export function AgentSheet() {
|
|
|
891
925
|
}
|
|
892
926
|
}
|
|
893
927
|
|
|
928
|
+
const handleRestoreConfigVersion = async (versionId: string) => {
|
|
929
|
+
if (!editing) return
|
|
930
|
+
const confirmed = window.confirm('Restore this saved agent configuration? Current settings will become a new history entry when restored.')
|
|
931
|
+
if (!confirmed) return
|
|
932
|
+
setRestoringConfigVersionId(versionId)
|
|
933
|
+
try {
|
|
934
|
+
await api('POST', '/config-versions/restore', { versionId })
|
|
935
|
+
await loadAgents()
|
|
936
|
+
if (
|
|
937
|
+
activeSessionId
|
|
938
|
+
&& currentSession?.agentId === editing.id
|
|
939
|
+
&& (
|
|
940
|
+
currentSession.shortcutForAgentId === editing.id
|
|
941
|
+
|| activeSessionId === editing.threadSessionId
|
|
942
|
+
)
|
|
943
|
+
) {
|
|
944
|
+
await refreshSession(activeSessionId)
|
|
945
|
+
}
|
|
946
|
+
toast.success('Agent configuration restored')
|
|
947
|
+
onClose()
|
|
948
|
+
} catch (err) {
|
|
949
|
+
toast.error(`Restore failed: ${errorMessage(err)}`)
|
|
950
|
+
} finally {
|
|
951
|
+
setRestoringConfigVersionId(null)
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
894
955
|
const handleExport = () => {
|
|
895
956
|
if (!editing) return
|
|
896
957
|
const recommendedProviders = agentSelectableProviders.some((providerOption) => (
|
|
@@ -1100,6 +1161,7 @@ export function AgentSheet() {
|
|
|
1100
1161
|
}
|
|
1101
1162
|
|
|
1102
1163
|
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
1164
|
+
const configVersionSummaries = configVersions.map((version) => buildAgentConfigVersionSummary(version))
|
|
1103
1165
|
|
|
1104
1166
|
return (
|
|
1105
1167
|
<>
|
|
@@ -2787,6 +2849,63 @@ export function AgentSheet() {
|
|
|
2787
2849
|
</div>
|
|
2788
2850
|
</SectionCard>
|
|
2789
2851
|
|
|
2852
|
+
{editing && (
|
|
2853
|
+
<SectionCard
|
|
2854
|
+
title="Configuration History"
|
|
2855
|
+
description="Recent saved versions for this agent."
|
|
2856
|
+
className="mb-6 border-white/[0.05] bg-white/[0.01]"
|
|
2857
|
+
action={(
|
|
2858
|
+
<button
|
|
2859
|
+
type="button"
|
|
2860
|
+
onClick={() => void loadAgentConfigVersions(editing.id)}
|
|
2861
|
+
disabled={configVersionsLoading}
|
|
2862
|
+
className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-[12px] font-600 text-text-3 hover:bg-white/[0.04] hover:text-text-2 transition-all disabled:opacity-50"
|
|
2863
|
+
style={{ fontFamily: 'inherit' }}
|
|
2864
|
+
>
|
|
2865
|
+
{configVersionsLoading ? 'Refreshing' : 'Refresh'}
|
|
2866
|
+
</button>
|
|
2867
|
+
)}
|
|
2868
|
+
>
|
|
2869
|
+
{configVersionsError ? (
|
|
2870
|
+
<div className="rounded-[12px] border border-red-500/20 bg-red-500/[0.06] p-3 text-[13px] text-red-300">
|
|
2871
|
+
{configVersionsError}
|
|
2872
|
+
</div>
|
|
2873
|
+
) : configVersionsLoading && configVersionSummaries.length === 0 ? (
|
|
2874
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-white/[0.02] p-3 text-[13px] text-text-3">
|
|
2875
|
+
Loading saved versions...
|
|
2876
|
+
</div>
|
|
2877
|
+
) : configVersionSummaries.length === 0 ? (
|
|
2878
|
+
<div className="rounded-[12px] border border-white/[0.06] bg-white/[0.02] p-3 text-[13px] text-text-3">
|
|
2879
|
+
No saved versions yet.
|
|
2880
|
+
</div>
|
|
2881
|
+
) : (
|
|
2882
|
+
<div className="space-y-3">
|
|
2883
|
+
{configVersionSummaries.slice(0, 8).map((summary) => (
|
|
2884
|
+
<div
|
|
2885
|
+
key={summary.id}
|
|
2886
|
+
className="flex flex-col gap-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] p-3 sm:flex-row sm:items-center sm:justify-between"
|
|
2887
|
+
>
|
|
2888
|
+
<div className="min-w-0">
|
|
2889
|
+
<div className="truncate text-[14px] font-700 text-text">{summary.title}</div>
|
|
2890
|
+
<div className="mt-1 truncate text-[12px] text-text-3">{summary.subtitle}</div>
|
|
2891
|
+
<div className="mt-1 text-[11px] text-text-3/70">{summary.meta}</div>
|
|
2892
|
+
</div>
|
|
2893
|
+
<button
|
|
2894
|
+
type="button"
|
|
2895
|
+
onClick={() => void handleRestoreConfigVersion(summary.id)}
|
|
2896
|
+
disabled={Boolean(restoringConfigVersionId)}
|
|
2897
|
+
className="shrink-0 rounded-[10px] border border-accent-bright/20 bg-accent-soft/30 px-3 py-2 text-[12px] font-700 text-accent-bright transition-all hover:bg-accent-soft disabled:opacity-50"
|
|
2898
|
+
style={{ fontFamily: 'inherit' }}
|
|
2899
|
+
>
|
|
2900
|
+
{restoringConfigVersionId === summary.id ? 'Restoring' : 'Restore'}
|
|
2901
|
+
</button>
|
|
2902
|
+
</div>
|
|
2903
|
+
))}
|
|
2904
|
+
</div>
|
|
2905
|
+
)}
|
|
2906
|
+
</SectionCard>
|
|
2907
|
+
)}
|
|
2908
|
+
|
|
2790
2909
|
<SectionCard
|
|
2791
2910
|
title="Utilities"
|
|
2792
2911
|
description="Import and export agents."
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { buildAgentConfigVersionSummary, formatAgentConfigVersionAge } from './agent-config-history'
|
|
5
|
+
import type { ConfigVersion } from '@/types/config-version'
|
|
6
|
+
|
|
7
|
+
function makeVersion(overrides: Partial<ConfigVersion> = {}): ConfigVersion {
|
|
8
|
+
return {
|
|
9
|
+
id: 'version_1',
|
|
10
|
+
entityKind: 'agent',
|
|
11
|
+
entityId: 'agent_1',
|
|
12
|
+
actor: 'user',
|
|
13
|
+
approvalId: null,
|
|
14
|
+
note: null,
|
|
15
|
+
createdAt: 1_700_000_000_000,
|
|
16
|
+
snapshot: {
|
|
17
|
+
name: 'Release Agent',
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
model: 'gpt-5.2',
|
|
20
|
+
},
|
|
21
|
+
...overrides,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('formatAgentConfigVersionAge formats compact relative ages', () => {
|
|
26
|
+
assert.equal(formatAgentConfigVersionAge(1_700_000_000_000, 1_700_000_030_000), 'just now')
|
|
27
|
+
assert.equal(formatAgentConfigVersionAge(1_700_000_000_000, 1_700_000_300_000), '5m ago')
|
|
28
|
+
assert.equal(formatAgentConfigVersionAge(1_700_000_000_000, 1_700_010_800_000), '3h ago')
|
|
29
|
+
assert.equal(formatAgentConfigVersionAge(1_700_000_000_000, 1_700_259_200_000), '3d ago')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('buildAgentConfigVersionSummary surfaces useful agent snapshot labels', () => {
|
|
33
|
+
const summary = buildAgentConfigVersionSummary(makeVersion(), 1_700_000_300_000)
|
|
34
|
+
|
|
35
|
+
assert.equal(summary.id, 'version_1')
|
|
36
|
+
assert.equal(summary.title, 'Release Agent')
|
|
37
|
+
assert.equal(summary.subtitle, 'openai / gpt-5.2')
|
|
38
|
+
assert.equal(summary.meta, '5m ago by user')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('buildAgentConfigVersionSummary handles sparse snapshots', () => {
|
|
42
|
+
const summary = buildAgentConfigVersionSummary(makeVersion({
|
|
43
|
+
actor: 'system',
|
|
44
|
+
snapshot: {},
|
|
45
|
+
}), 1_700_000_300_000)
|
|
46
|
+
|
|
47
|
+
assert.equal(summary.title, 'Previous agent settings')
|
|
48
|
+
assert.equal(summary.subtitle, 'No provider snapshot')
|
|
49
|
+
assert.equal(summary.meta, '5m ago by system')
|
|
50
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ConfigVersion } from '@/types/config-version'
|
|
2
|
+
|
|
3
|
+
export interface AgentConfigVersionSummary {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
subtitle: string
|
|
7
|
+
meta: string
|
|
8
|
+
createdAt: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function snapshotString(snapshot: Record<string, unknown>, key: string): string {
|
|
12
|
+
const value = snapshot[key]
|
|
13
|
+
return typeof value === 'string' && value.trim() ? value.trim() : ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatAgentConfigVersionAge(createdAt: number, now = Date.now()): string {
|
|
17
|
+
const ageMs = Math.max(0, now - createdAt)
|
|
18
|
+
const seconds = Math.floor(ageMs / 1000)
|
|
19
|
+
if (seconds < 60) return 'just now'
|
|
20
|
+
const minutes = Math.floor(seconds / 60)
|
|
21
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
22
|
+
const hours = Math.floor(minutes / 60)
|
|
23
|
+
if (hours < 24) return `${hours}h ago`
|
|
24
|
+
const days = Math.floor(hours / 24)
|
|
25
|
+
return `${days}d ago`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildAgentConfigVersionSummary(
|
|
29
|
+
version: ConfigVersion,
|
|
30
|
+
now = Date.now(),
|
|
31
|
+
): AgentConfigVersionSummary {
|
|
32
|
+
const snapshot = version.snapshot || {}
|
|
33
|
+
const name = snapshotString(snapshot, 'name')
|
|
34
|
+
const provider = snapshotString(snapshot, 'provider')
|
|
35
|
+
const model = snapshotString(snapshot, 'model')
|
|
36
|
+
const title = name || 'Previous agent settings'
|
|
37
|
+
const subtitle = provider && model
|
|
38
|
+
? `${provider} / ${model}`
|
|
39
|
+
: provider || model || 'No provider snapshot'
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: version.id,
|
|
43
|
+
title,
|
|
44
|
+
subtitle,
|
|
45
|
+
meta: `${formatAgentConfigVersionAge(version.createdAt, now)} by ${version.actor || 'user'}`,
|
|
46
|
+
createdAt: version.createdAt,
|
|
47
|
+
}
|
|
48
|
+
}
|