@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,483 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useState } from "react";
2
+ import { Webhook, Plus, Trash2, X, Copy, Check } from "lucide-react";
3
+ import { Badge } from "@nastechai/ui/ui/components/badge";
4
+ import { Button } from "@nastechai/ui/ui/components/button";
5
+ import { Select, SelectOption } from "@nastechai/ui/ui/components/select";
6
+ import { Spinner } from "@nastechai/ui/ui/components/spinner";
7
+ import { H2 } from "@nastechai/ui/ui/components/typography/h2";
8
+ import { api } from "@/lib/api";
9
+ import type { WebhookRoute, WebhooksResponse } from "@/lib/api";
10
+ import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
11
+ import { useToast } from "@nastechai/ui/hooks/use-toast";
12
+ import { useConfirmDelete } from "@nastechai/ui/hooks/use-confirm-delete";
13
+ import { useModalBehavior } from "@/hooks/useModalBehavior";
14
+ import { Toast } from "@nastechai/ui/ui/components/toast";
15
+ import { Card, CardContent } from "@nastechai/ui/ui/components/card";
16
+ import { Input } from "@nastechai/ui/ui/components/input";
17
+ import { Label } from "@nastechai/ui/ui/components/label";
18
+ import { usePageHeader } from "@/contexts/usePageHeader";
19
+ import { cn, themedBody } from "@/lib/utils";
20
+
21
+ interface CreatedWebhook {
22
+ url: string;
23
+ secret: string;
24
+ }
25
+
26
+ function CopyButton({ value }: { value: string }) {
27
+ const [copied, setCopied] = useState(false);
28
+ const handleCopy = useCallback(() => {
29
+ navigator.clipboard
30
+ .writeText(value)
31
+ .then(() => {
32
+ setCopied(true);
33
+ window.setTimeout(() => setCopied(false), 1500);
34
+ })
35
+ .catch(() => {});
36
+ }, [value]);
37
+ return (
38
+ <Button
39
+ ghost
40
+ size="icon"
41
+ title="Copy"
42
+ aria-label="Copy"
43
+ onClick={handleCopy}
44
+ className="text-muted-foreground hover:text-foreground"
45
+ >
46
+ {copied ? <Check /> : <Copy />}
47
+ </Button>
48
+ );
49
+ }
50
+
51
+ export default function WebhooksPage() {
52
+ const [data, setData] = useState<WebhooksResponse | null>(null);
53
+ const [loading, setLoading] = useState(true);
54
+ const { toast, showToast } = useToast();
55
+ const { setEnd } = usePageHeader();
56
+
57
+ // New subscription modal state
58
+ const [createModalOpen, setCreateModalOpen] = useState(false);
59
+ const [name, setName] = useState("");
60
+ const [description, setDescription] = useState("");
61
+ const [events, setEvents] = useState("");
62
+ const [deliver, setDeliver] = useState("log");
63
+ const [deliverOnly, setDeliverOnly] = useState(false);
64
+ const [prompt, setPrompt] = useState("");
65
+ const [creating, setCreating] = useState(false);
66
+ const [created, setCreated] = useState<CreatedWebhook | null>(null);
67
+
68
+ const closeCreateModal = useCallback(() => {
69
+ setCreateModalOpen(false);
70
+ setCreated(null);
71
+ }, []);
72
+ const createModalRef = useModalBehavior({
73
+ open: createModalOpen,
74
+ onClose: closeCreateModal,
75
+ });
76
+
77
+ const enabled = data?.enabled ?? false;
78
+ const subscriptions = data?.subscriptions ?? [];
79
+
80
+ const loadWebhooks = useCallback(() => {
81
+ api
82
+ .getWebhooks()
83
+ .then(setData)
84
+ .catch(() => showToast("Failed to load webhooks", "error"))
85
+ .finally(() => setLoading(false));
86
+ }, [showToast]);
87
+
88
+ useEffect(() => {
89
+ loadWebhooks();
90
+ }, [loadWebhooks]);
91
+
92
+ const resetForm = useCallback(() => {
93
+ setName("");
94
+ setDescription("");
95
+ setEvents("");
96
+ setDeliver("log");
97
+ setDeliverOnly(false);
98
+ setPrompt("");
99
+ }, []);
100
+
101
+ const handleCreate = async () => {
102
+ if (!name.trim()) {
103
+ showToast("Name required", "error");
104
+ return;
105
+ }
106
+ setCreating(true);
107
+ try {
108
+ const eventsList = events
109
+ .split(",")
110
+ .map((e) => e.trim())
111
+ .filter(Boolean);
112
+ const res = await api.createWebhook({
113
+ name: name.trim(),
114
+ description: description.trim() || undefined,
115
+ events: eventsList.length ? eventsList : undefined,
116
+ deliver,
117
+ deliver_only: deliverOnly,
118
+ prompt: prompt.trim() || undefined,
119
+ });
120
+ showToast("Created ✓", "success");
121
+ setCreated({ url: res.url, secret: res.secret ?? "" });
122
+ resetForm();
123
+ loadWebhooks();
124
+ } catch (e) {
125
+ showToast(`Failed to create: ${e}`, "error");
126
+ } finally {
127
+ setCreating(false);
128
+ }
129
+ };
130
+
131
+ const [togglingName, setTogglingName] = useState<string | null>(null);
132
+
133
+ const handleToggleEnabled = useCallback(
134
+ async (subName: string, nextEnabled: boolean) => {
135
+ setTogglingName(subName);
136
+ try {
137
+ await api.setWebhookEnabled(subName, nextEnabled);
138
+ showToast(
139
+ nextEnabled ? `Enabled: "${subName}"` : `Disabled: "${subName}"`,
140
+ "success",
141
+ );
142
+ loadWebhooks();
143
+ } catch (e) {
144
+ showToast(`Error: ${e}`, "error");
145
+ } finally {
146
+ setTogglingName(null);
147
+ }
148
+ },
149
+ [loadWebhooks, showToast],
150
+ );
151
+
152
+ const webhookDelete = useConfirmDelete({
153
+ onDelete: useCallback(
154
+ async (name: string) => {
155
+ try {
156
+ await api.deleteWebhook(name);
157
+ showToast(`Deleted: "${name}"`, "success");
158
+ loadWebhooks();
159
+ } catch (e) {
160
+ showToast(`Error: ${e}`, "error");
161
+ throw e;
162
+ }
163
+ },
164
+ [loadWebhooks, showToast],
165
+ ),
166
+ });
167
+
168
+ // Put "New subscription" button in page header
169
+ useLayoutEffect(() => {
170
+ setEnd(
171
+ <Button
172
+ className="uppercase"
173
+ size="sm"
174
+ disabled={!enabled}
175
+ prefix={<Plus />}
176
+ onClick={() => {
177
+ setCreated(null);
178
+ setCreateModalOpen(true);
179
+ }}
180
+ >
181
+ New subscription
182
+ </Button>,
183
+ );
184
+ return () => {
185
+ setEnd(null);
186
+ };
187
+ }, [setEnd, enabled, loading]);
188
+
189
+ if (loading) {
190
+ return (
191
+ <div className="flex items-center justify-center py-24">
192
+ <Spinner className="text-2xl text-primary" />
193
+ </div>
194
+ );
195
+ }
196
+
197
+ const pendingName = webhookDelete.pendingId ?? "";
198
+
199
+ return (
200
+ <div className="flex flex-col gap-6">
201
+ <Toast toast={toast} />
202
+
203
+ <DeleteConfirmDialog
204
+ open={webhookDelete.isOpen}
205
+ onCancel={webhookDelete.cancel}
206
+ onConfirm={webhookDelete.confirm}
207
+ title="Delete webhook"
208
+ description={
209
+ pendingName
210
+ ? `"${pendingName}" — this will permanently remove this webhook subscription.`
211
+ : "This will permanently remove this webhook subscription."
212
+ }
213
+ loading={webhookDelete.isDeleting}
214
+ />
215
+
216
+ {/* Create subscription modal */}
217
+ {createModalOpen && (
218
+ <div
219
+ ref={createModalRef}
220
+ className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
221
+ onClick={(e) => e.target === e.currentTarget && closeCreateModal()}
222
+ role="dialog"
223
+ aria-modal="true"
224
+ aria-labelledby="create-webhook-title"
225
+ >
226
+ <div className={cn(themedBody, "relative w-full max-w-lg border border-border bg-card shadow-2xl flex flex-col max-h-[90vh] overflow-y-auto")}>
227
+ <Button
228
+ ghost
229
+ size="icon"
230
+ onClick={closeCreateModal}
231
+ className="absolute right-2 top-2 text-muted-foreground hover:text-foreground"
232
+ aria-label="Close"
233
+ >
234
+ <X />
235
+ </Button>
236
+
237
+ <header className="p-5 pb-3 border-b border-border">
238
+ <h2
239
+ id="create-webhook-title"
240
+ className="font-mondwest text-display text-base tracking-wider"
241
+ >
242
+ New subscription
243
+ </h2>
244
+ </header>
245
+
246
+ {created ? (
247
+ <div className="p-5 grid gap-4">
248
+ <p className="text-sm text-muted-foreground">
249
+ Subscription created. Copy the secret now — it is only shown
250
+ once.
251
+ </p>
252
+
253
+ <div className="grid gap-2">
254
+ <Label>Webhook URL</Label>
255
+ <div className="flex items-center gap-2 border border-border bg-background/40 px-3 py-2">
256
+ <span className="flex-1 min-w-0 truncate font-mono text-xs">
257
+ {created.url}
258
+ </span>
259
+ <CopyButton value={created.url} />
260
+ </div>
261
+ </div>
262
+
263
+ <div className="grid gap-2">
264
+ <Label>Secret (shown once)</Label>
265
+ <div className="flex items-center gap-2 border border-warning/40 bg-warning/10 px-3 py-2">
266
+ <span className="flex-1 min-w-0 truncate font-mono text-xs">
267
+ {created.secret}
268
+ </span>
269
+ <CopyButton value={created.secret} />
270
+ </div>
271
+ </div>
272
+
273
+ <div className="flex justify-end">
274
+ <Button
275
+ className="uppercase"
276
+ size="sm"
277
+ onClick={closeCreateModal}
278
+ >
279
+ Done
280
+ </Button>
281
+ </div>
282
+ </div>
283
+ ) : (
284
+ <div className="p-5 grid gap-4">
285
+ <div className="grid gap-2">
286
+ <Label htmlFor="webhook-name">Name</Label>
287
+ <Input
288
+ id="webhook-name"
289
+ autoFocus
290
+ placeholder="e.g. github-push"
291
+ value={name}
292
+ onChange={(e) => setName(e.target.value)}
293
+ />
294
+ </div>
295
+
296
+ <div className="grid gap-2">
297
+ <Label htmlFor="webhook-description">Description</Label>
298
+ <Input
299
+ id="webhook-description"
300
+ placeholder="What this webhook does (optional)"
301
+ value={description}
302
+ onChange={(e) => setDescription(e.target.value)}
303
+ />
304
+ </div>
305
+
306
+ <div className="grid gap-2">
307
+ <Label htmlFor="webhook-events">Events</Label>
308
+ <Input
309
+ id="webhook-events"
310
+ placeholder="comma-separated, leave empty for all"
311
+ value={events}
312
+ onChange={(e) => setEvents(e.target.value)}
313
+ />
314
+ </div>
315
+
316
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
317
+ <div className="grid gap-2">
318
+ <Label htmlFor="webhook-deliver">Deliver to</Label>
319
+ <Select
320
+ id="webhook-deliver"
321
+ value={deliver}
322
+ onValueChange={(v) => setDeliver(v)}
323
+ >
324
+ <SelectOption value="log">Log</SelectOption>
325
+ <SelectOption value="telegram">Telegram</SelectOption>
326
+ <SelectOption value="discord">Discord</SelectOption>
327
+ <SelectOption value="slack">Slack</SelectOption>
328
+ <SelectOption value="email">Email</SelectOption>
329
+ <SelectOption value="github_comment">
330
+ GitHub comment
331
+ </SelectOption>
332
+ </Select>
333
+ </div>
334
+
335
+ <div className="grid gap-2">
336
+ <Label htmlFor="webhook-deliver-only">Deliver only</Label>
337
+ <label className="flex items-center gap-2 text-sm text-muted-foreground h-9">
338
+ <input
339
+ id="webhook-deliver-only"
340
+ type="checkbox"
341
+ checked={deliverOnly}
342
+ onChange={(e) => setDeliverOnly(e.target.checked)}
343
+ />
344
+ Skip the agent, deliver payload directly
345
+ </label>
346
+ </div>
347
+ </div>
348
+
349
+ <div className="grid gap-2">
350
+ <Label htmlFor="webhook-prompt">Prompt</Label>
351
+ <textarea
352
+ id="webhook-prompt"
353
+ className="flex min-h-[80px] w-full border border-border bg-background/40 px-3 py-2 text-sm font-courier shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30 focus-visible:border-foreground/25"
354
+ placeholder="Instructions for the agent when this webhook fires (optional)"
355
+ value={prompt}
356
+ onChange={(e) => setPrompt(e.target.value)}
357
+ />
358
+ </div>
359
+
360
+ <div className="flex justify-end">
361
+ <Button
362
+ className="uppercase"
363
+ size="sm"
364
+ onClick={handleCreate}
365
+ disabled={creating}
366
+ prefix={creating ? <Spinner /> : undefined}
367
+ >
368
+ {creating ? "Creating…" : "Create"}
369
+ </Button>
370
+ </div>
371
+ </div>
372
+ )}
373
+ </div>
374
+ </div>
375
+ )}
376
+
377
+ {!enabled && (
378
+ <Card>
379
+ <CardContent className="py-6 flex items-start gap-3 text-sm">
380
+ <Webhook className="h-5 w-5 shrink-0 text-warning" />
381
+ <div className="flex flex-col gap-1">
382
+ <span className="font-medium">Webhook platform disabled</span>
383
+ <span className="text-muted-foreground">
384
+ The webhook platform must be enabled in your messaging settings
385
+ before you can create subscriptions. Enable it, then return to
386
+ this page.
387
+ </span>
388
+ </div>
389
+ </CardContent>
390
+ </Card>
391
+ )}
392
+
393
+ <div className="flex flex-col gap-3">
394
+ <H2
395
+ variant="sm"
396
+ className="flex items-center gap-2 text-muted-foreground"
397
+ >
398
+ <Webhook className="h-4 w-4" />
399
+ Subscriptions ({subscriptions.length})
400
+ </H2>
401
+
402
+ <p className="text-xs text-muted-foreground -mt-1">
403
+ Disabled webhooks reject incoming events; the gateway hot-reloads
404
+ changes (no restart needed).
405
+ </p>
406
+
407
+ {subscriptions.length === 0 && (
408
+ <Card>
409
+ <CardContent className="py-8 text-center text-sm text-muted-foreground">
410
+ No webhook subscriptions yet.
411
+ </CardContent>
412
+ </Card>
413
+ )}
414
+
415
+ {subscriptions.map((sub: WebhookRoute) => (
416
+ <Card key={sub.name}>
417
+ <CardContent className="flex items-start gap-4 py-4">
418
+ <div className={cn("flex-1 min-w-0", !sub.enabled && "opacity-60")}>
419
+ <div className="flex items-center gap-2 mb-1 flex-wrap">
420
+ <span className="font-medium text-sm truncate">
421
+ {sub.name}
422
+ </span>
423
+ <Badge tone="outline">{sub.deliver}</Badge>
424
+ {sub.deliver_only && (
425
+ <Badge tone="secondary">deliver only</Badge>
426
+ )}
427
+ {!sub.enabled && <Badge tone="warning">disabled</Badge>}
428
+ </div>
429
+
430
+ {sub.description && (
431
+ <p className="text-xs text-muted-foreground mb-2">
432
+ {sub.description}
433
+ </p>
434
+ )}
435
+
436
+ <div className="flex items-center gap-1 flex-wrap mb-2">
437
+ {sub.events.length === 0 ? (
438
+ <Badge tone="secondary">(all)</Badge>
439
+ ) : (
440
+ sub.events.map((evt) => (
441
+ <Badge key={evt} tone="secondary">
442
+ {evt}
443
+ </Badge>
444
+ ))
445
+ )}
446
+ </div>
447
+
448
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
449
+ <span className="flex-1 min-w-0 truncate font-mono">
450
+ {sub.url}
451
+ </span>
452
+ <CopyButton value={sub.url} />
453
+ </div>
454
+ </div>
455
+
456
+ <div className="flex items-center gap-1 shrink-0">
457
+ <Button
458
+ ghost
459
+ size="sm"
460
+ className="uppercase"
461
+ disabled={togglingName === sub.name}
462
+ onClick={() => handleToggleEnabled(sub.name, !sub.enabled)}
463
+ >
464
+ {sub.enabled ? "Disable" : "Enable"}
465
+ </Button>
466
+ <Button
467
+ ghost
468
+ destructive
469
+ size="icon"
470
+ title="Delete"
471
+ aria-label="Delete"
472
+ onClick={() => webhookDelete.requestDelete(sub.name)}
473
+ >
474
+ <Trash2 />
475
+ </Button>
476
+ </div>
477
+ </CardContent>
478
+ </Card>
479
+ ))}
480
+ </div>
481
+ </div>
482
+ );
483
+ }
@@ -0,0 +1,64 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { Spinner } from "@nastechai/ui/ui/components/spinner";
3
+ import {
4
+ getPluginComponent,
5
+ getPluginLoadError,
6
+ onPluginRegistered,
7
+ } from "./registry";
8
+ import { useI18n } from "@/i18n";
9
+ import { cn } from "@/lib/utils";
10
+ import type { Translations } from "@/i18n/types";
11
+
12
+ /** Renders a plugin tab once its bundle has called `register()`. */
13
+ export function PluginPage({ name }: { name: string }) {
14
+ const { t } = useI18n();
15
+ // Subscribe in render (via useSyncExternalStore) so we never miss
16
+ // `register()` if the script loads before a useEffect would run.
17
+ const Component = useSyncExternalStore(
18
+ (onChange) => onPluginRegistered(onChange),
19
+ () => getPluginComponent(name) ?? null,
20
+ () => null,
21
+ );
22
+ const loadError = useSyncExternalStore(
23
+ (onChange) => onPluginRegistered(onChange),
24
+ () => getPluginLoadError(name) ?? null,
25
+ () => null,
26
+ );
27
+
28
+ if (Component) {
29
+ return <Component />;
30
+ }
31
+
32
+ if (loadError) {
33
+ const message = formatPluginError(loadError, t);
34
+ return (
35
+ <div
36
+ className={cn(
37
+ "max-w-lg p-4",
38
+ "font-mondwest text-sm tracking-[0.08em] text-text-secondary",
39
+ )}
40
+ role="alert"
41
+ >
42
+ {message}
43
+ </div>
44
+ );
45
+ }
46
+
47
+ return (
48
+ <div
49
+ className={cn(
50
+ "flex items-center gap-2 p-4",
51
+ "font-mondwest text-sm tracking-[0.1em] text-text-tertiary",
52
+ )}
53
+ >
54
+ <Spinner className="shrink-0" />
55
+ <span>{t.common.loading}</span>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ function formatPluginError(code: string, t: Translations): string {
61
+ if (code === "LOAD_FAILED") return t.common.pluginLoadFailed;
62
+ if (code === "NO_REGISTER") return t.common.pluginNotRegistered;
63
+ return code;
64
+ }
@@ -0,0 +1,6 @@
1
+ export { exposePluginSDK, getPluginComponent, onPluginRegistered, getRegisteredCount } from "./registry";
2
+ export { PluginPage } from "./PluginPage";
3
+ export { usePlugins } from "./usePlugins";
4
+ export { PluginSlot, KNOWN_SLOT_NAMES, registerSlot, getSlotEntries, onSlotRegistered, unregisterPluginSlots } from "./slots";
5
+ export type { KnownSlotName } from "./slots";
6
+ export type { PluginManifest, RegisteredPlugin } from "./types";