@camstack/addon-admin-ui 0.1.1 → 0.1.3

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 (127) hide show
  1. package/dist/assets/index-DjELGD4R.css +1 -0
  2. package/dist/assets/index-w55PwKyu.js +598 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +5 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -339
  10. package/src/components/addons/AddonUploadZone.tsx +0 -307
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -119
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -171
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -113
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -90
  69. package/src/components/metrics/PipelineStatus.tsx +0 -105
  70. package/src/components/metrics/ProcessResources.tsx +0 -139
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -238
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -224
  98. package/src/pages/Integrations.tsx +0 -330
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -525
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -210
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /package/{public → dist}/vendor/react.mjs +0 -0
@@ -1,525 +0,0 @@
1
- import { useState, useDeferredValue, useMemo } from 'react'
2
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
- import { Package, Download, RefreshCw, Map as MapIcon, Search, Check, Loader2, ChevronDown, ChevronUp } from 'lucide-react'
4
- import { useBackendClient } from '../../hooks/useBackendClient'
5
- import { AddonCard } from '../../components/addons/AddonCard'
6
- import type { AddonListItem, AgentInfo } from '../../components/addons/AddonCard'
7
- import { AddonUploadZone } from '../../components/addons/AddonUploadZone'
8
- import { UpdatesList } from '../../components/addons/UpdatesList'
9
- import type { UpdateEntry } from '../../components/addons/UpdatesList'
10
- import { CapabilityMap } from '../../components/addons/CapabilityMap'
11
- import type { CapabilityEntry } from '../../components/addons/CapabilityMap'
12
-
13
- // ---------------------------------------------------------------------------
14
- // Types
15
- // ---------------------------------------------------------------------------
16
-
17
- interface RawAddonManifest {
18
- id: string
19
- name: string
20
- version: string
21
- description?: string
22
- capabilities: readonly string[]
23
- requiredFeatures?: readonly string[]
24
- group?: 'core' | 'addon' | 'provider' | 'page'
25
- packageName: string
26
- packageVersion: string
27
- removable?: boolean
28
- }
29
-
30
- interface RawAddonListItem {
31
- manifest: RawAddonManifest
32
- enabled: boolean
33
- hasConfigSchema: boolean
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // Normalise raw addon to typed AddonListItem, deriving group from ID
38
- // ---------------------------------------------------------------------------
39
-
40
- function deriveGroup(manifest: RawAddonManifest, source?: string): AddonListItem['group'] {
41
- if (manifest.group) return manifest.group
42
- if (source === 'core' || manifest.packageName === '@camstack/core') return 'core'
43
- const pkg = manifest.packageName.toLowerCase()
44
- if (pkg.includes('provider')) return 'provider'
45
- if (pkg.includes('page') || pkg.includes('benchmark')) return 'page'
46
- return 'addon'
47
- }
48
-
49
- function normaliseAddon(raw: RawAddonListItem): AddonListItem | null {
50
- if (!raw?.manifest) return null
51
- return {
52
- manifest: {
53
- ...raw.manifest,
54
- capabilities: raw.manifest.capabilities ?? [],
55
- },
56
- enabled: raw.enabled ?? false,
57
- hasConfigSchema: raw.hasConfigSchema ?? false,
58
- group: deriveGroup(raw.manifest, (raw as any).source),
59
- source: (raw as any).source,
60
- installedOn: [],
61
- }
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // Derive capabilities from addon manifests
66
- // ---------------------------------------------------------------------------
67
-
68
- function deriveCapabilities(addons: AddonListItem[]): CapabilityEntry[] {
69
- const capMap = new Map<string, CapabilityEntry>()
70
-
71
- for (const addon of addons) {
72
- const capabilities = addon.manifest.capabilities ?? []
73
- for (const cap of capabilities) {
74
- const capName = typeof cap === 'string' ? cap : (cap as { name: string }).name
75
- const capMode = typeof cap === 'string' ? 'collection' : ((cap as { mode?: string }).mode ?? 'collection')
76
-
77
- if (!capMap.has(capName)) {
78
- capMap.set(capName, {
79
- name: capName,
80
- mode: capMode as CapabilityEntry['mode'],
81
- providers: [],
82
- })
83
- }
84
-
85
- const entry = capMap.get(capName)!
86
- entry.providers.push({
87
- addonId: addon.manifest.id,
88
- addonName: addon.manifest.name,
89
- active: addon.enabled,
90
- })
91
- }
92
- }
93
-
94
- return Array.from(capMap.values())
95
- }
96
-
97
- // ---------------------------------------------------------------------------
98
- // Group definitions
99
- // ---------------------------------------------------------------------------
100
-
101
- const GROUPS: { key: AddonListItem['group']; label: string }[] = [
102
- { key: 'core', label: 'Core (Builtins)' },
103
- { key: 'addon', label: 'Addon Packages' },
104
- { key: 'provider', label: 'Providers' },
105
- { key: 'page', label: 'Addon Pages' },
106
- ]
107
-
108
- // ---------------------------------------------------------------------------
109
- // Tab bar
110
- // ---------------------------------------------------------------------------
111
-
112
- type Tab = 'installed' | 'updates' | 'map'
113
-
114
- const TABS: { id: Tab; label: string; icon: React.ElementType }[] = [
115
- { id: 'installed', label: 'Installed', icon: Package },
116
- { id: 'updates', label: 'Updates', icon: RefreshCw },
117
- { id: 'map', label: 'Capability Map', icon: MapIcon },
118
- ]
119
-
120
- function TabBar({ active, onChange }: { active: Tab; onChange: (tab: Tab) => void }) {
121
- return (
122
- <div className="flex gap-1 border-b border-border pb-0">
123
- {TABS.map(({ id, label, icon: Icon }) => (
124
- <button
125
- key={id}
126
- type="button"
127
- onClick={() => onChange(id)}
128
- className={[
129
- 'flex items-center gap-1.5 px-4 py-2 text-xs font-medium border-b-2 -mb-px transition-colors',
130
- active === id
131
- ? 'border-primary text-primary'
132
- : 'border-transparent text-foreground-subtle hover:text-foreground',
133
- ].join(' ')}
134
- >
135
- <Icon className="h-3.5 w-3.5" />
136
- {label}
137
- </button>
138
- ))}
139
- </div>
140
- )
141
- }
142
-
143
- // ---------------------------------------------------------------------------
144
- // Installed tab
145
- // ---------------------------------------------------------------------------
146
-
147
- // ---------------------------------------------------------------------------
148
- // Package group — collapsible header for addons from the same npm package
149
- // ---------------------------------------------------------------------------
150
-
151
- interface PackageGroup {
152
- packageName: string
153
- version: string
154
- addons: AddonListItem[]
155
- }
156
-
157
- function groupByPackage(addons: AddonListItem[]): Array<PackageGroup | AddonListItem> {
158
- const packageMap = new Map<string, PackageGroup>()
159
- const ungrouped: AddonListItem[] = []
160
-
161
- for (const addon of addons) {
162
- const pkgName = addon.manifest.packageName
163
- if (!pkgName) {
164
- ungrouped.push(addon)
165
- continue
166
- }
167
- const existing = packageMap.get(pkgName)
168
- if (existing) {
169
- existing.addons.push(addon)
170
- } else {
171
- packageMap.set(pkgName, {
172
- packageName: pkgName,
173
- version: addon.manifest.packageVersion,
174
- addons: [addon],
175
- })
176
- }
177
- }
178
-
179
- const result: Array<PackageGroup | AddonListItem> = []
180
- for (const group of packageMap.values()) {
181
- // Only create a collapsible group if the package contains 2+ addons
182
- if (group.addons.length >= 2) {
183
- result.push(group)
184
- } else {
185
- result.push(group.addons[0]!)
186
- }
187
- }
188
- result.push(...ungrouped)
189
- return result
190
- }
191
-
192
- function isPackageGroup(item: PackageGroup | AddonListItem): item is PackageGroup {
193
- return 'packageName' in item && 'addons' in item
194
- }
195
-
196
- function PackageGroupHeader({ group, agents }: { group: PackageGroup; agents: AgentInfo[] }) {
197
- const [expanded, setExpanded] = useState(false)
198
-
199
- return (
200
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
201
- <button
202
- type="button"
203
- onClick={() => setExpanded((e) => !e)}
204
- className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-hover/50 transition-colors text-left"
205
- >
206
- <Package className="h-4 w-4 text-primary shrink-0" />
207
- <div className="flex-1 min-w-0 flex items-center gap-2 flex-wrap">
208
- <span className="text-xs font-semibold text-foreground truncate">{group.packageName}</span>
209
- <span className="text-[10px] rounded-full bg-primary/10 text-primary px-2 py-0.5 font-medium shrink-0">
210
- v{group.version}
211
- </span>
212
- <span className="text-[10px] rounded-full bg-surface-hover border border-border px-2 py-0.5 font-medium shrink-0">
213
- {group.addons.length} addons
214
- </span>
215
- </div>
216
- {expanded ? <ChevronUp className="h-4 w-4 text-foreground-subtle shrink-0" /> : <ChevronDown className="h-4 w-4 text-foreground-subtle shrink-0" />}
217
- </button>
218
- {expanded && (
219
- <div className="border-t border-border px-3 py-2 space-y-2">
220
- {group.addons.map((addon) => (
221
- <AddonCard key={addon.manifest.id} addon={addon} agents={agents} />
222
- ))}
223
- </div>
224
- )}
225
- </div>
226
- )
227
- }
228
-
229
- // ---------------------------------------------------------------------------
230
- // Installed tab
231
- // ---------------------------------------------------------------------------
232
-
233
- interface InstalledTabProps {
234
- addons: AddonListItem[]
235
- agents: AgentInfo[]
236
- onRefresh: () => void
237
- isRefreshing: boolean
238
- }
239
-
240
- function InstalledTab({ addons, agents, onRefresh, isRefreshing }: InstalledTabProps) {
241
- const client = useBackendClient()
242
- const queryClient = useQueryClient()
243
- const [searchInput, setSearchInput] = useState('')
244
- const deferredSearch = useDeferredValue(searchInput)
245
- const [showSearch, setShowSearch] = useState(false)
246
-
247
- // Search npm for available addons — uses deferred (debounced) value
248
- const { data: searchResults, isFetching: isSearching } = useQuery({
249
- queryKey: ['addon-search', deferredSearch],
250
- queryFn: () => client.trpc.bridgeAddons.searchAvailable.query({ query: deferredSearch || undefined }),
251
- enabled: showSearch,
252
- staleTime: 60_000,
253
- })
254
-
255
- // Install addon mutation
256
- const installMutation = useMutation({
257
- mutationFn: (packageName: string) => client.trpc.bridgeAddons.installPackage.mutate({ packageName }),
258
- onSuccess: (_data, packageName) => {
259
- queryClient.invalidateQueries({ queryKey: ['addons'] })
260
- queryClient.invalidateQueries({ queryKey: ['addon-search'] })
261
- queryClient.invalidateQueries({ queryKey: ['addon-pages'] })
262
- queryClient.invalidateQueries({ queryKey: ['capabilities'] })
263
- console.log(`[Addons] Installed ${packageName}`)
264
- },
265
- onError: (err, packageName) => {
266
- console.error(`[Addons] Install failed for ${packageName}:`, err)
267
- },
268
- })
269
-
270
- const grouped = GROUPS.map(({ key, label }) => ({
271
- key,
272
- label,
273
- items: addons.filter((a) => a.group === key),
274
- })).filter((g) => g.items.length > 0)
275
-
276
- // Filter search results: hide already installed
277
- const availableAddons = (searchResults ?? []).filter((r: any) => !r.installed)
278
-
279
- return (
280
- <div className="space-y-6">
281
- {/* Search bar + actions */}
282
- <div className="flex gap-2">
283
- <div className="relative flex-1">
284
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-foreground-subtle" />
285
- <input
286
- type="text"
287
- value={searchInput}
288
- onChange={(e) => { setSearchInput(e.target.value); setShowSearch(true) }}
289
- onFocus={() => setShowSearch(true)}
290
- placeholder="Search addons by name or capability..."
291
- className="w-full rounded-md border border-border bg-background pl-9 pr-3 py-2 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary"
292
- />
293
- {isSearching && (
294
- <Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-foreground-subtle animate-spin" />
295
- )}
296
- </div>
297
- <button
298
- type="button"
299
- onClick={onRefresh}
300
- disabled={isRefreshing}
301
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-surface hover:bg-surface-hover border border-border disabled:opacity-50 transition-colors"
302
- >
303
- <RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
304
- Refresh
305
- </button>
306
- </div>
307
-
308
- {/* Search results dropdown */}
309
- {showSearch && availableAddons.length > 0 && (
310
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
311
- <div className="px-3 py-2 text-[10px] uppercase tracking-wide text-foreground-subtle border-b border-border flex justify-between">
312
- <span>Available on npm ({availableAddons.length})</span>
313
- <button type="button" onClick={() => setShowSearch(false)} className="text-foreground-subtle hover:text-foreground">
314
- close
315
- </button>
316
- </div>
317
- <div className="max-h-64 overflow-auto divide-y divide-border">
318
- {availableAddons.map((addon: any) => (
319
- <div key={addon.name} className="flex items-center gap-3 px-3 py-2.5 hover:bg-surface-hover transition-colors">
320
- <div className="w-7 h-7 rounded-md bg-primary/10 flex items-center justify-center text-primary text-[10px] font-bold shrink-0">
321
- {addon.name.replace('@camstack/addon-', '').charAt(0).toUpperCase()}
322
- </div>
323
- <div className="flex-1 min-w-0">
324
- <div className="flex items-center gap-2">
325
- <span className="text-xs font-semibold truncate">{addon.name}</span>
326
- <span className="text-[10px] text-foreground-subtle">v{addon.version}</span>
327
- </div>
328
- <div className="text-[10px] text-foreground-subtle truncate">{addon.description}</div>
329
- {addon.keywords?.length > 0 && (
330
- <div className="flex gap-1 mt-0.5 flex-wrap">
331
- {addon.keywords.filter((k: string) => k !== 'camstack' && k !== 'addon' && k !== 'camstack-addon').slice(0, 5).map((k: string) => (
332
- <span key={k} className="text-[9px] px-1.5 py-0 rounded bg-primary/5 text-foreground-subtle">{k}</span>
333
- ))}
334
- </div>
335
- )}
336
- </div>
337
- <button
338
- type="button"
339
- onClick={() => installMutation.mutate(addon.name)}
340
- disabled={installMutation.isPending}
341
- className="flex items-center gap-1 px-2.5 py-1 text-[10px] font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors shrink-0"
342
- >
343
- {installMutation.isPending ? (
344
- <Loader2 className="h-3 w-3 animate-spin" />
345
- ) : (
346
- <Download className="h-3 w-3" />
347
- )}
348
- Install
349
- </button>
350
- </div>
351
- ))}
352
- </div>
353
- </div>
354
- )}
355
-
356
- {/* Show "all installed" if search returned only installed results */}
357
- {showSearch && searchResults && availableAddons.length === 0 && searchResults.length > 0 && (
358
- <div className="text-xs text-foreground-subtle flex items-center gap-1.5 px-1">
359
- <Check className="h-3.5 w-3.5 text-green-500" />
360
- All matching addons are already installed
361
- </div>
362
- )}
363
-
364
- {/* Upload zone — below search results */}
365
- <AddonUploadZone
366
- onUploadSuccess={() => {
367
- queryClient.invalidateQueries({ queryKey: ['addons'] })
368
- onRefresh()
369
- }}
370
- />
371
-
372
- {/* Installed addons grouped */}
373
- {addons.length === 0 && (
374
- <div className="text-xs text-foreground-subtle">No addons installed</div>
375
- )}
376
-
377
- {grouped.map(({ key, label, items }) => {
378
- const packageGrouped = groupByPackage(items)
379
- return (
380
- <div key={key} className="space-y-2">
381
- <h3 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wide flex items-center gap-2">
382
- {label}
383
- <span className="rounded-full bg-surface-hover border border-border px-2 py-0.5 text-[10px] normal-case font-normal">
384
- {items.length}
385
- </span>
386
- </h3>
387
- {packageGrouped.map((item) =>
388
- isPackageGroup(item) ? (
389
- <PackageGroupHeader key={item.packageName} group={item} agents={agents} />
390
- ) : (
391
- <AddonCard key={item.manifest.id} addon={item} agents={agents} />
392
- ),
393
- )}
394
- </div>
395
- )
396
- })}
397
- </div>
398
- )
399
- }
400
-
401
- // ---------------------------------------------------------------------------
402
- // Main page
403
- // ---------------------------------------------------------------------------
404
-
405
- export function AddonsPage() {
406
- const client = useBackendClient()
407
- const queryClient = useQueryClient()
408
- const [activeTab, setActiveTab] = useState<Tab>('installed')
409
-
410
- const { data: rawAddons, isLoading, isError } = useQuery<RawAddonListItem[]>({
411
- queryKey: ['addons', 'list'],
412
- queryFn: () => client.listAddons() as unknown as Promise<RawAddonListItem[]>,
413
- })
414
-
415
- // Fetch agents for the "Add agent" dropdown in AddonCard
416
- const { data: rawAgents = [] } = useQuery({
417
- queryKey: ['agents', 'list'],
418
- queryFn: () => client.listAgents(),
419
- staleTime: 30_000,
420
- })
421
-
422
- const agents: AgentInfo[] = useMemo(
423
- () =>
424
- (rawAgents as unknown as Record<string, unknown>[]).map((raw) => {
425
- const info = (raw.info ?? raw) as Record<string, unknown>
426
- return {
427
- id: String(info.id ?? ''),
428
- name: String(info.name ?? info.id ?? 'Unknown'),
429
- isHub: Boolean(raw.isHub ?? raw.hub ?? false),
430
- }
431
- }),
432
- [rawAgents],
433
- )
434
-
435
- const { data: updates = [], isFetching: isChecking } = useQuery({
436
- queryKey: ['updates'],
437
- queryFn: () => client.trpc.update.listUpdates.query(),
438
- staleTime: 60_000,
439
- })
440
-
441
- const reloadMutation = useMutation({
442
- mutationFn: () => client.trpc.bridgeAddons.reloadPackages.mutate(),
443
- onSuccess: () => {
444
- queryClient.invalidateQueries({ queryKey: ['addons'] })
445
- },
446
- })
447
-
448
- const updatePackageMutation = useMutation({
449
- mutationFn: (name: string) => client.trpc.update.updatePackage.mutate({ name }),
450
- onSuccess: () => {
451
- queryClient.invalidateQueries({ queryKey: ['updates'] })
452
- queryClient.invalidateQueries({ queryKey: ['addons'] })
453
- },
454
- })
455
-
456
- const addonList: AddonListItem[] = (rawAddons ?? []).map(normaliseAddon).filter((a): a is AddonListItem => a !== null)
457
-
458
- function handleCheckUpdates() {
459
- queryClient.invalidateQueries({ queryKey: ['updates'] })
460
- }
461
-
462
- function handleUpdate(id: string) {
463
- updatePackageMutation.mutate(id)
464
- }
465
-
466
- function handleUpdateAll() {
467
- for (const u of updates) {
468
- updatePackageMutation.mutate((u as unknown as UpdateEntry).id)
469
- }
470
- }
471
-
472
- return (
473
- <div className="p-6 space-y-5">
474
- {/* Header */}
475
- <div className="flex items-center justify-between">
476
- <div>
477
- <h1 className="text-lg font-semibold text-foreground">Addons &amp; Updates</h1>
478
- <p className="text-xs text-foreground-subtle mt-0.5">
479
- Manage installed addons, check for updates, and explore capability providers.
480
- </p>
481
- </div>
482
- {!isLoading && !isError && (
483
- <span className="text-[10px] rounded-full bg-primary/10 text-primary px-2 py-0.5 font-medium">
484
- {addonList.length} installed
485
- </span>
486
- )}
487
- </div>
488
-
489
- {/* Tabs */}
490
- <TabBar active={activeTab} onChange={setActiveTab} />
491
-
492
- {/* Loading / error states */}
493
- {isLoading && activeTab === 'installed' && (
494
- <div className="text-xs text-foreground-subtle animate-pulse">Loading…</div>
495
- )}
496
- {isError && activeTab === 'installed' && (
497
- <div className="text-xs text-danger">Failed to load addons</div>
498
- )}
499
-
500
- {/* Tab content */}
501
- {activeTab === 'installed' && !isLoading && !isError && (
502
- <InstalledTab
503
- addons={addonList}
504
- agents={agents}
505
- onRefresh={() => reloadMutation.mutate()}
506
- isRefreshing={reloadMutation.isPending}
507
- />
508
- )}
509
-
510
- {activeTab === 'updates' && (
511
- <UpdatesList
512
- updates={updates as unknown as UpdateEntry[]}
513
- onUpdate={handleUpdate}
514
- onUpdateAll={handleUpdateAll}
515
- onCheckUpdates={handleCheckUpdates}
516
- isChecking={isChecking}
517
- />
518
- )}
519
-
520
- {activeTab === 'map' && (
521
- <CapabilityMap capabilities={deriveCapabilities(addonList)} />
522
- )}
523
- </div>
524
- )
525
- }