@omercnet/paseo-omp 0.3.0-next.104.1 → 0.3.0-next.106.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/client/omp-config-surface.tsx +80 -10
- 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/docs/configuration.md +1 -1
- package/docs/images/model-role-picker-compact.webp +0 -0
- package/docs/images/model-role-picker-desktop.webp +0 -0
- package/index.server.ts +3 -0
- package/package.json +1 -1
- package/server/omp-models.ts +59 -0
- package/server/omp-settings.ts +30 -20
- package/server/provider/omp-rpc.ts +97 -19
- package/shared/omp-models.ts +49 -0
- package/shared/omp-settings.ts +227 -3
|
@@ -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
|
+
}
|
package/docs/configuration.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Configuration
|
|
2
2
|
|
|
3
|
-
Open the global **OMP** sidebar to browse and edit machine-wide state, or open the workspace **OMP** panel from the workspace tab or Explorer to manage project-scoped state. Scalar edits in the global surface use OMP's native `config set` and `config reset` commands. Workspace edits create validated overrides in `<workspace>/.omp/config.yml`; removing an override restores the effective global or default value.
|
|
3
|
+
Open the global **OMP** sidebar to browse and edit machine-wide state, or open the workspace **OMP** panel from the workspace tab or Explorer to manage project-scoped state. Scalar edits in the global surface use OMP's native `config set` and `config reset` commands. The same view provides bounded row editors for `modelRoles`, `retry.fallbackChains`, `cycleOrder`, `task.agentModelOverrides`, `task.agentServiceTierOverrides`, `task.agentPrewalk`, and `task.agentAdvisor` when OMP includes them in its native settings catalog. Workspace edits create validated overrides in `<workspace>/.omp/config.yml`; removing an override restores the effective global or default value. Other arrays and records, and all credentials, remain read-only.
|
|
4
4
|
|
|
5
5
|
The **Plugin** tab documents the supported `omp-plugin` launch options, including names-only inherited environment configuration. Paseo's public plugin API does not expose the effective provider options for active launches, so the tab does not claim profile values are active. Choose **OMP Plugin** when creating an agent. Model, mode, thinking level, system prompt, persistence, MCP servers, workspace, and agent environment use Paseo's standard provider controls.
|
|
6
6
|
|
|
Binary file
|
|
Binary file
|
package/index.server.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "./server/mcp-browser";
|
|
7
7
|
import { resolveListOmpMemory } from "./server/memory";
|
|
8
8
|
import { resolveListOmpConfig } from "./server/omp-config";
|
|
9
|
+
import { resolveListOmpModels } from "./server/omp-models";
|
|
9
10
|
import {
|
|
10
11
|
resolveInspectOmpPluginConfig,
|
|
11
12
|
resolveListOmpPlugins,
|
|
@@ -29,6 +30,7 @@ import { listHubProcesses, tailHubLog } from "./shared/hub";
|
|
|
29
30
|
import { openOmpMcpAuthorizationInPaseoBrowser } from "./shared/mcp";
|
|
30
31
|
import { listOmpMemory } from "./shared/memory";
|
|
31
32
|
import { listOmpConfig } from "./shared/omp-config";
|
|
33
|
+
import { listOmpModels } from "./shared/omp-models";
|
|
32
34
|
import {
|
|
33
35
|
inspectOmpPluginConfig,
|
|
34
36
|
listOmpPlugins,
|
|
@@ -59,6 +61,7 @@ export default function contribute(server: PluginServerContext) {
|
|
|
59
61
|
server.handle(listOmpMemory, scoped(resolveListOmpMemory));
|
|
60
62
|
server.handle(listOmpSessions, scoped(resolveListOmpSessions));
|
|
61
63
|
server.handle(listOmpConfig, scoped(resolveListOmpConfig));
|
|
64
|
+
server.handle(listOmpModels, scoped(resolveListOmpModels));
|
|
62
65
|
server.handle(listOmpPlugins, scoped(resolveListOmpPlugins));
|
|
63
66
|
server.handle(inspectOmpPluginConfig, scoped(resolveInspectOmpPluginConfig));
|
|
64
67
|
server.handle(mutateOmpPlugin, scoped(resolveMutateOmpPlugin));
|
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
4
|
+
import type { listOmpModels, OmpModelCandidate, OmpModelListResult } from "../shared/omp-models";
|
|
5
|
+
import { currentOmpEnvironment } from "./paths";
|
|
6
|
+
import {
|
|
7
|
+
type OmpModel,
|
|
8
|
+
OmpRpcRuntime,
|
|
9
|
+
type OmpRuntime,
|
|
10
|
+
type OmpRuntimeSession,
|
|
11
|
+
} from "./provider/omp-rpc";
|
|
12
|
+
import { OmpCleanupFailure, OmpPublicDataSerializer, OmpPublicError } from "./provider/security";
|
|
13
|
+
|
|
14
|
+
function mapOmpModel(model: OmpModel, serializer: OmpPublicDataSerializer): OmpModelCandidate {
|
|
15
|
+
const provider = serializer.text(model.provider, 256);
|
|
16
|
+
const id = serializer.text(model.id, 256);
|
|
17
|
+
return {
|
|
18
|
+
selector: `${provider}/${id}`,
|
|
19
|
+
provider,
|
|
20
|
+
id,
|
|
21
|
+
...(model.name !== undefined ? { name: serializer.text(model.name, 256) } : {}),
|
|
22
|
+
reasoning: model.reasoning === true,
|
|
23
|
+
input: (model.input ?? []).map((value) => serializer.text(value, 256)),
|
|
24
|
+
...(model.contextWindow !== undefined ? { contextWindow: model.contextWindow } : {}),
|
|
25
|
+
thinkingLevels: (model.thinking?.efforts ?? []).map((value) => serializer.text(value, 32)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function closeModelCatalogSession(session: OmpRuntimeSession): Promise<void> {
|
|
30
|
+
const cleanup = session.close();
|
|
31
|
+
try {
|
|
32
|
+
await cleanup;
|
|
33
|
+
} catch {
|
|
34
|
+
throw new OmpCleanupFailure("OMP model catalog cleanup failed", cleanup);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function resolveListOmpModels(
|
|
39
|
+
input: RpcInput<typeof listOmpModels>,
|
|
40
|
+
runtime: OmpRuntime = new OmpRpcRuntime({ environment: currentOmpEnvironment() }),
|
|
41
|
+
): Promise<OmpModelListResult> {
|
|
42
|
+
const cwd = input.cwd ?? homedir();
|
|
43
|
+
if (!isAbsolute(cwd) || cwd.includes("\0")) {
|
|
44
|
+
throw new OmpPublicError("The workspace path is invalid.");
|
|
45
|
+
}
|
|
46
|
+
const session = await runtime.startSession({
|
|
47
|
+
cwd,
|
|
48
|
+
environment: currentOmpEnvironment(),
|
|
49
|
+
noSession: true,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
const serializer = new OmpPublicDataSerializer();
|
|
53
|
+
return {
|
|
54
|
+
models: (await session.getAvailableModels()).map((model) => mapOmpModel(model, serializer)),
|
|
55
|
+
};
|
|
56
|
+
} finally {
|
|
57
|
+
await closeModelCatalogSession(session);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/server/omp-settings.ts
CHANGED
|
@@ -5,12 +5,13 @@ import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
|
5
5
|
import type { RpcInput } from "@getpaseo/plugin";
|
|
6
6
|
import { parseDocument, parse as parseYaml } from "yaml";
|
|
7
7
|
import {
|
|
8
|
+
isOmpStructuredSettingPath,
|
|
8
9
|
type listOmpSettings,
|
|
9
10
|
OMP_SETTINGS_CATALOG_VERSION,
|
|
10
|
-
type OmpScalarValue,
|
|
11
11
|
type OmpSetting,
|
|
12
12
|
type OmpSettingType,
|
|
13
13
|
OmpSettingTypeSchema,
|
|
14
|
+
parseOmpStructuredSettingValue,
|
|
14
15
|
type updateOmpSettings,
|
|
15
16
|
} from "../shared/omp-settings";
|
|
16
17
|
import { SerialMutationQueue } from "./mutation-queue";
|
|
@@ -305,14 +306,28 @@ async function loadCatalog(
|
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
function
|
|
309
|
-
if (
|
|
310
|
-
|
|
309
|
+
function serializeSettingValue(setting: OmpSetting, value: unknown): string | null {
|
|
310
|
+
if (isOmpStructuredSettingPath(setting.path)) {
|
|
311
|
+
const expectedType = setting.path === "cycleOrder" ? "array" : "record";
|
|
312
|
+
if (setting.type !== expectedType) return null;
|
|
313
|
+
const parsed = parseOmpStructuredSettingValue(setting.path, value);
|
|
314
|
+
return parsed === undefined ? null : JSON.stringify(parsed);
|
|
315
|
+
}
|
|
316
|
+
if (setting.type === "boolean") return typeof value === "boolean" ? String(value) : null;
|
|
317
|
+
if (setting.type === "number")
|
|
311
318
|
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
312
|
-
if (type === "string" || type === "enum")
|
|
319
|
+
if (setting.type === "string" || setting.type === "enum")
|
|
320
|
+
return typeof value === "string" ? value : null;
|
|
313
321
|
return null;
|
|
314
322
|
}
|
|
315
323
|
|
|
324
|
+
function isEditableSetting(setting: OmpSetting): boolean {
|
|
325
|
+
if (setting.redacted) return false;
|
|
326
|
+
if (["boolean", "number", "string", "enum"].includes(setting.type)) return true;
|
|
327
|
+
if (!isOmpStructuredSettingPath(setting.path)) return false;
|
|
328
|
+
return setting.type === (setting.path === "cycleOrder" ? "array" : "record");
|
|
329
|
+
}
|
|
330
|
+
|
|
316
331
|
export async function listOmpSettingsWithDependencies(
|
|
317
332
|
input: RpcInput<typeof listOmpSettings>,
|
|
318
333
|
dependencies: OmpSettingsDependencies,
|
|
@@ -373,26 +388,22 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
373
388
|
if (input.cwd) {
|
|
374
389
|
for (const change of input.changes) {
|
|
375
390
|
const setting = byPath.get(change.path);
|
|
376
|
-
if (
|
|
377
|
-
!setting ||
|
|
378
|
-
setting.redacted ||
|
|
379
|
-
!["boolean", "number", "string", "enum"].includes(setting.type)
|
|
380
|
-
) {
|
|
391
|
+
if (!setting || !isEditableSetting(setting)) {
|
|
381
392
|
return {
|
|
382
393
|
conflict: false,
|
|
383
394
|
appliedPaths: [],
|
|
384
395
|
failed: {
|
|
385
396
|
path: change.path,
|
|
386
|
-
message: "This setting cannot be edited
|
|
397
|
+
message: "This setting cannot be edited through the focused configuration editor.",
|
|
387
398
|
},
|
|
388
399
|
catalog: current,
|
|
389
400
|
};
|
|
390
401
|
}
|
|
391
|
-
if (change.operation === "set" &&
|
|
402
|
+
if (change.operation === "set" && serializeSettingValue(setting, change.value) === null) {
|
|
392
403
|
return {
|
|
393
404
|
conflict: false,
|
|
394
405
|
appliedPaths: [],
|
|
395
|
-
failed: { path: change.path, message: `
|
|
406
|
+
failed: { path: change.path, message: `Invalid ${change.path} value.` },
|
|
396
407
|
catalog: current,
|
|
397
408
|
};
|
|
398
409
|
}
|
|
@@ -440,15 +451,14 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
440
451
|
const appliedPaths: string[] = [];
|
|
441
452
|
for (const change of input.changes) {
|
|
442
453
|
const setting = byPath.get(change.path);
|
|
443
|
-
if (
|
|
444
|
-
!setting ||
|
|
445
|
-
setting.redacted ||
|
|
446
|
-
!["boolean", "number", "string", "enum"].includes(setting.type)
|
|
447
|
-
) {
|
|
454
|
+
if (!setting || !isEditableSetting(setting)) {
|
|
448
455
|
return {
|
|
449
456
|
conflict: false,
|
|
450
457
|
appliedPaths,
|
|
451
|
-
failed: {
|
|
458
|
+
failed: {
|
|
459
|
+
path: change.path,
|
|
460
|
+
message: "This setting cannot be edited through the focused configuration editor.",
|
|
461
|
+
},
|
|
452
462
|
catalog: await loadCatalog(executable, dependencies, input.cwd),
|
|
453
463
|
};
|
|
454
464
|
}
|
|
@@ -456,7 +466,7 @@ export async function updateOmpSettingsWithDependencies(
|
|
|
456
466
|
if (change.operation === "reset") {
|
|
457
467
|
args = ["reset", change.path];
|
|
458
468
|
} else {
|
|
459
|
-
const value =
|
|
469
|
+
const value = serializeSettingValue(setting, change.value);
|
|
460
470
|
args = value === null ? null : ["set", change.path, "--json", "--", value];
|
|
461
471
|
}
|
|
462
472
|
if (!args) {
|