gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { Container, Markdown, Spacer, TuiAltScreen, wrapTextWithAnsi, type Component, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
5
|
+
import { createChatViewport } from "../node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/chat-viewport.js";
|
|
6
|
+
import { installSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
7
|
+
import { sidebarHeader, sidebarPart } from "../lib/shell-sidebar.ts";
|
|
8
|
+
|
|
9
|
+
// Diagnostic baseline, not a speed gate. Keep the document settled: the same
|
|
10
|
+
// prebuilt lines and component survive every native fullscreen scroll frame.
|
|
11
|
+
const theme = { fg: (_color: string, text: string) => text, bold: (text: string) => text };
|
|
12
|
+
const strip = (text: string) => text.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").trimEnd();
|
|
13
|
+
const median = (samples: number[]) => {
|
|
14
|
+
const ordered = [...samples].sort((a, b) => a - b);
|
|
15
|
+
return (ordered[Math.floor((ordered.length - 1) / 2)]! + ordered[Math.floor(ordered.length / 2)]!) / 2;
|
|
16
|
+
};
|
|
17
|
+
const leaf = (rows: string[]): Component => ({ render: () => rows, invalidate() {} });
|
|
18
|
+
|
|
19
|
+
type Fixture = ReturnType<typeof createFixture>;
|
|
20
|
+
type Workload = "plain" | "ansi" | "markdown";
|
|
21
|
+
const markdownTheme = Object.fromEntries([
|
|
22
|
+
"heading", "link", "linkUrl", "code", "codeBlock", "codeBlockBorder", "quote", "quoteBorder",
|
|
23
|
+
"hr", "listBullet", "bold", "italic", "strikethrough", "underline",
|
|
24
|
+
].map((key) => [key, (text: string) => `\x1b[36m${text}\x1b[39m`])) as unknown as MarkdownTheme;
|
|
25
|
+
|
|
26
|
+
function createFixture(columns: number, rowCount: number, sidebar: boolean, workload: Workload = "plain") {
|
|
27
|
+
const rows = Array.from({ length: rowCount }, (_, index) => `ROW-${String(index).padStart(5, "0")} settled transcript`);
|
|
28
|
+
const counts = { transcript: 0, header: 0, rail: 0, markdownMiss: 0, widthBuild: 0, lastWidth: 0 };
|
|
29
|
+
const markdown = workload === "markdown" ? new Markdown(
|
|
30
|
+
rows.map((row) => `${row} **bold** and *italic* with a longer wrapped passage for the real Markdown parser.`).join("\n\n"),
|
|
31
|
+
0, 0, markdownTheme, undefined,
|
|
32
|
+
{ transform: (source) => { counts.markdownMiss++; return source; } },
|
|
33
|
+
) : undefined;
|
|
34
|
+
const wrapped = new Map<number, string[]>();
|
|
35
|
+
const document: Component = {
|
|
36
|
+
render: (width) => {
|
|
37
|
+
counts.transcript++;
|
|
38
|
+
counts.lastWidth = width;
|
|
39
|
+
if (markdown) return markdown.render(width);
|
|
40
|
+
if (workload === "plain") return rows;
|
|
41
|
+
let result = wrapped.get(width);
|
|
42
|
+
if (!result) {
|
|
43
|
+
counts.widthBuild++;
|
|
44
|
+
result = rows.flatMap((row) => wrapTextWithAnsi(`\x1b[35m${row} ${"ANSI styled long content ".repeat(8)}\x1b[39m`, width));
|
|
45
|
+
wrapped.set(width, result);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
},
|
|
49
|
+
invalidate: () => { markdown?.invalidate(); wrapped.clear(); },
|
|
50
|
+
};
|
|
51
|
+
const terminal = {
|
|
52
|
+
start() {}, stop() {}, async drainInput() {}, write() {},
|
|
53
|
+
get columns() { return columns; }, get rows() { return 24; },
|
|
54
|
+
moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {}, setTitle() {}, setProgress() {},
|
|
55
|
+
kittyProtocolActive: false, getBufferedInput() { return ""; }, hasPendingInput() { return false; },
|
|
56
|
+
} as never;
|
|
57
|
+
const above = new Container();
|
|
58
|
+
above.addChild(new Spacer(1));
|
|
59
|
+
const footer = new Container();
|
|
60
|
+
const viewport = createChatViewport({
|
|
61
|
+
document, pendingMessages: new Container(), status: new Container(), widgetsAbove: above,
|
|
62
|
+
editor: leaf(["╭─ editor ─╮", "│ prompt │", "╰──────────╯"]),
|
|
63
|
+
widgetsBelow: new Container(), footer, scrollbar: "hidden",
|
|
64
|
+
});
|
|
65
|
+
const tui = new TuiAltScreen(terminal, false);
|
|
66
|
+
tui.setLayoutRoot(viewport.root);
|
|
67
|
+
let dispose = () => {};
|
|
68
|
+
if (sidebar) footer.addChild(sidebarPart(tui, "footer", { render: () => { counts.rail++; return ["Status card"]; }, invalidate() {} }));
|
|
69
|
+
else footer.addChild(leaf(["Status card"]));
|
|
70
|
+
// Main's narrow fallback keeps the header even without a rail. Give the
|
|
71
|
+
// width-matched fallback control the same header and native layout hook.
|
|
72
|
+
if (sidebar || columns === 100) {
|
|
73
|
+
sidebarHeader(tui, { render: (width: number) => { counts.header++; return [`HEADER ${width}`]; }, invalidate() {} });
|
|
74
|
+
dispose = installSidebar(tui, theme);
|
|
75
|
+
}
|
|
76
|
+
tui.start();
|
|
77
|
+
const render = () => {
|
|
78
|
+
// Use the real alt-screen frame pipeline, not a hand-built layout node.
|
|
79
|
+
(tui as unknown as { doRender(): void }).doRender();
|
|
80
|
+
return [...((tui as unknown as { previousScreen: string[] }).previousScreen ?? [])];
|
|
81
|
+
};
|
|
82
|
+
render(); // establish native viewport height and scroll bounds before sampling
|
|
83
|
+
return { columns, rowCount, sidebar, workload, counts, document, viewport, render, stop: () => { dispose(); tui.stop(); } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function measure(fixture: Fixture, invalidate = false, anchors?: number[]) {
|
|
87
|
+
const { transcript, header, rail, markdownMiss, widthBuild } = fixture.counts;
|
|
88
|
+
fixture.viewport.transcript.scrollToEnd();
|
|
89
|
+
const maxTop = fixture.viewport.transcript.scrollTop;
|
|
90
|
+
const positions = anchors ?? [0, Math.floor(maxTop / 4), Math.floor(maxTop / 2), Math.floor(maxTop * 3 / 4), maxTop];
|
|
91
|
+
const samples: number[] = [];
|
|
92
|
+
const cpuSamples: number[] = [];
|
|
93
|
+
const screens: string[][] = [];
|
|
94
|
+
const rawScreens: string[][] = [];
|
|
95
|
+
for (let repeat = 0; repeat < 3; repeat++) {
|
|
96
|
+
for (const position of positions) {
|
|
97
|
+
fixture.viewport.transcript.scrollTo(position, { disableFollow: true });
|
|
98
|
+
if (invalidate) fixture.document.invalidate();
|
|
99
|
+
const cpuStart = process.cpuUsage();
|
|
100
|
+
const start = performance.now();
|
|
101
|
+
const rawScreen = fixture.render();
|
|
102
|
+
samples.push(performance.now() - start);
|
|
103
|
+
const cpu = process.cpuUsage(cpuStart);
|
|
104
|
+
cpuSamples.push((cpu.user + cpu.system) / 1000);
|
|
105
|
+
rawScreens.push(rawScreen);
|
|
106
|
+
screens.push(rawScreen.map(strip));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
positions, screens, rawScreens, samplesMs: samples.map((time) => Number(time.toFixed(3))),
|
|
111
|
+
medianMs: Number(median(samples).toFixed(3)), medianCpuMs: Number(median(cpuSamples).toFixed(3)),
|
|
112
|
+
renderCalls: { transcript: fixture.counts.transcript - transcript, header: fixture.counts.header - header, rail: fixture.counts.rail - rail,
|
|
113
|
+
markdownMiss: fixture.counts.markdownMiss - markdownMiss, widthBuild: fixture.counts.widthBuild - widthBuild },
|
|
114
|
+
contentWidth: fixture.counts.lastWidth,
|
|
115
|
+
viewportHeight: fixture.viewport.transcript.viewportHeight,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// At narrow widths the top header is the only status row: against the control
|
|
120
|
+
// that still docks "Status card", every frame keeps the same header and
|
|
121
|
+
// transcript bytes, the same spacer and editor bytes, and gives the reclaimed
|
|
122
|
+
// footer row to one more transcript row.
|
|
123
|
+
const DOCK_ROWS = 4; // spacer + three editor rows
|
|
124
|
+
function assertNarrowOwnsStatus(narrow: string[][], control: string[][], label: string) {
|
|
125
|
+
assert.equal(narrow.length, control.length, `${label} samples the same frames`);
|
|
126
|
+
for (const [frame, screen] of narrow.entries()) {
|
|
127
|
+
const reference = control[frame]!;
|
|
128
|
+
assert.equal(screen.length, reference.length, `${label} keeps terminal height`);
|
|
129
|
+
assert.equal(strip(reference.at(-1)!), "Status card", `${label} control docks Status`);
|
|
130
|
+
assert.deepEqual(screen.slice(-DOCK_ROWS), reference.slice(-DOCK_ROWS - 1, -1), `${label} keeps spacer and editor bytes`);
|
|
131
|
+
const body = reference.length - DOCK_ROWS - 1;
|
|
132
|
+
assert.deepEqual(screen.slice(0, body), reference.slice(0, body), `${label} keeps header and transcript bytes`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function transcriptRows(screen: string[]) {
|
|
137
|
+
return screen.flatMap((line) => [...line.matchAll(/ROW-\d{5} settled transcript/g)].map(([match]) => match));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
test("characterize settled native fullscreen scroll frames with and without the sidebar", () => {
|
|
141
|
+
for (const rowCount of [120, 1200]) {
|
|
142
|
+
const fixtures = [createFixture(140, rowCount, true), createFixture(140, rowCount, false), createFixture(100, rowCount, true), createFixture(100, rowCount, false)];
|
|
143
|
+
try {
|
|
144
|
+
const desktop = measure(fixtures[0]!);
|
|
145
|
+
// The narrow sidebar reclaims the footer row, so its taller viewport has a
|
|
146
|
+
// different maxTop; the narrow control samples its exact anchors.
|
|
147
|
+
const narrow = measure(fixtures[2]!);
|
|
148
|
+
const results = [desktop, measure(fixtures[1]!, false, desktop.positions), narrow, measure(fixtures[3]!, false, narrow.positions)];
|
|
149
|
+
for (let i = 0; i < fixtures.length; i++) {
|
|
150
|
+
const fixture = fixtures[i]!;
|
|
151
|
+
const result = results[i]!;
|
|
152
|
+
assert.equal(result.screens.length, result.positions.length * 3);
|
|
153
|
+
assert.equal(result.rawScreens.length, result.screens.length);
|
|
154
|
+
assert.equal(result.renderCalls.transcript, result.positions.length * 3, "every scroll traverses the settled document");
|
|
155
|
+
for (const [frame, screen] of result.screens.entries()) {
|
|
156
|
+
assert.equal(screen.length, 24, "native fullscreen retains terminal height");
|
|
157
|
+
assert.ok(screen.some((line) => line.startsWith("╭─ editor")), "editor remains visible");
|
|
158
|
+
const visible = transcriptRows(screen);
|
|
159
|
+
assert.ok(visible.length > 0, "transcript remains visible");
|
|
160
|
+
assert.equal(visible[0], `ROW-${String(result.positions[frame % result.positions.length]).padStart(5, "0")} settled transcript`, "visible scroll anchor matches requested position");
|
|
161
|
+
if (fixture.sidebar && fixture.columns === 140) {
|
|
162
|
+
assert.ok(screen.some((line) => line.includes("Status card")), "desktop rail is painted");
|
|
163
|
+
assert.equal(screen[0], "HEADER 138");
|
|
164
|
+
} else {
|
|
165
|
+
if (fixture.columns === 100) assert.equal(screen[0], "HEADER 100", "narrow fallback retains the full-width header");
|
|
166
|
+
else assert.ok(!screen.some((line) => line.startsWith("HEADER ")), "desktop sidebar-off control has no header");
|
|
167
|
+
if (fixture.sidebar) {
|
|
168
|
+
assert.equal(screen.at(-1), "╰──────────╯", "the narrow top header owns Status and reclaims the footer row");
|
|
169
|
+
assert.ok(!screen.some((line) => line.includes("Status card")), "no second status row at narrow width");
|
|
170
|
+
} else {
|
|
171
|
+
assert.equal(screen.at(-1), "Status card", "footer stays in the native dock");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Narrow installation only swaps the footer row for one more transcript row.
|
|
177
|
+
assertNarrowOwnsStatus(results[2]!.rawScreens, results[3]!.rawScreens, "narrow fallback");
|
|
178
|
+
// Desktop compositions have different geometry, but share the exact
|
|
179
|
+
// transcript rows at matching scroll anchors wherever both are visible.
|
|
180
|
+
assert.deepEqual(results[0]!.positions, results[1]!.positions);
|
|
181
|
+
for (let frame = 0; frame < results[0]!.screens.length; frame++) {
|
|
182
|
+
const on = transcriptRows(results[0]!.screens[frame]!);
|
|
183
|
+
const off = transcriptRows(results[1]!.screens[frame]!);
|
|
184
|
+
assert.deepEqual(on, off.slice(0, on.length));
|
|
185
|
+
}
|
|
186
|
+
console.log("scroll baseline", JSON.stringify(fixtures.map((fixture, index) => ({
|
|
187
|
+
rows: rowCount, columns: fixture.columns, sidebar: fixture.sidebar,
|
|
188
|
+
viewportHeight: results[index]!.viewportHeight, contentWidth: results[index]!.contentWidth,
|
|
189
|
+
medianMs: results[index]!.medianMs, medianCpuMs: results[index]!.medianCpuMs,
|
|
190
|
+
samplesMs: results[index]!.samplesMs, renderCalls: results[index]!.renderCalls,
|
|
191
|
+
}))));
|
|
192
|
+
} finally {
|
|
193
|
+
for (const fixture of fixtures) fixture.stop();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("wrapped ANSI and native Markdown distinguish width composition from settled cache hits and misses", () => {
|
|
199
|
+
for (const workload of ["ansi", "markdown"] as const) {
|
|
200
|
+
for (const rowCount of [120, 1200]) {
|
|
201
|
+
// The actual desktop transcript width is discovered from the native
|
|
202
|
+
// viewport render; the no-sidebar control uses that exact width.
|
|
203
|
+
const desktop = createFixture(140, rowCount, true, workload);
|
|
204
|
+
const width = desktop.counts.lastWidth;
|
|
205
|
+
const control = createFixture(width, rowCount, false, workload);
|
|
206
|
+
const narrow = createFixture(100, rowCount, true, workload);
|
|
207
|
+
const narrowControl = createFixture(100, rowCount, false, workload);
|
|
208
|
+
const fixtures = [desktop, control, narrow, narrowControl];
|
|
209
|
+
try {
|
|
210
|
+
const desktopHit = measure(desktop);
|
|
211
|
+
const narrowHit = measure(narrow);
|
|
212
|
+
const hits = [desktopHit, measure(control, false, desktopHit.positions), narrowHit, measure(narrowControl, false, narrowHit.positions)];
|
|
213
|
+
const misses = fixtures.map((fixture, index) => measure(fixture, true, hits[index]!.positions));
|
|
214
|
+
assert.equal(width, 87, "desktop transcript geometry stays fixed");
|
|
215
|
+
assert.equal(hits[0]!.contentWidth, hits[1]!.contentWidth);
|
|
216
|
+
for (let index = 0; index < fixtures.length; index++) {
|
|
217
|
+
const hit = hits[index]!;
|
|
218
|
+
const miss = misses[index]!;
|
|
219
|
+
assert.equal(hit.renderCalls.transcript, 15);
|
|
220
|
+
assert.equal(miss.renderCalls.transcript, 15);
|
|
221
|
+
assert.equal(hit.rawScreens.length, 15);
|
|
222
|
+
assert.equal(miss.rawScreens.length, 15);
|
|
223
|
+
assert.deepEqual(hit.rawScreens, miss.rawScreens, "invalidating only the document preserves frame bytes");
|
|
224
|
+
if (workload === "markdown") {
|
|
225
|
+
assert.equal(hit.renderCalls.markdownMiss, 0, "settled Markdown is served from its instance cache");
|
|
226
|
+
assert.equal(miss.renderCalls.markdownMiss, 15, "instance invalidation forces a parse each frame");
|
|
227
|
+
} else {
|
|
228
|
+
assert.equal(hit.renderCalls.widthBuild, 0, "settled ANSI wrapping is width-cached");
|
|
229
|
+
assert.equal(miss.renderCalls.widthBuild, 15);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
assertNarrowOwnsStatus(hits[2]!.rawScreens, hits[3]!.rawScreens, "narrow fallback");
|
|
233
|
+
assertNarrowOwnsStatus(misses[2]!.rawScreens, misses[3]!.rawScreens, "invalidated narrow fallback");
|
|
234
|
+
// Desktop header and dock are distinct; compare only the actual
|
|
235
|
+
// transcript viewport at equal width and scroll position.
|
|
236
|
+
assert.deepEqual(hits[0]!.positions, hits[1]!.positions);
|
|
237
|
+
for (const results of [hits, misses]) {
|
|
238
|
+
const height = Math.min(results[0]!.viewportHeight, results[1]!.viewportHeight);
|
|
239
|
+
const transcript = (screen: string[], start: number) => screen.slice(start, start + height).map((line) => line.slice(0, width).trimEnd());
|
|
240
|
+
for (let frame = 0; frame < results[0]!.screens.length; frame++) {
|
|
241
|
+
assert.deepEqual(transcript(results[0]!.screens[frame]!, 1), transcript(results[1]!.screens[frame]!, 0), "width-matched transcript pixels agree");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
console.log("scroll workload", JSON.stringify(fixtures.map((fixture, index) => ({
|
|
245
|
+
workload, rows: rowCount, columns: fixture.columns, contentWidth: hits[index]!.contentWidth,
|
|
246
|
+
sidebar: fixture.sidebar, viewportHeight: hits[index]!.viewportHeight,
|
|
247
|
+
hit: { medianMs: hits[index]!.medianMs, medianCpuMs: hits[index]!.medianCpuMs, samplesMs: hits[index]!.samplesMs, calls: hits[index]!.renderCalls },
|
|
248
|
+
miss: { medianMs: misses[index]!.medianMs, medianCpuMs: misses[index]!.medianCpuMs, samplesMs: misses[index]!.samplesMs, calls: misses[index]!.renderCalls },
|
|
249
|
+
}))));
|
|
250
|
+
} finally {
|
|
251
|
+
for (const fixture of fixtures) fixture.stop();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
package/tests/shell-todo.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
2
|
+
import test, { after, before, type TestContext } from "node:test";
|
|
3
3
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import {
|
|
5
5
|
applyTodo,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
todoSummary,
|
|
13
13
|
type TodoState,
|
|
14
14
|
} from "../lib/shell-todo.ts";
|
|
15
|
+
import { CARD_STYLE, cardStyle, setCardStyle, type CardStyle } from "../lib/shell-card.ts";
|
|
15
16
|
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
16
17
|
|
|
17
18
|
// Gentle Todo: a task list the model rewrites as it works. The reducer is
|
|
@@ -27,6 +28,42 @@ const plainTheme = {
|
|
|
27
28
|
},
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
// The card style defaults to float; these assertions pin the outlined (neon)
|
|
32
|
+
// panels unless a test switches the style itself.
|
|
33
|
+
const initialCardStyle = cardStyle();
|
|
34
|
+
before(() => setCardStyle(CARD_STYLE.NEON));
|
|
35
|
+
after(() => setCardStyle(initialCardStyle));
|
|
36
|
+
|
|
37
|
+
function useCardStyle(t: TestContext, style: CardStyle): void {
|
|
38
|
+
const found = cardStyle();
|
|
39
|
+
t.after(() => setCardStyle(found));
|
|
40
|
+
setCardStyle(style);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const BG_OPEN = "\x1b[48;5;22m";
|
|
44
|
+
const BG_CLOSE = "\x1b[49m";
|
|
45
|
+
|
|
46
|
+
/** The same theme with a background, so the float style applies (without one panels keep the frame). */
|
|
47
|
+
function withBackground<T extends object>(theme: T): T & { bg(color: string, text: string): string } {
|
|
48
|
+
return { ...theme, bg: (_color: string, text: string) => `${BG_OPEN}${text}${BG_CLOSE}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Float panel rows: a painted panel inside transparent one-column margins, between padding rows that keep the accent bar. */
|
|
52
|
+
function assertFloatRows(lines: readonly string[], width: number): void {
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
assert.equal(visibleWidth(line), width, `"${stripAnsi(line)}" is not ${width} wide`);
|
|
55
|
+
assert.ok(line.startsWith(` ${BG_OPEN}`) && line.endsWith(`${BG_CLOSE} `), `painted inside the margins: ${JSON.stringify(line)}`);
|
|
56
|
+
}
|
|
57
|
+
const padding = ` ▎${" ".repeat(width - 3)} `;
|
|
58
|
+
assert.equal(stripAnsi(lines[0]!), padding, "a padding row with the accent bar sits above the header");
|
|
59
|
+
assert.equal(stripAnsi(lines.at(-1)!), padding, "a padding row with the accent bar replaces the bottom rule");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The text of a body row, without the neon side rails or the float accent bar. */
|
|
63
|
+
function bodyText(row: string): string {
|
|
64
|
+
return stripAnsi(row).replace(/^ ?[│▎] /u, "").replace(/ ?│? ?$/u, "").trimEnd();
|
|
65
|
+
}
|
|
66
|
+
|
|
30
67
|
function seeded(): TodoState {
|
|
31
68
|
return applyTodo(
|
|
32
69
|
emptyTodo(),
|
|
@@ -130,6 +167,34 @@ test("todoPromptBlock lists open work with the rules, and stays silent when ther
|
|
|
130
167
|
assert.equal(todoPromptBlock(allDone, 0), undefined);
|
|
131
168
|
});
|
|
132
169
|
|
|
170
|
+
test("collapsed long tasks occupy one body line, including stale state, without losing task data", (t) => {
|
|
171
|
+
const title = "界🙂 long title ".repeat(100);
|
|
172
|
+
const note = "long description ".repeat(100);
|
|
173
|
+
for (const style of [CARD_STYLE.NEON, CARD_STYLE.FLOAT]) {
|
|
174
|
+
useCardStyle(t, style);
|
|
175
|
+
for (const status of ["in_progress", "pending", "blocked"]) {
|
|
176
|
+
const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title, note, status }] }, 1).state;
|
|
177
|
+
const snapshot = structuredClone(state);
|
|
178
|
+
for (const width of [16, 40, 70]) {
|
|
179
|
+
for (const staleTurns of [0, 4]) {
|
|
180
|
+
const rows = renderTodoCard(state, withBackground(plainTheme), width, { collapsed: true, staleTurns, scrollable: true });
|
|
181
|
+
assert.equal(rows.length, style === CARD_STYLE.FLOAT ? 5 : 3);
|
|
182
|
+
assert.ok(rows.some((row) => stripAnsi(row).includes("…")), "long text signals truncation");
|
|
183
|
+
for (const row of rows) assert.equal(visibleWidth(row), width);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
assert.deepEqual(state, snapshot);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("non-scrollable Todo caps physical wrapped rows, not task count", () => {
|
|
192
|
+
const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "long title ".repeat(300), status: "in_progress", note: "long note ".repeat(300) }] }, 1).state;
|
|
193
|
+
const rows = renderTodoCard(state, plainTheme, 40, { collapsed: false, staleTurns: 4 });
|
|
194
|
+
assert.ok(rows.length <= 14);
|
|
195
|
+
assert.match(stripAnsi(rows.at(-2)!), /…/);
|
|
196
|
+
});
|
|
197
|
+
|
|
133
198
|
test("renderTodoCard draws the framed list with status glyphs and keeps every line at width", () => {
|
|
134
199
|
const lines = renderTodoCard(seeded(), plainTheme, 60, { collapsed: false, staleTurns: 0, collapseKey: "ctrl+shift+t" });
|
|
135
200
|
for (const line of lines) assert.equal(visibleWidth(line), 60, `"${stripAnsi(line)}" is not 60 wide`);
|
|
@@ -172,8 +237,8 @@ test("renderTodoCard keeps the configured collapse shortcut in the header while
|
|
|
172
237
|
|
|
173
238
|
const staleCollapsed = renderTodoCard(seeded(), plainTheme, 70, { collapsed: true, staleTurns: 2, collapseKey: "ctrl+shift+t" }).map(stripAnsi);
|
|
174
239
|
assert.match(staleCollapsed[0], /^╭─ ❀ Todos ▸ Expand · 1 of 3 ─+ ctrl\+shift\+t expand ╮$/);
|
|
175
|
-
assert.
|
|
176
|
-
assert.match(staleCollapsed[
|
|
240
|
+
assert.equal(staleCollapsed.length, 3, "staleness stays in the single collapsed body row");
|
|
241
|
+
assert.match(staleCollapsed[1], /^│ stale · 2 turns · ◐ Fix quiet tools conflict · fixing conflict +│$/);
|
|
177
242
|
|
|
178
243
|
const fallback = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "Finished first", status: "done" }, { title: "First pending" }, { title: "Later pending" }] }, 1).state;
|
|
179
244
|
assert.match(renderTodoCard(fallback, plainTheme, 70, { collapsed: true, staleTurns: 0 }).map(stripAnsi)[1], /^│ ○ First pending +│$/);
|
|
@@ -293,3 +358,142 @@ test("renderTodoCard uses custom shortcuts, omits disabled shortcuts, and remain
|
|
|
293
358
|
for (const line of lines) assert.equal(visibleWidth(line), width, `"${stripAnsi(line)}" is not ${width} wide`);
|
|
294
359
|
}
|
|
295
360
|
});
|
|
361
|
+
|
|
362
|
+
test("renderTodoCard in the float style keeps its clickable control on the header row, one row below the top padding, above a separator row", (t) => {
|
|
363
|
+
const theme = withBackground(plainTheme);
|
|
364
|
+
for (const options of [
|
|
365
|
+
{ collapsed: false, staleTurns: 0, collapseKey: "ctrl+shift+t" },
|
|
366
|
+
{ collapsed: true, staleTurns: 0, collapseKey: "ctrl+shift+t" },
|
|
367
|
+
{ collapsed: false, staleTurns: 4 },
|
|
368
|
+
{ collapsed: false, staleTurns: 0, scrollable: true },
|
|
369
|
+
]) {
|
|
370
|
+
const neon = renderTodoCard(seeded(), theme, 60, options);
|
|
371
|
+
useCardStyle(t, CARD_STYLE.FLOAT);
|
|
372
|
+
const float = renderTodoCard(seeded(), theme, 60, options);
|
|
373
|
+
setCardStyle(CARD_STYLE.NEON);
|
|
374
|
+
assert.equal(float.length, neon.length + 2, "the top padding and separator rows add two rows");
|
|
375
|
+
const action = options.collapsed ? "▸ Expand" : "▾ Collapse";
|
|
376
|
+
const hint = options.collapseKey ? `ctrl\\+shift\\+t {3}` : "";
|
|
377
|
+
assert.match(stripAnsi(float[1]!), new RegExp(`^ ▎ ❀ Todos ${action} 1 of 3 +${hint}$`), "row 1 is the clickable header");
|
|
378
|
+
assert.match(stripAnsi(float[2]!), /^ ▎ +$/, "a blank separator row follows the header");
|
|
379
|
+
assertFloatRows(float, 60);
|
|
380
|
+
assert.deepEqual(float.slice(3, -1).map(bodyText), neon.slice(1, -1).map(bodyText));
|
|
381
|
+
}
|
|
382
|
+
useCardStyle(t, CARD_STYLE.FLOAT);
|
|
383
|
+
const tagged = withBackground({ ...plainTheme, fg: (color: string, text: string) => `<${color}>${text}</${color}>` });
|
|
384
|
+
const [, header] = renderTodoCard(seeded(), tagged, 120, { collapsed: false, staleTurns: 0, hovered: true });
|
|
385
|
+
assert.match(stripAnsi(header!), /^ <border>▎<\/border> <accent>❀ Todos <[a-zA-Z]+>▾ Collapse<\/[a-zA-Z]+><\/accent> <muted>1 of 3<\/muted> +$/, "the hovered control keeps its own role and case");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("float Todos keeps the configured shortcut visible at rail width without repeating the action", (t) => {
|
|
389
|
+
useCardStyle(t, CARD_STYLE.FLOAT);
|
|
390
|
+
const theme = withBackground(plainTheme);
|
|
391
|
+
for (const collapsed of [false, true]) {
|
|
392
|
+
for (const collapseKey of ["ctrl+shift+t", "alt+t", undefined]) {
|
|
393
|
+
const rows = renderTodoCard(seeded(), theme, 48, { collapsed, staleTurns: 0, collapseKey });
|
|
394
|
+
const header = stripAnsi(rows[1]!);
|
|
395
|
+
assert.match(header, collapsed ? /▸ Expand/ : /▾ Collapse/);
|
|
396
|
+
if (collapseKey) assert.ok(header.includes(collapseKey));
|
|
397
|
+
else assert.doesNotMatch(header, /ctrl\+shift\+t|alt\+t/);
|
|
398
|
+
assert.equal(visibleWidth(rows[1]!), 48);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("a done todo wraps to the float body so the strikethrough never re-wraps", (t) => {
|
|
404
|
+
useCardStyle(t, CARD_STYLE.FLOAT);
|
|
405
|
+
const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "word ".repeat(20).trim(), status: "done" }] }, 1).state;
|
|
406
|
+
const lines = renderTodoCard(state, withBackground(plainTheme), 40, { collapsed: false, staleTurns: 0 });
|
|
407
|
+
assertFloatRows(lines, 40);
|
|
408
|
+
for (const row of lines.slice(3, -1)) assert.match(stripAnsi(row), /^ ▎ [✓ ] ~[^~]+~ +$/u, "each physical row closes its own strikethrough");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// #1814 dropped: terminal, never counts as open, reads apart from done.
|
|
412
|
+
// #1820 blocked: open but not actionable, always carries its reason.
|
|
413
|
+
|
|
414
|
+
test("applyTodo accepts dropped and blocked, maps cancel vocabulary to dropped, and names every status on an unknown one", () => {
|
|
415
|
+
const { state, error } = applyTodo(
|
|
416
|
+
emptyTodo(),
|
|
417
|
+
{ action: "write", tasks: [{ title: "Old plan", status: "dropped" }, { title: "Deploy", status: "blocked", note: "waiting for an admin" }, { title: "A", status: "cancelled" }, { title: "B", status: "canceled" }, { title: "C", status: "abandoned" }] },
|
|
418
|
+
1,
|
|
419
|
+
);
|
|
420
|
+
assert.equal(error, undefined);
|
|
421
|
+
assert.deepEqual(state.tasks.map((task) => task.status), [TODO_STATUS.DROPPED, TODO_STATUS.BLOCKED, TODO_STATUS.DROPPED, TODO_STATUS.DROPPED, TODO_STATUS.DROPPED]);
|
|
422
|
+
assert.equal(state.tasks[1].note, "waiting for an admin");
|
|
423
|
+
const bad = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "X", status: "later" }] }, 1);
|
|
424
|
+
assert.equal(bad.error, 'unknown status "later" (use pending, in_progress, blocked, done, or dropped)');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("applyTodo rejects blocked without a note and leaves the list unchanged", () => {
|
|
428
|
+
const before = seeded();
|
|
429
|
+
for (const params of [
|
|
430
|
+
{ action: "add", title: "Deploy", status: "blocked" },
|
|
431
|
+
{ action: "update", id: 3, status: "blocked" },
|
|
432
|
+
{ action: "write", tasks: [{ title: "Deploy", status: "blocked", note: " " }] },
|
|
433
|
+
]) {
|
|
434
|
+
const result = applyTodo(before, params, 9);
|
|
435
|
+
assert.equal(result.error, "blocked needs a note naming what it waits for");
|
|
436
|
+
assert.equal(result.text, "Error: blocked needs a note naming what it waits for");
|
|
437
|
+
assert.strictEqual(result.state, before, `${params.action} must not change the list`);
|
|
438
|
+
}
|
|
439
|
+
const blocked = applyTodo(before, { action: "update", id: 3, status: "blocked", note: "waiting for the API contract" }, 9);
|
|
440
|
+
assert.equal(blocked.error, undefined);
|
|
441
|
+
assert.equal(blocked.text, "#3 Show git bash tails → blocked");
|
|
442
|
+
const unblocked = applyTodo(blocked.state, { action: "update", id: 3, status: "pending" }, 10);
|
|
443
|
+
assert.equal(unblocked.state.tasks[2].status, TODO_STATUS.PENDING);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("dropped tasks never count as open: a list of done and dropped is finished, silent, and never stale", () => {
|
|
447
|
+
const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "A", status: "done" }, { title: "B", status: "dropped" }, { title: "C", status: "dropped" }] }, 1).state;
|
|
448
|
+
assert.deepEqual(todoSummary(state), { done: 1, total: 3, open: 0 });
|
|
449
|
+
assert.equal(todoPromptBlock(state, 5), undefined);
|
|
450
|
+
assert.equal(staleTurns(state, 9), 0);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("blocked tasks stay open but alone never make the list stale; an actionable task still does", () => {
|
|
454
|
+
const blockedOnly = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "A", status: "done" }, { title: "Deploy", status: "blocked", note: "waiting for an admin" }] }, 1).state;
|
|
455
|
+
assert.deepEqual(todoSummary(blockedOnly), { done: 1, total: 2, open: 1 });
|
|
456
|
+
assert.equal(staleTurns(blockedOnly, 9), 0);
|
|
457
|
+
const block = todoPromptBlock(blockedOnly, 0) ?? "";
|
|
458
|
+
assert.match(block, /2\. \[blocked\] Deploy — waiting for an admin/);
|
|
459
|
+
assert.match(block, /A blocked task waits on something outside the list: do not work on it until its condition holds/);
|
|
460
|
+
assert.match(block, /mark the tasks it made obsolete dropped instead of leaving them pending or marking them done/);
|
|
461
|
+
const mixed = applyTodo(blockedOnly, { action: "add", title: "Write docs" }, 1).state;
|
|
462
|
+
assert.equal(staleTurns(mixed, 3), 2);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("renderTodoCard draws blocked with its reason and dropped dim without strikethrough, excluding dropped from the count", () => {
|
|
466
|
+
const state = applyTodo(
|
|
467
|
+
emptyTodo(),
|
|
468
|
+
{ action: "write", tasks: [{ title: "Ship", status: "done" }, { title: "Old plan", status: "dropped" }, { title: "Deploy", status: "blocked", note: "waiting for an admin" }, { title: "Docs" }] },
|
|
469
|
+
1,
|
|
470
|
+
).state;
|
|
471
|
+
const plain = renderTodoCard(state, plainTheme, 60, { collapsed: false, staleTurns: 0 }).map(stripAnsi);
|
|
472
|
+
assert.match(plain[0], /Todos ▾ Collapse · 1 of 3/);
|
|
473
|
+
assert.match(plain[2], /^│ ✕ Old plan +│$/);
|
|
474
|
+
assert.match(plain[3], /^│ ⊘ Deploy · waiting for an admin +│$/);
|
|
475
|
+
const roles: string[] = [];
|
|
476
|
+
const spy = { ...plainTheme, fg(color: string, text: string) { roles.push(`${color}:${text}`); return text; } };
|
|
477
|
+
renderTodoCard(state, spy, 60, { collapsed: false, staleTurns: 0 });
|
|
478
|
+
assert.ok(roles.includes("muted:✕") && roles.includes("dim:Old plan"), "dropped: muted glyph, dim title");
|
|
479
|
+
assert.ok(roles.includes("warning:⊘"), "blocked: warning glyph");
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("the collapsed card skips blocked tasks for the next one, and shows a blocked task only when nothing else is open", () => {
|
|
483
|
+
const state = applyTodo(emptyTodo(), { action: "write", tasks: [{ title: "Deploy", status: "blocked", note: "waiting for an admin" }, { title: "Docs" }] }, 1).state;
|
|
484
|
+
assert.match(renderTodoCard(state, plainTheme, 60, { collapsed: true, staleTurns: 0 }).map(stripAnsi)[1], /^│ ○ Docs +│$/);
|
|
485
|
+
const onlyBlocked = applyTodo(state, { action: "update", id: 2, status: "done" }, 2).state;
|
|
486
|
+
assert.match(renderTodoCard(onlyBlocked, plainTheme, 60, { collapsed: true, staleTurns: 0 }).map(stripAnsi)[1], /^│ ⊘ Deploy · waiting for an admin +│$/);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("renderTodoCard folds dropped tasks next to done ones in a long list", () => {
|
|
490
|
+
const tasks = Array.from({ length: 20 }, (_, index) => ({ title: `Task ${index + 1}`, status: index < 5 ? "done" : index < 8 ? "dropped" : "pending" }));
|
|
491
|
+
const state = applyTodo(emptyTodo(), { action: "write", tasks }, 1).state;
|
|
492
|
+
const plain = renderTodoCard(state, plainTheme, 60, { collapsed: false, staleTurns: 0 }).map(stripAnsi);
|
|
493
|
+
assert.equal(plain.length, 14);
|
|
494
|
+
assert.match(plain[0], /Todos ▾ Collapse · 5 of 17/);
|
|
495
|
+
assert.match(plain[1], /^│ ✓ 5 done +│$/);
|
|
496
|
+
assert.match(plain[2], /^│ ✕ 3 dropped +│$/);
|
|
497
|
+
assert.match(plain[3], /^│ ○ Task 9 +│$/);
|
|
498
|
+
assert.match(plain[12], /^│ … 3 more +│$/);
|
|
499
|
+
});
|
|
@@ -22,7 +22,7 @@ function payload(percent: number) {
|
|
|
22
22
|
test("UsageView frames the panel, keeps every line at width, and shows the empty state", () => {
|
|
23
23
|
const store = new UsageStore();
|
|
24
24
|
const events: string[] = [];
|
|
25
|
-
const view = new UsageView(store, { theme: plainTheme, now: () => NOW, active: () => undefined, onRefresh: async () => events.push("refresh"), onClose: () => events.push("close"), requestRender: () => events.push("render") });
|
|
25
|
+
const view = new UsageView(store, { theme: plainTheme, now: () => NOW, active: () => undefined, onRefresh: async () => { events.push("refresh"); }, onClose: () => events.push("close"), requestRender: () => events.push("render") });
|
|
26
26
|
const empty = view.render(90).map(stripAnsi);
|
|
27
27
|
assert.match(empty[0], /^╭─ ✿ Subscriptions ─+╮$/);
|
|
28
28
|
assert.match(empty[1], /No subscription usage yet/);
|
|
@@ -62,6 +62,33 @@ test("UsageView refetches on r and closes on escape or q", async () => {
|
|
|
62
62
|
assert.equal(events.filter((event) => event === "close").length, 2);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
test("UsageView starts a refresh at open and repaints when it settles", async () => {
|
|
66
|
+
const store = new UsageStore();
|
|
67
|
+
const events: string[] = [];
|
|
68
|
+
let resolveRefresh: (() => void) | undefined;
|
|
69
|
+
const view = new UsageView(store, {
|
|
70
|
+
theme: plainTheme,
|
|
71
|
+
now: () => NOW,
|
|
72
|
+
active: () => undefined,
|
|
73
|
+
onRefresh: () =>
|
|
74
|
+
new Promise<void>((resolve) => {
|
|
75
|
+
events.push("refresh");
|
|
76
|
+
resolveRefresh = resolve;
|
|
77
|
+
}),
|
|
78
|
+
onClose: () => events.push("close"),
|
|
79
|
+
requestRender: () => events.push("render"),
|
|
80
|
+
});
|
|
81
|
+
view.refresh();
|
|
82
|
+
assert.deepEqual(events, ["render", "refresh"], "opening the panel dispatches the refresh instead of waiting for it");
|
|
83
|
+
assert.match(stripAnsi(view.render(90)[0]), /✿ Subscriptions · refreshing…/, "the panel says it is refreshing while the dispatch is in flight");
|
|
84
|
+
store.record(parseCodexUsage(payload(40), NOW));
|
|
85
|
+
resolveRefresh!();
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
87
|
+
assert.deepEqual(events, ["render", "refresh", "render"], "settling repaints once");
|
|
88
|
+
assert.match(stripAnsi(view.render(90)[0]), /^╭─ ✿ Subscriptions ─+╮$/, "the title returns once the refresh settles");
|
|
89
|
+
assert.match(stripAnsi(view.render(90)[2]), /40%/, "the settled snapshot is drawn");
|
|
90
|
+
});
|
|
91
|
+
|
|
65
92
|
function click(x: number, y: number, width: number, height: number): TuiMouseEvent {
|
|
66
93
|
return { type: "click", button: "left", x, y, screenX: x, screenY: y, width, height, shift: false, alt: false, ctrl: false };
|
|
67
94
|
}
|