@swarmclawai/swarmclaw 0.7.1 → 0.7.2
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 +85 -139
- package/package.json +1 -1
- package/src/app/api/agents/[id]/thread/route.ts +1 -2
- package/src/app/api/agents/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
- package/src/app/api/{sessions → chats}/route.ts +5 -7
- package/src/app/api/plugins/route.ts +3 -0
- package/src/app/api/plugins/settings/route.ts +35 -0
- package/src/app/api/usage/route.ts +30 -0
- package/src/cli/index.js +35 -33
- package/src/cli/index.ts +40 -39
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-chat-list.tsx +3 -3
- package/src/components/agents/agent-list.tsx +8 -13
- package/src/components/agents/agent-sheet.tsx +2 -2
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +2 -2
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +10 -14
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
- package/src/components/chat/chat-header.tsx +156 -73
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +2 -2
- package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/connectors/connector-sheet.tsx +1 -1
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/layout/app-layout.tsx +23 -2
- package/src/components/plugins/plugin-list.tsx +475 -254
- package/src/components/plugins/plugin-sheet.tsx +124 -10
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/command-palette.tsx +0 -1
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/settings-page.tsx +1 -12
- package/src/components/usage/metrics-dashboard.tsx +73 -0
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/approvals.ts +4 -4
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution.ts +36 -105
- package/src/lib/server/chatroom-helpers.ts +3 -3
- package/src/lib/server/connectors/manager.ts +4 -4
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +2 -2
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/main-agent-loop.ts +25 -160
- package/src/lib/server/main-session.ts +6 -13
- package/src/lib/server/orchestrator-lg.ts +3 -3
- package/src/lib/server/orchestrator.ts +5 -5
- package/src/lib/server/plugins.ts +112 -4
- package/src/lib/server/provider-health.ts +5 -3
- package/src/lib/server/queue.ts +12 -10
- package/src/lib/server/session-run-manager.test.ts +9 -6
- package/src/lib/server/session-run-manager.ts +1 -3
- package/src/lib/server/session-tools/calendar.ts +376 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +5 -2
- package/src/lib/server/session-tools/context.ts +7 -3
- package/src/lib/server/session-tools/crud.ts +14 -6
- package/src/lib/server/session-tools/delegate.ts +95 -8
- package/src/lib/server/session-tools/discovery.ts +2 -2
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +322 -0
- package/src/lib/server/session-tools/file.ts +5 -2
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/image-gen.ts +382 -0
- package/src/lib/server/session-tools/index.ts +74 -49
- package/src/lib/server/session-tools/memory.ts +139 -2
- package/src/lib/server/session-tools/monitor.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform.ts +6 -3
- package/src/lib/server/session-tools/plugin-creator.ts +3 -3
- package/src/lib/server/session-tools/replicate.ts +303 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +4 -2
- package/src/lib/server/session-tools/session-info.ts +7 -4
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +2 -2
- package/src/lib/server/session-tools/wallet.ts +29 -2
- package/src/lib/server/session-tools/web.ts +44 -5
- package/src/lib/server/storage.ts +29 -9
- package/src/lib/server/stream-agent-chat.ts +72 -249
- package/src/lib/server/tool-aliases.ts +26 -15
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +32 -27
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.ts +3 -1
- package/src/stores/use-app-store.ts +5 -5
- package/src/stores/use-chat-store.ts +7 -7
- package/src/types/index.ts +65 -3
- /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -6,7 +6,7 @@ import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
|
6
6
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
7
7
|
import { api } from '@/lib/api-client'
|
|
8
8
|
import { toast } from 'sonner'
|
|
9
|
-
import type { PluginMeta, MarketplacePlugin } from '@/types'
|
|
9
|
+
import type { PluginMeta, PluginSettingsField, MarketplacePlugin } from '@/types'
|
|
10
10
|
|
|
11
11
|
function pluginDescription(plugin: PluginMeta): string {
|
|
12
12
|
const raw = (plugin.description || '').trim()
|
|
@@ -45,9 +45,38 @@ export function PluginSheet() {
|
|
|
45
45
|
const [search, setSearch] = useState('')
|
|
46
46
|
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
47
47
|
const [sort, setSort] = useState<'name' | 'downloads'>('downloads')
|
|
48
|
+
const [pluginSettingsValues, setPluginSettingsValues] = useState<Record<string, unknown>>({})
|
|
49
|
+
const [pluginSettingsLoading, setPluginSettingsLoading] = useState(false)
|
|
50
|
+
const [pluginSettingsSaving, setPluginSettingsSaving] = useState(false)
|
|
48
51
|
|
|
49
52
|
const editing = editingFilename ? plugins[editingFilename] : null
|
|
50
53
|
|
|
54
|
+
// Load per-plugin settings when editing a plugin that has settingsFields
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!editing?.settingsFields?.length) {
|
|
57
|
+
setPluginSettingsValues({})
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
setPluginSettingsLoading(true)
|
|
61
|
+
api<Record<string, unknown>>('GET', `/plugins/settings?pluginId=${encodeURIComponent(editing.filename)}`)
|
|
62
|
+
.then((data) => setPluginSettingsValues(data ?? {}))
|
|
63
|
+
.catch(() => setPluginSettingsValues({}))
|
|
64
|
+
.finally(() => setPluginSettingsLoading(false))
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
}, [editingFilename])
|
|
67
|
+
|
|
68
|
+
const savePluginSettings = useCallback(async () => {
|
|
69
|
+
if (!editing) return
|
|
70
|
+
setPluginSettingsSaving(true)
|
|
71
|
+
try {
|
|
72
|
+
await api('PUT', `/plugins/settings?pluginId=${encodeURIComponent(editing.filename)}`, pluginSettingsValues)
|
|
73
|
+
toast.success('Plugin settings saved')
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
toast.error(err instanceof Error ? err.message : 'Failed to save settings')
|
|
76
|
+
}
|
|
77
|
+
setPluginSettingsSaving(false)
|
|
78
|
+
}, [editing, pluginSettingsValues])
|
|
79
|
+
|
|
51
80
|
const loadMarketplace = useCallback(async () => {
|
|
52
81
|
setLoading(true)
|
|
53
82
|
try {
|
|
@@ -205,15 +234,46 @@ export function PluginSheet() {
|
|
|
205
234
|
</div>
|
|
206
235
|
</div>
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
{editing.settingsFields && editing.settingsFields.length > 0 && (
|
|
238
|
+
<div className="py-4 px-4 rounded-[14px] bg-surface border border-white/[0.06] space-y-3">
|
|
239
|
+
<div className="text-[13px] font-600 text-text">Settings</div>
|
|
240
|
+
{pluginSettingsLoading ? (
|
|
241
|
+
<p className="text-[11px] text-text-3/60">Loading...</p>
|
|
242
|
+
) : (
|
|
243
|
+
<>
|
|
244
|
+
{editing.settingsFields.map((field: PluginSettingsField) => (
|
|
245
|
+
<PluginSettingRow
|
|
246
|
+
key={field.key}
|
|
247
|
+
field={field}
|
|
248
|
+
value={pluginSettingsValues[field.key]}
|
|
249
|
+
onChange={(v) => setPluginSettingsValues((prev) => ({ ...prev, [field.key]: v }))}
|
|
250
|
+
/>
|
|
251
|
+
))}
|
|
252
|
+
<button
|
|
253
|
+
onClick={savePluginSettings}
|
|
254
|
+
disabled={pluginSettingsSaving}
|
|
255
|
+
className="w-full py-2 rounded-[10px] text-[12px] font-600 bg-accent-soft text-accent-bright border border-accent-bright/20
|
|
256
|
+
hover:bg-accent-soft/80 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default mt-1"
|
|
257
|
+
style={{ fontFamily: 'inherit' }}
|
|
258
|
+
>
|
|
259
|
+
{pluginSettingsSaving ? 'Saving...' : 'Save Settings'}
|
|
260
|
+
</button>
|
|
261
|
+
</>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{editing.source !== 'local' && (
|
|
267
|
+
<button
|
|
268
|
+
onClick={() => setConfirmDelete(true)}
|
|
269
|
+
disabled={deleting}
|
|
270
|
+
className="w-full py-2.5 rounded-[10px] text-[13px] font-600 bg-red-500/10 text-red-400 border border-red-500/20
|
|
271
|
+
hover:bg-red-500/20 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default"
|
|
272
|
+
style={{ fontFamily: 'inherit' }}
|
|
273
|
+
>
|
|
274
|
+
{deleting ? 'Deleting...' : 'Delete Plugin'}
|
|
275
|
+
</button>
|
|
276
|
+
)}
|
|
217
277
|
</div>
|
|
218
278
|
) : (
|
|
219
279
|
<div>
|
|
@@ -404,3 +464,57 @@ export function PluginSheet() {
|
|
|
404
464
|
</BottomSheet>
|
|
405
465
|
)
|
|
406
466
|
}
|
|
467
|
+
|
|
468
|
+
function PluginSettingRow({ field, value, onChange }: { field: PluginSettingsField; value: unknown; onChange: (v: unknown) => void }) {
|
|
469
|
+
const inputCls = 'w-full py-2 px-3 rounded-[8px] text-[12px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30'
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<div>
|
|
473
|
+
<label className="block text-[11px] font-600 text-text-2 mb-1">
|
|
474
|
+
{field.label}
|
|
475
|
+
{field.required && <span className="text-red-400 ml-0.5">*</span>}
|
|
476
|
+
</label>
|
|
477
|
+
{field.type === 'boolean' ? (
|
|
478
|
+
<div
|
|
479
|
+
onClick={() => onChange(!(value ?? field.defaultValue ?? false))}
|
|
480
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
481
|
+
${(value ?? field.defaultValue ?? false) ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
482
|
+
>
|
|
483
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
484
|
+
${(value ?? field.defaultValue ?? false) ? 'left-[22px]' : 'left-0.5'}`} />
|
|
485
|
+
</div>
|
|
486
|
+
) : field.type === 'select' ? (
|
|
487
|
+
<select
|
|
488
|
+
value={String(value ?? field.defaultValue ?? '')}
|
|
489
|
+
onChange={(e) => onChange(e.target.value)}
|
|
490
|
+
className={`${inputCls} cursor-pointer appearance-none`}
|
|
491
|
+
style={{ fontFamily: 'inherit' }}
|
|
492
|
+
>
|
|
493
|
+
<option value="">Select...</option>
|
|
494
|
+
{(field.options ?? []).map((opt) => (
|
|
495
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
496
|
+
))}
|
|
497
|
+
</select>
|
|
498
|
+
) : field.type === 'number' ? (
|
|
499
|
+
<input
|
|
500
|
+
type="number"
|
|
501
|
+
value={String(value ?? field.defaultValue ?? '')}
|
|
502
|
+
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : undefined)}
|
|
503
|
+
placeholder={field.placeholder}
|
|
504
|
+
className={inputCls}
|
|
505
|
+
style={{ fontFamily: 'inherit' }}
|
|
506
|
+
/>
|
|
507
|
+
) : (
|
|
508
|
+
<input
|
|
509
|
+
type={field.type === 'secret' ? 'password' : 'text'}
|
|
510
|
+
value={String(value ?? field.defaultValue ?? '')}
|
|
511
|
+
onChange={(e) => onChange(e.target.value || undefined)}
|
|
512
|
+
placeholder={field.placeholder}
|
|
513
|
+
className={inputCls}
|
|
514
|
+
style={{ fontFamily: 'inherit' }}
|
|
515
|
+
/>
|
|
516
|
+
)}
|
|
517
|
+
{field.help && <p className="text-[10px] text-text-3/60 mt-1">{field.help}</p>}
|
|
518
|
+
</div>
|
|
519
|
+
)
|
|
520
|
+
}
|
|
@@ -135,7 +135,7 @@ export function GatewayConnectionPanel() {
|
|
|
135
135
|
|
|
136
136
|
const reloadModes: { value: GatewayReloadMode; label: string; desc: string }[] = [
|
|
137
137
|
{ value: 'hot', label: 'Hot', desc: 'Only reload changed agents' },
|
|
138
|
-
{ value: 'hybrid', label: 'Hybrid', desc: 'Hot + restart stale
|
|
138
|
+
{ value: 'hybrid', label: 'Hybrid', desc: 'Hot + restart stale chats' },
|
|
139
139
|
{ value: 'full', label: 'Full', desc: 'Restart all agents on change' },
|
|
140
140
|
]
|
|
141
141
|
|
|
@@ -20,7 +20,7 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
20
20
|
updatedSessions: number
|
|
21
21
|
cancelledQueued: number
|
|
22
22
|
abortedRunning: number
|
|
23
|
-
}>('POST', '/
|
|
23
|
+
}>('POST', '/chats/heartbeat', { action: 'disable_all' })
|
|
24
24
|
await loadSessions()
|
|
25
25
|
setHeartbeatBulkNotice(
|
|
26
26
|
`Stopped heartbeat on ${result.updatedSessions} session(s); cancelled ${result.cancelledQueued} queued run(s), aborted ${result.abortedRunning} running run(s).`,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
-
import { createCredential, deleteCredential } from '@/lib/
|
|
5
|
+
import { createCredential, deleteCredential } from '@/lib/chats'
|
|
6
6
|
import { toast } from 'sonner'
|
|
7
7
|
import type { ProviderType } from '@/types'
|
|
8
8
|
import type { SettingsSectionProps } from './types'
|
|
@@ -16,7 +16,6 @@ import { EmbeddingSection } from './section-embedding'
|
|
|
16
16
|
import { MemorySection } from './section-memory'
|
|
17
17
|
import { SecretsSection } from './section-secrets'
|
|
18
18
|
import { ProvidersSection } from './section-providers'
|
|
19
|
-
import { PluginManager } from './plugin-manager'
|
|
20
19
|
|
|
21
20
|
interface Tab {
|
|
22
21
|
id: string
|
|
@@ -53,7 +52,7 @@ const TABS: Tab[] = [
|
|
|
53
52
|
{
|
|
54
53
|
id: 'integrations',
|
|
55
54
|
label: 'Integrations',
|
|
56
|
-
keywords: ['provider', 'secret', '
|
|
55
|
+
keywords: ['provider', 'secret', 'api', 'key', 'openai', 'anthropic', 'ollama', 'credential'],
|
|
57
56
|
icon: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 2v4m0 12v4M2 12h4m12 0h4" /><circle cx="12" cy="12" r="4" /><path d="M8 8L5.5 5.5M16 8l2.5-2.5M8 16l-2.5 2.5M16 16l2.5 2.5" /></svg>,
|
|
58
57
|
},
|
|
59
58
|
]
|
|
@@ -210,16 +209,6 @@ export function SettingsPage() {
|
|
|
210
209
|
<>
|
|
211
210
|
<ProvidersSection {...sectionProps} />
|
|
212
211
|
<SecretsSection {...sectionProps} />
|
|
213
|
-
<div className="mb-10">
|
|
214
|
-
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
215
|
-
Plugins
|
|
216
|
-
</h3>
|
|
217
|
-
<p className="text-[12px] text-text-3 mb-5">
|
|
218
|
-
Extend agent behavior with hooks. Install from the marketplace, a URL, or drop .js files into <code className="text-[11px] font-mono text-text-2">data/plugins/</code>.
|
|
219
|
-
<span className="text-text-3/70 ml-1">OpenClaw plugins are also supported.</span>
|
|
220
|
-
</p>
|
|
221
|
-
<PluginManager />
|
|
222
|
-
</div>
|
|
223
212
|
</>
|
|
224
213
|
)}
|
|
225
214
|
</div>
|
|
@@ -29,12 +29,20 @@ interface ProviderHealthEntry {
|
|
|
29
29
|
models: string[]
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
interface PluginUsageEntry {
|
|
33
|
+
definitionTokens: number
|
|
34
|
+
invocationTokens: number
|
|
35
|
+
invocations: number
|
|
36
|
+
estimatedCost: number
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
interface UsageResponse {
|
|
33
40
|
records: unknown[]
|
|
34
41
|
totalTokens: number
|
|
35
42
|
totalCost: number
|
|
36
43
|
byAgent: Record<string, { name: string; cost: number; tokens: number; count: number }>
|
|
37
44
|
byProvider: Record<string, { tokens: number; cost: number }>
|
|
45
|
+
byPlugin?: Record<string, PluginUsageEntry>
|
|
38
46
|
timeSeries: TimePoint[]
|
|
39
47
|
providerHealth?: Record<string, ProviderHealthEntry>
|
|
40
48
|
}
|
|
@@ -170,6 +178,18 @@ export function MetricsDashboard() {
|
|
|
170
178
|
cost: Math.round(v.cost * 10000) / 10000,
|
|
171
179
|
}))
|
|
172
180
|
|
|
181
|
+
const pluginData = Object.entries(data?.byPlugin ?? {})
|
|
182
|
+
.filter(([id]) => id !== '_system' && id !== '_unknown')
|
|
183
|
+
.sort((a, b) => (b[1].definitionTokens + b[1].invocationTokens) - (a[1].definitionTokens + a[1].invocationTokens))
|
|
184
|
+
.slice(0, 12)
|
|
185
|
+
.map(([id, v]) => ({
|
|
186
|
+
name: id.length > 18 ? id.slice(0, 18) + '…' : id,
|
|
187
|
+
definitionTokens: v.definitionTokens,
|
|
188
|
+
invocationTokens: v.invocationTokens,
|
|
189
|
+
invocations: v.invocations,
|
|
190
|
+
estimatedCost: v.estimatedCost,
|
|
191
|
+
}))
|
|
192
|
+
|
|
173
193
|
const tooltipStyle = {
|
|
174
194
|
contentStyle: {
|
|
175
195
|
background: 'var(--color-surface)',
|
|
@@ -288,6 +308,59 @@ export function MetricsDashboard() {
|
|
|
288
308
|
</ChartCard>
|
|
289
309
|
</div>
|
|
290
310
|
|
|
311
|
+
{/* Plugin Usage */}
|
|
312
|
+
{pluginData.length > 0 && (
|
|
313
|
+
<div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.28s both' }}>
|
|
314
|
+
<ChartCard title="Plugin Token Usage">
|
|
315
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
316
|
+
<BarChart data={pluginData} layout="vertical" margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
|
|
317
|
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" horizontal={false} />
|
|
318
|
+
<XAxis type="number" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={formatTokens} />
|
|
319
|
+
<YAxis type="category" dataKey="name" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} width={120} />
|
|
320
|
+
<Tooltip
|
|
321
|
+
{...tooltipStyle}
|
|
322
|
+
formatter={(value: number | undefined, name?: string) => [
|
|
323
|
+
formatTokens(value ?? 0),
|
|
324
|
+
name === 'definitionTokens' ? 'Context (definitions)' : 'Invocations',
|
|
325
|
+
]}
|
|
326
|
+
/>
|
|
327
|
+
<Bar dataKey="definitionTokens" fill="#818CF8" radius={[0, 0, 0, 0]} stackId="a" name="definitionTokens" />
|
|
328
|
+
<Bar dataKey="invocationTokens" fill="#34D399" radius={[0, 4, 4, 0]} stackId="a" name="invocationTokens" />
|
|
329
|
+
<Legend
|
|
330
|
+
verticalAlign="bottom"
|
|
331
|
+
iconType="circle"
|
|
332
|
+
iconSize={8}
|
|
333
|
+
formatter={(value: string) => (
|
|
334
|
+
<span style={{ color: '#a0a0b0', fontSize: 11 }}>
|
|
335
|
+
{value === 'definitionTokens' ? 'Context (definitions)' : 'Invocations'}
|
|
336
|
+
</span>
|
|
337
|
+
)}
|
|
338
|
+
/>
|
|
339
|
+
</BarChart>
|
|
340
|
+
</ResponsiveContainer>
|
|
341
|
+
</ChartCard>
|
|
342
|
+
|
|
343
|
+
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 mt-4">
|
|
344
|
+
{pluginData.filter((p) => p.invocations > 0).map((p, idx) => (
|
|
345
|
+
<div
|
|
346
|
+
key={p.name}
|
|
347
|
+
className="bg-surface-2 rounded-[10px] p-3 border border-white/[0.04] hover:bg-surface transition-all"
|
|
348
|
+
style={{ animation: 'spring-in 0.5s var(--ease-spring) both', animationDelay: `${0.3 + idx * 0.03}s` }}
|
|
349
|
+
>
|
|
350
|
+
<p className="text-[12px] font-600 text-text truncate">{p.name}</p>
|
|
351
|
+
<div className="flex items-baseline gap-2 mt-1">
|
|
352
|
+
<span className="text-[18px] font-display font-700 text-text">{p.invocations}</span>
|
|
353
|
+
<span className="text-[11px] text-text-3">calls</span>
|
|
354
|
+
</div>
|
|
355
|
+
<p className="text-[11px] text-text-3 mt-0.5">
|
|
356
|
+
{formatTokens(p.invocationTokens)} invocation tokens · {formatCost(p.estimatedCost)}
|
|
357
|
+
</p>
|
|
358
|
+
</div>
|
|
359
|
+
))}
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
)}
|
|
363
|
+
|
|
291
364
|
{/* Task KPIs */}
|
|
292
365
|
{taskMetrics && (
|
|
293
366
|
<div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
|
|
@@ -230,7 +230,7 @@ export function WebhookSheet() {
|
|
|
230
230
|
<div className="text-[11px] text-red-300/80 mt-1">{entry.error}</div>
|
|
231
231
|
)}
|
|
232
232
|
{entry.sessionId && (
|
|
233
|
-
<div className="text-[10px] text-text-3/50 mt-1 font-mono">
|
|
233
|
+
<div className="text-[10px] text-text-3/50 mt-1 font-mono">Chat: {entry.sessionId}</div>
|
|
234
234
|
)}
|
|
235
235
|
</div>
|
|
236
236
|
))}
|
package/src/lib/chat.ts
CHANGED
|
@@ -27,7 +27,7 @@ export async function streamChat(
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const key = getStoredAccessKey()
|
|
30
|
-
const res = await fetch(`/api/
|
|
30
|
+
const res = await fetch(`/api/chats/${sessionId}/chat`, {
|
|
31
31
|
method: 'POST',
|
|
32
32
|
headers: {
|
|
33
33
|
'Content-Type': 'application/json',
|
|
@@ -4,9 +4,11 @@ import type {
|
|
|
4
4
|
ProviderInfo, Credential, Credentials, ProviderType, SessionType,
|
|
5
5
|
} from '../types'
|
|
6
6
|
|
|
7
|
-
export const
|
|
7
|
+
export const fetchChats = () => api<Sessions>('GET', '/chats')
|
|
8
|
+
/** @deprecated Use fetchChats */
|
|
9
|
+
export const fetchSessions = fetchChats
|
|
8
10
|
|
|
9
|
-
export const
|
|
11
|
+
export const createChat = (
|
|
10
12
|
name: string,
|
|
11
13
|
cwd: string,
|
|
12
14
|
user: string,
|
|
@@ -16,23 +18,29 @@ export const createSession = (
|
|
|
16
18
|
apiEndpoint?: string | null,
|
|
17
19
|
sessionType?: SessionType,
|
|
18
20
|
agentId?: string | null,
|
|
19
|
-
|
|
21
|
+
plugins?: string[],
|
|
20
22
|
file?: string | null,
|
|
21
23
|
) =>
|
|
22
|
-
api<Session>('POST', '/
|
|
24
|
+
api<Session>('POST', '/chats', {
|
|
23
25
|
name, cwd: cwd || undefined, user,
|
|
24
26
|
provider, model, credentialId, apiEndpoint,
|
|
25
|
-
sessionType, agentId,
|
|
27
|
+
sessionType, agentId, plugins, file: file || undefined,
|
|
26
28
|
})
|
|
29
|
+
/** @deprecated Use createChat */
|
|
30
|
+
export const createSession = createChat
|
|
27
31
|
|
|
28
|
-
export const
|
|
29
|
-
api<Session>('PUT', `/
|
|
32
|
+
export const updateChat = (id: string, updates: Partial<Pick<Session, 'name' | 'cwd'>>) =>
|
|
33
|
+
api<Session>('PUT', `/chats/${id}`, updates)
|
|
34
|
+
/** @deprecated Use updateChat */
|
|
35
|
+
export const updateSession = updateChat
|
|
30
36
|
|
|
31
|
-
export const
|
|
32
|
-
api<string>('DELETE', `/
|
|
37
|
+
export const deleteChat = (id: string) =>
|
|
38
|
+
api<string>('DELETE', `/chats/${id}`)
|
|
39
|
+
/** @deprecated Use deleteChat */
|
|
40
|
+
export const deleteSession = deleteChat
|
|
33
41
|
|
|
34
42
|
export const fetchMessages = (id: string) =>
|
|
35
|
-
api<Message[]>('GET', `/
|
|
43
|
+
api<Message[]>('GET', `/chats/${id}/messages`)
|
|
36
44
|
|
|
37
45
|
export interface PaginatedMessages {
|
|
38
46
|
messages: Message[]
|
|
@@ -42,13 +50,15 @@ export interface PaginatedMessages {
|
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export const fetchMessagesPaginated = (id: string, limit: number = 100) =>
|
|
45
|
-
api<PaginatedMessages>('GET', `/
|
|
53
|
+
api<PaginatedMessages>('GET', `/chats/${id}/messages?limit=${limit}`)
|
|
46
54
|
|
|
47
55
|
export const clearMessages = (id: string) =>
|
|
48
|
-
api<string>('POST', `/
|
|
56
|
+
api<string>('POST', `/chats/${id}/clear`)
|
|
49
57
|
|
|
50
|
-
export const
|
|
51
|
-
api<string>('POST', `/
|
|
58
|
+
export const stopChat = (id: string) =>
|
|
59
|
+
api<string>('POST', `/chats/${id}/stop`)
|
|
60
|
+
/** @deprecated Use stopChat */
|
|
61
|
+
export const stopSession = stopChat
|
|
52
62
|
|
|
53
63
|
export const fetchDirs = async () => {
|
|
54
64
|
const data = await api<{ dirs: Directory[] }>('GET', '/dirs')
|
|
@@ -56,16 +66,16 @@ export const fetchDirs = async () => {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
export const devServer = (id: string, action: 'start' | 'stop' | 'status') =>
|
|
59
|
-
api<DevServerStatus>('POST', `/
|
|
69
|
+
api<DevServerStatus>('POST', `/chats/${id}/devserver`, { action })
|
|
60
70
|
|
|
61
71
|
export const checkBrowser = (id: string) =>
|
|
62
|
-
api<{ active: boolean }>('GET', `/
|
|
72
|
+
api<{ active: boolean }>('GET', `/chats/${id}/browser`)
|
|
63
73
|
|
|
64
74
|
export const stopBrowser = (id: string) =>
|
|
65
|
-
api<string>('DELETE', `/
|
|
75
|
+
api<string>('DELETE', `/chats/${id}/browser`)
|
|
66
76
|
|
|
67
77
|
export const deploy = (id: string, message: string) =>
|
|
68
|
-
api<DeployResult>('POST', `/
|
|
78
|
+
api<DeployResult>('POST', `/chats/${id}/deploy`, { message })
|
|
69
79
|
|
|
70
80
|
export const fetchProviders = () => api<ProviderInfo[]>('GET', '/providers')
|
|
71
81
|
|
|
@@ -43,7 +43,7 @@ export function streamClaudeCliChat({ session, message, imagePath, systemPrompt,
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Add MCP servers for enabled tools
|
|
46
|
-
const tools: string[] = session.
|
|
46
|
+
const tools: string[] = session.plugins || []
|
|
47
47
|
let mcpConfigPath: string | null = null
|
|
48
48
|
if (tools.includes('browser')) {
|
|
49
49
|
const proxyScript = path.join(process.cwd(), 'src/lib/server/playwright-proxy.mjs')
|
|
@@ -69,9 +69,9 @@ export async function submitDecision(id: string, approved: boolean): Promise<voi
|
|
|
69
69
|
const session = sessions[request.sessionId]
|
|
70
70
|
if (session) {
|
|
71
71
|
const toolId = getApprovalTargetId(request.data)
|
|
72
|
-
const currentTools = session.
|
|
72
|
+
const currentTools = session.plugins || []
|
|
73
73
|
if (toolId && !currentTools.includes(toolId)) {
|
|
74
|
-
session.
|
|
74
|
+
session.plugins = [...currentTools, toolId]
|
|
75
75
|
saveSessions(sessions)
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -99,9 +99,9 @@ export async function submitDecision(id: string, approved: boolean): Promise<voi
|
|
|
99
99
|
const sessions = loadSessions()
|
|
100
100
|
const session = sessions[request.sessionId]
|
|
101
101
|
if (session) {
|
|
102
|
-
const currentTools = session.
|
|
102
|
+
const currentTools = session.plugins || []
|
|
103
103
|
if (!currentTools.includes(filename)) {
|
|
104
|
-
session.
|
|
104
|
+
session.plugins = [...currentTools, filename]
|
|
105
105
|
saveSessions(sessions)
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -12,10 +12,12 @@ export interface CapabilityRoutingDecision {
|
|
|
12
12
|
intent: TaskIntent
|
|
13
13
|
confidence: number
|
|
14
14
|
preferredTools: string[]
|
|
15
|
-
preferredDelegates:
|
|
15
|
+
preferredDelegates: DelegateTool[]
|
|
16
16
|
primaryUrl?: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
20
|
+
|
|
19
21
|
function findFirstUrl(text: string): string | undefined {
|
|
20
22
|
const m = text.match(/https?:\/\/[^\s<>"')]+/i)
|
|
21
23
|
return m?.[0]
|
|
@@ -25,21 +27,21 @@ function containsAny(text: string, terms: string[]): boolean {
|
|
|
25
27
|
return terms.some((term) => text.includes(term))
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function normalizeDelegateOrder(
|
|
29
|
-
|
|
30
|
-
): Array<'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'> {
|
|
31
|
-
const fallback: Array<'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'> = [
|
|
30
|
+
function normalizeDelegateOrder(value: unknown): DelegateTool[] {
|
|
31
|
+
const fallback: DelegateTool[] = [
|
|
32
32
|
'delegate_to_claude_code',
|
|
33
33
|
'delegate_to_codex_cli',
|
|
34
34
|
'delegate_to_opencode_cli',
|
|
35
|
+
'delegate_to_gemini_cli',
|
|
35
36
|
]
|
|
36
37
|
if (!Array.isArray(value) || !value.length) return fallback
|
|
37
38
|
|
|
38
|
-
const mapped:
|
|
39
|
+
const mapped: DelegateTool[] = []
|
|
39
40
|
for (const raw of value) {
|
|
40
41
|
if (raw === 'claude') mapped.push('delegate_to_claude_code')
|
|
41
42
|
else if (raw === 'codex') mapped.push('delegate_to_codex_cli')
|
|
42
43
|
else if (raw === 'opencode') mapped.push('delegate_to_opencode_cli')
|
|
44
|
+
else if (raw === 'gemini') mapped.push('delegate_to_gemini_cli')
|
|
43
45
|
}
|
|
44
46
|
if (!mapped.length) return fallback
|
|
45
47
|
const deduped = Array.from(new Set(mapped))
|
|
@@ -51,7 +53,7 @@ function normalizeDelegateOrder(
|
|
|
51
53
|
|
|
52
54
|
export function routeTaskIntent(
|
|
53
55
|
message: string,
|
|
54
|
-
|
|
56
|
+
enabledPlugins: string[],
|
|
55
57
|
settings?: AppSettings | null,
|
|
56
58
|
): CapabilityRoutingDecision {
|
|
57
59
|
const text = (message || '').toLowerCase()
|
|
@@ -128,7 +130,7 @@ export function routeTaskIntent(
|
|
|
128
130
|
|
|
129
131
|
const browsing = !!url && (
|
|
130
132
|
containsAny(text, ['browser', 'click', 'fill form', 'log in', 'screenshot', 'navigate'])
|
|
131
|
-
||
|
|
133
|
+
|| enabledPlugins.includes('browser')
|
|
132
134
|
)
|
|
133
135
|
if (browsing) {
|
|
134
136
|
return {
|