@juspay/shooter 1.0.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 (327) hide show
  1. package/.claude/hooks/notifier.cjs +1431 -0
  2. package/.claude/settings.json +162 -0
  3. package/README.md +515 -0
  4. package/bin/shooter.cjs +141 -0
  5. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css +1 -0
  6. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.br +0 -0
  7. package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.gz +0 -0
  8. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css +1 -0
  9. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.br +1 -0
  10. package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.gz +0 -0
  11. package/build/client/_app/immutable/assets/3.C0uFg0IS.css +1 -0
  12. package/build/client/_app/immutable/assets/3.C0uFg0IS.css.br +0 -0
  13. package/build/client/_app/immutable/assets/3.C0uFg0IS.css.gz +0 -0
  14. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css +1 -0
  15. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.br +0 -0
  16. package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.gz +0 -0
  17. package/build/client/_app/immutable/assets/5.DRjApZQW.css +1 -0
  18. package/build/client/_app/immutable/assets/5.DRjApZQW.css.br +0 -0
  19. package/build/client/_app/immutable/assets/5.DRjApZQW.css.gz +0 -0
  20. package/build/client/_app/immutable/assets/6.AraZrY8I.css +1 -0
  21. package/build/client/_app/immutable/assets/6.AraZrY8I.css.br +0 -0
  22. package/build/client/_app/immutable/assets/6.AraZrY8I.css.gz +0 -0
  23. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css +1 -0
  24. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.br +0 -0
  25. package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.gz +0 -0
  26. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css +1 -0
  27. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.br +0 -0
  28. package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.gz +0 -0
  29. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css +1 -0
  30. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.br +0 -0
  31. package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.gz +0 -0
  32. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css +1 -0
  33. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.br +0 -0
  34. package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.gz +0 -0
  35. package/build/client/_app/immutable/chunks/BNJphC1q.js +56 -0
  36. package/build/client/_app/immutable/chunks/BNJphC1q.js.br +0 -0
  37. package/build/client/_app/immutable/chunks/BNJphC1q.js.gz +0 -0
  38. package/build/client/_app/immutable/chunks/BTGVxaYV.js +9 -0
  39. package/build/client/_app/immutable/chunks/BTGVxaYV.js.br +0 -0
  40. package/build/client/_app/immutable/chunks/BTGVxaYV.js.gz +0 -0
  41. package/build/client/_app/immutable/chunks/BlxrFPDK.js +1 -0
  42. package/build/client/_app/immutable/chunks/BlxrFPDK.js.br +0 -0
  43. package/build/client/_app/immutable/chunks/BlxrFPDK.js.gz +0 -0
  44. package/build/client/_app/immutable/chunks/Bvk7mfPM.js +1 -0
  45. package/build/client/_app/immutable/chunks/Bvk7mfPM.js.br +0 -0
  46. package/build/client/_app/immutable/chunks/Bvk7mfPM.js.gz +0 -0
  47. package/build/client/_app/immutable/chunks/CAokzuPQ.js +1 -0
  48. package/build/client/_app/immutable/chunks/CAokzuPQ.js.br +0 -0
  49. package/build/client/_app/immutable/chunks/CAokzuPQ.js.gz +0 -0
  50. package/build/client/_app/immutable/chunks/CGLrx-H5.js +1 -0
  51. package/build/client/_app/immutable/chunks/CGLrx-H5.js.br +0 -0
  52. package/build/client/_app/immutable/chunks/CGLrx-H5.js.gz +0 -0
  53. package/build/client/_app/immutable/chunks/CgCpWzEA.js +1 -0
  54. package/build/client/_app/immutable/chunks/CgCpWzEA.js.br +0 -0
  55. package/build/client/_app/immutable/chunks/CgCpWzEA.js.gz +0 -0
  56. package/build/client/_app/immutable/chunks/Cjwk_cGO.js +6 -0
  57. package/build/client/_app/immutable/chunks/Cjwk_cGO.js.br +0 -0
  58. package/build/client/_app/immutable/chunks/Cjwk_cGO.js.gz +0 -0
  59. package/build/client/_app/immutable/chunks/CtQ8EED1.js +11 -0
  60. package/build/client/_app/immutable/chunks/CtQ8EED1.js.br +0 -0
  61. package/build/client/_app/immutable/chunks/CtQ8EED1.js.gz +0 -0
  62. package/build/client/_app/immutable/chunks/DERQCisl.js +1 -0
  63. package/build/client/_app/immutable/chunks/DERQCisl.js.br +0 -0
  64. package/build/client/_app/immutable/chunks/DERQCisl.js.gz +0 -0
  65. package/build/client/_app/immutable/chunks/DKrg8TQs.js +1 -0
  66. package/build/client/_app/immutable/chunks/DKrg8TQs.js.br +0 -0
  67. package/build/client/_app/immutable/chunks/DKrg8TQs.js.gz +0 -0
  68. package/build/client/_app/immutable/chunks/DLu6yJIZ.js +1 -0
  69. package/build/client/_app/immutable/chunks/DLu6yJIZ.js.br +0 -0
  70. package/build/client/_app/immutable/chunks/DLu6yJIZ.js.gz +0 -0
  71. package/build/client/_app/immutable/chunks/Dkkpz_4D.js +126 -0
  72. package/build/client/_app/immutable/chunks/Dkkpz_4D.js.br +0 -0
  73. package/build/client/_app/immutable/chunks/Dkkpz_4D.js.gz +0 -0
  74. package/build/client/_app/immutable/chunks/DoczjQhA.js +1 -0
  75. package/build/client/_app/immutable/chunks/DoczjQhA.js.br +0 -0
  76. package/build/client/_app/immutable/chunks/DoczjQhA.js.gz +0 -0
  77. package/build/client/_app/immutable/chunks/PPVm8Dsz.js +1 -0
  78. package/build/client/_app/immutable/chunks/PPVm8Dsz.js.br +0 -0
  79. package/build/client/_app/immutable/chunks/PPVm8Dsz.js.gz +0 -0
  80. package/build/client/_app/immutable/chunks/RpcNruLP.js +2 -0
  81. package/build/client/_app/immutable/chunks/RpcNruLP.js.br +0 -0
  82. package/build/client/_app/immutable/chunks/RpcNruLP.js.gz +0 -0
  83. package/build/client/_app/immutable/chunks/a-St0Zwo.js +1 -0
  84. package/build/client/_app/immutable/chunks/a-St0Zwo.js.br +0 -0
  85. package/build/client/_app/immutable/chunks/a-St0Zwo.js.gz +0 -0
  86. package/build/client/_app/immutable/chunks/bo70OQUZ.js +1 -0
  87. package/build/client/_app/immutable/chunks/bo70OQUZ.js.br +0 -0
  88. package/build/client/_app/immutable/chunks/bo70OQUZ.js.gz +0 -0
  89. package/build/client/_app/immutable/entry/app.QvGgdvTI.js +2 -0
  90. package/build/client/_app/immutable/entry/app.QvGgdvTI.js.br +0 -0
  91. package/build/client/_app/immutable/entry/app.QvGgdvTI.js.gz +0 -0
  92. package/build/client/_app/immutable/entry/start.BntDNRMC.js +1 -0
  93. package/build/client/_app/immutable/entry/start.BntDNRMC.js.br +0 -0
  94. package/build/client/_app/immutable/entry/start.BntDNRMC.js.gz +0 -0
  95. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js +1 -0
  96. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.br +0 -0
  97. package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.gz +0 -0
  98. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js +1 -0
  99. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.br +0 -0
  100. package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.gz +0 -0
  101. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js +1 -0
  102. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.br +0 -0
  103. package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.gz +0 -0
  104. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js +3 -0
  105. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.br +0 -0
  106. package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.gz +0 -0
  107. package/build/client/_app/immutable/nodes/4.D-cIe70D.js +1 -0
  108. package/build/client/_app/immutable/nodes/4.D-cIe70D.js.br +0 -0
  109. package/build/client/_app/immutable/nodes/4.D-cIe70D.js.gz +0 -0
  110. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js +1 -0
  111. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.br +0 -0
  112. package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.gz +0 -0
  113. package/build/client/_app/immutable/nodes/6.BB7QE48r.js +2 -0
  114. package/build/client/_app/immutable/nodes/6.BB7QE48r.js.br +0 -0
  115. package/build/client/_app/immutable/nodes/6.BB7QE48r.js.gz +0 -0
  116. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js +2 -0
  117. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.br +0 -0
  118. package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.gz +0 -0
  119. package/build/client/_app/version.json +1 -0
  120. package/build/client/_app/version.json.br +0 -0
  121. package/build/client/_app/version.json.gz +0 -0
  122. package/build/client/app-icon.png +0 -0
  123. package/build/client/apple-touch-icon.png +0 -0
  124. package/build/client/favicon.png +0 -0
  125. package/build/client/favicon.svg +10 -0
  126. package/build/client/favicon.svg.br +0 -0
  127. package/build/client/favicon.svg.gz +0 -0
  128. package/build/client/manifest.webmanifest +1 -0
  129. package/build/client/pwa-192x192.png +0 -0
  130. package/build/client/pwa-512x512.png +0 -0
  131. package/build/client/registerSW.js +1 -0
  132. package/build/client/registerSW.js.br +0 -0
  133. package/build/client/registerSW.js.gz +0 -0
  134. package/build/client/sw.js +222 -0
  135. package/build/client/sw.js.br +0 -0
  136. package/build/client/sw.js.gz +0 -0
  137. package/build/client/workbox-5119daf5.js +3395 -0
  138. package/build/client/workbox-5119daf5.js.br +0 -0
  139. package/build/client/workbox-5119daf5.js.gz +0 -0
  140. package/build/env.js +94 -0
  141. package/build/handler.js +1494 -0
  142. package/build/index.js +345 -0
  143. package/build/pty-holder.cjs +510 -0
  144. package/build/server/chunks/0-q2IUp76Y.js +9 -0
  145. package/build/server/chunks/0-q2IUp76Y.js.map +1 -0
  146. package/build/server/chunks/1-CU50G5wZ.js +9 -0
  147. package/build/server/chunks/1-CU50G5wZ.js.map +1 -0
  148. package/build/server/chunks/2-D01t9s8T.js +9 -0
  149. package/build/server/chunks/2-D01t9s8T.js.map +1 -0
  150. package/build/server/chunks/3-5PUQ04wC.js +9 -0
  151. package/build/server/chunks/3-5PUQ04wC.js.map +1 -0
  152. package/build/server/chunks/4-e7gywnSG.js +9 -0
  153. package/build/server/chunks/4-e7gywnSG.js.map +1 -0
  154. package/build/server/chunks/5-CA1SA6KZ.js +9 -0
  155. package/build/server/chunks/5-CA1SA6KZ.js.map +1 -0
  156. package/build/server/chunks/6-71H221sV.js +9 -0
  157. package/build/server/chunks/6-71H221sV.js.map +1 -0
  158. package/build/server/chunks/7-Bo-vmdyz.js +9 -0
  159. package/build/server/chunks/7-Bo-vmdyz.js.map +1 -0
  160. package/build/server/chunks/_layout.svelte-SFHOxs74.js +132 -0
  161. package/build/server/chunks/_layout.svelte-SFHOxs74.js.map +1 -0
  162. package/build/server/chunks/_page.svelte-B4w-2wD-.js +120 -0
  163. package/build/server/chunks/_page.svelte-B4w-2wD-.js.map +1 -0
  164. package/build/server/chunks/_page.svelte-B_qAXjkh.js +213 -0
  165. package/build/server/chunks/_page.svelte-B_qAXjkh.js.map +1 -0
  166. package/build/server/chunks/_page.svelte-CsF1_TRG.js +50 -0
  167. package/build/server/chunks/_page.svelte-CsF1_TRG.js.map +1 -0
  168. package/build/server/chunks/_page.svelte-DJC6U-P0.js +68 -0
  169. package/build/server/chunks/_page.svelte-DJC6U-P0.js.map +1 -0
  170. package/build/server/chunks/_page.svelte-DQ6HBtsz.js +407 -0
  171. package/build/server/chunks/_page.svelte-DQ6HBtsz.js.map +1 -0
  172. package/build/server/chunks/_page.svelte-LbhhjP21.js +148 -0
  173. package/build/server/chunks/_page.svelte-LbhhjP21.js.map +1 -0
  174. package/build/server/chunks/_server.ts-BL2FGb5Z.js +387 -0
  175. package/build/server/chunks/_server.ts-BL2FGb5Z.js.map +1 -0
  176. package/build/server/chunks/_server.ts-BgdjBZco.js +47 -0
  177. package/build/server/chunks/_server.ts-BgdjBZco.js.map +1 -0
  178. package/build/server/chunks/_server.ts-BihKSdj_.js +59 -0
  179. package/build/server/chunks/_server.ts-BihKSdj_.js.map +1 -0
  180. package/build/server/chunks/_server.ts-BjOJsoy4.js +63 -0
  181. package/build/server/chunks/_server.ts-BjOJsoy4.js.map +1 -0
  182. package/build/server/chunks/_server.ts-C29xzfaw.js +77 -0
  183. package/build/server/chunks/_server.ts-C29xzfaw.js.map +1 -0
  184. package/build/server/chunks/_server.ts-CPa6DgIt.js +71 -0
  185. package/build/server/chunks/_server.ts-CPa6DgIt.js.map +1 -0
  186. package/build/server/chunks/_server.ts-CbDRDIoP.js +36 -0
  187. package/build/server/chunks/_server.ts-CbDRDIoP.js.map +1 -0
  188. package/build/server/chunks/_server.ts-Cl1OEWL4.js +54 -0
  189. package/build/server/chunks/_server.ts-Cl1OEWL4.js.map +1 -0
  190. package/build/server/chunks/_server.ts-ColfDHW8.js +60 -0
  191. package/build/server/chunks/_server.ts-ColfDHW8.js.map +1 -0
  192. package/build/server/chunks/_server.ts-Cv_OrRuL.js +494 -0
  193. package/build/server/chunks/_server.ts-Cv_OrRuL.js.map +1 -0
  194. package/build/server/chunks/_server.ts-D4MNi4cD.js +25 -0
  195. package/build/server/chunks/_server.ts-D4MNi4cD.js.map +1 -0
  196. package/build/server/chunks/_server.ts-DRVbgm6k.js +125 -0
  197. package/build/server/chunks/_server.ts-DRVbgm6k.js.map +1 -0
  198. package/build/server/chunks/_server.ts-DfajWaqh.js +39 -0
  199. package/build/server/chunks/_server.ts-DfajWaqh.js.map +1 -0
  200. package/build/server/chunks/_server.ts-y9-WYDMa.js +35 -0
  201. package/build/server/chunks/_server.ts-y9-WYDMa.js.map +1 -0
  202. package/build/server/chunks/auth-CEgFis71.js +32 -0
  203. package/build/server/chunks/auth-CEgFis71.js.map +1 -0
  204. package/build/server/chunks/client-CxCatAKr.js +255 -0
  205. package/build/server/chunks/client-CxCatAKr.js.map +1 -0
  206. package/build/server/chunks/error.svelte-BqdwMWdK.js +26 -0
  207. package/build/server/chunks/error.svelte-BqdwMWdK.js.map +1 -0
  208. package/build/server/chunks/exports-CJ0Q5XmL.js +4081 -0
  209. package/build/server/chunks/exports-CJ0Q5XmL.js.map +1 -0
  210. package/build/server/chunks/index2-DAxIoAO-.js +36 -0
  211. package/build/server/chunks/index2-DAxIoAO-.js.map +1 -0
  212. package/build/server/chunks/jsonl-parser-dmZU_Hyu.js +137 -0
  213. package/build/server/chunks/jsonl-parser-dmZU_Hyu.js.map +1 -0
  214. package/build/server/chunks/library-apns-BHxLmuIx.js +104 -0
  215. package/build/server/chunks/library-apns-BHxLmuIx.js.map +1 -0
  216. package/build/server/chunks/markdown-Bxrl3cCF.js +1241 -0
  217. package/build/server/chunks/markdown-Bxrl3cCF.js.map +1 -0
  218. package/build/server/chunks/pending-requests-D8UiTw7L.js +44 -0
  219. package/build/server/chunks/pending-requests-D8UiTw7L.js.map +1 -0
  220. package/build/server/chunks/pty-manager-C0FhBiVq.js +1697 -0
  221. package/build/server/chunks/pty-manager-C0FhBiVq.js.map +1 -0
  222. package/build/server/chunks/shared-server-BDY8jh20.js +200 -0
  223. package/build/server/chunks/shared-server-BDY8jh20.js.map +1 -0
  224. package/build/server/chunks/stores-D0HorpgL.js +36 -0
  225. package/build/server/chunks/stores-D0HorpgL.js.map +1 -0
  226. package/build/server/index.js +6466 -0
  227. package/build/server/index.js.map +1 -0
  228. package/build/server/manifest.js +184 -0
  229. package/build/server/manifest.js.map +1 -0
  230. package/build/shims.js +32 -0
  231. package/package.json +94 -0
  232. package/scripts/clipboard-shims/wl-paste +48 -0
  233. package/scripts/clipboard-shims/xclip +31 -0
  234. package/scripts/install.sh +477 -0
  235. package/scripts/setup-node-pty.sh +63 -0
  236. package/scripts/setup.cjs +571 -0
  237. package/scripts/test-runner.ts +243 -0
  238. package/scripts/vercel-env-commands.sh +60 -0
  239. package/server.ts +139 -0
  240. package/src/app.css +1835 -0
  241. package/src/app.d.ts +31 -0
  242. package/src/app.html +24 -0
  243. package/src/generated/types/APN.ts +305 -0
  244. package/src/generated/types/CLI.ts +52 -0
  245. package/src/generated/types/JWT.ts +92 -0
  246. package/src/generated/types/Terminal.ts +2736 -0
  247. package/src/generated/types/index.ts +6 -0
  248. package/src/lib/assets/icons/alert-triangle.svg +5 -0
  249. package/src/lib/assets/icons/bell.svg +4 -0
  250. package/src/lib/assets/icons/check-circle.svg +4 -0
  251. package/src/lib/assets/icons/file.svg +4 -0
  252. package/src/lib/assets/icons/folder.svg +3 -0
  253. package/src/lib/assets/icons/play.svg +3 -0
  254. package/src/lib/assets/icons/refresh.svg +4 -0
  255. package/src/lib/assets/icons/settings.svg +4 -0
  256. package/src/lib/assets/icons/terminal.svg +1 -0
  257. package/src/lib/assets/icons/tool.svg +3 -0
  258. package/src/lib/assets/icons/x-circle.svg +5 -0
  259. package/src/lib/modules/client/common/Card.svelte +26 -0
  260. package/src/lib/modules/client/common/EmptyState.svelte +36 -0
  261. package/src/lib/modules/client/common/Icon.svelte +61 -0
  262. package/src/lib/modules/client/common/StatusBadge.svelte +38 -0
  263. package/src/lib/modules/client/common/cache.ts +31 -0
  264. package/src/lib/modules/client/common/config-guard.ts +18 -0
  265. package/src/lib/modules/client/common/index.ts +12 -0
  266. package/src/lib/modules/client/common/markdown.ts +23 -0
  267. package/src/lib/modules/client/common/native-bridge.ts +50 -0
  268. package/src/lib/modules/client/common/time.ts +22 -0
  269. package/src/lib/modules/client/common/tool-title.ts +28 -0
  270. package/src/lib/modules/client/terminal/ChatView.svelte +400 -0
  271. package/src/lib/modules/client/terminal/CommandPalette.svelte +60 -0
  272. package/src/lib/modules/client/terminal/ConnectionStatus.svelte +99 -0
  273. package/src/lib/modules/client/terminal/LaunchSheet.svelte +294 -0
  274. package/src/lib/modules/client/terminal/QuickKeys.svelte +71 -0
  275. package/src/lib/modules/client/terminal/ShortcutsHelp.svelte +79 -0
  276. package/src/lib/modules/client/terminal/keyboard-shortcuts.ts +70 -0
  277. package/src/lib/modules/client/terminal/xterm-wrapper.ts +243 -0
  278. package/src/lib/modules/server/apn/library-apns.ts +137 -0
  279. package/src/lib/modules/server/apn/notification-history.ts +35 -0
  280. package/src/lib/modules/server/apn/notification-sessions.ts +117 -0
  281. package/src/lib/modules/server/apn/pending-requests.ts +65 -0
  282. package/src/lib/modules/server/apn/types.ts +51 -0
  283. package/src/lib/modules/server/auth.ts +34 -0
  284. package/src/lib/modules/server/cli/index.ts +79 -0
  285. package/src/lib/modules/server/cli/runner.ts +162 -0
  286. package/src/lib/modules/server/fcm/fcm-service.ts +72 -0
  287. package/src/lib/modules/server/sessions/jsonl-parser.ts +197 -0
  288. package/src/lib/modules/server/sessions/jsonl-reader.ts +301 -0
  289. package/src/lib/modules/server/sessions/opencode-reader.ts +264 -0
  290. package/src/lib/modules/server/sessions/types.ts +53 -0
  291. package/src/lib/modules/server/terminal/holder-client.ts +273 -0
  292. package/src/lib/modules/server/terminal/opencode-watcher.ts +661 -0
  293. package/src/lib/modules/server/terminal/pty-holder.cjs +510 -0
  294. package/src/lib/modules/server/terminal/pty-manager.ts +1012 -0
  295. package/src/lib/modules/server/terminal/session-watcher.ts +320 -0
  296. package/src/lib/modules/server/terminal/terminal-store.ts +198 -0
  297. package/src/lib/modules/server/ws/events-handler.ts +73 -0
  298. package/src/lib/modules/server/ws/keepalive.ts +108 -0
  299. package/src/lib/modules/server/ws/server.ts +93 -0
  300. package/src/lib/modules/server/ws/session-handler.ts +462 -0
  301. package/src/lib/modules/server/ws/terminal-handler.ts +197 -0
  302. package/src/lib/modules/server/ws/ticket-store.ts +58 -0
  303. package/src/lib/theme.css +529 -0
  304. package/src/lib/types/config.ts +6 -0
  305. package/src/routes/+layout.svelte +218 -0
  306. package/src/routes/+page.svelte +261 -0
  307. package/src/routes/api/debug/+server.ts +33 -0
  308. package/src/routes/api/device-token/+server.ts +85 -0
  309. package/src/routes/api/health/+server.ts +100 -0
  310. package/src/routes/api/notify/+server.ts +418 -0
  311. package/src/routes/api/qr-config/+server.ts +45 -0
  312. package/src/routes/api/response/+server.ts +73 -0
  313. package/src/routes/api/sessions/+server.ts +120 -0
  314. package/src/routes/api/terminals/+server.ts +141 -0
  315. package/src/routes/api/terminals/[id]/+server.ts +75 -0
  316. package/src/routes/api/terminals/[id]/paste-image/+server.ts +61 -0
  317. package/src/routes/api/terminals/[id]/resize/+server.ts +60 -0
  318. package/src/routes/api/webhook/+server.ts +42 -0
  319. package/src/routes/api/ws-status/+server.ts +23 -0
  320. package/src/routes/api/ws-ticket/+server.ts +86 -0
  321. package/src/routes/config/+page.svelte +600 -0
  322. package/src/routes/project/+page.svelte +274 -0
  323. package/src/routes/session/[id]/+page.svelte +434 -0
  324. package/src/routes/terminals/+page.svelte +618 -0
  325. package/src/routes/terminals/[id]/+page.svelte +968 -0
  326. package/svelte.config.js +18 -0
  327. package/tsconfig.json +14 -0
@@ -0,0 +1,274 @@
1
+ <script lang="ts">
2
+ import type { ShooterConfig } from '$lib/types/config';
3
+
4
+ import { page } from '$app/state';
5
+ import { Button, Pill, Shimmer } from '@juspay/svelte-ui-components';
6
+ import {
7
+ EmptyState,
8
+ formatRelativeTime,
9
+ getCached,
10
+ Icon,
11
+ isShooterConfig,
12
+ setCache,
13
+ } from '$lib/modules/client/common';
14
+ import { onDestroy, onMount } from 'svelte';
15
+
16
+ interface Session {
17
+ created: string;
18
+ gitBranch: string;
19
+ id: string;
20
+ messageCount: number;
21
+ modified: string;
22
+ projectPath: string;
23
+ source: 'claude-code' | 'opencode';
24
+ summary: string;
25
+ title: string;
26
+ }
27
+
28
+ interface ProjectGroup {
29
+ fullPath: string;
30
+ id: string;
31
+ lastModified: string;
32
+ name: string;
33
+ sessionCount: number;
34
+ sessions: Session[];
35
+ }
36
+
37
+ const POLL_INTERVAL_MS = 15_000;
38
+ const PAGE_SIZE = 20;
39
+
40
+ let project = $state<null | ProjectGroup>(null);
41
+ let loading = $state(true);
42
+ let config = $state<null | ShooterConfig>(null);
43
+ let pollTimer: null | ReturnType<typeof setInterval> = null;
44
+ let visibleCount = $state(PAGE_SIZE);
45
+
46
+ const projectId = $derived(page.url.searchParams.get('id') || '');
47
+
48
+ const visibleSessions = $derived(project ? project.sessions.slice(0, visibleCount) : []);
49
+ const hasMore = $derived(project ? visibleCount < project.sessions.length : false);
50
+
51
+ onMount(() => {
52
+ loadConfiguration();
53
+
54
+ // Show cached data immediately
55
+ const cached = getCached(`shooter_project_${projectId}`) as null | ProjectGroup;
56
+ if (cached) {
57
+ project = cached;
58
+ loading = false;
59
+ }
60
+
61
+ // Delay initial fetch to ensure config and projectId are resolved
62
+ setTimeout(() => {
63
+ void fetchProject();
64
+ }, 50);
65
+
66
+ pollTimer = setInterval(() => {
67
+ if (config?.apiKey && projectId) {
68
+ void fetchProject();
69
+ }
70
+ }, POLL_INTERVAL_MS);
71
+ });
72
+
73
+ onDestroy(() => {
74
+ if (pollTimer) {
75
+ clearInterval(pollTimer);
76
+ pollTimer = null;
77
+ }
78
+ });
79
+
80
+ function loadConfiguration(): void {
81
+ try {
82
+ const saved = localStorage.getItem('shooter_config');
83
+ if (saved) {
84
+ const parsed: unknown = JSON.parse(saved);
85
+ if (isShooterConfig(parsed)) {
86
+ config = parsed;
87
+ } else {
88
+ localStorage.removeItem('shooter_config');
89
+ config = null;
90
+ }
91
+ }
92
+ } catch {
93
+ // No configuration — expected on first visit
94
+ }
95
+ }
96
+
97
+ async function fetchProject(): Promise<void> {
98
+ if (!config?.apiKey || !projectId) {
99
+ loading = false;
100
+ return;
101
+ }
102
+
103
+ // Don't show loading spinner if we already have cached data
104
+ if (!project) {
105
+ loading = true;
106
+ }
107
+
108
+ try {
109
+ const response = await fetch('/api/sessions', {
110
+ headers: {
111
+ Authorization: `Bearer ${config.apiKey}`,
112
+ },
113
+ });
114
+
115
+ if (!response.ok) {
116
+ loading = false;
117
+ return;
118
+ }
119
+
120
+ const result: { projects: ProjectGroup[] } = await response.json();
121
+ const foundProject = result.projects.find((p) => p.id === projectId) || null;
122
+ project = foundProject;
123
+ if (foundProject) {
124
+ setCache(`shooter_project_${projectId}`, foundProject);
125
+ }
126
+ } catch (error) {
127
+ console.error('Failed to fetch project:', error);
128
+ } finally {
129
+ loading = false;
130
+ }
131
+ }
132
+
133
+ async function forceRefresh(): Promise<void> {
134
+ loading = true;
135
+ sessionStorage.removeItem(`shooter_project_${projectId}`);
136
+ visibleCount = PAGE_SIZE;
137
+ await fetchProject();
138
+ }
139
+
140
+ function loadMore(): void {
141
+ visibleCount += PAGE_SIZE;
142
+ }
143
+
144
+ function formatDate(ts: string): string {
145
+ return new Date(ts).toLocaleDateString('en-US', {
146
+ day: 'numeric',
147
+ hour: '2-digit',
148
+ minute: '2-digit',
149
+ month: 'short',
150
+ });
151
+ }
152
+
153
+ function truncate(text: string, maxLength: number): string {
154
+ if (text.length <= maxLength) {
155
+ return text;
156
+ }
157
+ return `${text.slice(0, maxLength).trimEnd()}...`;
158
+ }
159
+ </script>
160
+
161
+ <svelte:head>
162
+ <title>{project?.name || 'Project'} - Shooter</title>
163
+ <meta name="description" content="Project sessions sorted by latest update" />
164
+ </svelte:head>
165
+
166
+ <main class="main">
167
+ {#if loading && !project}
168
+ <div class="loading-container">
169
+ <Shimmer classes="shimmer-header" />
170
+ {#each Array(4) as _, i (i)}
171
+ <Shimmer classes="shimmer-card" />
172
+ {/each}
173
+ </div>
174
+ {:else if !project}
175
+ <div class="project-back-row">
176
+ <a href="/" class="back-link">
177
+ <span class="back-arrow">&larr;</span>
178
+ Back to Projects
179
+ </a>
180
+ </div>
181
+ <EmptyState
182
+ icon="alert-triangle"
183
+ title="Project Not Found"
184
+ description="The requested project could not be found."
185
+ />
186
+ {:else}
187
+ <div class="chat-session-header">
188
+ <div class="chat-session-header-top">
189
+ <a href="/" class="back-link">&#8592; Back to Projects</a>
190
+ <Button classes="btn-secondary" onclick={forceRefresh} disabled={loading}>
191
+ {#snippet children()}
192
+ <Icon name="refresh" size={14} />
193
+ Refresh
194
+ {/snippet}
195
+ </Button>
196
+ </div>
197
+ <h1 class="chat-session-title">{project.name}</h1>
198
+ <div class="chat-session-meta">
199
+ <span class="session-card-subtitle">{project.fullPath}</span>
200
+ <span>{project.sessionCount} sessions</span>
201
+ </div>
202
+ </div>
203
+
204
+ {#if project.sessions.length === 0}
205
+ <EmptyState
206
+ icon="bell"
207
+ title="No sessions yet"
208
+ description="Sessions for this project will appear here"
209
+ />
210
+ {:else}
211
+ <div class="sessions-container">
212
+ {#each visibleSessions as session (session.id)}
213
+ <a href="/session/{session.id}?project={projectId}" class="session-card">
214
+ <div class="session-card-header">
215
+ <div>
216
+ <h3 class="session-card-title">{session.title}</h3>
217
+ {#if session.summary}
218
+ <div class="session-card-subtitle">{truncate(session.summary, 80)}</div>
219
+ {/if}
220
+ </div>
221
+ <Pill text={formatRelativeTime(session.modified)} classes="pill-session-time" />
222
+ </div>
223
+ <div class="session-stats">
224
+ <span><strong>{session.messageCount}</strong> messages</span>
225
+ {#if session.gitBranch}
226
+ <Pill text="🌿 {session.gitBranch}" classes="pill-git-branch" />
227
+ {/if}
228
+ {#if session.source === 'opencode'}
229
+ <Pill text="OpenCode" classes="pill-source-opencode" />
230
+ {:else}
231
+ <Pill text="Claude Code" classes="pill-source-claude" />
232
+ {/if}
233
+ </div>
234
+ <div class="session-duration">Created {formatDate(session.created)}</div>
235
+ </a>
236
+ {/each}
237
+ </div>
238
+ {#if hasMore}
239
+ <div style="text-align: center; padding: 1rem;">
240
+ <Button classes="btn-secondary" onclick={loadMore}
241
+ text={`Load More (${project.sessions.length - visibleCount} remaining)`}
242
+ />
243
+ </div>
244
+ {/if}
245
+ {/if}
246
+ {/if}
247
+ </main>
248
+
249
+ <style>
250
+ .project-back-row {
251
+ margin-bottom: var(--space-5);
252
+ }
253
+
254
+ .chat-session-header-top {
255
+ display: flex;
256
+ justify-content: space-between;
257
+ align-items: center;
258
+ margin-bottom: var(--space-2);
259
+ }
260
+
261
+ .sessions-container {
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: var(--space-3);
265
+ }
266
+
267
+ @media (max-width: 480px) {
268
+ .chat-session-header-top {
269
+ flex-direction: column;
270
+ align-items: flex-start;
271
+ gap: var(--space-2);
272
+ }
273
+ }
274
+ </style>
@@ -0,0 +1,434 @@
1
+ <script lang="ts">
2
+ import type {
3
+ ConversationMessage,
4
+ MessagePart,
5
+ SessionInfo,
6
+ ToolUsePart,
7
+ } from '$lib/modules/server/sessions/types';
8
+
9
+ import { browser } from '$app/environment';
10
+ 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';
17
+ import ChatView from '$lib/modules/client/terminal/ChatView.svelte';
18
+ import { onMount } from 'svelte';
19
+
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
+ }
40
+
41
+ let session = $state<null | SessionInfo>(null);
42
+ let messages = $state<ConversationMessage[]>([]);
43
+ let loading = $state(true);
44
+ 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;
53
+
54
+ const sessionId = $derived(page.params.id);
55
+ const projectId = $derived(page.url.searchParams.get('project') || '');
56
+
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
+ }
63
+
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}`;
69
+ }
70
+
71
+ // Handle incoming WebSocket messages
72
+ function handleWsMessage(event: MessageEvent): void {
73
+ let data: WsMessage;
74
+ try {
75
+ data = JSON.parse(event.data);
76
+ } catch {
77
+ return;
78
+ }
79
+
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',
95
+ type: 'tool_use',
96
+ };
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: [
103
+ {
104
+ isError: data.isError || false,
105
+ output: data.output || '',
106
+ toolUseId: data.id || '',
107
+ type: 'tool_result',
108
+ },
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(),
128
+ };
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;
140
+ }
141
+ }
142
+ }
143
+
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
+ }
163
+
164
+ // Connect to the live session WebSocket
165
+ async function connectWebSocket(): Promise<void> {
166
+ if (!browser || !isSessionActive) {return;}
167
+
168
+ const saved = localStorage.getItem('shooter_config');
169
+ if (!saved) {return;}
170
+ const config = JSON.parse(saved);
171
+
172
+ connectionState = 'connecting';
173
+
174
+ try {
175
+ // Obtain a short-lived ticket
176
+ const ticketRes = await fetch('/api/ws-ticket', {
177
+ headers: { Authorization: `Bearer ${config.apiKey}` },
178
+ method: 'POST',
179
+ });
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
+
190
+ const socket = new WebSocket(wsUrl);
191
+
192
+ socket.onopen = () => {
193
+ connectionState = 'connected';
194
+ ws = socket;
195
+ wsReconnectAttempts = 0; // Reset on successful connection
196
+ };
197
+
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
+ };
216
+ } catch {
217
+ connectionState = 'disconnected';
218
+ }
219
+ }
220
+
221
+ function formatDate(ts: string): string {
222
+ return new Date(ts).toLocaleDateString([], {
223
+ day: 'numeric',
224
+ month: 'short',
225
+ year: 'numeric',
226
+ });
227
+ }
228
+
229
+ function shortPath(p: string): string {
230
+ const parts = p.split('/');
231
+ return parts.slice(-2).join('/');
232
+ }
233
+
234
+ async function fetchSession(): Promise<void> {
235
+ if (!browser) {
236
+ return;
237
+ }
238
+
239
+ // Don't show loading spinner if we already have cached data
240
+ if (messages.length === 0) {
241
+ loading = true;
242
+ }
243
+
244
+ try {
245
+ const saved = localStorage.getItem('shooter_config');
246
+ if (!saved) {
247
+ error = 'No configuration found. Please configure settings first.';
248
+ loading = false;
249
+ return;
250
+ }
251
+ const config = JSON.parse(saved);
252
+
253
+ const sid = sessionId || '';
254
+ const pid = projectId || '';
255
+ const queryStr = pid
256
+ ? `id=${encodeURIComponent(sid)}&project=${encodeURIComponent(pid)}`
257
+ : `id=${encodeURIComponent(sid)}`;
258
+ const res = await fetch(`/api/sessions?${queryStr}`, {
259
+ headers: { Authorization: `Bearer ${config.apiKey}` },
260
+ });
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();
276
+ }
277
+ }
278
+ } catch {
279
+ error = 'Failed to load session';
280
+ }
281
+ loading = false;
282
+ }
283
+
284
+ onMount(() => {
285
+ // Show cached data immediately
286
+ const cached = getCached(`shooter_session_${page.params.id}`) as null | {
287
+ messages: ConversationMessage[];
288
+ session: SessionInfo;
289
+ };
290
+ if (cached) {
291
+ session = cached.session;
292
+ messages = (cached.messages || []).reverse();
293
+ loading = false;
294
+ }
295
+
296
+ void fetchSession();
297
+
298
+ // Cleanup WebSocket and reconnect timer on component destroy
299
+ return () => {
300
+ if (wsReconnectTimer) {
301
+ clearTimeout(wsReconnectTimer);
302
+ wsReconnectTimer = null;
303
+ }
304
+ if (ws) {
305
+ ws.close();
306
+ ws = null;
307
+ }
308
+ };
309
+ });
310
+ </script>
311
+
312
+ <svelte:head>
313
+ <title>{session?.title || 'Session'} - Shooter</title>
314
+ <meta name="description" content="Session conversation view" />
315
+ </svelte:head>
316
+
317
+ <main class="main session-page-main">
318
+ {#if loading && messages.length === 0}
319
+ <div class="loading-container">
320
+ <Shimmer classes="shimmer-header" />
321
+ <div class="chat-container">
322
+ <Shimmer classes="shimmer-bubble shimmer-bubble-user" />
323
+ <Shimmer classes="shimmer-bubble shimmer-bubble-assistant" />
324
+ <Shimmer classes="shimmer-bubble shimmer-bubble-user-short" />
325
+ <Shimmer classes="shimmer-bubble shimmer-bubble-assistant-wide" />
326
+ <Shimmer classes="shimmer-bubble shimmer-bubble-assistant-short" />
327
+ </div>
328
+ </div>
329
+ {:else if error}
330
+ <div class="session-back-row">
331
+ <a href="/" class="back-link">
332
+ <span class="back-arrow">&larr;</span>
333
+ Back
334
+ </a>
335
+ </div>
336
+ <EmptyState icon="alert-triangle" title="Error" description={error} />
337
+ {: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" />
364
+ {/if}
365
+ </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}
375
+ </div>
376
+
377
+ <!-- Chat Container -->
378
+ <div class="session-chat-container">
379
+ <ChatView
380
+ messages={messages}
381
+ connectionState={connectionState}
382
+ showInput={false}
383
+ sessionEnded={!isSessionActive}
384
+ />
385
+ </div>
386
+ {/if}
387
+ </main>
388
+
389
+ <style>
390
+ /* Make the main element a flex column so the chat fills remaining space */
391
+ .session-page-main {
392
+ display: flex;
393
+ flex-direction: column;
394
+ overflow: hidden;
395
+ }
396
+
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
+ .session-back-row {
407
+ margin-bottom: var(--space-5);
408
+ }
409
+
410
+ /* Header top row: back link + connection status */
411
+ .chat-session-header-top {
412
+ display: flex;
413
+ align-items: center;
414
+ justify-content: space-between;
415
+ }
416
+
417
+ /* Title row: title + LIVE badge */
418
+ .chat-session-title-row {
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 0.5rem;
422
+ }
423
+
424
+ /* Connection status row */
425
+ .live-connection-row {
426
+ display: flex;
427
+ align-items: center;
428
+ }
429
+
430
+ /* Paused banner override */
431
+ :global(.banner-warning) {
432
+ margin-top: 0.5rem;
433
+ }
434
+ </style>