@omercnet/paseo-omp 0.2.1 → 0.3.0-next.91.1

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 (63) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +25 -13
  3. package/SUPPORT.md +6 -2
  4. package/TESTING.md +21 -18
  5. package/client/composer-pill-settings.tsx +157 -0
  6. package/client/external-url.ts +15 -0
  7. package/client/mcp-authorization.tsx +169 -0
  8. package/client/mcp-popover.tsx +155 -0
  9. package/client/memory-panel.tsx +8 -3
  10. package/client/memory-popover.tsx +8 -4
  11. package/client/omp-config-surface.tsx +189 -29
  12. package/client/omp-plugin-manager.tsx +302 -131
  13. package/client/omp-store-picker.tsx +89 -0
  14. package/client/omp-store-state.ts +45 -0
  15. package/client/paseo-types.ts +9 -0
  16. package/client/provider-diagnostics-state.ts +18 -7
  17. package/client/quota-popover.tsx +8 -3
  18. package/client/quota-state.ts +16 -7
  19. package/client/sessions-popover.tsx +8 -3
  20. package/docs/alpha-release-checklist.md +6 -8
  21. package/docs/configuration.md +8 -4
  22. package/docs/core-provider-issue-audit.md +3 -2
  23. package/docs/images/mcp-authorization-compact.png +0 -0
  24. package/docs/images/mcp-controls-wide.png +0 -0
  25. package/docs/images/plugin-manager.png +0 -0
  26. package/docs/images/workspace-settings.png +0 -0
  27. package/docs/installation.md +35 -19
  28. package/index.client.tsx +339 -123
  29. package/index.server.ts +44 -14
  30. package/package.json +7 -8
  31. package/paseo-plugin.json +2 -2
  32. package/scripts/prepare-dependencies.mjs +24 -0
  33. package/server/mcp-browser.ts +95 -0
  34. package/server/memory.ts +2 -2
  35. package/server/omp-config.ts +16 -7
  36. package/server/omp-plugins.ts +70 -21
  37. package/server/omp-settings.ts +232 -24
  38. package/server/paths.ts +128 -11
  39. package/server/provider/catalog.ts +3 -4
  40. package/server/provider/connection.ts +213 -9
  41. package/server/provider/host-tools.ts +71 -0
  42. package/server/provider/omp-rpc.ts +82 -15
  43. package/server/provider/profile-providers.ts +249 -0
  44. package/server/provider/registration.ts +11 -0
  45. package/server/provider/session-descriptors.ts +306 -1
  46. package/server/provider/session.ts +704 -249
  47. package/server/provider/subsessions.ts +4 -1
  48. package/server/provider/timeline-projector.ts +70 -33
  49. package/server/provider-diagnostics.ts +122 -36
  50. package/server/quota.ts +3 -2
  51. package/server/sessions.ts +2 -2
  52. package/shared/composer-pill-settings.ts +28 -0
  53. package/shared/external-url.ts +21 -0
  54. package/shared/hub.ts +3 -3
  55. package/shared/mcp.ts +47 -0
  56. package/shared/memory.ts +2 -1
  57. package/shared/omp-config.ts +5 -1
  58. package/shared/omp-plugins.ts +74 -33
  59. package/shared/omp-settings.ts +8 -1
  60. package/shared/omp-store.ts +58 -0
  61. package/shared/provider-diagnostics.ts +12 -3
  62. package/shared/quota.ts +2 -1
  63. package/shared/sessions.ts +2 -1
@@ -1,7 +1,7 @@
1
1
  import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
2
2
  import { TextInput } from "@getpaseo/plugin/client/react-native";
3
3
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
- import { useMemo, useRef, useState } from "react";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
5
  import type { TextStyle, ViewStyle } from "react-native";
6
6
  import { Pressable, Switch, Text, View } from "react-native";
7
7
  import {
@@ -18,18 +18,28 @@ import {
18
18
  type OmpPluginMutation,
19
19
  } from "../shared/omp-plugins";
20
20
 
21
+ import type { OmpStore } from "../shared/omp-store";
22
+ import { ompStoreKey } from "./omp-store-state";
23
+
21
24
  const PLUGINS_QUERY_KEY = ["paseo-omp", "plugins"] as const;
22
25
 
26
+ type ConfirmationOrigin =
27
+ | { surface: "install" }
28
+ | { surface: "plugin"; key: string }
29
+ | { surface: "config"; key: string };
30
+
23
31
  type PendingConfirmation =
24
32
  | {
25
33
  kind: "plugin";
26
34
  input: OmpPluginMutation;
35
+ origin: ConfirmationOrigin;
27
36
  title: string;
28
37
  warning: string;
29
38
  }
30
39
  | {
31
40
  kind: "config";
32
41
  input: OmpPluginConfigMutation;
42
+ origin: ConfirmationOrigin;
33
43
  title: string;
34
44
  warning: string;
35
45
  };
@@ -229,13 +239,55 @@ function MetadataRow({
229
239
  );
230
240
  }
231
241
 
232
- function confirmationFor(input: OmpPluginMutation): PendingConfirmation {
242
+ function ConfirmationPanel({
243
+ confirmation,
244
+ busy,
245
+ styles,
246
+ onCancel,
247
+ onConfirm,
248
+ }: {
249
+ confirmation: PendingConfirmation;
250
+ busy: boolean;
251
+ styles: PluginManagerStyles;
252
+ onCancel(): void;
253
+ onConfirm(): void;
254
+ }) {
255
+ const action = confirmation.input.action;
256
+ const confirmLabel = action === "set" ? "Confirm change" : `Confirm ${action}`;
257
+ return (
258
+ <View accessibilityRole="alert" style={styles.confirmation}>
259
+ <Text style={styles.cardTitle}>{confirmation.title}</Text>
260
+ <Text style={styles.warning}>{confirmation.warning}</Text>
261
+ <View style={styles.actions}>
262
+ <ActionButton label="Cancel" disabled={busy} styles={styles} onPress={onCancel} />
263
+ <ActionButton
264
+ label={busy ? "Applying…" : confirmLabel}
265
+ disabled={busy}
266
+ primary
267
+ danger={action === "uninstall" || action === "delete"}
268
+ styles={styles}
269
+ onPress={onConfirm}
270
+ />
271
+ </View>
272
+ </View>
273
+ );
274
+ }
275
+
276
+ function pluginCardIdentity(plugin: OmpInstalledPlugin): string {
277
+ return `${plugin.source}:${plugin.scope ?? "global"}:${plugin.path ?? plugin.id}:${plugin.version ?? "unknown"}`;
278
+ }
279
+
280
+ function confirmationFor(
281
+ input: OmpPluginMutation,
282
+ origin: ConfirmationOrigin,
283
+ ): PendingConfirmation {
233
284
  const target = input.action === "install" ? input.source : input.plugin;
234
285
  switch (input.action) {
235
286
  case "install":
236
287
  return {
237
288
  kind: "plugin",
238
289
  input,
290
+ origin,
239
291
  title: `Install ${target}?`,
240
292
  warning:
241
293
  "OMP plugins are trusted code. Installing can fetch code and change persistent state; runtime extensions load in a future OMP session.",
@@ -244,6 +296,7 @@ function confirmationFor(input: OmpPluginMutation): PendingConfirmation {
244
296
  return {
245
297
  kind: "plugin",
246
298
  input,
299
+ origin,
247
300
  title: `Enable ${target}?`,
248
301
  warning:
249
302
  "Enabling changes persistent state and permits this plugin's trusted code to load in future OMP sessions.",
@@ -252,6 +305,7 @@ function confirmationFor(input: OmpPluginMutation): PendingConfirmation {
252
305
  return {
253
306
  kind: "plugin",
254
307
  input,
308
+ origin,
255
309
  title: `Disable ${target}?`,
256
310
  warning:
257
311
  "Disabling changes persistent state. OMP sessions that are already running are not unloaded.",
@@ -260,6 +314,7 @@ function confirmationFor(input: OmpPluginMutation): PendingConfirmation {
260
314
  return {
261
315
  kind: "plugin",
262
316
  input,
317
+ origin,
263
318
  title: `Uninstall ${target}?`,
264
319
  warning:
265
320
  "Uninstalling removes the selected plugin registration and cached installation. Running OMP sessions are unchanged.",
@@ -268,6 +323,7 @@ function confirmationFor(input: OmpPluginMutation): PendingConfirmation {
268
323
  return {
269
324
  kind: "plugin",
270
325
  input,
326
+ origin,
271
327
  title: `Upgrade ${target}?`,
272
328
  warning:
273
329
  "Upgrading fetches and replaces trusted plugin code. The updated runtime loads in a future OMP session.",
@@ -278,11 +334,13 @@ function confirmationFor(input: OmpPluginMutation): PendingConfirmation {
278
334
  function configConfirmationFor(
279
335
  input: OmpPluginConfigMutation,
280
336
  setting: OmpPluginConfigSetting,
337
+ origin: ConfirmationOrigin,
281
338
  ): PendingConfirmation {
282
339
  if (input.action === "delete") {
283
340
  return {
284
341
  kind: "config",
285
342
  input,
343
+ origin,
286
344
  title: `Delete ${setting.key}?`,
287
345
  warning:
288
346
  "This changes persistent OMP plugin configuration. Future sessions will use the setting's default or environment fallback.",
@@ -291,6 +349,7 @@ function configConfirmationFor(
291
349
  return {
292
350
  kind: "config",
293
351
  input,
352
+ origin,
294
353
  title: `Set ${setting.key}?`,
295
354
  warning: setting.secret
296
355
  ? "This writes a secret value to OMP's persistent plugin configuration. The value will remain write-only in Paseo."
@@ -302,18 +361,27 @@ function ConfigSettingEditor({
302
361
  plugin,
303
362
  setting,
304
363
  busy,
364
+ confirmation,
365
+ confirmationOpen,
305
366
  theme,
306
367
  styles,
368
+ onCancel,
369
+ onApply,
307
370
  onConfirm,
308
371
  }: {
309
372
  plugin: string;
310
373
  setting: OmpPluginConfigSetting;
311
374
  busy: boolean;
375
+ confirmation: PendingConfirmation | null;
376
+ confirmationOpen: boolean;
312
377
  theme: PluginSurfaceProps["theme"];
313
378
  styles: PluginManagerStyles;
379
+ onCancel(): void;
380
+ onApply(): void;
314
381
  onConfirm(input: OmpPluginConfigMutation, setting: OmpPluginConfigSetting): void;
315
382
  }) {
316
383
  const [draft, setDraft] = useState<string | boolean>(setting.type === "boolean" ? false : "");
384
+ const blocked = busy || confirmationOpen;
317
385
  let value: string | number | boolean | undefined;
318
386
  let validationMessage: string | null = null;
319
387
  if (!setting.secret) {
@@ -373,7 +441,7 @@ function ConfigSettingEditor({
373
441
  <View style={styles.metadataRow}>
374
442
  <Switch
375
443
  accessibilityLabel={`New value for ${setting.key}`}
376
- disabled={busy}
444
+ disabled={blocked}
377
445
  value={draft === true}
378
446
  onValueChange={setDraft}
379
447
  />
@@ -387,8 +455,8 @@ function ConfigSettingEditor({
387
455
  <Pressable
388
456
  key={option}
389
457
  accessibilityRole="radio"
390
- accessibilityState={{ checked: selected, disabled: busy }}
391
- disabled={busy}
458
+ accessibilityState={{ checked: selected, disabled: blocked }}
459
+ disabled={blocked}
392
460
  onPress={() => setDraft(option)}
393
461
  style={[styles.scopeButton, selected ? styles.scopeButtonActive : null]}
394
462
  >
@@ -402,7 +470,7 @@ function ConfigSettingEditor({
402
470
  ) : (
403
471
  <TextInput
404
472
  accessibilityLabel={`New value for ${setting.key}`}
405
- editable={!busy}
473
+ editable={!blocked}
406
474
  value={typeof draft === "string" ? draft : ""}
407
475
  onChangeText={setDraft}
408
476
  placeholder="Enter a new value"
@@ -416,28 +484,38 @@ function ConfigSettingEditor({
416
484
  {validationMessage && (setting.type !== "boolean" || value === undefined) ? (
417
485
  <Text style={styles.error}>{validationMessage}</Text>
418
486
  ) : null}
419
- <View style={styles.actions}>
420
- {!setting.secret ? (
421
- <ActionButton
422
- label="Review change"
423
- disabled={busy || !candidate?.success}
424
- primary
425
- styles={styles}
426
- onPress={() => {
427
- if (candidate?.success) onConfirm(candidate.data, setting);
428
- }}
429
- />
430
- ) : null}
431
- {setting.configured ? (
432
- <ActionButton
433
- label="Delete setting"
434
- disabled={busy}
435
- danger
436
- styles={styles}
437
- onPress={() => onConfirm({ action: "delete", plugin, key: setting.key }, setting)}
438
- />
439
- ) : null}
440
- </View>
487
+ {confirmation ? (
488
+ <ConfirmationPanel
489
+ confirmation={confirmation}
490
+ busy={busy}
491
+ styles={styles}
492
+ onCancel={onCancel}
493
+ onConfirm={onApply}
494
+ />
495
+ ) : (
496
+ <View style={styles.actions}>
497
+ {!setting.secret ? (
498
+ <ActionButton
499
+ label="Review change"
500
+ disabled={blocked || !candidate?.success}
501
+ primary
502
+ styles={styles}
503
+ onPress={() => {
504
+ if (candidate?.success) onConfirm(candidate.data, setting);
505
+ }}
506
+ />
507
+ ) : null}
508
+ {setting.configured ? (
509
+ <ActionButton
510
+ label="Delete setting"
511
+ disabled={blocked}
512
+ danger
513
+ styles={styles}
514
+ onPress={() => onConfirm({ action: "delete", plugin, key: setting.key }, setting)}
515
+ />
516
+ ) : null}
517
+ </View>
518
+ )}
441
519
  </View>
442
520
  );
443
521
  }
@@ -445,19 +523,32 @@ function ConfigSettingEditor({
445
523
  function PluginCard({
446
524
  plugin,
447
525
  busy,
526
+ confirmation,
527
+ confirmationOpen,
528
+ workspaceScoped,
448
529
  inspecting,
449
530
  styles,
531
+ onCancel,
532
+ onApply,
450
533
  onConfirm,
451
534
  onInspect,
452
535
  }: {
453
536
  plugin: OmpInstalledPlugin;
454
537
  busy: boolean;
538
+ confirmation: PendingConfirmation | null;
539
+ confirmationOpen: boolean;
540
+ workspaceScoped: boolean;
455
541
  inspecting: boolean;
456
542
  styles: PluginManagerStyles;
457
- onConfirm(input: OmpPluginMutation): void;
543
+ onCancel(): void;
544
+ onApply(): void;
545
+ onConfirm(input: OmpPluginMutation, origin: ConfirmationOrigin): void;
458
546
  onInspect(plugin: string): void;
459
547
  }) {
460
- const scope = plugin.scope === "user" ? "user" : undefined;
548
+ const scope = plugin.scope ?? undefined;
549
+ const identity = pluginCardIdentity(plugin);
550
+ const blocked = busy || confirmationOpen;
551
+ const projectUnavailable = plugin.scope === "project" && !workspaceScoped;
461
552
  return (
462
553
  <View style={styles.pluginCard}>
463
554
  <View style={styles.cardHeader}>
@@ -490,56 +581,85 @@ function PluginCard({
490
581
  ) : null}
491
582
  {plugin.scope === "project" ? (
492
583
  <Text style={styles.warning}>
493
- Project-scoped actions require OMP to run from that project and are read-only here.
584
+ {workspaceScoped
585
+ ? "This installation is scoped to the current workspace."
586
+ : "Open this project's workspace OMP panel to manage this installation."}
494
587
  </Text>
495
588
  ) : null}
496
589
  {plugin.ambiguous ? (
497
590
  <Text style={styles.warning}>
498
- Multiple installations share this package identity, so lifecycle actions are read-only.
591
+ Multiple installations share this lifecycle target, so path-specific actions remain
592
+ unavailable.
593
+ </Text>
594
+ ) : null}
595
+ {plugin.configAmbiguous ? (
596
+ <Text style={styles.warning}>
597
+ Multiple installations share this package identity, so settings are read-only.
499
598
  </Text>
500
599
  ) : null}
501
600
  </View>
502
- <View style={styles.actions}>
503
- <ActionButton
504
- label={plugin.enabled ? "Disable" : "Enable"}
505
- disabled={busy || plugin.scope === "project" || plugin.ambiguous}
601
+ {confirmation ? (
602
+ <ConfirmationPanel
603
+ confirmation={confirmation}
604
+ busy={busy}
506
605
  styles={styles}
507
- onPress={() =>
508
- onConfirm({
509
- action: plugin.enabled ? "disable" : "enable",
510
- plugin: plugin.id,
511
- ...(scope ? { scope } : {}),
512
- })
513
- }
606
+ onCancel={onCancel}
607
+ onConfirm={onApply}
514
608
  />
515
- {plugin.configurable && plugin.packageName ? (
609
+ ) : (
610
+ <View style={styles.actions}>
516
611
  <ActionButton
517
- label={inspecting ? "Loading settings…" : "Configure settings"}
518
- disabled={busy || inspecting || plugin.scope === "project" || plugin.ambiguous}
612
+ label={plugin.enabled ? "Disable" : "Enable"}
613
+ disabled={blocked || projectUnavailable || plugin.ambiguous}
519
614
  styles={styles}
520
- onPress={() => onInspect(plugin.packageName ?? plugin.id)}
615
+ onPress={() =>
616
+ onConfirm(
617
+ {
618
+ action: plugin.enabled ? "disable" : "enable",
619
+ plugin: plugin.id,
620
+ ...(scope ? { scope } : {}),
621
+ },
622
+ { surface: "plugin", key: identity },
623
+ )
624
+ }
521
625
  />
522
- ) : null}
523
- {plugin.source === "marketplace" ? (
626
+ {plugin.configurable && plugin.packageName ? (
627
+ <ActionButton
628
+ label={inspecting ? "Loading settings…" : "Configure settings"}
629
+ disabled={
630
+ blocked || inspecting || plugin.scope === "project" || plugin.configAmbiguous
631
+ }
632
+ styles={styles}
633
+ onPress={() => onInspect(plugin.packageName ?? plugin.id)}
634
+ />
635
+ ) : null}
636
+ {plugin.source === "marketplace" ? (
637
+ <ActionButton
638
+ label="Upgrade"
639
+ disabled={blocked || projectUnavailable || plugin.ambiguous}
640
+ styles={styles}
641
+ onPress={() =>
642
+ onConfirm(
643
+ { action: "upgrade", plugin: plugin.id, ...(scope ? { scope } : {}) },
644
+ { surface: "plugin", key: identity },
645
+ )
646
+ }
647
+ />
648
+ ) : null}
524
649
  <ActionButton
525
- label="Upgrade"
526
- disabled={busy || plugin.scope === "project" || plugin.ambiguous}
650
+ label="Uninstall"
651
+ disabled={blocked || projectUnavailable || plugin.ambiguous}
652
+ danger
527
653
  styles={styles}
528
654
  onPress={() =>
529
- onConfirm({ action: "upgrade", plugin: plugin.id, ...(scope ? { scope } : {}) })
655
+ onConfirm(
656
+ { action: "uninstall", plugin: plugin.id, ...(scope ? { scope } : {}) },
657
+ { surface: "plugin", key: identity },
658
+ )
530
659
  }
531
660
  />
532
- ) : null}
533
- <ActionButton
534
- label="Uninstall"
535
- disabled={busy || plugin.scope === "project" || plugin.ambiguous}
536
- danger
537
- styles={styles}
538
- onPress={() =>
539
- onConfirm({ action: "uninstall", plugin: plugin.id, ...(scope ? { scope } : {}) })
540
- }
541
- />
542
- </View>
661
+ </View>
662
+ )}
543
663
  </View>
544
664
  );
545
665
  }
@@ -547,10 +667,15 @@ function PluginCard({
547
667
  export function OmpPluginManagerSection({
548
668
  theme,
549
669
  compact,
670
+ cwd,
671
+ store,
550
672
  }: {
551
673
  theme: PluginSurfaceProps["theme"];
552
674
  compact: boolean;
675
+ cwd?: string;
676
+ store?: OmpStore;
553
677
  }) {
678
+ const context = { store, ...(cwd ? { cwd } : {}) };
554
679
  const loadPlugins = useRpc(listOmpPlugins);
555
680
  const inspectPluginConfig = useRpc(inspectOmpPluginConfig);
556
681
  const mutatePlugin = useRpc(mutateOmpPlugin);
@@ -564,14 +689,19 @@ export function OmpPluginManagerSection({
564
689
  const [configEditGenerations, setConfigEditGenerations] = useState<Record<string, number>>({});
565
690
  const inspectionGeneration = useRef(0);
566
691
  const plugins = useQuery({
567
- queryKey: PLUGINS_QUERY_KEY,
568
- queryFn: () => loadPlugins({}),
692
+ queryKey: [...PLUGINS_QUERY_KEY, ompStoreKey(store), cwd ?? "global"],
693
+ queryFn: () => loadPlugins(context),
569
694
  staleTime: Number.POSITIVE_INFINITY,
570
695
  });
571
696
  const mutation = useMutation({
572
- mutationFn: (input: OmpPluginMutation) => mutatePlugin(input),
697
+ mutationKey: ["paseo-omp", "plugins", ompStoreKey(store)],
698
+ mutationFn: (input: OmpPluginMutation) => mutatePlugin({ ...input, ...context }),
573
699
  onSuccess: (result) => {
574
- queryClient.setQueryData(PLUGINS_QUERY_KEY, result.state);
700
+ void queryClient.invalidateQueries({ queryKey: PLUGINS_QUERY_KEY });
701
+ queryClient.setQueryData(
702
+ [...PLUGINS_QUERY_KEY, ompStoreKey(store), cwd ?? "global"],
703
+ result.state,
704
+ );
575
705
  setNotice({ tone: result.ok ? "success" : "error", text: result.message });
576
706
  if (result.ok && confirmation?.kind === "plugin" && confirmation.input.action === "install") {
577
707
  setSource("");
@@ -585,7 +715,7 @@ export function OmpPluginManagerSection({
585
715
  });
586
716
  const configInspection = useMutation({
587
717
  mutationFn: ({ plugin }: { plugin: string; generation: number }) =>
588
- inspectPluginConfig({ plugin }),
718
+ inspectPluginConfig({ plugin, ...context }),
589
719
  onSuccess: (result, request) => {
590
720
  if (inspectionGeneration.current === request.generation) setInspected(result);
591
721
  },
@@ -594,7 +724,8 @@ export function OmpPluginManagerSection({
594
724
  },
595
725
  });
596
726
  const configMutation = useMutation({
597
- mutationFn: (input: OmpPluginConfigMutation) => mutatePluginConfig(input),
727
+ mutationKey: ["paseo-omp", "plugin-config", ompStoreKey(store)],
728
+ mutationFn: (input: OmpPluginConfigMutation) => mutatePluginConfig({ ...input, ...context }),
598
729
  onSuccess: (result, input) => {
599
730
  if (result.config.available) setInspected(result.config);
600
731
  if (result.ok) {
@@ -604,6 +735,7 @@ export function OmpPluginManagerSection({
604
735
  [identity]: (current[identity] ?? 0) + 1,
605
736
  }));
606
737
  }
738
+ void queryClient.invalidateQueries({ queryKey: PLUGINS_QUERY_KEY });
607
739
  setNotice({ tone: result.ok ? "success" : "error", text: result.message });
608
740
  setConfirmation(null);
609
741
  },
@@ -613,6 +745,25 @@ export function OmpPluginManagerSection({
613
745
  },
614
746
  });
615
747
  const busy = mutation.isPending || configMutation.isPending;
748
+ useEffect(() => {
749
+ if (!confirmation) return;
750
+ const { origin } = confirmation;
751
+ if (
752
+ origin.surface === "plugin" &&
753
+ plugins.data?.available &&
754
+ !plugins.data.plugins.some((plugin) => pluginCardIdentity(plugin) === origin.key)
755
+ ) {
756
+ setConfirmation(null);
757
+ return;
758
+ }
759
+ if (
760
+ origin.surface === "config" &&
761
+ inspected &&
762
+ !inspected.settings.some((setting) => `${inspected.plugin}:${setting.key}` === origin.key)
763
+ ) {
764
+ setConfirmation(null);
765
+ }
766
+ }, [confirmation, inspected, plugins.data]);
616
767
 
617
768
  const requestInstall = () => {
618
769
  const parsed = OmpPluginInstallSourceSchema.safeParse(source);
@@ -624,18 +775,33 @@ export function OmpPluginManagerSection({
624
775
  return;
625
776
  }
626
777
  setNotice(null);
627
- setConfirmation(confirmationFor({ action: "install", source: parsed.data, scope: "user" }));
778
+ setConfirmation(
779
+ confirmationFor(
780
+ {
781
+ action: "install",
782
+ source: parsed.data,
783
+ scope: "user",
784
+ ...context,
785
+ },
786
+ { surface: "install" },
787
+ ),
788
+ );
628
789
  };
629
- const requestMutation = (input: OmpPluginMutation) => {
790
+ const requestMutation = (input: OmpPluginMutation, origin: ConfirmationOrigin) => {
630
791
  setNotice(null);
631
- setConfirmation(confirmationFor(input));
792
+ setConfirmation(confirmationFor({ ...input, ...context }, origin));
632
793
  };
633
794
  const requestConfigMutation = (
634
795
  input: OmpPluginConfigMutation,
635
796
  setting: OmpPluginConfigSetting,
636
797
  ) => {
637
798
  setNotice(null);
638
- setConfirmation(configConfirmationFor(input, setting));
799
+ setConfirmation(
800
+ configConfirmationFor({ ...input, ...context }, setting, {
801
+ surface: "config",
802
+ key: `${input.plugin}:${input.key}`,
803
+ }),
804
+ );
639
805
  };
640
806
  const inspect = (plugin: string) => {
641
807
  inspectionGeneration.current += 1;
@@ -665,7 +831,7 @@ export function OmpPluginManagerSection({
665
831
  </View>
666
832
  <ActionButton
667
833
  label={plugins.isFetching ? "Refreshing…" : "Refresh"}
668
- disabled={plugins.isFetching || busy}
834
+ disabled={plugins.isFetching || busy || confirmation !== null}
669
835
  styles={styles}
670
836
  onPress={() => {
671
837
  void plugins.refetch();
@@ -681,7 +847,7 @@ export function OmpPluginManagerSection({
681
847
  </Text>
682
848
  <TextInput
683
849
  accessibilityLabel="OMP plugin source"
684
- editable={!busy}
850
+ editable={!busy && confirmation === null}
685
851
  value={source}
686
852
  onChangeText={setSource}
687
853
  placeholder="name@marketplace or package source"
@@ -690,43 +856,26 @@ export function OmpPluginManagerSection({
690
856
  autoCorrect={false}
691
857
  style={styles.input}
692
858
  />
693
- <View style={styles.actions}>
694
- <ActionButton
695
- label="Review install"
696
- disabled={busy || source.length === 0}
697
- primary
859
+ {confirmation?.origin.surface === "install" ? (
860
+ <ConfirmationPanel
861
+ confirmation={confirmation}
862
+ busy={busy}
698
863
  styles={styles}
699
- onPress={requestInstall}
864
+ onCancel={() => setConfirmation(null)}
865
+ onConfirm={applyConfirmation}
700
866
  />
701
- </View>
702
- </View>
703
-
704
- {confirmation ? (
705
- <View accessibilityRole="alert" style={styles.confirmation}>
706
- <Text style={styles.cardTitle}>{confirmation.title}</Text>
707
- <Text style={styles.warning}>{confirmation.warning}</Text>
867
+ ) : (
708
868
  <View style={styles.actions}>
709
869
  <ActionButton
710
- label="Cancel"
711
- disabled={busy}
712
- styles={styles}
713
- onPress={() => setConfirmation(null)}
714
- />
715
- <ActionButton
716
- label={busy ? "Applying…" : "Confirm"}
717
- disabled={busy}
870
+ label="Review install"
871
+ disabled={busy || confirmation !== null || source.length === 0}
718
872
  primary
719
- danger={
720
- confirmation.kind === "plugin"
721
- ? confirmation.input.action === "uninstall"
722
- : confirmation.input.action === "delete"
723
- }
724
873
  styles={styles}
725
- onPress={applyConfirmation}
874
+ onPress={requestInstall}
726
875
  />
727
876
  </View>
728
- </View>
729
- ) : null}
877
+ )}
878
+ </View>
730
879
 
731
880
  {notice ? (
732
881
  <Text
@@ -763,23 +912,34 @@ export function OmpPluginManagerSection({
763
912
 
764
913
  {plugins.data?.plugins.length ? (
765
914
  <View style={styles.pluginGrid}>
766
- {plugins.data.plugins.map((plugin) => (
767
- <PluginCard
768
- key={`${plugin.source}:${plugin.scope ?? "global"}:${plugin.path ?? plugin.id}:${plugin.version ?? "unknown"}`}
769
- plugin={plugin}
770
- busy={busy}
771
- inspecting={
772
- configInspection.isPending &&
773
- configInspection.variables?.plugin === plugin.packageName
774
- }
775
- styles={styles}
776
- onConfirm={requestMutation}
777
- onInspect={inspect}
778
- />
779
- ))}
915
+ {plugins.data.plugins.map((plugin) => {
916
+ const identity = pluginCardIdentity(plugin);
917
+ const localConfirmation =
918
+ confirmation?.origin.surface === "plugin" && confirmation.origin.key === identity
919
+ ? confirmation
920
+ : null;
921
+ return (
922
+ <PluginCard
923
+ key={identity}
924
+ plugin={plugin}
925
+ busy={busy}
926
+ confirmation={localConfirmation}
927
+ confirmationOpen={confirmation !== null}
928
+ workspaceScoped={cwd !== undefined}
929
+ inspecting={
930
+ configInspection.isPending &&
931
+ configInspection.variables?.plugin === plugin.packageName
932
+ }
933
+ styles={styles}
934
+ onCancel={() => setConfirmation(null)}
935
+ onApply={applyConfirmation}
936
+ onConfirm={requestMutation}
937
+ onInspect={inspect}
938
+ />
939
+ );
940
+ })}
780
941
  </View>
781
942
  ) : null}
782
-
783
943
  {configInspection.error || configMutation.error ? (
784
944
  <Text accessibilityRole="alert" style={styles.error}>
785
945
  Could not update OMP plugin settings.
@@ -791,7 +951,7 @@ export function OmpPluginManagerSection({
791
951
  <Text style={styles.cardTitle}>Settings · {inspected.plugin}</Text>
792
952
  <ActionButton
793
953
  label="Close"
794
- disabled={busy}
954
+ disabled={busy || confirmation !== null}
795
955
  styles={styles}
796
956
  onPress={() => {
797
957
  inspectionGeneration.current += 1;
@@ -814,17 +974,28 @@ export function OmpPluginManagerSection({
814
974
  <Text style={styles.muted}>This plugin declares no settings.</Text>
815
975
  ) : null}
816
976
  <View style={styles.settings}>
817
- {inspected.settings.map((setting) => (
818
- <ConfigSettingEditor
819
- key={`${inspected.plugin}:${setting.key}:${configEditGenerations[`${inspected.plugin}:${setting.key}`] ?? 0}`}
820
- plugin={inspected.plugin}
821
- setting={setting}
822
- busy={busy || confirmation !== null}
823
- theme={theme}
824
- styles={styles}
825
- onConfirm={requestConfigMutation}
826
- />
827
- ))}
977
+ {inspected.settings.map((setting) => {
978
+ const identity = `${inspected.plugin}:${setting.key}`;
979
+ const localConfirmation =
980
+ confirmation?.origin.surface === "config" && confirmation.origin.key === identity
981
+ ? confirmation
982
+ : null;
983
+ return (
984
+ <ConfigSettingEditor
985
+ key={`${identity}:${configEditGenerations[identity] ?? 0}`}
986
+ plugin={inspected.plugin}
987
+ setting={setting}
988
+ busy={busy}
989
+ confirmation={localConfirmation}
990
+ confirmationOpen={confirmation !== null}
991
+ theme={theme}
992
+ styles={styles}
993
+ onCancel={() => setConfirmation(null)}
994
+ onApply={applyConfirmation}
995
+ onConfirm={requestConfigMutation}
996
+ />
997
+ );
998
+ })}
828
999
  </View>
829
1000
  </View>
830
1001
  ) : null}