@swarmclawai/swarmclaw 0.4.5 → 0.5.0
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 +8 -2
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +5 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +23 -5
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/connectors/[id]/route.ts +7 -4
- package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +4 -3
- package/src/app/api/openclaw/discover/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +3 -2
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/api/secrets/[id]/route.ts +1 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +18 -1
- package/src/app/api/sessions/[id]/route.ts +12 -3
- package/src/app/api/sessions/route.ts +6 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/cli/index.js +33 -1
- package/src/cli/spec.js +40 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +82 -3
- package/src/components/agents/agent-sheet.tsx +31 -0
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +14 -2
- package/src/components/chat/chat-header.tsx +168 -4
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +89 -16
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/projects/project-list.tsx +1 -0
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/settings/settings-page.tsx +1 -0
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +42 -5
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/providers/openclaw.ts +17 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/chat-execution.ts +44 -2
- package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
- package/src/lib/server/connectors/bluebubbles.ts +5 -2
- package/src/lib/server/connectors/googlechat.ts +14 -10
- package/src/lib/server/connectors/manager.ts +37 -15
- package/src/lib/server/connectors/openclaw.ts +1 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/main-agent-loop.ts +2 -3
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +11 -10
- package/src/lib/server/queue.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +4 -4
- package/src/lib/server/session-tools/delegate.ts +19 -3
- package/src/lib/server/session-tools/file.ts +20 -20
- package/src/lib/server/session-tools/index.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
- package/src/lib/server/session-tools/sandbox.ts +2 -2
- package/src/lib/server/session-tools/search-providers.ts +13 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +8 -8
- package/src/lib/server/storage.ts +62 -11
- package/src/lib/server/stream-agent-chat.ts +24 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/stores/use-app-store.ts +53 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +153 -5
- package/src/types/index.ts +127 -1
- package/tsconfig.json +13 -4
|
@@ -3,6 +3,7 @@ import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@
|
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { enqueueSessionRun } from '@/lib/server/session-run-manager'
|
|
5
5
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
6
|
+
import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main-session'
|
|
6
7
|
|
|
7
8
|
function buildSessionAwakeningPrompt(user: string | null | undefined): string {
|
|
8
9
|
const displayName = typeof user === 'string' && user.trim() ? user.trim() : 'there'
|
|
@@ -22,6 +23,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
22
23
|
const updates = await req.json()
|
|
23
24
|
const sessions = loadSessions()
|
|
24
25
|
if (!sessions[id]) return notFound()
|
|
26
|
+
const wasProtectedMain = isProtectedMainSession(sessions[id])
|
|
25
27
|
const hadMessagesBefore = Array.isArray(sessions[id].messages) && sessions[id].messages.length > 0
|
|
26
28
|
|
|
27
29
|
const agentIdUpdateProvided = updates.agentId !== undefined
|
|
@@ -33,7 +35,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
33
35
|
|
|
34
36
|
const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
|
|
35
37
|
|
|
36
|
-
if (updates.name !== undefined)
|
|
38
|
+
if (updates.name !== undefined) {
|
|
39
|
+
const nextName = typeof updates.name === 'string' ? updates.name.trim() : String(updates.name || '')
|
|
40
|
+
if (wasProtectedMain && nextName !== '__main__') {
|
|
41
|
+
return new NextResponse('Cannot rename main chat session', { status: 400 })
|
|
42
|
+
}
|
|
43
|
+
sessions[id].name = updates.name
|
|
44
|
+
}
|
|
37
45
|
if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
|
|
38
46
|
if (updates.provider !== undefined) sessions[id].provider = updates.provider
|
|
39
47
|
else if (agentIdUpdateProvided && linkedAgent?.provider) sessions[id].provider = linkedAgent.provider
|
|
@@ -62,8 +70,9 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
62
70
|
if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
|
|
63
71
|
if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
|
|
64
72
|
if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
|
|
73
|
+
ensureMainSessionFlag(sessions[id])
|
|
65
74
|
|
|
66
|
-
const shouldKickoffAwakening = sessions[id]
|
|
75
|
+
const shouldKickoffAwakening = isProtectedMainSession(sessions[id])
|
|
67
76
|
&& agentIdUpdateProvided
|
|
68
77
|
&& !!sessions[id].agentId
|
|
69
78
|
&& !hadMessagesBefore
|
|
@@ -92,7 +101,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
92
101
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
93
102
|
const { id } = await params
|
|
94
103
|
const sessions = loadSessions()
|
|
95
|
-
if (sessions[id]
|
|
104
|
+
if (isProtectedMainSession(sessions[id])) {
|
|
96
105
|
return new NextResponse('Cannot delete main chat session', { status: 403 })
|
|
97
106
|
}
|
|
98
107
|
if (active.has(id)) {
|
|
@@ -7,6 +7,7 @@ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
|
7
7
|
import { notify } from '@/lib/server/ws-hub'
|
|
8
8
|
import { getSessionRunState } from '@/lib/server/session-run-manager'
|
|
9
9
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
10
|
+
import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main-session'
|
|
10
11
|
export const dynamic = 'force-dynamic'
|
|
11
12
|
|
|
12
13
|
|
|
@@ -27,16 +28,18 @@ export async function DELETE(req: Request) {
|
|
|
27
28
|
return new NextResponse('Missing ids', { status: 400 })
|
|
28
29
|
}
|
|
29
30
|
const sessions = loadSessions()
|
|
31
|
+
let deleted = 0
|
|
30
32
|
for (const id of ids) {
|
|
31
|
-
if (sessions[id]
|
|
33
|
+
if (isProtectedMainSession(sessions[id])) continue
|
|
32
34
|
if (active.has(id)) {
|
|
33
35
|
try { active.get(id).kill() } catch {}
|
|
34
36
|
active.delete(id)
|
|
35
37
|
}
|
|
36
38
|
deleteSession(id)
|
|
39
|
+
deleted += 1
|
|
37
40
|
}
|
|
38
41
|
notify('sessions')
|
|
39
|
-
return NextResponse.json({ deleted: ids.length })
|
|
42
|
+
return NextResponse.json({ deleted, requested: ids.length })
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
export async function POST(req: Request) {
|
|
@@ -86,6 +89,7 @@ export async function POST(req: Request) {
|
|
|
86
89
|
heartbeatEnabled: body.heartbeatEnabled ?? null,
|
|
87
90
|
heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
|
|
88
91
|
}
|
|
92
|
+
ensureMainSessionFlag(sessions[id])
|
|
89
93
|
saveSessions(sessions)
|
|
90
94
|
notify('sessions')
|
|
91
95
|
return NextResponse.json(sessions[id])
|
package/src/app/globals.css
CHANGED
|
@@ -79,6 +79,9 @@
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
:root {
|
|
82
|
+
--font-dm-sans: 'Segoe UI';
|
|
83
|
+
--font-sora: 'Segoe UI';
|
|
84
|
+
--font-jetbrains-mono: 'SF Mono';
|
|
82
85
|
--radius: 0.625rem;
|
|
83
86
|
--background: #08080d;
|
|
84
87
|
--foreground: #e2e2ec;
|
|
@@ -203,6 +206,17 @@ body {
|
|
|
203
206
|
100% { background-position: 0% 50%; }
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
/* AI avatar mood animations */
|
|
210
|
+
@keyframes ai-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.15); } }
|
|
211
|
+
@keyframes ai-glow { 0%,100% { box-shadow: 0 0 0 0 rgba(99,102,241,0); } 50% { box-shadow: 0 0 12px 4px rgba(99,102,241,0.35); } }
|
|
212
|
+
@keyframes ai-shake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-3px); } 40% { transform: translateX(3px); } 60% { transform: translateX(-2px); } 80% { transform: translateX(2px); } }
|
|
213
|
+
@keyframes ai-bounce { 0%,100% { transform: scale(1); } 40% { transform: scale(1.25); } 60% { transform: scale(0.95); } }
|
|
214
|
+
|
|
215
|
+
.ai-mood-pulse { animation: ai-pulse 2s ease-in-out infinite; }
|
|
216
|
+
.ai-mood-glow { animation: ai-glow 1.8s ease-in-out infinite; }
|
|
217
|
+
.ai-mood-shake { animation: ai-shake 0.5s ease-in-out; }
|
|
218
|
+
.ai-mood-bounce { animation: ai-bounce 0.6s ease-out; }
|
|
219
|
+
|
|
206
220
|
/* ===== Markdown content ===== */
|
|
207
221
|
.msg-content pre {
|
|
208
222
|
background: #0a0a12 !important;
|
package/src/app/layout.tsx
CHANGED
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
import type { Metadata, Viewport } from "next"
|
|
2
|
-
import { Sora, DM_Sans, JetBrains_Mono } from "next/font/google"
|
|
3
2
|
import { TooltipProvider } from "@/components/ui/tooltip"
|
|
4
3
|
import { Toaster } from "@/components/ui/sonner"
|
|
5
4
|
import "./globals.css"
|
|
6
5
|
|
|
7
|
-
const sora = Sora({
|
|
8
|
-
variable: "--font-sora",
|
|
9
|
-
subsets: ["latin"],
|
|
10
|
-
weight: ["400", "500", "600", "700", "800"],
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
const dmSans = DM_Sans({
|
|
14
|
-
variable: "--font-dm-sans",
|
|
15
|
-
subsets: ["latin"],
|
|
16
|
-
weight: ["400", "500", "600", "700"],
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
const jetbrainsMono = JetBrains_Mono({
|
|
20
|
-
variable: "--font-jetbrains-mono",
|
|
21
|
-
subsets: ["latin"],
|
|
22
|
-
weight: ["400", "500"],
|
|
23
|
-
})
|
|
24
|
-
|
|
25
6
|
export const metadata: Metadata = {
|
|
26
7
|
title: "SwarmClaw",
|
|
27
8
|
description: "AI agent orchestration dashboard with multi-provider support",
|
|
@@ -34,6 +15,10 @@ export const viewport: Viewport = {
|
|
|
34
15
|
viewportFit: "cover",
|
|
35
16
|
}
|
|
36
17
|
|
|
18
|
+
// Avoid static prerendering for the app shell. This prevents flaky
|
|
19
|
+
// Turbopack prerender failures seen in detached fresh-install builds.
|
|
20
|
+
export const dynamic = "force-dynamic"
|
|
21
|
+
|
|
37
22
|
export default function RootLayout({
|
|
38
23
|
children,
|
|
39
24
|
}: Readonly<{
|
|
@@ -41,7 +26,7 @@ export default function RootLayout({
|
|
|
41
26
|
}>) {
|
|
42
27
|
return (
|
|
43
28
|
<html lang="en" className="dark">
|
|
44
|
-
<body className=
|
|
29
|
+
<body className="antialiased" cz-shortcut-listen="true">
|
|
45
30
|
<TooltipProvider>
|
|
46
31
|
{children}
|
|
47
32
|
<Toaster />
|
package/src/cli/index.js
CHANGED
|
@@ -17,6 +17,9 @@ const COMMAND_GROUPS = [
|
|
|
17
17
|
cmd('create', 'POST', '/agents', 'Create an agent', { expectsJsonBody: true }),
|
|
18
18
|
cmd('update', 'PUT', '/agents/:id', 'Update an agent', { expectsJsonBody: true }),
|
|
19
19
|
cmd('delete', 'DELETE', '/agents/:id', 'Delete an agent'),
|
|
20
|
+
cmd('trash', 'GET', '/agents/trash', 'List trashed agents'),
|
|
21
|
+
cmd('restore', 'POST', '/agents/trash', 'Restore a trashed agent', { expectsJsonBody: true }),
|
|
22
|
+
cmd('purge', 'DELETE', '/agents/trash', 'Permanently delete a trashed agent', { expectsJsonBody: true }),
|
|
20
23
|
cmd('thread', 'POST', '/agents/:id/thread', 'Get or create agent thread session'),
|
|
21
24
|
],
|
|
22
25
|
},
|
|
@@ -205,10 +208,36 @@ const COMMAND_GROUPS = [
|
|
|
205
208
|
},
|
|
206
209
|
{
|
|
207
210
|
name: 'openclaw',
|
|
208
|
-
description: 'OpenClaw discovery and
|
|
211
|
+
description: 'OpenClaw discovery, gateway control, and runtime APIs',
|
|
209
212
|
commands: [
|
|
210
213
|
cmd('discover', 'GET', '/openclaw/discover', 'Discover OpenClaw gateways'),
|
|
211
214
|
cmd('directory', 'GET', '/openclaw/directory', 'List directory entries from running OpenClaw connectors'),
|
|
215
|
+
cmd('gateway-status', 'GET', '/openclaw/gateway', 'Check OpenClaw gateway connection status'),
|
|
216
|
+
cmd('gateway', 'POST', '/openclaw/gateway', 'Call OpenClaw gateway RPC/control action', { expectsJsonBody: true }),
|
|
217
|
+
cmd('config-sync', 'GET', '/openclaw/config-sync', 'Detect OpenClaw gateway config issues'),
|
|
218
|
+
cmd('config-sync-repair', 'POST', '/openclaw/config-sync', 'Repair a detected OpenClaw config issue', { expectsJsonBody: true }),
|
|
219
|
+
cmd('approvals', 'GET', '/openclaw/approvals', 'List pending OpenClaw execution approvals'),
|
|
220
|
+
cmd('approvals-resolve', 'POST', '/openclaw/approvals', 'Resolve an OpenClaw execution approval', { expectsJsonBody: true }),
|
|
221
|
+
cmd('cron', 'GET', '/openclaw/cron', 'List OpenClaw cron jobs'),
|
|
222
|
+
cmd('cron-action', 'POST', '/openclaw/cron', 'Create/run/remove OpenClaw cron jobs', { expectsJsonBody: true }),
|
|
223
|
+
cmd('agent-files', 'GET', '/openclaw/agent-files', 'Fetch OpenClaw agent files'),
|
|
224
|
+
cmd('agent-files-set', 'PUT', '/openclaw/agent-files', 'Save an OpenClaw agent file', { expectsJsonBody: true }),
|
|
225
|
+
cmd('dotenv-keys', 'GET', '/openclaw/dotenv-keys', 'List gateway .env keys'),
|
|
226
|
+
cmd('exec-config', 'GET', '/openclaw/exec-config', 'Fetch OpenClaw exec approval config'),
|
|
227
|
+
cmd('exec-config-set', 'PUT', '/openclaw/exec-config', 'Save OpenClaw exec approval config', { expectsJsonBody: true }),
|
|
228
|
+
cmd('history-preview', 'GET', '/openclaw/history', 'Preview OpenClaw session history'),
|
|
229
|
+
cmd('history-merge', 'POST', '/openclaw/history', 'Merge OpenClaw session history into local session', { expectsJsonBody: true }),
|
|
230
|
+
cmd('media', 'GET', '/openclaw/media', 'Proxy OpenClaw media/file content'),
|
|
231
|
+
cmd('models', 'GET', '/openclaw/models', 'List allowed OpenClaw models'),
|
|
232
|
+
cmd('permissions', 'GET', '/openclaw/permissions', 'Get OpenClaw permission preset/config'),
|
|
233
|
+
cmd('permissions-set', 'PUT', '/openclaw/permissions', 'Apply OpenClaw permission preset', { expectsJsonBody: true }),
|
|
234
|
+
cmd('sandbox-env', 'GET', '/openclaw/sandbox-env', 'List OpenClaw sandbox env allowlist'),
|
|
235
|
+
cmd('sandbox-env-set', 'PUT', '/openclaw/sandbox-env', 'Update OpenClaw sandbox env allowlist', { expectsJsonBody: true }),
|
|
236
|
+
cmd('skills', 'GET', '/openclaw/skills', 'List OpenClaw skills and eligibility'),
|
|
237
|
+
cmd('skills-update', 'PATCH', '/openclaw/skills', 'Update OpenClaw skill state/config', { expectsJsonBody: true }),
|
|
238
|
+
cmd('skills-save', 'PUT', '/openclaw/skills', 'Save OpenClaw skill allowlist mode/config', { expectsJsonBody: true }),
|
|
239
|
+
cmd('skills-install', 'POST', '/openclaw/skills/install', 'Install OpenClaw skill dependencies', { expectsJsonBody: true }),
|
|
240
|
+
cmd('skills-remove', 'POST', '/openclaw/skills/remove', 'Remove OpenClaw skill', { expectsJsonBody: true }),
|
|
212
241
|
cmd('sync', 'POST', '/openclaw/sync', 'Run OpenClaw sync action', { expectsJsonBody: true }),
|
|
213
242
|
],
|
|
214
243
|
},
|
|
@@ -303,6 +332,9 @@ const COMMAND_GROUPS = [
|
|
|
303
332
|
defaultBody: { action: 'disable_all' },
|
|
304
333
|
}),
|
|
305
334
|
cmd('messages', 'GET', '/sessions/:id/messages', 'Get session messages'),
|
|
335
|
+
cmd('messages-update', 'PUT', '/sessions/:id/messages', 'Update session message metadata (e.g. bookmark)', { expectsJsonBody: true }),
|
|
336
|
+
cmd('fork', 'POST', '/sessions/:id/fork', 'Fork session from a specific message index', { expectsJsonBody: true }),
|
|
337
|
+
cmd('edit-resend', 'POST', '/sessions/:id/edit-resend', 'Edit and resend from a specific message index', { expectsJsonBody: true }),
|
|
306
338
|
cmd('main-loop', 'GET', '/sessions/:id/main-loop', 'Get main mission loop state'),
|
|
307
339
|
cmd('main-loop-action', 'POST', '/sessions/:id/main-loop', 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', {
|
|
308
340
|
expectsJsonBody: true,
|
package/src/cli/spec.js
CHANGED
|
@@ -7,6 +7,9 @@ const COMMAND_GROUPS = {
|
|
|
7
7
|
create: { description: 'Create an agent', method: 'POST', path: '/agents' },
|
|
8
8
|
update: { description: 'Update an agent', method: 'PUT', path: '/agents/:id', params: ['id'] },
|
|
9
9
|
delete: { description: 'Delete an agent', method: 'DELETE', path: '/agents/:id', params: ['id'] },
|
|
10
|
+
trash: { description: 'List trashed agents', method: 'GET', path: '/agents/trash' },
|
|
11
|
+
restore: { description: 'Restore a trashed agent', method: 'POST', path: '/agents/trash' },
|
|
12
|
+
purge: { description: 'Permanently delete a trashed agent', method: 'DELETE', path: '/agents/trash' },
|
|
10
13
|
},
|
|
11
14
|
},
|
|
12
15
|
auth: {
|
|
@@ -130,6 +133,40 @@ const COMMAND_GROUPS = {
|
|
|
130
133
|
graph: { description: 'Get orchestrator graph structure', method: 'GET', path: '/orchestrator/graph' },
|
|
131
134
|
},
|
|
132
135
|
},
|
|
136
|
+
openclaw: {
|
|
137
|
+
description: 'OpenClaw discovery, gateway control, and runtime APIs',
|
|
138
|
+
commands: {
|
|
139
|
+
discover: { description: 'Discover OpenClaw gateways', method: 'GET', path: '/openclaw/discover' },
|
|
140
|
+
directory: { description: 'List directory entries from running OpenClaw connectors', method: 'GET', path: '/openclaw/directory' },
|
|
141
|
+
'gateway-status': { description: 'Check OpenClaw gateway connection status', method: 'GET', path: '/openclaw/gateway' },
|
|
142
|
+
gateway: { description: 'Call OpenClaw gateway RPC/control action', method: 'POST', path: '/openclaw/gateway' },
|
|
143
|
+
'config-sync': { description: 'Detect OpenClaw gateway config issues', method: 'GET', path: '/openclaw/config-sync' },
|
|
144
|
+
'config-sync-repair': { description: 'Repair a detected OpenClaw config issue', method: 'POST', path: '/openclaw/config-sync' },
|
|
145
|
+
approvals: { description: 'List pending OpenClaw execution approvals', method: 'GET', path: '/openclaw/approvals' },
|
|
146
|
+
'approvals-resolve': { description: 'Resolve an OpenClaw execution approval', method: 'POST', path: '/openclaw/approvals' },
|
|
147
|
+
cron: { description: 'List OpenClaw cron jobs', method: 'GET', path: '/openclaw/cron' },
|
|
148
|
+
'cron-action': { description: 'Create/run/remove OpenClaw cron jobs', method: 'POST', path: '/openclaw/cron' },
|
|
149
|
+
'agent-files': { description: 'Fetch OpenClaw agent files', method: 'GET', path: '/openclaw/agent-files' },
|
|
150
|
+
'agent-files-set': { description: 'Save an OpenClaw agent file', method: 'PUT', path: '/openclaw/agent-files' },
|
|
151
|
+
'dotenv-keys': { description: 'List gateway .env keys', method: 'GET', path: '/openclaw/dotenv-keys' },
|
|
152
|
+
'exec-config': { description: 'Fetch OpenClaw exec approval config', method: 'GET', path: '/openclaw/exec-config' },
|
|
153
|
+
'exec-config-set': { description: 'Save OpenClaw exec approval config', method: 'PUT', path: '/openclaw/exec-config' },
|
|
154
|
+
'history-preview': { description: 'Preview OpenClaw session history', method: 'GET', path: '/openclaw/history' },
|
|
155
|
+
'history-merge': { description: 'Merge OpenClaw session history into local session', method: 'POST', path: '/openclaw/history' },
|
|
156
|
+
media: { description: 'Proxy OpenClaw media/file content', method: 'GET', path: '/openclaw/media' },
|
|
157
|
+
models: { description: 'List allowed OpenClaw models', method: 'GET', path: '/openclaw/models' },
|
|
158
|
+
permissions: { description: 'Get OpenClaw permission preset/config', method: 'GET', path: '/openclaw/permissions' },
|
|
159
|
+
'permissions-set': { description: 'Apply OpenClaw permission preset', method: 'PUT', path: '/openclaw/permissions' },
|
|
160
|
+
'sandbox-env': { description: 'List OpenClaw sandbox env allowlist', method: 'GET', path: '/openclaw/sandbox-env' },
|
|
161
|
+
'sandbox-env-set': { description: 'Update OpenClaw sandbox env allowlist', method: 'PUT', path: '/openclaw/sandbox-env' },
|
|
162
|
+
skills: { description: 'List OpenClaw skills and eligibility', method: 'GET', path: '/openclaw/skills' },
|
|
163
|
+
'skills-update': { description: 'Update OpenClaw skill state/config', method: 'PATCH', path: '/openclaw/skills' },
|
|
164
|
+
'skills-save': { description: 'Save OpenClaw skill allowlist mode/config', method: 'PUT', path: '/openclaw/skills' },
|
|
165
|
+
'skills-install': { description: 'Install OpenClaw skill dependencies', method: 'POST', path: '/openclaw/skills/install' },
|
|
166
|
+
'skills-remove': { description: 'Remove OpenClaw skill', method: 'POST', path: '/openclaw/skills/remove' },
|
|
167
|
+
sync: { description: 'Run OpenClaw sync action', method: 'POST', path: '/openclaw/sync' },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
133
170
|
plugins: {
|
|
134
171
|
description: 'Plugin listing/config/install',
|
|
135
172
|
commands: {
|
|
@@ -187,6 +224,9 @@ const COMMAND_GROUPS = {
|
|
|
187
224
|
'delete-many': { description: 'Delete multiple sessions (body: {"ids":[...]})', method: 'DELETE', path: '/sessions' },
|
|
188
225
|
'heartbeat-disable-all': { description: 'Disable all session heartbeats and cancel queued heartbeat runs', method: 'POST', path: '/sessions/heartbeat' },
|
|
189
226
|
messages: { description: 'Get session message history', method: 'GET', path: '/sessions/:id/messages', params: ['id'] },
|
|
227
|
+
'messages-update': { description: 'Update session message metadata (e.g. bookmark)', method: 'PUT', path: '/sessions/:id/messages', params: ['id'] },
|
|
228
|
+
fork: { description: 'Fork session from a specific message index', method: 'POST', path: '/sessions/:id/fork', params: ['id'] },
|
|
229
|
+
'edit-resend': { description: 'Edit and resend from a specific message index', method: 'POST', path: '/sessions/:id/edit-resend', params: ['id'] },
|
|
190
230
|
'main-loop': { description: 'Get main mission loop state for a session', method: 'GET', path: '/sessions/:id/main-loop', params: ['id'] },
|
|
191
231
|
'main-loop-action': { description: 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', method: 'POST', path: '/sessions/:id/main-loop', params: ['id'] },
|
|
192
232
|
chat: { description: 'Send chat message (SSE stream)', method: 'POST', path: '/sessions/:id/chat', params: ['id'], stream: true, waitable: true },
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
import multiavatar from '@multiavatar/multiavatar'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
seed?: string | null
|
|
8
|
+
name: string
|
|
9
|
+
size?: number
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AgentAvatar({ seed, name, size = 32, className = '' }: Props) {
|
|
14
|
+
const svgHtml = useMemo(() => {
|
|
15
|
+
if (!seed) return null
|
|
16
|
+
return multiavatar(seed)
|
|
17
|
+
}, [seed])
|
|
18
|
+
|
|
19
|
+
if (svgHtml) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={`shrink-0 rounded-full overflow-hidden ${className}`}
|
|
23
|
+
style={{ width: size, height: size }}
|
|
24
|
+
dangerouslySetInnerHTML={{ __html: svgHtml }}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fallback: initials
|
|
30
|
+
const initials = name
|
|
31
|
+
.split(/\s+/)
|
|
32
|
+
.slice(0, 2)
|
|
33
|
+
.map((w) => w[0] || '')
|
|
34
|
+
.join('')
|
|
35
|
+
.toUpperCase()
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={`shrink-0 rounded-full flex items-center justify-center bg-accent-soft text-accent-bright font-600 ${className}`}
|
|
40
|
+
style={{ width: size, height: size, fontSize: size * 0.38 }}
|
|
41
|
+
>
|
|
42
|
+
{initials || '?'}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -15,14 +15,17 @@ import {
|
|
|
15
15
|
DropdownMenuTrigger,
|
|
16
16
|
} from '@/components/ui/dropdown-menu'
|
|
17
17
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
18
|
+
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
19
|
+
import { AgentAvatar } from './agent-avatar'
|
|
18
20
|
|
|
19
21
|
interface Props {
|
|
20
22
|
agent: Agent
|
|
21
23
|
isDefault?: boolean
|
|
24
|
+
isRunning?: boolean
|
|
22
25
|
onSetDefault?: (id: string) => void
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
|
|
28
|
+
export function AgentCard({ agent, isDefault, isRunning, onSetDefault }: Props) {
|
|
26
29
|
const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
|
|
27
30
|
const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
28
31
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
@@ -34,6 +37,8 @@ export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
|
|
|
34
37
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
35
38
|
const [taskInput, setTaskInput] = useState('')
|
|
36
39
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
40
|
+
const approvals = useApprovalStore((s) => s.approvals)
|
|
41
|
+
const pendingApprovalCount = Object.values(approvals).filter((a) => a.agentId === agent.id).length
|
|
37
42
|
|
|
38
43
|
const handleClick = () => {
|
|
39
44
|
setEditingAgentId(agent.id)
|
|
@@ -111,13 +116,22 @@ export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
|
|
|
111
116
|
onClick={() => setConfirmDelete(true)}
|
|
112
117
|
className="text-red-400 focus:text-red-400"
|
|
113
118
|
>
|
|
114
|
-
|
|
119
|
+
Move to Trash
|
|
115
120
|
</DropdownMenuItem>
|
|
116
121
|
</DropdownMenuContent>
|
|
117
122
|
</DropdownMenu>
|
|
118
123
|
|
|
119
124
|
<div className="flex items-center gap-2.5">
|
|
125
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={28} />
|
|
126
|
+
{isRunning && (
|
|
127
|
+
<span className="shrink-0 w-2 h-2 rounded-full bg-emerald-400" style={{ animation: 'pulse 2s ease infinite' }} title="Running" />
|
|
128
|
+
)}
|
|
120
129
|
<span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{agent.name}</span>
|
|
130
|
+
{pendingApprovalCount > 0 && (
|
|
131
|
+
<span className="shrink-0 inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-red-500 text-white text-[10px] font-700">
|
|
132
|
+
{pendingApprovalCount}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
121
135
|
{isDefault && (
|
|
122
136
|
<span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-accent-bright bg-accent-soft px-2 py-0.5 rounded-[6px]">
|
|
123
137
|
default
|
|
@@ -207,9 +221,9 @@ export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
|
|
|
207
221
|
|
|
208
222
|
<ConfirmDialog
|
|
209
223
|
open={confirmDelete}
|
|
210
|
-
title="
|
|
211
|
-
message={`
|
|
212
|
-
confirmLabel="
|
|
224
|
+
title="Move to Trash"
|
|
225
|
+
message={`Move "${agent.name}" to trash? You can restore it later from the trash.`}
|
|
226
|
+
confirmLabel="Move to Trash"
|
|
213
227
|
danger
|
|
214
228
|
onConfirm={handleDelete}
|
|
215
229
|
onCancel={() => setConfirmDelete(false)}
|
|
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
6
|
import { fetchMessages } from '@/lib/sessions'
|
|
7
7
|
import type { Agent, Session } from '@/types'
|
|
8
|
+
import { AgentAvatar } from './agent-avatar'
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
inSidebar?: boolean
|
|
@@ -130,32 +131,38 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
130
131
|
style={{ fontFamily: 'inherit' }}
|
|
131
132
|
>
|
|
132
133
|
<div className="flex items-center gap-2.5">
|
|
133
|
-
{/*
|
|
134
|
-
<div className=
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</span>
|
|
140
|
-
{/* Provider badge */}
|
|
141
|
-
<span className="text-[10px] text-text-3/60 font-mono shrink-0">
|
|
142
|
-
{agent.model ? agent.model.split('/').pop()?.split(':')[0] : agent.provider}
|
|
143
|
-
</span>
|
|
144
|
-
</div>
|
|
145
|
-
{isTyping ? (
|
|
146
|
-
<div className="text-[12px] text-accent-bright/70 mt-1 pl-[18px] flex items-center gap-1.5">
|
|
147
|
-
<span className="flex gap-0.5">
|
|
148
|
-
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:0ms]" />
|
|
149
|
-
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
|
|
150
|
-
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
|
|
151
|
-
</span>
|
|
152
|
-
Typing...
|
|
134
|
+
{/* Avatar with status dot */}
|
|
135
|
+
<div className="relative shrink-0">
|
|
136
|
+
<AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={36} />
|
|
137
|
+
<div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-bg ${
|
|
138
|
+
isWorking ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]' : 'bg-text-3/30'
|
|
139
|
+
}`} />
|
|
153
140
|
</div>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
141
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
142
|
+
<div className="flex items-center gap-2">
|
|
143
|
+
<span className="font-display text-[13.5px] font-600 truncate flex-1 tracking-[-0.01em]">
|
|
144
|
+
{agent.name}
|
|
145
|
+
</span>
|
|
146
|
+
<span className="text-[10px] text-text-3/60 font-mono shrink-0">
|
|
147
|
+
{agent.model ? agent.model.split('/').pop()?.split(':')[0] : agent.provider}
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
{isTyping ? (
|
|
151
|
+
<div className="text-[12px] text-accent-bright/70 mt-0.5 flex items-center gap-1.5">
|
|
152
|
+
<span className="flex gap-0.5">
|
|
153
|
+
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:0ms]" />
|
|
154
|
+
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
|
|
155
|
+
<span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
|
|
156
|
+
</span>
|
|
157
|
+
Typing...
|
|
158
|
+
</div>
|
|
159
|
+
) : preview ? (
|
|
160
|
+
<div className="text-[12px] text-text-3/70 mt-0.5 truncate">
|
|
161
|
+
{preview}
|
|
162
|
+
</div>
|
|
163
|
+
) : null}
|
|
157
164
|
</div>
|
|
158
|
-
|
|
165
|
+
</div>
|
|
159
166
|
</button>
|
|
160
167
|
)
|
|
161
168
|
})}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import { PersonalityBuilder } from './personality-builder'
|
|
6
|
+
|
|
7
|
+
const FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md', 'MEMORY.md', 'AGENTS.md'] as const
|
|
8
|
+
const GUIDED_FILES = new Set(['SOUL.md', 'IDENTITY.md', 'USER.md'])
|
|
9
|
+
|
|
10
|
+
interface FileState {
|
|
11
|
+
content: string
|
|
12
|
+
original: string
|
|
13
|
+
loading: boolean
|
|
14
|
+
saving: boolean
|
|
15
|
+
error?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
agentId: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function AgentFilesEditor({ agentId }: Props) {
|
|
23
|
+
const [activeTab, setActiveTab] = useState<string>(FILES[0])
|
|
24
|
+
const [files, setFiles] = useState<Record<string, FileState>>({})
|
|
25
|
+
const [guidedMode, setGuidedMode] = useState(false)
|
|
26
|
+
|
|
27
|
+
const loadFiles = useCallback(async () => {
|
|
28
|
+
const initial: Record<string, FileState> = {}
|
|
29
|
+
for (const f of FILES) {
|
|
30
|
+
initial[f] = { content: '', original: '', loading: true, saving: false }
|
|
31
|
+
}
|
|
32
|
+
setFiles(initial)
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await api<Record<string, { content: string; error?: string }>>('GET', `/openclaw/agent-files?agentId=${agentId}`)
|
|
36
|
+
setFiles((prev) => {
|
|
37
|
+
const next = { ...prev }
|
|
38
|
+
for (const [name, data] of Object.entries(result)) {
|
|
39
|
+
next[name] = {
|
|
40
|
+
content: data.content,
|
|
41
|
+
original: data.content,
|
|
42
|
+
loading: false,
|
|
43
|
+
saving: false,
|
|
44
|
+
error: data.error,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return next
|
|
48
|
+
})
|
|
49
|
+
} catch (err: unknown) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
51
|
+
setFiles((prev) => {
|
|
52
|
+
const next = { ...prev }
|
|
53
|
+
for (const f of FILES) {
|
|
54
|
+
next[f] = { ...next[f], loading: false, error: message }
|
|
55
|
+
}
|
|
56
|
+
return next
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}, [agentId])
|
|
60
|
+
|
|
61
|
+
useEffect(() => { loadFiles() }, [loadFiles])
|
|
62
|
+
|
|
63
|
+
const handleContentChange = (filename: string, content: string) => {
|
|
64
|
+
setFiles((prev) => ({
|
|
65
|
+
...prev,
|
|
66
|
+
[filename]: { ...prev[filename], content },
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleSave = async (filename: string) => {
|
|
71
|
+
const file = files[filename]
|
|
72
|
+
if (!file || file.content === file.original) return
|
|
73
|
+
|
|
74
|
+
setFiles((prev) => ({
|
|
75
|
+
...prev,
|
|
76
|
+
[filename]: { ...prev[filename], saving: true, error: undefined },
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await api('PUT', '/openclaw/agent-files', { agentId, filename, content: file.content })
|
|
81
|
+
setFiles((prev) => ({
|
|
82
|
+
...prev,
|
|
83
|
+
[filename]: { ...prev[filename], saving: false, original: prev[filename].content },
|
|
84
|
+
}))
|
|
85
|
+
} catch (err: unknown) {
|
|
86
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
87
|
+
setFiles((prev) => ({
|
|
88
|
+
...prev,
|
|
89
|
+
[filename]: { ...prev[filename], saving: false, error: message },
|
|
90
|
+
}))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleGuidedSave = (content: string) => {
|
|
95
|
+
handleContentChange(activeTab, content)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const current = files[activeTab]
|
|
99
|
+
const isDirty = current && current.content !== current.original
|
|
100
|
+
const showGuided = guidedMode && GUIDED_FILES.has(activeTab)
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="flex flex-col h-full">
|
|
104
|
+
{/* Tab bar */}
|
|
105
|
+
<div className="flex gap-0.5 px-2 pt-2 pb-1 overflow-x-auto shrink-0">
|
|
106
|
+
{FILES.map((f) => {
|
|
107
|
+
const fileState = files[f]
|
|
108
|
+
const dirty = fileState && fileState.content !== fileState.original
|
|
109
|
+
return (
|
|
110
|
+
<button
|
|
111
|
+
key={f}
|
|
112
|
+
onClick={() => setActiveTab(f)}
|
|
113
|
+
className={`px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all whitespace-nowrap
|
|
114
|
+
${activeTab === f
|
|
115
|
+
? 'bg-accent-soft text-accent-bright'
|
|
116
|
+
: 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
117
|
+
style={{ fontFamily: 'inherit' }}
|
|
118
|
+
>
|
|
119
|
+
{f.replace('.md', '')}
|
|
120
|
+
{dirty && <span className="ml-1 text-amber-400">*</span>}
|
|
121
|
+
</button>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Guided toggle for personality files */}
|
|
127
|
+
{GUIDED_FILES.has(activeTab) && (
|
|
128
|
+
<div className="px-3 py-1 shrink-0">
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => setGuidedMode(!guidedMode)}
|
|
131
|
+
className={`text-[10px] font-600 px-2 py-0.5 rounded-[6px] cursor-pointer transition-all border-none
|
|
132
|
+
${guidedMode ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.04] text-text-3 hover:text-text-2'}`}
|
|
133
|
+
style={{ fontFamily: 'inherit' }}
|
|
134
|
+
>
|
|
135
|
+
{guidedMode ? 'Raw Editor' : 'Guided Editor'}
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{/* Editor area */}
|
|
141
|
+
<div className="flex-1 min-h-0 px-2 pb-2 overflow-y-auto">
|
|
142
|
+
{current?.loading ? (
|
|
143
|
+
<div className="flex items-center justify-center h-full text-[13px] text-text-3/50">Loading...</div>
|
|
144
|
+
) : current?.error ? (
|
|
145
|
+
<div className="flex items-center justify-center h-full text-[13px] text-red-400">{current.error}</div>
|
|
146
|
+
) : showGuided ? (
|
|
147
|
+
<div className="p-2">
|
|
148
|
+
<PersonalityBuilder
|
|
149
|
+
agentId={agentId}
|
|
150
|
+
fileType={activeTab as 'IDENTITY.md' | 'USER.md' | 'SOUL.md'}
|
|
151
|
+
content={current?.content ?? ''}
|
|
152
|
+
onSave={handleGuidedSave}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<textarea
|
|
157
|
+
value={current?.content ?? ''}
|
|
158
|
+
onChange={(e) => handleContentChange(activeTab, e.target.value)}
|
|
159
|
+
className="w-full h-full resize-none rounded-[10px] border border-white/[0.06] bg-black/20 px-3 py-2.5
|
|
160
|
+
text-[13px] text-text font-mono leading-relaxed outline-none
|
|
161
|
+
placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors"
|
|
162
|
+
placeholder={`${activeTab} content...`}
|
|
163
|
+
style={{ fontFamily: 'ui-monospace, monospace' }}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Save bar */}
|
|
169
|
+
<div className="shrink-0 px-3 pb-2 flex items-center gap-2">
|
|
170
|
+
<button
|
|
171
|
+
onClick={() => handleSave(activeTab)}
|
|
172
|
+
disabled={!isDirty || current?.saving}
|
|
173
|
+
className="px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600
|
|
174
|
+
cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed transition-all hover:brightness-110"
|
|
175
|
+
style={{ fontFamily: 'inherit' }}
|
|
176
|
+
>
|
|
177
|
+
{current?.saving ? 'Saving...' : 'Save'}
|
|
178
|
+
</button>
|
|
179
|
+
{isDirty && (
|
|
180
|
+
<span className="text-[11px] text-amber-400/70">Unsaved changes</span>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|