@nastechai/agent 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/eslint.config.js +23 -0
  2. package/index.html +24 -0
  3. package/package.json +54 -26
  4. package/package.json.bak +89 -0
  5. package/package.json.pub +88 -0
  6. package/src/App.tsx +1173 -0
  7. package/src/components/AuthWidget.tsx +150 -0
  8. package/src/components/AutoField.tsx +206 -0
  9. package/src/components/Backdrop.tsx +93 -0
  10. package/src/components/ChatSidebar.tsx +394 -0
  11. package/src/components/DeleteConfirmDialog.tsx +40 -0
  12. package/src/components/LanguageSwitcher.tsx +186 -0
  13. package/src/components/Markdown.tsx +383 -0
  14. package/src/components/ModelInfoCard.tsx +112 -0
  15. package/src/components/ModelPickerDialog.tsx +470 -0
  16. package/src/components/OAuthLoginModal.tsx +374 -0
  17. package/src/components/OAuthProvidersCard.tsx +287 -0
  18. package/src/components/PlatformsCard.tsx +97 -0
  19. package/src/components/ScheduleBuilder.tsx +273 -0
  20. package/src/components/SidebarFooter.tsx +42 -0
  21. package/src/components/SidebarStatusStrip.tsx +72 -0
  22. package/src/components/SlashPopover.tsx +171 -0
  23. package/src/components/ThemeSwitcher.tsx +243 -0
  24. package/src/components/ToolCall.tsx +228 -0
  25. package/src/components/ToolsetConfigDrawer.tsx +448 -0
  26. package/src/contexts/PageHeaderProvider.tsx +139 -0
  27. package/src/contexts/SystemActions.tsx +120 -0
  28. package/src/contexts/page-header-context.ts +12 -0
  29. package/src/contexts/system-actions-context.ts +18 -0
  30. package/src/contexts/usePageHeader.ts +10 -0
  31. package/src/contexts/useSystemActions.ts +15 -0
  32. package/src/hooks/useModalBehavior.ts +44 -0
  33. package/src/hooks/useSidebarStatus.ts +27 -0
  34. package/src/i18n/af.ts +702 -0
  35. package/src/i18n/context.tsx +123 -0
  36. package/src/i18n/de.ts +701 -0
  37. package/src/i18n/en.ts +708 -0
  38. package/src/i18n/es.ts +701 -0
  39. package/src/i18n/fr.ts +701 -0
  40. package/src/i18n/ga.ts +702 -0
  41. package/src/i18n/hu.ts +702 -0
  42. package/src/i18n/index.ts +2 -0
  43. package/src/i18n/it.ts +701 -0
  44. package/src/i18n/ja.ts +702 -0
  45. package/src/i18n/ko.ts +702 -0
  46. package/src/i18n/pt.ts +702 -0
  47. package/src/i18n/ru.ts +702 -0
  48. package/src/i18n/tr.ts +702 -0
  49. package/src/i18n/types.ts +710 -0
  50. package/src/i18n/uk.ts +702 -0
  51. package/src/i18n/zh-hant.ts +702 -0
  52. package/src/i18n/zh.ts +698 -0
  53. package/src/index.css +274 -0
  54. package/src/lib/api.ts +1585 -0
  55. package/src/lib/dashboard-flags.ts +15 -0
  56. package/src/lib/format.ts +9 -0
  57. package/src/lib/fuzzy.ts +192 -0
  58. package/src/lib/gatewayClient.ts +253 -0
  59. package/src/lib/nested.ts +23 -0
  60. package/src/lib/resolve-page-title.ts +41 -0
  61. package/src/lib/schedule.ts +382 -0
  62. package/src/lib/slashExec.ts +163 -0
  63. package/src/lib/utils.ts +35 -0
  64. package/src/main.tsx +25 -0
  65. package/src/pages/AnalyticsPage.tsx +601 -0
  66. package/src/pages/ChannelsPage.tsx +772 -0
  67. package/src/pages/ChatPage.tsx +889 -0
  68. package/src/pages/ConfigPage.tsx +660 -0
  69. package/src/pages/CronPage.tsx +524 -0
  70. package/src/pages/DocsPage.tsx +69 -0
  71. package/src/pages/EnvPage.tsx +918 -0
  72. package/src/pages/LogsPage.tsx +246 -0
  73. package/src/pages/McpPage.tsx +757 -0
  74. package/src/pages/ModelsPage.tsx +994 -0
  75. package/src/pages/PairingPage.tsx +276 -0
  76. package/src/pages/PluginsPage.tsx +580 -0
  77. package/src/pages/ProfilesPage.tsx +559 -0
  78. package/src/pages/SessionsPage.tsx +936 -0
  79. package/src/pages/SkillsPage.tsx +557 -0
  80. package/src/pages/SystemPage.tsx +1259 -0
  81. package/src/pages/WebhooksPage.tsx +483 -0
  82. package/src/plugins/PluginPage.tsx +64 -0
  83. package/src/plugins/index.ts +6 -0
  84. package/src/plugins/registry.ts +151 -0
  85. package/src/plugins/sdk.d.ts +160 -0
  86. package/src/plugins/slots.ts +199 -0
  87. package/src/plugins/types.ts +37 -0
  88. package/src/plugins/usePlugins.ts +133 -0
  89. package/src/themes/context.tsx +443 -0
  90. package/src/themes/fonts.ts +160 -0
  91. package/src/themes/index.ts +3 -0
  92. package/src/themes/presets.ts +477 -0
  93. package/src/themes/types.ts +187 -0
  94. package/tsconfig.app.json +34 -0
  95. package/tsconfig.json +7 -0
  96. package/tsconfig.node.json +26 -0
  97. package/vite.config.ts +124 -0
  98. package/vite.config.ts.timestamp-1780999102396-af6b77b30ebd8.mjs +105 -0
@@ -0,0 +1,580 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { ExternalLink, RefreshCw, Trash2, Eye, EyeOff } from "lucide-react";
3
+ import type { Translations } from "@/i18n/types";
4
+ import { Link } from "react-router-dom";
5
+ import { api } from "@/lib/api";
6
+ import type { HubAgentPluginRow, PluginsHubResponse } from "@/lib/api";
7
+ import { Button } from "@nastechai/ui/ui/components/button";
8
+ import { Badge } from "@nastechai/ui/ui/components/badge";
9
+ import { Select, SelectOption } from "@nastechai/ui/ui/components/select";
10
+ import { Switch } from "@nastechai/ui/ui/components/switch";
11
+ import { Spinner } from "@nastechai/ui/ui/components/spinner";
12
+ import { CommandBlock } from "@nastechai/ui/ui/components/command-block";
13
+ import { Card, CardContent, CardHeader, CardTitle } from "@nastechai/ui/ui/components/card";
14
+ import { ConfirmDialog } from "@nastechai/ui/ui/components/confirm-dialog";
15
+ import { Input } from "@nastechai/ui/ui/components/input";
16
+ import { Label } from "@nastechai/ui/ui/components/label";
17
+ import { useToast } from "@nastechai/ui/hooks/use-toast";
18
+ import { Toast } from "@nastechai/ui/ui/components/toast";
19
+ import { useI18n } from "@/i18n";
20
+ import { PluginSlot } from "@/plugins";
21
+ import { cn } from "@/lib/utils";
22
+ import { usePageHeader } from "@/contexts/usePageHeader";
23
+
24
+ /** Select value for built-in memory (`config` uses empty string). Never use `""` — UI Select maps empty value to an empty label. */
25
+ const MEMORY_PROVIDER_BUILTIN = "__nastech_memory_builtin__";
26
+
27
+ export default function PluginsPage() {
28
+ const [hub, setHub] = useState<PluginsHubResponse | null>(null);
29
+ const [loading, setLoading] = useState(true);
30
+ const [installId, setInstallId] = useState("");
31
+ const [installForce, setInstallForce] = useState(false);
32
+ const [installEnable, setInstallEnable] = useState(true);
33
+ const [installBusy, setInstallBusy] = useState(false);
34
+ const [rescanBusy, setRescanBusy] = useState(false);
35
+ const [memorySel, setMemorySel] = useState(MEMORY_PROVIDER_BUILTIN);
36
+ const [contextSel, setContextSel] = useState("compressor");
37
+ const [providerBusy, setProviderBusy] = useState(false);
38
+ const [rowBusy, setRowBusy] = useState<string | null>(null);
39
+
40
+ const { toast, showToast } = useToast();
41
+ const { t } = useI18n();
42
+ const { setAfterTitle } = usePageHeader();
43
+
44
+ const loadHub = useCallback(() => {
45
+ return api
46
+ .getPluginsHub()
47
+ .then((h) => {
48
+ setHub(h);
49
+ const p = h.providers;
50
+ setMemorySel(p.memory_provider ? p.memory_provider : MEMORY_PROVIDER_BUILTIN);
51
+ setContextSel(p.context_engine || "compressor");
52
+ })
53
+ .catch(() => showToast(t.common.loading, "error"));
54
+ }, [showToast, t.common.loading]);
55
+
56
+ useEffect(() => {
57
+ setLoading(true);
58
+ void loadHub().finally(() => setLoading(false));
59
+ }, [loadHub]);
60
+
61
+ useEffect(() => {
62
+ setAfterTitle(
63
+ <Button
64
+ ghost
65
+ size="icon"
66
+ className="shrink-0 text-muted-foreground hover:text-foreground"
67
+ disabled={loading || rescanBusy}
68
+ onClick={() => void onRescan()}
69
+ aria-label={t.pluginsPage.refreshDashboard}
70
+ >
71
+ {rescanBusy ? <Spinner /> : <RefreshCw />}
72
+ </Button>,
73
+ );
74
+ return () => setAfterTitle(null);
75
+ }, [loading, rescanBusy, setAfterTitle, t.pluginsPage.refreshDashboard]);
76
+
77
+ const onInstall = async () => {
78
+ const id = installId.trim();
79
+ if (!id) {
80
+ showToast(t.pluginsPage.installHint, "error");
81
+ return;
82
+ }
83
+ setInstallBusy(true);
84
+ try {
85
+ const r = await api.installAgentPlugin({
86
+ identifier: id,
87
+ force: installForce,
88
+ enable: installEnable,
89
+ });
90
+ showToast(`${r.plugin_name ?? id} installed`, "success");
91
+ if ((r.warnings?.length ?? 0) > 0) showToast(r.warnings!.join(" "), "error");
92
+ if ((r.missing_env?.length ?? 0) > 0)
93
+ showToast(`${t.pluginsPage.missingEnvWarn} ${r.missing_env!.join(", ")}`, "error");
94
+ setInstallId("");
95
+ await loadHub();
96
+ } catch (e) {
97
+ showToast(e instanceof Error ? e.message : "Install failed", "error");
98
+ } finally {
99
+ setInstallBusy(false);
100
+ }
101
+ };
102
+
103
+ const onRescan = async () => {
104
+ setRescanBusy(true);
105
+ try {
106
+ const rc = await api.rescanPlugins();
107
+ showToast(
108
+ `${t.pluginsPage.refreshDashboard} (${rc.count})`,
109
+ "success",
110
+ );
111
+ await loadHub();
112
+ } catch (e) {
113
+ showToast(e instanceof Error ? e.message : "Rescan failed", "error");
114
+ } finally {
115
+ setRescanBusy(false);
116
+ }
117
+ };
118
+
119
+ const onSaveProviders = async () => {
120
+ setProviderBusy(true);
121
+ try {
122
+ await api.savePluginProviders({
123
+ memory_provider:
124
+ memorySel === MEMORY_PROVIDER_BUILTIN ? "" : memorySel,
125
+ context_engine: contextSel,
126
+ });
127
+ showToast(t.pluginsPage.savedProviders, "success");
128
+ await loadHub();
129
+ } catch (e) {
130
+ showToast(e instanceof Error ? e.message : "Save failed", "error");
131
+ } finally {
132
+ setProviderBusy(false);
133
+ }
134
+ };
135
+
136
+ const setRuntimeLoading = async (name: string, fn: () => Promise<unknown>) => {
137
+ setRowBusy(name);
138
+ try {
139
+ await fn();
140
+ await loadHub();
141
+ } catch (e) {
142
+ showToast(e instanceof Error ? e.message : "Failed", "error");
143
+ } finally {
144
+ setRowBusy(null);
145
+ }
146
+ };
147
+
148
+ const rows = hub?.plugins ?? [];
149
+ const providers = hub?.providers;
150
+
151
+ return (
152
+ <div className="flex flex-col gap-4">
153
+ <PluginSlot name="plugins:top" />
154
+
155
+ <div className={cn("flex w-full flex-col gap-8")}>
156
+
157
+ {providers && (
158
+ <Card>
159
+ <CardHeader>
160
+ <CardTitle>{t.pluginsPage.providersHeading}</CardTitle>
161
+ <p className="text-xs tracking-[0.08em] text-text-tertiary">
162
+ {t.pluginsPage.providersHint}
163
+ </p>
164
+ </CardHeader>
165
+
166
+ <CardContent className="flex flex-col gap-6">
167
+
168
+ <div className="grid gap-6 sm:grid-cols-2 max-w-full">
169
+ <div className="grid gap-2 min-w-0">
170
+ <Label htmlFor="mem-provider">{t.pluginsPage.memoryProviderLabel}</Label>
171
+
172
+ <Select
173
+ id="mem-provider"
174
+ className="w-full"
175
+ value={memorySel}
176
+ onValueChange={setMemorySel}
177
+ >
178
+ <SelectOption value={MEMORY_PROVIDER_BUILTIN}>
179
+ {`(${t.pluginsPage.providerDefaults})`}
180
+ </SelectOption>
181
+
182
+ {providers.memory_options.map((o) => (
183
+ <SelectOption key={o.name} value={o.name}>
184
+ {o.name}
185
+ </SelectOption>
186
+ ))}
187
+ </Select>
188
+ </div>
189
+
190
+ <div className="grid gap-2 min-w-0">
191
+ <Label htmlFor="ctx-engine">{t.pluginsPage.contextEngineLabel}</Label>
192
+
193
+ <Select
194
+ id="ctx-engine"
195
+ className="w-full"
196
+ value={contextSel}
197
+ onValueChange={setContextSel}
198
+ >
199
+ <SelectOption value="compressor">compressor</SelectOption>
200
+
201
+ {providers.context_options
202
+ .filter((o) => o.name !== "compressor")
203
+ .map((o) => (
204
+ <SelectOption key={o.name} value={o.name}>
205
+ {o.name}
206
+ </SelectOption>
207
+ ))}
208
+ </Select>
209
+ </div>
210
+ </div>
211
+
212
+ <Button
213
+ className="w-fit uppercase"
214
+ size="sm"
215
+ disabled={providerBusy}
216
+ onClick={() => void onSaveProviders()}
217
+ prefix={providerBusy ? <Spinner /> : undefined}
218
+ >
219
+ {t.common.save}
220
+ </Button>
221
+ </CardContent>
222
+ </Card>
223
+ )}
224
+
225
+ <Card>
226
+ <CardHeader>
227
+ <CardTitle>{t.pluginsPage.installHeading}</CardTitle>
228
+ <p className="text-xs tracking-[0.08em] text-text-tertiary">
229
+ {t.pluginsPage.installHint}
230
+ </p>
231
+ </CardHeader>
232
+
233
+
234
+ <CardContent className="flex flex-col gap-4">
235
+
236
+ <div className="flex flex-col gap-2">
237
+
238
+ <Label htmlFor="install-url">{t.pluginsPage.identifierLabel}</Label>
239
+
240
+ <Input
241
+ className="font-mono-ui lowercase"
242
+ id="install-url"
243
+ placeholder="owner/repo or https://..."
244
+ spellCheck={false}
245
+ value={installId}
246
+ onChange={(e) => setInstallId(e.target.value)}
247
+ />
248
+ </div>
249
+
250
+
251
+ <div className="flex flex-wrap items-center gap-8">
252
+
253
+ <div className="flex items-center gap-3">
254
+
255
+ <Switch checked={installForce} onCheckedChange={setInstallForce} />
256
+
257
+ <span className="text-xs tracking-[0.06em] text-text-secondary">
258
+ {t.pluginsPage.forceReinstall}
259
+ </span>
260
+ </div>
261
+
262
+ <div className="flex items-center gap-3">
263
+
264
+ <Switch checked={installEnable} onCheckedChange={setInstallEnable} />
265
+
266
+ <span className="text-xs tracking-[0.06em] text-text-secondary">
267
+ {t.pluginsPage.enableAfterInstall}
268
+ </span>
269
+ </div>
270
+ </div>
271
+
272
+ <Button
273
+ className="w-fit uppercase"
274
+ size="sm"
275
+ disabled={installBusy}
276
+ onClick={() => void onInstall()}
277
+ prefix={installBusy ? <Spinner /> : undefined}
278
+ >
279
+ {t.pluginsPage.installBtn}
280
+ </Button>
281
+
282
+ <p className="text-xs tracking-[0.06em] text-text-tertiary">
283
+ {t.pluginsPage.rescanHint}
284
+ </p>
285
+
286
+ <p className="text-xs tracking-[0.06em] text-text-tertiary">
287
+ {t.pluginsPage.removeHint}
288
+ </p>
289
+ </CardContent>
290
+ </Card>
291
+
292
+ <div className="flex flex-col gap-3">
293
+
294
+ <h3 className="font-mondwest text-display text-xs tracking-[0.12em] text-text-secondary">
295
+ {t.pluginsPage.pluginListHeading}
296
+ </h3>
297
+
298
+ {loading ? (
299
+
300
+ <div className="flex items-center gap-2 py-8 text-xs text-text-tertiary">
301
+
302
+ <Spinner />
303
+ <span>{t.common.loading}</span>
304
+ </div>
305
+ ) : rows.length === 0 ? (
306
+
307
+ <p className="text-xs text-text-tertiary">{t.common.noResults}</p>
308
+ ) : (
309
+
310
+ <ul className="flex flex-col gap-3">
311
+
312
+ {rows.map((row: HubAgentPluginRow) => (
313
+
314
+ <li key={row.name}>
315
+
316
+
317
+ <PluginRowCard
318
+ {...{ row, rowBusy, setRuntimeLoading, showToast, t }}
319
+ />
320
+
321
+ </li>
322
+ ))}
323
+ </ul>
324
+ )}
325
+ </div>
326
+
327
+ {(hub?.orphan_dashboard_plugins?.length ?? 0) > 0 ? (
328
+
329
+
330
+ <div className="flex flex-col gap-3 opacity-95">
331
+
332
+ <h3 className="font-mondwest text-display text-xs tracking-[0.12em] text-text-secondary">
333
+ {t.pluginsPage.orphanHeading}
334
+ </h3>
335
+
336
+ <ul className="flex flex-col gap-2 rounded border border-current/15 p-4">
337
+
338
+ {hub!.orphan_dashboard_plugins.map((m) => (
339
+
340
+ <li className="text-xs text-text-secondary" key={m.name}>
341
+
342
+
343
+ {m.label ?? m.name} — {m.description || m.tab?.path}
344
+
345
+
346
+ {!m.tab?.hidden ? (
347
+
348
+
349
+ <Link className="ml-3 inline-flex items-center gap-1 underline" to={m.tab.path}>
350
+
351
+
352
+ <ExternalLink className="h-3 w-3 opacity-65" />
353
+
354
+ {t.pluginsPage.openTab}
355
+ </Link>
356
+ ) : null}
357
+ </li>
358
+ ))}
359
+ </ul>
360
+ </div>
361
+ ) : null}
362
+ </div>
363
+
364
+ <Toast toast={toast} />
365
+ <PluginSlot name="plugins:bottom" />
366
+ </div>
367
+ );
368
+ }
369
+
370
+ interface PluginRowCardProps {
371
+
372
+ row: HubAgentPluginRow;
373
+ rowBusy: string | null;
374
+ setRuntimeLoading: (
375
+ name: string,
376
+ fn: () => Promise<unknown>,
377
+ ) => Promise<void>;
378
+
379
+ showToast: (msg: string, variant: "success" | "error") => void;
380
+ t: Translations;
381
+ }
382
+
383
+ function PluginRowCard(props: PluginRowCardProps) {
384
+ const {
385
+ row,
386
+ rowBusy,
387
+ setRuntimeLoading,
388
+ showToast,
389
+ t,
390
+ } = props;
391
+
392
+ const dm = row.dashboard_manifest;
393
+
394
+ const tabPath = dm?.tab && !dm.tab.hidden ? dm.tab.override ?? dm.tab.path : null;
395
+
396
+ const busy = rowBusy === row.name;
397
+ const [confirmRemove, setConfirmRemove] = useState(false);
398
+
399
+ const badgeTone =
400
+ row.runtime_status === "enabled"
401
+ ? "success"
402
+ : row.runtime_status === "disabled"
403
+ ? "destructive"
404
+ : "outline";
405
+
406
+ return (
407
+
408
+ <Card className={cn(busy ? "opacity-70" : undefined)}>
409
+
410
+
411
+ <CardContent className="flex flex-col gap-4 px-6 py-4">
412
+
413
+
414
+ <div className="flex flex-wrap items-start justify-between gap-4">
415
+
416
+ <div className="flex min-w-0 flex-1 flex-wrap items-center gap-3">
417
+
418
+ <span className="truncate font-semibold">{row.name}</span>
419
+
420
+ <Badge tone="outline">
421
+ {t.pluginsPage.sourceBadge}: {row.source}
422
+ </Badge>
423
+
424
+ <Badge tone="outline">v{row.version || "—"}</Badge>
425
+
426
+ <Badge tone={badgeTone}>{row.runtime_status}</Badge>
427
+
428
+ {row.auth_required ? (
429
+ <Badge tone="destructive">{t.pluginsPage.authRequired}</Badge>
430
+ ) : null}
431
+ </div>
432
+
433
+ <div className="flex flex-wrap items-center gap-2 shrink-0">
434
+ {row.runtime_status === "enabled" ? (
435
+ <Button
436
+ disabled={busy}
437
+ ghost
438
+ size="sm"
439
+ onClick={() => {
440
+ void setRuntimeLoading(row.name, async () => {
441
+ await api.disableAgentPlugin(row.name);
442
+ showToast(t.pluginsPage.disableRuntime, "success");
443
+ });
444
+ }}
445
+ >
446
+ {t.pluginsPage.disableRuntime}
447
+ </Button>
448
+ ) : (
449
+ <Button
450
+ disabled={busy}
451
+ ghost
452
+ size="sm"
453
+ onClick={() => {
454
+ void setRuntimeLoading(row.name, async () => {
455
+ await api.enableAgentPlugin(row.name);
456
+ showToast(t.pluginsPage.enableRuntime, "success");
457
+ });
458
+ }}
459
+ >
460
+ {t.pluginsPage.enableRuntime}
461
+ </Button>
462
+ )}
463
+
464
+ {tabPath ? (
465
+
466
+ <Link
467
+ className={cn(
468
+ "inline-flex items-center rounded-none px-3 py-1.5",
469
+ "border border-current/25 hover:bg-current/10",
470
+ "font-mondwest text-display text-xs tracking-[0.1em]",
471
+ )}
472
+ to={tabPath}
473
+ >
474
+ {t.pluginsPage.openTab}
475
+ </Link>
476
+ ) : null}
477
+
478
+ {row.can_update_git ? (
479
+
480
+ <Button
481
+ disabled={busy}
482
+ ghost
483
+ size="sm"
484
+ onClick={() => {
485
+ void setRuntimeLoading(row.name, async () => {
486
+ await api.updateAgentPlugin(row.name);
487
+ showToast(t.pluginsPage.updateGit, "success");
488
+ });
489
+ }}
490
+ >
491
+ {busy ? <Spinner /> : null}
492
+ {t.pluginsPage.updateGit}
493
+ </Button>
494
+ ) : null}
495
+
496
+ {row.has_dashboard_manifest ? (
497
+ <Button
498
+ disabled={busy}
499
+ ghost
500
+ size="sm"
501
+ title={row.user_hidden ? t.pluginsPage.showInSidebar : t.pluginsPage.hideFromSidebar}
502
+ onClick={() => {
503
+ void setRuntimeLoading(row.name, async () => {
504
+ await api.setPluginVisibility(row.name, !row.user_hidden);
505
+ });
506
+ }}
507
+ >
508
+ {row.user_hidden ? (
509
+ <EyeOff className="h-3.5 w-3.5" />
510
+ ) : (
511
+ <Eye className="h-3.5 w-3.5" />
512
+ )}
513
+ {row.user_hidden ? t.pluginsPage.showInSidebar : t.pluginsPage.hideFromSidebar}
514
+ </Button>
515
+ ) : null}
516
+
517
+ {row.can_remove ? (
518
+
519
+
520
+ <Button
521
+ destructive
522
+ disabled={busy}
523
+ ghost
524
+ size="sm"
525
+ onClick={() => setConfirmRemove(true)}
526
+ >
527
+
528
+ {busy ? <Spinner /> : <Trash2 className="h-3.5 w-3.5" />}
529
+ </Button>
530
+ ) : null}
531
+ </div>
532
+ </div>
533
+
534
+ {row.description ? (
535
+ <p className="min-w-0 w-full text-xs tracking-[0.06em] text-text-secondary break-words">
536
+ {row.description}
537
+ </p>
538
+ ) : null}
539
+
540
+ {dm?.slots?.length ? (
541
+
542
+ <p className="text-xs tracking-[0.05em] text-text-tertiary">
543
+ {t.pluginsPage.dashboardSlots}: {dm.slots.join(", ")}
544
+ </p>
545
+ ) : null}
546
+
547
+ {row.auth_required ? (
548
+ <CommandBlock
549
+ label={t.pluginsPage.authRequiredHint}
550
+ code={row.auth_command}
551
+ />
552
+ ) : null}
553
+
554
+ {!row.has_dashboard_manifest && !dm ? (
555
+
556
+
557
+ <p className="text-xs italic text-text-disabled">
558
+ {t.pluginsPage.noDashboardTab}
559
+ </p>
560
+ ) : null}
561
+ </CardContent>
562
+
563
+ <ConfirmDialog
564
+ open={confirmRemove}
565
+ onCancel={() => setConfirmRemove(false)}
566
+ onConfirm={() => {
567
+ setConfirmRemove(false);
568
+ void setRuntimeLoading(row.name, async () => {
569
+ await api.removeAgentPlugin(row.name);
570
+ showToast(`${row.name} removed`, "success");
571
+ });
572
+ }}
573
+ title={t.pluginsPage.removeConfirm}
574
+ description={`This will remove the "${row.name}" plugin from your agent.`}
575
+ destructive
576
+ confirmLabel={t.common.delete}
577
+ />
578
+ </Card>
579
+ );
580
+ }