@selesai/code 0.8.5 → 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 (231) hide show
  1. package/CHANGELOG.md +28 -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/models.json +5 -0
  16. package/dist/defaults/settings.json +1 -0
  17. package/dist/extensions/agent-browser.test.ts +178 -2
  18. package/dist/extensions/agent-browser.ts +2 -0
  19. package/dist/extensions/copy-turn.test.ts +229 -0
  20. package/dist/extensions/copy-turn.ts +11 -0
  21. package/dist/extensions/grep-app/index.test.ts +356 -1
  22. package/dist/extensions/grep-app/index.ts +6 -1
  23. package/dist/extensions/handoff-new.test.ts +351 -161
  24. package/dist/extensions/handoff-new.ts +3 -0
  25. package/dist/extensions/inline-skills.test.ts +93 -0
  26. package/dist/extensions/inline-skills.ts +3 -0
  27. package/dist/extensions/model-prompt-injector/config.json +10 -0
  28. package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
  29. package/dist/extensions/model-prompt-injector/index.ts +151 -0
  30. package/dist/extensions/package.json +1 -0
  31. package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
  32. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
  33. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  34. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  35. package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
  36. package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
  37. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  38. package/dist/extensions/pi-subagents/agents/worker.md +1 -1
  39. package/dist/extensions/pi-subagents/docs/configuration.md +78 -11
  40. package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
  41. package/dist/extensions/pi-subagents/docs/missions.md +5 -3
  42. package/dist/extensions/pi-subagents/docs/models.md +1 -1
  43. package/dist/extensions/pi-subagents/docs/observability.md +42 -2
  44. package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
  45. package/dist/extensions/pi-subagents/docs/workflows.md +4 -4
  46. package/dist/extensions/pi-subagents/install.mjs +2 -2
  47. package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
  48. package/dist/extensions/pi-subagents/package.json +1 -1
  49. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
  50. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  51. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +16 -15
  52. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  53. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  54. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  55. package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
  56. package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
  57. package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
  58. package/dist/extensions/pi-subagents/src/extension/config.ts +59 -0
  59. package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
  60. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
  61. package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
  62. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
  63. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
  64. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
  65. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
  66. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
  67. package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
  68. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
  69. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
  70. package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
  71. package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
  72. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
  73. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
  74. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
  75. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +17 -6
  76. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
  77. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
  78. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +11 -3
  79. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
  80. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
  81. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
  82. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  83. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
  84. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
  85. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
  86. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
  87. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +35 -26
  88. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
  89. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
  90. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
  91. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
  92. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
  93. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
  94. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  95. package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
  96. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
  97. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
  98. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  99. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
  100. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
  101. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
  102. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
  103. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
  104. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
  105. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
  106. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
  107. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  108. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
  109. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
  110. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
  112. package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
  113. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
  114. package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
  115. package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
  116. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  117. package/dist/extensions/pi-subagents/src/shared/types.ts +66 -4
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
  120. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
  121. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
  122. package/dist/extensions/pi-subagents/src/tui/fleet.ts +129 -17
  123. package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
  124. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
  125. package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
  126. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
  127. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
  128. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  129. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
  133. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
  134. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
  135. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
  136. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
  137. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
  138. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
  139. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
  140. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
  141. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
  142. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
  143. package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
  144. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
  145. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
  146. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
  147. package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
  148. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
  149. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  150. package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
  152. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
  153. package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
  154. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
  155. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
  156. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  157. package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
  158. package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
  159. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +91 -0
  160. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
  161. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
  162. package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
  163. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
  164. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
  165. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
  166. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
  167. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
  168. package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
  169. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
  170. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
  171. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
  172. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
  173. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
  174. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
  175. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
  176. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
  177. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
  178. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
  179. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
  180. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
  181. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
  182. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
  183. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  184. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
  185. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
  186. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
  187. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
  188. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
  189. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
  190. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
  191. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
  192. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  193. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
  194. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
  195. package/dist/extensions/ponytail/index.js +11 -0
  196. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  197. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  198. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  199. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  200. package/dist/extensions/question/index.ts +33 -0
  201. package/dist/extensions/question/question-list.ts +28 -0
  202. package/dist/extensions/question/row-layout.ts +3 -0
  203. package/dist/extensions/question/tests/batch.test.ts +103 -65
  204. package/dist/extensions/question/tests/exp.test.ts +70 -0
  205. package/dist/extensions/question/tests/exp2.test.ts +59 -0
  206. package/dist/extensions/question/tests/exp3.test.ts +47 -0
  207. package/dist/extensions/question/tests/helpers.test.ts +75 -35
  208. package/dist/extensions/question/tests/question-list.test.ts +640 -198
  209. package/dist/extensions/question/tests/row-layout.test.ts +193 -114
  210. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  211. package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
  212. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  213. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  214. package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
  215. package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
  216. package/dist/extensions/rtk.test.ts +180 -1
  217. package/dist/extensions/tokenin-onboarding.ts +3 -0
  218. package/dist/extensions/undo.test.ts +720 -0
  219. package/dist/extensions/undo.ts +6 -0
  220. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  221. package/dist/extensions/workflow/extension.ts +3 -3
  222. package/dist/extensions/workflow/modes.ts +41 -19
  223. package/dist/skills/pi-subagents/SKILL.md +1 -1
  224. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
  225. package/dist/skills/pi-subagents/references/execution-controls.md +16 -15
  226. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
  227. package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
  228. package/docs/plans/workflow-autoloop-reference.md +1 -2
  229. package/docs/plans/workflow-handoff-carryover.md +108 -0
  230. package/docs/settings.md +6 -0
  231. package/package.json +4 -1
@@ -4,6 +4,7 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { describe, it } from "node:test";
6
6
  import { visibleWidth, type MarkdownTheme } from "@earendil-works/pi-tui";
7
+ import { EXTERNAL_RUN_REGISTRY_KEY, EXTERNAL_RUN_REGISTRY_VERSION, registerExternalRun } from "../../src/api/external-runs.ts";
7
8
  import { collectFleetSnapshot, openSubagentFleet, SubagentFleetComponent } from "../../src/tui/fleet.ts";
8
9
  import { persistForegroundRunHistory, restoreForegroundRunHistory } from "../../src/runs/foreground/foreground-history.ts";
9
10
  import { FLEET_STATUS_WIDGET_KEY } from "../../src/tui/fleet-status.ts";
@@ -13,6 +14,10 @@ import type { SubagentState } from "../../src/shared/types.ts";
13
14
  import { fauxAssistantMessage } from "@earendil-works/pi-ai/providers/faux";
14
15
  import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
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(),
@@ -168,6 +173,138 @@ describe("native subagent fleet", () => {
168
173
  }
169
174
  });
170
175
 
176
+ it("shows cached external jobs as display-only without reading paths or invoking controls", async () => {
177
+ clearExternalRuns();
178
+ registerExternalRun({
179
+ id: "external-review",
180
+ sessionId: "session-current",
181
+ source: "interactive-shell",
182
+ label: "Dependency review",
183
+ state: "completed",
184
+ startedAt: 100,
185
+ updatedAt: 200,
186
+ currentAction: "Inspecting package metadata",
187
+ preview: "No dependency blockers found.",
188
+ reportPath: "/outside/trusted/roots/report.md",
189
+ transcriptPath: "/outside/trusted/roots/transcript.jsonl",
190
+ });
191
+ const state = stateForTest();
192
+ const snapshot = collectFleetSnapshot(state);
193
+ assert.equal(snapshot.error, undefined);
194
+ assert.deepEqual(snapshot.items.map((item) => ({ key: item.key, kind: item.kind, agent: item.agent })), [{
195
+ key: "external:external-review",
196
+ kind: "external",
197
+ agent: "Dependency review",
198
+ }]);
199
+ let steerCalls = 0;
200
+ let stopCalls = 0;
201
+ let inspectCalls = 0;
202
+ const component = new SubagentFleetComponent(
203
+ { terminal: { rows: 32, columns: 120 }, requestRender() {} } as never,
204
+ theme as never,
205
+ state,
206
+ () => {},
207
+ {
208
+ refreshMs: 60_000,
209
+ markdownTheme,
210
+ actions: {
211
+ async steer() { steerCalls++; return { text: "unexpected" }; },
212
+ stop() { stopCalls++; return { text: "unexpected" }; },
213
+ async inspect() { inspectCalls++; return { text: "unexpected" }; },
214
+ },
215
+ },
216
+ );
217
+ try {
218
+ const initial = component.render(120).join("\n");
219
+ assert.match(initial, /Fleet inspector.*external display-only/);
220
+ assert.match(initial, /Source: external · display-only/);
221
+ assert.match(initial, /Owner: interactive-shell/);
222
+ assert.match(initial, /Inspecting package metadata/);
223
+ assert.match(initial, /Elapsed: 100ms/);
224
+ assert.match(initial, /No dependency blockers found/);
225
+ assert.match(initial, /Report path: \/outside\/trusted\/roots\/report\.md/);
226
+ assert.match(initial, /Transcript path: \/outside\/trusted\/roots\/transcript\.jsonl/);
227
+ assert.doesNotMatch(initial, /Herdr ·|steer ·|stop ·/);
228
+ component.handleInput("H");
229
+ assert.match(component.render(120).join("\n"), /display-only and have no Herdr controls/);
230
+ component.handleInput("s");
231
+ assert.match(component.render(120).join("\n"), /display-only and remain controlled/);
232
+ component.handleInput("D");
233
+ assert.match(component.render(120).join("\n"), /display-only and remain controlled/);
234
+ await new Promise((resolve) => setImmediate(resolve));
235
+ assert.deepEqual({ steerCalls, stopCalls, inspectCalls }, { steerCalls: 0, stopCalls: 0, inspectCalls: 0 });
236
+ } finally {
237
+ component.dispose();
238
+ clearExternalRuns();
239
+ }
240
+ });
241
+
242
+ it("keeps running external elapsed time live when endedAt is present", () => {
243
+ clearExternalRuns();
244
+ try {
245
+ const startedAt = Date.now() - 5_000;
246
+ registerExternalRun({
247
+ id: "external-running",
248
+ sessionId: "session-current",
249
+ source: "interactive-shell",
250
+ label: "Running external job",
251
+ state: "running",
252
+ startedAt,
253
+ updatedAt: startedAt + 100,
254
+ endedAt: startedAt + 100,
255
+ });
256
+ const component = new SubagentFleetComponent(
257
+ { terminal: { rows: 32, columns: 120 }, requestRender() {} } as never,
258
+ theme as never,
259
+ stateForTest(),
260
+ () => {},
261
+ { refreshMs: 60_000, markdownTheme },
262
+ );
263
+ try {
264
+ const rendered = component.render(120).join("\n");
265
+ assert.match(rendered, /Elapsed: 5\.0s/);
266
+ assert.doesNotMatch(rendered, /Elapsed: 100ms/);
267
+ } finally {
268
+ component.dispose();
269
+ }
270
+ } finally {
271
+ clearExternalRuns();
272
+ }
273
+ });
274
+
275
+ it("ignores malformed cached external jobs while rendering Fleet", () => {
276
+ clearExternalRuns();
277
+ const warnings: string[] = [];
278
+ const originalWarn = console.warn;
279
+ console.warn = (message?: unknown) => warnings.push(String(message));
280
+ (globalThis as Record<PropertyKey, unknown>)[Symbol.for(EXTERNAL_RUN_REGISTRY_KEY)] = {
281
+ version: EXTERNAL_RUN_REGISTRY_VERSION,
282
+ runs: new Map<string, unknown>([[
283
+ "session-current\0bad",
284
+ { id: "bad", sessionId: "session-current", source: "tool", label: "Bad external", state: "running", startedAt: Number.NaN },
285
+ ]]),
286
+ };
287
+ const state = stateForTest();
288
+ const component = new SubagentFleetComponent(
289
+ { terminal: { rows: 28, columns: 100 }, requestRender() {} } as never,
290
+ theme as never,
291
+ state,
292
+ () => {},
293
+ { refreshMs: 60_000, markdownTheme },
294
+ );
295
+ try {
296
+ const snapshot = collectFleetSnapshot(state);
297
+ assert.equal(snapshot.error, undefined);
298
+ assert.deepEqual(snapshot.items, []);
299
+ assert.match(warnings[0]!, /Removed Malformed cached external run/);
300
+ assert.match(component.render(100).join("\n"), /No current-session Fleet jobs/);
301
+ } finally {
302
+ console.warn = originalWarn;
303
+ component.dispose();
304
+ clearExternalRuns();
305
+ }
306
+ });
307
+
171
308
  it("shows live prompt summaries and opens Prompt Audit with the authored prompt visible", async () => {
172
309
  const sentinel = "PROMPT_AUDIT_SENTINEL_1021";
173
310
  const secondSentinel = "PROMPT_AUDIT_SECOND_CHILD";
@@ -367,16 +504,26 @@ describe("native subagent fleet", () => {
367
504
  assert.equal(snapshot.items[0]?.agent, "workflow");
368
505
  });
369
506
 
370
- it("labels workflow-owned foreground children and opens their parent Herdr inspector", async () => {
507
+ it("hides workflow-owned foreground duplicates and opens the workflow parent inspector", async () => {
371
508
  const state = stateForTest();
372
509
  state.asyncJobs.set("workflow-1", {
373
510
  asyncId: "workflow-1",
374
511
  asyncDir: "/tmp/workflow-1",
512
+ sessionId: "session-current",
375
513
  status: "running",
376
514
  mode: "workflow",
377
515
  startedAt: 10,
378
516
  updatedAt: 20,
379
517
  });
518
+ state.asyncJobs.set("unrelated", {
519
+ asyncId: "unrelated",
520
+ asyncDir: "/tmp/unrelated",
521
+ sessionId: "session-current",
522
+ status: "running",
523
+ mode: "single",
524
+ startedAt: 5,
525
+ updatedAt: 30,
526
+ });
380
527
  state.foregroundControls.set("child-1", {
381
528
  runId: "child-1",
382
529
  parentWorkflowRunId: "workflow-1",
@@ -387,8 +534,7 @@ describe("native subagent fleet", () => {
387
534
  activeChildren: new Map([[0, { index: 0, agent: "reviewer", startedAt: 10, updatedAt: 20 }]]),
388
535
  });
389
536
  const snapshot = collectFleetSnapshot(state);
390
- const child = snapshot.items.find((item) => item.key === "foreground-active:child-1:0");
391
- assert.equal(child?.kind, "foreground-active");
537
+ assert.deepEqual(snapshot.items.map((item) => item.key), ["async:unrelated", "async:workflow-1"]);
392
538
  const calls: Array<{ runId: string; asyncDir: string; index?: number }> = [];
393
539
  const component = new SubagentFleetComponent(
394
540
  { terminal: { rows: 28, columns: 100 }, requestRender() {} } as never,
@@ -406,7 +552,7 @@ describe("native subagent fleet", () => {
406
552
  },
407
553
  );
408
554
  try {
409
- assert.ok(component.render(100).some((line) => line.includes("Workflow child of: workflow-1 (review)")));
555
+ assert.ok(component.render(100).some((line) => line.includes("workflow")));
410
556
  component.handleInput("H");
411
557
  await new Promise((resolve) => setImmediate(resolve));
412
558
  assert.deepEqual(calls, [{ runId: "workflow-1", asyncDir: "/tmp/workflow-1" }]);
@@ -536,6 +682,7 @@ describe("native subagent fleet", () => {
536
682
  fs.writeFileSync(outputPath, "restored foreground output", "utf-8");
537
683
 
538
684
  const state = stateForTest();
685
+ state.artifactDirPreference = "project";
539
686
  state.foregroundRuns!.set("restored", {
540
687
  runId: "restored",
541
688
  mode: "single",
@@ -556,6 +703,7 @@ describe("native subagent fleet", () => {
556
703
 
557
704
  const restored = stateForTest();
558
705
  restored.baseCwd = cwd;
706
+ restored.artifactDirPreference = "project";
559
707
  assert.equal(restoreForegroundRunHistory(restored, { resultsDir }), 1);
560
708
  const snapshot = collectFleetSnapshot(restored);
561
709
  assert.deepEqual(snapshot.items.map((item) => item.key), ["foreground-recent:restored:0"]);
@@ -636,6 +784,7 @@ describe("native subagent fleet", () => {
636
784
  const cwd = path.join(root, "project");
637
785
  const artifactsRoot = getProjectArtifactsDir(cwd);
638
786
  const state = stateForTest();
787
+ state.artifactDirPreference = "project";
639
788
  for (let index = 0; index < 3; index++) {
640
789
  const outputPath = path.join(artifactsRoot, "outputs", `run-${index}`, "output.md");
641
790
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
@@ -654,6 +803,7 @@ describe("native subagent fleet", () => {
654
803
 
655
804
  const restored = stateForTest();
656
805
  restored.baseCwd = cwd;
806
+ restored.artifactDirPreference = "project";
657
807
  assert.equal(restoreForegroundRunHistory(restored, { resultsDir, limit: 2 }), 2);
658
808
  assert.deepEqual([...restored.foregroundRuns!.keys()], ["run-2", "run-1"]);
659
809
 
@@ -802,6 +952,7 @@ describe("native subagent fleet", () => {
802
952
  fs.writeFileSync(path.join(asyncDir, "status.json"), "{in-flight status", "utf-8");
803
953
  const state = stateForTest();
804
954
  state.baseCwd = path.join(root, "parent-cwd");
955
+ state.artifactDirPreference = "project";
805
956
  state.asyncJobs.set("async-custom-cwd", {
806
957
  asyncId: "async-custom-cwd",
807
958
  asyncDir,
@@ -883,6 +1034,7 @@ describe("native subagent fleet", () => {
883
1034
  try {
884
1035
  const state = stateForTest();
885
1036
  state.baseCwd = path.join(root, "parent-cwd");
1037
+ state.artifactDirPreference = "project";
886
1038
  const effectiveCwd = path.join(root, "effective-cwd");
887
1039
  const now = Date.now();
888
1040
  state.foregroundControls.set("foreground-live", {
@@ -1282,6 +1434,40 @@ describe("native subagent fleet", () => {
1282
1434
  }
1283
1435
  });
1284
1436
 
1437
+ it("periodically redraws only while the overlay remains open", (t) => {
1438
+ t.mock.timers.enable({ apis: ["setTimeout"] });
1439
+ const state = stateForTest();
1440
+ let renderRequests = 0;
1441
+ let closed = false;
1442
+ const tui = { terminal: { rows: 28, columns: 90 }, requestRender: () => { renderRequests++; } };
1443
+ const component = new SubagentFleetComponent(
1444
+ tui as never,
1445
+ theme as never,
1446
+ state,
1447
+ () => { closed = true; },
1448
+ { refreshMs: 10 },
1449
+ );
1450
+ t.mock.timers.tick(249);
1451
+ assert.equal(renderRequests, 0, "refresh cadence is bounded away from a hot loop");
1452
+ t.mock.timers.tick(1);
1453
+ assert.equal(renderRequests, 1);
1454
+ component.handleInput("\x1b");
1455
+ assert.equal(closed, true);
1456
+ t.mock.timers.tick(1_000);
1457
+ assert.equal(renderRequests, 1, "closing cancels periodic redraws");
1458
+
1459
+ const disposed = new SubagentFleetComponent(
1460
+ tui as never,
1461
+ theme as never,
1462
+ state,
1463
+ () => {},
1464
+ { refreshMs: 250 },
1465
+ );
1466
+ disposed.dispose();
1467
+ t.mock.timers.tick(1_000);
1468
+ assert.equal(renderRequests, 1, "disposing cancels periodic redraws");
1469
+ });
1470
+
1285
1471
  it("refreshes the roster while the overlay remains open", async () => {
1286
1472
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-fleet-refresh-"));
1287
1473
  try {
@@ -1293,7 +1479,7 @@ describe("native subagent fleet", () => {
1293
1479
  theme as never,
1294
1480
  state,
1295
1481
  () => {},
1296
- { asyncDirRoot: root, resultsDir: path.join(root, "results"), refreshMs: 10 },
1482
+ { asyncDirRoot: root, resultsDir: path.join(root, "results"), refreshMs: 250 },
1297
1483
  );
1298
1484
  let invalidations = 0;
1299
1485
  const originalInvalidate = component.invalidate.bind(component);
@@ -1305,7 +1491,7 @@ describe("native subagent fleet", () => {
1305
1491
  assert.ok(component.render(90).some((line) => line.includes("No tracked children")));
1306
1492
  const initialOutput = Array.from({ length: 40 }, (_, index) => `output line ${index}`).join("\n");
1307
1493
  const asyncDir = writeAsyncRun(root, { id: "appeared-live", output: initialOutput });
1308
- await new Promise((resolve) => setTimeout(resolve, 35));
1494
+ await new Promise((resolve) => setTimeout(resolve, 275));
1309
1495
  let lines = component.render(90);
1310
1496
  assert.ok(lines.some((line) => line.includes("appeared")));
1311
1497
  assert.ok(lines.some((line) => line.includes("output line 39")));
@@ -0,0 +1,103 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+ import { describe, it } from "node:test";
6
+
7
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
8
+
9
+ describe("handoff workflow carry-over", () => {
10
+ it("adopts the previous session's active async workflows and tells the agent", () => {
11
+ const script = String.raw`
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import fs from "node:fs";
15
+ import { DIRS } from "./src/shared/types.ts";
16
+ import registerSubagentExtension from "./index.ts";
17
+ const handlers = new Map();
18
+ const events = { listeners: new Map(), on(name, handler) { this.listeners.set(name, handler); return () => this.listeners.delete(name); }, emit(name, payload) { this.listeners.get(name)?.(payload); } };
19
+ const sent = [];
20
+ const widgets = [];
21
+ const pi = new Proxy({
22
+ events,
23
+ on(name, handler) { handlers.set(name, handler); },
24
+ registerTool() {}, registerCommand() {}, registerShortcut() {}, registerMessageRenderer() {},
25
+ sendMessage(message, options) { sent.push({ message, options }); }, getSessionName() { return undefined; },
26
+ }, { get(target, prop) { return prop in target ? target[prop] : () => undefined; } });
27
+
28
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "selesai-handoff-"));
29
+ const oldSession = path.join(tmp, "old-session.jsonl");
30
+ const newSession = path.join(tmp, "new-session.jsonl");
31
+ fs.writeFileSync(oldSession, JSON.stringify({ type: "session", version: 1, id: "old-session", timestamp: new Date().toISOString(), cwd: process.cwd() }) + "\n");
32
+ fs.writeFileSync(newSession, JSON.stringify({ type: "session", version: 1, id: "new-session", timestamp: new Date().toISOString(), cwd: process.cwd(), parentSession: oldSession }) + "\n");
33
+
34
+ // One active async workflow owned by the previous session.
35
+ const runId = "adopted-workflow-run";
36
+ const asyncDir = path.join(DIRS.async, runId);
37
+ fs.mkdirSync(asyncDir, { recursive: true });
38
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
39
+ runId,
40
+ state: "running",
41
+ sessionId: oldSession,
42
+ mode: "workflow",
43
+ workflowKey: "task",
44
+ agent: "builder",
45
+ startedAt: Date.now(),
46
+ steps: [{ agent: "builder", status: "running", phase: "build-1", description: "Implement the carry-over plan" }],
47
+ }));
48
+ fs.mkdirSync(path.join(DIRS.async, ".active-runs"), { recursive: true });
49
+ fs.writeFileSync(path.join(DIRS.async, ".active-runs", runId), "");
50
+
51
+ // A result file for an old-session run written during the switch window.
52
+ const resultRunId = "adopted-completed-run";
53
+ const resultPath = path.join(DIRS.results, resultRunId + ".json");
54
+ fs.mkdirSync(DIRS.results, { recursive: true });
55
+ fs.writeFileSync(resultPath, JSON.stringify({ runId: resultRunId, sessionId: oldSession, agent: "explorer", summary: "done", success: true }));
56
+
57
+ const ctx = { cwd: process.cwd(), hasUI: true, ui: { setWidget(key, value) { widgets.push([key, value]); }, requestRender() {}, onTerminalInput() { return () => {}; }, getEditorText() { return ""; }, notify() {}, theme: { fg(_name, text) { return text; }, bg(_name, text) { return text; }, bold(text) { return text; } } }, sessionManager: { getSessionId() { return "new-session"; }, getSessionFile() { return newSession; }, getHeader() { return { parentSession: oldSession }; }, getEntries() { return []; } }, modelRegistry: { getAvailable() { return []; } } };
58
+
59
+ registerSubagentExtension(pi);
60
+ handlers.get("session_start")({ reason: "new", previousSessionFile: oldSession }, ctx);
61
+ sent.length = 0;
62
+
63
+ // No handoff notice until a turn settles.
64
+ if (sent.length !== 0) throw new Error("handoff notice sent before the first settled turn");
65
+
66
+ handlers.get("agent_settled")();
67
+ const handoff = sent.filter((entry) => entry.message?.customType === "subagent-handoff-resume");
68
+ if (handoff.length !== 1) throw new Error(JSON.stringify(sent.map((entry) => entry.message)));
69
+ if (handoff[0].options?.triggerTurn !== true) throw new Error("handoff notice did not trigger a turn");
70
+ if (typeof handoff[0].message?.content !== "string" || !handoff[0].message.content.includes(runId) || !handoff[0].message.content.includes("carried over")) {
71
+ throw new Error("handoff notice lacks the adopted workflow: " + JSON.stringify(handoff[0].message));
72
+ }
73
+
74
+ // Only once per adoption.
75
+ handlers.get("agent_settled")();
76
+ if (sent.filter((entry) => entry.message?.customType === "subagent-handoff-resume").length !== 1) throw new Error("handoff notice repeated");
77
+
78
+ // Compaction resume message now lists the live workflows.
79
+ handlers.get("session_before_compact")({ reason: "threshold", signal: new AbortController().signal });
80
+ handlers.get("session_compact")({ reason: "threshold" });
81
+ const compaction = sent.filter((entry) => entry.message?.customType === "subagent-compaction-resume");
82
+ if (compaction.length !== 1) throw new Error("compaction resume missing: " + JSON.stringify(sent.map((entry) => entry.message?.customType)));
83
+ if (typeof compaction[0].message?.content !== "string" || !compaction[0].message.content.includes(runId)) {
84
+ throw new Error("compaction resume lacks the workflow manifest: " + JSON.stringify(compaction[0].message));
85
+ }
86
+
87
+ // Adopted old-session result gets delivered and cleaned up (batched
88
+ // notification debounces, so poll with a generous bound).
89
+ let delivered = false;
90
+ for (let attempt = 0; attempt < 40 && !delivered; attempt++) {
91
+ await new Promise((resolve) => setTimeout(resolve, 250));
92
+ delivered = !fs.existsSync(resultPath);
93
+ }
94
+ if (!delivered) throw new Error("adopted result file was not delivered");
95
+
96
+ await handlers.get("session_shutdown")();
97
+ `;
98
+ const env = { ...process.env };
99
+ delete env.SELESAI_SUBAGENT_CHILD;
100
+ execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env, stdio: "pipe" });
101
+ assert.ok(true);
102
+ });
103
+ });
@@ -54,6 +54,38 @@ describe("Herdr inspector bootstrap", () => {
54
54
  }
55
55
  });
56
56
 
57
+ it("uses PATH node when Pi is a standalone executable", async () => {
58
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-herdr-bootstrap-standalone-"));
59
+ const originalExecPath = process.execPath;
60
+ try {
61
+ writeCompletedRun(root);
62
+ process.execPath = path.join(root, process.platform === "win32" ? "pi.exe" : "pi");
63
+ const calls: string[][] = [];
64
+ const client: HerdrClient = {
65
+ run: async <T>(args: string[]) => {
66
+ calls.push(args);
67
+ if (args[0] === "--version") return { ok: true, data: "herdr 0.7.5" as T };
68
+ if (args[0] === "pane" && args[1] === "split") return { ok: true, data: { pane: { pane_id: "w1:p9" } } as T };
69
+ return { ok: true, data: {} as T };
70
+ },
71
+ };
72
+ const opened = await handleHerdrInspectorAction("inspector.open", { id: "run-123" }, {
73
+ cwd: root,
74
+ asyncDirRoot: root,
75
+ resultsDir: path.join(root, "results"),
76
+ client,
77
+ });
78
+ assert.equal(opened.isError, undefined);
79
+ const command = calls.find((args) => args[0] === "pane" && args[1] === "run")?.[3] ?? "";
80
+ assert.equal(command.startsWith("& "), process.platform === "win32");
81
+ assert.match(command, process.platform === "win32" ? /^& "node\.exe" / : /^node /);
82
+ assert.doesNotMatch(command, /(?:^|[\\/])pi(?:\.exe)?'/);
83
+ } finally {
84
+ process.execPath = originalExecPath;
85
+ fs.rmSync(root, { recursive: true, force: true });
86
+ }
87
+ });
88
+
57
89
  it("starts the packaged bootstrap with ordinary Node", () => {
58
90
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-herdr-bootstrap-run-"));
59
91
  try {
@@ -0,0 +1,59 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { describe, it } from "node:test";
7
+ import { formatShellCommand } from "../../src/inspectors/herdr/shell-command.ts";
8
+
9
+ describe("formatShellCommand on POSIX shells", () => {
10
+ const platform: NodeJS.Platform = "linux";
11
+
12
+ it("keeps a shell-safe executable bare so Nushell parses it as a command", () => {
13
+ assert.equal(
14
+ formatShellCommand("/usr/bin/node", ["/home/u/.pi/inspector-runner.mjs", "--async-dir", "/tmp/run-1|fc_abc"], platform),
15
+ "/usr/bin/node '/home/u/.pi/inspector-runner.mjs' '--async-dir' '/tmp/run-1|fc_abc'",
16
+ );
17
+ });
18
+
19
+ it("keeps bare command names bare", () => {
20
+ assert.equal(formatShellCommand("pi", ["review the diff"], platform), "pi 'review the diff'");
21
+ });
22
+
23
+ it("uses a bare sh invoker for an executable containing spaces and equals", () => {
24
+ assert.equal(
25
+ formatShellCommand("/opt/My Tools/node=bin", ["--flag"], platform),
26
+ `sh -c 'exec "$0" "$@"' '/opt/My Tools/node=bin' '--flag'`,
27
+ );
28
+ });
29
+
30
+ it("quotes executable paths and arguments containing quotes", () => {
31
+ assert.equal(
32
+ formatShellCommand("/opt/My Tools/it's node", ["it's", "a|b"], platform),
33
+ "sh -c 'exec \"$0\" \"$@\"' '/opt/My Tools/it'\\''s node' 'it'\\''s' 'a|b'",
34
+ );
35
+ });
36
+
37
+ it("executes an equals-sign path through the shell wrapper", { skip: process.platform === "win32" }, () => {
38
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi herdr shell="));
39
+ try {
40
+ const executable = path.join(root, "tool=' value");
41
+ fs.writeFileSync(executable, "#!/bin/sh\nprintf '%s' \"$1\"\n", "utf-8");
42
+ fs.chmodSync(executable, 0o755);
43
+ const launched = spawnSync("/bin/sh", ["-c", formatShellCommand(executable, ["arg=' value"], platform)], { encoding: "utf-8" });
44
+ assert.equal(launched.status, 0, launched.stderr);
45
+ assert.equal(launched.stdout, "arg=' value");
46
+ } finally {
47
+ fs.rmSync(root, { recursive: true, force: true });
48
+ }
49
+ });
50
+ });
51
+
52
+ describe("formatShellCommand on Windows PowerShell", () => {
53
+ it("prefixes the call operator and preserves argument quoting", () => {
54
+ assert.equal(
55
+ formatShellCommand("C:\\Program Files\\nodejs\\node.exe", ["two words", 'say "hi"'], "win32"),
56
+ '& "C:\\Program Files\\nodejs\\node.exe" "two words" "say \\"hi\\""',
57
+ );
58
+ });
59
+ });
@@ -103,6 +103,28 @@ describe("subagent extension child mode", () => {
103
103
  execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env: parentToolEnv(), stdio: "pipe" });
104
104
  });
105
105
 
106
+ it("declares packaged prompts through resources_discover", () => {
107
+ const script = String.raw`
108
+ import registerSubagentExtension from "./index.ts";
109
+ import * as fs from "node:fs";
110
+ const handlers = new Map();
111
+ const fakePi = new Proxy({
112
+ events: { on() { return () => {}; }, emit() {} },
113
+ registerTool() {}, registerCommand() {}, registerShortcut() {}, registerMessageRenderer() {}, sendMessage() {}, getSessionName() {},
114
+ on(event, handler) { handlers.set(event, handler); },
115
+ }, { get(target, prop) { return prop in target ? target[prop] : () => undefined; } });
116
+ registerSubagentExtension(fakePi);
117
+ const handler = handlers.get("resources_discover");
118
+ if (!handler) throw new Error("resources_discover handler not registered");
119
+ const result = handler({ type: "resources_discover", cwd: process.cwd(), reason: "startup" });
120
+ if (!result || !Array.isArray(result.promptPaths) || result.promptPaths.length !== 1) throw new Error("expected one promptPath, got " + JSON.stringify(result));
121
+ const promptDir = result.promptPaths[0];
122
+ if (!fs.existsSync(promptDir + "/review-loop.md")) throw new Error("review-loop.md not found under " + promptDir);
123
+ if (!fs.existsSync(promptDir + "/parallel-review.md")) throw new Error("parallel-review.md not found under " + promptDir);
124
+ `;
125
+ execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env: parentToolEnv(), stdio: "pipe" });
126
+ });
127
+
106
128
  it("shows omitted workflow async as background even when asyncByDefault is false", () => {
107
129
  const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-workflow-manifest-config-"));
108
130
  try {
@@ -346,6 +368,75 @@ describe("subagent extension child mode", () => {
346
368
  }
347
369
  });
348
370
 
371
+ it("uses configured main-window renderer spacing for call rows", () => {
372
+ const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-renderer-density-config-"));
373
+ try {
374
+ const configDir = path.join(agentDir, "extensions", "subagent");
375
+ fs.mkdirSync(configDir, { recursive: true });
376
+ fs.writeFileSync(path.join(configDir, "config.json"), JSON.stringify({ mainWindowRenderer: { horizontalSpacing: 0 } }), "utf-8");
377
+
378
+ const script = String.raw`
379
+ import registerSubagentExtension from "./index.ts";
380
+ const events = { on() { return () => {}; }, emit() {} };
381
+ let registeredTool;
382
+ const fakePi = new Proxy({
383
+ events,
384
+ registerTool(tool) { if (tool.name === "subagent") registeredTool = tool; },
385
+ registerCommand() {}, registerShortcut() {}, registerMessageRenderer() {}, sendMessage() {}, getSessionName() {},
386
+ }, { get(target, prop) { return prop in target ? target[prop] : () => undefined; } });
387
+ registerSubagentExtension(fakePi);
388
+ if (!registeredTool) throw new Error("tool not registered");
389
+ const theme = { fg(_name, text) { return text; }, bold(text) { return text; } };
390
+ const call = registeredTool.renderCall({ agent: "worker", async: true }, theme).render(120).map((line) => line.trimEnd());
391
+ if (call.length !== 1 || call[0] !== "subagentworker[async]") throw new Error("unexpected call row: " + JSON.stringify(call));
392
+ `;
393
+ const env = parentToolEnv();
394
+ env.SELESAI_CODING_AGENT_DIR = agentDir;
395
+ execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env, stdio: "pipe" });
396
+ } finally {
397
+ fs.rmSync(agentDir, { recursive: true, force: true });
398
+ }
399
+ });
400
+
401
+ it("uses configured main-window renderer density for slash results", () => {
402
+ const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-slash-renderer-density-config-"));
403
+ try {
404
+ const configDir = path.join(agentDir, "extensions", "subagent");
405
+ fs.mkdirSync(configDir, { recursive: true });
406
+ fs.writeFileSync(path.join(configDir, "config.json"), JSON.stringify({ mainWindowRenderer: { horizontalSpacing: 0, compactResultMaxLines: 3 } }), "utf-8");
407
+
408
+ const script = String.raw`
409
+ import registerSubagentExtension from "./index.ts";
410
+ const events = { on() { return () => {}; }, emit() {} };
411
+ let slashRenderer;
412
+ const fakePi = new Proxy({
413
+ events,
414
+ registerTool() {}, registerCommand() {}, registerShortcut() {}, sendMessage() {}, getSessionName() {},
415
+ registerMessageRenderer(type, renderer) { if (type === "subagent-slash-result") slashRenderer = renderer; },
416
+ }, { get(target, prop) { return prop in target ? target[prop] : () => undefined; } });
417
+ registerSubagentExtension(fakePi);
418
+ if (!slashRenderer) throw new Error("slash renderer not registered");
419
+ const result = slashRenderer({ details: {
420
+ requestId: "slash-density",
421
+ result: { content: [{ type: "text", text: "done" }], details: { mode: "parallel", results: [
422
+ { agent: "scout", task: "a", exitCode: 0, messages: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } },
423
+ { agent: "reviewer", task: "b", exitCode: 0, messages: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } },
424
+ { agent: "writer", task: "c", exitCode: 0, messages: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } },
425
+ ] } },
426
+ } }, { expanded: false }, { fg(_name, text) { return text; }, bg(_name, text) { return text; }, bold(text) { return text; } });
427
+ const lines = result.render(120);
428
+ if (lines.length !== 6) throw new Error("expected outer spacer, box rows, and three capped result rows: " + JSON.stringify(lines));
429
+ if (!lines[4].includes("rows hidden")) throw new Error("compact cap was not applied: " + JSON.stringify(lines));
430
+ if (!lines[3].includes(" ✓ Agent 1/3: scout")) throw new Error("zero spacing was not applied: " + JSON.stringify(lines));
431
+ `;
432
+ const env = parentToolEnv();
433
+ env.SELESAI_CODING_AGENT_DIR = agentDir;
434
+ execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env, stdio: "pipe" });
435
+ } finally {
436
+ fs.rmSync(agentDir, { recursive: true, force: true });
437
+ }
438
+ });
439
+
349
440
  it("registers only subagent_wait and honors waitTool disabled config", () => {
350
441
  const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-wait-tool-config-"));
351
442
  try {