@narumitw/pi-subagents 3.3.3 → 3.4.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.
Files changed (42) hide show
  1. package/README.md +73 -24
  2. package/dist/child-communication-bridge.ts +3 -2
  3. package/dist/child-communication-bridge.ts.map +1 -1
  4. package/dist/child-readiness-probe.ts +114 -17
  5. package/dist/child-readiness-probe.ts.map +2 -2
  6. package/dist/chunks/{chunk-FY5HIU6Y.ts → chunk-5OXDVXXO.ts} +1 -1
  7. package/dist/chunks/{chunk-FY5HIU6Y.ts.map → chunk-5OXDVXXO.ts.map} +1 -1
  8. package/dist/chunks/chunk-B3RWNGDX.ts +192 -0
  9. package/dist/chunks/chunk-B3RWNGDX.ts.map +7 -0
  10. package/dist/chunks/{chunk-ZOAFWLDJ.ts → chunk-PRIDTAJL.ts} +4 -16
  11. package/dist/chunks/chunk-PRIDTAJL.ts.map +7 -0
  12. package/dist/chunks/{chunk-YMVSJ5GE.ts → chunk-YV2P6XJX.ts} +46 -17
  13. package/dist/chunks/chunk-YV2P6XJX.ts.map +7 -0
  14. package/dist/chunks/{menu-KAIUEWL7.ts → menu-2UH4ICLL.ts} +74 -3
  15. package/dist/chunks/menu-2UH4ICLL.ts.map +7 -0
  16. package/dist/index.ts +553 -292
  17. package/dist/index.ts.map +3 -3
  18. package/docs/attachments.md +9 -3
  19. package/docs/design-principles.md +3 -1
  20. package/docs/tools.md +15 -1
  21. package/package.json +2 -2
  22. package/src/broker-credentials.ts +44 -13
  23. package/src/child-communication-tools.ts +2 -19
  24. package/src/child-readiness-probe.ts +144 -18
  25. package/src/child-readiness.ts +118 -0
  26. package/src/menu.ts +82 -3
  27. package/src/message-broker.ts +23 -10
  28. package/src/process.ts +59 -167
  29. package/src/resource-attachments.ts +1 -3
  30. package/src/rpc-line-decoder.ts +61 -0
  31. package/src/runtime.ts +21 -9
  32. package/src/settings.ts +2 -2
  33. package/src/subagents.ts +110 -18
  34. package/src/tools.ts +83 -37
  35. package/src/types.ts +1 -0
  36. package/src/widget-renderer.ts +119 -0
  37. package/src/widget.ts +245 -121
  38. package/dist/chunks/chunk-3FCHXUKZ.ts +0 -98
  39. package/dist/chunks/chunk-3FCHXUKZ.ts.map +0 -7
  40. package/dist/chunks/chunk-YMVSJ5GE.ts.map +0 -7
  41. package/dist/chunks/chunk-ZOAFWLDJ.ts.map +0 -7
  42. package/dist/chunks/menu-KAIUEWL7.ts.map +0 -7
package/dist/index.ts CHANGED
@@ -4,37 +4,43 @@ import {
4
4
  CHILD_COMMUNICATION_TOOL_NAMES,
5
5
  prepareTimeoutArguments,
6
6
  resolveTimeoutMs
7
- } from "./chunks/chunk-ZOAFWLDJ.ts";
7
+ } from "./chunks/chunk-PRIDTAJL.ts";
8
8
  import {
9
9
  addAttachmentIgnoreRules,
10
+ assertChildModelPair,
11
+ assertChildModelState,
12
+ assertTrustedChildResources,
10
13
  isAbortError,
11
14
  isRecord,
12
15
  isWithin,
16
+ parseReadinessFrame,
13
17
  realpath,
14
18
  throwIfAttachmentAborted,
15
19
  toPosixPath,
16
20
  toolSourceId
17
- } from "./chunks/chunk-3FCHXUKZ.ts";
21
+ } from "./chunks/chunk-B3RWNGDX.ts";
18
22
  import {
19
23
  BROKER_CREDENTIAL_FD,
20
24
  CHILD_READINESS_FD,
21
- CHILD_READINESS_FD_ENV,
25
+ MAX_BOOTSTRAP_BYTES,
22
26
  MAX_IDENTIFIER_LENGTH,
23
27
  MAX_MESSAGE_BYTES,
24
28
  MAX_READINESS_FRAME_BYTES,
25
29
  MessageBroker,
26
30
  assertChildBootstrapCapacity,
27
31
  childBootstrapEnvironment,
32
+ optionalIdentifier,
33
+ requiredIdentifier,
28
34
  sanitizeTerminalText,
29
35
  serializeChildBootstrap,
30
36
  validateMessage
31
- } from "./chunks/chunk-YMVSJ5GE.ts";
37
+ } from "./chunks/chunk-YV2P6XJX.ts";
32
38
  import {
33
39
  CHILD_CORE_TOOL_NAMES,
34
40
  DEFAULT_SUBAGENT_TOOLS,
35
41
  SUBAGENT_THINKING_LEVELS,
36
42
  TERMINAL_JOB_STATES
37
- } from "./chunks/chunk-FY5HIU6Y.ts";
43
+ } from "./chunks/chunk-5OXDVXXO.ts";
38
44
 
39
45
  // src/display-text.ts
40
46
  function sanitizeSubagentDisplayText(value) {
@@ -102,8 +108,8 @@ import { dirname, join } from "node:path";
102
108
  import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
103
109
  var DEFAULT_SUBAGENT_SETTINGS = Object.freeze({
104
110
  showWidget: true,
105
- showJobId: true,
106
- showTools: true,
111
+ showJobId: false,
112
+ showTools: false,
107
113
  showTiming: true
108
114
  });
109
115
  var SETTINGS_KEYS = Object.keys(DEFAULT_SUBAGENT_SETTINGS);
@@ -263,10 +269,68 @@ import * as path from "node:path";
263
269
  import { StringDecoder } from "node:string_decoder";
264
270
  import { fileURLToPath } from "node:url";
265
271
  import { getPackageDir } from "@earendil-works/pi-coding-agent";
272
+
273
+ // src/rpc-line-decoder.ts
274
+ var MAX_EVENT_LINE_BYTES = 256 * 1024;
275
+ var JsonLineDecoder = class {
276
+ constructor(onValue, onMalformed) {
277
+ this.onValue = onValue;
278
+ this.onMalformed = onMalformed;
279
+ }
280
+ onValue;
281
+ onMalformed;
282
+ fragments = [];
283
+ bytes = 0;
284
+ dropping = false;
285
+ decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
286
+ push(chunk) {
287
+ const data = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
288
+ let offset = 0;
289
+ while (offset < data.length) {
290
+ const newline = data.indexOf(10, offset);
291
+ const end = newline < 0 ? data.length : newline;
292
+ const fragment = data.subarray(offset, end);
293
+ if (!this.dropping) {
294
+ if (this.bytes + fragment.length > MAX_EVENT_LINE_BYTES) {
295
+ this.clear();
296
+ this.dropping = true;
297
+ this.onMalformed();
298
+ } else if (fragment.length > 0) {
299
+ this.fragments.push(Buffer.from(fragment));
300
+ this.bytes += fragment.length;
301
+ }
302
+ }
303
+ if (newline < 0) return;
304
+ this.complete();
305
+ offset = newline + 1;
306
+ }
307
+ }
308
+ finish() {
309
+ this.complete();
310
+ }
311
+ complete() {
312
+ const dropping = this.dropping;
313
+ const line = Buffer.concat(this.fragments, this.bytes);
314
+ this.clear();
315
+ this.dropping = false;
316
+ if (dropping || line.length === 0) return;
317
+ try {
318
+ const text = this.decoder.decode(line);
319
+ if (text.trim()) this.onValue(JSON.parse(text));
320
+ } catch {
321
+ this.onMalformed();
322
+ }
323
+ }
324
+ clear() {
325
+ this.fragments = [];
326
+ this.bytes = 0;
327
+ }
328
+ };
329
+
330
+ // src/process.ts
266
331
  var CORE_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
267
332
  var MAX_OUTPUT_BYTES = 32 * 1024;
268
333
  var MAX_ERROR_BYTES = 8 * 1024;
269
- var MAX_EVENT_LINE_BYTES = 256 * 1024;
270
334
  var RPC_RESPONSE_TIMEOUT_MS = 3e4;
271
335
  var KILL_GRACE_MS = 1e3;
272
336
  var MAX_WINDOWS_COMMAND_LINE_CODE_UNITS = 32767;
@@ -302,15 +366,9 @@ function buildPiArgs(request) {
302
366
  ];
303
367
  if (request.tools.includes("codemode")) args.push("-e", "builtin:codemode");
304
368
  for (const extension of request.extensions) args.push("-e", extension.path);
305
- if (requiresReadinessAttestation(request)) args.push("-e", childReadinessProbePath());
369
+ args.push("-e", childReadinessProbePath());
306
370
  for (const skill of request.skills) args.push("--skill", skill);
307
- args.push(
308
- "--model",
309
- request.model,
310
- "--thinking",
311
- request.thinkingLevel,
312
- request.projectTrusted ? "--approve" : "--no-approve"
313
- );
371
+ args.push("--thinking", request.thinkingLevel, request.projectTrusted ? "--approve" : "--no-approve");
314
372
  args.push("--tools", selectedChildTools(request).join(","));
315
373
  return args;
316
374
  }
@@ -346,9 +404,6 @@ function childCommunicationBridgePath() {
346
404
  function childReadinessProbePath() {
347
405
  return fileURLToPath(new URL("./child-readiness-probe.ts", import.meta.url));
348
406
  }
349
- function requiresReadinessAttestation(request) {
350
- return request.tools.includes("codemode") || request.extensions.length > 0;
351
- }
352
407
  function selectedChildTools(request) {
353
408
  return [
354
409
  .../* @__PURE__ */ new Set([
@@ -361,8 +416,8 @@ function selectedChildTools(request) {
361
416
  async function executeProcess(invocation, request) {
362
417
  const timeoutMs = resolveTimeoutMs(request.timeout);
363
418
  const expectedTools = selectedChildTools(request);
364
- const expectReadiness = requiresReadinessAttestation(request);
365
- if (expectReadiness) assertChildBootstrapCapacity(expectedTools);
419
+ const selection = { provider: request.provider, model: request.model, thinkingLevel: request.thinkingLevel };
420
+ assertChildBootstrapCapacity(expectedTools, selection);
366
421
  let latestOutput = "";
367
422
  let terminalOutput;
368
423
  let terminalStopReason;
@@ -397,7 +452,7 @@ async function executeProcess(invocation, request) {
397
452
  };
398
453
  let malformedEvents = 0;
399
454
  let rpcCounter = 0;
400
- let attachmentStartupPending = expectReadiness;
455
+ let attachmentReady = false;
401
456
  let attachmentStartupError;
402
457
  const pendingCommands = /* @__PURE__ */ new Map();
403
458
  let rpcInputError;
@@ -436,7 +491,9 @@ async function executeProcess(invocation, request) {
436
491
  const decoder = new JsonLineDecoder(
437
492
  (value) => {
438
493
  const event = value;
439
- if (expectReadiness && event.type === "extension_error" && (event.event === "session_start" || event.event === "resources_discover") && !attachmentStartupError) {
494
+ if (event.type === "extension_error" && ["session_start", "resources_discover", "model_select", "thinking_level_select"].includes(
495
+ String(event.event)
496
+ ) && !attachmentStartupError) {
440
497
  const detail = typeof event.error === "string" && event.error ? redactAttachmentPaths(sanitizeTerminalText(event.error), request) : "Unknown error.";
441
498
  attachmentStartupError = `Subagent attachment startup failed during ${event.event}: ${detail}`;
442
499
  return;
@@ -444,7 +501,9 @@ async function executeProcess(invocation, request) {
444
501
  if (event.type === "response" && typeof event.id === "string") {
445
502
  const pending = pendingCommands.get(event.id);
446
503
  if (!pending) return;
447
- if (event.success === true) {
504
+ if (event.command !== pending.command) {
505
+ rejectPendingCommand(event.id, new Error("Subagent RPC response returned an invalid command."));
506
+ } else if (event.success === true) {
448
507
  resolvePendingCommand(event.id, event);
449
508
  } else {
450
509
  rejectPendingCommand(
@@ -483,8 +542,9 @@ async function executeProcess(invocation, request) {
483
542
  },
484
543
  () => {
485
544
  malformedEvents++;
486
- if (attachmentStartupPending && !attachmentStartupError) {
545
+ if (!attachmentReady && !attachmentStartupError) {
487
546
  attachmentStartupError = "Subagent attachment startup emitted malformed or oversized RPC output.";
547
+ failRpcInput(new Error(attachmentStartupError));
488
548
  }
489
549
  }
490
550
  );
@@ -498,10 +558,10 @@ async function executeProcess(invocation, request) {
498
558
  let timedOut = false;
499
559
  let completed = false;
500
560
  let ready = false;
501
- let attachmentReady = !expectReadiness;
502
561
  let promptStarted = false;
503
562
  let readinessPipe;
504
563
  let deadline;
564
+ let startupDeadline;
505
565
  let forceClose;
506
566
  let escalation;
507
567
  let termination;
@@ -512,6 +572,7 @@ async function executeProcess(invocation, request) {
512
572
  if (settled) return;
513
573
  settled = true;
514
574
  if (deadline) clearTimeout(deadline);
575
+ if (startupDeadline) clearTimeout(startupDeadline);
515
576
  if (forceClose) clearTimeout(forceClose);
516
577
  if (escalation) clearTimeout(escalation);
517
578
  request.signal.removeEventListener("abort", onAbort);
@@ -526,6 +587,8 @@ async function executeProcess(invocation, request) {
526
587
  const terminate = (code) => {
527
588
  if (settled || terminating) return;
528
589
  terminating = true;
590
+ rejectPendingCommands(new Error("Subagent RPC process is terminating."));
591
+ if (startupDeadline) clearTimeout(startupDeadline);
529
592
  if (deadline) {
530
593
  clearTimeout(deadline);
531
594
  deadline = void 0;
@@ -561,15 +624,14 @@ async function executeProcess(invocation, request) {
561
624
  try {
562
625
  const environment = {
563
626
  ...globalThis.process.env,
564
- ...childBootstrapEnvironment(expectReadiness),
627
+ ...childBootstrapEnvironment(true),
565
628
  PI_SUBAGENT_DEPTH: String((Number.parseInt(globalThis.process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1)
566
629
  };
567
- if (!expectReadiness) delete environment[CHILD_READINESS_FD_ENV];
568
630
  process2 = spawn(invocation.command, invocation.args, {
569
631
  cwd: request.cwd,
570
632
  detached: globalThis.process.platform !== "win32",
571
633
  shell: false,
572
- stdio: expectReadiness ? ["pipe", "pipe", "pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe", "pipe"],
634
+ stdio: ["pipe", "pipe", "pipe", "pipe", "pipe"],
573
635
  env: environment
574
636
  });
575
637
  } catch (error2) {
@@ -626,8 +688,10 @@ async function executeProcess(invocation, request) {
626
688
  });
627
689
  };
628
690
  const startPrompt = () => {
629
- if (!spawned || !attachmentReady || promptStarted || settled || cancelled || terminating) return;
691
+ if (!spawned || !attachmentReady || promptStarted || settled || cancelled || terminating || request.signal.aborted)
692
+ return;
630
693
  promptStarted = true;
694
+ if (startupDeadline) clearTimeout(startupDeadline);
631
695
  void sendCommand(
632
696
  { type: "prompt", message: `Task: ${request.task}` },
633
697
  () => {
@@ -664,11 +728,18 @@ async function executeProcess(invocation, request) {
664
728
  };
665
729
  const failReadiness = (message) => {
666
730
  if (settled || terminating || attachmentReady) return;
667
- errorMessage = truncateText(redactAttachmentPaths(message, request), MAX_ERROR_BYTES).text;
731
+ const limited = truncateText(redactAttachmentPaths(sanitizeTerminalText(message), request), MAX_ERROR_BYTES);
732
+ errorMessage = limited.text;
733
+ truncated ||= limited.truncated;
668
734
  terminate(1);
669
735
  };
670
736
  const confirmAttachmentStartup = () => {
671
- void sendCommand({ type: "get_state" }, void 0, request.signal).then(async () => {
737
+ void sendCommand(
738
+ { type: "get_state" },
739
+ (response) => assertChildModelState(response.data, request),
740
+ request.signal
741
+ ).then(async () => {
742
+ if (settled || terminating || request.signal.aborted) return;
672
743
  if (attachmentStartupError) throw new Error(attachmentStartupError);
673
744
  if (!request.projectTrusted) {
674
745
  await sendCommand(
@@ -677,8 +748,8 @@ async function executeProcess(invocation, request) {
677
748
  request.signal
678
749
  );
679
750
  }
751
+ if (settled || terminating || request.signal.aborted) return;
680
752
  if (attachmentStartupError) throw new Error(attachmentStartupError);
681
- attachmentStartupPending = false;
682
753
  attachmentReady = true;
683
754
  startPrompt();
684
755
  }).catch((error2) => {
@@ -691,7 +762,9 @@ async function executeProcess(invocation, request) {
691
762
  spawned = true;
692
763
  startPrompt();
693
764
  });
694
- if (expectReadiness) {
765
+ {
766
+ startupDeadline = setTimeout(() => failReadiness("Subagent child readiness timed out."), RPC_RESPONSE_TIMEOUT_MS);
767
+ startupDeadline.unref();
695
768
  const candidate = process2.stdio[CHILD_READINESS_FD];
696
769
  if (!candidate || !("on" in candidate)) {
697
770
  errorMessage = "Subagent readiness pipe is unavailable.";
@@ -714,6 +787,7 @@ async function executeProcess(invocation, request) {
714
787
  let frame;
715
788
  try {
716
789
  frame = parseReadinessFrame(buffer, expectedTools.length);
790
+ if (frame.ok) assertChildModelPair(frame.model, request);
717
791
  } catch (error2) {
718
792
  failReadiness(error2 instanceof Error ? error2.message : String(error2));
719
793
  return;
@@ -779,7 +853,8 @@ async function executeProcess(invocation, request) {
779
853
  credentialPipe.end(
780
854
  serializeChildBootstrap({
781
855
  communication: request.communication,
782
- expectedTools: expectReadiness ? expectedTools : []
856
+ expectedTools,
857
+ selection
783
858
  })
784
859
  );
785
860
  } catch {
@@ -920,62 +995,6 @@ function killImmediateChild(process2) {
920
995
  } catch {
921
996
  }
922
997
  }
923
- function assertTrustedChildResources(value, cwd) {
924
- const invalid = () => new Error("Subagent child resource attestation returned invalid commands.");
925
- if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid();
926
- const commands = value.commands;
927
- if (!Array.isArray(commands)) throw invalid();
928
- let canonicalCwd;
929
- try {
930
- canonicalCwd = fs2.realpathSync(cwd);
931
- } catch {
932
- throw invalid();
933
- }
934
- for (const command of commands) {
935
- if (!command || typeof command !== "object" || Array.isArray(command)) throw invalid();
936
- const entry = command;
937
- if (entry.source !== "skill" && entry.source !== "prompt") continue;
938
- const resourcePath = entry.sourceInfo?.path;
939
- if (typeof resourcePath !== "string" || !resourcePath) throw invalid();
940
- const lexicalPath = path.resolve(cwd, resourcePath);
941
- let canonicalPath;
942
- try {
943
- canonicalPath = fs2.realpathSync(lexicalPath);
944
- } catch {
945
- throw invalid();
946
- }
947
- if (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath)) {
948
- throw new Error("Subagent child cannot load project resources because the project is not trusted.");
949
- }
950
- }
951
- }
952
- function parseReadinessFrame(buffer, expectedToolCount) {
953
- if (buffer.byteLength === 0) {
954
- throw new Error("Subagent readiness pipe closed without a result.");
955
- }
956
- const text = buffer.toString("utf8");
957
- const newline = text.indexOf("\n");
958
- if (newline < 0 || newline !== text.length - 1) {
959
- throw new Error("Subagent readiness pipe returned an invalid frame.");
960
- }
961
- let value;
962
- try {
963
- value = JSON.parse(text.slice(0, newline));
964
- } catch {
965
- throw new Error("Subagent readiness pipe returned malformed JSON.");
966
- }
967
- if (!value || typeof value !== "object" || Array.isArray(value)) {
968
- throw new Error("Subagent readiness pipe returned an invalid result.");
969
- }
970
- const record = value;
971
- if (record.ok === true && Object.keys(record).length === 2 && Array.isArray(record.sources) && record.sources.length === expectedToolCount && record.sources.every((source) => typeof source === "string" && /^[a-f0-9]{64}$/u.test(source))) {
972
- return { ok: true, sources: record.sources };
973
- }
974
- if (record.ok === false && Object.keys(record).length === 2 && typeof record.error === "string" && record.error.length > 0 && Buffer.byteLength(record.error, "utf8") <= MAX_ERROR_BYTES) {
975
- return { ok: false, error: sanitizeTerminalText(record.error) };
976
- }
977
- throw new Error("Subagent readiness pipe returned an invalid result.");
978
- }
979
998
  function redactAttachmentPaths(value, request) {
980
999
  return redactAttachmentPathVariants(value, attachmentPathVariants(request));
981
1000
  }
@@ -1043,58 +1062,6 @@ ${bytes.subarray(bytes.length - Math.max(0, maxBytes - 18)).toString("utf8").rep
1043
1062
  truncated: true
1044
1063
  };
1045
1064
  }
1046
- var JsonLineDecoder = class {
1047
- constructor(onValue, onMalformed) {
1048
- this.onValue = onValue;
1049
- this.onMalformed = onMalformed;
1050
- }
1051
- onValue;
1052
- onMalformed;
1053
- buffer = "";
1054
- dropping = false;
1055
- decoder = new StringDecoder("utf8");
1056
- push(chunk) {
1057
- this.buffer += typeof chunk === "string" ? chunk : this.decoder.write(chunk);
1058
- this.drain(false);
1059
- }
1060
- finish() {
1061
- this.buffer += this.decoder.end();
1062
- this.drain(true);
1063
- this.buffer = "";
1064
- this.dropping = false;
1065
- }
1066
- drain(flush) {
1067
- while (true) {
1068
- const newline = this.buffer.indexOf("\n");
1069
- if (newline < 0) break;
1070
- const line = this.buffer.slice(0, newline).replace(/\r$/u, "");
1071
- this.buffer = this.buffer.slice(newline + 1);
1072
- if (this.dropping) {
1073
- this.dropping = false;
1074
- continue;
1075
- }
1076
- this.parse(line);
1077
- }
1078
- if (!flush && Buffer.byteLength(this.buffer, "utf8") > MAX_EVENT_LINE_BYTES) {
1079
- this.onMalformed();
1080
- this.buffer = "";
1081
- this.dropping = true;
1082
- }
1083
- if (flush && this.buffer && !this.dropping) this.parse(this.buffer.replace(/\r$/u, ""));
1084
- }
1085
- parse(line) {
1086
- if (!line.trim()) return;
1087
- if (Buffer.byteLength(line, "utf8") > MAX_EVENT_LINE_BYTES) {
1088
- this.onMalformed();
1089
- return;
1090
- }
1091
- try {
1092
- this.onValue(JSON.parse(line));
1093
- } catch {
1094
- this.onMalformed();
1095
- }
1096
- }
1097
- };
1098
1065
 
1099
1066
  // src/resource-attachments.ts
1100
1067
  import { existsSync, statSync as statSync3 } from "node:fs";
@@ -1442,9 +1409,7 @@ function resolveResourcePath(value, kind, cwd, canonicalCwd, projectTrusted) {
1442
1409
  if (!stats.isFile() && !stats.isDirectory()) {
1443
1410
  throw new Error(`Subagent ${kind} path must reference a file or directory.`);
1444
1411
  }
1445
- if (!projectTrusted && (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath))) {
1446
- throw new Error(`Subagent ${kind} cannot load a project path because the project is not trusted.`);
1447
- }
1412
+ assertProjectPathTrusted(lexicalPath, canonicalPath, kind, cwd, canonicalCwd, projectTrusted);
1448
1413
  return canonicalPath;
1449
1414
  }
1450
1415
  async function assertResolvableExtensionAttachment(extensionPath, cwd, canonicalCwd, projectTrusted, signal) {
@@ -1863,7 +1828,6 @@ var SubagentRuntime = class {
1863
1828
  now;
1864
1829
  counter = 0;
1865
1830
  generation = 0;
1866
- deliveryEnabled = false;
1867
1831
  sessionActive = false;
1868
1832
  omittedJobs = 0;
1869
1833
  jobListeners = /* @__PURE__ */ new Set();
@@ -1872,7 +1836,6 @@ var SubagentRuntime = class {
1872
1836
  this.generation++;
1873
1837
  this.jobs.clear();
1874
1838
  this.omittedJobs = 0;
1875
- this.deliveryEnabled = true;
1876
1839
  this.sessionActive = true;
1877
1840
  this.notifyJobsChanged();
1878
1841
  }
@@ -1889,7 +1852,9 @@ var SubagentRuntime = class {
1889
1852
  jobId: job.jobId,
1890
1853
  task: job.taskText,
1891
1854
  state: job.state,
1855
+ ...job.modelOverrides,
1892
1856
  thinkingLevel: job.thinkingLevel,
1857
+ defaultThinkingLevel: job.defaultThinkingLevel,
1893
1858
  elapsedMs: Math.max(0, now - (job.startedAt ?? job.createdAt)),
1894
1859
  ...job.timeout !== void 0 ? { timeout: job.timeout } : {},
1895
1860
  tools: [...job.tools]
@@ -1905,6 +1870,7 @@ var SubagentRuntime = class {
1905
1870
  if (active >= MAX_ACTIVE_JOBS) {
1906
1871
  throw new Error(`Active subagent job limit reached (${MAX_ACTIVE_JOBS}).`);
1907
1872
  }
1873
+ const { provider, model } = input;
1908
1874
  const jobId = `job_${this.now().toString(36)}_${(++this.counter).toString(36)}`;
1909
1875
  const communication = this.broker.issueCredentials({
1910
1876
  jobId,
@@ -1931,7 +1897,9 @@ var SubagentRuntime = class {
1931
1897
  controller,
1932
1898
  tools: effectiveTools,
1933
1899
  taskText: input.task,
1900
+ modelOverrides: { ...input.modelOverrides },
1934
1901
  thinkingLevel: input.thinkingLevel,
1902
+ defaultThinkingLevel: input.defaultThinkingLevel,
1935
1903
  terminal,
1936
1904
  resolveTerminal,
1937
1905
  controlReady,
@@ -1939,7 +1907,6 @@ var SubagentRuntime = class {
1939
1907
  rejectControl,
1940
1908
  sendQueue: Promise.resolve(),
1941
1909
  limitations: [],
1942
- deliverySent: false,
1943
1910
  generation: this.generation
1944
1911
  };
1945
1912
  this.jobs.set(jobId, job);
@@ -1966,7 +1933,8 @@ var SubagentRuntime = class {
1966
1933
  toolSources: Object.fromEntries(
1967
1934
  Object.entries(input.toolSources).map(([tool, sources]) => [tool, [...sources]])
1968
1935
  ),
1969
- model: input.model,
1936
+ provider,
1937
+ model,
1970
1938
  thinkingLevel: input.thinkingLevel,
1971
1939
  cwd: input.cwd,
1972
1940
  timeout: input.timeout,
@@ -2100,7 +2068,6 @@ var SubagentRuntime = class {
2100
2068
  }
2101
2069
  async shutdown() {
2102
2070
  if (!this.sessionActive) return;
2103
- this.deliveryEnabled = false;
2104
2071
  this.sessionActive = false;
2105
2072
  const active = [...this.jobs.values()].filter((job) => !isTerminal(job.state));
2106
2073
  await Promise.allSettled(
@@ -2154,8 +2121,7 @@ var SubagentRuntime = class {
2154
2121
  this.prune();
2155
2122
  }
2156
2123
  deliver(job) {
2157
- if (!this.deliveryEnabled || job.deliverySent || job.generation !== this.generation) return;
2158
- job.deliverySent = true;
2124
+ if (!this.sessionActive || job.generation !== this.generation) return;
2159
2125
  const payload = this.waitResult(job, false);
2160
2126
  try {
2161
2127
  this.pi.sendMessage(
@@ -2276,6 +2242,20 @@ var SpawnParameters = Type.Object(
2276
2242
  description: "Self-contained task, constraints, and expected result. Maximum 50 KiB.",
2277
2243
  maxLength: MAX_TASK_BYTES
2278
2244
  }),
2245
+ provider: Type.Optional(
2246
+ Type.String({
2247
+ description: "Exact child provider ID. Defaults to the main agent's effective provider at spawn time.",
2248
+ minLength: 1,
2249
+ maxLength: MAX_BOOTSTRAP_BYTES
2250
+ })
2251
+ ),
2252
+ model: Type.Optional(
2253
+ Type.String({
2254
+ description: "Exact model ID within the selected provider. Defaults independently to the main agent's effective model ID at spawn time. Unlisted models fail without fallback.",
2255
+ minLength: 1,
2256
+ maxLength: MAX_BOOTSTRAP_BYTES
2257
+ })
2258
+ ),
2279
2259
  tools: Type.Optional(
2280
2260
  Type.Array(
2281
2261
  StringEnum(CHILD_CORE_TOOL_NAMES, {
@@ -2410,23 +2390,25 @@ function registerSubagentTools(pi, dependencies = {}) {
2410
2390
  if (ctx.cwd !== cwd || ctx.isProjectTrusted() !== projectTrusted) {
2411
2391
  throw new Error("Subagent spawn context changed during attachment validation; retry the request.");
2412
2392
  }
2413
- if (tools.includes("codemode") || attachments.extensions.length > 0) {
2414
- assertChildBootstrapCapacity([.../* @__PURE__ */ new Set([...attachments.effectiveTools, ...CHILD_COMMUNICATION_TOOL_NAMES])]);
2415
- }
2416
- const model = resolveChildModel(ctx, attachments.extensions.length > 0);
2417
- const thinkingLevel = resolveThinkingLevel(params.thinkingLevel ?? ctx.thinkingLevel ?? pi.getThinkingLevel());
2393
+ const { provider, model, modelOverrides } = resolveChildModel(ctx, params, attachments.extensions.length > 0);
2394
+ const defaultThinkingLevel = resolveThinkingLevel(ctx.thinkingLevel ?? pi.getThinkingLevel());
2395
+ const thinkingLevel = resolveThinkingLevel(params.thinkingLevel ?? defaultThinkingLevel);
2418
2396
  resolveTimeoutMs(params.timeout);
2419
2397
  assertChildCommandCapacity(
2420
2398
  {
2421
2399
  tools,
2422
2400
  skills: attachments.skills,
2423
2401
  extensions: attachments.extensions,
2424
- model,
2425
2402
  thinkingLevel,
2426
2403
  projectTrusted
2427
2404
  },
2428
2405
  dependencies.platform
2429
2406
  );
2407
+ assertChildBootstrapCapacity(selectedChildTools({ tools, extensions: attachments.extensions }), {
2408
+ provider,
2409
+ model,
2410
+ thinkingLevel
2411
+ });
2430
2412
  throwIfAborted2(signal, "Subagent spawn was cancelled");
2431
2413
  return toolResult(
2432
2414
  runtime.start({
@@ -2435,8 +2417,11 @@ function registerSubagentTools(pi, dependencies = {}) {
2435
2417
  skills: attachments.skills,
2436
2418
  extensions: attachments.extensions,
2437
2419
  toolSources: attachments.toolSources,
2420
+ provider,
2438
2421
  model,
2422
+ modelOverrides,
2439
2423
  thinkingLevel,
2424
+ defaultThinkingLevel,
2440
2425
  cwd,
2441
2426
  timeout: params.timeout,
2442
2427
  projectTrusted
@@ -2573,19 +2558,47 @@ function resolveTools(value) {
2573
2558
  }
2574
2559
  return tools;
2575
2560
  }
2576
- function resolveChildModel(ctx, hasAttachedExtensions) {
2577
- const model = ctx.model;
2578
- if (!model) throw new Error("Subagent model is unavailable because no main-agent model is selected.");
2579
- const provider = sanitizeTerminalText(model.provider).slice(0, 128);
2580
- if (!hasAttachedExtensions && ctx.modelRegistry.getRegisteredProviderIds().includes(model.provider)) {
2581
- throw new Error(`Subagent model provider ${provider} is unavailable because children disable parent extensions.`);
2561
+ function resolveChildModel(ctx, overrides, hasAttachedExtensions) {
2562
+ const parent = ctx.model;
2563
+ if (!parent && (overrides.provider === void 0 || overrides.model === void 0)) {
2564
+ throw new Error("Subagent model is unavailable because no main-agent model is selected.");
2565
+ }
2566
+ const provider = validateModelIdentifier(
2567
+ overrides.provider === void 0 ? parent?.provider : overrides.provider,
2568
+ "provider"
2569
+ );
2570
+ const model = validateModelIdentifier(overrides.model === void 0 ? parent?.id : overrides.model, "model");
2571
+ const displayProvider = sanitizeTerminalText(provider).slice(0, 128);
2572
+ if (!hasAttachedExtensions && ctx.modelRegistry.getRegisteredProviderIds().includes(provider)) {
2573
+ throw new Error(
2574
+ `Subagent model provider ${displayProvider} is unavailable because children disable parent extensions.`
2575
+ );
2582
2576
  }
2583
- if (ctx.modelRegistry.getProviderAuthStatus(model.provider).source === "runtime") {
2577
+ if (ctx.modelRegistry.getProviderAuthStatus(provider).source === "runtime") {
2584
2578
  throw new Error(
2585
- `Subagent model provider ${provider} uses a process-local runtime API key. Configure stored or environment credentials that child processes can read.`
2579
+ `Subagent model provider ${displayProvider} uses a process-local runtime API key. Configure stored or environment credentials that child processes can read.`
2586
2580
  );
2587
2581
  }
2588
- return `${model.provider}/${model.id}`;
2582
+ return {
2583
+ provider,
2584
+ model,
2585
+ modelOverrides: {
2586
+ ...provider !== parent?.provider ? { provider } : {},
2587
+ ...model !== parent?.id ? { model } : {}
2588
+ }
2589
+ };
2590
+ }
2591
+ function validateModelIdentifier(value, field) {
2592
+ if (typeof value === "string" && (value.length > MAX_BOOTSTRAP_BYTES || Buffer.byteLength(value, "utf8") > MAX_BOOTSTRAP_BYTES)) {
2593
+ throw new Error(`Subagent ${field} exceeds the ${MAX_BOOTSTRAP_BYTES}-byte child bootstrap size limit.`);
2594
+ }
2595
+ if (typeof value !== "string" || !value.trim() || [...value].some((character) => {
2596
+ const codePoint = character.codePointAt(0) ?? 0;
2597
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
2598
+ }) || sanitizeTerminalText(value) !== value) {
2599
+ throw new Error(`Subagent ${field} must be a non-empty exact ID without control characters.`);
2600
+ }
2601
+ return value;
2589
2602
  }
2590
2603
  function resolveThinkingLevel(value) {
2591
2604
  if (typeof value !== "string" || !THINKING_LEVEL_SET.has(value)) {
@@ -2608,23 +2621,10 @@ function resolveMainSendArguments(params) {
2608
2621
  }
2609
2622
  return recipient !== void 0 ? { kind: "request", recipient, message: params.message } : { kind: "response", requestId: requestId ?? "", message: params.message };
2610
2623
  }
2611
- function optionalIdentifier(value, field) {
2612
- return value === void 0 ? void 0 : requiredIdentifier(value, field);
2613
- }
2614
2624
  function requiredString(value, field) {
2615
2625
  if (typeof value !== "string" || !value.trim()) throw new Error(`Subagent ${field} is required.`);
2616
2626
  return value.trim();
2617
2627
  }
2618
- function requiredIdentifier(value, field) {
2619
- const identifier = requiredString(value, field);
2620
- if (identifier.length > MAX_IDENTIFIER_LENGTH || [...identifier].some((character) => {
2621
- const codePoint = character.codePointAt(0) ?? 0;
2622
- return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
2623
- })) {
2624
- throw new Error(`Subagent ${field} is invalid.`);
2625
- }
2626
- return identifier;
2627
- }
2628
2628
  function assertNotNested() {
2629
2629
  if ((Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) > 0) {
2630
2630
  throw new Error("Nested subagents are not supported by pi-subagents.");
@@ -2646,86 +2646,45 @@ function toolResult(value) {
2646
2646
  }
2647
2647
 
2648
2648
  // src/widget.ts
2649
+ import {
2650
+ createEditorStatusCollection
2651
+ } from "@narumitw/pi-tui-kit/editor-status-collection";
2652
+ import { createEditorStatusPublisher } from "@narumitw/pi-tui-kit/editor-status-publisher";
2653
+ import {
2654
+ createEditorStatusView
2655
+ } from "@narumitw/pi-tui-kit/editor-status-view";
2656
+
2657
+ // src/widget-renderer.ts
2649
2658
  import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2650
2659
  import { EditorStatusWidget } from "@narumitw/pi-tui-kit/editor-status-widget";
2651
- var SUBAGENT_WIDGET_KEY = "subagents";
2652
- var SUBAGENT_WIDGET_REFRESH_INTERVAL_MS = 1e3;
2653
- var WIDGET_OPTIONS = { placement: "aboveEditor" };
2654
2660
  var TASK_PREVIEW_WIDTH = 40;
2655
- function createSubagentWidgetController(runtime) {
2656
- let activeSession;
2657
- let refreshTimer;
2658
- let unsubscribeJobs;
2659
- let publishedValue;
2660
- let settings = { ...DEFAULT_SUBAGENT_SETTINGS };
2661
- let clearWidget;
2662
- const ownsSession = (ctx) => ctx.sessionManager === activeSession;
2663
- const stop = () => {
2664
- if (refreshTimer) clearInterval(refreshTimer);
2665
- refreshTimer = void 0;
2666
- unsubscribeJobs?.();
2667
- unsubscribeJobs = void 0;
2668
- };
2669
- const publish = (ctx) => {
2670
- if (ctx.mode !== "tui" || !ownsSession(ctx)) return;
2671
- const jobs = settings.showWidget ? runtime.activeJobsForDisplay() : [];
2672
- const value = `${JSON.stringify(settings)}
2673
- ${widgetValue(jobs)}`;
2674
- if (value === publishedValue) return;
2675
- if (jobs.length === 0) {
2676
- ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, void 0);
2677
- } else {
2678
- const snapshot = jobs.map(cloneDisplayJob);
2679
- const preferences = { ...settings };
2680
- ctx.ui.setWidget(
2681
- SUBAGENT_WIDGET_KEY,
2682
- (_tui, theme) => createSubagentWidget(snapshot, theme, preferences),
2683
- WIDGET_OPTIONS
2684
- );
2685
- }
2686
- publishedValue = value;
2687
- };
2688
- return {
2689
- start(ctx, preferences = DEFAULT_SUBAGENT_SETTINGS) {
2690
- stop();
2691
- clearWidget?.();
2692
- clearWidget = void 0;
2693
- settings = { ...preferences };
2694
- activeSession = ctx.sessionManager;
2695
- publishedValue = void 0;
2696
- if (ctx.mode !== "tui") return;
2697
- clearWidget = () => ctx.ui.setWidget(SUBAGENT_WIDGET_KEY, void 0);
2698
- clearWidget();
2699
- unsubscribeJobs = runtime.subscribeJobs(() => publish(ctx));
2700
- refreshTimer = setInterval(() => publish(ctx), SUBAGENT_WIDGET_REFRESH_INTERVAL_MS);
2701
- refreshTimer.unref();
2702
- publish(ctx);
2703
- },
2704
- update(ctx, preferences) {
2705
- if (!ownsSession(ctx)) return;
2706
- settings = { ...preferences };
2707
- publish(ctx);
2708
- },
2709
- shutdown(ctx) {
2710
- if (!ownsSession(ctx)) return;
2711
- stop();
2712
- clearWidget?.();
2713
- clearWidget = void 0;
2714
- publishedValue = void 0;
2715
- activeSession = void 0;
2716
- }
2717
- };
2718
- }
2719
- function createSubagentWidget(jobs, theme, settings) {
2720
- return new EditorStatusWidget({
2721
- theme,
2722
- renderBody: () => [
2723
- theme.fg("muted", `Subagents \xB7 ${jobs.length} active`),
2724
- ...jobs.map((job) => renderJob(job, theme, settings))
2725
- ]
2726
- });
2727
- }
2728
- function renderJob(job, theme, settings) {
2661
+ var SUBAGENT_WIDGET_RESERVED_ROWS = 8;
2662
+ var SUBAGENT_WIDGET_MAX_ROWS = 10;
2663
+ function subagentWidgetRowBudget(rows) {
2664
+ return Number.isFinite(rows) ? Math.min(SUBAGENT_WIDGET_MAX_ROWS, Math.max(0, Math.floor(rows) - SUBAGENT_WIDGET_RESERVED_ROWS)) : 0;
2665
+ }
2666
+ function renderSubagentWidgetSnapshot(snapshot, theme, width, maxRows) {
2667
+ const { jobs, items, settings, mode } = snapshot;
2668
+ const budget = Number.isFinite(maxRows) ? Math.max(0, Math.floor(maxRows)) : 0;
2669
+ if (!settings.showWidget || mode === "hidden" || jobs.length === 0 || items.length === 0 || budget < 2) return [];
2670
+ const running = jobs.filter((job) => job.state === "running").length;
2671
+ const counts = `Subagents \xB7 ${jobs.length} active \xB7 ${running} running \xB7 ${jobs.length - running} queued`;
2672
+ const removed = jobs.length - items.length;
2673
+ const summary = (shown) => `${counts}${jobs.length > shown ? ` \xB7 ${jobs.length - shown} not shown${removed ? ` (${removed} removed)` : ""}` : ""}`;
2674
+ const bodyRows = budget - 1;
2675
+ let body;
2676
+ if (mode === "compact" || bodyRows === 1) {
2677
+ body = [theme.fg("muted", summary(0))];
2678
+ } else {
2679
+ const shown = items.slice(0, bodyRows - 1);
2680
+ body = [
2681
+ theme.fg("muted", shown.length === jobs.length ? `Subagents \xB7 ${jobs.length} active` : summary(shown.length)),
2682
+ ...shown.map(({ value, mode: itemMode }) => renderJob(value, theme, settings, itemMode === "compact"))
2683
+ ];
2684
+ }
2685
+ return new EditorStatusWidget({ theme, renderBody: () => body }).render(width);
2686
+ }
2687
+ function renderJob(job, theme, settings, compact) {
2729
2688
  const running = job.state === "running";
2730
2689
  const symbol = theme.fg(running ? "accent" : "dim", running ? "\u25B6 " : "\u25CB ");
2731
2690
  const state = theme.fg(running ? "accent" : "muted", job.state);
@@ -2733,27 +2692,25 @@ function renderJob(job, theme, settings) {
2733
2692
  const taskText = sanitizeSubagentDisplayText(job.task);
2734
2693
  const task = stripTerminalSequences(truncateToWidth(taskText, TASK_PREVIEW_WIDTH, "\u2026"));
2735
2694
  const labels = [
2736
- ...settings.showJobId ? [theme.fg("text", jobId)] : [],
2737
- ...visibleWidth(task) > 0 ? [theme.fg("text", task)] : [],
2738
- state
2695
+ ...settings.showJobId && visibleWidth(jobId) > 0 ? [theme.fg("text", jobId)] : [],
2696
+ ...visibleWidth(task) > 0 ? [theme.fg("text", task)] : []
2739
2697
  ];
2698
+ const hasLabel = labels.length > 0;
2699
+ if (!hasLabel) labels.push(state);
2700
+ if (compact) return `${symbol}${labels.join(" \xB7 ")}${!running && hasLabel ? ` \xB7 ${state}` : ""}`;
2740
2701
  const tools = job.tools.length > 0 ? job.tools.map(sanitizeSubagentDisplayText).join(", ") : "none";
2741
- const timeout = job.timeout === void 0 ? "no timeout" : formatSeconds(job.timeout);
2702
+ const timeout = job.timeout === void 0 ? void 0 : formatSeconds(job.timeout);
2703
+ const elapsed = formatSeconds(Math.floor(job.elapsedMs / 1e3));
2704
+ const timing = running ? `${elapsed}${timeout === void 0 ? "" : ` / ${timeout}`}` : timeout === void 0 ? void 0 : `timeout: ${timeout}`;
2742
2705
  const details = [
2743
- `thinking: ${sanitizeSubagentDisplayText(job.thinkingLevel)}`,
2744
- ...settings.showTiming ? [`${formatSeconds(Math.floor(job.elapsedMs / 1e3))} / ${timeout}`] : [],
2706
+ ...job.provider !== void 0 ? [`provider: ${sanitizeSubagentDisplayText(job.provider)}`] : [],
2707
+ ...job.model !== void 0 ? [`model: ${sanitizeSubagentDisplayText(job.model)}`] : [],
2708
+ ...job.thinkingLevel !== job.defaultThinkingLevel ? [`thinking: ${sanitizeSubagentDisplayText(job.thinkingLevel)}`] : [],
2709
+ ...!running && hasLabel ? ["queued"] : [],
2710
+ ...settings.showTiming && timing !== void 0 ? [timing] : [],
2745
2711
  ...settings.showTools ? [`tools: ${tools}`] : []
2746
2712
  ];
2747
- const detail = details.length > 0 ? theme.fg("muted", ` \xB7 ${details.join(" \xB7 ")}`) : "";
2748
- return `${symbol}${labels.join(" \xB7 ")}${detail}`;
2749
- }
2750
- function widgetValue(jobs) {
2751
- return jobs.map(
2752
- (job) => `${job.jobId}\0${job.task}\0${job.state}\0${job.thinkingLevel}\0${Math.floor(job.elapsedMs / 1e3)}\0${job.timeout ?? ""}\0${job.tools.join(",")}`
2753
- ).join("\n");
2754
- }
2755
- function cloneDisplayJob(job) {
2756
- return { ...job, tools: [...job.tools] };
2713
+ return `${symbol}${labels.join(" \xB7 ")}${details.length > 0 ? theme.fg("muted", ` \xB7 ${details.join(" \xB7 ")}`) : ""}`;
2757
2714
  }
2758
2715
  function formatSeconds(value) {
2759
2716
  if (value < 60) return `${formatNumber(value)}s`;
@@ -2768,6 +2725,225 @@ function formatNumber(value) {
2768
2725
  return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/u, "").replace(/\.$/u, "");
2769
2726
  }
2770
2727
 
2728
+ // src/widget.ts
2729
+ var SUBAGENT_WIDGET_KEY = "subagents";
2730
+ var SUBAGENT_WIDGET_REFRESH_INTERVAL_MS = 1e3;
2731
+ function createCollection() {
2732
+ return createEditorStatusCollection({
2733
+ maxItems: 8,
2734
+ // Runtime accepts at most eight active jobs.
2735
+ // 50 KiB task + 64 tool names (128 UTF-16 units each) + 16 KiB model selection, plus metadata.
2736
+ maxContentSize: 128 * 1024,
2737
+ measure: (job) => Buffer.byteLength(
2738
+ [job.jobId, job.task, job.provider ?? "", job.model ?? "", job.thinkingLevel, ...job.tools].join("\0"),
2739
+ "utf8"
2740
+ ) + 128
2741
+ });
2742
+ }
2743
+ function createSubagentWidgetController(runtime) {
2744
+ let activeSession;
2745
+ let generation = 0;
2746
+ let refreshTimer;
2747
+ let unsubscribeJobs;
2748
+ let publisher;
2749
+ let collection;
2750
+ let view;
2751
+ let controls;
2752
+ let jobs = [];
2753
+ let publishedValue;
2754
+ let cyclePhase = "idle";
2755
+ let lastOutcome;
2756
+ let settings = { ...DEFAULT_SUBAGENT_SETTINGS };
2757
+ const ownsSession = (ctx) => activeSession !== void 0 && ctx.sessionManager === activeSession;
2758
+ const stop = (errors = []) => {
2759
+ try {
2760
+ for (const release of [
2761
+ () => {
2762
+ if (refreshTimer) clearInterval(refreshTimer);
2763
+ },
2764
+ () => unsubscribeJobs?.(),
2765
+ () => view?.dispose(),
2766
+ () => collection?.dispose(),
2767
+ // The publisher must still own its key when it attempts to clear it.
2768
+ () => publisher?.dispose()
2769
+ ]) {
2770
+ try {
2771
+ release();
2772
+ } catch (error) {
2773
+ errors.push(error);
2774
+ }
2775
+ }
2776
+ } finally {
2777
+ generation++;
2778
+ activeSession = void 0;
2779
+ refreshTimer = void 0;
2780
+ unsubscribeJobs = void 0;
2781
+ view = void 0;
2782
+ collection = void 0;
2783
+ publisher = void 0;
2784
+ controls = void 0;
2785
+ jobs = [];
2786
+ publishedValue = void 0;
2787
+ cyclePhase = "idle";
2788
+ lastOutcome = void 0;
2789
+ }
2790
+ if (errors.length === 1) throw errors[0];
2791
+ if (errors.length > 1) throw new AggregateError(errors, "Subagent widget cleanup failed");
2792
+ };
2793
+ const publish = () => {
2794
+ if (!view || !collection || !publisher) return;
2795
+ const items = collection.snapshot();
2796
+ const value = JSON.stringify([settings, view.mode, jobs, items]);
2797
+ if (value === publishedValue) return;
2798
+ publisher.publish(
2799
+ settings.showWidget && view.mode !== "hidden" && items.length > 0 ? { jobs, items, settings: { ...settings }, mode: view.mode } : void 0
2800
+ );
2801
+ publishedValue = value;
2802
+ };
2803
+ const refresh = () => {
2804
+ if (!view || !collection) return;
2805
+ const next = runtime.activeJobsForDisplay().map((job) => ({ ...job, tools: [...job.tools] }));
2806
+ const entries = next.map((job) => ({ id: job.jobId, value: job, revision: semanticRevision(job) }));
2807
+ collection.replace(entries);
2808
+ jobs = next;
2809
+ view.revision(JSON.stringify(entries.map(({ id, revision }) => [id, revision])));
2810
+ publish();
2811
+ };
2812
+ const finishCycle = (eligible) => {
2813
+ try {
2814
+ view?.settled(eligible);
2815
+ } finally {
2816
+ cyclePhase = "idle";
2817
+ lastOutcome = void 0;
2818
+ }
2819
+ };
2820
+ const startCycle = (phase) => {
2821
+ if (cyclePhase === "idle" || cyclePhase === "maintenance") lastOutcome = void 0;
2822
+ cyclePhase = phase;
2823
+ view?.agentStarted();
2824
+ };
2825
+ const syncActiveCycle = (ctx) => {
2826
+ if (ctx.isIdle()) {
2827
+ if (cyclePhase !== "idle" && cyclePhase !== "running") finishCycle(false);
2828
+ } else if (cyclePhase === "idle" || cyclePhase === "maintenance") {
2829
+ startCycle(ctx.signal === void 0 ? "unconfirmed" : "running");
2830
+ }
2831
+ };
2832
+ return {
2833
+ start(ctx, preferences = DEFAULT_SUBAGENT_SETTINGS, phase = "idle") {
2834
+ stop();
2835
+ const token = generation;
2836
+ const isCurrent = () => token === generation && ownsSession(ctx);
2837
+ try {
2838
+ activeSession = ctx.sessionManager;
2839
+ settings = { ...preferences };
2840
+ if (ctx.mode !== "tui" || !ctx.hasUI) return;
2841
+ collection = createCollection();
2842
+ view = createEditorStatusView({ onChange: publish, canCompact: () => settings.showWidget && jobs.length > 0 });
2843
+ publisher = createEditorStatusPublisher({
2844
+ key: SUBAGENT_WIDGET_KEY,
2845
+ context: ctx,
2846
+ isCurrent,
2847
+ render: (snapshot, theme, width, rows) => renderSubagentWidgetSnapshot(snapshot, theme, width, subagentWidgetRowBudget(rows))
2848
+ });
2849
+ if (phase === "maintenance") cyclePhase = phase;
2850
+ else if (phase !== "idle") startCycle(phase);
2851
+ syncActiveCycle(ctx);
2852
+ const ownedView = view;
2853
+ controls = {
2854
+ state: () => ({ mode: ownedView.mode, pinned: ownedView.pinned, trigger: ownedView.trigger }),
2855
+ setMode: (mode) => {
2856
+ if (isCurrent()) ownedView.setMode(mode);
2857
+ },
2858
+ setPinned: (pinned) => {
2859
+ if (isCurrent()) ownedView.setPinned(pinned);
2860
+ },
2861
+ setTrigger: (trigger) => {
2862
+ if (!isCurrent()) return;
2863
+ syncActiveCycle(ctx);
2864
+ ownedView.setTrigger(trigger);
2865
+ },
2866
+ resetItems() {
2867
+ if (!isCurrent()) return;
2868
+ collection?.dispose();
2869
+ collection = createCollection();
2870
+ refresh();
2871
+ },
2872
+ item(jobId) {
2873
+ if (!isCurrent()) return void 0;
2874
+ const handle = collection?.item(jobId);
2875
+ if (!handle) return void 0;
2876
+ const apply = (action) => {
2877
+ if (!isCurrent() || !handle[action]()) return false;
2878
+ publish();
2879
+ return true;
2880
+ };
2881
+ return { compact: () => apply("compact"), expand: () => apply("expand"), remove: () => apply("remove") };
2882
+ }
2883
+ };
2884
+ unsubscribeJobs = runtime.subscribeJobs(() => {
2885
+ if (isCurrent()) refresh();
2886
+ });
2887
+ refreshTimer = setInterval(() => {
2888
+ if (isCurrent()) refresh();
2889
+ }, SUBAGENT_WIDGET_REFRESH_INTERVAL_MS);
2890
+ refreshTimer.unref();
2891
+ refresh();
2892
+ } catch (error) {
2893
+ stop([error]);
2894
+ }
2895
+ },
2896
+ update(ctx, preferences) {
2897
+ if (!ownsSession(ctx)) return;
2898
+ settings = { ...preferences };
2899
+ if (ctx.mode === "tui") refresh();
2900
+ },
2901
+ shutdown(ctx) {
2902
+ if (!ownsSession(ctx)) return;
2903
+ stop();
2904
+ },
2905
+ controls(ctx) {
2906
+ return ownsSession(ctx) && ctx.mode === "tui" ? controls : void 0;
2907
+ },
2908
+ preparing(ctx) {
2909
+ if (!ownsSession(ctx) || !view) return;
2910
+ if (cyclePhase !== "auto-preparing" && cyclePhase !== "unconfirmed") finishCycle(false);
2911
+ startCycle("preparing");
2912
+ },
2913
+ automaticCompaction(ctx) {
2914
+ if (!ownsSession(ctx) || !view) return;
2915
+ if (cyclePhase === "preparing" || cyclePhase === "maintenance") finishCycle(false);
2916
+ if (cyclePhase === "idle" || cyclePhase === "unconfirmed") startCycle("auto-preparing");
2917
+ },
2918
+ clearCycle(ctx) {
2919
+ if (ownsSession(ctx)) finishCycle(false);
2920
+ },
2921
+ maintenance(ctx) {
2922
+ if (!ownsSession(ctx)) return;
2923
+ finishCycle(false);
2924
+ cyclePhase = "maintenance";
2925
+ },
2926
+ agentStarted(ctx) {
2927
+ if (ownsSession(ctx) && view) startCycle("running");
2928
+ },
2929
+ userMessage(ctx) {
2930
+ if (ownsSession(ctx)) view?.userMessage();
2931
+ },
2932
+ turnEnded(ctx, stopReason) {
2933
+ if (ownsSession(ctx) && cyclePhase !== "idle" && cyclePhase !== "maintenance") lastOutcome = stopReason;
2934
+ },
2935
+ settled(ctx, aborted) {
2936
+ if (!ownsSession(ctx) || !view) return;
2937
+ const eligible = !aborted && lastOutcome !== void 0 && lastOutcome !== "error" && lastOutcome !== "aborted";
2938
+ finishCycle(eligible);
2939
+ }
2940
+ };
2941
+ }
2942
+ function semanticRevision(job) {
2943
+ const { elapsedMs: _elapsedMs, ...semantic } = job;
2944
+ return JSON.stringify(semantic);
2945
+ }
2946
+
2771
2947
  // src/subagents.ts
2772
2948
  function subagents(pi, dependencies = {}) {
2773
2949
  registerMessageRenderers(pi);
@@ -2779,9 +2955,18 @@ function subagents(pi, dependencies = {}) {
2779
2955
  let owner = new AbortController();
2780
2956
  let ready = false;
2781
2957
  let menuOpen = false;
2958
+ let cyclePhase = "idle";
2782
2959
  let settings = { ...DEFAULT_SUBAGENT_SETTINGS };
2783
2960
  const menuTasks = /* @__PURE__ */ new Set();
2784
- const ownsSession = (ctx) => ctx.sessionManager === activeContext?.sessionManager;
2961
+ const ownsSession = (ctx) => activeContext !== void 0 && ctx.sessionManager === activeContext.sessionManager;
2962
+ const joinCleanup = async (steps, errors = []) => {
2963
+ const results = await Promise.allSettled(steps.map(async (step) => step()));
2964
+ for (const result of results) {
2965
+ if (result.status === "rejected") errors.push(result.reason);
2966
+ }
2967
+ if (errors.length === 1) throw errors[0];
2968
+ if (errors.length > 1) throw new AggregateError(errors, "Subagents session cleanup failed");
2969
+ };
2785
2970
  const openSettings = async (ctx, signal, isCurrent) => {
2786
2971
  if (signal.aborted || !isCurrent()) return "stale";
2787
2972
  if (ctx.mode !== "tui") {
@@ -2825,13 +3010,13 @@ function subagents(pi, dependencies = {}) {
2825
3010
  menuOpen = true;
2826
3011
  const generation = sessionGeneration;
2827
3012
  const signal = owner.signal;
2828
- const isCurrent = () => generation === sessionGeneration && ownsSession(ctx) && !signal.aborted;
3013
+ const isCurrent = () => generation === sessionGeneration && !signal.aborted && ownsSession(ctx);
2829
3014
  const task = (async () => {
2830
3015
  if (route === "settings") {
2831
3016
  await openSettings(ctx, signal, isCurrent);
2832
3017
  return;
2833
3018
  }
2834
- const { showSubagentsMenu } = await import("./chunks/menu-KAIUEWL7.ts");
3019
+ const { showSubagentsMenu } = await import("./chunks/menu-2UH4ICLL.ts");
2835
3020
  if (!isCurrent()) return;
2836
3021
  await showSubagentsMenu(ctx, {
2837
3022
  signal,
@@ -2846,7 +3031,8 @@ function subagents(pi, dependencies = {}) {
2846
3031
  if (!isCurrent()) throw new Error("The subagents session changed.");
2847
3032
  await tools.runtime.cancel(jobId);
2848
3033
  },
2849
- showSettings: (actionSignal) => openSettings(ctx, actionSignal, isCurrent)
3034
+ showSettings: (actionSignal) => openSettings(ctx, actionSignal, isCurrent),
3035
+ widget: widget.controls(ctx)
2850
3036
  });
2851
3037
  })();
2852
3038
  menuTasks.add(task);
@@ -2863,40 +3049,115 @@ function subagents(pi, dependencies = {}) {
2863
3049
  }
2864
3050
  });
2865
3051
  pi.on("session_start", async (_event, ctx) => {
3052
+ const previousContext = activeContext;
3053
+ const generation = ++sessionGeneration;
3054
+ ready = false;
3055
+ menuOpen = false;
2866
3056
  owner.abort();
2867
- if (activeContext) widget.shutdown(activeContext);
2868
3057
  owner = new AbortController();
3058
+ const signal = owner.signal;
2869
3059
  activeContext = ctx;
2870
- ready = false;
2871
- menuOpen = false;
2872
- const generation = ++sessionGeneration;
2873
- const isCurrent = () => generation === sessionGeneration && ownsSession(ctx) && !owner.signal.aborted;
2874
- await tools.startSession();
2875
- if (!isCurrent()) return;
2876
- await Promise.allSettled([...menuTasks]);
3060
+ cyclePhase = ctx.signal === void 0 ? "idle" : "running";
3061
+ const isCurrent = () => generation === sessionGeneration && !signal.aborted && ownsSession(ctx);
3062
+ const errors = [];
3063
+ try {
3064
+ if (previousContext) widget.shutdown(previousContext);
3065
+ } catch (error) {
3066
+ errors.push(error);
3067
+ }
3068
+ const failedHandoff = errors.length > 0;
3069
+ await joinCleanup(
3070
+ [
3071
+ () => failedHandoff ? tools.shutdown() : tools.startSession(),
3072
+ ...[...menuTasks].map((task) => () => task),
3073
+ ...failedHandoff ? [() => store.flush()] : []
3074
+ ],
3075
+ errors
3076
+ );
2877
3077
  if (!isCurrent()) return;
2878
3078
  const loaded = await store.load();
2879
3079
  if (!isCurrent()) return;
2880
3080
  settings = { ...loaded.settings };
3081
+ widget.start(ctx, settings, cyclePhase);
2881
3082
  ready = true;
2882
- widget.start(ctx, settings);
2883
3083
  if (loaded.diagnostics.length > 0) {
2884
3084
  const message = sanitizeSubagentDisplayText(`Subagent settings: ${loaded.diagnostics.join(" ")}`);
2885
3085
  if (ctx.hasUI) ctx.ui.notify(message, "warning");
2886
3086
  else throw new Error(message);
2887
3087
  }
2888
3088
  });
2889
- pi.on("session_shutdown", async (_event, ctx) => {
3089
+ pi.on("before_agent_start", (_event, ctx) => {
3090
+ if (!ownsSession(ctx)) return;
3091
+ cyclePhase = "preparing";
3092
+ if (ready) widget.preparing(ctx);
3093
+ });
3094
+ pi.on("session_before_compact", (event, ctx) => {
3095
+ if (!ownsSession(ctx)) return;
3096
+ if (event.reason === "manual") {
3097
+ cyclePhase = "maintenance";
3098
+ if (ready) widget.maintenance(ctx);
3099
+ } else {
3100
+ if (cyclePhase !== "running") cyclePhase = "auto-preparing";
3101
+ if (ready) widget.automaticCompaction(ctx);
3102
+ }
3103
+ });
3104
+ pi.on("session_before_tree", (_event, ctx) => {
3105
+ if (!ownsSession(ctx)) return;
3106
+ cyclePhase = "maintenance";
3107
+ if (ready) widget.maintenance(ctx);
3108
+ });
3109
+ pi.on("input", (_event, ctx) => {
3110
+ if (!ownsSession(ctx) || !ctx.isIdle()) return;
3111
+ cyclePhase = "idle";
3112
+ if (ready) widget.clearCycle(ctx);
3113
+ });
3114
+ pi.on("agent_start", (_event, ctx) => {
3115
+ if (!ownsSession(ctx)) return;
3116
+ cyclePhase = "running";
3117
+ if (ready) widget.agentStarted(ctx);
3118
+ });
3119
+ pi.on("message_start", (event, ctx) => {
3120
+ if (ready && ownsSession(ctx) && event.message.role === "user") widget.userMessage(ctx);
3121
+ });
3122
+ pi.on("turn_end", (event, ctx) => {
3123
+ if (ready && ownsSession(ctx) && event.message.role === "assistant")
3124
+ widget.turnEnded(ctx, event.message.stopReason);
3125
+ });
3126
+ pi.on("agent_settled", (event, ctx) => {
3127
+ if (!ownsSession(ctx)) return;
3128
+ cyclePhase = "idle";
3129
+ if (ready) widget.settled(ctx, event.aborted);
3130
+ });
3131
+ pi.on("session_tree", (_event, ctx) => {
2890
3132
  if (!ownsSession(ctx)) return;
3133
+ cyclePhase = "maintenance";
3134
+ if (!ready) return;
2891
3135
  sessionGeneration++;
3136
+ ready = false;
3137
+ menuOpen = false;
2892
3138
  owner.abort();
3139
+ owner = new AbortController();
3140
+ widget.start(ctx, settings, cyclePhase);
3141
+ ready = true;
3142
+ });
3143
+ pi.on("session_shutdown", async (_event, ctx) => {
3144
+ if (!ownsSession(ctx)) return;
3145
+ sessionGeneration++;
2893
3146
  ready = false;
2894
3147
  menuOpen = false;
2895
3148
  activeContext = void 0;
2896
- widget.shutdown(ctx);
2897
- const results = await Promise.allSettled([tools.shutdown(), ...menuTasks, store.flush()]);
2898
- const failed = results.find((result) => result.status === "rejected");
2899
- if (failed?.status === "rejected") throw failed.reason;
3149
+ cyclePhase = "idle";
3150
+ owner.abort();
3151
+ const errors = [];
3152
+ try {
3153
+ widget.shutdown(ctx);
3154
+ } catch (error) {
3155
+ errors.push(error);
3156
+ }
3157
+ await joinCleanup(
3158
+ [() => tools.shutdown(), ...[...menuTasks].map((task) => () => task), () => store.flush()],
3159
+ errors
3160
+ );
2900
3161
  });
2901
3162
  }
2902
3163
  export {