@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
package/README.md
CHANGED
|
@@ -4,16 +4,30 @@ Community OMP integration for Paseo. The plugin registers the distinct `omp-plug
|
|
|
4
4
|
|
|
5
5
|
> **Alpha preview:** persistence and protocol contracts are tested, but upgrades may still require re-importing sessions created by an earlier preview.
|
|
6
6
|
|
|
7
|
+
## Screenshots
|
|
8
|
+
|
|
9
|
+
Both PNGs come from the plugin's isolated Paseo development daemon at 2× pixel density. Browser
|
|
10
|
+
DevTools rewrites local filesystem paths and verifies that no private organization names remain
|
|
11
|
+
before capture.
|
|
12
|
+
|
|
13
|
+
### Native plugin manager
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
### Typed OMP configuration
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
7
21
|
## Quick start
|
|
8
22
|
|
|
9
|
-
Requirements: Paseo `>=0.
|
|
23
|
+
Requirements: Paseo `>=0.9.0-beta.1 <0.10.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Paseo 0.9 is required for the Help report's supported copy and external-link APIs, nested provider-subagent ancestry, spawning-tool links, npm-managed installation, and the complete client integration.
|
|
10
24
|
|
|
11
25
|
```bash
|
|
12
26
|
paseo plugin install npm:@omercnet/paseo-omp@<version>
|
|
13
27
|
paseo plugin ls paseo-omp
|
|
14
28
|
```
|
|
15
29
|
|
|
16
|
-
Open the **OMP** sidebar to review the provider-profile contract, choose which composer pills appear, browse and edit native scalar settings, manage OMP-native plugins, verify runtime health, and
|
|
30
|
+
Open the **OMP** sidebar to review the provider-profile contract, choose which composer pills appear, browse and edit native scalar settings, manage OMP-native plugins, verify runtime health, and generate a bounded support report under **Help**. The same tabs are available in the workspace OMP panel, except for the global Composer preference tab. Then create an agent and select **OMP Plugin**. Its optional **MCP** composer control runs OMP-native management commands in the current session; setup questions and authorization stay in that chat timeline, where OAuth can open in a workspace-scoped Paseo Browser or on the current device. Composer pill preferences are shared by clients connected to the same host; normal use requires no other plugin-specific settings.
|
|
17
31
|
|
|
18
32
|
- [Install, update, rollback, and local development](docs/installation.md)
|
|
19
33
|
- [Configuration and every provider option](docs/configuration.md)
|
|
@@ -22,13 +36,13 @@ Open the **OMP** sidebar to review the provider-profile contract, choose which c
|
|
|
22
36
|
- [Core-provider issue and parity audit](docs/core-provider-issue-audit.md)
|
|
23
37
|
- [Alpha release checklist](docs/alpha-release-checklist.md)
|
|
24
38
|
|
|
25
|
-
The plugin uses public Paseo 0.
|
|
39
|
+
The plugin uses public Paseo 0.9 beta provider and client contracts and registers distinct `omp-plugin` and `omp-plugin-<profile>` identities; it does not modify the bundled `omp` provider.
|
|
26
40
|
|
|
27
41
|
## Named OMP profiles
|
|
28
42
|
|
|
29
43
|
At startup the plugin discovers directory names under `~/.omp/profiles/` (or `PI_CONFIG_DIR`) and registers **OMP · <profile>** for each valid lowercase name. Select that provider when creating a profile-backed agent. Discovery, launch, recovery and persisted session listing share its fixed profile and session root. Matching command wrappers, including Doppler, remain supported; conflicting profile or session-directory overrides fail before launch. Plain `env NAME=value omp` wrappers are supported, but environment-control flags such as `env -i` and `env -u` are rejected because they can discard the selected store. Profile-specific XDG roots cannot be overridden by per-launch environment values. Reload the plugin after adding a profile.
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
Separate provider identities let the model picker request the correct profile catalog before an agent exists. Existing `omp-plugin` and bundled `omp` agents retain their provider; they are not migrated automatically. Profile names use OMP's lowercase grammar, including dotted names such as `team.prod`.
|
|
32
46
|
|
|
33
47
|
The OMP sidebar and memory panel expose an explicit store selector. Settings, plugins, quota, history, memory and diagnostics use that selection, and profile-agent popovers derive it from the provider ID. OMP-compatible XDG data and state roots are honored when a migrated profile uses them. Default-provider views are labelled **Daemon default store**; changing `providerOptions.command` on the default provider does not make those views profile-aware. Workspace configuration remains project-scoped, with the selected profile supplying inherited settings. Hub records are daemon-wide. The RPCs also accept an explicit absolute `store.agentDir` for custom stores; it is mutually exclusive with `store.profile`.
|
|
34
48
|
|
|
@@ -99,7 +113,7 @@ Existing agents whose provider is `omp` remain owned by the bundled provider. Ne
|
|
|
99
113
|
|
|
100
114
|
OMP `18.1.15` is the oldest version tested end to end. The direct provider's hard compatibility gate is `rpc-ui` protocol v2: metadata-free legacy ready frames and v1-only runtimes are rejected before a provider session opens because they cannot support the advertised persistence and conversation-rewind capabilities. Typed tool approvals remain capability-gated and fall back as described above. For terminal completion, a matching `agent_end.requestId` is authoritative and mismatches are ignored before state changes. Released OMP 18.2.x builds that omit that field use an ordered fallback requiring a fresh correlated native user entry followed by current-turn assistant activity, an idle non-compacting runtime, and no conflicting permission, tool, steer, or child-session work. Ambiguous events while OMP is active are ignored; unresolved ambiguity after confirmed idle fails only the Paseo turn. This trades a bounded residual same-agent stale-event risk for compatibility with released OMP instead of terminating and lazily restarting the runtime after every later prompt. Paseo clients, including mobile clients inside reconnect grace, do not own or terminate the daemon-managed OMP session.
|
|
101
115
|
|
|
102
|
-
Paseo 0.9.0-beta.1 is the canary target and provides npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping.
|
|
116
|
+
Paseo 0.9.0-beta.1 is the verified canary target and minimum supported release. It preserves nested provider-subagent ancestry and spawning-tool links, and provides npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping. The required full Docker canary passes there with OMP 18.1.15 and 18.2.0.
|
|
103
117
|
|
|
104
118
|
Install, update, disable, or remove `paseo-omp` independently of the bundled provider. Verify the provider snapshot contains `omp-plugin` after installation; a bundled `omp` entry may remain present and is not modified by this plugin.
|
|
105
119
|
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
useRpc,
|
|
6
6
|
useWorkspace,
|
|
7
7
|
} from "@getpaseo/plugin/client";
|
|
8
|
-
import { Icon, TextInput } from "@getpaseo/plugin/client/react-native";
|
|
8
|
+
import { copyText, Icon, TextInput } from "@getpaseo/plugin/client/react-native";
|
|
9
|
+
import { ExternalLink } from "@getpaseo/plugin/client/ui";
|
|
9
10
|
import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
10
11
|
import type { ReactNode } from "react";
|
|
11
12
|
import { useCallback, useMemo, useState } from "react";
|
|
@@ -13,20 +14,24 @@ import type { TextStyle, ViewStyle } from "react-native";
|
|
|
13
14
|
import { Pressable, ScrollView, Switch, Text, View } from "react-native";
|
|
14
15
|
import type { ComposerPillSettings } from "../shared/composer-pill-settings";
|
|
15
16
|
import { listOmpConfig, type OmpConfig } from "../shared/omp-config";
|
|
17
|
+
import { listOmpModels } from "../shared/omp-models";
|
|
16
18
|
import {
|
|
17
19
|
categorizeOmpSetting,
|
|
18
20
|
formatOmpSettingLabel,
|
|
21
|
+
isOmpStructuredSettingPath,
|
|
19
22
|
listOmpSettings,
|
|
20
23
|
OMP_SETTING_CATEGORIES,
|
|
21
|
-
type OmpScalarValue,
|
|
22
24
|
type OmpSetting,
|
|
23
25
|
type OmpSettingCategory,
|
|
26
|
+
parseOmpStructuredSettingValue,
|
|
24
27
|
updateOmpSettings,
|
|
25
28
|
} from "../shared/omp-settings";
|
|
26
29
|
import { type OmpStore, storeLabel } from "../shared/omp-store";
|
|
27
30
|
import { getOmpProviderHealth, type OmpProviderHealth } from "../shared/provider-diagnostics";
|
|
31
|
+
import { getOmpSupportReport, OMP_SUPPORT_ISSUE_URL } from "../shared/support-diagnostics";
|
|
28
32
|
import { ComposerPillSettingsSection } from "./composer-pill-settings";
|
|
29
33
|
import { openOmpExternalUrl } from "./external-url";
|
|
34
|
+
import { type OmpConfigSurfaceView as SurfaceView, surfaceViewsForScope } from "./omp-config-views";
|
|
30
35
|
import {
|
|
31
36
|
documentationForSettingCategory,
|
|
32
37
|
documentationForSettingPath,
|
|
@@ -34,7 +39,9 @@ import {
|
|
|
34
39
|
OMP_SETTINGS_REFERENCE,
|
|
35
40
|
type OmpDocumentationLink,
|
|
36
41
|
} from "./omp-doc-links";
|
|
42
|
+
import { normalizeOmpModels } from "./omp-model-picker-state";
|
|
37
43
|
import { OmpPluginManagerSection } from "./omp-plugin-manager";
|
|
44
|
+
import { type OmpModelCatalogState, StructuredRoutingEditor } from "./omp-routing-editor";
|
|
38
45
|
import { OmpStorePicker } from "./omp-store-picker";
|
|
39
46
|
import { ompStoreKey } from "./omp-store-state";
|
|
40
47
|
import {
|
|
@@ -56,6 +63,11 @@ import {
|
|
|
56
63
|
summarizeProviderStatus,
|
|
57
64
|
summarizeRpcUiSupport,
|
|
58
65
|
} from "./provider-diagnostics-state";
|
|
66
|
+
import {
|
|
67
|
+
refreshSupportReport,
|
|
68
|
+
type SupportReportCopyState,
|
|
69
|
+
supportDiagnosticsViewState,
|
|
70
|
+
} from "./support-diagnostics-state";
|
|
59
71
|
|
|
60
72
|
const SETTINGS_QUERY_KEY = ["paseo-omp", "settings"] as const;
|
|
61
73
|
const CONFIG_POLL_MS = 30_000;
|
|
@@ -91,6 +103,10 @@ export interface OmpConfigStyles {
|
|
|
91
103
|
docLinkPressed: ViewStyle;
|
|
92
104
|
docLinkText: TextStyle;
|
|
93
105
|
refreshLabel: TextStyle;
|
|
106
|
+
helpActions: ViewStyle;
|
|
107
|
+
helpReport: ViewStyle;
|
|
108
|
+
helpReportText: TextStyle;
|
|
109
|
+
helpLinkText: TextStyle;
|
|
94
110
|
card: ViewStyle;
|
|
95
111
|
cardHeader: ViewStyle;
|
|
96
112
|
cardTitle: TextStyle;
|
|
@@ -215,6 +231,26 @@ function useConfigStyles(theme: PluginSurfaceProps["theme"], compact: boolean):
|
|
|
215
231
|
backgroundColor: theme.colors.surface1,
|
|
216
232
|
},
|
|
217
233
|
refreshLabel: { color: theme.colors.foreground, fontSize: 13 },
|
|
234
|
+
helpActions: {
|
|
235
|
+
flexDirection: "row",
|
|
236
|
+
alignItems: "center",
|
|
237
|
+
flexWrap: "wrap",
|
|
238
|
+
gap: 8,
|
|
239
|
+
},
|
|
240
|
+
helpReport: {
|
|
241
|
+
maxWidth: "100%",
|
|
242
|
+
padding: compact ? 10 : 12,
|
|
243
|
+
borderWidth: 1,
|
|
244
|
+
borderColor: theme.colors.border,
|
|
245
|
+
borderRadius: 10,
|
|
246
|
+
backgroundColor: theme.colors.surface1,
|
|
247
|
+
},
|
|
248
|
+
helpReportText: {
|
|
249
|
+
color: theme.colors.foreground,
|
|
250
|
+
fontSize: compact ? 11 : 12,
|
|
251
|
+
lineHeight: compact ? 16 : 18,
|
|
252
|
+
},
|
|
253
|
+
helpLinkText: { color: theme.colors.accent, fontSize: 13, fontWeight: "600" },
|
|
218
254
|
cardHeader: {
|
|
219
255
|
flexDirection: "row",
|
|
220
256
|
alignItems: "center",
|
|
@@ -691,6 +727,131 @@ function ProviderHealthSection({
|
|
|
691
727
|
);
|
|
692
728
|
}
|
|
693
729
|
|
|
730
|
+
function SupportDiagnosticsSection({
|
|
731
|
+
theme,
|
|
732
|
+
styles,
|
|
733
|
+
cwd,
|
|
734
|
+
store,
|
|
735
|
+
}: {
|
|
736
|
+
theme: PluginSurfaceProps["theme"];
|
|
737
|
+
styles: OmpConfigStyles;
|
|
738
|
+
cwd?: string;
|
|
739
|
+
store?: OmpStore;
|
|
740
|
+
}) {
|
|
741
|
+
const loadReport = useRpc(getOmpSupportReport);
|
|
742
|
+
const queryClient = useQueryClient();
|
|
743
|
+
const context = { store, ...(cwd ? { cwd } : {}) };
|
|
744
|
+
const queryKey = ["paseo-omp", "support-report", ompStoreKey(store), cwd ?? "global"];
|
|
745
|
+
const report = useQuery({
|
|
746
|
+
queryKey,
|
|
747
|
+
queryFn: () => loadReport(context),
|
|
748
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
749
|
+
retry: false,
|
|
750
|
+
});
|
|
751
|
+
const refreshReport = useMutation({
|
|
752
|
+
mutationFn: () => refreshSupportReport(loadReport, context),
|
|
753
|
+
onSuccess: (value) => queryClient.setQueryData(queryKey, value),
|
|
754
|
+
});
|
|
755
|
+
const [copyState, setCopyState] = useState<SupportReportCopyState>("idle");
|
|
756
|
+
const [linkError, setLinkError] = useState(false);
|
|
757
|
+
const viewState = supportDiagnosticsViewState({
|
|
758
|
+
loading: report.isLoading,
|
|
759
|
+
refreshing: refreshReport.isPending || (report.isFetching && !report.isLoading),
|
|
760
|
+
hasReport: Boolean(report.data?.report),
|
|
761
|
+
reportFailed: Boolean(report.error || refreshReport.error),
|
|
762
|
+
copyState,
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const refresh = () => {
|
|
766
|
+
setCopyState("idle");
|
|
767
|
+
refreshReport.mutate();
|
|
768
|
+
};
|
|
769
|
+
const copyReport = async () => {
|
|
770
|
+
if (!report.data?.report) return;
|
|
771
|
+
setCopyState("copying");
|
|
772
|
+
try {
|
|
773
|
+
await copyText(report.data.report);
|
|
774
|
+
setCopyState("copied");
|
|
775
|
+
} catch {
|
|
776
|
+
setCopyState("error");
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
return (
|
|
781
|
+
<>
|
|
782
|
+
<View style={styles.sectionHeader}>
|
|
783
|
+
<Text style={styles.sectionTitle}>Support diagnostics</Text>
|
|
784
|
+
<Text style={styles.muted}>
|
|
785
|
+
Safe OMP runtime, compatibility, storage-state, and protocol counters for maintainers.
|
|
786
|
+
</Text>
|
|
787
|
+
</View>
|
|
788
|
+
<View style={styles.helpActions}>
|
|
789
|
+
<Pressable
|
|
790
|
+
accessibilityRole="button"
|
|
791
|
+
accessibilityLabel="Refresh OMP support report"
|
|
792
|
+
disabled={report.isFetching || refreshReport.isPending}
|
|
793
|
+
onPress={refresh}
|
|
794
|
+
style={styles.refresh}
|
|
795
|
+
>
|
|
796
|
+
<Icon name="RefreshCw" size={14} color={theme.colors.foreground} />
|
|
797
|
+
<Text style={styles.refreshLabel}>{viewState.refreshLabel}</Text>
|
|
798
|
+
</Pressable>
|
|
799
|
+
<Pressable
|
|
800
|
+
accessibilityRole="button"
|
|
801
|
+
accessibilityLabel="Copy OMP support report"
|
|
802
|
+
disabled={viewState.copyDisabled}
|
|
803
|
+
onPress={() => void copyReport()}
|
|
804
|
+
style={[styles.refresh, viewState.copyDisabled ? { opacity: 0.55 } : null]}
|
|
805
|
+
>
|
|
806
|
+
<Icon name="Copy" size={14} color={theme.colors.foreground} />
|
|
807
|
+
<Text style={styles.refreshLabel}>{viewState.copyLabel}</Text>
|
|
808
|
+
</Pressable>
|
|
809
|
+
<ExternalLink
|
|
810
|
+
href={OMP_SUPPORT_ISSUE_URL}
|
|
811
|
+
accessibilityLabel="Create paseo-omp GitHub issue"
|
|
812
|
+
onError={() => setLinkError(true)}
|
|
813
|
+
>
|
|
814
|
+
<Text style={styles.helpLinkText}>Create GitHub issue</Text>
|
|
815
|
+
</ExternalLink>
|
|
816
|
+
</View>
|
|
817
|
+
<Text style={styles.muted}>
|
|
818
|
+
Review before sharing. The report excludes prompts, transcripts, credentials, commands,
|
|
819
|
+
URLs, repository names, and private paths.
|
|
820
|
+
</Text>
|
|
821
|
+
{viewState.loadingMessage ? (
|
|
822
|
+
<Text accessibilityLiveRegion="polite" style={styles.muted}>
|
|
823
|
+
{viewState.loadingMessage}
|
|
824
|
+
</Text>
|
|
825
|
+
) : null}
|
|
826
|
+
{viewState.reportError ? (
|
|
827
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
828
|
+
{viewState.reportError}
|
|
829
|
+
</Text>
|
|
830
|
+
) : null}
|
|
831
|
+
{viewState.copyFeedback ? (
|
|
832
|
+
<Text
|
|
833
|
+
accessibilityLiveRegion="polite"
|
|
834
|
+
style={copyState === "error" ? styles.error : styles.muted}
|
|
835
|
+
>
|
|
836
|
+
{viewState.copyFeedback}
|
|
837
|
+
</Text>
|
|
838
|
+
) : null}
|
|
839
|
+
{linkError ? (
|
|
840
|
+
<Text accessibilityRole="alert" style={styles.error}>
|
|
841
|
+
Could not open GitHub. Copy the report and open the issue tracker manually.
|
|
842
|
+
</Text>
|
|
843
|
+
) : null}
|
|
844
|
+
{report.data?.report ? (
|
|
845
|
+
<View style={styles.helpReport}>
|
|
846
|
+
<Text selectable style={styles.helpReportText}>
|
|
847
|
+
{report.data.report}
|
|
848
|
+
</Text>
|
|
849
|
+
</View>
|
|
850
|
+
) : null}
|
|
851
|
+
</>
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
694
855
|
const CATEGORY_LABELS: Record<OmpSettingCategory, string> = {
|
|
695
856
|
appearance: "Appearance",
|
|
696
857
|
model: "Model",
|
|
@@ -736,16 +897,6 @@ function fallbackSettingsFromConfig(config: OmpConfig | null | undefined): OmpSe
|
|
|
736
897
|
return settings;
|
|
737
898
|
}
|
|
738
899
|
|
|
739
|
-
type SurfaceView = "overview" | "plugin" | "plugins" | "composer" | "configuration" | "diagnostics";
|
|
740
|
-
const SURFACE_VIEWS: readonly { id: SurfaceView; label: string }[] = [
|
|
741
|
-
{ id: "overview", label: "Overview" },
|
|
742
|
-
{ id: "plugin", label: "Plugin" },
|
|
743
|
-
{ id: "plugins", label: "OMP plugins" },
|
|
744
|
-
{ id: "composer", label: "Composer" },
|
|
745
|
-
{ id: "configuration", label: "Configuration" },
|
|
746
|
-
{ id: "diagnostics", label: "Diagnostics" },
|
|
747
|
-
];
|
|
748
|
-
|
|
749
900
|
function formatScalarValue(value: unknown): string {
|
|
750
901
|
if (value === undefined || value === null) return "Not set";
|
|
751
902
|
if (typeof value === "boolean") return value ? "Enabled" : "Disabled";
|
|
@@ -834,7 +985,7 @@ function StructuredSettingValue({
|
|
|
834
985
|
);
|
|
835
986
|
}
|
|
836
987
|
|
|
837
|
-
type SettingDraft = { operation: "set"; value:
|
|
988
|
+
type SettingDraft = { operation: "set"; value: unknown } | { operation: "reset" };
|
|
838
989
|
|
|
839
990
|
function EditableScalarValue({
|
|
840
991
|
setting,
|
|
@@ -891,6 +1042,9 @@ function ConfigurationCategory({
|
|
|
891
1042
|
styles,
|
|
892
1043
|
settings,
|
|
893
1044
|
drafts,
|
|
1045
|
+
theme,
|
|
1046
|
+
modelCatalog,
|
|
1047
|
+
modelRoles,
|
|
894
1048
|
disabled,
|
|
895
1049
|
workspaceScoped,
|
|
896
1050
|
onDraft,
|
|
@@ -898,6 +1052,9 @@ function ConfigurationCategory({
|
|
|
898
1052
|
}: {
|
|
899
1053
|
category: { id: OmpSettingCategory; label: string };
|
|
900
1054
|
styles: OmpConfigStyles;
|
|
1055
|
+
theme: PluginSurfaceProps["theme"];
|
|
1056
|
+
modelCatalog: OmpModelCatalogState;
|
|
1057
|
+
modelRoles: Readonly<Record<string, unknown>>;
|
|
901
1058
|
settings: readonly OmpSetting[];
|
|
902
1059
|
drafts: Readonly<Record<string, SettingDraft>>;
|
|
903
1060
|
disabled: boolean;
|
|
@@ -926,7 +1083,12 @@ function ConfigurationCategory({
|
|
|
926
1083
|
(setting.value !== null && typeof setting.value === "object");
|
|
927
1084
|
const editable =
|
|
928
1085
|
!setting.redacted && ["boolean", "number", "string", "enum"].includes(setting.type);
|
|
1086
|
+
const structuredEditable =
|
|
1087
|
+
!setting.redacted &&
|
|
1088
|
+
isOmpStructuredSettingPath(setting.path) &&
|
|
1089
|
+
setting.type === (setting.path === "cycleOrder" ? "array" : "record");
|
|
929
1090
|
const settingDocumentation = documentationForSettingPath(setting.path);
|
|
1091
|
+
const draft = drafts[setting.path];
|
|
930
1092
|
return (
|
|
931
1093
|
<View key={setting.path} style={styles.setting}>
|
|
932
1094
|
<View style={styles.settingHeader}>
|
|
@@ -934,7 +1096,7 @@ function ConfigurationCategory({
|
|
|
934
1096
|
{setting.workspaceOverride ? (
|
|
935
1097
|
<Text style={styles.source}>Workspace override</Text>
|
|
936
1098
|
) : null}
|
|
937
|
-
{!complex && !editable ? (
|
|
1099
|
+
{!complex && !editable && !structuredEditable ? (
|
|
938
1100
|
<StructuredSettingValue setting={setting} styles={styles} />
|
|
939
1101
|
) : null}
|
|
940
1102
|
</View>
|
|
@@ -953,7 +1115,7 @@ function ConfigurationCategory({
|
|
|
953
1115
|
{editable ? (
|
|
954
1116
|
<EditableScalarValue
|
|
955
1117
|
setting={setting}
|
|
956
|
-
draft={
|
|
1118
|
+
draft={draft}
|
|
957
1119
|
disabled={disabled}
|
|
958
1120
|
resetLabel={workspaceScoped ? "Remove workspace override" : "Reset to default"}
|
|
959
1121
|
showReset={!workspaceScoped || setting.workspaceOverride === true}
|
|
@@ -961,6 +1123,21 @@ function ConfigurationCategory({
|
|
|
961
1123
|
onSet={(value) => onDraft(setting.path, { operation: "set", value })}
|
|
962
1124
|
onReset={() => onDraft(setting.path, { operation: "reset" })}
|
|
963
1125
|
/>
|
|
1126
|
+
) : structuredEditable ? (
|
|
1127
|
+
<StructuredRoutingEditor
|
|
1128
|
+
setting={setting}
|
|
1129
|
+
value={draft?.operation === "set" ? draft.value : setting.value}
|
|
1130
|
+
disabled={disabled}
|
|
1131
|
+
resetLabel={workspaceScoped ? "Remove workspace override" : "Reset to default"}
|
|
1132
|
+
showReset={!workspaceScoped || setting.workspaceOverride === true}
|
|
1133
|
+
resetPending={draft?.operation === "reset"}
|
|
1134
|
+
styles={styles}
|
|
1135
|
+
theme={theme}
|
|
1136
|
+
modelCatalog={modelCatalog}
|
|
1137
|
+
modelRoles={modelRoles}
|
|
1138
|
+
onSet={(value) => onDraft(setting.path, { operation: "set", value })}
|
|
1139
|
+
onReset={() => onDraft(setting.path, { operation: "reset" })}
|
|
1140
|
+
/>
|
|
964
1141
|
) : complex ? (
|
|
965
1142
|
<StructuredSettingValue setting={setting} styles={styles} />
|
|
966
1143
|
) : null}
|
|
@@ -1022,6 +1199,7 @@ function OmpConfigContent({
|
|
|
1022
1199
|
const loadConfig = useRpc(listOmpConfig);
|
|
1023
1200
|
const loadSettings = useRpc(listOmpSettings);
|
|
1024
1201
|
const updateSettings = useRpc(updateOmpSettings);
|
|
1202
|
+
const loadModels = useRpc(listOmpModels);
|
|
1025
1203
|
const queryClient = useQueryClient();
|
|
1026
1204
|
const context = { store, ...(cwd ? { cwd } : {}) };
|
|
1027
1205
|
const pendingMutations = useIsMutating({ mutationKey: ["paseo-omp"] });
|
|
@@ -1036,6 +1214,11 @@ function OmpConfigContent({
|
|
|
1036
1214
|
queryFn: () => loadSettings(context),
|
|
1037
1215
|
staleTime: Number.POSITIVE_INFINITY,
|
|
1038
1216
|
});
|
|
1217
|
+
const modelsQuery = useQuery({
|
|
1218
|
+
queryKey: ["paseo-omp", "models", ompStoreKey(store), cwd ?? "global"],
|
|
1219
|
+
queryFn: () => loadModels(context),
|
|
1220
|
+
staleTime: 30_000,
|
|
1221
|
+
});
|
|
1039
1222
|
const [view, setView] = useState<SurfaceView>("overview");
|
|
1040
1223
|
const [activeCategory, setActiveCategory] = useState<OmpSettingCategory>("appearance");
|
|
1041
1224
|
const [search, setSearch] = useState("");
|
|
@@ -1043,9 +1226,7 @@ function OmpConfigContent({
|
|
|
1043
1226
|
const [documentationError, setDocumentationError] = useState<string | null>(null);
|
|
1044
1227
|
const styles = useConfigStyles(theme, layout.compact);
|
|
1045
1228
|
const normalizedSearch = search.trim().toLocaleLowerCase();
|
|
1046
|
-
const surfaceViews = cwd
|
|
1047
|
-
? SURFACE_VIEWS.filter((candidate) => candidate.id !== "composer")
|
|
1048
|
-
: SURFACE_VIEWS;
|
|
1229
|
+
const surfaceViews = surfaceViewsForScope(cwd !== undefined);
|
|
1049
1230
|
const catalog = useMemo(() => {
|
|
1050
1231
|
const sourceSettings = settingsQuery.data?.available
|
|
1051
1232
|
? settingsQuery.data.settings
|
|
@@ -1065,6 +1246,24 @@ function OmpConfigContent({
|
|
|
1065
1246
|
}
|
|
1066
1247
|
return { sourceSettings, matching, byCategory };
|
|
1067
1248
|
}, [configQuery.data?.config, normalizedSearch, settingsQuery.data]);
|
|
1249
|
+
const modelCatalog: OmpModelCatalogState = {
|
|
1250
|
+
models: normalizeOmpModels(modelsQuery.data?.models ?? []),
|
|
1251
|
+
loading: modelsQuery.isLoading,
|
|
1252
|
+
...(modelsQuery.error
|
|
1253
|
+
? { error: "Could not list OMP models. Freeform selectors remain available." }
|
|
1254
|
+
: {}),
|
|
1255
|
+
};
|
|
1256
|
+
const modelRolesValue = drafts.modelRoles
|
|
1257
|
+
? drafts.modelRoles.operation === "set"
|
|
1258
|
+
? drafts.modelRoles.value
|
|
1259
|
+
: undefined
|
|
1260
|
+
: catalog.sourceSettings.find((setting) => setting.path === "modelRoles")?.value;
|
|
1261
|
+
const modelRoles =
|
|
1262
|
+
modelRolesValue !== null &&
|
|
1263
|
+
typeof modelRolesValue === "object" &&
|
|
1264
|
+
!Array.isArray(modelRolesValue)
|
|
1265
|
+
? (modelRolesValue as Readonly<Record<string, unknown>>)
|
|
1266
|
+
: {};
|
|
1068
1267
|
const visibleCategories = CONFIG_CATEGORIES.filter(
|
|
1069
1268
|
(category) => (catalog.byCategory.get(category.id)?.length ?? 0) > 0,
|
|
1070
1269
|
);
|
|
@@ -1089,8 +1288,12 @@ function OmpConfigContent({
|
|
|
1089
1288
|
if (draft.operation === "reset") return { operation: "reset" as const, path };
|
|
1090
1289
|
const setting = byPath.get(path);
|
|
1091
1290
|
if (!setting) throw new Error(`Setting ${path} is no longer available.`);
|
|
1092
|
-
let value:
|
|
1093
|
-
if (
|
|
1291
|
+
let value: unknown = draft.value;
|
|
1292
|
+
if (isOmpStructuredSettingPath(path)) {
|
|
1293
|
+
const parsed = parseOmpStructuredSettingValue(path, value);
|
|
1294
|
+
if (parsed === undefined) throw new Error(`${path} contains invalid routing values.`);
|
|
1295
|
+
value = parsed;
|
|
1296
|
+
} else if (setting.type === "number") {
|
|
1094
1297
|
const raw = String(draft.value).trim();
|
|
1095
1298
|
if (!raw) throw new Error(`${path} requires a number.`);
|
|
1096
1299
|
const parsed = Number(raw);
|
|
@@ -1099,7 +1302,8 @@ function OmpConfigContent({
|
|
|
1099
1302
|
}
|
|
1100
1303
|
return { operation: "set" as const, path, value };
|
|
1101
1304
|
});
|
|
1102
|
-
|
|
1305
|
+
const input = updateOmpSettings.input.parse({ ...context, revision, changes });
|
|
1306
|
+
return updateSettings(input);
|
|
1103
1307
|
},
|
|
1104
1308
|
onSuccess: (result) => {
|
|
1105
1309
|
queryClient.setQueryData(settingsQueryKey, result.catalog);
|
|
@@ -1212,6 +1416,10 @@ function OmpConfigContent({
|
|
|
1212
1416
|
<ProviderHealthSection theme={theme} styles={styles} cwd={cwd} store={store} />
|
|
1213
1417
|
) : null}
|
|
1214
1418
|
|
|
1419
|
+
{view === "help" ? (
|
|
1420
|
+
<SupportDiagnosticsSection theme={theme} styles={styles} cwd={cwd} store={store} />
|
|
1421
|
+
) : null}
|
|
1422
|
+
|
|
1215
1423
|
{view === "configuration" ? (
|
|
1216
1424
|
<>
|
|
1217
1425
|
<View style={styles.sectionHeader}>
|
|
@@ -1221,14 +1429,22 @@ function OmpConfigContent({
|
|
|
1221
1429
|
accessibilityRole="button"
|
|
1222
1430
|
accessibilityLabel="Refresh OMP configuration"
|
|
1223
1431
|
style={styles.refresh}
|
|
1224
|
-
disabled={
|
|
1432
|
+
disabled={
|
|
1433
|
+
configQuery.isFetching || settingsQuery.isFetching || modelsQuery.isFetching
|
|
1434
|
+
}
|
|
1225
1435
|
onPress={() => {
|
|
1226
|
-
void Promise.all([
|
|
1436
|
+
void Promise.all([
|
|
1437
|
+
configQuery.refetch(),
|
|
1438
|
+
settingsQuery.refetch(),
|
|
1439
|
+
modelsQuery.refetch(),
|
|
1440
|
+
]);
|
|
1227
1441
|
}}
|
|
1228
1442
|
>
|
|
1229
1443
|
<Icon name="RefreshCw" size={14} color={theme.colors.foreground} />
|
|
1230
1444
|
<Text style={styles.refreshLabel}>
|
|
1231
|
-
{configQuery.isFetching || settingsQuery.isFetching
|
|
1445
|
+
{configQuery.isFetching || settingsQuery.isFetching || modelsQuery.isFetching
|
|
1446
|
+
? "Refreshing…"
|
|
1447
|
+
: "Refresh"}
|
|
1232
1448
|
</Text>
|
|
1233
1449
|
</Pressable>
|
|
1234
1450
|
</View>
|
|
@@ -1364,6 +1580,9 @@ function OmpConfigContent({
|
|
|
1364
1580
|
{selectedCategory ? (
|
|
1365
1581
|
<ConfigurationCategory
|
|
1366
1582
|
category={selectedCategory}
|
|
1583
|
+
theme={theme}
|
|
1584
|
+
modelCatalog={modelCatalog}
|
|
1585
|
+
modelRoles={modelRoles}
|
|
1367
1586
|
styles={styles}
|
|
1368
1587
|
settings={catalog.byCategory.get(selectedCategory.id) ?? []}
|
|
1369
1588
|
drafts={drafts}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type OmpConfigSurfaceView =
|
|
2
|
+
| "overview"
|
|
3
|
+
| "plugin"
|
|
4
|
+
| "plugins"
|
|
5
|
+
| "composer"
|
|
6
|
+
| "configuration"
|
|
7
|
+
| "diagnostics"
|
|
8
|
+
| "help";
|
|
9
|
+
|
|
10
|
+
const SURFACE_VIEWS: readonly { id: OmpConfigSurfaceView; label: string }[] = [
|
|
11
|
+
{ id: "overview", label: "Overview" },
|
|
12
|
+
{ id: "plugin", label: "Plugin" },
|
|
13
|
+
{ id: "plugins", label: "OMP plugins" },
|
|
14
|
+
{ id: "composer", label: "Composer" },
|
|
15
|
+
{ id: "configuration", label: "Configuration" },
|
|
16
|
+
{ id: "diagnostics", label: "Diagnostics" },
|
|
17
|
+
{ id: "help", label: "Help" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function surfaceViewsForScope(workspaceScoped: boolean) {
|
|
21
|
+
return workspaceScoped
|
|
22
|
+
? SURFACE_VIEWS.filter((candidate) => candidate.id !== "composer")
|
|
23
|
+
: SURFACE_VIEWS;
|
|
24
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { OmpModelCandidate } from "../shared/omp-models";
|
|
2
|
+
import { isValidOmpModelSelector } from "../shared/omp-settings";
|
|
3
|
+
|
|
4
|
+
export const OMP_MODEL_PICKER_RESULT_LIMIT = 8;
|
|
5
|
+
|
|
6
|
+
export type OmpModelPickerModel = {
|
|
7
|
+
selector: string;
|
|
8
|
+
label: string;
|
|
9
|
+
provider: string;
|
|
10
|
+
reasoning: boolean;
|
|
11
|
+
imageInput: boolean;
|
|
12
|
+
contextWindow?: number;
|
|
13
|
+
thinkingLevels: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type OmpModelPickerChoice =
|
|
17
|
+
| { kind: "alias"; selector: `@${string}`; label: string }
|
|
18
|
+
| { kind: "model"; model: OmpModelPickerModel; recommended: boolean };
|
|
19
|
+
|
|
20
|
+
const REASONING_ROLES: Readonly<Record<string, true>> = {
|
|
21
|
+
default: true,
|
|
22
|
+
task: true,
|
|
23
|
+
plan: true,
|
|
24
|
+
slow: true,
|
|
25
|
+
advisor: true,
|
|
26
|
+
reviewer: true,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function roleCapabilityMatch(role: string, model: OmpModelPickerModel): boolean | undefined {
|
|
30
|
+
const normalizedRole = role.trim().toLocaleLowerCase();
|
|
31
|
+
if (normalizedRole === "vision") return model.imageInput;
|
|
32
|
+
if (REASONING_ROLES[normalizedRole]) return model.reasoning;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeOmpModels(models: readonly OmpModelCandidate[]): OmpModelPickerModel[] {
|
|
37
|
+
return models.map((model) => ({
|
|
38
|
+
selector: model.selector,
|
|
39
|
+
label: model.name || model.id,
|
|
40
|
+
provider: model.provider,
|
|
41
|
+
reasoning: model.reasoning,
|
|
42
|
+
imageInput: model.input.some((input) => input.toLocaleLowerCase() === "image"),
|
|
43
|
+
...(model.contextWindow === null || model.contextWindow === undefined
|
|
44
|
+
? {}
|
|
45
|
+
: { contextWindow: model.contextWindow }),
|
|
46
|
+
thinkingLevels: model.thinkingLevels,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function aliasChoices(
|
|
51
|
+
modelRoles: Readonly<Record<string, unknown>>,
|
|
52
|
+
currentRole?: string,
|
|
53
|
+
): OmpModelPickerChoice[] {
|
|
54
|
+
return Object.keys(modelRoles)
|
|
55
|
+
.filter((role) => role.length > 0 && role !== currentRole)
|
|
56
|
+
.sort((left, right) => left.localeCompare(right))
|
|
57
|
+
.map((role) => ({ kind: "alias" as const, selector: `@${role}` as const, label: role }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function choiceSearchText(choice: OmpModelPickerChoice): string {
|
|
61
|
+
if (choice.kind === "alias") return `${choice.label} ${choice.selector}`;
|
|
62
|
+
const model = choice.model;
|
|
63
|
+
return [
|
|
64
|
+
model.label,
|
|
65
|
+
model.selector,
|
|
66
|
+
model.provider,
|
|
67
|
+
model.reasoning ? "reasoning" : "standard",
|
|
68
|
+
model.imageInput ? "image vision" : "text",
|
|
69
|
+
model.contextWindow?.toString() ?? "",
|
|
70
|
+
...model.thinkingLevels,
|
|
71
|
+
].join(" ");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function filterModelChoices(
|
|
75
|
+
models: readonly OmpModelPickerModel[],
|
|
76
|
+
aliases: readonly OmpModelPickerChoice[],
|
|
77
|
+
query: string,
|
|
78
|
+
role: string,
|
|
79
|
+
limit = OMP_MODEL_PICKER_RESULT_LIMIT,
|
|
80
|
+
): OmpModelPickerChoice[] {
|
|
81
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
82
|
+
const choices: OmpModelPickerChoice[] = [
|
|
83
|
+
...aliases,
|
|
84
|
+
...models.map((model) => ({
|
|
85
|
+
kind: "model" as const,
|
|
86
|
+
model,
|
|
87
|
+
recommended: roleCapabilityMatch(role, model) === true,
|
|
88
|
+
})),
|
|
89
|
+
];
|
|
90
|
+
return choices
|
|
91
|
+
.filter(
|
|
92
|
+
(choice) =>
|
|
93
|
+
normalizedQuery.length === 0 ||
|
|
94
|
+
choiceSearchText(choice).toLocaleLowerCase().includes(normalizedQuery),
|
|
95
|
+
)
|
|
96
|
+
.sort((left, right) => {
|
|
97
|
+
const leftRecommended = left.kind === "model" && left.recommended;
|
|
98
|
+
const rightRecommended = right.kind === "model" && right.recommended;
|
|
99
|
+
if (leftRecommended !== rightRecommended) return leftRecommended ? -1 : 1;
|
|
100
|
+
if (left.kind !== right.kind) return left.kind === "alias" ? -1 : 1;
|
|
101
|
+
return choiceSearchText(left).localeCompare(choiceSearchText(right));
|
|
102
|
+
})
|
|
103
|
+
.slice(0, Math.max(0, limit));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function formatModelSelector(selector: string, thinkingLevel?: string): string {
|
|
107
|
+
return thinkingLevel ? `${selector}:${thinkingLevel}` : selector;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type OmpThinkingSelectorChoice = {
|
|
111
|
+
selector: string;
|
|
112
|
+
available: boolean;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function thinkingSelectorChoice(
|
|
116
|
+
selector: string,
|
|
117
|
+
thinkingLevel: string,
|
|
118
|
+
): OmpThinkingSelectorChoice {
|
|
119
|
+
const suffixedSelector = formatModelSelector(selector, thinkingLevel);
|
|
120
|
+
return {
|
|
121
|
+
selector: suffixedSelector,
|
|
122
|
+
available: isValidOmpModelSelector(suffixedSelector),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function updatePickerValue(
|
|
127
|
+
currentValue: string | readonly string[],
|
|
128
|
+
selector: string,
|
|
129
|
+
mode: "replace" | "append",
|
|
130
|
+
): string | string[] {
|
|
131
|
+
if (mode === "replace") return selector;
|
|
132
|
+
const current = typeof currentValue === "string" ? [currentValue] : [...currentValue];
|
|
133
|
+
if (current.includes(selector)) return current;
|
|
134
|
+
return [...current, selector];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function commitPickerSelection(
|
|
138
|
+
disabled: boolean,
|
|
139
|
+
selector: string,
|
|
140
|
+
onSelect: (selector: string) => void,
|
|
141
|
+
): boolean {
|
|
142
|
+
if (disabled) return false;
|
|
143
|
+
onSelect(selector);
|
|
144
|
+
return true;
|
|
145
|
+
}
|