@omercnet/paseo-omp 0.2.1-next.72.1

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