@ian-pascoe/pi-minimal-subagents 0.6.5 → 0.7.0
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 +33 -9
- package/package.json +1 -1
- package/src/minimal-subagents-config.ts +18 -14
- package/src/minimal-subagents-context.ts +19 -86
- package/src/minimal-subagents-coordinator.ts +9 -1
- package/src/minimal-subagents-extension.ts +89 -0
- package/src/minimal-subagents-fork-lifecycle.ts +1 -1
- package/src/minimal-subagents-registry-wire.ts +0 -4
- package/src/minimal-subagents-registry.ts +2 -6
- package/src/minimal-subagents-render-contract.ts +5 -9
- package/src/minimal-subagents-rendering.ts +18 -0
- package/src/minimal-subagents-sessions.ts +83 -7
- package/src/minimal-subagents-settings-writer.ts +9 -8
- package/src/minimal-subagents-status-panel.ts +308 -103
- package/src/minimal-subagents-types.ts +6 -4
- package/src/minimal-subagents-ui.ts +2 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ filter in `~/.pi/agent/settings.json` using the repository-relative path:
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
From this package checkout, load the source directly with
|
|
32
|
-
`pi -e ./src/index.ts`. Requires Node `>=22.19.0` and Pi `>=0.
|
|
32
|
+
`pi -e ./src/index.ts`. Requires Node `>=22.19.0` and Pi `>=0.85.1`.
|
|
33
33
|
|
|
34
34
|
## Configuration
|
|
35
35
|
|
|
@@ -251,14 +251,38 @@ source file; it never substitutes the source session's newer head.
|
|
|
251
251
|
|
|
252
252
|
## Status and TUI
|
|
253
253
|
|
|
254
|
-
In TUI mode, `/subagents status` opens a
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
254
|
+
In TUI mode, `/subagents` or `/subagents status` opens a large, centered,
|
|
255
|
+
framed overlay. Up/Down selects a Child Agent; Enter opens its Child Session
|
|
256
|
+
Transcript. Escape returns to the tree, then Escape closes the overlay. The
|
|
257
|
+
viewer is read-only: Root Agent input and ongoing agent work remain intact.
|
|
258
|
+
|
|
259
|
+
Two consecutive Left Arrow presses within 500 ms open the same viewer when
|
|
260
|
+
the main editor is focused and completely empty. Drafts (including whitespace),
|
|
261
|
+
dialogs, and other overlays retain normal navigation. Explicit key-repeat
|
|
262
|
+
reports are ignored; legacy terminals cannot distinguish holding Left from
|
|
263
|
+
two presses.
|
|
264
|
+
|
|
265
|
+
The viewer and compact widget prioritize sibling subtrees containing running
|
|
266
|
+
Child Agents. Idle ancestors move with active descendants, parents stay above
|
|
267
|
+
their children, and equally active siblings retain their original order. Viewer
|
|
268
|
+
selection follows the Child Agent's identity through live reordering.
|
|
269
|
+
|
|
270
|
+
The transcript includes inherited context, earlier turns, pre-compaction
|
|
271
|
+
messages, and live output on the selected saved branch. Abandoned branches and
|
|
272
|
+
internal bookkeeping are excluded. Saved history can be inspected without
|
|
273
|
+
restoring a runtime; missing or unverified sessions report an explanation rather
|
|
274
|
+
than substituting another branch. Model-facing Recent Activity remains bounded.
|
|
275
|
+
|
|
276
|
+
Transcripts open at the latest output and refresh once per second. Up/Down
|
|
277
|
+
scrolls by line, Page Up/Page Down by page. Scrolling up pauses following; End
|
|
278
|
+
returns to live output. Reading position is retained through resize and tool
|
|
279
|
+
expansion. Reasoning is visible, tool output starts collapsed, and Pi's configured
|
|
280
|
+
tool-expansion key (normally Ctrl+O) reveals it. Images appear as explicit text
|
|
281
|
+
placeholders rather than inline images.
|
|
282
|
+
|
|
283
|
+
The tree header reports the effective access source, authored settings, direct
|
|
284
|
+
running/idle counts, and actual Coordinator Tool activation. Partial external
|
|
285
|
+
activation is reported as `N/6 active`; status does not repair it.
|
|
262
286
|
|
|
263
287
|
RPC mode receives a concise status notification. JSON and print modes produce
|
|
264
288
|
no observer-only output.
|
package/package.json
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
1
|
import type { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import { type Static, Type } from "typebox";
|
|
4
3
|
import { Value } from "typebox/value";
|
|
@@ -7,16 +6,18 @@ import { DEFAULT_MAX_SUBAGENT_DEPTH, THINKING_LEVELS } from "./minimal-subagents
|
|
|
7
6
|
const MODEL_ROLE_NAME_MAX_LENGTH = 64;
|
|
8
7
|
const MODEL_ROLE_HINT_MAX_LENGTH = 500;
|
|
9
8
|
|
|
10
|
-
const JsonValueSchema = Type.Unsafe<JsonValue>({});
|
|
11
9
|
const SettingsDocumentSchema = Type.Object({
|
|
12
|
-
minimalSubagents: Type.Optional(
|
|
10
|
+
minimalSubagents: Type.Optional(Type.Unknown()),
|
|
13
11
|
});
|
|
14
12
|
const MinimalSubagentsSettingsSchema = Type.Object({
|
|
15
|
-
enabled: Type.Optional(
|
|
16
|
-
maxSubagentDepth: Type.Optional(
|
|
17
|
-
modelRoles: Type.Optional(
|
|
13
|
+
enabled: Type.Optional(Type.Unknown()),
|
|
14
|
+
maxSubagentDepth: Type.Optional(Type.Unknown()),
|
|
15
|
+
modelRoles: Type.Optional(Type.Unknown()),
|
|
16
|
+
});
|
|
17
|
+
const ModelRoleObjectSchema = Type.Object({
|
|
18
|
+
model: Type.Optional(Type.Unknown()),
|
|
19
|
+
hint: Type.Optional(Type.Unknown()),
|
|
18
20
|
});
|
|
19
|
-
const JsonObjectSchema = Type.Record(Type.String(), JsonValueSchema);
|
|
20
21
|
const EnabledSettingSchema = Type.Boolean();
|
|
21
22
|
const PositiveSafeIntegerSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
22
23
|
const MaxSubagentDepthSettingSchema = Type.Union([PositiveSafeIntegerSchema, Type.Null()]);
|
|
@@ -28,7 +29,7 @@ const ExpandedModelRoleSchema = Type.Object(
|
|
|
28
29
|
},
|
|
29
30
|
{ additionalProperties: false },
|
|
30
31
|
);
|
|
31
|
-
const ModelRoleEntriesSchema = Type.Record(Type.String(),
|
|
32
|
+
const ModelRoleEntriesSchema = Type.Record(Type.String(), Type.Unknown());
|
|
32
33
|
const ModelRolesSettingSchema = Type.Union([ModelRoleEntriesSchema, Type.Null()]);
|
|
33
34
|
|
|
34
35
|
type ModelRoleThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
@@ -61,7 +62,7 @@ export interface ResolvedMinimalSubagentsConfig {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
interface MinimalSubagentsSettingsDocument {
|
|
64
|
-
minimalSubagents?:
|
|
65
|
+
minimalSubagents?: unknown;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
interface MinimalSubagentsConfigInput {
|
|
@@ -102,7 +103,7 @@ type ModelRoleWireValue =
|
|
|
102
103
|
| { kind: "delete" }
|
|
103
104
|
| { kind: "shorthand"; model: string }
|
|
104
105
|
| { kind: "expanded"; fields: Static<typeof ExpandedModelRoleSchema> }
|
|
105
|
-
| { kind: "malformed-expanded"; fields:
|
|
106
|
+
| { kind: "malformed-expanded"; fields: Static<typeof ModelRoleObjectSchema> }
|
|
106
107
|
| { kind: "invalid" };
|
|
107
108
|
|
|
108
109
|
type ModelRolesWireValue =
|
|
@@ -110,15 +111,17 @@ type ModelRolesWireValue =
|
|
|
110
111
|
| { kind: "entries"; entries: ReadonlyMap<string, ModelRoleWireValue> }
|
|
111
112
|
| { kind: "invalid" };
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Authored settings remain unparsed until the depth schema below validates their value.
|
|
115
|
+
function parseMaxSubagentDepthWireValue(value: unknown): MaxSubagentDepthWireValue {
|
|
114
116
|
if (!Value.Check(MaxSubagentDepthSettingSchema, value)) return { kind: "invalid" };
|
|
115
117
|
return value === null ? { kind: "reset" } : { kind: "depth", value };
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Role entries may contain arbitrary settings data; schemas classify them before model/hint validation.
|
|
121
|
+
function parseModelRoleWireValue(value: unknown): ModelRoleWireValue {
|
|
119
122
|
if (value === null) return { kind: "delete" };
|
|
120
123
|
if (Value.Check(ShorthandModelRoleSchema, value)) return { kind: "shorthand", model: value };
|
|
121
|
-
if (Value.Check(
|
|
124
|
+
if (Value.Check(ModelRoleObjectSchema, value)) {
|
|
122
125
|
return Value.Check(ExpandedModelRoleSchema, value)
|
|
123
126
|
? { kind: "expanded", fields: value }
|
|
124
127
|
: { kind: "malformed-expanded", fields: value };
|
|
@@ -132,7 +135,8 @@ function isExpandedModelRoleWireValue(
|
|
|
132
135
|
return value.kind === "expanded" || value.kind === "malformed-expanded";
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
|
|
138
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Validate the authored role collection before parsing each untrusted entry.
|
|
139
|
+
function parseModelRolesWireValue(value: unknown): ModelRolesWireValue {
|
|
136
140
|
if (!Value.Check(ModelRolesSettingSchema, value)) return { kind: "invalid" };
|
|
137
141
|
if (value === null) return { kind: "reset" };
|
|
138
142
|
return {
|
|
@@ -8,8 +8,6 @@ import type {
|
|
|
8
8
|
} from "./minimal-subagents-types.js";
|
|
9
9
|
|
|
10
10
|
const RECENT_AGENT_ACTIVITY_LIMIT = 12;
|
|
11
|
-
const CHILD_AGENT_TRANSCRIPT_MESSAGE_LIMIT = 24;
|
|
12
|
-
const CHILD_AGENT_TRANSCRIPT_PAIR_WINDOW = CHILD_AGENT_TRANSCRIPT_MESSAGE_LIMIT * 2;
|
|
13
11
|
const RECENT_AGENT_ACTIVITY_MAX_LINES = 20;
|
|
14
12
|
const RECENT_AGENT_ACTIVITY_MAX_BYTES = 2 * 1024;
|
|
15
13
|
|
|
@@ -47,110 +45,45 @@ function visibleMessageContent(content: string | readonly (TextContent | ImageCo
|
|
|
47
45
|
return contentText(content, "\n\n") || "(no text content)";
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
function
|
|
48
|
+
function replaceAgentMessageImages(message: AgentMessage): AgentMessage {
|
|
51
49
|
if (message.role === "user" || message.role === "custom") {
|
|
52
50
|
return {
|
|
53
51
|
...structuredClone(message),
|
|
54
52
|
content: Array.isArray(message.content)
|
|
55
|
-
? message.content
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
? message.content.map((content) =>
|
|
54
|
+
content.type === "image"
|
|
55
|
+
? { type: "text" as const, text: `[Image: ${content.mimeType}]` }
|
|
56
|
+
: structuredClone(content),
|
|
57
|
+
)
|
|
58
58
|
: message.content,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
if (message.role === "toolResult") {
|
|
62
62
|
return {
|
|
63
63
|
...structuredClone(message),
|
|
64
|
-
content: message.content
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
content: message.content.map((content) =>
|
|
65
|
+
content.type === "image"
|
|
66
|
+
? { type: "text" as const, text: `[Image: ${content.mimeType}]` }
|
|
67
|
+
: structuredClone(content),
|
|
68
|
+
),
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
return structuredClone(message);
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
message: AgentMessage;
|
|
74
|
-
originalIndex: number;
|
|
75
|
-
streaming: boolean;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Select at most 48 recent raw messages, retaining cross-cutoff tool pairs and omitting images. */
|
|
74
|
+
/** Clone the complete visible UI transcript, replacing images without bounding conversation history. */
|
|
79
75
|
export function selectChildAgentTranscript(
|
|
80
76
|
messages: readonly AgentMessage[],
|
|
81
77
|
streamingAssistantMessage?: AgentMessage,
|
|
82
78
|
): ChildAgentTranscriptSnapshot {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const indexed: IndexedTranscriptMessage[] = [
|
|
86
|
-
...pairWindow.map((message, windowIndex) => ({
|
|
87
|
-
message,
|
|
88
|
-
originalIndex: firstWindowIndex + windowIndex,
|
|
89
|
-
streaming: false,
|
|
90
|
-
})),
|
|
91
|
-
...(streamingAssistantMessage
|
|
92
|
-
? [
|
|
93
|
-
{
|
|
94
|
-
message: streamingAssistantMessage,
|
|
95
|
-
originalIndex: messages.length,
|
|
96
|
-
streaming: true,
|
|
97
|
-
},
|
|
98
|
-
]
|
|
99
|
-
: []),
|
|
100
|
-
];
|
|
101
|
-
const tail = indexed.slice(-CHILD_AGENT_TRANSCRIPT_MESSAGE_LIMIT).map((item) => ({
|
|
102
|
-
...item,
|
|
103
|
-
message: omitAgentMessageImages(item.message),
|
|
104
|
-
}));
|
|
105
|
-
const selectedIndexes = new Set(tail.map(({ originalIndex }) => originalIndex));
|
|
106
|
-
const calls = new Map<
|
|
107
|
-
string,
|
|
108
|
-
{ originalIndex: number; assistant: Extract<AgentMessage, { role: "assistant" }> }
|
|
109
|
-
>();
|
|
110
|
-
for (const item of indexed) {
|
|
111
|
-
if (item.message.role !== "assistant") continue;
|
|
112
|
-
for (const content of item.message.content) {
|
|
113
|
-
if (content.type === "toolCall") {
|
|
114
|
-
calls.set(content.id, { originalIndex: item.originalIndex, assistant: item.message });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const retained = tail.filter(
|
|
120
|
-
(item) => item.message.role !== "toolResult" || calls.has(item.message.toolCallId),
|
|
121
|
-
);
|
|
122
|
-
const missingCalls = new Map<
|
|
123
|
-
number,
|
|
124
|
-
{ assistant: Extract<AgentMessage, { role: "assistant" }>; callIds: Set<string> }
|
|
125
|
-
>();
|
|
126
|
-
for (const item of retained) {
|
|
127
|
-
if (item.message.role !== "toolResult") continue;
|
|
128
|
-
const call = calls.get(item.message.toolCallId);
|
|
129
|
-
if (!call || selectedIndexes.has(call.originalIndex)) continue;
|
|
130
|
-
const missing = missingCalls.get(call.originalIndex) ?? {
|
|
131
|
-
assistant: call.assistant,
|
|
132
|
-
callIds: new Set<string>(),
|
|
133
|
-
};
|
|
134
|
-
missing.callIds.add(item.message.toolCallId);
|
|
135
|
-
missingCalls.set(call.originalIndex, missing);
|
|
136
|
-
}
|
|
137
|
-
const prefixes: IndexedTranscriptMessage[] = [...missingCalls.entries()]
|
|
138
|
-
.sort(([left], [right]) => left - right)
|
|
139
|
-
.map(([originalIndex, missing]) => ({
|
|
140
|
-
message: {
|
|
141
|
-
...structuredClone(missing.assistant),
|
|
142
|
-
content: missing.assistant.content
|
|
143
|
-
.filter((content) => content.type === "toolCall" && missing.callIds.has(content.id))
|
|
144
|
-
.map((content) => structuredClone(content)),
|
|
145
|
-
},
|
|
146
|
-
originalIndex,
|
|
147
|
-
streaming: false,
|
|
148
|
-
}));
|
|
149
|
-
const selected = [...prefixes, ...retained];
|
|
150
|
-
const streamingAssistantIndex = selected.findIndex(({ streaming }) => streaming);
|
|
79
|
+
const visible = messages.filter((message) => message.role !== "custom" || message.display);
|
|
80
|
+
const streaming = streamingAssistantMessage && !messages.includes(streamingAssistantMessage);
|
|
151
81
|
return {
|
|
152
|
-
messages:
|
|
153
|
-
|
|
82
|
+
messages: [
|
|
83
|
+
...visible.map(replaceAgentMessageImages),
|
|
84
|
+
...(streaming ? [replaceAgentMessageImages(streamingAssistantMessage)] : []),
|
|
85
|
+
],
|
|
86
|
+
streamingAssistantIndex: streaming ? visible.length : undefined,
|
|
154
87
|
toolDefinitions: [],
|
|
155
88
|
};
|
|
156
89
|
}
|
|
@@ -452,14 +452,22 @@ export class MinimalSubagentsCoordinator {
|
|
|
452
452
|
};
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
/** Lazily inspect one Child
|
|
455
|
+
/** Lazily inspect one Child Session Transcript without restoring a missing runtime. */
|
|
456
456
|
inspectTranscript(agentId: string): ChildAgentTranscriptSnapshot {
|
|
457
457
|
const agent = this.requireAgent(agentId);
|
|
458
458
|
const runtime = this.runtimes.get(agentId);
|
|
459
459
|
const liveSnapshot = runtime?.snapshotActivityTranscript?.();
|
|
460
460
|
if (liveSnapshot) return liveSnapshot;
|
|
461
461
|
if (runtime) return selectChildAgentTranscript(runtime.snapshotActivityMessages());
|
|
462
|
+
let historyError: string | undefined;
|
|
463
|
+
try {
|
|
464
|
+
const saved = this.dependencies.sessions.readTranscript?.(agent);
|
|
465
|
+
if (saved) return saved;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
historyError = error instanceof Error ? error.message : String(error);
|
|
468
|
+
}
|
|
462
469
|
const fallback =
|
|
470
|
+
historyError ||
|
|
463
471
|
agent.unavailable_reason ||
|
|
464
472
|
agent.latest_result?.error ||
|
|
465
473
|
agent.latest_result?.output ||
|
|
@@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import {
|
|
4
4
|
buildSessionContext,
|
|
5
|
+
CustomEditor,
|
|
5
6
|
getAgentDir,
|
|
6
7
|
SessionManager,
|
|
7
8
|
SettingsManager,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
type ExtensionCommandContext,
|
|
10
11
|
type ExtensionContext,
|
|
11
12
|
type ExtensionFactory,
|
|
13
|
+
type ExtensionUIContext,
|
|
12
14
|
type MessageEndEvent,
|
|
13
15
|
type SessionBeforeForkEvent,
|
|
14
16
|
type SessionEntry,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
type SessionStartEvent,
|
|
17
19
|
type SessionTreeEvent,
|
|
18
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
|
|
19
22
|
import {
|
|
20
23
|
createSubagentAccessBranchRecord,
|
|
21
24
|
reconcileCoordinatorToolAccess,
|
|
@@ -393,6 +396,80 @@ const productionLifecycleEffects: MinimalSubagentsLifecycleEffects = {
|
|
|
393
396
|
createSessionFactory: (options) => new PiAgentSessionFactory(options),
|
|
394
397
|
};
|
|
395
398
|
|
|
399
|
+
/** Compose a focus-local key sequence without taking over the editor's other behavior. */
|
|
400
|
+
function installViewerShortcut(ui: ExtensionUIContext, open: () => void) {
|
|
401
|
+
const previous = ui.getEditorComponent();
|
|
402
|
+
let active = true;
|
|
403
|
+
let previousLeftAt: number | undefined;
|
|
404
|
+
const reset = () => {
|
|
405
|
+
previousLeftAt = undefined;
|
|
406
|
+
};
|
|
407
|
+
const factory: NonNullable<ReturnType<ExtensionUIContext["getEditorComponent"]>> = (
|
|
408
|
+
tui,
|
|
409
|
+
theme,
|
|
410
|
+
keybindings,
|
|
411
|
+
) => {
|
|
412
|
+
reset();
|
|
413
|
+
const editor =
|
|
414
|
+
previous?.(tui, theme, keybindings) ??
|
|
415
|
+
new CustomEditor(tui, theme, keybindings, { embedWorkingStatus: true });
|
|
416
|
+
let focused = false;
|
|
417
|
+
const handleInput = (data: string) => {
|
|
418
|
+
if (isKeyRelease(data)) {
|
|
419
|
+
editor.handleInput(data);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (
|
|
423
|
+
active &&
|
|
424
|
+
focused &&
|
|
425
|
+
editor.getText() === "" &&
|
|
426
|
+
matchesKey(data, "left") &&
|
|
427
|
+
!isKeyRepeat(data)
|
|
428
|
+
) {
|
|
429
|
+
const now = performance.now();
|
|
430
|
+
if (previousLeftAt !== undefined && now - previousLeftAt <= 500) {
|
|
431
|
+
reset();
|
|
432
|
+
open();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
previousLeftAt = now;
|
|
436
|
+
} else {
|
|
437
|
+
reset();
|
|
438
|
+
}
|
|
439
|
+
editor.handleInput(data);
|
|
440
|
+
};
|
|
441
|
+
// Forward the editor's complete interface, including app handlers and custom methods.
|
|
442
|
+
// Binding to the original instance also preserves private fields in custom editors.
|
|
443
|
+
return new Proxy(editor, {
|
|
444
|
+
has: (target, property) => property === "focused" || Reflect.has(target, property),
|
|
445
|
+
get(target, property) {
|
|
446
|
+
if (property === "focused") return focused;
|
|
447
|
+
if (property === "handleInput") return handleInput;
|
|
448
|
+
// oxlint-disable-next-line anti-slop/no-reflect-get -- SAFETY: Forward the SDK editor's open interface, including other extensions' methods, without replacing their receiver.
|
|
449
|
+
const value = Reflect.get(target, property, target);
|
|
450
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- SAFETY: Proxy forwarding binds callable members; this is not parsing external input.
|
|
451
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
452
|
+
},
|
|
453
|
+
set(target, property, value) {
|
|
454
|
+
if (property === "focused") {
|
|
455
|
+
focused = value;
|
|
456
|
+
if (!focused) reset();
|
|
457
|
+
}
|
|
458
|
+
return Reflect.set(target, property, value, target);
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
ui.setEditorComponent(factory);
|
|
463
|
+
return {
|
|
464
|
+
reset,
|
|
465
|
+
dispose() {
|
|
466
|
+
active = false;
|
|
467
|
+
reset();
|
|
468
|
+
if (ui.getEditorComponent() === factory) ui.setEditorComponent(previous);
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
396
473
|
/** Own coordinator, UI, and prepared-fork state for one root Pi session lifecycle. */
|
|
397
474
|
export class MinimalSubagentsLifecycleController {
|
|
398
475
|
private coordinator: MinimalSubagentsCoordinator | undefined;
|
|
@@ -407,6 +484,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
407
484
|
};
|
|
408
485
|
private uiController: MinimalSubagentsUiController | undefined;
|
|
409
486
|
private statusPanelController: MinimalSubagentsStatusPanelController | undefined;
|
|
487
|
+
private viewerShortcut: ReturnType<typeof installViewerShortcut> | undefined;
|
|
410
488
|
private accessSession: ActiveSubagentAccessSession | undefined;
|
|
411
489
|
private preparedFork:
|
|
412
490
|
| { sourceSessionFile: string; selectedBranchSnapshot: RegistrySnapshot }
|
|
@@ -619,6 +697,14 @@ export class MinimalSubagentsLifecycleController {
|
|
|
619
697
|
() => this.currentSubagentStatusAccess(),
|
|
620
698
|
);
|
|
621
699
|
|
|
700
|
+
this.viewerShortcut?.dispose();
|
|
701
|
+
this.viewerShortcut =
|
|
702
|
+
context.mode === "tui"
|
|
703
|
+
? installViewerShortcut(context.ui, () => {
|
|
704
|
+
void this.statusPanelController?.open();
|
|
705
|
+
})
|
|
706
|
+
: undefined;
|
|
707
|
+
|
|
622
708
|
if (hasHistoricalChildIdentity(context.sessionManager.getBranch())) {
|
|
623
709
|
context.ui.notify(
|
|
624
710
|
"Opened a former subagent session directly. It is now an independent root; former descendants and parent messaging were not restored. Concurrent ownership by its original root is unsupported.",
|
|
@@ -666,6 +752,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
666
752
|
_event: SessionTreeEvent,
|
|
667
753
|
context: ExtensionContext,
|
|
668
754
|
): Promise<void> {
|
|
755
|
+
this.viewerShortcut?.reset();
|
|
669
756
|
if (!this.coordinator) return;
|
|
670
757
|
const snapshot = replayRegistryEntries(
|
|
671
758
|
context.sessionManager.getBranch(),
|
|
@@ -805,6 +892,8 @@ export class MinimalSubagentsLifecycleController {
|
|
|
805
892
|
event: SessionShutdownEvent,
|
|
806
893
|
context: ExtensionContext,
|
|
807
894
|
): Promise<void> {
|
|
895
|
+
this.viewerShortcut?.dispose();
|
|
896
|
+
this.viewerShortcut = undefined;
|
|
808
897
|
this.statusPanelController?.dispose();
|
|
809
898
|
this.statusPanelController = undefined;
|
|
810
899
|
if (this.coordinator) {
|
|
@@ -2,7 +2,7 @@ import type { ForkSnapshot } from "./minimal-subagents-types.js";
|
|
|
2
2
|
import { canonicalPath } from "./minimal-subagents-paths.js";
|
|
3
3
|
|
|
4
4
|
declare global {
|
|
5
|
-
//
|
|
5
|
+
// A process-global handoff must be visible to replacement extension instances.
|
|
6
6
|
var minimalSubagentsForkSnapshots: Map<string, ForkSnapshot> | undefined;
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
1
|
import { Type, type Static } from "typebox";
|
|
3
2
|
|
|
4
|
-
/** Establishes Pi's recursive JSON owner type before Registry envelope parsing. */
|
|
5
|
-
export const RegistryJsonValueWireSchema = Type.Unsafe<JsonValue>({});
|
|
6
|
-
|
|
7
3
|
const NonnegativeNumberSchema = Type.Number({ minimum: 0 });
|
|
8
4
|
const NonEmptyStringSchema = Type.String({ minLength: 1 });
|
|
9
5
|
const TurnStatusSchema = Type.Union([
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
RegistryDeliveryTurnEventWireSchema,
|
|
29
29
|
RegistryEnvelopeWireSchema,
|
|
30
30
|
RegistryEventDiscriminantWireSchema,
|
|
31
|
-
RegistryJsonValueWireSchema,
|
|
32
31
|
RegistryLooseEnvelopeWireSchema,
|
|
33
32
|
RegistryMessageRecordedEventWireSchema,
|
|
34
33
|
RegistryRootProbeWireSchema,
|
|
@@ -846,7 +845,8 @@ function validParsedEvent(event: RegistryEventV2): ParsedRegistryEvent {
|
|
|
846
845
|
type RegistryParseInput = JsonValue | RegistryEventV2;
|
|
847
846
|
|
|
848
847
|
function parseRegistryEventRecord(
|
|
849
|
-
|
|
848
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Registry entry data is unparsed; ownership is checked first, then envelope and event schemas validate every consumed field.
|
|
849
|
+
value: unknown,
|
|
850
850
|
rootSessionId: string,
|
|
851
851
|
): ParsedRegistryEvent {
|
|
852
852
|
if (Value.Check(RegistryRootProbeWireSchema, value) && value.root_session_id !== rootSessionId) {
|
|
@@ -1191,10 +1191,6 @@ export function replayRegistryEntries(
|
|
|
1191
1191
|
const diagnostics: RegistryReplayDiagnostic[] = [];
|
|
1192
1192
|
entries.forEach((entry, entryIndex) => {
|
|
1193
1193
|
if (entry.type !== "custom" || entry.customType !== REGISTRY_ENTRY_TYPE) return;
|
|
1194
|
-
if (!Value.Check(RegistryJsonValueWireSchema, entry.data)) {
|
|
1195
|
-
reportDiagnostic(diagnostics, entryIndex, "invalid-envelope", "record must be JSON");
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
1194
|
const parsed = parseRegistryEventRecord(entry.data, rootSessionId);
|
|
1199
1195
|
if (parsed.kind === "foreign-root") return;
|
|
1200
1196
|
if (parsed.kind === "event") {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentToolResult,
|
|
3
|
-
MessageRenderer,
|
|
4
|
-
ToolDefinition,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
2
|
import { Type, type Static } from "typebox";
|
|
7
3
|
import { Value } from "typebox/value";
|
|
8
4
|
import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
|
|
@@ -339,8 +335,6 @@ export type RenderStatusAgent = Static<typeof RenderStatusAgentSchema>;
|
|
|
339
335
|
/** Raw tool arguments supplied by Pi's tool-rendering interface. */
|
|
340
336
|
export type CoordinatorToolCallInput = Parameters<NonNullable<ToolDefinition["renderCall"]>>[0];
|
|
341
337
|
|
|
342
|
-
type CoordinatorMessageInput = Parameters<MessageRenderer>[0];
|
|
343
|
-
|
|
344
338
|
/** Parsed tool-call arguments tagged by their coordinator tool name. */
|
|
345
339
|
export type ParsedCoordinatorToolCall =
|
|
346
340
|
| { toolName: "subagent"; args: SpawnCallArguments }
|
|
@@ -383,7 +377,8 @@ export function parseCoordinatorToolCall(
|
|
|
383
377
|
/** Parse one historical tool result exactly once at the transcript rendering boundary. */
|
|
384
378
|
export function parseCoordinatorToolResult(
|
|
385
379
|
toolName: CoordinatorToolName,
|
|
386
|
-
|
|
380
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Historical tool details are unparsed until the selected per-tool schema checks them below.
|
|
381
|
+
details: unknown,
|
|
387
382
|
): ParsedCoordinatorToolResult | undefined {
|
|
388
383
|
switch (toolName) {
|
|
389
384
|
case "subagent":
|
|
@@ -417,7 +412,8 @@ export type CoordinatorMessageRenderDetails = Static<typeof CoordinatorMessageRe
|
|
|
417
412
|
|
|
418
413
|
/** Parse optional custom-message details while tolerating legacy field names. */
|
|
419
414
|
export function parseCoordinatorMessageDetails(
|
|
420
|
-
|
|
415
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Historical custom-message metadata is validated by its owning schema below.
|
|
416
|
+
details: unknown,
|
|
421
417
|
): CoordinatorMessageRenderDetails | undefined {
|
|
422
418
|
return Value.Check(CoordinatorMessageRenderDetailsSchema, details) ? details : undefined;
|
|
423
419
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type WaitRenderDetails,
|
|
40
40
|
} from "./minimal-subagents-render-contract.js";
|
|
41
41
|
import { stripCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
|
|
42
|
+
import type { AgentSummary } from "./minimal-subagents-types.js";
|
|
42
43
|
|
|
43
44
|
export type { CoordinatorToolName } from "./minimal-subagents-render-contract.js";
|
|
44
45
|
|
|
@@ -98,6 +99,23 @@ function toolResultText(result: AgentToolResult<unknown>): string {
|
|
|
98
99
|
return text?.type === "text" ? text.text : "";
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/** Copy the hierarchy with active subtrees first, preserving sibling ties and ancestry. */
|
|
103
|
+
export function orderActiveAgentSubtrees(agents: readonly AgentSummary[]): AgentSummary[] {
|
|
104
|
+
const orderSiblings = (
|
|
105
|
+
siblings: readonly AgentSummary[],
|
|
106
|
+
): { agent: AgentSummary; active: boolean }[] =>
|
|
107
|
+
siblings
|
|
108
|
+
.map((agent) => {
|
|
109
|
+
const children = orderSiblings(agent.children);
|
|
110
|
+
return {
|
|
111
|
+
agent: { ...agent, children: children.map((child) => child.agent) },
|
|
112
|
+
active: agent.state === "running" || children.some((child) => child.active),
|
|
113
|
+
};
|
|
114
|
+
})
|
|
115
|
+
.sort((left, right) => Number(right.active) - Number(left.active));
|
|
116
|
+
return orderSiblings(agents).map(({ agent }) => agent);
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
/** Shared unavailable → running → latest-turn → idle status ladder for one subagent. */
|
|
102
120
|
export function subagentStatusLadder(agent: {
|
|
103
121
|
readonly availability?: string;
|