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

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 (184) hide show
  1. package/README.md +2 -2
  2. package/THIRD_PARTY_NOTICES.md +32 -3
  3. package/package.json +4 -1
  4. package/server.ts +190 -17
  5. package/web/app.css +9 -1
  6. package/web/app.js +7 -3
  7. package/web/assets/fonts/KaTeX_AMS-Regular.ttf +0 -0
  8. package/web/assets/fonts/KaTeX_AMS-Regular.woff +0 -0
  9. package/web/assets/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  10. package/web/assets/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  11. package/web/assets/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  12. package/web/assets/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  13. package/web/assets/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  14. package/web/assets/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  15. package/web/assets/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  16. package/web/assets/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  17. package/web/assets/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  18. package/web/assets/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  19. package/web/assets/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  20. package/web/assets/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  21. package/web/assets/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  22. package/web/assets/fonts/KaTeX_Main-Bold.ttf +0 -0
  23. package/web/assets/fonts/KaTeX_Main-Bold.woff +0 -0
  24. package/web/assets/fonts/KaTeX_Main-Bold.woff2 +0 -0
  25. package/web/assets/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  26. package/web/assets/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  27. package/web/assets/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  28. package/web/assets/fonts/KaTeX_Main-Italic.ttf +0 -0
  29. package/web/assets/fonts/KaTeX_Main-Italic.woff +0 -0
  30. package/web/assets/fonts/KaTeX_Main-Italic.woff2 +0 -0
  31. package/web/assets/fonts/KaTeX_Main-Regular.ttf +0 -0
  32. package/web/assets/fonts/KaTeX_Main-Regular.woff +0 -0
  33. package/web/assets/fonts/KaTeX_Main-Regular.woff2 +0 -0
  34. package/web/assets/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  35. package/web/assets/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  36. package/web/assets/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  37. package/web/assets/fonts/KaTeX_Math-Italic.ttf +0 -0
  38. package/web/assets/fonts/KaTeX_Math-Italic.woff +0 -0
  39. package/web/assets/fonts/KaTeX_Math-Italic.woff2 +0 -0
  40. package/web/assets/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  41. package/web/assets/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  42. package/web/assets/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  43. package/web/assets/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  44. package/web/assets/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  45. package/web/assets/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  46. package/web/assets/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  47. package/web/assets/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  48. package/web/assets/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  49. package/web/assets/fonts/KaTeX_Script-Regular.ttf +0 -0
  50. package/web/assets/fonts/KaTeX_Script-Regular.woff +0 -0
  51. package/web/assets/fonts/KaTeX_Script-Regular.woff2 +0 -0
  52. package/web/assets/fonts/KaTeX_Size1-Regular.ttf +0 -0
  53. package/web/assets/fonts/KaTeX_Size1-Regular.woff +0 -0
  54. package/web/assets/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  55. package/web/assets/fonts/KaTeX_Size2-Regular.ttf +0 -0
  56. package/web/assets/fonts/KaTeX_Size2-Regular.woff +0 -0
  57. package/web/assets/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  58. package/web/assets/fonts/KaTeX_Size3-Regular.ttf +0 -0
  59. package/web/assets/fonts/KaTeX_Size3-Regular.woff +0 -0
  60. package/web/assets/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  61. package/web/assets/fonts/KaTeX_Size4-Regular.ttf +0 -0
  62. package/web/assets/fonts/KaTeX_Size4-Regular.woff +0 -0
  63. package/web/assets/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  64. package/web/assets/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  65. package/web/assets/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  66. package/web/assets/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  67. package/web/assets/katex.min.css +1 -0
  68. package/web/assets/markdown.LICENSE.txt +78 -0
  69. package/web/assets/markdown.js +536 -21
  70. package/web/assets/mermaid/chunks/abnfDiagram-YUKMZFIV-FZYDJU67.js +1 -0
  71. package/web/assets/mermaid/chunks/architecture-WOLXFQ4H-BZVX64AA.js +1 -0
  72. package/web/assets/mermaid/chunks/architectureDiagram-47P4ROYG-2BMC2Z7S.js +36 -0
  73. package/web/assets/mermaid/chunks/blockDiagram-E7TT5TSB-FH6ZULKF.js +140 -0
  74. package/web/assets/mermaid/chunks/c4Diagram-CRA5TL53-OG62EAHM.js +38 -0
  75. package/web/assets/mermaid/chunks/chunk-22D53RF2.js +2 -0
  76. package/web/assets/mermaid/chunks/chunk-25KVF5ZE.js +1 -0
  77. package/web/assets/mermaid/chunks/chunk-4HYFSIZT.js +1 -0
  78. package/web/assets/mermaid/chunks/chunk-4W4OQ3QY.js +1 -0
  79. package/web/assets/mermaid/chunks/chunk-5TQRX2ND.js +1 -0
  80. package/web/assets/mermaid/chunks/chunk-64OFLKGX.js +322 -0
  81. package/web/assets/mermaid/chunks/chunk-64OFLKGX.js.LEGAL.txt +17 -0
  82. package/web/assets/mermaid/chunks/chunk-6UFWKTSA.js +1 -0
  83. package/web/assets/mermaid/chunks/chunk-6ZAAIG6Q.js +10 -0
  84. package/web/assets/mermaid/chunks/chunk-76QEHT4T.js +1 -0
  85. package/web/assets/mermaid/chunks/chunk-A7665WVC.js +1 -0
  86. package/web/assets/mermaid/chunks/chunk-AECJS4LP.js +130 -0
  87. package/web/assets/mermaid/chunks/chunk-AECJS4LP.js.LEGAL.txt +5 -0
  88. package/web/assets/mermaid/chunks/chunk-ATQQWCXO.js +2 -0
  89. package/web/assets/mermaid/chunks/chunk-DQVO43GU.js +1 -0
  90. package/web/assets/mermaid/chunks/chunk-DSKRGV6V.js +1 -0
  91. package/web/assets/mermaid/chunks/chunk-EIBFAW37.js +1 -0
  92. package/web/assets/mermaid/chunks/chunk-FBC3PYSO.js +1 -0
  93. package/web/assets/mermaid/chunks/chunk-FQ5IIL54.js +88 -0
  94. package/web/assets/mermaid/chunks/chunk-GZYY6JMP.js +1 -0
  95. package/web/assets/mermaid/chunks/chunk-HIGPS4L2.js +1 -0
  96. package/web/assets/mermaid/chunks/chunk-HP75Y56I.js +2 -0
  97. package/web/assets/mermaid/chunks/chunk-JRAHLUQM.js +32 -0
  98. package/web/assets/mermaid/chunks/chunk-KNYDPLLK.js +1 -0
  99. package/web/assets/mermaid/chunks/chunk-L465623K.js +136 -0
  100. package/web/assets/mermaid/chunks/chunk-L7BMR7WL.js +1 -0
  101. package/web/assets/mermaid/chunks/chunk-LLGPMHB6.js +1 -0
  102. package/web/assets/mermaid/chunks/chunk-NBH7YOBD.js +15 -0
  103. package/web/assets/mermaid/chunks/chunk-NCOVM3YF.js +1 -0
  104. package/web/assets/mermaid/chunks/chunk-PHKIAKHS.js +1 -0
  105. package/web/assets/mermaid/chunks/chunk-PM2IFX5O.js +1 -0
  106. package/web/assets/mermaid/chunks/chunk-PN62QGJW.js +72 -0
  107. package/web/assets/mermaid/chunks/chunk-QHMFFOZZ.js +1 -0
  108. package/web/assets/mermaid/chunks/chunk-QQZBUVKZ.js +1 -0
  109. package/web/assets/mermaid/chunks/chunk-RV7WQMNL.js +1 -0
  110. package/web/assets/mermaid/chunks/chunk-STBOPOHX.js +1 -0
  111. package/web/assets/mermaid/chunks/chunk-T3FZWEYV.js +1 -0
  112. package/web/assets/mermaid/chunks/chunk-TMMWB6NQ.js +1 -0
  113. package/web/assets/mermaid/chunks/chunk-TPW5CADB.js +1 -0
  114. package/web/assets/mermaid/chunks/chunk-TSOLLCID.js +2 -0
  115. package/web/assets/mermaid/chunks/chunk-TWGPYWIY.js +1 -0
  116. package/web/assets/mermaid/chunks/chunk-WGMEMLED.js +1 -0
  117. package/web/assets/mermaid/chunks/chunk-XC2LKMMW.js +1 -0
  118. package/web/assets/mermaid/chunks/chunk-XHVVPFJ5.js +1 -0
  119. package/web/assets/mermaid/chunks/chunk-YIN5CSIF.js +2 -0
  120. package/web/assets/mermaid/chunks/chunk-YIN5CSIF.js.LEGAL.txt +12 -0
  121. package/web/assets/mermaid/chunks/chunk-YORIST7Z.js +1 -0
  122. package/web/assets/mermaid/chunks/chunk-YPXVLPXW.js +3 -0
  123. package/web/assets/mermaid/chunks/chunk-Z4WODCD2.js +213 -0
  124. package/web/assets/mermaid/chunks/chunk-ZAERIUBF.js +1 -0
  125. package/web/assets/mermaid/chunks/chunk-ZDOBOOWG.js +62 -0
  126. package/web/assets/mermaid/chunks/classDiagram-v2-K4WV4PDN-HXQD675C.js +217 -0
  127. package/web/assets/mermaid/chunks/cose-bilkent-JH36ORCC-3VHNHM7I.js +1 -0
  128. package/web/assets/mermaid/chunks/cynefin-EF2NZ3EQ-ZO5PPXJG.js +1 -0
  129. package/web/assets/mermaid/chunks/cynefinDiagram-3GCD6N5R-LFWDWE4E.js +62 -0
  130. package/web/assets/mermaid/chunks/dagre-W4DXFKR2-SDBBVBGG.js +4 -0
  131. package/web/assets/mermaid/chunks/diagram-2UJZ2QOL-YCKW3WNJ.js +200 -0
  132. package/web/assets/mermaid/chunks/diagram-OVF4WLC6-62NNPE34.js +24 -0
  133. package/web/assets/mermaid/chunks/diagram-PFPMY2P6-7RR2GTZ5.js +30 -0
  134. package/web/assets/mermaid/chunks/diagram-UMYRVEAY-Q4I25SMR.js +24 -0
  135. package/web/assets/mermaid/chunks/diagram-YEKJPTXX-VNUTOPRK.js +41 -0
  136. package/web/assets/mermaid/chunks/diagram-ZIFT7M5P-KALXX5CE.js +3 -0
  137. package/web/assets/mermaid/chunks/ebnfDiagram-VR2GEFS7-WIFBISHL.js +1 -0
  138. package/web/assets/mermaid/chunks/elk-IJKZMXRS-LGG232IL.js +27 -0
  139. package/web/assets/mermaid/chunks/erDiagram-O2IAPWRE-KWX2J2TX.js +99 -0
  140. package/web/assets/mermaid/chunks/eventmodeling-K75KTNOO-3JNV4VSP.js +1 -0
  141. package/web/assets/mermaid/chunks/flowDiagram-OXPTDLAJ-CUMOBDC3.js +1 -0
  142. package/web/assets/mermaid/chunks/ganttDiagram-R7TSEDQI-GCS3MRFX.js +292 -0
  143. package/web/assets/mermaid/chunks/gitGraph-VSP46ZUC-HSAXKLAG.js +1 -0
  144. package/web/assets/mermaid/chunks/gitGraphDiagram-XJZIOB7I-FB763ROZ.js +106 -0
  145. package/web/assets/mermaid/chunks/info-OHQRW6UA-C4R2B34T.js +1 -0
  146. package/web/assets/mermaid/chunks/infoDiagram-5W2HQ5XZ-XCQU5T5D.js +2 -0
  147. package/web/assets/mermaid/chunks/ishikawaDiagram-K3B6WC7H-UWYT66G7.js +70 -0
  148. package/web/assets/mermaid/chunks/journeyDiagram-COXZFDF6-DOW66RYW.js +139 -0
  149. package/web/assets/mermaid/chunks/kanban-definition-P3RFRI5V-K55BXDLO.js +89 -0
  150. package/web/assets/mermaid/chunks/katex-5AUD5AJN.js +257 -0
  151. package/web/assets/mermaid/chunks/mindmap-definition-LZFPQGKD-WG3FIZ5U.js +96 -0
  152. package/web/assets/mermaid/chunks/packet-JDAUHWVQ-SFUSDIUN.js +1 -0
  153. package/web/assets/mermaid/chunks/pegDiagram-BGZESJAR-MQOH3WII.js +1 -0
  154. package/web/assets/mermaid/chunks/pie-XZMESJXO-M76QC4CD.js +1 -0
  155. package/web/assets/mermaid/chunks/pieDiagram-CAPJLFHJ-V3WNR4Y2.js +39 -0
  156. package/web/assets/mermaid/chunks/quadrantDiagram-GDMTTRAM-DJLTATHS.js +7 -0
  157. package/web/assets/mermaid/chunks/radar-ABXABTNO-HSBJ6SSD.js +1 -0
  158. package/web/assets/mermaid/chunks/railroad-I3PHUGI6-4QJMJ7LG.js +1 -0
  159. package/web/assets/mermaid/chunks/railroad-abnf-LNEFI6M7-ZLNZ5ZRH.js +1 -0
  160. package/web/assets/mermaid/chunks/railroad-ebnf-DWYD2UWJ-EBV6ZOUW.js +1 -0
  161. package/web/assets/mermaid/chunks/railroad-peg-JR7BVNR7-X3E47TO7.js +1 -0
  162. package/web/assets/mermaid/chunks/railroadDiagram-SM67HX2B-ZHEERYJS.js +1 -0
  163. package/web/assets/mermaid/chunks/requirementDiagram-X7JNWC4A-TVHJNPPI.js +84 -0
  164. package/web/assets/mermaid/chunks/sankeyDiagram-UM26HJRW-FXHWY3MM.js +40 -0
  165. package/web/assets/mermaid/chunks/sequenceDiagram-ZO4K6R2Y-OP7DCVPK.js +169 -0
  166. package/web/assets/mermaid/chunks/sizeCapture-INFHLROL-2GUJIKNX.js +1 -0
  167. package/web/assets/mermaid/chunks/stateDiagram-v2-TBUQTH76-R26D6XHK.js +291 -0
  168. package/web/assets/mermaid/chunks/swimlanes-N4OXWK64-FPGACWHE.js +1 -0
  169. package/web/assets/mermaid/chunks/swimlanesDiagram-K3J5GTZL-YMNX34Q2.js +8 -0
  170. package/web/assets/mermaid/chunks/timeline-definition-YOQAKGHF-R2U2UR4K.js +120 -0
  171. package/web/assets/mermaid/chunks/treeView-D4JQ5SDB-3BQHZUCI.js +1 -0
  172. package/web/assets/mermaid/chunks/treemap-SAJKECNS-6NJPJ5NM.js +1 -0
  173. package/web/assets/mermaid/chunks/usecaseDiagram-VIAY4XPW-BCEIGXDX.js +370 -0
  174. package/web/assets/mermaid/chunks/vennDiagram-BLWOH2XV-DOV5W2IV.js +34 -0
  175. package/web/assets/mermaid/chunks/wardley-7MLQ67FV-ITOM4KQ2.js +1 -0
  176. package/web/assets/mermaid/chunks/wardleyDiagram-YMQ3BMBF-CGJ7AG4L.js +78 -0
  177. package/web/assets/mermaid/chunks/xychartDiagram-TAQBALBS-2MTP7IBV.js +7 -0
  178. package/web/assets/mermaid/mermaid.js +11 -0
  179. package/web/assets/mermaid/mermaid.js.LEGAL.txt +7 -0
  180. package/web/index.html +1 -0
  181. package/web/markdown-diagrams.js +147 -0
  182. package/web/markdown.ts +41 -2
  183. package/web/slash-commands.js +1 -0
  184. 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
 
@@ -61,7 +61,7 @@ npm run verify
61
61
 
62
62
  ## 简体中文
63
63
 
64
- Pi Web 为 [Pi Coding Agent](https://pi.dev) 提供浏览器界面,灵感来自 DeepSeek Harness。在桌面或手机上切换多个工作区与会话,查看流式回答、工具调用和文件预览,浏览会话分支,并使用模型、斜杠命令、扩展与技能。Pi 和工具仍运行在自己的电脑上。支持明暗主题和自定义聊天背景。
64
+ Pi Web 为 [Pi Coding Agent](https://pi.dev) 提供浏览器界面,灵感来自 DeepSeek Harness。在桌面或手机上切换多个工作区与会话,查看流式回答、工具调用和文件预览,浏览会话分支,并使用模型、斜杠命令、扩展与技能。Pi 和工具仍运行在自己的电脑上。支持明暗主题和自定义聊天背景。回答和 Markdown 文件预览支持数学公式与 Mermaid 图表,图表模块按需加载。
65
65
 
66
66
  ### 系统要求
67
67
 
@@ -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.3",
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";
@@ -242,6 +244,8 @@ let heartbeat: ReturnType<typeof setInterval> | undefined;
242
244
  const MAX_IDLE_PROCESSES = 1;
243
245
  const modelInfoCache = new Map<string, ModelInfo>();
244
246
  const modelCatalogRefreshes = new Map<string, Promise<ModelCatalog>>();
247
+ const recentModelCatalogs = new Map<string, { catalog: ModelCatalog; refreshedAt: number }>();
248
+ const MODEL_CATALOG_REUSE_MS = 30_000;
245
249
  const commandCache = new Map<string, SlashCommandInfo[]>();
246
250
  const exportDownloads = new Map<string, ExportDownload>();
247
251
  const attachments = new AttachmentStore();
@@ -517,13 +521,23 @@ function isImmediateExtensionCommand(record: RuntimeSession, message: string): b
517
521
 
518
522
  function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number) {
519
523
  let timer: ReturnType<typeof setTimeout> | undefined;
520
- return (...args: A) => {
524
+ const schedule = (...args: A) => {
521
525
  if (timer) clearTimeout(timer);
522
- timer = setTimeout(() => fn(...args), ms);
526
+ timer = setTimeout(() => {
527
+ timer = undefined;
528
+ fn(...args);
529
+ }, ms);
523
530
  };
531
+ return Object.assign(schedule, {
532
+ cancel() {
533
+ if (timer) clearTimeout(timer);
534
+ timer = undefined;
535
+ },
536
+ });
524
537
  }
525
538
 
526
539
  const scheduleSave = debounce(() => {
540
+ if (shuttingDown) return;
527
541
  stateSaveChain = stateSaveChain.then(saveStateNow).catch(() => {});
528
542
  }, 120);
529
543
 
@@ -545,6 +559,7 @@ async function loadState(): Promise<void> {
545
559
  }) ?? { version: 1, workspaces: [], sessions: [] };
546
560
  sessions = new Map();
547
561
  modelInfoCache.clear();
562
+ recentModelCatalogs.clear();
548
563
  commandCache.clear();
549
564
  if (state.modelCache) {
550
565
  for (const [cwd, cached] of Object.entries(state.modelCache)) {
@@ -917,12 +932,26 @@ async function discoverFreshModelCatalog(cwd: string): Promise<ModelCatalog> {
917
932
  })();
918
933
  modelCatalogRefreshes.set(key, task);
919
934
  try {
920
- return await task;
935
+ const catalog = await task;
936
+ recentModelCatalogs.set(key, { catalog, refreshedAt: Date.now() });
937
+ return catalog;
921
938
  } finally {
922
939
  modelCatalogRefreshes.delete(key);
923
940
  }
924
941
  }
925
942
 
943
+ async function modelCatalogForNewSession(cwd: string, preferredModel?: unknown): Promise<ModelCatalog> {
944
+ const key = normalizePath(cwd);
945
+ const refreshing = modelCatalogRefreshes.get(key);
946
+ if (refreshing) return refreshing;
947
+ const recent = recentModelCatalogs.get(key);
948
+ if (recent && Date.now() - recent.refreshedAt < MODEL_CATALOG_REUSE_MS
949
+ && (!preferredModel || findCatalogModel(recent.catalog.models, preferredModel))) {
950
+ return recent.catalog;
951
+ }
952
+ return discoverFreshModelCatalog(cwd);
953
+ }
954
+
926
955
  function applyModelInfo(record: RuntimeSession, info: ModelInfo): void {
927
956
  record.models = info.models;
928
957
  record.thinkingLevels = info.thinkingLevels;
@@ -1591,7 +1620,7 @@ async function createSession(
1591
1620
  const ws = state.workspaces.find((w) => w.id === workspaceId);
1592
1621
  if (!ws) throw new Error("workspace not found");
1593
1622
  const info = modelInfoFromCatalog(
1594
- await discoverFreshModelCatalog(ws.path),
1623
+ await modelCatalogForNewSession(ws.path, preferences.model),
1595
1624
  preferences.model,
1596
1625
  preferences.thinkingLevel,
1597
1626
  );
@@ -2041,19 +2070,161 @@ function filePathFromUrl(url: string): string {
2041
2070
  return path;
2042
2071
  }
2043
2072
 
2044
- async function serveStatic(res: ServerResponse, url: string): Promise<void> {
2073
+ type StaticEncoding = "br" | "gzip" | "identity";
2074
+ interface StaticAsset {
2075
+ metadata: string;
2076
+ data: Buffer;
2077
+ etag: string;
2078
+ bytes: number;
2079
+ compressed: Partial<Record<"br" | "gzip", Buffer>>;
2080
+ compressing: Partial<Record<"br" | "gzip", Promise<Buffer>>>;
2081
+ }
2082
+
2083
+ const STATIC_CACHE_LIMIT = 32 * 1024 * 1024;
2084
+ const STATIC_ENTRY_LIMIT = 8 * 1024 * 1024;
2085
+ const staticAssets = new Map<string, StaticAsset>();
2086
+ const staticReads = new Map<string, Promise<StaticAsset>>();
2087
+ let staticCacheBytes = 0;
2088
+ const compressBrotliStatic = promisify(brotliCompress);
2089
+ const compressStatic = {
2090
+ br: (data: Buffer) => compressBrotliStatic(data, { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 4 } }),
2091
+ gzip: promisify(gzip),
2092
+ };
2093
+
2094
+ function evictStaticAsset(path: string): void {
2095
+ const entry = staticAssets.get(path);
2096
+ if (entry) staticCacheBytes -= entry.bytes;
2097
+ staticAssets.delete(path);
2098
+ }
2099
+
2100
+ function trimStaticCache(): void {
2101
+ while (staticCacheBytes > STATIC_CACHE_LIMIT || staticAssets.size > 256) {
2102
+ evictStaticAsset(staticAssets.keys().next().value!);
2103
+ }
2104
+ }
2105
+
2106
+ function staticMetadata(info: Awaited<ReturnType<typeof stat>>): string {
2107
+ return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
2108
+ }
2109
+
2110
+ async function loadStaticAsset(path: string): Promise<StaticAsset> {
2111
+ // Revalidate metadata even for cached entries so local rebuilds are visible immediately.
2112
+ for (let attempt = 0; attempt < 3; attempt++) {
2113
+ const info = await stat(path);
2114
+ if (!info.isFile()) throw new Error("not a file");
2115
+ const metadata = staticMetadata(info);
2116
+ const cached = staticAssets.get(path);
2117
+ if (cached?.metadata === metadata) {
2118
+ staticAssets.delete(path);
2119
+ staticAssets.set(path, cached);
2120
+ return cached;
2121
+ }
2122
+ evictStaticAsset(path);
2123
+ const key = `${path}\0${metadata}`;
2124
+ let reading = staticReads.get(key);
2125
+ if (!reading) {
2126
+ reading = (async () => {
2127
+ const data = await readFile(path);
2128
+ if (staticMetadata(await stat(path)) !== metadata) throw new Error("static asset changed during read");
2129
+ const entry: StaticAsset = {
2130
+ metadata, data, etag: `W/"${createHash("sha256").update(data).digest("hex")}"`,
2131
+ bytes: data.length, compressed: {}, compressing: {},
2132
+ };
2133
+ if (entry.bytes <= STATIC_ENTRY_LIMIT) {
2134
+ evictStaticAsset(path);
2135
+ staticAssets.set(path, entry);
2136
+ staticCacheBytes += entry.bytes;
2137
+ trimStaticCache();
2138
+ }
2139
+ return entry;
2140
+ })();
2141
+ staticReads.set(key, reading);
2142
+ }
2143
+ try {
2144
+ return await reading;
2145
+ } catch (error) {
2146
+ if (attempt === 2) throw error;
2147
+ } finally {
2148
+ if (staticReads.get(key) === reading) staticReads.delete(key);
2149
+ }
2150
+ }
2151
+ throw new Error("unable to read static asset");
2152
+ }
2153
+
2154
+ function staticEncoding(header: string | undefined, compressible: boolean): StaticEncoding | undefined {
2155
+ if (!header?.trim()) return "identity";
2156
+ const qualities = new Map<string, number>();
2157
+ for (const part of header.split(",")) {
2158
+ const [name, ...parameters] = part.trim().toLowerCase().split(";");
2159
+ if (!name) continue;
2160
+ const q = parameters.map((parameter) => parameter.trim()).find((parameter) => parameter.startsWith("q="));
2161
+ const value = q === undefined ? 1 : Number(q.slice(2));
2162
+ qualities.set(name, Number.isFinite(value) && value >= 0 && value <= 1 ? value : 0);
2163
+ }
2164
+ const quality = (encoding: StaticEncoding): number => qualities.get(encoding)
2165
+ ?? (encoding === "identity" ? (qualities.get("*") === 0 ? 0 : 1) : (qualities.get("*") ?? 0));
2166
+ const encodings: StaticEncoding[] = compressible ? ["br", "gzip", "identity"] : ["identity"];
2167
+ return encodings.filter((encoding) => quality(encoding) > 0)
2168
+ .sort((a, b) => quality(b) - quality(a))[0];
2169
+ }
2170
+
2171
+ async function staticRepresentation(path: string, entry: StaticAsset, encoding: StaticEncoding): Promise<Buffer> {
2172
+ if (encoding === "identity") return entry.data;
2173
+ const cached = entry.compressed[encoding];
2174
+ if (cached) return cached;
2175
+ let compressing = entry.compressing[encoding];
2176
+ if (!compressing) {
2177
+ compressing = compressStatic[encoding](entry.data).then((data) => {
2178
+ if (staticAssets.get(path) === entry && entry.bytes + data.length <= STATIC_ENTRY_LIMIT) {
2179
+ entry.compressed[encoding] = data;
2180
+ entry.bytes += data.length;
2181
+ staticCacheBytes += data.length;
2182
+ trimStaticCache();
2183
+ }
2184
+ return data;
2185
+ }).finally(() => { delete entry.compressing[encoding]; });
2186
+ entry.compressing[encoding] = compressing;
2187
+ }
2188
+ return compressing;
2189
+ }
2190
+
2191
+ function staticEtagMatches(header: string | undefined, etag: string): boolean {
2192
+ return Boolean(header?.split(",").some((candidate) => {
2193
+ const tag = candidate.trim();
2194
+ return tag === "*" || tag.replace(/^W\//, "") === etag.replace(/^W\//, "");
2195
+ }));
2196
+ }
2197
+
2198
+ async function serveStatic(req: IncomingMessage, res: ServerResponse, url: string): Promise<void> {
2045
2199
  const filePath = filePathFromUrl(url);
2046
2200
  try {
2047
- const data = await readFile(filePath);
2201
+ const entry = await loadStaticAsset(filePath);
2048
2202
  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);
2203
+ const encoding = staticEncoding(req.headers["accept-encoding"], /^(?:\.(?:js|css|html|json|svg|txt|wasm|woff2?|ttf|otf))$/.test(ext));
2204
+ const relative = filePath.slice(WEB_DIR.length + 1).split(sep).join("/");
2205
+ res.setHeader("content-type", MIME[ext] ?? "application/octet-stream");
2206
+ res.setHeader("cache-control", /^assets\/mermaid\/chunks\/[^/]+-[a-zA-Z0-9]{8}\.js$/.test(relative)
2207
+ ? "public, max-age=31536000, immutable" : "no-cache");
2208
+ res.setHeader("vary", "Accept-Encoding");
2209
+ res.setHeader("etag", entry.etag);
2210
+ if (!encoding) {
2211
+ res.writeHead(406, { "content-length": 0 });
2212
+ res.end();
2213
+ return;
2214
+ }
2215
+ if (encoding !== "identity") res.setHeader("content-encoding", encoding);
2216
+ if ((req.method === "GET" || req.method === "HEAD") && staticEtagMatches(req.headers["if-none-match"], entry.etag)) {
2217
+ res.writeHead(304);
2218
+ res.end();
2219
+ return;
2220
+ }
2221
+ const data = await staticRepresentation(filePath, entry, encoding);
2222
+ res.setHeader("content-length", data.length);
2223
+ res.writeHead(200);
2224
+ res.end(req.method === "HEAD" ? undefined : data);
2054
2225
  } catch {
2055
- res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
2056
- res.end("not found");
2226
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8", "content-length": 9 });
2227
+ res.end(req.method === "HEAD" ? undefined : "not found");
2057
2228
  }
2058
2229
  }
2059
2230
 
@@ -3299,7 +3470,6 @@ async function shutdownServer(): Promise<void> {
3299
3470
  await attachments.close();
3300
3471
  if (heartbeat) clearInterval(heartbeat);
3301
3472
  heartbeat = undefined;
3302
- await saveStateNow().catch(() => {});
3303
3473
  const activeServer = server;
3304
3474
  server = undefined;
3305
3475
  serverUrl = undefined;
@@ -3312,6 +3482,9 @@ async function shutdownServer(): Promise<void> {
3312
3482
  activeServer.close(() => resolveClose());
3313
3483
  });
3314
3484
  }
3485
+ scheduleSave.cancel();
3486
+ await stateSaveChain;
3487
+ await saveStateNow().catch(() => {});
3315
3488
  process.off("exit", exitHook);
3316
3489
  process.off("SIGINT", sigintHook);
3317
3490
  process.off("SIGTERM", sigintHook);
@@ -3385,7 +3558,7 @@ export async function startWebServer(options: {
3385
3558
  if (!handled) sendJson(res, 404, { error: "not found" });
3386
3559
  return;
3387
3560
  }
3388
- await serveStatic(res, url.pathname);
3561
+ await serveStatic(req, res, url.pathname);
3389
3562
  } catch (error) {
3390
3563
  if (!res.headersSent) sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
3391
3564
  else res.end();
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)}
package/web/app.js CHANGED
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
 
3
3
  import { markdown as renderMarkdown } from "./assets/markdown.js";
4
+ import { observeMarkdownDiagrams } from "./markdown-diagrams.js";
5
+
6
+ observeMarkdownDiagrams();
4
7
  import { webSlashCommands, unsupportedSlashCommands, parseSlashCommand } from "./slash-commands.js";
5
8
  import { getLanguage, setLanguage, t, translateUi, observeUi } from "./i18n.js";
6
9
 
@@ -437,7 +440,6 @@ function usesMobileFilePreview() {
437
440
 
438
441
  function updateViewport() {
439
442
  const viewport = window.visualViewport;
440
- if (viewport && viewport.scale !== 1) return;
441
443
  document.documentElement.style.setProperty("--pi-viewport-height", `${viewport?.height || window.innerHeight}px`);
442
444
  document.documentElement.style.setProperty("--pi-viewport-top", `${viewport?.offsetTop || 0}px`);
443
445
  }
@@ -5267,7 +5269,9 @@ function connectEvents() {
5267
5269
  if (!event.sessionId) return;
5268
5270
  S.snapshots.set(event.sessionId, { ...snapshotFor(event.sessionId), ...event });
5269
5271
  const current = event.sessionId === S.currentSessionId;
5270
- queueRender(false, { sidebar: false, conversation: current, overlay: current });
5272
+ // Settings do not depend on session snapshots. Rebuilding the panel on
5273
+ // every streamed chunk replaces controls between pointerdown and click.
5274
+ queueRender(false, { sidebar: false, conversation: current, overlay: false });
5271
5275
  return;
5272
5276
  }
5273
5277
  if (event.type === "workspaces") {
@@ -5290,7 +5294,7 @@ function connectEvents() {
5290
5294
  }
5291
5295
  }
5292
5296
  if (unchanged && !conversationChanged) return;
5293
- queueRender(false, { sidebar: true, conversation: conversationChanged, overlay: conversationChanged });
5297
+ queueRender(false, { sidebar: true, conversation: conversationChanged, overlay: false });
5294
5298
  }
5295
5299
  } catch {
5296
5300
  // ignore malformed event