gentle-pi 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
export type StartupUpdateChoice = "update" | "skip" | "omit" | "cancel";
|
|
4
|
+
type TerminalInput = Readable & { isRaw?: boolean; setRawMode?: (raw: boolean) => unknown };
|
|
5
|
+
type TerminalOutput = Writable & { columns?: number; rows?: number; isTTY?: boolean };
|
|
6
|
+
type UpdateOffer = { current: string; latest: string; notesUrl: string };
|
|
7
|
+
type PromptOptions = { input?: TerminalInput; output?: TerminalOutput; color?: boolean };
|
|
8
|
+
|
|
9
|
+
const choices: StartupUpdateChoice[] = ["update", "skip", "omit"];
|
|
10
|
+
const labels = ["Update now (gentle-shell upgrade)", "Not now", "Skip this version"];
|
|
11
|
+
|
|
12
|
+
function sanitize(value: string, limit: number): string {
|
|
13
|
+
// Bound work before parsing, remove whole escape sequences (including OSC titles),
|
|
14
|
+
// then remove remaining control and bidi characters. Never emit an OSC hyperlink.
|
|
15
|
+
return value.slice(0, 2048)
|
|
16
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)/g, "")
|
|
17
|
+
.replace(/(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]/g, "")
|
|
18
|
+
.replace(/\x1b[ -/]*[@-~]/g, "")
|
|
19
|
+
.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g, "")
|
|
20
|
+
.slice(0, limit);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cells(char: string): number {
|
|
24
|
+
const code = char.codePointAt(0)!;
|
|
25
|
+
if (/\p{Mark}/u.test(char) || code === 0x200d || code === 0xfe0f) return 0;
|
|
26
|
+
return code >= 0x1100 && (
|
|
27
|
+
code <= 0x115f || code === 0x2329 || code === 0x232a ||
|
|
28
|
+
(code >= 0x2e80 && code <= 0xa4cf) || (code >= 0xac00 && code <= 0xd7a3) ||
|
|
29
|
+
(code >= 0xf900 && code <= 0xfaff) || (code >= 0xfe10 && code <= 0xfe6f) ||
|
|
30
|
+
(code >= 0xff00 && code <= 0xff60) || (code >= 0xffe0 && code <= 0xffe6) ||
|
|
31
|
+
code >= 0x1f000
|
|
32
|
+
) ? 2 : 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function wrap(text: string, width: number): string[] {
|
|
36
|
+
const lines: string[] = [];
|
|
37
|
+
let line = "";
|
|
38
|
+
let used = 0;
|
|
39
|
+
for (const original of text) {
|
|
40
|
+
const char = cells(original) > width ? "?" : original;
|
|
41
|
+
const size = cells(char);
|
|
42
|
+
if (used + size > width) {
|
|
43
|
+
lines.push(line);
|
|
44
|
+
line = "";
|
|
45
|
+
used = 0;
|
|
46
|
+
}
|
|
47
|
+
line += char;
|
|
48
|
+
used += size;
|
|
49
|
+
}
|
|
50
|
+
lines.push(line);
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A pre-Pi chooser: returns intent only; it never invokes upgrade or persists policy. */
|
|
55
|
+
export function promptStartupUpdate(offer: UpdateOffer, options: PromptOptions = {}): Promise<StartupUpdateChoice> {
|
|
56
|
+
const input = options.input ?? process.stdin;
|
|
57
|
+
const output = options.output ?? process.stdout;
|
|
58
|
+
const color = options.color ?? output.isTTY === true;
|
|
59
|
+
const rose = (text: string) => color ? `\x1b[38;2;255;118;195m${text}\x1b[39m` : text;
|
|
60
|
+
const current = sanitize(offer.current, 40);
|
|
61
|
+
const latest = sanitize(offer.latest, 40);
|
|
62
|
+
const notes = sanitize(offer.notesUrl, 160);
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const wasRaw = input.isRaw === true;
|
|
66
|
+
const wasFlowing = input.readableFlowing === true;
|
|
67
|
+
let rawAttempted = false;
|
|
68
|
+
let cursorHidden = false;
|
|
69
|
+
let selected = 1;
|
|
70
|
+
let done = false;
|
|
71
|
+
let failure: unknown;
|
|
72
|
+
let outcome: StartupUpdateChoice = "skip";
|
|
73
|
+
let pendingWrites = 0;
|
|
74
|
+
let settlementScheduled = false;
|
|
75
|
+
let escape = "";
|
|
76
|
+
let escapeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
77
|
+
let previousMenuLines = 0;
|
|
78
|
+
let previousWidth = 0;
|
|
79
|
+
|
|
80
|
+
function settle() {
|
|
81
|
+
if (!done || pendingWrites || settlementScheduled) return;
|
|
82
|
+
settlementScheduled = true;
|
|
83
|
+
// Writable callback errors can be followed by an error event. Keep our
|
|
84
|
+
// listener until that event is drained, including during cursor cleanup.
|
|
85
|
+
setImmediate(() => {
|
|
86
|
+
output.removeListener("error", onError);
|
|
87
|
+
if (failure) reject(failure);
|
|
88
|
+
else resolve(outcome);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function write(text: string) {
|
|
93
|
+
pendingWrites++;
|
|
94
|
+
try {
|
|
95
|
+
output.write(text, (error?: Error | null) => {
|
|
96
|
+
pendingWrites--;
|
|
97
|
+
if (error) onError(error);
|
|
98
|
+
settle();
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
pendingWrites--;
|
|
102
|
+
onError(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function finish(choice: StartupUpdateChoice, error?: unknown) {
|
|
107
|
+
if (error && !failure) failure = error;
|
|
108
|
+
if (done) return;
|
|
109
|
+
done = true;
|
|
110
|
+
outcome = choice;
|
|
111
|
+
clearTimeout(escapeTimer);
|
|
112
|
+
input.removeListener("data", onData);
|
|
113
|
+
input.removeListener("end", onEnd);
|
|
114
|
+
input.removeListener("close", onEnd);
|
|
115
|
+
input.removeListener("error", onError);
|
|
116
|
+
try {
|
|
117
|
+
if (rawAttempted) input.setRawMode?.(wasRaw);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
failure ??= error;
|
|
120
|
+
}
|
|
121
|
+
if (!wasFlowing) input.pause();
|
|
122
|
+
if (cursorHidden) write("\x1b[?25h");
|
|
123
|
+
settle();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function onError(error: unknown) {
|
|
127
|
+
if (!failure) failure = error;
|
|
128
|
+
finish("skip", error);
|
|
129
|
+
}
|
|
130
|
+
function onEnd() { finish("skip"); }
|
|
131
|
+
|
|
132
|
+
function terminalWidth(): number {
|
|
133
|
+
const width = output.columns;
|
|
134
|
+
return typeof width === "number" && Number.isFinite(width) ? Math.max(1, Math.min(76, Math.floor(width))) : 76;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function render(initial = false) {
|
|
138
|
+
const width = terminalWidth();
|
|
139
|
+
const bordered = width >= 6;
|
|
140
|
+
const inner = bordered ? width - 4 : width;
|
|
141
|
+
const row = (text: string) => wrap(text, inner).map(line => {
|
|
142
|
+
const used = [...line].reduce((sum, char) => sum + cells(char), 0);
|
|
143
|
+
return bordered ? `| ${line}${" ".repeat(Math.max(0, inner - used))} |` : line;
|
|
144
|
+
});
|
|
145
|
+
const border = bordered ? rose(`+${"-".repeat(width - 2)}+`) : "";
|
|
146
|
+
const menu = labels.flatMap((label, index) => row(`${index === selected ? ">" : " "} ${index + 1}. ${label}`));
|
|
147
|
+
if (bordered) menu.push(border);
|
|
148
|
+
let prefix = "";
|
|
149
|
+
if (initial) {
|
|
150
|
+
// Start on a fresh line so the existing shell prompt is never erased.
|
|
151
|
+
const header = [
|
|
152
|
+
...(bordered ? [border] : []),
|
|
153
|
+
...row("Gentle Shell update available").map(rose),
|
|
154
|
+
...row(`${current} -> ${latest}`),
|
|
155
|
+
...row("Release notes:"),
|
|
156
|
+
...row(notes),
|
|
157
|
+
...row("Arrows / 1-3: select; Enter: confirm"),
|
|
158
|
+
...row("Esc: not now; Ctrl+C: cancel"),
|
|
159
|
+
];
|
|
160
|
+
prefix = `\r\n${header.join("\r\n")}\r\n`;
|
|
161
|
+
} else {
|
|
162
|
+
const rows = output.rows ?? 24;
|
|
163
|
+
// On resize or a very short terminal, append rather than moving above
|
|
164
|
+
// our visible menu. Never clear a screen or enter an alternate screen.
|
|
165
|
+
if (width === previousWidth && previousMenuLines < rows) {
|
|
166
|
+
prefix = `\x1b[${previousMenuLines}A\r`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const clear = !initial && prefix.startsWith("\x1b[") ? "\x1b[2K" : "";
|
|
170
|
+
write(prefix + menu.map(line => `${clear}${line}\r\n`).join(""));
|
|
171
|
+
previousMenuLines = menu.length;
|
|
172
|
+
previousWidth = width;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function select(index: number) {
|
|
176
|
+
selected = (index + choices.length) % choices.length;
|
|
177
|
+
render();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onData(chunk: Buffer | string) {
|
|
181
|
+
for (const char of chunk.toString()) {
|
|
182
|
+
if (done) break;
|
|
183
|
+
if (char === "\x03") { finish("cancel"); break; }
|
|
184
|
+
if (escape) {
|
|
185
|
+
clearTimeout(escapeTimer);
|
|
186
|
+
escape += char;
|
|
187
|
+
if (escape === "\x1b[" || escape === "\x1bO") {
|
|
188
|
+
escapeTimer = setTimeout(() => finish("skip"), 40);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (/^\x1b[\[O][ABCD]$/.test(escape)) {
|
|
192
|
+
select(selected + (char === "A" || char === "D" ? -1 : 1));
|
|
193
|
+
escape = "";
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
finish("skip");
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
if (char === "\x1b") {
|
|
200
|
+
escape = char;
|
|
201
|
+
escapeTimer = setTimeout(() => finish("skip"), 40);
|
|
202
|
+
} else if (/^[123]$/.test(char)) select(Number(char) - 1);
|
|
203
|
+
else if (char === "\r" || char === "\n") finish(choices[selected]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
output.on("error", onError);
|
|
208
|
+
input.on("error", onError);
|
|
209
|
+
input.on("end", onEnd);
|
|
210
|
+
input.on("close", onEnd);
|
|
211
|
+
try {
|
|
212
|
+
if (input.readableEnded || input.destroyed || output.destroyed) {
|
|
213
|
+
finish("skip");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (input.setRawMode) {
|
|
217
|
+
rawAttempted = true;
|
|
218
|
+
input.setRawMode(true);
|
|
219
|
+
}
|
|
220
|
+
cursorHidden = true;
|
|
221
|
+
write("\x1b[?25l");
|
|
222
|
+
if (!done) render(true);
|
|
223
|
+
if (!done) {
|
|
224
|
+
input.on("data", onData);
|
|
225
|
+
input.resume();
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
finish("skip", error);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { stripTerminalSequences, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
6
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
7
|
+
|
|
8
|
+
const FALLBACK_COLUMNS = 24;
|
|
9
|
+
const MAX_SNAPSHOT_BYTES = 256 * 1024;
|
|
10
|
+
|
|
11
|
+
/** Read only a bounded local snapshot, never poll Herdr's CLI/socket for geometry. */
|
|
12
|
+
export function createActivityColumns(socket: string, now: () => number = Date.now): () => number {
|
|
13
|
+
const path = isAbsolute(socket) ? join(dirname(socket), "session.json") : undefined;
|
|
14
|
+
let refreshAt = -Infinity;
|
|
15
|
+
let columns = FALLBACK_COLUMNS;
|
|
16
|
+
return () => {
|
|
17
|
+
const time = now();
|
|
18
|
+
if (time < refreshAt) return columns;
|
|
19
|
+
refreshAt = time + 5000;
|
|
20
|
+
columns = FALLBACK_COLUMNS;
|
|
21
|
+
if (!path) return columns;
|
|
22
|
+
let fd: number | undefined;
|
|
23
|
+
try {
|
|
24
|
+
// Reject FIFOs after opening without waiting for a writer; unsupported flags are omitted.
|
|
25
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
26
|
+
const stat = fstatSync(fd);
|
|
27
|
+
if (!stat.isFile() || stat.size > MAX_SNAPSHOT_BYTES) return columns;
|
|
28
|
+
// One extra byte rejects files that grew after fstat, without unlimited reads.
|
|
29
|
+
const buffer = Buffer.alloc(MAX_SNAPSHOT_BYTES + 1);
|
|
30
|
+
const size = readSync(fd, buffer, 0, buffer.length, 0);
|
|
31
|
+
if (size > MAX_SNAPSHOT_BYTES) return columns;
|
|
32
|
+
const snapshot = JSON.parse(buffer.toString("utf8", 0, size));
|
|
33
|
+
const width: unknown = snapshot?.sidebar_width;
|
|
34
|
+
if (typeof width === "number" && Number.isInteger(width) && width >= 6 && width <= 512) {
|
|
35
|
+
// Divider + possible scrollbar + three-space agent detail prefix.
|
|
36
|
+
columns = width - 5;
|
|
37
|
+
}
|
|
38
|
+
} catch { /* Missing/malformed snapshots use conservative geometry. */ }
|
|
39
|
+
finally {
|
|
40
|
+
try { if (fd !== undefined) closeSync(fd); } catch { /* Best effort close. */ }
|
|
41
|
+
}
|
|
42
|
+
return columns;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fit whole graphemes within cell, UTF-8 byte and Unicode scalar budgets. */
|
|
47
|
+
function fit(parts: string[], columns: number, bytes: number, characters: number, preferWords = true): number {
|
|
48
|
+
let cells = 0;
|
|
49
|
+
let used = 0;
|
|
50
|
+
let scalars = 0;
|
|
51
|
+
let count = 0;
|
|
52
|
+
let space = -1;
|
|
53
|
+
for (const part of parts) {
|
|
54
|
+
const width = visibleWidth(part);
|
|
55
|
+
const size = Buffer.byteLength(part);
|
|
56
|
+
const length = Array.from(part).length;
|
|
57
|
+
if (cells + width > columns || used + size > bytes || scalars + length > characters) break;
|
|
58
|
+
if (part === " ") space = count;
|
|
59
|
+
cells += width;
|
|
60
|
+
used += size;
|
|
61
|
+
scalars += length;
|
|
62
|
+
count++;
|
|
63
|
+
}
|
|
64
|
+
if (preferWords && count < parts.length && parts[count] !== " " && space > 0) return space;
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Only the active Todo title is free text. LF is private framing, not a token. */
|
|
69
|
+
export function activitySummary(tasks: unknown, columns = FALLBACK_COLUMNS): string | null {
|
|
70
|
+
if (!Array.isArray(tasks)) return null;
|
|
71
|
+
const title: unknown = tasks.find((task) => task?.status === "in_progress")?.title;
|
|
72
|
+
if (typeof title !== "string") return null;
|
|
73
|
+
const clipped = title.length > 4096;
|
|
74
|
+
// Bound sanitization/segmentation work, preserve ZWJ emoji but strip bidi controls.
|
|
75
|
+
/* eslint-disable no-control-regex */
|
|
76
|
+
const clean = stripTerminalSequences(title.slice(0, 4096))
|
|
77
|
+
.replace(/[\x00-\x1f\x7f-\x9f\u061c\u200b-\u200c\u200e-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/g, " ")
|
|
78
|
+
.replace(/\s+/g, " ").trim();
|
|
79
|
+
/* eslint-enable no-control-regex */
|
|
80
|
+
if (!clean) return null;
|
|
81
|
+
const parts = Array.from(graphemes.segment(clean), ({ segment }) => segment);
|
|
82
|
+
// The bounded sample may end inside a grapheme; never publish that fragment.
|
|
83
|
+
if (clipped) parts.pop();
|
|
84
|
+
columns = Number.isFinite(columns) ? Math.max(1, Math.min(512, Math.floor(columns))) : FALLBACK_COLUMNS;
|
|
85
|
+
if (columns <= 2) return columns === 1 ? "…" : "◐…";
|
|
86
|
+
// Herdr caps each token at 80 scalars; count our icon/indent before its trim.
|
|
87
|
+
const whole = fit(parts, columns - 2, 252, 78, false);
|
|
88
|
+
if (!clipped && whole === parts.length) return `◐ ${parts.join("")}`;
|
|
89
|
+
// Reserve the second-row indent and ellipsis; private LF framing is not a token byte.
|
|
90
|
+
let firstCount = fit(parts, columns - 2, 247, 78);
|
|
91
|
+
if (!firstCount) return columns === 3 ? "◐…" : "◐ …";
|
|
92
|
+
const restFits = (count: number): boolean => {
|
|
93
|
+
const first = `◐ ${parts.slice(0, count).join("").trimEnd()}`;
|
|
94
|
+
const rest = parts.slice(count);
|
|
95
|
+
while (rest[0] === " ") rest.shift();
|
|
96
|
+
return fit(rest, columns - 2, 256 - Buffer.byteLength(first) - 2, 78, false) === rest.length;
|
|
97
|
+
};
|
|
98
|
+
// Prefer words unless splitting one would preserve the entire two-row title.
|
|
99
|
+
if (!clipped && !restFits(firstCount)) {
|
|
100
|
+
const hardCount = fit(parts, columns - 2, 247, 78, false);
|
|
101
|
+
if (restFits(hardCount)) firstCount = hardCount;
|
|
102
|
+
}
|
|
103
|
+
const first = `◐ ${parts.slice(0, firstCount).join("").trimEnd()}`;
|
|
104
|
+
const rest = parts.slice(firstCount);
|
|
105
|
+
while (rest[0] === " ") rest.shift();
|
|
106
|
+
const bytes = 256 - Buffer.byteLength(first) - 2; // Two-space indent.
|
|
107
|
+
let count = fit(rest, columns - 2, bytes, 78, false);
|
|
108
|
+
const overflow = clipped || count < rest.length;
|
|
109
|
+
if (overflow) count = fit(rest, columns - 3, bytes - 3, 77);
|
|
110
|
+
const second = rest.slice(0, count).join("").trimEnd();
|
|
111
|
+
return `${first}\n ${second}${overflow ? "…" : ""}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function metadataArgs(pane: string, summary: string | null, seq: number): string[] {
|
|
115
|
+
const rows = summary?.split("\n") ?? [];
|
|
116
|
+
const tokens = ["summary", "summary2"].flatMap((token, index) => rows[index]
|
|
117
|
+
? ["--token", `${token}=${rows[index]}`] : ["--clear-token", token]);
|
|
118
|
+
return ["pane", "report-metadata", pane, "--source", "gentle:activity", "--agent", "pi",
|
|
119
|
+
"--seq", String(seq), "--ttl-ms", "30000", ...tokens];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function metadataTransport(env: NodeJS.ProcessEnv): (summary: string | null, seq: number) => Promise<void> {
|
|
123
|
+
return (summary, seq) => new Promise((resolve) => {
|
|
124
|
+
// No shell, bounded output/time, and failure never blocks Pi's handlers.
|
|
125
|
+
execFile(env.HERDR_BIN_PATH ?? "herdr", metadataArgs(env.HERDR_PANE_ID!, summary, seq), {
|
|
126
|
+
env, timeout: 1500, maxBuffer: 4096, windowsHide: true,
|
|
127
|
+
}, () => resolve());
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One in flight, one pending value; a clear supersedes queued active work. */
|
|
132
|
+
export class ActivityPublisher {
|
|
133
|
+
private pending: string | null | undefined;
|
|
134
|
+
private last: string | null | undefined;
|
|
135
|
+
private running = false;
|
|
136
|
+
private closed = false;
|
|
137
|
+
private cancel: (() => void) | undefined;
|
|
138
|
+
private seq = Date.now();
|
|
139
|
+
private readonly send: (summary: string | null, seq: number) => Promise<void>;
|
|
140
|
+
private readonly schedule: (fn: () => void) => () => void;
|
|
141
|
+
constructor(send: (summary: string | null, seq: number) => Promise<void>,
|
|
142
|
+
schedule: (fn: () => void) => () => void = (fn) => {
|
|
143
|
+
const timer = setTimeout(fn, 150);
|
|
144
|
+
timer.unref();
|
|
145
|
+
return () => clearTimeout(timer);
|
|
146
|
+
}) {
|
|
147
|
+
this.send = send;
|
|
148
|
+
this.schedule = schedule;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
update(summary: string | null, force = false): void {
|
|
152
|
+
if (this.closed || (!force && (this.pending !== undefined
|
|
153
|
+
? summary === this.pending : !this.running && summary === this.last))) return;
|
|
154
|
+
this.pending = summary;
|
|
155
|
+
if (summary === null) {
|
|
156
|
+
this.cancel?.();
|
|
157
|
+
this.cancel = undefined;
|
|
158
|
+
if (!this.running) void this.flush();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.enqueue();
|
|
162
|
+
}
|
|
163
|
+
private enqueue(): void {
|
|
164
|
+
if (this.running || this.cancel || this.pending === undefined) return;
|
|
165
|
+
this.cancel = this.schedule(() => { this.cancel = undefined; void this.flush(); });
|
|
166
|
+
}
|
|
167
|
+
private async flush(): Promise<void> {
|
|
168
|
+
if (this.pending === undefined) return;
|
|
169
|
+
const summary = this.pending;
|
|
170
|
+
this.pending = undefined;
|
|
171
|
+
this.running = true;
|
|
172
|
+
try { await this.send(summary, ++this.seq); this.last = summary; }
|
|
173
|
+
catch { /* Best effort; a refresh retries active state. */ }
|
|
174
|
+
finally { this.running = false; this.enqueue(); }
|
|
175
|
+
}
|
|
176
|
+
close(): void {
|
|
177
|
+
if (this.closed) return;
|
|
178
|
+
this.update(null, true);
|
|
179
|
+
this.closed = true;
|
|
180
|
+
this.cancel?.(); this.cancel = undefined;
|
|
181
|
+
if (!this.running) void this.flush();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -73,6 +73,8 @@ export interface InProcessReviewerRequest {
|
|
|
73
73
|
readonly selection: string;
|
|
74
74
|
/** Routing thinking label: off | minimal | low | medium | high | xhigh | max. Omitted is treated as "off". */
|
|
75
75
|
readonly thinking?: string;
|
|
76
|
+
/** Effective Pi transport preference; absent preserves the provider default. */
|
|
77
|
+
readonly transport?: SimpleStreamOptions["transport"];
|
|
76
78
|
/** Frozen Go-materialized prompt bytes, submitted verbatim as the one user message. */
|
|
77
79
|
readonly prompt: Buffer;
|
|
78
80
|
readonly timeoutMs: number;
|
|
@@ -272,6 +274,7 @@ export async function runInProcessReviewer(request: InProcessReviewerRequest, de
|
|
|
272
274
|
...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
|
|
273
275
|
...(auth.headers === undefined && attributionHeaders === undefined ? {} : { headers: attributionHeaders === undefined ? auth.headers : { ...attributionHeaders, ...auth.headers } }),
|
|
274
276
|
...(reasoning.reasoning === undefined ? {} : { reasoning: reasoning.reasoning }),
|
|
277
|
+
...(request.transport === undefined ? {} : { transport: request.transport }),
|
|
275
278
|
};
|
|
276
279
|
|
|
277
280
|
// An abort is classified by which signal actually fired, never by the
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Session-scoped display data only; never command output or process control. */
|
|
2
|
+
export const JOBS_SIDEBAR_EVENT = "gentle-ai:jobs-sidebar";
|
|
3
|
+
export const JOBS_STATUS_KEY = "gentle-jobs";
|
|
4
|
+
|
|
5
|
+
export interface RunningJobDisplay {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
startedAt: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface JobsSidebarSnapshot {
|
|
12
|
+
jobs: RunningJobDisplay[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isJobsSidebarSnapshot(value: unknown): value is JobsSidebarSnapshot {
|
|
16
|
+
if (!value || typeof value !== "object" || !Array.isArray((value as JobsSidebarSnapshot).jobs)) return false;
|
|
17
|
+
return (value as JobsSidebarSnapshot).jobs.every((job) => job && typeof job === "object" &&
|
|
18
|
+
typeof job.id === "string" && job.id.length > 0 && typeof job.label === "string" &&
|
|
19
|
+
typeof job.startedAt === "number" && Number.isFinite(job.startedAt));
|
|
20
|
+
}
|
package/lib/jobs-view.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Background jobs overlay (`/gentle:jobs`): this session's jobs beside the
|
|
2
|
+
// selected job's command, status, output file, and output tail. It shares the
|
|
3
|
+
// Agents overlay geometry and roles so both read as one family, but stays a
|
|
4
|
+
// small standalone view: jobs have no peers, groups, or threads.
|
|
5
|
+
|
|
6
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { JobRecord } from "./background-jobs.ts";
|
|
8
|
+
import { formatJobDuration, JOB_GLYPH, jobOutcome } from "./background-jobs-tools.ts";
|
|
9
|
+
import { measureAgentsViewLayout } from "./agents-view-layout.ts";
|
|
10
|
+
import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
11
|
+
|
|
12
|
+
export interface JobsViewDeps {
|
|
13
|
+
theme: { fg(role: string, text: string): string };
|
|
14
|
+
rows: () => number;
|
|
15
|
+
/** This session's jobs; the view sorts a copy by activity and recency. */
|
|
16
|
+
jobs: () => JobRecord[];
|
|
17
|
+
now: () => number;
|
|
18
|
+
onStop(job: JobRecord): void;
|
|
19
|
+
onClose(): void;
|
|
20
|
+
requestRender(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ROLE = { FRAME: "border", TITLE: "customMessageLabel", SELECTED: "accent", NAME: "text", META: "dim", KEY: "accent", KEY_TEXT: "dim", EMPTY: "dim" } as const;
|
|
24
|
+
const REFRESH_MS = 1000;
|
|
25
|
+
|
|
26
|
+
function glyph(job: JobRecord): [string, string] {
|
|
27
|
+
if (job.status === "running") return [job.kind === "monitor" ? "◉" : "◐", "accent"];
|
|
28
|
+
if (job.status === "stopped") return ["–", "dim"];
|
|
29
|
+
if (job.status === "exited" && job.exitCode === 0) return ["✓", "success"];
|
|
30
|
+
return ["✗", "error"];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rule = (length: number) => "─".repeat(Math.max(0, length));
|
|
34
|
+
const fit = (text: string, width: number) => {
|
|
35
|
+
const clipped = truncateToWidth(text, Math.max(0, width), "…");
|
|
36
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
37
|
+
};
|
|
38
|
+
const clean = (text: string) => sanitizeTerminalText(text).replace(/\s+/g, " ");
|
|
39
|
+
|
|
40
|
+
export class JobsView {
|
|
41
|
+
private readonly deps: JobsViewDeps;
|
|
42
|
+
private selectedId: string | undefined;
|
|
43
|
+
private details = false;
|
|
44
|
+
private listScroll = 0;
|
|
45
|
+
private closed = false;
|
|
46
|
+
private readonly timer: ReturnType<typeof setInterval>;
|
|
47
|
+
|
|
48
|
+
constructor(deps: JobsViewDeps) {
|
|
49
|
+
this.deps = deps;
|
|
50
|
+
// Elapsed time and the output tail move while a job runs.
|
|
51
|
+
this.timer = setInterval(() => {
|
|
52
|
+
if (this.deps.jobs().some((job) => job.status === "running")) this.deps.requestRender();
|
|
53
|
+
}, REFRESH_MS);
|
|
54
|
+
this.timer.unref?.();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
dispose(): void {
|
|
58
|
+
this.closed = true;
|
|
59
|
+
clearInterval(this.timer);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
invalidate(): void {}
|
|
63
|
+
|
|
64
|
+
handleInput(data: string): void {
|
|
65
|
+
if (this.closed) return;
|
|
66
|
+
const jobs = this.orderedJobs();
|
|
67
|
+
const index = Math.max(0, jobs.findIndex((job) => job.id === this.selected(jobs)?.id));
|
|
68
|
+
if (data === "q") this.close();
|
|
69
|
+
else if (matchesKey(data, Key.escape)) {
|
|
70
|
+
if (this.details) this.details = false;
|
|
71
|
+
else this.close();
|
|
72
|
+
} else if (data === "\t" || matchesKey(data, Key.enter)) this.details = !this.details;
|
|
73
|
+
else if (data === "j" || matchesKey(data, Key.down)) this.selectedId = jobs[Math.min(jobs.length - 1, index + 1)]?.id;
|
|
74
|
+
else if (data === "k" || matchesKey(data, Key.up)) this.selectedId = jobs[Math.max(0, index - 1)]?.id;
|
|
75
|
+
else if (data === "s") {
|
|
76
|
+
const job = this.selected(jobs);
|
|
77
|
+
if (job?.status === "running") this.deps.onStop(job);
|
|
78
|
+
} else return;
|
|
79
|
+
this.deps.requestRender();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render(width: number): string[] {
|
|
83
|
+
const layout = measureAgentsViewLayout(width, this.deps.rows(), this.details);
|
|
84
|
+
if (layout.width === 0 || layout.height === 0) return [];
|
|
85
|
+
if (layout.mode === "fallback") return [fit("× Close Jobs", layout.width)];
|
|
86
|
+
const { theme } = this.deps;
|
|
87
|
+
const jobs = this.orderedJobs();
|
|
88
|
+
const selected = this.selected(jobs);
|
|
89
|
+
const inner = layout.width - 2;
|
|
90
|
+
const running = jobs.filter((job) => job.status === "running").length;
|
|
91
|
+
const title = truncateToWidth(`${JOB_GLYPH} Jobs · ${running} running · ${jobs.length - running} finished`, Math.max(0, inner - 4), "…");
|
|
92
|
+
const top = theme.fg(ROLE.FRAME, "╭─ ") + theme.fg(ROLE.TITLE, title) + theme.fg(ROLE.FRAME, ` ${rule(inner - visibleWidth(title) - 3)}╮`);
|
|
93
|
+
const list = this.listLines(jobs, selected, layout.listWidth, layout.bodyRows);
|
|
94
|
+
const detail = selected ? this.detailLines(selected, layout.bodyRows) : [theme.fg(ROLE.EMPTY, "No background jobs in this session."), theme.fg(ROLE.EMPTY, "The agent starts them with bash_background or monitor.")];
|
|
95
|
+
const bar = theme.fg(ROLE.FRAME, "│");
|
|
96
|
+
const body: string[] = [];
|
|
97
|
+
for (let row = 0; row < layout.bodyRows; row += 1) {
|
|
98
|
+
if (layout.mode === "panes") body.push(`${bar} ${fit(list[row] ?? "", layout.listWidth)} ${bar} ${fit(detail[row] ?? "", layout.threadWidth)}${bar}`);
|
|
99
|
+
else body.push(`${bar} ${fit((this.details ? detail : list)[row] ?? "", layout.listWidth)} ${bar}`);
|
|
100
|
+
}
|
|
101
|
+
const keys = this.keys(selected, layout.mode === "narrow").map(([key, label]) => `${theme.fg(ROLE.KEY, key)} ${theme.fg(ROLE.KEY_TEXT, label)}`).join(" ");
|
|
102
|
+
return [top, ...body, `${bar} ${fit(keys, inner - 2)} ${bar}`, theme.fg(ROLE.FRAME, `╰${rule(inner)}╯`)];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private orderedJobs(): JobRecord[] {
|
|
106
|
+
return [...this.deps.jobs()].sort((a, b) =>
|
|
107
|
+
Number(b.status === "running") - Number(a.status === "running") || b.startedAt - a.startedAt);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private close(): void {
|
|
111
|
+
this.deps.onClose();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private selected(jobs: JobRecord[]): JobRecord | undefined {
|
|
115
|
+
const selected = jobs.find((job) => job.id === this.selectedId) ?? jobs[0];
|
|
116
|
+
this.selectedId = selected?.id;
|
|
117
|
+
return selected;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private elapsed(job: JobRecord): string {
|
|
121
|
+
return formatJobDuration((job.endedAt ?? this.deps.now()) - job.startedAt);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private listLines(jobs: JobRecord[], selected: JobRecord | undefined, width: number, rows: number): string[] {
|
|
125
|
+
const { theme } = this.deps;
|
|
126
|
+
if (jobs.length === 0) return [theme.fg(ROLE.EMPTY, "No jobs")];
|
|
127
|
+
const index = jobs.findIndex((job) => job.id === selected?.id);
|
|
128
|
+
if (index < this.listScroll) this.listScroll = index;
|
|
129
|
+
else if (index >= this.listScroll + rows) this.listScroll = index - rows + 1;
|
|
130
|
+
this.listScroll = Math.max(0, Math.min(this.listScroll, Math.max(0, jobs.length - rows)));
|
|
131
|
+
return jobs.slice(this.listScroll, this.listScroll + rows).map((job) => {
|
|
132
|
+
const [mark, role] = glyph(job);
|
|
133
|
+
const elapsed = this.elapsed(job);
|
|
134
|
+
const isSelected = job.id === selected?.id;
|
|
135
|
+
const name = fit(`${isSelected ? "›" : " "} ${mark} ${clean(job.label)}`, Math.max(0, width - visibleWidth(elapsed) - 1));
|
|
136
|
+
return `${theme.fg(isSelected ? ROLE.SELECTED : role === "accent" ? ROLE.NAME : role, name)} ${theme.fg(ROLE.META, elapsed)}`;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private detailLines(job: JobRecord, rows: number): string[] {
|
|
141
|
+
const { theme } = this.deps;
|
|
142
|
+
const head = [
|
|
143
|
+
theme.fg(ROLE.TITLE, `${clean(job.label)} · ${job.id}`),
|
|
144
|
+
`${theme.fg(ROLE.META, "Command")} ${clean(job.command)}`,
|
|
145
|
+
`${theme.fg(ROLE.META, "Status ")} ${clean(jobOutcome(job))} · ${this.elapsed(job)}${job.kind === "monitor" ? ` · ${job.events ?? 0} event${job.events === 1 ? "" : "s"}` : ""}`,
|
|
146
|
+
`${theme.fg(ROLE.META, "Output ")} ${clean(job.outputPath)}`,
|
|
147
|
+
"",
|
|
148
|
+
];
|
|
149
|
+
const room = Math.max(0, rows - head.length - 1);
|
|
150
|
+
const tail = job.tail.slice(-room).map((line) => sanitizeTerminalText(line));
|
|
151
|
+
return [...head, theme.fg(ROLE.META, tail.length > 0 ? "Last output" : "No output yet."), ...tail];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private keys(selected: JobRecord | undefined, narrow: boolean): Array<[string, string]> {
|
|
155
|
+
const keys: Array<[string, string]> = [["↑↓", "move"]];
|
|
156
|
+
if (narrow && selected) keys.push(["tab", this.details ? "list" : "details"]);
|
|
157
|
+
if (selected?.status === "running") keys.push(["s", "Stop"]);
|
|
158
|
+
keys.push(["q", "close"]);
|
|
159
|
+
return keys;
|
|
160
|
+
}
|
|
161
|
+
}
|