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,11 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
-
import { VisualCustomizeView } from "../lib/visual-customize-view.ts";
|
|
3
|
+
import { VisualCustomizeView, type CustomizeInline, type CustomizeRow } from "../lib/visual-customize-view.ts";
|
|
4
4
|
import { CommandPalette } from "../lib/command-palette.ts";
|
|
5
5
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import { getThemeByName, Theme } from "../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js";
|
|
7
7
|
|
|
8
8
|
const theme = { fg: (_role: string, text: string) => text };
|
|
9
|
+
type FoldableRow = CustomizeRow & { visible?: () => boolean; keyhint?: string };
|
|
9
10
|
|
|
10
11
|
test("Customize shares Commands card geometry, header and focused-row style at wide, narrow and short sizes", () => {
|
|
11
12
|
for (const [width, height] of [[100, 24], [36, 12], [76, 5]] as const) {
|
|
@@ -19,7 +20,7 @@ test("Customize shares Commands card geometry, header and focused-row style at w
|
|
|
19
20
|
assert.equal(customize.at(-1), commands.at(-1));
|
|
20
21
|
assert.ok(customize.every(line => visibleWidth(line) === width));
|
|
21
22
|
assert.ok(customize.length <= height);
|
|
22
|
-
assert.match(customize.join("\n"), /
|
|
23
|
+
assert.match(customize.join("\n"), /Customization.*esc/);
|
|
23
24
|
assert.match(customize.join("\n"), /▸ Animations/);
|
|
24
25
|
if (height > 5) {
|
|
25
26
|
view.handleInput("\x1b[C");
|
|
@@ -117,7 +118,7 @@ test("profile name submission never accepts a hidden field at heights 3–5, inc
|
|
|
117
118
|
|
|
118
119
|
test("profile preview and cancel never apply; keyboard name and confirmations require deliberate input", async () => {
|
|
119
120
|
const events: string[] = [];
|
|
120
|
-
const profile = { name: "Desk", themeName: "Light Theme", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false } } };
|
|
121
|
+
const profile = { name: "Desk", themeName: "Light Theme", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false, contextWarning: false } } };
|
|
121
122
|
const view = new VisualCustomizeView({ rows: [], theme, requestRender: () => {}, onClose: () => {}, profiles: {
|
|
122
123
|
list: () => [profile], save: (name, replace) => { events.push(`save:${name}:${replace}`); }, apply: (name) => { events.push(`apply:${name}`); }, delete: (name) => { events.push(`delete:${name}`); }, reset: () => { events.push("reset"); },
|
|
123
124
|
} });
|
|
@@ -126,6 +127,7 @@ test("profile preview and cancel never apply; keyboard name and confirmations re
|
|
|
126
127
|
assert.match(view.render(100).join("\n"), /potato.*cyan/);
|
|
127
128
|
assert.match(view.render(100).join("\n"), /hidden.*below-input.*compact/);
|
|
128
129
|
assert.match(view.render(100).join("\n"), /changes:off.*rdd:off.*agents:on.*todo:off.*usageCost:on.*modelDetails:off/);
|
|
130
|
+
assert.match(view.render(120).join("\n"), /changes:off.*rdd:off.*agents:on.*todo:off.*usageCost:on.*modelDetails:off.*contextWarning:off/);
|
|
129
131
|
view.handleInput("a"); view.handleInput("\x1b");
|
|
130
132
|
assert.deepEqual(events, []);
|
|
131
133
|
view.handleInput("p"); view.handleInput("s"); view.handleInput("New"); view.handleInput("\x7f"); view.handleInput("w\r");
|
|
@@ -145,7 +147,7 @@ test("profile preview and cancel never apply; keyboard name and confirmations re
|
|
|
145
147
|
|
|
146
148
|
test("profile confirmation stays visible at six rows for every destructive or applying action", async () => {
|
|
147
149
|
const events: string[] = [];
|
|
148
|
-
const profile = { name: "Desk", themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false } } };
|
|
150
|
+
const profile = { name: "Desk", themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false, contextWarning: false } } };
|
|
149
151
|
const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => 6, requestRender: () => {}, onClose: () => {}, profiles: {
|
|
150
152
|
list: () => [profile], save: (name, replace) => { events.push(`save:${name}:${replace}`); }, apply: (name) => { events.push(`apply:${name}`); }, delete: (name) => { events.push(`delete:${name}`); }, reset: () => { events.push("reset"); },
|
|
151
153
|
} });
|
|
@@ -163,7 +165,7 @@ test("profile confirmation stays visible at six rows for every destructive or ap
|
|
|
163
165
|
|
|
164
166
|
test("profile confirmation captures selected name across catalog mutation and cancels vanished targets", async () => {
|
|
165
167
|
const events: string[] = [];
|
|
166
|
-
const base = { themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false } } };
|
|
168
|
+
const base = { themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false, contextWarning: false } } };
|
|
167
169
|
let catalog = [{ ...base, name: "Desk" }, { ...base, name: "Mobile" }];
|
|
168
170
|
const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => 6, requestRender: () => {}, onClose: () => {}, profiles: {
|
|
169
171
|
list: () => catalog, save: (name, replace) => { events.push(`save:${name}:${replace}`); }, apply: (name) => { events.push(`apply:${name}`); }, delete: (name) => { events.push(`delete:${name}`); }, reset: () => { events.push("reset"); },
|
|
@@ -188,7 +190,7 @@ test("profile confirmation captures selected name across catalog mutation and ca
|
|
|
188
190
|
test("unrendered or clipped confirmations cannot be accepted", () => {
|
|
189
191
|
const events: string[] = [];
|
|
190
192
|
let height = 6;
|
|
191
|
-
const base = { name: "Desk", themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false } } };
|
|
193
|
+
const base = { name: "Desk", themeName: "Light", animationPolicy: "potato" as const, banner: { showRose: false, showTextLogo: true, color: "cyan" as const }, visual: { statusPlacement: "hidden" as const, headerPlacement: "below-input" as const, density: "compact" as const, visibility: { changes: false, rdd: false, agents: true, todo: false, usageCost: true, modelDetails: false, contextWarning: false } } };
|
|
192
194
|
const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => height, requestRender: () => {}, onClose: () => {}, profiles: {
|
|
193
195
|
list: () => [base], save: () => {}, apply: () => {}, delete: () => { events.push("delete"); }, reset: () => { events.push("reset"); },
|
|
194
196
|
} });
|
|
@@ -202,7 +204,7 @@ test("unrendered or clipped confirmations cannot be accepted", () => {
|
|
|
202
204
|
|
|
203
205
|
test("profile confirmations cannot execute after an unrendered resize hides full identity and yes", () => {
|
|
204
206
|
const name = "A".repeat(64);
|
|
205
|
-
const profile = { name, themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true } } };
|
|
207
|
+
const profile = { name, themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true } } };
|
|
206
208
|
for (const [key, action] of [["a", "apply"], ["d", "delete"], ["r", "replace"], ["z", "reset"]] as const) {
|
|
207
209
|
for (const width of [100, 36]) for (const shortHeight of [1, 2]) {
|
|
208
210
|
let height = 12;
|
|
@@ -224,7 +226,7 @@ test("profile confirmations cannot execute after an unrendered resize hides full
|
|
|
224
226
|
});
|
|
225
227
|
|
|
226
228
|
test("profile confirmation refuses a changed target identity without another render", () => {
|
|
227
|
-
let catalog = [{ name: "First", themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true } } }];
|
|
229
|
+
let catalog = [{ name: "First", themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true } } }];
|
|
228
230
|
const effects: string[] = [];
|
|
229
231
|
const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => 12, requestRender: () => {}, onClose: () => {}, profiles: { list: () => catalog, save: () => {}, apply: name => { effects.push(name); }, delete: () => {}, reset: () => {} } });
|
|
230
232
|
view.handleInput("p"); view.handleInput("a");
|
|
@@ -261,7 +263,7 @@ test("main controls require a visible selected row at the last rendered width an
|
|
|
261
263
|
});
|
|
262
264
|
|
|
263
265
|
test("profile confirmation refuses same-name content drift for apply, replace and delete", () => {
|
|
264
|
-
const original = { name: "Desk", themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true } } };
|
|
266
|
+
const original = { name: "Desk", themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true } } };
|
|
265
267
|
for (const key of ["a", "r", "d"]) {
|
|
266
268
|
let catalog = [original];
|
|
267
269
|
const effects: string[] = [];
|
|
@@ -311,7 +313,7 @@ test("category panel renders bounded wide and narrow frames with independent nav
|
|
|
311
313
|
test("long profile confirmation displays complete identity and yes instruction before accepting", async () => {
|
|
312
314
|
const name = "A".repeat(64);
|
|
313
315
|
const events: string[] = [];
|
|
314
|
-
const profile = { name, themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true } } };
|
|
316
|
+
const profile = { name, themeName: "dark", animationPolicy: "quality" as const, banner: { showRose: true, showTextLogo: true, color: "pink" as const }, visual: { statusPlacement: "auto" as const, headerPlacement: "top" as const, density: "comfortable" as const, visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true } } };
|
|
315
317
|
let height = 8;
|
|
316
318
|
const view = new VisualCustomizeView({ rows: [], theme, rowsAvailable: () => height, requestRender: () => {}, onClose: () => {}, profiles: { list: () => [profile], save: () => {}, apply: value => { events.push(value); }, delete: () => {}, reset: () => {} } });
|
|
317
319
|
view.handleInput("p");
|
|
@@ -416,3 +418,279 @@ test("view awaits async actions, reports errors and repaints after completion",
|
|
|
416
418
|
assert.deepEqual(errors, ["write failed"]);
|
|
417
419
|
assert.ok(renders >= 2);
|
|
418
420
|
});
|
|
421
|
+
|
|
422
|
+
// --- inline bridge (direct rows, no nested native dialogs) --------------------
|
|
423
|
+
|
|
424
|
+
const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
|
|
425
|
+
|
|
426
|
+
function inlineView(action: (inline: CustomizeInline) => void | Promise<void>, options: { rowsAvailable?: () => number; onClose?: () => void } = {}) {
|
|
427
|
+
return new VisualCustomizeView({ rows: [{ label: "Field row", action }], theme, rowsAvailable: options.rowsAvailable ?? (() => 12), requestRender: () => {}, onClose: options.onClose ?? (() => {}) });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
test("Notifications header reads Audio notifications while other panes read Customization", () => {
|
|
431
|
+
const view = new VisualCustomizeView({ rows: [
|
|
432
|
+
{ category: "Animations", label: "Speed", action: () => {} },
|
|
433
|
+
{ category: "Notifications", label: "Audio notifications: off", action: () => {} },
|
|
434
|
+
], theme, requestRender: () => {}, onClose: () => {} });
|
|
435
|
+
assert.equal(view.title(), "Customization");
|
|
436
|
+
assert.match(view.render(76).join("\n"), /Customization.*esc/);
|
|
437
|
+
view.handleInput("\x1b[B");
|
|
438
|
+
assert.equal(view.title(), "Audio notifications");
|
|
439
|
+
assert.match(view.render(76).join("\n"), /Audio notifications.*esc/);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("inline field accepts typed, pasted and Kitty input, submits on Enter and cancels with Escape without closing", async () => {
|
|
443
|
+
let closed = 0;
|
|
444
|
+
const submitted: string[] = [];
|
|
445
|
+
const view = inlineView(async inline => {
|
|
446
|
+
const value = await inline.input({ prompt: "Path", value: "" });
|
|
447
|
+
if (value !== undefined) submitted.push(value);
|
|
448
|
+
}, { onClose: () => { closed++; } });
|
|
449
|
+
view.render(76);
|
|
450
|
+
view.handleInput("\r");
|
|
451
|
+
await tick();
|
|
452
|
+
assert.match(view.render(76).join("\n"), /Path:/);
|
|
453
|
+
view.handleInput("a");
|
|
454
|
+
view.handleInput("\x1b[200~b c\x1b[201~");
|
|
455
|
+
view.handleInput("\x1b[100u");
|
|
456
|
+
assert.match(view.render(76).join("\n"), /Path: ab cd/);
|
|
457
|
+
view.handleInput("\x1b");
|
|
458
|
+
await tick();
|
|
459
|
+
assert.equal(closed, 0, "first Escape cancels the field, not the whole card");
|
|
460
|
+
assert.deepEqual(submitted, []);
|
|
461
|
+
assert.doesNotMatch(view.render(76).join("\n"), /Path:/);
|
|
462
|
+
view.handleInput("\x1b");
|
|
463
|
+
assert.equal(closed, 1, "second Escape closes the card");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("inline field submits a visible value and drops invisible resizes without writing", async () => {
|
|
467
|
+
let height = 12;
|
|
468
|
+
const submitted: string[] = [];
|
|
469
|
+
const view = inlineView(async inline => {
|
|
470
|
+
const value = await inline.input({ prompt: "Path", value: "" });
|
|
471
|
+
if (value !== undefined) submitted.push(value);
|
|
472
|
+
}, { rowsAvailable: () => height });
|
|
473
|
+
view.render(76);
|
|
474
|
+
view.handleInput("\r");
|
|
475
|
+
await tick();
|
|
476
|
+
view.handleInput("/local/a.wav");
|
|
477
|
+
view.render(76);
|
|
478
|
+
view.handleInput("\r");
|
|
479
|
+
await tick();
|
|
480
|
+
assert.deepEqual(submitted, ["/local/a.wav"]);
|
|
481
|
+
|
|
482
|
+
const hidden = inlineView(async inline => {
|
|
483
|
+
const value = await inline.input({ prompt: "Path", value: "x" });
|
|
484
|
+
if (value !== undefined) submitted.push(value);
|
|
485
|
+
}, { rowsAvailable: () => height });
|
|
486
|
+
hidden.render(76);
|
|
487
|
+
hidden.handleInput("\r");
|
|
488
|
+
await tick();
|
|
489
|
+
height = 3; // Resize below the preview area without another render.
|
|
490
|
+
hidden.handleInput("\r");
|
|
491
|
+
await tick();
|
|
492
|
+
assert.deepEqual(submitted, ["/local/a.wav"], "an Enter that cannot see the field must not submit");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("inline confirmation accepts only a fully visible yes and resolves false otherwise", async () => {
|
|
496
|
+
let height = 12;
|
|
497
|
+
const seen: boolean[] = [];
|
|
498
|
+
const view = inlineView(async inline => { seen.push(await inline.confirm("Replace invalid audio configuration? The existing file is malformed or unreadable.")); }, { rowsAvailable: () => height });
|
|
499
|
+
view.render(76);
|
|
500
|
+
view.handleInput("\r");
|
|
501
|
+
await tick();
|
|
502
|
+
assert.match(view.render(76).join("\n"), /y yes/);
|
|
503
|
+
view.handleInput("n");
|
|
504
|
+
await tick();
|
|
505
|
+
assert.deepEqual(seen, [false]);
|
|
506
|
+
|
|
507
|
+
const second = inlineView(async inline => { seen.push(await inline.confirm("Replace invalid audio configuration? The existing file is malformed or unreadable.")); }, { rowsAvailable: () => height });
|
|
508
|
+
second.render(76);
|
|
509
|
+
second.handleInput("\r");
|
|
510
|
+
await tick();
|
|
511
|
+
second.handleInput("y");
|
|
512
|
+
await tick();
|
|
513
|
+
assert.deepEqual(seen, [false, true]);
|
|
514
|
+
|
|
515
|
+
const short = inlineView(async inline => { seen.push(await inline.confirm("Replace invalid audio configuration? The existing file is malformed or unreadable.")); }, { rowsAvailable: () => 5 });
|
|
516
|
+
short.render(76);
|
|
517
|
+
short.handleInput("\r");
|
|
518
|
+
await tick();
|
|
519
|
+
short.handleInput("y");
|
|
520
|
+
await tick();
|
|
521
|
+
assert.deepEqual(seen, [false, true, false], "a confirmation with no visible yes must fail closed");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("dispose resolves a pending inline field as cancelled without writing", async () => {
|
|
525
|
+
let observed: CustomizeInline | undefined;
|
|
526
|
+
const submitted: string[] = [];
|
|
527
|
+
const view = inlineView(async inline => {
|
|
528
|
+
observed = inline;
|
|
529
|
+
const value = await inline.input({ prompt: "Path", value: "" });
|
|
530
|
+
if (value !== undefined) submitted.push(value);
|
|
531
|
+
});
|
|
532
|
+
view.render(76);
|
|
533
|
+
view.handleInput("\r");
|
|
534
|
+
await tick();
|
|
535
|
+
view.dispose();
|
|
536
|
+
await tick();
|
|
537
|
+
assert.deepEqual(submitted, []);
|
|
538
|
+
assert.equal(observed!.disposed, true, "the bridge signals disposal so rows cannot persist");
|
|
539
|
+
view.handleInput("\x1b");
|
|
540
|
+
assert.deepEqual(submitted, [], "a disposed view ignores further input");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("row key shortcuts run before panel shortcuts and are ignored outside the controls pane", () => {
|
|
544
|
+
const keys: string[] = [];
|
|
545
|
+
const view = new VisualCustomizeView({ rows: [{ category: "Notifications", label: "event", action: () => {}, key: (data) => { if (data === "p") { keys.push("p"); return true; } return false; } }],
|
|
546
|
+
theme, requestRender: () => {}, onClose: () => {}, profiles: { list: () => [], save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
|
|
547
|
+
view.render(76);
|
|
548
|
+
// Categories pane: the row key is not consulted, so the panel handles p.
|
|
549
|
+
view.handleInput("p");
|
|
550
|
+
assert.deepEqual(keys, []);
|
|
551
|
+
view.handleInput("\x1b");
|
|
552
|
+
view.handleInput("\x1b[C");
|
|
553
|
+
view.handleInput("p");
|
|
554
|
+
assert.deepEqual(keys, ["p"], "the highlighted row consumes p instead of opening profiles");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("Notifications non-event controls consume p without opening visual profiles and keep the audio title", () => {
|
|
558
|
+
const listed: string[] = [];
|
|
559
|
+
const rows: CustomizeRow[] = [
|
|
560
|
+
{ category: "Notifications", label: "Audio notifications: on", action: () => {} },
|
|
561
|
+
{ category: "Notifications", label: "Audio: unmuted", action: () => {} },
|
|
562
|
+
{ category: "Notifications", label: "Advanced: show per-event exceptions", action: () => {} },
|
|
563
|
+
];
|
|
564
|
+
const view = new VisualCustomizeView({ rows, theme, requestRender: () => {}, onClose: () => {}, profiles: { list: () => { listed.push("list"); return []; }, save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
|
|
565
|
+
view.render(76);
|
|
566
|
+
view.handleInput("\x1b[C");
|
|
567
|
+
for (let i = 0; i < rows.length; i++) {
|
|
568
|
+
view.handleInput("p");
|
|
569
|
+
const frame = view.render(76).join("\n");
|
|
570
|
+
assert.equal(view.title(), "Audio notifications", `non-event row ${i} must keep the audio title`);
|
|
571
|
+
assert.match(frame, /Audio notifications/, `non-event row ${i} must stay on the audio card`);
|
|
572
|
+
assert.doesNotMatch(frame, /Visual profiles/, `non-event row ${i} must not open the profile pane`);
|
|
573
|
+
if (i < rows.length - 1) view.handleInput("\x1b[B");
|
|
574
|
+
}
|
|
575
|
+
assert.deepEqual(listed, [], "the profile catalog must never be listed from Notifications controls");
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("p still opens visual profiles from a non-Notifications control", () => {
|
|
579
|
+
const listed: string[] = [];
|
|
580
|
+
const view = new VisualCustomizeView({ rows: [{ category: "Themes", label: "Dark", action: () => {} }], theme, requestRender: () => {}, onClose: () => {}, profiles: { list: () => { listed.push("list"); return []; }, save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
|
|
581
|
+
view.render(76);
|
|
582
|
+
view.handleInput("\x1b[C");
|
|
583
|
+
view.handleInput("p");
|
|
584
|
+
assert.match(view.render(76).join("\n"), /Visual profiles/);
|
|
585
|
+
assert.equal(listed.length, 1);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("the Notifications card footer is audio-specific and never advertises visual profiles", () => {
|
|
589
|
+
const profiles = { list: () => [], save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} };
|
|
590
|
+
const view = new VisualCustomizeView({ rows: [
|
|
591
|
+
{ category: "Animations", label: "Speed", action: () => {} },
|
|
592
|
+
{ category: "Notifications", label: "Audio notifications: off", action: () => {} },
|
|
593
|
+
], theme, requestRender: () => {}, onClose: () => {}, rowsAvailable: () => 24, profiles });
|
|
594
|
+
view.handleInput("\x1b[B");
|
|
595
|
+
view.handleInput("\x1b[C");
|
|
596
|
+
for (const width of [60, 80]) {
|
|
597
|
+
const frame = view.render(width).join("\n");
|
|
598
|
+
assert.match(frame, /Tab panes · Enter change · p test · Esc close/, `basic audio footer at width ${width}`);
|
|
599
|
+
assert.doesNotMatch(frame, /f WAV/, `basic audio footer must not advertise file editing at width ${width}`);
|
|
600
|
+
assert.doesNotMatch(frame, /p profiles/, `audio footer must not advertise profiles at width ${width}`);
|
|
601
|
+
}
|
|
602
|
+
const other = new VisualCustomizeView({ rows: [{ category: "Themes", label: "Dark", action: () => {} }], theme, requestRender: () => {}, onClose: () => {}, rowsAvailable: () => 24, profiles });
|
|
603
|
+
other.handleInput("\x1b[C");
|
|
604
|
+
assert.match(other.render(80).join("\n"), /p profiles/, "other categories keep the profiles hint");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("an async row key task holds busy until it settles and releases it after a rejection", async () => {
|
|
608
|
+
const errors: Error[] = [];
|
|
609
|
+
const keyed: string[] = [];
|
|
610
|
+
let actions = 0;
|
|
611
|
+
const view = new VisualCustomizeView({ rows: [{
|
|
612
|
+
category: "Notifications", label: "Async row", action: () => { actions++; },
|
|
613
|
+
key: data => { if (data === "f") { keyed.push("f"); return Promise.reject(new Error("key failed")); } return false; },
|
|
614
|
+
}], theme, requestRender: () => {}, onClose: () => {}, onError: error => errors.push(error), rowsAvailable: () => 12 });
|
|
615
|
+
view.render(76);
|
|
616
|
+
view.handleInput("\x1b[C");
|
|
617
|
+
view.handleInput("f");
|
|
618
|
+
view.handleInput("f");
|
|
619
|
+
assert.deepEqual(keyed, ["f"], "a second key is ignored while the async task is busy");
|
|
620
|
+
await tick(); await tick();
|
|
621
|
+
assert.equal(errors.length, 1, "a rejected key surfaces through onError");
|
|
622
|
+
assert.equal(errors[0]?.message, "key failed");
|
|
623
|
+
view.handleInput("\r");
|
|
624
|
+
await tick();
|
|
625
|
+
assert.equal(actions, 1, "busy is released after the rejected key task settles");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("a visible() false row is unreachable and its action or key is never invoked", async () => {
|
|
629
|
+
const actions: string[] = [];
|
|
630
|
+
const keys: string[] = [];
|
|
631
|
+
let advanced = false;
|
|
632
|
+
const rows: FoldableRow[] = [
|
|
633
|
+
{ category: "Notifications" as const, label: "Basic", action: () => { actions.push("basic"); } },
|
|
634
|
+
{ category: "Notifications" as const, label: "Hidden detail", visible: () => advanced, action: () => { actions.push("hidden"); }, key: (data: string) => { keys.push(data); return true; } },
|
|
635
|
+
];
|
|
636
|
+
const view = new VisualCustomizeView({ rows, theme, rowsAvailable: () => 12, requestRender: () => {}, onClose: () => {} });
|
|
637
|
+
view.render(76);
|
|
638
|
+
view.handleInput("\x1b[C");
|
|
639
|
+
for (let i = 0; i < 3; i++) { view.handleInput("\x1b[B"); assert.doesNotMatch(view.render(76).join("\n"), /Hidden detail/, "a collapsed row must not render"); }
|
|
640
|
+
view.handleInput("\r");
|
|
641
|
+
view.handleInput("f");
|
|
642
|
+
view.handleInput("p");
|
|
643
|
+
await tick();
|
|
644
|
+
assert.ok(!actions.includes("hidden"), "a hidden row action must never run");
|
|
645
|
+
assert.deepEqual(keys, [], "a hidden row key must never run");
|
|
646
|
+
advanced = true;
|
|
647
|
+
assert.match(view.render(76).join("\n"), /Hidden detail/, "expanding reveals the row");
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("selection clamps to a visible row after a collapse", async () => {
|
|
651
|
+
const actions: string[] = [];
|
|
652
|
+
let advanced = true;
|
|
653
|
+
const rows: FoldableRow[] = [
|
|
654
|
+
{ category: "Notifications" as const, label: "Basic", action: () => { actions.push("basic"); } },
|
|
655
|
+
{ category: "Notifications" as const, label: "Advanced", visible: () => advanced, action: () => { actions.push("advanced"); } },
|
|
656
|
+
];
|
|
657
|
+
const view = new VisualCustomizeView({ rows, theme, rowsAvailable: () => 12, requestRender: () => {}, onClose: () => {} });
|
|
658
|
+
view.render(76);
|
|
659
|
+
view.handleInput("\x1b[C");
|
|
660
|
+
view.handleInput("\x1b[B");
|
|
661
|
+
assert.match(view.render(76).join("\n"), /\u25b8 Advanced/);
|
|
662
|
+
advanced = false;
|
|
663
|
+
assert.doesNotMatch(view.render(76).join("\n"), /Advanced/);
|
|
664
|
+
view.handleInput("\r");
|
|
665
|
+
await tick();
|
|
666
|
+
assert.deepEqual(actions, ["basic"], "the collapsed selection clamps to the visible row");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("a throwing visible() callback hides its row instead of exposing it", async () => {
|
|
670
|
+
const actions: string[] = [];
|
|
671
|
+
const rows: FoldableRow[] = [
|
|
672
|
+
{ category: "Notifications" as const, label: "Boom", visible: () => { throw new Error("boom"); }, action: () => { actions.push("boom"); } },
|
|
673
|
+
{ category: "Notifications" as const, label: "Safe", action: () => { actions.push("safe"); } },
|
|
674
|
+
];
|
|
675
|
+
const view = new VisualCustomizeView({ rows, theme, rowsAvailable: () => 12, requestRender: () => {}, onClose: () => {} });
|
|
676
|
+
view.render(76);
|
|
677
|
+
view.handleInput("\x1b[C");
|
|
678
|
+
assert.doesNotMatch(view.render(76).join("\n"), /Boom/, "a row whose visibility check throws must fail closed");
|
|
679
|
+
view.handleInput("\r");
|
|
680
|
+
await tick();
|
|
681
|
+
assert.deepEqual(actions, ["safe"], "the throwing row must never be activated");
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("the audio footer drops f WAV when the selected row has no keyhint and advertises it when it does", () => {
|
|
685
|
+
const view = new VisualCustomizeView({ rows: [
|
|
686
|
+
{ category: "Notifications", label: "Audio notifications: off", action: () => {} },
|
|
687
|
+
{ category: "Notifications", label: "Success: success tone", action: () => {}, keyhint: "Tab panes · Enter change · f WAV · p test · Esc close" },
|
|
688
|
+
] as FoldableRow[], theme, rowsAvailable: () => 24, requestRender: () => {}, onClose: () => {}, profiles: { list: () => [], save: () => {}, apply: () => {}, delete: () => {}, reset: () => {} } });
|
|
689
|
+
view.handleInput("\x1b[C");
|
|
690
|
+
const basic = view.render(80).join("\n");
|
|
691
|
+
assert.match(basic, /Tab panes · Enter change · p test · Esc close/, "a row with no keyhint keeps the audio footer without a WAV hint");
|
|
692
|
+
assert.doesNotMatch(basic, /f WAV/, "the default audio footer must not advertise file editing");
|
|
693
|
+
view.handleInput("\x1b[B");
|
|
694
|
+
const advanced = view.render(80).join("\n");
|
|
695
|
+
assert.match(advanced, /Tab panes · Enter change · f WAV · p test · Esc close/, "a row with a keyhint advertises the WAV key");
|
|
696
|
+
});
|
|
@@ -58,17 +58,74 @@ function dispatchWriter(input: Record<string, unknown>) {
|
|
|
58
58
|
cwd,
|
|
59
59
|
hasUI: false,
|
|
60
60
|
ui: { confirm: async () => true },
|
|
61
|
-
} as ExtensionContext);
|
|
61
|
+
} as unknown as ExtensionContext);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async function assertAccepted(input: Record<string, unknown>, message: string) {
|
|
65
65
|
assert.equal(await dispatchWriter(input), undefined, message);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const RESEND = "Resend the same task text unchanged except for that section; never shorten or re-summarize it.";
|
|
69
|
+
const PROBLEM = /^(?:No `## Allowed edit surfaces` heading was found\.|The section has no entries\.|Repeated sections list different surfaces\.|Line ".+" is not a valid surface entry; move prose under a following Markdown heading\.|Line ".+" is not a valid surface entry; if it is a path, wrap the whole entry in backticks, otherwise move it under a following Markdown heading\.|Entry ".+" is not a narrow repository-relative path; remove absolute paths, `\.\.` segments, root globs, and stray backticks\.)$/;
|
|
70
|
+
|
|
71
|
+
// Every rejection is the canonical text, exactly one concrete problem, and the
|
|
72
|
+
// resend instruction; nothing else (review R3-005).
|
|
73
|
+
function rejectionProblem(reason: string): string {
|
|
74
|
+
assert.ok(reason.startsWith(`${REJECTION} `), "canonical rejection prefix");
|
|
75
|
+
assert.ok(reason.endsWith(` ${RESEND}`), "resend instruction suffix");
|
|
76
|
+
const problem = reason.slice(REJECTION.length + 1, reason.length - RESEND.length - 1);
|
|
77
|
+
assert.match(problem, PROBLEM);
|
|
78
|
+
return problem;
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
async function assertRejected(input: Record<string, unknown>, message: string) {
|
|
69
|
-
|
|
82
|
+
const result = await dispatchWriter(input);
|
|
83
|
+
assert.equal(result?.block, true, message);
|
|
84
|
+
rejectionProblem(result.reason);
|
|
70
85
|
}
|
|
71
86
|
|
|
87
|
+
// gentle-shell#1713: an unexplained rejection made the orchestrator resend
|
|
88
|
+
// shorter, re-summarized tasks (1786 -> 1542 chars) and lose requirements.
|
|
89
|
+
test("rejection names the offending line and asks for the same task unchanged", async () => {
|
|
90
|
+
const result = await dispatchWriter({
|
|
91
|
+
agent: "gentle-ai-worker",
|
|
92
|
+
mode: "task",
|
|
93
|
+
task: [
|
|
94
|
+
"Implement split transactions.",
|
|
95
|
+
"",
|
|
96
|
+
"## Allowed edit surfaces",
|
|
97
|
+
"- src/model.ts",
|
|
98
|
+
"- test/**/*.test.ts",
|
|
99
|
+
"",
|
|
100
|
+
"Requirements: one parent transaction with ordered allocations.",
|
|
101
|
+
].join("\n"),
|
|
102
|
+
});
|
|
103
|
+
assert.equal(result?.block, true);
|
|
104
|
+
assert.match(result.reason, /Line "Requirements: one parent transaction with ordered allocations\." is not a valid surface entry/);
|
|
105
|
+
assert.match(result.reason, /Resend the same task text unchanged except for that section; never shorten or re-summarize it\./);
|
|
106
|
+
const missing = await dispatchWriter({ agent: "gentle-ai-worker", mode: "task", task: "Implement it." });
|
|
107
|
+
assert.match(missing?.reason ?? "", /No `## Allowed edit surfaces` heading was found\./);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("each rejection names its concrete problem, and invalid paths are not called prose", async () => {
|
|
111
|
+
const problem = async (input: Record<string, unknown>) => rejectionProblem((await dispatchWriter({ agent: "gentle-ai-worker", mode: "task", ...input }))!.reason);
|
|
112
|
+
assert.equal(await problem({ task: "## Allowed edit surfaces\n\n## Return\nReport" }), "The section has no entries.");
|
|
113
|
+
assert.equal(await problem({ task: "## Allowed edit surfaces\nsrc/a.ts", context: "## Allowed edit surfaces\nsrc/b.ts" }), "Repeated sections list different surfaces.");
|
|
114
|
+
for (const path of ["/etc/passwd", "../outside.ts", "src/../../x.ts", "**/*.ts", "src/`odd.ts"]) {
|
|
115
|
+
assert.equal(await problem({ task: `## Allowed edit surfaces\n- ${path}` }), `Entry "${path}" is not a narrow repository-relative path; remove absolute paths, \`..\` segments, root globs, and stray backticks.`, path);
|
|
116
|
+
}
|
|
117
|
+
const long = `Then ${"verify every requirement ".repeat(8)}carefully.`;
|
|
118
|
+
const ambiguous = (shown: string) => `Line "${shown}" is not a valid surface entry; if it is a path, wrap the whole entry in backticks, otherwise move it under a following Markdown heading.`;
|
|
119
|
+
assert.equal(await problem({ task: `## Allowed edit surfaces\nsrc/a.ts\n${long}` }), ambiguous(`${long.slice(0, 120)}...`));
|
|
120
|
+
// A real path with a space must not be steered out of the section.
|
|
121
|
+
assert.equal(await problem({ task: "## Allowed edit surfaces\n- docs/with space.md" }), ambiguous("- docs/with space.md"));
|
|
122
|
+
// Whitespace that stays invalid even when quoted is plain prose.
|
|
123
|
+
assert.equal(await problem({ task: "## Allowed edit surfaces\n- ~ notes for later" }), `Line "- ~ notes for later" is not a valid surface entry; move prose under a following Markdown heading.`);
|
|
124
|
+
const control = await problem({ task: "## Allowed edit surfaces\nsrc/a\u0007.ts" });
|
|
125
|
+
assert.doesNotMatch(control, /\u0007/);
|
|
126
|
+
assert.match(control, /^Entry "src\/a \.ts" is not a narrow repository-relative path/);
|
|
127
|
+
});
|
|
128
|
+
|
|
72
129
|
test("task-scoped surfaces are accepted ahead of a deeper heading", async () => {
|
|
73
130
|
await assertAccepted({
|
|
74
131
|
agent: "gentle-ai-worker",
|