@selesai/code 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -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/models.json +5 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/agent-browser.test.ts +178 -2
- package/dist/extensions/agent-browser.ts +2 -0
- package/dist/extensions/copy-turn.test.ts +229 -0
- package/dist/extensions/copy-turn.ts +11 -0
- package/dist/extensions/grep-app/index.test.ts +356 -1
- package/dist/extensions/grep-app/index.ts +6 -1
- package/dist/extensions/handoff-new.test.ts +351 -161
- package/dist/extensions/handoff-new.ts +3 -0
- package/dist/extensions/inline-skills.test.ts +93 -0
- package/dist/extensions/inline-skills.ts +3 -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/builder.md +5 -2
- package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
- 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 -11
- 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 +4 -4
- 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 +16 -15
- 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 +59 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +146 -23
- 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 +17 -6
- 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 +11 -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/notify.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +224 -42
- 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 +35 -26
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +108 -9
- 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/background/wait-subscriptions.ts +2 -1
- 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/foreground-history.ts +7 -4
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +368 -50
- 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/session-lineage.ts +71 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +66 -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 +129 -17
- 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/src/workflows/workflow-auto-relaunch.ts +28 -0
- 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/handoff-adoption.test.ts +103 -0
- 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 +91 -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/session-lineage.test.ts +73 -0
- 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-auto-relaunch.test.ts +45 -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/ponytail/index.js +11 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
- package/dist/extensions/ponytail/test/extension.test.js +274 -140
- package/dist/extensions/ponytail/test/helpers.test.js +280 -92
- package/dist/extensions/question/index.ts +33 -0
- package/dist/extensions/question/question-list.ts +28 -0
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/batch.test.ts +103 -65
- 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 +75 -35
- package/dist/extensions/question/tests/question-list.test.ts +640 -198
- package/dist/extensions/question/tests/row-layout.test.ts +193 -114
- package/dist/extensions/question/tests/schemas.test.ts +42 -19
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
- package/dist/extensions/question/tests/wizard.test.ts +771 -50
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- package/dist/extensions/rtk.test.ts +180 -1
- package/dist/extensions/tokenin-onboarding.ts +3 -0
- package/dist/extensions/undo.test.ts +720 -0
- package/dist/extensions/undo.ts +6 -0
- package/dist/extensions/web-agent-onboarding.test.ts +415 -0
- package/dist/extensions/workflow/extension.ts +3 -3
- package/dist/extensions/workflow/modes.ts +41 -19
- 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 +16 -15
- 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/plans/workflow-autoloop-reference.md +1 -2
- package/docs/plans/workflow-handoff-carryover.md +108 -0
- package/docs/settings.md +6 -0
- package/package.json +4 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
|
-
import test from "node:test";
|
|
7
6
|
|
|
8
7
|
const require = createRequire(import.meta.url);
|
|
9
8
|
const { getPonytailInstructions } = require("../ponytail-instructions.cjs");
|
|
@@ -16,109 +15,298 @@ import {
|
|
|
16
15
|
writeDefaultMode,
|
|
17
16
|
} from "../index.js";
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
18
|
+
describe("ponytail helpers", () => {
|
|
19
|
+
it("parsePonytailCommand falls back to full when invoked bare and default is off", () => {
|
|
20
|
+
expect(parsePonytailCommand("", "off")).toEqual({ type: "set-mode", mode: "full" });
|
|
21
|
+
});
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
assert.deepEqual(parsePonytailCommand("default lite", "full"), { type: "set-default", mode: "lite" });
|
|
27
|
-
});
|
|
23
|
+
it("parsePonytailCommand falls back to DEFAULT_MODE for invalid defaults", () => {
|
|
24
|
+
expect(parsePonytailCommand("", "bogus")).toEqual({ type: "set-mode", mode: "full" });
|
|
25
|
+
});
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
it("parsePonytailCommand parses modes, status, and default subcommand", () => {
|
|
28
|
+
expect(parsePonytailCommand("ultra", "full")).toEqual({ type: "set-mode", mode: "ultra" });
|
|
29
|
+
expect(parsePonytailCommand("status", "full")).toEqual({ type: "status" });
|
|
30
|
+
expect(parsePonytailCommand("default lite", "full")).toEqual({ type: "set-default", mode: "lite" });
|
|
31
|
+
});
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
});
|
|
33
|
+
it("parsePonytailCommand rejects invalid modes and default modes", () => {
|
|
34
|
+
expect(parsePonytailCommand("bogus", "full")).toEqual({ type: "invalid", reason: "invalid-mode", mode: "bogus" });
|
|
35
|
+
expect(parsePonytailCommand("default bogus", "full")).toEqual({ type: "invalid", reason: "invalid-default-mode" });
|
|
36
|
+
});
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
assert.equal(resolveSessionMode({}, "full"), "full");
|
|
42
|
-
assert.equal(resolveSessionMode("not an array"), "full"); // DEFAULT_MODE fallback
|
|
43
|
-
});
|
|
38
|
+
it("parsePonytailCommand bare with default full returns full", () => {
|
|
39
|
+
expect(parsePonytailCommand("", "full")).toEqual({ type: "set-mode", mode: "full" });
|
|
40
|
+
});
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
process.env.XDG_CONFIG_HOME = tempDir;
|
|
51
|
-
delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
assert.equal(readDefaultMode(), "full");
|
|
55
|
-
assert.equal(writeDefaultMode("ultra"), "ultra");
|
|
56
|
-
assert.equal(readDefaultMode(), "ultra");
|
|
57
|
-
assert.ok(existsSync(configPath));
|
|
58
|
-
assert.deepEqual(JSON.parse(readFileSync(configPath, "utf8")), { defaultMode: "ultra" });
|
|
59
|
-
} finally {
|
|
60
|
-
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
61
|
-
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
62
|
-
if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
63
|
-
else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
|
|
64
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
65
|
-
}
|
|
66
|
-
});
|
|
42
|
+
it("resolveSessionMode prefers latest persisted session mode", () => {
|
|
43
|
+
const entries = [
|
|
44
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
|
|
45
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } },
|
|
46
|
+
];
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
|
|
48
|
+
expect(resolveSessionMode(entries, "full")).toBe("ultra");
|
|
49
|
+
});
|
|
70
50
|
|
|
71
|
-
|
|
51
|
+
it("resolveSessionMode returns fallback when entries is not an array", () => {
|
|
52
|
+
expect(resolveSessionMode(null, "ultra")).toBe("ultra");
|
|
53
|
+
expect(resolveSessionMode(undefined, "lite")).toBe("lite");
|
|
54
|
+
expect(resolveSessionMode({}, "full")).toBe("full");
|
|
55
|
+
expect(resolveSessionMode("not an array")).toBe("full"); // DEFAULT_MODE fallback
|
|
56
|
+
});
|
|
72
57
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
58
|
+
it("resolveSessionMode ignores non-ponytail entries and invalid modes", () => {
|
|
59
|
+
const entries = [
|
|
60
|
+
{ type: "custom", customType: "other", data: { mode: "ultra" } },
|
|
61
|
+
{ type: "custom", customType: "ponytail-mode", data: { mode: "bogus" } },
|
|
62
|
+
];
|
|
63
|
+
expect(resolveSessionMode(entries, "lite")).toBe("lite");
|
|
64
|
+
});
|
|
80
65
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
it("readDefaultMode and writeDefaultMode use XDG config path", () => {
|
|
67
|
+
const tempDir = mkdtempSync(join(tmpdir(), "ponytail-config-"));
|
|
68
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
69
|
+
const previousDefault = process.env.PONYTAIL_DEFAULT_MODE;
|
|
70
|
+
const configPath = join(tempDir, "ponytail", "config.json");
|
|
71
|
+
process.env.XDG_CONFIG_HOME = tempDir;
|
|
72
|
+
delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
74
|
+
try {
|
|
75
|
+
expect(readDefaultMode()).toBe("full");
|
|
76
|
+
expect(writeDefaultMode("ultra")).toBe("ultra");
|
|
77
|
+
expect(readDefaultMode()).toBe("ultra");
|
|
78
|
+
expect(existsSync(configPath)).toBe(true);
|
|
79
|
+
expect(JSON.parse(readFileSync(configPath, "utf8"))).toEqual({ defaultMode: "ultra" });
|
|
80
|
+
} finally {
|
|
81
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
82
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
83
|
+
if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
84
|
+
else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
|
|
85
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
91
86
|
}
|
|
92
|
-
}
|
|
87
|
+
});
|
|
93
88
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
it("readDefaultMode honors PONYTAIL_DEFAULT_MODE env var", () => {
|
|
90
|
+
const previous = process.env.PONYTAIL_DEFAULT_MODE;
|
|
91
|
+
process.env.PONYTAIL_DEFAULT_MODE = "lite";
|
|
92
|
+
try {
|
|
93
|
+
expect(readDefaultMode()).toBe("lite");
|
|
94
|
+
} finally {
|
|
95
|
+
if (previous === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
96
|
+
else process.env.PONYTAIL_DEFAULT_MODE = previous;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
it("readDefaultMode falls back to full when env is invalid", () => {
|
|
101
|
+
const previous = process.env.PONYTAIL_DEFAULT_MODE;
|
|
102
|
+
process.env.PONYTAIL_DEFAULT_MODE = "bogus";
|
|
103
|
+
try {
|
|
104
|
+
expect(readDefaultMode()).toBe("full");
|
|
105
|
+
} finally {
|
|
106
|
+
if (previous === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
|
|
107
|
+
else process.env.PONYTAIL_DEFAULT_MODE = previous;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
it("writeDefaultMode rejects invalid modes", () => {
|
|
112
|
+
expect(writeDefaultMode("bogus")).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("filterSkillBodyForMode keeps only requested intensity examples and rows", () => {
|
|
116
|
+
const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: Lite example\n- full: Full example\n- ultra: Ultra example\nOther line`;
|
|
117
|
+
|
|
118
|
+
const filtered = filterSkillBodyForMode(body, "ultra");
|
|
119
|
+
|
|
120
|
+
expect(filtered).not.toContain("keep lite");
|
|
121
|
+
expect(filtered).not.toContain("keep full");
|
|
122
|
+
expect(filtered).toContain("keep ultra");
|
|
123
|
+
expect(filtered).not.toContain("Lite example");
|
|
124
|
+
expect(filtered).toContain("Ultra example");
|
|
125
|
+
expect(filtered).toContain("Other line");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("compact Ponytail instructions retain core rules and mode distinctions", () => {
|
|
129
|
+
const compact = Object.fromEntries(
|
|
130
|
+
["lite", "full", "ultra"].map((mode) => [mode, getPonytailInstructions(mode, { compact: true })]),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
for (const [mode, instructions] of Object.entries(compact)) {
|
|
134
|
+
expect(instructions).toMatch(new RegExp(`^PONYTAIL ${mode.toUpperCase()}:`));
|
|
135
|
+
expect(instructions.length).toBeGreaterThanOrEqual(300);
|
|
136
|
+
expect(instructions.length).toBeLessThanOrEqual(450);
|
|
137
|
+
for (const phrase of ["stop ponytail", "Trace first", "root cause", "validation", "data-loss", "security", "accessibility", "runnable check"]) {
|
|
138
|
+
expect(instructions).toContain(phrase);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
expect(compact.lite).toMatch(/Build the ask; name a lazier alternative/);
|
|
143
|
+
expect(compact.full).toMatch(/Enforce ladder; shortest diff/);
|
|
144
|
+
expect(compact.ultra).toMatch(/YAGNI first; delete; challenge the rest/);
|
|
145
|
+
|
|
146
|
+
const fullSkill = getPonytailInstructions("full");
|
|
147
|
+
expect(fullSkill).toMatch(/## The ladder/);
|
|
148
|
+
expect(fullSkill.length).toBeGreaterThan(compact.full.length);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("review instructions remain independent in compact mode", () => {
|
|
152
|
+
const expected = "PONYTAIL MODE ACTIVE — level: review. Behavior defined by /ponytail-review skill.";
|
|
153
|
+
expect(getPonytailInstructions("review")).toBe(expected);
|
|
154
|
+
expect(getPonytailInstructions("review", { compact: true })).toBe(expected);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("filterSkillBodyForMode keeps rule bullets that contain a colon", () => {
|
|
158
|
+
// Regression: rule bullets outside the Intensity section (e.g. the
|
|
159
|
+
// "No unrequested abstractions:" rule or the `ponytail:` comment convention)
|
|
160
|
+
// contain a colon and must not be mistaken for mode-example lines.
|
|
161
|
+
const skillPath = new URL("../../../skills/ponytail/SKILL.md", import.meta.url);
|
|
162
|
+
const body = readFileSync(skillPath, "utf8");
|
|
163
|
+
|
|
164
|
+
const filtered = filterSkillBodyForMode(body, "full");
|
|
165
|
+
|
|
166
|
+
expect(filtered).toContain("No unrequested abstractions");
|
|
167
|
+
expect(filtered).toContain("Mark deliberate simplifications");
|
|
168
|
+
// The Intensity examples are still filtered down to the active mode.
|
|
169
|
+
expect(filtered).toContain('full: "`@lru_cache');
|
|
170
|
+
expect(filtered).not.toContain('lite: "Done');
|
|
171
|
+
expect(filtered).not.toContain('ultra: "No cache');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("filterSkillBodyForMode tolerates a missing body", () => {
|
|
175
|
+
expect(filterSkillBodyForMode(undefined, "full")).toBe("");
|
|
176
|
+
expect(filterSkillBodyForMode(null, "full")).toBe("");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("filterSkillBodyForMode keeps non-mode table rows", () => {
|
|
180
|
+
const body = `| **not-a-mode** | keep this |\n| **full** | keep full |`;
|
|
181
|
+
const filtered = filterSkillBodyForMode(body, "full");
|
|
182
|
+
expect(filtered).toContain("keep this");
|
|
183
|
+
expect(filtered).toContain("keep full");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("getPonytailInstructions falls back when the skill file is missing", () => {
|
|
187
|
+
const instructions = getPonytailInstructions("full", { compact: false });
|
|
188
|
+
// With the real skill file present this returns the file-based instructions;
|
|
189
|
+
// the fallback path is exercised by deleting the skill file temporarily.
|
|
190
|
+
expect(instructions.length).toBeGreaterThan(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("getPonytailInstructions falls back when the skill file is unreadable", () => {
|
|
194
|
+
const { getPonytailInstructions: get } = require("../ponytail-instructions.cjs");
|
|
195
|
+
const fs = require("node:fs");
|
|
196
|
+
const path = require("node:path");
|
|
197
|
+
const skillPath = path.join(__dirname, "..", "..", "..", "skills", "ponytail", "SKILL.md");
|
|
198
|
+
const original = fs.readFileSync(skillPath, "utf8");
|
|
199
|
+
try {
|
|
200
|
+
// Temporarily replace the skill file with a directory so readFileSync throws.
|
|
201
|
+
fs.rmSync(skillPath, { force: true });
|
|
202
|
+
fs.mkdirSync(skillPath, { recursive: true });
|
|
203
|
+
const fallback = get("full");
|
|
204
|
+
expect(fallback).toContain("PONYTAIL MODE ACTIVE");
|
|
205
|
+
expect(fallback).toContain("## The ladder");
|
|
206
|
+
} finally {
|
|
207
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
208
|
+
fs.writeFileSync(skillPath, original, "utf8");
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("getPonytailInstructions falls back for invalid modes", () => {
|
|
213
|
+
const { getPonytailInstructions: get } = require("../ponytail-instructions.cjs");
|
|
214
|
+
const fallback = get("bogus");
|
|
215
|
+
expect(fallback).toContain("PONYTAIL MODE ACTIVE");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("isShellSafe accepts ordinary paths and rejects hostile ones", () => {
|
|
219
|
+
const { isShellSafe } = require("../ponytail-config.cjs");
|
|
220
|
+
expect(isShellSafe("/Users/me/projects/my-app")).toBe(true);
|
|
221
|
+
expect(isShellSafe("C:\\Users\\me\\app")).toBe(true);
|
|
222
|
+
expect(isShellSafe("~/.config/ponytail")).toBe(true);
|
|
223
|
+
expect(isShellSafe("path; rm -rf /")).toBe(false);
|
|
224
|
+
expect(isShellSafe("path&echo")).toBe(false);
|
|
225
|
+
expect(isShellSafe("path$HOME")).toBe(false);
|
|
226
|
+
expect(isShellSafe("path`id`")).toBe(false);
|
|
227
|
+
expect(isShellSafe(42)).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("getClaudeDir honors CLAUDE_CONFIG_DIR and falls back to ~/.claude", () => {
|
|
231
|
+
const { getClaudeDir } = require("../ponytail-config.cjs");
|
|
232
|
+
const previous = process.env.CLAUDE_CONFIG_DIR;
|
|
233
|
+
process.env.CLAUDE_CONFIG_DIR = "/custom/claude";
|
|
234
|
+
try {
|
|
235
|
+
expect(getClaudeDir()).toBe("/custom/claude");
|
|
236
|
+
} finally {
|
|
237
|
+
if (previous === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
238
|
+
else process.env.CLAUDE_CONFIG_DIR = previous;
|
|
239
|
+
}
|
|
240
|
+
const os = require("node:os");
|
|
241
|
+
const path = require("node:path");
|
|
242
|
+
expect(getClaudeDir()).toBe(path.join(os.homedir(), ".claude"));
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("getConfigDir uses APPDATA on win32 without XDG", () => {
|
|
246
|
+
const { getConfigDir } = require("../ponytail-config.cjs");
|
|
247
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
248
|
+
const previousAppData = process.env.APPDATA;
|
|
249
|
+
const original = process.platform;
|
|
250
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
251
|
+
process.env.APPDATA = "/appdata";
|
|
252
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
253
|
+
try {
|
|
254
|
+
expect(getConfigDir()).toBe("/appdata/ponytail");
|
|
255
|
+
} finally {
|
|
256
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
257
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
258
|
+
if (previousAppData === undefined) delete process.env.APPDATA;
|
|
259
|
+
else process.env.APPDATA = previousAppData;
|
|
260
|
+
Object.defineProperty(process, "platform", { value: original });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("getConfigDir falls back to ~/.config/ponytail on POSIX", () => {
|
|
265
|
+
const { getConfigDir } = require("../ponytail-config.cjs");
|
|
266
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
267
|
+
const original = process.platform;
|
|
268
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
269
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
270
|
+
try {
|
|
271
|
+
const os = require("node:os");
|
|
272
|
+
const path = require("node:path");
|
|
273
|
+
expect(getConfigDir()).toBe(path.join(os.homedir(), ".config", "ponytail"));
|
|
274
|
+
} finally {
|
|
275
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
276
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
277
|
+
Object.defineProperty(process, "platform", { value: original });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("getConfigDir uses XDG_CONFIG_HOME when set", () => {
|
|
282
|
+
const { getConfigDir } = require("../ponytail-config.cjs");
|
|
283
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
284
|
+
process.env.XDG_CONFIG_HOME = "/xdg";
|
|
285
|
+
try {
|
|
286
|
+
expect(getConfigDir()).toBe("/xdg/ponytail");
|
|
287
|
+
} finally {
|
|
288
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
289
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("getConfigPath joins the config dir with config.json", () => {
|
|
294
|
+
const { getConfigPath } = require("../ponytail-config.cjs");
|
|
295
|
+
const previousXdg = process.env.XDG_CONFIG_HOME;
|
|
296
|
+
process.env.XDG_CONFIG_HOME = "/xdg";
|
|
297
|
+
try {
|
|
298
|
+
expect(getConfigPath()).toBe("/xdg/ponytail/config.json");
|
|
299
|
+
} finally {
|
|
300
|
+
if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
301
|
+
else process.env.XDG_CONFIG_HOME = previousXdg;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
108
304
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const filtered = filterSkillBodyForMode(body, "full");
|
|
117
|
-
|
|
118
|
-
assert.ok(filtered.includes("No unrequested abstractions"));
|
|
119
|
-
assert.ok(filtered.includes("Mark deliberate simplifications"));
|
|
120
|
-
// The Intensity examples are still filtered down to the active mode.
|
|
121
|
-
assert.ok(filtered.includes('full: "`@lru_cache'));
|
|
122
|
-
assert.ok(!filtered.includes('lite: "Done'));
|
|
123
|
-
assert.ok(!filtered.includes('ultra: "No cache'));
|
|
305
|
+
it("normalizeMode and normalizeConfigMode reject non-strings", () => {
|
|
306
|
+
const { normalizeMode, normalizeConfigMode, normalizePersistedMode } = require("../ponytail-config.cjs");
|
|
307
|
+
expect(normalizeMode(42)).toBeNull();
|
|
308
|
+
expect(normalizeConfigMode(42)).toBeNull();
|
|
309
|
+
expect(normalizePersistedMode(42)).toBeNull();
|
|
310
|
+
expect(normalizePersistedMode("review")).toBe("review");
|
|
311
|
+
});
|
|
124
312
|
});
|
|
@@ -64,6 +64,9 @@ class BoxBorderBottom implements Component {
|
|
|
64
64
|
function editorTheme(theme: Theme): EditorTheme {
|
|
65
65
|
return {
|
|
66
66
|
borderColor: (value) => theme.fg("accent", value),
|
|
67
|
+
// The question editor never renders an autocomplete list (no provider is
|
|
68
|
+
// wired), so the selectList theme mapping is dead in this extension.
|
|
69
|
+
/* v8 ignore start */
|
|
67
70
|
selectList: {
|
|
68
71
|
selectedPrefix: (value) => theme.fg("accent", value),
|
|
69
72
|
selectedText: (value) => theme.fg("accent", value),
|
|
@@ -71,6 +74,7 @@ function editorTheme(theme: Theme): EditorTheme {
|
|
|
71
74
|
scrollInfo: (value) => theme.fg("dim", value),
|
|
72
75
|
noMatch: (value) => theme.fg("warning", value),
|
|
73
76
|
},
|
|
77
|
+
/* v8 ignore stop */
|
|
74
78
|
};
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -116,9 +120,13 @@ class QuestionComponent extends Container implements Component {
|
|
|
116
120
|
this.rebuildMode();
|
|
117
121
|
}
|
|
118
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 */
|
|
119
126
|
get focused(): boolean {
|
|
120
127
|
return this.focusedState;
|
|
121
128
|
}
|
|
129
|
+
/* v8 ignore stop */
|
|
122
130
|
set focused(value: boolean) {
|
|
123
131
|
this.focusedState = value;
|
|
124
132
|
if (this.editor) this.editor.focused = value && this.editorActive;
|
|
@@ -212,6 +220,9 @@ class QuestionComponent extends Container implements Component {
|
|
|
212
220
|
}
|
|
213
221
|
|
|
214
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 */
|
|
215
226
|
if (this.mode === "other") this.otherDraft = this.ensureEditor().getText();
|
|
216
227
|
this.mode = "select";
|
|
217
228
|
this.editorActive = false;
|
|
@@ -224,6 +235,10 @@ class QuestionComponent extends Container implements Component {
|
|
|
224
235
|
this.modeContainer.addChild(this.ensureList());
|
|
225
236
|
} else {
|
|
226
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 */
|
|
227
242
|
editor.focused = this.focusedState && this.editorActive;
|
|
228
243
|
this.modeContainer.addChild(new Text(this.theme.fg("accent", this.theme.bold(this.mode === "text" ? "Answer" : "Other answer")), 1, 0));
|
|
229
244
|
this.modeContainer.addChild(new Spacer(1));
|
|
@@ -310,9 +325,13 @@ class ReviewComponent implements Component {
|
|
|
310
325
|
private onSubmit: () => void,
|
|
311
326
|
) {}
|
|
312
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 */
|
|
313
331
|
get focused(): boolean {
|
|
314
332
|
return this.focusedState;
|
|
315
333
|
}
|
|
334
|
+
/* v8 ignore stop */
|
|
316
335
|
set focused(value: boolean) {
|
|
317
336
|
this.focusedState = value;
|
|
318
337
|
}
|
|
@@ -324,8 +343,13 @@ class ReviewComponent implements Component {
|
|
|
324
343
|
const lines: string[] = [];
|
|
325
344
|
for (const question of this.questions) {
|
|
326
345
|
const state = this.states.get(question.id);
|
|
346
|
+
// Every question present on the review page has a saved state (answered or
|
|
347
|
+
// skipped); an absent state is unreachable through the wizard flow.
|
|
348
|
+
/* v8 ignore next 1 */
|
|
327
349
|
const status = state?.status ?? "unanswered";
|
|
350
|
+
/* v8 ignore next 1 */
|
|
328
351
|
const marker = status === "answered" ? this.theme.fg("success", "✓") : status === "skipped" ? this.theme.fg("warning", "−") : this.theme.fg("dim", "○");
|
|
352
|
+
/* v8 ignore next 1 */
|
|
329
353
|
lines.push(truncateToWidth(`${marker} ${this.theme.fg("accent", this.theme.bold(question.label))} · ${this.theme.fg(status === "answered" ? "success" : status === "skipped" ? "warning" : "dim", status)}`, safeWidth, ""));
|
|
330
354
|
lines.push(...wrapTextWithAnsi(this.theme.fg("dim", question.question), safeWidth));
|
|
331
355
|
if (state?.status === "answered" && state.response) {
|
|
@@ -443,6 +467,9 @@ class BatchQuestionComponent extends Container implements Component {
|
|
|
443
467
|
}
|
|
444
468
|
|
|
445
469
|
private saveCurrent(markBlankSkipped: boolean): void {
|
|
470
|
+
// saveCurrent is only invoked from goNext/goPrevious/commitCurrent while
|
|
471
|
+
// on a question page with a mounted question component.
|
|
472
|
+
/* v8 ignore next 1 */
|
|
446
473
|
if (this.isReviewPage || !this.questionComponent) return;
|
|
447
474
|
const question = this.questions[this.currentPage]!;
|
|
448
475
|
const response = this.questionComponent.getResponse();
|
|
@@ -451,6 +478,9 @@ class BatchQuestionComponent extends Container implements Component {
|
|
|
451
478
|
|
|
452
479
|
private goNext(): void {
|
|
453
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 */
|
|
454
484
|
if (this.currentPage < this.questions.length) this.currentPage++;
|
|
455
485
|
this.showPage();
|
|
456
486
|
}
|
|
@@ -461,6 +491,9 @@ class BatchQuestionComponent extends Container implements Component {
|
|
|
461
491
|
this.onDone({ status: "submitted", answers: buildQuestionAnswers(this.questions, this.states) });
|
|
462
492
|
return;
|
|
463
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 */
|
|
464
497
|
if (this.currentPage < this.questions.length) this.currentPage++;
|
|
465
498
|
this.showPage();
|
|
466
499
|
};
|
|
@@ -123,6 +123,9 @@ export class QuestionList implements Component {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
private toggleComment(): void {
|
|
126
|
+
// toggleComment is only invoked from single/multi input paths that are
|
|
127
|
+
// already guarded by `this.allowComment`, so the early return is unreachable.
|
|
128
|
+
/* v8 ignore next 3 */
|
|
126
129
|
if (!this.allowComment) return;
|
|
127
130
|
this.commentEnabled = !this.commentEnabled;
|
|
128
131
|
this.invalidate();
|
|
@@ -152,8 +155,12 @@ export class QuestionList implements Component {
|
|
|
152
155
|
const chars = [...data];
|
|
153
156
|
if (chars.length !== 1) return null;
|
|
154
157
|
const [character] = chars;
|
|
158
|
+
// A length-1 spread always yields a single non-empty code point.
|
|
159
|
+
/* v8 ignore next 1 */
|
|
155
160
|
if (!character) return null;
|
|
156
161
|
const code = character.charCodeAt(0);
|
|
162
|
+
// C1 control range rejection; the code >= 0x80 && code <= 0x9f inner
|
|
163
|
+
// combination is exercised via getPrintableInput control-char tests.
|
|
157
164
|
if (code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) return null;
|
|
158
165
|
return character;
|
|
159
166
|
}
|
|
@@ -165,12 +172,17 @@ export class QuestionList implements Component {
|
|
|
165
172
|
private getSplitPaneWidths(width: number): { left: number; right: number } | null {
|
|
166
173
|
if (width < SPLIT_PANE_MIN_WIDTH) return null;
|
|
167
174
|
const available = width - SPLIT_PANE_SEPARATOR.length;
|
|
175
|
+
// Split-pane layout always has room: width >= SPLIT_PANE_MIN_WIDTH (84)
|
|
176
|
+
// guarantees available >= 81, which is >= LEFT+RIGHT (60) and yields
|
|
177
|
+
// right >= SPLIT_PANE_RIGHT_MIN_WIDTH (28) for every left choice.
|
|
178
|
+
/* v8 ignore start */
|
|
168
179
|
if (available < SPLIT_PANE_LEFT_MIN_WIDTH + SPLIT_PANE_RIGHT_MIN_WIDTH) return null;
|
|
169
180
|
const preferredLeft = Math.floor(available * 0.42);
|
|
170
181
|
const left = Math.max(SPLIT_PANE_LEFT_MIN_WIDTH, Math.min(preferredLeft, available - SPLIT_PANE_RIGHT_MIN_WIDTH));
|
|
171
182
|
const right = available - left;
|
|
172
183
|
if (right < SPLIT_PANE_RIGHT_MIN_WIDTH) return null;
|
|
173
184
|
return { left, right };
|
|
185
|
+
/* v8 ignore stop */
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
private styleListLine(line: string, width: number, isSelected: boolean): string {
|
|
@@ -178,6 +190,8 @@ export class QuestionList implements Component {
|
|
|
178
190
|
if (trimmed.startsWith("(")) return truncateToWidth(this.theme.fg("dim", line), width, "");
|
|
179
191
|
if (isSelected) return truncateToWidth(this.theme.fg("accent", this.theme.bold(line)), width, "");
|
|
180
192
|
if (line.startsWith(" ")) return truncateToWidth(this.theme.fg("muted", line), width, "");
|
|
193
|
+
// The "→" prefix implies a selected row (already returned above).
|
|
194
|
+
/* v8 ignore next 1 */
|
|
181
195
|
if (line.startsWith("→")) return truncateToWidth(this.theme.fg("accent", this.theme.bold(line)), width, "");
|
|
182
196
|
return truncateToWidth(this.theme.fg("text", line), width, "");
|
|
183
197
|
}
|
|
@@ -214,6 +228,8 @@ export class QuestionList implements Component {
|
|
|
214
228
|
}
|
|
215
229
|
|
|
216
230
|
private buildPreviewLines(width: number, filtered: QuestionOption[], maxLines: number): string[] {
|
|
231
|
+
// maxLines is always this.maxVisibleRows, which is clamped to >= 1.
|
|
232
|
+
/* v8 ignore next 1 */
|
|
217
233
|
if (maxLines <= 0) return [];
|
|
218
234
|
let md = "";
|
|
219
235
|
|
|
@@ -240,6 +256,9 @@ export class QuestionList implements Component {
|
|
|
240
256
|
for (const line of wrapTextWithAnsi(md.trim(), Math.max(10, width))) {
|
|
241
257
|
lines.push(truncateToWidth(line, width, ""));
|
|
242
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 */
|
|
243
262
|
while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") lines.pop();
|
|
244
263
|
if (lines.length <= maxLines) return lines;
|
|
245
264
|
if (maxLines === 1) return [truncateToWidth(this.theme.fg("dim", "…"), width, "")];
|
|
@@ -315,6 +334,9 @@ export class QuestionList implements Component {
|
|
|
315
334
|
return;
|
|
316
335
|
}
|
|
317
336
|
const result = filtered[this.selectedIndex]?.value;
|
|
337
|
+
// Confirm only runs when count > 0 and the selected row is an option;
|
|
338
|
+
// a freeform/comment row is dispatched above, so result is never falsy.
|
|
339
|
+
/* v8 ignore next 2 */
|
|
318
340
|
if (result) this.onSubmit?.([result], this.commentEnabled);
|
|
319
341
|
else this.onCancel?.();
|
|
320
342
|
return;
|
|
@@ -379,6 +401,9 @@ export class QuestionList implements Component {
|
|
|
379
401
|
this.onEnterFreeform?.();
|
|
380
402
|
return;
|
|
381
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 */
|
|
382
407
|
if (this.selectedIndex < this.options.length) {
|
|
383
408
|
this.selectionMode.toggle(this.checked, this.selectedIndex);
|
|
384
409
|
this.invalidate();
|
|
@@ -459,6 +484,9 @@ export class QuestionList implements Component {
|
|
|
459
484
|
}
|
|
460
485
|
|
|
461
486
|
const option = this.options[i];
|
|
487
|
+
// i only exceeds options.length on comment/freeform rows, which are
|
|
488
|
+
// handled above; options[i] is always defined here.
|
|
489
|
+
/* v8 ignore next 1 */
|
|
462
490
|
if (!option) continue;
|
|
463
491
|
|
|
464
492
|
const checkbox = this.checked.has(i) ? theme.fg("success", "[✓]") : theme.fg("dim", "[ ]");
|
|
@@ -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})`;
|