@omercnet/paseo-omp 3.0.0 → 4.0.0-next.312.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/OVERVIEW.md CHANGED
@@ -2,7 +2,7 @@ OMP adds an agent provider for the OMP coding agent, plus an OMP sidebar and wor
2
2
 
3
3
  ## Setup
4
4
 
5
- - Paseo ^0.12.0.
5
+ - Paseo ^0.13.0.
6
6
  - OMP 18.1.15 or newer on the daemon host, speaking RPC protocol v2. Older runtimes are rejected.
7
7
  - Provider options let you change the OMP command, set environment values, choose an output redaction mode and set a session directory and timeout. Named OMP profiles appear as separate `OMP · <profile>` providers.
8
8
 
@@ -11,6 +11,7 @@ OMP adds an agent provider for the OMP coding agent, plus an OMP sidebar and wor
11
11
  - Edit OMP settings and project configuration for the default store or a named profile.
12
12
  - Manage OMP's own plugins, and view quota history, memory, hub processes, session history and a support report.
13
13
  - An **MCP** composer control runs OMP's MCP management in the current session, with authorization prompts shown in the chat.
14
+ - OMP agents can call Paseo's orchestration tools with the optional `serverId` argument and `list_hosts` to delegate work to other connected Paseo hosts.
14
15
 
15
16
  ## Reads and sends
16
17
 
package/README.md CHANGED
@@ -24,7 +24,7 @@ before capture.
24
24
 
25
25
  ## Quick start
26
26
 
27
- Requirements: Paseo `^0.12.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Workspace discovery passes `includeBackground`, which throws on hosts that do not advertise `features.backgroundWorkspaces` (new in 0.12), so older hosts are not supported.
27
+ Requirements: Paseo `^0.13.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Workspace discovery passes `includeBackground`, which throws on hosts that do not advertise `features.backgroundWorkspaces`; every 0.13 host does.
28
28
 
29
29
  ```bash
30
30
  paseo plugin install npm:@omercnet/paseo-omp@<version>
@@ -40,7 +40,7 @@ Open the **OMP** sidebar to review the provider-profile contract, choose which c
40
40
  - [Core-provider issue and parity audit](docs/core-provider-issue-audit.md)
41
41
  - [Release checklist](docs/release-checklist.md)
42
42
 
43
- The plugin uses public Paseo 0.12 provider and client contracts and registers distinct `omp-plugin` and `omp-plugin-<profile>` identities; it does not modify the bundled `omp` provider.
43
+ The plugin uses public Paseo 0.13 provider and client contracts and registers distinct `omp-plugin` and `omp-plugin-<profile>` identities; it does not modify the bundled `omp` provider.
44
44
 
45
45
  ## Named OMP profiles
46
46
 
@@ -54,7 +54,7 @@ Profile selection is request-local: concurrent clients cannot change each other'
54
54
 
55
55
  ## Paseo provider SDK coverage
56
56
 
57
- The capability inventory is pinned to `@getpaseo/plugin` 0.12.0, with nightly compatibility checks
57
+ The capability inventory is pinned to `@getpaseo/plugin` 0.13.0, with nightly compatibility checks
58
58
  against the latest and beta channels. Provider capability scoring covers
59
59
  all 17 entries in the SDK's `PROVIDER_CAPABILITIES`; the separate provider/client SDK table below
60
60
  also tracks the registration, screen, and sidebar APIs introduced in 0.11. This measures strict SDK surface
@@ -95,12 +95,12 @@ Provider functionality outside the capability flags is tracked separately:
95
95
  | --- | ---: | --- |
96
96
  | Registration metadata and sanitized SVG icon | **100%** | Stable `omp-plugin` identity, label, description, and bundled icon. |
97
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. |
98
- | Availability diagnostics | **50%** | The plugin implements a `checkAvailability` hook with bounded checks that distinguish missing, unrunnable, incompatible, and available OMP runtimes, but Paseo 0.12 does not call it. The host uses the `status()` hook below instead, which reuses this probe for the default launch. |
98
+ | Availability diagnostics | **50%** | The plugin implements a `checkAvailability` hook with bounded checks that distinguish missing, unrunnable, incompatible, and available OMP runtimes, but Paseo 0.13 does not call it. The host uses the `status()` hook below instead, which reuses this probe for the default launch. |
99
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. |
100
100
  | Models, modes, and thinking catalog | **100%** | Native catalog is mapped to opaque public model IDs with committed defaults and permission-gated modes. |
101
101
  | Connection `send` / `onEvent` / `close` lifecycle | **100%** | Request correlation, multi-session ownership, process recovery, teardown, and provider reload/removal are covered. |
102
102
  | Session launch: cwd, env, system prompt, title, and persistence | **100%** | Complete launch configuration is bounded, validated, forwarded, and re-read on recovery; selected host and profile daemon environment values are resolved only when catalog and session children spawn. |
103
- | MCP server forwarding and host tools | **100%** | Configured MCP tools are discovered, namespaced, labeled, executed, canceled, and bounded. Forwarded tools are namespaced `mcpx__<server>_<tool>`: OMP treats `mcp__` as its own MCP manager's namespace and deletes that whole namespace from the session registry on every `refreshMCPTools`, so a forwarded tool registered under it is accepted and then dropped. Registration races OMP's deferred MCP discovery on every supported release; from 18.9.0 the ordering is deterministic and the tools were always lost. An OMP approval policy or inline-docs glob keyed on the old `mcp__<server>_<tool>` spelling must be re-keyed to `mcpx__<server>_<tool>`; OMP matches approval entries by exact tool name. Caller-scoped Paseo orchestration tools retain their native names, such as `create_agent` and `list_profiles`, so OMP skills can invoke them directly. The MCP composer control delegates native server management to OMP instead of duplicating its profile and workspace precedence. Exact `toolPolicy` remains the separate 0% capability above. |
103
+ | MCP server forwarding and host tools | **100%** | Configured MCP tools are discovered, namespaced, labeled, executed, canceled, and bounded. Forwarded tools are namespaced `mcpx__<server>_<tool>`: OMP treats `mcp__` as its own MCP manager's namespace and deletes that whole namespace from the session registry on every `refreshMCPTools`, so a forwarded tool registered under it is accepted and then dropped. Registration races OMP's deferred MCP discovery on every supported release; from 18.9.0 the ordering is deterministic and the tools were always lost. An OMP approval policy or inline-docs glob keyed on the old `mcp__<server>_<tool>` spelling must be re-keyed to `mcpx__<server>_<tool>`; OMP matches approval entries by exact tool name. Caller-scoped Paseo orchestration tools retain their native names, such as `create_agent` and `list_profiles`, so OMP skills can invoke them directly. The MCP composer control delegates native server management to OMP instead of duplicating its profile and workspace precedence. Exact `toolPolicy` remains the separate 0% capability above. Through the same bridge, OMP agents get the optional `serverId` argument on Paseo's host-targeting orchestration tools and the `list_hosts` tool, so they can delegate work to other connected Paseo hosts; the bridged tools execute on the daemon host while OMP stays local. |
104
104
  | Denied native tools | **100%** | `disallowedTools` becomes an explicit OMP allow-list; unknown names fail closed. |
105
105
  | Commands and committed session state events | **100%** | Publishes `session.commands`, `session.opened`, `session.config`, `session.ready`, and request completion in protocol order. |
106
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. |
@@ -125,7 +125,7 @@ Existing agents whose provider is `omp` remain owned by the bundled provider. Ne
125
125
 
126
126
  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.
127
127
 
128
- Paseo 0.12 is the minimum supported release. The controlled Docker canary is pinned to official 0.12.0; nightly SDK checks cover the latest and beta channels. The canary preserves nested provider-subagent ancestry and spawning-tool links, and validates npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping.
128
+ Paseo 0.13 is the minimum supported release. The controlled Docker canary is pinned to official 0.13.0; nightly SDK checks cover the latest and beta channels. The canary preserves nested provider-subagent ancestry and spawning-tool links, and validates npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping.
129
129
 
130
130
  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.
131
131
 
@@ -0,0 +1,4 @@
1
+ import ompSvg from "../shared/omp.svg";
2
+
3
+ // The same mark the provider registers; Paseo tints `currentColor` with the surface's icon color.
4
+ export const OMP_ICON = { svg: ompSvg };
@@ -2,6 +2,7 @@ import type { PluginPopoverProps, PluginSidebarItemProps } from "@getpaseo/plugi
2
2
  import { SidebarRow } from "@getpaseo/plugin/client/ui";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { Text, View } from "react-native";
5
+ import { OMP_ICON } from "./hub-icon";
5
6
  import { HubProcessList } from "./hub-popover";
6
7
  import { CONFIG_SCREEN_ID, type HubSnapshot, hubTrailing } from "./sidebar-compat";
7
8
 
@@ -12,7 +13,7 @@ const HUB_SIDEBAR_QUERY_KEY = ["paseo-omp", "hub-sidebar"] as const;
12
13
  export function ConfigSidebarItem({ currentScreen, openScreen }: PluginSidebarItemProps) {
13
14
  return (
14
15
  <SidebarRow
15
- icon="Settings"
16
+ icon={OMP_ICON}
16
17
  label="OMP"
17
18
  active={currentScreen?.screenId === CONFIG_SCREEN_ID}
18
19
  onPress={() => openScreen({ screenId: CONFIG_SCREEN_ID })}
@@ -1,5 +1,5 @@
1
1
  import { type PluginTimelineItemProps, useRpc } from "@getpaseo/plugin/client";
2
- import { Icon } from "@getpaseo/plugin/client/react-native";
2
+ import { Icon } from "@getpaseo/plugin/client/ui";
3
3
  import { useMemo, useState } from "react";
4
4
  import { Pressable, Text, View } from "react-native";
5
5
  import {
@@ -1,5 +1,5 @@
1
1
  import { type PluginButtonContentProps, useAgent, usePaseo } from "@getpaseo/plugin/client";
2
- import { TextInput } from "@getpaseo/plugin/client/react-native";
2
+ import { TextInput } from "@getpaseo/plugin/client/ui";
3
3
  import { useMemo, useState } from "react";
4
4
  import { Pressable, Text, View } from "react-native";
5
5
  import { buildOmpMcpServerCommand, type OmpMcpServerAction } from "../shared/mcp";
@@ -5,8 +5,7 @@ import {
5
5
  useRpc,
6
6
  useWorkspace,
7
7
  } from "@getpaseo/plugin/client";
8
- import { copyText, Icon, TextInput } from "@getpaseo/plugin/client/react-native";
9
- import { ExternalLink } from "@getpaseo/plugin/client/ui";
8
+ import { copyText, ExternalLink, Icon, TextInput } from "@getpaseo/plugin/client/ui";
10
9
  import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
11
10
  import type { ReactNode } from "react";
12
11
  import { useCallback, useMemo, useState } from "react";
@@ -1,5 +1,5 @@
1
1
  import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
2
- import { TextInput } from "@getpaseo/plugin/client/react-native";
2
+ import { TextInput } from "@getpaseo/plugin/client/ui";
3
3
  import { useMemo, useState } from "react";
4
4
  import type { TextStyle, ViewStyle } from "react-native";
5
5
  import { Pressable, Text, View } from "react-native";
@@ -1,5 +1,5 @@
1
1
  import { type PluginSurfaceProps, useRpc } from "@getpaseo/plugin/client";
2
- import { TextInput } from "@getpaseo/plugin/client/react-native";
2
+ import { TextInput } from "@getpaseo/plugin/client/ui";
3
3
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  import { useEffect, useMemo, useRef, useState } from "react";
5
5
  import type { TextStyle, ViewStyle } from "react-native";
@@ -1,5 +1,5 @@
1
1
  import type { PluginSurfaceProps } from "@getpaseo/plugin/client";
2
- import { TextInput } from "@getpaseo/plugin/client/react-native";
2
+ import { TextInput } from "@getpaseo/plugin/client/ui";
3
3
  import { useState } from "react";
4
4
  import { Pressable, Text, View } from "react-native";
5
5
  import {
@@ -1,5 +1,5 @@
1
1
  import type { PluginButtonIconProps } from "@getpaseo/plugin/client";
2
- import { Icon } from "@getpaseo/plugin/client/react-native";
2
+ import { Icon } from "@getpaseo/plugin/client/ui";
3
3
  import type { ComponentType } from "react";
4
4
  import { View } from "react-native";
5
5
  import { type QuotaSeverity, quotaProviderIconName } from "./quota-state";
@@ -1,5 +1,5 @@
1
1
  import { type PluginButtonContentProps, useAgent, useRpc } from "@getpaseo/plugin/client";
2
- import { Icon } from "@getpaseo/plugin/client/react-native";
2
+ import { Icon } from "@getpaseo/plugin/client/ui";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { useMemo } from "react";
5
5
  import { Text, View } from "react-native";
@@ -0,0 +1,4 @@
1
+ declare module "*.svg" {
2
+ const markup: string;
3
+ export default markup;
4
+ }
package/index.client.tsx CHANGED
@@ -4,7 +4,7 @@ import type {
4
4
  PluginClientContext,
5
5
  PluginSurfaceProps,
6
6
  } from "@getpaseo/plugin/client";
7
- import { OmpIcon } from "./client/hub-icon";
7
+ import { OMP_ICON } from "./client/hub-icon";
8
8
  import { HubPopover } from "./client/hub-popover";
9
9
  import { ConfigSidebarItem, createHubSidebar } from "./client/hub-sidebar";
10
10
  import { summarizeHubProcesses } from "./client/hub-status";
@@ -184,7 +184,6 @@ export default function contribute(client: PluginClientContext) {
184
184
  id: "memory",
185
185
  title: "OMP Memory",
186
186
  icon: "Brain",
187
- context: "workspace",
188
187
  locations: ["explorer"],
189
188
  Component: OmpMemoryPanel,
190
189
  });
@@ -202,7 +201,6 @@ export default function contribute(client: PluginClientContext) {
202
201
  id: "workspace",
203
202
  title: "OMP",
204
203
  icon: "Settings",
205
- context: "workspace",
206
204
  locations: ["workspace", "explorer"],
207
205
  Component: OmpWorkspacePanel,
208
206
  });
@@ -279,7 +277,7 @@ export default function contribute(client: PluginClientContext) {
279
277
  agentId: agent.id,
280
278
  button: {
281
279
  title: "Hub processes",
282
- icon: OmpIcon,
280
+ icon: OMP_ICON,
283
281
  label: "Hub",
284
282
  visible: false,
285
283
  behavior: { kind: "popover", Content: HubPopover },
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@omercnet/paseo-omp",
9
- "version": "3.0.0",
9
+ "version": "4.0.0",
10
10
  "dependencies": {
11
- "@getpaseo/protocol": "0.12.0",
11
+ "@getpaseo/protocol": "0.13.0",
12
12
  "@modelcontextprotocol/sdk": "1.30.0",
13
13
  "yaml": "^2.9.0"
14
14
  }
15
15
  },
16
16
  "node_modules/@getpaseo/protocol": {
17
- "version": "0.12.0",
18
- "resolved": "https://registry.npmjs.org/@getpaseo/protocol/-/protocol-0.12.0.tgz",
19
- "integrity": "sha512-05jivVDeG18jWTpvQZ3yETNxH/AfHpHy9CgnP7O5ZxQLUr3onJIAqaGNzv15dPnGf4fU342UHQBFAE2t2Yc7fQ==",
17
+ "version": "0.13.0",
18
+ "resolved": "https://registry.npmjs.org/@getpaseo/protocol/-/protocol-0.13.0.tgz",
19
+ "integrity": "sha512-g5Hp6QP5hV7KTCrSRBVye2+qhptwnhhLXRn6PHaS1L8PgmeRh5LvrE8PZq3HRq8B1WxflVDuV2WRB2Z3FNcvJA==",
20
20
  "dependencies": {
21
21
  "semver": "^7.8.5",
22
22
  "zod": "^4.4.3"
@@ -121,18 +121,18 @@
121
121
  }
122
122
  },
123
123
  "node_modules/body-parser": {
124
- "version": "2.3.0",
125
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
126
- "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
124
+ "version": "2.4.0",
125
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz",
126
+ "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==",
127
127
  "license": "MIT",
128
128
  "dependencies": {
129
129
  "bytes": "^3.1.2",
130
- "content-type": "^2.0.0",
130
+ "content-type": "^2.1.0",
131
131
  "debug": "^4.4.3",
132
132
  "http-errors": "^2.0.1",
133
- "iconv-lite": "^0.7.2",
133
+ "iconv-lite": "^0.7.3",
134
134
  "on-finished": "^2.4.1",
135
- "qs": "^6.15.2",
135
+ "qs": "^6.16.1",
136
136
  "raw-body": "^3.0.2",
137
137
  "type-is": "^2.1.0"
138
138
  },
@@ -469,9 +469,9 @@
469
469
  "license": "MIT"
470
470
  },
471
471
  "node_modules/fast-uri": {
472
- "version": "3.1.8",
473
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
474
- "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
472
+ "version": "3.1.9",
473
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
474
+ "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
475
475
  "funding": [
476
476
  {
477
477
  "type": "github",
@@ -687,9 +687,9 @@
687
687
  "license": "ISC"
688
688
  },
689
689
  "node_modules/jose": {
690
- "version": "6.2.12",
691
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
692
- "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
690
+ "version": "6.2.13",
691
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz",
692
+ "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==",
693
693
  "license": "MIT",
694
694
  "funding": {
695
695
  "url": "https://github.com/sponsors/panva"
@@ -823,15 +823,19 @@
823
823
  }
824
824
  },
825
825
  "node_modules/on-finished": {
826
- "version": "2.4.1",
827
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
828
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
826
+ "version": "2.4.2",
827
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz",
828
+ "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==",
829
829
  "license": "MIT",
830
830
  "dependencies": {
831
831
  "ee-first": "1.1.1"
832
832
  },
833
833
  "engines": {
834
834
  "node": ">= 0.8"
835
+ },
836
+ "funding": {
837
+ "type": "opencollective",
838
+ "url": "https://opencollective.com/express"
835
839
  }
836
840
  },
837
841
  "node_modules/once": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "3.0.0",
3
+ "version": "4.0.0-next.312.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -53,16 +53,16 @@
53
53
  "typecheck": "tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.tests.json"
54
54
  },
55
55
  "dependencies": {
56
- "@getpaseo/protocol": "0.12.0",
56
+ "@getpaseo/protocol": "0.13.0",
57
57
  "@modelcontextprotocol/sdk": "1.30.0",
58
58
  "yaml": "^2.9.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@biomejs/biome": "^2.5.10",
62
- "@getpaseo/cli": "0.12.0",
63
- "@getpaseo/client": "0.12.0",
64
- "@getpaseo/plugin": "0.12.0",
65
- "@getpaseo/server": "0.12.0",
62
+ "@getpaseo/cli": "0.13.0",
63
+ "@getpaseo/client": "0.13.0",
64
+ "@getpaseo/plugin": "0.13.0",
65
+ "@getpaseo/server": "0.13.0",
66
66
  "@oh-my-pi/pi-agent-core": "18.10.1",
67
67
  "@oh-my-pi/pi-coding-agent": "18.10.1",
68
68
  "@tanstack/react-query": "^5.102.3",
@@ -70,9 +70,9 @@
70
70
  "@types/react": "^19.2.18",
71
71
  "@vitest/coverage-v8": "^5.0.0",
72
72
  "esbuild": "0.28.2",
73
+ "pino": "10.3.1",
73
74
  "react": "19.1.0",
74
75
  "react-native": "0.81.5",
75
- "pino": "10.3.1",
76
76
  "tsx": "^4.20.6",
77
77
  "typescript": "^7.0.2",
78
78
  "vitest": "^5.0.0",
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.12.0" },
4
+ "requirements": { "paseo": "^0.13.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 = "3.0.0"; // x-release-please-version
2
+ export const PASEO_OMP_PACKAGE_VERSION = "4.0.0-next.312.1"; // x-release-please-version
@@ -155,7 +155,7 @@ export const OMP_RPC_COMMAND_POLICIES = {
155
155
  predict_word_feedback: "unsupported",
156
156
  promote_queued_message: "unsupported",
157
157
  remove_queued_message: "unsupported",
158
- set_ask_dialog: "unsupported",
158
+ set_ask_dialog: "implemented",
159
159
  set_cache_warming: "unsupported",
160
160
  set_slow_mode: "unsupported",
161
161
  steer_subagent: "unsupported",
@@ -524,6 +524,10 @@ const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
524
524
  const TEXT = boundedString(MAX_TEXT_LENGTH);
525
525
  const RAW_DISPLAY_TEXT = boundedString(MAX_IMAGE_DATA_LENGTH);
526
526
  const DISPLAY_TRUNCATION_MARKER = "<truncated>";
527
+ /** OMP's `ask` tool has no upstream caps; these bound one dialog like the select schema does. */
528
+ export const MAX_ASK_QUESTIONS = 32;
529
+ export const MAX_ASK_OPTIONS = 128;
530
+ const MAX_ASK_QUESTION_ID_BYTES = 4_096;
527
531
 
528
532
  function boundRawDisplayContent(value: unknown): unknown {
529
533
  if (typeof value === "string") {
@@ -634,6 +638,7 @@ export const OMP_RPC_DIAGNOSTIC_COMMANDS = [
634
638
  "get_messages",
635
639
  "abort",
636
640
  "set_host_tools",
641
+ "set_ask_dialog",
637
642
  ] as const;
638
643
  export type OmpRpcDiagnosticCommand = (typeof OMP_RPC_DIAGNOSTIC_COMMANDS)[number];
639
644
  type JsonBoundViolation = {
@@ -1695,6 +1700,44 @@ const OmpExtensionUiRequestSchema = z.discriminatedUnion("method", [
1695
1700
  timeout: z.number().nonnegative().finite().optional(),
1696
1701
  })
1697
1702
  .strict(),
1703
+ z
1704
+ .object({
1705
+ ...ExtensionUiBase,
1706
+ method: z.literal("ask"),
1707
+ questions: z
1708
+ .array(
1709
+ z
1710
+ .object({
1711
+ id: boundedString(MAX_ASK_QUESTION_ID_BYTES, 1),
1712
+ question: boundedString(64 * 1024),
1713
+ header: boundedString(4_096).optional(),
1714
+ options: z
1715
+ .array(
1716
+ z
1717
+ .object({
1718
+ label: boundedString(4_096),
1719
+ description: boundedString(16_384).optional(),
1720
+ preview: boundedString(64 * 1024).optional(),
1721
+ })
1722
+ .strict(),
1723
+ )
1724
+ .max(MAX_ASK_OPTIONS),
1725
+ multi: z.boolean().optional(),
1726
+ recommended: z.number().int().nonnegative().optional(),
1727
+ })
1728
+ .strict(),
1729
+ )
1730
+ .min(1)
1731
+ .max(MAX_ASK_QUESTIONS),
1732
+ timeout: z.number().nonnegative().finite().optional(),
1733
+ })
1734
+ .strict()
1735
+ .superRefine((request, context) => {
1736
+ const ids = new Set(request.questions.map((question) => question.id));
1737
+ if (ids.size !== request.questions.length) {
1738
+ context.addIssue({ code: "custom", message: "invalid extension UI request" });
1739
+ }
1740
+ }),
1698
1741
  z.object({ ...ExtensionUiBase, method: z.literal("cancel"), targetId: IDENTIFIER }).strict(),
1699
1742
  z
1700
1743
  .object({
@@ -1980,7 +2023,37 @@ export interface OmpPersistedSessionMessages {
1980
2023
 
1981
2024
  export type OmpAvailableCommand = z.infer<typeof OmpAvailableCommandSchema>;
1982
2025
  export type OmpImage = { type: "image"; data: string; mimeType: string };
1983
- export type OmpExtensionUiResponse =
1984
- | { type: "extension_ui_response"; id: string; value: string }
1985
- | { type: "extension_ui_response"; id: string; confirmed: boolean }
1986
- | { type: "extension_ui_response"; id: string; cancelled: true; timedOut?: boolean };
2026
+ export const OmpExtensionUiResponseSchema = z.union([
2027
+ z.object({ type: z.literal("extension_ui_response"), id: IDENTIFIER, value: TEXT }).strict(),
2028
+ z
2029
+ .object({ type: z.literal("extension_ui_response"), id: IDENTIFIER, confirmed: z.boolean() })
2030
+ .strict(),
2031
+ z
2032
+ .object({
2033
+ type: z.literal("extension_ui_response"),
2034
+ id: IDENTIFIER,
2035
+ cancelled: z.literal(true),
2036
+ timedOut: z.boolean().optional(),
2037
+ })
2038
+ .strict(),
2039
+ z
2040
+ .object({
2041
+ type: z.literal("extension_ui_response"),
2042
+ id: IDENTIFIER,
2043
+ answers: z
2044
+ .array(
2045
+ z
2046
+ .object({
2047
+ id: boundedString(MAX_ASK_QUESTION_ID_BYTES, 1),
2048
+ selectedOptions: z.array(boundedString(4_096)).max(MAX_ASK_OPTIONS),
2049
+ customInput: TEXT.optional(),
2050
+ })
2051
+ .strict(),
2052
+ )
2053
+ .min(1)
2054
+ .max(MAX_ASK_QUESTIONS),
2055
+ })
2056
+ .strict(),
2057
+ ]);
2058
+ export type OmpExtensionUiResponse = z.infer<typeof OmpExtensionUiResponseSchema>;
2059
+ export const OmpAskDialogEnabledSchema = z.object({ enabled: z.boolean() }).strict();
@@ -71,6 +71,7 @@ const CHUNK_STALE_MS = 30_000;
71
71
  const MAX_PHYSICAL_FRAME_BYTES = 1024 * 1024;
72
72
  const MAX_STREAM_TEXT_LENGTH = 4 * 1024 * 1024;
73
73
  const MAX_ACTIVE_TOOLS = 64;
74
+ const MAX_ENDED_TOOL_IDS = 1_024;
74
75
  const MAX_PENDING_REQUESTS = 256;
75
76
  const MAX_PENDING_ONE_WAY_WRITES = 256;
76
77
  const MAX_PENDING_WRITE_BYTES = 8 * 1024 * 1024;
@@ -164,6 +165,9 @@ export class OmpRpcProcess {
164
165
  private readonly streamedBlocks = new Map<number, string>();
165
166
  private readonly activeToolCallIds = new Set<string>();
166
167
  private readonly announcedToolCallIds = new Set<string>();
168
+ // Detached background tools keep streaming updates after they return and after turn_end.
169
+ // FIFO-capped: an update for an id evicted after MAX_ENDED_TOOL_IDS later calls is flagged again.
170
+ private readonly endedToolCallIds = new Set<string>();
167
171
  private pendingWriteBytes = 0;
168
172
  private commandTextLength = 0;
169
173
  private lineParts: Buffer[] = [];
@@ -1081,6 +1085,8 @@ export class OmpRpcProcess {
1081
1085
  });
1082
1086
  return;
1083
1087
  }
1088
+ // Emitted once the session has no live or background work. Nothing consumes it yet.
1089
+ if (type === "session_settled") return;
1084
1090
  if (type === "ready") {
1085
1091
  if (this.readyReceived) {
1086
1092
  this.recordProtocolViolation("duplicate-ready", {
@@ -1121,6 +1127,12 @@ export class OmpRpcProcess {
1121
1127
  });
1122
1128
  return;
1123
1129
  }
1130
+ if (
1131
+ event.data.type === "tool_execution_update" &&
1132
+ this.endedToolCallIds.has(event.data.toolCallId)
1133
+ ) {
1134
+ return;
1135
+ }
1124
1136
  if (!this.acceptEventState(event.data)) {
1125
1137
  this.recordProtocolViolation("invalid-event-state", {
1126
1138
  reason: "event-state-transition",
@@ -1188,6 +1200,7 @@ export class OmpRpcProcess {
1188
1200
  return false;
1189
1201
  }
1190
1202
  this.activeToolCallIds.add(event.toolCallId);
1203
+ this.endedToolCallIds.delete(event.toolCallId);
1191
1204
  return true;
1192
1205
  }
1193
1206
  if (event.type === "tool_stream_update") {
@@ -1203,6 +1216,11 @@ export class OmpRpcProcess {
1203
1216
  if (!this.activeToolCallIds.has(event.toolCallId)) return false;
1204
1217
  this.activeToolCallIds.delete(event.toolCallId);
1205
1218
  this.announcedToolCallIds.delete(event.toolCallId);
1219
+ if (this.endedToolCallIds.size >= MAX_ENDED_TOOL_IDS) {
1220
+ const oldest = this.endedToolCallIds.values().next().value;
1221
+ if (oldest !== undefined) this.endedToolCallIds.delete(oldest);
1222
+ }
1223
+ this.endedToolCallIds.add(event.toolCallId);
1206
1224
  return true;
1207
1225
  }
1208
1226
  if (
@@ -10,6 +10,7 @@ import {
10
10
  MAX_REPLAY_MESSAGES,
11
11
  NAME,
12
12
  OMP_MESSAGE_PAGE_LIMIT,
13
+ OmpAskDialogEnabledSchema,
13
14
  type OmpAvailableCommand,
14
15
  OmpAvailableCommandsResultSchema,
15
16
  OmpBranchMessagesResultSchema,
@@ -18,6 +19,7 @@ import {
18
19
  type OmpCompactionResult,
19
20
  OmpCompactionResultSchema,
20
21
  type OmpExtensionUiResponse,
22
+ OmpExtensionUiResponseSchema,
21
23
  type OmpHostToolDefinition,
22
24
  OmpHostToolDefinitionSchema,
23
25
  type OmpHostToolResult,
@@ -546,7 +548,10 @@ class OmpRpcSession implements OmpRuntimeSession {
546
548
  }
547
549
 
548
550
  respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void> {
549
- return this.process.sendFrame(response);
551
+ // Callers chain `.catch` without a surrounding try, so a bad frame must reject, not throw.
552
+ const parsed = OmpExtensionUiResponseSchema.safeParse(response);
553
+ if (!parsed.success) return Promise.reject(parsed.error);
554
+ return this.process.sendFrame(parsed.data);
550
555
  }
551
556
  respondToToolApproval(response: OmpToolApprovalResponse): Promise<void> {
552
557
  return this.process.sendFrame(OmpToolApprovalResponseSchema.parse(response));
@@ -652,6 +657,7 @@ export class OmpRpcRuntime implements OmpRuntime {
652
657
  }),
653
658
  );
654
659
  options.signal?.throwIfAborted();
660
+ enableAskDialog(process);
655
661
  return new OmpRpcSession(
656
662
  process,
657
663
  removeAbortListener,
@@ -671,3 +677,17 @@ export class OmpRpcRuntime implements OmpRuntime {
671
677
  }
672
678
  }
673
679
  }
680
+
681
+ /**
682
+ * Opts into OMP's single-request `ask` dialog so every question reaches Paseo as one card that can
683
+ * be revisited before submitting. OMP applies the flag in command order, so it is set before any
684
+ * later prompt can ask; startup does not wait for the acknowledgement. OMP releases without
685
+ * `set_ask_dialog` reject it and keep the per-question select/editor fallback, which the
686
+ * permission bridge still handles.
687
+ */
688
+ function enableAskDialog(process: OmpRpcProcess): void {
689
+ process
690
+ .request({ type: "set_ask_dialog", enabled: true })
691
+ .then((data) => OmpAskDialogEnabledSchema.parse(data))
692
+ .catch(() => undefined);
693
+ }
@@ -173,7 +173,7 @@ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderReg
173
173
  id: "omp-plugin",
174
174
  label: "OMP Plugin",
175
175
  description: "Direct plugin provider for OMP's rpc-ui protocol",
176
- icon: "server/provider/omp.svg",
176
+ icon: "shared/omp.svg",
177
177
  providerOptionsSchema: OmpProviderOptionsSchema,
178
178
  async getCatalogCacheKey(catalogOptions) {
179
179
  const providerOptions = parseOmpProviderOptions(catalogOptions.providerOptions);
@@ -25,14 +25,23 @@ import type { OmpTimelineProjector, OmpTimelineScheduler } from "./timeline-proj
25
25
  type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
26
26
  type OmpQuestionRequest = Extract<
27
27
  Extract<OmpRpcEvent, { type: "extension_ui_request" }>,
28
- { method: "select" | "confirm" | "input" | "editor" }
28
+ { method: "select" | "confirm" | "input" | "editor" | "ask" }
29
29
  >;
30
+ type OmpAskRequest = Extract<OmpQuestionRequest, { method: "ask" }>;
30
31
  type Emit = (event: ProviderEvent) => void;
31
32
 
32
33
  const MAX_PENDING_PERMISSIONS = 32;
33
34
  const MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
34
35
  const MAX_RESOLVED_TOOL_APPROVAL_IDS = 1_024;
35
- const OMP_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
36
+ /**
37
+ * Select rows that hand off to a follow-up free-text request: `ask_user` extensions follow with
38
+ * `input`, OMP's own `ask` fallback (hosts without `set_ask_dialog`) follows with `editor`.
39
+ */
40
+ const OMP_FREEFORM_SENTINELS: readonly string[] = [
41
+ "✏️ Type custom response...",
42
+ "Other (type your own)",
43
+ ];
44
+ const OMP_ASK_RECOMMENDED_SUFFIX = " (Recommended)";
36
45
  const MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
37
46
 
38
47
  type PendingPermission = {
@@ -50,6 +59,14 @@ type PendingPermission = {
50
59
  turnId?: string;
51
60
  request: OmpQuestionRequest;
52
61
  freeformSentinel?: string;
62
+ askQuestions?: readonly PendingAskQuestion[];
63
+ };
64
+ /** One `ask` question as rendered on the Paseo card, with display labels mapped back to OMP. */
65
+ type PendingAskQuestion = {
66
+ id: string;
67
+ header: string;
68
+ multi: boolean;
69
+ nativeLabels: ReadonlyMap<string, string>;
53
70
  };
54
71
  type PendingToolPermission = {
55
72
  nativeId: string;
@@ -75,6 +92,96 @@ function permissionFingerprint(request: OmpQuestionRequest): string {
75
92
  return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
76
93
  }
77
94
 
95
+ type CardQuestion = {
96
+ header: string;
97
+ question: string;
98
+ options: Array<{ label: string; value?: string; description?: string; preview?: string }>;
99
+ multiSelect: boolean;
100
+ allowOther?: boolean;
101
+ placeholder?: string;
102
+ prefill?: string;
103
+ };
104
+ type AskAnswer = { id: string; selectedOptions: string[]; customInput?: string };
105
+
106
+ function uniqueLabel(base: string, used: Set<string>): string {
107
+ let label = base;
108
+ let suffix = 2;
109
+ while (used.has(label)) {
110
+ label = `${base} (${suffix})`;
111
+ suffix += 1;
112
+ }
113
+ used.add(label);
114
+ return label;
115
+ }
116
+
117
+ /**
118
+ * Paseo's question card submits a multi-select answer as one string: checked option labels in
119
+ * click order, then any typed answer, joined with ", ". Exact labels are consumed from the front,
120
+ * longest first so a label containing ", " wins over its prefix; the rest is the typed answer.
121
+ */
122
+ function splitMultiSelectAnswer(
123
+ answer: string,
124
+ labels: readonly string[],
125
+ ): { selected: string[]; custom: string } {
126
+ const candidates = [...labels].sort((left, right) => right.length - left.length);
127
+ const selected: string[] = [];
128
+ let remaining = answer;
129
+ while (remaining.length > 0) {
130
+ const label = candidates.find(
131
+ (candidate) =>
132
+ !selected.includes(candidate) &&
133
+ (remaining === candidate || remaining.startsWith(`${candidate}, `)),
134
+ );
135
+ if (label === undefined) break;
136
+ selected.push(label);
137
+ remaining = remaining === label ? "" : remaining.slice(label.length + 2);
138
+ }
139
+ return { selected, custom: remaining };
140
+ }
141
+
142
+ /**
143
+ * Converts one card answer to OMP's shape. OMP rejects unknown or repeated labels and, for
144
+ * single-select questions, a selection combined with typed text, so neither is ever produced.
145
+ */
146
+ function askAnswer(question: PendingAskQuestion, answer: unknown): AskAnswer {
147
+ const parts =
148
+ answer === undefined || answer === null
149
+ ? []
150
+ : typeof answer === "string"
151
+ ? [answer]
152
+ : Array.isArray(answer) && answer.every((part) => typeof part === "string")
153
+ ? answer
154
+ : undefined;
155
+ if (!parts) throw new OmpPublicError("OMP question response is invalid");
156
+ const labels = [...question.nativeLabels.keys()];
157
+ const selectedOptions: string[] = [];
158
+ const custom: string[] = [];
159
+ const select = (label: string) => {
160
+ const native = question.nativeLabels.get(label);
161
+ if (native !== undefined && !selectedOptions.includes(native)) selectedOptions.push(native);
162
+ };
163
+ for (const part of parts) {
164
+ if (question.nativeLabels.has(part)) {
165
+ select(part);
166
+ } else if (question.multi) {
167
+ const split = splitMultiSelectAnswer(part, labels);
168
+ for (const label of split.selected) select(label);
169
+ if (split.custom.trim()) custom.push(split.custom);
170
+ } else if (part.trim()) {
171
+ custom.push(part);
172
+ }
173
+ }
174
+ const customInput = custom.join(", ").trim();
175
+ if (
176
+ customInput.includes("\0") ||
177
+ utf8Bytes(customInput) > MAX_FREEFORM_RESPONSE_BYTES ||
178
+ (!question.multi && selectedOptions.length + (customInput ? 1 : 0) > 1)
179
+ ) {
180
+ throw new OmpPublicError("OMP question response is invalid");
181
+ }
182
+ return { id: question.id, selectedOptions, ...(customInput ? { customInput } : {}) };
183
+ }
184
+
78
185
  interface PermissionContext {
79
186
  runtime: OmpRuntimeSession;
80
187
  generation: number;
@@ -279,7 +386,7 @@ export class OmpSessionPermissions {
279
386
  }
280
387
 
281
388
  submitPendingFreeformSelection(
282
- request: Extract<OmpQuestionRequest, { method: "input" }>,
389
+ request: Extract<OmpQuestionRequest, { method: "input" | "editor" }>,
283
390
  ): boolean {
284
391
  const pending = this.pendingFreeformSelection;
285
392
  if (!pending) return false;
@@ -505,10 +612,17 @@ export class OmpSessionPermissions {
505
612
  if (!existingId) this.permissionSequence += 1;
506
613
  const id =
507
614
  existingId ?? `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
508
- const header = this.dataFilter.text(request.title ?? "OMP question", 4_096);
615
+ const header = this.dataFilter.text(
616
+ request.method === "ask"
617
+ ? request.questions.length === 1
618
+ ? "OMP question"
619
+ : `OMP questions (${request.questions.length})`
620
+ : (request.title ?? "OMP question"),
621
+ 4_096,
622
+ );
509
623
  const freeformSentinel =
510
- request.method === "select" && request.options.includes(OMP_ASK_USER_FREEFORM_SENTINEL)
511
- ? OMP_ASK_USER_FREEFORM_SENTINEL
624
+ request.method === "select"
625
+ ? request.options.find((option) => OMP_FREEFORM_SENTINELS.includes(option))
512
626
  : undefined;
513
627
  const optionValues = new Map<string, string>();
514
628
  const displayValues = new Map<string, string>();
@@ -523,14 +637,7 @@ export class OmpSessionPermissions {
523
637
  const options =
524
638
  request.method === "select"
525
639
  ? selectableOptions.map(({ nativeValue, index }) => {
526
- const baseLabel = this.dataFilter.text(nativeValue, 4_096);
527
- let label = baseLabel;
528
- let suffix = 2;
529
- while (usedOptionLabels.has(label)) {
530
- label = `${baseLabel} (${suffix})`;
531
- suffix += 1;
532
- }
533
- usedOptionLabels.add(label);
640
+ const label = uniqueLabel(this.dataFilter.text(nativeValue, 4_096), usedOptionLabels);
534
641
  const value = `${id}:option:${index}`;
535
642
  optionValues.set(value, nativeValue);
536
643
  displayValues.set(label, nativeValue);
@@ -550,6 +657,28 @@ export class OmpSessionPermissions {
550
657
  : undefined;
551
658
  const questionOptions =
552
659
  request.method === "confirm" ? [{ label: "Yes" }, { label: "No" }] : (options ?? []);
660
+ const ask = request.method === "ask" ? this.askCard(request) : undefined;
661
+ const cardQuestions: CardQuestion[] =
662
+ request.method === "ask"
663
+ ? (ask?.questions ?? [])
664
+ : [
665
+ {
666
+ header,
667
+ question: this.dataFilter.text(
668
+ request.method === "confirm" ? request.message : request.title,
669
+ 64 * 1024,
670
+ ),
671
+ options: questionOptions,
672
+ multiSelect: false,
673
+ ...(freeformSentinel ? { allowOther: true } : {}),
674
+ ...(request.method === "input" && request.placeholder
675
+ ? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
676
+ : {}),
677
+ ...((request.method === "input" || request.method === "editor") && request.prefill
678
+ ? { prefill: this.dataFilter.text(request.prefill) }
679
+ : {}),
680
+ },
681
+ ];
553
682
  const actions =
554
683
  request.method === "select"
555
684
  ? [
@@ -591,7 +720,7 @@ export class OmpSessionPermissions {
591
720
  runtime: this.runtime,
592
721
  request,
593
722
  retainedBytes: boundedJsonBytes(
594
- { request, header, options: questionOptions, actions },
723
+ { request, header, questions: cardQuestions, actions },
595
724
  MAX_PENDING_PERMISSION_BYTES,
596
725
  512,
597
726
  MAX_PENDING_PERMISSION_BYTES,
@@ -600,6 +729,7 @@ export class OmpSessionPermissions {
600
729
  ...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
601
730
  ...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
602
731
  ...(freeformSentinel ? { freeformSentinel } : {}),
732
+ ...(ask ? { askQuestions: ask.pending } : {}),
603
733
  };
604
734
  let retainedPermissionBytes = pending.retainedBytes;
605
735
  for (const [permissionId, retained] of this.pendingPermissions) {
@@ -647,26 +777,7 @@ export class OmpSessionPermissions {
647
777
  ...(request.method === "confirm"
648
778
  ? { description: this.dataFilter.text(request.message, 64 * 1024) }
649
779
  : {}),
650
- input: {
651
- questions: [
652
- {
653
- header,
654
- question: this.dataFilter.text(
655
- request.method === "confirm" ? request.message : request.title,
656
- 64 * 1024,
657
- ),
658
- options: questionOptions,
659
- multiSelect: false,
660
- ...(freeformSentinel ? { allowOther: true } : {}),
661
- ...(request.method === "input" && request.placeholder
662
- ? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
663
- : {}),
664
- ...((request.method === "input" || request.method === "editor") && request.prefill
665
- ? { prefill: this.dataFilter.text(request.prefill) }
666
- : {}),
667
- },
668
- ],
669
- },
780
+ input: { questions: cardQuestions },
670
781
  actions,
671
782
  },
672
783
  });
@@ -688,6 +799,76 @@ export class OmpSessionPermissions {
688
799
  .catch(() => this.handleRuntimeFailure());
689
800
  }
690
801
 
802
+ /**
803
+ * Renders every `ask` question on one card. Headers key the card's answers and option labels
804
+ * come back verbatim, so both are made unique here and mapped back to OMP's labels on submit.
805
+ */
806
+ private askCard(request: OmpAskRequest): {
807
+ questions: CardQuestion[];
808
+ pending: PendingAskQuestion[];
809
+ } {
810
+ const usedHeaders = new Set<string>();
811
+ const questions: CardQuestion[] = [];
812
+ const pending: PendingAskQuestion[] = [];
813
+ for (const [index, question] of request.questions.entries()) {
814
+ const header = uniqueLabel(
815
+ this.dataFilter.text(question.header?.trim() || `Question ${index + 1}`, 4_096),
816
+ usedHeaders,
817
+ );
818
+ const usedLabels = new Set<string>();
819
+ const nativeLabels = new Map<string, string>();
820
+ const options = question.options.map((option, optionIndex) => {
821
+ const label = uniqueLabel(
822
+ `${this.dataFilter.text(option.label, 4_096)}${
823
+ optionIndex === question.recommended ? OMP_ASK_RECOMMENDED_SUFFIX : ""
824
+ }`,
825
+ usedLabels,
826
+ );
827
+ nativeLabels.set(label, option.label);
828
+ return {
829
+ label,
830
+ ...(option.description
831
+ ? { description: this.dataFilter.text(option.description, 16_384) }
832
+ : {}),
833
+ ...(option.preview ? { preview: this.dataFilter.text(option.preview, 64 * 1024) } : {}),
834
+ };
835
+ });
836
+ const multi = question.multi === true;
837
+ questions.push({
838
+ header,
839
+ question: this.dataFilter.text(question.question, 64 * 1024),
840
+ options,
841
+ multiSelect: multi,
842
+ allowOther: true,
843
+ });
844
+ pending.push({ id: question.id, header, multi, nativeLabels });
845
+ }
846
+ return { questions, pending };
847
+ }
848
+
849
+ /** Maps card answers to OMP's per-question `selectedOptions`/`customInput` in question order. */
850
+ private askResponse(
851
+ pending: PendingPermission,
852
+ response: ProviderPermissionResponse,
853
+ ): OmpExtensionUiResponse {
854
+ const answers = response.behavior === "allow" ? response.updatedInput?.answers : undefined;
855
+ if (
856
+ !pending.askQuestions ||
857
+ !answers ||
858
+ typeof answers !== "object" ||
859
+ Array.isArray(answers)
860
+ ) {
861
+ throw new OmpPublicError("OMP question response requires answers");
862
+ }
863
+ return {
864
+ type: "extension_ui_response",
865
+ id: pending.nativeId,
866
+ answers: pending.askQuestions.map((question) =>
867
+ askAnswer(question, (answers as Record<string, unknown>)[question.header]),
868
+ ),
869
+ };
870
+ }
871
+
691
872
  private freeformSelection(
692
873
  pending: PendingPermission,
693
874
  response: ProviderPermissionResponse,
@@ -754,6 +935,7 @@ export class OmpSessionPermissions {
754
935
  if (response.behavior === "deny") {
755
936
  return { type: "extension_ui_response", id: nativeId, cancelled: true };
756
937
  }
938
+ if (request.method === "ask") return this.askResponse(pending, response);
757
939
  const selectedValue = response.selectedActionId
758
940
  ? pending.optionValues.get(response.selectedActionId)
759
941
  : undefined;
@@ -2280,13 +2280,17 @@ export class OmpProviderSession implements OmpOpenedSession {
2280
2280
  this.permissions.resolvePermissionByNativeId(event.targetId ?? "");
2281
2281
  return;
2282
2282
  }
2283
- if (event.method === "input" && this.permissions.submitPendingFreeformSelection(event))
2283
+ if (
2284
+ (event.method === "input" || event.method === "editor") &&
2285
+ this.permissions.submitPendingFreeformSelection(event)
2286
+ )
2284
2287
  return;
2285
2288
  if (
2286
2289
  event.method === "select" ||
2287
2290
  event.method === "confirm" ||
2288
2291
  event.method === "input" ||
2289
- event.method === "editor"
2292
+ event.method === "editor" ||
2293
+ event.method === "ask"
2290
2294
  ) {
2291
2295
  if (!this.capabilities.includes("permission")) {
2292
2296
  this.handleRuntimeFailure();
@@ -2459,7 +2463,9 @@ export class OmpProviderSession implements OmpOpenedSession {
2459
2463
  if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
2460
2464
  if (event.type === "tool_execution_end") {
2461
2465
  turn.activeToolCallIds.delete(event.toolCallId);
2462
- if (event.toolName === "ask_user") this.permissions.clearPendingFreeformSelection();
2466
+ if (event.toolName === "ask_user" || event.toolName === "ask") {
2467
+ this.permissions.clearPendingFreeformSelection();
2468
+ }
2463
2469
  }
2464
2470
  if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
2465
2471
  try {
@@ -1,12 +0,0 @@
1
- import type { PluginButtonIconProps } from "@getpaseo/plugin/client";
2
- import { Text, View } from "react-native";
3
-
4
- // omp (oh-my-pi) uses the option/alt-key glyph "⌥" as its mark; there is no separate
5
- // logo asset to bundle, so the pill renders the glyph directly at the host's icon size.
6
- export function OmpIcon({ size, color }: PluginButtonIconProps) {
7
- return (
8
- <View style={{ width: size, height: size, alignItems: "center", justifyContent: "center" }}>
9
- <Text style={{ fontSize: size * 0.85, lineHeight: size, color, fontWeight: "600" }}>⌥</Text>
10
- </View>
11
- );
12
- }
File without changes