@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -0
  4. package/SUPPORT.md +40 -0
  5. package/TESTING.md +147 -0
  6. package/client/hub-icon.tsx +12 -0
  7. package/client/hub-popover.tsx +132 -0
  8. package/client/hub-status.ts +29 -0
  9. package/client/memory-panel.tsx +71 -0
  10. package/client/memory-popover.tsx +70 -0
  11. package/client/omp-config-surface.tsx +1274 -0
  12. package/client/omp-doc-links.ts +117 -0
  13. package/client/omp-plugin-manager.tsx +833 -0
  14. package/client/provider-diagnostics-state.ts +250 -0
  15. package/client/provider-icon.tsx +27 -0
  16. package/client/provider-image.tsx +66 -0
  17. package/client/quota-popover.tsx +150 -0
  18. package/client/quota-state.ts +131 -0
  19. package/client/sessions-popover.tsx +73 -0
  20. package/docs/alpha-release-checklist.md +70 -0
  21. package/docs/configuration.md +122 -0
  22. package/docs/core-provider-issue-audit.md +108 -0
  23. package/docs/installation.md +73 -0
  24. package/index.client.tsx +272 -0
  25. package/index.server.ts +51 -0
  26. package/package.json +84 -0
  27. package/paseo-plugin.json +5 -0
  28. package/server/hub.ts +145 -0
  29. package/server/memory.ts +86 -0
  30. package/server/mutation-queue.ts +12 -0
  31. package/server/omp-config.ts +126 -0
  32. package/server/omp-plugins.ts +627 -0
  33. package/server/omp-settings.ts +291 -0
  34. package/server/paths.ts +64 -0
  35. package/server/provider/catalog.ts +173 -0
  36. package/server/provider/config-normalization.ts +148 -0
  37. package/server/provider/connection.ts +992 -0
  38. package/server/provider/host-tools.ts +706 -0
  39. package/server/provider/image.ts +143 -0
  40. package/server/provider/mcp-transport.ts +394 -0
  41. package/server/provider/omp-rpc.ts +2739 -0
  42. package/server/provider/omp.svg +5 -0
  43. package/server/provider/provider-options.ts +27 -0
  44. package/server/provider/registration.ts +151 -0
  45. package/server/provider/security.ts +317 -0
  46. package/server/provider/session-descriptors.ts +431 -0
  47. package/server/provider/session.ts +4451 -0
  48. package/server/provider/settings.ts +78 -0
  49. package/server/provider/subsessions.ts +847 -0
  50. package/server/provider/timeline-projector.ts +1764 -0
  51. package/server/provider-diagnostics.ts +1057 -0
  52. package/server/quota.ts +54 -0
  53. package/server/sessions.ts +58 -0
  54. package/shared/hub.ts +43 -0
  55. package/shared/memory.ts +23 -0
  56. package/shared/omp-config.ts +81 -0
  57. package/shared/omp-plugins.ts +223 -0
  58. package/shared/omp-settings.ts +207 -0
  59. package/shared/provider-diagnostics.ts +117 -0
  60. package/shared/provider-image.ts +160 -0
  61. package/shared/quota.ts +22 -0
  62. package/shared/sessions.ts +23 -0
  63. 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&apos;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
+ }