gentle-pi 3.7.0 → 4.1.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 +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -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/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -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 +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- 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 +16 -21
- 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-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- 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/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -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 +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -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 +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -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 +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -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 +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -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
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -994,11 +994,80 @@ function operationForTransition(transition: ReviewTransition): ReviewOperation {
|
|
|
994
994
|
throw new ReviewIntegrityError(`Unsupported reducer transition: ${transition}`);
|
|
995
995
|
}
|
|
996
996
|
|
|
997
|
+
function assertReducerInput(transition: ReviewTransition, input: unknown): asserts input is ReviewReducerInput {
|
|
998
|
+
const fail = (field: string): never => { throw new ReviewIntegrityError(`Graph reducer input ${field} is invalid`); };
|
|
999
|
+
const object = (value: unknown, field: string): Record<string, unknown> => {
|
|
1000
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return fail(field);
|
|
1001
|
+
return value as Record<string, unknown>;
|
|
1002
|
+
};
|
|
1003
|
+
const string = (value: unknown, field: string): void => { if (typeof value !== "string") fail(field); };
|
|
1004
|
+
const boolean = (value: unknown, field: string): void => { if (typeof value !== "boolean") fail(field); };
|
|
1005
|
+
const strings = (value: unknown, field: string): void => { if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) fail(field); };
|
|
1006
|
+
const rows = (value: unknown, field: string): void => {
|
|
1007
|
+
if (!Array.isArray(value)) return fail(field);
|
|
1008
|
+
// The canonical row decoder checks every field, enum and duplicate ID.
|
|
1009
|
+
createFrozenLedger(value);
|
|
1010
|
+
};
|
|
1011
|
+
const results = (value: unknown, field: string): void => {
|
|
1012
|
+
if (!Array.isArray(value)) return fail(field);
|
|
1013
|
+
for (const item of value) {
|
|
1014
|
+
const result = object(item, field);
|
|
1015
|
+
string(result.id, `${field}.id`);
|
|
1016
|
+
if (!Object.values(RESOLUTION_OUTCOME).some((outcome) => outcome === result.outcome)) fail(`${field}.outcome`);
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
const body = object(input, "object");
|
|
1020
|
+
switch (transition) {
|
|
1021
|
+
case REVIEW_TRANSITION.ORDINARY_DISCOVERY: rows(body.rows, "rows"); break;
|
|
1022
|
+
case REVIEW_TRANSITION.JUDGMENT_DAY_DISCOVERY: rows(body.judgeA, "judgeA"); rows(body.judgeB, "judgeB"); break;
|
|
1023
|
+
case REVIEW_TRANSITION.ORDINARY_EVIDENCE:
|
|
1024
|
+
results(body.deterministicResults, "deterministicResults");
|
|
1025
|
+
if (body.refuterResults !== undefined) results(body.refuterResults, "refuterResults");
|
|
1026
|
+
break;
|
|
1027
|
+
case REVIEW_TRANSITION.ORDINARY_FIX:
|
|
1028
|
+
case REVIEW_TRANSITION.JUDGMENT_DAY_FIX:
|
|
1029
|
+
string(body.candidateTree, "candidateTree"); strings(body.fixedIds, "fixedIds"); string(body.fixDiff, "fixDiff");
|
|
1030
|
+
if (body.changedPaths !== undefined) strings(body.changedPaths, "changedPaths");
|
|
1031
|
+
break;
|
|
1032
|
+
case REVIEW_TRANSITION.ORDINARY_NO_FIX: string(body.reason, "reason"); break;
|
|
1033
|
+
case REVIEW_TRANSITION.ORDINARY_FINAL_VERIFICATION:
|
|
1034
|
+
case REVIEW_TRANSITION.JUDGMENT_DAY_FINAL_VERIFICATION:
|
|
1035
|
+
boolean(body.passed, "passed"); if (body.reason !== undefined) string(body.reason, "reason"); break;
|
|
1036
|
+
case REVIEW_TRANSITION.ORDINARY_VALIDATION:
|
|
1037
|
+
case REVIEW_TRANSITION.JUDGMENT_DAY_REJUDGMENT: {
|
|
1038
|
+
const request = object(body.request, "request");
|
|
1039
|
+
strings(request.requested_ids, "request.requested_ids"); rows(request.frozen_rows, "request.frozen_rows"); string(request.frozen_ledger_hash, "request.frozen_ledger_hash");
|
|
1040
|
+
if (transition === REVIEW_TRANSITION.JUDGMENT_DAY_REJUDGMENT) {
|
|
1041
|
+
for (const field of ["fix_diff", "fix_diff_hash", "candidate_tree"]) string(request[field], `request.${field}`);
|
|
1042
|
+
if (typeof request.round !== "number" || !Number.isSafeInteger(request.round) || request.round < 1) fail("request.round");
|
|
1043
|
+
results(body.judgeAResults, "judgeAResults"); results(body.judgeBResults, "judgeBResults");
|
|
1044
|
+
} else {
|
|
1045
|
+
results(body.results, "results");
|
|
1046
|
+
const acceptance = object(request.original_acceptance_tests, "request.original_acceptance_tests");
|
|
1047
|
+
boolean(acceptance.passed, "request.original_acceptance_tests.passed"); string(acceptance.evidence_hash, "request.original_acceptance_tests.evidence_hash");
|
|
1048
|
+
strings(request.original_criterion_regressions, "request.original_criterion_regressions");
|
|
1049
|
+
if (!Array.isArray(request.correction_regressions) || !Array.isArray(request.follow_ups)) return fail("request.validation arrays");
|
|
1050
|
+
for (const value of request.correction_regressions) {
|
|
1051
|
+
const regression = object(value, "request.correction_regressions");
|
|
1052
|
+
string(regression.finding_id, "regression.finding_id"); string(regression.evidence_hash, "regression.evidence_hash"); boolean(regression.passed, "regression.passed");
|
|
1053
|
+
}
|
|
1054
|
+
for (const value of request.follow_ups) {
|
|
1055
|
+
const followUp = object(value, "request.follow_ups");
|
|
1056
|
+
for (const field of ["id", "location", "summary", "evidence_hash"]) string(followUp[field], `follow_up.${field}`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
default: fail("transition");
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
997
1065
|
function reduceReviewState(
|
|
998
1066
|
state: ReviewStateV1,
|
|
999
1067
|
transition: ReviewTransition,
|
|
1000
|
-
input:
|
|
1068
|
+
input: unknown,
|
|
1001
1069
|
): ReviewStateV1 {
|
|
1070
|
+
assertReducerInput(transition, input);
|
|
1002
1071
|
switch (transition) {
|
|
1003
1072
|
case REVIEW_TRANSITION.ORDINARY_DISCOVERY:
|
|
1004
1073
|
return recordOrdinaryDiscovery(state, input as OrdinaryDiscoveryInput);
|
|
@@ -1456,8 +1525,8 @@ export class ReviewTransactionStore {
|
|
|
1456
1525
|
const existing = current ? (current.body.lineages as Array<Record<string, unknown>>).find((value) => value.lineage_id === next.lineage_id && value.mode === "graph") : undefined;
|
|
1457
1526
|
if (previous && !existing) throw new ReviewIntegrityError("Graph predecessor is missing");
|
|
1458
1527
|
if (!previous && existing) throw new ReviewIntegrityError("Graph lineage already exists");
|
|
1459
|
-
|
|
1460
|
-
|
|
1528
|
+
if (existing?.head_event_id !== undefined && typeof existing.head_event_id !== "string") throw new ReviewIntegrityError("Graph head is invalid");
|
|
1529
|
+
const predecessor = typeof existing?.head_event_id === "string" ? existing.head_event_id : undefined;
|
|
1461
1530
|
const last = next.request_journal.at(-1);
|
|
1462
1531
|
const descriptor = (() => { try { return graph.readStoreDescriptor(); } catch { return undefined; } })();
|
|
1463
1532
|
const reducerTransition = eventContext?.transition ?? (predecessor === undefined ? "start" : last?.operation ?? "state-update");
|
|
@@ -18,7 +18,7 @@ const tokenFields = ["input", "output", "cacheRead", "cacheWrite", "reasoning",
|
|
|
18
18
|
* Only the fixed package catalog is cached; runtime instructions are not retained.
|
|
19
19
|
*/
|
|
20
20
|
let definitions: Array<{ name: string; fingerprint: string; fingerprintClass?: AgentClass }> | undefined;
|
|
21
|
-
const packagedAgentClassAliases = new Map([["sdd-proposal", "sdd-propose"]]
|
|
21
|
+
const packagedAgentClassAliases = new Map<string, string>([["sdd-proposal", "sdd-propose"]]);
|
|
22
22
|
function fingerprintAgentClassName(name: string): string {
|
|
23
23
|
const compatibilityName = name.startsWith("gentle-ai-") ? name.slice("gentle-ai-".length) : name;
|
|
24
24
|
return packagedAgentClassAliases.get(compatibilityName) ?? compatibilityName;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from "node:child_process";
|
|
2
|
-
import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
2
|
+
import { gentleAiIsolationEnv, resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
3
3
|
import { runtimeMetricsEnvAllows } from "./runtime-metrics-policy.ts";
|
|
4
4
|
import { isPublicRuntimeModelId, normalizeRuntimeModel, type RuntimeMetricBucket } from "./runtime-metrics.ts";
|
|
5
5
|
import type { ChildLaunchBucket } from "./runtime-metrics-children.ts";
|
|
@@ -105,7 +105,8 @@ export async function sendNativeRuntimeEvent(rows: readonly RuntimeMetricBucket[
|
|
|
105
105
|
const file = (deps.resolve ?? resolveGentleAiBinary)();
|
|
106
106
|
if (!runtimeMetricsEnvAllows(env)) return "disabled";
|
|
107
107
|
if (deps.signal?.aborted || deps.current?.() === false) return "discarded";
|
|
108
|
-
|
|
108
|
+
// S9: a bundled install keeps Gentle AI's state inside its prefix.
|
|
109
|
+
return await sendProcess(file, cwd, payload, gentleAiIsolationEnv(env), deps.spawn ?? spawn, deps.signal);
|
|
109
110
|
} catch { return "discarded"; }
|
|
110
111
|
finally { busy = false; }
|
|
111
112
|
}
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { decodePrintableKey } from "./pi-tui-keys.ts";
|
|
2
|
+
import { isKeyRelease, matchesKey, truncateToWidth, visibleWidth, type EditorComponent } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Native text-selection engine for GentlePromptEditor: shift+home / shift+end
|
|
6
|
+
* selection, alt+e / alt+a select-all, and replace-on-key (backspace / delete /
|
|
7
|
+
* printable character) with reverse-video highlight and a bottom-rule hint.
|
|
8
|
+
* Ported from @exopro/pi-select-del so the petal prompt owns the feature
|
|
9
|
+
* natively — no factory composition, no cross-extension focus handoff.
|
|
10
|
+
*
|
|
11
|
+
* Behavior contract (ported from pi-select-del, with atomic paste replacement):
|
|
12
|
+
* - shift+home — anchor at the cursor, move to line start (held presses at
|
|
13
|
+
* the edge keep the selection; only a zero-width span collapses)
|
|
14
|
+
* - shift+end — anchor at the cursor, move to line end
|
|
15
|
+
* - alt+e — select all; alt+a too when no extension shortcut claims it
|
|
16
|
+
* (Gentle Agents owns alt+a by default, gentle-shell#1565)
|
|
17
|
+
* - a registered extension shortcut always wins over these selection chords,
|
|
18
|
+
* matching CustomEditor's own precedence
|
|
19
|
+
* - backspace / delete / printable character or bracketed paste over an
|
|
20
|
+
* active selection — replace it in one atomic edit (undo restores text AND cursor)
|
|
21
|
+
* - any other key — collapse the selection first, then behave natively
|
|
22
|
+
*
|
|
23
|
+
* The engine drives the host editor's own internals (the EditorInternals
|
|
24
|
+
* surface every CustomEditor-derived editor exposes) and degrades to pure
|
|
25
|
+
* passthrough if that surface drifts: a pi upgrade can cost the selection
|
|
26
|
+
* features, never crash editing.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Cursor/anchor position in logical (line, col) editor coordinates. */
|
|
30
|
+
export interface Point {
|
|
31
|
+
line: number;
|
|
32
|
+
col: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Narrow view of the private Editor internals the engine relies on. Kept in
|
|
37
|
+
* one place so a pi upgrade only needs re-verifying against this interface.
|
|
38
|
+
*/
|
|
39
|
+
export interface EditorInternals {
|
|
40
|
+
state: { lines: string[]; cursorLine: number; cursorCol: number };
|
|
41
|
+
paddingX: number;
|
|
42
|
+
scrollOffset: number;
|
|
43
|
+
renderedVisibleLineCount: number;
|
|
44
|
+
/** Layout width the last base render wrapped at; soft-optional — falsy falls back to manual width math. */
|
|
45
|
+
lastWidth: number;
|
|
46
|
+
tui: { requestRender(): void };
|
|
47
|
+
autocompleteState: "regular" | "force" | null;
|
|
48
|
+
lastAction: "kill" | "yank" | "type-word" | null;
|
|
49
|
+
pushUndoSnapshot(): void;
|
|
50
|
+
setCursorCol(col: number): void;
|
|
51
|
+
moveToLineStart(): void;
|
|
52
|
+
moveToLineEnd(): void;
|
|
53
|
+
exitHistoryBrowsing(): void;
|
|
54
|
+
cancelAutocomplete(): void;
|
|
55
|
+
updateAutocomplete(): void;
|
|
56
|
+
buildVisualLineMap(width: number): Array<{ logicalLine: number; startCol: number; length: number }>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const INTERNAL_PROBE_MEMBERS = {
|
|
60
|
+
properties: [
|
|
61
|
+
"state",
|
|
62
|
+
"paddingX",
|
|
63
|
+
"scrollOffset",
|
|
64
|
+
"renderedVisibleLineCount",
|
|
65
|
+
"autocompleteState",
|
|
66
|
+
"lastAction",
|
|
67
|
+
"tui",
|
|
68
|
+
],
|
|
69
|
+
functions: [
|
|
70
|
+
"pushUndoSnapshot",
|
|
71
|
+
"setCursorCol",
|
|
72
|
+
"moveToLineStart",
|
|
73
|
+
"moveToLineEnd",
|
|
74
|
+
"exitHistoryBrowsing",
|
|
75
|
+
"cancelAutocomplete",
|
|
76
|
+
"updateAutocomplete",
|
|
77
|
+
"buildVisualLineMap",
|
|
78
|
+
],
|
|
79
|
+
} as const satisfies {
|
|
80
|
+
properties: readonly (keyof EditorInternals)[];
|
|
81
|
+
functions: readonly (keyof EditorInternals)[];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Members of `target` missing or malformed against the EditorInternals
|
|
86
|
+
* contract. Class-field trap: the properties are ES class fields (instance
|
|
87
|
+
* own-properties), invisible to any prototype probe — probe a real instance,
|
|
88
|
+
* never Editor.prototype or a subclass prototype.
|
|
89
|
+
*/
|
|
90
|
+
export function missingEditorInternals(target: object): string[] {
|
|
91
|
+
const missing: string[] = [];
|
|
92
|
+
const view = target as Record<string, unknown>;
|
|
93
|
+
for (const name of INTERNAL_PROBE_MEMBERS.properties) {
|
|
94
|
+
if (view[name] === undefined) missing.push(name);
|
|
95
|
+
}
|
|
96
|
+
for (const name of INTERNAL_PROBE_MEMBERS.functions) {
|
|
97
|
+
if (typeof view[name] !== "function") missing.push(name);
|
|
98
|
+
}
|
|
99
|
+
return missing;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function clamp(value: number, lo: number, hi: number): number {
|
|
103
|
+
return Math.max(lo, Math.min(hi, value));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Wrap the code-unit span [startCu, endCu) of a rendered editor row in reverse
|
|
108
|
+
* video. Positions are code-unit offsets into the row's PLAIN text (same unit
|
|
109
|
+
* the editor uses for cursorCol and visual-line map columns). The rendered row
|
|
110
|
+
* may already contain escape sequences (SGR colors, cursor markers); the walk
|
|
111
|
+
* passes those through untouched, keeping code-unit alignment. The span closes
|
|
112
|
+
* with SGR 27 (reverse off), not a full reset: row attributes set before the
|
|
113
|
+
* span must survive. A nested SGR reset re-arms reverse right after it.
|
|
114
|
+
*/
|
|
115
|
+
export function withReverseSpan(row: string, startCu: number, endCu: number): string {
|
|
116
|
+
let out = "";
|
|
117
|
+
let cu = 0;
|
|
118
|
+
let i = 0;
|
|
119
|
+
let opened = false;
|
|
120
|
+
while (i < row.length) {
|
|
121
|
+
if (!opened && cu >= startCu) {
|
|
122
|
+
out += "\x1b[7m";
|
|
123
|
+
opened = true;
|
|
124
|
+
}
|
|
125
|
+
if (opened && cu >= endCu) {
|
|
126
|
+
return `${out}\x1b[27m${row.slice(i)}`;
|
|
127
|
+
}
|
|
128
|
+
if (row[i] === "\x1b") {
|
|
129
|
+
const seq = row.slice(i, i + escapeSequenceLength(row, i));
|
|
130
|
+
out += seq;
|
|
131
|
+
// A nested SGR reset (e.g. the cursor block's own, when the cursor
|
|
132
|
+
// sits inside the span) clears reverse for everything after it:
|
|
133
|
+
// re-arm reverse right after it.
|
|
134
|
+
if (opened && cu < endCu && isSgrReset(seq)) out += "\x1b[7m";
|
|
135
|
+
i += seq.length;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
out += row[i];
|
|
139
|
+
i += 1;
|
|
140
|
+
cu += 1;
|
|
141
|
+
}
|
|
142
|
+
return opened ? `${out}\x1b[27m` : out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Length of the escape sequence at s[i] (s[i] === ESC). Unterminated sequences end the row. */
|
|
146
|
+
export function escapeSequenceLength(s: string, i: number): number {
|
|
147
|
+
const next = s[i + 1];
|
|
148
|
+
if (next === "[") {
|
|
149
|
+
// CSI: parameter/intermediate bytes 0x20-0x3F, final byte 0x40-0x7E.
|
|
150
|
+
for (let j = i + 2; j < s.length; j++) {
|
|
151
|
+
const code = s.charCodeAt(j);
|
|
152
|
+
if (code >= 0x40 && code <= 0x7e) return j - i + 1;
|
|
153
|
+
}
|
|
154
|
+
return s.length - i;
|
|
155
|
+
}
|
|
156
|
+
if (next === "]" || next === "_") {
|
|
157
|
+
// OSC / APC (cursor markers are APC): terminated by BEL or ST (ESC \).
|
|
158
|
+
const bel = s.indexOf("\x07", i + 2);
|
|
159
|
+
const st = s.indexOf("\x1b\\", i + 2);
|
|
160
|
+
if (bel === -1 && st === -1) return s.length - i;
|
|
161
|
+
if (bel === -1) return st - i + 2;
|
|
162
|
+
if (st === -1) return bel - i + 1;
|
|
163
|
+
return Math.min(bel - i + 1, st - i + 2);
|
|
164
|
+
}
|
|
165
|
+
return 2;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** True for SGR reset sequences: CSI ... m with an empty or all-zero parameter list. */
|
|
169
|
+
export function isSgrReset(seq: string): boolean {
|
|
170
|
+
const match = /^\x1b\[([\d;]*)m$/.exec(seq);
|
|
171
|
+
if (!match) return false;
|
|
172
|
+
const params = match[1];
|
|
173
|
+
if (params === "") return true;
|
|
174
|
+
return /^0+$/.test(params.split(";")[0]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Selection engine driving a host editor through the EditorInternals cast.
|
|
179
|
+
* `handleInput` takes the host's native dispatch as a `native` callback so the
|
|
180
|
+
* host keeps its own key chain (Esc gates, autocomplete, history) intact:
|
|
181
|
+
* selection keys are handled here; everything else collapses the anchor first,
|
|
182
|
+
* then behaves natively.
|
|
183
|
+
*/
|
|
184
|
+
const PASTE_START = "\x1b[200~";
|
|
185
|
+
const PASTE_END = "\x1b[201~";
|
|
186
|
+
const PASTE_END_TAIL_LENGTH = PASTE_END.length - 1;
|
|
187
|
+
/**
|
|
188
|
+
* Safety bound (UTF-8 bytes) for a bracketed-paste frame that has not yet
|
|
189
|
+
* seen its terminator. Checked only against a per-chunk byte count (never a
|
|
190
|
+
* full-buffer rescan), so it costs one Buffer.byteLength call per NEW chunk.
|
|
191
|
+
* Never applied once the terminator is found: a complete frame of any size,
|
|
192
|
+
* even one exceeding this bound, still reaches replaceSelectionWithPaste with
|
|
193
|
+
* one-step undo intact.
|
|
194
|
+
*/
|
|
195
|
+
const MAX_INCOMPLETE_PASTE_BYTES = 16 * 1024 * 1024;
|
|
196
|
+
|
|
197
|
+
export class SelectionEngine {
|
|
198
|
+
/** Selection anchor in logical (line, col) coordinates; adapter-exposed state. */
|
|
199
|
+
anchor: Point | null = null;
|
|
200
|
+
/**
|
|
201
|
+
* Chunks of an in-progress bracketed-paste frame (undefined when none is
|
|
202
|
+
* open). Chunks are only ever appended, never re-concatenated, so buffering
|
|
203
|
+
* N chunks stays O(N) total; the full frame is joined once, at completion.
|
|
204
|
+
*/
|
|
205
|
+
private pasteChunks: string[] | undefined;
|
|
206
|
+
/** Total code-unit length of `pasteChunks` before the chunk being processed; used to locate the terminator inside the joined frame. */
|
|
207
|
+
private pasteBufferedLength = 0;
|
|
208
|
+
/** Total UTF-8 byte length buffered so far while no terminator has been seen; compared against MAX_INCOMPLETE_PASTE_BYTES. */
|
|
209
|
+
private pasteBufferedBytes = 0;
|
|
210
|
+
/**
|
|
211
|
+
* Up to the last `PASTE_END_TAIL_LENGTH` characters buffered so far, carried
|
|
212
|
+
* forward so the terminator can be found even when split across chunk
|
|
213
|
+
* boundaries, without rescanning every previously buffered chunk.
|
|
214
|
+
*/
|
|
215
|
+
private pasteTail = "";
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Capability probe cache for the host internals: null until first use, then
|
|
219
|
+
* the (possibly empty) list of missing members, computed once on the first
|
|
220
|
+
* handleInput/render call. Any defect permanently DEGRADES the host to
|
|
221
|
+
* passthrough — selection features off, never a crash.
|
|
222
|
+
*/
|
|
223
|
+
private internalDefects: string[] | null = null;
|
|
224
|
+
|
|
225
|
+
private readonly editor: EditorComponent;
|
|
226
|
+
|
|
227
|
+
constructor(editor: EditorComponent) {
|
|
228
|
+
this.editor = editor;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** True once the probe found missing internals; computes and caches the probe on first call. */
|
|
232
|
+
get degraded(): boolean {
|
|
233
|
+
if (this.internalDefects === null) {
|
|
234
|
+
this.internalDefects = missingEditorInternals(this.editor);
|
|
235
|
+
}
|
|
236
|
+
return this.internalDefects.length > 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private get internals(): EditorInternals {
|
|
240
|
+
return this.editor as unknown as EditorInternals;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private get s(): EditorInternals["state"] {
|
|
244
|
+
return this.internals.state;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private cursor(): Point {
|
|
248
|
+
return { line: this.s.cursorLine, col: this.s.cursorCol };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private setCursor(p: Point): void {
|
|
252
|
+
this.s.cursorLine = p.line;
|
|
253
|
+
this.internals.setCursorCol(p.col);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Ordered (start, end) selection range, or null when no anchor is set. */
|
|
257
|
+
range(): [Point, Point] | null {
|
|
258
|
+
if (!this.anchor) return null;
|
|
259
|
+
const a = this.anchor;
|
|
260
|
+
const c = this.cursor();
|
|
261
|
+
const anchorFirst = a.line < c.line || (a.line === c.line && a.col < c.col);
|
|
262
|
+
return anchorFirst ? [a, c] : [c, a];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Number of characters covered by the active selection (0 when none). Counts line breaks a deletion would remove. */
|
|
266
|
+
selectionLength(): number {
|
|
267
|
+
const range = this.range();
|
|
268
|
+
if (!range) return 0;
|
|
269
|
+
const [start, end] = range;
|
|
270
|
+
const lines = this.s.lines;
|
|
271
|
+
if (start.line === end.line) return end.col - start.col;
|
|
272
|
+
let n = (lines[start.line] ?? "").length - start.col;
|
|
273
|
+
for (let i = start.line + 1; i < end.line; i++) n += (lines[i] ?? "").length;
|
|
274
|
+
return n + end.col + (end.line - start.line);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Selection dispatch in front of the native chain. Selection and replace
|
|
279
|
+
* keys are handled here; everything else collapses the anchor first, then
|
|
280
|
+
* behaves natively. Degraded hosts keep pure native key handling.
|
|
281
|
+
* `shortcut` probes registered extension shortcuts before a selection chord
|
|
282
|
+
* is consumed; it returns true when a shortcut handled the key.
|
|
283
|
+
*/
|
|
284
|
+
handleInput(data: string, native: (data: string) => void, shortcut?: (data: string) => boolean): void {
|
|
285
|
+
if (this.degraded) {
|
|
286
|
+
native(data);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (this.pasteChunks !== undefined || (this.anchor && data.startsWith(PASTE_START))) {
|
|
290
|
+
const priorLength = this.pasteBufferedLength;
|
|
291
|
+
const window = this.pasteTail + data;
|
|
292
|
+
const idx = window.indexOf(PASTE_END);
|
|
293
|
+
const chunks = (this.pasteChunks ??= []);
|
|
294
|
+
chunks.push(data);
|
|
295
|
+
this.pasteBufferedLength = priorLength + data.length;
|
|
296
|
+
if (idx < 0) {
|
|
297
|
+
this.pasteBufferedBytes += Buffer.byteLength(data, "utf8");
|
|
298
|
+
if (this.pasteBufferedBytes > MAX_INCOMPLETE_PASTE_BYTES) {
|
|
299
|
+
// Never seen a terminator and the buffered frame outgrew the safety
|
|
300
|
+
// bound: abandon it. The host editor started its own paste buffer at
|
|
301
|
+
// PASTE_START and is still waiting for PASTE_END, so forwarding the
|
|
302
|
+
// incomplete bytes to `native` could let it swallow later keystrokes
|
|
303
|
+
// — drop them instead of forwarding anything.
|
|
304
|
+
this.pasteChunks = undefined;
|
|
305
|
+
this.pasteBufferedLength = 0;
|
|
306
|
+
this.pasteBufferedBytes = 0;
|
|
307
|
+
this.pasteTail = "";
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.pasteTail = window.slice(-PASTE_END_TAIL_LENGTH);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const frameEnd = priorLength - this.pasteTail.length + idx + PASTE_END.length;
|
|
314
|
+
const buffered = chunks.join("");
|
|
315
|
+
const frame = buffered.slice(0, frameEnd);
|
|
316
|
+
const remaining = buffered.slice(frameEnd);
|
|
317
|
+
this.pasteChunks = undefined;
|
|
318
|
+
this.pasteBufferedLength = 0;
|
|
319
|
+
this.pasteBufferedBytes = 0;
|
|
320
|
+
this.pasteTail = "";
|
|
321
|
+
this.replaceSelectionWithPaste(frame, native);
|
|
322
|
+
if (remaining) this.handleInput(remaining, native, shortcut);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Kitty flag 2 release byte strings still match their own key, so
|
|
326
|
+
// releases must be dropped before any matchesKey.
|
|
327
|
+
if (isKeyRelease(data)) return;
|
|
328
|
+
if (this.isSelectionChord(data) && shortcut?.(data)) return;
|
|
329
|
+
if (matchesKey(data, "shift+home")) {
|
|
330
|
+
this.selectToLineEdge(false);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (matchesKey(data, "shift+end")) {
|
|
334
|
+
this.selectToLineEdge(true);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (matchesKey(data, "alt+e") || matchesKey(data, "alt+a")) {
|
|
338
|
+
this.selectAll();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (this.anchor) {
|
|
343
|
+
if (this.isReplaceKey(data)) {
|
|
344
|
+
const replaced = this.replaceSelection(data);
|
|
345
|
+
if (replaced) return;
|
|
346
|
+
// Selection collapsed to empty (anchor met cursor): native key behavior.
|
|
347
|
+
this.anchor = null;
|
|
348
|
+
native(data);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Movement, enter, history, kill/yank, app shortcuts: collapse first, then native behavior.
|
|
352
|
+
this.anchor = null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
native(data);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Let Pi normalize/register paste and take its one undo snapshot, then remove
|
|
359
|
+
* the selected text around the inserted text or marker in that transaction. */
|
|
360
|
+
private replaceSelectionWithPaste(frame: string, native: (data: string) => void): void {
|
|
361
|
+
const range = this.range();
|
|
362
|
+
this.anchor = null;
|
|
363
|
+
if (!range || frame === PASTE_START + PASTE_END) return native(frame);
|
|
364
|
+
const lines = this.s.lines;
|
|
365
|
+
const before = lines.join("\n");
|
|
366
|
+
const offset = (p: Point): number => lines.slice(0, p.line).reduce((n, line) => n + line.length + 1, 0) + p.col;
|
|
367
|
+
const [start, end] = range.map(offset);
|
|
368
|
+
if (start === end) return native(frame);
|
|
369
|
+
const cursor = offset(this.cursor());
|
|
370
|
+
native(frame);
|
|
371
|
+
const after = this.s.lines.join("\n");
|
|
372
|
+
const prefix = before.slice(0, cursor);
|
|
373
|
+
const suffix = before.slice(cursor);
|
|
374
|
+
// If Pi did not perform a plain insertion, retain its native result.
|
|
375
|
+
if (!after.startsWith(prefix) || !after.endsWith(suffix)) return;
|
|
376
|
+
const inserted = after.slice(prefix.length, after.length - suffix.length);
|
|
377
|
+
if (!inserted) return;
|
|
378
|
+
const result = before.slice(0, start) + inserted + before.slice(end);
|
|
379
|
+
this.s.lines = result.split("\n");
|
|
380
|
+
const caret = result.slice(0, start + inserted.length).split("\n");
|
|
381
|
+
this.s.cursorLine = caret.length - 1;
|
|
382
|
+
this.internals.setCursorCol(caret.at(-1)!.length);
|
|
383
|
+
this.internals.lastAction = null;
|
|
384
|
+
this.editor.onChange?.(this.editor.getText());
|
|
385
|
+
this.internals.tui.requestRender();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Keys whose native effect replaces a selection: backspace/delete (and shift variants) or a printable character. */
|
|
389
|
+
private isReplaceKey(data: string): boolean {
|
|
390
|
+
return (
|
|
391
|
+
matchesKey(data, "backspace") ||
|
|
392
|
+
matchesKey(data, "shift+backspace") ||
|
|
393
|
+
matchesKey(data, "delete") ||
|
|
394
|
+
matchesKey(data, "shift+delete") ||
|
|
395
|
+
this.insertsCharacter(data)
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* True when the input inserts a text character (Kitty/CSI-u and
|
|
401
|
+
* modify-other-keys aware, plus raw terminal bytes). The raw fallback
|
|
402
|
+
* accepts at most 4 UTF-16 code units with no control bytes; DEL and C1
|
|
403
|
+
* controls must not count as printable (the editor routes them to
|
|
404
|
+
* delete/other actions before its printable fallback, so re-submitting
|
|
405
|
+
* them after a splice would double-edit).
|
|
406
|
+
*/
|
|
407
|
+
private insertsCharacter(data: string): boolean {
|
|
408
|
+
if (decodePrintableKey(data) !== undefined) return true;
|
|
409
|
+
if (data.length > 4) return false;
|
|
410
|
+
for (let i = 0; i < data.length; i++) {
|
|
411
|
+
const c = data.charCodeAt(i);
|
|
412
|
+
if (c < 32 || c === 127 || (c >= 0x80 && c <= 0x9f)) return false;
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private selectToLineEdge(toEnd: boolean): void {
|
|
418
|
+
const before = this.cursor();
|
|
419
|
+
if (toEnd) this.internals.moveToLineEnd();
|
|
420
|
+
else this.internals.moveToLineStart();
|
|
421
|
+
this.internals.exitHistoryBrowsing();
|
|
422
|
+
// Repeated or held presses at the edge KEEP the selection: legacy
|
|
423
|
+
// terminals repeat the press byte-identically. Only a zero-width span
|
|
424
|
+
// (cursor landed exactly on the anchor) collapses.
|
|
425
|
+
this.anchor ??= before;
|
|
426
|
+
if (this.anchor.line === this.s.cursorLine && this.anchor.col === this.s.cursorCol) {
|
|
427
|
+
this.anchor = null;
|
|
428
|
+
}
|
|
429
|
+
if (this.internals.autocompleteState) this.internals.updateAutocomplete();
|
|
430
|
+
this.internals.tui.requestRender();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Chords the engine itself consumes; extension shortcuts are probed first. */
|
|
434
|
+
private isSelectionChord(data: string): boolean {
|
|
435
|
+
return matchesKey(data, "shift+home") || matchesKey(data, "shift+end")
|
|
436
|
+
|| matchesKey(data, "alt+e") || matchesKey(data, "alt+a");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Select the entire editor text (alt+e / alt+a). Cursor moves to the end of the last line. */
|
|
440
|
+
private selectAll(): void {
|
|
441
|
+
const lines = this.s.lines;
|
|
442
|
+
const lastLine = Math.max(0, lines.length - 1);
|
|
443
|
+
this.anchor = { line: 0, col: 0 };
|
|
444
|
+
this.s.cursorLine = lastLine;
|
|
445
|
+
this.internals.setCursorCol((lines[lastLine] ?? "").length);
|
|
446
|
+
this.internals.lastAction = null;
|
|
447
|
+
this.internals.exitHistoryBrowsing();
|
|
448
|
+
if (this.internals.autocompleteState) this.internals.cancelAutocomplete();
|
|
449
|
+
this.internals.tui.requestRender();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Replace the active selection in ONE atomic edit (delete, or delete +
|
|
454
|
+
* printable character): a single undo snapshot before any mutation, then
|
|
455
|
+
* one splice. Returns false when the span is empty; the caller falls back
|
|
456
|
+
* to native key behavior.
|
|
457
|
+
*/
|
|
458
|
+
private replaceSelection(data: string): boolean {
|
|
459
|
+
const range = this.range();
|
|
460
|
+
if (!range) return false;
|
|
461
|
+
const [start, end] = range;
|
|
462
|
+
if (start.line === end.line && start.col === end.col) return false;
|
|
463
|
+
const resolved = decodePrintableKey(data) ?? (this.insertsCharacter(data) ? data : undefined);
|
|
464
|
+
const cp = resolved?.codePointAt(0) ?? 0;
|
|
465
|
+
const char = cp === 127 || (cp >= 0x80 && cp <= 0x9f) ? undefined : resolved;
|
|
466
|
+
const internals = this.internals;
|
|
467
|
+
const lines = internals.state.lines;
|
|
468
|
+
internals.pushUndoSnapshot();
|
|
469
|
+
const merged =
|
|
470
|
+
(lines[start.line] ?? "").slice(0, start.col) + (char ?? "") + (lines[end.line] ?? "").slice(end.col);
|
|
471
|
+
lines.splice(start.line, end.line - start.line + 1, merged);
|
|
472
|
+
this.anchor = null;
|
|
473
|
+
this.setCursor({ line: start.line, col: start.col + (char?.length ?? 0) });
|
|
474
|
+
internals.exitHistoryBrowsing();
|
|
475
|
+
internals.lastAction = null;
|
|
476
|
+
if (internals.autocompleteState) internals.cancelAutocomplete();
|
|
477
|
+
this.editor.onChange?.(this.editor.getText());
|
|
478
|
+
internals.tui.requestRender();
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Render post-pass: wrap the selected span of each visible row in reverse
|
|
484
|
+
* video. `inset` is the number of columns the host render adds on EACH side
|
|
485
|
+
* before the editor's own content (0 for plain content rows, 1 for a
|
|
486
|
+
* one-column frame wall); `width` is the width the content was rendered at.
|
|
487
|
+
*/
|
|
488
|
+
decorateRows(rows: string[], width: number, inset: number): string[] {
|
|
489
|
+
const range = this.range();
|
|
490
|
+
if (!range) return rows;
|
|
491
|
+
const internals = this.internals;
|
|
492
|
+
const [start, end] = range;
|
|
493
|
+
const contentWidth = Math.max(1, width - inset * 2 - internals.paddingX * 2);
|
|
494
|
+
const layoutWidth = internals.lastWidth || Math.max(1, contentWidth - (internals.paddingX ? 0 : 1));
|
|
495
|
+
const visual = internals.buildVisualLineMap(layoutWidth);
|
|
496
|
+
for (let r = 0; r < internals.renderedVisibleLineCount; r++) {
|
|
497
|
+
const vr = visual[internals.scrollOffset + r];
|
|
498
|
+
if (!vr || vr.logicalLine < start.line || vr.logicalLine > end.line) continue;
|
|
499
|
+
const from =
|
|
500
|
+
clamp(vr.logicalLine === start.line ? start.col - vr.startCol : 0, 0, vr.length) +
|
|
501
|
+
internals.paddingX +
|
|
502
|
+
inset;
|
|
503
|
+
const to =
|
|
504
|
+
clamp(vr.logicalLine === end.line ? end.col - vr.startCol : vr.length, 0, vr.length) +
|
|
505
|
+
internals.paddingX +
|
|
506
|
+
inset;
|
|
507
|
+
if (to <= from) continue;
|
|
508
|
+
const index = 1 + r; // rows[0] is the top border/rule in every layout
|
|
509
|
+
if (index < rows.length) rows[index] = withReverseSpan(rows[index] ?? "", from, to);
|
|
510
|
+
}
|
|
511
|
+
return rows;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Bottom-rule selection hint. `corner` re-attaches the host frame's right
|
|
516
|
+
* corner glyph after the label. Widths always add up: the rule is truncated
|
|
517
|
+
* to make exact room for label + corner.
|
|
518
|
+
*/
|
|
519
|
+
decorateBottomRule(baseRow: string, width: number, corner: string): string {
|
|
520
|
+
const n = this.selectionLength();
|
|
521
|
+
if (n <= 0) return baseRow;
|
|
522
|
+
const label = ` ${n} char${n === 1 ? "" : "s"} selected - Del deletes - Alt+a select all `;
|
|
523
|
+
const labelWidth = visibleWidth(label);
|
|
524
|
+
if (labelWidth + visibleWidth(corner) + 1 >= width) return baseRow;
|
|
525
|
+
return `${truncateToWidth(baseRow, width - labelWidth - visibleWidth(corner), "")}${label}${corner}`;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
@@ -5,7 +5,7 @@ import { dirname, relative, resolve } from "node:path";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktree-registry.ts";
|
|
7
7
|
import { SessionChanges, readChangeSnapshot, sameSnapshot, textSnapshot, isSessionChangeEvidence,
|
|
8
|
-
SESSION_CHANGE_ENTRY,
|
|
8
|
+
SESSION_CHANGE_ENTRY, SESSION_CHANGE_RELAY, type SessionChangeEvidence, type ChangeSnapshot } from "./session-changes.ts";
|
|
9
9
|
|
|
10
10
|
const foreignPublishers = new WeakMap<ExtensionAPI, (sessionId: string, evidence: SessionChangeEvidence) => void>();
|
|
11
11
|
/** Internal authenticated handoff from a successful child tool callback, never a model message or public event. */
|
|
@@ -17,7 +17,10 @@ interface Pending { sessionId: string; inputPath: string; path: string; root: st
|
|
|
17
17
|
const normalized = (text: string) => text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
18
18
|
const unknown = (): ChangeSnapshot => ({ kind: "unavailable", reason: "Tool snapshots could not be verified; diff unavailable." });
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Observe explicit write/edit outcomes, never infer ownership from Git status or shell text.
|
|
22
|
+
* The evidence feeds session statistics and names the roots /gentle:changes reads from Git.
|
|
23
|
+
*/
|
|
21
24
|
export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env, resolver: WorktreeResolver = resolveSessionWorktree): void {
|
|
22
25
|
const child = env.GENTLE_PI_AGENTS_CHILD === "1";
|
|
23
26
|
const pending = new Map<string, Pending>();
|
|
@@ -26,8 +29,6 @@ export function installSessionChangeCapture(pi: ExtensionAPI, env: NodeJS.Proces
|
|
|
26
29
|
const publish = (evidence: SessionChangeEvidence) => {
|
|
27
30
|
if (!current || !store || current.sessionManager.getSessionId() !== store.sessionId) return;
|
|
28
31
|
if (store.record(evidence)) pi.appendEntry(SESSION_CHANGE_ENTRY, { sessionId: store.sessionId, evidence });
|
|
29
|
-
const notice = store.takeNotice();
|
|
30
|
-
pi.events.emit(SESSION_CHANGE_EVENT, { sessionId: store.sessionId, ...(notice ? { notice } : {}) });
|
|
31
32
|
};
|
|
32
33
|
foreignPublishers.set(pi, (sessionId, evidence) => {
|
|
33
34
|
if (!child && current?.sessionManager.getSessionId() === sessionId && isSessionChangeEvidence(evidence)) publish(evidence);
|