@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.
- package/README.md +19 -5
- package/client/omp-config-surface.tsx +243 -24
- package/client/omp-config-views.ts +24 -0
- package/client/omp-model-picker-state.ts +145 -0
- package/client/omp-model-picker.tsx +282 -0
- package/client/omp-routing-editor.tsx +307 -0
- package/client/support-diagnostics-state.ts +45 -0
- package/index.server.ts +27 -2
- package/package.json +2 -8
- package/paseo-plugin.json +1 -1
- package/server/omp-models.ts +59 -0
- package/server/omp-settings.ts +30 -20
- package/server/operational-failure-diagnostics.ts +76 -0
- package/server/package-version.ts +2 -0
- package/server/protocol-violation-diagnostics.ts +169 -0
- package/server/provider/catalog.ts +39 -10
- package/server/provider/connection.ts +60 -8
- package/server/provider/host-tools.ts +284 -34
- package/server/provider/mcp-transport.ts +2 -1
- package/server/provider/omp-rpc.ts +1011 -107
- package/server/provider/profile-providers.ts +7 -2
- package/server/provider/registration.ts +12 -2
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +45 -11
- package/server/provider/session.ts +200 -58
- package/server/provider/subsessions.ts +311 -73
- package/server/provider/timeline-projector.ts +34 -11
- package/server/support-diagnostics.ts +284 -0
- package/shared/omp-models.ts +49 -0
- package/shared/omp-settings.ts +227 -3
- package/shared/support-diagnostics.ts +32 -0
- package/CHANGELOG.md +0 -113
- package/SUPPORT.md +0 -44
- package/TESTING.md +0 -150
- package/docs/alpha-release-checklist.md +0 -68
- package/docs/configuration.md +0 -126
- package/docs/core-provider-issue-audit.md +0 -109
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +0 -89
- 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
|
+
}
|