@swarmclawai/swarmclaw 0.9.2 → 0.9.4
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 +12 -10
- package/bundled-skills/google-workspace/SKILL.md +2 -0
- package/package.json +1 -1
- package/src/app/agents/page.tsx +2 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +1 -1
- package/src/app/api/clawhub/install/route.ts +2 -0
- package/src/app/api/skills/[id]/route.ts +4 -0
- package/src/app/api/skills/route.ts +4 -0
- package/src/app/globals.css +28 -0
- package/src/app/home/page.tsx +11 -0
- package/src/app/settings/page.tsx +12 -5
- package/src/components/agents/agent-sheet.tsx +5 -5
- package/src/components/connectors/connector-list.tsx +2 -5
- package/src/components/logs/log-list.tsx +2 -5
- package/src/components/providers/provider-list.tsx +2 -5
- package/src/components/runs/run-list.tsx +2 -6
- package/src/components/schedules/schedule-list.tsx +7 -1
- package/src/components/ui/full-screen-loader.tsx +0 -29
- package/src/components/ui/page-loader.tsx +69 -0
- package/src/lib/runtime/runtime-loop.ts +21 -1
- package/src/lib/server/agents/agent-thread-session.test.ts +64 -0
- package/src/lib/server/agents/agent-thread-session.ts +1 -1
- package/src/lib/server/agents/main-agent-loop-advanced.test.ts +77 -0
- package/src/lib/server/agents/main-agent-loop.ts +259 -0
- package/src/lib/server/agents/orchestrator-lg.ts +12 -8
- package/src/lib/server/agents/orchestrator.ts +11 -7
- package/src/lib/server/chat-execution/chat-execution-advanced.test.ts +11 -10
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +116 -3
- package/src/lib/server/chat-execution/chat-execution-utils.test.ts +56 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +24 -0
- package/src/lib/server/chat-execution/chat-execution.ts +116 -29
- package/src/lib/server/chat-execution/chat-streaming-utils.ts +1 -38
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +67 -76
- package/src/lib/server/chat-execution/stream-agent-chat.ts +119 -110
- package/src/lib/server/chat-execution/stream-continuation.ts +1 -1
- package/src/lib/server/chatrooms/chatroom-helpers.test.ts +26 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +11 -8
- package/src/lib/server/connectors/contact-boundaries.ts +101 -0
- package/src/lib/server/connectors/manager.test.ts +504 -73
- package/src/lib/server/connectors/manager.ts +41 -10
- package/src/lib/server/connectors/session-consolidation.ts +2 -0
- package/src/lib/server/connectors/session-kind.ts +7 -0
- package/src/lib/server/connectors/session.test.ts +104 -0
- package/src/lib/server/connectors/session.ts +5 -2
- package/src/lib/server/identity-continuity.test.ts +4 -3
- package/src/lib/server/identity-continuity.ts +8 -4
- package/src/lib/server/memory/memory-policy.test.ts +5 -15
- package/src/lib/server/memory/memory-policy.ts +11 -41
- package/src/lib/server/memory/session-archive-memory.ts +2 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +46 -0
- package/src/lib/server/runtime/heartbeat-service.ts +5 -1
- package/src/lib/server/runtime/runtime-settings.test.ts +4 -4
- package/src/lib/server/runtime/runtime-settings.ts +4 -0
- package/src/lib/server/runtime/session-run-manager.ts +2 -0
- package/src/lib/server/session-reset-policy.test.ts +17 -3
- package/src/lib/server/session-reset-policy.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +11 -10
- package/src/lib/server/session-tools/crud.ts +41 -7
- package/src/lib/server/session-tools/delegate.ts +3 -3
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/manage-skills.test.ts +194 -0
- package/src/lib/server/session-tools/memory.ts +209 -48
- package/src/lib/server/session-tools/skill-runtime.test.ts +175 -0
- package/src/lib/server/session-tools/skill-runtime.ts +382 -0
- package/src/lib/server/session-tools/skills.ts +575 -0
- package/src/lib/server/skills/runtime-skill-resolver.test.ts +162 -0
- package/src/lib/server/skills/runtime-skill-resolver.ts +750 -0
- package/src/lib/server/skills/skill-discovery.ts +4 -0
- package/src/lib/server/skills/skills-normalize.test.ts +28 -0
- package/src/lib/server/skills/skills-normalize.ts +93 -1
- package/src/lib/server/storage.ts +1 -1
- package/src/lib/server/tasks/task-followups.test.ts +124 -0
- package/src/lib/server/tasks/task-followups.ts +88 -13
- package/src/types/index.ts +30 -2
- package/src/views/settings/section-runtime-loop.tsx +38 -0
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ The OpenClaw Control Plane in SwarmClaw adds:
|
|
|
64
64
|
- Gateway import/export JSON, clone flows, and richer external runtime fleet visibility
|
|
65
65
|
- Agent and route-target preferences for steering work toward OpenClaw gateways by tags or use case (`local-dev`, `single-vps`, `private-tailnet`, `browser-heavy`, `team-control`)
|
|
66
66
|
|
|
67
|
-
The Agent Inspector Panel lets you edit OpenClaw files (`SOUL.md`, `IDENTITY.md`, `USER.md`), tune personality/system behavior, and manage OpenClaw-compatible skills. SwarmClaw also supports importing OpenClaw `SKILL.md` files from URL.
|
|
67
|
+
The Agent Inspector Panel lets you edit OpenClaw files (`SOUL.md`, `IDENTITY.md`, `USER.md`), tune personality/system behavior, and manage OpenClaw-compatible skills. Skills are now discoverable by default, pinned skills stay always-on for an agent, and executable `SKILL.md` metadata can drive on-demand runtime skill selection. SwarmClaw also supports importing OpenClaw `SKILL.md` files from URL.
|
|
68
68
|
|
|
69
69
|
To connect an agent to an OpenClaw gateway:
|
|
70
70
|
|
|
@@ -155,7 +155,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
155
155
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
156
156
|
It also builds the production bundle so `npm run start` is ready immediately after install.
|
|
157
157
|
No Deno install is required; local sandbox execution is Docker-first with automatic host Node fallback.
|
|
158
|
-
To pin a version: `SWARMCLAW_VERSION=v0.9.
|
|
158
|
+
To pin a version: `SWARMCLAW_VERSION=v0.9.4 curl ... | bash`
|
|
159
159
|
|
|
160
160
|
Or run locally from the repo (friendly for non-technical users):
|
|
161
161
|
|
|
@@ -248,7 +248,7 @@ Notes:
|
|
|
248
248
|
- **Connector bridge** - Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, and others
|
|
249
249
|
- **Memory + knowledge** - hybrid search, memory graph, shared knowledge store, and auto-journaling
|
|
250
250
|
- **Operational guardrails** - capability policy, cost tracking, provider health, and credential failover
|
|
251
|
-
- **Extensibility** - plugin hooks/tools/UI extensions plus reusable skills
|
|
251
|
+
- **Extensibility** - plugin hooks/tools/UI extensions plus reusable skills with discovery-by-default runtime selection
|
|
252
252
|
|
|
253
253
|
For the full feature matrix and per-capability details, see:
|
|
254
254
|
- https://swarmclaw.ai/docs
|
|
@@ -364,6 +364,8 @@ Connector ingress now also supports optional pairing/allowlist policy:
|
|
|
364
364
|
- `/think` command can set connector thread thinking level (`low`, `medium`, `high`)
|
|
365
365
|
- Session overrides also support per-thread `/reply`, `/scope`, `/thread`, `/provider`, `/model`, `/idle`, `/maxage`, and `/reset` controls
|
|
366
366
|
|
|
367
|
+
Direct connector sessions are now the only source of routable connector state. Main agent threads no longer inherit outbound connector targets from mirrored history, and tool-only heartbeats stay out of visible main-thread history.
|
|
368
|
+
|
|
367
369
|
## Agent Tools
|
|
368
370
|
|
|
369
371
|
Agents can use the following tools when enabled:
|
|
@@ -727,15 +729,15 @@ On `v*` tags, GitHub Actions will:
|
|
|
727
729
|
2. Create a GitHub Release
|
|
728
730
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
729
731
|
|
|
730
|
-
#### v0.9.
|
|
732
|
+
#### v0.9.4 Release Readiness Notes
|
|
731
733
|
|
|
732
|
-
Before shipping `v0.9.
|
|
734
|
+
Before shipping `v0.9.4`, confirm the following user-facing changes are reflected in docs:
|
|
733
735
|
|
|
734
|
-
1.
|
|
735
|
-
2.
|
|
736
|
-
3.
|
|
737
|
-
4. Site and README install/version strings are updated to `v0.9.
|
|
738
|
-
5. The release tag, npm package version, and generated GitHub release install snippet all agree on the non-prefixed npm version (`0.9.
|
|
736
|
+
1. Skills docs explain that local skills are discoverable by default, while `skillIds` now mean pinned always-on skills for an agent.
|
|
737
|
+
2. Runtime-skill docs mention executable skill metadata, on-demand selection, and the `use_skill` / `manage_skills` flow instead of implying every discovered skill is inlined into the prompt.
|
|
738
|
+
3. Connector/heartbeat docs mention that routable connector state is kept on direct connector sessions only, sender quiet-boundary memories are enforced before reply generation, and tool-only heartbeats no longer pollute visible main-thread history.
|
|
739
|
+
4. Site and README install/version strings are updated to `v0.9.4`, including pinned install snippets, release notes index text, and sidebar/footer labels.
|
|
740
|
+
5. The release tag, npm package version, and generated GitHub release install snippet all agree on the non-prefixed npm version (`0.9.4`) versus the git tag (`v0.9.4`).
|
|
739
741
|
|
|
740
742
|
## CLI
|
|
741
743
|
|
|
@@ -4,6 +4,8 @@ description: Use Google Workspace CLI (`gws`) for Drive, Docs, Sheets, Gmail, Ca
|
|
|
4
4
|
homepage: https://github.com/googleworkspace/cli
|
|
5
5
|
metadata:
|
|
6
6
|
openclaw:
|
|
7
|
+
toolNames: [google_workspace, gws]
|
|
8
|
+
capabilities: [google-workspace, google-docs, google-drive, google-sheets, gmail, google-calendar, google-chat]
|
|
7
9
|
requires:
|
|
8
10
|
bins: [gws]
|
|
9
11
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
package/src/app/agents/page.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
6
6
|
import { useMediaQuery } from '@/hooks/use-media-query'
|
|
7
7
|
import { getViewPath } from '@/lib/app/navigation'
|
|
8
8
|
import { AgentChatList } from '@/components/agents/agent-chat-list'
|
|
9
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
9
10
|
|
|
10
11
|
export default function AgentsPage() {
|
|
11
12
|
const isDesktop = useMediaQuery('(min-width: 768px)')
|
|
@@ -30,5 +31,5 @@ export default function AgentsPage() {
|
|
|
30
31
|
if (!isDesktop) return <AgentChatList />
|
|
31
32
|
|
|
32
33
|
// Brief flash while redirecting, or no agents exist yet
|
|
33
|
-
return
|
|
34
|
+
return <PageLoader />
|
|
34
35
|
}
|
|
@@ -189,7 +189,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
189
189
|
syntheticSession.fallbackCredentialIds = route?.fallbackCredentialIds || syntheticSession.fallbackCredentialIds || []
|
|
190
190
|
syntheticSession.gatewayProfileId = route?.gatewayProfileId ?? syntheticSession.gatewayProfileId ?? null
|
|
191
191
|
syntheticSession.apiEndpoint = resolvedEndpoint
|
|
192
|
-
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
|
|
192
|
+
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent, syntheticSession.cwd)
|
|
193
193
|
const chatroomContext = buildChatroomSystemPrompt(freshChatroom, agents, agent.id)
|
|
194
194
|
const fullSystemPrompt = [agentSystemPrompt, chatroomContext].filter(Boolean).join('\n\n')
|
|
195
195
|
const history = buildHistoryForAgent(freshChatroom, agent.id, imagePath, attachedFiles)
|
|
@@ -45,6 +45,8 @@ export async function POST(req: Request) {
|
|
|
45
45
|
homepage: normalized.homepage,
|
|
46
46
|
primaryEnv: normalized.primaryEnv,
|
|
47
47
|
skillKey: normalized.skillKey,
|
|
48
|
+
toolNames: normalized.toolNames,
|
|
49
|
+
capabilities: normalized.capabilities,
|
|
48
50
|
always: normalized.always,
|
|
49
51
|
installOptions: normalized.installOptions,
|
|
50
52
|
skillRequirements: normalized.skillRequirements,
|
|
@@ -37,11 +37,15 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
37
37
|
homepage: normalized.homepage ?? null,
|
|
38
38
|
primaryEnv: normalized.primaryEnv ?? null,
|
|
39
39
|
skillKey: normalized.skillKey ?? null,
|
|
40
|
+
toolNames: normalized.toolNames,
|
|
41
|
+
capabilities: normalized.capabilities,
|
|
40
42
|
always: typeof normalized.always === 'boolean' ? normalized.always : false,
|
|
41
43
|
installOptions: normalized.installOptions,
|
|
42
44
|
skillRequirements: normalized.skillRequirements,
|
|
43
45
|
detectedEnvVars: normalized.detectedEnvVars,
|
|
44
46
|
security: normalized.security,
|
|
47
|
+
invocation: normalized.invocation,
|
|
48
|
+
commandDispatch: normalized.commandDispatch,
|
|
45
49
|
frontmatter: normalized.frontmatter,
|
|
46
50
|
scope: updatedScope,
|
|
47
51
|
agentIds: updatedAgentIds,
|
|
@@ -32,11 +32,15 @@ export async function POST(req: Request) {
|
|
|
32
32
|
homepage: normalized.homepage,
|
|
33
33
|
primaryEnv: normalized.primaryEnv,
|
|
34
34
|
skillKey: normalized.skillKey,
|
|
35
|
+
toolNames: normalized.toolNames,
|
|
36
|
+
capabilities: normalized.capabilities,
|
|
35
37
|
always: normalized.always,
|
|
36
38
|
installOptions: normalized.installOptions,
|
|
37
39
|
skillRequirements: normalized.skillRequirements,
|
|
38
40
|
detectedEnvVars: normalized.detectedEnvVars,
|
|
39
41
|
security: normalized.security,
|
|
42
|
+
invocation: normalized.invocation,
|
|
43
|
+
commandDispatch: normalized.commandDispatch,
|
|
40
44
|
frontmatter: normalized.frontmatter,
|
|
41
45
|
scope,
|
|
42
46
|
agentIds,
|
package/src/app/globals.css
CHANGED
|
@@ -287,6 +287,34 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
287
287
|
to { opacity: 1; transform: translateY(0); }
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/* ===== SwarmClaw Loader Keyframes ===== */
|
|
291
|
+
@keyframes sc-orbit {
|
|
292
|
+
from { transform: rotate(0deg); }
|
|
293
|
+
to { transform: rotate(360deg); }
|
|
294
|
+
}
|
|
295
|
+
@keyframes sc-ring {
|
|
296
|
+
from { transform: rotate(0deg) scale(1); }
|
|
297
|
+
50% { transform: rotate(180deg) scale(1.02); }
|
|
298
|
+
to { transform: rotate(360deg) scale(1); }
|
|
299
|
+
}
|
|
300
|
+
@keyframes sc-breathe {
|
|
301
|
+
0%, 100% { transform: scale(1); opacity: 0.9; }
|
|
302
|
+
50% { transform: scale(1.06); opacity: 1; }
|
|
303
|
+
}
|
|
304
|
+
@keyframes sc-glow {
|
|
305
|
+
0%, 100% { opacity: 0.5; transform: scale(0.9); }
|
|
306
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
307
|
+
}
|
|
308
|
+
@keyframes sc-text-fade {
|
|
309
|
+
0% { opacity: 0.6; }
|
|
310
|
+
100% { opacity: 1; }
|
|
311
|
+
}
|
|
312
|
+
@keyframes sc-progress {
|
|
313
|
+
0% { width: 0; margin-left: 0; }
|
|
314
|
+
50% { width: 70%; margin-left: 15%; }
|
|
315
|
+
100% { width: 0; margin-left: 100%; }
|
|
316
|
+
}
|
|
317
|
+
|
|
290
318
|
/* Heartbeat float animation */
|
|
291
319
|
@keyframes heartbeat-float {
|
|
292
320
|
0% { opacity: 1; transform: translateY(0) scale(1); }
|
package/src/app/home/page.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import { timeAgo, timeUntil } from '@/lib/time-format'
|
|
|
16
16
|
import type { Agent, Session, ActivityEntry, BoardTask, AppNotification } from '@/types'
|
|
17
17
|
import { HintTip } from '@/components/shared/hint-tip'
|
|
18
18
|
import { MainContent } from '@/components/layout/main-content'
|
|
19
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
19
20
|
import { SectionHeader } from '@/components/ui/section-header'
|
|
20
21
|
import { StatCard } from '@/components/ui/stat-card'
|
|
21
22
|
|
|
@@ -77,6 +78,7 @@ export default function HomePage() {
|
|
|
77
78
|
const [todayCost, setTodayCost] = useState(0)
|
|
78
79
|
const [costTrend, setCostTrend] = useState<{ cost: number; bucket: string }[]>([])
|
|
79
80
|
const [localhostBrowser, setLocalhostBrowser] = useState(false)
|
|
81
|
+
const [pageReady, setPageReady] = useState(false)
|
|
80
82
|
const mountedRef = useMountedRef()
|
|
81
83
|
|
|
82
84
|
useEffect(() => {
|
|
@@ -155,6 +157,7 @@ export default function HomePage() {
|
|
|
155
157
|
setTodayCost(todayPt?.cost || 0)
|
|
156
158
|
})
|
|
157
159
|
.catch(() => {})
|
|
160
|
+
.finally(() => { if (!cancelled && mountedRef.current) setPageReady(true) })
|
|
158
161
|
return () => {
|
|
159
162
|
cancelled = true
|
|
160
163
|
window.clearTimeout(connectorTimer)
|
|
@@ -188,6 +191,14 @@ export default function HomePage() {
|
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
193
|
|
|
194
|
+
if (!pageReady) {
|
|
195
|
+
return (
|
|
196
|
+
<MainContent>
|
|
197
|
+
<PageLoader label="Loading dashboard..." />
|
|
198
|
+
</MainContent>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
191
202
|
return (
|
|
192
203
|
<MainContent>
|
|
193
204
|
<div className="flex-1 overflow-y-auto">
|
|
@@ -4,6 +4,7 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
|
|
|
4
4
|
import type { ReactNode } from 'react'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { MainContent } from '@/components/layout/main-content'
|
|
7
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
7
8
|
import { inputClass } from '@/views/settings/utils'
|
|
8
9
|
import { UserPreferencesSection } from '@/views/settings/section-user-preferences'
|
|
9
10
|
import { ThemeSection } from '@/views/settings/section-theme'
|
|
@@ -84,6 +85,7 @@ export default function SettingsRoute() {
|
|
|
84
85
|
const loadSecrets = useAppStore((s) => s.loadSecrets)
|
|
85
86
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
86
87
|
const credentials = useAppStore((s) => s.credentials)
|
|
88
|
+
const [pageReady, setPageReady] = useState(false)
|
|
87
89
|
const [activeTab, setActiveTabRaw] = useState('general')
|
|
88
90
|
const contentRef = useRef<HTMLDivElement>(null)
|
|
89
91
|
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
|
@@ -98,11 +100,8 @@ export default function SettingsRoute() {
|
|
|
98
100
|
}, [])
|
|
99
101
|
|
|
100
102
|
useEffect(() => {
|
|
101
|
-
loadProviders()
|
|
102
|
-
|
|
103
|
-
loadSettings()
|
|
104
|
-
loadSecrets()
|
|
105
|
-
loadAgents()
|
|
103
|
+
Promise.all([loadProviders(), loadCredentials(), loadSettings(), loadSecrets(), loadAgents()])
|
|
104
|
+
.finally(() => setPageReady(true))
|
|
106
105
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
106
|
}, [])
|
|
108
107
|
|
|
@@ -298,6 +297,14 @@ export default function SettingsRoute() {
|
|
|
298
297
|
|
|
299
298
|
const visibleSections = sectionsByTab.get(activeTab) || []
|
|
300
299
|
|
|
300
|
+
if (!pageReady) {
|
|
301
|
+
return (
|
|
302
|
+
<MainContent>
|
|
303
|
+
<PageLoader label="Loading settings..." />
|
|
304
|
+
</MainContent>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
301
308
|
return (
|
|
302
309
|
<MainContent>
|
|
303
310
|
<div className="flex-1 flex h-full min-w-0">
|
|
@@ -2213,7 +2213,7 @@ export function AgentSheet() {
|
|
|
2213
2213
|
<div ref={(node) => { sectionRefs.current.tools = node }}>
|
|
2214
2214
|
<SectionCard
|
|
2215
2215
|
title="Tools & Delegation"
|
|
2216
|
-
description="Enable plugins, skills, MCP tools, and delegation behavior for this agent."
|
|
2216
|
+
description="Enable plugins, pin preferred skills, connect MCP tools, and configure delegation behavior for this agent."
|
|
2217
2217
|
>
|
|
2218
2218
|
{/* Plugins — hidden for providers that manage capabilities outside LangGraph */}
|
|
2219
2219
|
{!hasNativeCapabilities && (
|
|
@@ -2281,7 +2281,7 @@ export function AgentSheet() {
|
|
|
2281
2281
|
<div className="mb-8">
|
|
2282
2282
|
<div className="flex items-center justify-between mb-2">
|
|
2283
2283
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
|
|
2284
|
-
Skills <span className="normal-case tracking-normal font-normal text-text-3">(from ~/.claude/skills/)</span>
|
|
2284
|
+
Pinned Claude Skills <span className="normal-case tracking-normal font-normal text-text-3">(from ~/.claude/skills/)</span>
|
|
2285
2285
|
</label>
|
|
2286
2286
|
<button
|
|
2287
2287
|
onClick={loadClaudeSkills}
|
|
@@ -2297,7 +2297,7 @@ export function AgentSheet() {
|
|
|
2297
2297
|
Refresh
|
|
2298
2298
|
</button>
|
|
2299
2299
|
</div>
|
|
2300
|
-
<p className="text-[12px] text-text-3/60 mb-3">
|
|
2300
|
+
<p className="text-[12px] text-text-3/60 mb-3">Optional preference list. Pinned Claude skills are called out explicitly when this agent is delegated work.</p>
|
|
2301
2301
|
{claudeSkills.length > 0 ? (
|
|
2302
2302
|
<div className="flex flex-wrap gap-2">
|
|
2303
2303
|
{claudeSkills.map((s) => {
|
|
@@ -2328,9 +2328,9 @@ export function AgentSheet() {
|
|
|
2328
2328
|
{Object.keys(dynamicSkills).length > 0 && (
|
|
2329
2329
|
<div className="mb-8">
|
|
2330
2330
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
2331
|
-
|
|
2331
|
+
Pinned Skills <span className="normal-case tracking-normal font-normal text-text-3">(from Skills manager)</span>
|
|
2332
2332
|
</label>
|
|
2333
|
-
<p className="text-[12px] text-text-3/60 mb-3">
|
|
2333
|
+
<p className="text-[12px] text-text-3/60 mb-3">All ready local skills are discoverable by default. Pin skills here only when they should stay in this agent's prompt as always-on guidance.</p>
|
|
2334
2334
|
<div className="flex flex-wrap gap-2">
|
|
2335
2335
|
{Object.values(dynamicSkills).map((s) => {
|
|
2336
2336
|
const active = skillIds.includes(s.id)
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
resolveConnectorPlatformMeta,
|
|
15
15
|
} from '@/components/shared/connector-platform-icon'
|
|
16
16
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
17
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
17
18
|
import { StatusDot } from '@/components/ui/status-dot'
|
|
18
19
|
|
|
19
20
|
function relativeTime(ts: number): string {
|
|
@@ -158,11 +159,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
if (!loaded) {
|
|
161
|
-
return
|
|
162
|
-
<div className="flex-1 flex flex-col items-center justify-center px-6 py-12 text-center">
|
|
163
|
-
<p className="text-[13px] text-text-3">Loading connectors...</p>
|
|
164
|
-
</div>
|
|
165
|
-
)
|
|
162
|
+
return <PageLoader label="Loading connectors..." />
|
|
166
163
|
}
|
|
167
164
|
|
|
168
165
|
if (!list.length) {
|
|
@@ -5,6 +5,7 @@ import { api } from '@/lib/app/api-client'
|
|
|
5
5
|
import { useWs } from '@/hooks/use-ws'
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
7
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
8
9
|
import { safeStorageGetJson, safeStorageSet } from '@/lib/app/safe-storage'
|
|
9
10
|
|
|
10
11
|
interface LogEntry {
|
|
@@ -135,11 +136,7 @@ export function LogList() {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
if (loading) {
|
|
138
|
-
return
|
|
139
|
-
<div className="flex-1 flex items-center justify-center text-text-3 text-[13px]">
|
|
140
|
-
Loading logs...
|
|
141
|
-
</div>
|
|
142
|
-
)
|
|
139
|
+
return <PageLoader label="Loading logs..." />
|
|
143
140
|
}
|
|
144
141
|
|
|
145
142
|
const agentList = Object.values(agents)
|
|
@@ -8,6 +8,7 @@ import { useWs } from '@/hooks/use-ws'
|
|
|
8
8
|
import { api } from '@/lib/app/api-client'
|
|
9
9
|
import type { Credential, GatewayProfile } from '@/types'
|
|
10
10
|
import { dedup } from '@/lib/shared-utils'
|
|
11
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
11
12
|
import { StatusDot } from '@/components/ui/status-dot'
|
|
12
13
|
|
|
13
14
|
interface OpenClawDeployDraft {
|
|
@@ -254,11 +255,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
254
255
|
}, {})
|
|
255
256
|
|
|
256
257
|
if (!loaded) {
|
|
257
|
-
return
|
|
258
|
-
<div className={`flex-1 flex items-center justify-center ${inSidebar ? 'px-3 pb-4' : 'px-5'}`}>
|
|
259
|
-
<p className="text-[13px] text-text-3">Loading providers...</p>
|
|
260
|
-
</div>
|
|
261
|
-
)
|
|
258
|
+
return <PageLoader label="Loading providers..." />
|
|
262
259
|
}
|
|
263
260
|
|
|
264
261
|
return (
|
|
@@ -6,6 +6,7 @@ import { useNow } from '@/hooks/use-now'
|
|
|
6
6
|
import { useWs } from '@/hooks/use-ws'
|
|
7
7
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
8
|
import type { SessionRunRecord, SessionRunStatus } from '@/types'
|
|
9
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
9
10
|
import { formatElapsed } from '@/lib/format-display'
|
|
10
11
|
|
|
11
12
|
const STATUS_COLORS: Record<SessionRunStatus, { bg: string; text: string }> = {
|
|
@@ -55,12 +56,7 @@ export function RunList() {
|
|
|
55
56
|
const filtered = statusFilter ? runs.filter((r) => r.status === statusFilter) : runs
|
|
56
57
|
|
|
57
58
|
if (loading) {
|
|
58
|
-
return
|
|
59
|
-
<div className="flex-1 flex items-center justify-center text-text-3 text-[13px]">
|
|
60
|
-
<span className="w-4 h-4 rounded-full border-2 border-text-3/20 border-t-text-3/60 animate-spin mr-2" />
|
|
61
|
-
Loading runs...
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
59
|
+
return <PageLoader label="Loading runs..." />
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
return (
|
|
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { ScheduleCard } from './schedule-card'
|
|
6
6
|
import { SCHEDULE_TEMPLATES, FEATURED_TEMPLATE_IDS } from '@/lib/schedules/schedule-templates'
|
|
7
7
|
import { Newspaper, HeartPulse, PenLine, FileText } from 'lucide-react'
|
|
8
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
8
9
|
import { SearchInput } from '@/components/ui/search-input'
|
|
9
10
|
import { Button } from '@/components/ui/button'
|
|
10
11
|
|
|
@@ -27,8 +28,9 @@ export function ScheduleList({ inSidebar }: Props) {
|
|
|
27
28
|
const setTemplatePrefill = useAppStore((s) => s.setScheduleTemplatePrefill)
|
|
28
29
|
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
29
30
|
const [search, setSearch] = useState('')
|
|
31
|
+
const [loaded, setLoaded] = useState(false)
|
|
30
32
|
|
|
31
|
-
useEffect(() => { loadSchedules() }, [])
|
|
33
|
+
useEffect(() => { loadSchedules().finally(() => setLoaded(true)) }, [])
|
|
32
34
|
|
|
33
35
|
const filtered = useMemo(() => {
|
|
34
36
|
return Object.values(schedules)
|
|
@@ -40,6 +42,10 @@ export function ScheduleList({ inSidebar }: Props) {
|
|
|
40
42
|
.sort((a, b) => b.createdAt - a.createdAt)
|
|
41
43
|
}, [schedules, search, activeProjectFilter])
|
|
42
44
|
|
|
45
|
+
if (!loaded) {
|
|
46
|
+
return <PageLoader label="Loading schedules..." />
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
if (!filtered.length && !search) {
|
|
44
50
|
return (
|
|
45
51
|
<div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
|
|
@@ -130,35 +130,6 @@ export function FullScreenLoader(props: {
|
|
|
130
130
|
</div>
|
|
131
131
|
) : null}
|
|
132
132
|
|
|
133
|
-
{/* Loading animation keyframes */}
|
|
134
|
-
<style>{`
|
|
135
|
-
@keyframes sc-orbit {
|
|
136
|
-
from { transform: rotate(0deg); }
|
|
137
|
-
to { transform: rotate(360deg); }
|
|
138
|
-
}
|
|
139
|
-
@keyframes sc-ring {
|
|
140
|
-
from { transform: rotate(0deg) scale(1); }
|
|
141
|
-
50% { transform: rotate(180deg) scale(1.02); }
|
|
142
|
-
to { transform: rotate(360deg) scale(1); }
|
|
143
|
-
}
|
|
144
|
-
@keyframes sc-breathe {
|
|
145
|
-
0%, 100% { transform: scale(1); opacity: 0.9; }
|
|
146
|
-
50% { transform: scale(1.06); opacity: 1; }
|
|
147
|
-
}
|
|
148
|
-
@keyframes sc-glow {
|
|
149
|
-
0%, 100% { opacity: 0.5; transform: scale(0.9); }
|
|
150
|
-
50% { opacity: 1; transform: scale(1.1); }
|
|
151
|
-
}
|
|
152
|
-
@keyframes sc-text-fade {
|
|
153
|
-
0% { opacity: 0.6; }
|
|
154
|
-
100% { opacity: 1; }
|
|
155
|
-
}
|
|
156
|
-
@keyframes sc-progress {
|
|
157
|
-
0% { width: 0; margin-left: 0; }
|
|
158
|
-
50% { width: 70%; margin-left: 15%; }
|
|
159
|
-
100% { width: 0; margin-left: 100%; }
|
|
160
|
-
}
|
|
161
|
-
`}</style>
|
|
162
133
|
</div>
|
|
163
134
|
)
|
|
164
135
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight page-level loader — smaller sibling of FullScreenLoader.
|
|
3
|
+
* 3 orbiting dots, subtle glow ring, optional label.
|
|
4
|
+
* 150ms CSS animation-delay prevents flicker on fast loads.
|
|
5
|
+
*/
|
|
6
|
+
export function PageLoader({ label }: { label?: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className="flex-1 flex flex-col items-center justify-center select-none"
|
|
10
|
+
style={{
|
|
11
|
+
opacity: 0,
|
|
12
|
+
animation: 'fade-up 0.4s var(--ease-spring) 0.15s both',
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
{/* Orbital ring */}
|
|
16
|
+
<div className="relative w-[64px] h-[64px] mb-5">
|
|
17
|
+
{/* Glow pulse */}
|
|
18
|
+
<div
|
|
19
|
+
className="absolute inset-[-12px] rounded-full"
|
|
20
|
+
style={{
|
|
21
|
+
background: 'radial-gradient(circle, rgba(99,102,241,0.06) 0%, transparent 70%)',
|
|
22
|
+
animation: 'sc-glow 2.5s ease-in-out infinite',
|
|
23
|
+
}}
|
|
24
|
+
/>
|
|
25
|
+
|
|
26
|
+
{/* Ring */}
|
|
27
|
+
<div
|
|
28
|
+
className="absolute inset-0 rounded-full border border-white/[0.05]"
|
|
29
|
+
style={{ animation: 'sc-ring 3s linear infinite' }}
|
|
30
|
+
/>
|
|
31
|
+
|
|
32
|
+
{/* 3 orbiting dots */}
|
|
33
|
+
{[0, 1, 2].map((i) => (
|
|
34
|
+
<div
|
|
35
|
+
key={i}
|
|
36
|
+
className="absolute inset-0"
|
|
37
|
+
style={{
|
|
38
|
+
animation: 'sc-orbit 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite',
|
|
39
|
+
animationDelay: `${i * -0.8}s`,
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<div
|
|
43
|
+
className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
|
44
|
+
style={{
|
|
45
|
+
width: i === 0 ? 6 : 5,
|
|
46
|
+
height: i === 0 ? 6 : 5,
|
|
47
|
+
background: i === 0 ? '#818CF8' : `rgba(129, 140, 248, ${0.6 - i * 0.15})`,
|
|
48
|
+
boxShadow: i === 0 ? '0 0 10px rgba(99,102,241,0.4)' : 'none',
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Shimmer bar */}
|
|
56
|
+
<div className="w-[60px] h-[2px] rounded-full bg-white/[0.05] overflow-hidden">
|
|
57
|
+
<div
|
|
58
|
+
className="h-full rounded-full bg-accent-bright/50"
|
|
59
|
+
style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Optional label */}
|
|
64
|
+
{label && (
|
|
65
|
+
<p className="mt-3 text-[12px] text-text-3/60">{label}</p>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -21,6 +21,10 @@ export const CLAUDE_CODE_TIMEOUT_SEC_MIN = 5
|
|
|
21
21
|
export const CLAUDE_CODE_TIMEOUT_SEC_MAX = 7200
|
|
22
22
|
export const CLI_PROCESS_TIMEOUT_SEC_MIN = 10
|
|
23
23
|
export const CLI_PROCESS_TIMEOUT_SEC_MAX = 7200
|
|
24
|
+
export const STREAM_IDLE_STALL_SEC_MIN = 30
|
|
25
|
+
export const STREAM_IDLE_STALL_SEC_MAX = 600
|
|
26
|
+
export const REQUIRED_TOOL_KICKOFF_SEC_MIN = 10
|
|
27
|
+
export const REQUIRED_TOOL_KICKOFF_SEC_MAX = 120
|
|
24
28
|
|
|
25
29
|
export const DEFAULT_AGENT_LOOP_RECURSION_LIMIT = 300
|
|
26
30
|
export const DEFAULT_ORCHESTRATOR_LOOP_RECURSION_LIMIT = 80
|
|
@@ -30,9 +34,11 @@ export const DEFAULT_ONGOING_LOOP_MAX_RUNTIME_MINUTES = 60
|
|
|
30
34
|
export const DEFAULT_DELEGATION_MAX_DEPTH = 3
|
|
31
35
|
|
|
32
36
|
// Tool/process timeouts
|
|
33
|
-
export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC =
|
|
37
|
+
export const DEFAULT_SHELL_COMMAND_TIMEOUT_SEC = 120
|
|
34
38
|
export const DEFAULT_CLAUDE_CODE_TIMEOUT_SEC = 1800
|
|
35
39
|
export const DEFAULT_CLI_PROCESS_TIMEOUT_SEC = 1800
|
|
40
|
+
export const DEFAULT_STREAM_IDLE_STALL_SEC = 180
|
|
41
|
+
export const DEFAULT_REQUIRED_TOOL_KICKOFF_SEC = 45
|
|
36
42
|
|
|
37
43
|
function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
|
|
38
44
|
const parsed = typeof value === 'number'
|
|
@@ -55,6 +61,8 @@ export interface NormalizedRuntimeSettingFields {
|
|
|
55
61
|
shellCommandTimeoutSec: number
|
|
56
62
|
claudeCodeTimeoutSec: number
|
|
57
63
|
cliProcessTimeoutSec: number
|
|
64
|
+
streamIdleStallSec: number
|
|
65
|
+
requiredToolKickoffSec: number
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
export function normalizeRuntimeSettingFields(settings: Record<string, unknown>): NormalizedRuntimeSettingFields {
|
|
@@ -114,5 +122,17 @@ export function normalizeRuntimeSettingFields(settings: Record<string, unknown>)
|
|
|
114
122
|
CLI_PROCESS_TIMEOUT_SEC_MIN,
|
|
115
123
|
CLI_PROCESS_TIMEOUT_SEC_MAX,
|
|
116
124
|
),
|
|
125
|
+
streamIdleStallSec: parseIntSetting(
|
|
126
|
+
settings.streamIdleStallSec,
|
|
127
|
+
DEFAULT_STREAM_IDLE_STALL_SEC,
|
|
128
|
+
STREAM_IDLE_STALL_SEC_MIN,
|
|
129
|
+
STREAM_IDLE_STALL_SEC_MAX,
|
|
130
|
+
),
|
|
131
|
+
requiredToolKickoffSec: parseIntSetting(
|
|
132
|
+
settings.requiredToolKickoffSec,
|
|
133
|
+
DEFAULT_REQUIRED_TOOL_KICKOFF_SEC,
|
|
134
|
+
REQUIRED_TOOL_KICKOFF_SEC_MIN,
|
|
135
|
+
REQUIRED_TOOL_KICKOFF_SEC_MAX,
|
|
136
|
+
),
|
|
117
137
|
}
|
|
118
138
|
}
|
|
@@ -174,4 +174,68 @@ describe('ensureAgentThreadSession', () => {
|
|
|
174
174
|
|
|
175
175
|
assert.equal(output.session.openclawAgentId, 'main')
|
|
176
176
|
})
|
|
177
|
+
|
|
178
|
+
it('clears stale connector routing state from an existing agent shortcut session', () => {
|
|
179
|
+
const output = runWithTempDataDir(`
|
|
180
|
+
const storageMod = await import('@/lib/server/storage')
|
|
181
|
+
const storage = storageMod.default || storageMod['module.exports'] || storageMod
|
|
182
|
+
const helperMod = await import('@/lib/server/agents/agent-thread-session')
|
|
183
|
+
const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
|
|
184
|
+
|| helperMod.default?.ensureAgentThreadSession
|
|
185
|
+
|| helperMod['module.exports']?.ensureAgentThreadSession
|
|
186
|
+
|
|
187
|
+
const now = Date.now()
|
|
188
|
+
storage.saveAgents({
|
|
189
|
+
molly: {
|
|
190
|
+
id: 'molly',
|
|
191
|
+
name: 'Molly',
|
|
192
|
+
provider: 'openai',
|
|
193
|
+
model: 'gpt-test',
|
|
194
|
+
credentialId: null,
|
|
195
|
+
apiEndpoint: null,
|
|
196
|
+
fallbackCredentialIds: [],
|
|
197
|
+
heartbeatEnabled: true,
|
|
198
|
+
heartbeatIntervalSec: 600,
|
|
199
|
+
threadSessionId: 'agent-chat-molly-existing',
|
|
200
|
+
createdAt: now,
|
|
201
|
+
updatedAt: now,
|
|
202
|
+
plugins: ['memory'],
|
|
203
|
+
},
|
|
204
|
+
})
|
|
205
|
+
storage.saveSessions({
|
|
206
|
+
'agent-chat-molly-existing': {
|
|
207
|
+
id: 'agent-chat-molly-existing',
|
|
208
|
+
name: 'Molly',
|
|
209
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
210
|
+
user: 'default',
|
|
211
|
+
provider: 'openai',
|
|
212
|
+
model: 'gpt-old',
|
|
213
|
+
claudeSessionId: null,
|
|
214
|
+
codexThreadId: null,
|
|
215
|
+
opencodeSessionId: null,
|
|
216
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
217
|
+
messages: [],
|
|
218
|
+
createdAt: now,
|
|
219
|
+
lastActiveAt: now,
|
|
220
|
+
sessionType: 'human',
|
|
221
|
+
agentId: 'molly',
|
|
222
|
+
plugins: ['memory'],
|
|
223
|
+
connectorContext: {
|
|
224
|
+
connectorId: 'conn-1',
|
|
225
|
+
channelId: 'wrong-chat',
|
|
226
|
+
senderId: 'wrong-user',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const session = ensureAgentThreadSession('molly')
|
|
232
|
+
const persisted = storage.loadSessions()[session.id]
|
|
233
|
+
|
|
234
|
+
console.log(JSON.stringify({
|
|
235
|
+
connectorContext: persisted.connectorContext || null,
|
|
236
|
+
}))
|
|
237
|
+
`)
|
|
238
|
+
|
|
239
|
+
assert.equal(output.connectorContext, null)
|
|
240
|
+
})
|
|
177
241
|
})
|