@herbertgao/pi-extensions 2026.9.5 → 2026.9.7

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.
Files changed (276) hide show
  1. package/README.md +5 -22
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  4. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  5. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  6. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  7. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  8. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  9. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  10. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  11. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  12. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  13. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  14. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  15. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  16. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  17. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  18. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  19. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  20. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  21. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  22. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  23. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  24. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  25. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  26. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  27. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  28. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  29. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  30. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  31. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  32. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  33. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  34. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  35. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  36. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  37. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  38. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  39. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  40. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  47. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  48. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  49. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  50. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  51. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  52. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  53. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  54. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  55. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  56. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  57. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  58. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  59. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  60. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  61. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  62. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  63. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  64. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  65. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  66. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  67. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  68. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  69. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  70. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  71. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  72. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  73. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  74. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  75. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  76. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  77. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  78. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  79. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  80. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  81. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  82. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  83. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  84. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  85. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  86. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  87. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  88. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  89. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  90. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  91. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  92. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  93. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  94. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  95. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  96. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  97. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  98. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  99. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  100. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  101. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  102. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  103. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  104. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  105. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  106. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  107. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  108. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  109. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  110. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  111. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  112. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  113. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  114. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  115. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  116. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  117. package/node_modules/pi-lens/dist/index.js +27416 -24153
  118. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  119. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  120. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  121. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  122. package/node_modules/pi-lens/docs/audit1.md +5 -5
  123. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  124. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  125. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  126. package/node_modules/pi-lens/docs/features.md +4 -4
  127. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  128. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  129. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  130. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  131. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  132. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  133. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  134. package/node_modules/pi-lens/docs/settings.md +7 -4
  135. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  136. package/node_modules/pi-lens/docs/usage.md +2 -0
  137. package/node_modules/pi-lens/package.json +1 -1
  138. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  139. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  140. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  141. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  142. package/package.json +6 -17
  143. package/examples/pi-footer.json +0 -275
  144. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  145. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  146. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  147. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  148. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  149. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  150. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  151. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  152. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  153. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  154. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  155. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  156. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  157. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  158. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  159. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  160. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  161. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  162. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  163. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  164. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  165. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  166. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  167. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  168. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  169. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  170. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  171. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  172. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  173. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  174. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
  175. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  176. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  177. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  178. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  179. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  180. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  181. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  182. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  183. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  184. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  185. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  186. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  187. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  188. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  189. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  190. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  191. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  192. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  193. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  194. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  195. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  196. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  197. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  198. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  199. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  200. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  201. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  202. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  203. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  204. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  205. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  206. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  207. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  208. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  209. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  210. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  211. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  212. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  213. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  214. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  215. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  216. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  217. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  218. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  219. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  220. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  221. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  222. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  223. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  224. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  225. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  226. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  227. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  228. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  229. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  230. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  231. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  232. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  233. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  234. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  235. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  236. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  237. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  238. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  239. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  240. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  241. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  242. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  243. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  244. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  245. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  246. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  247. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  248. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  249. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  250. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  251. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  252. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  253. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  254. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  255. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  256. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  257. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  258. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  259. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  260. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  261. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  262. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  263. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  264. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  265. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  266. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  267. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  268. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  269. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  270. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  271. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  272. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  273. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  274. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  275. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  276. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
@@ -1,213 +1,203 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import {
3
- copyToClipboard as copyToHostClipboard,
4
- type ExtensionCommandContext,
5
- type KeybindingsManager,
6
- type Theme,
3
+ copyToClipboard as copyToHostClipboard,
4
+ type ExtensionCommandContext,
5
+ type KeybindingsManager,
6
+ type Theme,
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import {
9
- type Component,
10
- isKeyRelease,
11
- isKittyProtocolActive,
12
- Key,
13
- type OverlayHandle,
14
- parseKey,
15
- type TUI,
16
- TuiAltScreen,
17
- type TuiInputListener,
18
- type TuiInputListenerResult,
19
- truncateToWidth,
9
+ type Component,
10
+ isKeyRelease,
11
+ isKittyProtocolActive,
12
+ Key,
13
+ type OverlayHandle,
14
+ parseKey,
15
+ type TUI,
16
+ TuiAltScreen,
17
+ type TuiInputListener,
18
+ type TuiInputListenerResult,
19
+ truncateToWidth,
20
20
  } from "@earendil-works/pi-tui";
21
- import {
22
- type BtwKeybindingOverrides,
23
- BtwPasteGuard,
24
- resolveBtwShortcuts,
25
- setBtwShortcuts,
26
- } from "./keybindings.js";
21
+ import { type BtwKeybindingOverrides, BtwPasteGuard, resolveBtwShortcuts, setBtwShortcuts } from "./keybindings.js";
27
22
  import { formatKeyLabel, sanitizeSingleLine } from "./text.js";
28
23
 
29
24
  type BtwCustomOptions = Parameters<ExtensionCommandContext["ui"]["custom"]>[1];
30
25
  type BtwCustomFactory<T> = (
31
- tui: TUI,
32
- theme: Theme,
33
- keybindings: KeybindingsManager,
34
- done: (result: T) => void,
26
+ tui: TUI,
27
+ theme: Theme,
28
+ keybindings: KeybindingsManager,
29
+ done: (result: T) => void,
35
30
  ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>;
36
31
 
37
32
  type BtwFullscreenTui = TUI & {
38
- flash?: (message: string, durationMs?: number) => void;
39
- setLayoutRoot(component: Component | undefined): void;
40
- addInputListenerBeforeAll?(listener: TuiInputListener): () => void;
41
- addInputListenerBeforeViewport?(listener: TuiInputListener): () => void;
33
+ flash?: (message: string, durationMs?: number) => void;
34
+ setLayoutRoot(component: Component | undefined): void;
35
+ addInputListenerBeforeAll?(listener: TuiInputListener): () => void;
36
+ addInputListenerBeforeViewport?(listener: TuiInputListener): () => void;
42
37
  };
43
38
 
44
39
  export interface BtwFullscreenLayoutComponent extends Component {
45
- getFullscreenLayout(): Component;
40
+ getFullscreenLayout(): Component;
46
41
  }
47
42
 
48
43
  export interface BtwFullscreenOptions {
49
- keybindings?: BtwKeybindingOverrides;
50
- copyOnSelect?: boolean;
44
+ keybindings?: BtwKeybindingOverrides;
45
+ copyOnSelect?: boolean;
51
46
  }
52
47
 
53
48
  export type BtwFullscreenTuiFactory = (
54
- parent: TUI,
55
- theme: Theme,
56
- keybindings: KeybindingsManager,
57
- options: BtwFullscreenOptions,
49
+ parent: TUI,
50
+ theme: Theme,
51
+ keybindings: KeybindingsManager,
52
+ options: BtwFullscreenOptions,
58
53
  ) => BtwFullscreenTui;
59
54
 
60
55
  export interface BtwFullscreenDependencies {
61
- createTui?: BtwFullscreenTuiFactory;
62
- openUrl?: (url: string) => void;
63
- copyToClipboard?: (text: string) => Promise<void>;
64
- manualSelectionCopySupported?: boolean;
56
+ createTui?: BtwFullscreenTuiFactory;
57
+ openUrl?: (url: string) => void;
58
+ copyToClipboard?: (text: string) => Promise<void>;
59
+ manualSelectionCopySupported?: boolean;
65
60
  }
66
61
 
67
62
  export type RunBtwFullscreen = <T>(
68
- ctx: ExtensionCommandContext,
69
- run: (ctx: ExtensionCommandContext) => Promise<T>,
70
- options?: BtwFullscreenOptions,
63
+ ctx: ExtensionCommandContext,
64
+ run: (ctx: ExtensionCommandContext) => Promise<T>,
65
+ options?: BtwFullscreenOptions,
71
66
  ) => Promise<T>;
72
67
 
73
68
  type FullscreenOutcome<T> = { kind: "completed"; value: T } | { kind: "failed"; error: unknown };
74
69
 
75
70
  class FullscreenUiDisposedError extends Error {
76
- constructor() {
77
- super("The dedicated pi-btw UI was disposed.");
78
- this.name = "FullscreenUiDisposedError";
79
- }
71
+ constructor() {
72
+ super("The dedicated pi-btw UI was disposed.");
73
+ this.name = "FullscreenUiDisposedError";
74
+ }
80
75
  }
81
76
 
82
77
  export async function runBtwFullscreen<T>(
83
- ctx: ExtensionCommandContext,
84
- run: (ctx: ExtensionCommandContext) => Promise<T>,
85
- options: BtwFullscreenOptions = {},
86
- dependencies: BtwFullscreenDependencies = {},
78
+ ctx: ExtensionCommandContext,
79
+ run: (ctx: ExtensionCommandContext) => Promise<T>,
80
+ options: BtwFullscreenOptions = {},
81
+ dependencies: BtwFullscreenDependencies = {},
87
82
  ): Promise<T> {
88
- const createTui =
89
- dependencies.createTui ??
90
- ((
91
- parent: TUI,
92
- theme: Theme,
93
- keybindings: KeybindingsManager,
94
- fullscreenOptions: BtwFullscreenOptions,
95
- ) =>
96
- createBtwFullscreenTui(
97
- parent,
98
- theme,
99
- keybindings,
100
- fullscreenOptions.copyOnSelect ?? true,
101
- dependencies.manualSelectionCopySupported ?? hasManualSelectionCopyApi(),
102
- dependencies.openUrl ?? openUrlInBrowser,
103
- dependencies.copyToClipboard ?? copyToHostClipboard,
104
- ));
105
- let liveEditorText = ctx.ui.getEditorText();
106
- let restoreEditor = false;
107
- let host: BtwFullscreenHost<T> | undefined;
108
- const outcome = await ctx.ui.custom<FullscreenOutcome<T>>(
109
- (parent, theme, keybindings, done) => {
110
- host = new BtwFullscreenHost(
111
- parent,
112
- theme,
113
- keybindings,
114
- ctx,
115
- run,
116
- (value) => {
117
- try {
118
- liveEditorText = ctx.ui.getEditorText();
119
- restoreEditor = true;
120
- } catch {
121
- // A replaced session owns a different editor and must not receive stale text.
122
- }
123
- done(value);
124
- },
125
- createTui,
126
- options,
127
- );
128
- return host;
129
- },
130
- {
131
- overlay: true,
132
- onHandle: (handle) => host?.setParentOverlay(handle),
133
- },
134
- );
135
- if (restoreEditor) {
136
- try {
137
- if (ctx.ui.getEditorText() !== liveEditorText) ctx.ui.setEditorText(liveEditorText);
138
- } catch {
139
- // A replaced session owns a different editor and must not receive stale restoration.
140
- }
141
- }
142
- if (outcome.kind === "failed") throw outcome.error;
143
- return outcome.value;
83
+ const createTui =
84
+ dependencies.createTui ??
85
+ ((parent: TUI, theme: Theme, keybindings: KeybindingsManager, fullscreenOptions: BtwFullscreenOptions) =>
86
+ createBtwFullscreenTui(
87
+ parent,
88
+ theme,
89
+ keybindings,
90
+ fullscreenOptions.copyOnSelect ?? true,
91
+ dependencies.manualSelectionCopySupported ?? hasManualSelectionCopyApi(),
92
+ dependencies.openUrl ?? openUrlInBrowser,
93
+ dependencies.copyToClipboard ?? copyToHostClipboard,
94
+ ));
95
+ let liveEditorText = ctx.ui.getEditorText();
96
+ let restoreEditor = false;
97
+ let host: BtwFullscreenHost<T> | undefined;
98
+ const outcome = await ctx.ui.custom<FullscreenOutcome<T>>(
99
+ (parent, theme, keybindings, done) => {
100
+ host = new BtwFullscreenHost(
101
+ parent,
102
+ theme,
103
+ keybindings,
104
+ ctx,
105
+ run,
106
+ (value) => {
107
+ try {
108
+ liveEditorText = ctx.ui.getEditorText();
109
+ restoreEditor = true;
110
+ } catch {
111
+ // A replaced session owns a different editor and must not receive stale text.
112
+ }
113
+ done(value);
114
+ },
115
+ createTui,
116
+ options,
117
+ );
118
+ return host;
119
+ },
120
+ {
121
+ overlay: true,
122
+ onHandle: (handle) => host?.setParentOverlay(handle),
123
+ },
124
+ );
125
+ if (restoreEditor) {
126
+ try {
127
+ if (ctx.ui.getEditorText() !== liveEditorText) ctx.ui.setEditorText(liveEditorText);
128
+ } catch {
129
+ // A replaced session owns a different editor and must not receive stale restoration.
130
+ }
131
+ }
132
+ if (outcome.kind === "failed") throw outcome.error;
133
+ return outcome.value;
144
134
  }
145
135
 
146
136
  type BtwInputListeners = {
147
- beforeAll: Set<TuiInputListener>;
148
- beforeViewport: Set<TuiInputListener>;
149
- regular: Set<TuiInputListener>;
137
+ beforeAll: Set<TuiInputListener>;
138
+ beforeViewport: Set<TuiInputListener>;
139
+ regular: Set<TuiInputListener>;
150
140
  };
151
141
 
152
142
  const btwInputListeners = new WeakMap<BtwTuiAltScreen, BtwInputListeners>();
153
143
 
154
144
  function dispatchBtwInput(listeners: BtwInputListeners, data: string): TuiInputListenerResult {
155
- let current = data;
156
- for (const group of [listeners.beforeAll, listeners.beforeViewport, listeners.regular]) {
157
- for (const listener of group) {
158
- const result = listener(current);
159
- if (result?.consume) return result;
160
- if (result?.data !== undefined) current = result.data;
161
- }
162
- }
163
- return current === data ? undefined : { data: current };
145
+ let current = data;
146
+ for (const group of [listeners.beforeAll, listeners.beforeViewport, listeners.regular]) {
147
+ for (const listener of group) {
148
+ const result = listener(current);
149
+ if (result?.consume) return result;
150
+ if (result?.data !== undefined) current = result.data;
151
+ }
152
+ }
153
+ return current === data ? undefined : { data: current };
164
154
  }
165
155
 
166
156
  class BtwTuiAltScreen extends TuiAltScreen {
167
- hasFocusedOverlay(): boolean {
168
- return this.isOverlayFocused();
169
- }
170
-
171
- override addInputListener(listener: TuiInputListener): () => void {
172
- let listeners = btwInputListeners.get(this);
173
- if (!listeners) {
174
- const registeredListeners: BtwInputListeners = {
175
- beforeAll: new Set(),
176
- beforeViewport: new Set(),
177
- regular: new Set(),
178
- };
179
- btwInputListeners.set(this, registeredListeners);
180
- super.addInputListener((data) => dispatchBtwInput(registeredListeners, data));
181
- listeners = registeredListeners;
182
- }
183
- listeners.regular.add(listener);
184
- return () => listeners.regular.delete(listener);
185
- }
186
-
187
- addInputListenerBeforeAll(listener: TuiInputListener): () => void {
188
- const listeners = btwInputListeners.get(this);
189
- if (!listeners) return super.addInputListener(listener);
190
- listeners.beforeAll.add(listener);
191
- return () => listeners.beforeAll.delete(listener);
192
- }
193
-
194
- addInputListenerBeforeViewport(listener: TuiInputListener): () => void {
195
- const listeners = btwInputListeners.get(this);
196
- if (!listeners) return super.addInputListener(listener);
197
- listeners.beforeViewport.add(listener);
198
- return () => listeners.beforeViewport.delete(listener);
199
- }
200
-
201
- override removeInputListener(listener: TuiInputListener): void {
202
- const listeners = btwInputListeners.get(this);
203
- if (!listeners) {
204
- super.removeInputListener(listener);
205
- return;
206
- }
207
- listeners.beforeAll.delete(listener);
208
- listeners.beforeViewport.delete(listener);
209
- listeners.regular.delete(listener);
210
- }
157
+ hasFocusedOverlay(): boolean {
158
+ return this.isOverlayFocused();
159
+ }
160
+
161
+ override addInputListener(listener: TuiInputListener): () => void {
162
+ let listeners = btwInputListeners.get(this);
163
+ if (!listeners) {
164
+ const registeredListeners: BtwInputListeners = {
165
+ beforeAll: new Set(),
166
+ beforeViewport: new Set(),
167
+ regular: new Set(),
168
+ };
169
+ btwInputListeners.set(this, registeredListeners);
170
+ super.addInputListener((data) => dispatchBtwInput(registeredListeners, data));
171
+ listeners = registeredListeners;
172
+ }
173
+ listeners.regular.add(listener);
174
+ return () => listeners.regular.delete(listener);
175
+ }
176
+
177
+ addInputListenerBeforeAll(listener: TuiInputListener): () => void {
178
+ const listeners = btwInputListeners.get(this);
179
+ if (!listeners) return super.addInputListener(listener);
180
+ listeners.beforeAll.add(listener);
181
+ return () => listeners.beforeAll.delete(listener);
182
+ }
183
+
184
+ addInputListenerBeforeViewport(listener: TuiInputListener): () => void {
185
+ const listeners = btwInputListeners.get(this);
186
+ if (!listeners) return super.addInputListener(listener);
187
+ listeners.beforeViewport.add(listener);
188
+ return () => listeners.beforeViewport.delete(listener);
189
+ }
190
+
191
+ override removeInputListener(listener: TuiInputListener): void {
192
+ const listeners = btwInputListeners.get(this);
193
+ if (!listeners) {
194
+ super.removeInputListener(listener);
195
+ return;
196
+ }
197
+ listeners.beforeAll.delete(listener);
198
+ listeners.beforeViewport.delete(listener);
199
+ listeners.regular.delete(listener);
200
+ }
211
201
  }
212
202
 
213
203
  const BRACKETED_PASTE_START = "\u001b[200~";
@@ -215,554 +205,563 @@ const BRACKETED_PASTE_END = "\u001b[201~";
215
205
 
216
206
  // TuiAltScreen evaluates these actions before bottom, so shared keys cannot jump to latest.
217
207
  const ALT_SCREEN_ACTIONS_BEFORE_BOTTOM = [
218
- "tui.altScreen.search",
219
- "tui.altScreen.searchNext",
220
- "tui.altScreen.searchPrevious",
221
- "tui.altScreen.searchClose",
222
- "tui.altScreen.pageUp",
223
- "tui.altScreen.pageDown",
224
- "tui.altScreen.halfPageUp",
225
- "tui.altScreen.halfPageDown",
226
- "tui.altScreen.lineUp",
227
- "tui.altScreen.lineDown",
228
- "tui.altScreen.previousPrompt",
229
- "tui.altScreen.nextPrompt",
230
- "tui.altScreen.top",
208
+ "tui.altScreen.search",
209
+ "tui.altScreen.searchNext",
210
+ "tui.altScreen.searchPrevious",
211
+ "tui.altScreen.searchClose",
212
+ "tui.altScreen.pageUp",
213
+ "tui.altScreen.pageDown",
214
+ "tui.altScreen.halfPageUp",
215
+ "tui.altScreen.halfPageDown",
216
+ "tui.altScreen.lineUp",
217
+ "tui.altScreen.lineDown",
218
+ "tui.altScreen.previousPrompt",
219
+ "tui.altScreen.nextPrompt",
220
+ "tui.altScreen.top",
231
221
  ] as const;
232
222
  const KEY_MODIFIER_ORDER = ["shift", "ctrl", "alt", "super"] as const;
233
223
  const MATCHABLE_SPECIAL_KEYS = new Set([
234
- "space",
235
- "tab",
236
- "enter",
237
- "backspace",
238
- "delete",
239
- "insert",
240
- "home",
241
- "end",
242
- "pageup",
243
- "pagedown",
244
- "up",
245
- "down",
246
- "left",
247
- "right",
224
+ "space",
225
+ "tab",
226
+ "enter",
227
+ "backspace",
228
+ "delete",
229
+ "insert",
230
+ "home",
231
+ "end",
232
+ "pageup",
233
+ "pagedown",
234
+ "up",
235
+ "down",
236
+ "left",
237
+ "right",
248
238
  ]);
249
239
  const MATCHABLE_SYMBOL_KEYS = new Set("`-=[]\\;',./!@#$%^&*()_+|~{}:<>?");
250
240
 
251
241
  function normalizedKeyId(key: string): string {
252
- const parts = key.toLowerCase().split("+");
253
- const base = parts.at(-1);
254
- if (!base) return "";
255
- const normalizedBase = base === "esc" ? "escape" : base === "return" ? "enter" : base;
256
- const modifiers = KEY_MODIFIER_ORDER.filter((modifier) => parts.includes(modifier));
257
- return [...modifiers, normalizedBase].join("+");
242
+ const parts = key.toLowerCase().split("+");
243
+ const base = parts.at(-1);
244
+ if (!base) return "";
245
+ const normalizedBase = base === "esc" ? "escape" : base === "return" ? "enter" : base;
246
+ const modifiers = KEY_MODIFIER_ORDER.filter((modifier) => parts.includes(modifier));
247
+ return [...modifiers, normalizedBase].join("+");
258
248
  }
259
249
 
260
250
  function formatEffectiveKeyLabel(key: string): string {
261
- const parts = key.split("+");
262
- const base = parts.at(-1);
263
- if (base === "pageup") parts[parts.length - 1] = "pageUp";
264
- if (base === "pagedown") parts[parts.length - 1] = "pageDown";
265
- return formatKeyLabel(parts.join("+"));
251
+ const parts = key.split("+");
252
+ const base = parts.at(-1);
253
+ if (base === "pageup") parts[parts.length - 1] = "pageUp";
254
+ if (base === "pagedown") parts[parts.length - 1] = "pageDown";
255
+ return formatKeyLabel(parts.join("+"));
266
256
  }
267
257
 
268
258
  function canMatchKeyInput(key: string): boolean {
269
- const parts = key.split("+");
270
- const base = parts.at(-1) ?? "";
271
- const modifiers = parts.slice(0, -1);
272
- if (base === "escape") return modifiers.length === 0;
273
- if (base === "clear") {
274
- return (
275
- modifiers.length === 0 ||
276
- (modifiers.length === 1 && (modifiers[0] === "shift" || modifiers[0] === "ctrl"))
277
- );
278
- }
279
- if (/^f(?:[1-9]|1[0-2])$/u.test(base)) return modifiers.length === 0;
280
- return (
281
- MATCHABLE_SPECIAL_KEYS.has(base) ||
282
- (base.length === 1 && (/^[a-z0-9]$/u.test(base) || MATCHABLE_SYMBOL_KEYS.has(base)))
283
- );
259
+ const parts = key.split("+");
260
+ const base = parts.at(-1) ?? "";
261
+ const modifiers = parts.slice(0, -1);
262
+ if (base === "escape") return modifiers.length === 0;
263
+ if (base === "clear") {
264
+ return modifiers.length === 0 || (modifiers.length === 1 && (modifiers[0] === "shift" || modifiers[0] === "ctrl"));
265
+ }
266
+ if (/^f(?:[1-9]|1[0-2])$/u.test(base)) return modifiers.length === 0;
267
+ return (
268
+ MATCHABLE_SPECIAL_KEYS.has(base) ||
269
+ (base.length === 1 && (/^[a-z0-9]$/u.test(base) || MATCHABLE_SYMBOL_KEYS.has(base)))
270
+ );
284
271
  }
285
272
 
286
273
  function rawCtrlInput(base: string): string | undefined {
287
- if (base.length !== 1) return undefined;
288
- const rawBase = base === "-" ? "_" : base;
289
- if (!"abcdefghijklmnopqrstuvwxyz[\\]_".includes(rawBase)) return undefined;
290
- return String.fromCharCode(rawBase.charCodeAt(0) & 0x1f);
274
+ if (base.length !== 1) return undefined;
275
+ const rawBase = base === "-" ? "_" : base;
276
+ if (!"abcdefghijklmnopqrstuvwxyz[\\]_".includes(rawBase)) return undefined;
277
+ return String.fromCharCode(rawBase.charCodeAt(0) & 0x1f);
291
278
  }
292
279
 
293
280
  function legacyRawInput(key: string): string | undefined {
294
- const parts = key.split("+");
295
- const base = parts.at(-1) ?? "";
296
- if (parts.length === 2 && parts[0] === "ctrl") return rawCtrlInput(base);
297
- if (isKittyProtocolActive()) return undefined;
298
- if (parts.length === 2 && parts[0] === "alt" && base.length === 1) return `\u001b${base}`;
299
- if (parts.length === 3 && parts[0] === "ctrl" && parts[1] === "alt") {
300
- const input = rawCtrlInput(base);
301
- return input ? `\u001b${input}` : undefined;
302
- }
303
- return undefined;
281
+ const parts = key.split("+");
282
+ const base = parts.at(-1) ?? "";
283
+ if (parts.length === 2 && parts[0] === "ctrl") return rawCtrlInput(base);
284
+ if (isKittyProtocolActive()) return undefined;
285
+ if (parts.length === 2 && parts[0] === "alt" && base.length === 1) return `\u001b${base}`;
286
+ if (parts.length === 3 && parts[0] === "ctrl" && parts[1] === "alt") {
287
+ const input = rawCtrlInput(base);
288
+ return input ? `\u001b${input}` : undefined;
289
+ }
290
+ return undefined;
304
291
  }
305
292
 
306
293
  // Mirror matchesKey(), using parseKey() to canonicalize IDs that share legacy raw input.
307
294
  function keyInputIdentity(key: string): string {
308
- const identity = normalizedKeyId(key);
309
- const input = legacyRawInput(identity);
310
- return input ? normalizedKeyId(parseKey(input) ?? identity) : identity;
295
+ const identity = normalizedKeyId(key);
296
+ const input = legacyRawInput(identity);
297
+ return input ? normalizedKeyId(parseKey(input) ?? identity) : identity;
311
298
  }
312
299
 
313
300
  function hasManualSelectionCopyApi(): boolean {
314
- return (
315
- typeof TuiAltScreen.prototype.hasActiveSelection === "function" &&
316
- typeof TuiAltScreen.prototype.copyActiveSelectionToClipboard === "function"
317
- );
301
+ return (
302
+ typeof TuiAltScreen.prototype.hasActiveSelection === "function" &&
303
+ typeof TuiAltScreen.prototype.copyActiveSelectionToClipboard === "function"
304
+ );
318
305
  }
319
306
 
320
307
  function createBtwFullscreenTui(
321
- parent: TUI,
322
- theme: Theme,
323
- keybindings: KeybindingsManager,
324
- copyOnSelect: boolean,
325
- manualSelectionCopySupported: boolean,
326
- openUrl: (url: string) => void,
327
- copyToClipboard: (text: string) => Promise<void>,
308
+ parent: TUI,
309
+ theme: Theme,
310
+ keybindings: KeybindingsManager,
311
+ copyOnSelect: boolean,
312
+ manualSelectionCopySupported: boolean,
313
+ openUrl: (url: string) => void,
314
+ copyToClipboard: (text: string) => Promise<void>,
328
315
  ): BtwFullscreenTui {
329
- if (!copyOnSelect && !manualSelectionCopySupported) {
330
- throw new Error(
331
- "Manual fullscreen selection copying is unavailable in this Pi version; update Pi or enable automatic selection copying.",
332
- );
333
- }
334
- const styleSearchMatch = (text: string) =>
335
- theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
336
- const fullscreen = new BtwTuiAltScreen(
337
- parent.terminal,
338
- parent.getShowHardwareCursor(),
339
- undefined,
340
- {
341
- mouse: true,
342
- copyOnSelect,
343
- searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
344
- scrollToEndIndicator: () => {
345
- const unavailableKeyIdentities = new Set<string>([keyInputIdentity(Key.ctrl("c"))]);
346
- for (const action of ALT_SCREEN_ACTIONS_BEFORE_BOTTOM) {
347
- for (const actionKey of keybindings.getKeys(action)) {
348
- unavailableKeyIdentities.add(keyInputIdentity(String(actionKey)));
349
- }
350
- }
351
- if (!copyOnSelect) {
352
- for (const copyKey of keybindings.getKeys("app.message.copy")) {
353
- unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
354
- }
355
- }
356
- const key = keybindings
357
- .getKeys("tui.altScreen.bottom")
358
- .map((candidate) => keyInputIdentity(String(candidate)))
359
- .find(
360
- (identity) =>
361
- identity &&
362
- canMatchKeyInput(identity) &&
363
- !unavailableKeyIdentities.has(identity) &&
364
- formatEffectiveKeyLabel(identity),
365
- );
366
- const label = theme.fg("text", " ↓ Jump to latest message");
367
- const shortcut = key ? theme.fg("muted", ` · ${formatEffectiveKeyLabel(key)}`) : "";
368
- return theme.bg("selectedBg", `${label}${shortcut} `);
369
- },
370
- searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
371
- openUrl,
372
- copySelection: async (text) => {
373
- try {
374
- await copyToClipboard(text);
375
- return true;
376
- } catch {
377
- return false;
378
- }
379
- },
380
- },
381
- );
382
- if (!copyOnSelect) {
383
- let isInBracketedPaste = false;
384
- fullscreen.addInputListenerBeforeViewport((data) => {
385
- const wasInBracketedPaste = isInBracketedPaste;
386
- const startsBracketedPaste = data.includes(BRACKETED_PASTE_START);
387
- if (startsBracketedPaste) isInBracketedPaste = true;
388
- if (isInBracketedPaste && data.includes(BRACKETED_PASTE_END)) {
389
- isInBracketedPaste = false;
390
- }
391
- if (
392
- wasInBracketedPaste ||
393
- startsBracketedPaste ||
394
- fullscreen.hasFocusedOverlay() ||
395
- isKeyRelease(data) ||
396
- !keybindings.matches(data, "app.message.copy")
397
- ) {
398
- return undefined;
399
- }
400
- if (!fullscreen.hasActiveSelection()) {
401
- fullscreen.flash("No selection to copy");
402
- return { consume: true };
403
- }
404
- void fullscreen.copyActiveSelectionToClipboard().catch(() => fullscreen.flash("Copy failed"));
405
- return { consume: true };
406
- });
407
- }
408
- return fullscreen;
316
+ if (!copyOnSelect && !manualSelectionCopySupported) {
317
+ throw new Error(
318
+ "Manual fullscreen selection copying is unavailable in this Pi version; update Pi or enable automatic selection copying.",
319
+ );
320
+ }
321
+ const styleSearchMatch = (text: string) => theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
322
+ const fullscreen = new BtwTuiAltScreen(parent.terminal, parent.getShowHardwareCursor(), undefined, {
323
+ mouse: true,
324
+ copyOnSelect,
325
+ searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
326
+ scrollToEndIndicator: () => {
327
+ const unavailableKeyIdentities = new Set<string>([keyInputIdentity(Key.ctrl("c"))]);
328
+ for (const action of ALT_SCREEN_ACTIONS_BEFORE_BOTTOM) {
329
+ for (const actionKey of keybindings.getKeys(action)) {
330
+ unavailableKeyIdentities.add(keyInputIdentity(String(actionKey)));
331
+ }
332
+ }
333
+ if (!copyOnSelect) {
334
+ for (const copyKey of keybindings.getKeys("app.message.copy")) {
335
+ unavailableKeyIdentities.add(keyInputIdentity(String(copyKey)));
336
+ }
337
+ }
338
+ const key = keybindings
339
+ .getKeys("tui.altScreen.bottom")
340
+ .map((candidate) => keyInputIdentity(String(candidate)))
341
+ .find(
342
+ (identity) =>
343
+ identity &&
344
+ canMatchKeyInput(identity) &&
345
+ !unavailableKeyIdentities.has(identity) &&
346
+ formatEffectiveKeyLabel(identity),
347
+ );
348
+ const label = theme.fg("text", " ↓ Jump to latest message");
349
+ const shortcut = key ? theme.fg("muted", ` · ${formatEffectiveKeyLabel(key)}`) : "";
350
+ return theme.bg("selectedBg", `${label}${shortcut} `);
351
+ },
352
+ searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
353
+ openUrl,
354
+ copySelection: async (text) => {
355
+ try {
356
+ await copyToClipboard(text);
357
+ return true;
358
+ } catch {
359
+ return false;
360
+ }
361
+ },
362
+ });
363
+ if (!copyOnSelect) {
364
+ let isInBracketedPaste = false;
365
+ fullscreen.addInputListenerBeforeViewport((data) => {
366
+ const wasInBracketedPaste = isInBracketedPaste;
367
+ const startsBracketedPaste = data.includes(BRACKETED_PASTE_START);
368
+ if (startsBracketedPaste) isInBracketedPaste = true;
369
+ if (isInBracketedPaste && data.includes(BRACKETED_PASTE_END)) {
370
+ isInBracketedPaste = false;
371
+ }
372
+ if (
373
+ wasInBracketedPaste ||
374
+ startsBracketedPaste ||
375
+ fullscreen.hasFocusedOverlay() ||
376
+ isKeyRelease(data) ||
377
+ !keybindings.matches(data, "app.message.copy")
378
+ ) {
379
+ return undefined;
380
+ }
381
+ if (!fullscreen.hasActiveSelection()) {
382
+ fullscreen.flash("No selection to copy");
383
+ return { consume: true };
384
+ }
385
+ void fullscreen.copyActiveSelectionToClipboard().catch(() => fullscreen.flash("Copy failed"));
386
+ return { consume: true };
387
+ });
388
+ }
389
+ return fullscreen;
409
390
  }
410
391
 
411
392
  // Pi does not export its browser opener, so mirror its shell-free launcher for this isolated TUI.
412
393
  function openUrlInBrowser(target: string): void {
413
- const [command, args] =
414
- process.platform === "darwin"
415
- ? ["open", [target]]
416
- : process.platform === "win32"
417
- ? ["rundll32", ["url.dll,FileProtocolHandler", target]]
418
- : ["xdg-open", [target]];
419
- spawn(command, args, { stdio: "ignore", detached: true })
420
- .on("error", () => {})
421
- .unref();
394
+ const [command, args] =
395
+ process.platform === "darwin"
396
+ ? ["open", [target]]
397
+ : process.platform === "win32"
398
+ ? ["rundll32", ["url.dll,FileProtocolHandler", target]]
399
+ : ["xdg-open", [target]];
400
+ spawn(command, args, { stdio: "ignore", detached: true })
401
+ .on("error", () => {})
402
+ .unref();
422
403
  }
423
404
 
424
405
  class BtwFullscreenHost<T> implements Component {
425
- private fullscreen: BtwFullscreenTui | undefined;
426
- private parentOverlay: OverlayHandle | undefined;
427
- private cancelActiveCustom: (() => void) | undefined;
428
- private hardCancelActiveCustom: (() => void) | undefined;
429
- private removeHardCancelListener: (() => void) | undefined;
430
- private started = false;
431
- private disposed = false;
432
- private finished = false;
433
- private parentStopped = false;
434
- private parentRestoreAttempted = false;
435
- private fullscreenCreated = false;
436
- private fullscreenStopped = false;
437
- private parentRestoreQueued = false;
438
- private parentRestorePromise: Promise<void> | undefined;
439
- private cleanupError: unknown;
440
-
441
- constructor(
442
- private readonly parent: TUI,
443
- private readonly theme: Theme,
444
- private readonly keybindings: KeybindingsManager,
445
- private readonly ctx: ExtensionCommandContext,
446
- private readonly run: (ctx: ExtensionCommandContext) => Promise<T>,
447
- private readonly done: (outcome: FullscreenOutcome<T>) => void,
448
- private readonly createTui: BtwFullscreenTuiFactory,
449
- private readonly options: BtwFullscreenOptions,
450
- ) {
451
- queueMicrotask(() => void this.start());
452
- }
453
-
454
- setParentOverlay(overlay: OverlayHandle): void {
455
- this.parentOverlay = overlay;
456
- }
457
-
458
- render(width: number): string[] {
459
- return [truncateToWidth(this.theme.fg("muted", "Opening btw side thread…"), width)];
460
- }
461
-
462
- invalidate(): void {}
463
-
464
- dispose(): void {
465
- if (this.disposed || this.finished) return;
466
- this.disposed = true;
467
- this.cancelActiveCustom?.();
468
- }
469
-
470
- private async start(): Promise<void> {
471
- if (this.started || this.finished) return;
472
- this.started = true;
473
- let outcome: FullscreenOutcome<T>;
474
- try {
475
- if (this.disposed) throw new FullscreenUiDisposedError();
476
- this.parent.stop({ preserveScreen: true });
477
- this.parentStopped = true;
478
- if (this.disposed) throw new FullscreenUiDisposedError();
479
- this.fullscreen = this.createTui(this.parent, this.theme, this.keybindings, this.options);
480
- this.fullscreenCreated = true;
481
- this.fullscreen.start();
482
- const shortcuts = resolveBtwShortcuts(
483
- this.options.keybindings,
484
- this.keybindings,
485
- this.options.copyOnSelect ?? true,
486
- );
487
- setBtwShortcuts(this.fullscreen, shortcuts);
488
- // Negotiate before warning when possible: the first dispatched user input uses
489
- // the current mode. Recheck each input, including later mode transitions.
490
- let previousWarnings: readonly string[] = [];
491
- const reportWarnings = () => {
492
- const warnings = shortcuts.warnings;
493
- for (const warning of warnings) {
494
- if (previousWarnings.includes(warning)) continue;
495
- try {
496
- this.ctx.ui.notify(`Pi BTW: ${warning}`, "warning");
497
- } catch {
498
- /* A replaced context must not prevent terminal cleanup. */
499
- }
500
- }
501
- previousWarnings = warnings;
502
- };
503
- const pasteGuard = new BtwPasteGuard();
504
- // Waiting for the custom promise would leave follow-up keys bound to the side TUI.
505
- const addHardCancelListener =
506
- this.fullscreen.addInputListenerBeforeAll?.bind(this.fullscreen) ??
507
- this.fullscreen.addInputListenerBeforeViewport?.bind(this.fullscreen) ??
508
- this.fullscreen.addInputListener.bind(this.fullscreen);
509
- this.removeHardCancelListener = addHardCancelListener((data) => {
510
- reportWarnings();
511
- if (pasteGuard.consume(data) || !shortcuts.matches(data, "exit")) return undefined;
512
- this.disposed = true;
513
- try {
514
- this.hardCancelActiveCustom?.();
515
- } finally {
516
- // ProcessTerminal.stop() destroys its active input buffer. Keep cancellation
517
- // synchronous, then drain input before the physical Windows terminal handoff.
518
- // Pi has no public input injection, so do not replay bytes already coalesced
519
- // behind the hard-cancel key.
520
- this.queueParentRestore();
521
- }
522
- return { consume: true };
523
- });
524
- outcome = { kind: "completed", value: await this.run(this.createContext()) };
525
- } catch (error) {
526
- outcome = { kind: "failed", error };
527
- }
528
-
529
- try {
530
- this.cancelActiveCustom?.();
531
- } catch (error) {
532
- this.cleanupError ??= error;
533
- }
534
- if (this.parentRestorePromise) await this.parentRestorePromise;
535
- else this.restoreParent();
536
- if (this.cleanupError !== undefined) outcome = { kind: "failed", error: this.cleanupError };
537
- this.finished = true;
538
- this.done(outcome);
539
- }
540
-
541
- private queueParentRestore(): void {
542
- if (this.parentRestoreQueued || this.parentRestoreAttempted) return;
543
- this.parentRestoreQueued = true;
544
- this.parentRestorePromise = Promise.resolve().then(async () => {
545
- try {
546
- await this.fullscreen?.terminal.drainInput?.();
547
- } catch (error) {
548
- this.cleanupError ??= error;
549
- }
550
- this.parentRestoreQueued = false;
551
- this.restoreParent();
552
- });
553
- }
554
-
555
- private restoreParent(): void {
556
- const removeHardCancelListener = this.removeHardCancelListener;
557
- this.removeHardCancelListener = undefined;
558
- try {
559
- removeHardCancelListener?.();
560
- } catch (error) {
561
- this.cleanupError ??= error;
562
- }
563
- if (this.fullscreenCreated && !this.fullscreenStopped) {
564
- this.fullscreenStopped = true;
565
- try {
566
- this.fullscreen?.stop({ preserveScreen: true });
567
- } catch (error) {
568
- this.cleanupError ??= error;
569
- }
570
- }
571
- if (!this.parentStopped || this.parentRestoreAttempted) return;
572
- const parentOverlay = this.parentOverlay;
573
- this.parentOverlay = undefined;
574
- try {
575
- parentOverlay?.setHidden(true);
576
- } catch (error) {
577
- this.cleanupError ??= error;
578
- }
579
- try {
580
- this.parentRestoreAttempted = true;
581
- this.parent.start();
582
- this.parent.renderNow(false);
583
- } catch (error) {
584
- this.cleanupError ??= error;
585
- }
586
- }
587
-
588
- private createContext(): ExtensionCommandContext {
589
- const ui = new Proxy(this.ctx.ui, {
590
- get: (target, property) => {
591
- if (property === "custom") {
592
- return <Value>(factory: BtwCustomFactory<Value>, options?: BtwCustomOptions) =>
593
- this.showCustom(factory, options);
594
- }
595
- if (property === "notify") {
596
- return (
597
- message: string,
598
- level?: Parameters<ExtensionCommandContext["ui"]["notify"]>[1],
599
- ) => {
600
- target.notify(message, level);
601
- const display = sanitizeSingleLine(message);
602
- if (display) this.fullscreen?.flash?.(display);
603
- };
604
- }
605
- const value = Reflect.get(target, property, target) as unknown;
606
- return typeof value === "function" ? value.bind(target) : value;
607
- },
608
- });
609
- return new Proxy(this.ctx, {
610
- get: (target, property) => (property === "ui" ? ui : Reflect.get(target, property, target)),
611
- });
612
- }
613
-
614
- private showCustom<Value>(
615
- factory: BtwCustomFactory<Value>,
616
- options?: BtwCustomOptions,
617
- ): Promise<Value> {
618
- const fullscreen = this.fullscreen;
619
- if (!fullscreen || this.disposed || this.finished) {
620
- return Promise.reject(new FullscreenUiDisposedError());
621
- }
622
- if (this.cancelActiveCustom) {
623
- return Promise.reject(new Error("pi-btw attempted to open overlapping custom UI."));
624
- }
625
-
626
- return new Promise<Value>((resolve, reject) => {
627
- let component: (Component & { dispose?(): void }) | undefined;
628
- let overlay: OverlayHandle | undefined;
629
- let mounted = false;
630
- let layoutMounted = false;
631
- let factorySettled = false;
632
- let closed = false;
633
- let promiseSettled = false;
634
- let componentDisposed = false;
635
- let pendingValue: Value | undefined;
636
- let hasPendingValue = false;
637
-
638
- const disposeComponent = () => {
639
- if (!component || componentDisposed) return;
640
- componentDisposed = true;
641
- try {
642
- component.dispose?.();
643
- } catch {
644
- // Cleanup must continue so terminal ownership is restored.
645
- }
646
- };
647
- const unmount = () => {
648
- let cleanupError: unknown;
649
- try {
650
- if (overlay) overlay.hide();
651
- else if (mounted && layoutMounted) fullscreen.setLayoutRoot(undefined);
652
- else if (mounted && component) fullscreen.removeChild(component);
653
- } catch (error) {
654
- cleanupError = error;
655
- }
656
- if (overlay || mounted) {
657
- try {
658
- fullscreen.setFocus(null);
659
- fullscreen.requestRender();
660
- } catch (error) {
661
- cleanupError ??= error;
662
- }
663
- }
664
- disposeComponent();
665
- if (cleanupError !== undefined) throw cleanupError;
666
- };
667
- const complete = () => {
668
- if (promiseSettled || !hasPendingValue) return;
669
- promiseSettled = true;
670
- this.cancelActiveCustom = undefined;
671
- this.hardCancelActiveCustom = undefined;
672
- if (!factorySettled) {
673
- resolve(pendingValue as Value);
674
- return;
675
- }
676
- try {
677
- unmount();
678
- resolve(pendingValue as Value);
679
- } catch (error) {
680
- reject(error);
681
- }
682
- };
683
- const close = (value: Value) => {
684
- if (closed || promiseSettled) return;
685
- closed = true;
686
- pendingValue = value;
687
- hasPendingValue = true;
688
- complete();
689
- };
690
- const fail = (error: unknown) => {
691
- if (promiseSettled) return;
692
- closed = true;
693
- promiseSettled = true;
694
- this.cancelActiveCustom = undefined;
695
- this.hardCancelActiveCustom = undefined;
696
- try {
697
- unmount();
698
- reject(error);
699
- } catch (cleanupError) {
700
- reject(cleanupError);
701
- }
702
- };
703
- this.cancelActiveCustom = () => {
704
- if (promiseSettled) return;
705
- disposeComponent();
706
- if (!promiseSettled) fail(new FullscreenUiDisposedError());
707
- };
708
- this.hardCancelActiveCustom = () => {
709
- if (promiseSettled) return;
710
- try {
711
- component?.handleInput?.("\u0003");
712
- } catch (error) {
713
- fail(error);
714
- return;
715
- }
716
- this.cancelActiveCustom?.();
717
- };
718
-
719
- let created: ReturnType<BtwCustomFactory<Value>>;
720
- try {
721
- created = factory(fullscreen, this.theme, this.keybindings, close);
722
- } catch (error) {
723
- factorySettled = true;
724
- fail(error);
725
- return;
726
- }
727
- Promise.resolve(created)
728
- .then((value) => {
729
- component = value;
730
- factorySettled = true;
731
- if (promiseSettled) {
732
- disposeComponent();
733
- return;
734
- }
735
- if (closed) {
736
- complete();
737
- return;
738
- }
739
- if (options?.overlay) {
740
- const overlayOptions =
741
- typeof options.overlayOptions === "function"
742
- ? options.overlayOptions()
743
- : options.overlayOptions;
744
- overlay = fullscreen.showOverlay(component, overlayOptions);
745
- options.onHandle?.(overlay);
746
- } else {
747
- fullscreen.clear();
748
- mounted = true;
749
- if (isFullscreenLayoutComponent(component)) {
750
- layoutMounted = true;
751
- fullscreen.setLayoutRoot(component.getFullscreenLayout());
752
- } else {
753
- fullscreen.addChild(component);
754
- }
755
- fullscreen.setFocus(component);
756
- fullscreen.requestRender();
757
- }
758
- })
759
- .catch(fail);
760
- });
761
- }
406
+ private fullscreen: BtwFullscreenTui | undefined;
407
+ private parentOverlay: OverlayHandle | undefined;
408
+ private cancelActiveCustom: (() => void) | undefined;
409
+ private hardCancelActiveCustom: (() => void) | undefined;
410
+ private removeHardCancelListener: (() => void) | undefined;
411
+ private removeUpstreamAbortListener: (() => void) | undefined;
412
+ private started = false;
413
+ private disposed = false;
414
+ private finished = false;
415
+ private parentStopped = false;
416
+ private parentRestoreAttempted = false;
417
+ private fullscreenCreated = false;
418
+ private fullscreenStopped = false;
419
+ private parentRestoreQueued = false;
420
+ private parentRestorePromise: Promise<void> | undefined;
421
+ private cleanupError: unknown;
422
+ private readonly lifetimeController = new AbortController();
423
+
424
+ constructor(
425
+ private readonly parent: TUI,
426
+ private readonly theme: Theme,
427
+ private readonly keybindings: KeybindingsManager,
428
+ private readonly ctx: ExtensionCommandContext,
429
+ private readonly run: (ctx: ExtensionCommandContext) => Promise<T>,
430
+ private readonly done: (outcome: FullscreenOutcome<T>) => void,
431
+ private readonly createTui: BtwFullscreenTuiFactory,
432
+ private readonly options: BtwFullscreenOptions,
433
+ ) {
434
+ queueMicrotask(() => void this.start());
435
+ }
436
+
437
+ setParentOverlay(overlay: OverlayHandle): void {
438
+ this.parentOverlay = overlay;
439
+ }
440
+
441
+ render(width: number): string[] {
442
+ return [truncateToWidth(this.theme.fg("muted", "Opening btw side thread…"), width)];
443
+ }
444
+
445
+ invalidate(): void {}
446
+
447
+ dispose(): void {
448
+ if (this.disposed || this.finished) return;
449
+ this.disposed = true;
450
+ this.lifetimeController.abort();
451
+ this.cancelActiveCustom?.();
452
+ }
453
+
454
+ private async start(): Promise<void> {
455
+ if (this.started || this.finished) return;
456
+ this.started = true;
457
+ this.watchUpstreamCancellation();
458
+ let outcome: FullscreenOutcome<T>;
459
+ try {
460
+ if (this.disposed) throw new FullscreenUiDisposedError();
461
+ this.parent.stop({ preserveScreen: true });
462
+ this.parentStopped = true;
463
+ if (this.disposed) throw new FullscreenUiDisposedError();
464
+ this.fullscreen = this.createTui(this.parent, this.theme, this.keybindings, this.options);
465
+ this.fullscreenCreated = true;
466
+ this.fullscreen.start();
467
+ const shortcuts = resolveBtwShortcuts(
468
+ this.options.keybindings,
469
+ this.keybindings,
470
+ this.options.copyOnSelect ?? true,
471
+ );
472
+ setBtwShortcuts(this.fullscreen, shortcuts);
473
+ // Negotiate before warning when possible: the first dispatched user input uses
474
+ // the current mode. Recheck each input, including later mode transitions.
475
+ let previousWarnings: readonly string[] = [];
476
+ const reportWarnings = () => {
477
+ const warnings = shortcuts.warnings;
478
+ for (const warning of warnings) {
479
+ if (previousWarnings.includes(warning)) continue;
480
+ try {
481
+ this.ctx.ui.notify(`Pi BTW: ${warning}`, "warning");
482
+ } catch {
483
+ /* A replaced context must not prevent terminal cleanup. */
484
+ }
485
+ }
486
+ previousWarnings = warnings;
487
+ };
488
+ const pasteGuard = new BtwPasteGuard();
489
+ // Waiting for the custom promise would leave follow-up keys bound to the side TUI.
490
+ const addHardCancelListener =
491
+ this.fullscreen.addInputListenerBeforeAll?.bind(this.fullscreen) ??
492
+ this.fullscreen.addInputListenerBeforeViewport?.bind(this.fullscreen) ??
493
+ this.fullscreen.addInputListener.bind(this.fullscreen);
494
+ this.removeHardCancelListener = addHardCancelListener((data) => {
495
+ reportWarnings();
496
+ if (pasteGuard.consume(data) || !shortcuts.matches(data, "exit")) return undefined;
497
+ this.disposed = true;
498
+ this.lifetimeController.abort();
499
+ try {
500
+ this.hardCancelActiveCustom?.();
501
+ } finally {
502
+ // ProcessTerminal.stop() destroys its active input buffer. Keep cancellation
503
+ // synchronous, then drain input before the physical Windows terminal handoff.
504
+ // Pi has no public input injection, so do not replay bytes already coalesced
505
+ // behind the hard-cancel key.
506
+ this.queueParentRestore();
507
+ }
508
+ return { consume: true };
509
+ });
510
+ outcome = { kind: "completed", value: await this.run(this.createContext()) };
511
+ } catch (error) {
512
+ outcome = { kind: "failed", error };
513
+ }
514
+
515
+ try {
516
+ this.cancelActiveCustom?.();
517
+ } catch (error) {
518
+ this.cleanupError ??= error;
519
+ }
520
+ if (this.parentRestorePromise) await this.parentRestorePromise;
521
+ else this.restoreParent();
522
+ if (this.cleanupError !== undefined) outcome = { kind: "failed", error: this.cleanupError };
523
+ this.finished = true;
524
+ this.done(outcome);
525
+ }
526
+
527
+ private watchUpstreamCancellation(): void {
528
+ const signal = this.ctx.signal;
529
+ if (!signal) return;
530
+ const onAbort = () => this.dispose();
531
+ signal.addEventListener("abort", onAbort, { once: true });
532
+ this.removeUpstreamAbortListener = () => signal.removeEventListener("abort", onAbort);
533
+ if (signal.aborted) onAbort();
534
+ }
535
+
536
+ private queueParentRestore(): void {
537
+ if (this.parentRestoreQueued || this.parentRestoreAttempted) return;
538
+ this.parentRestoreQueued = true;
539
+ this.parentRestorePromise = Promise.resolve().then(async () => {
540
+ try {
541
+ await this.fullscreen?.terminal.drainInput?.();
542
+ } catch (error) {
543
+ this.cleanupError ??= error;
544
+ }
545
+ this.parentRestoreQueued = false;
546
+ this.restoreParent();
547
+ });
548
+ }
549
+
550
+ private restoreParent(): void {
551
+ const removeUpstreamAbortListener = this.removeUpstreamAbortListener;
552
+ this.removeUpstreamAbortListener = undefined;
553
+ try {
554
+ removeUpstreamAbortListener?.();
555
+ } catch (error) {
556
+ this.cleanupError ??= error;
557
+ }
558
+ const removeHardCancelListener = this.removeHardCancelListener;
559
+ this.removeHardCancelListener = undefined;
560
+ try {
561
+ removeHardCancelListener?.();
562
+ } catch (error) {
563
+ this.cleanupError ??= error;
564
+ }
565
+ if (this.fullscreenCreated && !this.fullscreenStopped) {
566
+ this.fullscreenStopped = true;
567
+ try {
568
+ this.fullscreen?.stop({ preserveScreen: true });
569
+ } catch (error) {
570
+ this.cleanupError ??= error;
571
+ }
572
+ }
573
+ if (!this.parentStopped || this.parentRestoreAttempted) return;
574
+ const parentOverlay = this.parentOverlay;
575
+ this.parentOverlay = undefined;
576
+ try {
577
+ parentOverlay?.setHidden(true);
578
+ } catch (error) {
579
+ this.cleanupError ??= error;
580
+ }
581
+ try {
582
+ this.parentRestoreAttempted = true;
583
+ this.parent.start();
584
+ this.parent.renderNow(false);
585
+ } catch (error) {
586
+ this.cleanupError ??= error;
587
+ }
588
+ }
589
+
590
+ private createContext(): ExtensionCommandContext {
591
+ const ui = new Proxy(this.ctx.ui, {
592
+ get: (target, property) => {
593
+ if (property === "custom") {
594
+ return <Value>(factory: BtwCustomFactory<Value>, options?: BtwCustomOptions) =>
595
+ this.showCustom(factory, options);
596
+ }
597
+ if (property === "notify") {
598
+ return (message: string, level?: Parameters<ExtensionCommandContext["ui"]["notify"]>[1]) => {
599
+ target.notify(message, level);
600
+ const display = sanitizeSingleLine(message);
601
+ if (display) this.fullscreen?.flash?.(display);
602
+ };
603
+ }
604
+ const value = Reflect.get(target, property, target) as unknown;
605
+ return typeof value === "function" ? value.bind(target) : value;
606
+ },
607
+ });
608
+ const signal = this.ctx.signal
609
+ ? AbortSignal.any([this.ctx.signal, this.lifetimeController.signal])
610
+ : this.lifetimeController.signal;
611
+ return new Proxy(this.ctx, {
612
+ get: (target, property) => {
613
+ if (property === "ui") return ui;
614
+ if (property === "signal") return signal;
615
+ return Reflect.get(target, property, target);
616
+ },
617
+ });
618
+ }
619
+
620
+ private showCustom<Value>(factory: BtwCustomFactory<Value>, options?: BtwCustomOptions): Promise<Value> {
621
+ const fullscreen = this.fullscreen;
622
+ if (!fullscreen || this.disposed || this.finished) {
623
+ return Promise.reject(new FullscreenUiDisposedError());
624
+ }
625
+ if (this.cancelActiveCustom) {
626
+ return Promise.reject(new Error("pi-btw attempted to open overlapping custom UI."));
627
+ }
628
+
629
+ return new Promise<Value>((resolve, reject) => {
630
+ let component: (Component & { dispose?(): void }) | undefined;
631
+ let overlay: OverlayHandle | undefined;
632
+ let mounted = false;
633
+ let layoutMounted = false;
634
+ let factorySettled = false;
635
+ let closed = false;
636
+ let promiseSettled = false;
637
+ let componentDisposed = false;
638
+ let pendingValue: Value | undefined;
639
+ let hasPendingValue = false;
640
+
641
+ const disposeComponent = () => {
642
+ if (!component || componentDisposed) return;
643
+ componentDisposed = true;
644
+ try {
645
+ component.dispose?.();
646
+ } catch {
647
+ // Cleanup must continue so terminal ownership is restored.
648
+ }
649
+ };
650
+ const unmount = () => {
651
+ let cleanupError: unknown;
652
+ try {
653
+ if (overlay) overlay.hide();
654
+ else if (mounted && layoutMounted) fullscreen.setLayoutRoot(undefined);
655
+ else if (mounted && component) fullscreen.removeChild(component);
656
+ } catch (error) {
657
+ cleanupError = error;
658
+ }
659
+ if (overlay || mounted) {
660
+ try {
661
+ fullscreen.setFocus(null);
662
+ fullscreen.requestRender();
663
+ } catch (error) {
664
+ cleanupError ??= error;
665
+ }
666
+ }
667
+ disposeComponent();
668
+ if (cleanupError !== undefined) throw cleanupError;
669
+ };
670
+ const complete = () => {
671
+ if (promiseSettled || !hasPendingValue) return;
672
+ promiseSettled = true;
673
+ this.cancelActiveCustom = undefined;
674
+ this.hardCancelActiveCustom = undefined;
675
+ if (!factorySettled) {
676
+ resolve(pendingValue as Value);
677
+ return;
678
+ }
679
+ try {
680
+ unmount();
681
+ resolve(pendingValue as Value);
682
+ } catch (error) {
683
+ reject(error);
684
+ }
685
+ };
686
+ const close = (value: Value) => {
687
+ if (closed || promiseSettled) return;
688
+ closed = true;
689
+ pendingValue = value;
690
+ hasPendingValue = true;
691
+ complete();
692
+ };
693
+ const fail = (error: unknown) => {
694
+ if (promiseSettled) return;
695
+ closed = true;
696
+ promiseSettled = true;
697
+ this.cancelActiveCustom = undefined;
698
+ this.hardCancelActiveCustom = undefined;
699
+ try {
700
+ unmount();
701
+ reject(error);
702
+ } catch (cleanupError) {
703
+ reject(cleanupError);
704
+ }
705
+ };
706
+ this.cancelActiveCustom = () => {
707
+ if (promiseSettled) return;
708
+ disposeComponent();
709
+ if (!promiseSettled) fail(new FullscreenUiDisposedError());
710
+ };
711
+ this.hardCancelActiveCustom = () => {
712
+ if (promiseSettled) return;
713
+ try {
714
+ component?.handleInput?.("\u0003");
715
+ } catch (error) {
716
+ fail(error);
717
+ return;
718
+ }
719
+ this.cancelActiveCustom?.();
720
+ };
721
+
722
+ let created: ReturnType<BtwCustomFactory<Value>>;
723
+ try {
724
+ created = factory(fullscreen, this.theme, this.keybindings, close);
725
+ } catch (error) {
726
+ factorySettled = true;
727
+ fail(error);
728
+ return;
729
+ }
730
+ Promise.resolve(created)
731
+ .then((value) => {
732
+ component = value;
733
+ factorySettled = true;
734
+ if (promiseSettled) {
735
+ disposeComponent();
736
+ return;
737
+ }
738
+ if (closed) {
739
+ complete();
740
+ return;
741
+ }
742
+ if (options?.overlay) {
743
+ const overlayOptions =
744
+ typeof options.overlayOptions === "function" ? options.overlayOptions() : options.overlayOptions;
745
+ overlay = fullscreen.showOverlay(component, overlayOptions);
746
+ options.onHandle?.(overlay);
747
+ } else {
748
+ fullscreen.clear();
749
+ mounted = true;
750
+ if (isFullscreenLayoutComponent(component)) {
751
+ layoutMounted = true;
752
+ fullscreen.setLayoutRoot(component.getFullscreenLayout());
753
+ } else {
754
+ fullscreen.addChild(component);
755
+ }
756
+ fullscreen.setFocus(component);
757
+ fullscreen.requestRender();
758
+ }
759
+ })
760
+ .catch(fail);
761
+ });
762
+ }
762
763
  }
763
764
 
764
- function isFullscreenLayoutComponent(
765
- component: Component,
766
- ): component is BtwFullscreenLayoutComponent {
767
- return "getFullscreenLayout" in component && typeof component.getFullscreenLayout === "function";
765
+ function isFullscreenLayoutComponent(component: Component): component is BtwFullscreenLayoutComponent {
766
+ return "getFullscreenLayout" in component && typeof component.getFullscreenLayout === "function";
768
767
  }