gentle-pi 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -8
- 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 +703 -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/bundled-install.md +397 -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 +81 -31
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +267 -40
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +9 -9
- 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 +688 -88
- package/extensions/gentle-ai.ts +886 -238
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +265 -69
- package/extensions/gentle-todo.ts +108 -13
- 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-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +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/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +74 -23
- 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/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 +18 -6
- 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 +266 -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 +86 -71
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +62 -19
- 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 +7 -3
- 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 +1 -1
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +73 -22
- 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/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/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/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 +288 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1722 -59
- 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 +249 -5
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +234 -13
- 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 +541 -96
- package/tests/gentle-todo.test.ts +253 -2
- 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 +2158 -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 +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-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/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 +101 -17
- 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/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/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 +30 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +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 +61 -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 +120 -2
- 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 +7 -7
- 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 +32 -4
- 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-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
|
() => {
|
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,
|
|
@@ -123,7 +127,7 @@ export function changesModel(changed: ChangedFile[]): ChangesModel {
|
|
|
123
127
|
|
|
124
128
|
export function changesSummary(model: ChangesModel): string {
|
|
125
129
|
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" : ""}
|
|
130
|
+
return `${model.files.length} ${noun} · +${model.added} −${model.deleted}${model.files.some(file => file.countsUnavailable) ? " · partial counts" : ""}`;
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
// One line: summary, the files joined by dots, and the command pushed to
|
|
@@ -132,7 +136,7 @@ export function renderChangesWidget(model: ChangesModel, theme: ChangesTheme, wi
|
|
|
132
136
|
if (model.files.length === 0) return [];
|
|
133
137
|
const noun = model.files.length === 1 ? "file" : "files";
|
|
134
138
|
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" : ""}
|
|
139
|
+
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
140
|
const hint = theme.fg("dim", CHANGES_COMMAND);
|
|
137
141
|
const list = model.files.map((file) => theme.fg("muted", file.path)).join(` ${dot} `);
|
|
138
142
|
const left = `${head} ${dot} ${list}`;
|
|
@@ -146,10 +150,14 @@ export function renderChangesWidget(model: ChangesModel, theme: ChangesTheme, wi
|
|
|
146
150
|
export interface GitResult {
|
|
147
151
|
stdout: string;
|
|
148
152
|
code: number;
|
|
153
|
+
/** The runner stopped reading at its output bound; stdout is partial. */
|
|
154
|
+
truncated?: boolean;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
export type GitRunner = (args: string[]) => Promise<GitResult>;
|
|
152
158
|
export type LineCounter = (path: string) => Promise<number>;
|
|
159
|
+
/** A cheap content identity (e.g. size and mtime) so same-count edits refresh their diff. */
|
|
160
|
+
export type FileRevision = (path: string) => Promise<string | undefined>;
|
|
153
161
|
|
|
154
162
|
export interface WorktreeChanges {
|
|
155
163
|
root: string;
|
|
@@ -189,76 +197,41 @@ export async function foreignRootBranch(git: GitRunner): Promise<string | undefi
|
|
|
189
197
|
return verified.code === 0 ? branch : "no commits yet";
|
|
190
198
|
}
|
|
191
199
|
|
|
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
|
-
|
|
200
|
+
// /gentle:changes: one group per registered root holding its working tree
|
|
201
|
+
// against HEAD. A root whose Git fails (timed out, rejected) has no group but
|
|
202
|
+
// is listed in failedRoots: a failed scan is never reported as a clean root.
|
|
242
203
|
export class WorktreeChangesTracker {
|
|
243
204
|
worktrees: WorktreeChanges[] = [];
|
|
205
|
+
failedRoots: string[] = [];
|
|
206
|
+
scannedRootCount = 0;
|
|
244
207
|
private inFlight: Promise<ChangesModel> | undefined;
|
|
245
208
|
private readonly discover: GitRunner;
|
|
246
209
|
private readonly gitForRoot: (root: string) => GitRunner;
|
|
247
210
|
private readonly linesForRoot: (root: string) => LineCounter;
|
|
248
211
|
private readonly registeredRoots: () => readonly string[];
|
|
212
|
+
private readonly revisionForRoot: (root: string) => FileRevision | undefined;
|
|
249
213
|
|
|
250
|
-
constructor(discover: GitRunner, gitForRoot: (root: string) => GitRunner, linesForRoot: (root: string) => LineCounter = () => noLines, registeredRoots: () => readonly string[] = () => []) {
|
|
214
|
+
constructor(discover: GitRunner, gitForRoot: (root: string) => GitRunner, linesForRoot: (root: string) => LineCounter = () => noLines, registeredRoots: () => readonly string[] = () => [], revisionForRoot: (root: string) => FileRevision | undefined = () => undefined) {
|
|
251
215
|
this.discover = discover;
|
|
252
216
|
this.gitForRoot = gitForRoot;
|
|
253
217
|
this.linesForRoot = linesForRoot;
|
|
254
218
|
this.registeredRoots = registeredRoots;
|
|
219
|
+
this.revisionForRoot = revisionForRoot;
|
|
255
220
|
}
|
|
256
221
|
|
|
222
|
+
private modelOf: WorktreeChanges[] | undefined;
|
|
223
|
+
private combined: ChangesModel = emptyChanges();
|
|
224
|
+
|
|
225
|
+
// Footers read this every frame; rebuild only when a scan replaced the trees.
|
|
257
226
|
get model(): ChangesModel {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
227
|
+
if (this.modelOf !== this.worktrees) {
|
|
228
|
+
this.modelOf = this.worktrees;
|
|
229
|
+
this.combined = changesModel(this.worktrees.flatMap((tree) => tree.model.files.map((file) => ({
|
|
230
|
+
...file,
|
|
231
|
+
path: this.worktrees.length === 1 ? file.path : `${tree.root}/${file.path}`,
|
|
232
|
+
}))));
|
|
233
|
+
}
|
|
234
|
+
return this.combined;
|
|
262
235
|
}
|
|
263
236
|
|
|
264
237
|
async start(): Promise<void> {
|
|
@@ -278,17 +251,29 @@ export class WorktreeChangesTracker {
|
|
|
278
251
|
}
|
|
279
252
|
|
|
280
253
|
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
254
|
// Discovery labels registered roots; it never grants visibility to siblings.
|
|
285
255
|
const roots = new Set(this.registeredRoots());
|
|
286
|
-
|
|
256
|
+
// No root to scan means no Git at all, not even branch discovery.
|
|
257
|
+
if (roots.size === 0) {
|
|
258
|
+
this.worktrees = [];
|
|
259
|
+
this.failedRoots = [];
|
|
260
|
+
this.scannedRootCount = 0;
|
|
261
|
+
return this.model;
|
|
262
|
+
}
|
|
263
|
+
const result = await this.discover(["worktree", "list", "--porcelain", "-z"]).catch(() => ({ code: 128, stdout: "" }));
|
|
264
|
+
const metadata = new Map((result.code === 0 ? parseWorktrees(result.stdout) : []).map((tree) => [tree.root, tree]));
|
|
265
|
+
// Roots scan in parallel so one slow root does not delay the others.
|
|
266
|
+
const failed: string[] = [];
|
|
267
|
+
const scanned = await Promise.all([...roots].map(async (root): Promise<WorktreeChanges | undefined> => {
|
|
287
268
|
const known = metadata.get(root);
|
|
288
269
|
try {
|
|
289
|
-
const tracker = new ChangesTracker(this.gitForRoot(root), this.linesForRoot(root));
|
|
270
|
+
const tracker = new ChangesTracker(this.gitForRoot(root), this.linesForRoot(root), this.revisionForRoot(root));
|
|
290
271
|
await tracker.start();
|
|
291
|
-
if (!tracker.
|
|
272
|
+
if (!tracker.statusRead) {
|
|
273
|
+
failed.push(root);
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
if (!tracker.model.files.length) return undefined;
|
|
292
277
|
// A root missing from this discovery scan belongs to a different Git
|
|
293
278
|
// clone entirely (e.g. an inner repository nested inside an outer
|
|
294
279
|
// one) -- worktree list can never describe a foreign clone, so ask
|
|
@@ -299,22 +284,40 @@ export class WorktreeChangesTracker {
|
|
|
299
284
|
// change scan above failing -- that must label the root
|
|
300
285
|
// UNKNOWN_BRANCH, not drop real, already-captured changes.
|
|
301
286
|
const branch = known ? known.branch : await foreignRootBranch(this.gitForRoot(root)).catch(() => UNKNOWN_BRANCH);
|
|
302
|
-
|
|
287
|
+
return { root, ...(branch ? { branch } : {}), model: tracker.model };
|
|
303
288
|
} catch {
|
|
304
|
-
//
|
|
289
|
+
// A root the caller stops registering mid-scan (e.g. a removed
|
|
290
|
+
// worktree) triggers the rescan below without it, not a failure.
|
|
291
|
+
failed.push(root);
|
|
292
|
+
return undefined;
|
|
305
293
|
}
|
|
306
|
-
}
|
|
294
|
+
}));
|
|
295
|
+
const trees = scanned.filter((tree): tree is WorktreeChanges => tree !== undefined);
|
|
307
296
|
const latest = new Set(this.registeredRoots());
|
|
308
297
|
// A root admitted during status must not wait for a later poll (which may
|
|
309
298
|
// be disabled). Ordinary overlapping polls still share exactly one scan.
|
|
310
299
|
if (latest.size !== roots.size || [...latest].some((root) => !roots.has(root))) return this.capture();
|
|
311
300
|
this.worktrees = trees;
|
|
301
|
+
this.failedRoots = failed;
|
|
302
|
+
this.scannedRootCount = roots.size;
|
|
312
303
|
return this.model;
|
|
313
304
|
}
|
|
314
305
|
}
|
|
315
306
|
|
|
316
307
|
const noLines: LineCounter = async () => 0;
|
|
317
308
|
|
|
309
|
+
// Per-file line counts and revision lookups are file IO: a large change set
|
|
310
|
+
// runs this many files at a time instead of one by one or all at once.
|
|
311
|
+
export const FILE_WORK_CONCURRENCY = 8;
|
|
312
|
+
|
|
313
|
+
async function eachLimited<T>(items: readonly T[], limit: number, work: (item: T) => Promise<void>): Promise<void> {
|
|
314
|
+
let next = 0;
|
|
315
|
+
const worker = async () => {
|
|
316
|
+
while (next < items.length) await work(items[next++]);
|
|
317
|
+
};
|
|
318
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
319
|
+
}
|
|
320
|
+
|
|
318
321
|
const NUMSTAT_ARGS = ["diff", "--numstat", "HEAD"];
|
|
319
322
|
const PORCELAIN_ARGS = ["status", "--porcelain=v1", "--untracked-files=all", "-z"];
|
|
320
323
|
|
|
@@ -324,20 +327,27 @@ const PORCELAIN_ARGS = ["status", "--porcelain=v1", "--untracked-files=all", "-z
|
|
|
324
327
|
export class ChangesTracker {
|
|
325
328
|
private readonly git: GitRunner;
|
|
326
329
|
private readonly countLines: LineCounter;
|
|
330
|
+
private readonly revision: FileRevision | undefined;
|
|
327
331
|
private current: ChangesModel = emptyChanges();
|
|
328
332
|
private available = false;
|
|
329
333
|
private inFlight: Promise<ChangesModel> | undefined;
|
|
330
334
|
private queued = false;
|
|
331
335
|
|
|
332
|
-
constructor(git: GitRunner, countLines: LineCounter = noLines) {
|
|
336
|
+
constructor(git: GitRunner, countLines: LineCounter = noLines, revision?: FileRevision) {
|
|
333
337
|
this.git = git;
|
|
334
338
|
this.countLines = countLines;
|
|
339
|
+
this.revision = revision;
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
get model(): ChangesModel {
|
|
338
343
|
return this.current;
|
|
339
344
|
}
|
|
340
345
|
|
|
346
|
+
/** False when the last start could not read `git status`. */
|
|
347
|
+
get statusRead(): boolean {
|
|
348
|
+
return this.available;
|
|
349
|
+
}
|
|
350
|
+
|
|
341
351
|
async start(): Promise<void> {
|
|
342
352
|
const files = await this.capture();
|
|
343
353
|
this.available = files !== undefined;
|
|
@@ -372,9 +382,14 @@ export class ChangesTracker {
|
|
|
372
382
|
if (porcelain.code !== 0) return undefined;
|
|
373
383
|
const files = snapshotChanges({ numstat: numstat.stdout, porcelain: porcelain.stdout });
|
|
374
384
|
// Untracked files never appear in numstat; count their lines directly.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
385
|
+
await eachLimited(files, FILE_WORK_CONCURRENCY, async (file) => {
|
|
386
|
+
const [added, revision] = await Promise.all([
|
|
387
|
+
file.status === CHANGE_STATUS.UNTRACKED ? this.countLines(file.path).catch(() => 0) : undefined,
|
|
388
|
+
this.revision?.(file.path).catch(() => undefined),
|
|
389
|
+
]);
|
|
390
|
+
if (added !== undefined) file.added = added;
|
|
391
|
+
if (revision) file.diffRevision = revision;
|
|
392
|
+
});
|
|
378
393
|
return files;
|
|
379
394
|
}
|
|
380
395
|
}
|
|
@@ -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
|
};
|