@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.
Files changed (65) hide show
  1. package/README.md +45 -137
  2. package/dist/agent-resolver.d.ts +20 -0
  3. package/dist/agent-resolver.js +155 -21
  4. package/dist/bridge-client.d.ts +55 -0
  5. package/dist/bridge-client.js +453 -0
  6. package/dist/bridge-server.d.ts +72 -0
  7. package/dist/bridge-server.js +490 -0
  8. package/dist/builtin-agents.d.ts +24 -0
  9. package/dist/builtin-agents.js +152 -0
  10. package/dist/child-bridge.d.ts +31 -15
  11. package/dist/child-bridge.js +95 -117
  12. package/dist/child-control.d.ts +58 -0
  13. package/dist/child-control.js +176 -0
  14. package/dist/child-process.d.ts +21 -0
  15. package/dist/child-process.js +93 -0
  16. package/dist/commands.d.ts +2 -2
  17. package/dist/commands.js +52 -60
  18. package/dist/diagnostics.d.ts +0 -2
  19. package/dist/diagnostics.js +0 -8
  20. package/dist/domain.d.ts +57 -27
  21. package/dist/domain.js +60 -11
  22. package/dist/extension.d.ts +26 -1
  23. package/dist/extension.js +243 -46
  24. package/dist/host-adapter.d.ts +37 -12
  25. package/dist/host-adapter.js +214 -63
  26. package/dist/json-lines.js +2 -1
  27. package/dist/launch-spec.d.ts +8 -6
  28. package/dist/launch-spec.js +61 -18
  29. package/dist/manager.d.ts +93 -49
  30. package/dist/manager.js +1175 -528
  31. package/dist/markdown.d.ts +8 -0
  32. package/dist/markdown.js +21 -0
  33. package/dist/protocol.d.ts +33 -52
  34. package/dist/protocol.js +41 -52
  35. package/dist/registry.d.ts +3 -8
  36. package/dist/registry.js +41 -163
  37. package/dist/rpc-events.d.ts +3 -5
  38. package/dist/rpc-events.js +5 -9
  39. package/dist/runtime.d.ts +66 -11
  40. package/dist/runtime.js +182 -208
  41. package/dist/session-bootstrap.d.ts +23 -7
  42. package/dist/session-bootstrap.js +79 -15
  43. package/dist/session-fork.d.ts +16 -0
  44. package/dist/session-fork.js +67 -0
  45. package/dist/state.d.ts +7 -0
  46. package/dist/state.js +60 -25
  47. package/dist/tool-activation.d.ts +18 -0
  48. package/dist/tool-activation.js +227 -0
  49. package/dist/tools.d.ts +6 -3
  50. package/dist/tools.js +213 -72
  51. package/dist/widget.d.ts +4 -8
  52. package/dist/widget.js +55 -64
  53. package/package.json +51 -52
  54. package/dist/child-nudge.d.ts +0 -24
  55. package/dist/child-nudge.js +0 -102
  56. package/dist/connector.d.ts +0 -53
  57. package/dist/connector.js +0 -283
  58. package/dist/pause-gate.d.ts +0 -14
  59. package/dist/pause-gate.js +0 -64
  60. package/dist/rpc-adapter.d.ts +0 -40
  61. package/dist/rpc-adapter.js +0 -194
  62. package/dist/runner-entry.d.ts +0 -24
  63. package/dist/runner-entry.js +0 -166
  64. package/dist/runner.d.ts +0 -52
  65. 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, launchDetachedRunner, recoverDetachedRunner } from "./runtime.js";
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
- const channel = {
46
- async deliver(report) {
47
- pi.sendMessage({
48
- customType: "pi-subagent-report",
49
- content: `[Subagent ${report.agent} ${report.childId}: ${report.reason}]\n${report.message}`,
50
- display: true,
51
- details: report,
52
- }, { triggerTurn: true, deliverAs: "nextTurn" });
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
- const ownsAttachment = (identity) => manager.ownsHostAttachment(identity);
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 launchDetachedRunner({
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 recoverDetachedRunner({
93
- registry,
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 recovery = await manager.recover();
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
- pi.sendMessage({
120
- customType: "pi-subagent-recovery-failure",
121
- content: `Subagent recovery failed:\n${recovery.failures
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
- void manager.list().then((children) => widget?.refresh(children));
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();
@@ -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
- attach(spec: LaunchSpec): Promise<HostAttachment>;
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;