@omercnet/paseo-omp 0.2.1 → 0.3.0
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/CHANGELOG.md +26 -0
- package/README.md +25 -13
- package/SUPPORT.md +6 -2
- package/TESTING.md +21 -18
- package/client/composer-pill-settings.tsx +157 -0
- package/client/external-url.ts +15 -0
- package/client/mcp-authorization.tsx +169 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +8 -3
- package/client/memory-popover.tsx +8 -4
- package/client/omp-config-surface.tsx +189 -29
- package/client/omp-plugin-manager.tsx +302 -131
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/paseo-types.ts +9 -0
- package/client/provider-diagnostics-state.ts +18 -7
- package/client/quota-popover.tsx +8 -3
- package/client/quota-state.ts +16 -7
- package/client/sessions-popover.tsx +8 -3
- package/docs/alpha-release-checklist.md +6 -8
- package/docs/configuration.md +8 -4
- package/docs/core-provider-issue-audit.md +3 -2
- 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 +35 -19
- package/index.client.tsx +339 -123
- package/index.server.ts +44 -14
- package/package.json +7 -8
- package/paseo-plugin.json +2 -2
- package/scripts/prepare-dependencies.mjs +24 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +2 -2
- package/server/omp-config.ts +16 -7
- package/server/omp-plugins.ts +70 -21
- package/server/omp-settings.ts +232 -24
- package/server/paths.ts +128 -11
- package/server/provider/catalog.ts +3 -4
- package/server/provider/connection.ts +213 -9
- package/server/provider/host-tools.ts +71 -0
- package/server/provider/omp-rpc.ts +82 -15
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/session-descriptors.ts +306 -1
- package/server/provider/session.ts +704 -249
- package/server/provider/subsessions.ts +4 -1
- package/server/provider/timeline-projector.ts +70 -33
- package/server/provider-diagnostics.ts +122 -36
- package/server/quota.ts +3 -2
- package/server/sessions.ts +2 -2
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/external-url.ts +21 -0
- package/shared/hub.ts +3 -3
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +2 -1
- package/shared/omp-config.ts +5 -1
- package/shared/omp-plugins.ts +74 -33
- package/shared/omp-settings.ts +8 -1
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +12 -3
- package/shared/quota.ts +2 -1
- package/shared/sessions.ts +2 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { type PluginButtonContentProps, useAgent, usePaseo } from "@getpaseo/plugin/client";
|
|
2
|
+
import { TextInput } from "@getpaseo/plugin/client/react-native";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { Pressable, Text, View } from "react-native";
|
|
5
|
+
import { buildOmpMcpServerCommand, type OmpMcpServerAction } from "../shared/mcp";
|
|
6
|
+
|
|
7
|
+
import { isOmpPluginProvider } from "./omp-store-state";
|
|
8
|
+
|
|
9
|
+
const GENERAL_ACTIONS = [
|
|
10
|
+
{ label: "List servers", command: "/mcp list" },
|
|
11
|
+
{ label: "Add server", command: "/mcp add" },
|
|
12
|
+
{ label: "Reload", command: "/mcp reload" },
|
|
13
|
+
{ label: "Resources", command: "/mcp resources" },
|
|
14
|
+
{ label: "Prompts", command: "/mcp prompts" },
|
|
15
|
+
{ label: "Notifications", command: "/mcp notifications" },
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
const SERVER_ACTIONS: ReadonlyArray<{ label: string; action: OmpMcpServerAction }> = [
|
|
19
|
+
{ label: "Test", action: "test" },
|
|
20
|
+
{ label: "Authorize", action: "reauth" },
|
|
21
|
+
{ label: "Enable", action: "enable" },
|
|
22
|
+
{ label: "Disable", action: "disable" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export function McpPopover(props: PluginButtonContentProps) {
|
|
26
|
+
const { theme, layout, close } = props;
|
|
27
|
+
const agentId = props.context === "agent" ? props.agentId : "";
|
|
28
|
+
const agent = useAgent(agentId, ({ provider, status }) => ({ provider, status }));
|
|
29
|
+
const paseo = usePaseo();
|
|
30
|
+
const [serverName, setServerName] = useState("");
|
|
31
|
+
const [pendingCommand, setPendingCommand] = useState<string | null>(null);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const styles = useMemo(
|
|
34
|
+
() => ({
|
|
35
|
+
root: { gap: layout.compact ? 10 : 12, width: layout.compact ? undefined : 340 },
|
|
36
|
+
title: { color: theme.colors.foreground, fontSize: 14, fontWeight: "700" as const },
|
|
37
|
+
muted: { color: theme.colors.foregroundMuted, fontSize: 12, lineHeight: 17 },
|
|
38
|
+
error: { color: theme.colors.statusDanger, fontSize: 12 },
|
|
39
|
+
actions: { flexDirection: "row" as const, flexWrap: "wrap" as const, gap: 7 },
|
|
40
|
+
action: {
|
|
41
|
+
paddingHorizontal: 10,
|
|
42
|
+
paddingVertical: 8,
|
|
43
|
+
borderWidth: 1,
|
|
44
|
+
borderColor: theme.colors.border,
|
|
45
|
+
borderRadius: 8,
|
|
46
|
+
backgroundColor: theme.colors.surface1,
|
|
47
|
+
},
|
|
48
|
+
actionPressed: { opacity: 0.72 },
|
|
49
|
+
actionDisabled: { opacity: 0.45 },
|
|
50
|
+
actionText: { color: theme.colors.foreground, fontSize: 12, fontWeight: "600" as const },
|
|
51
|
+
input: {
|
|
52
|
+
color: theme.colors.foreground,
|
|
53
|
+
borderWidth: 1,
|
|
54
|
+
borderColor: theme.colors.border,
|
|
55
|
+
borderRadius: 8,
|
|
56
|
+
backgroundColor: theme.colors.surface0,
|
|
57
|
+
paddingHorizontal: 10,
|
|
58
|
+
paddingVertical: 8,
|
|
59
|
+
fontSize: 13,
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
[layout.compact, theme],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const targetCommands = SERVER_ACTIONS.map(({ label, action }) => ({
|
|
66
|
+
label,
|
|
67
|
+
command: buildOmpMcpServerCommand(action, serverName),
|
|
68
|
+
}));
|
|
69
|
+
const unavailable = !isOmpPluginProvider(agent?.provider);
|
|
70
|
+
|
|
71
|
+
async function send(command: string): Promise<void> {
|
|
72
|
+
setPendingCommand(command);
|
|
73
|
+
setError(null);
|
|
74
|
+
try {
|
|
75
|
+
await paseo.agents.ref(agentId).send(command);
|
|
76
|
+
close();
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
setError(cause instanceof Error ? cause.message : "Could not send the OMP MCP command.");
|
|
79
|
+
} finally {
|
|
80
|
+
setPendingCommand(null);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (unavailable)
|
|
85
|
+
return <Text style={styles.muted}>MCP controls require an OMP Plugin agent.</Text>;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<View style={styles.root}>
|
|
89
|
+
<Text style={styles.title}>OMP MCP</Text>
|
|
90
|
+
<Text style={styles.muted}>
|
|
91
|
+
Commands run in this OMP session. Results, setup questions, and authorization stay in the
|
|
92
|
+
chat timeline so they remain usable from remote and mobile clients.
|
|
93
|
+
</Text>
|
|
94
|
+
<View style={styles.actions}>
|
|
95
|
+
{GENERAL_ACTIONS.map((action) => (
|
|
96
|
+
<Pressable
|
|
97
|
+
key={action.command}
|
|
98
|
+
accessibilityRole="button"
|
|
99
|
+
accessibilityLabel={action.label}
|
|
100
|
+
disabled={pendingCommand !== null}
|
|
101
|
+
onPress={() => void send(action.command)}
|
|
102
|
+
style={({ pressed }) => [
|
|
103
|
+
styles.action,
|
|
104
|
+
pressed ? styles.actionPressed : null,
|
|
105
|
+
pendingCommand !== null ? styles.actionDisabled : null,
|
|
106
|
+
]}
|
|
107
|
+
>
|
|
108
|
+
<Text style={styles.actionText}>
|
|
109
|
+
{pendingCommand === action.command ? "Sending…" : action.label}
|
|
110
|
+
</Text>
|
|
111
|
+
</Pressable>
|
|
112
|
+
))}
|
|
113
|
+
</View>
|
|
114
|
+
<TextInput
|
|
115
|
+
accessibilityLabel="OMP MCP server name"
|
|
116
|
+
autoCapitalize="none"
|
|
117
|
+
autoCorrect={false}
|
|
118
|
+
onChangeText={setServerName}
|
|
119
|
+
placeholder="Server name"
|
|
120
|
+
placeholderTextColor={theme.colors.foregroundMuted}
|
|
121
|
+
style={styles.input}
|
|
122
|
+
value={serverName}
|
|
123
|
+
/>
|
|
124
|
+
<View style={styles.actions}>
|
|
125
|
+
{targetCommands.map((action) => (
|
|
126
|
+
<Pressable
|
|
127
|
+
key={action.label}
|
|
128
|
+
accessibilityRole="button"
|
|
129
|
+
accessibilityLabel={`${action.label} MCP server`}
|
|
130
|
+
disabled={!action.command || pendingCommand !== null}
|
|
131
|
+
onPress={() => action.command && void send(action.command)}
|
|
132
|
+
style={({ pressed }) => [
|
|
133
|
+
styles.action,
|
|
134
|
+
pressed ? styles.actionPressed : null,
|
|
135
|
+
!action.command || pendingCommand !== null ? styles.actionDisabled : null,
|
|
136
|
+
]}
|
|
137
|
+
>
|
|
138
|
+
<Text style={styles.actionText}>
|
|
139
|
+
{pendingCommand === action.command ? "Sending…" : action.label}
|
|
140
|
+
</Text>
|
|
141
|
+
</Pressable>
|
|
142
|
+
))}
|
|
143
|
+
</View>
|
|
144
|
+
{agent?.status === "running" ? (
|
|
145
|
+
<Text style={styles.muted}>The command may wait until the active turn finishes.</Text>
|
|
146
|
+
) : null}
|
|
147
|
+
{serverName.trim() && targetCommands.every((action) => !action.command) ? (
|
|
148
|
+
<Text style={styles.error}>
|
|
149
|
+
Server names may contain letters, numbers, dash, underscore, dot, and colon.
|
|
150
|
+
</Text>
|
|
151
|
+
) : null}
|
|
152
|
+
{error ? <Text style={styles.error}>{error}</Text> : null}
|
|
153
|
+
</View>
|
|
154
|
+
);
|
|
155
|
+
}
|
package/client/memory-panel.tsx
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import type { PluginWorkspacePanelProps } from "@getpaseo/plugin/client";
|
|
2
2
|
import { useRpc, useWorkspace } from "@getpaseo/plugin/client";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
|
-
import { useMemo } from "react";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
5
|
import { ScrollView, Text, View } from "react-native";
|
|
6
6
|
import { listOmpMemory } from "../shared/memory";
|
|
7
|
+
import type { OmpStore } from "../shared/omp-store";
|
|
8
|
+
import { OmpStorePicker } from "./omp-store-picker";
|
|
9
|
+
import { ompStoreKey } from "./omp-store-state";
|
|
7
10
|
|
|
8
11
|
const MEMORY_POLL_MS = 15_000;
|
|
9
12
|
|
|
10
13
|
export function OmpMemoryPanel({ theme, layout, workspaceId }: PluginWorkspacePanelProps) {
|
|
11
14
|
const directory = useWorkspace(workspaceId, (workspace) => workspace.directory) ?? "";
|
|
15
|
+
const [store, setStore] = useState<OmpStore>();
|
|
12
16
|
const loadMemory = useRpc(listOmpMemory);
|
|
13
17
|
const memory = useQuery({
|
|
14
|
-
queryKey: ["paseo-omp", "memory", directory],
|
|
15
|
-
queryFn: () => loadMemory({ cwd: directory }),
|
|
18
|
+
queryKey: ["paseo-omp", "memory", ompStoreKey(store), directory],
|
|
19
|
+
queryFn: () => loadMemory({ cwd: directory, store }),
|
|
16
20
|
enabled: directory.length > 0,
|
|
17
21
|
refetchInterval: MEMORY_POLL_MS,
|
|
18
22
|
});
|
|
@@ -53,6 +57,7 @@ export function OmpMemoryPanel({ theme, layout, workspaceId }: PluginWorkspacePa
|
|
|
53
57
|
{memory.data?.bank ? `Bank: ${memory.data.bank}` : "Retained workspace facts"}
|
|
54
58
|
</Text>
|
|
55
59
|
</View>
|
|
60
|
+
<OmpStorePicker theme={theme} store={store} onChange={setStore} />
|
|
56
61
|
{memory.isLoading ? <Text style={styles.subtitle}>Loading retained facts…</Text> : null}
|
|
57
62
|
{memory.error ? <Text style={styles.error}>Could not read workspace memory.</Text> : null}
|
|
58
63
|
{!memory.isLoading && !memory.error && (memory.data?.facts.length ?? 0) === 0 ? (
|
|
@@ -3,6 +3,8 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import { Text, View } from "react-native";
|
|
5
5
|
import { listOmpMemory } from "../shared/memory";
|
|
6
|
+
import { storeForProvider, storeLabel } from "../shared/omp-store";
|
|
7
|
+
import { ompStoreKey } from "./omp-store-state";
|
|
6
8
|
|
|
7
9
|
const MEMORY_POLL_MS = 15_000;
|
|
8
10
|
const PREVIEW_LIMIT = 20;
|
|
@@ -10,11 +12,13 @@ const PREVIEW_LIMIT = 20;
|
|
|
10
12
|
export function MemoryPopover(props: PluginButtonContentProps) {
|
|
11
13
|
const { theme, layout } = props;
|
|
12
14
|
const agentId = props.context === "agent" ? props.agentId : "";
|
|
13
|
-
const
|
|
15
|
+
const agent = useAgent(agentId, ({ cwd, provider }) => ({ cwd, provider }));
|
|
16
|
+
const cwd = agent?.cwd ?? "";
|
|
17
|
+
const store = storeForProvider(agent?.provider);
|
|
14
18
|
const loadMemory = useRpc(listOmpMemory);
|
|
15
19
|
const memory = useQuery({
|
|
16
|
-
queryKey: ["paseo-omp", "memory", cwd],
|
|
17
|
-
queryFn: () => loadMemory({ cwd }),
|
|
20
|
+
queryKey: ["paseo-omp", "memory", ompStoreKey(store), cwd],
|
|
21
|
+
queryFn: () => loadMemory({ cwd, store }),
|
|
18
22
|
enabled: cwd.length > 0,
|
|
19
23
|
refetchInterval: MEMORY_POLL_MS,
|
|
20
24
|
});
|
|
@@ -50,7 +54,7 @@ export function MemoryPopover(props: PluginButtonContentProps) {
|
|
|
50
54
|
return (
|
|
51
55
|
<View style={styles.root}>
|
|
52
56
|
<View style={styles.header}>
|
|
53
|
-
<Text style={styles.title}>OMP Memory</Text>
|
|
57
|
+
<Text style={styles.title}>OMP Memory · {storeLabel(store)}</Text>
|
|
54
58
|
<Text style={styles.muted}>{facts.length} facts</Text>
|
|
55
59
|
</View>
|
|
56
60
|
<Text style={styles.muted}>{memory.data?.bank ?? "Workspace memory"}</Text>
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type PluginSurfaceProps,
|
|
3
|
+
type PluginWorkspacePanelProps,
|
|
4
|
+
usePaseo,
|
|
5
|
+
useRpc,
|
|
6
|
+
useWorkspace,
|
|
7
|
+
} from "@getpaseo/plugin/client";
|
|
2
8
|
import { Icon, TextInput } from "@getpaseo/plugin/client/react-native";
|
|
3
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
9
|
+
import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
10
|
import type { ReactNode } from "react";
|
|
5
11
|
import { useCallback, useMemo, useState } from "react";
|
|
6
12
|
import type { TextStyle, ViewStyle } from "react-native";
|
|
7
|
-
import {
|
|
13
|
+
import { Pressable, ScrollView, Switch, Text, View } from "react-native";
|
|
14
|
+
import type { ComposerPillSettings } from "../shared/composer-pill-settings";
|
|
8
15
|
import { listOmpConfig, type OmpConfig } from "../shared/omp-config";
|
|
9
16
|
import {
|
|
10
17
|
categorizeOmpSetting,
|
|
@@ -16,7 +23,10 @@ import {
|
|
|
16
23
|
type OmpSettingCategory,
|
|
17
24
|
updateOmpSettings,
|
|
18
25
|
} from "../shared/omp-settings";
|
|
26
|
+
import { type OmpStore, storeLabel } from "../shared/omp-store";
|
|
19
27
|
import { getOmpProviderHealth, type OmpProviderHealth } from "../shared/provider-diagnostics";
|
|
28
|
+
import { ComposerPillSettingsSection } from "./composer-pill-settings";
|
|
29
|
+
import { openOmpExternalUrl } from "./external-url";
|
|
20
30
|
import {
|
|
21
31
|
documentationForSettingCategory,
|
|
22
32
|
documentationForSettingPath,
|
|
@@ -25,6 +35,8 @@ import {
|
|
|
25
35
|
type OmpDocumentationLink,
|
|
26
36
|
} from "./omp-doc-links";
|
|
27
37
|
import { OmpPluginManagerSection } from "./omp-plugin-manager";
|
|
38
|
+
import { OmpStorePicker } from "./omp-store-picker";
|
|
39
|
+
import { ompStoreKey } from "./omp-store-state";
|
|
28
40
|
import {
|
|
29
41
|
type BinaryHealthSummary,
|
|
30
42
|
loadReadyProviderSnapshot,
|
|
@@ -560,7 +572,7 @@ function ProcessSection({
|
|
|
560
572
|
health: OmpProviderHealth;
|
|
561
573
|
}) {
|
|
562
574
|
return (
|
|
563
|
-
<SectionCard styles={styles} title="
|
|
575
|
+
<SectionCard styles={styles} title="Hub metadata">
|
|
564
576
|
<KeyValueRow
|
|
565
577
|
styles={styles}
|
|
566
578
|
label="OMP Hub"
|
|
@@ -574,16 +586,20 @@ function ProcessSection({
|
|
|
574
586
|
function ProviderHealthSection({
|
|
575
587
|
theme,
|
|
576
588
|
styles,
|
|
589
|
+
cwd,
|
|
590
|
+
store,
|
|
577
591
|
}: {
|
|
578
592
|
theme: PluginSurfaceProps["theme"];
|
|
579
593
|
styles: OmpConfigStyles;
|
|
594
|
+
cwd?: string;
|
|
595
|
+
store?: OmpStore;
|
|
580
596
|
}) {
|
|
581
597
|
const paseo = usePaseo();
|
|
582
598
|
const queryClient = useQueryClient();
|
|
583
599
|
const loadHealth = useRpc(getOmpProviderHealth);
|
|
584
600
|
const health = useQuery({
|
|
585
|
-
queryKey: HEALTH_QUERY_KEY,
|
|
586
|
-
queryFn: () => loadHealth({}),
|
|
601
|
+
queryKey: [...HEALTH_QUERY_KEY, ompStoreKey(store), cwd ?? "global"],
|
|
602
|
+
queryFn: () => loadHealth({ store, ...(cwd ? { cwd } : {}) }),
|
|
587
603
|
});
|
|
588
604
|
const providers = useQuery({
|
|
589
605
|
queryKey: PROVIDERS_QUERY_KEY,
|
|
@@ -593,8 +609,16 @@ function ProviderHealthSection({
|
|
|
593
609
|
mutationFn: async () => {
|
|
594
610
|
const result = await refreshProviderDiagnostics({
|
|
595
611
|
providers: paseo.providers,
|
|
596
|
-
|
|
597
|
-
|
|
612
|
+
providerIds: [
|
|
613
|
+
...selectKnownOmpProviders(providers.data?.entries ?? []).map((provider) => provider.id),
|
|
614
|
+
...(store?.profile ? [`omp-plugin-${store.profile}`] : []),
|
|
615
|
+
],
|
|
616
|
+
loadForcedHealth: () => loadHealth({ store, force: true, ...(cwd ? { cwd } : {}) }),
|
|
617
|
+
cacheHealth: (value) =>
|
|
618
|
+
queryClient.setQueryData(
|
|
619
|
+
[...HEALTH_QUERY_KEY, ompStoreKey(store), cwd ?? "global"],
|
|
620
|
+
value,
|
|
621
|
+
),
|
|
598
622
|
cacheProviders: (value) => queryClient.setQueryData(PROVIDERS_QUERY_KEY, value),
|
|
599
623
|
});
|
|
600
624
|
if (result.failed) throw new Error("Could not fully refresh OMP provider health.");
|
|
@@ -621,8 +645,9 @@ function ProviderHealthSection({
|
|
|
621
645
|
</View>
|
|
622
646
|
</View>
|
|
623
647
|
<Text style={styles.muted}>
|
|
624
|
-
|
|
625
|
-
|
|
648
|
+
Binary probes run from {cwd ? "this workspace" : "the daemon working directory"}.
|
|
649
|
+
Configuration, storage, MCP, and databases use {storeLabel(store)}. Hub metadata remains
|
|
650
|
+
shared across profiles.
|
|
626
651
|
</Text>
|
|
627
652
|
|
|
628
653
|
{health.isLoading ? <Text style={styles.muted}>Checking the omp installation…</Text> : null}
|
|
@@ -711,11 +736,12 @@ function fallbackSettingsFromConfig(config: OmpConfig | null | undefined): OmpSe
|
|
|
711
736
|
return settings;
|
|
712
737
|
}
|
|
713
738
|
|
|
714
|
-
type SurfaceView = "overview" | "plugin" | "plugins" | "configuration" | "diagnostics";
|
|
739
|
+
type SurfaceView = "overview" | "plugin" | "plugins" | "composer" | "configuration" | "diagnostics";
|
|
715
740
|
const SURFACE_VIEWS: readonly { id: SurfaceView; label: string }[] = [
|
|
716
741
|
{ id: "overview", label: "Overview" },
|
|
717
742
|
{ id: "plugin", label: "Plugin" },
|
|
718
743
|
{ id: "plugins", label: "OMP plugins" },
|
|
744
|
+
{ id: "composer", label: "Composer" },
|
|
719
745
|
{ id: "configuration", label: "Configuration" },
|
|
720
746
|
{ id: "diagnostics", label: "Diagnostics" },
|
|
721
747
|
];
|
|
@@ -814,6 +840,8 @@ function EditableScalarValue({
|
|
|
814
840
|
setting,
|
|
815
841
|
draft,
|
|
816
842
|
disabled,
|
|
843
|
+
resetLabel,
|
|
844
|
+
showReset,
|
|
817
845
|
styles,
|
|
818
846
|
onSet,
|
|
819
847
|
onReset,
|
|
@@ -821,6 +849,8 @@ function EditableScalarValue({
|
|
|
821
849
|
setting: OmpSetting;
|
|
822
850
|
draft: SettingDraft | undefined;
|
|
823
851
|
disabled: boolean;
|
|
852
|
+
resetLabel: string;
|
|
853
|
+
showReset: boolean;
|
|
824
854
|
styles: OmpConfigStyles;
|
|
825
855
|
onSet(value: string | boolean): void;
|
|
826
856
|
onReset(): void;
|
|
@@ -829,7 +859,7 @@ function EditableScalarValue({
|
|
|
829
859
|
return (
|
|
830
860
|
<View style={styles.recordList}>
|
|
831
861
|
{draft?.operation === "reset" ? (
|
|
832
|
-
<Text style={styles.muted}>
|
|
862
|
+
<Text style={styles.muted}>{resetLabel} when changes are applied</Text>
|
|
833
863
|
) : setting.type === "boolean" ? (
|
|
834
864
|
<Switch
|
|
835
865
|
accessibilityLabel={`Toggle ${formatOmpSettingLabel(setting.path)}`}
|
|
@@ -847,9 +877,11 @@ function EditableScalarValue({
|
|
|
847
877
|
style={styles.scalarInput}
|
|
848
878
|
/>
|
|
849
879
|
)}
|
|
850
|
-
|
|
851
|
-
<
|
|
852
|
-
|
|
880
|
+
{showReset ? (
|
|
881
|
+
<Pressable accessibilityRole="button" disabled={disabled} onPress={onReset}>
|
|
882
|
+
<Text style={styles.resetAction}>{resetLabel}</Text>
|
|
883
|
+
</Pressable>
|
|
884
|
+
) : null}
|
|
853
885
|
</View>
|
|
854
886
|
);
|
|
855
887
|
}
|
|
@@ -860,6 +892,7 @@ function ConfigurationCategory({
|
|
|
860
892
|
settings,
|
|
861
893
|
drafts,
|
|
862
894
|
disabled,
|
|
895
|
+
workspaceScoped,
|
|
863
896
|
onDraft,
|
|
864
897
|
onOpenDocumentation,
|
|
865
898
|
}: {
|
|
@@ -868,6 +901,7 @@ function ConfigurationCategory({
|
|
|
868
901
|
settings: readonly OmpSetting[];
|
|
869
902
|
drafts: Readonly<Record<string, SettingDraft>>;
|
|
870
903
|
disabled: boolean;
|
|
904
|
+
workspaceScoped: boolean;
|
|
871
905
|
onDraft(path: string, draft: SettingDraft): void;
|
|
872
906
|
onOpenDocumentation(link: OmpDocumentationLink): void;
|
|
873
907
|
}) {
|
|
@@ -897,6 +931,9 @@ function ConfigurationCategory({
|
|
|
897
931
|
<View key={setting.path} style={styles.setting}>
|
|
898
932
|
<View style={styles.settingHeader}>
|
|
899
933
|
<Text style={styles.cardTitle}>{formatOmpSettingLabel(setting.path)}</Text>
|
|
934
|
+
{setting.workspaceOverride ? (
|
|
935
|
+
<Text style={styles.source}>Workspace override</Text>
|
|
936
|
+
) : null}
|
|
900
937
|
{!complex && !editable ? (
|
|
901
938
|
<StructuredSettingValue setting={setting} styles={styles} />
|
|
902
939
|
) : null}
|
|
@@ -918,6 +955,8 @@ function ConfigurationCategory({
|
|
|
918
955
|
setting={setting}
|
|
919
956
|
draft={drafts[setting.path]}
|
|
920
957
|
disabled={disabled}
|
|
958
|
+
resetLabel={workspaceScoped ? "Remove workspace override" : "Reset to default"}
|
|
959
|
+
showReset={!workspaceScoped || setting.workspaceOverride === true}
|
|
921
960
|
styles={styles}
|
|
922
961
|
onSet={(value) => onDraft(setting.path, { operation: "set", value })}
|
|
923
962
|
onReset={() => onDraft(setting.path, { operation: "reset" })}
|
|
@@ -938,15 +977,17 @@ function ConfigurationCategory({
|
|
|
938
977
|
function SurfaceTabs({
|
|
939
978
|
styles,
|
|
940
979
|
selected,
|
|
980
|
+
views,
|
|
941
981
|
onSelect,
|
|
942
982
|
}: {
|
|
943
983
|
styles: OmpConfigStyles;
|
|
944
984
|
selected: SurfaceView;
|
|
985
|
+
views: readonly { id: SurfaceView; label: string }[];
|
|
945
986
|
onSelect: (view: SurfaceView) => void;
|
|
946
987
|
}) {
|
|
947
988
|
return (
|
|
948
989
|
<View accessibilityRole="tablist" style={styles.topTabs}>
|
|
949
|
-
{
|
|
990
|
+
{views.map((view) => {
|
|
950
991
|
const active = selected === view.id;
|
|
951
992
|
return (
|
|
952
993
|
<Pressable
|
|
@@ -965,19 +1006,34 @@ function SurfaceTabs({
|
|
|
965
1006
|
</View>
|
|
966
1007
|
);
|
|
967
1008
|
}
|
|
968
|
-
|
|
1009
|
+
function OmpConfigContent({
|
|
1010
|
+
theme,
|
|
1011
|
+
layout,
|
|
1012
|
+
cwd,
|
|
1013
|
+
store,
|
|
1014
|
+
onStoreChange,
|
|
1015
|
+
onComposerPillSettingsChange,
|
|
1016
|
+
}: PluginSurfaceProps & {
|
|
1017
|
+
cwd?: string;
|
|
1018
|
+
store?: OmpStore;
|
|
1019
|
+
onStoreChange(store: OmpStore | undefined): void;
|
|
1020
|
+
onComposerPillSettingsChange?: (settings: ComposerPillSettings) => void;
|
|
1021
|
+
}) {
|
|
969
1022
|
const loadConfig = useRpc(listOmpConfig);
|
|
970
1023
|
const loadSettings = useRpc(listOmpSettings);
|
|
971
1024
|
const updateSettings = useRpc(updateOmpSettings);
|
|
972
1025
|
const queryClient = useQueryClient();
|
|
1026
|
+
const context = { store, ...(cwd ? { cwd } : {}) };
|
|
1027
|
+
const pendingMutations = useIsMutating({ mutationKey: ["paseo-omp"] });
|
|
973
1028
|
const configQuery = useQuery({
|
|
974
|
-
queryKey: ["paseo-omp", "config"],
|
|
975
|
-
queryFn: () => loadConfig(
|
|
1029
|
+
queryKey: ["paseo-omp", "config", ompStoreKey(store), cwd ?? "global"],
|
|
1030
|
+
queryFn: () => loadConfig(context),
|
|
976
1031
|
refetchInterval: CONFIG_POLL_MS,
|
|
977
1032
|
});
|
|
1033
|
+
const settingsQueryKey = [...SETTINGS_QUERY_KEY, ompStoreKey(store), cwd ?? "global"];
|
|
978
1034
|
const settingsQuery = useQuery({
|
|
979
|
-
queryKey:
|
|
980
|
-
queryFn: () => loadSettings(
|
|
1035
|
+
queryKey: settingsQueryKey,
|
|
1036
|
+
queryFn: () => loadSettings(context),
|
|
981
1037
|
staleTime: Number.POSITIVE_INFINITY,
|
|
982
1038
|
});
|
|
983
1039
|
const [view, setView] = useState<SurfaceView>("overview");
|
|
@@ -987,6 +1043,9 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
987
1043
|
const [documentationError, setDocumentationError] = useState<string | null>(null);
|
|
988
1044
|
const styles = useConfigStyles(theme, layout.compact);
|
|
989
1045
|
const normalizedSearch = search.trim().toLocaleLowerCase();
|
|
1046
|
+
const surfaceViews = cwd
|
|
1047
|
+
? SURFACE_VIEWS.filter((candidate) => candidate.id !== "composer")
|
|
1048
|
+
: SURFACE_VIEWS;
|
|
990
1049
|
const catalog = useMemo(() => {
|
|
991
1050
|
const sourceSettings = settingsQuery.data?.available
|
|
992
1051
|
? settingsQuery.data.settings
|
|
@@ -1014,13 +1073,14 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
1014
1073
|
const openDocumentation = useCallback(async (link: OmpDocumentationLink) => {
|
|
1015
1074
|
setDocumentationError(null);
|
|
1016
1075
|
try {
|
|
1017
|
-
await
|
|
1076
|
+
await openOmpExternalUrl(link.url);
|
|
1018
1077
|
} catch {
|
|
1019
1078
|
setDocumentationError(`Could not open ${link.label.toLocaleLowerCase()}.`);
|
|
1020
1079
|
}
|
|
1021
1080
|
}, []);
|
|
1022
1081
|
|
|
1023
1082
|
const save = useMutation({
|
|
1083
|
+
mutationKey: ["paseo-omp", "settings", ompStoreKey(store)],
|
|
1024
1084
|
mutationFn: async () => {
|
|
1025
1085
|
const revision = settingsQuery.data?.revision;
|
|
1026
1086
|
if (!revision) throw new Error("OMP settings cannot be edited without a current revision.");
|
|
@@ -1039,10 +1099,12 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
1039
1099
|
}
|
|
1040
1100
|
return { operation: "set" as const, path, value };
|
|
1041
1101
|
});
|
|
1042
|
-
return updateSettings({ revision, changes });
|
|
1102
|
+
return updateSettings({ ...context, revision, changes });
|
|
1043
1103
|
},
|
|
1044
1104
|
onSuccess: (result) => {
|
|
1045
|
-
queryClient.setQueryData(
|
|
1105
|
+
queryClient.setQueryData(settingsQueryKey, result.catalog);
|
|
1106
|
+
void queryClient.invalidateQueries({ queryKey: SETTINGS_QUERY_KEY });
|
|
1107
|
+
void queryClient.invalidateQueries({ queryKey: ["paseo-omp", "config"] });
|
|
1046
1108
|
if (
|
|
1047
1109
|
!result.conflict &&
|
|
1048
1110
|
!result.failed &&
|
|
@@ -1059,14 +1121,35 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
1059
1121
|
},
|
|
1060
1122
|
});
|
|
1061
1123
|
const draftCount = Object.keys(drafts).length;
|
|
1124
|
+
const workspaceOverrideCount = catalog.sourceSettings.filter(
|
|
1125
|
+
(setting) => setting.workspaceOverride,
|
|
1126
|
+
).length;
|
|
1062
1127
|
const displayedConfigPath = settingsQuery.data?.available
|
|
1063
1128
|
? settingsQuery.data.path
|
|
1064
1129
|
: configQuery.data?.path;
|
|
1065
1130
|
|
|
1066
1131
|
return (
|
|
1067
1132
|
<ScrollView contentContainerStyle={styles.root}>
|
|
1068
|
-
<Text style={styles.pageTitle}>OMP</Text>
|
|
1069
|
-
|
|
1133
|
+
<Text style={styles.pageTitle}>{cwd ? "Workspace OMP" : "OMP"}</Text>
|
|
1134
|
+
{cwd ? (
|
|
1135
|
+
<Text selectable style={styles.muted}>
|
|
1136
|
+
Project-scoped view · {cwd}
|
|
1137
|
+
</Text>
|
|
1138
|
+
) : null}
|
|
1139
|
+
{view !== "composer" ? (
|
|
1140
|
+
<>
|
|
1141
|
+
<OmpStorePicker
|
|
1142
|
+
theme={theme}
|
|
1143
|
+
store={store}
|
|
1144
|
+
onChange={onStoreChange}
|
|
1145
|
+
disabled={pendingMutations > 0}
|
|
1146
|
+
/>
|
|
1147
|
+
<Text style={styles.muted}>
|
|
1148
|
+
Switching stores clears unapplied edits and pending confirmations.
|
|
1149
|
+
</Text>
|
|
1150
|
+
</>
|
|
1151
|
+
) : null}
|
|
1152
|
+
<SurfaceTabs styles={styles} selected={view} views={surfaceViews} onSelect={setView} />
|
|
1070
1153
|
|
|
1071
1154
|
{view === "overview" ? (
|
|
1072
1155
|
<>
|
|
@@ -1078,12 +1161,29 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
1078
1161
|
Could not read the native OMP configuration.
|
|
1079
1162
|
</Text>
|
|
1080
1163
|
) : (
|
|
1081
|
-
<SectionCard
|
|
1164
|
+
<SectionCard
|
|
1165
|
+
styles={styles}
|
|
1166
|
+
title={cwd ? "Workspace configuration" : "Native configuration"}
|
|
1167
|
+
>
|
|
1082
1168
|
<KeyValueRow
|
|
1083
1169
|
styles={styles}
|
|
1084
1170
|
label="Source"
|
|
1085
1171
|
value={displayedConfigPath ?? "Source unavailable"}
|
|
1086
1172
|
/>
|
|
1173
|
+
{cwd ? (
|
|
1174
|
+
<>
|
|
1175
|
+
<KeyValueRow styles={styles} label="Scope" value="Workspace / project" />
|
|
1176
|
+
<KeyValueRow
|
|
1177
|
+
styles={styles}
|
|
1178
|
+
label="Overrides"
|
|
1179
|
+
value={`${workspaceOverrideCount} project-specific settings`}
|
|
1180
|
+
/>
|
|
1181
|
+
<Text style={styles.muted}>
|
|
1182
|
+
Settings without a workspace override inherit their effective global or default
|
|
1183
|
+
value.
|
|
1184
|
+
</Text>
|
|
1185
|
+
</>
|
|
1186
|
+
) : null}
|
|
1087
1187
|
<KeyValueRow
|
|
1088
1188
|
styles={styles}
|
|
1089
1189
|
label="Status"
|
|
@@ -1102,10 +1202,15 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
1102
1202
|
{view === "plugin" ? <PluginConfigurationSection styles={styles} /> : null}
|
|
1103
1203
|
|
|
1104
1204
|
{view === "plugins" ? (
|
|
1105
|
-
<OmpPluginManagerSection theme={theme} compact={layout.compact} />
|
|
1205
|
+
<OmpPluginManagerSection theme={theme} compact={layout.compact} cwd={cwd} store={store} />
|
|
1206
|
+
) : null}
|
|
1207
|
+
{view === "composer" && !cwd && onComposerPillSettingsChange ? (
|
|
1208
|
+
<ComposerPillSettingsSection theme={theme} onChange={onComposerPillSettingsChange} />
|
|
1106
1209
|
) : null}
|
|
1107
1210
|
|
|
1108
|
-
{view === "diagnostics" ?
|
|
1211
|
+
{view === "diagnostics" ? (
|
|
1212
|
+
<ProviderHealthSection theme={theme} styles={styles} cwd={cwd} store={store} />
|
|
1213
|
+
) : null}
|
|
1109
1214
|
|
|
1110
1215
|
{view === "configuration" ? (
|
|
1111
1216
|
<>
|
|
@@ -1147,6 +1252,13 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
1147
1252
|
) : (
|
|
1148
1253
|
<Text style={styles.source}>Source unavailable</Text>
|
|
1149
1254
|
)}
|
|
1255
|
+
{cwd ? (
|
|
1256
|
+
<Text style={styles.muted}>
|
|
1257
|
+
{workspaceOverrideCount} project-specific overrides. All other effective values
|
|
1258
|
+
inherit global configuration or OMP defaults. Applying a change creates or updates
|
|
1259
|
+
the override in .omp/config.yml; removing an override restores inheritance.
|
|
1260
|
+
</Text>
|
|
1261
|
+
) : null}
|
|
1150
1262
|
</View>
|
|
1151
1263
|
{documentationError ? (
|
|
1152
1264
|
<Text accessibilityRole="alert" style={styles.error}>
|
|
@@ -1256,6 +1368,7 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
1256
1368
|
settings={catalog.byCategory.get(selectedCategory.id) ?? []}
|
|
1257
1369
|
drafts={drafts}
|
|
1258
1370
|
disabled={!settingsQuery.data?.revision || save.isPending}
|
|
1371
|
+
workspaceScoped={cwd !== undefined}
|
|
1259
1372
|
onDraft={(path, draft) =>
|
|
1260
1373
|
setDrafts((current) => ({ ...current, [path]: draft }))
|
|
1261
1374
|
}
|
|
@@ -1272,3 +1385,50 @@ export function OmpConfigSurface({ theme, layout }: PluginSurfaceProps) {
|
|
|
1272
1385
|
</ScrollView>
|
|
1273
1386
|
);
|
|
1274
1387
|
}
|
|
1388
|
+
|
|
1389
|
+
function OmpStoreContent(
|
|
1390
|
+
props: PluginSurfaceProps & {
|
|
1391
|
+
cwd?: string;
|
|
1392
|
+
onComposerPillSettingsChange?: (settings: ComposerPillSettings) => void;
|
|
1393
|
+
},
|
|
1394
|
+
) {
|
|
1395
|
+
const [store, setStore] = useState<OmpStore>();
|
|
1396
|
+
// Remount every editor when its target changes: drafts, confirmations, and mutation notices
|
|
1397
|
+
// belong to one store/workspace and must never be applied to the next selection.
|
|
1398
|
+
return (
|
|
1399
|
+
<OmpConfigContent
|
|
1400
|
+
key={`${ompStoreKey(store)}:${props.cwd ?? "global"}`}
|
|
1401
|
+
{...props}
|
|
1402
|
+
store={store}
|
|
1403
|
+
onStoreChange={setStore}
|
|
1404
|
+
/>
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
export function OmpConfigSurface(
|
|
1409
|
+
props: PluginSurfaceProps & {
|
|
1410
|
+
onComposerPillSettingsChange: (settings: ComposerPillSettings) => void;
|
|
1411
|
+
},
|
|
1412
|
+
) {
|
|
1413
|
+
return <OmpStoreContent {...props} />;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
export function OmpWorkspacePanel(props: PluginWorkspacePanelProps) {
|
|
1417
|
+
const cwd = useWorkspace(props.workspaceId, (workspace) => workspace.directory);
|
|
1418
|
+
if (!cwd) {
|
|
1419
|
+
return (
|
|
1420
|
+
<View
|
|
1421
|
+
style={{
|
|
1422
|
+
flex: 1,
|
|
1423
|
+
padding: props.layout.compact ? 16 : 24,
|
|
1424
|
+
backgroundColor: props.theme.colors.surface0,
|
|
1425
|
+
}}
|
|
1426
|
+
>
|
|
1427
|
+
<Text style={{ color: props.theme.colors.foregroundMuted }}>
|
|
1428
|
+
Loading workspace OMP settings…
|
|
1429
|
+
</Text>
|
|
1430
|
+
</View>
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
return <OmpStoreContent {...props} cwd={cwd} />;
|
|
1434
|
+
}
|