gentle-pi 3.7.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/assets/agents/gentle-ai-explore.md +4 -4
- package/assets/agents/gentle-ai-verify.md +6 -4
- package/assets/agents/gentle-ai-worker.md +7 -9
- package/assets/orchestrator-delegation.md +40 -41
- package/assets/orchestrator-memory.md +1 -22
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator.md +12 -24
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/gentle-shell.mjs +254 -29
- package/docs/delegated-verification.md +26 -1
- package/docs/gentle-agents-activity.md +24 -0
- package/docs/gentle-shell.md +92 -28
- package/docs/native-authority-architecture.md +2 -2
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +112 -206
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +26 -0
- package/extensions/child-safety.ts +23 -0
- package/extensions/gentle-agents.ts +429 -262
- package/extensions/gentle-ai.ts +781 -683
- package/extensions/gentle-shell.ts +1375 -88
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +18 -7
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1419 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/quiet-tools.ts +179 -87
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +16 -12
- package/extensions/startup-banner.ts +60 -31
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-protocol.ts +60 -0
- package/lib/agents-runner.ts +96 -98
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/bounded-writer-admission.ts +147 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/codemode-renderer.ts +185 -0
- package/lib/command-palette-catalog.ts +3 -9
- package/lib/command-palette.ts +25 -14
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-launcher.ts +24 -13
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +227 -0
- package/lib/native-review-cli.ts +49 -108
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +67 -17
- package/lib/review-candidate-view.ts +112 -25
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +156 -11
- package/lib/review-sidebar-state.ts +223 -0
- package/lib/selection-engine.ts +515 -0
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-worktree-registry.ts +14 -2
- package/lib/shell-bar.ts +179 -24
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +2 -1
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +78 -14
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +23 -13
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +66 -10
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +379 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +108 -0
- package/lib/visual-customize-view.ts +330 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/yolo-session-policy.ts +240 -0
- package/package.json +20 -8
- package/runtime/gentle-shell-launcher.mjs +23 -12
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/native-review-cli.mjs +49 -108
- package/runtime/review-risk-assessment.mjs +154 -9
- package/scripts/build-runtime-modules.mjs +1 -0
- package/scripts/gentle-ai-installer.mjs +14 -13
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/run-test-suite.mjs +76 -0
- package/scripts/test-packed-runner.mjs +31 -14
- package/scripts/verify-package-files.mjs +11 -22
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +9 -15
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-profiles.test.ts +18 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +40 -0
- package/tests/agents-runner.test.ts +378 -89
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/bounded-writer-admission.test.ts +95 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/card-style-policy.test.ts +55 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-safety.test.ts +82 -0
- package/tests/codemode-rendering.test.ts +491 -0
- package/tests/command-palette.test.ts +39 -3
- package/tests/delegated-key-learnings-contract.test.ts +0 -76
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +170 -2
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +54 -0
- package/tests/gentle-agents.test.ts +1047 -252
- package/tests/gentle-ai-binary.test.ts +3 -3
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +68 -54
- package/tests/gentle-ai-renderer.test.ts +487 -8
- package/tests/gentle-ai.test.ts +288 -65
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-shell-bin.test.ts +651 -114
- package/tests/gentle-shell-launcher.test.ts +99 -52
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell.test.ts +3839 -187
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +80 -8
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +29 -19
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/nan-provider.test.ts +471 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +6 -120
- package/tests/native-review-parity-runtime.test.ts +100 -3
- package/tests/odd-integration.test.ts +67 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
- package/tests/odd-routing-contract.test.ts +86 -35
- package/tests/orchestrator-budget.test.ts +14 -39
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +252 -115
- package/tests/packed-runner-owned-path.test.ts +46 -0
- package/tests/persona-single-channel.test.ts +6 -6
- package/tests/provider-defect-handoff.test.ts +3 -11
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +409 -184
- package/tests/rdd-aware-verification-contract.test.ts +76 -1
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +176 -12
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +298 -0
- package/tests/review-contract-prompt.test.ts +108 -43
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-routing.test.ts +611 -5
- package/tests/review-controller-workspace-root.test.ts +163 -4
- package/tests/review-host-relay-routing.test.ts +338 -2
- package/tests/review-integration-v2-forward.test.ts +200 -0
- package/tests/review-ledger-contract.test.ts +10 -34
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +498 -6
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/run-test-suite.test.ts +124 -0
- package/tests/runtime-harness.mjs +145 -786
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/selection-engine.test.ts +421 -0
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-worktree-registry.test.ts +77 -0
- package/tests/shell-bar.test.ts +382 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +52 -0
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-sidebar-layout.test.ts +325 -21
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +87 -1
- package/tests/shell-usage-view.test.ts +27 -0
- package/tests/shell-usage.test.ts +73 -0
- package/tests/skill-registry.test.ts +50 -1
- package/tests/startup-banner.test.ts +130 -2
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/telemetry-trigger.test.ts +81 -20
- package/tests/theme-customization.test.ts +72 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +110 -0
- package/tests/visual-customize-view.test.ts +418 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/yolo-customize.test.ts +256 -0
- package/tests/yolo-mode-runtime.test.ts +161 -0
- package/tests/yolo-mode.test.ts +261 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
|
@@ -26,7 +26,7 @@ const event = (taskId = "local-task") => childEvent("local-session", taskId, lau
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
test("installed package definitions retain classification after the actual routing transform", () => {
|
|
29
|
-
//
|
|
29
|
+
// The installer copies packaged agent assets verbatim on first install.
|
|
30
30
|
// Isolate the real pure routing writer; do not run an installer or read user agents.
|
|
31
31
|
const source = readFileSync(new URL("../extensions/gentle-ai.ts", import.meta.url), "utf8");
|
|
32
32
|
const transform = source.match(/function updateFrontmatterRouting\([\s\S]*?\n\}/)?.[0];
|
|
@@ -34,17 +34,9 @@ test("installed package definitions retain classification after the actual routi
|
|
|
34
34
|
const route = runInNewContext(`(${stripTypeScriptTypes(transform)})`);
|
|
35
35
|
for (const entry of readdirSync(new URL("../assets/agents/", import.meta.url)).filter(file => file.endsWith(".md"))) {
|
|
36
36
|
const file = entry.slice(0, -3);
|
|
37
|
-
const className = file
|
|
38
|
-
: file.startsWith("gentle-ai-") ? file.slice("gentle-ai-".length) : file;
|
|
37
|
+
const className = file.startsWith("gentle-ai-") ? file.slice("gentle-ai-".length) : file;
|
|
39
38
|
const kind = parseAgentClass(className);
|
|
40
|
-
if (
|
|
41
|
-
assert.equal(kind, undefined, "remediation stays dark in the existing telemetry taxonomy");
|
|
42
|
-
const definition = parseAgentDefinition(readFileSync(new URL(`../assets/agents/${file}.md`, import.meta.url), "utf8"), file, "global");
|
|
43
|
-
assert.ok("instructions" in definition);
|
|
44
|
-
assert.equal(classifyBuiltinAgent(definition), "unknown");
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
assert.ok(kind, `${file}: telemetry class`);
|
|
39
|
+
if (!kind) continue; // Review/Judgment Day agents are not child telemetry classes.
|
|
48
40
|
const asset = new URL(`../assets/agents/${file}.md`, import.meta.url);
|
|
49
41
|
const content = readFileSync(asset, "utf8");
|
|
50
42
|
const copied = parseAgentDefinition(content, asset.pathname, "global");
|
|
@@ -54,7 +46,7 @@ test("installed package definitions retain classification after the actual routi
|
|
|
54
46
|
const parsed = parseAgentDefinition(route(content, entry), asset.pathname, "global");
|
|
55
47
|
assert.ok("instructions" in parsed);
|
|
56
48
|
assert.equal(classifyBuiltinAgent(parsed), kind, `${kind}: installed routing`);
|
|
57
|
-
const customizedClass =
|
|
49
|
+
const customizedClass = parseAgentClass(parsed.name) ?? "unknown";
|
|
58
50
|
assert.equal(classifyBuiltinAgent({ ...parsed, instructions: `${parsed.instructions}\nOverride` }), customizedClass);
|
|
59
51
|
assert.equal(classifyBuiltinAgent({ ...parsed, tools: ["different-tool"] }), customizedClass);
|
|
60
52
|
assert.equal(classifyBuiltinAgent({ ...parsed, description: "different description" }), customizedClass);
|
|
@@ -63,13 +55,13 @@ test("installed package definitions retain classification after the actual routi
|
|
|
63
55
|
}
|
|
64
56
|
});
|
|
65
57
|
|
|
66
|
-
test("packaged
|
|
67
|
-
const path = new URL("../assets/agents/
|
|
58
|
+
test("packaged named worker is encoded with its public telemetry class", () => {
|
|
59
|
+
const path = new URL("../assets/agents/gentle-ai-worker.md", import.meta.url);
|
|
68
60
|
const packaged = parseAgentDefinition(readFileSync(path, "utf8"), path.pathname, "global");
|
|
69
61
|
assert.ok("instructions" in packaged);
|
|
70
62
|
const selection = launchSelection(packaged, { provider: "openai", id: "gpt-4o" }, "high");
|
|
71
|
-
assert.equal(selection.agentClass, "
|
|
72
|
-
const completed = childEvent("local-session", "
|
|
63
|
+
assert.equal(selection.agentClass, "worker");
|
|
64
|
+
const completed = childEvent("local-session", "worker-task", selection, "completed", {
|
|
73
65
|
coverage: "final_assistant_messages_only", agentSettled: true, responses: [response()], droppedResponses: 0,
|
|
74
66
|
});
|
|
75
67
|
assert.ok(completed);
|
|
@@ -79,15 +71,16 @@ test("packaged sdd-proposal is encoded only as canonical sdd-propose", () => {
|
|
|
79
71
|
const snapshot = composition.snapshot();
|
|
80
72
|
const payload = encodeNativeRuntimeEvent(snapshot.responses, snapshot.launches);
|
|
81
73
|
assert.ok(payload);
|
|
82
|
-
assert.equal(JSON.parse(payload).rows[0].agent_class, "
|
|
83
|
-
assert.ok(!payload.includes("
|
|
74
|
+
assert.equal(JSON.parse(payload).rows[0].agent_class, "worker");
|
|
75
|
+
assert.ok(!payload.includes("gentle-ai-worker"));
|
|
84
76
|
});
|
|
85
77
|
|
|
86
|
-
test("
|
|
87
|
-
const path = new URL("../assets/agents/
|
|
78
|
+
test("prefixed packaged names require exact fingerprints and do not expose private names", () => {
|
|
79
|
+
const path = new URL("../assets/agents/gentle-ai-verify.md", import.meta.url);
|
|
88
80
|
const packaged = parseAgentDefinition(readFileSync(path, "utf8"), path.pathname, "global");
|
|
89
81
|
assert.ok("instructions" in packaged);
|
|
90
|
-
assert.equal(classifyBuiltinAgent(
|
|
82
|
+
assert.equal(classifyBuiltinAgent(packaged), "verify");
|
|
83
|
+
assert.equal(classifyBuiltinAgent({ ...packaged, instructions: "customized instructions" }), "unknown");
|
|
91
84
|
|
|
92
85
|
assert.equal(classifyBuiltinAgent(workerDefinition), "worker");
|
|
93
86
|
// This is a packaged frontmatter name, but it is absent from the telemetry
|
|
@@ -132,7 +125,7 @@ test("launch distribution and each observed combination remain independent and p
|
|
|
132
125
|
});
|
|
133
126
|
|
|
134
127
|
test("registered child launch identity survives catalog state and missing response evidence inherits selection", () => {
|
|
135
|
-
const path = new URL("../assets/agents/
|
|
128
|
+
const path = new URL("../assets/agents/gentle-ai-explore.md", import.meta.url);
|
|
136
129
|
const agent = parseAgentDefinition(readFileSync(path, "utf8"), path.pathname, "global");
|
|
137
130
|
assert.ok("instructions" in agent);
|
|
138
131
|
const selection = launchSelection(agent, { provider: "openai-codex", id: "gpt-5.6-terra" }, "high");
|
|
@@ -140,7 +133,7 @@ test("registered child launch identity survives catalog state and missing respon
|
|
|
140
133
|
usage: { input: 3, output: 2 } } }, { observeResponses: true })
|
|
141
134
|
.filter(event => event.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
142
135
|
assert.ok(observation?.type === TASK_EVENT.RESPONSE_OBSERVATION);
|
|
143
|
-
const completed = childEvent("local-session", "
|
|
136
|
+
const completed = childEvent("local-session", "explore-task", selection, "completed", {
|
|
144
137
|
coverage: "final_assistant_messages_only", agentSettled: true,
|
|
145
138
|
responses: Array(5).fill(observation.observation), droppedResponses: 0,
|
|
146
139
|
});
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { GentlePromptEditor } from "../extensions/gentle-shell.ts";
|
|
5
|
+
import { SelectionEngine } from "../lib/selection-engine.ts";
|
|
6
|
+
import { decodePrintableKey } from "../lib/pi-tui-keys.ts";
|
|
7
|
+
import { CURSOR_MARKER, visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
8
|
+
import { CARD_STYLE, cardStyle, setCardStyle } from "../lib/shell-card.ts";
|
|
9
|
+
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
10
|
+
|
|
11
|
+
// Native selection engine tests: drive a real CustomEditor through the
|
|
12
|
+
// SelectionEngine the same way GentlePromptEditor wires it — engine.handleInput
|
|
13
|
+
// in front, native dispatch back into the editor. Covers the ported contract:
|
|
14
|
+
// shift+home/end selection, replace-on-delete, alt+a select all, collapse on
|
|
15
|
+
// movement, zero-width no-op at the edge, highlight + hint rendering, and
|
|
16
|
+
// degraded passthrough.
|
|
17
|
+
|
|
18
|
+
type CtorParams = ConstructorParameters<typeof CustomEditor>;
|
|
19
|
+
|
|
20
|
+
function makeEditor(): CustomEditor {
|
|
21
|
+
const tui = { terminal: { rows: 30, columns: 100 }, requestRender: () => {} } as unknown as CtorParams[0];
|
|
22
|
+
const theme = { borderColor: (s: string) => s, selectList: {} } as unknown as CtorParams[1];
|
|
23
|
+
const kb = { matches: () => false } as unknown as CtorParams[2];
|
|
24
|
+
const editor = new CustomEditor(tui, theme, kb);
|
|
25
|
+
(editor as unknown as { focused: boolean }).focused = true;
|
|
26
|
+
return editor;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function floatEditor(bg?: (role: string, text: string) => string): GentlePromptEditor {
|
|
30
|
+
const tui = { terminal: { rows: 30, columns: 100 }, requestRender() {} } as unknown as CtorParams[0];
|
|
31
|
+
const identity = (text: string) => text;
|
|
32
|
+
const theme = { borderColor: identity, selectList: {
|
|
33
|
+
selectedPrefix: identity, selectedText: identity, description: identity, scrollInfo: identity, noMatch: identity,
|
|
34
|
+
} } as unknown as CtorParams[1];
|
|
35
|
+
const editor = new GentlePromptEditor(tui, theme, { matches: () => false } as unknown as CtorParams[2], {
|
|
36
|
+
fg: (_r, t) => t, bg, bold: identity, requestRender() {}, pending: () => false,
|
|
37
|
+
now: () => 0, doubleEscCancelEnabled: () => false, dispatchQueuedText() {},
|
|
38
|
+
});
|
|
39
|
+
editor.focused = true;
|
|
40
|
+
return editor;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const promptBg = (_role: string, text: string) => `\x1b[44m${text}\x1b[49m`;
|
|
44
|
+
const click = (x: number, y: number, width: number): TuiMouseEvent => ({
|
|
45
|
+
type: "click", button: "left", x, y, width, height: 30, screenX: x, screenY: y, shift: false, alt: false, ctrl: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("T2 float prompt native cursor, selection, replacement and mouse share geometry", () => {
|
|
49
|
+
const previous = cardStyle();
|
|
50
|
+
setCardStyle(CARD_STYLE.FLOAT);
|
|
51
|
+
const editor = floatEditor(promptBg);
|
|
52
|
+
try {
|
|
53
|
+
editor.borderColor = (text) => `\x1b[35m${text}\x1b[39m`;
|
|
54
|
+
editor.setText("abcd");
|
|
55
|
+
editor.handleInput("\x1b[D");
|
|
56
|
+
let rows = editor.render(80);
|
|
57
|
+
assert.match(rows[0], /\x1b\[35m▎\x1b\[39m/);
|
|
58
|
+
assert.equal(visibleWidth(rows[1].split(CURSOR_MARKER)[0]), 6);
|
|
59
|
+
assert.ok(rows[1].includes(`${CURSOR_MARKER}\x1b[7md\x1b[0m\x1b[44m`));
|
|
60
|
+
editor.handleInput("\x1b[1;2H");
|
|
61
|
+
rows = editor.render(80);
|
|
62
|
+
assert.equal(stripAnsi(rows[1].replaceAll(CURSOR_MARKER, "")).trim(), "▎ abcd");
|
|
63
|
+
assert.ok(rows[1].includes(`${CURSOR_MARKER}\x1b[7ma\x1b[0m\x1b[44m\x1b[7mbc\x1b[27md`), "native selection inversion survives the software cursor reset");
|
|
64
|
+
assert.match(rows[2], /3 chars selected/);
|
|
65
|
+
assert.doesNotMatch(rows[2], /╯/);
|
|
66
|
+
assert.ok(rows[2].includes("\x1b[44m"));
|
|
67
|
+
editor.handleInput("X");
|
|
68
|
+
assert.equal(editor.getText(), "Xd");
|
|
69
|
+
editor.setText("abcd");
|
|
70
|
+
editor.render(30);
|
|
71
|
+
editor.handleMouse(click(4, 1, 30));
|
|
72
|
+
assert.deepEqual(editor.getCursor(), { line: 0, col: 1 });
|
|
73
|
+
editor.handleMouse(click(10, 0, 30));
|
|
74
|
+
assert.deepEqual(editor.getCursor(), { line: 0, col: 1 }, "padding row is inert");
|
|
75
|
+
editor.handleMouse(click(10, 2, 30));
|
|
76
|
+
assert.deepEqual(editor.getCursor(), { line: 0, col: 1 }, "hint row is inert");
|
|
77
|
+
editor.borderColor = (text) => `\x1b[36m${text}\x1b[39m`;
|
|
78
|
+
assert.match(editor.render(30)[1], /\x1b\[36m▎\x1b\[39m/, "accent reads the current public mode callback");
|
|
79
|
+
assert.equal(editor.handleMouse({ ...click(3, 1, 30), type: "press" }), undefined, "native drag selection remains unhandled");
|
|
80
|
+
editor.setText("abcdefghijklmnop");
|
|
81
|
+
const wrapped = editor.render(12);
|
|
82
|
+
assert.equal(wrapped.length, 5, "native layout width=6 wraps into three content rows");
|
|
83
|
+
editor.handleMouse(click(5, 2, 12));
|
|
84
|
+
assert.deepEqual(editor.getCursor(), { line: 0, col: 8 });
|
|
85
|
+
editor.handleInput("\x1ba");
|
|
86
|
+
assert.ok(editor.render(10).slice(1, -1).every((row) => row.includes("\x1b[7m")), "selection paints every wrapped content row");
|
|
87
|
+
} finally { editor.dispose(); setCardStyle(previous); }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("T2 float prompt actual native completions retain start row, height and click application", async () => {
|
|
91
|
+
const previous = cardStyle();
|
|
92
|
+
setCardStyle(CARD_STYLE.FLOAT);
|
|
93
|
+
const editor = floatEditor(promptBg);
|
|
94
|
+
editor.setAutocompleteProvider({
|
|
95
|
+
async getSuggestions() { return { prefix: "/", items: [{ value: "/alpha", label: "alpha" }, { value: "/beta", label: "beta" }] }; },
|
|
96
|
+
applyCompletion(_lines, _line, _col, item) { return { lines: [item.value], cursorLine: 0, cursorCol: item.value.length }; },
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
editor.handleInput("/");
|
|
100
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
101
|
+
assert.equal(editor.isShowingAutocomplete(), true);
|
|
102
|
+
const rows = editor.render(30);
|
|
103
|
+
assert.equal(rows.length, 5);
|
|
104
|
+
assert.match(stripAnsi(rows[3]), /^ ▎ → alpha/);
|
|
105
|
+
assert.match(stripAnsi(rows[4]), /^ ▎ beta/);
|
|
106
|
+
assert.ok(rows.every((row) => visibleWidth(row) === 30));
|
|
107
|
+
assert.equal(visibleWidth(rows[1].split(CURSOR_MARKER)[0]), 4);
|
|
108
|
+
assert.equal(editor.handleMouse(click(4, 4, 30))?.handled, true);
|
|
109
|
+
assert.equal(editor.getText(), "/beta");
|
|
110
|
+
assert.equal(editor.isShowingAutocomplete(), false);
|
|
111
|
+
} finally { editor.dispose(); setCardStyle(previous); }
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("T2 float prompt actual editor fallback and neon bytes remain exact at all widths", () => {
|
|
115
|
+
const previous = cardStyle();
|
|
116
|
+
const editor = floatEditor(promptBg);
|
|
117
|
+
const fallback = floatEditor((_role, text) => text);
|
|
118
|
+
try {
|
|
119
|
+
for (const width of [0, 1, 2, 9, 10, 30, 80]) {
|
|
120
|
+
// Pi 0.99.2 wordWrapLine recurses on a wide glyph at layoutWidth=1.
|
|
121
|
+
const text = width < 10 ? "abcd" : "雪abcd";
|
|
122
|
+
editor.setText(text); fallback.setText(text);
|
|
123
|
+
setCardStyle(CARD_STYLE.NEON);
|
|
124
|
+
const neon = editor.render(width);
|
|
125
|
+
setCardStyle(CARD_STYLE.FLOAT);
|
|
126
|
+
assert.deepEqual(fallback.render(width), neon);
|
|
127
|
+
const rows = editor.render(width);
|
|
128
|
+
if (width < 10) assert.deepEqual(rows, neon);
|
|
129
|
+
else assert.match(stripAnsi(rows[0]), /^ ▎ ✿/);
|
|
130
|
+
assert.ok(rows.every((row) => visibleWidth(row) <= width));
|
|
131
|
+
}
|
|
132
|
+
} finally { editor.dispose(); fallback.dispose(); setCardStyle(previous); }
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const END = "\x1b[F";
|
|
136
|
+
const HOME = "\x1b[H";
|
|
137
|
+
const SHIFT_HOME = "\x1b[1;2H";
|
|
138
|
+
const SHIFT_END = "\x1b[1;2F";
|
|
139
|
+
const RIGHT = "\x1b[C";
|
|
140
|
+
const DEL = "\x1b[3~";
|
|
141
|
+
const BACKSPACE = "\x7f";
|
|
142
|
+
const ALT_A = "\x1ba";
|
|
143
|
+
|
|
144
|
+
function cursorOf(editor: CustomEditor): { line: number; col: number } {
|
|
145
|
+
return (editor as unknown as { getCursor(): { line: number; col: number } }).getCursor();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
test("shift+home selects to line start; delete replaces the selection atomically", () => {
|
|
149
|
+
const editor = makeEditor();
|
|
150
|
+
const engine = new SelectionEngine(editor);
|
|
151
|
+
const native = (d: string) => editor.handleInput(d);
|
|
152
|
+
editor.setText("hello world");
|
|
153
|
+
editor.handleInput(END);
|
|
154
|
+
engine.handleInput(SHIFT_HOME, native);
|
|
155
|
+
assert.equal(cursorOf(editor).col, 0);
|
|
156
|
+
engine.handleInput(DEL, native);
|
|
157
|
+
assert.equal(editor.getText(), "");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("alt+a selects all; backspace replaces the whole text", () => {
|
|
161
|
+
const editor = makeEditor();
|
|
162
|
+
const engine = new SelectionEngine(editor);
|
|
163
|
+
const native = (d: string) => editor.handleInput(d);
|
|
164
|
+
editor.setText("abc\ndef");
|
|
165
|
+
engine.handleInput(ALT_A, native);
|
|
166
|
+
engine.handleInput(BACKSPACE, native);
|
|
167
|
+
assert.equal(editor.getText(), "");
|
|
168
|
+
assert.equal(cursorOf(editor).line, 0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("movement collapses the selection; later delete behaves natively", () => {
|
|
172
|
+
const editor = makeEditor();
|
|
173
|
+
const engine = new SelectionEngine(editor);
|
|
174
|
+
const native = (d: string) => editor.handleInput(d);
|
|
175
|
+
editor.setText("hello");
|
|
176
|
+
editor.handleInput(END);
|
|
177
|
+
engine.handleInput(SHIFT_HOME, native);
|
|
178
|
+
engine.handleInput(RIGHT, native);
|
|
179
|
+
engine.handleInput(DEL, native);
|
|
180
|
+
assert.equal(editor.getText(), "hllo");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("shift+end at line end: zero-width selection, delete is a no-op", () => {
|
|
184
|
+
const editor = makeEditor();
|
|
185
|
+
const engine = new SelectionEngine(editor);
|
|
186
|
+
const native = (d: string) => editor.handleInput(d);
|
|
187
|
+
editor.setText("hi");
|
|
188
|
+
editor.handleInput(END);
|
|
189
|
+
engine.handleInput(SHIFT_END, native);
|
|
190
|
+
engine.handleInput(DEL, native);
|
|
191
|
+
assert.equal(editor.getText(), "hi");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("render wraps the selected span in reverse video and shows the hint", () => {
|
|
195
|
+
const tui = { terminal: { rows: 30, columns: 100 }, requestRender: () => {} } as unknown as CtorParams[0];
|
|
196
|
+
const theme = { borderColor: (s: string) => s, selectList: {} } as unknown as CtorParams[1];
|
|
197
|
+
const kb = { matches: () => false } as unknown as CtorParams[2];
|
|
198
|
+
const editor = new GentlePromptEditor(tui, theme, kb, {
|
|
199
|
+
fg: (_color, text) => text,
|
|
200
|
+
bold: (text) => text,
|
|
201
|
+
requestRender: () => {},
|
|
202
|
+
pending: () => false,
|
|
203
|
+
now: () => Date.now(),
|
|
204
|
+
doubleEscCancelEnabled: () => false,
|
|
205
|
+
dispatchQueuedText: () => {},
|
|
206
|
+
});
|
|
207
|
+
(editor as unknown as { focused: boolean }).focused = true;
|
|
208
|
+
const engine = (editor as unknown as { selectionEngine: SelectionEngine }).selectionEngine;
|
|
209
|
+
const native = (d: string) => editor.handleInput(d);
|
|
210
|
+
editor.setText("hello world");
|
|
211
|
+
editor.handleInput(END);
|
|
212
|
+
engine.handleInput(SHIFT_HOME, native);
|
|
213
|
+
const rows = editor.render(80);
|
|
214
|
+
const content = rows[1] ?? "";
|
|
215
|
+
assert.ok(content.includes("\x1b[7m"), "reverse-video span missing");
|
|
216
|
+
const last = rows[rows.length - 1] ?? "";
|
|
217
|
+
assert.ok(last.includes("chars selected"), "selection hint missing on the bottom rule");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("degraded host: pure passthrough, no selection behavior", () => {
|
|
221
|
+
const minimal = {
|
|
222
|
+
getText: () => "",
|
|
223
|
+
setText: (_text: string) => {},
|
|
224
|
+
handleInput: (_data: string) => {},
|
|
225
|
+
render: (_width: number) => [] as string[],
|
|
226
|
+
invalidate: () => {},
|
|
227
|
+
} as unknown as CustomEditor;
|
|
228
|
+
const engine = new SelectionEngine(minimal);
|
|
229
|
+
assert.equal(engine.degraded, true);
|
|
230
|
+
let nativeSeen = "";
|
|
231
|
+
engine.handleInput(SHIFT_HOME, (d) => {
|
|
232
|
+
nativeSeen = d;
|
|
233
|
+
});
|
|
234
|
+
assert.equal(nativeSeen, SHIFT_HOME);
|
|
235
|
+
assert.equal(engine.anchor, null);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// --- Focused terminal-key decode + undo-transaction coverage (review round 2):
|
|
239
|
+
// the extension-era suites were replaced by the native engine, so these pin the
|
|
240
|
+
// contracts the review explicitly named: kitty press/repeat/release filtering,
|
|
241
|
+
// CSI-u DEL/C1 delete-only replacement, and exactly-one-undo replacement.
|
|
242
|
+
|
|
243
|
+
const KITTY_A_PRESS = "\x1b[97;1:1u"; // 'a', kitty flag 2, press
|
|
244
|
+
const KITTY_A_RELEASE = "\x1b[97;1:3u"; // 'a', kitty flag 2, release
|
|
245
|
+
|
|
246
|
+
test("kitty CSI-u press replaces the selection; release is dropped and keeps it", () => {
|
|
247
|
+
const editor = makeEditor();
|
|
248
|
+
const engine = new SelectionEngine(editor);
|
|
249
|
+
const native = (d: string) => editor.handleInput(d);
|
|
250
|
+
editor.setText("hello");
|
|
251
|
+
editor.handleInput(END);
|
|
252
|
+
engine.handleInput(SHIFT_HOME, native);
|
|
253
|
+
engine.handleInput(KITTY_A_RELEASE, native);
|
|
254
|
+
assert.equal(editor.getText(), "hello");
|
|
255
|
+
assert.deepEqual(engine.anchor, { line: 0, col: 5 });
|
|
256
|
+
engine.handleInput(KITTY_A_PRESS, native);
|
|
257
|
+
assert.equal(editor.getText(), "a");
|
|
258
|
+
assert.deepEqual(cursorOf(editor), { line: 0, col: 1 });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("repeated shift+home at the line edge keeps the selection", () => {
|
|
262
|
+
const editor = makeEditor();
|
|
263
|
+
const engine = new SelectionEngine(editor);
|
|
264
|
+
const native = (d: string) => editor.handleInput(d);
|
|
265
|
+
editor.setText("hello");
|
|
266
|
+
editor.handleInput(END);
|
|
267
|
+
engine.handleInput(SHIFT_HOME, native);
|
|
268
|
+
engine.handleInput(SHIFT_HOME, native);
|
|
269
|
+
assert.notEqual(engine.anchor, null);
|
|
270
|
+
assert.equal(engine.selectionLength(), 5);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("modifyOtherKeys: printable decodes; ctrl-modified and control codepoints are rejected", () => {
|
|
274
|
+
assert.equal(decodePrintableKey("\x1b[27;1;97~"), "a");
|
|
275
|
+
assert.equal(decodePrintableKey("\x1b[27;5;97~"), undefined);
|
|
276
|
+
assert.equal(decodePrintableKey("\x1b[27;1;27~"), undefined);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("CSI-u DEL and C1 codepoints replace the selection with a pure delete", () => {
|
|
280
|
+
for (const data of ["\x1b[27;1;127~", "\x1b[27;1;155~"]) {
|
|
281
|
+
const editor = makeEditor();
|
|
282
|
+
const engine = new SelectionEngine(editor);
|
|
283
|
+
const native = (d: string) => editor.handleInput(d);
|
|
284
|
+
editor.setText("hi");
|
|
285
|
+
editor.handleInput(END);
|
|
286
|
+
engine.handleInput(SHIFT_HOME, native);
|
|
287
|
+
engine.handleInput(data, native);
|
|
288
|
+
assert.equal(editor.getText(), "", `control byte from ${JSON.stringify(data)} must not be inserted`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("bracketed paste replaces a multiline interior selection and restores it with one undo", () => {
|
|
293
|
+
const editor = makeEditor();
|
|
294
|
+
const engine = new SelectionEngine(editor);
|
|
295
|
+
const native = (d: string) => editor.handleInput(d);
|
|
296
|
+
editor.setText("prefix old\ntext suffix");
|
|
297
|
+
editor.handleInput(HOME);
|
|
298
|
+
engine.handleInput(SHIFT_END, native);
|
|
299
|
+
engine.handleInput("\x1b[200~new\nlines\x1b[201~", native);
|
|
300
|
+
assert.equal(editor.getText(), "prefix old\nnew\nlines");
|
|
301
|
+
(editor as unknown as { undo(): void }).undo();
|
|
302
|
+
assert.equal(editor.getText(), "prefix old\ntext suffix");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("printable replacement is exactly one undo transaction", () => {
|
|
306
|
+
const editor = makeEditor();
|
|
307
|
+
const engine = new SelectionEngine(editor);
|
|
308
|
+
const native = (d: string) => editor.handleInput(d);
|
|
309
|
+
editor.setText("hello world");
|
|
310
|
+
editor.handleInput(END);
|
|
311
|
+
engine.handleInput(SHIFT_HOME, native);
|
|
312
|
+
const internals = editor as unknown as { pushUndoSnapshot(): void };
|
|
313
|
+
const original = internals.pushUndoSnapshot.bind(editor);
|
|
314
|
+
let snapshots = 0;
|
|
315
|
+
internals.pushUndoSnapshot = () => {
|
|
316
|
+
snapshots += 1;
|
|
317
|
+
original();
|
|
318
|
+
};
|
|
319
|
+
engine.handleInput("x", native);
|
|
320
|
+
assert.equal(editor.getText(), "x");
|
|
321
|
+
assert.equal(snapshots, 1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const PASTE_START = "\x1b[200~";
|
|
325
|
+
const PASTE_END = "\x1b[201~";
|
|
326
|
+
const MAX_INCOMPLETE_PASTE_BYTES = 16 * 1024 * 1024;
|
|
327
|
+
|
|
328
|
+
test("bracketed paste terminator split across six single-byte chunks still completes the paste", () => {
|
|
329
|
+
const editor = makeEditor();
|
|
330
|
+
const engine = new SelectionEngine(editor);
|
|
331
|
+
const native = (d: string) => editor.handleInput(d);
|
|
332
|
+
editor.setText("prefix old\ntext suffix");
|
|
333
|
+
editor.handleInput(HOME);
|
|
334
|
+
engine.handleInput(SHIFT_END, native);
|
|
335
|
+
engine.handleInput(PASTE_START, native);
|
|
336
|
+
engine.handleInput("new\nlines", native);
|
|
337
|
+
for (const byte of PASTE_END) engine.handleInput(byte, native);
|
|
338
|
+
assert.equal(editor.getText(), "prefix old\nnew\nlines");
|
|
339
|
+
(editor as unknown as { undo(): void }).undo();
|
|
340
|
+
assert.equal(editor.getText(), "prefix old\ntext suffix");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("an incomplete bracketed paste that crosses the 16 MiB bound without a terminator is abandoned, never forwarded to native", () => {
|
|
344
|
+
const editor = makeEditor();
|
|
345
|
+
const engine = new SelectionEngine(editor);
|
|
346
|
+
const native = (d: string) => editor.handleInput(d);
|
|
347
|
+
editor.setText("hello");
|
|
348
|
+
engine.handleInput(ALT_A, native);
|
|
349
|
+
engine.handleInput(PASTE_START, native);
|
|
350
|
+
const payload = "z".repeat(MAX_INCOMPLETE_PASTE_BYTES - PASTE_START.length + 1);
|
|
351
|
+
engine.handleInput(payload, native);
|
|
352
|
+
assert.equal(editor.getText(), "hello", "no partial paste bytes are ever forwarded to the native editor");
|
|
353
|
+
engine.handleInput("x", native);
|
|
354
|
+
assert.equal(editor.getText(), "x", "input after abandonment behaves like a fresh keystroke over the still-active selection");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("the 16 MiB incomplete-frame bound counts UTF-8 bytes, not UTF-16 code units", () => {
|
|
358
|
+
const editor = makeEditor();
|
|
359
|
+
const engine = new SelectionEngine(editor);
|
|
360
|
+
const native = (d: string) => editor.handleInput(d);
|
|
361
|
+
editor.setText("hello");
|
|
362
|
+
engine.handleInput(ALT_A, native);
|
|
363
|
+
engine.handleInput(PASTE_START, native);
|
|
364
|
+
// "é" is one UTF-16 code unit but two UTF-8 bytes: 10M code units is under
|
|
365
|
+
// the 16 MiB code-unit count but 20 MB in UTF-8 bytes, over the bound.
|
|
366
|
+
const payload = "é".repeat(10 * 1024 * 1024);
|
|
367
|
+
engine.handleInput(payload, native);
|
|
368
|
+
assert.equal(editor.getText(), "hello", "a multibyte payload under 16M code units but over 16 MiB in UTF-8 bytes must still be abandoned");
|
|
369
|
+
engine.handleInput("x", native);
|
|
370
|
+
assert.equal(editor.getText(), "x");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("many-chunk bracketed paste never rescans the full accumulated buffer for the terminator", () => {
|
|
374
|
+
const editor = makeEditor();
|
|
375
|
+
const engine = new SelectionEngine(editor);
|
|
376
|
+
const native = (d: string) => editor.handleInput(d);
|
|
377
|
+
editor.setText("hello");
|
|
378
|
+
engine.handleInput(ALT_A, native);
|
|
379
|
+
const chunkCount = 1500; // > 1000 chars so the host editor collapses it into its own marker
|
|
380
|
+
// Deterministic regression check (avoids flaky wall-clock timing on slow/CI
|
|
381
|
+
// hardware): instrument String.prototype.indexOf while chunks are buffered
|
|
382
|
+
// and record the length of every string searched for PASTE_END. A
|
|
383
|
+
// full-buffer rescan (the pre-fix O(n^2) bug) would search a string that
|
|
384
|
+
// grows with every chunk; the tail-bounded scan only ever searches
|
|
385
|
+
// `pasteTail + <this chunk>`, so its length stays bounded regardless of how
|
|
386
|
+
// many chunks were already buffered.
|
|
387
|
+
const originalIndexOf = String.prototype.indexOf;
|
|
388
|
+
const searchedLengths: number[] = [];
|
|
389
|
+
// biome-ignore lint: intentional prototype patch, restored in `finally` below.
|
|
390
|
+
String.prototype.indexOf = function (this: string, searchString: string, position?: number): number {
|
|
391
|
+
if (searchString === PASTE_END) searchedLengths.push(this.length);
|
|
392
|
+
return originalIndexOf.call(this, searchString, position);
|
|
393
|
+
};
|
|
394
|
+
try {
|
|
395
|
+
engine.handleInput(PASTE_START, native);
|
|
396
|
+
for (let i = 0; i < chunkCount; i++) engine.handleInput("z", native);
|
|
397
|
+
engine.handleInput(PASTE_END, native);
|
|
398
|
+
} finally {
|
|
399
|
+
String.prototype.indexOf = originalIndexOf;
|
|
400
|
+
}
|
|
401
|
+
assert.match(editor.getText(), /^\[paste #1 \d+ chars\]$/, "the oversized paste still completed (collapsed into the host editor's own marker)");
|
|
402
|
+
// Bound: PASTE_END tail carry (5 chars) + the largest single chunk sent
|
|
403
|
+
// here (the 6-char PASTE_END chunk itself) = 11 chars, never the ~1500+
|
|
404
|
+
// char accumulated buffer a full-buffer rescan would search. The final
|
|
405
|
+
// `native(frame)` call hands the fully assembled frame to the underlying
|
|
406
|
+
// host editor exactly once, and that editor's own paste handling does its
|
|
407
|
+
// own one-time scan over the assembled length — a single unavoidable O(final
|
|
408
|
+
// length) cost outside SelectionEngine, not a per-chunk rescan; exactly one
|
|
409
|
+
// such large search is tolerated below.
|
|
410
|
+
const smallSearches = searchedLengths.filter((len) => len <= 11);
|
|
411
|
+
const largeSearches = searchedLengths.filter((len) => len > 11);
|
|
412
|
+
assert.equal(
|
|
413
|
+
smallSearches.length,
|
|
414
|
+
chunkCount + 2,
|
|
415
|
+
`expected exactly one tail-bounded terminator search per input chunk (start + ${chunkCount} z's + end), saw ${smallSearches.length}`,
|
|
416
|
+
);
|
|
417
|
+
assert.ok(
|
|
418
|
+
largeSearches.length <= 1,
|
|
419
|
+
`expected at most one full-frame scan (the host editor's own one-time assembled-paste handling), saw ${largeSearches.length} large searches (lengths: ${largeSearches.join(", ")}) — a full-buffer rescan during chunk intake regressed`,
|
|
420
|
+
);
|
|
421
|
+
});
|