pim-agent 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/package.json +4 -3
  2. package/packages/core/src/attachments/AttachmentStore.ts +8 -13
  3. package/packages/core/src/attachments/Attachments.ts +2 -4
  4. package/packages/core/src/extensions/CoreExtensions.ts +1 -1
  5. package/packages/core/src/extensions/apply-patch/coordinator.ts +1 -1
  6. package/packages/core/src/extensions/apply-patch/executor.ts +21 -25
  7. package/packages/core/src/extensions/apply-patch/matcher.ts +1 -1
  8. package/packages/core/src/extensions/apply-patch/model.ts +2 -6
  9. package/packages/core/src/extensions/apply-patch/parser.ts +60 -162
  10. package/packages/core/src/extensions/apply-patch/schema.ts +0 -1
  11. package/packages/core/src/extensions/bash/MemoryCap.ts +5 -4
  12. package/packages/core/src/extensions/bash/capture.ts +16 -23
  13. package/packages/core/src/extensions/bash/format.ts +2 -2
  14. package/packages/core/src/extensions/bash/image.ts +4 -8
  15. package/packages/core/src/extensions/bash/index.ts +1 -1
  16. package/packages/core/src/extensions/bash/render.ts +1 -1
  17. package/packages/core/src/extensions/bash/run.ts +9 -15
  18. package/packages/core/src/extensions/bash/schema.ts +4 -4
  19. package/packages/core/src/extensions/edit/edit.ts +14 -33
  20. package/packages/core/src/extensions/glob/glob.ts +1 -3
  21. package/packages/core/src/extensions/glob/index.ts +7 -9
  22. package/packages/core/src/extensions/glob/render.ts +2 -4
  23. package/packages/core/src/extensions/glob/schema.ts +1 -1
  24. package/packages/core/src/extensions/grep/grep.ts +7 -7
  25. package/packages/core/src/extensions/grep/render.ts +5 -6
  26. package/packages/core/src/extensions/grep/schema.ts +3 -7
  27. package/packages/core/src/extensions/read/ImageMemory.ts +2 -8
  28. package/packages/core/src/extensions/read/image.ts +3 -3
  29. package/packages/core/src/extensions/read/index.ts +2 -14
  30. package/packages/core/src/extensions/read/read.ts +5 -6
  31. package/packages/core/src/extensions/read/render.ts +1 -1
  32. package/packages/core/src/extensions/read/schema.ts +2 -2
  33. package/packages/core/src/extensions/subagent/render.ts +2 -2
  34. package/packages/core/src/extensions/subagent/subagent.ts +10 -21
  35. package/packages/core/src/extensions/system-prompt/index.ts +3 -2
  36. package/packages/core/src/extensions/system-prompt/prompt.ts +13 -3
  37. package/packages/core/src/extensions/todo/render.ts +1 -4
  38. package/packages/core/src/extensions/todo/todo.ts +16 -23
  39. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +3 -11
  40. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +1 -3
  41. package/packages/core/src/extensions/web-fetch/fetch.ts +4 -12
  42. package/packages/core/src/extensions/web-fetch/image.ts +3 -4
  43. package/packages/core/src/extensions/web-fetch/index.ts +2 -13
  44. package/packages/core/src/extensions/web-fetch/render.ts +3 -4
  45. package/packages/core/src/extensions/web-fetch/schema.ts +3 -4
  46. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +23 -31
  47. package/packages/core/src/extensions/web-search/SearchBreaker.ts +1 -1
  48. package/packages/core/src/extensions/web-search/SearchChain.ts +6 -5
  49. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +2 -2
  50. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +1 -2
  51. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +3 -3
  52. package/packages/core/src/extensions/write/write.ts +1 -1
  53. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +1 -3
  54. package/packages/core/src/picker/PickerItem.ts +1 -1
  55. package/packages/core/src/picker/PickerService.ts +3 -7
  56. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +1 -2
  57. package/packages/core/src/picker/catalog.ts +7 -15
  58. package/packages/core/src/picker/filePickerWorker.ts +1 -3
  59. package/packages/core/src/picker/ranker.ts +1 -1
  60. package/packages/core/src/picker/token.ts +3 -4
  61. package/packages/core/src/session/AgentRuntime.ts +5 -8
  62. package/packages/core/src/session/EventLog.ts +6 -12
  63. package/packages/core/src/session/ReadCursors.ts +3 -4
  64. package/packages/core/src/session/SearchIndex.ts +49 -86
  65. package/packages/core/src/session/SearchTokens.ts +4 -7
  66. package/packages/core/src/session/SessionCache.ts +8 -6
  67. package/packages/core/src/session/SessionDigest.ts +10 -19
  68. package/packages/core/src/session/SessionHost.ts +53 -83
  69. package/packages/core/src/session/SessionLease.ts +12 -15
  70. package/packages/core/src/session/SessionMeta.ts +16 -32
  71. package/packages/core/src/session/SessionName.ts +3 -7
  72. package/packages/core/src/session/SessionRegistry.ts +13 -29
  73. package/packages/core/src/session/SessionUi.ts +12 -16
  74. package/packages/core/src/session/WriteMark.ts +7 -16
  75. package/packages/core/src/shared/Cli.ts +1 -1
  76. package/packages/core/src/shared/DaemonUnit.ts +1 -2
  77. package/packages/core/src/shared/DiffLines.ts +16 -34
  78. package/packages/core/src/shared/DiffRenderer.ts +8 -27
  79. package/packages/core/src/shared/Directories.ts +4 -4
  80. package/packages/core/src/shared/EditMatcher.ts +18 -50
  81. package/packages/core/src/shared/ExtensionToggles.ts +3 -6
  82. package/packages/core/src/shared/FileEnumerator.ts +73 -57
  83. package/packages/core/src/shared/FileScanner.ts +5 -13
  84. package/packages/core/src/shared/FileWatch.ts +13 -17
  85. package/packages/core/src/shared/Format.ts +2 -3
  86. package/packages/core/src/shared/FuzzyMatcher.ts +18 -19
  87. package/packages/core/src/shared/Git.ts +22 -47
  88. package/packages/core/src/shared/GitMonitor.ts +12 -23
  89. package/packages/core/src/shared/GlobExclusions.ts +0 -4
  90. package/packages/core/src/shared/Http.ts +1 -1
  91. package/packages/core/src/shared/ImageMime.ts +3 -5
  92. package/packages/core/src/shared/Images.ts +17 -27
  93. package/packages/core/src/shared/Languages.ts +2 -2
  94. package/packages/core/src/shared/Levenshtein.ts +1 -6
  95. package/packages/core/src/shared/Lines.ts +1 -1
  96. package/packages/core/src/shared/McpClient.ts +2 -2
  97. package/packages/core/src/shared/PatchSummary.ts +1 -1
  98. package/packages/core/src/shared/Paths.ts +4 -1
  99. package/packages/core/src/shared/PiExtensions.ts +5 -12
  100. package/packages/core/src/shared/PimSettings.ts +14 -15
  101. package/packages/core/src/shared/Proc.ts +2 -3
  102. package/packages/core/src/shared/RateLimiter.ts +1 -1
  103. package/packages/core/src/shared/Renderer.ts +1 -3
  104. package/packages/core/src/shared/RepoDiff.ts +24 -46
  105. package/packages/core/src/shared/SearchRender.ts +3 -4
  106. package/packages/core/src/shared/SpillCache.ts +2 -3
  107. package/packages/core/src/shared/SubagentLogs.ts +4 -6
  108. package/packages/core/src/shared/Supervisor.ts +19 -32
  109. package/packages/core/src/shared/Surface.ts +1 -1
  110. package/packages/core/src/shared/Sweeper.ts +1 -1
  111. package/packages/core/src/shared/Tools.ts +17 -30
  112. package/packages/core/src/shared/Updater.ts +15 -15
  113. package/packages/core/src/view/BodyRenderer.ts +1 -1
  114. package/packages/core/src/view/DiffBlocks.ts +0 -1
  115. package/packages/core/src/view/DiffExpand.ts +10 -25
  116. package/packages/core/src/view/DiffLayout.ts +2 -2
  117. package/packages/core/src/view/DiffPairs.ts +2 -2
  118. package/packages/core/src/view/MarkdownPainter.ts +2 -19
  119. package/packages/core/src/view/MovePath.ts +4 -17
  120. package/packages/core/src/view/Painting.ts +4 -4
  121. package/packages/core/src/view/ViewBlock.ts +11 -12
  122. package/packages/daemon/src/Daemon.ts +2 -7
  123. package/packages/daemon/src/DaemonInstall.ts +5 -12
  124. package/packages/daemon/src/Surfaces.ts +3 -12
  125. package/packages/daemon/src/TelegramSurface.ts +0 -1
  126. package/packages/daemon/src/index.ts +0 -1
  127. package/packages/protocol/src/Command.ts +33 -41
  128. package/packages/protocol/src/Diff.ts +0 -5
  129. package/packages/protocol/src/ImageRoute.ts +2 -5
  130. package/packages/protocol/src/ServerEvent.ts +68 -105
  131. package/packages/server/src/AttachmentEndpoint.ts +45 -19
  132. package/packages/server/src/ClientConnection.ts +11 -12
  133. package/packages/server/src/ImageEndpoint.ts +3 -4
  134. package/packages/server/src/ProbeClient.ts +26 -59
  135. package/packages/server/src/Reloader.ts +4 -7
  136. package/packages/server/src/SendFileTool.ts +1 -1
  137. package/packages/server/src/SessionCatalogue.ts +35 -47
  138. package/packages/server/src/SessionProjection.ts +7 -12
  139. package/packages/server/src/SessionStream.ts +33 -80
  140. package/packages/server/src/StaticClient.ts +7 -8
  141. package/packages/server/src/WebOptions.ts +2 -6
  142. package/packages/server/src/WsGateway.ts +35 -96
  143. package/packages/server/src/probe.ts +6 -18
  144. package/packages/telegram/src/Bot.ts +2 -2
  145. package/packages/telegram/src/Commands.ts +108 -65
  146. package/packages/telegram/src/Config.ts +1 -6
  147. package/packages/telegram/src/Markdown.ts +4 -13
  148. package/packages/telegram/src/Renderer.ts +1 -1
  149. package/packages/telegram/src/Session.ts +3 -10
  150. package/packages/telegram/src/TaskScheduler.ts +31 -10
  151. package/packages/telegram/src/TaskStore.ts +11 -1
  152. package/packages/telegram/src/TaskTool.ts +1 -1
  153. package/packages/telegram/src/UpdateConfirm.ts +2 -2
  154. package/packages/tui/src/extensions/_init/index.ts +2 -5
  155. package/packages/tui/src/extensions/file-picker/index.ts +2 -6
  156. package/packages/tui/src/extensions/footer/index.ts +0 -1
  157. package/packages/tui/src/extensions/footer/powerline.ts +16 -28
  158. package/packages/tui/src/extensions/pim/index.ts +1 -1
  159. package/packages/tui/src/extensions/session-lease/index.ts +5 -6
  160. package/packages/tui/src/extensions/working-indicator/index.ts +1 -2
  161. package/packages/tui/src/themes/themeCliArgs.ts +1 -1
  162. package/packages/web/dist/client/assets/abnfDiagram-O67JEVCF-WweOU5DK.js +1 -0
  163. package/packages/web/dist/client/assets/arc-O2PmZXWt.js +1 -0
  164. package/packages/web/dist/client/assets/architecture-7GRP2DOG-COBxVilU.js +1 -0
  165. package/packages/web/dist/client/assets/architectureDiagram-NJMV4G6O-DwYq1YaM.js +36 -0
  166. package/packages/web/dist/client/assets/array-BifhSqXX.js +1 -0
  167. package/packages/web/dist/client/assets/blockDiagram-BEXU5L5S-CVab041n.js +140 -0
  168. package/packages/web/dist/client/assets/c4Diagram-YGBWAQC7-BmotHez3.js +38 -0
  169. package/packages/web/dist/client/assets/channel-BKV-IcTA.js +1 -0
  170. package/packages/web/dist/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  171. package/packages/web/dist/client/assets/chunk-3FUC2YCW-CWNS7-n4.js +2 -0
  172. package/packages/web/dist/client/assets/chunk-5DYCD2WN-DflvXohu.js +1 -0
  173. package/packages/web/dist/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  174. package/packages/web/dist/client/assets/chunk-742MDFTN-B_0t6_RE.js +1 -0
  175. package/packages/web/dist/client/assets/chunk-7INBJB4K-wKj0Q3Kv.js +62 -0
  176. package/packages/web/dist/client/assets/chunk-7M6MHVWA-BMvUghc8.js +213 -0
  177. package/packages/web/dist/client/assets/chunk-7PRAP22T-BY41wkcT.js +1 -0
  178. package/packages/web/dist/client/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  179. package/packages/web/dist/client/assets/chunk-FOHPRMQF-CoA4358r.js +161 -0
  180. package/packages/web/dist/client/assets/chunk-GTNCS2PH-PqY_YoS0.js +88 -0
  181. package/packages/web/dist/client/assets/chunk-GWA4HPMP-fozSU5ty.js +1 -0
  182. package/packages/web/dist/client/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  183. package/packages/web/dist/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  184. package/packages/web/dist/client/assets/chunk-MBY4JIJT-CrHrC3jw.js +72 -0
  185. package/packages/web/dist/client/assets/chunk-NETBCI7D-DWdAxieG.js +1 -0
  186. package/packages/web/dist/client/assets/chunk-O7XYJQB3-B6uOn9iS.js +128 -0
  187. package/packages/web/dist/client/assets/chunk-UA2S7LBM-BxYSHqyy.js +1 -0
  188. package/packages/web/dist/client/assets/chunk-WEXAMYUT-CtwIaQDa.js +1 -0
  189. package/packages/web/dist/client/assets/chunk-XXDRQBXY-fgBFe-PX.js +1 -0
  190. package/packages/web/dist/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  191. package/packages/web/dist/client/assets/chunk-Z7XXMR3K-CPonoV3p.js +10 -0
  192. package/packages/web/dist/client/assets/chunk-ZIGJFQKS-Cdu87cTj.js +2 -0
  193. package/packages/web/dist/client/assets/classDiagram-v2-NBCMYWYE-DSWsPaa7.js +217 -0
  194. package/packages/web/dist/client/assets/core-CBoS7AST.js +3 -0
  195. package/packages/web/dist/client/assets/cose-bilkent-JH36ORCC-CTwlldtJ.js +1 -0
  196. package/packages/web/dist/client/assets/cynefin-OW5HDTMX-CZQ4EPNJ.js +1 -0
  197. package/packages/web/dist/client/assets/cynefinDiagram-VND7K2PF-C2gp6H_Q.js +62 -0
  198. package/packages/web/dist/client/assets/cytoscape.esm-B-1iW2ft.js +321 -0
  199. package/packages/web/dist/client/assets/dagre-6A5THRUB-CDxTP47t.js +4 -0
  200. package/packages/web/dist/client/assets/defaultLocale-BFoDCU3G.js +1 -0
  201. package/packages/web/dist/client/assets/diagram-22UHCM2B-BIohE1lI.js +200 -0
  202. package/packages/web/dist/client/assets/diagram-3UASUU5V-TwAUir-w.js +24 -0
  203. package/packages/web/dist/client/assets/diagram-ATOU4E4O-BM1JQrIu.js +3 -0
  204. package/packages/web/dist/client/assets/diagram-CDSNMT55-BKIqhxSI.js +30 -0
  205. package/packages/web/dist/client/assets/diagram-MLGK6HIB-CVe3QDHr.js +24 -0
  206. package/packages/web/dist/client/assets/diagram-MPIPVDR6-Bj-MQIFI.js +41 -0
  207. package/packages/web/dist/client/assets/dist-DlZpcyc2.js +1 -0
  208. package/packages/web/dist/client/assets/ebnfDiagram-ZINNZB2B-C06mmvmz.js +1 -0
  209. package/packages/web/dist/client/assets/elk-276RUBZZ-CizJSNEj.js +27 -0
  210. package/packages/web/dist/client/assets/erDiagram-OPXOYQCR-M3FQkKI1.js +99 -0
  211. package/packages/web/dist/client/assets/eventmodeling-NTZA5JFV-D_UXLpIu.js +1 -0
  212. package/packages/web/dist/client/assets/flowDiagram-KWPJA3E3-BT4eEEVm.js +1 -0
  213. package/packages/web/dist/client/assets/ganttDiagram-FUAMR5RP-CojF9Q16.js +292 -0
  214. package/packages/web/dist/client/assets/gitGraph-4MIJSDKK-YvKqGEPk.js +1 -0
  215. package/packages/web/dist/client/assets/gitGraphDiagram-X574FWY7-XqNqIFo9.js +106 -0
  216. package/packages/web/dist/client/assets/graphlib-DS17s2tU.js +1 -0
  217. package/packages/web/dist/client/assets/index--2CDtzvY.css +1 -0
  218. package/packages/web/dist/client/assets/index-bT6QPJZs.js +47 -0
  219. package/packages/web/dist/client/assets/info-A6RAGUB7-B_1BCzM2.js +1 -0
  220. package/packages/web/dist/client/assets/infoDiagram-VRGFBTTK-zFX8B-dP.js +2 -0
  221. package/packages/web/dist/client/assets/init-C-OQMol4.js +1 -0
  222. package/packages/web/dist/client/assets/ishikawaDiagram-OU5B5YK6-DLzQ_Uyf.js +70 -0
  223. package/packages/web/dist/client/assets/journeyDiagram-ZHPQQLJL-S3WRMmoa.js +139 -0
  224. package/packages/web/dist/client/assets/kanban-definition-PNTS6WVX-B5Z8I-kM.js +89 -0
  225. package/packages/web/dist/client/assets/katex-ZlcWpGUi.js +257 -0
  226. package/packages/web/dist/client/assets/line-BGMH-i2X.js +1 -0
  227. package/packages/web/dist/client/assets/linear-KsrestXG.js +1 -0
  228. package/packages/web/dist/client/assets/mermaid-parser.core-DNffyJJA.js +7 -0
  229. package/packages/web/dist/client/assets/mermaid.core-Cz3vJeQG.js +44 -0
  230. package/packages/web/dist/client/assets/mindmap-definition-NLK3R4M7-CzA61XUf.js +96 -0
  231. package/packages/web/dist/client/assets/ordinal-BDEzSJ7C.js +1 -0
  232. package/packages/web/dist/client/assets/packet-AYTQ26CC-BO55e7cR.js +1 -0
  233. package/packages/web/dist/client/assets/path-fybaL0A-.js +1 -0
  234. package/packages/web/dist/client/assets/pegDiagram-GJSIUBJH-DeRiXYNd.js +1 -0
  235. package/packages/web/dist/client/assets/pie-WAS4IAKB-D9ibrQRe.js +1 -0
  236. package/packages/web/dist/client/assets/pieDiagram-5QR66LMP-BaBpPP33.js +39 -0
  237. package/packages/web/dist/client/assets/quadrantDiagram-O4NWA36T-DIfn4GjM.js +7 -0
  238. package/packages/web/dist/client/assets/radar-RG4KPBEZ-Dmn5-nJq.js +1 -0
  239. package/packages/web/dist/client/assets/railroad-74A4TZTK-ZCWNEd2Z.js +1 -0
  240. package/packages/web/dist/client/assets/railroad-abnf-HS5TGJTU-_swqhy0y.js +1 -0
  241. package/packages/web/dist/client/assets/railroad-ebnf-LZEXJU2U-CytbazPx.js +1 -0
  242. package/packages/web/dist/client/assets/railroad-peg-WCYAUIDC-Bg8QW17T.js +1 -0
  243. package/packages/web/dist/client/assets/railroadDiagram-XR7U4H2S-CDPZ39sy.js +1 -0
  244. package/packages/web/dist/client/assets/requirementDiagram-PLB6GJNP-CyCI8Yul.js +84 -0
  245. package/packages/web/dist/client/assets/rough.esm-Dy-Kn_BL.js +1 -0
  246. package/packages/web/dist/client/assets/sankeyDiagram-IPEJSGJF-p_XAPv72.js +40 -0
  247. package/packages/web/dist/client/assets/sequenceDiagram-PO4LG4MO-DmR6IP_o.js +169 -0
  248. package/packages/web/dist/client/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  249. package/packages/web/dist/client/assets/src-gnv7pJ1z.js +1 -0
  250. package/packages/web/dist/client/assets/stateDiagram-v2-GCMORJYK-7ZTHl6ih.js +291 -0
  251. package/packages/web/dist/client/assets/swimlanes-2SLR337P-N6bj71pw.js +1 -0
  252. package/packages/web/dist/client/assets/swimlanesDiagram-TC7HE7FX-BJQ2CBsp.js +8 -0
  253. package/packages/web/dist/client/assets/timeline-definition-EJHVYXUP-GNez0kW8.js +120 -0
  254. package/packages/web/dist/client/assets/treeView-Q6P3EWNA-DyQZkboR.js +1 -0
  255. package/packages/web/dist/client/assets/treemap-WGGIJYW6-_J8Pw2TM.js +1 -0
  256. package/packages/web/dist/client/assets/usecaseDiagram-POWQR4AR-Bg-CzKjl.js +328 -0
  257. package/packages/web/dist/client/assets/vennDiagram-UO4OBE2U-DBQKHXhK.js +34 -0
  258. package/packages/web/dist/client/assets/wardley-WFR3VGLG-B5aAsQ3F.js +1 -0
  259. package/packages/web/dist/client/assets/wardleyDiagram-VNRHLVJA-BKUQ1LK6.js +78 -0
  260. package/packages/web/dist/client/assets/xychartDiagram-PMCCYNJV-BI2sVbks.js +7 -0
  261. package/packages/web/dist/client/index.html +2 -2
  262. package/packages/web/dist/client/assets/core-C5fJJN75.js +0 -3
  263. package/packages/web/dist/client/assets/index-C5D7B_8d.js +0 -45
  264. package/packages/web/dist/client/assets/index-OVLzpES-.css +0 -1
@@ -4,16 +4,10 @@ import type { ImageContent } from "@earendil-works/pi-ai";
4
4
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
5
5
 
6
6
  import { Attachments } from "#core/attachments/Attachments";
7
- import type { PickerItem } from "#core/picker/PickerItem";
8
7
  import { Directories } from "#core/shared/Directories";
9
- import type { DirectoryListing } from "#core/shared/Directories";
10
- import { Git, type GitBranch } from "#core/shared/Git";
8
+ import { Git } from "#core/shared/Git";
11
9
  import { GitMonitor, type GitRun } from "#core/shared/GitMonitor";
12
- import {
13
- PiExtensions,
14
- type ExtensionEntry,
15
- type ExtensionScope,
16
- } from "#core/shared/PiExtensions";
10
+ import { PiExtensions, type ExtensionScope } from "#core/shared/PiExtensions";
17
11
  import { RepoDiff } from "#core/shared/RepoDiff";
18
12
  import { ReadCursors } from "#core/session/ReadCursors";
19
13
  import type { SessionHost } from "#core/session/SessionHost";
@@ -23,14 +17,7 @@ import { PimVersion } from "#core/shared/PimVersion";
23
17
  import { SubagentLogs } from "#core/shared/SubagentLogs";
24
18
  import type { UpdateOutcome } from "#core/shared/Updater";
25
19
  import type { Command } from "#protocol/Command";
26
- import type { ChangeList, FileDiff, FileLines } from "#protocol/Diff";
27
- import type {
28
- ModelView,
29
- ProjectView,
30
- SearchHitView,
31
- ServerEvent,
32
- SessionSummaryView,
33
- } from "#protocol/ServerEvent";
20
+ import type { ResponseEvent, ServerEvent } from "#protocol/ServerEvent";
34
21
  import { ClientConnection } from "./ClientConnection";
35
22
  import { Reloader } from "./Reloader";
36
23
  import { SessionCatalogue } from "./SessionCatalogue";
@@ -46,48 +33,28 @@ export type WsGatewayDeps = {
46
33
  readonly hostname?: string;
47
34
  /** 0 asks the OS for a free port; read it back from `port`. */
48
35
  readonly port?: number;
49
- /** Where uploaded bytes are kept; defaults to `~/.pim/attachments`. */
36
+ /** Defaults to `~/.pim/attachments`. */
50
37
  readonly attachmentsRoot?: string;
51
- /** Where `read` spilled the pictures it showed the model; defaults to `~/.pim/cache`. */
38
+ /** Images cached by `read`. Defaults to `~/.pim/cache`. */
52
39
  readonly imagesRoot?: string;
53
- /** Where the read cursors live; defaults to `~/.pim/read.json`. */
40
+ /** Defaults to `~/.pim/read.json`. */
54
41
  readonly readCursorsPath?: string;
55
- /** Where the per-session and per-project overrides live; defaults to `~/.pim/sessions.json`. */
42
+ /** Defaults to `~/.pim/sessions.json`. */
56
43
  readonly sessionMetaPath?: string;
57
- /** How often a session file is polled where `fs.watch` says nothing; the default is a second. */
44
+ /** Session file poll interval. Defaults to 1s. */
58
45
  readonly pollMs?: number;
59
- /** Longest a dialog may hold an extension waiting; the default is `SessionStream`'s three minutes. */
46
+ /** Defaults to 3 minutes. */
60
47
  readonly requestCeilingMs?: number;
61
- /** How long a pending dialog outlives its last reader; the default is `SessionStream`'s fifteen seconds. */
48
+ /** Defaults to 15s. */
62
49
  readonly detachGraceMs?: number;
63
- /** The built web client; defaults to the bundle shipped beside this package. */
50
+ /** Defaults to the bundled web client. */
64
51
  readonly clientDir?: string;
65
- /** Runs the update a `reload` asks for, reporting each step as it starts. */
66
52
  readonly update?: (onStep: (label: string) => void) => Promise<UpdateOutcome>;
67
- /** Ends this process so the supervisor replaces it with the updated code. */
68
53
  readonly shutdown?: () => Promise<void>;
69
54
  };
70
55
 
71
- type Outcome = {
72
- readonly error?: string;
73
- readonly items?: readonly PickerItem[];
74
- readonly sessions?: readonly SessionSummaryView[];
75
- readonly projects?: readonly ProjectView[];
76
- readonly hits?: readonly SearchHitView[];
77
- readonly dropped?: readonly string[];
78
- readonly scanned?: number;
79
- readonly models?: readonly ModelView[];
80
- readonly thinkingLevels?: readonly string[];
81
- readonly extensions?: readonly ExtensionEntry[];
82
- readonly directory?: DirectoryListing;
83
- readonly branches?: readonly GitBranch[];
84
- readonly commit?: { readonly sha: string };
85
- readonly changes?: ChangeList;
86
- readonly fileDiff?: FileDiff;
87
- readonly fileLines?: FileLines;
88
- readonly restored?: readonly string[];
89
- /** The message was dispatched as an extension command: no turn started, no entry written. */
90
- readonly dispatched?: boolean;
56
+ type Outcome = Omit<ResponseEvent, "type" | "id" | "success"> & {
57
+ /** Runs after the response is sent. */
91
58
  readonly after?: () => void;
92
59
  };
93
60
 
@@ -101,19 +68,16 @@ export const DEFAULT_PORT = 4319;
101
68
 
102
69
  export const DEFAULT_HOSTNAME = "127.0.0.1";
103
70
 
104
- // Bun 1.3.14 never frees the slot of a server-closed socket, so an unbounded `Server.stop(true)` hangs.
71
+ // Bun 1.3.14 can hang in `Server.stop(true)` after closing sockets server-side.
105
72
  const STOP_GRACE_MS = 250;
106
73
 
107
- /**
108
- * Pi's own dispatch parse, three lines of `_tryExecuteExtensionCommand`
109
- * (`agent-session.js:954`); a pi upgrade re-verifies it.
110
- */
74
+ /** Mirrors pi's `_tryExecuteExtensionCommand` parse (`agent-session.js:954`). */
111
75
  function commandNameOf(text: string): string {
112
76
  const spaceIndex = text.indexOf(" ");
113
77
  return spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
114
78
  }
115
79
 
116
- /** The transport half of pim-server: a WebSocket endpoint over the session runtime in `core`. */
80
+ /** WebSocket + HTTP endpoint over the session runtime in `core`. */
117
81
  export class WsGateway {
118
82
  private readonly registry: SessionRegistry;
119
83
  private readonly hostname: string;
@@ -140,8 +104,6 @@ export class WsGateway {
140
104
 
141
105
  public constructor(deps: WsGatewayDeps) {
142
106
  this.registry = deps.registry;
143
- // `SessionStream` is the sink, so the whole routing is a lookup: a host
144
- // outlives the stream that speaks for it, and pi asks per call.
145
107
  this.registry.setUi((host) =>
146
108
  host.sessionId === undefined
147
109
  ? undefined
@@ -213,13 +175,13 @@ export class WsGateway {
213
175
  if (ImageEndpoint.owns(pathname)) {
214
176
  return this.images.handle(req);
215
177
  }
216
- // Try the upgrade first: the socket must answer on every path.
178
+ // The socket must answer on every path.
217
179
  return server.upgrade(req) ? undefined : this.client.handle(req);
218
180
  },
219
181
  websocket: {
220
182
  backpressureLimit: 8 << 20,
221
183
  closeOnBackpressureLimit: false,
222
- // Dedicated, so the window survives between frames: the repeated event envelope is most of a delta.
184
+ // Dedicated keeps the window between frames, which shrinks the repeated envelopes.
223
185
  perMessageDeflate: { compress: "dedicated", decompress: "dedicated" },
224
186
  open: (ws) => {
225
187
  this.connections.set(ws, new ClientConnection(ws));
@@ -324,11 +286,7 @@ export class WsGateway {
324
286
  return await this.catalogue.list(command);
325
287
  case "search_sessions":
326
288
  return await this.catalogue.search(command);
327
- // The four below take a session this server may never have opened: a row
328
- // is archived or renamed from the sidebar without being attached to, so
329
- // none of them may reach for a stream. A sidecar write moves no session
330
- // file and produces no `sessions_changed`, so the broadcast each ends
331
- // with is the only word a listing already in a client's hands gets.
289
+ // These may target a session this server never opened, so they must not need a stream.
332
290
  case "set_session_name": {
333
291
  const name = await this.registry.setName(
334
292
  command.sessionId,
@@ -364,8 +322,6 @@ export class WsGateway {
364
322
  cwd: command.cwd,
365
323
  pinned: command.value,
366
324
  });
367
- // The flag and the place it takes are one fact to a sidebar; a pin
368
- // that arrived without a rank would sort by nothing until the next listing.
369
325
  this.broadcast({ type: "pins_changed", order: await this.meta.pins() });
370
326
  return {};
371
327
  case "set_project_expanded":
@@ -425,7 +381,7 @@ export class WsGateway {
425
381
  this.requireStream(connection).host.cwd
426
382
  ),
427
383
  };
428
- // Reading the repository never moves it, so neither of these takes the `repoBusy` refusal.
384
+ // Read-only, so allowed while `repoBusy`.
429
385
  case "list_changes":
430
386
  return {
431
387
  changes: await RepoDiff.listChanges(
@@ -550,7 +506,7 @@ export class WsGateway {
550
506
  return sessionId ? this.streams.get(sessionId) : undefined;
551
507
  }
552
508
 
553
- /** A session's directory decides which project extensions are listed; a connection without one reads this process's. */
509
+ /** Uses the process cwd when the connection is not attached. */
554
510
  private scopeFor(connection: ClientConnection): ExtensionScope {
555
511
  return {
556
512
  cwd: this.streamFor(connection)?.host.cwd ?? process.cwd(),
@@ -561,7 +517,7 @@ export class WsGateway {
561
517
  private rebuildAgents(): void {
562
518
  for (const stream of this.streams.values()) {
563
519
  const { host } = stream;
564
- // Through the turn queue: detaching the agent under a running turn kills it.
520
+ // Queued behind any running turn, which detaching the agent would kill.
565
521
  void host
566
522
  .serialize(() => host.invalidate())
567
523
  .catch((err: unknown) => {
@@ -579,7 +535,7 @@ export class WsGateway {
579
535
  return stream;
580
536
  }
581
537
 
582
- // Derive the log path from the parent session and call id: a client-sent path reads arbitrary JSONL off this machine.
538
+ // The log path is derived server-side; never accept a client-sent path.
583
539
  private async watchSubagent(
584
540
  connection: ClientConnection,
585
541
  stream: SessionStream,
@@ -594,7 +550,7 @@ export class WsGateway {
594
550
  if (path === null) {
595
551
  return { error: `malformed call id: ${command.callId}` };
596
552
  }
597
- // A running call may not have written its log yet; the watch waits for it.
553
+ // A running call may not have written its log yet.
598
554
  if (!stream.isRunning(command.callId) && !(await Bun.file(path).exists())) {
599
555
  return { error: `no subagent log for call ${command.callId}` };
600
556
  }
@@ -641,19 +597,16 @@ export class WsGateway {
641
597
  return {};
642
598
  }
643
599
 
644
- /** A watch costs a poll, so only attached sessions get one — and the catalogue only while someone is connected. */
600
+ /** Watches only attached sessions, and the catalogue only while anyone is connected. */
645
601
  private syncWatches(): void {
646
602
  for (const [sessionId, stream] of this.streams) {
647
- // Attachment, not attention: a hidden tab that lost its watches would stop hearing about the session.
603
+ // Attached, not attentive: hidden tabs still need updates.
648
604
  stream.watchFiles(this.isAttached(sessionId));
649
605
  }
650
606
  this.catalogue.watch(this.connections.size > 0);
651
607
  }
652
608
 
653
- /**
654
- * An operation that moves the working tree cannot run under an agent that
655
- * may be halfway through an edit; `push` leaves the tree alone, so it can.
656
- */
609
+ /** `movesTree` operations are refused while an agent in the cwd is mid-turn. */
657
610
  private async runGit<T = never>(
658
611
  stream: SessionStream,
659
612
  movesTree: boolean,
@@ -701,7 +654,7 @@ export class WsGateway {
701
654
  return false;
702
655
  }
703
656
 
704
- /** A turn starting anywhere freezes the branch menu of every session beside it, which only their own state can say. */
657
+ /** Pushes fresh state to the other sessions in `cwd` when its busy flag flips. */
705
658
  private syncRepoBusy(cwd: string, source: string): void {
706
659
  const busy = this.repoBusy(cwd);
707
660
  if (this.busyRepos.has(cwd) === busy) {
@@ -747,7 +700,6 @@ export class WsGateway {
747
700
  .some((connection) => connection.sessionId === sessionId);
748
701
  }
749
702
 
750
- // The TUI never attaches, so reading a session there clears no dot here and a turn it runs trips no mark.
751
703
  private isBeingRead(sessionId: string): boolean {
752
704
  return this.connections
753
705
  .values()
@@ -796,7 +748,7 @@ export class WsGateway {
796
748
  ...(target.cwd === undefined ? {} : { cwd: target.cwd }),
797
749
  ...(target.like === undefined ? {} : { like: target.like }),
798
750
  });
799
- // Pi assigns the id and the session file only once an agent exists.
751
+ // Pi assigns the id and session file only once an agent exists.
800
752
  const agent = host.agentSession ?? (await host.ensureAgent());
801
753
  const id = agent.sessionId;
802
754
  const path = agent.sessionFile ?? host.settings.sessionPath;
@@ -816,8 +768,7 @@ export class WsGateway {
816
768
  });
817
769
  stream.start();
818
770
  this.catalogue.track(id, host.status);
819
- // Observed rather than subscribed: this never leaves, and a reader that
820
- // never leaves is a session that is never detached.
771
+ // Observe, not subscribe, so this doesn't count as an attached client.
821
772
  stream.observe((event) => {
822
773
  if (event.type === "session_state") {
823
774
  this.catalogue.onStatus(id, event.status);
@@ -832,18 +783,14 @@ export class WsGateway {
832
783
  stream: SessionStream,
833
784
  command: Command & { readonly type: "user_message" }
834
785
  ): Promise<Outcome> {
835
- // Trimmed once, here: pi dispatches on the text it is handed after the
836
- // fallthrough trims it, so a client that does not trim would otherwise
837
- // slip a command past this check and into pi's own.
786
+ // Pi trims before dispatching, so trim here too or a padded command slips past this check.
838
787
  const text = command.text.trim();
839
- // Read before anything is rendered: what a command is, is its own text.
840
788
  const dispatched = await this.dispatchCommand(stream, text);
841
789
  const taken = this.uploads.take(
842
790
  stream.sessionId,
843
791
  (command.attachments ?? []).map((ref) => ref.id)
844
792
  );
845
- // A slash command carries no pictures; the uploads are still taken, so the
846
- // endpoint stops holding their bytes for the rest of the session.
793
+ // Uploads are taken even for commands, so the endpoint releases them.
847
794
  if (dispatched) {
848
795
  return { dispatched: true };
849
796
  }
@@ -852,11 +799,7 @@ export class WsGateway {
852
799
  return {};
853
800
  }
854
801
 
855
- /**
856
- * Whether `text` names an extension command, having started it if it does.
857
- * Only the decision is awaited: the handler runs on beyond the ack, exactly
858
- * as a turn does.
859
- */
802
+ /** Starts `text` if it is an extension command; only the decision is awaited. */
860
803
  private async dispatchCommand(
861
804
  stream: SessionStream,
862
805
  text: string
@@ -865,8 +808,6 @@ export class WsGateway {
865
808
  if (!text.startsWith("/")) {
866
809
  return false;
867
810
  }
868
- // The picker offers extension commands only once an agent exists, so this
869
- // is someone typing from memory; cold is idle, so nothing is queued behind it.
870
811
  const agent = host.agentSession ?? (await host.ensureAgent());
871
812
  const name = commandNameOf(text);
872
813
  if (agent.extensionRunner.getCommand(name) === undefined) {
@@ -874,9 +815,7 @@ export class WsGateway {
874
815
  }
875
816
  void stream
876
817
  .dispatch(`/${name}`, async () => {
877
- // Prompted whole, never merged into the queue: a merged string starts
878
- // with the earlier message, and pi reads the leading `/` before it
879
- // looks at `isStreaming` — so the turn we are holding dispatches it.
818
+ // Never merged into the queue, or the leading `/` would be lost.
880
819
  if (host.isStreaming) {
881
820
  await agent.prompt(text, {
882
821
  streamingBehavior: "steer",
@@ -894,7 +833,7 @@ export class WsGateway {
894
833
  return true;
895
834
  }
896
835
 
897
- // Merge into the queued message: pi holds at most one, so a second queued send would be unreachable.
836
+ // Merge with the queued message: pi holds at most one.
898
837
  private prompt(
899
838
  stream: SessionStream,
900
839
  text: string,
@@ -160,25 +160,16 @@ for (const path of values.upload ?? []) {
160
160
  process.stderr.write(`uploaded: ${JSON.stringify(file)}\n`);
161
161
  }
162
162
 
163
- if (values.cancel) {
164
- const response = await probe.send({
165
- type: "cancel",
166
- sessionId: probe.sessionId ?? "",
167
- });
168
- process.stderr.write(`cancel: ${JSON.stringify(response)}\n`);
169
- }
170
- if (values.dequeue) {
171
- const response = await probe.send({
172
- type: "dequeue",
173
- sessionId: probe.sessionId ?? "",
174
- });
175
- process.stderr.write(`dequeue: ${JSON.stringify(response)}\n`);
176
- }
177
-
178
163
  const sessionId = probe.sessionId ?? "";
179
164
  const report = (what: string, response: ResponseEvent): void => {
180
165
  process.stderr.write(`${what}: ${JSON.stringify(response)}\n`);
181
166
  };
167
+ if (values.cancel) {
168
+ report("cancel", await probe.send({ type: "cancel", sessionId }));
169
+ }
170
+ if (values.dequeue) {
171
+ report("dequeue", await probe.send({ type: "dequeue", sessionId }));
172
+ }
182
173
  if (values.rename !== undefined) {
183
174
  report("rename", await probe.rename(sessionId, values.rename || null));
184
175
  }
@@ -200,9 +191,6 @@ if (values.unpin !== undefined) {
200
191
 
201
192
  if (values.prompt !== undefined) {
202
193
  await probe.promptWith(values.prompt, uploaded);
203
- }
204
-
205
- if (values.prompt !== undefined) {
206
194
  await probe.waitFor(
207
195
  (event) => event.type === "session_state" && event.status === "idle",
208
196
  { timeoutMs: 10 * 60_000, from: mark }
@@ -20,7 +20,7 @@ export class Bot {
20
20
  private readonly scheduler: TaskScheduler;
21
21
  private readonly config: TelegramConfig;
22
22
  private readonly commands: Commands;
23
- /** Grammy's long poll, which only settles once the bot is stopped. */
23
+ /** Settles only once the bot is stopped. */
24
24
  private polling: Promise<void> | undefined;
25
25
 
26
26
  public constructor(config: TelegramConfig, runtime: AgentRuntime) {
@@ -106,7 +106,7 @@ export class Bot {
106
106
  this.registry.setBotUsername(username);
107
107
  console.log(`bot @${username} ready`);
108
108
  await this.scheduler.start();
109
- // Polling outlives this call: awaiting it would hold every other surface down with it.
109
+ // Not awaited: it resolves only when polling stops.
110
110
  this.polling = this.grammy.start().catch((err: unknown) => {
111
111
  console.error("[bot] polling stopped:", err);
112
112
  });
@@ -34,6 +34,12 @@ const CB_EFFORT = "effort";
34
34
  const CB_LOGS = "logs";
35
35
  const CB_MODEL = "model";
36
36
  const CB_TEMPORARY = "temporary";
37
+ const MODEL_PICKS_CAP = 100;
38
+
39
+ type ModelPick = {
40
+ readonly sessionId: SessionId;
41
+ readonly candidates: readonly string[];
42
+ };
37
43
 
38
44
  type BotCommand = { readonly command: string; readonly description: string };
39
45
 
@@ -175,6 +181,7 @@ export class Commands {
175
181
  private readonly config: TelegramConfig;
176
182
  private readonly api: Api;
177
183
  private readonly registry: SessionRegistry;
184
+ private readonly modelPicks: Map<string, ModelPick>;
178
185
 
179
186
  private static readonly COMMANDS: readonly CommandSpec[] = [
180
187
  {
@@ -261,6 +268,7 @@ export class Commands {
261
268
  this.config = config;
262
269
  this.api = api;
263
270
  this.registry = registry;
271
+ this.modelPicks = new Map();
264
272
  }
265
273
 
266
274
  public async handleCommand(
@@ -295,34 +303,8 @@ export class Commands {
295
303
  ctx: Filter<Context, "callback_query:data">
296
304
  ): Promise<void> {
297
305
  const data = ctx.callbackQuery.data;
298
-
299
306
  if (data.startsWith(`${CB_MODEL}|`)) {
300
- const idx1 = data.indexOf("|");
301
- const idx2 = data.lastIndexOf("|");
302
- const modelId = data.slice(idx1 + 1, idx2);
303
- const keyPart = data.slice(idx2 + 1);
304
- const session = this.registry.get(decodeId(keyPart));
305
- await ctx.answerCallbackQuery({ text: `Model: ${modelId}` });
306
- try {
307
- const result = await session.setModel(modelId);
308
- if (result.ok) {
309
- await safeEditMessage(
310
- ctx,
311
- `<b>Model</b> → <code>${Markdown.escape(result.id)}</code>`
312
- );
313
- } else {
314
- await safeEditMessage(
315
- ctx,
316
- strikeOriginal(ctx, `⚠️ model set failed: ${modelId}`)
317
- );
318
- }
319
- } catch (err) {
320
- console.error(`[bot] model callback failed for ${modelId}:`, err);
321
- await safeEditMessage(
322
- ctx,
323
- strikeOriginal(ctx, `⚠️ model set failed: ${(err as Error).message}`)
324
- );
325
- }
307
+ await this.onModelPicked(ctx, data);
326
308
  return;
327
309
  }
328
310
 
@@ -331,24 +313,7 @@ export class Commands {
331
313
  const keyPart = colon >= 0 ? data.slice(colon + 1) : "";
332
314
 
333
315
  if (action === CB_CLEAR_CONFIRM && keyPart) {
334
- const session = this.registry.get(decodeId(keyPart));
335
- const wasBusy = session.isStreaming;
336
- await ctx.answerCallbackQuery({
337
- text: wasBusy ? "Queued — clearing after current turn" : "Cleared",
338
- });
339
- try {
340
- await session.clear();
341
- await safeEditMessage(
342
- ctx,
343
- strikeOriginal(ctx, "Context window cleared.")
344
- );
345
- } catch (err) {
346
- console.error(`[bot] queued clear failed:`, err);
347
- await safeEditMessage(
348
- ctx,
349
- strikeOriginal(ctx, `⚠️ clear failed: ${(err as Error).message}`)
350
- );
351
- }
316
+ await this.onClearConfirmed(ctx, keyPart);
352
317
  return;
353
318
  }
354
319
  if (action === CB_CLEAR_CANCEL) {
@@ -358,24 +323,87 @@ export class Commands {
358
323
  }
359
324
  const picker = PICKERS.find((one) => one.action === action);
360
325
  if (picker && keyPart) {
361
- const parts = splitValueAndKey(keyPart);
362
- const applyTo = parts && picker.apply(parts.value);
363
- if (!parts || !applyTo) {
364
- await ctx.answerCallbackQuery();
365
- return;
366
- }
367
- const session = this.registry.get(decodeId(parts.key));
368
- await applyTo(session);
369
- await ctx.answerCallbackQuery({
370
- text: `${picker.title}: ${picker.label(parts.value)}`,
371
- });
372
- const { kb, html } = buildPicker(picker, session, parts.value);
373
- await safeEditMessage(ctx, html, kb);
326
+ await this.onPicked(ctx, picker, keyPart);
374
327
  return;
375
328
  }
376
329
  await ctx.answerCallbackQuery();
377
330
  }
378
331
 
332
+ private async onModelPicked(
333
+ ctx: Filter<Context, "callback_query:data">,
334
+ data: string
335
+ ): Promise<void> {
336
+ const [, token = "", index = ""] = data.split("|");
337
+ const pick = this.modelPicks.get(token);
338
+ const modelId = pick?.candidates[Number(index)];
339
+ if (!pick || modelId === undefined) {
340
+ await ctx.answerCallbackQuery({ text: "Expired — run /model again" });
341
+ return;
342
+ }
343
+ const session = this.registry.get(pick.sessionId);
344
+ await ctx.answerCallbackQuery({ text: `Model: ${modelId}` });
345
+ try {
346
+ const result = await session.setModel(modelId);
347
+ await safeEditMessage(
348
+ ctx,
349
+ result.ok
350
+ ? `<b>Model</b> → <code>${Markdown.escape(result.id)}</code>`
351
+ : strikeOriginal(ctx, `⚠️ model set failed: ${modelId}`)
352
+ );
353
+ } catch (err) {
354
+ console.error(`[bot] model callback failed for ${modelId}:`, err);
355
+ await safeEditMessage(
356
+ ctx,
357
+ strikeOriginal(ctx, `⚠️ model set failed: ${(err as Error).message}`)
358
+ );
359
+ }
360
+ }
361
+
362
+ private async onClearConfirmed(
363
+ ctx: Filter<Context, "callback_query:data">,
364
+ keyPart: string
365
+ ): Promise<void> {
366
+ const session = this.registry.get(decodeId(keyPart));
367
+ await ctx.answerCallbackQuery({
368
+ text: session.isStreaming
369
+ ? "Queued — clearing after current turn"
370
+ : "Cleared",
371
+ });
372
+ try {
373
+ await session.clear();
374
+ await safeEditMessage(
375
+ ctx,
376
+ strikeOriginal(ctx, "Context window cleared.")
377
+ );
378
+ } catch (err) {
379
+ console.error(`[bot] queued clear failed:`, err);
380
+ await safeEditMessage(
381
+ ctx,
382
+ strikeOriginal(ctx, `⚠️ clear failed: ${(err as Error).message}`)
383
+ );
384
+ }
385
+ }
386
+
387
+ private async onPicked(
388
+ ctx: Filter<Context, "callback_query:data">,
389
+ picker: Picker,
390
+ keyPart: string
391
+ ): Promise<void> {
392
+ const parts = splitValueAndKey(keyPart);
393
+ const applyTo = parts && picker.apply(parts.value);
394
+ if (!parts || !applyTo) {
395
+ await ctx.answerCallbackQuery();
396
+ return;
397
+ }
398
+ const session = this.registry.get(decodeId(parts.key));
399
+ await applyTo(session);
400
+ await ctx.answerCallbackQuery({
401
+ text: `${picker.title}: ${picker.label(parts.value)}`,
402
+ });
403
+ const { kb, html } = buildPicker(picker, session, parts.value);
404
+ await safeEditMessage(ctx, html, kb);
405
+ }
406
+
379
407
  private async runQueued(
380
408
  ctx: Filter<Context, "message">,
381
409
  session: Session,
@@ -497,11 +525,14 @@ export class Commands {
497
525
  private async cmdModelWrite(session: Session, args: string): Promise<void> {
498
526
  const result = await session.setModel(args);
499
527
  if (!result.ok) {
500
- const key = encodeId(session.id);
528
+ const token = this.rememberModelPick({
529
+ sessionId: session.id,
530
+ candidates: result.candidates,
531
+ });
501
532
  const kb = new InlineKeyboard();
502
- for (const c of result.candidates) {
503
- kb.text(c, `${CB_MODEL}|${c}|${key}`).row();
504
- }
533
+ result.candidates.forEach((c, i) => {
534
+ kb.text(c, `${CB_MODEL}|${token}|${i}`).row();
535
+ });
505
536
  const header =
506
537
  result.kind === "ambiguous"
507
538
  ? `⚠️ Multiple matches for "${Markdown.escape(args)}". Please choose one below or use /model with a more specific name.`
@@ -515,6 +546,18 @@ export class Commands {
515
546
  );
516
547
  }
517
548
 
549
+ private rememberModelPick(pick: ModelPick): string {
550
+ const token = Math.random().toString(36).slice(2, 10);
551
+ this.modelPicks.set(token, pick);
552
+ if (this.modelPicks.size > MODEL_PICKS_CAP) {
553
+ const oldest = this.modelPicks.keys().next().value;
554
+ if (oldest !== undefined) {
555
+ this.modelPicks.delete(oldest);
556
+ }
557
+ }
558
+ return token;
559
+ }
560
+
518
561
  private async cmdEffort(session: Session): Promise<void> {
519
562
  const supported = session.supportedThinkingLevels;
520
563
  if (supported.length <= 1) {
@@ -583,7 +626,7 @@ export class Commands {
583
626
  const notes = outcome.skipped.map(
584
627
  (s) => `\nSkipped ${s.label}: ${s.reason}.`
585
628
  );
586
- // Restart siblings first; this daemon restarts by exiting, so it must go last.
629
+ // This process restarts by exiting, so restart siblings first.
587
630
  await Supervisor.restartSiblings(DaemonUnit);
588
631
  if (!Supervisor.isSupervised()) {
589
632
  await progress(
@@ -738,7 +781,7 @@ function strikeOriginal(
738
781
  note: string
739
782
  ): string {
740
783
  const original = ctx.callbackQuery.message?.text ?? "";
741
- return `<s>${Markdown.escape(original)}</s>\n\n<i>${note}</i>`;
784
+ return `<s>${Markdown.escape(original)}</s>\n\n<i>${Markdown.escape(note)}</i>`;
742
785
  }
743
786
 
744
787
  function renderCompactSuccess(result: SessionCompactResult): string {
@@ -763,6 +806,6 @@ async function safeEditMessage(
763
806
  reply_markup: replyMarkup,
764
807
  });
765
808
  } catch {
766
- // Message may have aged out past Telegram's edit window — non-fatal.
809
+ // The message may be past Telegram's edit window.
767
810
  }
768
811
  }
@@ -10,7 +10,6 @@ export type Cli = {
10
10
  readonly cwd?: string;
11
11
  readonly model?: string;
12
12
  readonly configDir?: string;
13
- readonly printConfig: boolean;
14
13
  };
15
14
 
16
15
  export type TelegramConfig = {
@@ -40,7 +39,6 @@ function parseArgs(args: ReadonlyArray<string>): Cli {
40
39
  let cwd: string | undefined;
41
40
  let model: string | undefined;
42
41
  let configDir: string | undefined;
43
- let printConfig = false;
44
42
 
45
43
  Argv.scan(args, (key, take) => {
46
44
  switch (key) {
@@ -59,15 +57,12 @@ function parseArgs(args: ReadonlyArray<string>): Cli {
59
57
  case "--config-dir":
60
58
  configDir = take();
61
59
  break;
62
- case "--print-config":
63
- printConfig = true;
64
- break;
65
60
  case "--mode":
66
61
  take();
67
62
  break;
68
63
  }
69
64
  });
70
- return { token, allow, cwd, model, configDir, printConfig };
65
+ return { token, allow, cwd, model, configDir };
71
66
  }
72
67
 
73
68
  async function load(cli: Cli): Promise<TelegramConfig> {