@ashx-j/lunr 0.2.21 → 0.2.23

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 (461) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/README.md +23 -5
  3. package/dist/builtin-extensions/index.d.ts.map +1 -1
  4. package/dist/builtin-extensions/index.js +1 -0
  5. package/dist/builtin-extensions/index.js.map +1 -1
  6. package/dist/builtin-extensions/lunr-browser.d.ts +9 -0
  7. package/dist/builtin-extensions/lunr-browser.d.ts.map +1 -0
  8. package/dist/builtin-extensions/lunr-browser.js +169 -0
  9. package/dist/builtin-extensions/lunr-browser.js.map +1 -0
  10. package/dist/builtin-extensions/lunr-todos.d.ts.map +1 -1
  11. package/dist/builtin-extensions/lunr-todos.js +15 -0
  12. package/dist/builtin-extensions/lunr-todos.js.map +1 -1
  13. package/dist/builtin-extensions/pi-intercom/index.d.ts.map +1 -1
  14. package/dist/builtin-extensions/pi-intercom/index.js +3 -0
  15. package/dist/builtin-extensions/pi-intercom/index.js.map +1 -1
  16. package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.d.ts +1 -1
  17. package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.d.ts.map +1 -1
  18. package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.js +5 -1
  19. package/dist/builtin-extensions/pi-subagents/src/extension/control-notices.js.map +1 -1
  20. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts +2 -2
  21. package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
  22. package/dist/builtin-extensions/pi-subagents/src/extension/index.js +18 -5
  23. package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
  24. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts +4 -3
  25. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.d.ts.map +1 -1
  26. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js +3 -0
  27. package/dist/builtin-extensions/pi-subagents/src/extension/tool-description.js.map +1 -1
  28. package/dist/builtin-extensions/pi-subagents/src/intercom/communication.d.ts +17 -0
  29. package/dist/builtin-extensions/pi-subagents/src/intercom/communication.d.ts.map +1 -0
  30. package/dist/builtin-extensions/pi-subagents/src/intercom/communication.js +45 -0
  31. package/dist/builtin-extensions/pi-subagents/src/intercom/communication.js.map +1 -0
  32. package/dist/builtin-extensions/pi-subagents/src/intercom/intercom-bridge.d.ts.map +1 -1
  33. package/dist/builtin-extensions/pi-subagents/src/intercom/intercom-bridge.js +4 -11
  34. package/dist/builtin-extensions/pi-subagents/src/intercom/intercom-bridge.js.map +1 -1
  35. package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.d.ts +4 -1
  36. package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.d.ts.map +1 -1
  37. package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.js +160 -51
  38. package/dist/builtin-extensions/pi-subagents/src/intercom/native-supervisor-channel.js.map +1 -1
  39. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-ask.d.ts.map +1 -1
  40. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-ask.js +5 -0
  41. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-ask.js.map +1 -1
  42. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-questions.d.ts +2 -0
  43. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-questions.d.ts.map +1 -1
  44. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-questions.js +3 -2
  45. package/dist/builtin-extensions/pi-subagents/src/intercom/supervisor-questions.js.map +1 -1
  46. package/dist/builtin-extensions/pi-subagents/src/runs/background/auto-drain.d.ts +1 -0
  47. package/dist/builtin-extensions/pi-subagents/src/runs/background/auto-drain.d.ts.map +1 -1
  48. package/dist/builtin-extensions/pi-subagents/src/runs/background/auto-drain.js +2 -1
  49. package/dist/builtin-extensions/pi-subagents/src/runs/background/auto-drain.js.map +1 -1
  50. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-wait.d.ts +3 -0
  51. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-wait.d.ts.map +1 -1
  52. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-wait.js +11 -0
  53. package/dist/builtin-extensions/pi-subagents/src/runs/background/subagent-wait.js.map +1 -1
  54. package/dist/builtin-extensions/pi-subagents/src/runs/background/wait-tool.d.ts.map +1 -1
  55. package/dist/builtin-extensions/pi-subagents/src/runs/background/wait-tool.js +41 -3
  56. package/dist/builtin-extensions/pi-subagents/src/runs/background/wait-tool.js.map +1 -1
  57. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.d.ts.map +1 -1
  58. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js +4 -0
  59. package/dist/builtin-extensions/pi-subagents/src/runs/foreground/subagent-executor.js.map +1 -1
  60. package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.d.ts.map +1 -1
  61. package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.js +3 -0
  62. package/dist/builtin-extensions/pi-subagents/src/runs/shared/pi-args.js.map +1 -1
  63. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-control.d.ts +1 -0
  64. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-control.d.ts.map +1 -1
  65. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-control.js +3 -0
  66. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-control.js.map +1 -1
  67. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.d.ts.map +1 -1
  68. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.js +1 -1
  69. package/dist/builtin-extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.js.map +1 -1
  70. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +1 -0
  71. package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
  72. package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
  73. package/dist/builtin-extensions/pi-subagents/src/tui/communication.d.ts +18 -0
  74. package/dist/builtin-extensions/pi-subagents/src/tui/communication.d.ts.map +1 -0
  75. package/dist/builtin-extensions/pi-subagents/src/tui/communication.js +48 -0
  76. package/dist/builtin-extensions/pi-subagents/src/tui/communication.js.map +1 -0
  77. package/dist/builtin-extensions/pi-web-access/index.d.ts.map +1 -1
  78. package/dist/builtin-extensions/pi-web-access/index.js +11 -7
  79. package/dist/builtin-extensions/pi-web-access/index.js.map +1 -1
  80. package/dist/cli/args.d.ts.map +1 -1
  81. package/dist/cli/args.js +2 -1
  82. package/dist/cli/args.js.map +1 -1
  83. package/dist/cli/install-cli.d.ts.map +1 -1
  84. package/dist/cli/install-cli.js +15 -45
  85. package/dist/cli/install-cli.js.map +1 -1
  86. package/dist/core/agent-session-runtime.d.ts +9 -0
  87. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  88. package/dist/core/agent-session-runtime.js +126 -29
  89. package/dist/core/agent-session-runtime.js.map +1 -1
  90. package/dist/core/agent-session.d.ts +22 -0
  91. package/dist/core/agent-session.d.ts.map +1 -1
  92. package/dist/core/agent-session.js +250 -105
  93. package/dist/core/agent-session.js.map +1 -1
  94. package/dist/core/browser/network.d.ts +9 -0
  95. package/dist/core/browser/network.d.ts.map +1 -0
  96. package/dist/core/browser/network.js +146 -0
  97. package/dist/core/browser/network.js.map +1 -0
  98. package/dist/core/browser/runtime.d.ts +24 -0
  99. package/dist/core/browser/runtime.d.ts.map +1 -0
  100. package/dist/core/browser/runtime.js +289 -0
  101. package/dist/core/browser/runtime.js.map +1 -0
  102. package/dist/core/browser/schema.d.ts +16 -0
  103. package/dist/core/browser/schema.d.ts.map +1 -0
  104. package/dist/core/browser/schema.js +36 -0
  105. package/dist/core/browser/schema.js.map +1 -0
  106. package/dist/core/browser/settings.d.ts +8 -0
  107. package/dist/core/browser/settings.d.ts.map +1 -0
  108. package/dist/core/browser/settings.js +30 -0
  109. package/dist/core/browser/settings.js.map +1 -0
  110. package/dist/core/browser/setup.d.ts +2 -0
  111. package/dist/core/browser/setup.d.ts.map +1 -0
  112. package/dist/core/browser/setup.js +16 -0
  113. package/dist/core/browser/setup.js.map +1 -0
  114. package/dist/core/customize.d.ts.map +1 -1
  115. package/dist/core/customize.js +15 -14
  116. package/dist/core/customize.js.map +1 -1
  117. package/dist/core/export-html/index.d.ts.map +1 -1
  118. package/dist/core/export-html/index.js +1 -1
  119. package/dist/core/export-html/index.js.map +1 -1
  120. package/dist/core/extensions/runner.d.ts +1 -0
  121. package/dist/core/extensions/runner.d.ts.map +1 -1
  122. package/dist/core/extensions/runner.js +1 -1
  123. package/dist/core/extensions/runner.js.map +1 -1
  124. package/dist/core/extensions/types.d.ts +1 -1
  125. package/dist/core/extensions/types.d.ts.map +1 -1
  126. package/dist/core/extensions/types.js.map +1 -1
  127. package/dist/core/install-features.d.ts.map +1 -1
  128. package/dist/core/install-features.js +10 -8
  129. package/dist/core/install-features.js.map +1 -1
  130. package/dist/core/memory-cap.d.ts.map +1 -1
  131. package/dist/core/memory-cap.js +5 -3
  132. package/dist/core/memory-cap.js.map +1 -1
  133. package/dist/core/model-tiers.d.ts.map +1 -1
  134. package/dist/core/model-tiers.js +9 -6
  135. package/dist/core/model-tiers.js.map +1 -1
  136. package/dist/core/permissions.d.ts.map +1 -1
  137. package/dist/core/permissions.js +9 -0
  138. package/dist/core/permissions.js.map +1 -1
  139. package/dist/core/plan-mode.d.ts.map +1 -1
  140. package/dist/core/plan-mode.js +3 -0
  141. package/dist/core/plan-mode.js.map +1 -1
  142. package/dist/core/process-registry.d.ts.map +1 -1
  143. package/dist/core/process-registry.js +6 -4
  144. package/dist/core/process-registry.js.map +1 -1
  145. package/dist/core/runtime-scope.d.ts +11 -0
  146. package/dist/core/runtime-scope.d.ts.map +1 -0
  147. package/dist/core/runtime-scope.js +3 -0
  148. package/dist/core/runtime-scope.js.map +1 -0
  149. package/dist/core/sdk.d.ts.map +1 -1
  150. package/dist/core/sdk.js +13 -0
  151. package/dist/core/sdk.js.map +1 -1
  152. package/dist/core/session-handoff.d.ts +35 -0
  153. package/dist/core/session-handoff.d.ts.map +1 -0
  154. package/dist/core/session-handoff.js +214 -0
  155. package/dist/core/session-handoff.js.map +1 -0
  156. package/dist/core/session-manager.d.ts +20 -0
  157. package/dist/core/session-manager.d.ts.map +1 -1
  158. package/dist/core/session-manager.js +184 -22
  159. package/dist/core/session-manager.js.map +1 -1
  160. package/dist/core/session-ownership.d.ts +28 -0
  161. package/dist/core/session-ownership.d.ts.map +1 -0
  162. package/dist/core/session-ownership.js +138 -0
  163. package/dist/core/session-ownership.js.map +1 -0
  164. package/dist/core/session-retention.d.ts.map +1 -1
  165. package/dist/core/session-retention.js +11 -2
  166. package/dist/core/session-retention.js.map +1 -1
  167. package/dist/core/settings-manager.d.ts +11 -0
  168. package/dist/core/settings-manager.d.ts.map +1 -1
  169. package/dist/core/settings-manager.js +27 -0
  170. package/dist/core/settings-manager.js.map +1 -1
  171. package/dist/core/slash-commands.d.ts.map +1 -1
  172. package/dist/core/slash-commands.js +2 -0
  173. package/dist/core/slash-commands.js.map +1 -1
  174. package/dist/core/subagent-wait-interruption.d.ts +11 -0
  175. package/dist/core/subagent-wait-interruption.d.ts.map +1 -0
  176. package/dist/core/subagent-wait-interruption.js +19 -0
  177. package/dist/core/subagent-wait-interruption.js.map +1 -0
  178. package/dist/core/system-prompt.d.ts.map +1 -1
  179. package/dist/core/system-prompt.js +7 -2
  180. package/dist/core/system-prompt.js.map +1 -1
  181. package/dist/core/todo-settings.d.ts +5 -0
  182. package/dist/core/todo-settings.d.ts.map +1 -0
  183. package/dist/core/todo-settings.js +7 -0
  184. package/dist/core/todo-settings.js.map +1 -0
  185. package/dist/core/tools/bash.d.ts.map +1 -1
  186. package/dist/core/tools/bash.js +4 -4
  187. package/dist/core/tools/bash.js.map +1 -1
  188. package/dist/core/tools/edit.d.ts.map +1 -1
  189. package/dist/core/tools/edit.js +5 -3
  190. package/dist/core/tools/edit.js.map +1 -1
  191. package/dist/core/tools/find.d.ts.map +1 -1
  192. package/dist/core/tools/find.js +4 -4
  193. package/dist/core/tools/find.js.map +1 -1
  194. package/dist/core/tools/grep.d.ts.map +1 -1
  195. package/dist/core/tools/grep.js +4 -4
  196. package/dist/core/tools/grep.js.map +1 -1
  197. package/dist/core/tools/ls.d.ts.map +1 -1
  198. package/dist/core/tools/ls.js +4 -4
  199. package/dist/core/tools/ls.js.map +1 -1
  200. package/dist/core/tools/read.d.ts.map +1 -1
  201. package/dist/core/tools/read.js +4 -4
  202. package/dist/core/tools/read.js.map +1 -1
  203. package/dist/core/tools/render-utils.d.ts +14 -5
  204. package/dist/core/tools/render-utils.d.ts.map +1 -1
  205. package/dist/core/tools/render-utils.js +34 -7
  206. package/dist/core/tools/render-utils.js.map +1 -1
  207. package/dist/core/tools/write.d.ts.map +1 -1
  208. package/dist/core/tools/write.js +4 -4
  209. package/dist/core/tools/write.js.map +1 -1
  210. package/dist/core/usage-service.d.ts.map +1 -1
  211. package/dist/core/usage-service.js +5 -3
  212. package/dist/core/usage-service.js.map +1 -1
  213. package/dist/gateway/adapters/discord.d.ts +12 -1
  214. package/dist/gateway/adapters/discord.d.ts.map +1 -1
  215. package/dist/gateway/adapters/discord.js +112 -17
  216. package/dist/gateway/adapters/discord.js.map +1 -1
  217. package/dist/gateway/adapters/telegram.d.ts +4 -2
  218. package/dist/gateway/adapters/telegram.d.ts.map +1 -1
  219. package/dist/gateway/adapters/telegram.js +34 -11
  220. package/dist/gateway/adapters/telegram.js.map +1 -1
  221. package/dist/gateway/agent-bridge.d.ts +14 -3
  222. package/dist/gateway/agent-bridge.d.ts.map +1 -1
  223. package/dist/gateway/agent-bridge.js +252 -60
  224. package/dist/gateway/agent-bridge.js.map +1 -1
  225. package/dist/gateway/approval.d.ts +1 -0
  226. package/dist/gateway/approval.d.ts.map +1 -1
  227. package/dist/gateway/approval.js +60 -18
  228. package/dist/gateway/approval.js.map +1 -1
  229. package/dist/gateway/authz.d.ts +2 -0
  230. package/dist/gateway/authz.d.ts.map +1 -1
  231. package/dist/gateway/authz.js +13 -0
  232. package/dist/gateway/authz.js.map +1 -1
  233. package/dist/gateway/buttons.d.ts +3 -1
  234. package/dist/gateway/buttons.d.ts.map +1 -1
  235. package/dist/gateway/buttons.js +18 -3
  236. package/dist/gateway/buttons.js.map +1 -1
  237. package/dist/gateway/command.d.ts.map +1 -1
  238. package/dist/gateway/command.js +49 -4
  239. package/dist/gateway/command.js.map +1 -1
  240. package/dist/gateway/commands.d.ts.map +1 -1
  241. package/dist/gateway/commands.js +18 -3
  242. package/dist/gateway/commands.js.map +1 -1
  243. package/dist/gateway/config.d.ts +5 -0
  244. package/dist/gateway/config.d.ts.map +1 -1
  245. package/dist/gateway/config.js +5 -0
  246. package/dist/gateway/config.js.map +1 -1
  247. package/dist/gateway/conversations.d.ts +14 -0
  248. package/dist/gateway/conversations.d.ts.map +1 -0
  249. package/dist/gateway/conversations.js +30 -0
  250. package/dist/gateway/conversations.js.map +1 -0
  251. package/dist/gateway/download.d.ts +2 -0
  252. package/dist/gateway/download.d.ts.map +1 -0
  253. package/dist/gateway/download.js +36 -0
  254. package/dist/gateway/download.js.map +1 -0
  255. package/dist/gateway/index.d.ts.map +1 -1
  256. package/dist/gateway/index.js +85 -26
  257. package/dist/gateway/index.js.map +1 -1
  258. package/dist/gateway/mobile-commands.d.ts +24 -0
  259. package/dist/gateway/mobile-commands.d.ts.map +1 -0
  260. package/dist/gateway/mobile-commands.js +422 -0
  261. package/dist/gateway/mobile-commands.js.map +1 -0
  262. package/dist/gateway/presenter.d.ts +16 -0
  263. package/dist/gateway/presenter.d.ts.map +1 -0
  264. package/dist/gateway/presenter.js +210 -0
  265. package/dist/gateway/presenter.js.map +1 -0
  266. package/dist/gateway/router.d.ts +1 -0
  267. package/dist/gateway/router.d.ts.map +1 -1
  268. package/dist/gateway/router.js +32 -3
  269. package/dist/gateway/router.js.map +1 -1
  270. package/dist/gateway/service.d.ts +46 -0
  271. package/dist/gateway/service.d.ts.map +1 -0
  272. package/dist/gateway/service.js +395 -0
  273. package/dist/gateway/service.js.map +1 -0
  274. package/dist/gateway/setup.d.ts +4 -0
  275. package/dist/gateway/setup.d.ts.map +1 -0
  276. package/dist/gateway/setup.js +146 -0
  277. package/dist/gateway/setup.js.map +1 -0
  278. package/dist/gateway/types.d.ts +4 -2
  279. package/dist/gateway/types.d.ts.map +1 -1
  280. package/dist/gateway/types.js.map +1 -1
  281. package/dist/index.d.ts +2 -0
  282. package/dist/index.d.ts.map +1 -1
  283. package/dist/index.js +2 -0
  284. package/dist/index.js.map +1 -1
  285. package/dist/main.d.ts.map +1 -1
  286. package/dist/main.js +2 -1
  287. package/dist/main.js.map +1 -1
  288. package/dist/modes/interactive/components/assistant-message.d.ts +19 -2
  289. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  290. package/dist/modes/interactive/components/assistant-message.js +78 -17
  291. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  292. package/dist/modes/interactive/components/copyable-text.d.ts +27 -0
  293. package/dist/modes/interactive/components/copyable-text.d.ts.map +1 -0
  294. package/dist/modes/interactive/components/copyable-text.js +156 -0
  295. package/dist/modes/interactive/components/copyable-text.js.map +1 -0
  296. package/dist/modes/interactive/components/custom-entry.d.ts +1 -0
  297. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -1
  298. package/dist/modes/interactive/components/custom-entry.js +8 -0
  299. package/dist/modes/interactive/components/custom-entry.js.map +1 -1
  300. package/dist/modes/interactive/components/custom-message.d.ts +1 -0
  301. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  302. package/dist/modes/interactive/components/custom-message.js +8 -0
  303. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  304. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  305. package/dist/modes/interactive/components/session-selector.js +16 -0
  306. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  307. package/dist/modes/interactive/components/settings-selector.d.ts +8 -1
  308. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  309. package/dist/modes/interactive/components/settings-selector.js +51 -0
  310. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  311. package/dist/modes/interactive/components/thinking-tail.d.ts +9 -2
  312. package/dist/modes/interactive/components/thinking-tail.d.ts.map +1 -1
  313. package/dist/modes/interactive/components/thinking-tail.js +43 -1
  314. package/dist/modes/interactive/components/thinking-tail.js.map +1 -1
  315. package/dist/modes/interactive/components/tool-execution.d.ts +4 -3
  316. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  317. package/dist/modes/interactive/components/tool-execution.js +34 -45
  318. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  319. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  320. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  321. package/dist/modes/interactive/interactive-mode.js +267 -44
  322. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  323. package/dist/modes/interactive/theme/theme.d.ts +1 -0
  324. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  325. package/dist/modes/interactive/theme/theme.js +23 -0
  326. package/dist/modes/interactive/theme/theme.js.map +1 -1
  327. package/dist/node-runtime/agent-session-services-GZVSOGYR.js +49 -0
  328. package/dist/node-runtime/{builtin-extensions-JCOPAAB5.js → builtin-extensions-QVCXNQT4.js} +6 -5
  329. package/dist/node-runtime/{chunk-LTYWTR3V.js → chunk-2MI5OPMN.js} +17 -2
  330. package/dist/node-runtime/{chunk-W3CVH5RX.js → chunk-2TKODGBZ.js} +11 -6
  331. package/dist/node-runtime/chunk-4EULYYR4.js +62 -0
  332. package/dist/node-runtime/{chunk-6N22HULO.js → chunk-5D7LC2ZQ.js} +7 -2
  333. package/dist/node-runtime/{chunk-KBKMCJDU.js → chunk-5ZQXRPMC.js} +19 -4
  334. package/dist/node-runtime/{chunk-BOHIDQT4.js → chunk-6VYZ5UFP.js} +1 -1
  335. package/dist/node-runtime/chunk-AZJHNQKQ.js +205 -0
  336. package/dist/node-runtime/{chunk-A3KMVBZV.js → chunk-BBUVIEZB.js} +1 -10
  337. package/dist/node-runtime/{chunk-IRPJXI6U.js → chunk-BX2RB477.js} +1 -1
  338. package/dist/node-runtime/chunk-CCCN2UCX.js +14 -0
  339. package/dist/node-runtime/{chunk-25I2BHAA.js → chunk-CSUZVNYE.js} +1 -1
  340. package/dist/node-runtime/chunk-DD5YWFSJ.js +708 -0
  341. package/dist/node-runtime/chunk-DQMGU675.js +3409 -0
  342. package/dist/node-runtime/{chunk-QZMODZQ2.js → chunk-E24EO34J.js} +2 -1
  343. package/dist/node-runtime/chunk-EKVFP6YF.js +355 -0
  344. package/dist/node-runtime/chunk-GJ2RDBRL.js +672 -0
  345. package/dist/node-runtime/{chunk-W3SILANP.js → chunk-GMHJCCEN.js} +8 -8
  346. package/dist/node-runtime/{chunk-DDWHZ633.js → chunk-GS42T6MX.js} +1 -1
  347. package/dist/node-runtime/chunk-GS66LV4M.js +2830 -0
  348. package/dist/node-runtime/chunk-H4K3JDRB.js +47 -0
  349. package/dist/node-runtime/{chunk-2SJB2AWP.js → chunk-HMBGROQK.js} +3 -3
  350. package/dist/node-runtime/{chunk-GB2MGFLI.js → chunk-HPUP7E4T.js} +216 -201
  351. package/dist/node-runtime/{chunk-YMEAYRBE.js → chunk-HTFRRDSF.js} +2 -7
  352. package/dist/node-runtime/{chunk-OPEK5RRY.js → chunk-IF24A3FH.js} +1 -1
  353. package/dist/node-runtime/chunk-IMLCWLPI.js +9 -0
  354. package/dist/node-runtime/chunk-KKUTQQV7.js +144 -0
  355. package/dist/node-runtime/chunk-LFZDVKSP.js +521 -0
  356. package/dist/node-runtime/{chunk-2BUAYZ7K.js → chunk-LLUIPGDR.js} +581 -197
  357. package/dist/node-runtime/{chunk-7NBZ5P7I.js → chunk-NAY6UUQZ.js} +1 -1
  358. package/dist/node-runtime/chunk-OAR4TYZE.js +146 -0
  359. package/dist/node-runtime/{chunk-STERBGUB.js → chunk-OFKSVRV7.js} +2 -2
  360. package/dist/node-runtime/{chunk-JZQ7F36T.js → chunk-PCEW76PM.js} +19 -3
  361. package/dist/node-runtime/{chunk-WLLDXJRT.js → chunk-QAZOULTU.js} +29 -1014
  362. package/dist/node-runtime/{chunk-6KDIOJL3.js → chunk-QH6YL7VK.js} +3 -3
  363. package/dist/node-runtime/chunk-R3K2F5GI.js +122 -0
  364. package/dist/node-runtime/{chunk-RNBHABBA.js → chunk-RDDAZOMU.js} +23 -0
  365. package/dist/node-runtime/{chunk-SY2TFMLO.js → chunk-RIR5EDDJ.js} +46 -151
  366. package/dist/node-runtime/chunk-RMQZU2YU.js +117 -0
  367. package/dist/node-runtime/{chunk-6BEIBLQ3.js → chunk-RWTKOC3B.js} +1 -1
  368. package/dist/node-runtime/{chunk-G6LRAP36.js → chunk-TQGPP4SC.js} +1 -1
  369. package/dist/node-runtime/{chunk-Y5KOEMBH.js → chunk-TVJUOJZJ.js} +9 -674
  370. package/dist/node-runtime/chunk-UAEOTO52.js +119 -0
  371. package/dist/node-runtime/{chunk-AY6CAJSL.js → chunk-UJOCPOMX.js} +6 -3
  372. package/dist/node-runtime/chunk-UP2XBVH5.js +40 -0
  373. package/dist/node-runtime/{chunk-WJVFOIB5.js → chunk-UQUCFAJ3.js} +1 -1
  374. package/dist/node-runtime/chunk-URD2SLIT.js +1019 -0
  375. package/dist/node-runtime/chunk-US544NN6.js +13 -0
  376. package/dist/node-runtime/chunk-UWPJL22I.js +341 -0
  377. package/dist/node-runtime/{chunk-4I23VTC4.js → chunk-VWCE2HSZ.js} +21 -124
  378. package/dist/node-runtime/{chunk-VZR3NCM7.js → chunk-W64JC4E7.js} +18 -14
  379. package/dist/node-runtime/{chunk-AWVD5IEY.js → chunk-X3URCYCU.js} +23 -12
  380. package/dist/node-runtime/{chunk-GKLEQ3I4.js → chunk-XLTL257W.js} +126 -27
  381. package/dist/node-runtime/{chunk-WKDK4C5P.js → chunk-YDO2F2IA.js} +38 -164
  382. package/dist/node-runtime/{chunk-PDNXVXMD.js → chunk-YHQ3NBB3.js} +58 -48
  383. package/dist/node-runtime/chunk-YPHAIRZT.js +10 -0
  384. package/dist/node-runtime/cli-runtime.js +2 -2
  385. package/dist/node-runtime/command-PDVTBRWA.js +45 -0
  386. package/dist/node-runtime/{commands-MV5KOZ64.js → commands-KAMO2T6P.js} +10 -10
  387. package/dist/node-runtime/commands-XDCFGIGW.js +36 -0
  388. package/dist/node-runtime/config-RORQUQFG.js +20 -0
  389. package/dist/node-runtime/{customize-LF5VST47.js → customize-VFJV3U6A.js} +2 -1
  390. package/dist/node-runtime/{direct-tool-executor-BK7CLEBX.js → direct-tool-executor-YAVOLDFY.js} +8 -8
  391. package/dist/node-runtime/{export-html-TEXVV5RP.js → export-html-KSOIMVPI.js} +4 -3
  392. package/dist/node-runtime/{extension-input-O4HSAEM4.js → extension-input-E7BJZ35H.js} +4 -4
  393. package/dist/node-runtime/{extension-selector-X5FCX43D.js → extension-selector-WU42XDTU.js} +4 -4
  394. package/dist/node-runtime/{extract-XLQSJN4V.js → extract-EQJE7FZZ.js} +2 -2
  395. package/dist/node-runtime/{gateway-BM6NLOH4.js → gateway-AYWM5DIO.js} +275 -994
  396. package/dist/node-runtime/{gemini-search-U7SUMPQA.js → gemini-search-35OCRFLN.js} +1 -1
  397. package/dist/node-runtime/{gemini-url-context-PXWCBQHK.js → gemini-url-context-UQY3PHHW.js} +2 -2
  398. package/dist/node-runtime/index.js +104 -65
  399. package/dist/node-runtime/{init-3C7Z24OI.js → init-X2LIUB2H.js} +3 -3
  400. package/dist/node-runtime/{install-cli-EXMJQESX.js → install-cli-3BO4MMQ6.js} +21 -82
  401. package/dist/node-runtime/interactive-mode-4MFRKAHG.js +72 -0
  402. package/dist/node-runtime/{interactive-view-ILS6TAN5.js → interactive-view-TUZHCQM3.js} +8 -8
  403. package/dist/node-runtime/lunr-browser-RYFIIVJN.js +174 -0
  404. package/dist/node-runtime/{lunr-cron-4VHP4E4B.js → lunr-cron-YWXEPZMQ.js} +19 -5
  405. package/dist/node-runtime/main.js +44 -31
  406. package/dist/node-runtime/{memory-cap-3S4XRLNP.js → memory-cap-EL3KL4JJ.js} +2 -1
  407. package/dist/node-runtime/mobile-commands-LVHDAUDP.js +32 -0
  408. package/dist/node-runtime/model-runtime-PLQKAG4B.js +16 -0
  409. package/dist/node-runtime/{model-tiers-NJLHUEGK.js → model-tiers-OU3U5CZG.js} +2 -1
  410. package/dist/node-runtime/{package-manager-cli-N37546V5.js → package-manager-cli-6GT3Z3CK.js} +22 -15
  411. package/dist/node-runtime/{pi-intercom-NV2LW6X7.js → pi-intercom-HBPK5HSJ.js} +4 -1
  412. package/dist/node-runtime/{pi-mcp-adapter-GUFZWW54.js → pi-mcp-adapter-E6WULBKF.js} +4 -4
  413. package/dist/node-runtime/{pi-subagents-6MUIFNDM.js → pi-subagents-CDWK2AKY.js} +236 -79
  414. package/dist/node-runtime/{pi-web-access-2CQGFLT2.js → pi-web-access-WTGMNZ32.js} +15 -15
  415. package/dist/node-runtime/process-registry-4FAKZQOL.js +31 -0
  416. package/dist/node-runtime/{proxy-modes-V6YVFH3C.js → proxy-modes-OSQCWRBI.js} +9 -9
  417. package/dist/node-runtime/rpc-entry.js +44 -31
  418. package/dist/node-runtime/{rpc-mode-W5ZTLTMZ.js → rpc-mode-O3XV4SID.js} +2 -2
  419. package/dist/node-runtime/runtime-4VA6JKNF.js +373 -0
  420. package/dist/node-runtime/{runtime-bridges-2VQTLPOZ.js → runtime-bridges-3XUQ46XW.js} +6 -5
  421. package/dist/node-runtime/service-ISSP6VHE.js +36 -0
  422. package/dist/node-runtime/session-handoff-HC7R5E24.js +33 -0
  423. package/dist/node-runtime/{session-manager-E7AREDTV.js → session-manager-CFN4UFM2.js} +2 -1
  424. package/dist/node-runtime/{session-selector-FJ7M2AIX.js → session-selector-GIM4DHQ2.js} +5 -4
  425. package/dist/node-runtime/{settings-manager-CUGAZWAB.js → settings-manager-6VEMXRDL.js} +7 -2
  426. package/dist/node-runtime/setup-5SR2J35A.js +155 -0
  427. package/dist/node-runtime/setup-JBKOLJ6F.js +18 -0
  428. package/dist/node-runtime/{subagent-executor-MH4A7CCF.js → subagent-executor-M5VWB3D3.js} +26 -18
  429. package/dist/node-runtime/theme-UOO5RVFN.js +68 -0
  430. package/dist/node-runtime/trust-manager-MMISNCYO.js +17 -0
  431. package/dist/node-runtime/{update-cli-XTZMZGOL.js → update-cli-UVKJWRRJ.js} +7 -2
  432. package/dist/node-runtime/usage-service-LFB2SWYV.js +37 -0
  433. package/dist/node-runtime/{video-extract-OYT5WKS5.js → video-extract-OOZHR7SU.js} +2 -2
  434. package/dist/node-runtime/{youtube-extract-TQYKGFGS.js → youtube-extract-FNPAUNHU.js} +6 -6
  435. package/docs/browser.md +86 -0
  436. package/docs/extensions.md +2 -0
  437. package/docs/features.md +96 -16
  438. package/docs/interactive-startup.md +2 -1
  439. package/docs/sessions.md +10 -0
  440. package/docs/settings.md +2 -0
  441. package/docs/tui.md +2 -0
  442. package/docs/usage.md +16 -0
  443. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  444. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  445. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  446. package/examples/extensions/gondolin/package-lock.json +2 -2
  447. package/examples/extensions/gondolin/package.json +1 -1
  448. package/examples/extensions/sandbox/package-lock.json +2 -2
  449. package/examples/extensions/sandbox/package.json +1 -1
  450. package/examples/extensions/with-deps/package-lock.json +2 -2
  451. package/examples/extensions/with-deps/package.json +1 -1
  452. package/package.json +7 -4
  453. package/scripts/install-browser.mjs +20 -0
  454. package/dist/node-runtime/agent-session-services-C743TYQE.js +0 -37
  455. package/dist/node-runtime/chunk-VGYZ3APQ.js +0 -59
  456. package/dist/node-runtime/chunk-XI77KVKA.js +0 -6009
  457. package/dist/node-runtime/command-P4NYCJ2M.js +0 -13
  458. package/dist/node-runtime/interactive-mode-EVVBA64D.js +0 -59
  459. package/dist/node-runtime/{chunk-UWQYRSYQ.js → chunk-YJMMG7C6.js} +25 -25
  460. package/dist/node-runtime/{mcp-panel-IIGMXQGP.js → mcp-panel-KTTFXJZ4.js} +3 -3
  461. package/dist/node-runtime/{src-BPQFJR76.js → src-IVPNZ2BX.js} +9 -9
@@ -0,0 +1,156 @@
1
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@ashx-j/lunr-tui";
2
+ import { theme } from "../theme/theme.js";
3
+ function sourceLines(source) {
4
+ const lines = [];
5
+ const pattern = /([^\r\n]*)(\r\n|\r|\n|$)/g;
6
+ for (const match of source.matchAll(pattern)) {
7
+ const start = match.index;
8
+ const text = match[1];
9
+ const newline = match[2];
10
+ lines.push({ start, end: start + text.length + newline.length, text });
11
+ if (!newline)
12
+ break;
13
+ }
14
+ return lines;
15
+ }
16
+ function parseFenceOpening(line) {
17
+ const match = /^( {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
18
+ if (!match)
19
+ return undefined;
20
+ const fence = match[2];
21
+ const info = match[3];
22
+ if (fence[0] === "`" && info.includes("`"))
23
+ return undefined;
24
+ return {
25
+ marker: fence[0],
26
+ length: fence.length,
27
+ copyable: info.trim() === "lunr-copy",
28
+ };
29
+ }
30
+ function isFenceClosing(line, opening) {
31
+ const match = /^( {0,3})(`+|~+)[ \t]*$/.exec(line);
32
+ if (!match)
33
+ return false;
34
+ const fence = match[2];
35
+ return fence[0] === opening.marker && fence.length >= opening.length;
36
+ }
37
+ function payloadEndBeforeClosing(source, closingStart) {
38
+ if (closingStart >= 2 && source.slice(closingStart - 2, closingStart) === "\r\n")
39
+ return closingStart - 2;
40
+ if (closingStart >= 1 && (source[closingStart - 1] === "\n" || source[closingStart - 1] === "\r")) {
41
+ return closingStart - 1;
42
+ }
43
+ return closingStart;
44
+ }
45
+ export function parseCopyableTextSegments(source) {
46
+ const lines = sourceLines(source);
47
+ const segments = [];
48
+ let emittedThrough = 0;
49
+ let lineIndex = 0;
50
+ while (lineIndex < lines.length) {
51
+ const openingLine = lines[lineIndex];
52
+ const opening = parseFenceOpening(openingLine.text);
53
+ if (!opening) {
54
+ lineIndex++;
55
+ continue;
56
+ }
57
+ let closingIndex = -1;
58
+ for (let i = lineIndex + 1; i < lines.length; i++) {
59
+ if (isFenceClosing(lines[i].text, opening)) {
60
+ closingIndex = i;
61
+ break;
62
+ }
63
+ }
64
+ if (!opening.copyable) {
65
+ if (closingIndex < 0)
66
+ break;
67
+ lineIndex = closingIndex + 1;
68
+ continue;
69
+ }
70
+ if (openingLine.start > emittedThrough) {
71
+ segments.push({ type: "markdown", text: source.slice(emittedThrough, openingLine.start) });
72
+ }
73
+ const payloadStart = openingLine.end;
74
+ if (closingIndex < 0) {
75
+ segments.push({
76
+ type: "copyable",
77
+ payload: source.slice(payloadStart),
78
+ complete: false,
79
+ start: openingLine.start,
80
+ });
81
+ return segments;
82
+ }
83
+ const closingLine = lines[closingIndex];
84
+ segments.push({
85
+ type: "copyable",
86
+ payload: source.slice(payloadStart, payloadEndBeforeClosing(source, closingLine.start)),
87
+ complete: true,
88
+ start: openingLine.start,
89
+ });
90
+ emittedThrough = closingLine.end;
91
+ lineIndex = closingIndex + 1;
92
+ }
93
+ if (emittedThrough < source.length) {
94
+ segments.push({ type: "markdown", text: source.slice(emittedThrough) });
95
+ }
96
+ return segments;
97
+ }
98
+ export class CopyableTextBlockComponent {
99
+ renderedRows = 0;
100
+ payload;
101
+ complete;
102
+ outputPad;
103
+ status;
104
+ onCopy;
105
+ constructor(payload, complete, outputPad, status, onCopy) {
106
+ this.payload = payload;
107
+ this.complete = complete;
108
+ this.outputPad = outputPad;
109
+ this.status = status;
110
+ this.onCopy = onCopy;
111
+ }
112
+ invalidate() {
113
+ this.renderedRows = 0;
114
+ }
115
+ render(width) {
116
+ if (width <= 0) {
117
+ this.renderedRows = 0;
118
+ return [];
119
+ }
120
+ const horizontalPad = Math.min(this.outputPad, Math.max(0, width - 1));
121
+ const pad = " ".repeat(horizontalPad);
122
+ const contentWidth = Math.max(1, width - horizontalPad * 2);
123
+ const paint = (content) => {
124
+ const line = truncateToWidth(`${pad}${content}`, width, "");
125
+ return theme.bg("userMessageBg", `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`);
126
+ };
127
+ const lines = [paint("")];
128
+ const payloadLines = this.payload.replace(/\t/g, " ").split("\n");
129
+ for (const payloadLine of payloadLines) {
130
+ for (const wrappedLine of wrapTextWithAnsi(payloadLine, contentWidth)) {
131
+ lines.push(paint(theme.fg("userMessageText", wrappedLine)));
132
+ }
133
+ }
134
+ if (typeof this.status === "object") {
135
+ for (const wrappedLine of wrapTextWithAnsi(`Copy failed: ${this.status.error}`, contentWidth)) {
136
+ lines.push(paint(theme.fg("error", wrappedLine)));
137
+ }
138
+ }
139
+ lines.push(paint(""));
140
+ this.renderedRows = lines.length;
141
+ return lines;
142
+ }
143
+ handleClick(localY, width, localX) {
144
+ this.render(width);
145
+ if (!this.complete ||
146
+ this.status === "copying" ||
147
+ localY < 0 ||
148
+ localY >= this.renderedRows ||
149
+ (localX !== undefined && (localX < 0 || localX >= width))) {
150
+ return false;
151
+ }
152
+ this.onCopy();
153
+ return true;
154
+ }
155
+ }
156
+ //# sourceMappingURL=copyable-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copyable-text.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/copyable-text.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@ashx-j/lunr-tui\";\nimport { theme } from \"../theme/theme.ts\";\n\nexport type CopyableTextSegment =\n\t| { type: \"markdown\"; text: string }\n\t| { type: \"copyable\"; payload: string; complete: boolean; start: number };\n\ninterface SourceLine {\n\tstart: number;\n\tend: number;\n\ttext: string;\n}\n\ninterface FenceOpening {\n\tmarker: \"`\" | \"~\";\n\tlength: number;\n\tcopyable: boolean;\n}\n\nfunction sourceLines(source: string): SourceLine[] {\n\tconst lines: SourceLine[] = [];\n\tconst pattern = /([^\\r\\n]*)(\\r\\n|\\r|\\n|$)/g;\n\tfor (const match of source.matchAll(pattern)) {\n\t\tconst start = match.index;\n\t\tconst text = match[1];\n\t\tconst newline = match[2];\n\t\tlines.push({ start, end: start + text.length + newline.length, text });\n\t\tif (!newline) break;\n\t}\n\treturn lines;\n}\n\nfunction parseFenceOpening(line: string): FenceOpening | undefined {\n\tconst match = /^( {0,3})(`{3,}|~{3,})(.*)$/.exec(line);\n\tif (!match) return undefined;\n\tconst fence = match[2];\n\tconst info = match[3];\n\tif (fence[0] === \"`\" && info.includes(\"`\")) return undefined;\n\treturn {\n\t\tmarker: fence[0] as \"`\" | \"~\",\n\t\tlength: fence.length,\n\t\tcopyable: info.trim() === \"lunr-copy\",\n\t};\n}\n\nfunction isFenceClosing(line: string, opening: FenceOpening): boolean {\n\tconst match = /^( {0,3})(`+|~+)[ \\t]*$/.exec(line);\n\tif (!match) return false;\n\tconst fence = match[2];\n\treturn fence[0] === opening.marker && fence.length >= opening.length;\n}\n\nfunction payloadEndBeforeClosing(source: string, closingStart: number): number {\n\tif (closingStart >= 2 && source.slice(closingStart - 2, closingStart) === \"\\r\\n\") return closingStart - 2;\n\tif (closingStart >= 1 && (source[closingStart - 1] === \"\\n\" || source[closingStart - 1] === \"\\r\")) {\n\t\treturn closingStart - 1;\n\t}\n\treturn closingStart;\n}\n\nexport function parseCopyableTextSegments(source: string): CopyableTextSegment[] {\n\tconst lines = sourceLines(source);\n\tconst segments: CopyableTextSegment[] = [];\n\tlet emittedThrough = 0;\n\tlet lineIndex = 0;\n\n\twhile (lineIndex < lines.length) {\n\t\tconst openingLine = lines[lineIndex];\n\t\tconst opening = parseFenceOpening(openingLine.text);\n\t\tif (!opening) {\n\t\t\tlineIndex++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet closingIndex = -1;\n\t\tfor (let i = lineIndex + 1; i < lines.length; i++) {\n\t\t\tif (isFenceClosing(lines[i].text, opening)) {\n\t\t\t\tclosingIndex = i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tif (!opening.copyable) {\n\t\t\tif (closingIndex < 0) break;\n\t\t\tlineIndex = closingIndex + 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (openingLine.start > emittedThrough) {\n\t\t\tsegments.push({ type: \"markdown\", text: source.slice(emittedThrough, openingLine.start) });\n\t\t}\n\n\t\tconst payloadStart = openingLine.end;\n\t\tif (closingIndex < 0) {\n\t\t\tsegments.push({\n\t\t\t\ttype: \"copyable\",\n\t\t\t\tpayload: source.slice(payloadStart),\n\t\t\t\tcomplete: false,\n\t\t\t\tstart: openingLine.start,\n\t\t\t});\n\t\t\treturn segments;\n\t\t}\n\n\t\tconst closingLine = lines[closingIndex];\n\t\tsegments.push({\n\t\t\ttype: \"copyable\",\n\t\t\tpayload: source.slice(payloadStart, payloadEndBeforeClosing(source, closingLine.start)),\n\t\t\tcomplete: true,\n\t\t\tstart: openingLine.start,\n\t\t});\n\t\temittedThrough = closingLine.end;\n\t\tlineIndex = closingIndex + 1;\n\t}\n\n\tif (emittedThrough < source.length) {\n\t\tsegments.push({ type: \"markdown\", text: source.slice(emittedThrough) });\n\t}\n\treturn segments;\n}\n\nexport type CopyableTextStatus = \"idle\" | \"copying\" | \"copied\" | { error: string };\n\nexport class CopyableTextBlockComponent implements Component {\n\tprivate renderedRows = 0;\n\tprivate readonly payload: string;\n\tprivate readonly complete: boolean;\n\tprivate readonly outputPad: number;\n\tprivate readonly status: CopyableTextStatus;\n\tprivate readonly onCopy: () => void;\n\n\tconstructor(payload: string, complete: boolean, outputPad: number, status: CopyableTextStatus, onCopy: () => void) {\n\t\tthis.payload = payload;\n\t\tthis.complete = complete;\n\t\tthis.outputPad = outputPad;\n\t\tthis.status = status;\n\t\tthis.onCopy = onCopy;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.renderedRows = 0;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (width <= 0) {\n\t\t\tthis.renderedRows = 0;\n\t\t\treturn [];\n\t\t}\n\n\t\tconst horizontalPad = Math.min(this.outputPad, Math.max(0, width - 1));\n\t\tconst pad = \" \".repeat(horizontalPad);\n\t\tconst contentWidth = Math.max(1, width - horizontalPad * 2);\n\t\tconst paint = (content: string): string => {\n\t\t\tconst line = truncateToWidth(`${pad}${content}`, width, \"\");\n\t\t\treturn theme.bg(\"userMessageBg\", `${line}${\" \".repeat(Math.max(0, width - visibleWidth(line)))}`);\n\t\t};\n\t\tconst lines = [paint(\"\")];\n\t\tconst payloadLines = this.payload.replace(/\\t/g, \" \").split(\"\\n\");\n\n\t\tfor (const payloadLine of payloadLines) {\n\t\t\tfor (const wrappedLine of wrapTextWithAnsi(payloadLine, contentWidth)) {\n\t\t\t\tlines.push(paint(theme.fg(\"userMessageText\", wrappedLine)));\n\t\t\t}\n\t\t}\n\n\t\tif (typeof this.status === \"object\") {\n\t\t\tfor (const wrappedLine of wrapTextWithAnsi(`Copy failed: ${this.status.error}`, contentWidth)) {\n\t\t\t\tlines.push(paint(theme.fg(\"error\", wrappedLine)));\n\t\t\t}\n\t\t}\n\t\tlines.push(paint(\"\"));\n\t\tthis.renderedRows = lines.length;\n\t\treturn lines;\n\t}\n\n\thandleClick(localY: number, width: number, localX?: number): boolean {\n\t\tthis.render(width);\n\t\tif (\n\t\t\t!this.complete ||\n\t\t\tthis.status === \"copying\" ||\n\t\t\tlocalY < 0 ||\n\t\t\tlocalY >= this.renderedRows ||\n\t\t\t(localX !== undefined && (localX < 0 || localX >= width))\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\t\tthis.onCopy();\n\t\treturn true;\n\t}\n}\n"]}
@@ -13,6 +13,7 @@ export declare class CustomEntryComponent extends Container {
13
13
  constructor(entry: CustomEntry<unknown>, renderer: EntryRenderer);
14
14
  hasContent(): boolean;
15
15
  setExpanded(expanded: boolean): void;
16
+ handleClick(localY: number, width: number): boolean;
16
17
  invalidate(): void;
17
18
  private rebuild;
18
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"custom-entry.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-entry.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,SAAS,EAAgB,MAAM,wBAAwB,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAGpE;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;IAClD,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,eAAe,CAAC,CAAY;IACpC,OAAO,CAAC,SAAS,CAAS;IAE1B,YAAY,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,aAAa,EAK/D;IAED,UAAU,IAAI,OAAO,CAEpB;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;CAsBf","sourcesContent":["import type { Component } from \"@ashx-j/lunr-tui\";\nimport { Box, Container, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport type { EntryRenderer } from \"../../../core/extensions/types.ts\";\nimport type { CustomEntry } from \"../../../core/session-manager.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Component that renders a custom session entry from extensions.\n * The host owns transcript spacing; renderer output should provide only its content.\n */\nexport class CustomEntryComponent extends Container {\n\tprivate entry: CustomEntry<unknown>;\n\tprivate renderer: EntryRenderer;\n\tprivate customComponent?: Component;\n\tprivate _expanded = false;\n\n\tconstructor(entry: CustomEntry<unknown>, renderer: EntryRenderer) {\n\t\tsuper();\n\t\tthis.entry = entry;\n\t\tthis.renderer = renderer;\n\t\tthis.rebuild();\n\t}\n\n\thasContent(): boolean {\n\t\treturn this.customComponent !== undefined;\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tthis.clear();\n\t\tthis.customComponent = undefined;\n\n\t\tlet component: Component | undefined;\n\t\ttry {\n\t\t\tcomponent = this.renderer(this.entry, { expanded: this._expanded }, theme);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n\t\t\tbox.addChild(new Text(theme.fg(\"error\", `[${this.entry.customType}] renderer failed: ${message}`), 0, 0));\n\t\t\tcomponent = box;\n\t\t}\n\n\t\tif (!component) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.customComponent = component;\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(component);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-entry.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-entry.ts"],"names":[],"mappings":"AACA,OAAO,EAAO,SAAS,EAAgB,MAAM,wBAAwB,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAGpE;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;IAClD,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,eAAe,CAAC,CAAY;IACpC,OAAO,CAAC,SAAS,CAAS;IAE1B,YAAY,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,aAAa,EAK/D;IAED,UAAU,IAAI,OAAO,CAEpB;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAEQ,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAK3D;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;CAsBf","sourcesContent":["import type { Component } from \"@ashx-j/lunr-tui\";\nimport { Box, Container, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport type { EntryRenderer } from \"../../../core/extensions/types.ts\";\nimport type { CustomEntry } from \"../../../core/session-manager.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Component that renders a custom session entry from extensions.\n * The host owns transcript spacing; renderer output should provide only its content.\n */\nexport class CustomEntryComponent extends Container {\n\tprivate entry: CustomEntry<unknown>;\n\tprivate renderer: EntryRenderer;\n\tprivate customComponent?: Component;\n\tprivate _expanded = false;\n\n\tconstructor(entry: CustomEntry<unknown>, renderer: EntryRenderer) {\n\t\tsuper();\n\t\tthis.entry = entry;\n\t\tthis.renderer = renderer;\n\t\tthis.rebuild();\n\t}\n\n\thasContent(): boolean {\n\t\treturn this.customComponent !== undefined;\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride handleClick(localY: number, width: number): boolean {\n\t\tif (super.handleClick(localY, width)) return true;\n\t\tif (!this.customComponent || localY < 1) return false;\n\t\tthis.setExpanded(!this._expanded);\n\t\treturn true;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tthis.clear();\n\t\tthis.customComponent = undefined;\n\n\t\tlet component: Component | undefined;\n\t\ttry {\n\t\t\tcomponent = this.renderer(this.entry, { expanded: this._expanded }, theme);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n\t\t\tbox.addChild(new Text(theme.fg(\"error\", `[${this.entry.customType}] renderer failed: ${message}`), 0, 0));\n\t\t\tcomponent = box;\n\t\t}\n\n\t\tif (!component) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.customComponent = component;\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(component);\n\t}\n}\n"]}
@@ -24,6 +24,14 @@ export class CustomEntryComponent extends Container {
24
24
  this.rebuild();
25
25
  }
26
26
  }
27
+ handleClick(localY, width) {
28
+ if (super.handleClick(localY, width))
29
+ return true;
30
+ if (!this.customComponent || localY < 1)
31
+ return false;
32
+ this.setExpanded(!this._expanded);
33
+ return true;
34
+ }
27
35
  invalidate() {
28
36
  super.invalidate();
29
37
  this.rebuild();
@@ -1 +1 @@
1
- {"version":3,"file":"custom-entry.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-entry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGtE,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;GAGG;AACH,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IAC1C,KAAK,CAAuB;IAC5B,QAAQ,CAAgB;IACxB,eAAe,CAAa;IAC5B,SAAS,GAAG,KAAK,CAAC;IAE1B,YAAY,KAA2B,EAAE,QAAuB,EAAE;QACjE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAED,UAAU,GAAY;QACrB,OAAO,IAAI,CAAC,eAAe,KAAK,SAAS,CAAC;IAAA,CAC1C;IAED,WAAW,CAAC,QAAiB,EAAQ;QACpC,IAAI,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;YAC1B,IAAI,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC;IAAA,CACD;IAEQ,UAAU,GAAS;QAC3B,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAEO,OAAO,GAAS;QACvB,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QAEjC,IAAI,SAAgC,CAAC;QACrC,IAAI,CAAC;YACJ,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,KAAK,CAAC,CAAC;QAC5E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC,CAAC;YACvE,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,sBAAsB,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1G,SAAS,GAAG,GAAG,CAAC;QACjB,CAAC;QAED,IAAI,CAAC,SAAS,EAAE,CAAC;YAChB,OAAO;QACR,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QACjC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAAA,CACzB;CACD","sourcesContent":["import type { Component } from \"@ashx-j/lunr-tui\";\nimport { Box, Container, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport type { EntryRenderer } from \"../../../core/extensions/types.ts\";\nimport type { CustomEntry } from \"../../../core/session-manager.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Component that renders a custom session entry from extensions.\n * The host owns transcript spacing; renderer output should provide only its content.\n */\nexport class CustomEntryComponent extends Container {\n\tprivate entry: CustomEntry<unknown>;\n\tprivate renderer: EntryRenderer;\n\tprivate customComponent?: Component;\n\tprivate _expanded = false;\n\n\tconstructor(entry: CustomEntry<unknown>, renderer: EntryRenderer) {\n\t\tsuper();\n\t\tthis.entry = entry;\n\t\tthis.renderer = renderer;\n\t\tthis.rebuild();\n\t}\n\n\thasContent(): boolean {\n\t\treturn this.customComponent !== undefined;\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tthis.clear();\n\t\tthis.customComponent = undefined;\n\n\t\tlet component: Component | undefined;\n\t\ttry {\n\t\t\tcomponent = this.renderer(this.entry, { expanded: this._expanded }, theme);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n\t\t\tbox.addChild(new Text(theme.fg(\"error\", `[${this.entry.customType}] renderer failed: ${message}`), 0, 0));\n\t\t\tcomponent = box;\n\t\t}\n\n\t\tif (!component) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.customComponent = component;\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(component);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-entry.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-entry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGtE,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;GAGG;AACH,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IAC1C,KAAK,CAAuB;IAC5B,QAAQ,CAAgB;IACxB,eAAe,CAAa;IAC5B,SAAS,GAAG,KAAK,CAAC;IAE1B,YAAY,KAA2B,EAAE,QAAuB,EAAE;QACjE,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAED,UAAU,GAAY;QACrB,OAAO,IAAI,CAAC,eAAe,KAAK,SAAS,CAAC;IAAA,CAC1C;IAED,WAAW,CAAC,QAAiB,EAAQ;QACpC,IAAI,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YACjC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;YAC1B,IAAI,CAAC,OAAO,EAAE,CAAC;QAChB,CAAC;IAAA,CACD;IAEQ,WAAW,CAAC,MAAc,EAAE,KAAa,EAAW;QAC5D,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAClD,IAAI,CAAC,IAAI,CAAC,eAAe,IAAI,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QACtD,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClC,OAAO,IAAI,CAAC;IAAA,CACZ;IAEQ,UAAU,GAAS;QAC3B,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAEO,OAAO,GAAS;QACvB,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QAEjC,IAAI,SAAgC,CAAC;QACrC,IAAI,CAAC;YACJ,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,KAAK,CAAC,CAAC;QAC5E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC,CAAC;YACvE,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,sBAAsB,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC1G,SAAS,GAAG,GAAG,CAAC;QACjB,CAAC;QAED,IAAI,CAAC,SAAS,EAAE,CAAC;YAChB,OAAO;QACR,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,SAAS,CAAC;QACjC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAAA,CACzB;CACD","sourcesContent":["import type { Component } from \"@ashx-j/lunr-tui\";\nimport { Box, Container, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport type { EntryRenderer } from \"../../../core/extensions/types.ts\";\nimport type { CustomEntry } from \"../../../core/session-manager.ts\";\nimport { theme } from \"../theme/theme.ts\";\n\n/**\n * Component that renders a custom session entry from extensions.\n * The host owns transcript spacing; renderer output should provide only its content.\n */\nexport class CustomEntryComponent extends Container {\n\tprivate entry: CustomEntry<unknown>;\n\tprivate renderer: EntryRenderer;\n\tprivate customComponent?: Component;\n\tprivate _expanded = false;\n\n\tconstructor(entry: CustomEntry<unknown>, renderer: EntryRenderer) {\n\t\tsuper();\n\t\tthis.entry = entry;\n\t\tthis.renderer = renderer;\n\t\tthis.rebuild();\n\t}\n\n\thasContent(): boolean {\n\t\treturn this.customComponent !== undefined;\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride handleClick(localY: number, width: number): boolean {\n\t\tif (super.handleClick(localY, width)) return true;\n\t\tif (!this.customComponent || localY < 1) return false;\n\t\tthis.setExpanded(!this._expanded);\n\t\treturn true;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tthis.clear();\n\t\tthis.customComponent = undefined;\n\n\t\tlet component: Component | undefined;\n\t\ttry {\n\t\t\tcomponent = this.renderer(this.entry, { expanded: this._expanded }, theme);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst box = new Box(1, 1, (text) => theme.bg(\"customMessageBg\", text));\n\t\t\tbox.addChild(new Text(theme.fg(\"error\", `[${this.entry.customType}] renderer failed: ${message}`), 0, 0));\n\t\t\tcomponent = box;\n\t\t}\n\n\t\tif (!component) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.customComponent = component;\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(component);\n\t}\n}\n"]}
@@ -14,6 +14,7 @@ export declare class CustomMessageComponent extends Container {
14
14
  private _expanded;
15
15
  constructor(message: CustomMessage<unknown>, customRenderer?: MessageRenderer, markdownTheme?: MarkdownTheme);
16
16
  setExpanded(expanded: boolean): void;
17
+ handleClick(localY: number, width: number): boolean;
17
18
  invalidate(): void;
18
19
  private rebuild;
19
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"custom-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAO,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,wBAAwB,CAAC;AACpG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAC,CAAY;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAAS;IAE1B,YACC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAC/B,cAAc,CAAC,EAAE,eAAe,EAChC,aAAa,GAAE,aAAkC,EAajD;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;CAiDf","sourcesContent":["import type { TextContent } from \"@ashx-j/lunr-ai\";\nimport type { Component } from \"@ashx-j/lunr-tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.ts\";\nimport type { CustomMessage } from \"../../../core/messages.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create box with purple background (used for default rendering)\n\t\tthis.box = new Box(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\t// Remove previous content component\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\t// Try custom renderer first - it handles its own styling\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Custom renderer provides its own styled component\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering\n\t\t\t}\n\t\t}\n\n\t\t// Default rendering uses our box\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\t// Default rendering: label + content\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[${this.message.customType}]\\x1b[22m`);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\t// Extract text content\n\t\tlet text: string;\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\ttext = this.message.content;\n\t\t} else {\n\t\t\ttext = this.message.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(text, 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-message.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAO,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,wBAAwB,CAAC;AACpG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,cAAc,CAAC,CAAkB;IACzC,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,eAAe,CAAC,CAAY;IACpC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,SAAS,CAAS;IAE1B,YACC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAC/B,cAAc,CAAC,EAAE,eAAe,EAChC,aAAa,GAAE,aAAkC,EAajD;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAKnC;IAEQ,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAK3D;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;CAiDf","sourcesContent":["import type { TextContent } from \"@ashx-j/lunr-ai\";\nimport type { Component } from \"@ashx-j/lunr-tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.ts\";\nimport type { CustomMessage } from \"../../../core/messages.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create box with purple background (used for default rendering)\n\t\tthis.box = new Box(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride handleClick(localY: number, width: number): boolean {\n\t\tif (super.handleClick(localY, width)) return true;\n\t\tif (!this.customRenderer || !this.customComponent || localY < 1) return false;\n\t\tthis.setExpanded(!this._expanded);\n\t\treturn true;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\t// Remove previous content component\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\t// Try custom renderer first - it handles its own styling\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Custom renderer provides its own styled component\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering\n\t\t\t}\n\t\t}\n\n\t\t// Default rendering uses our box\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\t// Default rendering: label + content\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[${this.message.customType}]\\x1b[22m`);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\t// Extract text content\n\t\tlet text: string;\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\ttext = this.message.content;\n\t\t} else {\n\t\t\ttext = this.message.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(text, 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
@@ -27,6 +27,14 @@ export class CustomMessageComponent extends Container {
27
27
  this.rebuild();
28
28
  }
29
29
  }
30
+ handleClick(localY, width) {
31
+ if (super.handleClick(localY, width))
32
+ return true;
33
+ if (!this.customRenderer || !this.customComponent || localY < 1)
34
+ return false;
35
+ this.setExpanded(!this._expanded);
36
+ return true;
37
+ }
30
38
  invalidate() {
31
39
  super.invalidate();
32
40
  this.rebuild();
@@ -1 +1 @@
1
- {"version":3,"file":"custom-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TextContent } from \"@ashx-j/lunr-ai\";\nimport type { Component } from \"@ashx-j/lunr-tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.ts\";\nimport type { CustomMessage } from \"../../../core/messages.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create box with purple background (used for default rendering)\n\t\tthis.box = new Box(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\t// Remove previous content component\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\t// Try custom renderer first - it handles its own styling\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Custom renderer provides its own styled component\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering\n\t\t\t}\n\t\t}\n\n\t\t// Default rendering uses our box\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\t// Default rendering: label + content\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[${this.message.customType}]\\x1b[22m`);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\t// Extract text content\n\t\tlet text: string;\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\ttext = this.message.content;\n\t\t} else {\n\t\t\ttext = this.message.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(text, 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
1
+ {"version":3,"file":"custom-message.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/custom-message.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TextContent } from \"@ashx-j/lunr-ai\";\nimport type { Component } from \"@ashx-j/lunr-tui\";\nimport { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@ashx-j/lunr-tui\";\nimport type { MessageRenderer } from \"../../../core/extensions/types.ts\";\nimport type { CustomMessage } from \"../../../core/messages.ts\";\nimport { getMarkdownTheme, theme } from \"../theme/theme.ts\";\n\n/**\n * Component that renders a custom message entry from extensions.\n * Uses distinct styling to differentiate from user messages.\n */\nexport class CustomMessageComponent extends Container {\n\tprivate message: CustomMessage<unknown>;\n\tprivate customRenderer?: MessageRenderer;\n\tprivate box: Box;\n\tprivate customComponent?: Component;\n\tprivate markdownTheme: MarkdownTheme;\n\tprivate _expanded = false;\n\n\tconstructor(\n\t\tmessage: CustomMessage<unknown>,\n\t\tcustomRenderer?: MessageRenderer,\n\t\tmarkdownTheme: MarkdownTheme = getMarkdownTheme(),\n\t) {\n\t\tsuper();\n\t\tthis.message = message;\n\t\tthis.customRenderer = customRenderer;\n\t\tthis.markdownTheme = markdownTheme;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create box with purple background (used for default rendering)\n\t\tthis.box = new Box(1, 1, (t) => theme.bg(\"customMessageBg\", t));\n\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this._expanded !== expanded) {\n\t\t\tthis._expanded = expanded;\n\t\t\tthis.rebuild();\n\t\t}\n\t}\n\n\toverride handleClick(localY: number, width: number): boolean {\n\t\tif (super.handleClick(localY, width)) return true;\n\t\tif (!this.customRenderer || !this.customComponent || localY < 1) return false;\n\t\tthis.setExpanded(!this._expanded);\n\t\treturn true;\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\t// Remove previous content component\n\t\tif (this.customComponent) {\n\t\t\tthis.removeChild(this.customComponent);\n\t\t\tthis.customComponent = undefined;\n\t\t}\n\t\tthis.removeChild(this.box);\n\n\t\t// Try custom renderer first - it handles its own styling\n\t\tif (this.customRenderer) {\n\t\t\ttry {\n\t\t\t\tconst component = this.customRenderer(this.message, { expanded: this._expanded }, theme);\n\t\t\t\tif (component) {\n\t\t\t\t\t// Custom renderer provides its own styled component\n\t\t\t\t\tthis.customComponent = component;\n\t\t\t\t\tthis.addChild(component);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Fall through to default rendering\n\t\t\t}\n\t\t}\n\n\t\t// Default rendering uses our box\n\t\tthis.addChild(this.box);\n\t\tthis.box.clear();\n\n\t\t// Default rendering: label + content\n\t\tconst label = theme.fg(\"customMessageLabel\", `\\x1b[1m[${this.message.customType}]\\x1b[22m`);\n\t\tthis.box.addChild(new Text(label, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\t// Extract text content\n\t\tlet text: string;\n\t\tif (typeof this.message.content === \"string\") {\n\t\t\ttext = this.message.content;\n\t\t} else {\n\t\t\ttext = this.message.content\n\t\t\t\t.filter((c): c is TextContent => c.type === \"text\")\n\t\t\t\t.map((c) => c.text)\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tthis.box.addChild(\n\t\t\tnew Markdown(text, 0, 0, this.markdownTheme, {\n\t\t\t\tcolor: (text: string) => theme.fg(\"customMessageText\", text),\n\t\t\t}),\n\t\t);\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"session-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype Focusable,\n\tgetKeybindings,\n\tInput,\n\tSpacer,\n\tText,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@ashx-j/lunr-tui\";\nimport { KeybindingsManager } from \"../../../core/keybindings.ts\";\nimport type { SessionInfo, SessionListProgress } from \"../../../core/session-manager.ts\";\nimport { canonicalizePath as _canonicalizePath } from \"../../../utils/paths.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint, keyText } from \"./keybinding-hints.ts\";\nimport { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from \"./session-selector-search.ts\";\n\ntype SessionScope = \"current\" | \"all\";\n\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (!path) return path;\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\nfunction formatSessionDate(date: Date): string {\n\tconst now = new Date();\n\tconst diffMs = now.getTime() - date.getTime();\n\tconst diffMins = Math.floor(diffMs / 60000);\n\tconst diffHours = Math.floor(diffMs / 3600000);\n\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\tif (diffMins < 1) return \"now\";\n\tif (diffMins < 60) return `${diffMins}m`;\n\tif (diffHours < 24) return `${diffHours}h`;\n\tif (diffDays < 7) return `${diffDays}d`;\n\tif (diffDays < 30) return `${Math.floor(diffDays / 7)}w`;\n\tif (diffDays < 365) return `${Math.floor(diffDays / 30)}mo`;\n\treturn `${Math.floor(diffDays / 365)}y`;\n}\n\nfunction canonicalizePath(path: string | undefined): string | undefined {\n\tif (!path) return path;\n\treturn _canonicalizePath(path);\n}\n\nclass SessionSelectorHeader implements Component {\n\tprivate scope: SessionScope;\n\tprivate sortMode: SortMode;\n\tprivate nameFilter: NameFilter;\n\tprivate requestRender: () => void;\n\tprivate loading = false;\n\tprivate loadProgress: { loaded: number; total: number } | null = null;\n\tprivate showPath = false;\n\tprivate confirmingDeletePath: string | null = null;\n\tprivate statusMessage: { type: \"info\" | \"error\"; message: string } | null = null;\n\tprivate statusTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate showRenameHint = false;\n\n\tconstructor(scope: SessionScope, sortMode: SortMode, nameFilter: NameFilter, requestRender: () => void) {\n\t\tthis.scope = scope;\n\t\tthis.sortMode = sortMode;\n\t\tthis.nameFilter = nameFilter;\n\t\tthis.requestRender = requestRender;\n\t}\n\n\tsetScope(scope: SessionScope): void {\n\t\tthis.scope = scope;\n\t}\n\n\tsetSortMode(sortMode: SortMode): void {\n\t\tthis.sortMode = sortMode;\n\t}\n\n\tsetNameFilter(nameFilter: NameFilter): void {\n\t\tthis.nameFilter = nameFilter;\n\t}\n\n\tsetLoading(loading: boolean): void {\n\t\tthis.loading = loading;\n\t\t// Progress is scoped to the current load; clear whenever the loading state is set\n\t\tthis.loadProgress = null;\n\t}\n\n\tsetProgress(loaded: number, total: number): void {\n\t\tthis.loadProgress = { loaded, total };\n\t}\n\n\tsetShowPath(showPath: boolean): void {\n\t\tthis.showPath = showPath;\n\t}\n\n\tsetShowRenameHint(show: boolean): void {\n\t\tthis.showRenameHint = show;\n\t}\n\n\tsetConfirmingDeletePath(path: string | null): void {\n\t\tthis.confirmingDeletePath = path;\n\t}\n\n\tprivate clearStatusTimeout(): void {\n\t\tif (!this.statusTimeout) return;\n\t\tclearTimeout(this.statusTimeout);\n\t\tthis.statusTimeout = null;\n\t}\n\n\tsetStatusMessage(msg: { type: \"info\" | \"error\"; message: string } | null, autoHideMs?: number): void {\n\t\tthis.clearStatusTimeout();\n\t\tthis.statusMessage = msg;\n\t\tif (!msg || !autoHideMs) return;\n\n\t\tthis.statusTimeout = setTimeout(() => {\n\t\t\tthis.statusMessage = null;\n\t\t\tthis.statusTimeout = null;\n\t\t\tthis.requestRender();\n\t\t}, autoHideMs);\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst title = this.scope === \"current\" ? \"Resume Session (Current Folder)\" : \"Resume Session (All)\";\n\t\tconst leftText = theme.bold(title);\n\n\t\tconst sortLabel = this.sortMode === \"threaded\" ? \"Threaded\" : this.sortMode === \"recent\" ? \"Recent\" : \"Fuzzy\";\n\t\tconst sortText = theme.fg(\"muted\", \"Sort: \") + theme.fg(\"accent\", sortLabel);\n\n\t\tconst nameLabel = this.nameFilter === \"all\" ? \"All\" : \"Named\";\n\t\tconst nameText = theme.fg(\"muted\", \"Name: \") + theme.fg(\"accent\", nameLabel);\n\n\t\tlet scopeText: string;\n\t\tif (this.loading) {\n\t\t\tconst progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : \"...\";\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", `Loading ${progressText}`)}`;\n\t\t} else if (this.scope === \"current\") {\n\t\t\tscopeText = `${theme.fg(\"accent\", \"◉ Current Folder\")}${theme.fg(\"muted\", \" | ○ All\")}`;\n\t\t} else {\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", \"◉ All\")}`;\n\t\t}\n\n\t\tconst rightText = truncateToWidth(`${scopeText} ${nameText} ${sortText}`, width, \"\");\n\t\tconst availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);\n\t\tconst left = truncateToWidth(leftText, availableLeft, \"\");\n\t\tconst spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));\n\n\t\t// Build hint lines - changes based on state (all branches truncate to width)\n\t\tlet hintLine1: string;\n\t\tlet hintLine2: string;\n\t\tif (this.confirmingDeletePath !== null) {\n\t\t\tconst confirmHint = `Delete session? ${keyHint(\"tui.select.confirm\", \"confirm\")} · ${keyHint(\"tui.select.cancel\", \"cancel\")}`;\n\t\t\thintLine1 = theme.fg(\"error\", truncateToWidth(confirmHint, width, \"…\"));\n\t\t\thintLine2 = \"\";\n\t\t} else if (this.statusMessage) {\n\t\t\tconst color = this.statusMessage.type === \"error\" ? \"error\" : \"accent\";\n\t\t\thintLine1 = theme.fg(color, truncateToWidth(this.statusMessage.message, width, \"…\"));\n\t\t\thintLine2 = \"\";\n\t\t} else {\n\t\t\tconst pathState = this.showPath ? \"(on)\" : \"(off)\";\n\t\t\tconst sep = theme.fg(\"muted\", \" · \");\n\t\t\tconst hint1 =\n\t\t\t\tkeyHint(\"tui.input.tab\", \"scope\") + sep + theme.fg(\"muted\", 're:<pattern> regex · \"phrase\" exact');\n\t\t\tconst hint2Parts = [\n\t\t\t\tkeyHint(\"app.session.toggleSort\", \"sort\"),\n\t\t\t\tkeyHint(\"app.session.toggleNamedFilter\", \"named\"),\n\t\t\t\tkeyHint(\"app.session.delete\", \"delete\"),\n\t\t\t\tkeyHint(\"app.session.togglePath\", `path ${pathState}`),\n\t\t\t];\n\t\t\tif (this.showRenameHint) {\n\t\t\t\thint2Parts.push(keyHint(\"app.session.rename\", \"rename\"));\n\t\t\t}\n\t\t\tconst hint2 = hint2Parts.join(sep);\n\t\t\thintLine1 = truncateToWidth(hint1, width, \"…\");\n\t\t\thintLine2 = truncateToWidth(hint2, width, \"…\");\n\t\t}\n\n\t\treturn [`${left}${\" \".repeat(spacing)}${rightText}`, hintLine1, hintLine2];\n\t}\n}\n\n/** A session tree node for hierarchical display */\ninterface SessionTreeNode {\n\tsession: SessionInfo;\n\tchildren: SessionTreeNode[];\n\tlatestActivity: number;\n}\n\n/** Flattened node for display with tree structure info */\ninterface FlatSessionNode {\n\tsession: SessionInfo;\n\tdepth: number;\n\tisLast: boolean;\n\t/** For each ancestor level, whether there are more siblings after it */\n\tancestorContinues: boolean[];\n}\n\n/**\n * Build a tree structure from sessions based on parentSessionPath.\n * Returns root nodes sorted by modified date (descending).\n */\nfunction buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] {\n\tconst byPath = new Map<string, SessionTreeNode>();\n\n\tfor (const session of sessions) {\n\t\tconst sessionPath = canonicalizePath(session.path) ?? session.path;\n\t\tbyPath.set(sessionPath, { session, children: [], latestActivity: session.modified.getTime() });\n\t}\n\n\tconst roots: SessionTreeNode[] = [];\n\n\tfor (const session of sessions) {\n\t\tconst sessionPath = canonicalizePath(session.path) ?? session.path;\n\t\tconst node = byPath.get(sessionPath)!;\n\t\tconst parentPath = canonicalizePath(session.parentSessionPath);\n\n\t\tif (parentPath && byPath.has(parentPath)) {\n\t\t\tbyPath.get(parentPath)!.children.push(node);\n\t\t} else {\n\t\t\troots.push(node);\n\t\t}\n\t}\n\n\tconst updateLatestActivity = (node: SessionTreeNode): number => {\n\t\tlet latestActivity = node.session.modified.getTime();\n\t\tfor (const child of node.children) {\n\t\t\tlatestActivity = Math.max(latestActivity, updateLatestActivity(child));\n\t\t}\n\t\tnode.latestActivity = latestActivity;\n\t\treturn latestActivity;\n\t};\n\n\tfor (const root of roots) {\n\t\tupdateLatestActivity(root);\n\t}\n\n\t// Sort children and roots by latest activity in each subtree (descending)\n\tconst sortNodes = (nodes: SessionTreeNode[]): void => {\n\t\tnodes.sort((a, b) => b.latestActivity - a.latestActivity);\n\t\tfor (const node of nodes) {\n\t\t\tsortNodes(node.children);\n\t\t}\n\t};\n\tsortNodes(roots);\n\n\treturn roots;\n}\n\n/**\n * Flatten tree into display list with tree structure metadata.\n */\nfunction flattenSessionTree(roots: SessionTreeNode[]): FlatSessionNode[] {\n\tconst result: FlatSessionNode[] = [];\n\n\tconst walk = (node: SessionTreeNode, depth: number, ancestorContinues: boolean[], isLast: boolean): void => {\n\t\tresult.push({ session: node.session, depth, isLast, ancestorContinues });\n\n\t\tfor (let i = 0; i < node.children.length; i++) {\n\t\t\tconst childIsLast = i === node.children.length - 1;\n\t\t\t// Only show continuation line for non-root ancestors\n\t\t\tconst continues = depth > 0 ? !isLast : false;\n\t\t\twalk(node.children[i]!, depth + 1, [...ancestorContinues, continues], childIsLast);\n\t\t}\n\t};\n\n\tfor (let i = 0; i < roots.length; i++) {\n\t\twalk(roots[i]!, 0, [], i === roots.length - 1);\n\t}\n\n\treturn result;\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component, Focusable {\n\tpublic getSelectedSessionPath(): string | undefined {\n\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\treturn selected?.session.path;\n\t}\n\tprivate allSessions: SessionInfo[] = [];\n\tprivate filteredSessions: FlatSessionNode[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tprivate showCwd = false;\n\tprivate sortMode: SortMode = \"threaded\";\n\tprivate nameFilter: NameFilter = \"all\";\n\tprivate keybindings: KeybindingsManager;\n\tprivate showPath = false;\n\tprivate confirmingDeletePath: string | null = null;\n\tprivate currentSessionCanonicalPath?: string;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tpublic onExit: () => void = () => {};\n\tpublic onToggleScope?: () => void;\n\tpublic onToggleSort?: () => void;\n\tpublic onToggleNameFilter?: () => void;\n\tpublic onTogglePath?: (showPath: boolean) => void;\n\tpublic onDeleteConfirmationChange?: (path: string | null) => void;\n\tpublic onDeleteSession?: (sessionPath: string) => Promise<void>;\n\tpublic onRenameSession?: (sessionPath: string) => void;\n\tpublic onError?: (message: string) => void;\n\tprivate maxVisible: number = 10; // Max sessions visible (one line each)\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\n\tconstructor(\n\t\tsessions: SessionInfo[],\n\t\tshowCwd: boolean,\n\t\tsortMode: SortMode,\n\t\tnameFilter: NameFilter,\n\t\tkeybindings: KeybindingsManager,\n\t\tcurrentSessionFilePath?: string,\n\t) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = [];\n\t\tthis.searchInput = new Input();\n\t\tthis.showCwd = showCwd;\n\t\tthis.sortMode = sortMode;\n\t\tthis.nameFilter = nameFilter;\n\t\tthis.keybindings = keybindings;\n\t\tthis.currentSessionCanonicalPath = canonicalizePath(currentSessionFilePath);\n\t\tthis.filterSessions(\"\");\n\n\t\t// Handle Enter in search input - select current item\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\tif (this.filteredSessions[this.selectedIndex]) {\n\t\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\t\tif (this.onSelect) {\n\t\t\t\t\tthis.onSelect(selected.session.path);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tsetSortMode(sortMode: SortMode): void {\n\t\tthis.sortMode = sortMode;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tsetNameFilter(nameFilter: NameFilter): void {\n\t\tthis.nameFilter = nameFilter;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tsetSessions(sessions: SessionInfo[], showCwd: boolean): void {\n\t\tthis.allSessions = sessions;\n\t\tthis.showCwd = showCwd;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tprivate filterSessions(query: string): void {\n\t\tconst trimmed = query.trim();\n\t\tconst nameFiltered =\n\t\t\tthis.nameFilter === \"all\" ? this.allSessions : this.allSessions.filter((session) => hasSessionName(session));\n\n\t\tif (this.sortMode === \"threaded\" && !trimmed) {\n\t\t\t// Threaded mode without search: show tree structure\n\t\t\tconst roots = buildSessionTree(nameFiltered);\n\t\t\tthis.filteredSessions = flattenSessionTree(roots);\n\t\t} else {\n\t\t\t// Other modes or with search: flat list\n\t\t\tconst filtered = filterAndSortSessions(nameFiltered, query, this.sortMode, \"all\");\n\t\t\tthis.filteredSessions = filtered.map((session) => ({\n\t\t\t\tsession,\n\t\t\t\tdepth: 0,\n\t\t\t\tisLast: true,\n\t\t\t\tancestorContinues: [],\n\t\t\t}));\n\t\t}\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tprivate setConfirmingDeletePath(path: string | null): void {\n\t\tthis.confirmingDeletePath = path;\n\t\tthis.onDeleteConfirmationChange?.(path);\n\t}\n\n\tprivate startDeleteConfirmationForSelectedSession(): void {\n\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\tif (!selected) return;\n\n\t\t// Prevent deleting current session\n\t\tif (this.isCurrentSessionPath(selected.session.path)) {\n\t\t\tthis.onError?.(\"Cannot delete the currently active session\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setConfirmingDeletePath(selected.session.path);\n\t}\n\n\tprivate isCurrentSessionPath(path: string): boolean {\n\t\tif (!this.currentSessionCanonicalPath) return false;\n\t\treturn (canonicalizePath(path) ?? path) === this.currentSessionCanonicalPath;\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Render search input\n\t\tlines.push(...this.searchInput.render(width));\n\t\tlines.push(\"\"); // Blank line after search\n\n\t\tif (this.filteredSessions.length === 0) {\n\t\t\tlet emptyMessage: string;\n\t\t\tif (this.nameFilter === \"named\") {\n\t\t\t\tconst toggleKey = keyText(\"app.session.toggleNamedFilter\");\n\t\t\t\tif (this.showCwd) {\n\t\t\t\t\temptyMessage = ` No named sessions found. Press ${toggleKey} to show all.`;\n\t\t\t\t} else {\n\t\t\t\t\temptyMessage = ` No named sessions in current folder. Press ${toggleKey} to show all, or Tab to view all.`;\n\t\t\t\t}\n\t\t\t} else if (this.showCwd) {\n\t\t\t\t// \"All\" scope - no sessions anywhere that match filter\n\t\t\t\temptyMessage = \" No sessions found\";\n\t\t\t} else {\n\t\t\t\t// \"Current folder\" scope - hint to try \"all\"\n\t\t\t\temptyMessage = \" No sessions in current folder. Press Tab to view all.\";\n\t\t\t}\n\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(emptyMessage, width, \"…\")));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);\n\n\t\t// Render visible sessions (one line each with tree structure)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst node = this.filteredSessions[i]!;\n\t\t\tconst session = node.session;\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isConfirmingDelete = session.path === this.confirmingDeletePath;\n\t\t\tconst isCurrent = this.isCurrentSessionPath(session.path);\n\n\t\t\t// Build tree prefix\n\t\t\tconst prefix = this.buildTreePrefix(node);\n\n\t\t\t// Session display text (name or first message)\n\t\t\tconst displayText = session.name ?? session.firstMessage;\n\t\t\tconst normalizedMessage = displayText.replace(/[\\x00-\\x1f\\x7f]/g, \" \").trim();\n\n\t\t\t// Right side: message count and age\n\t\t\tconst age = formatSessionDate(session.modified);\n\t\t\tconst msgCount = String(session.messageCount);\n\t\t\tlet rightPart = `${msgCount} ${age}`;\n\t\t\tif (this.showCwd && session.cwd) {\n\t\t\t\trightPart = `${shortenPath(session.cwd)} ${rightPart}`;\n\t\t\t}\n\t\t\tif (this.showPath) {\n\t\t\t\trightPart = `${shortenPath(session.path)} ${rightPart}`;\n\t\t\t}\n\n\t\t\t// Cursor\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\n\t\t\t// Calculate available width for message\n\t\t\tconst prefixWidth = visibleWidth(prefix);\n\t\t\tconst rightWidth = visibleWidth(rightPart) + 2; // +2 for spacing\n\t\t\tconst availableForMsg = width - 2 - prefixWidth - rightWidth; // -2 for cursor\n\n\t\t\tconst truncatedMsg = truncateToWidth(normalizedMessage, Math.max(10, availableForMsg), \"…\");\n\n\t\t\t// Style message - selected row uses accent text (no background fill), matching the other selectors\n\t\t\tlet messageColor: \"error\" | \"accent\" | null = null;\n\t\t\tif (isConfirmingDelete) {\n\t\t\t\tmessageColor = \"error\";\n\t\t\t} else if (isSelected || isCurrent) {\n\t\t\t\tmessageColor = \"accent\";\n\t\t\t}\n\t\t\tconst styledMsg = messageColor ? theme.fg(messageColor, truncatedMsg) : truncatedMsg;\n\n\t\t\t// Build line\n\t\t\tconst leftPart = cursor + theme.fg(\"dim\", prefix) + styledMsg;\n\t\t\tconst leftWidth = visibleWidth(leftPart);\n\t\t\tconst spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart));\n\t\t\tconst styledRight = theme.fg(isConfirmingDelete ? \"error\" : \"dim\", rightPart);\n\n\t\t\tconst line = leftPart + \" \".repeat(spacing) + styledRight;\n\t\t\tlines.push(truncateToWidth(line, width));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredSessions.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;\n\t\t\tconst scrollInfo = theme.fg(\"muted\", truncateToWidth(scrollText, width, \"\"));\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate buildTreePrefix(node: FlatSessionNode): string {\n\t\tif (node.depth === 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tconst parts = node.ancestorContinues.map((continues) => (continues ? \"│ \" : \" \"));\n\t\tconst branch = node.isLast ? \"└─ \" : \"├─ \";\n\t\treturn parts.join(\"\") + branch;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\t// Handle delete confirmation state first - intercept all keys\n\t\tif (this.confirmingDeletePath !== null) {\n\t\t\tif (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\t\tconst pathToDelete = this.confirmingDeletePath;\n\t\t\t\tthis.setConfirmingDeletePath(null);\n\t\t\t\tvoid this.onDeleteSession?.(pathToDelete);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\t\tthis.setConfirmingDeletePath(null);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Ignore all other keys while confirming\n\t\t\treturn;\n\t\t}\n\n\t\tif (kb.matches(keyData, \"tui.input.tab\")) {\n\t\t\tif (this.onToggleScope) {\n\t\t\t\tthis.onToggleScope();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (kb.matches(keyData, \"app.session.toggleSort\")) {\n\t\t\tthis.onToggleSort?.();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.keybindings.matches(keyData, \"app.session.toggleNamedFilter\")) {\n\t\t\tthis.onToggleNameFilter?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+P: toggle path display\n\t\tif (kb.matches(keyData, \"app.session.togglePath\")) {\n\t\t\tthis.showPath = !this.showPath;\n\t\t\tthis.onTogglePath?.(this.showPath);\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace)\n\t\tif (kb.matches(keyData, \"app.session.delete\")) {\n\t\t\tthis.startDeleteConfirmationForSelectedSession();\n\t\t\treturn;\n\t\t}\n\n\t\t// Rename selected session\n\t\tif (kb.matches(keyData, \"app.session.rename\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected) {\n\t\t\t\tthis.onRenameSession?.(selected.session.path);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+Backspace: non-invasive convenience alias for delete\n\t\t// Only triggers deletion when the query is empty; otherwise it is forwarded to the input\n\t\tif (kb.matches(keyData, \"app.session.deleteNoninvasive\")) {\n\t\t\tif (this.searchInput.getValue().length > 0) {\n\t\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.startDeleteConfirmationForSelectedSession();\n\t\t\treturn;\n\t\t}\n\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Page up - jump up by maxVisible items\n\t\telse if (kb.matches(keyData, \"tui.select.pageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t}\n\t\t// Page down - jump down by maxVisible items\n\t\telse if (kb.matches(keyData, \"tui.select.pageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + this.maxVisible);\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.session.path);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t}\n\t}\n}\n\ntype SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;\n\n/**\n * Delete a session file, trying the `trash` CLI first, then falling back to unlink\n */\nasync function deleteSessionFile(\n\tsessionPath: string,\n): Promise<{ ok: boolean; method: \"trash\" | \"unlink\"; error?: string }> {\n\t// Try `trash` first (if installed)\n\tconst trashArgs = sessionPath.startsWith(\"-\") ? [\"--\", sessionPath] : [sessionPath];\n\tconst trashResult = spawnSync(\"trash\", trashArgs, { encoding: \"utf-8\" });\n\n\tconst getTrashErrorHint = (): string | null => {\n\t\tconst parts: string[] = [];\n\t\tif (trashResult.error) {\n\t\t\tparts.push(trashResult.error.message);\n\t\t}\n\t\tconst stderr = trashResult.stderr?.trim();\n\t\tif (stderr) {\n\t\t\tparts.push(stderr.split(\"\\n\")[0] ?? stderr);\n\t\t}\n\t\tif (parts.length === 0) return null;\n\t\treturn `trash: ${parts.join(\" · \").slice(0, 200)}`;\n\t};\n\n\t// If trash reports success, or the file is gone afterwards, treat it as successful\n\tif (trashResult.status === 0 || !existsSync(sessionPath)) {\n\t\treturn { ok: true, method: \"trash\" };\n\t}\n\n\t// Fallback to permanent deletion\n\ttry {\n\t\tawait unlink(sessionPath);\n\t\treturn { ok: true, method: \"unlink\" };\n\t} catch (err) {\n\t\tconst unlinkError = err instanceof Error ? err.message : String(err);\n\t\tconst trashErrorHint = getTrashErrorHint();\n\t\tconst error = trashErrorHint ? `${unlinkError} (${trashErrorHint})` : unlinkError;\n\t\treturn { ok: false, method: \"unlink\", error };\n\t}\n}\n\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container implements Focusable {\n\thandleInput(data: string): void {\n\t\tif (this.mode === \"rename\") {\n\t\t\tconst kb = getKeybindings();\n\t\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\t\tthis.exitRenameMode();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.renameInput.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.sessionList.handleInput(data);\n\t}\n\n\tprivate canRename = true;\n\tprivate sessionList: SessionList;\n\tprivate header: SessionSelectorHeader;\n\tprivate keybindings: KeybindingsManager;\n\tprivate scope: SessionScope = \"current\";\n\tprivate sortMode: SortMode = \"threaded\";\n\tprivate nameFilter: NameFilter = \"all\";\n\tprivate currentSessions: SessionInfo[] | null = null;\n\tprivate allSessions: SessionInfo[] | null = null;\n\tprivate currentSessionsLoader: SessionsLoader;\n\tprivate allSessionsLoader: SessionsLoader;\n\tprivate requestRender: () => void;\n\tprivate renameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;\n\tprivate currentLoading = false;\n\tprivate allLoading = false;\n\tprivate allLoadSeq = 0;\n\n\tprivate mode: \"list\" | \"rename\" = \"list\";\n\tprivate renameInput = new Input();\n\tprivate renameTargetPath: string | null = null;\n\n\t// Focusable implementation - propagate to sessionList for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.sessionList.focused = value;\n\t\tthis.renameInput.focused = value;\n\t\tif (value && this.mode === \"rename\") {\n\t\t\tthis.renameInput.focused = true;\n\t\t}\n\t}\n\n\tprivate buildBaseLayout(content: Component, options?: { showHeader?: boolean }): void {\n\t\tthis.clear();\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder((s) => theme.fg(\"accent\", s)));\n\t\tthis.addChild(new Spacer(1));\n\t\tif (options?.showHeader ?? true) {\n\t\t\tthis.addChild(this.header);\n\t\t\tthis.addChild(new Spacer(1));\n\t\t}\n\t\tthis.addChild(content);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder((s) => theme.fg(\"accent\", s)));\n\t}\n\n\tconstructor(\n\t\tcurrentSessionsLoader: SessionsLoader,\n\t\tallSessionsLoader: SessionsLoader,\n\t\tonSelect: (sessionPath: string) => void,\n\t\tonCancel: () => void,\n\t\tonExit: () => void,\n\t\trequestRender: () => void,\n\t\toptions?: {\n\t\t\trenameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;\n\t\t\tshowRenameHint?: boolean;\n\t\t\tkeybindings?: KeybindingsManager;\n\t\t},\n\t\tcurrentSessionFilePath?: string,\n\t) {\n\t\tsuper();\n\t\tthis.keybindings = options?.keybindings ?? KeybindingsManager.create();\n\t\tthis.currentSessionsLoader = currentSessionsLoader;\n\t\tthis.allSessionsLoader = allSessionsLoader;\n\t\tthis.requestRender = requestRender;\n\t\tthis.header = new SessionSelectorHeader(this.scope, this.sortMode, this.nameFilter, this.requestRender);\n\t\tconst renameSession = options?.renameSession;\n\t\tthis.renameSession = renameSession;\n\t\tthis.canRename = !!renameSession;\n\t\tthis.header.setShowRenameHint(options?.showRenameHint ?? this.canRename);\n\n\t\t// Create session list (starts empty, will be populated after load)\n\t\tthis.sessionList = new SessionList(\n\t\t\t[],\n\t\t\tfalse,\n\t\t\tthis.sortMode,\n\t\t\tthis.nameFilter,\n\t\t\tthis.keybindings,\n\t\t\tcurrentSessionFilePath,\n\t\t);\n\n\t\tthis.buildBaseLayout(this.sessionList);\n\n\t\tthis.renameInput.onSubmit = (value) => {\n\t\t\tvoid this.confirmRename(value);\n\t\t};\n\n\t\t// Ensure header status timeouts are cleared when leaving the selector\n\t\tconst clearStatusMessage = () => this.header.setStatusMessage(null);\n\t\tthis.sessionList.onSelect = (sessionPath) => {\n\t\t\tclearStatusMessage();\n\t\t\tonSelect(sessionPath);\n\t\t};\n\t\tthis.sessionList.onCancel = () => {\n\t\t\tclearStatusMessage();\n\t\t\tonCancel();\n\t\t};\n\t\tthis.sessionList.onExit = () => {\n\t\t\tclearStatusMessage();\n\t\t\tonExit();\n\t\t};\n\t\tthis.sessionList.onToggleScope = () => this.toggleScope();\n\t\tthis.sessionList.onToggleSort = () => this.toggleSortMode();\n\t\tthis.sessionList.onToggleNameFilter = () => this.toggleNameFilter();\n\t\tthis.sessionList.onRenameSession = (sessionPath) => {\n\t\t\tif (!renameSession) return;\n\t\t\tif (this.scope === \"current\" && this.currentLoading) return;\n\t\t\tif (this.scope === \"all\" && this.allLoading) return;\n\n\t\t\tconst sessions = this.scope === \"all\" ? (this.allSessions ?? []) : (this.currentSessions ?? []);\n\t\t\tconst session = sessions.find((s) => s.path === sessionPath);\n\t\t\tthis.enterRenameMode(sessionPath, session?.name);\n\t\t};\n\n\t\t// Sync list events to header\n\t\tthis.sessionList.onTogglePath = (showPath) => {\n\t\t\tthis.header.setShowPath(showPath);\n\t\t\tthis.requestRender();\n\t\t};\n\t\tthis.sessionList.onDeleteConfirmationChange = (path) => {\n\t\t\tthis.header.setConfirmingDeletePath(path);\n\t\t\tthis.requestRender();\n\t\t};\n\t\tthis.sessionList.onError = (msg) => {\n\t\t\tthis.header.setStatusMessage({ type: \"error\", message: msg }, 3000);\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\t// Handle session deletion\n\t\tthis.sessionList.onDeleteSession = async (sessionPath: string) => {\n\t\t\tconst result = await deleteSessionFile(sessionPath);\n\n\t\t\tif (result.ok) {\n\t\t\t\tif (this.currentSessions) {\n\t\t\t\t\tthis.currentSessions = this.currentSessions.filter((s) => s.path !== sessionPath);\n\t\t\t\t}\n\t\t\t\tif (this.allSessions) {\n\t\t\t\t\tthis.allSessions = this.allSessions.filter((s) => s.path !== sessionPath);\n\t\t\t\t}\n\n\t\t\t\tconst sessions = this.scope === \"all\" ? (this.allSessions ?? []) : (this.currentSessions ?? []);\n\t\t\t\tconst showCwd = this.scope === \"all\";\n\t\t\t\tthis.sessionList.setSessions(sessions, showCwd);\n\n\t\t\t\tconst msg = result.method === \"trash\" ? \"Session moved to trash\" : \"Session deleted\";\n\t\t\t\tthis.header.setStatusMessage({ type: \"info\", message: msg }, 2000);\n\t\t\t\tawait this.refreshSessionsAfterMutation();\n\t\t\t} else {\n\t\t\t\tconst errorMessage = result.error ?? \"Unknown error\";\n\t\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to delete: ${errorMessage}` }, 3000);\n\t\t\t}\n\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\t// Start loading current sessions immediately\n\t\tthis.loadCurrentSessions();\n\t}\n\n\tprivate loadCurrentSessions(): void {\n\t\tvoid this.loadScope(\"current\", \"initial\");\n\t}\n\n\tprivate enterRenameMode(sessionPath: string, currentName: string | undefined): void {\n\t\tthis.mode = \"rename\";\n\t\tthis.renameTargetPath = sessionPath;\n\t\tthis.renameInput.setValue(currentName ?? \"\");\n\t\tthis.renameInput.focused = true;\n\n\t\tconst panel = new Container();\n\t\tpanel.addChild(new Text(theme.bold(\"Rename Session\"), 1, 0));\n\t\tpanel.addChild(new Spacer(1));\n\t\tpanel.addChild(this.renameInput);\n\t\tpanel.addChild(new Spacer(1));\n\t\tpanel.addChild(\n\t\t\tnew Text(\n\t\t\t\ttheme.fg(\"muted\", `${keyText(\"tui.select.confirm\")} to save · ${keyText(\"tui.select.cancel\")} to cancel`),\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\n\t\tthis.buildBaseLayout(panel, { showHeader: false });\n\t\tthis.requestRender();\n\t}\n\n\tprivate exitRenameMode(): void {\n\t\tthis.mode = \"list\";\n\t\tthis.renameTargetPath = null;\n\n\t\tthis.buildBaseLayout(this.sessionList);\n\n\t\tthis.requestRender();\n\t}\n\n\tprivate async confirmRename(value: string): Promise<void> {\n\t\tconst next = value.trim();\n\t\tif (!next) return;\n\t\tconst target = this.renameTargetPath;\n\t\tif (!target) {\n\t\t\tthis.exitRenameMode();\n\t\t\treturn;\n\t\t}\n\n\t\t// Find current name for callback\n\t\tconst renameSession = this.renameSession;\n\t\tif (!renameSession) {\n\t\t\tthis.exitRenameMode();\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait renameSession(target, next);\n\t\t\tawait this.refreshSessionsAfterMutation();\n\t\t} finally {\n\t\t\tthis.exitRenameMode();\n\t\t}\n\t}\n\n\tprivate async loadScope(scope: SessionScope, reason: \"initial\" | \"refresh\" | \"toggle\"): Promise<void> {\n\t\tconst showCwd = scope === \"all\";\n\n\t\t// Mark loading\n\t\tif (scope === \"current\") {\n\t\t\tthis.currentLoading = true;\n\t\t} else {\n\t\t\tthis.allLoading = true;\n\t\t}\n\n\t\tconst seq = scope === \"all\" ? ++this.allLoadSeq : undefined;\n\t\tthis.header.setScope(scope);\n\t\tthis.header.setLoading(true);\n\t\tthis.requestRender();\n\n\t\tconst onProgress = (loaded: number, total: number) => {\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\t\t\tthis.header.setProgress(loaded, total);\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\ttry {\n\t\t\tconst sessions = await (scope === \"current\"\n\t\t\t\t? this.currentSessionsLoader(onProgress)\n\t\t\t\t: this.allSessionsLoader(onProgress));\n\n\t\t\tif (scope === \"current\") {\n\t\t\t\tthis.currentSessions = sessions;\n\t\t\t\tthis.currentLoading = false;\n\t\t\t} else {\n\t\t\t\tthis.allSessions = sessions;\n\t\t\t\tthis.allLoading = false;\n\t\t\t}\n\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\n\t\t\tthis.header.setLoading(false);\n\t\t\tthis.sessionList.setSessions(sessions, showCwd);\n\t\t\tthis.requestRender();\n\t\t} catch (err) {\n\t\t\tif (scope === \"current\") {\n\t\t\t\tthis.currentLoading = false;\n\t\t\t} else {\n\t\t\t\tthis.allLoading = false;\n\t\t\t}\n\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tthis.header.setLoading(false);\n\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to load sessions: ${message}` }, 4000);\n\n\t\t\tif (reason === \"initial\") {\n\t\t\t\tthis.sessionList.setSessions([], showCwd);\n\t\t\t}\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate toggleSortMode(): void {\n\t\t// Cycle: threaded -> recent -> relevance -> threaded\n\t\tthis.sortMode = this.sortMode === \"threaded\" ? \"recent\" : this.sortMode === \"recent\" ? \"relevance\" : \"threaded\";\n\t\tthis.header.setSortMode(this.sortMode);\n\t\tthis.sessionList.setSortMode(this.sortMode);\n\t\tthis.requestRender();\n\t}\n\n\tprivate toggleNameFilter(): void {\n\t\tthis.nameFilter = this.nameFilter === \"all\" ? \"named\" : \"all\";\n\t\tthis.header.setNameFilter(this.nameFilter);\n\t\tthis.sessionList.setNameFilter(this.nameFilter);\n\t\tthis.requestRender();\n\t}\n\n\tprivate async refreshSessionsAfterMutation(): Promise<void> {\n\t\tawait this.loadScope(this.scope, \"refresh\");\n\t}\n\n\tprivate toggleScope(): void {\n\t\tif (this.scope === \"current\") {\n\t\t\tthis.scope = \"all\";\n\t\t\tthis.header.setScope(this.scope);\n\n\t\t\tif (this.allSessions !== null) {\n\t\t\t\tthis.header.setLoading(false);\n\t\t\t\tthis.sessionList.setSessions(this.allSessions, true);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!this.allLoading) {\n\t\t\t\tvoid this.loadScope(\"all\", \"toggle\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.scope = \"current\";\n\t\tthis.header.setScope(this.scope);\n\t\tthis.header.setLoading(this.currentLoading);\n\t\tthis.sessionList.setSessions(this.currentSessions ?? [], false);\n\t\tthis.requestRender();\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/session-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { unlink } from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype Focusable,\n\tgetKeybindings,\n\tInput,\n\tSpacer,\n\tText,\n\ttruncateToWidth,\n\tvisibleWidth,\n} from \"@ashx-j/lunr-tui\";\nimport { KeybindingsManager } from \"../../../core/keybindings.ts\";\nimport type { SessionInfo, SessionListProgress } from \"../../../core/session-manager.ts\";\nimport { SessionOwnership } from \"../../../core/session-ownership.ts\";\nimport { canonicalizePath as _canonicalizePath } from \"../../../utils/paths.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint, keyText } from \"./keybinding-hints.ts\";\nimport { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from \"./session-selector-search.ts\";\n\ntype SessionScope = \"current\" | \"all\";\n\nfunction shortenPath(path: string): string {\n\tconst home = os.homedir();\n\tif (!path) return path;\n\tif (path.startsWith(home)) {\n\t\treturn `~${path.slice(home.length)}`;\n\t}\n\treturn path;\n}\n\nfunction formatSessionDate(date: Date): string {\n\tconst now = new Date();\n\tconst diffMs = now.getTime() - date.getTime();\n\tconst diffMins = Math.floor(diffMs / 60000);\n\tconst diffHours = Math.floor(diffMs / 3600000);\n\tconst diffDays = Math.floor(diffMs / 86400000);\n\n\tif (diffMins < 1) return \"now\";\n\tif (diffMins < 60) return `${diffMins}m`;\n\tif (diffHours < 24) return `${diffHours}h`;\n\tif (diffDays < 7) return `${diffDays}d`;\n\tif (diffDays < 30) return `${Math.floor(diffDays / 7)}w`;\n\tif (diffDays < 365) return `${Math.floor(diffDays / 30)}mo`;\n\treturn `${Math.floor(diffDays / 365)}y`;\n}\n\nfunction canonicalizePath(path: string | undefined): string | undefined {\n\tif (!path) return path;\n\treturn _canonicalizePath(path);\n}\n\nclass SessionSelectorHeader implements Component {\n\tprivate scope: SessionScope;\n\tprivate sortMode: SortMode;\n\tprivate nameFilter: NameFilter;\n\tprivate requestRender: () => void;\n\tprivate loading = false;\n\tprivate loadProgress: { loaded: number; total: number } | null = null;\n\tprivate showPath = false;\n\tprivate confirmingDeletePath: string | null = null;\n\tprivate statusMessage: { type: \"info\" | \"error\"; message: string } | null = null;\n\tprivate statusTimeout: ReturnType<typeof setTimeout> | null = null;\n\tprivate showRenameHint = false;\n\n\tconstructor(scope: SessionScope, sortMode: SortMode, nameFilter: NameFilter, requestRender: () => void) {\n\t\tthis.scope = scope;\n\t\tthis.sortMode = sortMode;\n\t\tthis.nameFilter = nameFilter;\n\t\tthis.requestRender = requestRender;\n\t}\n\n\tsetScope(scope: SessionScope): void {\n\t\tthis.scope = scope;\n\t}\n\n\tsetSortMode(sortMode: SortMode): void {\n\t\tthis.sortMode = sortMode;\n\t}\n\n\tsetNameFilter(nameFilter: NameFilter): void {\n\t\tthis.nameFilter = nameFilter;\n\t}\n\n\tsetLoading(loading: boolean): void {\n\t\tthis.loading = loading;\n\t\t// Progress is scoped to the current load; clear whenever the loading state is set\n\t\tthis.loadProgress = null;\n\t}\n\n\tsetProgress(loaded: number, total: number): void {\n\t\tthis.loadProgress = { loaded, total };\n\t}\n\n\tsetShowPath(showPath: boolean): void {\n\t\tthis.showPath = showPath;\n\t}\n\n\tsetShowRenameHint(show: boolean): void {\n\t\tthis.showRenameHint = show;\n\t}\n\n\tsetConfirmingDeletePath(path: string | null): void {\n\t\tthis.confirmingDeletePath = path;\n\t}\n\n\tprivate clearStatusTimeout(): void {\n\t\tif (!this.statusTimeout) return;\n\t\tclearTimeout(this.statusTimeout);\n\t\tthis.statusTimeout = null;\n\t}\n\n\tsetStatusMessage(msg: { type: \"info\" | \"error\"; message: string } | null, autoHideMs?: number): void {\n\t\tthis.clearStatusTimeout();\n\t\tthis.statusMessage = msg;\n\t\tif (!msg || !autoHideMs) return;\n\n\t\tthis.statusTimeout = setTimeout(() => {\n\t\t\tthis.statusMessage = null;\n\t\t\tthis.statusTimeout = null;\n\t\t\tthis.requestRender();\n\t\t}, autoHideMs);\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst title = this.scope === \"current\" ? \"Resume Session (Current Folder)\" : \"Resume Session (All)\";\n\t\tconst leftText = theme.bold(title);\n\n\t\tconst sortLabel = this.sortMode === \"threaded\" ? \"Threaded\" : this.sortMode === \"recent\" ? \"Recent\" : \"Fuzzy\";\n\t\tconst sortText = theme.fg(\"muted\", \"Sort: \") + theme.fg(\"accent\", sortLabel);\n\n\t\tconst nameLabel = this.nameFilter === \"all\" ? \"All\" : \"Named\";\n\t\tconst nameText = theme.fg(\"muted\", \"Name: \") + theme.fg(\"accent\", nameLabel);\n\n\t\tlet scopeText: string;\n\t\tif (this.loading) {\n\t\t\tconst progressText = this.loadProgress ? `${this.loadProgress.loaded}/${this.loadProgress.total}` : \"...\";\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", `Loading ${progressText}`)}`;\n\t\t} else if (this.scope === \"current\") {\n\t\t\tscopeText = `${theme.fg(\"accent\", \"◉ Current Folder\")}${theme.fg(\"muted\", \" | ○ All\")}`;\n\t\t} else {\n\t\t\tscopeText = `${theme.fg(\"muted\", \"○ Current Folder | \")}${theme.fg(\"accent\", \"◉ All\")}`;\n\t\t}\n\n\t\tconst rightText = truncateToWidth(`${scopeText} ${nameText} ${sortText}`, width, \"\");\n\t\tconst availableLeft = Math.max(0, width - visibleWidth(rightText) - 1);\n\t\tconst left = truncateToWidth(leftText, availableLeft, \"\");\n\t\tconst spacing = Math.max(0, width - visibleWidth(left) - visibleWidth(rightText));\n\n\t\t// Build hint lines - changes based on state (all branches truncate to width)\n\t\tlet hintLine1: string;\n\t\tlet hintLine2: string;\n\t\tif (this.confirmingDeletePath !== null) {\n\t\t\tconst confirmHint = `Delete session? ${keyHint(\"tui.select.confirm\", \"confirm\")} · ${keyHint(\"tui.select.cancel\", \"cancel\")}`;\n\t\t\thintLine1 = theme.fg(\"error\", truncateToWidth(confirmHint, width, \"…\"));\n\t\t\thintLine2 = \"\";\n\t\t} else if (this.statusMessage) {\n\t\t\tconst color = this.statusMessage.type === \"error\" ? \"error\" : \"accent\";\n\t\t\thintLine1 = theme.fg(color, truncateToWidth(this.statusMessage.message, width, \"…\"));\n\t\t\thintLine2 = \"\";\n\t\t} else {\n\t\t\tconst pathState = this.showPath ? \"(on)\" : \"(off)\";\n\t\t\tconst sep = theme.fg(\"muted\", \" · \");\n\t\t\tconst hint1 =\n\t\t\t\tkeyHint(\"tui.input.tab\", \"scope\") + sep + theme.fg(\"muted\", 're:<pattern> regex · \"phrase\" exact');\n\t\t\tconst hint2Parts = [\n\t\t\t\tkeyHint(\"app.session.toggleSort\", \"sort\"),\n\t\t\t\tkeyHint(\"app.session.toggleNamedFilter\", \"named\"),\n\t\t\t\tkeyHint(\"app.session.delete\", \"delete\"),\n\t\t\t\tkeyHint(\"app.session.togglePath\", `path ${pathState}`),\n\t\t\t];\n\t\t\tif (this.showRenameHint) {\n\t\t\t\thint2Parts.push(keyHint(\"app.session.rename\", \"rename\"));\n\t\t\t}\n\t\t\tconst hint2 = hint2Parts.join(sep);\n\t\t\thintLine1 = truncateToWidth(hint1, width, \"…\");\n\t\t\thintLine2 = truncateToWidth(hint2, width, \"…\");\n\t\t}\n\n\t\treturn [`${left}${\" \".repeat(spacing)}${rightText}`, hintLine1, hintLine2];\n\t}\n}\n\n/** A session tree node for hierarchical display */\ninterface SessionTreeNode {\n\tsession: SessionInfo;\n\tchildren: SessionTreeNode[];\n\tlatestActivity: number;\n}\n\n/** Flattened node for display with tree structure info */\ninterface FlatSessionNode {\n\tsession: SessionInfo;\n\tdepth: number;\n\tisLast: boolean;\n\t/** For each ancestor level, whether there are more siblings after it */\n\tancestorContinues: boolean[];\n}\n\n/**\n * Build a tree structure from sessions based on parentSessionPath.\n * Returns root nodes sorted by modified date (descending).\n */\nfunction buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] {\n\tconst byPath = new Map<string, SessionTreeNode>();\n\n\tfor (const session of sessions) {\n\t\tconst sessionPath = canonicalizePath(session.path) ?? session.path;\n\t\tbyPath.set(sessionPath, { session, children: [], latestActivity: session.modified.getTime() });\n\t}\n\n\tconst roots: SessionTreeNode[] = [];\n\n\tfor (const session of sessions) {\n\t\tconst sessionPath = canonicalizePath(session.path) ?? session.path;\n\t\tconst node = byPath.get(sessionPath)!;\n\t\tconst parentPath = canonicalizePath(session.parentSessionPath);\n\n\t\tif (parentPath && byPath.has(parentPath)) {\n\t\t\tbyPath.get(parentPath)!.children.push(node);\n\t\t} else {\n\t\t\troots.push(node);\n\t\t}\n\t}\n\n\tconst updateLatestActivity = (node: SessionTreeNode): number => {\n\t\tlet latestActivity = node.session.modified.getTime();\n\t\tfor (const child of node.children) {\n\t\t\tlatestActivity = Math.max(latestActivity, updateLatestActivity(child));\n\t\t}\n\t\tnode.latestActivity = latestActivity;\n\t\treturn latestActivity;\n\t};\n\n\tfor (const root of roots) {\n\t\tupdateLatestActivity(root);\n\t}\n\n\t// Sort children and roots by latest activity in each subtree (descending)\n\tconst sortNodes = (nodes: SessionTreeNode[]): void => {\n\t\tnodes.sort((a, b) => b.latestActivity - a.latestActivity);\n\t\tfor (const node of nodes) {\n\t\t\tsortNodes(node.children);\n\t\t}\n\t};\n\tsortNodes(roots);\n\n\treturn roots;\n}\n\n/**\n * Flatten tree into display list with tree structure metadata.\n */\nfunction flattenSessionTree(roots: SessionTreeNode[]): FlatSessionNode[] {\n\tconst result: FlatSessionNode[] = [];\n\n\tconst walk = (node: SessionTreeNode, depth: number, ancestorContinues: boolean[], isLast: boolean): void => {\n\t\tresult.push({ session: node.session, depth, isLast, ancestorContinues });\n\n\t\tfor (let i = 0; i < node.children.length; i++) {\n\t\t\tconst childIsLast = i === node.children.length - 1;\n\t\t\t// Only show continuation line for non-root ancestors\n\t\t\tconst continues = depth > 0 ? !isLast : false;\n\t\t\twalk(node.children[i]!, depth + 1, [...ancestorContinues, continues], childIsLast);\n\t\t}\n\t};\n\n\tfor (let i = 0; i < roots.length; i++) {\n\t\twalk(roots[i]!, 0, [], i === roots.length - 1);\n\t}\n\n\treturn result;\n}\n\n/**\n * Custom session list component with multi-line items and search\n */\nclass SessionList implements Component, Focusable {\n\tpublic getSelectedSessionPath(): string | undefined {\n\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\treturn selected?.session.path;\n\t}\n\tprivate allSessions: SessionInfo[] = [];\n\tprivate filteredSessions: FlatSessionNode[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate searchInput: Input;\n\tprivate showCwd = false;\n\tprivate sortMode: SortMode = \"threaded\";\n\tprivate nameFilter: NameFilter = \"all\";\n\tprivate keybindings: KeybindingsManager;\n\tprivate showPath = false;\n\tprivate confirmingDeletePath: string | null = null;\n\tprivate currentSessionCanonicalPath?: string;\n\tpublic onSelect?: (sessionPath: string) => void;\n\tpublic onCancel?: () => void;\n\tpublic onExit: () => void = () => {};\n\tpublic onToggleScope?: () => void;\n\tpublic onToggleSort?: () => void;\n\tpublic onToggleNameFilter?: () => void;\n\tpublic onTogglePath?: (showPath: boolean) => void;\n\tpublic onDeleteConfirmationChange?: (path: string | null) => void;\n\tpublic onDeleteSession?: (sessionPath: string) => Promise<void>;\n\tpublic onRenameSession?: (sessionPath: string) => void;\n\tpublic onError?: (message: string) => void;\n\tprivate maxVisible: number = 10; // Max sessions visible (one line each)\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\n\tconstructor(\n\t\tsessions: SessionInfo[],\n\t\tshowCwd: boolean,\n\t\tsortMode: SortMode,\n\t\tnameFilter: NameFilter,\n\t\tkeybindings: KeybindingsManager,\n\t\tcurrentSessionFilePath?: string,\n\t) {\n\t\tthis.allSessions = sessions;\n\t\tthis.filteredSessions = [];\n\t\tthis.searchInput = new Input();\n\t\tthis.showCwd = showCwd;\n\t\tthis.sortMode = sortMode;\n\t\tthis.nameFilter = nameFilter;\n\t\tthis.keybindings = keybindings;\n\t\tthis.currentSessionCanonicalPath = canonicalizePath(currentSessionFilePath);\n\t\tthis.filterSessions(\"\");\n\n\t\t// Handle Enter in search input - select current item\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\tif (this.filteredSessions[this.selectedIndex]) {\n\t\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\t\tif (this.onSelect) {\n\t\t\t\t\tthis.onSelect(selected.session.path);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n\n\tsetSortMode(sortMode: SortMode): void {\n\t\tthis.sortMode = sortMode;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tsetNameFilter(nameFilter: NameFilter): void {\n\t\tthis.nameFilter = nameFilter;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tsetSessions(sessions: SessionInfo[], showCwd: boolean): void {\n\t\tthis.allSessions = sessions;\n\t\tthis.showCwd = showCwd;\n\t\tthis.filterSessions(this.searchInput.getValue());\n\t}\n\n\tprivate filterSessions(query: string): void {\n\t\tconst trimmed = query.trim();\n\t\tconst nameFiltered =\n\t\t\tthis.nameFilter === \"all\" ? this.allSessions : this.allSessions.filter((session) => hasSessionName(session));\n\n\t\tif (this.sortMode === \"threaded\" && !trimmed) {\n\t\t\t// Threaded mode without search: show tree structure\n\t\t\tconst roots = buildSessionTree(nameFiltered);\n\t\t\tthis.filteredSessions = flattenSessionTree(roots);\n\t\t} else {\n\t\t\t// Other modes or with search: flat list\n\t\t\tconst filtered = filterAndSortSessions(nameFiltered, query, this.sortMode, \"all\");\n\t\t\tthis.filteredSessions = filtered.map((session) => ({\n\t\t\t\tsession,\n\t\t\t\tdepth: 0,\n\t\t\t\tisLast: true,\n\t\t\t\tancestorContinues: [],\n\t\t\t}));\n\t\t}\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSessions.length - 1));\n\t}\n\n\tprivate setConfirmingDeletePath(path: string | null): void {\n\t\tthis.confirmingDeletePath = path;\n\t\tthis.onDeleteConfirmationChange?.(path);\n\t}\n\n\tprivate startDeleteConfirmationForSelectedSession(): void {\n\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\tif (!selected) return;\n\n\t\t// Prevent deleting current session\n\t\tif (this.isCurrentSessionPath(selected.session.path)) {\n\t\t\tthis.onError?.(\"Cannot delete the currently active session\");\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setConfirmingDeletePath(selected.session.path);\n\t}\n\n\tprivate isCurrentSessionPath(path: string): boolean {\n\t\tif (!this.currentSessionCanonicalPath) return false;\n\t\treturn (canonicalizePath(path) ?? path) === this.currentSessionCanonicalPath;\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Render search input\n\t\tlines.push(...this.searchInput.render(width));\n\t\tlines.push(\"\"); // Blank line after search\n\n\t\tif (this.filteredSessions.length === 0) {\n\t\t\tlet emptyMessage: string;\n\t\t\tif (this.nameFilter === \"named\") {\n\t\t\t\tconst toggleKey = keyText(\"app.session.toggleNamedFilter\");\n\t\t\t\tif (this.showCwd) {\n\t\t\t\t\temptyMessage = ` No named sessions found. Press ${toggleKey} to show all.`;\n\t\t\t\t} else {\n\t\t\t\t\temptyMessage = ` No named sessions in current folder. Press ${toggleKey} to show all, or Tab to view all.`;\n\t\t\t\t}\n\t\t\t} else if (this.showCwd) {\n\t\t\t\t// \"All\" scope - no sessions anywhere that match filter\n\t\t\t\temptyMessage = \" No sessions found\";\n\t\t\t} else {\n\t\t\t\t// \"Current folder\" scope - hint to try \"all\"\n\t\t\t\temptyMessage = \" No sessions in current folder. Press Tab to view all.\";\n\t\t\t}\n\t\t\tlines.push(theme.fg(\"muted\", truncateToWidth(emptyMessage, width, \"…\")));\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate visible range with scrolling\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredSessions.length - this.maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + this.maxVisible, this.filteredSessions.length);\n\n\t\t// Render visible sessions (one line each with tree structure)\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst node = this.filteredSessions[i]!;\n\t\t\tconst session = node.session;\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isConfirmingDelete = session.path === this.confirmingDeletePath;\n\t\t\tconst isCurrent = this.isCurrentSessionPath(session.path);\n\n\t\t\t// Build tree prefix\n\t\t\tconst prefix = this.buildTreePrefix(node);\n\n\t\t\t// Session display text (name or first message)\n\t\t\tconst displayText = session.name ?? session.firstMessage;\n\t\t\tconst normalizedMessage = displayText.replace(/[\\x00-\\x1f\\x7f]/g, \" \").trim();\n\n\t\t\t// Right side: message count and age\n\t\t\tconst age = formatSessionDate(session.modified);\n\t\t\tconst msgCount = String(session.messageCount);\n\t\t\tlet rightPart = `${msgCount} ${age}`;\n\t\t\tif (this.showCwd && session.cwd) {\n\t\t\t\trightPart = `${shortenPath(session.cwd)} ${rightPart}`;\n\t\t\t}\n\t\t\tif (this.showPath) {\n\t\t\t\trightPart = `${shortenPath(session.path)} ${rightPart}`;\n\t\t\t}\n\n\t\t\t// Cursor\n\t\t\tconst cursor = isSelected ? theme.fg(\"accent\", \"› \") : \" \";\n\n\t\t\t// Calculate available width for message\n\t\t\tconst prefixWidth = visibleWidth(prefix);\n\t\t\tconst rightWidth = visibleWidth(rightPart) + 2; // +2 for spacing\n\t\t\tconst availableForMsg = width - 2 - prefixWidth - rightWidth; // -2 for cursor\n\n\t\t\tconst truncatedMsg = truncateToWidth(normalizedMessage, Math.max(10, availableForMsg), \"…\");\n\n\t\t\t// Style message - selected row uses accent text (no background fill), matching the other selectors\n\t\t\tlet messageColor: \"error\" | \"accent\" | null = null;\n\t\t\tif (isConfirmingDelete) {\n\t\t\t\tmessageColor = \"error\";\n\t\t\t} else if (isSelected || isCurrent) {\n\t\t\t\tmessageColor = \"accent\";\n\t\t\t}\n\t\t\tconst styledMsg = messageColor ? theme.fg(messageColor, truncatedMsg) : truncatedMsg;\n\n\t\t\t// Build line\n\t\t\tconst leftPart = cursor + theme.fg(\"dim\", prefix) + styledMsg;\n\t\t\tconst leftWidth = visibleWidth(leftPart);\n\t\t\tconst spacing = Math.max(1, width - leftWidth - visibleWidth(rightPart));\n\t\t\tconst styledRight = theme.fg(isConfirmingDelete ? \"error\" : \"dim\", rightPart);\n\n\t\t\tconst line = leftPart + \" \".repeat(spacing) + styledRight;\n\t\t\tlines.push(truncateToWidth(line, width));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredSessions.length) {\n\t\t\tconst scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;\n\t\t\tconst scrollInfo = theme.fg(\"muted\", truncateToWidth(scrollText, width, \"\"));\n\t\t\tlines.push(scrollInfo);\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate buildTreePrefix(node: FlatSessionNode): string {\n\t\tif (node.depth === 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tconst parts = node.ancestorContinues.map((continues) => (continues ? \"│ \" : \" \"));\n\t\tconst branch = node.isLast ? \"└─ \" : \"├─ \";\n\t\treturn parts.join(\"\") + branch;\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\t// Handle delete confirmation state first - intercept all keys\n\t\tif (this.confirmingDeletePath !== null) {\n\t\t\tif (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\t\tconst pathToDelete = this.confirmingDeletePath;\n\t\t\t\tthis.setConfirmingDeletePath(null);\n\t\t\t\tvoid this.onDeleteSession?.(pathToDelete);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\t\tthis.setConfirmingDeletePath(null);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Ignore all other keys while confirming\n\t\t\treturn;\n\t\t}\n\n\t\tif (kb.matches(keyData, \"tui.input.tab\")) {\n\t\t\tif (this.onToggleScope) {\n\t\t\t\tthis.onToggleScope();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (kb.matches(keyData, \"app.session.toggleSort\")) {\n\t\t\tthis.onToggleSort?.();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.keybindings.matches(keyData, \"app.session.toggleNamedFilter\")) {\n\t\t\tthis.onToggleNameFilter?.();\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+P: toggle path display\n\t\tif (kb.matches(keyData, \"app.session.togglePath\")) {\n\t\t\tthis.showPath = !this.showPath;\n\t\t\tthis.onTogglePath?.(this.showPath);\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace)\n\t\tif (kb.matches(keyData, \"app.session.delete\")) {\n\t\t\tthis.startDeleteConfirmationForSelectedSession();\n\t\t\treturn;\n\t\t}\n\n\t\t// Rename selected session\n\t\tif (kb.matches(keyData, \"app.session.rename\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected) {\n\t\t\t\tthis.onRenameSession?.(selected.session.path);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Ctrl+Backspace: non-invasive convenience alias for delete\n\t\t// Only triggers deletion when the query is empty; otherwise it is forwarded to the input\n\t\tif (kb.matches(keyData, \"app.session.deleteNoninvasive\")) {\n\t\t\tif (this.searchInput.getValue().length > 0) {\n\t\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.startDeleteConfirmationForSelectedSession();\n\t\t\treturn;\n\t\t}\n\n\t\t// Up arrow\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - 1);\n\t\t}\n\t\t// Down arrow\n\t\telse if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);\n\t\t}\n\t\t// Page up - jump up by maxVisible items\n\t\telse if (kb.matches(keyData, \"tui.select.pageUp\")) {\n\t\t\tthis.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);\n\t\t}\n\t\t// Page down - jump down by maxVisible items\n\t\telse if (kb.matches(keyData, \"tui.select.pageDown\")) {\n\t\t\tthis.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + this.maxVisible);\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tconst selected = this.filteredSessions[this.selectedIndex];\n\t\t\tif (selected && this.onSelect) {\n\t\t\t\tthis.onSelect(selected.session.path);\n\t\t\t}\n\t\t}\n\t\t// Escape - cancel\n\t\telse if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tif (this.onCancel) {\n\t\t\t\tthis.onCancel();\n\t\t\t}\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterSessions(this.searchInput.getValue());\n\t\t}\n\t}\n}\n\ntype SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;\n\n/**\n * Delete a session file, trying the `trash` CLI first, then falling back to unlink\n */\nasync function deleteSessionFile(\n\tsessionPath: string,\n): Promise<{ ok: boolean; method: \"trash\" | \"unlink\"; error?: string }> {\n\tlet ownership: SessionOwnership;\n\ttry {\n\t\townership = new SessionOwnership(sessionPath);\n\t} catch (error) {\n\t\treturn { ok: false, method: \"unlink\", error: error instanceof Error ? error.message : String(error) };\n\t}\n\ttry {\n\t\treturn await deleteOwnedSessionFile(sessionPath);\n\t} finally {\n\t\townership.release();\n\t}\n}\n\nasync function deleteOwnedSessionFile(\n\tsessionPath: string,\n): Promise<{ ok: boolean; method: \"trash\" | \"unlink\"; error?: string }> {\n\t// Try `trash` first (if installed)\n\tconst trashArgs = sessionPath.startsWith(\"-\") ? [\"--\", sessionPath] : [sessionPath];\n\tconst trashResult = spawnSync(\"trash\", trashArgs, { encoding: \"utf-8\" });\n\n\tconst getTrashErrorHint = (): string | null => {\n\t\tconst parts: string[] = [];\n\t\tif (trashResult.error) {\n\t\t\tparts.push(trashResult.error.message);\n\t\t}\n\t\tconst stderr = trashResult.stderr?.trim();\n\t\tif (stderr) {\n\t\t\tparts.push(stderr.split(\"\\n\")[0] ?? stderr);\n\t\t}\n\t\tif (parts.length === 0) return null;\n\t\treturn `trash: ${parts.join(\" · \").slice(0, 200)}`;\n\t};\n\n\t// If trash reports success, or the file is gone afterwards, treat it as successful\n\tif (trashResult.status === 0 || !existsSync(sessionPath)) {\n\t\treturn { ok: true, method: \"trash\" };\n\t}\n\n\t// Fallback to permanent deletion\n\ttry {\n\t\tawait unlink(sessionPath);\n\t\treturn { ok: true, method: \"unlink\" };\n\t} catch (err) {\n\t\tconst unlinkError = err instanceof Error ? err.message : String(err);\n\t\tconst trashErrorHint = getTrashErrorHint();\n\t\tconst error = trashErrorHint ? `${unlinkError} (${trashErrorHint})` : unlinkError;\n\t\treturn { ok: false, method: \"unlink\", error };\n\t}\n}\n\n/**\n * Component that renders a session selector\n */\nexport class SessionSelectorComponent extends Container implements Focusable {\n\thandleInput(data: string): void {\n\t\tif (this.mode === \"rename\") {\n\t\t\tconst kb = getKeybindings();\n\t\t\tif (kb.matches(data, \"tui.select.cancel\")) {\n\t\t\t\tthis.exitRenameMode();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.renameInput.handleInput(data);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.sessionList.handleInput(data);\n\t}\n\n\tprivate canRename = true;\n\tprivate sessionList: SessionList;\n\tprivate header: SessionSelectorHeader;\n\tprivate keybindings: KeybindingsManager;\n\tprivate scope: SessionScope = \"current\";\n\tprivate sortMode: SortMode = \"threaded\";\n\tprivate nameFilter: NameFilter = \"all\";\n\tprivate currentSessions: SessionInfo[] | null = null;\n\tprivate allSessions: SessionInfo[] | null = null;\n\tprivate currentSessionsLoader: SessionsLoader;\n\tprivate allSessionsLoader: SessionsLoader;\n\tprivate requestRender: () => void;\n\tprivate renameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;\n\tprivate currentLoading = false;\n\tprivate allLoading = false;\n\tprivate allLoadSeq = 0;\n\n\tprivate mode: \"list\" | \"rename\" = \"list\";\n\tprivate renameInput = new Input();\n\tprivate renameTargetPath: string | null = null;\n\n\t// Focusable implementation - propagate to sessionList for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.sessionList.focused = value;\n\t\tthis.renameInput.focused = value;\n\t\tif (value && this.mode === \"rename\") {\n\t\t\tthis.renameInput.focused = true;\n\t\t}\n\t}\n\n\tprivate buildBaseLayout(content: Component, options?: { showHeader?: boolean }): void {\n\t\tthis.clear();\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder((s) => theme.fg(\"accent\", s)));\n\t\tthis.addChild(new Spacer(1));\n\t\tif (options?.showHeader ?? true) {\n\t\t\tthis.addChild(this.header);\n\t\t\tthis.addChild(new Spacer(1));\n\t\t}\n\t\tthis.addChild(content);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder((s) => theme.fg(\"accent\", s)));\n\t}\n\n\tconstructor(\n\t\tcurrentSessionsLoader: SessionsLoader,\n\t\tallSessionsLoader: SessionsLoader,\n\t\tonSelect: (sessionPath: string) => void,\n\t\tonCancel: () => void,\n\t\tonExit: () => void,\n\t\trequestRender: () => void,\n\t\toptions?: {\n\t\t\trenameSession?: (sessionPath: string, currentName: string | undefined) => Promise<void>;\n\t\t\tshowRenameHint?: boolean;\n\t\t\tkeybindings?: KeybindingsManager;\n\t\t},\n\t\tcurrentSessionFilePath?: string,\n\t) {\n\t\tsuper();\n\t\tthis.keybindings = options?.keybindings ?? KeybindingsManager.create();\n\t\tthis.currentSessionsLoader = currentSessionsLoader;\n\t\tthis.allSessionsLoader = allSessionsLoader;\n\t\tthis.requestRender = requestRender;\n\t\tthis.header = new SessionSelectorHeader(this.scope, this.sortMode, this.nameFilter, this.requestRender);\n\t\tconst renameSession = options?.renameSession;\n\t\tthis.renameSession = renameSession;\n\t\tthis.canRename = !!renameSession;\n\t\tthis.header.setShowRenameHint(options?.showRenameHint ?? this.canRename);\n\n\t\t// Create session list (starts empty, will be populated after load)\n\t\tthis.sessionList = new SessionList(\n\t\t\t[],\n\t\t\tfalse,\n\t\t\tthis.sortMode,\n\t\t\tthis.nameFilter,\n\t\t\tthis.keybindings,\n\t\t\tcurrentSessionFilePath,\n\t\t);\n\n\t\tthis.buildBaseLayout(this.sessionList);\n\n\t\tthis.renameInput.onSubmit = (value) => {\n\t\t\tvoid this.confirmRename(value);\n\t\t};\n\n\t\t// Ensure header status timeouts are cleared when leaving the selector\n\t\tconst clearStatusMessage = () => this.header.setStatusMessage(null);\n\t\tthis.sessionList.onSelect = (sessionPath) => {\n\t\t\tclearStatusMessage();\n\t\t\tonSelect(sessionPath);\n\t\t};\n\t\tthis.sessionList.onCancel = () => {\n\t\t\tclearStatusMessage();\n\t\t\tonCancel();\n\t\t};\n\t\tthis.sessionList.onExit = () => {\n\t\t\tclearStatusMessage();\n\t\t\tonExit();\n\t\t};\n\t\tthis.sessionList.onToggleScope = () => this.toggleScope();\n\t\tthis.sessionList.onToggleSort = () => this.toggleSortMode();\n\t\tthis.sessionList.onToggleNameFilter = () => this.toggleNameFilter();\n\t\tthis.sessionList.onRenameSession = (sessionPath) => {\n\t\t\tif (!renameSession) return;\n\t\t\tif (this.scope === \"current\" && this.currentLoading) return;\n\t\t\tif (this.scope === \"all\" && this.allLoading) return;\n\n\t\t\tconst sessions = this.scope === \"all\" ? (this.allSessions ?? []) : (this.currentSessions ?? []);\n\t\t\tconst session = sessions.find((s) => s.path === sessionPath);\n\t\t\tthis.enterRenameMode(sessionPath, session?.name);\n\t\t};\n\n\t\t// Sync list events to header\n\t\tthis.sessionList.onTogglePath = (showPath) => {\n\t\t\tthis.header.setShowPath(showPath);\n\t\t\tthis.requestRender();\n\t\t};\n\t\tthis.sessionList.onDeleteConfirmationChange = (path) => {\n\t\t\tthis.header.setConfirmingDeletePath(path);\n\t\t\tthis.requestRender();\n\t\t};\n\t\tthis.sessionList.onError = (msg) => {\n\t\t\tthis.header.setStatusMessage({ type: \"error\", message: msg }, 3000);\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\t// Handle session deletion\n\t\tthis.sessionList.onDeleteSession = async (sessionPath: string) => {\n\t\t\tconst result = await deleteSessionFile(sessionPath);\n\n\t\t\tif (result.ok) {\n\t\t\t\tif (this.currentSessions) {\n\t\t\t\t\tthis.currentSessions = this.currentSessions.filter((s) => s.path !== sessionPath);\n\t\t\t\t}\n\t\t\t\tif (this.allSessions) {\n\t\t\t\t\tthis.allSessions = this.allSessions.filter((s) => s.path !== sessionPath);\n\t\t\t\t}\n\n\t\t\t\tconst sessions = this.scope === \"all\" ? (this.allSessions ?? []) : (this.currentSessions ?? []);\n\t\t\t\tconst showCwd = this.scope === \"all\";\n\t\t\t\tthis.sessionList.setSessions(sessions, showCwd);\n\n\t\t\t\tconst msg = result.method === \"trash\" ? \"Session moved to trash\" : \"Session deleted\";\n\t\t\t\tthis.header.setStatusMessage({ type: \"info\", message: msg }, 2000);\n\t\t\t\tawait this.refreshSessionsAfterMutation();\n\t\t\t} else {\n\t\t\t\tconst errorMessage = result.error ?? \"Unknown error\";\n\t\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to delete: ${errorMessage}` }, 3000);\n\t\t\t}\n\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\t// Start loading current sessions immediately\n\t\tthis.loadCurrentSessions();\n\t}\n\n\tprivate loadCurrentSessions(): void {\n\t\tvoid this.loadScope(\"current\", \"initial\");\n\t}\n\n\tprivate enterRenameMode(sessionPath: string, currentName: string | undefined): void {\n\t\tthis.mode = \"rename\";\n\t\tthis.renameTargetPath = sessionPath;\n\t\tthis.renameInput.setValue(currentName ?? \"\");\n\t\tthis.renameInput.focused = true;\n\n\t\tconst panel = new Container();\n\t\tpanel.addChild(new Text(theme.bold(\"Rename Session\"), 1, 0));\n\t\tpanel.addChild(new Spacer(1));\n\t\tpanel.addChild(this.renameInput);\n\t\tpanel.addChild(new Spacer(1));\n\t\tpanel.addChild(\n\t\t\tnew Text(\n\t\t\t\ttheme.fg(\"muted\", `${keyText(\"tui.select.confirm\")} to save · ${keyText(\"tui.select.cancel\")} to cancel`),\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\n\t\tthis.buildBaseLayout(panel, { showHeader: false });\n\t\tthis.requestRender();\n\t}\n\n\tprivate exitRenameMode(): void {\n\t\tthis.mode = \"list\";\n\t\tthis.renameTargetPath = null;\n\n\t\tthis.buildBaseLayout(this.sessionList);\n\n\t\tthis.requestRender();\n\t}\n\n\tprivate async confirmRename(value: string): Promise<void> {\n\t\tconst next = value.trim();\n\t\tif (!next) return;\n\t\tconst target = this.renameTargetPath;\n\t\tif (!target) {\n\t\t\tthis.exitRenameMode();\n\t\t\treturn;\n\t\t}\n\n\t\t// Find current name for callback\n\t\tconst renameSession = this.renameSession;\n\t\tif (!renameSession) {\n\t\t\tthis.exitRenameMode();\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait renameSession(target, next);\n\t\t\tawait this.refreshSessionsAfterMutation();\n\t\t} finally {\n\t\t\tthis.exitRenameMode();\n\t\t}\n\t}\n\n\tprivate async loadScope(scope: SessionScope, reason: \"initial\" | \"refresh\" | \"toggle\"): Promise<void> {\n\t\tconst showCwd = scope === \"all\";\n\n\t\t// Mark loading\n\t\tif (scope === \"current\") {\n\t\t\tthis.currentLoading = true;\n\t\t} else {\n\t\t\tthis.allLoading = true;\n\t\t}\n\n\t\tconst seq = scope === \"all\" ? ++this.allLoadSeq : undefined;\n\t\tthis.header.setScope(scope);\n\t\tthis.header.setLoading(true);\n\t\tthis.requestRender();\n\n\t\tconst onProgress = (loaded: number, total: number) => {\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\t\t\tthis.header.setProgress(loaded, total);\n\t\t\tthis.requestRender();\n\t\t};\n\n\t\ttry {\n\t\t\tconst sessions = await (scope === \"current\"\n\t\t\t\t? this.currentSessionsLoader(onProgress)\n\t\t\t\t: this.allSessionsLoader(onProgress));\n\n\t\t\tif (scope === \"current\") {\n\t\t\t\tthis.currentSessions = sessions;\n\t\t\t\tthis.currentLoading = false;\n\t\t\t} else {\n\t\t\t\tthis.allSessions = sessions;\n\t\t\t\tthis.allLoading = false;\n\t\t\t}\n\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\n\t\t\tthis.header.setLoading(false);\n\t\t\tthis.sessionList.setSessions(sessions, showCwd);\n\t\t\tthis.requestRender();\n\t\t} catch (err) {\n\t\t\tif (scope === \"current\") {\n\t\t\t\tthis.currentLoading = false;\n\t\t\t} else {\n\t\t\t\tthis.allLoading = false;\n\t\t\t}\n\n\t\t\tif (scope !== this.scope) return;\n\t\t\tif (seq !== undefined && seq !== this.allLoadSeq) return;\n\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tthis.header.setLoading(false);\n\t\t\tthis.header.setStatusMessage({ type: \"error\", message: `Failed to load sessions: ${message}` }, 4000);\n\n\t\t\tif (reason === \"initial\") {\n\t\t\t\tthis.sessionList.setSessions([], showCwd);\n\t\t\t}\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate toggleSortMode(): void {\n\t\t// Cycle: threaded -> recent -> relevance -> threaded\n\t\tthis.sortMode = this.sortMode === \"threaded\" ? \"recent\" : this.sortMode === \"recent\" ? \"relevance\" : \"threaded\";\n\t\tthis.header.setSortMode(this.sortMode);\n\t\tthis.sessionList.setSortMode(this.sortMode);\n\t\tthis.requestRender();\n\t}\n\n\tprivate toggleNameFilter(): void {\n\t\tthis.nameFilter = this.nameFilter === \"all\" ? \"named\" : \"all\";\n\t\tthis.header.setNameFilter(this.nameFilter);\n\t\tthis.sessionList.setNameFilter(this.nameFilter);\n\t\tthis.requestRender();\n\t}\n\n\tprivate async refreshSessionsAfterMutation(): Promise<void> {\n\t\tawait this.loadScope(this.scope, \"refresh\");\n\t}\n\n\tprivate toggleScope(): void {\n\t\tif (this.scope === \"current\") {\n\t\t\tthis.scope = \"all\";\n\t\t\tthis.header.setScope(this.scope);\n\n\t\t\tif (this.allSessions !== null) {\n\t\t\t\tthis.header.setLoading(false);\n\t\t\t\tthis.sessionList.setSessions(this.allSessions, true);\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!this.allLoading) {\n\t\t\t\tvoid this.loadScope(\"all\", \"toggle\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.scope = \"current\";\n\t\tthis.header.setScope(this.scope);\n\t\tthis.header.setLoading(this.currentLoading);\n\t\tthis.sessionList.setSessions(this.currentSessions ?? [], false);\n\t\tthis.requestRender();\n\t}\n\n\tgetSessionList(): SessionList {\n\t\treturn this.sessionList;\n\t}\n}\n"]}
@@ -4,6 +4,7 @@ import { unlink } from "node:fs/promises";
4
4
  import * as os from "node:os";
5
5
  import { Container, getKeybindings, Input, Spacer, Text, truncateToWidth, visibleWidth, } from "@ashx-j/lunr-tui";
6
6
  import { KeybindingsManager } from "../../../core/keybindings.js";
7
+ import { SessionOwnership } from "../../../core/session-ownership.js";
7
8
  import { canonicalizePath as _canonicalizePath } from "../../../utils/paths.js";
8
9
  import { theme } from "../theme/theme.js";
9
10
  import { DynamicBorder } from "./dynamic-border.js";
@@ -529,6 +530,21 @@ class SessionList {
529
530
  * Delete a session file, trying the `trash` CLI first, then falling back to unlink
530
531
  */
531
532
  async function deleteSessionFile(sessionPath) {
533
+ let ownership;
534
+ try {
535
+ ownership = new SessionOwnership(sessionPath);
536
+ }
537
+ catch (error) {
538
+ return { ok: false, method: "unlink", error: error instanceof Error ? error.message : String(error) };
539
+ }
540
+ try {
541
+ return await deleteOwnedSessionFile(sessionPath);
542
+ }
543
+ finally {
544
+ ownership.release();
545
+ }
546
+ }
547
+ async function deleteOwnedSessionFile(sessionPath) {
532
548
  // Try `trash` first (if installed)
533
549
  const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath];
534
550
  const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });