@prestyj/cli 5.10.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 (455) hide show
  1. package/README.md +3 -4
  2. package/dist/app-sidecar-auth-broadcast.test.d.ts +2 -0
  3. package/dist/app-sidecar-auth-broadcast.test.d.ts.map +1 -0
  4. package/dist/app-sidecar-auth-broadcast.test.js +282 -0
  5. package/dist/app-sidecar-auth-broadcast.test.js.map +1 -0
  6. package/dist/app-sidecar-sessions.d.ts +5 -2
  7. package/dist/app-sidecar-sessions.d.ts.map +1 -1
  8. package/dist/app-sidecar-sessions.js +34 -8
  9. package/dist/app-sidecar-sessions.js.map +1 -1
  10. package/dist/app-sidecar-sessions.test.js +95 -0
  11. package/dist/app-sidecar-sessions.test.js.map +1 -1
  12. package/dist/app-sidecar.js +497 -128
  13. package/dist/app-sidecar.js.map +1 -1
  14. package/dist/cli/command-routing.d.ts +1 -1
  15. package/dist/cli/command-routing.d.ts.map +1 -1
  16. package/dist/cli/command-routing.js +1 -0
  17. package/dist/cli/command-routing.js.map +1 -1
  18. package/dist/cli/command-routing.test.js +1 -0
  19. package/dist/cli/command-routing.test.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +167 -47
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config.d.ts +2 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +10 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/config.test.js +10 -1
  28. package/dist/config.test.js.map +1 -1
  29. package/dist/core/agent-notifications.d.ts +62 -0
  30. package/dist/core/agent-notifications.d.ts.map +1 -0
  31. package/dist/core/agent-notifications.js +92 -0
  32. package/dist/core/agent-notifications.js.map +1 -0
  33. package/dist/core/agent-notifications.test.d.ts +2 -0
  34. package/dist/core/agent-notifications.test.d.ts.map +1 -0
  35. package/dist/core/agent-notifications.test.js +97 -0
  36. package/dist/core/agent-notifications.test.js.map +1 -0
  37. package/dist/core/agent-session-checkpoint-flush.test.d.ts +2 -0
  38. package/dist/core/agent-session-checkpoint-flush.test.d.ts.map +1 -0
  39. package/dist/core/agent-session-checkpoint-flush.test.js +201 -0
  40. package/dist/core/agent-session-checkpoint-flush.test.js.map +1 -0
  41. package/dist/core/agent-session-compaction.test.js +323 -6
  42. package/dist/core/agent-session-compaction.test.js.map +1 -1
  43. package/dist/core/agent-session-crash-durability.test.d.ts +2 -0
  44. package/dist/core/agent-session-crash-durability.test.d.ts.map +1 -0
  45. package/dist/core/agent-session-crash-durability.test.js +102 -0
  46. package/dist/core/agent-session-crash-durability.test.js.map +1 -0
  47. package/dist/core/agent-session-marker-anchors.test.js +181 -2
  48. package/dist/core/agent-session-marker-anchors.test.js.map +1 -1
  49. package/dist/core/agent-session-model-switch.test.d.ts +2 -0
  50. package/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  51. package/dist/core/agent-session-model-switch.test.js +161 -0
  52. package/dist/core/agent-session-model-switch.test.js.map +1 -0
  53. package/dist/core/agent-session-notifications.test.d.ts +2 -0
  54. package/dist/core/agent-session-notifications.test.d.ts.map +1 -0
  55. package/dist/core/agent-session-notifications.test.js +114 -0
  56. package/dist/core/agent-session-notifications.test.js.map +1 -0
  57. package/dist/core/agent-session-process-gate.test.d.ts +2 -0
  58. package/dist/core/agent-session-process-gate.test.d.ts.map +1 -0
  59. package/dist/core/agent-session-process-gate.test.js +92 -0
  60. package/dist/core/agent-session-process-gate.test.js.map +1 -0
  61. package/dist/core/agent-session-queue.test.js +65 -0
  62. package/dist/core/agent-session-queue.test.js.map +1 -1
  63. package/dist/core/agent-session-review-coverage.test.js +80 -23
  64. package/dist/core/agent-session-review-coverage.test.js.map +1 -1
  65. package/dist/core/agent-session-slash-restore.test.d.ts +2 -0
  66. package/dist/core/agent-session-slash-restore.test.d.ts.map +1 -0
  67. package/dist/core/agent-session-slash-restore.test.js +121 -0
  68. package/dist/core/agent-session-slash-restore.test.js.map +1 -0
  69. package/dist/core/agent-session.d.ts +214 -10
  70. package/dist/core/agent-session.d.ts.map +1 -1
  71. package/dist/core/agent-session.js +978 -212
  72. package/dist/core/agent-session.js.map +1 -1
  73. package/dist/core/compaction/active-context.test.js +21 -0
  74. package/dist/core/compaction/active-context.test.js.map +1 -1
  75. package/dist/core/compaction/compactor.d.ts +108 -8
  76. package/dist/core/compaction/compactor.d.ts.map +1 -1
  77. package/dist/core/compaction/compactor.js +423 -119
  78. package/dist/core/compaction/compactor.js.map +1 -1
  79. package/dist/core/compaction/compactor.test.js +360 -8
  80. package/dist/core/compaction/compactor.test.js.map +1 -1
  81. package/dist/core/compaction/policy.d.ts +17 -0
  82. package/dist/core/compaction/policy.d.ts.map +1 -0
  83. package/dist/core/compaction/policy.js +22 -0
  84. package/dist/core/compaction/policy.js.map +1 -0
  85. package/dist/core/compaction/policy.test.d.ts +2 -0
  86. package/dist/core/compaction/policy.test.d.ts.map +1 -0
  87. package/dist/core/compaction/policy.test.js +35 -0
  88. package/dist/core/compaction/policy.test.js.map +1 -0
  89. package/dist/core/compaction/query-aware-selector.d.ts +33 -0
  90. package/dist/core/compaction/query-aware-selector.d.ts.map +1 -0
  91. package/dist/core/compaction/query-aware-selector.js +202 -0
  92. package/dist/core/compaction/query-aware-selector.js.map +1 -0
  93. package/dist/core/compaction/query-aware-selector.test.d.ts +2 -0
  94. package/dist/core/compaction/query-aware-selector.test.d.ts.map +1 -0
  95. package/dist/core/compaction/query-aware-selector.test.js +96 -0
  96. package/dist/core/compaction/query-aware-selector.test.js.map +1 -0
  97. package/dist/core/compaction/tool-result-pruner.d.ts +24 -11
  98. package/dist/core/compaction/tool-result-pruner.d.ts.map +1 -1
  99. package/dist/core/compaction/tool-result-pruner.js +98 -26
  100. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  101. package/dist/core/compaction/tool-result-pruner.test.js +178 -6
  102. package/dist/core/compaction/tool-result-pruner.test.js.map +1 -1
  103. package/dist/core/event-bus.d.ts +23 -0
  104. package/dist/core/event-bus.d.ts.map +1 -1
  105. package/dist/core/event-bus.js +10 -0
  106. package/dist/core/event-bus.js.map +1 -1
  107. package/dist/core/extensions/loader.d.ts.map +1 -1
  108. package/dist/core/extensions/loader.js +28 -8
  109. package/dist/core/extensions/loader.js.map +1 -1
  110. package/dist/core/extensions/plugin-bundles.d.ts +23 -0
  111. package/dist/core/extensions/plugin-bundles.d.ts.map +1 -0
  112. package/dist/core/extensions/plugin-bundles.js +228 -0
  113. package/dist/core/extensions/plugin-bundles.js.map +1 -0
  114. package/dist/core/extensions/plugin-bundles.test.d.ts +2 -0
  115. package/dist/core/extensions/plugin-bundles.test.d.ts.map +1 -0
  116. package/dist/core/extensions/plugin-bundles.test.js +110 -0
  117. package/dist/core/extensions/plugin-bundles.test.js.map +1 -0
  118. package/dist/core/foreign-session-import.d.ts +120 -0
  119. package/dist/core/foreign-session-import.d.ts.map +1 -0
  120. package/dist/core/foreign-session-import.js +598 -0
  121. package/dist/core/foreign-session-import.js.map +1 -0
  122. package/dist/core/foreign-session-import.test.d.ts +2 -0
  123. package/dist/core/foreign-session-import.test.d.ts.map +1 -0
  124. package/dist/core/foreign-session-import.test.js +220 -0
  125. package/dist/core/foreign-session-import.test.js.map +1 -0
  126. package/dist/core/foreign-transcript-blocks.d.ts +51 -0
  127. package/dist/core/foreign-transcript-blocks.d.ts.map +1 -0
  128. package/dist/core/foreign-transcript-blocks.js +151 -0
  129. package/dist/core/foreign-transcript-blocks.js.map +1 -0
  130. package/dist/core/ideal-review.d.ts +20 -1
  131. package/dist/core/ideal-review.d.ts.map +1 -1
  132. package/dist/core/ideal-review.js +37 -2
  133. package/dist/core/ideal-review.js.map +1 -1
  134. package/dist/core/ideal-review.test.js +99 -3
  135. package/dist/core/ideal-review.test.js.map +1 -1
  136. package/dist/core/loop-breaker.d.ts.map +1 -1
  137. package/dist/core/loop-breaker.js +1 -0
  138. package/dist/core/loop-breaker.js.map +1 -1
  139. package/dist/core/lsp/client.d.ts +15 -0
  140. package/dist/core/lsp/client.d.ts.map +1 -1
  141. package/dist/core/lsp/client.js +39 -1
  142. package/dist/core/lsp/client.js.map +1 -1
  143. package/dist/core/lsp/manager.d.ts +47 -9
  144. package/dist/core/lsp/manager.d.ts.map +1 -1
  145. package/dist/core/lsp/manager.js +96 -69
  146. package/dist/core/lsp/manager.js.map +1 -1
  147. package/dist/core/lsp/manager.test.js +18 -0
  148. package/dist/core/lsp/manager.test.js.map +1 -1
  149. package/dist/core/lsp/pool.d.ts +113 -0
  150. package/dist/core/lsp/pool.d.ts.map +1 -0
  151. package/dist/core/lsp/pool.js +255 -0
  152. package/dist/core/lsp/pool.js.map +1 -0
  153. package/dist/core/lsp/pool.test.d.ts +2 -0
  154. package/dist/core/lsp/pool.test.d.ts.map +1 -0
  155. package/dist/core/lsp/pool.test.js +286 -0
  156. package/dist/core/lsp/pool.test.js.map +1 -0
  157. package/dist/core/lsp/servers.d.ts.map +1 -1
  158. package/dist/core/lsp/servers.js +43 -3
  159. package/dist/core/lsp/servers.js.map +1 -1
  160. package/dist/core/lsp/servers.test.d.ts +2 -0
  161. package/dist/core/lsp/servers.test.d.ts.map +1 -0
  162. package/dist/core/lsp/servers.test.js +67 -0
  163. package/dist/core/lsp/servers.test.js.map +1 -0
  164. package/dist/core/lsp/windows.test.js +2 -14
  165. package/dist/core/lsp/windows.test.js.map +1 -1
  166. package/dist/core/mcp/catalog-cache.d.ts +63 -0
  167. package/dist/core/mcp/catalog-cache.d.ts.map +1 -0
  168. package/dist/core/mcp/catalog-cache.js +163 -0
  169. package/dist/core/mcp/catalog-cache.js.map +1 -0
  170. package/dist/core/mcp/catalog-cache.test.d.ts +2 -0
  171. package/dist/core/mcp/catalog-cache.test.d.ts.map +1 -0
  172. package/dist/core/mcp/catalog-cache.test.js +99 -0
  173. package/dist/core/mcp/catalog-cache.test.js.map +1 -0
  174. package/dist/core/mcp/client-session-recovery.test.d.ts +2 -0
  175. package/dist/core/mcp/client-session-recovery.test.d.ts.map +1 -0
  176. package/dist/core/mcp/client-session-recovery.test.js +231 -0
  177. package/dist/core/mcp/client-session-recovery.test.js.map +1 -0
  178. package/dist/core/mcp/client.d.ts +158 -0
  179. package/dist/core/mcp/client.d.ts.map +1 -1
  180. package/dist/core/mcp/client.js +476 -35
  181. package/dist/core/mcp/client.js.map +1 -1
  182. package/dist/core/mcp/elicitation-bridge.d.ts +41 -0
  183. package/dist/core/mcp/elicitation-bridge.d.ts.map +1 -0
  184. package/dist/core/mcp/elicitation-bridge.js +52 -0
  185. package/dist/core/mcp/elicitation-bridge.js.map +1 -0
  186. package/dist/core/mcp/elicitation.test.d.ts +2 -0
  187. package/dist/core/mcp/elicitation.test.d.ts.map +1 -0
  188. package/dist/core/mcp/elicitation.test.js +158 -0
  189. package/dist/core/mcp/elicitation.test.js.map +1 -0
  190. package/dist/core/mcp/index.d.ts +5 -1
  191. package/dist/core/mcp/index.d.ts.map +1 -1
  192. package/dist/core/mcp/index.js +2 -0
  193. package/dist/core/mcp/index.js.map +1 -1
  194. package/dist/core/mcp/legacy-negotiation.test.d.ts +2 -0
  195. package/dist/core/mcp/legacy-negotiation.test.d.ts.map +1 -0
  196. package/dist/core/mcp/legacy-negotiation.test.js +80 -0
  197. package/dist/core/mcp/legacy-negotiation.test.js.map +1 -0
  198. package/dist/core/mcp/oauth-provider.d.ts +1 -2
  199. package/dist/core/mcp/oauth-provider.d.ts.map +1 -1
  200. package/dist/core/mcp/oauth-provider.js +6 -2
  201. package/dist/core/mcp/oauth-provider.js.map +1 -1
  202. package/dist/core/mcp/oauth-store.d.ts +1 -1
  203. package/dist/core/mcp/oauth-store.d.ts.map +1 -1
  204. package/dist/core/mcp/oauth-store.js.map +1 -1
  205. package/dist/core/mcp/session-elicitation-wiring.test.d.ts +2 -0
  206. package/dist/core/mcp/session-elicitation-wiring.test.d.ts.map +1 -0
  207. package/dist/core/mcp/session-elicitation-wiring.test.js +138 -0
  208. package/dist/core/mcp/session-elicitation-wiring.test.js.map +1 -0
  209. package/dist/core/mcp/shared-pool.d.ts +158 -0
  210. package/dist/core/mcp/shared-pool.d.ts.map +1 -0
  211. package/dist/core/mcp/shared-pool.js +235 -0
  212. package/dist/core/mcp/shared-pool.js.map +1 -0
  213. package/dist/core/mcp/shared-pool.test.d.ts +2 -0
  214. package/dist/core/mcp/shared-pool.test.d.ts.map +1 -0
  215. package/dist/core/mcp/shared-pool.test.js +425 -0
  216. package/dist/core/mcp/shared-pool.test.js.map +1 -0
  217. package/dist/core/mcp/types.d.ts +12 -0
  218. package/dist/core/mcp/types.d.ts.map +1 -1
  219. package/dist/core/mcp/when-connected.test.d.ts +2 -0
  220. package/dist/core/mcp/when-connected.test.d.ts.map +1 -0
  221. package/dist/core/mcp/when-connected.test.js +46 -0
  222. package/dist/core/mcp/when-connected.test.js.map +1 -0
  223. package/dist/core/nolan-context.d.ts.map +1 -1
  224. package/dist/core/nolan-context.js +8 -0
  225. package/dist/core/nolan-context.js.map +1 -1
  226. package/dist/core/nolan-context.test.js +40 -0
  227. package/dist/core/nolan-context.test.js.map +1 -1
  228. package/dist/core/nolan-prompt.js +3 -0
  229. package/dist/core/nolan-prompt.js.map +1 -1
  230. package/dist/core/nolan-prompt.test.js +5 -0
  231. package/dist/core/nolan-prompt.test.js.map +1 -1
  232. package/dist/core/persistent-shell.d.ts +3 -2
  233. package/dist/core/persistent-shell.d.ts.map +1 -1
  234. package/dist/core/persistent-shell.js +10 -3
  235. package/dist/core/persistent-shell.js.map +1 -1
  236. package/dist/core/process-gate.d.ts +45 -0
  237. package/dist/core/process-gate.d.ts.map +1 -0
  238. package/dist/core/process-gate.js +79 -0
  239. package/dist/core/process-gate.js.map +1 -0
  240. package/dist/core/process-gate.test.d.ts +2 -0
  241. package/dist/core/process-gate.test.d.ts.map +1 -0
  242. package/dist/core/process-gate.test.js +87 -0
  243. package/dist/core/process-gate.test.js.map +1 -0
  244. package/dist/core/process-manager-notifications.test.d.ts +2 -0
  245. package/dist/core/process-manager-notifications.test.d.ts.map +1 -0
  246. package/dist/core/process-manager-notifications.test.js +217 -0
  247. package/dist/core/process-manager-notifications.test.js.map +1 -0
  248. package/dist/core/process-manager.d.ts +50 -1
  249. package/dist/core/process-manager.d.ts.map +1 -1
  250. package/dist/core/process-manager.js +189 -2
  251. package/dist/core/process-manager.js.map +1 -1
  252. package/dist/core/project-discovery.d.ts +31 -6
  253. package/dist/core/project-discovery.d.ts.map +1 -1
  254. package/dist/core/project-discovery.js +287 -31
  255. package/dist/core/project-discovery.js.map +1 -1
  256. package/dist/core/project-discovery.test.js +124 -0
  257. package/dist/core/project-discovery.test.js.map +1 -1
  258. package/dist/core/regrounding.d.ts.map +1 -1
  259. package/dist/core/regrounding.js +1 -0
  260. package/dist/core/regrounding.js.map +1 -1
  261. package/dist/core/run-claim.d.ts +37 -0
  262. package/dist/core/run-claim.d.ts.map +1 -0
  263. package/dist/core/run-claim.js +46 -0
  264. package/dist/core/run-claim.js.map +1 -0
  265. package/dist/core/run-claim.test.d.ts +2 -0
  266. package/dist/core/run-claim.test.d.ts.map +1 -0
  267. package/dist/core/run-claim.test.js +107 -0
  268. package/dist/core/run-claim.test.js.map +1 -0
  269. package/dist/core/run-journal.test.d.ts +2 -0
  270. package/dist/core/run-journal.test.d.ts.map +1 -0
  271. package/dist/core/run-journal.test.js +195 -0
  272. package/dist/core/run-journal.test.js.map +1 -0
  273. package/dist/core/run-lifecycle.d.ts +22 -3
  274. package/dist/core/run-lifecycle.d.ts.map +1 -1
  275. package/dist/core/run-lifecycle.js +12 -3
  276. package/dist/core/run-lifecycle.js.map +1 -1
  277. package/dist/core/sandbox-domains.d.ts +14 -0
  278. package/dist/core/sandbox-domains.d.ts.map +1 -0
  279. package/dist/core/sandbox-domains.js +76 -0
  280. package/dist/core/sandbox-domains.js.map +1 -0
  281. package/dist/core/sandbox-feedback.d.ts +22 -0
  282. package/dist/core/sandbox-feedback.d.ts.map +1 -0
  283. package/dist/core/sandbox-feedback.js +69 -0
  284. package/dist/core/sandbox-feedback.js.map +1 -0
  285. package/dist/core/sandbox-feedback.test.d.ts +2 -0
  286. package/dist/core/sandbox-feedback.test.d.ts.map +1 -0
  287. package/dist/core/sandbox-feedback.test.js +32 -0
  288. package/dist/core/sandbox-feedback.test.js.map +1 -0
  289. package/dist/core/sandbox.d.ts +91 -0
  290. package/dist/core/sandbox.d.ts.map +1 -0
  291. package/dist/core/sandbox.js +254 -0
  292. package/dist/core/sandbox.js.map +1 -0
  293. package/dist/core/sandbox.test.d.ts +2 -0
  294. package/dist/core/sandbox.test.d.ts.map +1 -0
  295. package/dist/core/sandbox.test.js +157 -0
  296. package/dist/core/sandbox.test.js.map +1 -0
  297. package/dist/core/session-compaction.d.ts +6 -0
  298. package/dist/core/session-compaction.d.ts.map +1 -1
  299. package/dist/core/session-compaction.js +11 -1
  300. package/dist/core/session-compaction.js.map +1 -1
  301. package/dist/core/session-compaction.test.js +18 -1
  302. package/dist/core/session-compaction.test.js.map +1 -1
  303. package/dist/core/session-export.d.ts.map +1 -1
  304. package/dist/core/session-export.js +12 -4
  305. package/dist/core/session-export.js.map +1 -1
  306. package/dist/core/session-history.d.ts +95 -6
  307. package/dist/core/session-history.d.ts.map +1 -1
  308. package/dist/core/session-history.js +261 -17
  309. package/dist/core/session-history.js.map +1 -1
  310. package/dist/core/session-history.test.js +173 -2
  311. package/dist/core/session-history.test.js.map +1 -1
  312. package/dist/core/session-manager.d.ts +223 -6
  313. package/dist/core/session-manager.d.ts.map +1 -1
  314. package/dist/core/session-manager.js +515 -26
  315. package/dist/core/session-manager.js.map +1 -1
  316. package/dist/core/session-manager.test.js +147 -3
  317. package/dist/core/session-manager.test.js.map +1 -1
  318. package/dist/core/session-notification-visibility.test.d.ts +2 -0
  319. package/dist/core/session-notification-visibility.test.d.ts.map +1 -0
  320. package/dist/core/session-notification-visibility.test.js +113 -0
  321. package/dist/core/session-notification-visibility.test.js.map +1 -0
  322. package/dist/core/session-preview.d.ts +5 -4
  323. package/dist/core/session-preview.d.ts.map +1 -1
  324. package/dist/core/session-preview.js +12 -5
  325. package/dist/core/session-preview.js.map +1 -1
  326. package/dist/core/session-restore-display.test.js +25 -0
  327. package/dist/core/session-restore-display.test.js.map +1 -1
  328. package/dist/core/session-storage.d.ts.map +1 -1
  329. package/dist/core/session-storage.js +39 -3
  330. package/dist/core/session-storage.js.map +1 -1
  331. package/dist/core/session-storage.test.js +6 -1
  332. package/dist/core/session-storage.test.js.map +1 -1
  333. package/dist/core/settings-manager.d.ts +8 -2
  334. package/dist/core/settings-manager.d.ts.map +1 -1
  335. package/dist/core/settings-manager.js +26 -0
  336. package/dist/core/settings-manager.js.map +1 -1
  337. package/dist/core/steering.d.ts +12 -0
  338. package/dist/core/steering.d.ts.map +1 -1
  339. package/dist/core/steering.js +16 -0
  340. package/dist/core/steering.js.map +1 -1
  341. package/dist/core/subagent-manager.d.ts +13 -0
  342. package/dist/core/subagent-manager.d.ts.map +1 -1
  343. package/dist/core/subagent-manager.js +39 -1
  344. package/dist/core/subagent-manager.js.map +1 -1
  345. package/dist/core/subagent-manager.test.js +43 -7
  346. package/dist/core/subagent-manager.test.js.map +1 -1
  347. package/dist/core/verification-evidence.d.ts +17 -0
  348. package/dist/core/verification-evidence.d.ts.map +1 -0
  349. package/dist/core/verification-evidence.js +283 -0
  350. package/dist/core/verification-evidence.js.map +1 -0
  351. package/dist/core/verification-evidence.test.d.ts +2 -0
  352. package/dist/core/verification-evidence.test.d.ts.map +1 -0
  353. package/dist/core/verification-evidence.test.js +103 -0
  354. package/dist/core/verification-evidence.test.js.map +1 -0
  355. package/dist/interactive.d.ts.map +1 -1
  356. package/dist/interactive.js +17 -3
  357. package/dist/interactive.js.map +1 -1
  358. package/dist/modes/acp-mode.d.ts +122 -0
  359. package/dist/modes/acp-mode.d.ts.map +1 -0
  360. package/dist/modes/acp-mode.js +1366 -0
  361. package/dist/modes/acp-mode.js.map +1 -0
  362. package/dist/modes/acp-mode.test.d.ts +2 -0
  363. package/dist/modes/acp-mode.test.d.ts.map +1 -0
  364. package/dist/modes/acp-mode.test.js +1065 -0
  365. package/dist/modes/acp-mode.test.js.map +1 -0
  366. package/dist/modes/index.d.ts +1 -0
  367. package/dist/modes/index.d.ts.map +1 -1
  368. package/dist/modes/index.js +1 -0
  369. package/dist/modes/index.js.map +1 -1
  370. package/dist/modes/json-mode.d.ts +28 -0
  371. package/dist/modes/json-mode.d.ts.map +1 -1
  372. package/dist/modes/json-mode.js +37 -0
  373. package/dist/modes/json-mode.js.map +1 -1
  374. package/dist/modes/json-mode.test.d.ts +2 -0
  375. package/dist/modes/json-mode.test.d.ts.map +1 -0
  376. package/dist/modes/json-mode.test.js +84 -0
  377. package/dist/modes/json-mode.test.js.map +1 -0
  378. package/dist/modes/subagent-worker-mode.d.ts +3 -0
  379. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  380. package/dist/modes/subagent-worker-mode.js +91 -11
  381. package/dist/modes/subagent-worker-mode.js.map +1 -1
  382. package/dist/modes/subagent-worker-mode.test.d.ts +2 -0
  383. package/dist/modes/subagent-worker-mode.test.d.ts.map +1 -0
  384. package/dist/modes/subagent-worker-mode.test.js +52 -0
  385. package/dist/modes/subagent-worker-mode.test.js.map +1 -0
  386. package/dist/session.d.ts +29 -0
  387. package/dist/session.d.ts.map +1 -1
  388. package/dist/session.js +41 -0
  389. package/dist/session.js.map +1 -1
  390. package/dist/tools/bash.d.ts +2 -1
  391. package/dist/tools/bash.d.ts.map +1 -1
  392. package/dist/tools/bash.js +55 -16
  393. package/dist/tools/bash.js.map +1 -1
  394. package/dist/tools/goals.d.ts +2 -2
  395. package/dist/tools/grep.d.ts.map +1 -1
  396. package/dist/tools/grep.js +2 -1
  397. package/dist/tools/grep.js.map +1 -1
  398. package/dist/tools/index.d.ts +13 -2
  399. package/dist/tools/index.d.ts.map +1 -1
  400. package/dist/tools/index.js +20 -17
  401. package/dist/tools/index.js.map +1 -1
  402. package/dist/tools/index.test.d.ts +2 -0
  403. package/dist/tools/index.test.d.ts.map +1 -0
  404. package/dist/tools/index.test.js +55 -0
  405. package/dist/tools/index.test.js.map +1 -0
  406. package/dist/tools/read-only-bash.d.ts +3 -0
  407. package/dist/tools/read-only-bash.d.ts.map +1 -1
  408. package/dist/tools/read-only-bash.js +7 -9
  409. package/dist/tools/read-only-bash.js.map +1 -1
  410. package/dist/tools/subagent-control.d.ts.map +1 -1
  411. package/dist/tools/subagent-control.js +6 -2
  412. package/dist/tools/subagent-control.js.map +1 -1
  413. package/dist/tools/subagent-shared.d.ts +6 -0
  414. package/dist/tools/subagent-shared.d.ts.map +1 -1
  415. package/dist/tools/subagent-shared.js +6 -0
  416. package/dist/tools/subagent-shared.js.map +1 -1
  417. package/dist/tools/subagent.d.ts.map +1 -1
  418. package/dist/tools/subagent.js +43 -8
  419. package/dist/tools/subagent.js.map +1 -1
  420. package/dist/tools/subagent.test.js +65 -3
  421. package/dist/tools/subagent.test.js.map +1 -1
  422. package/dist/tools/task-output.d.ts.map +1 -1
  423. package/dist/tools/task-output.js +3 -1
  424. package/dist/tools/task-output.js.map +1 -1
  425. package/dist/tools/tool-search.d.ts +12 -1
  426. package/dist/tools/tool-search.d.ts.map +1 -1
  427. package/dist/tools/tool-search.js +22 -3
  428. package/dist/tools/tool-search.js.map +1 -1
  429. package/dist/tools/tool-search.test.d.ts +2 -0
  430. package/dist/tools/tool-search.test.d.ts.map +1 -0
  431. package/dist/tools/tool-search.test.js +69 -0
  432. package/dist/tools/tool-search.test.js.map +1 -0
  433. package/dist/tools/web-fetch.d.ts +1 -1
  434. package/dist/tools/web-fetch.d.ts.map +1 -1
  435. package/dist/tools/web-fetch.js +5 -2
  436. package/dist/tools/web-fetch.js.map +1 -1
  437. package/dist/types.d.ts +7 -0
  438. package/dist/types.d.ts.map +1 -1
  439. package/dist/ui/App.d.ts.map +1 -1
  440. package/dist/ui/App.js +27 -1
  441. package/dist/ui/App.js.map +1 -1
  442. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  443. package/dist/ui/hooks/useAgentLoop.js +18 -4
  444. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  445. package/dist/utils/github.test.js +3 -3
  446. package/dist/utils/github.test.js.map +1 -1
  447. package/dist/utils/image.d.ts +26 -3
  448. package/dist/utils/image.d.ts.map +1 -1
  449. package/dist/utils/image.js +83 -14
  450. package/dist/utils/image.js.map +1 -1
  451. package/dist/utils/image.test.d.ts +2 -0
  452. package/dist/utils/image.test.d.ts.map +1 -0
  453. package/dist/utils/image.test.js +139 -0
  454. package/dist/utils/image.test.js.map +1 -0
  455. package/package.json +7 -6
@@ -24,6 +24,7 @@ import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
24
24
  import { setStreamDiagnostic } from "@prestyj/agent";
25
25
  import { AgentSession } from "./core/agent-session.js";
26
26
  import { RunLifecycle } from "./core/run-lifecycle.js";
27
+ import { RunClaim } from "./core/run-claim.js";
27
28
  import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
28
29
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
29
30
  import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
@@ -33,7 +34,7 @@ import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
33
34
  import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
34
35
  import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
35
36
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
36
- 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";
37
38
  import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
38
39
  import { AuthStorage } from "./core/auth-storage.js";
39
40
  import { cleanupToolOutputs } from "./tools/overflow.js";
@@ -47,6 +48,7 @@ import { loginKimi } from "./core/oauth/kimi.js";
47
48
  import { AUTH_PROVIDERS } from "./core/auth-providers.js";
48
49
  import { ensureAppDirs, loadSavedSettings } from "./config.js";
49
50
  import { SettingsManager } from "./core/settings-manager.js";
51
+ import { installPlugin, listInstalledPlugins, removePlugin, } from "./core/extensions/plugin-bundles.js";
50
52
  import { getModel, getDefaultThinkingLevel, getContextWindow, getAllModels, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
51
53
  import { resolveStartOrFallback } from "./core/resolve-start.js";
52
54
  import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
@@ -64,13 +66,18 @@ import { enrichProcessPath } from "./core/shell-path.js";
64
66
  import { downscaleForPreview, shrinkToFit, validateVisionImage } from "./utils/image.js";
65
67
  import { startServeMode } from "./modes/serve-mode.js";
66
68
  import { loadTelegramConfig, saveTelegramConfig, verifyBotToken } from "./core/telegram-config.js";
67
- 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";
68
70
  import { buildSnapshot, levelForXp, rankForLevel } from "./core/progress/ranks.js";
69
71
  import { loadProgress, peekProgress, updateProgress } from "./core/progress/store.js";
70
72
  import { awardPrompt, awardCommits } from "./core/progress/engine.js";
71
73
  import { detectNewCommits, repoKey } from "./core/progress/git-xp.js";
72
74
  import { rebuildFromSessions } from "./core/progress/rebuild.js";
73
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
+ };
74
81
  const ALL_PROVIDERS = [
75
82
  // US
76
83
  "anthropic",
@@ -87,6 +94,12 @@ const ALL_PROVIDERS = [
87
94
  "sakana",
88
95
  "openrouter",
89
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
+ }
90
103
  function appSettingsFile() {
91
104
  return path.join(os.homedir(), ".ezcoder", "ezcoder-app.json");
92
105
  }
@@ -110,6 +123,8 @@ async function loadAppSettings() {
110
123
  projectModels: raw.projectModels && typeof raw.projectModels === "object" ? raw.projectModels : undefined,
111
124
  autopilot: raw.autopilot && typeof raw.autopilot === "object" ? raw.autopilot : undefined,
112
125
  nolanModels: raw.nolanModels && typeof raw.nolanModels === "object" ? raw.nolanModels : undefined,
126
+ projectRoots: stringArray(raw.projectRoots),
127
+ hiddenProjects: stringArray(raw.hiddenProjects),
113
128
  };
114
129
  }
115
130
  catch {
@@ -363,30 +378,6 @@ function detectHookKind(text) {
363
378
  return "regrounding";
364
379
  return null;
365
380
  }
366
- // Separator AgentSession.prompt() inserts between a command's prompt body and
367
- // the user's trailing args. Must stay in sync with the expansion there.
368
- const COMMAND_ARGS_SEP = "\n\n## User Instructions\n\n";
369
- /**
370
- * Reverse a prompt-template command's expansion. When a `/name` command runs,
371
- * the agent persists the FULL expanded prompt body as the user message — so on
372
- * resume the raw body would render instead of the short `/name` chip the user
373
- * saw live. Given the candidate commands (built-in + custom) and a restored
374
- * message body, recover the original `/name [args]` invocation. Returns null
375
- * when the text isn't a known command body (an ordinary user message).
376
- */
377
- function detectPromptCommand(text, candidates) {
378
- for (const c of candidates) {
379
- if (!c.prompt)
380
- continue;
381
- if (text === c.prompt)
382
- return `/${c.name}`;
383
- if (text.startsWith(c.prompt + COMMAND_ARGS_SEP)) {
384
- const args = text.slice(c.prompt.length + COMMAND_ARGS_SEP.length).trim();
385
- return args ? `/${c.name} ${args}` : `/${c.name}`;
386
- }
387
- }
388
- return null;
389
- }
390
381
  /** A short transport summary for display (URL for http/sse, command+args for stdio). */
391
382
  function mcpRowSummary(config) {
392
383
  if (config.url)
@@ -598,6 +589,20 @@ async function main() {
598
589
  // request to its window's session via the `x-gg-session` header (and the
599
590
  // `?session=` query for the SSE /events stream).
600
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();
601
606
  const memoryStore = new MemoryStore({
602
607
  onChange: ({ memories }) => {
603
608
  for (const ctx of sessions.values()) {
@@ -621,6 +626,13 @@ async function main() {
621
626
  });
622
627
  const usageCache = new Map();
623
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;
624
636
  // 429 backoff: quota endpoints are auxiliary UI data. Honor Retry-After when
625
637
  // provided; otherwise retain the unavailable snapshot for 30 minutes. Clamp
626
638
  // the provider value so a malformed header can neither hammer the endpoint
@@ -635,6 +647,9 @@ async function main() {
635
647
  // Moonshot platform API key is metered per-token, not per plan window.
636
648
  const authKey = provider === "moonshot" ? MOONSHOT_OAUTH_KEY : provider;
637
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);
638
653
  return { provider, displayName, connected: false, windows: [], fetchedAt: Date.now() };
639
654
  }
640
655
  try {
@@ -678,6 +693,18 @@ async function main() {
678
693
  ...(backoffMs !== undefined && { backoffMs: String(backoffMs) }),
679
694
  });
680
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
+ }
681
708
  return {
682
709
  provider,
683
710
  displayName,
@@ -699,6 +726,11 @@ async function main() {
699
726
  usageRequests.set(provider, request);
700
727
  try {
701
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
+ }
702
734
  // Anthropic can return utilization before it assigns reset timestamps
703
735
  // (notably before the account's first active request). Retry that partial
704
736
  // snapshot quickly; complete snapshots keep the normal one-minute cache.
@@ -766,7 +798,15 @@ async function main() {
766
798
  const sessionPath = typeof body.sessionPath === "string" && body.sessionPath ? body.sessionPath : undefined;
767
799
  const id = randomUUID();
768
800
  try {
769
- 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 });
770
810
  sessions.set(id, ctx);
771
811
  log("INFO", "app-sidecar", "session created", {
772
812
  id,
@@ -1042,7 +1082,7 @@ async function createProgressManager(agentDir, broadcastAll) {
1042
1082
  * logger, PATH, shared auth file, and radio live at the daemon level.
1043
1083
  */
1044
1084
  async function createSession(deps, opts) {
1045
- const { auth, progress, memoryStore, jiwaStore } = deps;
1085
+ const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders } = deps;
1046
1086
  const paths = deps.paths;
1047
1087
  const mode = opts.mode;
1048
1088
  let chatAgent = opts.chatAgent;
@@ -1178,6 +1218,17 @@ async function createSession(deps, opts) {
1178
1218
  })
1179
1219
  .catch(() => { });
1180
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
+ });
1181
1232
  // The session file path to resume (passed by the daemon's POST /session);
1182
1233
  // empty/unset starts a fresh session.
1183
1234
  const resumeSessionPath = opts.sessionPath;
@@ -1191,6 +1242,7 @@ async function createSession(deps, opts) {
1191
1242
  signal: abort.signal,
1192
1243
  // Keep MCP startup off the readiness path in both modes.
1193
1244
  backgroundMcpConnect: true,
1245
+ onMcpElicit: elicitations.onElicit,
1194
1246
  // Keep restore-time auto-compaction off the readiness path too: its summary
1195
1247
  // LLM call (30s timeout) used to freeze waitForReady — and with it the whole
1196
1248
  // window (project picker, session list) — whenever a resumed session was
@@ -1513,6 +1565,10 @@ async function createSession(deps, opts) {
1513
1565
  recordApprovedPlanMarkers(d.text);
1514
1566
  });
1515
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() }));
1516
1572
  session.eventBus.on("tool_call_start", (d) => {
1517
1573
  toolCallNames.set(d.toolCallId, d.name);
1518
1574
  broadcast("tool_call_start", d);
@@ -1564,10 +1620,29 @@ async function createSession(deps, opts) {
1564
1620
  session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
1565
1621
  session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
1566
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();
1567
1627
  const runLifecycle = new RunLifecycle((runState) => {
1568
1628
  running = runState !== "idle";
1569
1629
  if (runState === "cancelling")
1570
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
+ }),
1571
1646
  });
1572
1647
  const cancelledRunEndGenerations = new Set();
1573
1648
  let pendingCancelDrain = null;
@@ -1643,7 +1718,7 @@ async function createSession(deps, opts) {
1643
1718
  // with `@Nolan …`; he reads EZ Coder's transcript (one-way — EZ Coder never sees
1644
1719
  // Nolan's) and hands back runnable prompts + mentorship. Created lazily on the
1645
1720
  // first `@Nolan` so windows that never use Nolan pay zero cost. His events ride the
1646
- // 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.
1647
1722
  let nolanSession = null;
1648
1723
  let nolanAbort = new AbortController();
1649
1724
  let nolanRunning = false;
@@ -1673,7 +1748,7 @@ async function createSession(deps, opts) {
1673
1748
  return { provider: st.provider, model: st.model };
1674
1749
  }
1675
1750
  /** Footer payload: Nolan's effective model + whether it's a pin. Merged into
1676
- * /state, the SSE ready frame, and every ken_model_change broadcast. */
1751
+ * /state, the SSE ready frame, and every nolan_model_change broadcast. */
1677
1752
  function nolanStatePayload() {
1678
1753
  const st = session.getState();
1679
1754
  return effectiveNolanModel(nolanModelOverride, { provider: st.provider, model: st.model });
@@ -1689,13 +1764,13 @@ async function createSession(deps, opts) {
1689
1764
  if (st.provider === provider && st.model === model)
1690
1765
  return;
1691
1766
  await nolanSession.switchModel(provider, model);
1692
- log("INFO", "app-sidecar", "ken session model synced", { provider, model });
1767
+ log("INFO", "app-sidecar", "nolan session model synced", { provider, model });
1693
1768
  }
1694
1769
  async function ensureNolanSession() {
1695
1770
  if (nolanSession)
1696
1771
  return nolanSession;
1697
1772
  const target = nolanCurrentModel();
1698
- const ken = new AgentSession({
1773
+ const nolanAgent = new AgentSession({
1699
1774
  provider: target.provider,
1700
1775
  model: target.model,
1701
1776
  cwd,
@@ -1704,38 +1779,41 @@ async function createSession(deps, opts) {
1704
1779
  allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
1705
1780
  transient: true,
1706
1781
  signal: nolanAbort.signal,
1707
- // 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
1708
1786
  // TTL regardless of the user's global speedProfile pick.
1709
1787
  forceLongCacheRetention: true,
1710
1788
  });
1711
- await ken.initialize();
1712
- // 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
1713
1791
  // webview routes them to the Nolan bubble, never EZ Coder's.
1714
- ken.eventBus.on("text_delta", (d) => broadcast("nolan_text_delta", d));
1715
- ken.eventBus.on("thinking_delta", (d) => broadcast("nolan_thinking_delta", d));
1716
- 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) => {
1717
1795
  nolanToolCallNames.set(d.toolCallId, d.name);
1718
1796
  broadcast("nolan_tool_call_start", d);
1719
1797
  });
1720
- ken.eventBus.on("tool_call_update", (d) => broadcast("nolan_tool_call_update", d));
1721
- 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) => {
1722
1800
  nolanToolCallNames.delete(d.toolCallId);
1723
1801
  broadcast("nolan_tool_call_end", d);
1724
1802
  });
1725
1803
  // Native server tools (Anthropic web_search) stream text both before AND
1726
1804
  // after them in the same turn; forward so the webview can break the bubble
1727
1805
  // (otherwise "...work.Local tools..." glues together). Mirrors the build bus.
1728
- ken.eventBus.on("server_tool_call", (d) => broadcast("nolan_server_tool_call", d));
1729
- ken.eventBus.on("turn_end", (d) => broadcast("nolan_turn_end", d));
1730
- ken.eventBus.on("error", (d) => {
1731
- 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);
1732
1810
  });
1733
- nolanSession = ken;
1734
- log("INFO", "app-sidecar", "ken session ready", {
1811
+ nolanSession = nolanAgent;
1812
+ log("INFO", "app-sidecar", "nolan session ready", {
1735
1813
  provider: target.provider,
1736
1814
  model: target.model,
1737
1815
  });
1738
- return ken;
1816
+ return nolanAgent;
1739
1817
  }
1740
1818
  // ── Autopilot Nolan (auto-reviewer) ──────────────────────────
1741
1819
  // A THIRD read-only AgentSession, separate from chat Nolan. In autopilot mode
@@ -1758,13 +1836,13 @@ async function createSession(deps, opts) {
1758
1836
  if (st.provider === provider && st.model === model)
1759
1837
  return;
1760
1838
  await nolanAutoSession.switchModel(provider, model);
1761
- log("INFO", "app-sidecar", "ken autopilot session model synced", { provider, model });
1839
+ log("INFO", "app-sidecar", "nolan autopilot session model synced", { provider, model });
1762
1840
  }
1763
1841
  async function ensureNolanAutoSession() {
1764
1842
  if (nolanAutoSession)
1765
1843
  return nolanAutoSession;
1766
1844
  const target = nolanCurrentModel();
1767
- const ken = new AgentSession({
1845
+ const nolanAgent = new AgentSession({
1768
1846
  provider: target.provider,
1769
1847
  model: target.model,
1770
1848
  cwd,
@@ -1773,26 +1851,32 @@ async function createSession(deps, opts) {
1773
1851
  allowedMcpServers: NOLAN_ALLOWED_MCP_SERVERS,
1774
1852
  transient: true,
1775
1853
  signal: nolanAutoAbort.signal,
1854
+ // Route this window's MCP elicitation prompts through the shared bridge.
1855
+ onMcpElicit: elicitations.onElicit,
1776
1856
  // Autopilot review rounds routinely span the injected EZ Coder run
1777
1857
  // (often >5 min) regardless of the user's global speedProfile pick.
1778
1858
  forceLongCacheRetention: true,
1779
1859
  });
1780
1860
  // Nolan is already the independent autopilot reviewer; recursively running
1781
1861
  // his own Ideal self-review adds latency and can corrupt the verdict shape.
1782
- ken.setIdealReviewSuppressed(true);
1783
- await ken.initialize();
1862
+ nolanAgent.setIdealReviewSuppressed(true);
1863
+ await nolanAgent.initialize();
1784
1864
  // Deliberately no bus bridge: the review is silent. Errors surface via the
1785
1865
  // runAutopilotReview try/catch as autopilot_error frames.
1786
- nolanAutoSession = ken;
1787
- log("INFO", "app-sidecar", "ken autopilot session ready", {
1866
+ nolanAutoSession = nolanAgent;
1867
+ log("INFO", "app-sidecar", "nolan autopilot session ready", {
1788
1868
  provider: target.provider,
1789
1869
  model: target.model,
1790
1870
  });
1791
- return ken;
1871
+ return nolanAgent;
1792
1872
  }
1793
1873
  function abortOwnedWork() {
1794
1874
  cancelGeneration++;
1795
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();
1796
1880
  // Stop a run-all sweep and every async child through AgentSession's signal.
1797
1881
  taskRunAll = false;
1798
1882
  autopilotCancelled = true;
@@ -1804,9 +1888,9 @@ async function createSession(deps, opts) {
1804
1888
  nolanAutoAbort = new AbortController();
1805
1889
  nolanAutoSession?.setSignal(nolanAutoAbort.signal);
1806
1890
  }
1807
- function finishOwnedGeneration(generation, emitCancelledFallback) {
1891
+ function finishOwnedGeneration(generation, emitCancelledFallback, outcome = "completed") {
1808
1892
  const cancelled = runLifecycle.isCancellationRequested(generation);
1809
- const settlement = runLifecycle.settle(generation);
1893
+ const settlement = runLifecycle.settle(generation, outcome);
1810
1894
  if (!settlement.settled)
1811
1895
  return cancelled;
1812
1896
  // A replacement signal is safe only after the provider-backed owner settled.
@@ -1884,8 +1968,9 @@ async function createSession(deps, opts) {
1884
1968
  });
1885
1969
  }
1886
1970
  }
1887
- if (ownsGeneration)
1888
- finishOwnedGeneration(generation, false);
1971
+ if (ownsGeneration) {
1972
+ finishOwnedGeneration(generation, false, runSucceeded ? "completed" : "failed");
1973
+ }
1889
1974
  // A cancelled injected run is still owned by the surrounding autopilot
1890
1975
  // cycle; its outer finalizer emits the one terminal cancelled run_end.
1891
1976
  if (!(cancelled && !ownsGeneration)) {
@@ -1904,7 +1989,10 @@ async function createSession(deps, opts) {
1904
1989
  // completed ones so they drop out of the Tasks modal automatically (users
1905
1990
  // never have to delete finished tasks by hand).
1906
1991
  broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
1907
- broadcast("queued", { count: session.getQueuedCount() });
1992
+ broadcast("queued", {
1993
+ count: session.getQueuedCount(),
1994
+ messages: session.listQueuedMessages(),
1995
+ });
1908
1996
  broadcast("extras", footerExtras());
1909
1997
  }
1910
1998
  }
@@ -1918,7 +2006,7 @@ async function createSession(deps, opts) {
1918
2006
  autopilotReviewing = true;
1919
2007
  broadcast("autopilot_review_start", {});
1920
2008
  try {
1921
- const ken = await ensureNolanAutoSession();
2009
+ const nolanAgent = await ensureNolanAutoSession();
1922
2010
  const digest = buildNolanAutopilotContext({
1923
2011
  cwd,
1924
2012
  gitBranch,
@@ -1927,8 +2015,8 @@ async function createSession(deps, opts) {
1927
2015
  injectedPrompts: [...injectedAutopilotPrompts],
1928
2016
  workflowCommands: await loadWorkflowCommandSpecs(),
1929
2017
  });
1930
- await ken.prompt(digest);
1931
- return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
2018
+ await nolanAgent.prompt(digest);
2019
+ return parseAutopilotVerdict(lastAssistantText(nolanAgent.getMessages()));
1932
2020
  }
1933
2021
  catch (err) {
1934
2022
  if (!autopilotCancelled)
@@ -1958,7 +2046,7 @@ async function createSession(deps, opts) {
1958
2046
  autopilotReviewing = true;
1959
2047
  broadcast("autopilot_review_start", {});
1960
2048
  try {
1961
- const ken = await ensureNolanAutoSession();
2049
+ const nolanAgent = await ensureNolanAutoSession();
1962
2050
  // Re-read the plan file (the run may have revised it in place); fall
1963
2051
  // back to the content captured at exit_plan time.
1964
2052
  const planContent = await fs.readFile(planPath, "utf-8").catch(() => pendingPlanContent);
@@ -1971,10 +2059,10 @@ async function createSession(deps, opts) {
1971
2059
  workflowCommands: await loadWorkflowCommandSpecs(),
1972
2060
  planContent,
1973
2061
  });
1974
- await ken.prompt(digest);
2062
+ await nolanAgent.prompt(digest);
1975
2063
  if (autopilotCancelled || planGeneration !== genAtStart)
1976
2064
  return null;
1977
- return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
2065
+ return parseAutopilotVerdict(lastAssistantText(nolanAgent.getMessages()));
1978
2066
  }
1979
2067
  catch (err) {
1980
2068
  // User action mid-review (manual Accept aborts the nolanAuto run): drop
@@ -2000,7 +2088,7 @@ async function createSession(deps, opts) {
2000
2088
  const IMPLEMENT_PLAN_PROMPT = "The plan has been approved. Implement it now, following each step in order.";
2001
2089
  // Drive the review→prompt→review loop for one finished user turn. Only ever
2002
2090
  // called after shouldStartAutopilotCycle approves the turn (POST /prompt or
2003
- // 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
2004
2092
  // error paths, so there's no recursion and no guard tangle. The loop's
2005
2093
  // control flow lives in driveAutopilotCycle (core/autopilot-cycle.ts) so
2006
2094
  // every exit path is unit-tested; this only wires the real dependencies.
@@ -2064,7 +2152,7 @@ async function createSession(deps, opts) {
2064
2152
  // label stays the clean prompt.
2065
2153
  const framed = frameAutopilotInjection(IMPLEMENT_PLAN_PROMPT);
2066
2154
  injectedAutopilotPrompts.push(framed);
2067
- return runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(framed));
2155
+ return runAgent(IMPLEMENT_PLAN_PROMPT, () => session.prompt(framed, AUTOMATION_PROVENANCE));
2068
2156
  },
2069
2157
  // Lean context per user turn: wipe prior review history so each new
2070
2158
  // turn starts cheap, while within this cycle the few review messages
@@ -2093,19 +2181,21 @@ async function createSession(deps, opts) {
2093
2181
  },
2094
2182
  // Autopilot-injected run: EZ Coder receives the framed prompt (no human
2095
2183
  // is watching this turn) while run_start keeps the clean label.
2096
- runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body))),
2184
+ runPrompt: (body) => runAgent(body, () => session.prompt(frameAutopilotInjection(body), AUTOMATION_PROVENANCE)),
2097
2185
  emit: (event) => {
2098
2186
  // Persist the terminal verdict marker so a resumed session renders the
2099
2187
  // same Nolan bubble the live run showed instead of dropping it or
2100
2188
  // falling back to the raw verdict text (e.g. ALL_CLEAR).
2101
2189
  if (event.type === "autopilot_done") {
2102
2190
  // Broadcast the SAME copySeed the persisted marker will produce on
2103
- // resume, so the live all-clear wording matches the resumed one
2104
- // (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.
2105
2195
  const seed = autopilotMarkerCopySeed({
2106
2196
  version: 1,
2107
2197
  phase: "done",
2108
- afterMessageCount: session.getMessages().filter((m) => m.role !== "system").length,
2198
+ afterMessageCount: session.getPersistedTranscriptCount(),
2109
2199
  });
2110
2200
  broadcast(event.type, { ...event.data, copySeed: seed });
2111
2201
  void session.persistAutopilotMarker("done");
@@ -2150,7 +2240,10 @@ async function createSession(deps, opts) {
2150
2240
  const next = session.takeNextQueuedMessage();
2151
2241
  if (!next)
2152
2242
  return;
2153
- broadcast("queued", { count: session.getQueuedCount() });
2243
+ broadcast("queued", {
2244
+ count: session.getQueuedCount(),
2245
+ messages: session.listQueuedMessages(),
2246
+ });
2154
2247
  if (!next.text.trim() && next.attachments.length === 0)
2155
2248
  continue;
2156
2249
  // A queued message draining as a fresh turn supersedes any pending
@@ -2236,7 +2329,7 @@ async function createSession(deps, opts) {
2236
2329
  const shortId = task.id.slice(0, 8);
2237
2330
  const completionHint = `\n\n---\nWhen you have fully completed this task, call the tasks tool to mark it done:\n` +
2238
2331
  `tasks({ action: "done", id: "${shortId}" })`;
2239
- await runAgent(task.title, () => session.prompt(task.prompt + completionHint));
2332
+ await runAgent(task.title, () => session.prompt(task.prompt + completionHint, AUTOMATION_PROVENANCE));
2240
2333
  // The agent typically marks the task done via the tasks tool during the run;
2241
2334
  // prune completed tasks and push the refreshed list so the modal drops them.
2242
2335
  broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
@@ -2469,6 +2562,45 @@ async function createSession(deps, opts) {
2469
2562
  });
2470
2563
  return;
2471
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
+ }
2472
2604
  if (method === "GET" && url === "/settings") {
2473
2605
  // `configured` is true only when the user explicitly saved a projects root
2474
2606
  // (the ezcoder-app.json file exists with a value) — not when we fall back to the
@@ -2555,9 +2687,53 @@ async function createSession(deps, opts) {
2555
2687
  });
2556
2688
  return;
2557
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
+ }
2558
2729
  if (method === "GET" && url === "/projects") {
2559
- // Scan ezcoder + Claude Code + Codex session stores for known projects.
2560
- 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
+ }))
2561
2737
  .then((projects) => json(res, 200, { projects }))
2562
2738
  .catch((err) => {
2563
2739
  captureSidecarError(err, "app-sidecar.projects.discover");
@@ -2667,7 +2843,7 @@ async function createSession(deps, opts) {
2667
2843
  // Nolan (mentor) turns to interleave: group by the non-system message count
2668
2844
  // they were recorded after, so each lands right after that message. A
2669
2845
  // turn becomes two wire rows: the `@Nolan` question (user) + Nolan's reply
2670
- // (assistant), both flagged `ken` so the webview tints them.
2846
+ // (assistant), both flagged `nolan` so the webview tints them.
2671
2847
  // Deduped; stale anchors are clamped to the last message (Nolan turns
2672
2848
  // carry real conversation, so they render at the end instead of
2673
2849
  // vanishing).
@@ -2683,16 +2859,16 @@ async function createSession(deps, opts) {
2683
2859
  return;
2684
2860
  nolanByCount.delete(count);
2685
2861
  for (const turn of turns) {
2686
- history.push({ role: "user", text: `@Nolan ${turn.question}`, ken: true });
2687
- 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 });
2688
2864
  }
2689
2865
  };
2690
2866
  // Autopilot verdict markers to interleave, same anchor scheme as Nolan
2691
2867
  // turns — each becomes a single assistant row the webview renders
2692
2868
  // exactly like the live `autopilot` item (never a raw verdict string).
2693
- // Compact/continuation rewrites can carry old markers whose original
2694
- // afterMessageCount is beyond the restored message list; dropping those
2695
- // 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.
2696
2872
  const restoredMessageCount = messages.filter((m) => m.role !== "system").length;
2697
2873
  const autopilotByCount = new Map();
2698
2874
  for (const marker of normalizeAutopilotMarkersForHistory(session.getAutopilotMarkers(), restoredMessageCount)) {
@@ -2776,6 +2952,21 @@ async function createSession(deps, opts) {
2776
2952
  },
2777
2953
  });
2778
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
+ }
2779
2970
  }
2780
2971
  };
2781
2972
  let nonSystemCount = 0;
@@ -2784,10 +2975,11 @@ async function createSession(deps, opts) {
2784
2975
  flushNolan(0);
2785
2976
  flushAutopilot(0);
2786
2977
  flushAppMarkers(0);
2787
- for (const msg of messages) {
2788
- if (msg.role === "system")
2789
- continue;
2790
- nonSystemCount++;
2978
+ await replayMessagesInOrder(messages, async (msg, count) => {
2979
+ nonSystemCount = count;
2980
+ const visibility = getHistoryMessageVisibility(msg);
2981
+ if (visibility === "hidden")
2982
+ return;
2791
2983
  if (msg.role === "tool") {
2792
2984
  // Tool result messages: check for ImageContent blocks (screenshots,
2793
2985
  // generated images) and emit a toolImages entry.
@@ -2831,20 +3023,39 @@ async function createSession(deps, opts) {
2831
3023
  });
2832
3024
  }
2833
3025
  }
2834
- continue;
3026
+ return;
2835
3027
  }
2836
3028
  // User or assistant message — text/hook/command/compacted extraction,
2837
3029
  // plus sub-agent group detection for assistant tool_calls.
2838
3030
  if (msg.role === "user") {
2839
3031
  // Rebuild the live bubble: strip the steering wrapper, drop
2840
3032
  // attachment/file notes the model saw but the bubble never showed.
2841
- const restored = restoreUserRow(msg.content);
3033
+ const restored = restoreUserRow(msg.content, msg.provenance);
2842
3034
  const text = restored.text;
2843
- const hook = detectHookKind(text);
2844
- const compacted = !hook && text.startsWith("[Previous conversation summary]");
2845
- const command = !hook && !compacted ? detectPromptCommand(text, commandCandidates) : null;
2846
- if (text.trim() || restored.images.length > 0) {
2847
- 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)) {
2848
3059
  history.push({
2849
3060
  role: "user",
2850
3061
  text: command ?? text,
@@ -2907,12 +3118,13 @@ async function createSession(deps, opts) {
2907
3118
  });
2908
3119
  }
2909
3120
  }
2910
- // Interleave any Nolan turns / autopilot / app markers recorded right
2911
- // after this message.
2912
- flushNolan(nonSystemCount);
2913
- flushAutopilot(nonSystemCount);
2914
- flushAppMarkers(nonSystemCount);
2915
- }
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
+ });
2916
3128
  // Flush remaining Nolan turns whose anchor is at/after the message count so
2917
3129
  // none are dropped. Autopilot/app markers beyond the restored message
2918
3130
  // count were already filtered above; any remaining marker here is valid.
@@ -2973,7 +3185,12 @@ async function createSession(deps, opts) {
2973
3185
  return;
2974
3186
  }
2975
3187
  if (method === "POST" && url === "/prompt") {
2976
- 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) => {
2977
3194
  if (raw === null)
2978
3195
  return;
2979
3196
  let text;
@@ -2993,14 +3210,17 @@ async function createSession(deps, opts) {
2993
3210
  json(res, 400, { error: "empty prompt" });
2994
3211
  return;
2995
3212
  }
2996
- if (runLifecycle.running && runLifecycle.isCancellationRequested(runLifecycle.generation)) {
3213
+ if (runLifecycle.running &&
3214
+ runLifecycle.isCancellationRequested(runLifecycle.generation)) {
2997
3215
  json(res, 409, {
2998
3216
  error: runLifecycle.state === "cancelling" ? "run_cancelling" : "cancel_failed",
2999
3217
  runState: runLifecycle.state,
3000
3218
  });
3001
3219
  return;
3002
3220
  }
3003
- 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) {
3004
3224
  // Queue prompts as mid-run steering (mirrors the CLI). Also queue while
3005
3225
  // an autopilot cycle is active but between injected runs (build idle,
3006
3226
  // Nolan reviewing) so the message never starts a run that collides with
@@ -3009,20 +3229,47 @@ async function createSession(deps, opts) {
3009
3229
  // path as a non-queued attachment prompt when it drains.
3010
3230
  const prepared = attachments.length > 0 ? await prepareAttachments(cwd, attachments) : [];
3011
3231
  const count = session.queueMessage(text, prepared);
3012
- broadcast("queued", { count });
3232
+ broadcast("queued", { count, messages: session.listQueuedMessages() });
3013
3233
  json(res, 202, { queued: true, count });
3014
3234
  return;
3015
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();
3016
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));
3017
3254
  // Webview display hint for this prompt's user bubble (nolanSent shimmer
3018
- // label / enhancer highlight segments). Anchored +1 so it attaches to
3019
- // the user message the prompt below is about to push. Queued prompts
3020
- // skip this (their position in the run is unpredictable).
3021
- 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)))) {
3022
3268
  void session
3023
3269
  .persistAppMarker("user_hint", {
3024
- ...(meta.nolanSent === true ? { nolanSent: true } : {}),
3025
- ...(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 } : {}),
3026
3273
  }, 1)
3027
3274
  .catch(() => { });
3028
3275
  }
@@ -3033,12 +3280,6 @@ async function createSession(deps, opts) {
3033
3280
  // feedback, anything) supersedes the pending plan — the bump also
3034
3281
  // invalidates any in-flight Nolan plan review.
3035
3282
  clearPendingPlan();
3036
- // Gate inputs captured around the run: whether this turn is a workflow
3037
- // slash command (attachment prompts skip slash expansion entirely), and
3038
- // how many assistant messages the run actually adds. Computed even when
3039
- // autopilot is currently off — the toggle can flip ON mid-run, and the
3040
- // gate reads the post-run value.
3041
- const workflowCommand = attachments.length === 0 && isWorkflowCommandText(text, await loadWorkflowCommandSpecs());
3042
3283
  const assistantsBefore = countAssistantMessages(session.getMessages());
3043
3284
  const messagesBefore = session.getMessages().length;
3044
3285
  await runAgent(text, async () => {
@@ -3090,14 +3331,18 @@ async function createSession(deps, opts) {
3090
3331
  // A prompt sent while Nolan was reviewing (build idle) queued but had no
3091
3332
  // run to steer into — run it now as a fresh turn so it never strands.
3092
3333
  await runStrandedQueue();
3334
+ })
3335
+ .finally(() => {
3336
+ if (claimedStart)
3337
+ runClaim.release();
3093
3338
  });
3094
3339
  return;
3095
3340
  }
3096
3341
  // Nolan Grout (mentor): an independent read-only advisory run on the nolanSession.
3097
- // 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
3098
3343
  // webview keeps the bubbles separate. The context digest is assembled fresh
3099
3344
  // from the BUILD session's transcript each turn (one-way mirror).
3100
- if (method === "POST" && url === "/ken/prompt") {
3345
+ if (method === "POST" && url === "/nolan/prompt") {
3101
3346
  if (mode === "chat") {
3102
3347
  json(res, 404, { error: "Nolan is not available in EZ Chat." });
3103
3348
  return;
@@ -3125,18 +3370,18 @@ async function createSession(deps, opts) {
3125
3370
  nolanRunning = true;
3126
3371
  broadcast("nolan_run_start", { text });
3127
3372
  try {
3128
- const ken = await ensureNolanSession();
3373
+ const nolanAgent = await ensureNolanSession();
3129
3374
  const digest = await buildNolanContext(session, cwd, gitBranch, text, await loadWorkflowCommandSpecs(), injectedAutopilotPrompts);
3130
- await ken.prompt(digest);
3375
+ await nolanAgent.prompt(digest);
3131
3376
  // Record the turn against the BUILD session so it persists + survives
3132
3377
  // resume (advisory custom entry, never an LLM message). Reply is Nolan's
3133
3378
  // last assistant message; skip persistence if he produced nothing.
3134
- const reply = lastAssistantText(ken.getMessages());
3379
+ const reply = lastAssistantText(nolanAgent.getMessages());
3135
3380
  if (reply.trim())
3136
3381
  await session.persistNolanTurn(text, reply);
3137
3382
  }
3138
3383
  catch (err) {
3139
- broadcastError("nolan_error", "ken run failed", err);
3384
+ broadcastError("nolan_error", "nolan run failed", err);
3140
3385
  }
3141
3386
  finally {
3142
3387
  nolanRunning = false;
@@ -3149,7 +3394,7 @@ async function createSession(deps, opts) {
3149
3394
  });
3150
3395
  return;
3151
3396
  }
3152
- if (method === "POST" && url === "/ken/cancel") {
3397
+ if (method === "POST" && url === "/nolan/cancel") {
3153
3398
  nolanAbort.abort();
3154
3399
  nolanAbort = new AbortController();
3155
3400
  nolanSession?.setSignal(nolanAbort.signal);
@@ -3524,7 +3769,7 @@ async function createSession(deps, opts) {
3524
3769
  // no-op event (keeps footer/event tests from treating an EZ Coder
3525
3770
  // switch as a Nolan switch).
3526
3771
  if (!nolanModelOverride)
3527
- broadcast("ken_model_change", nolanStatePayload());
3772
+ broadcast("nolan_model_change", nolanStatePayload());
3528
3773
  // The new model usually has a different context window — push extras so
3529
3774
  // the footer's context meter rescales immediately.
3530
3775
  broadcast("extras", footerExtras());
@@ -3536,7 +3781,7 @@ async function createSession(deps, opts) {
3536
3781
  // { model: null } / "" to clear (Nolan resumes following EZ Coder). Applies
3537
3782
  // to BOTH Nolan sessions (chat + autopilot reviewer); a switch landing while
3538
3783
  // either is mid-run defers via the pending-model mechanics.
3539
- if (method === "POST" && url === "/ken/model") {
3784
+ if (method === "POST" && url === "/nolan/model") {
3540
3785
  void readBody(req, res).then(async (raw) => {
3541
3786
  if (raw === null)
3542
3787
  return;
@@ -3571,17 +3816,49 @@ async function createSession(deps, opts) {
3571
3816
  await saveNolanModelPref(cwd, nolanModelOverride);
3572
3817
  await syncNolanModel(target.provider, target.id);
3573
3818
  await syncNolanAutoModel(target.provider, target.id);
3574
- log("INFO", "app-sidecar", "ken model pinned", {
3819
+ log("INFO", "app-sidecar", "nolan model pinned", {
3575
3820
  provider: target.provider,
3576
3821
  model: target.id,
3577
3822
  });
3578
3823
  }
3579
3824
  const payload = nolanStatePayload();
3580
- broadcast("ken_model_change", payload);
3825
+ broadcast("nolan_model_change", payload);
3581
3826
  json(res, 200, payload);
3582
3827
  });
3583
3828
  return;
3584
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
+ }
3585
3862
  if (method === "POST" && url === "/kill") {
3586
3863
  void readBody(req, res).then(async (raw) => {
3587
3864
  if (raw === null)
@@ -3605,6 +3882,33 @@ async function createSession(deps, opts) {
3605
3882
  });
3606
3883
  return;
3607
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
+ }
3608
3912
  if (method === "POST" && url === "/thinking") {
3609
3913
  const st = session.getState();
3610
3914
  const next = getNextThinkingLevel(st.provider, st.model, session.getThinkingLevel());
@@ -3639,7 +3943,7 @@ async function createSession(deps, opts) {
3639
3943
  const generation = runLifecycle.generation;
3640
3944
  if (!pendingCancelDrain || pendingCancelDrain.generation !== generation) {
3641
3945
  pendingCancelDrain = { generation, text: session.drainQueue() };
3642
- broadcast("queued", { count: 0 });
3946
+ broadcast("queued", { count: 0, messages: [] });
3643
3947
  }
3644
3948
  const result = await runLifecycle.cancel(CANCEL_TIMEOUT_MS);
3645
3949
  const drained = pendingCancelDrain.text;
@@ -3799,7 +4103,17 @@ async function createSession(deps, opts) {
3799
4103
  ...(baseUrl ? { baseUrl } : {}),
3800
4104
  };
3801
4105
  await auth.setCredentials(storageKey, creds);
3802
- 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", {});
3803
4117
  json(res, 200, { ok: true });
3804
4118
  });
3805
4119
  return;
@@ -3825,7 +4139,18 @@ async function createSession(deps, opts) {
3825
4139
  json(res, 409, { error: "a login is already in progress" });
3826
4140
  return;
3827
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
+ }
3828
4152
  oauthInFlight = true;
4153
+ oauthInFlightProviders.add(provider);
3829
4154
  json(res, 202, { accepted: true });
3830
4155
  void (async () => {
3831
4156
  const cb = authCallbacks();
@@ -3846,10 +4171,19 @@ async function createSession(deps, opts) {
3846
4171
  throw new Error(`OAuth not implemented for ${provider}`);
3847
4172
  }
3848
4173
  await auth.setCredentials(storageKey, creds);
3849
- 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", {});
3850
4181
  }
3851
4182
  catch (err) {
3852
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.
3853
4187
  broadcast("auth_error", {
3854
4188
  provider,
3855
4189
  message: err instanceof Error ? err.message : String(err),
@@ -3857,6 +4191,7 @@ async function createSession(deps, opts) {
3857
4191
  }
3858
4192
  finally {
3859
4193
  oauthInFlight = false;
4194
+ oauthInFlightProviders.delete(provider);
3860
4195
  pendingCode = null;
3861
4196
  }
3862
4197
  })();
@@ -3885,6 +4220,39 @@ async function createSession(deps, opts) {
3885
4220
  });
3886
4221
  return;
3887
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
+ }
3888
4256
  if (method === "POST" && url === "/auth/logout") {
3889
4257
  void readBody(req, res).then(async (raw) => {
3890
4258
  if (raw === null)
@@ -4270,6 +4638,7 @@ async function createSession(deps, opts) {
4270
4638
  json(res, 404, { error: "not found" });
4271
4639
  }
4272
4640
  async function dispose() {
4641
+ elicitations.cancelAll();
4273
4642
  tasksPollStopped = true;
4274
4643
  if (tasksPoll)
4275
4644
  clearTimeout(tasksPoll);