@prestyj/cli 5.8.0 → 5.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/README.md +1 -1
  2. package/assets/skills/evidence-led-ui/LICENSES.md +12 -0
  3. package/assets/skills/evidence-led-ui/SKILL.md +15 -7
  4. package/assets/skills/evidence-led-ui/references/anti-defaults.md +9 -1
  5. package/assets/skills/evidence-led-ui/references/craft-rulings.md +33 -0
  6. package/assets/skills/evidence-led-ui/references/production-contract.md +41 -21
  7. package/assets/skills/evidence-led-ui/references/provenance.md +7 -5
  8. package/assets/skills/evidence-led-ui/references/quality-rubric.md +10 -10
  9. package/dist/app-sidecar-sessions.d.ts +8 -0
  10. package/dist/app-sidecar-sessions.d.ts.map +1 -0
  11. package/dist/app-sidecar-sessions.js +31 -0
  12. package/dist/app-sidecar-sessions.js.map +1 -0
  13. package/dist/app-sidecar-sessions.test.d.ts +2 -0
  14. package/dist/app-sidecar-sessions.test.d.ts.map +1 -0
  15. package/dist/app-sidecar-sessions.test.js +83 -0
  16. package/dist/app-sidecar-sessions.test.js.map +1 -0
  17. package/dist/app-sidecar.js +623 -120
  18. package/dist/app-sidecar.js.map +1 -1
  19. package/dist/chat-agents/general.test.js +5 -3
  20. package/dist/chat-agents/general.test.js.map +1 -1
  21. package/dist/chat-agents/memory.d.ts.map +1 -1
  22. package/dist/chat-agents/memory.js +14 -5
  23. package/dist/chat-agents/memory.js.map +1 -1
  24. package/dist/chat-agents/memory.test.js +11 -1
  25. package/dist/chat-agents/memory.test.js.map +1 -1
  26. package/dist/chat-agents/shared.js +1 -1
  27. package/dist/chat-agents/shared.js.map +1 -1
  28. package/dist/chat-agents/specialists.test.js +3 -2
  29. package/dist/chat-agents/specialists.test.js.map +1 -1
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +59 -30
  32. package/dist/cli.js.map +1 -1
  33. package/dist/config.d.ts +18 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +50 -2
  36. package/dist/config.js.map +1 -1
  37. package/dist/config.test.js +61 -1
  38. package/dist/config.test.js.map +1 -1
  39. package/dist/core/agent-mcp-passthrough.test.d.ts +2 -0
  40. package/dist/core/agent-mcp-passthrough.test.d.ts.map +1 -0
  41. package/dist/core/agent-mcp-passthrough.test.js +61 -0
  42. package/dist/core/agent-mcp-passthrough.test.js.map +1 -0
  43. package/dist/core/agent-session-add-dir.test.d.ts +2 -0
  44. package/dist/core/agent-session-add-dir.test.d.ts.map +1 -0
  45. package/dist/core/agent-session-add-dir.test.js +110 -0
  46. package/dist/core/agent-session-add-dir.test.js.map +1 -0
  47. package/dist/core/agent-session-compaction.test.js +23 -7
  48. package/dist/core/agent-session-compaction.test.js.map +1 -1
  49. package/dist/core/agent-session-marker-anchors.test.d.ts +2 -0
  50. package/dist/core/agent-session-marker-anchors.test.d.ts.map +1 -0
  51. package/dist/core/agent-session-marker-anchors.test.js +147 -0
  52. package/dist/core/agent-session-marker-anchors.test.js.map +1 -0
  53. package/dist/core/agent-session-memory-tail.test.js +4 -7
  54. package/dist/core/agent-session-memory-tail.test.js.map +1 -1
  55. package/dist/core/agent-session-queue.test.js +4 -7
  56. package/dist/core/agent-session-queue.test.js.map +1 -1
  57. package/dist/core/agent-session-subagent-policy.test.js +7 -3
  58. package/dist/core/agent-session-subagent-policy.test.js.map +1 -1
  59. package/dist/core/agent-session.d.ts +46 -0
  60. package/dist/core/agent-session.d.ts.map +1 -1
  61. package/dist/core/agent-session.js +177 -19
  62. package/dist/core/agent-session.js.map +1 -1
  63. package/dist/core/agent-tools-allowlist.test.js +5 -3
  64. package/dist/core/agent-tools-allowlist.test.js.map +1 -1
  65. package/dist/core/agents.d.ts +12 -1
  66. package/dist/core/agents.d.ts.map +1 -1
  67. package/dist/core/agents.js +25 -1
  68. package/dist/core/agents.js.map +1 -1
  69. package/dist/core/agents.test.d.ts +2 -0
  70. package/dist/core/agents.test.d.ts.map +1 -0
  71. package/dist/core/agents.test.js +67 -0
  72. package/dist/core/agents.test.js.map +1 -0
  73. package/dist/core/auth-providers.js +1 -1
  74. package/dist/core/auth-providers.js.map +1 -1
  75. package/dist/core/auth-storage.d.ts +1 -1
  76. package/dist/core/auth-storage.d.ts.map +1 -1
  77. package/dist/core/auth-storage.js +1 -1
  78. package/dist/core/auth-storage.js.map +1 -1
  79. package/dist/core/compaction/compactor.d.ts.map +1 -1
  80. package/dist/core/compaction/compactor.js +20 -11
  81. package/dist/core/compaction/compactor.js.map +1 -1
  82. package/dist/core/compaction/compactor.test.js +17 -4
  83. package/dist/core/compaction/compactor.test.js.map +1 -1
  84. package/dist/core/compaction/token-estimator.test.js +1 -1
  85. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  86. package/dist/core/encode-cwd.d.ts +17 -0
  87. package/dist/core/encode-cwd.d.ts.map +1 -1
  88. package/dist/core/encode-cwd.js +20 -7
  89. package/dist/core/encode-cwd.js.map +1 -1
  90. package/dist/core/encode-cwd.test.js +20 -1
  91. package/dist/core/encode-cwd.test.js.map +1 -1
  92. package/dist/core/event-bus.d.ts +5 -0
  93. package/dist/core/event-bus.d.ts.map +1 -1
  94. package/dist/core/event-bus.js +6 -0
  95. package/dist/core/event-bus.js.map +1 -1
  96. package/dist/core/goal-controller.test.js +3 -3
  97. package/dist/core/goal-controller.test.js.map +1 -1
  98. package/dist/core/ideal-review.d.ts.map +1 -1
  99. package/dist/core/ideal-review.js +6 -1
  100. package/dist/core/ideal-review.js.map +1 -1
  101. package/dist/core/ideal-review.test.js +7 -2
  102. package/dist/core/ideal-review.test.js.map +1 -1
  103. package/dist/core/index.d.ts +2 -2
  104. package/dist/core/index.d.ts.map +1 -1
  105. package/dist/core/index.js +2 -2
  106. package/dist/core/index.js.map +1 -1
  107. package/dist/core/input-normalization.test.d.ts +2 -0
  108. package/dist/core/input-normalization.test.d.ts.map +1 -0
  109. package/dist/core/input-normalization.test.js +75 -0
  110. package/dist/core/input-normalization.test.js.map +1 -0
  111. package/dist/core/local-endpoint-store.d.ts +50 -0
  112. package/dist/core/local-endpoint-store.d.ts.map +1 -0
  113. package/dist/core/local-endpoint-store.js +201 -0
  114. package/dist/core/local-endpoint-store.js.map +1 -0
  115. package/dist/core/local-endpoint-store.test.d.ts +2 -0
  116. package/dist/core/local-endpoint-store.test.d.ts.map +1 -0
  117. package/dist/core/local-endpoint-store.test.js +131 -0
  118. package/dist/core/local-endpoint-store.test.js.map +1 -0
  119. package/dist/core/loop-breaker.d.ts +38 -1
  120. package/dist/core/loop-breaker.d.ts.map +1 -1
  121. package/dist/core/loop-breaker.js +84 -2
  122. package/dist/core/loop-breaker.js.map +1 -1
  123. package/dist/core/loop-breaker.test.js +108 -1
  124. package/dist/core/loop-breaker.test.js.map +1 -1
  125. package/dist/core/lsp/client.d.ts +50 -0
  126. package/dist/core/lsp/client.d.ts.map +1 -1
  127. package/dist/core/lsp/client.js +158 -13
  128. package/dist/core/lsp/client.js.map +1 -1
  129. package/dist/core/lsp/jsonrpc.d.ts +38 -1
  130. package/dist/core/lsp/jsonrpc.d.ts.map +1 -1
  131. package/dist/core/lsp/jsonrpc.js +13 -1
  132. package/dist/core/lsp/jsonrpc.js.map +1 -1
  133. package/dist/core/lsp/manager.d.ts.map +1 -1
  134. package/dist/core/lsp/manager.js +23 -2
  135. package/dist/core/lsp/manager.js.map +1 -1
  136. package/dist/core/lsp/manager.test.js +32 -1
  137. package/dist/core/lsp/manager.test.js.map +1 -1
  138. package/dist/core/lsp/test-support.d.ts +18 -0
  139. package/dist/core/lsp/test-support.d.ts.map +1 -0
  140. package/dist/core/lsp/test-support.js +27 -0
  141. package/dist/core/lsp/test-support.js.map +1 -0
  142. package/dist/core/lsp/uri.test.d.ts +2 -0
  143. package/dist/core/lsp/uri.test.d.ts.map +1 -0
  144. package/dist/core/lsp/uri.test.js +56 -0
  145. package/dist/core/lsp/uri.test.js.map +1 -0
  146. package/dist/core/lsp/windows.test.d.ts +2 -0
  147. package/dist/core/lsp/windows.test.d.ts.map +1 -0
  148. package/dist/core/lsp/windows.test.js +251 -0
  149. package/dist/core/lsp/windows.test.js.map +1 -0
  150. package/dist/core/mcp/resolve-stdio.d.ts +35 -0
  151. package/dist/core/mcp/resolve-stdio.d.ts.map +1 -1
  152. package/dist/core/mcp/resolve-stdio.js +80 -1
  153. package/dist/core/mcp/resolve-stdio.js.map +1 -1
  154. package/dist/core/mcp/resolve-stdio.test.js +99 -4
  155. package/dist/core/mcp/resolve-stdio.test.js.map +1 -1
  156. package/dist/core/message-images.d.ts +12 -0
  157. package/dist/core/message-images.d.ts.map +1 -0
  158. package/dist/core/message-images.js +52 -0
  159. package/dist/core/message-images.js.map +1 -0
  160. package/dist/core/message-images.test.d.ts +2 -0
  161. package/dist/core/message-images.test.d.ts.map +1 -0
  162. package/dist/core/message-images.test.js +62 -0
  163. package/dist/core/message-images.test.js.map +1 -0
  164. package/dist/core/network-guard.d.ts +47 -0
  165. package/dist/core/network-guard.d.ts.map +1 -0
  166. package/dist/core/network-guard.js +208 -0
  167. package/dist/core/network-guard.js.map +1 -0
  168. package/dist/core/network-guard.test.d.ts +2 -0
  169. package/dist/core/network-guard.test.d.ts.map +1 -0
  170. package/dist/core/network-guard.test.js +72 -0
  171. package/dist/core/network-guard.test.js.map +1 -0
  172. package/dist/core/persistent-shell.d.ts +3 -1
  173. package/dist/core/persistent-shell.d.ts.map +1 -1
  174. package/dist/core/persistent-shell.js +13 -3
  175. package/dist/core/persistent-shell.js.map +1 -1
  176. package/dist/core/process-manager-dev-server-repro.test.js +34 -12
  177. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
  178. package/dist/core/process-manager.d.ts +1 -1
  179. package/dist/core/process-manager.d.ts.map +1 -1
  180. package/dist/core/process-manager.js +23 -13
  181. package/dist/core/process-manager.js.map +1 -1
  182. package/dist/core/project-discovery.d.ts +8 -1
  183. package/dist/core/project-discovery.d.ts.map +1 -1
  184. package/dist/core/project-discovery.js +145 -62
  185. package/dist/core/project-discovery.js.map +1 -1
  186. package/dist/core/project-discovery.test.js +131 -1
  187. package/dist/core/project-discovery.test.js.map +1 -1
  188. package/dist/core/prompt-commands.d.ts.map +1 -1
  189. package/dist/core/prompt-commands.js +89 -156
  190. package/dist/core/prompt-commands.js.map +1 -1
  191. package/dist/core/prompt-commands.test.js +4 -10
  192. package/dist/core/prompt-commands.test.js.map +1 -1
  193. package/dist/core/radio.test.js +4 -1
  194. package/dist/core/radio.test.js.map +1 -1
  195. package/dist/core/runtime-mode.d.ts +2 -0
  196. package/dist/core/runtime-mode.d.ts.map +1 -1
  197. package/dist/core/runtime-mode.js +4 -0
  198. package/dist/core/runtime-mode.js.map +1 -1
  199. package/dist/core/session-export.d.ts +98 -0
  200. package/dist/core/session-export.d.ts.map +1 -0
  201. package/dist/core/session-export.js +356 -0
  202. package/dist/core/session-export.js.map +1 -0
  203. package/dist/core/session-export.test.d.ts +2 -0
  204. package/dist/core/session-export.test.d.ts.map +1 -0
  205. package/dist/core/session-export.test.js +353 -0
  206. package/dist/core/session-export.test.js.map +1 -0
  207. package/dist/core/session-manager.d.ts +27 -7
  208. package/dist/core/session-manager.d.ts.map +1 -1
  209. package/dist/core/session-manager.js +257 -124
  210. package/dist/core/session-manager.js.map +1 -1
  211. package/dist/core/session-storage.d.ts +57 -0
  212. package/dist/core/session-storage.d.ts.map +1 -0
  213. package/dist/core/session-storage.js +592 -0
  214. package/dist/core/session-storage.js.map +1 -0
  215. package/dist/core/session-storage.test.d.ts +2 -0
  216. package/dist/core/session-storage.test.d.ts.map +1 -0
  217. package/dist/core/session-storage.test.js +272 -0
  218. package/dist/core/session-storage.test.js.map +1 -0
  219. package/dist/core/settings-manager.d.ts +7 -0
  220. package/dist/core/settings-manager.d.ts.map +1 -1
  221. package/dist/core/settings-manager.js +17 -0
  222. package/dist/core/settings-manager.js.map +1 -1
  223. package/dist/core/setup-history.d.ts +0 -6
  224. package/dist/core/setup-history.d.ts.map +1 -1
  225. package/dist/core/setup-history.js +2 -18
  226. package/dist/core/setup-history.js.map +1 -1
  227. package/dist/core/setup-history.test.js +11 -26
  228. package/dist/core/setup-history.test.js.map +1 -1
  229. package/dist/core/sidecar-error-reporter.d.ts +12 -0
  230. package/dist/core/sidecar-error-reporter.d.ts.map +1 -0
  231. package/dist/core/sidecar-error-reporter.js +57 -0
  232. package/dist/core/sidecar-error-reporter.js.map +1 -0
  233. package/dist/core/sidecar-error-reporter.test.d.ts +2 -0
  234. package/dist/core/sidecar-error-reporter.test.d.ts.map +1 -0
  235. package/dist/core/sidecar-error-reporter.test.js +61 -0
  236. package/dist/core/sidecar-error-reporter.test.js.map +1 -0
  237. package/dist/core/skills-routing.test.js +3 -1
  238. package/dist/core/skills-routing.test.js.map +1 -1
  239. package/dist/core/skills.d.ts +1 -1
  240. package/dist/core/skills.d.ts.map +1 -1
  241. package/dist/core/skills.js +4 -1
  242. package/dist/core/skills.js.map +1 -1
  243. package/dist/core/slash-commands.d.ts +18 -0
  244. package/dist/core/slash-commands.d.ts.map +1 -1
  245. package/dist/core/slash-commands.js +42 -10
  246. package/dist/core/slash-commands.js.map +1 -1
  247. package/dist/core/slash-commands.test.d.ts +2 -0
  248. package/dist/core/slash-commands.test.d.ts.map +1 -0
  249. package/dist/core/slash-commands.test.js +59 -0
  250. package/dist/core/slash-commands.test.js.map +1 -0
  251. package/dist/core/speed-benchmark.test.js +9 -3
  252. package/dist/core/speed-benchmark.test.js.map +1 -1
  253. package/dist/core/subagent-manager.d.ts +8 -1
  254. package/dist/core/subagent-manager.d.ts.map +1 -1
  255. package/dist/core/subagent-manager.js +35 -2
  256. package/dist/core/subagent-manager.js.map +1 -1
  257. package/dist/core/subagent-manager.test.js +34 -0
  258. package/dist/core/subagent-manager.test.js.map +1 -1
  259. package/dist/core/subagent-store.test.js +14 -2
  260. package/dist/core/subagent-store.test.js.map +1 -1
  261. package/dist/core/verify-commands.d.ts.map +1 -1
  262. package/dist/core/verify-commands.js +1 -6
  263. package/dist/core/verify-commands.js.map +1 -1
  264. package/dist/core/verify-commands.test.js +1 -5
  265. package/dist/core/verify-commands.test.js.map +1 -1
  266. package/dist/core/workspace-guard.d.ts +40 -0
  267. package/dist/core/workspace-guard.d.ts.map +1 -0
  268. package/dist/core/workspace-guard.js +115 -0
  269. package/dist/core/workspace-guard.js.map +1 -0
  270. package/dist/core/workspace-guard.test.d.ts +2 -0
  271. package/dist/core/workspace-guard.test.d.ts.map +1 -0
  272. package/dist/core/workspace-guard.test.js +107 -0
  273. package/dist/core/workspace-guard.test.js.map +1 -0
  274. package/dist/index.d.ts +1 -1
  275. package/dist/index.d.ts.map +1 -1
  276. package/dist/index.js +1 -1
  277. package/dist/index.js.map +1 -1
  278. package/dist/modes/json-mode.d.ts +7 -0
  279. package/dist/modes/json-mode.d.ts.map +1 -1
  280. package/dist/modes/json-mode.js +10 -0
  281. package/dist/modes/json-mode.js.map +1 -1
  282. package/dist/modes/subagent-worker-mode.d.ts +2 -0
  283. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  284. package/dist/modes/subagent-worker-mode.js +11 -0
  285. package/dist/modes/subagent-worker-mode.js.map +1 -1
  286. package/dist/session.d.ts +4 -4
  287. package/dist/session.d.ts.map +1 -1
  288. package/dist/session.js +38 -101
  289. package/dist/session.js.map +1 -1
  290. package/dist/session.test.d.ts +2 -0
  291. package/dist/session.test.d.ts.map +1 -0
  292. package/dist/session.test.js +35 -0
  293. package/dist/session.test.js.map +1 -0
  294. package/dist/system-prompt.d.ts +23 -1
  295. package/dist/system-prompt.d.ts.map +1 -1
  296. package/dist/system-prompt.js +119 -31
  297. package/dist/system-prompt.js.map +1 -1
  298. package/dist/system-prompt.test.js +154 -14
  299. package/dist/system-prompt.test.js.map +1 -1
  300. package/dist/test-support/fake-home.d.ts +20 -0
  301. package/dist/test-support/fake-home.d.ts.map +1 -0
  302. package/dist/test-support/fake-home.js +37 -0
  303. package/dist/test-support/fake-home.js.map +1 -0
  304. package/dist/tools/bash.d.ts +4 -2
  305. package/dist/tools/bash.d.ts.map +1 -1
  306. package/dist/tools/bash.js +57 -9
  307. package/dist/tools/bash.js.map +1 -1
  308. package/dist/tools/bash.test.js +156 -8
  309. package/dist/tools/bash.test.js.map +1 -1
  310. package/dist/tools/edit-robustness.test.js +3 -1
  311. package/dist/tools/edit-robustness.test.js.map +1 -1
  312. package/dist/tools/edit.d.ts +2 -1
  313. package/dist/tools/edit.d.ts.map +1 -1
  314. package/dist/tools/edit.js +7 -1
  315. package/dist/tools/edit.js.map +1 -1
  316. package/dist/tools/edit.test.js +7 -0
  317. package/dist/tools/edit.test.js.map +1 -1
  318. package/dist/tools/exit-plan.d.ts.map +1 -1
  319. package/dist/tools/exit-plan.js +15 -4
  320. package/dist/tools/exit-plan.js.map +1 -1
  321. package/dist/tools/exit-plan.test.d.ts +2 -0
  322. package/dist/tools/exit-plan.test.d.ts.map +1 -0
  323. package/dist/tools/exit-plan.test.js +72 -0
  324. package/dist/tools/exit-plan.test.js.map +1 -0
  325. package/dist/tools/find.d.ts.map +1 -1
  326. package/dist/tools/find.js +4 -1
  327. package/dist/tools/find.js.map +1 -1
  328. package/dist/tools/grep.d.ts +3 -1
  329. package/dist/tools/grep.d.ts.map +1 -1
  330. package/dist/tools/grep.js +143 -14
  331. package/dist/tools/grep.js.map +1 -1
  332. package/dist/tools/grep.test.js +59 -0
  333. package/dist/tools/grep.test.js.map +1 -1
  334. package/dist/tools/index.d.ts +14 -0
  335. package/dist/tools/index.d.ts.map +1 -1
  336. package/dist/tools/index.js +6 -5
  337. package/dist/tools/index.js.map +1 -1
  338. package/dist/tools/overflow.test.js +4 -7
  339. package/dist/tools/overflow.test.js.map +1 -1
  340. package/dist/tools/path-utils.d.ts +15 -0
  341. package/dist/tools/path-utils.d.ts.map +1 -1
  342. package/dist/tools/path-utils.js +17 -0
  343. package/dist/tools/path-utils.js.map +1 -1
  344. package/dist/tools/path-utils.test.js +7 -2
  345. package/dist/tools/path-utils.test.js.map +1 -1
  346. package/dist/tools/prompt-hints.js +1 -1
  347. package/dist/tools/prompt-hints.js.map +1 -1
  348. package/dist/tools/screenshot.js +1 -1
  349. package/dist/tools/screenshot.js.map +1 -1
  350. package/dist/tools/search-code.d.ts.map +1 -1
  351. package/dist/tools/search-code.js +6 -4
  352. package/dist/tools/search-code.js.map +1 -1
  353. package/dist/tools/skill.d.ts.map +1 -1
  354. package/dist/tools/skill.js +4 -2
  355. package/dist/tools/skill.js.map +1 -1
  356. package/dist/tools/source-path.d.ts.map +1 -1
  357. package/dist/tools/source-path.js +9 -0
  358. package/dist/tools/source-path.js.map +1 -1
  359. package/dist/tools/subagent-shared.d.ts +1 -1
  360. package/dist/tools/subagent-shared.d.ts.map +1 -1
  361. package/dist/tools/subagent-shared.js +4 -1
  362. package/dist/tools/subagent-shared.js.map +1 -1
  363. package/dist/tools/subagent-shared.test.js +21 -1
  364. package/dist/tools/subagent-shared.test.js.map +1 -1
  365. package/dist/tools/subagent.d.ts +1 -1
  366. package/dist/tools/subagent.d.ts.map +1 -1
  367. package/dist/tools/subagent.js +8 -0
  368. package/dist/tools/subagent.js.map +1 -1
  369. package/dist/tools/web-fetch.d.ts +2 -1
  370. package/dist/tools/web-fetch.d.ts.map +1 -1
  371. package/dist/tools/web-fetch.js +17 -6
  372. package/dist/tools/web-fetch.js.map +1 -1
  373. package/dist/tools/web-fetch.test.js +34 -0
  374. package/dist/tools/web-fetch.test.js.map +1 -1
  375. package/dist/tools/web-search.d.ts +2 -1
  376. package/dist/tools/web-search.d.ts.map +1 -1
  377. package/dist/tools/web-search.js +34 -7
  378. package/dist/tools/web-search.js.map +1 -1
  379. package/dist/tools/write.d.ts +2 -1
  380. package/dist/tools/write.d.ts.map +1 -1
  381. package/dist/tools/write.js +7 -1
  382. package/dist/tools/write.js.map +1 -1
  383. package/dist/tools/write.test.js +12 -0
  384. package/dist/tools/write.test.js.map +1 -1
  385. package/dist/ui/App.d.ts.map +1 -1
  386. package/dist/ui/App.js +22 -85
  387. package/dist/ui/App.js.map +1 -1
  388. package/dist/ui/app-items.d.ts +11 -12
  389. package/dist/ui/app-items.d.ts.map +1 -1
  390. package/dist/ui/app-items.js +10 -0
  391. package/dist/ui/app-items.js.map +1 -1
  392. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  393. package/dist/ui/components/ActivityIndicator.js +1 -0
  394. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  395. package/dist/ui/components/Footer.d.ts +1 -1
  396. package/dist/ui/components/Footer.js +2 -2
  397. package/dist/ui/components/Footer.js.map +1 -1
  398. package/dist/ui/components/Footer.test.js +1 -1
  399. package/dist/ui/hooks/useAgentLoop.d.ts +5 -2
  400. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  401. package/dist/ui/hooks/useAgentLoop.js +49 -8
  402. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  403. package/dist/ui/hooks/usePixelFixFlow.d.ts +1 -2
  404. package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -1
  405. package/dist/ui/hooks/usePixelFixFlow.js +2 -5
  406. package/dist/ui/hooks/usePixelFixFlow.js.map +1 -1
  407. package/dist/ui/hooks/useTranscriptHistory.d.ts +8 -5
  408. package/dist/ui/hooks/useTranscriptHistory.d.ts.map +1 -1
  409. package/dist/ui/hooks/useTranscriptHistory.js +2 -30
  410. package/dist/ui/hooks/useTranscriptHistory.js.map +1 -1
  411. package/dist/ui/login.js +1 -1
  412. package/dist/ui/login.js.map +1 -1
  413. package/dist/ui/terminal-history-status-renderers.d.ts +1 -2
  414. package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -1
  415. package/dist/ui/terminal-history-status-renderers.js +2 -15
  416. package/dist/ui/terminal-history-status-renderers.js.map +1 -1
  417. package/dist/ui/terminal-history.d.ts.map +1 -1
  418. package/dist/ui/terminal-history.js +2 -4
  419. package/dist/ui/terminal-history.js.map +1 -1
  420. package/dist/ui/thinking-level-cycle.test.js +8 -8
  421. package/dist/ui/thinking-level-cycle.test.js.map +1 -1
  422. package/dist/ui/transcript/MiscRows.d.ts +1 -4
  423. package/dist/ui/transcript/MiscRows.d.ts.map +1 -1
  424. package/dist/ui/transcript/MiscRows.js +2 -7
  425. package/dist/ui/transcript/MiscRows.js.map +1 -1
  426. package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
  427. package/dist/ui/transcript/TranscriptRenderer.js +1 -3
  428. package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
  429. package/dist/ui/transcript/presentation.d.ts +1 -9
  430. package/dist/ui/transcript/presentation.d.ts.map +1 -1
  431. package/dist/ui/transcript/presentation.js +0 -9
  432. package/dist/ui/transcript/presentation.js.map +1 -1
  433. package/dist/ui/transcript/spacing.d.ts +2 -2
  434. package/dist/ui/transcript/spacing.d.ts.map +1 -1
  435. package/dist/ui/transcript/spacing.js +0 -1
  436. package/dist/ui/transcript/spacing.js.map +1 -1
  437. package/dist/ui/transcript/spacing.test.js +1 -3
  438. package/dist/ui/transcript/spacing.test.js.map +1 -1
  439. package/dist/ui/tui-history-parity.test.js +2 -9
  440. package/dist/ui/tui-history-parity.test.js.map +1 -1
  441. package/dist/utils/github.d.ts +20 -0
  442. package/dist/utils/github.d.ts.map +1 -0
  443. package/dist/utils/github.js +65 -0
  444. package/dist/utils/github.js.map +1 -0
  445. package/dist/utils/github.test.d.ts +2 -0
  446. package/dist/utils/github.test.d.ts.map +1 -0
  447. package/dist/utils/github.test.js +23 -0
  448. package/dist/utils/github.test.js.map +1 -0
  449. package/dist/utils/http-body.d.ts +19 -0
  450. package/dist/utils/http-body.d.ts.map +1 -0
  451. package/dist/utils/http-body.js +58 -0
  452. package/dist/utils/http-body.js.map +1 -0
  453. package/dist/utils/http-body.test.d.ts +2 -0
  454. package/dist/utils/http-body.test.d.ts.map +1 -0
  455. package/dist/utils/http-body.test.js +68 -0
  456. package/dist/utils/http-body.test.js.map +1 -0
  457. package/dist/utils/process.d.ts +26 -3
  458. package/dist/utils/process.d.ts.map +1 -1
  459. package/dist/utils/process.js +68 -10
  460. package/dist/utils/process.js.map +1 -1
  461. package/dist/utils/process.test.d.ts +2 -0
  462. package/dist/utils/process.test.d.ts.map +1 -0
  463. package/dist/utils/process.test.js +42 -0
  464. package/dist/utils/process.test.js.map +1 -0
  465. package/dist/utils/text.d.ts +9 -0
  466. package/dist/utils/text.d.ts.map +1 -0
  467. package/dist/utils/text.js +11 -0
  468. package/dist/utils/text.js.map +1 -0
  469. package/package.json +4 -4
@@ -21,9 +21,10 @@ import { parseArgs } from "node:util";
21
21
  import { environmentSecrets, formatError, redactValue } from "@prestyj/ai";
22
22
  import { runJsonMode } from "./modes/json-mode.js";
23
23
  import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
24
+ import { setStreamDiagnostic } from "@prestyj/agent";
24
25
  import { AgentSession } from "./core/agent-session.js";
25
26
  import { RunLifecycle } from "./core/run-lifecycle.js";
26
- import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, sessionsDirForChatAgent, switchChatAgent, } from "./chat-agents/index.js";
27
+ import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
27
28
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
28
29
  import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
29
30
  import { buildNolanSystemPrompt, buildNolanAutopilotSystemPrompt } from "./core/nolan-prompt.js";
@@ -33,9 +34,12 @@ import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycl
33
34
  import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
34
35
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
35
36
  import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
37
+ import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
36
38
  import { AuthStorage } from "./core/auth-storage.js";
37
39
  import { cleanupToolOutputs } from "./tools/overflow.js";
38
- import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
40
+ import { readCappedBody } from "./utils/http-body.js";
41
+ import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@prestyj/core";
42
+ import { LocalEndpointError, addCustomEndpoint, listAllEndpoints, removeCustomEndpoint, syncEndpointCredentials, } from "./core/local-endpoint-store.js";
39
43
  import { loginAnthropic } from "./core/oauth/anthropic.js";
40
44
  import { loginOpenAI } from "./core/oauth/openai.js";
41
45
  import { loginGemini } from "./core/oauth/gemini.js";
@@ -43,19 +47,21 @@ import { loginKimi } from "./core/oauth/kimi.js";
43
47
  import { AUTH_PROVIDERS } from "./core/auth-providers.js";
44
48
  import { ensureAppDirs, loadSavedSettings } from "./config.js";
45
49
  import { SettingsManager } from "./core/settings-manager.js";
46
- import { getModel, getMaxThinkingLevel, getContextWindow, MODELS } from "./core/model-registry.js";
50
+ import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
47
51
  import { resolveStartOrFallback } from "./core/resolve-start.js";
48
52
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
53
+ import { getGitHubOpenCounts, getGitHubRepoSlug } from "./utils/github.js";
49
54
  import { extractPlanSteps } from "./utils/plan-steps.js";
50
55
  import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
51
56
  import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
52
57
  import { loadCustomCommands } from "./core/custom-commands.js";
53
- import { discoverProjects, listRecentSessions } from "./core/project-discovery.js";
58
+ import { discoverProjects } from "./core/project-discovery.js";
59
+ import { listSidecarSessions } from "./app-sidecar-sessions.js";
54
60
  import { loadTasksSync, saveTasksSync, pruneDoneTasksSync, getNextPendingTask, markTaskInProgress, } from "./core/task-store.js";
55
61
  import { initLogger, log } from "./core/logger.js";
56
62
  import { RADIO_STATIONS, getCurrentStation, getRadioVolume, playRadio, setRadioVolume, stopRadio, } from "./core/radio.js";
57
63
  import { enrichProcessPath } from "./core/shell-path.js";
58
- import { downscaleForPreview, validateVisionImage } from "./utils/image.js";
64
+ import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
59
65
  import { startServeMode } from "./modes/serve-mode.js";
60
66
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
61
67
  import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
@@ -64,6 +70,7 @@ import { loadProgress, peekProgress, updateProgress } from "./core/progress/stor
64
70
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
65
71
  import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
66
72
  import { rebuildFromSessions } from "./core/progress/rebuild.js";
73
+ import { captureSidecarError, flushSidecarErrors, shouldCaptureToolFailure, shouldCaptureUsagePollingError, wrapSidecarHandler, } from "./core/sidecar-error-reporter.js";
67
74
  const ALL_PROVIDERS = [
68
75
  // US
69
76
  "anthropic",
@@ -169,6 +176,7 @@ async function persistModelSelection(settingsFile, provider, model) {
169
176
  await sm.set("defaultModel", model);
170
177
  }
171
178
  catch (err) {
179
+ captureSidecarError(err, "app-sidecar.settings.persist-model");
172
180
  log("WARN", "app-sidecar", "failed to persist model selection", { err: String(err) });
173
181
  }
174
182
  }
@@ -185,6 +193,7 @@ async function persistThinkingLevel(settingsFile, level) {
185
193
  await sm.set("thinkingLevel", level);
186
194
  }
187
195
  catch (err) {
196
+ captureSidecarError(err, "app-sidecar.settings.persist-thinking");
188
197
  log("WARN", "app-sidecar", "failed to persist thinking level", { err: String(err) });
189
198
  }
190
199
  }
@@ -202,19 +211,28 @@ async function prepareAttachments(cwd, attachments) {
202
211
  const fileName = `${Date.now().toString(36)}-${safe}`;
203
212
  const filePath = path.join(dir, fileName);
204
213
  const buf = Buffer.from(a.data, "base64");
205
- // Validate image attachments before they become native image content blocks.
206
- // A corrupt or unsupported-format image (e.g. a malformed .ico, or a .png
207
- // with a bad IDAT) makes the provider reject the ENTIRE turn ("image data
208
- // ... not a valid image") before the agent can respond. Downgrade such files
209
- // to a plain "file" attachment so the model gets a path note and inspects
210
- // them with its tools instead of the request 400ing.
214
+ // Validate and cap image attachments before they become native image content
215
+ // blocks. Anthropic applies a 2000 px per-dimension cap once conversation
216
+ // history contains more than 20 images, so every attachment must be safe for
217
+ // later turns too. Keep the original on disk, but send the resized bytes.
218
+ // Corrupt or unsupported images become plain files so they cannot reject the
219
+ // entire provider request.
211
220
  let prepared = { ...a };
212
221
  if (a.kind === "image") {
213
- const validatedType = await validateVisionImage(buf).catch(() => null);
214
- if (validatedType)
215
- prepared = { ...a, mediaType: validatedType };
216
- else
222
+ try {
223
+ const resized = await shrinkToFit(buf, a.mediaType);
224
+ const validatedType = await validateVisionImage(resized.buffer);
225
+ prepared = validatedType
226
+ ? {
227
+ ...a,
228
+ mediaType: validatedType,
229
+ data: resized.buffer.toString("base64"),
230
+ }
231
+ : { ...a, kind: "file" };
232
+ }
233
+ catch {
217
234
  prepared = { ...a, kind: "file" };
235
+ }
218
236
  }
219
237
  try {
220
238
  await fs.writeFile(filePath, buf);
@@ -227,6 +245,9 @@ async function prepareAttachments(cwd, attachments) {
227
245
  return out;
228
246
  }
229
247
  const FILE_SEARCH_LIMIT = 20;
248
+ // Upper bound on files walked per search (baseline #8 memory cap). Far above the
249
+ // 20-result output limit, so relevance/recency ranking is unaffected in practice.
250
+ const FILE_SEARCH_SCAN_CAP = 50_000;
230
251
  /** Score a candidate path against a lowercased query. Higher is better; a
231
252
  * negative score means "no match". Basename hits beat path hits; prefix beats
232
253
  * substring; shorter paths break ties. */
@@ -279,8 +300,13 @@ async function searchProjectFiles(cwd, rawQuery) {
279
300
  }
280
301
  const ig = ignore.default().add(gitignore);
281
302
  // `stats: true` gives mtime without a second stat pass, so the empty-query
282
- // "recent files" path is a single walk.
283
- const entries = await fg.default("**/*", {
303
+ // "recent files" path is a single walk. Stream + hard scan cap (baseline #8):
304
+ // collecting the full result array retained ~0.9 MB per 1k files (18.5 MB at
305
+ // 20k), unbounded. Bail after FILE_SEARCH_SCAN_CAP entries so a giant repo
306
+ // can't balloon RSS; the newest/most-relevant matches still surface because
307
+ // the cap is far above the 20-result output limit.
308
+ const entries = [];
309
+ const scanStream = fg.default.stream("**/*", {
284
310
  cwd,
285
311
  dot: false,
286
312
  onlyFiles: true,
@@ -289,6 +315,17 @@ async function searchProjectFiles(cwd, rawQuery) {
289
315
  followSymbolicLinks: false,
290
316
  stats: true,
291
317
  });
318
+ for await (const entry of scanStream) {
319
+ entries.push(entry);
320
+ if (entries.length >= FILE_SEARCH_SCAN_CAP) {
321
+ scanStream.destroy();
322
+ log("DEBUG", "app-sidecar", "file search scan cap hit", {
323
+ cap: String(FILE_SEARCH_SCAN_CAP),
324
+ cwd,
325
+ });
326
+ break;
327
+ }
328
+ }
292
329
  const files = entries.filter((e) => !ig.ignores(e.path));
293
330
  if (!query) {
294
331
  return files
@@ -311,6 +348,11 @@ async function searchProjectFiles(cwd, rawQuery) {
311
348
  * hook prompt, by its distinctive opening phrase. Returns the hook kind so the
312
349
  * webview can render the short notice line instead of the full prompt body.
313
350
  */
351
+ /** `?tools=` on /export. Anything unrecognised (including absent) falls back to
352
+ * the human-facing `summary` default rather than dumping every payload. */
353
+ function parseToolDetail(value) {
354
+ return value === "none" || value === "full" ? value : "summary";
355
+ }
314
356
  function detectHookKind(text) {
315
357
  const t = text.trimStart();
316
358
  if (t.startsWith("Ideal? Review the actual work"))
@@ -418,6 +460,7 @@ async function runJsonModeIfRequested() {
418
460
  "max-turns": { type: "string" },
419
461
  "system-prompt": { type: "string" },
420
462
  tools: { type: "string" },
463
+ "mcp-servers": { type: "string" },
421
464
  "prompt-cache-key": { type: "string" },
422
465
  },
423
466
  allowPositionals: true,
@@ -434,16 +477,30 @@ async function runJsonModeIfRequested() {
434
477
  .filter(Boolean)
435
478
  : [];
436
479
  const allowedTools = parsedTools.length > 0 ? parsedTools : undefined;
480
+ // MCP servers the agent definition asked for. Without forwarding these, an
481
+ // allow-listed child connects no MCP at all and loses live code search.
482
+ const parsedMcpServers = values["mcp-servers"]
483
+ ? values["mcp-servers"]
484
+ .split(",")
485
+ .map((s) => s.trim())
486
+ .filter(Boolean)
487
+ : [];
488
+ const allowedMcpServers = parsedMcpServers.length > 0 ? parsedMcpServers : undefined;
437
489
  await runJsonMode({
438
490
  message: positionals[0] ?? "",
439
491
  provider: (values.provider ?? "anthropic"),
440
- model: values.model ?? "claude-opus-4-8",
492
+ model: values.model ?? "claude-opus-5",
441
493
  cwd: process.cwd(),
442
494
  systemPrompt: values["system-prompt"],
443
495
  maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
444
496
  allowedTools,
497
+ allowedMcpServers,
445
498
  promptCacheKey: values["prompt-cache-key"],
446
- }).catch((err) => {
499
+ }).catch(async (err) => {
500
+ captureSidecarError(err, "app-sidecar.json-mode", {
501
+ provider: String(values.provider ?? "anthropic"),
502
+ });
503
+ await flushSidecarErrors();
447
504
  process.stderr.write((err instanceof Error ? err.message : String(err)) + "\n");
448
505
  process.exit(1);
449
506
  });
@@ -452,13 +509,8 @@ async function runJsonModeIfRequested() {
452
509
  // ── Daemon-level HTTP helpers (shared by the session-management routes) ─────
453
510
  // The per-session route table has its own local copies; these serve the
454
511
  // daemon's own POST /session / DELETE /session routes.
455
- function daemonReadBody(req) {
456
- return new Promise((resolve, reject) => {
457
- const chunks = [];
458
- req.on("data", (c) => chunks.push(c));
459
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
460
- req.on("error", reject);
461
- });
512
+ function daemonReadBody(req, res) {
513
+ return readCappedBody(req, res);
462
514
  }
463
515
  function daemonJson(res, status, body) {
464
516
  res.writeHead(status, {
@@ -486,6 +538,31 @@ async function main() {
486
538
  // ~/.ezcoder/debug.log (initLogger truncates on each start).
487
539
  const sidecarLog = path.join(paths.agentDir, "ezcoder-app-sidecar.log");
488
540
  initLogger(sidecarLog);
541
+ // The desktop sidecar previously omitted the stream diagnostic hook used by
542
+ // the CLI, leaving device-specific provider stalls impossible to distinguish from
543
+ // event-loop starvation or a broken streaming network path. Keep routine
544
+ // phases lightweight; timeout phases include non-sensitive runtime context.
545
+ setStreamDiagnostic((phase, data) => {
546
+ const includeRuntime = phase === "idle_timeout_fired" ||
547
+ phase === "hard_timeout_fired" ||
548
+ phase === "stall_exhausted";
549
+ log("INFO", "stream", phase, {
550
+ ...(data ?? {}),
551
+ ...(includeRuntime
552
+ ? {
553
+ platform: process.platform,
554
+ arch: process.arch,
555
+ osRelease: os.release(),
556
+ node: process.version,
557
+ logicalCpus: os.cpus().length,
558
+ totalMemoryMb: Math.round(os.totalmem() / 1024 / 1024),
559
+ freeMemoryMb: Math.round(os.freemem() / 1024 / 1024),
560
+ rssMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
561
+ processUptimeSec: Math.round(process.uptime()),
562
+ }
563
+ : {}),
564
+ });
565
+ });
489
566
  // Global last-resort guards, installed as early as the logger allows so they
490
567
  // cover the WHOLE lifecycle — including startup/initialize, the phase the
491
568
  // "sidecar did not start in time" bug lives in. The sidecar is a long-lived
@@ -544,18 +621,24 @@ async function main() {
544
621
  });
545
622
  const usageCache = new Map();
546
623
  const usageRequests = new Map();
547
- // 429 backoff: when the provider rate-limits the usage endpoint, hold the
548
- // error result until this timestamp instead of re-polling (and re-logging a
549
- // WARN) every 60s — the old cadence hammered a limited endpoint for hours.
624
+ // 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
625
+ // provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
626
+ // the provider value so a malformed header can neither hammer the endpoint
627
+ // nor suppress usage data forever.
550
628
  const usageRateLimitedUntil = new Map();
551
- const USAGE_RATE_LIMIT_BACKOFF_MS = 5 * 60_000;
629
+ const USAGE_RATE_LIMIT_FALLBACK_BACKOFF_MS = 30 * 60_000;
630
+ const USAGE_RATE_LIMIT_MIN_BACKOFF_MS = 60_000;
631
+ const USAGE_RATE_LIMIT_MAX_BACKOFF_MS = 24 * 60 * 60_000;
552
632
  async function fetchUsageProvider(provider) {
553
- const displayName = provider === "anthropic" ? "Anthropic" : "Codex";
554
- if (!(await auth.hasProviderAuth(provider))) {
633
+ const displayName = provider === "anthropic" ? "Anthropic" : provider === "openai" ? "Codex" : "Kimi";
634
+ // Kimi plan usage is tracked on the OAuth credential specifically — the
635
+ // Moonshot platform API key is metered per-token, not per plan window.
636
+ const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
637
+ if (!(await auth.hasProviderAuth(authKey))) {
555
638
  return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
556
639
  }
557
640
  try {
558
- let credentials = await auth.resolveCredentials(provider);
641
+ let credentials = await auth.resolveCredentials(authKey);
559
642
  try {
560
643
  const snapshot = {
561
644
  ...(await fetchSubscriptionUsage(provider, credentials)),
@@ -568,7 +651,7 @@ async function main() {
568
651
  // A provider can revoke an access token before its stored expiry. Refresh
569
652
  // once on 401, matching inference auth recovery, then retry the usage call.
570
653
  if (error instanceof SubscriptionUsageError && error.status === 401) {
571
- credentials = await auth.resolveCredentials(provider, { forceRefresh: true });
654
+ credentials = await auth.resolveCredentials(authKey, { forceRefresh: true });
572
655
  const snapshot = {
573
656
  ...(await fetchSubscriptionUsage(provider, credentials)),
574
657
  connected: true,
@@ -581,11 +664,20 @@ async function main() {
581
664
  }
582
665
  catch (error) {
583
666
  const message = error instanceof Error ? error.message : String(error);
584
- log("WARN", "app-sidecar", "subscription usage fetch failed", { provider, message });
667
+ if (shouldCaptureUsagePollingError(error)) {
668
+ captureSidecarError(error, "app-sidecar.usage.fetch", { provider });
669
+ }
670
+ let backoffMs;
585
671
  if (error instanceof SubscriptionUsageError && error.status === 429) {
586
- usageRateLimitedUntil.set(provider, Date.now() + USAGE_RATE_LIMIT_BACKOFF_MS);
672
+ backoffMs = Math.min(USAGE_RATE_LIMIT_MAX_BACKOFF_MS, Math.max(USAGE_RATE_LIMIT_MIN_BACKOFF_MS, error.retryAfterMs ?? USAGE_RATE_LIMIT_FALLBACK_BACKOFF_MS));
673
+ usageRateLimitedUntil.set(provider, Date.now() + backoffMs);
587
674
  }
588
- const connected = await auth.hasProviderAuth(provider);
675
+ log("WARN", "app-sidecar", "subscription usage fetch failed", {
676
+ provider,
677
+ message,
678
+ ...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
679
+ });
680
+ const connected = await auth.hasProviderAuth(authKey);
589
681
  return {
590
682
  provider,
591
683
  displayName,
@@ -640,7 +732,7 @@ async function main() {
640
732
  return null;
641
733
  }
642
734
  }
643
- const server = http.createServer((req, res) => {
735
+ const server = http.createServer(wrapSidecarHandler((req, res) => {
644
736
  const url = req.url ?? "/";
645
737
  const method = req.method ?? "GET";
646
738
  // CORS preflight — the webview origin differs from 127.0.0.1.
@@ -656,7 +748,9 @@ async function main() {
656
748
  // ── Daemon-level routes (session lifecycle) ──────────────────────────
657
749
  // Create a session for a window: { mode?, cwd, sessionPath? } → { sessionId }.
658
750
  if (method === "POST" && url === "/session") {
659
- void daemonReadBody(req).then(async (raw) => {
751
+ void daemonReadBody(req, res).then(async (raw) => {
752
+ if (raw === null)
753
+ return;
660
754
  let body = {};
661
755
  try {
662
756
  body = raw ? JSON.parse(raw) : {};
@@ -684,6 +778,7 @@ async function main() {
684
778
  }
685
779
  catch (err) {
686
780
  const message = err instanceof Error ? err.message : String(err);
781
+ captureSidecarError(err, "app-sidecar.session.create");
687
782
  log("ERROR", "app-sidecar", "session create failed", { message });
688
783
  daemonJson(res, 500, { error: message });
689
784
  }
@@ -714,12 +809,13 @@ async function main() {
714
809
  if (method === "GET" && (url === "/usage" || url.startsWith("/usage?"))) {
715
810
  void (async () => {
716
811
  const provider = new URL(url, `http://${host}`).searchParams.get("provider");
717
- if (provider !== "anthropic" && provider !== "openai") {
812
+ if (provider !== "anthropic" && provider !== "openai" && provider !== "moonshot") {
718
813
  daemonJson(res, 400, { error: "unsupported usage provider" });
719
814
  return;
720
815
  }
721
816
  daemonJson(res, 200, await subscriptionUsage(provider));
722
817
  })().catch((error) => {
818
+ captureSidecarError(error, "app-sidecar.usage.request");
723
819
  log("ERROR", "app-sidecar", "subscription usage request failed", {
724
820
  message: error instanceof Error ? error.message : String(error),
725
821
  });
@@ -735,7 +831,7 @@ async function main() {
735
831
  return;
736
832
  }
737
833
  ctx.handle(req, res, url, method);
738
- });
834
+ }, "app-sidecar.http"));
739
835
  server.listen(port, host, () => {
740
836
  const addr = server.address();
741
837
  // The Rust shell reads this line to learn the daemon port.
@@ -752,6 +848,8 @@ async function main() {
752
848
  // Radio playback is app-wide (one stream across all windows), so it stops
753
849
  // at the daemon level, not per session.
754
850
  stopRadio();
851
+ // Close the ~/.ezcoder progress fs.watch handle (baseline #8 leak fix).
852
+ progress.dispose();
755
853
  await Promise.all([...sessions.values()].map((c) => c.dispose().catch(() => { })));
756
854
  server.close();
757
855
  process.exit(0);
@@ -880,6 +978,7 @@ async function createProgressManager(agentDir, broadcastAll) {
880
978
  broadcastAll(buildSnapshot(updated), originId);
881
979
  }
882
980
  catch (err) {
981
+ captureSidecarError(err, "app-sidecar.progress.award");
883
982
  log("DEBUG", "app-sidecar", "progress award failed", {
884
983
  message: err instanceof Error ? err.message : String(err),
885
984
  });
@@ -888,6 +987,7 @@ async function createProgressManager(agentDir, broadcastAll) {
888
987
  // Watch ~/.ezcoder (dir watch survives the atomic tmp+rename) for progress.json
889
988
  // writes from other daemon processes; debounce, reload read-only, dedupe by nonce.
890
989
  let watchDebounce = null;
990
+ let progressWatcher = null;
891
991
  try {
892
992
  const watcher = fsWatch(agentDir, (_event, filename) => {
893
993
  if (filename !== "progress.json")
@@ -909,13 +1009,30 @@ async function createProgressManager(agentDir, broadcastAll) {
909
1009
  }, 150);
910
1010
  });
911
1011
  watcher.unref();
1012
+ progressWatcher = watcher;
912
1013
  }
913
1014
  catch (err) {
1015
+ captureSidecarError(err, "app-sidecar.progress.watch");
914
1016
  log("DEBUG", "app-sidecar", "progress watch unavailable", {
915
1017
  message: err instanceof Error ? err.message : String(err),
916
1018
  });
917
1019
  }
918
- return { snapshot, awardRun };
1020
+ // Dispose closes the fs.watch handle (baseline #8: it was previously never
1021
+ // closed — a per-daemon leak) and clears any pending debounce timer.
1022
+ function dispose() {
1023
+ if (watchDebounce) {
1024
+ clearTimeout(watchDebounce);
1025
+ watchDebounce = null;
1026
+ }
1027
+ try {
1028
+ progressWatcher?.close();
1029
+ }
1030
+ catch {
1031
+ // Already closed / never opened — nothing to do.
1032
+ }
1033
+ progressWatcher = null;
1034
+ }
1035
+ return { snapshot, awardRun, dispose };
919
1036
  }
920
1037
  /**
921
1038
  * Build one in-process agent session: its AgentSession, SSE client set, event
@@ -934,6 +1051,10 @@ async function createSession(deps, opts) {
934
1051
  // parsing); the daemon owns the real listen host.
935
1052
  const host = "127.0.0.1";
936
1053
  const saved = loadSavedSettings(paths.settingsFile);
1054
+ // Native login/logout and other live sessions share auth.json. Refresh the
1055
+ // daemon-level snapshot before choosing this session's provider so a project
1056
+ // never boots against credentials that were just replaced or disconnected.
1057
+ await auth.load();
937
1058
  // Per-project model/thinking prefs win over the shared global settings.json:
938
1059
  // each window (one project cwd) restores its own selection instead of every
939
1060
  // window reading the same single global slot that the last writer clobbered
@@ -952,9 +1073,14 @@ async function createSession(deps, opts) {
952
1073
  });
953
1074
  }
954
1075
  // Per-project thinking prefs win over the global settings.json fallback.
1076
+ // With no saved level, the default follows the active credential's endpoint:
1077
+ // Kimi K3 on the OAuth coding endpoint starts at its declared default (high),
1078
+ // matching the official kimi-code CLI's plan-usage profile.
955
1079
  const thinkEnabled = projectPrefs?.thinkingEnabled ?? saved.thinkingEnabled;
956
1080
  const thinkingLevel = thinkEnabled
957
- ? (projectPrefs?.thinkingLevel ?? saved.thinkingLevel ?? getMaxThinkingLevel(model))
1081
+ ? (projectPrefs?.thinkingLevel ??
1082
+ saved.thinkingLevel ??
1083
+ getDefaultThinkingLevel(model, { baseUrl: auth.getStoredBaseUrl(provider) }))
958
1084
  : undefined;
959
1085
  // ── SSE fan-out (declared before the session so plan callbacks can use it) ─
960
1086
  const clients = new Set();
@@ -991,6 +1117,25 @@ async function createSession(deps, opts) {
991
1117
  // /help
992
1118
  .replaceAll(/see \/help/gi, "check the help menu"));
993
1119
  }
1120
+ /**
1121
+ * Guidance for a connection failure against the ACTIVE local endpoint, or
1122
+ * undefined when that isn't the situation (so the generic wording stands).
1123
+ * "Disable your VPN / allow us through the firewall" is useless advice for a
1124
+ * server running on this machine — name what the user has to restart.
1125
+ */
1126
+ function localNetworkGuidance(source) {
1127
+ if (source !== "network")
1128
+ return undefined;
1129
+ const state = session.getState();
1130
+ if (state.provider !== "local")
1131
+ return undefined;
1132
+ const parsed = parseLocalModelId(state.model);
1133
+ const probe = localProbes.find((p) => p.endpoint.id === parsed?.endpointId);
1134
+ if (!probe)
1135
+ return undefined;
1136
+ return (`${probe.endpoint.label} stopped responding at ${probe.endpoint.baseUrl}. ` +
1137
+ "Start it again and retry, or pick another model.");
1138
+ }
994
1139
  // Turn any thrown value into the same clear headline/message/guidance shape
995
1140
  // the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
996
1141
  // the full detail, and broadcast it under `type` ("error" or "nolan_error").
@@ -1000,7 +1145,12 @@ async function createSession(deps, opts) {
1000
1145
  function broadcastError(type, logLabel, err) {
1001
1146
  const f = formatError(err);
1002
1147
  const message = f.message ? desktopGuidance(f.message) : undefined;
1003
- const guidance = desktopGuidance(f.guidance);
1148
+ const guidance = localNetworkGuidance(f.source) ?? desktopGuidance(f.guidance);
1149
+ captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
1150
+ scope: type,
1151
+ ...(f.provider ? { provider: f.provider } : {}),
1152
+ ...(f.statusCode != null ? { status: String(f.statusCode) } : {}),
1153
+ });
1004
1154
  log("ERROR", "app-sidecar", logLabel, {
1005
1155
  headline: f.headline,
1006
1156
  source: f.source,
@@ -1058,6 +1208,7 @@ async function createSession(deps, opts) {
1058
1208
  chatAgent = nextAgent;
1059
1209
  broadcast("chat_agent_change", { chatAgent: nextAgent });
1060
1210
  await session.persistAppMarker("agent_handoff", { chatAgent: nextAgent }).catch((error) => {
1211
+ captureSidecarError(error, "app-sidecar.chat-agent.persist-handoff");
1061
1212
  log("WARN", "app-sidecar", "agent handoff marker persist failed", {
1062
1213
  message: error instanceof Error ? error.message : String(error),
1063
1214
  });
@@ -1110,17 +1261,116 @@ async function createSession(deps, opts) {
1110
1261
  }
1111
1262
  }
1112
1263
  log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
1264
+ // ── Local models (Ollama / LM Studio / llama.cpp / vLLM) ──
1265
+ // Probing four HTTP endpoints must never delay readiness, so this runs in the
1266
+ // background (same shape as backgroundMcpConnect) and pushes a models_change
1267
+ // frame when it lands.
1268
+ let localProbes = [];
1269
+ async function scanLocalModels(force) {
1270
+ const endpoints = await listAllEndpoints();
1271
+ const { probes, models } = await discoverLocalModels(endpoints, { force });
1272
+ localProbes = probes;
1273
+ // Only endpoints that answered get a credential: writing one for a server
1274
+ // that isn't running would make `hasProviderAuth("local")` true forever.
1275
+ await syncEndpointCredentials(probes.filter((probe) => probe.reachable).map((probe) => probe.endpoint));
1276
+ clearRuntimeModels((m) => m.provider === "local");
1277
+ registerRuntimeModels(models);
1278
+ log("INFO", "app-sidecar", "local model scan", {
1279
+ reachable: probes.filter((p) => p.reachable).length + "/" + probes.length,
1280
+ models: String(models.length),
1281
+ });
1282
+ return probes;
1283
+ }
1284
+ /** Endpoint rows + models, in the shape the Local models UI renders. */
1285
+ function localStatePayload() {
1286
+ return {
1287
+ endpoints: localProbes.map((probe) => ({
1288
+ id: probe.endpoint.id,
1289
+ label: probe.endpoint.label,
1290
+ baseUrl: probe.endpoint.baseUrl,
1291
+ kind: probe.endpoint.kind,
1292
+ custom: probe.endpoint.custom === true,
1293
+ reachable: probe.reachable,
1294
+ ...(probe.reason ? { reason: probe.reason } : {}),
1295
+ models: probe.models.map((m) => ({
1296
+ id: formatLocalModelId(probe.endpoint.id, m.rawId),
1297
+ rawId: m.rawId,
1298
+ contextWindow: m.contextWindow,
1299
+ contextWindowKnown: m.contextWindowKnown,
1300
+ supportsTools: m.supportsTools,
1301
+ supportsImages: m.supportsImages,
1302
+ supportsThinking: m.supportsThinking,
1303
+ ...(m.loaded === undefined ? {} : { loaded: m.loaded }),
1304
+ })),
1305
+ })),
1306
+ };
1307
+ }
1308
+ /**
1309
+ * Why `modelId` can't be selected right now, or `undefined` when it can.
1310
+ * Two real footguns get a clear answer instead of a mid-run provider error:
1311
+ * a model with no tool calling (can't drive the agent at all), and a server
1312
+ * that has since been shut down.
1313
+ */
1314
+ async function localModelBlocker(modelId) {
1315
+ const parsed = parseLocalModelId(modelId);
1316
+ if (!parsed)
1317
+ return undefined;
1318
+ const endpoints = await listAllEndpoints();
1319
+ const endpoint = endpoints.find((e) => e.id === parsed.endpointId);
1320
+ if (!endpoint)
1321
+ return `Unknown local endpoint "${parsed.endpointId}" — re-scan for local models.`;
1322
+ const probe = await probeEndpoint(endpoint);
1323
+ // Keep the cached view honest: this probe is fresher than the last scan.
1324
+ localProbes = localProbes.map((p) => (p.endpoint.id === endpoint.id ? probe : p));
1325
+ if (!probe.reachable) {
1326
+ return `${endpoint.label} isn't running at ${endpoint.baseUrl}. Start it and scan again.`;
1327
+ }
1328
+ const model = probe.models.find((m) => m.rawId === parsed.rawId);
1329
+ if (!model) {
1330
+ return `${endpoint.label} no longer serves "${parsed.rawId}".`;
1331
+ }
1332
+ if (!model.supportsTools) {
1333
+ return `${parsed.rawId} has no tool calling, so it can't run the agent. Pick a tool-capable model.`;
1334
+ }
1335
+ registerRuntimeModels(probe.models.map((m) => localModelInfo(m, endpoint)));
1336
+ return undefined;
1337
+ }
1338
+ /**
1339
+ * A restored per-project pref can ask for thinking on a local model that
1340
+ * turns out not to reason (capabilities are only known after a probe). Drop
1341
+ * the level once we know, so the first prompt doesn't carry a
1342
+ * `reasoning_effort` the server rejects.
1343
+ */
1344
+ function clampLocalThinking() {
1345
+ const st = session.getState();
1346
+ const level = session.getThinkingLevel();
1347
+ if (!level || st.provider !== "local")
1348
+ return;
1349
+ if (isThinkingLevelSupported(st.provider, st.model, level))
1350
+ return;
1351
+ session.setThinkingLevel(undefined);
1352
+ broadcast("thinking_change", {
1353
+ thinkingLevel: null,
1354
+ supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
1355
+ });
1356
+ }
1113
1357
  // Workspace extras (context window, git status, background tasks). Git state
1114
1358
  // is resolved once at startup and refreshed after every run; the context
1115
1359
  // window follows the active model.
1116
- const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
1360
+ const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount, initialGitHubSlug] = await Promise.all([
1117
1361
  getGitBranch(cwd).catch(() => null),
1118
1362
  isGitRepo(cwd).catch(() => false),
1119
1363
  getGitDirtyFileCount(cwd).catch(() => 0),
1364
+ getGitHubRepoSlug(cwd).catch(() => null),
1120
1365
  ]);
1121
1366
  let gitBranch = initialGitBranch;
1122
1367
  let gitIsRepo = initialGitIsRepo;
1123
1368
  let gitDirtyFileCount = initialDirtyFileCount;
1369
+ // Open issue/PR counts for the origin repo's GitHub slug, via the `gh` CLI's
1370
+ // auth. null = unknown (gh missing/unauthed, non-GitHub origin) → chips hidden.
1371
+ const gitHubSlug = initialGitHubSlug;
1372
+ let gitHubIssues = null;
1373
+ let gitHubPRs = null;
1124
1374
  function currentContextWindow() {
1125
1375
  const st = session.getState();
1126
1376
  return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
@@ -1133,9 +1383,41 @@ async function createSession(deps, opts) {
1133
1383
  gitBranch,
1134
1384
  isGitRepo: gitIsRepo,
1135
1385
  gitDirtyFileCount,
1386
+ gitHubIssues,
1387
+ gitHubPRs,
1388
+ gitHubRepoUrl: gitHubSlug ? `https://github.com/${gitHubSlug}` : null,
1136
1389
  tasks: session.listBackgroundProcesses(),
1390
+ // Roots added with /add-dir — the header shows a badge when non-empty.
1391
+ additionalRoots: session.getAdditionalRoots(),
1137
1392
  };
1138
1393
  }
1394
+ void scanLocalModels(false)
1395
+ .then(() => {
1396
+ clampLocalThinking();
1397
+ broadcast("extras", footerExtras());
1398
+ })
1399
+ .then(() => broadcast("models_change", { local: localStatePayload() }))
1400
+ .catch((err) => {
1401
+ // A discovery failure is never fatal — the user simply has no local
1402
+ // models. Log it; don't push an error row into the transcript.
1403
+ log("WARN", "app-sidecar", "local model scan failed", {
1404
+ error: err instanceof Error ? err.message : String(err),
1405
+ });
1406
+ });
1407
+ // Refresh the GitHub counts and broadcast only on change. Transient failures
1408
+ // keep the last-known numbers so the chips don't flicker off on a timeout.
1409
+ async function refreshGitHubCounts() {
1410
+ if (!gitHubSlug)
1411
+ return;
1412
+ const counts = await getGitHubOpenCounts(gitHubSlug);
1413
+ if (!counts)
1414
+ return;
1415
+ if (counts.issues !== gitHubIssues || counts.prs !== gitHubPRs) {
1416
+ gitHubIssues = counts.issues;
1417
+ gitHubPRs = counts.prs;
1418
+ broadcast("extras", footerExtras());
1419
+ }
1420
+ }
1139
1421
  // tool_call_end carries no tool name (only the id), so remember each call's
1140
1422
  // name from tool_call_start to log a useful line on completion. Mirrors the
1141
1423
  // CLI's logging so the app sidecar's ~/.ezcoder/ezcoder-app-sidecar.log records tool
@@ -1184,6 +1466,7 @@ async function createSession(deps, opts) {
1184
1466
  .catch(() => false)
1185
1467
  .then(() => syncApprovedPlanProgress(generation))
1186
1468
  .catch((error) => {
1469
+ captureSidecarError(error, "app-sidecar.plan.refresh-progress");
1187
1470
  log("WARN", "app-sidecar", "plan progress refresh failed", {
1188
1471
  message: error instanceof Error ? error.message : String(error),
1189
1472
  });
@@ -1238,6 +1521,11 @@ async function createSession(deps, opts) {
1238
1521
  session.eventBus.on("tool_call_end", (d) => {
1239
1522
  const name = toolCallNames.get(d.toolCallId) ?? "unknown";
1240
1523
  toolCallNames.delete(d.toolCallId);
1524
+ if (d.isError && shouldCaptureToolFailure(name, d.result)) {
1525
+ // Expected model-correctable validation failures stay in the local log and
1526
+ // conversation. Unexpected failures are reported without private result data.
1527
+ captureSidecarError(new Error(`Tool ${name} failed`), `tool.${name}`, { tool: name });
1528
+ }
1241
1529
  log(d.isError ? "ERROR" : "INFO", "tool", `Tool call ended: ${name}`, {
1242
1530
  id: d.toolCallId,
1243
1531
  durationMs: String(d.durationMs),
@@ -1264,6 +1552,9 @@ async function createSession(deps, opts) {
1264
1552
  session.eventBus.on("server_tool_call", (d) => broadcast("server_tool_call", d));
1265
1553
  session.eventBus.on("turn_end", (d) => broadcast("turn_end", d));
1266
1554
  session.eventBus.on("agent_done", (d) => broadcast("agent_done", d));
1555
+ // Non-clean stop (max_tokens/refusal/provider error) — info-style frame so
1556
+ // the webview can warn instead of presenting truncated output as complete.
1557
+ session.eventBus.on("truncated", (d) => broadcast("truncated", d));
1267
1558
  session.eventBus.on("error", (d) => {
1268
1559
  broadcastError("error", "agent error", d.error);
1269
1560
  });
@@ -1568,6 +1859,9 @@ async function createSession(deps, opts) {
1568
1859
  isGitRepo(cwd).catch(() => gitIsRepo),
1569
1860
  getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
1570
1861
  ]);
1862
+ // A run may have opened/closed issues or PRs — refresh fire-and-forget so
1863
+ // teardown isn't delayed by the network. Broadcasts itself on change.
1864
+ void refreshGitHubCounts();
1571
1865
  // Serialize behind any marker/tool-triggered refresh so the terminal
1572
1866
  // progress snapshot uses the live plan file. Once every canonical step
1573
1867
  // is complete, remove the approved plan from future system prompts and
@@ -1584,6 +1878,7 @@ async function createSession(deps, opts) {
1584
1878
  catch (error) {
1585
1879
  // Keep tracking when prompt cleanup fails; hiding the widget here
1586
1880
  // would claim completion while the approved-plan contract remained.
1881
+ captureSidecarError(error, "app-sidecar.plan.cleanup");
1587
1882
  log("WARN", "app-sidecar", "completed plan cleanup failed", {
1588
1883
  message: error instanceof Error ? error.message : String(error),
1589
1884
  });
@@ -2039,13 +2334,22 @@ async function createSession(deps, opts) {
2039
2334
  gitPoll.unref?.();
2040
2335
  };
2041
2336
  scheduleGitPoll(5000);
2042
- function readBody(req) {
2043
- return new Promise((resolve, reject) => {
2044
- const chunks = [];
2045
- req.on("data", (c) => chunks.push(c));
2046
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
2047
- req.on("error", reject);
2048
- });
2337
+ // GitHub issue/PR counts change outside the app (web UI, teammates), so poll
2338
+ // on a slow cadence. Network-bound, so keep it well under the search API's
2339
+ // rate budget (2 calls per tick). No-op when the origin isn't a GitHub repo.
2340
+ let gitHubPoll;
2341
+ let gitHubPollStopped = false;
2342
+ const scheduleGitHubPoll = (delay) => {
2343
+ if (gitHubPollStopped)
2344
+ return;
2345
+ gitHubPoll = setTimeout(() => {
2346
+ void refreshGitHubCounts().finally(() => scheduleGitHubPoll(60_000));
2347
+ }, delay);
2348
+ gitHubPoll.unref?.();
2349
+ };
2350
+ scheduleGitHubPoll(2000);
2351
+ function readBody(req, res) {
2352
+ return readCappedBody(req, res);
2049
2353
  }
2050
2354
  function json(res, status, body) {
2051
2355
  const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
@@ -2083,7 +2387,10 @@ async function createSession(deps, opts) {
2083
2387
  void memoryStore
2084
2388
  .snapshot()
2085
2389
  .then((snapshot) => json(res, 200, snapshot))
2086
- .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
2390
+ .catch((error) => {
2391
+ captureSidecarError(error, "app-sidecar.memory.snapshot");
2392
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2393
+ });
2087
2394
  return;
2088
2395
  }
2089
2396
  if (method === "DELETE" && url.startsWith("/memories/")) {
@@ -2096,14 +2403,20 @@ async function createSession(deps, opts) {
2096
2403
  .forget(id)
2097
2404
  .then(() => memoryStore.snapshot())
2098
2405
  .then((snapshot) => json(res, 200, snapshot))
2099
- .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
2406
+ .catch((error) => {
2407
+ captureSidecarError(error, "app-sidecar.memory.forget");
2408
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2409
+ });
2100
2410
  return;
2101
2411
  }
2102
2412
  if (method === "GET" && url === "/jiwa") {
2103
2413
  void jiwaStore
2104
2414
  .snapshot()
2105
2415
  .then((snapshot) => json(res, 200, snapshot))
2106
- .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
2416
+ .catch((error) => {
2417
+ captureSidecarError(error, "app-sidecar.jiwa.snapshot");
2418
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2419
+ });
2107
2420
  return;
2108
2421
  }
2109
2422
  if (method === "DELETE" && url.startsWith("/jiwa/")) {
@@ -2116,7 +2429,10 @@ async function createSession(deps, opts) {
2116
2429
  .forget(id)
2117
2430
  .then(() => jiwaStore.snapshot())
2118
2431
  .then((snapshot) => json(res, 200, snapshot))
2119
- .catch((error) => json(res, 500, { error: error instanceof Error ? error.message : String(error) }));
2432
+ .catch((error) => {
2433
+ captureSidecarError(error, "app-sidecar.jiwa.forget");
2434
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2435
+ });
2120
2436
  return;
2121
2437
  }
2122
2438
  if (method === "GET" && (url === "/events" || url.startsWith("/events?"))) {
@@ -2174,7 +2490,9 @@ async function createSession(deps, opts) {
2174
2490
  return;
2175
2491
  }
2176
2492
  if (method === "POST" && url === "/settings") {
2177
- void readBody(req).then(async (raw) => {
2493
+ void readBody(req, res).then(async (raw) => {
2494
+ if (raw === null)
2495
+ return;
2178
2496
  let projectsRoot;
2179
2497
  try {
2180
2498
  projectsRoot = JSON.parse(raw).projectsRoot ?? "";
@@ -2197,7 +2515,9 @@ async function createSession(deps, opts) {
2197
2515
  return;
2198
2516
  }
2199
2517
  if (method === "POST" && url === "/create-project") {
2200
- void readBody(req).then(async (raw) => {
2518
+ void readBody(req, res).then(async (raw) => {
2519
+ if (raw === null)
2520
+ return;
2201
2521
  let name;
2202
2522
  try {
2203
2523
  name = JSON.parse(raw).name ?? "";
@@ -2229,6 +2549,7 @@ async function createSession(deps, opts) {
2229
2549
  json(res, 200, { path: dir });
2230
2550
  }
2231
2551
  catch (err) {
2552
+ captureSidecarError(err, "app-sidecar.project.create");
2232
2553
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
2233
2554
  }
2234
2555
  });
@@ -2239,6 +2560,7 @@ async function createSession(deps, opts) {
2239
2560
  void discoverProjects()
2240
2561
  .then((projects) => json(res, 200, { projects }))
2241
2562
  .catch((err) => {
2563
+ captureSidecarError(err, "app-sidecar.projects.discover");
2242
2564
  log("ERROR", "app-sidecar", "discoverProjects failed", {
2243
2565
  message: err instanceof Error ? err.message : String(err),
2244
2566
  });
@@ -2253,37 +2575,14 @@ async function createSession(deps, opts) {
2253
2575
  return;
2254
2576
  }
2255
2577
  const requestedAgent = new URL(url, `http://${host}`).searchParams.get("chatAgent");
2256
- if (requestedAgent === "all") {
2257
- void Promise.all(CHAT_AGENT_IDS.map(async (agentId) => {
2258
- const agentSessions = await listRecentSessions(target, 5, chatAgentSessionsDir(paths.sessionsDir, agentId));
2259
- return agentSessions.map((item) => ({ ...item, chatAgent: agentId }));
2260
- }))
2261
- .then(async (groups) => {
2262
- const dated = await Promise.all(groups.flat().map(async (item) => ({
2263
- item,
2264
- mtime: await fs
2265
- .stat(item.path)
2266
- .then((stat) => stat.mtimeMs)
2267
- .catch(() => 0),
2268
- })));
2269
- const recent = dated
2270
- .sort((left, right) => right.mtime - left.mtime)
2271
- .slice(0, 5)
2272
- .map(({ item }) => item);
2273
- json(res, 200, { sessions: recent });
2274
- })
2275
- .catch(() => json(res, 200, { sessions: [] }));
2276
- return;
2277
- }
2278
- // The picker may be served by a sidecar currently running in Chat mode.
2279
- // An omitted chatAgent always means coding history; chat callers identify
2280
- // their namespace explicitly (one agent or "all").
2281
- const sessionsDir = requestedAgent
2282
- ? sessionsDirForChatAgent(paths.sessionsDir, requestedAgent)
2283
- : paths.sessionsDir;
2284
- void listRecentSessions(target, 5, sessionsDir)
2578
+ // An omitted chatAgent means coding history; chat callers identify one
2579
+ // agent or request the combined, recency-sorted "all" listing.
2580
+ void listSidecarSessions(target, requestedAgent, paths.sessionsDir)
2285
2581
  .then((sessions) => json(res, 200, { sessions }))
2286
- .catch(() => json(res, 200, { sessions: [] }));
2582
+ .catch((error) => {
2583
+ captureSidecarError(error, "app-sidecar.sessions.list");
2584
+ json(res, 200, { sessions: [] });
2585
+ });
2287
2586
  return;
2288
2587
  }
2289
2588
  if (method === "GET" && url.startsWith("/files")) {
@@ -2291,6 +2590,7 @@ async function createSession(deps, opts) {
2291
2590
  void searchProjectFiles(cwd, q)
2292
2591
  .then((files) => json(res, 200, { files }))
2293
2592
  .catch((err) => {
2593
+ captureSidecarError(err, "app-sidecar.files.search");
2294
2594
  log("ERROR", "app-sidecar", "searchProjectFiles failed", {
2295
2595
  message: err instanceof Error ? err.message : String(err),
2296
2596
  });
@@ -2298,6 +2598,30 @@ async function createSession(deps, opts) {
2298
2598
  });
2299
2599
  return;
2300
2600
  }
2601
+ // Markdown transcript export for the app's download button. Serialized
2602
+ // here rather than in the webview because the webview's transcript model
2603
+ // deliberately keeps tool activity in the LiveToolPanel — exporting from
2604
+ // there would hand the user a coding session with the coding missing.
2605
+ // `?name=1` asks for the suggested filename only (the save dialog needs it
2606
+ // before there is a path), so the markdown never crosses IPC twice.
2607
+ if (method === "GET" && (url === "/export" || url.startsWith("/export?"))) {
2608
+ const query = new URLSearchParams(url.slice(url.indexOf("?") + 1));
2609
+ const st = session.getState();
2610
+ const filename = defaultExportFilename(mode);
2611
+ if (query.get("name") === "1") {
2612
+ json(res, 200, { filename });
2613
+ return;
2614
+ }
2615
+ const markdown = sessionToMarkdown({
2616
+ mode,
2617
+ cwd,
2618
+ provider: st.provider,
2619
+ model: st.model,
2620
+ ...(st.sessionId ? { sessionId: st.sessionId } : {}),
2621
+ }, session.getMessages(), { toolDetail: parseToolDetail(query.get("tools")) });
2622
+ json(res, 200, { filename, markdown });
2623
+ return;
2624
+ }
2301
2625
  if (method === "GET" && url === "/history") {
2302
2626
  // Reconstruct the transcript from persisted messages so resume is 1:1 with
2303
2627
  // the live SSE stream. Walks ALL message types (not just user/assistant):
@@ -2617,21 +2941,41 @@ async function createSession(deps, opts) {
2617
2941
  description: c.description,
2618
2942
  source: "built-in",
2619
2943
  }));
2944
+ // Desktop-safe registry actions belong in the same picker as workflows.
2945
+ // Most registry commands have dedicated app controls or TUI-only flows;
2946
+ // multi-root management has no other affordance, so expose only these.
2947
+ const workspaceActions = [
2948
+ {
2949
+ name: "add-dir",
2950
+ aliases: ["adddir"],
2951
+ description: "Add another project folder to this workspace",
2952
+ source: "built-in",
2953
+ },
2954
+ {
2955
+ name: "remove-dir",
2956
+ aliases: ["removedir"],
2957
+ description: "Remove an added project folder from this workspace",
2958
+ source: "built-in",
2959
+ },
2960
+ ];
2620
2961
  const custom = (await loadCustomCommands(cwd))
2621
- // A custom command can't shadow a built-in name.
2622
- .filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name))
2962
+ // A custom command can't shadow a built-in name or app action.
2963
+ .filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name) &&
2964
+ !workspaceActions.some((action) => action.name === c.name))
2623
2965
  .map((c) => ({
2624
2966
  name: c.name,
2625
2967
  aliases: [],
2626
2968
  description: c.description,
2627
2969
  source: "custom",
2628
2970
  }));
2629
- json(res, 200, { commands: [...builtins, ...custom] });
2971
+ json(res, 200, { commands: [...workspaceActions, ...builtins, ...custom] });
2630
2972
  })();
2631
2973
  return;
2632
2974
  }
2633
2975
  if (method === "POST" && url === "/prompt") {
2634
- void readBody(req).then(async (raw) => {
2976
+ void readBody(req, res).then(async (raw) => {
2977
+ if (raw === null)
2978
+ return;
2635
2979
  let text;
2636
2980
  let attachments;
2637
2981
  let meta;
@@ -2758,7 +3102,9 @@ async function createSession(deps, opts) {
2758
3102
  json(res, 404, { error: "Nolan is not available in EZ Chat." });
2759
3103
  return;
2760
3104
  }
2761
- void readBody(req).then(async (raw) => {
3105
+ void readBody(req, res).then(async (raw) => {
3106
+ if (raw === null)
3107
+ return;
2762
3108
  let text;
2763
3109
  try {
2764
3110
  text = JSON.parse(raw).text ?? "";
@@ -2817,7 +3163,9 @@ async function createSession(deps, opts) {
2817
3163
  json(res, 404, { error: "Autopilot is not available in EZ Chat." });
2818
3164
  return;
2819
3165
  }
2820
- void readBody(req).then(async (raw) => {
3166
+ void readBody(req, res).then(async (raw) => {
3167
+ if (raw === null)
3168
+ return;
2821
3169
  let enabled;
2822
3170
  try {
2823
3171
  enabled = Boolean(JSON.parse(raw).enabled);
@@ -2838,7 +3186,9 @@ async function createSession(deps, opts) {
2838
3186
  return;
2839
3187
  }
2840
3188
  if (method === "POST" && url === "/enhance") {
2841
- void readBody(req).then(async (raw) => {
3189
+ void readBody(req, res).then(async (raw) => {
3190
+ if (raw === null)
3191
+ return;
2842
3192
  let text;
2843
3193
  try {
2844
3194
  text = JSON.parse(raw).text ?? "";
@@ -2859,6 +3209,7 @@ async function createSession(deps, opts) {
2859
3209
  }
2860
3210
  catch (err) {
2861
3211
  const message = err instanceof Error ? err.message : String(err);
3212
+ captureSidecarError(err, "app-sidecar.prompt-enhancer");
2862
3213
  log("ERROR", "app-sidecar", "enhance failed", { message });
2863
3214
  json(res, 500, { error: message });
2864
3215
  }
@@ -2886,7 +3237,9 @@ async function createSession(deps, opts) {
2886
3237
  return;
2887
3238
  }
2888
3239
  if (method === "POST" && url === "/radio/volume") {
2889
- void readBody(req).then((raw) => {
3240
+ void readBody(req, res).then((raw) => {
3241
+ if (raw === null)
3242
+ return;
2890
3243
  let volume;
2891
3244
  try {
2892
3245
  volume = Number(JSON.parse(raw).volume);
@@ -2909,7 +3262,9 @@ async function createSession(deps, opts) {
2909
3262
  return;
2910
3263
  }
2911
3264
  if (method === "POST" && url === "/radio") {
2912
- void readBody(req).then((raw) => {
3265
+ void readBody(req, res).then((raw) => {
3266
+ if (raw === null)
3267
+ return;
2913
3268
  let station;
2914
3269
  try {
2915
3270
  station = JSON.parse(raw).station ?? "";
@@ -2933,7 +3288,9 @@ async function createSession(deps, opts) {
2933
3288
  return;
2934
3289
  }
2935
3290
  if (method === "POST" && url === "/tasks/run") {
2936
- void readBody(req).then((raw) => {
3291
+ void readBody(req, res).then((raw) => {
3292
+ if (raw === null)
3293
+ return;
2937
3294
  let id;
2938
3295
  let all;
2939
3296
  try {
@@ -2955,7 +3312,9 @@ async function createSession(deps, opts) {
2955
3312
  return;
2956
3313
  }
2957
3314
  if (method === "POST" && url === "/tasks/add") {
2958
- void readBody(req).then((raw) => {
3315
+ void readBody(req, res).then((raw) => {
3316
+ if (raw === null)
3317
+ return;
2959
3318
  let title;
2960
3319
  let prompt;
2961
3320
  try {
@@ -2987,7 +3346,9 @@ async function createSession(deps, opts) {
2987
3346
  return;
2988
3347
  }
2989
3348
  if (method === "POST" && url === "/tasks/update") {
2990
- void readBody(req).then((raw) => {
3349
+ void readBody(req, res).then((raw) => {
3350
+ if (raw === null)
3351
+ return;
2991
3352
  let id;
2992
3353
  let status;
2993
3354
  let title;
@@ -3032,7 +3393,9 @@ async function createSession(deps, opts) {
3032
3393
  return;
3033
3394
  }
3034
3395
  if (method === "POST" && url === "/tasks/delete") {
3035
- void readBody(req).then((raw) => {
3396
+ void readBody(req, res).then((raw) => {
3397
+ if (raw === null)
3398
+ return;
3036
3399
  let id;
3037
3400
  try {
3038
3401
  id = JSON.parse(raw).id ?? "";
@@ -3064,18 +3427,38 @@ async function createSession(deps, opts) {
3064
3427
  loggedIn.push(p);
3065
3428
  }
3066
3429
  // Just the names, grouped by provider in registry order — the UI shows
3067
- // a clean multi-column list of model ids.
3068
- const models = MODELS.filter((m) => loggedIn.includes(m.provider)).map((m) => ({
3069
- id: m.id,
3070
- name: m.name,
3071
- provider: m.provider,
3072
- }));
3430
+ // a clean multi-column list of model ids. Local models come from the
3431
+ // runtime registry (populated by the background scan) and are always
3432
+ // listed: their "login" is the endpoint answering a probe.
3433
+ const models = getAllModels()
3434
+ .filter((m) => m.provider === "local" || loggedIn.includes(m.provider))
3435
+ .map((m) => {
3436
+ if (m.provider !== "local") {
3437
+ return { id: m.id, name: m.name, provider: m.provider };
3438
+ }
3439
+ const probed = findProbedModel(localProbes, m.id);
3440
+ return {
3441
+ id: m.id,
3442
+ name: m.name,
3443
+ provider: m.provider,
3444
+ local: true,
3445
+ endpoint: probed?.endpoint.label ?? parseLocalModelId(m.id)?.endpointId,
3446
+ // A local model that can't call tools can't run the agent — the UI
3447
+ // renders it disabled rather than hiding it, so the user learns why.
3448
+ supportsTools: probed?.model.supportsTools ?? true,
3449
+ contextWindow: m.contextWindow,
3450
+ contextWindowKnown: probed?.model.contextWindowKnown ?? false,
3451
+ supportsThinking: m.supportsThinking,
3452
+ };
3453
+ });
3073
3454
  json(res, 200, { models });
3074
3455
  })();
3075
3456
  return;
3076
3457
  }
3077
3458
  if (method === "POST" && url === "/model") {
3078
- void readBody(req).then(async (raw) => {
3459
+ void readBody(req, res).then(async (raw) => {
3460
+ if (raw === null)
3461
+ return;
3079
3462
  let modelId;
3080
3463
  try {
3081
3464
  modelId = JSON.parse(raw).model ?? "";
@@ -3096,6 +3479,13 @@ async function createSession(deps, opts) {
3096
3479
  // Pick up any natively-written keys (desktop app) before resolving the
3097
3480
  // new model's credentials, so switching to a just-added provider works.
3098
3481
  await auth.reload();
3482
+ if (target.provider === "local") {
3483
+ const problem = await localModelBlocker(target.id);
3484
+ if (problem) {
3485
+ json(res, 409, { error: problem });
3486
+ return;
3487
+ }
3488
+ }
3099
3489
  await session.switchModel(target.provider, target.id);
3100
3490
  // Nolan follows EZ Coder's model only while un-pinned; a user-set Nolan
3101
3491
  // override survives EZ Coder model switches untouched.
@@ -3147,7 +3537,9 @@ async function createSession(deps, opts) {
3147
3537
  // to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
3148
3538
  // either is mid-run defers via the pending-model mechanics.
3149
3539
  if (method === "POST" && url === "/ken/model") {
3150
- void readBody(req).then(async (raw) => {
3540
+ void readBody(req, res).then(async (raw) => {
3541
+ if (raw === null)
3542
+ return;
3151
3543
  let modelId;
3152
3544
  try {
3153
3545
  const parsed = JSON.parse(raw).model;
@@ -3191,7 +3583,9 @@ async function createSession(deps, opts) {
3191
3583
  return;
3192
3584
  }
3193
3585
  if (method === "POST" && url === "/kill") {
3194
- void readBody(req).then(async (raw) => {
3586
+ void readBody(req, res).then(async (raw) => {
3587
+ if (raw === null)
3588
+ return;
3195
3589
  let id;
3196
3590
  try {
3197
3591
  id = JSON.parse(raw).id ?? "";
@@ -3269,6 +3663,7 @@ async function createSession(deps, opts) {
3269
3663
  drained,
3270
3664
  });
3271
3665
  })().catch((error) => {
3666
+ captureSidecarError(error, "app-sidecar.run.cancel");
3272
3667
  broadcast("cancel_failed", { error: "cancel_failed", runState: runLifecycle.state });
3273
3668
  json(res, 500, {
3274
3669
  error: "cancel_failed",
@@ -3296,6 +3691,7 @@ async function createSession(deps, opts) {
3296
3691
  json(res, 200, { ok: true });
3297
3692
  })
3298
3693
  .catch((err) => {
3694
+ captureSidecarError(err, "app-sidecar.session.new");
3299
3695
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
3300
3696
  });
3301
3697
  return;
@@ -3310,7 +3706,9 @@ async function createSession(deps, opts) {
3310
3706
  // 3. session_reset tells the webview to clear its transcript; it then runs
3311
3707
  // the "implement it now" prompt in the clean session.
3312
3708
  if (method === "POST" && url === "/plan/accept") {
3313
- void readBody(req).then(async (raw) => {
3709
+ void readBody(req, res).then(async (raw) => {
3710
+ if (raw === null)
3711
+ return;
3314
3712
  let planPath;
3315
3713
  try {
3316
3714
  planPath = JSON.parse(raw).planPath || undefined;
@@ -3350,6 +3748,7 @@ async function createSession(deps, opts) {
3350
3748
  json(res, 200, { ok: true, planTotal });
3351
3749
  }
3352
3750
  catch (err) {
3751
+ captureSidecarError(err, "app-sidecar.plan.accept");
3353
3752
  json(res, 500, { error: err instanceof Error ? err.message : String(err) });
3354
3753
  }
3355
3754
  });
@@ -3361,7 +3760,9 @@ async function createSession(deps, opts) {
3361
3760
  return;
3362
3761
  }
3363
3762
  if (method === "POST" && url === "/auth/apikey") {
3364
- void readBody(req).then(async (raw) => {
3763
+ void readBody(req, res).then(async (raw) => {
3764
+ if (raw === null)
3765
+ return;
3365
3766
  let provider = "";
3366
3767
  let key;
3367
3768
  let variant;
@@ -3404,7 +3805,9 @@ async function createSession(deps, opts) {
3404
3805
  return;
3405
3806
  }
3406
3807
  if (method === "POST" && url === "/auth/oauth/start") {
3407
- void readBody(req).then((raw) => {
3808
+ void readBody(req, res).then((raw) => {
3809
+ if (raw === null)
3810
+ return;
3408
3811
  let provider = "";
3409
3812
  try {
3410
3813
  provider = JSON.parse(raw).provider ?? "";
@@ -3446,6 +3849,7 @@ async function createSession(deps, opts) {
3446
3849
  broadcast("auth_done", { provider });
3447
3850
  }
3448
3851
  catch (err) {
3852
+ captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
3449
3853
  broadcast("auth_error", {
3450
3854
  provider,
3451
3855
  message: err instanceof Error ? err.message : String(err),
@@ -3460,7 +3864,9 @@ async function createSession(deps, opts) {
3460
3864
  return;
3461
3865
  }
3462
3866
  if (method === "POST" && url === "/auth/oauth/code") {
3463
- void readBody(req).then((raw) => {
3867
+ void readBody(req, res).then((raw) => {
3868
+ if (raw === null)
3869
+ return;
3464
3870
  let code;
3465
3871
  try {
3466
3872
  code = JSON.parse(raw).code ?? "";
@@ -3480,7 +3886,9 @@ async function createSession(deps, opts) {
3480
3886
  return;
3481
3887
  }
3482
3888
  if (method === "POST" && url === "/auth/logout") {
3483
- void readBody(req).then(async (raw) => {
3889
+ void readBody(req, res).then(async (raw) => {
3890
+ if (raw === null)
3891
+ return;
3484
3892
  let provider;
3485
3893
  try {
3486
3894
  provider = JSON.parse(raw).provider ?? "";
@@ -3503,6 +3911,84 @@ async function createSession(deps, opts) {
3503
3911
  });
3504
3912
  return;
3505
3913
  }
3914
+ // ── Local models ──────────────────────────────────────
3915
+ // GET returns the last scan (cheap, no probing) so opening the modal is
3916
+ // instant; POST /local/scan is the explicit refresh.
3917
+ if (method === "GET" && url === "/local") {
3918
+ json(res, 200, localStatePayload());
3919
+ return;
3920
+ }
3921
+ if (method === "POST" && url === "/local/scan") {
3922
+ void scanLocalModels(true)
3923
+ .then(() => {
3924
+ broadcast("models_change", { local: localStatePayload() });
3925
+ json(res, 200, localStatePayload());
3926
+ })
3927
+ .catch((err) => {
3928
+ broadcastError("error", "local model scan failed", err);
3929
+ json(res, 500, { error: "Local model scan failed — see the sidecar log." });
3930
+ });
3931
+ return;
3932
+ }
3933
+ if (method === "POST" && url === "/local/endpoints") {
3934
+ void readBody(req, res).then(async (raw) => {
3935
+ if (raw === null)
3936
+ return;
3937
+ let body;
3938
+ try {
3939
+ body = JSON.parse(raw);
3940
+ }
3941
+ catch {
3942
+ json(res, 400, { error: "invalid JSON body" });
3943
+ return;
3944
+ }
3945
+ try {
3946
+ const endpoint = await addCustomEndpoint({
3947
+ baseUrl: body.baseUrl ?? "",
3948
+ ...(body.label ? { label: body.label } : {}),
3949
+ ...(body.apiKey ? { apiKey: body.apiKey } : {}),
3950
+ });
3951
+ await scanLocalModels(true);
3952
+ broadcast("models_change", { local: localStatePayload() });
3953
+ json(res, 200, {
3954
+ endpoint: { id: endpoint.id, label: endpoint.label },
3955
+ ...localStatePayload(),
3956
+ });
3957
+ }
3958
+ catch (err) {
3959
+ // Validation errors are the user's typo, not a system fault — 400 with
3960
+ // the exact reason, and nothing in the transcript.
3961
+ if (err instanceof LocalEndpointError) {
3962
+ json(res, 400, { error: err.message });
3963
+ return;
3964
+ }
3965
+ broadcastError("error", "add local endpoint failed", err);
3966
+ json(res, 500, { error: "Could not save the endpoint — see the sidecar log." });
3967
+ }
3968
+ });
3969
+ return;
3970
+ }
3971
+ if (method === "DELETE" && url.startsWith("/local/endpoints/")) {
3972
+ const id = decodeURIComponent(url.slice("/local/endpoints/".length));
3973
+ void (async () => {
3974
+ try {
3975
+ await removeCustomEndpoint(id);
3976
+ clearRuntimeModels((m) => m.provider === "local" && parseLocalModelId(m.id)?.endpointId === id);
3977
+ await scanLocalModels(true);
3978
+ broadcast("models_change", { local: localStatePayload() });
3979
+ json(res, 200, localStatePayload());
3980
+ }
3981
+ catch (err) {
3982
+ if (err instanceof LocalEndpointError) {
3983
+ json(res, 400, { error: err.message });
3984
+ return;
3985
+ }
3986
+ broadcastError("error", "remove local endpoint failed", err);
3987
+ json(res, 500, { error: "Could not remove the endpoint — see the sidecar log." });
3988
+ }
3989
+ })();
3990
+ return;
3991
+ }
3506
3992
  // ── Telegram config (mirrors `ezcoder telegram`) ─────────
3507
3993
  if (method === "GET" && url === "/telegram") {
3508
3994
  void loadTelegramConfig().then((cfg) => {
@@ -3519,7 +4005,9 @@ async function createSession(deps, opts) {
3519
4005
  return;
3520
4006
  }
3521
4007
  if (method === "POST" && url === "/telegram") {
3522
- void readBody(req).then(async (raw) => {
4008
+ void readBody(req, res).then(async (raw) => {
4009
+ if (raw === null)
4010
+ return;
3523
4011
  let botTokenInput;
3524
4012
  let userIdInput;
3525
4013
  try {
@@ -3587,6 +4075,7 @@ async function createSession(deps, opts) {
3587
4075
  }
3588
4076
  catch (err) {
3589
4077
  serveController = null;
4078
+ captureSidecarError(err, "app-sidecar.serve.start", { provider: st.provider });
3590
4079
  json(res, 400, { error: err instanceof Error ? err.message : String(err) });
3591
4080
  }
3592
4081
  })();
@@ -3613,6 +4102,7 @@ async function createSession(deps, opts) {
3613
4102
  void buildMcpRows(targetCwd)
3614
4103
  .then((servers) => json(res, 200, { servers }))
3615
4104
  .catch((err) => {
4105
+ captureSidecarError(err, "app-sidecar.mcp.list");
3616
4106
  log("ERROR", "app-sidecar", "buildMcpRows failed", {
3617
4107
  message: err instanceof Error ? err.message : String(err),
3618
4108
  });
@@ -3621,7 +4111,9 @@ async function createSession(deps, opts) {
3621
4111
  return;
3622
4112
  }
3623
4113
  if (method === "POST" && url === "/mcp/add") {
3624
- void readBody(req).then(async (raw) => {
4114
+ void readBody(req, res).then(async (raw) => {
4115
+ if (raw === null)
4116
+ return;
3625
4117
  let line;
3626
4118
  let scopeValue;
3627
4119
  let bodyCwd;
@@ -3669,6 +4161,7 @@ async function createSession(deps, opts) {
3669
4161
  });
3670
4162
  }
3671
4163
  catch (err) {
4164
+ captureSidecarError(err, "app-sidecar.mcp.add", { server: config.name });
3672
4165
  json(res, 500, {
3673
4166
  error: err instanceof Error ? err.message : String(err),
3674
4167
  });
@@ -3677,7 +4170,9 @@ async function createSession(deps, opts) {
3677
4170
  return;
3678
4171
  }
3679
4172
  if (method === "POST" && url === "/mcp/remove") {
3680
- void readBody(req).then(async (raw) => {
4173
+ void readBody(req, res).then(async (raw) => {
4174
+ if (raw === null)
4175
+ return;
3681
4176
  let name;
3682
4177
  let scopeValue;
3683
4178
  let bodyCwd;
@@ -3714,7 +4209,9 @@ async function createSession(deps, opts) {
3714
4209
  // `mcp_auth_error`. Responds 202 immediately and runs the flow in the
3715
4210
  // background (the browser round-trip can take a while).
3716
4211
  if (method === "POST" && url === "/mcp/login") {
3717
- void readBody(req).then(async (raw) => {
4212
+ void readBody(req, res).then(async (raw) => {
4213
+ if (raw === null)
4214
+ return;
3718
4215
  let name;
3719
4216
  let scopeValue;
3720
4217
  let bodyCwd;
@@ -3758,6 +4255,7 @@ async function createSession(deps, opts) {
3758
4255
  }
3759
4256
  }
3760
4257
  catch (err) {
4258
+ captureSidecarError(err, "app-sidecar.mcp.login", { server: name });
3761
4259
  broadcast("mcp_auth_error", {
3762
4260
  name,
3763
4261
  message: err instanceof Error ? err.message : String(err),
@@ -3778,6 +4276,9 @@ async function createSession(deps, opts) {
3778
4276
  gitPollStopped = true;
3779
4277
  if (gitPoll)
3780
4278
  clearTimeout(gitPoll);
4279
+ gitHubPollStopped = true;
4280
+ if (gitHubPoll)
4281
+ clearTimeout(gitHubPoll);
3781
4282
  // Stop the Telegram serve loop + dispose its per-chat sessions.
3782
4283
  if (serveController)
3783
4284
  await serveController.stop().catch(() => { });
@@ -3804,7 +4305,9 @@ async function createSession(deps, opts) {
3804
4305
  dispose,
3805
4306
  };
3806
4307
  }
3807
- main().catch((err) => {
4308
+ main().catch(async (err) => {
4309
+ captureSidecarError(err, "app-sidecar.main", { severity: "fatal" });
4310
+ await flushSidecarErrors();
3808
4311
  const message = err instanceof Error ? err.message : String(err);
3809
4312
  process.stderr.write(`GG_APP_FATAL ${message}\n`);
3810
4313
  process.exit(1);