@sheason/pi-coding-agent 0.74.1-sheason.0 → 0.78.0-sheason.0.6.0-alpha.2

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 (516) hide show
  1. package/CHANGELOG.md +256 -4
  2. package/README.md +16 -8
  3. package/dist/bun/cli.d.ts.map +1 -1
  4. package/dist/bun/cli.js.map +1 -1
  5. package/dist/cli/args.d.ts +7 -2
  6. package/dist/cli/args.d.ts.map +1 -1
  7. package/dist/cli/args.js +49 -1
  8. package/dist/cli/args.js.map +1 -1
  9. package/dist/cli/config-selector.d.ts +2 -2
  10. package/dist/cli/config-selector.d.ts.map +1 -1
  11. package/dist/cli/config-selector.js +1 -1
  12. package/dist/cli/config-selector.js.map +1 -1
  13. package/dist/cli/file-processor.d.ts.map +1 -1
  14. package/dist/cli/file-processor.js +2 -3
  15. package/dist/cli/file-processor.js.map +1 -1
  16. package/dist/cli/initial-message.d.ts +1 -1
  17. package/dist/cli/initial-message.d.ts.map +1 -1
  18. package/dist/cli/initial-message.js.map +1 -1
  19. package/dist/cli/list-models.d.ts +1 -1
  20. package/dist/cli/list-models.d.ts.map +1 -1
  21. package/dist/cli/list-models.js.map +1 -1
  22. package/dist/cli/session-picker.d.ts +1 -1
  23. package/dist/cli/session-picker.d.ts.map +1 -1
  24. package/dist/cli/session-picker.js.map +1 -1
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +4 -6
  27. package/dist/cli.js.map +1 -1
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +61 -32
  30. package/dist/config.js.map +1 -1
  31. package/dist/core/agent-session-proxy.d.ts +268 -0
  32. package/dist/core/agent-session-proxy.d.ts.map +1 -0
  33. package/dist/core/agent-session-proxy.js +2 -0
  34. package/dist/core/agent-session-proxy.js.map +1 -0
  35. package/dist/core/agent-session-runtime.d.ts +10 -10
  36. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  37. package/dist/core/agent-session-runtime.js +14 -14
  38. package/dist/core/agent-session-runtime.js.map +1 -1
  39. package/dist/core/agent-session-services.d.ts +8 -7
  40. package/dist/core/agent-session-services.d.ts.map +1 -1
  41. package/dist/core/agent-session-services.js +4 -2
  42. package/dist/core/agent-session-services.js.map +1 -1
  43. package/dist/core/agent-session.d.ts +60 -27
  44. package/dist/core/agent-session.d.ts.map +1 -1
  45. package/dist/core/agent-session.js +303 -177
  46. package/dist/core/agent-session.js.map +1 -1
  47. package/dist/core/auth-guidance.d.ts.map +1 -1
  48. package/dist/core/auth-guidance.js.map +1 -1
  49. package/dist/core/auth-storage.d.ts +1 -1
  50. package/dist/core/auth-storage.d.ts.map +1 -1
  51. package/dist/core/auth-storage.js +3 -2
  52. package/dist/core/auth-storage.js.map +1 -1
  53. package/dist/core/bash-executor.d.ts +1 -1
  54. package/dist/core/bash-executor.d.ts.map +1 -1
  55. package/dist/core/bash-executor.js.map +1 -1
  56. package/dist/core/compaction/branch-summarization.d.ts +3 -3
  57. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  58. package/dist/core/compaction/branch-summarization.js.map +1 -1
  59. package/dist/core/compaction/compaction.d.ts +5 -5
  60. package/dist/core/compaction/compaction.d.ts.map +1 -1
  61. package/dist/core/compaction/compaction.js +41 -37
  62. package/dist/core/compaction/compaction.js.map +1 -1
  63. package/dist/core/compaction/index.d.ts +3 -3
  64. package/dist/core/compaction/index.d.ts.map +1 -1
  65. package/dist/core/compaction/index.js.map +1 -1
  66. package/dist/core/exec.d.ts.map +1 -1
  67. package/dist/core/exec.js.map +1 -1
  68. package/dist/core/export-html/index.d.ts +1 -1
  69. package/dist/core/export-html/index.d.ts.map +1 -1
  70. package/dist/core/export-html/index.js +8 -6
  71. package/dist/core/export-html/index.js.map +1 -1
  72. package/dist/core/export-html/template.js +23 -6
  73. package/dist/core/export-html/tool-renderer.d.ts +2 -2
  74. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  75. package/dist/core/export-html/tool-renderer.js.map +1 -1
  76. package/dist/core/extensions/index.d.ts +8 -8
  77. package/dist/core/extensions/index.d.ts.map +1 -1
  78. package/dist/core/extensions/index.js.map +1 -1
  79. package/dist/core/extensions/loader.d.ts +2 -2
  80. package/dist/core/extensions/loader.d.ts.map +1 -1
  81. package/dist/core/extensions/loader.js +17 -34
  82. package/dist/core/extensions/loader.js.map +1 -1
  83. package/dist/core/extensions/runner.d.ts +12 -7
  84. package/dist/core/extensions/runner.d.ts.map +1 -1
  85. package/dist/core/extensions/runner.js +36 -2
  86. package/dist/core/extensions/runner.js.map +1 -1
  87. package/dist/core/extensions/types.d.ts +26 -24
  88. package/dist/core/extensions/types.d.ts.map +1 -1
  89. package/dist/core/extensions/types.js.map +1 -1
  90. package/dist/core/extensions/wrapper.d.ts +2 -2
  91. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  92. package/dist/core/extensions/wrapper.js.map +1 -1
  93. package/dist/core/footer-data-provider.d.ts +3 -1
  94. package/dist/core/footer-data-provider.d.ts.map +1 -1
  95. package/dist/core/footer-data-provider.js +4 -0
  96. package/dist/core/footer-data-provider.js.map +1 -1
  97. package/dist/core/http-dispatcher.d.ts +21 -0
  98. package/dist/core/http-dispatcher.d.ts.map +1 -0
  99. package/dist/core/http-dispatcher.js +48 -0
  100. package/dist/core/http-dispatcher.js.map +1 -0
  101. package/dist/core/index.d.ts +8 -8
  102. package/dist/core/index.d.ts.map +1 -1
  103. package/dist/core/index.js.map +1 -1
  104. package/dist/core/keybindings.d.ts.map +1 -1
  105. package/dist/core/keybindings.js.map +1 -1
  106. package/dist/core/local-agent-session-proxy.d.ts +82 -0
  107. package/dist/core/local-agent-session-proxy.d.ts.map +1 -0
  108. package/dist/core/local-agent-session-proxy.js +531 -0
  109. package/dist/core/local-agent-session-proxy.js.map +1 -0
  110. package/dist/core/messages.d.ts +0 -9
  111. package/dist/core/messages.d.ts.map +1 -1
  112. package/dist/core/messages.js +0 -10
  113. package/dist/core/messages.js.map +1 -1
  114. package/dist/core/model-registry.d.ts +4 -4
  115. package/dist/core/model-registry.d.ts.map +1 -1
  116. package/dist/core/model-registry.js +72 -16
  117. package/dist/core/model-registry.js.map +1 -1
  118. package/dist/core/model-resolver.d.ts +1 -1
  119. package/dist/core/model-resolver.d.ts.map +1 -1
  120. package/dist/core/model-resolver.js +1 -1
  121. package/dist/core/model-resolver.js.map +1 -1
  122. package/dist/core/output-guard.d.ts +1 -0
  123. package/dist/core/output-guard.d.ts.map +1 -1
  124. package/dist/core/output-guard.js +52 -22
  125. package/dist/core/output-guard.js.map +1 -1
  126. package/dist/core/package-manager.d.ts +7 -1
  127. package/dist/core/package-manager.d.ts.map +1 -1
  128. package/dist/core/package-manager.js +129 -64
  129. package/dist/core/package-manager.js.map +1 -1
  130. package/dist/core/prompt-templates.d.ts +1 -1
  131. package/dist/core/prompt-templates.d.ts.map +1 -1
  132. package/dist/core/prompt-templates.js +12 -24
  133. package/dist/core/prompt-templates.js.map +1 -1
  134. package/dist/core/provider-display-names.d.ts.map +1 -1
  135. package/dist/core/provider-display-names.js +0 -1
  136. package/dist/core/provider-display-names.js.map +1 -1
  137. package/dist/core/resolve-config-value.d.ts +9 -1
  138. package/dist/core/resolve-config-value.d.ts.map +1 -1
  139. package/dist/core/resolve-config-value.js +134 -11
  140. package/dist/core/resolve-config-value.js.map +1 -1
  141. package/dist/core/resource-loader.d.ts +13 -10
  142. package/dist/core/resource-loader.d.ts.map +1 -1
  143. package/dist/core/resource-loader.js +41 -33
  144. package/dist/core/resource-loader.js.map +1 -1
  145. package/dist/core/sdk.d.ts +15 -13
  146. package/dist/core/sdk.d.ts.map +1 -1
  147. package/dist/core/sdk.js +24 -17
  148. package/dist/core/sdk.js.map +1 -1
  149. package/dist/core/session-manager.d.ts +20 -10
  150. package/dist/core/session-manager.d.ts.map +1 -1
  151. package/dist/core/session-manager.js +201 -106
  152. package/dist/core/session-manager.js.map +1 -1
  153. package/dist/core/settings-manager.d.ts +5 -0
  154. package/dist/core/settings-manager.d.ts.map +1 -1
  155. package/dist/core/settings-manager.js +31 -13
  156. package/dist/core/settings-manager.js.map +1 -1
  157. package/dist/core/skills.d.ts +2 -2
  158. package/dist/core/skills.d.ts.map +1 -1
  159. package/dist/core/skills.js +10 -27
  160. package/dist/core/skills.js.map +1 -1
  161. package/dist/core/slash-commands.d.ts +1 -1
  162. package/dist/core/slash-commands.d.ts.map +1 -1
  163. package/dist/core/slash-commands.js.map +1 -1
  164. package/dist/core/source-info.d.ts +1 -1
  165. package/dist/core/source-info.d.ts.map +1 -1
  166. package/dist/core/source-info.js.map +1 -1
  167. package/dist/core/system-prompt.d.ts +1 -1
  168. package/dist/core/system-prompt.d.ts.map +1 -1
  169. package/dist/core/system-prompt.js +16 -9
  170. package/dist/core/system-prompt.js.map +1 -1
  171. package/dist/core/telemetry.d.ts +1 -1
  172. package/dist/core/telemetry.d.ts.map +1 -1
  173. package/dist/core/telemetry.js.map +1 -1
  174. package/dist/core/tools/bash.d.ts +2 -2
  175. package/dist/core/tools/bash.d.ts.map +1 -1
  176. package/dist/core/tools/bash.js +55 -54
  177. package/dist/core/tools/bash.js.map +1 -1
  178. package/dist/core/tools/edit-diff.d.ts +3 -1
  179. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  180. package/dist/core/tools/edit-diff.js +8 -1
  181. package/dist/core/tools/edit-diff.js.map +1 -1
  182. package/dist/core/tools/edit.d.ts +5 -3
  183. package/dist/core/tools/edit.d.ts.map +1 -1
  184. package/dist/core/tools/edit.js +51 -91
  185. package/dist/core/tools/edit.js.map +1 -1
  186. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  187. package/dist/core/tools/file-mutation-queue.js +27 -12
  188. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  189. package/dist/core/tools/find.d.ts +2 -2
  190. package/dist/core/tools/find.d.ts.map +1 -1
  191. package/dist/core/tools/find.js +2 -3
  192. package/dist/core/tools/find.js.map +1 -1
  193. package/dist/core/tools/grep.d.ts +2 -2
  194. package/dist/core/tools/grep.d.ts.map +1 -1
  195. package/dist/core/tools/grep.js +3 -3
  196. package/dist/core/tools/grep.js.map +1 -1
  197. package/dist/core/tools/index.d.ts +17 -17
  198. package/dist/core/tools/index.d.ts.map +1 -1
  199. package/dist/core/tools/index.js.map +1 -1
  200. package/dist/core/tools/ls.d.ts +2 -2
  201. package/dist/core/tools/ls.d.ts.map +1 -1
  202. package/dist/core/tools/ls.js +10 -12
  203. package/dist/core/tools/ls.js.map +1 -1
  204. package/dist/core/tools/output-accumulator.d.ts +3 -1
  205. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  206. package/dist/core/tools/output-accumulator.js +9 -3
  207. package/dist/core/tools/output-accumulator.js.map +1 -1
  208. package/dist/core/tools/path-utils.d.ts +2 -0
  209. package/dist/core/tools/path-utils.d.ts.map +1 -1
  210. package/dist/core/tools/path-utils.js +39 -21
  211. package/dist/core/tools/path-utils.js.map +1 -1
  212. package/dist/core/tools/read.d.ts +2 -2
  213. package/dist/core/tools/read.d.ts.map +1 -1
  214. package/dist/core/tools/read.js +15 -15
  215. package/dist/core/tools/read.js.map +1 -1
  216. package/dist/core/tools/render-utils.d.ts +5 -2
  217. package/dist/core/tools/render-utils.d.ts.map +1 -1
  218. package/dist/core/tools/render-utils.js +17 -1
  219. package/dist/core/tools/render-utils.js.map +1 -1
  220. package/dist/core/tools/tool-definition-wrapper.d.ts +1 -1
  221. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  222. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  223. package/dist/core/tools/truncate.d.ts.map +1 -1
  224. package/dist/core/tools/truncate.js +12 -2
  225. package/dist/core/tools/truncate.js.map +1 -1
  226. package/dist/core/tools/write.d.ts +1 -1
  227. package/dist/core/tools/write.d.ts.map +1 -1
  228. package/dist/core/tools/write.js +25 -41
  229. package/dist/core/tools/write.js.map +1 -1
  230. package/dist/d-pi-worker.d.ts +12 -0
  231. package/dist/d-pi-worker.d.ts.map +1 -0
  232. package/dist/d-pi-worker.js +9 -0
  233. package/dist/d-pi-worker.js.map +1 -0
  234. package/dist/index.d.ts +30 -28
  235. package/dist/index.d.ts.map +1 -1
  236. package/dist/index.js +5 -3
  237. package/dist/index.js.map +1 -1
  238. package/dist/main.d.ts +1 -1
  239. package/dist/main.d.ts.map +1 -1
  240. package/dist/main.js +100 -39
  241. package/dist/main.js.map +1 -1
  242. package/dist/migrations.d.ts.map +1 -1
  243. package/dist/migrations.js +118 -1
  244. package/dist/migrations.js.map +1 -1
  245. package/dist/modes/connect/auth-headers.d.ts +2 -0
  246. package/dist/modes/connect/auth-headers.d.ts.map +1 -0
  247. package/dist/modes/connect/auth-headers.js +2 -0
  248. package/dist/modes/connect/auth-headers.js.map +1 -0
  249. package/dist/modes/connect/client-extension-sync.d.ts +13 -0
  250. package/dist/modes/connect/client-extension-sync.d.ts.map +1 -0
  251. package/dist/modes/connect/client-extension-sync.js +51 -0
  252. package/dist/modes/connect/client-extension-sync.js.map +1 -0
  253. package/dist/modes/connect/connect-mode.d.ts +6 -0
  254. package/dist/modes/connect/connect-mode.d.ts.map +1 -0
  255. package/dist/modes/connect/connect-mode.js +29 -0
  256. package/dist/modes/connect/connect-mode.js.map +1 -0
  257. package/dist/modes/connect/remote-agent-session-proxy.d.ts +81 -0
  258. package/dist/modes/connect/remote-agent-session-proxy.d.ts.map +1 -0
  259. package/dist/modes/connect/remote-agent-session-proxy.js +326 -0
  260. package/dist/modes/connect/remote-agent-session-proxy.js.map +1 -0
  261. package/dist/modes/connect/sse-client.d.ts +18 -0
  262. package/dist/modes/connect/sse-client.d.ts.map +1 -0
  263. package/dist/modes/connect/sse-client.js +90 -0
  264. package/dist/modes/connect/sse-client.js.map +1 -0
  265. package/dist/modes/index.d.ts +5 -5
  266. package/dist/modes/index.d.ts.map +1 -1
  267. package/dist/modes/index.js.map +1 -1
  268. package/dist/modes/interactive/components/armin.d.ts.map +1 -1
  269. package/dist/modes/interactive/components/armin.js.map +1 -1
  270. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  271. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  272. package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
  273. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  274. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  275. package/dist/modes/interactive/components/bordered-loader.d.ts +1 -1
  276. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  277. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  278. package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
  279. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  280. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  281. package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
  282. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  283. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  284. package/dist/modes/interactive/components/config-selector.d.ts +4 -4
  285. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  286. package/dist/modes/interactive/components/config-selector.js +8 -5
  287. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  288. package/dist/modes/interactive/components/countdown-timer.d.ts +2 -2
  289. package/dist/modes/interactive/components/countdown-timer.d.ts.map +1 -1
  290. package/dist/modes/interactive/components/countdown-timer.js +2 -2
  291. package/dist/modes/interactive/components/countdown-timer.js.map +1 -1
  292. package/dist/modes/interactive/components/custom-editor.d.ts +1 -1
  293. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  294. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  295. package/dist/modes/interactive/components/custom-message.d.ts +2 -2
  296. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  297. package/dist/modes/interactive/components/custom-message.js +0 -1
  298. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  299. package/dist/modes/interactive/components/daxnuts.d.ts.map +1 -1
  300. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  301. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  302. package/dist/modes/interactive/components/diff.js.map +1 -1
  303. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  304. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  305. package/dist/modes/interactive/components/earendil-announcement.d.ts.map +1 -1
  306. package/dist/modes/interactive/components/earendil-announcement.js.map +1 -1
  307. package/dist/modes/interactive/components/extension-editor.d.ts +1 -1
  308. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  309. package/dist/modes/interactive/components/extension-editor.js +14 -6
  310. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  311. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  312. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  313. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  314. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  315. package/dist/modes/interactive/components/footer.d.ts +15 -4
  316. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  317. package/dist/modes/interactive/components/footer.js +126 -8
  318. package/dist/modes/interactive/components/footer.js.map +1 -1
  319. package/dist/modes/interactive/components/index.d.ts +31 -31
  320. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  321. package/dist/modes/interactive/components/index.js.map +1 -1
  322. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  323. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  324. package/dist/modes/interactive/components/login-dialog.d.ts +7 -1
  325. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  326. package/dist/modes/interactive/components/login-dialog.js +28 -5
  327. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  328. package/dist/modes/interactive/components/model-selector.d.ts +2 -2
  329. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  330. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  331. package/dist/modes/interactive/components/oauth-selector.d.ts +1 -1
  332. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  333. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  334. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  335. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  336. package/dist/modes/interactive/components/session-selector-search.d.ts +1 -1
  337. package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -1
  338. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  339. package/dist/modes/interactive/components/session-selector.d.ts +3 -3
  340. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  341. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  342. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  343. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  344. package/dist/modes/interactive/components/settings-selector.js +15 -0
  345. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  346. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  347. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  348. package/dist/modes/interactive/components/skill-invocation-message.d.ts +1 -1
  349. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  350. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  351. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  352. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  353. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  354. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  355. package/dist/modes/interactive/components/tool-execution.d.ts +1 -1
  356. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  357. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  358. package/dist/modes/interactive/components/tree-selector.d.ts +1 -1
  359. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  360. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  361. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  362. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  363. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  364. package/dist/modes/interactive/components/user-message.js.map +1 -1
  365. package/dist/modes/interactive/interactive-mode.d.ts +53 -7
  366. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  367. package/dist/modes/interactive/interactive-mode.js +1247 -205
  368. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  369. package/dist/modes/interactive/theme/dark.json +5 -4
  370. package/dist/modes/interactive/theme/light.json +5 -4
  371. package/dist/modes/interactive/theme/theme.d.ts +22 -3
  372. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  373. package/dist/modes/interactive/theme/theme.js +130 -69
  374. package/dist/modes/interactive/theme/theme.js.map +1 -1
  375. package/dist/modes/print-mode.d.ts +1 -1
  376. package/dist/modes/print-mode.d.ts.map +1 -1
  377. package/dist/modes/print-mode.js.map +1 -1
  378. package/dist/modes/rpc/rpc-client.d.ts +8 -5
  379. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  380. package/dist/modes/rpc/rpc-client.js +65 -8
  381. package/dist/modes/rpc/rpc-client.js.map +1 -1
  382. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  383. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  384. package/dist/modes/rpc/rpc-mode.js +18 -4
  385. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  386. package/dist/modes/rpc/rpc-types.d.ts +5 -4
  387. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  388. package/dist/modes/rpc/rpc-types.js.map +1 -1
  389. package/dist/modes/serve/api-handlers.d.ts +4 -0
  390. package/dist/modes/serve/api-handlers.d.ts.map +1 -0
  391. package/dist/modes/serve/api-handlers.js +324 -0
  392. package/dist/modes/serve/api-handlers.js.map +1 -0
  393. package/dist/modes/serve/http-server.d.ts +14 -0
  394. package/dist/modes/serve/http-server.d.ts.map +1 -0
  395. package/dist/modes/serve/http-server.js +94 -0
  396. package/dist/modes/serve/http-server.js.map +1 -0
  397. package/dist/modes/serve/serve-mode.d.ts +10 -0
  398. package/dist/modes/serve/serve-mode.d.ts.map +1 -0
  399. package/dist/modes/serve/serve-mode.js +217 -0
  400. package/dist/modes/serve/serve-mode.js.map +1 -0
  401. package/dist/package-manager-cli.d.ts.map +1 -1
  402. package/dist/package-manager-cli.js +62 -7
  403. package/dist/package-manager-cli.js.map +1 -1
  404. package/dist/utils/ansi.d.ts.map +1 -1
  405. package/dist/utils/ansi.js +47 -72
  406. package/dist/utils/ansi.js.map +1 -1
  407. package/dist/utils/changelog.d.ts +1 -1
  408. package/dist/utils/changelog.d.ts.map +1 -1
  409. package/dist/utils/changelog.js.map +1 -1
  410. package/dist/utils/child-process.d.ts +5 -2
  411. package/dist/utils/child-process.d.ts.map +1 -1
  412. package/dist/utils/child-process.js +9 -7
  413. package/dist/utils/child-process.js.map +1 -1
  414. package/dist/utils/clipboard-image.d.ts.map +1 -1
  415. package/dist/utils/clipboard-image.js.map +1 -1
  416. package/dist/utils/clipboard-native.d.ts +3 -1
  417. package/dist/utils/clipboard-native.d.ts.map +1 -1
  418. package/dist/utils/clipboard-native.js +14 -8
  419. package/dist/utils/clipboard-native.js.map +1 -1
  420. package/dist/utils/clipboard.d.ts.map +1 -1
  421. package/dist/utils/clipboard.js.map +1 -1
  422. package/dist/utils/deprecation.d.ts +4 -0
  423. package/dist/utils/deprecation.d.ts.map +1 -0
  424. package/dist/utils/deprecation.js +13 -0
  425. package/dist/utils/deprecation.js.map +1 -0
  426. package/dist/utils/exif-orientation.d.ts +1 -1
  427. package/dist/utils/exif-orientation.d.ts.map +1 -1
  428. package/dist/utils/exif-orientation.js.map +1 -1
  429. package/dist/utils/html.d.ts +7 -0
  430. package/dist/utils/html.d.ts.map +1 -0
  431. package/dist/utils/html.js +40 -0
  432. package/dist/utils/html.js.map +1 -0
  433. package/dist/utils/image-convert.d.ts.map +1 -1
  434. package/dist/utils/image-convert.js.map +1 -1
  435. package/dist/utils/image-resize-core.d.ts +30 -0
  436. package/dist/utils/image-resize-core.d.ts.map +1 -0
  437. package/dist/utils/image-resize-core.js +124 -0
  438. package/dist/utils/image-resize-core.js.map +1 -0
  439. package/dist/utils/image-resize-worker.d.ts +2 -0
  440. package/dist/utils/image-resize-worker.d.ts.map +1 -0
  441. package/dist/utils/image-resize-worker.js +31 -0
  442. package/dist/utils/image-resize-worker.js.map +1 -0
  443. package/dist/utils/image-resize.d.ts +7 -27
  444. package/dist/utils/image-resize.d.ts.map +1 -1
  445. package/dist/utils/image-resize.js +75 -131
  446. package/dist/utils/image-resize.js.map +1 -1
  447. package/dist/utils/json.d.ts +3 -0
  448. package/dist/utils/json.d.ts.map +1 -0
  449. package/dist/utils/json.js +7 -0
  450. package/dist/utils/json.js.map +1 -0
  451. package/dist/utils/paths.d.ts +16 -1
  452. package/dist/utils/paths.d.ts.map +1 -1
  453. package/dist/utils/paths.js +49 -7
  454. package/dist/utils/paths.js.map +1 -1
  455. package/dist/utils/shell.d.ts.map +1 -1
  456. package/dist/utils/shell.js +6 -1
  457. package/dist/utils/shell.js.map +1 -1
  458. package/dist/utils/syntax-highlight.d.ts +12 -0
  459. package/dist/utils/syntax-highlight.d.ts.map +1 -0
  460. package/dist/utils/syntax-highlight.js +118 -0
  461. package/dist/utils/syntax-highlight.js.map +1 -0
  462. package/dist/utils/tools-manager.d.ts.map +1 -1
  463. package/dist/utils/tools-manager.js +4 -1
  464. package/dist/utils/tools-manager.js.map +1 -1
  465. package/dist/utils/version-check.d.ts +2 -1
  466. package/dist/utils/version-check.d.ts.map +1 -1
  467. package/dist/utils/version-check.js +9 -4
  468. package/dist/utils/version-check.js.map +1 -1
  469. package/dist/utils/windows-self-update.d.ts +3 -0
  470. package/dist/utils/windows-self-update.d.ts.map +1 -0
  471. package/dist/utils/windows-self-update.js +77 -0
  472. package/dist/utils/windows-self-update.js.map +1 -0
  473. package/docs/custom-provider.md +111 -21
  474. package/docs/development.md +1 -1
  475. package/docs/extensions.md +13 -7
  476. package/docs/index.md +13 -3
  477. package/docs/models.md +32 -13
  478. package/docs/packages.md +9 -6
  479. package/docs/providers.md +13 -5
  480. package/docs/quickstart.md +24 -1
  481. package/docs/rpc.md +2 -1
  482. package/docs/sdk.md +8 -0
  483. package/docs/session-format.md +1 -1
  484. package/docs/sessions.md +8 -0
  485. package/docs/settings.md +8 -6
  486. package/docs/skills.md +3 -4
  487. package/docs/terminal-setup.md +8 -0
  488. package/docs/termux.md +3 -3
  489. package/docs/tui.md +2 -2
  490. package/docs/usage.md +13 -2
  491. package/examples/extensions/README.md +1 -0
  492. package/examples/extensions/custom-provider-anthropic/index.ts +1 -1
  493. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  494. package/examples/extensions/custom-provider-anthropic/package.json +2 -2
  495. package/examples/extensions/custom-provider-gitlab-duo/index.ts +54 -3
  496. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  497. package/examples/extensions/custom-provider-gitlab-duo/test.ts +1 -1
  498. package/examples/extensions/doom-overlay/doom-component.ts +2 -2
  499. package/examples/extensions/doom-overlay/index.ts +3 -3
  500. package/examples/extensions/git-merge-and-resolve.ts +115 -0
  501. package/examples/extensions/input-transform-streaming.ts +39 -0
  502. package/examples/extensions/overlay-qa-tests.ts +97 -66
  503. package/examples/extensions/overlay-test.ts +7 -4
  504. package/examples/extensions/plan-mode/index.ts +1 -1
  505. package/examples/extensions/sandbox/package-lock.json +2 -2
  506. package/examples/extensions/sandbox/package.json +2 -2
  507. package/examples/extensions/subagent/README.md +3 -0
  508. package/examples/extensions/subagent/index.ts +42 -20
  509. package/examples/extensions/with-deps/package-lock.json +2 -2
  510. package/examples/extensions/with-deps/package.json +3 -3
  511. package/npm-shrinkwrap.json +1790 -0
  512. package/package.json +39 -32
  513. package/dist/utils/uuid.d.ts +0 -2
  514. package/dist/utils/uuid.d.ts.map +0 -1
  515. package/dist/utils/uuid.js +0 -40
  516. package/dist/utils/uuid.js.map +0 -1
@@ -7,14 +7,17 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { getProviders, } from "@sheason/pi-ai";
10
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@sheason/pi-tui";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, SelectList, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@sheason/pi-tui";
11
+ import chalk from "chalk";
11
12
  import { spawn, spawnSync } from "child_process";
12
13
  import { APP_NAME, APP_TITLE, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
13
- import { parseSkillBlock } from "../../core/agent-session.js";
14
+ import { parseSkillBlock, } from "../../core/agent-session.js";
14
15
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
16
+ import { ExtensionRunner as ExtensionRunnerImpl } from "../../core/extensions/runner.js";
15
17
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
18
+ import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
16
19
  import { KeybindingsManager } from "../../core/keybindings.js";
17
- import { createCompactionSummaryMessage, } from "../../core/messages.js";
20
+ import { createCompactionSummaryMessage } from "../../core/messages.js";
18
21
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
19
22
  import { DefaultPackageManager } from "../../core/package-manager.js";
20
23
  import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
@@ -31,6 +34,7 @@ import { getPiUserAgent } from "../../utils/pi-user-agent.js";
31
34
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
32
35
  import { ensureTool } from "../../utils/tools-manager.js";
33
36
  import { checkForNewPiVersion } from "../../utils/version-check.js";
37
+ import { loadRemoteClientExtensions } from "../connect/client-extension-sync.js";
34
38
  import { ArminComponent } from "./components/armin.js";
35
39
  import { AssistantMessageComponent } from "./components/assistant-message.js";
36
40
  import { BashExecutionComponent } from "./components/bash-execution.js";
@@ -46,7 +50,7 @@ import { EarendilAnnouncementComponent } from "./components/earendil-announcemen
46
50
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
47
51
  import { ExtensionInputComponent } from "./components/extension-input.js";
48
52
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
49
- import { FooterComponent } from "./components/footer.js";
53
+ import { FooterComponent, formatTokens } from "./components/footer.js";
50
54
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
51
55
  import { LoginDialogComponent } from "./components/login-dialog.js";
52
56
  import { ModelSelectorComponent } from "./components/model-selector.js";
@@ -59,7 +63,7 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
59
63
  import { TreeSelectorComponent } from "./components/tree-selector.js";
60
64
  import { UserMessageComponent } from "./components/user-message.js";
61
65
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
62
- import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
66
+ import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getSelectListTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
63
67
  function isExpandable(obj) {
64
68
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
65
69
  }
@@ -90,6 +94,27 @@ function isAnthropicSubscriptionAuthKey(apiKey) {
90
94
  function isUnknownModel(model) {
91
95
  return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown";
92
96
  }
97
+ function quoteIfNeeded(value) {
98
+ if (value.length > 0 && !/[^a-zA-Z0-9_\-./~:@]/.test(value)) {
99
+ return value;
100
+ }
101
+ return `'${value.replace(/'/g, `'\\''`)}'`;
102
+ }
103
+ export function formatResumeCommand(sessionManager) {
104
+ if (!process.stdout.isTTY)
105
+ return undefined;
106
+ if (!sessionManager.isPersisted())
107
+ return undefined;
108
+ const sessionFile = sessionManager.getSessionFile();
109
+ if (!sessionFile || !fs.existsSync(sessionFile))
110
+ return undefined;
111
+ const args = [APP_NAME];
112
+ if (!sessionManager.usesDefaultSessionDir()) {
113
+ args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
114
+ }
115
+ args.push("--session", sessionManager.getSessionId());
116
+ return args.join(" ");
117
+ }
93
118
  function hasDefaultModelProvider(providerId) {
94
119
  return providerId in defaultModelPerProvider;
95
120
  }
@@ -105,7 +130,6 @@ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProvi
105
130
  return !oauthProviderIds.has(providerId);
106
131
  }
107
132
  export class InteractiveMode {
108
- options;
109
133
  runtimeHost;
110
134
  ui;
111
135
  chatContainer;
@@ -125,6 +149,7 @@ export class InteractiveMode {
125
149
  version;
126
150
  isInitialized = false;
127
151
  onInputCallback;
152
+ pendingUserInputs = [];
128
153
  loadingAnimation = undefined;
129
154
  workingMessage = undefined;
130
155
  workingVisible = true;
@@ -176,6 +201,8 @@ export class InteractiveMode {
176
201
  extensionInput = undefined;
177
202
  extensionEditor = undefined;
178
203
  extensionTerminalInputUnsubscribers = new Set();
204
+ // Client-side extension runner for connect mode (undefined in local mode)
205
+ _clientExtensionRunner = undefined;
179
206
  // Extension widgets (components rendered above/below the editor)
180
207
  extensionWidgetsAbove = new Map();
181
208
  extensionWidgetsBelow = new Map();
@@ -189,31 +216,61 @@ export class InteractiveMode {
189
216
  builtInHeader = undefined;
190
217
  // Custom header from extension (undefined = use built-in header)
191
218
  customHeader = undefined;
192
- // Convenience accessors
219
+ options;
220
+ /** Proxy for connect mode — when set, core operations go through the proxy */
221
+ proxy;
222
+ /** Set the proxy after construction (used by connect mode for late binding) */
223
+ setProxy(proxy) {
224
+ this.proxy = proxy;
225
+ }
226
+ // Convenience accessors — return undefined when runtimeHost is not set (connect mode)
193
227
  get session() {
194
- return this.runtimeHost.session;
228
+ return this.runtimeHost?.session;
195
229
  }
196
230
  get agent() {
197
- return this.session.agent;
231
+ return this.session?.agent;
198
232
  }
199
233
  get sessionManager() {
200
- return this.session.sessionManager;
234
+ return this.session?.sessionManager;
201
235
  }
202
236
  get settingsManager() {
203
- return this.session.settingsManager;
237
+ return this.session?.settingsManager;
238
+ }
239
+ // Safe accessors that return defaults in connect mode
240
+ get showTerminalProgress() {
241
+ return this.runtimeHost ? this.settingsManager.getShowTerminalProgress() : false;
242
+ }
243
+ get showImages() {
244
+ return this.runtimeHost ? this.settingsManager.getShowImages() : false;
245
+ }
246
+ get imageWidthCells() {
247
+ return this.runtimeHost ? this.settingsManager.getImageWidthCells() : 80;
248
+ }
249
+ get currentCwd() {
250
+ return this.runtimeHost ? this.sessionManager.getCwd() : process.cwd();
204
251
  }
205
252
  constructor(runtimeHost, options = {}) {
206
- this.options = options;
207
253
  this.runtimeHost = runtimeHost;
208
- this.runtimeHost.setBeforeSessionInvalidate(() => {
209
- this.resetExtensionUI();
210
- });
211
- this.runtimeHost.setRebindSession(async () => {
212
- await this.rebindCurrentSession();
213
- });
254
+ this.options = options;
255
+ this.proxy = options.proxy;
256
+ // Only register runtime callbacks when NOT using proxy (connect mode)
257
+ if (this.runtimeHost) {
258
+ this.runtimeHost.setBeforeSessionInvalidate(() => {
259
+ this.resetExtensionUI();
260
+ });
261
+ this.runtimeHost.setRebindSession(async () => {
262
+ await this.rebindCurrentSession();
263
+ });
264
+ }
214
265
  this.version = VERSION;
215
- this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
216
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
266
+ // In connect mode (proxy set), use defaults for UI setup since session/settingsManager
267
+ // are not available. In normal mode, read from settingsManager.
268
+ const showHardwareCursor = this.runtimeHost ? this.settingsManager.getShowHardwareCursor() : false;
269
+ const clearOnShrink = this.runtimeHost ? this.settingsManager.getClearOnShrink() : true;
270
+ const editorPaddingX = this.runtimeHost ? this.settingsManager.getEditorPaddingX() : 0;
271
+ const autocompleteMaxVisible = this.runtimeHost ? this.settingsManager.getAutocompleteMaxVisible() : 8;
272
+ this.ui = new TUI(new ProcessTerminal(), showHardwareCursor);
273
+ this.ui.setClearOnShrink(clearOnShrink);
217
274
  this.headerContainer = new Container();
218
275
  this.chatContainer = new Container();
219
276
  this.pendingMessagesContainer = new Container();
@@ -222,8 +279,6 @@ export class InteractiveMode {
222
279
  this.widgetContainerBelow = new Container();
223
280
  this.keybindings = KeybindingsManager.create();
224
281
  setKeybindings(this.keybindings);
225
- const editorPaddingX = this.settingsManager.getEditorPaddingX();
226
- const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
227
282
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
228
283
  paddingX: editorPaddingX,
229
284
  autocompleteMaxVisible,
@@ -231,14 +286,19 @@ export class InteractiveMode {
231
286
  this.editor = this.defaultEditor;
232
287
  this.editorContainer = new Container();
233
288
  this.editorContainer.addChild(this.editor);
234
- this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
235
- this.footer = new FooterComponent(this.session, this.footerDataProvider);
236
- this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
289
+ this.footerDataProvider = new FooterDataProvider(this.runtimeHost ? this.sessionManager.getCwd() : process.cwd());
290
+ this.footer = new FooterComponent(this.runtimeHost ? this.session : undefined, this.footerDataProvider);
291
+ this.footer.setAutoCompactEnabled(this.runtimeHost ? this.session.autoCompactionEnabled : false);
237
292
  // Load hide thinking block setting
238
- this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
293
+ this.hideThinkingBlock = this.runtimeHost ? this.settingsManager.getHideThinkingBlock() : false;
239
294
  // Register themes from resource loader and initialize
240
- setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
241
- initTheme(this.settingsManager.getTheme(), true);
295
+ if (this.runtimeHost) {
296
+ setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
297
+ initTheme(this.settingsManager.getTheme(), true);
298
+ }
299
+ else {
300
+ initTheme("dark", true);
301
+ }
242
302
  }
243
303
  getAutocompleteSourceTag(sourceInfo) {
244
304
  if (!sourceInfo) {
@@ -319,8 +379,7 @@ export class InteractiveMode {
319
379
  }));
320
380
  // Convert extension commands to SlashCommand format
321
381
  const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
322
- const extensionCommands = this.session.extensionRunner
323
- .getRegisteredCommands()
382
+ const extensionCommands = this.session.extensionRunner.getRegisteredCommands()
324
383
  .filter((cmd) => !builtinCommandNames.has(cmd.name))
325
384
  .map((cmd) => ({
326
385
  name: cmd.invocationName,
@@ -353,6 +412,57 @@ export class InteractiveMode {
353
412
  this.editor.setAutocompleteProvider?.(provider);
354
413
  }
355
414
  }
415
+ /** Set up autocomplete for connect mode with a reduced command set */
416
+ setupConnectModeAutocomplete() {
417
+ // Fire-and-forget: fetch commands from server, build dynamic autocomplete.
418
+ // Falls back to builtin-only if the fetch fails.
419
+ void this._fetchAndSetConnectModeAutocomplete();
420
+ }
421
+ async _fetchAndSetConnectModeAutocomplete() {
422
+ try {
423
+ const commands = await this.proxy.fetchCommands();
424
+ this._applyConnectModeAutocomplete(commands);
425
+ }
426
+ catch {
427
+ this._applyConnectModeAutocompleteFallback();
428
+ }
429
+ }
430
+ _applyConnectModeAutocomplete(commands) {
431
+ const slashCommands = commands.map((cmd) => ({
432
+ name: cmd.name,
433
+ description: cmd.description,
434
+ ...(cmd.argumentHint && { argumentHint: cmd.argumentHint }),
435
+ }));
436
+ const provider = new CombinedAutocompleteProvider(slashCommands, this.currentCwd);
437
+ this.autocompleteProvider = provider;
438
+ this.defaultEditor.setAutocompleteProvider(provider);
439
+ }
440
+ _applyConnectModeAutocompleteFallback() {
441
+ const fallbackCommands = [
442
+ { name: "settings", description: "Open settings menu" },
443
+ {
444
+ name: "model",
445
+ description: "Switch model (e.g. /model provider/model-id)",
446
+ argumentHint: "<provider/model-id>",
447
+ },
448
+ { name: "compact", description: "Manually compact the session context" },
449
+ { name: "new", description: "Start a new session" },
450
+ { name: "fork", description: "Create a new fork from a previous user message" },
451
+ { name: "tree", description: "Navigate session tree (switch branches)" },
452
+ { name: "clone", description: "Duplicate the current session at the current position" },
453
+ { name: "resume", description: "Resume a different session" },
454
+ { name: "name", description: "Set session display name" },
455
+ { name: "copy", description: "Copy last agent message to clipboard" },
456
+ { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" },
457
+ { name: "hotkeys", description: "Show all keyboard shortcuts" },
458
+ { name: "changelog", description: "Show changelog entries" },
459
+ { name: "session", description: "Show session info and stats" },
460
+ { name: "quit", description: "Quit" },
461
+ ];
462
+ const provider = new CombinedAutocompleteProvider(fallbackCommands, this.currentCwd);
463
+ this.autocompleteProvider = provider;
464
+ this.defaultEditor.setAutocompleteProvider(provider);
465
+ }
356
466
  showStartupNoticesIfNeeded() {
357
467
  if (this.startupNoticesShown) {
358
468
  return;
@@ -384,49 +494,81 @@ export class InteractiveMode {
384
494
  return;
385
495
  this.registerSignalHandlers();
386
496
  // Load changelog (only show new entries, skip for resumed sessions)
387
- this.changelogMarkdown = this.getChangelogForDisplay();
497
+ // In connect mode, skip changelog since we don't have a local session
498
+ if (this.runtimeHost) {
499
+ this.changelogMarkdown = this.getChangelogForDisplay();
500
+ }
388
501
  // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
389
502
  // Both are needed: fd for autocomplete, rg for grep tool and bash commands
390
503
  const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
391
504
  this.fdPath = fdPath;
505
+ if (this.runtimeHost &&
506
+ this.session.scopedModels.length > 0 &&
507
+ (this.options.verbose || !this.settingsManager.getQuietStartup())) {
508
+ const modelList = this.session.scopedModels.map((sm) => {
509
+ const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : "";
510
+ return `${sm.model.id}${thinkingStr}`;
511
+ }).join(", ");
512
+ const cycleKeys = this.keybindings.getKeys("app.model.cycleForward");
513
+ const cycleHint = cycleKeys.length > 0
514
+ ? theme.fg("muted", ` (${formatKeyText(cycleKeys.join("/"), { capitalize: true })} to cycle)`)
515
+ : "";
516
+ console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
517
+ }
392
518
  // Add header container as first child
393
519
  this.ui.addChild(this.headerContainer);
394
520
  // Add header with keybindings from config (unless silenced)
395
- if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
396
- const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
397
- // Build startup instructions using keybinding hint helpers
398
- const hint = (keybinding, description) => keyHint(keybinding, description);
399
- const expandedInstructions = [
400
- hint("app.interrupt", "to interrupt"),
401
- hint("app.clear", "to clear"),
402
- rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
403
- hint("app.exit", "to exit (empty)"),
404
- hint("app.suspend", "to suspend"),
405
- keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
406
- hint("app.thinking.cycle", "to cycle thinking level"),
407
- rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
408
- hint("app.model.select", "to select model"),
409
- hint("app.tools.expand", "to expand tools"),
410
- hint("app.thinking.toggle", "to expand thinking"),
411
- hint("app.editor.external", "for external editor"),
412
- rawKeyHint("/", "for commands"),
413
- rawKeyHint("!", "to run bash"),
414
- rawKeyHint("!!", "to run bash (no context)"),
415
- hint("app.message.followUp", "to queue follow-up"),
416
- hint("app.message.dequeue", "to edit all queued messages"),
417
- hint("app.clipboard.pasteImage", "to paste image"),
418
- rawKeyHint("drop files", "to attach"),
419
- ].join("\n");
420
- const compactInstructions = [
421
- hint("app.interrupt", "interrupt"),
422
- rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
423
- rawKeyHint("/", "commands"),
424
- rawKeyHint("!", "bash"),
425
- hint("app.tools.expand", "more"),
426
- ].join(theme.fg("muted", " · "));
427
- const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
428
- const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
429
- this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
521
+ // In connect mode, always show header since we don't have settingsManager
522
+ const showHeader = this.runtimeHost ? this.options.verbose || !this.settingsManager.getQuietStartup() : true;
523
+ if (showHeader) {
524
+ const bannerData = this.options.banner;
525
+ if (bannerData) {
526
+ // Connect mode: render banner from structured data sent by server
527
+ const logo = theme.bold(theme.fg("accent", bannerData.appName)) + theme.fg("dim", ` v${bannerData.version}`);
528
+ const renderHint = (hint) => theme.fg("dim", hint.key) + theme.fg("muted", ` ${hint.description}`);
529
+ const expandedInstructions = bannerData.expandedHints.map(renderHint).join("\n");
530
+ const compactInstructions = bannerData.compactHints.map(renderHint).join(theme.fg("muted", " · "));
531
+ const compactOnboarding = theme.fg("dim", bannerData.compactOnboarding);
532
+ const onboarding = theme.fg("dim", bannerData.onboarding);
533
+ this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
534
+ }
535
+ else {
536
+ // Local mode: generate banner from keybindings + theme
537
+ const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
538
+ // Build startup instructions using keybinding hint helpers
539
+ const hint = (keybinding, description) => keyHint(keybinding, description);
540
+ const expandedInstructions = [
541
+ hint("app.interrupt", "to interrupt"),
542
+ hint("app.clear", "to clear"),
543
+ rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
544
+ hint("app.exit", "to exit (empty)"),
545
+ hint("app.suspend", "to suspend"),
546
+ keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
547
+ hint("app.thinking.cycle", "to cycle thinking level"),
548
+ rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
549
+ hint("app.model.select", "to select model"),
550
+ hint("app.tools.expand", "to expand tools"),
551
+ hint("app.thinking.toggle", "to expand thinking"),
552
+ hint("app.editor.external", "for external editor"),
553
+ rawKeyHint("/", "for commands"),
554
+ rawKeyHint("!", "to run bash"),
555
+ rawKeyHint("!!", "to run bash (no context)"),
556
+ hint("app.message.followUp", "to queue follow-up"),
557
+ hint("app.message.dequeue", "to edit all queued messages"),
558
+ hint("app.clipboard.pasteImage", "to paste image"),
559
+ rawKeyHint("drop files", "to attach"),
560
+ ].join("\n");
561
+ const compactInstructions = [
562
+ hint("app.interrupt", "interrupt"),
563
+ rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
564
+ rawKeyHint("/", "commands"),
565
+ rawKeyHint("!", "bash"),
566
+ hint("app.tools.expand", "more"),
567
+ ].join(theme.fg("muted", " · "));
568
+ const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
569
+ const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
570
+ this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
571
+ }
430
572
  // Setup UI layout
431
573
  this.headerContainer.addChild(new Spacer(1));
432
574
  this.headerContainer.addChild(this.builtInHeader);
@@ -437,6 +579,50 @@ export class InteractiveMode {
437
579
  this.builtInHeader = new Text("", 0, 0);
438
580
  this.headerContainer.addChild(this.builtInHeader);
439
581
  }
582
+ // In connect mode, render loaded resources from banner data into chat container
583
+ if (this.options.banner) {
584
+ const banner = this.options.banner;
585
+ // Loaded resource sections
586
+ for (const section of banner.loadedResources) {
587
+ const sectionHeader = theme.fg("mdHeading", `[${section.name}]`);
588
+ const compactBody = theme.fg("dim", ` ${section.compactList}`);
589
+ const expandedBody = theme.fg("dim", section.expandedList
590
+ .split("\n")
591
+ .map((l) => ` ${l}`)
592
+ .join("\n"));
593
+ const expandable = new ExpandableText(() => `${sectionHeader}\n${compactBody}`, () => `${sectionHeader}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
594
+ this.chatContainer.addChild(expandable);
595
+ this.chatContainer.addChild(new Spacer(1));
596
+ }
597
+ // Diagnostics (Skill conflicts, Prompt conflicts, etc.)
598
+ for (const diag of banner.diagnostics) {
599
+ const lines = [];
600
+ for (const entry of diag.entries) {
601
+ if (entry.type === "collision" && entry.collision) {
602
+ const c = entry.collision;
603
+ lines.push(` "${c.name}" collision:`);
604
+ lines.push(` ✓ auto (${c.winnerSource ?? "local"}) ${c.winnerPath}`);
605
+ lines.push(` ✗ ${c.loserPath} (skipped)`);
606
+ }
607
+ else {
608
+ lines.push(` ${entry.message}`);
609
+ }
610
+ }
611
+ if (lines.length > 0) {
612
+ this.chatContainer.addChild(new Text(`${theme.fg("warning", `[${diag.label}]`)}\n${lines.join("\n")}`, 0, 0));
613
+ this.chatContainer.addChild(new Spacer(1));
614
+ }
615
+ }
616
+ // What's New (changelog)
617
+ if (banner.changelogMarkdown) {
618
+ this.chatContainer.addChild(new DynamicBorder());
619
+ this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
620
+ this.chatContainer.addChild(new Spacer(1));
621
+ this.chatContainer.addChild(new Markdown(banner.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()));
622
+ this.chatContainer.addChild(new Spacer(1));
623
+ this.chatContainer.addChild(new DynamicBorder());
624
+ }
625
+ }
440
626
  this.ui.addChild(this.chatContainer);
441
627
  this.ui.addChild(this.pendingMessagesContainer);
442
628
  this.ui.addChild(this.statusContainer);
@@ -452,9 +638,42 @@ export class InteractiveMode {
452
638
  this.ui.start();
453
639
  this.isInitialized = true;
454
640
  // Initialize extensions first so resources are shown before messages
455
- await this.rebindCurrentSession();
641
+ if (this.runtimeHost) {
642
+ await this.rebindCurrentSession();
643
+ }
644
+ else if (this.proxy) {
645
+ this.subscribeToAgent();
646
+ // Set initial footer snapshot from proxy
647
+ const snapshot = this.proxy.getSnapshot();
648
+ this.footer.setSnapshot(snapshot);
649
+ this.footerDataProvider.setCwd(snapshot.cwd);
650
+ this.footerDataProvider.setAvailableProviderCount(snapshot.availableProviderCount);
651
+ // If agent is already streaming, create the working loader
652
+ // (agent_start event fired before we subscribed)
653
+ if (snapshot.isStreaming) {
654
+ this.loadingAnimation = this.createWorkingLoader();
655
+ this.statusContainer.addChild(this.loadingAnimation);
656
+ }
657
+ // Apply remote settings to editor
658
+ if (snapshot.remoteSettings) {
659
+ const paddingX = snapshot.remoteSettings.editorPaddingX;
660
+ this.defaultEditor.setPaddingX(paddingX);
661
+ if (this.editor !== this.defaultEditor) {
662
+ this.editor.setPaddingX?.(paddingX);
663
+ }
664
+ }
665
+ // Set up autocomplete for connect mode with a reduced command set
666
+ this.setupConnectModeAutocomplete();
667
+ // Load client-side extensions for UI (shortcuts, widgets, status, etc.)
668
+ await this.bindConnectModeExtensions();
669
+ }
456
670
  // Render initial messages AFTER showing loaded resources
457
- this.renderInitialMessages();
671
+ if (this.runtimeHost) {
672
+ this.renderInitialMessages();
673
+ }
674
+ else if (this.proxy) {
675
+ this.renderInitialMessages();
676
+ }
458
677
  // Set up theme file watcher
459
678
  onThemeChange(() => {
460
679
  this.ui.invalidate();
@@ -466,19 +685,27 @@ export class InteractiveMode {
466
685
  this.ui.requestRender();
467
686
  });
468
687
  // Initialize available provider count for footer display
469
- await this.updateAvailableProviderCount();
688
+ // In connect mode, skip since we don't have a model registry
689
+ if (this.runtimeHost) {
690
+ await this.updateAvailableProviderCount();
691
+ }
470
692
  }
471
693
  /**
472
694
  * Update terminal title with session name and cwd.
473
695
  */
474
696
  updateTerminalTitle() {
475
- const cwdBasename = path.basename(this.sessionManager.getCwd());
476
- const sessionName = this.sessionManager.getSessionName();
477
- if (sessionName) {
478
- this.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);
697
+ const cwdBasename = path.basename(this.currentCwd);
698
+ if (this.runtimeHost) {
699
+ const sessionName = this.sessionManager.getSessionName();
700
+ if (sessionName) {
701
+ this.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);
702
+ }
703
+ else {
704
+ this.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);
705
+ }
479
706
  }
480
707
  else {
481
- this.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);
708
+ this.ui.terminal.setTitle(`${APP_TITLE} (connect) - ${cwdBasename}`);
482
709
  }
483
710
  }
484
711
  /**
@@ -487,51 +714,64 @@ export class InteractiveMode {
487
714
  */
488
715
  async run() {
489
716
  await this.init();
490
- // Start version check asynchronously
491
- checkForNewPiVersion(this.version).then((newVersion) => {
492
- if (newVersion) {
493
- this.showNewVersionNotification(newVersion);
494
- }
495
- });
496
- // Start package update check asynchronously
497
- this.checkForPackageUpdates().then((updates) => {
498
- if (updates.length > 0) {
499
- this.showPackageUpdateNotification(updates);
500
- }
501
- });
502
- // Check tmux keyboard setup asynchronously
503
- this.checkTmuxKeyboardSetup().then((warning) => {
504
- if (warning) {
505
- this.showWarning(warning);
506
- }
507
- });
717
+ // Start version check asynchronously (skip in connect mode)
718
+ if (this.runtimeHost) {
719
+ checkForNewPiVersion(this.version).then((newRelease) => {
720
+ if (newRelease) {
721
+ this.showNewVersionNotification(newRelease);
722
+ }
723
+ });
724
+ // Start package update check asynchronously
725
+ this.checkForPackageUpdates().then((updates) => {
726
+ if (updates.length > 0) {
727
+ this.showPackageUpdateNotification(updates);
728
+ }
729
+ });
730
+ // Check tmux keyboard setup asynchronously
731
+ this.checkTmuxKeyboardSetup().then((warning) => {
732
+ if (warning) {
733
+ this.showWarning(warning);
734
+ }
735
+ });
736
+ }
508
737
  // Show startup warnings
509
738
  const { migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages } = this.options;
510
739
  if (migratedProviders && migratedProviders.length > 0) {
511
740
  this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
512
741
  }
513
- const modelsJsonError = this.session.modelRegistry.getError();
514
- if (modelsJsonError) {
515
- this.showError(`models.json error: ${modelsJsonError}`);
742
+ if (this.runtimeHost) {
743
+ const modelsJsonError = this.session.modelRegistry.getError();
744
+ if (modelsJsonError) {
745
+ this.showError(`models.json error: ${modelsJsonError}`);
746
+ }
516
747
  }
517
748
  if (modelFallbackMessage) {
518
749
  this.showWarning(modelFallbackMessage);
519
750
  }
520
- void this.maybeWarnAboutAnthropicSubscriptionAuth();
751
+ if (this.runtimeHost) {
752
+ void this.maybeWarnAboutAnthropicSubscriptionAuth();
753
+ }
521
754
  // Process initial messages
522
755
  if (initialMessage) {
523
756
  try {
524
- await this.session.prompt(initialMessage, { images: initialImages });
757
+ if (this.proxy) {
758
+ // Proxy uses a different image format; convert or skip images
759
+ await this.proxy.prompt(initialMessage);
760
+ }
761
+ else {
762
+ await this.session.prompt(initialMessage, { images: initialImages });
763
+ }
525
764
  }
526
765
  catch (error) {
527
766
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
528
767
  this.showError(errorMessage);
529
768
  }
530
769
  }
770
+ const cmdTarget = this.proxy ?? this.session;
531
771
  if (initialMessages) {
532
772
  for (const message of initialMessages) {
533
773
  try {
534
- await this.session.prompt(message);
774
+ await cmdTarget.prompt(message);
535
775
  }
536
776
  catch (error) {
537
777
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -543,7 +783,7 @@ export class InteractiveMode {
543
783
  while (true) {
544
784
  const userInput = await this.getUserInput();
545
785
  try {
546
- await this.session.prompt(userInput);
786
+ await cmdTarget.prompt(userInput);
547
787
  }
548
788
  catch (error) {
549
789
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -654,7 +894,7 @@ export class InteractiveMode {
654
894
  getMarkdownThemeWithSettings() {
655
895
  return {
656
896
  ...getMarkdownTheme(),
657
- codeBlockIndent: this.settingsManager.getCodeBlockIndent(),
897
+ codeBlockIndent: this.runtimeHost ? this.settingsManager.getCodeBlockIndent() : " ",
658
898
  };
659
899
  }
660
900
  // =========================================================================
@@ -1092,6 +1332,9 @@ export class InteractiveMode {
1092
1332
  const uiContext = this.createExtensionUIContext();
1093
1333
  await this.session.bindExtensions({
1094
1334
  uiContext,
1335
+ abortHandler: () => {
1336
+ this.restoreQueuedMessagesToEditor({ abort: true });
1337
+ },
1095
1338
  commandContextActions: {
1096
1339
  waitForIdle: () => this.session.agent.waitForIdle(),
1097
1340
  newSession: async (options) => {
@@ -1169,7 +1412,94 @@ export class InteractiveMode {
1169
1412
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1170
1413
  this.showStartupNoticesIfNeeded();
1171
1414
  }
1415
+ /**
1416
+ * Load and bind client-side extensions for connect mode.
1417
+ * Extensions run with a real UI context but stub session/model access.
1418
+ * Tool and event registrations are no-ops (handled by the server).
1419
+ */
1420
+ async bindConnectModeExtensions() {
1421
+ if (!this.proxy)
1422
+ return;
1423
+ const snapshot = this.proxy.getSnapshot();
1424
+ const remoteClientExtensionsUrl = this.options.remoteClientExtensionsUrl;
1425
+ if (!remoteClientExtensionsUrl)
1426
+ return;
1427
+ try {
1428
+ const cwd = snapshot.cwd || process.cwd();
1429
+ // Load server-synchronized client extensions. Connect mode intentionally
1430
+ // does not load local extension paths; the server is the source of truth.
1431
+ const result = await loadRemoteClientExtensions(remoteClientExtensionsUrl, cwd, this.options.remoteClientExtensionHeaders);
1432
+ if (result.extensions.length === 0)
1433
+ return;
1434
+ const runner = ExtensionRunnerImpl.createClientSide(result.extensions, result.runtime, cwd);
1435
+ // Bind core actions — most are no-ops since the server handles them.
1436
+ // Only UI-facing actions need real implementations.
1437
+ runner.bindCore({
1438
+ sendMessage: () => { },
1439
+ sendUserMessage: () => { },
1440
+ appendEntry: () => { },
1441
+ setSessionName: () => { },
1442
+ getSessionName: () => undefined,
1443
+ setLabel: () => { },
1444
+ getActiveTools: () => [],
1445
+ getAllTools: () => [],
1446
+ setActiveTools: () => { },
1447
+ refreshTools: () => { },
1448
+ getCommands: () => [],
1449
+ setModel: () => Promise.resolve(false),
1450
+ getThinkingLevel: () => "medium",
1451
+ setThinkingLevel: () => { },
1452
+ }, {
1453
+ getModel: () => undefined,
1454
+ isIdle: () => true,
1455
+ getSignal: () => undefined,
1456
+ abort: () => {
1457
+ this.proxy.abort();
1458
+ },
1459
+ hasPendingMessages: () => false,
1460
+ shutdown: () => {
1461
+ void this.shutdown();
1462
+ },
1463
+ getContextUsage: () => undefined,
1464
+ compact: () => { },
1465
+ getSystemPrompt: () => "",
1466
+ }, {
1467
+ registerProvider: () => { },
1468
+ unregisterProvider: () => { },
1469
+ });
1470
+ // Bind command context — delegate to proxy
1471
+ runner.bindCommandContext({
1472
+ waitForIdle: () => Promise.resolve(),
1473
+ newSession: async () => {
1474
+ await this.proxy.newSession();
1475
+ return { cancelled: false };
1476
+ },
1477
+ fork: async () => {
1478
+ await this.proxy.fork();
1479
+ return { cancelled: false };
1480
+ },
1481
+ navigateTree: async () => ({ cancelled: false }),
1482
+ switchSession: async () => ({ cancelled: false }),
1483
+ reload: async () => {
1484
+ await this.proxy.reload();
1485
+ },
1486
+ });
1487
+ // Set the real UI context so extensions can interact with the TUI
1488
+ const uiContext = this.createExtensionUIContext();
1489
+ runner.setUIContext(uiContext);
1490
+ // Set up extension shortcuts
1491
+ this.setupExtensionShortcuts(runner);
1492
+ this._clientExtensionRunner = runner;
1493
+ // Emit session_start so extensions can initialize their UI
1494
+ await runner.emit({ type: "session_start", reason: "startup" });
1495
+ }
1496
+ catch (err) {
1497
+ // Non-fatal: extensions are best-effort in connect mode
1498
+ process.stderr.write(`[connect] Failed to load client-side extensions: ${err instanceof Error ? err.message : String(err)}\n`);
1499
+ }
1500
+ }
1172
1501
  applyRuntimeSettings() {
1502
+ configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
1173
1503
  this.footer.setSession(this.session);
1174
1504
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1175
1505
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
@@ -1215,7 +1545,7 @@ export class InteractiveMode {
1215
1545
  * Get a registered tool definition by name (for custom rendering).
1216
1546
  */
1217
1547
  getRegisteredToolDefinition(toolName) {
1218
- return this.session.getToolDefinition(toolName);
1548
+ return this.session?.getToolDefinition(toolName);
1219
1549
  }
1220
1550
  /**
1221
1551
  * Set up keyboard shortcuts registered by extensions.
@@ -1234,7 +1564,9 @@ export class InteractiveMode {
1234
1564
  model: this.session.model,
1235
1565
  isIdle: () => !this.session.isStreaming,
1236
1566
  signal: this.session.agent.signal,
1237
- abort: () => this.session.abort(),
1567
+ abort: () => {
1568
+ this.restoreQueuedMessagesToEditor({ abort: true });
1569
+ },
1238
1570
  hasPendingMessages: () => this.session.pendingMessageCount > 0,
1239
1571
  shutdown: () => {
1240
1572
  this.shutdownRequested = true;
@@ -1397,6 +1729,11 @@ export class InteractiveMode {
1397
1729
  this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1398
1730
  }
1399
1731
  this.setHiddenThinkingLabel();
1732
+ // Invalidate client-side extension runner (connect mode)
1733
+ if (this._clientExtensionRunner) {
1734
+ this._clientExtensionRunner.invalidate("Session replaced — extension context is stale");
1735
+ this._clientExtensionRunner = undefined;
1736
+ }
1400
1737
  }
1401
1738
  // Maximum total widget lines to prevent viewport overflow
1402
1739
  static MAX_WIDGET_LINES = 10;
@@ -1509,8 +1846,54 @@ export class InteractiveMode {
1509
1846
  }
1510
1847
  /**
1511
1848
  * Create the ExtensionUIContext for extensions.
1849
+ * In connect mode (proxy set), returns a headless implementation that rejects interactive dialogs.
1512
1850
  */
1513
1851
  createExtensionUIContext() {
1852
+ // Connect mode: real UI for non-interactive operations, defaults for dialogs
1853
+ if (this.proxy) {
1854
+ return {
1855
+ // Interactive dialogs — select is real TUI, others return defaults
1856
+ select: (title, options, opts) => this.showExtensionSelector(title, options, opts),
1857
+ confirm: () => Promise.resolve(false),
1858
+ input: () => Promise.resolve(undefined),
1859
+ custom: () => Promise.resolve(undefined),
1860
+ editor: () => Promise.resolve(undefined),
1861
+ // Non-interactive UI — real TUI implementations
1862
+ notify: (message, type) => this.showExtensionNotify(message, type),
1863
+ onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
1864
+ setStatus: (key, text) => this.setExtensionStatus(key, text),
1865
+ setWorkingMessage: (message) => {
1866
+ this.workingMessage = message;
1867
+ if (this.loadingAnimation) {
1868
+ this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
1869
+ }
1870
+ },
1871
+ setWorkingVisible: (visible) => this.setWorkingVisible(visible),
1872
+ setWorkingIndicator: (options) => this.setWorkingIndicator(options),
1873
+ setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),
1874
+ setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
1875
+ setFooter: (factory) => this.setExtensionFooter(factory),
1876
+ setHeader: (factory) => this.setExtensionHeader(factory),
1877
+ setTitle: (title) => this.ui.terminal.setTitle(title),
1878
+ pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
1879
+ setEditorText: (text) => this.editor.setText(text),
1880
+ getEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),
1881
+ addAutocompleteProvider: (factory) => {
1882
+ this.autocompleteProviderWrappers.push(factory);
1883
+ this.setupAutocompleteProvider();
1884
+ },
1885
+ setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1886
+ getEditorComponent: () => this.editorComponentFactory,
1887
+ get theme() {
1888
+ return theme;
1889
+ },
1890
+ getAllThemes: () => [],
1891
+ getTheme: () => undefined,
1892
+ setTheme: () => ({ success: false }),
1893
+ getToolsExpanded: () => this.toolOutputExpanded,
1894
+ setToolsExpanded: (expanded) => this.setToolsExpanded(expanded),
1895
+ };
1896
+ }
1514
1897
  return {
1515
1898
  select: (title, options, opts) => this.showExtensionSelector(title, options, opts),
1516
1899
  confirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),
@@ -1855,11 +2238,18 @@ export class InteractiveMode {
1855
2238
  // Set up handlers on defaultEditor - they use this.editor for text access
1856
2239
  // so they work correctly regardless of which editor is active
1857
2240
  this.defaultEditor.onEscape = () => {
1858
- if (this.session.isStreaming) {
2241
+ const isStreaming = this.proxy ? this.proxy.isStreaming : this.session.isStreaming;
2242
+ const isBashRunning = this.proxy ? this.proxy.isBashRunning : this.session.isBashRunning;
2243
+ if (isStreaming) {
1859
2244
  this.restoreQueuedMessagesToEditor({ abort: true });
1860
2245
  }
1861
- else if (this.session.isBashRunning) {
1862
- this.session.abortBash();
2246
+ else if (isBashRunning) {
2247
+ if (this.proxy) {
2248
+ this.proxy.abortBash();
2249
+ }
2250
+ else {
2251
+ this.session.abortBash();
2252
+ }
1863
2253
  }
1864
2254
  else if (this.isBashMode) {
1865
2255
  this.editor.setText("");
@@ -1868,7 +2258,7 @@ export class InteractiveMode {
1868
2258
  }
1869
2259
  else if (!this.editor.getText().trim()) {
1870
2260
  // Double-escape with empty editor triggers /tree, /fork, or nothing based on setting
1871
- const action = this.settingsManager.getDoubleEscapeAction();
2261
+ const action = this.settingsManager?.getDoubleEscapeAction() ?? "tree";
1872
2262
  if (action !== "none") {
1873
2263
  const now = Date.now();
1874
2264
  if (now - this.lastEscapeTime < 500) {
@@ -1923,7 +2313,16 @@ export class InteractiveMode {
1923
2313
  if (!image) {
1924
2314
  return;
1925
2315
  }
1926
- // Write to temp file
2316
+ if (this.proxy) {
2317
+ // Connect mode: send as base64 data URL via prompt images
2318
+ const base64 = Buffer.from(image.bytes).toString("base64");
2319
+ const dataUrl = `data:${image.mimeType};base64,${base64}`;
2320
+ const text = this.editor.getText().trim() || "Look at this image";
2321
+ await this.proxy.prompt(text, { images: [{ url: dataUrl, mediaType: image.mimeType }] });
2322
+ this.editor.setText("");
2323
+ return;
2324
+ }
2325
+ // Local mode: write to temp file and insert path
1927
2326
  const tmpDir = os.tmpdir();
1928
2327
  const ext = extensionForImageMimeType(image.mimeType) ?? "png";
1929
2328
  const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
@@ -1942,6 +2341,13 @@ export class InteractiveMode {
1942
2341
  text = text.trim();
1943
2342
  if (!text)
1944
2343
  return;
2344
+ // In connect mode, handle commands via proxy
2345
+ if (this.proxy) {
2346
+ if (text.startsWith("/")) {
2347
+ await this.handleConnectModeCommand(text);
2348
+ return;
2349
+ }
2350
+ }
1945
2351
  // Handle commands
1946
2352
  if (text === "/settings") {
1947
2353
  this.showSettingsSelector();
@@ -2067,6 +2473,11 @@ export class InteractiveMode {
2067
2473
  }
2068
2474
  // Handle bash command (! for normal, !! for excluded from context)
2069
2475
  if (text.startsWith("!")) {
2476
+ if (this.proxy) {
2477
+ this.showWarning("Bash commands are not supported in connect mode.");
2478
+ this.editor.setText(text);
2479
+ return;
2480
+ }
2070
2481
  const isExcluded = text.startsWith("!!");
2071
2482
  const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
2072
2483
  if (command) {
@@ -2083,8 +2494,8 @@ export class InteractiveMode {
2083
2494
  }
2084
2495
  }
2085
2496
  // Queue input during compaction (extension commands execute immediately)
2086
- if (this.session.isCompacting) {
2087
- if (this.isExtensionCommand(text)) {
2497
+ if (this.proxy ? this.proxy.isCompacting : this.session.isCompacting) {
2498
+ if (!this.proxy && this.isExtensionCommand(text)) {
2088
2499
  this.editor.addToHistory?.(text);
2089
2500
  this.editor.setText("");
2090
2501
  await this.session.prompt(text);
@@ -2096,10 +2507,15 @@ export class InteractiveMode {
2096
2507
  }
2097
2508
  // If streaming, use prompt() with steer behavior
2098
2509
  // This handles extension commands (execute immediately), prompt template expansion, and queueing
2099
- if (this.session.isStreaming) {
2510
+ if (this.proxy ? this.proxy.isStreaming : this.session.isStreaming) {
2100
2511
  this.editor.addToHistory?.(text);
2101
2512
  this.editor.setText("");
2102
- await this.session.prompt(text, { streamingBehavior: "steer" });
2513
+ if (this.proxy) {
2514
+ this.proxy.steer(text);
2515
+ }
2516
+ else {
2517
+ await this.session.prompt(text, { streamingBehavior: "steer" });
2518
+ }
2103
2519
  this.updatePendingMessagesDisplay();
2104
2520
  this.ui.requestRender();
2105
2521
  return;
@@ -2110,11 +2526,117 @@ export class InteractiveMode {
2110
2526
  if (this.onInputCallback) {
2111
2527
  this.onInputCallback(text);
2112
2528
  }
2529
+ else {
2530
+ this.pendingUserInputs.push(text);
2531
+ }
2113
2532
  this.editor.addToHistory?.(text);
2114
2533
  };
2115
2534
  }
2535
+ /** Handle slash commands in connect mode */
2536
+ async handleConnectModeCommand(text) {
2537
+ this.editor.setText("");
2538
+ const cmd = text.split(" ")[0];
2539
+ const arg = text.slice(cmd.length + 1).trim() || undefined;
2540
+ switch (cmd) {
2541
+ case "/quit":
2542
+ await this.shutdown();
2543
+ break;
2544
+ case "/compact":
2545
+ await this.proxy.compact();
2546
+ break;
2547
+ case "/model":
2548
+ if (arg) {
2549
+ this.proxy.setModel(arg);
2550
+ this.showStatus(`Model: ${arg}`);
2551
+ }
2552
+ else {
2553
+ this.showConnectModeModelSelector();
2554
+ }
2555
+ break;
2556
+ case "/scoped-models":
2557
+ this.showConnectModeScopedModelsSelector();
2558
+ break;
2559
+ case "/settings":
2560
+ this.showConnectModeSettingsSelector();
2561
+ break;
2562
+ case "/export":
2563
+ this.showStatus("Not available in connect mode");
2564
+ break;
2565
+ case "/import":
2566
+ this.showStatus("Not available in connect mode");
2567
+ break;
2568
+ case "/share":
2569
+ this.showStatus("Not available in connect mode");
2570
+ break;
2571
+ case "/login":
2572
+ this.showStatus("Not available in connect mode — configure auth on the server");
2573
+ break;
2574
+ case "/logout":
2575
+ this.showStatus("Not available in connect mode — configure auth on the server");
2576
+ break;
2577
+ case "/new":
2578
+ await this.proxy.newSession();
2579
+ // UI reset is handled by the session_replaced event
2580
+ break;
2581
+ case "/fork":
2582
+ this.showConnectModeForkSelector();
2583
+ break;
2584
+ case "/tree":
2585
+ this.showConnectModeTreeSelector();
2586
+ break;
2587
+ case "/clone":
2588
+ await this.proxy.fork();
2589
+ // UI reset is handled by the session_replaced event
2590
+ break;
2591
+ case "/resume":
2592
+ this.showConnectModeSessionSelector();
2593
+ break;
2594
+ case "/name":
2595
+ if (arg) {
2596
+ this.proxy.renameSession(arg);
2597
+ this.showStatus(`Session renamed to: ${arg}`);
2598
+ }
2599
+ else {
2600
+ this.showWarning("Usage: /name <session-name>");
2601
+ }
2602
+ break;
2603
+ case "/copy":
2604
+ this.handleConnectModeCopy();
2605
+ break;
2606
+ case "/reload":
2607
+ await this.proxy.reload();
2608
+ this.setupConnectModeAutocomplete();
2609
+ this.showStatus("Reloaded");
2610
+ break;
2611
+ case "/hotkeys":
2612
+ this.handleHotkeysCommand();
2613
+ break;
2614
+ case "/changelog":
2615
+ this.handleChangelogCommand();
2616
+ break;
2617
+ case "/session":
2618
+ this.showConnectModeSessionInfo();
2619
+ break;
2620
+ default: {
2621
+ // Check client-side extension runner first
2622
+ if (this._clientExtensionRunner) {
2623
+ const cmdName = cmd.startsWith("/") ? cmd.slice(1) : cmd;
2624
+ const resolvedCmd = this._clientExtensionRunner.getCommand(cmdName);
2625
+ if (resolvedCmd) {
2626
+ const ctx = this._clientExtensionRunner.createCommandContext();
2627
+ await resolvedCmd.handler(arg ?? "", ctx);
2628
+ break;
2629
+ }
2630
+ }
2631
+ // Non-builtin commands (skill, prompt template, extension) are sent as prompts.
2632
+ // The server's agent-session handles skill expansion and prompt template resolution.
2633
+ await this.proxy.prompt(text);
2634
+ }
2635
+ }
2636
+ }
2116
2637
  subscribeToAgent() {
2117
- this.unsubscribe = this.session.subscribe(async (event) => {
2638
+ const eventSource = this.proxy ?? this.session;
2639
+ this.unsubscribe = eventSource.subscribe(async (event) => {
2118
2640
  await this.handleEvent(event);
2119
2641
  });
2120
2642
  }
@@ -2126,7 +2648,7 @@ export class InteractiveMode {
2126
2648
  switch (event.type) {
2127
2649
  case "agent_start":
2128
2650
  this.pendingTools.clear();
2129
- if (this.settingsManager.getShowTerminalProgress()) {
2651
+ if (this.showTerminalProgress) {
2130
2652
  this.ui.terminal.setProgress(true);
2131
2653
  }
2132
2654
  // Restore main escape handler if retry handler is still active
@@ -2189,9 +2711,9 @@ export class InteractiveMode {
2189
2711
  if (content.type === "toolCall") {
2190
2712
  if (!this.pendingTools.has(content.id)) {
2191
2713
  const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
2192
- showImages: this.settingsManager.getShowImages(),
2193
- imageWidthCells: this.settingsManager.getImageWidthCells(),
2194
- }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2714
+ showImages: this.showImages,
2715
+ imageWidthCells: this.imageWidthCells,
2716
+ }, this.getRegisteredToolDefinition(content.name), this.ui, this.currentCwd);
2195
2717
  component.setExpanded(this.toolOutputExpanded);
2196
2718
  this.chatContainer.addChild(component);
2197
2719
  this.pendingTools.set(content.id, component);
@@ -2214,7 +2736,7 @@ export class InteractiveMode {
2214
2736
  this.streamingMessage = event.message;
2215
2737
  let errorMessage;
2216
2738
  if (this.streamingMessage.stopReason === "aborted") {
2217
- const retryAttempt = this.session.retryAttempt;
2739
+ const retryAttempt = this.runtimeHost ? this.session.retryAttempt : 0;
2218
2740
  errorMessage =
2219
2741
  retryAttempt > 0
2220
2742
  ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
@@ -2250,9 +2772,9 @@ export class InteractiveMode {
2250
2772
  let component = this.pendingTools.get(event.toolCallId);
2251
2773
  if (!component) {
2252
2774
  component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
2253
- showImages: this.settingsManager.getShowImages(),
2254
- imageWidthCells: this.settingsManager.getImageWidthCells(),
2255
- }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
2775
+ showImages: this.showImages,
2776
+ imageWidthCells: this.imageWidthCells,
2777
+ }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.currentCwd);
2256
2778
  component.setExpanded(this.toolOutputExpanded);
2257
2779
  this.chatContainer.addChild(component);
2258
2780
  this.pendingTools.set(event.toolCallId, component);
@@ -2279,7 +2801,7 @@ export class InteractiveMode {
2279
2801
  break;
2280
2802
  }
2281
2803
  case "agent_end":
2282
- if (this.settingsManager.getShowTerminalProgress()) {
2804
+ if (this.showTerminalProgress) {
2283
2805
  this.ui.terminal.setProgress(false);
2284
2806
  }
2285
2807
  if (this.loadingAnimation) {
@@ -2296,14 +2818,70 @@ export class InteractiveMode {
2296
2818
  await this.checkShutdownRequested();
2297
2819
  this.ui.requestRender();
2298
2820
  break;
2821
+ case "turn_stats": {
2822
+ const { tps, output, input, cacheRead, cacheWrite, total, duration } = event;
2823
+ const parts = [];
2824
+ if (output > 0) {
2825
+ parts.push(`TPS ${tps.toFixed(1)} tok/s`);
2826
+ }
2827
+ parts.push(`out ${formatTokens(output)}`);
2828
+ parts.push(`in ${formatTokens(input)}`);
2829
+ if (cacheRead > 0 || cacheWrite > 0) {
2830
+ parts.push(`cache r/w ${formatTokens(cacheRead)}/${formatTokens(cacheWrite)}`);
2831
+ }
2832
+ parts.push(`total ${formatTokens(total)}`);
2833
+ parts.push(`${duration.toFixed(1)}s`);
2834
+ this.showStatus(parts.join(", "));
2835
+ break;
2836
+ }
2837
+ case "session_replaced": {
2838
+ // Server session was replaced (new/resume/fork) — reset UI and render new session
2839
+ this.chatContainer.clear();
2840
+ this.pendingMessagesContainer.clear();
2841
+ this.compactionQueuedMessages = [];
2842
+ this.pendingTools.clear();
2843
+ if (this.loadingAnimation) {
2844
+ this.loadingAnimation.stop();
2845
+ this.loadingAnimation = undefined;
2846
+ }
2847
+ this.statusContainer.clear();
2848
+ this.streamingComponent = undefined;
2849
+ this.streamingMessage = undefined;
2850
+ // Reset extension UI and reload client-side extensions
2851
+ this.resetExtensionUI();
2852
+ // Update footer with new session state
2853
+ const snapshot = this.proxy.getSnapshot();
2854
+ this.footer.setSnapshot(snapshot);
2855
+ this.footerDataProvider.setCwd(snapshot.cwd);
2856
+ this.footerDataProvider.setAvailableProviderCount(snapshot.availableProviderCount);
2857
+ // Re-bind client-side extensions for the new session
2858
+ void this.bindConnectModeExtensions();
2859
+ // Render messages from the new session
2860
+ this.renderInitialMessages();
2861
+ // Show confirmation
2862
+ const label = event.reason === "new"
2863
+ ? "New session started"
2864
+ : event.reason === "resume"
2865
+ ? "Session resumed"
2866
+ : "Forked to new session";
2867
+ this.chatContainer.addChild(new Spacer(1));
2868
+ this.chatContainer.addChild(new Text(theme.fg("accent", `✓ ${label}`), 1, 1));
2869
+ this.ui.requestRender();
2870
+ break;
2871
+ }
2299
2872
  case "compaction_start": {
2300
- if (this.settingsManager.getShowTerminalProgress()) {
2873
+ if (this.showTerminalProgress) {
2301
2874
  this.ui.terminal.setProgress(true);
2302
2875
  }
2303
2876
  // Keep editor active; submissions are queued during compaction.
2304
2877
  this.autoCompactionEscapeHandler = this.defaultEditor.onEscape;
2305
2878
  this.defaultEditor.onEscape = () => {
2306
- this.session.abortCompaction();
2879
+ if (this.proxy) {
2880
+ this.proxy.abort();
2881
+ }
2882
+ else {
2883
+ this.session.abortCompaction();
2884
+ }
2307
2885
  };
2308
2886
  this.statusContainer.clear();
2309
2887
  const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
@@ -2316,7 +2894,7 @@ export class InteractiveMode {
2316
2894
  break;
2317
2895
  }
2318
2896
  case "compaction_end": {
2319
- if (this.settingsManager.getShowTerminalProgress()) {
2897
+ if (this.showTerminalProgress) {
2320
2898
  this.ui.terminal.setProgress(false);
2321
2899
  }
2322
2900
  if (this.autoCompactionEscapeHandler) {
@@ -2355,11 +2933,33 @@ export class InteractiveMode {
2355
2933
  this.ui.requestRender();
2356
2934
  break;
2357
2935
  }
2936
+ case "state_update": {
2937
+ // Update footer snapshot for connect mode
2938
+ if (this.proxy && event.snapshot) {
2939
+ const currentSnapshot = this.proxy.getSnapshot();
2940
+ const merged = { ...currentSnapshot, ...event.snapshot };
2941
+ this.footer.setSnapshot(merged);
2942
+ if (event.snapshot.cwd !== undefined) {
2943
+ this.footerDataProvider.setCwd(event.snapshot.cwd);
2944
+ }
2945
+ if (event.snapshot.availableProviderCount !== undefined) {
2946
+ this.footerDataProvider.setAvailableProviderCount(event.snapshot.availableProviderCount);
2947
+ }
2948
+ }
2949
+ this.footer.invalidate();
2950
+ this.ui.requestRender();
2951
+ break;
2952
+ }
2358
2953
  case "auto_retry_start": {
2359
2954
  // Set up escape to abort retry
2360
2955
  this.retryEscapeHandler = this.defaultEditor.onEscape;
2361
2956
  this.defaultEditor.onEscape = () => {
2362
- this.session.abortRetry();
2957
+ if (this.proxy) {
2958
+ this.proxy.abort();
2959
+ }
2960
+ else {
2961
+ this.session.abortRetry();
2962
+ }
2363
2963
  };
2364
2964
  // Show retry indicator
2365
2965
  this.statusContainer.clear();
@@ -2404,12 +3004,9 @@ export class InteractiveMode {
2404
3004
  getUserMessageText(message) {
2405
3005
  if (message.role !== "user")
2406
3006
  return "";
2407
- return this.getTextContent(message.content);
2408
- }
2409
- getTextContent(content) {
2410
- const textBlocks = typeof content === "string"
2411
- ? [{ type: "text", text: content }]
2412
- : content.filter((c) => c.type === "text");
3007
+ const textBlocks = typeof message.content === "string"
3008
+ ? [{ type: "text", text: message.content }]
3009
+ : message.content.filter((c) => c.type === "text");
2413
3010
  return textBlocks.map((c) => c.text).join("");
2414
3011
  }
2415
3012
  /**
@@ -2436,21 +3033,24 @@ export class InteractiveMode {
2436
3033
  this.ui.requestRender();
2437
3034
  }
2438
3035
  addMessageToChat(message, options) {
2439
- const renderMessage = message;
2440
- switch (renderMessage.role) {
3036
+ switch (message.role) {
2441
3037
  case "bashExecution": {
2442
- const component = new BashExecutionComponent(renderMessage.command, this.ui, renderMessage.excludeFromContext);
2443
- if (renderMessage.output) {
2444
- component.appendOutput(renderMessage.output);
3038
+ const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
3039
+ if (message.output) {
3040
+ component.appendOutput(message.output);
2445
3041
  }
2446
- component.setComplete(renderMessage.exitCode, renderMessage.cancelled, renderMessage.truncated ? { truncated: true } : undefined, renderMessage.fullOutputPath);
3042
+ component.setComplete(message.exitCode, message.cancelled, message.truncated ? { truncated: true } : undefined, message.fullOutputPath);
2447
3043
  this.chatContainer.addChild(component);
2448
3044
  break;
2449
3045
  }
2450
3046
  case "custom": {
2451
- if (renderMessage.display) {
2452
- const renderer = this.session.extensionRunner.getMessageRenderer(renderMessage.customType);
2453
- const component = new CustomMessageComponent(renderMessage, renderer, this.getMarkdownThemeWithSettings());
3047
+ if (message.display) {
3048
+ if (this.chatContainer.children.length > 0) {
3049
+ this.chatContainer.addChild(new Spacer(1));
3050
+ }
3051
+ const renderer = this.session?.extensionRunner.getMessageRenderer(message.customType) ??
3052
+ this._clientExtensionRunner?.getMessageRenderer(message.customType);
3053
+ const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());
2454
3054
  component.setExpanded(this.toolOutputExpanded);
2455
3055
  this.chatContainer.addChild(component);
2456
3056
  }
@@ -2458,20 +3058,20 @@ export class InteractiveMode {
2458
3058
  }
2459
3059
  case "compactionSummary": {
2460
3060
  this.chatContainer.addChild(new Spacer(1));
2461
- const component = new CompactionSummaryMessageComponent(renderMessage, this.getMarkdownThemeWithSettings());
3061
+ const component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
2462
3062
  component.setExpanded(this.toolOutputExpanded);
2463
3063
  this.chatContainer.addChild(component);
2464
3064
  break;
2465
3065
  }
2466
3066
  case "branchSummary": {
2467
3067
  this.chatContainer.addChild(new Spacer(1));
2468
- const component = new BranchSummaryMessageComponent(renderMessage, this.getMarkdownThemeWithSettings());
3068
+ const component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
2469
3069
  component.setExpanded(this.toolOutputExpanded);
2470
3070
  this.chatContainer.addChild(component);
2471
3071
  break;
2472
3072
  }
2473
3073
  case "user": {
2474
- const textContent = this.getUserMessageText(renderMessage);
3074
+ const textContent = this.getUserMessageText(message);
2475
3075
  if (textContent) {
2476
3076
  if (this.chatContainer.children.length > 0) {
2477
3077
  this.chatContainer.addChild(new Spacer(1));
@@ -2498,25 +3098,8 @@ export class InteractiveMode {
2498
3098
  }
2499
3099
  break;
2500
3100
  }
2501
- case "user-with-attachments": {
2502
- const textContent = this.getTextContent(renderMessage.content);
2503
- if (textContent) {
2504
- if (this.chatContainer.children.length > 0) {
2505
- this.chatContainer.addChild(new Spacer(1));
2506
- }
2507
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
2508
- this.chatContainer.addChild(userComponent);
2509
- if (options?.populateHistory) {
2510
- this.editor.addToHistory?.(textContent);
2511
- }
2512
- }
2513
- break;
2514
- }
2515
- case "artifact": {
2516
- break;
2517
- }
2518
3101
  case "assistant": {
2519
- const assistantComponent = new AssistantMessageComponent(renderMessage, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
3102
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2520
3103
  this.chatContainer.addChild(assistantComponent);
2521
3104
  break;
2522
3105
  }
@@ -2525,7 +3108,7 @@ export class InteractiveMode {
2525
3108
  break;
2526
3109
  }
2527
3110
  default: {
2528
- const _exhaustive = renderMessage;
3111
+ const _exhaustive = message;
2529
3112
  }
2530
3113
  }
2531
3114
  }
@@ -2550,15 +3133,15 @@ export class InteractiveMode {
2550
3133
  for (const content of message.content) {
2551
3134
  if (content.type === "toolCall") {
2552
3135
  const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
2553
- showImages: this.settingsManager.getShowImages(),
2554
- imageWidthCells: this.settingsManager.getImageWidthCells(),
2555
- }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
3136
+ showImages: this.showImages,
3137
+ imageWidthCells: this.imageWidthCells,
3138
+ }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager?.getCwd() ?? this.currentCwd);
2556
3139
  component.setExpanded(this.toolOutputExpanded);
2557
3140
  this.chatContainer.addChild(component);
2558
3141
  if (message.stopReason === "aborted" || message.stopReason === "error") {
2559
3142
  let errorMessage;
2560
3143
  if (message.stopReason === "aborted") {
2561
- const retryAttempt = this.session.retryAttempt;
3144
+ const retryAttempt = this.session?.retryAttempt ?? 0;
2562
3145
  errorMessage =
2563
3146
  retryAttempt > 0
2564
3147
  ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
@@ -2594,6 +3177,11 @@ export class InteractiveMode {
2594
3177
  this.ui.requestRender();
2595
3178
  }
2596
3179
  renderInitialMessages() {
3180
+ if (this.proxy) {
3181
+ // Connect mode: render from proxy messages directly
3182
+ this.renderSessionContext({ messages: [...this.proxy.messages], thinkingLevel: "", model: null }, { updateFooter: true, populateHistory: true });
3183
+ return;
3184
+ }
2597
3185
  // Get aligned messages and entries from session context
2598
3186
  const context = this.sessionManager.buildSessionContext();
2599
3187
  this.renderSessionContext(context, {
@@ -2609,6 +3197,10 @@ export class InteractiveMode {
2609
3197
  }
2610
3198
  }
2611
3199
  async getUserInput() {
3200
+ const queuedInput = this.pendingUserInputs.shift();
3201
+ if (queuedInput !== undefined) {
3202
+ return queuedInput;
3203
+ }
2612
3204
  return new Promise((resolve) => {
2613
3205
  this.onInputCallback = (text) => {
2614
3206
  this.onInputCallback = undefined;
@@ -2618,8 +3210,13 @@ export class InteractiveMode {
2618
3210
  }
2619
3211
  rebuildChatFromMessages() {
2620
3212
  this.chatContainer.clear();
2621
- const context = this.sessionManager.buildSessionContext();
2622
- this.renderSessionContext(context);
3213
+ if (this.proxy) {
3214
+ this.renderSessionContext({ messages: [...this.proxy.messages], thinkingLevel: "", model: null });
3215
+ }
3216
+ else {
3217
+ const context = this.sessionManager.buildSessionContext();
3218
+ this.renderSessionContext(context);
3219
+ }
2623
3220
  }
2624
3221
  // =========================================================================
2625
3222
  // Key handlers
@@ -2644,16 +3241,44 @@ export class InteractiveMode {
2644
3241
  * repaint the final frame while the process is exiting.
2645
3242
  */
2646
3243
  isShuttingDown = false;
2647
- async shutdown() {
3244
+ async shutdown(options) {
2648
3245
  if (this.isShuttingDown)
2649
3246
  return;
2650
3247
  this.isShuttingDown = true;
2651
3248
  this.unregisterSignalHandlers();
3249
+ if (options?.fromSignal) {
3250
+ // Signal-triggered shutdown (SIGTERM/SIGHUP). Emit extension cleanup
3251
+ // (session_shutdown) BEFORE touching the terminal. Extension teardown
3252
+ // such as removing sockets does not write to the tty, so it must not be
3253
+ // skipped if a later terminal-restore write fails on a dead or stalled
3254
+ // terminal. If the terminal is gone, the restore writes below emit EIO,
3255
+ // which the stdout/stderr error handler turns into emergencyTerminalExit;
3256
+ // the render loop is already idle, so this cannot hot-spin (see #4144).
3257
+ await this.runtimeHost.dispose();
3258
+ await this.ui.terminal.drainInput(1000);
3259
+ this.stop();
3260
+ process.exit(0);
3261
+ }
3262
+ // Interactive quit (Ctrl+D, Ctrl+C, /quit, extension shutdown()). Stop the
3263
+ // TUI before emitting shutdown events so extension UI cleanup cannot repaint
3264
+ // the final frame while the process is exiting.
2652
3265
  // Drain any in-flight Kitty key release events before stopping.
2653
3266
  // This prevents escape sequences from leaking to the parent shell over slow SSH.
2654
3267
  await this.ui.terminal.drainInput(1000);
2655
3268
  this.stop();
2656
- await this.runtimeHost.dispose();
3269
+ if (this.runtimeHost) {
3270
+ await this.runtimeHost.dispose();
3271
+ const sessionManager = this.sessionManager;
3272
+ if (sessionManager) {
3273
+ const resumeCommand = formatResumeCommand(sessionManager);
3274
+ if (resumeCommand) {
3275
+ process.stdout.write(`${chalk.dim("To resume this session:")} ${resumeCommand}\n`);
3276
+ }
3277
+ }
3278
+ }
3279
+ else if (this.proxy && "dispose" in this.proxy) {
3280
+ this.proxy.dispose();
3281
+ }
2657
3282
  process.exit(0);
2658
3283
  }
2659
3284
  emergencyTerminalExit() {
@@ -2712,11 +3337,12 @@ export class InteractiveMode {
2712
3337
  }
2713
3338
  for (const signal of signals) {
2714
3339
  const handler = () => {
2715
- if (signal === "SIGHUP") {
2716
- this.emergencyTerminalExit();
2717
- }
3340
+ // SIGHUP no longer hard-exits: graceful shutdown emits session_shutdown
3341
+ // first, then attempts terminal restore. A genuinely dead terminal
3342
+ // surfaces as an EIO on the restore writes, which the stdout/stderr
3343
+ // error handler converts into emergencyTerminalExit (see #4144, #5080).
2718
3344
  killTrackedDetachedChildren();
2719
- void this.shutdown();
3345
+ void this.shutdown({ fromSignal: true });
2720
3346
  };
2721
3347
  process.prependListener(signal, handler);
2722
3348
  this.signalCleanupHandlers.push(() => process.off(signal, handler));
@@ -2797,7 +3423,12 @@ export class InteractiveMode {
2797
3423
  if (this.session.isStreaming) {
2798
3424
  this.editor.addToHistory?.(text);
2799
3425
  this.editor.setText("");
2800
- await this.session.prompt(text, { streamingBehavior: "followUp" });
3426
+ if (this.proxy) {
3427
+ this.proxy.followUp(text);
3428
+ }
3429
+ else {
3430
+ await this.session.prompt(text, { streamingBehavior: "followUp" });
3431
+ }
2801
3432
  this.updatePendingMessagesDisplay();
2802
3433
  this.ui.requestRender();
2803
3434
  }
@@ -2821,12 +3452,18 @@ export class InteractiveMode {
2821
3452
  this.editor.borderColor = theme.getBashModeBorderColor();
2822
3453
  }
2823
3454
  else {
2824
- const level = this.session.thinkingLevel || "off";
3455
+ const level = this.proxy ? this.proxy.thinkingLevel : this.session?.thinkingLevel || "off";
2825
3456
  this.editor.borderColor = theme.getThinkingBorderColor(level);
2826
3457
  }
2827
3458
  this.ui.requestRender();
2828
3459
  }
2829
3460
  cycleThinkingLevel() {
3461
+ // Connect mode: no local session — route through the proxy. The proxy is
3462
+ // fire-and-forget; the resulting state change arrives via SSE.
3463
+ if (!this.session) {
3464
+ this.proxy.cycleThinkingLevel(1);
3465
+ return;
3466
+ }
2830
3467
  const newLevel = this.session.cycleThinkingLevel();
2831
3468
  if (newLevel === undefined) {
2832
3469
  this.showStatus("Current model does not support thinking");
@@ -2839,9 +3476,19 @@ export class InteractiveMode {
2839
3476
  }
2840
3477
  async cycleModel(direction) {
2841
3478
  try {
2842
- const result = await this.session.cycleModel(direction);
3479
+ let result;
3480
+ if (this.session) {
3481
+ result = await this.session.cycleModel(direction);
3482
+ }
3483
+ else {
3484
+ // Connect mode: no local session — route through the proxy. The
3485
+ // proxy is fire-and-forget and returns void; the actual model
3486
+ // change is reflected via SSE.
3487
+ this.proxy.cycleModel(direction === "forward" ? 1 : -1);
3488
+ }
2843
3489
  if (result === undefined) {
2844
- const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
3490
+ const scopedCount = this.session?.scopedModels.length ?? 0;
3491
+ const msg = scopedCount > 0 ? "Only one model in scope" : "Only one model available";
2845
3492
  this.showStatus(msg);
2846
3493
  }
2847
3494
  else {
@@ -2886,7 +3533,7 @@ export class InteractiveMode {
2886
3533
  }
2887
3534
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
2888
3535
  }
2889
- openExternalEditor() {
3536
+ async openExternalEditor() {
2890
3537
  // Determine editor (respect $VISUAL, then $EDITOR)
2891
3538
  const editorCmd = process.env.VISUAL || process.env.EDITOR;
2892
3539
  if (!editorCmd) {
@@ -2902,13 +3549,20 @@ export class InteractiveMode {
2902
3549
  this.ui.stop();
2903
3550
  // Split by space to support editor arguments (e.g., "code --wait")
2904
3551
  const [editor, ...editorArgs] = editorCmd.split(" ");
2905
- // Spawn editor synchronously with inherited stdio for interactive editing
2906
- const result = spawnSync(editor, [...editorArgs, tmpFile], {
2907
- stdio: "inherit",
2908
- shell: process.platform === "win32",
3552
+ process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`);
3553
+ // Do not use spawnSync here. On Windows, synchronous child_process calls can keep
3554
+ // Node/libuv's console input read active after ui.stop() pauses stdin, racing
3555
+ // vim/nvim for the console input buffer until Ctrl+C cancels the pending read.
3556
+ const status = await new Promise((resolve) => {
3557
+ const child = spawn(editor, [...editorArgs, tmpFile], {
3558
+ stdio: "inherit",
3559
+ shell: process.platform === "win32",
3560
+ });
3561
+ child.on("error", () => resolve(null));
3562
+ child.on("close", (code) => resolve(code));
2909
3563
  });
2910
3564
  // On successful exit (status 0), replace editor content
2911
- if (result.status === 0) {
3565
+ if (status === 0) {
2912
3566
  const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
2913
3567
  this.editor.setText(newContent);
2914
3568
  }
@@ -2938,6 +3592,7 @@ export class InteractiveMode {
2938
3592
  showError(errorMessage) {
2939
3593
  this.chatContainer.addChild(new Spacer(1));
2940
3594
  this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
3595
+ this.chatContainer.addChild(new Spacer(1));
2941
3596
  this.ui.requestRender();
2942
3597
  }
2943
3598
  showWarning(warningMessage) {
@@ -2945,17 +3600,26 @@ export class InteractiveMode {
2945
3600
  this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
2946
3601
  this.ui.requestRender();
2947
3602
  }
2948
- showNewVersionNotification(newVersion) {
3603
+ showNewVersionNotification(release) {
2949
3604
  const action = theme.fg("accent", `${APP_NAME} update`);
2950
- const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. Run `) + action;
2951
- const changelogUrl = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md";
3605
+ const updateInstruction = theme.fg("muted", `New version ${release.version} is available. Run `) + action;
3606
+ const changelogUrl = "https://pi.dev/changelog";
2952
3607
  const changelogLink = getCapabilities().hyperlinks
2953
3608
  ? hyperlink(theme.fg("accent", "open changelog"), changelogUrl)
2954
3609
  : theme.fg("accent", changelogUrl);
2955
3610
  const changelogLine = theme.fg("muted", "Changelog: ") + changelogLink;
3611
+ const note = release.note?.trim();
2956
3612
  this.chatContainer.addChild(new Spacer(1));
2957
3613
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2958
- this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}\n${changelogLine}`, 1, 0));
3614
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0));
3615
+ if (note) {
3616
+ this.chatContainer.addChild(new Spacer(1));
3617
+ this.chatContainer.addChild(new Markdown(note, 1, 0, this.getMarkdownThemeWithSettings(), {
3618
+ color: (text) => theme.fg("muted", text),
3619
+ }));
3620
+ this.chatContainer.addChild(new Spacer(1));
3621
+ }
3622
+ this.chatContainer.addChild(new Text(changelogLine, 1, 0));
2959
3623
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2960
3624
  this.ui.requestRender();
2961
3625
  }
@@ -2974,13 +3638,15 @@ export class InteractiveMode {
2974
3638
  * Combines session queue and compaction queue.
2975
3639
  */
2976
3640
  getAllQueuedMessages() {
3641
+ const steeringSource = this.proxy ? this.proxy.steeringMessages : this.session.getSteeringMessages();
3642
+ const followUpSource = this.proxy ? this.proxy.followUpMessages : this.session.getFollowUpMessages();
2977
3643
  return {
2978
3644
  steering: [
2979
- ...this.session.getSteeringMessages(),
3645
+ ...steeringSource,
2980
3646
  ...this.compactionQueuedMessages.filter((msg) => msg.mode === "steer").map((msg) => msg.text),
2981
3647
  ],
2982
3648
  followUp: [
2983
- ...this.session.getFollowUpMessages(),
3649
+ ...followUpSource,
2984
3650
  ...this.compactionQueuedMessages.filter((msg) => msg.mode === "followUp").map((msg) => msg.text),
2985
3651
  ],
2986
3652
  };
@@ -2990,7 +3656,9 @@ export class InteractiveMode {
2990
3656
  * Clears both session queue and compaction queue.
2991
3657
  */
2992
3658
  clearAllQueues() {
2993
- const { steering, followUp } = this.session.clearQueue();
3659
+ const sessionQueued = this.proxy
3660
+ ? { steering: [...this.proxy.steeringMessages], followUp: [...this.proxy.followUpMessages] }
3661
+ : this.session.clearQueue();
2994
3662
  const compactionSteering = this.compactionQueuedMessages
2995
3663
  .filter((msg) => msg.mode === "steer")
2996
3664
  .map((msg) => msg.text);
@@ -2999,8 +3667,8 @@ export class InteractiveMode {
2999
3667
  .map((msg) => msg.text);
3000
3668
  this.compactionQueuedMessages = [];
3001
3669
  return {
3002
- steering: [...steering, ...compactionSteering],
3003
- followUp: [...followUp, ...compactionFollowUp],
3670
+ steering: [...sessionQueued.steering, ...compactionSteering],
3671
+ followUp: [...sessionQueued.followUp, ...compactionFollowUp],
3004
3672
  };
3005
3673
  }
3006
3674
  updatePendingMessagesDisplay() {
@@ -3027,7 +3695,7 @@ export class InteractiveMode {
3027
3695
  if (allQueued.length === 0) {
3028
3696
  this.updatePendingMessagesDisplay();
3029
3697
  if (options?.abort) {
3030
- this.agent.abort();
3698
+ (this.proxy ?? this.session).abort();
3031
3699
  }
3032
3700
  return 0;
3033
3701
  }
@@ -3037,7 +3705,7 @@ export class InteractiveMode {
3037
3705
  this.editor.setText(combinedText);
3038
3706
  this.updatePendingMessagesDisplay();
3039
3707
  if (options?.abort) {
3040
- this.agent.abort();
3708
+ (this.proxy ?? this.session).abort();
3041
3709
  }
3042
3710
  return allQueued.length;
3043
3711
  }
@@ -3051,6 +3719,8 @@ export class InteractiveMode {
3051
3719
  isExtensionCommand(text) {
3052
3720
  if (!text.startsWith("/"))
3053
3721
  return false;
3722
+ if (!this.session)
3723
+ return false;
3054
3724
  const extensionRunner = this.session.extensionRunner;
3055
3725
  const spaceIndex = text.indexOf(" ");
3056
3726
  const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
@@ -3064,11 +3734,31 @@ export class InteractiveMode {
3064
3734
  this.compactionQueuedMessages = [];
3065
3735
  this.updatePendingMessagesDisplay();
3066
3736
  const restoreQueue = (error) => {
3067
- this.session.clearQueue();
3737
+ if (this.session) {
3738
+ this.session.clearQueue();
3739
+ }
3068
3740
  this.compactionQueuedMessages = queuedMessages;
3069
3741
  this.updatePendingMessagesDisplay();
3070
3742
  this.showError(`Failed to send queued message${queuedMessages.length > 1 ? "s" : ""}: ${error instanceof Error ? error.message : String(error)}`);
3071
3743
  };
3744
+ // Connect mode: send all queued messages through the proxy
3745
+ if (this.proxy) {
3746
+ try {
3747
+ for (const message of queuedMessages) {
3748
+ if (message.mode === "followUp") {
3749
+ this.proxy.followUp(message.text);
3750
+ }
3751
+ else {
3752
+ this.proxy.steer(message.text);
3753
+ }
3754
+ }
3755
+ this.updatePendingMessagesDisplay();
3756
+ }
3757
+ catch (error) {
3758
+ restoreQueue(error);
3759
+ }
3760
+ return;
3761
+ }
3072
3762
  try {
3073
3763
  if (options?.willRetry) {
3074
3764
  // When retry is pending, queue messages for the retry turn
@@ -3164,6 +3854,7 @@ export class InteractiveMode {
3164
3854
  steeringMode: this.session.steeringMode,
3165
3855
  followUpMode: this.session.followUpMode,
3166
3856
  transport: this.settingsManager.getTransport(),
3857
+ httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
3167
3858
  thinkingLevel: this.session.thinkingLevel,
3168
3859
  availableThinkingLevels: this.session.getAvailableThinkingLevels(),
3169
3860
  currentTheme: this.settingsManager.getTheme() || "dark",
@@ -3221,6 +3912,11 @@ export class InteractiveMode {
3221
3912
  this.settingsManager.setTransport(transport);
3222
3913
  this.session.agent.transport = transport;
3223
3914
  },
3915
+ onHttpIdleTimeoutMsChange: (timeoutMs) => {
3916
+ this.settingsManager.setHttpIdleTimeoutMs(timeoutMs);
3917
+ configureHttpDispatcher(timeoutMs);
3918
+ this.showStatus(`HTTP idle timeout: ${formatHttpIdleTimeoutMs(timeoutMs)}`);
3919
+ },
3224
3920
  onThinkingLevelChange: (level) => {
3225
3921
  this.session.setThinkingLevel(level);
3226
3922
  this.footer.invalidate();
@@ -3465,6 +4161,10 @@ export class InteractiveMode {
3465
4161
  });
3466
4162
  }
3467
4163
  showUserMessageSelector() {
4164
+ if (!this.session) {
4165
+ this.showStatus("Fork selector not available in connect mode");
4166
+ return;
4167
+ }
3468
4168
  const userMessages = this.session.getUserMessagesForForking();
3469
4169
  if (userMessages.length === 0) {
3470
4170
  this.showStatus("No messages to fork from");
@@ -3517,6 +4217,10 @@ export class InteractiveMode {
3517
4217
  }
3518
4218
  }
3519
4219
  showTreeSelector(initialSelectedId) {
4220
+ if (!this.sessionManager) {
4221
+ this.showStatus("Tree selector not available in connect mode");
4222
+ return;
4223
+ }
3520
4224
  const tree = this.sessionManager.getTree();
3521
4225
  const realLeafId = this.sessionManager.getLeafId();
3522
4226
  const initialFilterMode = this.settingsManager.getTreeFilterMode();
@@ -3620,7 +4324,9 @@ export class InteractiveMode {
3620
4324
  }
3621
4325
  showSessionSelector() {
3622
4326
  this.showSelector((done) => {
3623
- const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), SessionManager.listAll, async (sessionPath) => {
4327
+ const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), (onProgress) => this.sessionManager.usesDefaultSessionDir()
4328
+ ? SessionManager.listAll(onProgress)
4329
+ : SessionManager.listAll(this.sessionManager.getSessionDir(), onProgress), async (sessionPath) => {
3624
4330
  done();
3625
4331
  await this.handleResumeSession(sessionPath);
3626
4332
  }, () => {
@@ -3642,6 +4348,343 @@ export class InteractiveMode {
3642
4348
  return { component: selector, focus: selector };
3643
4349
  });
3644
4350
  }
4351
+ /** Show settings selector in connect mode using remote settings from snapshot */
4352
+ showConnectModeSettingsSelector() {
4353
+ const snapshot = this.proxy.getSnapshot();
4354
+ const rs = snapshot.remoteSettings;
4355
+ this.showSelector((done) => {
4356
+ const selector = new SettingsSelectorComponent({
4357
+ autoCompact: rs.autoCompact,
4358
+ showImages: rs.showImages,
4359
+ imageWidthCells: rs.imageWidthCells,
4360
+ autoResizeImages: rs.autoResizeImages,
4361
+ blockImages: rs.blockImages,
4362
+ enableSkillCommands: rs.enableSkillCommands,
4363
+ steeringMode: rs.steeringMode,
4364
+ followUpMode: rs.followUpMode,
4365
+ transport: rs.transport,
4366
+ httpIdleTimeoutMs: rs.httpIdleTimeoutMs,
4367
+ thinkingLevel: rs.thinkingLevel,
4368
+ availableThinkingLevels: [...rs.availableThinkingLevels],
4369
+ currentTheme: rs.currentTheme,
4370
+ availableThemes: [...rs.availableThemes],
4371
+ hideThinkingBlock: rs.hideThinkingBlock,
4372
+ collapseChangelog: rs.collapseChangelog,
4373
+ enableInstallTelemetry: rs.enableInstallTelemetry,
4374
+ doubleEscapeAction: rs.doubleEscapeAction,
4375
+ treeFilterMode: rs.treeFilterMode,
4376
+ showHardwareCursor: rs.showHardwareCursor,
4377
+ editorPaddingX: rs.editorPaddingX,
4378
+ autocompleteMaxVisible: rs.autocompleteMaxVisible,
4379
+ quietStartup: rs.quietStartup,
4380
+ clearOnShrink: rs.clearOnShrink,
4381
+ showTerminalProgress: rs.showTerminalProgress,
4382
+ warnings: rs.warnings,
4383
+ }, {
4384
+ onAutoCompactChange: (enabled) => {
4385
+ this.proxy.setAutoCompactEnabled(enabled);
4386
+ this.footer.setAutoCompactEnabled(enabled);
4387
+ },
4388
+ onShowImagesChange: (v) => {
4389
+ this.proxy.updateSettings({ showImages: v });
4390
+ },
4391
+ onImageWidthCellsChange: (v) => {
4392
+ this.proxy.updateSettings({ imageWidthCells: v });
4393
+ },
4394
+ onAutoResizeImagesChange: (v) => {
4395
+ this.proxy.updateSettings({ autoResizeImages: v });
4396
+ },
4397
+ onBlockImagesChange: (v) => {
4398
+ this.proxy.updateSettings({ blockImages: v });
4399
+ },
4400
+ onEnableSkillCommandsChange: (enabled) => {
4401
+ this.proxy.updateSettings({ enableSkillCommands: enabled });
4402
+ },
4403
+ onSteeringModeChange: (mode) => {
4404
+ this.proxy.setSteeringMode(mode);
4405
+ },
4406
+ onFollowUpModeChange: (mode) => {
4407
+ this.proxy.setFollowUpMode(mode);
4408
+ },
4409
+ onTransportChange: (v) => {
4410
+ this.proxy.updateSettings({ transport: v });
4411
+ },
4412
+ onHttpIdleTimeoutMsChange: (v) => {
4413
+ this.proxy.updateSettings({ httpIdleTimeoutMs: v });
4414
+ },
4415
+ onThinkingLevelChange: (level) => {
4416
+ this.proxy.setThinkingLevel(level);
4417
+ this.footer.invalidate();
4418
+ this.updateEditorBorderColor();
4419
+ },
4420
+ onThemeChange: (v) => {
4421
+ this.proxy.updateSettings({ theme: v });
4422
+ },
4423
+ onHideThinkingBlockChange: (v) => {
4424
+ this.proxy.updateSettings({ hideThinkingBlock: v });
4425
+ },
4426
+ onCollapseChangelogChange: (v) => {
4427
+ this.proxy.updateSettings({ collapseChangelog: v });
4428
+ },
4429
+ onEnableInstallTelemetryChange: (v) => {
4430
+ this.proxy.updateSettings({ enableInstallTelemetry: v });
4431
+ },
4432
+ onDoubleEscapeActionChange: (v) => {
4433
+ this.proxy.updateSettings({ doubleEscapeAction: v });
4434
+ },
4435
+ onTreeFilterModeChange: (v) => {
4436
+ this.proxy.updateSettings({ treeFilterMode: v });
4437
+ },
4438
+ onShowHardwareCursorChange: (v) => {
4439
+ this.proxy.updateSettings({ showHardwareCursor: v });
4440
+ },
4441
+ onEditorPaddingXChange: (v) => {
4442
+ this.proxy.updateSettings({ editorPaddingX: v });
4443
+ },
4444
+ onAutocompleteMaxVisibleChange: (v) => {
4445
+ this.proxy.updateSettings({ autocompleteMaxVisible: v });
4446
+ },
4447
+ onQuietStartupChange: (v) => {
4448
+ this.proxy.updateSettings({ quietStartup: v });
4449
+ },
4450
+ onClearOnShrinkChange: (v) => {
4451
+ this.proxy.updateSettings({ clearOnShrink: v });
4452
+ },
4453
+ onShowTerminalProgressChange: (v) => {
4454
+ this.proxy.updateSettings({ showTerminalProgress: v });
4455
+ },
4456
+ onWarningsChange: (v) => {
4457
+ this.proxy.updateSettings({ warnings: v });
4458
+ },
4459
+ onCancel: done,
4460
+ });
4461
+ return { component: selector, focus: selector };
4462
+ });
4463
+ }
4464
+ /** Show model selector in connect mode */
4465
+ async showConnectModeModelSelector() {
4466
+ try {
4467
+ const models = await this.proxy.fetchModels();
4468
+ if (models.length === 0) {
4469
+ this.showStatus("No models available");
4470
+ return;
4471
+ }
4472
+ const currentModelId = this.proxy.model;
4473
+ const items = models.map((m) => ({
4474
+ value: `${m.provider}/${m.id}`,
4475
+ label: `${m.id} [${m.provider}]`,
4476
+ description: m.name,
4477
+ }));
4478
+ this.showSelector((done) => {
4479
+ const selectList = new SelectList(items, Math.min(items.length, Math.floor(this.ui.terminal.rows / 2)), getSelectListTheme(), { minPrimaryColumnWidth: 20, maxPrimaryColumnWidth: 48 });
4480
+ // Preselect current model
4481
+ const currentIndex = items.findIndex((item) => item.value === currentModelId || item.label === currentModelId);
4482
+ if (currentIndex !== -1) {
4483
+ selectList.setSelectedIndex(currentIndex);
4484
+ }
4485
+ selectList.onSelect = (item) => {
4486
+ const modelId = item.value;
4487
+ this.proxy.setModel(modelId);
4488
+ done();
4489
+ this.showStatus(`Model: ${modelId}`);
4490
+ };
4491
+ selectList.onCancel = () => {
4492
+ done();
4493
+ this.ui.requestRender();
4494
+ };
4495
+ const container = new Container();
4496
+ container.addChild(new DynamicBorder());
4497
+ container.addChild(new Spacer(1));
4498
+ container.addChild(new Text(theme.fg("muted", "Select a model"), 0, 0));
4499
+ container.addChild(new Spacer(1));
4500
+ container.addChild(selectList);
4501
+ container.addChild(new DynamicBorder());
4502
+ return { component: container, focus: selectList };
4503
+ });
4504
+ }
4505
+ catch (e) {
4506
+ this.showError(`Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
4507
+ }
4508
+ }
4509
+ /** Show scoped models selector in connect mode */
4510
+ async showConnectModeScopedModelsSelector() {
4511
+ try {
4512
+ const items = await this.proxy.fetchModels();
4513
+ if (items.length === 0) {
4514
+ this.showStatus("No models available");
4515
+ return;
4516
+ }
4517
+ const snapshot = this.proxy.getSnapshot();
4518
+ const enabledModelIds = snapshot.scopedModelIds;
4519
+ // `ModelItemData` is now field-compatible with `Model<any>` (see
4520
+ // core/agent-session-proxy.ts), so the only gap is the strict
4521
+ // structural mismatch on `compat?` (optional) and `headers?` (optional)
4522
+ // and the union-typed `api` field. A two-step cast skips those
4523
+ // without touching upstream's strict `ModelsConfig.allModels` type.
4524
+ // TODO: upstream `ScopedModelsSelectorComponent` should accept
4525
+ // `ModelItemData` (or a `ModelDisplay` alias) directly.
4526
+ const allModels = items;
4527
+ this.showSelector((done) => {
4528
+ const selector = new ScopedModelsSelectorComponent({ allModels, enabledModelIds }, {
4529
+ onChange: (ids) => {
4530
+ this.proxy.setScopedModels(ids);
4531
+ },
4532
+ onPersist: (ids) => {
4533
+ // Mirror upstream: null or full coverage = clear filter.
4534
+ const patterns = ids === null || ids.length === allModels.length ? undefined : ids;
4535
+ this.proxy.setEnabledModels(patterns ? [...patterns] : undefined);
4536
+ this.showStatus("Model selection saved to settings");
4537
+ },
4538
+ onCancel: () => {
4539
+ done();
4540
+ this.ui.requestRender();
4541
+ },
4542
+ });
4543
+ return { component: selector, focus: selector };
4544
+ });
4545
+ }
4546
+ catch (e) {
4547
+ this.showError(`Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
4548
+ }
4549
+ }
4550
+ /** Show fork selector in connect mode */
4551
+ async showConnectModeForkSelector() {
4552
+ try {
4553
+ const messages = await this.proxy.fetchUserMessages();
4554
+ if (messages.length === 0) {
4555
+ this.showStatus("No messages to fork from");
4556
+ return;
4557
+ }
4558
+ this.showSelector((done) => {
4559
+ const selector = new UserMessageSelectorComponent(messages.map((m) => ({ id: m.id, text: m.text })), async (entryId) => {
4560
+ try {
4561
+ await this.proxy.fork(entryId);
4562
+ done();
4563
+ this.showStatus("Forked to new session");
4564
+ }
4565
+ catch (error) {
4566
+ done();
4567
+ this.showError(error instanceof Error ? error.message : String(error));
4568
+ }
4569
+ }, () => {
4570
+ done();
4571
+ this.ui.requestRender();
4572
+ });
4573
+ return { component: selector, focus: selector.getMessageList() };
4574
+ });
4575
+ }
4576
+ catch (e) {
4577
+ this.showError(`Failed to fetch messages: ${e instanceof Error ? e.message : String(e)}`);
4578
+ }
4579
+ }
4580
+ /** Show tree selector in connect mode */
4581
+ async showConnectModeTreeSelector() {
4582
+ try {
4583
+ const treeData = await this.proxy.fetchTree();
4584
+ if (treeData.length === 0) {
4585
+ this.showStatus("No entries in session");
4586
+ return;
4587
+ }
4588
+ const nodes = this._convertTreeDataToNodes(treeData);
4589
+ this.showSelector((done) => {
4590
+ const selector = new TreeSelectorComponent(nodes, null, this.ui.terminal.rows, async (entryId) => {
4591
+ try {
4592
+ await this.proxy.fork(entryId);
4593
+ done();
4594
+ this.showStatus("Navigated to branch");
4595
+ }
4596
+ catch (error) {
4597
+ done();
4598
+ this.showError(error instanceof Error ? error.message : String(error));
4599
+ }
4600
+ }, () => {
4601
+ done();
4602
+ this.ui.requestRender();
4603
+ }, (entryId, label) => {
4604
+ this.proxy.setLabel(entryId, label);
4605
+ });
4606
+ return { component: selector, focus: selector };
4607
+ });
4608
+ }
4609
+ catch (e) {
4610
+ this.showError(`Failed to fetch tree: ${e instanceof Error ? e.message : String(e)}`);
4611
+ }
4612
+ }
4613
+ /** Convert wire-format TreeNodeData to SessionTreeNode for TreeSelectorComponent */
4614
+ _convertTreeDataToNodes(data) {
4615
+ return data.map((node) => ({
4616
+ entry: { id: node.id, type: node.type, parentId: node.parentId, timestamp: node.timestamp },
4617
+ children: this._convertTreeDataToNodes(node.children),
4618
+ label: node.label,
4619
+ }));
4620
+ }
4621
+ /** Show session selector in connect mode */
4622
+ showConnectModeSessionSelector() {
4623
+ this.showSelector((done) => {
4624
+ const selector = new SessionSelectorComponent(async (_onProgress) => {
4625
+ const sessions = await this.proxy.getSessions();
4626
+ return sessions.map((s) => ({
4627
+ path: s.path,
4628
+ id: s.id,
4629
+ cwd: s.cwd,
4630
+ name: s.name,
4631
+ parentSessionPath: s.parentSessionPath,
4632
+ created: new Date(s.created),
4633
+ modified: new Date(s.modified),
4634
+ messageCount: s.messageCount,
4635
+ firstMessage: s.firstMessage,
4636
+ allMessagesText: "",
4637
+ }));
4638
+ }, async () => [], async (sessionPath) => {
4639
+ try {
4640
+ await this.proxy.switchSession(sessionPath);
4641
+ done();
4642
+ this.showStatus("Switched session");
4643
+ }
4644
+ catch (error) {
4645
+ done();
4646
+ this.showError(error instanceof Error ? error.message : String(error));
4647
+ }
4648
+ }, () => {
4649
+ done();
4650
+ this.ui.requestRender();
4651
+ }, () => {
4652
+ void this.shutdown();
4653
+ }, () => this.ui.requestRender());
4654
+ return { component: selector, focus: selector };
4655
+ });
4656
+ }
4657
+ /** Copy last agent message in connect mode */
4658
+ handleConnectModeCopy() {
4659
+ const msgs = this.proxy.messages;
4660
+ const lastAssistant = [...msgs].reverse().find((m) => m.role === "assistant");
4661
+ if (!lastAssistant) {
4662
+ this.showStatus("No assistant message to copy");
4663
+ return;
4664
+ }
4665
+ const text = lastAssistant.content
4666
+ .filter((p) => p.type === "text")
4667
+ .map((p) => p.text)
4668
+ .join("\n");
4669
+ if (text) {
4670
+ void copyToClipboard(text);
4671
+ this.showStatus("Copied to clipboard");
4672
+ }
4673
+ }
4674
+ /** Show session info in connect mode */
4675
+ showConnectModeSessionInfo() {
4676
+ const snapshot = this.proxy.getSnapshot();
4677
+ const parts = [
4678
+ `Model: ${snapshot.model}`,
4679
+ `Session: ${snapshot.sessionName ?? "(unnamed)"}`,
4680
+ `File: ${snapshot.sessionFile ?? "N/A"}`,
4681
+ `CWD: ${snapshot.cwd}`,
4682
+ `Messages: ${snapshot.messages.length}`,
4683
+ `Context: ${snapshot.contextUsage.percent !== null ? `${snapshot.contextUsage.percent.toFixed(1)}%` : "?"} / ${snapshot.contextUsage.contextWindow}`,
4684
+ `Tokens: ↑${snapshot.tokenUsage.input} ↓${snapshot.tokenUsage.output} $${snapshot.tokenUsage.cost.toFixed(3)}`,
4685
+ ];
4686
+ this.showStatus(parts.join(" • "));
4687
+ }
3645
4688
  async handleResumeSession(sessionPath, options) {
3646
4689
  if (this.loadingAnimation) {
3647
4690
  this.loadingAnimation.stop();
@@ -3923,9 +4966,7 @@ export class InteractiveMode {
3923
4966
  });
3924
4967
  }
3925
4968
  async showLoginDialog(providerId, providerName) {
3926
- const providerInfo = this.session.modelRegistry.authStorage
3927
- .getOAuthProviders()
3928
- .find((provider) => provider.id === providerId);
4969
+ const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((provider) => provider.id === providerId);
3929
4970
  const previousModel = this.session.model;
3930
4971
  // Providers that use callback servers (can paste redirect URL)
3931
4972
  const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
@@ -3973,12 +5014,12 @@ export class InteractiveMode {
3973
5014
  }
3974
5015
  });
3975
5016
  }
3976
- else if (providerId === "github-copilot") {
3977
- // GitHub Copilot polls after onAuth
3978
- dialog.showWaiting("Waiting for browser authentication...");
3979
- }
3980
5017
  // For Anthropic: onPrompt is called immediately after
3981
5018
  },
5019
+ onDeviceCode: (info) => {
5020
+ dialog.showDeviceCode(info);
5021
+ dialog.showWaiting("Waiting for authentication...");
5022
+ },
3982
5023
  onPrompt: async (prompt) => {
3983
5024
  return dialog.showPrompt(prompt.message, prompt.placeholder);
3984
5025
  },
@@ -4035,6 +5076,7 @@ export class InteractiveMode {
4035
5076
  };
4036
5077
  try {
4037
5078
  await this.session.reload();
5079
+ configureHttpDispatcher(this.settingsManager.getHttpIdleTimeoutMs());
4038
5080
  this.keybindings.reload();
4039
5081
  const activeHeader = this.customHeader ?? this.builtInHeader;
4040
5082
  if (isExpandable(activeHeader)) {
@@ -4610,7 +5652,7 @@ export class InteractiveMode {
4610
5652
  }
4611
5653
  stop() {
4612
5654
  this.unregisterSignalHandlers();
4613
- if (this.settingsManager.getShowTerminalProgress()) {
5655
+ if (this.settingsManager?.getShowTerminalProgress()) {
4614
5656
  this.ui.terminal.setProgress(false);
4615
5657
  }
4616
5658
  if (this.loadingAnimation) {