@omercnet/paseo-omp 1.1.0 → 1.2.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/README.md CHANGED
@@ -24,7 +24,7 @@ before capture.
24
24
 
25
25
  ## Quick start
26
26
 
27
- Requirements: Paseo `^0.9.2 || ^0.10.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Paseo 0.9.2 is required for reliable plugin-provider request failure containment, reload cleanup, and daemon shutdown ordering; 0.10.1 is verified against the same public plugin contracts.
27
+ Requirements: Paseo `^0.9.2 || ^0.10.0 || ^0.11.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Paseo 0.9.2 is required for reliable plugin-provider request failure containment, reload cleanup, and daemon shutdown ordering; 0.10.1 is verified against the same public plugin contracts.
28
28
 
29
29
  ```bash
30
30
  paseo plugin install npm:@omercnet/paseo-omp@<version>
@@ -54,9 +54,13 @@ Profile selection is request-local: concurrent clients cannot change each other'
54
54
 
55
55
  ## Paseo provider SDK coverage
56
56
 
57
- This matrix inventories the complete capability set exported by the pinned `@getpaseo/plugin` 0.10.1 provider SDK, whose public provider surface is unchanged since 0.9.0; compatibility is checked nightly against the 0.9, 0.10, and latest SDK channels. It measures strict SDK surface coverage, not general product quality.
57
+ The capability inventory is pinned to `@getpaseo/plugin` 0.11.0-beta.3 (PR #251), with nightly
58
+ compatibility checks against supported 0.9 and 0.10 hosts. Provider capability scoring still covers
59
+ all 17 entries in the SDK's `PROVIDER_CAPABILITIES`; the separate provider/client SDK table below
60
+ also tracks the 0.11 registration, screen, and sidebar APIs. This measures strict SDK surface
61
+ coverage, not general product quality.
58
62
 
59
- **Current capability completeness: 61.8%.** The provider advertises 11 of 17 capabilities (64.7%); ten are complete and `session.configure` is partial.
63
+ **Provider capability completeness: 61.8%.** The provider advertises 11 of 17 capabilities (64.7%); ten are complete and `session.configure` is partial.
60
64
 
61
65
  Scoring is deliberately mechanical so releases remain comparable:
62
66
 
@@ -91,7 +95,7 @@ Provider functionality outside the capability flags is tracked separately:
91
95
  | --- | ---: | --- |
92
96
  | Registration metadata and sanitized SVG icon | **100%** | Stable `omp-plugin` identity, label, description, and bundled icon. |
93
97
  | Strict `providerOptionsSchema` | **100%** | Command, literal environment, names-only profile inherited environment, output-redaction mode, session directory, RPC timeout, and role-model options are validated and normalized. Host-wide inherited names are separately persisted and merged at launch. |
94
- | Availability diagnostics | **100%** | Bounded checks distinguish missing, unrunnable, incompatible, and available OMP runtimes. |
98
+ | Availability diagnostics | **50%** | The plugin implements a `checkAvailability` hook with bounded checks that distinguish missing, unrunnable, incompatible, and available OMP runtimes, but no released daemon calls it (no upstream reference at v0.10.2 or v0.11.0-beta.3). Hosts treat the provider as available when a connection opens, and launch failures surface at session start. |
95
99
  | Catalog cache identity | **100%** | Hash includes effective options, merged host/profile inherited-environment names, settings, scope, cwd, and default command, but never resolves or fingerprints inherited values. |
96
100
  | Models, modes, and thinking catalog | **100%** | Native catalog is mapped to opaque public model IDs with committed defaults and permission-gated modes. |
97
101
  | Connection `send` / `onEvent` / `close` lifecycle | **100%** | Request correlation, multi-session ownership, process recovery, teardown, and provider reload/removal are covered. |
@@ -101,6 +105,9 @@ Provider functionality outside the capability flags is tracked separately:
101
105
  | Commands and committed session state events | **100%** | Publishes `session.commands`, `session.opened`, `session.config`, `session.ready`, and request completion in protocol order. |
102
106
  | Built-in timeline snapshots | **100%** | Assistant, reasoning, tools, todos, notifications, errors, compaction, and friendly MCP labels use stable IDs and complete snapshots. OMP emits client-safe PNG/JPEG when possible; validated legacy WebP images render on capable clients with an explicit per-image fallback elsewhere. |
103
107
  | Usage reporting | **100%** | Periodic, post-compaction, fallback, terminal, timeout, and recovered-runtime samples publish `session.usage`. |
108
+ | 0.11 provider `status()` | **0%** | Not registered. Its request carries only an optional daemon-resolved `launch`; the daemon supplies one only when a registration declares `command`, which would move launch ownership to the daemon. Without that, status cannot see per-agent `providerOptions.command`, so probing a guessed default could wrongly mark a valid custom-command provider unavailable. |
109
+ | 0.11 screens and sidebar items | **100%** | The global config surface migrates to `addScreen({ id: "config" })`, preserving legacy links and encoding profile selection in screen params. The Hub status row uses `addSidebarHeaderItem` + `SidebarRow`, lists processes and tails logs in a reusable popover. Hosts without the 0.11 APIs retain the static config row/surface; the Hub row is 0.11-only. |
110
+ | 0.11 usage source | **0%** | Not registered. `server/quota.ts` reads OMP's recorded `usage_history`, not live quota, so there is no live fetcher to register. Upstream Claude and Codex sources fetch live quota for the Anthropic and Codex OAuth credentials in the daemon's default or `OMP_PROFILE` store; other providers and named profiles remain covered only by this plugin's historical quota pill. |
104
111
 
105
112
  Tracking rules:
106
113
 
@@ -1,4 +1,9 @@
1
- import { type PluginButtonContentProps, useAgent, useRpc } from "@getpaseo/plugin/client";
1
+ import {
2
+ type PluginButtonContentProps,
3
+ type PluginHostProps,
4
+ useAgent,
5
+ useRpc,
6
+ } from "@getpaseo/plugin/client";
2
7
  import { useQuery } from "@tanstack/react-query";
3
8
  import { useMemo, useState } from "react";
4
9
  import { Pressable, Text, View } from "react-native";
@@ -34,16 +39,31 @@ function toneColor(
34
39
  }
35
40
 
36
41
  export function HubPopover(props: PluginButtonContentProps) {
37
- const { theme, layout } = props;
38
42
  const agentId = props.context === "agent" ? props.agentId : "";
39
43
  const cwd = useAgent(agentId, (agent) => agent.cwd) ?? "";
44
+ return <HubProcessList theme={props.theme} layout={props.layout} cwd={cwd} />;
45
+ }
46
+
47
+ /**
48
+ * Process cards with a per-process log tail, for one workspace directory. Pass `processes` when
49
+ * the caller already polls them; the list then skips its own process query.
50
+ */
51
+ export function HubProcessList({
52
+ theme,
53
+ layout,
54
+ cwd,
55
+ processes: provided,
56
+ }: Pick<PluginHostProps, "theme" | "layout"> & {
57
+ cwd: string;
58
+ processes?: readonly HubProcess[];
59
+ }) {
40
60
  const loadProcesses = useRpc(listHubProcesses);
41
61
  const loadLog = useRpc(tailHubLog);
42
62
  const [selectedName, setSelectedName] = useState<string | null>(null);
43
63
  const processes = useQuery({
44
64
  queryKey: ["paseo-omp", "processes", cwd],
45
65
  queryFn: () => loadProcesses({ cwd }),
46
- enabled: cwd.length > 0,
66
+ enabled: cwd.length > 0 && provided === undefined,
47
67
  refetchInterval: PROCESS_POLL_MS,
48
68
  });
49
69
  const log = useQuery({
@@ -80,9 +100,13 @@ export function HubPopover(props: PluginButtonContentProps) {
80
100
  [layout.compact, theme],
81
101
  );
82
102
 
83
- if (processes.isLoading) return <Text style={styles.muted}>Loading hub processes…</Text>;
84
- if (processes.error) return <Text style={styles.error}>Could not read omp hub state.</Text>;
85
- const items = processes.data?.processes ?? [];
103
+ if (!provided && processes.isLoading) {
104
+ return <Text style={styles.muted}>Loading hub processes…</Text>;
105
+ }
106
+ if (!provided && processes.error) {
107
+ return <Text style={styles.error}>Could not read omp hub state.</Text>;
108
+ }
109
+ const items = provided ?? processes.data?.processes ?? [];
86
110
  if (items.length === 0) return <Text style={styles.muted}>No hub-supervised processes.</Text>;
87
111
 
88
112
  return (
@@ -0,0 +1,135 @@
1
+ import type { PluginHostProps } from "@getpaseo/plugin/client";
2
+ // Namespace import: on 0.9/0.10 hosts this module lacks SidebarRow, which is only rendered from
3
+ // 0.11-only sidebar items. Typed locally so older SDK typechecks still compile.
4
+ import * as pluginUi from "@getpaseo/plugin/client/ui";
5
+ import { useQuery } from "@tanstack/react-query";
6
+ import type { ComponentType, ReactNode } from "react";
7
+ import { Text, View } from "react-native";
8
+ import { HubProcessList } from "./hub-popover";
9
+ import { CONFIG_SCREEN_ID, type HubSnapshot, hubTrailing } from "./sidebar-compat";
10
+
11
+ // The row is always mounted; a slower poll bounds per-client RPC fan-out across workspaces.
12
+ const HUB_POLL_MS = 15_000;
13
+ const HUB_SIDEBAR_QUERY_KEY = ["paseo-omp", "hub-sidebar"] as const;
14
+
15
+ interface OpenScreenInput {
16
+ screenId: string;
17
+ params?: Record<string, string>;
18
+ }
19
+ type HostProps = Pick<PluginHostProps, "theme" | "layout">;
20
+ interface PopoverProps extends HostProps {
21
+ close(): void;
22
+ openScreen(input: OpenScreenInput): void;
23
+ }
24
+ interface SidebarItemProps extends HostProps {
25
+ currentScreen: { screenId: string; params: Record<string, string> } | null;
26
+ openScreen(input: OpenScreenInput): void;
27
+ openPopover(Content: ComponentType<PopoverProps>): void;
28
+ }
29
+ type SidebarRowComponent = ComponentType<{
30
+ id?: string;
31
+ icon?: string | ComponentType<{ size: number; color: string }>;
32
+ label?: string;
33
+ onPress(): void;
34
+ active?: boolean;
35
+ trailing?: ReactNode;
36
+ }>;
37
+
38
+ const uiModule: object = pluginUi;
39
+ // Present on every 0.11 host, the only hosts that render these items. The guard and cast keep
40
+ // older-SDK typechecks (which lack SidebarRow) compiling; presence check only, because host
41
+ // components may be memo/forwardRef objects rather than plain functions.
42
+ const SidebarRow: SidebarRowComponent | undefined =
43
+ "SidebarRow" in uiModule && uiModule.SidebarRow
44
+ ? (uiModule.SidebarRow as SidebarRowComponent)
45
+ : undefined;
46
+
47
+ export function ConfigSidebarItem({ currentScreen, openScreen }: SidebarItemProps) {
48
+ if (!SidebarRow) return null;
49
+ return (
50
+ <SidebarRow
51
+ icon="Settings"
52
+ label="OMP"
53
+ active={currentScreen?.screenId === CONFIG_SCREEN_ID}
54
+ onPress={() => openScreen({ screenId: CONFIG_SCREEN_ID })}
55
+ />
56
+ );
57
+ }
58
+
59
+ export function createHubSidebar(loadSnapshot: () => Promise<HubSnapshot>) {
60
+ const useHubSnapshot = () =>
61
+ useQuery({
62
+ queryKey: HUB_SIDEBAR_QUERY_KEY,
63
+ queryFn: loadSnapshot,
64
+ refetchInterval: HUB_POLL_MS,
65
+ });
66
+
67
+ function HubSidebarPopover({ theme, layout }: PopoverProps) {
68
+ const snapshot = useHubSnapshot();
69
+ const muted = { color: theme.colors.foregroundMuted, fontSize: 13 };
70
+ if (snapshot.isLoading) return <Text style={muted}>Loading hub processes…</Text>;
71
+ if (!snapshot.data) {
72
+ return (
73
+ <Text style={{ color: theme.colors.statusDanger, fontSize: 13 }}>
74
+ Could not read omp hub state.
75
+ </Text>
76
+ );
77
+ }
78
+ const active = snapshot.data.workspaces.filter(({ processes }) => processes.length > 0);
79
+ const notes = [
80
+ snapshot.data.unreadable > 0
81
+ ? `${snapshot.data.unreadable} workspace(s) could not be read.`
82
+ : undefined,
83
+ snapshot.data.truncated ? "Showing the first 50 workspaces." : undefined,
84
+ ].filter(Boolean);
85
+ return (
86
+ <View style={{ gap: layout.compact ? 12 : 14 }}>
87
+ {notes.map((note) => (
88
+ <Text key={note} style={{ ...muted, fontSize: 12 }}>
89
+ {note}
90
+ </Text>
91
+ ))}
92
+ {active.length === 0 ? <Text style={muted}>No hub-supervised processes.</Text> : null}
93
+ {active.map(({ cwd, processes }) => (
94
+ <View key={cwd} style={{ gap: 6 }}>
95
+ <Text numberOfLines={1} style={{ ...muted, fontSize: 12 }}>
96
+ {cwd}
97
+ </Text>
98
+ <HubProcessList theme={theme} layout={layout} cwd={cwd} processes={processes} />
99
+ </View>
100
+ ))}
101
+ </View>
102
+ );
103
+ }
104
+
105
+ function HubSidebarItem({ theme, openPopover }: SidebarItemProps) {
106
+ const snapshot = useHubSnapshot();
107
+ if (!SidebarRow) return null;
108
+ const state = hubTrailing(snapshot.data, snapshot.error !== null);
109
+ const trailing = state ? (
110
+ <View
111
+ accessibilityLabel={state.accessibilityLabel}
112
+ style={{ flexDirection: "row", alignItems: "center", gap: 6 }}
113
+ >
114
+ {state.running ? (
115
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{state.running}</Text>
116
+ ) : null}
117
+ {state.failed || state.unreadable ? (
118
+ <Text style={{ color: theme.colors.statusDanger, fontSize: 12, fontWeight: "600" }}>
119
+ {[state.failed, state.unreadable ? "!" : undefined].filter(Boolean).join(" ")}
120
+ </Text>
121
+ ) : null}
122
+ </View>
123
+ ) : undefined;
124
+ return (
125
+ <SidebarRow
126
+ icon="Activity"
127
+ label="OMP Hub"
128
+ trailing={trailing}
129
+ onPress={() => openPopover(HubSidebarPopover)}
130
+ />
131
+ );
132
+ }
133
+
134
+ return { HubSidebarItem, HubSidebarPopover };
135
+ }
@@ -23,7 +23,7 @@ export function OmpMemoryPanel({ theme, layout, workspaceId }: PluginWorkspacePa
23
23
  const styles = useMemo(
24
24
  () => ({
25
25
  root: {
26
- flex: 1,
26
+ flexGrow: 1,
27
27
  gap: layout.compact ? 10 : 14,
28
28
  padding: layout.compact ? 16 : 24,
29
29
  backgroundColor: theme.colors.surface0,
@@ -70,7 +70,7 @@ export function useConfigStyles(
70
70
  return useMemo(
71
71
  () => ({
72
72
  root: {
73
- flex: 1,
73
+ flexGrow: 1,
74
74
  gap: compact ? 10 : 14,
75
75
  padding: compact ? 16 : 24,
76
76
  backgroundColor: theme.colors.surface0,
@@ -914,11 +914,14 @@ function OmpConfigContent({
914
914
  cwd,
915
915
  store,
916
916
  onStoreChange,
917
+ storeInHistory = false,
917
918
  onComposerPillSettingsChange,
918
919
  }: PluginSurfaceProps & {
919
920
  cwd?: string;
920
921
  store?: OmpStore;
921
922
  onStoreChange(store: OmpStore | undefined): void;
923
+ /** True when each store selection is a separate history entry (0.11 screen params). */
924
+ storeInHistory?: boolean;
922
925
  onComposerPillSettingsChange?: (settings: ComposerPillSettings) => void;
923
926
  }) {
924
927
  const loadConfig = useRpc(listOmpConfig);
@@ -1074,7 +1077,9 @@ function OmpConfigContent({
1074
1077
  disabled={pendingMutations > 0}
1075
1078
  />
1076
1079
  <Text style={styles.muted}>
1077
- Switching stores clears unapplied edits and pending confirmations.
1080
+ {storeInHistory
1081
+ ? "Each store opens as its own page; unapplied edits stay with the previous store, and Back returns to them."
1082
+ : "Switching stores clears unapplied edits and pending confirmations."}
1078
1083
  </Text>
1079
1084
  </>
1080
1085
  ) : null}
@@ -1339,9 +1344,14 @@ function OmpStoreContent(
1339
1344
  props: PluginSurfaceProps & {
1340
1345
  cwd?: string;
1341
1346
  onComposerPillSettingsChange?: (settings: ComposerPillSettings) => void;
1347
+ /** Controlled selection (0.11 screen params); omitted, the surface keeps local state. */
1348
+ store?: OmpStore;
1349
+ onStoreChange?: (store: OmpStore | undefined) => void;
1342
1350
  },
1343
1351
  ) {
1344
- const [store, setStore] = useState<OmpStore>();
1352
+ const [localStore, setLocalStore] = useState<OmpStore>();
1353
+ const controlled = props.onStoreChange !== undefined;
1354
+ const store = controlled ? props.store : localStore;
1345
1355
  // Remount every editor when its target changes: drafts, confirmations, and mutation notices
1346
1356
  // belong to one store/workspace and must never be applied to the next selection.
1347
1357
  return (
@@ -1349,7 +1359,8 @@ function OmpStoreContent(
1349
1359
  key={`${ompStoreKey(store)}:${props.cwd ?? "global"}`}
1350
1360
  {...props}
1351
1361
  store={store}
1352
- onStoreChange={setStore}
1362
+ onStoreChange={props.onStoreChange ?? setLocalStore}
1363
+ storeInHistory={controlled}
1353
1364
  />
1354
1365
  );
1355
1366
  }
@@ -1357,6 +1368,8 @@ function OmpStoreContent(
1357
1368
  export function OmpConfigSurface(
1358
1369
  props: PluginSurfaceProps & {
1359
1370
  onComposerPillSettingsChange: (settings: ComposerPillSettings) => void;
1371
+ store?: OmpStore;
1372
+ onStoreChange?: (store: OmpStore | undefined) => void;
1360
1373
  },
1361
1374
  ) {
1362
1375
  return <OmpStoreContent {...props} />;
@@ -0,0 +1,160 @@
1
+ import type { HubProcess } from "../shared/hub";
2
+ import { isOmpProfileName, type OmpStore } from "../shared/omp-store";
3
+ import { hubProcessTone } from "./hub-status";
4
+
5
+ /** The sidebar id the 0.10 `addSidebarItem` used; the 0.11 screen keeps it so saved links work. */
6
+ export const CONFIG_SCREEN_ID = "config";
7
+ export const HUB_SIDEBAR_ITEM_ID = "hub";
8
+
9
+ type ScreenParams = Readonly<Record<string, string>>;
10
+
11
+ /**
12
+ * The 0.11 screen/sidebar-item API, typed structurally so 0.9/0.10 SDK typechecks still compile.
13
+ * On 0.10 hosts these members are absent at runtime.
14
+ */
15
+ export interface ScreenCapableClient {
16
+ addScreen(contribution: {
17
+ id: string;
18
+ title: string | ((params: ScreenParams) => string);
19
+ Component: (props: never) => unknown;
20
+ }): () => void;
21
+ addSidebarHeaderItem(contribution: {
22
+ id: string;
23
+ title: string;
24
+ Component: (props: never) => unknown;
25
+ }): () => void;
26
+ openScreen(input: { screenId: string; params?: Record<string, string> }): void;
27
+ }
28
+
29
+ /** True only on Paseo 0.11+ hosts, which provide screens, header items, and `openScreen`. */
30
+ export function supportsScreens<Client extends object>(
31
+ client: Client,
32
+ ): client is Client & ScreenCapableClient {
33
+ const candidate = client as Partial<Record<keyof ScreenCapableClient, unknown>>;
34
+ return (
35
+ typeof candidate.addScreen === "function" &&
36
+ typeof candidate.addSidebarHeaderItem === "function" &&
37
+ typeof candidate.openScreen === "function"
38
+ );
39
+ }
40
+
41
+ /** A `profile` screen param selects that store; anything invalid falls back to the default. */
42
+ export function configStoreFromParams(params: ScreenParams | undefined): OmpStore | undefined {
43
+ const profile = params?.profile?.trim();
44
+ return profile && isOmpProfileName(profile) ? { profile } : undefined;
45
+ }
46
+
47
+ export function configParamsFromStore(store: OmpStore | undefined): Record<string, string> {
48
+ return store?.profile ? { profile: store.profile } : {};
49
+ }
50
+
51
+ export function configScreenTitle(params: ScreenParams): string {
52
+ const store = configStoreFromParams(params);
53
+ return store?.profile ? `OMP · ${store.profile}` : "OMP";
54
+ }
55
+
56
+ export interface HubWorkspaceProcesses {
57
+ cwd: string;
58
+ processes: readonly HubProcess[];
59
+ }
60
+
61
+ export interface HubSidebarSummary {
62
+ running: number;
63
+ failed: number;
64
+ total: number;
65
+ }
66
+
67
+ export function summarizeHubWorkspaces(
68
+ workspaces: readonly HubWorkspaceProcesses[],
69
+ ): HubSidebarSummary {
70
+ const summary: HubSidebarSummary = { running: 0, failed: 0, total: 0 };
71
+ for (const { processes } of workspaces) {
72
+ for (const process of processes) {
73
+ summary.total += 1;
74
+ const tone = hubProcessTone(process);
75
+ if (tone === "success") summary.running += 1;
76
+ else if (tone === "danger") summary.failed += 1;
77
+ }
78
+ }
79
+ return summary;
80
+ }
81
+
82
+ /** Distinct, non-empty workspace directories in first-seen order, bounded for polling cost. */
83
+ export function workspaceDirectories(
84
+ workspaces: ReadonlyArray<{ workspaceDirectory?: string; projectRootPath: string }>,
85
+ limit = 50,
86
+ ): { directories: string[]; truncated: boolean } {
87
+ const directories = new Set<string>();
88
+ for (const workspace of workspaces) {
89
+ const directory = workspace.workspaceDirectory || workspace.projectRootPath;
90
+ if (!directory || directories.has(directory)) continue;
91
+ if (directories.size >= limit) return { directories: [...directories], truncated: true };
92
+ directories.add(directory);
93
+ }
94
+ return { directories: [...directories], truncated: false };
95
+ }
96
+
97
+ export interface HubSnapshot {
98
+ workspaces: HubWorkspaceProcesses[];
99
+ /** More workspaces exist than were polled; counts are a lower bound. */
100
+ truncated: boolean;
101
+ /** Workspaces whose hub state could not be read this poll. */
102
+ unreadable: number;
103
+ }
104
+
105
+ /**
106
+ * One poll of hub state. A failing workspace never hides the others; the poll fails only when
107
+ * the workspace list itself fails or every workspace is unreadable.
108
+ */
109
+ export async function loadHubSnapshot(
110
+ listWorkspaces: () => Promise<{
111
+ entries: ReadonlyArray<{ workspaceDirectory?: string; projectRootPath: string }>;
112
+ pageInfo?: { hasMore?: boolean };
113
+ }>,
114
+ listProcesses: (cwd: string) => Promise<readonly HubProcess[]>,
115
+ ): Promise<HubSnapshot> {
116
+ const page = await listWorkspaces();
117
+ const { directories, truncated } = workspaceDirectories(page.entries);
118
+ const results = await Promise.allSettled(
119
+ directories.map(async (cwd) => ({ cwd, processes: await listProcesses(cwd) })),
120
+ );
121
+ const workspaces: HubWorkspaceProcesses[] = [];
122
+ for (const result of results) if (result.status === "fulfilled") workspaces.push(result.value);
123
+ if (directories.length > 0 && workspaces.length === 0) {
124
+ throw new Error("OMP hub state is unreadable");
125
+ }
126
+ return {
127
+ workspaces,
128
+ truncated: truncated || page.pageInfo?.hasMore === true,
129
+ unreadable: directories.length - workspaces.length,
130
+ };
131
+ }
132
+
133
+ export interface HubTrailing {
134
+ running?: string;
135
+ failed?: string;
136
+ /** Some or all hub state could not be read. */
137
+ unreadable: boolean;
138
+ accessibilityLabel: string;
139
+ }
140
+
141
+ /** What the Hub row's trailing slot shows; null when there is nothing to report. */
142
+ export function hubTrailing(
143
+ snapshot: HubSnapshot | undefined,
144
+ loadFailed: boolean,
145
+ ): HubTrailing | null {
146
+ const summary = summarizeHubWorkspaces(snapshot?.workspaces ?? []);
147
+ const atLeast = snapshot?.truncated ? "+" : "";
148
+ const unreadable = loadFailed || (snapshot?.unreadable ?? 0) > 0;
149
+ if (summary.total === 0 && !unreadable) return null;
150
+ const running = summary.total > 0 ? `${summary.running}${atLeast} running` : undefined;
151
+ const failed = summary.failed > 0 ? `${summary.failed}${atLeast} failed` : undefined;
152
+ return {
153
+ ...(running ? { running } : {}),
154
+ ...(failed ? { failed } : {}),
155
+ unreadable,
156
+ accessibilityLabel: [running, failed, unreadable ? "some hub state unreadable" : undefined]
157
+ .filter(Boolean)
158
+ .join(", "),
159
+ };
160
+ }
package/index.client.tsx CHANGED
@@ -4,8 +4,10 @@ import type {
4
4
  PluginClientContext,
5
5
  PluginSurfaceProps,
6
6
  } from "@getpaseo/plugin/client";
7
+ import type { ComponentType } from "react";
7
8
  import { OmpIcon } from "./client/hub-icon";
8
9
  import { HubPopover } from "./client/hub-popover";
10
+ import { ConfigSidebarItem, createHubSidebar } from "./client/hub-sidebar";
9
11
  import { summarizeHubProcesses } from "./client/hub-status";
10
12
  import { OmpMcpAuthorizationCard } from "./client/mcp-authorization";
11
13
  import { McpPopover } from "./client/mcp-popover";
@@ -29,6 +31,15 @@ import {
29
31
  quotaSummaryForProvider,
30
32
  } from "./client/quota-state";
31
33
  import { SessionsPopover } from "./client/sessions-popover";
34
+ import {
35
+ CONFIG_SCREEN_ID,
36
+ configParamsFromStore,
37
+ configScreenTitle,
38
+ configStoreFromParams,
39
+ HUB_SIDEBAR_ITEM_ID,
40
+ loadHubSnapshot,
41
+ supportsScreens,
42
+ } from "./client/sidebar-compat";
32
43
  import {
33
44
  type ComposerPillSettings,
34
45
  composerPillSettings,
@@ -100,6 +111,69 @@ async function loadAgents(paseo: PaseoApi): Promise<AgentEntry[]> {
100
111
  return entries;
101
112
  }
102
113
 
114
+ /**
115
+ * Paseo 0.11+: a param-addressable `config` screen (same id as the old sidebar item, so saved
116
+ * links resolve to it), a header row that opens it, and a Hub status row. 0.9/0.10: the original
117
+ * surface and static sidebar row, unchanged.
118
+ */
119
+ export function registerConfigAndHub(
120
+ client: Pick<PluginClientContext, "addSurface" | "addSidebarItem" | "rpc" | "paseo">,
121
+ ConfigSurface: ComponentType<PluginSurfaceProps>,
122
+ onComposerPillSettingsChange: (settings: ComposerPillSettings) => void,
123
+ ): () => void {
124
+ if (!supportsScreens(client)) {
125
+ const removeSurface = client.addSurface(CONFIG_SCREEN_ID, ConfigSurface);
126
+ const removeItem = client.addSidebarItem({
127
+ id: CONFIG_SCREEN_ID,
128
+ title: "OMP",
129
+ icon: "Settings",
130
+ surface: CONFIG_SCREEN_ID,
131
+ });
132
+ return () => {
133
+ removeItem();
134
+ removeSurface();
135
+ };
136
+ }
137
+ const screens = client;
138
+ function ConfigScreen(props: PluginSurfaceProps & { params: Record<string, string> }) {
139
+ const current = configStoreFromParams(props.params);
140
+ return (
141
+ <OmpConfigSurface
142
+ {...props}
143
+ onComposerPillSettingsChange={onComposerPillSettingsChange}
144
+ store={current}
145
+ onStoreChange={(store) => {
146
+ // Re-selecting the current store must not push a duplicate screen onto history.
147
+ if (ompStoreKey(store) === ompStoreKey(current)) return;
148
+ screens.openScreen({ screenId: CONFIG_SCREEN_ID, params: configParamsFromStore(store) });
149
+ }}
150
+ />
151
+ );
152
+ }
153
+ const { HubSidebarItem } = createHubSidebar(() =>
154
+ loadHubSnapshot(
155
+ () => client.paseo.workspaces.list({ page: { limit: 200 } }),
156
+ async (cwd) => (await client.rpc(listHubProcesses, { cwd })).processes,
157
+ ),
158
+ );
159
+ const removers = [
160
+ screens.addScreen({ id: CONFIG_SCREEN_ID, title: configScreenTitle, Component: ConfigScreen }),
161
+ screens.addSidebarHeaderItem({
162
+ id: CONFIG_SCREEN_ID,
163
+ title: "OMP",
164
+ Component: ConfigSidebarItem,
165
+ }),
166
+ screens.addSidebarHeaderItem({
167
+ id: HUB_SIDEBAR_ITEM_ID,
168
+ title: "OMP Hub",
169
+ Component: HubSidebarItem,
170
+ }),
171
+ ];
172
+ return () => {
173
+ for (const remove of removers.reverse()) remove();
174
+ };
175
+ }
176
+
103
177
  export default function contribute(client: PluginClientContext) {
104
178
  const pills = new Map<string, PillEntry>();
105
179
  let preferences: ComposerPillSettings | undefined;
@@ -159,21 +233,19 @@ export default function contribute(client: PluginClientContext) {
159
233
  openPanel("workspace", { location: "workspace" });
160
234
  },
161
235
  });
162
- const removeConfigSurface = client.addSurface("config", ConfigSurface);
163
- const removeConfigSidebarItem = client.addSidebarItem({
164
- id: "config",
165
- title: "OMP",
166
- icon: "Settings",
167
- surface: "config",
168
- });
236
+ const usesScreens = supportsScreens(client);
237
+ const removeConfigEntry = registerConfigAndHub(client, ConfigSurface, applyComposerPillSettings);
169
238
  const removeOpenConfig = client.addCommandCenterItem({
170
239
  id: "open-config",
171
240
  title: "Open OMP",
172
241
  icon: "Settings",
173
242
  keywords: ["omp", "config", "settings", "models", "providers", "composer", "pills"],
174
243
  context: "global",
175
- onSelect({ openSurface }) {
176
- openSurface("config");
244
+ onSelect(capabilities) {
245
+ // Branch on the same gate as registration: the screen exists only on the 0.11 path.
246
+ if (usesScreens && "openScreen" in capabilities) {
247
+ capabilities.openScreen({ screenId: CONFIG_SCREEN_ID });
248
+ } else capabilities.openSurface(CONFIG_SCREEN_ID);
177
249
  },
178
250
  });
179
251
  const removeImageRenderer = client.addTimelineRenderer({
@@ -497,8 +569,7 @@ export default function contribute(client: PluginClientContext) {
497
569
  removeMcpAuthorizationRenderer();
498
570
  removeImageTransformer();
499
571
  removeOpenConfig();
500
- removeConfigSidebarItem();
501
- removeConfigSurface();
572
+ removeConfigEntry();
502
573
  removeOpenWorkspace();
503
574
  removeWorkspacePanel();
504
575
  removeOpenMemory();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -51,16 +51,16 @@
51
51
  "typecheck": "tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.tests.json"
52
52
  },
53
53
  "dependencies": {
54
- "@getpaseo/protocol": "0.10.1",
54
+ "@getpaseo/protocol": "0.11.0-beta.3",
55
55
  "@modelcontextprotocol/sdk": "1.30.0",
56
56
  "yaml": "^2.9.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@biomejs/biome": "^2.5.10",
60
- "@getpaseo/cli": "0.10.1",
61
- "@getpaseo/client": "0.10.1",
62
- "@getpaseo/plugin": "0.10.1",
63
- "@getpaseo/server": "0.10.1",
60
+ "@getpaseo/cli": "0.11.0-beta.3",
61
+ "@getpaseo/client": "0.11.0-beta.3",
62
+ "@getpaseo/plugin": "0.11.0-beta.3",
63
+ "@getpaseo/server": "0.11.0-beta.3",
64
64
  "@oh-my-pi/pi-agent-core": "18.2.0",
65
65
  "@oh-my-pi/pi-coding-agent": "18.2.0",
66
66
  "@tanstack/react-query": "^5.102.3",
package/paseo-plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "paseo-omp",
3
3
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
4
- "requirements": { "paseo": "^0.9.2 || ^0.10.0" },
4
+ "requirements": { "paseo": "^0.9.2 || ^0.10.0 || ^0.11.0" },
5
5
  "build": [["node", "scripts/prepare-dependencies.mjs"]]
6
6
  }
@@ -1,2 +1,2 @@
1
1
  /** Release Please keeps this bundle-safe constant aligned with package.json. */
2
- export const PASEO_OMP_PACKAGE_VERSION = "1.1.0"; // x-release-please-version
2
+ export const PASEO_OMP_PACKAGE_VERSION = "1.2.0"; // x-release-please-version