synabun 2026.5.3-beta → 2026.5.13-beta

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 (307) hide show
  1. package/CHANGELOG.md +202 -211
  2. package/mcp-server/dist/config.d.ts +13 -0
  3. package/mcp-server/dist/config.d.ts.map +1 -0
  4. package/mcp-server/dist/config.js +59 -0
  5. package/mcp-server/dist/config.js.map +1 -0
  6. package/mcp-server/dist/http.d.ts +20 -0
  7. package/mcp-server/dist/http.d.ts.map +1 -0
  8. package/mcp-server/dist/http.js +104 -0
  9. package/mcp-server/dist/http.js.map +1 -0
  10. package/mcp-server/dist/index.d.ts +19 -0
  11. package/mcp-server/dist/index.d.ts.map +1 -0
  12. package/mcp-server/dist/index.js +439 -0
  13. package/mcp-server/dist/index.js.map +1 -0
  14. package/mcp-server/dist/preload-http.d.ts +3 -0
  15. package/mcp-server/dist/preload-http.d.ts.map +1 -0
  16. package/mcp-server/dist/preload-http.js +26 -0
  17. package/mcp-server/dist/preload-http.js.map +1 -0
  18. package/mcp-server/dist/preload.d.ts +3 -0
  19. package/mcp-server/dist/preload.d.ts.map +1 -0
  20. package/mcp-server/dist/preload.js +30 -0
  21. package/mcp-server/dist/preload.js.map +1 -0
  22. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts +19 -0
  23. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.d.ts.map +1 -0
  24. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js +403 -0
  25. package/mcp-server/dist/scripts/migrate-qdrant-to-sqlite.js.map +1 -0
  26. package/mcp-server/dist/services/categories.d.ts +61 -0
  27. package/mcp-server/dist/services/categories.d.ts.map +1 -0
  28. package/mcp-server/dist/services/categories.js +356 -0
  29. package/mcp-server/dist/services/categories.js.map +1 -0
  30. package/mcp-server/dist/services/discord.d.ts +112 -0
  31. package/mcp-server/dist/services/discord.d.ts.map +1 -0
  32. package/mcp-server/dist/services/discord.js +477 -0
  33. package/mcp-server/dist/services/discord.js.map +1 -0
  34. package/mcp-server/dist/services/file-checksums.d.ts +14 -0
  35. package/mcp-server/dist/services/file-checksums.d.ts.map +1 -0
  36. package/mcp-server/dist/services/file-checksums.js +34 -0
  37. package/mcp-server/dist/services/file-checksums.js.map +1 -0
  38. package/mcp-server/dist/services/local-embeddings.d.ts +23 -0
  39. package/mcp-server/dist/services/local-embeddings.d.ts.map +1 -0
  40. package/mcp-server/dist/services/local-embeddings.js +71 -0
  41. package/mcp-server/dist/services/local-embeddings.js.map +1 -0
  42. package/mcp-server/dist/services/neural-interface.d.ts +113 -0
  43. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -0
  44. package/mcp-server/dist/services/neural-interface.js +348 -0
  45. package/mcp-server/dist/services/neural-interface.js.map +1 -0
  46. package/mcp-server/dist/services/profiles.d.ts +29 -0
  47. package/mcp-server/dist/services/profiles.d.ts.map +1 -0
  48. package/mcp-server/dist/services/profiles.js +257 -0
  49. package/mcp-server/dist/services/profiles.js.map +1 -0
  50. package/mcp-server/dist/services/sqlite.d.ts +85 -0
  51. package/mcp-server/dist/services/sqlite.d.ts.map +1 -0
  52. package/mcp-server/dist/services/sqlite.js +660 -0
  53. package/mcp-server/dist/services/sqlite.js.map +1 -0
  54. package/mcp-server/dist/tools/browser-advanced.d.ts +58 -0
  55. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -0
  56. package/mcp-server/dist/tools/browser-advanced.js +104 -0
  57. package/mcp-server/dist/tools/browser-advanced.js.map +1 -0
  58. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +21 -0
  59. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -0
  60. package/mcp-server/dist/tools/browser-cheatsheet.js +114 -0
  61. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -0
  62. package/mcp-server/dist/tools/browser-interact.d.ts +216 -0
  63. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -0
  64. package/mcp-server/dist/tools/browser-interact.js +218 -0
  65. package/mcp-server/dist/tools/browser-interact.js.map +1 -0
  66. package/mcp-server/dist/tools/browser-navigate.d.ts +86 -0
  67. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -0
  68. package/mcp-server/dist/tools/browser-navigate.js +96 -0
  69. package/mcp-server/dist/tools/browser-navigate.js.map +1 -0
  70. package/mcp-server/dist/tools/browser-observe.d.ts +362 -0
  71. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -0
  72. package/mcp-server/dist/tools/browser-observe.js +1279 -0
  73. package/mcp-server/dist/tools/browser-observe.js.map +1 -0
  74. package/mcp-server/dist/tools/browser.d.ts +18 -0
  75. package/mcp-server/dist/tools/browser.d.ts.map +1 -0
  76. package/mcp-server/dist/tools/browser.js +88 -0
  77. package/mcp-server/dist/tools/browser.js.map +1 -0
  78. package/mcp-server/dist/tools/card-tools.d.ts +82 -0
  79. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -0
  80. package/mcp-server/dist/tools/card-tools.js +137 -0
  81. package/mcp-server/dist/tools/card-tools.js.map +1 -0
  82. package/mcp-server/dist/tools/card.d.ts +8 -0
  83. package/mcp-server/dist/tools/card.d.ts.map +1 -0
  84. package/mcp-server/dist/tools/card.js +16 -0
  85. package/mcp-server/dist/tools/card.js.map +1 -0
  86. package/mcp-server/dist/tools/category.d.ts +32 -0
  87. package/mcp-server/dist/tools/category.d.ts.map +1 -0
  88. package/mcp-server/dist/tools/category.js +290 -0
  89. package/mcp-server/dist/tools/category.js.map +1 -0
  90. package/mcp-server/dist/tools/discord-channel.d.ts +44 -0
  91. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -0
  92. package/mcp-server/dist/tools/discord-channel.js +236 -0
  93. package/mcp-server/dist/tools/discord-channel.js.map +1 -0
  94. package/mcp-server/dist/tools/discord-guild.d.ts +20 -0
  95. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -0
  96. package/mcp-server/dist/tools/discord-guild.js +138 -0
  97. package/mcp-server/dist/tools/discord-guild.js.map +1 -0
  98. package/mcp-server/dist/tools/discord-member.d.ts +26 -0
  99. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -0
  100. package/mcp-server/dist/tools/discord-member.js +122 -0
  101. package/mcp-server/dist/tools/discord-member.js.map +1 -0
  102. package/mcp-server/dist/tools/discord-message.d.ts +36 -0
  103. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -0
  104. package/mcp-server/dist/tools/discord-message.js +184 -0
  105. package/mcp-server/dist/tools/discord-message.js.map +1 -0
  106. package/mcp-server/dist/tools/discord-onboarding.d.ts +32 -0
  107. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -0
  108. package/mcp-server/dist/tools/discord-onboarding.js +179 -0
  109. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -0
  110. package/mcp-server/dist/tools/discord-role.d.ts +34 -0
  111. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -0
  112. package/mcp-server/dist/tools/discord-role.js +178 -0
  113. package/mcp-server/dist/tools/discord-role.js.map +1 -0
  114. package/mcp-server/dist/tools/discord-thread.d.ts +28 -0
  115. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -0
  116. package/mcp-server/dist/tools/discord-thread.js +88 -0
  117. package/mcp-server/dist/tools/discord-thread.js.map +1 -0
  118. package/mcp-server/dist/tools/discord-webhook.d.ts +32 -0
  119. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -0
  120. package/mcp-server/dist/tools/discord-webhook.js +154 -0
  121. package/mcp-server/dist/tools/discord-webhook.js.map +1 -0
  122. package/mcp-server/dist/tools/discord.d.ts +7 -0
  123. package/mcp-server/dist/tools/discord.d.ts.map +1 -0
  124. package/mcp-server/dist/tools/discord.js +25 -0
  125. package/mcp-server/dist/tools/discord.js.map +1 -0
  126. package/mcp-server/dist/tools/forget.d.ts +14 -0
  127. package/mcp-server/dist/tools/forget.d.ts.map +1 -0
  128. package/mcp-server/dist/tools/forget.js +26 -0
  129. package/mcp-server/dist/tools/forget.js.map +1 -0
  130. package/mcp-server/dist/tools/git-tools.d.ts +24 -0
  131. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -0
  132. package/mcp-server/dist/tools/git-tools.js +107 -0
  133. package/mcp-server/dist/tools/git-tools.js.map +1 -0
  134. package/mcp-server/dist/tools/git.d.ts +7 -0
  135. package/mcp-server/dist/tools/git.d.ts.map +1 -0
  136. package/mcp-server/dist/tools/git.js +11 -0
  137. package/mcp-server/dist/tools/git.js.map +1 -0
  138. package/mcp-server/dist/tools/gsc/experience.d.ts +135 -0
  139. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -0
  140. package/mcp-server/dist/tools/gsc/experience.js +351 -0
  141. package/mcp-server/dist/tools/gsc/experience.js.map +1 -0
  142. package/mcp-server/dist/tools/gsc/helpers.d.ts +78 -0
  143. package/mcp-server/dist/tools/gsc/helpers.d.ts.map +1 -0
  144. package/mcp-server/dist/tools/gsc/helpers.js +258 -0
  145. package/mcp-server/dist/tools/gsc/helpers.js.map +1 -0
  146. package/mcp-server/dist/tools/gsc/indexing.d.ts +121 -0
  147. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -0
  148. package/mcp-server/dist/tools/gsc/indexing.js +357 -0
  149. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -0
  150. package/mcp-server/dist/tools/gsc/inspect.d.ts +78 -0
  151. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -0
  152. package/mcp-server/dist/tools/gsc/inspect.js +321 -0
  153. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -0
  154. package/mcp-server/dist/tools/gsc/navigate.d.ts +44 -0
  155. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -0
  156. package/mcp-server/dist/tools/gsc/navigate.js +197 -0
  157. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -0
  158. package/mcp-server/dist/tools/gsc/performance.d.ts +84 -0
  159. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -0
  160. package/mcp-server/dist/tools/gsc/performance.js +274 -0
  161. package/mcp-server/dist/tools/gsc/performance.js.map +1 -0
  162. package/mcp-server/dist/tools/gsc/settings.d.ts +153 -0
  163. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -0
  164. package/mcp-server/dist/tools/gsc/settings.js +425 -0
  165. package/mcp-server/dist/tools/gsc/settings.js.map +1 -0
  166. package/mcp-server/dist/tools/gsc.d.ts +7 -0
  167. package/mcp-server/dist/tools/gsc.d.ts.map +1 -0
  168. package/mcp-server/dist/tools/gsc.js +51 -0
  169. package/mcp-server/dist/tools/gsc.js.map +1 -0
  170. package/mcp-server/dist/tools/image-tools.d.ts +24 -0
  171. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -0
  172. package/mcp-server/dist/tools/image-tools.js +80 -0
  173. package/mcp-server/dist/tools/image-tools.js.map +1 -0
  174. package/mcp-server/dist/tools/image.d.ts +10 -0
  175. package/mcp-server/dist/tools/image.d.ts.map +1 -0
  176. package/mcp-server/dist/tools/image.js +14 -0
  177. package/mcp-server/dist/tools/image.js.map +1 -0
  178. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +97 -0
  179. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -0
  180. package/mcp-server/dist/tools/leonardo-browser-tools.js +228 -0
  181. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -0
  182. package/mcp-server/dist/tools/leonardo.d.ts +13 -0
  183. package/mcp-server/dist/tools/leonardo.d.ts.map +1 -0
  184. package/mcp-server/dist/tools/leonardo.js +21 -0
  185. package/mcp-server/dist/tools/leonardo.js.map +1 -0
  186. package/mcp-server/dist/tools/loop.d.ts +30 -0
  187. package/mcp-server/dist/tools/loop.d.ts.map +1 -0
  188. package/mcp-server/dist/tools/loop.js +357 -0
  189. package/mcp-server/dist/tools/loop.js.map +1 -0
  190. package/mcp-server/dist/tools/memories.d.ts +26 -0
  191. package/mcp-server/dist/tools/memories.d.ts.map +1 -0
  192. package/mcp-server/dist/tools/memories.js +96 -0
  193. package/mcp-server/dist/tools/memories.js.map +1 -0
  194. package/mcp-server/dist/tools/profile.d.ts +16 -0
  195. package/mcp-server/dist/tools/profile.d.ts.map +1 -0
  196. package/mcp-server/dist/tools/profile.js +59 -0
  197. package/mcp-server/dist/tools/profile.js.map +1 -0
  198. package/mcp-server/dist/tools/recall.d.ts +41 -0
  199. package/mcp-server/dist/tools/recall.d.ts.map +1 -0
  200. package/mcp-server/dist/tools/recall.js +273 -0
  201. package/mcp-server/dist/tools/recall.js.map +1 -0
  202. package/mcp-server/dist/tools/reflect.d.ts +44 -0
  203. package/mcp-server/dist/tools/reflect.d.ts.map +1 -0
  204. package/mcp-server/dist/tools/reflect.js +118 -0
  205. package/mcp-server/dist/tools/reflect.js.map +1 -0
  206. package/mcp-server/dist/tools/remember.d.ts +38 -0
  207. package/mcp-server/dist/tools/remember.d.ts.map +1 -0
  208. package/mcp-server/dist/tools/remember.js +88 -0
  209. package/mcp-server/dist/tools/remember.js.map +1 -0
  210. package/mcp-server/dist/tools/response.d.ts +23 -0
  211. package/mcp-server/dist/tools/response.d.ts.map +1 -0
  212. package/mcp-server/dist/tools/response.js +24 -0
  213. package/mcp-server/dist/tools/response.js.map +1 -0
  214. package/mcp-server/dist/tools/restore.d.ts +14 -0
  215. package/mcp-server/dist/tools/restore.d.ts.map +1 -0
  216. package/mcp-server/dist/tools/restore.js +23 -0
  217. package/mcp-server/dist/tools/restore.js.map +1 -0
  218. package/mcp-server/dist/tools/sync.d.ts +18 -0
  219. package/mcp-server/dist/tools/sync.d.ts.map +1 -0
  220. package/mcp-server/dist/tools/sync.js +93 -0
  221. package/mcp-server/dist/tools/sync.js.map +1 -0
  222. package/mcp-server/dist/tools/tictactoe-tools.d.ts +18 -0
  223. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -0
  224. package/mcp-server/dist/tools/tictactoe-tools.js +78 -0
  225. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -0
  226. package/mcp-server/dist/tools/tictactoe.d.ts +7 -0
  227. package/mcp-server/dist/tools/tictactoe.d.ts.map +1 -0
  228. package/mcp-server/dist/tools/tictactoe.js +11 -0
  229. package/mcp-server/dist/tools/tictactoe.js.map +1 -0
  230. package/mcp-server/dist/tools/utils.d.ts +10 -0
  231. package/mcp-server/dist/tools/utils.d.ts.map +1 -0
  232. package/mcp-server/dist/tools/utils.js +30 -0
  233. package/mcp-server/dist/tools/utils.js.map +1 -0
  234. package/mcp-server/dist/tools/whiteboard-tools.d.ts +194 -0
  235. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -0
  236. package/mcp-server/dist/tools/whiteboard-tools.js +198 -0
  237. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -0
  238. package/mcp-server/dist/tools/whiteboard.d.ts +8 -0
  239. package/mcp-server/dist/tools/whiteboard.d.ts.map +1 -0
  240. package/mcp-server/dist/tools/whiteboard.js +16 -0
  241. package/mcp-server/dist/tools/whiteboard.js.map +1 -0
  242. package/mcp-server/dist/tui.d.ts +3 -0
  243. package/mcp-server/dist/tui.d.ts.map +1 -0
  244. package/mcp-server/dist/tui.js +807 -0
  245. package/mcp-server/dist/tui.js.map +1 -0
  246. package/mcp-server/dist/types.d.ts +77 -0
  247. package/mcp-server/dist/types.d.ts.map +1 -0
  248. package/mcp-server/dist/types.js +2 -0
  249. package/mcp-server/dist/types.js.map +1 -0
  250. package/mcp-server/src/http.ts +5 -1
  251. package/mcp-server/src/index.ts +22 -3
  252. package/mcp-server/src/services/profiles.ts +25 -5
  253. package/neural-interface/lib/opencode-client.js +517 -0
  254. package/neural-interface/lib/opencode-v2-client.js +235 -0
  255. package/neural-interface/package.json +1 -0
  256. package/neural-interface/public/shared/cdx/cdx-panel.js +39 -16
  257. package/neural-interface/public/shared/cdx/cdx-render.js +28 -4
  258. package/neural-interface/public/shared/cdx/cdx-styles.js +41 -20
  259. package/neural-interface/public/shared/cdx/cdx-tabs.js +149 -10
  260. package/neural-interface/public/shared/ocp/ocp-events.js +246 -0
  261. package/neural-interface/public/shared/ocp/ocp-panel.js +351 -126
  262. package/neural-interface/public/shared/ocp/ocp-plan.js +567 -0
  263. package/neural-interface/public/shared/ocp/ocp-questions.js +626 -0
  264. package/neural-interface/public/shared/ocp/ocp-render.js +437 -63
  265. package/neural-interface/public/shared/ocp/ocp-send.js +358 -0
  266. package/neural-interface/public/shared/ocp/ocp-state.js +177 -0
  267. package/neural-interface/public/shared/ocp/ocp-stream.js +208 -0
  268. package/neural-interface/public/shared/ocp/ocp-styles.js +372 -41
  269. package/neural-interface/public/shared/ocp/ocp-tabs.js +934 -466
  270. package/neural-interface/public/shared/ocp/ocp-trace.js +175 -0
  271. package/neural-interface/public/shared/ocp/ocp-ws.js +16 -2
  272. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +646 -0
  273. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +81 -0
  274. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-gauge.js +332 -0
  275. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +437 -0
  276. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +632 -0
  277. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +1369 -0
  278. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +443 -0
  279. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +519 -0
  280. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1475 -0
  281. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +627 -0
  282. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +337 -0
  283. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +418 -0
  284. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +2815 -0
  285. package/neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js +313 -0
  286. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +367 -0
  287. package/neural-interface/public/shared/storage.js +28 -5
  288. package/neural-interface/public/shared/styles.css +43 -3
  289. package/neural-interface/public/shared/ui-automation-studio.js +5 -32
  290. package/neural-interface/public/shared/ui-claude-panel.js +216 -62
  291. package/neural-interface/public/shared/ui-cost-widget.js +55 -34
  292. package/neural-interface/public/shared/ui-explorer.js +40 -21
  293. package/neural-interface/public/shared/ui-file-explorer.js +48 -28
  294. package/neural-interface/public/shared/ui-image-gallery.js +45 -21
  295. package/neural-interface/public/shared/ui-link.js +105 -51
  296. package/neural-interface/public/shared/ui-navbar.js +82 -37
  297. package/neural-interface/public/shared/ui-opencode-panel-v2.js +15 -0
  298. package/neural-interface/public/shared/ui-sessions.js +189 -0
  299. package/neural-interface/public/shared/ui-sidepanel-layout.js +55 -9
  300. package/neural-interface/public/shared/ui-sidepanel-tray.js +79 -11
  301. package/neural-interface/public/shared/ui-skills.js +5 -30
  302. package/neural-interface/public/shared/ui-terminal.js +232 -41
  303. package/neural-interface/public/shared/ui-tooltip.js +62 -18
  304. package/neural-interface/public/shared/ui-whiteboard.js +135 -91
  305. package/neural-interface/server.js +1300 -223
  306. package/package.json +3 -2
  307. package/setup.js +12 -1
package/CHANGELOG.md CHANGED
@@ -1,212 +1,203 @@
1
- ## THIS IS A NEW CHANGELOG START FRESH ##
1
+ # Project Changelog
2
+
3
+ ## 2026-05-13
4
+
5
+ ### Fixed — Sidepanel Pills Open Blank Session After Server Restart
6
+ - **Pill click landed on "New chat" instead of resuming the underlying Claude session** — `loadConfig()` in `neural-interface/public/shared/ui-claude-panel.js` detected a server-bootId mismatch and `removeItem`'d `STOR.tabs` / `STOR.tabsLegacy` / `STOR.session` outright. Pills (rendered by `ui-sidepanel-tray.js`) read those same keys but render before the panel inits, so they survived the restart visually — but the first pill click triggered `toggleClaudePanel` → `loadConfig` → wipe → `restoreTabs` found nothing → fell through to `createTab(null, 'New chat')`.
7
+ - Replaced the wipe with a new `_scrubStaleTabState(key)` helper added above `restoreTabs()`. On bootId mismatch it now **preserves** resumable identity (`id`, `sessionId`, `label`, `project`, `model`, `effort`) and **drops** runtime-only state that would lie post-restart (`running` / `_wasRunning`, `queue` / `queuePaused`, `planMode` / `planFilePath`, `sessionCost` — re-synced via `syncSessionCosts()`). Tabs with no `sessionId` and only a default label ("New chat", "New session", "Claude") are pruned; the legacy `STOR.session` single-sessionId key is left intact.
8
+ - Codex and OpenCode panels never had a bootId wipe, so their pills already worked across restarts — Claude was the lone outlier.
9
+
10
+ ## 2026-05-12
2
11
 
3
- ## 2026-05-03
4
-
5
- ### Fixed — OpenCode Sidepanel: Auto-Recovery for Kimi/OpenCode `question` Tool Deadlock
6
-
7
- - **Symptom** — In the OpenCode sidepanel with `kimi-for-coding/k2p6` (and likely other non-Anthropic providers via OpenCode 1.14.30), answering an interactive question card (e.g. `/synabun changelog`'s Save/Edit/Cancel prompt, or any skill that calls the OpenCode `question` tool) hung the session indefinitely. Diagnostic from `~/.local/share/opencode/log/2026-05-01T235721.log` (session `ses_2144a6db9ffenIy1ILBw42mMeg` / question `que_debd75e030018zoXNsm4WhqaPC`): `service=question … asking` + `bus type=question.asked publishing` at 03:17:33 → user-answer `POST /question/<id>` returned **200 in 24 ms** at 03:21:16 → **12 minutes of total log silence** (no `service=llm providerID=kimi-for-coding modelID=k2p6 … stream` follow-up, no `tool.registry status=completed`, no bus events, no session.processor activity) → user clicked Stop at 03:33:23 (`session.prompt cancel` / `error=Aborted process`). OpenCode-side stall: HTTP layer accepted the answer but `session.processor` never resumed the LLM stream.
8
- - **Fix — sidepanel-side watchdog + auto-recovery** in `neural-interface/public/shared/ocp/ocp-tabs.js`. Single-file change.
9
- - **`QUESTION_REPLY_WATCHDOG_MS = 75000`** — generous enough for slow tool-follow-up TTFT (typically 1–3 s) but recovers fast enough that the user isn't burned (previous behavior: 12-min stall before manual Stop).
10
- - **`buildAnswerSyntheticMessage(questions, answers)`** — converts the question card's `pendingAnswers` object into a human-readable string. Single answer → bare value; multi-answer → `Here are my answers:\n- Q: A\n…`; multi-select arrays joined with `, `.
11
- - **`scheduleQuestionReplyWatchdog(tab, syntheticMessage)`** — sets `tab._questionReplyWatchdogId` setTimeout + caches the synthetic on `tab._questionReplyAnswerSnapshot`. Both transient (NOT in `saveTabs` serialization list — auto-discarded on reload). Idempotent: re-scheduling clears the prior timer.
12
- - **`clearQuestionReplyWatchdog(tab)`** — idempotent timer-clear + snapshot reset. Wired into 4 sites: `closeTab`, `sendMessage` start (after `clearPlanFinalizationWatchdog`), `abortTab` (after `clearPlanFinalizationWatchdog`), and `handleSSEEvent` immediately after `tab._lastEventAt = Date.now()` — that single insertion catches every "LLM is alive" signal (`message.part.delta`, `message.updated`, `message.completed`, `session.idle`, `session.error`, `tool.start`, `tool.result`, etc.) without per-handler hooks.
13
- - **`executeQuestionReplyRecovery(tab, syntheticMessage)`** async — switches back to the originating tab if the user navigated away (`_tabs.indexOf(tab)` + `switchTab(idx)` since `sendMessage` operates on `activeTab()`). Renders banner ("Model didn't resume after answering. Auto-recovering — resending your answer as a regular message…"), rejects pending send request, resets `running`/`turnStartedAt`/turn state, then `await requestWs('message:abort', { sessionId }, 5000)` to settle the abort server-side BEFORE `await sendMessage(syntheticMessage)` — prevents a late `session.idle` from the abort racing into the new turn and flipping `tab.running` back to false.
14
- - **Wired into `renderOpencodeQuestion`'s `question:reply` `.then(resp)`** success branch — schedules watchdog only when `resp.status < 400` (don't watchdog a failed reply). Anthropic / Qwen flows emit `message.part.delta` within ~1 s of `question:reply`, clearing the watchdog before it ever fires; only the Kimi-style stall reaches the recovery path.
15
-
16
- ### Fixed — OpenCode Sidepanel MCP Profile Dropdown Label Sync
17
-
18
- - **`populateProfileDropdown()` missing label update** — `neural-interface/public/shared/ocp/ocp-panel.js:1405`. When the `mcp:profile-changed` event fired (e.g., from MCP profile action='set'), the dropdown menu items were rebuilt with correct selection state, but the visible label itself was never updated. This left the OpenCode sidepanel showing a stale profile name while the MCP profile had changed.
19
- - **Fix** — Added 4 lines inside `populateProfileDropdown(dd)` immediately before the `for (const p of _mcpProfiles)` rebuild loop: `const matched = _mcpProfiles.find(p => p.id === _currentProfile); lbl.textContent = matched ? matched.label : _currentProfile; dd.classList.add('has-value'); dd._value = _currentProfile;`. The dropdown label now stays in sync with profile changes via the MCP tool, sidepanel API, or file reload.
20
-
21
- ## 2026-05-02
22
-
23
- ### Fixed — OpenCode Sidepanel: Read-Only Tools After Plan→Build / Plan→Chat Transition (Qwen + non-Anthropic models)
24
-
25
- - **Symptom** — In OpenCode plan mode with Qwen 3.6 Plus (and likely any non-Anthropic provider), clicking "Continue with implementation" on the `PLAN COMPLETE` card put the panel into build mode but the model only saw read-only tools (`read`/`glob`/`grep`/`task`/`webfetch`/`todowrite`/`skill`/`question` + MCP). It then hallucinated `Bash` calls and got `Model tried to call unavailable tool 'Bash'` repeatedly. The missing tools matched exactly the `{write, edit, patch, bash}` set the panel disables in plan mode.
26
- - **Root cause** — `neural-interface/public/shared/ocp/ocp-tabs.js:1257`. Plan-mode `sendMessage` calls send `body.tools = { write:false, edit:false, patch:false, bash:false }` as belt-and-suspenders against OpenCode sometimes ignoring `mode:plan`. OpenCode's SDK persists those per-message restrictions at the **session level** — subsequent build/chat turns that omitted `body.tools` inherited the disable. `setTabMode('build')` (post-plan card) and `setTabMode('chat')` (mode toggle) flipped the UI and the `body.mode` field, but the underlying tools remained disabled, so the build/chat agent never got its mutating tools back.
27
- - **Fix** — Mirror the disable: when `tab.mode !== 'plan'`, explicitly send `body.tools = { write:true, edit:true, patch:true, bash:true }` to override the persisted restriction. Symmetric, idempotent, and applies to both build and chat (covers post-plan-card, mode-toggle, and Tab-cycle paths). No new state — each `sendMessage` decides based on the live `tab.mode`.
28
-
29
- ### Fixed — Sidepanel Tray Pills Hidden Until Toolbar Click (Cross-WindowId Orphaning)
30
-
31
- - **Fresh browser tabs saw zero pills despite real session history** — `neural-interface/public/shared/ui-sidepanel-tray.js` only consulted the *current* `windowId`-scoped storage key (`synabun-{provider}-tabs-{windowId}`). Each new browser tab generates a fresh `windowId` via `sessionStorage`, so the placeholder system found nothing and rendered nothing — even though the user had ~70 OpenCode / ~14 Codex / 10 Claude meaningful sessions in `data/ui-state.json` under prior `windowId`-scoped keys. To see any pill, the user had to click the provider's toolbar icon to force `ensurePanel()` → `restoreTabs()` → render. Single-area fix in `ui-sidepanel-tray.js`:
32
- - **`storage.keys()` accessor** — Added in `neural-interface/public/shared/storage.js` so the tray module can enumerate the in-memory cache for cross-window discovery (`return Object.keys(_cache)`).
33
- - **`providerPayloads(provider)` (replaces `providerPayload`)** — Returns an array. Reads the current windowId's key first; if it has no meaningful tabs (per provider's state predicate), falls back to `pickMostRecentMeaningfulPayload(provider, currentKey)` which scans `storage.keys()` in *reverse* (insertion-order ≈ recency) for the freshest non-empty `synabun-{provider}-tabs-*` key whose tabs satisfy `hasClaudeState` / `hasCodexState` / `hasOpenCodeState`. Legacy single-session keys (`synabun-claude-panel-session`, `synabun-codex-panel-thread`) still merge in for back-compat.
34
- - **`meaningfulPills(provider)` with dedupe** — Flattens every payload's tabs and dedupes by `cp:{sessionId}` / `cxp:{threadId}` / `ocp:{sessionId}` so the same session in multiple windowIds renders only once. Iteration order biases toward current windowId.
35
- - **`migrateSourceToCurrentWindow(provider, sourceKey)`** — On placeholder pill click, the source key's data is moved into the current windowId's key BEFORE `toggleXxxPanel()` runs. Each panel's window-scoped `restoreTabs()` then picks it up with `saved.id` preserved, so the post-toggle `emit('xxx-panel:show', { tabId })` switches to the clicked tab. If current windowId already has meaningful tabs, the source's meaningful tabs are appended (deduped); otherwise the source replaces wholesale.
36
- - **Pill carries `dataset.sourceKey`** — `createPlaceholder` now stamps `pill.dataset.sourceKey = payload.key`. Click handler passes it to `openProvider(provider, id, payload.key)`.
37
-
38
- ### Added — Sidepanel Session Lifecycle: Kill-on-Close + WindowId Storage GC
39
-
40
- - **`POST /api/sidepanel/kill-session` server endpoint** — `neural-interface/server.js` accepts `{ provider, windowId, sessionId, threadId }` and force-kills the backend session immediately rather than waiting for the orphan grace timer:
41
- - **claude** — `_orphanedProcs.get(_orphanKey(windowId, sessionId))` → `clearTimeout(orphan.killTimer)` + `orphan.kill()` + delete from map + `_releaseSessionLock(sessionId)`.
42
- - **codex** — `_codexOrphanedProcs.get(_codexOrphanKey(windowId, sessionId))` → `clearTimeout` + `orphan.kill()` + delete from map.
43
- - **opencode** — `ocpProxy('DELETE', '/session/{sessionId}')` because OpenCode sessions live on the OpenCode SDK server, not in an orphan map. Returns `killed:true` only on 2xx.
44
- - **Placeholder pill X-button now calls kill-session before storage delete** — `ui-sidepanel-tray.js`: new `killBackendSession(provider, tab, sourceKey)` POSTs to the endpoint, with `windowIdFromKey()` extracting the windowId from `synabun-{provider}-tabs-{windowId}`. Best-effort: empty catch on the fetch so a network/404 still removes the storage entry. Wired into the close handler before `writeProviderPayload(...)`.
45
- - **WindowId registry GC for OpenCode** — `neural-interface/public/shared/ocp/ocp-tabs.js` adds `STOR.windowRegistry = 'synabun-ocp-windows'` (JSON map of `windowId → epoch ms`). New `_updateOcpWindowRegistry()` runs from `saveTabs()` and at module load. New `_cleanStaleOcpWindows()` runs at module load: drops registered windowIds idle > 24h, plus an "unregistered sweep" that drops any `synabun-ocp-tabs-{wid}` whose `wid` isn't in the registry at all (covers legacy entries from before the registry existed). Both passes skip the current windowId so module-load ordering is safe.
46
- - **WindowId registry GC for Codex** — Mirrored shape in `neural-interface/public/shared/cdx/cdx-icons.js` (added `STOR.windowRegistry = 'synabun-codex-panel-windows'`) and `neural-interface/public/shared/cdx/cdx-tabs.js` (`_updateCodexWindowRegistry()` + `_cleanStaleCodexWindows()` with the same 24h staleness + unregistered-sweep pattern, called from `saveTabs()` and at module load).
47
- - **Result on first run after deploy** — Server-side `data/ui-state.json` cleanup: ~70 `synabun-ocp-tabs-*` orphan keys → 0; ~14 `synabun-codex-panel-tabs-*` → 0; Claude tab keys pruned to 4 live registered windows (within 24h). Going forward, every `saveTabs()` registers the current windowId, so subsequent sessions are protected by the 24h timestamp grace.
48
-
49
- ### Changed — Claude Panel WindowId GC Tightened
50
-
51
- - **Unregistered-sweep arm added to `_cleanStaleWindows`** — `neural-interface/public/shared/ui-claude-panel.js` previously only dropped registered windowIds idle > 24h. Extended to also walk `storage.keys()` and remove any `synabun-claude-panel-tabs-{wid}` whose `wid` isn't present in `synabun-claude-panel-windows` at all (skips current windowId via early `continue`). Catches legacy entries written before the registry existed and orphans whose registry record was lost.
52
- - **Module-load GC + registry update** — Added `_cleanStaleWindows(); _updateWindowRegistry();` calls at module load (immediately after the function definitions, before `restoreTabs()`'s definition). Previously GC only ran inside `restoreTabs()`, which runs lazily inside `ensurePanel()` — meaning the cleanup never fired if the user never opened the Claude panel that session. Now the registry stays accurate and stale keys get pruned on every page load.
53
-
54
- ## 2026-05-01
55
-
56
- ### Added — Notifications Drawer + Ruleset Version Alerts
57
-
58
- - **`neural-interface/templates/ruleset-versions.json` manifest** — New file declaring per-format version + summary + updatedAt for the five condensed rulesets (`claude`, `codex`, `cursor`, `generic`, `gemini`). Version-bump-on-change model: bump only when the relevant section in `CLAUDE-template.md` ships a meaningful update; whitespace edits don't need a bump. Seeds all five formats at `1.0.0` with summary "Initial tracked version".
59
- - **`GET /api/claude-code/ruleset/versions` endpoint** — `neural-interface/server.js` exposes per-format fingerprints. Manifest entry (when present) becomes `fingerprint: "v:<version>"`; when missing, server falls back to `"h:<sha1[0..16]>"` of the section text extracted via the existing `_RULESET_MARKERS` map (mirrors the markers used by `/api/claude-code/ruleset`). Cached by `max(template-mtime, manifest-mtime)` so the disk read happens at most once per change.
60
- - **Updates tab in the Notifications drawer** — `neural-interface/public/shared/ui-sessions.js` adds a fifth tab (now first in the bar): `'updates'`. Shows ruleset / SynaBun / CLI-tool updates as cards grouped by kind, with `Open & copy` (acts on the update — opens Settings with the right section deep-linked) and `Mark as read` buttons. New `computeUpdates()` derives the list by comparing live data against `localStorage["synabun:notifications:acked-v1"]`. New `renderUpdatesTab()` / `renderUpdateCard()` / `handleUpdateAction()` / `handleUpdateAck()` / `refreshRulesetVersions()` / `ackRulesetUpdate()` / `ackSynabunUpdate()` / `ackToolUpdate()` helpers. Module imports `getSynabunUpdateData()` / `openSynabunUpdateModal()` from `ui-update.js`, `getToolUpdateData()` / `runToolUpdate()` / `TOOL_LABELS` from `ui-tool-updates.js`, and `openSettingsModal()` from `ui-settings.js`. New `RULESET_LABELS` map gives each format a display title, paste-target file (`CLAUDE.md` / `AGENTS.md` / `.cursorrules` / `GEMINI.md`), and Settings section ID for the deep-link. Drawer auto-defaults to the Updates tab when there is at least one unread item, otherwise falls back to Sessions.
61
- - **First-run seeding** — On the very first load with no acked state in localStorage, the engine seeds every ruleset's `_acked.rulesets[fmt]` to its current fingerprint. Existing users don't get a flood of "new version" alerts on day one — they only see notifications when a version genuinely bumps after their first visit.
62
- - **Navbar count badge for unread updates** — `neural-interface/public/shared/html-shell.js` adds a `<span class="count-badge" id="titlebar-notifications-count" hidden>` inside `#titlebar-sessions-btn`. `neural-interface/public/shared/ui-navbar.js` listens for the new `notifications:updated` event (dispatched by `ui-sessions.js`'s `emitNotificationsUpdate()`), writes the count, toggles `hidden`, and updates the button's tooltip to `Notifications · N updates`. Bootstraps via `fetchNotificationsForBadge()` on init plus a 5-minute polling fallback so the badge stays accurate without ever opening the drawer.
63
- - **Read-only accessors on the existing update modules** — `getSynabunUpdateData()` exported from `ui-update.js`, `getToolUpdateData()` + `runToolUpdate(key)` exported from `ui-tool-updates.js`. The drawer reads cached state from those modules instead of duplicating the polling logic, so the existing top-right SynaBun update button and per-CLI-tool toolbar badges keep working unchanged and the drawer just mirrors them.
64
- - **Updates-tab styling** — `neural-interface/public/shared/styles.css` adds `.sm-card-update`, `.sm-update-icon`, `.sm-update-title`, `.sm-update-version`, `.sm-update-meta`, `.sm-update-actions`, `.sm-btn-ghost`, `.sm-btn-primary`, `.sm-badge-warning`, plus an orange-pill `#titlebar-sessions-btn .count-badge` for the navbar.
65
- - **Bell + per-card icons** — `ICON_BELL`, `ICON_RULESET`, `ICON_PACKAGE`, `ICON_UP_ARROW` SVG constants added to `ui-sessions.js`. Bell is the empty-state and tab icon; the rest are per-card kind icons.
66
-
67
- ### Changed — Session Monitor Renamed to Notifications Drawer
68
-
69
- - **Window identity rewritten** — `neural-interface/public/shared/ui-sessions.js` `<h3>` reads `Notifications`, comment header rewritten to describe the window as the centralised notification surface, default `_activeTab` is now `'updates'` so the drawer opens to the most relevant tab. Tooltip on `#titlebar-sessions-btn` in `neural-interface/public/shared/html-shell.js` reads `Notifications`. Tutorial step 7 in `neural-interface/public/shared/ui-tutorial-steps.js` now labels the area `Notifications` with sublabel `Sessions, alerts, and ruleset / CLI updates`. CSS section comment in `neural-interface/public/shared/styles.css` renamed to `Notifications drawer`. Internal CSS classes (`.sm-*`, `.session-monitor-panel`), DOM IDs (`#titlebar-sessions-btn`, `#session-monitor-panel`), and event names (`session-monitor:closed`) kept as-is for backward compat with the rest of the app.
70
-
71
- ### Changed — Removed Dedicated Stale-Loops Alert Button (Titlebar)
72
-
73
- - **Button + wiring deleted** — `#titlebar-loops-alert-btn` markup removed from `neural-interface/public/shared/html-shell.js`, the entire `if (loopsAlertBtn) { ... }` wiring block (~17 lines) removed from `neural-interface/public/shared/ui-navbar.js`, and the `.bar-loops-alert-btn` / `:hover` / `.count-badge` rules (~22 lines) removed from `neural-interface/public/shared/styles.css`. `openLoopsTab` dropped from the navbar import set; the export itself is kept in `ui-sessions.js` as a small public API surface.
74
- - **Loops still surface via the drawer** — Stale loops continue to feed the `loops:updated` event and the existing `Loops` tab badge in the Notifications drawer. The 30s `fetchLoopsForBadge()` polling is preserved in `ui-navbar.js` so the Loops tab badge stays accurate without ever opening the drawer.
75
-
76
- ### Changed — Titlebar Icon Polish (Notifications, Image Gallery)
77
-
78
- - **Notifications button now uses a bell glyph** — `#titlebar-sessions-btn` in `neural-interface/public/shared/html-shell.js` swapped from the legacy "tray with downward arrow" multi-path SVG to a clean two-path bell (24×24 viewBox, stroke-width 1.6, line-cap/join round). Count-badge `<span>` is unchanged so the orange `N` overlay still works.
79
- - **Image Gallery button now uses the universal photo glyph** — `#titlebar-gallery-btn` swapped from a complex multi-path bag/basket-style icon (read as cluttered next to its cleaner siblings) to the universal photo icon: rounded rectangle frame, small circle (sun), diagonal mountain line. 24×24 viewBox, stroke-width 1.5, matches the rest of the navbar.
80
-
81
- ### Fixed — OpenCode Sidepanel Plan-Mode Post-Plan Card Silent Stall (Deepseek + non-Anthropic models)
82
-
83
- - **Plan rendered but no `PLAN COMPLETE` card** — In OpenCode plan mode with non-Anthropic models (e.g. Deepseek v4 Pro), the plan would stream successfully but the post-plan action card (Edit / Continue / Compact) never appeared. The agent activity dock stayed "active" with no spinner, forcing the user to press Stop. Root cause: `showPostPlanUI(tab)` was only triggered from `tool.start` ExitPlanMode (Anthropic-only), `session.idle`, `session.status:idle`, and the WS-resolve fallback. When the planning subagent's `session.idle` fired but the parent never finalized (parent never received the task `tool.result`), none of the trigger sites fired. Single-file fix in `neural-interface/public/shared/ocp/ocp-tabs.js`:
84
- - **`maybeShowPostPlanUI(tab, {source, forceRecapture})` helper** — Idempotent post-plan trigger gated on `mode === 'plan'`, `!showPostPlanActions`, and `planContent.length >= 40`. Centralizes the decision so non-`ExitPlanMode` terminal paths don't render an empty card.
85
- - **8-second watchdog** — `schedulePlanFinalizationWatchdog(tab)` armed when `recordAgentChildEvent` records a subagent `session.idle`. If the parent's terminal signal doesn't fire by then, the watchdog force-finalizes (`tab.running = false`, `removeThinking`, `finalizeStreamingMessage`) and surfaces the post-plan card. `clearPlanFinalizationWatchdog(tab)` runs in every parent terminal path: WS-resolve fallback, `abortTab`, background-event idle, `message.completed`, `session.status:idle`, `session.idle`, `session.error`, `setTabMode` leaving plan.
86
- - **ExitPlanMode mirror in `message.part.updated`** — Modern OpenCode SSE emits tools via `message.part.updated`, not legacy `tool.start`. Mirrored the ExitPlanMode/`plan_exit` detection (sets `_exitPlanDetected`, captures `toolInput.plan`, calls `showPostPlanUI`) so Anthropic models on modern OpenCode no longer rely solely on the `session.idle` fallback.
87
- - **`message.completed` post-plan trigger** — Both rendered and fallback branches now call `maybeShowPostPlanUI` after clearing `tab.running`. Fixes the legacy-event silent stall.
88
- - **`session.error` / `error` post-plan recovery** — When an error toast fires after plan content was already streamed, `maybeShowPostPlanUI` is called with `forceRecapture: true` so the user can still continue, edit, or retry without re-planning from scratch.
89
- - **`sendMessage` catch-block recovery** — On non-abort, non-(timeout-with-recent-SSE) failures, `maybeShowPostPlanUI` runs with `forceRecapture: true` so a 4xx/5xx WS rejection after a successful plan stream doesn't strand the user.
90
- - **Per-turn state reset** — `sendMessage` now resets `_exitPlanDetected` and `_pendingPostPlanCheck` (and clears the watchdog) at turn start. Previously these were only cleared on entering plan mode, breaking multi-turn plan sessions where the second turn's fallbacks would short-circuit on `!_exitPlanDetected`.
91
- - **Watchdog ID not persisted** — Verified `_planFinalizationWatchdogId` is excluded from `saveTabs` serialization (only listed transient fields are persisted).
92
-
93
- ## 2026-04-30
94
-
95
- ### Fixed — Duplicate Hook Entries in `.claude/settings.json` (Claude Code Hook Installation)
96
- - **Every Claude Code hook fired twice** — `addHookToSettings()` in `neural-interface/server.js` keyed dedup on exact command-string match, but `hookCommandString()` produces two valid forms for the same script depending on `targetProjectPath`: relative `node hooks/claude-code/<script>` for the SynaBun project itself, absolute `node "/abs/path/<script>"` otherwise. Different install flows (global cascade vs per-project) wrote different forms to the same `<project>/.claude/settings.json`, both passed the exact-string check, and accumulated. Symptom: every Stop-hook block reason was emitted twice (the user saw two near-identical "SynaBun Loop: Before this iteration ends..." Stop-hook errors per turn), and every recall-context block injected into `UserPromptSubmit` showed up twice. The two invocations also raced on the `pending-remember`/loop flag retry counters, so `MAX_RETRIES` could be hit in a single turn.
97
- - **Variant-aware dedup with self-healing sweep** — New `hookCommandVariants(scriptName, targetProjectPath)` returns the set of every valid command-string form for a given script (relative + absolute when the target is `PACKAGE_ROOT`, absolute only otherwise). `addHookToSettings()` now walks `settings.hooks[def.event]` BEFORE pushing and removes any pre-existing entries whose command is in that set, keeping at most the first canonical match. Idempotent on re-install.
98
- - **Startup self-heal across global + every registered project** — New `dedupeAllSettingsHooks()` runs in the existing startup block (`server.js`, just before the orphaned-loop cleanup). Walks `~/.claude/settings.json` and every `<projectPath>/.claude/settings.json` in the registered-projects list, applies the same variant-aware sweep across every event in `HOOK_SCRIPTS`, rewrites only when something changed. Logs `Hooks: deduped N stray entr[y/ies] across M settings file(s)` so the repair is visible. Cleans up existing damage from prior versions of the install path without user action.
99
-
100
- ### Added — Session Monitor Loops Tab + Title-Bar Stale-Loop Alert
101
- - **New `Loops` tab in Session Monitor** — `neural-interface/public/shared/ui-sessions.js` adds a fourth tab between `Agents` and `Health` with `ICON_LOOP` glyph. Module-level `loops`/`staleLoopCount` state. Tab badge turns warning-orange when `staleCount > 0`. `_activeTab` enum extended with `'loops'`.
102
- - **Per-loop card with state classification and live stalled timer** — `renderLoopCard()` shows a `Running` / `Finished` / `Orphaned` / `Stale` badge (driven by `loopStateClass()`/`loopStateLabel()`), a `${current}/${total}` iteration counter, a live `Stalled <Xm Ys>` timer (`now - lastIterationAt`, gray under 5min, red `.sm-loop-stalled-hot` at ≥5min), task preview, started-at relative time, terminal short-id with alive/dead pulse, and a per-loop `Stop` button that calls `POST /api/loop/stop` with that `terminalSessionId`. The stalled cell carries `data-stalled-ms`/`data-fetched-at` so the 1s `_stalledTickerTimer` can re-write `formatStalled(...)` without a server round-trip. Ticker only runs while `_activeTab === 'loops'` (`startStalledTicker()`/`stopStalledTicker()`).
103
- - **Bulk action bar** — `Clear stale` (calls new `POST /api/loop/cleanup`, deletes only `active === false` files; disabled when zero stale) and `Clear all` (calls existing `POST /api/loop/stop` with empty body to kill PTY + close browser tab + delete file for every loop, with a `confirm()` prompt because it kills running work).
104
- - **Title-bar alert button next to the cost value** — `neural-interface/public/shared/html-shell.js` adds `#titlebar-loops-alert-btn` immediately before `#titlebar-cost-btn` in the `bar-right` group, hidden by default. `neural-interface/public/shared/ui-navbar.js` imports the new `openLoopsTab` / `fetchLoopsForBadge` exports, wires click → `openLoopsTab()`, subscribes to the new `loops:updated` event (toggles `hidden` and writes `#titlebar-loops-count`), and bootstraps via `fetchLoopsForBadge()` plus a 30s polling fallback so the badge stays accurate even when the Session Monitor was never opened (the WS only connects when the panel opens).
105
- - **Backend endpoints** — `GET /api/loop/list` returns the full inventory of `data/loop/*.json` (active + inactive) with classified `state`, `stalledMs`, `terminalAlive`, plus a `staleCount` field. `POST /api/loop/cleanup` deletes only `active === false` files (no PTY/browser side-effects since they're leftovers); corrupt JSON is treated as stale and removed too. Both endpoints filter out `pending-*.json` and `active-pointer.json` from the scan so the loop pointer file isn't classified as a loop.
106
- - **`session:loops` WebSocket broadcast** — New `scanLoopInventory()` helper shared by the GET endpoint and a periodic polling loop that broadcasts on hash change (drops minute counters from the hash so it doesn't churn every tick). `handleSessionMonitorWebSocket()`'s `session:init` payload now carries `loops` + `staleCount` alongside `sessions`/`leaks`. `ui-sessions.js` `handleWsMessage()` updates state and re-renders on `session:loops`, plus emits `loops:updated` so the title-bar badge reflects WS pushes immediately when the panel is open.
107
- - **CSS** — `neural-interface/public/shared/styles.css` adds `.sm-loop-running/-finished/-orphan/-stale` card borders, `.sm-loop-badge` color variants, `.sm-loop-iter` mono pill, `.sm-loop-stalled[-hot]` color states, `.sm-loops-actionbar` sticky-bottom action bar, and `.bar-loops-alert-btn` orange icon styling with a red `count-badge` overlay matching the existing `titlebar-trash-btn` pattern.
108
-
109
- ### Fixed — OpenCode Sidepanel Tables Not Rendering (Markdown Pipeline)
110
- - **Root cause: missing CSS, not parser** — `marked@14` in `neural-interface/public/shared/ocp/ocp-render.js` correctly produced GFM `<table>/<thead>/<tbody>/<tr>/<th>/<td>` HTML (`gfm: true` set, `<table>` whitelisted in `renderAssistantMarkdown()`'s `hasContent` check), but `neural-interface/public/shared/ocp/ocp-styles.js` had ZERO rules targeting any table element. Default browser tables on the panel's translucent dark background produced no visible borders and dark-on-dark text — effectively invisible. The Claude sidepanel (`ui-claude-panel.js:677-687`) already had table styles, which is why tables rendered correctly there but not in OpenCode.
111
- - **Markdown tables CSS block** — Added `.ocp-msg-assistant .ocp-table-wrapper` (`overflow-x: auto`, rounded border, dark bg), `table` (`border-collapse`, monospace 11px, `min-width: 100%`), `thead` background, `th` (bold, padded, bottom border, `white-space: nowrap`), `td` (padded, subtle row separators, `vertical-align: top`), `tbody tr:hover td` highlight, column dividers between adjacent cells, and `th[align]/td[align]` overrides honoring GFM column alignment.
112
- - **`wrapTables()` post-processor** — New helper in `ocp-render.js` wraps every unwrapped `<table>` in `<div class="ocp-table-wrapper">` so wide tables get horizontal scroll without breaking `border-collapse` (you can't put `display: block; overflow-x: auto` directly on the table element). Idempotent via the `parent.classList.contains('ocp-table-wrapper')` guard. Called from `postProcessRenderedHtml()` so initial render, every streaming frame, finalize, and history-replay paths all wrap consistently.
113
-
114
- ### Changed — OpenCode Sidepanel Markdown Polish (Headings, HR, Task Lists)
115
- - **Heading sizing fill-in** — Added explicit `h1` (15px), `h2` (13.5px), and `h4`/`h5`/`h6` (11.5px, semibold, brighter text) rules in `neural-interface/public/shared/ocp/ocp-styles.js`. Previously only `h3` had a size override; the others fell back to browser defaults that didn't match the panel scale.
116
- - **HR gradient line** — `<hr>` now renders as a 1px linear-gradient line (transparent → 10%-white at 30-70% → transparent) so the rare horizontal-rule output looks intentional instead of a stark default browser bar. Note: `renderAssistantMarkdown` still strips `<hr>` to clean up CLI noise; the styling is in place if that filter is later removed.
117
- - **GFM task list checkboxes** — Added `accent-color` and indent rules so `[ ]` / `[x]` checkboxes inherit the panel's off-white accent and render without the default disc bullet, using `:has(> li > input[type="checkbox"])` to scope the tweak to actual task lists only.
118
-
119
- ### Added — MCP Profile Change Sync for Codex/OpenCode Sidepanels
120
- - **Live profile-change broadcast** — `neural-interface/server.js` now normalizes MCP profile names, broadcasts `mcp:profile-changed` over `/ws/sync`, and includes current `presets` plus `synced` results whenever `applyMcpProfile()` changes the active profile.
121
- - **File-driven profile watcher** — New `startMcpProfileWatchers()` watches runtime `active-profile.json` locations, debounces writes from `mcp__SynaBun__profile(action=set)`, and routes external changes through `applyMcpProfile(..., 'file')` so all profile files and CLI env configs stay aligned.
122
- - **Sidepanel profile listeners** — `neural-interface/public/shared/ui-sync.js` forwards `mcp:profile-changed` to the shared state bus, and `cdx-panel.js` / `ocp-panel.js` now use `applyMcpProfileState()` to rebuild dropdown items and selected labels immediately when the AI changes the profile.
123
-
124
- ### Changed — MCP Registry Profile Activation Consistency
125
- - **Registry activation uses the shared profile path** — `/api/mcp/registry/activate` now calls `applyMcpProfile(profileValue, 'registry')` instead of writing only `DATA_HOME/data/active-profile.json`, so settings-driven activation updates every runtime profile file, Codex/OpenCode env sync, and the live sidepanel broadcast through the same code path.
126
-
127
- ## 2026-04-29
128
-
129
- ### Added — Google Search Console MCP Tool Group (30 tools)
130
- - **Full GSC control via browser automation** — New `gsc` tool group registered in `mcp-server/src/tools/gsc.ts` (barrel) plus `mcp-server/src/tools/gsc/{helpers,navigate,inspect,performance,indexing,experience,settings}.ts` (~1300 LOC). 30 tools cover every section of Google Search Console without an API key, using the user's authenticated Google session in the SynaBun-managed Playwright browser.
131
- - **Tools shipped** —
132
- - **Navigation & property:** `gsc_navigate` (33 page enums incl. all 17 enhancement reports), `gsc_property` (list/select/current/add).
133
- - **URL Inspection:** `gsc_inspect_url` (polls up to 30s, returns structured JSON of indexingState/canonicals/lastCrawl/sitemap/robots/enhancements), `gsc_inspect_test_live` (90s poll for live test), `gsc_inspect_request_indexing` (mutating), `gsc_inspect_view_crawled` (html/screenshot/http_response/more_info tabs).
134
- - **Performance:** `gsc_performance_query` (searchType, dateRange preset/custom, dimension, filter chips, comparison, auto-scroll grid, returns totals + table JSON), `gsc_performance_export` (CSV/Excel/Sheets), `gsc_performance_chart_screenshot`.
135
- - **Indexing:** `gsc_pages_report` (totals + reasons + drill-down), `gsc_pages_validate_fix` (mutating), `gsc_videos_report`, `gsc_sitemap` (list/submit/delete/view_errors), `gsc_removals` (list_temporary/list_outdated/list_safesearch/new), `gsc_removals_cancel`.
136
- - **Experience & links:** `gsc_cwv_report`, `gsc_https_report`, `gsc_security_issues`, `gsc_manual_actions` (incl. reconsideration submit), `gsc_enhancements` (any of 17 structured-data reports), `gsc_links_report` (4 sections, auto-pagination), `gsc_links_export`.
137
- - **Settings & meta:** `gsc_settings`, `gsc_crawl_stats`, `gsc_users` (list/add/remove/change_role), `gsc_associations` (Analytics/Merchant/Ads/Play/YouTube/Actions/Chrome Web Store), `gsc_disavow` (download/upload/delete, mutating), `gsc_shopping`, `gsc_extract_table` (generic grid → JSON), `gsc_screenshot`.
138
- - **Locale tolerance** — All selectors prefer stable URL paths (`/inspect`, `/performance/search-analytics`, `/index/pages`, `/sitemaps`, `/removals`, `/links`, `/core-web-vitals/MOBILE`, etc.), material-icon ligature text (`home`, `trending_up`, `search`, `account_tree`), and aria roles. Translated UI strings are matched only as fallback regex with both English and Portuguese variants. Verified against the existing logged-in `sc-domain:synabun.ai` session running in Portuguese.
139
- - **Polling helper** — `pollFor()` in `mcp-server/src/tools/gsc/helpers.ts` evaluates a JS predicate every 400ms (configurable) up to a timeout, returning the first truthy result. Used by every async-loading panel (URL Inspection result render, live-test completion, request-indexing modal, sitemap submission status, validate-fix confirmation, removal submission). Default cap 30s, bumped to 90s for live URL tests.
140
- - **Generic table extractor** — `TABLE_EXTRACTOR_SCRIPT(scope)` walks `[role="grid"]` / `[role="table"]` / `<table>`, emits `{ headers, rows }` with header→cell mapping plus per-row `url` when an anchor is present. Backbone of every report tool.
141
- - **Auth check** — `ensureAuth()` returns `'Not authenticated. Sign into the Google account...'` when the page redirects to `accounts.google.com`, instead of failing on missing selectors.
142
- - **Profile presets** — Added `gsc` to `full` and `browser` presets and added a dedicated `gsc` preset (`git, image, browser, gsc`) in `mcp-server/src/services/profiles.ts`. `VALID_GROUPS` extended with `'gsc'`.
143
- - **Index wiring** — `mcp-server/src/index.ts` imports `registerGscTools`, registers the group via `setToolGroup('gsc', ...)`, exposes the group line in `TOOL_GROUP_INSTRUCTIONS.gsc`, and tracks usage via the `gsc_` prefix in `groupToolPrefixes`.
144
- - **Documentation** — Updated `README.md` (architecture box, MCP-tools heading and total `76 → 106`, added a full GSC section grouping all 30 tools by area, plus a tip about authentication and polling timeouts) and `CLAUDE.md` (architecture line bumped from `72 tools / 9 groups` to `106 / 12` with the locale-tolerance note).
145
-
146
- ## 2026-04-28
147
-
148
- ### Added — OpenCode Model Capability Explainers
149
- - **Capability metadata and inline help** — Extended `CAP_BADGES` in `neural-interface/public/shared/ocp/ocp-tabs.js` with abbreviations and human-readable descriptions for `Reason`, `Tools`, `Vision`, `Audio`, `Video`, `PDF`, `ImgGen`, `TTS`, and `Files`. `populateModelDropdown()` now renders an inline `ocp-cap-help` strip that updates on capability chip hover/focus and active filter state, so users can understand each model capability without a floating tooltip covering the controls.
150
-
151
- ### Fixed — OpenCode Model Selector Filtering and Tooltip Overlap
152
- - **Capability filters no longer stack additively** — Replaced the previous `activeCaps` `Set` / AND matching logic in `populateModelDropdown()` with a single `activeCapKey`, so selecting a capability replaces the previous filter and clicking the active capability clears it. Added an `All` chip and `ocp-dd-empty` clear action for no-result states.
153
- - **Pill explanations no longer overlap the filter row** — Removed `data-tooltip` overlays from capability filter pills and moved their explanations into the dropdown header via `showCapHelp()`, `restoreCapHelp()`, and `bindCapHelp()`. Model-row capability badges still keep compact `data-tooltip` explanations, positioned above the badge instead of over the pill bar.
154
-
155
- ### Changed — OpenCode Model Selector Visual Refresh
156
- - **Neutral OpenCode-style selector surface** — Restyled `#ocp-model-dd .ocp-dd-menu` in `neural-interface/public/shared/ocp/ocp-styles.js` with a darker glassy surface, restrained monochrome capability chips/badges, neutral favorite states, a wider 520px model menu, and grid-based model rows for model name, capabilities, and favorite star.
157
- - **Subtle selector motion** — Added `ocp-model-menu-in` and `ocp-cap-help-in` animations, plus lightweight hover/active transitions for capability chips and model rows, making the selector feel fresher without the earlier rainbow-heavy visual treatment.
158
-
159
- ### Fixed — SynaBun MCP Auto-Heal After Claude Code Updates
160
- - **MCP entry survives CC updates** — Claude Code updates (especially when the installer runs as root via `sudo npm i -g @anthropic-ai/claude-code`) could wipe `~/.claude.json` or strip `mcpServers.SynaBun`, leaving the side panel and CLI with SynaBun MCP disabled until the user manually re-registered it via the Neural Interface setup window. Added `healSynaBunMcp()` to `hooks/claude-code/session-start.mjs` that runs on every session start: reads `~/.claude.json`, ensures `mcpServers.SynaBun = { type: 'http', url: 'http://localhost:3344/mcp' }`, writes back only if missing or incorrect (idempotent on already-correct state). Heal events are logged to `data/compact-debug.log` with the `MCP-HEAL` prefix. Wrapped in `try/catch` — non-critical failures cannot block session start.
161
-
162
- ### Changed — OpenCode Capability Help Strip Polish
163
- - **No layout jump on capability pill hover** — `.ocp-cap-help` strip in `neural-interface/public/shared/ocp/ocp-tabs.js` now stays mounted at all times, falling back to the `ALL_CAP_HELP` ("All — Showing every visible model.") badge when no pill is hovered and no filter is active. Replaced the prior `capHelp.hidden = true/false` toggling — which caused model rows below to shift on every mouseenter/leave — with a persistent strip and `applyCapHelp()` helper that tracks `lastHelpKey` to suppress redundant animation restarts.
164
- - **Smooth filter-toggle collapse** — Filter-toggle button (`.ocp-cap-filter-toggle`) now toggles `.ocp-cap-help--collapsed` on the strip instead of hiding it abruptly. CSS in `neural-interface/public/shared/ocp/ocp-styles.js` animates `max-height`, `opacity`, `margin`, `padding`, and `border-width` over 220ms (cubic-bezier 0.2/0.8/0.2/1) for a smooth open/close, and `restoreCapHelp()` only fires on expand.
165
- - **Crossfade-feel on badge text swap** — Added `@keyframes ocp-cap-help-swap` (opacity 0.45 → 1, 1px translateY, 0.4px blur clearing) and `.ocp-cap-help--swap` class. `applyCapHelp()` removes the class, forces reflow via `void capHelp.offsetWidth`, then re-adds it whenever `lastHelpKey` changes, producing a 180ms ease-out fade between badge descriptions instead of an abrupt text flip.
166
- - **Description text wraps to two lines** — `.ocp-cap-help-text` switched from `white-space: nowrap` + ellipsis to `-webkit-line-clamp: 2` + `-webkit-box-orient: vertical` + `white-space: normal`. Long descriptions like "Stronger at multi-step reasoning, planning, and hard problem solving." now wrap fully within the strip's 32-80px height range instead of truncating with an ellipsis.
167
- - **Active filter highlight** — Added `.ocp-cap-help--active` modifier that brightens the strip's border and gradient background when a real capability filter (not "All") is active, giving visual continuity between the pill and the explanation.
168
-
169
- ### Fixed — SynaBun Loop "No active loop" After /clear (Claude Code Hook-Driven Loop)
170
- - **Stop hook stuck blocking with `call \`loop\` action \`update\``** — `mcp-server/src/tools/loop.ts` `resolveSessionId` returned `process.env.CLAUDE_SESSION_ID` without verifying a flag file existed. Under HTTP MCP transport (stateless, no per-call env) and after `/clear` (env stale), the lookup returned null → `loop update` reported `"No active loop"` → `hooks/claude-code/stop.mjs:489-503` re-blocked 3× per iteration with the journal-update nag, then advanced iterations with a stale journal.
171
- - **`resolveSessionId` now verifies before trusting env** — added `isFreshActive()` (45-min staleness rule). New strategy chain: explicit param → verified `CLAUDE_SESSION_ID` (flag must exist + be active) → `SYNABUN_TERMINAL_SESSION` scan → `active-pointer.json` fallback → legacy unclaimed loops → exactly-one-fresh-active fallback.
172
- - **Hook writes `data/loop/active-pointer.json` on every claim** — `writeActivePointer()` / `clearActivePointer()` in `hooks/claude-code/stop.mjs` persist `{sessionId, terminalSessionId, claudeSessionId, updatedAt}` on exact-match, Strategy A, and Strategy B claim paths. Cleared on iteration cap. Bridges env gap between hook (live Claude env) and HTTP MCP daemon (stale env).
173
- - **`cleanupStaleLoops` aware of pointer file** — `hooks/claude-code/shared.mjs` skips `active-pointer.json` from the `.json` flag scan (different schema, would have been wrongly deleted as inactive) and drops the pointer when its target flag no longer exists.
174
- - **MCP daemon rebuild** — recompiled `mcp-server/dist/tools/loop.js` via direct `node node_modules/typescript/bin/tsc` (the `node_modules/.bin/tsc` shim was broken).
175
-
176
- ### Fixed — OpenCode Sidepanel "Edit Plan" Empty Content
177
- - **Robust click handler with multi-layer fallback chain** — Replaced silent-fail `setOnEditPlan` callback in `neural-interface/public/shared/ocp/ocp-panel.js` (previously bailed on `if (!tab.planContent) return`) with a handler mirroring `ui-claude-panel.js`. Chain: `tab.planFilePath` → `extractPlanTextLoose(tab)` → `requestAnimationFrame` retry → `fetchPlanFromMessagesAPI()` (WS `messages:list` proxy) → `/api/latest-plan` disk fallback. Added `setBusy()` opacity/pointer-events on the post-plan card while async work is in flight, `noFile()` rendering a visible `renderErrorMessage` instead of silently bailing, and `materialize()` posting to `/api/create-plan`.
178
- - **Card element threaded through callback** — `renderPostPlanCard()` in `neural-interface/public/shared/ocp/ocp-render.js` now passes the card DOM node to `onEditPlan(card)`; `showPostPlanUI()` in `ocp-tabs.js` forwards as `_onEditPlan(tab, card)` so the click handler can disable/restore the card during async failures.
179
- - **`extractPlanTextLoose(tab)` four-layer scan** — New exported helper in `ocp-tabs.js` ignores the `text.length > 80` and `tab.mode !== 'plan'` guards from the original `capturePlanContent`. Order: cached `tab.planContent` → ExitPlanMode tool card args (parses `.ocp-tool-args` JSON for `input.plan/.markdown/.content/.text`, regex-matched by `/exit\s*plan|plan[\s_-]?exit/i` on `.ocp-tool-name`) → last `.ocp-msg-assistant` text bubble → standalone `.ocp-think-block .ocp-think-content` (reasoning-only output) → Agent/task subagent `.ocp-tool-result`. Last-resort fallback to any short bubble.
180
- - **ExitPlanMode tool input captured at `tool.start`** — `tool.start` handler in `ocp-tabs.js` reads `toolInput.plan || .markdown || .content || .text` and stores it on `tab.planContent` before `showPostPlanUI()`. Falls back to `capturePlanContent()` if input fields empty. The plan text lives in tool args, not the assistant message bubble.
181
- - **Live capture during `message.part.updated`** — Both `text` and `reasoning|thinking|thought` part types now write to `tab.planContent` (length-monotonic) when `tab.mode === 'plan'`, so post-plan extraction has authoritative content even when the bubble rendered into a Thought block. Handles plan agents that emit all output as reasoning.
182
- - **`fetchPlanFromMessagesAPI()` server-side fallback** — In `ocp-panel.js` `setOnEditPlan`, calls `requestWs('messages:list', { sessionId }, 15000)` (existing WS proxy at `server.js:5285` → `GET /session/{id}/message`). Walks the most recent assistant message, concatenates `type === 'text'` parts then `reason|think` parts (length > 40 threshold). Bypasses DOM scraping entirely.
183
- - **`tab._planModeStartedAt` timestamp for disk fallback** — `setTabMode()` stamps `Date.now()` when entering plan mode. `diskFallback()` validates `/api/latest-plan` mtime against plan-mode start (skipped when unset to support pre-existing tabs).
184
-
185
- ### Fixed — OpenCode "Request message:send timed out" False Toast
186
- - **WS request timeout bumped 6 min → 30 min** — `requestWs('message:send', ...)` in `neural-interface/public/shared/ocp/ocp-tabs.js` `sendMessage` was rejecting at 360000ms; Plan agent + subagent runs routinely exceed that. Bumped to 1800000ms.
187
- - **SSE liveness suppresses red error** — `handleSSEEvent()` stamps `tab._lastEventAt = Date.now()` on every event. `sendMessage` catch block detects `/^Request\s+message:send\s+timed out$/i` and, when `Date.now() - tab._lastEventAt < 60000`, suppresses `renderErrorMessage` AND skips the `tab.running = false` / `endTurn` cleanup so SSE can drive completion to `session.idle` naturally.
188
-
189
- ### Changed — OpenCode Plan Mode SDK-Aligned Tool Restrictions
190
- - **`body.mode` + per-message `body.tools` restrictions** — `sendMessage` in `neural-interface/public/shared/ocp/ocp-tabs.js` now sets `body.mode = tab.mode` (canonical SDK field per OpenCode `app.modes()`) and, when `tab.mode === 'plan'`, pins `body.tools = { write: false, edit: false, patch: false, bash: false }`. Belt-and-suspenders enforcement so the plan agent cannot even attempt mutating tools.
191
-
192
- ### Changed — OpenCode Plan-Mode Tool Denials Soft-Fail
193
- - **Permission denials render as neutral "Skipped" tile** — `tool.result` handler in `neural-interface/public/shared/ocp/ocp-tabs.js` detects plan-mode permission errors via `/permission|not allowed|forbidden|read.?only|disabled|disallow|file_edit|edit not|write not|patch not|bash not/i`, renders the tool card with status `complete` and message "Skipped — plan mode is read-only" instead of the red error badge.
194
-
195
- ### Fixed — Restart Click Failed After Native-Module Crash During Shutdown
196
- - **`Restart Server` from apps dropdown left no server running** — `/api/server/restart` in `neural-interface/server.js:1631` exited with sentinel code `75` so the `setup.js` supervisor would respawn. Native module teardown (sqlite / Playwright / node-pty) crashed mid-exit with `mutex lock failed: Invalid argument` (libc++abi → SIGABRT), clobbering the sentinel exit code. The supervisor's `child.on('exit')` in `setup.js:264` saw an abnormal non-75 code, hit the `fail()` + `process.exit(code)` branch, and died — no respawn, shell prompt returned.
197
- - **Marker-file fallback makes restart resilient to crash codes** — `/api/server/restart` now writes `DATA_HOME/restart-requested` via `writeFileSync(RESTART_MARKER, ...)` before deferring shutdown. `setup.js` `child.on('exit')` treats marker presence the same as exit code `75` and respawns regardless of the actual code, then deletes the marker. Added `unlinkSync(RESTART_MARKER)` at the top of `startServer()` to clear stale markers on supervisor startup so a leftover from an unsupervised crash cannot trigger a spurious respawn. The crash-rate limiter (`5` restarts in `60s`) still applies so any crash loop stays bounded.
198
-
199
- ## 2026-04-27
200
-
201
- ### Fixed — OpenCode Sidepanel Plan Mode Subagent Stall
202
- - **Plan mode appeared frozen during `task` subagent runs** — When the OpenCode plan agent dispatched a `task` subagent (e.g. `@explore`), the panel showed no progress for 60-150+ seconds while the subagent worked, leading users to abort. Root cause: `tab.toolActivityChildSessions` (parent task entry → child session id map) was only populated by `recordToolResult()` after the subagent completed, so all child SSE events (`tool.start`, `tool.result`, `message.part.updated`, `session.idle`) failed `findAgentEntryByChildSession()` lookup at `ocp-tabs.js:1570` and were dropped at line 1577. Fix in `neural-interface/public/shared/ocp/ocp-tabs.js`:
203
- - **Top-level `session.created` interceptor** in `handleSSEEvent()` — when `child.parentID` matches a tab's session id, links the first running `task`/`agent` activity entry without `childSessionId` via `linkChildSession()`.
204
- - **Fallback link in `case 'message.part.updated'`** task-tool branch — reads `part.state.metadata.sessionId` when `session.created` arrived late.
205
- - **Fallback link in `case 'tool.start'`** agent-tool branch — reads `event.metadata.sessionId` / `event.state.metadata.sessionId`.
206
- - Once linked, child events route via `findAgentEntryByChildSession` → `recordAgentChildEvent` populates `entry.steps` → `syncAgentChildIndicator` updates thinking indicator with live child tool names instead of stuck "Thinking…".
207
-
208
- ### Fixed — Sidepanel Plan Mode "No File Created" (Claude Code)
209
- - **`ExitPlanMode.input.plan` ignored by post-plan hook** — `hooks/claude-code/post-plan.mjs` previously stored only existing plan files matched against stringified `tool_response`, so when the UI failed to capture, no file ever reached SQLite or disk. Hook now reads the authoritative markdown via `readPlanField(input.tool_input)` / `readPlanField(input.tool_response)`. When no match exists (or the matched file is stale vs `tool_input.plan`), the hook authors `data/plans/YYYY-MM-DD/<slug>.md` itself using `extractPlanTitle()` + `slugify()` + `uniqueSlugPath()` before embedding and storing. Debug log tags the new path as `method: 'authored-from-tool_input'`.
210
- - **Inverted priority in `extractPlanTextFromMessage()`** — `neural-interface/public/shared/ui-claude-panel.js:5414` previously preferred any text block > 100 chars over `ExitPlanMode.input.plan`, so preamble narration was written to the plan file instead of the plan. Reordered to check `exitTool.input.plan` first, text fallback last.
211
- - **Eager-capture chain captured DOM thinking text** — three `renderAssistant()` callsites at lines 4968, 5048, 5123 used `direct || extractPlanTextFromMessage(msg) || extractPlanText(tab)`, so when `ExitPlanMode.input.plan` was empty the chain fell through to DOM walks that grabbed `Thought›` thinking renders. Tightened to `const captured = direct;` — eager capture now only fires on authoritative plan input; the post-plan hook is the rescue when direct is empty.
212
- - **`/api/create-plan` accepted thinking-prefix garbage** — `neural-interface/server.js:11960` slugified the first non-empty line into filenames like `thought-the-user-wants-me-to-search-....md`. Added 422 rejection when content has no H1 AND first line matches `/^(thought\b|›|thinking[:\s]|let me\b|i'?ll\b|i will\b|the user wants? me\b)/i`, preventing garbage plan files from accumulating.
12
+ ### Fixed — OpenCode V2 Sidepanel Error Auto-Heal
13
+ - **Stale header error pill recovery** — `ocp-v2-state.js` now has explicit `healing`, `clearErrors()`, and `setHealing()` state so transient OpenCode/WebSocket failures no longer leave the sidepanel header stuck on `error` after the connection recovers.
14
+ - **Debounced panel auto-heal** — `ocp-v2-panel.js` now reacts to recoverable errors, `offline`, and `reconnecting` statuses by reconnecting the sidepanel WebSocket, calling `api.init()` to ensure `opencode serve` is healthy, rehydrating the active session/messages, and only creating a fresh session when the current one is unavailable.
15
+ - **Recovery-aware status propagation** — `ocp-v2-ws.js` broadcasts `reconnecting`/`error` status into registered panel stores on WebSocket close/open failure, and the header now shows `healing` / `reconnecting` states instead of immediately surfacing a red error pill.
16
+ - **Child panel stale badge cleanup** — `ocp-v2-childpanel.js` clears recoverable child/sub-agent errors once the server reports ready, preventing minimized child pills from retaining false error badges after recovery.
17
+
18
+ ### Fixed — OpenCode V2 Sidepanel Notification Wiring (DONE/ERROR)
19
+ - **Missing DONE notification on turn completion** — `ocp-v2-ws.js` previously only fired ASK notifications for permissions/questions; `session.idle` only flipped the running flag without notifying. Added a `running→idle` transition check that fires `NOTIF_TYPE.DONE` via `uiNotify('panel', ...)` only on a real transition, so idle echoes after reconnect do not re-fire.
20
+ - **Missing ERROR notification on session failure** — `session.error` only pushed the error into state; now also fires `NOTIF_TYPE.ERROR` via `uiNotify` so the user is alerted when not focused on the panel.
21
+ - **Per-turn dedup for outcome notifications** — Added `_outcomeKey` `WeakMap<store, NOTIF_TYPE>` to dedup repeated DONE/ERROR fires within a single turn; previous ERROR locks out subsequent DONE for the same turn, mirroring the legacy `ocp-tabs.js` lockout. Reset is wired via `_ensureOutcomeReset(store)` which subscribes once to `running:set` from `subscribeSession()`, so the dedup clears whenever a new turn starts — including local sends from `ocp-v2-send.js` that flip `running` directly on the store rather than through a ws event.
22
+ - **Parent-aware notification labels** — Sub-agent (child panel) outcomes display as `↳ {title}` to visually distinguish them from primary-session toasts, matching the existing ASK and ACTION label conventions.
23
+
24
+ ### Fixed — OpenCode V2 Cascading Close & Phantom Pill Swap
25
+ - **`switchToSession` no longer resurrects the just-closed tab** — `handlePillClose()` in `ocp-v2-panel.js` called `removeTab(sid)` then `switchToSession(next)`, but `switchToSession()` ran `if (prevSid) ensureTab(prevSid, getState().sessionInfo)` while `state.sessionId` still pointed at the deleted session — re-adding it to `_tabSessionIds` every time. Fix: clear `setSession(null, null)` BEFORE `removeTab` when the closing session is active, and guard `switchToSession`'s ensureTab call with `if (prevSid && _tabSessionIds.includes(prevSid))` so no other call path can resurrect a removed tab either.
26
+ - **Pill X now matches header X destructiveness** — Tray pill close on `.ocpv2-session-pill` previously only ran the local `handlePillClose()` (no server delete), leaving orphan sessions on the OpenCode backend. Pill X now `await api.sessionDelete(sessionId)` before the local cleanup, mirroring the header X "Delete + switch" behavior.
27
+
28
+ ### Fixed — OpenCode V2 Tray Placeholder Mismatch
29
+ - **`ui-sidepanel-tray.js` now reads the v2 storage shape** — `PROVIDERS.opencode.tabsKeyPrefix` was pinned at the legacy v1 per-window key `synabun-ocp-tabs`, but OCP v2 writes to the single global key `opencode-v2-tabs` holding a plain `string[]` of session IDs. The tray rendered ghost placeholder pills from stale v1 data alongside real v2 pills; clicking a placeholder ran `openProvider('opencode')` → `toggleOpencodePanel()` which closes-if-open, perceived as a cascading panel swap. Added a `v2Plain: true` flag to the opencode provider config; `scopedTabsKey()`, `readPayload()`, `writeProviderPayload()`, and `pickMostRecentMeaningfulPayload()` now branch on it to handle the unscoped single-key `string[]` shape.
30
+ - **One-shot legacy v1 cleanup** — New `cleanupLegacyV1Once()` in `ui-sidepanel-tray.js` deletes every stale `synabun-ocp-tabs-*` storage key on first opencode read, then sets `opencode-v1-cleanup-done` so it only runs once. Eliminates phantom v1 placeholders permanently.
31
+ - **OCP v2 now dispatches `sidepanel-tray:provider-loaded`** — Claude (`ui-claude-panel.js`) and Codex (`cdx-panel.js`) emit this event after boot so `ui-sidepanel-tray.js` can drop its placeholder pills; OCP v2's `boot()` silently skipped the dispatch, so placeholders survived even after the real panel rendered its own pills. Added the `window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'opencode' } }))` call inside `boot()` after `_booted = true`.
32
+ - **`hasOpenCodeState` short-circuits on `sessionId`** — V2 plain entries have no `draft`/`plan`/`title` fields; predicate now treats any entry with a `sessionId` as meaningful so placeholder pills render correctly during the pre-boot window.
33
+ - **`tabLabel` fallback for unbooted v2 placeholders** — Placeholder pills show `OpenCode · {sid8}` when no title is persisted yet (v2 plain payload doesn't store titles), instead of a generic "New session" label.
34
+
35
+ ### Changed — Sidepanel Header & Tray Pill Polish
36
+ - **`stopPropagation` on OCP header buttons** — `+` (new session), Minimize, Close, and Slide buttons in `ocp-v2-panel.js` now `event.stopPropagation()` so a click no longer bubbles into the session-menu open/close handler. Removes a race where a stray header click could toggle the session dropdown.
37
+ - **`data-tooltip-pos` on every sidepanel pill close button** — Tray pill X buttons in `ui-claude-panel.js` (`.cp-session-pill`), `cdx/cdx-tabs.js` (`.cxp-session-pill`), `ocp-v2-panel.js` (`.ocpv2-session-pill`), and the placeholder pills in `ui-sidepanel-tray.js` all got `data-tooltip-pos="top"` so the "Close" tooltip stops clipping at the bottom edge of the tray. OCP header buttons got `data-tooltip-pos="bottom"` for symmetry.
38
+
39
+ ### Fixed — OpenCode V2 Sub-Agent Sidepanel Reliability
40
+ - **Sub-agent tray pill no longer disappears on spawn** — Sub-agent sessions (e.g. the `Task` / `Explore` tool) used to fire server-side without producing a tray pill, leaving the user to dig through the Resume header. Multiple detection gaps fixed in `ocp-v2-manager.js`: new `readParentID()` helper reads `parentID` / `parentId` / `parent_id` / `session.parentID` variants; `AGENT_TOOL_NAMES` expanded with `invoke` / `subagent` / `spawn` / `explore` / `plan` casing variants; new `scanForOrphanChildren()` calls `api.sessionList()` on `registerPrimaryPanel` and `bindPrimarySession` to find children whose `parentID` matches the active session; a new `session.updated` listener catches sub-agents whose `parentID` arrives after creation; the fallback tool-event path no longer requires strict parent-equality so a mid-spawn tab switch still attaches.
41
+ - **Late `session.created` events buffered** — `ocp-v2-ws.js` now keeps a 32-entry / 60 s TTL ring of `session.created` events and replays them to every new `subscribeSessionCreated` subscriber. The panel manager registers only after `boot()` finishes, so child spawns that fired during boot used to be dropped silently — now they are seen.
42
+ - **Child pill survives a missing tray** — `ocp-v2-childpanel.js` split pill mounting into `buildPillElement()` + `attachPillToTray()`. If `#term-minimized-tray` isn't in the DOM yet (panel never opened), the pill polls every 250 ms (up to 15 s) until it can attach, instead of returning `null` and never appearing. Pill also stamps its own `data-session-id` and is inserted directly after the matching parent pill.
43
+ - **Header status stuck on "idle" while server still streaming** — Both primary and child panel headers showed `idle` even while the agent was streaming. `store.setRunning(true)` was only called from `ocp-v2-send.js` when the user pressed Send locally. `ocp-v2-ws.js` `dispatchEvent` now flips `running` on for any incoming `message.updated` whose assistant `info.time.completed` is `null` / `0`, and for any `message.part.updated` with `state.status` of `running` or `pending`. `ocp-v2-panel.js` gained `syncRunningFromHydration()` which scans the transcript after every hydrate (`boot()` tab-restore, `switchToSession()`, `refreshSessionMessages()` auto-heal). `ocp-v2-manager.js` `hydrateChildPanel()` runs the same scan for the child store so the sub-agent header reflects in-flight state after reload.
44
+
45
+ ### Added — OpenCode V2 Sub-Agent Transition Polish
46
+ - **Spawn entrance animation** — When a sub-agent attaches, the new child pill in the tray now slides in from +28 px with a soft overshoot (`@keyframes ocpv2-child-pill-spawn`, 0.55 s `cubic-bezier(0.34, 1.18, 0.5, 1)`), a radial glow `::after` burst (`@keyframes ocpv2-child-pill-flash`, 1.4 s), and an outer blue-accent `box-shadow`. The `ocpv2-pill-spawning` class auto-removes after 1.6 s so subsequent hover styles aren't shadowed.
47
+ - **Child panel cross-panel flash** — `.ocpv2-panel-child.ocpv2-open` now runs `@keyframes ocpv2-child-panel-flash` for 0.9 s on open, briefly tinting the panel's `box-shadow` with the blue sub-agent accent so the handoff from parent → child sidepanel reads unmistakably.
48
+ - **Spawn toast notification** — `ocp-v2-manager.js` now calls `uiNotify('panel', NOTIF_TYPE.ACTION, '↳ Sub-agent of <parent>')` when a child is created, giving the user immediate visual feedback in the notifications drawer.
49
+ - **Parent pill state-change transitions** — `.ocpv2-pill-has-child` got an explicit `transition: transform 0.4s cubic-bezier(0.34, 1.2, 0.5, 1), border-color 0.2s, box-shadow 0.2s` so the parent pill animates smoothly into its "child attached" appearance instead of snapping.
50
+
51
+ ### Changed — OpenCode V2 Sub-Agent Header Affordances
52
+ - **Header status shows the task, not "idle"** — `ocp-v2-childpanel.js` accepts a new `taskInfo` option and exposes `instance.setTaskInfo()`. New `currentTaskLabel(s)` builds the badge from (priority) the currently-running tool name → `${subagentType} · ${summary}` → summary alone → subagent type alone. `syncHeader()` displays the label both while running and while idle, with the same string as `data-tooltip`. `ocp-v2-manager.js` added `describeAgentTask(part, ev, toolName)` to extract `{subagentType, summary}` from the parent's tool input (`state.input.description` / `state.input.prompt` / `subagent_type` / `agent`) and `lookupParentTaskInfo(childSid)` which scans the parent store's recent tool parts when no replay part is available (session.created / session.updated / orphan-scan paths). Task info is threaded through `ensureChildSpawn` → `spawnChildPanel` → `createChildPanel`.
53
+ - **Child pill tooltip pops left, not above** — `ocp-v2-childpanel.js` sets `data-tooltip-pos="left"` on both the child tray pill and its close button. The tray is right-anchored, so `above` tooltips were getting clipped at the screen edge; left placement uses `ui-tooltip.js`'s existing left path. The tray pill tooltip also appends ` — ${taskLabel}` so the minimized state still surfaces the agent's task.
54
+
55
+ ## 2026-05-11
56
+
57
+ ### Fixed — Long-Session Memory Leaks
58
+ - **Bounded `_orphanedProcs` map** — Claude-skin orphan processes accumulated in `server.js` when a kill timer never fired (crash, kill -9, sleep) and an entry was never reattached. Added a 60-second sweep (`setInterval(...).unref()`) that deletes entries whose `proc` is null/killed/exited and clears the stale `killTimer`.
59
+ - **Pruned `_ocpSessionOwners` / `_ocpSessionActivity`** — `_ocpStartSilenceWatchdog` in `server.js` now removes session entries with no live clients and `lastEventAt` older than 30 minutes. Watchdog scan no longer iterates dead sessions for the lifetime of the process.
60
+ - **Idle screencast frame waste** — `onScreencastFrame` in `server.js` now bails out early (still ack'ing `Page.screencastFrameAck` to keep the CDP pipeline alive) when `session.clients` is empty, skipping JPEG buffer allocation and broadcast. Removed the per-frame `frameCount++` and conditional `console.log` that allocated even when not logging.
61
+
62
+ ### Changed — Whiteboard Drag Hot Path
63
+ - **rAF-batched `renderAll()`** — `ui-whiteboard.js` now schedules renders through `requestAnimationFrame` via `_renderRafId`. Multiple mousemove handlers used to call `renderAll()` synchronously per pointer event; renders are coalesced to one per frame. Added `renderAllImmediate()` for explicit flushes.
64
+ - **`_elementMap` O(1) lookups** — Added `_findEl(id)` Map cache. Rebuilt lazily when `_elements.length` changes; `_invalidateElementMap()` is called from `undo()`/`redo()` to cover same-length object replacement. Replaced 8 `_elements.find(el2 => el2.id === ...)` calls inside `onMouseMove` (move-multi, shape-create, section-create, move, move-arrow, move-pen, rotate, resize).
65
+ - **Cached root rect** — Added `_rootRectCached` + `_getRootRect()`/`_invalidateRootRect()`. `clientToCanvas()` and the pen-drawing path reuse the cached rect for the duration of a gesture; cache is cleared at mouseup and on window resize. Eliminates `_root.getBoundingClientRect()` layout flushes inside the 60 Hz mousemove loop.
66
+
67
+ ### Changed — Sidepanel Markdown Rendering
68
+ - **OCP v2 incremental render** — Rewrote `render()` in `ocp-v2-render.js` to use a key/sig reconciler (`_reconcile`, `_partSig`, `_msgSig`) instead of `_container.innerHTML = ''` followed by a full rebuild. User messages, assistant part bubbles, error banners, permission cards, question cards, and the post-plan card now have stable composite keys and content signatures so unchanged nodes are reused. Scroll position is far more stable across streaming deltas.
69
+ - **LRU markdown memoization** — Added a 512-entry insertion-order LRU cache around `renderMarkdown()` in `ocp-v2-render.js`, `md()` in `cdx/cdx-render.js`, and `md()` in `ocp/ocp-render.js`. Identical text re-rendered during streaming/state-change passes now returns cached HTML instead of re-invoking `marked.parse()`.
70
+
71
+ ### Changed — Storage Layer
72
+ - **Deferred `JSON.parse` to flush time** — `storage.js` introduces `STR_SENTINEL`. `setItem()` no longer parses the caller-provided string on every call; `_dirty[key] = STR_SENTINEL` records "parse the current cache string at flush time". `_flushToServer()` and the `beforeunload` `sendBeacon` path resolve sentinels by parsing the cached string once when actually flushing. Big win for callers that store large snapshots (whiteboard, codex transcripts) where every keystroke previously triggered a redundant `JSON.parse`.
73
+
74
+ ### Changed — Floating Terminal Drag Compositing
75
+ - **Simplified `.term-float-tab.float-dragging` shadow** — In `styles.css`, replaced the multi-layer `box-shadow: var(--shadow-2xl), 0 0 0 1px rgba(0,0,0,0.4)` with a single `0 8px 24px rgba(0,0,0,0.45), 0 0 0 1px rgba(0,0,0,0.4)`, dropped the `box-shadow` transition (kept `border-radius`), and added `will-change: transform, left, top` so the dragging window stays on its own composited layer. Removes per-frame GPU compositing thrash during drag.
76
+
77
+ ## 2026-05-09
78
+
79
+ ### Added — Reasoning Effort Picker in OpenCode V2 Sidepanel
80
+
81
+ - **Variant picker in compose footer** — New `neural-interface/public/shared/ocp-v2/ocp-v2-variantpicker.js` module renders a small dropdown beside the model picker that lets users pick a reasoning/thinking effort (`low` / `medium` / `high` / `max` / `xhigh` / `minimal` / `none`) for any OpenCode model that exposes a `variants` map. Auto-hides when the active model has no variants. Includes a "default" entry that clears the variant so the SDK falls back to its built-in default.
82
+ - Persists per-model selection in `localStorage` under `ocp-v2-model-variants` so each model remembers its preferred effort across sessions.
83
+ - Subscribes to `config:model` and restores the saved variant whenever the user switches models.
84
+ - Variants ordered by `PREFERRED_ORDER` (`none, minimal, low, medium, high, max, xhigh`); unknown variant names appended alphabetically; entries with `disabled: true` filtered out.
85
+ - **State plumbing** — Added `variant: null` field plus `setVariant()` setter to `neural-interface/public/shared/ocp-v2/ocp-v2-state.js` (emits `config:variant` event).
86
+ - **Send pipeline** — `neural-interface/public/shared/ocp-v2/ocp-v2-send.js` mounts the variant picker before the model picker in `.ocpv2-footer-right` and forwards `variant: s.variant || undefined` on every `api.send()` call. WS API in `neural-interface/public/shared/ocp-v2/ocp-v2-ws.js` carries the field through to the server, and `neural-interface/server.js` forwards it to `opencodeV2Client.session.prompt(...)` (the v2 client already passes through arbitrary fields via `...rest`).
87
+ - **Styles** — Added `#ocpv2-variant-dd` rules to `neural-interface/public/shared/ocp-v2/ocp-v2-styles.js`: brain icon, capitalised variant names, smaller right-anchored menu, reused `ocpv2-model-menu-in` animation.
88
+
89
+ ## 2026-05-07
90
+
91
+ ### Changed — Empty-Plan Re-Prompt Button (Claude Panel)
92
+ - **Replaced "Plan text was empty" status text with an interactive button** — When the user clicks `Edit plan` in the post-plan card but the plan extractor and `/api/latest-plan` disk fallback both fail to surface a plan file, `neural-interface/public/shared/ui-claude-panel.js` previously printed a static `appendStatus()` line ("Plan text was empty — ask Claude to re-output the plan, or click Continue with implementation to proceed."). The `noFile()` callback in the `'plan'` action handler now renders an inline `msg-status` row containing the short text "Plan text was empty." plus a primary `post-plan-action pp-primary` button labeled "Ask Claude to re-output the plan". Clicking the button populates `#cp-input` with a re-prompt instruction (`Please re-output the full plan you were preparing. Use ExitPlanMode with the complete plan text...`) and calls `send()`. Button disables on click, and falls back to a status if the WebSocket is closed. Scoped to the Claude panel only — `ocp-panel.js` retains the existing `renderErrorMessage` pattern.
93
+
94
+ ### Added — OpenCode Sidepanel Trace System
95
+ - **`ocp-trace.js` client trace bus** — New `neural-interface/public/shared/ocp/ocp-trace.js` exporting `trace()`, `traceTimer()`, `setTraceLevel()`, `setTraceContext()`, `onTrace()`, and `exportTraceLog()`. 5000-event ring buffer, level-gated routing (silent/errors/normal/verbose, persisted in `localStorage:ocp.traceLevel`), CSS-styled `[ocp-trace]` `console.debug` mirror, 250ms batched POST to `/api/ocp/trace/ingest`, `keepalive: true` flush on `pagehide`/`visibilitychange`.
96
+ - **Server trace endpoints** — Added `POST /api/ocp/trace/ingest`, `GET /api/ocp/trace/snapshot?tail=N`, `POST /api/ocp/trace/clear`, and `WS /ws/ocp-trace` to `neural-interface/server.js`. Appends each batch to `data/ocp-trace.jsonl` (rotates at 50MB), broadcasts to live `/ws/ocp-trace` viewer clients. New `ocpTraceEmit(tag, fields)` helper for server-originated events.
97
+ - **Trace points across all OCP modules** — `ocp-ws.js` (request lifecycle with timing), `ocp-events.js` (`event:in` router decisions, `q:asked`/`queued`/`render`), `ocp-stream.js` (`stream:finalize`), `ocp-plan.js` (`plan:exitPlanMode`, `onTurnTerminal`, `proseDetected`, `postUI:show`, `watchdog:fire`), `ocp-questions.js` (`q:reply` timer, `q:list`, `perm:render`/`respond`, `q:poll`), `ocp-send.js` (`send:start`, `send:fetch` timer, `send:turn:end`, `send:error`), `ocp-tabs.js` (`tab:switch` + `setTraceContext` on switch).
98
+ - **Server-side `ocpTraceEmit`** — `q:reply:recv`/`q:reply:done` (with `via` and ms), `q:list:done`, `perm:respond:done`, `send:dispatch` (records `planSystemInjected` when `agent==='plan'`), `sse:relay` for high-signal SSE events (`question.*`/`permission.*`/`session.*`).
99
+
100
+ ### Added — OpenCode Plan-Mode "Force Question Tool" Recovery
101
+ - **4th recovery button** — `renderProseQuestionRecoveryCard()` in `neural-interface/public/shared/ocp/ocp-render.js` now renders a "Force question tool now" button alongside the existing Reply directly / Re-ask / Dismiss buttons. Wired in `ocp-plan.js` to send a hard prompt with a literal `question({ questions: [...] })` one-shot example so models like Kimi K2.6 can pattern-match the tool-call shape.
102
+ - **`question:list` polling fallback** — `startQuestionPolling()` / `stopQuestionPolling()` in `neural-interface/public/shared/ocp/ocp-questions.js`. While a plan turn is active and no question card is visible, polls `question.list()` every 2.5s and surfaces any pending question via `backfillPendingQuestions()`. Wired from `ocp-send.js` to start on plan-mode turn start and stop on resolve / abort / error.
103
+
104
+ ### Fixed — OpenCode Question Reply "Fetch Failed" Frozen UI
105
+ - **Non-terminal reply error path** — In `neural-interface/public/shared/ocp/ocp-questions.js renderOpencodeQuestion`, the `requestWs('question:reply')` catch handler no longer flips `tab.running=false` / `endTurn(tab)`. Instead it re-enables the question card (`locked=''`, buttons re-enabled) and shows "Click an answer again to retry." Previously a transient SDK fetch error caused the entire turn to be marked complete, freezing the UI behind a stale card.
106
+ - **Composer unblocking on prose-recovery dismiss** — `onReplyDirectly` and `onDismiss` callbacks in `ocp-plan.js showProseQuestionRecoveryUI` now defensively clear `tab.showPostPlanActions = false` so the composer always re-enables when the user closes the prose-recovery card.
107
+
108
+ ### Fixed — OpenCode Sidepanel Question Card Multi-Question Routing Trace
109
+ - **`event:in` + `q:queued` diagnostics** — `neural-interface/public/shared/ocp/ocp-events.js` now traces every routed SDK event with `eventType`, `sid`, `partType`, `toolName`, plus `q:asked` (with question count) and `q:queued` (with `reason: no-container | active-question-busy`). Makes it possible to determine — from the trace alone — whether the model emitted `question.asked`, whether the router rendered or queued it, and why.
110
+
111
+ ### Changed — OpenCode Plan-Mode System Instruction
112
+ - **One-shot `question` tool example** — `PLAN_MODE_INSTRUCTION` in `neural-interface/lib/opencode-client.js` and the duplicate `OPENCODE_PLAN_MODE_INSTRUCTION` in `neural-interface/server.js` now include a literal `question({ questions: [{ question, header, options: [{label, description}], multiSelect }] })` example block plus a "If you find yourself writing 'let me ask a few questions'… STOP and call the `question` tool" reinforcement. Kimi K2.6 ignored the abstract directive but pattern-matches the concrete example.
113
+
114
+ ### Added — OpenCode Sidepanel V2 (Clean-Slate Rebuild)
115
+ - **Fresh SDK wrapper `lib/opencode-v2-client.js`** — New ~190-line wrapper around `@opencode-ai/sdk/v2`'s `createOpencodeClient`. Pass-through philosophy: NO `normalizeV2Event` translation, NO `buildSystemForPlanMode` injection, NO legacy shape massaging. `connect({ port })` runs an `event.subscribe()` loop with 1s→10s exponential-backoff auto-reconnect via `AbortController`, emitting `{ eventType, event }` envelopes verbatim through an internal `event.onEvent(listener)` registry. Surfaces `session.list/create/get/delete/abort/messages/prompt` and `permission.reply`; `unwrap()` translates hey-api `{ data, error, response }` to `{ status, data }` and throws on error; `clean()` strips `undefined` keys so the URL builder doesn't append `?foo=undefined`. Verbose log gated on `OCPV2_VERBOSE=1`.
116
+ - **New WS route `/ws/opencode-v2` + REST endpoints** — `neural-interface/server.js` adds `_ocpV2WsClients` Set, `broadcastToOpencodeV2Clients()`, `installOpencodeV2Listener()` (independent bridge from legacy `installOpencodeClientListener`), and `handleOpencodeV2Ws(ws)`. The handler implements `init / session:list / session:create / session:get / session:delete / session:messages / message:send / message:abort / permission:reply` over id-correlated JSON frames, with per-session `AbortController` map so a fresh send cancels the previous in-flight prompt cleanly. New REST: `GET /api/opencode-v2/status`, `POST /api/opencode-v2/serve/start`, `POST /api/opencode-v2/serve/stop`. Wired `opencodeV2Client.connect/disconnect` into both `ensureOpencodeServer` boot points and `stopOpencodeServer`, sharing the same `opencode serve` process on port 4096 as the legacy panel. `/ws/opencode-v2` added to the upgrade allowlist and connection router; legacy `/ws/opencode-skin` route remains registered (orphaned).
117
+ - **Browser modules under `public/shared/ocp-v2/`** — Six fresh modules totaling under 1500 lines:
118
+ - `ocp-v2-styles.js` — `.ocpv2-*` CSS injected once, slide-in panel, message bubbles, tool cards with status badges, reasoning collapsibles, permission cards, error banners, compose footer with autosizing textarea.
119
+ - `ocp-v2-state.js` — pub/sub state store: `serverStatus`, `sessionId`/`sessionInfo`, `messageOrder[]`, `messages: Map<id,{parts:Map}>`, `running`, `pendingPermission`, `errors[]`. Setters fire `notify()` to all `subscribe()` listeners.
120
+ - `ocp-v2-ws.js` — WebSocket client. `api.{init,sessionList,sessionCreate,sessionGet,sessionDelete,sessionMessages,send,abort,permissionReply}` returns id-correlated promises. `handleEvent()` dispatches `message.updated` / `message.removed` / `message.part.updated` / `session.idle` / `session.error` / `permission.asked` / `permission.replied` into state mutations; auto-reconnect after 1.5s on close.
121
+ - `ocp-v2-render.js` — full CLI parity renderers: text (minimal Markdown — paragraphs, fences, inline code, **bold**, *italic*, lists, links — no external deps), reasoning (collapsible `<details>`, open while streaming), tool (header with name + status badge, input JSON, output `<pre>`, error variant), file chip, step markers, permission card with `Allow once / Always / Reject` buttons that lock on click and call `api.permissionReply()`, error banner. Mounts on the messages container and re-renders on every state notification.
122
+ - `ocp-v2-send.js` — compose textarea + Send/Abort button. Enter sends, Shift+Enter newline, optimistic user message rendered before SDK echoes back, button toggles to Abort while `running`. `doAbort()` calls `api.abort(sessionId)` and force-clears `running`.
123
+ - `ocp-v2-panel.js` — slide-in panel container. Lazy boot on first toggle: `api.init()` → `setServerStatus` → `startFreshSession()` (auto-creates one fresh session per panel open, matches CLI single-session model). Header shows `OpenCode · {sid8}` + status pill (`ready` / `running` / `error`) + a `New` button to spawn a new session. Emits `opencode-panel:visibility` so the existing navbar binding still highlights the topright button.
124
+ - **Barrel + navbar swap** — New `public/shared/ui-opencode-panel-v2.js` re-exports `toggleOpencodePanel` / `isOpencodePanelOpen` / `openOpencodeWithPrompt` from `ocp-v2/ocp-v2-panel.js`. Single-line edit in `public/shared/ui-navbar.js` repoints the import from `./ui-opencode-panel.js` → `./ui-opencode-panel-v2.js`. Topright OpenCode button now opens the V2 panel; old `public/shared/ocp/**` (13 files, ~14000 lines), `public/shared/ui-opencode-panel.js`, and `lib/opencode-client.js` remain unmodified on disk as a rollback safety net.
125
+
126
+ ## 2026-05-06
127
+
128
+ ### Fixed — Terminal ESC Key Now Aborts CLI TUIs On First Press (Floating + Docked)
129
+ - **Single ESC reaches the PTY instead of `\x1b\x1b`** — In `neural-interface/public/shared/ui-terminal.js`, the global window-capture ESC handler in `initTerminal()` sent `\x1b` over the WS without `e.preventDefault()` / `e.stopImmediatePropagation()`, so xterm.js's own keydown listener on `.xterm-helper-textarea` fired afterwards and re-emitted ESC via `term.onData(...)` (line 1915). The PTY received `\x1b\x1b`, which Bubbletea-based TUIs (OpenCode, Claude Code, Codex) interpret as a Meta-prefix and ignore — so the running task never aborted. Handler now sends one `\x1b`, calls `e.preventDefault()` + `e.stopImmediatePropagation()` to block the duplicate, and re-focuses via `requestAnimationFrame(() => session.term?.focus())` instead of the old `term?.blur()` / `ta.blur()` pair (focus loss had forced the user to click back into the terminal between keystrokes).
130
+ - **Docked terminal now has ESC coverage** — Previous early return `if (!isXterm) return;` left the docked panel without any ESC fallback when focus drifted to a tab title or panel chrome. Handler now resolves the target session by DOM ancestry: xterm helper textarea → its viewport's `dataset.sessionId`, else `.term-float-tab` → that tab's session, else inside `_panel` (`#terminal-panel`) → `_sessions[_activeIdx]`. Skips non-terminal `input/textarea/select/[contenteditable]` so search/rename/path inputs still consume ESC normally.
131
+ - **Removed redundant per-floating-window ESC branch** — `attachTab()`'s `win.addEventListener('keydown', ...)` had a duplicate ESC sender that was unreachable for the common case (skipped when target was a textarea). Stripped the ESC branch; the Ctrl+C copy-on-selection branch stays as the floating-window-only fallback.
132
+
133
+ ## 2026-05-05
134
+
135
+ ### Fixed — OpenCode Plan-Mode Stall Recovery Now Always Shows PLAN COMPLETE (Followup)
136
+ - **PLAN COMPLETE always renders after a stall** — In `neural-interface/public/shared/ocp/ocp-tabs.js`, the `executePlanStallRecovery()` added in the prior fix took the wrong fallback when `tab.planContent` ended up shorter than 40 chars: it called `showProseQuestionRecoveryUI(tab)` (which renders a card titled `MODEL ASKED QUESTIONS IN PROSE`), instead of the user-expected `PLAN COMPLETE`. Reproduced on `opencode-go/kimi-k2.6` plan mode where the user reported "the agent asked a few questions … then automatically returned an error … but why isn't it presenting the plan complete normally?". Recovery now unconditionally calls `maybeShowPostPlanUI(tab, { source: \`stall:${reason}\`, force: true })` — `force: true` skips both the 40-char gate AND the prose-question guard. The prose-question recovery card is for a different bug class (model wrote questions as text instead of using the `question` tool) and should never be the stall fallback.
137
+ - **Broader content extraction via `extractPlanTextLoose`** — The previous recovery called `capturePlanContent(tab)` which only adopts DOM text > 80 chars from the *last* `.ocp-msg-assistant` bubble, missing two key paths used by kimi-k2.6: (1) the live thinking block content (rendered into `.ocp-think-content-live` by the new inline `<think>` parser), and (2) `tab.planContent` not being populated by `message.part.delta` events (deltas update the DOM only — only `message.part.updated` updates `tab.planContent`). Recovery now calls `extractPlanTextLoose(tab)` which already covers all six extraction sources: existing `tab.planContent` → `ExitPlanMode` tool args → `todowrite`/`tasks` todos → final assistant text bubble (>40 chars) → standalone `.ocp-think-block .ocp-think-content` (matches both finalized AND live-streaming thinking blocks since the live block carries both classes) → subagent (Agent/task) tool result → last-resort any assistant bubble.
138
+
139
+ ### Fixed — OpenCode Sidepanel Plan Mode Hang on Non-Anthropic Models (Tiered Stall Watchdog)
140
+ - **Auto-recovers when the model never finalizes** — In `neural-interface/public/shared/ocp/ocp-tabs.js`, plan-mode turns on non-Anthropic providers (e.g., `opencode-go/kimi-k2.6`) often hang because the model never calls `ExitPlanMode` and the OpenCode server never delivers a terminal `session.idle` / `session.status:idle` / `message.completed`. The synchronous POST blocks for up to 30 min, so the user had to manually press STOP for the PLAN COMPLETE card to appear (manual STOP triggers `message:abort` → server emits `session.idle` → post-plan funnel). New `schedulePlanStallWatchdog(tab)` runs a single-shot silence timer that is re-armed on every SSE event in `handleSSEEvent`, plus a one-shot hard-cap timer set in `sendMessage`.
141
+ - **Tiered behavior** — `PLAN_STALL_SOFT_MS` (60 s of silence) renders a soft prompt card with `Abort & show captured plan` / `Wait longer` / `Cancel turn`; `PLAN_STALL_AUTO_MS` (180 s of silence) auto-recovers without prompting; `PLAN_STALL_HARD_CAP_MS` (8 min total turn duration) force-recovers regardless of activity.
142
+ - **Recovery path (`executePlanStallRecovery`)** — Captures latest plan content via `capturePlanContent`, rejects the pending WS request via `rejectPending`, sends `message:abort` so the OpenCode server fires `session.idle` and stops burning model compute, force-finalizes tab UI (`tab.running = false; endTurn(tab); setTurnStatus(tab)`), then renders PLAN COMPLETE card if `tab.planContent.length >= 40` else falls back to the prose-question recovery card.
143
+ - **Suppressed during interactive question** — `hasActiveQuestion(tab)` short-circuits both arm and fire so a user taking 90 s to answer a `question` tool prompt doesn't trigger a false stall.
144
+ - **Couples with finalization watchdog clearing** — `clearPlanFinalizationWatchdog(tab)` now also calls `clearPlanStallTimers(tab)`, so all 16 existing terminal/abort sites (message.completed, session.status:idle, session.idle, session.error, abortTab, sendMessage start/resolved/catch, setTabMode, etc.) clear stall timers without per-site edits.
145
+ - **Soft-prompt card** — New `renderPlanStallSoftCard()` in `neural-interface/public/shared/ocp/ocp-render.js` mirrors the prose-question recovery card with a cool-blue accent (vs. amber) so users can visually distinguish "stuck" from "asked questions in prose". CSS in `neural-interface/public/shared/ocp/ocp-styles.js` (`.ocp-plan-stall-card` and friends).
146
+
147
+ ### Fixed — OpenCode Sidepanel Live Thinking Stream for Inline `<think>` Tag Models
148
+ - **CLI-style typing for Kimi K2 / DeepSeek R1 / Qwen QwQ via opencode-go** — In `neural-interface/public/shared/ocp/ocp-render.js`, models that emit reasoning as inline `<think>…</think>` tags (rather than as a structured `partType === 'reasoning'` part) previously had their reasoning render as raw `<think>` text in the live bubble, with `<think>` parsing only happening at `finalizeStreamingMessage()`. The user complaint: thinking should "type into" the sidepanel as it does in the OpenCode CLI. New stateful parser `_routeStreamingChunk(el, container, chunk)` walks each delta, splits at `<think>` / `</think>` boundaries, and routes inside-tag content to the live thinking block via `_appendThinkTextOnly` and outside-tag content to the live text node via `_appendRawTextOnly`.
149
+ - **Element-level state** — `el._thinkInside` tracks whether subsequent deltas continue inside an unclosed `<think>` block. `el._thinkPendingTag` buffers a partial tag that lands at a chunk boundary (e.g., delta ending with `<thi` or `</thin`); `_matchTrailingTagPrefix(s, target)` finds the longest suffix of `s` that prefixes `target`, so the partial flushes correctly when the rest of the tag arrives in the next delta.
150
+ - **`setStreamRawText` split path** — Full-payload `message.part.updated` events that include `<think>…</think>` no longer clobber the live split. When the full text contains `<think>`, the parser uses `parseThinkSegments` to compute the canonical `textJoined` (non-thinking) and `thinkJoined` (thinking) strings and adopts each side independently with the existing length-gate. Sets `el._thinkInside` from the trailing segment so subsequent deltas continue correctly. Fast path (no inline tags) keeps the original direct-write behavior.
151
+ - **No regression for explicit reasoning parts** — Anthropic and other providers that emit `partType === 'reasoning'` with structured deltas continue to flow through `appendThinkChunk` directly; only the `appendStreamChunk` (text delta) path got the new inline parser.
152
+
153
+ ### Fixed — OpenCode Sidepanel Live Thinking Stream (CLI Parity)
154
+ - **Token-by-token reasoning typing** — Reasoning deltas in `neural-interface/public/shared/ocp/ocp-render.js` previously routed through `scheduleStreamingRender()` which rebuilt the entire `<details>` block via `slot.innerHTML = thinkBlockHtml(...)` every 16 ms (O(n) per tick over the growing buffer). The block now lives in a persistent `<details class="ocp-think-block ocp-think-block-live" open>` skeleton built once by the new `getOrCreateLiveThinkContent(el)`; each delta does `live.appendChild(document.createTextNode(chunk))` (O(1)) so reasoning text "types in" character-by-character, mirroring the OpenCode CLI feel.
155
+ - **Snapshot suffix append** — `setStreamThinkText()` now slices the new suffix when a `message.part.updated` snapshot arrives longer than the current buffer and appends only that suffix as a single text node, preserving the persistent live element (no flicker, no scroll-position loss).
156
+ - **Auto-scroll respects manual scroll-up** — New `autoScrollLiveThink(live)` only scrolls the live content to the bottom when the user is within 32 px of it, so scrolling up to read earlier reasoning isn't fought.
157
+ - **Removed obsolete render path** — Deleted `renderStreamingElement()` and `scheduleStreamingRender()` (the 16 ms `innerHTML`-rebuild path) since the live append path replaces it.
158
+ - **Live styling cues** — Added `.ocp-think-block-live` (slightly brighter border) and a pulsing `Thinking…` summary label via `@keyframes ocp-think-pulse` in `neural-interface/public/shared/ocp/ocp-styles.js`, plus `scroll-behavior: smooth` on `.ocp-think-content-live`.
159
+
160
+ ### Fixed — OpenCode Plan Mode Auto-Finalizes After ExitPlanMode
161
+ - **PLAN COMPLETE without manual STOP** — When a plan-mode model emitted `ExitPlanMode` but the OpenCode server never delivered a terminal `session.idle` / `message.completed`, the post-plan card stayed buried until the user pressed STOP (which triggered `message:abort` → server-emitted `session.idle` → post-plan funnel). The `schedulePlanFinalizationWatchdog()` in `neural-interface/public/shared/ocp/ocp-tabs.js` was previously armed only inside `recordAgentChildEvent` for the Deepseek subagent-idle case, never for parent-level `ExitPlanMode` without subagents (e.g., Anthropic plan mode). The watchdog is now armed (with a 5 s timeout) in BOTH ExitPlanMode detection sites — the legacy `tool.start` handler and the modern `message.part.updated` handler — replacing the lone `clearPlanFinalizationWatchdog(tab)` call with a clear-then-schedule pair.
162
+ - **Parameterized timeout** — `schedulePlanFinalizationWatchdog(tab, { timeoutMs = 8000 } = {})` accepts an optional `timeoutMs`; the subagent-idle case keeps the 8 s default while ExitPlanMode-detected uses 5 s (the tool already returned, terminal event should arrive in 1–3 s).
163
+ - **Relaxed min-length guard on watchdog fire** — When `_exitPlanDetected = true`, the 40-char `tab.planContent` minimum is bypassed so plans whose text only lives on `toolInput.plan` (no separate assistant prose) still render. The existing `maybeShowPostPlanUI(tab, { force: tab._exitPlanDetected })` already passes `force: true` for the tool-detected path.
164
+
165
+ ### Fixed — Claude Sidepanel Plan-Mode "Plan Text Was Empty" Edit Flow
166
+ - **Server-side plan file authoring before `killProc()`** — `neural-interface/server.js` ExitPlanMode hook (`handleClaudeSkinWebSocket`, ~line 6616) now calls `writePlanFile(inputPlan || planText, { cwd: workDir })` before the kill. Previously the kill (intentional, prevents `--print`-mode auto-implementation) ran before `PostToolUse` could fire, so `post-plan.mjs` never authored a plan file from `tool_input.plan`. Result: clicking "Edit Plan" hit `noFile()` with "Plan text was empty — ask Claude to re-output the plan…". Server now writes `data/plans/YYYY-MM-DD/<slug>.md` directly from `ExitPlanMode.input.plan` (or the preceding text block as fallback).
167
+ - **`writePlanFile()` helper extracted from `/api/create-plan`** — both the new ExitPlanMode hook and the existing `POST /api/create-plan` route share one writer. Helper is idempotent: same slug + identical content returns the existing path so server-side and client-side races never produce `slug-2.md` duplicates.
168
+ - **Tightened narration-prefix filter `_NARRATION_PREFIX`** — regex now catches `now i (have|see|need)`, `here's`, `looking at`, `based on (the|my|your)`, `the user (wants|asked|is asking|needs)`, `okay/alright` openers, `found it`, `the recent` — preventing prose narration from being slugified into garbage filenames like `now-i-have-a-good-understanding-of-the-context.md` or `here-s-the-refined-plan-based-on-your-selections.md`.
169
+ - **New `plan_file_written` system event** — server emits `{ type: 'system', subtype: 'plan_file_written', path, name }` to the client BEFORE the assistant event so `handleTabEvent()` in `neural-interface/public/shared/ui-claude-panel.js` sets `tab.planFilePath` immediately. Edit Plan now takes `openPlan(tab.planFilePath)` on the very first click — no race, no fallback chain, no `requestAnimationFrame` retry needed in the happy path.
170
+ - **Removed redundant `/api/create-plan` POST from 3 eager-capture sites** — `renderAssistant()` (ExitPlanMode-detection block, partial-message dedup branch, new-message branch) no longer POSTs the captured plan content. Capture still stashes `tab._planContent` from `exitBlock.input.plan` for the implementation-prompt flow, but file authoring is exclusively server-side.
171
+ - **Removed `extractPlanText`/`materialize` DOM-scrape fallback from Edit Plan click handler** — `renderPostPlanActions()`'s `a.action === 'plan'` branch no longer scrapes `.msg-body` rendered prose and POSTs it to `/api/create-plan`. The DOM only ever held rendered prose narration, never authoritative plan markdown — that path was the source of the garbage-named plan files. Click flow simplified to: `tab.planFilePath` → `openPlan` immediately; else one `requestAnimationFrame` retry for in-flight `plan_file_written`; else `diskFallback` via `GET /api/latest-plan`; else `noFile()`.
172
+
173
+ ### Fixed — OpenCode Plan Mode Forces `question` Tool + Prose-Question Recovery
174
+ - **Plan-mode prompt-prefix injection** — `neural-interface/server.js` previously sent `body.mode='plan'` and `body.tools={...}` without any prompt instruction, so models like Kimi K2.6 wrote clarification questions as prose ("Before I proceed, a couple of quick questions:") instead of calling the OpenCode `question` tool — the sidepanel only renders interactive cards for actual `question` tool calls, so prose questions stranded the user. New `OPENCODE_PLAN_MODE_INSTRUCTION` constant (mirroring the existing `CODEX_PLAN_MODE_PREFIXES` at ~line 2828) is now prepended to the first text part in every `body.mode === 'plan'` send: it explicitly states `You MUST call the \`question\` tool`, forbids prose questions, and reminds the model to call `ExitPlanMode` when ready. Idempotent via a `startsWith('[PLAN MODE]')` guard so the prefix isn't double-injected on multi-turn plan refinement.
175
+ - **Per-turn question-tool usage tracking** — `neural-interface/public/shared/ocp/ocp-tabs.js` adds a `tab._questionToolUsedThisTurn` flag set to `true` in all three question-detection sites (`message.part.updated` tool branch, `tool.start` handler, and the dedicated `question.asked` SSE event), reset to `false` at the start of `sendMessage()`, when entering plan mode in `setTabMode()`, and on tab clear.
176
+ - **Prose-question detector** — New `looksLikeProseQuestions(content)` helper scans the last 400 chars of captured plan text for trailing patterns: `quick/a couple of/two/before i proceed … questions:`, `Question N:` markers, or 2+ trailing `?` per line.
177
+ - **`maybeShowPostPlanUI` recovery reroute** — When `!force && !_exitPlanDetected && !_questionToolUsedThisTurn && looksLikeProseQuestions(planContent)`, the function now calls `showProseQuestionRecoveryUI(tab)` and returns `false` instead of stamping the misleading PLAN COMPLETE card. Fixes the long-standing "press STOP after prose questions → PLAN COMPLETE for half-finished plan" trap.
178
+ - **`MODEL ASKED QUESTIONS IN PROSE` recovery card** — New `renderProseQuestionRecoveryCard()` / `removeProseQuestionRecoveryCards()` exports in `neural-interface/public/shared/ocp/ocp-render.js`, plus a new `.ocp-prose-question-card` block in `neural-interface/public/shared/ocp/ocp-styles.js` (amber accent so it's visually distinct from the green-ish PLAN COMPLETE). Three actions: **Reply directly** focuses the OCP input field; **Re-ask using question tool** sends a follow-up prompt instructing the model to re-ask using the `question` tool; **Dismiss** removes the card. Cards are also cleared on next `sendMessage()` and on `setTabMode()` exit-plan.
179
+
180
+ ### Added — AskUserQuestion Manual QA Test Suite
181
+ - **Test suite at `tests/ask-user-question/`** — Manual QA suite for the `AskUserQuestion` interactive question card across Claude, Codex, and OpenCode sidepanels. Each scenario is a directive copy-paste prompt that forces the panel CLI to invoke its `AskUserQuestion` equivalent (Claude `control_request`, Codex `request_user_input`, OpenCode `tool.start` / `question.asked`) with a specific shape, so visual and functional rendering can be verified across all three panels.
182
+ - **`README.md`** — Master entry with the panel-to-render-function map (`renderAskUserQuestion()` in `neural-interface/public/shared/ui-claude-panel.js`, `renderUserInputRequest()` in `neural-interface/public/shared/cdx/cdx-requests.js`, `renderQuestionCard()` in `neural-interface/public/shared/ocp/ocp-render.js`), pre-flight steps, and recommended run order.
183
+ - **`checklist.md`** — Per-panel verification checklist covering visual checks (header chip, options layout, `Submit (x/y)` progress, locked state, inter-block spacing) and functional checks (selection toggling, multi-select, "Other" / custom-input path, submit transmission, queue behavior). Includes a scenario × panel scoreboard.
184
+ - **`master-prompt.md`** — Single 4-question stress prompt (mixed multi/single-select, long labels, descriptions) for end-to-end smoke testing after granular scenarios.
185
+ - **`scenarios/01-12-*.md`** — 12 numbered scenarios covering single-select min/max, multi-select, batched 2/4-question cards with mixed types, `Other` / custom typed answer, sequential queue (`_questionQueue` / `processQuestionQueue`), plan-mode integration, long-content overflow, header-only cards, no-options free-text fallback, and cancel-mid-answer queue clearing.
186
+
187
+ ## 2026-05-04
188
+
189
+ ### Fixed — OpenCode Sidepanel Project Selector Lost the Project Path
190
+ - **Session created in the right directory** — The `session:create` WS handler in `neural-interface/server.js` now forwards the panel-supplied cwd to OpenCode as `POST /session?directory=<encoded path>`. OpenCode binds each session to the directory passed in this query parameter (`session.directory`); previously the call omitted it, so every OpenCode session inherited the serve cwd (`PACKAGE_ROOT` = SynaBun) and ALL agent tool calls (Read, Bash, Edit, Write) resolved relative to SynaBun even when the user had picked a different project in the footer. With the query param wired through, OpenCode's tools natively resolve at the project root the user selected.
191
+ - **Fresh-session cwd routing** — Switching the project via the OpenCode footer dropdown previously mutated only `tab.project` in `neural-interface/public/shared/ocp/ocp-panel.js`, leaving the existing OpenCode session bound to the cwd it was first created with. Added `setActiveProject()` in `neural-interface/public/shared/ocp/ocp-tabs.js` that updates `tab.project` + `STOR.project`, drops the bound `tab.sessionId` (along with title, token usage, plan/post-plan, watchdogs, tool activity, question/permission queues, and snapshot flush), refreshes the project chip label, and clears the messages container so the next `sendMessage()` creates a fresh OpenCode session at the new cwd. The projectbar dropdown click handler and the name-modal commit now both route through `setActiveProject()` instead of touching `tab.project` directly.
192
+ - **Greeting carries the absolute project path** — Updated `buildOpencodeGreetingUserPrompt()` in `neural-interface/server.js` to spell out the absolute project working directory (`Project working directory: \`/abs/path\``) and instruct the agent to use it for all file operations whenever the selected project differs from `PACKAGE_ROOT`. Belt-and-suspenders alongside the new `?directory=` query param, so the agent's first turn explicitly states the cwd it should be operating in.
193
+
194
+ ### Fixed — Codex Sidepanel Project Switching
195
+ - **Fresh-session cwd routing** — Fixed the Codex footer project selector so changing projects updates the bound `_project` state in `neural-interface/public/shared/cdx/cdx-tabs.js` instead of only mutating `tab.project` in `neural-interface/public/shared/cdx/cdx-panel.js`; the new `setActiveProject()` keeps `tab.project`, `STOR.project`, branch UI, and fresh thread reset state in sync before `dispatchPrompt()` sends `query.cwd`.
196
+ - **Sandbox root refresh** — Sends `reset_thread` to the Codex websocket after a project change and clears `_threadId` when the server explicitly returns `threadId: null`, preventing stale app-server thread IDs from leaking into fresh sessions and causing workspace-write permissions to target the old folder.
197
+
198
+ ### Added — Pending Remember Picker on Notifications Health Tab
199
+ - **Inline provider/model picker** — New "Run Pending Remembers" button on the Notifications drawer Health tab in `neural-interface/public/shared/ui-sessions.js`. Clicking expands an inline picker with provider buttons (Claude Code / Codex / OpenCode), a per-provider model dropdown, and Cancel/Run actions, so each orphaned `pending-remember` flag can be summarized into a real memory by the user's chosen sidepanel CLI instead of an isolated background agent.
200
+ - **Per-provider model loading** — Claude models pulled from `/api/claude/config`, OpenCode models flattened from `/api/opencode/providers/full` as `provider/model` ids, Codex falls back to a static list (`gpt-5-codex`, `gpt-5`, `o4`, `o4-mini`) since its model list is WebSocket-only.
201
+ - **Sidepanel routing** — Run dispatches the prompt to the chosen sidepanel: Claude via `sendToPanel({ newTab: true, autoSubmit: true })`, OpenCode via `openOpencodeWithPrompt({ autoSend: true })`, Codex by toggling the panel and pre-filling `#cxp-input` (auto-send is unreliable from outside the Codex panel).
202
+ - **Server endpoint** — New `GET /api/sessions/pending-remember-prompt` in `neural-interface/server.js` returns `{ ok, count, prompt }`; resolves each orphaned session id to its `~/.claude/projects/<projKey>/<sessionId>.jsonl` transcript and embeds those paths in the prompt so the executing CLI can read them.
203
+ - **Picker styling** — Added `.sm-pr-picker` / `.sm-pr-row` / `.sm-pr-providers` / `.sm-pr-select` rules in `neural-interface/public/shared/styles.css`, including an `active` pill state for the selected provider.