@oversk7/pi-web-dsh-style 1.3.2 → 1.3.4

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 (187) hide show
  1. package/README.md +10 -2
  2. package/THIRD_PARTY_NOTICES.md +32 -3
  3. package/package.json +4 -1
  4. package/server.ts +234 -21
  5. package/sse-channel.ts +37 -1
  6. package/web/app.css +9 -1
  7. package/web/app.js +96 -25
  8. package/web/assets/fonts/KaTeX_AMS-Regular.ttf +0 -0
  9. package/web/assets/fonts/KaTeX_AMS-Regular.woff +0 -0
  10. package/web/assets/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  11. package/web/assets/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  12. package/web/assets/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  13. package/web/assets/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  14. package/web/assets/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  15. package/web/assets/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  16. package/web/assets/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  17. package/web/assets/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  18. package/web/assets/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  19. package/web/assets/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  20. package/web/assets/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  21. package/web/assets/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  22. package/web/assets/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  23. package/web/assets/fonts/KaTeX_Main-Bold.ttf +0 -0
  24. package/web/assets/fonts/KaTeX_Main-Bold.woff +0 -0
  25. package/web/assets/fonts/KaTeX_Main-Bold.woff2 +0 -0
  26. package/web/assets/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  27. package/web/assets/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  28. package/web/assets/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  29. package/web/assets/fonts/KaTeX_Main-Italic.ttf +0 -0
  30. package/web/assets/fonts/KaTeX_Main-Italic.woff +0 -0
  31. package/web/assets/fonts/KaTeX_Main-Italic.woff2 +0 -0
  32. package/web/assets/fonts/KaTeX_Main-Regular.ttf +0 -0
  33. package/web/assets/fonts/KaTeX_Main-Regular.woff +0 -0
  34. package/web/assets/fonts/KaTeX_Main-Regular.woff2 +0 -0
  35. package/web/assets/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  36. package/web/assets/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  37. package/web/assets/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  38. package/web/assets/fonts/KaTeX_Math-Italic.ttf +0 -0
  39. package/web/assets/fonts/KaTeX_Math-Italic.woff +0 -0
  40. package/web/assets/fonts/KaTeX_Math-Italic.woff2 +0 -0
  41. package/web/assets/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  42. package/web/assets/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  43. package/web/assets/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  44. package/web/assets/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  45. package/web/assets/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  46. package/web/assets/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  47. package/web/assets/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  48. package/web/assets/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  49. package/web/assets/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  50. package/web/assets/fonts/KaTeX_Script-Regular.ttf +0 -0
  51. package/web/assets/fonts/KaTeX_Script-Regular.woff +0 -0
  52. package/web/assets/fonts/KaTeX_Script-Regular.woff2 +0 -0
  53. package/web/assets/fonts/KaTeX_Size1-Regular.ttf +0 -0
  54. package/web/assets/fonts/KaTeX_Size1-Regular.woff +0 -0
  55. package/web/assets/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  56. package/web/assets/fonts/KaTeX_Size2-Regular.ttf +0 -0
  57. package/web/assets/fonts/KaTeX_Size2-Regular.woff +0 -0
  58. package/web/assets/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  59. package/web/assets/fonts/KaTeX_Size3-Regular.ttf +0 -0
  60. package/web/assets/fonts/KaTeX_Size3-Regular.woff +0 -0
  61. package/web/assets/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  62. package/web/assets/fonts/KaTeX_Size4-Regular.ttf +0 -0
  63. package/web/assets/fonts/KaTeX_Size4-Regular.woff +0 -0
  64. package/web/assets/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  65. package/web/assets/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  66. package/web/assets/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  67. package/web/assets/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  68. package/web/assets/katex.min.css +1 -0
  69. package/web/assets/markdown.LICENSE.txt +78 -0
  70. package/web/assets/markdown.js +536 -21
  71. package/web/assets/mermaid/chunks/abnfDiagram-YUKMZFIV-FZYDJU67.js +1 -0
  72. package/web/assets/mermaid/chunks/architecture-WOLXFQ4H-BZVX64AA.js +1 -0
  73. package/web/assets/mermaid/chunks/architectureDiagram-47P4ROYG-2BMC2Z7S.js +36 -0
  74. package/web/assets/mermaid/chunks/blockDiagram-E7TT5TSB-FH6ZULKF.js +140 -0
  75. package/web/assets/mermaid/chunks/c4Diagram-CRA5TL53-OG62EAHM.js +38 -0
  76. package/web/assets/mermaid/chunks/chunk-22D53RF2.js +2 -0
  77. package/web/assets/mermaid/chunks/chunk-25KVF5ZE.js +1 -0
  78. package/web/assets/mermaid/chunks/chunk-4HYFSIZT.js +1 -0
  79. package/web/assets/mermaid/chunks/chunk-4W4OQ3QY.js +1 -0
  80. package/web/assets/mermaid/chunks/chunk-5TQRX2ND.js +1 -0
  81. package/web/assets/mermaid/chunks/chunk-64OFLKGX.js +322 -0
  82. package/web/assets/mermaid/chunks/chunk-64OFLKGX.js.LEGAL.txt +17 -0
  83. package/web/assets/mermaid/chunks/chunk-6UFWKTSA.js +1 -0
  84. package/web/assets/mermaid/chunks/chunk-6ZAAIG6Q.js +10 -0
  85. package/web/assets/mermaid/chunks/chunk-76QEHT4T.js +1 -0
  86. package/web/assets/mermaid/chunks/chunk-A7665WVC.js +1 -0
  87. package/web/assets/mermaid/chunks/chunk-AECJS4LP.js +130 -0
  88. package/web/assets/mermaid/chunks/chunk-AECJS4LP.js.LEGAL.txt +5 -0
  89. package/web/assets/mermaid/chunks/chunk-ATQQWCXO.js +2 -0
  90. package/web/assets/mermaid/chunks/chunk-DQVO43GU.js +1 -0
  91. package/web/assets/mermaid/chunks/chunk-DSKRGV6V.js +1 -0
  92. package/web/assets/mermaid/chunks/chunk-EIBFAW37.js +1 -0
  93. package/web/assets/mermaid/chunks/chunk-FBC3PYSO.js +1 -0
  94. package/web/assets/mermaid/chunks/chunk-FQ5IIL54.js +88 -0
  95. package/web/assets/mermaid/chunks/chunk-GZYY6JMP.js +1 -0
  96. package/web/assets/mermaid/chunks/chunk-HIGPS4L2.js +1 -0
  97. package/web/assets/mermaid/chunks/chunk-HP75Y56I.js +2 -0
  98. package/web/assets/mermaid/chunks/chunk-JRAHLUQM.js +32 -0
  99. package/web/assets/mermaid/chunks/chunk-KNYDPLLK.js +1 -0
  100. package/web/assets/mermaid/chunks/chunk-L465623K.js +136 -0
  101. package/web/assets/mermaid/chunks/chunk-L7BMR7WL.js +1 -0
  102. package/web/assets/mermaid/chunks/chunk-LLGPMHB6.js +1 -0
  103. package/web/assets/mermaid/chunks/chunk-NBH7YOBD.js +15 -0
  104. package/web/assets/mermaid/chunks/chunk-NCOVM3YF.js +1 -0
  105. package/web/assets/mermaid/chunks/chunk-PHKIAKHS.js +1 -0
  106. package/web/assets/mermaid/chunks/chunk-PM2IFX5O.js +1 -0
  107. package/web/assets/mermaid/chunks/chunk-PN62QGJW.js +72 -0
  108. package/web/assets/mermaid/chunks/chunk-QHMFFOZZ.js +1 -0
  109. package/web/assets/mermaid/chunks/chunk-QQZBUVKZ.js +1 -0
  110. package/web/assets/mermaid/chunks/chunk-RV7WQMNL.js +1 -0
  111. package/web/assets/mermaid/chunks/chunk-STBOPOHX.js +1 -0
  112. package/web/assets/mermaid/chunks/chunk-T3FZWEYV.js +1 -0
  113. package/web/assets/mermaid/chunks/chunk-TMMWB6NQ.js +1 -0
  114. package/web/assets/mermaid/chunks/chunk-TPW5CADB.js +1 -0
  115. package/web/assets/mermaid/chunks/chunk-TSOLLCID.js +2 -0
  116. package/web/assets/mermaid/chunks/chunk-TWGPYWIY.js +1 -0
  117. package/web/assets/mermaid/chunks/chunk-WGMEMLED.js +1 -0
  118. package/web/assets/mermaid/chunks/chunk-XC2LKMMW.js +1 -0
  119. package/web/assets/mermaid/chunks/chunk-XHVVPFJ5.js +1 -0
  120. package/web/assets/mermaid/chunks/chunk-YIN5CSIF.js +2 -0
  121. package/web/assets/mermaid/chunks/chunk-YIN5CSIF.js.LEGAL.txt +12 -0
  122. package/web/assets/mermaid/chunks/chunk-YORIST7Z.js +1 -0
  123. package/web/assets/mermaid/chunks/chunk-YPXVLPXW.js +3 -0
  124. package/web/assets/mermaid/chunks/chunk-Z4WODCD2.js +213 -0
  125. package/web/assets/mermaid/chunks/chunk-ZAERIUBF.js +1 -0
  126. package/web/assets/mermaid/chunks/chunk-ZDOBOOWG.js +62 -0
  127. package/web/assets/mermaid/chunks/classDiagram-v2-K4WV4PDN-HXQD675C.js +217 -0
  128. package/web/assets/mermaid/chunks/cose-bilkent-JH36ORCC-3VHNHM7I.js +1 -0
  129. package/web/assets/mermaid/chunks/cynefin-EF2NZ3EQ-ZO5PPXJG.js +1 -0
  130. package/web/assets/mermaid/chunks/cynefinDiagram-3GCD6N5R-LFWDWE4E.js +62 -0
  131. package/web/assets/mermaid/chunks/dagre-W4DXFKR2-SDBBVBGG.js +4 -0
  132. package/web/assets/mermaid/chunks/diagram-2UJZ2QOL-YCKW3WNJ.js +200 -0
  133. package/web/assets/mermaid/chunks/diagram-OVF4WLC6-62NNPE34.js +24 -0
  134. package/web/assets/mermaid/chunks/diagram-PFPMY2P6-7RR2GTZ5.js +30 -0
  135. package/web/assets/mermaid/chunks/diagram-UMYRVEAY-Q4I25SMR.js +24 -0
  136. package/web/assets/mermaid/chunks/diagram-YEKJPTXX-VNUTOPRK.js +41 -0
  137. package/web/assets/mermaid/chunks/diagram-ZIFT7M5P-KALXX5CE.js +3 -0
  138. package/web/assets/mermaid/chunks/ebnfDiagram-VR2GEFS7-WIFBISHL.js +1 -0
  139. package/web/assets/mermaid/chunks/elk-IJKZMXRS-LGG232IL.js +27 -0
  140. package/web/assets/mermaid/chunks/erDiagram-O2IAPWRE-KWX2J2TX.js +99 -0
  141. package/web/assets/mermaid/chunks/eventmodeling-K75KTNOO-3JNV4VSP.js +1 -0
  142. package/web/assets/mermaid/chunks/flowDiagram-OXPTDLAJ-CUMOBDC3.js +1 -0
  143. package/web/assets/mermaid/chunks/ganttDiagram-R7TSEDQI-GCS3MRFX.js +292 -0
  144. package/web/assets/mermaid/chunks/gitGraph-VSP46ZUC-HSAXKLAG.js +1 -0
  145. package/web/assets/mermaid/chunks/gitGraphDiagram-XJZIOB7I-FB763ROZ.js +106 -0
  146. package/web/assets/mermaid/chunks/info-OHQRW6UA-C4R2B34T.js +1 -0
  147. package/web/assets/mermaid/chunks/infoDiagram-5W2HQ5XZ-XCQU5T5D.js +2 -0
  148. package/web/assets/mermaid/chunks/ishikawaDiagram-K3B6WC7H-UWYT66G7.js +70 -0
  149. package/web/assets/mermaid/chunks/journeyDiagram-COXZFDF6-DOW66RYW.js +139 -0
  150. package/web/assets/mermaid/chunks/kanban-definition-P3RFRI5V-K55BXDLO.js +89 -0
  151. package/web/assets/mermaid/chunks/katex-5AUD5AJN.js +257 -0
  152. package/web/assets/mermaid/chunks/mindmap-definition-LZFPQGKD-WG3FIZ5U.js +96 -0
  153. package/web/assets/mermaid/chunks/packet-JDAUHWVQ-SFUSDIUN.js +1 -0
  154. package/web/assets/mermaid/chunks/pegDiagram-BGZESJAR-MQOH3WII.js +1 -0
  155. package/web/assets/mermaid/chunks/pie-XZMESJXO-M76QC4CD.js +1 -0
  156. package/web/assets/mermaid/chunks/pieDiagram-CAPJLFHJ-V3WNR4Y2.js +39 -0
  157. package/web/assets/mermaid/chunks/quadrantDiagram-GDMTTRAM-DJLTATHS.js +7 -0
  158. package/web/assets/mermaid/chunks/radar-ABXABTNO-HSBJ6SSD.js +1 -0
  159. package/web/assets/mermaid/chunks/railroad-I3PHUGI6-4QJMJ7LG.js +1 -0
  160. package/web/assets/mermaid/chunks/railroad-abnf-LNEFI6M7-ZLNZ5ZRH.js +1 -0
  161. package/web/assets/mermaid/chunks/railroad-ebnf-DWYD2UWJ-EBV6ZOUW.js +1 -0
  162. package/web/assets/mermaid/chunks/railroad-peg-JR7BVNR7-X3E47TO7.js +1 -0
  163. package/web/assets/mermaid/chunks/railroadDiagram-SM67HX2B-ZHEERYJS.js +1 -0
  164. package/web/assets/mermaid/chunks/requirementDiagram-X7JNWC4A-TVHJNPPI.js +84 -0
  165. package/web/assets/mermaid/chunks/sankeyDiagram-UM26HJRW-FXHWY3MM.js +40 -0
  166. package/web/assets/mermaid/chunks/sequenceDiagram-ZO4K6R2Y-OP7DCVPK.js +169 -0
  167. package/web/assets/mermaid/chunks/sizeCapture-INFHLROL-2GUJIKNX.js +1 -0
  168. package/web/assets/mermaid/chunks/stateDiagram-v2-TBUQTH76-R26D6XHK.js +291 -0
  169. package/web/assets/mermaid/chunks/swimlanes-N4OXWK64-FPGACWHE.js +1 -0
  170. package/web/assets/mermaid/chunks/swimlanesDiagram-K3J5GTZL-YMNX34Q2.js +8 -0
  171. package/web/assets/mermaid/chunks/timeline-definition-YOQAKGHF-R2U2UR4K.js +120 -0
  172. package/web/assets/mermaid/chunks/treeView-D4JQ5SDB-3BQHZUCI.js +1 -0
  173. package/web/assets/mermaid/chunks/treemap-SAJKECNS-6NJPJ5NM.js +1 -0
  174. package/web/assets/mermaid/chunks/usecaseDiagram-VIAY4XPW-BCEIGXDX.js +370 -0
  175. package/web/assets/mermaid/chunks/vennDiagram-BLWOH2XV-DOV5W2IV.js +34 -0
  176. package/web/assets/mermaid/chunks/wardley-7MLQ67FV-ITOM4KQ2.js +1 -0
  177. package/web/assets/mermaid/chunks/wardleyDiagram-YMQ3BMBF-CGJ7AG4L.js +78 -0
  178. package/web/assets/mermaid/chunks/xychartDiagram-TAQBALBS-2MTP7IBV.js +7 -0
  179. package/web/assets/mermaid/mermaid.js +11 -0
  180. package/web/assets/mermaid/mermaid.js.LEGAL.txt +7 -0
  181. package/web/index.html +1 -0
  182. package/web/markdown-diagrams.js +147 -0
  183. package/web/markdown.ts +41 -2
  184. package/web/session-sync.d.ts +12 -0
  185. package/web/session-sync.js +105 -0
  186. package/web/slash-commands.js +1 -0
  187. package/web/themes.css +46 -0
package/README.md CHANGED
@@ -16,7 +16,7 @@ A browser interface for [Pi Coding Agent](https://pi.dev), inspired by DeepSeek
16
16
 
17
17
  ## English
18
18
 
19
- Pi Web brings your Pi sessions to desktop and mobile browsers. Run multiple workspaces and sessions, inspect streaming replies and tool calls, preview files, revisit conversation branches, and use models, slash commands, extensions and skills. Pi and its tools keep running on your own computer. Choose light or dark themes and a custom chat background.
19
+ Pi Web brings your Pi sessions to desktop and mobile browsers. Run multiple workspaces and sessions, inspect streaming replies and tool calls, preview files, revisit conversation branches, and use models, slash commands, extensions and skills. Pi and its tools keep running on your own computer. Choose light or dark themes and a custom chat background. Replies and Markdown file previews render mathematical formulas and Mermaid diagrams, with diagram modules loaded on demand.
20
20
 
21
21
  ### Requirements
22
22
 
@@ -40,6 +40,10 @@ pi --web
40
40
 
41
41
  You can also enter `/web` in an existing Pi terminal. The page opens on your computer; the default local address is `http://127.0.0.1:18789`. To stop the service, run `/web --stop` or exit the host Pi process. Closing the browser tab alone does not stop it.
42
42
 
43
+ Each visible page subscribes to its selected session, receives an initial snapshot, and then receives only changes. Hidden tabs pause live updates and notifications; returning to the page synchronizes the latest state. Pi and its tools continue running while no page is viewing them.
44
+
45
+ Switching back to a newly created or previously loaded session shows its cached content immediately and keeps the composer available while refreshing. Repeated opens share pending requests, and sessions with a running process return their current snapshot without waiting for RPC refreshes.
46
+
43
47
  The interface starts in Chinese. To switch to English, open **设置 (Settings) → 外观 (Appearance) → 界面语言 (Interface language) → English**. Your choice is saved in this browser.
44
48
 
45
49
  ### Phone and remote access
@@ -61,7 +65,7 @@ npm run verify
61
65
 
62
66
  ## 简体中文
63
67
 
64
- Pi Web 为 [Pi Coding Agent](https://pi.dev) 提供浏览器界面,灵感来自 DeepSeek Harness。在桌面或手机上切换多个工作区与会话,查看流式回答、工具调用和文件预览,浏览会话分支,并使用模型、斜杠命令、扩展与技能。Pi 和工具仍运行在自己的电脑上。支持明暗主题和自定义聊天背景。
68
+ Pi Web 为 [Pi Coding Agent](https://pi.dev) 提供浏览器界面,灵感来自 DeepSeek Harness。在桌面或手机上切换多个工作区与会话,查看流式回答、工具调用和文件预览,浏览会话分支,并使用模型、斜杠命令、扩展与技能。Pi 和工具仍运行在自己的电脑上。支持明暗主题和自定义聊天背景。回答和 Markdown 文件预览支持数学公式与 Mermaid 图表,图表模块按需加载。
65
69
 
66
70
  ### 系统要求
67
71
 
@@ -85,6 +89,10 @@ pi --web
85
89
 
86
90
  也可以在已有的 Pi 终端中输入 `/web`。网页会在本机打开,默认地址为 `http://127.0.0.1:18789`。执行 `/web --stop` 或退出宿主 Pi 可停止服务;仅关闭浏览器标签页不会停止服务。
87
91
 
92
+ 每个可见网页只订阅正在查看的会话,首次接收快照,随后只传输变化内容。隐藏标签页会暂停实时更新和通知,返回网页后同步最新状态。没有网页查看时,Pi 和工具仍会继续运行。
93
+
94
+ 切回新建或已加载的会话时,立即显示缓存内容,后台刷新期间仍可编辑输入。重复打开会复用待完成请求;已有运行进程的会话直接返回当前快照,避免等待 RPC 刷新。
95
+
88
96
  界面默认使用中文。打开 **设置 → 外观 → 界面语言**,即可一键切换中文或 English;选择会保存在当前浏览器。
89
97
 
90
98
  ### 手机与远程访问
@@ -3,9 +3,10 @@
3
3
  This package contains adapted web UI styles and bundled font assets from the
4
4
  projects below. Their licenses are reproduced here as required.
5
5
 
6
- The browser Markdown renderer also bundles markdown-it and highlight.js with
7
- their dependencies. Their license texts are included in
8
- `web/assets/markdown.LICENSE.txt`, next to the bundled renderer.
6
+ The browser Markdown renderer bundles markdown-it, highlight.js, KaTeX,
7
+ markdown-it-texmath, and Mermaid. Their license texts are included in
8
+ `web/assets/markdown.LICENSE.txt`. Mermaid dependency notices are also included
9
+ alongside the entry and chunks under `web/assets/mermaid/` as `.LEGAL.txt` files.
9
10
 
10
11
  ## DeepSeek Harness
11
12
 
@@ -92,3 +93,31 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
92
93
  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
93
94
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
94
95
  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
96
+
97
+ ## Oh Pi Themes — Tokyo Night
98
+
99
+ Source: https://github.com/ifiokjr/oh-pi/tree/main/packages/themes
100
+
101
+ The chat palette in `web/themes.css` adapts the Tokyo Night semantic colors.
102
+
103
+ MIT License
104
+
105
+ Copyright (c) 2025 Ifiok Jr.
106
+
107
+ Permission is hereby granted, free of charge, to any person obtaining a copy
108
+ of this software and associated documentation files (the "Software"), to deal
109
+ in the Software without restriction, including without limitation the rights
110
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
111
+ copies of the Software, and to permit persons to whom the Software is
112
+ furnished to do so, subject to the following conditions:
113
+
114
+ The above copyright notice and this permission notice shall be included in all
115
+ copies or substantial portions of the Software.
116
+
117
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
118
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
119
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
120
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
121
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
122
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
123
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oversk7/pi-web-dsh-style",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "An unofficial DSH-styled Windows browser UI for pi coding agent with configurable LAN and native frp relay access",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -87,7 +87,10 @@
87
87
  "fast-xml-parser": "^5.11.1",
88
88
  "fflate": "^0.8.3",
89
89
  "highlight.js": "^11.12.0",
90
+ "katex": "^0.19.0",
90
91
  "markdown-it": "^15.0.2",
92
+ "markdown-it-texmath": "^1.0.0",
93
+ "mermaid": "^12.1.0",
91
94
  "pdfjs-dist": "^6.3.289"
92
95
  }
93
96
  }
package/server.ts CHANGED
@@ -1,7 +1,9 @@
1
- import type { ServerResponse } from "node:http";
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { createServer } from "node:http";
3
3
  import { spawn } from "node:child_process";
4
- import { randomUUID } from "node:crypto";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { promisify } from "node:util";
6
+ import { brotliCompress, constants as zlibConstants, gzip } from "node:zlib";
5
7
  import { existsSync } from "node:fs";
6
8
  import { chmod, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink } from "node:fs/promises";
7
9
  import { homedir, platform, tmpdir } from "node:os";
@@ -181,6 +183,7 @@ interface RuntimeSession {
181
183
  streaming: boolean;
182
184
  needsAttention: boolean;
183
185
  starting?: Promise<void>;
186
+ openRefresh?: Promise<void>;
184
187
  modelRefresh?: Promise<void>;
185
188
  commandRefresh?: Promise<void>;
186
189
  lastUsed: number;
@@ -242,6 +245,8 @@ let heartbeat: ReturnType<typeof setInterval> | undefined;
242
245
  const MAX_IDLE_PROCESSES = 1;
243
246
  const modelInfoCache = new Map<string, ModelInfo>();
244
247
  const modelCatalogRefreshes = new Map<string, Promise<ModelCatalog>>();
248
+ const recentModelCatalogs = new Map<string, { catalog: ModelCatalog; refreshedAt: number }>();
249
+ const MODEL_CATALOG_REUSE_MS = 30_000;
245
250
  const commandCache = new Map<string, SlashCommandInfo[]>();
246
251
  const exportDownloads = new Map<string, ExportDownload>();
247
252
  const attachments = new AttachmentStore();
@@ -517,13 +522,23 @@ function isImmediateExtensionCommand(record: RuntimeSession, message: string): b
517
522
 
518
523
  function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number) {
519
524
  let timer: ReturnType<typeof setTimeout> | undefined;
520
- return (...args: A) => {
525
+ const schedule = (...args: A) => {
521
526
  if (timer) clearTimeout(timer);
522
- timer = setTimeout(() => fn(...args), ms);
527
+ timer = setTimeout(() => {
528
+ timer = undefined;
529
+ fn(...args);
530
+ }, ms);
523
531
  };
532
+ return Object.assign(schedule, {
533
+ cancel() {
534
+ if (timer) clearTimeout(timer);
535
+ timer = undefined;
536
+ },
537
+ });
524
538
  }
525
539
 
526
540
  const scheduleSave = debounce(() => {
541
+ if (shuttingDown) return;
527
542
  stateSaveChain = stateSaveChain.then(saveStateNow).catch(() => {});
528
543
  }, 120);
529
544
 
@@ -545,6 +560,7 @@ async function loadState(): Promise<void> {
545
560
  }) ?? { version: 1, workspaces: [], sessions: [] };
546
561
  sessions = new Map();
547
562
  modelInfoCache.clear();
563
+ recentModelCatalogs.clear();
548
564
  commandCache.clear();
549
565
  if (state.modelCache) {
550
566
  for (const [cwd, cached] of Object.entries(state.modelCache)) {
@@ -605,7 +621,14 @@ function firstMessageTitle(session: SessionInfo): string {
605
621
  // ---------------------------------------------------------------------------
606
622
 
607
623
  function broadcastEvent(event: Json): void {
608
- for (const client of [...sseClients]) sendSseEvent(client, event);
624
+ for (const client of [...sseClients]) {
625
+ if (event.sessionId && client.sessionId !== event.sessionId) continue;
626
+ sendSseEvent(client, event);
627
+ }
628
+ }
629
+
630
+ function hasSessionSubscribers(sessionId: string): boolean {
631
+ return [...sseClients].some((client) => !client.closed && client.sessionId === sessionId);
609
632
  }
610
633
 
611
634
  function sessionSummary(record: RuntimeSession): Json {
@@ -669,12 +692,17 @@ function setPendingQueue(record: RuntimeSession, queue: RpcQueueSnapshot): void
669
692
  const snapshotScheduler = createSnapshotScheduler<RuntimeSession>(emitSessionSnapshot);
670
693
 
671
694
  function broadcastSessionSnapshot(record: RuntimeSession): void {
672
- if (shuttingDown) return;
695
+ if (shuttingDown || !hasSessionSubscribers(record.id)) return;
673
696
  snapshotScheduler.schedule(record.id, record, !record.streaming || record.dialogs.length > 0);
674
697
  }
675
698
 
676
699
  function emitSessionSnapshot(record: RuntimeSession): void {
677
- broadcastEvent({
700
+ if (shuttingDown || !hasSessionSubscribers(record.id)) return;
701
+ broadcastEvent(sessionSnapshot(record));
702
+ }
703
+
704
+ function sessionSnapshot(record: RuntimeSession): Json {
705
+ return {
678
706
  type: "snapshot",
679
707
  sessionId: record.id,
680
708
  session: sessionSummary(record),
@@ -688,7 +716,7 @@ function emitSessionSnapshot(record: RuntimeSession): void {
688
716
  messages: browserMessages(snapshotMessages(record)),
689
717
  pendingQueue: pendingQueueSnapshot(record),
690
718
  dialogs: record.dialogs,
691
- });
719
+ };
692
720
  }
693
721
 
694
722
  function serializeWorkspaces(): Json[] {
@@ -917,12 +945,26 @@ async function discoverFreshModelCatalog(cwd: string): Promise<ModelCatalog> {
917
945
  })();
918
946
  modelCatalogRefreshes.set(key, task);
919
947
  try {
920
- return await task;
948
+ const catalog = await task;
949
+ recentModelCatalogs.set(key, { catalog, refreshedAt: Date.now() });
950
+ return catalog;
921
951
  } finally {
922
952
  modelCatalogRefreshes.delete(key);
923
953
  }
924
954
  }
925
955
 
956
+ async function modelCatalogForNewSession(cwd: string, preferredModel?: unknown): Promise<ModelCatalog> {
957
+ const key = normalizePath(cwd);
958
+ const refreshing = modelCatalogRefreshes.get(key);
959
+ if (refreshing) return refreshing;
960
+ const recent = recentModelCatalogs.get(key);
961
+ if (recent && Date.now() - recent.refreshedAt < MODEL_CATALOG_REUSE_MS
962
+ && (!preferredModel || findCatalogModel(recent.catalog.models, preferredModel))) {
963
+ return recent.catalog;
964
+ }
965
+ return discoverFreshModelCatalog(cwd);
966
+ }
967
+
926
968
  function applyModelInfo(record: RuntimeSession, info: ModelInfo): void {
927
969
  record.models = info.models;
928
970
  record.thinkingLevels = info.thinkingLevels;
@@ -1444,6 +1486,19 @@ async function ensureProcess(record: RuntimeSession): Promise<void> {
1444
1486
  return task;
1445
1487
  }
1446
1488
 
1489
+ function refreshOpenedSession(record: RuntimeSession): void {
1490
+ if (record.openRefresh || record.streaming || record.starting) return;
1491
+ record.openRefresh = (async () => {
1492
+ await Promise.allSettled([refreshState(record), refreshMessages(record)]);
1493
+ if (record.models.length === 0) await refreshModels(record).catch(() => {});
1494
+ applyCachedModelInfo(record);
1495
+ await refreshStats(record);
1496
+ broadcastSessionSnapshot(record);
1497
+ })().catch((error) => {
1498
+ console.error("[pi-web] open session refresh failed:", error);
1499
+ }).finally(() => { record.openRefresh = undefined; });
1500
+ }
1501
+
1447
1502
  async function openSessionRecord(record: RuntimeSession): Promise<void> {
1448
1503
  record.lastUsed = Date.now();
1449
1504
  applyCachedCommands(record);
@@ -1506,6 +1561,14 @@ async function openSessionRecord(record: RuntimeSession): Promise<void> {
1506
1561
  return;
1507
1562
  }
1508
1563
 
1564
+ if (record.proc && record.state) {
1565
+ // Live events keep this snapshot current; browsing must not wait for RPC.
1566
+ applyCachedModelInfo(record);
1567
+ refreshOpenedSession(record);
1568
+ broadcastSessionSnapshot(record);
1569
+ return;
1570
+ }
1571
+
1509
1572
  if (!record.proc) {
1510
1573
  await ensureProcess(record);
1511
1574
  } else {
@@ -1591,7 +1654,7 @@ async function createSession(
1591
1654
  const ws = state.workspaces.find((w) => w.id === workspaceId);
1592
1655
  if (!ws) throw new Error("workspace not found");
1593
1656
  const info = modelInfoFromCatalog(
1594
- await discoverFreshModelCatalog(ws.path),
1657
+ await modelCatalogForNewSession(ws.path, preferences.model),
1595
1658
  preferences.model,
1596
1659
  preferences.thinkingLevel,
1597
1660
  );
@@ -2041,19 +2104,161 @@ function filePathFromUrl(url: string): string {
2041
2104
  return path;
2042
2105
  }
2043
2106
 
2044
- async function serveStatic(res: ServerResponse, url: string): Promise<void> {
2107
+ type StaticEncoding = "br" | "gzip" | "identity";
2108
+ interface StaticAsset {
2109
+ metadata: string;
2110
+ data: Buffer;
2111
+ etag: string;
2112
+ bytes: number;
2113
+ compressed: Partial<Record<"br" | "gzip", Buffer>>;
2114
+ compressing: Partial<Record<"br" | "gzip", Promise<Buffer>>>;
2115
+ }
2116
+
2117
+ const STATIC_CACHE_LIMIT = 32 * 1024 * 1024;
2118
+ const STATIC_ENTRY_LIMIT = 8 * 1024 * 1024;
2119
+ const staticAssets = new Map<string, StaticAsset>();
2120
+ const staticReads = new Map<string, Promise<StaticAsset>>();
2121
+ let staticCacheBytes = 0;
2122
+ const compressBrotliStatic = promisify(brotliCompress);
2123
+ const compressStatic = {
2124
+ br: (data: Buffer) => compressBrotliStatic(data, { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 4 } }),
2125
+ gzip: promisify(gzip),
2126
+ };
2127
+
2128
+ function evictStaticAsset(path: string): void {
2129
+ const entry = staticAssets.get(path);
2130
+ if (entry) staticCacheBytes -= entry.bytes;
2131
+ staticAssets.delete(path);
2132
+ }
2133
+
2134
+ function trimStaticCache(): void {
2135
+ while (staticCacheBytes > STATIC_CACHE_LIMIT || staticAssets.size > 256) {
2136
+ evictStaticAsset(staticAssets.keys().next().value!);
2137
+ }
2138
+ }
2139
+
2140
+ function staticMetadata(info: Awaited<ReturnType<typeof stat>>): string {
2141
+ return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
2142
+ }
2143
+
2144
+ async function loadStaticAsset(path: string): Promise<StaticAsset> {
2145
+ // Revalidate metadata even for cached entries so local rebuilds are visible immediately.
2146
+ for (let attempt = 0; attempt < 3; attempt++) {
2147
+ const info = await stat(path);
2148
+ if (!info.isFile()) throw new Error("not a file");
2149
+ const metadata = staticMetadata(info);
2150
+ const cached = staticAssets.get(path);
2151
+ if (cached?.metadata === metadata) {
2152
+ staticAssets.delete(path);
2153
+ staticAssets.set(path, cached);
2154
+ return cached;
2155
+ }
2156
+ evictStaticAsset(path);
2157
+ const key = `${path}\0${metadata}`;
2158
+ let reading = staticReads.get(key);
2159
+ if (!reading) {
2160
+ reading = (async () => {
2161
+ const data = await readFile(path);
2162
+ if (staticMetadata(await stat(path)) !== metadata) throw new Error("static asset changed during read");
2163
+ const entry: StaticAsset = {
2164
+ metadata, data, etag: `W/"${createHash("sha256").update(data).digest("hex")}"`,
2165
+ bytes: data.length, compressed: {}, compressing: {},
2166
+ };
2167
+ if (entry.bytes <= STATIC_ENTRY_LIMIT) {
2168
+ evictStaticAsset(path);
2169
+ staticAssets.set(path, entry);
2170
+ staticCacheBytes += entry.bytes;
2171
+ trimStaticCache();
2172
+ }
2173
+ return entry;
2174
+ })();
2175
+ staticReads.set(key, reading);
2176
+ }
2177
+ try {
2178
+ return await reading;
2179
+ } catch (error) {
2180
+ if (attempt === 2) throw error;
2181
+ } finally {
2182
+ if (staticReads.get(key) === reading) staticReads.delete(key);
2183
+ }
2184
+ }
2185
+ throw new Error("unable to read static asset");
2186
+ }
2187
+
2188
+ function staticEncoding(header: string | undefined, compressible: boolean): StaticEncoding | undefined {
2189
+ if (!header?.trim()) return "identity";
2190
+ const qualities = new Map<string, number>();
2191
+ for (const part of header.split(",")) {
2192
+ const [name, ...parameters] = part.trim().toLowerCase().split(";");
2193
+ if (!name) continue;
2194
+ const q = parameters.map((parameter) => parameter.trim()).find((parameter) => parameter.startsWith("q="));
2195
+ const value = q === undefined ? 1 : Number(q.slice(2));
2196
+ qualities.set(name, Number.isFinite(value) && value >= 0 && value <= 1 ? value : 0);
2197
+ }
2198
+ const quality = (encoding: StaticEncoding): number => qualities.get(encoding)
2199
+ ?? (encoding === "identity" ? (qualities.get("*") === 0 ? 0 : 1) : (qualities.get("*") ?? 0));
2200
+ const encodings: StaticEncoding[] = compressible ? ["br", "gzip", "identity"] : ["identity"];
2201
+ return encodings.filter((encoding) => quality(encoding) > 0)
2202
+ .sort((a, b) => quality(b) - quality(a))[0];
2203
+ }
2204
+
2205
+ async function staticRepresentation(path: string, entry: StaticAsset, encoding: StaticEncoding): Promise<Buffer> {
2206
+ if (encoding === "identity") return entry.data;
2207
+ const cached = entry.compressed[encoding];
2208
+ if (cached) return cached;
2209
+ let compressing = entry.compressing[encoding];
2210
+ if (!compressing) {
2211
+ compressing = compressStatic[encoding](entry.data).then((data) => {
2212
+ if (staticAssets.get(path) === entry && entry.bytes + data.length <= STATIC_ENTRY_LIMIT) {
2213
+ entry.compressed[encoding] = data;
2214
+ entry.bytes += data.length;
2215
+ staticCacheBytes += data.length;
2216
+ trimStaticCache();
2217
+ }
2218
+ return data;
2219
+ }).finally(() => { delete entry.compressing[encoding]; });
2220
+ entry.compressing[encoding] = compressing;
2221
+ }
2222
+ return compressing;
2223
+ }
2224
+
2225
+ function staticEtagMatches(header: string | undefined, etag: string): boolean {
2226
+ return Boolean(header?.split(",").some((candidate) => {
2227
+ const tag = candidate.trim();
2228
+ return tag === "*" || tag.replace(/^W\//, "") === etag.replace(/^W\//, "");
2229
+ }));
2230
+ }
2231
+
2232
+ async function serveStatic(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
2045
2233
  const filePath = filePathFromUrl(url);
2046
2234
  try {
2047
- const data = await readFile(filePath);
2235
+ const entry = await loadStaticAsset(filePath);
2048
2236
  const ext = extname(filePath).toLowerCase();
2049
- res.writeHead(200, {
2050
- "content-type": MIME[ext] ?? "application/octet-stream",
2051
- "cache-control": "no-cache",
2052
- });
2053
- res.end(data);
2237
+ const encoding = staticEncoding(req.headers["accept-encoding"], /^(?:\.(?:js|css|html|json|svg|txt|wasm|woff2?|ttf|otf))$/.test(ext));
2238
+ const relative = filePath.slice(WEB_DIR.length + 1).split(sep).join("/");
2239
+ res.setHeader("content-type", MIME[ext] ?? "application/octet-stream");
2240
+ res.setHeader("cache-control", /^assets\/mermaid\/chunks\/[^/]+-[a-zA-Z0-9]{8}\.js$/.test(relative)
2241
+ ? "public, max-age=31536000, immutable" : "no-cache");
2242
+ res.setHeader("vary", "Accept-Encoding");
2243
+ res.setHeader("etag", entry.etag);
2244
+ if (!encoding) {
2245
+ res.writeHead(406, { "content-length": 0 });
2246
+ res.end();
2247
+ return;
2248
+ }
2249
+ if (encoding !== "identity") res.setHeader("content-encoding", encoding);
2250
+ if ((req.method === "GET" || req.method === "HEAD") && staticEtagMatches(req.headers["if-none-match"], entry.etag)) {
2251
+ res.writeHead(304);
2252
+ res.end();
2253
+ return;
2254
+ }
2255
+ const data = await staticRepresentation(filePath, entry, encoding);
2256
+ res.setHeader("content-length", data.length);
2257
+ res.writeHead(200);
2258
+ res.end(req.method === "HEAD" ? undefined : data);
2054
2259
  } catch {
2055
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
2056
- res.end("not found");
2260
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "content-length": 9 });
2261
+ res.end(req.method === "HEAD" ? undefined : "not found");
2057
2262
  }
2058
2263
  }
2059
2264
 
@@ -2422,6 +2627,9 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
2422
2627
  }
2423
2628
 
2424
2629
  if (method === "GET" && path === "/api/events") {
2630
+ const sessionId = url.searchParams.get("sessionId") || undefined;
2631
+ const subscribedSession = sessionId ? sessions.get(sessionId) : undefined;
2632
+ if (sessionId && !subscribedSession) return sendJson(res, 404, { error: "session not found" });
2425
2633
  res.writeHead(200, {
2426
2634
  "content-type": "text/event-stream; charset=utf-8",
2427
2635
  "cache-control": "no-store",
@@ -2433,10 +2641,13 @@ async function handleApi(req: import("node:http").IncomingMessage, res: ServerRe
2433
2641
  const removeClient = () => {
2434
2642
  closeSseClient(client);
2435
2643
  sseClients.delete(client);
2644
+ if (sessionId && !hasSessionSubscribers(sessionId)) snapshotScheduler.cancel(sessionId);
2436
2645
  };
2437
2646
  client = createSseClient(res, removeClient);
2647
+ client.sessionId = sessionId;
2438
2648
  sseClients.add(client);
2439
2649
  req.on("close", removeClient);
2650
+ if (subscribedSession) sendSseEvent(client, sessionSnapshot(subscribedSession));
2440
2651
  return true;
2441
2652
  }
2442
2653
 
@@ -3299,7 +3510,6 @@ async function shutdownServer(): Promise<void> {
3299
3510
  await attachments.close();
3300
3511
  if (heartbeat) clearInterval(heartbeat);
3301
3512
  heartbeat = undefined;
3302
- await saveStateNow().catch(() => {});
3303
3513
  const activeServer = server;
3304
3514
  server = undefined;
3305
3515
  serverUrl = undefined;
@@ -3312,6 +3522,9 @@ async function shutdownServer(): Promise<void> {
3312
3522
  activeServer.close(() => resolveClose());
3313
3523
  });
3314
3524
  }
3525
+ scheduleSave.cancel();
3526
+ await stateSaveChain;
3527
+ await saveStateNow().catch(() => {});
3315
3528
  process.off("exit", exitHook);
3316
3529
  process.off("SIGINT", sigintHook);
3317
3530
  process.off("SIGTERM", sigintHook);
@@ -3385,7 +3598,7 @@ export async function startWebServer(options: {
3385
3598
  if (!handled) sendJson(res, 404, { error: "not found" });
3386
3599
  return;
3387
3600
  }
3388
- await serveStatic(res, url.pathname);
3601
+ await serveStatic(req, res, url.pathname);
3389
3602
  } catch (error) {
3390
3603
  if (!res.headersSent) sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
3391
3604
  else res.end();
package/sse-channel.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { canonicalizeJson, diffSessionSnapshots, type JsonValue } from "./web/session-sync.js";
2
+
1
3
  export type SseEvent = Record<string, unknown>;
2
4
 
3
5
  // Coalesce before serializing full histories, so a burst of tokens does not
@@ -44,6 +46,9 @@ export interface SseWritable {
44
46
 
45
47
  export interface SseClient {
46
48
  res: SseWritable;
49
+ sessionId?: string;
50
+ snapshotBaseline?: Record<string, JsonValue>;
51
+ snapshotRevision: number;
47
52
  waitingForDrain: boolean;
48
53
  pendingEvents: Map<string, SseEvent>;
49
54
  closed: boolean;
@@ -59,6 +64,7 @@ function eventKey(event: SseEvent): string {
59
64
  export function createSseClient(res: SseWritable, onError: () => void): SseClient {
60
65
  return {
61
66
  res,
67
+ snapshotRevision: 0,
62
68
  waitingForDrain: false,
63
69
  pendingEvents: new Map(),
64
70
  closed: false,
@@ -68,7 +74,24 @@ export function createSseClient(res: SseWritable, onError: () => void): SseClien
68
74
 
69
75
  function writeEvent(client: SseClient, event: SseEvent): boolean {
70
76
  try {
71
- const accepted = client.res.write(`data: ${JSON.stringify(event)}\n\n`);
77
+ let outgoing = event;
78
+ const snapshot = event.type === "snapshot" ? event as Record<string, JsonValue> : undefined;
79
+ if (snapshot) {
80
+ const revision = client.snapshotRevision + 1;
81
+ if (client.snapshotBaseline && client.snapshotBaseline.sessionId === snapshot.sessionId) {
82
+ const patches = diffSessionSnapshots(client.snapshotBaseline, snapshot);
83
+ if (patches.length === 0) return true;
84
+ outgoing = { type: "session_patch", sessionId: snapshot.sessionId, baseRevision: client.snapshotRevision, revision, patches };
85
+ } else {
86
+ outgoing = { ...snapshot, revision };
87
+ }
88
+ }
89
+ const accepted = client.res.write(`data: ${JSON.stringify(outgoing)}\n\n`);
90
+ // A false return still means Node accepted the bytes into its buffer.
91
+ if (snapshot) {
92
+ client.snapshotBaseline = snapshot;
93
+ client.snapshotRevision++;
94
+ }
72
95
  if (accepted) return true;
73
96
  client.waitingForDrain = true;
74
97
  client.res.once("drain", () => flushPendingEvents(client));
@@ -93,7 +116,18 @@ function flushPendingEvents(client: SseClient): void {
93
116
 
94
117
  export function sendSseEvent(client: SseClient, event: SseEvent): void {
95
118
  if (client.closed) return;
119
+ if (event.type === "snapshot") {
120
+ try {
121
+ const { revision: _revision, ...body } = event;
122
+ event = canonicalizeJson(body) as Record<string, JsonValue>;
123
+ } catch {
124
+ closeSseClient(client);
125
+ client.onError();
126
+ return;
127
+ }
128
+ }
96
129
  if (client.waitingForDrain) {
130
+ // Snapshot entries are detached full targets, diffed only when written.
97
131
  // Keep only the newest event of each kind while the browser catches up.
98
132
  // This bounds memory even when snapshots contain a long conversation.
99
133
  client.pendingEvents.set(eventKey(event), event);
@@ -106,4 +140,6 @@ export function closeSseClient(client: SseClient): void {
106
140
  client.closed = true;
107
141
  client.waitingForDrain = false;
108
142
  client.pendingEvents.clear();
143
+ client.snapshotBaseline = undefined;
144
+ client.snapshotRevision = 0;
109
145
  }
package/web/app.css CHANGED
@@ -284,6 +284,14 @@ body[data-workspace-dragging] .YDXeBa_projectRow[draggable=true]{cursor:grabbing
284
284
  ._markdown_1nba0_5 .md-code-container>.md-code-block{border:0;border-radius:0;margin:0}
285
285
  ._markdown_1nba0_5 .md-code-block code{font:inherit;white-space:inherit}
286
286
  ._markdown_1nba0_5 .md-table-wrap{max-width:100%;overflow-x:auto}
287
+ ._markdown_1nba0_5 .katex{font-size:1.1em;overflow-wrap:normal;word-break:normal}
288
+ ._markdown_1nba0_5 .katex-display{max-width:100%;margin:16px 0;padding:4px 0;overflow:auto hidden;overscroll-behavior-inline:contain}
289
+ ._markdown_1nba0_5 .md-mermaid{min-width:0;max-width:100%;margin:16px 0;overflow-x:auto;overscroll-behavior-inline:contain}
290
+ ._markdown_1nba0_5 .md-mermaid>svg{display:block;max-width:100%;height:auto;margin:0 auto}
291
+ ._markdown_1nba0_5 .md-mermaid pre{margin:0}
292
+ ._markdown_1nba0_5 eq{display:inline}
293
+ ._markdown_1nba0_5 .eqno{display:flex;align-items:center;gap:12px;max-width:100%}
294
+ ._markdown_1nba0_5 eqn{display:block;min-width:0;flex:1}
287
295
  [data-chat-flow-kind=assistant-text] .md-table-wrap{max-width:100%;margin:18px 0;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-base);overflow-x:auto;overscroll-behavior-inline:contain}
288
296
  [data-chat-flow-kind=assistant-text] .md-table-wrap table{width:max-content;min-width:100%;border-collapse:separate;border-spacing:0;font-size:14px;line-height:1.55}
289
297
  [data-chat-flow-kind=assistant-text] .md-table-wrap :is(th,td){box-sizing:border-box;min-width:112px;max-width:360px;padding:9px 12px;text-align:left;vertical-align:top;border-right:1px solid var(--dsw-alias-border-l2);border-bottom:1px solid var(--dsw-alias-border-l2);overflow-wrap:anywhere}
@@ -730,7 +738,7 @@ body[data-ds-dark-theme] :is(.pi-filePreview,._markdown_1nba0_5) .hljs-meta,body
730
738
  .pi-loginCard .pi-settingsAction{width:100%;min-height:44px}
731
739
  .pi-lanUrl{box-sizing:border-box;width:100%;min-width:0;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:12px/20px var(--ds-font-family-code)}
732
740
  .pi-copyField{box-sizing:border-box;width:100%;min-height:160px;max-height:40vh;margin:12px 0;padding:10px;resize:vertical;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:16px/24px var(--dsw-font-family)}
733
- @media(pointer:coarse){
741
+ @media(max-width:640px),(pointer:coarse){
734
742
  #root{box-sizing:border-box;position:fixed;top:var(--pi-viewport-top,0px);left:0;right:0;height:var(--pi-viewport-height,100dvh);padding:env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left)}
735
743
  .wSkVaW_root,.wSkVaW_composerSeat{--dsh-composer-text-max-height:min(200px,calc(var(--pi-viewport-height,100dvh) * .3))}
736
744
  .wSkVaW_composerSeat{padding-bottom:env(safe-area-inset-bottom)}