@selesai/code 0.8.4 → 0.8.6

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 (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -1
@@ -1,136 +1,202 @@
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 { wrapPlain, buildItemBlocks, flattenBlocks, renderSingleSelectRows } from "../row-layout.ts";
5
4
  import type { ItemBlock, QuestionOption } from "../types.ts";
6
5
 
7
6
  const opts = (labels: string[]): QuestionOption[] => labels.map((label) => ({ value: label, label }));
8
7
 
9
- test("wrapPlain: empty → ['']", () => {
10
- assert.deepEqual(wrapPlain("", 20), [""]);
11
- assert.deepEqual(wrapPlain(" ", 20), [""]);
12
- });
8
+ describe("row-layout", () => {
9
+ it("wrapPlain: empty → ['']", () => {
10
+ expect(wrapPlain("", 20)).toEqual([""]);
11
+ expect(wrapPlain(" ", 20)).toEqual([""]);
12
+ });
13
13
 
14
- test("wrapPlain: single word fits", () => {
15
- assert.deepEqual(wrapPlain("hello", 20), ["hello"]);
16
- });
14
+ it("wrapPlain: single word fits", () => {
15
+ expect(wrapPlain("hello", 20)).toEqual(["hello"]);
16
+ });
17
17
 
18
- test("wrapPlain: word longer than width → chunked", () => {
19
- assert.deepEqual(wrapPlain("abcdefgh", 3), ["abc", "def", "gh"]);
20
- });
18
+ it("wrapPlain: word longer than width → chunked", () => {
19
+ expect(wrapPlain("abcdefgh", 3)).toEqual(["abc", "def", "gh"]);
20
+ });
21
21
 
22
- test("wrapPlain: multiple words wrapping", () => {
23
- assert.deepEqual(wrapPlain("hello world foo", 7), ["hello", "world", "foo"]);
24
- assert.deepEqual(wrapPlain("hello world foo", 11), ["hello world", "foo"]);
25
- });
22
+ it("wrapPlain: multiple words wrapping", () => {
23
+ expect(wrapPlain("hello world foo", 7)).toEqual(["hello", "world", "foo"]);
24
+ expect(wrapPlain("hello world foo", 11)).toEqual(["hello world", "foo"]);
25
+ });
26
26
 
27
- test("wrapPlain: width=1 → char-by-char", () => {
28
- assert.deepEqual(wrapPlain("abc", 1), ["a", "b", "c"]);
29
- });
27
+ it("wrapPlain: width=1 → char-by-char", () => {
28
+ expect(wrapPlain("abc", 1)).toEqual(["a", "b", "c"]);
29
+ });
30
30
 
31
- test("buildItemBlocks: 3 options, selected=0 → arrow on block 0", () => {
32
- const blocks = buildItemBlocks(opts(["a", "b", "c"]), 80, false, false, false, 0);
33
- assert.equal(blocks.length, 3);
34
- assert.equal(blocks[0]!.itemIndex, 0);
35
- assert.ok(blocks[0]!.lines[0]!.startsWith("→"));
36
- assert.ok(blocks[0]!.lines[0]!.includes("1. a"));
37
- assert.ok(blocks[1]!.lines[0]!.startsWith(" "));
38
- });
31
+ it("wrapPlain: long word mid-sentence chunks and continues", () => {
32
+ expect(wrapPlain("a verylongwordhere b", 5)).toEqual(["a", "veryl", "ongwo", "rdher", "e b"]);
33
+ });
39
34
 
40
- test("buildItemBlocks: selected=1 → arrow pointer on block 1", () => {
41
- const blocks = buildItemBlocks(opts(["a", "b", "c"]), 80, false, false, false, 1);
42
- assert.ok(blocks[1]!.lines[0]!.startsWith("→"));
43
- assert.ok(blocks[0]!.lines[0]!.startsWith(" "));
44
- });
35
+ it("buildItemBlocks: 3 options, selected=0 → arrow on block 0", () => {
36
+ const blocks = buildItemBlocks(opts(["a", "b", "c"]), 80, false, false, false, 0);
37
+ expect(blocks).toHaveLength(3);
38
+ expect(blocks[0]!.itemIndex).toBe(0);
39
+ expect(blocks[0]!.lines[0]!.startsWith("→")).toBe(true);
40
+ expect(blocks[0]!.lines[0]!).toContain("1. a");
41
+ expect(blocks[1]!.lines[0]!.startsWith(" ")).toBe(true);
42
+ });
45
43
 
46
- test("buildItemBlocks: with description → description lines included", () => {
47
- const blocks = buildItemBlocks(
48
- [{ value: "a", label: "a", description: "some desc" }],
49
- 80,
50
- false,
51
- false,
52
- false,
53
- 0,
54
- );
55
- assert.ok(blocks[0]!.lines.length > 1);
56
- assert.ok(blocks[0]!.lines.some((l) => l.includes("some desc")));
57
- });
44
+ it("buildItemBlocks: selected=1 → arrow pointer on block 1", () => {
45
+ const blocks = buildItemBlocks(opts(["a", "b", "c"]), 80, false, false, false, 1);
46
+ expect(blocks[1]!.lines[0]!.startsWith("→")).toBe(true);
47
+ expect(blocks[0]!.lines[0]!.startsWith(" ")).toBe(true);
48
+ });
58
49
 
59
- test("buildItemBlocks: hideDescriptions=true → no description lines", () => {
60
- const blocks = buildItemBlocks(
61
- [{ value: "a", label: "a", description: "some desc" }],
62
- 80,
63
- false,
64
- false,
65
- false,
66
- 0,
67
- true,
68
- );
69
- // Only the title line, no description
70
- assert.equal(blocks[0]!.lines.length, 1);
71
- });
50
+ it("buildItemBlocks: with description → description lines included", () => {
51
+ const blocks = buildItemBlocks(
52
+ [{ value: "a", label: "a", description: "some desc" }],
53
+ 80,
54
+ false,
55
+ false,
56
+ false,
57
+ 0,
58
+ );
59
+ expect(blocks[0]!.lines.length).toBeGreaterThan(1);
60
+ expect(blocks[0]!.lines.some((l) => l.includes("some desc"))).toBe(true);
61
+ });
72
62
 
73
- test("buildItemBlocks: allowFreeform → freeform block appended", () => {
74
- const blocks = buildItemBlocks(opts(["a"]), 80, true, false, false, 0);
75
- assert.equal(blocks.length, 2);
76
- assert.ok(blocks[1]!.lines[0]!.includes("Type custom answer"));
77
- });
63
+ it("buildItemBlocks: hideDescriptions=true → no description lines", () => {
64
+ const blocks = buildItemBlocks(
65
+ [{ value: "a", label: "a", description: "some desc" }],
66
+ 80,
67
+ false,
68
+ false,
69
+ false,
70
+ 0,
71
+ true,
72
+ );
73
+ // Only the title line, no description
74
+ expect(blocks[0]!.lines).toHaveLength(1);
75
+ });
78
76
 
79
- test("buildItemBlocks: allowComment → comment-toggle block appended", () => {
80
- const blocks = buildItemBlocks(opts(["a"]), 80, false, true, false, 0);
81
- assert.equal(blocks.length, 2);
82
- assert.ok(blocks[1]!.lines[0]!.includes("[ ]"));
83
- assert.ok(blocks[1]!.lines[0]!.includes("Add extra context"));
84
- });
77
+ it("buildItemBlocks: allowFreeform → freeform block appended", () => {
78
+ const blocks = buildItemBlocks(opts(["a"]), 80, true, false, false, 0);
79
+ expect(blocks).toHaveLength(2);
80
+ expect(blocks[1]!.lines[0]!).toContain("Type custom answer");
81
+ });
85
82
 
86
- test("buildItemBlocks: allowComment + commentEnabled → [✓]", () => {
87
- const blocks = buildItemBlocks(opts(["a"]), 80, false, true, true, 0);
88
- assert.ok(blocks[1]!.lines[0]!.includes("[✓]"));
89
- });
83
+ it("buildItemBlocks: allowComment → comment-toggle block appended", () => {
84
+ const blocks = buildItemBlocks(opts(["a"]), 80, false, true, false, 0);
85
+ expect(blocks).toHaveLength(2);
86
+ expect(blocks[1]!.lines[0]!).toContain("[ ]");
87
+ expect(blocks[1]!.lines[0]!).toContain("Add extra context");
88
+ });
90
89
 
91
- test("flattenBlocks: correct selected flags", () => {
92
- const blocks: ItemBlock[] = [
93
- { itemIndex: 0, lines: ["a", "b"] },
94
- { itemIndex: 1, lines: ["c"] },
95
- ];
96
- const rows = flattenBlocks(blocks, 1);
97
- assert.equal(rows.length, 3);
98
- assert.equal(rows[0]!.selected, false);
99
- assert.equal(rows[1]!.selected, false);
100
- assert.equal(rows[2]!.selected, true);
101
- });
90
+ it("buildItemBlocks: allowComment + commentEnabled → [✓]", () => {
91
+ const blocks = buildItemBlocks(opts(["a"]), 80, false, true, true, 0);
92
+ expect(blocks[1]!.lines[0]!).toContain("[✓]");
93
+ });
102
94
 
103
- test("renderSingleSelectRows: few rows, all visible", () => {
104
- const rows = renderSingleSelectRows({
105
- options: opts(["a", "b"]),
106
- selectedIndex: 0,
107
- width: 80,
108
- allowFreeform: false,
95
+ it("buildItemBlocks: narrow width wraps labels", () => {
96
+ const blocks = buildItemBlocks(opts(["a very long label"]), 12, false, false, false, 0);
97
+ expect(blocks[0]!.lines.length).toBeGreaterThan(1);
109
98
  });
110
- assert.equal(rows.length, 2);
111
- });
112
99
 
113
- test("renderSingleSelectRows: maxRows truncation with indicator", () => {
114
- const rows = renderSingleSelectRows({
115
- options: opts(["a", "b", "c", "c", "e", "f", "g", "h"]),
116
- selectedIndex: 0,
117
- width: 80,
118
- allowFreeform: false,
119
- maxRows: 3,
120
- });
121
- assert.ok(rows.length <= 3);
122
- // Last row should be the indicator
123
- assert.ok(rows[rows.length - 1]!.line.includes("("));
124
- });
100
+ it("flattenBlocks: correct selected flags", () => {
101
+ const blocks: ItemBlock[] = [
102
+ { itemIndex: 0, lines: ["a", "b"] },
103
+ { itemIndex: 1, lines: ["c"] },
104
+ ];
105
+ const rows = flattenBlocks(blocks, 1);
106
+ expect(rows).toHaveLength(3);
107
+ expect(rows[0]!.selected).toBe(false);
108
+ expect(rows[1]!.selected).toBe(false);
109
+ expect(rows[2]!.selected).toBe(true);
110
+ });
111
+
112
+ it("renderSingleSelectRows: few rows, all visible", () => {
113
+ const rows = renderSingleSelectRows({
114
+ options: opts(["a", "b"]),
115
+ selectedIndex: 0,
116
+ width: 80,
117
+ allowFreeform: false,
118
+ });
119
+ expect(rows).toHaveLength(2);
120
+ });
121
+
122
+ it("renderSingleSelectRows: maxRows truncation with indicator", () => {
123
+ const rows = renderSingleSelectRows({
124
+ options: opts(["a", "b", "c", "c", "e", "f", "g", "h"]),
125
+ selectedIndex: 0,
126
+ width: 80,
127
+ allowFreeform: false,
128
+ maxRows: 3,
129
+ });
130
+ expect(rows.length).toBeLessThanOrEqual(3);
131
+ // Last row should be the indicator
132
+ expect(rows[rows.length - 1]!.line).toContain("(");
133
+ });
134
+
135
+ it("renderSingleSelectRows: selected block larger than available → only selected + indicator", () => {
136
+ const rows = renderSingleSelectRows({
137
+ options: [{ value: "a", label: "a".repeat(100) }],
138
+ selectedIndex: 0,
139
+ width: 10,
140
+ allowFreeform: false,
141
+ maxRows: 3,
142
+ });
143
+ expect(rows.length).toBeLessThanOrEqual(3);
144
+ expect(rows.every((r) => r.selected || r.line.includes("("))).toBe(true);
145
+ });
146
+
147
+ it("renderSingleSelectRows: maxRows=1 shows only the selected line", () => {
148
+ const rows = renderSingleSelectRows({
149
+ options: opts(["a", "b"]),
150
+ selectedIndex: 0,
151
+ width: 80,
152
+ allowFreeform: false,
153
+ maxRows: 1,
154
+ });
155
+ expect(rows.length).toBeLessThanOrEqual(1);
156
+ });
125
157
 
126
- test("renderSingleSelectRows: selected block larger than available → only selected + indicator", () => {
127
- const rows = renderSingleSelectRows({
128
- options: [{ value: "a", label: "a".repeat(100) }],
129
- selectedIndex: 0,
130
- width: 10,
131
- allowFreeform: false,
132
- maxRows: 3,
133
- });
134
- assert.ok(rows.length <= 3);
135
- assert.ok(rows.every((r) => r.selected || r.line.includes("(")));
136
- });
158
+ it("renderSingleSelectRows: empty options with freeform still renders", () => {
159
+ const rows = renderSingleSelectRows({
160
+ options: [],
161
+ selectedIndex: 0,
162
+ width: 80,
163
+ allowFreeform: true,
164
+ maxRows: 5,
165
+ });
166
+ expect(rows.length).toBeGreaterThan(0);
167
+ });
168
+
169
+ it("renderSingleSelectRows: empty options without freeform returns empty", () => {
170
+ const rows = renderSingleSelectRows({
171
+ options: [],
172
+ selectedIndex: 0,
173
+ width: 80,
174
+ allowFreeform: false,
175
+ maxRows: 5,
176
+ });
177
+ expect(rows).toEqual([]);
178
+ });
179
+
180
+ it("renderSingleSelectRows: previous block fits when next does not", () => {
181
+ // Selected block is small; the next block is too large but the previous fits.
182
+ const rows = renderSingleSelectRows({
183
+ options: [
184
+ { value: "a", label: "A" },
185
+ { value: "b", label: "B" },
186
+ { value: "c", label: "C".repeat(100) },
187
+ ],
188
+ selectedIndex: 1,
189
+ width: 10,
190
+ allowFreeform: false,
191
+ maxRows: 3,
192
+ });
193
+ expect(rows.length).toBeLessThanOrEqual(3);
194
+ expect(rows.some((r) => r.line.includes("A"))).toBe(true);
195
+ });
196
+
197
+ it("buildItemBlocks: wrapped freeform label uses continuation prefix", () => {
198
+ const blocks = buildItemBlocks(opts(["a"]), 12, true, false, false, 1);
199
+ expect(blocks[1]!.lines.length).toBeGreaterThan(1);
200
+ expect(blocks[1]!.lines[1]!.startsWith(" ")).toBe(true);
201
+ });
202
+ });
@@ -1,5 +1,4 @@
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 { Value } from "typebox/value";
5
4
 
@@ -9,22 +8,46 @@ const valid = {
9
8
  questions: [{ type: "select", question: "Pick", options: [{ value: "yes", label: "Yes" }] }],
10
9
  };
11
10
 
12
- test("QuestionParamsSchema accepts typed batch requests", () => {
13
- assert.equal(Value.Check(QuestionParamsSchema, valid), true);
14
- });
11
+ describe("QuestionParamsSchema", () => {
12
+ it("accepts typed batch requests", () => {
13
+ expect(Value.Check(QuestionParamsSchema, valid)).toBe(true);
14
+ });
15
+
16
+ it("accepts multiselect, text, ids, labels, context, allowOther, descriptions", () => {
17
+ expect(Value.Check(QuestionParamsSchema, {
18
+ questions: [
19
+ { type: "multiselect", id: "m", label: "M", question: "Pick many", context: "ctx", options: [{ value: "a", label: "A", description: "d" }], allowOther: true },
20
+ { type: "text", question: "Explain" },
21
+ ],
22
+ })).toBe(true);
23
+ });
24
+
25
+ it("rejects legacy and mistyped fields", () => {
26
+ expect(Value.Check(QuestionParamsSchema, { ...valid, timeout: 1000 })).toBe(false);
27
+ expect(Value.Check(QuestionParamsSchema, {
28
+ questions: [{ ...valid.questions[0], allowMultiple: true }],
29
+ })).toBe(false);
30
+ expect(Value.Check(QuestionParamsSchema, {
31
+ questions: [{ ...valid.questions[0], allowComment: true }],
32
+ })).toBe(false);
33
+ expect(Value.Check(QuestionParamsSchema, {
34
+ questions: [{ type: "text", question: "Explain", options: [] }],
35
+ })).toBe(false);
36
+ expect(Value.Check(QuestionParamsSchema, {
37
+ questions: [{ ...valid.questions[0], options: [{ value: "yes", label: "Yes", extra: true }] }],
38
+ })).toBe(false);
39
+ });
15
40
 
16
- test("QuestionParamsSchema rejects legacy and mistyped fields", () => {
17
- assert.equal(Value.Check(QuestionParamsSchema, { ...valid, timeout: 1000 }), false);
18
- assert.equal(Value.Check(QuestionParamsSchema, {
19
- questions: [{ ...valid.questions[0], allowMultiple: true }],
20
- }), false);
21
- assert.equal(Value.Check(QuestionParamsSchema, {
22
- questions: [{ ...valid.questions[0], allowComment: true }],
23
- }), false);
24
- assert.equal(Value.Check(QuestionParamsSchema, {
25
- questions: [{ type: "text", question: "Explain", options: [] }],
26
- }), false);
27
- assert.equal(Value.Check(QuestionParamsSchema, {
28
- questions: [{ ...valid.questions[0], options: [{ value: "yes", label: "Yes", extra: true }] }],
29
- }), false);
41
+ it("rejects empty questions and bad option shapes", () => {
42
+ expect(Value.Check(QuestionParamsSchema, { questions: [] })).toBe(false);
43
+ expect(Value.Check(QuestionParamsSchema, {
44
+ questions: [{ type: "select", question: "Pick", options: [] }],
45
+ })).toBe(false);
46
+ expect(Value.Check(QuestionParamsSchema, {
47
+ questions: [{ type: "select", question: "Pick", options: [{ value: "yes" }] }],
48
+ })).toBe(false);
49
+ expect(Value.Check(QuestionParamsSchema, {
50
+ questions: [{ type: "select", question: "Pick", options: [{ label: "Yes" }] }],
51
+ })).toBe(false);
52
+ });
30
53
  });
@@ -1,85 +1,107 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { normalizeShortcutSpec, isValidShortcutSpec, resolveShortcut } from "../shortcuts.ts";
5
- import { DISABLED_SHORTCUT } from "../constants.ts";
6
-
7
- test("normalizeShortcutSpec: undefined → undefined", () => {
8
- assert.equal(normalizeShortcutSpec(undefined), undefined);
9
- });
10
-
11
- test("normalizeShortcutSpec: null → null", () => {
12
- assert.equal(normalizeShortcutSpec(null), null);
13
- });
14
-
15
- test("normalizeShortcutSpec: disable values → null", () => {
16
- assert.equal(normalizeShortcutSpec("off"), null);
17
- assert.equal(normalizeShortcutSpec("none"), null);
18
- assert.equal(normalizeShortcutSpec("disabled"), null);
19
- assert.equal(normalizeShortcutSpec(""), null);
20
- assert.equal(normalizeShortcutSpec(" off "), null);
21
- });
22
-
23
- test("normalizeShortcutSpec: normalizes case and trims", () => {
24
- assert.equal(normalizeShortcutSpec("Alt+O"), "alt+o");
25
- assert.equal(normalizeShortcutSpec(" Ctrl+G "), "ctrl+g");
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { normalizeShortcutSpec, isValidShortcutSpec, resolveShortcut, resolveShortcuts, buildShortcut } from "../shortcuts.ts";
4
+ import { DISABLED_SHORTCUT, DEFAULT_COMMENT_TOGGLE_KEY } from "../constants.ts";
5
+
6
+ describe("shortcut resolution", () => {
7
+ it("normalizeShortcutSpec: undefined → undefined", () => {
8
+ expect(normalizeShortcutSpec(undefined)).toBeUndefined();
9
+ });
10
+
11
+ it("normalizeShortcutSpec: null → null", () => {
12
+ expect(normalizeShortcutSpec(null)).toBeNull();
13
+ });
14
+
15
+ it("normalizeShortcutSpec: disable values → null", () => {
16
+ expect(normalizeShortcutSpec("off")).toBeNull();
17
+ expect(normalizeShortcutSpec("none")).toBeNull();
18
+ expect(normalizeShortcutSpec("disabled")).toBeNull();
19
+ expect(normalizeShortcutSpec("")).toBeNull();
20
+ expect(normalizeShortcutSpec(" off ")).toBeNull();
21
+ });
22
+
23
+ it("normalizeShortcutSpec: normalizes case and trims", () => {
24
+ expect(normalizeShortcutSpec("Alt+O")).toBe("alt+o");
25
+ expect(normalizeShortcutSpec(" Ctrl+G ")).toBe("ctrl+g");
26
+ });
27
+
28
+ it("isValidShortcutSpec: empty → false", () => {
29
+ expect(isValidShortcutSpec("")).toBe(false);
30
+ expect(isValidShortcutSpec(" ")).toBe(false);
31
+ });
32
+
33
+ it("isValidShortcutSpec: valid specs", () => {
34
+ expect(isValidShortcutSpec("alt+o")).toBe(true);
35
+ expect(isValidShortcutSpec("ctrl+g")).toBe(true);
36
+ expect(isValidShortcutSpec("shift+f1")).toBe(true);
37
+ });
38
+
39
+ it("isValidShortcutSpec: leading/trailing + → false", () => {
40
+ expect(isValidShortcutSpec("+o")).toBe(false);
41
+ expect(isValidShortcutSpec("o+")).toBe(false);
42
+ });
43
+
44
+ it("isValidShortcutSpec: double + → false", () => {
45
+ expect(isValidShortcutSpec("alt++o")).toBe(false);
46
+ });
47
+
48
+ it("isValidShortcutSpec: illegal characters → false", () => {
49
+ expect(isValidShortcutSpec("alt o")).toBe(false);
50
+ expect(isValidShortcutSpec("alt\n")).toBe(false);
51
+ });
52
+
53
+ it("buildShortcut returns a matching shortcut", () => {
54
+ const shortcut = buildShortcut("ctrl+g");
55
+ expect(shortcut.disabled).toBe(false);
56
+ expect(shortcut.spec).toBe("ctrl+g");
57
+ expect(shortcut.matches("\x07")).toBe(true);
58
+ expect(shortcut.matches("x")).toBe(false);
59
+ });
60
+
61
+ it("resolveShortcut: uses param when valid", () => {
62
+ const result = resolveShortcut("alt+o", undefined, "alt+o");
63
+ expect(result.disabled).toBe(false);
64
+ expect(result.spec).toBe("alt+o");
65
+ });
66
+
67
+ it("resolveShortcut: param undefined, uses env", () => {
68
+ const result = resolveShortcut(undefined, "ctrl+x", "alt+o");
69
+ expect(result.disabled).toBe(false);
70
+ expect(result.spec).toBe("ctrl+x");
71
+ });
72
+
73
+ it("resolveShortcut: param null → disabled", () => {
74
+ expect(resolveShortcut(null, "ctrl+x", "alt+o")).toBe(DISABLED_SHORTCUT);
75
+ });
76
+
77
+ it("resolveShortcut: param undefined, env undefined, uses default", () => {
78
+ const result = resolveShortcut(undefined, undefined, "alt+o");
79
+ expect(result.disabled).toBe(false);
80
+ expect(result.spec).toBe("alt+o");
81
+ });
82
+
83
+ it("resolveShortcut: param 'off' → disabled", () => {
84
+ expect(resolveShortcut("off", "ctrl+x", "alt+o")).toBe(DISABLED_SHORTCUT);
85
+ });
86
+
87
+ it("resolveShortcut: invalid param falls through to env", () => {
88
+ const result = resolveShortcut("invalid++", "ctrl+x", "alt+o");
89
+ expect(result.disabled).toBe(false);
90
+ expect(result.spec).toBe("ctrl+x");
91
+ });
92
+
93
+ it("resolveShortcut: all invalid → disabled", () => {
94
+ expect(resolveShortcut("invalid++", "also++", "default++")).toBe(DISABLED_SHORTCUT);
95
+ });
96
+
97
+ it("resolveShortcuts builds the commentToggle map with the default key", () => {
98
+ const shortcuts = resolveShortcuts(undefined, undefined);
99
+ expect(shortcuts.commentToggle.disabled).toBe(false);
100
+ expect(shortcuts.commentToggle.spec).toBe(DEFAULT_COMMENT_TOGGLE_KEY);
101
+ });
102
+
103
+ it("DISABLED_SHORTCUT matches nothing", () => {
104
+ expect(DISABLED_SHORTCUT.matches("anything")).toBe(false);
105
+ expect(DISABLED_SHORTCUT.disabled).toBe(true);
106
+ });
26
107
  });
27
-
28
- test("isValidShortcutSpec: empty → false", () => {
29
- assert.equal(isValidShortcutSpec(""), false);
30
- assert.equal(isValidShortcutSpec(" "), false);
31
- });
32
-
33
- test("isValidShortcutSpec: valid specs", () => {
34
- assert.equal(isValidShortcutSpec("alt+o"), true);
35
- assert.equal(isValidShortcutSpec("ctrl+g"), true);
36
- assert.equal(isValidShortcutSpec("shift+f1"), true);
37
- });
38
-
39
- test("isValidShortcutSpec: leading/trailing + → false", () => {
40
- assert.equal(isValidShortcutSpec("+o"), false);
41
- assert.equal(isValidShortcutSpec("o+"), false);
42
- });
43
-
44
- test("isValidShortcutSpec: double + → false", () => {
45
- assert.equal(isValidShortcutSpec("alt++o"), false);
46
- });
47
-
48
- test("resolveShortcut: uses param when valid", () => {
49
- const result = resolveShortcut("alt+o", undefined, "alt+o");
50
- assert.equal(result.disabled, false);
51
- assert.equal(result.spec, "alt+o");
52
- });
53
-
54
- test("resolveShortcut: param undefined, uses env", () => {
55
- const result = resolveShortcut(undefined, "ctrl+x", "alt+o");
56
- assert.equal(result.disabled, false);
57
- assert.equal(result.spec, "ctrl+x");
58
- });
59
-
60
- test("resolveShortcut: param null → disabled", () => {
61
- const result = resolveShortcut(null, "ctrl+x", "alt+o");
62
- assert.equal(result, DISABLED_SHORTCUT);
63
- });
64
-
65
- test("resolveShortcut: param undefined, env undefined, uses default", () => {
66
- const result = resolveShortcut(undefined, undefined, "alt+o");
67
- assert.equal(result.disabled, false);
68
- assert.equal(result.spec, "alt+o");
69
- });
70
-
71
- test("resolveShortcut: param 'off' → disabled", () => {
72
- const result = resolveShortcut("off", "ctrl+x", "alt+o");
73
- assert.equal(result, DISABLED_SHORTCUT);
74
- });
75
-
76
- test("resolveShortcut: invalid param falls through to env", () => {
77
- const result = resolveShortcut("invalid++", "ctrl+x", "alt+o");
78
- assert.equal(result.disabled, false);
79
- assert.equal(result.spec, "ctrl+x");
80
- });
81
-
82
- test("resolveShortcut: all invalid → disabled", () => {
83
- const result = resolveShortcut("invalid++", "also++", "default++");
84
- assert.equal(result, DISABLED_SHORTCUT);
85
- });