@omercnet/paseo-omp 0.2.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.
- package/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/SUPPORT.md +40 -0
- package/TESTING.md +147 -0
- package/client/hub-icon.tsx +12 -0
- package/client/hub-popover.tsx +132 -0
- package/client/hub-status.ts +29 -0
- package/client/memory-panel.tsx +71 -0
- package/client/memory-popover.tsx +70 -0
- package/client/omp-config-surface.tsx +1274 -0
- package/client/omp-doc-links.ts +117 -0
- package/client/omp-plugin-manager.tsx +833 -0
- package/client/provider-diagnostics-state.ts +250 -0
- package/client/provider-icon.tsx +27 -0
- package/client/provider-image.tsx +66 -0
- package/client/quota-popover.tsx +150 -0
- package/client/quota-state.ts +131 -0
- package/client/sessions-popover.tsx +73 -0
- package/docs/alpha-release-checklist.md +70 -0
- package/docs/configuration.md +122 -0
- package/docs/core-provider-issue-audit.md +108 -0
- package/docs/installation.md +73 -0
- package/index.client.tsx +272 -0
- package/index.server.ts +51 -0
- package/package.json +84 -0
- package/paseo-plugin.json +5 -0
- package/server/hub.ts +145 -0
- package/server/memory.ts +86 -0
- package/server/mutation-queue.ts +12 -0
- package/server/omp-config.ts +126 -0
- package/server/omp-plugins.ts +627 -0
- package/server/omp-settings.ts +291 -0
- package/server/paths.ts +64 -0
- package/server/provider/catalog.ts +173 -0
- package/server/provider/config-normalization.ts +148 -0
- package/server/provider/connection.ts +992 -0
- package/server/provider/host-tools.ts +706 -0
- package/server/provider/image.ts +143 -0
- package/server/provider/mcp-transport.ts +394 -0
- package/server/provider/omp-rpc.ts +2739 -0
- package/server/provider/omp.svg +5 -0
- package/server/provider/provider-options.ts +27 -0
- package/server/provider/registration.ts +151 -0
- package/server/provider/security.ts +317 -0
- package/server/provider/session-descriptors.ts +431 -0
- package/server/provider/session.ts +4451 -0
- package/server/provider/settings.ts +78 -0
- package/server/provider/subsessions.ts +847 -0
- package/server/provider/timeline-projector.ts +1764 -0
- package/server/provider-diagnostics.ts +1057 -0
- package/server/quota.ts +54 -0
- package/server/sessions.ts +58 -0
- package/shared/hub.ts +43 -0
- package/shared/memory.ts +23 -0
- package/shared/omp-config.ts +81 -0
- package/shared/omp-plugins.ts +223 -0
- package/shared/omp-settings.ts +207 -0
- package/shared/provider-diagnostics.ts +117 -0
- package/shared/provider-image.ts +160 -0
- package/shared/quota.ts +22 -0
- package/shared/sessions.ts +23 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,833 @@
|
|
|
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 { 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
|
+
const PLUGINS_QUERY_KEY = ["paseo-omp", "plugins"] as const;
|
|
22
|
+
|
|
23
|
+
type PendingConfirmation =
|
|
24
|
+
| {
|
|
25
|
+
kind: "plugin";
|
|
26
|
+
input: OmpPluginMutation;
|
|
27
|
+
title: string;
|
|
28
|
+
warning: string;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
kind: "config";
|
|
32
|
+
input: OmpPluginConfigMutation;
|
|
33
|
+
title: string;
|
|
34
|
+
warning: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type PluginManagerStyles = {
|
|
38
|
+
root: ViewStyle;
|
|
39
|
+
header: ViewStyle;
|
|
40
|
+
title: TextStyle;
|
|
41
|
+
muted: TextStyle;
|
|
42
|
+
error: TextStyle;
|
|
43
|
+
success: TextStyle;
|
|
44
|
+
card: ViewStyle;
|
|
45
|
+
cardHeader: ViewStyle;
|
|
46
|
+
cardTitle: TextStyle;
|
|
47
|
+
metadata: ViewStyle;
|
|
48
|
+
metadataRow: ViewStyle;
|
|
49
|
+
metadataLabel: TextStyle;
|
|
50
|
+
metadataValue: TextStyle;
|
|
51
|
+
pluginGrid: ViewStyle;
|
|
52
|
+
pluginCard: ViewStyle;
|
|
53
|
+
actions: ViewStyle;
|
|
54
|
+
button: ViewStyle;
|
|
55
|
+
buttonPrimary: ViewStyle;
|
|
56
|
+
buttonDanger: ViewStyle;
|
|
57
|
+
buttonDisabled: ViewStyle;
|
|
58
|
+
buttonText: TextStyle;
|
|
59
|
+
buttonTextPrimary: TextStyle;
|
|
60
|
+
input: TextStyle;
|
|
61
|
+
scopeRow: ViewStyle;
|
|
62
|
+
scopeButton: ViewStyle;
|
|
63
|
+
scopeButtonActive: ViewStyle;
|
|
64
|
+
scopeText: TextStyle;
|
|
65
|
+
scopeTextActive: TextStyle;
|
|
66
|
+
confirmation: ViewStyle;
|
|
67
|
+
warning: TextStyle;
|
|
68
|
+
settings: ViewStyle;
|
|
69
|
+
setting: ViewStyle;
|
|
70
|
+
settingName: TextStyle;
|
|
71
|
+
settingDescription: TextStyle;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function usePluginManagerStyles(
|
|
75
|
+
theme: PluginSurfaceProps["theme"],
|
|
76
|
+
compact: boolean,
|
|
77
|
+
): PluginManagerStyles {
|
|
78
|
+
return useMemo(
|
|
79
|
+
() => ({
|
|
80
|
+
root: { gap: compact ? 10 : 14 },
|
|
81
|
+
header: {
|
|
82
|
+
flexDirection: "row",
|
|
83
|
+
alignItems: "center",
|
|
84
|
+
justifyContent: "space-between",
|
|
85
|
+
flexWrap: "wrap",
|
|
86
|
+
gap: 8,
|
|
87
|
+
},
|
|
88
|
+
title: { color: theme.colors.foreground, fontSize: compact ? 16 : 18, fontWeight: "600" },
|
|
89
|
+
muted: { color: theme.colors.foregroundMuted, fontSize: 13, lineHeight: 18 },
|
|
90
|
+
error: { color: theme.colors.statusDanger, fontSize: 13, lineHeight: 18 },
|
|
91
|
+
success: { color: theme.colors.statusSuccess, fontSize: 13, lineHeight: 18 },
|
|
92
|
+
card: {
|
|
93
|
+
gap: 10,
|
|
94
|
+
padding: compact ? 10 : 12,
|
|
95
|
+
borderWidth: 1,
|
|
96
|
+
borderColor: theme.colors.border,
|
|
97
|
+
borderRadius: 10,
|
|
98
|
+
backgroundColor: theme.colors.surface1,
|
|
99
|
+
},
|
|
100
|
+
cardHeader: {
|
|
101
|
+
flexDirection: "row",
|
|
102
|
+
alignItems: "center",
|
|
103
|
+
justifyContent: "space-between",
|
|
104
|
+
flexWrap: "wrap",
|
|
105
|
+
gap: 8,
|
|
106
|
+
},
|
|
107
|
+
cardTitle: { color: theme.colors.foreground, fontSize: 14, fontWeight: "600", flexShrink: 1 },
|
|
108
|
+
metadata: { gap: 5 },
|
|
109
|
+
metadataRow: { flexDirection: compact ? "column" : "row", gap: compact ? 1 : 8 },
|
|
110
|
+
metadataLabel: {
|
|
111
|
+
width: compact ? undefined : 80,
|
|
112
|
+
color: theme.colors.foregroundMuted,
|
|
113
|
+
fontSize: 12,
|
|
114
|
+
},
|
|
115
|
+
metadataValue: { color: theme.colors.foreground, fontSize: 12, flexShrink: 1 },
|
|
116
|
+
pluginGrid: { flexDirection: "row", flexWrap: "wrap", alignItems: "flex-start", gap: 10 },
|
|
117
|
+
pluginCard: {
|
|
118
|
+
width: compact ? "100%" : 320,
|
|
119
|
+
flexGrow: compact ? 0 : 1,
|
|
120
|
+
gap: 8,
|
|
121
|
+
padding: compact ? 10 : 12,
|
|
122
|
+
borderWidth: 1,
|
|
123
|
+
borderColor: theme.colors.border,
|
|
124
|
+
borderRadius: 10,
|
|
125
|
+
backgroundColor: theme.colors.surface1,
|
|
126
|
+
},
|
|
127
|
+
actions: { flexDirection: "row", flexWrap: "wrap", gap: 7 },
|
|
128
|
+
button: {
|
|
129
|
+
paddingHorizontal: 10,
|
|
130
|
+
paddingVertical: 7,
|
|
131
|
+
borderWidth: 1,
|
|
132
|
+
borderColor: theme.colors.border,
|
|
133
|
+
borderRadius: 7,
|
|
134
|
+
backgroundColor: theme.colors.surface0,
|
|
135
|
+
},
|
|
136
|
+
buttonPrimary: { borderColor: theme.colors.accent, backgroundColor: theme.colors.accent },
|
|
137
|
+
buttonDanger: { borderColor: theme.colors.statusDanger },
|
|
138
|
+
buttonDisabled: { opacity: 0.5 },
|
|
139
|
+
buttonText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" },
|
|
140
|
+
buttonTextPrimary: { color: theme.colors.accentForeground },
|
|
141
|
+
input: {
|
|
142
|
+
color: theme.colors.foreground,
|
|
143
|
+
borderWidth: 1,
|
|
144
|
+
borderColor: theme.colors.border,
|
|
145
|
+
borderRadius: 8,
|
|
146
|
+
backgroundColor: theme.colors.surface0,
|
|
147
|
+
paddingHorizontal: 10,
|
|
148
|
+
paddingVertical: 8,
|
|
149
|
+
fontSize: 13,
|
|
150
|
+
},
|
|
151
|
+
scopeRow: { flexDirection: "row", flexWrap: "wrap", gap: 6 },
|
|
152
|
+
scopeButton: {
|
|
153
|
+
paddingHorizontal: 9,
|
|
154
|
+
paddingVertical: 6,
|
|
155
|
+
borderWidth: 1,
|
|
156
|
+
borderColor: theme.colors.border,
|
|
157
|
+
borderRadius: 7,
|
|
158
|
+
},
|
|
159
|
+
scopeButtonActive: { backgroundColor: theme.colors.surface2 },
|
|
160
|
+
scopeText: { color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
161
|
+
scopeTextActive: { color: theme.colors.foreground, fontWeight: "600" },
|
|
162
|
+
confirmation: {
|
|
163
|
+
gap: 9,
|
|
164
|
+
padding: compact ? 10 : 12,
|
|
165
|
+
borderWidth: 1,
|
|
166
|
+
borderColor: theme.colors.statusWarning,
|
|
167
|
+
borderRadius: 10,
|
|
168
|
+
backgroundColor: theme.colors.surface1,
|
|
169
|
+
},
|
|
170
|
+
warning: { color: theme.colors.statusWarning, fontSize: 13, lineHeight: 18 },
|
|
171
|
+
settings: { gap: 7 },
|
|
172
|
+
setting: { gap: 3, paddingTop: 7, borderTopWidth: 1, borderTopColor: theme.colors.border },
|
|
173
|
+
settingName: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" },
|
|
174
|
+
settingDescription: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
|
|
175
|
+
}),
|
|
176
|
+
[compact, theme],
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function ActionButton({
|
|
181
|
+
label,
|
|
182
|
+
disabled,
|
|
183
|
+
primary,
|
|
184
|
+
danger,
|
|
185
|
+
styles,
|
|
186
|
+
onPress,
|
|
187
|
+
}: {
|
|
188
|
+
label: string;
|
|
189
|
+
disabled: boolean;
|
|
190
|
+
primary?: boolean;
|
|
191
|
+
danger?: boolean;
|
|
192
|
+
styles: PluginManagerStyles;
|
|
193
|
+
onPress(): void;
|
|
194
|
+
}) {
|
|
195
|
+
return (
|
|
196
|
+
<Pressable
|
|
197
|
+
accessibilityRole="button"
|
|
198
|
+
accessibilityState={{ disabled }}
|
|
199
|
+
disabled={disabled}
|
|
200
|
+
onPress={onPress}
|
|
201
|
+
style={[
|
|
202
|
+
styles.button,
|
|
203
|
+
primary ? styles.buttonPrimary : null,
|
|
204
|
+
danger ? styles.buttonDanger : null,
|
|
205
|
+
disabled ? styles.buttonDisabled : null,
|
|
206
|
+
]}
|
|
207
|
+
>
|
|
208
|
+
<Text style={[styles.buttonText, primary ? styles.buttonTextPrimary : null]}>{label}</Text>
|
|
209
|
+
</Pressable>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function MetadataRow({
|
|
214
|
+
label,
|
|
215
|
+
value,
|
|
216
|
+
styles,
|
|
217
|
+
}: {
|
|
218
|
+
label: string;
|
|
219
|
+
value: string;
|
|
220
|
+
styles: PluginManagerStyles;
|
|
221
|
+
}) {
|
|
222
|
+
return (
|
|
223
|
+
<View style={styles.metadataRow}>
|
|
224
|
+
<Text style={styles.metadataLabel}>{label}</Text>
|
|
225
|
+
<Text selectable style={styles.metadataValue}>
|
|
226
|
+
{value}
|
|
227
|
+
</Text>
|
|
228
|
+
</View>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function confirmationFor(input: OmpPluginMutation): PendingConfirmation {
|
|
233
|
+
const target = input.action === "install" ? input.source : input.plugin;
|
|
234
|
+
switch (input.action) {
|
|
235
|
+
case "install":
|
|
236
|
+
return {
|
|
237
|
+
kind: "plugin",
|
|
238
|
+
input,
|
|
239
|
+
title: `Install ${target}?`,
|
|
240
|
+
warning:
|
|
241
|
+
"OMP plugins are trusted code. Installing can fetch code and change persistent state; runtime extensions load in a future OMP session.",
|
|
242
|
+
};
|
|
243
|
+
case "enable":
|
|
244
|
+
return {
|
|
245
|
+
kind: "plugin",
|
|
246
|
+
input,
|
|
247
|
+
title: `Enable ${target}?`,
|
|
248
|
+
warning:
|
|
249
|
+
"Enabling changes persistent state and permits this plugin's trusted code to load in future OMP sessions.",
|
|
250
|
+
};
|
|
251
|
+
case "disable":
|
|
252
|
+
return {
|
|
253
|
+
kind: "plugin",
|
|
254
|
+
input,
|
|
255
|
+
title: `Disable ${target}?`,
|
|
256
|
+
warning:
|
|
257
|
+
"Disabling changes persistent state. OMP sessions that are already running are not unloaded.",
|
|
258
|
+
};
|
|
259
|
+
case "uninstall":
|
|
260
|
+
return {
|
|
261
|
+
kind: "plugin",
|
|
262
|
+
input,
|
|
263
|
+
title: `Uninstall ${target}?`,
|
|
264
|
+
warning:
|
|
265
|
+
"Uninstalling removes the selected plugin registration and cached installation. Running OMP sessions are unchanged.",
|
|
266
|
+
};
|
|
267
|
+
case "upgrade":
|
|
268
|
+
return {
|
|
269
|
+
kind: "plugin",
|
|
270
|
+
input,
|
|
271
|
+
title: `Upgrade ${target}?`,
|
|
272
|
+
warning:
|
|
273
|
+
"Upgrading fetches and replaces trusted plugin code. The updated runtime loads in a future OMP session.",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function configConfirmationFor(
|
|
279
|
+
input: OmpPluginConfigMutation,
|
|
280
|
+
setting: OmpPluginConfigSetting,
|
|
281
|
+
): PendingConfirmation {
|
|
282
|
+
if (input.action === "delete") {
|
|
283
|
+
return {
|
|
284
|
+
kind: "config",
|
|
285
|
+
input,
|
|
286
|
+
title: `Delete ${setting.key}?`,
|
|
287
|
+
warning:
|
|
288
|
+
"This changes persistent OMP plugin configuration. Future sessions will use the setting's default or environment fallback.",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
kind: "config",
|
|
293
|
+
input,
|
|
294
|
+
title: `Set ${setting.key}?`,
|
|
295
|
+
warning: setting.secret
|
|
296
|
+
? "This writes a secret value to OMP's persistent plugin configuration. The value will remain write-only in Paseo."
|
|
297
|
+
: "This changes persistent OMP plugin configuration for future sessions.",
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function ConfigSettingEditor({
|
|
302
|
+
plugin,
|
|
303
|
+
setting,
|
|
304
|
+
busy,
|
|
305
|
+
theme,
|
|
306
|
+
styles,
|
|
307
|
+
onConfirm,
|
|
308
|
+
}: {
|
|
309
|
+
plugin: string;
|
|
310
|
+
setting: OmpPluginConfigSetting;
|
|
311
|
+
busy: boolean;
|
|
312
|
+
theme: PluginSurfaceProps["theme"];
|
|
313
|
+
styles: PluginManagerStyles;
|
|
314
|
+
onConfirm(input: OmpPluginConfigMutation, setting: OmpPluginConfigSetting): void;
|
|
315
|
+
}) {
|
|
316
|
+
const [draft, setDraft] = useState<string | boolean>(setting.type === "boolean" ? false : "");
|
|
317
|
+
let value: string | number | boolean | undefined;
|
|
318
|
+
let validationMessage: string | null = null;
|
|
319
|
+
if (!setting.secret) {
|
|
320
|
+
if (setting.type === "boolean") {
|
|
321
|
+
value = draft === true;
|
|
322
|
+
} else if (setting.type === "number") {
|
|
323
|
+
const raw = typeof draft === "string" ? draft.trim() : "";
|
|
324
|
+
const parsed = raw ? Number(raw) : Number.NaN;
|
|
325
|
+
if (!Number.isFinite(parsed)) validationMessage = "Enter a finite number.";
|
|
326
|
+
else if (setting.minimum !== undefined && parsed < setting.minimum) {
|
|
327
|
+
validationMessage = `Minimum: ${setting.minimum}`;
|
|
328
|
+
} else if (setting.maximum !== undefined && parsed > setting.maximum) {
|
|
329
|
+
validationMessage = `Maximum: ${setting.maximum}`;
|
|
330
|
+
} else value = parsed;
|
|
331
|
+
} else if (setting.type === "enum") {
|
|
332
|
+
if (typeof draft !== "string" || !setting.enumValues.includes(draft)) {
|
|
333
|
+
validationMessage = "Choose one of the documented values.";
|
|
334
|
+
} else value = draft;
|
|
335
|
+
} else if (typeof draft === "string") {
|
|
336
|
+
value = draft;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const candidate =
|
|
340
|
+
value === undefined
|
|
341
|
+
? null
|
|
342
|
+
: OmpPluginConfigMutationSchema.safeParse({
|
|
343
|
+
action: "set",
|
|
344
|
+
plugin,
|
|
345
|
+
key: setting.key,
|
|
346
|
+
value,
|
|
347
|
+
});
|
|
348
|
+
if (candidate && !candidate.success) validationMessage = "Enter a valid bounded value.";
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<View style={styles.setting}>
|
|
352
|
+
<Text selectable style={styles.settingName}>
|
|
353
|
+
{setting.key} · {setting.type}
|
|
354
|
+
</Text>
|
|
355
|
+
<Text style={setting.secret ? styles.warning : styles.muted}>
|
|
356
|
+
{setting.secret
|
|
357
|
+
? setting.configured
|
|
358
|
+
? "Secret · configured"
|
|
359
|
+
: "Secret · not configured"
|
|
360
|
+
: setting.configured
|
|
361
|
+
? "Configured"
|
|
362
|
+
: "Not configured"}
|
|
363
|
+
</Text>
|
|
364
|
+
{setting.description ? (
|
|
365
|
+
<Text style={styles.settingDescription}>{setting.description}</Text>
|
|
366
|
+
) : null}
|
|
367
|
+
{setting.secret ? (
|
|
368
|
+
<Text style={styles.warning}>
|
|
369
|
+
Secret writes are unavailable because OMP accepts plugin setting values through process
|
|
370
|
+
arguments. Configure this secret directly with OMP.
|
|
371
|
+
</Text>
|
|
372
|
+
) : setting.type === "boolean" ? (
|
|
373
|
+
<View style={styles.metadataRow}>
|
|
374
|
+
<Switch
|
|
375
|
+
accessibilityLabel={`New value for ${setting.key}`}
|
|
376
|
+
disabled={busy}
|
|
377
|
+
value={draft === true}
|
|
378
|
+
onValueChange={setDraft}
|
|
379
|
+
/>
|
|
380
|
+
<Text style={styles.metadataValue}>{draft === true ? "True" : "False"}</Text>
|
|
381
|
+
</View>
|
|
382
|
+
) : setting.type === "enum" ? (
|
|
383
|
+
<View accessibilityRole="radiogroup" style={styles.scopeRow}>
|
|
384
|
+
{setting.enumValues.map((option) => {
|
|
385
|
+
const selected = draft === option;
|
|
386
|
+
return (
|
|
387
|
+
<Pressable
|
|
388
|
+
key={option}
|
|
389
|
+
accessibilityRole="radio"
|
|
390
|
+
accessibilityState={{ checked: selected, disabled: busy }}
|
|
391
|
+
disabled={busy}
|
|
392
|
+
onPress={() => setDraft(option)}
|
|
393
|
+
style={[styles.scopeButton, selected ? styles.scopeButtonActive : null]}
|
|
394
|
+
>
|
|
395
|
+
<Text style={[styles.scopeText, selected ? styles.scopeTextActive : null]}>
|
|
396
|
+
{option}
|
|
397
|
+
</Text>
|
|
398
|
+
</Pressable>
|
|
399
|
+
);
|
|
400
|
+
})}
|
|
401
|
+
</View>
|
|
402
|
+
) : (
|
|
403
|
+
<TextInput
|
|
404
|
+
accessibilityLabel={`New value for ${setting.key}`}
|
|
405
|
+
editable={!busy}
|
|
406
|
+
value={typeof draft === "string" ? draft : ""}
|
|
407
|
+
onChangeText={setDraft}
|
|
408
|
+
placeholder="Enter a new value"
|
|
409
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
410
|
+
keyboardType={setting.type === "number" ? "numeric" : "default"}
|
|
411
|
+
autoCapitalize="none"
|
|
412
|
+
autoCorrect={false}
|
|
413
|
+
style={styles.input}
|
|
414
|
+
/>
|
|
415
|
+
)}
|
|
416
|
+
{validationMessage && (setting.type !== "boolean" || value === undefined) ? (
|
|
417
|
+
<Text style={styles.error}>{validationMessage}</Text>
|
|
418
|
+
) : 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>
|
|
441
|
+
</View>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function PluginCard({
|
|
446
|
+
plugin,
|
|
447
|
+
busy,
|
|
448
|
+
inspecting,
|
|
449
|
+
styles,
|
|
450
|
+
onConfirm,
|
|
451
|
+
onInspect,
|
|
452
|
+
}: {
|
|
453
|
+
plugin: OmpInstalledPlugin;
|
|
454
|
+
busy: boolean;
|
|
455
|
+
inspecting: boolean;
|
|
456
|
+
styles: PluginManagerStyles;
|
|
457
|
+
onConfirm(input: OmpPluginMutation): void;
|
|
458
|
+
onInspect(plugin: string): void;
|
|
459
|
+
}) {
|
|
460
|
+
const scope = plugin.scope === "user" ? "user" : undefined;
|
|
461
|
+
return (
|
|
462
|
+
<View style={styles.pluginCard}>
|
|
463
|
+
<View style={styles.cardHeader}>
|
|
464
|
+
<Text selectable style={styles.cardTitle}>
|
|
465
|
+
{plugin.id}
|
|
466
|
+
</Text>
|
|
467
|
+
<Text style={plugin.enabled ? styles.success : styles.muted}>
|
|
468
|
+
{plugin.enabled ? "Enabled" : "Disabled"}
|
|
469
|
+
</Text>
|
|
470
|
+
</View>
|
|
471
|
+
<View style={styles.metadata}>
|
|
472
|
+
<MetadataRow
|
|
473
|
+
styles={styles}
|
|
474
|
+
label="Source"
|
|
475
|
+
value={plugin.source === "marketplace" ? `Marketplace · ${plugin.scope}` : "npm / linked"}
|
|
476
|
+
/>
|
|
477
|
+
<MetadataRow styles={styles} label="Version" value={plugin.version ?? "Unknown"} />
|
|
478
|
+
{plugin.path ? <MetadataRow styles={styles} label="Path" value={plugin.path} /> : null}
|
|
479
|
+
{plugin.description ? (
|
|
480
|
+
<MetadataRow styles={styles} label="Description" value={plugin.description} />
|
|
481
|
+
) : null}
|
|
482
|
+
{plugin.enabledFeatures.length > 0 ? (
|
|
483
|
+
<MetadataRow styles={styles} label="Features" value={plugin.enabledFeatures.join(", ")} />
|
|
484
|
+
) : null}
|
|
485
|
+
{plugin.usesDefaultFeatures ? (
|
|
486
|
+
<MetadataRow styles={styles} label="Features" value="Manifest defaults" />
|
|
487
|
+
) : null}
|
|
488
|
+
{plugin.shadowed ? (
|
|
489
|
+
<Text style={styles.warning}>Shadowed by an enabled project-scoped installation.</Text>
|
|
490
|
+
) : null}
|
|
491
|
+
{plugin.scope === "project" ? (
|
|
492
|
+
<Text style={styles.warning}>
|
|
493
|
+
Project-scoped actions require OMP to run from that project and are read-only here.
|
|
494
|
+
</Text>
|
|
495
|
+
) : null}
|
|
496
|
+
{plugin.ambiguous ? (
|
|
497
|
+
<Text style={styles.warning}>
|
|
498
|
+
Multiple installations share this package identity, so lifecycle actions are read-only.
|
|
499
|
+
</Text>
|
|
500
|
+
) : null}
|
|
501
|
+
</View>
|
|
502
|
+
<View style={styles.actions}>
|
|
503
|
+
<ActionButton
|
|
504
|
+
label={plugin.enabled ? "Disable" : "Enable"}
|
|
505
|
+
disabled={busy || plugin.scope === "project" || plugin.ambiguous}
|
|
506
|
+
styles={styles}
|
|
507
|
+
onPress={() =>
|
|
508
|
+
onConfirm({
|
|
509
|
+
action: plugin.enabled ? "disable" : "enable",
|
|
510
|
+
plugin: plugin.id,
|
|
511
|
+
...(scope ? { scope } : {}),
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
/>
|
|
515
|
+
{plugin.configurable && plugin.packageName ? (
|
|
516
|
+
<ActionButton
|
|
517
|
+
label={inspecting ? "Loading settings…" : "Configure settings"}
|
|
518
|
+
disabled={busy || inspecting || plugin.scope === "project" || plugin.ambiguous}
|
|
519
|
+
styles={styles}
|
|
520
|
+
onPress={() => onInspect(plugin.packageName ?? plugin.id)}
|
|
521
|
+
/>
|
|
522
|
+
) : null}
|
|
523
|
+
{plugin.source === "marketplace" ? (
|
|
524
|
+
<ActionButton
|
|
525
|
+
label="Upgrade"
|
|
526
|
+
disabled={busy || plugin.scope === "project" || plugin.ambiguous}
|
|
527
|
+
styles={styles}
|
|
528
|
+
onPress={() =>
|
|
529
|
+
onConfirm({ action: "upgrade", plugin: plugin.id, ...(scope ? { scope } : {}) })
|
|
530
|
+
}
|
|
531
|
+
/>
|
|
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>
|
|
543
|
+
</View>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function OmpPluginManagerSection({
|
|
548
|
+
theme,
|
|
549
|
+
compact,
|
|
550
|
+
}: {
|
|
551
|
+
theme: PluginSurfaceProps["theme"];
|
|
552
|
+
compact: boolean;
|
|
553
|
+
}) {
|
|
554
|
+
const loadPlugins = useRpc(listOmpPlugins);
|
|
555
|
+
const inspectPluginConfig = useRpc(inspectOmpPluginConfig);
|
|
556
|
+
const mutatePlugin = useRpc(mutateOmpPlugin);
|
|
557
|
+
const mutatePluginConfig = useRpc(mutateOmpPluginConfig);
|
|
558
|
+
const queryClient = useQueryClient();
|
|
559
|
+
const styles = usePluginManagerStyles(theme, compact);
|
|
560
|
+
const [source, setSource] = useState("");
|
|
561
|
+
const [confirmation, setConfirmation] = useState<PendingConfirmation | null>(null);
|
|
562
|
+
const [notice, setNotice] = useState<{ tone: "success" | "error"; text: string } | null>(null);
|
|
563
|
+
const [inspected, setInspected] = useState<OmpPluginConfigState | null>(null);
|
|
564
|
+
const [configEditGenerations, setConfigEditGenerations] = useState<Record<string, number>>({});
|
|
565
|
+
const inspectionGeneration = useRef(0);
|
|
566
|
+
const plugins = useQuery({
|
|
567
|
+
queryKey: PLUGINS_QUERY_KEY,
|
|
568
|
+
queryFn: () => loadPlugins({}),
|
|
569
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
570
|
+
});
|
|
571
|
+
const mutation = useMutation({
|
|
572
|
+
mutationFn: (input: OmpPluginMutation) => mutatePlugin(input),
|
|
573
|
+
onSuccess: (result) => {
|
|
574
|
+
queryClient.setQueryData(PLUGINS_QUERY_KEY, result.state);
|
|
575
|
+
setNotice({ tone: result.ok ? "success" : "error", text: result.message });
|
|
576
|
+
if (result.ok && confirmation?.kind === "plugin" && confirmation.input.action === "install") {
|
|
577
|
+
setSource("");
|
|
578
|
+
}
|
|
579
|
+
setConfirmation(null);
|
|
580
|
+
},
|
|
581
|
+
onError: () => {
|
|
582
|
+
setNotice({ tone: "error", text: "The plugin operation could not be completed." });
|
|
583
|
+
setConfirmation(null);
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
const configInspection = useMutation({
|
|
587
|
+
mutationFn: ({ plugin }: { plugin: string; generation: number }) =>
|
|
588
|
+
inspectPluginConfig({ plugin }),
|
|
589
|
+
onSuccess: (result, request) => {
|
|
590
|
+
if (inspectionGeneration.current === request.generation) setInspected(result);
|
|
591
|
+
},
|
|
592
|
+
onError: (_error, request) => {
|
|
593
|
+
if (inspectionGeneration.current === request.generation) setInspected(null);
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
const configMutation = useMutation({
|
|
597
|
+
mutationFn: (input: OmpPluginConfigMutation) => mutatePluginConfig(input),
|
|
598
|
+
onSuccess: (result, input) => {
|
|
599
|
+
if (result.config.available) setInspected(result.config);
|
|
600
|
+
if (result.ok) {
|
|
601
|
+
const identity = `${input.plugin}:${input.key}`;
|
|
602
|
+
setConfigEditGenerations((current) => ({
|
|
603
|
+
...current,
|
|
604
|
+
[identity]: (current[identity] ?? 0) + 1,
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
setNotice({ tone: result.ok ? "success" : "error", text: result.message });
|
|
608
|
+
setConfirmation(null);
|
|
609
|
+
},
|
|
610
|
+
onError: () => {
|
|
611
|
+
setNotice({ tone: "error", text: "The plugin setting operation could not be completed." });
|
|
612
|
+
setConfirmation(null);
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
const busy = mutation.isPending || configMutation.isPending;
|
|
616
|
+
|
|
617
|
+
const requestInstall = () => {
|
|
618
|
+
const parsed = OmpPluginInstallSourceSchema.safeParse(source);
|
|
619
|
+
if (!parsed.success) {
|
|
620
|
+
setNotice({
|
|
621
|
+
tone: "error",
|
|
622
|
+
text: "Enter one valid package, marketplace ID, Git source, or local path without options or control characters.",
|
|
623
|
+
});
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
setNotice(null);
|
|
627
|
+
setConfirmation(confirmationFor({ action: "install", source: parsed.data, scope: "user" }));
|
|
628
|
+
};
|
|
629
|
+
const requestMutation = (input: OmpPluginMutation) => {
|
|
630
|
+
setNotice(null);
|
|
631
|
+
setConfirmation(confirmationFor(input));
|
|
632
|
+
};
|
|
633
|
+
const requestConfigMutation = (
|
|
634
|
+
input: OmpPluginConfigMutation,
|
|
635
|
+
setting: OmpPluginConfigSetting,
|
|
636
|
+
) => {
|
|
637
|
+
setNotice(null);
|
|
638
|
+
setConfirmation(configConfirmationFor(input, setting));
|
|
639
|
+
};
|
|
640
|
+
const inspect = (plugin: string) => {
|
|
641
|
+
inspectionGeneration.current += 1;
|
|
642
|
+
if (inspected?.plugin === plugin) {
|
|
643
|
+
setInspected(null);
|
|
644
|
+
configInspection.reset();
|
|
645
|
+
setConfirmation(null);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const generation = inspectionGeneration.current;
|
|
649
|
+
setInspected(null);
|
|
650
|
+
setConfirmation(null);
|
|
651
|
+
configInspection.mutate({ plugin, generation });
|
|
652
|
+
};
|
|
653
|
+
const applyConfirmation = () => {
|
|
654
|
+
if (!confirmation) return;
|
|
655
|
+
if (confirmation.kind === "plugin") mutation.mutate(confirmation.input);
|
|
656
|
+
else configMutation.mutate(confirmation.input);
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
return (
|
|
660
|
+
<View style={styles.root}>
|
|
661
|
+
<View style={styles.header}>
|
|
662
|
+
<View>
|
|
663
|
+
<Text style={styles.title}>OMP plugins</Text>
|
|
664
|
+
<Text style={styles.muted}>Manage OMP's native plugins, not Paseo plugins.</Text>
|
|
665
|
+
</View>
|
|
666
|
+
<ActionButton
|
|
667
|
+
label={plugins.isFetching ? "Refreshing…" : "Refresh"}
|
|
668
|
+
disabled={plugins.isFetching || busy}
|
|
669
|
+
styles={styles}
|
|
670
|
+
onPress={() => {
|
|
671
|
+
void plugins.refetch();
|
|
672
|
+
}}
|
|
673
|
+
/>
|
|
674
|
+
</View>
|
|
675
|
+
|
|
676
|
+
<View style={styles.card}>
|
|
677
|
+
<Text style={styles.cardTitle}>Install a plugin</Text>
|
|
678
|
+
<Text style={styles.muted}>
|
|
679
|
+
Enter one source accepted by OMP, such as a package, name@marketplace, Git URL, or local
|
|
680
|
+
path.
|
|
681
|
+
</Text>
|
|
682
|
+
<TextInput
|
|
683
|
+
accessibilityLabel="OMP plugin source"
|
|
684
|
+
editable={!busy}
|
|
685
|
+
value={source}
|
|
686
|
+
onChangeText={setSource}
|
|
687
|
+
placeholder="name@marketplace or package source"
|
|
688
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
689
|
+
autoCapitalize="none"
|
|
690
|
+
autoCorrect={false}
|
|
691
|
+
style={styles.input}
|
|
692
|
+
/>
|
|
693
|
+
<View style={styles.actions}>
|
|
694
|
+
<ActionButton
|
|
695
|
+
label="Review install"
|
|
696
|
+
disabled={busy || source.length === 0}
|
|
697
|
+
primary
|
|
698
|
+
styles={styles}
|
|
699
|
+
onPress={requestInstall}
|
|
700
|
+
/>
|
|
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>
|
|
708
|
+
<View style={styles.actions}>
|
|
709
|
+
<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}
|
|
718
|
+
primary
|
|
719
|
+
danger={
|
|
720
|
+
confirmation.kind === "plugin"
|
|
721
|
+
? confirmation.input.action === "uninstall"
|
|
722
|
+
: confirmation.input.action === "delete"
|
|
723
|
+
}
|
|
724
|
+
styles={styles}
|
|
725
|
+
onPress={applyConfirmation}
|
|
726
|
+
/>
|
|
727
|
+
</View>
|
|
728
|
+
</View>
|
|
729
|
+
) : null}
|
|
730
|
+
|
|
731
|
+
{notice ? (
|
|
732
|
+
<Text
|
|
733
|
+
accessibilityRole="alert"
|
|
734
|
+
style={notice.tone === "success" ? styles.success : styles.error}
|
|
735
|
+
>
|
|
736
|
+
{notice.text}
|
|
737
|
+
</Text>
|
|
738
|
+
) : null}
|
|
739
|
+
{plugins.isLoading ? <Text style={styles.muted}>Loading OMP plugins…</Text> : null}
|
|
740
|
+
{plugins.error ? (
|
|
741
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
742
|
+
Could not load OMP plugin state.
|
|
743
|
+
</Text>
|
|
744
|
+
) : null}
|
|
745
|
+
{plugins.data?.error ? (
|
|
746
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
747
|
+
{plugins.data.error}
|
|
748
|
+
</Text>
|
|
749
|
+
) : null}
|
|
750
|
+
{plugins.data?.available && plugins.data.plugins.length === 0 ? (
|
|
751
|
+
<View style={styles.card}>
|
|
752
|
+
<Text style={styles.cardTitle}>No plugins installed</Text>
|
|
753
|
+
<Text style={styles.muted}>
|
|
754
|
+
OMP reported an empty npm and marketplace plugin catalog.
|
|
755
|
+
</Text>
|
|
756
|
+
</View>
|
|
757
|
+
) : null}
|
|
758
|
+
{plugins.data?.droppedCount ? (
|
|
759
|
+
<Text style={styles.warning}>
|
|
760
|
+
{plugins.data.droppedCount} invalid or excess plugin records were omitted.
|
|
761
|
+
</Text>
|
|
762
|
+
) : null}
|
|
763
|
+
|
|
764
|
+
{plugins.data?.plugins.length ? (
|
|
765
|
+
<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
|
+
))}
|
|
780
|
+
</View>
|
|
781
|
+
) : null}
|
|
782
|
+
|
|
783
|
+
{configInspection.error || configMutation.error ? (
|
|
784
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
785
|
+
Could not update OMP plugin settings.
|
|
786
|
+
</Text>
|
|
787
|
+
) : null}
|
|
788
|
+
{inspected ? (
|
|
789
|
+
<View style={styles.card}>
|
|
790
|
+
<View style={styles.cardHeader}>
|
|
791
|
+
<Text style={styles.cardTitle}>Settings · {inspected.plugin}</Text>
|
|
792
|
+
<ActionButton
|
|
793
|
+
label="Close"
|
|
794
|
+
disabled={busy}
|
|
795
|
+
styles={styles}
|
|
796
|
+
onPress={() => {
|
|
797
|
+
inspectionGeneration.current += 1;
|
|
798
|
+
setInspected(null);
|
|
799
|
+
setConfirmation(null);
|
|
800
|
+
}}
|
|
801
|
+
/>
|
|
802
|
+
</View>
|
|
803
|
+
<Text style={styles.muted}>
|
|
804
|
+
Current values and defaults are withheld. New values are write-only and are never
|
|
805
|
+
returned to the app.
|
|
806
|
+
</Text>
|
|
807
|
+
{inspected.error ? <Text style={styles.error}>{inspected.error}</Text> : null}
|
|
808
|
+
{inspected.droppedCount > 0 ? (
|
|
809
|
+
<Text style={styles.warning}>
|
|
810
|
+
{inspected.droppedCount} invalid or excess setting definitions were omitted.
|
|
811
|
+
</Text>
|
|
812
|
+
) : null}
|
|
813
|
+
{inspected.available && inspected.settings.length === 0 ? (
|
|
814
|
+
<Text style={styles.muted}>This plugin declares no settings.</Text>
|
|
815
|
+
) : null}
|
|
816
|
+
<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
|
+
))}
|
|
828
|
+
</View>
|
|
829
|
+
</View>
|
|
830
|
+
) : null}
|
|
831
|
+
</View>
|
|
832
|
+
);
|
|
833
|
+
}
|