gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { USER_PI_HOME_ENV } from "../lib/gentle-shell-launcher.ts";
|
|
5
|
+
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
6
|
+
import { withOverlayRepaint } from "../lib/overlay-repaint.ts";
|
|
7
|
+
import { createStatsLoader, currentSessionStats, type StatsLoader } from "../lib/stats-collector.ts";
|
|
8
|
+
import { StatsView } from "../lib/stats-view.ts";
|
|
9
|
+
|
|
10
|
+
// Gentle Stats: /gentle:stats opens a full-terminal panel over local Pi
|
|
11
|
+
// session history (tokens, cost, models, activity). It only reads the
|
|
12
|
+
// session files Pi already writes; nothing new is persisted.
|
|
13
|
+
//
|
|
14
|
+
// History spans two homes: the active one (Gentle Shell's isolated home) and
|
|
15
|
+
// the user's regular Pi home, which the launcher records before isolating.
|
|
16
|
+
// Launched without it (plain pi, or an older launcher), the conventional
|
|
17
|
+
// ~/.pi/agent stands in. The loader reads an alias of the same root once and
|
|
18
|
+
// counts a session copied into both homes once.
|
|
19
|
+
|
|
20
|
+
export const STATS_COMMAND_NAME = "gentle:stats";
|
|
21
|
+
|
|
22
|
+
/** Optional shortcut; there is no default key because the common alt+ keys are taken. */
|
|
23
|
+
export function statsViewKey(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
24
|
+
const value = env.GENTLE_PI_STATS_VIEW_KEY?.trim();
|
|
25
|
+
return !value || value.toLowerCase() === "off" ? undefined : value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The sessions roots /gentle:stats reads: the active home and the user's original Pi home. */
|
|
29
|
+
export function statsSessionRoots(agentDir: string, env: NodeJS.ProcessEnv = process.env, home: string = homedir()): string[] {
|
|
30
|
+
const userHome = env[USER_PI_HOME_ENV] || join(home, ".pi", "agent");
|
|
31
|
+
return [join(agentDir, "sessions"), join(userHome, "sessions")];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GentleStatsDeps {
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
sessionsRoots?: () => readonly string[];
|
|
38
|
+
loader?: StatsLoader;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function gentleStats(pi: ExtensionAPI, deps: GentleStatsDeps = {}): void {
|
|
42
|
+
const env = deps.env ?? process.env;
|
|
43
|
+
const now = deps.now ?? (() => Date.now());
|
|
44
|
+
const sessionsRoots = deps.sessionsRoots ?? (() => statsSessionRoots(getAgentDir(), env));
|
|
45
|
+
// One loader per process keeps its per-file cache across openings.
|
|
46
|
+
const loader = deps.loader ?? createStatsLoader();
|
|
47
|
+
const overlays = new Set<StatsView>();
|
|
48
|
+
|
|
49
|
+
const openOverlay = async (ctx: ExtensionContext) => {
|
|
50
|
+
if (!ctx.hasUI) return;
|
|
51
|
+
if (ctx.mode !== "tui") {
|
|
52
|
+
ctx.ui.notify("The stats overlay requires TUI mode.", "warning");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let view: StatsView | undefined;
|
|
56
|
+
await ctx.ui.custom<null>(
|
|
57
|
+
(tui, theme, _keybindings, done) => {
|
|
58
|
+
const close = withOverlayRepaint(tui, done);
|
|
59
|
+
view = new StatsView({
|
|
60
|
+
theme,
|
|
61
|
+
rows: () => Math.max(0, tui.terminal.rows),
|
|
62
|
+
cwd: ctx.sessionManager.getCwd(),
|
|
63
|
+
now,
|
|
64
|
+
load: () => loader.load(sessionsRoots()),
|
|
65
|
+
current: () => currentSessionStats(ctx.sessionManager.getEntries(), { sessionId: ctx.sessionManager.getSessionId() ?? "", now: now() }),
|
|
66
|
+
onClose: () => close(null),
|
|
67
|
+
requestRender: () => tui.requestRender(),
|
|
68
|
+
});
|
|
69
|
+
overlays.add(view);
|
|
70
|
+
// Open at once with a loading state; the scan repaints when it settles.
|
|
71
|
+
void view.load();
|
|
72
|
+
const interaction = createNativeFullscreenInteraction({
|
|
73
|
+
keyboardTarget: view,
|
|
74
|
+
requestRender: () => tui.requestRender(),
|
|
75
|
+
});
|
|
76
|
+
interaction.addChild(view);
|
|
77
|
+
return interaction;
|
|
78
|
+
},
|
|
79
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 0, anchor: "center" } },
|
|
80
|
+
).finally(() => {
|
|
81
|
+
view?.dispose();
|
|
82
|
+
if (view) overlays.delete(view);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
pi.registerCommand(STATS_COMMAND_NAME, {
|
|
87
|
+
description: "Show local Pi usage stats: activity heatmap, tokens, cost, models, and this session. r cycles the range, s the project scope.",
|
|
88
|
+
handler: async (_args, ctx) => openOverlay(ctx),
|
|
89
|
+
});
|
|
90
|
+
const viewKey = statsViewKey(env);
|
|
91
|
+
if (viewKey) {
|
|
92
|
+
pi.registerShortcut(viewKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
|
|
93
|
+
description: "Show local Pi usage stats",
|
|
94
|
+
handler: async (ctx) => openOverlay(ctx),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
pi.on("session_shutdown", () => {
|
|
98
|
+
for (const view of overlays) { view.handleInput("q"); view.dispose(); }
|
|
99
|
+
overlays.clear();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
2
|
+
import { ScrollView, Text, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { appendSystemPromptOnce } from "../lib/append-system-prompt.ts";
|
|
3
4
|
import { NativePointerRegion } from "../lib/native-pointer-region.ts";
|
|
4
|
-
import {
|
|
5
|
+
import { panelHeaderRow } from "../lib/shell-card.ts";
|
|
6
|
+
import { sidebarPart, sidebarState } from "../lib/shell-sidebar.ts";
|
|
5
7
|
import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
6
8
|
import {
|
|
7
9
|
applyTodo,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
TODO_DETAILS_KEY,
|
|
13
15
|
TODO_GLYPH,
|
|
14
16
|
TODO_TOOL_NAME,
|
|
17
|
+
todoCardTone,
|
|
15
18
|
todoPromptBlock,
|
|
16
19
|
todoSummary,
|
|
17
20
|
type TodoParams,
|
|
@@ -21,12 +24,16 @@ import {
|
|
|
21
24
|
// Gentle Todo: the task list the model keeps while it works, drawn as a
|
|
22
25
|
// Gentle Shell card above the editor. Three things keep it current that a
|
|
23
26
|
// static tool description cannot: `write` replaces the whole list in one
|
|
24
|
-
// call,
|
|
27
|
+
// call, each run's system prompt carries the open tasks and the rules, and
|
|
25
28
|
// a list that goes untouched while tasks stay open is marked stale for both
|
|
26
|
-
// the human and the model.
|
|
29
|
+
// the human and the model. Long runs also receive one request-local reminder
|
|
30
|
+
// after several tool-use turns, without waking the agent or changing tasks.
|
|
27
31
|
|
|
28
32
|
const WIDGET_KEY = "gentle-todo";
|
|
33
|
+
const STATUS_ENUM = ["pending", "in_progress", "blocked", "done", "dropped"];
|
|
29
34
|
const COLLAPSE_KEY_DEFAULT = "ctrl+shift+t";
|
|
35
|
+
const REMIND_AFTER_TOOL_TURNS = 4;
|
|
36
|
+
const TODO_REMINDER = "The todo plan has not been reviewed during several tool-use turns. Check whether it still matches the work performed and update tasks where needed. Do not mark work done without verification.";
|
|
30
37
|
const TOOL_PARAMETERS = {
|
|
31
38
|
type: "object",
|
|
32
39
|
additionalProperties: false,
|
|
@@ -43,15 +50,15 @@ const TOOL_PARAMETERS = {
|
|
|
43
50
|
properties: {
|
|
44
51
|
id: { type: "integer", description: "Existing task id to keep." },
|
|
45
52
|
title: { type: "string", description: "Short imperative title, e.g. 'Write the parser'." },
|
|
46
|
-
status: { type: "string", enum:
|
|
47
|
-
note: { type: "string", description: "What is happening right now, shown while in_progress, e.g. 'writing tests'." },
|
|
53
|
+
status: { type: "string", enum: STATUS_ENUM, description: "Defaults to pending. blocked: open but waiting on something outside the list; dropped: will not be done, on purpose." },
|
|
54
|
+
note: { type: "string", description: "What is happening right now, shown while in_progress, e.g. 'writing tests'; required for blocked, naming what it waits for, e.g. 'waiting for an admin'." },
|
|
48
55
|
},
|
|
49
56
|
},
|
|
50
57
|
},
|
|
51
58
|
id: { type: "integer", description: "Task id for update." },
|
|
52
59
|
title: { type: "string", description: "Title for add, or a new title for update." },
|
|
53
|
-
status: { type: "string", enum:
|
|
54
|
-
note: { type: "string", description: "Note for add or update." },
|
|
60
|
+
status: { type: "string", enum: STATUS_ENUM, description: "Status for add or update." },
|
|
61
|
+
note: { type: "string", description: "Note for add or update; required for blocked." },
|
|
55
62
|
},
|
|
56
63
|
} as const;
|
|
57
64
|
|
|
@@ -70,12 +77,18 @@ export function todoCollapseKey(env: NodeJS.ProcessEnv = process.env): string |
|
|
|
70
77
|
interface TodoSession {
|
|
71
78
|
state: TodoState;
|
|
72
79
|
turn: number;
|
|
80
|
+
/** Separate from the existing run-based card freshness. */
|
|
81
|
+
toolTurnsSinceWrite: number;
|
|
82
|
+
reminded: boolean;
|
|
83
|
+
wroteThisTurn: boolean;
|
|
73
84
|
collapsed: boolean;
|
|
74
85
|
/** A finished list stays on screen for the turn it finished in, then clears. */
|
|
75
86
|
clearOnNextTurn: boolean;
|
|
76
87
|
ui: ExtensionContext["ui"] | undefined;
|
|
77
88
|
host: { requestRender(): void } | undefined;
|
|
78
89
|
tui: TUI | undefined;
|
|
90
|
+
/** Resolve the visible host when the key is pressed, not when it renders. */
|
|
91
|
+
scrollTodo?: (lines: number) => void;
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
function sessionKey(ctx: ExtensionContext): string {
|
|
@@ -91,7 +104,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
91
104
|
const key = sessionKey(ctx);
|
|
92
105
|
let current = sessions.get(key);
|
|
93
106
|
if (!current) {
|
|
94
|
-
current = { state: emptyTodo(), turn: 0, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
|
|
107
|
+
current = { state: emptyTodo(), turn: 0, toolTurnsSinceWrite: 0, reminded: false, wroteThisTurn: false, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
|
|
95
108
|
sessions.set(key, current);
|
|
96
109
|
}
|
|
97
110
|
return current;
|
|
@@ -103,18 +116,51 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
103
116
|
current.host?.requestRender();
|
|
104
117
|
};
|
|
105
118
|
|
|
106
|
-
const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1],
|
|
119
|
+
const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1], spacer: boolean): Component & { dispose(): void; digest(): string; scrollBy(lines: number): void } => {
|
|
107
120
|
let hovered = false;
|
|
121
|
+
let body: string[] = [];
|
|
122
|
+
let collapsed = current.collapsed;
|
|
123
|
+
// Widgets are measured as leaves, so native layout cannot assign this
|
|
124
|
+
// body a viewport. Use native scroll state and explicitly slice its lines
|
|
125
|
+
// while keeping the collapse header and card footer outside the viewport.
|
|
126
|
+
const scroll = new ScrollView({ render: () => body, invalidate() {} }, { follow: "none", overscroll: "contain" });
|
|
127
|
+
const scrollBy = (lines: number) => {
|
|
128
|
+
if (current.collapsed) return;
|
|
129
|
+
scroll.scrollBy(lines);
|
|
130
|
+
if (current.tui) invalidateSidebar(current.tui);
|
|
131
|
+
current.host?.requestRender();
|
|
132
|
+
};
|
|
133
|
+
// The row the rendered card draws its header on: 0 for the outlined
|
|
134
|
+
// frame, 1 below the float panel's top padding row.
|
|
135
|
+
let headerRow = 0;
|
|
108
136
|
const card: Component = {
|
|
109
137
|
render(width: number) {
|
|
138
|
+
const stale = staleTurns(current.state, current.turn);
|
|
139
|
+
headerRow = panelHeaderRow(theme, width, todoCardTone(stale));
|
|
110
140
|
const lines = renderTodoCard(current.state, theme, width, {
|
|
111
141
|
collapsed: current.collapsed,
|
|
112
|
-
staleTurns:
|
|
142
|
+
staleTurns: stale,
|
|
113
143
|
collapseKey,
|
|
114
144
|
hovered,
|
|
115
|
-
|
|
145
|
+
scrollable: true,
|
|
116
146
|
});
|
|
117
|
-
|
|
147
|
+
if (collapsed !== current.collapsed) {
|
|
148
|
+
collapsed = current.collapsed;
|
|
149
|
+
scroll.scrollToStart();
|
|
150
|
+
}
|
|
151
|
+
// At most one third of the screen, capped at 16 rows. Read height
|
|
152
|
+
// on every render so a mobile resize immediately releases chat space.
|
|
153
|
+
const terminalRows = current.tui?.terminal?.rows ?? 48;
|
|
154
|
+
const height = Math.min(16, Math.floor(terminalRows / 3));
|
|
155
|
+
const bodyStart = headerRow === 1 ? 3 : 1;
|
|
156
|
+
body = lines.slice(bodyStart, -1);
|
|
157
|
+
const overflow = !current.collapsed && lines.length + Number(spacer) > height;
|
|
158
|
+
const room = Math.max(1, height - bodyStart - 1 - Number(spacer) - Number(overflow));
|
|
159
|
+
scroll.updateLayout(body.length, current.collapsed ? body.length : room, () => current.host?.requestRender());
|
|
160
|
+
const visible = scroll.render(width).slice(scroll.scrollTop, scroll.scrollTop + scroll.viewportHeight);
|
|
161
|
+
const output = [...lines.slice(0, bodyStart), ...visible, ...lines.slice(-1)];
|
|
162
|
+
if (overflow) output.push(theme.fg("muted", truncateToWidth(`↑↓ ${scroll.scrollTop + 1}–${scroll.scrollTop + visible.length}/${body.length} · ctrl+shift+↑/↓`, width)));
|
|
163
|
+
return spacer && output.length > 0 ? [...output, ""] : output;
|
|
118
164
|
},
|
|
119
165
|
invalidate() {
|
|
120
166
|
hovered = false;
|
|
@@ -122,10 +168,10 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
122
168
|
};
|
|
123
169
|
const region = new NativePointerRegion(card, {
|
|
124
170
|
onHover(event) {
|
|
125
|
-
// The region spans the whole card, but only the header row
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
const next = event.y ===
|
|
171
|
+
// The region spans the whole card, but only the header row is the
|
|
172
|
+
// clickable control, so a move elsewhere in the card clears hover
|
|
173
|
+
// exactly like leaving the region entirely would.
|
|
174
|
+
const next = event.y === headerRow;
|
|
129
175
|
if (next === hovered) return { handled: true };
|
|
130
176
|
hovered = next;
|
|
131
177
|
return { handled: true, render: true };
|
|
@@ -136,12 +182,21 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
136
182
|
current.host?.requestRender();
|
|
137
183
|
},
|
|
138
184
|
onClick(event) {
|
|
139
|
-
if (event.button !== "left" || event.y !==
|
|
185
|
+
if (event.button !== "left" || event.y !== headerRow) return undefined;
|
|
140
186
|
toggle(current);
|
|
141
187
|
return { handled: true, render: true };
|
|
142
188
|
},
|
|
189
|
+
onWheel(event) {
|
|
190
|
+
if (current.collapsed || body.length <= scroll.viewportHeight) return undefined;
|
|
191
|
+
scrollBy(event.wheelDelta ?? 0);
|
|
192
|
+
return { handled: true, render: true };
|
|
193
|
+
},
|
|
143
194
|
});
|
|
144
195
|
return {
|
|
196
|
+
scrollBy,
|
|
197
|
+
// Sidebar caches must observe height-only resizes and local viewport
|
|
198
|
+
// changes, even when terminal width and task content stay the same.
|
|
199
|
+
digest: () => JSON.stringify([current.tui?.terminal?.rows, current.collapsed, current.turn, hovered, scroll.scrollTop]),
|
|
145
200
|
render: (width) => region.render(width),
|
|
146
201
|
handleMouse: (event) => region.handleMouse(event),
|
|
147
202
|
invalidate: () => region.invalidate(),
|
|
@@ -153,6 +208,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
153
208
|
if (current.tui) invalidateSidebar(current.tui);
|
|
154
209
|
if (!current.ui) return;
|
|
155
210
|
if (current.state.tasks.length === 0) {
|
|
211
|
+
current.scrollTodo = undefined;
|
|
156
212
|
current.ui.setWidget(WIDGET_KEY, undefined);
|
|
157
213
|
return;
|
|
158
214
|
}
|
|
@@ -160,7 +216,15 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
160
216
|
current.ui.setWidget(WIDGET_KEY, (tui, theme) => {
|
|
161
217
|
snapshot.host = tui;
|
|
162
218
|
snapshot.tui = tui;
|
|
163
|
-
|
|
219
|
+
const bottom = todoCard(snapshot, theme, true);
|
|
220
|
+
const rail = todoCard(snapshot, theme, false);
|
|
221
|
+
snapshot.scrollTodo = (lines) => {
|
|
222
|
+
// Cached rail lines do not re-render when a narrow→wide resize
|
|
223
|
+
// restores the sidebar. Select ownership from the live layout.
|
|
224
|
+
const state = tui.terminal ? sidebarState(tui) : undefined;
|
|
225
|
+
(state?.active && state.ownsHost?.() ? rail : bottom).scrollBy(lines);
|
|
226
|
+
};
|
|
227
|
+
return sidebarPart(tui, "todo", bottom, rail);
|
|
164
228
|
});
|
|
165
229
|
};
|
|
166
230
|
|
|
@@ -175,6 +239,8 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
175
239
|
"Mark a task in_progress before starting it and done right after finishing it; keep exactly one task in_progress.",
|
|
176
240
|
"Prefer write with the complete list whenever the plan changes; keep ids of tasks that already exist.",
|
|
177
241
|
"Never mark a task done while tests fail or the work is partial; add a task for the blocker instead.",
|
|
242
|
+
"Mark a task blocked with a note naming what it waits for only when that is outside the list (a person, another team, an authorization); a prerequisite in the list is ordering, so keep the task pending. Return it to pending once the condition holds.",
|
|
243
|
+
"When the plan changes, mark tasks it made obsolete dropped, never done; dropped tasks no longer count as open.",
|
|
178
244
|
],
|
|
179
245
|
parameters: TOOL_PARAMETERS,
|
|
180
246
|
executionMode: "sequential",
|
|
@@ -192,6 +258,11 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
192
258
|
if (!result.error) {
|
|
193
259
|
current.state = result.state;
|
|
194
260
|
current.clearOnNextTurn = false;
|
|
261
|
+
if (params.action !== "list") {
|
|
262
|
+
current.toolTurnsSinceWrite = 0;
|
|
263
|
+
current.reminded = false;
|
|
264
|
+
current.wroteThisTurn = true;
|
|
265
|
+
}
|
|
195
266
|
if (current.tui) invalidateSidebar(current.tui);
|
|
196
267
|
}
|
|
197
268
|
return {
|
|
@@ -210,6 +281,13 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
210
281
|
});
|
|
211
282
|
}
|
|
212
283
|
|
|
284
|
+
for (const [key, lines] of [["ctrl+shift+up", -3], ["ctrl+shift+down", 3]] as const) {
|
|
285
|
+
pi.registerShortcut(key, {
|
|
286
|
+
description: `Scroll the Todo list ${lines < 0 ? "up" : "down"}`,
|
|
287
|
+
handler: async (ctx) => session(ctx).scrollTodo?.(lines),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
213
291
|
pi.on("session_start", (_event, ctx) => {
|
|
214
292
|
const current = session(ctx);
|
|
215
293
|
// A list that was already finished when the session was left is history,
|
|
@@ -217,6 +295,9 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
217
295
|
const replayed = replayTodo(ctx.sessionManager.getBranch());
|
|
218
296
|
current.state = replayed.tasks.length > 0 && todoSummary(replayed).open === 0 ? { ...replayed, tasks: [] } : replayed;
|
|
219
297
|
current.turn = ctx.sessionManager.getBranch().filter((entry) => (entry as { type?: string }).type === "message" && (entry as { message?: { role?: string } }).message?.role === "user").length;
|
|
298
|
+
current.toolTurnsSinceWrite = 0;
|
|
299
|
+
current.reminded = false;
|
|
300
|
+
current.wroteThisTurn = false;
|
|
220
301
|
current.ui = ctx.hasUI ? ctx.ui : undefined;
|
|
221
302
|
show(current);
|
|
222
303
|
});
|
|
@@ -236,7 +317,32 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
|
|
|
236
317
|
}
|
|
237
318
|
const block = todoPromptBlock(current.state, staleTurns(current.state, current.turn));
|
|
238
319
|
if (!block) return undefined;
|
|
239
|
-
|
|
320
|
+
// gentle-shell#1485: pi-claude-bridge drops a handler-returned
|
|
321
|
+
// systemPrompt, so the open-tasks block goes through appendSystemPrompt.
|
|
322
|
+
appendSystemPromptOnce(event.systemPromptOptions, block);
|
|
323
|
+
return undefined;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
pi.on("turn_end", (event, ctx) => {
|
|
327
|
+
const current = session(ctx);
|
|
328
|
+
// A turn containing a valid todo write is already reconciled. Failed
|
|
329
|
+
// writes and list reads deliberately do not refresh this counter.
|
|
330
|
+
if (!current.wroteThisTurn && event.message.role === "assistant" && event.message.content.some((part) => part.type === "toolCall")) {
|
|
331
|
+
current.toolTurnsSinceWrite += 1;
|
|
332
|
+
}
|
|
333
|
+
current.wroteThisTurn = false;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
pi.on("context", (event, ctx) => {
|
|
337
|
+
const current = session(ctx);
|
|
338
|
+
if (current.reminded || current.toolTurnsSinceWrite < REMIND_AFTER_TOOL_TURNS) return undefined;
|
|
339
|
+
if (!current.state.tasks.some((task) => task.status === "pending" || task.status === "in_progress")) return undefined;
|
|
340
|
+
current.reminded = true;
|
|
341
|
+
// Context transforms are request-local: no transcript entry, UI
|
|
342
|
+
// notification, continuation or provider-specific wake is needed.
|
|
343
|
+
return {
|
|
344
|
+
messages: [...event.messages, { role: "custom" as const, customType: "gentle-todo-reminder", content: TODO_REMINDER, display: false, timestamp: Date.now() }],
|
|
345
|
+
};
|
|
240
346
|
});
|
|
241
347
|
|
|
242
348
|
pi.on("tool_execution_end", (event, ctx) => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 ExoPro. Inspired by @jasonish/pi-prompt-history
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared atomic JSON writer (design §D3): serialize to a `.tmp` file in the
|
|
9
|
+
* SAME directory as the target, then renameSync it into place — a same-dir
|
|
10
|
+
* rename is atomic on POSIX/APFS, so readers see the old or the new file,
|
|
11
|
+
* never a partial write. Any error returns false and never throws.
|
|
12
|
+
*
|
|
13
|
+
* No fsync: both consumers treat lost writes as derived cache (a lost index
|
|
14
|
+
* rebuilds on the next open; a lost tombstone resurfaces a prompt the user
|
|
15
|
+
* can re-delete), so the per-write fsync cost is not justified — the crash
|
|
16
|
+
* window is documented, not fixed. The staging name is unique per write
|
|
17
|
+
* (`.tmp-<pid>-<ts>`, the same convention as the store.ts writers): the
|
|
18
|
+
* state dir is shared across concurrent pi instances, so a fixed
|
|
19
|
+
* `${filePath}.tmp` would let two writers clobber the same staging file
|
|
20
|
+
* (torn target JSON, spurious rename failures). A failed write unlinks its
|
|
21
|
+
* staging file, so orphaned `.tmp` files do not accumulate.
|
|
22
|
+
*/
|
|
23
|
+
export function writeJsonAtomic(filePath: string, value: unknown): boolean {
|
|
24
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
25
|
+
try {
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(tmpPath, JSON.stringify(value), "utf8");
|
|
28
|
+
fs.renameSync(tmpPath, filePath);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
try {
|
|
32
|
+
fs.unlinkSync(tmpPath);
|
|
33
|
+
} catch {
|
|
34
|
+
// staging file never created or already renamed
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 ExoPro. Inspired by @jasonish/pi-prompt-history
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { writeJsonAtomic } from "./atomic-write.ts";
|
|
8
|
+
import { promptDedupKey } from "./selector-helpers.ts";
|
|
9
|
+
|
|
10
|
+
/** Name of the tombstone file inside the injected state dir (spec C4). */
|
|
11
|
+
const HIDE_FILE_NAME = "hidden.json";
|
|
12
|
+
|
|
13
|
+
/** Marks tombstone entries written in the exact (hashed) key format. */
|
|
14
|
+
const TOMBSTONE_KEY_PREFIX = "sha256:";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* UI-level prompt identity: whitespace-collapsed, trimmed, case-insensitive,
|
|
18
|
+
* never truncated. The store's scope deletes sweep by this same identity,
|
|
19
|
+
* so a tombstone hides exactly the copies a delete removes.
|
|
20
|
+
*/
|
|
21
|
+
export function promptIdentity(text: string): string {
|
|
22
|
+
return text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tombstone key for `text`: a SHA-256 of its full prompt identity. Exact —
|
|
27
|
+
* two prompts that merely share a prefix get different keys — and hashed,
|
|
28
|
+
* so hidden.json never holds the text of a deleted prompt.
|
|
29
|
+
*/
|
|
30
|
+
export function tombstoneKey(text: string): string {
|
|
31
|
+
return (
|
|
32
|
+
TOMBSTONE_KEY_PREFIX +
|
|
33
|
+
createHash("sha256").update(promptIdentity(text)).digest("hex")
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether `text` is hidden by the tombstone set `keys`. Hashed keys match
|
|
39
|
+
* the exact prompt identity. Plaintext entries from the earlier prefix
|
|
40
|
+
* format (`promptDedupKey`: the first 120 normalized characters) stay
|
|
41
|
+
* honored as written, so upgrading never resurfaces a hidden prompt; only
|
|
42
|
+
* new deletions use the exact format.
|
|
43
|
+
*/
|
|
44
|
+
export function isPromptHidden(keys: ReadonlySet<string>, text: string): boolean {
|
|
45
|
+
if (keys.size === 0) return false;
|
|
46
|
+
return keys.has(tombstoneKey(text)) || keys.has(promptDedupKey(text));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retention cap for hidden.json (slice-05 D5): the tombstone file is a
|
|
51
|
+
* rebuildable derived cache, not a retention guarantee, so it holds at
|
|
52
|
+
* most this many keys in recency order; hiding past the cap drops the
|
|
53
|
+
* OLDEST keys from the front.
|
|
54
|
+
*/
|
|
55
|
+
export const HIDE_FILE_MAX_ENTRIES = 1000;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Shared recovery warning for a file that exists but cannot be trusted
|
|
59
|
+
* (spec C4, fail-closed READ half): toast-suitable, names hidden.json, and
|
|
60
|
+
* gives the user the explicit restore-or-delete choice.
|
|
61
|
+
*/
|
|
62
|
+
const RECOVERY_MESSAGE =
|
|
63
|
+
"The prompt-history hide list (hidden.json) is corrupt or unreadable. History is blocked until you restore the file or delete it (hidden prompts may then reappear).";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Result of one tombstone write (spec C4): `written` on a successful atomic
|
|
67
|
+
* write, or an error object carrying a short, toast-suitable reason. Never
|
|
68
|
+
* throws.
|
|
69
|
+
*/
|
|
70
|
+
export type HideResult =
|
|
71
|
+
| { status: "written" }
|
|
72
|
+
| { status: "error"; message: string };
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Result of one tombstone read (spec C4): `trusted` keys when the file is
|
|
76
|
+
* missing or holds a valid array, or `untrusted` when the file exists but
|
|
77
|
+
* cannot be trusted. History reads FAIL CLOSED on `untrusted`: callers must
|
|
78
|
+
* block the drain instead of emptying the tombstone set, because hidden
|
|
79
|
+
* prompts may contain secrets an empty set would resurface.
|
|
80
|
+
*/
|
|
81
|
+
export type HiddenRead =
|
|
82
|
+
| { status: "trusted"; keys: Set<string> }
|
|
83
|
+
| {
|
|
84
|
+
status: "untrusted";
|
|
85
|
+
reason: "unreadable" | "corrupt" | "malformed";
|
|
86
|
+
message: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read the tombstone key set from `stateDir/hidden.json` — the READ half of
|
|
91
|
+
* the hide-file contract (spec C4). Fail-closed for history: a file that
|
|
92
|
+
* exists but is unreadable, corrupt, or wrong-shaped returns `untrusted`
|
|
93
|
+
* with the recovery warning so callers block the drain; it never degrades
|
|
94
|
+
* to an empty trusted set. A MISSING file — before any deletion — is the
|
|
95
|
+
* safe empty case and reads `trusted` with no keys. A valid array is
|
|
96
|
+
* trusted; junk items inside it are ignored, never trusted. Keys are the
|
|
97
|
+
* `tombstoneKey` hashes written by `hidePrompt`, or plaintext entries from
|
|
98
|
+
* the earlier prefix format (see `isPromptHidden`); the call never throws.
|
|
99
|
+
* A valid array's stored order is preserved (the recency order — oldest
|
|
100
|
+
* first — that `hidePrompt` maintains and caps).
|
|
101
|
+
*/
|
|
102
|
+
export function readHiddenPrompts(stateDir: string): HiddenRead {
|
|
103
|
+
let raw: string;
|
|
104
|
+
try {
|
|
105
|
+
raw = fs.readFileSync(path.join(stateDir, HIDE_FILE_NAME), "utf8");
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const code = (error as { code?: unknown } | null | undefined)?.code;
|
|
108
|
+
if (code === "ENOENT") {
|
|
109
|
+
// Missing before any deletion: the safe empty tombstone set.
|
|
110
|
+
return { status: "trusted", keys: new Set<string>() };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
status: "untrusted",
|
|
114
|
+
reason: "unreadable",
|
|
115
|
+
message: RECOVERY_MESSAGE,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
let parsed: unknown;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(raw);
|
|
121
|
+
} catch {
|
|
122
|
+
return { status: "untrusted", reason: "corrupt", message: RECOVERY_MESSAGE };
|
|
123
|
+
}
|
|
124
|
+
const keys = new Set<string>();
|
|
125
|
+
if (!Array.isArray(parsed)) {
|
|
126
|
+
return {
|
|
127
|
+
status: "untrusted",
|
|
128
|
+
reason: "malformed",
|
|
129
|
+
message: RECOVERY_MESSAGE,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
for (const item of parsed) {
|
|
133
|
+
if (typeof item === "string" && item !== "") keys.add(item);
|
|
134
|
+
}
|
|
135
|
+
return { status: "trusted", keys };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Write the tombstone key for `text` into `stateDir/hidden.json` — the
|
|
140
|
+
* WRITE half of the hide-file contract (spec C4). The key is the shared
|
|
141
|
+
* `tombstoneKey` (byte-match normative with the drain and seed filters via
|
|
142
|
+
* `isPromptHidden` — never a re-implementation). The file array is RECENCY-ordered — oldest key
|
|
143
|
+
* first, newest key appended last — and re-hiding an existing key
|
|
144
|
+
* refreshes it to the end (delete + add, since Set.add on a present
|
|
145
|
+
* member keeps its old position). The file is capped at
|
|
146
|
+
* `HIDE_FILE_MAX_ENTRIES` (1000): after the append, keys drop from the
|
|
147
|
+
* FRONT until the file fits, so hidden.json stays a bounded cache — a
|
|
148
|
+
* dropped (oldest) prompt may reappear in the list and can be deleted
|
|
149
|
+
* again. Keys persist in that insertion order — NO sort — via the shared
|
|
150
|
+
* atomic tmp+rename writer. An untrusted existing file is never silently
|
|
151
|
+
* reset (a clean rewrite would clear the blocked state one hide later):
|
|
152
|
+
* hidePrompt refuses with the recovery warning until the user restores or
|
|
153
|
+
* deletes the file. A missing file is the clean baseline; any write
|
|
154
|
+
* failure returns an error object for the delete-flow toast; the call
|
|
155
|
+
* never throws.
|
|
156
|
+
*/
|
|
157
|
+
export function hidePrompt(stateDir: string, text: string): HideResult {
|
|
158
|
+
const read = readHiddenPrompts(stateDir);
|
|
159
|
+
if (read.status === "untrusted") {
|
|
160
|
+
// Refuse without writing: never reset the untrusted state silently.
|
|
161
|
+
return { status: "error", message: read.message };
|
|
162
|
+
}
|
|
163
|
+
// Recency order (slice-05 D5): the set iterates in stored file order
|
|
164
|
+
// (oldest first); delete+add refreshes a re-hidden key to the END.
|
|
165
|
+
const key = tombstoneKey(text);
|
|
166
|
+
read.keys.delete(key);
|
|
167
|
+
read.keys.add(key);
|
|
168
|
+
// Cap: drop the OLDEST keys from the front once over the limit.
|
|
169
|
+
const ordered = [...read.keys];
|
|
170
|
+
if (ordered.length > HIDE_FILE_MAX_ENTRIES) {
|
|
171
|
+
ordered.splice(0, ordered.length - HIDE_FILE_MAX_ENTRIES);
|
|
172
|
+
}
|
|
173
|
+
const written = writeJsonAtomic(
|
|
174
|
+
path.join(stateDir, HIDE_FILE_NAME),
|
|
175
|
+
ordered,
|
|
176
|
+
);
|
|
177
|
+
return written
|
|
178
|
+
? { status: "written" }
|
|
179
|
+
: {
|
|
180
|
+
status: "error",
|
|
181
|
+
message: "Could not write the hide file; the prompt may reappear.",
|
|
182
|
+
};
|
|
183
|
+
}
|