@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.
Files changed (119) hide show
  1. package/README.md +85 -139
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/thread/route.ts +1 -2
  4. package/src/app/api/agents/route.ts +1 -1
  5. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  6. package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
  7. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  8. package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
  9. package/src/app/api/{sessions → chats}/route.ts +5 -7
  10. package/src/app/api/plugins/route.ts +3 -0
  11. package/src/app/api/plugins/settings/route.ts +35 -0
  12. package/src/app/api/usage/route.ts +30 -0
  13. package/src/cli/index.js +35 -33
  14. package/src/cli/index.ts +40 -39
  15. package/src/cli/spec.js +29 -27
  16. package/src/components/agents/agent-card.tsx +1 -1
  17. package/src/components/agents/agent-chat-list.tsx +3 -3
  18. package/src/components/agents/agent-list.tsx +8 -13
  19. package/src/components/agents/agent-sheet.tsx +2 -2
  20. package/src/components/agents/cron-job-form.tsx +3 -3
  21. package/src/components/agents/inspector-panel.tsx +2 -2
  22. package/src/components/auth/setup-wizard.tsx +5 -38
  23. package/src/components/chat/chat-area.tsx +10 -14
  24. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
  25. package/src/components/chat/chat-header.tsx +156 -73
  26. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
  27. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  28. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  29. package/src/components/chat/message-bubble.tsx +4 -1
  30. package/src/components/chat/message-list.tsx +2 -2
  31. package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
  32. package/src/components/chat/session-debug-panel.tsx +1 -1
  33. package/src/components/chat/tool-request-banner.tsx +3 -3
  34. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  35. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  36. package/src/components/connectors/connector-sheet.tsx +1 -1
  37. package/src/components/home/home-view.tsx +1 -1
  38. package/src/components/layout/app-layout.tsx +23 -2
  39. package/src/components/plugins/plugin-list.tsx +475 -254
  40. package/src/components/plugins/plugin-sheet.tsx +124 -10
  41. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  42. package/src/components/shared/command-palette.tsx +0 -1
  43. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  44. package/src/components/shared/settings/section-providers.tsx +1 -1
  45. package/src/components/shared/settings/settings-page.tsx +1 -12
  46. package/src/components/usage/metrics-dashboard.tsx +73 -0
  47. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  48. package/src/lib/chat.ts +1 -1
  49. package/src/lib/{sessions.ts → chats.ts} +28 -18
  50. package/src/lib/providers/claude-cli.ts +1 -1
  51. package/src/lib/server/approvals.ts +4 -4
  52. package/src/lib/server/capability-router.ts +10 -8
  53. package/src/lib/server/chat-execution.ts +36 -105
  54. package/src/lib/server/chatroom-helpers.ts +3 -3
  55. package/src/lib/server/connectors/manager.ts +4 -4
  56. package/src/lib/server/cost.ts +34 -1
  57. package/src/lib/server/daemon-state.ts +2 -2
  58. package/src/lib/server/heartbeat-service.ts +1 -1
  59. package/src/lib/server/main-agent-loop.ts +25 -160
  60. package/src/lib/server/main-session.ts +6 -13
  61. package/src/lib/server/orchestrator-lg.ts +3 -3
  62. package/src/lib/server/orchestrator.ts +5 -5
  63. package/src/lib/server/plugins.ts +112 -4
  64. package/src/lib/server/provider-health.ts +5 -3
  65. package/src/lib/server/queue.ts +12 -10
  66. package/src/lib/server/session-run-manager.test.ts +9 -6
  67. package/src/lib/server/session-run-manager.ts +1 -3
  68. package/src/lib/server/session-tools/calendar.ts +376 -0
  69. package/src/lib/server/session-tools/canvas.ts +1 -1
  70. package/src/lib/server/session-tools/chatroom.ts +4 -2
  71. package/src/lib/server/session-tools/connector.ts +5 -2
  72. package/src/lib/server/session-tools/context.ts +7 -3
  73. package/src/lib/server/session-tools/crud.ts +14 -6
  74. package/src/lib/server/session-tools/delegate.ts +95 -8
  75. package/src/lib/server/session-tools/discovery.ts +2 -2
  76. package/src/lib/server/session-tools/edit_file.ts +4 -2
  77. package/src/lib/server/session-tools/email.ts +322 -0
  78. package/src/lib/server/session-tools/file.ts +5 -2
  79. package/src/lib/server/session-tools/git.ts +1 -1
  80. package/src/lib/server/session-tools/http.ts +1 -1
  81. package/src/lib/server/session-tools/image-gen.ts +382 -0
  82. package/src/lib/server/session-tools/index.ts +74 -49
  83. package/src/lib/server/session-tools/memory.ts +139 -2
  84. package/src/lib/server/session-tools/monitor.ts +1 -1
  85. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  86. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  87. package/src/lib/server/session-tools/platform.ts +6 -3
  88. package/src/lib/server/session-tools/plugin-creator.ts +3 -3
  89. package/src/lib/server/session-tools/replicate.ts +303 -0
  90. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  91. package/src/lib/server/session-tools/sandbox.ts +4 -2
  92. package/src/lib/server/session-tools/schedule.ts +4 -2
  93. package/src/lib/server/session-tools/session-info.ts +7 -4
  94. package/src/lib/server/session-tools/shell.ts +5 -2
  95. package/src/lib/server/session-tools/subagent.ts +2 -2
  96. package/src/lib/server/session-tools/wallet.ts +29 -2
  97. package/src/lib/server/session-tools/web.ts +44 -5
  98. package/src/lib/server/storage.ts +29 -9
  99. package/src/lib/server/stream-agent-chat.ts +72 -249
  100. package/src/lib/server/tool-aliases.ts +26 -15
  101. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  102. package/src/lib/server/tool-capability-policy.ts +32 -27
  103. package/src/lib/tool-definitions.ts +4 -0
  104. package/src/lib/validation/schemas.ts +3 -1
  105. package/src/stores/use-app-store.ts +5 -5
  106. package/src/stores/use-chat-store.ts +7 -7
  107. package/src/types/index.ts +65 -3
  108. /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
  109. /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
  110. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  111. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  112. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  113. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  114. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  115. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  116. /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
  117. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  118. /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
  119. /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
- <button
209
- onClick={() => setConfirmDelete(true)}
210
- disabled={deleting}
211
- className="w-full py-2.5 rounded-[10px] text-[13px] font-600 bg-red-500/10 text-red-400 border border-red-500/20
212
- hover:bg-red-500/20 transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default"
213
- style={{ fontFamily: 'inherit' }}
214
- >
215
- {deleting ? 'Deleting...' : 'Delete Plugin'}
216
- </button>
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 sessions' },
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
 
@@ -77,7 +77,6 @@ export function CommandPalette() {
77
77
 
78
78
  // Chats (sessions)
79
79
  for (const session of Object.values(sessions)) {
80
- if (session.name === '__main__') continue
81
80
  result.push({
82
81
  id: `chat:${session.id}`,
83
82
  label: session.name || 'Untitled chat',
@@ -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', '/sessions/heartbeat', { action: 'disable_all' })
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/sessions'
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', 'plugin', 'api', 'key', 'openai', 'anthropic', 'ollama', 'credential'],
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 &middot; {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">Session: {entry.sessionId}</div>
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/sessions/${sessionId}/chat`, {
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 fetchSessions = () => api<Sessions>('GET', '/sessions')
7
+ export const fetchChats = () => api<Sessions>('GET', '/chats')
8
+ /** @deprecated Use fetchChats */
9
+ export const fetchSessions = fetchChats
8
10
 
9
- export const createSession = (
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
- tools?: string[],
21
+ plugins?: string[],
20
22
  file?: string | null,
21
23
  ) =>
22
- api<Session>('POST', '/sessions', {
24
+ api<Session>('POST', '/chats', {
23
25
  name, cwd: cwd || undefined, user,
24
26
  provider, model, credentialId, apiEndpoint,
25
- sessionType, agentId, tools, file: file || undefined,
27
+ sessionType, agentId, plugins, file: file || undefined,
26
28
  })
29
+ /** @deprecated Use createChat */
30
+ export const createSession = createChat
27
31
 
28
- export const updateSession = (id: string, updates: Partial<Pick<Session, 'name' | 'cwd'>>) =>
29
- api<Session>('PUT', `/sessions/${id}`, updates)
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 deleteSession = (id: string) =>
32
- api<string>('DELETE', `/sessions/${id}`)
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', `/sessions/${id}/messages`)
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', `/sessions/${id}/messages?limit=${limit}`)
53
+ api<PaginatedMessages>('GET', `/chats/${id}/messages?limit=${limit}`)
46
54
 
47
55
  export const clearMessages = (id: string) =>
48
- api<string>('POST', `/sessions/${id}/clear`)
56
+ api<string>('POST', `/chats/${id}/clear`)
49
57
 
50
- export const stopSession = (id: string) =>
51
- api<string>('POST', `/sessions/${id}/stop`)
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', `/sessions/${id}/devserver`, { action })
69
+ api<DevServerStatus>('POST', `/chats/${id}/devserver`, { action })
60
70
 
61
71
  export const checkBrowser = (id: string) =>
62
- api<{ active: boolean }>('GET', `/sessions/${id}/browser`)
72
+ api<{ active: boolean }>('GET', `/chats/${id}/browser`)
63
73
 
64
74
  export const stopBrowser = (id: string) =>
65
- api<string>('DELETE', `/sessions/${id}/browser`)
75
+ api<string>('DELETE', `/chats/${id}/browser`)
66
76
 
67
77
  export const deploy = (id: string, message: string) =>
68
- api<DeployResult>('POST', `/sessions/${id}/deploy`, { message })
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.tools || []
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.tools || []
72
+ const currentTools = session.plugins || []
73
73
  if (toolId && !currentTools.includes(toolId)) {
74
- session.tools = [...currentTools, toolId]
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.tools || []
102
+ const currentTools = session.plugins || []
103
103
  if (!currentTools.includes(filename)) {
104
- session.tools = [...currentTools, filename]
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: Array<'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'>
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
- value: unknown,
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: Array<'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'> = []
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
- enabledTools: string[],
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
- || enabledTools.includes('browser')
133
+ || enabledPlugins.includes('browser')
132
134
  )
133
135
  if (browsing) {
134
136
  return {