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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import promptHistoryExtension from "../extensions/history/index.ts";
|
|
7
|
+
import { writeHistoryCapturePolicy } from "../lib/history-capture-policy.ts";
|
|
8
|
+
|
|
9
|
+
// The module-level selector gate reads process.env directly (that path has
|
|
10
|
+
// no deps.env injection); keep the suite hermetic regardless of the ambient
|
|
11
|
+
// shell so the off-path assertions cannot be flipped by the environment.
|
|
12
|
+
delete process.env.GENTLE_PI_HISTORY_CAPTURE;
|
|
13
|
+
|
|
14
|
+
function makeRoot(): string {
|
|
15
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-off-"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CWD = "/pi-history-test/project-off";
|
|
19
|
+
|
|
20
|
+
interface Harness {
|
|
21
|
+
commandHandler: (args: unknown, ctx: unknown) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load the extension against a temp root and capture the registered
|
|
26
|
+
* shortcut + history command handlers from the fake pi.
|
|
27
|
+
*/
|
|
28
|
+
function loadWithCommand(
|
|
29
|
+
env: NodeJS.ProcessEnv,
|
|
30
|
+
root: string,
|
|
31
|
+
configHome: string = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-off-config-")),
|
|
32
|
+
): Harness {
|
|
33
|
+
const shortcuts: Array<[string, { handler: unknown }]> = [];
|
|
34
|
+
const commands: Array<[string, { handler: unknown }]> = [];
|
|
35
|
+
const pi = {
|
|
36
|
+
on: () => {},
|
|
37
|
+
registerShortcut: (key: string, def: { handler: unknown }) => {
|
|
38
|
+
shortcuts.push([key, def]);
|
|
39
|
+
},
|
|
40
|
+
registerCommand: (name: string, def: { handler: unknown }) => {
|
|
41
|
+
commands.push([name, def]);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
promptHistoryExtension(pi as never, {
|
|
45
|
+
env,
|
|
46
|
+
root,
|
|
47
|
+
cwd: CWD,
|
|
48
|
+
instanceId: "inst-off",
|
|
49
|
+
now: () => 1700000000000,
|
|
50
|
+
// Keep any opted-in warm-up away from the real ~/.pi/agent.
|
|
51
|
+
agentDir: path.join(root, "agent"),
|
|
52
|
+
sessionsRoot: path.join(root, "sessions"),
|
|
53
|
+
// An empty config home by default: the Customize preference is unset (off).
|
|
54
|
+
gentlePiConfigHome: configHome,
|
|
55
|
+
});
|
|
56
|
+
const command = commands.find(([name]) => name === "history");
|
|
57
|
+
assert.ok(command, "the history command must be registered");
|
|
58
|
+
assert.equal(shortcuts.length, 1, "the shortcut must still be registered");
|
|
59
|
+
return {
|
|
60
|
+
commandHandler: command[1].handler as Harness["commandHandler"],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fakeCtx(notifyCalls: Array<[string, string]>) {
|
|
65
|
+
return {
|
|
66
|
+
ui: {
|
|
67
|
+
notify: (message: string, level: string) => {
|
|
68
|
+
notifyCalls.push([message, level]);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The open flow reads the injected deps (env/root/cwd), never the module
|
|
75
|
+
// defaults, so the enabled direction is testable against a temp root too.
|
|
76
|
+
|
|
77
|
+
test("with capture disabled, extension load writes nothing", async () => {
|
|
78
|
+
const root = makeRoot();
|
|
79
|
+
loadWithCommand({}, root);
|
|
80
|
+
// Flush the setImmediate warm-up.
|
|
81
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
82
|
+
// Nothing at all: no registry, no seed, no store file.
|
|
83
|
+
assert.deepEqual(fs.readdirSync(root), []);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("with capture disabled, the history command imports nothing and warns", async () => {
|
|
87
|
+
const root = makeRoot();
|
|
88
|
+
const { commandHandler } = loadWithCommand({}, root);
|
|
89
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
90
|
+
const notifyCalls: Array<[string, string]> = [];
|
|
91
|
+
await commandHandler([], fakeCtx(notifyCalls));
|
|
92
|
+
assert.equal(notifyCalls.length, 1);
|
|
93
|
+
assert.equal(notifyCalls[0][1], "warning");
|
|
94
|
+
assert.ok(
|
|
95
|
+
notifyCalls[0][0].includes("GENTLE_PI_HISTORY_CAPTURE"),
|
|
96
|
+
`the warning must name the switch, got: ${notifyCalls[0][0]}`,
|
|
97
|
+
);
|
|
98
|
+
assert.ok(
|
|
99
|
+
notifyCalls[0][0].includes("Gentle → Customize"),
|
|
100
|
+
`the warning must name the Customize control, got: ${notifyCalls[0][0]}`,
|
|
101
|
+
);
|
|
102
|
+
// The gate must fire before the drain: no migration, no seed, no store.
|
|
103
|
+
assert.deepEqual(fs.readdirSync(root), []);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("with capture enabled, opening the selector reads without initializing the store", async () => {
|
|
107
|
+
const root = makeRoot();
|
|
108
|
+
const { commandHandler } = loadWithCommand({ GENTLE_PI_HISTORY_CAPTURE: "1" }, root);
|
|
109
|
+
// Open before the opted-in warm-up tick: the open flow alone must not
|
|
110
|
+
// migrate, register, seed, or create a capture file.
|
|
111
|
+
const notifyCalls: Array<[string, string]> = [];
|
|
112
|
+
await commandHandler([], fakeCtx(notifyCalls));
|
|
113
|
+
assert.deepEqual(notifyCalls, [["No prompt history available.", "warning"]]);
|
|
114
|
+
assert.deepEqual(fs.readdirSync(root), []);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("the Customize preference opens the selector without the env switch", async () => {
|
|
118
|
+
const root = makeRoot();
|
|
119
|
+
const configHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-off-config-"));
|
|
120
|
+
const { commandHandler } = loadWithCommand({}, root, configHome);
|
|
121
|
+
const notifyCalls: Array<[string, string]> = [];
|
|
122
|
+
await commandHandler([], fakeCtx(notifyCalls));
|
|
123
|
+
assert.equal(notifyCalls[0][1], "warning");
|
|
124
|
+
assert.match(notifyCalls[0][0], /disabled/);
|
|
125
|
+
// The same loaded extension honors a later toggle without restart.
|
|
126
|
+
writeHistoryCapturePolicy("on", { gentlePiConfigHome: configHome });
|
|
127
|
+
await commandHandler([], fakeCtx(notifyCalls));
|
|
128
|
+
assert.deepEqual(notifyCalls[1], ["No prompt history available.", "warning"]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("an explicit env off names the override instead of the Customize fix", async () => {
|
|
132
|
+
const root = makeRoot();
|
|
133
|
+
const configHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-off-config-"));
|
|
134
|
+
writeHistoryCapturePolicy("on", { gentlePiConfigHome: configHome });
|
|
135
|
+
const { commandHandler } = loadWithCommand({ GENTLE_PI_HISTORY_CAPTURE: "false" }, root, configHome);
|
|
136
|
+
const notifyCalls: Array<[string, string]> = [];
|
|
137
|
+
await commandHandler([], fakeCtx(notifyCalls));
|
|
138
|
+
assert.equal(notifyCalls.length, 1);
|
|
139
|
+
assert.match(notifyCalls[0][0], /disabled by GENTLE_PI_HISTORY_CAPTURE, which overrides the Gentle → Customize → History preference/);
|
|
140
|
+
assert.deepEqual(fs.readdirSync(root), []);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a malformed Customize preference is reported as invalid, not just off", async () => {
|
|
144
|
+
const root = makeRoot();
|
|
145
|
+
const configHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-off-config-"));
|
|
146
|
+
const preference = path.join(configHome, "history-capture.json");
|
|
147
|
+
fs.writeFileSync(preference, "{not json", "utf8");
|
|
148
|
+
const { commandHandler } = loadWithCommand({}, root, configHome);
|
|
149
|
+
const notifyCalls: Array<[string, string]> = [];
|
|
150
|
+
await commandHandler([], fakeCtx(notifyCalls));
|
|
151
|
+
assert.equal(notifyCalls.length, 1);
|
|
152
|
+
assert.equal(notifyCalls[0][1], "warning");
|
|
153
|
+
assert.match(notifyCalls[0][0], /Gentle → Customize → History preference is invalid or unreadable/);
|
|
154
|
+
assert.ok(notifyCalls[0][0].includes(preference), `the warning must name the file, got: ${notifyCalls[0][0]}`);
|
|
155
|
+
assert.ok(notifyCalls[0][0].includes("GENTLE_PI_HISTORY_CAPTURE=1"));
|
|
156
|
+
// Reporting never repairs: the malformed file and the store stay untouched.
|
|
157
|
+
assert.equal(fs.readFileSync(preference, "utf8"), "{not json");
|
|
158
|
+
assert.deepEqual(fs.readdirSync(root), []);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("an env value that defers to a malformed preference still reports it", async () => {
|
|
162
|
+
const root = makeRoot();
|
|
163
|
+
const configHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-off-config-"));
|
|
164
|
+
fs.writeFileSync(path.join(configHome, "history-capture.json"), "[]", "utf8");
|
|
165
|
+
const { commandHandler } = loadWithCommand({ GENTLE_PI_HISTORY_CAPTURE: "maybe" }, root, configHome);
|
|
166
|
+
const notifyCalls: Array<[string, string]> = [];
|
|
167
|
+
await commandHandler([], fakeCtx(notifyCalls));
|
|
168
|
+
assert.equal(notifyCalls.length, 1);
|
|
169
|
+
assert.match(notifyCalls[0][0], /preference is invalid or unreadable/);
|
|
170
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Stage-1 open-flow wiring tests (a22588fc T31/T32 port, adapted to the
|
|
8
|
+
* slice-02 DrainResult drain contract). NEVER import
|
|
9
|
+
* extensions/history/index.ts — it pulls the pi-tui runtime graph (§D3).
|
|
10
|
+
* The wiring is pinned by source-parse (command-registration pattern);
|
|
11
|
+
* loader behavior uses fs-only fixtures under the OS temp dir — NEVER the
|
|
12
|
+
* user's real ~/.pi/agent/history.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const indexSource = fs.readFileSync(
|
|
16
|
+
fileURLToPath(new URL("../extensions/history/index.ts", import.meta.url)),
|
|
17
|
+
"utf8",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
function openHistorySelectorBody(): string {
|
|
21
|
+
const start = indexSource.indexOf("async function openHistorySelector(");
|
|
22
|
+
assert.ok(start >= 0, "openHistorySelector should exist");
|
|
23
|
+
const end = indexSource.indexOf("export default function", start);
|
|
24
|
+
assert.ok(end > start, "extension entry point should follow");
|
|
25
|
+
return indexSource.slice(start, end);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// T31 (adapted) — store-only drain wiring over the DrainResult contract.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
test("T31 (adapted): the store drain is the entries source — DrainResult unwrapped", () => {
|
|
33
|
+
const body = openHistorySelectorBody();
|
|
34
|
+
const drainIdx = body.indexOf('const drained = drainForScope("project")');
|
|
35
|
+
assert.ok(
|
|
36
|
+
drainIdx >= 0,
|
|
37
|
+
"the load step must drain the store through drainForScope",
|
|
38
|
+
);
|
|
39
|
+
assert.ok(
|
|
40
|
+
!body.includes("mergeHistoryEntries("),
|
|
41
|
+
"no live transcript merge in the open flow (store-only scopes)",
|
|
42
|
+
);
|
|
43
|
+
assert.ok(
|
|
44
|
+
body.includes('drained.status === "blocked"'),
|
|
45
|
+
"the DrainResult blocked variant must be unwrapped before any records build",
|
|
46
|
+
);
|
|
47
|
+
assert.ok(
|
|
48
|
+
body.includes("const entries = drained.prompts"),
|
|
49
|
+
"the ok variant feeds the entries list",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("T31 (adapted): records are built via recordsFromEntries over the drained entries", () => {
|
|
54
|
+
const body = openHistorySelectorBody();
|
|
55
|
+
const recIdx = body.indexOf("recordsFromEntries(entries)");
|
|
56
|
+
assert.ok(
|
|
57
|
+
recIdx >= 0,
|
|
58
|
+
"records build through the shared recordsFromEntries helper",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("T31: the command-registration pins hold beside the drain contract", () => {
|
|
63
|
+
const definitions =
|
|
64
|
+
indexSource.split("async function openHistorySelector(").length - 1;
|
|
65
|
+
assert.equal(definitions, 1, "openHistorySelector defined exactly once");
|
|
66
|
+
const calls = indexSource.split("openHistorySelector(ctx)").length - 1;
|
|
67
|
+
assert.equal(
|
|
68
|
+
calls,
|
|
69
|
+
2,
|
|
70
|
+
"exactly the two entry-point call sites — the drain contract adds no occurrence",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// T32 (adapted) — synchronous open-path wiring (no-await source-parse).
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
test("T32 (adapted): the open path never awaits a records build (source-parse)", () => {
|
|
79
|
+
const body = openHistorySelectorBody();
|
|
80
|
+
assert.ok(
|
|
81
|
+
!body.includes("startBackgroundIndexBuild"),
|
|
82
|
+
"no background build kick lives in the selector",
|
|
83
|
+
);
|
|
84
|
+
assert.ok(
|
|
85
|
+
!/await\s+recordsFromEntries/.test(body),
|
|
86
|
+
"the open path never awaits the records build (sync const declaration)",
|
|
87
|
+
);
|
|
88
|
+
const drainIdx = body.indexOf('drainForScope("project")');
|
|
89
|
+
const emptyIdx = body.indexOf("entries.length === 0");
|
|
90
|
+
assert.ok(
|
|
91
|
+
drainIdx >= 0 && emptyIdx > drainIdx,
|
|
92
|
+
"the empty guard follows the drain",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Stage-1 skeleton pins: the minimal openable unit and its policy seams.
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
test("the stage-1 selector skeleton wires cancel through the overlay close", () => {
|
|
101
|
+
assert.ok(
|
|
102
|
+
indexSource.includes('kb.matches(_d, "tui.select.cancel")'),
|
|
103
|
+
"the dispatch table handles cancel",
|
|
104
|
+
);
|
|
105
|
+
assert.ok(
|
|
106
|
+
indexSource.includes("this.onCancel()"),
|
|
107
|
+
"cancel routes to the overlay close callback",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("the selection result pastes into the editor via pasteToEditor", () => {
|
|
112
|
+
const body = openHistorySelectorBody();
|
|
113
|
+
assert.ok(
|
|
114
|
+
body.includes("ctx.ui.pasteToEditor(selected.text)"),
|
|
115
|
+
"the selected prompt enters the editor through the paste pipeline (a22588fc)",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/** Method body slice (lazy-windowing.test.ts pattern; first "\n }" close). */
|
|
120
|
+
function methodBodyOf(name: string): string {
|
|
121
|
+
const decl = indexSource.indexOf(`private ${name}(`);
|
|
122
|
+
assert.ok(decl >= 0, `private ${name}() should exist in extensions/history/index.ts`);
|
|
123
|
+
const end = indexSource.indexOf("\n }", decl);
|
|
124
|
+
assert.ok(end > decl, `private ${name}() body should close`);
|
|
125
|
+
return indexSource.slice(decl, end);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// T33 — AC-S6-3: merged header totals + third transient dim indexing segment.
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
test("T33 (AC-S6-3): header totals derive from filteredRecords — derivation untouched", () => {
|
|
133
|
+
const body = methodBodyOf("rebuildListWithWidth");
|
|
134
|
+
assert.ok(
|
|
135
|
+
body.includes("const count = this.filteredRecords.length;"),
|
|
136
|
+
"N derives from filteredRecords (merged by construction)",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("T33 (AC-S6-3): loaded segment present, indexing segment removed", () => {
|
|
141
|
+
const body = methodBodyOf("rebuildListWithWidth");
|
|
142
|
+
const setTextAt = body.indexOf("headerRow.setText(");
|
|
143
|
+
assert.ok(setTextAt >= 0, "the header must keep the existing setText call");
|
|
144
|
+
const setTextRegion = body.slice(
|
|
145
|
+
setTextAt,
|
|
146
|
+
body.indexOf("this.listContainer.clear()"),
|
|
147
|
+
);
|
|
148
|
+
assert.ok(
|
|
149
|
+
setTextRegion.includes("loaded ") &&
|
|
150
|
+
setTextRegion.includes("this.loadedCount"),
|
|
151
|
+
"the loaded segment stays (user-restored)",
|
|
152
|
+
);
|
|
153
|
+
assert.ok(
|
|
154
|
+
!setTextRegion.includes("indexing "),
|
|
155
|
+
"the indexing segment stays removed",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("T33 (AC-S6-3): Change 2 structural pins still hold beside the third segment", () => {
|
|
160
|
+
assert.ok(
|
|
161
|
+
indexSource.includes("private static readonly OVERLAY_LINES = 30;"),
|
|
162
|
+
"OVERLAY_LINES = 30 intact",
|
|
163
|
+
);
|
|
164
|
+
const body = methodBodyOf("rebuildListWithWidth");
|
|
165
|
+
const addChildCount = body.split("addChild(").length - 1;
|
|
166
|
+
assert.equal(addChildCount, 4, "no new addChild in rebuildListWithWidth");
|
|
167
|
+
const classAt = indexSource.indexOf("class PromptHistorySelector");
|
|
168
|
+
const ctorAt = indexSource.indexOf("constructor(", classAt);
|
|
169
|
+
const ctorEnd = indexSource.indexOf('this.applyFilter("")', ctorAt);
|
|
170
|
+
const ctorAddChild =
|
|
171
|
+
indexSource.slice(ctorAt, ctorEnd).split("this.addChild(").length - 1;
|
|
172
|
+
assert.equal(ctorAddChild, 14, "the constructor child sequence is unchanged");
|
|
173
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
7
|
+
import promptHistoryExtension from "../extensions/history/index.ts";
|
|
8
|
+
import {
|
|
9
|
+
editorOverlayMargin,
|
|
10
|
+
SIDEBAR_OVERLAY_PADDING,
|
|
11
|
+
} from "../extensions/history/selector-helpers.ts";
|
|
12
|
+
import { projectHash } from "../extensions/history/store.ts";
|
|
13
|
+
import {
|
|
14
|
+
installSidebar,
|
|
15
|
+
SIDEBAR_RAIL_COLUMNS,
|
|
16
|
+
} from "../lib/shell-sidebar-layout.ts";
|
|
17
|
+
import { sidebarPart, sidebarState } from "../lib/shell-sidebar.ts";
|
|
18
|
+
|
|
19
|
+
// Sidebar-aware overlay margin (restored from Carolina's #1394 work,
|
|
20
|
+
// e2cca1f9b^). The history extension reads the terminal-owned sidebar state
|
|
21
|
+
// contract (lib/shell-sidebar.ts) without importing gentle-shell; the rail
|
|
22
|
+
// width comes from the published `railColumns` field instead of a constant
|
|
23
|
+
// duplicated in the history extension.
|
|
24
|
+
|
|
25
|
+
function terminalWithState(state: unknown): object {
|
|
26
|
+
return {
|
|
27
|
+
[Symbol.for("gentle-pi.experimental-sidebar.state")]: state,
|
|
28
|
+
} as object;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const OWNING = {
|
|
32
|
+
active: true,
|
|
33
|
+
ownsHost: () => true,
|
|
34
|
+
railColumns: SIDEBAR_RAIL_COLUMNS,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
test("the published rail reservation pins the gentle-shell rail geometry (RAIL_WIDTH 50 + GAP 3)", () => {
|
|
38
|
+
assert.equal(SIDEBAR_RAIL_COLUMNS, 53);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("padding constant pins the user-directed 1-column breathing room", () => {
|
|
42
|
+
assert.equal(SIDEBAR_OVERLAY_PADDING, 1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("returns 0 for absent, primitive, or null terminals", () => {
|
|
46
|
+
assert.equal(editorOverlayMargin(undefined), 0);
|
|
47
|
+
assert.equal(editorOverlayMargin(null), 0);
|
|
48
|
+
assert.equal(editorOverlayMargin(42), 0);
|
|
49
|
+
assert.equal(editorOverlayMargin("terminal"), 0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("returns 0 when no sidebar state is stored on the terminal", () => {
|
|
53
|
+
assert.equal(editorOverlayMargin({}), 0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returns 0 for malformed state shapes", () => {
|
|
57
|
+
assert.equal(editorOverlayMargin(terminalWithState(undefined)), 0);
|
|
58
|
+
assert.equal(editorOverlayMargin(terminalWithState(null)), 0);
|
|
59
|
+
assert.equal(editorOverlayMargin(terminalWithState("active")), 0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("returns 0 unless active is exactly true AND ownsHost is a function", () => {
|
|
63
|
+
assert.equal(
|
|
64
|
+
editorOverlayMargin(terminalWithState({ ...OWNING, ownsHost: undefined })),
|
|
65
|
+
0,
|
|
66
|
+
"active without ownsHost",
|
|
67
|
+
);
|
|
68
|
+
assert.equal(
|
|
69
|
+
editorOverlayMargin(terminalWithState({ ...OWNING, active: false })),
|
|
70
|
+
0,
|
|
71
|
+
"inactive",
|
|
72
|
+
);
|
|
73
|
+
assert.equal(
|
|
74
|
+
editorOverlayMargin(terminalWithState({ ...OWNING, active: 1 })),
|
|
75
|
+
0,
|
|
76
|
+
"non-boolean truthy active",
|
|
77
|
+
);
|
|
78
|
+
assert.equal(
|
|
79
|
+
editorOverlayMargin(
|
|
80
|
+
terminalWithState({ ...OWNING, ownsHost: "not-a-function" }),
|
|
81
|
+
),
|
|
82
|
+
0,
|
|
83
|
+
"non-function ownsHost",
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("returns 0 unless the sidebar publishes a positive integer rail reservation", () => {
|
|
88
|
+
for (const railColumns of [undefined, 0, -53, 53.5, "53", Number.NaN]) {
|
|
89
|
+
assert.equal(
|
|
90
|
+
editorOverlayMargin(terminalWithState({ ...OWNING, railColumns })),
|
|
91
|
+
0,
|
|
92
|
+
`railColumns ${String(railColumns)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns the rail reservation plus padding only while the sidebar owns the host", () => {
|
|
98
|
+
assert.equal(editorOverlayMargin(terminalWithState(OWNING)), 54);
|
|
99
|
+
assert.equal(
|
|
100
|
+
editorOverlayMargin(terminalWithState({ ...OWNING, railColumns: 40 })),
|
|
101
|
+
41,
|
|
102
|
+
"the margin follows the published reservation",
|
|
103
|
+
);
|
|
104
|
+
assert.equal(
|
|
105
|
+
editorOverlayMargin(
|
|
106
|
+
terminalWithState({ ...OWNING, ownsHost: () => false }),
|
|
107
|
+
),
|
|
108
|
+
0,
|
|
109
|
+
"state present but host not owned (regular mode / unpatched root)",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a throwing ownsHost degrades to 0 instead of breaking the picker", () => {
|
|
114
|
+
assert.equal(
|
|
115
|
+
editorOverlayMargin(
|
|
116
|
+
terminalWithState({
|
|
117
|
+
...OWNING,
|
|
118
|
+
ownsHost: () => {
|
|
119
|
+
throw new Error("boom");
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
0,
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Seam: the real gentle-shell sidebar publishes the reservation it paints.
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
const NODE = Symbol.for("@earendil-works/pi-tui/layout-node");
|
|
132
|
+
const shellTheme = {
|
|
133
|
+
fg: (_color: string, text: string) => text,
|
|
134
|
+
bold: (text: string) => text,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
function sidebarHost(columns: number) {
|
|
138
|
+
const root = {
|
|
139
|
+
render: () => ["transcript"],
|
|
140
|
+
invalidate() {},
|
|
141
|
+
[NODE]: () => ({ type: "vstack", entries: [] }),
|
|
142
|
+
};
|
|
143
|
+
const host = {
|
|
144
|
+
mode: "fullscreen",
|
|
145
|
+
terminal: { columns },
|
|
146
|
+
layoutRoot: root,
|
|
147
|
+
requestRender() {},
|
|
148
|
+
};
|
|
149
|
+
const tui = host as unknown as TUI;
|
|
150
|
+
sidebarPart(tui, "footer", {
|
|
151
|
+
render: (_width: number) => ["Status"],
|
|
152
|
+
invalidate() {},
|
|
153
|
+
});
|
|
154
|
+
return { host, tui, root };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
test("the installed sidebar publishes its rail reservation; the margin tracks the breakpoint", (t) => {
|
|
158
|
+
const { host, tui, root } = sidebarHost(140);
|
|
159
|
+
t.after(installSidebar(tui, shellTheme));
|
|
160
|
+
assert.equal(sidebarState(tui).railColumns, SIDEBAR_RAIL_COLUMNS);
|
|
161
|
+
root[NODE]();
|
|
162
|
+
assert.equal(
|
|
163
|
+
editorOverlayMargin(host.terminal),
|
|
164
|
+
SIDEBAR_RAIL_COLUMNS + SIDEBAR_OVERLAY_PADDING,
|
|
165
|
+
"fullscreen at the breakpoint with a painting rail",
|
|
166
|
+
);
|
|
167
|
+
host.terminal.columns = 139;
|
|
168
|
+
root[NODE]();
|
|
169
|
+
assert.equal(editorOverlayMargin(host.terminal), 0, "below the breakpoint");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Overlay options: the picker re-reads the margin on every render pass.
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
const CWD = "/pi-history-fixtures/project-overlay-margin";
|
|
177
|
+
|
|
178
|
+
interface OverlayOptions {
|
|
179
|
+
anchor?: string;
|
|
180
|
+
width?: string;
|
|
181
|
+
offsetY?: number;
|
|
182
|
+
margin?: { right: number };
|
|
183
|
+
visible?: (columns: number, rows: number) => boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function captureOverlayOptions(terminal: object): Promise<OverlayOptions> {
|
|
187
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-margin-"));
|
|
188
|
+
const dir = path.join(root, "projects", projectHash(CWD));
|
|
189
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
190
|
+
fs.writeFileSync(
|
|
191
|
+
path.join(dir, "store.jsonl"),
|
|
192
|
+
`${JSON.stringify({ v: 1, text: "p00", ts: 1_700_000_000_000 })}\n`,
|
|
193
|
+
);
|
|
194
|
+
const commands: Array<[string, { handler: unknown }]> = [];
|
|
195
|
+
promptHistoryExtension(
|
|
196
|
+
{
|
|
197
|
+
on: () => {},
|
|
198
|
+
registerShortcut: () => {},
|
|
199
|
+
registerCommand: (name: string, def: { handler: unknown }) => {
|
|
200
|
+
commands.push([name, def]);
|
|
201
|
+
},
|
|
202
|
+
} as never,
|
|
203
|
+
{
|
|
204
|
+
env: { GENTLE_PI_HISTORY_CAPTURE: "1" },
|
|
205
|
+
gentlePiConfigHome: fs.mkdtempSync(
|
|
206
|
+
path.join(os.tmpdir(), "pi-history-margin-config-"),
|
|
207
|
+
),
|
|
208
|
+
root,
|
|
209
|
+
cwd: CWD,
|
|
210
|
+
instanceId: "inst-margin",
|
|
211
|
+
agentDir: path.join(root, "agent"),
|
|
212
|
+
sessionsRoot: path.join(root, "sessions"),
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
const command = commands.find(([name]) => name === "history");
|
|
216
|
+
assert.ok(command, "the history command must be registered");
|
|
217
|
+
const handler = command[1].handler as (
|
|
218
|
+
args: unknown,
|
|
219
|
+
ctx: unknown,
|
|
220
|
+
) => Promise<void>;
|
|
221
|
+
let captured: OverlayOptions | undefined;
|
|
222
|
+
const plain = (_color: string, text: string) => text;
|
|
223
|
+
await handler([], {
|
|
224
|
+
ui: {
|
|
225
|
+
notify: (message: string) => assert.fail(`unexpected: ${message}`),
|
|
226
|
+
pasteToEditor: () => {},
|
|
227
|
+
custom: (
|
|
228
|
+
factory: (
|
|
229
|
+
tui: unknown,
|
|
230
|
+
theme: unknown,
|
|
231
|
+
keybindings: unknown,
|
|
232
|
+
done: (result: unknown) => void,
|
|
233
|
+
) => unknown,
|
|
234
|
+
options: { overlayOptions?: OverlayOptions | (() => OverlayOptions) },
|
|
235
|
+
) => {
|
|
236
|
+
// Mount first, like pi: the factory captures the TUI handle, then
|
|
237
|
+
// the host resolves the overlay options.
|
|
238
|
+
factory(
|
|
239
|
+
{ requestRender: () => {}, terminal },
|
|
240
|
+
{ fg: plain, bg: plain, bold: (text: string) => text },
|
|
241
|
+
undefined,
|
|
242
|
+
() => {},
|
|
243
|
+
);
|
|
244
|
+
const resolved = options.overlayOptions;
|
|
245
|
+
captured = typeof resolved === "function" ? resolved() : resolved;
|
|
246
|
+
return Promise.resolve(null);
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
assert.ok(captured, "the selector must pass overlay options");
|
|
251
|
+
return captured;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
test("the picker keeps the native full-window overlay without a painting sidebar", async () => {
|
|
255
|
+
const options = await captureOverlayOptions({});
|
|
256
|
+
assert.equal(options.anchor, "bottom-center");
|
|
257
|
+
assert.equal(options.width, "100%");
|
|
258
|
+
assert.equal(options.offsetY, 5);
|
|
259
|
+
assert.equal(options.margin, undefined);
|
|
260
|
+
assert.equal(options.visible?.(200, 50), true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("the picker margin stays live while the overlay is open", async () => {
|
|
264
|
+
const state = { ...OWNING };
|
|
265
|
+
const options = await captureOverlayOptions(terminalWithState(state));
|
|
266
|
+
assert.deepEqual(options.margin, { right: 54 });
|
|
267
|
+
// Resizing below the breakpoint: pi-tui calls visible() on every render
|
|
268
|
+
// pass, then re-reads margin while resolving the layout.
|
|
269
|
+
state.active = false;
|
|
270
|
+
assert.equal(options.visible?.(139, 50), true);
|
|
271
|
+
assert.equal(options.margin, undefined);
|
|
272
|
+
state.active = true;
|
|
273
|
+
assert.equal(options.visible?.(140, 50), true);
|
|
274
|
+
assert.deepEqual(options.margin, { right: 54 });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Review advisory A4 (#1480): the live margin must follow the REAL installed
|
|
278
|
+
// sidebar after the overlay opened, not only a hand-built state object.
|
|
279
|
+
test("the open picker's margin follows the real sidebar across the breakpoint and teardown", async (t) => {
|
|
280
|
+
const { host, tui, root } = sidebarHost(139);
|
|
281
|
+
let uninstall = installSidebar(tui, shellTheme);
|
|
282
|
+
t.after(() => uninstall());
|
|
283
|
+
root[NODE]();
|
|
284
|
+
const options = await captureOverlayOptions(host.terminal);
|
|
285
|
+
assert.equal(options.margin, undefined, "opened below the breakpoint: no rail");
|
|
286
|
+
|
|
287
|
+
// Widening across the breakpoint paints the rail while the picker is open.
|
|
288
|
+
host.terminal.columns = 140;
|
|
289
|
+
root[NODE]();
|
|
290
|
+
assert.equal(options.visible?.(140, 50), true);
|
|
291
|
+
assert.deepEqual(options.margin, {
|
|
292
|
+
right: SIDEBAR_RAIL_COLUMNS + SIDEBAR_OVERLAY_PADDING,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Narrowing below it removes the rail again.
|
|
296
|
+
host.terminal.columns = 139;
|
|
297
|
+
root[NODE]();
|
|
298
|
+
assert.equal(options.visible?.(139, 50), true);
|
|
299
|
+
assert.equal(options.margin, undefined);
|
|
300
|
+
|
|
301
|
+
// Back at the breakpoint, then the sidebar is torn down: the rail
|
|
302
|
+
// disappears and the picker returns to the full window.
|
|
303
|
+
host.terminal.columns = 140;
|
|
304
|
+
root[NODE]();
|
|
305
|
+
assert.equal(options.visible?.(140, 50), true);
|
|
306
|
+
assert.deepEqual(options.margin, {
|
|
307
|
+
right: SIDEBAR_RAIL_COLUMNS + SIDEBAR_OVERLAY_PADDING,
|
|
308
|
+
});
|
|
309
|
+
uninstall();
|
|
310
|
+
uninstall = () => {};
|
|
311
|
+
assert.equal(options.visible?.(140, 50), true);
|
|
312
|
+
assert.equal(options.margin, undefined);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("the margin getter reports the value refreshed by the last visible() pass", async () => {
|
|
316
|
+
const state = { ...OWNING };
|
|
317
|
+
const options = await captureOverlayOptions(terminalWithState(state));
|
|
318
|
+
assert.deepEqual(options.margin, { right: 54 });
|
|
319
|
+
// pi-tui re-reads margin several times per layout; between visible()
|
|
320
|
+
// passes every read agrees, and the next pass picks up the change.
|
|
321
|
+
state.active = false;
|
|
322
|
+
assert.deepEqual(options.margin, { right: 54 });
|
|
323
|
+
assert.equal(options.visible?.(139, 50), true);
|
|
324
|
+
assert.equal(options.margin, undefined);
|
|
325
|
+
assert.equal(options.margin, undefined);
|
|
326
|
+
});
|