@omercnet/paseo-omp 0.3.0 → 0.4.0-next.114.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 (43) hide show
  1. package/README.md +19 -5
  2. package/client/omp-config-surface.tsx +243 -24
  3. package/client/omp-config-views.ts +24 -0
  4. package/client/omp-model-picker-state.ts +145 -0
  5. package/client/omp-model-picker.tsx +282 -0
  6. package/client/omp-routing-editor.tsx +307 -0
  7. package/client/support-diagnostics-state.ts +45 -0
  8. package/index.server.ts +27 -2
  9. package/package.json +2 -8
  10. package/paseo-plugin.json +1 -1
  11. package/server/omp-models.ts +59 -0
  12. package/server/omp-settings.ts +30 -20
  13. package/server/operational-failure-diagnostics.ts +76 -0
  14. package/server/package-version.ts +2 -0
  15. package/server/protocol-violation-diagnostics.ts +169 -0
  16. package/server/provider/catalog.ts +39 -10
  17. package/server/provider/connection.ts +60 -8
  18. package/server/provider/host-tools.ts +284 -34
  19. package/server/provider/mcp-transport.ts +2 -1
  20. package/server/provider/omp-rpc.ts +1011 -107
  21. package/server/provider/profile-providers.ts +7 -2
  22. package/server/provider/registration.ts +12 -2
  23. package/server/provider/security.ts +8 -10
  24. package/server/provider/session-descriptors.ts +45 -11
  25. package/server/provider/session.ts +200 -58
  26. package/server/provider/subsessions.ts +311 -73
  27. package/server/provider/timeline-projector.ts +34 -11
  28. package/server/support-diagnostics.ts +284 -0
  29. package/shared/omp-models.ts +49 -0
  30. package/shared/omp-settings.ts +227 -3
  31. package/shared/support-diagnostics.ts +32 -0
  32. package/CHANGELOG.md +0 -113
  33. package/SUPPORT.md +0 -44
  34. package/TESTING.md +0 -150
  35. package/docs/alpha-release-checklist.md +0 -68
  36. package/docs/configuration.md +0 -126
  37. package/docs/core-provider-issue-audit.md +0 -109
  38. package/docs/images/mcp-authorization-compact.png +0 -0
  39. package/docs/images/mcp-controls-wide.png +0 -0
  40. package/docs/images/plugin-manager.png +0 -0
  41. package/docs/images/workspace-settings.png +0 -0
  42. package/docs/installation.md +0 -89
  43. package/tsconfig.json +0 -16
@@ -0,0 +1,282 @@
1
+ import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
2
+ import { TextInput } from "@getpaseo/plugin/client/react-native";
3
+ import { useMemo, useState } from "react";
4
+ import type { TextStyle, ViewStyle } from "react-native";
5
+ import { Pressable, Text, View } from "react-native";
6
+ import {
7
+ commitPickerSelection,
8
+ filterModelChoices,
9
+ type OmpModelPickerChoice,
10
+ type OmpModelPickerModel,
11
+ thinkingSelectorChoice,
12
+ } from "./omp-model-picker-state";
13
+
14
+ type OmpModelPickerProps = {
15
+ theme: PluginSurfaceProps["theme"];
16
+ models: readonly OmpModelPickerModel[];
17
+ aliases: readonly OmpModelPickerChoice[];
18
+ role: string;
19
+ disabled: boolean;
20
+ loading: boolean;
21
+ error?: string;
22
+ onSelect(selector: string): void;
23
+ };
24
+
25
+ type OmpModelPickerStyles = {
26
+ root: ViewStyle;
27
+ trigger: ViewStyle;
28
+ triggerText: TextStyle;
29
+ panel: ViewStyle;
30
+ search: TextStyle;
31
+ status: TextStyle;
32
+ error: TextStyle;
33
+ result: ViewStyle;
34
+ resultPressed: ViewStyle;
35
+ titleRow: ViewStyle;
36
+ title: TextStyle;
37
+ selector: TextStyle;
38
+ metadata: TextStyle;
39
+ provenance: TextStyle;
40
+ thinkingRow: ViewStyle;
41
+ thinking: ViewStyle;
42
+ buttonDisabled: ViewStyle;
43
+ thinkingText: TextStyle;
44
+ };
45
+
46
+ function formatContextWindow(tokens?: number): string {
47
+ if (tokens === undefined) return "Context unknown";
48
+ if (tokens >= 1_000_000) return `${Number((tokens / 1_000_000).toFixed(1))}M context`;
49
+ if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}K context`;
50
+ return `${tokens} context`;
51
+ }
52
+
53
+ export function OmpModelPicker({
54
+ theme,
55
+ models,
56
+ aliases,
57
+ role,
58
+ disabled,
59
+ loading,
60
+ error,
61
+ onSelect,
62
+ }: OmpModelPickerProps) {
63
+ const [open, setOpen] = useState(false);
64
+ const [query, setQuery] = useState("");
65
+ const choices = useMemo(
66
+ () => filterModelChoices(models, aliases, query, role),
67
+ [aliases, models, query, role],
68
+ );
69
+ const styles = useMemo(
70
+ () => ({
71
+ root: { gap: 6, width: "100%" as const, maxWidth: 520 },
72
+ trigger: {
73
+ alignSelf: "flex-start" as const,
74
+ paddingHorizontal: 9,
75
+ paddingVertical: 6,
76
+ borderWidth: 1,
77
+ borderColor: open ? theme.colors.accent : theme.colors.border,
78
+ borderRadius: 7,
79
+ backgroundColor: theme.colors.surface1,
80
+ },
81
+ triggerText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" as const },
82
+ panel: {
83
+ gap: 6,
84
+ padding: 8,
85
+ borderWidth: 1,
86
+ borderColor: theme.colors.border,
87
+ borderRadius: 9,
88
+ backgroundColor: theme.colors.surface0,
89
+ },
90
+ search: {
91
+ color: theme.colors.foreground,
92
+ borderWidth: 1,
93
+ borderColor: theme.colors.border,
94
+ borderRadius: 7,
95
+ backgroundColor: theme.colors.surface1,
96
+ paddingHorizontal: 9,
97
+ paddingVertical: 6,
98
+ fontSize: 13,
99
+ },
100
+ status: { color: theme.colors.foregroundMuted, fontSize: 12 },
101
+ error: { color: theme.colors.statusDanger, fontSize: 12 },
102
+ result: {
103
+ gap: 3,
104
+ paddingHorizontal: 9,
105
+ paddingVertical: 7,
106
+ borderWidth: 1,
107
+ borderColor: theme.colors.border,
108
+ borderRadius: 7,
109
+ backgroundColor: theme.colors.surface1,
110
+ },
111
+ resultPressed: { borderColor: theme.colors.accent, backgroundColor: theme.colors.surface2 },
112
+ titleRow: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 6 },
113
+ title: { color: theme.colors.foreground, fontSize: 12, fontWeight: "700" as const },
114
+ selector: { color: theme.colors.foregroundMuted, fontSize: 11, fontFamily: "monospace" },
115
+ metadata: { color: theme.colors.foregroundMuted, fontSize: 11 },
116
+ provenance: { color: theme.colors.accent, fontSize: 11, fontWeight: "600" as const },
117
+ thinkingRow: {
118
+ flexDirection: "row" as const,
119
+ flexWrap: "wrap" as const,
120
+ gap: 5,
121
+ marginTop: 3,
122
+ },
123
+ thinking: {
124
+ paddingHorizontal: 7,
125
+ paddingVertical: 4,
126
+ borderWidth: 1,
127
+ borderColor: theme.colors.border,
128
+ borderRadius: 999,
129
+ backgroundColor: theme.colors.surface0,
130
+ },
131
+ buttonDisabled: { opacity: 0.45 },
132
+ thinkingText: { color: theme.colors.foreground, fontSize: 10, fontWeight: "600" as const },
133
+ }),
134
+ [open, theme],
135
+ );
136
+
137
+ const choose = (selector: string) => {
138
+ if (!commitPickerSelection(disabled, selector, onSelect)) return;
139
+ setOpen(false);
140
+ setQuery("");
141
+ };
142
+
143
+ return (
144
+ <View style={styles.root}>
145
+ <Pressable
146
+ accessibilityRole="button"
147
+ accessibilityLabel={`${open ? "Close" : "Open"} model picker for ${role || "this row"}`}
148
+ accessibilityState={{ disabled, expanded: open }}
149
+ disabled={disabled}
150
+ onPress={() => setOpen((current) => !current)}
151
+ style={styles.trigger}
152
+ >
153
+ <Text style={styles.triggerText}>{open ? "Close models" : "Choose model"}</Text>
154
+ </Pressable>
155
+ {open ? (
156
+ <View accessibilityLabel={`Model choices for ${role || "this row"}`} style={styles.panel}>
157
+ <TextInput
158
+ accessibilityLabel="Search available OMP models"
159
+ autoFocus
160
+ accessibilityRole="combobox"
161
+ accessibilityState={{ expanded: true }}
162
+ editable={!disabled}
163
+ placeholder="Search provider, model, selector, or thinking level"
164
+ placeholderTextColor={theme.colors.foregroundMuted}
165
+ value={query}
166
+ onChangeText={setQuery}
167
+ onKeyPress={(event) => {
168
+ if (event.nativeEvent.key === "Escape") setOpen(false);
169
+ }}
170
+ style={styles.search}
171
+ />
172
+ {loading ? <Text style={styles.status}>Loading OMP models…</Text> : null}
173
+ {error ? (
174
+ <Text accessibilityRole="alert" style={styles.error}>
175
+ {error}
176
+ </Text>
177
+ ) : null}
178
+ {!loading && !error && models.length === 0 && aliases.length > 0 ? (
179
+ <Text style={styles.status}>
180
+ OMP reported no concrete models. Configured role aliases remain available.
181
+ </Text>
182
+ ) : null}
183
+ {!loading && choices.length === 0 ? (
184
+ <Text style={styles.status}>No models or role aliases match this search.</Text>
185
+ ) : null}
186
+ {choices.map((choice) =>
187
+ choice.kind === "alias" ? (
188
+ <Pressable
189
+ key={choice.selector}
190
+ accessibilityRole="button"
191
+ accessibilityLabel={`Select role alias ${choice.selector}`}
192
+ accessibilityState={{ disabled }}
193
+ disabled={disabled}
194
+ onPress={() => choose(choice.selector)}
195
+ style={({ pressed }) => [styles.result, pressed ? styles.resultPressed : null]}
196
+ >
197
+ <Text style={styles.title}>Role alias · {choice.label}</Text>
198
+ <Text style={styles.selector}>{choice.selector}</Text>
199
+ </Pressable>
200
+ ) : (
201
+ <ModelResult
202
+ key={choice.model.selector}
203
+ choice={choice}
204
+ styles={styles}
205
+ disabled={disabled}
206
+ onSelect={choose}
207
+ />
208
+ ),
209
+ )}
210
+ </View>
211
+ ) : null}
212
+ </View>
213
+ );
214
+ }
215
+
216
+ function ModelResult({
217
+ choice,
218
+ styles,
219
+ disabled,
220
+ onSelect,
221
+ }: {
222
+ choice: Extract<OmpModelPickerChoice, { kind: "model" }>;
223
+ styles: OmpModelPickerStyles;
224
+ onSelect(selector: string): void;
225
+ disabled: boolean;
226
+ }) {
227
+ const { model } = choice;
228
+ const capabilityText = [
229
+ model.reasoning ? "Reasoning" : "Standard",
230
+ model.imageInput ? "Image input" : "Text input",
231
+ formatContextWindow(model.contextWindow),
232
+ ].join(" · ");
233
+ return (
234
+ <View style={styles.result}>
235
+ <Pressable
236
+ accessibilityRole="button"
237
+ accessibilityLabel={`Select ${model.label}, ${model.selector}`}
238
+ accessibilityState={{ disabled }}
239
+ disabled={disabled}
240
+ onPress={() => onSelect(model.selector)}
241
+ style={({ pressed }) => (pressed ? styles.resultPressed : undefined)}
242
+ >
243
+ <View style={styles.titleRow}>
244
+ <Text style={styles.title}>{model.label}</Text>
245
+ <Text style={styles.metadata}>{model.provider}</Text>
246
+ </View>
247
+ <Text style={styles.selector}>{model.selector}</Text>
248
+ <Text style={styles.metadata}>{capabilityText}</Text>
249
+ {choice.recommended ? (
250
+ <Text style={styles.provenance}>Matches this role’s capabilities</Text>
251
+ ) : null}
252
+ </Pressable>
253
+ {model.thinkingLevels.length > 0 ? (
254
+ <View accessibilityLabel={`Thinking levels for ${model.label}`} style={styles.thinkingRow}>
255
+ {model.thinkingLevels.map((level) => {
256
+ const thinkingChoice = thinkingSelectorChoice(model.selector, level);
257
+ const thinkingDisabled = disabled || !thinkingChoice.available;
258
+ return (
259
+ <Pressable
260
+ key={level}
261
+ accessibilityRole="button"
262
+ accessibilityLabel={
263
+ thinkingChoice.available
264
+ ? `Select ${model.label} with ${level} thinking`
265
+ : `${level} thinking unavailable: selector exceeds 513 UTF-8 bytes`
266
+ }
267
+ accessibilityState={{ disabled: thinkingDisabled }}
268
+ disabled={thinkingDisabled}
269
+ onPress={() => onSelect(thinkingChoice.selector)}
270
+ style={[styles.thinking, thinkingDisabled ? styles.buttonDisabled : null]}
271
+ >
272
+ <Text style={styles.thinkingText}>
273
+ {thinkingChoice.available ? level : `${level} · too long`}
274
+ </Text>
275
+ </Pressable>
276
+ );
277
+ })}
278
+ </View>
279
+ ) : null}
280
+ </View>
281
+ );
282
+ }
@@ -0,0 +1,307 @@
1
+ import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
2
+ import { TextInput } from "@getpaseo/plugin/client/react-native";
3
+ import { useState } from "react";
4
+ import { Pressable, Text, View } from "react-native";
5
+ import {
6
+ formatOmpSettingLabel,
7
+ isOmpStructuredSettingPath,
8
+ type OmpSetting,
9
+ parseOmpStructuredSettingValue,
10
+ } from "../shared/omp-settings";
11
+ import type { OmpConfigStyles } from "./omp-config-surface";
12
+ import { OmpModelPicker } from "./omp-model-picker";
13
+ import {
14
+ aliasChoices,
15
+ type OmpModelPickerModel,
16
+ updatePickerValue,
17
+ } from "./omp-model-picker-state";
18
+
19
+ type RoutingRecord = Record<string, unknown>;
20
+
21
+ export type OmpModelCatalogState = {
22
+ models: readonly OmpModelPickerModel[];
23
+ loading: boolean;
24
+ error?: string;
25
+ };
26
+
27
+ type StructuredRoutingEditorProps = {
28
+ setting: OmpSetting;
29
+ value: unknown;
30
+ disabled: boolean;
31
+ resetLabel: string;
32
+ showReset: boolean;
33
+ resetPending: boolean;
34
+ styles: OmpConfigStyles;
35
+ theme: PluginSurfaceProps["theme"];
36
+ modelCatalog: OmpModelCatalogState;
37
+ modelRoles: Readonly<Record<string, unknown>>;
38
+ onSet(value: unknown): void;
39
+ onReset(): void;
40
+ };
41
+
42
+ function asRecord(value: unknown): RoutingRecord {
43
+ return value !== null && typeof value === "object" && !Array.isArray(value)
44
+ ? (value as RoutingRecord)
45
+ : {};
46
+ }
47
+
48
+ function asStringArray(value: unknown): string[] {
49
+ return Array.isArray(value) ? value.map((item) => (typeof item === "string" ? item : "")) : [];
50
+ }
51
+
52
+ function replaceRecordKey(record: RoutingRecord, oldKey: string, newKey: string): RoutingRecord {
53
+ const next: RoutingRecord = {};
54
+ for (const [key, value] of Object.entries(record)) next[key === oldKey ? newKey : key] = value;
55
+ return next;
56
+ }
57
+
58
+ function removeRecordKey(record: RoutingRecord, removedKey: string): RoutingRecord {
59
+ return Object.fromEntries(Object.entries(record).filter(([key]) => key !== removedKey));
60
+ }
61
+
62
+ function nextRecordKey(record: RoutingRecord, prefix: string): string {
63
+ let suffix = 1;
64
+ while (Object.hasOwn(record, `${prefix}${suffix}`)) suffix += 1;
65
+ return `${prefix}${suffix}`;
66
+ }
67
+
68
+ function RowButton({
69
+ label,
70
+ disabled,
71
+ styles,
72
+ onPress,
73
+ }: {
74
+ label: string;
75
+ disabled: boolean;
76
+ styles: OmpConfigStyles;
77
+ onPress(): void;
78
+ }) {
79
+ return (
80
+ <Pressable
81
+ accessibilityRole="button"
82
+ accessibilityLabel={label}
83
+ disabled={disabled}
84
+ onPress={onPress}
85
+ style={styles.editorAction}
86
+ >
87
+ <Text style={styles.editorActionText}>{label}</Text>
88
+ </Pressable>
89
+ );
90
+ }
91
+
92
+ function OrderedRoleEditor({
93
+ value,
94
+ disabled,
95
+ styles,
96
+ onSet,
97
+ }: Pick<StructuredRoutingEditorProps, "value" | "disabled" | "styles" | "onSet">) {
98
+ const roles = asStringArray(value);
99
+ return (
100
+ <View style={styles.recordList}>
101
+ {roles.map((role, index) => (
102
+ // biome-ignore lint/suspicious/noArrayIndexKey: ordered rows are edited by position and must keep input identity while their value changes.
103
+ <View key={index} style={styles.recordRow}>
104
+ <Text style={styles.recordKey}>{index + 1}</Text>
105
+ <TextInput
106
+ accessibilityLabel={`Cycle role ${index + 1}`}
107
+ editable={!disabled}
108
+ value={role}
109
+ onChangeText={(nextRole) =>
110
+ onSet(roles.map((current, position) => (position === index ? nextRole : current)))
111
+ }
112
+ style={styles.scalarInput}
113
+ />
114
+ <RowButton
115
+ label={`Remove cycle role ${index + 1}`}
116
+ disabled={disabled}
117
+ styles={styles}
118
+ onPress={() => onSet(roles.filter((_, position) => position !== index))}
119
+ />
120
+ </View>
121
+ ))}
122
+ <RowButton
123
+ label="Add cycle role"
124
+ disabled={disabled}
125
+ styles={styles}
126
+ onPress={() => onSet([...roles, ""])}
127
+ />
128
+ </View>
129
+ );
130
+ }
131
+
132
+ function RecordRoutingEditor({
133
+ setting,
134
+ value,
135
+ disabled,
136
+ styles,
137
+ theme,
138
+ modelCatalog,
139
+ modelRoles,
140
+ onSet,
141
+ }: Pick<
142
+ StructuredRoutingEditorProps,
143
+ "setting" | "value" | "disabled" | "styles" | "onSet" | "theme" | "modelCatalog" | "modelRoles"
144
+ >) {
145
+ const record = asRecord(value);
146
+ const [keyErrors, setKeyErrors] = useState<Record<string, string>>({});
147
+ const entries = Object.entries(record);
148
+ const fallbackChains = setting.path === "retry.fallbackChains";
149
+ const agentModels = setting.path === "task.agentModelOverrides";
150
+ const keyLabel = setting.path.startsWith("task.")
151
+ ? "Agent"
152
+ : fallbackChains
153
+ ? "Role or model"
154
+ : "Role";
155
+ const valueLabel = fallbackChains
156
+ ? "Fallback selectors, comma separated"
157
+ : agentModels
158
+ ? "Model selectors, comma separated"
159
+ : setting.path === "task.agentServiceTierOverrides"
160
+ ? "Service tier"
161
+ : setting.path === "task.agentPrewalk" || setting.path === "task.agentAdvisor"
162
+ ? "on, off, or model selector"
163
+ : "Model selector";
164
+ const supportsModelPicker = setting.path !== "task.agentServiceTierOverrides";
165
+
166
+ return (
167
+ <View style={styles.recordList}>
168
+ {entries.map(([key, rawValue], index) => {
169
+ const textValue = Array.isArray(rawValue)
170
+ ? rawValue.filter((item): item is string => typeof item === "string").join(", ")
171
+ : typeof rawValue === "string"
172
+ ? rawValue
173
+ : "";
174
+ return (
175
+ // biome-ignore lint/suspicious/noArrayIndexKey: record keys are editable, so row position preserves input identity.
176
+ <View key={index} style={styles.recordRow}>
177
+ <TextInput
178
+ accessibilityLabel={`${formatOmpSettingLabel(setting.path)} ${keyLabel} ${index + 1}`}
179
+ editable={!disabled}
180
+ value={key}
181
+ onChangeText={(newKey) => {
182
+ if (newKey !== key && Object.hasOwn(record, newKey)) {
183
+ setKeyErrors((current) => ({
184
+ ...current,
185
+ [key]: `Another entry already uses ${newKey}.`,
186
+ }));
187
+ return;
188
+ }
189
+ setKeyErrors((current) => {
190
+ const next = { ...current };
191
+ delete next[key];
192
+ delete next[newKey];
193
+ return next;
194
+ });
195
+ onSet(replaceRecordKey(record, key, newKey));
196
+ }}
197
+ style={styles.scalarInput}
198
+ />
199
+ {keyErrors[key] ? (
200
+ <Text accessibilityRole="alert" style={styles.error}>
201
+ {keyErrors[key]}
202
+ </Text>
203
+ ) : null}
204
+ <TextInput
205
+ accessibilityLabel={`${formatOmpSettingLabel(setting.path)} ${valueLabel} ${index + 1}`}
206
+ editable={!disabled}
207
+ value={textValue}
208
+ onChangeText={(newValue) => {
209
+ const next = { ...record };
210
+ if (fallbackChains || agentModels) {
211
+ const selectors = newValue
212
+ .split(",")
213
+ .map((selector) => selector.trim())
214
+ .filter(Boolean);
215
+ next[key] = agentModels && selectors.length === 1 ? selectors[0] : selectors;
216
+ } else {
217
+ next[key] = newValue;
218
+ }
219
+ onSet(next);
220
+ }}
221
+ style={styles.scalarInput}
222
+ />
223
+ {supportsModelPicker ? (
224
+ <OmpModelPicker
225
+ theme={theme}
226
+ models={modelCatalog.models}
227
+ aliases={
228
+ setting.path === "modelRoles"
229
+ ? []
230
+ : aliasChoices(modelRoles, fallbackChains ? key : undefined)
231
+ }
232
+ role={
233
+ setting.path === "task.agentAdvisor"
234
+ ? "advisor"
235
+ : setting.path === "task.agentPrewalk"
236
+ ? ""
237
+ : key.replace(/^@/u, "")
238
+ }
239
+ disabled={disabled}
240
+ loading={modelCatalog.loading}
241
+ error={modelCatalog.error}
242
+ onSelect={(selector) => {
243
+ const next = { ...record };
244
+ if (fallbackChains || agentModels) {
245
+ next[key] = updatePickerValue(
246
+ Array.isArray(rawValue)
247
+ ? rawValue.filter((item): item is string => typeof item === "string")
248
+ : typeof rawValue === "string"
249
+ ? rawValue
250
+ : [],
251
+ selector,
252
+ "append",
253
+ );
254
+ } else {
255
+ next[key] = updatePickerValue(textValue, selector, "replace");
256
+ }
257
+ onSet(next);
258
+ }}
259
+ />
260
+ ) : null}
261
+ <RowButton
262
+ label={`Remove ${key || "routing entry"}`}
263
+ disabled={disabled}
264
+ styles={styles}
265
+ onPress={() => onSet(removeRecordKey(record, key))}
266
+ />
267
+ </View>
268
+ );
269
+ })}
270
+ <RowButton
271
+ label={`Add ${setting.path === "modelRoles" ? "model role" : fallbackChains ? "fallback chain" : "agent override"}`}
272
+ disabled={disabled}
273
+ styles={styles}
274
+ onPress={() => {
275
+ const key = nextRecordKey(record, setting.path.startsWith("task.") ? "agent" : "role");
276
+ onSet({ ...record, [key]: fallbackChains || agentModels ? [] : "" });
277
+ }}
278
+ />
279
+ </View>
280
+ );
281
+ }
282
+
283
+ export function StructuredRoutingEditor(props: StructuredRoutingEditorProps) {
284
+ if (!isOmpStructuredSettingPath(props.setting.path)) return null;
285
+ const parsed = parseOmpStructuredSettingValue(props.setting.path, props.value);
286
+ return (
287
+ <View style={props.styles.recordList}>
288
+ {props.resetPending ? (
289
+ <Text style={props.styles.muted}>{props.resetLabel} when changes are applied</Text>
290
+ ) : props.setting.path === "cycleOrder" ? (
291
+ <OrderedRoleEditor {...props} />
292
+ ) : (
293
+ <RecordRoutingEditor {...props} />
294
+ )}
295
+ {!props.resetPending && parsed === undefined ? (
296
+ <Text accessibilityRole="alert" style={props.styles.error}>
297
+ Complete every routing name and value before applying changes.
298
+ </Text>
299
+ ) : null}
300
+ {props.showReset ? (
301
+ <Pressable accessibilityRole="button" disabled={props.disabled} onPress={props.onReset}>
302
+ <Text style={props.styles.resetAction}>{props.resetLabel}</Text>
303
+ </Pressable>
304
+ ) : null}
305
+ </View>
306
+ );
307
+ }
@@ -0,0 +1,45 @@
1
+ import type { OmpStore } from "../shared/omp-store";
2
+
3
+ type SupportReportInput = { store?: OmpStore; cwd?: string; force?: boolean };
4
+ type SupportReportOutput = { report: string };
5
+
6
+ export async function refreshSupportReport(
7
+ loadReport: (input: SupportReportInput) => Promise<SupportReportOutput>,
8
+ input: Omit<SupportReportInput, "force">,
9
+ ): Promise<SupportReportOutput> {
10
+ return await loadReport({ ...input, force: true });
11
+ }
12
+
13
+ export type SupportReportCopyState = "idle" | "copying" | "copied" | "error";
14
+
15
+ export interface SupportDiagnosticsViewState {
16
+ refreshLabel: string;
17
+ copyLabel: string;
18
+ copyDisabled: boolean;
19
+ loadingMessage: string | null;
20
+ reportError: string | null;
21
+ copyFeedback: string | null;
22
+ }
23
+
24
+ /** Maps request and copy state to the exact accessible feedback rendered by the Help tab. */
25
+ export function supportDiagnosticsViewState(input: {
26
+ loading: boolean;
27
+ refreshing: boolean;
28
+ hasReport: boolean;
29
+ reportFailed: boolean;
30
+ copyState: SupportReportCopyState;
31
+ }): SupportDiagnosticsViewState {
32
+ return {
33
+ refreshLabel: input.refreshing ? "Refreshing…" : "Refresh",
34
+ copyLabel: input.copyState === "copying" ? "Copying…" : "Copy report",
35
+ copyDisabled: !input.hasReport || input.copyState === "copying",
36
+ loadingMessage: input.loading ? "Collecting OMP diagnostics…" : null,
37
+ reportError: input.reportFailed ? "Could not collect OMP diagnostics." : null,
38
+ copyFeedback:
39
+ input.copyState === "copied"
40
+ ? "Report copied."
41
+ : input.copyState === "error"
42
+ ? "Could not copy. Select the report text and copy it manually."
43
+ : null,
44
+ };
45
+ }