@abraca/nuxt 2.14.0 → 2.16.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 (44) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/assets/editor.css +3 -1
  3. package/dist/runtime/components/ACodeEditor.vue +16 -2
  4. package/dist/runtime/components/ANodePanel.vue +7 -5
  5. package/dist/runtime/components/aware/ASlider.d.vue.ts +1 -1
  6. package/dist/runtime/components/aware/ASlider.vue.d.ts +1 -1
  7. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  8. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  9. package/dist/runtime/components/editor/AColorPalettePopover.vue +97 -5
  10. package/dist/runtime/components/editor/AIconPickerPopover.vue +81 -3
  11. package/dist/runtime/components/editor/AMetaNumberStepper.d.vue.ts +40 -0
  12. package/dist/runtime/components/editor/AMetaNumberStepper.vue +214 -0
  13. package/dist/runtime/components/editor/AMetaNumberStepper.vue.d.ts +40 -0
  14. package/dist/runtime/components/registry/APluginBrowser.vue +18 -2
  15. package/dist/runtime/components/renderers/ACalendarRenderer.vue +7 -1
  16. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  17. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  18. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  19. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  20. package/dist/runtime/components/settings/APluginInstallDialog.d.vue.ts +39 -0
  21. package/dist/runtime/components/settings/APluginInstallDialog.vue +254 -0
  22. package/dist/runtime/components/settings/APluginInstallDialog.vue.d.ts +39 -0
  23. package/dist/runtime/components/settings/APluginsTabInstalled.d.vue.ts +7 -0
  24. package/dist/runtime/components/settings/APluginsTabInstalled.vue +413 -0
  25. package/dist/runtime/components/settings/APluginsTabInstalled.vue.d.ts +7 -0
  26. package/dist/runtime/components/settings/APluginsTabPending.d.vue.ts +24 -0
  27. package/dist/runtime/components/settings/APluginsTabPending.vue +248 -0
  28. package/dist/runtime/components/settings/APluginsTabPending.vue.d.ts +24 -0
  29. package/dist/runtime/components/settings/ASettingsPluginsPanel.d.vue.ts +14 -1
  30. package/dist/runtime/components/settings/ASettingsPluginsPanel.vue +34 -80
  31. package/dist/runtime/components/settings/ASettingsPluginsPanel.vue.d.ts +14 -1
  32. package/dist/runtime/composables/useDeclinedSpacePlugins.d.ts +7 -0
  33. package/dist/runtime/composables/useDeclinedSpacePlugins.js +24 -0
  34. package/dist/runtime/composables/useLoadTimePending.d.ts +29 -0
  35. package/dist/runtime/composables/useLoadTimePending.js +37 -0
  36. package/dist/runtime/composables/usePluginCatalog.d.ts +5 -1
  37. package/dist/runtime/composables/usePluginCatalog.js +34 -0
  38. package/dist/runtime/composables/useTouchDrag.d.ts +21 -4
  39. package/dist/runtime/composables/useTouchDrag.js +30 -0
  40. package/dist/runtime/composables/useUploadedPluginStore.d.ts +43 -0
  41. package/dist/runtime/composables/useUploadedPluginStore.js +66 -0
  42. package/dist/runtime/extensions/views/MetaFieldView.vue +17 -28
  43. package/dist/runtime/plugin-abracadabra.client.js +48 -1
  44. package/package.json +1 -1
@@ -0,0 +1,413 @@
1
+ <script setup>
2
+ import { ref, computed } from "vue";
3
+ import { useToast } from "#imports";
4
+ import { useInstalledPlugins, normalizePluginUrl } from "../../composables/useInstalledPlugins";
5
+ import { usePluginCatalog } from "../../composables/usePluginCatalog";
6
+ import { _putUploadedPluginBlob } from "../../composables/useUploadedPluginStore";
7
+ const props = defineProps({
8
+ builtins: { type: Array, required: false }
9
+ });
10
+ const toast = useToast();
11
+ const catalog = usePluginCatalog();
12
+ const {
13
+ entries,
14
+ install: installEntry,
15
+ uninstall: uninstallEntry,
16
+ setEnabled,
17
+ updateMeta,
18
+ isBuiltinEnabled,
19
+ setBuiltinEnabled
20
+ } = useInstalledPlugins();
21
+ const pendingReload = ref(false);
22
+ const urlInput = ref("");
23
+ const urlError = ref("");
24
+ const dialogOpen = ref(false);
25
+ const dialogMode = ref("url");
26
+ const dialogManifest = ref(null);
27
+ const dialogSource = ref("");
28
+ const dialogPrevAcked = ref(void 0);
29
+ const pendingStorageUrl = ref("");
30
+ async function startUrlInstall() {
31
+ const raw = urlInput.value.trim();
32
+ if (!raw) return;
33
+ urlError.value = "";
34
+ let url;
35
+ try {
36
+ url = normalizePluginUrl(raw);
37
+ } catch (e) {
38
+ urlError.value = e.message;
39
+ return;
40
+ }
41
+ if (entries.value.some((e) => e.url === url)) {
42
+ urlError.value = "Already installed.";
43
+ return;
44
+ }
45
+ let manifest = await catalog.resolveExternalManifest(url);
46
+ if (!manifest) manifest = catalog.synthesizeUnknownManifest(url);
47
+ dialogMode.value = "url";
48
+ dialogManifest.value = manifest;
49
+ dialogSource.value = url;
50
+ pendingStorageUrl.value = url;
51
+ dialogPrevAcked.value = void 0;
52
+ dialogOpen.value = true;
53
+ }
54
+ const fileInput = ref(null);
55
+ function triggerUpload() {
56
+ fileInput.value?.click();
57
+ }
58
+ async function onFileChosen(ev) {
59
+ const input = ev.target;
60
+ const file = input.files?.[0];
61
+ input.value = "";
62
+ if (!file) return;
63
+ const lower = file.name.toLowerCase();
64
+ if (lower.endsWith(".zip")) {
65
+ await handleZipUpload(file);
66
+ return;
67
+ }
68
+ if (!lower.endsWith(".js") && !lower.endsWith(".mjs")) {
69
+ toast.add({ title: "Unsupported file type", description: "Pick a .js / .mjs / .zip bundle.", color: "error" });
70
+ return;
71
+ }
72
+ try {
73
+ const bytes = new Uint8Array(await file.arrayBuffer());
74
+ const sha256 = await sha256Hex(bytes);
75
+ if (entries.value.some((e) => e.sha256 === sha256)) {
76
+ toast.add({ title: "Already uploaded", description: "This bundle is already installed.", color: "info" });
77
+ return;
78
+ }
79
+ await _putUploadedPluginBlob(sha256, bytes, file.type || "text/javascript");
80
+ const url = `idb-plugin:${sha256}`;
81
+ const manifest = catalog.synthesizeUnknownManifest(file.name);
82
+ dialogMode.value = "upload";
83
+ dialogManifest.value = manifest;
84
+ dialogSource.value = file.name;
85
+ pendingStorageUrl.value = url;
86
+ dialogPrevAcked.value = void 0;
87
+ dialogOpen.value = true;
88
+ pendingUploadSha.value = sha256;
89
+ pendingUploadName.value = file.name;
90
+ } catch (e) {
91
+ toast.add({ title: "Upload failed", description: e.message, color: "error" });
92
+ }
93
+ }
94
+ async function handleZipUpload(file) {
95
+ let JSZip = null;
96
+ try {
97
+ const mod = await import(
98
+ /* @vite-ignore */
99
+ "jszip"
100
+ );
101
+ JSZip = mod.default ?? mod;
102
+ } catch {
103
+ toast.add({
104
+ title: "jszip is required for .zip uploads",
105
+ description: "Install jszip as a peer dependency to enable .zip plugin uploads.",
106
+ color: "warning"
107
+ });
108
+ return;
109
+ }
110
+ if (!JSZip) return;
111
+ try {
112
+ const buf = await file.arrayBuffer();
113
+ const zip = await JSZip.loadAsync(buf);
114
+ const jsEntry = zip.file("plugin.js") ?? zip.file("plugin.mjs");
115
+ const manifestEntry = zip.file("plugin.manifest.json");
116
+ if (!jsEntry) {
117
+ toast.add({
118
+ title: "Missing plugin.js",
119
+ description: "The .zip must contain plugin.js (or plugin.mjs) at its root.",
120
+ color: "error"
121
+ });
122
+ return;
123
+ }
124
+ const jsBytes = await jsEntry.async("uint8array");
125
+ let manifest = catalog.synthesizeUnknownManifest(file.name);
126
+ if (manifestEntry) {
127
+ try {
128
+ const txt = await manifestEntry.async("string");
129
+ const parsed = JSON.parse(txt);
130
+ if (parsed && typeof parsed.id === "string") manifest = parsed;
131
+ } catch {
132
+ toast.add({
133
+ title: "Manifest invalid",
134
+ description: "plugin.manifest.json could not be parsed \u2014 falling back to unknown manifest.",
135
+ color: "warning"
136
+ });
137
+ }
138
+ }
139
+ const sha256 = await sha256Hex(jsBytes);
140
+ if (entries.value.some((e) => e.sha256 === sha256)) {
141
+ toast.add({ title: "Already uploaded", description: "This bundle is already installed.", color: "info" });
142
+ return;
143
+ }
144
+ await _putUploadedPluginBlob(sha256, jsBytes, "text/javascript");
145
+ const url = `idb-plugin:${sha256}`;
146
+ dialogMode.value = "upload";
147
+ dialogManifest.value = manifest;
148
+ dialogSource.value = file.name;
149
+ pendingStorageUrl.value = url;
150
+ dialogPrevAcked.value = void 0;
151
+ dialogOpen.value = true;
152
+ pendingUploadSha.value = sha256;
153
+ pendingUploadName.value = manifest.name || file.name;
154
+ } catch (e) {
155
+ toast.add({ title: ".zip upload failed", description: e.message, color: "error" });
156
+ }
157
+ }
158
+ const pendingUploadSha = ref("");
159
+ const pendingUploadName = ref("");
160
+ async function sha256Hex(bytes) {
161
+ const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
162
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
163
+ }
164
+ function reloadNow() {
165
+ window.location.reload();
166
+ }
167
+ function onConfirm(ack) {
168
+ installEntry(pendingStorageUrl.value);
169
+ const meta = {
170
+ origin: dialogMode.value,
171
+ trustAcknowledgedAt: ack.trustAcknowledgedAt,
172
+ acknowledgedVersion: ack.acknowledgedVersion,
173
+ acknowledgedCapabilities: ack.acknowledgedCapabilities,
174
+ version: dialogManifest.value?.version,
175
+ label: dialogManifest.value?.name,
176
+ description: dialogManifest.value?.description,
177
+ id: dialogManifest.value?.id
178
+ };
179
+ if (dialogMode.value === "upload") {
180
+ meta.sha256 = pendingUploadSha.value;
181
+ meta.label = pendingUploadName.value;
182
+ }
183
+ updateMeta(pendingStorageUrl.value, meta);
184
+ urlInput.value = "";
185
+ pendingReload.value = true;
186
+ toast.add({
187
+ title: `${dialogManifest.value?.name || dialogManifest.value?.id || "Plugin"} queued`,
188
+ description: "Reload to activate.",
189
+ color: "success"
190
+ });
191
+ }
192
+ function onCancel() {
193
+ }
194
+ const builtinList = computed(() => props.builtins ?? []);
195
+ const externalEntries = computed(() => entries.value);
196
+ function originBadge(origin) {
197
+ if (origin === "registry") return { label: "Registry", color: "primary" };
198
+ if (origin === "upload") return { label: "Upload", color: "warning" };
199
+ if (origin === "space") return { label: "Space", color: "info" };
200
+ return { label: "URL", color: "warning" };
201
+ }
202
+ function onToggleBuiltin(name, enabled) {
203
+ setBuiltinEnabled(name, enabled);
204
+ pendingReload.value = true;
205
+ }
206
+ function onToggleEntry(url, enabled) {
207
+ setEnabled(url, enabled);
208
+ pendingReload.value = true;
209
+ }
210
+ function onUninstall(url) {
211
+ uninstallEntry(url);
212
+ pendingReload.value = true;
213
+ }
214
+ </script>
215
+
216
+ <template>
217
+ <div class="flex flex-col gap-6">
218
+ <UAlert
219
+ v-if="pendingReload"
220
+ title="Reload to apply"
221
+ description="Plugin changes take effect after the next page reload."
222
+ icon="i-lucide-refresh-cw"
223
+ color="warning"
224
+ variant="subtle"
225
+ :actions="[{ label: 'Reload now', onClick: reloadNow }]"
226
+ />
227
+
228
+ <!-- Built-ins -->
229
+ <section v-if="builtinList.length" class="flex flex-col gap-2">
230
+ <div class="px-1">
231
+ <h4 class="text-sm font-semibold text-(--ui-text-highlighted)">
232
+ Built-in
233
+ </h4>
234
+ <p class="text-xs text-(--ui-text-muted) mt-0.5">
235
+ First-party plugins bundled with this app.
236
+ </p>
237
+ </div>
238
+ <div class="flex flex-col rounded-lg border border-(--ui-border) overflow-hidden">
239
+ <div
240
+ v-for="plugin in builtinList"
241
+ :key="plugin.name"
242
+ class="flex items-center gap-3 px-3.5 py-2.5 not-last:border-b border-(--ui-border)"
243
+ >
244
+ <UIcon
245
+ :name="plugin.icon ?? 'i-lucide-plug'"
246
+ class="size-4 text-(--ui-text-muted) shrink-0"
247
+ />
248
+ <div class="flex-1 min-w-0">
249
+ <p class="text-sm font-medium text-(--ui-text)">
250
+ {{ plugin.label ?? plugin.name }}
251
+ </p>
252
+ <p
253
+ v-if="plugin.version || plugin.description"
254
+ class="text-xs text-(--ui-text-dimmed)"
255
+ >
256
+ <template v-if="plugin.version">v{{ plugin.version }}</template>
257
+ <template v-if="plugin.version && plugin.description"> · </template>
258
+ <template v-if="plugin.description">{{ plugin.description }}</template>
259
+ </p>
260
+ </div>
261
+ <USwitch
262
+ :model-value="isBuiltinEnabled(plugin.name)"
263
+ @update:model-value="(v) => onToggleBuiltin(plugin.name, v)"
264
+ />
265
+ </div>
266
+ </div>
267
+ </section>
268
+
269
+ <!-- External entries -->
270
+ <section class="flex flex-col gap-2">
271
+ <div class="px-1 flex items-baseline justify-between">
272
+ <div>
273
+ <h4 class="text-sm font-semibold text-(--ui-text-highlighted)">
274
+ Installed
275
+ </h4>
276
+ <p class="text-xs text-(--ui-text-muted) mt-0.5">
277
+ Third-party plugins from the registry, a URL, or an upload.
278
+ </p>
279
+ </div>
280
+ <span
281
+ v-if="externalEntries.length"
282
+ class="text-xs text-(--ui-text-dimmed)"
283
+ >
284
+ {{ externalEntries.length }}
285
+ </span>
286
+ </div>
287
+ <div
288
+ v-if="externalEntries.length"
289
+ class="flex flex-col rounded-lg border border-(--ui-border) overflow-hidden"
290
+ >
291
+ <div
292
+ v-for="entry in externalEntries"
293
+ :key="entry.url"
294
+ class="flex items-center gap-3 px-3.5 py-2.5 not-last:border-b border-(--ui-border)"
295
+ >
296
+ <div class="flex-1 min-w-0">
297
+ <div class="flex items-center gap-2">
298
+ <p class="text-sm font-medium text-(--ui-text) truncate">
299
+ {{ entry.label ?? entry.name }}
300
+ </p>
301
+ <UBadge
302
+ :label="originBadge(entry.origin).label"
303
+ :color="originBadge(entry.origin).color"
304
+ variant="subtle"
305
+ size="xs"
306
+ />
307
+ <UBadge
308
+ v-if="entry.version"
309
+ :label="`v${entry.version}`"
310
+ color="neutral"
311
+ variant="subtle"
312
+ size="xs"
313
+ />
314
+ </div>
315
+ <p class="text-xs text-(--ui-text-dimmed) truncate font-mono mt-0.5">
316
+ {{ entry.url }}
317
+ </p>
318
+ <p
319
+ v-if="entry.error"
320
+ class="text-xs text-(--ui-error) mt-1 flex items-start gap-1"
321
+ >
322
+ <UIcon name="i-lucide-alert-circle" class="size-3 shrink-0 mt-0.5" />
323
+ <span>{{ entry.error }}</span>
324
+ </p>
325
+ </div>
326
+ <USwitch
327
+ :model-value="entry.enabled"
328
+ @update:model-value="(v) => onToggleEntry(entry.url, v)"
329
+ />
330
+ <UButton
331
+ icon="i-lucide-trash-2"
332
+ size="xs"
333
+ variant="ghost"
334
+ color="neutral"
335
+ square
336
+ aria-label="Uninstall"
337
+ @click="onUninstall(entry.url)"
338
+ />
339
+ </div>
340
+ </div>
341
+ <div
342
+ v-else
343
+ class="flex items-center gap-2 rounded-lg border border-dashed border-(--ui-border) px-4 py-6 text-sm text-(--ui-text-muted) justify-center"
344
+ >
345
+ <UIcon name="i-lucide-package-open" class="size-4" />
346
+ Nothing installed yet.
347
+ </div>
348
+ </section>
349
+
350
+ <!-- Add by URL + Upload -->
351
+ <section class="flex flex-col gap-2">
352
+ <div class="px-1">
353
+ <h4 class="text-sm font-semibold text-(--ui-text-highlighted)">
354
+ Install a plugin
355
+ </h4>
356
+ <p class="text-xs text-(--ui-text-muted) mt-0.5">
357
+ From a URL, an npm package, a GitHub repo, or a local file. All untrusted sources require an acknowledgement before activation.
358
+ </p>
359
+ </div>
360
+ <div class="flex gap-2">
361
+ <div class="flex-1">
362
+ <UInput
363
+ v-model="urlInput"
364
+ placeholder="https://… or npm:my-plugin or github:user/repo"
365
+ icon="i-lucide-link-2"
366
+ size="md"
367
+ class="w-full"
368
+ @keyup.enter="startUrlInstall"
369
+ />
370
+ <p
371
+ v-if="urlError"
372
+ class="text-xs text-(--ui-error) mt-1 px-1"
373
+ >
374
+ {{ urlError }}
375
+ </p>
376
+ </div>
377
+ <UButton
378
+ label="Add by URL"
379
+ icon="i-lucide-link-2"
380
+ color="primary"
381
+ size="md"
382
+ @click="startUrlInstall"
383
+ />
384
+ <UButton
385
+ label="Upload"
386
+ icon="i-lucide-upload"
387
+ color="neutral"
388
+ variant="outline"
389
+ size="md"
390
+ @click="triggerUpload"
391
+ />
392
+ <input
393
+ ref="fileInput"
394
+ type="file"
395
+ accept=".js,.mjs,.zip"
396
+ class="hidden"
397
+ @change="onFileChosen"
398
+ >
399
+ </div>
400
+ </section>
401
+
402
+ <APluginInstallDialog
403
+ v-if="dialogManifest"
404
+ v-model:open="dialogOpen"
405
+ :mode="dialogMode"
406
+ :manifest="dialogManifest"
407
+ :source="dialogSource"
408
+ :previously-acknowledged="dialogPrevAcked"
409
+ @confirm="onConfirm"
410
+ @cancel="onCancel"
411
+ />
412
+ </div>
413
+ </template>
@@ -0,0 +1,7 @@
1
+ import type { BuiltinPluginEntry } from './ASettingsPluginsPanel.vue.js';
2
+ type __VLS_Props = {
3
+ builtins?: readonly BuiltinPluginEntry[];
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,24 @@
1
+ import type { PluginManifest } from '@abraca/plugin';
2
+ export interface PendingSpacePluginEntry {
3
+ /** Plugin id from the manifest. */
4
+ id: string;
5
+ /** Resolved bundle URL. */
6
+ url: string;
7
+ /** Version, when the space-plugins map records one. */
8
+ version?: string;
9
+ /** Resolved manifest, when the URL exposed a sibling JSON. */
10
+ manifest?: PluginManifest;
11
+ }
12
+ type __VLS_Props = {
13
+ /**
14
+ * Space-declared plugins that did NOT auto-load (not in registry).
15
+ * Pre-filtered by the host's space-plugins watcher. When omitted, the
16
+ * tab shows the empty-state explainer.
17
+ */
18
+ declared?: readonly PendingSpacePluginEntry[];
19
+ /** Active space id. Required for decline persistence to be per-space. */
20
+ spaceId?: string;
21
+ };
22
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const _default: typeof __VLS_export;
24
+ export default _default;
@@ -0,0 +1,248 @@
1
+ <script setup>
2
+ import { computed, ref } from "vue";
3
+ import { useAbracadabra } from "../../composables/useAbracadabra";
4
+ import { useInstalledPlugins } from "../../composables/useInstalledPlugins";
5
+ import { useDeclinedSpacePlugins } from "../../composables/useDeclinedSpacePlugins";
6
+ import { usePluginCatalog } from "../../composables/usePluginCatalog";
7
+ import { useLoadTimePending } from "../../composables/useLoadTimePending";
8
+ const props = defineProps({
9
+ declared: { type: Array, required: false },
10
+ spaceId: { type: String, required: false }
11
+ });
12
+ const declared = computed(() => props.declared ?? []);
13
+ const abra = useAbracadabra();
14
+ const fallbackSpaceId = computed(() => {
15
+ const meta = abra.currentSpaceMeta?.value;
16
+ return meta?.space_id;
17
+ });
18
+ const effectiveSpaceId = computed(() => props.spaceId ?? fallbackSpaceId.value ?? "");
19
+ const catalog = usePluginCatalog();
20
+ const declinedMemory = useDeclinedSpacePlugins();
21
+ const loadTime = useLoadTimePending();
22
+ const { install: installEntry, updateMeta, entries } = useInstalledPlugins();
23
+ const visible = computed(() => {
24
+ const sid = effectiveSpaceId.value;
25
+ if (!sid) return declared.value;
26
+ return declared.value.filter((e) => !declinedMemory.isDeclined(sid, e.id));
27
+ });
28
+ const loadTimeVisible = computed(() => loadTime.entries.value);
29
+ const dialogOpen = ref(false);
30
+ const dialogManifest = ref(null);
31
+ const dialogSource = ref("");
32
+ const dialogPrevAcked = ref(void 0);
33
+ const pendingEntry = ref(null);
34
+ const pendingKind = ref("space");
35
+ const pendingLoadTimeUrl = ref("");
36
+ async function openDialog(entry) {
37
+ pendingEntry.value = entry;
38
+ pendingKind.value = "space";
39
+ const manifest = entry.manifest ?? await catalog.resolveExternalManifest(entry.url) ?? catalog.synthesizeUnknownManifest(entry.id, entry.version);
40
+ const prior = entries.value.find((e) => e.id === entry.id);
41
+ dialogPrevAcked.value = prior?.acknowledgedCapabilities;
42
+ dialogManifest.value = manifest;
43
+ dialogSource.value = entry.url;
44
+ dialogOpen.value = true;
45
+ }
46
+ async function openLoadTimeDialog(url) {
47
+ const lt = loadTimeVisible.value.find((e) => e.url === url);
48
+ if (!lt) return;
49
+ pendingKind.value = "load-time";
50
+ pendingLoadTimeUrl.value = url;
51
+ pendingEntry.value = { id: lt.id, url: lt.url, version: lt.observedVersion };
52
+ const manifest = await catalog.resolveExternalManifest(lt.url) ?? catalog.synthesizeUnknownManifest(lt.id, lt.observedVersion);
53
+ const installed = entries.value.find((e) => e.url === url);
54
+ dialogPrevAcked.value = installed?.acknowledgedCapabilities;
55
+ dialogManifest.value = manifest;
56
+ dialogSource.value = lt.url;
57
+ dialogOpen.value = true;
58
+ }
59
+ function onConfirm(ack) {
60
+ const entry = pendingEntry.value;
61
+ if (!entry) return;
62
+ if (pendingKind.value === "load-time") {
63
+ updateMeta(pendingLoadTimeUrl.value, {
64
+ version: dialogManifest.value?.version,
65
+ label: dialogManifest.value?.name,
66
+ description: dialogManifest.value?.description,
67
+ trustAcknowledgedAt: ack.trustAcknowledgedAt,
68
+ acknowledgedVersion: ack.acknowledgedVersion,
69
+ acknowledgedCapabilities: ack.acknowledgedCapabilities
70
+ });
71
+ loadTime.clear(pendingLoadTimeUrl.value);
72
+ return;
73
+ }
74
+ installEntry(entry.url);
75
+ updateMeta(entry.url, {
76
+ origin: "space",
77
+ id: entry.id,
78
+ version: dialogManifest.value?.version ?? entry.version,
79
+ label: dialogManifest.value?.name,
80
+ description: dialogManifest.value?.description,
81
+ trustAcknowledgedAt: ack.trustAcknowledgedAt,
82
+ acknowledgedVersion: ack.acknowledgedVersion,
83
+ acknowledgedCapabilities: ack.acknowledgedCapabilities
84
+ });
85
+ }
86
+ function onDecline() {
87
+ if (pendingKind.value === "load-time") {
88
+ loadTime.clear(pendingLoadTimeUrl.value);
89
+ return;
90
+ }
91
+ const entry = pendingEntry.value;
92
+ const sid = effectiveSpaceId.value;
93
+ if (!entry || !sid) return;
94
+ declinedMemory.decline(sid, entry.id);
95
+ }
96
+ </script>
97
+
98
+ <template>
99
+ <div class="flex flex-col gap-4">
100
+ <UAlert
101
+ icon="i-lucide-shield-question"
102
+ color="info"
103
+ variant="subtle"
104
+ title="Why is this here?"
105
+ description="Entries appear here when (a) the active space asks for a plugin not in the official registry, or (b) an already-installed plugin's manifest changed since you last acknowledged it. Review each one before activating."
106
+ />
107
+
108
+ <!-- Load-time pending: installed plugins whose manifest drifted -->
109
+ <section v-if="loadTimeVisible.length" class="flex flex-col gap-2">
110
+ <div class="px-1">
111
+ <h4 class="text-sm font-semibold text-(--ui-text-highlighted)">
112
+ Needs re-review
113
+ </h4>
114
+ <p class="text-xs text-(--ui-text-muted) mt-0.5">
115
+ These plugins are installed but their manifest changed since your last approval. They were not loaded this session.
116
+ </p>
117
+ </div>
118
+ <div class="flex flex-col rounded-lg border border-(--ui-border) overflow-hidden">
119
+ <div
120
+ v-for="lt in loadTimeVisible"
121
+ :key="lt.url"
122
+ class="flex items-start gap-3 px-3.5 py-3 not-last:border-b border-(--ui-border)"
123
+ >
124
+ <UIcon
125
+ name="i-lucide-shield-alert"
126
+ class="size-4 text-(--ui-warning) shrink-0 mt-0.5"
127
+ />
128
+ <div class="flex-1 min-w-0">
129
+ <div class="flex items-center gap-2">
130
+ <p class="text-sm font-medium text-(--ui-text) truncate">
131
+ {{ lt.id }}
132
+ </p>
133
+ <UBadge
134
+ :label="`v${lt.observedVersion}`"
135
+ color="warning"
136
+ variant="subtle"
137
+ size="xs"
138
+ />
139
+ <UBadge
140
+ :label="lt.reason === 'capabilities-grew' ? 'New permissions' : lt.reason === 'version-changed' ? 'New version' : 'Never approved'"
141
+ color="warning"
142
+ variant="soft"
143
+ size="xs"
144
+ />
145
+ </div>
146
+ <p class="text-xs text-(--ui-text-dimmed) font-mono truncate mt-0.5">
147
+ {{ lt.url }}
148
+ </p>
149
+ </div>
150
+ <UButton
151
+ label="Review"
152
+ icon="i-lucide-shield-check"
153
+ size="xs"
154
+ variant="outline"
155
+ color="warning"
156
+ @click="openLoadTimeDialog(lt.url)"
157
+ />
158
+ </div>
159
+ </div>
160
+ </section>
161
+
162
+ <div
163
+ v-if="!declared.length && !loadTimeVisible.length"
164
+ class="flex flex-col items-center justify-center gap-2 py-10 text-center"
165
+ >
166
+ <UIcon name="i-lucide-hourglass" class="size-8 text-(--ui-text-dimmed)" />
167
+ <p class="text-sm text-(--ui-text-muted)">
168
+ No pending plugins for this space.
169
+ </p>
170
+ <p class="text-xs text-(--ui-text-dimmed) max-w-sm">
171
+ When a space declares a plugin that isn't in the registry, it appears here for you to review.
172
+ </p>
173
+ </div>
174
+
175
+ <div
176
+ v-else-if="declared.length && !visible.length"
177
+ class="flex flex-col items-center justify-center gap-2 py-10 text-center"
178
+ >
179
+ <UIcon name="i-lucide-check-circle-2" class="size-8 text-(--ui-text-dimmed)" />
180
+ <p class="text-sm text-(--ui-text-muted)">
181
+ All space-declared entries handled.
182
+ </p>
183
+ <p class="text-xs text-(--ui-text-dimmed)">
184
+ You declined every space-declared plugin not in the registry.
185
+ </p>
186
+ </div>
187
+
188
+ <div
189
+ v-else-if="visible.length"
190
+ class="flex flex-col rounded-lg border border-(--ui-border) overflow-hidden"
191
+ >
192
+ <div
193
+ v-for="entry in visible"
194
+ :key="entry.id"
195
+ class="flex items-start gap-3 px-3.5 py-3 not-last:border-b border-(--ui-border)"
196
+ >
197
+ <UIcon
198
+ name="i-lucide-globe"
199
+ class="size-4 text-(--ui-info) shrink-0 mt-0.5"
200
+ />
201
+ <div class="flex-1 min-w-0">
202
+ <div class="flex items-center gap-2">
203
+ <p class="text-sm font-medium text-(--ui-text) truncate">
204
+ {{ entry.manifest?.name ?? entry.id }}
205
+ </p>
206
+ <UBadge
207
+ v-if="entry.version"
208
+ :label="`v${entry.version}`"
209
+ color="neutral"
210
+ variant="subtle"
211
+ size="xs"
212
+ />
213
+ <UBadge
214
+ label="Not in registry"
215
+ color="warning"
216
+ variant="subtle"
217
+ size="xs"
218
+ />
219
+ </div>
220
+ <p class="text-xs text-(--ui-text-dimmed) font-mono truncate mt-0.5">
221
+ {{ entry.url }}
222
+ </p>
223
+ </div>
224
+ <UButton
225
+ label="Review"
226
+ icon="i-lucide-shield-check"
227
+ size="xs"
228
+ variant="outline"
229
+ color="warning"
230
+ @click="openDialog(entry)"
231
+ />
232
+ </div>
233
+ </div>
234
+
235
+ <APluginInstallDialog
236
+ v-if="dialogManifest"
237
+ v-model:open="dialogOpen"
238
+ mode="space"
239
+ :manifest="dialogManifest"
240
+ :source="dialogSource"
241
+ :previously-acknowledged="dialogPrevAcked"
242
+ :space-id="effectiveSpaceId"
243
+ :declinable="true"
244
+ @confirm="onConfirm"
245
+ @decline="onDecline"
246
+ />
247
+ </div>
248
+ </template>