dsh-plugin-workbench 0.0.3 → 0.0.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/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@
4
4
  格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
5
5
  版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
6
6
 
7
+ ## [0.0.4] - 2026-08-17
8
+
9
+ ### Fixed
10
+
11
+ - 长中文文本(如小说 .txt)预览卡顿:无语法高亮的文件不再渲染高亮叠加层,
12
+ 直接由 textarea 显示文本;代码文件超过 64KB 自动降级为纯文本编辑,
13
+ 避免每次按键重新注入并布局几十万字符的两层文本
14
+ - **md 文件一律以纯文本模式显示**(不再做语法高亮叠加):20KB+ 的 md 加载
15
+ 从约 60ms 降到约 10ms,彻底消除 md 的拖选错位与加载卡顿;大文件加载时
16
+ 先让浏览器绘制一帧再渲染内容,界面不再在打开文件的同一帧内冻结
17
+ - md 长文本:去掉悬停时反复弹出的「Ctrl+S 保存」提示;高亮层加粗改为
18
+ 纯颜色强调(粗体改变字宽会导致拖选时高亮层与文本换行错位),并增加
19
+ 每帧滚动锁定,保证拖选自动滚动时两层始终对齐
20
+ - **拖选错位根因**:textarea 底部会预留约一行给光标,导致其可滚动范围比
21
+ 高亮层大,长文件滚到接近底部时高亮层文本比选中框低约一行(选中框浮在
22
+ 显示文本上方)。已给高亮层补齐等量底部滚动余量,两层滚动范围完全一致
23
+ - 打开/切换含大量文件的目录不再卡顿:文件树行改为 memo 组件,点击文件
24
+ 只重渲染受影响的单行(此前每次点击都重渲染整棵文件树);切换工作区时
25
+ 恢复已展开目录改为分批加载
26
+
27
+ ## [Unreleased]
28
+
7
29
  ## [0.0.3] - 2026-08-16
8
30
 
9
31
  ### Added
package/README.md CHANGED
@@ -13,10 +13,11 @@ DSH Web GUI 的 VS Code 风格文件浏览器插件:文件树 + 可编辑代
13
13
 
14
14
  - 文件树:懒加载、2 秒自动刷新、每工作区独立展开状态
15
15
  - 文件图标:常见格式显示着色徽章(代码)/ emoji(图片、音视频、压缩包等),目录展开/收起区分
16
- - 可编辑预览:透明 textarea 叠加语法高亮,`Ctrl+S`/`Cmd+S` 保存
16
+ - 可编辑预览:透明 textarea 叠加语法高亮,`Ctrl+S`/`Cmd+S` 保存;
17
+ md/.txt 等散文格式与超大代码文件自动降级为纯文本编辑,加载快、不卡界面
17
18
  - 自动换行:标签栏一键切换软换行(仅显示层换行,不改动文件内容)或长行横向滚动,偏好持久化
18
19
  - 标签页:多文件、拖拽排序、收起/弹出;亮暗主题一键切换
19
- - 语法高亮:highlight.js(JS/TS、Python、JSON、HTML、CSS、Markdown、Shell 等)
20
+ - 语法高亮:highlight.js(JS/TS、Python、JSON、HTML、CSS、Shell 等;Markdown 以纯文本显示)
20
21
 
21
22
  ## 安装
22
23
 
package/lib/client.js CHANGED
@@ -30,7 +30,7 @@ window.__ModuleLoader__.load({
30
30
  let react = require("react");
31
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
32
  //#region \0dsh-css:src/client/files.module.css.mjs
33
- const css$1 = "[data-fe-theme=light]{--fe-bg:#fff;--fe-sidebar-bg:#f3f3f3;--fe-fg:#1f1f1f;--fe-muted:#6e6e6e;--fe-faint:#9d9d9d;--fe-border:#e5e5e5;--fe-border-strong:#c8c8c8;--fe-accent:#005fb8;--fe-hover:#e8e8e8;--fe-selection:#add6ff;--fe-token-keyword:#00f;--fe-token-string:#a31515;--fe-token-comment:green;--fe-token-number:#098658;--fe-token-function:#795e26;--fe-token-type:#267f99;--fe-token-variable:#001080;--fe-token-tag:maroon;--fe-token-meta:#6e6e6e}[data-fe-theme=dark]{--fe-bg:#1e1e1e;--fe-sidebar-bg:#252526;--fe-fg:#d4d4d4;--fe-muted:#ccc;--fe-faint:#9d9d9d;--fe-border:#3c3c3c;--fe-border-strong:#454545;--fe-accent:#007acc;--fe-hover:#2a2d2e;--fe-selection:#264f78;--fe-token-keyword:#569cd6;--fe-token-string:#ce9178;--fe-token-comment:#6a9955;--fe-token-number:#b5cea8;--fe-token-function:#dcdcaa;--fe-token-type:#4ec9b0;--fe-token-variable:#9cdcfe;--fe-token-tag:#569cd6;--fe-token-meta:#9b9b9b}.Myjg4a_column{box-sizing:border-box;background:var(--fe-sidebar-bg);min-width:0;height:100%;color:var(--fe-fg);flex-direction:column;font-size:13px;display:flex;overflow:hidden}.Myjg4a_header{box-sizing:border-box;border-bottom:1px solid var(--fe-border);flex:none;align-items:center;gap:6px;height:34px;padding:0 8px;display:flex}.Myjg4a_headerTitle{text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;letter-spacing:.04em;min-width:0;color:var(--fe-muted);flex:1;font-size:11px;font-weight:600;overflow:hidden}.Myjg4a_headerActions{flex:none;align-items:center;gap:2px;display:flex}.Myjg4a_action{appearance:none;color:var(--fe-muted);cursor:pointer;background:0 0;border:none;border-radius:5px;justify-content:center;align-items:center;width:22px;height:22px;padding:0;font-size:13px;line-height:1;display:inline-flex}.Myjg4a_action:hover:not(:disabled){background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_action:disabled{opacity:.5;cursor:default}.Myjg4a_treeArea{flex:1;min-height:0;padding:4px;overflow-y:auto}.Myjg4a_row{cursor:pointer;user-select:none;white-space:nowrap;height:22px;color:var(--fe-fg);border-radius:3px;align-items:center;gap:6px;font-size:13px;display:flex}.Myjg4a_row:hover{background:var(--fe-hover)}.Myjg4a_rowSelected{background:var(--fe-selection)}.Myjg4a_rowSelected .Myjg4a_name{color:var(--fe-fg)}.Myjg4a_chevron{text-align:center;width:12px;color:var(--fe-muted);flex:none;font-size:10px;line-height:22px}.Myjg4a_icon{text-align:center;flex:none;width:16px;font-size:13px;line-height:22px}.Myjg4a_fileBadge{letter-spacing:-.3px;box-sizing:border-box;vertical-align:middle;user-select:none;border-radius:3px;justify-content:center;align-items:center;width:16px;height:15px;padding:0 1px;font-size:8px;font-weight:700;line-height:1;display:inline-flex}.Myjg4a_fileBadgeLong{letter-spacing:-.4px;font-size:6.5px}.Myjg4a_name{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.Myjg4a_size{color:var(--fe-faint);flex:none;font-size:11px}.Myjg4a_actions{opacity:0;flex:none;align-items:center;gap:2px;transition:opacity .12s;display:flex}.Myjg4a_row:hover .Myjg4a_actions,.Myjg4a_row:focus-within .Myjg4a_actions{opacity:1}.Myjg4a_rowHint{color:var(--fe-faint);padding:2px 8px 2px 0;font-size:12px;line-height:20px}.Myjg4a_rowError{color:#f48771;padding:2px 8px 2px 0;font-size:12px;line-height:18px}.Myjg4a_preview{border-right:1px solid var(--fe-border);background:var(--fe-bg);flex-direction:column;flex:0 0 55%;min-width:240px;min-height:0;animation:.18s Myjg4a_fe-preview-in;display:flex;overflow:hidden}.Myjg4a_previewClosing{animation:.2s forwards Myjg4a_fe-preview-out}@keyframes Myjg4a_fe-preview-in{0%{opacity:0}to{opacity:1}}@keyframes Myjg4a_fe-preview-out{0%{opacity:1}to{opacity:0}}.Myjg4a_handle{cursor:col-resize;touch-action:none;z-index:2;background:0 0;flex:none;width:6px;margin-left:-4px;margin-right:-2px}.Myjg4a_handle:hover,.Myjg4a_handle:active{background:var(--fe-accent);opacity:.35}.Myjg4a_tabBar{border-bottom:1px solid var(--fe-border);background:var(--fe-sidebar-bg);flex:none;align-items:stretch;height:34px;display:flex}.Myjg4a_tabScroller{scrollbar-width:thin;flex:1;align-items:stretch;min-width:0;display:flex;overflow-x:auto}.Myjg4a_collapse,.Myjg4a_tabAction{appearance:none;color:var(--fe-muted);cursor:pointer;background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;width:26px;height:26px;margin:auto 4px;padding:0;font-size:14px;line-height:1;display:inline-flex}.Myjg4a_collapse:hover,.Myjg4a_tabAction:hover{background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_tabActionActive{background:var(--fe-hover);color:var(--fe-accent)}.Myjg4a_tab{max-width:180px;color:var(--fe-muted);border-right:1px solid var(--fe-border);cursor:pointer;user-select:none;white-space:nowrap;flex:none;align-items:center;gap:6px;padding:0 8px 0 12px;font-size:13px;display:flex}.Myjg4a_tab:hover{background:var(--fe-hover)}.Myjg4a_tabActive{background:var(--fe-bg);color:var(--fe-fg);box-shadow:inset 0 -1px 0 var(--fe-accent)}.Myjg4a_tabName{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.Myjg4a_tabIcon{flex:none;justify-content:center;align-items:center;width:16px;font-size:13px;line-height:1;display:inline-flex}.Myjg4a_tabClose{appearance:none;color:var(--fe-faint);cursor:pointer;background:0 0;border:none;border-radius:3px;justify-content:center;align-items:center;width:16px;height:16px;padding:0;font-size:11px;display:inline-flex}.Myjg4a_tabClose:hover{background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_tabDot{background:var(--fe-fg);opacity:.85;border-radius:50%;flex:none;width:8px;height:8px}.Myjg4a_previewBody{flex:1;min-height:0;overflow:auto}.Myjg4a_editor{height:100%;min-height:0;position:relative}.Myjg4a_editorHighlight{pointer-events:none;white-space:pre;tab-size:2;color:var(--fe-fg);background:0 0;margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;position:absolute;inset:0;overflow:auto}.Myjg4a_editorHighlight code{font-family:inherit;font-size:inherit;line-height:inherit;white-space:inherit}.Myjg4a_editorTextarea{box-sizing:border-box;resize:none;color:#0000;caret-color:var(--fe-fg);white-space:pre;tab-size:2;background:0 0;border:none;outline:none;margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;position:absolute;inset:0;overflow:auto}.Myjg4a_editor[data-wrap=on] .Myjg4a_editorHighlight,.Myjg4a_editor[data-wrap=on] .Myjg4a_editorTextarea{white-space:pre-wrap;overflow-wrap:anywhere}.Myjg4a_editor[data-wrap=on] .Myjg4a_editorTextarea{overflow-x:hidden}.Myjg4a_saveError{color:#1e1e1e;background:#f48771;border-radius:4px;padding:4px 10px;font-size:12px;position:absolute;bottom:8px;right:8px}.Myjg4a_previewHint{color:var(--fe-faint);padding:12px;font-size:12px;line-height:18px}.Myjg4a_previewMeta{color:var(--fe-faint);font-size:11px}.Myjg4a_placeholder{text-align:center;color:var(--fe-faint);flex:1;justify-content:center;align-items:center;padding:16px;font-size:12px;line-height:18px;display:flex}.Myjg4a_editorHighlight .hljs-keyword,.Myjg4a_editorHighlight .hljs-selector-tag,.Myjg4a_editorHighlight .hljs-literal{color:var(--fe-token-keyword)}.Myjg4a_editorHighlight .hljs-string,.Myjg4a_editorHighlight .hljs-regexp,.Myjg4a_editorHighlight .hljs-addition{color:var(--fe-token-string)}.Myjg4a_editorHighlight .hljs-comment,.Myjg4a_editorHighlight .hljs-quote{color:var(--fe-token-comment);font-style:italic}.Myjg4a_editorHighlight .hljs-number,.Myjg4a_editorHighlight .hljs-symbol,.Myjg4a_editorHighlight .hljs-bullet{color:var(--fe-token-number)}.Myjg4a_editorHighlight .hljs-title,.Myjg4a_editorHighlight .hljs-section,.Myjg4a_editorHighlight .hljs-title.function_{color:var(--fe-token-function)}.Myjg4a_editorHighlight .hljs-type,.Myjg4a_editorHighlight .hljs-built_in,.Myjg4a_editorHighlight .hljs-class .hljs-title{color:var(--fe-token-type)}.Myjg4a_editorHighlight .hljs-attr,.Myjg4a_editorHighlight .hljs-attribute,.Myjg4a_editorHighlight .hljs-variable,.Myjg4a_editorHighlight .hljs-template-variable,.Myjg4a_editorHighlight .hljs-params{color:var(--fe-token-variable)}.Myjg4a_editorHighlight .hljs-tag,.Myjg4a_editorHighlight .hljs-name{color:var(--fe-token-tag)}.Myjg4a_editorHighlight .hljs-meta{color:var(--fe-token-meta)}.Myjg4a_editorHighlight .hljs-emphasis{font-style:italic}.Myjg4a_editorHighlight .hljs-strong{font-weight:700}body[data-dsh-maid-atelier] [data-skin-chrome=top-trim],body[data-dsh-maid-atelier] [data-skin-chrome=bottom-trim]{translate:calc(var(--maid-sidebar-width,0px) + var(--dsh-explorer-width,0px) + var(--dsh-preview-width,0px)) 0!important}[class*=explorerCol]{border-right:1px solid var(--fe-border)!important}";
33
+ const css$1 = "[data-fe-theme=light]{--fe-bg:#fff;--fe-sidebar-bg:#f3f3f3;--fe-fg:#1f1f1f;--fe-muted:#6e6e6e;--fe-faint:#9d9d9d;--fe-border:#e5e5e5;--fe-border-strong:#c8c8c8;--fe-accent:#005fb8;--fe-hover:#e8e8e8;--fe-selection:#add6ff;--fe-token-keyword:#00f;--fe-token-string:#a31515;--fe-token-comment:green;--fe-token-number:#098658;--fe-token-function:#795e26;--fe-token-type:#267f99;--fe-token-variable:#001080;--fe-token-tag:maroon;--fe-token-meta:#6e6e6e}[data-fe-theme=dark]{--fe-bg:#1e1e1e;--fe-sidebar-bg:#252526;--fe-fg:#d4d4d4;--fe-muted:#ccc;--fe-faint:#9d9d9d;--fe-border:#3c3c3c;--fe-border-strong:#454545;--fe-accent:#007acc;--fe-hover:#2a2d2e;--fe-selection:#264f78;--fe-token-keyword:#569cd6;--fe-token-string:#ce9178;--fe-token-comment:#6a9955;--fe-token-number:#b5cea8;--fe-token-function:#dcdcaa;--fe-token-type:#4ec9b0;--fe-token-variable:#9cdcfe;--fe-token-tag:#569cd6;--fe-token-meta:#9b9b9b}.Myjg4a_column{box-sizing:border-box;background:var(--fe-sidebar-bg);min-width:0;height:100%;color:var(--fe-fg);flex-direction:column;font-size:13px;display:flex;overflow:hidden}.Myjg4a_header{box-sizing:border-box;border-bottom:1px solid var(--fe-border);flex:none;align-items:center;gap:6px;height:34px;padding:0 8px;display:flex}.Myjg4a_headerTitle{text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;letter-spacing:.04em;min-width:0;color:var(--fe-muted);flex:1;font-size:11px;font-weight:600;overflow:hidden}.Myjg4a_headerActions{flex:none;align-items:center;gap:2px;display:flex}.Myjg4a_action{appearance:none;color:var(--fe-muted);cursor:pointer;background:0 0;border:none;border-radius:5px;justify-content:center;align-items:center;width:22px;height:22px;padding:0;font-size:13px;line-height:1;display:inline-flex}.Myjg4a_action:hover:not(:disabled){background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_action:disabled{opacity:.5;cursor:default}.Myjg4a_treeArea{flex:1;min-height:0;padding:4px;overflow-y:auto}.Myjg4a_row{cursor:pointer;user-select:none;white-space:nowrap;height:22px;color:var(--fe-fg);border-radius:3px;align-items:center;gap:6px;font-size:13px;display:flex}.Myjg4a_row:hover{background:var(--fe-hover)}.Myjg4a_rowSelected{background:var(--fe-selection)}.Myjg4a_rowSelected .Myjg4a_name{color:var(--fe-fg)}.Myjg4a_chevron{text-align:center;width:12px;color:var(--fe-muted);flex:none;font-size:10px;line-height:22px}.Myjg4a_icon{text-align:center;flex:none;width:16px;font-size:13px;line-height:22px}.Myjg4a_fileBadge{letter-spacing:-.3px;box-sizing:border-box;vertical-align:middle;user-select:none;border-radius:3px;justify-content:center;align-items:center;width:16px;height:15px;padding:0 1px;font-size:8px;font-weight:700;line-height:1;display:inline-flex}.Myjg4a_fileBadgeLong{letter-spacing:-.4px;font-size:6.5px}.Myjg4a_name{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.Myjg4a_size{color:var(--fe-faint);flex:none;font-size:11px}.Myjg4a_actions{opacity:0;flex:none;align-items:center;gap:2px;transition:opacity .12s;display:flex}.Myjg4a_row:hover .Myjg4a_actions,.Myjg4a_row:focus-within .Myjg4a_actions{opacity:1}.Myjg4a_rowHint{color:var(--fe-faint);padding:2px 8px 2px 0;font-size:12px;line-height:20px}.Myjg4a_rowError{color:#f48771;padding:2px 8px 2px 0;font-size:12px;line-height:18px}.Myjg4a_preview{border-right:1px solid var(--fe-border);background:var(--fe-bg);flex-direction:column;flex:0 0 55%;min-width:240px;min-height:0;animation:.18s Myjg4a_fe-preview-in;display:flex;overflow:hidden}.Myjg4a_previewClosing{animation:.2s forwards Myjg4a_fe-preview-out}@keyframes Myjg4a_fe-preview-in{0%{opacity:0}to{opacity:1}}@keyframes Myjg4a_fe-preview-out{0%{opacity:1}to{opacity:0}}.Myjg4a_handle{cursor:col-resize;touch-action:none;z-index:2;background:0 0;flex:none;width:6px;margin-left:-4px;margin-right:-2px}.Myjg4a_handle:hover,.Myjg4a_handle:active{background:var(--fe-accent);opacity:.35}.Myjg4a_tabBar{border-bottom:1px solid var(--fe-border);background:var(--fe-sidebar-bg);flex:none;align-items:stretch;height:34px;display:flex}.Myjg4a_tabScroller{scrollbar-width:thin;flex:1;align-items:stretch;min-width:0;display:flex;overflow-x:auto}.Myjg4a_collapse,.Myjg4a_tabAction{appearance:none;color:var(--fe-muted);cursor:pointer;background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;width:26px;height:26px;margin:auto 4px;padding:0;font-size:14px;line-height:1;display:inline-flex}.Myjg4a_collapse:hover,.Myjg4a_tabAction:hover{background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_tabActionActive{background:var(--fe-hover);color:var(--fe-accent)}.Myjg4a_tab{max-width:180px;color:var(--fe-muted);border-right:1px solid var(--fe-border);cursor:pointer;user-select:none;white-space:nowrap;flex:none;align-items:center;gap:6px;padding:0 8px 0 12px;font-size:13px;display:flex}.Myjg4a_tab:hover{background:var(--fe-hover)}.Myjg4a_tabActive{background:var(--fe-bg);color:var(--fe-fg);box-shadow:inset 0 -1px 0 var(--fe-accent)}.Myjg4a_tabName{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.Myjg4a_tabIcon{flex:none;justify-content:center;align-items:center;width:16px;font-size:13px;line-height:1;display:inline-flex}.Myjg4a_tabClose{appearance:none;color:var(--fe-faint);cursor:pointer;background:0 0;border:none;border-radius:3px;justify-content:center;align-items:center;width:16px;height:16px;padding:0;font-size:11px;display:inline-flex}.Myjg4a_tabClose:hover{background:var(--fe-hover);color:var(--fe-fg)}.Myjg4a_tabDot{background:var(--fe-fg);opacity:.85;border-radius:50%;flex:none;width:8px;height:8px}.Myjg4a_previewBody{flex:1;min-height:0;overflow:auto}.Myjg4a_editor{contain:layout paint;height:100%;min-height:0;position:relative}.Myjg4a_editorPlain .Myjg4a_editorTextarea{color:var(--fe-fg)}.Myjg4a_editorHint{background:var(--fe-hover);color:var(--fe-muted);pointer-events:none;border-radius:4px;padding:4px 10px;font-size:11px;position:absolute;bottom:8px;left:8px}.Myjg4a_editorHighlight{pointer-events:none;white-space:pre;tab-size:2;color:var(--fe-fg);background:0 0;margin:0;padding:12px 12px calc(12px + 1.5em);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;position:absolute;inset:0;overflow:auto}.Myjg4a_editorHighlight code{font-family:inherit;font-size:inherit;line-height:inherit;white-space:inherit}.Myjg4a_editorTextarea{box-sizing:border-box;resize:none;color:#0000;caret-color:var(--fe-fg);white-space:pre;tab-size:2;background:0 0;border:none;outline:none;margin:0;padding:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;line-height:1.5;position:absolute;inset:0;overflow:auto}.Myjg4a_editor[data-wrap=on] .Myjg4a_editorHighlight,.Myjg4a_editor[data-wrap=on] .Myjg4a_editorTextarea{white-space:pre-wrap;overflow-wrap:anywhere}.Myjg4a_editor[data-wrap=on] .Myjg4a_editorTextarea{overflow-x:hidden}.Myjg4a_saveError{color:#1e1e1e;background:#f48771;border-radius:4px;padding:4px 10px;font-size:12px;position:absolute;bottom:8px;right:8px}.Myjg4a_previewHint{color:var(--fe-faint);padding:12px;font-size:12px;line-height:18px}.Myjg4a_previewMeta{color:var(--fe-faint);font-size:11px}.Myjg4a_placeholder{text-align:center;color:var(--fe-faint);flex:1;justify-content:center;align-items:center;padding:16px;font-size:12px;line-height:18px;display:flex}.Myjg4a_editorHighlight .hljs-keyword,.Myjg4a_editorHighlight .hljs-selector-tag,.Myjg4a_editorHighlight .hljs-literal{color:var(--fe-token-keyword)}.Myjg4a_editorHighlight .hljs-string,.Myjg4a_editorHighlight .hljs-regexp,.Myjg4a_editorHighlight .hljs-addition{color:var(--fe-token-string)}.Myjg4a_editorHighlight .hljs-comment,.Myjg4a_editorHighlight .hljs-quote{color:var(--fe-token-comment);font-style:italic}.Myjg4a_editorHighlight .hljs-number,.Myjg4a_editorHighlight .hljs-symbol,.Myjg4a_editorHighlight .hljs-bullet{color:var(--fe-token-number)}.Myjg4a_editorHighlight .hljs-title,.Myjg4a_editorHighlight .hljs-section,.Myjg4a_editorHighlight .hljs-title.function_{color:var(--fe-token-function)}.Myjg4a_editorHighlight .hljs-type,.Myjg4a_editorHighlight .hljs-built_in,.Myjg4a_editorHighlight .hljs-class .hljs-title{color:var(--fe-token-type)}.Myjg4a_editorHighlight .hljs-attr,.Myjg4a_editorHighlight .hljs-attribute,.Myjg4a_editorHighlight .hljs-variable,.Myjg4a_editorHighlight .hljs-template-variable,.Myjg4a_editorHighlight .hljs-params{color:var(--fe-token-variable)}.Myjg4a_editorHighlight .hljs-tag,.Myjg4a_editorHighlight .hljs-name{color:var(--fe-token-tag)}.Myjg4a_editorHighlight .hljs-meta{color:var(--fe-token-meta)}.Myjg4a_editorHighlight .hljs-emphasis{font-style:italic}.Myjg4a_editorHighlight .hljs-strong{color:var(--fe-accent);font-weight:inherit}body[data-dsh-maid-atelier] [data-skin-chrome=top-trim],body[data-dsh-maid-atelier] [data-skin-chrome=bottom-trim]{translate:calc(var(--maid-sidebar-width,0px) + var(--dsh-explorer-width,0px) + var(--dsh-preview-width,0px)) 0!important}[class*=explorerCol]{border-right:1px solid var(--fe-border)!important}";
34
34
  const tagId = "dsh-plugin-workbench/files.module.css";
35
35
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
36
36
  const tag = document.createElement("style");
@@ -47,6 +47,8 @@ window.__ModuleLoader__.load({
47
47
  "column": "Myjg4a_column",
48
48
  "editor": "Myjg4a_editor",
49
49
  "editorHighlight": "Myjg4a_editorHighlight",
50
+ "editorHint": "Myjg4a_editorHint",
51
+ "editorPlain": "Myjg4a_editorPlain",
50
52
  "editorTextarea": "Myjg4a_editorTextarea",
51
53
  "fe-preview-in": "Myjg4a_fe-preview-in",
52
54
  "fe-preview-out": "Myjg4a_fe-preview-out",
@@ -735,6 +737,10 @@ window.__ModuleLoader__.load({
735
737
  }
736
738
  /** How often the visible tree is re-listed to pick up disk changes. */
737
739
  const REFRESH_MS = 2e3;
740
+ /** How many previously-expanded folders to re-list at once on workspace switch. */
741
+ const DIR_LOAD_BATCH = 4;
742
+ /** Pause between batches when restoring expanded folders. */
743
+ const DIR_LOAD_GAP_MS = 50;
738
744
  const EMPTY_EXPANDED = /* @__PURE__ */ new Set();
739
745
  /** True when two directory listings are identical (name/kind/size). */
740
746
  function sameEntries(a, b) {
@@ -742,6 +748,69 @@ window.__ModuleLoader__.load({
742
748
  for (let i = 0; i < a.length; i += 1) if (a[i].name !== b[i].name || a[i].kind !== b[i].kind || a[i].size !== b[i].size) return false;
743
749
  return true;
744
750
  }
751
+ /**
752
+ * One tree row, memoized so opening/activating a file (which changes the
753
+ * active-path highlight) re-renders only the affected row — with a workspace
754
+ * full of files, re-rendering the whole tree on every click is what made
755
+ * opening files feel laggy.
756
+ */
757
+ const TreeRow = (0, react.memo)(function TreeRow({ entry, depth, isActive, isExpanded, onToggleDir, onRefreshDir, onSelect, onDoubleClick, onOpenExternal, t }) {
758
+ const isDir = entry.kind === "dir";
759
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
760
+ className: `${files_module_css_default.row} ${isActive ? files_module_css_default.rowSelected : ""}`,
761
+ style: { paddingLeft: 8 + depth * 14 },
762
+ onClick: () => onSelect(entry),
763
+ onDoubleClick: () => onDoubleClick(entry),
764
+ role: "treeitem",
765
+ "aria-selected": isActive,
766
+ "aria-expanded": isDir ? isExpanded : void 0,
767
+ title: entry.name,
768
+ children: [
769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
770
+ className: files_module_css_default.chevron,
771
+ onClick: (e) => {
772
+ e.stopPropagation();
773
+ if (isDir) onToggleDir(entry.path);
774
+ },
775
+ children: isDir ? isExpanded ? "▾" : "▸" : ""
776
+ }),
777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
778
+ className: files_module_css_default.icon,
779
+ children: isDir ? isExpanded ? "📂" : "📁" : entry.kind === "file" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileIcon, { name: entry.name }) : "·"
780
+ }),
781
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
782
+ className: files_module_css_default.name,
783
+ children: entry.name
784
+ }),
785
+ entry.kind === "file" && entry.size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
786
+ className: files_module_css_default.size,
787
+ children: formatSize$1(entry.size)
788
+ }),
789
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
790
+ className: files_module_css_default.actions,
791
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
792
+ type: "button",
793
+ className: files_module_css_default.action,
794
+ title: t("action.open"),
795
+ onClick: (e) => {
796
+ e.stopPropagation();
797
+ onOpenExternal(entry.path);
798
+ },
799
+ children: "↗"
800
+ }), isDir && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
801
+ type: "button",
802
+ className: files_module_css_default.action,
803
+ title: t("action.refresh"),
804
+ onClick: (e) => {
805
+ e.stopPropagation();
806
+ onRefreshDir(entry.path);
807
+ },
808
+ children: "⟳"
809
+ })]
810
+ })
811
+ ]
812
+ });
813
+ });
745
814
  function FileExplorer({ width, useSessions, t, listDir, openPath }) {
746
815
  const sessionList = useSessions((s) => s);
747
816
  const currentId = sessionList.current;
@@ -769,6 +838,8 @@ window.__ModuleLoader__.load({
769
838
  expanded,
770
839
  rootLoading
771
840
  };
841
+ const childrenRef = (0, react.useRef)(children);
842
+ childrenRef.current = children;
772
843
  (0, react.useEffect)(() => {
773
844
  document.body.style.setProperty("--dsh-explorer-width", `${width}px`);
774
845
  return () => {
@@ -790,12 +861,18 @@ window.__ModuleLoader__.load({
790
861
  setRootLoading(true);
791
862
  const controller = new AbortController();
792
863
  rootAbortRef.current = controller;
793
- listDir(cwd, controller.signal).then((result) => {
864
+ listDir(cwd, controller.signal).then(async (result) => {
794
865
  if (controller.signal.aborted) return;
795
866
  setRoot(result.root);
796
867
  setChildren({ [result.root]: result.entries });
797
868
  setRootError(void 0);
798
- for (const dirPath of expanded) if (dirPath !== result.root) loadDir(dirPath);
869
+ const dirs = [...expanded].filter((dirPath) => dirPath !== result.root);
870
+ for (let i = 0; i < dirs.length; i += DIR_LOAD_BATCH) {
871
+ if (controller.signal.aborted) return;
872
+ const batch = dirs.slice(i, i + DIR_LOAD_BATCH);
873
+ await Promise.all(batch.map((dirPath) => loadDir(dirPath)));
874
+ if (i + DIR_LOAD_BATCH < dirs.length) await new Promise((resolve) => setTimeout(resolve, DIR_LOAD_GAP_MS));
875
+ }
799
876
  }).catch((error) => {
800
877
  if (controller.signal.aborted) return;
801
878
  setRootError(error instanceof Error ? error.message : String(error));
@@ -905,12 +982,8 @@ window.__ModuleLoader__.load({
905
982
  [cwdKey]: next
906
983
  };
907
984
  });
908
- if (!children[dirPath]) loadDir(dirPath);
909
- }, [
910
- cwdKey,
911
- children,
912
- loadDir
913
- ]);
985
+ if (!childrenRef.current[dirPath]) loadDir(dirPath);
986
+ }, [cwdKey, loadDir]);
914
987
  const refreshDir = (0, react.useCallback)((dirPath) => {
915
988
  setChildren((prev) => {
916
989
  const next = { ...prev };
@@ -946,59 +1019,17 @@ window.__ModuleLoader__.load({
946
1019
  const isLoading = isDir && loadingDirs.has(entry.path);
947
1020
  const childEntries = isDir && isExpanded ? children[entry.path] : void 0;
948
1021
  const error = isDir ? dirErrors[entry.path] : void 0;
949
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
950
- className: `${files_module_css_default.row} ${entry.kind === "file" && activePath === entry.path ? files_module_css_default.rowSelected : ""}`,
951
- style: { paddingLeft: 8 + depth * 14 },
952
- onClick: () => onRowClick(entry),
953
- onDoubleClick: () => onRowDoubleClick(entry),
954
- role: "treeitem",
955
- "aria-selected": entry.kind === "file" && activePath === entry.path,
956
- "aria-expanded": isDir ? isExpanded : void 0,
957
- title: entry.name,
958
- children: [
959
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
960
- className: files_module_css_default.chevron,
961
- onClick: (e) => {
962
- e.stopPropagation();
963
- if (isDir) toggleDir(entry.path);
964
- },
965
- children: isDir ? isExpanded ? "▾" : "▸" : ""
966
- }),
967
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
968
- className: files_module_css_default.icon,
969
- children: isDir ? isExpanded ? "📂" : "📁" : entry.kind === "file" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileIcon, { name: entry.name }) : "·"
970
- }),
971
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
972
- className: files_module_css_default.name,
973
- children: entry.name
974
- }),
975
- entry.kind === "file" && entry.size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
976
- className: files_module_css_default.size,
977
- children: formatSize$1(entry.size)
978
- }),
979
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
980
- className: files_module_css_default.actions,
981
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
982
- type: "button",
983
- className: files_module_css_default.action,
984
- title: t("action.open"),
985
- onClick: (e) => {
986
- e.stopPropagation();
987
- openPath(entry.path);
988
- },
989
- children: "↗"
990
- }), isDir && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
991
- type: "button",
992
- className: files_module_css_default.action,
993
- title: t("action.refresh"),
994
- onClick: (e) => {
995
- e.stopPropagation();
996
- refreshDir(entry.path);
997
- },
998
- children: "⟳"
999
- })]
1000
- })
1001
- ]
1022
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeRow, {
1023
+ entry,
1024
+ depth,
1025
+ isActive: entry.kind === "file" && activePath === entry.path,
1026
+ isExpanded,
1027
+ onToggleDir: toggleDir,
1028
+ onRefreshDir: refreshDir,
1029
+ onSelect: onRowClick,
1030
+ onDoubleClick: onRowDoubleClick,
1031
+ onOpenExternal: openPath,
1032
+ t
1002
1033
  }), isDir && isExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1003
1034
  isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1004
1035
  className: files_module_css_default.rowHint,
@@ -8705,6 +8736,20 @@ window.__ModuleLoader__.load({
8705
8736
  const PREVIEW_MIN = 240;
8706
8737
  const CHAT_MIN = 240;
8707
8738
  const PREVIEW_TOO_LARGE_LABEL = "512KB";
8739
+ /**
8740
+ * Above this size the overlay editor (syntax-highlight layer + transparent
8741
+ * textarea) falls back to a plain textarea: re-injecting and re-laying out
8742
+ * hundreds of KB of wrapped text on every keystroke is what makes the page
8743
+ * lag. The plain textarea keeps editing, wrapping and scrolling — it only
8744
+ * loses the colors, which files this big rarely need anyway.
8745
+ */
8746
+ const HIGHLIGHT_MAX_BYTES = 65536;
8747
+ /**
8748
+ * Languages always rendered as a plain textarea, never the overlay: the
8749
+ * highlight layer costs a full extra layout pass (and with CJK text a
8750
+ * fragile alignment surface) for prose formats where colors add little.
8751
+ */
8752
+ const PLAIN_LANGUAGES = /* @__PURE__ */ new Set(["markdown"]);
8708
8753
  function basenameOf(path) {
8709
8754
  const trimmed = path.replace(/[\\/]+$/, "");
8710
8755
  const idx = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
@@ -8750,35 +8795,38 @@ window.__ModuleLoader__.load({
8750
8795
  const previewRef = (0, react.useRef)(null);
8751
8796
  const handleRef = (0, react.useRef)(null);
8752
8797
  const highlightRef = (0, react.useRef)(null);
8798
+ const textareaRef = (0, react.useRef)(null);
8753
8799
  const refresh = (0, react.useCallback)(() => bump((v) => v + 1), []);
8754
8800
  (0, react.useEffect)(() => {
8755
8801
  const cache = cacheRef.current;
8756
8802
  for (const [path] of cache) if (!tabs.includes(path)) cache.delete(path);
8757
- const controllers = [];
8758
- for (const path of tabs) {
8759
- if (cache.has(path)) continue;
8760
- cache.set(path, { status: "loading" });
8761
- const controller = new AbortController();
8762
- controllers.push(controller);
8763
- readFile(path, controller.signal).then((result) => {
8764
- if (!controller.signal.aborted) {
8765
- cache.set(path, previewDataOf(result));
8766
- refresh();
8767
- }
8768
- }).catch((error) => {
8769
- if (!controller.signal.aborted) {
8770
- cache.set(path, {
8771
- status: "error",
8772
- message: error instanceof Error ? error.message : String(error)
8773
- });
8774
- refresh();
8775
- }
8776
- });
8803
+ const target = active ?? tabs[0];
8804
+ if (target === void 0 || cache.has(target)) {
8805
+ refresh();
8806
+ return;
8777
8807
  }
8808
+ const controller = new AbortController();
8809
+ cache.set(target, { status: "loading" });
8810
+ readFile(target, controller.signal).then((result) => {
8811
+ if (!controller.signal.aborted) {
8812
+ cache.set(target, previewDataOf(result));
8813
+ if (result.size > HIGHLIGHT_MAX_BYTES) requestAnimationFrame(refresh);
8814
+ else refresh();
8815
+ }
8816
+ }).catch((error) => {
8817
+ if (!controller.signal.aborted) {
8818
+ cache.set(target, {
8819
+ status: "error",
8820
+ message: error instanceof Error ? error.message : String(error)
8821
+ });
8822
+ refresh();
8823
+ }
8824
+ });
8778
8825
  refresh();
8779
- return () => controllers.forEach((c) => c.abort());
8826
+ return () => controller.abort();
8780
8827
  }, [
8781
8828
  tabs,
8829
+ active,
8782
8830
  readFile,
8783
8831
  refresh
8784
8832
  ]);
@@ -8798,6 +8846,28 @@ window.__ModuleLoader__.load({
8798
8846
  }
8799
8847
  }, [tabs.length]);
8800
8848
  const isOpen = !collapsed && (tabs.length > 0 || closing || hasOpenedRef.current);
8849
+ const activeData = active !== void 0 ? cacheRef.current.get(active) : void 0;
8850
+ const language = active !== void 0 ? detectLanguage(active) : void 0;
8851
+ const tooBig = (activeData?.size ?? 0) > HIGHLIGHT_MAX_BYTES;
8852
+ const plain = language === void 0 || tooBig || language !== void 0 && PLAIN_LANGUAGES.has(language);
8853
+ (0, react.useEffect)(() => {
8854
+ const pre = highlightRef.current;
8855
+ const ta = textareaRef.current;
8856
+ if (pre === null || ta === null || plain) return void 0;
8857
+ let raf = 0;
8858
+ const tick = () => {
8859
+ pre.scrollTop = ta.scrollTop;
8860
+ pre.scrollLeft = ta.scrollLeft;
8861
+ raf = requestAnimationFrame(tick);
8862
+ };
8863
+ raf = requestAnimationFrame(tick);
8864
+ return () => cancelAnimationFrame(raf);
8865
+ }, [
8866
+ isOpen,
8867
+ active,
8868
+ activeData?.status,
8869
+ plain
8870
+ ]);
8801
8871
  (0, react.useEffect)(() => {
8802
8872
  if (!isOpen) {
8803
8873
  document.body.style.removeProperty("--dsh-preview-width");
@@ -8894,7 +8964,6 @@ window.__ModuleLoader__.load({
8894
8964
  const onTabDragEnd = (0, react.useCallback)(() => {
8895
8965
  setDragPath(void 0);
8896
8966
  }, []);
8897
- const activeData = active !== void 0 ? cacheRef.current.get(active) : void 0;
8898
8967
  if (!isOpen) return null;
8899
8968
  const renderBody = () => {
8900
8969
  if (activeData === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -8906,35 +8975,36 @@ window.__ModuleLoader__.load({
8906
8975
  className: files_module_css_default.previewHint,
8907
8976
  children: t("preview.loading")
8908
8977
  });
8909
- case "loaded": {
8910
- const highlighted = active !== void 0 ? highlightCode(activeData.draft ?? "", active) : "";
8911
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8912
- className: files_module_css_default.editor,
8913
- "data-wrap": wrap ? "on" : "off",
8914
- children: [
8915
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
8916
- ref: highlightRef,
8917
- className: files_module_css_default.editorHighlight,
8918
- "aria-hidden": "true",
8919
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { dangerouslySetInnerHTML: { __html: highlighted } })
8920
- }),
8921
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
8922
- className: files_module_css_default.editorTextarea,
8923
- value: activeData.draft ?? "",
8924
- onChange: (e) => onTextareaChange(e.target.value),
8925
- onScroll: onTextareaScroll,
8926
- onKeyDown,
8927
- spellCheck: false,
8928
- wrap: wrap ? "soft" : "off",
8929
- title: t("action.saveHint")
8930
- }),
8931
- saveError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8932
- className: files_module_css_default.saveError,
8933
- children: saveError
8934
- })
8935
- ]
8936
- });
8937
- }
8978
+ case "loaded": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8979
+ className: `${files_module_css_default.editor}${plain ? ` ${files_module_css_default.editorPlain}` : ""}`,
8980
+ "data-wrap": wrap ? "on" : "off",
8981
+ children: [
8982
+ !plain && active !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
8983
+ ref: highlightRef,
8984
+ className: files_module_css_default.editorHighlight,
8985
+ "aria-hidden": "true",
8986
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { dangerouslySetInnerHTML: { __html: highlightCode(activeData.draft ?? "", active) } })
8987
+ }),
8988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
8989
+ ref: textareaRef,
8990
+ className: files_module_css_default.editorTextarea,
8991
+ value: activeData.draft ?? "",
8992
+ onChange: (e) => onTextareaChange(e.target.value),
8993
+ onScroll: onTextareaScroll,
8994
+ onKeyDown,
8995
+ spellCheck: false,
8996
+ wrap: wrap ? "soft" : "off"
8997
+ }),
8998
+ language !== void 0 && tooBig && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8999
+ className: files_module_css_default.editorHint,
9000
+ children: t("preview.highlightOff")
9001
+ }),
9002
+ saveError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9003
+ className: files_module_css_default.saveError,
9004
+ children: saveError
9005
+ })
9006
+ ]
9007
+ });
8938
9008
  case "binary": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8939
9009
  className: files_module_css_default.previewHint,
8940
9010
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("preview.binary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -9051,7 +9121,8 @@ window.__ModuleLoader__.load({
9051
9121
  "preview.binary": "二进制文件,无法预览",
9052
9122
  "preview.tooLarge": "文件过大,仅支持预览不超过 {limit}",
9053
9123
  "preview.emptyDir": "空目录",
9054
- "preview.error": "加载失败"
9124
+ "preview.error": "加载失败",
9125
+ "preview.highlightOff": "大文件:已关闭语法高亮(仍可编辑)"
9055
9126
  };
9056
9127
  const en = {
9057
9128
  "placeholder.noSession": "No session",
@@ -9069,7 +9140,8 @@ window.__ModuleLoader__.load({
9069
9140
  "preview.binary": "Binary file, cannot preview",
9070
9141
  "preview.tooLarge": "File too large to preview (limit {limit})",
9071
9142
  "preview.emptyDir": "Empty directory",
9072
- "preview.error": "Load failed"
9143
+ "preview.error": "Load failed",
9144
+ "preview.highlightOff": "Large file: syntax highlighting off (still editable)"
9073
9145
  };
9074
9146
  //#endregion
9075
9147
  //#region src/client/index.ts