@omercnet/paseo-omp 5.1.1-next.336.1 → 5.1.1-next.340.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
@@ -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
+ - While OMP subagents run, a row under each `task` card in the conversation lets you steer one with a message or stop it after confirmation. It needs OMP 18.4.9 or newer.
14
15
  - OMP agents can call Paseo's orchestration tools with the optional `serverId` argument and `list_hosts` to delegate work to other connected Paseo hosts.
15
16
 
16
17
  ## Reads and sends
package/README.md CHANGED
@@ -31,7 +31,7 @@ paseo plugin install npm:@omercnet/paseo-omp@<version>
31
31
  paseo plugin ls paseo-omp
32
32
  ```
33
33
 
34
- Open the **OMP** sidebar to review the provider-profile contract, choose which composer pills appear, browse and edit native scalar settings, manage OMP-native plugins, verify runtime health, and generate a bounded support report under **Help**. The same tabs are available in the workspace OMP panel, except for the global Composer preference tab. Then create an agent and select **OMP Plugin**. Its optional **MCP** composer control runs OMP-native management commands in the current session; setup questions and authorization stay in that chat timeline, where OAuth can open in a workspace-scoped Paseo Browser or on the current device. Composer pill preferences are shared by clients connected to the same host; normal use requires no other plugin-specific settings.
34
+ Open the **OMP** sidebar to review the provider-profile contract, choose which composer pills appear, browse and edit native scalar settings, manage OMP-native plugins, verify runtime health, and generate a bounded support report under **Help**. The same tabs are available in the workspace OMP panel, except for the global Composer preference tab. Then create an agent and select **OMP Plugin**. Its optional **MCP** composer control runs OMP-native management commands in the current session; setup questions and authorization stay in that chat timeline, where OAuth can open in a workspace-scoped Paseo Browser or on the current device. While OMP subagents run, a row under each `task` card in the conversation lets you steer or stop one of them; this needs OMP 18.4.9 or newer. Composer pill preferences are shared by clients connected to the same host; normal use requires no other plugin-specific settings.
35
35
 
36
36
  - [Install, update, rollback, and local development](docs/installation.md)
37
37
  - [Configuration and every provider option](docs/configuration.md)
@@ -84,7 +84,7 @@ Scoring is deliberately mechanical so releases remain comparable:
84
84
  | `session.revert.both` | **0%** | Not advertised because OMP cannot atomically rewind conversation and workspace files. |
85
85
  | `session.revert.conversation` | **100%** | Native branch-based conversation rewind, replay deduplication, ownership transfer, and failure quarantine. |
86
86
  | `session.revert.files` | **0%** | Not advertised because OMP does not expose a native file-only rewind contract. |
87
- | `session.subsession` | **100%** | Direct and nested OMP subagents become Paseo child sessions with lifecycle, ancestry, progress, and replay. |
87
+ | `session.subsession` | **100%** | Direct and nested OMP subagents become Paseo child sessions with lifecycle, ancestry, progress, and replay. Paseo's child sessions are read-only, so the provider also publishes a plugin timeline row under each `task` card that steers or stops one running child through OMP's `steer_subagent` and `cancel_subagent` (OMP 18.4.9 or newer) without stopping the parent turn. |
88
88
  | `session.unarchive` | **0%** | Not advertised; there is no native OMP archive state to reverse. |
89
89
  | `permission` | **100%** | Typed tool permissions when negotiated, with a bounded generic interaction fallback for OMP 18.1.15. Allow, deny, cancel, timeout, and interruption are covered. |
90
90
  | `permission.tool_policy` | **0%** | Not advertised. Exact MCP preapproval policy cannot be preserved through OMP `set_host_tools`, so the plugin fails closed instead of broadening access. |
@@ -0,0 +1,108 @@
1
+ import type { OmpSubagentStrip } from "../shared/subagents";
2
+
3
+ export type OmpSubagentRow = OmpSubagentStrip["subagents"][number];
4
+
5
+ /**
6
+ * One row of a subagent strip at a time owns the stop confirmation, the steering draft, or the
7
+ * pending request. The state keeps that row as last listed, so its draft stays visible after the
8
+ * subagent leaves the strip.
9
+ */
10
+ export type SubagentStripState =
11
+ | { kind: "idle" }
12
+ | { kind: "confirming-stop"; subagent: OmpSubagentRow }
13
+ | { kind: "steering"; subagent: OmpSubagentRow; draft: string }
14
+ | { kind: "stopping"; subagent: OmpSubagentRow }
15
+ | { kind: "sending"; subagent: OmpSubagentRow; draft: string };
16
+
17
+ export type SubagentStripAction =
18
+ | { type: "request-stop"; subagent: OmpSubagentRow }
19
+ | { type: "start-steer"; subagent: OmpSubagentRow }
20
+ | { type: "edit-draft"; draft: string }
21
+ | { type: "dismiss" }
22
+ | { type: "submit" }
23
+ | { type: "settled" }
24
+ | { type: "failed" };
25
+
26
+ type PendingState = Extract<SubagentStripState, { kind: "stopping" | "sending" }>;
27
+
28
+ export const INITIAL_SUBAGENT_STRIP_STATE: SubagentStripState = { kind: "idle" };
29
+
30
+ export function isSendableDraft(draft: string): boolean {
31
+ return draft.trim().length > 0;
32
+ }
33
+
34
+ export function isSubagentRequestPending(state: SubagentStripState): state is PendingState {
35
+ return state.kind === "stopping" || state.kind === "sending";
36
+ }
37
+
38
+ /** The subagent a row's controls belong to while it owns the strip, or `null`. */
39
+ export function activeSubagent(state: SubagentStripState): OmpSubagentRow | null {
40
+ return state.kind === "idle" ? null : state.subagent;
41
+ }
42
+
43
+ /**
44
+ * The request a submit starts from this state, or `null` when it starts none. A submit while a
45
+ * request is pending starts nothing, so a repeated press cannot send a second steering message.
46
+ */
47
+ export function requestedSubmission(state: SubagentStripState): PendingState | null {
48
+ if (isSubagentRequestPending(state)) return null;
49
+ const next = subagentStripReducer(state, { type: "submit" });
50
+ return isSubagentRequestPending(next) ? next : null;
51
+ }
52
+
53
+ export function subagentStripReducer(
54
+ state: SubagentStripState,
55
+ action: SubagentStripAction,
56
+ ): SubagentStripState {
57
+ if (isSubagentRequestPending(state)) return settle(state, action);
58
+ switch (action.type) {
59
+ case "request-stop":
60
+ return { kind: "confirming-stop", subagent: action.subagent };
61
+ case "start-steer":
62
+ return { kind: "steering", subagent: action.subagent, draft: "" };
63
+ case "edit-draft":
64
+ return state.kind === "steering" ? { ...state, draft: action.draft } : state;
65
+ case "dismiss":
66
+ return INITIAL_SUBAGENT_STRIP_STATE;
67
+ case "submit":
68
+ if (state.kind === "confirming-stop") return { kind: "stopping", subagent: state.subagent };
69
+ if (state.kind === "steering" && isSendableDraft(state.draft)) {
70
+ return { kind: "sending", subagent: state.subagent, draft: state.draft };
71
+ }
72
+ return state;
73
+ default:
74
+ return state;
75
+ }
76
+ }
77
+
78
+ /** A failed send reopens its draft for another try; every other outcome closes the row. */
79
+ function settle(state: PendingState, action: SubagentStripAction): SubagentStripState {
80
+ if (action.type === "settled") return INITIAL_SUBAGENT_STRIP_STATE;
81
+ if (action.type !== "failed") return state;
82
+ return state.kind === "sending"
83
+ ? { kind: "steering", subagent: state.subagent, draft: state.draft }
84
+ : INITIAL_SUBAGENT_STRIP_STATE;
85
+ }
86
+
87
+ export function stopOutcomeMessage(cancelled: boolean): string {
88
+ return cancelled ? "Subagent stopped" : "The subagent had already finished";
89
+ }
90
+
91
+ /** The host's RPC error keeps the handler's text in `userMessage`, without transport details. */
92
+ export function subagentRequestError(cause: unknown): string {
93
+ if (cause instanceof Error) {
94
+ const userMessage: unknown = Reflect.get(cause, "userMessage");
95
+ return typeof userMessage === "string" && userMessage ? userMessage : cause.message;
96
+ }
97
+ return "The OMP subagent request failed";
98
+ }
99
+
100
+ /** Compact elapsed time, such as `42s`, `3m`, or `1h 5m`. */
101
+ export function formatElapsed(milliseconds: number): string {
102
+ const seconds = Math.max(0, Math.floor(milliseconds / 1_000));
103
+ if (seconds < 60) return `${seconds}s`;
104
+ const minutes = Math.floor(seconds / 60);
105
+ if (minutes < 60) return `${minutes}m`;
106
+ const hours = Math.floor(minutes / 60);
107
+ return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`;
108
+ }
@@ -0,0 +1,391 @@
1
+ import {
2
+ type PluginHostProps,
3
+ type PluginTimelineItemProps,
4
+ useRpc,
5
+ } from "@getpaseo/plugin/client";
6
+ import { Icon, TextInput, useToast } from "@getpaseo/plugin/client/ui";
7
+ import { useEffect, useMemo, useReducer, useState } from "react";
8
+ import { Pressable, Text, View } from "react-native";
9
+ import {
10
+ cancelOmpSubagent,
11
+ OMP_SUBAGENT_STEER_MAX_LENGTH,
12
+ type OmpSubagentStrip as OmpSubagentStripData,
13
+ steerOmpSubagent,
14
+ } from "../shared/subagents";
15
+ import {
16
+ activeSubagent,
17
+ formatElapsed,
18
+ INITIAL_SUBAGENT_STRIP_STATE,
19
+ isSendableDraft,
20
+ isSubagentRequestPending,
21
+ type OmpSubagentRow,
22
+ requestedSubmission,
23
+ type SubagentStripAction,
24
+ type SubagentStripState,
25
+ stopOutcomeMessage,
26
+ subagentRequestError,
27
+ subagentStripReducer,
28
+ } from "./subagents-state";
29
+
30
+ const ROW_HEIGHT = 28;
31
+
32
+ function createStyles(theme: PluginHostProps["theme"], compact: boolean) {
33
+ return {
34
+ root: { gap: 2, paddingVertical: 2 },
35
+ row: {
36
+ flexDirection: "row" as const,
37
+ alignItems: "center" as const,
38
+ gap: 8,
39
+ height: ROW_HEIGHT,
40
+ paddingLeft: compact ? 2 : 4,
41
+ },
42
+ dot: { width: 6, height: 6, borderRadius: 3, backgroundColor: theme.colors.accent },
43
+ dotFinished: { backgroundColor: theme.colors.foregroundMuted },
44
+ agent: { color: theme.colors.foreground, fontSize: 13, flexShrink: 0 },
45
+ description: { color: theme.colors.foregroundMuted, fontSize: 13, flexShrink: 1, minWidth: 0 },
46
+ elapsed: {
47
+ color: theme.colors.foregroundMuted,
48
+ fontSize: 12,
49
+ fontVariant: ["tabular-nums" as const],
50
+ },
51
+ actions: { flexDirection: "row" as const, alignItems: "center" as const, gap: 2 },
52
+ iconButton: {
53
+ width: 24,
54
+ height: 24,
55
+ borderRadius: 6,
56
+ alignItems: "center" as const,
57
+ justifyContent: "center" as const,
58
+ },
59
+ textButton: {
60
+ height: 24,
61
+ paddingHorizontal: 8,
62
+ borderRadius: 6,
63
+ justifyContent: "center" as const,
64
+ },
65
+ hovered: { backgroundColor: theme.colors.surface2 },
66
+ disabled: { opacity: 0.5 },
67
+ stopLabel: { color: theme.colors.statusDanger, fontSize: 12 },
68
+ keepLabel: { color: theme.colors.foregroundMuted, fontSize: 12 },
69
+ tooltip: {
70
+ position: "absolute" as const,
71
+ bottom: 28,
72
+ right: 0,
73
+ paddingHorizontal: 6,
74
+ paddingVertical: 2,
75
+ borderRadius: 6,
76
+ backgroundColor: theme.colors.surface2,
77
+ borderWidth: 1,
78
+ borderColor: theme.colors.border,
79
+ },
80
+ tooltipText: { color: theme.colors.foreground, fontSize: 11 },
81
+ input: {
82
+ flexBasis: 360,
83
+ flexShrink: 1,
84
+ minWidth: 0,
85
+ height: 24,
86
+ paddingHorizontal: 8,
87
+ borderRadius: 6,
88
+ borderWidth: 1,
89
+ borderColor: theme.colors.border,
90
+ backgroundColor: theme.colors.surface1,
91
+ color: theme.colors.foreground,
92
+ fontSize: 13,
93
+ },
94
+ };
95
+ }
96
+
97
+ type Styles = ReturnType<typeof createStyles>;
98
+
99
+ /** Re-renders once per second while `active`, for elapsed times. */
100
+ function useSecondTick(active: boolean): number {
101
+ const [now, setNow] = useState(() => Date.now());
102
+ useEffect(() => {
103
+ if (!active) return;
104
+ const timer = setInterval(() => setNow(Date.now()), 1_000);
105
+ return () => clearInterval(timer);
106
+ }, [active]);
107
+ return now;
108
+ }
109
+
110
+ /**
111
+ * Running OMP subagents of one `task` call, published by the provider below its tool card. Paseo
112
+ * shows provider subagents read-only, so the plugin adds steer and stop here.
113
+ */
114
+ export function OmpSubagentStrip({
115
+ agentId,
116
+ item,
117
+ theme,
118
+ layout,
119
+ }: PluginTimelineItemProps<OmpSubagentStripData>) {
120
+ const cancel = useRpc(cancelOmpSubagent);
121
+ const steer = useRpc(steerOmpSubagent);
122
+ const toast = useToast();
123
+ const styles = useMemo(() => createStyles(theme, layout.compact), [theme, layout.compact]);
124
+ const [state, dispatch] = useReducer(subagentStripReducer, INITIAL_SUBAGENT_STRIP_STATE);
125
+ const listed = item.data.subagents;
126
+ const active = activeSubagent(state);
127
+ const unlisted = active && !listed.some((row) => row.id === active.id) ? active : null;
128
+ const now = useSecondTick(listed.length > 0);
129
+
130
+ async function submit(): Promise<void> {
131
+ const request = requestedSubmission(state);
132
+ dispatch({ type: "submit" });
133
+ if (!request) return;
134
+ try {
135
+ if (request.kind === "stopping") {
136
+ const result = await cancel({ agentId, subagentId: request.subagent.id });
137
+ toast.show(stopOutcomeMessage(result.cancelled), {
138
+ variant: result.cancelled ? "success" : "default",
139
+ });
140
+ } else {
141
+ await steer({ agentId, subagentId: request.subagent.id, message: request.draft });
142
+ toast.show("Message sent to the subagent", { variant: "success" });
143
+ }
144
+ dispatch({ type: "settled" });
145
+ } catch (cause) {
146
+ toast.error(subagentRequestError(cause));
147
+ dispatch({ type: "failed" });
148
+ }
149
+ }
150
+
151
+ if (listed.length === 0 && !unlisted) return null;
152
+ return (
153
+ <View style={styles.root}>
154
+ {listed.map((row) => (
155
+ <SubagentRow
156
+ key={row.id}
157
+ row={row}
158
+ listed
159
+ now={now}
160
+ state={state}
161
+ styles={styles}
162
+ theme={theme}
163
+ dispatch={dispatch}
164
+ submit={submit}
165
+ />
166
+ ))}
167
+ {unlisted ? (
168
+ <SubagentRow
169
+ row={unlisted}
170
+ listed={false}
171
+ now={now}
172
+ state={state}
173
+ styles={styles}
174
+ theme={theme}
175
+ dispatch={dispatch}
176
+ submit={submit}
177
+ />
178
+ ) : null}
179
+ </View>
180
+ );
181
+ }
182
+
183
+ interface RowProps {
184
+ row: OmpSubagentRow;
185
+ /** False keeps the row that owns a draft visible after its subagent leaves the strip. */
186
+ listed: boolean;
187
+ now: number;
188
+ state: SubagentStripState;
189
+ styles: Styles;
190
+ theme: PluginHostProps["theme"];
191
+ dispatch: (action: SubagentStripAction) => void;
192
+ submit(): Promise<void>;
193
+ }
194
+
195
+ function SubagentRow(props: RowProps) {
196
+ const { row, listed, now, state, styles } = props;
197
+ const owner = activeSubagent(state)?.id === row.id ? state : null;
198
+ const steering = owner?.kind === "steering" || owner?.kind === "sending";
199
+ return (
200
+ <View style={styles.row}>
201
+ <View style={[styles.dot, listed ? null : styles.dotFinished]} />
202
+ <Text numberOfLines={1} style={styles.agent}>
203
+ {row.agent}
204
+ </Text>
205
+ {steering ? (
206
+ <SteerControls {...props} />
207
+ ) : (
208
+ <>
209
+ <Text numberOfLines={1} style={styles.description}>
210
+ {row.description ?? ""}
211
+ </Text>
212
+ {owner?.kind === "confirming-stop" || owner?.kind === "stopping" ? (
213
+ <StopConfirmation {...props} />
214
+ ) : (
215
+ <>
216
+ <Text style={styles.elapsed}>
217
+ {listed ? formatElapsed(now - row.runningSince) : "finished"}
218
+ </Text>
219
+ {listed ? <RowActions {...props} /> : null}
220
+ </>
221
+ )}
222
+ </>
223
+ )}
224
+ </View>
225
+ );
226
+ }
227
+
228
+ function RowActions({ row, state, styles, theme, dispatch }: RowProps) {
229
+ const pending = isSubagentRequestPending(state);
230
+ return (
231
+ <View style={styles.actions}>
232
+ <IconButton
233
+ icon="MessageSquare"
234
+ label="Steer: send this subagent a message at its next step"
235
+ disabled={pending}
236
+ styles={styles}
237
+ theme={theme}
238
+ onPress={() => dispatch({ type: "start-steer", subagent: row })}
239
+ />
240
+ <IconButton
241
+ icon="CircleStop"
242
+ label="Stop this subagent and its nested subagents; the parent keeps running"
243
+ disabled={pending}
244
+ styles={styles}
245
+ theme={theme}
246
+ onPress={() => dispatch({ type: "request-stop", subagent: row })}
247
+ />
248
+ </View>
249
+ );
250
+ }
251
+
252
+ function StopConfirmation({ state, styles, dispatch, submit }: RowProps) {
253
+ const stopping = state.kind === "stopping";
254
+ return (
255
+ <View style={styles.actions}>
256
+ <TextButton
257
+ label={stopping ? "Stopping..." : "Stop"}
258
+ accessibilityLabel="Confirm stopping this subagent"
259
+ labelStyle={styles.stopLabel}
260
+ disabled={stopping}
261
+ styles={styles}
262
+ onPress={() => void submit()}
263
+ />
264
+ <TextButton
265
+ label="Keep"
266
+ accessibilityLabel="Keep this subagent running"
267
+ labelStyle={styles.keepLabel}
268
+ disabled={stopping}
269
+ styles={styles}
270
+ onPress={() => dispatch({ type: "dismiss" })}
271
+ />
272
+ </View>
273
+ );
274
+ }
275
+
276
+ function SteerControls({ row, state, styles, theme, dispatch, submit }: RowProps) {
277
+ const sending = state.kind === "sending";
278
+ const draft = state.kind === "steering" || state.kind === "sending" ? state.draft : "";
279
+ return (
280
+ <>
281
+ <TextInput
282
+ accessibilityLabel={`Message for ${row.agent}`}
283
+ autoFocus
284
+ editable={!sending}
285
+ maxLength={OMP_SUBAGENT_STEER_MAX_LENGTH}
286
+ onChangeText={(text) => dispatch({ type: "edit-draft", draft: text })}
287
+ onKeyPress={(event) => {
288
+ if (event.nativeEvent.key === "Escape") dispatch({ type: "dismiss" });
289
+ }}
290
+ onSubmitEditing={() => void submit()}
291
+ placeholder="Message the subagent"
292
+ placeholderTextColor={theme.colors.foregroundMuted}
293
+ style={styles.input}
294
+ value={draft}
295
+ />
296
+ <View style={styles.actions}>
297
+ <IconButton
298
+ icon="SendHorizontal"
299
+ label="Send to the subagent (Enter)"
300
+ disabled={sending || !isSendableDraft(draft)}
301
+ styles={styles}
302
+ theme={theme}
303
+ onPress={() => void submit()}
304
+ />
305
+ <IconButton
306
+ icon="X"
307
+ label="Close (Esc)"
308
+ disabled={sending}
309
+ styles={styles}
310
+ theme={theme}
311
+ onPress={() => dispatch({ type: "dismiss" })}
312
+ />
313
+ </View>
314
+ </>
315
+ );
316
+ }
317
+
318
+ function IconButton({
319
+ icon,
320
+ label,
321
+ disabled,
322
+ styles,
323
+ theme,
324
+ onPress,
325
+ }: {
326
+ icon: string;
327
+ label: string;
328
+ disabled: boolean;
329
+ styles: Styles;
330
+ theme: PluginHostProps["theme"];
331
+ onPress(): void;
332
+ }) {
333
+ const [hovered, setHovered] = useState(false);
334
+ return (
335
+ <View>
336
+ <Pressable
337
+ accessibilityRole="button"
338
+ accessibilityLabel={label}
339
+ disabled={disabled}
340
+ onHoverIn={() => setHovered(true)}
341
+ onHoverOut={() => setHovered(false)}
342
+ onPress={onPress}
343
+ style={[
344
+ styles.iconButton,
345
+ hovered && !disabled ? styles.hovered : null,
346
+ disabled ? styles.disabled : null,
347
+ ]}
348
+ >
349
+ <Icon name={icon} size={14} color={theme.colors.foregroundMuted} />
350
+ </Pressable>
351
+ {hovered && !disabled ? (
352
+ <View pointerEvents="none" style={styles.tooltip}>
353
+ <Text numberOfLines={1} style={styles.tooltipText}>
354
+ {label}
355
+ </Text>
356
+ </View>
357
+ ) : null}
358
+ </View>
359
+ );
360
+ }
361
+
362
+ function TextButton({
363
+ label,
364
+ accessibilityLabel,
365
+ labelStyle,
366
+ disabled,
367
+ styles,
368
+ onPress,
369
+ }: {
370
+ label: string;
371
+ accessibilityLabel: string;
372
+ labelStyle: Styles["stopLabel"];
373
+ disabled: boolean;
374
+ styles: Styles;
375
+ onPress(): void;
376
+ }) {
377
+ const [hovered, setHovered] = useState(false);
378
+ return (
379
+ <Pressable
380
+ accessibilityRole="button"
381
+ accessibilityLabel={accessibilityLabel}
382
+ disabled={disabled}
383
+ onHoverIn={() => setHovered(true)}
384
+ onHoverOut={() => setHovered(false)}
385
+ onPress={onPress}
386
+ style={[styles.textButton, hovered && !disabled ? styles.hovered : null]}
387
+ >
388
+ <Text style={labelStyle}>{label}</Text>
389
+ </Pressable>
390
+ );
391
+ }
package/index.client.tsx CHANGED
@@ -38,6 +38,7 @@ import {
38
38
  HUB_SIDEBAR_ITEM_ID,
39
39
  loadHubSnapshot,
40
40
  } from "./client/sidebar-compat";
41
+ import { OmpSubagentStrip } from "./client/subagents-strip";
41
42
  import {
42
43
  type ComposerPillSettings,
43
44
  composerPillSettings,
@@ -48,6 +49,7 @@ import { OMP_MCP_AUTH_TIMELINE_KIND, ompMcpAuthorizationTimelineSchema } from ".
48
49
  import { type OmpStore, storeForProvider } from "./shared/omp-store";
49
50
  import { ompImageTimelineSchema, transformOmpImageToolItem } from "./shared/provider-image";
50
51
  import { listOmpQuotas } from "./shared/quota";
52
+ import { OMP_SUBAGENT_STRIP_KIND, ompSubagentStripSchema } from "./shared/subagents";
51
53
 
52
54
  const PAGE_LIMIT = 200;
53
55
  const MAX_PAGES = 10;
@@ -237,6 +239,12 @@ export default function contribute(client: PluginClientContext) {
237
239
  schema: ompMcpAuthorizationTimelineSchema,
238
240
  Component: OmpMcpAuthorizationCard,
239
241
  });
242
+ const removeSubagentStripRenderer = client.addTimelineRenderer({
243
+ kind: OMP_SUBAGENT_STRIP_KIND,
244
+ version: 1,
245
+ schema: ompSubagentStripSchema,
246
+ Component: OmpSubagentStrip,
247
+ });
240
248
  const removeImageTransformer = client.addTimelineTransformer({
241
249
  id: "omp-images",
242
250
  query: { itemType: "tool_call" },
@@ -544,6 +552,7 @@ export default function contribute(client: PluginClientContext) {
544
552
  pills.clear();
545
553
  removeImageRenderer();
546
554
  removeMcpAuthorizationRenderer();
555
+ removeSubagentStripRenderer();
547
556
  removeImageTransformer();
548
557
  removeOpenConfig();
549
558
  removeConfigEntry();
package/index.server.ts CHANGED
@@ -29,6 +29,11 @@ import { OmpSessionLimit } from "./server/provider/session-limit";
29
29
  import { resolveGetOmpProviderHealth } from "./server/provider-diagnostics";
30
30
  import { resolveListOmpQuotas } from "./server/quota";
31
31
  import { resolveListOmpSessions } from "./server/sessions";
32
+ import {
33
+ OmpSubagentControlRegistry,
34
+ resolveCancelOmpSubagent,
35
+ resolveSteerOmpSubagent,
36
+ } from "./server/subagents";
32
37
  import { resolveGetOmpSupportReport } from "./server/support-diagnostics";
33
38
  import { composerPillSettings } from "./shared/composer-pill-settings";
34
39
  import { listHubProcesses, tailHubLog } from "./shared/hub";
@@ -48,6 +53,7 @@ import { getOmpProviderHealth } from "./shared/provider-diagnostics";
48
53
  import { providerLaunchSettings } from "./shared/provider-launch-settings";
49
54
  import { listOmpQuotas } from "./shared/quota";
50
55
  import { listOmpSessions } from "./shared/sessions";
56
+ import { cancelOmpSubagent, steerOmpSubagent } from "./shared/subagents";
51
57
  import { getOmpSupportReport } from "./shared/support-diagnostics";
52
58
 
53
59
  function scoped<T extends { store?: OmpStore }, R>(handler: (input: T) => R) {
@@ -68,6 +74,7 @@ export default function contribute(server: PluginServerContext) {
68
74
  return current.status === "ready" ? current.values.maxSessions : undefined;
69
75
  });
70
76
  const removeSessionLimitListener = launchSettings.subscribe(() => sessionLimit.refresh());
77
+ const subagentControlRegistry = new OmpSubagentControlRegistry();
71
78
  const protocolViolations = new OmpProtocolViolationCollector();
72
79
  const operationalFailures = new OmpOperationalFailureCollector();
73
80
  const profiles = discoverOmpProfilesSync();
@@ -77,6 +84,7 @@ export default function contribute(server: PluginServerContext) {
77
84
  createProfileOmpProvider(profile, {
78
85
  browserAuthorizationRegistry,
79
86
  sessionLimit,
87
+ subagentControlRegistry,
80
88
  reportProtocolViolation: protocolViolations.report,
81
89
  reportOperationalFailure: operationalFailures.report,
82
90
  resolveHostInheritEnv,
@@ -104,6 +112,12 @@ export default function contribute(server: PluginServerContext) {
104
112
  server.handle(openOmpMcpAuthorizationInPaseoBrowser, (input) =>
105
113
  resolveOpenOmpMcpAuthorizationInPaseoBrowser(input, browserAuthorizationRegistry),
106
114
  );
115
+ server.handle(cancelOmpSubagent, (input) =>
116
+ resolveCancelOmpSubagent(input, subagentControlRegistry),
117
+ );
118
+ server.handle(steerOmpSubagent, (input) =>
119
+ resolveSteerOmpSubagent(input, subagentControlRegistry),
120
+ );
107
121
  const removeSessionOpenHook = server.before("agent.session_open", ({ request }) => {
108
122
  if (request.provider !== "omp-plugin" && !request.provider.startsWith("omp-plugin-")) return;
109
123
  return withOmpSessionOpenEnv(request);
@@ -112,6 +126,7 @@ export default function contribute(server: PluginServerContext) {
112
126
  createOmpProvider({
113
127
  browserAuthorizationRegistry,
114
128
  sessionLimit,
129
+ subagentControlRegistry,
115
130
  reportProtocolViolation: protocolViolations.report,
116
131
  reportOperationalFailure: operationalFailures.report,
117
132
  resolveHostInheritEnv,
@@ -120,6 +135,7 @@ export default function contribute(server: PluginServerContext) {
120
135
  return () => {
121
136
  browserAuthorizationRegistry.clear();
122
137
  removeSessionLimitListener();
138
+ subagentControlRegistry.clear();
123
139
  removeSessionOpenHook();
124
140
  };
125
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omercnet/paseo-omp",
3
- "version": "5.1.1-next.336.1",
3
+ "version": "5.1.1-next.340.1",
4
4
  "type": "module",
5
5
  "description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
6
6
  "license": "MIT",
@@ -64,8 +64,8 @@
64
64
  "@getpaseo/client": "0.14.0",
65
65
  "@getpaseo/plugin": "0.14.0",
66
66
  "@getpaseo/server": "0.14.0",
67
- "@oh-my-pi/pi-agent-core": "18.12.6",
68
- "@oh-my-pi/pi-coding-agent": "18.12.6",
67
+ "@oh-my-pi/pi-agent-core": "18.14.0",
68
+ "@oh-my-pi/pi-coding-agent": "18.14.0",
69
69
  "@tanstack/react-query": "^5.102.3",
70
70
  "@types/node": "^24.10.1",
71
71
  "@types/react": "^19.2.18",
@@ -1,2 +1,2 @@
1
1
  /** Release Please keeps this bundle-safe constant aligned with package.json. */
2
- export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.336.1"; // x-release-please-version
2
+ export const PASEO_OMP_PACKAGE_VERSION = "5.1.1-next.340.1"; // x-release-please-version