@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.
- package/README.md +10 -2
- package/THIRD_PARTY_NOTICES.md +32 -3
- package/package.json +4 -1
- package/server.ts +234 -21
- package/sse-channel.ts +37 -1
- package/web/app.css +9 -1
- package/web/app.js +96 -25
- package/web/assets/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/web/assets/fonts/KaTeX_Main-Bold.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/web/assets/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/web/assets/fonts/KaTeX_Main-Italic.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Main-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/web/assets/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/web/assets/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/web/assets/fonts/KaTeX_Math-Italic.woff +0 -0
- package/web/assets/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Script-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/web/assets/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/web/assets/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/web/assets/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/web/assets/katex.min.css +1 -0
- package/web/assets/markdown.LICENSE.txt +78 -0
- package/web/assets/markdown.js +536 -21
- package/web/assets/mermaid/chunks/abnfDiagram-YUKMZFIV-FZYDJU67.js +1 -0
- package/web/assets/mermaid/chunks/architecture-WOLXFQ4H-BZVX64AA.js +1 -0
- package/web/assets/mermaid/chunks/architectureDiagram-47P4ROYG-2BMC2Z7S.js +36 -0
- package/web/assets/mermaid/chunks/blockDiagram-E7TT5TSB-FH6ZULKF.js +140 -0
- package/web/assets/mermaid/chunks/c4Diagram-CRA5TL53-OG62EAHM.js +38 -0
- package/web/assets/mermaid/chunks/chunk-22D53RF2.js +2 -0
- package/web/assets/mermaid/chunks/chunk-25KVF5ZE.js +1 -0
- package/web/assets/mermaid/chunks/chunk-4HYFSIZT.js +1 -0
- package/web/assets/mermaid/chunks/chunk-4W4OQ3QY.js +1 -0
- package/web/assets/mermaid/chunks/chunk-5TQRX2ND.js +1 -0
- package/web/assets/mermaid/chunks/chunk-64OFLKGX.js +322 -0
- package/web/assets/mermaid/chunks/chunk-64OFLKGX.js.LEGAL.txt +17 -0
- package/web/assets/mermaid/chunks/chunk-6UFWKTSA.js +1 -0
- package/web/assets/mermaid/chunks/chunk-6ZAAIG6Q.js +10 -0
- package/web/assets/mermaid/chunks/chunk-76QEHT4T.js +1 -0
- package/web/assets/mermaid/chunks/chunk-A7665WVC.js +1 -0
- package/web/assets/mermaid/chunks/chunk-AECJS4LP.js +130 -0
- package/web/assets/mermaid/chunks/chunk-AECJS4LP.js.LEGAL.txt +5 -0
- package/web/assets/mermaid/chunks/chunk-ATQQWCXO.js +2 -0
- package/web/assets/mermaid/chunks/chunk-DQVO43GU.js +1 -0
- package/web/assets/mermaid/chunks/chunk-DSKRGV6V.js +1 -0
- package/web/assets/mermaid/chunks/chunk-EIBFAW37.js +1 -0
- package/web/assets/mermaid/chunks/chunk-FBC3PYSO.js +1 -0
- package/web/assets/mermaid/chunks/chunk-FQ5IIL54.js +88 -0
- package/web/assets/mermaid/chunks/chunk-GZYY6JMP.js +1 -0
- package/web/assets/mermaid/chunks/chunk-HIGPS4L2.js +1 -0
- package/web/assets/mermaid/chunks/chunk-HP75Y56I.js +2 -0
- package/web/assets/mermaid/chunks/chunk-JRAHLUQM.js +32 -0
- package/web/assets/mermaid/chunks/chunk-KNYDPLLK.js +1 -0
- package/web/assets/mermaid/chunks/chunk-L465623K.js +136 -0
- package/web/assets/mermaid/chunks/chunk-L7BMR7WL.js +1 -0
- package/web/assets/mermaid/chunks/chunk-LLGPMHB6.js +1 -0
- package/web/assets/mermaid/chunks/chunk-NBH7YOBD.js +15 -0
- package/web/assets/mermaid/chunks/chunk-NCOVM3YF.js +1 -0
- package/web/assets/mermaid/chunks/chunk-PHKIAKHS.js +1 -0
- package/web/assets/mermaid/chunks/chunk-PM2IFX5O.js +1 -0
- package/web/assets/mermaid/chunks/chunk-PN62QGJW.js +72 -0
- package/web/assets/mermaid/chunks/chunk-QHMFFOZZ.js +1 -0
- package/web/assets/mermaid/chunks/chunk-QQZBUVKZ.js +1 -0
- package/web/assets/mermaid/chunks/chunk-RV7WQMNL.js +1 -0
- package/web/assets/mermaid/chunks/chunk-STBOPOHX.js +1 -0
- package/web/assets/mermaid/chunks/chunk-T3FZWEYV.js +1 -0
- package/web/assets/mermaid/chunks/chunk-TMMWB6NQ.js +1 -0
- package/web/assets/mermaid/chunks/chunk-TPW5CADB.js +1 -0
- package/web/assets/mermaid/chunks/chunk-TSOLLCID.js +2 -0
- package/web/assets/mermaid/chunks/chunk-TWGPYWIY.js +1 -0
- package/web/assets/mermaid/chunks/chunk-WGMEMLED.js +1 -0
- package/web/assets/mermaid/chunks/chunk-XC2LKMMW.js +1 -0
- package/web/assets/mermaid/chunks/chunk-XHVVPFJ5.js +1 -0
- package/web/assets/mermaid/chunks/chunk-YIN5CSIF.js +2 -0
- package/web/assets/mermaid/chunks/chunk-YIN5CSIF.js.LEGAL.txt +12 -0
- package/web/assets/mermaid/chunks/chunk-YORIST7Z.js +1 -0
- package/web/assets/mermaid/chunks/chunk-YPXVLPXW.js +3 -0
- package/web/assets/mermaid/chunks/chunk-Z4WODCD2.js +213 -0
- package/web/assets/mermaid/chunks/chunk-ZAERIUBF.js +1 -0
- package/web/assets/mermaid/chunks/chunk-ZDOBOOWG.js +62 -0
- package/web/assets/mermaid/chunks/classDiagram-v2-K4WV4PDN-HXQD675C.js +217 -0
- package/web/assets/mermaid/chunks/cose-bilkent-JH36ORCC-3VHNHM7I.js +1 -0
- package/web/assets/mermaid/chunks/cynefin-EF2NZ3EQ-ZO5PPXJG.js +1 -0
- package/web/assets/mermaid/chunks/cynefinDiagram-3GCD6N5R-LFWDWE4E.js +62 -0
- package/web/assets/mermaid/chunks/dagre-W4DXFKR2-SDBBVBGG.js +4 -0
- package/web/assets/mermaid/chunks/diagram-2UJZ2QOL-YCKW3WNJ.js +200 -0
- package/web/assets/mermaid/chunks/diagram-OVF4WLC6-62NNPE34.js +24 -0
- package/web/assets/mermaid/chunks/diagram-PFPMY2P6-7RR2GTZ5.js +30 -0
- package/web/assets/mermaid/chunks/diagram-UMYRVEAY-Q4I25SMR.js +24 -0
- package/web/assets/mermaid/chunks/diagram-YEKJPTXX-VNUTOPRK.js +41 -0
- package/web/assets/mermaid/chunks/diagram-ZIFT7M5P-KALXX5CE.js +3 -0
- package/web/assets/mermaid/chunks/ebnfDiagram-VR2GEFS7-WIFBISHL.js +1 -0
- package/web/assets/mermaid/chunks/elk-IJKZMXRS-LGG232IL.js +27 -0
- package/web/assets/mermaid/chunks/erDiagram-O2IAPWRE-KWX2J2TX.js +99 -0
- package/web/assets/mermaid/chunks/eventmodeling-K75KTNOO-3JNV4VSP.js +1 -0
- package/web/assets/mermaid/chunks/flowDiagram-OXPTDLAJ-CUMOBDC3.js +1 -0
- package/web/assets/mermaid/chunks/ganttDiagram-R7TSEDQI-GCS3MRFX.js +292 -0
- package/web/assets/mermaid/chunks/gitGraph-VSP46ZUC-HSAXKLAG.js +1 -0
- package/web/assets/mermaid/chunks/gitGraphDiagram-XJZIOB7I-FB763ROZ.js +106 -0
- package/web/assets/mermaid/chunks/info-OHQRW6UA-C4R2B34T.js +1 -0
- package/web/assets/mermaid/chunks/infoDiagram-5W2HQ5XZ-XCQU5T5D.js +2 -0
- package/web/assets/mermaid/chunks/ishikawaDiagram-K3B6WC7H-UWYT66G7.js +70 -0
- package/web/assets/mermaid/chunks/journeyDiagram-COXZFDF6-DOW66RYW.js +139 -0
- package/web/assets/mermaid/chunks/kanban-definition-P3RFRI5V-K55BXDLO.js +89 -0
- package/web/assets/mermaid/chunks/katex-5AUD5AJN.js +257 -0
- package/web/assets/mermaid/chunks/mindmap-definition-LZFPQGKD-WG3FIZ5U.js +96 -0
- package/web/assets/mermaid/chunks/packet-JDAUHWVQ-SFUSDIUN.js +1 -0
- package/web/assets/mermaid/chunks/pegDiagram-BGZESJAR-MQOH3WII.js +1 -0
- package/web/assets/mermaid/chunks/pie-XZMESJXO-M76QC4CD.js +1 -0
- package/web/assets/mermaid/chunks/pieDiagram-CAPJLFHJ-V3WNR4Y2.js +39 -0
- package/web/assets/mermaid/chunks/quadrantDiagram-GDMTTRAM-DJLTATHS.js +7 -0
- package/web/assets/mermaid/chunks/radar-ABXABTNO-HSBJ6SSD.js +1 -0
- package/web/assets/mermaid/chunks/railroad-I3PHUGI6-4QJMJ7LG.js +1 -0
- package/web/assets/mermaid/chunks/railroad-abnf-LNEFI6M7-ZLNZ5ZRH.js +1 -0
- package/web/assets/mermaid/chunks/railroad-ebnf-DWYD2UWJ-EBV6ZOUW.js +1 -0
- package/web/assets/mermaid/chunks/railroad-peg-JR7BVNR7-X3E47TO7.js +1 -0
- package/web/assets/mermaid/chunks/railroadDiagram-SM67HX2B-ZHEERYJS.js +1 -0
- package/web/assets/mermaid/chunks/requirementDiagram-X7JNWC4A-TVHJNPPI.js +84 -0
- package/web/assets/mermaid/chunks/sankeyDiagram-UM26HJRW-FXHWY3MM.js +40 -0
- package/web/assets/mermaid/chunks/sequenceDiagram-ZO4K6R2Y-OP7DCVPK.js +169 -0
- package/web/assets/mermaid/chunks/sizeCapture-INFHLROL-2GUJIKNX.js +1 -0
- package/web/assets/mermaid/chunks/stateDiagram-v2-TBUQTH76-R26D6XHK.js +291 -0
- package/web/assets/mermaid/chunks/swimlanes-N4OXWK64-FPGACWHE.js +1 -0
- package/web/assets/mermaid/chunks/swimlanesDiagram-K3J5GTZL-YMNX34Q2.js +8 -0
- package/web/assets/mermaid/chunks/timeline-definition-YOQAKGHF-R2U2UR4K.js +120 -0
- package/web/assets/mermaid/chunks/treeView-D4JQ5SDB-3BQHZUCI.js +1 -0
- package/web/assets/mermaid/chunks/treemap-SAJKECNS-6NJPJ5NM.js +1 -0
- package/web/assets/mermaid/chunks/usecaseDiagram-VIAY4XPW-BCEIGXDX.js +370 -0
- package/web/assets/mermaid/chunks/vennDiagram-BLWOH2XV-DOV5W2IV.js +34 -0
- package/web/assets/mermaid/chunks/wardley-7MLQ67FV-ITOM4KQ2.js +1 -0
- package/web/assets/mermaid/chunks/wardleyDiagram-YMQ3BMBF-CGJ7AG4L.js +78 -0
- package/web/assets/mermaid/chunks/xychartDiagram-TAQBALBS-2MTP7IBV.js +7 -0
- package/web/assets/mermaid/mermaid.js +11 -0
- package/web/assets/mermaid/mermaid.js.LEGAL.txt +7 -0
- package/web/index.html +1 -0
- package/web/markdown-diagrams.js +147 -0
- package/web/markdown.ts +41 -2
- package/web/session-sync.d.ts +12 -0
- package/web/session-sync.js +105 -0
- package/web/slash-commands.js +1 -0
- 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
|
### 手机与远程访问
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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
|
|
7
|
-
|
|
8
|
-
`web/assets/markdown.LICENSE.txt
|
|
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.
|
|
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
|
-
|
|
525
|
+
const schedule = (...args: A) => {
|
|
521
526
|
if (timer) clearTimeout(timer);
|
|
522
|
-
timer = setTimeout(() =>
|
|
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])
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2235
|
+
const entry = await loadStaticAsset(filePath);
|
|
2048
2236
|
const ext = extname(filePath).toLowerCase();
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
})
|
|
2053
|
-
|
|
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
|
-
|
|
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)}
|