gentle-pi 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Standalone v1 codec and pure active-branch replay (gentle-shell#1064).
|
|
2
|
+
// No Pi API, disk access, shared binding store, or orchestrator application.
|
|
3
|
+
import { isValidProfileName } from "./agent-profiles.ts";
|
|
4
|
+
import {
|
|
5
|
+
isSafeAgentName,
|
|
6
|
+
isThinkingLevel,
|
|
7
|
+
normalizeModelId,
|
|
8
|
+
normalizeRoutingEntry,
|
|
9
|
+
type AgentModelConfig,
|
|
10
|
+
type AgentRoutingEntry,
|
|
11
|
+
} from "./model-routing-authority.ts";
|
|
12
|
+
|
|
13
|
+
export const SESSION_PROFILE_CUSTOM_TYPE = "gentle-pi.session-profile/v1";
|
|
14
|
+
const SESSION_PROFILE_FAMILY = "gentle-pi.session-profile/";
|
|
15
|
+
|
|
16
|
+
export type SessionProfileOrigin = "user" | "local" | "repo" | "global";
|
|
17
|
+
|
|
18
|
+
export interface SessionProfileBindPayload {
|
|
19
|
+
kind: "bind";
|
|
20
|
+
origin: SessionProfileOrigin;
|
|
21
|
+
name: string;
|
|
22
|
+
modelProfiles: AgentModelConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SessionProfileClearPayload {
|
|
26
|
+
kind: "clear";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Public structural entry shape only; ordinary messages need no custom fields. */
|
|
30
|
+
export interface SessionProfileEntry {
|
|
31
|
+
type: string;
|
|
32
|
+
customType?: string;
|
|
33
|
+
data?: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type SessionProfileReadResult =
|
|
37
|
+
| { status: "absent" }
|
|
38
|
+
| { status: "bound"; binding: SessionProfileBindPayload }
|
|
39
|
+
| { status: "cleared" }
|
|
40
|
+
| { status: "invalid" }
|
|
41
|
+
| { status: "unsupported" };
|
|
42
|
+
|
|
43
|
+
export type SessionProfileReplayResult =
|
|
44
|
+
| { status: "absent" }
|
|
45
|
+
| (Exclude<SessionProfileReadResult, { status: "absent" }> & {
|
|
46
|
+
entryIndex: number;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hasOwn(value: Record<string, unknown>, key: string): boolean {
|
|
54
|
+
return Object.hasOwn(value, key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isOrigin(value: unknown): value is SessionProfileOrigin {
|
|
58
|
+
return (
|
|
59
|
+
value === "user" ||
|
|
60
|
+
value === "local" ||
|
|
61
|
+
value === "repo" ||
|
|
62
|
+
value === "global"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Validate known fields before the permissive routing helper can drop them. */
|
|
67
|
+
function readRoute(
|
|
68
|
+
value: unknown,
|
|
69
|
+
encoderInput = false,
|
|
70
|
+
): AgentRoutingEntry | undefined {
|
|
71
|
+
if (typeof value === "string") return normalizeRoutingEntry(value);
|
|
72
|
+
if (!isRecord(value)) return undefined;
|
|
73
|
+
// Typed normalized routes can own optional undefined fields. Omit only those
|
|
74
|
+
// encoder fields; persisted fields and legacy effort retain strict validation.
|
|
75
|
+
const route = encoderInput
|
|
76
|
+
? Object.fromEntries(
|
|
77
|
+
Object.entries(value).filter(
|
|
78
|
+
([key, field]) =>
|
|
79
|
+
!((key === "model" || key === "thinking") && field === undefined),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
: value;
|
|
83
|
+
if (hasOwn(route, "model") && normalizeModelId(route.model) === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (hasOwn(route, "thinking") && !isThinkingLevel(route.thinking))
|
|
86
|
+
return undefined;
|
|
87
|
+
if (hasOwn(route, "effort") && !isThinkingLevel(route.effort))
|
|
88
|
+
return undefined;
|
|
89
|
+
// Project only own known fields. Unknown extras, even an unknown-only route,
|
|
90
|
+
// carry no routing meaning. Thinking wins over legacy effort as elsewhere.
|
|
91
|
+
const known = Object.fromEntries(
|
|
92
|
+
["model", "thinking", "effort"]
|
|
93
|
+
.filter((key) => hasOwn(route, key))
|
|
94
|
+
.map((key) => [key, route[key]]),
|
|
95
|
+
);
|
|
96
|
+
const normalized = normalizeRoutingEntry(known);
|
|
97
|
+
if (!normalized) return undefined;
|
|
98
|
+
return Object.fromEntries(
|
|
99
|
+
Object.entries(normalized).filter(([, field]) => field !== undefined),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readSnapshot(
|
|
104
|
+
value: unknown,
|
|
105
|
+
encoderInput = false,
|
|
106
|
+
): AgentModelConfig | undefined {
|
|
107
|
+
if (!isRecord(value)) return undefined;
|
|
108
|
+
const entries: Array<[string, AgentRoutingEntry]> = [];
|
|
109
|
+
for (const [name, rawRoute] of Object.entries(value)) {
|
|
110
|
+
// Shared agent-name policy, applied here without the forgiving whole-config
|
|
111
|
+
// normalization that would silently discard a bad route. Output below uses
|
|
112
|
+
// own data properties, never prototype setters.
|
|
113
|
+
if (!isSafeAgentName(name)) return undefined;
|
|
114
|
+
const route = readRoute(rawRoute, encoderInput);
|
|
115
|
+
if (route === undefined) return undefined;
|
|
116
|
+
entries.push([name, route]);
|
|
117
|
+
}
|
|
118
|
+
return Object.fromEntries(entries);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readBind(
|
|
122
|
+
data: Record<string, unknown>,
|
|
123
|
+
encoderInput = false,
|
|
124
|
+
): SessionProfileBindPayload | undefined {
|
|
125
|
+
if (!["origin", "name", "modelProfiles"].every((key) => hasOwn(data, key)))
|
|
126
|
+
return undefined;
|
|
127
|
+
if (!isOrigin(data.origin) || !isValidProfileName(data.name)) return undefined;
|
|
128
|
+
const modelProfiles = readSnapshot(data.modelProfiles, encoderInput);
|
|
129
|
+
if (modelProfiles === undefined) return undefined;
|
|
130
|
+
return { kind: "bind", origin: data.origin, name: data.name, modelProfiles };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Create a detached user selection payload; never appends or claims durability. */
|
|
134
|
+
export function createSessionProfileBind(
|
|
135
|
+
name: string,
|
|
136
|
+
modelProfiles: AgentModelConfig,
|
|
137
|
+
): SessionProfileBindPayload {
|
|
138
|
+
const binding = readBind({ origin: "user", name, modelProfiles }, true);
|
|
139
|
+
if (!binding) throw new TypeError("Invalid session profile binding");
|
|
140
|
+
return binding;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createSessionProfileClear(): SessionProfileClearPayload {
|
|
144
|
+
return { kind: "clear" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Unknown family identifiers are terminal unsupported states, regardless of data. */
|
|
148
|
+
export function readSessionProfileEntry(
|
|
149
|
+
entry: SessionProfileEntry,
|
|
150
|
+
): SessionProfileReadResult {
|
|
151
|
+
if (
|
|
152
|
+
entry.type !== "custom" ||
|
|
153
|
+
typeof entry.customType !== "string" ||
|
|
154
|
+
!entry.customType.startsWith(SESSION_PROFILE_FAMILY)
|
|
155
|
+
)
|
|
156
|
+
return { status: "absent" };
|
|
157
|
+
if (entry.customType !== SESSION_PROFILE_CUSTOM_TYPE)
|
|
158
|
+
return { status: "unsupported" };
|
|
159
|
+
const data = entry.data;
|
|
160
|
+
if (!isRecord(data) || !hasOwn(data, "kind")) return { status: "invalid" };
|
|
161
|
+
if (data.kind === "clear") return { status: "cleared" };
|
|
162
|
+
if (data.kind !== "bind") return { status: "invalid" };
|
|
163
|
+
const binding = readBind(data);
|
|
164
|
+
return binding ? { status: "bound", binding } : { status: "invalid" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Caller MUST supply ALREADY disk-corroborated active-branch entries, in branch
|
|
169
|
+
* order (oldest to newest). This function cannot establish that precondition:
|
|
170
|
+
* it does not read disk, call Pi, enforce append-failure recovery, establish
|
|
171
|
+
* durability, or update the shared map. In-memory getBranch() alone is NOT
|
|
172
|
+
* corroboration. Sibling and future paths must be excluded by the caller.
|
|
173
|
+
*
|
|
174
|
+
* The newest family entry wins even when invalid or unsupported; never revive
|
|
175
|
+
* an older binding. Cleared permits current fallback at a later consumer,
|
|
176
|
+
* whereas invalid/unsupported must remain distinct from absent. entryIndex is
|
|
177
|
+
* the actual supplied branch offset for a later adapter's location warning.
|
|
178
|
+
*/
|
|
179
|
+
export function replaySessionProfileBranch(
|
|
180
|
+
entries: readonly SessionProfileEntry[],
|
|
181
|
+
): SessionProfileReplayResult {
|
|
182
|
+
for (let entryIndex = entries.length - 1; entryIndex >= 0; entryIndex--) {
|
|
183
|
+
const result = readSessionProfileEntry(entries[entryIndex]);
|
|
184
|
+
if (result.status !== "absent") return { ...result, entryIndex };
|
|
185
|
+
}
|
|
186
|
+
return { status: "absent" };
|
|
187
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { lstatSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
|
|
6
6
|
export const SESSION_WORKTREE_ENTRY = "gentle-pi.session-worktree/v1";
|
|
7
7
|
export const SESSION_WORKTREE_CHANGED = "gentle-pi:session-worktree-changed";
|
|
@@ -23,12 +23,16 @@ export function worktreeGitEnvironment(env: NodeJS.ProcessEnv = process.env): No
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// Match Pi's ordinary path spelling; Git, not the argument, establishes identity.
|
|
26
|
+
function canonicalDirectory(path: string, cwd: string): string {
|
|
27
|
+
let spelling = path.replace(/^@/, "").replace(/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/g, " ");
|
|
28
|
+
if (spelling === "~" || spelling.startsWith("~/")) spelling = homedir() + spelling.slice(1);
|
|
29
|
+
const canonical = realpathSync(resolve(cwd, spelling));
|
|
30
|
+
return statSync(canonical).isDirectory() ? canonical : dirname(canonical);
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
export function resolveSessionWorktreeWithGit(path: string, cwd: string, run: typeof execFileSync = execFileSync): WorktreeIdentity | undefined {
|
|
27
34
|
try {
|
|
28
|
-
|
|
29
|
-
if (spelling === "~" || spelling.startsWith("~/")) spelling = homedir() + spelling.slice(1);
|
|
30
|
-
const canonical = realpathSync(resolve(cwd, spelling));
|
|
31
|
-
const directory = statSync(canonical).isDirectory() ? canonical : dirname(canonical);
|
|
35
|
+
const directory = canonicalDirectory(path, cwd);
|
|
32
36
|
// Ambient Git routing must not redirect a path into another repository.
|
|
33
37
|
const env = worktreeGitEnvironment();
|
|
34
38
|
const git = (arg: string) => String(run("git", ["--no-optional-locks", "-C", directory, "rev-parse", "--path-format=absolute", arg], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], shell: false, windowsHide: true, env })).replace(/\r?\n$/, "");
|
|
@@ -38,7 +42,69 @@ export function resolveSessionWorktreeWithGit(path: string, cwd: string, run: ty
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
// Filesystem identity of everything Git used to answer: no `.git` between the
|
|
46
|
+
// directory and its root, and the same root, `.git` entry and common dir.
|
|
47
|
+
// Undefined when any of it cannot be proven, so the answer is not memoized.
|
|
48
|
+
function worktreeFingerprint(directory: string, identity: WorktreeIdentity): string | undefined {
|
|
49
|
+
const same = (a: string, b: string) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
50
|
+
try {
|
|
51
|
+
for (let current = directory; !same(current, identity.root); current = dirname(current)) {
|
|
52
|
+
if (dirname(current) === current) return undefined;
|
|
53
|
+
try { lstatSync(join(current, ".git")); return undefined; }
|
|
54
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") return undefined; }
|
|
55
|
+
}
|
|
56
|
+
const marker = lstatSync(join(identity.root, ".git"));
|
|
57
|
+
const parts = [statSync(identity.root), marker, statSync(identity.commonDir)].map(entry => `${entry.dev}:${entry.ino}`);
|
|
58
|
+
// A linked worktree's `.git` file can be rewritten in place to another clone.
|
|
59
|
+
if (marker.isFile()) parts.push(`${marker.size}:${marker.mtimeMs}`);
|
|
60
|
+
return parts.join("|");
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface WorktreeMemoOptions { ttlMs?: number; now?: () => number }
|
|
67
|
+
interface MemoEntry { identity: WorktreeIdentity; directory: string; fingerprint: string; expiresAt: number }
|
|
68
|
+
const memoInvalidators = new Set<() => void>();
|
|
69
|
+
|
|
70
|
+
// Startup asks the same (cwd, cwd) question from several extensions; each
|
|
71
|
+
// answer costs two synchronous Git children. Only proven repositories are
|
|
72
|
+
// memoized, and every hit re-checks their filesystem identity, so removal,
|
|
73
|
+
// replacement, a nested `.git` and bootstrap (never cached) are seen at once.
|
|
74
|
+
// The TTL bounds what stat cannot see, such as Git config edits.
|
|
75
|
+
export function memoizeWorktreeResolver(resolver: WorktreeResolver, { ttlMs = 5000, now = Date.now }: WorktreeMemoOptions = {}): WorktreeResolver {
|
|
76
|
+
const memo = new Map<string, MemoEntry>();
|
|
77
|
+
memoInvalidators.add(() => memo.clear());
|
|
78
|
+
return (path, cwd) => {
|
|
79
|
+
const key = `${path}\u0000${cwd}`;
|
|
80
|
+
const cached = memo.get(key);
|
|
81
|
+
if (cached && cached.expiresAt > now()) {
|
|
82
|
+
try {
|
|
83
|
+
const directory = canonicalDirectory(path, cwd);
|
|
84
|
+
if (directory === cached.directory && worktreeFingerprint(directory, cached.identity) === cached.fingerprint) return { ...cached.identity };
|
|
85
|
+
} catch { /* Fall through to a fresh resolution. */ }
|
|
86
|
+
}
|
|
87
|
+
memo.delete(key);
|
|
88
|
+
const identity = resolver(path, cwd);
|
|
89
|
+
if (!identity) return undefined;
|
|
90
|
+
try {
|
|
91
|
+
const directory = canonicalDirectory(path, cwd);
|
|
92
|
+
const fingerprint = worktreeFingerprint(directory, identity);
|
|
93
|
+
if (fingerprint !== undefined) {
|
|
94
|
+
if (memo.size >= 256) memo.clear();
|
|
95
|
+
memo.set(key, { identity: { ...identity }, directory, fingerprint, expiresAt: now() + ttlMs });
|
|
96
|
+
}
|
|
97
|
+
} catch { /* Unprovable identity is returned but never memoized. */ }
|
|
98
|
+
return identity;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Drops every memoized worktree identity; the next lookup asks Git again. */
|
|
103
|
+
export function clearSessionWorktreeMemo(): void {
|
|
104
|
+
for (const invalidate of memoInvalidators) invalidate();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const resolveSessionWorktree: WorktreeResolver = memoizeWorktreeResolver(resolveSessionWorktreeWithGit);
|
|
42
108
|
|
|
43
109
|
export function toolWorktreePath(name: string, input: Record<string, unknown>): string | undefined {
|
|
44
110
|
if (!["read", "write", "edit", "grep", "find", "ls"].includes(name)) return undefined;
|
|
@@ -82,7 +148,7 @@ export class SessionWorktreeRegistry {
|
|
|
82
148
|
if (!this.isCurrent()) throw new Error("Cannot register a worktree for an inactive session.");
|
|
83
149
|
const identity = this.refreshIdentity();
|
|
84
150
|
const target = this.resolver(path, this.cwd);
|
|
85
|
-
if (!target || !identity || target.commonDir !== identity.commonDir) throw new Error("Select an existing worktree in the same Git clone as this session.");
|
|
151
|
+
if (!target || !identity || target.commonDir !== identity.commonDir) throw new Error("Select an existing worktree in the same Git clone as this session. To work in an independent Git repository, delegate with subagent_run with repository_root instead; it asks for your consent first.");
|
|
86
152
|
return target.root;
|
|
87
153
|
}
|
|
88
154
|
|
package/lib/shell-bar.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { CARD_TONE, floatRows, panelInnerWidth, renderCard } from "./shell-card.
|
|
|
6
6
|
import { REVIEW_SCOPE_UNAVAILABLE, REVIEW_SIDEBAR_LABELS, type ReviewSidebarSnapshot } from "./review-sidebar-state.ts";
|
|
7
7
|
import type { VisualSettings } from "./visual-customization-policy.ts";
|
|
8
8
|
import { renderChangesWidget, type ChangesModel } from "./shell-changes.ts";
|
|
9
|
+
import type { RunningJobDisplay } from "./jobs-sidebar-state.ts";
|
|
10
|
+
import { JOB_GLYPH } from "./background-jobs-tools.ts";
|
|
9
11
|
|
|
10
12
|
type Presentation = Pick<VisualSettings, "density" | "visibility">;
|
|
11
13
|
type HeaderPresentation = Presentation & Partial<Pick<VisualSettings, "headerPlacement" | "statusPlacement">>;
|
|
@@ -18,6 +20,7 @@ export { gaugeTone, renderGauge, type GaugeTone };
|
|
|
18
20
|
|
|
19
21
|
export interface ShellBarModel {
|
|
20
22
|
review?: ReviewSidebarSnapshot;
|
|
23
|
+
jobs?: RunningJobDisplay[];
|
|
21
24
|
profile?: string;
|
|
22
25
|
changes?: { files: number; added: number; deleted: number; notice?: string };
|
|
23
26
|
cwd: string;
|
|
@@ -35,11 +38,10 @@ export interface ShellBarModel {
|
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
// The live header row above the fullscreen rail: session identity plus the
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// never reach the header, so there is nothing on this type for them to leak
|
|
41
|
-
// through.
|
|
41
|
+
// live counters. Only the structured jobs count may join context/cost when
|
|
42
|
+
// Status is absent; opaque extension statuses and working state stay excluded.
|
|
42
43
|
export interface ShellHeaderModel {
|
|
44
|
+
jobs?: number;
|
|
43
45
|
cwd: string;
|
|
44
46
|
branch: string | null;
|
|
45
47
|
dirty: number | undefined;
|
|
@@ -55,9 +57,14 @@ export interface ShellHeaderModel {
|
|
|
55
57
|
usage: ProviderUsage | undefined;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
export function buildShellHeaderModel(model: ShellBarModel): ShellHeaderModel {
|
|
60
|
+
export function buildShellHeaderModel(model: ShellBarModel, showJobs = false): ShellHeaderModel {
|
|
59
61
|
const { cwd, branch, dirty, modelId, effort, profile, contextPercent, costTotal, subscription, usage } = model;
|
|
60
|
-
return { cwd, branch, dirty, modelId, effort, profile, contextPercent, costTotal, subscription, usage
|
|
62
|
+
return { cwd, branch, dirty, modelId, effort, profile, contextPercent, costTotal, subscription, usage,
|
|
63
|
+
...(showJobs && model.jobs?.length ? { jobs: model.jobs.length } : {}) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function shellJobsCount(count: number): string {
|
|
67
|
+
return `${JOB_GLYPH} ${count} job${count === 1 ? "" : "s"}`;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
/** A column span (`[start, end)`, in the rendered line's visible columns) a click must land in to hit the usage segment. */
|
|
@@ -110,6 +117,13 @@ export function shellEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
110
117
|
return !(value === "0" || value === "false" || value === "off");
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
// Herdr's native Pi detection matches Pi's standard loader row ("⠋ Working...").
|
|
121
|
+
// Inside a Herdr pane keep that row, which Pi shows only during real activity,
|
|
122
|
+
// so detection works without a local manifest override.
|
|
123
|
+
export function keepNativeWorkingRow(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
124
|
+
return env.HERDR_ENV === "1" && env.GENTLE_PI_AGENTS_CHILD !== "1";
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
export function formatTokens(count: number): string {
|
|
114
128
|
if (count < 1000) return count.toString();
|
|
115
129
|
if (count < 10_000) return `${(count / 1000).toFixed(1)}k`;
|
|
@@ -166,6 +180,7 @@ function buildSegments(model: ShellBarModel, theme: ShellBarTheme, presentation?
|
|
|
166
180
|
location,
|
|
167
181
|
...(presentation?.visibility.modelDetails === false ? [] : [modelSegment]),
|
|
168
182
|
...(presentation?.visibility.usageCost === false ? [] : [context, cost, ...(usage ? [usage] : [])]),
|
|
183
|
+
...(model.jobs?.length ? [theme.fg(ROLE.STATUS, shellJobsCount(model.jobs.length))] : []),
|
|
169
184
|
...statuses,
|
|
170
185
|
];
|
|
171
186
|
}
|
|
@@ -237,6 +252,10 @@ export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme
|
|
|
237
252
|
...(model.review.scope !== REVIEW_SCOPE_UNAVAILABLE ? [label(sanitizeStatus(model.review.scope))] : []),
|
|
238
253
|
],
|
|
239
254
|
}] : []),
|
|
255
|
+
...(model.jobs?.length ? [{
|
|
256
|
+
title: "Jobs",
|
|
257
|
+
lines: [...model.jobs].sort((a, b) => b.startedAt - a.startedAt).map((job) => value(sanitizeStatus(job.label))),
|
|
258
|
+
}] : []),
|
|
240
259
|
{ title: "Integrations", lines: model.statuses.length
|
|
241
260
|
? model.statuses.map((status) => theme.fg(ROLE.STATUS, sanitizeStatus(status)))
|
|
242
261
|
: [label("No status reported")] },
|
|
@@ -360,7 +379,11 @@ export function shellHeaderUsageHit(chrome: ShellHeaderChrome, x: number, y: num
|
|
|
360
379
|
|
|
361
380
|
function headerContent(model: ShellHeaderModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: Presentation): ShellHeaderResult {
|
|
362
381
|
const targetWidth = Math.max(0, Math.floor(width));
|
|
363
|
-
const
|
|
382
|
+
const jobs = model.jobs ? theme.fg(ROLE.STATUS, shellJobsCount(model.jobs)) : undefined;
|
|
383
|
+
const ctxCost = joinSegments([
|
|
384
|
+
...(jobs ? [jobs] : []),
|
|
385
|
+
...(presentation?.visibility.usageCost === false ? [] : [contextSegment(model.contextPercent, theme), costSegment(model.costTotal, model.subscription, theme)]),
|
|
386
|
+
], theme);
|
|
364
387
|
const windows = model.usage ? (selectUsageLimit(model.usage, model.modelId)?.windows ?? []) : [];
|
|
365
388
|
const leftStages = headerLeftStages(model, theme, presentation?.visibility.modelDetails !== false)
|
|
366
389
|
.map((stage) => presentation?.density === "minimal" ? stage.slice(1) : stage);
|
|
@@ -381,7 +404,7 @@ function headerContent(model: ShellHeaderModel, theme: ShellBarTheme, width: num
|
|
|
381
404
|
];
|
|
382
405
|
for (const { leftIndex, usageStage } of attempts) {
|
|
383
406
|
const usageText = usageStage ? usageSegmentText(windows, theme, usageStage, usageHint) : undefined;
|
|
384
|
-
const right =
|
|
407
|
+
const right = usageText ? joinSegments([ctxCost, usageText], theme) : ctxCost;
|
|
385
408
|
const left = joinSegments(leftStages[leftIndex]!, theme);
|
|
386
409
|
if (visibleWidth(left) + (right ? RIGHT_PADDING : 0) + visibleWidth(right) > targetWidth) continue;
|
|
387
410
|
const text = left + " ".repeat(targetWidth - visibleWidth(left) - visibleWidth(right)) + right;
|
|
@@ -390,6 +413,10 @@ function headerContent(model: ShellHeaderModel, theme: ShellBarTheme, width: num
|
|
|
390
413
|
return { text, usageSpan: { start: usageStart, end: usageStart + visibleWidth(usageText) } };
|
|
391
414
|
}
|
|
392
415
|
const brand = presentation?.density === "minimal" ? "" : theme.fg(ROLE.BRAND, theme.bold(HEADER_BRAND));
|
|
416
|
+
if (jobs && visibleWidth(jobs) <= targetWidth) {
|
|
417
|
+
const left = visibleWidth(brand) + RIGHT_PADDING + visibleWidth(jobs) <= targetWidth ? brand : "";
|
|
418
|
+
return { text: left + " ".repeat(targetWidth - visibleWidth(left) - visibleWidth(jobs)) + jobs };
|
|
419
|
+
}
|
|
393
420
|
return { text: visibleWidth(brand) <= targetWidth ? brand : "" };
|
|
394
421
|
}
|
|
395
422
|
|
|
@@ -399,11 +426,14 @@ export function renderShellBelowInputFloat(model: ShellBarModel, theme: ShellBar
|
|
|
399
426
|
if (presentation?.headerPlacement !== "below-input") return undefined;
|
|
400
427
|
const targetWidth = Math.max(0, Math.floor(width));
|
|
401
428
|
if (floatHeaderPaint(theme, targetWidth, () => "") === undefined) return undefined;
|
|
402
|
-
const chrome = renderShellHeaderChrome(buildShellHeaderModel(model), theme, targetWidth, usageHint, presentation);
|
|
429
|
+
const chrome = renderShellHeaderChrome(buildShellHeaderModel(model, presentation.statusPlacement === "hidden"), theme, targetWidth, usageHint, presentation);
|
|
403
430
|
const changesRow = changes?.files.length && presentation.visibility.changes !== false
|
|
404
431
|
? floatHeaderRow(theme, targetWidth, (inner) => renderChangesWidget(changes, theme, inner)[0] ?? "")
|
|
405
432
|
: undefined;
|
|
406
|
-
const statuses = presentation.statusPlacement === "hidden" ? [] :
|
|
433
|
+
const statuses = presentation.statusPlacement === "hidden" ? [] : [
|
|
434
|
+
...(model.jobs?.length ? [shellJobsCount(model.jobs.length)] : []),
|
|
435
|
+
...model.statuses.map(sanitizeStatus).filter(Boolean),
|
|
436
|
+
];
|
|
407
437
|
const statusRow = statuses.length
|
|
408
438
|
? floatHeaderRow(theme, targetWidth, () => joinSegments(statuses.map((status) => theme.fg(ROLE.STATUS, status)), theme))
|
|
409
439
|
: undefined;
|
|
@@ -429,7 +459,7 @@ export function renderShellBelowInputFloat(model: ShellBarModel, theme: ShellBar
|
|
|
429
459
|
export function renderShellBottomOnlyBar(model: ShellBarModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: HeaderPresentation, changes?: ChangesModel): string[] {
|
|
430
460
|
const grouped = renderShellBelowInputFloat(model, theme, width, usageHint, presentation, changes);
|
|
431
461
|
if (grouped) return grouped.rows;
|
|
432
|
-
const headerModel = buildShellHeaderModel(model);
|
|
462
|
+
const headerModel = buildShellHeaderModel(model, true);
|
|
433
463
|
// Neon, missing backgrounds and sub-minimum widths keep the old row count.
|
|
434
464
|
const rows = [headerContent(headerModel, theme, width, usageHint, presentation).text];
|
|
435
465
|
const statuses = model.statuses.map(sanitizeStatus).filter((status) => status.length > 0).map((status) => theme.fg(ROLE.STATUS, status));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Key, matchesKey, truncateToWidth, visibleWidth, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
2
2
|
import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
3
|
-
import { basename } from "node:path";
|
|
4
|
-
import { CHANGE_STATUS, changesSummary, type ChangedFile, type ChangesModel, type WorktreeChanges } from "./shell-changes.ts";
|
|
3
|
+
import { basename, posix } from "node:path";
|
|
4
|
+
import { CHANGE_STATUS, DIFF_TOO_LARGE, changesSummary, type ChangedFile, type ChangesModel, type WorktreeChanges } from "./shell-changes.ts";
|
|
5
5
|
import { paintHoverable } from "./shell-hover.ts";
|
|
6
6
|
|
|
7
7
|
// Gentle Shell changes overlay: a framed two-pane view with the working
|
|
@@ -37,7 +37,7 @@ const ROLE = {
|
|
|
37
37
|
} as const;
|
|
38
38
|
|
|
39
39
|
const HEADER_PREFIXES = ["diff --git", "index ", "--- ", "+++ ", "new file mode", "deleted file mode", "similarity index", "rename from", "rename to", "Binary files"];
|
|
40
|
-
const LIST_MAX_WIDTH =
|
|
40
|
+
const LIST_MAX_WIDTH = 48;
|
|
41
41
|
const LIST_RATIO = 0.35;
|
|
42
42
|
const CHROME_ROWS = 3;
|
|
43
43
|
const MIN_BODY_ROWS = 1;
|
|
@@ -80,8 +80,32 @@ function fileCounts(file: ChangedFile, theme: ChangesViewTheme): string {
|
|
|
80
80
|
return `${theme.fg(ROLE.ADDED, `+${file.added}`)} ${theme.fg(ROLE.REMOVED, `-${file.deleted}`)}`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// The name leads and the directory trails, so a narrow list clips the
|
|
84
|
+
// directory instead of the part that identifies the file.
|
|
83
85
|
function fileLabel(file: ChangedFile, theme: ChangesViewTheme, hovered: boolean, idleRole: string): string {
|
|
84
|
-
|
|
86
|
+
const directory = posix.dirname(file.path.replace(/\\/g, "/"));
|
|
87
|
+
const label = `${paintHoverable(theme, `${FILE_STATUS[file.status]} ${displayText(posix.basename(file.path.replace(/\\/g, "/")))}`, hovered, idleRole)} ${fileCounts(file, theme)}`;
|
|
88
|
+
return directory === "." ? label : `${label} ${theme.fg(ROLE.EMPTY, displayText(directory))}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The diff pane's first row: the selected file's full path, clipped from the
|
|
92
|
+
// left so the file name always stays visible.
|
|
93
|
+
const PATH_HEADER_ROWS = 1;
|
|
94
|
+
|
|
95
|
+
function pathHeader(file: ChangedFile, theme: ChangesViewTheme, width: number): string {
|
|
96
|
+
const status = `${FILE_STATUS[file.status]} `;
|
|
97
|
+
let path = displayText(file.path);
|
|
98
|
+
const room = Math.max(1, width - visibleWidth(status));
|
|
99
|
+
if (visibleWidth(path) > room) {
|
|
100
|
+
const characters = [...path];
|
|
101
|
+
while (characters.length > 0 && visibleWidth(`…${characters.join("")}`) > room) characters.shift();
|
|
102
|
+
path = `…${characters.join("")}`;
|
|
103
|
+
}
|
|
104
|
+
return theme.fg(ROLE.SELECTED, status + path);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function diffRows(bodyRows: number): number {
|
|
108
|
+
return Math.max(MIN_BODY_ROWS, bodyRows - PATH_HEADER_ROWS);
|
|
85
109
|
}
|
|
86
110
|
|
|
87
111
|
function fit(text: string, width: number): string {
|
|
@@ -90,10 +114,11 @@ function fit(text: string, width: number): string {
|
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
// Both standalone files and the worktree accordion use the original frame.
|
|
93
|
-
function renderPanes(width: number, rows: number, theme: ChangesViewTheme, title: string, leftLine: (row: number) => string,
|
|
117
|
+
function renderPanes(width: number, rows: number, theme: ChangesViewTheme, title: string, leftLine: (row: number) => string, diffLines: string[], keys: string, headerFile?: ChangedFile): string[] {
|
|
94
118
|
const inner = Math.max(8, width - 2);
|
|
95
119
|
const listWidth = Math.min(LIST_MAX_WIDTH, Math.floor(inner * LIST_RATIO));
|
|
96
120
|
const diffWidth = inner - listWidth - 4;
|
|
121
|
+
const rightLines = headerFile ? [pathHeader(headerFile, theme, diffWidth), ...diffLines] : diffLines;
|
|
97
122
|
const titleText = truncateToWidth(title, inner - 4, "…");
|
|
98
123
|
const top = theme.fg(ROLE.FRAME, "╭─ ") + theme.fg(ROLE.TITLE, titleText) + theme.fg(ROLE.FRAME, ` ${rule(inner - visibleWidth(titleText) - 3)}╮`);
|
|
99
124
|
const body: string[] = [];
|
|
@@ -292,7 +317,7 @@ export class WorktreeChangesView {
|
|
|
292
317
|
};
|
|
293
318
|
const keys = theme.fg(ROLE.KEY_TEXT, "j/k select enter toggle/open ← parent/fold ctrl+j/k scroll r refresh esc close");
|
|
294
319
|
this.pointerLayout = pointerLayout(width, height);
|
|
295
|
-
return renderPanes(width, height, theme, `✎ Changes · ${this.trees.length} worktrees`, leftLine, preview, keys);
|
|
320
|
+
return renderPanes(width, height, theme, `✎ Changes · ${this.trees.length} worktrees`, leftLine, preview, keys, selected?.file);
|
|
296
321
|
}
|
|
297
322
|
|
|
298
323
|
invalidate(): void {
|
|
@@ -406,8 +431,8 @@ export class ChangesView {
|
|
|
406
431
|
// the scroll keys are checked before the open key; a real Enter is CR.
|
|
407
432
|
if (data === "j" || matchesKey(data, Key.down)) this.select(this.selected + 1);
|
|
408
433
|
else if (data === "k" || matchesKey(data, Key.up)) this.select(this.selected - 1);
|
|
409
|
-
else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("j"))) this.scrollBy(this.bodyRows());
|
|
410
|
-
else if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.ctrl("k"))) this.scrollBy(-this.bodyRows());
|
|
434
|
+
else if (matchesKey(data, Key.pageDown) || matchesKey(data, Key.ctrl("j"))) this.scrollBy(diffRows(this.bodyRows()));
|
|
435
|
+
else if (matchesKey(data, Key.pageUp) || matchesKey(data, Key.ctrl("k"))) this.scrollBy(-diffRows(this.bodyRows()));
|
|
411
436
|
else if (data === "o" || matchesKey(data, Key.enter)) {
|
|
412
437
|
const file = this.model.files[this.selected];
|
|
413
438
|
if (file) this.deps.onOpen(file);
|
|
@@ -464,7 +489,7 @@ export class ChangesView {
|
|
|
464
489
|
this.fileScroll = this.clampFileScroll(this.fileScroll, rows);
|
|
465
490
|
const keys = KEYS.map(([key, label]) => `${theme.fg(ROLE.KEY, key)} ${theme.fg(ROLE.KEY_TEXT, key === "esc" ? (this.deps.backLabel ?? label) : label)}`).join(" ");
|
|
466
491
|
this.pointerLayout = pointerLayout(width, rows);
|
|
467
|
-
return renderPanes(width, rows, theme, `✎ Changes · ${changesSummary(this.model)}`, (row) => this.fileLine(row), this.visibleDiff(rows), keys);
|
|
492
|
+
return renderPanes(width, rows, theme, `✎ Changes · ${changesSummary(this.model)}`, (row) => this.fileLine(row), this.visibleDiff(rows), keys, this.model.files[this.selected]);
|
|
468
493
|
}
|
|
469
494
|
|
|
470
495
|
invalidate(): void {
|
|
@@ -500,8 +525,9 @@ export class ChangesView {
|
|
|
500
525
|
const lines = this.diffs.get(file.path);
|
|
501
526
|
if (!lines) return [];
|
|
502
527
|
if (lines.length === 0) return [this.deps.theme.fg(ROLE.EMPTY, EMPTY_DIFF)];
|
|
503
|
-
|
|
504
|
-
|
|
528
|
+
const visible = diffRows(rows);
|
|
529
|
+
this.diffScroll = Math.min(this.diffScroll, Math.max(0, lines.length - visible));
|
|
530
|
+
return lines.slice(this.diffScroll, this.diffScroll + visible);
|
|
505
531
|
}
|
|
506
532
|
|
|
507
533
|
private select(index: number): void {
|
|
@@ -534,7 +560,7 @@ export class ChangesView {
|
|
|
534
560
|
const file = this.model.files[this.selected];
|
|
535
561
|
const lines = file ? this.diffs.get(file.path) : undefined;
|
|
536
562
|
if (!lines) return false;
|
|
537
|
-
const next = Math.max(0, Math.min(Math.max(0, lines.length - rows), this.diffScroll + Math.trunc(delta)));
|
|
563
|
+
const next = Math.max(0, Math.min(Math.max(0, lines.length - diffRows(rows)), this.diffScroll + Math.trunc(delta)));
|
|
538
564
|
if (next === this.diffScroll) return false;
|
|
539
565
|
this.diffScroll = next;
|
|
540
566
|
return true;
|
|
@@ -557,7 +583,8 @@ export class ChangesView {
|
|
|
557
583
|
void this.deps.loadDiff(file).then(
|
|
558
584
|
(text) => {
|
|
559
585
|
if (this.disposed) return;
|
|
560
|
-
|
|
586
|
+
// An oversized diff arrives as one plain line pointing at the editor.
|
|
587
|
+
this.diffs.set(file.path, text === DIFF_TOO_LARGE ? [this.deps.theme.fg(ROLE.EMPTY, text)] : colorDiff(text, this.deps.theme));
|
|
561
588
|
this.deps.requestRender();
|
|
562
589
|
},
|
|
563
590
|
() => {
|