@subshell-ai/plugin-oh-my-pi 0.3.0 → 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 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.12.6.
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,4 +1,4 @@
1
- //#region ../../plugin-api/dist/types-DPLl6DHG.d.ts
1
+ //#region ../../plugin-api/dist/types-D2qRfuN_.d.ts
2
2
  //#region src/previews.d.ts
3
3
  /**
4
4
  * How a harness reports loopback ports its own tooling started, for the
@@ -26,10 +26,79 @@ interface HarnessPreviews {
26
26
  /** One-time human action required for reporting to work, if any. */
27
27
  setup?: string;
28
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
+ }
29
61
  /** How the plugin learns about children: harness lifecycle hooks, or a loaded extension. */
30
62
  type HarnessSubagentDiscovery = "hooks" | "extension";
31
63
  /** Whether the sidebar can move the parent's own view to a child, or the harness keeps that in-app. */
32
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>;
33
102
  /**
34
103
  * What a plugin declares about its harness's child agents. The host uses
35
104
  * `discovery`/`navigation` to decide what the sidebar may offer; `setup`, when
@@ -43,6 +112,13 @@ interface HarnessSubagentSupport {
43
112
  navigation: HarnessSubagentNavigation;
44
113
  /** One-time user action required for reporting to work, if any */
45
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;
46
122
  }
47
123
  //#endregion
48
124
  //#region src/types.d.ts
@@ -737,8 +813,11 @@ declare const manifest: SubshellManifest;
737
813
  *
738
814
  * The same directory carries the subagent bridge. omp keeps its child roster
739
815
  * on the session event bus; the extension reports it so the sidebar can hang
740
- * those children under the parent subshell. omp offers no extension API for
741
- * moving the parent's terminal onto a child, so navigation stays unavailable.
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.
742
821
  */
743
822
  declare const createPlugin: HarnessPluginFactory;
744
823
  //#endregion
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../../plugin-api/dist/manifest-DgWWfiH-.js
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",
@@ -481,6 +481,19 @@ export default function (pi) {
481
481
  let queued = false;
482
482
  let heartbeat = null;
483
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;
484
497
  const label = value => {
485
498
  if (typeof value !== 'string') return undefined;
486
499
  const trimmed = value.trim().slice(0, MAX_TEXT);
@@ -511,6 +524,35 @@ export default function (pi) {
511
524
  return key ? agents.delete(key) : false;
512
525
  };
513
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
+
514
556
  const report = async () => {
515
557
  if (reporting) {
516
558
  queued = true;
@@ -528,20 +570,66 @@ export default function (pi) {
528
570
  }
529
571
  reporting = true;
530
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
+ }
531
587
  const response = await fetch(base + '/api/subshells/' + encodeURIComponent(id) + '/subagents/report', {
532
588
  method: 'POST',
533
589
  headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
534
- body: JSON.stringify({
535
- event: 'snapshot',
536
- agents: [...agents.values()].slice(0, MAX_AGENTS),
537
- selectedId: null,
538
- canSelect: false,
539
- }),
590
+ body: JSON.stringify(payload),
540
591
  signal: AbortSignal.timeout(2000),
541
592
  });
542
- if (!response.ok && !warned) {
543
- warned = true;
544
- pi.logger?.debug?.('subshell subagent report failed', { status: response.status });
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;
545
633
  }
546
634
  } catch {
547
635
  // A failed report is not fatal: the next frame reports again, and a pane
@@ -595,6 +683,8 @@ export default function (pi) {
595
683
 
596
684
  const reset = () => {
597
685
  agents.clear();
686
+ acknowledged = undefined;
687
+ declined = undefined;
598
688
  if (timer !== null) {
599
689
  clearTimeout(timer);
600
690
  timer = null;
@@ -609,6 +699,7 @@ export default function (pi) {
609
699
  pi.on('session_start', (_event, ctx) => {
610
700
  role = ctx && ctx.agent && ctx.agent.kind === 'sub' ? 'sub' : 'main';
611
701
  if (role !== 'main') return;
702
+ captureMode();
612
703
  return reset();
613
704
  });
614
705
 
@@ -638,7 +729,7 @@ export default function (pi) {
638
729
  const parsed = parseManifest({
639
730
  name: "@subshell-ai/plugin-oh-my-pi",
640
731
  description: "Oh My Pi harness plugin for Subshell",
641
- version: "0.3.0",
732
+ version: "0.3.1",
642
733
  license: "Apache-2.0",
643
734
  type: "module",
644
735
  main: "dist/index.js",
@@ -913,13 +1004,16 @@ function withoutLegacyProviderExtension(flags) {
913
1004
  *
914
1005
  * The same directory carries the subagent bridge. omp keeps its child roster
915
1006
  * on the session event bus; the extension reports it so the sidebar can hang
916
- * those children under the parent subshell. omp offers no extension API for
917
- * moving the parent's terminal onto a child, so navigation stays unavailable.
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.
918
1012
  */
919
1013
  const createPlugin = () => ({
920
1014
  subagents: {
921
1015
  discovery: "extension",
922
- navigation: "unavailable"
1016
+ navigation: "native"
923
1017
  },
924
1018
  previews: { reporting: "extension" },
925
1019
  capabilities: () => [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subshell-ai/plugin-oh-my-pi",
3
3
  "description": "Oh My Pi harness plugin for Subshell",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",