@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.
- package/.claude/hooks/notifier.cjs +1431 -0
- package/.claude/settings.json +162 -0
- package/README.md +515 -0
- package/bin/shooter.cjs +141 -0
- package/build/client/_app/immutable/assets/0.CM9Hl6d-.css +1 -0
- package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.br +0 -0
- package/build/client/_app/immutable/assets/0.CM9Hl6d-.css.gz +0 -0
- package/build/client/_app/immutable/assets/2.CAShZ7lQ.css +1 -0
- package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.br +1 -0
- package/build/client/_app/immutable/assets/2.CAShZ7lQ.css.gz +0 -0
- package/build/client/_app/immutable/assets/3.C0uFg0IS.css +1 -0
- package/build/client/_app/immutable/assets/3.C0uFg0IS.css.br +0 -0
- package/build/client/_app/immutable/assets/3.C0uFg0IS.css.gz +0 -0
- package/build/client/_app/immutable/assets/4.cJuCkJKZ.css +1 -0
- package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.br +0 -0
- package/build/client/_app/immutable/assets/4.cJuCkJKZ.css.gz +0 -0
- package/build/client/_app/immutable/assets/5.DRjApZQW.css +1 -0
- package/build/client/_app/immutable/assets/5.DRjApZQW.css.br +0 -0
- package/build/client/_app/immutable/assets/5.DRjApZQW.css.gz +0 -0
- package/build/client/_app/immutable/assets/6.AraZrY8I.css +1 -0
- package/build/client/_app/immutable/assets/6.AraZrY8I.css.br +0 -0
- package/build/client/_app/immutable/assets/6.AraZrY8I.css.gz +0 -0
- package/build/client/_app/immutable/assets/7.BCJ1IuMx.css +1 -0
- package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.br +0 -0
- package/build/client/_app/immutable/assets/7.BCJ1IuMx.css.gz +0 -0
- package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css +1 -0
- package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.br +0 -0
- package/build/client/_app/immutable/assets/ChatView.CsdBAOKx.css.gz +0 -0
- package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css +1 -0
- package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.br +0 -0
- package/build/client/_app/immutable/assets/markdown.B0b5w2tq.css.gz +0 -0
- package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css +1 -0
- package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.br +0 -0
- package/build/client/_app/immutable/assets/xterm.DFuMZ0ql.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BNJphC1q.js +56 -0
- package/build/client/_app/immutable/chunks/BNJphC1q.js.br +0 -0
- package/build/client/_app/immutable/chunks/BNJphC1q.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BTGVxaYV.js +9 -0
- package/build/client/_app/immutable/chunks/BTGVxaYV.js.br +0 -0
- package/build/client/_app/immutable/chunks/BTGVxaYV.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BlxrFPDK.js +1 -0
- package/build/client/_app/immutable/chunks/BlxrFPDK.js.br +0 -0
- package/build/client/_app/immutable/chunks/BlxrFPDK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Bvk7mfPM.js +1 -0
- package/build/client/_app/immutable/chunks/Bvk7mfPM.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bvk7mfPM.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CAokzuPQ.js +1 -0
- package/build/client/_app/immutable/chunks/CAokzuPQ.js.br +0 -0
- package/build/client/_app/immutable/chunks/CAokzuPQ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CGLrx-H5.js +1 -0
- package/build/client/_app/immutable/chunks/CGLrx-H5.js.br +0 -0
- package/build/client/_app/immutable/chunks/CGLrx-H5.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CgCpWzEA.js +1 -0
- package/build/client/_app/immutable/chunks/CgCpWzEA.js.br +0 -0
- package/build/client/_app/immutable/chunks/CgCpWzEA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Cjwk_cGO.js +6 -0
- package/build/client/_app/immutable/chunks/Cjwk_cGO.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cjwk_cGO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CtQ8EED1.js +11 -0
- package/build/client/_app/immutable/chunks/CtQ8EED1.js.br +0 -0
- package/build/client/_app/immutable/chunks/CtQ8EED1.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DERQCisl.js +1 -0
- package/build/client/_app/immutable/chunks/DERQCisl.js.br +0 -0
- package/build/client/_app/immutable/chunks/DERQCisl.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DKrg8TQs.js +1 -0
- package/build/client/_app/immutable/chunks/DKrg8TQs.js.br +0 -0
- package/build/client/_app/immutable/chunks/DKrg8TQs.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DLu6yJIZ.js +1 -0
- package/build/client/_app/immutable/chunks/DLu6yJIZ.js.br +0 -0
- package/build/client/_app/immutable/chunks/DLu6yJIZ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dkkpz_4D.js +126 -0
- package/build/client/_app/immutable/chunks/Dkkpz_4D.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dkkpz_4D.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DoczjQhA.js +1 -0
- package/build/client/_app/immutable/chunks/DoczjQhA.js.br +0 -0
- package/build/client/_app/immutable/chunks/DoczjQhA.js.gz +0 -0
- package/build/client/_app/immutable/chunks/PPVm8Dsz.js +1 -0
- package/build/client/_app/immutable/chunks/PPVm8Dsz.js.br +0 -0
- package/build/client/_app/immutable/chunks/PPVm8Dsz.js.gz +0 -0
- package/build/client/_app/immutable/chunks/RpcNruLP.js +2 -0
- package/build/client/_app/immutable/chunks/RpcNruLP.js.br +0 -0
- package/build/client/_app/immutable/chunks/RpcNruLP.js.gz +0 -0
- package/build/client/_app/immutable/chunks/a-St0Zwo.js +1 -0
- package/build/client/_app/immutable/chunks/a-St0Zwo.js.br +0 -0
- package/build/client/_app/immutable/chunks/a-St0Zwo.js.gz +0 -0
- package/build/client/_app/immutable/chunks/bo70OQUZ.js +1 -0
- package/build/client/_app/immutable/chunks/bo70OQUZ.js.br +0 -0
- package/build/client/_app/immutable/chunks/bo70OQUZ.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.QvGgdvTI.js +2 -0
- package/build/client/_app/immutable/entry/app.QvGgdvTI.js.br +0 -0
- package/build/client/_app/immutable/entry/app.QvGgdvTI.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BntDNRMC.js +1 -0
- package/build/client/_app/immutable/entry/start.BntDNRMC.js.br +0 -0
- package/build/client/_app/immutable/entry/start.BntDNRMC.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js +1 -0
- package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.CzkdvJ7j.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.MG1QhfrI.js +1 -0
- package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.MG1QhfrI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.B4MlOSh6.js +1 -0
- package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.B4MlOSh6.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DIwYkjDn.js +3 -0
- package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DIwYkjDn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.D-cIe70D.js +1 -0
- package/build/client/_app/immutable/nodes/4.D-cIe70D.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.D-cIe70D.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.D7zPRe3L.js +1 -0
- package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.D7zPRe3L.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.BB7QE48r.js +2 -0
- package/build/client/_app/immutable/nodes/6.BB7QE48r.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.BB7QE48r.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.D8mqsrZG.js +2 -0
- package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.D8mqsrZG.js.gz +0 -0
- package/build/client/_app/version.json +1 -0
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/client/app-icon.png +0 -0
- package/build/client/apple-touch-icon.png +0 -0
- package/build/client/favicon.png +0 -0
- package/build/client/favicon.svg +10 -0
- package/build/client/favicon.svg.br +0 -0
- package/build/client/favicon.svg.gz +0 -0
- package/build/client/manifest.webmanifest +1 -0
- package/build/client/pwa-192x192.png +0 -0
- package/build/client/pwa-512x512.png +0 -0
- package/build/client/registerSW.js +1 -0
- package/build/client/registerSW.js.br +0 -0
- package/build/client/registerSW.js.gz +0 -0
- package/build/client/sw.js +222 -0
- package/build/client/sw.js.br +0 -0
- package/build/client/sw.js.gz +0 -0
- package/build/client/workbox-5119daf5.js +3395 -0
- package/build/client/workbox-5119daf5.js.br +0 -0
- package/build/client/workbox-5119daf5.js.gz +0 -0
- package/build/env.js +94 -0
- package/build/handler.js +1494 -0
- package/build/index.js +345 -0
- package/build/pty-holder.cjs +510 -0
- package/build/server/chunks/0-q2IUp76Y.js +9 -0
- package/build/server/chunks/0-q2IUp76Y.js.map +1 -0
- package/build/server/chunks/1-CU50G5wZ.js +9 -0
- package/build/server/chunks/1-CU50G5wZ.js.map +1 -0
- package/build/server/chunks/2-D01t9s8T.js +9 -0
- package/build/server/chunks/2-D01t9s8T.js.map +1 -0
- package/build/server/chunks/3-5PUQ04wC.js +9 -0
- package/build/server/chunks/3-5PUQ04wC.js.map +1 -0
- package/build/server/chunks/4-e7gywnSG.js +9 -0
- package/build/server/chunks/4-e7gywnSG.js.map +1 -0
- package/build/server/chunks/5-CA1SA6KZ.js +9 -0
- package/build/server/chunks/5-CA1SA6KZ.js.map +1 -0
- package/build/server/chunks/6-71H221sV.js +9 -0
- package/build/server/chunks/6-71H221sV.js.map +1 -0
- package/build/server/chunks/7-Bo-vmdyz.js +9 -0
- package/build/server/chunks/7-Bo-vmdyz.js.map +1 -0
- package/build/server/chunks/_layout.svelte-SFHOxs74.js +132 -0
- package/build/server/chunks/_layout.svelte-SFHOxs74.js.map +1 -0
- package/build/server/chunks/_page.svelte-B4w-2wD-.js +120 -0
- package/build/server/chunks/_page.svelte-B4w-2wD-.js.map +1 -0
- package/build/server/chunks/_page.svelte-B_qAXjkh.js +213 -0
- package/build/server/chunks/_page.svelte-B_qAXjkh.js.map +1 -0
- package/build/server/chunks/_page.svelte-CsF1_TRG.js +50 -0
- package/build/server/chunks/_page.svelte-CsF1_TRG.js.map +1 -0
- package/build/server/chunks/_page.svelte-DJC6U-P0.js +68 -0
- package/build/server/chunks/_page.svelte-DJC6U-P0.js.map +1 -0
- package/build/server/chunks/_page.svelte-DQ6HBtsz.js +407 -0
- package/build/server/chunks/_page.svelte-DQ6HBtsz.js.map +1 -0
- package/build/server/chunks/_page.svelte-LbhhjP21.js +148 -0
- package/build/server/chunks/_page.svelte-LbhhjP21.js.map +1 -0
- package/build/server/chunks/_server.ts-BL2FGb5Z.js +387 -0
- package/build/server/chunks/_server.ts-BL2FGb5Z.js.map +1 -0
- package/build/server/chunks/_server.ts-BgdjBZco.js +47 -0
- package/build/server/chunks/_server.ts-BgdjBZco.js.map +1 -0
- package/build/server/chunks/_server.ts-BihKSdj_.js +59 -0
- package/build/server/chunks/_server.ts-BihKSdj_.js.map +1 -0
- package/build/server/chunks/_server.ts-BjOJsoy4.js +63 -0
- package/build/server/chunks/_server.ts-BjOJsoy4.js.map +1 -0
- package/build/server/chunks/_server.ts-C29xzfaw.js +77 -0
- package/build/server/chunks/_server.ts-C29xzfaw.js.map +1 -0
- package/build/server/chunks/_server.ts-CPa6DgIt.js +71 -0
- package/build/server/chunks/_server.ts-CPa6DgIt.js.map +1 -0
- package/build/server/chunks/_server.ts-CbDRDIoP.js +36 -0
- package/build/server/chunks/_server.ts-CbDRDIoP.js.map +1 -0
- package/build/server/chunks/_server.ts-Cl1OEWL4.js +54 -0
- package/build/server/chunks/_server.ts-Cl1OEWL4.js.map +1 -0
- package/build/server/chunks/_server.ts-ColfDHW8.js +60 -0
- package/build/server/chunks/_server.ts-ColfDHW8.js.map +1 -0
- package/build/server/chunks/_server.ts-Cv_OrRuL.js +494 -0
- package/build/server/chunks/_server.ts-Cv_OrRuL.js.map +1 -0
- package/build/server/chunks/_server.ts-D4MNi4cD.js +25 -0
- package/build/server/chunks/_server.ts-D4MNi4cD.js.map +1 -0
- package/build/server/chunks/_server.ts-DRVbgm6k.js +125 -0
- package/build/server/chunks/_server.ts-DRVbgm6k.js.map +1 -0
- package/build/server/chunks/_server.ts-DfajWaqh.js +39 -0
- package/build/server/chunks/_server.ts-DfajWaqh.js.map +1 -0
- package/build/server/chunks/_server.ts-y9-WYDMa.js +35 -0
- package/build/server/chunks/_server.ts-y9-WYDMa.js.map +1 -0
- package/build/server/chunks/auth-CEgFis71.js +32 -0
- package/build/server/chunks/auth-CEgFis71.js.map +1 -0
- package/build/server/chunks/client-CxCatAKr.js +255 -0
- package/build/server/chunks/client-CxCatAKr.js.map +1 -0
- package/build/server/chunks/error.svelte-BqdwMWdK.js +26 -0
- package/build/server/chunks/error.svelte-BqdwMWdK.js.map +1 -0
- package/build/server/chunks/exports-CJ0Q5XmL.js +4081 -0
- package/build/server/chunks/exports-CJ0Q5XmL.js.map +1 -0
- package/build/server/chunks/index2-DAxIoAO-.js +36 -0
- package/build/server/chunks/index2-DAxIoAO-.js.map +1 -0
- package/build/server/chunks/jsonl-parser-dmZU_Hyu.js +137 -0
- package/build/server/chunks/jsonl-parser-dmZU_Hyu.js.map +1 -0
- package/build/server/chunks/library-apns-BHxLmuIx.js +104 -0
- package/build/server/chunks/library-apns-BHxLmuIx.js.map +1 -0
- package/build/server/chunks/markdown-Bxrl3cCF.js +1241 -0
- package/build/server/chunks/markdown-Bxrl3cCF.js.map +1 -0
- package/build/server/chunks/pending-requests-D8UiTw7L.js +44 -0
- package/build/server/chunks/pending-requests-D8UiTw7L.js.map +1 -0
- package/build/server/chunks/pty-manager-C0FhBiVq.js +1697 -0
- package/build/server/chunks/pty-manager-C0FhBiVq.js.map +1 -0
- package/build/server/chunks/shared-server-BDY8jh20.js +200 -0
- package/build/server/chunks/shared-server-BDY8jh20.js.map +1 -0
- package/build/server/chunks/stores-D0HorpgL.js +36 -0
- package/build/server/chunks/stores-D0HorpgL.js.map +1 -0
- package/build/server/index.js +6466 -0
- package/build/server/index.js.map +1 -0
- package/build/server/manifest.js +184 -0
- package/build/server/manifest.js.map +1 -0
- package/build/shims.js +32 -0
- package/package.json +94 -0
- package/scripts/clipboard-shims/wl-paste +48 -0
- package/scripts/clipboard-shims/xclip +31 -0
- package/scripts/install.sh +477 -0
- package/scripts/setup-node-pty.sh +63 -0
- package/scripts/setup.cjs +571 -0
- package/scripts/test-runner.ts +243 -0
- package/scripts/vercel-env-commands.sh +60 -0
- package/server.ts +139 -0
- package/src/app.css +1835 -0
- package/src/app.d.ts +31 -0
- package/src/app.html +24 -0
- package/src/generated/types/APN.ts +305 -0
- package/src/generated/types/CLI.ts +52 -0
- package/src/generated/types/JWT.ts +92 -0
- package/src/generated/types/Terminal.ts +2736 -0
- package/src/generated/types/index.ts +6 -0
- package/src/lib/assets/icons/alert-triangle.svg +5 -0
- package/src/lib/assets/icons/bell.svg +4 -0
- package/src/lib/assets/icons/check-circle.svg +4 -0
- package/src/lib/assets/icons/file.svg +4 -0
- package/src/lib/assets/icons/folder.svg +3 -0
- package/src/lib/assets/icons/play.svg +3 -0
- package/src/lib/assets/icons/refresh.svg +4 -0
- package/src/lib/assets/icons/settings.svg +4 -0
- package/src/lib/assets/icons/terminal.svg +1 -0
- package/src/lib/assets/icons/tool.svg +3 -0
- package/src/lib/assets/icons/x-circle.svg +5 -0
- package/src/lib/modules/client/common/Card.svelte +26 -0
- package/src/lib/modules/client/common/EmptyState.svelte +36 -0
- package/src/lib/modules/client/common/Icon.svelte +61 -0
- package/src/lib/modules/client/common/StatusBadge.svelte +38 -0
- package/src/lib/modules/client/common/cache.ts +31 -0
- package/src/lib/modules/client/common/config-guard.ts +18 -0
- package/src/lib/modules/client/common/index.ts +12 -0
- package/src/lib/modules/client/common/markdown.ts +23 -0
- package/src/lib/modules/client/common/native-bridge.ts +50 -0
- package/src/lib/modules/client/common/time.ts +22 -0
- package/src/lib/modules/client/common/tool-title.ts +28 -0
- package/src/lib/modules/client/terminal/ChatView.svelte +400 -0
- package/src/lib/modules/client/terminal/CommandPalette.svelte +60 -0
- package/src/lib/modules/client/terminal/ConnectionStatus.svelte +99 -0
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +294 -0
- package/src/lib/modules/client/terminal/QuickKeys.svelte +71 -0
- package/src/lib/modules/client/terminal/ShortcutsHelp.svelte +79 -0
- package/src/lib/modules/client/terminal/keyboard-shortcuts.ts +70 -0
- package/src/lib/modules/client/terminal/xterm-wrapper.ts +243 -0
- package/src/lib/modules/server/apn/library-apns.ts +137 -0
- package/src/lib/modules/server/apn/notification-history.ts +35 -0
- package/src/lib/modules/server/apn/notification-sessions.ts +117 -0
- package/src/lib/modules/server/apn/pending-requests.ts +65 -0
- package/src/lib/modules/server/apn/types.ts +51 -0
- package/src/lib/modules/server/auth.ts +34 -0
- package/src/lib/modules/server/cli/index.ts +79 -0
- package/src/lib/modules/server/cli/runner.ts +162 -0
- package/src/lib/modules/server/fcm/fcm-service.ts +72 -0
- package/src/lib/modules/server/sessions/jsonl-parser.ts +197 -0
- package/src/lib/modules/server/sessions/jsonl-reader.ts +301 -0
- package/src/lib/modules/server/sessions/opencode-reader.ts +264 -0
- package/src/lib/modules/server/sessions/types.ts +53 -0
- package/src/lib/modules/server/terminal/holder-client.ts +273 -0
- package/src/lib/modules/server/terminal/opencode-watcher.ts +661 -0
- package/src/lib/modules/server/terminal/pty-holder.cjs +510 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +1012 -0
- package/src/lib/modules/server/terminal/session-watcher.ts +320 -0
- package/src/lib/modules/server/terminal/terminal-store.ts +198 -0
- package/src/lib/modules/server/ws/events-handler.ts +73 -0
- package/src/lib/modules/server/ws/keepalive.ts +108 -0
- package/src/lib/modules/server/ws/server.ts +93 -0
- package/src/lib/modules/server/ws/session-handler.ts +462 -0
- package/src/lib/modules/server/ws/terminal-handler.ts +197 -0
- package/src/lib/modules/server/ws/ticket-store.ts +58 -0
- package/src/lib/theme.css +529 -0
- package/src/lib/types/config.ts +6 -0
- package/src/routes/+layout.svelte +218 -0
- package/src/routes/+page.svelte +261 -0
- package/src/routes/api/debug/+server.ts +33 -0
- package/src/routes/api/device-token/+server.ts +85 -0
- package/src/routes/api/health/+server.ts +100 -0
- package/src/routes/api/notify/+server.ts +418 -0
- package/src/routes/api/qr-config/+server.ts +45 -0
- package/src/routes/api/response/+server.ts +73 -0
- package/src/routes/api/sessions/+server.ts +120 -0
- package/src/routes/api/terminals/+server.ts +141 -0
- package/src/routes/api/terminals/[id]/+server.ts +75 -0
- package/src/routes/api/terminals/[id]/paste-image/+server.ts +61 -0
- package/src/routes/api/terminals/[id]/resize/+server.ts +60 -0
- package/src/routes/api/webhook/+server.ts +42 -0
- package/src/routes/api/ws-status/+server.ts +23 -0
- package/src/routes/api/ws-ticket/+server.ts +86 -0
- package/src/routes/config/+page.svelte +600 -0
- package/src/routes/project/+page.svelte +274 -0
- package/src/routes/session/[id]/+page.svelte +434 -0
- package/src/routes/terminals/+page.svelte +618 -0
- package/src/routes/terminals/[id]/+page.svelte +968 -0
- package/svelte.config.js +18 -0
- 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">←</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">← 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">←</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">← 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">📁 {shortPath(session.projectPath)}</span>
|
|
368
|
+
<span class="chat-session-meta-item">🌿 {session.gitBranch}</span>
|
|
369
|
+
<span class="chat-session-meta-item">💬 {session.messageCount} messages</span>
|
|
370
|
+
<span class="chat-session-meta-item">📅 {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>
|