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