gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
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
|
}
|
package/lib/shell-prompt.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { stripAnsi } from "./terminal-theme.ts";
|
|
3
|
+
import { CARD_STYLE, type CardStyle } from "./shell-card.ts";
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// with a petal that shows what the agent is doing. Everything here is pure.
|
|
5
|
+
// Pure prompt chrome: neon keeps Pi's rule/content/rule rows in a rounded
|
|
6
|
+
// frame; float retains those indices as painted status/content/padding.
|
|
7
7
|
|
|
8
8
|
export const PROMPT_STATE = {
|
|
9
9
|
IDLE: "idle",
|
|
@@ -43,6 +43,8 @@ export function scanWorkingText(text: string, tick: number, fg: (role: string, t
|
|
|
43
43
|
const PETAL_TONE_FRAMES = [PETAL_TONE.DEEP, PETAL_TONE.SOFT, PETAL_TONE.ROSE, PETAL_TONE.BRIGHT, PETAL_TONE.ROSE, PETAL_TONE.SOFT, PETAL_TONE.DEEP, PETAL_TONE.DEEP] as const;
|
|
44
44
|
|
|
45
45
|
export interface PromptFrameOptions {
|
|
46
|
+
style?: CardStyle;
|
|
47
|
+
bg?: (role: string, text: string) => string;
|
|
46
48
|
state: PromptState;
|
|
47
49
|
tick: number;
|
|
48
50
|
borderColor: (text: string) => string;
|
|
@@ -50,11 +52,17 @@ export interface PromptFrameOptions {
|
|
|
50
52
|
bold?: (text: string) => string;
|
|
51
53
|
/**
|
|
52
54
|
* Overrides the editor's own bottom scroll indicator (e.g. "esc again to
|
|
53
|
-
* cancel"). Both share the bottom rule
|
|
55
|
+
* cancel"). Both share the bottom rule / float hint slot; an explicit
|
|
54
56
|
* hint always wins because it reflects state the editor cannot render on
|
|
55
57
|
* its own.
|
|
56
58
|
*/
|
|
57
59
|
escHint?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Overrides the generic "working…" label while state is WORKING with an
|
|
62
|
+
* explicit, orchestrator-reported ODD phase label (e.g. "exploring…").
|
|
63
|
+
* Ignored outside the WORKING state; undefined falls back to "working…".
|
|
64
|
+
*/
|
|
65
|
+
workingLabel?: string;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
// A terminal cell cannot grow, so the petal earns presence with weight and
|
|
@@ -93,8 +101,13 @@ function rule(length: number): string {
|
|
|
93
101
|
return "─".repeat(Math.max(0, length));
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
function stateLabel(options: PromptFrameOptions): string | undefined {
|
|
105
|
+
if (options.state === PROMPT_STATE.WORKING) return options.workingLabel ?? STATE_LABEL[PROMPT_STATE.WORKING];
|
|
106
|
+
return STATE_LABEL[options.state];
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
function topRule(width: number, options: PromptFrameOptions, indicator: string | undefined): string {
|
|
97
|
-
const label = indicator ??
|
|
110
|
+
const label = indicator ?? stateLabel(options);
|
|
98
111
|
const scanning = options.state === PROMPT_STATE.WORKING;
|
|
99
112
|
const glyph = petalGlyph(options.state, options.tick);
|
|
100
113
|
const petal = options.fg(petalTone(options.state, options.tick), options.bold ? options.bold(glyph) : glyph);
|
|
@@ -120,8 +133,91 @@ function sideRules(line: string, innerWidth: number, options: PromptFrameOptions
|
|
|
120
133
|
return options.borderColor("│") + content + options.borderColor("│");
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
export
|
|
136
|
+
export interface PromptLayout {
|
|
137
|
+
width: number;
|
|
138
|
+
nativeWidth: number;
|
|
139
|
+
prefixWidth: number;
|
|
140
|
+
background: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Resolve fallback before native wrapping, selection, completion and mouse geometry. */
|
|
144
|
+
export function resolvePromptLayout(width: number, style?: CardStyle, bg?: PromptFrameOptions["bg"]): PromptLayout {
|
|
145
|
+
width = Math.max(0, Math.floor(width));
|
|
146
|
+
let background = "";
|
|
147
|
+
if (style === CARD_STYLE.FLOAT && width >= 10 && bg) {
|
|
148
|
+
try {
|
|
149
|
+
const painted = bg("toolSuccessBg", " ");
|
|
150
|
+
const match = painted.match(/^((?:\x1b\[[\d;]*m)+) (?:\x1b\[(?:49|0)?m)$/);
|
|
151
|
+
if (match) {
|
|
152
|
+
for (const sgr of match[1].matchAll(/\x1b\[([\d;]*)m/g)) {
|
|
153
|
+
for (const command of promptSgrCommands(sgr[1])) {
|
|
154
|
+
const code = command[0];
|
|
155
|
+
if (code === 0 || code === 49) background = "";
|
|
156
|
+
else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107) ||
|
|
157
|
+
(code === 48 && ((command[1] === 5 && command.length === 3) || (command[1] === 2 && command.length === 5)))) {
|
|
158
|
+
background = `\x1b[${command.join(";")}m`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch { /* Missing theme background: retain the entire neon path. */ }
|
|
164
|
+
}
|
|
165
|
+
return { width, nativeWidth: Math.max(1, width - (background ? 5 : 2)), prefixWidth: background ? 3 : 1, background };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Extended-colour parameters belong to one command: zero RGB channels
|
|
169
|
+
// must never be interpreted as full styling resets.
|
|
170
|
+
function promptSgrCommands(parameters: string): number[][] {
|
|
171
|
+
const codes = parameters.split(";").map(Number);
|
|
172
|
+
const commands: number[][] = [];
|
|
173
|
+
for (let i = 0; i < codes.length;) {
|
|
174
|
+
const extended = codes[i] === 38 || codes[i] === 48 || codes[i] === 58;
|
|
175
|
+
const length = extended && codes[i + 1] === 2 ? 5 : extended && codes[i + 1] === 5 ? 3 : 1;
|
|
176
|
+
commands.push(codes.slice(i, i + length));
|
|
177
|
+
i += length;
|
|
178
|
+
}
|
|
179
|
+
return commands;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Rearm only the panel background, leaving marker/inversion bytes intact.
|
|
183
|
+
function rearmPromptBackground(text: string, background: string): string {
|
|
184
|
+
return text.replace(/\x1b\[([\d;]*)m/g, (sgr, parameters: string) =>
|
|
185
|
+
promptSgrCommands(parameters).some(([code]) => code === 0 || code === 49) ? sgr + background : sgr);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Same chrome and horizontal prefix for editable and native completion rows. */
|
|
189
|
+
export function floatPromptRow(line: string, layout: PromptLayout, borderColor: PromptFrameOptions["borderColor"]): string {
|
|
190
|
+
const clipped = truncateToWidth(line, layout.nativeWidth, "");
|
|
191
|
+
const content = borderColor("▎") + " " + clipped + " ".repeat(Math.max(0, layout.nativeWidth - visibleWidth(clipped))) + " ";
|
|
192
|
+
return " " + layout.background + rearmPromptBackground(content, layout.background) + "\x1b[49m ";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function floatPromptHint(top: string, bottom: string, options: PromptFrameOptions, width: number): string {
|
|
196
|
+
const glyph = petalGlyph(options.state, options.tick);
|
|
197
|
+
const petal = options.fg(petalTone(options.state, options.tick), options.bold ? options.bold(glyph) : glyph);
|
|
198
|
+
const label = stateLabel(options) ?? "waiting for input";
|
|
199
|
+
let paintedLabel = "";
|
|
200
|
+
if (label) {
|
|
201
|
+
paintedLabel = options.state === PROMPT_STATE.WORKING
|
|
202
|
+
? scanWorkingText(label, options.tick, options.fg)
|
|
203
|
+
: options.fg(LABEL_ROLE, label);
|
|
204
|
+
}
|
|
205
|
+
const info = [scrollIndicator(top), options.escHint ?? scrollIndicator(bottom)]
|
|
206
|
+
.filter((text): text is string => Boolean(text))
|
|
207
|
+
.map((text) => options.fg(LABEL_ROLE, text));
|
|
208
|
+
const hint = truncateToWidth(" " + petal + (paintedLabel ? " " + paintedLabel : "") + (info.length ? " · " + info.join(" · ") : ""), width, "");
|
|
209
|
+
return hint + " ".repeat(Math.max(0, width - visibleWidth(hint)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function framePromptLines(lines: string[], width: number, options: PromptFrameOptions, layout = resolvePromptLayout(width, options.style, options.bg)): string[] {
|
|
124
213
|
width = Math.max(0, Math.floor(width));
|
|
214
|
+
if (layout.background && lines.length >= 2) {
|
|
215
|
+
return [
|
|
216
|
+
floatPromptRow(floatPromptHint(lines[0], lines[lines.length - 1], options, layout.nativeWidth).slice(2), layout, options.borderColor),
|
|
217
|
+
...lines.slice(1, -1).map((line) => floatPromptRow(line, layout, options.borderColor)),
|
|
218
|
+
floatPromptRow("", layout, options.borderColor),
|
|
219
|
+
];
|
|
220
|
+
}
|
|
125
221
|
if (lines.length < 2) return lines.map((line) => truncateToWidth(line, width, ""));
|
|
126
222
|
if (width < 2) return lines.map((_line, index) => width === 0 ? "" : options.borderColor(index === 0 ? "╭" : index === lines.length - 1 ? "╰" : "│"));
|
|
127
223
|
const innerWidth = width - 2;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ScrollView, VStack, visibleWidth, type Component, type TUI, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
2
2
|
import { sidebarState, type SidebarRail } from "./shell-sidebar.ts";
|
|
3
3
|
import type { ShellBarTheme } from "./shell-bar.ts";
|
|
4
|
+
import { CARD_STYLE, cardStyle, type CardStyle } from "./shell-card.ts";
|
|
4
5
|
import { renderSidebarBanner } from "./shell-sidebar-banner.ts";
|
|
6
|
+
import type { Density, HeaderPlacement, StatusPlacement } from "./visual-customization-policy.ts";
|
|
5
7
|
|
|
6
8
|
export const SIDEBAR_BREAKPOINT = 140;
|
|
7
9
|
const RAIL_WIDTH = 50;
|
|
@@ -12,6 +14,8 @@ const RAIL_PADDING = 1;
|
|
|
12
14
|
// the card instead of touching the terminal edge.
|
|
13
15
|
const HEADER_RIGHT_INSET = RAIL_PADDING + 1;
|
|
14
16
|
const GAP = 3;
|
|
17
|
+
/** Right-edge columns the painting rail takes from the editor column; published as `railColumns`. */
|
|
18
|
+
export const SIDEBAR_RAIL_COLUMNS = RAIL_WIDTH + GAP;
|
|
15
19
|
// Experimental Pi 0.85.1 internals. Only the fullscreen layout tree is adapted;
|
|
16
20
|
// regular mode keeps native scrollback and the original bottom components.
|
|
17
21
|
const NODE = Symbol.for("@earendil-works/pi-tui/layout-node");
|
|
@@ -27,6 +31,8 @@ type PreparedRail = {
|
|
|
27
31
|
revision: number;
|
|
28
32
|
width: number;
|
|
29
33
|
mode: string | undefined;
|
|
34
|
+
headerPlacement: HeaderPlacement;
|
|
35
|
+
cardStyle: CardStyle;
|
|
30
36
|
root: LayoutRoot;
|
|
31
37
|
theme: ShellBarTheme;
|
|
32
38
|
parts: Array<[string, SidebarRail]>;
|
|
@@ -68,8 +74,28 @@ function railDigest(rail: SidebarRail): string | undefined {
|
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
export const STATUS_OWNER = { HEADER: "header", BOTTOM: "bottom" } as const;
|
|
78
|
+
export type StatusOwner = (typeof STATUS_OWNER)[keyof typeof STATUS_OWNER];
|
|
79
|
+
export interface StatusOwnerInput {
|
|
80
|
+
mode: string | undefined;
|
|
81
|
+
columns: number;
|
|
82
|
+
statusPlacement: StatusPlacement;
|
|
83
|
+
headerPlacement: HeaderPlacement;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Which single status row owns a narrow fullscreen terminal, so the header and
|
|
88
|
+
* the bottom bar never both paint there. A configured top header wins;
|
|
89
|
+
* otherwise the bottom bar does, unless Status is hidden and the below-input
|
|
90
|
+
* header is all that is left. Wide and regular layouts keep their own rules.
|
|
91
|
+
*/
|
|
92
|
+
export function narrowStatusOwner(input: StatusOwnerInput): StatusOwner | undefined {
|
|
93
|
+
if (input.mode !== "fullscreen" || input.columns >= SIDEBAR_BREAKPOINT) return undefined;
|
|
94
|
+
return input.headerPlacement === "top" || input.statusPlacement === "hidden" ? STATUS_OWNER.HEADER : STATUS_OWNER.BOTTOM;
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
/** Installs the fullscreen rail: wraps the host layout root with the [rail, transcript] hstack and returns a disposer restoring the original layout. */
|
|
72
|
-
export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
98
|
+
export function installSidebar(tui: TUI, theme: ShellBarTheme, placement: () => StatusPlacement = () => "auto", headerPlacement: () => HeaderPlacement = () => "top", density: () => Density = () => "comfortable"): () => void {
|
|
73
99
|
if (!tui.terminal) return () => {};
|
|
74
100
|
const host = tui as Host;
|
|
75
101
|
const state = sidebarState(tui);
|
|
@@ -89,7 +115,17 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
89
115
|
// revision counter, exactly like the whole-rail memo already did.
|
|
90
116
|
const sectionCache = new Map<string, SectionCacheEntry>();
|
|
91
117
|
state.active = false;
|
|
92
|
-
|
|
118
|
+
// Hidden removes Status everywhere, including regular mode where the rail
|
|
119
|
+
// never mounts, so it is published independently of the fullscreen layout.
|
|
120
|
+
state.statusHidden = () => !stopped && placement() === "hidden";
|
|
121
|
+
// A narrow top header is the only status row there. Whether it paints comes
|
|
122
|
+
// from the last layout pass (a blank or failed header never swallows the
|
|
123
|
+
// bottom bar); the geometry is read live so a resize applies before the next pass.
|
|
124
|
+
const headerOwnsStatus = () => !stopped && !failed && headerLines.length > 0 && headerPlacement() === "top" &&
|
|
125
|
+
narrowStatusOwner({ mode: host.mode, columns: tui.terminal.columns, statusPlacement: placement(), headerPlacement: headerPlacement() }) === STATUS_OWNER.HEADER;
|
|
126
|
+
state.headerOwnsStatus = headerOwnsStatus;
|
|
127
|
+
state.railColumns = SIDEBAR_RAIL_COLUMNS;
|
|
128
|
+
state.ownsHost = () => !stopped && host.mode === "fullscreen" && tui.terminal.columns >= SIDEBAR_BREAKPOINT && (placement() === "auto" || placement() === "right") && !!host.layoutRoot && roots.has(host.layoutRoot);
|
|
93
129
|
const rail: Component = {
|
|
94
130
|
render: () => railLines,
|
|
95
131
|
invalidate() {
|
|
@@ -143,6 +179,10 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
143
179
|
};
|
|
144
180
|
scroll.handleMouse = (event) => {
|
|
145
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;
|
|
146
186
|
// Consume even at the boundary or over blank rail space: Pi 0.85.1
|
|
147
187
|
// can send unconsumed delta to the primary transcript despite containment.
|
|
148
188
|
scroll.scrollBy(event.wheelDelta ?? 0);
|
|
@@ -156,14 +196,30 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
156
196
|
};
|
|
157
197
|
const prepare = (width: number, root: LayoutRoot): boolean => {
|
|
158
198
|
state.active = false;
|
|
159
|
-
if (stopped || failed || host.mode !== "fullscreen"
|
|
199
|
+
if (stopped || failed || host.mode !== "fullscreen") {
|
|
160
200
|
prepared = undefined;
|
|
201
|
+
headerLines = [];
|
|
161
202
|
return false;
|
|
162
203
|
}
|
|
204
|
+
const railEligible = width >= SIDEBAR_BREAKPOINT && placement() !== "bottom" && placement() !== "hidden";
|
|
205
|
+
if (!railEligible) {
|
|
206
|
+
prepared = undefined;
|
|
207
|
+
try {
|
|
208
|
+
const headerPart = state.parts.get("header");
|
|
209
|
+
headerLines = headerPlacement() === "top" && headerPart
|
|
210
|
+
? [...(headerPart.render(Math.max(0, width)) ?? [])] : [];
|
|
211
|
+
if (!headerLines.some((line) => line.trim() !== "")) headerLines = [];
|
|
212
|
+
} catch {
|
|
213
|
+
failed = true;
|
|
214
|
+
headerLines = [];
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const style = cardStyle();
|
|
163
219
|
const parts = [...state.parts.entries()];
|
|
164
220
|
const digests = parts.map(([, rail]) => railDigest(rail));
|
|
165
221
|
const unchanged = prepared?.revision === cache.revision &&
|
|
166
|
-
prepared.width === width && prepared.mode === host.mode && prepared.root === root && prepared.theme === theme &&
|
|
222
|
+
prepared.width === width && prepared.mode === host.mode && prepared.headerPlacement === headerPlacement() && prepared.cardStyle === style && prepared.root === root && prepared.theme === theme &&
|
|
167
223
|
prepared.parts.length === parts.length && prepared.parts.every(([key, part], index) => parts[index]?.[0] === key && parts[index]?.[1] === part) &&
|
|
168
224
|
prepared.digests.length === digests.length && prepared.digests.every((digest, index) => digest === digests[index]);
|
|
169
225
|
if (unchanged) {
|
|
@@ -179,7 +235,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
179
235
|
const preparedHeaderLines = [...(headerPart?.render(Math.max(0, width - HEADER_RIGHT_INSET)) ?? [])];
|
|
180
236
|
const headerActive = headerPart !== undefined && preparedHeaderLines.some((line) => line.trim() !== "");
|
|
181
237
|
const contentWidth = scroll.getContentWidth(RAIL_WIDTH);
|
|
182
|
-
const sections = ["footer", "agents", "todo"].map((key) => {
|
|
238
|
+
const sections = ["footer", "agents", "todo"].filter((key) => key !== "todo" || state.visibility?.todo !== false).map((key) => {
|
|
183
239
|
const component = state.parts.get(key);
|
|
184
240
|
if (!component) {
|
|
185
241
|
sectionCache.delete(key);
|
|
@@ -204,23 +260,23 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
204
260
|
railLines = [];
|
|
205
261
|
if (sections.length && branding.length) {
|
|
206
262
|
railLines.push(...branding.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING)));
|
|
207
|
-
} else if (sections.length && headerActive) {
|
|
208
|
-
//
|
|
209
|
-
//
|
|
263
|
+
} else if (sections.length && headerActive && density() === "comfortable" && style === CARD_STYLE.NEON) {
|
|
264
|
+
// Preserve neon's header gap. Float's painted top padding starts
|
|
265
|
+
// on the same body row as the transcript, without an external gap.
|
|
210
266
|
railLines.push("");
|
|
211
267
|
}
|
|
212
268
|
for (const section of sections) {
|
|
213
269
|
// One blank row separates a section from the banner or the
|
|
214
|
-
// previous section;
|
|
215
|
-
if (hits.length > 0 || branding.length > 0) railLines.push("");
|
|
270
|
+
// previous section; neon's header gap above is not a section.
|
|
271
|
+
if ((hits.length > 0 || branding.length > 0) && density() === "comfortable") railLines.push("");
|
|
216
272
|
const startY = railLines.length;
|
|
217
273
|
railLines.push(...section.lines.map((line) => " ".repeat(RAIL_PADDING) + line + " ".repeat(RAIL_PADDING)));
|
|
218
274
|
hits.push({ key: section.key, component: section.component, startY, height: section.lines.length, width: contentWidth - RAIL_PADDING * 2 });
|
|
219
275
|
}
|
|
220
276
|
// Height is owned by the native ScrollView, never by the transcript.
|
|
221
277
|
const active = railLines.length > 0 && railLines.every((line) => visibleWidth(line) <= contentWidth);
|
|
222
|
-
headerLines =
|
|
223
|
-
prepared = { revision: cache.revision, width, mode: host.mode, root, theme, parts, digests, contentWidth, active, lines: railLines, hits, headerLines, headerActive: headerLines.length > 0 };
|
|
278
|
+
headerLines = headerActive && headerPlacement() === "top" ? preparedHeaderLines : [];
|
|
279
|
+
prepared = { revision: cache.revision, width, mode: host.mode, headerPlacement: headerPlacement(), cardStyle: style, root, theme, parts, digests, contentWidth, active, lines: railLines, hits, headerLines, headerActive: headerLines.length > 0 };
|
|
224
280
|
state.active = active;
|
|
225
281
|
return active;
|
|
226
282
|
} catch {
|
|
@@ -264,6 +320,11 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
264
320
|
}
|
|
265
321
|
return { ...node, entries: entries.map((entry, index) => index === entries.length - 1 ? { ...entry, component: wrapped! } : entry) };
|
|
266
322
|
};
|
|
323
|
+
// Without a rail the native layout stays in place; a hidden Status, or a
|
|
324
|
+
// narrow top header owning it, only frees the footer's reserved dock row,
|
|
325
|
+
// exactly as the rail does.
|
|
326
|
+
const nativeLayout = () => placement() === "hidden" || headerOwnsStatus() ? reclaimFooterRow(original.call(root)) : original.call(root);
|
|
327
|
+
const nativeHost = { render: () => [], invalidate() {}, [NODE]: nativeLayout };
|
|
267
328
|
const left = { render: () => [], invalidate() {}, [NODE]: () => reclaimFooterRow(original.call(root)) };
|
|
268
329
|
// Stable component wrapping the [left, scroll] hstack behind its own
|
|
269
330
|
// NODE, exactly like `left` wraps the native transcript: the header
|
|
@@ -278,10 +339,17 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
278
339
|
] }),
|
|
279
340
|
};
|
|
280
341
|
const replacement = () => {
|
|
281
|
-
if (!prepare(tui.terminal.columns, root))
|
|
342
|
+
if (!prepare(tui.terminal.columns, root)) {
|
|
343
|
+
if (failed || stopped || host.mode !== "fullscreen") return original.call(root);
|
|
344
|
+
if (!headerLines.length) return nativeLayout();
|
|
345
|
+
return { type: "vstack", gap: 0, align: "stretch", entries: [
|
|
346
|
+
{ component: header, basis: "auto", grow: 0, shrink: 0, minSize: 1 },
|
|
347
|
+
{ component: nativeHost, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
|
348
|
+
] };
|
|
349
|
+
}
|
|
282
350
|
const current = prepared!;
|
|
283
351
|
if (current.presentation?.scrollTop === scroll.scrollTop) return current.presentation.output;
|
|
284
|
-
const output: LayoutNode = current.headerActive
|
|
352
|
+
const output: LayoutNode = current.headerActive && headerPlacement() !== "below-input"
|
|
285
353
|
? { type: "vstack", gap: 0, align: "stretch", entries: [
|
|
286
354
|
{ component: header, basis: "auto", grow: 0, shrink: 0, minSize: 1 },
|
|
287
355
|
{ component: hstackHost, basis: 0, grow: 1, shrink: 1, minSize: 1 },
|
|
@@ -311,7 +379,7 @@ export function installSidebar(tui: TUI, theme: ShellBarTheme): () => void {
|
|
|
311
379
|
stopped = true;
|
|
312
380
|
state.active = false;
|
|
313
381
|
clearInterval(timer);
|
|
314
|
-
scroll.
|
|
382
|
+
scroll.setScrollbar("hidden");
|
|
315
383
|
for (const cleanup of cleanups.reverse()) cleanup();
|
|
316
384
|
tui.requestRender();
|
|
317
385
|
};
|
package/lib/shell-sidebar.ts
CHANGED
|
@@ -3,9 +3,23 @@ import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
|
3
3
|
// Store on the terminal, not a module singleton: extension loaders may isolate
|
|
4
4
|
// modules, while Pi keeps this terminal across regular/fullscreen transitions.
|
|
5
5
|
const STATE = Symbol.for("gentle-pi.experimental-sidebar.state");
|
|
6
|
+
/** In-process notification after a visual preference is persisted. */
|
|
7
|
+
export const VISUAL_SETTINGS_CHANGED = "gentle-pi.visual-settings-changed";
|
|
6
8
|
export interface SidebarState {
|
|
7
9
|
active: boolean;
|
|
10
|
+
visibility?: { todo?: boolean };
|
|
8
11
|
ownsHost?: () => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Columns the fullscreen rail reserves at the right edge (rail plus gap)
|
|
14
|
+
* while `active && ownsHost()`. Overlays read it to stay inside the editor
|
|
15
|
+
* column (the prompt-history picker's right margin) without importing
|
|
16
|
+
* gentle-shell.
|
|
17
|
+
*/
|
|
18
|
+
railColumns?: number;
|
|
19
|
+
/** True while Status placement is "hidden": the bottom Status bar paints nothing at any width or mode. */
|
|
20
|
+
statusHidden?: () => boolean;
|
|
21
|
+
/** True while a painting top header is the only status row of a narrow fullscreen terminal: the bottom Status bar steps aside. */
|
|
22
|
+
headerOwnsStatus?: () => boolean;
|
|
9
23
|
parts: Map<string, SidebarRail>;
|
|
10
24
|
}
|
|
11
25
|
|
|
@@ -32,7 +46,7 @@ export function sidebarPart<T extends Component & { dispose?(): void }>(tui: TUI
|
|
|
32
46
|
state.parts.set(key, rail);
|
|
33
47
|
return {
|
|
34
48
|
...bottom,
|
|
35
|
-
render: (width: number) => state.active && state.ownsHost?.() ? [] : bottom.render(width),
|
|
49
|
+
render: (width: number) => (key === "todo" && state.visibility?.todo === false) || (key === "footer" && (state.statusHidden?.() || state.headerOwnsStatus?.())) || (state.active && state.ownsHost?.()) ? [] : bottom.render(width),
|
|
36
50
|
dispose() {
|
|
37
51
|
if (state.parts.get(key) === rail) state.parts.delete(key);
|
|
38
52
|
bottom.dispose?.();
|