@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.
@@ -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
+ }
@@ -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. Arrays, records, and credentials remain read-only in both surfaces.
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
 
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "0.3.0-next.104.1",
3
+ "version": "0.3.0-next.106.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -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
+ }
@@ -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 serializeScalar(type: OmpSettingType, value: OmpScalarValue): string | null {
309
- if (type === "boolean") return typeof value === "boolean" ? String(value) : null;
310
- if (type === "number")
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") return typeof value === "string" ? value : null;
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 as a workspace scalar value.",
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" && serializeScalar(setting.type, change.value) === null) {
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: `Expected a ${setting.type} value.` },
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: { path: change.path, message: "This setting cannot be edited as a scalar value." },
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 = serializeScalar(setting.type, change.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) {