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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { TSchema } from "typebox";
|
|
4
|
+
|
|
5
|
+
const MAX_STATUSES = 8;
|
|
6
|
+
const MAX_ACTIVE_OBSERVATIONS = 8;
|
|
7
|
+
type Outcome = { kind: "success" | "cli_error" | "native_failure" | "unknown"; code?: string; phase?: string; mutation_outcome?: string; retry_safe?: boolean; next_action?: string };
|
|
8
|
+
interface StatusSample {
|
|
9
|
+
timeout_ms: number;
|
|
10
|
+
total_ms: number;
|
|
11
|
+
resolution_ms: number;
|
|
12
|
+
adapter_ms: number;
|
|
13
|
+
decode_ms: number;
|
|
14
|
+
outcome: Outcome;
|
|
15
|
+
}
|
|
16
|
+
interface Summary {
|
|
17
|
+
schema: "gentle-pi.status-timing/v1";
|
|
18
|
+
host_total_ms: number;
|
|
19
|
+
dispatch_ms: number;
|
|
20
|
+
sidebar_ms: number;
|
|
21
|
+
completion_ms: number;
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
clock_unavailable?: true;
|
|
24
|
+
statuses: StatusSample[];
|
|
25
|
+
}
|
|
26
|
+
interface HostObservation {
|
|
27
|
+
clock: () => number;
|
|
28
|
+
current: () => boolean;
|
|
29
|
+
claim: () => boolean;
|
|
30
|
+
claimed: boolean;
|
|
31
|
+
started: number;
|
|
32
|
+
lastNativeEnd: number;
|
|
33
|
+
summary: Summary;
|
|
34
|
+
}
|
|
35
|
+
interface NativeObservation { host: HostObservation; sample: StatusSample; }
|
|
36
|
+
const hostScope = new AsyncLocalStorage<HostObservation>();
|
|
37
|
+
const nativeScope = new AsyncLocalStorage<NativeObservation>();
|
|
38
|
+
|
|
39
|
+
// Observer failures must never replace the operation's result or exception.
|
|
40
|
+
function safely<T>(run: () => T): T | undefined {
|
|
41
|
+
try { return run(); } catch { return undefined; }
|
|
42
|
+
}
|
|
43
|
+
function now(host: HostObservation): number {
|
|
44
|
+
const value = safely(host.clock);
|
|
45
|
+
if (value !== undefined && Number.isFinite(value) && value >= 0) return value;
|
|
46
|
+
host.summary.clock_unavailable = true;
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
function elapsed(start: number, end: number): number { return Math.max(0, end - start); }
|
|
50
|
+
|
|
51
|
+
// Failure/v2 codes and next_action are extensible strings, not safe diagnostic
|
|
52
|
+
// text. Only recognized values enter this summary; no messages or raw envelopes.
|
|
53
|
+
const CLI_CODES = new Set(["unavailable", "timeout", "non-zero", "signal", "unexpected-stderr", "output-limit", "empty-output", "malformed-json", "schema-incompatible", "identity-mismatch", "version-incompatible", "cancelled", "package-local-binary-missing", "unsupported-transition-operation"]);
|
|
54
|
+
const NATIVE_CODES = new Set(["operation_timeout", "invalid_request", "binding_revision_conflict", "internal_error", "cancelled"]);
|
|
55
|
+
function outcome(error: unknown): Outcome {
|
|
56
|
+
return safely((): Outcome => {
|
|
57
|
+
if (typeof error !== "object" || error === null) return { kind: "unknown" };
|
|
58
|
+
const value = error as { name?: unknown; code?: unknown; failureEnvelope?: Record<string, unknown> };
|
|
59
|
+
if (value.name === "NativeReviewCliError") return { kind: "cli_error", code: typeof value.code === "string" && CLI_CODES.has(value.code) ? value.code : "other" };
|
|
60
|
+
if (value.name !== "NativeReviewIntegrationError" || !value.failureEnvelope) return { kind: "unknown" };
|
|
61
|
+
const failure = value.failureEnvelope;
|
|
62
|
+
const result: Outcome = { kind: "native_failure", code: typeof failure.code === "string" && NATIVE_CODES.has(failure.code) ? failure.code : "other" };
|
|
63
|
+
if (["preflight", "pre_native", "native_running", "native_committed", "reconciliation"].includes(String(failure.phase))) result.phase = String(failure.phase);
|
|
64
|
+
if (["not_started", "committed", "unknown"].includes(String(failure.mutationOutcome))) result.mutation_outcome = String(failure.mutationOutcome);
|
|
65
|
+
if (typeof failure.retrySafe === "boolean") result.retry_safe = failure.retrySafe;
|
|
66
|
+
if (["stop", "review.status"].includes(String(failure.nextAction))) result.next_action = String(failure.nextAction);
|
|
67
|
+
return result;
|
|
68
|
+
}) ?? { kind: "unknown" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The next already-authorized STATUS-bearing tool call consumes the arm. */
|
|
72
|
+
export class StatusTimingDiagnostics {
|
|
73
|
+
private armed = false;
|
|
74
|
+
private epoch = 0;
|
|
75
|
+
private manager: ExtensionContext["sessionManager"] | undefined;
|
|
76
|
+
private session: string | undefined;
|
|
77
|
+
private last: Summary | undefined;
|
|
78
|
+
private readonly active = new Set<HostObservation>();
|
|
79
|
+
private readonly clock: () => number;
|
|
80
|
+
constructor(clock = () => performance.now()) { this.clock = clock; }
|
|
81
|
+
|
|
82
|
+
reset(ctx?: ExtensionContext): void {
|
|
83
|
+
this.epoch++;
|
|
84
|
+
this.armed = false;
|
|
85
|
+
this.last = undefined;
|
|
86
|
+
for (const host of this.active) host.summary.statuses.length = 0;
|
|
87
|
+
this.active.clear();
|
|
88
|
+
this.manager = ctx?.sessionManager;
|
|
89
|
+
this.session = safely(() => ctx?.sessionManager.getSessionId());
|
|
90
|
+
}
|
|
91
|
+
private matches(ctx: ExtensionContext): boolean {
|
|
92
|
+
return safely(() => this.manager === ctx.sessionManager && !!this.session && this.session === ctx.sessionManager.getSessionId()) === true;
|
|
93
|
+
}
|
|
94
|
+
command(args: string, ctx: ExtensionContext): void {
|
|
95
|
+
if (!this.matches(ctx)) this.reset(ctx);
|
|
96
|
+
const action = args.trim() || "show";
|
|
97
|
+
if (action === "enable") {
|
|
98
|
+
this.reset(ctx);
|
|
99
|
+
this.armed = !!this.session;
|
|
100
|
+
ctx.ui.notify(this.armed
|
|
101
|
+
? "STATUS timing armed for the next separately authorized STATUS-bearing tool call. No STATUS is invoked or retried. Use /gentle:status-timing show to consult the completed summary."
|
|
102
|
+
: "STATUS timing requires a live session.", "info");
|
|
103
|
+
} else if (action === "disable") {
|
|
104
|
+
this.reset(ctx);
|
|
105
|
+
ctx.ui.notify("STATUS timing disabled; in-memory observations cleared.", "info");
|
|
106
|
+
} else if (action === "show") {
|
|
107
|
+
ctx.ui.notify(this.last ? JSON.stringify(this.last, null, 2) : `No completed STATUS timing summary. Diagnostic is ${this.armed ? "armed" : "off"}.`, "info");
|
|
108
|
+
} else {
|
|
109
|
+
ctx.ui.notify("Unknown /gentle:status-timing sub-action. Use enable, disable, or show.", "warning");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
tool<TParams extends TSchema, TDetails>(definition: ToolDefinition<TParams, TDetails>): ToolDefinition<TParams, TDetails> {
|
|
113
|
+
return {
|
|
114
|
+
...definition,
|
|
115
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
116
|
+
const run = () => definition.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
117
|
+
if (!this.matches(ctx)) this.reset(ctx);
|
|
118
|
+
if (!this.armed || this.active.size >= MAX_ACTIVE_OBSERVATIONS) return run();
|
|
119
|
+
const epoch = this.epoch;
|
|
120
|
+
const host: HostObservation = {
|
|
121
|
+
clock: this.clock,
|
|
122
|
+
current: () => this.epoch === epoch && this.matches(ctx),
|
|
123
|
+
claim: () => {
|
|
124
|
+
if (!host.current() || !this.armed) return false;
|
|
125
|
+
this.armed = false;
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
claimed: false, started: 0, lastNativeEnd: 0,
|
|
129
|
+
summary: { schema: "gentle-pi.status-timing/v1", host_total_ms: 0, dispatch_ms: 0, sidebar_ms: 0, completion_ms: 0, truncated: false, statuses: [] },
|
|
130
|
+
};
|
|
131
|
+
this.active.add(host);
|
|
132
|
+
host.started = now(host);
|
|
133
|
+
return hostScope.run(host, async () => {
|
|
134
|
+
try { return await run(); }
|
|
135
|
+
finally {
|
|
136
|
+
this.active.delete(host);
|
|
137
|
+
safely(() => {
|
|
138
|
+
if (!host.claimed || !host.current()) return;
|
|
139
|
+
const ended = now(host);
|
|
140
|
+
host.summary.host_total_ms = elapsed(host.started, ended);
|
|
141
|
+
host.summary.completion_ms = elapsed(host.lastNativeEnd, ended);
|
|
142
|
+
this.last = host.summary;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Only called by the existing STATUS entry points; never invokes one itself. */
|
|
152
|
+
export async function observeNativeStatus<T>(timeoutMs: number, run: () => Promise<T>): Promise<T> {
|
|
153
|
+
const host = hostScope.getStore();
|
|
154
|
+
if (!host || !safely(host.current)) return run();
|
|
155
|
+
if (!host.claimed) {
|
|
156
|
+
if (!safely(host.claim)) return run();
|
|
157
|
+
host.claimed = true;
|
|
158
|
+
host.summary.dispatch_ms = elapsed(host.started, now(host));
|
|
159
|
+
}
|
|
160
|
+
const sample: StatusSample = { timeout_ms: timeoutMs, total_ms: 0, resolution_ms: 0, adapter_ms: 0, decode_ms: 0, outcome: { kind: "success" } };
|
|
161
|
+
if (host.summary.statuses.length < MAX_STATUSES) host.summary.statuses.push(sample);
|
|
162
|
+
else host.summary.truncated = true;
|
|
163
|
+
const started = now(host);
|
|
164
|
+
return nativeScope.run({ host, sample }, async () => {
|
|
165
|
+
try { return await run(); }
|
|
166
|
+
catch (error) { safely(() => { sample.outcome = outcome(error); }); throw error; }
|
|
167
|
+
finally {
|
|
168
|
+
safely(() => {
|
|
169
|
+
const ended = now(host);
|
|
170
|
+
sample.total_ms = elapsed(started, ended);
|
|
171
|
+
host.lastNativeEnd = ended;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function activeNative(): NativeObservation | undefined {
|
|
178
|
+
const observation = nativeScope.getStore();
|
|
179
|
+
return observation && safely(observation.host.current) ? observation : undefined;
|
|
180
|
+
}
|
|
181
|
+
export function measureStatusSync<T>(stage: "resolution" | "decode", run: () => T): T {
|
|
182
|
+
const observation = activeNative();
|
|
183
|
+
if (!observation) return run();
|
|
184
|
+
const started = now(observation.host);
|
|
185
|
+
try { return run(); }
|
|
186
|
+
finally { safely(() => { observation.sample[`${stage}_ms`] += elapsed(started, now(observation.host)); }); }
|
|
187
|
+
}
|
|
188
|
+
export async function measureStatusAsync<T>(stage: "adapter", run: () => Promise<T>): Promise<T> {
|
|
189
|
+
const observation = activeNative();
|
|
190
|
+
if (!observation) return run();
|
|
191
|
+
const started = now(observation.host);
|
|
192
|
+
try { return await run(); }
|
|
193
|
+
finally { safely(() => { observation.sample[`${stage}_ms`] += elapsed(started, now(observation.host)); }); }
|
|
194
|
+
}
|
|
195
|
+
export function measureStatusSidebar<T>(run: () => T): T {
|
|
196
|
+
const host = hostScope.getStore();
|
|
197
|
+
if (!host || !safely(host.current)) return run();
|
|
198
|
+
const started = now(host);
|
|
199
|
+
try { return run(); }
|
|
200
|
+
finally { safely(() => { host.summary.sidebar_ms += elapsed(started, now(host)); }); }
|
|
201
|
+
}
|
package/lib/telemetry-trigger.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { gentleAiIsolationEnv } from "./gentle-ai-binary.ts";
|
|
2
3
|
|
|
3
4
|
// gentle-ai#4309 owns anonymous usage telemetry end to end: install and
|
|
4
5
|
// heartbeat events, the notice, every kill switch, and the wire contract.
|
|
@@ -128,7 +129,8 @@ export function spawnTelemetryTrigger(options: SpawnTelemetryTriggerOptions): Te
|
|
|
128
129
|
try {
|
|
129
130
|
const child = spawnImpl(options.executable, ["telemetry", "trigger", "--json"], {
|
|
130
131
|
cwd: options.cwd,
|
|
131
|
-
|
|
132
|
+
// S9: a bundled install keeps Gentle AI's telemetry state inside its prefix.
|
|
133
|
+
env: gentleAiIsolationEnv(options.env),
|
|
132
134
|
detached: true,
|
|
133
135
|
windowsHide: true,
|
|
134
136
|
stdio: "ignore",
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { CURSOR_MARKER, Editor, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
|
|
4
|
-
// Private shape audited against @earendil-works/pi-tui 0.99.1, 0.99.2
|
|
4
|
+
// Private shape audited against @earendil-works/pi-tui 0.99.1, 0.99.2, 1.0.0, 1.1.0 and 1.2.0:
|
|
5
5
|
// editor.js state/pastes/history, sticky cursor reset, layoutText/render geometry,
|
|
6
6
|
// autocomplete cancellation and { state, pastes, pasteCounter } undo restoration;
|
|
7
7
|
// undo-stack.js clone-on-push/pop snapshots. The two 0.99 releases ship identical
|
|
8
8
|
// editor/undo-stack sources; 1.0.0 is a touched-contract audit, not byte identity.
|
|
9
|
-
//
|
|
9
|
+
// The 1.1.0 touched-contract audit retains clone-on-push snapshots with the
|
|
10
|
+
// paste registry, cursor resets, layoutText/render geometry and history exit.
|
|
11
|
+
// cancelAutocomplete aborts/debounces requests before clearing its UI;
|
|
12
|
+
// the adapter still calls that host-owned method, never mutating its request state.
|
|
13
|
+
// Actual installed bundled/unbundled tests exercise identity, edit/undo, paste,
|
|
10
14
|
// selection, wrapping/scroll and autocomplete; fabricated metadata tests only
|
|
11
15
|
// prove exact-version admission for the older audited releases.
|
|
16
|
+
// 1.2.0 changes only editor cursor rendering to paired pi:fc/pi:/fc APC
|
|
17
|
+
// markers resolved by the host TUI (including hardware-cursor suppression).
|
|
18
|
+
// Keep these markers intact and rearm selection after their reverse-off;
|
|
19
|
+
// state, undo, paste, layout and autocomplete contracts remain unchanged.
|
|
12
20
|
// Never silently adapt another build: undo also owns the paste registry.
|
|
13
21
|
interface Position { line: number; col: number }
|
|
14
22
|
interface State { lines: string[]; cursorLine: number; cursorCol: number }
|
|
@@ -47,7 +55,7 @@ interface PrivateEditor {
|
|
|
47
55
|
|
|
48
56
|
// The single audited-release source for the adapter and Gentle Shell's runtime
|
|
49
57
|
// identity gates. Add a release only after re-auditing the files named above.
|
|
50
|
-
export const AUDITED_PI_EDITOR_VERSIONS = Object.freeze(["0.99.1", "0.99.2", "1.0.0"] as const);
|
|
58
|
+
export const AUDITED_PI_EDITOR_VERSIONS = Object.freeze(["0.99.1", "0.99.2", "1.0.0", "1.1.0", "1.2.0"] as const);
|
|
51
59
|
export type AuditedPiEditorVersion = (typeof AUDITED_PI_EDITOR_VERSIONS)[number];
|
|
52
60
|
|
|
53
61
|
export function isAuditedPiEditorVersion(version: unknown): version is AuditedPiEditorVersion {
|
|
@@ -79,7 +87,7 @@ function hasEditorIdentity(value: unknown, version: string, editorClass: typeof
|
|
|
79
87
|
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
80
88
|
const marker = /\[paste #(\d+)(?: (?:\+\d+ lines|\d+ chars))?\]/g;
|
|
81
89
|
|
|
82
|
-
// Only CSI SGR and Pi's own zero-width cursor
|
|
90
|
+
// Only CSI SGR and Pi's own zero-width cursor markers are expected in editor
|
|
83
91
|
// text. Unknown escapes leave the unmodified frame intact (fail closed).
|
|
84
92
|
function highlightRenderedRow(row: string, chunk: LayoutLine, left: number, right: number, padding: number): string | undefined {
|
|
85
93
|
const cursor = chunk.hasCursor ? chunk.cursorPos : undefined;
|
|
@@ -110,8 +118,22 @@ function highlightRenderedRow(row: string, chunk: LayoutLine, left: number, righ
|
|
|
110
118
|
result += token;
|
|
111
119
|
if (selected && !inverse) { result += "\x1b[7m"; inverse = true; }
|
|
112
120
|
} else if (token.startsWith("\x1b")) {
|
|
113
|
-
if (token !== CURSOR_MARKER) return undefined;
|
|
121
|
+
if (token !== CURSOR_MARKER && token !== "\x1b_pi:fc\x07" && token !== "\x1b_pi:/fc\x07") return undefined;
|
|
122
|
+
// Close selection *before* the cursor pair, never inside its cell:
|
|
123
|
+
// otherwise a later host SGR 7 would immediately meet our SGR 27.
|
|
124
|
+
// Closing before CURSOR_MARKER also keeps it adjacent to pi:fc so
|
|
125
|
+
// hardware-cursor suppression can still recognize the focused pair.
|
|
126
|
+
const local = col - padding;
|
|
127
|
+
if ((token === CURSOR_MARKER || token === "\x1b_pi:fc\x07") && selected &&
|
|
128
|
+
!(local >= left && local < right && local < chunk.text.length)) {
|
|
129
|
+
if (!nativeInverse && inverse) result += "\x1b[27m";
|
|
130
|
+
selected = false;
|
|
131
|
+
inverse = nativeInverse;
|
|
132
|
+
}
|
|
114
133
|
result += token;
|
|
134
|
+
// The host resolves pi:/fc to SGR 27, or drops the pair when the
|
|
135
|
+
// hardware cursor owns this cell. Selection must survive either path.
|
|
136
|
+
if (token === "\x1b_pi:/fc\x07" && selected) result += "\x1b[7m";
|
|
115
137
|
} else {
|
|
116
138
|
const local = col - padding;
|
|
117
139
|
const shouldSelect = local >= left && local < right && local < chunk.text.length;
|
|
@@ -18,6 +18,8 @@ export interface VisualVisibility {
|
|
|
18
18
|
todo: boolean;
|
|
19
19
|
usageCost: boolean;
|
|
20
20
|
modelDetails: boolean;
|
|
21
|
+
/** High-context warning notice; independent from audio notifications. */
|
|
22
|
+
contextWarning: boolean;
|
|
21
23
|
}
|
|
22
24
|
export interface VisualSettings {
|
|
23
25
|
statusPlacement: StatusPlacement;
|
|
@@ -29,13 +31,13 @@ export const DEFAULT_VISUAL_SETTINGS: VisualSettings = {
|
|
|
29
31
|
statusPlacement: STATUS_PLACEMENT.AUTO,
|
|
30
32
|
headerPlacement: HEADER_PLACEMENT.TOP,
|
|
31
33
|
density: DENSITY.COMFORTABLE,
|
|
32
|
-
visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true },
|
|
34
|
+
visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true },
|
|
33
35
|
};
|
|
34
36
|
/** Sections row order; `rdd` follows `changes` as the RDD group follows Changes in the Status card. */
|
|
35
|
-
export const VISUAL_SECTION_KEYS = ["changes", "rdd", "agents", "todo", "usageCost", "modelDetails"] as const satisfies readonly (keyof VisualVisibility)[];
|
|
36
|
-
/** Keys added after v1 shipped: stored settings may omit them together, and they default to shown. */
|
|
37
|
-
const ADDED_SECTION_KEYS: readonly (keyof VisualVisibility)[] = ["rdd"];
|
|
38
|
-
const
|
|
37
|
+
export const VISUAL_SECTION_KEYS = ["changes", "rdd", "agents", "todo", "usageCost", "modelDetails", "contextWarning"] as const satisfies readonly (keyof VisualVisibility)[];
|
|
38
|
+
/** Keys added after v1 shipped: stored settings may omit them together or one at a time, and they default to shown. */
|
|
39
|
+
const ADDED_SECTION_KEYS: readonly (keyof VisualVisibility)[] = ["rdd", "contextWarning"];
|
|
40
|
+
const BASE_SECTION_KEYS = VISUAL_SECTION_KEYS.filter((key) => !ADDED_SECTION_KEYS.includes(key));
|
|
39
41
|
interface VisualOptions { gentlePiConfigHome?: string }
|
|
40
42
|
export interface VisualResolution {
|
|
41
43
|
settings: VisualSettings;
|
|
@@ -64,13 +66,22 @@ export function isVisualSettings(value: unknown): value is VisualSettings {
|
|
|
64
66
|
&& keysMatch(visibility, VISUAL_SECTION_KEYS)
|
|
65
67
|
&& Object.values(visibility).every((item) => typeof item === "boolean");
|
|
66
68
|
}
|
|
67
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Reader-side validation: accepts the current shape plus every historical one.
|
|
71
|
+
* Every key must be known, every base key present, and every present value a
|
|
72
|
+
* boolean; missing added keys (`rdd`, `contextWarning`) then default to shown,
|
|
73
|
+
* while present values (including `rdd: false`) are preserved. Writers stay strict.
|
|
74
|
+
*/
|
|
68
75
|
export function normalizeVisualSettings(value: unknown): VisualSettings | undefined {
|
|
69
76
|
if (!record(value) || !record(value.visibility)) return undefined;
|
|
70
77
|
const visibility = value.visibility;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
if (!Object.keys(visibility).every((key) => (VISUAL_SECTION_KEYS as readonly string[]).includes(key))) return undefined;
|
|
79
|
+
if (!BASE_SECTION_KEYS.every((key) => Object.hasOwn(visibility, key))) return undefined;
|
|
80
|
+
// A key that is present is authoritative: only a missing added key may default to shown.
|
|
81
|
+
if (!Object.keys(visibility).every((key) => typeof visibility[key] === "boolean")) return undefined;
|
|
82
|
+
const candidate = keysMatch(visibility, VISUAL_SECTION_KEYS)
|
|
83
|
+
? value
|
|
84
|
+
: { ...value, visibility: Object.fromEntries(VISUAL_SECTION_KEYS.map((key) => [key, visibility[key] ?? true])) };
|
|
74
85
|
return isVisualSettings(candidate) ? candidate : undefined;
|
|
75
86
|
}
|
|
76
87
|
export function parseVisualSettingsFile(raw: string): VisualSettings | undefined {
|
|
@@ -29,14 +29,45 @@ function dropLastGrapheme(text: string): string {
|
|
|
29
29
|
return segments.length ? text.slice(0, segments[segments.length - 1]!.index) : text;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/** Category-aware footer; the audio card advertises audio keys, never the visual profiles pane. */
|
|
33
|
+
function footerHint(category: string, navOnly: boolean, previewLines: number, keyhint?: string): string {
|
|
34
|
+
if (navOnly) return "↑/↓ categories · → open · Esc close";
|
|
35
|
+
if (category === "Notifications") return keyhint ?? "Tab panes · Enter change · p test · Esc close";
|
|
36
|
+
return previewLines ? "←/→ or Tab panes · ↑/↓ move · Enter apply · p profiles · Esc close" : "Preview omitted · ←/→ panes · ↑/↓ move · Enter apply · Esc close";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A row is interactive only while its `visible()` check passes; a throwing check fails closed. */
|
|
40
|
+
function rowVisible(row: CustomizeRow): boolean {
|
|
41
|
+
if (!row.visible) return true;
|
|
42
|
+
try { return row.visible() !== false; } catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type CustomizeCategory = "Animations" | "Banner" | "Themes" | "Editor" | "History" | "Notifications" | "Layout" | "Cards" | "Sections" | "Profiles" | "Reset";
|
|
33
46
|
|
|
34
47
|
export interface CustomizeRow {
|
|
35
48
|
category?: CustomizeCategory;
|
|
36
49
|
label: string | (() => string);
|
|
37
50
|
/** Read-only representation of the highlighted choice, never an application. */
|
|
38
51
|
preview?: () => { title: string; sample: string };
|
|
39
|
-
|
|
52
|
+
/** When present and it returns false the row is hidden from its category; an exception fails closed. */
|
|
53
|
+
visible?: () => boolean;
|
|
54
|
+
/** Audio-footer hint for this row; omitted rows use the category default (never advertising `f WAV`). */
|
|
55
|
+
keyhint?: string;
|
|
56
|
+
/** Runs on Enter/Space. May await the inline bridge for a row-scoped edit or consent. */
|
|
57
|
+
action(inline: CustomizeInline): void | Promise<void>;
|
|
58
|
+
/** Row-scoped shortcuts, checked before panel shortcuts. Return `true` for a synchronous
|
|
59
|
+
* consumption, or a Promise for an asynchronous task (e.g. the inline WAV field) the view
|
|
60
|
+
* must keep busy until it settles so overlapping keys/actions cannot write stale state. */
|
|
61
|
+
key?(data: string, inline: CustomizeInline): boolean | Promise<unknown>;
|
|
62
|
+
}
|
|
63
|
+
/** Minimal in-card bridge so a row never opens a nested native dialog. */
|
|
64
|
+
export interface CustomizeInline {
|
|
65
|
+
/** Opens an inline field on the highlighted row; resolves on submit or undefined when cancelled. */
|
|
66
|
+
input(request: { prompt: string; value: string }): Promise<string | undefined>;
|
|
67
|
+
/** Opens an inline confirmation; resolves true only after an explicit, fully visible yes. */
|
|
68
|
+
confirm(message: string): Promise<boolean>;
|
|
69
|
+
/** Set once the owning card is disposed; a row must never persist after this. */
|
|
70
|
+
disposed: boolean;
|
|
40
71
|
}
|
|
41
72
|
export interface ProfileActions {
|
|
42
73
|
list(): VisualProfile[];
|
|
@@ -75,6 +106,98 @@ export class VisualCustomizeView {
|
|
|
75
106
|
private confirmation?: { action: "replace" | "apply" | "delete" | "reset"; name?: string; fingerprint?: string };
|
|
76
107
|
private confirmationVisible = false;
|
|
77
108
|
private profileBusy = false;
|
|
109
|
+
private inlineInput?: { prompt: string; value: string; resolve: (value: string | undefined) => void; visible: boolean };
|
|
110
|
+
private inlineConfirm?: { message: string; resolve: (value: boolean) => void; visible: boolean };
|
|
111
|
+
private readonly inline: CustomizeInline = {
|
|
112
|
+
input: request => this.openInlineInput(request),
|
|
113
|
+
confirm: message => this.openInlineConfirm(message),
|
|
114
|
+
disposed: false,
|
|
115
|
+
};
|
|
116
|
+
/** Header and diagnostic context for the active pane; Notifications is explicitly audio. */
|
|
117
|
+
title(): string { return (this.categories[this.categoryIndex] ?? "Settings") === "Notifications" ? "Audio notifications" : "Customization"; }
|
|
118
|
+
/** Ends the interaction and resolves any pending inline field/consent as cancelled. */
|
|
119
|
+
dispose(): void { this.closed = true; this.inline.disposed = true; this.cancelInline(); }
|
|
120
|
+
private cancelInline(): void {
|
|
121
|
+
const input = this.inlineInput;
|
|
122
|
+
if (input) { this.inlineInput = undefined; input.resolve(undefined); }
|
|
123
|
+
const confirm = this.inlineConfirm;
|
|
124
|
+
if (confirm) { this.inlineConfirm = undefined; confirm.resolve(false); }
|
|
125
|
+
}
|
|
126
|
+
private openInlineInput(request: { prompt: string; value: string }): Promise<string | undefined> {
|
|
127
|
+
if (this.closed) return Promise.resolve(undefined);
|
|
128
|
+
return new Promise(resolve => { this.inlineInput = { prompt: request.prompt, value: request.value, resolve, visible: false }; });
|
|
129
|
+
}
|
|
130
|
+
private openInlineConfirm(message: string): Promise<boolean> {
|
|
131
|
+
if (this.closed) return Promise.resolve(false);
|
|
132
|
+
return new Promise(resolve => { this.inlineConfirm = { message, resolve, visible: false }; });
|
|
133
|
+
}
|
|
134
|
+
/** Runs one exclusive row task, keeping `busy` set until it settles so no stale write can win. */
|
|
135
|
+
private withBusy(run: () => void | Promise<unknown>): void {
|
|
136
|
+
if (this.busy) return;
|
|
137
|
+
this.busy = true;
|
|
138
|
+
try {
|
|
139
|
+
void Promise.resolve(run()).catch((error: unknown) => {
|
|
140
|
+
this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
141
|
+
}).finally(() => { this.busy = false; if (!this.closed) this.options.requestRender(); });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
this.busy = false;
|
|
144
|
+
this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Wraps an inline prompt into at most `capacity` fully visible lines. */
|
|
148
|
+
private wrapInline(text: string, inner: number, capacity: number): string[] | undefined {
|
|
149
|
+
if (capacity < 1 || inner < 1) return undefined;
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
let remaining = text;
|
|
152
|
+
while (remaining.length) {
|
|
153
|
+
if (lines.length >= capacity) return undefined;
|
|
154
|
+
if (visibleWidth(remaining) <= inner) { lines.push(remaining); return lines; }
|
|
155
|
+
let chunk = "";
|
|
156
|
+
for (const { segment } of graphemes.segment(remaining)) {
|
|
157
|
+
if (visibleWidth(chunk + segment) > inner) break;
|
|
158
|
+
chunk += segment;
|
|
159
|
+
}
|
|
160
|
+
if (!chunk) return undefined;
|
|
161
|
+
lines.push(chunk);
|
|
162
|
+
remaining = remaining.slice(chunk.length);
|
|
163
|
+
}
|
|
164
|
+
return lines.length ? lines : undefined;
|
|
165
|
+
}
|
|
166
|
+
/** Recomputes the inline area from the live terminal height, never from stale render state. */
|
|
167
|
+
private previewCapacity(): { inner: number; lines: number; navOnly: boolean } | undefined {
|
|
168
|
+
const width = this.mainWidth;
|
|
169
|
+
if (width === undefined || width <= 0) return undefined;
|
|
170
|
+
const available = this.options.rowsAvailable?.() ?? 24;
|
|
171
|
+
const height = Number.isFinite(available) ? Math.max(0, Math.min(24, Math.floor(available))) : 0;
|
|
172
|
+
if (width < CARD_PADDING + 1 || height < 4) return undefined;
|
|
173
|
+
const navOnly = width < 60 && this.pane === "categories" && this.categories.length > 1;
|
|
174
|
+
return { inner: width - CARD_PADDING, lines: height >= 11 ? 3 : height >= 8 ? 1 : 0, navOnly };
|
|
175
|
+
}
|
|
176
|
+
private handleInlineInput(data: string): void {
|
|
177
|
+
const field = this.inlineInput!;
|
|
178
|
+
if (matchesKey(data, Key.escape)) { this.inlineInput = undefined; field.resolve(undefined); }
|
|
179
|
+
else if (data.startsWith("\x1b[200~")) {
|
|
180
|
+
const pasted = pastedText(data);
|
|
181
|
+
if (pasted !== undefined) field.value = appendInput(field.value, pasted);
|
|
182
|
+
} else if (matchesKey(data, Key.enter) || data.endsWith("\r") && safeText(data.slice(0, -1))) {
|
|
183
|
+
if (!matchesKey(data, Key.enter)) field.value = appendInput(field.value, data.slice(0, -1));
|
|
184
|
+
// Guard against a resize that hides the field before this Enter: an unseen value never submits.
|
|
185
|
+
const area = this.previewCapacity();
|
|
186
|
+
field.visible = area !== undefined && !area.navOnly && area.lines > 0 && visibleWidth(`${field.prompt}: ${field.value}▏`) <= area.inner;
|
|
187
|
+
if (field.visible) { this.inlineInput = undefined; field.resolve(field.value); }
|
|
188
|
+
} else if (matchesKey(data, Key.backspace) || data === "\x7f") field.value = dropLastGrapheme(field.value);
|
|
189
|
+
else { const text = decodeKittyPrintable(data) ?? data; if (safeText(text)) field.value = appendInput(field.value, text); }
|
|
190
|
+
this.options.requestRender();
|
|
191
|
+
}
|
|
192
|
+
private handleInlineConfirm(data: string): void {
|
|
193
|
+
const confirm = this.inlineConfirm!;
|
|
194
|
+
const area = this.previewCapacity();
|
|
195
|
+
confirm.visible = area !== undefined && !area.navOnly && area.lines > 0
|
|
196
|
+
&& this.wrapInline(`${confirm.message} y yes · any other key cancel`, area.inner, area.lines) !== undefined;
|
|
197
|
+
this.inlineConfirm = undefined;
|
|
198
|
+
confirm.resolve(matchesKey(data, "y") && confirm.visible);
|
|
199
|
+
this.options.requestRender();
|
|
200
|
+
}
|
|
78
201
|
openProfiles(): void { this.profileOpen = true; this.error = undefined; this.options.requestRender(); }
|
|
79
202
|
private profileAction(action: () => void | Promise<void>): void {
|
|
80
203
|
if (this.profileBusy) return;
|
|
@@ -176,18 +299,41 @@ export class VisualCustomizeView {
|
|
|
176
299
|
}
|
|
177
300
|
|
|
178
301
|
private categoryRows(): CustomizeRow[] {
|
|
179
|
-
return this.options.rows.filter(row => (row.category ?? "Settings") === this.categories[this.categoryIndex]);
|
|
302
|
+
return this.options.rows.filter(row => (row.category ?? "Settings") === this.categories[this.categoryIndex] && rowVisible(row));
|
|
180
303
|
}
|
|
181
304
|
|
|
182
305
|
handleInput(data: string): void {
|
|
183
306
|
if (this.closed || isKeyRelease(data)) return;
|
|
307
|
+
if (this.inlineInput) { this.handleInlineInput(data); return; }
|
|
308
|
+
if (this.inlineConfirm) { this.handleInlineConfirm(data); return; }
|
|
184
309
|
if (this.profileOpen && this.options.profiles) { this.handleProfiles(data); return; }
|
|
185
|
-
if (matchesKey(data, Key.escape)) { this.
|
|
186
|
-
if (this.options.profiles && matchesKey(data, "p")) { this.openProfiles(); return; }
|
|
310
|
+
if (matchesKey(data, Key.escape)) { this.dispose(); this.options.onClose(); return; }
|
|
187
311
|
const rows = this.categoryRows();
|
|
188
312
|
const length = this.pane === "categories" ? this.categories.length : rows.length;
|
|
313
|
+
if (length) {
|
|
314
|
+
this.selected = Math.min(this.controls.get(this.categories[this.categoryIndex]!) ?? 0, Math.max(0, rows.length - 1));
|
|
315
|
+
// Row shortcuts outrank panel shortcuts (Notifications `p` previews instead of opening profiles).
|
|
316
|
+
if (this.pane === "controls" && !this.busy) {
|
|
317
|
+
let consumed = false;
|
|
318
|
+
try {
|
|
319
|
+
const keyed = rows[this.selected]?.key?.(data, this.inline);
|
|
320
|
+
if (keyed !== undefined && keyed !== false) {
|
|
321
|
+
consumed = true;
|
|
322
|
+
// An async key (local WAV field + validation) owns the card until it settles.
|
|
323
|
+
if (typeof (keyed as Promise<unknown>).then === "function") this.withBusy(() => keyed as Promise<unknown>);
|
|
324
|
+
}
|
|
325
|
+
} catch (error) {
|
|
326
|
+
consumed = true;
|
|
327
|
+
this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
328
|
+
}
|
|
329
|
+
if (consumed) { this.options.requestRender(); return; }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Notifications rows own `p` for a per-event preview. When no event row is
|
|
333
|
+
// selected the panel must consume it instead of opening visual profiles.
|
|
334
|
+
if (matchesKey(data, "p") && this.pane === "controls" && (this.categories[this.categoryIndex] ?? "Settings") === "Notifications") { this.options.requestRender(); return; }
|
|
335
|
+
if (this.options.profiles && matchesKey(data, "p")) { this.openProfiles(); return; }
|
|
189
336
|
if (!length) return;
|
|
190
|
-
this.selected = Math.min(this.controls.get(this.categories[this.categoryIndex]!) ?? 0, Math.max(0, rows.length - 1));
|
|
191
337
|
if (matchesKey(data, Key.left) || matchesKey(data, Key.tab) && this.pane === "controls") this.pane = "categories";
|
|
192
338
|
else if (matchesKey(data, Key.right) || matchesKey(data, Key.tab) && this.pane === "categories") this.pane = "controls";
|
|
193
339
|
else if (matchesKey(data, Key.up) || matchesKey(data, "k")) {
|
|
@@ -200,15 +346,7 @@ export class VisualCustomizeView {
|
|
|
200
346
|
else if ((matchesKey(data, Key.enter) || matchesKey(data, Key.space)) && !this.busy &&
|
|
201
347
|
this.mainWidth !== undefined && this.mainWidth >= CARD_PADDING + 3 &&
|
|
202
348
|
(this.options.rowsAvailable?.() ?? 24) >= 4 && rows[this.selected]) {
|
|
203
|
-
this.
|
|
204
|
-
try {
|
|
205
|
-
void Promise.resolve(rows[this.selected]?.action()).catch((error: unknown) => {
|
|
206
|
-
this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
207
|
-
}).finally(() => { this.busy = false; if (!this.closed) this.options.requestRender(); });
|
|
208
|
-
} catch (error) {
|
|
209
|
-
this.busy = false;
|
|
210
|
-
this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
211
|
-
}
|
|
349
|
+
this.withBusy(() => rows[this.selected]!.action(this.inline));
|
|
212
350
|
}
|
|
213
351
|
this.options.requestRender();
|
|
214
352
|
}
|
|
@@ -225,7 +363,7 @@ export class VisualCustomizeView {
|
|
|
225
363
|
if (!height) return [];
|
|
226
364
|
// A card needs two border rows. At extreme dimensions keep the old
|
|
227
365
|
// read-only heading rather than letting invisible controls be activated.
|
|
228
|
-
if (width < CARD_PADDING + 1 || height < 4) return Array.from({ length: Math.min(height, 2) }, (_, i) => truncateToWidth(i ? "Enlarge terminal" :
|
|
366
|
+
if (width < CARD_PADDING + 1 || height < 4) return Array.from({ length: Math.min(height, 2) }, (_, i) => truncateToWidth(i ? "Enlarge terminal" : this.title(), width, ""));
|
|
229
367
|
const inner = width - CARD_PADDING;
|
|
230
368
|
const category = this.categories[this.categoryIndex] ?? "Settings";
|
|
231
369
|
const rows = this.categoryRows();
|
|
@@ -250,7 +388,7 @@ export class VisualCustomizeView {
|
|
|
250
388
|
};
|
|
251
389
|
const join = (left: string, right: string) => leftWidth ? `${left}${" ".repeat(Math.max(0, leftWidth - visibleWidth(stripAnsi(left))))} ${paint("│", 1, "border")} ${right}` : right;
|
|
252
390
|
const heading = navOnly ? "Categories · choose a category →" : `${category} · ${rows.length ? `${this.selected + 1}/${rows.length}` : "empty"}`;
|
|
253
|
-
const lines: string[] = [paint(
|
|
391
|
+
const lines: string[] = [paint(this.title(), inner, "accent")];
|
|
254
392
|
// Commands uses a header, grouped section labels, whitespace and a footer.
|
|
255
393
|
if (showHeading) lines.push(join(leftWidth ? paint("CATEGORIES", leftWidth, "accent") : "", paint(heading, rightWidth, "accent")));
|
|
256
394
|
if (showTopGap) lines.push("");
|
|
@@ -262,7 +400,19 @@ export class VisualCustomizeView {
|
|
|
262
400
|
const right = item ? renderPaletteSelection(item, rightWidth, start + i === position, navOnly || this.pane === "controls" ? this.options.theme : { fg: this.options.theme.fg.bind(this.options.theme) }) : rows.length ? "" : paint("No settings available", rightWidth, "muted");
|
|
263
401
|
lines.push(join(left, right));
|
|
264
402
|
}
|
|
265
|
-
if (
|
|
403
|
+
if (this.inlineConfirm) {
|
|
404
|
+
const shows = !navOnly && previewLines > 0 ? this.wrapInline(`${this.inlineConfirm.message} y yes · any other key cancel`, inner, previewLines) : undefined;
|
|
405
|
+
this.inlineConfirm.visible = shows !== undefined;
|
|
406
|
+
if (shows) for (const line of shows) lines.push(paint(line, inner, "warning"));
|
|
407
|
+
else if (previewLines > 0) lines.push(paint("Confirmation needs more space · enlarge terminal", inner, "warning"));
|
|
408
|
+
} else if (this.inlineInput) {
|
|
409
|
+
const field = `${this.inlineInput.prompt}: ${this.inlineInput.value}▏`;
|
|
410
|
+
this.inlineInput.visible = !navOnly && previewLines > 0 && visibleWidth(field) <= inner;
|
|
411
|
+
if (previewLines > 0) {
|
|
412
|
+
lines.push(paint(field, inner, "accent"));
|
|
413
|
+
if (previewLines === 3) lines.push(paint(this.inlineInput.visible ? "Enter confirm · Esc cancel" : "Value too wide · enlarge terminal · Esc cancel", inner, "muted"));
|
|
414
|
+
}
|
|
415
|
+
} else if (previewLines === 3) {
|
|
266
416
|
let preview: ReturnType<NonNullable<CustomizeRow["preview"]>> | undefined;
|
|
267
417
|
try { if (!navOnly) preview = rows[this.selected]?.preview?.(); } catch { /* Never substitute the active theme for an unreadable source. */ }
|
|
268
418
|
lines.push("");
|
|
@@ -270,9 +420,10 @@ export class VisualCustomizeView {
|
|
|
270
420
|
lines.push(paint(navOnly ? "" : preview?.sample ?? "No read-only sample for this choice", inner, "muted"));
|
|
271
421
|
} else if (previewLines === 1) lines.push(paint(navOnly ? "→ controls · Esc close" : "Preview omitted · enlarge terminal", inner, "muted"));
|
|
272
422
|
if (showBottomGap) lines.push("");
|
|
273
|
-
if (showHeading) lines.push(paint(navOnly
|
|
423
|
+
if (showHeading) lines.push(paint(footerHint(category, navOnly, previewLines, rows[this.selected]?.keyhint), inner, "muted"));
|
|
274
424
|
// The top-right close hint mirrors Commands without consuming another row.
|
|
275
|
-
|
|
425
|
+
const title = this.title();
|
|
426
|
+
lines[0] = paint(title + " ".repeat(Math.max(1, inner - visibleWidth(title) - 3)) + "esc", inner, "accent");
|
|
276
427
|
return renderPaletteCard(lines.slice(0, height - 2), width, this.options.theme);
|
|
277
428
|
}
|
|
278
429
|
private detailSize(width: number): { height: number; inner: number; capacity: number } {
|