@omercnet/paseo-omp 0.2.1-next.72.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 (78) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/SUPPORT.md +42 -0
  5. package/TESTING.md +150 -0
  6. package/client/composer-pill-settings.tsx +157 -0
  7. package/client/hub-icon.tsx +12 -0
  8. package/client/hub-popover.tsx +132 -0
  9. package/client/hub-status.ts +29 -0
  10. package/client/mcp-authorization.tsx +168 -0
  11. package/client/mcp-popover.tsx +155 -0
  12. package/client/memory-panel.tsx +76 -0
  13. package/client/memory-popover.tsx +74 -0
  14. package/client/omp-config-surface.tsx +1433 -0
  15. package/client/omp-doc-links.ts +117 -0
  16. package/client/omp-plugin-manager.tsx +1004 -0
  17. package/client/omp-store-picker.tsx +89 -0
  18. package/client/omp-store-state.ts +45 -0
  19. package/client/provider-diagnostics-state.ts +262 -0
  20. package/client/provider-icon.tsx +27 -0
  21. package/client/provider-image.tsx +66 -0
  22. package/client/quota-popover.tsx +155 -0
  23. package/client/quota-state.ts +140 -0
  24. package/client/sessions-popover.tsx +78 -0
  25. package/docs/alpha-release-checklist.md +68 -0
  26. package/docs/configuration.md +126 -0
  27. package/docs/core-provider-issue-audit.md +108 -0
  28. package/docs/images/mcp-authorization-compact.png +0 -0
  29. package/docs/images/mcp-controls-wide.png +0 -0
  30. package/docs/images/plugin-manager.png +0 -0
  31. package/docs/images/workspace-settings.png +0 -0
  32. package/docs/installation.md +67 -0
  33. package/index.client.tsx +488 -0
  34. package/index.server.ts +81 -0
  35. package/package.json +84 -0
  36. package/paseo-plugin.json +5 -0
  37. package/scripts/prepare-dependencies.mjs +20 -0
  38. package/server/hub.ts +145 -0
  39. package/server/mcp-browser.ts +95 -0
  40. package/server/memory.ts +86 -0
  41. package/server/mutation-queue.ts +12 -0
  42. package/server/omp-config.ts +135 -0
  43. package/server/omp-plugins.ts +676 -0
  44. package/server/omp-settings.ts +499 -0
  45. package/server/paths.ts +181 -0
  46. package/server/provider/catalog.ts +172 -0
  47. package/server/provider/config-normalization.ts +148 -0
  48. package/server/provider/connection.ts +1196 -0
  49. package/server/provider/host-tools.ts +777 -0
  50. package/server/provider/image.ts +143 -0
  51. package/server/provider/mcp-transport.ts +394 -0
  52. package/server/provider/omp-rpc.ts +2806 -0
  53. package/server/provider/omp.svg +5 -0
  54. package/server/provider/profile-providers.ts +249 -0
  55. package/server/provider/provider-options.ts +27 -0
  56. package/server/provider/registration.ts +162 -0
  57. package/server/provider/security.ts +317 -0
  58. package/server/provider/session-descriptors.ts +736 -0
  59. package/server/provider/session.ts +4796 -0
  60. package/server/provider/settings.ts +78 -0
  61. package/server/provider/subsessions.ts +850 -0
  62. package/server/provider/timeline-projector.ts +1801 -0
  63. package/server/provider-diagnostics.ts +1143 -0
  64. package/server/quota.ts +55 -0
  65. package/server/sessions.ts +58 -0
  66. package/shared/composer-pill-settings.ts +28 -0
  67. package/shared/hub.ts +43 -0
  68. package/shared/mcp.ts +47 -0
  69. package/shared/memory.ts +24 -0
  70. package/shared/omp-config.ts +85 -0
  71. package/shared/omp-plugins.ts +264 -0
  72. package/shared/omp-settings.ts +214 -0
  73. package/shared/omp-store.ts +58 -0
  74. package/shared/provider-diagnostics.ts +126 -0
  75. package/shared/provider-image.ts +160 -0
  76. package/shared/quota.ts +23 -0
  77. package/shared/sessions.ts +24 -0
  78. package/tsconfig.json +16 -0
@@ -0,0 +1,1004 @@
1
+ import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
2
+ import { TextInput } from "@getpaseo/plugin/client/react-native";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import type { TextStyle, ViewStyle } from "react-native";
6
+ import { Pressable, Switch, Text, View } from "react-native";
7
+ import {
8
+ inspectOmpPluginConfig,
9
+ listOmpPlugins,
10
+ mutateOmpPlugin,
11
+ mutateOmpPluginConfig,
12
+ type OmpInstalledPlugin,
13
+ type OmpPluginConfigMutation,
14
+ OmpPluginConfigMutationSchema,
15
+ type OmpPluginConfigSetting,
16
+ type OmpPluginConfigState,
17
+ OmpPluginInstallSourceSchema,
18
+ type OmpPluginMutation,
19
+ } from "../shared/omp-plugins";
20
+
21
+ import type { OmpStore } from "../shared/omp-store";
22
+ import { ompStoreKey } from "./omp-store-state";
23
+
24
+ const PLUGINS_QUERY_KEY = ["paseo-omp", "plugins"] as const;
25
+
26
+ type ConfirmationOrigin =
27
+ | { surface: "install" }
28
+ | { surface: "plugin"; key: string }
29
+ | { surface: "config"; key: string };
30
+
31
+ type PendingConfirmation =
32
+ | {
33
+ kind: "plugin";
34
+ input: OmpPluginMutation;
35
+ origin: ConfirmationOrigin;
36
+ title: string;
37
+ warning: string;
38
+ }
39
+ | {
40
+ kind: "config";
41
+ input: OmpPluginConfigMutation;
42
+ origin: ConfirmationOrigin;
43
+ title: string;
44
+ warning: string;
45
+ };
46
+
47
+ type PluginManagerStyles = {
48
+ root: ViewStyle;
49
+ header: ViewStyle;
50
+ title: TextStyle;
51
+ muted: TextStyle;
52
+ error: TextStyle;
53
+ success: TextStyle;
54
+ card: ViewStyle;
55
+ cardHeader: ViewStyle;
56
+ cardTitle: TextStyle;
57
+ metadata: ViewStyle;
58
+ metadataRow: ViewStyle;
59
+ metadataLabel: TextStyle;
60
+ metadataValue: TextStyle;
61
+ pluginGrid: ViewStyle;
62
+ pluginCard: ViewStyle;
63
+ actions: ViewStyle;
64
+ button: ViewStyle;
65
+ buttonPrimary: ViewStyle;
66
+ buttonDanger: ViewStyle;
67
+ buttonDisabled: ViewStyle;
68
+ buttonText: TextStyle;
69
+ buttonTextPrimary: TextStyle;
70
+ input: TextStyle;
71
+ scopeRow: ViewStyle;
72
+ scopeButton: ViewStyle;
73
+ scopeButtonActive: ViewStyle;
74
+ scopeText: TextStyle;
75
+ scopeTextActive: TextStyle;
76
+ confirmation: ViewStyle;
77
+ warning: TextStyle;
78
+ settings: ViewStyle;
79
+ setting: ViewStyle;
80
+ settingName: TextStyle;
81
+ settingDescription: TextStyle;
82
+ };
83
+
84
+ function usePluginManagerStyles(
85
+ theme: PluginSurfaceProps["theme"],
86
+ compact: boolean,
87
+ ): PluginManagerStyles {
88
+ return useMemo(
89
+ () => ({
90
+ root: { gap: compact ? 10 : 14 },
91
+ header: {
92
+ flexDirection: "row",
93
+ alignItems: "center",
94
+ justifyContent: "space-between",
95
+ flexWrap: "wrap",
96
+ gap: 8,
97
+ },
98
+ title: { color: theme.colors.foreground, fontSize: compact ? 16 : 18, fontWeight: "600" },
99
+ muted: { color: theme.colors.foregroundMuted, fontSize: 13, lineHeight: 18 },
100
+ error: { color: theme.colors.statusDanger, fontSize: 13, lineHeight: 18 },
101
+ success: { color: theme.colors.statusSuccess, fontSize: 13, lineHeight: 18 },
102
+ card: {
103
+ gap: 10,
104
+ padding: compact ? 10 : 12,
105
+ borderWidth: 1,
106
+ borderColor: theme.colors.border,
107
+ borderRadius: 10,
108
+ backgroundColor: theme.colors.surface1,
109
+ },
110
+ cardHeader: {
111
+ flexDirection: "row",
112
+ alignItems: "center",
113
+ justifyContent: "space-between",
114
+ flexWrap: "wrap",
115
+ gap: 8,
116
+ },
117
+ cardTitle: { color: theme.colors.foreground, fontSize: 14, fontWeight: "600", flexShrink: 1 },
118
+ metadata: { gap: 5 },
119
+ metadataRow: { flexDirection: compact ? "column" : "row", gap: compact ? 1 : 8 },
120
+ metadataLabel: {
121
+ width: compact ? undefined : 80,
122
+ color: theme.colors.foregroundMuted,
123
+ fontSize: 12,
124
+ },
125
+ metadataValue: { color: theme.colors.foreground, fontSize: 12, flexShrink: 1 },
126
+ pluginGrid: { flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: 10 },
127
+ pluginCard: {
128
+ width: compact ? "100%" : 320,
129
+ flexGrow: compact ? 0 : 1,
130
+ gap: 8,
131
+ padding: compact ? 10 : 12,
132
+ borderWidth: 1,
133
+ borderColor: theme.colors.border,
134
+ borderRadius: 10,
135
+ backgroundColor: theme.colors.surface1,
136
+ },
137
+ actions: { flexDirection: "row", flexWrap: "wrap", gap: 7 },
138
+ button: {
139
+ paddingHorizontal: 10,
140
+ paddingVertical: 7,
141
+ borderWidth: 1,
142
+ borderColor: theme.colors.border,
143
+ borderRadius: 7,
144
+ backgroundColor: theme.colors.surface0,
145
+ },
146
+ buttonPrimary: { borderColor: theme.colors.accent, backgroundColor: theme.colors.accent },
147
+ buttonDanger: { borderColor: theme.colors.statusDanger },
148
+ buttonDisabled: { opacity: 0.5 },
149
+ buttonText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" },
150
+ buttonTextPrimary: { color: theme.colors.accentForeground },
151
+ input: {
152
+ color: theme.colors.foreground,
153
+ borderWidth: 1,
154
+ borderColor: theme.colors.border,
155
+ borderRadius: 8,
156
+ backgroundColor: theme.colors.surface0,
157
+ paddingHorizontal: 10,
158
+ paddingVertical: 8,
159
+ fontSize: 13,
160
+ },
161
+ scopeRow: { flexDirection: "row", flexWrap: "wrap", gap: 6 },
162
+ scopeButton: {
163
+ paddingHorizontal: 9,
164
+ paddingVertical: 6,
165
+ borderWidth: 1,
166
+ borderColor: theme.colors.border,
167
+ borderRadius: 7,
168
+ },
169
+ scopeButtonActive: { backgroundColor: theme.colors.surface2 },
170
+ scopeText: { color: theme.colors.foregroundMuted, fontSize: 12 },
171
+ scopeTextActive: { color: theme.colors.foreground, fontWeight: "600" },
172
+ confirmation: {
173
+ gap: 9,
174
+ padding: compact ? 10 : 12,
175
+ borderWidth: 1,
176
+ borderColor: theme.colors.statusWarning,
177
+ borderRadius: 10,
178
+ backgroundColor: theme.colors.surface1,
179
+ },
180
+ warning: { color: theme.colors.statusWarning, fontSize: 13, lineHeight: 18 },
181
+ settings: { gap: 7 },
182
+ setting: { gap: 3, paddingTop: 7, borderTopWidth: 1, borderTopColor: theme.colors.border },
183
+ settingName: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" },
184
+ settingDescription: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
185
+ }),
186
+ [compact, theme],
187
+ );
188
+ }
189
+
190
+ function ActionButton({
191
+ label,
192
+ disabled,
193
+ primary,
194
+ danger,
195
+ styles,
196
+ onPress,
197
+ }: {
198
+ label: string;
199
+ disabled: boolean;
200
+ primary?: boolean;
201
+ danger?: boolean;
202
+ styles: PluginManagerStyles;
203
+ onPress(): void;
204
+ }) {
205
+ return (
206
+ <Pressable
207
+ accessibilityRole="button"
208
+ accessibilityState={{ disabled }}
209
+ disabled={disabled}
210
+ onPress={onPress}
211
+ style={[
212
+ styles.button,
213
+ primary ? styles.buttonPrimary : null,
214
+ danger ? styles.buttonDanger : null,
215
+ disabled ? styles.buttonDisabled : null,
216
+ ]}
217
+ >
218
+ <Text style={[styles.buttonText, primary ? styles.buttonTextPrimary : null]}>{label}</Text>
219
+ </Pressable>
220
+ );
221
+ }
222
+
223
+ function MetadataRow({
224
+ label,
225
+ value,
226
+ styles,
227
+ }: {
228
+ label: string;
229
+ value: string;
230
+ styles: PluginManagerStyles;
231
+ }) {
232
+ return (
233
+ <View style={styles.metadataRow}>
234
+ <Text style={styles.metadataLabel}>{label}</Text>
235
+ <Text selectable style={styles.metadataValue}>
236
+ {value}
237
+ </Text>
238
+ </View>
239
+ );
240
+ }
241
+
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 {
284
+ const target = input.action === "install" ? input.source : input.plugin;
285
+ switch (input.action) {
286
+ case "install":
287
+ return {
288
+ kind: "plugin",
289
+ input,
290
+ origin,
291
+ title: `Install ${target}?`,
292
+ warning:
293
+ "OMP plugins are trusted code. Installing can fetch code and change persistent state; runtime extensions load in a future OMP session.",
294
+ };
295
+ case "enable":
296
+ return {
297
+ kind: "plugin",
298
+ input,
299
+ origin,
300
+ title: `Enable ${target}?`,
301
+ warning:
302
+ "Enabling changes persistent state and permits this plugin's trusted code to load in future OMP sessions.",
303
+ };
304
+ case "disable":
305
+ return {
306
+ kind: "plugin",
307
+ input,
308
+ origin,
309
+ title: `Disable ${target}?`,
310
+ warning:
311
+ "Disabling changes persistent state. OMP sessions that are already running are not unloaded.",
312
+ };
313
+ case "uninstall":
314
+ return {
315
+ kind: "plugin",
316
+ input,
317
+ origin,
318
+ title: `Uninstall ${target}?`,
319
+ warning:
320
+ "Uninstalling removes the selected plugin registration and cached installation. Running OMP sessions are unchanged.",
321
+ };
322
+ case "upgrade":
323
+ return {
324
+ kind: "plugin",
325
+ input,
326
+ origin,
327
+ title: `Upgrade ${target}?`,
328
+ warning:
329
+ "Upgrading fetches and replaces trusted plugin code. The updated runtime loads in a future OMP session.",
330
+ };
331
+ }
332
+ }
333
+
334
+ function configConfirmationFor(
335
+ input: OmpPluginConfigMutation,
336
+ setting: OmpPluginConfigSetting,
337
+ origin: ConfirmationOrigin,
338
+ ): PendingConfirmation {
339
+ if (input.action === "delete") {
340
+ return {
341
+ kind: "config",
342
+ input,
343
+ origin,
344
+ title: `Delete ${setting.key}?`,
345
+ warning:
346
+ "This changes persistent OMP plugin configuration. Future sessions will use the setting's default or environment fallback.",
347
+ };
348
+ }
349
+ return {
350
+ kind: "config",
351
+ input,
352
+ origin,
353
+ title: `Set ${setting.key}?`,
354
+ warning: setting.secret
355
+ ? "This writes a secret value to OMP's persistent plugin configuration. The value will remain write-only in Paseo."
356
+ : "This changes persistent OMP plugin configuration for future sessions.",
357
+ };
358
+ }
359
+
360
+ function ConfigSettingEditor({
361
+ plugin,
362
+ setting,
363
+ busy,
364
+ confirmation,
365
+ confirmationOpen,
366
+ theme,
367
+ styles,
368
+ onCancel,
369
+ onApply,
370
+ onConfirm,
371
+ }: {
372
+ plugin: string;
373
+ setting: OmpPluginConfigSetting;
374
+ busy: boolean;
375
+ confirmation: PendingConfirmation | null;
376
+ confirmationOpen: boolean;
377
+ theme: PluginSurfaceProps["theme"];
378
+ styles: PluginManagerStyles;
379
+ onCancel(): void;
380
+ onApply(): void;
381
+ onConfirm(input: OmpPluginConfigMutation, setting: OmpPluginConfigSetting): void;
382
+ }) {
383
+ const [draft, setDraft] = useState<string | boolean>(setting.type === "boolean" ? false : "");
384
+ const blocked = busy || confirmationOpen;
385
+ let value: string | number | boolean | undefined;
386
+ let validationMessage: string | null = null;
387
+ if (!setting.secret) {
388
+ if (setting.type === "boolean") {
389
+ value = draft === true;
390
+ } else if (setting.type === "number") {
391
+ const raw = typeof draft === "string" ? draft.trim() : "";
392
+ const parsed = raw ? Number(raw) : Number.NaN;
393
+ if (!Number.isFinite(parsed)) validationMessage = "Enter a finite number.";
394
+ else if (setting.minimum !== undefined && parsed < setting.minimum) {
395
+ validationMessage = `Minimum: ${setting.minimum}`;
396
+ } else if (setting.maximum !== undefined && parsed > setting.maximum) {
397
+ validationMessage = `Maximum: ${setting.maximum}`;
398
+ } else value = parsed;
399
+ } else if (setting.type === "enum") {
400
+ if (typeof draft !== "string" || !setting.enumValues.includes(draft)) {
401
+ validationMessage = "Choose one of the documented values.";
402
+ } else value = draft;
403
+ } else if (typeof draft === "string") {
404
+ value = draft;
405
+ }
406
+ }
407
+ const candidate =
408
+ value === undefined
409
+ ? null
410
+ : OmpPluginConfigMutationSchema.safeParse({
411
+ action: "set",
412
+ plugin,
413
+ key: setting.key,
414
+ value,
415
+ });
416
+ if (candidate && !candidate.success) validationMessage = "Enter a valid bounded value.";
417
+
418
+ return (
419
+ <View style={styles.setting}>
420
+ <Text selectable style={styles.settingName}>
421
+ {setting.key} · {setting.type}
422
+ </Text>
423
+ <Text style={setting.secret ? styles.warning : styles.muted}>
424
+ {setting.secret
425
+ ? setting.configured
426
+ ? "Secret · configured"
427
+ : "Secret · not configured"
428
+ : setting.configured
429
+ ? "Configured"
430
+ : "Not configured"}
431
+ </Text>
432
+ {setting.description ? (
433
+ <Text style={styles.settingDescription}>{setting.description}</Text>
434
+ ) : null}
435
+ {setting.secret ? (
436
+ <Text style={styles.warning}>
437
+ Secret writes are unavailable because OMP accepts plugin setting values through process
438
+ arguments. Configure this secret directly with OMP.
439
+ </Text>
440
+ ) : setting.type === "boolean" ? (
441
+ <View style={styles.metadataRow}>
442
+ <Switch
443
+ accessibilityLabel={`New value for ${setting.key}`}
444
+ disabled={blocked}
445
+ value={draft === true}
446
+ onValueChange={setDraft}
447
+ />
448
+ <Text style={styles.metadataValue}>{draft === true ? "True" : "False"}</Text>
449
+ </View>
450
+ ) : setting.type === "enum" ? (
451
+ <View accessibilityRole="radiogroup" style={styles.scopeRow}>
452
+ {setting.enumValues.map((option) => {
453
+ const selected = draft === option;
454
+ return (
455
+ <Pressable
456
+ key={option}
457
+ accessibilityRole="radio"
458
+ accessibilityState={{ checked: selected, disabled: blocked }}
459
+ disabled={blocked}
460
+ onPress={() => setDraft(option)}
461
+ style={[styles.scopeButton, selected ? styles.scopeButtonActive : null]}
462
+ >
463
+ <Text style={[styles.scopeText, selected ? styles.scopeTextActive : null]}>
464
+ {option}
465
+ </Text>
466
+ </Pressable>
467
+ );
468
+ })}
469
+ </View>
470
+ ) : (
471
+ <TextInput
472
+ accessibilityLabel={`New value for ${setting.key}`}
473
+ editable={!blocked}
474
+ value={typeof draft === "string" ? draft : ""}
475
+ onChangeText={setDraft}
476
+ placeholder="Enter a new value"
477
+ placeholderTextColor={theme.colors.foregroundMuted}
478
+ keyboardType={setting.type === "number" ? "numeric" : "default"}
479
+ autoCapitalize="none"
480
+ autoCorrect={false}
481
+ style={styles.input}
482
+ />
483
+ )}
484
+ {validationMessage && (setting.type !== "boolean" || value === undefined) ? (
485
+ <Text style={styles.error}>{validationMessage}</Text>
486
+ ) : null}
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
+ )}
519
+ </View>
520
+ );
521
+ }
522
+
523
+ function PluginCard({
524
+ plugin,
525
+ busy,
526
+ confirmation,
527
+ confirmationOpen,
528
+ workspaceScoped,
529
+ inspecting,
530
+ styles,
531
+ onCancel,
532
+ onApply,
533
+ onConfirm,
534
+ onInspect,
535
+ }: {
536
+ plugin: OmpInstalledPlugin;
537
+ busy: boolean;
538
+ confirmation: PendingConfirmation | null;
539
+ confirmationOpen: boolean;
540
+ workspaceScoped: boolean;
541
+ inspecting: boolean;
542
+ styles: PluginManagerStyles;
543
+ onCancel(): void;
544
+ onApply(): void;
545
+ onConfirm(input: OmpPluginMutation, origin: ConfirmationOrigin): void;
546
+ onInspect(plugin: string): void;
547
+ }) {
548
+ const scope = plugin.scope ?? undefined;
549
+ const identity = pluginCardIdentity(plugin);
550
+ const blocked = busy || confirmationOpen;
551
+ const projectUnavailable = plugin.scope === "project" && !workspaceScoped;
552
+ return (
553
+ <View style={styles.pluginCard}>
554
+ <View style={styles.cardHeader}>
555
+ <Text selectable style={styles.cardTitle}>
556
+ {plugin.id}
557
+ </Text>
558
+ <Text style={plugin.enabled ? styles.success : styles.muted}>
559
+ {plugin.enabled ? "Enabled" : "Disabled"}
560
+ </Text>
561
+ </View>
562
+ <View style={styles.metadata}>
563
+ <MetadataRow
564
+ styles={styles}
565
+ label="Source"
566
+ value={plugin.source === "marketplace" ? `Marketplace · ${plugin.scope}` : "npm / linked"}
567
+ />
568
+ <MetadataRow styles={styles} label="Version" value={plugin.version ?? "Unknown"} />
569
+ {plugin.path ? <MetadataRow styles={styles} label="Path" value={plugin.path} /> : null}
570
+ {plugin.description ? (
571
+ <MetadataRow styles={styles} label="Description" value={plugin.description} />
572
+ ) : null}
573
+ {plugin.enabledFeatures.length > 0 ? (
574
+ <MetadataRow styles={styles} label="Features" value={plugin.enabledFeatures.join(", ")} />
575
+ ) : null}
576
+ {plugin.usesDefaultFeatures ? (
577
+ <MetadataRow styles={styles} label="Features" value="Manifest defaults" />
578
+ ) : null}
579
+ {plugin.shadowed ? (
580
+ <Text style={styles.warning}>Shadowed by an enabled project-scoped installation.</Text>
581
+ ) : null}
582
+ {plugin.scope === "project" ? (
583
+ <Text style={styles.warning}>
584
+ {workspaceScoped
585
+ ? "This installation is scoped to the current workspace."
586
+ : "Open this project's workspace OMP panel to manage this installation."}
587
+ </Text>
588
+ ) : null}
589
+ {plugin.ambiguous ? (
590
+ <Text style={styles.warning}>
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.
598
+ </Text>
599
+ ) : null}
600
+ </View>
601
+ {confirmation ? (
602
+ <ConfirmationPanel
603
+ confirmation={confirmation}
604
+ busy={busy}
605
+ styles={styles}
606
+ onCancel={onCancel}
607
+ onConfirm={onApply}
608
+ />
609
+ ) : (
610
+ <View style={styles.actions}>
611
+ <ActionButton
612
+ label={plugin.enabled ? "Disable" : "Enable"}
613
+ disabled={blocked || projectUnavailable || plugin.ambiguous}
614
+ styles={styles}
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
+ }
625
+ />
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}
649
+ <ActionButton
650
+ label="Uninstall"
651
+ disabled={blocked || projectUnavailable || plugin.ambiguous}
652
+ danger
653
+ styles={styles}
654
+ onPress={() =>
655
+ onConfirm(
656
+ { action: "uninstall", plugin: plugin.id, ...(scope ? { scope } : {}) },
657
+ { surface: "plugin", key: identity },
658
+ )
659
+ }
660
+ />
661
+ </View>
662
+ )}
663
+ </View>
664
+ );
665
+ }
666
+
667
+ export function OmpPluginManagerSection({
668
+ theme,
669
+ compact,
670
+ cwd,
671
+ store,
672
+ }: {
673
+ theme: PluginSurfaceProps["theme"];
674
+ compact: boolean;
675
+ cwd?: string;
676
+ store?: OmpStore;
677
+ }) {
678
+ const context = { store, ...(cwd ? { cwd } : {}) };
679
+ const loadPlugins = useRpc(listOmpPlugins);
680
+ const inspectPluginConfig = useRpc(inspectOmpPluginConfig);
681
+ const mutatePlugin = useRpc(mutateOmpPlugin);
682
+ const mutatePluginConfig = useRpc(mutateOmpPluginConfig);
683
+ const queryClient = useQueryClient();
684
+ const styles = usePluginManagerStyles(theme, compact);
685
+ const [source, setSource] = useState("");
686
+ const [confirmation, setConfirmation] = useState<PendingConfirmation | null>(null);
687
+ const [notice, setNotice] = useState<{ tone: "success" | "error"; text: string } | null>(null);
688
+ const [inspected, setInspected] = useState<OmpPluginConfigState | null>(null);
689
+ const [configEditGenerations, setConfigEditGenerations] = useState<Record<string, number>>({});
690
+ const inspectionGeneration = useRef(0);
691
+ const plugins = useQuery({
692
+ queryKey: [...PLUGINS_QUERY_KEY, ompStoreKey(store), cwd ?? "global"],
693
+ queryFn: () => loadPlugins(context),
694
+ staleTime: Number.POSITIVE_INFINITY,
695
+ });
696
+ const mutation = useMutation({
697
+ mutationKey: ["paseo-omp", "plugins", ompStoreKey(store)],
698
+ mutationFn: (input: OmpPluginMutation) => mutatePlugin({ ...input, ...context }),
699
+ onSuccess: (result) => {
700
+ void queryClient.invalidateQueries({ queryKey: PLUGINS_QUERY_KEY });
701
+ queryClient.setQueryData(
702
+ [...PLUGINS_QUERY_KEY, ompStoreKey(store), cwd ?? "global"],
703
+ result.state,
704
+ );
705
+ setNotice({ tone: result.ok ? "success" : "error", text: result.message });
706
+ if (result.ok && confirmation?.kind === "plugin" && confirmation.input.action === "install") {
707
+ setSource("");
708
+ }
709
+ setConfirmation(null);
710
+ },
711
+ onError: () => {
712
+ setNotice({ tone: "error", text: "The plugin operation could not be completed." });
713
+ setConfirmation(null);
714
+ },
715
+ });
716
+ const configInspection = useMutation({
717
+ mutationFn: ({ plugin }: { plugin: string; generation: number }) =>
718
+ inspectPluginConfig({ plugin, ...context }),
719
+ onSuccess: (result, request) => {
720
+ if (inspectionGeneration.current === request.generation) setInspected(result);
721
+ },
722
+ onError: (_error, request) => {
723
+ if (inspectionGeneration.current === request.generation) setInspected(null);
724
+ },
725
+ });
726
+ const configMutation = useMutation({
727
+ mutationKey: ["paseo-omp", "plugin-config", ompStoreKey(store)],
728
+ mutationFn: (input: OmpPluginConfigMutation) => mutatePluginConfig({ ...input, ...context }),
729
+ onSuccess: (result, input) => {
730
+ if (result.config.available) setInspected(result.config);
731
+ if (result.ok) {
732
+ const identity = `${input.plugin}:${input.key}`;
733
+ setConfigEditGenerations((current) => ({
734
+ ...current,
735
+ [identity]: (current[identity] ?? 0) + 1,
736
+ }));
737
+ }
738
+ void queryClient.invalidateQueries({ queryKey: PLUGINS_QUERY_KEY });
739
+ setNotice({ tone: result.ok ? "success" : "error", text: result.message });
740
+ setConfirmation(null);
741
+ },
742
+ onError: () => {
743
+ setNotice({ tone: "error", text: "The plugin setting operation could not be completed." });
744
+ setConfirmation(null);
745
+ },
746
+ });
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]);
767
+
768
+ const requestInstall = () => {
769
+ const parsed = OmpPluginInstallSourceSchema.safeParse(source);
770
+ if (!parsed.success) {
771
+ setNotice({
772
+ tone: "error",
773
+ text: "Enter one valid package, marketplace ID, Git source, or local path without options or control characters.",
774
+ });
775
+ return;
776
+ }
777
+ setNotice(null);
778
+ setConfirmation(
779
+ confirmationFor(
780
+ {
781
+ action: "install",
782
+ source: parsed.data,
783
+ scope: "user",
784
+ ...context,
785
+ },
786
+ { surface: "install" },
787
+ ),
788
+ );
789
+ };
790
+ const requestMutation = (input: OmpPluginMutation, origin: ConfirmationOrigin) => {
791
+ setNotice(null);
792
+ setConfirmation(confirmationFor({ ...input, ...context }, origin));
793
+ };
794
+ const requestConfigMutation = (
795
+ input: OmpPluginConfigMutation,
796
+ setting: OmpPluginConfigSetting,
797
+ ) => {
798
+ setNotice(null);
799
+ setConfirmation(
800
+ configConfirmationFor({ ...input, ...context }, setting, {
801
+ surface: "config",
802
+ key: `${input.plugin}:${input.key}`,
803
+ }),
804
+ );
805
+ };
806
+ const inspect = (plugin: string) => {
807
+ inspectionGeneration.current += 1;
808
+ if (inspected?.plugin === plugin) {
809
+ setInspected(null);
810
+ configInspection.reset();
811
+ setConfirmation(null);
812
+ return;
813
+ }
814
+ const generation = inspectionGeneration.current;
815
+ setInspected(null);
816
+ setConfirmation(null);
817
+ configInspection.mutate({ plugin, generation });
818
+ };
819
+ const applyConfirmation = () => {
820
+ if (!confirmation) return;
821
+ if (confirmation.kind === "plugin") mutation.mutate(confirmation.input);
822
+ else configMutation.mutate(confirmation.input);
823
+ };
824
+
825
+ return (
826
+ <View style={styles.root}>
827
+ <View style={styles.header}>
828
+ <View>
829
+ <Text style={styles.title}>OMP plugins</Text>
830
+ <Text style={styles.muted}>Manage OMP's native plugins, not Paseo plugins.</Text>
831
+ </View>
832
+ <ActionButton
833
+ label={plugins.isFetching ? "Refreshing…" : "Refresh"}
834
+ disabled={plugins.isFetching || busy || confirmation !== null}
835
+ styles={styles}
836
+ onPress={() => {
837
+ void plugins.refetch();
838
+ }}
839
+ />
840
+ </View>
841
+
842
+ <View style={styles.card}>
843
+ <Text style={styles.cardTitle}>Install a plugin</Text>
844
+ <Text style={styles.muted}>
845
+ Enter one source accepted by OMP, such as a package, name@marketplace, Git URL, or local
846
+ path.
847
+ </Text>
848
+ <TextInput
849
+ accessibilityLabel="OMP plugin source"
850
+ editable={!busy && confirmation === null}
851
+ value={source}
852
+ onChangeText={setSource}
853
+ placeholder="name@marketplace or package source"
854
+ placeholderTextColor={theme.colors.foregroundMuted}
855
+ autoCapitalize="none"
856
+ autoCorrect={false}
857
+ style={styles.input}
858
+ />
859
+ {confirmation?.origin.surface === "install" ? (
860
+ <ConfirmationPanel
861
+ confirmation={confirmation}
862
+ busy={busy}
863
+ styles={styles}
864
+ onCancel={() => setConfirmation(null)}
865
+ onConfirm={applyConfirmation}
866
+ />
867
+ ) : (
868
+ <View style={styles.actions}>
869
+ <ActionButton
870
+ label="Review install"
871
+ disabled={busy || confirmation !== null || source.length === 0}
872
+ primary
873
+ styles={styles}
874
+ onPress={requestInstall}
875
+ />
876
+ </View>
877
+ )}
878
+ </View>
879
+
880
+ {notice ? (
881
+ <Text
882
+ accessibilityRole="alert"
883
+ style={notice.tone === "success" ? styles.success : styles.error}
884
+ >
885
+ {notice.text}
886
+ </Text>
887
+ ) : null}
888
+ {plugins.isLoading ? <Text style={styles.muted}>Loading OMP plugins…</Text> : null}
889
+ {plugins.error ? (
890
+ <Text accessibilityRole="alert" style={styles.error}>
891
+ Could not load OMP plugin state.
892
+ </Text>
893
+ ) : null}
894
+ {plugins.data?.error ? (
895
+ <Text accessibilityRole="alert" style={styles.error}>
896
+ {plugins.data.error}
897
+ </Text>
898
+ ) : null}
899
+ {plugins.data?.available && plugins.data.plugins.length === 0 ? (
900
+ <View style={styles.card}>
901
+ <Text style={styles.cardTitle}>No plugins installed</Text>
902
+ <Text style={styles.muted}>
903
+ OMP reported an empty npm and marketplace plugin catalog.
904
+ </Text>
905
+ </View>
906
+ ) : null}
907
+ {plugins.data?.droppedCount ? (
908
+ <Text style={styles.warning}>
909
+ {plugins.data.droppedCount} invalid or excess plugin records were omitted.
910
+ </Text>
911
+ ) : null}
912
+
913
+ {plugins.data?.plugins.length ? (
914
+ <View style={styles.pluginGrid}>
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
+ })}
941
+ </View>
942
+ ) : null}
943
+ {configInspection.error || configMutation.error ? (
944
+ <Text accessibilityRole="alert" style={styles.error}>
945
+ Could not update OMP plugin settings.
946
+ </Text>
947
+ ) : null}
948
+ {inspected ? (
949
+ <View style={styles.card}>
950
+ <View style={styles.cardHeader}>
951
+ <Text style={styles.cardTitle}>Settings · {inspected.plugin}</Text>
952
+ <ActionButton
953
+ label="Close"
954
+ disabled={busy || confirmation !== null}
955
+ styles={styles}
956
+ onPress={() => {
957
+ inspectionGeneration.current += 1;
958
+ setInspected(null);
959
+ setConfirmation(null);
960
+ }}
961
+ />
962
+ </View>
963
+ <Text style={styles.muted}>
964
+ Current values and defaults are withheld. New values are write-only and are never
965
+ returned to the app.
966
+ </Text>
967
+ {inspected.error ? <Text style={styles.error}>{inspected.error}</Text> : null}
968
+ {inspected.droppedCount > 0 ? (
969
+ <Text style={styles.warning}>
970
+ {inspected.droppedCount} invalid or excess setting definitions were omitted.
971
+ </Text>
972
+ ) : null}
973
+ {inspected.available && inspected.settings.length === 0 ? (
974
+ <Text style={styles.muted}>This plugin declares no settings.</Text>
975
+ ) : null}
976
+ <View style={styles.settings}>
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
+ })}
999
+ </View>
1000
+ </View>
1001
+ ) : null}
1002
+ </View>
1003
+ );
1004
+ }