termdock 1.4.43 → 1.4.45

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 (84) hide show
  1. package/dist/client/assets/{abnfDiagram-VRR7QNED-B3X7vTL7.js → abnfDiagram-VRR7QNED-Bt36T7gE.js} +1 -1
  2. package/dist/client/assets/{arc-OmYbmu75.js → arc-DhZtXG4q.js} +1 -1
  3. package/dist/client/assets/{architectureDiagram-ZJ3FMSHR-CNNIlew_.js → architectureDiagram-ZJ3FMSHR-C4BLot_l.js} +1 -1
  4. package/dist/client/assets/{blockDiagram-677ZJIJ3-eoDvsGst.js → blockDiagram-677ZJIJ3-BG8NrPiD.js} +1 -1
  5. package/dist/client/assets/{c4Diagram-LMCZKHZV-BYElS7nn.js → c4Diagram-LMCZKHZV-m3lWYsQI.js} +1 -1
  6. package/dist/client/assets/channel-Ck9tD_4_.js +1 -0
  7. package/dist/client/assets/{chunk-2Q5K7J3B-DgBhC1KN.js → chunk-2Q5K7J3B-BiBc5Uv0.js} +1 -1
  8. package/dist/client/assets/{chunk-32BRIVSS-B4LjEHPP.js → chunk-32BRIVSS-C1g4c11U.js} +1 -1
  9. package/dist/client/assets/{chunk-5VM5RSS4-BrBK8Xy3.js → chunk-5VM5RSS4-Ctw1x2FG.js} +1 -1
  10. package/dist/client/assets/{chunk-EX3LRPZG-QndtZFQ6.js → chunk-EX3LRPZG-BdI4v3f4.js} +1 -1
  11. package/dist/client/assets/{chunk-JWPE2WC7-B8bZHm4g.js → chunk-JWPE2WC7-K8nPht_5.js} +1 -1
  12. package/dist/client/assets/{chunk-MOJQB5TN-B5Kzsd5Z.js → chunk-MOJQB5TN-DWsVsFoK.js} +1 -1
  13. package/dist/client/assets/{chunk-RYQCIY6F-BdNa9gwg.js → chunk-RYQCIY6F-0MHmqGNs.js} +1 -1
  14. package/dist/client/assets/{chunk-V7JOEXUC-DpQA4l-k.js → chunk-V7JOEXUC-BjQQBzKq.js} +1 -1
  15. package/dist/client/assets/{chunk-VR4S4FIN-CtGdy3K8.js → chunk-VR4S4FIN-Bau1q0xN.js} +1 -1
  16. package/dist/client/assets/{chunk-XXDRQBXY-CZBBj4P_.js → chunk-XXDRQBXY-BIzI5y55.js} +1 -1
  17. package/dist/client/assets/classDiagram-OUVF2IWQ-CtEpUyds.js +1 -0
  18. package/dist/client/assets/classDiagram-v2-EOCWNBFH-CtEpUyds.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-JH36ORCC-gtzwCMyn.js → cose-bilkent-JH36ORCC-C_6nPODG.js} +1 -1
  20. package/dist/client/assets/{cynefin-VYW2F7L2-B7X5gKj-.js → cynefin-VYW2F7L2-DZfIXs6F.js} +1 -1
  21. package/dist/client/assets/{cynefinDiagram-TSTJHNR4-BoKfaePi.js → cynefinDiagram-TSTJHNR4-Cwtffn_E.js} +1 -1
  22. package/dist/client/assets/{dagre-VKFMJZFB-Bjh8y-RO.js → dagre-VKFMJZFB-n2L4piqF.js} +1 -1
  23. package/dist/client/assets/{diagram-FQU43EPY-Cx8vWSw0.js → diagram-FQU43EPY-DYc1Xsf9.js} +1 -1
  24. package/dist/client/assets/{diagram-G47NLZAW-DyKuyGsn.js → diagram-G47NLZAW-Bty2-g6M.js} +1 -1
  25. package/dist/client/assets/{diagram-NH7WQ7WH-DwnG0pD9.js → diagram-NH7WQ7WH-B7NwHUrU.js} +1 -1
  26. package/dist/client/assets/{diagram-OA4YK3LP-wn7Eux1Q.js → diagram-OA4YK3LP-BB8nlvCx.js} +1 -1
  27. package/dist/client/assets/{diagram-WEI45ONY-DNy0FovQ.js → diagram-WEI45ONY-2hoKf8a6.js} +1 -1
  28. package/dist/client/assets/{ebnfDiagram-CCIWWBDH-TFnZlH4k.js → ebnfDiagram-CCIWWBDH-BARVSv3m.js} +1 -1
  29. package/dist/client/assets/{erDiagram-Q63AITRT-Cx3_8T9Q.js → erDiagram-Q63AITRT-5jgm5Ksi.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-23GEKE2U-CZ7KiBe4.js → flowDiagram-23GEKE2U-BqCA33p5.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-NO4QXBWP-BRTRytgo.js → ganttDiagram-NO4QXBWP-BL1NGUYm.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-IHSO6WYX-DyNUwP6-.js → gitGraphDiagram-IHSO6WYX-D9Qw6ov4.js} +1 -1
  33. package/dist/client/assets/index-BYpFR_L5.js +436 -0
  34. package/dist/client/assets/index-DyTRm1PJ.css +1 -0
  35. package/dist/client/assets/{infoDiagram-FWYZ7A6U-ClLiuIL8.js → infoDiagram-FWYZ7A6U-ZrovoSsZ.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-FXEZZL3T-CbO_ecRd.js → ishikawaDiagram-FXEZZL3T-uFSMTzKn.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-5HDEW3XC-YKjq3svJ.js → journeyDiagram-5HDEW3XC-DNXNzmef.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-HUTT4EX6-CcnV3jvx.js → kanban-definition-HUTT4EX6-Db2NpkWT.js} +1 -1
  39. package/dist/client/assets/{linear-3N9evV-e.js → linear-DjF6fII5.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-C8HeTU54.js → mermaid.core-BXI9ENYY.js} +4 -4
  41. package/dist/client/assets/{mindmap-definition-LN4V7U3C-CgkCCxMO.js → mindmap-definition-LN4V7U3C-CjcF3QgK.js} +1 -1
  42. package/dist/client/assets/{pegDiagram-2B236MQR-CQRK4LBF.js → pegDiagram-2B236MQR-BckzTuEj.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-ENE6RG2P-BHrDXKeR.js → pieDiagram-ENE6RG2P-Ch2Ty0Y2.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-ABIIQ3AL-CuJ9UyOO.js → quadrantDiagram-ABIIQ3AL-DvXeQj4a.js} +1 -1
  45. package/dist/client/assets/{railroadDiagram-RFXS5EU6-D0lUBYT1.js → railroadDiagram-RFXS5EU6-BUurE4S_.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-TGXJPOKE-s5kYB_Ya.js → requirementDiagram-TGXJPOKE-CfnNu5HR.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-HTMAVEWB-Bhw1mAOH.js → sankeyDiagram-HTMAVEWB-BfpeK6jA.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-DBY2YBRQ-qnNrmb6g.js → sequenceDiagram-DBY2YBRQ-CmkcMlBD.js} +1 -1
  49. package/dist/client/assets/{sizeCapture-X5ZJPWSS-DufN5SxK.js → sizeCapture-X5ZJPWSS-BvnnE9Ev.js} +1 -1
  50. package/dist/client/assets/{stateDiagram-2N3HPSRC-Y5rMnVoS.js → stateDiagram-2N3HPSRC-D-gs0yW-.js} +1 -1
  51. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-gXYMxyju.js +1 -0
  52. package/dist/client/assets/{swimlanes-5IMT3BWC-Cmrmdfpk.js → swimlanes-5IMT3BWC-BW65nQch.js} +2 -2
  53. package/dist/client/assets/swimlanesDiagram-G3AALYLV-DmhG9XuP.js +8 -0
  54. package/dist/client/assets/{timeline-definition-FHXFAJF6-CqHH1NSC.js → timeline-definition-FHXFAJF6-Mj3zCwTc.js} +1 -1
  55. package/dist/client/assets/{vennDiagram-L72KCM5P-C2l_9LuC.js → vennDiagram-L72KCM5P-gXnoVb66.js} +1 -1
  56. package/dist/client/assets/{wardleyDiagram-EHGQE667-CNf23qHc.js → wardleyDiagram-EHGQE667-Cgbh1_F7.js} +1 -1
  57. package/dist/client/assets/{xychartDiagram-FW5EYKEG-RIp6RoVE.js → xychartDiagram-FW5EYKEG-DQW7oknN.js} +1 -1
  58. package/dist/client/icons/agents/kimi.svg +5 -0
  59. package/dist/client/index.html +3 -3
  60. package/dist/client/sw-notifications.js +163 -36
  61. package/dist/client/sw.js +1 -1
  62. package/dist/server/agent/installers.js +145 -1
  63. package/dist/server/agent/registry.js +3 -0
  64. package/dist/server/cli.js +25 -0
  65. package/dist/server/entry.js +22 -1
  66. package/dist/server/notifications/pushService.js +121 -35
  67. package/dist/server/notifications/pushViewers.js +44 -0
  68. package/dist/server/ptyHost.js +17 -0
  69. package/dist/server/ptyhost/manager.js +152 -5
  70. package/dist/server/quota/tokenDiscovery.js +25 -0
  71. package/dist/server/routes/filesystem.js +4 -2
  72. package/dist/server/routes/notifications.js +9 -3
  73. package/dist/server/routes/terminal.js +82 -106
  74. package/dist/server/utils/contextDraft.js +36 -0
  75. package/dist/server/utils/requestDeadline.js +104 -0
  76. package/dist/server/utils/settings.js +18 -9
  77. package/package.json +1 -1
  78. package/dist/client/assets/channel-DBZQbHSi.js +0 -1
  79. package/dist/client/assets/classDiagram-OUVF2IWQ-BsrNzS08.js +0 -1
  80. package/dist/client/assets/classDiagram-v2-EOCWNBFH-BsrNzS08.js +0 -1
  81. package/dist/client/assets/index-CbYQuue6.css +0 -1
  82. package/dist/client/assets/index-DB-0aiWT.js +0 -433
  83. package/dist/client/assets/stateDiagram-v2-6OUMAXLB-BSqYeylv.js +0 -1
  84. package/dist/client/assets/swimlanesDiagram-G3AALYLV-CUwNpVXZ.js +0 -8
package/dist/client/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let s,e={};const i=(i,l)=>(i=new URL(i+".js",l).href,e[i]||new Promise(e=>{if("document"in self){const s=document.createElement("script");s.src=i,s.onload=e,document.head.appendChild(s)}else s=i,importScripts(i),e()}).then(()=>{let s=e[i];if(!s)throw new Error(`Module ${i} didn’t register its module`);return s}));self.define=(l,n)=>{const r=s||("document"in self?document.currentScript.src:"")||location.href;if(e[r])return;let a={};const o=s=>i(s,r),u={module:{uri:r},exports:a,require:o};e[r]=Promise.all(l.map(s=>u[s]||o(s))).then(s=>(n(...s),a))}}define(["./workbox-9c191d2f"],function(s){"use strict";importScripts("sw-notifications.js"),self.skipWaiting(),s.clientsClaim(),s.precacheAndRoute([{url:"sw-notifications.js",revision:"2ed386d64dc020bf9a7da8efd9dbea42"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"pwa-64x64.png",revision:"c1ca9f6daa67743b38564b230314277a"},{url:"pwa-512x512.svg",revision:"149a151133f52feb34ab6ecedf349936"},{url:"pwa-512x512.png",revision:"0885dd7ec8932e7f878b013d51a380a4"},{url:"pwa-192x192.svg",revision:"541ace289ca5d6f3d269d1286cde5283"},{url:"pwa-192x192.png",revision:"4d6f899888cfb8ccd7a1fd3c9e583ad2"},{url:"maskable-icon-512x512.png",revision:"4d489331fd81f0713c400b26426442b9"},{url:"index.html",revision:"75a3a2ea7df85d2ed6ccce0deb75df09"},{url:"favicon.ico",revision:"2994debfa6f59a0898e24eb0c54d64b5"},{url:"apple-touch-icon-180x180.png",revision:"628c2e2f28a9b545aa57be1352025643"},{url:"icons/agents/opencode.svg",revision:"851c2931d95821ba2b39b68587b48976"},{url:"icons/agents/grok.svg",revision:"cd745b0b964545bc7a6b4f36021c3298"},{url:"icons/agents/goose.svg",revision:"c79b927278983c19098d77b4b0c11ab3"},{url:"icons/agents/gemini.svg",revision:"7a64f6d6f80a908255e9c0256903882e"},{url:"icons/agents/droid.svg",revision:"f28723b8c5c115e0908ba3c72613e256"},{url:"icons/agents/cursor.svg",revision:"a1528f3d3cda497f75a5db28defb19de"},{url:"icons/agents/copilot.svg",revision:"dd52e96c1eac0b6ec4bedcd0ddd3b52d"},{url:"icons/agents/codex.svg",revision:"608e55babb0b217e1f2da287342b46d3"},{url:"icons/agents/claude.svg",revision:"08af0225b7acdd8e8f573f3247e5e1c3"},{url:"icons/agents/amp.svg",revision:"af12f8b33faa81e4d46ea7b93c7e43f3"},{url:"fonts/TermdockStarSymbol-Subset.woff2",revision:"853a8d84112dd97a91d6cff1e7ab6bdf"},{url:"fonts/SymbolsNerdFontMono-Regular.woff2",revision:"54113ea8487e607f9d9a482c5877aed4"},{url:"fonts/NotoSansSymbols2-Subset.woff2",revision:"8257a6beabcc8858199b208908a96ee9"},{url:"fonts/NotoSansMonoCJKsc-Subset.woff2",revision:"e52357967aa973c04cce881c50cce0c8"},{url:"fonts/NotoSansMonoCJKsc-Bold-Subset.woff2",revision:"7e37d6b9fb1339898c00ac3b49c8b12e"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf",revision:"ed6fa47e3c1fc9aa68e9015a5300419f"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Italic.ttf",revision:"5624a73ef0a476e93ef5949dd6e452e1"},{url:"fonts/JetBrainsMonoNLNerdFontMono-BoldItalic.ttf",revision:"9b7603423a37fd2f544f75e9669689a0"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Bold.ttf",revision:"7deed80e7a4195a51f2c0b82aa364e97"},{url:"fonts/JetBrainsMonoNL-Regular.woff2",revision:"3bb268888f64810d496dd10295d4337c"},{url:"fonts/JetBrainsMonoNL-Italic.woff2",revision:"e035d3c7673175237d40687fc14d5b40"},{url:"fonts/JetBrainsMonoNL-BoldItalic.woff2",revision:"bc3577cc820eaef00d9a985058bca2b7"},{url:"fonts/JetBrainsMonoNL-Bold.woff2",revision:"7c0305a4db8f2977b061631755556ec0"},{url:"assets/xychartDiagram-FW5EYKEG-RIp6RoVE.js",revision:null},{url:"assets/wardleyDiagram-EHGQE667-CNf23qHc.js",revision:null},{url:"assets/vennDiagram-L72KCM5P-C2l_9LuC.js",revision:null},{url:"assets/timeline-definition-FHXFAJF6-CqHH1NSC.js",revision:null},{url:"assets/terminal-C7X98Amf.js",revision:null},{url:"assets/swimlanesDiagram-G3AALYLV-CUwNpVXZ.js",revision:null},{url:"assets/swimlanes-5IMT3BWC-Cmrmdfpk.js",revision:null},{url:"assets/stateDiagram-v2-6OUMAXLB-BSqYeylv.js",revision:null},{url:"assets/stateDiagram-2N3HPSRC-Y5rMnVoS.js",revision:null},{url:"assets/sizeCapture-X5ZJPWSS-DufN5SxK.js",revision:null},{url:"assets/sequenceDiagram-DBY2YBRQ-qnNrmb6g.js",revision:null},{url:"assets/sankeyDiagram-HTMAVEWB-Bhw1mAOH.js",revision:null},{url:"assets/requirementDiagram-TGXJPOKE-s5kYB_Ya.js",revision:null},{url:"assets/react-XDCjal2T.js",revision:null},{url:"assets/railroadDiagram-RFXS5EU6-D0lUBYT1.js",revision:null},{url:"assets/quadrantDiagram-ABIIQ3AL-CuJ9UyOO.js",revision:null},{url:"assets/purify.es-CC4Brkr_.js",revision:null},{url:"assets/pieDiagram-ENE6RG2P-BHrDXKeR.js",revision:null},{url:"assets/pegDiagram-2B236MQR-CQRK4LBF.js",revision:null},{url:"assets/ordinal-Cboi1Yqb.js",revision:null},{url:"assets/mindmap-definition-LN4V7U3C-CgkCCxMO.js",revision:null},{url:"assets/mermaid.core-C8HeTU54.js",revision:null},{url:"assets/map-BdqsVdb3.js",revision:null},{url:"assets/linear-3N9evV-e.js",revision:null},{url:"assets/layout-1eBIAoSl.js",revision:null},{url:"assets/katex-HP8lGamR.js",revision:null},{url:"assets/katex-DnlPpQZa.js",revision:null},{url:"assets/katex-55ZPZeOF.css",revision:null},{url:"assets/kanban-definition-HUTT4EX6-CcnV3jvx.js",revision:null},{url:"assets/journeyDiagram-5HDEW3XC-YKjq3svJ.js",revision:null},{url:"assets/ishikawaDiagram-FXEZZL3T-CbO_ecRd.js",revision:null},{url:"assets/init-Gi6I4Gst.js",revision:null},{url:"assets/infoDiagram-FWYZ7A6U-ClLiuIL8.js",revision:null},{url:"assets/index-DB-0aiWT.js",revision:null},{url:"assets/index-CbYQuue6.css",revision:null},{url:"assets/index-Ca-rq7yX.js",revision:null},{url:"assets/graph-D2o_JWn5.js",revision:null},{url:"assets/gitGraphDiagram-IHSO6WYX-DyNUwP6-.js",revision:null},{url:"assets/ganttDiagram-NO4QXBWP-BRTRytgo.js",revision:null},{url:"assets/flowDiagram-23GEKE2U-CZ7KiBe4.js",revision:null},{url:"assets/erDiagram-Q63AITRT-Cx3_8T9Q.js",revision:null},{url:"assets/ebnfDiagram-CCIWWBDH-TFnZlH4k.js",revision:null},{url:"assets/dnd-BCFAPC0L.js",revision:null},{url:"assets/diffWorker-BFIcKxCt.js",revision:null},{url:"assets/diagram-WEI45ONY-DNy0FovQ.js",revision:null},{url:"assets/diagram-OA4YK3LP-wn7Eux1Q.js",revision:null},{url:"assets/diagram-NH7WQ7WH-DwnG0pD9.js",revision:null},{url:"assets/diagram-G47NLZAW-DyKuyGsn.js",revision:null},{url:"assets/diagram-FQU43EPY-Cx8vWSw0.js",revision:null},{url:"assets/defaultLocale-CrowFXzY.js",revision:null},{url:"assets/dagre-VKFMJZFB-Bjh8y-RO.js",revision:null},{url:"assets/cytoscape.esm-D3_iZ_3b.js",revision:null},{url:"assets/cynefinDiagram-TSTJHNR4-BoKfaePi.js",revision:null},{url:"assets/cynefin-VYW2F7L2-B7X5gKj-.js",revision:null},{url:"assets/cose-bilkent-JH36ORCC-gtzwCMyn.js",revision:null},{url:"assets/classDiagram-v2-EOCWNBFH-BsrNzS08.js",revision:null},{url:"assets/classDiagram-OUVF2IWQ-BsrNzS08.js",revision:null},{url:"assets/chunk-XXDRQBXY-CZBBj4P_.js",revision:null},{url:"assets/chunk-VR4S4FIN-CtGdy3K8.js",revision:null},{url:"assets/chunk-V7JOEXUC-DpQA4l-k.js",revision:null},{url:"assets/chunk-RYQCIY6F-BdNa9gwg.js",revision:null},{url:"assets/chunk-MOJQB5TN-B5Kzsd5Z.js",revision:null},{url:"assets/chunk-JWPE2WC7-B8bZHm4g.js",revision:null},{url:"assets/chunk-EX3LRPZG-QndtZFQ6.js",revision:null},{url:"assets/chunk-5VM5RSS4-BrBK8Xy3.js",revision:null},{url:"assets/chunk-32BRIVSS-B4LjEHPP.js",revision:null},{url:"assets/chunk-2Q5K7J3B-DgBhC1KN.js",revision:null},{url:"assets/channel-DBZQbHSi.js",revision:null},{url:"assets/c4Diagram-LMCZKHZV-BYElS7nn.js",revision:null},{url:"assets/blockDiagram-677ZJIJ3-eoDvsGst.js",revision:null},{url:"assets/architectureDiagram-ZJ3FMSHR-CNNIlew_.js",revision:null},{url:"assets/arc-OmYbmu75.js",revision:null},{url:"assets/abnfDiagram-VRR7QNED-B3X7vTL7.js",revision:null},{url:"assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf",revision:null},{url:"assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2",revision:null},{url:"assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2",revision:null},{url:"assets/KaTeX_Size4-Regular-DWFBv043.ttf",revision:null},{url:"assets/KaTeX_Size3-Regular-DgpXs0kz.ttf",revision:null},{url:"assets/KaTeX_Size2-Regular-Dy4dx90m.woff2",revision:null},{url:"assets/KaTeX_Size2-Regular-B7gKUWhC.ttf",revision:null},{url:"assets/KaTeX_Size1-Regular-mCD8mA8B.woff2",revision:null},{url:"assets/KaTeX_Size1-Regular-Dbsnue_I.ttf",revision:null},{url:"assets/KaTeX_Script-Regular-D3wIWfF6.woff2",revision:null},{url:"assets/KaTeX_Script-Regular-C5JkGWo-.ttf",revision:null},{url:"assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2",revision:null},{url:"assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf",revision:null},{url:"assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf",revision:null},{url:"assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2",revision:null},{url:"assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2",revision:null},{url:"assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf",revision:null},{url:"assets/KaTeX_Math-Italic-t53AETM-.woff2",revision:null},{url:"assets/KaTeX_Math-Italic-flOr_0UB.ttf",revision:null},{url:"assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2",revision:null},{url:"assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf",revision:null},{url:"assets/KaTeX_Main-Regular-ypZvNtVU.ttf",revision:null},{url:"assets/KaTeX_Main-Regular-B22Nviop.woff2",revision:null},{url:"assets/KaTeX_Main-Italic-NWA7e6Wa.woff2",revision:null},{url:"assets/KaTeX_Main-Italic-3WenGoN9.ttf",revision:null},{url:"assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf",revision:null},{url:"assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2",revision:null},{url:"assets/KaTeX_Main-Bold-waoOVXN0.ttf",revision:null},{url:"assets/KaTeX_Main-Bold-Cx986IdX.woff2",revision:null},{url:"assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2",revision:null},{url:"assets/KaTeX_Fraktur-Regular-CB_wures.ttf",revision:null},{url:"assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2",revision:null},{url:"assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf",revision:null},{url:"assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf",revision:null},{url:"assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2",revision:null},{url:"assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2",revision:null},{url:"assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf",revision:null},{url:"assets/KaTeX_AMS-Regular-DRggAlZN.ttf",revision:null},{url:"assets/KaTeX_AMS-Regular-BQhdFMY1.woff2",revision:null},{url:"apple-touch-icon-180x180.png",revision:"628c2e2f28a9b545aa57be1352025643"},{url:"favicon.ico",revision:"2994debfa6f59a0898e24eb0c54d64b5"},{url:"maskable-icon-512x512.png",revision:"4d489331fd81f0713c400b26426442b9"},{url:"pwa-192x192.png",revision:"4d6f899888cfb8ccd7a1fd3c9e583ad2"},{url:"pwa-512x512.png",revision:"0885dd7ec8932e7f878b013d51a380a4"},{url:"robots.txt",revision:"d9339907142917dee4aac78494ac9cfe"},{url:"sw-notifications.js",revision:"2ed386d64dc020bf9a7da8efd9dbea42"},{url:"fonts/JetBrainsMonoNL-Bold.woff2",revision:"7c0305a4db8f2977b061631755556ec0"},{url:"fonts/JetBrainsMonoNL-BoldItalic.woff2",revision:"bc3577cc820eaef00d9a985058bca2b7"},{url:"fonts/JetBrainsMonoNL-Italic.woff2",revision:"e035d3c7673175237d40687fc14d5b40"},{url:"fonts/JetBrainsMonoNL-Regular.woff2",revision:"3bb268888f64810d496dd10295d4337c"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Bold.ttf",revision:"7deed80e7a4195a51f2c0b82aa364e97"},{url:"fonts/JetBrainsMonoNLNerdFontMono-BoldItalic.ttf",revision:"9b7603423a37fd2f544f75e9669689a0"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Italic.ttf",revision:"5624a73ef0a476e93ef5949dd6e452e1"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf",revision:"ed6fa47e3c1fc9aa68e9015a5300419f"},{url:"fonts/NotoSansMonoCJKsc-Bold-Subset.woff2",revision:"7e37d6b9fb1339898c00ac3b49c8b12e"},{url:"fonts/NotoSansMonoCJKsc-Subset.woff2",revision:"e52357967aa973c04cce881c50cce0c8"},{url:"fonts/NotoSansSymbols2-Subset.woff2",revision:"8257a6beabcc8858199b208908a96ee9"},{url:"fonts/SymbolsNerdFontMono-Regular.woff2",revision:"54113ea8487e607f9d9a482c5877aed4"},{url:"fonts/TermdockStarSymbol-Subset.woff2",revision:"853a8d84112dd97a91d6cff1e7ab6bdf"},{url:"manifest.webmanifest",revision:"83f9a43b74c236012adafb077fb6d774"}],{}),s.cleanupOutdatedCaches(),s.registerRoute(new s.NavigationRoute(s.createHandlerBoundToURL("index.html"),{denylist:[/^\/api\//,/^\/health$/]}))});
1
+ if(!self.define){let s,e={};const i=(i,n)=>(i=new URL(i+".js",n).href,e[i]||new Promise(e=>{if("document"in self){const s=document.createElement("script");s.src=i,s.onload=e,document.head.appendChild(s)}else s=i,importScripts(i),e()}).then(()=>{let s=e[i];if(!s)throw new Error(`Module ${i} didn’t register its module`);return s}));self.define=(n,l)=>{const r=s||("document"in self?document.currentScript.src:"")||location.href;if(e[r])return;let a={};const o=s=>i(s,r),u={module:{uri:r},exports:a,require:o};e[r]=Promise.all(n.map(s=>u[s]||o(s))).then(s=>(l(...s),a))}}define(["./workbox-9c191d2f"],function(s){"use strict";importScripts("sw-notifications.js"),self.skipWaiting(),s.clientsClaim(),s.precacheAndRoute([{url:"sw-notifications.js",revision:"91964c9b57d28d34a4e9e4911f8d72d8"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"pwa-64x64.png",revision:"c1ca9f6daa67743b38564b230314277a"},{url:"pwa-512x512.svg",revision:"149a151133f52feb34ab6ecedf349936"},{url:"pwa-512x512.png",revision:"0885dd7ec8932e7f878b013d51a380a4"},{url:"pwa-192x192.svg",revision:"541ace289ca5d6f3d269d1286cde5283"},{url:"pwa-192x192.png",revision:"4d6f899888cfb8ccd7a1fd3c9e583ad2"},{url:"maskable-icon-512x512.png",revision:"4d489331fd81f0713c400b26426442b9"},{url:"index.html",revision:"1ab8065947d404bcd4d7af31a5bb373f"},{url:"favicon.ico",revision:"2994debfa6f59a0898e24eb0c54d64b5"},{url:"apple-touch-icon-180x180.png",revision:"628c2e2f28a9b545aa57be1352025643"},{url:"icons/agents/opencode.svg",revision:"851c2931d95821ba2b39b68587b48976"},{url:"icons/agents/kimi.svg",revision:"5366c710f4f763022fe159a69cf17409"},{url:"icons/agents/grok.svg",revision:"cd745b0b964545bc7a6b4f36021c3298"},{url:"icons/agents/goose.svg",revision:"c79b927278983c19098d77b4b0c11ab3"},{url:"icons/agents/gemini.svg",revision:"7a64f6d6f80a908255e9c0256903882e"},{url:"icons/agents/droid.svg",revision:"f28723b8c5c115e0908ba3c72613e256"},{url:"icons/agents/cursor.svg",revision:"a1528f3d3cda497f75a5db28defb19de"},{url:"icons/agents/copilot.svg",revision:"dd52e96c1eac0b6ec4bedcd0ddd3b52d"},{url:"icons/agents/codex.svg",revision:"608e55babb0b217e1f2da287342b46d3"},{url:"icons/agents/claude.svg",revision:"08af0225b7acdd8e8f573f3247e5e1c3"},{url:"icons/agents/amp.svg",revision:"af12f8b33faa81e4d46ea7b93c7e43f3"},{url:"fonts/TermdockStarSymbol-Subset.woff2",revision:"853a8d84112dd97a91d6cff1e7ab6bdf"},{url:"fonts/SymbolsNerdFontMono-Regular.woff2",revision:"54113ea8487e607f9d9a482c5877aed4"},{url:"fonts/NotoSansSymbols2-Subset.woff2",revision:"8257a6beabcc8858199b208908a96ee9"},{url:"fonts/NotoSansMonoCJKsc-Subset.woff2",revision:"e52357967aa973c04cce881c50cce0c8"},{url:"fonts/NotoSansMonoCJKsc-Bold-Subset.woff2",revision:"7e37d6b9fb1339898c00ac3b49c8b12e"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf",revision:"ed6fa47e3c1fc9aa68e9015a5300419f"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Italic.ttf",revision:"5624a73ef0a476e93ef5949dd6e452e1"},{url:"fonts/JetBrainsMonoNLNerdFontMono-BoldItalic.ttf",revision:"9b7603423a37fd2f544f75e9669689a0"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Bold.ttf",revision:"7deed80e7a4195a51f2c0b82aa364e97"},{url:"fonts/JetBrainsMonoNL-Regular.woff2",revision:"3bb268888f64810d496dd10295d4337c"},{url:"fonts/JetBrainsMonoNL-Italic.woff2",revision:"e035d3c7673175237d40687fc14d5b40"},{url:"fonts/JetBrainsMonoNL-BoldItalic.woff2",revision:"bc3577cc820eaef00d9a985058bca2b7"},{url:"fonts/JetBrainsMonoNL-Bold.woff2",revision:"7c0305a4db8f2977b061631755556ec0"},{url:"assets/xychartDiagram-FW5EYKEG-DQW7oknN.js",revision:null},{url:"assets/wardleyDiagram-EHGQE667-Cgbh1_F7.js",revision:null},{url:"assets/vennDiagram-L72KCM5P-gXnoVb66.js",revision:null},{url:"assets/timeline-definition-FHXFAJF6-Mj3zCwTc.js",revision:null},{url:"assets/terminal-C7X98Amf.js",revision:null},{url:"assets/swimlanesDiagram-G3AALYLV-DmhG9XuP.js",revision:null},{url:"assets/swimlanes-5IMT3BWC-BW65nQch.js",revision:null},{url:"assets/stateDiagram-v2-6OUMAXLB-gXYMxyju.js",revision:null},{url:"assets/stateDiagram-2N3HPSRC-D-gs0yW-.js",revision:null},{url:"assets/sizeCapture-X5ZJPWSS-BvnnE9Ev.js",revision:null},{url:"assets/sequenceDiagram-DBY2YBRQ-CmkcMlBD.js",revision:null},{url:"assets/sankeyDiagram-HTMAVEWB-BfpeK6jA.js",revision:null},{url:"assets/requirementDiagram-TGXJPOKE-CfnNu5HR.js",revision:null},{url:"assets/react-XDCjal2T.js",revision:null},{url:"assets/railroadDiagram-RFXS5EU6-BUurE4S_.js",revision:null},{url:"assets/quadrantDiagram-ABIIQ3AL-DvXeQj4a.js",revision:null},{url:"assets/purify.es-CC4Brkr_.js",revision:null},{url:"assets/pieDiagram-ENE6RG2P-Ch2Ty0Y2.js",revision:null},{url:"assets/pegDiagram-2B236MQR-BckzTuEj.js",revision:null},{url:"assets/ordinal-Cboi1Yqb.js",revision:null},{url:"assets/mindmap-definition-LN4V7U3C-CjcF3QgK.js",revision:null},{url:"assets/mermaid.core-BXI9ENYY.js",revision:null},{url:"assets/map-BdqsVdb3.js",revision:null},{url:"assets/linear-DjF6fII5.js",revision:null},{url:"assets/layout-1eBIAoSl.js",revision:null},{url:"assets/katex-HP8lGamR.js",revision:null},{url:"assets/katex-DnlPpQZa.js",revision:null},{url:"assets/katex-55ZPZeOF.css",revision:null},{url:"assets/kanban-definition-HUTT4EX6-Db2NpkWT.js",revision:null},{url:"assets/journeyDiagram-5HDEW3XC-DNXNzmef.js",revision:null},{url:"assets/ishikawaDiagram-FXEZZL3T-uFSMTzKn.js",revision:null},{url:"assets/init-Gi6I4Gst.js",revision:null},{url:"assets/infoDiagram-FWYZ7A6U-ZrovoSsZ.js",revision:null},{url:"assets/index-DyTRm1PJ.css",revision:null},{url:"assets/index-Ca-rq7yX.js",revision:null},{url:"assets/index-BYpFR_L5.js",revision:null},{url:"assets/graph-D2o_JWn5.js",revision:null},{url:"assets/gitGraphDiagram-IHSO6WYX-D9Qw6ov4.js",revision:null},{url:"assets/ganttDiagram-NO4QXBWP-BL1NGUYm.js",revision:null},{url:"assets/flowDiagram-23GEKE2U-BqCA33p5.js",revision:null},{url:"assets/erDiagram-Q63AITRT-5jgm5Ksi.js",revision:null},{url:"assets/ebnfDiagram-CCIWWBDH-BARVSv3m.js",revision:null},{url:"assets/dnd-BCFAPC0L.js",revision:null},{url:"assets/diffWorker-BFIcKxCt.js",revision:null},{url:"assets/diagram-WEI45ONY-2hoKf8a6.js",revision:null},{url:"assets/diagram-OA4YK3LP-BB8nlvCx.js",revision:null},{url:"assets/diagram-NH7WQ7WH-B7NwHUrU.js",revision:null},{url:"assets/diagram-G47NLZAW-Bty2-g6M.js",revision:null},{url:"assets/diagram-FQU43EPY-DYc1Xsf9.js",revision:null},{url:"assets/defaultLocale-CrowFXzY.js",revision:null},{url:"assets/dagre-VKFMJZFB-n2L4piqF.js",revision:null},{url:"assets/cytoscape.esm-D3_iZ_3b.js",revision:null},{url:"assets/cynefinDiagram-TSTJHNR4-Cwtffn_E.js",revision:null},{url:"assets/cynefin-VYW2F7L2-DZfIXs6F.js",revision:null},{url:"assets/cose-bilkent-JH36ORCC-C_6nPODG.js",revision:null},{url:"assets/classDiagram-v2-EOCWNBFH-CtEpUyds.js",revision:null},{url:"assets/classDiagram-OUVF2IWQ-CtEpUyds.js",revision:null},{url:"assets/chunk-XXDRQBXY-BIzI5y55.js",revision:null},{url:"assets/chunk-VR4S4FIN-Bau1q0xN.js",revision:null},{url:"assets/chunk-V7JOEXUC-BjQQBzKq.js",revision:null},{url:"assets/chunk-RYQCIY6F-0MHmqGNs.js",revision:null},{url:"assets/chunk-MOJQB5TN-DWsVsFoK.js",revision:null},{url:"assets/chunk-JWPE2WC7-K8nPht_5.js",revision:null},{url:"assets/chunk-EX3LRPZG-BdI4v3f4.js",revision:null},{url:"assets/chunk-5VM5RSS4-Ctw1x2FG.js",revision:null},{url:"assets/chunk-32BRIVSS-C1g4c11U.js",revision:null},{url:"assets/chunk-2Q5K7J3B-BiBc5Uv0.js",revision:null},{url:"assets/channel-Ck9tD_4_.js",revision:null},{url:"assets/c4Diagram-LMCZKHZV-m3lWYsQI.js",revision:null},{url:"assets/blockDiagram-677ZJIJ3-BG8NrPiD.js",revision:null},{url:"assets/architectureDiagram-ZJ3FMSHR-C4BLot_l.js",revision:null},{url:"assets/arc-DhZtXG4q.js",revision:null},{url:"assets/abnfDiagram-VRR7QNED-Bt36T7gE.js",revision:null},{url:"assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf",revision:null},{url:"assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2",revision:null},{url:"assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2",revision:null},{url:"assets/KaTeX_Size4-Regular-DWFBv043.ttf",revision:null},{url:"assets/KaTeX_Size3-Regular-DgpXs0kz.ttf",revision:null},{url:"assets/KaTeX_Size2-Regular-Dy4dx90m.woff2",revision:null},{url:"assets/KaTeX_Size2-Regular-B7gKUWhC.ttf",revision:null},{url:"assets/KaTeX_Size1-Regular-mCD8mA8B.woff2",revision:null},{url:"assets/KaTeX_Size1-Regular-Dbsnue_I.ttf",revision:null},{url:"assets/KaTeX_Script-Regular-D3wIWfF6.woff2",revision:null},{url:"assets/KaTeX_Script-Regular-C5JkGWo-.ttf",revision:null},{url:"assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2",revision:null},{url:"assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf",revision:null},{url:"assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf",revision:null},{url:"assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2",revision:null},{url:"assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2",revision:null},{url:"assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf",revision:null},{url:"assets/KaTeX_Math-Italic-t53AETM-.woff2",revision:null},{url:"assets/KaTeX_Math-Italic-flOr_0UB.ttf",revision:null},{url:"assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2",revision:null},{url:"assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf",revision:null},{url:"assets/KaTeX_Main-Regular-ypZvNtVU.ttf",revision:null},{url:"assets/KaTeX_Main-Regular-B22Nviop.woff2",revision:null},{url:"assets/KaTeX_Main-Italic-NWA7e6Wa.woff2",revision:null},{url:"assets/KaTeX_Main-Italic-3WenGoN9.ttf",revision:null},{url:"assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf",revision:null},{url:"assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2",revision:null},{url:"assets/KaTeX_Main-Bold-waoOVXN0.ttf",revision:null},{url:"assets/KaTeX_Main-Bold-Cx986IdX.woff2",revision:null},{url:"assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2",revision:null},{url:"assets/KaTeX_Fraktur-Regular-CB_wures.ttf",revision:null},{url:"assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2",revision:null},{url:"assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf",revision:null},{url:"assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf",revision:null},{url:"assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2",revision:null},{url:"assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2",revision:null},{url:"assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf",revision:null},{url:"assets/KaTeX_AMS-Regular-DRggAlZN.ttf",revision:null},{url:"assets/KaTeX_AMS-Regular-BQhdFMY1.woff2",revision:null},{url:"apple-touch-icon-180x180.png",revision:"628c2e2f28a9b545aa57be1352025643"},{url:"favicon.ico",revision:"2994debfa6f59a0898e24eb0c54d64b5"},{url:"maskable-icon-512x512.png",revision:"4d489331fd81f0713c400b26426442b9"},{url:"pwa-192x192.png",revision:"4d6f899888cfb8ccd7a1fd3c9e583ad2"},{url:"pwa-512x512.png",revision:"0885dd7ec8932e7f878b013d51a380a4"},{url:"robots.txt",revision:"d9339907142917dee4aac78494ac9cfe"},{url:"sw-notifications.js",revision:"91964c9b57d28d34a4e9e4911f8d72d8"},{url:"fonts/JetBrainsMonoNL-Bold.woff2",revision:"7c0305a4db8f2977b061631755556ec0"},{url:"fonts/JetBrainsMonoNL-BoldItalic.woff2",revision:"bc3577cc820eaef00d9a985058bca2b7"},{url:"fonts/JetBrainsMonoNL-Italic.woff2",revision:"e035d3c7673175237d40687fc14d5b40"},{url:"fonts/JetBrainsMonoNL-Regular.woff2",revision:"3bb268888f64810d496dd10295d4337c"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Bold.ttf",revision:"7deed80e7a4195a51f2c0b82aa364e97"},{url:"fonts/JetBrainsMonoNLNerdFontMono-BoldItalic.ttf",revision:"9b7603423a37fd2f544f75e9669689a0"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Italic.ttf",revision:"5624a73ef0a476e93ef5949dd6e452e1"},{url:"fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf",revision:"ed6fa47e3c1fc9aa68e9015a5300419f"},{url:"fonts/NotoSansMonoCJKsc-Bold-Subset.woff2",revision:"7e37d6b9fb1339898c00ac3b49c8b12e"},{url:"fonts/NotoSansMonoCJKsc-Subset.woff2",revision:"e52357967aa973c04cce881c50cce0c8"},{url:"fonts/NotoSansSymbols2-Subset.woff2",revision:"8257a6beabcc8858199b208908a96ee9"},{url:"fonts/SymbolsNerdFontMono-Regular.woff2",revision:"54113ea8487e607f9d9a482c5877aed4"},{url:"fonts/TermdockStarSymbol-Subset.woff2",revision:"853a8d84112dd97a91d6cff1e7ab6bdf"},{url:"manifest.webmanifest",revision:"83f9a43b74c236012adafb077fb6d774"}],{}),s.cleanupOutdatedCaches(),s.registerRoute(new s.NavigationRoute(s.createHandlerBoundToURL("index.html"),{denylist:[/^\/api\//,/^\/health$/]}))});
@@ -19,7 +19,7 @@ import path from 'node:path';
19
19
  import { execFile } from 'node:child_process';
20
20
  import { agentBySlug, isPluginAgent } from './registry.js';
21
21
  import { loadPlugins, resolveHookTarget } from './plugins.js';
22
- export const HOOK_AGENTS = ['claude', 'codex', 'copilot', 'opencode', 'pi', 'grok'];
22
+ export const HOOK_AGENTS = ['claude', 'codex', 'copilot', 'opencode', 'pi', 'grok', 'kimi'];
23
23
  const DISPLAY_NAMES = {
24
24
  claude: 'Claude Code',
25
25
  codex: 'Codex',
@@ -27,6 +27,7 @@ const DISPLAY_NAMES = {
27
27
  opencode: 'OpenCode',
28
28
  pi: 'Pi',
29
29
  grok: 'Grok Build',
30
+ kimi: 'Kimi Code',
30
31
  };
31
32
  // ---------------------------------------------------------------------------
32
33
  // Paths and the hook command line
@@ -58,6 +59,7 @@ function targetPath(agent) {
58
59
  case 'opencode': return path.join(xdgConfigDir(), 'opencode', 'plugins', OWNED_FILE_STEM_JS);
59
60
  case 'pi': return path.join(homeDir(), '.pi', 'agent', 'extensions', 'termdock', 'index.ts');
60
61
  case 'grok': return path.join(homeDir(), '.grok', 'hooks', OWNED_FILE_STEM_JSON);
62
+ case 'kimi': return path.join(homeDir(), '.kimi-code', 'config.toml');
61
63
  }
62
64
  }
63
65
  function abbreviateHome(p) {
@@ -127,6 +129,29 @@ const CODEX_HOOK_EVENTS = [
127
129
  ['UserPromptSubmit', 'prompt-submit'],
128
130
  ['Stop', 'stop'],
129
131
  ];
132
+ /**
133
+ * Kimi Code's hook events (~/.kimi-code/config.toml `[[hooks]]` entries, TOML).
134
+ * `PermissionRequest` is the genuine block (fires right before the approval
135
+ * wait); `PostToolUse` is the way *back* — Kimi has a `PermissionResult`
136
+ * event, but the approved tool completing is the same recovery edge Claude
137
+ * uses, and it keeps the state machine on the shared vocabulary. `Stop` does
138
+ * NOT fire when the user interrupts (Esc) or the turn errors — Kimi sends
139
+ * `Interrupt` / `StopFailure` instead, so both fold onto `stop` to keep the
140
+ * pane from sticking in working.
141
+ */
142
+ export const KIMI_HOOK_EVENTS = [
143
+ ['SessionStart', 'session-start'],
144
+ ['UserPromptSubmit', 'prompt-submit'],
145
+ ['PermissionRequest', 'permission-request'],
146
+ ['PostToolUse', 'tool-complete'],
147
+ ['Stop', 'stop'],
148
+ ['StopFailure', 'stop'],
149
+ ['Interrupt', 'stop'],
150
+ ['SessionEnd', 'session-end'],
151
+ ];
152
+ /** Seconds Kimi gives one termdock hook before killing it (its default is
153
+ * 30s; ours writes a few bytes). */
154
+ const KIMI_HOOK_TIMEOUT_SECS = 5;
130
155
  /**
131
156
  * Grok Build's hook events — Claude Code's vocabulary (grok mirrors it
132
157
  * deliberately, it even reads ~/.claude/settings.json) — plus the matcher
@@ -473,6 +498,113 @@ function ownedFileUninstall(file, mark) {
473
498
  return 'Removed';
474
499
  }
475
500
  // ---------------------------------------------------------------------------
501
+ // TOML hooks installer (Kimi Code): kimi keeps hooks in `[[hooks]]` array-of-
502
+ // tables inside ~/.kimi-code/config.toml — free-form TOML we must NOT rewrite
503
+ // wholesale (comments, ordering, other tables are the user's). So instead of
504
+ // parsing, the file is treated as a sequence of *sections* (a `[table]` /
505
+ // `[[array]]` header line plus the lines up to the next header); termdock
506
+ // owns exactly the sections whose command carries the marker, appends fresh
507
+ // `[[hooks]]` blocks at the end of the file, and never touches the rest.
508
+ // ---------------------------------------------------------------------------
509
+ /** Split TOML text into sections: everything before the first header is one
510
+ * (preamble) section; each later section starts at its header line.
511
+ * `sections.join('\n')` reproduces the input byte-for-byte. */
512
+ function splitTomlSections(text) {
513
+ const lines = text.split('\n');
514
+ const sections = [];
515
+ let current = [];
516
+ for (const line of lines) {
517
+ if (/^\s*\[/.test(line) && current.length > 0) {
518
+ sections.push(current.join('\n'));
519
+ current = [];
520
+ }
521
+ current.push(line);
522
+ }
523
+ sections.push(current.join('\n'));
524
+ return sections;
525
+ }
526
+ /** The `event = "…"` value of a `[[hooks]]` section, if it declares one. */
527
+ function tomlSectionEvent(section) {
528
+ const m = section.match(/^\s*event\s*=\s*"([^"]+)"\s*(?:#.*)?$/m);
529
+ return m ? m[1] : null;
530
+ }
531
+ /** The `command = '…'` value of a `[[hooks]]` section (literal-string form —
532
+ * the only form termdock writes; the command embeds double quotes). */
533
+ function tomlSectionCommand(section) {
534
+ const m = section.match(/^\s*command\s*=\s*'(.*)'\s*(?:#.*)?$/m);
535
+ return m ? m[1] : null;
536
+ }
537
+ /** One `[[hooks]]` block as termdock writes it. Kimi allows exactly the
538
+ * fields event/matcher/command/timeout. */
539
+ function tomlHookBlock(hookEvent, command) {
540
+ return `[[hooks]]\nevent = "${hookEvent}"\ncommand = '${command}'\ntimeout = ${KIMI_HOOK_TIMEOUT_SECS}`;
541
+ }
542
+ function readTextFile(file) {
543
+ try {
544
+ return fs.readFileSync(file, 'utf8');
545
+ }
546
+ catch {
547
+ return null;
548
+ }
549
+ }
550
+ // Exported for tests — the public entry points are hooksState/installHooks/
551
+ // uninstallHooks; these take an explicit file path so tests can use tmp files.
552
+ export function tomlHooksState(file, agent, events) {
553
+ const text = readTextFile(file);
554
+ if (text === null)
555
+ return 'not-installed';
556
+ const mark = marker(agent);
557
+ const ours = splitTomlSections(text).filter((s) => s.includes(mark));
558
+ if (ours.length === 0)
559
+ return 'not-installed';
560
+ for (const [hookEvent, sentinel] of events) {
561
+ const expected = hookCommand(agent, sentinel);
562
+ const ok = ours.some((s) => tomlSectionEvent(s) === hookEvent && tomlSectionCommand(s) === expected);
563
+ if (!ok)
564
+ return 'outdated';
565
+ }
566
+ return 'installed';
567
+ }
568
+ /** Merge termdock's `[[hooks]]` blocks into config.toml, preserving
569
+ * everything else (including the user's own hooks) byte-for-byte. */
570
+ export function tomlHooksInstall(file, agent, events) {
571
+ const text = readTextFile(file) ?? '';
572
+ const mark = marker(agent);
573
+ // Drop any previous termdock blocks (stale exe path / older event set)…
574
+ const kept = splitTomlSections(text).filter((s) => !s.includes(mark));
575
+ let out = kept.join('\n');
576
+ // …then append the fresh set at the end of the file, where new
577
+ // `[[hooks]]` tables can never swallow the user's trailing keys.
578
+ if (out.length > 0 && !out.endsWith('\n'))
579
+ out += '\n';
580
+ if (out.trim().length > 0)
581
+ out += '\n';
582
+ out += events
583
+ .map(([hookEvent, sentinel]) => tomlHookBlock(hookEvent, hookCommand(agent, sentinel)))
584
+ .join('\n\n');
585
+ out += '\n';
586
+ writeAtomic(file, out);
587
+ }
588
+ /** Remove every termdock `[[hooks]]` block, leaving the rest of the file
589
+ * (user hooks included) untouched. */
590
+ export function tomlHooksUninstall(file, agent) {
591
+ const text = readTextFile(file);
592
+ if (text === null)
593
+ return 'Nothing installed; nothing to remove';
594
+ const mark = marker(agent);
595
+ const sections = splitTomlSections(text);
596
+ const removed = sections.filter((s) => s.includes(mark)).length;
597
+ if (removed === 0)
598
+ return 'No termdock hooks found; nothing to remove';
599
+ const out = sections
600
+ .filter((s) => !s.includes(mark))
601
+ .join('\n')
602
+ .replace(/\n{3,}/g, '\n\n')
603
+ .replace(/\n+$/, '\n');
604
+ writeAtomic(file, out);
605
+ return 'Removed';
606
+ }
607
+ // ---------------------------------------------------------------------------
476
608
  // Public API — built-in agents
477
609
  // ---------------------------------------------------------------------------
478
610
  export function hooksState(agent) {
@@ -481,6 +613,8 @@ export function hooksState(agent) {
481
613
  return hookMapState(file, agent, CLAUDE_HOOK_EVENTS);
482
614
  if (agent === 'codex')
483
615
  return hookMapState(file, agent, CODEX_HOOK_EVENTS);
616
+ if (agent === 'kimi')
617
+ return tomlHooksState(file, agent, KIMI_HOOK_EVENTS);
484
618
  const expected = ownedFileContent(agent);
485
619
  if (expected === null)
486
620
  return 'not-installed';
@@ -518,6 +652,13 @@ export async function installHooks(agent) {
518
652
  };
519
653
  }
520
654
  }
655
+ if (agent === 'kimi') {
656
+ tomlHooksInstall(file, agent, KIMI_HOOK_EVENTS);
657
+ return {
658
+ summary: 'Installed — run /reload in any open Kimi session (or restart it) for hooks to take effect',
659
+ devMode,
660
+ };
661
+ }
521
662
  const content = ownedFileContent(agent);
522
663
  if (content === null)
523
664
  throw new Error(`no owned-file integration for ${agent}`);
@@ -529,6 +670,9 @@ export async function uninstallHooks(agent) {
529
670
  if (agent === 'claude' || agent === 'codex') {
530
671
  return hookMapUninstall(file, agent);
531
672
  }
673
+ if (agent === 'kimi') {
674
+ return tomlHooksUninstall(file, agent);
675
+ }
532
676
  return ownedFileUninstall(file, marker(agent));
533
677
  }
534
678
  /** Collect all plugin agents that define hooks. */
@@ -18,6 +18,7 @@ const AGENTS = [
18
18
  { slug: 'antigravity', displayName: 'Antigravity', aliases: ['agy', 'antigravity'], accentColor: '#2563EB', icon: null },
19
19
  { slug: 'grok', displayName: 'Grok', aliases: ['grok'], accentColor: '#000000', icon: 'grok' },
20
20
  { slug: 'qwen', displayName: 'Qwen Code', aliases: ['qwen', 'qwen-code'], accentColor: '#7C3AED', icon: null },
21
+ { slug: 'kimi', displayName: 'Kimi Code', aliases: ['kimi', 'kimi-code'], accentColor: '#000000', icon: 'kimi' },
21
22
  ];
22
23
  const BY_ALIAS = new Map();
23
24
  const BY_SLUG = new Map();
@@ -137,6 +138,7 @@ function resumeCommandFor(agent, sessionId, flags) {
137
138
  case 'cursor': return `cursor-agent${f} --resume ${sessionId}`;
138
139
  case 'copilot': return `copilot${f} --resume ${sessionId}`;
139
140
  case 'grok': return `grok${f} --resume ${sessionId}`;
141
+ case 'kimi': return `kimi${f} --session ${sessionId}`;
140
142
  default: return null;
141
143
  }
142
144
  }
@@ -150,6 +152,7 @@ const STALE_SESSION_FLAGS = {
150
152
  codex: ['--last'],
151
153
  grok: ['--resume', '-r', '--load', '--continue', '-c', '--session-id', '-s',
152
154
  '--fork-session', '--worktree', '-w', '--worktree-ref', '--ref'],
155
+ kimi: ['--session', '-S', '--continue', '-c'],
153
156
  };
154
157
  /**
155
158
  * The launch-flag tail of argv worth replaying on a resume command, or null
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ // libuv 线程池默认只有 4 个线程,慢/挂起的异步 IO(fs.promises、crypto 等)
3
+ // 会把池耗尽,表现为整个服务的 IO 全部卡死。必须在任何异步 fs 调用之前
4
+ // 设置(同步 fs 不占线程池,所以放在文件体顶部仍然足够早)。
5
+ process.env.UV_THREADPOOL_SIZE ||= '16';
2
6
  import { config as loadDotenv } from 'dotenv';
3
7
  import os from 'os';
4
8
  import { resolve } from 'path';
@@ -1915,6 +1919,26 @@ async function ensureTmuxFocusEvents() {
1915
1919
  maxBuffer: 64 * 1024,
1916
1920
  });
1917
1921
  }
1922
+ // Let tmux forward modified Enter keys (Ctrl/Shift/Alt+Enter) to pane apps.
1923
+ // The option only exists in tmux >= 3.2; on older versions the probe fails
1924
+ // and we silently skip.
1925
+ async function ensureTmuxExtendedKeys() {
1926
+ try {
1927
+ const { stdout } = await execFileAsync('tmux', ['show-options', '-gqv', 'extended-keys'], {
1928
+ timeout: 5000,
1929
+ maxBuffer: 64 * 1024,
1930
+ });
1931
+ if (stdout.trim() === 'on')
1932
+ return;
1933
+ await execFileAsync('tmux', ['set-option', '-g', 'extended-keys', 'on'], {
1934
+ timeout: 5000,
1935
+ maxBuffer: 64 * 1024,
1936
+ });
1937
+ }
1938
+ catch {
1939
+ // extended-keys unsupported on this tmux; nothing to do
1940
+ }
1941
+ }
1918
1942
  async function ensureTmuxScrollbackProfile(sessionName) {
1919
1943
  await execFileAsync('tmux', ['set-option', '-g', 'history-limit', String(TERMDOCK_TMUX_HISTORY_LIMIT)], {
1920
1944
  timeout: 5000,
@@ -2060,6 +2084,7 @@ async function ensureStampedTmuxSession(sessionName, cwd) {
2060
2084
  });
2061
2085
  }
2062
2086
  await ensureTmuxFocusEvents();
2087
+ await ensureTmuxExtendedKeys();
2063
2088
  await ensureTmuxColorEnvironment(sessionName);
2064
2089
  await ensureTmuxCliSessionOptions(sessionName);
2065
2090
  await setTmuxOption(sessionName, '@termdock-version', TERMDOCK_VERSION);
@@ -22,6 +22,7 @@ import { isUpgradeRequestAuthenticated, requireAuth } from './utils/authProtecti
22
22
  import { localAccessManager } from './utils/localAccess.js';
23
23
  import { isAllowedHost, isUpgradeOriginAllowed, validateHostMiddleware, } from './utils/requestSecurity.js';
24
24
  import { getCookieSecurityOptions, setSecureCookieMode } from './utils/cookieSecurity.js';
25
+ import { requestDeadlineMiddleware } from './utils/requestDeadline.js';
25
26
  import { startOnboardingServer, stopOnboardingServer, getOnboardingServerUrl } from './onboardingServer.js';
26
27
  import { CertificateWatcher } from './certificateWatcher.js';
27
28
  import { writeDiffTraceLog, writeErrorLog, writeJsonLog, writeTextLog } from './utils/serverLogger.js';
@@ -29,6 +30,22 @@ import { getTermdockVersion, TERMDOCK_CAPABILITIES, TERMDOCK_PROTOCOL_VERSION, }
29
30
  import { PORT, DEFAULT_HOST } from './config.js';
30
31
  const CLIENT_STATE_COOKIE = 'termdock-client';
31
32
  export const DEFAULT_PORT = PORT.backend;
33
+ /** Read one cookie from a raw Cookie header (upgrade requests bypass the
34
+ * cookie-parser middleware). Values are percent-encoded. */
35
+ function getCookieValue(cookieHeader, name) {
36
+ if (!cookieHeader)
37
+ return undefined;
38
+ const pattern = new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=([^;]*)`);
39
+ const match = cookieHeader.match(pattern);
40
+ if (!match)
41
+ return undefined;
42
+ try {
43
+ return decodeURIComponent(match[1]);
44
+ }
45
+ catch {
46
+ return match[1];
47
+ }
48
+ }
32
49
  const CLIENT_LOG_DEDUP_WINDOW_MS = 5_000;
33
50
  const CLIENT_LOG_RATE_WINDOW_MS = 10_000;
34
51
  const CLIENT_LOG_RATE_LIMIT = 120;
@@ -302,6 +319,9 @@ export function createApp(options = {}) {
302
319
  });
303
320
  next();
304
321
  });
322
+ // 全局请求兜底超时:任何 handler 挂起最多活 30s(长连接流按表豁免),
323
+ // 到期销毁 socket 释放浏览器连接槽。必须在所有路由与静态资源之前挂载。
324
+ app.use(requestDeadlineMiddleware());
305
325
  app.use('/api', (req, res, next) => {
306
326
  // JSON/API responses must never hit browser conditional caching. A 304 with
307
327
  // an empty body breaks fetch().json() callers and looks like random IO
@@ -523,11 +543,12 @@ export function startServer(options = {}) {
523
543
  }
524
544
  const sessionId = match[1];
525
545
  const clientId = crypto.randomUUID();
546
+ const pushClientId = getCookieValue(cookieHeader, CLIENT_STATE_COOKIE);
526
547
  // 短线重连时客户端会带上 ?since=<lastSeq>,让服务端只补发增量。
527
548
  const sinceParam = url.searchParams.get('since');
528
549
  const sinceSeq = sinceParam ? Math.max(0, Number.parseInt(sinceParam, 10) || 0) : 0;
529
550
  wss.handleUpgrade(request, socket, head, (ws) => {
530
- handleTerminalWebSocket(ws, sessionId, clientId, { sinceSeq });
551
+ handleTerminalWebSocket(ws, sessionId, clientId, { sinceSeq, pushClientId });
531
552
  });
532
553
  });
533
554
  server.listen(port, host, () => {
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import webpush from 'web-push';
5
+ import { getViewingPushClientIds } from './pushViewers.js';
5
6
  const storeDirectory = path.join(os.homedir(), '.termdock');
6
7
  const storePath = path.join(storeDirectory, 'push-notifications.json');
7
8
  let store = null;
@@ -20,7 +21,14 @@ function readStore() {
20
21
  && typeof parsed.vapid?.publicKey === 'string'
21
22
  && typeof parsed.vapid?.privateKey === 'string'
22
23
  && Array.isArray(parsed.subscriptions)) {
23
- store = parsed;
24
+ // Subscriptions written before exitEnabled existed default to opt-out.
25
+ store = {
26
+ ...parsed,
27
+ subscriptions: parsed.subscriptions.map((entry) => ({
28
+ ...entry,
29
+ exitEnabled: entry.exitEnabled === true,
30
+ })),
31
+ };
24
32
  }
25
33
  }
26
34
  catch {
@@ -34,7 +42,11 @@ function readStore() {
34
42
  };
35
43
  persistStore(store);
36
44
  }
37
- webpush.setVapidDetails('mailto:push@termdock.local', store.vapid.publicKey, store.vapid.privateKey);
45
+ // VAPID subject must be a mailto: or https: contact URI. Configurable for
46
+ // self-hosters; defaults to the project repository instead of a fake mailbox.
47
+ const vapidSubject = process.env.TERMDOCK_VAPID_SUBJECT?.trim()
48
+ || 'https://github.com/Jovines/termdock';
49
+ webpush.setVapidDetails(vapidSubject, store.vapid.publicKey, store.vapid.privateKey);
38
50
  return store;
39
51
  }
40
52
  export function getVapidPublicKey() {
@@ -67,10 +79,43 @@ export function removePushSubscription(clientId, endpoint) {
67
79
  if (current.subscriptions.length !== previousLength)
68
80
  persistStore(current);
69
81
  }
70
- async function sendPayload(payload) {
82
+ function isZhLocale(locale) {
83
+ return locale.toLowerCase().startsWith('zh');
84
+ }
85
+ /**
86
+ * Localized push copy, mirroring the client-side getAgentNotificationText in
87
+ * useTerminalStore.ts so both delivery paths read the same for one locale.
88
+ */
89
+ export function agentNotificationText(locale, kind, agentName) {
90
+ if (isZhLocale(locale)) {
91
+ if (kind === 'waiting')
92
+ return { title: `${agentName} 需要你的处理`, body: '点按返回对应会话' };
93
+ if (kind === 'done')
94
+ return { title: `${agentName} 已完成`, body: '点按查看结果' };
95
+ return { title: `${agentName} 已退出`, body: '点按查看结果' };
96
+ }
97
+ if (kind === 'waiting')
98
+ return { title: `${agentName} needs your input`, body: 'Tap to return to the session' };
99
+ if (kind === 'done')
100
+ return { title: `${agentName} finished`, body: 'Tap to see the result' };
101
+ return { title: `${agentName} exited`, body: 'Tap to see the result' };
102
+ }
103
+ export function terminalExitText(locale, exitCode) {
104
+ if (isZhLocale(locale)) {
105
+ return {
106
+ title: '终端会话已退出',
107
+ body: exitCode !== null ? `进程已退出(代码 ${exitCode}),点按查看` : '进程已退出,点按查看',
108
+ };
109
+ }
110
+ return {
111
+ title: 'Terminal session ended',
112
+ body: exitCode !== null ? `Process exited (code ${exitCode}). Tap to view.` : 'Process exited. Tap to view.',
113
+ };
114
+ }
115
+ async function sendPayload(options) {
71
116
  const current = readStore();
72
117
  const invalidEndpoints = new Set();
73
- await Promise.all(current.subscriptions.filter((entry) => entry.aiEnabled).map(async (entry) => {
118
+ await Promise.all(current.subscriptions.filter((entry) => (options.enabled(entry) && !options.skipClientIds?.has(entry.clientId))).map(async (entry) => {
74
119
  try {
75
120
  const subscription = {
76
121
  endpoint: entry.endpoint,
@@ -78,12 +123,12 @@ async function sendPayload(payload) {
78
123
  keys: entry.keys,
79
124
  };
80
125
  await webpush.sendNotification(subscription, JSON.stringify({
81
- ...payload,
126
+ ...options.build(entry.locale),
82
127
  alertStyle: entry.alertStyle,
83
128
  locale: entry.locale,
84
129
  }), {
85
- TTL: 60 * 60,
86
- urgency: payload.kind === 'waiting' ? 'high' : 'normal',
130
+ TTL: options.ttlSeconds ?? 60 * 60,
131
+ urgency: options.urgency ?? 'normal',
87
132
  });
88
133
  }
89
134
  catch (error) {
@@ -104,39 +149,80 @@ async function sendPayload(payload) {
104
149
  export function notifyAgentTransition(sessionId, previous, current) {
105
150
  if (!shouldNotifyAgentTransition(previous, current))
106
151
  return;
107
- const agentExited = current.agentStatus === null && previous?.agentStatus === 'working';
108
- const kind = agentExited ? 'exited' : current.agentStatus;
152
+ const agentExited = (current.agentStatus === null || current.agentStatus === 'idle')
153
+ && (previous?.agentStatus === 'working' || previous?.agentStatus === 'waiting');
154
+ const kind = (agentExited ? 'exited' : current.agentStatus);
109
155
  const agentName = current.agent?.displayName ?? previous?.agent?.displayName ?? 'Agent';
110
- const title = kind === 'waiting'
111
- ? `${agentName} 需要你的处理`
112
- : kind === 'done'
113
- ? `${agentName} 已完成`
114
- : `${agentName} 已退出`;
115
156
  void sendPayload({
116
- kind,
117
- title,
118
- body: current.agentMessage ?? (kind === 'waiting' ? '点按返回对应会话' : '点按查看结果'),
119
- // Same tag for all transitions of one session new notification
120
- // replaces the old one (key behaviour on iOS where getNotifications()
121
- // is unavailable for programmatic cleanup).
122
- tag: `agent:${sessionId}`,
123
- // Per-transition key so the 5 s dedup window only suppresses
124
- // duplicate deliveries of the same transition, not later ones.
125
- dedupKey: `agent:${sessionId}:${kind}:${current.agentActivity}`,
126
- sessionId,
127
- url: `/?session=${encodeURIComponent(sessionId)}`,
157
+ enabled: (entry) => entry.aiEnabled,
158
+ skipClientIds: getViewingPushClientIds(sessionId),
159
+ urgency: kind === 'waiting' ? 'high' : 'normal',
160
+ ttlSeconds: kind === 'waiting' ? 24 * 60 * 60 : 60 * 60,
161
+ build: (locale) => {
162
+ const text = agentNotificationText(locale, kind, agentName);
163
+ return {
164
+ kind,
165
+ title: text.title,
166
+ body: current.agentMessage ?? text.body,
167
+ // Same tag for all transitions of one session → new notification
168
+ // replaces the old one (key behaviour on iOS where getNotifications()
169
+ // is unavailable for programmatic cleanup).
170
+ tag: `agent:${sessionId}`,
171
+ // Per-transition key so the 5 s dedup window only suppresses
172
+ // duplicate deliveries of the same transition, not later ones.
173
+ dedupKey: `agent:${sessionId}:${kind}:${current.agentActivity}`,
174
+ sessionId,
175
+ url: `/?session=${encodeURIComponent(sessionId)}`,
176
+ };
177
+ },
178
+ });
179
+ }
180
+ /**
181
+ * Push for a terminal process that exited on its own (not a user-initiated
182
+ * close — those dispose the exit listener before killing the pty). Sessions
183
+ * with a live agent are covered by notifyAgentTransition instead, so the
184
+ * caller skips those to avoid double notifications for one event.
185
+ */
186
+ export function notifyTerminalExit(sessionId, exitCode) {
187
+ void sendPayload({
188
+ enabled: (entry) => entry.exitEnabled,
189
+ skipClientIds: getViewingPushClientIds(sessionId),
190
+ build: (locale) => {
191
+ const text = terminalExitText(locale, exitCode);
192
+ return {
193
+ kind: 'exit',
194
+ title: text.title,
195
+ body: text.body,
196
+ tag: `exit:${sessionId}`,
197
+ dedupKey: `exit:${sessionId}:${exitCode ?? 'signal'}`,
198
+ sessionId,
199
+ url: `/?session=${encodeURIComponent(sessionId)}`,
200
+ };
201
+ },
128
202
  });
129
203
  }
130
204
  export function shouldNotifyAgentTransition(previous, current) {
131
205
  if (!previous)
132
206
  return false;
133
- const needsAttention = current.reviewed !== true
134
- && (current.agentStatus === 'waiting' || current.agentStatus === 'done');
135
- const attentionChanged = previous.agentStatus !== current.agentStatus
136
- || previous.agentActivity !== current.agentActivity
137
- || previous.reviewed === true;
138
- const agentExited = current.agentStatus === null
139
- && previous.agentStatus === 'working'
140
- && current.reviewed !== true;
141
- return (needsAttention && attentionChanged) || agentExited;
207
+ // Only status or activity changes carry new information. A `reviewed` flip
208
+ // is the user's own acknowledgement (or a stop stamping an unread turn) and
209
+ // must not re-notify by itself — the previous reviewed-based gate here
210
+ // silently suppressed every mid-turn waiting event, because reviewed stays
211
+ // true while an agent is working or waiting and only `stop` clears it.
212
+ const stateChanged = previous.agentStatus !== current.agentStatus
213
+ || previous.agentActivity !== current.agentActivity;
214
+ if (!stateChanged)
215
+ return false;
216
+ if (current.agentStatus === 'waiting' || current.agentStatus === 'done') {
217
+ // waiting: any new wait event (entering waiting, or a new activity round
218
+ // while waiting) deserves a push. done: only entering done — a second
219
+ // stop for the same turn, or an ack-only broadcast, must stay silent.
220
+ return current.agentStatus === 'waiting' || previous.agentStatus !== 'done';
221
+ }
222
+ // Agent exited: from working or waiting (including a hook-emitted
223
+ // session-end that lands on 'idle' before the process poll clears the
224
+ // session). done → exit is deliberately silent — the completion was
225
+ // already notified.
226
+ return (current.agentStatus === null || current.agentStatus === 'idle')
227
+ && (previous.agentStatus === 'working' || previous.agentStatus === 'waiting');
142
228
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Tracks which push-subscribed clients are actively viewing which terminal
3
+ * session, so agent-transition pushes can skip the very client that is
4
+ * already looking at the session (its notification would be redundant).
5
+ *
6
+ * The per-terminal WebSocket reports viewing state (session active + document
7
+ * visible + window focused + stream ready — deliberately NOT textarea focus,
8
+ * since mobile users routinely watch output with the keyboard dismissed) on
9
+ * every change, including a viewing=false when the user switches sessions,
10
+ * backgrounds the app, or the window blurs — and the WS close handler clears
11
+ * the entry when the connection dies (e.g. the iOS PWA was killed without a
12
+ * clean viewing=false).
13
+ */
14
+ const viewingSessionsByClient = new Map();
15
+ export function setClientViewingSession(pushClientId, sessionId, viewing) {
16
+ if (!pushClientId || !sessionId)
17
+ return;
18
+ if (!viewing) {
19
+ const sessions = viewingSessionsByClient.get(pushClientId);
20
+ if (!sessions)
21
+ return;
22
+ sessions.delete(sessionId);
23
+ if (sessions.size === 0)
24
+ viewingSessionsByClient.delete(pushClientId);
25
+ return;
26
+ }
27
+ let sessions = viewingSessionsByClient.get(pushClientId);
28
+ if (!sessions) {
29
+ sessions = new Set();
30
+ viewingSessionsByClient.set(pushClientId, sessions);
31
+ }
32
+ sessions.add(sessionId);
33
+ }
34
+ /** Push subscription clientIds currently looking at the given session. */
35
+ export function getViewingPushClientIds(sessionId) {
36
+ const result = new Set();
37
+ if (!sessionId)
38
+ return result;
39
+ for (const [pushClientId, sessions] of viewingSessionsByClient) {
40
+ if (sessions.has(sessionId))
41
+ result.add(pushClientId);
42
+ }
43
+ return result;
44
+ }