@herbertgao/pi-extensions 2026.9.6 → 2026.9.8

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 (304) hide show
  1. package/README.md +3 -8
  2. package/THIRD_PARTY_NOTICES.md +0 -4
  3. package/node_modules/@narumitw/pi-btw/README.md +2 -0
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +52 -53
  7. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
  8. package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
  9. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
  10. package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
  11. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
  12. package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
  13. package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
  14. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
  15. package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
  16. package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
  17. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
  18. package/node_modules/grok-mermaid/CHANGELOG.md +46 -0
  19. package/node_modules/grok-mermaid/LICENSE +205 -0
  20. package/node_modules/grok-mermaid/README.md +191 -0
  21. package/node_modules/grok-mermaid/dist/ansi.d.ts +16 -0
  22. package/node_modules/grok-mermaid/dist/ansi.d.ts.map +1 -0
  23. package/node_modules/grok-mermaid/dist/ansi.js +23 -0
  24. package/node_modules/grok-mermaid/dist/ansi.js.map +1 -0
  25. package/node_modules/grok-mermaid/dist/canvas.d.ts +87 -0
  26. package/node_modules/grok-mermaid/dist/canvas.d.ts.map +1 -0
  27. package/node_modules/grok-mermaid/dist/canvas.js +366 -0
  28. package/node_modules/grok-mermaid/dist/canvas.js.map +1 -0
  29. package/node_modules/grok-mermaid/dist/graph.d.ts +74 -0
  30. package/node_modules/grok-mermaid/dist/graph.d.ts.map +1 -0
  31. package/node_modules/grok-mermaid/dist/graph.js +91 -0
  32. package/node_modules/grok-mermaid/dist/graph.js.map +1 -0
  33. package/node_modules/grok-mermaid/dist/index.d.ts +32 -0
  34. package/node_modules/grok-mermaid/dist/index.d.ts.map +1 -0
  35. package/node_modules/grok-mermaid/dist/index.js +100 -0
  36. package/node_modules/grok-mermaid/dist/index.js.map +1 -0
  37. package/node_modules/grok-mermaid/dist/labels.d.ts +62 -0
  38. package/node_modules/grok-mermaid/dist/labels.d.ts.map +1 -0
  39. package/node_modules/grok-mermaid/dist/labels.js +324 -0
  40. package/node_modules/grok-mermaid/dist/labels.js.map +1 -0
  41. package/node_modules/grok-mermaid/dist/layout-seq.d.ts +12 -0
  42. package/node_modules/grok-mermaid/dist/layout-seq.d.ts.map +1 -0
  43. package/node_modules/grok-mermaid/dist/layout-seq.js +194 -0
  44. package/node_modules/grok-mermaid/dist/layout-seq.js.map +1 -0
  45. package/node_modules/grok-mermaid/dist/layout.d.ts +87 -0
  46. package/node_modules/grok-mermaid/dist/layout.d.ts.map +1 -0
  47. package/node_modules/grok-mermaid/dist/layout.js +881 -0
  48. package/node_modules/grok-mermaid/dist/layout.js.map +1 -0
  49. package/node_modules/grok-mermaid/dist/parse.d.ts +83 -0
  50. package/node_modules/grok-mermaid/dist/parse.d.ts.map +1 -0
  51. package/node_modules/grok-mermaid/dist/parse.js +1151 -0
  52. package/node_modules/grok-mermaid/dist/parse.js.map +1 -0
  53. package/node_modules/grok-mermaid/dist/source-box.d.ts +18 -0
  54. package/node_modules/grok-mermaid/dist/source-box.d.ts.map +1 -0
  55. package/node_modules/grok-mermaid/dist/source-box.js +78 -0
  56. package/node_modules/grok-mermaid/dist/source-box.js.map +1 -0
  57. package/node_modules/grok-mermaid/dist/types.d.ts +42 -0
  58. package/node_modules/grok-mermaid/dist/types.d.ts.map +1 -0
  59. package/node_modules/grok-mermaid/dist/types.js +1 -0
  60. package/node_modules/grok-mermaid/dist/types.js.map +1 -0
  61. package/node_modules/grok-mermaid/dist/width-data.d.ts +2 -0
  62. package/node_modules/grok-mermaid/dist/width-data.d.ts.map +1 -0
  63. package/node_modules/grok-mermaid/dist/width-data.js +994 -0
  64. package/node_modules/grok-mermaid/dist/width-data.js.map +1 -0
  65. package/node_modules/grok-mermaid/dist/width.d.ts +18 -0
  66. package/node_modules/grok-mermaid/dist/width.d.ts.map +1 -0
  67. package/node_modules/grok-mermaid/dist/width.js +76 -0
  68. package/node_modules/grok-mermaid/dist/width.js.map +1 -0
  69. package/node_modules/grok-mermaid/package.json +49 -0
  70. package/node_modules/grok-mermaid/src/ansi.ts +34 -0
  71. package/node_modules/grok-mermaid/src/canvas.ts +373 -0
  72. package/node_modules/grok-mermaid/src/graph.ts +142 -0
  73. package/node_modules/grok-mermaid/src/index.ts +104 -0
  74. package/node_modules/grok-mermaid/src/labels.ts +326 -0
  75. package/node_modules/grok-mermaid/src/layout-seq.ts +203 -0
  76. package/node_modules/grok-mermaid/src/layout.ts +1015 -0
  77. package/node_modules/grok-mermaid/src/parse.ts +1189 -0
  78. package/node_modules/grok-mermaid/src/source-box.ts +89 -0
  79. package/node_modules/grok-mermaid/src/types.ts +43 -0
  80. package/node_modules/grok-mermaid/src/width-data.ts +993 -0
  81. package/node_modules/grok-mermaid/src/width.ts +74 -0
  82. package/node_modules/pi-lens/CHANGELOG.md +239 -0
  83. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
  84. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
  85. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
  86. package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
  87. package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
  88. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
  89. package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
  90. package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
  91. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
  92. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
  93. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
  95. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
  97. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
  99. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
  100. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
  108. package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
  109. package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
  110. package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
  111. package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
  112. package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
  113. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
  114. package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
  115. package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
  116. package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
  117. package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
  118. package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
  119. package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
  120. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
  121. package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
  122. package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
  123. package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
  124. package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
  125. package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
  126. package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
  127. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
  128. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
  129. package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
  130. package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
  131. package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
  132. package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
  133. package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
  134. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
  135. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
  136. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
  137. package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
  138. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
  139. package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
  140. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
  141. package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
  142. package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
  143. package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
  144. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
  145. package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
  146. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
  147. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
  148. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
  149. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
  150. package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
  151. package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
  152. package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
  153. package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
  154. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
  155. package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
  156. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
  157. package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
  158. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
  159. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
  160. package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
  161. package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
  162. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
  163. package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
  164. package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
  165. package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
  166. package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
  167. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
  168. package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
  169. package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
  170. package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
  171. package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
  172. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
  173. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
  174. package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
  175. package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
  176. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
  177. package/node_modules/pi-lens/dist/index.js +27416 -24153
  178. package/node_modules/pi-lens/dist/mcp/server.js +5 -2
  179. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
  180. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
  181. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
  182. package/node_modules/pi-lens/docs/audit1.md +5 -5
  183. package/node_modules/pi-lens/docs/dependencies.md +11 -0
  184. package/node_modules/pi-lens/docs/dispositions.md +52 -0
  185. package/node_modules/pi-lens/docs/environment-variables.md +2 -2
  186. package/node_modules/pi-lens/docs/features.md +4 -4
  187. package/node_modules/pi-lens/docs/globalconfig.md +3 -1
  188. package/node_modules/pi-lens/docs/language-coverage.md +1 -0
  189. package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
  190. package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
  191. package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
  192. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
  193. package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
  194. package/node_modules/pi-lens/docs/settings.md +7 -4
  195. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  196. package/node_modules/pi-lens/docs/usage.md +2 -0
  197. package/node_modules/pi-lens/package.json +1 -1
  198. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
  199. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
  200. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
  201. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
  202. package/package.json +6 -11
  203. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
  204. package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
  205. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
  206. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
  207. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
  208. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
  209. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
  210. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  211. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  212. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
  213. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
  214. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
  215. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
  216. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
  217. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
  218. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
  219. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
  220. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
  221. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
  222. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
  223. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
  224. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
  225. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
  226. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
  227. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
  228. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
  229. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
  230. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
  231. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
  232. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
  233. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
  234. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
  235. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
  236. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
  237. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
  238. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
  239. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
  240. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
  241. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
  242. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
  243. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
  244. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
  245. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
  246. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
  247. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
  248. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
  249. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
  250. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
  251. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
  252. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
  253. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
  254. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
  255. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
  256. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
  257. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
  258. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
  259. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
  260. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
  261. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
  262. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
  263. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
  264. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
  265. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
  266. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
  267. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
  268. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
  269. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
  270. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
  271. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
  272. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
  273. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
  274. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
  275. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
  276. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
  277. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
  278. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
  279. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
  280. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
  281. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
  282. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
  283. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
  284. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
  285. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
  286. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
  287. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
  288. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
  289. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
  290. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
  291. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
  292. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
  293. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
  294. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
  295. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
  296. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
  297. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
  298. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
  299. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
  300. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
  301. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
  302. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
  303. package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
  304. 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
  }