@selesai/code 0.5.21 → 0.5.23
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.
- package/README.md +1 -1
- package/dist/core/agent-session-skill-block.test.d.ts +2 -0
- package/dist/core/agent-session-skill-block.test.d.ts.map +1 -0
- package/dist/core/agent-session-skill-block.test.js +20 -0
- package/dist/core/agent-session-skill-block.test.js.map +1 -0
- package/dist/core/agent-session.d.ts +7 -4
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +22 -12
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +1 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/data/capabilities.d.ts +48 -0
- package/dist/data/capabilities.d.ts.map +1 -0
- package/dist/data/capabilities.js +282 -0
- package/dist/data/capabilities.js.map +1 -0
- package/dist/data/navigation.d.ts +3 -0
- package/dist/data/navigation.d.ts.map +1 -0
- package/dist/data/navigation.js +90 -0
- package/dist/data/navigation.js.map +1 -0
- package/dist/extensions/inline-skills.test.ts +96 -0
- package/dist/extensions/inline-skills.ts +92 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +39 -0
- package/dist/extensions/pi-powerline-footer/tps.ts +172 -113
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +0 -4
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +1 -1
- package/dist/extensions/question/batch.ts +89 -25
- package/dist/extensions/question/constants.ts +1 -1
- package/dist/extensions/question/helpers.ts +34 -78
- package/dist/extensions/question/index.ts +327 -564
- package/dist/extensions/question/package.json +2 -1
- package/dist/extensions/question/question-list.ts +21 -14
- package/dist/extensions/question/row-layout.ts +2 -2
- package/dist/extensions/question/schemas.ts +33 -47
- package/dist/extensions/question/selection-mode.ts +5 -7
- package/dist/extensions/question/tests/batch.test.ts +65 -16
- package/dist/extensions/question/tests/helpers.test.ts +31 -156
- package/dist/extensions/question/tests/question-list.test.ts +17 -3
- package/dist/extensions/question/tests/row-layout.test.ts +4 -4
- package/dist/extensions/question/tests/schemas.test.ts +30 -0
- package/dist/extensions/question/tests/wizard.test.ts +99 -0
- package/dist/extensions/question/types.ts +65 -66
- package/dist/extensions/workflow/adapter.ts +20 -77
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +10 -10
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/skills/workflow-creation/SKILL.md +2 -2
- package/docs/skills.md +8 -0
- package/docs/workflows.md +4 -4
- package/package.json +1 -1
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
11
|
"@selesai/code": "*",
|
|
12
|
+
"@earendil-works/pi-ai": "*",
|
|
12
13
|
"@earendil-works/pi-tui": "*",
|
|
13
14
|
"typebox": "*"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
16
|
-
"test": "
|
|
17
|
+
"test": "tsx --test tests/**/*.test.ts"
|
|
17
18
|
}
|
|
18
19
|
}
|
|
@@ -7,14 +7,12 @@ import {
|
|
|
7
7
|
type Component,
|
|
8
8
|
fuzzyFilter,
|
|
9
9
|
Key,
|
|
10
|
-
Markdown,
|
|
11
10
|
matchesKey,
|
|
12
11
|
truncateToWidth,
|
|
13
12
|
wrapTextWithAnsi,
|
|
14
13
|
} from "@earendil-works/pi-tui";
|
|
15
14
|
|
|
16
15
|
import { COMMENT_TOGGLE_LABEL, SPLIT_PANE_LEFT_MIN_WIDTH, SPLIT_PANE_MIN_WIDTH, SPLIT_PANE_RIGHT_MIN_WIDTH, SPLIT_PANE_SEPARATOR } from "./constants.ts";
|
|
17
|
-
import { safeMarkdownTheme } from "./helpers.ts";
|
|
18
16
|
import { matchesDown, matchesUp } from "./navigation.ts";
|
|
19
17
|
import { renderSingleSelectRows } from "./row-layout.ts";
|
|
20
18
|
import type { QuestionOption, ResolvedShortcut } from "./types.ts";
|
|
@@ -67,6 +65,22 @@ export class QuestionList implements Component {
|
|
|
67
65
|
return this.commentEnabled;
|
|
68
66
|
}
|
|
69
67
|
|
|
68
|
+
restoreSelection(values: readonly string[], comment?: string): void {
|
|
69
|
+
const indexes = values
|
|
70
|
+
.map((value) => this.options.findIndex((option) => option.value === value))
|
|
71
|
+
.filter((index) => index >= 0);
|
|
72
|
+
this.checked = new Set(indexes);
|
|
73
|
+
this.selectedIndex = indexes[0] ?? 0;
|
|
74
|
+
this.commentEnabled = Boolean(comment);
|
|
75
|
+
this.searchQuery = "";
|
|
76
|
+
this.invalidate();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getCheckedValues(): string[] {
|
|
80
|
+
if (!this.selectionMode.multi) return [];
|
|
81
|
+
return this.selectionMode.buildResult({ selectedIndex: this.selectedIndex, checked: this.checked, options: this.options });
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
setMaxVisibleRows(rows: number): void {
|
|
71
85
|
if (this.selectionMode.multi) return; // ponytail: multi-select uses fixed max 10, no-op here
|
|
72
86
|
const next = Math.max(1, Math.floor(rows));
|
|
@@ -201,7 +215,6 @@ export class QuestionList implements Component {
|
|
|
201
215
|
|
|
202
216
|
private buildPreviewLines(width: number, filtered: QuestionOption[], maxLines: number): string[] {
|
|
203
217
|
if (maxLines <= 0) return [];
|
|
204
|
-
const mdTheme = safeMarkdownTheme();
|
|
205
218
|
let md = "";
|
|
206
219
|
|
|
207
220
|
if (this.isCommentToggleRow(this.selectedIndex, filtered)) {
|
|
@@ -223,14 +236,9 @@ export class QuestionList implements Component {
|
|
|
223
236
|
}
|
|
224
237
|
}
|
|
225
238
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
lines
|
|
229
|
-
} else {
|
|
230
|
-
lines = [];
|
|
231
|
-
for (const line of wrapTextWithAnsi(md.trim(), Math.max(10, width))) {
|
|
232
|
-
lines.push(truncateToWidth(line, width, ""));
|
|
233
|
-
}
|
|
239
|
+
const lines: string[] = [];
|
|
240
|
+
for (const line of wrapTextWithAnsi(md.trim(), Math.max(10, width))) {
|
|
241
|
+
lines.push(truncateToWidth(line, width, ""));
|
|
234
242
|
}
|
|
235
243
|
while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") lines.pop();
|
|
236
244
|
if (lines.length <= maxLines) return lines;
|
|
@@ -306,7 +314,7 @@ export class QuestionList implements Component {
|
|
|
306
314
|
this.onEnterFreeform?.();
|
|
307
315
|
return;
|
|
308
316
|
}
|
|
309
|
-
const result = filtered[this.selectedIndex]?.
|
|
317
|
+
const result = filtered[this.selectedIndex]?.value;
|
|
310
318
|
if (result) this.onSubmit?.([result], this.commentEnabled);
|
|
311
319
|
else this.onCancel?.();
|
|
312
320
|
return;
|
|
@@ -393,8 +401,7 @@ export class QuestionList implements Component {
|
|
|
393
401
|
checked: this.checked,
|
|
394
402
|
options: this.options,
|
|
395
403
|
});
|
|
396
|
-
|
|
397
|
-
else this.onCancel?.();
|
|
404
|
+
this.onSubmit?.(result, this.commentEnabled);
|
|
398
405
|
}
|
|
399
406
|
}
|
|
400
407
|
|
|
@@ -60,11 +60,11 @@ export function buildItemBlocks(
|
|
|
60
60
|
if (allowComment) {
|
|
61
61
|
allItems.push({
|
|
62
62
|
type: "comment-toggle",
|
|
63
|
-
option: { label: `${commentEnabled ? "[✓]" : "[ ]"} ${COMMENT_TOGGLE_LABEL}` },
|
|
63
|
+
option: { value: "__comment__", label: `${commentEnabled ? "[✓]" : "[ ]"} ${COMMENT_TOGGLE_LABEL}` },
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
if (allowFreeform) {
|
|
67
|
-
allItems.push({ type: "freeform", option: { label: FREEFORM_LABEL } });
|
|
67
|
+
allItems.push({ type: "freeform", option: { value: "__other__", label: FREEFORM_LABEL } });
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
return allItems.map((item, itemIndex) => {
|
|
@@ -1,57 +1,43 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Strict, batch-first TypeBox schema for the question tool.
|
|
2
2
|
|
|
3
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
5
|
|
|
5
|
-
const OptionSchema = Type.
|
|
6
|
-
Type.String({ description: "
|
|
7
|
-
Type.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}),
|
|
11
|
-
]);
|
|
6
|
+
const OptionSchema = Type.Object({
|
|
7
|
+
value: Type.String({ description: "Stable machine value returned when selected" }),
|
|
8
|
+
label: Type.String({ description: "Human-readable option label" }),
|
|
9
|
+
description: Type.Optional(Type.String({ description: "Optional help text shown under the option" })),
|
|
10
|
+
}, { additionalProperties: false });
|
|
12
11
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Type.Array(OptionSchema, {
|
|
19
|
-
description: "Optional multiple-choice options. Each option may be a string or { label, description }.",
|
|
20
|
-
}),
|
|
21
|
-
),
|
|
22
|
-
allowMultiple: Type.Optional(Type.Boolean({ description: "Allow selecting multiple options. Default: false" })),
|
|
23
|
-
allowFreeform: Type.Optional(
|
|
24
|
-
Type.Boolean({
|
|
25
|
-
description: "Allow a custom freeform answer. Defaults to true when no options are provided, false when options exist.",
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
allowComment: Type.Optional(Type.Boolean({ description: "Collect an optional comment after selecting one or more options. Default: false" })),
|
|
29
|
-
commentToggleKey: Type.Optional(
|
|
30
|
-
Type.String({
|
|
31
|
-
description:
|
|
32
|
-
"Shortcut for toggling the optional comment/extra-context row when allowComment is true, e.g. 'ctrl+g'. Pass 'off' to disable. Default: PI_QUESTION_COMMENT_TOGGLE_KEY env var if set, otherwise 'ctrl+g'.",
|
|
33
|
-
}),
|
|
34
|
-
),
|
|
12
|
+
const QuestionBase = {
|
|
13
|
+
id: Type.Optional(Type.String({ description: "Unique answer id. Missing ids become q1, q2, and so on." })),
|
|
14
|
+
label: Type.Optional(Type.String({ description: "Short page label. Defaults to Q1, Q2, and so on." })),
|
|
15
|
+
question: Type.String({ description: "Question shown to the user" }),
|
|
16
|
+
context: Type.Optional(Type.String({ description: "Optional plain-text context shown before the question" })),
|
|
35
17
|
};
|
|
36
18
|
|
|
37
|
-
const
|
|
38
|
-
...
|
|
39
|
-
|
|
40
|
-
})
|
|
19
|
+
const SelectQuestionSchema = Type.Object({
|
|
20
|
+
...QuestionBase,
|
|
21
|
+
type: StringEnum(["select"] as const),
|
|
22
|
+
options: Type.Array(OptionSchema, { minItems: 1, description: "Available choices" }),
|
|
23
|
+
allowOther: Type.Optional(Type.Boolean({ description: "Allow a custom Other text answer" })),
|
|
24
|
+
}, { additionalProperties: false });
|
|
41
25
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
})
|
|
26
|
+
const MultiSelectQuestionSchema = Type.Object({
|
|
27
|
+
...QuestionBase,
|
|
28
|
+
type: StringEnum(["multiselect"] as const),
|
|
29
|
+
options: Type.Array(OptionSchema, { minItems: 1, description: "Available choices" }),
|
|
30
|
+
allowOther: Type.Optional(Type.Boolean({ description: "Allow custom Other text alongside selected values" })),
|
|
31
|
+
}, { additionalProperties: false });
|
|
47
32
|
|
|
48
|
-
const
|
|
49
|
-
|
|
33
|
+
const TextQuestionSchema = Type.Object({
|
|
34
|
+
...QuestionBase,
|
|
35
|
+
type: StringEnum(["text"] as const),
|
|
36
|
+
}, { additionalProperties: false });
|
|
37
|
+
|
|
38
|
+
export const QuestionParamsSchema = Type.Object({
|
|
39
|
+
questions: Type.Array(Type.Union([SelectQuestionSchema, MultiSelectQuestionSchema, TextQuestionSchema]), {
|
|
50
40
|
minItems: 1,
|
|
51
|
-
description:
|
|
52
|
-
"Questions to answer in one paged form. User can move between pages with Tab/Shift+Tab or Left/Right, then press Enter on the final review page to submit all answers.",
|
|
41
|
+
description: "One or more typed questions shown as a paged wizard followed by atomic review and submission.",
|
|
53
42
|
}),
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
export const QuestionParamsSchema = Type.Union([SingleQuestionParamsSchema, BatchQuestionParamsSchema]);
|
|
43
|
+
}, { additionalProperties: false });
|
|
@@ -20,8 +20,8 @@ export const SingleSelect: SelectionMode = {
|
|
|
20
20
|
multi: false,
|
|
21
21
|
toggle() {},
|
|
22
22
|
buildResult(state) {
|
|
23
|
-
const
|
|
24
|
-
return
|
|
23
|
+
const value = state.options[state.selectedIndex]?.value;
|
|
24
|
+
return value ? [value] : [];
|
|
25
25
|
},
|
|
26
26
|
handleNumberKey(idx, count) {
|
|
27
27
|
return { toggle: false, selectIndex: Math.min(idx, count - 1) };
|
|
@@ -36,12 +36,10 @@ export const MultiSelect: SelectionMode = {
|
|
|
36
36
|
else checked.add(index);
|
|
37
37
|
},
|
|
38
38
|
buildResult(state) {
|
|
39
|
-
|
|
39
|
+
return Array.from(state.checked)
|
|
40
40
|
.sort((a, b) => a - b)
|
|
41
|
-
.map((i) => state.options[i]?.
|
|
42
|
-
.filter((
|
|
43
|
-
const fallback = state.options[state.selectedIndex]?.label;
|
|
44
|
-
return selectedTitles.length > 0 ? selectedTitles : fallback ? [fallback] : [];
|
|
41
|
+
.map((i) => state.options[i]?.value)
|
|
42
|
+
.filter((value): value is string => !!value);
|
|
45
43
|
},
|
|
46
44
|
handleNumberKey(idx, count) {
|
|
47
45
|
return { toggle: true, selectIndex: Math.min(idx, count - 1) };
|
|
@@ -1,26 +1,75 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { buildQuestionAnswers, prepareQuestions, saveDraftQuestion } from "../batch.ts";
|
|
5
|
+
import type { DraftQuestionState } from "../types.ts";
|
|
5
6
|
|
|
6
|
-
test("
|
|
7
|
-
const result =
|
|
8
|
-
{ question: "Pick a color", options: ["Red", "Blue"] },
|
|
9
|
-
{ id: "notes", label: "Notes", question: "Anything else?" },
|
|
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?" },
|
|
10
11
|
]);
|
|
11
12
|
assert.ok("questions" in result);
|
|
12
13
|
if (!("questions" in result)) return;
|
|
13
|
-
assert.deepEqual(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
assert.deepEqual(result.questions, [
|
|
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
|
+
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
|
+
});
|
|
51
|
+
|
|
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);
|
|
20
56
|
});
|
|
21
57
|
|
|
22
|
-
test("
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
]);
|
|
26
75
|
});
|
|
@@ -2,177 +2,52 @@ import test from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
normalizeOptions,
|
|
6
|
-
getOptionsFormatError,
|
|
7
|
-
createFreeformResponse,
|
|
8
5
|
createSelectionResponse,
|
|
6
|
+
createTextResponse,
|
|
7
|
+
formatQuestionResponse,
|
|
9
8
|
formatResponseSummary,
|
|
10
|
-
formatOptionsForMessage,
|
|
11
|
-
parseDialogSelections,
|
|
12
|
-
isCancelled,
|
|
13
9
|
oneLine,
|
|
10
|
+
previewText,
|
|
14
11
|
} from "../helpers.ts";
|
|
12
|
+
import type { PreparedQuestion } from "../types.ts";
|
|
15
13
|
|
|
16
|
-
test("
|
|
17
|
-
assert.
|
|
18
|
-
|
|
19
|
-
{ label: "b" },
|
|
20
|
-
]);
|
|
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" });
|
|
21
17
|
});
|
|
22
18
|
|
|
23
|
-
test("
|
|
24
|
-
assert.
|
|
25
|
-
|
|
26
|
-
[{ label: "a", description: "desc" }],
|
|
27
|
-
);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("normalizeOptions: mixed string and object", () => {
|
|
31
|
-
assert.deepEqual(normalizeOptions(["a", { label: "b", description: "d" }]), [
|
|
32
|
-
{ label: "a" },
|
|
33
|
-
{ label: "b", description: "d" },
|
|
34
|
-
]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("normalizeOptions: empty/undefined", () => {
|
|
38
|
-
assert.deepEqual(normalizeOptions(undefined), []);
|
|
39
|
-
assert.deepEqual(normalizeOptions([]), []);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("normalizeOptions: whitespace-only labels filtered", () => {
|
|
43
|
-
assert.deepEqual(normalizeOptions([" ", "a", ""]), [{ label: "a" }]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("getOptionsFormatError: rejects JSON-encoded option arrays", () => {
|
|
47
|
-
assert.match(getOptionsFormatError(['[{"label":"Yes","description":"desc"}]']) ?? "", /JSON-encoded string/);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("getOptionsFormatError: allows normal string labels", () => {
|
|
51
|
-
assert.equal(getOptionsFormatError(["Yes", "No"]), null);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("createFreeformResponse: null/undefined/empty → null", () => {
|
|
55
|
-
assert.equal(createFreeformResponse(null), null);
|
|
56
|
-
assert.equal(createFreeformResponse(undefined), null);
|
|
57
|
-
assert.equal(createFreeformResponse(""), null);
|
|
58
|
-
assert.equal(createFreeformResponse(" "), null);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("createFreeformResponse: non-empty → { kind: freeform }", () => {
|
|
62
|
-
assert.deepEqual(createFreeformResponse("hello"), { kind: "freeform", text: "hello" });
|
|
63
|
-
assert.deepEqual(createFreeformResponse(" trim "), { kind: "freeform", text: "trim" });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("createSelectionResponse: empty → null", () => {
|
|
67
|
-
assert.equal(createSelectionResponse([]), null);
|
|
68
|
-
assert.equal(createSelectionResponse(["", " "]), null);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("createSelectionResponse: single", () => {
|
|
72
|
-
assert.deepEqual(createSelectionResponse(["a"]), { kind: "selection", selections: ["a"] });
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("createSelectionResponse: multiple with trim", () => {
|
|
76
|
-
assert.deepEqual(createSelectionResponse([" a ", "b"]), { kind: "selection", selections: ["a", "b"] });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("createSelectionResponse: with comment", () => {
|
|
80
|
-
assert.deepEqual(createSelectionResponse(["a"], "note"), {
|
|
19
|
+
test("createSelectionResponse supports values plus Other and rejects empty", () => {
|
|
20
|
+
assert.equal(createSelectionResponse([], " "), null);
|
|
21
|
+
assert.deepEqual(createSelectionResponse([" a ", "b"], " other "), {
|
|
81
22
|
kind: "selection",
|
|
82
|
-
|
|
83
|
-
|
|
23
|
+
values: ["a", "b"],
|
|
24
|
+
otherText: "other",
|
|
84
25
|
});
|
|
85
26
|
});
|
|
86
27
|
|
|
87
|
-
test("
|
|
88
|
-
assert.
|
|
89
|
-
|
|
90
|
-
selections: ["a"],
|
|
91
|
-
comment: "note",
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("createSelectionResponse: empty comment → no comment field", () => {
|
|
96
|
-
assert.deepEqual(createSelectionResponse(["a"], " "), { kind: "selection", selections: ["a"] });
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("formatResponseSummary: freeform → text", () => {
|
|
100
|
-
assert.equal(formatResponseSummary({ kind: "freeform", text: "hello" }), "hello");
|
|
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");
|
|
101
31
|
});
|
|
102
32
|
|
|
103
|
-
test("
|
|
104
|
-
|
|
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");
|
|
105
43
|
});
|
|
106
44
|
|
|
107
|
-
test("
|
|
108
|
-
assert.equal(
|
|
109
|
-
|
|
110
|
-
"a, b — note",
|
|
111
|
-
);
|
|
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)}…`);
|
|
112
48
|
});
|
|
113
49
|
|
|
114
|
-
test("
|
|
115
|
-
assert.equal(formatOptionsForMessage([]), "");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("formatOptionsForMessage: 1 option", () => {
|
|
119
|
-
assert.equal(formatOptionsForMessage([{ label: "a" }]), "1. a");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("formatOptionsForMessage: with description", () => {
|
|
123
|
-
assert.equal(formatOptionsForMessage([{ label: "a", description: "desc" }]), "1. a — desc");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("formatOptionsForMessage: multiple", () => {
|
|
127
|
-
assert.equal(
|
|
128
|
-
formatOptionsForMessage([{ label: "a" }, { label: "b", description: "d" }]),
|
|
129
|
-
"1. a\n2. b — d",
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("parseDialogSelections: basic CSV", () => {
|
|
134
|
-
assert.deepEqual(parseDialogSelections("a,b,c"), ["a", "b", "c"]);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("parseDialogSelections: whitespace trimmed", () => {
|
|
138
|
-
assert.deepEqual(parseDialogSelections(" a , b "), ["a", "b"]);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("parseDialogSelections: empty → []", () => {
|
|
142
|
-
assert.deepEqual(parseDialogSelections(""), []);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("parseDialogSelections: empty entries filtered", () => {
|
|
146
|
-
assert.deepEqual(parseDialogSelections("a,,b"), ["a", "b"]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("isCancelled: null/undefined → true", () => {
|
|
150
|
-
assert.equal(isCancelled(null), true);
|
|
151
|
-
assert.equal(isCancelled(undefined), true);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("isCancelled: falsy non-null → false", () => {
|
|
155
|
-
assert.equal(isCancelled(""), false);
|
|
156
|
-
assert.equal(isCancelled(0), false);
|
|
157
|
-
assert.equal(isCancelled(false), false);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("oneLine: undefined → empty string", () => {
|
|
161
|
-
assert.equal(oneLine(undefined), "");
|
|
162
|
-
assert.equal(oneLine(null), "");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test("oneLine: short → unchanged", () => {
|
|
166
|
-
assert.equal(oneLine("hello"), "hello");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("oneLine: long → truncated with …", () => {
|
|
170
|
-
const long = "a".repeat(120);
|
|
171
|
-
const result = oneLine(long, 10);
|
|
172
|
-
assert.equal(result.length, 10);
|
|
173
|
-
assert.equal(result.endsWith("…"), true);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("oneLine: whitespace collapsed", () => {
|
|
50
|
+
test("oneLine collapses whitespace and truncates", () => {
|
|
177
51
|
assert.equal(oneLine(" hello world "), "hello world");
|
|
178
|
-
});
|
|
52
|
+
assert.equal(oneLine("a".repeat(20), 10), `${"a".repeat(9)}…`);
|
|
53
|
+
});
|
|
@@ -36,7 +36,7 @@ const disabledShortcut: ResolvedShortcut = {
|
|
|
36
36
|
matches: ((_data: string) => false) as (data: string) => false,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const opts = (labels: string[]) => labels.map((label) => ({ label }));
|
|
39
|
+
const opts = (labels: string[]) => labels.map((label) => ({ value: label, label }));
|
|
40
40
|
|
|
41
41
|
// Terminal data bytes
|
|
42
42
|
const ESC = "\x1b";
|
|
@@ -163,12 +163,26 @@ test("MultiSelect: confirm with checked items → onSubmit with sorted labels",
|
|
|
163
163
|
assert.deepEqual(submitted, [["a", "b"], false]); // sorted by index
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
test("
|
|
166
|
+
test("QuestionList: restores saved multi-select choices and comment state", () => {
|
|
167
|
+
const list = new QuestionList(opts(["a", "b", "c"]), MultiSelect, false, true, fakeTheme, fakeKeybindings(), disabledShortcut);
|
|
168
|
+
list.restoreSelection(["a", "c"], "reason");
|
|
169
|
+
assert.deepEqual([...((list as any).checked)], [0, 2]);
|
|
170
|
+
assert.equal((list as any).selectedIndex, 0);
|
|
171
|
+
assert.equal(list.isCommentEnabled(), true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("QuestionList: restores saved single-select cursor", () => {
|
|
175
|
+
const list = new QuestionList(opts(["a", "b", "c"]), SingleSelect, false, false, fakeTheme, fakeKeybindings(), disabledShortcut);
|
|
176
|
+
list.restoreSelection(["b"]);
|
|
177
|
+
assert.equal((list as any).selectedIndex, 1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("MultiSelect: confirm with nothing checked → empty selection", () => {
|
|
167
181
|
const list = new QuestionList(opts(["a", "b"]), MultiSelect, false, false, fakeTheme, fakeKeybindings(["tui.select.confirm"]), disabledShortcut);
|
|
168
182
|
let submitted: [string[], boolean] | null = null;
|
|
169
183
|
list.onSubmit = (selections, commentEnabled) => (submitted = [selections, commentEnabled]);
|
|
170
184
|
list.handleInput(ENTER);
|
|
171
|
-
assert.deepEqual(submitted, [[
|
|
185
|
+
assert.deepEqual(submitted, [[], false]);
|
|
172
186
|
});
|
|
173
187
|
|
|
174
188
|
test("MultiSelect: confirm with no options → onCancel", () => {
|
|
@@ -4,7 +4,7 @@ import assert from "node:assert/strict";
|
|
|
4
4
|
import { wrapPlain, buildItemBlocks, flattenBlocks, renderSingleSelectRows } from "../row-layout.ts";
|
|
5
5
|
import type { ItemBlock, QuestionOption } from "../types.ts";
|
|
6
6
|
|
|
7
|
-
const opts = (labels: string[]): QuestionOption[] => labels.map((label) => ({ label }));
|
|
7
|
+
const opts = (labels: string[]): QuestionOption[] => labels.map((label) => ({ value: label, label }));
|
|
8
8
|
|
|
9
9
|
test("wrapPlain: empty → ['']", () => {
|
|
10
10
|
assert.deepEqual(wrapPlain("", 20), [""]);
|
|
@@ -45,7 +45,7 @@ test("buildItemBlocks: selected=1 → arrow pointer on block 1", () => {
|
|
|
45
45
|
|
|
46
46
|
test("buildItemBlocks: with description → description lines included", () => {
|
|
47
47
|
const blocks = buildItemBlocks(
|
|
48
|
-
[{ label: "a", description: "some desc" }],
|
|
48
|
+
[{ value: "a", label: "a", description: "some desc" }],
|
|
49
49
|
80,
|
|
50
50
|
false,
|
|
51
51
|
false,
|
|
@@ -58,7 +58,7 @@ test("buildItemBlocks: with description → description lines included", () => {
|
|
|
58
58
|
|
|
59
59
|
test("buildItemBlocks: hideDescriptions=true → no description lines", () => {
|
|
60
60
|
const blocks = buildItemBlocks(
|
|
61
|
-
[{ label: "a", description: "some desc" }],
|
|
61
|
+
[{ value: "a", label: "a", description: "some desc" }],
|
|
62
62
|
80,
|
|
63
63
|
false,
|
|
64
64
|
false,
|
|
@@ -125,7 +125,7 @@ test("renderSingleSelectRows: maxRows truncation with indicator", () => {
|
|
|
125
125
|
|
|
126
126
|
test("renderSingleSelectRows: selected block larger than available → only selected + indicator", () => {
|
|
127
127
|
const rows = renderSingleSelectRows({
|
|
128
|
-
options: [{ label: "a".repeat(100) }],
|
|
128
|
+
options: [{ value: "a", label: "a".repeat(100) }],
|
|
129
129
|
selectedIndex: 0,
|
|
130
130
|
width: 10,
|
|
131
131
|
allowFreeform: false,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { Value } from "typebox/value";
|
|
5
|
+
|
|
6
|
+
import { QuestionParamsSchema } from "../schemas.ts";
|
|
7
|
+
|
|
8
|
+
const valid = {
|
|
9
|
+
questions: [{ type: "select", question: "Pick", options: [{ value: "yes", label: "Yes" }] }],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
test("QuestionParamsSchema accepts typed batch requests", () => {
|
|
13
|
+
assert.equal(Value.Check(QuestionParamsSchema, valid), true);
|
|
14
|
+
});
|
|
15
|
+
|
|
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);
|
|
30
|
+
});
|