@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,1274 @@
|
|
|
1
|
+
import { type PluginSurfaceProps, usePaseo, useRpc } from "@getpaseo/plugin/client";
|
|
2
|
+
import { Icon, TextInput } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { useCallback, useMemo, useState } from "react";
|
|
6
|
+
import type { TextStyle, ViewStyle } from "react-native";
|
|
7
|
+
import { Linking, Pressable, ScrollView, Switch, Text, View } from "react-native";
|
|
8
|
+
import { listOmpConfig, type OmpConfig } from "../shared/omp-config";
|
|
9
|
+
import {
|
|
10
|
+
categorizeOmpSetting,
|
|
11
|
+
formatOmpSettingLabel,
|
|
12
|
+
listOmpSettings,
|
|
13
|
+
OMP_SETTING_CATEGORIES,
|
|
14
|
+
type OmpScalarValue,
|
|
15
|
+
type OmpSetting,
|
|
16
|
+
type OmpSettingCategory,
|
|
17
|
+
updateOmpSettings,
|
|
18
|
+
} from "../shared/omp-settings";
|
|
19
|
+
import { getOmpProviderHealth, type OmpProviderHealth } from "../shared/provider-diagnostics";
|
|
20
|
+
import {
|
|
21
|
+
documentationForSettingCategory,
|
|
22
|
+
documentationForSettingPath,
|
|
23
|
+
OMP_SETTINGS_GUIDES,
|
|
24
|
+
OMP_SETTINGS_REFERENCE,
|
|
25
|
+
type OmpDocumentationLink,
|
|
26
|
+
} from "./omp-doc-links";
|
|
27
|
+
import { OmpPluginManagerSection } from "./omp-plugin-manager";
|
|
28
|
+
import {
|
|
29
|
+
type BinaryHealthSummary,
|
|
30
|
+
loadReadyProviderSnapshot,
|
|
31
|
+
lspTone,
|
|
32
|
+
mcpTone,
|
|
33
|
+
type PathStateSummary,
|
|
34
|
+
processTone,
|
|
35
|
+
refreshProviderDiagnostics,
|
|
36
|
+
rpcUiTone,
|
|
37
|
+
selectKnownOmpProviders,
|
|
38
|
+
summarizeBinaryHealth,
|
|
39
|
+
summarizeLspSupport,
|
|
40
|
+
summarizeMcpDiagnostics,
|
|
41
|
+
summarizeMemoryBackend,
|
|
42
|
+
summarizePathState,
|
|
43
|
+
summarizeProcessDiagnostics,
|
|
44
|
+
summarizeProviderStatus,
|
|
45
|
+
summarizeRpcUiSupport,
|
|
46
|
+
} from "./provider-diagnostics-state";
|
|
47
|
+
|
|
48
|
+
const SETTINGS_QUERY_KEY = ["paseo-omp", "settings"] as const;
|
|
49
|
+
const CONFIG_POLL_MS = 30_000;
|
|
50
|
+
const HEALTH_QUERY_KEY = ["paseo-omp", "provider-health"] as const;
|
|
51
|
+
const PROVIDERS_QUERY_KEY = ["paseo-omp", "provider-snapshot"] as const;
|
|
52
|
+
|
|
53
|
+
export interface OmpConfigStyles {
|
|
54
|
+
root: ViewStyle;
|
|
55
|
+
pageTitle: TextStyle;
|
|
56
|
+
topTabs: ViewStyle;
|
|
57
|
+
topTab: ViewStyle;
|
|
58
|
+
topTabActive: ViewStyle;
|
|
59
|
+
topTabLabel: TextStyle;
|
|
60
|
+
topTabLabelActive: TextStyle;
|
|
61
|
+
workspace: ViewStyle;
|
|
62
|
+
categoryRail: ViewStyle;
|
|
63
|
+
categoryList: ViewStyle;
|
|
64
|
+
categoryButton: ViewStyle;
|
|
65
|
+
categoryButtonActive: ViewStyle;
|
|
66
|
+
categoryLabel: TextStyle;
|
|
67
|
+
categoryLabelActive: TextStyle;
|
|
68
|
+
categoryContent: ViewStyle;
|
|
69
|
+
search: TextStyle;
|
|
70
|
+
sectionHeader: ViewStyle;
|
|
71
|
+
sectionHeaderRow: ViewStyle;
|
|
72
|
+
sectionTitle: TextStyle;
|
|
73
|
+
source: TextStyle;
|
|
74
|
+
muted: TextStyle;
|
|
75
|
+
error: TextStyle;
|
|
76
|
+
refresh: ViewStyle;
|
|
77
|
+
docsActions: ViewStyle;
|
|
78
|
+
docLink: ViewStyle;
|
|
79
|
+
docLinkPressed: ViewStyle;
|
|
80
|
+
docLinkText: TextStyle;
|
|
81
|
+
refreshLabel: TextStyle;
|
|
82
|
+
card: ViewStyle;
|
|
83
|
+
cardHeader: ViewStyle;
|
|
84
|
+
cardTitle: TextStyle;
|
|
85
|
+
row: ViewStyle;
|
|
86
|
+
rowLabel: TextStyle;
|
|
87
|
+
rowValue: TextStyle;
|
|
88
|
+
setting: ViewStyle;
|
|
89
|
+
settingHeader: ViewStyle;
|
|
90
|
+
settingPath: TextStyle;
|
|
91
|
+
settingDescription: TextStyle;
|
|
92
|
+
settingValue: TextStyle;
|
|
93
|
+
collectionSummary: TextStyle;
|
|
94
|
+
chipList: ViewStyle;
|
|
95
|
+
chip: ViewStyle;
|
|
96
|
+
chipText: TextStyle;
|
|
97
|
+
recordList: ViewStyle;
|
|
98
|
+
recordRow: ViewStyle;
|
|
99
|
+
recordKey: TextStyle;
|
|
100
|
+
recordValue: TextStyle;
|
|
101
|
+
editorActions: ViewStyle;
|
|
102
|
+
editorAction: ViewStyle;
|
|
103
|
+
editorActionPrimary: ViewStyle;
|
|
104
|
+
editorActionText: TextStyle;
|
|
105
|
+
editorActionTextPrimary: TextStyle;
|
|
106
|
+
scalarInput: TextStyle;
|
|
107
|
+
resetAction: TextStyle;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function useConfigStyles(theme: PluginSurfaceProps["theme"], compact: boolean): OmpConfigStyles {
|
|
111
|
+
return useMemo(
|
|
112
|
+
() => ({
|
|
113
|
+
root: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
gap: compact ? 10 : 14,
|
|
116
|
+
padding: compact ? 16 : 24,
|
|
117
|
+
backgroundColor: theme.colors.surface0,
|
|
118
|
+
},
|
|
119
|
+
pageTitle: {
|
|
120
|
+
color: theme.colors.foreground,
|
|
121
|
+
fontSize: compact ? 22 : 26,
|
|
122
|
+
fontWeight: "700",
|
|
123
|
+
},
|
|
124
|
+
topTabs: {
|
|
125
|
+
flexWrap: "wrap",
|
|
126
|
+
flexDirection: "row",
|
|
127
|
+
alignSelf: "flex-start",
|
|
128
|
+
gap: 4,
|
|
129
|
+
padding: 4,
|
|
130
|
+
borderWidth: 1,
|
|
131
|
+
borderColor: theme.colors.border,
|
|
132
|
+
borderRadius: 10,
|
|
133
|
+
backgroundColor: theme.colors.surface1,
|
|
134
|
+
},
|
|
135
|
+
topTab: {
|
|
136
|
+
paddingHorizontal: compact ? 10 : 14,
|
|
137
|
+
paddingVertical: 8,
|
|
138
|
+
borderRadius: 7,
|
|
139
|
+
},
|
|
140
|
+
topTabActive: { backgroundColor: theme.colors.accent },
|
|
141
|
+
topTabLabel: { color: theme.colors.foregroundMuted, fontSize: 13, fontWeight: "600" },
|
|
142
|
+
topTabLabelActive: { color: theme.colors.accentForeground },
|
|
143
|
+
workspace: {
|
|
144
|
+
flexDirection: compact ? "column" : "row",
|
|
145
|
+
alignItems: compact ? "stretch" : "flex-start",
|
|
146
|
+
gap: compact ? 10 : 18,
|
|
147
|
+
},
|
|
148
|
+
categoryRail: {
|
|
149
|
+
width: compact ? "100%" : 220,
|
|
150
|
+
gap: 10,
|
|
151
|
+
padding: 10,
|
|
152
|
+
borderWidth: 1,
|
|
153
|
+
borderColor: theme.colors.border,
|
|
154
|
+
borderRadius: 10,
|
|
155
|
+
backgroundColor: theme.colors.surface1,
|
|
156
|
+
},
|
|
157
|
+
categoryList: { gap: 4 },
|
|
158
|
+
categoryButton: { paddingHorizontal: 10, paddingVertical: 9, borderRadius: 7 },
|
|
159
|
+
categoryButtonActive: { backgroundColor: theme.colors.surface2 },
|
|
160
|
+
categoryLabel: { color: theme.colors.foregroundMuted, fontSize: 13, fontWeight: "500" },
|
|
161
|
+
categoryLabelActive: { color: theme.colors.foreground, fontWeight: "700" },
|
|
162
|
+
categoryContent: { flex: compact ? undefined : 1, minWidth: 0, gap: 10 },
|
|
163
|
+
search: {
|
|
164
|
+
color: theme.colors.foreground,
|
|
165
|
+
borderWidth: 1,
|
|
166
|
+
borderColor: theme.colors.border,
|
|
167
|
+
borderRadius: 8,
|
|
168
|
+
backgroundColor: theme.colors.surface0,
|
|
169
|
+
paddingHorizontal: 10,
|
|
170
|
+
paddingVertical: 8,
|
|
171
|
+
fontSize: 13,
|
|
172
|
+
},
|
|
173
|
+
sectionHeader: { gap: 4, marginTop: compact ? 2 : 4 },
|
|
174
|
+
sectionHeaderRow: { flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 10 },
|
|
175
|
+
sectionTitle: {
|
|
176
|
+
color: theme.colors.foreground,
|
|
177
|
+
fontSize: compact ? 16 : 18,
|
|
178
|
+
fontWeight: "600",
|
|
179
|
+
},
|
|
180
|
+
source: { color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
181
|
+
muted: { color: theme.colors.foregroundMuted, fontSize: 13 },
|
|
182
|
+
error: { color: theme.colors.statusDanger, fontSize: 13 },
|
|
183
|
+
docsActions: { flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 6 },
|
|
184
|
+
docLink: {
|
|
185
|
+
paddingHorizontal: 8,
|
|
186
|
+
paddingVertical: 5,
|
|
187
|
+
borderWidth: 1,
|
|
188
|
+
borderColor: theme.colors.border,
|
|
189
|
+
borderRadius: 7,
|
|
190
|
+
backgroundColor: theme.colors.surface1,
|
|
191
|
+
},
|
|
192
|
+
docLinkPressed: { opacity: 0.72 },
|
|
193
|
+
docLinkText: { color: theme.colors.accent, fontSize: 12, fontWeight: "600" },
|
|
194
|
+
refresh: {
|
|
195
|
+
flexDirection: "row",
|
|
196
|
+
alignItems: "center",
|
|
197
|
+
gap: 6,
|
|
198
|
+
paddingHorizontal: 10,
|
|
199
|
+
paddingVertical: 6,
|
|
200
|
+
borderWidth: 1,
|
|
201
|
+
borderColor: theme.colors.border,
|
|
202
|
+
borderRadius: 8,
|
|
203
|
+
backgroundColor: theme.colors.surface1,
|
|
204
|
+
},
|
|
205
|
+
refreshLabel: { color: theme.colors.foreground, fontSize: 13 },
|
|
206
|
+
cardHeader: {
|
|
207
|
+
flexDirection: "row",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
justifyContent: "space-between",
|
|
210
|
+
flexWrap: "wrap",
|
|
211
|
+
gap: 8,
|
|
212
|
+
},
|
|
213
|
+
card: {
|
|
214
|
+
gap: 8,
|
|
215
|
+
padding: compact ? 10 : 12,
|
|
216
|
+
borderWidth: 1,
|
|
217
|
+
borderColor: theme.colors.border,
|
|
218
|
+
borderRadius: 10,
|
|
219
|
+
backgroundColor: theme.colors.surface1,
|
|
220
|
+
},
|
|
221
|
+
cardTitle: { color: theme.colors.foreground, fontSize: 14, fontWeight: "600" },
|
|
222
|
+
row: { flexDirection: "row", gap: 8, flexWrap: "wrap" },
|
|
223
|
+
rowLabel: { color: theme.colors.foregroundMuted, fontSize: 13 },
|
|
224
|
+
rowValue: { color: theme.colors.foreground, fontSize: 13, flexShrink: 1 },
|
|
225
|
+
setting: {
|
|
226
|
+
gap: 5,
|
|
227
|
+
paddingVertical: 10,
|
|
228
|
+
borderTopWidth: 1,
|
|
229
|
+
borderTopColor: theme.colors.border,
|
|
230
|
+
},
|
|
231
|
+
settingHeader: {
|
|
232
|
+
flexDirection: "row",
|
|
233
|
+
alignItems: "baseline",
|
|
234
|
+
justifyContent: "space-between",
|
|
235
|
+
gap: 12,
|
|
236
|
+
},
|
|
237
|
+
settingPath: { color: theme.colors.foregroundMuted, fontSize: 11, flexShrink: 1 },
|
|
238
|
+
settingDescription: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
|
|
239
|
+
settingValue: { color: theme.colors.foreground, fontSize: 13, fontWeight: "600" },
|
|
240
|
+
collectionSummary: { color: theme.colors.foregroundMuted, fontSize: 12 },
|
|
241
|
+
chipList: { flexDirection: "row", flexWrap: "wrap", gap: 6 },
|
|
242
|
+
chip: {
|
|
243
|
+
paddingHorizontal: 8,
|
|
244
|
+
paddingVertical: 5,
|
|
245
|
+
borderWidth: 1,
|
|
246
|
+
borderColor: theme.colors.border,
|
|
247
|
+
borderRadius: 999,
|
|
248
|
+
backgroundColor: theme.colors.surface2,
|
|
249
|
+
},
|
|
250
|
+
chipText: { color: theme.colors.foreground, fontSize: 12 },
|
|
251
|
+
recordList: { gap: 6 },
|
|
252
|
+
recordRow: {
|
|
253
|
+
flexDirection: compact ? "column" : "row",
|
|
254
|
+
alignItems: compact ? "flex-start" : "baseline",
|
|
255
|
+
gap: compact ? 2 : 12,
|
|
256
|
+
paddingVertical: 5,
|
|
257
|
+
},
|
|
258
|
+
recordKey: {
|
|
259
|
+
width: compact ? undefined : 150,
|
|
260
|
+
color: theme.colors.foregroundMuted,
|
|
261
|
+
fontSize: 12,
|
|
262
|
+
fontWeight: "600",
|
|
263
|
+
},
|
|
264
|
+
recordValue: { flex: 1, color: theme.colors.foreground, fontSize: 12 },
|
|
265
|
+
editorActions: {
|
|
266
|
+
flexDirection: "row",
|
|
267
|
+
alignItems: "center",
|
|
268
|
+
justifyContent: "flex-end",
|
|
269
|
+
gap: 8,
|
|
270
|
+
padding: 10,
|
|
271
|
+
borderWidth: 1,
|
|
272
|
+
borderColor: theme.colors.border,
|
|
273
|
+
borderRadius: 10,
|
|
274
|
+
backgroundColor: theme.colors.surface1,
|
|
275
|
+
},
|
|
276
|
+
editorAction: {
|
|
277
|
+
paddingHorizontal: 12,
|
|
278
|
+
paddingVertical: 8,
|
|
279
|
+
borderWidth: 1,
|
|
280
|
+
borderColor: theme.colors.border,
|
|
281
|
+
borderRadius: 8,
|
|
282
|
+
},
|
|
283
|
+
editorActionPrimary: {
|
|
284
|
+
backgroundColor: theme.colors.accent,
|
|
285
|
+
borderColor: theme.colors.accent,
|
|
286
|
+
},
|
|
287
|
+
editorActionText: { color: theme.colors.foreground, fontSize: 13, fontWeight: "600" },
|
|
288
|
+
editorActionTextPrimary: { color: theme.colors.accentForeground },
|
|
289
|
+
scalarInput: {
|
|
290
|
+
minWidth: 180,
|
|
291
|
+
maxWidth: 420,
|
|
292
|
+
color: theme.colors.foreground,
|
|
293
|
+
borderWidth: 1,
|
|
294
|
+
borderColor: theme.colors.border,
|
|
295
|
+
borderRadius: 7,
|
|
296
|
+
backgroundColor: theme.colors.surface0,
|
|
297
|
+
paddingHorizontal: 9,
|
|
298
|
+
paddingVertical: 6,
|
|
299
|
+
fontSize: 13,
|
|
300
|
+
},
|
|
301
|
+
resetAction: { color: theme.colors.accent, fontSize: 12, fontWeight: "600" },
|
|
302
|
+
}),
|
|
303
|
+
[compact, theme],
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function KeyValueRow({
|
|
308
|
+
styles,
|
|
309
|
+
label,
|
|
310
|
+
value,
|
|
311
|
+
valueColor,
|
|
312
|
+
}: {
|
|
313
|
+
styles: OmpConfigStyles;
|
|
314
|
+
label: string;
|
|
315
|
+
value: string;
|
|
316
|
+
valueColor?: string;
|
|
317
|
+
}) {
|
|
318
|
+
return (
|
|
319
|
+
<View style={styles.row}>
|
|
320
|
+
<Text style={styles.rowLabel}>{label}</Text>
|
|
321
|
+
<Text style={[styles.rowValue, valueColor ? { color: valueColor } : null]}>{value}</Text>
|
|
322
|
+
</View>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function SectionCard({
|
|
327
|
+
styles,
|
|
328
|
+
title,
|
|
329
|
+
action,
|
|
330
|
+
children,
|
|
331
|
+
}: {
|
|
332
|
+
styles: OmpConfigStyles;
|
|
333
|
+
title: string;
|
|
334
|
+
action?: ReactNode;
|
|
335
|
+
children: ReactNode;
|
|
336
|
+
}) {
|
|
337
|
+
return (
|
|
338
|
+
<View style={styles.card}>
|
|
339
|
+
<View style={styles.cardHeader}>
|
|
340
|
+
<Text style={styles.cardTitle}>{title}</Text>
|
|
341
|
+
{action}
|
|
342
|
+
</View>
|
|
343
|
+
{children}
|
|
344
|
+
</View>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function DocumentationLink({
|
|
349
|
+
link,
|
|
350
|
+
styles,
|
|
351
|
+
onOpen,
|
|
352
|
+
}: {
|
|
353
|
+
link: OmpDocumentationLink;
|
|
354
|
+
styles: OmpConfigStyles;
|
|
355
|
+
onOpen(link: OmpDocumentationLink): void;
|
|
356
|
+
}) {
|
|
357
|
+
return (
|
|
358
|
+
<Pressable
|
|
359
|
+
accessibilityRole="link"
|
|
360
|
+
accessibilityLabel={link.accessibilityLabel}
|
|
361
|
+
accessibilityHint="Opens in your browser"
|
|
362
|
+
onPress={() => onOpen(link)}
|
|
363
|
+
style={({ pressed }) => [styles.docLink, pressed ? styles.docLinkPressed : null]}
|
|
364
|
+
>
|
|
365
|
+
<Text style={styles.docLinkText}>{link.label}</Text>
|
|
366
|
+
</Pressable>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function ProviderSetupSection({ styles }: { styles: OmpConfigStyles }) {
|
|
371
|
+
return (
|
|
372
|
+
<SectionCard styles={styles} title="OMP Plugin">
|
|
373
|
+
<Text style={styles.muted}>
|
|
374
|
+
Launch settings currently come from the active Paseo provider profile.
|
|
375
|
+
</Text>
|
|
376
|
+
<KeyValueRow
|
|
377
|
+
styles={styles}
|
|
378
|
+
label="Per agent"
|
|
379
|
+
value="Model, mode, thinking, MCP servers, and persistence"
|
|
380
|
+
/>
|
|
381
|
+
<KeyValueRow
|
|
382
|
+
styles={styles}
|
|
383
|
+
label="Provider profile"
|
|
384
|
+
value="Command, environment, session directory, RPC timeout, role models, and denied tools"
|
|
385
|
+
/>
|
|
386
|
+
</SectionCard>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function PluginConfigurationSection({ styles }: { styles: OmpConfigStyles }) {
|
|
391
|
+
return (
|
|
392
|
+
<>
|
|
393
|
+
<SectionCard styles={styles} title="OMP Plugin launch options">
|
|
394
|
+
<Text style={styles.muted}>
|
|
395
|
+
Paseo does not expose the effective providerOptions for active launches through the plugin
|
|
396
|
+
API. Configure these values in the provider profile; this tab documents the supported
|
|
397
|
+
contract without claiming defaults are active.
|
|
398
|
+
</Text>
|
|
399
|
+
<KeyValueRow styles={styles} label="Command" value="Executable and argument prefix" />
|
|
400
|
+
<KeyValueRow
|
|
401
|
+
styles={styles}
|
|
402
|
+
label="Inherited environment"
|
|
403
|
+
value="Daemon variable names copied at OMP spawn time"
|
|
404
|
+
/>
|
|
405
|
+
<KeyValueRow
|
|
406
|
+
styles={styles}
|
|
407
|
+
label="Explicit environment"
|
|
408
|
+
value="Stored non-secret overrides"
|
|
409
|
+
/>
|
|
410
|
+
<KeyValueRow styles={styles} label="Output redaction" value="None or configured values" />
|
|
411
|
+
<KeyValueRow
|
|
412
|
+
styles={styles}
|
|
413
|
+
label="Runtime parameters"
|
|
414
|
+
value="Session directory, RPC timeout, small, slow, and plan models"
|
|
415
|
+
/>
|
|
416
|
+
<KeyValueRow styles={styles} label="Denied tools" value="Native OMP tool restrictions" />
|
|
417
|
+
</SectionCard>
|
|
418
|
+
<Text style={styles.muted}>
|
|
419
|
+
Inherited environment configuration stores names only. Values stay in the daemon environment
|
|
420
|
+
and are resolved only when OMP starts.
|
|
421
|
+
</Text>
|
|
422
|
+
</>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
function toneColor(theme: PluginSurfaceProps["theme"], tone: BinaryHealthSummary["tone"]): string {
|
|
426
|
+
if (tone === "ok") return theme.colors.statusSuccess;
|
|
427
|
+
if (tone === "warning") return theme.colors.statusWarning;
|
|
428
|
+
if (tone === "danger") return theme.colors.statusDanger;
|
|
429
|
+
return theme.colors.foregroundMuted;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function BinarySection({
|
|
433
|
+
theme,
|
|
434
|
+
styles,
|
|
435
|
+
health,
|
|
436
|
+
}: {
|
|
437
|
+
theme: PluginSurfaceProps["theme"];
|
|
438
|
+
styles: OmpConfigStyles;
|
|
439
|
+
health: OmpProviderHealth;
|
|
440
|
+
}) {
|
|
441
|
+
const summary = summarizeBinaryHealth(health.binary);
|
|
442
|
+
return (
|
|
443
|
+
<SectionCard styles={styles} title="omp binary">
|
|
444
|
+
<KeyValueRow
|
|
445
|
+
styles={styles}
|
|
446
|
+
label="Status"
|
|
447
|
+
value={summary.label}
|
|
448
|
+
valueColor={toneColor(theme, summary.tone)}
|
|
449
|
+
/>
|
|
450
|
+
{health.binary.resolvedPath ? (
|
|
451
|
+
<KeyValueRow styles={styles} label="Resolved path" value={health.binary.resolvedPath} />
|
|
452
|
+
) : null}
|
|
453
|
+
{health.binary.processCleanupFailed ? (
|
|
454
|
+
<KeyValueRow
|
|
455
|
+
styles={styles}
|
|
456
|
+
label="Process cleanup"
|
|
457
|
+
value="Timed-out probe did not confirm exit"
|
|
458
|
+
valueColor={theme.colors.statusDanger}
|
|
459
|
+
/>
|
|
460
|
+
) : null}
|
|
461
|
+
</SectionCard>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function CapabilitiesSection({
|
|
466
|
+
theme,
|
|
467
|
+
styles,
|
|
468
|
+
health,
|
|
469
|
+
}: {
|
|
470
|
+
theme: PluginSurfaceProps["theme"];
|
|
471
|
+
styles: OmpConfigStyles;
|
|
472
|
+
health: OmpProviderHealth;
|
|
473
|
+
}) {
|
|
474
|
+
return (
|
|
475
|
+
<SectionCard styles={styles} title="Runtime compatibility">
|
|
476
|
+
<KeyValueRow
|
|
477
|
+
styles={styles}
|
|
478
|
+
label="rpc-ui protocol"
|
|
479
|
+
value={summarizeRpcUiSupport(health.rpcUi)}
|
|
480
|
+
valueColor={toneColor(theme, rpcUiTone(health.rpcUi))}
|
|
481
|
+
/>
|
|
482
|
+
<KeyValueRow
|
|
483
|
+
styles={styles}
|
|
484
|
+
label="LSP tool"
|
|
485
|
+
value={summarizeLspSupport(health.lsp)}
|
|
486
|
+
valueColor={toneColor(theme, lspTone(health.lsp))}
|
|
487
|
+
/>
|
|
488
|
+
<KeyValueRow
|
|
489
|
+
styles={styles}
|
|
490
|
+
label="MCP"
|
|
491
|
+
value={summarizeMcpDiagnostics(health.mcp)}
|
|
492
|
+
valueColor={toneColor(theme, mcpTone(health.mcp))}
|
|
493
|
+
/>
|
|
494
|
+
</SectionCard>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function pathValue(path: string, state: PathStateSummary): string {
|
|
499
|
+
return `${path} (${state.label})`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function StorageSection({
|
|
503
|
+
theme,
|
|
504
|
+
styles,
|
|
505
|
+
health,
|
|
506
|
+
}: {
|
|
507
|
+
theme: PluginSurfaceProps["theme"];
|
|
508
|
+
styles: OmpConfigStyles;
|
|
509
|
+
health: OmpProviderHealth;
|
|
510
|
+
}) {
|
|
511
|
+
const agentRoot = summarizePathState(health.roots.agentRootState);
|
|
512
|
+
const configPath = summarizePathState(health.roots.configState);
|
|
513
|
+
const sessionRoot = summarizePathState(health.roots.sessionRootState);
|
|
514
|
+
const agentDb = summarizePathState(health.databases.agentDbState);
|
|
515
|
+
const historyDb = summarizePathState(health.databases.historyDbState);
|
|
516
|
+
return (
|
|
517
|
+
<SectionCard styles={styles} title="Storage">
|
|
518
|
+
<KeyValueRow
|
|
519
|
+
styles={styles}
|
|
520
|
+
label="Agent root"
|
|
521
|
+
value={pathValue(health.roots.agentRoot, agentRoot)}
|
|
522
|
+
valueColor={toneColor(theme, agentRoot.tone)}
|
|
523
|
+
/>
|
|
524
|
+
<KeyValueRow
|
|
525
|
+
styles={styles}
|
|
526
|
+
label="Config file"
|
|
527
|
+
value={pathValue(health.roots.configPath, configPath)}
|
|
528
|
+
valueColor={toneColor(theme, configPath.tone)}
|
|
529
|
+
/>
|
|
530
|
+
<KeyValueRow
|
|
531
|
+
styles={styles}
|
|
532
|
+
label="Session root"
|
|
533
|
+
value={pathValue(health.roots.sessionRoot, sessionRoot)}
|
|
534
|
+
valueColor={toneColor(theme, sessionRoot.tone)}
|
|
535
|
+
/>
|
|
536
|
+
<KeyValueRow
|
|
537
|
+
styles={styles}
|
|
538
|
+
label="agent.db"
|
|
539
|
+
value={agentDb.label}
|
|
540
|
+
valueColor={toneColor(theme, agentDb.tone)}
|
|
541
|
+
/>
|
|
542
|
+
<KeyValueRow
|
|
543
|
+
styles={styles}
|
|
544
|
+
label="history.db"
|
|
545
|
+
value={historyDb.label}
|
|
546
|
+
valueColor={toneColor(theme, historyDb.tone)}
|
|
547
|
+
/>
|
|
548
|
+
<KeyValueRow styles={styles} label="Memory backend" value={summarizeMemoryBackend(health)} />
|
|
549
|
+
</SectionCard>
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function ProcessSection({
|
|
554
|
+
theme,
|
|
555
|
+
styles,
|
|
556
|
+
health,
|
|
557
|
+
}: {
|
|
558
|
+
theme: PluginSurfaceProps["theme"];
|
|
559
|
+
styles: OmpConfigStyles;
|
|
560
|
+
health: OmpProviderHealth;
|
|
561
|
+
}) {
|
|
562
|
+
return (
|
|
563
|
+
<SectionCard styles={styles} title="Processes">
|
|
564
|
+
<KeyValueRow
|
|
565
|
+
styles={styles}
|
|
566
|
+
label="OMP Hub"
|
|
567
|
+
value={summarizeProcessDiagnostics(health.process)}
|
|
568
|
+
valueColor={toneColor(theme, processTone(health.process))}
|
|
569
|
+
/>
|
|
570
|
+
</SectionCard>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function ProviderHealthSection({
|
|
575
|
+
theme,
|
|
576
|
+
styles,
|
|
577
|
+
}: {
|
|
578
|
+
theme: PluginSurfaceProps["theme"];
|
|
579
|
+
styles: OmpConfigStyles;
|
|
580
|
+
}) {
|
|
581
|
+
const paseo = usePaseo();
|
|
582
|
+
const queryClient = useQueryClient();
|
|
583
|
+
const loadHealth = useRpc(getOmpProviderHealth);
|
|
584
|
+
const health = useQuery({
|
|
585
|
+
queryKey: HEALTH_QUERY_KEY,
|
|
586
|
+
queryFn: () => loadHealth({}),
|
|
587
|
+
});
|
|
588
|
+
const providers = useQuery({
|
|
589
|
+
queryKey: PROVIDERS_QUERY_KEY,
|
|
590
|
+
queryFn: () => loadReadyProviderSnapshot(paseo.providers),
|
|
591
|
+
});
|
|
592
|
+
const refresh = useMutation({
|
|
593
|
+
mutationFn: async () => {
|
|
594
|
+
const result = await refreshProviderDiagnostics({
|
|
595
|
+
providers: paseo.providers,
|
|
596
|
+
loadForcedHealth: () => loadHealth({ force: true }),
|
|
597
|
+
cacheHealth: (value) => queryClient.setQueryData(HEALTH_QUERY_KEY, value),
|
|
598
|
+
cacheProviders: (value) => queryClient.setQueryData(PROVIDERS_QUERY_KEY, value),
|
|
599
|
+
});
|
|
600
|
+
if (result.failed) throw new Error("Could not fully refresh OMP provider health.");
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
const isRefreshing = refresh.isPending || health.isFetching || providers.isFetching;
|
|
604
|
+
const knownProviders = providers.data ? selectKnownOmpProviders(providers.data.entries) : [];
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
<>
|
|
608
|
+
<View style={styles.sectionHeader}>
|
|
609
|
+
<View style={styles.sectionHeaderRow}>
|
|
610
|
+
<Text style={styles.sectionTitle}>Provider health</Text>
|
|
611
|
+
<Pressable
|
|
612
|
+
accessibilityRole="button"
|
|
613
|
+
accessibilityLabel="Refresh OMP provider health"
|
|
614
|
+
style={styles.refresh}
|
|
615
|
+
disabled={isRefreshing}
|
|
616
|
+
onPress={() => refresh.mutate()}
|
|
617
|
+
>
|
|
618
|
+
<Icon name="RefreshCw" size={14} color={theme.colors.foreground} />
|
|
619
|
+
<Text style={styles.refreshLabel}>{isRefreshing ? "Refreshing…" : "Refresh"}</Text>
|
|
620
|
+
</Pressable>
|
|
621
|
+
</View>
|
|
622
|
+
</View>
|
|
623
|
+
<Text style={styles.muted}>
|
|
624
|
+
These checks use the daemon's global OMP command and default storage, not per-agent
|
|
625
|
+
profile overrides.
|
|
626
|
+
</Text>
|
|
627
|
+
|
|
628
|
+
{health.isLoading ? <Text style={styles.muted}>Checking the omp installation…</Text> : null}
|
|
629
|
+
{health.error ? (
|
|
630
|
+
<Text style={styles.error}>Could not check the omp installation. Try refreshing.</Text>
|
|
631
|
+
) : null}
|
|
632
|
+
{refresh.error ? (
|
|
633
|
+
<Text style={styles.error}>Could not fully refresh provider health. Try again.</Text>
|
|
634
|
+
) : null}
|
|
635
|
+
{health.data ? <BinarySection theme={theme} styles={styles} health={health.data} /> : null}
|
|
636
|
+
{health.data ? (
|
|
637
|
+
<CapabilitiesSection theme={theme} styles={styles} health={health.data} />
|
|
638
|
+
) : null}
|
|
639
|
+
{health.data ? <StorageSection theme={theme} styles={styles} health={health.data} /> : null}
|
|
640
|
+
{health.data ? <ProcessSection theme={theme} styles={styles} health={health.data} /> : null}
|
|
641
|
+
|
|
642
|
+
{providers.isLoading ? <Text style={styles.muted}>Loading provider status…</Text> : null}
|
|
643
|
+
{providers.error ? (
|
|
644
|
+
<Text style={styles.error}>Could not load provider status. Try refreshing.</Text>
|
|
645
|
+
) : null}
|
|
646
|
+
{providers.data && knownProviders.length === 0 ? (
|
|
647
|
+
<Text style={styles.muted}>No OMP provider entries reported yet.</Text>
|
|
648
|
+
) : null}
|
|
649
|
+
{knownProviders.length > 0 ? (
|
|
650
|
+
<SectionCard styles={styles} title="Registered providers">
|
|
651
|
+
{knownProviders.map((provider) => {
|
|
652
|
+
const status = summarizeProviderStatus(provider);
|
|
653
|
+
return (
|
|
654
|
+
<KeyValueRow
|
|
655
|
+
key={provider.id}
|
|
656
|
+
styles={styles}
|
|
657
|
+
label={provider.label}
|
|
658
|
+
value={status.label}
|
|
659
|
+
valueColor={toneColor(theme, status.tone)}
|
|
660
|
+
/>
|
|
661
|
+
);
|
|
662
|
+
})}
|
|
663
|
+
</SectionCard>
|
|
664
|
+
) : null}
|
|
665
|
+
</>
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const CATEGORY_LABELS: Record<OmpSettingCategory, string> = {
|
|
670
|
+
appearance: "Appearance",
|
|
671
|
+
model: "Model",
|
|
672
|
+
interaction: "Interaction",
|
|
673
|
+
context: "Context",
|
|
674
|
+
memory: "Memory",
|
|
675
|
+
files: "Files",
|
|
676
|
+
shell: "Shell",
|
|
677
|
+
tools: "Tools",
|
|
678
|
+
tasks: "Tasks",
|
|
679
|
+
providers: "Providers",
|
|
680
|
+
general: "General",
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const CONFIG_CATEGORIES = OMP_SETTING_CATEGORIES.map((id) => ({ id, label: CATEGORY_LABELS[id] }));
|
|
684
|
+
|
|
685
|
+
function fallbackSettingsFromConfig(config: OmpConfig | null | undefined): OmpSetting[] {
|
|
686
|
+
if (!config) return [];
|
|
687
|
+
const settings: OmpSetting[] = [];
|
|
688
|
+
const visit = (value: unknown, path: string) => {
|
|
689
|
+
if (Array.isArray(value)) {
|
|
690
|
+
settings.push({ path, type: "array", value, description: "" });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (value !== null && typeof value === "object") {
|
|
694
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
695
|
+
if (entries.length === 0) {
|
|
696
|
+
settings.push({ path, type: "record", value, description: "" });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
for (const [key, nested] of entries) visit(nested, path ? `${path}.${key}` : key);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (typeof value === "boolean") {
|
|
703
|
+
settings.push({ path, type: "boolean", value, description: "" });
|
|
704
|
+
} else if (typeof value === "number") {
|
|
705
|
+
settings.push({ path, type: "number", value, description: "" });
|
|
706
|
+
} else if (typeof value === "string") {
|
|
707
|
+
settings.push({ path, type: "string", value, description: "" });
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
visit(config, "");
|
|
711
|
+
return settings;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
type SurfaceView = "overview" | "plugin" | "plugins" | "configuration" | "diagnostics";
|
|
715
|
+
const SURFACE_VIEWS: readonly { id: SurfaceView; label: string }[] = [
|
|
716
|
+
{ id: "overview", label: "Overview" },
|
|
717
|
+
{ id: "plugin", label: "Plugin" },
|
|
718
|
+
{ id: "plugins", label: "OMP plugins" },
|
|
719
|
+
{ id: "configuration", label: "Configuration" },
|
|
720
|
+
{ id: "diagnostics", label: "Diagnostics" },
|
|
721
|
+
];
|
|
722
|
+
|
|
723
|
+
function formatScalarValue(value: unknown): string {
|
|
724
|
+
if (value === undefined || value === null) return "Not set";
|
|
725
|
+
if (typeof value === "boolean") return value ? "Enabled" : "Disabled";
|
|
726
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
727
|
+
const serialized = JSON.stringify(value);
|
|
728
|
+
if (!serialized) return "Not set";
|
|
729
|
+
return serialized.length > 240 ? `${serialized.slice(0, 237)}…` : serialized;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function StructuredSettingValue({
|
|
733
|
+
setting,
|
|
734
|
+
styles,
|
|
735
|
+
}: {
|
|
736
|
+
setting: OmpSetting;
|
|
737
|
+
styles: OmpConfigStyles;
|
|
738
|
+
}) {
|
|
739
|
+
if (setting.redacted) {
|
|
740
|
+
return (
|
|
741
|
+
<Text style={styles.settingValue}>
|
|
742
|
+
{setting.configured === true
|
|
743
|
+
? "Configured (hidden)"
|
|
744
|
+
: setting.configured === false
|
|
745
|
+
? "Not set"
|
|
746
|
+
: "Hidden"}
|
|
747
|
+
</Text>
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
const value = setting.value;
|
|
751
|
+
if (!Array.isArray(value) && (value === null || typeof value !== "object")) {
|
|
752
|
+
return <Text style={styles.settingValue}>{formatScalarValue(value)}</Text>;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (Array.isArray(value)) {
|
|
756
|
+
if (value.length === 0) return <Text style={styles.muted}>None</Text>;
|
|
757
|
+
const allScalar = value.every(
|
|
758
|
+
(item) => item === null || ["boolean", "number", "string"].includes(typeof item),
|
|
759
|
+
);
|
|
760
|
+
const occurrences = new Map<string, number>();
|
|
761
|
+
const items = value.map((item) => {
|
|
762
|
+
const text = formatScalarValue(item);
|
|
763
|
+
const occurrence = (occurrences.get(text) ?? 0) + 1;
|
|
764
|
+
occurrences.set(text, occurrence);
|
|
765
|
+
return { item, key: `${text}-${occurrence}`, text };
|
|
766
|
+
});
|
|
767
|
+
return (
|
|
768
|
+
<View style={styles.recordList}>
|
|
769
|
+
<Text style={styles.collectionSummary}>{value.length} items</Text>
|
|
770
|
+
{allScalar ? (
|
|
771
|
+
<View style={styles.chipList}>
|
|
772
|
+
{items.map(({ key, text }) => (
|
|
773
|
+
<View key={key} style={styles.chip}>
|
|
774
|
+
<Text style={styles.chipText}>{text}</Text>
|
|
775
|
+
</View>
|
|
776
|
+
))}
|
|
777
|
+
</View>
|
|
778
|
+
) : (
|
|
779
|
+
items.map(({ item, key }, position) => (
|
|
780
|
+
<View key={key} style={styles.recordRow}>
|
|
781
|
+
<Text style={styles.recordKey}>{position + 1}</Text>
|
|
782
|
+
<Text selectable style={styles.recordValue}>
|
|
783
|
+
{formatScalarValue(item)}
|
|
784
|
+
</Text>
|
|
785
|
+
</View>
|
|
786
|
+
))
|
|
787
|
+
)}
|
|
788
|
+
</View>
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
793
|
+
if (entries.length === 0) return <Text style={styles.muted}>None</Text>;
|
|
794
|
+
return (
|
|
795
|
+
<View style={styles.recordList}>
|
|
796
|
+
<Text style={styles.collectionSummary}>{entries.length} entries</Text>
|
|
797
|
+
{entries.map(([key, item]) => (
|
|
798
|
+
<View key={key} style={styles.recordRow}>
|
|
799
|
+
<Text selectable style={styles.recordKey}>
|
|
800
|
+
{key}
|
|
801
|
+
</Text>
|
|
802
|
+
<Text selectable style={styles.recordValue}>
|
|
803
|
+
{formatScalarValue(item)}
|
|
804
|
+
</Text>
|
|
805
|
+
</View>
|
|
806
|
+
))}
|
|
807
|
+
</View>
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
type SettingDraft = { operation: "set"; value: string | boolean } | { operation: "reset" };
|
|
812
|
+
|
|
813
|
+
function EditableScalarValue({
|
|
814
|
+
setting,
|
|
815
|
+
draft,
|
|
816
|
+
disabled,
|
|
817
|
+
styles,
|
|
818
|
+
onSet,
|
|
819
|
+
onReset,
|
|
820
|
+
}: {
|
|
821
|
+
setting: OmpSetting;
|
|
822
|
+
draft: SettingDraft | undefined;
|
|
823
|
+
disabled: boolean;
|
|
824
|
+
styles: OmpConfigStyles;
|
|
825
|
+
onSet(value: string | boolean): void;
|
|
826
|
+
onReset(): void;
|
|
827
|
+
}) {
|
|
828
|
+
const value = draft?.operation === "set" ? draft.value : setting.value;
|
|
829
|
+
return (
|
|
830
|
+
<View style={styles.recordList}>
|
|
831
|
+
{draft?.operation === "reset" ? (
|
|
832
|
+
<Text style={styles.muted}>Will reset to the OMP default</Text>
|
|
833
|
+
) : setting.type === "boolean" ? (
|
|
834
|
+
<Switch
|
|
835
|
+
accessibilityLabel={`Toggle ${formatOmpSettingLabel(setting.path)}`}
|
|
836
|
+
disabled={disabled}
|
|
837
|
+
value={value === true}
|
|
838
|
+
onValueChange={onSet}
|
|
839
|
+
/>
|
|
840
|
+
) : (
|
|
841
|
+
<TextInput
|
|
842
|
+
accessibilityLabel={`Edit ${formatOmpSettingLabel(setting.path)}`}
|
|
843
|
+
editable={!disabled}
|
|
844
|
+
keyboardType={setting.type === "number" ? "numeric" : "default"}
|
|
845
|
+
value={value === undefined ? "" : String(value)}
|
|
846
|
+
onChangeText={onSet}
|
|
847
|
+
style={styles.scalarInput}
|
|
848
|
+
/>
|
|
849
|
+
)}
|
|
850
|
+
<Pressable accessibilityRole="button" disabled={disabled} onPress={onReset}>
|
|
851
|
+
<Text style={styles.resetAction}>Reset to default</Text>
|
|
852
|
+
</Pressable>
|
|
853
|
+
</View>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function ConfigurationCategory({
|
|
858
|
+
category,
|
|
859
|
+
styles,
|
|
860
|
+
settings,
|
|
861
|
+
drafts,
|
|
862
|
+
disabled,
|
|
863
|
+
onDraft,
|
|
864
|
+
onOpenDocumentation,
|
|
865
|
+
}: {
|
|
866
|
+
category: { id: OmpSettingCategory; label: string };
|
|
867
|
+
styles: OmpConfigStyles;
|
|
868
|
+
settings: readonly OmpSetting[];
|
|
869
|
+
drafts: Readonly<Record<string, SettingDraft>>;
|
|
870
|
+
disabled: boolean;
|
|
871
|
+
onDraft(path: string, draft: SettingDraft): void;
|
|
872
|
+
onOpenDocumentation(link: OmpDocumentationLink): void;
|
|
873
|
+
}) {
|
|
874
|
+
const categoryDocumentation = documentationForSettingCategory(category.id);
|
|
875
|
+
return (
|
|
876
|
+
<SectionCard
|
|
877
|
+
styles={styles}
|
|
878
|
+
title={`${category.label} · ${settings.length}`}
|
|
879
|
+
action={
|
|
880
|
+
categoryDocumentation ? (
|
|
881
|
+
<DocumentationLink
|
|
882
|
+
link={categoryDocumentation}
|
|
883
|
+
styles={styles}
|
|
884
|
+
onOpen={onOpenDocumentation}
|
|
885
|
+
/>
|
|
886
|
+
) : undefined
|
|
887
|
+
}
|
|
888
|
+
>
|
|
889
|
+
{settings.map((setting) => {
|
|
890
|
+
const complex =
|
|
891
|
+
Array.isArray(setting.value) ||
|
|
892
|
+
(setting.value !== null && typeof setting.value === "object");
|
|
893
|
+
const editable =
|
|
894
|
+
!setting.redacted && ["boolean", "number", "string", "enum"].includes(setting.type);
|
|
895
|
+
const settingDocumentation = documentationForSettingPath(setting.path);
|
|
896
|
+
return (
|
|
897
|
+
<View key={setting.path} style={styles.setting}>
|
|
898
|
+
<View style={styles.settingHeader}>
|
|
899
|
+
<Text style={styles.cardTitle}>{formatOmpSettingLabel(setting.path)}</Text>
|
|
900
|
+
{!complex && !editable ? (
|
|
901
|
+
<StructuredSettingValue setting={setting} styles={styles} />
|
|
902
|
+
) : null}
|
|
903
|
+
</View>
|
|
904
|
+
<Text selectable style={styles.settingPath}>
|
|
905
|
+
{setting.path} · {setting.type}
|
|
906
|
+
</Text>
|
|
907
|
+
{settingDocumentation ? (
|
|
908
|
+
<View style={styles.docsActions}>
|
|
909
|
+
<DocumentationLink
|
|
910
|
+
link={settingDocumentation}
|
|
911
|
+
styles={styles}
|
|
912
|
+
onOpen={onOpenDocumentation}
|
|
913
|
+
/>
|
|
914
|
+
</View>
|
|
915
|
+
) : null}
|
|
916
|
+
{editable ? (
|
|
917
|
+
<EditableScalarValue
|
|
918
|
+
setting={setting}
|
|
919
|
+
draft={drafts[setting.path]}
|
|
920
|
+
disabled={disabled}
|
|
921
|
+
styles={styles}
|
|
922
|
+
onSet={(value) => onDraft(setting.path, { operation: "set", value })}
|
|
923
|
+
onReset={() => onDraft(setting.path, { operation: "reset" })}
|
|
924
|
+
/>
|
|
925
|
+
) : complex ? (
|
|
926
|
+
<StructuredSettingValue setting={setting} styles={styles} />
|
|
927
|
+
) : null}
|
|
928
|
+
{setting.description ? (
|
|
929
|
+
<Text style={styles.settingDescription}>{setting.description}</Text>
|
|
930
|
+
) : null}
|
|
931
|
+
</View>
|
|
932
|
+
);
|
|
933
|
+
})}
|
|
934
|
+
</SectionCard>
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function SurfaceTabs({
|
|
939
|
+
styles,
|
|
940
|
+
selected,
|
|
941
|
+
onSelect,
|
|
942
|
+
}: {
|
|
943
|
+
styles: OmpConfigStyles;
|
|
944
|
+
selected: SurfaceView;
|
|
945
|
+
onSelect: (view: SurfaceView) => void;
|
|
946
|
+
}) {
|
|
947
|
+
return (
|
|
948
|
+
<View accessibilityRole="tablist" style={styles.topTabs}>
|
|
949
|
+
{SURFACE_VIEWS.map((view) => {
|
|
950
|
+
const active = selected === view.id;
|
|
951
|
+
return (
|
|
952
|
+
<Pressable
|
|
953
|
+
key={view.id}
|
|
954
|
+
accessibilityRole="tab"
|
|
955
|
+
accessibilityState={{ selected: active }}
|
|
956
|
+
onPress={() => onSelect(view.id)}
|
|
957
|
+
style={[styles.topTab, active ? styles.topTabActive : null]}
|
|
958
|
+
>
|
|
959
|
+
<Text style={[styles.topTabLabel, active ? styles.topTabLabelActive : null]}>
|
|
960
|
+
{view.label}
|
|
961
|
+
</Text>
|
|
962
|
+
</Pressable>
|
|
963
|
+
);
|
|
964
|
+
})}
|
|
965
|
+
</View>
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
969
|
+
const loadConfig = useRpc(listOmpConfig);
|
|
970
|
+
const loadSettings = useRpc(listOmpSettings);
|
|
971
|
+
const updateSettings = useRpc(updateOmpSettings);
|
|
972
|
+
const queryClient = useQueryClient();
|
|
973
|
+
const configQuery = useQuery({
|
|
974
|
+
queryKey: ["paseo-omp", "config"],
|
|
975
|
+
queryFn: () => loadConfig({}),
|
|
976
|
+
refetchInterval: CONFIG_POLL_MS,
|
|
977
|
+
});
|
|
978
|
+
const settingsQuery = useQuery({
|
|
979
|
+
queryKey: SETTINGS_QUERY_KEY,
|
|
980
|
+
queryFn: () => loadSettings({}),
|
|
981
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
982
|
+
});
|
|
983
|
+
const [view, setView] = useState<SurfaceView>("overview");
|
|
984
|
+
const [activeCategory, setActiveCategory] = useState<OmpSettingCategory>("appearance");
|
|
985
|
+
const [search, setSearch] = useState("");
|
|
986
|
+
const [drafts, setDrafts] = useState<Record<string, SettingDraft>>({});
|
|
987
|
+
const [documentationError, setDocumentationError] = useState<string | null>(null);
|
|
988
|
+
const styles = useConfigStyles(theme, layout.compact);
|
|
989
|
+
const normalizedSearch = search.trim().toLocaleLowerCase();
|
|
990
|
+
const catalog = useMemo(() => {
|
|
991
|
+
const sourceSettings = settingsQuery.data?.available
|
|
992
|
+
? settingsQuery.data.settings
|
|
993
|
+
: fallbackSettingsFromConfig(configQuery.data?.config);
|
|
994
|
+
const matching = sourceSettings.filter((setting) => {
|
|
995
|
+
if (!normalizedSearch) return true;
|
|
996
|
+
return `${setting.path}\n${setting.description}`
|
|
997
|
+
.toLocaleLowerCase()
|
|
998
|
+
.includes(normalizedSearch);
|
|
999
|
+
});
|
|
1000
|
+
const byCategory = new Map<OmpSettingCategory, OmpSetting[]>();
|
|
1001
|
+
for (const setting of matching) {
|
|
1002
|
+
const category = categorizeOmpSetting(setting.path);
|
|
1003
|
+
const group = byCategory.get(category);
|
|
1004
|
+
if (group) group.push(setting);
|
|
1005
|
+
else byCategory.set(category, [setting]);
|
|
1006
|
+
}
|
|
1007
|
+
return { sourceSettings, matching, byCategory };
|
|
1008
|
+
}, [configQuery.data?.config, normalizedSearch, settingsQuery.data]);
|
|
1009
|
+
const visibleCategories = CONFIG_CATEGORIES.filter(
|
|
1010
|
+
(category) => (catalog.byCategory.get(category.id)?.length ?? 0) > 0,
|
|
1011
|
+
);
|
|
1012
|
+
const selectedCategory =
|
|
1013
|
+
visibleCategories.find((category) => category.id === activeCategory) ?? visibleCategories[0];
|
|
1014
|
+
const openDocumentation = useCallback(async (link: OmpDocumentationLink) => {
|
|
1015
|
+
setDocumentationError(null);
|
|
1016
|
+
try {
|
|
1017
|
+
await Linking.openURL(link.url);
|
|
1018
|
+
} catch {
|
|
1019
|
+
setDocumentationError(`Could not open ${link.label.toLocaleLowerCase()}.`);
|
|
1020
|
+
}
|
|
1021
|
+
}, []);
|
|
1022
|
+
|
|
1023
|
+
const save = useMutation({
|
|
1024
|
+
mutationFn: async () => {
|
|
1025
|
+
const revision = settingsQuery.data?.revision;
|
|
1026
|
+
if (!revision) throw new Error("OMP settings cannot be edited without a current revision.");
|
|
1027
|
+
const byPath = new Map(catalog.sourceSettings.map((setting) => [setting.path, setting]));
|
|
1028
|
+
const changes = Object.entries(drafts).map(([path, draft]) => {
|
|
1029
|
+
if (draft.operation === "reset") return { operation: "reset" as const, path };
|
|
1030
|
+
const setting = byPath.get(path);
|
|
1031
|
+
if (!setting) throw new Error(`Setting ${path} is no longer available.`);
|
|
1032
|
+
let value: OmpScalarValue = draft.value;
|
|
1033
|
+
if (setting.type === "number") {
|
|
1034
|
+
const raw = String(draft.value).trim();
|
|
1035
|
+
if (!raw) throw new Error(`${path} requires a number.`);
|
|
1036
|
+
const parsed = Number(raw);
|
|
1037
|
+
if (!Number.isFinite(parsed)) throw new Error(`${path} requires a finite number.`);
|
|
1038
|
+
value = parsed;
|
|
1039
|
+
}
|
|
1040
|
+
return { operation: "set" as const, path, value };
|
|
1041
|
+
});
|
|
1042
|
+
return updateSettings({ revision, changes });
|
|
1043
|
+
},
|
|
1044
|
+
onSuccess: (result) => {
|
|
1045
|
+
queryClient.setQueryData(SETTINGS_QUERY_KEY, result.catalog);
|
|
1046
|
+
if (
|
|
1047
|
+
!result.conflict &&
|
|
1048
|
+
!result.failed &&
|
|
1049
|
+
result.appliedPaths.length === Object.keys(drafts).length
|
|
1050
|
+
) {
|
|
1051
|
+
setDrafts({});
|
|
1052
|
+
} else if (result.appliedPaths.length > 0) {
|
|
1053
|
+
setDrafts((current) => {
|
|
1054
|
+
const next = { ...current };
|
|
1055
|
+
for (const path of result.appliedPaths) delete next[path];
|
|
1056
|
+
return next;
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
const draftCount = Object.keys(drafts).length;
|
|
1062
|
+
const displayedConfigPath = settingsQuery.data?.available
|
|
1063
|
+
? settingsQuery.data.path
|
|
1064
|
+
: configQuery.data?.path;
|
|
1065
|
+
|
|
1066
|
+
return (
|
|
1067
|
+
<ScrollView contentContainerStyle={styles.root}>
|
|
1068
|
+
<Text style={styles.pageTitle}>OMP</Text>
|
|
1069
|
+
<SurfaceTabs styles={styles} selected={view} onSelect={setView} />
|
|
1070
|
+
|
|
1071
|
+
{view === "overview" ? (
|
|
1072
|
+
<>
|
|
1073
|
+
<ProviderSetupSection styles={styles} />
|
|
1074
|
+
{configQuery.isLoading ? (
|
|
1075
|
+
<Text style={styles.muted}>Loading the native configuration…</Text>
|
|
1076
|
+
) : configQuery.error ? (
|
|
1077
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
1078
|
+
Could not read the native OMP configuration.
|
|
1079
|
+
</Text>
|
|
1080
|
+
) : (
|
|
1081
|
+
<SectionCard styles={styles} title="Native configuration">
|
|
1082
|
+
<KeyValueRow
|
|
1083
|
+
styles={styles}
|
|
1084
|
+
label="Source"
|
|
1085
|
+
value={displayedConfigPath ?? "Source unavailable"}
|
|
1086
|
+
/>
|
|
1087
|
+
<KeyValueRow
|
|
1088
|
+
styles={styles}
|
|
1089
|
+
label="Status"
|
|
1090
|
+
value={configQuery.data?.available ? "Available" : "Unavailable"}
|
|
1091
|
+
/>
|
|
1092
|
+
<KeyValueRow
|
|
1093
|
+
styles={styles}
|
|
1094
|
+
label="Settings discovered"
|
|
1095
|
+
value={String(catalog.sourceSettings.length)}
|
|
1096
|
+
/>
|
|
1097
|
+
</SectionCard>
|
|
1098
|
+
)}
|
|
1099
|
+
</>
|
|
1100
|
+
) : null}
|
|
1101
|
+
|
|
1102
|
+
{view === "plugin" ? <PluginConfigurationSection styles={styles} /> : null}
|
|
1103
|
+
|
|
1104
|
+
{view === "plugins" ? (
|
|
1105
|
+
<OmpPluginManagerSection theme={theme} compact={layout.compact} />
|
|
1106
|
+
) : null}
|
|
1107
|
+
|
|
1108
|
+
{view === "diagnostics" ? <ProviderHealthSection theme={theme} styles={styles} /> : null}
|
|
1109
|
+
|
|
1110
|
+
{view === "configuration" ? (
|
|
1111
|
+
<>
|
|
1112
|
+
<View style={styles.sectionHeader}>
|
|
1113
|
+
<View style={styles.sectionHeaderRow}>
|
|
1114
|
+
<Text style={styles.sectionTitle}>Configuration</Text>
|
|
1115
|
+
<Pressable
|
|
1116
|
+
accessibilityRole="button"
|
|
1117
|
+
accessibilityLabel="Refresh OMP configuration"
|
|
1118
|
+
style={styles.refresh}
|
|
1119
|
+
disabled={configQuery.isFetching || settingsQuery.isFetching}
|
|
1120
|
+
onPress={() => {
|
|
1121
|
+
void Promise.all([configQuery.refetch(), settingsQuery.refetch()]);
|
|
1122
|
+
}}
|
|
1123
|
+
>
|
|
1124
|
+
<Icon name="RefreshCw" size={14} color={theme.colors.foreground} />
|
|
1125
|
+
<Text style={styles.refreshLabel}>
|
|
1126
|
+
{configQuery.isFetching || settingsQuery.isFetching ? "Refreshing…" : "Refresh"}
|
|
1127
|
+
</Text>
|
|
1128
|
+
</Pressable>
|
|
1129
|
+
</View>
|
|
1130
|
+
<View style={styles.docsActions}>
|
|
1131
|
+
<DocumentationLink
|
|
1132
|
+
link={OMP_SETTINGS_REFERENCE}
|
|
1133
|
+
styles={styles}
|
|
1134
|
+
onOpen={openDocumentation}
|
|
1135
|
+
/>
|
|
1136
|
+
{OMP_SETTINGS_GUIDES.map((link) => (
|
|
1137
|
+
<DocumentationLink
|
|
1138
|
+
key={link.url}
|
|
1139
|
+
link={link}
|
|
1140
|
+
styles={styles}
|
|
1141
|
+
onOpen={openDocumentation}
|
|
1142
|
+
/>
|
|
1143
|
+
))}
|
|
1144
|
+
</View>
|
|
1145
|
+
{displayedConfigPath ? (
|
|
1146
|
+
<Text style={styles.source}>{`Source: ${displayedConfigPath}`}</Text>
|
|
1147
|
+
) : (
|
|
1148
|
+
<Text style={styles.source}>Source unavailable</Text>
|
|
1149
|
+
)}
|
|
1150
|
+
</View>
|
|
1151
|
+
{documentationError ? (
|
|
1152
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
1153
|
+
{documentationError}
|
|
1154
|
+
</Text>
|
|
1155
|
+
) : null}
|
|
1156
|
+
|
|
1157
|
+
{draftCount > 0 ? (
|
|
1158
|
+
<View style={styles.editorActions}>
|
|
1159
|
+
<Text style={styles.muted}>{draftCount} unsaved changes</Text>
|
|
1160
|
+
<Pressable
|
|
1161
|
+
accessibilityRole="button"
|
|
1162
|
+
disabled={save.isPending}
|
|
1163
|
+
onPress={() => setDrafts({})}
|
|
1164
|
+
style={styles.editorAction}
|
|
1165
|
+
>
|
|
1166
|
+
<Text style={styles.editorActionText}>Discard</Text>
|
|
1167
|
+
</Pressable>
|
|
1168
|
+
<Pressable
|
|
1169
|
+
accessibilityRole="button"
|
|
1170
|
+
disabled={save.isPending}
|
|
1171
|
+
onPress={() => save.mutate()}
|
|
1172
|
+
style={[styles.editorAction, styles.editorActionPrimary]}
|
|
1173
|
+
>
|
|
1174
|
+
<Text style={[styles.editorActionText, styles.editorActionTextPrimary]}>
|
|
1175
|
+
{save.isPending ? "Applying…" : "Apply changes"}
|
|
1176
|
+
</Text>
|
|
1177
|
+
</Pressable>
|
|
1178
|
+
</View>
|
|
1179
|
+
) : null}
|
|
1180
|
+
{save.error ? (
|
|
1181
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
1182
|
+
{save.error instanceof Error ? save.error.message : "Could not apply OMP settings."}
|
|
1183
|
+
</Text>
|
|
1184
|
+
) : null}
|
|
1185
|
+
{save.data?.conflict ? (
|
|
1186
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
1187
|
+
OMP configuration changed outside Paseo. Review the refreshed values and apply again.
|
|
1188
|
+
</Text>
|
|
1189
|
+
) : null}
|
|
1190
|
+
{save.data?.failed ? (
|
|
1191
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
1192
|
+
{save.data.failed.path}: {save.data.failed.message}
|
|
1193
|
+
</Text>
|
|
1194
|
+
) : null}
|
|
1195
|
+
|
|
1196
|
+
{settingsQuery.isLoading ? (
|
|
1197
|
+
<Text style={styles.muted}>Loading the OMP settings catalog…</Text>
|
|
1198
|
+
) : null}
|
|
1199
|
+
{settingsQuery.error || settingsQuery.data?.error ? (
|
|
1200
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
1201
|
+
OMP settings metadata is unavailable. Showing the safe values read from the config
|
|
1202
|
+
file when available.
|
|
1203
|
+
</Text>
|
|
1204
|
+
) : null}
|
|
1205
|
+
{settingsQuery.data?.droppedCount ? (
|
|
1206
|
+
<Text style={styles.muted}>
|
|
1207
|
+
{settingsQuery.data.droppedCount} settings use unsupported metadata types and are not
|
|
1208
|
+
shown.
|
|
1209
|
+
</Text>
|
|
1210
|
+
) : null}
|
|
1211
|
+
{!settingsQuery.isLoading && catalog.sourceSettings.length === 0 ? (
|
|
1212
|
+
<Text style={styles.muted}>OMP reported no readable settings.</Text>
|
|
1213
|
+
) : null}
|
|
1214
|
+
|
|
1215
|
+
{catalog.sourceSettings.length > 0 ? (
|
|
1216
|
+
<View style={styles.workspace}>
|
|
1217
|
+
<View style={styles.categoryRail}>
|
|
1218
|
+
<TextInput
|
|
1219
|
+
accessibilityLabel="Search OMP settings"
|
|
1220
|
+
placeholder="Search settings"
|
|
1221
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
1222
|
+
value={search}
|
|
1223
|
+
onChangeText={setSearch}
|
|
1224
|
+
style={styles.search}
|
|
1225
|
+
/>
|
|
1226
|
+
<Text style={styles.source}>
|
|
1227
|
+
{catalog.matching.length} of {catalog.sourceSettings.length} settings
|
|
1228
|
+
</Text>
|
|
1229
|
+
<View style={styles.categoryList}>
|
|
1230
|
+
{visibleCategories.map((category) => {
|
|
1231
|
+
const active = selectedCategory?.id === category.id;
|
|
1232
|
+
const count = catalog.byCategory.get(category.id)?.length ?? 0;
|
|
1233
|
+
return (
|
|
1234
|
+
<Pressable
|
|
1235
|
+
key={category.id}
|
|
1236
|
+
accessibilityRole="button"
|
|
1237
|
+
accessibilityState={{ selected: active }}
|
|
1238
|
+
onPress={() => setActiveCategory(category.id)}
|
|
1239
|
+
style={[styles.categoryButton, active ? styles.categoryButtonActive : null]}
|
|
1240
|
+
>
|
|
1241
|
+
<Text
|
|
1242
|
+
style={[styles.categoryLabel, active ? styles.categoryLabelActive : null]}
|
|
1243
|
+
>
|
|
1244
|
+
{category.label} · {count}
|
|
1245
|
+
</Text>
|
|
1246
|
+
</Pressable>
|
|
1247
|
+
);
|
|
1248
|
+
})}
|
|
1249
|
+
</View>
|
|
1250
|
+
</View>
|
|
1251
|
+
<View style={styles.categoryContent}>
|
|
1252
|
+
{selectedCategory ? (
|
|
1253
|
+
<ConfigurationCategory
|
|
1254
|
+
category={selectedCategory}
|
|
1255
|
+
styles={styles}
|
|
1256
|
+
settings={catalog.byCategory.get(selectedCategory.id) ?? []}
|
|
1257
|
+
drafts={drafts}
|
|
1258
|
+
disabled={!settingsQuery.data?.revision || save.isPending}
|
|
1259
|
+
onDraft={(path, draft) =>
|
|
1260
|
+
setDrafts((current) => ({ ...current, [path]: draft }))
|
|
1261
|
+
}
|
|
1262
|
+
onOpenDocumentation={openDocumentation}
|
|
1263
|
+
/>
|
|
1264
|
+
) : (
|
|
1265
|
+
<Text style={styles.muted}>No settings match this search.</Text>
|
|
1266
|
+
)}
|
|
1267
|
+
</View>
|
|
1268
|
+
</View>
|
|
1269
|
+
) : null}
|
|
1270
|
+
</>
|
|
1271
|
+
) : null}
|
|
1272
|
+
</ScrollView>
|
|
1273
|
+
);
|
|
1274
|
+
}
|