gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { gentlePiConfigHome } from "./agent-home.ts";
|
|
5
|
+
|
|
6
|
+
export const VISUAL_SCHEMA = "gentle-pi.visual-customization/v1";
|
|
7
|
+
export const STATUS_PLACEMENT = { AUTO: "auto", RIGHT: "right", BOTTOM: "bottom", HIDDEN: "hidden" } as const;
|
|
8
|
+
export const HEADER_PLACEMENT = { TOP: "top", BELOW_INPUT: "below-input" } as const;
|
|
9
|
+
export const DENSITY = { COMFORTABLE: "comfortable", COMPACT: "compact", MINIMAL: "minimal" } as const;
|
|
10
|
+
export type StatusPlacement = (typeof STATUS_PLACEMENT)[keyof typeof STATUS_PLACEMENT];
|
|
11
|
+
export type HeaderPlacement = (typeof HEADER_PLACEMENT)[keyof typeof HEADER_PLACEMENT];
|
|
12
|
+
export type Density = (typeof DENSITY)[keyof typeof DENSITY];
|
|
13
|
+
|
|
14
|
+
export interface VisualVisibility {
|
|
15
|
+
changes: boolean;
|
|
16
|
+
rdd: boolean;
|
|
17
|
+
agents: boolean;
|
|
18
|
+
todo: boolean;
|
|
19
|
+
usageCost: boolean;
|
|
20
|
+
modelDetails: boolean;
|
|
21
|
+
/** High-context warning notice; independent from audio notifications. */
|
|
22
|
+
contextWarning: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface VisualSettings {
|
|
25
|
+
statusPlacement: StatusPlacement;
|
|
26
|
+
headerPlacement: HeaderPlacement;
|
|
27
|
+
density: Density;
|
|
28
|
+
visibility: VisualVisibility;
|
|
29
|
+
}
|
|
30
|
+
export const DEFAULT_VISUAL_SETTINGS: VisualSettings = {
|
|
31
|
+
statusPlacement: STATUS_PLACEMENT.AUTO,
|
|
32
|
+
headerPlacement: HEADER_PLACEMENT.TOP,
|
|
33
|
+
density: DENSITY.COMFORTABLE,
|
|
34
|
+
visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true },
|
|
35
|
+
};
|
|
36
|
+
/** Sections row order; `rdd` follows `changes` as the RDD group follows Changes in the Status card. */
|
|
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));
|
|
41
|
+
interface VisualOptions { gentlePiConfigHome?: string }
|
|
42
|
+
export interface VisualResolution {
|
|
43
|
+
settings: VisualSettings;
|
|
44
|
+
source: "global_file" | "default";
|
|
45
|
+
malformed: boolean;
|
|
46
|
+
readError: boolean;
|
|
47
|
+
globalFile: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function keysMatch(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
54
|
+
return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
55
|
+
}
|
|
56
|
+
function member<T extends string>(value: unknown, choices: Record<string, T>): value is T {
|
|
57
|
+
return typeof value === "string" && Object.values(choices).includes(value as T);
|
|
58
|
+
}
|
|
59
|
+
export function isVisualSettings(value: unknown): value is VisualSettings {
|
|
60
|
+
if (!record(value) || !keysMatch(value, ["statusPlacement", "headerPlacement", "density", "visibility"])) return false;
|
|
61
|
+
const visibility = value.visibility;
|
|
62
|
+
return member(value.statusPlacement, STATUS_PLACEMENT)
|
|
63
|
+
&& member(value.headerPlacement, HEADER_PLACEMENT)
|
|
64
|
+
&& member(value.density, DENSITY)
|
|
65
|
+
&& record(visibility)
|
|
66
|
+
&& keysMatch(visibility, VISUAL_SECTION_KEYS)
|
|
67
|
+
&& Object.values(visibility).every((item) => typeof item === "boolean");
|
|
68
|
+
}
|
|
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
|
+
*/
|
|
75
|
+
export function normalizeVisualSettings(value: unknown): VisualSettings | undefined {
|
|
76
|
+
if (!record(value) || !record(value.visibility)) return undefined;
|
|
77
|
+
const visibility = value.visibility;
|
|
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])) };
|
|
85
|
+
return isVisualSettings(candidate) ? candidate : undefined;
|
|
86
|
+
}
|
|
87
|
+
export function parseVisualSettingsFile(raw: string): VisualSettings | undefined {
|
|
88
|
+
try {
|
|
89
|
+
const parsed: unknown = JSON.parse(raw);
|
|
90
|
+
if (!record(parsed) || parsed.schema !== VISUAL_SCHEMA) return undefined;
|
|
91
|
+
const { schema: _schema, ...settings } = parsed;
|
|
92
|
+
return normalizeVisualSettings(settings);
|
|
93
|
+
} catch { return undefined; }
|
|
94
|
+
}
|
|
95
|
+
export function resolveVisualSettings(options: VisualOptions = {}): VisualResolution {
|
|
96
|
+
const globalFile = join(options.gentlePiConfigHome ?? gentlePiConfigHome(), "visual-customization.json");
|
|
97
|
+
try {
|
|
98
|
+
const settings = parseVisualSettingsFile(readFileSync(globalFile, "utf8"));
|
|
99
|
+
return { settings: settings ?? structuredClone(DEFAULT_VISUAL_SETTINGS), source: "global_file", malformed: settings === undefined, readError: false, globalFile };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const missing = record(error) && error.code === "ENOENT";
|
|
102
|
+
return { settings: structuredClone(DEFAULT_VISUAL_SETTINGS), source: missing ? "default" : "global_file", malformed: false, readError: !missing, globalFile };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Same-directory rename keeps partial JSON invisible to readers. */
|
|
106
|
+
export function writeVisualSettings(settings: VisualSettings, options: VisualOptions = {}): string {
|
|
107
|
+
if (!isVisualSettings(settings)) throw new TypeError("Invalid visual settings");
|
|
108
|
+
const home = options.gentlePiConfigHome ?? gentlePiConfigHome();
|
|
109
|
+
const path = join(home, "visual-customization.json");
|
|
110
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
111
|
+
mkdirSync(home, { recursive: true });
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(temporary, `${JSON.stringify({ schema: VISUAL_SCHEMA, ...settings })}\n`, { flag: "wx", mode: 0o600 });
|
|
114
|
+
renameSync(temporary, path);
|
|
115
|
+
} finally {
|
|
116
|
+
try { unlinkSync(temporary); } catch { /* Rename consumed the temporary file. */ }
|
|
117
|
+
}
|
|
118
|
+
return path;
|
|
119
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { decodeKittyPrintable, isKeyRelease, matchesKey, Key, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { VisualProfile } from "./visual-profiles.ts";
|
|
3
|
+
import { CARD_PADDING, renderPaletteCard, renderPaletteSelection } from "./command-palette.ts";
|
|
4
|
+
import { stripAnsi } from "./terminal-theme.ts";
|
|
5
|
+
|
|
6
|
+
const MAX_INPUT_CODEPOINTS = 512;
|
|
7
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
8
|
+
|
|
9
|
+
function safeText(text: string): boolean {
|
|
10
|
+
return [...text].every((character) => character === "\u200c" || character === "\u200d" || !/[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u.test(character));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function appendInput(current: string, addition: string): string {
|
|
14
|
+
if (!safeText(addition)) return current;
|
|
15
|
+
return current + [...addition].slice(0, Math.max(0, MAX_INPUT_CODEPOINTS - [...current].length)).join("");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Pi 0.85.1's terminal forwards bracketed paste as one wrapped input event
|
|
19
|
+
// to custom overlays. Only unwrap a complete frame, never terminal controls.
|
|
20
|
+
function pastedText(data: string): string | undefined {
|
|
21
|
+
const start = "\x1b[200~", end = "\x1b[201~";
|
|
22
|
+
if (!data.startsWith(start) || !data.endsWith(end)) return undefined;
|
|
23
|
+
const text = data.slice(start.length, -end.length);
|
|
24
|
+
return safeText(text) ? text : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function dropLastGrapheme(text: string): string {
|
|
28
|
+
const segments = [...graphemes.segment(text)];
|
|
29
|
+
return segments.length ? text.slice(0, segments[segments.length - 1]!.index) : text;
|
|
30
|
+
}
|
|
31
|
+
|
|
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";
|
|
46
|
+
|
|
47
|
+
export interface CustomizeRow {
|
|
48
|
+
category?: CustomizeCategory;
|
|
49
|
+
label: string | (() => string);
|
|
50
|
+
/** Read-only representation of the highlighted choice, never an application. */
|
|
51
|
+
preview?: () => { title: string; sample: string };
|
|
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;
|
|
71
|
+
}
|
|
72
|
+
export interface ProfileActions {
|
|
73
|
+
list(): VisualProfile[];
|
|
74
|
+
save(name: string, replace: boolean): void | Promise<void>;
|
|
75
|
+
apply(name: string): void | Promise<void>;
|
|
76
|
+
delete(name: string): void | Promise<void>;
|
|
77
|
+
reset(): void | Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
export interface CustomizeViewOptions {
|
|
80
|
+
profiles?: ProfileActions;
|
|
81
|
+
rows: CustomizeRow[];
|
|
82
|
+
theme: { fg(color: string, text: string): string; bg?(color: string, text: string): string };
|
|
83
|
+
requestRender(): void;
|
|
84
|
+
rowsAvailable?: () => number;
|
|
85
|
+
onError?: (error: Error) => void;
|
|
86
|
+
onClose(): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One interaction per instance; the selection survives updates to store-backed labels. */
|
|
90
|
+
export class VisualCustomizeView {
|
|
91
|
+
private selected = 0;
|
|
92
|
+
private mainWidth?: number;
|
|
93
|
+
private categoryIndex = 0;
|
|
94
|
+
private pane: "categories" | "controls";
|
|
95
|
+
private readonly categories: string[];
|
|
96
|
+
private readonly controls = new Map<string, number>();
|
|
97
|
+
private closed = false;
|
|
98
|
+
private busy = false;
|
|
99
|
+
private readonly options: CustomizeViewOptions;
|
|
100
|
+
private error?: string;
|
|
101
|
+
private profileOpen = false;
|
|
102
|
+
private profileIndex = 0;
|
|
103
|
+
private profileInput?: string;
|
|
104
|
+
private profileWidth?: number;
|
|
105
|
+
private profileInputVisible = false;
|
|
106
|
+
private confirmation?: { action: "replace" | "apply" | "delete" | "reset"; name?: string; fingerprint?: string };
|
|
107
|
+
private confirmationVisible = false;
|
|
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
|
+
}
|
|
201
|
+
openProfiles(): void { this.profileOpen = true; this.error = undefined; this.options.requestRender(); }
|
|
202
|
+
private profileAction(action: () => void | Promise<void>): void {
|
|
203
|
+
if (this.profileBusy) return;
|
|
204
|
+
this.profileBusy = true;
|
|
205
|
+
try {
|
|
206
|
+
void Promise.resolve(action()).catch((error: unknown) => {
|
|
207
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
208
|
+
this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
209
|
+
}).finally(() => { this.profileBusy = false; this.options.requestRender(); });
|
|
210
|
+
} catch (error) {
|
|
211
|
+
this.profileBusy = false;
|
|
212
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
213
|
+
this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
private confirmationLines(choice: NonNullable<VisualCustomizeView["confirmation"]>, inner: number, capacity: number): string[] | undefined {
|
|
217
|
+
const prompt = `Confirm ${choice.action} ${choice.name ?? "catalog"}? y yes · any other key cancel`;
|
|
218
|
+
const lines: string[] = [];
|
|
219
|
+
if (visibleWidth(prompt) <= inner) lines.push(prompt);
|
|
220
|
+
else {
|
|
221
|
+
lines.push(`Confirm ${choice.action}`);
|
|
222
|
+
let remaining = `${choice.name ?? "catalog"}?`;
|
|
223
|
+
while (remaining.length && inner > 0) {
|
|
224
|
+
let chunk = "";
|
|
225
|
+
for (const { segment } of graphemes.segment(remaining)) {
|
|
226
|
+
if (visibleWidth(chunk + segment) > inner) break;
|
|
227
|
+
chunk += segment;
|
|
228
|
+
}
|
|
229
|
+
if (!chunk) return undefined;
|
|
230
|
+
lines.push(chunk);
|
|
231
|
+
remaining = remaining.slice(chunk.length);
|
|
232
|
+
}
|
|
233
|
+
if (remaining.length) return undefined;
|
|
234
|
+
lines.push("y yes · any other key cancel");
|
|
235
|
+
}
|
|
236
|
+
return lines.length <= capacity && lines.every(line => visibleWidth(line) <= inner) ? lines : undefined;
|
|
237
|
+
}
|
|
238
|
+
private handleProfiles(data: string): void {
|
|
239
|
+
const profiles = this.options.profiles!;
|
|
240
|
+
if (matchesKey(data, Key.escape)) {
|
|
241
|
+
if (this.profileInput !== undefined) this.profileInput = undefined;
|
|
242
|
+
else if (this.confirmation) { this.confirmation = undefined; this.confirmationVisible = false; }
|
|
243
|
+
else this.profileOpen = false;
|
|
244
|
+
} else if (this.profileInput !== undefined) {
|
|
245
|
+
if (data.startsWith("\x1b[200~")) {
|
|
246
|
+
const pasted = pastedText(data);
|
|
247
|
+
if (pasted !== undefined) this.profileInput = appendInput(this.profileInput, pasted);
|
|
248
|
+
} else if (matchesKey(data, Key.enter) || data.endsWith("\r") && safeText(data.slice(0, -1))) {
|
|
249
|
+
if (!matchesKey(data, Key.enter)) this.profileInput = appendInput(this.profileInput, data.slice(0, -1));
|
|
250
|
+
// A resize or error may hide an open field. Never save a value that
|
|
251
|
+
// cannot fit in the current profile card at its last rendered width.
|
|
252
|
+
if (this.profileWidth !== undefined) this.renderProfiles(this.profileWidth);
|
|
253
|
+
if (this.profileInputVisible) {
|
|
254
|
+
const name = this.profileInput;
|
|
255
|
+
this.profileInput = undefined;
|
|
256
|
+
this.profileInputVisible = false;
|
|
257
|
+
this.profileAction(() => profiles.save(name, false));
|
|
258
|
+
}
|
|
259
|
+
} else if (matchesKey(data, Key.backspace) || data === "\x7f") this.profileInput = dropLastGrapheme(this.profileInput);
|
|
260
|
+
else { const text = decodeKittyPrintable(data) ?? data; if (safeText(text)) this.profileInput = appendInput(this.profileInput, text); }
|
|
261
|
+
} else if (this.confirmation) {
|
|
262
|
+
const choice = this.confirmation;
|
|
263
|
+
const width = this.profileWidth;
|
|
264
|
+
const size = width === undefined ? undefined : this.detailSize(width);
|
|
265
|
+
const visible = this.confirmationVisible && size !== undefined && size.height >= 3 && size.inner >= 1
|
|
266
|
+
&& this.confirmationLines(choice, size.inner, size.capacity) !== undefined;
|
|
267
|
+
this.confirmation = undefined;
|
|
268
|
+
this.confirmationVisible = false;
|
|
269
|
+
if (matchesKey(data, "y") && visible) {
|
|
270
|
+
if (choice.action === "reset") this.profileAction(() => profiles.reset());
|
|
271
|
+
else if (choice.name) {
|
|
272
|
+
const name = choice.name;
|
|
273
|
+
try {
|
|
274
|
+
const current = profiles.list().find(item => item.name === name);
|
|
275
|
+
if (!current || JSON.stringify(current) !== choice.fingerprint) this.error = "Profile changed; choose again.";
|
|
276
|
+
else this.profileAction(() => choice.action === "replace" ? profiles.save(name, true) : choice.action === "apply" ? profiles.apply(name) : profiles.delete(name));
|
|
277
|
+
} catch (error) { this.error = error instanceof Error ? error.message : String(error); }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else if (!this.profileBusy) {
|
|
281
|
+
let items: VisualProfile[];
|
|
282
|
+
try { items = profiles.list(); } catch (error) { this.error = error instanceof Error ? error.message : String(error); this.options.requestRender(); return; }
|
|
283
|
+
if (matchesKey(data, Key.up) || matchesKey(data, "k")) this.profileIndex = (this.profileIndex + items.length - 1) % (items.length || 1);
|
|
284
|
+
else if (matchesKey(data, Key.down) || matchesKey(data, "j")) this.profileIndex = (this.profileIndex + 1) % (items.length || 1);
|
|
285
|
+
else if (matchesKey(data, "s")) { this.profileInput = ""; this.profileInputVisible = false; }
|
|
286
|
+
else if (matchesKey(data, "z")) { this.confirmation = { action: "reset" }; this.confirmationVisible = false; }
|
|
287
|
+
else if (items.length && (matchesKey(data, "r") || matchesKey(data, "a") || matchesKey(data, "d"))) {
|
|
288
|
+
const target = items[this.profileIndex]!;
|
|
289
|
+
this.confirmation = { action: matchesKey(data, "r") ? "replace" : matchesKey(data, "a") ? "apply" : "delete", name: target.name, fingerprint: JSON.stringify(target) };
|
|
290
|
+
this.confirmationVisible = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
this.options.requestRender();
|
|
294
|
+
}
|
|
295
|
+
constructor(options: CustomizeViewOptions) {
|
|
296
|
+
this.options = options;
|
|
297
|
+
this.categories = [...new Set(options.rows.map(row => row.category ?? "Settings"))];
|
|
298
|
+
this.pane = options.rows.some(row => row.category) ? "categories" : "controls";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private categoryRows(): CustomizeRow[] {
|
|
302
|
+
return this.options.rows.filter(row => (row.category ?? "Settings") === this.categories[this.categoryIndex] && rowVisible(row));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
handleInput(data: string): void {
|
|
306
|
+
if (this.closed || isKeyRelease(data)) return;
|
|
307
|
+
if (this.inlineInput) { this.handleInlineInput(data); return; }
|
|
308
|
+
if (this.inlineConfirm) { this.handleInlineConfirm(data); return; }
|
|
309
|
+
if (this.profileOpen && this.options.profiles) { this.handleProfiles(data); return; }
|
|
310
|
+
if (matchesKey(data, Key.escape)) { this.dispose(); this.options.onClose(); return; }
|
|
311
|
+
const rows = this.categoryRows();
|
|
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; }
|
|
336
|
+
if (!length) return;
|
|
337
|
+
if (matchesKey(data, Key.left) || matchesKey(data, Key.tab) && this.pane === "controls") this.pane = "categories";
|
|
338
|
+
else if (matchesKey(data, Key.right) || matchesKey(data, Key.tab) && this.pane === "categories") this.pane = "controls";
|
|
339
|
+
else if (matchesKey(data, Key.up) || matchesKey(data, "k")) {
|
|
340
|
+
if (this.pane === "categories") this.categoryIndex = (this.categoryIndex + length - 1) % length;
|
|
341
|
+
else this.controls.set(this.categories[this.categoryIndex]!, (this.selected + length - 1) % length);
|
|
342
|
+
} else if (matchesKey(data, Key.down) || matchesKey(data, "j")) {
|
|
343
|
+
if (this.pane === "categories") this.categoryIndex = (this.categoryIndex + 1) % length;
|
|
344
|
+
else this.controls.set(this.categories[this.categoryIndex]!, (this.selected + 1) % length);
|
|
345
|
+
} else if ((matchesKey(data, Key.enter) || matchesKey(data, Key.space)) && this.pane === "categories") this.pane = "controls";
|
|
346
|
+
else if ((matchesKey(data, Key.enter) || matchesKey(data, Key.space)) && !this.busy &&
|
|
347
|
+
this.mainWidth !== undefined && this.mainWidth >= CARD_PADDING + 3 &&
|
|
348
|
+
(this.options.rowsAvailable?.() ?? 24) >= 4 && rows[this.selected]) {
|
|
349
|
+
this.withBusy(() => rows[this.selected]!.action(this.inline));
|
|
350
|
+
}
|
|
351
|
+
this.options.requestRender();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
render(width: number): string[] {
|
|
355
|
+
if (this.profileOpen && this.options.profiles) {
|
|
356
|
+
if (width <= 0) { this.confirmationVisible = false; return []; }
|
|
357
|
+
return this.renderProfiles(width);
|
|
358
|
+
}
|
|
359
|
+
this.mainWidth = width;
|
|
360
|
+
if (width <= 0) { this.confirmationVisible = false; return []; }
|
|
361
|
+
const available = this.options.rowsAvailable?.() ?? 24;
|
|
362
|
+
const height = Number.isFinite(available) ? Math.max(0, Math.min(24, Math.floor(available))) : 0;
|
|
363
|
+
if (!height) return [];
|
|
364
|
+
// A card needs two border rows. At extreme dimensions keep the old
|
|
365
|
+
// read-only heading rather than letting invisible controls be activated.
|
|
366
|
+
if (width < CARD_PADDING + 1 || height < 4) return Array.from({ length: Math.min(height, 2) }, (_, i) => truncateToWidth(i ? "Enlarge terminal" : this.title(), width, ""));
|
|
367
|
+
const inner = width - CARD_PADDING;
|
|
368
|
+
const category = this.categories[this.categoryIndex] ?? "Settings";
|
|
369
|
+
const rows = this.categoryRows();
|
|
370
|
+
const narrow = width < 60;
|
|
371
|
+
const navOnly = narrow && this.pane === "categories" && this.categories.length > 1;
|
|
372
|
+
const leftWidth = narrow ? 0 : Math.min(20, Math.max(14, Math.floor(inner * 0.28)));
|
|
373
|
+
const rightWidth = inner - leftWidth - (leftWidth ? 3 : 0);
|
|
374
|
+
const previewLines = height >= 11 ? 3 : height >= 8 ? 1 : 0;
|
|
375
|
+
const showHeading = height >= 6;
|
|
376
|
+
const showTopGap = height >= 12;
|
|
377
|
+
const showBottomGap = height >= 9;
|
|
378
|
+
const chrome = 1 + Number(showHeading) * 2 + Number(showTopGap) + Number(showBottomGap);
|
|
379
|
+
const contentHeight = Math.max(1, height - 2 - chrome - previewLines);
|
|
380
|
+
this.selected = Math.min(this.controls.get(category) ?? 0, Math.max(0, rows.length - 1));
|
|
381
|
+
const position = navOnly ? this.categoryIndex : this.selected;
|
|
382
|
+
const listLength = navOnly ? this.categories.length : rows.length;
|
|
383
|
+
const start = Math.min(Math.max(0, position - contentHeight + 1), Math.max(0, listLength - contentHeight));
|
|
384
|
+
const navStart = Math.min(Math.max(0, this.categoryIndex - contentHeight + 1), Math.max(0, this.categories.length - contentHeight));
|
|
385
|
+
const paint = (text: string, size: number, role = "text") => {
|
|
386
|
+
const clipped = truncateToWidth(text, Math.max(0, size), "…");
|
|
387
|
+
return this.options.theme.fg(role, clipped + " ".repeat(Math.max(0, size - visibleWidth(clipped))));
|
|
388
|
+
};
|
|
389
|
+
const join = (left: string, right: string) => leftWidth ? `${left}${" ".repeat(Math.max(0, leftWidth - visibleWidth(stripAnsi(left))))} ${paint("│", 1, "border")} ${right}` : right;
|
|
390
|
+
const heading = navOnly ? "Categories · choose a category →" : `${category} · ${rows.length ? `${this.selected + 1}/${rows.length}` : "empty"}`;
|
|
391
|
+
const lines: string[] = [paint(this.title(), inner, "accent")];
|
|
392
|
+
// Commands uses a header, grouped section labels, whitespace and a footer.
|
|
393
|
+
if (showHeading) lines.push(join(leftWidth ? paint("CATEGORIES", leftWidth, "accent") : "", paint(heading, rightWidth, "accent")));
|
|
394
|
+
if (showTopGap) lines.push("");
|
|
395
|
+
for (let i = 0; i < contentHeight; i++) {
|
|
396
|
+
const nav = this.categories[navStart + i];
|
|
397
|
+
const left = nav ? renderPaletteSelection(nav, leftWidth, navStart + i === this.categoryIndex, this.pane === "categories" ? this.options.theme : { fg: this.options.theme.fg.bind(this.options.theme) }) : "";
|
|
398
|
+
const row = rows[start + i];
|
|
399
|
+
const item = navOnly ? this.categories[start + i] : row ? typeof row.label === "function" ? row.label() : row.label : undefined;
|
|
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");
|
|
401
|
+
lines.push(join(left, right));
|
|
402
|
+
}
|
|
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) {
|
|
416
|
+
let preview: ReturnType<NonNullable<CustomizeRow["preview"]>> | undefined;
|
|
417
|
+
try { if (!navOnly) preview = rows[this.selected]?.preview?.(); } catch { /* Never substitute the active theme for an unreadable source. */ }
|
|
418
|
+
lines.push("");
|
|
419
|
+
lines.push(paint(navOnly ? "→ controls · Esc close" : preview ? `Preview · ${preview.title}` : "Preview unavailable", inner, "accent"));
|
|
420
|
+
lines.push(paint(navOnly ? "" : preview?.sample ?? "No read-only sample for this choice", inner, "muted"));
|
|
421
|
+
} else if (previewLines === 1) lines.push(paint(navOnly ? "→ controls · Esc close" : "Preview omitted · enlarge terminal", inner, "muted"));
|
|
422
|
+
if (showBottomGap) lines.push("");
|
|
423
|
+
if (showHeading) lines.push(paint(footerHint(category, navOnly, previewLines, rows[this.selected]?.keyhint), inner, "muted"));
|
|
424
|
+
// The top-right close hint mirrors Commands without consuming another row.
|
|
425
|
+
const title = this.title();
|
|
426
|
+
lines[0] = paint(title + " ".repeat(Math.max(1, inner - visibleWidth(title) - 3)) + "esc", inner, "accent");
|
|
427
|
+
return renderPaletteCard(lines.slice(0, height - 2), width, this.options.theme);
|
|
428
|
+
}
|
|
429
|
+
private detailSize(width: number): { height: number; inner: number; capacity: number } {
|
|
430
|
+
const available = this.options.rowsAvailable?.() ?? 24;
|
|
431
|
+
const height = Number.isFinite(available) ? Math.max(0, Math.min(24, Math.floor(available))) : 0;
|
|
432
|
+
return { height, inner: Math.max(0, width - CARD_PADDING), capacity: Math.max(0, height - 2) };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private detailFrame(lines: string[], width: number, height: number): string[] {
|
|
436
|
+
if (!height || width <= 0) return [];
|
|
437
|
+
if (width < CARD_PADDING + 1 || height < 3) return lines.slice(0, height).map(line => truncateToWidth(stripAnsi(line), width, ""));
|
|
438
|
+
return renderPaletteCard(lines.slice(0, height - 2), width, this.options.theme);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private renderProfiles(width: number): string[] {
|
|
442
|
+
this.profileWidth = width;
|
|
443
|
+
this.profileInputVisible = false;
|
|
444
|
+
const { height, inner, capacity } = this.detailSize(width);
|
|
445
|
+
if (height < 3 || inner < 1) { this.confirmationVisible = false; return this.detailFrame(["Visual profiles", "Enlarge terminal"], width, height); }
|
|
446
|
+
let items: VisualProfile[];
|
|
447
|
+
try { items = this.options.profiles!.list(); } catch (error) {
|
|
448
|
+
this.confirmationVisible = false;
|
|
449
|
+
return this.detailFrame([this.options.theme.fg("accent", "Visual profiles"), `Profiles unavailable: ${error instanceof Error ? error.message : String(error)}`, "Esc back"], width, height);
|
|
450
|
+
}
|
|
451
|
+
this.profileIndex = Math.min(this.profileIndex, Math.max(0, items.length - 1));
|
|
452
|
+
const selected = items[this.profileIndex];
|
|
453
|
+
const choice = this.confirmation;
|
|
454
|
+
const promptLines = choice ? this.confirmationLines(choice, inner, capacity) : undefined;
|
|
455
|
+
this.confirmationVisible = promptLines !== undefined;
|
|
456
|
+
const lines: string[] = promptLines ? promptLines.map(line => this.options.theme.fg("warning", line)) : choice ? [this.options.theme.fg("warning", "Confirmation unavailable · Esc back")] : [];
|
|
457
|
+
// Input outranks the error, header and preview on tiny terminals.
|
|
458
|
+
const input = this.profileInput !== undefined ? `New profile name: ${this.profileInput}▏` : undefined;
|
|
459
|
+
if (input && lines.length < capacity) {
|
|
460
|
+
this.profileInputVisible = visibleWidth(input) <= inner;
|
|
461
|
+
lines.push(this.options.theme.fg("accent", input));
|
|
462
|
+
}
|
|
463
|
+
if (lines.length < capacity) lines.push(this.options.theme.fg("accent", "Visual profiles (preview only)"));
|
|
464
|
+
if (lines.length < capacity) lines.push(selected ? renderPaletteSelection(`${this.profileIndex + 1}/${items.length}: ${selected.name}`, inner, true, this.options.theme) : this.options.theme.fg("muted", "No saved profiles"));
|
|
465
|
+
const details = selected ? [
|
|
466
|
+
`Theme: ${selected.themeName}`,
|
|
467
|
+
`Animation: ${selected.animationPolicy} · Banner: rose ${selected.banner.showRose ? "on" : "off"}, logo ${selected.banner.showTextLogo ? "on" : "off"}, ${selected.banner.color}`,
|
|
468
|
+
`Layout: ${selected.visual.statusPlacement} · ${selected.visual.headerPlacement} · ${selected.visual.density}`,
|
|
469
|
+
`Sections: ${Object.entries(selected.visual.visibility).map(([key, value]) => `${key}:${value ? "on" : "off"}`).join(" · ")}`,
|
|
470
|
+
] : [];
|
|
471
|
+
const tail = this.error ? [`Error: ${this.error}`] : [];
|
|
472
|
+
const footer = "↑/↓ select · s save current · r replace · a apply · d delete · z reset catalog · Esc back";
|
|
473
|
+
const free = Math.max(0, capacity - lines.length - tail.length - 1);
|
|
474
|
+
lines.push(...details.slice(0, free).map(line => this.options.theme.fg("text", line)));
|
|
475
|
+
for (const line of tail) if (lines.length < capacity) lines.push(this.options.theme.fg(line.startsWith("Error:") ? "error" : "accent", line));
|
|
476
|
+
if (lines.length < capacity) lines.push(this.options.theme.fg("muted", footer));
|
|
477
|
+
return this.detailFrame(lines, width, height);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
invalidate(): void { this.options.requestRender(); }
|
|
481
|
+
}
|