@lyhue1991/dsh-soup 0.2.0

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/lib/client.js ADDED
@@ -0,0 +1,2117 @@
1
+ /**
2
+ * dsh-soup — 浏览器半区:项目资源管理器。
3
+ *
4
+ * 布局:注册进 shell 原生的 `details` 右列(并列网格,非悬浮),
5
+ * 有 Session log 时在它右侧放 📁;空白新会话则在输入框上方、
6
+ * workspace/preset 控件行的右端提供同一入口。
7
+ * 宽度与左侧工作区一致:默认 280px、可拖 264–420px(直接改写 shell 网格
8
+ * 的 details 轨,并用 MutationObserver 在 shell 重渲染后维持宽度)。
9
+ *
10
+ * 与宿主通信:走同源 HTTP 路由 `/api/dsh-soup`(POST JSON),
11
+ * 即永久插件(profile bundle)规范桥梁,而非 dynamic 半区的 host.call。
12
+ * 文件「预览」为只读展示:markdown/html/json/csv 有专属渲染器,图片预览,
13
+ * 其余按纯文本;不提供编辑与保存。
14
+ */
15
+ window.__ModuleLoader__.load({
16
+ id: '@lyhue1991/dsh-soup',
17
+ factory: (require) => {
18
+ var module = { exports: {} }
19
+ var exports = module.exports
20
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
21
+
22
+ var React = require('react')
23
+ var create = React.createElement
24
+
25
+ // 可选依赖:DSH 自带的不可信 Markdown 渲染器(micromark 管线,与聊天
26
+ // 消息同款;不安全协议与相对链接被禁用)与 Shiki 代码高亮块(CodeBlock,
27
+ // 懒加载语法包 + 复制按钮)。旧版宿主缺该模块时回退纯文本预览。
28
+ // 注意:export 是 React.memo() 的产物——是 exotic 组件对象而非 function,
29
+ // 判断只能验真值,不能 typeof==='function'。
30
+ var MarkdownText = null
31
+ var CodeBlock = null
32
+ try {
33
+ var uiPrim = require('@deepseek-ai/dsh-client-ui-primitives')
34
+ var cand = uiPrim && uiPrim.MarkdownText
35
+ if (cand) MarkdownText = cand
36
+ else {
37
+ try { window.__DSH_TREE_REQ_ERR = 'seed missing MarkdownText; type=' + typeof cand } catch (_) {}
38
+ }
39
+ var candBlock = uiPrim && uiPrim.CodeBlock
40
+ if (candBlock) CodeBlock = candBlock
41
+ } catch (err) {
42
+ try { window.__DSH_TREE_REQ_ERR = 'require threw: ' + String((err && err.message) || err) } catch (_) {}
43
+ }
44
+
45
+ // ------------------------------------------------------------------
46
+ // 样式:一次性注入 <style data-plugin>(与官方插件 css 内联约定一致)
47
+ // ------------------------------------------------------------------
48
+ var EXPL_CSS = '.expl-panel{position:relative;height:100%;min-width:0;width:100%;display:flex;flex-direction:column;background:var(--dsw-specific-sidebar-fill);color:var(--dsw-alias-label-primary);font-size:13px;font-family:var(--dsw-font-family);overflow:hidden;box-sizing:border-box;}' +
49
+ '.expl-resize{position:absolute;left:0;top:0;bottom:0;width:7px;cursor:col-resize;z-index:6;touch-action:none;}' +
50
+ '.expl-resize::after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:3px;height:40px;border-radius:2px;background:var(--dsw-alias-interactive-bg-hover);opacity:0;transition:opacity .15s;}' +
51
+ '.expl-resize:hover::after{opacity:1;}' +
52
+ '[data-side="details"]{display:none!important;}' +
53
+ '.expl-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:12px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;}' +
54
+ '.expl-title{font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;font-size:14px;}' +
55
+ '.expl-head-btns{display:flex;gap:2px;flex-wrap:nowrap;}' +
56
+ '.expl-btn{background:transparent;border:none;color:var(--dsw-alias-label-secondary);font-size:15px;cursor:pointer;padding:3px 7px;border-radius:8px;}' +
57
+ '.expl-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}' +
58
+ '.expl-path{font-size:11px;color:var(--dsw-alias-label-secondary);padding:7px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;text-align:left;flex:none;cursor:default;}' +
59
+ '.expl-error{color:var(--dsw-alias-state-error-primary);padding:6px 14px;font-size:12px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;}' +
60
+ '.expl-notice{color:var(--dsw-alias-state-success-primary);padding:6px 14px;font-size:12px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;}' +
61
+ '.expl-bulk{color:var(--dsw-alias-state-business-primary);padding:6px 14px;font-size:12px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;}' +
62
+ '.expl-body{overflow:auto;flex:1;padding:6px 0 12px;}' +
63
+ '.expl-row-main{display:flex;align-items:center;gap:6px;padding-top:2px;padding-bottom:2px;padding-right:10px;cursor:pointer;border-radius:8px;margin:0 4px;height:40px;box-sizing:border-box;user-select:none;}' +
64
+ '.expl-row-main:hover{background:var(--dsw-alias-interactive-bg-hover);}' +
65
+ '.expl-row-main.selected{background:var(--dsw-alias-interactive-bg-hover-accent);}' +
66
+ '.expl-row-main.drop-target,.expl-body.drop-target{outline:1px dashed var(--dsw-alias-state-business-primary);outline-offset:-2px;background:var(--dsw-alias-interactive-bg-hover-accent);}' +
67
+ '.expl-caret{flex:none;cursor:pointer;line-height:1;}' +
68
+ '.expl-caret-big{width:24px;font-size:33px;color:var(--dsw-alias-label-secondary);display:flex;align-items:center;justify-content:flex-start;}' +
69
+ '.expl-caret-sm{width:24px;font-size:33px;color:var(--dsw-alias-label-tertiary);display:flex;align-items:center;justify-content:flex-start;}' +
70
+ '.expl-icon{flex:none;display:flex;align-items:center;}.expl-icon svg{width:16px;height:16px;display:block;}' +
71
+ '.expl-uploads{flex:none;border-bottom:1px solid var(--dsw-alias-border-l1);padding:5px 14px;display:flex;flex-direction:column;gap:3px;background:var(--dsw-specific-tip);}' +
72
+ '.expl-upload-row{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--dsw-alias-label-secondary);}' +
73
+ '.expl-upload-done{color:var(--dsw-alias-state-success-primary);}' +
74
+ '.expl-upload-pct{margin-left:auto;font-family:var(--ds-font-family-code);color:var(--dsw-alias-label-caption);}' +
75
+ '.expl-upload-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}' +
76
+ '.expl-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);}' +
77
+ '.expl-size{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;margin-left:8px;}' +
78
+ '.expl-muted{color:var(--dsw-alias-label-tertiary);padding:4px 14px;font-size:12px;}' +
79
+ '.expl-menu-mask{position:fixed;inset:0;z-index:1980;}' +
80
+ '.expl-menu{position:fixed;z-index:1990;min-width:172px;background:var(--dsw-specific-menu);border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;padding:4px;box-shadow:var(--dsw-shadow-lv3);pointer-events:auto;}' +
81
+ '.expl-menu-item{display:block;width:100%;text-align:left;background:transparent;border:none;color:var(--dsw-alias-label-primary);font-size:13px;padding:6px 10px;border-radius:8px;cursor:pointer;line-height:1.4;}' +
82
+ '.expl-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover);}' +
83
+ '.expl-danger{color:var(--dsw-alias-state-error-primary);} .expl-menu-item.expl-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger);}' +
84
+ '.expl-menu-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1);}' +
85
+ '.expl-inline-input{flex:1;min-width:0;height:30px;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-state-business-primary);border-radius:6px;padding:0 5px;outline:none;font-family:var(--ds-font-family-code);}' +
86
+ '.expl-toggle{display:inline-flex;align-items:center;justify-content:center;cursor:pointer;} ' +
87
+ '.expl-toggle:hover{background:var(--dsw-alias-interactive-bg-hover);}' +
88
+ '.expl-tool{width:34px;height:32px;padding:0;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:transparent;border-radius:18px;font-size:15px;line-height:1;}' +
89
+ '.expl-active{background:var(--dsw-alias-interactive-bg-hover-accent);border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);}' +
90
+ '.expl-hero-dock{position:relative;width:100%;height:0;margin-top:-8px;pointer-events:none;}' +
91
+ '.expl-hero-dock .expl-tool{position:absolute;right:calc(var(--dsh-composer-side-clearance) + 12px);bottom:0;width:28px;height:28px;border-radius:14px;font-size:14px;pointer-events:auto;}'
92
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="@lyhue1991/dsh-soup/explorer.css"]') === null) {
93
+ var tag = document.createElement('style')
94
+ tag.dataset.plugin = '@lyhue1991/dsh-soup'
95
+ tag.dataset.pluginCss = '@lyhue1991/dsh-soup/explorer.css'
96
+ tag.textContent = EXPL_CSS
97
+ document.head.appendChild(tag)
98
+ }
99
+
100
+ // GoalBar 多行版:沿用 DSH 原生 GoalBar 的结构和主题,仅放开单行截断。
101
+ // 布局分两行:head 行(图标 + 相位标签 + 操作按钮),body 行(objective 多行)。
102
+ var GOAL_CSS =
103
+ '.dsh-goal-dock{box-sizing:border-box;width:calc(100% - var(--dsh-composer-side-clearance) - var(--dsh-composer-side-clearance) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset));margin:0 auto;}' +
104
+ '.dsh-goal-bar{box-sizing:border-box;display:flex;flex-direction:column;gap:4px;width:100%;max-width:calc(var(--dsh-composer-card-max-width) - 4 * var(--dsh-composer-dock-inset));margin:0 auto;padding:6px 5px 6px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-specific-tip);font-family:var(--dsw-font-family);}' +
105
+ '.dsh-goal-head{display:flex;align-items:center;gap:10px;min-width:0;}' +
106
+ '.dsh-goal-glyph{display:inline-flex;align-items:center;height:24px;flex:none;color:var(--dsw-alias-label-tertiary);}' +
107
+ '.dsh-goal-label{flex:none;font-size:13px;line-height:24px;font-weight:500;color:var(--dsw-alias-label-primary);}' +
108
+ '.dsh-goal-rounds{flex:none;font-size:11px;line-height:18px;color:var(--dsw-alias-label-caption);font-family:var(--ds-font-family-code);}' +
109
+ '.dsh-goal-objective{font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary-dimmed);white-space:pre-wrap;overflow-wrap:anywhere;padding:0 22px 2px 0;}' +
110
+ '.dsh-goal-error{flex:1;min-width:0;overflow:hidden;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;}' +
111
+ '.dsh-goal-input{box-sizing:border-box;width:100%;min-height:64px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-base);font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);resize:none;overflow:hidden;outline:none;}' +
112
+ '.dsh-goal-input:focus{border-color:var(--dsw-alias-state-business-primary);}' +
113
+ '.dsh-goal-actions{display:flex;align-items:center;gap:10px;flex:none;height:28px;margin-left:auto;}' +
114
+ '.dsh-goal-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:none;border-radius:999px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;}' +
115
+ '.dsh-goal-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);}' +
116
+ '.dsh-goal-btn:disabled{opacity:.4;cursor:default;}'
117
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="@lyhue1991/dsh-soup/goal.css"]') === null) {
118
+ var goalTag = document.createElement('style')
119
+ goalTag.dataset.plugin = '@lyhue1991/dsh-soup'
120
+ goalTag.dataset.pluginCss = '@lyhue1991/dsh-soup/goal.css'
121
+ goalTag.textContent = GOAL_CSS
122
+ document.head.appendChild(goalTag)
123
+ }
124
+
125
+ // 文件标签页视图(conversation.view 'dsh-soup-files'):子 tab 条 + 预览区。
126
+ // 纯预览(无编辑):markdown/html/json/csv 各有专属渲染器。
127
+ var FILEVIEW_CSS =
128
+ '.dfv-root{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--dsw-specific-sidebar-fill);color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);}' +
129
+ '.dfv-tabbar{display:flex;align-items:center;gap:2px;flex:none;overflow-x:auto;padding:6px 10px 0;border-bottom:1px solid var(--dsw-alias-border-l1);scrollbar-width:none;}' +
130
+ '.dfv-tabbar::-webkit-scrollbar{display:none;}' +
131
+ '.dfv-tab{display:inline-flex;align-items:center;gap:5px;padding:5px 8px 5px 10px;border:none;border-radius:8px 8px 0 0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.4;cursor:pointer;white-space:nowrap;max-width:220px;}' +
132
+ '.dfv-tab:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}' +
133
+ '.dfv-tab.active{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:inset 0 -2px 0 var(--dsw-alias-state-business-primary);}' +
134
+ '.dfv-tab-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}' +
135
+ '.dfv-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border:none;border-radius:4px;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1;cursor:pointer;padding:0;}' +
136
+ '.dfv-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}' +
137
+ '.dfv-body{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);}' +
138
+ '.dfv-toolbar{display:flex;align-items:center;gap:8px;flex:none;padding:6px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:11px;color:var(--dsw-alias-label-secondary);}' +
139
+ '.dfv-toolbar-path{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left;font-family:var(--ds-font-family-code);}' +
140
+ '.dfv-btn{flex:none;background:transparent;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);font-size:11px;line-height:1;padding:4px 9px;border-radius:7px;cursor:pointer;}' +
141
+ '.dfv-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}' +
142
+ '.dfv-error{padding:8px 14px;font-size:12px;color:var(--dsw-alias-state-error-primary);flex:none;}' +
143
+ '.dfv-muted{padding:24px 14px;font-size:13px;color:var(--dsw-alias-label-tertiary);text-align:center;}' +
144
+ '.dfv-cap{padding:5px 14px;font-size:11px;color:var(--dsw-alias-label-caption);border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;}' +
145
+ '.dfv-code{flex:1;min-height:0;width:100%;box-sizing:border-box;margin:0;padding:10px 14px;overflow:auto;background:transparent;color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code);font-size:12.5px;line-height:1.55;white-space:pre;tab-size:2;user-select:text;}' +
146
+ '.dfv-code-wrap{flex:1;min-height:0;overflow:auto;padding:8px 10px;}' +
147
+ '.dfv-nb{flex:1;min-height:0;overflow:auto;background:var(--dsw-alias-bg-base);}' +
148
+ '.dfv-nb-cell{display:flex;border-bottom:1px solid var(--dsw-alias-border-l1);}' +
149
+ '.dfv-nb-gutter{flex:none;width:44px;padding:8px 6px;text-align:right;font-family:var(--ds-font-family-code);font-size:11px;color:var(--dsw-alias-label-caption);border-right:1px solid var(--dsw-alias-border-l1);user-select:none;background:var(--dsw-specific-sidebar-fill);}' +
150
+ '.dfv-nb-cellbody{flex:1;min-width:0;padding:4px 0;}' +
151
+ '.dfv-nb-cellbody .md-code-block,.dfv-nb-cellbody .dfv-code{margin:4px 10px;border-radius:8px;overflow:hidden;}' +
152
+ '.dfv-nb-cell.dfv-nb-markdown .dfv-md-wrap{padding:2px 14px;}' +
153
+ '.dfv-nb-raw{color:var(--dsw-alias-label-secondary);}' +
154
+ '.dfv-nb-outputs{border-top:1px dashed var(--dsw-alias-border-l1);padding:6px 10px;display:flex;flex-direction:column;gap:8px;}' +
155
+ '.dfv-nb-out{margin:0;padding:6px 8px;font-family:var(--ds-font-family-code);font-size:12px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border-radius:6px;max-height:320px;overflow:auto;}' +
156
+ '.dfv-nb-out-err{color:var(--dsw-alias-state-error-primary);}' +
157
+ '.dfv-nb-error{border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;overflow:hidden;}' +
158
+ '.dfv-nb-error-head{padding:5px 10px;font-family:var(--ds-font-family-code);font-size:12px;font-weight:600;color:#fff;background:var(--dsw-alias-state-error-primary);}' +
159
+ '.dfv-nb-trace{margin:0;padding:8px 10px;font-family:var(--ds-font-family-code);font-size:11.5px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-1);max-height:260px;overflow:auto;}' +
160
+ '.dfv-nb-img{max-width:100%;height:auto;border-radius:6px;}' +
161
+ '.dfv-nb-html{width:100%;height:240px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:#fff;}' +
162
+ '.dfv-md-wrap{flex:1;min-height:0;overflow:auto;background:var(--dsw-alias-bg-base);}' +
163
+ '.dfv-md{padding:6px 18px 16px;font-size:13px;line-height:1.65;color:var(--dsw-alias-label-primary);}' +
164
+ '.dfv-frame{flex:1;min-height:0;width:100%;border:none;background:#fff;}' +
165
+ '.dfv-image-wrap{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:16px;overflow:auto;}' +
166
+ '.dfv-image{max-width:100%;max-height:100%;object-fit:contain;border-radius:8px;box-shadow:var(--dsw-shadow-lv2);}' +
167
+ '.dfv-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:13px;}' +
168
+ '.dfv-json .dfv-tok-key{color:#82aaff;}.dfv-json .dfv-tok-str{color:#9ece8f;}.dfv-json .dfv-tok-num{color:#e0a96d;}.dfv-json .dfv-tok-bool,.dfv-json .dfv-tok-null{color:#c792ea;}' +
169
+ '.dfv-table-wrap{flex:1;min-height:0;overflow:auto;}' +
170
+ '.dfv-table{border-collapse:collapse;font-size:12px;font-family:var(--ds-font-family-code);}' +
171
+ '.dfv-th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:600;text-align:left;padding:5px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);border-right:1px solid var(--dsw-alias-border-l1);white-space:nowrap;}' +
172
+ '.dfv-td{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;max-width:360px;}'
173
+ if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="@lyhue1991/dsh-soup/fileview.css"]') === null) {
174
+ var fileviewTag = document.createElement('style')
175
+ fileviewTag.dataset.plugin = '@lyhue1991/dsh-soup'
176
+ fileviewTag.dataset.pluginCss = '@lyhue1991/dsh-soup/fileview.css'
177
+ fileviewTag.textContent = FILEVIEW_CSS
178
+ document.head.appendChild(fileviewTag)
179
+ }
180
+
181
+ /** 需要的客户端服务:slots、layout、timer、sessions + remote.goals(goal 动作动词)。 */
182
+ var inject = ['slots', 'layout', 'timer', 'sessions', 'remote', 'remote.goals']
183
+
184
+ // ------------------------------------------------------------------
185
+ // 状态存储(模块级,供 Panel 与 HeaderAction 共享)
186
+ // ------------------------------------------------------------------
187
+ var state = {
188
+ open: false, cwd: '', tree: [], menu: null, error: '', notice: '',
189
+ selected: new Set(), lastIndex: null,
190
+ renaming: null, newItem: null,
191
+ dropTarget: null, dragPaths: null,
192
+ files: { list: [], active: null },
193
+ uploads: [],
194
+ }
195
+ var listeners = new Set()
196
+ function setState(patch) {
197
+ state = Object.assign({}, state, patch)
198
+ listeners.forEach(function (fn) { fn() })
199
+ }
200
+ function subscribe(fn) {
201
+ listeners.add(fn)
202
+ return function () { listeners.delete(fn) }
203
+ }
204
+ function useStore() {
205
+ var force = React.useReducer(function (x) { return x + 1 }, 0)[1]
206
+ React.useEffect(function () { return subscribe(function () { force() }) }, [])
207
+ return state
208
+ }
209
+
210
+ var visibleRows = []
211
+ var uploadInputEl = null
212
+ var layout = null
213
+
214
+ var DETAILS_MIN = 264
215
+ var DETAILS_MAX = 420
216
+ var DETAILS_DEFAULT = 280
217
+ function clampW(w) { return Math.max(DETAILS_MIN, Math.min(DETAILS_MAX, w)) }
218
+
219
+ function pathJoin(dir, name) { return String(dir || '/').replace(/\/+$/, '') + '/' + name }
220
+ function parentOf(path) {
221
+ var p = String(path || '/').replace(/\/+$/, '')
222
+ var i = p.lastIndexOf('/')
223
+ return i <= 0 ? '/' : p.slice(0, i)
224
+ }
225
+ function baseName(path) {
226
+ var p = String(path || '').replace(/\/+$/, '')
227
+ var i = p.lastIndexOf('/')
228
+ return i < 0 ? p : p.slice(i + 1)
229
+ }
230
+
231
+ // ------------------------------------------------------------------
232
+ // 与宿主通信:POST /api/dsh-soup
233
+ // ------------------------------------------------------------------
234
+ function hostBase() {
235
+ var origin = globalThis.location && globalThis.location.origin
236
+ return origin !== undefined && origin !== 'null' && origin !== '' ? origin : 'http://dsh.internal'
237
+ }
238
+ function rpc(action, args) {
239
+ return fetch(new URL('/api/dsh-soup', hostBase()), {
240
+ method: 'POST',
241
+ // x-dsh-soup:宿主路由的强制头(防跨站简单请求伪造),
242
+ // content-type 必须 application/json,两者缺一会被 403。
243
+ headers: { 'content-type': 'application/json', 'x-dsh-soup': '1' },
244
+ body: JSON.stringify({ action: action, args: args || {} }),
245
+ }).then(function (res) {
246
+ return res.json().catch(function () { return { ok: false, error: '宿主响应解析失败' } })
247
+ }).catch(function (err) {
248
+ return { ok: false, error: '宿主不可达: ' + String((err && err.message) || err) }
249
+ })
250
+ }
251
+
252
+ async function loadDir(path, attempt) {
253
+ var res = await rpc('list', { path: path, sessionId: activeSessionId })
254
+ // 新会话刚切换时,宿主会话注册表可能尚未纳入其 cwd——稍候重试。
255
+ if ((!res || !res.ok) && /超出允许范围/.test((res && res.error) || '') && (attempt || 0) < 3) {
256
+ await new Promise(function (r) { setTimeout(r, 600) })
257
+ return loadDir(path, (attempt || 0) + 1)
258
+ }
259
+ if (!res || !res.ok) {
260
+ setState({ error: (res && res.error) || '读取失败' })
261
+ return null
262
+ }
263
+ return res.entries || []
264
+ }
265
+
266
+ async function defaultCwd(sessionId, hint) {
267
+ // 以会话工作目录为准:优先用客户端会话快照里的 cwd(与官方细节栏同源,
268
+ // 始终指向"当前会话所在项目"),其次查宿主会话 header。
269
+ if (hint) return hint
270
+ if (sessionId) {
271
+ var res = await rpc('sessionCwd', { sessionId: sessionId })
272
+ if (res && res.ok && res.cwd) return res.cwd
273
+ }
274
+ // 已知会话但不能确定其工作目录时,回退到文件系统根,而不是"宿主启动目录"——
275
+ // 后者(root)会让资源管理器误显示与当前会话无关的启动路径。
276
+ return '/'
277
+ }
278
+
279
+ async function trackSession(sessionId, cwdHint) {
280
+ activeSessionId = sessionId || null
281
+ var cwd = await defaultCwd(sessionId, cwdHint)
282
+ // 会话切换:预览列表按新 cwd 裁剪(范围外自动关闭),并清掉上一路径的旧错误横幅
283
+ var files = pruneFilesToScope(state.files, cwd)
284
+ setState({ cwd: cwd, selected: new Set(), renaming: null, newItem: null, menu: null, error: '', notice: '', files: files })
285
+ var items = await loadDir(cwd)
286
+ if (items) setState({ error: '', tree: items })
287
+ }
288
+
289
+ async function refresh() {
290
+ if (!state.cwd) return
291
+ var items = await loadDir(state.cwd)
292
+ if (items) setState({ error: '', tree: items })
293
+ }
294
+
295
+ async function toggleNode(node) {
296
+ if (node.type !== 'directory') return
297
+ if (node.children == null) {
298
+ node.loading = true
299
+ setState({ tree: state.tree })
300
+ var items = await loadDir(node.path)
301
+ node.loading = false
302
+ if (items) items.forEach(function (c) { c.parent = node })
303
+ node.children = items || []
304
+ node.open = true
305
+ setState({ tree: state.tree })
306
+ } else {
307
+ node.open = !node.open
308
+ setState({ tree: state.tree })
309
+ }
310
+ }
311
+
312
+ function formatSize(n) {
313
+ if (typeof n !== 'number' || n < 0) return ''
314
+ if (n < 1024) return n + ' B'
315
+ if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'
316
+ if (n < 1073741824) return (n / 1048576).toFixed(1) + ' MB'
317
+ return (n / 1073741824).toFixed(1) + ' GB'
318
+ }
319
+
320
+ // ------------------------------------------------------------------
321
+ // 文件类型图标:采用 JupyterLab ui-components 的 filetype SVG 集
322
+ // (BSD-3-Clause License,github.com/jupyterlab/jupyterlab)。
323
+ // 固定品牌色、16px 渲染;未命中扩展名按「代码文件→编辑器图标、其余→空白文件」兜底。
324
+ // ------------------------------------------------------------------
325
+ var NB_SVG = {
326
+ folder: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 24 24"><path fill="#FBC02D" d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8z" class="jp-icon3 jp-icon-selectable"/></svg>',
327
+ folderFavorite: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#000" viewBox="0 0 24 24"><path fill="#F9A825" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2m-2.06 11L15 15.28 12.06 17l.78-3.33-2.59-2.24 3.41-.29L15 8l1.34 3.14 3.41.29-2.59 2.24z" class="jp-icon3 jp-icon-selectable"/></svg>',
328
+ file: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 22 22"><path fill="currentColor" d="m19.3 8.2-5.5-5.5c-.3-.3-.7-.5-1.2-.5H3.9c-.8.1-1.6.9-1.6 1.8v14.1c0 .9.7 1.6 1.6 1.6h14.2c.9 0 1.6-.7 1.6-1.6V9.4c.1-.5-.1-.9-.4-1.2m-5.8-3.3 3.4 3.6h-3.4zm3.9 12.7H4.7c-.1 0-.2 0-.2-.2V4.7c0-.2.1-.3.2-.3h7.2v4.4s0 .8.3 1.1 1.1.3 1.1.3h4.3v7.2s-.1.2-.2.2" class="jp-icon3 jp-icon-selectable"/></svg>',
329
+ textEditor: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 24 24"><path fill="currentColor" d="M15 15H3v2h12zm0-8H3v2h12zM3 13h18v-2H3zm0 8h18v-2H3zM3 3v2h18V3z" class="jp-text-editor-icon-color jp-icon-selectable"/></svg>',
330
+ markdown: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 22 22"><path fill="#7B1FA2" d="M5 14.9h12l-6.1 6zm9.4-6.8c0-1.3-.1-2.9-.1-4.5-.4 1.4-.9 2.9-1.3 4.3l-1.3 4.3h-2L8.5 7.9c-.4-1.3-.7-2.9-1-4.3-.1 1.6-.1 3.2-.2 4.6L7 12.4H4.8l.7-11h3.3L10 5c.4 1.2.7 2.7 1 3.9.3-1.2.7-2.6 1-3.9l1.2-3.7h3.3l.6 11h-2.4z" class="jp-icon-contrast0 jp-icon-selectable"/></svg>',
331
+ notebook: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 22 22"><g fill="#EF6C00" class="jp-notebook-icon-color jp-icon-selectable"><path d="M18.7 3.3v15.4H3.3V3.3zm1.5-1.5H1.8v18.3h18.3z"/><path d="m16.5 16.5-5.4-4.3-5.6 4.3v-11h11z"/></g></svg>',
332
+ json: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 22 22"><g fill="#F9A825" class="jp-json-icon-color jp-icon-selectable"><path d="M20.2 11.8c-1.6 0-1.7.5-1.7 1 0 .4.1.9.1 1.3.1.5.1.9.1 1.3 0 1.7-1.4 2.3-3.5 2.3h-.9v-1.9h.5c1.1 0 1.4 0 1.4-.8 0-.3 0-.6-.1-1 0-.4-.1-.8-.1-1.2 0-1.3 0-1.8 1.3-2-1.3-.2-1.3-.7-1.3-2 0-.4.1-.8.1-1.2.1-.4.1-.7.1-1 0-.8-.4-.7-1.4-.8h-.5V4.1h.9c2.2 0 3.5.7 3.5 2.3 0 .4-.1.9-.1 1.3-.1.5-.1.9-.1 1.3 0 .5.2 1 1.7 1zM1.8 10.1c1.6 0 1.7-.5 1.7-1 0-.4-.1-.9-.1-1.3-.1-.5-.1-.9-.1-1.3 0-1.6 1.4-2.3 3.5-2.3h.9v1.9h-.5c-1 0-1.4 0-1.4.8 0 .3 0 .6.1 1 0 .2.1.6.1 1 0 1.3 0 1.8-1.3 2C6 11.2 6 11.7 6 13c0 .4-.1.8-.1 1.2-.1.3-.1.7-.1 1 0 .8.3.8 1.4.8h.5v1.9h-.9c-2.1 0-3.5-.6-3.5-2.3 0-.4.1-.9.1-1.3.1-.5.1-.9.1-1.3 0-.5-.2-1-1.7-1z"/><circle cx="11" cy="13.8" r="2.1"/><circle cx="11" cy="8.2" r="2.1"/></g></svg>',
333
+ yaml: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 22 22"><g fill="#D81B60" class="jp-icon-contrast2 jp-icon-selectable"><path d="M7.2 18.6v-5.4L3 5.6h3.3l1.4 3.1c.3.9.6 1.6 1 2.5.3-.8.6-1.6 1-2.5l1.4-3.1h3.4l-4.4 7.6v5.5z"/><circle cx="17.6" cy="16.5" r="2.1"/><circle cx="17.6" cy="11" r="2.1"/></g></svg>',
334
+ python: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="-10 -10 131.161 132.389"><path fill="#306998" d="M54.919 0c-4.584.022-8.961.413-12.813 1.095C30.76 3.099 28.7 7.295 28.7 15.032v10.219h26.813v3.406H18.638c-7.793 0-14.616 4.684-16.75 13.594-2.462 10.213-2.571 16.586 0 27.25 1.905 7.938 6.457 13.594 14.25 13.594h9.218v-12.25c0-8.85 7.657-16.657 16.75-16.657h26.782c7.454 0 13.406-6.138 13.406-13.625v-25.53c0-7.267-6.13-12.726-13.406-13.938C64.282.328 59.502-.02 54.918 0m-14.5 8.22c2.77 0 5.031 2.298 5.031 5.125 0 2.816-2.262 5.093-5.031 5.093-2.78 0-5.031-2.277-5.031-5.093 0-2.827 2.251-5.125 5.03-5.125" class="jp-icon-selectable"/><path fill="#ffd43b" d="M85.638 28.657v11.906c0 9.231-7.826 17-16.75 17H42.106c-7.336 0-13.406 6.279-13.406 13.625V96.72c0 7.266 6.319 11.54 13.406 13.625 8.488 2.495 16.627 2.946 26.782 0 6.75-1.955 13.406-5.888 13.406-13.625V86.5H55.513v-3.405H95.7c7.793 0 10.696-5.436 13.406-13.594 2.8-8.399 2.68-16.476 0-27.25-1.925-7.758-5.604-13.594-13.406-13.594zM70.575 93.313c2.78 0 5.031 2.278 5.031 5.094 0 2.827-2.251 5.125-5.031 5.125-2.77 0-5.031-2.298-5.031-5.125 0-2.816 2.261-5.094 5.031-5.094" class="jp-icon-selectable"/></svg>',
335
+ julia: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 325 300"><path fill="#cb3c33" d="M150.898 225c0 41.422-33.578 75-75 75s-75-33.578-75-75 33.579-75 75-75 75 33.578 75 75" class="jp-brand0 jp-icon-selectable"/><path fill="#389826" d="M237.5 75c0 41.422-33.578 75-75 75s-75-33.578-75-75 33.578-75 75-75 75 33.578 75 75" class="jp-brand0 jp-icon-selectable"/><path fill="#9558b2" d="M324.102 225c0 41.422-33.579 75-75 75s-75-33.578-75-75 33.578-75 75-75 75 33.578 75 75" class="jp-brand0 jp-icon-selectable"/></svg>',
336
+ rKernel: '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" width="16" height="16" viewBox="0 0 724 561"><path class="jp-icon-selectable" d="M361.453,485.937 C162.329,485.937 0.906,377.828 0.906,244.469 C0.906,111.109 162.329,3.000 361.453,3.000 C560.578,3.000 722.000,111.109 722.000,244.469 C722.000,377.828 560.578,485.937 361.453,485.937 ZM416.641,97.406 C265.289,97.406 142.594,171.314 142.594,262.484 C142.594,353.654 265.289,427.562 416.641,427.562 C567.992,427.562 679.687,377.033 679.687,262.484 C679.687,147.971 567.992,97.406 416.641,97.406 Z" fill="rgb(179,179,179)" fill-rule="evenodd"/><path class="jp-icon-selectable" d="M550.000,377.000 C550.000,377.000 571.822,383.585 584.500,390.000 C588.899,392.226 596.510,396.668 602.000,402.500 C607.378,408.212 610.000,414.000 610.000,414.000 L696.000,559.000 L557.000,559.062 L492.000,437.000 C492.000,437.000 478.690,414.131 470.500,407.500 C463.668,401.969 460.755,400.000 454.000,400.000 C449.298,400.000 420.974,400.000 420.974,400.000 L421.000,558.974 L298.000,559.026 L298.000,152.938 L545.000,152.938 C545.000,152.938 657.500,154.967 657.500,262.000 C657.500,369.033 550.000,377.000 550.000,377.000 ZM496.500,241.024 L422.037,240.976 L422.000,310.026 L496.500,310.002 C496.500,310.002 531.000,309.895 531.000,274.877 C531.000,239.155 496.500,241.024 496.500,241.024 Z" fill="rgb(52,101,176)" fill-rule="evenodd"/></svg>',
337
+ spreadsheet: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 22 22"><path fill="#4CAF50" d="M2.2 2.2v17.6h17.6V2.2zm15.4 7.7h-5.5V4.4h5.5zM9.9 4.4v5.5H4.4V4.4zm-5.5 7.7h5.5v5.5H4.4zm7.7 5.5v-5.5h5.5v5.5z" class="jp-icon-contrast1 jp-icon-selectable"/></svg>',
338
+ pdf: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 22 22"><path fill="#FF2A2A" d="m17.933 13.667 3.987 3.987-1.117 1.117-2.51-2.51-1.066 1.067 2.36 2.36-1.117 1.117-2.36-2.36-2.43 2.429-1.477-1.478zm-6.163-3.93-3.496 3.496.53.53q.905.906 1.83.932.929.031 1.784-.825.852-.852.822-1.773t-.94-1.83zm-.36-2.594 1.557 1.558q1.305 1.305 1.754 2.13.456.825.468 1.727.015.79-.296 1.493-.31.702-.951 1.343-.649.648-1.355.963-.702.31-1.493.295-.905-.015-1.738-.472-.829-.46-2.118-1.75L5.68 12.874zm-5.76-5.76L8.1 3.835Q9.195 4.93 9.291 6q.103 1.07-.795 1.968-.901.902-1.976.802-1.067-.1-2.16-1.193l-.975-.975L1.397 8.59-.08 7.113Zm.406 2.548-1.6 1.6.817.818q.43.43.871.457.445.023.829-.361t.357-.825-.457-.871z" class="jp-icon-selectable"/></svg>',
339
+ image: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 22 22"><path fill="#FFF" d="M2.2 2.2h17.5v17.5H2.2z" class="jp-icon-brand4 jp-icon-selectable-inverse"/><path fill="#3F51B5" d="M2.2 2.2v17.5h17.5l.1-17.5zm12.1 2.2c1.2 0 2.2 1 2.2 2.2s-1 2.2-2.2 2.2-2.2-1-2.2-2.2 1-2.2 2.2-2.2M4.4 17.6l3.3-8.8 3.3 6.6 2.2-3.2 4.4 5.4z" class="jp-icon-brand0 jp-icon-selectable"/></svg>',
340
+ html5: '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 512 512"><path fill="#000" d="M108.4 0h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206 23h-20.3V0h63.7v23H229v46h-23m53.5-69h24.1l14.8 24.3L313.2 0h24.1v69h-23V34.8l-16.1 24.8-16.1-24.8V69h-22.6m89.2-69h23v46.2h32.6V69h-55.6" class="jp-icon0 jp-icon-selectable"/><path fill="#e44d26" d="m107.6 471-33-370.4h362.8l-33 370.2L255.7 512" class="jp-icon-selectable"/><path fill="#f16529" d="M256 480.5V131h148.3L376 447" class="jp-icon-selectable"/><path fill="#ebebeb" d="M142 176.3h114v45.4h-64.2l4.2 46.5h60v45.3H154.4m2 22.8H202l3.2 36.3 50.8 13.6v47.4l-93.2-26" class="jp-icon-selectable-inverse"/><path fill="#fff" d="M369.6 176.3H255.8v45.4h109.6m-4.1 46.5H255.8v45.4h56l-5.3 59-50.7 13.6v47.2l93-25.8" class="jp-icon-selectable-inverse"/></svg>',
341
+ }
342
+
343
+ /** 扩展名 → JupyterLab filetype 图标;未命中走代码编辑器/空白文件兜底。 */
344
+ function iconSvgFor(node) {
345
+ if (node.type === 'directory') return node.open ? NB_SVG.folderFavorite : NB_SVG.folder
346
+ var ext = extOfName(node.name)
347
+ if (Object.prototype.hasOwnProperty.call(FILE_ICON_BY_EXT, ext)) return FILE_ICON_BY_EXT[ext]
348
+ // 已知代码/配置语言 → 文本编辑器图标;其余(LICENSE、无扩展名等)→ 空白文件
349
+ var codeish = Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, ext)
350
+ || ['css', 'scss', 'less', 'xml', 'log', 'gitignore'].indexOf(ext) >= 0
351
+ || ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'].indexOf(ext) >= 0
352
+ return codeish ? NB_SVG.textEditor : NB_SVG.file
353
+ }
354
+
355
+ var FILE_ICON_BY_EXT = {
356
+ ipynb: NB_SVG.notebook,
357
+ md: NB_SVG.markdown, markdown: NB_SVG.markdown,
358
+ json: NB_SVG.json, jsonc: NB_SVG.json,
359
+ yaml: NB_SVG.yaml, yml: NB_SVG.yaml,
360
+ py: NB_SVG.python, jl: NB_SVG.julia, r: NB_SVG.rKernel,
361
+ csv: NB_SVG.spreadsheet, tsv: NB_SVG.spreadsheet, xls: NB_SVG.spreadsheet, xlsx: NB_SVG.spreadsheet,
362
+ pdf: NB_SVG.pdf,
363
+ png: NB_SVG.image, jpg: NB_SVG.image, jpeg: NB_SVG.image, gif: NB_SVG.image,
364
+ webp: NB_SVG.image, svg: NB_SVG.image, bmp: NB_SVG.image, ico: NB_SVG.image,
365
+ html: NB_SVG.html5, htm: NB_SVG.html5,
366
+ }
367
+
368
+ function findNode(nodes, path) {
369
+ if (!Array.isArray(nodes)) return null
370
+ for (var i = 0; i < nodes.length; i++) {
371
+ var n = nodes[i]
372
+ if (n.path === path) return n
373
+ if (Array.isArray(n.children) && n.children.length) {
374
+ var r = findNode(n.children, path)
375
+ if (r) return r
376
+ }
377
+ }
378
+ return null
379
+ }
380
+
381
+ function rowIndex(path) {
382
+ for (var i = 0; i < visibleRows.length; i++) if (visibleRows[i] === path) return i
383
+ return -1
384
+ }
385
+
386
+ function onRowClick(e, node) {
387
+ e.stopPropagation()
388
+ var path = node.path
389
+ var meta = e.metaKey || e.ctrlKey
390
+ var shift = e.shiftKey
391
+ var sel = new Set(state.selected)
392
+ var lastIndex = state.lastIndex
393
+ if (shift && lastIndex != null) {
394
+ var idx = rowIndex(path)
395
+ var from = Math.min(lastIndex, idx)
396
+ var to = Math.max(lastIndex, idx)
397
+ for (var i = from; i <= to; i++) { if (visibleRows[i]) sel.add(visibleRows[i]) }
398
+ } else if (meta) {
399
+ if (sel.has(path)) sel.delete(path); else sel.add(path)
400
+ lastIndex = rowIndex(path)
401
+ } else {
402
+ sel.clear(); sel.add(path); lastIndex = rowIndex(path)
403
+ }
404
+ setState({ selected: sel, lastIndex: lastIndex })
405
+ }
406
+
407
+ function onRowDoubleClick(node) {
408
+ if (node.type === 'directory') {
409
+ toggleNode(node)
410
+ } else {
411
+ openFileInTab(node.path)
412
+ }
413
+ }
414
+
415
+ function onRowContext(e, node) {
416
+ e.preventDefault()
417
+ e.stopPropagation()
418
+ var sel = new Set(state.selected)
419
+ if (!sel.has(node.path)) { sel.clear(); sel.add(node.path); setState({ selected: sel }) }
420
+ setState({ menu: { x: e.clientX, y: e.clientY, node: node } })
421
+ }
422
+
423
+ function onBlankContext(e) {
424
+ e.preventDefault()
425
+ e.stopPropagation()
426
+ setState({ selected: new Set(), lastIndex: null, menu: { x: e.clientX, y: e.clientY, node: null } })
427
+ }
428
+
429
+ function onBlankClick(e) {
430
+ if (e.target && e.target.closest && e.target.closest('[data-path]')) return
431
+ setState({ selected: new Set(), lastIndex: null })
432
+ }
433
+
434
+ async function openSelection() {
435
+ var paths = Array.from(state.selected)
436
+ setState({ menu: null })
437
+ for (var i = 0; i < paths.length; i++) await rpc('open', { path: paths[i] })
438
+ }
439
+
440
+ async function trashSelection() {
441
+ var paths = Array.from(state.selected)
442
+ setState({ menu: null })
443
+ var errors = []
444
+ for (var i = 0; i < paths.length; i++) {
445
+ var res = await rpc('trash', { path: paths[i] })
446
+ if (!res || !res.ok) errors.push(baseName(paths[i]) + ': ' + ((res && res.error) || '失败') + ((res && res.hint) ? '(' + res.hint + ')' : ''))
447
+ }
448
+ if (errors.length) setState({ error: errors.join(';') })
449
+ setState({ selected: new Set(), lastIndex: null })
450
+ await refresh()
451
+ }
452
+
453
+ function startRename(node) { setState({ renaming: node.path, menu: null }) }
454
+
455
+ function commitRename(path, name) {
456
+ if (!name || name === '.' || name === '..' || name.includes('/') || name.includes('\\')) {
457
+ setState({ renaming: null })
458
+ return
459
+ }
460
+ var dir = parentOf(path)
461
+ var dest = pathJoin(dir, name)
462
+ var sel = new Set(state.selected)
463
+ if (sel.has(path)) { sel.delete(path); sel.add(dest) }
464
+ setState({ renaming: null, selected: sel })
465
+ ;(async function () {
466
+ var res = await rpc('move', { from: path, to: dest })
467
+ if (!res || !res.ok) setState({ error: (res && res.error) || '重命名失败' })
468
+ await refresh()
469
+ })()
470
+ }
471
+
472
+ function startNew(parent, isDir) {
473
+ setState({ newItem: { parent: parent, isDir: isDir }, menu: null })
474
+ if (parent !== state.cwd) {
475
+ var parentNode = findNode(state.tree, parent)
476
+ if (parentNode && parentNode.children == null) toggleNode(parentNode)
477
+ }
478
+ }
479
+
480
+ function commitNew(parent, name, isDir) {
481
+ if (!name || name === '.' || name === '..' || name.includes('/') || name.includes('\\')) {
482
+ setState({ newItem: null })
483
+ return
484
+ }
485
+ setState({ newItem: null })
486
+ ;(async function () {
487
+ var res = await rpc('create', { dir: parent, name: name, isDir: isDir })
488
+ if (!res || !res.ok) setState({ error: (res && res.error) || '创建失败' })
489
+ await refresh()
490
+ })()
491
+ }
492
+
493
+ /** 成功通知:3 秒后自动消失(仅当未被更新的消息覆盖时才清除)。 */
494
+ var noticeTimer = null
495
+ function showNotice(text) {
496
+ setState({ notice: text, error: '' })
497
+ if (noticeTimer) clearTimeout(noticeTimer)
498
+ noticeTimer = setTimeout(function () {
499
+ if (state.notice === text) setState({ notice: '' })
500
+ }, 3000)
501
+ }
502
+
503
+ function copyPath(target) {
504
+ var p = target && target.path ? target.path : state.cwd
505
+ setState({ menu: null })
506
+ try {
507
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
508
+ navigator.clipboard.writeText(p).then(function () { showNotice('已复制路径') }).catch(function () {})
509
+ }
510
+ } catch (err) {}
511
+ }
512
+
513
+ function onDragStart(e, node) {
514
+ var paths = state.selected.has(node.path) ? Array.from(state.selected) : [node.path]
515
+ setState({ dragPaths: paths })
516
+ try { e.dataTransfer.setData('text/plain', paths.join('\n')) } catch (err) {}
517
+ e.dataTransfer.effectAllowed = 'move'
518
+ }
519
+
520
+ function doDrop(e, targetDir) {
521
+ if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
522
+ uploadFiles(targetDir, Array.from(e.dataTransfer.files))
523
+ return
524
+ }
525
+ movePaths(state.dragPaths || [], targetDir)
526
+ }
527
+
528
+ function onRowDrop(e, node) {
529
+ e.preventDefault()
530
+ e.stopPropagation()
531
+ if (node.type !== 'directory') return
532
+ doDrop(e, node.path)
533
+ setState({ dropTarget: null })
534
+ }
535
+
536
+ function onBodyDrop(e) {
537
+ e.preventDefault()
538
+ doDrop(e, state.cwd)
539
+ setState({ dropTarget: null })
540
+ }
541
+
542
+ async function movePaths(paths, targetDir) {
543
+ var errors = []
544
+ for (var i = 0; i < paths.length; i++) {
545
+ var p = paths[i]
546
+ if (p === '/' || !p) continue
547
+ var name = baseName(p)
548
+ var dest = pathJoin(targetDir, name)
549
+ if (dest === p) continue
550
+ if (targetDir === p || targetDir.startsWith(p + '/')) { errors.push('不能把文件夹移入自身: ' + name); continue }
551
+ var res = await rpc('move', { from: p, to: dest })
552
+ if (!res || !res.ok) errors.push(name + ': ' + ((res && res.error) || '失败'))
553
+ }
554
+ setState({ dragPaths: null })
555
+ if (errors.length) setState({ error: errors.join(';') })
556
+ await refresh()
557
+ }
558
+
559
+ function fileToBase64(file) {
560
+ return new Promise(function (resolve, reject) {
561
+ var r = new FileReader()
562
+ r.onload = function () { var s = String(r.result || ''); var i = s.indexOf(','); resolve(i >= 0 ? s.slice(i + 1) : s) }
563
+ r.onerror = function () { reject(new Error('读取文件失败')) }
564
+ r.readAsDataURL(file)
565
+ })
566
+ }
567
+
568
+ /** 单块大小:超过则走分块通道(JupyterLab 同款 1MB)。 */
569
+ var UPLOAD_CHUNK_SIZE = 1024 * 1024
570
+
571
+ function setUploadEntry(entry) {
572
+ var list = (state.uploads || []).slice()
573
+ var found = false
574
+ for (var i = 0; i < list.length; i++) {
575
+ if (list[i].name === entry.name) { list[i] = entry; found = true; break }
576
+ }
577
+ if (!found) list.push(entry)
578
+ setState({ uploads: list })
579
+ }
580
+
581
+ function updateUpload(name, loaded, total, idx, totalFiles, down) {
582
+ setUploadEntry({ name: name, loaded: loaded, total: total, idx: idx, totalFiles: totalFiles, done: false, down: !!down })
583
+ }
584
+
585
+ function finishUploads(count, okCount, failedName, down) {
586
+ var verb = down ? '已下载' : '已上传'
587
+ setUploadEntry({
588
+ name: '✓ ' + verb + ' ' + (okCount === count ? count + ' 个文件' : okCount + '/' + count + ' 个文件' + (failedName ? '(' + failedName + ' 失败)' : '')),
589
+ loaded: 0, total: 0, idx: 0, totalFiles: 0, done: true, down: !!down,
590
+ })
591
+ window.setTimeout(function () { setState({ uploads: [] }) }, 1600)
592
+ }
593
+
594
+ /** 单文件分块上传:>1MB 切片逐块 POST,首块覆盖、后续追加。 */
595
+ async function uploadOneFile(dir, f, idx, totalFiles) {
596
+ var chunked = f.size > UPLOAD_CHUNK_SIZE
597
+ var chunks = Math.max(1, Math.ceil(f.size / UPLOAD_CHUNK_SIZE))
598
+ updateUpload(f.name, 0, f.size, idx, totalFiles)
599
+ for (var c = 0; c < chunks; c++) {
600
+ var start = c * UPLOAD_CHUNK_SIZE
601
+ var blob = f.slice(start, Math.min(f.size, start + UPLOAD_CHUNK_SIZE))
602
+ var b64 = await fileToBase64(blob)
603
+ var res = await rpc('upload', {
604
+ dir: dir, name: f.name, data: b64,
605
+ chunk: chunked ? c + 1 : undefined,
606
+ })
607
+ if (!res || !res.ok) throw new Error((res && res.error) || '上传失败')
608
+ updateUpload(f.name, Math.min(f.size, start + UPLOAD_CHUNK_SIZE), f.size, idx, totalFiles)
609
+ }
610
+ }
611
+
612
+ async function uploadFiles(dir, files) {
613
+ if (!files || !files.length) return
614
+ var errors = []
615
+ var okCount = 0
616
+ for (var i = 0; i < files.length; i++) {
617
+ var f = files[i]
618
+ try {
619
+ await uploadOneFile(dir, f, i + 1, files.length)
620
+ okCount++
621
+ } catch (err) {
622
+ errors.push(f.name + ': ' + String((err && err.message) || err))
623
+ }
624
+ }
625
+ finishUploads(files.length, okCount, errors.length ? errors[0].split(':')[0] : '')
626
+ if (errors.length) setState({ error: errors.join(';'), notice: '' })
627
+ await refresh()
628
+ }
629
+
630
+ /** ≤100MB 走页面内 fetch + 进度行 + Blob 另存;>100MB 走浏览器原生下载。 */
631
+ var DOWNLOAD_PROGRESS_LIMIT = 100 * 1024 * 1024
632
+
633
+ async function downloadFile(path) {
634
+ setState({ menu: null })
635
+ var res = await rpc('download', { path: path, sessionId: activeSessionId })
636
+ if (!res.ok && /超出允许范围/.test(res.error || '')) {
637
+ await new Promise(function (r) { setTimeout(r, 600) })
638
+ res = await rpc('download', { path: path, sessionId: activeSessionId })
639
+ }
640
+ if (!res || !res.ok) {
641
+ setState({ error: (res && res.error) || '下载失败' })
642
+ return
643
+ }
644
+ var name = res.name || baseName(path)
645
+ var size = res.size || 0
646
+ // 大文件:浏览器原生下载(流式落盘、零内存),无进度
647
+ if (size > DOWNLOAD_PROGRESS_LIMIT) {
648
+ var a = document.createElement('a')
649
+ a.href = res.url
650
+ a.download = name
651
+ document.body.appendChild(a)
652
+ a.click()
653
+ a.remove()
654
+ showNotice('已开始下载 ' + name + '(大文件,由浏览器接管)')
655
+ return
656
+ }
657
+ // 中小文件:fetch 流式读字节计进度,完成后 Blob 另存(峰值≈文件体积)
658
+ updateUpload(name, 0, size, 0, 1, true)
659
+ try {
660
+ var r = await fetch(res.url)
661
+ if (!r.ok) throw new Error('HTTP ' + r.status)
662
+ var total = Number(r.headers.get('content-length')) || size
663
+ var reader = r.body.getReader()
664
+ var received = 0
665
+ var chunks = []
666
+ for (;;) {
667
+ var part = await reader.read()
668
+ if (part.done) break
669
+ chunks.push(part.value)
670
+ received += part.value.length
671
+ updateUpload(name, received, total, 0, 1, true)
672
+ }
673
+ var blobUrl = URL.createObjectURL(new Blob(chunks))
674
+ var da = document.createElement('a')
675
+ da.href = blobUrl
676
+ da.download = name
677
+ document.body.appendChild(da)
678
+ da.click()
679
+ da.remove()
680
+ window.setTimeout(function () { URL.revokeObjectURL(blobUrl) }, 5000)
681
+ finishUploads(1, 1, '', true)
682
+ } catch (err) {
683
+ setState({ error: '下载失败: ' + String((err && err.message) || err), uploads: [] })
684
+ }
685
+ }
686
+
687
+ function onUploadPicker(e) {
688
+ var files = Array.from((e.target && e.target.files) || [])
689
+ uploadFiles(state.cwd, files)
690
+ e.target.value = ''
691
+ }
692
+
693
+ // ------------------------------------------------------------------
694
+ // 文件标签页:右键/双击「预览」把文件以只读预览展示进会话区的「预览」
695
+ // tab(原生 conversation.view 槽位,与 对话/轨迹 同级)。
696
+ // markdown/html/json/csv 有专属渲染器;图片预览;其余按纯文本。
697
+ // 状态存模块级 store,切走 tab 再切回来内容不丢。
698
+ // ------------------------------------------------------------------
699
+ var FILES_TAB_LABEL = '预览'
700
+
701
+ /** 当前活跃会话:list/read/download 携带给宿主,供围栏直查该会话 cwd。 */
702
+ var activeSessionId = null
703
+
704
+ /** 同时打开的预览 tab 上限:超过后挤掉最早打开的(FIFO)。 */
705
+ var FILES_MAX_OPEN = 5
706
+
707
+ /** path 是否落在 base 目录子树内(base 为根或空时视为全量放行)。 */
708
+ function withinScope(path, base) {
709
+ if (!base || base === '/') return true
710
+ return path === base || path.indexOf(base + '/') === 0
711
+ }
712
+
713
+ /**
714
+ * 会话切换时按新 cwd 裁剪预览列表:范围外的 tab 自动关闭。
715
+ * 若当前激活文件被裁掉,激活项顺移到剩余最后一个。
716
+ * 列表无变化时返回原引用,避免触发多余渲染。
717
+ */
718
+ function pruneFilesToScope(files, cwd) {
719
+ var kept = files.list.filter(function (f) { return withinScope(f.path, cwd) })
720
+ try { window.__DFV_PRUNE = (window.__DFV_PRUNE || []) ; window.__DFV_PRUNE.push({ cwd: cwd, before: files.list.length, after: kept.length }) } catch (_) {}
721
+ if (kept.length === files.list.length) return files
722
+ var activeKept = kept.some(function (f) { return f.path === files.active })
723
+ var active = files.active
724
+ if (!activeKept) active = kept.length ? kept[kept.length - 1].path : null
725
+ return { list: kept, active: active }
726
+ }
727
+
728
+ function setFiles(patch) {
729
+ setState({ files: Object.assign({}, state.files, patch) })
730
+ try {
731
+ var f = state.files
732
+ window.__DFV_DBG = { n: f.list.length, active: f.active, ops: ((window.__DFV_DBG && window.__DFV_DBG.ops) || 0) + 1 }
733
+ } catch (_) {}
734
+ }
735
+
736
+ function findFileEntry(path) {
737
+ for (var i = 0; i < state.files.list.length; i++) {
738
+ if (state.files.list[i].path === path) return state.files.list[i]
739
+ }
740
+ return null
741
+ }
742
+
743
+ /** 点击原生 header 的「预览」tab,把 view ring 切到预览视图。 */
744
+ function activateFilesView() {
745
+ try {
746
+ var tabs = document.querySelectorAll('[role="tab"]')
747
+ for (var i = 0; i < tabs.length; i++) {
748
+ if (tabs[i].textContent === FILES_TAB_LABEL) { tabs[i].click(); return true }
749
+ }
750
+ } catch (err) {}
751
+ return false
752
+ }
753
+
754
+ async function openFileInTab(path) {
755
+ setState({ menu: null })
756
+ var entry = findFileEntry(path)
757
+ if (!entry) {
758
+ entry = {
759
+ path: path, name: baseName(path),
760
+ loading: true, loaded: false, error: '',
761
+ kind: null, content: '', dataUrl: '', data: '',
762
+ truncated: false, size: 0,
763
+ }
764
+ // FIFO:最多同时 5 个预览,新开的挤掉最早打开的
765
+ var list = state.files.list.slice()
766
+ while (list.length >= FILES_MAX_OPEN) list.shift()
767
+ list.push(entry)
768
+ setFiles({ list: list, active: path })
769
+ } else if (entry.loaded) {
770
+ setFiles({ active: path })
771
+ activateFilesView()
772
+ return
773
+ } else {
774
+ setFiles({ active: path })
775
+ }
776
+ activateFilesView()
777
+ var res = await rpc('read', { path: path, sessionId: activeSessionId })
778
+ if (!res.ok && /超出允许范围/.test(res.error || '')) {
779
+ await new Promise(function (r) { setTimeout(r, 600) })
780
+ res = await rpc('read', { path: path, sessionId: activeSessionId })
781
+ }
782
+ var cur = findFileEntry(path)
783
+ if (!cur) return
784
+ cur.loading = false
785
+ if (!res || !res.ok) {
786
+ cur.error = (res && res.error) || '读取失败'
787
+ } else {
788
+ cur.error = ''
789
+ cur.loaded = true
790
+ cur.kind = res.kind
791
+ cur.size = res.size || 0
792
+ if (res.kind === 'text') { cur.content = res.content; cur.truncated = !!res.truncated }
793
+ else if (res.kind === 'image') { cur.dataUrl = 'data:' + res.mime + ';base64,' + res.data }
794
+ else if (res.kind === 'pdf') { cur.data = res.data }
795
+ }
796
+ setFiles({ list: state.files.list.slice(), active: path })
797
+ }
798
+
799
+ async function reloadFile(path) {
800
+ var entry = findFileEntry(path)
801
+ if (!entry) return
802
+ // 复位 loaded,让 openFileInTab 走重读路径而不是「已打开直接激活」。
803
+ entry.loaded = false
804
+ await openFileInTab(path)
805
+ }
806
+
807
+ function closeFileTab(path) {
808
+ var list = state.files.list
809
+ var idx = -1
810
+ for (var i = 0; i < list.length; i++) if (list[i].path === path) { idx = i; break }
811
+ if (idx < 0) return
812
+ var next = list.slice(0, idx).concat(list.slice(idx + 1))
813
+ var active = state.files.active
814
+ if (active === path) {
815
+ var neighbor = next[Math.min(idx, next.length - 1)]
816
+ active = neighbor ? neighbor.path : null
817
+ }
818
+ setFiles({ list: next, active: active })
819
+ }
820
+
821
+ function setActiveFile(path) {
822
+ setFiles({ active: path })
823
+ }
824
+
825
+ function NameInput(props) {
826
+ var initial = props.initial
827
+ var selectBase = props.selectBase !== false
828
+ var onCommit = props.onCommit
829
+ var onCancel = props.onCancel
830
+ var valState = React.useState(initial)
831
+ var val = valState[0]
832
+ var setVal = valState[1]
833
+ var settled = React.useRef(false)
834
+ var inputRef = React.useRef(null)
835
+ React.useEffect(function () {
836
+ var el = inputRef.current
837
+ if (!el) return
838
+ el.focus()
839
+ if (selectBase) {
840
+ var dot = initial.lastIndexOf('.')
841
+ if (dot <= 0) el.select(); else el.setSelectionRange(0, dot)
842
+ } else {
843
+ el.select()
844
+ }
845
+ }, [])
846
+ function settle(fn) { if (settled.current) return; settled.current = true; fn() }
847
+ return create('input', {
848
+ ref: inputRef,
849
+ type: 'text',
850
+ value: val,
851
+ spellCheck: false,
852
+ autoComplete: 'off',
853
+ className: 'expl-inline-input',
854
+ onChange: function (e) { setVal(e.target.value) },
855
+ onClick: function (e) { e.stopPropagation() },
856
+ onBlur: function () { settle(function () { onCommit(val) }) },
857
+ onKeyDown: function (e) {
858
+ if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); settle(function () { onCommit(val) }) }
859
+ else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); settle(function () { onCancel() }) }
860
+ },
861
+ })
862
+ }
863
+
864
+ function Row(props) {
865
+ var node = props.node
866
+ var depth = props.depth
867
+ var isDir = node.type === 'directory'
868
+ var isSelected = state.selected.has(node.path)
869
+ var isRenaming = state.renaming === node.path
870
+ var isDropTarget = state.dropTarget === node.path
871
+ visibleRows.push(node.path)
872
+
873
+ var rowMain = create('div', {
874
+ className: 'expl-row-main' + (isSelected ? ' selected' : '') + (isDropTarget ? ' drop-target' : ''),
875
+ 'data-path': node.path,
876
+ style: { paddingLeft: 8 + depth * 16 },
877
+ draggable: true,
878
+ onClick: function (e) { onRowClick(e, node) },
879
+ onDoubleClick: function () { onRowDoubleClick(node) },
880
+ onContextMenu: function (e) { onRowContext(e, node) },
881
+ onDragStart: function (e) { onDragStart(e, node) },
882
+ onDragOver: function (e) { if (isDir) { e.preventDefault(); e.stopPropagation(); if (state.dropTarget !== node.path) setState({ dropTarget: node.path }) } },
883
+ onDragLeave: function (e) { if (state.dropTarget === node.path) setState({ dropTarget: null }) },
884
+ onDrop: function (e) { if (isDir) onRowDrop(e, node) },
885
+ },
886
+ create('span', {
887
+ className: 'expl-caret' + (isDir ? ' expl-caret-big' : ' expl-caret-sm'),
888
+ onClick: function (e) { if (isDir) { e.stopPropagation(); toggleNode(node) } },
889
+ }, isDir ? (node.open ? '▾' : '▸') : '·'),
890
+ create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: iconSvgFor(node) } }),
891
+ isRenaming
892
+ ? create(NameInput, {
893
+ initial: node.name,
894
+ selectBase: true,
895
+ onCommit: function (name) { commitRename(node.path, name) },
896
+ onCancel: function () { setState({ renaming: null }) },
897
+ })
898
+ : create('span', { className: 'expl-name' }, node.name),
899
+ !isDir ? create('span', { className: 'expl-size' }, formatSize(node.size)) : null,
900
+ )
901
+
902
+ var children = []
903
+ if (state.newItem && state.newItem.parent === node.path && isDir && node.open) {
904
+ children.push(create('div', { key: '__new__', className: 'expl-row' },
905
+ create('div', { className: 'expl-row-main', style: { paddingLeft: 8 + (depth + 1) * 16 } },
906
+ create('span', { className: 'expl-icon' }, state.newItem.isDir ? '📁' : '📄'),
907
+ create(NameInput, {
908
+ initial: '',
909
+ selectBase: false,
910
+ onCommit: function (name) { commitNew(state.newItem.parent, name, state.newItem.isDir) },
911
+ onCancel: function () { setState({ newItem: null }) },
912
+ }),
913
+ ),
914
+ ))
915
+ }
916
+ if (isDir && node.open) {
917
+ if (node.loading) {
918
+ children.push(create('div', { key: '__loading__', className: 'expl-muted', style: { paddingLeft: 8 + (depth + 1) * 16 } }, '加载中…'))
919
+ } else {
920
+ ;(node.children || []).forEach(function (c) { children.push(create(Row, { key: c.path, node: c, depth: depth + 1 })) })
921
+ }
922
+ }
923
+
924
+ return create('div', { key: node.path, className: 'expl-row' }, rowMain, children)
925
+ }
926
+
927
+ function buildMenuItems(node) {
928
+ var items = []
929
+ var multi = state.selected.size > 1
930
+ if (node) {
931
+ if (!multi) {
932
+ if (node.type !== 'directory') {
933
+ items.push({ key: 'open-tab', label: '👁 预览', onClick: function () { openFileInTab(node.path) } })
934
+ items.push({ key: 'open', label: '🖥 用系统打开', onClick: openSelection, separatorAfter: true })
935
+ } else {
936
+ items.push({ key: 'open', label: '🖥 用系统打开', onClick: openSelection, separatorAfter: true })
937
+ }
938
+ items.push({ key: 'rename', label: '✎ 重命名', onClick: function () { startRename(node) } })
939
+ items.push({ key: 'trash', label: '🗑 移到废纸篓', danger: true, onClick: trashSelection, separatorAfter: true })
940
+ } else {
941
+ items.push({ key: 'trash', label: '🗑 移到废纸篓 (' + state.selected.size + ' 项)', danger: true, onClick: trashSelection, separatorAfter: true })
942
+ }
943
+ if (node.type === 'directory') {
944
+ items.push({ key: 'newfile', label: '📄 新建文件', onClick: function () { startNew(node.path, false) } })
945
+ items.push({ key: 'newfolder', label: '📁 新建文件夹', onClick: function () { startNew(node.path, true) }, separatorAfter: true })
946
+ }
947
+ items.push({ key: 'copy', label: '⧉ 复制路径', onClick: function () { copyPath(node) } })
948
+ if (node.type !== 'directory') {
949
+ items.push({ key: 'download', label: '⬇ 下载', onClick: function () { downloadFile(node.path) } })
950
+ }
951
+ } else {
952
+ items.push({ key: 'newfile', label: '📄 新建文件', onClick: function () { startNew(state.cwd, false) } })
953
+ items.push({ key: 'newfolder', label: '📁 新建文件夹', onClick: function () { startNew(state.cwd, true) } })
954
+ items.push({ key: 'refresh', label: '⟳ 刷新', onClick: refresh, separatorAfter: true })
955
+ if (state.selected.size > 0) items.push({ key: 'none', label: '取消选择', onClick: function () { setState({ selected: new Set(), lastIndex: null }) } })
956
+ }
957
+ return items
958
+ }
959
+
960
+ function Menu(props) {
961
+ var menu = props.menu
962
+ var menuRef = React.useRef(null)
963
+ var posState = React.useState({ x: menu.x, y: menu.y })
964
+ var pos = posState[0]
965
+ var setPos = posState[1]
966
+ React.useEffect(function () {
967
+ var el = menuRef.current
968
+ if (!el) return
969
+ var rect = el.getBoundingClientRect()
970
+ var margin = 6
971
+ var nx = menu.x
972
+ var ny = menu.y
973
+ if (menu.x + rect.width > window.innerWidth - margin) nx = Math.max(margin, window.innerWidth - rect.width - margin)
974
+ if (menu.y + rect.height > window.innerHeight - margin) ny = Math.max(margin, window.innerHeight - rect.height - margin)
975
+ setPos({ x: nx, y: ny })
976
+ }, [menu.x, menu.y])
977
+ React.useEffect(function () {
978
+ if (typeof document === 'undefined') return
979
+ function down(e) { if (menuRef.current && !menuRef.current.contains(e.target)) setState({ menu: null }) }
980
+ function key(e) { if (e.key === 'Escape') setState({ menu: null }) }
981
+ document.addEventListener('mousedown', down)
982
+ document.addEventListener('keydown', key)
983
+ return function () {
984
+ document.removeEventListener('mousedown', down)
985
+ document.removeEventListener('keydown', key)
986
+ }
987
+ }, [])
988
+
989
+ var items = buildMenuItems(menu.node)
990
+ var menuChildren = []
991
+ items.forEach(function (it) {
992
+ menuChildren.push(create('button', {
993
+ key: it.key,
994
+ className: 'expl-menu-item' + (it.danger ? ' expl-danger' : ''),
995
+ onClick: function () { setState({ menu: null }); it.onClick() },
996
+ }, it.label))
997
+ if (it.separatorAfter) menuChildren.push(create('div', { key: it.key + '-sep', className: 'expl-menu-sep' }))
998
+ })
999
+ return create('div', { ref: menuRef, className: 'expl-menu', style: { left: pos.x, top: pos.y } }, menuChildren)
1000
+ }
1001
+
1002
+ // ------------------------------------------------------------------
1003
+ // 文件视图(conversation.view 'dsh-soup-files'):
1004
+ // 子 tab 条 + 只读预览区(markdown/html/json/csv 渲染 / 图片预览 / 二进制提示)。
1005
+ // ------------------------------------------------------------------
1006
+ // ------------------------------------------------------------------
1007
+ // 预览渲染器:按扩展名分派(信任模型参考 JupyterLab viewer——
1008
+ // markdown 走不可信安全渲染管线,html 进无 allow-* 的沙箱 iframe)。
1009
+ // ------------------------------------------------------------------
1010
+ /** 取路径小写扩展名。 */
1011
+ function extOfName(p) {
1012
+ var m = /\.([A-Za-z0-9]+)$/.exec(String(p || ''))
1013
+ return m ? m[1].toLowerCase() : ''
1014
+ }
1015
+
1016
+ /** 预览渲染器类型:markdown | html | json | csv | tsv | notebook | code | text。 */
1017
+ function previewKind(path) {
1018
+ var ext = extOfName(path)
1019
+ if (ext === 'md' || ext === 'markdown') return 'markdown'
1020
+ if (ext === 'html' || ext === 'htm') return 'html'
1021
+ if (ext === 'json') return 'json'
1022
+ if (ext === 'ipynb') return 'notebook'
1023
+ if (ext === 'csv') return 'csv'
1024
+ if (ext === 'tsv') return 'tsv'
1025
+ return 'text'
1026
+ }
1027
+
1028
+ // 扩展名 → Shiki 语言提示。对齐 DSH 读卡片的 LANG_BY_EXTENSION
1029
+ // (packages/fs/tool-fs read-render.ts),json/md/markdown/html/htm 除外——
1030
+ // 它们走上方专属渲染器。
1031
+ var CODE_LANG_BY_EXT = {
1032
+ ts: 'ts', tsx: 'tsx', mts: 'ts', cts: 'ts',
1033
+ js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js',
1034
+ jsonc: 'json',
1035
+ py: 'py', rb: 'rb', go: 'go', rs: 'rs', java: 'java',
1036
+ c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', hpp: 'cpp', cxx: 'cpp',
1037
+ cs: 'cs', kt: 'kotlin', swift: 'swift', php: 'php',
1038
+ sh: 'sh', bash: 'sh', zsh: 'sh',
1039
+ yaml: 'yaml', yml: 'yaml', toml: 'toml', ini: 'ini',
1040
+ mdx: 'mdx', css: 'css', scss: 'scss', less: 'less',
1041
+ sql: 'sql', xml: 'xml', lua: 'lua',
1042
+ }
1043
+
1044
+ /** 超过该字符数的代码文件不做高亮(Shiki 首次高亮的耗时保护)。 */
1045
+ var CODE_HIGHLIGHT_MAX_CHARS = 400000
1046
+
1047
+ /** RFC 4180 风格的分隔符解析(引号感知,"" 转义),返回行数组。 */
1048
+ function parseDelimited(text, delim) {
1049
+ var rows = []
1050
+ var row = []
1051
+ var field = ''
1052
+ var inQuotes = false
1053
+ var n = text.length
1054
+ for (var i = 0; i < n; i++) {
1055
+ var ch = text[i]
1056
+ if (inQuotes) {
1057
+ if (ch === '"') {
1058
+ if (text[i + 1] === '"') { field += '"'; i++ } else { inQuotes = false }
1059
+ } else {
1060
+ field += ch
1061
+ }
1062
+ } else if (ch === '"') {
1063
+ inQuotes = true
1064
+ } else if (ch === delim) {
1065
+ row.push(field); field = ''
1066
+ } else if (ch === '\n') {
1067
+ row.push(field); rows.push(row); row = []; field = ''
1068
+ } else if (ch !== '\r') {
1069
+ field += ch
1070
+ }
1071
+ }
1072
+ if (field !== '' || row.length > 0) { row.push(field); rows.push(row) }
1073
+ return rows
1074
+ }
1075
+
1076
+ /** 把 pretty JSON 切成着色 token 数组(纯文本/React 节点,无 innerHTML)。 */
1077
+ function jsonColorNodes(pretty) {
1078
+ var out = []
1079
+ var last = 0
1080
+ var re = /("(?:\\.|[^"\\])*")(\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g
1081
+ var m
1082
+ while ((m = re.exec(pretty)) !== null) {
1083
+ if (m.index > last) out.push(pretty.slice(last, m.index))
1084
+ if (m[1] !== undefined) {
1085
+ out.push(create('span', { key: out.length, className: m[2] ? 'dfv-tok-key' : 'dfv-tok-str' }, m[1]))
1086
+ if (m[2]) out.push(m[2])
1087
+ } else if (m[0] === 'true' || m[0] === 'false') {
1088
+ out.push(create('span', { key: out.length, className: 'dfv-tok-bool' }, m[0]))
1089
+ } else if (m[0] === 'null') {
1090
+ out.push(create('span', { key: out.length, className: 'dfv-tok-null' }, m[0]))
1091
+ } else {
1092
+ out.push(create('span', { key: out.length, className: 'dfv-tok-num' }, m[0]))
1093
+ }
1094
+ last = re.lastIndex
1095
+ }
1096
+ if (last < pretty.length) out.push(pretty.slice(last))
1097
+ return out
1098
+ }
1099
+
1100
+ var CSV_MAX_ROWS = 500
1101
+ var CSV_MAX_COLS = 64
1102
+
1103
+ function MarkdownPreview(props) {
1104
+ if (MarkdownText) {
1105
+ return create('div', { className: 'dfv-md-wrap' },
1106
+ create('div', { className: 'dfv-md' }, create(MarkdownText, { text: props.text })))
1107
+ }
1108
+ // 回退:宿主缺 ui-primitives 时按纯文本展示
1109
+ return create('pre', { className: 'dfv-code' }, props.text)
1110
+ }
1111
+
1112
+ function HtmlPreview(props) {
1113
+ // 可交互预览:给 allow-scripts/popups/forms/modals(对齐 JupyterLab
1114
+ // trusted 档),但坚持不给 allow-same-origin——脚本跑在 opaque origin,
1115
+ // 触不到 GUI 的 DOM/存储/Cookie;srcdoc 注入 + no-referrer 收敛外链。
1116
+ return create('iframe', {
1117
+ className: 'dfv-frame',
1118
+ sandbox: 'allow-scripts allow-popups allow-forms allow-modals',
1119
+ referrerPolicy: 'no-referrer',
1120
+ title: props.title || 'HTML 预览',
1121
+ srcDoc: props.text,
1122
+ })
1123
+ }
1124
+
1125
+ function PdfPreview(props) {
1126
+ // 对齐 JupyterLab pdf-extension:base64 → Blob → objectURL → 原生查看器
1127
+ // 嵌入(object 标签)。不加 sandbox——渲染方是浏览器自带的受信 PDF 查看器。
1128
+ var st = React.useState({ url: null, error: '' })
1129
+ var state = st[0]
1130
+ var setSt = st[1]
1131
+ React.useEffect(function () {
1132
+ var cancelled = false
1133
+ var made = null
1134
+ fetch('data:application/pdf;base64,' + props.data)
1135
+ .then(function (r) { return r.blob() })
1136
+ .then(function (blob) {
1137
+ if (cancelled) { URL.revokeObjectURL(made); return }
1138
+ made = URL.createObjectURL(blob)
1139
+ setSt({ url: made, error: '' })
1140
+ })
1141
+ .catch(function (e) {
1142
+ if (!cancelled) setSt({ url: null, error: String((e && e.message) || e) })
1143
+ })
1144
+ return function () {
1145
+ cancelled = true
1146
+ if (made) URL.revokeObjectURL(made)
1147
+ }
1148
+ }, [props.data])
1149
+ if (state.error) return create('div', { className: 'dfv-error' }, 'PDF 预览失败: ' + state.error)
1150
+ if (!state.url) return create('div', { className: 'dfv-empty' }, 'PDF 加载中…')
1151
+ return create('object', {
1152
+ className: 'dfv-frame dfv-pdf',
1153
+ type: 'application/pdf',
1154
+ data: state.url,
1155
+ 'aria-label': 'PDF 预览',
1156
+ }, '当前浏览器无法内嵌预览 PDF,请右键用系统打开。')
1157
+ }
1158
+
1159
+ // ------------------------------------------------------------------
1160
+ // Notebook(.ipynb)静态预览,GitHub 风格:markdown 单元走 MarkdownText,
1161
+ // 代码单元走 CodeBlock;输出按 MIME 择优渲染(image > html > md > text)。
1162
+ // 输出 HTML 与文件 HTML 同规——无 allow-* 沙箱 iframe 禁脚本防注入。
1163
+ // ------------------------------------------------------------------
1164
+ /** notebook 的 source 字段是 string 或行数组,统一成字符串。 */
1165
+ function nbSource(src) {
1166
+ return Array.isArray(src) ? src.join('') : String(src == null ? '' : src)
1167
+ }
1168
+
1169
+ /** 剥离 traceback 里的 ANSI 转义序列。 */
1170
+ function stripAnsi(s) {
1171
+ return String(s == null ? '' : s).replace(/\u001b\[[0-9;]*[A-Za-z]/g, '')
1172
+ }
1173
+
1174
+ var NB_MIME_PREF = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml', 'text/html', 'text/markdown', 'text/latex', 'text/plain']
1175
+
1176
+ function nbPickMime(data) {
1177
+ if (!data) return null
1178
+ for (var i = 0; i < NB_MIME_PREF.length; i++) {
1179
+ if (Object.prototype.hasOwnProperty.call(data, NB_MIME_PREF[i])) return NB_MIME_PREF[i]
1180
+ }
1181
+ return null
1182
+ }
1183
+
1184
+ /** 内核语言名 → Shiki 提示;未知语言由 CodeBlock 自动回退纯文本。 */
1185
+ var NB_LANG_HINT = { python: 'py', r: 'r', julia: 'julia', javascript: 'js', typescript: 'ts', bash: 'sh', ruby: 'rb' }
1186
+
1187
+ function NbOutput(props) {
1188
+ var out = props.out
1189
+ var t = out && out.output_type
1190
+ if (t === 'stream') {
1191
+ return create('pre', { className: 'dfv-nb-out' + (out.name === 'stderr' ? ' dfv-nb-out-err' : '') }, nbSource(out.text))
1192
+ }
1193
+ if (t === 'error') {
1194
+ var tb = (Array.isArray(out.traceback) ? out.traceback : []).map(stripAnsi).join('\n')
1195
+ return create('div', { className: 'dfv-nb-error' },
1196
+ create('div', { className: 'dfv-nb-error-head' }, stripAnsi(out.ename) + ': ' + stripAnsi(out.evalue)),
1197
+ tb ? create('pre', { className: 'dfv-nb-trace' }, tb) : null)
1198
+ }
1199
+ if (t === 'execute_result' || t === 'display_data') {
1200
+ var mime = nbPickMime(out.data)
1201
+ if (!mime) return null
1202
+ if (mime === 'image/png' || mime === 'image/jpeg' || mime === 'image/gif') {
1203
+ return create('img', { className: 'dfv-nb-img', src: 'data:' + mime + ';base64,' + out.data[mime], alt: 'notebook output image' })
1204
+ }
1205
+ if (mime === 'image/svg+xml') {
1206
+ return create('img', { className: 'dfv-nb-img', src: 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(nbSource(out.data[mime])))), alt: 'notebook output svg' })
1207
+ }
1208
+ if (mime === 'text/html') {
1209
+ return create('iframe', { className: 'dfv-nb-html', sandbox: '', title: 'HTML 输出', srcDoc: nbSource(out.data[mime]) })
1210
+ }
1211
+ if (mime === 'text/markdown' && MarkdownText) {
1212
+ return create('div', { className: 'dfv-md-wrap dfv-nb-md' }, create(MarkdownText, { text: nbSource(out.data[mime]) }))
1213
+ }
1214
+ var plain = Object.prototype.hasOwnProperty.call(out.data || {}, 'text/plain') ? out.data['text/plain'] : out.data[mime]
1215
+ return create('pre', { className: 'dfv-nb-out' }, nbSource(plain))
1216
+ }
1217
+ return null
1218
+ }
1219
+
1220
+ function NotebookPreview(props) {
1221
+ var nb = null
1222
+ try { nb = JSON.parse(props.text) } catch (err) { nb = undefined }
1223
+ if (nb === undefined || typeof nb !== 'object' || nb === null) {
1224
+ return create(React.Fragment, null,
1225
+ create('div', { className: 'dfv-cap' }, 'ipynb 解析失败,按纯文本显示'),
1226
+ create('pre', { className: 'dfv-code' }, props.text))
1227
+ }
1228
+ var cells = Array.isArray(nb.cells) ? nb.cells : []
1229
+ if (!cells.length && Array.isArray(nb.worksheets)) {
1230
+ // nbformat v3:worksheets[].cells 扁平合并
1231
+ for (var w = 0; w < nb.worksheets.length; w++) {
1232
+ cells = cells.concat(Array.isArray(nb.worksheets[w].cells) ? nb.worksheets[w].cells : [])
1233
+ }
1234
+ }
1235
+ var ks = nb.metadata && nb.metadata.kernelspec
1236
+ var kernelName = (ks && (ks.display_name || ks.name)) || ''
1237
+ var langName = nb.metadata && nb.metadata.language_info && nb.metadata.language_info.name
1238
+ var codeLang = (langName && Object.prototype.hasOwnProperty.call(NB_LANG_HINT, String(langName).toLowerCase()))
1239
+ ? NB_LANG_HINT[String(langName).toLowerCase()]
1240
+ : undefined
1241
+
1242
+ var cellEls = cells.map(function (cell, i) {
1243
+ var ct = cell.cell_type
1244
+ var body = null
1245
+ if (ct === 'code') {
1246
+ var outputs = (Array.isArray(cell.outputs) ? cell.outputs : []).map(function (o, j) {
1247
+ return create(NbOutput, { key: j, out: o })
1248
+ })
1249
+ var codeSrc = nbSource(cell.source)
1250
+ body = create('div', { className: 'dfv-nb-cellbody' },
1251
+ CodeBlock
1252
+ ? create(CodeBlock, { code: codeSrc, lang: codeLang })
1253
+ : create('pre', { className: 'dfv-code' }, codeSrc),
1254
+ outputs.length ? create('div', { className: 'dfv-nb-outputs' }, outputs) : null)
1255
+ } else if (ct === 'markdown') {
1256
+ body = MarkdownText
1257
+ ? create('div', { className: 'dfv-md-wrap dfv-nb-cellbody' }, create(MarkdownText, { text: nbSource(cell.source) }))
1258
+ : create('pre', { className: 'dfv-code dfv-nb-cellbody' }, nbSource(cell.source))
1259
+ } else {
1260
+ body = create('pre', { className: 'dfv-code dfv-nb-cellbody dfv-nb-raw' }, nbSource(cell.source))
1261
+ }
1262
+ var badge = ct === 'code'
1263
+ ? '[' + (cell.execution_count != null ? cell.execution_count : ' ') + ']'
1264
+ : ct === 'markdown' ? 'Md' : 'raw'
1265
+ return create('div', { key: i, className: 'dfv-nb-cell dfv-nb-' + ct },
1266
+ create('div', { className: 'dfv-nb-gutter' }, badge),
1267
+ body)
1268
+ })
1269
+
1270
+ return create('div', { className: 'dfv-nb' },
1271
+ create('div', { className: 'dfv-cap' },
1272
+ 'Jupyter Notebook' + (kernelName ? ' · ' + kernelName : '') + ' · ' + cells.length + ' 个单元格'),
1273
+ cellEls.length ? cellEls : create('div', { className: 'dfv-muted' }, '(空笔记本)'))
1274
+ }
1275
+
1276
+ function JsonPreview(props) {
1277
+ var failed = false
1278
+ try { JSON.parse(props.text) } catch (err) { failed = true }
1279
+ if (failed) {
1280
+ return create(React.Fragment, null,
1281
+ create('div', { className: 'dfv-cap' }, 'JSON 解析失败,按纯文本显示'),
1282
+ create('pre', { className: 'dfv-code' }, props.text))
1283
+ }
1284
+ return create('pre', { className: 'dfv-json dfv-code' }, jsonColorNodes(JSON.stringify(JSON.parse(props.text), null, 2)))
1285
+ }
1286
+
1287
+ function DelimitedPreview(props) {
1288
+ var delim = props.delim || ','
1289
+ var rows = parseDelimited(props.text, delim)
1290
+ var totalRows = rows.length
1291
+ // 首行作表头(数据导出的常见约定)
1292
+ var headerCells = totalRows > 0 ? rows[0].slice(0, CSV_MAX_COLS) : []
1293
+ var cols = headerCells.length
1294
+ for (var r = 1; r <= Math.min(totalRows - 1, CSV_MAX_ROWS); r++) {
1295
+ cols = Math.max(cols, Math.min(rows[r].length, CSV_MAX_COLS))
1296
+ }
1297
+ var caps = []
1298
+ if (totalRows > CSV_MAX_ROWS + 1) caps.push('共 ' + totalRows + ' 行,仅预览前 ' + CSV_MAX_ROWS + ' 行')
1299
+ var trs = []
1300
+ for (var ri = 1; ri <= Math.min(totalRows - 1, CSV_MAX_ROWS); ri++) {
1301
+ var cells = rows[ri]
1302
+ var tds = []
1303
+ for (var c = 0; c < cols; c++) {
1304
+ tds.push(create('td', { key: c, className: 'dfv-td' }, cells[c] != null ? String(cells[c]) : ''))
1305
+ }
1306
+ trs.push(create('tr', { key: ri }, tds))
1307
+ }
1308
+ return create(React.Fragment, null,
1309
+ caps.length ? create('div', { className: 'dfv-cap' }, caps.join(';')) : null,
1310
+ create('div', { className: 'dfv-table-wrap' },
1311
+ create('table', { className: 'dfv-table' },
1312
+ create('thead', null,
1313
+ create('tr', null, headerCells.map(function (cellText, ci) {
1314
+ return create('th', { key: ci, className: 'dfv-th' }, cellText != null ? String(cellText) : '')
1315
+ })),
1316
+ ),
1317
+ create('tbody', null, trs),
1318
+ ),
1319
+ ),
1320
+ )
1321
+ }
1322
+
1323
+ function formatBytes(n) {
1324
+ if (typeof n !== 'number' || n < 0) return ''
1325
+ if (n < 1024) return n + ' B'
1326
+ if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'
1327
+ if (n < 1073741824) return (n / 1048576).toFixed(1) + ' MB'
1328
+ return (n / 1073741824).toFixed(1) + ' GB'
1329
+ }
1330
+
1331
+ function FileContent(props) {
1332
+ var entry = props.entry
1333
+ if (entry.loading) {
1334
+ return create('div', { className: 'dfv-empty' }, '加载中…')
1335
+ }
1336
+ if (entry.error) {
1337
+ return create('div', { className: 'dfv-error' }, entry.error)
1338
+ }
1339
+ if (entry.kind === 'binary') {
1340
+ return create('div', { className: 'dfv-muted' },
1341
+ '二进制文件(' + formatBytes(entry.size) + '),请在资源管理器中用系统打开')
1342
+ }
1343
+ if (entry.kind === 'image-too-large') {
1344
+ return create('div', { className: 'dfv-muted' },
1345
+ '图片过大(' + formatBytes(entry.size) + ',上限 ' + formatBytes(entry.limit) + ')')
1346
+ }
1347
+ if (entry.kind === 'image') {
1348
+ return create('div', { className: 'dfv-image-wrap' },
1349
+ create('img', { className: 'dfv-image', src: entry.dataUrl, alt: entry.name }))
1350
+ }
1351
+ if (entry.kind === 'pdf-too-large') {
1352
+ return create('div', { className: 'dfv-muted' },
1353
+ 'PDF 过大(' + formatBytes(entry.size) + ',上限 ' + formatBytes(entry.limit) + '),请用系统打开')
1354
+ }
1355
+ if (entry.kind === 'pdf') {
1356
+ return create(PdfPreview, { data: entry.data })
1357
+ }
1358
+ // 文本类:按扩展名选预览渲染器(纯只读,无编辑)
1359
+ var pk = previewKind(entry.path)
1360
+ var view = null
1361
+ if (pk === 'markdown') view = create(MarkdownPreview, { text: entry.content })
1362
+ else if (pk === 'html') view = create(HtmlPreview, { text: entry.content, title: entry.name })
1363
+ else if (pk === 'json') view = create(JsonPreview, { text: entry.content })
1364
+ else if (pk === 'notebook') view = create(NotebookPreview, { text: entry.content })
1365
+ else if (pk === 'csv' || pk === 'tsv') view = create(DelimitedPreview, { text: entry.content, delim: pk === 'tsv' ? '\t' : ',' })
1366
+ else {
1367
+ // 代码文件:DSH CodeBlock(Shiki 高亮,语法包懒加载);无高亮条件回退纯文本
1368
+ var lang = Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, extOfName(entry.path))
1369
+ ? CODE_LANG_BY_EXT[extOfName(entry.path)]
1370
+ : undefined
1371
+ if (CodeBlock && lang && entry.content.length <= CODE_HIGHLIGHT_MAX_CHARS) {
1372
+ view = create('div', { className: 'dfv-code-wrap' }, create(CodeBlock, { code: entry.content, lang: lang }))
1373
+ } else {
1374
+ view = create('pre', { className: 'dfv-code' }, entry.content)
1375
+ }
1376
+ }
1377
+ return create(React.Fragment, null,
1378
+ entry.truncated ? create('div', { className: 'dfv-cap' },
1379
+ '文件超过 2 MB,仅预览前 2 MB;完整内容请用系统编辑器打开。') : null,
1380
+ view,
1381
+ )
1382
+ }
1383
+
1384
+ function FilesView(props) {
1385
+ var s = useStore()
1386
+ var files = s.files
1387
+ var active = files.active ? findFileEntry(files.active) : null
1388
+
1389
+ var tabs = files.list.map(function (entry) {
1390
+ return create('button', {
1391
+ key: entry.path,
1392
+ type: 'button',
1393
+ className: 'dfv-tab' + (files.active === entry.path ? ' active' : ''),
1394
+ title: entry.path,
1395
+ onClick: function () { setActiveFile(entry.path) },
1396
+ },
1397
+ create('span', { className: 'dfv-tab-name' }, entry.name),
1398
+ create('span', {
1399
+ className: 'dfv-close',
1400
+ role: 'button',
1401
+ title: '关闭',
1402
+ onClick: function (e) { e.stopPropagation(); closeFileTab(entry.path) },
1403
+ }, '✕'),
1404
+ )
1405
+ })
1406
+
1407
+ var body = null
1408
+ if (!active) {
1409
+ body = create('div', { className: 'dfv-empty' },
1410
+ create('span', { style: { fontSize: 22 } }, '📄'),
1411
+ '在右侧资源管理器中双击或右键「预览」文件',
1412
+ )
1413
+ } else {
1414
+ body = create('div', { className: 'dfv-body' },
1415
+ create('div', { className: 'dfv-toolbar' },
1416
+ create('span', { className: 'dfv-toolbar-path', title: active.path }, active.path),
1417
+ typeof active.size === 'number' && active.size > 0 ? create('span', null, formatBytes(active.size)) : null,
1418
+ active.loaded ? create('button', {
1419
+ type: 'button',
1420
+ className: 'dfv-btn',
1421
+ title: '从磁盘重新读取(刷新预览)',
1422
+ onClick: function () { reloadFile(active.path) },
1423
+ }, '⟳ 重载') : null,
1424
+ ),
1425
+ create(FileContent, { key: active.path, entry: active }),
1426
+ )
1427
+ }
1428
+
1429
+ return create('div', { className: 'dfv-root' },
1430
+ files.list.length > 0 ? create('div', { className: 'dfv-tabbar' }, tabs) : null,
1431
+ body,
1432
+ )
1433
+ }
1434
+
1435
+ function findFrameEl(startEl) {
1436
+ var cur = startEl && startEl.parentElement
1437
+ while (cur) {
1438
+ try {
1439
+ var gtc = cur.style && cur.style.gridTemplateColumns
1440
+ if (gtc && gtc.indexOf('1fr') !== -1) return cur
1441
+ } catch (e) {}
1442
+ cur = cur.parentElement
1443
+ }
1444
+ return null
1445
+ }
1446
+
1447
+ function setHeroDetailsWidth(startEl, width) {
1448
+ var frame = findFrameEl(startEl)
1449
+ if (!frame) return false
1450
+ try {
1451
+ var gtc = frame.style.gridTemplateColumns
1452
+ if (!gtc) return false
1453
+ var parts = gtc.trim().split(/\s+/)
1454
+ if (parts.length < 3) return false
1455
+ parts[parts.length - 1] = width + 'px'
1456
+ frame.style.gridTemplateColumns = parts.join(' ')
1457
+ return true
1458
+ } catch (e) {
1459
+ return false
1460
+ }
1461
+ }
1462
+
1463
+ function Panel(props) {
1464
+ var s = useStore()
1465
+ var sessionId = props && props.sessionId
1466
+ // 客户端会话快照是本会话 cwd 的可信来源(与官方 DetailsPanel 同源,
1467
+ // 始终是当前会话所在项目目录,而非宿主启动时的根目录)。
1468
+ var useSessions = props && props.useSessions
1469
+ var liveCwd = useSessions ? useSessions(function (list) {
1470
+ return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
1471
+ }) : undefined
1472
+ var cwdRef = React.useRef(liveCwd)
1473
+ cwdRef.current = liveCwd || cwdRef.current
1474
+ var panelRef = React.useRef(null)
1475
+ var frameRef = React.useRef(null)
1476
+ var widthRef = React.useRef(DETAILS_DEFAULT)
1477
+ var dragRef = React.useRef({ startX: 0, startW: 0, active: false })
1478
+
1479
+ React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
1480
+
1481
+ var setFrameWidth = React.useCallback(function (w) {
1482
+ var frame = frameRef.current
1483
+ if (!frame) return
1484
+ try {
1485
+ var gtc = frame.style.gridTemplateColumns
1486
+ if (!gtc) return
1487
+ var parts = gtc.trim().split(/\s+/)
1488
+ if (parts.length < 3) return
1489
+ parts[parts.length - 1] = Math.round(w) + 'px'
1490
+ frame.style.gridTemplateColumns = parts.join(' ')
1491
+ } catch (e) {}
1492
+ }, [])
1493
+
1494
+ React.useEffect(function () {
1495
+ var el = panelRef.current
1496
+ if (!el) return
1497
+ var mo = null
1498
+ var cancelled = false
1499
+ function attach() {
1500
+ var frame = findFrameEl(el)
1501
+ if (!frame || mo) return
1502
+ frameRef.current = frame
1503
+ function sync() {
1504
+ try {
1505
+ var gtc = frame.style.gridTemplateColumns
1506
+ if (!gtc) return
1507
+ var parts = gtc.trim().split(/\s+/)
1508
+ if (parts.length < 3) return
1509
+ var last = parseFloat(parts[parts.length - 1])
1510
+ if (isNaN(last) || last <= 1) { setState({ open: false }); return }
1511
+ setState({ open: true })
1512
+ if (Math.round(last) !== Math.round(widthRef.current)) {
1513
+ parts[parts.length - 1] = Math.round(widthRef.current) + 'px'
1514
+ frame.style.gridTemplateColumns = parts.join(' ')
1515
+ }
1516
+ } catch (e) {}
1517
+ }
1518
+ mo = new MutationObserver(sync)
1519
+ mo.observe(frame, { attributes: true, attributeFilter: ['style'] })
1520
+ sync()
1521
+ }
1522
+ attach()
1523
+ if (!mo) {
1524
+ var tries = 0
1525
+ var t = setInterval(function () {
1526
+ if (cancelled || mo) { clearInterval(t); return }
1527
+ attach()
1528
+ if (++tries > 40) clearInterval(t)
1529
+ }, 100)
1530
+ }
1531
+ return function () { cancelled = true; if (mo) mo.disconnect() }
1532
+ }, [])
1533
+
1534
+ function onResizeDown(e) {
1535
+ e.preventDefault()
1536
+ try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) {}
1537
+ dragRef.current = { startX: e.clientX, startW: widthRef.current, active: true }
1538
+ if (frameRef.current) { try { frameRef.current.style.transition = 'none' } catch (e) {} }
1539
+ }
1540
+ function onResizeMove(e) {
1541
+ if (!dragRef.current.active) return
1542
+ var w = clampW(dragRef.current.startW - (e.clientX - dragRef.current.startX))
1543
+ widthRef.current = w
1544
+ setFrameWidth(w)
1545
+ }
1546
+ function onResizeUp(e) {
1547
+ dragRef.current.active = false
1548
+ try { e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) {}
1549
+ if (frameRef.current) { try { frameRef.current.style.transition = '' } catch (e) {} }
1550
+ }
1551
+
1552
+ visibleRows.length = 0
1553
+
1554
+ var bodyChildren = []
1555
+ if (state.newItem && state.newItem.parent === state.cwd) {
1556
+ bodyChildren.push(create('div', { key: '__new__', className: 'expl-row' },
1557
+ create('div', { className: 'expl-row-main', style: { paddingLeft: 8 } },
1558
+ create('span', { className: 'expl-icon' }, state.newItem.isDir ? '📁' : '📄'),
1559
+ create(NameInput, {
1560
+ initial: '',
1561
+ selectBase: false,
1562
+ onCommit: function (name) { commitNew(state.newItem.parent, name, state.newItem.isDir) },
1563
+ onCancel: function () { setState({ newItem: null }) },
1564
+ }),
1565
+ ),
1566
+ ))
1567
+ }
1568
+ if (s.tree.length === 0 && !(state.newItem && state.newItem.parent === state.cwd)) {
1569
+ bodyChildren.push(create('div', { key: '__empty__', className: 'expl-muted' }, '(空目录)'))
1570
+ } else {
1571
+ s.tree.forEach(function (node) { bodyChildren.push(create(Row, { key: node.path, node: node, depth: 0 })) })
1572
+ }
1573
+
1574
+ var container = create('div', {
1575
+ className: 'expl-body' + (state.dropTarget === state.cwd ? ' drop-target' : ''),
1576
+ onClick: onBlankClick,
1577
+ onContextMenu: onBlankContext,
1578
+ onDragOver: function (e) { e.preventDefault(); e.stopPropagation(); if (state.dropTarget !== state.cwd) setState({ dropTarget: state.cwd }) },
1579
+ onDragLeave: function (e) { if (state.dropTarget === state.cwd) setState({ dropTarget: null }) },
1580
+ onDrop: onBodyDrop,
1581
+ }, bodyChildren)
1582
+
1583
+ return create('div', { ref: panelRef, className: 'expl-panel' },
1584
+ create('div', { className: 'expl-resize', onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }),
1585
+ create('div', { className: 'expl-head' },
1586
+ create('span', { className: 'expl-title' }, '📁 资源管理器'),
1587
+ create('div', { className: 'expl-head-btns' },
1588
+ create('button', { className: 'expl-btn', onClick: function () { if (uploadInputEl) uploadInputEl.click() }, title: '上传文件' }, '⬆'),
1589
+ create('button', {
1590
+ className: 'expl-btn',
1591
+ onClick: function (event) {
1592
+ try { layout.closeDetails() } catch (e) {}
1593
+ setHeroDetailsWidth(event.currentTarget, 0)
1594
+ },
1595
+ title: '关闭',
1596
+ 'aria-label': '关闭资源管理器',
1597
+ }, '✕'),
1598
+ ),
1599
+ ),
1600
+ create('div', { className: 'expl-path', title: s.cwd || '' }, s.cwd || '…'),
1601
+ (s.uploads && s.uploads.length) ? create('div', { className: 'expl-uploads' },
1602
+ s.uploads.map(function (u, i) {
1603
+ var pct = u.done ? null : (u.total ? Math.round((u.loaded / u.total) * 100) : 0)
1604
+ return create('div', { key: i, className: 'expl-upload-row' + (u.done ? ' expl-upload-done' : '') },
1605
+ create('span', { className: 'expl-upload-glyph' }, u.done ? '✓' : (u.down ? '⬇' : '⬆')),
1606
+ create('span', { className: 'expl-upload-text' },
1607
+ u.done ? u.name
1608
+ : (u.down ? '下载中 ' : '上传中 ') + (u.totalFiles > 1 ? '(' + u.idx + '/' + u.totalFiles + ') ' : '') + u.name,
1609
+ ),
1610
+ !u.done && u.total ? create('span', { className: 'expl-upload-pct' }, pct + '%') : null,
1611
+ )
1612
+ }),
1613
+ ) : null,
1614
+ create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange: onUploadPicker }),
1615
+ s.error ? create('div', { className: 'expl-error' }, s.error) : null,
1616
+ s.notice ? create('div', { className: 'expl-notice' }, s.notice) : null,
1617
+ s.selected.size > 1
1618
+ ? create('div', { className: 'expl-bulk' }, '已选 ' + s.selected.size + ' 项 · 拖到文件夹可移动')
1619
+ : null,
1620
+ container,
1621
+ s.menu ? create('div', { className: 'expl-menu-mask', onMouseDown: function () { setState({ menu: null }) }, onContextMenu: function (e) { e.preventDefault(); setState({ menu: null }) } }) : null,
1622
+ s.menu ? create(Menu, { menu: s.menu }) : null,
1623
+ )
1624
+ }
1625
+
1626
+ function HeaderAction(props) {
1627
+ var s = useStore()
1628
+ var sessionId = props && props.sessionId
1629
+ var useSessions = props && props.useSessions
1630
+ var liveCwd = useSessions ? useSessions(function (list) {
1631
+ return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
1632
+ }) : undefined
1633
+ var cwdRef = React.useRef(liveCwd)
1634
+ cwdRef.current = liveCwd || cwdRef.current
1635
+ React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
1636
+ return create('button', {
1637
+ type: 'button',
1638
+ className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
1639
+ onClick: function () { try { if (s.open) layout.closeDetails(); else layout.openDetails() } catch (e) {} },
1640
+ title: '项目资源管理器',
1641
+ 'aria-label': '项目资源管理器',
1642
+ }, '📁')
1643
+ }
1644
+
1645
+ function HeroAction(props) {
1646
+ var s = useStore()
1647
+ var session = props && props.session
1648
+ var useSessions = props && props.useSessions
1649
+ var liveCwd = useSessions ? useSessions(function (list) {
1650
+ return (props.sessionId && list.byId[props.sessionId]) ? list.byId[props.sessionId].cwd : undefined
1651
+ }) : undefined
1652
+ var blank = !!(session && session.blank && session.composerPhase === 'blank')
1653
+ React.useEffect(function () {
1654
+ if (blank) trackSession(props.sessionId, liveCwd)
1655
+ }, [blank, props.sessionId])
1656
+ if (!blank) return null
1657
+ return create('div', { className: 'expl-hero-dock' },
1658
+ create('button', {
1659
+ type: 'button',
1660
+ className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
1661
+ onClick: function (event) {
1662
+ if (s.open) {
1663
+ try { layout.closeDetails() } catch (e) {}
1664
+ setHeroDetailsWidth(event.currentTarget, 0)
1665
+ } else if (!setHeroDetailsWidth(event.currentTarget, DETAILS_DEFAULT)) {
1666
+ try { layout.openDetails() } catch (e) {}
1667
+ }
1668
+ },
1669
+ title: '项目资源管理器',
1670
+ 'aria-label': '项目资源管理器',
1671
+ }, '📁'))
1672
+ }
1673
+
1674
+ // ------------------------------------------------------------------
1675
+ // 速度徽标:DOM 注入到 Deep diving(role=status)旁。
1676
+ // 不改 DSH 源码。用 MutationObserver 定位消息流里的 Deep diving,
1677
+ // 把徽标节点 append 进其行内(inline-flex 同行),实现永远紧贴。
1678
+ // ------------------------------------------------------------------
1679
+ // 模块级 timer 引用:apply(ctx) 里赋值(与 dsh-soup 现有 layout 同款模式)
1680
+ var timerRef = null
1681
+
1682
+ function SpeedBadge(props) {
1683
+ var sessionId = props && (props.sessionId || (props.session && props.session.id))
1684
+
1685
+ // 持有最新状态的最新值的 ref(供 MutationObserver 回调读取)
1686
+ var statusRef = React.useRef(null)
1687
+ var dotsRef = React.useRef(1)
1688
+ var badgeElRef = React.useRef(null)
1689
+ // hostRef:当前挂载的 Deep diving 宿主元素(observer 找到后持有)
1690
+ var hostRef = React.useRef(null)
1691
+
1692
+ // 省略号循环
1693
+ React.useEffect(function () {
1694
+ if (!timerRef) return
1695
+ var stop = timerRef.interval(function () {
1696
+ dotsRef.current = dotsRef.current >= 3 ? 1 : dotsRef.current + 1
1697
+ if (hostRef.current) renderBadge()
1698
+ }, 400)
1699
+ return function () { stop() }
1700
+ }, [])
1701
+
1702
+ // 速度轮询
1703
+ React.useEffect(function () {
1704
+ if (!sessionId || !timerRef) return
1705
+ var cancelled = false
1706
+ var poll = function () {
1707
+ rpc('speed-status', { sessionId: sessionId }).then(function (res) {
1708
+ if (!cancelled && res && res.ok) {
1709
+ statusRef.current = res
1710
+ if (hostRef.current) renderBadge()
1711
+ }
1712
+ }).catch(function () {})
1713
+ }
1714
+ poll()
1715
+ var stop = timerRef.interval(poll, 300)
1716
+ return function () { cancelled = true; stop() }
1717
+ }, [sessionId])
1718
+
1719
+ // 渲染徽标内容到 badgeEl(由需要时调用;这里用函数声明提升,需放在 effect 外)
1720
+ function renderBadge() {
1721
+ var el = badgeElRef.current
1722
+ if (!el) return
1723
+ var st = statusRef.current
1724
+ // 清空
1725
+ while (el.firstChild) el.removeChild(el.firstChild)
1726
+ if (!st || st.phase === 'idle' || st.phase === 'done') {
1727
+ el.style.display = 'none'
1728
+ return
1729
+ }
1730
+ el.style.display = 'inline-flex'
1731
+ if (st.phase === 'waiting') {
1732
+ // 宿主 .turnStatus 用 background-clip:text + 渐变透明色,内部子元素
1733
+ // 会继承 text-fill-color 而把颜色冲成渐变;必须 important 覆盖。
1734
+ el.style.cssText = 'display:inline-flex;align-items:center;margin-left:10px;font-weight:400;font-size:13px;color:var(--dsw-alias-label-caption);'
1735
+ el.style.setProperty('color', 'var(--dsw-alias-label-caption)', 'important')
1736
+ el.style.setProperty('-webkit-text-fill-color', 'var(--dsw-alias-label-caption)', 'important')
1737
+ el.textContent = '正在等待模型' + new Array(dotsRef.current + 1).join('.')
1738
+ return
1739
+ }
1740
+ // 流式阶段:token 计数立即显示;瞬时速率窗口未满(tps=0)时先不显示徽标,
1741
+ // 而不是回退到「正在等待模型」(否则短输出全程都显示等待)。
1742
+ var tps = st.tps || 0
1743
+ var bg = tps >= 50 ? '#53b3cb' : tps >= 30 ? '#9bc53d' : tps >= 15 ? '#f9c22e' : '#e01a4f'
1744
+ el.style.cssText = 'display:inline-flex;align-items:center;gap:6px;margin-left:10px;font-weight:400;font-size:11px;color:var(--dsw-alias-label-primary);-webkit-text-fill-color:var(--dsw-alias-label-primary);'
1745
+ var tok = document.createElement('span')
1746
+ tok.style.cssText = 'display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-label-caption);-webkit-text-fill-color:var(--dsw-alias-label-caption);'
1747
+ var SVG = 'http://www.w3.org/2000/svg'
1748
+ var svg = document.createElementNS(SVG, 'svg')
1749
+ svg.setAttribute('width', '10'); svg.setAttribute('height', '10'); svg.setAttribute('viewBox', '0 0 10 10')
1750
+ svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '1.2')
1751
+ svg.setAttribute('stroke-linecap', 'round'); svg.setAttribute('stroke-linejoin', 'round')
1752
+ var ln = document.createElementNS(SVG, 'line'); ln.setAttribute('x1', '5'); ln.setAttribute('y1', '1.5'); ln.setAttribute('x2', '5'); ln.setAttribute('y2', '8.5'); svg.appendChild(ln)
1753
+ var poly = document.createElementNS(SVG, 'polyline'); poly.setAttribute('points', '2 6 5 8.5 8 6'); svg.appendChild(poly)
1754
+ tok.appendChild(svg)
1755
+ tok.appendChild(document.createTextNode(String(Math.round(st.tokens))))
1756
+ el.appendChild(tok)
1757
+ if (tps > 0) {
1758
+ var pill = document.createElement('span')
1759
+ pill.style.cssText = 'margin-left:6px;padding:1px 6px;border-radius:4px;background:' + bg + ';color:#fff;-webkit-text-fill-color:#fff;font-size:11px;font-weight:500;'
1760
+ pill.textContent = tps.toFixed(1) + ' t/s'
1761
+ el.appendChild(pill)
1762
+ }
1763
+ }
1764
+
1765
+ // MutationObserver:定位 Deep diving 并把 badgeEl 挂进去
1766
+ React.useEffect(function () {
1767
+ if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return
1768
+ // 初始化徽标 DOM 节点
1769
+ if (badgeElRef.current === null) {
1770
+ badgeElRef.current = document.createElement('span')
1771
+ badgeElRef.current.setAttribute('data-dsh-speed-badge', '')
1772
+ badgeElRef.current.style.display = 'none'
1773
+ }
1774
+ var badge = badgeElRef.current
1775
+
1776
+ function findTurnStatus() {
1777
+ var candidates = document.querySelectorAll('[data-chat-flow] [role="status"], [data-chat-flow] [aria-live="polite"]')
1778
+ for (var i = 0; i < candidates.length; i++) {
1779
+ if (candidates[i].textContent && candidates[i].textContent.indexOf('Deep diving') !== -1) return candidates[i]
1780
+ }
1781
+ return null
1782
+ }
1783
+
1784
+ function attach() {
1785
+ // 已挂载且宿主仍在 DOM 中时无需重复查找:徽标内容由 timer/poll 驱动
1786
+ // renderBadge 更新,observer 只负责把徽标挂进/移出宿主元素。
1787
+ if (hostRef.current && hostRef.current.isConnected) {
1788
+ // 若 DSH 在徽标之后又追加了子元素(如 15s 后的 elapsed clock),
1789
+ // 把徽标移到末尾,让它始终紧跟 Deep diving 的计时。
1790
+ if (badge.parentNode !== hostRef.current || hostRef.current.lastElementChild !== badge) {
1791
+ try { hostRef.current.appendChild(badge) } catch (e) {}
1792
+ }
1793
+ return
1794
+ }
1795
+ var target = findTurnStatus()
1796
+ if (target !== null) {
1797
+ hostRef.current = target
1798
+ if (badge.parentNode !== target) { try { target.appendChild(badge) } catch (e) {} }
1799
+ renderBadge()
1800
+ } else if (hostRef.current !== null) {
1801
+ if (badge.parentNode) { try { badge.parentNode.removeChild(badge) } catch (e) {} }
1802
+ hostRef.current = null
1803
+ }
1804
+ }
1805
+
1806
+ var mo = new MutationObserver(function () { attach() })
1807
+ mo.observe(document.body, { childList: true, subtree: true })
1808
+ attach()
1809
+ return function () {
1810
+ mo.disconnect()
1811
+ if (badge.parentNode) { try { badge.parentNode.removeChild(badge) } catch (e) {} }
1812
+ hostRef.current = null
1813
+ }
1814
+ }, [])
1815
+
1816
+ return null
1817
+ }
1818
+
1819
+ // ------------------------------------------------------------------
1820
+ // ------------------------------------------------------------------
1821
+ // GoalBar 多行版:复用 DSH 原生 GoalBar 的 goal projection 与动作动词
1822
+ // (onEdit/onPause/onResume/onClear),仅在展示上把单行截断放开为多行、
1823
+ // 编辑用 textarea。通过 conversation.input.dock 同 id 'goal' + 更低 priority
1824
+ // (-1 < 0) 遮蔽默认实现,不改 DSH 源码,工具与数据仍走原生 goal。
1825
+ // ------------------------------------------------------------------
1826
+ var GOAL_PHASE_LABELS = {
1827
+ active: 'phase.active',
1828
+ paused: 'phase.paused',
1829
+ blocked: 'phase.blocked',
1830
+ }
1831
+
1832
+ function goalIcon(children) {
1833
+ return create('svg', {
1834
+ width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none',
1835
+ stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round',
1836
+ strokeLinejoin: 'round', 'aria-hidden': true,
1837
+ }, children)
1838
+ }
1839
+
1840
+ var ICON_GOAL = goalIcon([
1841
+ create('circle', { key: 'a', cx: '12', cy: '12', r: '10' }),
1842
+ create('circle', { key: 'b', cx: '12', cy: '12', r: '6' }),
1843
+ create('circle', { key: 'c', cx: '12', cy: '12', r: '2', fill: 'currentColor', stroke: 'none' }),
1844
+ ])
1845
+ var ICON_PAUSE = goalIcon([
1846
+ create('rect', { key: 'a', x: '6', y: '4', width: '4', height: '16', rx: '1', fill: 'currentColor', stroke: 'none' }),
1847
+ create('rect', { key: 'b', x: '14', y: '4', width: '4', height: '16', rx: '1', fill: 'currentColor', stroke: 'none' }),
1848
+ ])
1849
+ var ICON_PLAY = goalIcon([
1850
+ create('path', { key: 'p', d: 'M6 4l14 8-14 8V4z', fill: 'currentColor', stroke: 'none' }),
1851
+ ])
1852
+ var ICON_EDIT = goalIcon([
1853
+ create('path', { key: 'a', d: 'M12 20h9' }),
1854
+ create('path', { key: 'b', d: 'M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z' }),
1855
+ ])
1856
+ var ICON_CHECK = goalIcon([
1857
+ create('polyline', { key: 'a', points: '20 6 9 17 4 12' }),
1858
+ ])
1859
+ var ICON_CLOSE = goalIcon([
1860
+ create('line', { key: 'a', x1: '18', y1: '6', x2: '6', y2: '18' }),
1861
+ create('line', { key: 'b', x1: '6', y1: '6', x2: '18', y2: '18' }),
1862
+ ])
1863
+ var ICON_TRASH = goalIcon([
1864
+ create('polyline', { key: 'a', points: '3 6 5 6 21 6' }),
1865
+ create('path', { key: 'b', d: 'M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6' }),
1866
+ create('path', { key: 'c', d: 'M10 11v6' }),
1867
+ create('path', { key: 'd', d: 'M14 11v6' }),
1868
+ create('path', { key: 'e', d: 'M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2' }),
1869
+ ])
1870
+
1871
+ function GoalBar(props) {
1872
+ var goal = props.goal
1873
+ var roundsStarted = props.roundsStarted
1874
+ var onEdit = props.onEdit
1875
+ var onPause = props.onPause
1876
+ var onResume = props.onResume
1877
+ var onClear = props.onClear
1878
+ var t = props.t || function (k) { return k }
1879
+
1880
+ var editing = React.useState(false)
1881
+ var draft = React.useState('')
1882
+ var pending = React.useState(false)
1883
+ var actionError = React.useState(null)
1884
+ var clearedGoalId = React.useState(null)
1885
+ var pendingRef = React.useRef(false)
1886
+ var textareaRef = React.useRef(null)
1887
+
1888
+ var goalId = goal ? goal.id : undefined
1889
+ React.useEffect(function () {
1890
+ editing[1](false)
1891
+ actionError[1](null)
1892
+ clearedGoalId[1](null)
1893
+ }, [goalId])
1894
+
1895
+ React.useEffect(function () {
1896
+ var input = textareaRef.current
1897
+ if (!input) return
1898
+ input.style.height = 'auto'
1899
+ input.style.height = Math.max(64, input.scrollHeight) + 'px'
1900
+ }, [draft[0], editing[0]])
1901
+
1902
+ function runAction(action) {
1903
+ if (pendingRef.current) return Promise.resolve(undefined)
1904
+ pendingRef.current = true
1905
+ pending[1](true)
1906
+ actionError[1](null)
1907
+ return Promise.resolve().then(function () { return action() }).then(function (result) {
1908
+ pendingRef.current = false
1909
+ pending[1](false)
1910
+ if (!result || !result.ok) {
1911
+ var err = result && result.error ? (result.error.message + ' (' + result.error.code + ')') : '操作失败'
1912
+ actionError[1](err)
1913
+ }
1914
+ return result
1915
+ }).catch(function (e) {
1916
+ pendingRef.current = false
1917
+ pending[1](false)
1918
+ actionError[1](String((e && e.message) || e))
1919
+ })
1920
+ }
1921
+
1922
+ function handleSave() {
1923
+ var trimmed = draft[0].trim()
1924
+ if (trimmed === '') return
1925
+ runAction(function () { return onEdit(trimmed) }).then(function (result) {
1926
+ if (result && result.ok) {
1927
+ editing[1](false)
1928
+ }
1929
+ })
1930
+ }
1931
+
1932
+ function handleClear() {
1933
+ if (!goal) return
1934
+ var clearedId = goal.id
1935
+ runAction(onClear).then(function (result) {
1936
+ if (result && result.ok) clearedGoalId[1](clearedId)
1937
+ })
1938
+ }
1939
+
1940
+ function openEdit() {
1941
+ if (!goal) return
1942
+ draft[1](goal.objective)
1943
+ editing[1](true)
1944
+ }
1945
+
1946
+ function iconBtn(label, onClick, iconNode, disabled) {
1947
+ return create('button', {
1948
+ type: 'button', title: label, 'aria-label': label,
1949
+ className: 'dsh-goal-btn', disabled: disabled === true || pending[0],
1950
+ onClick: function () { void onClick() },
1951
+ }, iconNode)
1952
+ }
1953
+
1954
+ if (goal === undefined || goal === null || goal.phase === 'complete' || goal.id === clearedGoalId[0]) return null
1955
+
1956
+ if (editing[0]) {
1957
+ return create('div', { className: 'dsh-goal-dock', 'data-goal-bar': '' },
1958
+ create('div', { className: 'dsh-goal-bar' },
1959
+ create('div', { className: 'dsh-goal-head' },
1960
+ create('span', { className: 'dsh-goal-glyph' }, ICON_GOAL),
1961
+ create('span', { className: 'dsh-goal-label' }, t(GOAL_PHASE_LABELS[goal.phase] || 'phase.active')),
1962
+ actionError[0] !== null && create('span', { className: 'dsh-goal-error', role: 'alert' }, actionError[0]),
1963
+ create('div', { className: 'dsh-goal-actions' },
1964
+ iconBtn(t('action.save'), handleSave, ICON_CHECK, draft[0].trim() === ''),
1965
+ iconBtn(t('action.cancel'), function () { editing[1](false) }, ICON_CLOSE),
1966
+ ),
1967
+ ),
1968
+ create('textarea', {
1969
+ className: 'dsh-goal-input',
1970
+ ref: textareaRef,
1971
+ 'aria-label': t('objective.aria'),
1972
+ value: draft[0],
1973
+ autoFocus: true,
1974
+ onChange: function (e) { draft[1](e.target.value) },
1975
+ onKeyDown: function (e) {
1976
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); void handleSave() }
1977
+ else if (e.key === 'Escape') { e.preventDefault(); editing[1](false) }
1978
+ },
1979
+ }),
1980
+ ),
1981
+ )
1982
+ }
1983
+
1984
+ var title = goal.phase === 'blocked' && goal.blockedReason ? goal.blockedReason.message : undefined
1985
+ var rounds = roundsStarted !== undefined && goal.maxGoalRounds !== undefined
1986
+ ? roundsStarted + '/' + goal.maxGoalRounds
1987
+ : undefined
1988
+ return create('div', { className: 'dsh-goal-dock', 'data-goal-bar': '' },
1989
+ create('div', { className: 'dsh-goal-bar', title: title || '' },
1990
+ create('div', { className: 'dsh-goal-head' },
1991
+ create('span', { className: 'dsh-goal-glyph' }, ICON_GOAL),
1992
+ create('span', { className: 'dsh-goal-label' }, t(GOAL_PHASE_LABELS[goal.phase] || 'phase.active')),
1993
+ rounds !== undefined && create('span', { className: 'dsh-goal-rounds' }, rounds),
1994
+ actionError[0] !== null && create('span', { className: 'dsh-goal-error', role: 'alert' }, actionError[0]),
1995
+ create('div', { className: 'dsh-goal-actions' },
1996
+ goal.phase === 'active' && iconBtn(t('action.pause'), function () { return runAction(onPause) }, ICON_PAUSE),
1997
+ goal.phase === 'paused' && iconBtn(t('action.resume'), function () { return runAction(onResume) }, ICON_PLAY),
1998
+ iconBtn(t('action.edit'), openEdit, ICON_EDIT),
1999
+ iconBtn(t('action.clear'), handleClear, ICON_TRASH),
2000
+ ),
2001
+ ),
2002
+ create('div', { className: 'dsh-goal-objective' }, goal.objective),
2003
+ ),
2004
+ )
2005
+ }
2006
+
2007
+ function GoalDock(props) {
2008
+ var projection = props.useProjection('goal')
2009
+ var goal = projection === undefined ? undefined : projection === null ? null : projection.goal
2010
+ return create(GoalBar, {
2011
+ goal: goal,
2012
+ roundsStarted: projection === undefined || projection === null ? undefined : projection.roundsStarted,
2013
+ onEdit: props.onEdit,
2014
+ onPause: props.onPause,
2015
+ onResume: props.onResume,
2016
+ onClear: props.onClear,
2017
+ t: props.t,
2018
+ })
2019
+ }
2020
+
2021
+ /** 应用浏览器半区:注册会话头部/空白会话切换按钮、details 右列面板、GoalBar、速度徽标。 */
2022
+ function apply(ctx) {
2023
+ var slots = ctx.slots
2024
+ layout = ctx.layout
2025
+ timerRef = ctx.timer
2026
+ var sessions = ctx.sessions
2027
+ var remoteGoals = ctx.remote.goals
2028
+ var disposers = []
2029
+ slots.inject('conversation.session.header.utilities', function () {
2030
+ disposers.push(slots.register(
2031
+ { name: 'conversation.session.header.utilities', id: 'dsh-soup-toggle', order: 10, label: '资源管理器' },
2032
+ function (props) { return create(HeaderAction, props) },
2033
+ ))
2034
+ })
2035
+ slots.inject('details', function () {
2036
+ disposers.push(slots.register(
2037
+ // `details` is a single slot occupied by the shell at priority 0.
2038
+ // Lower priorities render first, so this intentionally shadows it.
2039
+ { name: 'details', priority: -1 },
2040
+ function (props) { return create(Panel, props) },
2041
+ ))
2042
+ })
2043
+ // 文件标签页:与 对话/轨迹 同级的原生 view tab(ui-trajectory 同款注册方式)。
2044
+ // 槽位 entry 是静态的,多文件由 FilesView 内部子 tab 条管理。
2045
+ slots.inject('conversation.view', function () {
2046
+ disposers.push(slots.register(
2047
+ { name: 'conversation.view', id: 'dsh-soup-files', order: 20, label: FILES_TAB_LABEL },
2048
+ function (props) { return create(FilesView, props) },
2049
+ ))
2050
+ })
2051
+ // 从 goal projection 读取当前 CAS ref(与原生 GoalBar 同款逻辑)。
2052
+ function refOf(sessionId) {
2053
+ try {
2054
+ var binding = sessions.binding(sessionId)
2055
+ var face = binding && binding.session && binding.session.projections && binding.session.projections.faceOf('goal')
2056
+ var snapshot = face && face.getSnapshot()
2057
+ if (snapshot && snapshot.goal) return { id: snapshot.goal.id, revision: snapshot.goal.revision }
2058
+ } catch (e) { /* ignore */ }
2059
+ return undefined
2060
+ }
2061
+ var noCurrentGoal = {
2062
+ ok: false,
2063
+ error: { code: 'no-current-goal', message: 'no current goal to mutate', details: {} },
2064
+ }
2065
+ slots.inject('conversation.input.dock', function () {
2066
+ disposers.push(slots.register(
2067
+ { name: 'conversation.input.dock', id: 'dsh-soup-hero-toggle', order: -10 },
2068
+ function (props) { return create(HeroAction, props) },
2069
+ ))
2070
+ // GoalBar 多行版:同 id 'goal'、更低 priority (-1) -> 遮蔽默认单行实现。
2071
+ // inject 提供 onEdit/onPause/onResume/onClear 动作动词(与原生 GoalBar 一致)。
2072
+ disposers.push(slots.register(
2073
+ {
2074
+ name: 'conversation.input.dock', id: 'goal', order: 10, priority: -1, locale: 'goal',
2075
+ inject: function (sessionId) {
2076
+ return {
2077
+ onEdit: function (objective) {
2078
+ var ref = refOf(sessionId)
2079
+ if (ref === undefined) return Promise.resolve(noCurrentGoal)
2080
+ return remoteGoals.edit(sessionId, ref, { objective: objective })
2081
+ },
2082
+ onPause: function () {
2083
+ var ref = refOf(sessionId)
2084
+ if (ref === undefined) return Promise.resolve(noCurrentGoal)
2085
+ return remoteGoals.pause(sessionId, ref)
2086
+ },
2087
+ onResume: function () {
2088
+ var ref = refOf(sessionId)
2089
+ if (ref === undefined) return Promise.resolve(noCurrentGoal)
2090
+ return remoteGoals.resume(sessionId, ref)
2091
+ },
2092
+ onClear: function () {
2093
+ var ref = refOf(sessionId)
2094
+ if (ref === undefined) return Promise.resolve(noCurrentGoal)
2095
+ return remoteGoals.clear(sessionId, ref)
2096
+ },
2097
+ }
2098
+ },
2099
+ },
2100
+ function (props) { return create(GoalDock, props) },
2101
+ ))
2102
+ disposers.push(slots.register(
2103
+ { name: 'conversation.input.dock', id: 'dsh-speed-badge', order: 30, label: '速度徽标' },
2104
+ function (props) { return create(SpeedBadge, props) },
2105
+ ))
2106
+ })
2107
+ ctx.effect(
2108
+ function () { return function () { disposers.forEach(function (d) { try { d() } catch (e) {} }) } },
2109
+ 'dsh-soup: explorer toggles + details panel + speed badge',
2110
+ )
2111
+ }
2112
+
2113
+ exports.apply = apply
2114
+ exports.inject = inject
2115
+ return module.exports
2116
+ },
2117
+ })