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
package/lib/shell-changes.ts
CHANGED
|
@@ -27,7 +27,6 @@ export interface ChangesModel {
|
|
|
27
27
|
files: ChangedFile[];
|
|
28
28
|
added: number;
|
|
29
29
|
deleted: number;
|
|
30
|
-
notice?: string;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
export interface ChangesSnapshot {
|
|
@@ -46,6 +45,11 @@ interface NumstatEntry {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
export const CHANGES_COMMAND = "/gentle:changes";
|
|
48
|
+
// One selected file's diff is the only bounded thing in Changes: past either
|
|
49
|
+
// limit the panel shows DIFF_TOO_LARGE instead of buffering and coloring it.
|
|
50
|
+
export const DIFF_MAX_BYTES = 1024 * 1024;
|
|
51
|
+
export const DIFF_MAX_LINES = 20_000;
|
|
52
|
+
export const DIFF_TOO_LARGE = "Diff too large — press o to open in editor.";
|
|
49
53
|
const WIDGET_GLYPH = "✎";
|
|
50
54
|
const STATUS_BY_CODE: Record<string, ChangeStatus> = {
|
|
51
55
|
A: CHANGE_STATUS.ADDED,
|
|
@@ -106,6 +110,8 @@ export function snapshotChanges(snapshot: ChangesSnapshot): ChangedFile[] {
|
|
|
106
110
|
const counts = new Map(parseNumstat(snapshot.numstat).map((entry) => [entry.path, entry]));
|
|
107
111
|
const files: ChangedFile[] = [];
|
|
108
112
|
for (const [path, status] of parsePorcelain(snapshot.porcelain)) {
|
|
113
|
+
// Dependency backups are local artifacts; tracked files remain visible.
|
|
114
|
+
if (status === CHANGE_STATUS.UNTRACKED && /(?:^|\/)node_modules\.backup-[^/]+\//.test(path)) continue;
|
|
109
115
|
const entry = counts.get(path);
|
|
110
116
|
files.push({ path, added: entry?.added ?? 0, deleted: entry?.deleted ?? 0, status });
|
|
111
117
|
}
|
|
@@ -123,7 +129,7 @@ export function changesModel(changed: ChangedFile[]): ChangesModel {
|
|
|
123
129
|
|
|
124
130
|
export function changesSummary(model: ChangesModel): string {
|
|
125
131
|
const noun = model.files.length === 1 ? "file" : "files";
|
|
126
|
-
return `${model.files.length} ${noun} · +${model.added} −${model.deleted}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}
|
|
132
|
+
return `${model.files.length} ${noun} · +${model.added} −${model.deleted}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}`;
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
// One line: summary, the files joined by dots, and the command pushed to
|
|
@@ -132,7 +138,7 @@ export function renderChangesWidget(model: ChangesModel, theme: ChangesTheme, wi
|
|
|
132
138
|
if (model.files.length === 0) return [];
|
|
133
139
|
const noun = model.files.length === 1 ? "file" : "files";
|
|
134
140
|
const dot = theme.fg("muted", "·");
|
|
135
|
-
const head = `${theme.fg("accent", WIDGET_GLYPH)} ${theme.fg("text", `${model.files.length} ${noun}`)} ${dot} ${theme.fg("success", `+${model.added}`)} ${theme.fg("error", `−${model.deleted}`)}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}
|
|
141
|
+
const head = `${theme.fg("accent", WIDGET_GLYPH)} ${theme.fg("text", `${model.files.length} ${noun}`)} ${dot} ${theme.fg("success", `+${model.added}`)} ${theme.fg("error", `−${model.deleted}`)}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}`;
|
|
136
142
|
const hint = theme.fg("dim", CHANGES_COMMAND);
|
|
137
143
|
const list = model.files.map((file) => theme.fg("muted", file.path)).join(` ${dot} `);
|
|
138
144
|
const left = `${head} ${dot} ${list}`;
|
|
@@ -146,10 +152,14 @@ export function renderChangesWidget(model: ChangesModel, theme: ChangesTheme, wi
|
|
|
146
152
|
export interface GitResult {
|
|
147
153
|
stdout: string;
|
|
148
154
|
code: number;
|
|
155
|
+
/** The runner stopped reading at its output bound; stdout is partial. */
|
|
156
|
+
truncated?: boolean;
|
|
149
157
|
}
|
|
150
158
|
|
|
151
159
|
export type GitRunner = (args: string[]) => Promise<GitResult>;
|
|
152
160
|
export type LineCounter = (path: string) => Promise<number>;
|
|
161
|
+
/** A cheap content identity (e.g. size and mtime) so same-count edits refresh their diff. */
|
|
162
|
+
export type FileRevision = (path: string) => Promise<string | undefined>;
|
|
153
163
|
|
|
154
164
|
export interface WorktreeChanges {
|
|
155
165
|
root: string;
|
|
@@ -189,76 +199,41 @@ export async function foreignRootBranch(git: GitRunner): Promise<string | undefi
|
|
|
189
199
|
return verified.code === 0 ? branch : "no commits yet";
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
// real detached HEAD, or UNKNOWN_BRANCH when the root could not even be
|
|
196
|
-
// asked), decorates the trees from the cache, and tells the caller when a
|
|
197
|
-
// fresh answer arrived so the view can repaint. Git is touched only while the
|
|
198
|
-
// overlay is open, never at startup or on evidence refresh.
|
|
199
|
-
export class RootBranchLabels {
|
|
200
|
-
private readonly labels = new Map<string, string | undefined>();
|
|
201
|
-
private readonly pending = new Map<string, Promise<void>>();
|
|
202
|
-
private readonly gitForRoot: (root: string) => GitRunner;
|
|
203
|
-
private readonly onChange: () => void;
|
|
204
|
-
constructor(gitForRoot: (root: string) => GitRunner, onChange: () => void = () => {}) {
|
|
205
|
-
this.gitForRoot = gitForRoot;
|
|
206
|
-
this.onChange = onChange;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
decorate(trees: readonly WorktreeChanges[]): WorktreeChanges[] {
|
|
210
|
-
return trees.map((tree) => {
|
|
211
|
-
if (tree.branch !== undefined) return tree;
|
|
212
|
-
if (!this.labels.has(tree.root)) this.resolve(tree.root);
|
|
213
|
-
const branch = this.labels.get(tree.root);
|
|
214
|
-
return branch === undefined ? tree : { ...tree, branch };
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Every in-flight resolution has finished. */
|
|
219
|
-
async settled(): Promise<void> {
|
|
220
|
-
while (this.pending.size) await Promise.all(this.pending.values());
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private resolve(root: string): void {
|
|
224
|
-
if (this.pending.has(root)) return;
|
|
225
|
-
// A rejection here means the root could not even be asked -- distinct
|
|
226
|
-
// from foreignRootBranch resolving to undefined, which means git
|
|
227
|
-
// answered and there really is no branch (a real detached HEAD).
|
|
228
|
-
const task = foreignRootBranch(this.gitForRoot(root))
|
|
229
|
-
.then(
|
|
230
|
-
(branch) => branch,
|
|
231
|
-
() => UNKNOWN_BRANCH,
|
|
232
|
-
)
|
|
233
|
-
.then((branch) => {
|
|
234
|
-
this.labels.set(root, branch);
|
|
235
|
-
this.pending.delete(root);
|
|
236
|
-
this.onChange();
|
|
237
|
-
});
|
|
238
|
-
this.pending.set(root, task);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
202
|
+
// /gentle:changes: one group per registered root holding its working tree
|
|
203
|
+
// against HEAD. A root whose Git fails (timed out, rejected) has no group but
|
|
204
|
+
// is listed in failedRoots: a failed scan is never reported as a clean root.
|
|
242
205
|
export class WorktreeChangesTracker {
|
|
243
206
|
worktrees: WorktreeChanges[] = [];
|
|
207
|
+
failedRoots: string[] = [];
|
|
208
|
+
scannedRootCount = 0;
|
|
244
209
|
private inFlight: Promise<ChangesModel> | undefined;
|
|
245
210
|
private readonly discover: GitRunner;
|
|
246
211
|
private readonly gitForRoot: (root: string) => GitRunner;
|
|
247
212
|
private readonly linesForRoot: (root: string) => LineCounter;
|
|
248
213
|
private readonly registeredRoots: () => readonly string[];
|
|
214
|
+
private readonly revisionForRoot: (root: string) => FileRevision | undefined;
|
|
249
215
|
|
|
250
|
-
constructor(discover: GitRunner, gitForRoot: (root: string) => GitRunner, linesForRoot: (root: string) => LineCounter = () => noLines, registeredRoots: () => readonly string[] = () => []) {
|
|
216
|
+
constructor(discover: GitRunner, gitForRoot: (root: string) => GitRunner, linesForRoot: (root: string) => LineCounter = () => noLines, registeredRoots: () => readonly string[] = () => [], revisionForRoot: (root: string) => FileRevision | undefined = () => undefined) {
|
|
251
217
|
this.discover = discover;
|
|
252
218
|
this.gitForRoot = gitForRoot;
|
|
253
219
|
this.linesForRoot = linesForRoot;
|
|
254
220
|
this.registeredRoots = registeredRoots;
|
|
221
|
+
this.revisionForRoot = revisionForRoot;
|
|
255
222
|
}
|
|
256
223
|
|
|
224
|
+
private modelOf: WorktreeChanges[] | undefined;
|
|
225
|
+
private combined: ChangesModel = emptyChanges();
|
|
226
|
+
|
|
227
|
+
// Footers read this every frame; rebuild only when a scan replaced the trees.
|
|
257
228
|
get model(): ChangesModel {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
229
|
+
if (this.modelOf !== this.worktrees) {
|
|
230
|
+
this.modelOf = this.worktrees;
|
|
231
|
+
this.combined = changesModel(this.worktrees.flatMap((tree) => tree.model.files.map((file) => ({
|
|
232
|
+
...file,
|
|
233
|
+
path: this.worktrees.length === 1 ? file.path : `${tree.root}/${file.path}`,
|
|
234
|
+
}))));
|
|
235
|
+
}
|
|
236
|
+
return this.combined;
|
|
262
237
|
}
|
|
263
238
|
|
|
264
239
|
async start(): Promise<void> {
|
|
@@ -278,17 +253,29 @@ export class WorktreeChangesTracker {
|
|
|
278
253
|
}
|
|
279
254
|
|
|
280
255
|
private async capture(): Promise<ChangesModel> {
|
|
281
|
-
const result = await this.discover(["worktree", "list", "--porcelain", "-z"]).catch(() => ({ code: 128, stdout: "" }));
|
|
282
|
-
const trees: WorktreeChanges[] = [];
|
|
283
|
-
const metadata = new Map((result.code === 0 ? parseWorktrees(result.stdout) : []).map((tree) => [tree.root, tree]));
|
|
284
256
|
// Discovery labels registered roots; it never grants visibility to siblings.
|
|
285
257
|
const roots = new Set(this.registeredRoots());
|
|
286
|
-
|
|
258
|
+
// No root to scan means no Git at all, not even branch discovery.
|
|
259
|
+
if (roots.size === 0) {
|
|
260
|
+
this.worktrees = [];
|
|
261
|
+
this.failedRoots = [];
|
|
262
|
+
this.scannedRootCount = 0;
|
|
263
|
+
return this.model;
|
|
264
|
+
}
|
|
265
|
+
const result = await this.discover(["worktree", "list", "--porcelain", "-z"]).catch(() => ({ code: 128, stdout: "" }));
|
|
266
|
+
const metadata = new Map((result.code === 0 ? parseWorktrees(result.stdout) : []).map((tree) => [tree.root, tree]));
|
|
267
|
+
// Roots scan in parallel so one slow root does not delay the others.
|
|
268
|
+
const failed: string[] = [];
|
|
269
|
+
const scanned = await Promise.all([...roots].map(async (root): Promise<WorktreeChanges | undefined> => {
|
|
287
270
|
const known = metadata.get(root);
|
|
288
271
|
try {
|
|
289
|
-
const tracker = new ChangesTracker(this.gitForRoot(root), this.linesForRoot(root));
|
|
272
|
+
const tracker = new ChangesTracker(this.gitForRoot(root), this.linesForRoot(root), this.revisionForRoot(root));
|
|
290
273
|
await tracker.start();
|
|
291
|
-
if (!tracker.
|
|
274
|
+
if (!tracker.statusRead) {
|
|
275
|
+
failed.push(root);
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
if (!tracker.model.files.length) return undefined;
|
|
292
279
|
// A root missing from this discovery scan belongs to a different Git
|
|
293
280
|
// clone entirely (e.g. an inner repository nested inside an outer
|
|
294
281
|
// one) -- worktree list can never describe a foreign clone, so ask
|
|
@@ -299,22 +286,40 @@ export class WorktreeChangesTracker {
|
|
|
299
286
|
// change scan above failing -- that must label the root
|
|
300
287
|
// UNKNOWN_BRANCH, not drop real, already-captured changes.
|
|
301
288
|
const branch = known ? known.branch : await foreignRootBranch(this.gitForRoot(root)).catch(() => UNKNOWN_BRANCH);
|
|
302
|
-
|
|
289
|
+
return { root, ...(branch ? { branch } : {}), model: tracker.model };
|
|
303
290
|
} catch {
|
|
304
|
-
//
|
|
291
|
+
// A root the caller stops registering mid-scan (e.g. a removed
|
|
292
|
+
// worktree) triggers the rescan below without it, not a failure.
|
|
293
|
+
failed.push(root);
|
|
294
|
+
return undefined;
|
|
305
295
|
}
|
|
306
|
-
}
|
|
296
|
+
}));
|
|
297
|
+
const trees = scanned.filter((tree): tree is WorktreeChanges => tree !== undefined);
|
|
307
298
|
const latest = new Set(this.registeredRoots());
|
|
308
299
|
// A root admitted during status must not wait for a later poll (which may
|
|
309
300
|
// be disabled). Ordinary overlapping polls still share exactly one scan.
|
|
310
301
|
if (latest.size !== roots.size || [...latest].some((root) => !roots.has(root))) return this.capture();
|
|
311
302
|
this.worktrees = trees;
|
|
303
|
+
this.failedRoots = failed;
|
|
304
|
+
this.scannedRootCount = roots.size;
|
|
312
305
|
return this.model;
|
|
313
306
|
}
|
|
314
307
|
}
|
|
315
308
|
|
|
316
309
|
const noLines: LineCounter = async () => 0;
|
|
317
310
|
|
|
311
|
+
// Per-file line counts and revision lookups are file IO: a large change set
|
|
312
|
+
// runs this many files at a time instead of one by one or all at once.
|
|
313
|
+
export const FILE_WORK_CONCURRENCY = 8;
|
|
314
|
+
|
|
315
|
+
async function eachLimited<T>(items: readonly T[], limit: number, work: (item: T) => Promise<void>): Promise<void> {
|
|
316
|
+
let next = 0;
|
|
317
|
+
const worker = async () => {
|
|
318
|
+
while (next < items.length) await work(items[next++]);
|
|
319
|
+
};
|
|
320
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
321
|
+
}
|
|
322
|
+
|
|
318
323
|
const NUMSTAT_ARGS = ["diff", "--numstat", "HEAD"];
|
|
319
324
|
const PORCELAIN_ARGS = ["status", "--porcelain=v1", "--untracked-files=all", "-z"];
|
|
320
325
|
|
|
@@ -324,20 +329,27 @@ const PORCELAIN_ARGS = ["status", "--porcelain=v1", "--untracked-files=all", "-z
|
|
|
324
329
|
export class ChangesTracker {
|
|
325
330
|
private readonly git: GitRunner;
|
|
326
331
|
private readonly countLines: LineCounter;
|
|
332
|
+
private readonly revision: FileRevision | undefined;
|
|
327
333
|
private current: ChangesModel = emptyChanges();
|
|
328
334
|
private available = false;
|
|
329
335
|
private inFlight: Promise<ChangesModel> | undefined;
|
|
330
336
|
private queued = false;
|
|
331
337
|
|
|
332
|
-
constructor(git: GitRunner, countLines: LineCounter = noLines) {
|
|
338
|
+
constructor(git: GitRunner, countLines: LineCounter = noLines, revision?: FileRevision) {
|
|
333
339
|
this.git = git;
|
|
334
340
|
this.countLines = countLines;
|
|
341
|
+
this.revision = revision;
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
get model(): ChangesModel {
|
|
338
345
|
return this.current;
|
|
339
346
|
}
|
|
340
347
|
|
|
348
|
+
/** False when the last start could not read `git status`. */
|
|
349
|
+
get statusRead(): boolean {
|
|
350
|
+
return this.available;
|
|
351
|
+
}
|
|
352
|
+
|
|
341
353
|
async start(): Promise<void> {
|
|
342
354
|
const files = await this.capture();
|
|
343
355
|
this.available = files !== undefined;
|
|
@@ -372,9 +384,14 @@ export class ChangesTracker {
|
|
|
372
384
|
if (porcelain.code !== 0) return undefined;
|
|
373
385
|
const files = snapshotChanges({ numstat: numstat.stdout, porcelain: porcelain.stdout });
|
|
374
386
|
// Untracked files never appear in numstat; count their lines directly.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
387
|
+
await eachLimited(files, FILE_WORK_CONCURRENCY, async (file) => {
|
|
388
|
+
const [added, revision] = await Promise.all([
|
|
389
|
+
file.status === CHANGE_STATUS.UNTRACKED ? this.countLines(file.path).catch(() => 0) : undefined,
|
|
390
|
+
this.revision?.(file.path).catch(() => undefined),
|
|
391
|
+
]);
|
|
392
|
+
if (added !== undefined) file.added = added;
|
|
393
|
+
if (revision) file.diffRevision = revision;
|
|
394
|
+
});
|
|
378
395
|
return files;
|
|
379
396
|
}
|
|
380
397
|
}
|
package/lib/shell-prompt.ts
CHANGED
|
@@ -74,7 +74,9 @@ export const DOUBLE_ESC_CANCEL_HINT = "esc again to cancel";
|
|
|
74
74
|
export const IDLE_ESC_CLEAR_HINT = "esc again to clear";
|
|
75
75
|
const LABEL_ROLE = "muted";
|
|
76
76
|
const HINT_ROLE = "dim";
|
|
77
|
-
|
|
77
|
+
// Audited legacy SGR and Pi 1.2 APC cursors. Do not resolve the latter
|
|
78
|
+
// here: the host TUI owns hardware-cursor suppression after composition.
|
|
79
|
+
const EMPTY_FAKE_CURSORS = ["\x1b[7m \x1b[0m", "\x1b_pi:fc\x07 \x1b_pi:/fc\x07"];
|
|
78
80
|
const SCROLL_INDICATOR = /[↑↓] \d+ more/;
|
|
79
81
|
const STATE_LABEL: Record<PromptState, string | undefined> = {
|
|
80
82
|
[PROMPT_STATE.IDLE]: undefined,
|
|
@@ -228,9 +230,9 @@ export function framePromptLines(lines: string[], width: number, options: Prompt
|
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
export function withPromptHint(line: string, hint: string, fg: PromptFrameOptions["fg"]): string {
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
233
|
-
const afterCursor =
|
|
233
|
+
const cursor = EMPTY_FAKE_CURSORS.find((marker) => line.includes(marker));
|
|
234
|
+
if (!cursor) return line;
|
|
235
|
+
const afterCursor = line.indexOf(cursor) + cursor.length;
|
|
234
236
|
const trailing = line.slice(afterCursor);
|
|
235
237
|
if (trailing.trim() !== "" || trailing.length < hint.length + 1) return line;
|
|
236
238
|
return `${line.slice(0, afterCursor)} ${fg(HINT_ROLE, hint)}${" ".repeat(trailing.length - hint.length - 1)}`;
|
|
@@ -179,6 +179,10 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () =>
|
|
|
179
179
|
};
|
|
180
180
|
scroll.handleMouse = (event) => {
|
|
181
181
|
if (event.type === "wheel") {
|
|
182
|
+
// A bounded section (Todo) owns its inner viewport. Only wheel
|
|
183
|
+
// events it declines should move the surrounding rail.
|
|
184
|
+
const sectionResult = dispatchPartMouse(event);
|
|
185
|
+
if (sectionResult?.handled) return sectionResult;
|
|
182
186
|
// Consume even at the boundary or over blank rail space: Pi 0.85.1
|
|
183
187
|
// can send unconsumed delta to the primary transcript despite containment.
|
|
184
188
|
scroll.scrollBy(event.wheelDelta ?? 0);
|
|
@@ -375,7 +379,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () =>
|
|
|
375
379
|
stopped = true;
|
|
376
380
|
state.active = false;
|
|
377
381
|
clearInterval(timer);
|
|
378
|
-
scroll.
|
|
382
|
+
scroll.setScrollbar("hidden");
|
|
379
383
|
for (const cleanup of cleanups.reverse()) cleanup();
|
|
380
384
|
tui.requestRender();
|
|
381
385
|
};
|
package/lib/shell-todo.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
1
|
+
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { CARD_TONE, panelHeaderRow, panelInnerWidth, renderCard, type CardTheme, type CardTone } from "./shell-card.ts";
|
|
3
3
|
import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
4
4
|
import { paintHoverable } from "./shell-hover.ts";
|
|
@@ -11,7 +11,11 @@ import { paintHoverable } from "./shell-hover.ts";
|
|
|
11
11
|
export const TODO_STATUS = {
|
|
12
12
|
PENDING: "pending",
|
|
13
13
|
IN_PROGRESS: "in_progress",
|
|
14
|
+
/** Open, but waiting on something outside the list; always carries a note naming it. */
|
|
15
|
+
BLOCKED: "blocked",
|
|
14
16
|
DONE: "done",
|
|
17
|
+
/** Will not be done, on purpose: terminal, and never counts as open. */
|
|
18
|
+
DROPPED: "dropped",
|
|
15
19
|
} as const;
|
|
16
20
|
|
|
17
21
|
export type TodoStatus = (typeof TODO_STATUS)[keyof typeof TODO_STATUS];
|
|
@@ -86,10 +90,20 @@ export interface TodoRenderOptions {
|
|
|
86
90
|
export const TODO_DETAILS_KEY = "gentleTodo";
|
|
87
91
|
export const TODO_TOOL_NAME = "todo";
|
|
88
92
|
export const TODO_GLYPH = "❀";
|
|
89
|
-
const STATUS_ALIASES: Record<string, TodoStatus> = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
const STATUS_ALIASES: Record<string, TodoStatus> = {
|
|
94
|
+
completed: TODO_STATUS.DONE,
|
|
95
|
+
complete: TODO_STATUS.DONE,
|
|
96
|
+
doing: TODO_STATUS.IN_PROGRESS,
|
|
97
|
+
todo: TODO_STATUS.PENDING,
|
|
98
|
+
cancelled: TODO_STATUS.DROPPED,
|
|
99
|
+
canceled: TODO_STATUS.DROPPED,
|
|
100
|
+
abandoned: TODO_STATUS.DROPPED,
|
|
101
|
+
};
|
|
102
|
+
const STATUS_LIST = "pending, in_progress, blocked, done, or dropped";
|
|
103
|
+
const BLOCKED_NEEDS_NOTE = "blocked needs a note naming what it waits for";
|
|
104
|
+
const STATUS_GLYPH: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "○", [TODO_STATUS.IN_PROGRESS]: "◐", [TODO_STATUS.BLOCKED]: "⊘", [TODO_STATUS.DONE]: "✓", [TODO_STATUS.DROPPED]: "✕" };
|
|
105
|
+
const STATUS_ROLE: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "text", [TODO_STATUS.IN_PROGRESS]: "accent", [TODO_STATUS.BLOCKED]: "text", [TODO_STATUS.DONE]: "dim", [TODO_STATUS.DROPPED]: "dim" };
|
|
106
|
+
const GLYPH_ROLE: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "muted", [TODO_STATUS.IN_PROGRESS]: "accent", [TODO_STATUS.BLOCKED]: "warning", [TODO_STATUS.DONE]: "success", [TODO_STATUS.DROPPED]: "muted" };
|
|
93
107
|
const NOTE_ROLE = "muted";
|
|
94
108
|
const STALE_AFTER_TURNS = 2;
|
|
95
109
|
/** Above this many rows the finished tasks fold into one line and the rest is capped. */
|
|
@@ -111,20 +125,30 @@ function parseStatus(value: unknown): TodoStatus | undefined {
|
|
|
111
125
|
return STATUS_ALIASES[normalized];
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
function countStatus(state: TodoState, status: TodoStatus): number {
|
|
129
|
+
return state.tasks.filter((task) => task.status === status).length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Dropped tasks are neither done nor open. */
|
|
114
133
|
export function todoSummary(state: TodoState): TodoSummary {
|
|
115
|
-
const done = state
|
|
116
|
-
return { done, total: state.tasks.length, open: state.tasks.length - done };
|
|
134
|
+
const done = countStatus(state, TODO_STATUS.DONE);
|
|
135
|
+
return { done, total: state.tasks.length, open: state.tasks.length - done - countStatus(state, TODO_STATUS.DROPPED) };
|
|
117
136
|
}
|
|
118
137
|
|
|
138
|
+
// Only work the model can pick up goes stale: a list whose open tasks all
|
|
139
|
+
// wait on something outside it has nothing to bring up to date.
|
|
119
140
|
export function staleTurns(state: TodoState, currentTurn: number): number {
|
|
120
|
-
|
|
141
|
+
const actionable = state.tasks.some((task) => task.status === TODO_STATUS.PENDING || task.status === TODO_STATUS.IN_PROGRESS);
|
|
142
|
+
if (state.updatedTurn === null || !actionable) return 0;
|
|
121
143
|
return Math.max(0, currentTurn - state.updatedTurn);
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
function summaryText(state: TodoState): string {
|
|
125
147
|
const { done, total } = todoSummary(state);
|
|
126
|
-
const inProgress = state
|
|
127
|
-
|
|
148
|
+
const inProgress = countStatus(state, TODO_STATUS.IN_PROGRESS);
|
|
149
|
+
const blocked = countStatus(state, TODO_STATUS.BLOCKED);
|
|
150
|
+
const dropped = countStatus(state, TODO_STATUS.DROPPED);
|
|
151
|
+
return `${total} ${total === 1 ? "task" : "tasks"} · ${done} done · ${inProgress} in progress${blocked ? ` · ${blocked} blocked` : ""}${dropped ? ` · ${dropped} dropped` : ""}`;
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
function taskLine(task: TodoTask): string {
|
|
@@ -135,8 +159,9 @@ function buildTask(input: TodoTaskInput, id: number): TodoTask | string {
|
|
|
135
159
|
const title = cleanText(input.title);
|
|
136
160
|
if (title.length === 0) return "title is required";
|
|
137
161
|
const status = parseStatus(input.status);
|
|
138
|
-
if (status === undefined) return `unknown status "${input.status}" (use
|
|
162
|
+
if (status === undefined) return `unknown status "${input.status}" (use ${STATUS_LIST})`;
|
|
139
163
|
const note = cleanText(input.note);
|
|
164
|
+
if (status === TODO_STATUS.BLOCKED && note.length === 0) return BLOCKED_NEEDS_NOTE;
|
|
140
165
|
return { id, title, status, ...(note ? { note } : {}) };
|
|
141
166
|
}
|
|
142
167
|
|
|
@@ -233,15 +258,28 @@ export function replayTodo(entries: readonly unknown[]): TodoState {
|
|
|
233
258
|
return state;
|
|
234
259
|
}
|
|
235
260
|
|
|
236
|
-
|
|
261
|
+
// gentle-shell#1367, #1662: the system prompt is a cached prefix for every
|
|
262
|
+
// provider, so the block it carries is byte-stable for the whole session:
|
|
263
|
+
// rules only, no task content and no counters. The current list travels as a
|
|
264
|
+
// request-local message at the end of the conversation (todoStateMessage),
|
|
265
|
+
// where a change costs only the tail of the request, never the prefix.
|
|
266
|
+
const TODO_PROMPT_BLOCK = [
|
|
267
|
+
"## Todo list",
|
|
268
|
+
"Keep it current with the `todo` tool: mark a task in_progress before starting it, done right after finishing it, and rewrite the whole list with `write` whenever the plan changes. Update it before you end the turn.",
|
|
269
|
+
"A blocked task waits on something outside the list: do not work on it until its condition holds. When the plan changes, mark the tasks it made obsolete dropped instead of leaving them pending or marking them done.",
|
|
270
|
+
"While tasks stay open, the current list arrives as the last message of each request; it is not repeated here.",
|
|
271
|
+
].join("\n");
|
|
272
|
+
|
|
273
|
+
/** The byte-stable rules block appended to every system prompt. */
|
|
274
|
+
export function todoPromptBlock(): string {
|
|
275
|
+
return TODO_PROMPT_BLOCK;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The open-task snapshot delivered as a request-local message; undefined when nothing is open. */
|
|
279
|
+
export function todoStateMessage(state: TodoState): string | undefined {
|
|
237
280
|
if (todoSummary(state).open === 0) return undefined;
|
|
238
281
|
const lines = state.tasks.map((task, index) => `${index + 1}. [${task.status}] ${task.title}${task.note ? ` — ${task.note}` : ""}`);
|
|
239
|
-
|
|
240
|
-
return [
|
|
241
|
-
"## Todo list",
|
|
242
|
-
"Keep it current with the `todo` tool: mark a task in_progress before starting it, done right after finishing it, and rewrite the whole list with `write` whenever the plan changes. Update it before you end the turn.",
|
|
243
|
-
...lines,
|
|
244
|
-
].join("\n") + staleLine;
|
|
282
|
+
return ["Current todo list:", ...lines].join("\n");
|
|
245
283
|
}
|
|
246
284
|
|
|
247
285
|
function taskRow(task: TodoTask, theme: TodoTheme, inner: number): string {
|
|
@@ -253,18 +291,22 @@ function taskRow(task: TodoTask, theme: TodoTheme, inner: number): string {
|
|
|
253
291
|
.join("\n");
|
|
254
292
|
}
|
|
255
293
|
const title = task.title;
|
|
256
|
-
const
|
|
294
|
+
const showsNote = task.status === TODO_STATUS.IN_PROGRESS || task.status === TODO_STATUS.BLOCKED;
|
|
295
|
+
const note = showsNote && task.note ? ` ${theme.fg(NOTE_ROLE, "·")} ${theme.fg(NOTE_ROLE, task.note)}` : "";
|
|
257
296
|
return `${theme.fg(GLYPH_ROLE[task.status], STATUS_GLYPH[task.status])} ${theme.fg(STATUS_ROLE[task.status], title)}${note}`;
|
|
258
297
|
}
|
|
259
298
|
|
|
260
|
-
// Long lists keep the card short: done tasks fold into "✓ N done"
|
|
261
|
-
// open ones fill the remaining rows, with a
|
|
299
|
+
// Long lists keep the card short: done and dropped tasks fold into "✓ N done"
|
|
300
|
+
// and "✕ N dropped", and the open ones fill the remaining rows, with a
|
|
301
|
+
// trailing count for the rest.
|
|
262
302
|
function bodyRows(state: TodoState, theme: TodoTheme, inner: number): string[] {
|
|
263
303
|
if (state.tasks.length <= ROW_CAP) return state.tasks.map((task) => taskRow(task, theme, inner));
|
|
264
|
-
const
|
|
265
|
-
const open = state.tasks.filter((task) => task.status !== TODO_STATUS.DONE);
|
|
304
|
+
const open = state.tasks.filter((task) => task.status !== TODO_STATUS.DONE && task.status !== TODO_STATUS.DROPPED);
|
|
266
305
|
const rows: string[] = [];
|
|
267
|
-
|
|
306
|
+
for (const [status, label] of [[TODO_STATUS.DONE, "done"], [TODO_STATUS.DROPPED, "dropped"]] as const) {
|
|
307
|
+
const count = countStatus(state, status);
|
|
308
|
+
if (count > 0) rows.push(`${theme.fg(GLYPH_ROLE[status], STATUS_GLYPH[status])} ${theme.fg(NOTE_ROLE, `${count} ${label}`)}`);
|
|
309
|
+
}
|
|
268
310
|
const room = ROW_CAP - rows.length - (open.length > ROW_CAP - rows.length ? 1 : 0);
|
|
269
311
|
for (const task of open.slice(0, room)) rows.push(taskRow(task, theme, inner));
|
|
270
312
|
if (open.length > room) rows.push(theme.fg(NOTE_ROLE, `… ${open.length - room} more`));
|
|
@@ -276,6 +318,8 @@ function collapsedRow(state: TodoState, theme: TodoTheme, inner: number): string
|
|
|
276
318
|
if (active) return taskRow(active, theme, inner);
|
|
277
319
|
const pending = state.tasks.find((task) => task.status === TODO_STATUS.PENDING);
|
|
278
320
|
if (pending) return taskRow(pending, theme, inner);
|
|
321
|
+
const blocked = state.tasks.find((task) => task.status === TODO_STATUS.BLOCKED);
|
|
322
|
+
if (blocked) return taskRow(blocked, theme, inner);
|
|
279
323
|
const { open } = todoSummary(state);
|
|
280
324
|
return `${theme.fg(GLYPH_ROLE[TODO_STATUS.PENDING], STATUS_GLYPH[TODO_STATUS.PENDING])} ${theme.fg(NOTE_ROLE, `${open} open`)}`;
|
|
281
325
|
}
|
|
@@ -288,6 +332,8 @@ export function todoCardTone(staleTurns: number): CardTone {
|
|
|
288
332
|
export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number, options: TodoRenderOptions): string[] {
|
|
289
333
|
if (state.tasks.length === 0) return [];
|
|
290
334
|
const { done, total } = todoSummary(state);
|
|
335
|
+
// Dropped work is not part of the plan anymore, so it leaves the denominator.
|
|
336
|
+
const planned = total - countStatus(state, TODO_STATUS.DROPPED);
|
|
291
337
|
const stale = options.staleTurns >= STALE_AFTER_TURNS;
|
|
292
338
|
const action = options.collapsed ? "expand" : "collapse";
|
|
293
339
|
const actionLabel = action[0]!.toUpperCase() + action.slice(1);
|
|
@@ -300,12 +346,21 @@ export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number
|
|
|
300
346
|
// Content columns of the card body in the active style (float is narrower).
|
|
301
347
|
const inner = panelInnerWidth(theme, width, tone);
|
|
302
348
|
const rows = options.collapsed ? [collapsedRow(state, theme, inner)] : options.scrollable ? state.tasks.map((task) => taskRow(task, theme, inner)) : bodyRows(state, theme, inner);
|
|
303
|
-
const
|
|
349
|
+
const staleLabel = theme.fg(NOTE_ROLE, `stale · ${options.staleTurns} turns`);
|
|
350
|
+
let body = options.collapsed
|
|
351
|
+
? [truncateToWidth(`${stale ? `${staleLabel} · ` : ""}${rows[0]}`, inner, "…")]
|
|
352
|
+
: stale ? [staleLabel, ...rows] : rows;
|
|
353
|
+
if (!options.collapsed && !options.scrollable) {
|
|
354
|
+
// Budget physical lines after wrapping: one long task can exceed the
|
|
355
|
+
// old task-count cap by itself. Scrollable hosts retain every line.
|
|
356
|
+
body = body.flatMap((row) => wrapTextWithAnsi(row, Math.max(1, inner)));
|
|
357
|
+
if (body.length > ROW_CAP) body = [...body.slice(0, ROW_CAP - 1), theme.fg(NOTE_ROLE, "… more")];
|
|
358
|
+
}
|
|
304
359
|
// The clickable control paints the shared hover role while hovered --
|
|
305
360
|
// same treatment every other clickable surface uses -- instead of its
|
|
306
361
|
// ordinary accent role.
|
|
307
362
|
return renderCard(
|
|
308
|
-
{ title: `Todos ${paintHoverable(theme, control, options.hovered, "accent")}`, subtitle: `${done} of ${
|
|
363
|
+
{ title: `Todos ${paintHoverable(theme, control, options.hovered, "accent")}`, subtitle: `${done} of ${planned}`, body, tone, glyph: TODO_GLYPH },
|
|
309
364
|
theme,
|
|
310
365
|
width,
|
|
311
366
|
{ expanded: true, hint, panel: true },
|
package/lib/shell-usage.ts
CHANGED
|
@@ -269,7 +269,7 @@ export function formatReset(resetAt: number | null, now: number): string {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
function parseWindow(raw: RawWindow | null | undefined, now: number): UsageWindow | undefined {
|
|
272
|
-
if (!raw ||
|
|
272
|
+
if (!raw || !isFiniteNumber(raw.used_percent) || !isFiniteNumber(raw.limit_window_seconds) || raw.limit_window_seconds <= 0) return undefined;
|
|
273
273
|
const resetAt =
|
|
274
274
|
typeof raw.reset_at === "number" ? raw.reset_at * 1000 : typeof raw.reset_after_seconds === "number" ? now + raw.reset_after_seconds * 1000 : null;
|
|
275
275
|
return { label: windowLabel(raw.limit_window_seconds), usedPercent: raw.used_percent, windowSeconds: raw.limit_window_seconds, resetAt };
|
|
@@ -297,9 +297,10 @@ export function parseCodexUsage(payload: unknown, now: number): ProviderUsage {
|
|
|
297
297
|
function headerWindow(headers: Record<string, string>, kind: "primary" | "secondary"): UsageWindow | undefined {
|
|
298
298
|
const used = Number.parseFloat(headers[`${HEADER_PREFIX}${kind}-used-percent`] ?? "");
|
|
299
299
|
if (!Number.isFinite(used)) return undefined;
|
|
300
|
-
|
|
300
|
+
// Missing or invalid duration is not a zero-minute quota or an assumed 5h window.
|
|
301
|
+
const seconds = Number(headers[`${HEADER_PREFIX}${kind}-window-minutes`]) * MINUTE;
|
|
302
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return undefined;
|
|
301
303
|
const resetAt = Number.parseInt(headers[`${HEADER_PREFIX}${kind}-reset-at`] ?? "", 10);
|
|
302
|
-
const seconds = Number.isFinite(minutes) ? minutes * MINUTE : 0;
|
|
303
304
|
return { label: windowLabel(seconds), usedPercent: used, windowSeconds: seconds, resetAt: Number.isFinite(resetAt) ? resetAt * 1000 : null };
|
|
304
305
|
}
|
|
305
306
|
|