gentle-pi 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -8
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +703 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +81 -31
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +267 -40
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +9 -9
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +688 -88
- package/extensions/gentle-ai.ts +886 -238
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +265 -69
- package/extensions/gentle-todo.ts +108 -13
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +74 -23
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +18 -6
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +62 -19
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +7 -3
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +1 -1
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +73 -22
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1722 -59
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +249 -5
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +234 -13
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +541 -96
- package/tests/gentle-todo.test.ts +253 -2
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +101 -17
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +30 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +120 -2
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +7 -7
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +32 -4
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
package/lib/shell-todo.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
1
|
+
import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { CARD_TONE, panelHeaderRow, panelInnerWidth, renderCard, type CardTheme, type CardTone } from "./shell-card.ts";
|
|
3
3
|
import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
4
4
|
import { paintHoverable } from "./shell-hover.ts";
|
|
@@ -11,7 +11,11 @@ import { paintHoverable } from "./shell-hover.ts";
|
|
|
11
11
|
export const TODO_STATUS = {
|
|
12
12
|
PENDING: "pending",
|
|
13
13
|
IN_PROGRESS: "in_progress",
|
|
14
|
+
/** Open, but waiting on something outside the list; always carries a note naming it. */
|
|
15
|
+
BLOCKED: "blocked",
|
|
14
16
|
DONE: "done",
|
|
17
|
+
/** Will not be done, on purpose: terminal, and never counts as open. */
|
|
18
|
+
DROPPED: "dropped",
|
|
15
19
|
} as const;
|
|
16
20
|
|
|
17
21
|
export type TodoStatus = (typeof TODO_STATUS)[keyof typeof TODO_STATUS];
|
|
@@ -86,10 +90,20 @@ export interface TodoRenderOptions {
|
|
|
86
90
|
export const TODO_DETAILS_KEY = "gentleTodo";
|
|
87
91
|
export const TODO_TOOL_NAME = "todo";
|
|
88
92
|
export const TODO_GLYPH = "❀";
|
|
89
|
-
const STATUS_ALIASES: Record<string, TodoStatus> = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
const STATUS_ALIASES: Record<string, TodoStatus> = {
|
|
94
|
+
completed: TODO_STATUS.DONE,
|
|
95
|
+
complete: TODO_STATUS.DONE,
|
|
96
|
+
doing: TODO_STATUS.IN_PROGRESS,
|
|
97
|
+
todo: TODO_STATUS.PENDING,
|
|
98
|
+
cancelled: TODO_STATUS.DROPPED,
|
|
99
|
+
canceled: TODO_STATUS.DROPPED,
|
|
100
|
+
abandoned: TODO_STATUS.DROPPED,
|
|
101
|
+
};
|
|
102
|
+
const STATUS_LIST = "pending, in_progress, blocked, done, or dropped";
|
|
103
|
+
const BLOCKED_NEEDS_NOTE = "blocked needs a note naming what it waits for";
|
|
104
|
+
const STATUS_GLYPH: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "○", [TODO_STATUS.IN_PROGRESS]: "◐", [TODO_STATUS.BLOCKED]: "⊘", [TODO_STATUS.DONE]: "✓", [TODO_STATUS.DROPPED]: "✕" };
|
|
105
|
+
const STATUS_ROLE: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "text", [TODO_STATUS.IN_PROGRESS]: "accent", [TODO_STATUS.BLOCKED]: "text", [TODO_STATUS.DONE]: "dim", [TODO_STATUS.DROPPED]: "dim" };
|
|
106
|
+
const GLYPH_ROLE: Record<TodoStatus, string> = { [TODO_STATUS.PENDING]: "muted", [TODO_STATUS.IN_PROGRESS]: "accent", [TODO_STATUS.BLOCKED]: "warning", [TODO_STATUS.DONE]: "success", [TODO_STATUS.DROPPED]: "muted" };
|
|
93
107
|
const NOTE_ROLE = "muted";
|
|
94
108
|
const STALE_AFTER_TURNS = 2;
|
|
95
109
|
/** Above this many rows the finished tasks fold into one line and the rest is capped. */
|
|
@@ -111,20 +125,30 @@ function parseStatus(value: unknown): TodoStatus | undefined {
|
|
|
111
125
|
return STATUS_ALIASES[normalized];
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
function countStatus(state: TodoState, status: TodoStatus): number {
|
|
129
|
+
return state.tasks.filter((task) => task.status === status).length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Dropped tasks are neither done nor open. */
|
|
114
133
|
export function todoSummary(state: TodoState): TodoSummary {
|
|
115
|
-
const done = state
|
|
116
|
-
return { done, total: state.tasks.length, open: state.tasks.length - done };
|
|
134
|
+
const done = countStatus(state, TODO_STATUS.DONE);
|
|
135
|
+
return { done, total: state.tasks.length, open: state.tasks.length - done - countStatus(state, TODO_STATUS.DROPPED) };
|
|
117
136
|
}
|
|
118
137
|
|
|
138
|
+
// Only work the model can pick up goes stale: a list whose open tasks all
|
|
139
|
+
// wait on something outside it has nothing to bring up to date.
|
|
119
140
|
export function staleTurns(state: TodoState, currentTurn: number): number {
|
|
120
|
-
|
|
141
|
+
const actionable = state.tasks.some((task) => task.status === TODO_STATUS.PENDING || task.status === TODO_STATUS.IN_PROGRESS);
|
|
142
|
+
if (state.updatedTurn === null || !actionable) return 0;
|
|
121
143
|
return Math.max(0, currentTurn - state.updatedTurn);
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
function summaryText(state: TodoState): string {
|
|
125
147
|
const { done, total } = todoSummary(state);
|
|
126
|
-
const inProgress = state
|
|
127
|
-
|
|
148
|
+
const inProgress = countStatus(state, TODO_STATUS.IN_PROGRESS);
|
|
149
|
+
const blocked = countStatus(state, TODO_STATUS.BLOCKED);
|
|
150
|
+
const dropped = countStatus(state, TODO_STATUS.DROPPED);
|
|
151
|
+
return `${total} ${total === 1 ? "task" : "tasks"} · ${done} done · ${inProgress} in progress${blocked ? ` · ${blocked} blocked` : ""}${dropped ? ` · ${dropped} dropped` : ""}`;
|
|
128
152
|
}
|
|
129
153
|
|
|
130
154
|
function taskLine(task: TodoTask): string {
|
|
@@ -135,8 +159,9 @@ function buildTask(input: TodoTaskInput, id: number): TodoTask | string {
|
|
|
135
159
|
const title = cleanText(input.title);
|
|
136
160
|
if (title.length === 0) return "title is required";
|
|
137
161
|
const status = parseStatus(input.status);
|
|
138
|
-
if (status === undefined) return `unknown status "${input.status}" (use
|
|
162
|
+
if (status === undefined) return `unknown status "${input.status}" (use ${STATUS_LIST})`;
|
|
139
163
|
const note = cleanText(input.note);
|
|
164
|
+
if (status === TODO_STATUS.BLOCKED && note.length === 0) return BLOCKED_NEEDS_NOTE;
|
|
140
165
|
return { id, title, status, ...(note ? { note } : {}) };
|
|
141
166
|
}
|
|
142
167
|
|
|
@@ -240,6 +265,7 @@ export function todoPromptBlock(state: TodoState, stale: number): string | undef
|
|
|
240
265
|
return [
|
|
241
266
|
"## Todo list",
|
|
242
267
|
"Keep it current with the `todo` tool: mark a task in_progress before starting it, done right after finishing it, and rewrite the whole list with `write` whenever the plan changes. Update it before you end the turn.",
|
|
268
|
+
"A blocked task waits on something outside the list: do not work on it until its condition holds. When the plan changes, mark the tasks it made obsolete dropped instead of leaving them pending or marking them done.",
|
|
243
269
|
...lines,
|
|
244
270
|
].join("\n") + staleLine;
|
|
245
271
|
}
|
|
@@ -253,18 +279,22 @@ function taskRow(task: TodoTask, theme: TodoTheme, inner: number): string {
|
|
|
253
279
|
.join("\n");
|
|
254
280
|
}
|
|
255
281
|
const title = task.title;
|
|
256
|
-
const
|
|
282
|
+
const showsNote = task.status === TODO_STATUS.IN_PROGRESS || task.status === TODO_STATUS.BLOCKED;
|
|
283
|
+
const note = showsNote && task.note ? ` ${theme.fg(NOTE_ROLE, "·")} ${theme.fg(NOTE_ROLE, task.note)}` : "";
|
|
257
284
|
return `${theme.fg(GLYPH_ROLE[task.status], STATUS_GLYPH[task.status])} ${theme.fg(STATUS_ROLE[task.status], title)}${note}`;
|
|
258
285
|
}
|
|
259
286
|
|
|
260
|
-
// Long lists keep the card short: done tasks fold into "✓ N done"
|
|
261
|
-
// open ones fill the remaining rows, with a
|
|
287
|
+
// Long lists keep the card short: done and dropped tasks fold into "✓ N done"
|
|
288
|
+
// and "✕ N dropped", and the open ones fill the remaining rows, with a
|
|
289
|
+
// trailing count for the rest.
|
|
262
290
|
function bodyRows(state: TodoState, theme: TodoTheme, inner: number): string[] {
|
|
263
291
|
if (state.tasks.length <= ROW_CAP) return state.tasks.map((task) => taskRow(task, theme, inner));
|
|
264
|
-
const
|
|
265
|
-
const open = state.tasks.filter((task) => task.status !== TODO_STATUS.DONE);
|
|
292
|
+
const open = state.tasks.filter((task) => task.status !== TODO_STATUS.DONE && task.status !== TODO_STATUS.DROPPED);
|
|
266
293
|
const rows: string[] = [];
|
|
267
|
-
|
|
294
|
+
for (const [status, label] of [[TODO_STATUS.DONE, "done"], [TODO_STATUS.DROPPED, "dropped"]] as const) {
|
|
295
|
+
const count = countStatus(state, status);
|
|
296
|
+
if (count > 0) rows.push(`${theme.fg(GLYPH_ROLE[status], STATUS_GLYPH[status])} ${theme.fg(NOTE_ROLE, `${count} ${label}`)}`);
|
|
297
|
+
}
|
|
268
298
|
const room = ROW_CAP - rows.length - (open.length > ROW_CAP - rows.length ? 1 : 0);
|
|
269
299
|
for (const task of open.slice(0, room)) rows.push(taskRow(task, theme, inner));
|
|
270
300
|
if (open.length > room) rows.push(theme.fg(NOTE_ROLE, `… ${open.length - room} more`));
|
|
@@ -276,6 +306,8 @@ function collapsedRow(state: TodoState, theme: TodoTheme, inner: number): string
|
|
|
276
306
|
if (active) return taskRow(active, theme, inner);
|
|
277
307
|
const pending = state.tasks.find((task) => task.status === TODO_STATUS.PENDING);
|
|
278
308
|
if (pending) return taskRow(pending, theme, inner);
|
|
309
|
+
const blocked = state.tasks.find((task) => task.status === TODO_STATUS.BLOCKED);
|
|
310
|
+
if (blocked) return taskRow(blocked, theme, inner);
|
|
279
311
|
const { open } = todoSummary(state);
|
|
280
312
|
return `${theme.fg(GLYPH_ROLE[TODO_STATUS.PENDING], STATUS_GLYPH[TODO_STATUS.PENDING])} ${theme.fg(NOTE_ROLE, `${open} open`)}`;
|
|
281
313
|
}
|
|
@@ -288,6 +320,8 @@ export function todoCardTone(staleTurns: number): CardTone {
|
|
|
288
320
|
export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number, options: TodoRenderOptions): string[] {
|
|
289
321
|
if (state.tasks.length === 0) return [];
|
|
290
322
|
const { done, total } = todoSummary(state);
|
|
323
|
+
// Dropped work is not part of the plan anymore, so it leaves the denominator.
|
|
324
|
+
const planned = total - countStatus(state, TODO_STATUS.DROPPED);
|
|
291
325
|
const stale = options.staleTurns >= STALE_AFTER_TURNS;
|
|
292
326
|
const action = options.collapsed ? "expand" : "collapse";
|
|
293
327
|
const actionLabel = action[0]!.toUpperCase() + action.slice(1);
|
|
@@ -300,12 +334,21 @@ export function renderTodoCard(state: TodoState, theme: TodoTheme, width: number
|
|
|
300
334
|
// Content columns of the card body in the active style (float is narrower).
|
|
301
335
|
const inner = panelInnerWidth(theme, width, tone);
|
|
302
336
|
const rows = options.collapsed ? [collapsedRow(state, theme, inner)] : options.scrollable ? state.tasks.map((task) => taskRow(task, theme, inner)) : bodyRows(state, theme, inner);
|
|
303
|
-
const
|
|
337
|
+
const staleLabel = theme.fg(NOTE_ROLE, `stale · ${options.staleTurns} turns`);
|
|
338
|
+
let body = options.collapsed
|
|
339
|
+
? [truncateToWidth(`${stale ? `${staleLabel} · ` : ""}${rows[0]}`, inner, "…")]
|
|
340
|
+
: stale ? [staleLabel, ...rows] : rows;
|
|
341
|
+
if (!options.collapsed && !options.scrollable) {
|
|
342
|
+
// Budget physical lines after wrapping: one long task can exceed the
|
|
343
|
+
// old task-count cap by itself. Scrollable hosts retain every line.
|
|
344
|
+
body = body.flatMap((row) => wrapTextWithAnsi(row, Math.max(1, inner)));
|
|
345
|
+
if (body.length > ROW_CAP) body = [...body.slice(0, ROW_CAP - 1), theme.fg(NOTE_ROLE, "… more")];
|
|
346
|
+
}
|
|
304
347
|
// The clickable control paints the shared hover role while hovered --
|
|
305
348
|
// same treatment every other clickable surface uses -- instead of its
|
|
306
349
|
// ordinary accent role.
|
|
307
350
|
return renderCard(
|
|
308
|
-
{ title: `Todos ${paintHoverable(theme, control, options.hovered, "accent")}`, subtitle: `${done} of ${
|
|
351
|
+
{ title: `Todos ${paintHoverable(theme, control, options.hovered, "accent")}`, subtitle: `${done} of ${planned}`, body, tone, glyph: TODO_GLYPH },
|
|
309
352
|
theme,
|
|
310
353
|
width,
|
|
311
354
|
{ expanded: true, hint, panel: true },
|
package/lib/shell-usage.ts
CHANGED
|
@@ -269,7 +269,7 @@ export function formatReset(resetAt: number | null, now: number): string {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
function parseWindow(raw: RawWindow | null | undefined, now: number): UsageWindow | undefined {
|
|
272
|
-
if (!raw ||
|
|
272
|
+
if (!raw || !isFiniteNumber(raw.used_percent) || !isFiniteNumber(raw.limit_window_seconds) || raw.limit_window_seconds <= 0) return undefined;
|
|
273
273
|
const resetAt =
|
|
274
274
|
typeof raw.reset_at === "number" ? raw.reset_at * 1000 : typeof raw.reset_after_seconds === "number" ? now + raw.reset_after_seconds * 1000 : null;
|
|
275
275
|
return { label: windowLabel(raw.limit_window_seconds), usedPercent: raw.used_percent, windowSeconds: raw.limit_window_seconds, resetAt };
|
|
@@ -297,9 +297,10 @@ export function parseCodexUsage(payload: unknown, now: number): ProviderUsage {
|
|
|
297
297
|
function headerWindow(headers: Record<string, string>, kind: "primary" | "secondary"): UsageWindow | undefined {
|
|
298
298
|
const used = Number.parseFloat(headers[`${HEADER_PREFIX}${kind}-used-percent`] ?? "");
|
|
299
299
|
if (!Number.isFinite(used)) return undefined;
|
|
300
|
-
|
|
300
|
+
// Missing or invalid duration is not a zero-minute quota or an assumed 5h window.
|
|
301
|
+
const seconds = Number(headers[`${HEADER_PREFIX}${kind}-window-minutes`]) * MINUTE;
|
|
302
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return undefined;
|
|
301
303
|
const resetAt = Number.parseInt(headers[`${HEADER_PREFIX}${kind}-reset-at`] ?? "", 10);
|
|
302
|
-
const seconds = Number.isFinite(minutes) ? minutes * MINUTE : 0;
|
|
303
304
|
return { label: windowLabel(seconds), usedPercent: used, windowSeconds: seconds, resetAt: Number.isFinite(resetAt) ? resetAt * 1000 : null };
|
|
304
305
|
}
|
|
305
306
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { TSchema } from "typebox";
|
|
4
|
+
|
|
5
|
+
const MAX_STATUSES = 8;
|
|
6
|
+
const MAX_ACTIVE_OBSERVATIONS = 8;
|
|
7
|
+
type Outcome = { kind: "success" | "cli_error" | "native_failure" | "unknown"; code?: string; phase?: string; mutation_outcome?: string; retry_safe?: boolean; next_action?: string };
|
|
8
|
+
interface StatusSample {
|
|
9
|
+
timeout_ms: number;
|
|
10
|
+
total_ms: number;
|
|
11
|
+
resolution_ms: number;
|
|
12
|
+
adapter_ms: number;
|
|
13
|
+
decode_ms: number;
|
|
14
|
+
outcome: Outcome;
|
|
15
|
+
}
|
|
16
|
+
interface Summary {
|
|
17
|
+
schema: "gentle-pi.status-timing/v1";
|
|
18
|
+
host_total_ms: number;
|
|
19
|
+
dispatch_ms: number;
|
|
20
|
+
sidebar_ms: number;
|
|
21
|
+
completion_ms: number;
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
clock_unavailable?: true;
|
|
24
|
+
statuses: StatusSample[];
|
|
25
|
+
}
|
|
26
|
+
interface HostObservation {
|
|
27
|
+
clock: () => number;
|
|
28
|
+
current: () => boolean;
|
|
29
|
+
claim: () => boolean;
|
|
30
|
+
claimed: boolean;
|
|
31
|
+
started: number;
|
|
32
|
+
lastNativeEnd: number;
|
|
33
|
+
summary: Summary;
|
|
34
|
+
}
|
|
35
|
+
interface NativeObservation { host: HostObservation; sample: StatusSample; }
|
|
36
|
+
const hostScope = new AsyncLocalStorage<HostObservation>();
|
|
37
|
+
const nativeScope = new AsyncLocalStorage<NativeObservation>();
|
|
38
|
+
|
|
39
|
+
// Observer failures must never replace the operation's result or exception.
|
|
40
|
+
function safely<T>(run: () => T): T | undefined {
|
|
41
|
+
try { return run(); } catch { return undefined; }
|
|
42
|
+
}
|
|
43
|
+
function now(host: HostObservation): number {
|
|
44
|
+
const value = safely(host.clock);
|
|
45
|
+
if (value !== undefined && Number.isFinite(value) && value >= 0) return value;
|
|
46
|
+
host.summary.clock_unavailable = true;
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
function elapsed(start: number, end: number): number { return Math.max(0, end - start); }
|
|
50
|
+
|
|
51
|
+
// Failure/v2 codes and next_action are extensible strings, not safe diagnostic
|
|
52
|
+
// text. Only recognized values enter this summary; no messages or raw envelopes.
|
|
53
|
+
const CLI_CODES = new Set(["unavailable", "timeout", "non-zero", "signal", "unexpected-stderr", "output-limit", "empty-output", "malformed-json", "schema-incompatible", "identity-mismatch", "version-incompatible", "cancelled", "package-local-binary-missing", "unsupported-transition-operation"]);
|
|
54
|
+
const NATIVE_CODES = new Set(["operation_timeout", "invalid_request", "binding_revision_conflict", "internal_error", "cancelled"]);
|
|
55
|
+
function outcome(error: unknown): Outcome {
|
|
56
|
+
return safely((): Outcome => {
|
|
57
|
+
if (typeof error !== "object" || error === null) return { kind: "unknown" };
|
|
58
|
+
const value = error as { name?: unknown; code?: unknown; failureEnvelope?: Record<string, unknown> };
|
|
59
|
+
if (value.name === "NativeReviewCliError") return { kind: "cli_error", code: typeof value.code === "string" && CLI_CODES.has(value.code) ? value.code : "other" };
|
|
60
|
+
if (value.name !== "NativeReviewIntegrationError" || !value.failureEnvelope) return { kind: "unknown" };
|
|
61
|
+
const failure = value.failureEnvelope;
|
|
62
|
+
const result: Outcome = { kind: "native_failure", code: typeof failure.code === "string" && NATIVE_CODES.has(failure.code) ? failure.code : "other" };
|
|
63
|
+
if (["preflight", "pre_native", "native_running", "native_committed", "reconciliation"].includes(String(failure.phase))) result.phase = String(failure.phase);
|
|
64
|
+
if (["not_started", "committed", "unknown"].includes(String(failure.mutationOutcome))) result.mutation_outcome = String(failure.mutationOutcome);
|
|
65
|
+
if (typeof failure.retrySafe === "boolean") result.retry_safe = failure.retrySafe;
|
|
66
|
+
if (["stop", "review.status"].includes(String(failure.nextAction))) result.next_action = String(failure.nextAction);
|
|
67
|
+
return result;
|
|
68
|
+
}) ?? { kind: "unknown" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The next already-authorized STATUS-bearing tool call consumes the arm. */
|
|
72
|
+
export class StatusTimingDiagnostics {
|
|
73
|
+
private armed = false;
|
|
74
|
+
private epoch = 0;
|
|
75
|
+
private manager: ExtensionContext["sessionManager"] | undefined;
|
|
76
|
+
private session: string | undefined;
|
|
77
|
+
private last: Summary | undefined;
|
|
78
|
+
private readonly active = new Set<HostObservation>();
|
|
79
|
+
private readonly clock: () => number;
|
|
80
|
+
constructor(clock = () => performance.now()) { this.clock = clock; }
|
|
81
|
+
|
|
82
|
+
reset(ctx?: ExtensionContext): void {
|
|
83
|
+
this.epoch++;
|
|
84
|
+
this.armed = false;
|
|
85
|
+
this.last = undefined;
|
|
86
|
+
for (const host of this.active) host.summary.statuses.length = 0;
|
|
87
|
+
this.active.clear();
|
|
88
|
+
this.manager = ctx?.sessionManager;
|
|
89
|
+
this.session = safely(() => ctx?.sessionManager.getSessionId());
|
|
90
|
+
}
|
|
91
|
+
private matches(ctx: ExtensionContext): boolean {
|
|
92
|
+
return safely(() => this.manager === ctx.sessionManager && !!this.session && this.session === ctx.sessionManager.getSessionId()) === true;
|
|
93
|
+
}
|
|
94
|
+
command(args: string, ctx: ExtensionContext): void {
|
|
95
|
+
if (!this.matches(ctx)) this.reset(ctx);
|
|
96
|
+
const action = args.trim() || "show";
|
|
97
|
+
if (action === "enable") {
|
|
98
|
+
this.reset(ctx);
|
|
99
|
+
this.armed = !!this.session;
|
|
100
|
+
ctx.ui.notify(this.armed
|
|
101
|
+
? "STATUS timing armed for the next separately authorized STATUS-bearing tool call. No STATUS is invoked or retried. Use /gentle:status-timing show to consult the completed summary."
|
|
102
|
+
: "STATUS timing requires a live session.", "info");
|
|
103
|
+
} else if (action === "disable") {
|
|
104
|
+
this.reset(ctx);
|
|
105
|
+
ctx.ui.notify("STATUS timing disabled; in-memory observations cleared.", "info");
|
|
106
|
+
} else if (action === "show") {
|
|
107
|
+
ctx.ui.notify(this.last ? JSON.stringify(this.last, null, 2) : `No completed STATUS timing summary. Diagnostic is ${this.armed ? "armed" : "off"}.`, "info");
|
|
108
|
+
} else {
|
|
109
|
+
ctx.ui.notify("Unknown /gentle:status-timing sub-action. Use enable, disable, or show.", "warning");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
tool<TParams extends TSchema, TDetails>(definition: ToolDefinition<TParams, TDetails>): ToolDefinition<TParams, TDetails> {
|
|
113
|
+
return {
|
|
114
|
+
...definition,
|
|
115
|
+
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
116
|
+
const run = () => definition.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
117
|
+
if (!this.matches(ctx)) this.reset(ctx);
|
|
118
|
+
if (!this.armed || this.active.size >= MAX_ACTIVE_OBSERVATIONS) return run();
|
|
119
|
+
const epoch = this.epoch;
|
|
120
|
+
const host: HostObservation = {
|
|
121
|
+
clock: this.clock,
|
|
122
|
+
current: () => this.epoch === epoch && this.matches(ctx),
|
|
123
|
+
claim: () => {
|
|
124
|
+
if (!host.current() || !this.armed) return false;
|
|
125
|
+
this.armed = false;
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
claimed: false, started: 0, lastNativeEnd: 0,
|
|
129
|
+
summary: { schema: "gentle-pi.status-timing/v1", host_total_ms: 0, dispatch_ms: 0, sidebar_ms: 0, completion_ms: 0, truncated: false, statuses: [] },
|
|
130
|
+
};
|
|
131
|
+
this.active.add(host);
|
|
132
|
+
host.started = now(host);
|
|
133
|
+
return hostScope.run(host, async () => {
|
|
134
|
+
try { return await run(); }
|
|
135
|
+
finally {
|
|
136
|
+
this.active.delete(host);
|
|
137
|
+
safely(() => {
|
|
138
|
+
if (!host.claimed || !host.current()) return;
|
|
139
|
+
const ended = now(host);
|
|
140
|
+
host.summary.host_total_ms = elapsed(host.started, ended);
|
|
141
|
+
host.summary.completion_ms = elapsed(host.lastNativeEnd, ended);
|
|
142
|
+
this.last = host.summary;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Only called by the existing STATUS entry points; never invokes one itself. */
|
|
152
|
+
export async function observeNativeStatus<T>(timeoutMs: number, run: () => Promise<T>): Promise<T> {
|
|
153
|
+
const host = hostScope.getStore();
|
|
154
|
+
if (!host || !safely(host.current)) return run();
|
|
155
|
+
if (!host.claimed) {
|
|
156
|
+
if (!safely(host.claim)) return run();
|
|
157
|
+
host.claimed = true;
|
|
158
|
+
host.summary.dispatch_ms = elapsed(host.started, now(host));
|
|
159
|
+
}
|
|
160
|
+
const sample: StatusSample = { timeout_ms: timeoutMs, total_ms: 0, resolution_ms: 0, adapter_ms: 0, decode_ms: 0, outcome: { kind: "success" } };
|
|
161
|
+
if (host.summary.statuses.length < MAX_STATUSES) host.summary.statuses.push(sample);
|
|
162
|
+
else host.summary.truncated = true;
|
|
163
|
+
const started = now(host);
|
|
164
|
+
return nativeScope.run({ host, sample }, async () => {
|
|
165
|
+
try { return await run(); }
|
|
166
|
+
catch (error) { safely(() => { sample.outcome = outcome(error); }); throw error; }
|
|
167
|
+
finally {
|
|
168
|
+
safely(() => {
|
|
169
|
+
const ended = now(host);
|
|
170
|
+
sample.total_ms = elapsed(started, ended);
|
|
171
|
+
host.lastNativeEnd = ended;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function activeNative(): NativeObservation | undefined {
|
|
178
|
+
const observation = nativeScope.getStore();
|
|
179
|
+
return observation && safely(observation.host.current) ? observation : undefined;
|
|
180
|
+
}
|
|
181
|
+
export function measureStatusSync<T>(stage: "resolution" | "decode", run: () => T): T {
|
|
182
|
+
const observation = activeNative();
|
|
183
|
+
if (!observation) return run();
|
|
184
|
+
const started = now(observation.host);
|
|
185
|
+
try { return run(); }
|
|
186
|
+
finally { safely(() => { observation.sample[`${stage}_ms`] += elapsed(started, now(observation.host)); }); }
|
|
187
|
+
}
|
|
188
|
+
export async function measureStatusAsync<T>(stage: "adapter", run: () => Promise<T>): Promise<T> {
|
|
189
|
+
const observation = activeNative();
|
|
190
|
+
if (!observation) return run();
|
|
191
|
+
const started = now(observation.host);
|
|
192
|
+
try { return await run(); }
|
|
193
|
+
finally { safely(() => { observation.sample[`${stage}_ms`] += elapsed(started, now(observation.host)); }); }
|
|
194
|
+
}
|
|
195
|
+
export function measureStatusSidebar<T>(run: () => T): T {
|
|
196
|
+
const host = hostScope.getStore();
|
|
197
|
+
if (!host || !safely(host.current)) return run();
|
|
198
|
+
const started = now(host);
|
|
199
|
+
try { return run(); }
|
|
200
|
+
finally { safely(() => { host.summary.sidebar_ms += elapsed(started, now(host)); }); }
|
|
201
|
+
}
|
package/lib/telemetry-trigger.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import { gentleAiIsolationEnv } from "./gentle-ai-binary.ts";
|
|
2
3
|
|
|
3
4
|
// gentle-ai#4309 owns anonymous usage telemetry end to end: install and
|
|
4
5
|
// heartbeat events, the notice, every kill switch, and the wire contract.
|
|
@@ -128,7 +129,8 @@ export function spawnTelemetryTrigger(options: SpawnTelemetryTriggerOptions): Te
|
|
|
128
129
|
try {
|
|
129
130
|
const child = spawnImpl(options.executable, ["telemetry", "trigger", "--json"], {
|
|
130
131
|
cwd: options.cwd,
|
|
131
|
-
|
|
132
|
+
// S9: a bundled install keeps Gentle AI's telemetry state inside its prefix.
|
|
133
|
+
env: gentleAiIsolationEnv(options.env),
|
|
132
134
|
detached: true,
|
|
133
135
|
windowsHide: true,
|
|
134
136
|
stdio: "ignore",
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { CURSOR_MARKER, Editor, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
|
|
4
|
-
// Private shape audited against @earendil-works/pi-tui 0.99.1, 0.99.2 and 1.
|
|
4
|
+
// Private shape audited against @earendil-works/pi-tui 0.99.1, 0.99.2, 1.0.0 and 1.1.0:
|
|
5
5
|
// editor.js state/pastes/history, sticky cursor reset, layoutText/render geometry,
|
|
6
6
|
// autocomplete cancellation and { state, pastes, pasteCounter } undo restoration;
|
|
7
7
|
// undo-stack.js clone-on-push/pop snapshots. The two 0.99 releases ship identical
|
|
8
8
|
// editor/undo-stack sources; 1.0.0 is a touched-contract audit, not byte identity.
|
|
9
|
-
//
|
|
9
|
+
// The 1.1.0 touched-contract audit retains clone-on-push snapshots with the
|
|
10
|
+
// paste registry, cursor resets, layoutText/render geometry and history exit.
|
|
11
|
+
// cancelAutocomplete aborts/debounces requests before clearing its UI;
|
|
12
|
+
// the adapter still calls that host-owned method, never mutating its request state.
|
|
13
|
+
// Actual installed bundled/unbundled tests exercise identity, edit/undo, paste,
|
|
10
14
|
// selection, wrapping/scroll and autocomplete; fabricated metadata tests only
|
|
11
15
|
// prove exact-version admission for the older audited releases.
|
|
12
16
|
// Never silently adapt another build: undo also owns the paste registry.
|
|
@@ -47,7 +51,7 @@ interface PrivateEditor {
|
|
|
47
51
|
|
|
48
52
|
// The single audited-release source for the adapter and Gentle Shell's runtime
|
|
49
53
|
// identity gates. Add a release only after re-auditing the files named above.
|
|
50
|
-
export const AUDITED_PI_EDITOR_VERSIONS = Object.freeze(["0.99.1", "0.99.2", "1.0.0"] as const);
|
|
54
|
+
export const AUDITED_PI_EDITOR_VERSIONS = Object.freeze(["0.99.1", "0.99.2", "1.0.0", "1.1.0"] as const);
|
|
51
55
|
export type AuditedPiEditorVersion = (typeof AUDITED_PI_EDITOR_VERSIONS)[number];
|
|
52
56
|
|
|
53
57
|
export function isAuditedPiEditorVersion(version: unknown): version is AuditedPiEditorVersion {
|
|
@@ -18,6 +18,8 @@ export interface VisualVisibility {
|
|
|
18
18
|
todo: boolean;
|
|
19
19
|
usageCost: boolean;
|
|
20
20
|
modelDetails: boolean;
|
|
21
|
+
/** High-context warning notice; independent from audio notifications. */
|
|
22
|
+
contextWarning: boolean;
|
|
21
23
|
}
|
|
22
24
|
export interface VisualSettings {
|
|
23
25
|
statusPlacement: StatusPlacement;
|
|
@@ -29,13 +31,13 @@ export const DEFAULT_VISUAL_SETTINGS: VisualSettings = {
|
|
|
29
31
|
statusPlacement: STATUS_PLACEMENT.AUTO,
|
|
30
32
|
headerPlacement: HEADER_PLACEMENT.TOP,
|
|
31
33
|
density: DENSITY.COMFORTABLE,
|
|
32
|
-
visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true },
|
|
34
|
+
visibility: { changes: true, rdd: true, agents: true, todo: true, usageCost: true, modelDetails: true, contextWarning: true },
|
|
33
35
|
};
|
|
34
36
|
/** Sections row order; `rdd` follows `changes` as the RDD group follows Changes in the Status card. */
|
|
35
|
-
export const VISUAL_SECTION_KEYS = ["changes", "rdd", "agents", "todo", "usageCost", "modelDetails"] as const satisfies readonly (keyof VisualVisibility)[];
|
|
36
|
-
/** Keys added after v1 shipped: stored settings may omit them together, and they default to shown. */
|
|
37
|
-
const ADDED_SECTION_KEYS: readonly (keyof VisualVisibility)[] = ["rdd"];
|
|
38
|
-
const
|
|
37
|
+
export const VISUAL_SECTION_KEYS = ["changes", "rdd", "agents", "todo", "usageCost", "modelDetails", "contextWarning"] as const satisfies readonly (keyof VisualVisibility)[];
|
|
38
|
+
/** Keys added after v1 shipped: stored settings may omit them together or one at a time, and they default to shown. */
|
|
39
|
+
const ADDED_SECTION_KEYS: readonly (keyof VisualVisibility)[] = ["rdd", "contextWarning"];
|
|
40
|
+
const BASE_SECTION_KEYS = VISUAL_SECTION_KEYS.filter((key) => !ADDED_SECTION_KEYS.includes(key));
|
|
39
41
|
interface VisualOptions { gentlePiConfigHome?: string }
|
|
40
42
|
export interface VisualResolution {
|
|
41
43
|
settings: VisualSettings;
|
|
@@ -64,13 +66,22 @@ export function isVisualSettings(value: unknown): value is VisualSettings {
|
|
|
64
66
|
&& keysMatch(visibility, VISUAL_SECTION_KEYS)
|
|
65
67
|
&& Object.values(visibility).every((item) => typeof item === "boolean");
|
|
66
68
|
}
|
|
67
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Reader-side validation: accepts the current shape plus every historical one.
|
|
71
|
+
* Every key must be known, every base key present, and every present value a
|
|
72
|
+
* boolean; missing added keys (`rdd`, `contextWarning`) then default to shown,
|
|
73
|
+
* while present values (including `rdd: false`) are preserved. Writers stay strict.
|
|
74
|
+
*/
|
|
68
75
|
export function normalizeVisualSettings(value: unknown): VisualSettings | undefined {
|
|
69
76
|
if (!record(value) || !record(value.visibility)) return undefined;
|
|
70
77
|
const visibility = value.visibility;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
if (!Object.keys(visibility).every((key) => (VISUAL_SECTION_KEYS as readonly string[]).includes(key))) return undefined;
|
|
79
|
+
if (!BASE_SECTION_KEYS.every((key) => Object.hasOwn(visibility, key))) return undefined;
|
|
80
|
+
// A key that is present is authoritative: only a missing added key may default to shown.
|
|
81
|
+
if (!Object.keys(visibility).every((key) => typeof visibility[key] === "boolean")) return undefined;
|
|
82
|
+
const candidate = keysMatch(visibility, VISUAL_SECTION_KEYS)
|
|
83
|
+
? value
|
|
84
|
+
: { ...value, visibility: Object.fromEntries(VISUAL_SECTION_KEYS.map((key) => [key, visibility[key] ?? true])) };
|
|
74
85
|
return isVisualSettings(candidate) ? candidate : undefined;
|
|
75
86
|
}
|
|
76
87
|
export function parseVisualSettingsFile(raw: string): VisualSettings | undefined {
|