@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
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { SessionViewResponse, ShooterConfig } from '$generated/types';
2
3
  import type {
3
4
  ConversationMessage,
4
5
  MessagePart,
@@ -8,279 +9,400 @@
8
9
 
9
10
  import { browser } from '$app/environment';
10
11
  import { page } from '$app/state';
11
- import { Banner, Pill, Shimmer } from '@juspay/svelte-ui-components';
12
- import {
13
- EmptyState,
14
- getCached,
15
- setCache,
16
- } from '$lib/modules/client/common';
12
+ import { getCached, setCache } from '$lib/modules/client/common';
17
13
  import ChatView from '$lib/modules/client/terminal/ChatView.svelte';
14
+ import { Shimmer } from '@juspay/svelte-ui-components';
18
15
  import { onMount } from 'svelte';
19
16
 
20
- interface SessionResponse {
21
- messages: ConversationMessage[];
22
- session: SessionInfo;
23
- }
24
-
25
- // WebSocket message types from the live session channel
26
- interface WsMessage {
27
- content?: MessagePart[];
28
- id?: string;
29
- input?: Record<string, unknown>;
30
- isError?: boolean;
31
- message?: string;
32
- name?: string;
33
- output?: string;
34
- role?: string;
35
- status?: string;
36
- text?: string;
37
- timestamp?: string;
38
- type: string;
39
- }
17
+ // --- State ---
40
18
 
41
19
  let session = $state<null | SessionInfo>(null);
42
20
  let messages = $state<ConversationMessage[]>([]);
43
21
  let loading = $state(true);
44
22
  let error = $state<null | string>(null);
45
-
46
- // Live streaming state
47
- type ConnectionState = 'connected' | 'connecting' | 'disconnected' | 'idle';
48
- let isSessionActive = $state(false);
49
- let connectionState = $state<ConnectionState>('idle');
50
- let ws = $state<null | WebSocket>(null);
51
- let wsReconnectAttempts = 0;
52
- let wsReconnectTimer: number | null = null;
23
+ let hasMoreMessages = $state(false);
24
+ let loadingMore = $state(false);
25
+ let currentLimit = $state(200);
26
+
27
+ // Terminal + WS connection state
28
+ let connectedTerminalId = $state<null | string>(null);
29
+ let chatConnectionState = $state<'connected' | 'connecting' | 'disconnected' | 'idle' | 'reconnecting'>('idle');
30
+ // sendStatus:
31
+ // 'idle' = no terminal yet; input ready for first send
32
+ // 'connecting' = calling /api/sessions/connect
33
+ // 'resuming' = waiting for session history (claude loading)
34
+ // 'ready' = history received; input goes straight to PTY
35
+ let sendStatus = $state<'connecting' | 'idle' | 'ready' | 'resuming'>('idle');
36
+ let pendingMessage: null | string = null;
37
+
38
+ // Non-reactive WS refs
39
+ let terminalWs: null | WebSocket = null;
40
+ let sessionWs: null | WebSocket = null;
41
+ let sessionReconnectTimer: null | ReturnType<typeof setTimeout> = null;
42
+ let disposed = false;
53
43
 
54
44
  const sessionId = $derived(page.params.id);
55
45
  const projectId = $derived(page.url.searchParams.get('project') || '');
56
46
 
57
- // Check if a session was modified within the last 5 minutes
58
- function checkSessionActive(sess: SessionInfo): boolean {
59
- const modifiedTime = new Date(sess.modified).getTime();
60
- const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
61
- return modifiedTime > fiveMinutesAgo;
62
- }
47
+ // Disable send button while a message is in-flight
48
+ const sendDisabled = $derived(sendStatus === 'connecting' || sendStatus === 'resuming');
63
49
 
64
- // Generate a unique message ID for WebSocket-delivered messages
65
- let wsMessageCounter = 0;
66
- function nextWsMessageId(): string {
67
- wsMessageCounter++;
68
- return `ws-msg-${Date.now()}-${wsMessageCounter}`;
50
+ // --- Helpers ---
51
+
52
+ function getConfig(): null | ShooterConfig {
53
+ try {
54
+ const saved = localStorage.getItem('shooter_config');
55
+ if (!saved) { return null; }
56
+ return JSON.parse(saved) as ShooterConfig;
57
+ } catch {
58
+ return null;
59
+ }
69
60
  }
70
61
 
71
- // Handle incoming WebSocket messages
72
- function handleWsMessage(event: MessageEvent): void {
73
- let data: WsMessage;
62
+ async function getWsTicket(): Promise<null | string> {
63
+ const config = getConfig();
64
+ if (!config) { return null; }
74
65
  try {
75
- data = JSON.parse(event.data);
66
+ const res = await fetch('/api/ws-ticket', {
67
+ headers: { Authorization: `Bearer ${config.apiKey}` },
68
+ method: 'POST',
69
+ });
70
+ if (!res.ok) { return null; }
71
+ const data: { ticket: string } = await res.json();
72
+ return data.ticket;
76
73
  } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // --- Session WS (receive chat messages) ---
79
+
80
+ async function connectSessionWs(sessionWsPath: string, termId: string): Promise<void> {
81
+ if (disposed) { return; }
82
+
83
+ // Avoid stacking connections
84
+ if (
85
+ sessionWs &&
86
+ (sessionWs.readyState === WebSocket.OPEN || sessionWs.readyState === WebSocket.CONNECTING)
87
+ ) {
77
88
  return;
78
89
  }
79
90
 
80
- if (data.type === 'message') {
81
- // Full message (user or assistant)
82
- const newMsg: ConversationMessage = {
83
- id: nextWsMessageId(),
84
- parts: data.content || [],
85
- role: (data.role as 'assistant' | 'system' | 'user') || 'assistant',
86
- timestamp: data.timestamp || new Date().toISOString(),
87
- };
88
- messages = [...messages, newMsg];
89
- } else if (data.type === 'tool-use') {
90
- // Append tool_use block to the last assistant message
91
- const toolPart: ToolUsePart = {
92
- id: data.id || nextWsMessageId(),
93
- input: data.input || {},
94
- toolName: data.name || 'Unknown',
91
+ const ticket = await getWsTicket();
92
+ if (!ticket || disposed) { return; }
93
+
94
+ chatConnectionState = 'connecting';
95
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
96
+ const socket = new WebSocket(
97
+ `${protocol}//${window.location.host}${sessionWsPath}?ticket=${ticket}`
98
+ );
99
+
100
+ socket.onopen = () => {
101
+ if (disposed) { socket.close(); return; }
102
+ chatConnectionState = 'connected';
103
+ socket.send(JSON.stringify({ sessionId: termId, type: 'subscribe' }));
104
+ };
105
+
106
+ socket.onmessage = (event) => {
107
+ if (disposed) { return; }
108
+ try { handleSessionMessage(JSON.parse(event.data as string)); } catch { /* ignore */ }
109
+ };
110
+
111
+ socket.onclose = () => {
112
+ if (disposed) { return; }
113
+ chatConnectionState = 'disconnected';
114
+ sessionWs = null;
115
+ if (connectedTerminalId) {
116
+ if (sessionReconnectTimer) { clearTimeout(sessionReconnectTimer); }
117
+ sessionReconnectTimer = setTimeout(() => {
118
+ sessionReconnectTimer = null;
119
+ if (!disposed && connectedTerminalId) {
120
+ void connectSessionWs(`/ws/session/${connectedTerminalId}`, connectedTerminalId);
121
+ }
122
+ }, 2000);
123
+ }
124
+ };
125
+
126
+ socket.onerror = () => {
127
+ if (!disposed) { chatConnectionState = 'disconnected'; }
128
+ };
129
+
130
+ sessionWs = socket;
131
+ }
132
+
133
+ // --- Terminal WS (send PTY input only) ---
134
+
135
+ async function connectTerminalWs(wsPath: string): Promise<void> {
136
+ if (disposed) { return; }
137
+
138
+ const ticket = await getWsTicket();
139
+ if (!ticket || disposed) { return; }
140
+
141
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
142
+ const ws = new WebSocket(`${protocol}//${window.location.host}${wsPath}?ticket=${ticket}`);
143
+
144
+ ws.onopen = () => {
145
+ if (disposed) { ws.close(); return; }
146
+ terminalWs = ws;
147
+ if (sendStatus === 'ready' && pendingMessage) {
148
+ ws.send(JSON.stringify({ data: `${pendingMessage}\n`, type: 'input' }));
149
+ pendingMessage = null;
150
+ }
151
+ };
152
+
153
+ ws.onclose = () => {
154
+ if (terminalWs === ws) { terminalWs = null; }
155
+ };
156
+ }
157
+
158
+ // --- Message handler (mirrors terminal page) ---
159
+
160
+ function handleSessionMessage(msg: Record<string, unknown>): void {
161
+ if (msg.type === 'history') {
162
+ const historyRaw = (msg.messages || []) as {
163
+ content: MessagePart[];
164
+ id: string;
165
+ role: string;
166
+ timestamp: string;
167
+ }[];
168
+ messages = historyRaw.map((m) => ({
169
+ id: m.id,
170
+ parts: m.content,
171
+ role: m.role as ConversationMessage['role'],
172
+ timestamp: m.timestamp,
173
+ }));
174
+ // WS history is always complete
175
+ hasMoreMessages = false;
176
+
177
+ if (sendStatus === 'resuming') {
178
+ sendStatus = 'ready';
179
+ if (pendingMessage && terminalWs?.readyState === WebSocket.OPEN) {
180
+ terminalWs.send(JSON.stringify({ data: `${pendingMessage}\n`, type: 'input' }));
181
+ pendingMessage = null;
182
+ }
183
+ }
184
+ } else if (msg.type === 'message') {
185
+ messages = [
186
+ ...messages,
187
+ {
188
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
189
+ parts: (msg.content as MessagePart[]) || [],
190
+ role: (msg.role as ConversationMessage['role']) || 'assistant',
191
+ timestamp: (msg.timestamp as string) || new Date().toISOString(),
192
+ },
193
+ ];
194
+ } else if (msg.type === 'tool-use') {
195
+ const toolUsePart: ToolUsePart = {
196
+ id: (msg.id as string) || `tool-${Date.now()}`,
197
+ input: (msg.input as Record<string, unknown>) || {},
198
+ toolName: msg.name as string,
95
199
  type: 'tool_use',
96
200
  };
97
- appendToLastAssistant(toolPart);
98
- } else if (data.type === 'tool-result') {
99
- // Append a system message with the tool result
100
- const resultMsg: ConversationMessage = {
101
- id: nextWsMessageId(),
102
- parts: [
201
+ const lastToolMsg = messages.length > 0 ? messages[messages.length - 1] : null;
202
+ if (lastToolMsg?.role === 'assistant') {
203
+ messages = [
204
+ ...messages.slice(0, -1),
205
+ { ...lastToolMsg, parts: [...lastToolMsg.parts, toolUsePart] },
206
+ ];
207
+ } else {
208
+ messages = [
209
+ ...messages,
103
210
  {
104
- isError: data.isError || false,
105
- output: data.output || '',
106
- toolUseId: data.id || '',
107
- type: 'tool_result',
211
+ id: `tool-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
212
+ parts: [toolUsePart],
213
+ role: 'assistant',
214
+ timestamp: new Date().toISOString(),
108
215
  },
109
- ],
110
- role: 'system',
111
- timestamp: new Date().toISOString(),
112
- };
113
- messages = [...messages, resultMsg];
114
- } else if (data.type === 'thinking') {
115
- // Append thinking block to the last assistant message
116
- const thinkPart: MessagePart = {
117
- content: data.text || '',
118
- type: 'thinking',
119
- };
120
- appendToLastAssistant(thinkPart);
121
- } else if (data.type === 'error') {
122
- // Server-sent error — display in the UI
123
- const errorMsg: ConversationMessage = {
124
- id: nextWsMessageId(),
125
- parts: [{ content: data.text || data.message || 'Unknown error', type: 'text' }],
126
- role: 'system',
127
- timestamp: new Date().toISOString(),
216
+ ];
217
+ }
218
+ } else if (msg.type === 'tool-result') {
219
+ const toolResultPart: MessagePart = {
220
+ isError: (msg.isError as boolean) || false,
221
+ output: (msg.output as string) || '',
222
+ toolUseId: msg.id as string,
223
+ type: 'tool_result',
128
224
  };
129
- messages = [...messages, errorMsg];
130
- } else if (data.type === 'permission-requested') {
131
- // Permission request — log for now (full UI integration would go here)
132
- console.log('[session] Permission requested:', data);
133
- } else if (data.type === 'session-end') {
134
- // Session has ended — mark as inactive
135
- isSessionActive = false;
136
- connectionState = 'disconnected';
137
- if (ws) {
138
- ws.close();
139
- ws = null;
225
+ const lastResultMsg = messages.length > 0 ? messages[messages.length - 1] : null;
226
+ if (lastResultMsg?.role === 'system') {
227
+ messages = [
228
+ ...messages.slice(0, -1),
229
+ { ...lastResultMsg, parts: [...lastResultMsg.parts, toolResultPart] },
230
+ ];
231
+ } else {
232
+ messages = [
233
+ ...messages,
234
+ {
235
+ id: `result-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
236
+ parts: [toolResultPart],
237
+ role: 'system',
238
+ timestamp: new Date().toISOString(),
239
+ },
240
+ ];
241
+ }
242
+ } else if (msg.type === 'thinking') {
243
+ const thinkPart: MessagePart = { content: (msg.text as string) || '', type: 'thinking' };
244
+ const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null;
245
+ if (lastMsg?.role === 'assistant') {
246
+ messages = [
247
+ ...messages.slice(0, -1),
248
+ { ...lastMsg, parts: [...lastMsg.parts, thinkPart] },
249
+ ];
250
+ } else {
251
+ messages = [
252
+ ...messages,
253
+ {
254
+ id: `think-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
255
+ parts: [thinkPart],
256
+ role: 'assistant',
257
+ timestamp: new Date().toISOString(),
258
+ },
259
+ ];
140
260
  }
261
+ } else if (msg.type === 'session-end') {
262
+ sendStatus = 'idle';
141
263
  }
142
264
  }
143
265
 
144
- // Append a part to the last assistant message, or create one
145
- function appendToLastAssistant(part: MessagePart): void {
146
- const lastMsg = messages.length > 0 ? messages[messages.length - 1] : null;
147
- if (lastMsg?.role === 'assistant') {
148
- // Mutate the parts array and reassign to trigger reactivity
149
- const updatedParts = [...lastMsg.parts, part];
150
- const updatedMsg = { ...lastMsg, parts: updatedParts };
151
- messages = [...messages.slice(0, -1), updatedMsg];
152
- } else {
153
- // Create a new assistant message to hold this part
154
- const newMsg: ConversationMessage = {
155
- id: nextWsMessageId(),
156
- parts: [part],
157
- role: 'assistant',
158
- timestamp: new Date().toISOString(),
159
- };
160
- messages = [...messages, newMsg];
161
- }
162
- }
266
+ // --- Connect + send flow ---
163
267
 
164
- // Connect to the live session WebSocket
165
- async function connectWebSocket(): Promise<void> {
166
- if (!browser || !isSessionActive) {return;}
268
+ async function connectAndSend(): Promise<void> {
269
+ if (!session || disposed) { sendStatus = 'idle'; return; }
167
270
 
168
- const saved = localStorage.getItem('shooter_config');
169
- if (!saved) {return;}
170
- const config = JSON.parse(saved);
271
+ const config = getConfig();
272
+ if (!config) { sendStatus = 'idle'; return; }
171
273
 
172
- connectionState = 'connecting';
274
+ sendStatus = 'connecting';
173
275
 
174
276
  try {
175
- // Obtain a short-lived ticket
176
- const ticketRes = await fetch('/api/ws-ticket', {
177
- headers: { Authorization: `Bearer ${config.apiKey}` },
277
+ const command = session.source === 'opencode' ? 'opencode' : 'claude';
278
+ const res = await fetch('/api/sessions/connect', {
279
+ body: JSON.stringify({ command, cwd: session.projectPath, sessionId }),
280
+ headers: {
281
+ Authorization: `Bearer ${config.apiKey}`,
282
+ 'Content-Type': 'application/json',
283
+ },
178
284
  method: 'POST',
179
285
  });
180
- if (!ticketRes.ok) {
181
- connectionState = 'disconnected';
182
- return;
183
- }
184
- const { ticket } = await ticketRes.json();
185
-
186
- // Build WebSocket URL
187
- const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
188
- const wsUrl = `${wsProtocol}//${window.location.host}/ws/session/${sessionId}?ticket=${ticket}`;
189
286
 
190
- const socket = new WebSocket(wsUrl);
287
+ if (!res.ok || disposed) { sendStatus = 'idle'; return; }
191
288
 
192
- socket.onopen = () => {
193
- connectionState = 'connected';
194
- ws = socket;
195
- wsReconnectAttempts = 0; // Reset on successful connection
196
- };
289
+ const result: { sessionWs: string; terminalId: string; ws: string } = await res.json();
290
+ connectedTerminalId = result.terminalId;
291
+ sendStatus = 'resuming';
197
292
 
198
- socket.onmessage = handleWsMessage;
199
-
200
- socket.onclose = () => {
201
- connectionState = 'disconnected';
202
- ws = null;
203
- // Reconnect with backoff (max 5 retries)
204
- if (isSessionActive && wsReconnectAttempts < 5) {
205
- wsReconnectAttempts++;
206
- wsReconnectTimer = window.setTimeout(() => {
207
- void connectWebSocket();
208
- }, 2000);
209
- }
210
- };
211
-
212
- socket.onerror = () => {
213
- connectionState = 'disconnected';
214
- ws = null;
215
- };
293
+ void connectSessionWs(result.sessionWs, result.terminalId);
294
+ void connectTerminalWs(result.ws);
216
295
  } catch {
217
- connectionState = 'disconnected';
296
+ sendStatus = 'idle';
218
297
  }
219
298
  }
220
299
 
221
- function formatDate(ts: string): string {
222
- return new Date(ts).toLocaleDateString([], {
223
- day: 'numeric',
224
- month: 'short',
225
- year: 'numeric',
226
- });
227
- }
300
+ // --- Called by ChatView on submit ---
228
301
 
229
- function shortPath(p: string): string {
230
- const parts = p.split('/');
231
- return parts.slice(-2).join('/');
232
- }
302
+ function sendMessage(text: string): void {
303
+ if (!text.trim()) { return; }
233
304
 
234
- async function fetchSession(): Promise<void> {
235
- if (!browser) {
305
+ if (sendStatus === 'ready' && terminalWs?.readyState === WebSocket.OPEN) {
306
+ terminalWs.send(JSON.stringify({ data: `${text}\n`, type: 'input' }));
236
307
  return;
237
308
  }
238
309
 
239
- // Don't show loading spinner if we already have cached data
240
- if (messages.length === 0) {
241
- loading = true;
310
+ // Already connecting just update queued message
311
+ if (sendStatus === 'connecting' || sendStatus === 'resuming') {
312
+ pendingMessage = text;
313
+ return;
242
314
  }
243
315
 
316
+ // Not connected yet — queue and connect
317
+ pendingMessage = text;
318
+ void connectAndSend();
319
+ }
320
+
321
+ // --- REST fetch (initial messages) ---
322
+
323
+ async function fetchSession(): Promise<void> {
324
+ if (!browser) { return; }
325
+ if (messages.length === 0) { loading = true; }
326
+
244
327
  try {
245
- const saved = localStorage.getItem('shooter_config');
246
- if (!saved) {
328
+ const config = getConfig();
329
+ if (!config) {
247
330
  error = 'No configuration found. Please configure settings first.';
248
331
  loading = false;
249
332
  return;
250
333
  }
251
- const config = JSON.parse(saved);
252
334
 
253
335
  const sid = sessionId || '';
254
336
  const pid = projectId || '';
255
337
  const queryStr = pid
256
- ? `id=${encodeURIComponent(sid)}&project=${encodeURIComponent(pid)}`
257
- : `id=${encodeURIComponent(sid)}`;
258
- const res = await fetch(`/api/sessions?${queryStr}`, {
338
+ ? `id=${encodeURIComponent(sid)}&project=${encodeURIComponent(pid)}&limit=${currentLimit}`
339
+ : `id=${encodeURIComponent(sid)}&limit=${currentLimit}`;
340
+
341
+ let res = await fetch(`/api/sessions?${queryStr}`, {
259
342
  headers: { Authorization: `Bearer ${config.apiKey}` },
260
343
  });
261
- if (!res.ok) {
262
- error = 'Session not found';
263
- loading = false;
264
- return;
265
- }
266
- const data: SessionResponse = await res.json();
267
- session = data.session;
268
- messages = (data.messages || []).reverse();
269
- setCache(`shooter_session_${sid}`, { messages: data.messages || [], session: data.session });
270
-
271
- // Check if session is active and connect WebSocket if so
272
- if (data.session) {
273
- isSessionActive = checkSessionActive(data.session);
274
- if (isSessionActive) {
275
- void connectWebSocket();
344
+
345
+ // Fallback: search across all projects if not found
346
+ if (res.status === 404 && !pid) {
347
+ const projectsRes = await fetch('/api/sessions', {
348
+ headers: { Authorization: `Bearer ${config.apiKey}` },
349
+ });
350
+ if (projectsRes.ok) {
351
+ const projectsData: { projects?: { id: string; sessions?: { id: string }[] }[] } =
352
+ await projectsRes.json();
353
+ for (const project of projectsData.projects || []) {
354
+ if ((project.sessions || []).find((s) => s.id === sid)) {
355
+ res = await fetch(
356
+ `/api/sessions?id=${encodeURIComponent(sid)}&project=${encodeURIComponent(project.id)}`,
357
+ { headers: { Authorization: `Bearer ${config.apiKey}` } }
358
+ );
359
+ break;
360
+ }
361
+ }
276
362
  }
277
363
  }
364
+
365
+ if (!res.ok) { error = 'Session not found'; loading = false; return; }
366
+
367
+ const data: SessionViewResponse = await res.json();
368
+ session = data.session as SessionInfo;
369
+ const rawMessages = Array.isArray(data.messages) ? (data.messages as unknown as ConversationMessage[]) : [];
370
+ messages = rawMessages;
371
+ hasMoreMessages = rawMessages.length >= currentLimit;
372
+ setCache(`shooter_session_${sid}`, { messages: rawMessages, session: data.session });
278
373
  } catch {
279
374
  error = 'Failed to load session';
280
375
  }
281
376
  loading = false;
282
377
  }
283
378
 
379
+ async function loadEarlierMessages(): Promise<void> {
380
+ if (!browser || loadingMore) { return; }
381
+ const config = getConfig();
382
+ if (!config) { return; }
383
+
384
+ loadingMore = true;
385
+ try {
386
+ const newLimit = currentLimit + 200;
387
+ const sid = sessionId || '';
388
+ const pid = projectId || '';
389
+ const queryParts = [`id=${encodeURIComponent(sid)}`, `limit=${newLimit}`];
390
+ if (pid) { queryParts.push(`project=${encodeURIComponent(pid)}`); }
391
+ const res = await fetch(`/api/sessions?${queryParts.join('&')}`, {
392
+ headers: { Authorization: `Bearer ${config.apiKey}` },
393
+ });
394
+ if (!res.ok) { return; }
395
+ const data: SessionViewResponse = await res.json();
396
+ const rawMessages = Array.isArray(data.messages) ? (data.messages as unknown as ConversationMessage[]) : [];
397
+ messages = rawMessages;
398
+ currentLimit = newLimit;
399
+ hasMoreMessages = rawMessages.length >= newLimit;
400
+ setCache(`shooter_session_${sid}`, { messages: rawMessages, session: data.session });
401
+ } catch { /* best effort */ } finally { loadingMore = false; }
402
+ }
403
+
404
+ // --- Lifecycle ---
405
+
284
406
  onMount(() => {
285
407
  // Show cached data immediately
286
408
  const cached = getCached(`shooter_session_${page.params.id}`) as null | {
@@ -289,22 +411,49 @@
289
411
  };
290
412
  if (cached) {
291
413
  session = cached.session;
292
- messages = (cached.messages || []).reverse();
414
+ messages = Array.isArray(cached.messages) ? cached.messages : [];
293
415
  loading = false;
294
416
  }
295
417
 
296
- void fetchSession();
418
+ void fetchSession().then(async () => {
419
+ if (!session || disposed) { return; }
420
+
421
+ // Check if there's already a running terminal for this session
422
+ const config = getConfig();
423
+ if (!config) { return; }
424
+
425
+ try {
426
+ const command = session.source === 'opencode' ? 'opencode' : 'claude';
427
+ const res = await fetch('/api/sessions/connect', {
428
+ body: JSON.stringify({
429
+ command,
430
+ cwd: session.projectPath,
431
+ noCreate: true,
432
+ sessionId,
433
+ }),
434
+ headers: {
435
+ Authorization: `Bearer ${config.apiKey}`,
436
+ 'Content-Type': 'application/json',
437
+ },
438
+ method: 'POST',
439
+ });
440
+
441
+ if (res.ok && !disposed) {
442
+ const result: { sessionWs: string; terminalId: string; ws: string } = await res.json();
443
+ connectedTerminalId = result.terminalId;
444
+ sendStatus = 'resuming';
445
+ void connectSessionWs(result.sessionWs, result.terminalId);
446
+ void connectTerminalWs(result.ws);
447
+ }
448
+ // 404 = no existing terminal, that's fine
449
+ } catch { /* ignore */ }
450
+ });
297
451
 
298
- // Cleanup WebSocket and reconnect timer on component destroy
299
452
  return () => {
300
- if (wsReconnectTimer) {
301
- clearTimeout(wsReconnectTimer);
302
- wsReconnectTimer = null;
303
- }
304
- if (ws) {
305
- ws.close();
306
- ws = null;
307
- }
453
+ disposed = true;
454
+ if (sessionReconnectTimer) { clearTimeout(sessionReconnectTimer); }
455
+ sessionWs?.close();
456
+ terminalWs?.close();
308
457
  };
309
458
  });
310
459
  </script>
@@ -323,112 +472,159 @@
323
472
  <Shimmer classes="shimmer-bubble shimmer-bubble-assistant" />
324
473
  <Shimmer classes="shimmer-bubble shimmer-bubble-user-short" />
325
474
  <Shimmer classes="shimmer-bubble shimmer-bubble-assistant-wide" />
326
- <Shimmer classes="shimmer-bubble shimmer-bubble-assistant-short" />
327
475
  </div>
328
476
  </div>
329
477
  {:else if error}
330
478
  <div class="session-back-row">
331
- <a href="/" class="back-link">
332
- <span class="back-arrow">&larr;</span>
333
- Back
334
- </a>
479
+ <a href={projectId ? `/project?id=${projectId}` : '/'} class="back-link">← Back</a>
335
480
  </div>
336
- <EmptyState icon="alert-triangle" title="Error" description={error} />
481
+ <p class="error-text">{error}</p>
337
482
  {:else if session}
338
- <!-- Session Header -->
339
- <div class="chat-session-header">
340
- <div class="chat-session-header-top">
341
- <a href="/project?id={projectId}" class="back-link">&#8592; Back to Project</a>
342
- {#if isSessionActive || connectionState === 'connected' || connectionState === 'connecting'}
343
- <div class="live-connection-row">
344
- {#if connectionState === 'connected'}
345
- <span class="connection-status connected">
346
- <span class="connection-status-dot"></span>
347
- </span>
348
- {:else if connectionState === 'connecting'}
349
- <span class="connection-status reconnecting">
350
- <span class="connection-status-dot"></span>
351
- </span>
352
- {:else if connectionState === 'disconnected'}
353
- <span class="connection-status disconnected">
354
- <span class="connection-status-dot"></span>
355
- </span>
356
- {/if}
357
- </div>
358
- {/if}
359
- </div>
360
- <div class="chat-session-title-row">
361
- <h1 class="chat-session-title">{session.title}</h1>
362
- {#if isSessionActive}
363
- <Pill text="LIVE" classes="pill-live" />
483
+ <!-- Compact header -->
484
+ <div class="session-header">
485
+ <div class="session-header-row">
486
+ <a href={projectId ? `/project?id=${projectId}` : '/'} class="back-link">← Back</a>
487
+ {#if sendStatus === 'connecting'}
488
+ <span class="resume-status">Connecting...</span>
489
+ {:else if sendStatus === 'resuming'}
490
+ <span class="resume-status">Resuming session...</span>
491
+ {:else if chatConnectionState === 'connected'}
492
+ <span class="connection-dot connected"></span>
493
+ {:else if chatConnectionState === 'reconnecting'}
494
+ <span class="connection-dot reconnecting"></span>
364
495
  {/if}
365
496
  </div>
366
- <div class="chat-session-meta">
367
- <span class="chat-session-meta-item">&#128193; {shortPath(session.projectPath)}</span>
368
- <span class="chat-session-meta-item">&#127807; {session.gitBranch}</span>
369
- <span class="chat-session-meta-item">&#128172; {session.messageCount} messages</span>
370
- <span class="chat-session-meta-item">&#128197; {formatDate(session.created)}</span>
371
- </div>
372
- {#if connectionState === 'disconnected' && isSessionActive}
373
- <Banner text="Live updates paused" classes="banner-warning" />
374
- {/if}
497
+ <h1 class="session-title">{session.title}</h1>
375
498
  </div>
376
499
 
377
- <!-- Chat Container -->
500
+ <!-- Chat -->
378
501
  <div class="session-chat-container">
502
+ {#if hasMoreMessages}
503
+ <div class="load-earlier-row">
504
+ <button
505
+ class="load-earlier-btn"
506
+ onclick={loadEarlierMessages}
507
+ disabled={loadingMore}
508
+ >
509
+ {loadingMore ? 'Loading...' : 'Load earlier messages'}
510
+ </button>
511
+ </div>
512
+ {/if}
379
513
  <ChatView
380
- messages={messages}
381
- connectionState={connectionState}
382
- showInput={false}
383
- sessionEnded={!isSessionActive}
514
+ messages={[...messages].reverse()}
515
+ newestFirst={true}
516
+ connectionState={chatConnectionState}
517
+ showInput={true}
518
+ {sendDisabled}
519
+ onSendInput={sendMessage}
520
+ sessionEnded={false}
384
521
  />
385
522
  </div>
386
523
  {/if}
387
524
  </main>
388
525
 
389
526
  <style>
390
- /* Make the main element a flex column so the chat fills remaining space */
391
527
  .session-page-main {
392
528
  display: flex;
393
529
  flex-direction: column;
394
530
  overflow: hidden;
395
531
  }
396
532
 
397
- /* Chat container fills remaining space */
398
- .session-chat-container {
399
- flex: 1;
400
- min-height: 0;
401
- display: flex;
402
- flex-direction: column;
403
- }
404
-
405
- /* Back link (used in error state) */
406
533
  .session-back-row {
407
534
  margin-bottom: var(--space-5);
408
535
  }
409
536
 
410
- /* Header top row: back link + connection status */
411
- .chat-session-header-top {
537
+ .error-text {
538
+ color: var(--text-secondary);
539
+ padding: var(--space-4);
540
+ }
541
+
542
+ /* Compact header */
543
+ .session-header {
544
+ padding: var(--space-3) var(--space-4);
545
+ border-bottom: 1px solid var(--border);
546
+ flex-shrink: 0;
547
+ }
548
+
549
+ .session-header-row {
412
550
  display: flex;
413
551
  align-items: center;
414
552
  justify-content: space-between;
553
+ margin-bottom: var(--space-1);
554
+ }
555
+
556
+ .session-title {
557
+ font-size: var(--text-base);
558
+ font-weight: 600;
559
+ color: var(--text-primary);
560
+ margin: 0;
561
+ white-space: nowrap;
562
+ overflow: hidden;
563
+ text-overflow: ellipsis;
564
+ }
565
+
566
+ .resume-status {
567
+ font-size: 0.75rem;
568
+ color: var(--text-tertiary, #888);
569
+ animation: pulse 1.5s ease-in-out infinite;
570
+ }
571
+
572
+ @keyframes pulse {
573
+ 0%, 100% { opacity: 1; }
574
+ 50% { opacity: 0.4; }
415
575
  }
416
576
 
417
- /* Title row: title + LIVE badge */
418
- .chat-session-title-row {
577
+ .connection-dot {
578
+ display: inline-block;
579
+ width: 7px;
580
+ height: 7px;
581
+ border-radius: 50%;
582
+ flex-shrink: 0;
583
+ }
584
+
585
+ .connection-dot.connected {
586
+ background: #4ade80;
587
+ }
588
+
589
+ .connection-dot.reconnecting {
590
+ background: #f59e0b;
591
+ animation: pulse 1s ease-in-out infinite;
592
+ }
593
+
594
+ /* Chat container fills remaining space */
595
+ .session-chat-container {
596
+ flex: 1;
597
+ min-height: 0;
419
598
  display: flex;
420
- align-items: center;
421
- gap: 0.5rem;
599
+ flex-direction: column;
422
600
  }
423
601
 
424
- /* Connection status row */
425
- .live-connection-row {
602
+ /* Load earlier row */
603
+ .load-earlier-row {
426
604
  display: flex;
427
- align-items: center;
605
+ justify-content: center;
606
+ padding: var(--space-2) var(--space-3);
607
+ flex-shrink: 0;
608
+ border-bottom: 1px solid var(--border);
609
+ }
610
+
611
+ .load-earlier-btn {
612
+ font-size: 0.8rem;
613
+ color: var(--text-secondary);
614
+ background: none;
615
+ border: 1px solid var(--border);
616
+ border-radius: var(--radius-sm, 4px);
617
+ padding: 4px 12px;
618
+ cursor: pointer;
619
+ }
620
+
621
+ .load-earlier-btn:disabled {
622
+ opacity: 0.5;
623
+ cursor: not-allowed;
428
624
  }
429
625
 
430
- /* Paused banner override */
431
- :global(.banner-warning) {
432
- margin-top: 0.5rem;
626
+ .load-earlier-btn:hover:not(:disabled) {
627
+ color: var(--text-primary);
628
+ border-color: var(--text-tertiary, #888);
433
629
  }
434
630
  </style>