@subshell-ai/plugin-oh-my-pi 0.2.3 → 0.3.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/README.md +2 -2
- package/dist/index.d.ts +117 -4
- package/dist/index.js +264 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
`@subshell-ai/plugin-oh-my-pi` launches the `omp` CLI from Subshell. It provides
|
|
4
4
|
model, provider, and thinking settings, automatic per-launch MCP
|
|
5
|
-
configuration, and sidebar subagent monitoring. Install and authenticate Oh My
|
|
5
|
+
configuration, and sidebar subagent monitoring with view switching. Install and authenticate Oh My
|
|
6
6
|
Pi separately on each node where you want to run it.
|
|
7
7
|
|
|
8
8
|
See the [Oh My Pi guide](https://docs.subshell.sh/agents/oh-my-pi) for setup
|
|
9
|
-
and supported behavior. The integration is verified with Oh My Pi 18.
|
|
9
|
+
and supported behavior. The integration is verified with Oh My Pi 18.18.7.
|
|
10
10
|
Automatic conversation resumption and attention hooks aren't supported.
|
|
11
11
|
|
|
12
12
|
Licensed under Apache-2.0.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,104 @@
|
|
|
1
|
-
//#region ../../plugin-api/dist/types-
|
|
1
|
+
//#region ../../plugin-api/dist/types-D2qRfuN_.d.ts
|
|
2
|
+
//#region src/previews.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* How a harness reports loopback ports its own tooling started, for the
|
|
5
|
+
* preview picker.
|
|
6
|
+
*
|
|
7
|
+
* Port discovery attributes sockets by walking the pane's process tree. A
|
|
8
|
+
* harness that brokers its tool shells through a shared daemon (oh-my-pi
|
|
9
|
+
* does) leaves that tree, so a server its tools start is reachable but
|
|
10
|
+
* invisible. A plugin that declares the `previews` capability ships a
|
|
11
|
+
* reporter the control plane can list those ports from.
|
|
12
|
+
*
|
|
13
|
+
* The report carries ports only, never a host: the node dials loopback, so a
|
|
14
|
+
* report cannot point the proxy at another machine.
|
|
15
|
+
*/
|
|
16
|
+
type HarnessPreviewReporting = "extension" | "hooks";
|
|
17
|
+
/**
|
|
18
|
+
* What a harness declares about its preview reporting. The rules mirror
|
|
19
|
+
* {@link HarnessSubagentSupport}: `reporting` is checked against the union at
|
|
20
|
+
* load, and `setup` is the copy the preset editor shows when a human act is
|
|
21
|
+
* needed before reports start.
|
|
22
|
+
*/
|
|
23
|
+
interface HarnessPreviews {
|
|
24
|
+
/** How reports reach the plane: a per-launch extension, or harness lifecycle hooks. */
|
|
25
|
+
reporting: HarnessPreviewReporting;
|
|
26
|
+
/** One-time human action required for reporting to work, if any. */
|
|
27
|
+
setup?: string;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/subagents.d.ts
|
|
31
|
+
/** What a reported agent is doing, as the harness itself says. */
|
|
32
|
+
type HarnessSubagentStatus = "running" | "waiting" | "idle";
|
|
33
|
+
/** A harness agent belongs to an existing terminal, never to a new subshell. */
|
|
34
|
+
interface HarnessSubagent {
|
|
35
|
+
/** Harness-side identifier; unique among this parent's current children */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Parent agent within the harness, when the harness nests */
|
|
38
|
+
parentId?: string;
|
|
39
|
+
/** Display name as the harness reports it; generic ones render numbered */
|
|
40
|
+
name: string;
|
|
41
|
+
/** What the harness says this agent is doing */
|
|
42
|
+
status: HarnessSubagentStatus;
|
|
43
|
+
/**
|
|
44
|
+
* The harness's own one-line label for what this child was asked to do,
|
|
45
|
+
* when the harness has one to give. The host bounds it and normalizes it
|
|
46
|
+
* like a name; it is the harness's summary of the task, never the prompt
|
|
47
|
+
* text or anything read from the child's transcript.
|
|
48
|
+
*/
|
|
49
|
+
activity?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A reported child as the control plane SERVES it: what the harness said, plus
|
|
53
|
+
* the server's own observation of when the child appeared. Viewers derive
|
|
54
|
+
* elapsed time from that stamp against their own clock, so a roster that
|
|
55
|
+
* stopped being polled ages honestly instead of freezing at its last read.
|
|
56
|
+
*/
|
|
57
|
+
interface HarnessSubagentView extends HarnessSubagent {
|
|
58
|
+
/** Epoch milliseconds of when the server first saw this child; never sent by a harness */
|
|
59
|
+
startedAt: number;
|
|
60
|
+
}
|
|
2
61
|
/** How the plugin learns about children: harness lifecycle hooks, or a loaded extension. */
|
|
3
62
|
type HarnessSubagentDiscovery = "hooks" | "extension";
|
|
4
63
|
/** Whether the sidebar can move the parent's own view to a child, or the harness keeps that in-app. */
|
|
5
64
|
type HarnessSubagentNavigation = "native" | "unavailable";
|
|
65
|
+
/**
|
|
66
|
+
* Pane operations the host hands an integration that drives its harness's
|
|
67
|
+
* own agent picker. The plugin runs in the control plane, so it cannot open a
|
|
68
|
+
* terminal itself; these are the only two acts it needs, and neither submits
|
|
69
|
+
* a prompt on its own.
|
|
70
|
+
*/
|
|
71
|
+
interface HarnessSubagentPane {
|
|
72
|
+
/** The parent pane's current screen with ANSI stripped, exactly as painted. */
|
|
73
|
+
read(): Promise<string>;
|
|
74
|
+
/**
|
|
75
|
+
* Type raw bytes into the pane's own input. Nothing is appended or
|
|
76
|
+
* translated: send `\r` for Enter, `\u001b[1;3D` for Alt+Left, and so on.
|
|
77
|
+
*/
|
|
78
|
+
type(text: string): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
/** What the host hands an integration's {@link HarnessSubagentSupport.select}. */
|
|
81
|
+
interface HarnessSubagentSelectInput {
|
|
82
|
+
/** The child to show, or null to return the parent itself */
|
|
83
|
+
agentId: string | null;
|
|
84
|
+
/** The roster the child came from, server-stamped and bounded */
|
|
85
|
+
agents: readonly HarnessSubagentView[];
|
|
86
|
+
/** The parent terminal this selection must happen in */
|
|
87
|
+
pane: HarnessSubagentPane;
|
|
88
|
+
}
|
|
89
|
+
/** What the integration must report back after the harness actually switched. */
|
|
90
|
+
interface HarnessSubagentSelectResult {
|
|
91
|
+
/** The view the harness showed when the act finished; null for the parent */
|
|
92
|
+
selectedId: string | null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* An integration's own way to move its terminal's view. This is for harnesses
|
|
96
|
+
* whose picker or inspector exists only inside their TUI (Codex's `/subagents`,
|
|
97
|
+
* Claude's agent panel, pi's fleet inspector): the integration reads the pane,
|
|
98
|
+
* sends the harness's own keys, and reports the view it verified. Extensions
|
|
99
|
+
* without a pane driver keep using the report-loop command.
|
|
100
|
+
*/
|
|
101
|
+
type HarnessSubagentSelect = (input: HarnessSubagentSelectInput) => Promise<HarnessSubagentSelectResult>;
|
|
6
102
|
/**
|
|
7
103
|
* What a plugin declares about its harness's child agents. The host uses
|
|
8
104
|
* `discovery`/`navigation` to decide what the sidebar may offer; `setup`, when
|
|
@@ -16,6 +112,13 @@ interface HarnessSubagentSupport {
|
|
|
16
112
|
navigation: HarnessSubagentNavigation;
|
|
17
113
|
/** One-time user action required for reporting to work, if any */
|
|
18
114
|
setup?: string;
|
|
115
|
+
/**
|
|
116
|
+
* The integration's own pane driver, when it has one. Requires
|
|
117
|
+
* `navigation: "native"`; the host uses it instead of queueing a command.
|
|
118
|
+
* Works for hook and extension discovery alike: the host only needs the
|
|
119
|
+
* pane's read/type, and the edit gate is the same.
|
|
120
|
+
*/
|
|
121
|
+
select?: HarnessSubagentSelect;
|
|
19
122
|
}
|
|
20
123
|
//#endregion
|
|
21
124
|
//#region src/types.d.ts
|
|
@@ -43,7 +146,7 @@ type PluginType = "agent-harness" | "terminal" | "network";
|
|
|
43
146
|
* and a network plugin claiming `resume` are both refused at load, for the
|
|
44
147
|
* same reason a claimed-but-unimplemented capability is.
|
|
45
148
|
*/
|
|
46
|
-
type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "publish" | "supervise" | "guard" | "settings";
|
|
149
|
+
type PluginCapability = "mcp" | "resume" | "attention" | "subagents" | "previews" | "publish" | "supervise" | "guard" | "settings";
|
|
47
150
|
/** A harness preset as defined by the user (decoded JSON blobs). */
|
|
48
151
|
interface PresetDefinition {
|
|
49
152
|
/** Human-friendly preset name */
|
|
@@ -380,6 +483,13 @@ interface PluginSecrets {
|
|
|
380
483
|
interface SubshellPlugin {
|
|
381
484
|
/** Native child-agent reporting and optional in-place navigation. */
|
|
382
485
|
subagents?: HarnessSubagentSupport;
|
|
486
|
+
/**
|
|
487
|
+
* Native preview-port reporting: the harness tells the plane which loopback
|
|
488
|
+
* ports its tooling serves, so the picker lists servers discovery cannot
|
|
489
|
+
* attribute to the pane's process tree. Declare the `previews` capability
|
|
490
|
+
* with it.
|
|
491
|
+
*/
|
|
492
|
+
previews?: HarnessPreviews;
|
|
383
493
|
/** Builds the argv (no shell) used to launch a subshell. */
|
|
384
494
|
buildCommand(input: BuildCommandInput): string[];
|
|
385
495
|
/** Validates a preset definition before saving. */
|
|
@@ -703,8 +813,11 @@ declare const manifest: SubshellManifest;
|
|
|
703
813
|
*
|
|
704
814
|
* The same directory carries the subagent bridge. omp keeps its child roster
|
|
705
815
|
* on the session event bus; the extension reports it so the sidebar can hang
|
|
706
|
-
* those children under the parent subshell. omp
|
|
707
|
-
* moving the parent's terminal onto a child, so
|
|
816
|
+
* those children under the parent subshell. omp has no extension hook for
|
|
817
|
+
* moving the parent's terminal onto a child yet, so the bridge reaches the
|
|
818
|
+
* TUI's own public focus method through the class omp exposes as
|
|
819
|
+
* pi.pi.InteractiveMode (verified against 18.18.7). If that surface goes
|
|
820
|
+
* away, the bridge falls back to status-only reporting.
|
|
708
821
|
*/
|
|
709
822
|
declare const createPlugin: HarnessPluginFactory;
|
|
710
823
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region ../../plugin-api/dist/manifest-
|
|
1
|
+
//#region ../../plugin-api/dist/manifest-CrULrf80.js
|
|
2
2
|
/** Every {@link PluginType}, for anything that has to iterate or validate them. */
|
|
3
3
|
const PLUGIN_TYPES = [
|
|
4
4
|
"agent-harness",
|
|
@@ -287,6 +287,158 @@ function validateGenericPreset(preset) {
|
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
289
|
//#endregion
|
|
290
|
+
//#region src/previews.ts
|
|
291
|
+
/**
|
|
292
|
+
* omp brokers tool shells through its own daemon, so servers those tools
|
|
293
|
+
* start live outside the pane's process tree and port discovery cannot see
|
|
294
|
+
* them. This extension watches tool results for loopback URLs and reports
|
|
295
|
+
* the ports to the control plane, where the preview picker lists them.
|
|
296
|
+
*
|
|
297
|
+
* It deliberately reports what the output says, not a scan: a tool result
|
|
298
|
+
* from something that executes (bash or a custom tool) is the harness's own
|
|
299
|
+
* evidence that it started something, so file-reading tools are skipped. The
|
|
300
|
+
* report is port-only (the node dials loopback), bounded, and coalesced. A
|
|
301
|
+
* port the harness has not mentioned for ten minutes stops being refreshed,
|
|
302
|
+
* and the server's TTL retires it. A pane without the identity environment
|
|
303
|
+
* has no endpoint and stays quiet.
|
|
304
|
+
*/
|
|
305
|
+
const PREVIEW_BRIDGE = `
|
|
306
|
+
export default function (pi) {
|
|
307
|
+
const REPORT_DELAY_MS = 750;
|
|
308
|
+
const HEARTBEAT_MS = 60_000;
|
|
309
|
+
const FRESH_MS = 10 * 60_000;
|
|
310
|
+
const MAX_PORTS = 32;
|
|
311
|
+
const MAX_TITLE = 120;
|
|
312
|
+
const MAX_SCAN = 64 * 1024;
|
|
313
|
+
const SKIP_TOOLS = { read: true, edit: true, write: true, grep: true, glob: true };
|
|
314
|
+
const URL_RE = /(?:https?:\\/\\/)?(?:localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\]):(\\d{1,5})(?!\\d)/gi;
|
|
315
|
+
|
|
316
|
+
const ports = new Map();
|
|
317
|
+
let timer = null;
|
|
318
|
+
let heartbeat = null;
|
|
319
|
+
let reporting = false;
|
|
320
|
+
let queued = false;
|
|
321
|
+
let warned = false;
|
|
322
|
+
let disabled = false;
|
|
323
|
+
const prune = now => {
|
|
324
|
+
for (const [port, entry] of ports) {
|
|
325
|
+
if (now - entry.seenAt > FRESH_MS) ports.delete(port);
|
|
326
|
+
}
|
|
327
|
+
if (ports.size === 0 && heartbeat !== null) {
|
|
328
|
+
clearInterval(heartbeat);
|
|
329
|
+
heartbeat = null;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const remember = (port, title) => {
|
|
333
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return;
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
prune(now);
|
|
336
|
+
const previous = ports.get(port);
|
|
337
|
+
if (previous) {
|
|
338
|
+
previous.seenAt = now;
|
|
339
|
+
if (title && !previous.title) previous.title = title;
|
|
340
|
+
} else {
|
|
341
|
+
if (ports.size >= MAX_PORTS) return;
|
|
342
|
+
ports.set(port, { title, seenAt: now });
|
|
343
|
+
}
|
|
344
|
+
if (heartbeat === null) heartbeat = setInterval(beat, HEARTBEAT_MS);
|
|
345
|
+
schedule();
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const scan = text => {
|
|
349
|
+
const bounded = text.slice(0, MAX_SCAN);
|
|
350
|
+
URL_RE.lastIndex = 0;
|
|
351
|
+
let match;
|
|
352
|
+
while ((match = URL_RE.exec(bounded))) {
|
|
353
|
+
const port = Number(match[1]);
|
|
354
|
+
const start = bounded.lastIndexOf('\\n', match.index) + 1;
|
|
355
|
+
const end = bounded.indexOf('\\n', match.index);
|
|
356
|
+
const line = bounded.slice(start, end === -1 ? bounded.length : end).trim();
|
|
357
|
+
remember(port, line ? line.slice(0, MAX_TITLE) : undefined);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const report = async () => {
|
|
362
|
+
if (disabled || ports.size === 0) return;
|
|
363
|
+
if (reporting) {
|
|
364
|
+
queued = true;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const base = process.env.SUBSHELL_BASE_URL;
|
|
368
|
+
const token = process.env.SUBSHELL_API_KEY;
|
|
369
|
+
const id = process.env.SUBSHELL_ID;
|
|
370
|
+
if (!base || !token || !id) {
|
|
371
|
+
if (!warned) {
|
|
372
|
+
warned = true;
|
|
373
|
+
pi.logger?.debug?.('subshell preview report skipped: the pane identity environment is missing');
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
prune(Date.now());
|
|
378
|
+
if (ports.size === 0) return;
|
|
379
|
+
reporting = true;
|
|
380
|
+
try {
|
|
381
|
+
const response = await fetch(base + '/api/subshells/' + encodeURIComponent(id) + '/views/report', {
|
|
382
|
+
method: 'POST',
|
|
383
|
+
headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
|
|
384
|
+
body: JSON.stringify({
|
|
385
|
+
ports: [...ports.entries()].map(([port, entry]) => ({
|
|
386
|
+
port,
|
|
387
|
+
...(entry.title ? { title: entry.title } : {}),
|
|
388
|
+
})),
|
|
389
|
+
}),
|
|
390
|
+
signal: AbortSignal.timeout(2000),
|
|
391
|
+
});
|
|
392
|
+
if (response.status === 404 || response.status === 401) {
|
|
393
|
+
// The server either predates the route or no longer accepts this
|
|
394
|
+
// pane's key. Retrying every heartbeat cannot fix either, so stop
|
|
395
|
+
// until a restart hands the extension a new process.
|
|
396
|
+
disabled = true;
|
|
397
|
+
if (heartbeat !== null) {
|
|
398
|
+
clearInterval(heartbeat);
|
|
399
|
+
heartbeat = null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!response.ok && !warned) {
|
|
403
|
+
warned = true;
|
|
404
|
+
pi.logger?.debug?.('subshell preview report failed', { status: response.status });
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
// A failed report is not fatal: the next tool result reports again.
|
|
408
|
+
} finally {
|
|
409
|
+
reporting = false;
|
|
410
|
+
if (queued) {
|
|
411
|
+
queued = false;
|
|
412
|
+
void report();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const schedule = () => {
|
|
418
|
+
if (timer !== null) return;
|
|
419
|
+
timer = setTimeout(() => {
|
|
420
|
+
timer = null;
|
|
421
|
+
void report();
|
|
422
|
+
}, REPORT_DELAY_MS);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const beat = () => {
|
|
426
|
+
prune(Date.now());
|
|
427
|
+
if (ports.size > 0) void report();
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
pi.on('tool_result', event => {
|
|
431
|
+
if (!event || event.isError) return;
|
|
432
|
+
const name = typeof event.toolName === 'string' ? event.toolName : '';
|
|
433
|
+
if (SKIP_TOOLS[name] === true) return;
|
|
434
|
+
const content = Array.isArray(event.content) ? event.content : [];
|
|
435
|
+
for (const block of content) {
|
|
436
|
+
if (block && block.type === 'text' && typeof block.text === 'string') scan(block.text);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
`;
|
|
441
|
+
//#endregion
|
|
290
442
|
//#region src/subagents.ts
|
|
291
443
|
/**
|
|
292
444
|
* omp's session event bus already carries the roster the task tool is running:
|
|
@@ -329,6 +481,19 @@ export default function (pi) {
|
|
|
329
481
|
let queued = false;
|
|
330
482
|
let heartbeat = null;
|
|
331
483
|
let warned = false;
|
|
484
|
+
// omp has no extension hook for moving the TUI's view to a child. The class
|
|
485
|
+
// that owns the move is exposed to extensions as pi.pi.InteractiveMode, so
|
|
486
|
+
// capture the live instance through a property it reads while rendering and
|
|
487
|
+
// call its public focusAgentSession/unfocusSession. Restoring the getter on
|
|
488
|
+
// first read leaves the host untouched afterwards. Verified against omp
|
|
489
|
+
// 18.18.7; if the class or property disappears, the rows stay status-only
|
|
490
|
+
// instead of failing the report.
|
|
491
|
+
let mode = null;
|
|
492
|
+
let probing = false;
|
|
493
|
+
let switchWarned = false;
|
|
494
|
+
let focusing = false;
|
|
495
|
+
let acknowledged;
|
|
496
|
+
let declined;
|
|
332
497
|
const label = value => {
|
|
333
498
|
if (typeof value !== 'string') return undefined;
|
|
334
499
|
const trimmed = value.trim().slice(0, MAX_TEXT);
|
|
@@ -359,6 +524,35 @@ export default function (pi) {
|
|
|
359
524
|
return key ? agents.delete(key) : false;
|
|
360
525
|
};
|
|
361
526
|
|
|
527
|
+
const captureMode = () => {
|
|
528
|
+
if (probing || mode) return;
|
|
529
|
+
const type = pi.pi && pi.pi.InteractiveMode;
|
|
530
|
+
const proto = typeof type === 'function' ? type.prototype : undefined;
|
|
531
|
+
const descriptor = proto && Object.getOwnPropertyDescriptor(proto, 'viewSession');
|
|
532
|
+
if (!descriptor || typeof descriptor.get !== 'function') return;
|
|
533
|
+
probing = true;
|
|
534
|
+
Object.defineProperty(proto, 'viewSession', {
|
|
535
|
+
configurable: true,
|
|
536
|
+
get() {
|
|
537
|
+
mode = this;
|
|
538
|
+
Object.defineProperty(proto, 'viewSession', descriptor);
|
|
539
|
+
return descriptor.get.call(this);
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const canSelect = () =>
|
|
545
|
+
role === 'main' &&
|
|
546
|
+
mode !== null &&
|
|
547
|
+
typeof mode.focusAgentSession === 'function' &&
|
|
548
|
+
typeof mode.unfocusSession === 'function' &&
|
|
549
|
+
!mode.collabGuest;
|
|
550
|
+
const selectedId = () => {
|
|
551
|
+
if (!mode) return null;
|
|
552
|
+
const id = mode.focusedAgentId;
|
|
553
|
+
return typeof id === 'string' && agents.has(id) ? id : null;
|
|
554
|
+
};
|
|
555
|
+
|
|
362
556
|
const report = async () => {
|
|
363
557
|
if (reporting) {
|
|
364
558
|
queued = true;
|
|
@@ -376,20 +570,66 @@ export default function (pi) {
|
|
|
376
570
|
}
|
|
377
571
|
reporting = true;
|
|
378
572
|
try {
|
|
573
|
+
const selectable = canSelect();
|
|
574
|
+
const payload = {
|
|
575
|
+
event: 'snapshot',
|
|
576
|
+
agents: [...agents.values()].slice(0, MAX_AGENTS),
|
|
577
|
+
selectedId: selectedId(),
|
|
578
|
+
canSelect: selectable,
|
|
579
|
+
};
|
|
580
|
+
if (acknowledged !== undefined) payload.acknowledged = acknowledged;
|
|
581
|
+
if (!selectable) {
|
|
582
|
+
payload.unavailableReason =
|
|
583
|
+
mode && mode.collabGuest
|
|
584
|
+
? 'Viewing agents is unavailable in a collab session.'
|
|
585
|
+
: 'This Oh My Pi build has no agent view to switch. Use its own agent selector.';
|
|
586
|
+
}
|
|
379
587
|
const response = await fetch(base + '/api/subshells/' + encodeURIComponent(id) + '/subagents/report', {
|
|
380
588
|
method: 'POST',
|
|
381
589
|
headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
|
|
382
|
-
body: JSON.stringify(
|
|
383
|
-
event: 'snapshot',
|
|
384
|
-
agents: [...agents.values()].slice(0, MAX_AGENTS),
|
|
385
|
-
selectedId: null,
|
|
386
|
-
canSelect: false,
|
|
387
|
-
}),
|
|
590
|
+
body: JSON.stringify(payload),
|
|
388
591
|
signal: AbortSignal.timeout(2000),
|
|
389
592
|
});
|
|
390
|
-
if (!response.ok
|
|
391
|
-
warned
|
|
392
|
-
|
|
593
|
+
if (!response.ok) {
|
|
594
|
+
if (!warned) {
|
|
595
|
+
warned = true;
|
|
596
|
+
pi.logger?.debug?.('subshell subagent report failed', { status: response.status });
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const data = await response.json().catch(() => null);
|
|
601
|
+
const command = data && data.command;
|
|
602
|
+
// The same command comes back until it is acknowledged, and a refused
|
|
603
|
+
// one is skipped rather than retried every heartbeat until it expires.
|
|
604
|
+
if (!command || !selectable || focusing || command.id === acknowledged || command.id === declined) return;
|
|
605
|
+
// The server can still list a child this binding has already dropped:
|
|
606
|
+
// it can settle between the click and the command arriving here.
|
|
607
|
+
// Decline an id this binding no longer tracks rather than asking the
|
|
608
|
+
// host to focus a finished child.
|
|
609
|
+
if (command.agentId !== null && !agents.has(command.agentId)) {
|
|
610
|
+
declined = command.id;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const target = mode;
|
|
614
|
+
focusing = true;
|
|
615
|
+
try {
|
|
616
|
+
if (command.agentId === null) await target.unfocusSession();
|
|
617
|
+
else await target.focusAgentSession(command.agentId);
|
|
618
|
+
acknowledged = command.id;
|
|
619
|
+
declined = undefined;
|
|
620
|
+
// Fold the confirmed view and the acknowledgement into a trailing
|
|
621
|
+
// report; the running one already carried the old state.
|
|
622
|
+
void report();
|
|
623
|
+
} catch (error) {
|
|
624
|
+
declined = command.id;
|
|
625
|
+
if (!switchWarned) {
|
|
626
|
+
switchWarned = true;
|
|
627
|
+
pi.logger?.debug?.('subshell subagent view switch failed', {
|
|
628
|
+
error: error instanceof Error ? error.message : String(error),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
} finally {
|
|
632
|
+
focusing = false;
|
|
393
633
|
}
|
|
394
634
|
} catch {
|
|
395
635
|
// A failed report is not fatal: the next frame reports again, and a pane
|
|
@@ -443,6 +683,8 @@ export default function (pi) {
|
|
|
443
683
|
|
|
444
684
|
const reset = () => {
|
|
445
685
|
agents.clear();
|
|
686
|
+
acknowledged = undefined;
|
|
687
|
+
declined = undefined;
|
|
446
688
|
if (timer !== null) {
|
|
447
689
|
clearTimeout(timer);
|
|
448
690
|
timer = null;
|
|
@@ -457,6 +699,7 @@ export default function (pi) {
|
|
|
457
699
|
pi.on('session_start', (_event, ctx) => {
|
|
458
700
|
role = ctx && ctx.agent && ctx.agent.kind === 'sub' ? 'sub' : 'main';
|
|
459
701
|
if (role !== 'main') return;
|
|
702
|
+
captureMode();
|
|
460
703
|
return reset();
|
|
461
704
|
});
|
|
462
705
|
|
|
@@ -486,7 +729,7 @@ export default function (pi) {
|
|
|
486
729
|
const parsed = parseManifest({
|
|
487
730
|
name: "@subshell-ai/plugin-oh-my-pi",
|
|
488
731
|
description: "Oh My Pi harness plugin for Subshell",
|
|
489
|
-
version: "0.
|
|
732
|
+
version: "0.3.1",
|
|
490
733
|
license: "Apache-2.0",
|
|
491
734
|
type: "module",
|
|
492
735
|
main: "dist/index.js",
|
|
@@ -761,16 +1004,21 @@ function withoutLegacyProviderExtension(flags) {
|
|
|
761
1004
|
*
|
|
762
1005
|
* The same directory carries the subagent bridge. omp keeps its child roster
|
|
763
1006
|
* on the session event bus; the extension reports it so the sidebar can hang
|
|
764
|
-
* those children under the parent subshell. omp
|
|
765
|
-
* moving the parent's terminal onto a child, so
|
|
1007
|
+
* those children under the parent subshell. omp has no extension hook for
|
|
1008
|
+
* moving the parent's terminal onto a child yet, so the bridge reaches the
|
|
1009
|
+
* TUI's own public focus method through the class omp exposes as
|
|
1010
|
+
* pi.pi.InteractiveMode (verified against 18.18.7). If that surface goes
|
|
1011
|
+
* away, the bridge falls back to status-only reporting.
|
|
766
1012
|
*/
|
|
767
1013
|
const createPlugin = () => ({
|
|
768
1014
|
subagents: {
|
|
769
1015
|
discovery: "extension",
|
|
770
|
-
navigation: "
|
|
1016
|
+
navigation: "native"
|
|
771
1017
|
},
|
|
1018
|
+
previews: { reporting: "extension" },
|
|
772
1019
|
capabilities: () => [
|
|
773
1020
|
"subagents",
|
|
1021
|
+
"previews",
|
|
774
1022
|
"mcp",
|
|
775
1023
|
"settings"
|
|
776
1024
|
],
|
|
@@ -806,11 +1054,13 @@ const createPlugin = () => ({
|
|
|
806
1054
|
omp: { extensions: [
|
|
807
1055
|
"bridge.mjs",
|
|
808
1056
|
"subagents.mjs",
|
|
1057
|
+
"previews.mjs",
|
|
809
1058
|
"provider.mjs"
|
|
810
1059
|
] }
|
|
811
1060
|
}),
|
|
812
1061
|
"bridge.mjs": "export default function () {}\n",
|
|
813
1062
|
"subagents.mjs": SUBAGENT_BRIDGE,
|
|
1063
|
+
"previews.mjs": PREVIEW_BRIDGE,
|
|
814
1064
|
"provider.mjs": MODEL_CLI_PROVIDER_SHIM,
|
|
815
1065
|
"mcp.json": fileContent
|
|
816
1066
|
},
|