@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
@@ -39,6 +39,26 @@ describe("workflow launch params", () => {
39
39
  );
40
40
  });
41
41
 
42
+ it("keeps a bridge override scoped to the target workflow child", () => {
43
+ assert.deepEqual(
44
+ prepareWorkflowLaunchParams(
45
+ {},
46
+ { agent: "worker", task: "Run", intercomBridge: { mode: "off" } },
47
+ "workflow-run",
48
+ "isolated",
49
+ ),
50
+ {
51
+ agent: "worker",
52
+ task: "Run",
53
+ intercomBridge: { mode: "off" },
54
+ async: false,
55
+ workflowParentRunId: "workflow-run",
56
+ workflowKey: "isolated",
57
+ },
58
+ );
59
+ assert.equal(prepareWorkflowLaunchParams({}, { agent: "worker", task: "Run" }, "workflow-run", "sibling").intercomBridge, undefined);
60
+ });
61
+
42
62
  it("places workflow child gates inside managed worktree tasks", () => {
43
63
  assert.deepEqual(
44
64
  prepareWorkflowLaunchParams(
@@ -61,6 +81,25 @@ describe("workflow launch params", () => {
61
81
  );
62
82
  });
63
83
 
84
+ it("preserves a bridge override for retained workflow children", () => {
85
+ assert.deepEqual(
86
+ prepareWorkflowLaunchParams(
87
+ {},
88
+ { resume: "retained-run", task: "Continue", intercomBridge: { mode: "off" } },
89
+ "workflow-run",
90
+ "continue",
91
+ ),
92
+ {
93
+ action: "resume",
94
+ id: "retained-run",
95
+ message: "Continue",
96
+ workflowParentRunId: "workflow-run",
97
+ workflowKey: "continue",
98
+ intercomBridge: { mode: "off" },
99
+ },
100
+ );
101
+ });
102
+
64
103
  it("rejects gate defaults on retained resume items", () => {
65
104
  assert.throws(
66
105
  () => prepareWorkflowLaunchParams(
@@ -82,7 +121,7 @@ describe("workflow launch params", () => {
82
121
  );
83
122
  });
84
123
 
85
- it("preserves execution limits when routing retained resume items", () => {
124
+ it("preserves execution limits and fan-out identity when routing retained resume items", () => {
86
125
  assert.deepEqual(
87
126
  prepareWorkflowLaunchParams(
88
127
  { turnBudget: { maxTurns: 8 }, toolBudget: { hard: 12, block: ["read"] } },
@@ -95,7 +134,7 @@ describe("workflow launch params", () => {
95
134
  },
96
135
  "workflow-run",
97
136
  "continue",
98
- { missionDetached: true },
137
+ { missionDetached: true, runFanoutBudget: { version: 1, rootRunId: "root-run", directory: "/tmp/fanout", limit: 64, parentPath: "parent" } },
99
138
  ),
100
139
  {
101
140
  action: "resume",
@@ -103,6 +142,7 @@ describe("workflow launch params", () => {
103
142
  message: "Continue carefully",
104
143
  workflowParentRunId: "workflow-run",
105
144
  workflowKey: "continue",
145
+ runFanoutBudget: { version: 1, rootRunId: "root-run", directory: "/tmp/fanout", limit: 64, parentPath: "parent/workflow[continue]" },
106
146
  mission: false,
107
147
  timeoutMs: 5_000,
108
148
  turnBudget: { maxTurns: 3, graceTurns: 1 },
@@ -120,12 +120,13 @@ class QuestionComponent extends Container implements Component {
120
120
  this.rebuildMode();
121
121
  }
122
122
 
123
+ // Only the wizard-level focused state is read; the inner getter is kept for
124
+ // API symmetry but never invoked.
125
+ /* v8 ignore start */
123
126
  get focused(): boolean {
124
- // Only the wizard-level focused state is read; the inner getter is kept
125
- // for API symmetry but never invoked.
126
- /* v8 ignore next 1 */
127
127
  return this.focusedState;
128
128
  }
129
+ /* v8 ignore stop */
129
130
  set focused(value: boolean) {
130
131
  this.focusedState = value;
131
132
  if (this.editor) this.editor.focused = value && this.editorActive;
@@ -219,6 +220,9 @@ class QuestionComponent extends Container implements Component {
219
220
  }
220
221
 
221
222
  private showSelect(): void {
223
+ // showSelect is only invoked from other mode (the escape handler), so the
224
+ // mode guard's false branch is unreachable through the public flow.
225
+ /* v8 ignore next 1 */
222
226
  if (this.mode === "other") this.otherDraft = this.ensureEditor().getText();
223
227
  this.mode = "select";
224
228
  this.editorActive = false;
@@ -231,6 +235,10 @@ class QuestionComponent extends Container implements Component {
231
235
  this.modeContainer.addChild(this.ensureList());
232
236
  } else {
233
237
  const editor = this.ensureEditor();
238
+ // editorActive is always true in this branch: rebuildMode runs from the
239
+ // constructor, showOther (sets editorActive = true), and showSelect
240
+ // (which takes the select branch above).
241
+ /* v8 ignore next 1 */
234
242
  editor.focused = this.focusedState && this.editorActive;
235
243
  this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold(this.mode === "text" ? "Answer" : "Other answer")), 1, 0));
236
244
  this.modeContainer.addChild(new Spacer(1));
@@ -317,12 +325,13 @@ class ReviewComponent implements Component {
317
325
  private onSubmit: () => void,
318
326
  ) {}
319
327
 
328
+ // Only the setter is used by the wizard; the getter is kept for the
329
+ // Component contract but never read.
330
+ /* v8 ignore start */
320
331
  get focused(): boolean {
321
- // Only the setter is used by the wizard; the getter is kept for the
322
- // Component contract but never read.
323
- /* v8 ignore next 1 */
324
332
  return this.focusedState;
325
333
  }
334
+ /* v8 ignore stop */
326
335
  set focused(value: boolean) {
327
336
  this.focusedState = value;
328
337
  }
@@ -469,6 +478,9 @@ class BatchQuestionComponent extends Container implements Component {
469
478
 
470
479
  private goNext(): void {
471
480
  this.saveCurrent(true);
481
+ // goNext only runs on question pages, where currentPage is always below
482
+ // questions.length; the guard is defensive.
483
+ /* v8 ignore next 1 */
472
484
  if (this.currentPage < this.questions.length) this.currentPage++;
473
485
  this.showPage();
474
486
  }
@@ -479,6 +491,9 @@ class BatchQuestionComponent extends Container implements Component {
479
491
  this.onDone({ status: "submitted", answers: buildQuestionAnswers(this.questions, this.states) });
480
492
  return;
481
493
  }
494
+ // commitCurrent only runs on question pages, where currentPage is always
495
+ // below questions.length; the guard is defensive.
496
+ /* v8 ignore next 1 */
482
497
  if (this.currentPage < this.questions.length) this.currentPage++;
483
498
  this.showPage();
484
499
  };
@@ -256,6 +256,9 @@ export class QuestionList implements Component {
256
256
  for (const line of wrapTextWithAnsi(md.trim(), Math.max(10, width))) {
257
257
  lines.push(truncateToWidth(line, width, ""));
258
258
  }
259
+ // md is always trimmed, and wrapTextWithAnsi never emits a trailing
260
+ // empty line for trimmed input; the pop is defensive.
261
+ /* v8 ignore next 1 */
259
262
  while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") lines.pop();
260
263
  if (lines.length <= maxLines) return lines;
261
264
  if (maxLines === 1) return [truncateToWidth(this.theme.fg("dim", "…"), width, "")];
@@ -333,8 +336,8 @@ export class QuestionList implements Component {
333
336
  const result = filtered[this.selectedIndex]?.value;
334
337
  // Confirm only runs when count > 0 and the selected row is an option;
335
338
  // a freeform/comment row is dispatched above, so result is never falsy.
339
+ /* v8 ignore next 2 */
336
340
  if (result) this.onSubmit?.([result], this.commentEnabled);
337
- /* v8 ignore next 1 */
338
341
  else this.onCancel?.();
339
342
  return;
340
343
  }
@@ -398,6 +401,9 @@ export class QuestionList implements Component {
398
401
  this.onEnterFreeform?.();
399
402
  return;
400
403
  }
404
+ // Comment and freeform rows are intercepted above, so selectedIndex
405
+ // can only point at a real option here; the guard is defensive.
406
+ /* v8 ignore next 1 */
401
407
  if (this.selectedIndex < this.options.length) {
402
408
  this.selectionMode.toggle(this.checked, this.selectedIndex);
403
409
  this.invalidate();
@@ -125,6 +125,9 @@ export function renderSingleSelectRows({
125
125
 
126
126
  const safeMaxRows = Math.max(1, Math.floor(maxRows));
127
127
  const selectedBlock = blocks[selectedIndex] ?? blocks[0];
128
+ // Blocks is never empty here: empty blocks means empty allRows, which is
129
+ // already returned by the allRows.length <= maxRows guard above.
130
+ /* v8 ignore next 1 */
128
131
  if (!selectedBlock) return [];
129
132
 
130
133
  const indicator = ` (${selectedIndex + 1}/${itemCount})`;
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import questionExtension from "../index.ts";
4
+
5
+ const theme = {
6
+ fg: (_name: string, value: string) => value,
7
+ bg: (_name: string, value: string) => value,
8
+ bold: (value: string) => value,
9
+ };
10
+ const tui = { requestRender() {}, terminal: { rows: 24 } };
11
+ const keybindings = {
12
+ matches: (data: string, action: string) =>
13
+ (data === "\r" && action === "tui.select.confirm") ||
14
+ (data === "\x1b" && action === "tui.select.cancel"),
15
+ getKeys: () => ["Enter"],
16
+ };
17
+
18
+ async function runWizard(params: unknown) {
19
+ let tool: any;
20
+ let component: any;
21
+ let done!: (value: unknown) => void;
22
+ questionExtension({ registerTool: (entry: unknown) => (tool = entry), events: { emit() {} } } as any);
23
+ const execution = tool.execute("call", params, undefined, undefined, {
24
+ mode: "tui",
25
+ ui: {
26
+ custom: (factory: (...args: any[]) => unknown) => new Promise((resolve) => {
27
+ done = resolve;
28
+ component = factory(tui, theme, keybindings, done);
29
+ }),
30
+ },
31
+ } as any);
32
+ return { component: () => component, execution };
33
+ }
34
+
35
+ describe("exp", () => {
36
+ it("text question focused=false", async () => {
37
+ const { component, execution } = await runWizard({
38
+ questions: [{ type: "text", question: "Type" }],
39
+ });
40
+ component().focused = false;
41
+ component().focused = true;
42
+ component().focused = false;
43
+ component().handleInput("x");
44
+ component().handleInput("\r");
45
+ await execution;
46
+ expect(true).toBe(true);
47
+ });
48
+
49
+ it("multiselect empty confirm", async () => {
50
+ const { component, execution } = await runWizard({
51
+ questions: [{ type: "multiselect", question: "Pick", options: [{ value: "a", label: "A" }] }],
52
+ });
53
+ component().handleInput("\r"); // empty confirm → no commit
54
+ component().handleInput(" ");
55
+ component().handleInput("\r"); // now commits
56
+ await execution;
57
+ expect(true).toBe(true);
58
+ });
59
+
60
+ it("text empty submit", async () => {
61
+ const { component, execution } = await runWizard({
62
+ questions: [{ type: "text", question: "Type" }],
63
+ });
64
+ component().handleInput("\r"); // empty submit → no commit
65
+ component().handleInput("a");
66
+ component().handleInput("\r");
67
+ await execution;
68
+ expect(true).toBe(true);
69
+ });
70
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import questionExtension from "../index.ts";
4
+
5
+ const theme = {
6
+ fg: (_name: string, value: string) => value,
7
+ bg: (_name: string, value: string) => value,
8
+ bold: (value: string) => value,
9
+ };
10
+ const tui = { requestRender() {}, terminal: { rows: 24 } };
11
+ const keybindings = {
12
+ matches: (data: string, action: string) =>
13
+ (data === "\r" && action === "tui.select.confirm") ||
14
+ (data === "\x1b" && action === "tui.select.cancel"),
15
+ getKeys: () => ["Enter"],
16
+ };
17
+
18
+ async function runWizard(params: unknown) {
19
+ let tool: any;
20
+ let component: any;
21
+ let done!: (value: unknown) => void;
22
+ questionExtension({ registerTool: (entry: unknown) => (tool = entry), events: { emit() {} } } as any);
23
+ const execution = tool.execute("call", params, undefined, undefined, {
24
+ mode: "tui",
25
+ ui: {
26
+ custom: (factory: (...args: any[]) => unknown) => new Promise((resolve) => {
27
+ done = resolve;
28
+ component = factory(tui, theme, keybindings, done);
29
+ }),
30
+ },
31
+ } as any);
32
+ return { component: () => component, execution };
33
+ }
34
+
35
+ describe("exp2", () => {
36
+ it("text question construction only", async () => {
37
+ const { component, execution } = await runWizard({
38
+ questions: [{ type: "text", question: "Type" }],
39
+ });
40
+ component().render(120);
41
+ component().handleInput("a");
42
+ component().handleInput("\r");
43
+ await execution;
44
+ expect(true).toBe(true);
45
+ });
46
+
47
+ it("blur then non-enter key", async () => {
48
+ const { component, execution } = await runWizard({
49
+ questions: [{ type: "text", question: "Type" }],
50
+ });
51
+ component().handleInput("\x1b"); // blur
52
+ component().handleInput("x"); // non-enter while blurred
53
+ component().handleInput("\r"); // re-focus
54
+ component().handleInput("a");
55
+ component().handleInput("\r");
56
+ await execution;
57
+ expect(true).toBe(true);
58
+ });
59
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import questionExtension from "../index.ts";
4
+
5
+ const theme = {
6
+ fg: (_name: string, value: string) => value,
7
+ bg: (_name: string, value: string) => value,
8
+ bold: (value: string) => value,
9
+ };
10
+ const tui = { requestRender() {}, terminal: { rows: 24 } };
11
+ const keybindings = {
12
+ matches: (data: string, action: string) =>
13
+ (data === "\r" && action === "tui.select.confirm") ||
14
+ (data === "\x1b" && action === "tui.select.cancel"),
15
+ getKeys: () => ["Enter"],
16
+ };
17
+
18
+ async function runWizard(params: unknown) {
19
+ let tool: any;
20
+ let component: any;
21
+ let done!: (value: unknown) => void;
22
+ questionExtension({ registerTool: (entry: unknown) => (tool = entry), events: { emit() {} } } as any);
23
+ const execution = tool.execute("call", params, undefined, undefined, {
24
+ mode: "tui",
25
+ ui: {
26
+ custom: (factory: (...args: any[]) => unknown) => new Promise((resolve) => {
27
+ done = resolve;
28
+ component = factory(tui, theme, keybindings, done);
29
+ }),
30
+ },
31
+ } as any);
32
+ return { component: () => component, execution };
33
+ }
34
+
35
+ describe("exp3", () => {
36
+ it("focused true then false on text", async () => {
37
+ const { component, execution } = await runWizard({
38
+ questions: [{ type: "text", question: "Type" }],
39
+ });
40
+ component().focused = true; // editor.focused = true && true
41
+ component().focused = false; // editor.focused = false && true
42
+ component().handleInput("a");
43
+ component().handleInput("\r");
44
+ await execution;
45
+ expect(true).toBe(true);
46
+ });
47
+ });
@@ -83,4 +83,11 @@ describe("question helpers", () => {
83
83
  expect(previewText("a".repeat(10), 10, 2)).toBe("a".repeat(10));
84
84
  expect(previewText("a".repeat(11), 10, 2)).toBe(`${"a".repeat(9)}…`);
85
85
  });
86
+
87
+ it("previewText handles nullish values", () => {
88
+ expect(previewText(null)).toBe("");
89
+ expect(previewText(undefined)).toBe("");
90
+ expect(previewText(null, 10, 1)).toBe("");
91
+ expect(previewText(undefined, 10, 1)).toBe("");
92
+ });
86
93
  });
@@ -552,3 +552,143 @@ describe("QuestionList comment toggle", () => {
552
552
  expect(list.isCommentEnabled()).toBe(true);
553
553
  });
554
554
  });
555
+
556
+ describe("QuestionList split-pane preview truncation", () => {
557
+ const longDescription = "A very long description that keeps wrapping and wrapping well past the preview pane width so the preview must be truncated with an ellipsis marker";
558
+
559
+ it("maxLines 1 collapses the preview to a single ellipsis line", () => {
560
+ const list = new QuestionList(
561
+ [{ value: "a", label: "Alpha", description: longDescription }],
562
+ SingleSelect,
563
+ false,
564
+ false,
565
+ fakeTheme,
566
+ fakeKeybindings(),
567
+ disabledShortcut,
568
+ );
569
+ list.setMaxVisibleRows(1);
570
+ const lines = list.render(120);
571
+ expect(lines.some((l) => l.includes("…"))).toBe(true);
572
+ });
573
+
574
+ it("maxLines above 1 keeps leading lines and appends an ellipsis", () => {
575
+ const list = new QuestionList(
576
+ [{ value: "a", label: "Alpha", description: longDescription }],
577
+ SingleSelect,
578
+ false,
579
+ false,
580
+ fakeTheme,
581
+ fakeKeybindings(),
582
+ disabledShortcut,
583
+ );
584
+ list.setMaxVisibleRows(2);
585
+ const lines = list.render(120);
586
+ expect(lines.some((l) => l.includes("…"))).toBe(true);
587
+ });
588
+ });
589
+
590
+ describe("QuestionList remaining branch coverage", () => {
591
+ it("single-select confirm on comment-toggle row toggles the comment", () => {
592
+ const list = new QuestionList(opts(["a"]), SingleSelect, false, true, fakeTheme, fakeKeybindings(["tui.select.confirm"]), enabledShortcut);
593
+ list.handleInput(CTRL_J); // to comment-toggle row
594
+ list.handleInput(ENTER);
595
+ expect(list.isCommentEnabled()).toBe(true);
596
+ });
597
+
598
+ it("multi-select renders the selected comment-toggle row with an enabled checkbox", () => {
599
+ const list = new QuestionList(opts(["a"]), MultiSelect, false, true, fakeTheme, fakeKeybindings(), enabledShortcut);
600
+ list.handleInput(CTRL_G); // enable comment
601
+ list.handleInput(CTRL_J); // to comment-toggle row
602
+ const lines = list.render(80);
603
+ expect(lines.some((l) => l.includes("[✓]"))).toBe(true);
604
+ expect(lines.some((l) => l.includes("→"))).toBe(true);
605
+ });
606
+
607
+ it("multi-select renders a checked option with a success checkbox", () => {
608
+ const list = new QuestionList(opts(["a", "b"]), MultiSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
609
+ list.handleInput(SPACE); // check option 0
610
+ const lines = list.render(80);
611
+ expect(lines.some((l) => l.includes("[✓]"))).toBe(true);
612
+ });
613
+ });
614
+
615
+ describe("QuestionList final branch coverage", () => {
616
+ it("setMaxVisibleRows with the same value is a no-op", () => {
617
+ const list = new QuestionList(opts(["a"]), SingleSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
618
+ list.setMaxVisibleRows(12);
619
+ expect((list as any).maxVisibleRows).toBe(12);
620
+ });
621
+
622
+ it("single-select render styles the scroll indicator line", () => {
623
+ const list = new QuestionList(opts(["a", "b", "c", "d", "e", "f"]), SingleSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
624
+ list.setMaxVisibleRows(3);
625
+ const lines = list.render(80);
626
+ expect(lines.some((l) => l.includes("("))).toBe(true);
627
+ });
628
+
629
+ it("single-select render styles description lines", () => {
630
+ const list = new QuestionList(
631
+ [
632
+ { value: "a", label: "Alpha", description: "some desc" },
633
+ { value: "b", label: "Beta" },
634
+ ],
635
+ SingleSelect,
636
+ false,
637
+ false,
638
+ fakeTheme,
639
+ fakeKeybindings(),
640
+ disabledShortcut,
641
+ );
642
+ list.handleInput(CTRL_J); // select Beta so Alpha's description row is unselected
643
+ const lines = list.render(80);
644
+ expect(lines.some((l) => l.includes("some desc"))).toBe(true);
645
+ });
646
+
647
+ it("preview shows Enabled when the comment row is selected with comment on", () => {
648
+ const list = new QuestionList(opts(["a"]), SingleSelect, false, true, fakeTheme, fakeKeybindings(), enabledShortcut);
649
+ list.handleInput(CTRL_G); // enable comment
650
+ list.handleInput(CTRL_J); // to comment-toggle row
651
+ const lines = list.render(120);
652
+ expect(lines.some((l) => l.includes("Enabled"))).toBe(true);
653
+ });
654
+
655
+ it("preview shows the current filter on the freeform row", () => {
656
+ const list = new QuestionList(opts(["a"]), SingleSelect, true, false, fakeTheme, fakeKeybindings(), disabledShortcut);
657
+ list.handleInput("a"); // search keeps the option, selects index 0
658
+ list.handleInput(CTRL_J); // to freeform row
659
+ const lines = list.render(120);
660
+ expect(lines.some((l) => l.includes("Current filter"))).toBe(true);
661
+ });
662
+
663
+ it("preview shows the current filter on a selected option", () => {
664
+ const list = new QuestionList(opts(["a"]), SingleSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
665
+ list.handleInput("a"); // search matches, selected stays 0
666
+ const lines = list.render(120);
667
+ expect(lines.some((l) => l.includes("Filter:"))).toBe(true);
668
+ });
669
+
670
+ it("single-select number key beyond the filtered options is ignored", () => {
671
+ const list = new QuestionList(opts(["a", "b"]), SingleSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
672
+ list.handleInput("3");
673
+ expect((list as any).selectedIndex).toBe(0);
674
+ });
675
+
676
+ it("multi-select up from a non-zero index moves up", () => {
677
+ const list = new QuestionList(opts(["a", "b"]), MultiSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
678
+ list.handleInput(CTRL_J); // to 1
679
+ list.handleInput(CTRL_K); // back to 0
680
+ expect((list as any).selectedIndex).toBe(0);
681
+ });
682
+
683
+ it("multi-select number key beyond the options is ignored", () => {
684
+ const list = new QuestionList(opts(["a", "b"]), MultiSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
685
+ list.handleInput("3");
686
+ expect((list as any).checked.size).toBe(0);
687
+ });
688
+
689
+ it("multi-select input that matches nothing reaches the confirm check safely", () => {
690
+ const list = new QuestionList(opts(["a"]), MultiSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
691
+ list.handleInput("x");
692
+ expect((list as any).checked.size).toBe(0);
693
+ });
694
+ });
@@ -199,4 +199,17 @@ describe("row-layout", () => {
199
199
  expect(blocks[1]!.lines.length).toBeGreaterThan(1);
200
200
  expect(blocks[1]!.lines[1]!.startsWith(" ")).toBe(true);
201
201
  });
202
+
203
+ it("renderSingleSelectRows: out-of-range selectedIndex falls back to the first block", () => {
204
+ const rows = renderSingleSelectRows({
205
+ options: opts(["a", "b"]),
206
+ selectedIndex: 7,
207
+ width: 40,
208
+ allowFreeform: false,
209
+ maxRows: 1,
210
+ });
211
+ expect(rows.length).toBeGreaterThan(0);
212
+ expect(rows[0]!.selected).toBe(true);
213
+ expect(rows[0]!.line).toContain("a");
214
+ });
202
215
  });
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { MultiSelect, SingleSelect } from "../selection-mode.ts";
4
+
5
+ describe("selection-mode", () => {
6
+ it("SingleSelect reports single mode and no-op toggle", () => {
7
+ expect(SingleSelect.multi).toBe(false);
8
+ const checked = new Set<number>([1]);
9
+ SingleSelect.toggle(checked, 0);
10
+ expect([...checked]).toEqual([1]);
11
+ });
12
+
13
+ it("SingleSelect buildResult returns the selected value", () => {
14
+ const options = [{ value: "a", label: "A" }];
15
+ expect(SingleSelect.buildResult({ selectedIndex: 0, checked: new Set(), options })).toEqual(["a"]);
16
+ // Out-of-range index (or empty options) → no value → empty result.
17
+ expect(SingleSelect.buildResult({ selectedIndex: 2, checked: new Set(), options })).toEqual([]);
18
+ expect(SingleSelect.buildResult({ selectedIndex: 0, checked: new Set(), options: [] })).toEqual([]);
19
+ });
20
+
21
+ it("SingleSelect handleNumberKey clamps to the last option", () => {
22
+ expect(SingleSelect.handleNumberKey(5, 3)).toEqual({ toggle: false, selectIndex: 2 });
23
+ expect(SingleSelect.handleNumberKey(1, 3)).toEqual({ toggle: false, selectIndex: 1 });
24
+ });
25
+
26
+ it("MultiSelect toggle adds, removes, and ignores negative indexes", () => {
27
+ expect(MultiSelect.multi).toBe(true);
28
+ const checked = new Set<number>();
29
+ MultiSelect.toggle(checked, 0);
30
+ expect(checked.has(0)).toBe(true);
31
+ MultiSelect.toggle(checked, 0);
32
+ expect(checked.has(0)).toBe(false);
33
+ MultiSelect.toggle(checked, -1);
34
+ expect(checked.size).toBe(0);
35
+ });
36
+
37
+ it("MultiSelect buildResult sorts and filters missing options", () => {
38
+ const options = [
39
+ { value: "a", label: "A" },
40
+ { value: "b", label: "B" },
41
+ ];
42
+ const checked = new Set([1, 0]);
43
+ expect(MultiSelect.buildResult({ selectedIndex: 0, checked, options })).toEqual(["a", "b"]);
44
+ // Indexes pointing past the options array are filtered out.
45
+ const sparse = new Set([0, 1, 2]);
46
+ expect(MultiSelect.buildResult({ selectedIndex: 0, checked: sparse, options })).toEqual(["a", "b"]);
47
+ expect(MultiSelect.buildResult({ selectedIndex: 0, checked: new Set(), options })).toEqual([]);
48
+ });
49
+
50
+ it("MultiSelect handleNumberKey clamps to the last option", () => {
51
+ expect(MultiSelect.handleNumberKey(5, 3)).toEqual({ toggle: true, selectIndex: 2 });
52
+ expect(MultiSelect.handleNumberKey(0, 3)).toEqual({ toggle: true, selectIndex: 0 });
53
+ });
54
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { QuestionList } from "../question-list.ts";
3
+ import { SingleSelect } from "../selection-mode.ts";
4
+ import type { KeybindingsManager, Theme } from "@selesai/code";
5
+ import type { ResolvedShortcut } from "../types.ts";
6
+
7
+ const fakeTheme = { fg: (_n: string, s: string) => s, bold: (s: string) => s } as unknown as Theme;
8
+ const fakeKeybindings = (actions: string[] = []): KeybindingsManager =>
9
+ ({ matches: (_d: string, a: string) => actions.includes(a), getKeys: () => [] }) as unknown as KeybindingsManager;
10
+ const disabledShortcut: ResolvedShortcut = { disabled: true, spec: null, matches: ((_d: string) => false) as (d: string) => false };
11
+
12
+ describe("ig3", () => {
13
+ it("confirm option", () => {
14
+ const list = new QuestionList([{ value: "a", label: "A" }], SingleSelect, false, false, fakeTheme, fakeKeybindings(["tui.select.confirm"]), disabledShortcut);
15
+ let submitted = false;
16
+ list.onSubmit = () => (submitted = true);
17
+ list.handleInput("\r");
18
+ expect(submitted).toBe(true);
19
+ });
20
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import questionExtension from "../index.ts";
3
+
4
+ const theme = { fg: (_n: string, s: string) => s, bg: (_n: string, s: string) => s, bold: (s: string) => s };
5
+ const tui = { requestRender() {}, terminal: { rows: 24 } };
6
+ const keybindings = { matches: () => false, getKeys: () => ["Enter"] };
7
+
8
+ async function runWizard(params: unknown) {
9
+ let tool: any;
10
+ let component: any;
11
+ let done!: (value: unknown) => void;
12
+ questionExtension({ registerTool: (entry: unknown) => (tool = entry), events: { emit() {} } } as any);
13
+ const execution = tool.execute("call", params, undefined, undefined, {
14
+ mode: "tui",
15
+ ui: { custom: (factory: (...args: any[]) => unknown) => new Promise((resolve) => { done = resolve; component = factory(tui, theme, keybindings, done); }) },
16
+ } as any);
17
+ return { component: () => component, execution };
18
+ }
19
+
20
+ describe("probe", () => {
21
+ it("text question focused false", async () => {
22
+ const { component, execution } = await runWizard({ questions: [{ type: "text", question: "Type" }] });
23
+ component().focused = false;
24
+ component().handleInput("a");
25
+ component().handleInput("\r");
26
+ await execution;
27
+ expect(true).toBe(true);
28
+ });
29
+ });
@@ -8,7 +8,7 @@ description: |
8
8
  other agents contribute context, planning, or execution.
9
9
  ---
10
10
 
11
- # Pi Subagents
11
+ # Selesai Subagents
12
12
 
13
13
  This skill is for the main parent orchestrator only. Do not inject or follow it inside spawned child subagents. The parent session owns delegation, orchestration, review fanout, and final fix-worker launches. Ordinary children should not run their own subagent workflows; the explicit exception is a delegated fanout child whose resolved builtin `tools` includes `subagent`, and that child may use `subagent` only for the fanout work the parent assigned.
14
14