@selesai/code 0.8.6 → 0.8.7

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 (192) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/core/agent-session.js +13 -1
  3. package/dist/core/extensions/types.d.ts +2 -0
  4. package/dist/core/provider-composer.js +17 -4
  5. package/dist/core/settings-manager.d.ts +2 -0
  6. package/dist/core/settings-manager.js +3 -0
  7. package/dist/core/system-prompt.js +2 -8
  8. package/dist/core/system-prompt.test.js +6 -0
  9. package/dist/core/tools/index.d.ts +1 -0
  10. package/dist/core/tools/index.js +1 -0
  11. package/dist/core/tools/schema-prune.d.ts +16 -0
  12. package/dist/core/tools/schema-prune.js +39 -0
  13. package/dist/core/tools/schema-prune.test.d.ts +1 -0
  14. package/dist/core/tools/schema-prune.test.js +111 -0
  15. package/dist/defaults/settings.json +1 -0
  16. package/dist/extensions/model-prompt-injector/config.json +10 -0
  17. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  18. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  19. package/dist/extensions/package.json +1 -0
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  21. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  22. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  23. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  24. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  25. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  26. package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
  27. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  28. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  29. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  30. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  31. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  32. package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
  33. package/dist/extensions/pi-subagents/install.mjs +2 -2
  34. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  35. package/dist/extensions/pi-subagents/package.json +1 -1
  36. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  37. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  38. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
  39. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  40. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  41. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  42. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  43. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  44. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  45. package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
  46. package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
  47. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  48. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  49. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  50. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  51. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  52. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  53. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  54. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  55. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  56. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  57. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  58. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  59. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  60. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  61. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  62. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
  63. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  64. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
  66. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  67. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  68. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  69. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  70. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
  71. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  72. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  73. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
  74. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
  75. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  76. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  77. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  78. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  80. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  81. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  82. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  83. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  84. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
  85. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  86. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  88. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  89. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  90. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  96. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  97. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  98. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  99. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  100. package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
  101. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  102. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  104. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  105. package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
  106. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  107. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  108. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  109. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  110. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  111. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  112. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  113. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  114. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  115. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  116. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  117. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  118. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  119. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  120. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  121. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  122. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  123. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  124. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  125. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  126. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  127. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  128. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  129. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  130. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  131. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  132. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  133. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  134. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  135. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  136. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  137. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  138. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  139. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  140. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
  141. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  142. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  143. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  144. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  145. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  146. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  147. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  148. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  149. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  150. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  151. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  152. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  153. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  154. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  155. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  156. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  157. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  158. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  159. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  160. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  161. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  162. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  163. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  164. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  165. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  166. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  167. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  168. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  169. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  170. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  171. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  172. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  173. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  174. package/dist/extensions/question/index.ts +21 -6
  175. package/dist/extensions/question/question-list.ts +7 -1
  176. package/dist/extensions/question/row-layout.ts +3 -0
  177. package/dist/extensions/question/tests/exp.test.ts +70 -0
  178. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  179. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  180. package/dist/extensions/question/tests/helpers.test.ts +7 -0
  181. package/dist/extensions/question/tests/question-list.test.ts +140 -0
  182. package/dist/extensions/question/tests/row-layout.test.ts +13 -0
  183. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  184. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  185. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  186. package/dist/skills/pi-subagents/SKILL.md +1 -1
  187. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  188. package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
  189. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  190. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  191. package/docs/settings.md +6 -0
  192. package/package.json +3 -3
@@ -1,66 +1,159 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import {
4
+ EXTERNAL_RUN_LIMITS,
4
5
  EXTERNAL_RUN_REGISTRY_KEY,
5
- registerExternalRunProvider,
6
+ EXTERNAL_RUN_REGISTRY_VERSION,
7
+ listExternalRuns,
8
+ registerExternalRun,
6
9
  snapshotExternalRuns,
10
+ unregisterExternalRun,
11
+ updateExternalRun,
7
12
  } from "../../src/api/external-runs.ts";
8
13
 
9
14
  function clearRegistry(): void {
10
15
  delete (globalThis as Record<PropertyKey, unknown>)[Symbol.for(EXTERNAL_RUN_REGISTRY_KEY)];
11
16
  }
12
17
 
13
- test("external-run providers expose scoped observational records without controls", () => {
18
+ test("external runs register, update, list, and unregister cached display records", () => {
14
19
  clearRegistry();
15
- const dispose = registerExternalRunProvider({
16
- name: "interactive-shell",
17
- listExternalRuns: () => [{
18
- id: "review-794",
19
- sessionId: "session-a",
20
- source: "interactive_shell",
21
- state: "completed",
22
- completionReason: "auto-close-quiet",
23
- cwd: "/tmp/review",
24
- reportPath: "/tmp/review.md",
25
- exitCode: null,
26
- residualRisks: ["The terminal runtime owns process control."],
27
- }],
20
+ assert.deepEqual(registerExternalRun({
21
+ id: "review-1083",
22
+ sessionId: "session-a",
23
+ source: "interactive-shell",
24
+ label: "Dependency review",
25
+ state: "running",
26
+ startedAt: 100,
27
+ currentAction: "Inspecting package metadata",
28
+ }), {
29
+ id: "review-1083",
30
+ sessionId: "session-a",
31
+ source: "interactive-shell",
32
+ label: "Dependency review",
33
+ state: "running",
34
+ startedAt: 100,
35
+ currentAction: "Inspecting package metadata",
28
36
  });
29
- assert.deepEqual(snapshotExternalRuns("session-a"), [{
30
- provider: "interactive-shell",
31
- id: "review-794",
37
+ assert.deepEqual(snapshotExternalRuns("session-b"), []);
38
+ assert.deepEqual(updateExternalRun("session-a", "review-1083", {
39
+ state: "completed",
40
+ updatedAt: 190,
41
+ endedAt: 200,
42
+ preview: "Review complete",
43
+ reportPath: "/tmp/review.md",
44
+ }), {
45
+ id: "review-1083",
32
46
  sessionId: "session-a",
33
- source: "interactive_shell",
47
+ source: "interactive-shell",
48
+ label: "Dependency review",
34
49
  state: "completed",
35
- completionReason: "auto-close-quiet",
36
- cwd: "/tmp/review",
50
+ startedAt: 100,
51
+ updatedAt: 190,
52
+ endedAt: 200,
53
+ currentAction: "Inspecting package metadata",
54
+ preview: "Review complete",
37
55
  reportPath: "/tmp/review.md",
38
- exitCode: null,
39
- residualRisks: ["The terminal runtime owns process control."],
40
- }]);
41
- assert.deepEqual(snapshotExternalRuns("session-b"), []);
42
- dispose();
56
+ });
57
+ assert.deepEqual(listExternalRuns("session-a"), snapshotExternalRuns("session-a"));
58
+ assert.equal(unregisterExternalRun("session-a", "review-1083"), true);
59
+ assert.equal(unregisterExternalRun("session-a", "review-1083"), false);
60
+ assert.deepEqual(snapshotExternalRuns("session-a"), []);
43
61
  clearRegistry();
44
62
  });
45
63
 
46
- test("external-run providers reject malformed and duplicate records", () => {
64
+ test("external run validation is atomic and display text is bounded", () => {
47
65
  clearRegistry();
48
- assert.throws(() => registerExternalRunProvider({ name: " bad", listExternalRuns: () => [] }), /trimmed/);
49
- registerExternalRunProvider({
50
- name: "interactive-shell",
51
- listExternalRuns: () => [
52
- { id: "run", sessionId: "session-a", source: "interactive_shell", state: "running", extra: true },
53
- ] as never,
66
+ registerExternalRun({
67
+ id: "run",
68
+ sessionId: "session-a",
69
+ source: "tool",
70
+ label: "Initial",
71
+ state: "running",
72
+ startedAt: 1,
54
73
  });
55
- assert.throws(() => snapshotExternalRuns("session-a"), /unknown fields: extra/);
56
- clearRegistry();
57
- registerExternalRunProvider({
58
- name: "interactive-shell",
59
- listExternalRuns: () => [
60
- { id: "run", sessionId: "session-a", source: "interactive_shell", state: "running" },
61
- { id: "run", sessionId: "session-a", source: "interactive_shell", state: "running" },
62
- ],
74
+ assert.throws(
75
+ () => updateExternalRun("session-a", "run", { state: "invalid" as never, label: "Changed" }),
76
+ /state is invalid/,
77
+ );
78
+ assert.equal(snapshotExternalRuns("session-a")[0]?.label, "Initial");
79
+ const updated = updateExternalRun("session-a", "run", {
80
+ label: `\u001b[31m${"x".repeat(EXTERNAL_RUN_LIMITS.maxTextLength + 20)}`,
81
+ preview: `line 1\n${"p".repeat(EXTERNAL_RUN_LIMITS.maxPreviewLength + 20)}`,
63
82
  });
64
- assert.throws(() => snapshotExternalRuns("session-a"), /duplicate run 'run'/);
83
+ assert.equal(updated.label.length, EXTERNAL_RUN_LIMITS.maxTextLength);
84
+ assert.ok(updated.preview!.length <= EXTERNAL_RUN_LIMITS.maxPreviewLength);
85
+ assert.doesNotMatch(updated.label, /\u001b/);
86
+ assert.doesNotMatch(updated.preview!, /\n/);
87
+ assert.throws(
88
+ () => registerExternalRun({ id: "unsafe\nrun", sessionId: "session-a", source: "tool", label: "Unsafe", state: "running", startedAt: 1 }),
89
+ /display-safe/,
90
+ );
91
+ assert.throws(
92
+ () => registerExternalRun({ id: "extra", sessionId: "session-a", source: "tool", label: "Extra", state: "running", startedAt: 1, cwd: "/private" } as never),
93
+ /unknown fields: cwd/,
94
+ );
95
+ assert.throws(
96
+ () => registerExternalRun({ id: "invalid-date", sessionId: "session-a", source: "tool", label: "Invalid date", state: "running", startedAt: Number.MAX_SAFE_INTEGER }),
97
+ /safe timestamp/,
98
+ );
99
+ clearRegistry();
100
+ });
101
+
102
+ test("external snapshots throw on malformed cached records by default", () => {
103
+ clearRegistry();
104
+ try {
105
+ (globalThis as Record<PropertyKey, unknown>)[Symbol.for(EXTERNAL_RUN_REGISTRY_KEY)] = {
106
+ version: EXTERNAL_RUN_REGISTRY_VERSION,
107
+ runs: new Map<string, unknown>([
108
+ ["session-a\0bad", { id: "bad", sessionId: "session-a", source: "tool", label: "Bad", state: "running", startedAt: Number.NaN }],
109
+ ["session-a\0good", { id: "good", sessionId: "session-a", source: "tool", label: "Good", state: "running", startedAt: 1 }],
110
+ ]),
111
+ };
112
+ assert.throws(
113
+ () => snapshotExternalRuns("session-a"),
114
+ /Malformed cached external run 'session-a\0bad': External run startedAt must be a non-negative safe timestamp/,
115
+ );
116
+ } finally {
117
+ clearRegistry();
118
+ }
119
+ });
120
+
121
+ test("external snapshots can remove malformed cached records for display-only callers", () => {
122
+ clearRegistry();
123
+ const diagnostics: string[] = [];
124
+ try {
125
+ (globalThis as Record<PropertyKey, unknown>)[Symbol.for(EXTERNAL_RUN_REGISTRY_KEY)] = {
126
+ version: EXTERNAL_RUN_REGISTRY_VERSION,
127
+ runs: new Map<string, unknown>([
128
+ ["session-a\0bad", { id: "bad", sessionId: "session-a", source: "tool", label: "Bad", state: "running", startedAt: Number.NaN }],
129
+ ["session-a\0good", { id: "good", sessionId: "session-a", source: "tool", label: "Good", state: "running", startedAt: 1 }],
130
+ ]),
131
+ };
132
+ assert.deepEqual(snapshotExternalRuns("session-a", { ignoreMalformed: true, onMalformedRecord: (message) => diagnostics.push(message) }).map((run) => run.id), ["good"]);
133
+ assert.deepEqual(diagnostics, ["Malformed cached external run 'session-a\0bad': External run startedAt must be a non-negative safe timestamp."]);
134
+ assert.deepEqual(snapshotExternalRuns("session-a").map((run) => run.id), ["good"]);
135
+ } finally {
136
+ clearRegistry();
137
+ }
138
+ });
139
+
140
+ test("external snapshots apply count and serialized byte caps without provider callbacks", () => {
141
+ clearRegistry();
142
+ for (let index = 0; index < EXTERNAL_RUN_LIMITS.maxSnapshotRuns + 5; index++) {
143
+ registerExternalRun({
144
+ id: `run-${index}`,
145
+ sessionId: "session-a",
146
+ source: "tool",
147
+ label: `Run ${index}`,
148
+ state: index === 0 ? "running" : "completed",
149
+ startedAt: index,
150
+ updatedAt: index,
151
+ preview: "p".repeat(EXTERNAL_RUN_LIMITS.maxPreviewLength),
152
+ });
153
+ }
154
+ const snapshot = snapshotExternalRuns("session-a");
155
+ assert.ok(snapshot.length <= EXTERNAL_RUN_LIMITS.maxSnapshotRuns);
156
+ assert.ok(Buffer.byteLength(JSON.stringify(snapshot), "utf8") <= EXTERNAL_RUN_LIMITS.maxSerializedBytes);
157
+ assert.equal(snapshot[0]?.id, "run-0", "active jobs sort ahead of terminal jobs");
65
158
  clearRegistry();
66
159
  });
@@ -3,6 +3,7 @@ import { describe, it } from "node:test";
3
3
  import { Editor, type EditorComponent, visibleWidth } from "@earendil-works/pi-tui";
4
4
  import type { ExtensionContext } from "@selesai/code";
5
5
  import type { SubagentState } from "../../src/shared/types.ts";
6
+ import { EXTERNAL_RUN_REGISTRY_KEY, EXTERNAL_RUN_REGISTRY_VERSION, registerExternalRun } from "../../src/api/external-runs.ts";
6
7
  import { collectFleetSnapshot } from "../../src/tui/fleet.ts";
7
8
  import {
8
9
  FLEET_STATUS_WIDGET_KEY,
@@ -13,6 +14,10 @@ import {
13
14
  resolveFleetViewPlacement,
14
15
  } from "../../src/tui/fleet-status.ts";
15
16
 
17
+ function clearExternalRuns(): void {
18
+ delete (globalThis as Record<PropertyKey, unknown>)[Symbol.for(EXTERNAL_RUN_REGISTRY_KEY)];
19
+ }
20
+
16
21
  function stateForTest(): SubagentState {
17
22
  return {
18
23
  baseCwd: process.cwd(),
@@ -46,6 +51,73 @@ describe("below-editor subagent FleetView", () => {
46
51
  assert.equal(formatFleetTokens(1_250_000), "↓ 1.3M tokens");
47
52
  });
48
53
 
54
+ it("renders cached external jobs with an external marker and elapsed time", () => {
55
+ clearExternalRuns();
56
+ registerExternalRun({
57
+ id: "external-review",
58
+ sessionId: "session-current",
59
+ source: "interactive-shell",
60
+ label: "Dependency review",
61
+ state: "running",
62
+ startedAt: Date.now() - 11_000,
63
+ currentAction: "Inspecting package metadata",
64
+ });
65
+ const state = stateForTest();
66
+ state.activeAsyncCapacity = { used: 0, limit: 0 };
67
+ assert.deepEqual(collectFleetStatusEntries(state).map((entry) => ({ key: entry.key, agent: entry.agent, description: entry.description })), [{
68
+ key: "external:external-review",
69
+ agent: "external · Dependency review",
70
+ description: "Inspecting package metadata",
71
+ }]);
72
+ let widgetFactory: ((tui: unknown, theme: typeof theme) => { render(width: number): string[] }) | undefined;
73
+ const ctx = {
74
+ hasUI: true,
75
+ ui: {
76
+ setWidget(_key: string, content: typeof widgetFactory | undefined) { if (content) widgetFactory = content; },
77
+ onTerminalInput() { return () => {}; },
78
+ getEditorText() { return ""; },
79
+ requestRender() {},
80
+ notify() {},
81
+ theme,
82
+ },
83
+ } as unknown as ExtensionContext;
84
+ const fleet = new SubagentFleetStatus(state, () => {}, { refreshMs: 60_000 });
85
+ try {
86
+ fleet.setContext(ctx);
87
+ const tui = { requestRender() {}, focusedComponent: Object.create(Editor.prototype) as Editor };
88
+ const component = widgetFactory!(tui, theme);
89
+ const compact = component.render(80)[0]!;
90
+ assert.match(compact, /1 active job/);
91
+ assert.doesNotMatch(compact, /Async runs|tokens/);
92
+ fleet.handleKey("\x1b[B");
93
+ const expanded = component.render(80).join("\n");
94
+ assert.match(expanded, /external · Dependency review · running/);
95
+ assert.match(expanded, /11s/);
96
+ assert.doesNotMatch(expanded.split("\n").find((line) => line.includes("Dependency review"))!, /tokens/);
97
+ } finally {
98
+ fleet.dispose();
99
+ clearExternalRuns();
100
+ }
101
+ });
102
+
103
+ it("warns when compact FleetView cannot inspect external jobs", () => {
104
+ clearExternalRuns();
105
+ const warnings: string[] = [];
106
+ const originalWarn = console.warn;
107
+ console.warn = (message?: unknown) => warnings.push(String(message));
108
+ (globalThis as Record<PropertyKey, unknown>)[Symbol.for(EXTERNAL_RUN_REGISTRY_KEY)] = {
109
+ version: EXTERNAL_RUN_REGISTRY_VERSION + 1,
110
+ runs: new Map(),
111
+ };
112
+ try {
113
+ assert.deepEqual(collectFleetStatusEntries(stateForTest()), []);
114
+ assert.match(warnings[0]!, /Failed to inspect external jobs/);
115
+ } finally {
116
+ console.warn = originalWarn;
117
+ clearExternalRuns();
118
+ }
119
+ });
120
+
49
121
  it("resolves configured FleetView placement with a below-editor fallback", () => {
50
122
  assert.equal(resolveFleetViewPlacement(undefined), "belowEditor");
51
123
  assert.equal(resolveFleetViewPlacement("belowEditor"), "belowEditor");
@@ -417,6 +489,16 @@ describe("below-editor subagent FleetView", () => {
417
489
  thinking: "medium",
418
490
  startedAt: 10,
419
491
  lastUpdate: 20,
492
+ ...(index === 4 ? {
493
+ children: [{
494
+ id: "nested-hidden-child",
495
+ parentRunId: "nested-4",
496
+ depth: 2,
497
+ path: [{ runId: "supervisor", stepIndex: 0 }, { runId: "nested-4" }],
498
+ state: "running" as const,
499
+ agent: "hidden-child",
500
+ }],
501
+ } : {}),
420
502
  })),
421
503
  });
422
504
  let widgetFactory: ((tui: unknown, theme: typeof theme) => { render(width: number): string[] }) | undefined;
@@ -448,7 +530,8 @@ describe("below-editor subagent FleetView", () => {
448
530
  assert.match(line!, new RegExp(state));
449
531
  }
450
532
  assert.doesNotMatch(lines, /leaf-4.*running/);
451
- assert.match(lines, /\+1 nested leaves/);
533
+ assert.doesNotMatch(lines, /hidden-child/);
534
+ assert.match(lines, /\+2 nested leaves/);
452
535
  } finally {
453
536
  fleet.dispose();
454
537
  }
@@ -594,6 +677,174 @@ describe("below-editor subagent FleetView", () => {
594
677
  }]);
595
678
  });
596
679
 
680
+ it("renders a workflow-owned foreground child under its workflow parent", () => {
681
+ const state = stateForTest();
682
+ state.asyncJobs.set("workflow-1", {
683
+ asyncId: "workflow-1",
684
+ asyncDir: "/tmp/workflow-1",
685
+ status: "running",
686
+ mode: "workflow",
687
+ startedAt: 10,
688
+ updatedAt: 20,
689
+ nestedChildren: [{
690
+ id: "owner-nested",
691
+ parentRunId: "workflow-1",
692
+ depth: 1,
693
+ path: [{ runId: "workflow-1" }],
694
+ state: "running",
695
+ agent: "owner-nested",
696
+ }],
697
+ });
698
+ state.foregroundControls.set("child-1", {
699
+ runId: "child-1",
700
+ parentWorkflowRunId: "workflow-1",
701
+ workflowKey: "review",
702
+ mode: "single",
703
+ startedAt: 11,
704
+ updatedAt: 20,
705
+ activeChildren: new Map([[0, { index: 0, agent: "reviewer", startedAt: 11, updatedAt: 20 }]]),
706
+ nestedChildren: [{
707
+ id: "nested-review",
708
+ parentRunId: "child-1",
709
+ parentStepIndex: 0,
710
+ depth: 1,
711
+ path: [{ runId: "child-1", stepIndex: 0 }],
712
+ state: "running",
713
+ agent: "nested-reviewer",
714
+ }],
715
+ });
716
+ state.foregroundControls.set("child-2", {
717
+ runId: "child-2",
718
+ parentWorkflowRunId: "workflow-1",
719
+ workflowKey: "test",
720
+ mode: "single",
721
+ startedAt: 12,
722
+ updatedAt: 20,
723
+ activeChildren: new Map([[0, { index: 0, agent: "tester", startedAt: 12, updatedAt: 20 }]]),
724
+ nestedChildren: [{
725
+ id: "nested-test",
726
+ parentRunId: "child-2",
727
+ parentStepIndex: 0,
728
+ depth: 1,
729
+ path: [{ runId: "child-2", stepIndex: 0 }],
730
+ state: "running",
731
+ agent: "nested-tester",
732
+ }],
733
+ });
734
+ const entries = collectFleetStatusEntries(state);
735
+ assert.deepEqual(entries.map((entry) => [entry.key, entry.parentKey]), [
736
+ ["async:workflow-1", undefined],
737
+ ["foreground-active:child-1:0", "async:workflow-1"],
738
+ ["foreground-active:child-2:0", "async:workflow-1"],
739
+ ]);
740
+
741
+ let widgetFactory: ((tui: unknown, theme: typeof theme) => { render(width: number): string[] }) | undefined;
742
+ const ctx = {
743
+ hasUI: true,
744
+ ui: {
745
+ setWidget(_key: string, content: typeof widgetFactory | undefined) { if (content) widgetFactory = content; },
746
+ onTerminalInput() { return () => {}; },
747
+ getEditorText() { return ""; },
748
+ requestRender() {},
749
+ notify() {},
750
+ theme,
751
+ },
752
+ } as unknown as ExtensionContext;
753
+ const fleet = new SubagentFleetStatus(state, () => {}, { refreshMs: 60_000 });
754
+ try {
755
+ fleet.setContext(ctx);
756
+ const component = widgetFactory!({ requestRender() {}, focusedComponent: Object.create(Editor.prototype) as Editor }, theme);
757
+ const compact = component.render(120);
758
+ assert.match(compact[0]!, /2 active agents/, "workflow wrappers must not be counted as leaf agents");
759
+ assert.doesNotMatch(compact[0]!, /3 active agents/);
760
+ assert.deepEqual(fleet.handleKey("\x1b[B"), { consume: true });
761
+ const lines = component.render(120);
762
+ const workflowIndex = lines.findIndex((line) => line.includes("workflow · running"));
763
+ const childIndex = lines.findIndex((line) => line.includes("reviewer · running"));
764
+ const nestedIndex = lines.findIndex((line) => line.includes("nested-reviewer"));
765
+ const secondChildIndex = lines.findIndex((line) => line.includes("tester · running"));
766
+ const secondNestedIndex = lines.findIndex((line) => line.includes("nested-tester"));
767
+ const ownerNestedIndex = lines.findIndex((line) => line.includes("owner-nested"));
768
+ assert.ok(workflowIndex >= 0 && childIndex > workflowIndex && nestedIndex > childIndex);
769
+ assert.ok(secondChildIndex > nestedIndex && secondNestedIndex > secondChildIndex && ownerNestedIndex > secondNestedIndex);
770
+ assert.match(lines[childIndex]!, /├─.*reviewer/);
771
+ assert.match(lines[nestedIndex]!, /├─.*nested-reviewer/);
772
+ assert.match(lines[secondNestedIndex]!, /├─.*nested-tester/);
773
+ assert.match(lines[ownerNestedIndex]!, /└─.*owner-nested/);
774
+ } finally {
775
+ fleet.dispose();
776
+ }
777
+ });
778
+
779
+ it("renders recursive nested runs and steps within the existing row budget", () => {
780
+ const state = stateForTest();
781
+ state.asyncJobs.set("supervisor", {
782
+ asyncId: "supervisor",
783
+ asyncDir: "/tmp/supervisor",
784
+ status: "running",
785
+ mode: "single",
786
+ startedAt: 10,
787
+ updatedAt: 20,
788
+ steps: [{ agent: "supervisor", index: 0, status: "running" }],
789
+ nestedChildren: [{
790
+ id: "level-one",
791
+ parentRunId: "supervisor",
792
+ parentStepIndex: 0,
793
+ depth: 1,
794
+ path: [{ runId: "supervisor", stepIndex: 0 }],
795
+ state: "running",
796
+ agent: "level-one",
797
+ children: [{
798
+ id: "level-two",
799
+ parentRunId: "level-one",
800
+ depth: 2,
801
+ path: [{ runId: "supervisor", stepIndex: 0 }, { runId: "level-one" }],
802
+ state: "running",
803
+ mode: "parallel",
804
+ steps: [{
805
+ agent: "level-two",
806
+ status: "running",
807
+ children: [0, 1, 2, 3].map((index) => ({
808
+ id: `leaf-${index}`,
809
+ parentRunId: "level-two",
810
+ depth: 3,
811
+ path: [{ runId: "level-two" }],
812
+ state: "running" as const,
813
+ agent: `leaf-${index}`,
814
+ })),
815
+ }],
816
+ }],
817
+ }],
818
+ });
819
+ let widgetFactory: ((tui: unknown, theme: typeof theme) => { render(width: number): string[] }) | undefined;
820
+ const ctx = {
821
+ hasUI: true,
822
+ ui: {
823
+ setWidget(_key: string, content: typeof widgetFactory | undefined) { if (content) widgetFactory = content; },
824
+ onTerminalInput() { return () => {}; },
825
+ getEditorText() { return ""; },
826
+ requestRender() {},
827
+ notify() {},
828
+ theme,
829
+ },
830
+ } as unknown as ExtensionContext;
831
+ const fleet = new SubagentFleetStatus(state, () => {}, { refreshMs: 60_000 });
832
+ try {
833
+ fleet.setContext(ctx);
834
+ const component = widgetFactory!({ requestRender() {}, focusedComponent: Object.create(Editor.prototype) as Editor }, theme);
835
+ assert.deepEqual(fleet.handleKey("\x1b[B"), { consume: true });
836
+ const lines = component.render(140).join("\n");
837
+ assert.match(lines, /level-one/);
838
+ assert.match(lines, /level-two/);
839
+ assert.match(lines, /leaf-0/);
840
+ assert.match(lines, /leaf-1/);
841
+ assert.doesNotMatch(lines, /leaf-[23]/);
842
+ assert.match(lines, /\+2 nested leaves/);
843
+ } finally {
844
+ fleet.dispose();
845
+ }
846
+ });
847
+
597
848
  it("uses the same item keys as the full inspector", () => {
598
849
  const state = stateForTest();
599
850
  state.foregroundControls.set("foreground", {