@juspay/shooter 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (488) hide show
  1. package/.claude/hooks/notifier.cjs +117 -33
  2. package/.claude/settings.json +14 -14
  3. package/README.md +116 -84
  4. package/bin/shooter.cjs +471 -102
  5. package/build/client/_app/immutable/assets/{0.CM9Hl6d-.css → 0.BhZOCxO4.css} +1 -1
  6. package/build/client/_app/immutable/assets/0.BhZOCxO4.css.br +0 -0
  7. package/build/client/_app/immutable/assets/0.BhZOCxO4.css.gz +0 -0
  8. package/build/client/_app/immutable/assets/1.BYutk3aU.css +1 -0
  9. package/build/client/_app/immutable/assets/1.BYutk3aU.css.br +0 -0
  10. package/build/client/_app/immutable/assets/1.BYutk3aU.css.gz +0 -0
  11. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.gz +0 -0
  12. package/build/client/_app/immutable/assets/3.DGDHCVnW.css +1 -0
  13. package/build/client/_app/immutable/assets/3.DGDHCVnW.css.br +0 -0
  14. package/build/client/_app/immutable/assets/3.DGDHCVnW.css.gz +0 -0
  15. package/build/client/_app/immutable/assets/4.BFUut--w.css +1 -0
  16. package/build/client/_app/immutable/assets/4.BFUut--w.css.br +0 -0
  17. package/build/client/_app/immutable/assets/4.BFUut--w.css.gz +0 -0
  18. package/build/client/_app/immutable/assets/5.BTOx7yt7.css +1 -0
  19. package/build/client/_app/immutable/assets/5.BTOx7yt7.css.br +0 -0
  20. package/build/client/_app/immutable/assets/5.BTOx7yt7.css.gz +0 -0
  21. package/build/client/_app/immutable/assets/6.eZGZN-BF.css +1 -0
  22. package/build/client/_app/immutable/assets/6.eZGZN-BF.css.br +0 -0
  23. package/build/client/_app/immutable/assets/6.eZGZN-BF.css.gz +0 -0
  24. package/build/client/_app/immutable/assets/7.DwS5ZHBh.css +1 -0
  25. package/build/client/_app/immutable/assets/7.DwS5ZHBh.css.br +0 -0
  26. package/build/client/_app/immutable/assets/7.DwS5ZHBh.css.gz +0 -0
  27. package/build/client/_app/immutable/assets/ChatView.CwWbzIL-.css +1 -0
  28. package/build/client/_app/immutable/assets/ChatView.CwWbzIL-.css.br +0 -0
  29. package/build/client/_app/immutable/assets/ChatView.CwWbzIL-.css.gz +0 -0
  30. package/build/client/_app/immutable/assets/Phone.FQEfwCX2.css +1 -0
  31. package/build/client/_app/immutable/assets/Phone.FQEfwCX2.css.br +0 -0
  32. package/build/client/_app/immutable/assets/Phone.FQEfwCX2.css.gz +0 -0
  33. package/build/client/_app/immutable/assets/markdown.Dc-OSJWY.css +1 -0
  34. package/build/client/_app/immutable/assets/markdown.Dc-OSJWY.css.br +0 -0
  35. package/build/client/_app/immutable/assets/markdown.Dc-OSJWY.css.gz +0 -0
  36. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.gz +0 -0
  37. package/build/client/_app/immutable/chunks/B-K5Sh65.js +1 -0
  38. package/build/client/_app/immutable/chunks/B-K5Sh65.js.br +0 -0
  39. package/build/client/_app/immutable/chunks/B-K5Sh65.js.gz +0 -0
  40. package/build/client/_app/immutable/chunks/B5NAKyil.js +20 -0
  41. package/build/client/_app/immutable/chunks/B5NAKyil.js.br +0 -0
  42. package/build/client/_app/immutable/chunks/B5NAKyil.js.gz +0 -0
  43. package/build/client/_app/immutable/chunks/B8XegpSE.js +1 -0
  44. package/build/client/_app/immutable/chunks/B8XegpSE.js.br +0 -0
  45. package/build/client/_app/immutable/chunks/B8XegpSE.js.gz +0 -0
  46. package/build/client/_app/immutable/chunks/B8zoBsv3.js +6 -0
  47. package/build/client/_app/immutable/chunks/B8zoBsv3.js.br +0 -0
  48. package/build/client/_app/immutable/chunks/B8zoBsv3.js.gz +0 -0
  49. package/build/client/_app/immutable/chunks/BLszSzTT.js +1 -0
  50. package/build/client/_app/immutable/chunks/BLszSzTT.js.br +0 -0
  51. package/build/client/_app/immutable/chunks/BLszSzTT.js.gz +0 -0
  52. package/build/client/_app/immutable/chunks/BOYo8yTr.js +1 -0
  53. package/build/client/_app/immutable/chunks/BOYo8yTr.js.br +0 -0
  54. package/build/client/_app/immutable/chunks/BOYo8yTr.js.gz +0 -0
  55. package/build/client/_app/immutable/chunks/BTGVxaYV.js.gz +0 -0
  56. package/build/client/_app/immutable/chunks/BYqGCrTe.js +1 -0
  57. package/build/client/_app/immutable/chunks/BYqGCrTe.js.br +0 -0
  58. package/build/client/_app/immutable/chunks/BYqGCrTe.js.gz +0 -0
  59. package/build/client/_app/immutable/chunks/BlxrFPDK.js.gz +0 -0
  60. package/build/client/_app/immutable/chunks/Bu1aqm5j.js +1 -0
  61. package/build/client/_app/immutable/chunks/Bu1aqm5j.js.br +0 -0
  62. package/build/client/_app/immutable/chunks/Bu1aqm5j.js.gz +0 -0
  63. package/build/client/_app/immutable/chunks/C4mLaWWx.js +1 -0
  64. package/build/client/_app/immutable/chunks/C4mLaWWx.js.br +0 -0
  65. package/build/client/_app/immutable/chunks/C4mLaWWx.js.gz +0 -0
  66. package/build/client/_app/immutable/chunks/CQjSATpv.js +61 -0
  67. package/build/client/_app/immutable/chunks/CQjSATpv.js.br +0 -0
  68. package/build/client/_app/immutable/chunks/CQjSATpv.js.gz +0 -0
  69. package/build/client/_app/immutable/chunks/CSoRdFvv.js +1 -0
  70. package/build/client/_app/immutable/chunks/CSoRdFvv.js.br +0 -0
  71. package/build/client/_app/immutable/chunks/CSoRdFvv.js.gz +0 -0
  72. package/build/client/_app/immutable/chunks/CZHsSL_X.js +1 -0
  73. package/build/client/_app/immutable/chunks/CZHsSL_X.js.br +0 -0
  74. package/build/client/_app/immutable/chunks/CZHsSL_X.js.gz +0 -0
  75. package/build/client/_app/immutable/chunks/DLu6yJIZ.js.gz +0 -0
  76. package/build/client/_app/immutable/chunks/DSU1n5N_.js +1 -0
  77. package/build/client/_app/immutable/chunks/DSU1n5N_.js.br +0 -0
  78. package/build/client/_app/immutable/chunks/DSU1n5N_.js.gz +0 -0
  79. package/build/client/_app/immutable/chunks/DVkn4r72.js +1 -0
  80. package/build/client/_app/immutable/chunks/DVkn4r72.js.br +0 -0
  81. package/build/client/_app/immutable/chunks/DVkn4r72.js.gz +0 -0
  82. package/build/client/_app/immutable/chunks/DjsDGxCa.js +41 -0
  83. package/build/client/_app/immutable/chunks/DjsDGxCa.js.br +0 -0
  84. package/build/client/_app/immutable/chunks/DjsDGxCa.js.gz +0 -0
  85. package/build/client/_app/immutable/chunks/PPVm8Dsz.js.gz +0 -0
  86. package/build/client/_app/immutable/chunks/UJOiqIYE.js +1 -0
  87. package/build/client/_app/immutable/chunks/UJOiqIYE.js.br +0 -0
  88. package/build/client/_app/immutable/chunks/UJOiqIYE.js.gz +0 -0
  89. package/build/client/_app/immutable/chunks/r0JawsZc.js +2 -0
  90. package/build/client/_app/immutable/chunks/r0JawsZc.js.br +0 -0
  91. package/build/client/_app/immutable/chunks/r0JawsZc.js.gz +0 -0
  92. package/build/client/_app/immutable/entry/app.Z3zMnuSx.js +2 -0
  93. package/build/client/_app/immutable/entry/app.Z3zMnuSx.js.br +0 -0
  94. package/build/client/_app/immutable/entry/app.Z3zMnuSx.js.gz +0 -0
  95. package/build/client/_app/immutable/entry/start.Dd-gIP4y.js +1 -0
  96. package/build/client/_app/immutable/entry/start.Dd-gIP4y.js.br +2 -0
  97. package/build/client/_app/immutable/entry/start.Dd-gIP4y.js.gz +0 -0
  98. package/build/client/_app/immutable/nodes/0.D2YR8tTD.js +1 -0
  99. package/build/client/_app/immutable/nodes/0.D2YR8tTD.js.br +0 -0
  100. package/build/client/_app/immutable/nodes/0.D2YR8tTD.js.gz +0 -0
  101. package/build/client/_app/immutable/nodes/1.B3m6rO4C.js +1 -0
  102. package/build/client/_app/immutable/nodes/1.B3m6rO4C.js.br +0 -0
  103. package/build/client/_app/immutable/nodes/1.B3m6rO4C.js.gz +0 -0
  104. package/build/client/_app/immutable/nodes/2.CyRB2euU.js +1 -0
  105. package/build/client/_app/immutable/nodes/2.CyRB2euU.js.br +0 -0
  106. package/build/client/_app/immutable/nodes/2.CyRB2euU.js.gz +0 -0
  107. package/build/client/_app/immutable/nodes/3.3yohCM25.js +3 -0
  108. package/build/client/_app/immutable/nodes/3.3yohCM25.js.br +0 -0
  109. package/build/client/_app/immutable/nodes/3.3yohCM25.js.gz +0 -0
  110. package/build/client/_app/immutable/nodes/4.DEAcwl7l.js +1 -0
  111. package/build/client/_app/immutable/nodes/4.DEAcwl7l.js.br +0 -0
  112. package/build/client/_app/immutable/nodes/4.DEAcwl7l.js.gz +0 -0
  113. package/build/client/_app/immutable/nodes/5.C6bLGWQR.js +4 -0
  114. package/build/client/_app/immutable/nodes/5.C6bLGWQR.js.br +0 -0
  115. package/build/client/_app/immutable/nodes/5.C6bLGWQR.js.gz +0 -0
  116. package/build/client/_app/immutable/nodes/6.ByTzlA2D.js +2 -0
  117. package/build/client/_app/immutable/nodes/6.ByTzlA2D.js.br +0 -0
  118. package/build/client/_app/immutable/nodes/6.ByTzlA2D.js.gz +0 -0
  119. package/build/client/_app/immutable/nodes/7.BPMfwzd2.js +2 -0
  120. package/build/client/_app/immutable/nodes/7.BPMfwzd2.js.br +0 -0
  121. package/build/client/_app/immutable/nodes/7.BPMfwzd2.js.gz +0 -0
  122. package/build/client/_app/version.json +1 -1
  123. package/build/client/_app/version.json.br +0 -0
  124. package/build/client/_app/version.json.gz +0 -0
  125. package/build/client/favicon.svg.gz +0 -0
  126. package/build/client/manifest.json +13 -0
  127. package/build/client/manifest.json.br +0 -0
  128. package/build/client/manifest.json.gz +0 -0
  129. package/build/pty-holder.cjs +37 -8
  130. package/build/server/chunks/0-Vk38tI2J.js +9 -0
  131. package/build/server/chunks/0-Vk38tI2J.js.map +1 -0
  132. package/build/server/chunks/1-BvYQX5MR.js +9 -0
  133. package/build/server/chunks/1-BvYQX5MR.js.map +1 -0
  134. package/build/server/chunks/2-Cl7R4Qk2.js +9 -0
  135. package/build/server/chunks/2-Cl7R4Qk2.js.map +1 -0
  136. package/build/server/chunks/3-Ck7ewhOX.js +9 -0
  137. package/build/server/chunks/3-Ck7ewhOX.js.map +1 -0
  138. package/build/server/chunks/4-CnDeRm2Z.js +9 -0
  139. package/build/server/chunks/4-CnDeRm2Z.js.map +1 -0
  140. package/build/server/chunks/5-IxitzEvN.js +9 -0
  141. package/build/server/chunks/5-IxitzEvN.js.map +1 -0
  142. package/build/server/chunks/6-CyZ3r1iS.js +9 -0
  143. package/build/server/chunks/6-CyZ3r1iS.js.map +1 -0
  144. package/build/server/chunks/7-BmI7du46.js +9 -0
  145. package/build/server/chunks/7-BmI7du46.js.map +1 -0
  146. package/build/server/chunks/Button-Cs1aE6ka.js +80 -0
  147. package/build/server/chunks/Button-Cs1aE6ka.js.map +1 -0
  148. package/build/server/chunks/EmptyState-DDFH1K8g.js +26 -0
  149. package/build/server/chunks/EmptyState-DDFH1K8g.js.map +1 -0
  150. package/build/server/chunks/Icon-CEUrotA6.js +36 -0
  151. package/build/server/chunks/Icon-CEUrotA6.js.map +1 -0
  152. package/build/server/chunks/Shimmer-DB8W1zt6.js +10 -0
  153. package/build/server/chunks/Shimmer-DB8W1zt6.js.map +1 -0
  154. package/build/server/chunks/_error.svelte-uCOJNxvr.js +39 -0
  155. package/build/server/chunks/_error.svelte-uCOJNxvr.js.map +1 -0
  156. package/build/server/chunks/_layout.svelte-CtWmEJwe.js +56 -0
  157. package/build/server/chunks/_layout.svelte-CtWmEJwe.js.map +1 -0
  158. package/build/server/chunks/_page.svelte-BcZaKdX9.js +45 -0
  159. package/build/server/chunks/_page.svelte-BcZaKdX9.js.map +1 -0
  160. package/build/server/chunks/_page.svelte-BdYynOck.js +85 -0
  161. package/build/server/chunks/_page.svelte-BdYynOck.js.map +1 -0
  162. package/build/server/chunks/_page.svelte-BgevQjq1.js +101 -0
  163. package/build/server/chunks/_page.svelte-BgevQjq1.js.map +1 -0
  164. package/build/server/chunks/_page.svelte-CVq6tRb3.js +550 -0
  165. package/build/server/chunks/_page.svelte-CVq6tRb3.js.map +1 -0
  166. package/build/server/chunks/_page.svelte-CxWcQ0Am.js +651 -0
  167. package/build/server/chunks/_page.svelte-CxWcQ0Am.js.map +1 -0
  168. package/build/server/chunks/_page.svelte-DO4oa_LY.js +44 -0
  169. package/build/server/chunks/_page.svelte-DO4oa_LY.js.map +1 -0
  170. package/build/server/chunks/{_server.ts-CbDRDIoP.js → _server.ts-BStnNIcq.js} +9 -11
  171. package/build/server/chunks/_server.ts-BStnNIcq.js.map +1 -0
  172. package/build/server/chunks/{_server.ts-DRVbgm6k.js → _server.ts-CAxsWKvS.js} +22 -25
  173. package/build/server/chunks/_server.ts-CAxsWKvS.js.map +1 -0
  174. package/build/server/chunks/{_server.ts-CPa6DgIt.js → _server.ts-COu0vNpd.js} +6 -6
  175. package/build/server/chunks/_server.ts-COu0vNpd.js.map +1 -0
  176. package/build/server/chunks/{_server.ts-C29xzfaw.js → _server.ts-CTpcLUH8.js} +10 -10
  177. package/build/server/chunks/_server.ts-CTpcLUH8.js.map +1 -0
  178. package/build/server/chunks/{_server.ts-D4MNi4cD.js → _server.ts-Cf84YIaW.js} +3 -3
  179. package/build/server/chunks/{_server.ts-D4MNi4cD.js.map → _server.ts-Cf84YIaW.js.map} +1 -1
  180. package/build/server/chunks/{_server.ts-BL2FGb5Z.js → _server.ts-Ch-6iOHp.js} +99 -53
  181. package/build/server/chunks/_server.ts-Ch-6iOHp.js.map +1 -0
  182. package/build/server/chunks/_server.ts-CtH0dhUp.js +71 -0
  183. package/build/server/chunks/_server.ts-CtH0dhUp.js.map +1 -0
  184. package/build/server/chunks/_server.ts-DB_Kg97c.js +73 -0
  185. package/build/server/chunks/_server.ts-DB_Kg97c.js.map +1 -0
  186. package/build/server/chunks/{_server.ts-DfajWaqh.js → _server.ts-DV8zTCF9.js} +7 -9
  187. package/build/server/chunks/_server.ts-DV8zTCF9.js.map +1 -0
  188. package/build/server/chunks/_server.ts-DYpJImqd.js +99 -0
  189. package/build/server/chunks/_server.ts-DYpJImqd.js.map +1 -0
  190. package/build/server/chunks/{_server.ts-ColfDHW8.js → _server.ts-DYvb9ijZ.js} +21 -10
  191. package/build/server/chunks/_server.ts-DYvb9ijZ.js.map +1 -0
  192. package/build/server/chunks/{_server.ts-Cv_OrRuL.js → _server.ts-Deok2y88.js} +209 -34
  193. package/build/server/chunks/_server.ts-Deok2y88.js.map +1 -0
  194. package/build/server/chunks/{_server.ts-y9-WYDMa.js → _server.ts-WhTJBEJy.js} +5 -4
  195. package/build/server/chunks/{_server.ts-y9-WYDMa.js.map → _server.ts-WhTJBEJy.js.map} +1 -1
  196. package/build/server/chunks/{_server.ts-BjOJsoy4.js → _server.ts-XzT2UHM1.js} +6 -5
  197. package/build/server/chunks/_server.ts-XzT2UHM1.js.map +1 -0
  198. package/build/server/chunks/{_server.ts-BgdjBZco.js → _server.ts-tSpgyl1D.js} +7 -5
  199. package/build/server/chunks/_server.ts-tSpgyl1D.js.map +1 -0
  200. package/build/server/chunks/{_server.ts-BihKSdj_.js → _server.ts-vekTmWAx.js} +8 -8
  201. package/build/server/chunks/_server.ts-vekTmWAx.js.map +1 -0
  202. package/build/server/chunks/{auth-CEgFis71.js → auth-DeCdZ83n.js} +2 -2
  203. package/build/server/chunks/{auth-CEgFis71.js.map → auth-DeCdZ83n.js.map} +1 -1
  204. package/build/server/chunks/client-BdGHe_hY.js +25 -0
  205. package/build/server/chunks/client-BdGHe_hY.js.map +1 -0
  206. package/build/server/chunks/client2-CCBGA-2V.js +7 -0
  207. package/build/server/chunks/client2-CCBGA-2V.js.map +1 -0
  208. package/build/server/chunks/error-DDXB3duW.js +12 -0
  209. package/build/server/chunks/error-DDXB3duW.js.map +1 -0
  210. package/build/server/chunks/{exports-CJ0Q5XmL.js → index-DwaY1cAm.js} +1111 -1634
  211. package/build/server/chunks/index-DwaY1cAm.js.map +1 -0
  212. package/build/server/chunks/index-server-CrDaL06Y.js +9 -0
  213. package/build/server/chunks/index-server-CrDaL06Y.js.map +1 -0
  214. package/build/server/chunks/index2-CgclKpUj.js +58 -0
  215. package/build/server/chunks/index2-CgclKpUj.js.map +1 -0
  216. package/build/server/chunks/{library-apns-BHxLmuIx.js → library-apns-BqJbvSKh.js} +4 -4
  217. package/build/server/chunks/library-apns-BqJbvSKh.js.map +1 -0
  218. package/build/server/chunks/markdown-W_mTBct0.js +8 -0
  219. package/build/server/chunks/markdown-W_mTBct0.js.map +1 -0
  220. package/build/server/chunks/opencode-db-path-DcfhJtJy.js +15 -0
  221. package/build/server/chunks/opencode-db-path-DcfhJtJy.js.map +1 -0
  222. package/build/server/chunks/{pty-manager-C0FhBiVq.js → pty-manager-BQVB7IVj.js} +155 -326
  223. package/build/server/chunks/pty-manager-BQVB7IVj.js.map +1 -0
  224. package/build/server/chunks/root-DDSnEAZv.js +1171 -0
  225. package/build/server/chunks/root-DDSnEAZv.js.map +1 -0
  226. package/build/server/chunks/{shared-server-BDY8jh20.js → shared-server-sSGG17Df.js} +2 -3
  227. package/build/server/chunks/{shared-server-BDY8jh20.js.map → shared-server-sSGG17Df.js.map} +1 -1
  228. package/build/server/chunks/state.svelte-hBbXlUak.js +11 -0
  229. package/build/server/chunks/state.svelte-hBbXlUak.js.map +1 -0
  230. package/build/server/chunks/stores-DHNzYNpX.js +28 -0
  231. package/build/server/chunks/stores-DHNzYNpX.js.map +1 -0
  232. package/build/server/index.js +1085 -2242
  233. package/build/server/index.js.map +1 -1
  234. package/build/server/manifest.js +39 -25
  235. package/build/server/manifest.js.map +1 -1
  236. package/package.json +32 -9
  237. package/scripts/fix-generated-types.sh +37 -0
  238. package/scripts/homebrew/shooter.rb +51 -0
  239. package/scripts/install.sh +348 -186
  240. package/scripts/setup.cjs +215 -45
  241. package/server.ts +114 -71
  242. package/src/app.css +12 -3
  243. package/src/app.d.ts +13 -20
  244. package/src/app.html +3 -2
  245. package/src/generated/types/API.ts +280 -0
  246. package/src/generated/types/APN.ts +186 -203
  247. package/src/generated/types/CLI.ts +18 -25
  248. package/src/generated/types/Client.ts +589 -0
  249. package/src/generated/types/Config.ts +53 -0
  250. package/src/generated/types/Holder.ts +638 -0
  251. package/src/generated/types/JWT.ts +39 -50
  252. package/src/generated/types/Notification.ts +426 -0
  253. package/src/generated/types/OpenCode.ts +356 -0
  254. package/src/generated/types/Sessions.ts +570 -0
  255. package/src/generated/types/Terminal.ts +2184 -2071
  256. package/src/generated/types/WsProtocol.ts +2004 -0
  257. package/src/generated/types/index.ts +9 -3
  258. package/src/lib/env.ts +29 -0
  259. package/src/lib/modules/client/common/cache.ts +10 -2
  260. package/src/lib/modules/client/common/config-guard.ts +37 -5
  261. package/src/lib/modules/client/common/error.ts +10 -0
  262. package/src/lib/modules/client/common/index.ts +6 -5
  263. package/src/lib/modules/client/common/markdown.ts +22 -1
  264. package/src/lib/modules/client/common/native-bridge.ts +28 -20
  265. package/src/lib/modules/client/common/time.ts +13 -11
  266. package/src/lib/modules/client/terminal/ChatView.svelte +354 -74
  267. package/src/lib/modules/client/terminal/CommandPalette.svelte +3 -2
  268. package/src/lib/modules/client/terminal/ConnectionStatus.svelte +7 -1
  269. package/src/lib/modules/client/terminal/LaunchSheet.svelte +147 -84
  270. package/src/lib/modules/client/terminal/QuickKeys.svelte +3 -1
  271. package/src/lib/modules/client/terminal/ShortcutsHelp.svelte +2 -5
  272. package/src/lib/modules/client/terminal/keyboard-shortcuts.ts +27 -24
  273. package/src/lib/modules/client/terminal/xterm-wrapper.ts +74 -45
  274. package/src/lib/modules/server/apn/library-apns.ts +3 -2
  275. package/src/lib/modules/server/apn/notification-history.ts +2 -13
  276. package/src/lib/modules/server/apn/notification-sessions.ts +3 -13
  277. package/src/lib/modules/server/apn/pending-requests.ts +3 -8
  278. package/src/lib/modules/server/apn/types.ts +5 -4
  279. package/src/lib/modules/server/cli/index.ts +3 -2
  280. package/src/lib/modules/server/fcm/fcm-service.ts +8 -6
  281. package/src/lib/modules/server/sessions/jsonl-parser.ts +3 -3
  282. package/src/lib/modules/server/sessions/jsonl-reader.ts +86 -26
  283. package/src/lib/modules/server/sessions/opencode-db-path.ts +26 -0
  284. package/src/lib/modules/server/sessions/opencode-reader.ts +13 -15
  285. package/src/lib/modules/server/sessions/process-detector.ts +103 -0
  286. package/src/lib/modules/server/sessions/types.ts +11 -22
  287. package/src/lib/modules/server/terminal/holder-client.ts +272 -248
  288. package/src/lib/modules/server/terminal/opencode-watcher.ts +547 -556
  289. package/src/lib/modules/server/terminal/pty-holder.cjs +37 -8
  290. package/src/lib/modules/server/terminal/pty-manager.ts +157 -115
  291. package/src/lib/modules/server/terminal/session-watcher.ts +6 -4
  292. package/src/lib/modules/server/terminal/terminal-store.ts +131 -128
  293. package/src/lib/modules/server/utils/error.ts +9 -0
  294. package/src/lib/modules/server/ws/events-handler.ts +12 -6
  295. package/src/lib/modules/server/ws/keepalive.ts +86 -69
  296. package/src/lib/modules/server/ws/server.ts +43 -37
  297. package/src/lib/modules/server/ws/session-handler.ts +332 -147
  298. package/src/lib/modules/server/ws/terminal-handler.ts +29 -17
  299. package/src/lib/modules/server/ws/ticket-store.ts +29 -26
  300. package/src/lib/theme.css +30 -0
  301. package/src/lib/types/config.ts +1 -6
  302. package/src/routes/+error.svelte +94 -0
  303. package/src/routes/+layout.svelte +66 -31
  304. package/src/routes/+page.svelte +25 -22
  305. package/src/routes/api/debug/+server.ts +3 -1
  306. package/src/routes/api/device-token/+server.ts +60 -60
  307. package/src/routes/api/health/+server.ts +81 -73
  308. package/src/routes/api/notify/+server.ts +115 -68
  309. package/src/routes/api/qr-config/+server.ts +30 -32
  310. package/src/routes/api/response/+server.ts +9 -4
  311. package/src/routes/api/sessions/+server.ts +15 -5
  312. package/src/routes/api/sessions/connect/+server.ts +125 -0
  313. package/src/routes/api/sessions/detect/+server.ts +27 -0
  314. package/src/routes/api/terminals/+server.ts +26 -24
  315. package/src/routes/api/terminals/[id]/+server.ts +13 -7
  316. package/src/routes/api/terminals/[id]/paste-image/+server.ts +54 -52
  317. package/src/routes/api/terminals/[id]/resize/+server.ts +6 -3
  318. package/src/routes/api/webhook/+server.ts +8 -10
  319. package/src/routes/api/ws-status/+server.ts +7 -5
  320. package/src/routes/api/ws-ticket/+server.ts +42 -41
  321. package/src/routes/config/+page.svelte +149 -75
  322. package/src/routes/project/+page.svelte +165 -35
  323. package/src/routes/session/[id]/+page.svelte +479 -283
  324. package/src/routes/terminals/+page.svelte +58 -45
  325. package/src/routes/terminals/[id]/+page.svelte +223 -91
  326. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.br +0 -0
  327. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.gz +0 -0
  328. package/build/client/_app/immutable/assets/3.C0uFg0IS.css +0 -1
  329. package/build/client/_app/immutable/assets/3.C0uFg0IS.css.br +0 -0
  330. package/build/client/_app/immutable/assets/3.C0uFg0IS.css.gz +0 -0
  331. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css +0 -1
  332. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.br +0 -0
  333. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.gz +0 -0
  334. package/build/client/_app/immutable/assets/5.DRjApZQW.css +0 -1
  335. package/build/client/_app/immutable/assets/5.DRjApZQW.css.br +0 -0
  336. package/build/client/_app/immutable/assets/5.DRjApZQW.css.gz +0 -0
  337. package/build/client/_app/immutable/assets/6.AraZrY8I.css +0 -1
  338. package/build/client/_app/immutable/assets/6.AraZrY8I.css.br +0 -0
  339. package/build/client/_app/immutable/assets/6.AraZrY8I.css.gz +0 -0
  340. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css +0 -1
  341. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.br +0 -0
  342. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.gz +0 -0
  343. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css +0 -1
  344. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.br +0 -0
  345. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.gz +0 -0
  346. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css +0 -1
  347. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.br +0 -0
  348. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.gz +0 -0
  349. package/build/client/_app/immutable/chunks/BNJphC1q.js +0 -56
  350. package/build/client/_app/immutable/chunks/BNJphC1q.js.br +0 -0
  351. package/build/client/_app/immutable/chunks/BNJphC1q.js.gz +0 -0
  352. package/build/client/_app/immutable/chunks/Bvk7mfPM.js +0 -1
  353. package/build/client/_app/immutable/chunks/Bvk7mfPM.js.br +0 -0
  354. package/build/client/_app/immutable/chunks/Bvk7mfPM.js.gz +0 -0
  355. package/build/client/_app/immutable/chunks/CAokzuPQ.js +0 -1
  356. package/build/client/_app/immutable/chunks/CAokzuPQ.js.br +0 -0
  357. package/build/client/_app/immutable/chunks/CAokzuPQ.js.gz +0 -0
  358. package/build/client/_app/immutable/chunks/CGLrx-H5.js +0 -1
  359. package/build/client/_app/immutable/chunks/CGLrx-H5.js.br +0 -0
  360. package/build/client/_app/immutable/chunks/CGLrx-H5.js.gz +0 -0
  361. package/build/client/_app/immutable/chunks/CgCpWzEA.js +0 -1
  362. package/build/client/_app/immutable/chunks/CgCpWzEA.js.br +0 -0
  363. package/build/client/_app/immutable/chunks/CgCpWzEA.js.gz +0 -0
  364. package/build/client/_app/immutable/chunks/Cjwk_cGO.js +0 -6
  365. package/build/client/_app/immutable/chunks/Cjwk_cGO.js.br +0 -0
  366. package/build/client/_app/immutable/chunks/Cjwk_cGO.js.gz +0 -0
  367. package/build/client/_app/immutable/chunks/CtQ8EED1.js +0 -11
  368. package/build/client/_app/immutable/chunks/CtQ8EED1.js.br +0 -0
  369. package/build/client/_app/immutable/chunks/CtQ8EED1.js.gz +0 -0
  370. package/build/client/_app/immutable/chunks/DERQCisl.js +0 -1
  371. package/build/client/_app/immutable/chunks/DERQCisl.js.br +0 -0
  372. package/build/client/_app/immutable/chunks/DERQCisl.js.gz +0 -0
  373. package/build/client/_app/immutable/chunks/DKrg8TQs.js +0 -1
  374. package/build/client/_app/immutable/chunks/DKrg8TQs.js.br +0 -0
  375. package/build/client/_app/immutable/chunks/DKrg8TQs.js.gz +0 -0
  376. package/build/client/_app/immutable/chunks/Dkkpz_4D.js +0 -126
  377. package/build/client/_app/immutable/chunks/Dkkpz_4D.js.br +0 -0
  378. package/build/client/_app/immutable/chunks/Dkkpz_4D.js.gz +0 -0
  379. package/build/client/_app/immutable/chunks/DoczjQhA.js +0 -1
  380. package/build/client/_app/immutable/chunks/DoczjQhA.js.br +0 -0
  381. package/build/client/_app/immutable/chunks/DoczjQhA.js.gz +0 -0
  382. package/build/client/_app/immutable/chunks/RpcNruLP.js +0 -2
  383. package/build/client/_app/immutable/chunks/RpcNruLP.js.br +0 -0
  384. package/build/client/_app/immutable/chunks/RpcNruLP.js.gz +0 -0
  385. package/build/client/_app/immutable/chunks/a-St0Zwo.js +0 -1
  386. package/build/client/_app/immutable/chunks/a-St0Zwo.js.br +0 -0
  387. package/build/client/_app/immutable/chunks/a-St0Zwo.js.gz +0 -0
  388. package/build/client/_app/immutable/chunks/bo70OQUZ.js +0 -1
  389. package/build/client/_app/immutable/chunks/bo70OQUZ.js.br +0 -0
  390. package/build/client/_app/immutable/chunks/bo70OQUZ.js.gz +0 -0
  391. package/build/client/_app/immutable/entry/app.QvGgdvTI.js +0 -2
  392. package/build/client/_app/immutable/entry/app.QvGgdvTI.js.br +0 -0
  393. package/build/client/_app/immutable/entry/app.QvGgdvTI.js.gz +0 -0
  394. package/build/client/_app/immutable/entry/start.BntDNRMC.js +0 -1
  395. package/build/client/_app/immutable/entry/start.BntDNRMC.js.br +0 -0
  396. package/build/client/_app/immutable/entry/start.BntDNRMC.js.gz +0 -0
  397. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js +0 -1
  398. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.br +0 -0
  399. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.gz +0 -0
  400. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js +0 -1
  401. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.br +0 -0
  402. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.gz +0 -0
  403. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js +0 -1
  404. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.br +0 -0
  405. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.gz +0 -0
  406. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js +0 -3
  407. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.br +0 -0
  408. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.gz +0 -0
  409. package/build/client/_app/immutable/nodes/4.D-cIe70D.js +0 -1
  410. package/build/client/_app/immutable/nodes/4.D-cIe70D.js.br +0 -0
  411. package/build/client/_app/immutable/nodes/4.D-cIe70D.js.gz +0 -0
  412. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js +0 -1
  413. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.br +0 -0
  414. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.gz +0 -0
  415. package/build/client/_app/immutable/nodes/6.BB7QE48r.js +0 -2
  416. package/build/client/_app/immutable/nodes/6.BB7QE48r.js.br +0 -0
  417. package/build/client/_app/immutable/nodes/6.BB7QE48r.js.gz +0 -0
  418. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js +0 -2
  419. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.br +0 -0
  420. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.gz +0 -0
  421. package/build/client/manifest.webmanifest +0 -1
  422. package/build/client/registerSW.js +0 -1
  423. package/build/client/registerSW.js.br +0 -0
  424. package/build/client/registerSW.js.gz +0 -0
  425. package/build/client/sw.js +0 -222
  426. package/build/client/sw.js.br +0 -0
  427. package/build/client/sw.js.gz +0 -0
  428. package/build/client/workbox-5119daf5.js +0 -3395
  429. package/build/client/workbox-5119daf5.js.br +0 -0
  430. package/build/client/workbox-5119daf5.js.gz +0 -0
  431. package/build/server/chunks/0-q2IUp76Y.js +0 -9
  432. package/build/server/chunks/0-q2IUp76Y.js.map +0 -1
  433. package/build/server/chunks/1-CU50G5wZ.js +0 -9
  434. package/build/server/chunks/1-CU50G5wZ.js.map +0 -1
  435. package/build/server/chunks/2-D01t9s8T.js +0 -9
  436. package/build/server/chunks/2-D01t9s8T.js.map +0 -1
  437. package/build/server/chunks/3-5PUQ04wC.js +0 -9
  438. package/build/server/chunks/3-5PUQ04wC.js.map +0 -1
  439. package/build/server/chunks/4-e7gywnSG.js +0 -9
  440. package/build/server/chunks/4-e7gywnSG.js.map +0 -1
  441. package/build/server/chunks/5-CA1SA6KZ.js +0 -9
  442. package/build/server/chunks/5-CA1SA6KZ.js.map +0 -1
  443. package/build/server/chunks/6-71H221sV.js +0 -9
  444. package/build/server/chunks/6-71H221sV.js.map +0 -1
  445. package/build/server/chunks/7-Bo-vmdyz.js +0 -9
  446. package/build/server/chunks/7-Bo-vmdyz.js.map +0 -1
  447. package/build/server/chunks/_layout.svelte-SFHOxs74.js +0 -132
  448. package/build/server/chunks/_layout.svelte-SFHOxs74.js.map +0 -1
  449. package/build/server/chunks/_page.svelte-B4w-2wD-.js +0 -120
  450. package/build/server/chunks/_page.svelte-B4w-2wD-.js.map +0 -1
  451. package/build/server/chunks/_page.svelte-B_qAXjkh.js +0 -213
  452. package/build/server/chunks/_page.svelte-B_qAXjkh.js.map +0 -1
  453. package/build/server/chunks/_page.svelte-CsF1_TRG.js +0 -50
  454. package/build/server/chunks/_page.svelte-CsF1_TRG.js.map +0 -1
  455. package/build/server/chunks/_page.svelte-DJC6U-P0.js +0 -68
  456. package/build/server/chunks/_page.svelte-DJC6U-P0.js.map +0 -1
  457. package/build/server/chunks/_page.svelte-DQ6HBtsz.js +0 -407
  458. package/build/server/chunks/_page.svelte-DQ6HBtsz.js.map +0 -1
  459. package/build/server/chunks/_page.svelte-LbhhjP21.js +0 -148
  460. package/build/server/chunks/_page.svelte-LbhhjP21.js.map +0 -1
  461. package/build/server/chunks/_server.ts-BL2FGb5Z.js.map +0 -1
  462. package/build/server/chunks/_server.ts-BgdjBZco.js.map +0 -1
  463. package/build/server/chunks/_server.ts-BihKSdj_.js.map +0 -1
  464. package/build/server/chunks/_server.ts-BjOJsoy4.js.map +0 -1
  465. package/build/server/chunks/_server.ts-C29xzfaw.js.map +0 -1
  466. package/build/server/chunks/_server.ts-CPa6DgIt.js.map +0 -1
  467. package/build/server/chunks/_server.ts-CbDRDIoP.js.map +0 -1
  468. package/build/server/chunks/_server.ts-Cl1OEWL4.js +0 -54
  469. package/build/server/chunks/_server.ts-Cl1OEWL4.js.map +0 -1
  470. package/build/server/chunks/_server.ts-ColfDHW8.js.map +0 -1
  471. package/build/server/chunks/_server.ts-Cv_OrRuL.js.map +0 -1
  472. package/build/server/chunks/_server.ts-DRVbgm6k.js.map +0 -1
  473. package/build/server/chunks/_server.ts-DfajWaqh.js.map +0 -1
  474. package/build/server/chunks/client-CxCatAKr.js +0 -255
  475. package/build/server/chunks/client-CxCatAKr.js.map +0 -1
  476. package/build/server/chunks/error.svelte-BqdwMWdK.js +0 -26
  477. package/build/server/chunks/error.svelte-BqdwMWdK.js.map +0 -1
  478. package/build/server/chunks/exports-CJ0Q5XmL.js.map +0 -1
  479. package/build/server/chunks/index2-DAxIoAO-.js +0 -36
  480. package/build/server/chunks/index2-DAxIoAO-.js.map +0 -1
  481. package/build/server/chunks/jsonl-parser-dmZU_Hyu.js +0 -137
  482. package/build/server/chunks/jsonl-parser-dmZU_Hyu.js.map +0 -1
  483. package/build/server/chunks/library-apns-BHxLmuIx.js.map +0 -1
  484. package/build/server/chunks/markdown-Bxrl3cCF.js +0 -1241
  485. package/build/server/chunks/markdown-Bxrl3cCF.js.map +0 -1
  486. package/build/server/chunks/pty-manager-C0FhBiVq.js.map +0 -1
  487. package/build/server/chunks/stores-D0HorpgL.js +0 -36
  488. package/build/server/chunks/stores-D0HorpgL.js.map +0 -1
@@ -11,21 +11,23 @@
11
11
  * mode to avoid contention.
12
12
  */
13
13
 
14
+ import type {
15
+ OpenCodeMessage,
16
+ OpenCodePart,
17
+ OpenCodePartData,
18
+ OpenCodeSession,
19
+ } from '$generated/types';
20
+
14
21
  import Database from 'better-sqlite3';
15
22
  import * as fs from 'fs';
16
- import * as path from 'path';
17
23
 
18
24
  import type { ConversationMessage, MessagePart } from '../sessions/types';
19
25
 
26
+ import { resolveOpenCodeDbPath } from '../sessions/opencode-db-path';
27
+
20
28
  // ── Constants ────────────────────────────────────────────────────────
21
29
 
22
- const OPENCODE_DB_PATH = (() => {
23
- if (process.platform === 'darwin') {
24
- return path.join(process.env.HOME || '', 'Library', 'Application Support', 'opencode', 'opencode.db');
25
- }
26
- const xdgData = process.env.XDG_DATA_HOME || path.join(process.env.HOME || '', '.local', 'share');
27
- return path.join(xdgData, 'opencode', 'opencode.db');
28
- })();
30
+ const OPENCODE_DB_PATH = resolveOpenCodeDbPath();
29
31
 
30
32
  /** Poll interval in milliseconds. */
31
33
  const POLL_INTERVAL_MS = 2000;
@@ -33,621 +35,610 @@ const POLL_INTERVAL_MS = 2000;
33
35
  /** Maximum parameters per SQLite IN clause (SQLite limit is 999). */
34
36
  const SQLITE_MAX_PARAMS = 500;
35
37
 
36
- /**
37
- * Normalise a timestamp from OpenCode's SQLite database to milliseconds.
38
- *
39
- * OpenCode currently stores `time_created` / `time_updated` as Unix
40
- * **milliseconds**, but this is not formally documented and could change.
41
- * A simple heuristic distinguishes seconds from milliseconds:
42
- * - Values < 1e12 (~2001-09-09 in ms, ~33658 AD in seconds) are seconds.
43
- * - Values >= 1e12 are already milliseconds.
44
- */
45
- function toMillis(timestamp: number): number {
46
- return timestamp < 1e12 ? timestamp * 1000 : timestamp;
47
- }
48
-
49
- // ── SQLite Row Types ─────────────────────────────────────────────────
50
-
51
- interface OpenCodeMessage {
52
- data: string; // JSON
53
- id: string;
54
- session_id: string;
55
- time_created: number;
56
- time_updated: number;
57
- }
58
-
59
- interface OpenCodePart {
60
- data: string; // JSON
61
- id: string;
62
- message_id: string;
63
- session_id: string;
64
- time_created: number;
65
- time_updated: number;
66
- }
67
-
68
- /** Raw part data as stored in OpenCode's SQLite `part.data` JSON column. */
69
- interface OpenCodePartData {
70
- callID?: string;
71
- id?: string;
72
- state?: { input?: Record<string, unknown> };
73
- text?: string;
74
- tool?: string;
75
- type: string;
76
- }
77
-
78
38
  // ── Per-session Watcher State ────────────────────────────────────────
79
-
80
- interface OpenCodeSession {
81
- directory: string;
82
- id: string;
83
- time_created: number;
84
- time_updated: number;
85
- }
39
+ // WatchState is a runtime/behavioral type (callbacks, emittedSets,
40
+ // intervalHandle) and stays local.
86
41
 
87
42
  interface WatchState {
88
- callbacks: Set<(messages: ConversationMessage[]) => void>;
89
- /** Set of message IDs we have already emitted, to avoid duplicates. */
90
- emittedMessageIds: Set<string>;
91
- /** Set of part IDs we have already emitted, to avoid duplicates on update. */
92
- emittedPartIds: Set<string>;
93
- intervalHandle: ReturnType<typeof setInterval>;
94
- /** Highest time_created we have seen for messages (milliseconds). */
95
- lastMessageTime: number;
96
- /** Highest time_updated we have seen for parts (milliseconds). */
97
- lastPartTime: number;
98
- sessionId: string;
43
+ callbacks: Set<(messages: ConversationMessage[]) => void>;
44
+ /** Set of message IDs we have already emitted, to avoid duplicates. */
45
+ emittedMessageIds: Set<string>;
46
+ /** Map of part ID to last-seen time_updated, to detect in-place updates. */
47
+ emittedPartUpdatedAt: Map<string, number>;
48
+ intervalHandle: ReturnType<typeof setInterval>;
49
+ /** Highest time_created we have seen for messages (milliseconds). */
50
+ lastMessageTime: number;
51
+ /** Highest time_updated we have seen for parts (milliseconds). */
52
+ lastPartTime: number;
53
+ sessionId: string;
99
54
  }
100
55
 
101
- // ── OpenCodeWatcher Class ────────────────────────────────────────────
102
-
103
56
  class OpenCodeWatcher {
104
- private watchers = new Map<string, WatchState>();
105
-
106
- /**
107
- * Find the most recent non-archived OpenCode session that matches
108
- * the given working directory. Checks session.directory equals or
109
- * starts with `cwd`.
110
- *
111
- * Returns the session ID or null if none found.
112
- */
113
- findSessionId(cwd: string, createdAfter?: number): null | string {
114
- const db = openDb();
115
- if (!db) {
116
- return null;
117
- }
118
-
119
- try {
120
- // Match sessions that were active (updated) after the terminal was launched.
121
- // OpenCode resumes existing sessions rather than always creating new ones,
122
- // so we filter on time_updated (not time_created) to find the session
123
- // that's being actively used by this terminal instance.
124
- //
125
- // createdAfter is JS milliseconds (Date.getTime()), but OpenCode may
126
- // store time_updated in seconds. Use the smaller of the two
127
- // representations so the filter works regardless of DB unit.
128
- const timeFilter = createdAfter
129
- ? Math.min(createdAfter, Math.floor(createdAfter / 1000))
130
- : 0;
131
- const row = db
132
- .prepare(
133
- `
57
+ private watchers = new Map<string, WatchState>();
58
+
59
+ /**
60
+ * Find the most recent non-archived OpenCode session that matches
61
+ * the given working directory. Checks session.directory equals or
62
+ * starts with `cwd`.
63
+ *
64
+ * Returns the session ID or null if none found.
65
+ */
66
+ findSessionId(cwd: string, createdAfter?: number): null | string {
67
+ const db = openDb();
68
+ if (!db) {
69
+ return null;
70
+ }
71
+
72
+ try {
73
+ // Match sessions that were active (updated) after the terminal was launched.
74
+ // OpenCode resumes existing sessions rather than always creating new ones,
75
+ // so we filter on time_updated (not time_created) to find the session
76
+ // that's being actively used by this terminal instance.
77
+ //
78
+ // createdAfter is JS milliseconds (Date.getTime()), but OpenCode may
79
+ // store time_updated in either seconds or milliseconds. Provide both
80
+ // representations and let the SQL match whichever unit the DB uses:
81
+ // - If time_updated is in seconds: compare against secondsFilter
82
+ // - If time_updated is in milliseconds (>= 1e12): compare against millisFilter
83
+ const millisFilter = createdAfter ?? 0;
84
+ const secondsFilter = createdAfter ? Math.floor(createdAfter / 1000) : 0;
85
+ // Escape LIKE metacharacters (%, _) in the cwd to prevent false
86
+ // matches on paths containing those characters. Uses \ as the
87
+ // escape character declared via the ESCAPE clause.
88
+ const cwdLikePattern = `${cwd.replace(/[%_\\]/g, '\\$&')}/%`;
89
+ const row = db
90
+ .prepare(
91
+ `
134
92
  SELECT id
135
93
  FROM session
136
94
  WHERE (time_archived IS NULL OR time_archived = 0)
137
- AND (directory = ? OR directory LIKE ? || '/%')
138
- AND time_updated > ?
95
+ AND (directory = ? OR directory LIKE ? ESCAPE '\\')
96
+ AND (
97
+ (time_updated >= 1e12 AND time_updated > ?)
98
+ OR (time_updated < 1e12 AND time_updated > ?)
99
+ )
139
100
  ORDER BY time_updated DESC
140
101
  LIMIT 1
141
102
  `
142
- )
143
- .get(cwd, cwd, timeFilter) as OpenCodeSession | undefined;
144
-
145
- return row?.id ?? null;
146
- } catch (error) {
147
- console.error('[opencode-watcher] Failed to find session:', error);
148
- return null;
149
- } finally {
150
- db.close();
151
- }
152
- }
153
-
154
- /**
155
- * Read all messages and parts for a session from SQLite, converting
156
- * them to ConversationMessage format.
157
- */
158
- getHistory(sessionId: string): ConversationMessage[] {
159
- const db = openDb();
160
- if (!db) {
161
- return [];
162
- }
163
-
164
- try {
165
- // Fetch all messages for this session, ordered chronologically.
166
- const messages = db
167
- .prepare(
168
- `
103
+ )
104
+ .get(cwd, cwdLikePattern, millisFilter, secondsFilter) as OpenCodeSession | undefined;
105
+
106
+ return row?.id ?? null;
107
+ } catch (error) {
108
+ console.error('[opencode-watcher] Failed to find session:', error);
109
+ return null;
110
+ } finally {
111
+ db.close();
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Read all messages and parts for a session from SQLite, converting
117
+ * them to ConversationMessage format.
118
+ */
119
+ getHistory(sessionId: string): ConversationMessage[] {
120
+ const db = openDb();
121
+ if (!db) {
122
+ return [];
123
+ }
124
+
125
+ try {
126
+ // Fetch all messages for this session, ordered chronologically.
127
+ const messages = db
128
+ .prepare(
129
+ `
169
130
  SELECT id, session_id, time_created, time_updated, data
170
131
  FROM message
171
132
  WHERE session_id = ?
172
133
  ORDER BY time_created ASC
173
134
  `
174
- )
175
- .all(sessionId) as OpenCodeMessage[];
176
-
177
- if (messages.length === 0) {
178
- return [];
179
- }
180
-
181
- // Fetch all parts for these messages (batched to avoid SQLite param limit).
182
- const messageIds = messages.map((m) => m.id);
183
- const parts = batchInQuery<OpenCodePart>(
184
- db,
185
- `SELECT id, message_id, session_id, time_created, time_updated, data
135
+ )
136
+ .all(sessionId) as OpenCodeMessage[];
137
+
138
+ if (messages.length === 0) {
139
+ return [];
140
+ }
141
+
142
+ // Fetch all parts for these messages (batched to avoid SQLite param limit).
143
+ const messageIds = messages.map((m) => m.id);
144
+ const parts = batchInQuery<OpenCodePart>(
145
+ db,
146
+ `SELECT id, message_id, session_id, time_created, time_updated, data
186
147
  FROM part
187
148
  WHERE message_id IN (__PLACEHOLDERS__)
188
149
  ORDER BY time_created ASC`,
189
- messageIds
190
- );
191
-
192
- // Group parts by message ID.
193
- const partsByMessage = new Map<string, OpenCodePart[]>();
194
- for (const part of parts) {
195
- if (!partsByMessage.has(part.message_id)) {
196
- partsByMessage.set(part.message_id, []);
197
- }
198
- partsByMessage.get(part.message_id)!.push(part);
199
- }
200
-
201
- return this.buildMessages(messages, partsByMessage);
202
- } catch (error) {
203
- console.error('[opencode-watcher] Failed to read history:', error);
204
- return [];
205
- } finally {
206
- db.close();
207
- }
208
- }
209
-
210
- /**
211
- * Stop watching a specific session. If a callback is provided, only that
212
- * subscriber is removed — the interval keeps running while other subscribers
213
- * remain. If no callback is provided, all subscribers and the interval are
214
- * cleared (backward compat).
215
- */
216
- stop(sessionId: string, callback?: (messages: ConversationMessage[]) => void): void {
217
- const state = this.watchers.get(sessionId);
218
- if (!state) {
219
- return;
220
- }
221
-
222
- if (callback) {
223
- state.callbacks.delete(callback);
224
- console.log(
225
- `[opencode-watcher] Removed subscriber from session: ${sessionId} ` +
226
- `(remaining=${state.callbacks.size})`
227
- );
228
-
229
- // Only tear down the interval when no subscribers remain.
230
- if (state.callbacks.size > 0) {
231
- return;
232
- }
233
- }
234
-
235
- clearInterval(state.intervalHandle);
236
- this.watchers.delete(sessionId);
237
- console.log(`[opencode-watcher] Stopped watching session: ${sessionId}`);
238
- }
239
-
240
- /**
241
- * Stop all active watchers.
242
- */
243
- stopAll(): void {
244
- for (const [sessionId] of this.watchers) {
245
- this.stop(sessionId);
246
- }
247
- }
248
-
249
- /**
250
- * Start polling the SQLite DB every 2 seconds for new messages/parts
251
- * in the given session. Converts new data to ConversationMessage
252
- * format and invokes the callback.
253
- */
254
- watch(sessionId: string, callback: (messages: ConversationMessage[]) => void): void {
255
- const existing = this.watchers.get(sessionId);
256
- if (existing) {
257
- // Already watching — just add the new callback, don't create a new interval.
258
- existing.callbacks.add(callback);
259
- console.log(
260
- `[opencode-watcher] Added subscriber to session: ${sessionId} ` +
261
- `(total=${existing.callbacks.size})`
262
- );
263
- return;
264
- }
265
-
266
- // Determine the initial high-water marks by scanning existing data.
267
- const { emittedMessageIds, emittedPartIds, lastMessageTime, lastPartTime } =
268
- this.getHighWaterMarks(sessionId);
269
-
270
- const intervalHandle = setInterval(() => {
271
- this.poll(sessionId);
272
- }, POLL_INTERVAL_MS);
273
-
274
- const state: WatchState = {
275
- callbacks: new Set([callback]),
276
- emittedMessageIds,
277
- emittedPartIds,
278
- intervalHandle,
279
- lastMessageTime,
280
- lastPartTime,
281
- sessionId
282
- };
283
-
284
- this.watchers.set(sessionId, state);
285
- console.log(
286
- `[opencode-watcher] Watching session: ${sessionId} ` +
287
- `(lastMsg=${lastMessageTime}, lastPart=${lastPartTime})`
288
- );
289
- }
290
-
291
- // ── Private Helpers ────────────────────────────────────────────────
292
-
293
- /**
294
- * Convert OpenCode messages + their parts into ConversationMessage
295
- * objects for consumption by the session handler.
296
- */
297
- private buildMessages(
298
- messages: OpenCodeMessage[],
299
- partsByMessage: Map<string, OpenCodePart[]>
300
- ): ConversationMessage[] {
301
- const result: ConversationMessage[] = [];
302
-
303
- for (const msg of messages) {
304
- // Parse message data to determine role.
305
- let msgData: { agent?: string; role?: string } = {};
306
- try {
307
- msgData = JSON.parse(msg.data) as typeof msgData;
308
- } catch {
309
- // Skip unparseable message data.
310
- continue;
311
- }
312
-
313
- const role = msgData.role === 'user' ? 'user' : 'assistant';
314
- const msgParts = partsByMessage.get(msg.id) || [];
315
-
316
- // Convert each part to a MessagePart.
317
- const parts: MessagePart[] = [];
318
-
319
- for (const part of msgParts) {
320
- let partData: OpenCodePartData;
321
- try {
322
- partData = JSON.parse(part.data) as OpenCodePartData;
323
- } catch {
324
- continue;
325
- }
326
-
327
- const converted = convertPartToMessagePart(partData);
328
- if (converted) {
329
- parts.push(converted);
330
- }
331
- }
332
-
333
- // Skip messages with no usable content.
334
- if (parts.length === 0) {
335
- console.debug(`[opencode-watcher] Skipping message ${msg.id} (no usable parts)`);
336
- continue;
337
- }
338
-
339
- result.push({
340
- id: msg.id,
341
- parts,
342
- role,
343
- timestamp: new Date(toMillis(msg.time_created)).toISOString(),
344
- });
345
- }
346
-
347
- return result;
348
- }
349
-
350
- /**
351
- * Scan existing messages/parts to determine the starting high-water
352
- * marks for time-based polling. Also collects the set of already-seen
353
- * message IDs so we do not re-emit them.
354
- */
355
- private getHighWaterMarks(sessionId: string): {
356
- emittedMessageIds: Set<string>;
357
- emittedPartIds: Set<string>;
358
- lastMessageTime: number;
359
- lastPartTime: number;
360
- } {
361
- const db = openDb();
362
- if (!db) {
363
- return { emittedMessageIds: new Set(), emittedPartIds: new Set(), lastMessageTime: 0, lastPartTime: 0 };
364
- }
365
-
366
- try {
367
- const msgRow = db
368
- .prepare(
369
- `
150
+ messageIds
151
+ );
152
+
153
+ // Group parts by message ID.
154
+ const partsByMessage = new Map<string, OpenCodePart[]>();
155
+ for (const part of parts) {
156
+ if (!partsByMessage.has(part.message_id)) {
157
+ partsByMessage.set(part.message_id, []);
158
+ }
159
+ partsByMessage.get(part.message_id)!.push(part);
160
+ }
161
+
162
+ return this.buildMessages(messages, partsByMessage);
163
+ } catch (error) {
164
+ console.error('[opencode-watcher] Failed to read history:', error);
165
+ return [];
166
+ } finally {
167
+ db.close();
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Stop watching a specific session. If a callback is provided, only that
173
+ * subscriber is removed — the interval keeps running while other subscribers
174
+ * remain. If no callback is provided, all subscribers and the interval are
175
+ * cleared (backward compat).
176
+ */
177
+ stop(sessionId: string, callback?: (messages: ConversationMessage[]) => void): void {
178
+ const state = this.watchers.get(sessionId);
179
+ if (!state) {
180
+ return;
181
+ }
182
+
183
+ if (callback) {
184
+ state.callbacks.delete(callback);
185
+ console.log(
186
+ `[opencode-watcher] Removed subscriber from session: ${sessionId} ` +
187
+ `(remaining=${state.callbacks.size})`
188
+ );
189
+
190
+ // Only tear down the interval when no subscribers remain.
191
+ if (state.callbacks.size > 0) {
192
+ return;
193
+ }
194
+ }
195
+
196
+ clearInterval(state.intervalHandle);
197
+ this.watchers.delete(sessionId);
198
+ console.log(`[opencode-watcher] Stopped watching session: ${sessionId}`);
199
+ }
200
+
201
+ /**
202
+ * Stop all active watchers.
203
+ */
204
+ stopAll(): void {
205
+ for (const [sessionId] of this.watchers) {
206
+ this.stop(sessionId);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Start polling the SQLite DB every 2 seconds for new messages/parts
212
+ * in the given session. Converts new data to ConversationMessage
213
+ * format and invokes the callback.
214
+ */
215
+ watch(sessionId: string, callback: (messages: ConversationMessage[]) => void): void {
216
+ const existing = this.watchers.get(sessionId);
217
+ if (existing) {
218
+ // Already watching — just add the new callback, don't create a new interval.
219
+ existing.callbacks.add(callback);
220
+ console.log(
221
+ `[opencode-watcher] Added subscriber to session: ${sessionId} ` +
222
+ `(total=${existing.callbacks.size})`
223
+ );
224
+ return;
225
+ }
226
+
227
+ // Determine the initial high-water marks by scanning existing data.
228
+ const { emittedMessageIds, emittedPartUpdatedAt, lastMessageTime, lastPartTime } =
229
+ this.getHighWaterMarks(sessionId);
230
+
231
+ const intervalHandle = setInterval(() => {
232
+ this.poll(sessionId);
233
+ }, POLL_INTERVAL_MS);
234
+
235
+ const state: WatchState = {
236
+ callbacks: new Set([callback]),
237
+ emittedMessageIds,
238
+ emittedPartUpdatedAt,
239
+ intervalHandle,
240
+ lastMessageTime,
241
+ lastPartTime,
242
+ sessionId,
243
+ };
244
+
245
+ this.watchers.set(sessionId, state);
246
+ console.log(
247
+ `[opencode-watcher] Watching session: ${sessionId} ` +
248
+ `(lastMsg=${lastMessageTime}, lastPart=${lastPartTime})`
249
+ );
250
+ }
251
+
252
+ // ── Private Helpers ────────────────────────────────────────────────
253
+
254
+ /**
255
+ * Convert OpenCode messages + their parts into ConversationMessage
256
+ * objects for consumption by the session handler.
257
+ */
258
+ private buildMessages(
259
+ messages: OpenCodeMessage[],
260
+ partsByMessage: Map<string, OpenCodePart[]>
261
+ ): ConversationMessage[] {
262
+ const result: ConversationMessage[] = [];
263
+
264
+ for (const msg of messages) {
265
+ // Parse message data to determine role.
266
+ let msgData: { agent?: string; role?: string } = {};
267
+ try {
268
+ msgData = JSON.parse(msg.data) as typeof msgData;
269
+ } catch {
270
+ // Skip unparseable message data.
271
+ continue;
272
+ }
273
+
274
+ const role = msgData.role === 'user' ? 'user' : 'assistant';
275
+ const msgParts = partsByMessage.get(msg.id) || [];
276
+
277
+ // Convert each part to a MessagePart.
278
+ const parts: MessagePart[] = [];
279
+
280
+ for (const part of msgParts) {
281
+ let partData: OpenCodePartData;
282
+ try {
283
+ partData = JSON.parse(part.data) as OpenCodePartData;
284
+ } catch {
285
+ continue;
286
+ }
287
+
288
+ const converted = convertPartToMessagePart(partData);
289
+ if (converted) {
290
+ parts.push(converted);
291
+ }
292
+ }
293
+
294
+ // Skip messages with no usable content.
295
+ if (parts.length === 0) {
296
+ console.debug(`[opencode-watcher] Skipping message ${msg.id} (no usable parts)`);
297
+ continue;
298
+ }
299
+
300
+ result.push({
301
+ id: msg.id,
302
+ parts,
303
+ role,
304
+ timestamp: new Date(toMillis(msg.time_created)).toISOString(),
305
+ });
306
+ }
307
+
308
+ return result;
309
+ }
310
+
311
+ /**
312
+ * Scan existing messages/parts to determine the starting high-water
313
+ * marks for time-based polling. Also collects the set of already-seen
314
+ * message IDs so we do not re-emit them.
315
+ */
316
+ private getHighWaterMarks(sessionId: string): {
317
+ emittedMessageIds: Set<string>;
318
+ emittedPartUpdatedAt: Map<string, number>;
319
+ lastMessageTime: number;
320
+ lastPartTime: number;
321
+ } {
322
+ const db = openDb();
323
+ if (!db) {
324
+ return {
325
+ emittedMessageIds: new Set(),
326
+ emittedPartUpdatedAt: new Map(),
327
+ lastMessageTime: 0,
328
+ lastPartTime: 0,
329
+ };
330
+ }
331
+
332
+ try {
333
+ const msgRow = db
334
+ .prepare(
335
+ `
370
336
  SELECT MAX(time_created) as maxTime
371
337
  FROM message
372
338
  WHERE session_id = ?
373
339
  `
374
- )
375
- .get(sessionId) as undefined | { maxTime: null | number };
340
+ )
341
+ .get(sessionId) as undefined | { maxTime: null | number };
376
342
 
377
- const partRow = db
378
- .prepare(
379
- `
343
+ const partRow = db
344
+ .prepare(
345
+ `
380
346
  SELECT MAX(time_updated) as maxTime
381
347
  FROM part
382
348
  WHERE session_id = ?
383
349
  `
384
- )
385
- .get(sessionId) as undefined | { maxTime: null | number };
350
+ )
351
+ .get(sessionId) as undefined | { maxTime: null | number };
386
352
 
387
- // Collect all existing message IDs.
388
- const existingIds = db
389
- .prepare(
390
- `
353
+ // Collect all existing message IDs.
354
+ const existingIds = db
355
+ .prepare(
356
+ `
391
357
  SELECT id FROM message WHERE session_id = ?
392
358
  `
393
- )
394
- .all(sessionId) as { id: string }[];
359
+ )
360
+ .all(sessionId) as { id: string }[];
395
361
 
396
- const emittedMessageIds = new Set(existingIds.map((r) => r.id));
362
+ const emittedMessageIds = new Set(existingIds.map((r) => r.id));
397
363
 
398
- // Collect all existing part IDs.
399
- const existingPartIds = db
400
- .prepare(
401
- `
402
- SELECT id FROM part WHERE session_id = ?
364
+ // Collect all existing part IDs with their time_updated for change detection.
365
+ const existingParts = db
366
+ .prepare(
367
+ `
368
+ SELECT id, time_updated FROM part WHERE session_id = ?
403
369
  `
404
- )
405
- .all(sessionId) as { id: string }[];
406
-
407
- const emittedPartIds = new Set(existingPartIds.map((r) => r.id));
408
-
409
- return {
410
- emittedMessageIds,
411
- emittedPartIds,
412
- lastMessageTime: msgRow?.maxTime ?? 0,
413
- lastPartTime: partRow?.maxTime ?? 0
414
- };
415
- } catch (error) {
416
- console.error('[opencode-watcher] Failed to get high-water marks:', error);
417
- return { emittedMessageIds: new Set(), emittedPartIds: new Set(), lastMessageTime: 0, lastPartTime: 0 };
418
- } finally {
419
- db.close();
420
- }
421
- }
422
-
423
- /**
424
- * Single poll iteration. Opens the DB, queries for new messages and
425
- * updated parts, converts them to ConversationMessage[], and invokes
426
- * the watcher callbacks.
427
- */
428
- private poll(sessionId: string): void {
429
- const state = this.watchers.get(sessionId);
430
- if (!state) {
431
- return;
432
- }
433
-
434
- const db = openDb();
435
- if (!db) {
436
- return;
437
- }
438
-
439
- try {
440
- const results: ConversationMessage[] = [];
441
-
442
- // ── 1. Check for brand-new messages ──────────────────────────
443
- const newMessages = db
444
- .prepare(
445
- `
370
+ )
371
+ .all(sessionId) as { id: string; time_updated: number }[];
372
+
373
+ const emittedPartUpdatedAt = new Map<string, number>();
374
+ for (const p of existingParts) {
375
+ emittedPartUpdatedAt.set(p.id, p.time_updated);
376
+ }
377
+
378
+ return {
379
+ emittedMessageIds,
380
+ emittedPartUpdatedAt,
381
+ lastMessageTime: msgRow?.maxTime ?? 0,
382
+ lastPartTime: partRow?.maxTime ?? 0,
383
+ };
384
+ } catch (error) {
385
+ console.error('[opencode-watcher] Failed to get high-water marks:', error);
386
+ return {
387
+ emittedMessageIds: new Set(),
388
+ emittedPartUpdatedAt: new Map(),
389
+ lastMessageTime: 0,
390
+ lastPartTime: 0,
391
+ };
392
+ } finally {
393
+ db.close();
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Single poll iteration. Opens the DB, queries for new messages and
399
+ * updated parts, converts them to ConversationMessage[], and invokes
400
+ * the watcher callbacks.
401
+ */
402
+ private poll(sessionId: string): void {
403
+ const state = this.watchers.get(sessionId);
404
+ if (!state) {
405
+ return;
406
+ }
407
+
408
+ const db = openDb();
409
+ if (!db) {
410
+ return;
411
+ }
412
+
413
+ try {
414
+ const results: ConversationMessage[] = [];
415
+
416
+ // ── 1. Check for brand-new messages ──────────────────────────
417
+ const newMessages = db
418
+ .prepare(
419
+ `
446
420
  SELECT id, session_id, time_created, time_updated, data
447
421
  FROM message
448
422
  WHERE session_id = ? AND time_created > ?
449
423
  ORDER BY time_created ASC
450
424
  `
451
- )
452
- .all(sessionId, state.lastMessageTime) as OpenCodeMessage[];
453
-
454
- if (newMessages.length > 0) {
455
- // Deduplicate: skip messages we have already emitted (guards
456
- // against clock-tie edge cases where time_created equals the
457
- // high-water mark and the row slips through the > filter again).
458
- const dedupedMessages = newMessages.filter(
459
- (m) => !state.emittedMessageIds.has(m.id)
460
- );
461
-
462
- if (dedupedMessages.length > 0) {
463
- // Fetch parts for these new messages (batched to avoid SQLite param limit).
464
- const newMsgIds = dedupedMessages.map((m) => m.id);
465
- const newParts = batchInQuery<OpenCodePart>(
466
- db,
467
- `SELECT id, message_id, session_id, time_created, time_updated, data
425
+ )
426
+ .all(sessionId, state.lastMessageTime) as OpenCodeMessage[];
427
+
428
+ if (newMessages.length > 0) {
429
+ // Deduplicate: skip messages we have already emitted (guards
430
+ // against clock-tie edge cases where time_created equals the
431
+ // high-water mark and the row slips through the > filter again).
432
+ const dedupedMessages = newMessages.filter((m) => !state.emittedMessageIds.has(m.id));
433
+
434
+ if (dedupedMessages.length > 0) {
435
+ // Fetch parts for these new messages (batched to avoid SQLite param limit).
436
+ const newMsgIds = dedupedMessages.map((m) => m.id);
437
+ const newParts = batchInQuery<OpenCodePart>(
438
+ db,
439
+ `SELECT id, message_id, session_id, time_created, time_updated, data
468
440
  FROM part
469
441
  WHERE message_id IN (__PLACEHOLDERS__)
470
442
  ORDER BY time_created ASC`,
471
- newMsgIds
472
- );
473
-
474
- // Group parts by message.
475
- const partsByMessage = new Map<string, OpenCodePart[]>();
476
- for (const part of newParts) {
477
- if (!partsByMessage.has(part.message_id)) {
478
- partsByMessage.set(part.message_id, []);
479
- }
480
- partsByMessage.get(part.message_id)!.push(part);
481
- }
482
-
483
- const newEntries = this.buildMessages(dedupedMessages, partsByMessage);
484
- results.push(...newEntries);
485
-
486
- // Track emitted part IDs and update part high-water mark.
487
- for (const part of newParts) {
488
- if (part.time_updated > state.lastPartTime) {
489
- state.lastPartTime = part.time_updated;
490
- }
491
- state.emittedPartIds.add(part.id);
492
- }
493
- }
494
-
495
- // Always update message high-water marks (even for already-emitted messages).
496
- for (const msg of newMessages) {
497
- if (msg.time_created > state.lastMessageTime) {
498
- state.lastMessageTime = msg.time_created;
499
- }
500
- state.emittedMessageIds.add(msg.id);
501
- }
502
- }
503
-
504
- // ── 2. Check for updated parts on existing messages ──────────
505
- // Parts may be added or updated after the initial message row is
506
- // created (e.g., streaming assistant response). Look for parts
507
- // whose time_updated exceeds our last-seen mark, but whose parent
508
- // message is NOT in the new-messages set (those were handled above).
509
- const updatedParts = db
510
- .prepare(
511
- `
443
+ newMsgIds
444
+ );
445
+
446
+ // Group parts by message.
447
+ const partsByMessage = new Map<string, OpenCodePart[]>();
448
+ for (const part of newParts) {
449
+ if (!partsByMessage.has(part.message_id)) {
450
+ partsByMessage.set(part.message_id, []);
451
+ }
452
+ partsByMessage.get(part.message_id)!.push(part);
453
+ }
454
+
455
+ const newEntries = this.buildMessages(dedupedMessages, partsByMessage);
456
+ results.push(...newEntries);
457
+
458
+ // Track emitted part timestamps and update part high-water mark.
459
+ for (const part of newParts) {
460
+ if (part.time_updated > state.lastPartTime) {
461
+ state.lastPartTime = part.time_updated;
462
+ }
463
+ state.emittedPartUpdatedAt.set(part.id, part.time_updated);
464
+ }
465
+ }
466
+
467
+ // Always update message high-water marks (even for already-emitted messages).
468
+ for (const msg of newMessages) {
469
+ if (msg.time_created > state.lastMessageTime) {
470
+ state.lastMessageTime = msg.time_created;
471
+ }
472
+ state.emittedMessageIds.add(msg.id);
473
+ }
474
+ }
475
+
476
+ // ── 2. Check for updated parts on existing messages ──────────
477
+ // Parts may be added or updated after the initial message row is
478
+ // created (e.g., streaming assistant response). Look for parts
479
+ // whose time_updated exceeds our last-seen mark, but whose parent
480
+ // message is NOT in the new-messages set (those were handled above).
481
+ const updatedParts = db
482
+ .prepare(
483
+ `
512
484
  SELECT id, message_id, session_id, time_created, time_updated, data
513
485
  FROM part
514
486
  WHERE session_id = ? AND time_updated > ?
515
487
  ORDER BY time_created ASC
516
488
  `
517
- )
518
- .all(sessionId, state.lastPartTime) as OpenCodePart[];
519
-
520
- if (updatedParts.length > 0) {
521
- // Filter out parts that belong to messages we just processed,
522
- // AND parts we have already emitted (avoid duplicates).
523
- const newMsgIdSet = new Set(newMessages.map((m) => m.id));
524
- const newParts = updatedParts.filter(
525
- (p) => !newMsgIdSet.has(p.message_id) && !state.emittedPartIds.has(p.id)
526
- );
527
-
528
- if (newParts.length > 0) {
529
- // Group the NEW parts by message ID.
530
- const partsByMessage = new Map<string, OpenCodePart[]>();
531
- for (const part of newParts) {
532
- if (!partsByMessage.has(part.message_id)) {
533
- partsByMessage.set(part.message_id, []);
534
- }
535
- partsByMessage.get(part.message_id)!.push(part);
536
- }
537
-
538
- // Fetch the parent messages for context (batched to avoid SQLite param limit).
539
- const affectedMsgIds = [...partsByMessage.keys()];
540
- const affectedMessages = batchInQuery<OpenCodeMessage>(
541
- db,
542
- `SELECT id, session_id, time_created, time_updated, data
489
+ )
490
+ .all(sessionId, state.lastPartTime) as OpenCodePart[];
491
+
492
+ if (updatedParts.length > 0) {
493
+ // Filter out parts that belong to messages we just processed.
494
+ // For previously-emitted parts, allow re-emission only if
495
+ // time_updated has increased (in-place update detection).
496
+ const newMsgIdSet = new Set(newMessages.map((m) => m.id));
497
+ const newParts = updatedParts.filter((p) => {
498
+ if (newMsgIdSet.has(p.message_id)) {
499
+ return false;
500
+ }
501
+ const lastSeen = state.emittedPartUpdatedAt.get(p.id);
502
+ // New part (never emitted) or updated since last emission
503
+ return lastSeen === undefined || p.time_updated > lastSeen;
504
+ });
505
+
506
+ if (newParts.length > 0) {
507
+ // Group the NEW parts by message ID.
508
+ const partsByMessage = new Map<string, OpenCodePart[]>();
509
+ for (const part of newParts) {
510
+ if (!partsByMessage.has(part.message_id)) {
511
+ partsByMessage.set(part.message_id, []);
512
+ }
513
+ partsByMessage.get(part.message_id)!.push(part);
514
+ }
515
+
516
+ // Fetch the parent messages for context (batched to avoid SQLite param limit).
517
+ const affectedMsgIds = [...partsByMessage.keys()];
518
+ const affectedMessages = batchInQuery<OpenCodeMessage>(
519
+ db,
520
+ `SELECT id, session_id, time_created, time_updated, data
543
521
  FROM message
544
522
  WHERE id IN (__PLACEHOLDERS__)
545
523
  ORDER BY time_created ASC`,
546
- affectedMsgIds
547
- );
548
-
549
- // Build entries from ONLY the new parts (delta), not all
550
- // parts for the message. This prevents re-emitting content
551
- // the session handler has already seen.
552
- const updatedEntries = this.buildMessages(affectedMessages, partsByMessage);
553
- results.push(...updatedEntries);
554
-
555
- // Track emitted part IDs.
556
- for (const part of newParts) {
557
- state.emittedPartIds.add(part.id);
558
- }
559
- }
560
-
561
- // Update part high-water mark.
562
- for (const part of updatedParts) {
563
- if (part.time_updated > state.lastPartTime) {
564
- state.lastPartTime = part.time_updated;
565
- }
566
- }
567
- }
568
-
569
- // ── 3. Invoke all callbacks if there are new entries ─────────
570
- if (results.length > 0) {
571
- for (const cb of state.callbacks) {
572
- try {
573
- cb(results);
574
- } catch (cbError) {
575
- console.error('[opencode-watcher] Callback error:', cbError);
576
- }
577
- }
578
- }
579
- } catch (error) {
580
- // Log but do not crash — the next poll will retry.
581
- console.error('[opencode-watcher] Poll error:', error);
582
- } finally {
583
- db.close();
584
- }
585
- }
524
+ affectedMsgIds
525
+ );
526
+
527
+ // Build entries from ONLY the new parts (delta), not all
528
+ // parts for the message. This prevents re-emitting content
529
+ // the session handler has already seen.
530
+ const updatedEntries = this.buildMessages(affectedMessages, partsByMessage);
531
+ results.push(...updatedEntries);
532
+
533
+ // Track emitted part timestamps for change detection.
534
+ for (const part of newParts) {
535
+ state.emittedPartUpdatedAt.set(part.id, part.time_updated);
536
+ }
537
+ }
538
+
539
+ // Update part high-water mark.
540
+ for (const part of updatedParts) {
541
+ if (part.time_updated > state.lastPartTime) {
542
+ state.lastPartTime = part.time_updated;
543
+ }
544
+ }
545
+ }
546
+
547
+ // ── 3. Invoke all callbacks if there are new entries ─────────
548
+ if (results.length > 0) {
549
+ for (const cb of state.callbacks) {
550
+ try {
551
+ cb(results);
552
+ } catch (cbError) {
553
+ console.error('[opencode-watcher] Callback error:', cbError);
554
+ }
555
+ }
556
+ }
557
+ } catch (error) {
558
+ // Log but do not crash — the next poll will retry.
559
+ console.error('[opencode-watcher] Poll error:', error);
560
+ } finally {
561
+ db.close();
562
+ }
563
+ }
586
564
  }
587
565
 
588
- // ── Database Helpers ─────────────────────────────────────────────────
566
+ // ── OpenCodeWatcher Class ────────────────────────────────────────────
589
567
 
590
568
  /**
591
569
  * Execute a SELECT query with an IN clause, batching in chunks of
592
570
  * SQLITE_MAX_PARAMS to stay within SQLite's 999-parameter limit.
593
571
  */
594
- function batchInQuery<T>(
595
- db: Database.Database,
596
- sql: string,
597
- ids: string[]
598
- ): T[] {
599
- if (ids.length === 0) return [];
600
-
601
- const results: T[] = [];
602
- for (let i = 0; i < ids.length; i += SQLITE_MAX_PARAMS) {
603
- const chunk = ids.slice(i, i + SQLITE_MAX_PARAMS);
604
- const placeholders = chunk.map(() => '?').join(',');
605
- const query = sql.replace('__PLACEHOLDERS__', placeholders);
606
- const rows = db.prepare(query).all(...chunk) as T[];
607
- results.push(...rows);
608
- }
609
- return results;
572
+ function batchInQuery<T>(db: Database.Database, sql: string, ids: string[]): T[] {
573
+ if (ids.length === 0) {
574
+ return [];
575
+ }
576
+
577
+ const results: T[] = [];
578
+ for (let i = 0; i < ids.length; i += SQLITE_MAX_PARAMS) {
579
+ const chunk = ids.slice(i, i + SQLITE_MAX_PARAMS);
580
+ const placeholders = chunk.map(() => '?').join(',');
581
+ const query = sql.replace('__PLACEHOLDERS__', placeholders);
582
+ const rows = db.prepare(query).all(...chunk) as T[];
583
+ results.push(...rows);
584
+ }
585
+ return results;
610
586
  }
611
587
 
612
- // ── Part Conversion ──────────────────────────────────────────────────
613
- // Maps OpenCode part types to MessagePart directly, skipping the
614
- // intermediate Record<string, unknown> stage.
588
+ // ── Database Helpers ─────────────────────────────────────────────────
615
589
 
616
590
  function convertPartToMessagePart(data: OpenCodePartData): MessagePart | null {
617
- switch (data.type) {
618
- case 'reasoning':
619
- return { content: data.text || '', type: 'thinking' };
620
-
621
- case 'text':
622
- return { content: data.text || '', type: 'text' };
623
-
624
- case 'tool':
625
- return {
626
- id: data.callID || data.id || '',
627
- input: data.state?.input || {},
628
- toolName: data.tool || 'Unknown',
629
- type: 'tool_use'
630
- };
631
-
632
- default:
633
- // Skip snapshot, patch, step-start, step-finish, subtask, retry, compaction
634
- return null;
635
- }
591
+ switch (data.type) {
592
+ case 'reasoning':
593
+ return { content: data.text || '', type: 'thinking' };
594
+
595
+ case 'text':
596
+ return { content: data.text || '', type: 'text' };
597
+
598
+ case 'tool':
599
+ return {
600
+ id: data.callID || data.id || '',
601
+ input: data.state?.input || {},
602
+ toolName: data.tool || 'Unknown',
603
+ type: 'tool_use',
604
+ };
605
+
606
+ default:
607
+ // Skip snapshot, patch, step-start, step-finish, subtask, retry, compaction
608
+ return null;
609
+ }
636
610
  }
637
611
 
612
+ // ── Part Conversion ──────────────────────────────────────────────────
613
+ // Maps OpenCode part types to MessagePart directly, skipping the
614
+ // intermediate Record<string, unknown> stage.
615
+
638
616
  /**
639
617
  * Open the OpenCode SQLite database in read-only mode.
640
618
  * Returns null if the file does not exist or cannot be opened.
641
619
  */
642
620
  function openDb(): Database.Database | null {
643
- if (!fs.existsSync(OPENCODE_DB_PATH)) {
644
- return null;
645
- }
646
- try {
647
- return new Database(OPENCODE_DB_PATH, { readonly: true });
648
- } catch {
649
- return null;
650
- }
621
+ if (!fs.existsSync(OPENCODE_DB_PATH)) {
622
+ return null;
623
+ }
624
+ try {
625
+ return new Database(OPENCODE_DB_PATH, { readonly: true });
626
+ } catch {
627
+ return null;
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Normalise a timestamp from OpenCode's SQLite database to milliseconds.
633
+ *
634
+ * OpenCode currently stores `time_created` / `time_updated` as Unix
635
+ * **milliseconds**, but this is not formally documented and could change.
636
+ * A simple heuristic distinguishes seconds from milliseconds:
637
+ * - Values < 1e12 (~2001-09-09 in ms, ~33658 AD in seconds) are seconds.
638
+ * - Values >= 1e12 are already milliseconds.
639
+ */
640
+ function toMillis(timestamp: number): number {
641
+ return timestamp < 1e12 ? timestamp * 1000 : timestamp;
651
642
  }
652
643
 
653
644
  // ── Singleton ────────────────────────────────────────────────────────
@@ -656,6 +647,6 @@ function openDb(): Database.Database | null {
656
647
 
657
648
  const OW_GLOBAL_KEY = '__shooter_opencode_watcher';
658
649
  export const openCodeWatcher: OpenCodeWatcher =
659
- ((globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] as OpenCodeWatcher) ||
660
- new OpenCodeWatcher();
650
+ ((globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] as OpenCodeWatcher) ||
651
+ new OpenCodeWatcher();
661
652
  (globalThis as Record<string, unknown>)[OW_GLOBAL_KEY] = openCodeWatcher;