gentle-pi 4.0.0 → 4.2.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 +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- 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 +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- 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 +29 -25
- 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/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- 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/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -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 +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- 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/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 +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- 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.ts +1 -1
- 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/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- 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-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- 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 +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- 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 +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- 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 +7 -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 +79 -32
- 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/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- 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 +52 -1
- 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 +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -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 +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- 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 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- 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-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- 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 +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- 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 +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -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-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- 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 +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- 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 +6 -5
- 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/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- 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 +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- 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 +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -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 +5 -4
- 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-risk-assessment.test.ts +109 -33
- 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-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- 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-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- 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 +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -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 +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -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 +27 -1
- package/tests/test-strength-contract.test.ts +124 -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.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { TuiAltScreen, visibleWidth, type Terminal, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
4
|
-
import { CHANGE_STATUS, changesModel, type ChangedFile } from "../lib/shell-changes.ts";
|
|
4
|
+
import { CHANGE_STATUS, DIFF_TOO_LARGE, changesModel, type ChangedFile } from "../lib/shell-changes.ts";
|
|
5
5
|
import { WorktreeChangesView, ChangesView, colorDiff, type ChangesViewDeps } from "../lib/shell-changes-view.ts";
|
|
6
6
|
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
7
7
|
|
|
@@ -106,8 +106,8 @@ test("worktree accordion keeps groups and nested files beside a framed lazy diff
|
|
|
106
106
|
const lines = component.render(100);
|
|
107
107
|
assert.equal(lines.length, 10);
|
|
108
108
|
assert.match(lines[0], /^╭─ ✎ Changes/);
|
|
109
|
-
assert.match(lines[1], /^│ ▾ main · main +│
|
|
110
|
-
assert.match(lines[2], /^│ ▸ M same.ts +\+1 -0
|
|
109
|
+
assert.match(lines[1], /^│ ▾ main · main +│ M same.ts +│$/);
|
|
110
|
+
assert.match(lines[2], /^│ ▸ M same.ts +\+1 -0 +│ \+\/main:same.ts +│$/);
|
|
111
111
|
assert.match(lines[3], /^│ ▸ detached · linked +│/);
|
|
112
112
|
assert.match(lines[9], /^╰─+╯$/);
|
|
113
113
|
for (const line of lines) assert.equal(visibleWidth(line), 100);
|
|
@@ -131,6 +131,34 @@ test("worktree accordion keeps groups and nested files beside a framed lazy diff
|
|
|
131
131
|
assert.equal(closed, 1);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
// Long paths were clipped to `tools/linkedin-automation/scr…`, hiding the file
|
|
135
|
+
// name. Rows lead with the name and trail the directory; the diff pane shows
|
|
136
|
+
// the full path, clipped from the left so the name always survives.
|
|
137
|
+
test("changes rows lead with the file name and the diff pane shows the full path", async () => {
|
|
138
|
+
const path = "tools/linkedin-automation/scripts/connect.mjs";
|
|
139
|
+
const trees = [{ root: "/main", branch: "feat/linkedin", model: changesModel([file(path, 214, 0, CHANGE_STATUS.ADDED)]) }];
|
|
140
|
+
const component = new WorktreeChangesView(trees, {
|
|
141
|
+
theme: plainTheme, rows: 10,
|
|
142
|
+
loadDiff: async () => "@@ -0,0 +1,1 @@\n+# Scheduled",
|
|
143
|
+
onOpen: () => {}, onClose: () => {}, requestRender() {}, onRefresh() {},
|
|
144
|
+
});
|
|
145
|
+
component.handleInput("\r");
|
|
146
|
+
component.handleInput("j");
|
|
147
|
+
await settle();
|
|
148
|
+
const lines = component.render(120);
|
|
149
|
+
assert.match(lines[2]!, /^│ ▸ A connect\.mjs \+214 -0 tools/, "the file name leads the row");
|
|
150
|
+
assert.match(lines[1]!, /│ A tools\/linkedin-automation\/scripts\/connect\.mjs +│$/, "the diff pane starts with the full path");
|
|
151
|
+
assert.match(lines[2]!, /│ @@ -0,0 \+1,1 @@ +│$/, "the diff follows the path line");
|
|
152
|
+
assert.match(component.render(70)[1]!, /│ A …[^│]*\/connect\.mjs *│$/, "a narrow pane clips the path from the left");
|
|
153
|
+
|
|
154
|
+
const standalone = view({ loadDiff: async () => "+x" }, [file(path, 1, 0, CHANGE_STATUS.ADDED)]).view;
|
|
155
|
+
await settle();
|
|
156
|
+
const single = standalone.render(120);
|
|
157
|
+
assert.match(single[1]!, /│ ▸ A connect\.mjs \+1 -0 tools/);
|
|
158
|
+
assert.match(single[1]!, /│ A tools\/linkedin-automation\/scripts\/connect\.mjs +│$/);
|
|
159
|
+
assert.match(single[2]!, /│ \+x +│$/);
|
|
160
|
+
});
|
|
161
|
+
|
|
134
162
|
test("worktree accordion selects a clicked file without opening its editor", () => {
|
|
135
163
|
const opened: string[] = [];
|
|
136
164
|
const component = new WorktreeChangesView([{ root: "/main", branch: "main", model: changesModel([file("a.ts", 1, 0), file("b.ts", 1, 0)]) }], {
|
|
@@ -147,9 +175,10 @@ test("worktree accordion selects a clicked file without opening its editor", ()
|
|
|
147
175
|
|
|
148
176
|
test("worktree accordion receives native fullscreen press and release as a file click", async () => {
|
|
149
177
|
let onInput: ((data: string) => void) | undefined;
|
|
150
|
-
const
|
|
151
|
-
start(input) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 8; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {},
|
|
178
|
+
const terminalFixture = {
|
|
179
|
+
start(input: Parameters<Terminal["start"]>[0]) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 8; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {}, setProgramStatus() {},
|
|
152
180
|
};
|
|
181
|
+
const terminal: Terminal = terminalFixture;
|
|
153
182
|
const opened: string[] = [];
|
|
154
183
|
const component = new WorktreeChangesView([{ root: "/main", branch: "main", model: changesModel([file("a.ts", 1, 0), file("b.ts", 1, 0)]) }], {
|
|
155
184
|
theme: plainTheme, rows: 8, loadDiff: async () => "+preview",
|
|
@@ -297,9 +326,10 @@ test("non-left gestures pass through with current, stale, and missing layouts in
|
|
|
297
326
|
test("right press reaches the native Windows paste fallback when eligible", () => {
|
|
298
327
|
let onInput: ((data: string) => void) | undefined;
|
|
299
328
|
let pasted = 0;
|
|
300
|
-
const
|
|
301
|
-
start(input) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 12; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {},
|
|
329
|
+
const terminalFixture = {
|
|
330
|
+
start(input: Parameters<Terminal["start"]>[0]) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 12; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {}, setProgramStatus() {},
|
|
302
331
|
};
|
|
332
|
+
const terminal: Terminal = terminalFixture;
|
|
303
333
|
const { view: component } = view();
|
|
304
334
|
const tui = new TuiAltScreen(terminal, false, undefined, { mouse: true, onRightClickPaste: () => { pasted++; } });
|
|
305
335
|
const platform = Object.getOwnPropertyDescriptor(process, "platform")!;
|
|
@@ -413,9 +443,10 @@ test("accordion selection scrolls flattened rows and diff scrolling does not ope
|
|
|
413
443
|
await settle();
|
|
414
444
|
assert.match(component.render(100).join("\n"), /▸ M file-19.ts/);
|
|
415
445
|
component.handleInput("\x0a");
|
|
416
|
-
assert.match(component.render(100)[1],
|
|
446
|
+
assert.match(component.render(100)[1], /M file-19\.ts/, "the path line stays fixed while the diff scrolls");
|
|
447
|
+
assert.match(component.render(100)[2], /\+line 4/, "a page is the diff rows below the path line");
|
|
417
448
|
component.handleInput("\x0b");
|
|
418
|
-
assert.match(component.render(100)[
|
|
449
|
+
assert.match(component.render(100)[2], /\+line 0/);
|
|
419
450
|
assert.equal(opened, 0);
|
|
420
451
|
for (const width of [1, 8, 20, 40, 100]) {
|
|
421
452
|
for (const line of component.render(width)) assert.ok(visibleWidth(line) <= width);
|
|
@@ -502,8 +533,9 @@ test("ChangesView renders a framed two-pane layout at the requested size", async
|
|
|
502
533
|
for (const line of lines) assert.equal(visibleWidth(line), 80, `"${stripAnsi(line)}" is not 80 wide`);
|
|
503
534
|
const plain = lines.map(stripAnsi);
|
|
504
535
|
assert.match(plain[0], /^╭─ ✎ Changes · 2 files · \+12 −1 ─+╮$/);
|
|
505
|
-
assert.match(plain[1], /^│ ▸ M
|
|
506
|
-
assert.match(plain[2], /^│ A
|
|
536
|
+
assert.match(plain[1], /^│ ▸ M a\.ts \+2 -1 lib +│ M lib\/a\.ts +│$/);
|
|
537
|
+
assert.match(plain[2], /^│ A b\.ts \+10 -0 lib +│ @@ -1,2 \+1,3 @@ +│$/);
|
|
538
|
+
assert.match(plain[3], /^│ +│ const a = 1; +│$/);
|
|
507
539
|
assert.match(plain[11], /^╰─+╯$/);
|
|
508
540
|
assert.match(plain[10], /j\/k file .* o open in editor .* esc close/);
|
|
509
541
|
});
|
|
@@ -532,8 +564,9 @@ test("ChangesView renders tabbed CRLF modified diffs with aligned frame columns"
|
|
|
532
564
|
assert.ok(!line.includes("\t"), "raw tab must not leak into rendered line");
|
|
533
565
|
}
|
|
534
566
|
const plain = lines.map(stripAnsi);
|
|
535
|
-
assert.match(plain[1], /^│ ▸ M
|
|
536
|
-
assert.match(plain[2], /^│ A
|
|
567
|
+
assert.match(plain[1], /^│ ▸ M a\.ts \+2 -1 lib +│ M lib\/a\.ts +│$/);
|
|
568
|
+
assert.match(plain[2], /^│ A b\.ts \+10 -0 lib +│ @@ -1,2 \+1,3 @@ +│$/);
|
|
569
|
+
assert.match(plain[3], /^│ +│ +const a = 1; +│$/);
|
|
537
570
|
});
|
|
538
571
|
|
|
539
572
|
test("ChangesView loads the selected diff lazily and moves with j/k and arrows", async () => {
|
|
@@ -543,11 +576,11 @@ test("ChangesView loads the selected diff lazily and moves with j/k and arrows",
|
|
|
543
576
|
component.handleInput("j");
|
|
544
577
|
await settle();
|
|
545
578
|
assert.deepEqual(calls, ["lib/a.ts", "lib/b.ts"]);
|
|
546
|
-
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A
|
|
579
|
+
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
|
|
547
580
|
component.handleInput("\x1b[A");
|
|
548
|
-
assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M
|
|
581
|
+
assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M a\.ts/);
|
|
549
582
|
component.handleInput("k");
|
|
550
|
-
assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M
|
|
583
|
+
assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M a\.ts/);
|
|
551
584
|
assert.ok(events.filter((event) => event === "render").length >= 2);
|
|
552
585
|
});
|
|
553
586
|
|
|
@@ -557,16 +590,33 @@ test("ChangesView scrolls the diff pane and shows an empty state for files witho
|
|
|
557
590
|
await settle();
|
|
558
591
|
component.handleInput("\x1b[6~");
|
|
559
592
|
const plain = component.render(80).map(stripAnsi);
|
|
560
|
-
assert.doesNotMatch(plain[
|
|
561
|
-
assert.match(plain[
|
|
593
|
+
assert.doesNotMatch(plain[2], /@@/);
|
|
594
|
+
assert.match(plain[2], /\+line \d+/);
|
|
562
595
|
component.handleInput("\x0b");
|
|
563
|
-
assert.match(stripAnsi(component.render(80)[
|
|
596
|
+
assert.match(stripAnsi(component.render(80)[2]), /@@/, "ctrl+k scrolls back to the top");
|
|
564
597
|
component.handleInput("\x0a");
|
|
565
|
-
assert.doesNotMatch(stripAnsi(component.render(80)[
|
|
598
|
+
assert.doesNotMatch(stripAnsi(component.render(80)[2]), /@@/, "ctrl+j scrolls a page down");
|
|
566
599
|
|
|
567
600
|
const empty = view({ async loadDiff() { return ""; } });
|
|
568
601
|
await settle();
|
|
569
|
-
assert.match(stripAnsi(empty.view.render(80)[
|
|
602
|
+
assert.match(stripAnsi(empty.view.render(80)[2]), /no diff for this file/);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
test("ChangesView shows an oversized diff as one plain open-in-editor line and stays responsive", async () => {
|
|
606
|
+
const tagged = view({ theme: taggedTheme, async loadDiff() { return DIFF_TOO_LARGE; } }).view;
|
|
607
|
+
await settle();
|
|
608
|
+
const rendered = tagged.render(100).join("\n");
|
|
609
|
+
assert.match(rendered, /<dim>Diff too large/, "one plain line instead of a colored diff");
|
|
610
|
+
assert.doesNotMatch(rendered, /<toolDiff\w+>Diff too large/);
|
|
611
|
+
const { view: component, events } = view({ async loadDiff(target) { return target.path === "lib/a.ts" ? DIFF_TOO_LARGE : DIFF_A; } });
|
|
612
|
+
await settle();
|
|
613
|
+
assert.match(stripAnsi(component.render(140).join("\n")), /Diff too large — press o to open in editor\./);
|
|
614
|
+
component.handleInput("j");
|
|
615
|
+
await settle();
|
|
616
|
+
assert.match(stripAnsi(component.render(140).join("\n")), /\+const c = 4;/);
|
|
617
|
+
component.handleInput("k");
|
|
618
|
+
component.handleInput("o");
|
|
619
|
+
assert.ok(events.includes("open:lib/a.ts"));
|
|
570
620
|
});
|
|
571
621
|
|
|
572
622
|
test("ChangesView opens the selected file and closes on escape or q", async () => {
|
|
@@ -589,7 +639,7 @@ test("ChangesView paints the shared hover role over an unselected file row, neve
|
|
|
589
639
|
// Row y=1 is the selected file (lib/a.ts, index 0); row y=2 is lib/b.ts.
|
|
590
640
|
const hoverOther = component.handleMouse(mouseButton("move", "none", 3, 2, 12));
|
|
591
641
|
assert.deepEqual(hoverOther, { handled: true, render: true });
|
|
592
|
-
assert.match(component.render(80)[2]!, /<warning>
|
|
642
|
+
assert.match(component.render(80)[2]!, /<warning>A b\.ts/, "the hovered, unselected row paints the shared hover role");
|
|
593
643
|
assert.doesNotMatch(component.render(80)[1]!, /<warning>/, "the selected row keeps its own role instead");
|
|
594
644
|
|
|
595
645
|
const hoverSelected = component.handleMouse(mouseButton("move", "none", 3, 1, 12));
|
|
@@ -610,15 +660,16 @@ test("ChangesView click selects a file without opening it", async () => {
|
|
|
610
660
|
component.handleMouse(mouse("release", 3, 2));
|
|
611
661
|
component.handleMouse(mouse("click", 3, 2));
|
|
612
662
|
await settle();
|
|
613
|
-
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A
|
|
663
|
+
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
|
|
614
664
|
assert.deepEqual(events.filter((event) => event.startsWith("open:")), []);
|
|
615
665
|
});
|
|
616
666
|
|
|
617
667
|
test("ChangesView receives native fullscreen press and release as a click", async () => {
|
|
618
668
|
let onInput: ((data: string) => void) | undefined;
|
|
619
|
-
const
|
|
620
|
-
start(input) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 12; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {},
|
|
669
|
+
const terminalFixture = {
|
|
670
|
+
start(input: Parameters<Terminal["start"]>[0]) { onInput = input; }, stop() {}, async drainInput() {}, write() {}, get columns() { return 80; }, get rows() { return 12; }, get kittyProtocolActive() { return false; }, moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {}, setProgramStatus() {},
|
|
621
671
|
};
|
|
672
|
+
const terminal: Terminal = terminalFixture;
|
|
622
673
|
const { view: component, events } = view();
|
|
623
674
|
const tui = new TuiAltScreen(terminal, false, undefined, { mouse: true });
|
|
624
675
|
tui.setLayoutRoot(component);
|
|
@@ -627,7 +678,7 @@ test("ChangesView receives native fullscreen press and release as a click", asyn
|
|
|
627
678
|
try {
|
|
628
679
|
onInput?.("\x1b[<0;4;3M");
|
|
629
680
|
onInput?.("\x1b[<0;4;3m");
|
|
630
|
-
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A
|
|
681
|
+
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
|
|
631
682
|
assert.deepEqual(events.filter((event) => event.startsWith("open:")), []);
|
|
632
683
|
} finally {
|
|
633
684
|
tui.stop();
|
|
@@ -643,7 +694,7 @@ test("ChangesView rebuilds pointer geometry when its overlay height changes", as
|
|
|
643
694
|
assert.deepEqual(component.handleMouse(mouse("click", 3, 2, undefined, 8)), { handled: true, render: false });
|
|
644
695
|
component.render(80);
|
|
645
696
|
assert.deepEqual(component.handleMouse(mouse("click", 3, 2, undefined, 8)), { handled: true, render: true });
|
|
646
|
-
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A
|
|
697
|
+
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
|
|
647
698
|
});
|
|
648
699
|
|
|
649
700
|
test("ChangesView scrolls the file list and diff independently, ignores hover, and clears stale pointer layouts", async () => {
|
|
@@ -653,15 +704,15 @@ test("ChangesView scrolls the file list and diff independently, ignores hover, a
|
|
|
653
704
|
await settle();
|
|
654
705
|
component.render(80);
|
|
655
706
|
component.handleMouse(mouse("move", 3, 2, undefined, 8));
|
|
656
|
-
assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M
|
|
707
|
+
assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M 0\.ts/);
|
|
657
708
|
assert.deepEqual(events.filter((event) => event.startsWith("open:")), []);
|
|
658
709
|
|
|
659
710
|
assert.deepEqual(component.handleMouse(mouse("wheel", 3, 1, 2, 8)), { handled: true, render: true });
|
|
660
|
-
assert.match(stripAnsi(component.render(80)[1]), /^│ M
|
|
711
|
+
assert.match(stripAnsi(component.render(80)[1]), /^│ M 2\.ts/);
|
|
661
712
|
component.update(changesModel(files));
|
|
662
|
-
assert.match(stripAnsi(component.render(80)[1]), /^│ M
|
|
713
|
+
assert.match(stripAnsi(component.render(80)[1]), /^│ M 2\.ts/, "an unchanged live refresh preserves the independently scrolled file viewport");
|
|
663
714
|
assert.deepEqual(component.handleMouse(mouse("wheel", 50, 1, 2, 8)), { handled: true, render: true });
|
|
664
|
-
assert.doesNotMatch(stripAnsi(component.render(80)[
|
|
715
|
+
assert.doesNotMatch(stripAnsi(component.render(80)[2]), /@@/);
|
|
665
716
|
assert.deepEqual(component.handleMouse(mouse("wheel", 50, 1, -100, 8)), { handled: true, render: true });
|
|
666
717
|
assert.deepEqual(component.handleMouse(mouse("wheel", 50, 1, -1, 8)), { handled: true, render: false });
|
|
667
718
|
|
|
@@ -679,9 +730,9 @@ test("ChangesView keeps keyboard selection visible after file-list scrolling", a
|
|
|
679
730
|
component.render(80);
|
|
680
731
|
component.handleMouse(mouse("wheel", 3, 1, 2, 8));
|
|
681
732
|
component.handleInput("j");
|
|
682
|
-
assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M
|
|
733
|
+
assert.match(stripAnsi(component.render(80)[1]), /^│ ▸ M 1\.ts/);
|
|
683
734
|
component.update(changesModel(files.slice(0, 3)));
|
|
684
|
-
assert.match(stripAnsi(component.render(80)[1]), /^│ M
|
|
735
|
+
assert.match(stripAnsi(component.render(80)[1]), /^│ M 0\.ts/);
|
|
685
736
|
});
|
|
686
737
|
|
|
687
738
|
test("ChangesView.update keeps the selected file, reloads moved diffs, and survives an empty tree", async () => {
|
|
@@ -693,7 +744,7 @@ test("ChangesView.update keeps the selected file, reloads moved diffs, and survi
|
|
|
693
744
|
|
|
694
745
|
component.update(changesModel([file("lib/a.ts", 5, 1), file("lib/b.ts", 10, 0, CHANGE_STATUS.ADDED), file("lib/c.ts", 1, 0)]));
|
|
695
746
|
await settle();
|
|
696
|
-
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A
|
|
747
|
+
assert.match(stripAnsi(component.render(80)[2]), /^│ ▸ A b\.ts/);
|
|
697
748
|
assert.deepEqual(calls, ["lib/a.ts", "lib/b.ts"], "unchanged selected file must not reload");
|
|
698
749
|
component.handleInput("k");
|
|
699
750
|
await settle();
|
|
@@ -9,8 +9,8 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
9
9
|
import {
|
|
10
10
|
CHANGE_STATUS,
|
|
11
11
|
ChangesTracker,
|
|
12
|
+
FILE_WORK_CONCURRENCY,
|
|
12
13
|
WorktreeChangesTracker,
|
|
13
|
-
RootBranchLabels,
|
|
14
14
|
UNKNOWN_BRANCH,
|
|
15
15
|
foreignRootBranch,
|
|
16
16
|
parseWorktrees,
|
|
@@ -112,14 +112,6 @@ test("changesSummary and the widget describe the session at a glance", () => {
|
|
|
112
112
|
assert.equal(visibleWidth(line), 120, "the command sits on the right edge");
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
test("changesSummary and the widget surface the capture limit", () => {
|
|
116
|
-
const model = changesModel([file("a.ts", 1, 0)]);
|
|
117
|
-
model.notice = "Session change capture limit reached; additional changes are not displayed.";
|
|
118
|
-
assert.equal(changesSummary(model), "1 file · +1 −0 · capture limit reached");
|
|
119
|
-
assert.match(renderChangesWidget(model, plainTheme, 200)[0], /capture limit reached/);
|
|
120
|
-
assert.doesNotMatch(changesSummary(changesModel([file("a.ts", 1, 0)])), /capture limit reached/);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
115
|
test("renderChangesWidget colors counts by direction and yields nothing when clean", () => {
|
|
124
116
|
const model = changesModel([file("a.ts", 1, 2)]);
|
|
125
117
|
const [line] = renderChangesWidget(model, taggedTheme, 400);
|
|
@@ -167,6 +159,24 @@ test("ChangesTracker reads the working tree on start and again on refresh", asyn
|
|
|
167
159
|
assert.deepEqual(calls[1], ["status", "--porcelain=v1", "--untracked-files=all", "-z"]);
|
|
168
160
|
});
|
|
169
161
|
|
|
162
|
+
test("ChangesTracker skips untracked dependency backups but preserves tracked edits and legitimate new files", async () => {
|
|
163
|
+
const backup = "node_modules.backup-20261007-192802/pkg/index.js";
|
|
164
|
+
const nestedBackup = "packages/app/node_modules.backup-local/pkg/index.js";
|
|
165
|
+
const tracked = "node_modules.backup-local/tracked.ts";
|
|
166
|
+
const legitimate = ["src/new.ts", "node_modules.backup-notes.md", "node_modules.backup-/new.ts"];
|
|
167
|
+
const { git } = fakeGit(
|
|
168
|
+
[`4\t2\t${tracked}\n`],
|
|
169
|
+
[[`?? ${backup}`, `?? ${nestedBackup}`, ` M ${tracked}`, ...legitimate.map((path) => `?? ${path}`)].join("\0") + "\0"],
|
|
170
|
+
);
|
|
171
|
+
const counted: string[] = [];
|
|
172
|
+
const tracker = new ChangesTracker(git, async (path) => { counted.push(path); return 3; });
|
|
173
|
+
await tracker.start();
|
|
174
|
+
assert.deepEqual(counted.sort(), [...legitimate].sort());
|
|
175
|
+
assert.deepEqual(tracker.model, changesModel([
|
|
176
|
+
file(tracked, 4, 2), ...legitimate.map((path) => file(path, 3, 0, CHANGE_STATUS.UNTRACKED)),
|
|
177
|
+
]));
|
|
178
|
+
});
|
|
179
|
+
|
|
170
180
|
test("ChangesTracker stays quiet outside a git repository", async () => {
|
|
171
181
|
const { git, calls } = fakeGit([""], [""], 128);
|
|
172
182
|
const tracker = new ChangesTracker(git);
|
|
@@ -210,6 +220,7 @@ test("registered roots hide clean and missing worktrees while metadata never adm
|
|
|
210
220
|
assert.equal(tracker.model.files.length, 2, "identical filenames in different roots remain distinct");
|
|
211
221
|
assert.equal(new Set(tracker.model.files.map((file) => file.path)).size, 2);
|
|
212
222
|
assert.ok(!calls.includes("/pruned") && !calls.includes("/bare"));
|
|
223
|
+
assert.deepEqual(tracker.failedRoots, ["/gone"], "a registered root Git cannot read is a failure, not a clean root");
|
|
213
224
|
roots += "worktree /new\0branch refs/heads/new\0\0";
|
|
214
225
|
registered.push("/new");
|
|
215
226
|
await tracker.refresh();
|
|
@@ -221,6 +232,29 @@ test("registered roots hide clean and missing worktrees while metadata never adm
|
|
|
221
232
|
assert.deepEqual(tracker.model, emptyChanges());
|
|
222
233
|
});
|
|
223
234
|
|
|
235
|
+
test("a root whose Git status fails or rejects is kept apart from clean roots", async () => {
|
|
236
|
+
let broken = true;
|
|
237
|
+
const tracker = new WorktreeChangesTracker(async () => ({ stdout: "", code: 0 }), (root) => async (args) => {
|
|
238
|
+
if (broken && root === "/rejects") throw new Error("git timed out");
|
|
239
|
+
if (broken && root === "/fails" && args[0] === "status") return { stdout: "", code: 128 };
|
|
240
|
+
return { stdout: args[0] === "status" && root === "/dirty" ? "?? a.ts\0" : "", code: 0 };
|
|
241
|
+
}, undefined, () => ["/dirty", "/clean", "/fails", "/rejects"]);
|
|
242
|
+
await tracker.start();
|
|
243
|
+
assert.deepEqual(tracker.worktrees.map((tree) => tree.root), ["/dirty"]);
|
|
244
|
+
assert.deepEqual([...tracker.failedRoots].sort(), ["/fails", "/rejects"], "a failed scan is not a clean root");
|
|
245
|
+
assert.equal(tracker.scannedRootCount, 4);
|
|
246
|
+
broken = false;
|
|
247
|
+
await tracker.refresh();
|
|
248
|
+
assert.deepEqual(tracker.failedRoots, [], "a later successful scan clears the failure");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("no registered root is neither a failure nor a scan", async () => {
|
|
252
|
+
const tracker = new WorktreeChangesTracker(async () => { throw new Error("no Git expected"); }, () => async () => { throw new Error("no Git expected"); });
|
|
253
|
+
await tracker.start();
|
|
254
|
+
assert.deepEqual(tracker.failedRoots, []);
|
|
255
|
+
assert.equal(tracker.scannedRootCount, 0);
|
|
256
|
+
});
|
|
257
|
+
|
|
224
258
|
test("only registered worktrees are scanned, with whole preexisting and untracked contents", async () => {
|
|
225
259
|
const queried: string[] = [];
|
|
226
260
|
const roots = ["/used"];
|
|
@@ -244,7 +278,7 @@ test("worktree refresh shares one scan so polling faster than discovery cannot p
|
|
|
244
278
|
scans++;
|
|
245
279
|
if (scans === 2) await new Promise<void>((resolve) => { release = resolve; });
|
|
246
280
|
return { stdout: "", code: 0 };
|
|
247
|
-
}, () => async () => ({ stdout: "", code: 0 }));
|
|
281
|
+
}, () => async () => ({ stdout: "", code: 0 }), undefined, () => ["/root"]); // A root to scan; none means no Git at all.
|
|
248
282
|
await tracker.start();
|
|
249
283
|
const first = tracker.refresh();
|
|
250
284
|
const overlapping = tracker.refresh();
|
|
@@ -479,57 +513,40 @@ test("ChangesTracker counts the lines of untracked files, which git numstat leav
|
|
|
479
513
|
assert.deepEqual(counted, ["notes.md", "empty.md"]);
|
|
480
514
|
});
|
|
481
515
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
516
|
+
test("ChangesTracker counts lines and reads revisions for every file with bounded concurrency", async () => {
|
|
517
|
+
const paths = Array.from({ length: 30 }, (_, index) => `file-${String(index).padStart(2, "0")}.ts`);
|
|
518
|
+
const porcelain = [" M tracked.ts\0", ...paths.map((path) => `?? ${path}\0`)].join("");
|
|
519
|
+
const git = async (args: string[]) => ({ stdout: args[0] === "status" ? porcelain : "4\t1\ttracked.ts\n", code: 0 });
|
|
520
|
+
const pending = new Map<string, number>();
|
|
521
|
+
const counted: string[] = [];
|
|
522
|
+
let maxFiles = 0;
|
|
523
|
+
let overlapped = false;
|
|
524
|
+
// Every per-file call stays pending for a moment, so concurrent work overlaps.
|
|
525
|
+
const work = async <T>(path: string, result: () => T): Promise<T> => {
|
|
526
|
+
pending.set(path, (pending.get(path) ?? 0) + 1);
|
|
527
|
+
if (pending.get(path)! > 1) overlapped = true;
|
|
528
|
+
maxFiles = Math.max(maxFiles, pending.size);
|
|
529
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
530
|
+
const left = pending.get(path)! - 1;
|
|
531
|
+
if (left === 0) pending.delete(path); else pending.set(path, left);
|
|
532
|
+
return result();
|
|
493
533
|
};
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
assert.equal(
|
|
505
|
-
|
|
506
|
-
assert.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
assert.deepEqual(
|
|
510
|
-
assert.
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// A root that could not even be asked (removed, git unavailable, the runner
|
|
514
|
-
// itself rejecting) must read UNKNOWN_BRANCH -- never the same undefined a
|
|
515
|
-
// genuine detached HEAD produces, and never silently stay unlabelled forever.
|
|
516
|
-
test("RootBranchLabels labels a root UNKNOWN_BRANCH when it could not be asked, distinct from a real detached HEAD", async () => {
|
|
517
|
-
let changed = 0;
|
|
518
|
-
const labels = new RootBranchLabels((root) => async () => {
|
|
519
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
520
|
-
if (root === "/gone") throw new Error("root removed");
|
|
521
|
-
return { stdout: "", code: 1 };
|
|
522
|
-
}, () => changed++);
|
|
523
|
-
const trees = [
|
|
524
|
-
{ root: "/gone", model: changesModel([]) },
|
|
525
|
-
{ root: "/detached", model: changesModel([]) },
|
|
526
|
-
];
|
|
527
|
-
labels.decorate(trees);
|
|
528
|
-
await labels.settled();
|
|
529
|
-
assert.deepEqual(
|
|
530
|
-
labels.decorate(trees).map((tree) => tree.branch),
|
|
531
|
-
[UNKNOWN_BRANCH, undefined],
|
|
532
|
-
"a failed ask reads UNKNOWN_BRANCH; a real detached HEAD still reads undefined (the view's detached fallback)",
|
|
533
|
-
);
|
|
534
|
-
assert.equal(changed, 2, "a failed ask still resolves and asks the view to repaint, instead of hanging forever");
|
|
534
|
+
const tracker = new ChangesTracker(git, (path) => work(path, () => {
|
|
535
|
+
counted.push(path);
|
|
536
|
+
if (path === "file-07.ts") throw new Error("unreadable");
|
|
537
|
+
return 2;
|
|
538
|
+
}), (path) => work(path, () => {
|
|
539
|
+
if (path === "file-07.ts") throw new Error("unreadable");
|
|
540
|
+
return `rev:${path}`;
|
|
541
|
+
}));
|
|
542
|
+
await tracker.start();
|
|
543
|
+
const files = new Map(tracker.model.files.map((entry) => [entry.path, entry]));
|
|
544
|
+
assert.equal(maxFiles, FILE_WORK_CONCURRENCY, "files run concurrently up to the limit, never past it");
|
|
545
|
+
assert.equal(FILE_WORK_CONCURRENCY, 8);
|
|
546
|
+
assert.ok(overlapped, "a file's line count and revision lookup run together");
|
|
547
|
+
assert.deepEqual([...counted].sort(), paths, "every untracked file is counted once; tracked files keep numstat counts");
|
|
548
|
+
assert.equal(files.size, 31);
|
|
549
|
+
assert.deepEqual(files.get("tracked.ts"), { ...file("tracked.ts", 4, 1), diffRevision: "rev:tracked.ts" });
|
|
550
|
+
assert.deepEqual(files.get("file-07.ts"), file("file-07.ts", 0, 0, CHANGE_STATUS.UNTRACKED), "a failing lookup keeps its fallback");
|
|
551
|
+
for (const path of paths.filter((path) => path !== "file-07.ts")) assert.deepEqual(files.get(path), { ...file(path, 2, 0, CHANGE_STATUS.UNTRACKED), diffRevision: `rev:${path}` });
|
|
535
552
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { initTheme, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Container, TuiAltScreen } from "@earendil-works/pi-tui";
|
|
5
|
+
import { createChatViewport } from "../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/chat-viewport.js";
|
|
6
|
+
import { buildShellBarModel, type ShellFooterData } from "../extensions/gentle-shell.ts";
|
|
7
|
+
import { createQuietToolRenderer } from "../extensions/quiet-tools.ts";
|
|
8
|
+
|
|
9
|
+
initTheme("dark");
|
|
10
|
+
|
|
11
|
+
test("native fullscreen scroll frames reuse complete tool cards and session statistics", () => {
|
|
12
|
+
let decoration = 0;
|
|
13
|
+
let scans = 0;
|
|
14
|
+
let projections = 0;
|
|
15
|
+
const theme = {
|
|
16
|
+
fg: (_color: string, text: string) => { decoration++; return text; },
|
|
17
|
+
bg: (_color: string, text: string) => { decoration++; return `\x1b[48;5;235m${text}\x1b[49m`; },
|
|
18
|
+
bold: (text: string) => text,
|
|
19
|
+
};
|
|
20
|
+
const ctx = {
|
|
21
|
+
model: { id: "model", contextWindow: 1000 },
|
|
22
|
+
getContextUsage() { projections++; return { percent: 10, contextWindow: 1000 }; },
|
|
23
|
+
modelRegistry: { isUsingOAuth: () => false },
|
|
24
|
+
sessionManager: {
|
|
25
|
+
getEntries() { scans++; return []; }, getSessionId: () => "session", getLeafId: () => "leaf",
|
|
26
|
+
getCwd: () => "/repo", getSessionName: () => "session",
|
|
27
|
+
},
|
|
28
|
+
} as unknown as ExtensionContext;
|
|
29
|
+
const footerData = { getGitBranch: () => "main", getExtensionStatuses: () => new Map() } as unknown as ShellFooterData;
|
|
30
|
+
const pi = { getThinkingLevel: () => "high" } as unknown as ExtensionAPI;
|
|
31
|
+
const document = new Container();
|
|
32
|
+
const renderer = createQuietToolRenderer("read");
|
|
33
|
+
for (let tool = 0; tool < 50; tool++) {
|
|
34
|
+
const context = { args: { path: `src/file-${tool}.ts` }, state: {}, isPartial: false, executionStarted: true };
|
|
35
|
+
document.addChild(renderer.renderCall!(context.args, theme as never, context as never));
|
|
36
|
+
document.addChild(renderer.renderResult!({ content: [{ type: "text", text: `result-${tool}` }], details: {} },
|
|
37
|
+
{ expanded: false, isPartial: false }, theme as never, context as never));
|
|
38
|
+
}
|
|
39
|
+
const footer = new Container();
|
|
40
|
+
footer.addChild({ render: () => [`cost ${buildShellBarModel(pi, ctx, footerData).costTotal}`], invalidate() {} });
|
|
41
|
+
const viewport = createChatViewport({
|
|
42
|
+
document, pendingMessages: new Container(), status: new Container(), widgetsAbove: new Container(),
|
|
43
|
+
editor: { render: () => ["prompt"], invalidate() {} }, widgetsBelow: new Container(), footer, scrollbar: "hidden",
|
|
44
|
+
});
|
|
45
|
+
const terminal = {
|
|
46
|
+
start() {}, stop() {}, async drainInput() {}, write() {}, columns: 100, rows: 24,
|
|
47
|
+
moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {},
|
|
48
|
+
setTitle() {}, setProgress() {}, kittyProtocolActive: false, getBufferedInput() { return ""; }, hasPendingInput() { return false; },
|
|
49
|
+
} as never;
|
|
50
|
+
const tui = new TuiAltScreen(terminal, false);
|
|
51
|
+
tui.setLayoutRoot(viewport.root);
|
|
52
|
+
const host = tui as unknown as { doRender(): void; previousScreen: string[] };
|
|
53
|
+
tui.start();
|
|
54
|
+
try {
|
|
55
|
+
host.doRender();
|
|
56
|
+
const warmDecoration = decoration;
|
|
57
|
+
assert.deepEqual([scans, projections], [1, 1]);
|
|
58
|
+
const screens: string[] = [];
|
|
59
|
+
for (let frame = 0; frame < 20; frame++) {
|
|
60
|
+
if (frame % 2) viewport.transcript.scrollToEnd();
|
|
61
|
+
else viewport.transcript.scrollTo(0, { disableFollow: true });
|
|
62
|
+
host.doRender();
|
|
63
|
+
screens.push(host.previousScreen.join("\n"));
|
|
64
|
+
}
|
|
65
|
+
assert.notEqual(screens[0], screens[1], "scroll really changes the visible transcript");
|
|
66
|
+
assert.equal(decoration, warmDecoration, "even the native frame path reuses settled chrome");
|
|
67
|
+
assert.deepEqual([scans, projections], [1, 1], "scroll/pulse-like frames do not rescan unchanged session state");
|
|
68
|
+
} finally { tui.stop(); }
|
|
69
|
+
});
|