Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions paseo-omp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Tracking rules:
2. Raise a row only when the provider advertises the capability and an observable contract test covers its success and failure boundaries.
3. Keep native-agent limitations at less than 100% even when the adapter itself is complete; do not count undocumented fallbacks as support.
4. Keep the detailed evidence and regression locations in [TESTING.md](TESTING.md); this README is the public progress ledger.
5. Record one wire-diff outcome per OMP release. `node scripts/omp-wire-diff.mjs --omp <rpc-wire.schema.json> --protocol server/provider/omp-rpc-protocol.ts` prints a `clean` / `additive-optional` / `additive-required` / `removed-or-renamed` / `type-change` table and exits 2 on release-blocking drift; attach that table to the scoring change so the release stays comparable.

## Compatibility and coexistence

Expand Down
2 changes: 1 addition & 1 deletion paseo-omp/SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ After the failure is isolated:
- report a Paseo plugin SDK, loader, or provider-protocol defect to [Paseo](https://github.com/getpaseo/paseo/issues);
- keep adaptation, packaging, and cross-project compatibility work in this repository.

Do not put credentials, private repository paths, session transcripts, or unredacted RPC payloads in an issue. Report vulnerabilities through the [private GitHub Security Advisory form](https://github.com/omercnet/paseo-plugins/security/advisories/new), not a public issue.
Do not put credentials, private repository paths, session transcripts, or unredacted RPC payloads in an issue. If a maintainer asks for a transcript excerpt, use **Copy bundle** in **OMP → Help**. It appends an opt-in 32 KiB journal excerpt to the same report, and you must review and redact that excerpt before pasting it. Report vulnerabilities through the [private GitHub Security Advisory form](https://github.com/omercnet/paseo-plugins/security/advisories/new), not a public issue.

## Supported versions

Expand Down
2 changes: 1 addition & 1 deletion paseo-omp/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ The controlled canary passed end to end with OMP 18.1.15 and 18.2.0 on 2026-09-1
Treat every upstream `rpc-ui` change as explicit compatibility work. Do not widen a Zod schema with `passthrough`, `unknown`, or an optional field merely to accept a new frame.

1. Open an issue with the [OMP RPC compatibility template](https://github.com/omercnet/paseo-plugins/issues/new?template=omp-rpc-compatibility.yml). Record exact OMP, plugin, Paseo daemon, and Paseo app versions; the negotiated protocol and capabilities; the smallest reproduction; and sanitized frame shapes. Never attach credentials, private paths, prompts, or transcripts.
2. Reproduce against both the reported OMP revision and the pinned minimum-tested `omp/18.1.15` binary. Classify the change as additive optional, additive required, removed or renamed, type or semantic change, or negotiation change.
2. Reproduce against both the reported OMP revision and the pinned minimum-tested `omp/18.1.15` binary. Classify the change as additive optional, additive required, removed or renamed, type or semantic change, or negotiation change. Before claiming support, run `node scripts/omp-wire-diff.mjs --omp <rpc-wire.schema.json> --protocol server/provider/omp-rpc-protocol.ts` and paste its classification table into the issue. The lever is offline, exits 2 on additive-required, removed-or-renamed, or type-change drift, and never edits the strict parser.
3. Compare the affected ready, request, response, or event shape with the strict schemas in `server/provider/omp-rpc-protocol.ts`. Decide whether the plugin can support both contracts without ambiguity. A breaking contract requires an explicit compatibility decision and changelog entry, not silent coercion.

Required drift is release-blocking. If OMP adds a mandatory frame, removes or renames a required method or field, or changes an existing field's meaning, keep the strict parser and make session startup or the active request fail visibly. Do not silently discard the frame, make the requirement optional, or route around negotiation. Resume release work only after both sides have an explicit compatible contract, fixtures, focused regressions, and a real-binary result.
Expand Down
177 changes: 157 additions & 20 deletions paseo-omp/client/hub-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import type { PluginHostProps } from "@getpaseo/plugin/client";
import { useRpc } from "@getpaseo/plugin/client";
// Namespace import: on 0.9/0.10 hosts this module lacks SidebarRow, which is only rendered from
// 0.11-only sidebar items. Typed locally so older SDK typechecks still compile.
import * as pluginUi from "@getpaseo/plugin/client/ui";
import { useQuery } from "@tanstack/react-query";
import type { ComponentType, ReactNode } from "react";
import { Text, View } from "react-native";
import { Pressable, Text, View } from "react-native";
import type { AvailabilityDisplay } from "../shared/availability-display";
import { toAvailabilityDisplay } from "../shared/availability-display";
import { getOmpProviderHealth } from "../shared/provider-diagnostics";
import { HubProcessList } from "./hub-popover";
import { availabilityTrailingSuffix } from "./hub-status";
import { freshnessNotice, type PillFreshness, type PillKind } from "./pill-freshness";
import { HUB_AVAILABILITY_QUERY_KEY } from "./provider-diagnostics-state";
import { CONFIG_SCREEN_ID, type HubSnapshot, hubTrailing } from "./sidebar-compat";

// The row is always mounted; a slower poll bounds per-client RPC fan-out across workspaces.
const HUB_POLL_MS = 15_000;
// Shared with the config surface health read; staleTime mirrors the server health cache TTL.
const HUB_AVAILABILITY_STALE_MS = 30_000;
const HUB_SIDEBAR_QUERY_KEY = ["paseo-omp", "hub-sidebar"] as const;

interface OpenScreenInput {
Expand Down Expand Up @@ -56,6 +65,39 @@ export function ConfigSidebarItem({ currentScreen, openScreen }: SidebarItemProp
);
}

/**
* Stale/error banner for a composer pill's popover (QW3). Pure presentation: the state machine
* lives in `pill-freshness.ts` and Retry reuses the owner's existing refresh function, so this
* adds no timer and no RPC. Renders nothing while fresh.
*/
export function PillFreshnessBanner({
kind,
state,
theme,
onRetry,
}: {
kind: PillKind;
state: PillFreshness;
theme: PluginHostProps["theme"];
onRetry(): void;
}) {
const notice = freshnessNotice(state, kind);
if (!notice) return null;
const color = notice.tone === "danger" ? theme.colors.statusDanger : theme.colors.statusWarning;
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Text style={{ color, fontSize: 12, flexShrink: 1 }}>{notice.text}</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={`Retry ${kind} refresh`}
onPress={onRetry}
>
<Text style={{ color: theme.colors.accent, fontSize: 12, fontWeight: "600" }}>Retry</Text>
</Pressable>
</View>
);
}

export function createHubSidebar(loadSnapshot: () => Promise<HubSnapshot>) {
const useHubSnapshot = () =>
useQuery({
Expand All @@ -64,15 +106,83 @@ export function createHubSidebar(loadSnapshot: () => Promise<HubSnapshot>) {
refetchInterval: HUB_POLL_MS,
});

function HubSidebarPopover({ theme, layout }: PopoverProps) {
function AvailabilityBanner({
theme,
display,
onRetry,
openScreen,
}: {
theme: PluginHostProps["theme"];
display: AvailabilityDisplay;
onRetry(): void;
openScreen(input: OpenScreenInput): void;
}) {
if (!display.showBadge) return null;
const color =
display.tone === "danger" ? theme.colors.statusDanger : theme.colors.statusWarning;
return (
<View style={{ gap: 4 }}>
<Text style={{ color, fontSize: 13, fontWeight: "600" }}>{display.title}</Text>
{display.detail ? (
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
{display.detail}
</Text>
) : null}
<View style={{ flexDirection: "row", gap: 12 }}>
{display.action === "retry" ? (
<Pressable
accessibilityRole="button"
accessibilityLabel="Retry OMP probe"
onPress={onRetry}
>
<Text style={{ color: theme.colors.accent, fontSize: 12, fontWeight: "600" }}>
Retry
</Text>
</Pressable>
) : null}
{display.action === "open-diagnostics" ? (
<Pressable
accessibilityRole="button"
accessibilityLabel="Open OMP diagnostics"
onPress={() => openScreen({ screenId: CONFIG_SCREEN_ID })}
>
<Text style={{ color: theme.colors.accent, fontSize: 12, fontWeight: "600" }}>
Open diagnostics
</Text>
</Pressable>
) : null}
</View>
</View>
);
}

function HubSidebarPopover({ theme, layout, openScreen }: PopoverProps) {
const snapshot = useHubSnapshot();
const loadHealth = useRpc(getOmpProviderHealth);
const health = useQuery({
queryKey: HUB_AVAILABILITY_QUERY_KEY,
queryFn: () => loadHealth({}),
staleTime: HUB_AVAILABILITY_STALE_MS,
});
const muted = { color: theme.colors.foregroundMuted, fontSize: 13 };
const availability = health.data ? toAvailabilityDisplay(health.data) : undefined;
const banner = availability ? (
<AvailabilityBanner
theme={theme}
display={availability}
onRetry={() => void health.refetch()}
openScreen={openScreen}
/>
) : null;
if (snapshot.isLoading) return <Text style={muted}>Loading hub processes…</Text>;
if (!snapshot.data) {
return (
<Text style={{ color: theme.colors.statusDanger, fontSize: 13 }}>
Could not read omp hub state.
</Text>
<View style={{ gap: layout.compact ? 12 : 14 }}>
{banner}
<Text style={{ color: theme.colors.statusDanger, fontSize: 13 }}>
Could not read omp hub state.
</Text>
</View>
);
}
const active = snapshot.data.workspaces.filter(({ processes }) => processes.length > 0);
Expand All @@ -84,6 +194,7 @@ export function createHubSidebar(loadSnapshot: () => Promise<HubSnapshot>) {
].filter(Boolean);
return (
<View style={{ gap: layout.compact ? 12 : 14 }}>
{banner}
{notes.map((note) => (
<Text key={note} style={{ ...muted, fontSize: 12 }}>
{note}
Expand All @@ -104,23 +215,49 @@ export function createHubSidebar(loadSnapshot: () => Promise<HubSnapshot>) {

function HubSidebarItem({ theme, openPopover }: SidebarItemProps) {
const snapshot = useHubSnapshot();
const loadHealth = useRpc(getOmpProviderHealth);
const health = useQuery({
queryKey: HUB_AVAILABILITY_QUERY_KEY,
queryFn: () => loadHealth({}),
staleTime: HUB_AVAILABILITY_STALE_MS,
});
if (!SidebarRow) return null;
const display = health.data ? toAvailabilityDisplay(health.data) : undefined;
const availability = availabilityTrailingSuffix(display);
const state = hubTrailing(snapshot.data, snapshot.error !== null);
const trailing = state ? (
<View
accessibilityLabel={state.accessibilityLabel}
style={{ flexDirection: "row", alignItems: "center", gap: 6 }}
>
{state.running ? (
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{state.running}</Text>
) : null}
{state.failed || state.unreadable ? (
<Text style={{ color: theme.colors.statusDanger, fontSize: 12, fontWeight: "600" }}>
{[state.failed, state.unreadable ? "!" : undefined].filter(Boolean).join(" ")}
</Text>
) : null}
</View>
) : undefined;
const accessibilityLabel = [availability, state?.accessibilityLabel].filter(Boolean).join(", ");
const trailing =
availability || state ? (
<View
accessibilityLabel={accessibilityLabel || undefined}
style={{ flexDirection: "row", alignItems: "center", gap: 6 }}
>
{availability ? (
<Text
style={{
color:
display?.tone === "danger"
? theme.colors.statusDanger
: theme.colors.statusWarning,
fontSize: 12,
fontWeight: "600",
}}
>
{availability}
</Text>
) : null}
{state?.running ? (
<Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
{state.running}
</Text>
) : null}
{state?.failed || state?.unreadable ? (
<Text style={{ color: theme.colors.statusDanger, fontSize: 12, fontWeight: "600" }}>
{[state?.failed, state?.unreadable ? "!" : undefined].filter(Boolean).join(" ")}
</Text>
) : null}
</View>
) : undefined;
return (
<SidebarRow
icon="Activity"
Expand Down
11 changes: 11 additions & 0 deletions paseo-omp/client/hub-status.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AvailabilityDisplay } from "../shared/availability-display";
import type { HubProcess } from "../shared/hub";

export type HubProcessTone = "success" | "warning" | "danger" | "muted";
Expand Down Expand Up @@ -27,3 +28,13 @@ export function summarizeHubProcesses(processes: readonly HubProcess[]): {
label: `Hub · ${processes.length}${hasFailure ? " !" : ""}`,
};
}

/**
* Trailing suffix for a pre-launch availability problem; null when available (or not yet
* observed), so the Hub row shows no badge and its accessibility label stays unchanged.
*/
export function availabilityTrailingSuffix(
display: AvailabilityDisplay | undefined,
): string | null {
return display?.showBadge ? display.label : null;
}
34 changes: 34 additions & 0 deletions paseo-omp/client/omp-config-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "../shared/omp-settings";
import { type OmpStore, storeLabel } from "../shared/omp-store";
import { getOmpProviderHealth, type OmpProviderHealth } from "../shared/provider-diagnostics";
import { classifySettingImpact, deriveRestartRequired } from "../shared/restart-required";
import { getOmpSupportReport, OMP_SUPPORT_ISSUE_URL } from "../shared/support-diagnostics";
import { ComposerPillSettingsSection } from "./composer-pill-settings";
import { openOmpExternalUrl } from "./external-url";
Expand All @@ -44,6 +45,7 @@ import { OmpPluginManagerSection } from "./omp-plugin-manager";
import { type OmpModelCatalogState, StructuredRoutingEditor } from "./omp-routing-editor";
import { OmpStorePicker } from "./omp-store-picker";
import { ompStoreKey } from "./omp-store-state";
import { POLICY_BOUNDARY_COPY } from "./policy-explainer";
import {
type BinaryHealthSummary,
loadReadyProviderSnapshot,
Expand Down Expand Up @@ -195,6 +197,17 @@ function PluginConfigurationSection({ styles }: { styles: OmpConfigStyles }) {
</>
);
}
function PolicyExplainerCard({ styles }: { styles: OmpConfigStyles }) {
return (
<SectionCard styles={styles} title={POLICY_BOUNDARY_COPY.title}>
<Text style={styles.muted}>{POLICY_BOUNDARY_COPY.body}</Text>
<Text selectable style={styles.settingPath}>
Canonical copy: {POLICY_BOUNDARY_COPY.docAnchor}
</Text>
</SectionCard>
);
}

function toneColor(theme: PluginSurfaceProps["theme"], tone: BinaryHealthSummary["tone"]): string {
if (tone === "ok") return theme.colors.statusSuccess;
if (tone === "warning") return theme.colors.statusWarning;
Expand Down Expand Up @@ -814,13 +827,19 @@ function ConfigurationCategory({
setting.type === (setting.path === "cycleOrder" ? "array" : "record");
const settingDocumentation = documentationForSettingPath(setting.path);
const draft = drafts[setting.path];
const impact = classifySettingImpact(setting.path);
return (
<View key={setting.path} style={styles.setting}>
<View style={styles.settingHeader}>
<Text style={styles.cardTitle}>{formatOmpSettingLabel(setting.path)}</Text>
{setting.workspaceOverride ? (
<Text style={styles.source}>Workspace override</Text>
) : null}
{impact === "live" ? null : (
<Text style={[styles.source, { flexShrink: 1 }]}>
{impact === "new-sessions" ? "New sessions" : "Restart required"}
</Text>
)}
{!complex && !editable && !structuredEditable ? (
<StructuredSettingValue setting={setting} styles={styles} />
) : null}
Expand Down Expand Up @@ -1053,6 +1072,7 @@ function OmpConfigContent({
},
});
const draftCount = Object.keys(drafts).length;
const restartRequired = deriveRestartRequired(Object.keys(drafts));
const workspaceOverrideCount = catalog.sourceSettings.filter(
(setting) => setting.workspaceOverride,
).length;
Expand Down Expand Up @@ -1136,6 +1156,7 @@ function OmpConfigContent({
{view === "plugin" ? (
<>
<PluginConfigurationSection styles={styles} />
<PolicyExplainerCard styles={styles} />
{!cwd ? <ProviderLaunchSettingsSection theme={theme} /> : null}
</>
) : null}
Expand Down Expand Up @@ -1217,6 +1238,19 @@ function OmpConfigContent({
</Text>
) : null}

{draftCount > 0 && restartRequired.requiresRestart ? (
<Text style={styles.muted}>
{`Restart required: ${restartRequired.affectedPaths.length} change${
restartRequired.affectedPaths.length === 1 ? "" : "s"
} apply to new sessions only (${
restartRequired.reason === "approval-mode"
? "OMP fixes approval mode at launch"
: restartRequired.reason === "settings-live-reject"
? "OMP rejects live settings"
: "OMP fixes approval mode at launch and rejects live settings"
}).`}
</Text>
) : null}
{draftCount > 0 ? (
<View style={styles.editorActions}>
<Text style={styles.muted}>{draftCount} unsaved changes</Text>
Expand Down
Loading