@prestyj/cli 5.9.0 → 5.11.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 (675) hide show
  1. package/README.md +4 -5
  2. package/assets/skills/evidence-led-ui/SKILL.md +1 -1
  3. package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
  4. package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
  5. package/dist/app-sidecar-auth-broadcast.test.js +282 -0
  6. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
  7. package/dist/app-sidecar-sessions.d.ts +5 -2
  8. package/dist/app-sidecar-sessions.d.ts.map +1 -1
  9. package/dist/app-sidecar-sessions.js +34 -8
  10. package/dist/app-sidecar-sessions.js.map +1 -1
  11. package/dist/app-sidecar-sessions.test.js +95 -0
  12. package/dist/app-sidecar-sessions.test.js.map +1 -1
  13. package/dist/app-sidecar.js +871 -142
  14. package/dist/app-sidecar.js.map +1 -1
  15. package/dist/chat-agents/general.test.js +5 -3
  16. package/dist/chat-agents/general.test.js.map +1 -1
  17. package/dist/chat-agents/memory.d.ts.map +1 -1
  18. package/dist/chat-agents/memory.js +14 -5
  19. package/dist/chat-agents/memory.js.map +1 -1
  20. package/dist/chat-agents/memory.test.js +11 -1
  21. package/dist/chat-agents/memory.test.js.map +1 -1
  22. package/dist/chat-agents/shared.js +1 -1
  23. package/dist/chat-agents/shared.js.map +1 -1
  24. package/dist/chat-agents/specialists.test.js +3 -2
  25. package/dist/chat-agents/specialists.test.js.map +1 -1
  26. package/dist/cli/command-routing.d.ts +1 -1
  27. package/dist/cli/command-routing.d.ts.map +1 -1
  28. package/dist/cli/command-routing.js +1 -0
  29. package/dist/cli/command-routing.js.map +1 -1
  30. package/dist/cli/command-routing.test.js +1 -0
  31. package/dist/cli/command-routing.test.js.map +1 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +183 -51
  34. package/dist/cli.js.map +1 -1
  35. package/dist/config.d.ts +16 -0
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +51 -2
  38. package/dist/config.js.map +1 -1
  39. package/dist/config.test.js +71 -2
  40. package/dist/config.test.js.map +1 -1
  41. package/dist/core/agent-mcp-passthrough.test.d.ts +2 -0
  42. package/dist/core/agent-mcp-passthrough.test.d.ts.map +1 -0
  43. package/dist/core/agent-mcp-passthrough.test.js +61 -0
  44. package/dist/core/agent-mcp-passthrough.test.js.map +1 -0
  45. package/dist/core/agent-notifications.d.ts +62 -0
  46. package/dist/core/agent-notifications.d.ts.map +1 -0
  47. package/dist/core/agent-notifications.js +92 -0
  48. package/dist/core/agent-notifications.js.map +1 -0
  49. package/dist/core/agent-notifications.test.d.ts +2 -0
  50. package/dist/core/agent-notifications.test.d.ts.map +1 -0
  51. package/dist/core/agent-notifications.test.js +97 -0
  52. package/dist/core/agent-notifications.test.js.map +1 -0
  53. package/dist/core/agent-session-add-dir.test.d.ts +2 -0
  54. package/dist/core/agent-session-add-dir.test.d.ts.map +1 -0
  55. package/dist/core/agent-session-add-dir.test.js +110 -0
  56. package/dist/core/agent-session-add-dir.test.js.map +1 -0
  57. package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
  58. package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
  59. package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
  60. package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
  61. package/dist/core/agent-session-compaction.test.js +327 -13
  62. package/dist/core/agent-session-compaction.test.js.map +1 -1
  63. package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
  64. package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
  65. package/dist/core/agent-session-crash-durability.test.js +102 -0
  66. package/dist/core/agent-session-crash-durability.test.js.map +1 -0
  67. package/dist/core/agent-session-marker-anchors.test.d.ts +2 -0
  68. package/dist/core/agent-session-marker-anchors.test.d.ts.map +1 -0
  69. package/dist/core/agent-session-marker-anchors.test.js +326 -0
  70. package/dist/core/agent-session-marker-anchors.test.js.map +1 -0
  71. package/dist/core/agent-session-memory-tail.test.js +4 -7
  72. package/dist/core/agent-session-memory-tail.test.js.map +1 -1
  73. package/dist/core/agent-session-model-switch.test.d.ts +2 -0
  74. package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  75. package/dist/core/agent-session-model-switch.test.js +161 -0
  76. package/dist/core/agent-session-model-switch.test.js.map +1 -0
  77. package/dist/core/agent-session-notifications.test.d.ts +2 -0
  78. package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
  79. package/dist/core/agent-session-notifications.test.js +114 -0
  80. package/dist/core/agent-session-notifications.test.js.map +1 -0
  81. package/dist/core/agent-session-process-gate.test.d.ts +2 -0
  82. package/dist/core/agent-session-process-gate.test.d.ts.map +1 -0
  83. package/dist/core/agent-session-process-gate.test.js +92 -0
  84. package/dist/core/agent-session-process-gate.test.js.map +1 -0
  85. package/dist/core/agent-session-queue.test.js +69 -7
  86. package/dist/core/agent-session-queue.test.js.map +1 -1
  87. package/dist/core/agent-session-review-coverage.test.js +80 -23
  88. package/dist/core/agent-session-review-coverage.test.js.map +1 -1
  89. package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
  90. package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
  91. package/dist/core/agent-session-slash-restore.test.js +121 -0
  92. package/dist/core/agent-session-slash-restore.test.js.map +1 -0
  93. package/dist/core/agent-session-subagent-policy.test.js +7 -3
  94. package/dist/core/agent-session-subagent-policy.test.js.map +1 -1
  95. package/dist/core/agent-session.d.ts +256 -10
  96. package/dist/core/agent-session.d.ts.map +1 -1
  97. package/dist/core/agent-session.js +1071 -219
  98. package/dist/core/agent-session.js.map +1 -1
  99. package/dist/core/agent-tools-allowlist.test.js +5 -3
  100. package/dist/core/agent-tools-allowlist.test.js.map +1 -1
  101. package/dist/core/agents.d.ts +11 -0
  102. package/dist/core/agents.d.ts.map +1 -1
  103. package/dist/core/agents.js +21 -0
  104. package/dist/core/agents.js.map +1 -1
  105. package/dist/core/agents.test.d.ts +2 -0
  106. package/dist/core/agents.test.d.ts.map +1 -0
  107. package/dist/core/agents.test.js +67 -0
  108. package/dist/core/agents.test.js.map +1 -0
  109. package/dist/core/auth-providers.js +1 -1
  110. package/dist/core/auth-providers.js.map +1 -1
  111. package/dist/core/compaction/active-context.test.js +21 -0
  112. package/dist/core/compaction/active-context.test.js.map +1 -1
  113. package/dist/core/compaction/compactor.d.ts +108 -8
  114. package/dist/core/compaction/compactor.d.ts.map +1 -1
  115. package/dist/core/compaction/compactor.js +423 -119
  116. package/dist/core/compaction/compactor.js.map +1 -1
  117. package/dist/core/compaction/compactor.test.js +361 -9
  118. package/dist/core/compaction/compactor.test.js.map +1 -1
  119. package/dist/core/compaction/policy.d.ts +17 -0
  120. package/dist/core/compaction/policy.d.ts.map +1 -0
  121. package/dist/core/compaction/policy.js +22 -0
  122. package/dist/core/compaction/policy.js.map +1 -0
  123. package/dist/core/compaction/policy.test.d.ts +2 -0
  124. package/dist/core/compaction/policy.test.d.ts.map +1 -0
  125. package/dist/core/compaction/policy.test.js +35 -0
  126. package/dist/core/compaction/policy.test.js.map +1 -0
  127. package/dist/core/compaction/query-aware-selector.d.ts +33 -0
  128. package/dist/core/compaction/query-aware-selector.d.ts.map +1 -0
  129. package/dist/core/compaction/query-aware-selector.js +202 -0
  130. package/dist/core/compaction/query-aware-selector.js.map +1 -0
  131. package/dist/core/compaction/query-aware-selector.test.d.ts +2 -0
  132. package/dist/core/compaction/query-aware-selector.test.d.ts.map +1 -0
  133. package/dist/core/compaction/query-aware-selector.test.js +96 -0
  134. package/dist/core/compaction/query-aware-selector.test.js.map +1 -0
  135. package/dist/core/compaction/token-estimator.test.js +1 -1
  136. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  137. package/dist/core/compaction/tool-result-pruner.d.ts +24 -11
  138. package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
  139. package/dist/core/compaction/tool-result-pruner.js +98 -26
  140. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  141. package/dist/core/compaction/tool-result-pruner.test.js +178 -6
  142. package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
  143. package/dist/core/encode-cwd.d.ts +17 -0
  144. package/dist/core/encode-cwd.d.ts.map +1 -1
  145. package/dist/core/encode-cwd.js +20 -7
  146. package/dist/core/encode-cwd.js.map +1 -1
  147. package/dist/core/encode-cwd.test.js +20 -1
  148. package/dist/core/encode-cwd.test.js.map +1 -1
  149. package/dist/core/event-bus.d.ts +23 -0
  150. package/dist/core/event-bus.d.ts.map +1 -1
  151. package/dist/core/event-bus.js +10 -0
  152. package/dist/core/event-bus.js.map +1 -1
  153. package/dist/core/extensions/loader.d.ts.map +1 -1
  154. package/dist/core/extensions/loader.js +28 -8
  155. package/dist/core/extensions/loader.js.map +1 -1
  156. package/dist/core/extensions/plugin-bundles.d.ts +23 -0
  157. package/dist/core/extensions/plugin-bundles.d.ts.map +1 -0
  158. package/dist/core/extensions/plugin-bundles.js +228 -0
  159. package/dist/core/extensions/plugin-bundles.js.map +1 -0
  160. package/dist/core/extensions/plugin-bundles.test.d.ts +2 -0
  161. package/dist/core/extensions/plugin-bundles.test.d.ts.map +1 -0
  162. package/dist/core/extensions/plugin-bundles.test.js +110 -0
  163. package/dist/core/extensions/plugin-bundles.test.js.map +1 -0
  164. package/dist/core/foreign-session-import.d.ts +120 -0
  165. package/dist/core/foreign-session-import.d.ts.map +1 -0
  166. package/dist/core/foreign-session-import.js +598 -0
  167. package/dist/core/foreign-session-import.js.map +1 -0
  168. package/dist/core/foreign-session-import.test.d.ts +2 -0
  169. package/dist/core/foreign-session-import.test.d.ts.map +1 -0
  170. package/dist/core/foreign-session-import.test.js +220 -0
  171. package/dist/core/foreign-session-import.test.js.map +1 -0
  172. package/dist/core/foreign-transcript-blocks.d.ts +51 -0
  173. package/dist/core/foreign-transcript-blocks.d.ts.map +1 -0
  174. package/dist/core/foreign-transcript-blocks.js +151 -0
  175. package/dist/core/foreign-transcript-blocks.js.map +1 -0
  176. package/dist/core/ideal-review.d.ts +20 -1
  177. package/dist/core/ideal-review.d.ts.map +1 -1
  178. package/dist/core/ideal-review.js +43 -3
  179. package/dist/core/ideal-review.js.map +1 -1
  180. package/dist/core/ideal-review.test.js +106 -5
  181. package/dist/core/ideal-review.test.js.map +1 -1
  182. package/dist/core/local-endpoint-store.d.ts +50 -0
  183. package/dist/core/local-endpoint-store.d.ts.map +1 -0
  184. package/dist/core/local-endpoint-store.js +201 -0
  185. package/dist/core/local-endpoint-store.js.map +1 -0
  186. package/dist/core/local-endpoint-store.test.d.ts +2 -0
  187. package/dist/core/local-endpoint-store.test.d.ts.map +1 -0
  188. package/dist/core/local-endpoint-store.test.js +131 -0
  189. package/dist/core/local-endpoint-store.test.js.map +1 -0
  190. package/dist/core/loop-breaker.d.ts.map +1 -1
  191. package/dist/core/loop-breaker.js +1 -0
  192. package/dist/core/loop-breaker.js.map +1 -1
  193. package/dist/core/lsp/client.d.ts +65 -0
  194. package/dist/core/lsp/client.d.ts.map +1 -1
  195. package/dist/core/lsp/client.js +197 -14
  196. package/dist/core/lsp/client.js.map +1 -1
  197. package/dist/core/lsp/jsonrpc.d.ts +38 -1
  198. package/dist/core/lsp/jsonrpc.d.ts.map +1 -1
  199. package/dist/core/lsp/jsonrpc.js +13 -1
  200. package/dist/core/lsp/jsonrpc.js.map +1 -1
  201. package/dist/core/lsp/manager.d.ts +47 -9
  202. package/dist/core/lsp/manager.d.ts.map +1 -1
  203. package/dist/core/lsp/manager.js +107 -59
  204. package/dist/core/lsp/manager.js.map +1 -1
  205. package/dist/core/lsp/manager.test.js +50 -1
  206. package/dist/core/lsp/manager.test.js.map +1 -1
  207. package/dist/core/lsp/pool.d.ts +113 -0
  208. package/dist/core/lsp/pool.d.ts.map +1 -0
  209. package/dist/core/lsp/pool.js +255 -0
  210. package/dist/core/lsp/pool.js.map +1 -0
  211. package/dist/core/lsp/pool.test.d.ts +2 -0
  212. package/dist/core/lsp/pool.test.d.ts.map +1 -0
  213. package/dist/core/lsp/pool.test.js +286 -0
  214. package/dist/core/lsp/pool.test.js.map +1 -0
  215. package/dist/core/lsp/servers.d.ts.map +1 -1
  216. package/dist/core/lsp/servers.js +43 -3
  217. package/dist/core/lsp/servers.js.map +1 -1
  218. package/dist/core/lsp/servers.test.d.ts +2 -0
  219. package/dist/core/lsp/servers.test.d.ts.map +1 -0
  220. package/dist/core/lsp/servers.test.js +67 -0
  221. package/dist/core/lsp/servers.test.js.map +1 -0
  222. package/dist/core/lsp/test-support.d.ts +18 -0
  223. package/dist/core/lsp/test-support.d.ts.map +1 -0
  224. package/dist/core/lsp/test-support.js +27 -0
  225. package/dist/core/lsp/test-support.js.map +1 -0
  226. package/dist/core/lsp/uri.test.d.ts +2 -0
  227. package/dist/core/lsp/uri.test.d.ts.map +1 -0
  228. package/dist/core/lsp/uri.test.js +56 -0
  229. package/dist/core/lsp/uri.test.js.map +1 -0
  230. package/dist/core/lsp/windows.test.d.ts +2 -0
  231. package/dist/core/lsp/windows.test.d.ts.map +1 -0
  232. package/dist/core/lsp/windows.test.js +239 -0
  233. package/dist/core/lsp/windows.test.js.map +1 -0
  234. package/dist/core/mcp/catalog-cache.d.ts +63 -0
  235. package/dist/core/mcp/catalog-cache.d.ts.map +1 -0
  236. package/dist/core/mcp/catalog-cache.js +163 -0
  237. package/dist/core/mcp/catalog-cache.js.map +1 -0
  238. package/dist/core/mcp/catalog-cache.test.d.ts +2 -0
  239. package/dist/core/mcp/catalog-cache.test.d.ts.map +1 -0
  240. package/dist/core/mcp/catalog-cache.test.js +99 -0
  241. package/dist/core/mcp/catalog-cache.test.js.map +1 -0
  242. package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
  243. package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
  244. package/dist/core/mcp/client-session-recovery.test.js +231 -0
  245. package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
  246. package/dist/core/mcp/client.d.ts +158 -0
  247. package/dist/core/mcp/client.d.ts.map +1 -1
  248. package/dist/core/mcp/client.js +476 -35
  249. package/dist/core/mcp/client.js.map +1 -1
  250. package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
  251. package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
  252. package/dist/core/mcp/elicitation-bridge.js +52 -0
  253. package/dist/core/mcp/elicitation-bridge.js.map +1 -0
  254. package/dist/core/mcp/elicitation.test.d.ts +2 -0
  255. package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
  256. package/dist/core/mcp/elicitation.test.js +158 -0
  257. package/dist/core/mcp/elicitation.test.js.map +1 -0
  258. package/dist/core/mcp/index.d.ts +5 -1
  259. package/dist/core/mcp/index.d.ts.map +1 -1
  260. package/dist/core/mcp/index.js +2 -0
  261. package/dist/core/mcp/index.js.map +1 -1
  262. package/dist/core/mcp/legacy-negotiation.test.d.ts +2 -0
  263. package/dist/core/mcp/legacy-negotiation.test.d.ts.map +1 -0
  264. package/dist/core/mcp/legacy-negotiation.test.js +80 -0
  265. package/dist/core/mcp/legacy-negotiation.test.js.map +1 -0
  266. package/dist/core/mcp/oauth-provider.d.ts +1 -2
  267. package/dist/core/mcp/oauth-provider.d.ts.map +1 -1
  268. package/dist/core/mcp/oauth-provider.js +6 -2
  269. package/dist/core/mcp/oauth-provider.js.map +1 -1
  270. package/dist/core/mcp/oauth-store.d.ts +1 -1
  271. package/dist/core/mcp/oauth-store.d.ts.map +1 -1
  272. package/dist/core/mcp/oauth-store.js.map +1 -1
  273. package/dist/core/mcp/resolve-stdio.d.ts +35 -0
  274. package/dist/core/mcp/resolve-stdio.d.ts.map +1 -1
  275. package/dist/core/mcp/resolve-stdio.js +80 -1
  276. package/dist/core/mcp/resolve-stdio.js.map +1 -1
  277. package/dist/core/mcp/resolve-stdio.test.js +99 -4
  278. package/dist/core/mcp/resolve-stdio.test.js.map +1 -1
  279. package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
  280. package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
  281. package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
  282. package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
  283. package/dist/core/mcp/shared-pool.d.ts +158 -0
  284. package/dist/core/mcp/shared-pool.d.ts.map +1 -0
  285. package/dist/core/mcp/shared-pool.js +235 -0
  286. package/dist/core/mcp/shared-pool.js.map +1 -0
  287. package/dist/core/mcp/shared-pool.test.d.ts +2 -0
  288. package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
  289. package/dist/core/mcp/shared-pool.test.js +425 -0
  290. package/dist/core/mcp/shared-pool.test.js.map +1 -0
  291. package/dist/core/mcp/types.d.ts +12 -0
  292. package/dist/core/mcp/types.d.ts.map +1 -1
  293. package/dist/core/mcp/when-connected.test.d.ts +2 -0
  294. package/dist/core/mcp/when-connected.test.d.ts.map +1 -0
  295. package/dist/core/mcp/when-connected.test.js +46 -0
  296. package/dist/core/mcp/when-connected.test.js.map +1 -0
  297. package/dist/core/network-guard.d.ts +47 -0
  298. package/dist/core/network-guard.d.ts.map +1 -0
  299. package/dist/core/network-guard.js +208 -0
  300. package/dist/core/network-guard.js.map +1 -0
  301. package/dist/core/network-guard.test.d.ts +2 -0
  302. package/dist/core/network-guard.test.d.ts.map +1 -0
  303. package/dist/core/network-guard.test.js +72 -0
  304. package/dist/core/network-guard.test.js.map +1 -0
  305. package/dist/core/nolan-context.d.ts.map +1 -1
  306. package/dist/core/nolan-context.js +8 -0
  307. package/dist/core/nolan-context.js.map +1 -1
  308. package/dist/core/nolan-context.test.js +40 -0
  309. package/dist/core/nolan-context.test.js.map +1 -1
  310. package/dist/core/nolan-prompt.js +3 -0
  311. package/dist/core/nolan-prompt.js.map +1 -1
  312. package/dist/core/nolan-prompt.test.js +5 -0
  313. package/dist/core/nolan-prompt.test.js.map +1 -1
  314. package/dist/core/persistent-shell.d.ts +4 -1
  315. package/dist/core/persistent-shell.d.ts.map +1 -1
  316. package/dist/core/persistent-shell.js +21 -4
  317. package/dist/core/persistent-shell.js.map +1 -1
  318. package/dist/core/process-gate.d.ts +45 -0
  319. package/dist/core/process-gate.d.ts.map +1 -0
  320. package/dist/core/process-gate.js +79 -0
  321. package/dist/core/process-gate.js.map +1 -0
  322. package/dist/core/process-gate.test.d.ts +2 -0
  323. package/dist/core/process-gate.test.d.ts.map +1 -0
  324. package/dist/core/process-gate.test.js +87 -0
  325. package/dist/core/process-gate.test.js.map +1 -0
  326. package/dist/core/process-manager-dev-server-repro.test.js +34 -12
  327. package/dist/core/process-manager-dev-server-repro.test.js.map +1 -1
  328. package/dist/core/process-manager-notifications.test.d.ts +2 -0
  329. package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
  330. package/dist/core/process-manager-notifications.test.js +217 -0
  331. package/dist/core/process-manager-notifications.test.js.map +1 -0
  332. package/dist/core/process-manager.d.ts +51 -2
  333. package/dist/core/process-manager.d.ts.map +1 -1
  334. package/dist/core/process-manager.js +212 -15
  335. package/dist/core/process-manager.js.map +1 -1
  336. package/dist/core/project-discovery.d.ts +38 -6
  337. package/dist/core/project-discovery.d.ts.map +1 -1
  338. package/dist/core/project-discovery.js +327 -40
  339. package/dist/core/project-discovery.js.map +1 -1
  340. package/dist/core/project-discovery.test.js +226 -1
  341. package/dist/core/project-discovery.test.js.map +1 -1
  342. package/dist/core/prompt-commands.d.ts.map +1 -1
  343. package/dist/core/prompt-commands.js +89 -156
  344. package/dist/core/prompt-commands.js.map +1 -1
  345. package/dist/core/prompt-commands.test.js +4 -10
  346. package/dist/core/prompt-commands.test.js.map +1 -1
  347. package/dist/core/radio.test.js +4 -1
  348. package/dist/core/radio.test.js.map +1 -1
  349. package/dist/core/regrounding.d.ts.map +1 -1
  350. package/dist/core/regrounding.js +1 -0
  351. package/dist/core/regrounding.js.map +1 -1
  352. package/dist/core/run-claim.d.ts +37 -0
  353. package/dist/core/run-claim.d.ts.map +1 -0
  354. package/dist/core/run-claim.js +46 -0
  355. package/dist/core/run-claim.js.map +1 -0
  356. package/dist/core/run-claim.test.d.ts +2 -0
  357. package/dist/core/run-claim.test.d.ts.map +1 -0
  358. package/dist/core/run-claim.test.js +107 -0
  359. package/dist/core/run-claim.test.js.map +1 -0
  360. package/dist/core/run-journal.test.d.ts +2 -0
  361. package/dist/core/run-journal.test.d.ts.map +1 -0
  362. package/dist/core/run-journal.test.js +195 -0
  363. package/dist/core/run-journal.test.js.map +1 -0
  364. package/dist/core/run-lifecycle.d.ts +22 -3
  365. package/dist/core/run-lifecycle.d.ts.map +1 -1
  366. package/dist/core/run-lifecycle.js +12 -3
  367. package/dist/core/run-lifecycle.js.map +1 -1
  368. package/dist/core/runtime-mode.d.ts +2 -0
  369. package/dist/core/runtime-mode.d.ts.map +1 -1
  370. package/dist/core/runtime-mode.js +4 -0
  371. package/dist/core/runtime-mode.js.map +1 -1
  372. package/dist/core/sandbox-domains.d.ts +14 -0
  373. package/dist/core/sandbox-domains.d.ts.map +1 -0
  374. package/dist/core/sandbox-domains.js +76 -0
  375. package/dist/core/sandbox-domains.js.map +1 -0
  376. package/dist/core/sandbox-feedback.d.ts +22 -0
  377. package/dist/core/sandbox-feedback.d.ts.map +1 -0
  378. package/dist/core/sandbox-feedback.js +69 -0
  379. package/dist/core/sandbox-feedback.js.map +1 -0
  380. package/dist/core/sandbox-feedback.test.d.ts +2 -0
  381. package/dist/core/sandbox-feedback.test.d.ts.map +1 -0
  382. package/dist/core/sandbox-feedback.test.js +32 -0
  383. package/dist/core/sandbox-feedback.test.js.map +1 -0
  384. package/dist/core/sandbox.d.ts +91 -0
  385. package/dist/core/sandbox.d.ts.map +1 -0
  386. package/dist/core/sandbox.js +254 -0
  387. package/dist/core/sandbox.js.map +1 -0
  388. package/dist/core/sandbox.test.d.ts +2 -0
  389. package/dist/core/sandbox.test.d.ts.map +1 -0
  390. package/dist/core/sandbox.test.js +157 -0
  391. package/dist/core/sandbox.test.js.map +1 -0
  392. package/dist/core/session-compaction.d.ts +6 -0
  393. package/dist/core/session-compaction.d.ts.map +1 -1
  394. package/dist/core/session-compaction.js +11 -1
  395. package/dist/core/session-compaction.js.map +1 -1
  396. package/dist/core/session-compaction.test.js +18 -1
  397. package/dist/core/session-compaction.test.js.map +1 -1
  398. package/dist/core/session-export.d.ts +98 -0
  399. package/dist/core/session-export.d.ts.map +1 -0
  400. package/dist/core/session-export.js +364 -0
  401. package/dist/core/session-export.js.map +1 -0
  402. package/dist/core/session-export.test.d.ts +2 -0
  403. package/dist/core/session-export.test.d.ts.map +1 -0
  404. package/dist/core/session-export.test.js +353 -0
  405. package/dist/core/session-export.test.js.map +1 -0
  406. package/dist/core/session-history.d.ts +95 -6
  407. package/dist/core/session-history.d.ts.map +1 -1
  408. package/dist/core/session-history.js +261 -17
  409. package/dist/core/session-history.js.map +1 -1
  410. package/dist/core/session-history.test.js +173 -2
  411. package/dist/core/session-history.test.js.map +1 -1
  412. package/dist/core/session-manager.d.ts +223 -6
  413. package/dist/core/session-manager.d.ts.map +1 -1
  414. package/dist/core/session-manager.js +515 -26
  415. package/dist/core/session-manager.js.map +1 -1
  416. package/dist/core/session-manager.test.js +147 -3
  417. package/dist/core/session-manager.test.js.map +1 -1
  418. package/dist/core/session-notification-visibility.test.d.ts +2 -0
  419. package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
  420. package/dist/core/session-notification-visibility.test.js +113 -0
  421. package/dist/core/session-notification-visibility.test.js.map +1 -0
  422. package/dist/core/session-preview.d.ts +5 -4
  423. package/dist/core/session-preview.d.ts.map +1 -1
  424. package/dist/core/session-preview.js +12 -5
  425. package/dist/core/session-preview.js.map +1 -1
  426. package/dist/core/session-restore-display.test.js +25 -0
  427. package/dist/core/session-restore-display.test.js.map +1 -1
  428. package/dist/core/session-storage.d.ts.map +1 -1
  429. package/dist/core/session-storage.js +64 -4
  430. package/dist/core/session-storage.js.map +1 -1
  431. package/dist/core/session-storage.test.js +10 -2
  432. package/dist/core/session-storage.test.js.map +1 -1
  433. package/dist/core/settings-manager.d.ts +13 -2
  434. package/dist/core/settings-manager.d.ts.map +1 -1
  435. package/dist/core/settings-manager.js +36 -0
  436. package/dist/core/settings-manager.js.map +1 -1
  437. package/dist/core/setup-history.d.ts +0 -6
  438. package/dist/core/setup-history.d.ts.map +1 -1
  439. package/dist/core/setup-history.js +2 -18
  440. package/dist/core/setup-history.js.map +1 -1
  441. package/dist/core/setup-history.test.js +11 -26
  442. package/dist/core/setup-history.test.js.map +1 -1
  443. package/dist/core/skills-routing.test.js +3 -1
  444. package/dist/core/skills-routing.test.js.map +1 -1
  445. package/dist/core/slash-commands.d.ts +18 -0
  446. package/dist/core/slash-commands.d.ts.map +1 -1
  447. package/dist/core/slash-commands.js +42 -10
  448. package/dist/core/slash-commands.js.map +1 -1
  449. package/dist/core/slash-commands.test.d.ts +2 -0
  450. package/dist/core/slash-commands.test.d.ts.map +1 -0
  451. package/dist/core/slash-commands.test.js +59 -0
  452. package/dist/core/slash-commands.test.js.map +1 -0
  453. package/dist/core/speed-benchmark.test.js +9 -3
  454. package/dist/core/speed-benchmark.test.js.map +1 -1
  455. package/dist/core/steering.d.ts +12 -0
  456. package/dist/core/steering.d.ts.map +1 -1
  457. package/dist/core/steering.js +16 -0
  458. package/dist/core/steering.js.map +1 -1
  459. package/dist/core/subagent-manager.d.ts +14 -1
  460. package/dist/core/subagent-manager.d.ts.map +1 -1
  461. package/dist/core/subagent-manager.js +48 -1
  462. package/dist/core/subagent-manager.js.map +1 -1
  463. package/dist/core/subagent-manager.test.js +43 -7
  464. package/dist/core/subagent-manager.test.js.map +1 -1
  465. package/dist/core/subagent-store.test.js +14 -2
  466. package/dist/core/subagent-store.test.js.map +1 -1
  467. package/dist/core/verification-evidence.d.ts +17 -0
  468. package/dist/core/verification-evidence.d.ts.map +1 -0
  469. package/dist/core/verification-evidence.js +283 -0
  470. package/dist/core/verification-evidence.js.map +1 -0
  471. package/dist/core/verification-evidence.test.d.ts +2 -0
  472. package/dist/core/verification-evidence.test.d.ts.map +1 -0
  473. package/dist/core/verification-evidence.test.js +103 -0
  474. package/dist/core/verification-evidence.test.js.map +1 -0
  475. package/dist/core/workspace-guard.d.ts +2 -0
  476. package/dist/core/workspace-guard.d.ts.map +1 -1
  477. package/dist/core/workspace-guard.js +4 -1
  478. package/dist/core/workspace-guard.js.map +1 -1
  479. package/dist/core/workspace-guard.test.js +21 -0
  480. package/dist/core/workspace-guard.test.js.map +1 -1
  481. package/dist/interactive.d.ts.map +1 -1
  482. package/dist/interactive.js +17 -3
  483. package/dist/interactive.js.map +1 -1
  484. package/dist/modes/acp-mode.d.ts +122 -0
  485. package/dist/modes/acp-mode.d.ts.map +1 -0
  486. package/dist/modes/acp-mode.js +1366 -0
  487. package/dist/modes/acp-mode.js.map +1 -0
  488. package/dist/modes/acp-mode.test.d.ts +2 -0
  489. package/dist/modes/acp-mode.test.d.ts.map +1 -0
  490. package/dist/modes/acp-mode.test.js +1065 -0
  491. package/dist/modes/acp-mode.test.js.map +1 -0
  492. package/dist/modes/index.d.ts +1 -0
  493. package/dist/modes/index.d.ts.map +1 -1
  494. package/dist/modes/index.js +1 -0
  495. package/dist/modes/index.js.map +1 -1
  496. package/dist/modes/json-mode.d.ts +35 -0
  497. package/dist/modes/json-mode.d.ts.map +1 -1
  498. package/dist/modes/json-mode.js +38 -0
  499. package/dist/modes/json-mode.js.map +1 -1
  500. package/dist/modes/json-mode.test.d.ts +2 -0
  501. package/dist/modes/json-mode.test.d.ts.map +1 -0
  502. package/dist/modes/json-mode.test.js +84 -0
  503. package/dist/modes/json-mode.test.js.map +1 -0
  504. package/dist/modes/subagent-worker-mode.d.ts +5 -0
  505. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  506. package/dist/modes/subagent-worker-mode.js +91 -11
  507. package/dist/modes/subagent-worker-mode.js.map +1 -1
  508. package/dist/modes/subagent-worker-mode.test.d.ts +2 -0
  509. package/dist/modes/subagent-worker-mode.test.d.ts.map +1 -0
  510. package/dist/modes/subagent-worker-mode.test.js +52 -0
  511. package/dist/modes/subagent-worker-mode.test.js.map +1 -0
  512. package/dist/session.d.ts +29 -0
  513. package/dist/session.d.ts.map +1 -1
  514. package/dist/session.js +41 -0
  515. package/dist/session.js.map +1 -1
  516. package/dist/system-prompt.d.ts +11 -1
  517. package/dist/system-prompt.d.ts.map +1 -1
  518. package/dist/system-prompt.js +39 -18
  519. package/dist/system-prompt.js.map +1 -1
  520. package/dist/system-prompt.test.js +33 -17
  521. package/dist/system-prompt.test.js.map +1 -1
  522. package/dist/test-support/fake-home.d.ts +20 -0
  523. package/dist/test-support/fake-home.d.ts.map +1 -0
  524. package/dist/test-support/fake-home.js +37 -0
  525. package/dist/test-support/fake-home.js.map +1 -0
  526. package/dist/tools/bash.d.ts +3 -1
  527. package/dist/tools/bash.d.ts.map +1 -1
  528. package/dist/tools/bash.js +70 -14
  529. package/dist/tools/bash.js.map +1 -1
  530. package/dist/tools/bash.test.js +122 -7
  531. package/dist/tools/bash.test.js.map +1 -1
  532. package/dist/tools/edit-robustness.test.js +3 -1
  533. package/dist/tools/edit-robustness.test.js.map +1 -1
  534. package/dist/tools/find.d.ts.map +1 -1
  535. package/dist/tools/find.js +4 -1
  536. package/dist/tools/find.js.map +1 -1
  537. package/dist/tools/goals.d.ts +2 -2
  538. package/dist/tools/grep.d.ts +3 -1
  539. package/dist/tools/grep.d.ts.map +1 -1
  540. package/dist/tools/grep.js +137 -12
  541. package/dist/tools/grep.js.map +1 -1
  542. package/dist/tools/grep.test.js +53 -0
  543. package/dist/tools/grep.test.js.map +1 -1
  544. package/dist/tools/index.d.ts +19 -2
  545. package/dist/tools/index.d.ts.map +1 -1
  546. package/dist/tools/index.js +22 -19
  547. package/dist/tools/index.js.map +1 -1
  548. package/dist/tools/index.test.d.ts +2 -0
  549. package/dist/tools/index.test.d.ts.map +1 -0
  550. package/dist/tools/index.test.js +55 -0
  551. package/dist/tools/index.test.js.map +1 -0
  552. package/dist/tools/overflow.test.js +4 -7
  553. package/dist/tools/overflow.test.js.map +1 -1
  554. package/dist/tools/path-utils.d.ts +15 -0
  555. package/dist/tools/path-utils.d.ts.map +1 -1
  556. package/dist/tools/path-utils.js +17 -0
  557. package/dist/tools/path-utils.js.map +1 -1
  558. package/dist/tools/path-utils.test.js +7 -2
  559. package/dist/tools/path-utils.test.js.map +1 -1
  560. package/dist/tools/prompt-hints.js +1 -1
  561. package/dist/tools/prompt-hints.js.map +1 -1
  562. package/dist/tools/read-only-bash.d.ts +3 -0
  563. package/dist/tools/read-only-bash.d.ts.map +1 -1
  564. package/dist/tools/read-only-bash.js +7 -9
  565. package/dist/tools/read-only-bash.js.map +1 -1
  566. package/dist/tools/search-code.d.ts.map +1 -1
  567. package/dist/tools/search-code.js +6 -4
  568. package/dist/tools/search-code.js.map +1 -1
  569. package/dist/tools/subagent-control.d.ts.map +1 -1
  570. package/dist/tools/subagent-control.js +6 -2
  571. package/dist/tools/subagent-control.js.map +1 -1
  572. package/dist/tools/subagent-shared.d.ts +6 -0
  573. package/dist/tools/subagent-shared.d.ts.map +1 -1
  574. package/dist/tools/subagent-shared.js +6 -0
  575. package/dist/tools/subagent-shared.js.map +1 -1
  576. package/dist/tools/subagent.d.ts +1 -1
  577. package/dist/tools/subagent.d.ts.map +1 -1
  578. package/dist/tools/subagent.js +51 -8
  579. package/dist/tools/subagent.js.map +1 -1
  580. package/dist/tools/subagent.test.js +65 -3
  581. package/dist/tools/subagent.test.js.map +1 -1
  582. package/dist/tools/task-output.d.ts.map +1 -1
  583. package/dist/tools/task-output.js +3 -1
  584. package/dist/tools/task-output.js.map +1 -1
  585. package/dist/tools/tool-search.d.ts +12 -1
  586. package/dist/tools/tool-search.d.ts.map +1 -1
  587. package/dist/tools/tool-search.js +22 -3
  588. package/dist/tools/tool-search.js.map +1 -1
  589. package/dist/tools/tool-search.test.d.ts +2 -0
  590. package/dist/tools/tool-search.test.d.ts.map +1 -0
  591. package/dist/tools/tool-search.test.js +69 -0
  592. package/dist/tools/tool-search.test.js.map +1 -0
  593. package/dist/tools/web-fetch.d.ts +3 -2
  594. package/dist/tools/web-fetch.d.ts.map +1 -1
  595. package/dist/tools/web-fetch.js +22 -8
  596. package/dist/tools/web-fetch.js.map +1 -1
  597. package/dist/tools/web-fetch.test.js +34 -0
  598. package/dist/tools/web-fetch.test.js.map +1 -1
  599. package/dist/tools/web-search.d.ts +2 -1
  600. package/dist/tools/web-search.d.ts.map +1 -1
  601. package/dist/tools/web-search.js +21 -5
  602. package/dist/tools/web-search.js.map +1 -1
  603. package/dist/types.d.ts +7 -0
  604. package/dist/types.d.ts.map +1 -1
  605. package/dist/ui/App.d.ts.map +1 -1
  606. package/dist/ui/App.js +32 -81
  607. package/dist/ui/App.js.map +1 -1
  608. package/dist/ui/app-items.d.ts +1 -12
  609. package/dist/ui/app-items.d.ts.map +1 -1
  610. package/dist/ui/app-items.js.map +1 -1
  611. package/dist/ui/components/Footer.d.ts +1 -1
  612. package/dist/ui/components/Footer.js +2 -2
  613. package/dist/ui/components/Footer.js.map +1 -1
  614. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  615. package/dist/ui/hooks/useAgentLoop.js +18 -4
  616. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  617. package/dist/ui/hooks/usePixelFixFlow.d.ts +1 -2
  618. package/dist/ui/hooks/usePixelFixFlow.d.ts.map +1 -1
  619. package/dist/ui/hooks/usePixelFixFlow.js +2 -5
  620. package/dist/ui/hooks/usePixelFixFlow.js.map +1 -1
  621. package/dist/ui/login.js +1 -1
  622. package/dist/ui/login.js.map +1 -1
  623. package/dist/ui/terminal-history-status-renderers.d.ts +1 -2
  624. package/dist/ui/terminal-history-status-renderers.d.ts.map +1 -1
  625. package/dist/ui/terminal-history-status-renderers.js +2 -15
  626. package/dist/ui/terminal-history-status-renderers.js.map +1 -1
  627. package/dist/ui/terminal-history.d.ts.map +1 -1
  628. package/dist/ui/terminal-history.js +2 -4
  629. package/dist/ui/terminal-history.js.map +1 -1
  630. package/dist/ui/thinking-level-cycle.test.js +8 -8
  631. package/dist/ui/thinking-level-cycle.test.js.map +1 -1
  632. package/dist/ui/transcript/MiscRows.d.ts +1 -4
  633. package/dist/ui/transcript/MiscRows.d.ts.map +1 -1
  634. package/dist/ui/transcript/MiscRows.js +2 -7
  635. package/dist/ui/transcript/MiscRows.js.map +1 -1
  636. package/dist/ui/transcript/TranscriptRenderer.d.ts.map +1 -1
  637. package/dist/ui/transcript/TranscriptRenderer.js +1 -3
  638. package/dist/ui/transcript/TranscriptRenderer.js.map +1 -1
  639. package/dist/ui/transcript/presentation.d.ts +1 -9
  640. package/dist/ui/transcript/presentation.d.ts.map +1 -1
  641. package/dist/ui/transcript/presentation.js +0 -9
  642. package/dist/ui/transcript/presentation.js.map +1 -1
  643. package/dist/ui/transcript/spacing.d.ts +2 -2
  644. package/dist/ui/transcript/spacing.d.ts.map +1 -1
  645. package/dist/ui/transcript/spacing.js +0 -1
  646. package/dist/ui/transcript/spacing.js.map +1 -1
  647. package/dist/ui/transcript/spacing.test.js +1 -3
  648. package/dist/ui/transcript/spacing.test.js.map +1 -1
  649. package/dist/ui/tui-history-parity.test.js +2 -9
  650. package/dist/ui/tui-history-parity.test.js.map +1 -1
  651. package/dist/utils/github.d.ts +20 -0
  652. package/dist/utils/github.d.ts.map +1 -0
  653. package/dist/utils/github.js +65 -0
  654. package/dist/utils/github.js.map +1 -0
  655. package/dist/utils/github.test.d.ts +2 -0
  656. package/dist/utils/github.test.d.ts.map +1 -0
  657. package/dist/utils/github.test.js +23 -0
  658. package/dist/utils/github.test.js.map +1 -0
  659. package/dist/utils/image.d.ts +26 -3
  660. package/dist/utils/image.d.ts.map +1 -1
  661. package/dist/utils/image.js +83 -14
  662. package/dist/utils/image.js.map +1 -1
  663. package/dist/utils/image.test.d.ts +2 -0
  664. package/dist/utils/image.test.d.ts.map +1 -0
  665. package/dist/utils/image.test.js +139 -0
  666. package/dist/utils/image.test.js.map +1 -0
  667. package/dist/utils/process.d.ts +26 -3
  668. package/dist/utils/process.d.ts.map +1 -1
  669. package/dist/utils/process.js +68 -10
  670. package/dist/utils/process.js.map +1 -1
  671. package/dist/utils/process.test.d.ts +2 -0
  672. package/dist/utils/process.test.d.ts.map +1 -0
  673. package/dist/utils/process.test.js +42 -0
  674. package/dist/utils/process.test.js.map +1 -0
  675. package/package.json +6 -5
@@ -21,8 +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";
27
+ import { RunClaim } from "./core/run-claim.js";
26
28
  import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
27
29
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
28
30
  import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
@@ -32,11 +34,13 @@ import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
32
34
  import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
33
35
  import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
34
36
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
35
- import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, restoreUserRow, restoreAssistantTexts, autopilotMarkerCopySeed, } from "./core/session-history.js";
37
+ import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, getHistoryMessageVisibility, replayMessagesInOrder, restoreUserRow, restoreAssistantTexts, resolveRestoredCommand, autopilotMarkerCopySeed, } from "./core/session-history.js";
38
+ import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
36
39
  import { AuthStorage } from "./core/auth-storage.js";
37
40
  import { cleanupToolOutputs } from "./tools/overflow.js";
38
41
  import { readCappedBody } from "./utils/http-body.js";
39
- import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, } from "@prestyj/core";
42
+ import { fetchSubscriptionUsage, MOONSHOT_OAUTH_KEY, SubscriptionUsageError, XIAOMI_CREDITS_KEY, discoverLocalModels, findProbedModel, formatLocalModelId, parseLocalModelId, probeEndpoint, toModelInfo as localModelInfo, } from "@prestyj/core";
43
+ import { LocalEndpointError, addCustomEndpoint, listAllEndpoints, removeCustomEndpoint, syncEndpointCredentials, } from "./core/local-endpoint-store.js";
40
44
  import { loginAnthropic } from "./core/oauth/anthropic.js";
41
45
  import { loginOpenAI } from "./core/oauth/openai.js";
42
46
  import { loginGemini } from "./core/oauth/gemini.js";
@@ -44,9 +48,11 @@ import { loginKimi } from "./core/oauth/kimi.js";
44
48
  import { AUTH_PROVIDERS } from "./core/auth-providers.js";
45
49
  import { ensureAppDirs, loadSavedSettings } from "./config.js";
46
50
  import { SettingsManager } from "./core/settings-manager.js";
47
- import { getModel, getDefaultThinkingLevel, getContextWindow, MODELS, } from "./core/model-registry.js";
51
+ import { installPlugin, listInstalledPlugins, removePlugin, } from "./core/extensions/plugin-bundles.js";
52
+ import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
48
53
  import { resolveStartOrFallback } from "./core/resolve-start.js";
49
54
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
55
+ import { getGitHubOpenCounts, getGitHubRepoSlug } from "./utils/github.js";
50
56
  import { extractPlanSteps } from "./utils/plan-steps.js";
51
57
  import { getNextThinkingLevel, getSupportedThinkingLevels, isThinkingLevelSupported, } from "./core/thinking-level.js";
52
58
  import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
@@ -60,13 +66,18 @@ import { enrichProcessPath } from "./core/shell-path.js";
60
66
  import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
61
67
  import { startServeMode } from "./modes/serve-mode.js";
62
68
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
63
- import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, } from "./core/mcp/index.js";
69
+ import { loadServers, addServer, removeServer, getServer, parseMcpAddCommand, MCPClientManager, McpOAuthStore, createElicitationBridge, } from "./core/mcp/index.js";
64
70
  import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
65
71
  import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
66
72
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
67
73
  import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
68
74
  import { rebuildFromSessions } from "./core/progress/rebuild.js";
69
75
  import { captureSidecarError, flushSidecarErrors, shouldCaptureToolFailure, shouldCaptureUsagePollingError, wrapSidecarHandler, } from "./core/sidecar-error-reporter.js";
76
+ const AUTOMATION_PROVENANCE = {
77
+ source: "runtime",
78
+ kind: "automation",
79
+ visibility: "hidden",
80
+ };
70
81
  const ALL_PROVIDERS = [
71
82
  // US
72
83
  "anthropic",
@@ -83,6 +94,12 @@ const ALL_PROVIDERS = [
83
94
  "sakana",
84
95
  "openrouter",
85
96
  ];
97
+ function stringArray(value) {
98
+ if (!Array.isArray(value))
99
+ return undefined;
100
+ const out = value.filter((v) => typeof v === "string" && v.trim().length > 0);
101
+ return out.length > 0 ? out : undefined;
102
+ }
86
103
  function appSettingsFile() {
87
104
  return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
88
105
  }
@@ -106,6 +123,8 @@ async function loadAppSettings() {
106
123
  projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
107
124
  autopilot: raw.autopilot && typeof raw.autopilot === "object" ? raw.autopilot : undefined,
108
125
  nolanModels: raw.nolanModels && typeof raw.nolanModels === "object" ? raw.nolanModels : undefined,
126
+ projectRoots: stringArray(raw.projectRoots),
127
+ hiddenProjects: stringArray(raw.hiddenProjects),
109
128
  };
110
129
  }
111
130
  catch {
@@ -344,6 +363,11 @@ async function searchProjectFiles(cwd, rawQuery) {
344
363
  * hook prompt, by its distinctive opening phrase. Returns the hook kind so the
345
364
  * webview can render the short notice line instead of the full prompt body.
346
365
  */
366
+ /** `?tools=` on /export. Anything unrecognised (including absent) falls back to
367
+ * the human-facing `summary` default rather than dumping every payload. */
368
+ function parseToolDetail(value) {
369
+ return value === "none" || value === "full" ? value : "summary";
370
+ }
347
371
  function detectHookKind(text) {
348
372
  const t = text.trimStart();
349
373
  if (t.startsWith("Ideal? Review the actual work"))
@@ -354,30 +378,6 @@ function detectHookKind(text) {
354
378
  return "regrounding";
355
379
  return null;
356
380
  }
357
- // Separator AgentSession.prompt() inserts between a command's prompt body and
358
- // the user's trailing args. Must stay in sync with the expansion there.
359
- const COMMAND_ARGS_SEP = "\n\n## User Instructions\n\n";
360
- /**
361
- * Reverse a prompt-template command's expansion. When a `/name` command runs,
362
- * the agent persists the FULL expanded prompt body as the user message — so on
363
- * resume the raw body would render instead of the short `/name` chip the user
364
- * saw live. Given the candidate commands (built-in + custom) and a restored
365
- * message body, recover the original `/name [args]` invocation. Returns null
366
- * when the text isn't a known command body (an ordinary user message).
367
- */
368
- function detectPromptCommand(text, candidates) {
369
- for (const c of candidates) {
370
- if (!c.prompt)
371
- continue;
372
- if (text === c.prompt)
373
- return `/${c.name}`;
374
- if (text.startsWith(c.prompt + COMMAND_ARGS_SEP)) {
375
- const args = text.slice(c.prompt.length + COMMAND_ARGS_SEP.length).trim();
376
- return args ? `/${c.name} ${args}` : `/${c.name}`;
377
- }
378
- }
379
- return null;
380
- }
381
381
  /** A short transport summary for display (URL for http/sse, command+args for stdio). */
382
382
  function mcpRowSummary(config) {
383
383
  if (config.url)
@@ -451,6 +451,7 @@ async function runJsonModeIfRequested() {
451
451
  "max-turns": { type: "string" },
452
452
  "system-prompt": { type: "string" },
453
453
  tools: { type: "string" },
454
+ "mcp-servers": { type: "string" },
454
455
  "prompt-cache-key": { type: "string" },
455
456
  },
456
457
  allowPositionals: true,
@@ -467,14 +468,24 @@ async function runJsonModeIfRequested() {
467
468
  .filter(Boolean)
468
469
  : [];
469
470
  const allowedTools = parsedTools.length > 0 ? parsedTools : undefined;
471
+ // MCP servers the agent definition asked for. Without forwarding these, an
472
+ // allow-listed child connects no MCP at all and loses live code search.
473
+ const parsedMcpServers = values["mcp-servers"]
474
+ ? values["mcp-servers"]
475
+ .split(",")
476
+ .map((s) => s.trim())
477
+ .filter(Boolean)
478
+ : [];
479
+ const allowedMcpServers = parsedMcpServers.length > 0 ? parsedMcpServers : undefined;
470
480
  await runJsonMode({
471
481
  message: positionals[0] ?? "",
472
482
  provider: (values.provider ?? "anthropic"),
473
- model: values.model ?? "claude-opus-4-8",
483
+ model: values.model ?? "claude-opus-5",
474
484
  cwd: process.cwd(),
475
485
  systemPrompt: values["system-prompt"],
476
486
  maxTurns: maxTurnsRaw ? parseInt(maxTurnsRaw, 10) : undefined,
477
487
  allowedTools,
488
+ allowedMcpServers,
478
489
  promptCacheKey: values["prompt-cache-key"],
479
490
  }).catch(async (err) => {
480
491
  captureSidecarError(err, "app-sidecar.json-mode", {
@@ -518,6 +529,31 @@ async function main() {
518
529
  // ~/.ezcoder/debug.log (initLogger truncates on each start).
519
530
  const sidecarLog = path.join(paths.agentDir, "ezcoder-app-sidecar.log");
520
531
  initLogger(sidecarLog);
532
+ // The desktop sidecar previously omitted the stream diagnostic hook used by
533
+ // the CLI, leaving device-specific provider stalls impossible to distinguish from
534
+ // event-loop starvation or a broken streaming network path. Keep routine
535
+ // phases lightweight; timeout phases include non-sensitive runtime context.
536
+ setStreamDiagnostic((phase, data) => {
537
+ const includeRuntime = phase === "idle_timeout_fired" ||
538
+ phase === "hard_timeout_fired" ||
539
+ phase === "stall_exhausted";
540
+ log("INFO", "stream", phase, {
541
+ ...(data ?? {}),
542
+ ...(includeRuntime
543
+ ? {
544
+ platform: process.platform,
545
+ arch: process.arch,
546
+ osRelease: os.release(),
547
+ node: process.version,
548
+ logicalCpus: os.cpus().length,
549
+ totalMemoryMb: Math.round(os.totalmem() / 1024 / 1024),
550
+ freeMemoryMb: Math.round(os.freemem() / 1024 / 1024),
551
+ rssMb: Math.round(process.memoryUsage().rss / 1024 / 1024),
552
+ processUptimeSec: Math.round(process.uptime()),
553
+ }
554
+ : {}),
555
+ });
556
+ });
521
557
  // Global last-resort guards, installed as early as the logger allows so they
522
558
  // cover the WHOLE lifecycle — including startup/initialize, the phase the
523
559
  // "sidecar did not start in time" bug lives in. The sidecar is a long-lived
@@ -553,6 +589,20 @@ async function main() {
553
589
  // request to its window's session via the `x-gg-session` header (and the
554
590
  // `?session=` query for the SSE /events stream).
555
591
  const sessions = new Map();
592
+ /**
593
+ * Fan one frame out to every window.
594
+ *
595
+ * For state that is genuinely global rather than per-session — `~/.ezcoder/auth.json`
596
+ * is shared by all windows, so connecting a provider in one must refresh the
597
+ * model picker in all of them. Mirrors the memory/jiwa/progress fan-outs.
598
+ */
599
+ const broadcastAll = (type, data) => {
600
+ for (const ctx of sessions.values())
601
+ ctx.broadcast(type, data);
602
+ };
603
+ // Providers currently mid-OAuth in some window. Daemon-level so two windows
604
+ // cannot race two browser flows for the same provider into one auth file.
605
+ const oauthInFlightProviders = new Set();
556
606
  const memoryStore = new MemoryStore({
557
607
  onChange: ({ memories }) => {
558
608
  for (const ctx of sessions.values()) {
@@ -576,6 +626,13 @@ async function main() {
576
626
  });
577
627
  const usageCache = new Map();
578
628
  const usageRequests = new Map();
629
+ // Last snapshot that actually carried windows, per provider. Replayed while a
630
+ // fetch is failing so the title meter never blinks out of existence. Bounded
631
+ // by USAGE_LAST_GOOD_MAX_AGE_MS — a provider that never recovers must stop
632
+ // reporting rather than freeze a percentage (and a long-past reset time) on
633
+ // screen forever. The 429 backoff alone runs to 24h, far past any usefulness.
634
+ const usageLastGood = new Map();
635
+ const USAGE_LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
579
636
  // 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
580
637
  // provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
581
638
  // the provider value so a malformed header can neither hammer the endpoint
@@ -590,6 +647,9 @@ async function main() {
590
647
  // Moonshot platform API key is metered per-token, not per plan window.
591
648
  const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
592
649
  if (!(await auth.hasProviderAuth(authKey))) {
650
+ // Logged out: drop the replay cache so a later login on a DIFFERENT
651
+ // account can never inherit the previous one's numbers.
652
+ usageLastGood.delete(provider);
593
653
  return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
594
654
  }
595
655
  try {
@@ -633,6 +693,18 @@ async function main() {
633
693
  ...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
634
694
  });
635
695
  const connected = await auth.hasProviderAuth(authKey);
696
+ // Transient failures (notably the 429s these auxiliary quota endpoints
697
+ // hand out) must not blank the meter. Keep serving the last good
698
+ // snapshot, flagged `stale`, so the bar stays put instead of flickering
699
+ // out for the whole backoff window and back in on the next success.
700
+ // Past the max age it's dropped — no data beats confidently wrong data.
701
+ const lastGood = usageLastGood.get(provider);
702
+ if (lastGood && Date.now() - lastGood.fetchedAt >= USAGE_LAST_GOOD_MAX_AGE_MS) {
703
+ usageLastGood.delete(provider);
704
+ }
705
+ else if (connected && lastGood) {
706
+ return { ...lastGood, stale: true };
707
+ }
636
708
  return {
637
709
  provider,
638
710
  displayName,
@@ -654,6 +726,11 @@ async function main() {
654
726
  usageRequests.set(provider, request);
655
727
  try {
656
728
  const result = await request;
729
+ // Never re-store a replay — it would keep its original `fetchedAt`, but
730
+ // writing it back muddies the "last GOOD" contract for no gain.
731
+ if (result.connected && !result.error && !result.stale && result.windows.length > 0) {
732
+ usageLastGood.set(provider, result);
733
+ }
657
734
  // Anthropic can return utilization before it assigns reset timestamps
658
735
  // (notably before the account's first active request). Retry that partial
659
736
  // snapshot quickly; complete snapshots keep the normal one-minute cache.
@@ -721,7 +798,15 @@ async function main() {
721
798
  const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
722
799
  const id = randomUUID();
723
800
  try {
724
- const ctx = await createSession({ auth, paths, progress, memoryStore, jiwaStore }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
801
+ const ctx = await createSession({
802
+ auth,
803
+ paths,
804
+ progress,
805
+ memoryStore,
806
+ jiwaStore,
807
+ broadcastAll,
808
+ oauthInFlightProviders,
809
+ }, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
725
810
  sessions.set(id, ctx);
726
811
  log("INFO", "app-sidecar", "session created", {
727
812
  id,
@@ -997,7 +1082,7 @@ async function createProgressManager(agentDir, broadcastAll) {
997
1082
  * logger, PATH, shared auth file, and radio live at the daemon level.
998
1083
  */
999
1084
  async function createSession(deps, opts) {
1000
- const { auth, progress, memoryStore, jiwaStore } = deps;
1085
+ const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders } = deps;
1001
1086
  const paths = deps.paths;
1002
1087
  const mode = opts.mode;
1003
1088
  let chatAgent = opts.chatAgent;
@@ -1072,6 +1157,25 @@ async function createSession(deps, opts) {
1072
1157
  // /help
1073
1158
  .replaceAll(/see \/help/gi, "check the help menu"));
1074
1159
  }
1160
+ /**
1161
+ * Guidance for a connection failure against the ACTIVE local endpoint, or
1162
+ * undefined when that isn't the situation (so the generic wording stands).
1163
+ * "Disable your VPN / allow us through the firewall" is useless advice for a
1164
+ * server running on this machine — name what the user has to restart.
1165
+ */
1166
+ function localNetworkGuidance(source) {
1167
+ if (source !== "network")
1168
+ return undefined;
1169
+ const state = session.getState();
1170
+ if (state.provider !== "local")
1171
+ return undefined;
1172
+ const parsed = parseLocalModelId(state.model);
1173
+ const probe = localProbes.find((p) => p.endpoint.id === parsed?.endpointId);
1174
+ if (!probe)
1175
+ return undefined;
1176
+ return (`${probe.endpoint.label} stopped responding at ${probe.endpoint.baseUrl}. ` +
1177
+ "Start it again and retry, or pick another model.");
1178
+ }
1075
1179
  // Turn any thrown value into the same clear headline/message/guidance shape
1076
1180
  // the TUI shows (see gg-ai's formatError) instead of a bare `err.message`, log
1077
1181
  // the full detail, and broadcast it under `type` ("error" or "nolan_error").
@@ -1081,7 +1185,7 @@ async function createSession(deps, opts) {
1081
1185
  function broadcastError(type, logLabel, err) {
1082
1186
  const f = formatError(err);
1083
1187
  const message = f.message ? desktopGuidance(f.message) : undefined;
1084
- const guidance = desktopGuidance(f.guidance);
1188
+ const guidance = localNetworkGuidance(f.source) ?? desktopGuidance(f.guidance);
1085
1189
  captureSidecarError(err, `app-sidecar.${logLabel.replaceAll(" ", "-")}`, {
1086
1190
  scope: type,
1087
1191
  ...(f.provider ? { provider: f.provider } : {}),
@@ -1114,6 +1218,17 @@ async function createSession(deps, opts) {
1114
1218
  })
1115
1219
  .catch(() => { });
1116
1220
  }
1221
+ // ── MCP elicitation bridge ─────────────────────────────────
1222
+ // An MCP server can ask for user input in the middle of a tool call. The
1223
+ // bridge parks the promise; we broadcast the prompt over SSE and resolve it
1224
+ // when the webview POSTs /mcp/elicit/:id.
1225
+ const elicitations = createElicitationBridge({
1226
+ broadcast: (prompt) => broadcast("mcp_elicit", prompt),
1227
+ onTimeout: (prompt) => log("WARN", "app-sidecar", "MCP elicitation timed out", {
1228
+ id: prompt.id,
1229
+ server: prompt.server,
1230
+ }),
1231
+ });
1117
1232
  // The session file path to resume (passed by the daemon's POST /session);
1118
1233
  // empty/unset starts a fresh session.
1119
1234
  const resumeSessionPath = opts.sessionPath;
@@ -1127,6 +1242,7 @@ async function createSession(deps, opts) {
1127
1242
  signal: abort.signal,
1128
1243
  // Keep MCP startup off the readiness path in both modes.
1129
1244
  backgroundMcpConnect: true,
1245
+ onMcpElicit: elicitations.onElicit,
1130
1246
  // Keep restore-time auto-compaction off the readiness path too: its summary
1131
1247
  // LLM call (30s timeout) used to freeze waitForReady — and with it the whole
1132
1248
  // window (project picker, session list) — whenever a resumed session was
@@ -1197,17 +1313,116 @@ async function createSession(deps, opts) {
1197
1313
  }
1198
1314
  }
1199
1315
  log("INFO", "app-sidecar", "session ready", { provider, model, mode, chatAgent, cwd });
1316
+ // ── Local models (Ollama / LM Studio / llama.cpp / vLLM) ──
1317
+ // Probing four HTTP endpoints must never delay readiness, so this runs in the
1318
+ // background (same shape as backgroundMcpConnect) and pushes a models_change
1319
+ // frame when it lands.
1320
+ let localProbes = [];
1321
+ async function scanLocalModels(force) {
1322
+ const endpoints = await listAllEndpoints();
1323
+ const { probes, models } = await discoverLocalModels(endpoints, { force });
1324
+ localProbes = probes;
1325
+ // Only endpoints that answered get a credential: writing one for a server
1326
+ // that isn't running would make `hasProviderAuth("local")` true forever.
1327
+ await syncEndpointCredentials(probes.filter((probe) => probe.reachable).map((probe) => probe.endpoint));
1328
+ clearRuntimeModels((m) => m.provider === "local");
1329
+ registerRuntimeModels(models);
1330
+ log("INFO", "app-sidecar", "local model scan", {
1331
+ reachable: probes.filter((p) => p.reachable).length + "/" + probes.length,
1332
+ models: String(models.length),
1333
+ });
1334
+ return probes;
1335
+ }
1336
+ /** Endpoint rows + models, in the shape the Local models UI renders. */
1337
+ function localStatePayload() {
1338
+ return {
1339
+ endpoints: localProbes.map((probe) => ({
1340
+ id: probe.endpoint.id,
1341
+ label: probe.endpoint.label,
1342
+ baseUrl: probe.endpoint.baseUrl,
1343
+ kind: probe.endpoint.kind,
1344
+ custom: probe.endpoint.custom === true,
1345
+ reachable: probe.reachable,
1346
+ ...(probe.reason ? { reason: probe.reason } : {}),
1347
+ models: probe.models.map((m) => ({
1348
+ id: formatLocalModelId(probe.endpoint.id, m.rawId),
1349
+ rawId: m.rawId,
1350
+ contextWindow: m.contextWindow,
1351
+ contextWindowKnown: m.contextWindowKnown,
1352
+ supportsTools: m.supportsTools,
1353
+ supportsImages: m.supportsImages,
1354
+ supportsThinking: m.supportsThinking,
1355
+ ...(m.loaded === undefined ? {} : { loaded: m.loaded }),
1356
+ })),
1357
+ })),
1358
+ };
1359
+ }
1360
+ /**
1361
+ * Why `modelId` can't be selected right now, or `undefined` when it can.
1362
+ * Two real footguns get a clear answer instead of a mid-run provider error:
1363
+ * a model with no tool calling (can't drive the agent at all), and a server
1364
+ * that has since been shut down.
1365
+ */
1366
+ async function localModelBlocker(modelId) {
1367
+ const parsed = parseLocalModelId(modelId);
1368
+ if (!parsed)
1369
+ return undefined;
1370
+ const endpoints = await listAllEndpoints();
1371
+ const endpoint = endpoints.find((e) => e.id === parsed.endpointId);
1372
+ if (!endpoint)
1373
+ return `Unknown local endpoint "${parsed.endpointId}" — re-scan for local models.`;
1374
+ const probe = await probeEndpoint(endpoint);
1375
+ // Keep the cached view honest: this probe is fresher than the last scan.
1376
+ localProbes = localProbes.map((p) => (p.endpoint.id === endpoint.id ? probe : p));
1377
+ if (!probe.reachable) {
1378
+ return `${endpoint.label} isn't running at ${endpoint.baseUrl}. Start it and scan again.`;
1379
+ }
1380
+ const model = probe.models.find((m) => m.rawId === parsed.rawId);
1381
+ if (!model) {
1382
+ return `${endpoint.label} no longer serves "${parsed.rawId}".`;
1383
+ }
1384
+ if (!model.supportsTools) {
1385
+ return `${parsed.rawId} has no tool calling, so it can't run the agent. Pick a tool-capable model.`;
1386
+ }
1387
+ registerRuntimeModels(probe.models.map((m) => localModelInfo(m, endpoint)));
1388
+ return undefined;
1389
+ }
1390
+ /**
1391
+ * A restored per-project pref can ask for thinking on a local model that
1392
+ * turns out not to reason (capabilities are only known after a probe). Drop
1393
+ * the level once we know, so the first prompt doesn't carry a
1394
+ * `reasoning_effort` the server rejects.
1395
+ */
1396
+ function clampLocalThinking() {
1397
+ const st = session.getState();
1398
+ const level = session.getThinkingLevel();
1399
+ if (!level || st.provider !== "local")
1400
+ return;
1401
+ if (isThinkingLevelSupported(st.provider, st.model, level))
1402
+ return;
1403
+ session.setThinkingLevel(undefined);
1404
+ broadcast("thinking_change", {
1405
+ thinkingLevel: null,
1406
+ supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
1407
+ });
1408
+ }
1200
1409
  // Workspace extras (context window, git status, background tasks). Git state
1201
1410
  // is resolved once at startup and refreshed after every run; the context
1202
1411
  // window follows the active model.
1203
- const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount] = await Promise.all([
1412
+ const [initialGitBranch, initialGitIsRepo, initialDirtyFileCount, initialGitHubSlug] = await Promise.all([
1204
1413
  getGitBranch(cwd).catch(() => null),
1205
1414
  isGitRepo(cwd).catch(() => false),
1206
1415
  getGitDirtyFileCount(cwd).catch(() => 0),
1416
+ getGitHubRepoSlug(cwd).catch(() => null),
1207
1417
  ]);
1208
1418
  let gitBranch = initialGitBranch;
1209
1419
  let gitIsRepo = initialGitIsRepo;
1210
1420
  let gitDirtyFileCount = initialDirtyFileCount;
1421
+ // Open issue/PR counts for the origin repo's GitHub slug, via the `gh` CLI's
1422
+ // auth. null = unknown (gh missing/unauthed, non-GitHub origin) → chips hidden.
1423
+ const gitHubSlug = initialGitHubSlug;
1424
+ let gitHubIssues = null;
1425
+ let gitHubPRs = null;
1211
1426
  function currentContextWindow() {
1212
1427
  const st = session.getState();
1213
1428
  return getContextWindow(st.model, { provider: st.provider, accountId: st.accountId });
@@ -1220,9 +1435,41 @@ async function createSession(deps, opts) {
1220
1435
  gitBranch,
1221
1436
  isGitRepo: gitIsRepo,
1222
1437
  gitDirtyFileCount,
1438
+ gitHubIssues,
1439
+ gitHubPRs,
1440
+ gitHubRepoUrl: gitHubSlug ? `https://github.com/${gitHubSlug}` : null,
1223
1441
  tasks: session.listBackgroundProcesses(),
1442
+ // Roots added with /add-dir — the header shows a badge when non-empty.
1443
+ additionalRoots: session.getAdditionalRoots(),
1224
1444
  };
1225
1445
  }
1446
+ void scanLocalModels(false)
1447
+ .then(() => {
1448
+ clampLocalThinking();
1449
+ broadcast("extras", footerExtras());
1450
+ })
1451
+ .then(() => broadcast("models_change", { local: localStatePayload() }))
1452
+ .catch((err) => {
1453
+ // A discovery failure is never fatal — the user simply has no local
1454
+ // models. Log it; don't push an error row into the transcript.
1455
+ log("WARN", "app-sidecar", "local model scan failed", {
1456
+ error: err instanceof Error ? err.message : String(err),
1457
+ });
1458
+ });
1459
+ // Refresh the GitHub counts and broadcast only on change. Transient failures
1460
+ // keep the last-known numbers so the chips don't flicker off on a timeout.
1461
+ async function refreshGitHubCounts() {
1462
+ if (!gitHubSlug)
1463
+ return;
1464
+ const counts = await getGitHubOpenCounts(gitHubSlug);
1465
+ if (!counts)
1466
+ return;
1467
+ if (counts.issues !== gitHubIssues || counts.prs !== gitHubPRs) {
1468
+ gitHubIssues = counts.issues;
1469
+ gitHubPRs = counts.prs;
1470
+ broadcast("extras", footerExtras());
1471
+ }
1472
+ }
1226
1473
  // tool_call_end carries no tool name (only the id), so remember each call's
1227
1474
  // name from tool_call_start to log a useful line on completion. Mirrors the
1228
1475
  // CLI's logging so the app sidecar's ~/.ezcoder/ezcoder-app-sidecar.log records tool
@@ -1318,6 +1565,10 @@ async function createSession(deps, opts) {
1318
1565
  recordApprovedPlanMarkers(d.text);
1319
1566
  });
1320
1567
  session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
1568
+ // The agent consumed queued steering at a turn boundary. Re-broadcast as the
1569
+ // usual `queued` depth update so the webview drops the pending affordance the
1570
+ // moment the message lands in the loop, not at run_end.
1571
+ session.eventBus.on("queue_drained", (d) => broadcast("queued", { count: d.count, messages: session.listQueuedMessages() }));
1321
1572
  session.eventBus.on("tool_call_start", (d) => {
1322
1573
  toolCallNames.set(d.toolCallId, d.name);
1323
1574
  broadcast("tool_call_start", d);
@@ -1369,10 +1620,29 @@ async function createSession(deps, opts) {
1369
1620
  session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
1370
1621
  session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
1371
1622
  let running = false;
1623
+ // Closes the window between `/prompt` deciding to start a run and `runAgent`
1624
+ // flipping `running` — that stretch awaits, so Node yields inside it. See
1625
+ // RunClaim.
1626
+ const runClaim = new RunClaim();
1372
1627
  const runLifecycle = new RunLifecycle((runState) => {
1373
1628
  running = runState !== "idle";
1374
1629
  if (runState === "cancelling")
1375
1630
  broadcast("run_cancelling", { runState });
1631
+ },
1632
+ // Durable run journal. Fire-and-forget on purpose: an unwritten journal
1633
+ // entry is a missed crash hint, while a journal write that throws inside
1634
+ // begin()/settle() would break run ownership itself.
1635
+ {
1636
+ started: (generation) => void session.persistRunStarted(generation).catch((err) => {
1637
+ log("WARN", "app-sidecar", "failed to journal run start", {
1638
+ error: err instanceof Error ? err.message : String(err),
1639
+ });
1640
+ }),
1641
+ finished: (generation, outcome) => void session.persistRunFinished(generation, outcome).catch((err) => {
1642
+ log("WARN", "app-sidecar", "failed to journal run finish", {
1643
+ error: err instanceof Error ? err.message : String(err),
1644
+ });
1645
+ }),
1376
1646
  });
1377
1647
  const cancelledRunEndGenerations = new Set();
1378
1648
  let pendingCancelDrain = null;
@@ -1448,7 +1718,7 @@ async function createSession(deps, opts) {
1448
1718
  // with `@Nolan …`; he reads EZ Coder's transcript (one-way — EZ Coder never sees
1449
1719
  // Nolan's) and hands back runnable prompts + mentorship. Created lazily on the
1450
1720
  // first `@Nolan` so windows that never use Nolan pay zero cost. His events ride the
1451
- // SAME SSE stream with `ken_`-prefixed types, routed to the Nolan bubble.
1721
+ // SAME SSE stream with `nolan_`-prefixed types, routed to the Nolan bubble.
1452
1722
  let nolanSession = null;
1453
1723
  let nolanAbort = new AbortController();
1454
1724
  let nolanRunning = false;
@@ -1478,7 +1748,7 @@ async function createSession(deps, opts) {
1478
1748
  return { provider: st.provider, model: st.model };
1479
1749
  }
1480
1750
  /** Footer payload: Nolan's effective model + whether it's a pin. Merged into
1481
- * /state, the SSE ready frame, and every ken_model_change broadcast. */
1751
+ * /state, the SSE ready frame, and every nolan_model_change broadcast. */
1482
1752
  function nolanStatePayload() {
1483
1753
  const st = session.getState();
1484
1754
  return effectiveNolanModel(nolanModelOverride, { provider: st.provider, model: st.model });
@@ -1494,13 +1764,13 @@ async function createSession(deps, opts) {
1494
1764
  if (st.provider === provider && st.model === model)
1495
1765
  return;
1496
1766
  await nolanSession.switchModel(provider, model);
1497
- log("INFO", "app-sidecar", "ken session model synced", { provider, model });
1767
+ log("INFO", "app-sidecar", "nolan session model synced", { provider, model });
1498
1768
  }
1499
1769
  async function ensureNolanSession() {
1500
1770
  if (nolanSession)
1501
1771
  return nolanSession;
1502
1772
  const target = nolanCurrentModel();
1503
- const ken = new AgentSession({
1773
+ const nolanAgent = new AgentSession({
1504
1774
  provider: target.provider,
1505
1775
  model: target.model,
1506
1776
  cwd,
@@ -1509,38 +1779,41 @@ async function createSession(deps, opts) {
1509
1779
  allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
1510
1780
  transient: true,
1511
1781
  signal: nolanAbort.signal,
1512
- // Nolan's bursty, spread-out turns (chat) outlast the default 5-min cache
1782
+ // Nolan belongs to this window, so route MCP elicitation prompts there.
1783
+ // Keeping daemon sessions interactive also lets them share one pooled MCP connection.
1784
+ onMcpElicit: elicitations.onElicit,
1785
+ // Nolan's bursty, spread-out turns outlast the default cache
1513
1786
  // TTL regardless of the user's global speedProfile pick.
1514
1787
  forceLongCacheRetention: true,
1515
1788
  });
1516
- await ken.initialize();
1517
- // Bridge Nolan's bus to the shared SSE fan-out with ken_-prefixed types so the
1789
+ await nolanAgent.initialize();
1790
+ // Bridge Nolan's bus to the shared SSE fan-out with nolan_-prefixed types so the
1518
1791
  // webview routes them to the Nolan bubble, never EZ Coder's.
1519
- ken.eventBus.on("text_delta", (d) => broadcast("nolan_text_delta", d));
1520
- ken.eventBus.on("thinking_delta", (d) => broadcast("nolan_thinking_delta", d));
1521
- ken.eventBus.on("tool_call_start", (d) => {
1792
+ nolanAgent.eventBus.on("text_delta", (d) => broadcast("nolan_text_delta", d));
1793
+ nolanAgent.eventBus.on("thinking_delta", (d) => broadcast("nolan_thinking_delta", d));
1794
+ nolanAgent.eventBus.on("tool_call_start", (d) => {
1522
1795
  nolanToolCallNames.set(d.toolCallId, d.name);
1523
1796
  broadcast("nolan_tool_call_start", d);
1524
1797
  });
1525
- ken.eventBus.on("tool_call_update", (d) => broadcast("nolan_tool_call_update", d));
1526
- ken.eventBus.on("tool_call_end", (d) => {
1798
+ nolanAgent.eventBus.on("tool_call_update", (d) => broadcast("nolan_tool_call_update", d));
1799
+ nolanAgent.eventBus.on("tool_call_end", (d) => {
1527
1800
  nolanToolCallNames.delete(d.toolCallId);
1528
1801
  broadcast("nolan_tool_call_end", d);
1529
1802
  });
1530
1803
  // Native server tools (Anthropic web_search) stream text both before AND
1531
1804
  // after them in the same turn; forward so the webview can break the bubble
1532
1805
  // (otherwise "...work.Local tools..." glues together). Mirrors the build bus.
1533
- ken.eventBus.on("server_tool_call", (d) => broadcast("nolan_server_tool_call", d));
1534
- ken.eventBus.on("turn_end", (d) => broadcast("nolan_turn_end", d));
1535
- ken.eventBus.on("error", (d) => {
1536
- broadcastError("nolan_error", "ken error", d.error);
1806
+ nolanAgent.eventBus.on("server_tool_call", (d) => broadcast("nolan_server_tool_call", d));
1807
+ nolanAgent.eventBus.on("turn_end", (d) => broadcast("nolan_turn_end", d));
1808
+ nolanAgent.eventBus.on("error", (d) => {
1809
+ broadcastError("nolan_error", "nolan error", d.error);
1537
1810
  });
1538
- nolanSession = ken;
1539
- log("INFO", "app-sidecar", "ken session ready", {
1811
+ nolanSession = nolanAgent;
1812
+ log("INFO", "app-sidecar", "nolan session ready", {
1540
1813
  provider: target.provider,
1541
1814
  model: target.model,
1542
1815
  });
1543
- return ken;
1816
+ return nolanAgent;
1544
1817
  }
1545
1818
  // ── Autopilot Nolan (auto-reviewer) ──────────────────────────
1546
1819
  // A THIRD read-only AgentSession, separate from chat Nolan. In autopilot mode
@@ -1563,13 +1836,13 @@ async function createSession(deps, opts) {
1563
1836
  if (st.provider === provider && st.model === model)
1564
1837
  return;
1565
1838
  await nolanAutoSession.switchModel(provider, model);
1566
- log("INFO", "app-sidecar", "ken autopilot session model synced", { provider, model });
1839
+ log("INFO", "app-sidecar", "nolan autopilot session model synced", { provider, model });
1567
1840
  }
1568
1841
  async function ensureNolanAutoSession() {
1569
1842
  if (nolanAutoSession)
1570
1843
  return nolanAutoSession;
1571
1844
  const target = nolanCurrentModel();
1572
- const ken = new AgentSession({
1845
+ const nolanAgent = new AgentSession({
1573
1846
  provider: target.provider,
1574
1847
  model: target.model,
1575
1848
  cwd,
@@ -1578,26 +1851,32 @@ async function createSession(deps, opts) {
1578
1851
  allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
1579
1852
  transient: true,
1580
1853
  signal: nolanAutoAbort.signal,
1854
+ // Route this window's MCP elicitation prompts through the shared bridge.
1855
+ onMcpElicit: elicitations.onElicit,
1581
1856
  // Autopilot review rounds routinely span the injected EZ Coder run
1582
1857
  // (often >5 min) regardless of the user's global speedProfile pick.
1583
1858
  forceLongCacheRetention: true,
1584
1859
  });
1585
1860
  // Nolan is already the independent autopilot reviewer; recursively running
1586
1861
  // his own Ideal self-review adds latency and can corrupt the verdict shape.
1587
- ken.setIdealReviewSuppressed(true);
1588
- await ken.initialize();
1862
+ nolanAgent.setIdealReviewSuppressed(true);
1863
+ await nolanAgent.initialize();
1589
1864
  // Deliberately no bus bridge: the review is silent. Errors surface via the
1590
1865
  // runAutopilotReview try/catch as autopilot_error frames.
1591
- nolanAutoSession = ken;
1592
- log("INFO", "app-sidecar", "ken autopilot session ready", {
1866
+ nolanAutoSession = nolanAgent;
1867
+ log("INFO", "app-sidecar", "nolan autopilot session ready", {
1593
1868
  provider: target.provider,
1594
1869
  model: target.model,
1595
1870
  });
1596
- return ken;
1871
+ return nolanAgent;
1597
1872
  }
1598
1873
  function abortOwnedWork() {
1599
1874
  cancelGeneration++;
1600
1875
  abort.abort();
1876
+ // An MCP tool call parked on user input is not cancelled by the signal —
1877
+ // the promise lives in the bridge. Release it, or the aborted turn's tool
1878
+ // call never returns.
1879
+ elicitations.cancelAll();
1601
1880
  // Stop a run-all sweep and every async child through AgentSession's signal.
1602
1881
  taskRunAll = false;
1603
1882
  autopilotCancelled = true;
@@ -1609,9 +1888,9 @@ async function createSession(deps, opts) {
1609
1888
  nolanAutoAbort = new AbortController();
1610
1889
  nolanAutoSession?.setSignal(nolanAutoAbort.signal);
1611
1890
  }
1612
- function finishOwnedGeneration(generation, emitCancelledFallback) {
1891
+ function finishOwnedGeneration(generation, emitCancelledFallback, outcome = "completed") {
1613
1892
  const cancelled = runLifecycle.isCancellationRequested(generation);
1614
- const settlement = runLifecycle.settle(generation);
1893
+ const settlement = runLifecycle.settle(generation, outcome);
1615
1894
  if (!settlement.settled)
1616
1895
  return cancelled;
1617
1896
  // A replacement signal is safe only after the provider-backed owner settled.
@@ -1664,6 +1943,9 @@ async function createSession(deps, opts) {
1664
1943
  isGitRepo(cwd).catch(() => gitIsRepo),
1665
1944
  getGitDirtyFileCount(cwd).catch(() => gitDirtyFileCount),
1666
1945
  ]);
1946
+ // A run may have opened/closed issues or PRs — refresh fire-and-forget so
1947
+ // teardown isn't delayed by the network. Broadcasts itself on change.
1948
+ void refreshGitHubCounts();
1667
1949
  // Serialize behind any marker/tool-triggered refresh so the terminal
1668
1950
  // progress snapshot uses the live plan file. Once every canonical step
1669
1951
  // is complete, remove the approved plan from future system prompts and
@@ -1686,8 +1968,9 @@ async function createSession(deps, opts) {
1686
1968
  });
1687
1969
  }
1688
1970
  }
1689
- if (ownsGeneration)
1690
- finishOwnedGeneration(generation, false);
1971
+ if (ownsGeneration) {
1972
+ finishOwnedGeneration(generation, false, runSucceeded ? "completed" : "failed");
1973
+ }
1691
1974
  // A cancelled injected run is still owned by the surrounding autopilot
1692
1975
  // cycle; its outer finalizer emits the one terminal cancelled run_end.
1693
1976
  if (!(cancelled && !ownsGeneration)) {
@@ -1706,7 +1989,10 @@ async function createSession(deps, opts) {
1706
1989
  // completed ones so they drop out of the Tasks modal automatically (users
1707
1990
  // never have to delete finished tasks by hand).
1708
1991
  broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
1709
- broadcast("queued", { count: session.getQueuedCount() });
1992
+ broadcast("queued", {
1993
+ count: session.getQueuedCount(),
1994
+ messages: session.listQueuedMessages(),
1995
+ });
1710
1996
  broadcast("extras", footerExtras());
1711
1997
  }
1712
1998
  }
@@ -1720,7 +2006,7 @@ async function createSession(deps, opts) {
1720
2006
  autopilotReviewing = true;
1721
2007
  broadcast("autopilot_review_start", {});
1722
2008
  try {
1723
- const ken = await ensureNolanAutoSession();
2009
+ const nolanAgent = await ensureNolanAutoSession();
1724
2010
  const digest = buildNolanAutopilotContext({
1725
2011
  cwd,
1726
2012
  gitBranch,
@@ -1729,8 +2015,8 @@ async function createSession(deps, opts) {
1729
2015
  injectedPrompts: [...injectedAutopilotPrompts],
1730
2016
  workflowCommands: await loadWorkflowCommandSpecs(),
1731
2017
  });
1732
- await ken.prompt(digest);
1733
- return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
2018
+ await nolanAgent.prompt(digest);
2019
+ return parseAutopilotVerdict(lastAssistantText(nolanAgent.getMessages()));
1734
2020
  }
1735
2021
  catch (err) {
1736
2022
  if (!autopilotCancelled)
@@ -1760,7 +2046,7 @@ async function createSession(deps, opts) {
1760
2046
  autopilotReviewing = true;
1761
2047
  broadcast("autopilot_review_start", {});
1762
2048
  try {
1763
- const ken = await ensureNolanAutoSession();
2049
+ const nolanAgent = await ensureNolanAutoSession();
1764
2050
  // Re-read the plan file (the run may have revised it in place); fall
1765
2051
  // back to the content captured at exit_plan time.
1766
2052
  const planContent = await fs.readFile(planPath, "utf-8").catch(() => pendingPlanContent);
@@ -1773,10 +2059,10 @@ async function createSession(deps, opts) {
1773
2059
  workflowCommands: await loadWorkflowCommandSpecs(),
1774
2060
  planContent,
1775
2061
  });
1776
- await ken.prompt(digest);
2062
+ await nolanAgent.prompt(digest);
1777
2063
  if (autopilotCancelled || planGeneration !== genAtStart)
1778
2064
  return null;
1779
- return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
2065
+ return parseAutopilotVerdict(lastAssistantText(nolanAgent.getMessages()));
1780
2066
  }
1781
2067
  catch (err) {
1782
2068
  // User action mid-review (manual Accept aborts the nolanAuto run): drop
@@ -1802,7 +2088,7 @@ async function createSession(deps, opts) {
1802
2088
  const IMPLEMENT_PLAN_PROMPT = "The plan has been approved. Implement it now, following each step in order.";
1803
2089
  // Drive the review→prompt→review loop for one finished user turn. Only ever
1804
2090
  // called after shouldStartAutopilotCycle approves the turn (POST /prompt or
1805
- // the stranded-queue drain) — never from the task runner, resume, /ken, or
2091
+ // the stranded-queue drain) — never from the task runner, resume, /nolan, or
1806
2092
  // error paths, so there's no recursion and no guard tangle. The loop's
1807
2093
  // control flow lives in driveAutopilotCycle (core/autopilot-cycle.ts) so
1808
2094
  // every exit path is unit-tested; this only wires the real dependencies.
@@ -1866,7 +2152,7 @@ async function createSession(deps, opts) {
1866
2152
  // label stays the clean prompt.
1867
2153
  const framed = frameAutopilotInjection(IMPLEMENT_PLAN_PROMPT);
1868
2154
  injectedAutopilotPrompts.push(framed);
1869
- return runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(framed));
2155
+ return runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(framed, AUTOMATION_PROVENANCE));
1870
2156
  },
1871
2157
  // Lean context per user turn: wipe prior review history so each new
1872
2158
  // turn starts cheap, while within this cycle the few review messages
@@ -1895,19 +2181,21 @@ async function createSession(deps, opts) {
1895
2181
  },
1896
2182
  // Autopilot-injected run: EZ Coder receives the framed prompt (no human
1897
2183
  // is watching this turn) while run_start keeps the clean label.
1898
- runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body))),
2184
+ runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body), AUTOMATION_PROVENANCE)),
1899
2185
  emit: (event) => {
1900
2186
  // Persist the terminal verdict marker so a resumed session renders the
1901
2187
  // same Nolan bubble the live run showed instead of dropping it or
1902
2188
  // falling back to the raw verdict text (e.g. ALL_CLEAR).
1903
2189
  if (event.type === "autopilot_done") {
1904
2190
  // Broadcast the SAME copySeed the persisted marker will produce on
1905
- // resume, so the live all-clear wording matches the resumed one
1906
- // (computed before persist — same synchronous message count).
2191
+ // resume, so the live all-clear wording matches the resumed one.
2192
+ // Must use the PERSISTED count — that's what persistAutopilotMarker
2193
+ // anchors against, and it trails the in-memory list after a run
2194
+ // whose messages never made it to disk.
1907
2195
  const seed = autopilotMarkerCopySeed({
1908
2196
  version: 1,
1909
2197
  phase: "done",
1910
- afterMessageCount: session.getMessages().filter((m) => m.role !== "system").length,
2198
+ afterMessageCount: session.getPersistedTranscriptCount(),
1911
2199
  });
1912
2200
  broadcast(event.type, { ...event.data, copySeed: seed });
1913
2201
  void session.persistAutopilotMarker("done");
@@ -1952,7 +2240,10 @@ async function createSession(deps, opts) {
1952
2240
  const next = session.takeNextQueuedMessage();
1953
2241
  if (!next)
1954
2242
  return;
1955
- broadcast("queued", { count: session.getQueuedCount() });
2243
+ broadcast("queued", {
2244
+ count: session.getQueuedCount(),
2245
+ messages: session.listQueuedMessages(),
2246
+ });
1956
2247
  if (!next.text.trim() && next.attachments.length === 0)
1957
2248
  continue;
1958
2249
  // A queued message draining as a fresh turn supersedes any pending
@@ -2038,7 +2329,7 @@ async function createSession(deps, opts) {
2038
2329
  const shortId = task.id.slice(0, 8);
2039
2330
  const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2040
2331
  `tasks({ action: "done", id: "${shortId}" })`;
2041
- await runAgent(task.title, () => session.prompt(task.prompt + completionHint));
2332
+ await runAgent(task.title, () => session.prompt(task.prompt + completionHint, AUTOMATION_PROVENANCE));
2042
2333
  // The agent typically marks the task done via the tasks tool during the run;
2043
2334
  // prune completed tasks and push the refreshed list so the modal drops them.
2044
2335
  broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
@@ -2136,6 +2427,20 @@ async function createSession(deps, opts) {
2136
2427
  gitPoll.unref?.();
2137
2428
  };
2138
2429
  scheduleGitPoll(5000);
2430
+ // GitHub issue/PR counts change outside the app (web UI, teammates), so poll
2431
+ // on a slow cadence. Network-bound, so keep it well under the search API's
2432
+ // rate budget (2 calls per tick). No-op when the origin isn't a GitHub repo.
2433
+ let gitHubPoll;
2434
+ let gitHubPollStopped = false;
2435
+ const scheduleGitHubPoll = (delay) => {
2436
+ if (gitHubPollStopped)
2437
+ return;
2438
+ gitHubPoll = setTimeout(() => {
2439
+ void refreshGitHubCounts().finally(() => scheduleGitHubPoll(60_000));
2440
+ }, delay);
2441
+ gitHubPoll.unref?.();
2442
+ };
2443
+ scheduleGitHubPoll(2000);
2139
2444
  function readBody(req, res) {
2140
2445
  return readCappedBody(req, res);
2141
2446
  }
@@ -2257,6 +2562,45 @@ async function createSession(deps, opts) {
2257
2562
  });
2258
2563
  return;
2259
2564
  }
2565
+ if (method === "GET" && url === "/plugins") {
2566
+ void listInstalledPlugins(paths.extensionsDir)
2567
+ .then((plugins) => json(res, 200, { plugins }))
2568
+ .catch((error) => {
2569
+ captureSidecarError(error, "app-sidecar.plugins.list");
2570
+ json(res, 500, { error: error instanceof Error ? error.message : String(error) });
2571
+ });
2572
+ return;
2573
+ }
2574
+ if (method === "POST" && url === "/plugins/install") {
2575
+ void readBody(req, res).then(async (raw) => {
2576
+ if (raw === null)
2577
+ return;
2578
+ try {
2579
+ const bundlePath = JSON.parse(raw).bundlePath;
2580
+ if (typeof bundlePath !== "string" || !path.isAbsolute(bundlePath)) {
2581
+ json(res, 400, { error: "bundlePath must be an absolute path" });
2582
+ return;
2583
+ }
2584
+ const plugin = await installPlugin(bundlePath, paths.extensionsDir);
2585
+ json(res, 200, { plugin, restartRequired: true });
2586
+ }
2587
+ catch (error) {
2588
+ captureSidecarError(error, "app-sidecar.plugins.install");
2589
+ json(res, 400, { error: error instanceof Error ? error.message : String(error) });
2590
+ }
2591
+ });
2592
+ return;
2593
+ }
2594
+ if (method === "DELETE" && url.startsWith("/plugins/")) {
2595
+ const pluginId = decodeURIComponent(url.slice("/plugins/".length));
2596
+ void removePlugin(pluginId, paths.extensionsDir)
2597
+ .then(() => json(res, 200, { removed: pluginId, restartRequired: true }))
2598
+ .catch((error) => {
2599
+ captureSidecarError(error, "app-sidecar.plugins.remove");
2600
+ json(res, 400, { error: error instanceof Error ? error.message : String(error) });
2601
+ });
2602
+ return;
2603
+ }
2260
2604
  if (method === "GET" && url === "/settings") {
2261
2605
  // `configured` is true only when the user explicitly saved a projects root
2262
2606
  // (the ezcoder-app.json file exists with a value) — not when we fall back to the
@@ -2343,9 +2687,53 @@ async function createSession(deps, opts) {
2343
2687
  });
2344
2688
  return;
2345
2689
  }
2690
+ // Dismiss a project from the picker (or restore it with hidden:false). The
2691
+ // path is kept rather than the row: sessions in scratch dirs keep
2692
+ // re-surfacing, so the user's decision has to outlive any one scan.
2693
+ if (method === "POST" && url === "/projects/hidden") {
2694
+ void readBody(req, res).then(async (raw) => {
2695
+ if (raw === null)
2696
+ return;
2697
+ let body;
2698
+ try {
2699
+ body = JSON.parse(raw);
2700
+ }
2701
+ catch {
2702
+ json(res, 400, { error: "invalid JSON body" });
2703
+ return;
2704
+ }
2705
+ const target = body.path?.trim();
2706
+ if (!target) {
2707
+ json(res, 400, { error: "path is required" });
2708
+ return;
2709
+ }
2710
+ const key = path.resolve(target);
2711
+ try {
2712
+ const settings = await loadAppSettings();
2713
+ const current = new Set((settings.hiddenProjects ?? []).map((p) => path.resolve(p)));
2714
+ if (body.hidden === false)
2715
+ current.delete(key);
2716
+ else
2717
+ current.add(key);
2718
+ settings.hiddenProjects = current.size > 0 ? Array.from(current) : undefined;
2719
+ await saveAppSettings(settings);
2720
+ json(res, 200, { hidden: Array.from(current) });
2721
+ }
2722
+ catch (err) {
2723
+ captureSidecarError(err, "app-sidecar.projects.hidden");
2724
+ json(res, 500, { error: err instanceof Error ? err.message : String(err) });
2725
+ }
2726
+ });
2727
+ return;
2728
+ }
2346
2729
  if (method === "GET" && url === "/projects") {
2347
- // Scan ezcoder + Claude Code + Codex session stores for known projects.
2348
- void discoverProjects()
2730
+ // Session stores plus configured project folders, including unopened projects.
2731
+ void loadAppSettings()
2732
+ .then(({ projectsRoot, projectRoots, hiddenProjects }) => discoverProjects({
2733
+ projectsRoot,
2734
+ extraRoots: projectRoots,
2735
+ hiddenPaths: hiddenProjects,
2736
+ }))
2349
2737
  .then((projects) => json(res, 200, { projects }))
2350
2738
  .catch((err) => {
2351
2739
  captureSidecarError(err, "app-sidecar.projects.discover");
@@ -2386,6 +2774,30 @@ async function createSession(deps, opts) {
2386
2774
  });
2387
2775
  return;
2388
2776
  }
2777
+ // Markdown transcript export for the app's download button. Serialized
2778
+ // here rather than in the webview because the webview's transcript model
2779
+ // deliberately keeps tool activity in the LiveToolPanel — exporting from
2780
+ // there would hand the user a coding session with the coding missing.
2781
+ // `?name=1` asks for the suggested filename only (the save dialog needs it
2782
+ // before there is a path), so the markdown never crosses IPC twice.
2783
+ if (method === "GET" && (url === "/export" || url.startsWith("/export?"))) {
2784
+ const query = new URLSearchParams(url.slice(url.indexOf("?") + 1));
2785
+ const st = session.getState();
2786
+ const filename = defaultExportFilename(mode);
2787
+ if (query.get("name") === "1") {
2788
+ json(res, 200, { filename });
2789
+ return;
2790
+ }
2791
+ const markdown = sessionToMarkdown({
2792
+ mode,
2793
+ cwd,
2794
+ provider: st.provider,
2795
+ model: st.model,
2796
+ ...(st.sessionId ? { sessionId: st.sessionId } : {}),
2797
+ }, session.getMessages(), { toolDetail: parseToolDetail(query.get("tools")) });
2798
+ json(res, 200, { filename, markdown });
2799
+ return;
2800
+ }
2389
2801
  if (method === "GET" && url === "/history") {
2390
2802
  // Reconstruct the transcript from persisted messages so resume is 1:1 with
2391
2803
  // the live SSE stream. Walks ALL message types (not just user/assistant):
@@ -2431,7 +2843,7 @@ async function createSession(deps, opts) {
2431
2843
  // Nolan (mentor) turns to interleave: group by the non-system message count
2432
2844
  // they were recorded after, so each lands right after that message. A
2433
2845
  // turn becomes two wire rows: the `@Nolan` question (user) + Nolan's reply
2434
- // (assistant), both flagged `ken` so the webview tints them.
2846
+ // (assistant), both flagged `nolan` so the webview tints them.
2435
2847
  // Deduped; stale anchors are clamped to the last message (Nolan turns
2436
2848
  // carry real conversation, so they render at the end instead of
2437
2849
  // vanishing).
@@ -2447,16 +2859,16 @@ async function createSession(deps, opts) {
2447
2859
  return;
2448
2860
  nolanByCount.delete(count);
2449
2861
  for (const turn of turns) {
2450
- history.push({ role: "user", text: `@Nolan ${turn.question}`, ken: true });
2451
- history.push({ role: "assistant", text: turn.reply, ken: true });
2862
+ history.push({ role: "user", text: `@Nolan ${turn.question}`, nolan: true });
2863
+ history.push({ role: "assistant", text: turn.reply, nolan: true });
2452
2864
  }
2453
2865
  };
2454
2866
  // Autopilot verdict markers to interleave, same anchor scheme as Nolan
2455
2867
  // turns — each becomes a single assistant row the webview renders
2456
2868
  // exactly like the live `autopilot` item (never a raw verdict string).
2457
- // Compact/continuation rewrites can carry old markers whose original
2458
- // afterMessageCount is beyond the restored message list; dropping those
2459
- // prevents stale all-clear bubbles from bunching at the bottom on resume.
2869
+ // Normalization pulls anchors left over from an unrebased compaction
2870
+ // back to where the marker was actually written, so stale all-clear
2871
+ // bubbles no longer bunch at the bottom of a reopened session.
2460
2872
  const restoredMessageCount = messages.filter((m) => m.role !== "system").length;
2461
2873
  const autopilotByCount = new Map();
2462
2874
  for (const marker of normalizeAutopilotMarkersForHistory(session.getAutopilotMarkers(), restoredMessageCount)) {
@@ -2540,6 +2952,21 @@ async function createSession(deps, opts) {
2540
2952
  },
2541
2953
  });
2542
2954
  }
2955
+ else if (marker.kind === "interrupted_run") {
2956
+ // Rendered as an error row: the run's tools already changed the
2957
+ // repo, so the user needs to see it and decide what to do. We
2958
+ // never replay it — that would duplicate those changes.
2959
+ history.push({
2960
+ role: "assistant",
2961
+ text: "",
2962
+ error: {
2963
+ scope: "interrupted_run",
2964
+ headline: "A run was interrupted",
2965
+ message: "EZ Coder stopped mid-run, so this turn is incomplete. Any files its tools already changed are still on disk.",
2966
+ guidance: "Review the working tree, then re-send the request if you still want it.",
2967
+ },
2968
+ });
2969
+ }
2543
2970
  }
2544
2971
  };
2545
2972
  let nonSystemCount = 0;
@@ -2548,10 +2975,11 @@ async function createSession(deps, opts) {
2548
2975
  flushNolan(0);
2549
2976
  flushAutopilot(0);
2550
2977
  flushAppMarkers(0);
2551
- for (const msg of messages) {
2552
- if (msg.role === "system")
2553
- continue;
2554
- nonSystemCount++;
2978
+ await replayMessagesInOrder(messages, async (msg, count) => {
2979
+ nonSystemCount = count;
2980
+ const visibility = getHistoryMessageVisibility(msg);
2981
+ if (visibility === "hidden")
2982
+ return;
2555
2983
  if (msg.role === "tool") {
2556
2984
  // Tool result messages: check for ImageContent blocks (screenshots,
2557
2985
  // generated images) and emit a toolImages entry.
@@ -2595,20 +3023,39 @@ async function createSession(deps, opts) {
2595
3023
  });
2596
3024
  }
2597
3025
  }
2598
- continue;
3026
+ return;
2599
3027
  }
2600
3028
  // User or assistant message — text/hook/command/compacted extraction,
2601
3029
  // plus sub-agent group detection for assistant tool_calls.
2602
3030
  if (msg.role === "user") {
2603
3031
  // Rebuild the live bubble: strip the steering wrapper, drop
2604
3032
  // attachment/file notes the model saw but the bubble never showed.
2605
- const restored = restoreUserRow(msg.content);
3033
+ const restored = restoreUserRow(msg.content, msg.provenance);
2606
3034
  const text = restored.text;
2607
- const hook = detectHookKind(text);
2608
- const compacted = !hook && text.startsWith("[Previous conversation summary]");
2609
- const command = !hook && !compacted ? detectPromptCommand(text, commandCandidates) : null;
2610
- if (text.trim() || restored.images.length > 0) {
2611
- const hint = userHintByCount.get(nonSystemCount);
3035
+ const hook = msg.provenance ? null : detectHookKind(text);
3036
+ const compacted = visibility === "summary" ||
3037
+ (!msg.provenance && !hook && text.startsWith("[Previous conversation summary]"));
3038
+ const hint = userHintByCount.get(nonSystemCount);
3039
+ // The typed invocation persisted alongside the prompt is
3040
+ // authoritative. Reversing the expanded body only works while the
3041
+ // template is byte-identical, and templates drift (edited
3042
+ // `.ezcoder/commands/*.md`, reworded built-ins, app-vs-CLI phrasing) —
3043
+ // after which the resumed session dumped the raw multi-KB body
3044
+ // instead of the `/name` chip. Older sessions have no hint, so the
3045
+ // body match stays as the fallback.
3046
+ const command = !hook && !compacted
3047
+ ? resolveRestoredCommand(typeof hint?.command === "string" ? hint.command : null, text, commandCandidates)
3048
+ : null;
3049
+ // Autopilot injected this turn — live showed only the Nolan-tinted
3050
+ // marker for it, never a user bubble. Emitting one here would print
3051
+ // the injected instruction a second time, unstyled.
3052
+ //
3053
+ // Pushed background-status updates are skipped for the same reason:
3054
+ // the live run rendered no bubble for them, so showing them here
3055
+ // would fill a reopened session with machine-facing status lines
3056
+ // the user never saw while working.
3057
+ if ((!msg.provenance || (!restored.autopilotInjected && !restored.notification)) &&
3058
+ (text.trim() || restored.images.length > 0)) {
2612
3059
  history.push({
2613
3060
  role: "user",
2614
3061
  text: command ?? text,
@@ -2671,12 +3118,13 @@ async function createSession(deps, opts) {
2671
3118
  });
2672
3119
  }
2673
3120
  }
2674
- // Interleave any Nolan turns / autopilot / app markers recorded right
2675
- // after this message.
2676
- flushNolan(nonSystemCount);
2677
- flushAutopilot(nonSystemCount);
2678
- flushAppMarkers(nonSystemCount);
2679
- }
3121
+ }, (count) => {
3122
+ // Markers flush after every physical message, including tools and
3123
+ // provenance-hidden runtime context.
3124
+ flushNolan(count);
3125
+ flushAutopilot(count);
3126
+ flushAppMarkers(count);
3127
+ });
2680
3128
  // Flush remaining Nolan turns whose anchor is at/after the message count so
2681
3129
  // none are dropped. Autopilot/app markers beyond the restored message
2682
3130
  // count were already filtered above; any remaining marker here is valid.
@@ -2705,21 +3153,44 @@ async function createSession(deps, opts) {
2705
3153
  description: c.description,
2706
3154
  source: "built-in",
2707
3155
  }));
3156
+ // Desktop-safe registry actions belong in the same picker as workflows.
3157
+ // Most registry commands have dedicated app controls or TUI-only flows;
3158
+ // multi-root management has no other affordance, so expose only these.
3159
+ const workspaceActions = [
3160
+ {
3161
+ name: "add-dir",
3162
+ aliases: ["adddir"],
3163
+ description: "Add another project folder to this workspace",
3164
+ source: "built-in",
3165
+ },
3166
+ {
3167
+ name: "remove-dir",
3168
+ aliases: ["removedir"],
3169
+ description: "Remove an added project folder from this workspace",
3170
+ source: "built-in",
3171
+ },
3172
+ ];
2708
3173
  const custom = (await loadCustomCommands(cwd))
2709
- // A custom command can't shadow a built-in name.
2710
- .filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name))
3174
+ // A custom command can't shadow a built-in name or app action.
3175
+ .filter((c) => !PROMPT_COMMANDS.some((b) => b.name === c.name) &&
3176
+ !workspaceActions.some((action) => action.name === c.name))
2711
3177
  .map((c) => ({
2712
3178
  name: c.name,
2713
3179
  aliases: [],
2714
3180
  description: c.description,
2715
3181
  source: "custom",
2716
3182
  }));
2717
- json(res, 200, { commands: [...builtins, ...custom] });
3183
+ json(res, 200, { commands: [...workspaceActions, ...builtins, ...custom] });
2718
3184
  })();
2719
3185
  return;
2720
3186
  }
2721
3187
  if (method === "POST" && url === "/prompt") {
2722
- void readBody(req, res).then(async (raw) => {
3188
+ // Per-request: only the prompt that actually claimed the start may release
3189
+ // it. A bare release would let an early-returning request (bad JSON, or a
3190
+ // prompt that queued) clear a claim another request is still holding.
3191
+ let claimedStart = false;
3192
+ void readBody(req, res)
3193
+ .then(async (raw) => {
2723
3194
  if (raw === null)
2724
3195
  return;
2725
3196
  let text;
@@ -2739,14 +3210,17 @@ async function createSession(deps, opts) {
2739
3210
  json(res, 400, { error: "empty prompt" });
2740
3211
  return;
2741
3212
  }
2742
- if (runLifecycle.running && runLifecycle.isCancellationRequested(runLifecycle.generation)) {
3213
+ if (runLifecycle.running &&
3214
+ runLifecycle.isCancellationRequested(runLifecycle.generation)) {
2743
3215
  json(res, 409, {
2744
3216
  error: runLifecycle.state === "cancelling" ? "run_cancelling" : "cancel_failed",
2745
3217
  runState: runLifecycle.state,
2746
3218
  });
2747
3219
  return;
2748
3220
  }
2749
- if (running || autopilotActive) {
3221
+ // `runClaim` covers the gap before `runAgent` flips `running`: a
3222
+ // prompt arriving in that window must queue, not start a second run.
3223
+ if (running || runClaim.active || autopilotActive) {
2750
3224
  // Queue prompts as mid-run steering (mirrors the CLI). Also queue while
2751
3225
  // an autopilot cycle is active but between injected runs (build idle,
2752
3226
  // Nolan reviewing) so the message never starts a run that collides with
@@ -2755,20 +3229,47 @@ async function createSession(deps, opts) {
2755
3229
  // path as a non-queued attachment prompt when it drains.
2756
3230
  const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
2757
3231
  const count = session.queueMessage(text, prepared);
2758
- broadcast("queued", { count });
3232
+ broadcast("queued", { count, messages: session.listQueuedMessages() });
2759
3233
  json(res, 202, { queued: true, count });
2760
3234
  return;
2761
3235
  }
3236
+ // Claim the run NOW, synchronously. Everything below this line may
3237
+ // yield, and `running` does not flip until runAgent begins.
3238
+ claimedStart = runClaim.claim();
2762
3239
  json(res, 202, { accepted: true });
3240
+ // Gate inputs captured around the run: whether this turn is a workflow
3241
+ // slash command (attachment prompts skip slash expansion entirely), and
3242
+ // how many assistant messages the run actually adds. Computed even when
3243
+ // autopilot is currently off — the toggle can flip ON mid-run, and the
3244
+ // gate reads the post-run value.
3245
+ const workflowCommand = attachments.length === 0 &&
3246
+ isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
3247
+ // Does this input actually expand into a persisted user message? Asked
3248
+ // of the session itself, because only it knows whether the command
3249
+ // resolves here (name/alias casing, custom `.ezcoder/commands`, non-coder
3250
+ // agents that don't expand at all). A looser guess would anchor the
3251
+ // hint at +1 with no message to land on — decorating an unrelated
3252
+ // later bubble with the wrong `/name`.
3253
+ const expandsToTemplate = attachments.length === 0 && (await session.willExpandPromptTemplate(text));
2763
3254
  // Webview display hint for this prompt's user bubble (nolanSent shimmer
2764
- // label / enhancer highlight segments). Anchored +1 so it attaches to
2765
- // the user message the prompt below is about to push. Queued prompts
2766
- // skip this (their position in the run is unpredictable).
2767
- if (meta && (meta.nolanSent === true || Array.isArray(meta.enhancements))) {
3255
+ // label / enhancer highlight segments / the `/name` a command was typed
3256
+ // as). Anchored +1 so it attaches to the user message the prompt below
3257
+ // is about to push. Queued prompts skip this (their position in the run
3258
+ // is unpredictable).
3259
+ //
3260
+ // Recording the invocation matters because the agent persists the
3261
+ // EXPANDED template as the user message. Resume used to recover
3262
+ // `/name` by matching that body against the current templates, which
3263
+ // silently fails the moment a template is edited or reworded — the
3264
+ // reopened session then rendered the raw multi-KB prompt instead of
3265
+ // the command chip.
3266
+ if (expandsToTemplate ||
3267
+ (meta && (meta.nolanSent === true || Array.isArray(meta.enhancements)))) {
2768
3268
  void session
2769
3269
  .persistAppMarker("user_hint", {
2770
- ...(meta.nolanSent === true ? { nolanSent: true } : {}),
2771
- ...(Array.isArray(meta.enhancements) ? { enhancements: meta.enhancements } : {}),
3270
+ ...(expandsToTemplate ? { command: text.trim() } : {}),
3271
+ ...(meta?.nolanSent === true ? { nolanSent: true } : {}),
3272
+ ...(Array.isArray(meta?.enhancements) ? { enhancements: meta.enhancements } : {}),
2772
3273
  }, 1)
2773
3274
  .catch(() => { });
2774
3275
  }
@@ -2779,12 +3280,6 @@ async function createSession(deps, opts) {
2779
3280
  // feedback, anything) supersedes the pending plan — the bump also
2780
3281
  // invalidates any in-flight Nolan plan review.
2781
3282
  clearPendingPlan();
2782
- // Gate inputs captured around the run: whether this turn is a workflow
2783
- // slash command (attachment prompts skip slash expansion entirely), and
2784
- // how many assistant messages the run actually adds. Computed even when
2785
- // autopilot is currently off — the toggle can flip ON mid-run, and the
2786
- // gate reads the post-run value.
2787
- const workflowCommand = attachments.length === 0 && isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
2788
3283
  const assistantsBefore = countAssistantMessages(session.getMessages());
2789
3284
  const messagesBefore = session.getMessages().length;
2790
3285
  await runAgent(text, async () => {
@@ -2836,14 +3331,18 @@ async function createSession(deps, opts) {
2836
3331
  // A prompt sent while Nolan was reviewing (build idle) queued but had no
2837
3332
  // run to steer into — run it now as a fresh turn so it never strands.
2838
3333
  await runStrandedQueue();
3334
+ })
3335
+ .finally(() => {
3336
+ if (claimedStart)
3337
+ runClaim.release();
2839
3338
  });
2840
3339
  return;
2841
3340
  }
2842
3341
  // Nolan Grout (mentor): an independent read-only advisory run on the nolanSession.
2843
- // Runs concurrently with a build run — its events are ken_-prefixed so the
3342
+ // Runs concurrently with a build run — its events are nolan_-prefixed so the
2844
3343
  // webview keeps the bubbles separate. The context digest is assembled fresh
2845
3344
  // from the BUILD session's transcript each turn (one-way mirror).
2846
- if (method === "POST" && url === "/ken/prompt") {
3345
+ if (method === "POST" && url === "/nolan/prompt") {
2847
3346
  if (mode === "chat") {
2848
3347
  json(res, 404, { error: "Nolan is not available in EZ Chat." });
2849
3348
  return;
@@ -2871,18 +3370,18 @@ async function createSession(deps, opts) {
2871
3370
  nolanRunning = true;
2872
3371
  broadcast("nolan_run_start", { text });
2873
3372
  try {
2874
- const ken = await ensureNolanSession();
3373
+ const nolanAgent = await ensureNolanSession();
2875
3374
  const digest = await buildNolanContext(session, cwd, gitBranch, text, await loadWorkflowCommandSpecs(), injectedAutopilotPrompts);
2876
- await ken.prompt(digest);
3375
+ await nolanAgent.prompt(digest);
2877
3376
  // Record the turn against the BUILD session so it persists + survives
2878
3377
  // resume (advisory custom entry, never an LLM message). Reply is Nolan's
2879
3378
  // last assistant message; skip persistence if he produced nothing.
2880
- const reply = lastAssistantText(ken.getMessages());
3379
+ const reply = lastAssistantText(nolanAgent.getMessages());
2881
3380
  if (reply.trim())
2882
3381
  await session.persistNolanTurn(text, reply);
2883
3382
  }
2884
3383
  catch (err) {
2885
- broadcastError("nolan_error", "ken run failed", err);
3384
+ broadcastError("nolan_error", "nolan run failed", err);
2886
3385
  }
2887
3386
  finally {
2888
3387
  nolanRunning = false;
@@ -2895,7 +3394,7 @@ async function createSession(deps, opts) {
2895
3394
  });
2896
3395
  return;
2897
3396
  }
2898
- if (method === "POST" && url === "/ken/cancel") {
3397
+ if (method === "POST" && url === "/nolan/cancel") {
2899
3398
  nolanAbort.abort();
2900
3399
  nolanAbort = new AbortController();
2901
3400
  nolanSession?.setSignal(nolanAbort.signal);
@@ -3173,12 +3672,30 @@ async function createSession(deps, opts) {
3173
3672
  loggedIn.push(p);
3174
3673
  }
3175
3674
  // Just the names, grouped by provider in registry order — the UI shows
3176
- // a clean multi-column list of model ids.
3177
- const models = MODELS.filter((m) => loggedIn.includes(m.provider)).map((m) => ({
3178
- id: m.id,
3179
- name: m.name,
3180
- provider: m.provider,
3181
- }));
3675
+ // a clean multi-column list of model ids. Local models come from the
3676
+ // runtime registry (populated by the background scan) and are always
3677
+ // listed: their "login" is the endpoint answering a probe.
3678
+ const models = getAllModels()
3679
+ .filter((m) => m.provider === "local" || loggedIn.includes(m.provider))
3680
+ .map((m) => {
3681
+ if (m.provider !== "local") {
3682
+ return { id: m.id, name: m.name, provider: m.provider };
3683
+ }
3684
+ const probed = findProbedModel(localProbes, m.id);
3685
+ return {
3686
+ id: m.id,
3687
+ name: m.name,
3688
+ provider: m.provider,
3689
+ local: true,
3690
+ endpoint: probed?.endpoint.label ?? parseLocalModelId(m.id)?.endpointId,
3691
+ // A local model that can't call tools can't run the agent — the UI
3692
+ // renders it disabled rather than hiding it, so the user learns why.
3693
+ supportsTools: probed?.model.supportsTools ?? true,
3694
+ contextWindow: m.contextWindow,
3695
+ contextWindowKnown: probed?.model.contextWindowKnown ?? false,
3696
+ supportsThinking: m.supportsThinking,
3697
+ };
3698
+ });
3182
3699
  json(res, 200, { models });
3183
3700
  })();
3184
3701
  return;
@@ -3207,6 +3724,13 @@ async function createSession(deps, opts) {
3207
3724
  // Pick up any natively-written keys (desktop app) before resolving the
3208
3725
  // new model's credentials, so switching to a just-added provider works.
3209
3726
  await auth.reload();
3727
+ if (target.provider === "local") {
3728
+ const problem = await localModelBlocker(target.id);
3729
+ if (problem) {
3730
+ json(res, 409, { error: problem });
3731
+ return;
3732
+ }
3733
+ }
3210
3734
  await session.switchModel(target.provider, target.id);
3211
3735
  // Nolan follows EZ Coder's model only while un-pinned; a user-set Nolan
3212
3736
  // override survives EZ Coder model switches untouched.
@@ -3245,7 +3769,7 @@ async function createSession(deps, opts) {
3245
3769
  // no-op event (keeps footer/event tests from treating an EZ Coder
3246
3770
  // switch as a Nolan switch).
3247
3771
  if (!nolanModelOverride)
3248
- broadcast("ken_model_change", nolanStatePayload());
3772
+ broadcast("nolan_model_change", nolanStatePayload());
3249
3773
  // The new model usually has a different context window — push extras so
3250
3774
  // the footer's context meter rescales immediately.
3251
3775
  broadcast("extras", footerExtras());
@@ -3257,7 +3781,7 @@ async function createSession(deps, opts) {
3257
3781
  // { model: null } / "" to clear (Nolan resumes following EZ Coder). Applies
3258
3782
  // to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
3259
3783
  // either is mid-run defers via the pending-model mechanics.
3260
- if (method === "POST" && url === "/ken/model") {
3784
+ if (method === "POST" && url === "/nolan/model") {
3261
3785
  void readBody(req, res).then(async (raw) => {
3262
3786
  if (raw === null)
3263
3787
  return;
@@ -3292,17 +3816,49 @@ async function createSession(deps, opts) {
3292
3816
  await saveNolanModelPref(cwd, nolanModelOverride);
3293
3817
  await syncNolanModel(target.provider, target.id);
3294
3818
  await syncNolanAutoModel(target.provider, target.id);
3295
- log("INFO", "app-sidecar", "ken model pinned", {
3819
+ log("INFO", "app-sidecar", "nolan model pinned", {
3296
3820
  provider: target.provider,
3297
3821
  model: target.id,
3298
3822
  });
3299
3823
  }
3300
3824
  const payload = nolanStatePayload();
3301
- broadcast("ken_model_change", payload);
3825
+ broadcast("nolan_model_change", payload);
3302
3826
  json(res, 200, payload);
3303
3827
  });
3304
3828
  return;
3305
3829
  }
3830
+ // Pending queued steering, for the composer's cancel affordance.
3831
+ if (method === "GET" && url === "/queued") {
3832
+ json(res, 200, { queued: session.listQueuedMessages() });
3833
+ return;
3834
+ }
3835
+ // Cancel one pending queued message by id.
3836
+ if (method === "POST" && url === "/queued/cancel") {
3837
+ void readBody(req, res).then((raw) => {
3838
+ if (raw === null)
3839
+ return;
3840
+ let id;
3841
+ try {
3842
+ id = JSON.parse(raw).id ?? "";
3843
+ }
3844
+ catch {
3845
+ json(res, 400, { error: "invalid JSON body" });
3846
+ return;
3847
+ }
3848
+ if (!id.trim()) {
3849
+ json(res, 400, { error: "missing queued message id" });
3850
+ return;
3851
+ }
3852
+ // `false` means it already drained into the run between render and
3853
+ // click. That is a race, not an error, so report it as a normal result
3854
+ // and let the client reconcile from the fresh list.
3855
+ const cancelled = session.cancelQueuedMessage(id);
3856
+ const queued = session.listQueuedMessages();
3857
+ broadcast("queued", { count: queued.length, messages: queued });
3858
+ json(res, 200, { cancelled, queued });
3859
+ });
3860
+ return;
3861
+ }
3306
3862
  if (method === "POST" && url === "/kill") {
3307
3863
  void readBody(req, res).then(async (raw) => {
3308
3864
  if (raw === null)
@@ -3326,6 +3882,33 @@ async function createSession(deps, opts) {
3326
3882
  });
3327
3883
  return;
3328
3884
  }
3885
+ // Import a Claude Code / Codex / Cursor transcript into a resumable GG
3886
+ // Coder session. The importer never throws — it returns a typed failure so
3887
+ // the app can show the reason verbatim.
3888
+ if (method === "POST" && url === "/import-transcript") {
3889
+ void readBody(req, res).then(async (raw) => {
3890
+ if (raw === null)
3891
+ return;
3892
+ let body;
3893
+ try {
3894
+ body = JSON.parse(raw);
3895
+ }
3896
+ catch {
3897
+ json(res, 400, { error: "invalid JSON body" });
3898
+ return;
3899
+ }
3900
+ const filePath = body.path?.trim();
3901
+ if (!filePath) {
3902
+ json(res, 400, { error: "missing transcript path" });
3903
+ return;
3904
+ }
3905
+ const result = await session.importForeignTranscript(filePath, {
3906
+ ...(body.cwd ? { cwd: body.cwd } : {}),
3907
+ });
3908
+ json(res, result.ok ? 200 : 400, result);
3909
+ });
3910
+ return;
3911
+ }
3329
3912
  if (method === "POST" && url === "/thinking") {
3330
3913
  const st = session.getState();
3331
3914
  const next = getNextThinkingLevel(st.provider, st.model, session.getThinkingLevel());
@@ -3360,7 +3943,7 @@ async function createSession(deps, opts) {
3360
3943
  const generation = runLifecycle.generation;
3361
3944
  if (!pendingCancelDrain || pendingCancelDrain.generation !== generation) {
3362
3945
  pendingCancelDrain = { generation, text: session.drainQueue() };
3363
- broadcast("queued", { count: 0 });
3946
+ broadcast("queued", { count: 0, messages: [] });
3364
3947
  }
3365
3948
  const result = await runLifecycle.cancel(CANCEL_TIMEOUT_MS);
3366
3949
  const drained = pendingCancelDrain.text;
@@ -3520,7 +4103,17 @@ async function createSession(deps, opts) {
3520
4103
  ...(baseUrl ? { baseUrl } : {}),
3521
4104
  };
3522
4105
  await auth.setCredentials(storageKey, creds);
3523
- broadcast("auth_done", { provider });
4106
+ // auth.json is shared by every window, so this is a global change:
4107
+ // close their login modals and refresh their provider lists too.
4108
+ broadcastAll("auth_done", { provider });
4109
+ // `auth_done` means "a login succeeded" (modals close on it).
4110
+ // `auth_change` means "auth.json changed" — which a DISCONNECT also is,
4111
+ // so connection state has one signal that covers both directions.
4112
+ broadcastAll("auth_change", { provider });
4113
+ // A newly connected provider unlocks its models. `/models` filters on
4114
+ // who is logged in, so every window's picker is now stale — without
4115
+ // this the new models don't appear until the session is reopened.
4116
+ broadcastAll("models_change", {});
3524
4117
  json(res, 200, { ok: true });
3525
4118
  });
3526
4119
  return;
@@ -3546,7 +4139,18 @@ async function createSession(deps, opts) {
3546
4139
  json(res, 409, { error: "a login is already in progress" });
3547
4140
  return;
3548
4141
  }
4142
+ // A login writes the shared ~/.ezcoder/auth.json, so two windows racing the
4143
+ // same provider means two browser tabs and two token exchanges whose
4144
+ // writes clobber each other. The per-session flag above cannot see
4145
+ // that — guard the provider daemon-wide as well.
4146
+ if (oauthInFlightProviders.has(provider)) {
4147
+ json(res, 409, {
4148
+ error: `a ${meta.label} login is already in progress in another window`,
4149
+ });
4150
+ return;
4151
+ }
3549
4152
  oauthInFlight = true;
4153
+ oauthInFlightProviders.add(provider);
3550
4154
  json(res, 202, { accepted: true });
3551
4155
  void (async () => {
3552
4156
  const cb = authCallbacks();
@@ -3567,10 +4171,19 @@ async function createSession(deps, opts) {
3567
4171
  throw new Error(`OAuth not implemented for ${provider}`);
3568
4172
  }
3569
4173
  await auth.setCredentials(storageKey, creds);
3570
- broadcast("auth_done", { provider });
4174
+ // Terminal outcome of a GLOBAL change: every window's login modal
4175
+ // should close and its provider list refresh, not just the one
4176
+ // that started the flow.
4177
+ broadcastAll("auth_done", { provider });
4178
+ broadcastAll("auth_change", { provider });
4179
+ // The OAuth provider's models just became selectable everywhere.
4180
+ broadcastAll("models_change", {});
3571
4181
  }
3572
4182
  catch (err) {
3573
4183
  captureSidecarError(err, "app-sidecar.auth.oauth", { provider });
4184
+ // Deliberately session-scoped: this is the outcome of ONE window's
4185
+ // attempt. Another window that never pressed Connect has nothing to
4186
+ // show an error about, and its modal correctly still offers login.
3574
4187
  broadcast("auth_error", {
3575
4188
  provider,
3576
4189
  message: err instanceof Error ? err.message : String(err),
@@ -3578,6 +4191,7 @@ async function createSession(deps, opts) {
3578
4191
  }
3579
4192
  finally {
3580
4193
  oauthInFlight = false;
4194
+ oauthInFlightProviders.delete(provider);
3581
4195
  pendingCode = null;
3582
4196
  }
3583
4197
  })();
@@ -3606,6 +4220,39 @@ async function createSession(deps, opts) {
3606
4220
  });
3607
4221
  return;
3608
4222
  }
4223
+ if (method === "POST" && url.startsWith("/mcp/elicit/")) {
4224
+ const id = decodeURIComponent(url.slice("/mcp/elicit/".length));
4225
+ void readBody(req, res).then((raw) => {
4226
+ if (raw === null)
4227
+ return;
4228
+ let result;
4229
+ try {
4230
+ const parsed = JSON.parse(raw);
4231
+ if (parsed.action !== "accept" &&
4232
+ parsed.action !== "decline" &&
4233
+ parsed.action !== "cancel") {
4234
+ json(res, 400, { error: "action must be accept, decline, or cancel" });
4235
+ return;
4236
+ }
4237
+ result =
4238
+ parsed.action === "accept"
4239
+ ? { action: "accept", content: parsed.content ?? {} }
4240
+ : { action: parsed.action };
4241
+ }
4242
+ catch {
4243
+ json(res, 400, { error: "invalid JSON body" });
4244
+ return;
4245
+ }
4246
+ // Unknown id means it already timed out or was cancelled by an abort —
4247
+ // the tool call has moved on, so the answer has nowhere to go.
4248
+ if (!elicitations.settle(id, result)) {
4249
+ json(res, 409, { error: "no elicitation is awaiting a response" });
4250
+ return;
4251
+ }
4252
+ json(res, 200, { ok: true });
4253
+ });
4254
+ return;
4255
+ }
3609
4256
  if (method === "POST" && url === "/auth/logout") {
3610
4257
  void readBody(req, res).then(async (raw) => {
3611
4258
  if (raw === null)
@@ -3632,6 +4279,84 @@ async function createSession(deps, opts) {
3632
4279
  });
3633
4280
  return;
3634
4281
  }
4282
+ // ── Local models ──────────────────────────────────────
4283
+ // GET returns the last scan (cheap, no probing) so opening the modal is
4284
+ // instant; POST /local/scan is the explicit refresh.
4285
+ if (method === "GET" && url === "/local") {
4286
+ json(res, 200, localStatePayload());
4287
+ return;
4288
+ }
4289
+ if (method === "POST" && url === "/local/scan") {
4290
+ void scanLocalModels(true)
4291
+ .then(() => {
4292
+ broadcast("models_change", { local: localStatePayload() });
4293
+ json(res, 200, localStatePayload());
4294
+ })
4295
+ .catch((err) => {
4296
+ broadcastError("error", "local model scan failed", err);
4297
+ json(res, 500, { error: "Local model scan failed — see the sidecar log." });
4298
+ });
4299
+ return;
4300
+ }
4301
+ if (method === "POST" && url === "/local/endpoints") {
4302
+ void readBody(req, res).then(async (raw) => {
4303
+ if (raw === null)
4304
+ return;
4305
+ let body;
4306
+ try {
4307
+ body = JSON.parse(raw);
4308
+ }
4309
+ catch {
4310
+ json(res, 400, { error: "invalid JSON body" });
4311
+ return;
4312
+ }
4313
+ try {
4314
+ const endpoint = await addCustomEndpoint({
4315
+ baseUrl: body.baseUrl ?? "",
4316
+ ...(body.label ? { label: body.label } : {}),
4317
+ ...(body.apiKey ? { apiKey: body.apiKey } : {}),
4318
+ });
4319
+ await scanLocalModels(true);
4320
+ broadcast("models_change", { local: localStatePayload() });
4321
+ json(res, 200, {
4322
+ endpoint: { id: endpoint.id, label: endpoint.label },
4323
+ ...localStatePayload(),
4324
+ });
4325
+ }
4326
+ catch (err) {
4327
+ // Validation errors are the user's typo, not a system fault — 400 with
4328
+ // the exact reason, and nothing in the transcript.
4329
+ if (err instanceof LocalEndpointError) {
4330
+ json(res, 400, { error: err.message });
4331
+ return;
4332
+ }
4333
+ broadcastError("error", "add local endpoint failed", err);
4334
+ json(res, 500, { error: "Could not save the endpoint — see the sidecar log." });
4335
+ }
4336
+ });
4337
+ return;
4338
+ }
4339
+ if (method === "DELETE" && url.startsWith("/local/endpoints/")) {
4340
+ const id = decodeURIComponent(url.slice("/local/endpoints/".length));
4341
+ void (async () => {
4342
+ try {
4343
+ await removeCustomEndpoint(id);
4344
+ clearRuntimeModels((m) => m.provider === "local" && parseLocalModelId(m.id)?.endpointId === id);
4345
+ await scanLocalModels(true);
4346
+ broadcast("models_change", { local: localStatePayload() });
4347
+ json(res, 200, localStatePayload());
4348
+ }
4349
+ catch (err) {
4350
+ if (err instanceof LocalEndpointError) {
4351
+ json(res, 400, { error: err.message });
4352
+ return;
4353
+ }
4354
+ broadcastError("error", "remove local endpoint failed", err);
4355
+ json(res, 500, { error: "Could not remove the endpoint — see the sidecar log." });
4356
+ }
4357
+ })();
4358
+ return;
4359
+ }
3635
4360
  // ── Telegram config (mirrors `ezcoder telegram`) ─────────
3636
4361
  if (method === "GET" && url === "/telegram") {
3637
4362
  void loadTelegramConfig().then((cfg) => {
@@ -3913,12 +4638,16 @@ async function createSession(deps, opts) {
3913
4638
  json(res, 404, { error: "not found" });
3914
4639
  }
3915
4640
  async function dispose() {
4641
+ elicitations.cancelAll();
3916
4642
  tasksPollStopped = true;
3917
4643
  if (tasksPoll)
3918
4644
  clearTimeout(tasksPoll);
3919
4645
  gitPollStopped = true;
3920
4646
  if (gitPoll)
3921
4647
  clearTimeout(gitPoll);
4648
+ gitHubPollStopped = true;
4649
+ if (gitHubPoll)
4650
+ clearTimeout(gitHubPoll);
3922
4651
  // Stop the Telegram serve loop + dispose its per-chat sessions.
3923
4652
  if (serveController)
3924
4653
  await serveController.stop().catch(() => { });