@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,93 @@
|
|
|
1
|
+
// WebSocket server routing module.
|
|
2
|
+
// Called from the custom server entry point (server.ts at project root) on HTTP upgrade events.
|
|
3
|
+
// Routes connections to the appropriate handler based on URL path:
|
|
4
|
+
// /ws/terminal/:id -> Terminal I/O (raw PTY stream)
|
|
5
|
+
// /ws/session/:id -> Live structured session stream
|
|
6
|
+
// /ws/events -> Global event bus (broadcasts)
|
|
7
|
+
|
|
8
|
+
import type { IncomingMessage } from 'http';
|
|
9
|
+
import type { Duplex } from 'stream';
|
|
10
|
+
import type { WebSocket, WebSocketServer } from 'ws';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
broadcastEvent as broadcastEventToClients,
|
|
14
|
+
getEventsClientCount,
|
|
15
|
+
handleEventsConnection,
|
|
16
|
+
} from './events-handler.js';
|
|
17
|
+
import { handleSessionConnection } from './session-handler.js';
|
|
18
|
+
import { handleTerminalConnection } from './terminal-handler.js';
|
|
19
|
+
export type { ShooterEvent } from './events-handler.js';
|
|
20
|
+
|
|
21
|
+
// ── Connection tracking ──────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** All active WebSocket connections (used by keepalive). */
|
|
24
|
+
const allConnections = new Set<WebSocket>();
|
|
25
|
+
|
|
26
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the set of all tracked connections (needed by keepalive module).
|
|
30
|
+
*/
|
|
31
|
+
export function getAllConnections(): Set<WebSocket> {
|
|
32
|
+
return allConnections;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns the number of clients connected to the events channel.
|
|
37
|
+
* Used by the notifier to decide between WebSocket broadcast vs APNs push.
|
|
38
|
+
*/
|
|
39
|
+
export function getConnectedClientCount(): number {
|
|
40
|
+
return getEventsClientCount();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handle an HTTP upgrade request by routing it to the correct WebSocket handler.
|
|
45
|
+
* Destroys the socket if the URL does not match any known route.
|
|
46
|
+
*/
|
|
47
|
+
export function setupWebSocketHandlers(
|
|
48
|
+
wss: WebSocketServer,
|
|
49
|
+
request: IncomingMessage,
|
|
50
|
+
socket: Duplex,
|
|
51
|
+
head: Buffer
|
|
52
|
+
): void {
|
|
53
|
+
const url = new URL(request.url || '', `http://${request.headers.host}`);
|
|
54
|
+
const pathname = url.pathname;
|
|
55
|
+
|
|
56
|
+
// Route matching
|
|
57
|
+
const terminalMatch = /^\/ws\/terminal\/(.+)$/.exec(pathname);
|
|
58
|
+
const sessionMatch = /^\/ws\/session\/(.+)$/.exec(pathname);
|
|
59
|
+
const isEvents = pathname === '/ws/events';
|
|
60
|
+
|
|
61
|
+
if (!terminalMatch && !sessionMatch && !isEvents) {
|
|
62
|
+
socket.destroy();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {
|
|
67
|
+
allConnections.add(ws);
|
|
68
|
+
|
|
69
|
+
ws.on('close', () => {
|
|
70
|
+
allConnections.delete(ws);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ws.on('error', () => {
|
|
74
|
+
// Prevent unhandled error crashes; cleanup happens in 'close'.
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (terminalMatch) {
|
|
78
|
+
const terminalId = terminalMatch[1];
|
|
79
|
+
handleTerminalConnection(ws, terminalId);
|
|
80
|
+
} else if (sessionMatch) {
|
|
81
|
+
const sessionId = sessionMatch[1];
|
|
82
|
+
handleSessionConnection(ws, sessionId);
|
|
83
|
+
} else if (isEvents) {
|
|
84
|
+
handleEventsConnection(ws);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Broadcast an event to all clients connected on the /ws/events channel.
|
|
91
|
+
* Delegates to the events handler which owns the client set.
|
|
92
|
+
*/
|
|
93
|
+
export { broadcastEventToClients as broadcastEvent };
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// WebSocket handler for /ws/session/:id — structured session stream.
|
|
2
|
+
// Used by the Chat view for AI sessions. Sends parsed conversation history
|
|
3
|
+
// on connect and streams new messages (text, tool-use, tool-result,
|
|
4
|
+
// thinking) as they appear.
|
|
5
|
+
|
|
6
|
+
import type { WebSocket } from 'ws';
|
|
7
|
+
|
|
8
|
+
import type { ConversationMessage, MessagePart } from '../sessions/types';
|
|
9
|
+
|
|
10
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Inbound messages from the client. */
|
|
13
|
+
type ClientMessage =
|
|
14
|
+
| { sessionId: string; type: 'subscribe'; }
|
|
15
|
+
| { text: string; type: 'send-input'; }
|
|
16
|
+
| { type: 'cancel' };
|
|
17
|
+
|
|
18
|
+
/** A message in the history payload. */
|
|
19
|
+
interface HistoryMessage {
|
|
20
|
+
content: HistoryPart[];
|
|
21
|
+
id: string;
|
|
22
|
+
role: MessageRole;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A single part within a history message — discriminated union. */
|
|
27
|
+
type HistoryPart =
|
|
28
|
+
| { content: string; type: 'text'; }
|
|
29
|
+
| { content: string; type: 'thinking'; }
|
|
30
|
+
| { id: string; input: Record<string, unknown>; toolName: string; type: 'tool_use'; }
|
|
31
|
+
| { isError: boolean; output: string; toolUseId: string; type: 'tool_result'; };
|
|
32
|
+
|
|
33
|
+
interface ManagedTerminal {
|
|
34
|
+
id: string;
|
|
35
|
+
openCodeSessionId: null | string;
|
|
36
|
+
pty: {
|
|
37
|
+
pid: number;
|
|
38
|
+
write: (data: string) => void;
|
|
39
|
+
};
|
|
40
|
+
sessionFile: null | string;
|
|
41
|
+
status: 'exited' | 'running';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Role values that appear in session messages. */
|
|
45
|
+
type MessageRole = 'assistant' | 'system' | 'user';
|
|
46
|
+
|
|
47
|
+
interface PtyManagerLike {
|
|
48
|
+
getTerminal: (id: string) => ManagedTerminal | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── PTY Manager interface ────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** Outbound messages to the client. */
|
|
54
|
+
type ServerMessage =
|
|
55
|
+
| { content: TextContentBlock[]; role: MessageRole; timestamp: string; type: 'message'; }
|
|
56
|
+
| { id: string; input: Record<string, unknown>; name: string; status: 'running'; type: 'tool-use'; }
|
|
57
|
+
| { id: string; isError: boolean; output: string; status: 'done'; type: 'tool-result'; }
|
|
58
|
+
| { message: string; type: 'error'; }
|
|
59
|
+
| { messages: HistoryMessage[]; type: 'history'; }
|
|
60
|
+
| { text: string; type: 'thinking'; }
|
|
61
|
+
| { type: 'session-end' };
|
|
62
|
+
|
|
63
|
+
interface SessionWatcherLike {
|
|
64
|
+
getHistory: (sessionFile: string) => ConversationMessage[];
|
|
65
|
+
subscribe: (sessionFile: string, callback: (messages: ConversationMessage[]) => void) => () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Session Watcher interface ────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/** Content block in the live 'message' payload. */
|
|
71
|
+
interface TextContentBlock {
|
|
72
|
+
content: string;
|
|
73
|
+
type: 'text';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Module-level references ──────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
let _ptyManager: null | PtyManagerLike = null;
|
|
79
|
+
let _sessionWatcher: null | SessionWatcherLike = null;
|
|
80
|
+
|
|
81
|
+
/** Per-connection state tracked for cleanup. */
|
|
82
|
+
interface ConnectionState {
|
|
83
|
+
retryInterval: ReturnType<typeof setInterval> | null;
|
|
84
|
+
terminalId: string;
|
|
85
|
+
unsubscribe: (() => void) | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handle a new WebSocket connection on the `/ws/session/:id` channel.
|
|
90
|
+
* Sends full conversation history on connect, then streams new entries
|
|
91
|
+
* as they appear in the session file.
|
|
92
|
+
*/
|
|
93
|
+
export function handleSessionConnection(ws: WebSocket, terminalId: string): void {
|
|
94
|
+
const state: ConnectionState = { retryInterval: null, terminalId, unsubscribe: null };
|
|
95
|
+
|
|
96
|
+
// ── 1. Look up the terminal ──────────────────────────────────────
|
|
97
|
+
if (!_ptyManager) {
|
|
98
|
+
safeSend(ws, { message: 'PTY manager not initialised', type: 'error' });
|
|
99
|
+
ws.close(1011, 'PTY manager not initialised');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const terminal = _ptyManager.getTerminal(terminalId);
|
|
104
|
+
if (!terminal) {
|
|
105
|
+
safeSend(ws, { message: `Terminal not found: ${terminalId}`, type: 'error' });
|
|
106
|
+
ws.close(1008, 'Terminal not found');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 2. Subscribe to session file ─────────────────────────────────
|
|
111
|
+
subscribeToSession(ws, state, terminal);
|
|
112
|
+
|
|
113
|
+
// ── 3. Handle messages from the client ───────────────────────────
|
|
114
|
+
ws.on('message', (raw: Buffer | string) => {
|
|
115
|
+
const data = typeof raw === 'string' ? raw : raw.toString('utf-8');
|
|
116
|
+
const msg = parseClientMessage(data);
|
|
117
|
+
if (!msg) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
switch (msg.type) {
|
|
123
|
+
case 'cancel': {
|
|
124
|
+
// Send SIGINT to the terminal process.
|
|
125
|
+
const currentTerminal = _ptyManager?.getTerminal(state.terminalId);
|
|
126
|
+
if (!currentTerminal || currentTerminal.status === 'exited') {
|
|
127
|
+
safeSend(ws, { message: 'Terminal has exited', type: 'error' });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
currentTerminal.pty.write('\x03');
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case 'send-input': {
|
|
135
|
+
// Write text + newline to PTY stdin (the Chat view sends complete
|
|
136
|
+
// messages, not raw keystrokes).
|
|
137
|
+
const currentTerminal = _ptyManager?.getTerminal(state.terminalId);
|
|
138
|
+
if (!currentTerminal || currentTerminal.status === 'exited') {
|
|
139
|
+
safeSend(ws, { message: 'Terminal has exited', type: 'error' });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
currentTerminal.pty.write(`${msg.text }\n`);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case 'subscribe': {
|
|
147
|
+
// (Re)subscribe to a different session. Clean up the old subscription
|
|
148
|
+
// and attach to the new terminal.
|
|
149
|
+
const newTerminal = _ptyManager?.getTerminal(msg.sessionId);
|
|
150
|
+
if (!newTerminal) {
|
|
151
|
+
safeSend(ws, {
|
|
152
|
+
message: `Terminal not found: ${msg.sessionId}`,
|
|
153
|
+
type: 'error',
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Tear down old subscription and retry interval.
|
|
159
|
+
if (state.retryInterval) {
|
|
160
|
+
clearInterval(state.retryInterval);
|
|
161
|
+
state.retryInterval = null;
|
|
162
|
+
}
|
|
163
|
+
if (state.unsubscribe) {
|
|
164
|
+
state.unsubscribe();
|
|
165
|
+
state.unsubscribe = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
state.terminalId = msg.sessionId;
|
|
169
|
+
subscribeToSession(ws, state, newTerminal);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
console.error(`[ws/session] Error handling ${msg.type} for ${state.terminalId}:`, errMsg);
|
|
176
|
+
safeSend(ws, { message: `Failed to handle ${msg.type}: ${errMsg}`, type: 'error' });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ── 4. Cleanup on disconnect ─────────────────────────────────────
|
|
181
|
+
ws.on('close', () => {
|
|
182
|
+
if (state.retryInterval) {
|
|
183
|
+
clearInterval(state.retryInterval);
|
|
184
|
+
state.retryInterval = null;
|
|
185
|
+
}
|
|
186
|
+
if (state.unsubscribe) {
|
|
187
|
+
state.unsubscribe();
|
|
188
|
+
state.unsubscribe = null;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
ws.on('error', () => {
|
|
193
|
+
// Errors are followed by 'close', which handles cleanup.
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Register the PTY manager instance. Called once during server bootstrap.
|
|
201
|
+
*/
|
|
202
|
+
export function setPtyManager(manager: PtyManagerLike): void {
|
|
203
|
+
_ptyManager = manager;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Register the session watcher instance. Called once during server bootstrap.
|
|
208
|
+
*/
|
|
209
|
+
export function setSessionWatcher(watcher: SessionWatcherLike): void {
|
|
210
|
+
_sessionWatcher = watcher;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Conversion: ConversationMessage → wire format ────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Convert ConversationMessage[] into HistoryMessage[] for the initial
|
|
217
|
+
* `history` payload sent when a client connects.
|
|
218
|
+
*/
|
|
219
|
+
function conversationToHistory(messages: ConversationMessage[]): HistoryMessage[] {
|
|
220
|
+
return messages
|
|
221
|
+
.filter(msg => msg.parts.length > 0)
|
|
222
|
+
.map(msg => ({
|
|
223
|
+
content: msg.parts.map(partToHistoryPart),
|
|
224
|
+
id: msg.id,
|
|
225
|
+
role: msg.role,
|
|
226
|
+
timestamp: msg.timestamp,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Convert a single ConversationMessage into one or more ServerMessages
|
|
232
|
+
* for the live stream.
|
|
233
|
+
*/
|
|
234
|
+
function conversationToLive(msg: ConversationMessage): ServerMessage[] {
|
|
235
|
+
const messages: ServerMessage[] = [];
|
|
236
|
+
const textBlocks: TextContentBlock[] = [];
|
|
237
|
+
|
|
238
|
+
for (const part of msg.parts) {
|
|
239
|
+
switch (part.type) {
|
|
240
|
+
case 'text':
|
|
241
|
+
textBlocks.push({ content: part.content, type: 'text' });
|
|
242
|
+
break;
|
|
243
|
+
case 'thinking':
|
|
244
|
+
messages.push({ text: part.content, type: 'thinking' });
|
|
245
|
+
break;
|
|
246
|
+
case 'tool_result':
|
|
247
|
+
messages.push({
|
|
248
|
+
id: part.toolUseId,
|
|
249
|
+
isError: part.isError,
|
|
250
|
+
output: part.output,
|
|
251
|
+
status: 'done',
|
|
252
|
+
type: 'tool-result',
|
|
253
|
+
});
|
|
254
|
+
break;
|
|
255
|
+
case 'tool_use':
|
|
256
|
+
messages.push({
|
|
257
|
+
id: part.id,
|
|
258
|
+
input: part.input,
|
|
259
|
+
name: part.toolName,
|
|
260
|
+
status: 'running',
|
|
261
|
+
type: 'tool-use',
|
|
262
|
+
});
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (textBlocks.length > 0) {
|
|
268
|
+
messages.push({
|
|
269
|
+
content: textBlocks,
|
|
270
|
+
role: msg.role,
|
|
271
|
+
timestamp: msg.timestamp,
|
|
272
|
+
type: 'message',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return messages;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Parse and validate an inbound client message. */
|
|
280
|
+
function parseClientMessage(raw: string): ClientMessage | null {
|
|
281
|
+
try {
|
|
282
|
+
const msg = JSON.parse(raw);
|
|
283
|
+
if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
switch (msg.type) {
|
|
288
|
+
case 'cancel':
|
|
289
|
+
return { type: 'cancel' };
|
|
290
|
+
case 'send-input':
|
|
291
|
+
if (typeof msg.text !== 'string' || msg.text.length === 0) {return null;}
|
|
292
|
+
// Cap input length at 10KB to prevent abuse.
|
|
293
|
+
if (msg.text.length > 10240) {return null;}
|
|
294
|
+
return { text: msg.text, type: 'send-input' };
|
|
295
|
+
case 'subscribe':
|
|
296
|
+
if (typeof msg.sessionId !== 'string' || msg.sessionId.length === 0) {return null;}
|
|
297
|
+
return { sessionId: msg.sessionId, type: 'subscribe' };
|
|
298
|
+
default:
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Connection state ─────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Map a single MessagePart to the HistoryPart wire format.
|
|
310
|
+
*/
|
|
311
|
+
function partToHistoryPart(part: MessagePart): HistoryPart {
|
|
312
|
+
switch (part.type) {
|
|
313
|
+
case 'text':
|
|
314
|
+
return { content: part.content, type: 'text' };
|
|
315
|
+
case 'thinking':
|
|
316
|
+
return { content: part.content, type: 'thinking' };
|
|
317
|
+
case 'tool_result':
|
|
318
|
+
return { isError: part.isError, output: part.output, toolUseId: part.toolUseId, type: 'tool_result' };
|
|
319
|
+
case 'tool_use':
|
|
320
|
+
return { id: part.id, input: part.input, toolName: part.toolName, type: 'tool_use' };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Main handler ─────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/** Safely send a JSON message over a WebSocket. */
|
|
327
|
+
function safeSend(ws: WebSocket, msg: ServerMessage): boolean {
|
|
328
|
+
try {
|
|
329
|
+
if (ws.readyState !== 1 /* OPEN */) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
ws.send(JSON.stringify(msg));
|
|
333
|
+
return true;
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Subscribe to a terminal's session file. Sends the full history as a
|
|
341
|
+
* `history` message, then streams new entries as they appear.
|
|
342
|
+
*/
|
|
343
|
+
function subscribeToSession(
|
|
344
|
+
ws: WebSocket,
|
|
345
|
+
state: ConnectionState,
|
|
346
|
+
terminal: ManagedTerminal
|
|
347
|
+
): void {
|
|
348
|
+
// Use sessionFile for Claude Code (JSONL) or openCodeSessionId for OpenCode (SQLite).
|
|
349
|
+
const sessionKey = terminal.sessionFile || terminal.openCodeSessionId;
|
|
350
|
+
|
|
351
|
+
// If no session key yet and the terminal is still running, the session ID
|
|
352
|
+
// may not have been discovered yet (e.g., pty-manager is still polling for
|
|
353
|
+
// the OpenCode session). Poll until it appears rather than giving up.
|
|
354
|
+
if (!sessionKey && terminal.status === 'running') {
|
|
355
|
+
safeSend(ws, { messages: [], type: 'history' });
|
|
356
|
+
|
|
357
|
+
// Clear any previous retry interval to prevent accumulation on re-subscribe.
|
|
358
|
+
if (state.retryInterval) {
|
|
359
|
+
clearInterval(state.retryInterval);
|
|
360
|
+
state.retryInterval = null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
state.retryInterval = setInterval(() => {
|
|
364
|
+
// Re-fetch the terminal to get the latest sessionFile / openCodeSessionId.
|
|
365
|
+
const freshTerminal = _ptyManager?.getTerminal(terminal.id);
|
|
366
|
+
if (!freshTerminal) {
|
|
367
|
+
if (state.retryInterval) {
|
|
368
|
+
clearInterval(state.retryInterval);
|
|
369
|
+
state.retryInterval = null;
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const key = freshTerminal.sessionFile || freshTerminal.openCodeSessionId;
|
|
375
|
+
if (key || freshTerminal.status === 'exited') {
|
|
376
|
+
if (state.retryInterval) {
|
|
377
|
+
clearInterval(state.retryInterval);
|
|
378
|
+
state.retryInterval = null;
|
|
379
|
+
}
|
|
380
|
+
if (key) {
|
|
381
|
+
// Session discovered — send history and subscribe.
|
|
382
|
+
subscribeWithSessionKey(ws, state, freshTerminal, key);
|
|
383
|
+
} else {
|
|
384
|
+
safeSend(ws, { type: 'session-end' });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}, 2000);
|
|
388
|
+
|
|
389
|
+
// Cleanup is handled by the single 'close' listener in handleSessionConnection.
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// If no session key (e.g., a plain shell or already exited), send empty history.
|
|
394
|
+
if (!sessionKey) {
|
|
395
|
+
safeSend(ws, { messages: [], type: 'history' });
|
|
396
|
+
|
|
397
|
+
if (terminal.status === 'exited') {
|
|
398
|
+
safeSend(ws, { type: 'session-end' });
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
subscribeWithSessionKey(ws, state, terminal, sessionKey);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Subscribe to a session using an already-known session key. Sends history
|
|
408
|
+
* and starts streaming new entries. Extracted so both the normal path and
|
|
409
|
+
* the retry-after-discovery path can share it.
|
|
410
|
+
*/
|
|
411
|
+
function subscribeWithSessionKey(
|
|
412
|
+
ws: WebSocket,
|
|
413
|
+
state: ConnectionState,
|
|
414
|
+
terminal: ManagedTerminal,
|
|
415
|
+
sessionKey: string
|
|
416
|
+
): void {
|
|
417
|
+
if (!_sessionWatcher) {
|
|
418
|
+
safeSend(ws, { message: 'Session watcher not initialised', type: 'error' });
|
|
419
|
+
safeSend(ws, { messages: [], type: 'history' });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Send full history ────────────────────────────────────────────
|
|
424
|
+
try {
|
|
425
|
+
const allMessages = _sessionWatcher.getHistory(sessionKey);
|
|
426
|
+
const historyMessages = conversationToHistory(allMessages);
|
|
427
|
+
safeSend(ws, { messages: historyMessages, type: 'history' });
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.error(`[ws/session] Failed to read history for ${terminal.id}:`, err);
|
|
430
|
+
safeSend(ws, { messages: [], type: 'history' });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── Stream new messages ────────────────────────────────────────
|
|
434
|
+
try {
|
|
435
|
+
const unsubscribe = _sessionWatcher.subscribe(
|
|
436
|
+
sessionKey,
|
|
437
|
+
(messages: ConversationMessage[]) => {
|
|
438
|
+
if (ws.readyState !== 1 /* OPEN */) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const msg of messages) {
|
|
443
|
+
const liveMessages = conversationToLive(msg);
|
|
444
|
+
for (const liveMsg of liveMessages) {
|
|
445
|
+
safeSend(ws, liveMsg);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Store unsubscribe so we can clean up on close or re-subscribe.
|
|
452
|
+
state.unsubscribe = unsubscribe;
|
|
453
|
+
} catch (err) {
|
|
454
|
+
console.error(`[ws/session] Failed to subscribe for ${terminal.id}:`, err);
|
|
455
|
+
safeSend(ws, { message: 'Failed to subscribe to session updates', type: 'error' });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// If the terminal already exited, signal it.
|
|
459
|
+
if (terminal.status === 'exited') {
|
|
460
|
+
safeSend(ws, { type: 'session-end' });
|
|
461
|
+
}
|
|
462
|
+
}
|