@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
@@ -1,75 +1,113 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
1
+ import { describe, expect, it } from "vitest";
3
2
 
4
3
  import { buildQuestionAnswers, prepareQuestions, saveDraftQuestion } from "../batch.ts";
5
4
  import type { DraftQuestionState } from "../types.ts";
6
5
 
7
- test("prepareQuestions: normalizes typed questions and generated ids", () => {
8
- const result = prepareQuestions([
9
- { type: "select", question: "Pick a color", options: [{ value: "red", label: "Red" }, { value: "blue", label: "Blue" }] },
10
- { type: "text", id: "notes", label: "Notes", question: "Anything else?" },
11
- ]);
12
- assert.ok("questions" in result);
13
- if (!("questions" in result)) return;
14
- assert.deepEqual(result.questions, [
15
- {
16
- id: "q1",
17
- label: "Q1",
18
- question: "Pick a color",
19
- context: undefined,
6
+ describe("question batch helpers", () => {
7
+ it("prepareQuestions: normalizes typed questions and generated ids", () => {
8
+ const result = prepareQuestions([
9
+ { type: "select", question: "Pick a color", options: [{ value: "red", label: "Red" }, { value: "blue", label: "Blue" }] },
10
+ { type: "text", id: "notes", label: "Notes", question: "Anything else?" },
11
+ ]);
12
+ expect("questions" in result).toBe(true);
13
+ if (!("questions" in result)) return;
14
+ expect(result.questions).toEqual([
15
+ {
16
+ id: "q1",
17
+ label: "Q1",
18
+ question: "Pick a color",
19
+ context: undefined,
20
+ type: "select",
21
+ options: [
22
+ { value: "red", label: "Red", description: undefined },
23
+ { value: "blue", label: "Blue", description: undefined },
24
+ ],
25
+ allowOther: false,
26
+ },
27
+ {
28
+ id: "notes",
29
+ label: "Notes",
30
+ question: "Anything else?",
31
+ context: undefined,
32
+ type: "text",
33
+ options: [],
34
+ allowOther: false,
35
+ },
36
+ ]);
37
+ });
38
+
39
+ it("prepareQuestions: rejects empty batches, generated-id collisions, and duplicate option values", () => {
40
+ expect(prepareQuestions([])).toEqual({ error: "questions must contain at least one question." });
41
+ expect((prepareQuestions([
42
+ { type: "text", id: "q2", question: "One" },
43
+ { type: "text", question: "Two" },
44
+ ]) as { error: string }).error).toMatch(/duplicate id: q2/);
45
+ expect((prepareQuestions([{
20
46
  type: "select",
21
- options: [
22
- { value: "red", label: "Red", description: undefined },
23
- { value: "blue", label: "Blue", description: undefined },
24
- ],
25
- allowOther: false,
26
- },
27
- {
28
- id: "notes",
29
- label: "Notes",
30
- question: "Anything else?",
31
- context: undefined,
32
- type: "text",
33
- options: [],
47
+ question: "Choose",
48
+ options: [{ value: "same", label: "A" }, { value: "same", label: "B" }],
49
+ }]) as { error: string }).error).toMatch(/duplicate value: same/);
50
+ });
51
+
52
+ it("prepareQuestions: rejects missing question, bad type, empty options, and bad option fields", () => {
53
+ expect((prepareQuestions([{ type: "text" }]) as { error: string }).error).toMatch(/non-empty question/);
54
+ expect((prepareQuestions([{ type: "radio", question: "X", options: [{ value: "a", label: "A" }] }]) as { error: string }).error).toMatch(/type must be select, multiselect, or text/);
55
+ expect((prepareQuestions([{ type: "select", question: "X", options: [] }]) as { error: string }).error).toMatch(/at least one option/);
56
+ expect((prepareQuestions([{ type: "select", question: "X", options: [{ value: "", label: "A" }] }]) as { error: string }).error).toMatch(/value and label must be non-empty/);
57
+ expect((prepareQuestions([{ type: "select", question: "X", options: [{ value: "a", label: "" }] }]) as { error: string }).error).toMatch(/value and label must be non-empty/);
58
+ });
59
+
60
+ it("prepareQuestions: trims id/label/context and defaults allowOther", () => {
61
+ const result = prepareQuestions([
62
+ { type: "multiselect", id: " m1 ", label: " Multi ", question: " Pick many ", context: " ctx ", options: [{ value: "a", label: "A", description: " desc " }] },
63
+ ]);
64
+ expect("questions" in result).toBe(true);
65
+ if (!("questions" in result)) return;
66
+ expect(result.questions[0]).toEqual({
67
+ id: "m1",
68
+ label: "Multi",
69
+ question: "Pick many",
70
+ context: "ctx",
71
+ type: "multiselect",
72
+ options: [{ value: "a", label: "A", description: "desc" }],
34
73
  allowOther: false,
35
- },
36
- ]);
37
- });
74
+ });
75
+ });
38
76
 
39
- test("prepareQuestions: rejects empty batches, generated-id collisions, and duplicate option values", () => {
40
- assert.deepEqual(prepareQuestions([]), { error: "questions must contain at least one question." });
41
- assert.match((prepareQuestions([
42
- { type: "text", id: "q2", question: "One" },
43
- { type: "text", question: "Two" },
44
- ]) as { error: string }).error, /duplicate id: q2/);
45
- assert.match((prepareQuestions([{
46
- type: "select",
47
- question: "Choose",
48
- options: [{ value: "same", label: "A" }, { value: "same", label: "B" }],
49
- }]) as { error: string }).error, /duplicate value: same/);
50
- });
77
+ it("saveDraftQuestion: clearing an answer while navigating back leaves it unanswered", () => {
78
+ const states = new Map<string, DraftQuestionState>([["q1", { status: "answered", response: { kind: "text", text: "old" } }]]);
79
+ saveDraftQuestion(states, "q1", null, false);
80
+ expect(states.has("q1")).toBe(false);
81
+ });
51
82
 
52
- test("saveDraftQuestion: clearing an answer while navigating back leaves it unanswered", () => {
53
- const states = new Map<string, DraftQuestionState>([["q1", { status: "answered", response: { kind: "text", text: "old" } }]]);
54
- saveDraftQuestion(states, "q1", null, false);
55
- assert.equal(states.has("q1"), false);
56
- });
83
+ it("saveDraftQuestion: blank answer with markBlankSkipped marks skipped", () => {
84
+ const states = new Map<string, DraftQuestionState>();
85
+ saveDraftQuestion(states, "q1", null, true);
86
+ expect(states.get("q1")).toEqual({ status: "skipped" });
87
+ });
88
+
89
+ it("saveDraftQuestion: response marks answered", () => {
90
+ const states = new Map<string, DraftQuestionState>();
91
+ saveDraftQuestion(states, "q1", { kind: "text", text: "answer" }, true);
92
+ expect(states.get("q1")).toEqual({ status: "answered", response: { kind: "text", text: "answer" } });
93
+ });
57
94
 
58
- test("buildQuestionAnswers: preserves answered, skipped, and unvisited states", () => {
59
- const prepared = prepareQuestions([
60
- { type: "text", id: "a", question: "A" },
61
- { type: "text", id: "b", question: "B" },
62
- { type: "text", id: "c", question: "C" },
63
- ]);
64
- assert.ok("questions" in prepared);
65
- if (!("questions" in prepared)) return;
66
- const states = new Map<string, DraftQuestionState>([
67
- ["a", { status: "answered", response: { kind: "text", text: "answer" } }],
68
- ["b", { status: "skipped" }],
69
- ]);
70
- assert.deepEqual(buildQuestionAnswers(prepared.questions, states), [
71
- { id: "a", status: "answered", response: { kind: "text", text: "answer" } },
72
- { id: "b", status: "skipped" },
73
- { id: "c", status: "unanswered" },
74
- ]);
95
+ it("buildQuestionAnswers: preserves answered, skipped, and unvisited states", () => {
96
+ const prepared = prepareQuestions([
97
+ { type: "text", id: "a", question: "A" },
98
+ { type: "text", id: "b", question: "B" },
99
+ { type: "text", id: "c", question: "C" },
100
+ ]);
101
+ expect("questions" in prepared).toBe(true);
102
+ if (!("questions" in prepared)) return;
103
+ const states = new Map<string, DraftQuestionState>([
104
+ ["a", { status: "answered", response: { kind: "text", text: "answer" } }],
105
+ ["b", { status: "skipped" }],
106
+ ]);
107
+ expect(buildQuestionAnswers(prepared.questions, states)).toEqual([
108
+ { id: "a", status: "answered", response: { kind: "text", text: "answer" } },
109
+ { id: "b", status: "skipped" },
110
+ { id: "c", status: "unanswered" },
111
+ ]);
112
+ });
75
113
  });
@@ -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
+ });
@@ -1,53 +1,93 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
1
+ import { describe, expect, it } from "vitest";
3
2
 
4
3
  import {
5
4
  createSelectionResponse,
6
5
  createTextResponse,
7
6
  formatQuestionResponse,
8
7
  formatResponseSummary,
8
+ formatOptionsForMessage,
9
9
  oneLine,
10
10
  previewText,
11
+ trimText,
11
12
  } from "../helpers.ts";
12
13
  import type { PreparedQuestion } from "../types.ts";
13
14
 
14
- test("createTextResponse trims edges and rejects blank text", () => {
15
- assert.equal(createTextResponse(" "), null);
16
- assert.deepEqual(createTextResponse(" hello\nworld "), { kind: "text", text: "hello\nworld" });
17
- });
15
+ describe("question helpers", () => {
16
+ it("trimText trims and returns undefined for blank", () => {
17
+ expect(trimText(" ")).toBeUndefined();
18
+ expect(trimText(undefined)).toBeUndefined();
19
+ expect(trimText(null)).toBeUndefined();
20
+ expect(trimText(" hello ")).toBe("hello");
21
+ });
18
22
 
19
- test("createSelectionResponse supports values plus Other and rejects empty", () => {
20
- assert.equal(createSelectionResponse([], " "), null);
21
- assert.deepEqual(createSelectionResponse([" a ", "b"], " other "), {
22
- kind: "selection",
23
- values: ["a", "b"],
24
- otherText: "other",
23
+ it("createTextResponse trims edges and rejects blank text", () => {
24
+ expect(createTextResponse(" ")).toBeNull();
25
+ expect(createTextResponse(undefined)).toBeNull();
26
+ expect(createTextResponse(" hello\nworld ")).toEqual({ kind: "text", text: "hello\nworld" });
25
27
  });
26
- });
27
28
 
28
- test("formatResponseSummary returns machine values", () => {
29
- assert.equal(formatResponseSummary({ kind: "selection", values: ["a", "b"], otherText: "c" }), "a, b, c");
30
- assert.equal(formatResponseSummary({ kind: "text", text: "hello" }), "hello");
31
- });
29
+ it("createSelectionResponse supports values plus Other and rejects empty", () => {
30
+ expect(createSelectionResponse([], " ")).toBeNull();
31
+ expect(createSelectionResponse([], undefined)).toBeNull();
32
+ expect(createSelectionResponse([" a ", "b"], " other ")).toEqual({
33
+ kind: "selection",
34
+ values: ["a", "b"],
35
+ otherText: "other",
36
+ });
37
+ expect(createSelectionResponse(["a"], undefined)).toEqual({ kind: "selection", values: ["a"] });
38
+ });
32
39
 
33
- test("formatQuestionResponse maps stable values to labels", () => {
34
- const question: PreparedQuestion = {
35
- id: "q1",
36
- label: "Q1",
37
- question: "Pick",
38
- type: "multiselect",
39
- options: [{ value: "a", label: "Alpha" }, { value: "b", label: "Beta" }],
40
- allowOther: true,
41
- };
42
- assert.equal(formatQuestionResponse(question, { kind: "selection", values: ["b"], otherText: "Gamma" }), "Beta, Other: Gamma");
43
- });
40
+ it("formatResponseSummary returns machine values", () => {
41
+ expect(formatResponseSummary({ kind: "selection", values: ["a", "b"], otherText: "c" })).toBe("a, b, c");
42
+ expect(formatResponseSummary({ kind: "selection", values: ["a"] })).toBe("a");
43
+ expect(formatResponseSummary({ kind: "text", text: "hello" })).toBe("hello");
44
+ });
44
45
 
45
- test("previewText bounds multiline summaries", () => {
46
- assert.equal(previewText("one\ntwo\nthree", 160, 2), "one\ntwo…");
47
- assert.equal(previewText("a".repeat(200), 10, 2), `${"a".repeat(9)}…`);
48
- });
46
+ it("formatQuestionResponse maps stable values to labels", () => {
47
+ const question: PreparedQuestion = {
48
+ id: "q1",
49
+ label: "Q1",
50
+ question: "Pick",
51
+ type: "multiselect",
52
+ options: [{ value: "a", label: "Alpha" }, { value: "b", label: "Beta" }],
53
+ allowOther: true,
54
+ };
55
+ expect(formatQuestionResponse(question, { kind: "selection", values: ["b"], otherText: "Gamma" })).toBe("Beta, Other: Gamma");
56
+ expect(formatQuestionResponse(question, { kind: "selection", values: ["unknown"] })).toBe("unknown");
57
+ expect(formatQuestionResponse(question, { kind: "text", text: "plain" })).toBe("plain");
58
+ });
49
59
 
50
- test("oneLine collapses whitespace and truncates", () => {
51
- assert.equal(oneLine(" hello world "), "hello world");
52
- assert.equal(oneLine("a".repeat(20), 10), `${"a".repeat(9)}…`);
60
+ it("formatOptionsForMessage numbers options with descriptions", () => {
61
+ expect(formatOptionsForMessage([
62
+ { value: "a", label: "Alpha" },
63
+ { value: "b", label: "Beta", description: "the beta" },
64
+ ])).toBe("1. Alpha\n2. Beta — the beta");
65
+ });
66
+
67
+ it("previewText bounds multiline summaries", () => {
68
+ expect(previewText("one\ntwo\nthree", 160, 2)).toBe("one\ntwo…");
69
+ expect(previewText("a".repeat(200), 10, 2)).toBe(`${"a".repeat(9)}…`);
70
+ expect(previewText("short", 160, 2)).toBe("short");
71
+ expect(previewText("", 160, 2)).toBe("");
72
+ });
73
+
74
+ it("oneLine collapses whitespace and truncates", () => {
75
+ expect(oneLine(" hello world ")).toBe("hello world");
76
+ expect(oneLine("a".repeat(20), 10)).toBe(`${"a".repeat(9)}…`);
77
+ expect(oneLine(undefined)).toBe("");
78
+ expect(oneLine(42)).toBe("42");
79
+ });
80
+
81
+ it("previewText handles single-line and exact-boundary text", () => {
82
+ expect(previewText("one line", 160, 2)).toBe("one line");
83
+ expect(previewText("a".repeat(10), 10, 2)).toBe("a".repeat(10));
84
+ expect(previewText("a".repeat(11), 10, 2)).toBe(`${"a".repeat(9)}…`);
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
+ });
53
93
  });