@hheei/pi-subagents 0.1.2 → 0.2.3
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 +45 -137
- package/dist/agent-resolver.d.ts +20 -0
- package/dist/agent-resolver.js +155 -21
- package/dist/bridge-client.d.ts +55 -0
- package/dist/bridge-client.js +453 -0
- package/dist/bridge-server.d.ts +72 -0
- package/dist/bridge-server.js +490 -0
- package/dist/builtin-agents.d.ts +24 -0
- package/dist/builtin-agents.js +152 -0
- package/dist/child-bridge.d.ts +31 -15
- package/dist/child-bridge.js +95 -117
- package/dist/child-control.d.ts +58 -0
- package/dist/child-control.js +176 -0
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +93 -0
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +52 -60
- package/dist/diagnostics.d.ts +0 -2
- package/dist/diagnostics.js +0 -8
- package/dist/domain.d.ts +57 -27
- package/dist/domain.js +60 -11
- package/dist/extension.d.ts +26 -1
- package/dist/extension.js +243 -46
- package/dist/host-adapter.d.ts +37 -12
- package/dist/host-adapter.js +214 -63
- package/dist/json-lines.js +2 -1
- package/dist/launch-spec.d.ts +8 -6
- package/dist/launch-spec.js +61 -18
- package/dist/manager.d.ts +93 -49
- package/dist/manager.js +1175 -528
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.js +21 -0
- package/dist/protocol.d.ts +33 -52
- package/dist/protocol.js +41 -52
- package/dist/registry.d.ts +3 -8
- package/dist/registry.js +41 -163
- package/dist/rpc-events.d.ts +3 -5
- package/dist/rpc-events.js +5 -9
- package/dist/runtime.d.ts +66 -11
- package/dist/runtime.js +182 -208
- package/dist/session-bootstrap.d.ts +23 -7
- package/dist/session-bootstrap.js +79 -15
- package/dist/session-fork.d.ts +16 -0
- package/dist/session-fork.js +67 -0
- package/dist/state.d.ts +7 -0
- package/dist/state.js +60 -25
- package/dist/tool-activation.d.ts +18 -0
- package/dist/tool-activation.js +227 -0
- package/dist/tools.d.ts +6 -3
- package/dist/tools.js +213 -72
- package/dist/widget.d.ts +4 -8
- package/dist/widget.js +55 -64
- package/package.json +51 -52
- package/dist/child-nudge.d.ts +0 -24
- package/dist/child-nudge.js +0 -102
- package/dist/connector.d.ts +0 -53
- package/dist/connector.js +0 -283
- package/dist/pause-gate.d.ts +0 -14
- package/dist/pause-gate.js +0 -64
- package/dist/rpc-adapter.d.ts +0 -40
- package/dist/rpc-adapter.js +0 -194
- package/dist/runner-entry.d.ts +0 -24
- package/dist/runner-entry.js +0 -166
- package/dist/runner.d.ts +0 -52
- package/dist/runner.js +0 -930
package/dist/extension.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import { registerExtensionLifecycle } from "@hheei/pi-ext-core";
|
|
1
|
+
import { errorMessage, getBackgroundDelivery, registerExtensionLifecycle, TASK_REGISTRY_SERVICE_KEY, waitForService, } from "@hheei/pi-ext-core";
|
|
2
|
+
import { ChildBridgeServer } from "./bridge-server.js";
|
|
2
3
|
import { registerChildBridge } from "./child-bridge.js";
|
|
3
4
|
import { bindParentStatus, registerParentCommands } from "./commands.js";
|
|
4
5
|
import { BRIDGE_ENVIRONMENT_KEYS, isThinkingLevel } from "./domain.js";
|
|
5
6
|
import { createCmuxHostAdapter, createHerdrHostAdapter, selectHostAdapter, } from "./host-adapter.js";
|
|
6
7
|
import { SubagentManager } from "./manager.js";
|
|
8
|
+
import { transformSubagentsMarkdown } from "./markdown.js";
|
|
7
9
|
import { createSubagentRegistry } from "./registry.js";
|
|
8
|
-
import { createRuntimeTokenStore,
|
|
10
|
+
import { createRuntimeTokenStore, launchChild, openChildPanel, parentBridgeEndpoint, prepareRuntimeDirectory, } from "./runtime.js";
|
|
9
11
|
import { persistSubagentIntent, resolveSubagentLaunch } from "./session-bootstrap.js";
|
|
12
|
+
import { resolveForkSessionReference } from "./session-fork.js";
|
|
13
|
+
import { registerInteractiveToolActivation } from "./tool-activation.js";
|
|
10
14
|
import { registerParentTools } from "./tools.js";
|
|
11
15
|
import { createSubagentWidget } from "./widget.js";
|
|
12
16
|
const CHILD_ENV_KEYS = Object.values(BRIDGE_ENVIRONMENT_KEYS);
|
|
17
|
+
/** How long startup waits for a child that survived a parent restart to reconnect its bridge. */
|
|
18
|
+
const ADOPT_WINDOW_MS = 5_000;
|
|
19
|
+
function diagnose(message) {
|
|
20
|
+
if (process.env.DEBUG || process.env.PI_SUBAGENTS_DEBUG) {
|
|
21
|
+
console.error(`pi-subagents: ${message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
function requiredBridgeValue(env, key) {
|
|
14
25
|
const value = env[key];
|
|
15
26
|
if (typeof value !== "string" || value === "") {
|
|
@@ -29,6 +40,72 @@ function childIdentityFromEnv(env) {
|
|
|
29
40
|
token: requiredBridgeValue(env, BRIDGE_ENVIRONMENT_KEYS.token),
|
|
30
41
|
};
|
|
31
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* How child reports reach the parent conversation.
|
|
45
|
+
*
|
|
46
|
+
* Busy parents receive a steer into the next model step. Idle parents wait until all registered
|
|
47
|
+
* background work finishes; decision requests and blockers bypass that wait. The shared gate
|
|
48
|
+
* coordinates this channel with Task/Bash results so only one message requests a new idle turn.
|
|
49
|
+
*/
|
|
50
|
+
export function createParentChannel(pi, deps) {
|
|
51
|
+
const held = [];
|
|
52
|
+
let disposed = false;
|
|
53
|
+
const flush = (triggerTurn, urgentOnly = false) => {
|
|
54
|
+
if (held.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const reports = urgentOnly ? held.filter((report) => report.reason === "blocked") : [...held];
|
|
57
|
+
if (reports.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
for (const report of reports)
|
|
60
|
+
held.splice(held.indexOf(report), 1);
|
|
61
|
+
try {
|
|
62
|
+
pi.sendMessage({
|
|
63
|
+
customType: "subagent-report",
|
|
64
|
+
content: reports.map(reportText).join("\n\n"),
|
|
65
|
+
display: true,
|
|
66
|
+
details: { reports },
|
|
67
|
+
}, { triggerTurn, deliverAs: "steer" });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
held.unshift(...reports);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const registration = deps.delivery.registerChannel({
|
|
75
|
+
isIdle: deps.isIdle,
|
|
76
|
+
hasPending: () => !deps.signal?.aborted && held.length > 0,
|
|
77
|
+
flush,
|
|
78
|
+
});
|
|
79
|
+
const stopWatching = pi.on("agent_start", () => registration.request());
|
|
80
|
+
const stopSettled = pi.on("agent_settled", () => registration.request());
|
|
81
|
+
return {
|
|
82
|
+
async deliver(report) {
|
|
83
|
+
if (disposed)
|
|
84
|
+
throw new Error("Parent report channel is disposed");
|
|
85
|
+
held.push(report);
|
|
86
|
+
registration.request(report.reason === "blocked");
|
|
87
|
+
},
|
|
88
|
+
dispose() {
|
|
89
|
+
disposed = true;
|
|
90
|
+
registration.dispose();
|
|
91
|
+
stopWatching();
|
|
92
|
+
stopSettled();
|
|
93
|
+
// The session is going away, so the held reports are appended for the record without
|
|
94
|
+
// starting a run in a parent that is being torn down.
|
|
95
|
+
flush(false);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Pi publishes the skills it loaded with every prompt; a definition names them the same way, so
|
|
101
|
+
* the launch path needs exactly the name and the file path from that list.
|
|
102
|
+
*/
|
|
103
|
+
export function skillCatalogFromLoaded(skills) {
|
|
104
|
+
return skills.map((skill) => ({ name: skill.name, path: skill.filePath }));
|
|
105
|
+
}
|
|
106
|
+
function reportText(report) {
|
|
107
|
+
return `[Subagent ${report.agent} ${report.childId}: ${report.reason}]\n${report.message}`;
|
|
108
|
+
}
|
|
32
109
|
export default function piSubagentsExtension(pi) {
|
|
33
110
|
const child = childIdentityFromEnv(process.env);
|
|
34
111
|
if (child !== undefined) {
|
|
@@ -41,19 +118,64 @@ export default function piSubagentsExtension(pi) {
|
|
|
41
118
|
const context = runtime.extension;
|
|
42
119
|
const parentSessionId = context.sessionManager.getSessionId();
|
|
43
120
|
const registry = createSubagentRegistry({ parentSessionId });
|
|
44
|
-
const tokens = createRuntimeTokenStore();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
121
|
+
const tokens = createRuntimeTokenStore({ parentSessionId, diagnose });
|
|
122
|
+
// Presentation is chosen once per parent session: herdr first, then cmux, and a parent with
|
|
123
|
+
// neither runs every child in the background. Successful selection stays silent; only actual
|
|
124
|
+
// failures and fallback diagnostics are reported by the launch path.
|
|
125
|
+
const ownerId = parentSessionId;
|
|
126
|
+
const ownsAttachment = (identity) => identity.createdBy === ownerId;
|
|
127
|
+
const hostSelection = await selectHostAdapter({
|
|
128
|
+
adapters: {
|
|
129
|
+
herdr: createHerdrHostAdapter({ ownerId, ownsAttachment }),
|
|
130
|
+
cmux: createCmuxHostAdapter({ ownerId, ownsAttachment }),
|
|
53
131
|
},
|
|
54
|
-
};
|
|
132
|
+
});
|
|
133
|
+
const delivery = getBackgroundDelivery(pi);
|
|
134
|
+
const channel = createParentChannel(pi, {
|
|
135
|
+
isIdle: () => context.isIdle(),
|
|
136
|
+
delivery,
|
|
137
|
+
signal: runtime.signal,
|
|
138
|
+
});
|
|
139
|
+
runtime.resources.add("parent-channel", () => channel.dispose());
|
|
55
140
|
let manager;
|
|
56
|
-
|
|
141
|
+
// The parent listens; every child dials this socket, so the bridge connection is both the
|
|
142
|
+
// control plane and the only honest answer to "is this child still running?".
|
|
143
|
+
const server = new ChildBridgeServer({
|
|
144
|
+
endpoint: parentBridgeEndpoint(parentSessionId),
|
|
145
|
+
parentSessionId,
|
|
146
|
+
authorize: async (frame) => {
|
|
147
|
+
const expected = tokens.get(frame.runtimeIdentity);
|
|
148
|
+
if (expected === undefined) {
|
|
149
|
+
return `no bridge token is recorded for runtime ${frame.runtimeIdentity}`;
|
|
150
|
+
}
|
|
151
|
+
if (expected !== frame.token)
|
|
152
|
+
return "bridge token mismatch";
|
|
153
|
+
// The token says which runtime is calling; the registry says which child that runtime
|
|
154
|
+
// belongs to. Without this check a child could name another child's id and take over
|
|
155
|
+
// its control channel, so a hello that does not match its record is refused.
|
|
156
|
+
const record = await registry.get(frame.subagentId).catch(() => undefined);
|
|
157
|
+
if (record === undefined)
|
|
158
|
+
return `no recorded child ${frame.subagentId}`;
|
|
159
|
+
if (record.runtime?.runtimeIdentity !== frame.runtimeIdentity) {
|
|
160
|
+
return `runtime ${frame.runtimeIdentity} is not the recorded runtime of child ${frame.subagentId}`;
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
},
|
|
164
|
+
handleRequest: (request) => manager.handleChildRequest(request.childId, request.operation, request.payload),
|
|
165
|
+
diagnose,
|
|
166
|
+
});
|
|
167
|
+
await prepareRuntimeDirectory(diagnose);
|
|
168
|
+
await server.listen();
|
|
169
|
+
// Closing is awaited: the endpoint is unlinked after the server is closed, so a reload that
|
|
170
|
+
// started the next server first could have this close unlink the new socket path.
|
|
171
|
+
runtime.resources.add("subagent-bridge", () => server.close());
|
|
172
|
+
// A definition names skills the way Pi lists them, so the parent's loaded skill set is
|
|
173
|
+
// the only name → path catalog; Pi hands it over before every prompt of this session.
|
|
174
|
+
let skillCatalog = [];
|
|
175
|
+
const unsubscribeSkillCatalog = pi.on("before_agent_start", (event) => {
|
|
176
|
+
skillCatalog = skillCatalogFromLoaded(event.systemPromptOptions.skills);
|
|
177
|
+
});
|
|
178
|
+
runtime.resources.add("subagent-skill-catalog", unsubscribeSkillCatalog);
|
|
57
179
|
manager = new SubagentManager({
|
|
58
180
|
parentSessionId,
|
|
59
181
|
registry,
|
|
@@ -67,10 +189,38 @@ export default function piSubagentsExtension(pi) {
|
|
|
67
189
|
if (!isThinkingLevel(thinking)) {
|
|
68
190
|
throw new Error(`Unsupported parent thinking level: ${thinking}`);
|
|
69
191
|
}
|
|
192
|
+
const records = await registry.list().catch(() => []);
|
|
193
|
+
const existingSubagentIds = records.map((record) => record.subagentId);
|
|
194
|
+
let referencedSessionPath;
|
|
195
|
+
const forkTarget = input.forkFrom?.trim();
|
|
196
|
+
if (forkTarget !== undefined && forkTarget !== "") {
|
|
197
|
+
try {
|
|
198
|
+
if (forkTarget === "current") {
|
|
199
|
+
throw new Error("forkFrom cannot be 'current'. Use 'parent' to reference the parent session or specify a subagent id like 'agent-1'.");
|
|
200
|
+
}
|
|
201
|
+
const parentSessionFile = context.sessionManager.getSessionFile();
|
|
202
|
+
referencedSessionPath = await resolveForkSessionReference({
|
|
203
|
+
forkFrom: forkTarget,
|
|
204
|
+
...(parentSessionFile === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { parentSessionPath: parentSessionFile }),
|
|
207
|
+
registry,
|
|
208
|
+
onWarning: (message) => context.ui.notify(message, "warning"),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
context.ui.notify(`Referencing session failed: ${errorMessage(error)}; starting fresh session without reference`, "warning");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
70
215
|
return resolveSubagentLaunch({
|
|
71
216
|
input,
|
|
72
217
|
cwd: context.cwd,
|
|
73
218
|
modelRegistry: context.modelRegistry,
|
|
219
|
+
skillCatalog,
|
|
220
|
+
existingSubagentIds,
|
|
221
|
+
enforceEnabled: true,
|
|
222
|
+
onWarning: (message) => context.ui.notify(message, "warning"),
|
|
223
|
+
...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
|
|
74
224
|
parent: {
|
|
75
225
|
model: { provider: model.provider, id: model.id },
|
|
76
226
|
thinking,
|
|
@@ -80,59 +230,106 @@ export default function piSubagentsExtension(pi) {
|
|
|
80
230
|
bootstrap(input) {
|
|
81
231
|
return persistSubagentIntent({ registry, ...input });
|
|
82
232
|
},
|
|
233
|
+
bridge: server,
|
|
83
234
|
launch(record) {
|
|
84
|
-
return
|
|
235
|
+
return launchChild({
|
|
85
236
|
registry,
|
|
86
237
|
record,
|
|
87
238
|
tokens,
|
|
239
|
+
waitForBridge: (childId, signal) => server.waitForConnection(childId, {
|
|
240
|
+
timeoutMs: ADOPT_WINDOW_MS,
|
|
241
|
+
...(signal === undefined ? {} : { signal }),
|
|
242
|
+
}),
|
|
88
243
|
signal: runtime.signal,
|
|
244
|
+
diagnose,
|
|
89
245
|
});
|
|
90
246
|
},
|
|
247
|
+
presentation: {
|
|
248
|
+
reason: hostSelection.reason,
|
|
249
|
+
...(hostSelection.available
|
|
250
|
+
? {
|
|
251
|
+
openPanel: (record) => openChildPanel({
|
|
252
|
+
registry,
|
|
253
|
+
record,
|
|
254
|
+
tokens,
|
|
255
|
+
host: hostSelection.adapter,
|
|
256
|
+
waitForBridge: (childId, signal) => server.waitForConnection(childId, {
|
|
257
|
+
timeoutMs: ADOPT_WINDOW_MS,
|
|
258
|
+
...(signal === undefined ? {} : { signal }),
|
|
259
|
+
}),
|
|
260
|
+
signal: runtime.signal,
|
|
261
|
+
diagnose,
|
|
262
|
+
}),
|
|
263
|
+
}
|
|
264
|
+
: {}),
|
|
265
|
+
},
|
|
266
|
+
// Adoption is a wait, never a spawn: a child that survived this parent reconnects on
|
|
267
|
+
// its own, and one that did not stays recorded until a send starts it again.
|
|
91
268
|
connect(record) {
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
record,
|
|
95
|
-
tokens,
|
|
269
|
+
return server.waitForConnection(record.subagentId, {
|
|
270
|
+
timeoutMs: ADOPT_WINDOW_MS,
|
|
96
271
|
signal: runtime.signal,
|
|
97
272
|
});
|
|
98
273
|
},
|
|
99
|
-
attachHost: {
|
|
100
|
-
select(preferredHost) {
|
|
101
|
-
return selectHostAdapter({
|
|
102
|
-
adapters: {
|
|
103
|
-
herdr: createHerdrHostAdapter({
|
|
104
|
-
ownerId: parentSessionId,
|
|
105
|
-
ownsAttachment,
|
|
106
|
-
}),
|
|
107
|
-
cmux: createCmuxHostAdapter({
|
|
108
|
-
ownerId: parentSessionId,
|
|
109
|
-
ownsAttachment,
|
|
110
|
-
}),
|
|
111
|
-
},
|
|
112
|
-
...(preferredHost === undefined ? {} : { preferredHost }),
|
|
113
|
-
});
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
274
|
});
|
|
117
|
-
const
|
|
275
|
+
const unregisterWork = delivery.registerSource({
|
|
276
|
+
activeCount: () => manager.activeCount,
|
|
277
|
+
onChange: (listener) => manager.onChange(listener),
|
|
278
|
+
});
|
|
279
|
+
runtime.resources.add("subagent-background-work", unregisterWork);
|
|
280
|
+
// Recovery must never take the extension down with it: a registry this version cannot
|
|
281
|
+
// read (for example records written by an older release) is reported in the session and
|
|
282
|
+
// the user is told how to reset it.
|
|
283
|
+
const recovery = await manager.recover().catch((error) => ({
|
|
284
|
+
recovered: [],
|
|
285
|
+
failures: [
|
|
286
|
+
{
|
|
287
|
+
childId: "registry",
|
|
288
|
+
reason: `${errorMessage(error)}. Delete the stale records (or the whole registry file) to reset this parent session's subagent state.`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
}));
|
|
118
292
|
if (recovery.failures.length > 0) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.map((failure) => `- ${failure.childId}: ${failure.reason}`)
|
|
123
|
-
.join("\n")}`,
|
|
124
|
-
display: true,
|
|
125
|
-
details: recovery.failures,
|
|
126
|
-
}, { triggerTurn: false });
|
|
293
|
+
context.ui.notify(`Subagent recovery failed:\n${recovery.failures
|
|
294
|
+
.map((failure) => `- ${failure.childId}: ${failure.reason}`)
|
|
295
|
+
.join("\n")}`, "error");
|
|
127
296
|
}
|
|
128
297
|
registerParentTools(pi, manager);
|
|
298
|
+
if (typeof pi.registerMarkdownTransformer === "function") {
|
|
299
|
+
pi.registerMarkdownTransformer(transformSubagentsMarkdown);
|
|
300
|
+
}
|
|
301
|
+
const activation = registerInteractiveToolActivation({
|
|
302
|
+
pi,
|
|
303
|
+
manager,
|
|
304
|
+
context,
|
|
305
|
+
});
|
|
306
|
+
runtime.resources.add("interactive-tool-activation", () => activation.dispose());
|
|
129
307
|
registerParentCommands(pi, manager);
|
|
308
|
+
void waitForService(pi, TASK_REGISTRY_SERVICE_KEY, { signal: runtime.signal })
|
|
309
|
+
.then((taskRegistry) => {
|
|
310
|
+
if (runtime.signal.aborted)
|
|
311
|
+
return;
|
|
312
|
+
const unbind = manager.bindTaskRegistry(taskRegistry);
|
|
313
|
+
runtime.resources.add("subagent-task-registry", unbind);
|
|
314
|
+
})
|
|
315
|
+
.catch((error) => {
|
|
316
|
+
diagnose(`Task registry service unavailable for subagents: ${errorMessage(error)}`);
|
|
317
|
+
});
|
|
130
318
|
const widget = createSubagentWidget(pi, context, runtime.signal);
|
|
131
319
|
const refreshWidget = () => {
|
|
132
|
-
|
|
320
|
+
if (runtime.signal.aborted)
|
|
321
|
+
return;
|
|
322
|
+
void manager
|
|
323
|
+
.list()
|
|
324
|
+
.then((children) => {
|
|
325
|
+
if (runtime.signal.aborted)
|
|
326
|
+
return;
|
|
327
|
+
widget?.refresh(children);
|
|
328
|
+
})
|
|
329
|
+
.catch(() => { });
|
|
133
330
|
};
|
|
134
331
|
const unsubscribe = manager.onChange(refreshWidget);
|
|
135
|
-
const unbindStatus = bindParentStatus(pi, context, manager);
|
|
332
|
+
const unbindStatus = bindParentStatus(pi, context, manager, runtime.signal);
|
|
136
333
|
refreshWidget();
|
|
137
334
|
runtime.resources.add("subagent-widget", () => {
|
|
138
335
|
unsubscribe();
|
package/dist/host-adapter.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { LaunchSpec } from "./launch-spec.js";
|
|
2
2
|
export type HostKind = "herdr" | "cmux";
|
|
3
|
-
/** Where a host command failed: session/env, layout, or the launch command itself. */
|
|
4
|
-
export type HostFailureClass = "environment" | "pane" | "command";
|
|
5
3
|
export interface HostCommandResult {
|
|
6
4
|
readonly stdout: string;
|
|
7
5
|
readonly stderr: string;
|
|
@@ -22,15 +20,11 @@ export type HostSelection = {
|
|
|
22
20
|
readonly available: true;
|
|
23
21
|
readonly selectedHost: HostKind;
|
|
24
22
|
readonly adapter: HostAdapter;
|
|
25
|
-
readonly explicit: boolean;
|
|
26
23
|
readonly reason: string;
|
|
27
|
-
readonly attempts: readonly HostCapability[];
|
|
28
24
|
} | {
|
|
29
25
|
readonly available: false;
|
|
30
26
|
readonly selectedHost: null;
|
|
31
|
-
readonly explicit: boolean;
|
|
32
27
|
readonly reason: string;
|
|
33
|
-
readonly attempts: readonly HostCapability[];
|
|
34
28
|
};
|
|
35
29
|
export interface HostAttachmentIdentity {
|
|
36
30
|
readonly host: HostKind;
|
|
@@ -41,18 +35,27 @@ export interface HostObservation {
|
|
|
41
35
|
readonly identity: HostAttachmentIdentity;
|
|
42
36
|
readonly alive: boolean;
|
|
43
37
|
readonly known: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether a human is looking at this panel. Absent when the host cannot say, which includes
|
|
40
|
+
* both hosts without the capability and a lookup that failed just now; the caller must treat
|
|
41
|
+
* an absent answer as "do not touch", because an unfocused answer lets it close the panel.
|
|
42
|
+
*/
|
|
43
|
+
readonly focused?: boolean;
|
|
44
44
|
readonly detail: string;
|
|
45
45
|
}
|
|
46
46
|
export interface HostAttachment {
|
|
47
47
|
readonly identity: HostAttachmentIdentity;
|
|
48
48
|
readonly launch: HostCommandResult;
|
|
49
|
+
/** True when `observe()` can report focus; idle reclaim needs it and fails open without it. */
|
|
50
|
+
readonly reportsFocus: boolean;
|
|
49
51
|
observe(): Promise<HostObservation>;
|
|
50
52
|
cleanup(): Promise<HostCommandResult>;
|
|
51
53
|
}
|
|
52
54
|
export interface HostAdapter {
|
|
53
55
|
readonly kind: HostKind;
|
|
54
56
|
probe(): Promise<HostCapability>;
|
|
55
|
-
|
|
57
|
+
/** Opens the child's panel and runs the LaunchSpec in it. */
|
|
58
|
+
open(spec: LaunchSpec): Promise<HostAttachment>;
|
|
56
59
|
}
|
|
57
60
|
export interface SelectHostAdapterOptions {
|
|
58
61
|
readonly adapters: Readonly<Record<HostKind, HostAdapter>>;
|
|
@@ -60,17 +63,39 @@ export interface SelectHostAdapterOptions {
|
|
|
60
63
|
}
|
|
61
64
|
/** Selects a presentation host visibly; an explicit unavailable host never falls back. */
|
|
62
65
|
export declare function selectHostAdapter(options: SelectHostAdapterOptions): Promise<HostSelection>;
|
|
63
|
-
export declare class HostCommandError extends Error {
|
|
64
|
-
readonly kind: HostFailureClass;
|
|
65
|
-
readonly result: HostCommandResult;
|
|
66
|
-
constructor(kind: HostFailureClass, message: string, result: HostCommandResult);
|
|
67
|
-
}
|
|
68
66
|
export declare const systemHostCommandRunner: HostCommandRunner;
|
|
69
67
|
export interface HostAdapterOptions {
|
|
70
68
|
readonly runner?: HostCommandRunner;
|
|
71
69
|
readonly ownerId: string;
|
|
72
70
|
readonly ownsAttachment: (identity: HostAttachmentIdentity) => boolean;
|
|
73
71
|
readonly timeoutMs?: number;
|
|
72
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
74
73
|
}
|
|
75
74
|
export declare function createHerdrHostAdapter(options: HostAdapterOptions): HostAdapter;
|
|
76
75
|
export declare function createCmuxHostAdapter(options: HostAdapterOptions): HostAdapter;
|
|
76
|
+
/**
|
|
77
|
+
* Herdr tab creation carries cwd, env, label, and explicit workspace id matching the parent
|
|
78
|
+
* process, ensuring the child tab spawns in the process's own workspace rather than whatever
|
|
79
|
+
* workspace happens to have user focus.
|
|
80
|
+
*/
|
|
81
|
+
export declare function herdrTabCreateArgs(spec: LaunchSpec, workspaceId?: string): string[];
|
|
82
|
+
export declare function parseHerdrWorkspaceId(output: string): string | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Resolves the workspace id of the current process so new tabs open in the same workspace.
|
|
85
|
+
* Reads environment variables first, falling back to herdr pane queries.
|
|
86
|
+
*/
|
|
87
|
+
export declare function resolveHerdrProcessWorkspaceId(runner: HostCommandRunner, timeoutMs: number, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
|
|
88
|
+
/**
|
|
89
|
+
* Focus is answered from two listings: `tab list` says whether the child's tab is focused and
|
|
90
|
+
* which workspace owns it, `workspace list` says whether that workspace is the rendered one. A
|
|
91
|
+
* child is being watched only when both are focused. Anything missing — a failed listing, an absent
|
|
92
|
+
* field, a tab that is not there — is unknown, never "unfocused", because the caller closes a panel
|
|
93
|
+
* on an unfocused answer.
|
|
94
|
+
*/
|
|
95
|
+
export declare function herdrPanelFocused(tabList: string, workspaceList: string, tabId: string): boolean | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Reads the pane's process state. A pane that no longer exists is a definite "not alive" — that is
|
|
98
|
+
* how a closed panel looks — while a query that never ran or failed for any other reason is
|
|
99
|
+
* unknown, because the caller closes the panel on "not alive".
|
|
100
|
+
*/
|
|
101
|
+
export declare function herdrPaneAlive(result: HostCommandResult): boolean | undefined;
|