@lyhue1991/dsh-soup 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/client.js +456 -420
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,8 @@ dsh plugin add @lyhue1991/dsh-soup
|
|
|
97
97
|
|
|
98
98
|
## 更新日志
|
|
99
99
|
|
|
100
|
-
- **0.4.
|
|
100
|
+
- **0.4.1(当前)** — **浏览器半区内部整理**:`lib/client.js` 按四大功能分区重排并加统一标题注释(i18n 基座 → 资源管理器 → 会话内预览 → 速率徽标 → 多行 GoalBar → apply 注册),纯重排无逻辑变更、无行为差异。
|
|
101
|
+
- **0.4.0** —
|
|
101
102
|
- **语言适配(i18n)**:dsh-soup 全部文案(资源管理器、预览页、右键菜单、上传/下载、速度徽标、GoalBar)跟随 DSH 通用设置的语言切换(zh/en),切换即时生效。
|
|
102
103
|
- **预览 tab 纯阅读模式**:激活预览时隐藏底部输入框、「N 轮 · M 步」统计行、GoalBar 与速度徽标——整屏干净内容区,切回对话/轨迹恢复。
|
|
103
104
|
- **0.3.0** —
|
package/lib/client.js
CHANGED
|
@@ -198,6 +198,10 @@ window.__ModuleLoader__.load({
|
|
|
198
198
|
/** 需要的客户端服务:slots、layout、timer、sessions、locale(i18n 词典)+ remote.goals(goal 动作动词)。 */
|
|
199
199
|
var inject = ['slots', 'layout', 'timer', 'sessions', 'locale', 'remote', 'remote.goals']
|
|
200
200
|
|
|
201
|
+
// ------------------------------------------------------------------
|
|
202
|
+
// ------------------------------------------------------------------
|
|
203
|
+
// ▓▓ 区域零 · i18n / 词典
|
|
204
|
+
// 注册双语词典 NS/DICT;组件内 t()、组件外 T(),语言切换即时生效。
|
|
201
205
|
// ------------------------------------------------------------------
|
|
202
206
|
// i18n:走 DSH 官方插件词典机制(ui-cordis / ui-conversation 同款)——
|
|
203
207
|
// ctx.locale.register(NS, DICT) 注册;槽位配置加 locale: NS 后组件 props
|
|
@@ -362,6 +366,10 @@ window.__ModuleLoader__.load({
|
|
|
362
366
|
return state
|
|
363
367
|
}
|
|
364
368
|
|
|
369
|
+
// ------------------------------------------------------------------
|
|
370
|
+
// ▓▓ 区域一 · 资源管理器 · 🧅 葱(共享数据层)
|
|
371
|
+
// 共享 store、/api/dsh-soup RPC、树数据加载/自动刷新、selection、拖拽/上传/下载、类型图标 —— 预览亦复用其中 store/rpc。
|
|
372
|
+
// ------------------------------------------------------------------
|
|
365
373
|
var visibleRows = []
|
|
366
374
|
var uploadInputEl = null
|
|
367
375
|
var layout = null
|
|
@@ -991,137 +999,10 @@ window.__ModuleLoader__.load({
|
|
|
991
999
|
}
|
|
992
1000
|
|
|
993
1001
|
// ------------------------------------------------------------------
|
|
994
|
-
// 文件标签页:右键/双击「预览」把文件以只读预览展示进会话区的「预览」
|
|
995
|
-
// tab(原生 conversation.view 槽位,与 对话/轨迹 同级)。
|
|
996
|
-
// markdown/html/json/csv 有专属渲染器;图片预览;其余按纯文本。
|
|
997
|
-
// 状态存模块级 store,切走 tab 再切回来内容不丢。
|
|
998
1002
|
// ------------------------------------------------------------------
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
var activeSessionId = null
|
|
1003
|
-
|
|
1004
|
-
/** 同时打开的预览 tab 上限:超过后挤掉最早打开的(FIFO)。 */
|
|
1005
|
-
var FILES_MAX_OPEN = 5
|
|
1006
|
-
|
|
1007
|
-
/** path 是否落在 base 目录子树内(base 为根或空时视为全量放行)。 */
|
|
1008
|
-
function withinScope(path, base) {
|
|
1009
|
-
if (!base || base === '/') return true
|
|
1010
|
-
return path === base || path.indexOf(base + '/') === 0
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
/**
|
|
1014
|
-
* 会话切换时按新 cwd 裁剪预览列表:范围外的 tab 自动关闭。
|
|
1015
|
-
* 若当前激活文件被裁掉,激活项顺移到剩余最后一个。
|
|
1016
|
-
* 列表无变化时返回原引用,避免触发多余渲染。
|
|
1017
|
-
*/
|
|
1018
|
-
function pruneFilesToScope(files, cwd) {
|
|
1019
|
-
var kept = files.list.filter(function (f) { return withinScope(f.path, cwd) })
|
|
1020
|
-
try { window.__DFV_PRUNE = (window.__DFV_PRUNE || []) ; window.__DFV_PRUNE.push({ cwd: cwd, before: files.list.length, after: kept.length }) } catch (_) {}
|
|
1021
|
-
if (kept.length === files.list.length) return files
|
|
1022
|
-
var activeKept = kept.some(function (f) { return f.path === files.active })
|
|
1023
|
-
var active = files.active
|
|
1024
|
-
if (!activeKept) active = kept.length ? kept[kept.length - 1].path : null
|
|
1025
|
-
return { list: kept, active: active }
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function setFiles(patch) {
|
|
1029
|
-
setState({ files: Object.assign({}, state.files, patch) })
|
|
1030
|
-
try {
|
|
1031
|
-
var f = state.files
|
|
1032
|
-
window.__DFV_DBG = { n: f.list.length, active: f.active, ops: ((window.__DFV_DBG && window.__DFV_DBG.ops) || 0) + 1 }
|
|
1033
|
-
} catch (_) {}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function findFileEntry(path) {
|
|
1037
|
-
for (var i = 0; i < state.files.list.length; i++) {
|
|
1038
|
-
if (state.files.list[i].path === path) return state.files.list[i]
|
|
1039
|
-
}
|
|
1040
|
-
return null
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
/** 点击原生 header 的「预览」tab,把 view ring 切到预览视图。 */
|
|
1044
|
-
function activateFilesView() {
|
|
1045
|
-
try {
|
|
1046
|
-
var tabs = document.querySelectorAll('[role="tab"]')
|
|
1047
|
-
for (var i = 0; i < tabs.length; i++) {
|
|
1048
|
-
if (tabs[i].textContent === FILES_TAB_LABEL()) { tabs[i].click(); return true }
|
|
1049
|
-
}
|
|
1050
|
-
} catch (err) {}
|
|
1051
|
-
return false
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
async function openFileInTab(path) {
|
|
1055
|
-
setState({ menu: null })
|
|
1056
|
-
var entry = findFileEntry(path)
|
|
1057
|
-
if (!entry) {
|
|
1058
|
-
entry = {
|
|
1059
|
-
path: path, name: baseName(path),
|
|
1060
|
-
loading: true, loaded: false, error: '',
|
|
1061
|
-
kind: null, content: '', dataUrl: '', data: '',
|
|
1062
|
-
truncated: false, size: 0,
|
|
1063
|
-
}
|
|
1064
|
-
// FIFO:最多同时 5 个预览,新开的挤掉最早打开的
|
|
1065
|
-
var list = state.files.list.slice()
|
|
1066
|
-
while (list.length >= FILES_MAX_OPEN) list.shift()
|
|
1067
|
-
list.push(entry)
|
|
1068
|
-
setFiles({ list: list, active: path })
|
|
1069
|
-
} else if (entry.loaded) {
|
|
1070
|
-
setFiles({ active: path })
|
|
1071
|
-
activateFilesView()
|
|
1072
|
-
return
|
|
1073
|
-
} else {
|
|
1074
|
-
setFiles({ active: path })
|
|
1075
|
-
}
|
|
1076
|
-
activateFilesView()
|
|
1077
|
-
var res = await rpc('read', { path: path, sessionId: activeSessionId })
|
|
1078
|
-
if (!res.ok && /超出允许范围/.test(res.error || '')) {
|
|
1079
|
-
await new Promise(function (r) { setTimeout(r, 600) })
|
|
1080
|
-
res = await rpc('read', { path: path, sessionId: activeSessionId })
|
|
1081
|
-
}
|
|
1082
|
-
var cur = findFileEntry(path)
|
|
1083
|
-
if (!cur) return
|
|
1084
|
-
cur.loading = false
|
|
1085
|
-
if (!res || !res.ok) {
|
|
1086
|
-
cur.error = (res && res.error) || T('explorer.readFail')
|
|
1087
|
-
} else {
|
|
1088
|
-
cur.error = ''
|
|
1089
|
-
cur.loaded = true
|
|
1090
|
-
cur.kind = res.kind
|
|
1091
|
-
cur.size = res.size || 0
|
|
1092
|
-
if (res.kind === 'text') { cur.content = res.content; cur.truncated = !!res.truncated }
|
|
1093
|
-
else if (res.kind === 'image') { cur.dataUrl = 'data:' + res.mime + ';base64,' + res.data }
|
|
1094
|
-
else if (res.kind === 'pdf') { cur.data = res.data }
|
|
1095
|
-
}
|
|
1096
|
-
setFiles({ list: state.files.list.slice(), active: path })
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
async function reloadFile(path) {
|
|
1100
|
-
var entry = findFileEntry(path)
|
|
1101
|
-
if (!entry) return
|
|
1102
|
-
// 复位 loaded,让 openFileInTab 走重读路径而不是「已打开直接激活」。
|
|
1103
|
-
entry.loaded = false
|
|
1104
|
-
await openFileInTab(path)
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
function closeFileTab(path) {
|
|
1108
|
-
var list = state.files.list
|
|
1109
|
-
var idx = -1
|
|
1110
|
-
for (var i = 0; i < list.length; i++) if (list[i].path === path) { idx = i; break }
|
|
1111
|
-
if (idx < 0) return
|
|
1112
|
-
var next = list.slice(0, idx).concat(list.slice(idx + 1))
|
|
1113
|
-
var active = state.files.active
|
|
1114
|
-
if (active === path) {
|
|
1115
|
-
var neighbor = next[Math.min(idx, next.length - 1)]
|
|
1116
|
-
active = neighbor ? neighbor.path : null
|
|
1117
|
-
}
|
|
1118
|
-
setFiles({ list: next, active: active })
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
function setActiveFile(path) {
|
|
1122
|
-
setFiles({ active: path })
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1003
|
+
// ▓▓ 区域一 · 资源管理器 · 🧅 葱(列表 UI)
|
|
1004
|
+
// NameInput / Row / buildMenuItems / Menu:行渲染、多选、右键菜单、重命名/新建。
|
|
1005
|
+
// ------------------------------------------------------------------
|
|
1125
1006
|
function NameInput(props) {
|
|
1126
1007
|
var initial = props.initial
|
|
1127
1008
|
var selectBase = props.selectBase !== false
|
|
@@ -1299,81 +1180,448 @@ window.__ModuleLoader__.load({
|
|
|
1299
1180
|
return create('div', { ref: menuRef, className: 'expl-menu', style: { left: pos.x, top: pos.y } }, menuChildren)
|
|
1300
1181
|
}
|
|
1301
1182
|
|
|
1302
|
-
// ------------------------------------------------------------------
|
|
1303
|
-
// 文件视图(conversation.view 'dsh-soup-files'):
|
|
1304
|
-
// 子 tab 条 + 只读预览区(markdown/html/json/csv 渲染 / 图片预览 / 二进制提示)。
|
|
1305
1183
|
// ------------------------------------------------------------------
|
|
1306
1184
|
// ------------------------------------------------------------------
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1185
|
+
// ▓▓ 区域一 · 资源管理器 · 🧅 葱(面板与入口)
|
|
1186
|
+
// Panel(右列 details 面板)/ HeaderAction(Session log 侧 📁)/ HeroAction(空白会话 📁)。
|
|
1309
1187
|
// ------------------------------------------------------------------
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
var
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
return 'text'
|
|
1326
|
-
}
|
|
1188
|
+
function Panel(props) {
|
|
1189
|
+
var s = useStore()
|
|
1190
|
+
var sessionId = props && props.sessionId
|
|
1191
|
+
// 客户端会话快照是本会话 cwd 的可信来源(与官方 DetailsPanel 同源,
|
|
1192
|
+
// 始终是当前会话所在项目目录,而非宿主启动时的根目录)。
|
|
1193
|
+
var useSessions = props && props.useSessions
|
|
1194
|
+
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1195
|
+
return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
|
|
1196
|
+
}) : undefined
|
|
1197
|
+
var cwdRef = React.useRef(liveCwd)
|
|
1198
|
+
cwdRef.current = liveCwd || cwdRef.current
|
|
1199
|
+
var panelRef = React.useRef(null)
|
|
1200
|
+
var frameRef = React.useRef(null)
|
|
1201
|
+
var widthRef = React.useRef(DETAILS_DEFAULT)
|
|
1202
|
+
var dragRef = React.useRef({ startX: 0, startW: 0, active: false })
|
|
1327
1203
|
|
|
1328
|
-
|
|
1329
|
-
// (packages/fs/tool-fs read-render.ts),json/md/markdown/html/htm 除外——
|
|
1330
|
-
// 它们走上方专属渲染器。
|
|
1331
|
-
var CODE_LANG_BY_EXT = {
|
|
1332
|
-
ts: 'ts', tsx: 'tsx', mts: 'ts', cts: 'ts',
|
|
1333
|
-
js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js',
|
|
1334
|
-
jsonc: 'json',
|
|
1335
|
-
py: 'py', rb: 'rb', go: 'go', rs: 'rs', java: 'java',
|
|
1336
|
-
c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', hpp: 'cpp', cxx: 'cpp',
|
|
1337
|
-
cs: 'cs', kt: 'kotlin', swift: 'swift', php: 'php',
|
|
1338
|
-
sh: 'sh', bash: 'sh', zsh: 'sh',
|
|
1339
|
-
yaml: 'yaml', yml: 'yaml', toml: 'toml', ini: 'ini',
|
|
1340
|
-
mdx: 'mdx', css: 'css', scss: 'scss', less: 'less',
|
|
1341
|
-
sql: 'sql', xml: 'xml', lua: 'lua',
|
|
1342
|
-
}
|
|
1204
|
+
React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
|
|
1343
1205
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1206
|
+
var setFrameWidth = React.useCallback(function (w) {
|
|
1207
|
+
var frame = frameRef.current
|
|
1208
|
+
if (!frame) return
|
|
1209
|
+
try {
|
|
1210
|
+
var gtc = frame.style.gridTemplateColumns
|
|
1211
|
+
if (!gtc) return
|
|
1212
|
+
var parts = gtc.trim().split(/\s+/)
|
|
1213
|
+
if (parts.length < 3) return
|
|
1214
|
+
parts[parts.length - 1] = Math.round(w) + 'px'
|
|
1215
|
+
frame.style.gridTemplateColumns = parts.join(' ')
|
|
1216
|
+
} catch (e) {}
|
|
1217
|
+
}, [])
|
|
1346
1218
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1219
|
+
React.useEffect(function () {
|
|
1220
|
+
var el = panelRef.current
|
|
1221
|
+
if (!el) return
|
|
1222
|
+
var mo = null
|
|
1223
|
+
var cancelled = false
|
|
1224
|
+
function attach() {
|
|
1225
|
+
var frame = findFrameEl(el)
|
|
1226
|
+
if (!frame || mo) return
|
|
1227
|
+
frameRef.current = frame
|
|
1228
|
+
function sync() {
|
|
1229
|
+
try {
|
|
1230
|
+
var gtc = frame.style.gridTemplateColumns
|
|
1231
|
+
if (!gtc) return
|
|
1232
|
+
var parts = gtc.trim().split(/\s+/)
|
|
1233
|
+
if (parts.length < 3) return
|
|
1234
|
+
var last = parseFloat(parts[parts.length - 1])
|
|
1235
|
+
if (isNaN(last) || last <= 1) { setState({ open: false }); return }
|
|
1236
|
+
setState({ open: true })
|
|
1237
|
+
if (Math.round(last) !== Math.round(widthRef.current)) {
|
|
1238
|
+
parts[parts.length - 1] = Math.round(widthRef.current) + 'px'
|
|
1239
|
+
frame.style.gridTemplateColumns = parts.join(' ')
|
|
1240
|
+
}
|
|
1241
|
+
} catch (e) {}
|
|
1361
1242
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
row.push(field); field = ''
|
|
1366
|
-
} else if (ch === '\n') {
|
|
1367
|
-
row.push(field); rows.push(row); row = []; field = ''
|
|
1368
|
-
} else if (ch !== '\r') {
|
|
1369
|
-
field += ch
|
|
1243
|
+
mo = new MutationObserver(sync)
|
|
1244
|
+
mo.observe(frame, { attributes: true, attributeFilter: ['style'] })
|
|
1245
|
+
sync()
|
|
1370
1246
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1247
|
+
attach()
|
|
1248
|
+
if (!mo) {
|
|
1249
|
+
var tries = 0
|
|
1250
|
+
var t = setInterval(function () {
|
|
1251
|
+
if (cancelled || mo) { clearInterval(t); return }
|
|
1252
|
+
attach()
|
|
1253
|
+
if (++tries > 40) clearInterval(t)
|
|
1254
|
+
}, 100)
|
|
1255
|
+
}
|
|
1256
|
+
return function () { cancelled = true; if (mo) mo.disconnect() }
|
|
1257
|
+
}, [])
|
|
1258
|
+
|
|
1259
|
+
function onResizeDown(e) {
|
|
1260
|
+
e.preventDefault()
|
|
1261
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) {}
|
|
1262
|
+
dragRef.current = { startX: e.clientX, startW: widthRef.current, active: true }
|
|
1263
|
+
if (frameRef.current) { try { frameRef.current.style.transition = 'none' } catch (e) {} }
|
|
1264
|
+
}
|
|
1265
|
+
function onResizeMove(e) {
|
|
1266
|
+
if (!dragRef.current.active) return
|
|
1267
|
+
var w = clampW(dragRef.current.startW - (e.clientX - dragRef.current.startX))
|
|
1268
|
+
widthRef.current = w
|
|
1269
|
+
setFrameWidth(w)
|
|
1270
|
+
}
|
|
1271
|
+
function onResizeUp(e) {
|
|
1272
|
+
dragRef.current.active = false
|
|
1273
|
+
try { e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) {}
|
|
1274
|
+
if (frameRef.current) { try { frameRef.current.style.transition = '' } catch (e) {} }
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
visibleRows.length = 0
|
|
1278
|
+
|
|
1279
|
+
var bodyChildren = []
|
|
1280
|
+
if (state.newItem && state.newItem.parent === state.cwd) {
|
|
1281
|
+
bodyChildren.push(create('div', { key: '__new__', className: 'expl-row' },
|
|
1282
|
+
create('div', { className: 'expl-row-main', style: { paddingLeft: 8 } },
|
|
1283
|
+
create('span', { className: 'expl-icon' }, state.newItem.isDir ? '📁' : '📄'),
|
|
1284
|
+
create(NameInput, {
|
|
1285
|
+
initial: '',
|
|
1286
|
+
selectBase: false,
|
|
1287
|
+
onCommit: function (name) { commitNew(state.newItem.parent, name, state.newItem.isDir) },
|
|
1288
|
+
onCancel: function () { setState({ newItem: null }) },
|
|
1289
|
+
}),
|
|
1290
|
+
),
|
|
1291
|
+
))
|
|
1292
|
+
}
|
|
1293
|
+
if (s.tree.length === 0 && !(state.newItem && state.newItem.parent === state.cwd)) {
|
|
1294
|
+
bodyChildren.push(create('div', { key: '__empty__', className: 'expl-muted' }, T('explorer.emptyDir')))
|
|
1295
|
+
} else {
|
|
1296
|
+
s.tree.forEach(function (node) { bodyChildren.push(create(Row, { key: node.path, node: node, depth: 0 })) })
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
var container = create('div', {
|
|
1300
|
+
className: 'expl-body' + (state.dropTarget === state.cwd ? ' drop-target' : ''),
|
|
1301
|
+
onClick: onBlankClick,
|
|
1302
|
+
onContextMenu: onBlankContext,
|
|
1303
|
+
onDragOver: function (e) { e.preventDefault(); e.stopPropagation(); if (state.dropTarget !== state.cwd) setState({ dropTarget: state.cwd }) },
|
|
1304
|
+
onDragLeave: function (e) { if (state.dropTarget === state.cwd) setState({ dropTarget: null }) },
|
|
1305
|
+
onDrop: onBodyDrop,
|
|
1306
|
+
}, bodyChildren)
|
|
1307
|
+
|
|
1308
|
+
return create('div', { ref: panelRef, className: 'expl-panel' },
|
|
1309
|
+
create('div', { className: 'expl-resize', onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }),
|
|
1310
|
+
create('div', { className: 'expl-head' },
|
|
1311
|
+
create('span', { className: 'expl-title' }, T('explorer.title')),
|
|
1312
|
+
create('div', { className: 'expl-head-btns' },
|
|
1313
|
+
create('button', {
|
|
1314
|
+
className: 'expl-btn',
|
|
1315
|
+
onClick: function () { refreshAll() },
|
|
1316
|
+
title: T('explorer.refreshTitle'),
|
|
1317
|
+
'aria-label': T('explorer.refresh'),
|
|
1318
|
+
}, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } })),
|
|
1319
|
+
create('button', {
|
|
1320
|
+
className: 'expl-btn',
|
|
1321
|
+
onClick: function () { if (uploadInputEl) uploadInputEl.click() },
|
|
1322
|
+
title: T('explorer.upload'),
|
|
1323
|
+
'aria-label': T('explorer.upload'),
|
|
1324
|
+
}, '⬆'),
|
|
1325
|
+
create('button', {
|
|
1326
|
+
className: 'expl-btn',
|
|
1327
|
+
style: { fontSize: '18px' },
|
|
1328
|
+
onClick: function (event) {
|
|
1329
|
+
try { layout.closeDetails() } catch (e) {}
|
|
1330
|
+
setHeroDetailsWidth(event.currentTarget, 0)
|
|
1331
|
+
},
|
|
1332
|
+
title: T('explorer.close'),
|
|
1333
|
+
'aria-label': T('explorer.closePanel'),
|
|
1334
|
+
}, '✕'),
|
|
1335
|
+
),
|
|
1336
|
+
),
|
|
1337
|
+
create('div', { className: 'expl-path', title: s.cwd || '' }, s.cwd || '…'),
|
|
1338
|
+
(s.uploads && s.uploads.length) ? create('div', { className: 'expl-uploads' },
|
|
1339
|
+
s.uploads.map(function (u, i) {
|
|
1340
|
+
var pct = u.done ? null : (u.total ? Math.round((u.loaded / u.total) * 100) : 0)
|
|
1341
|
+
return create('div', { key: i, className: 'expl-upload-row' + (u.done ? ' expl-upload-done' : '') },
|
|
1342
|
+
create('span', { className: 'expl-upload-glyph' }, u.done ? '✓' : (u.down ? '⬇' : '⬆')),
|
|
1343
|
+
create('span', { className: 'expl-upload-text' },
|
|
1344
|
+
u.done ? u.name
|
|
1345
|
+
: (u.down ? T('explorer.downloading') : T('explorer.uploading')) + (u.totalFiles > 1 ? '(' + u.idx + '/' + u.totalFiles + ') ' : '') + u.name,
|
|
1346
|
+
),
|
|
1347
|
+
!u.done && u.total ? create('span', { className: 'expl-upload-pct' }, pct + '%') : null,
|
|
1348
|
+
)
|
|
1349
|
+
}),
|
|
1350
|
+
) : null,
|
|
1351
|
+
create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange: onUploadPicker }),
|
|
1352
|
+
s.error ? create('div', { className: 'expl-error' }, s.error) : null,
|
|
1353
|
+
s.notice ? create('div', { className: 'expl-notice' }, s.notice) : null,
|
|
1354
|
+
s.selected.size > 1
|
|
1355
|
+
? create('div', { className: 'expl-bulk' }, T('explorer.bulkSelected', { n: s.selected.size }))
|
|
1356
|
+
: null,
|
|
1357
|
+
container,
|
|
1358
|
+
s.menu ? create('div', { className: 'expl-menu-mask', onMouseDown: function () { setState({ menu: null }) }, onContextMenu: function (e) { e.preventDefault(); setState({ menu: null }) } }) : null,
|
|
1359
|
+
s.menu ? create(Menu, { menu: s.menu }) : null,
|
|
1360
|
+
)
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function HeaderAction(props) {
|
|
1364
|
+
var s = useStore()
|
|
1365
|
+
var sessionId = props && props.sessionId
|
|
1366
|
+
var useSessions = props && props.useSessions
|
|
1367
|
+
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1368
|
+
return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
|
|
1369
|
+
}) : undefined
|
|
1370
|
+
var cwdRef = React.useRef(liveCwd)
|
|
1371
|
+
cwdRef.current = liveCwd || cwdRef.current
|
|
1372
|
+
React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
|
|
1373
|
+
return create('button', {
|
|
1374
|
+
type: 'button',
|
|
1375
|
+
className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
|
|
1376
|
+
onClick: function () { try { if (s.open) layout.closeDetails(); else layout.openDetails() } catch (e) {} },
|
|
1377
|
+
title: T('explorer.label'),
|
|
1378
|
+
'aria-label': T('explorer.label'),
|
|
1379
|
+
}, '📁')
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function HeroAction(props) {
|
|
1383
|
+
var s = useStore()
|
|
1384
|
+
var session = props && props.session
|
|
1385
|
+
var useSessions = props && props.useSessions
|
|
1386
|
+
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1387
|
+
return (props.sessionId && list.byId[props.sessionId]) ? list.byId[props.sessionId].cwd : undefined
|
|
1388
|
+
}) : undefined
|
|
1389
|
+
var blank = !!(session && session.blank && session.composerPhase === 'blank')
|
|
1390
|
+
React.useEffect(function () {
|
|
1391
|
+
if (blank) trackSession(props.sessionId, liveCwd)
|
|
1392
|
+
}, [blank, props.sessionId])
|
|
1393
|
+
if (!blank) return null
|
|
1394
|
+
return create('div', { className: 'expl-hero-dock' },
|
|
1395
|
+
create('button', {
|
|
1396
|
+
type: 'button',
|
|
1397
|
+
className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
|
|
1398
|
+
onClick: function (event) {
|
|
1399
|
+
if (s.open) {
|
|
1400
|
+
try { layout.closeDetails() } catch (e) {}
|
|
1401
|
+
setHeroDetailsWidth(event.currentTarget, 0)
|
|
1402
|
+
} else if (!setHeroDetailsWidth(event.currentTarget, DETAILS_DEFAULT)) {
|
|
1403
|
+
try { layout.openDetails() } catch (e) {}
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
title: T('explorer.label'),
|
|
1407
|
+
'aria-label': T('explorer.label'),
|
|
1408
|
+
}, '📁'))
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// ------------------------------------------------------------------
|
|
1412
|
+
// ------------------------------------------------------------------
|
|
1413
|
+
// ▓▓ 区域二 · 会话内预览 · 🫚 姜(files tab 状态)
|
|
1414
|
+
// files tab 状态机:打开/重载/关闭、FIFO(≤5)、会话切换按 cwd 裁剪。
|
|
1415
|
+
// ------------------------------------------------------------------
|
|
1416
|
+
// 文件标签页:右键/双击「预览」把文件以只读预览展示进会话区的「预览」
|
|
1417
|
+
// tab(原生 conversation.view 槽位,与 对话/轨迹 同级)。
|
|
1418
|
+
// markdown/html/json/csv 有专属渲染器;图片预览;其余按纯文本。
|
|
1419
|
+
// 状态存模块级 store,切走 tab 再切回来内容不丢。
|
|
1420
|
+
// ------------------------------------------------------------------
|
|
1421
|
+
var FILES_TAB_LABEL = function () { return T('files.tab') }
|
|
1422
|
+
|
|
1423
|
+
/** 当前活跃会话:list/read/download 携带给宿主,供围栏直查该会话 cwd。 */
|
|
1424
|
+
var activeSessionId = null
|
|
1425
|
+
|
|
1426
|
+
/** 同时打开的预览 tab 上限:超过后挤掉最早打开的(FIFO)。 */
|
|
1427
|
+
var FILES_MAX_OPEN = 5
|
|
1428
|
+
|
|
1429
|
+
/** path 是否落在 base 目录子树内(base 为根或空时视为全量放行)。 */
|
|
1430
|
+
function withinScope(path, base) {
|
|
1431
|
+
if (!base || base === '/') return true
|
|
1432
|
+
return path === base || path.indexOf(base + '/') === 0
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* 会话切换时按新 cwd 裁剪预览列表:范围外的 tab 自动关闭。
|
|
1437
|
+
* 若当前激活文件被裁掉,激活项顺移到剩余最后一个。
|
|
1438
|
+
* 列表无变化时返回原引用,避免触发多余渲染。
|
|
1439
|
+
*/
|
|
1440
|
+
function pruneFilesToScope(files, cwd) {
|
|
1441
|
+
var kept = files.list.filter(function (f) { return withinScope(f.path, cwd) })
|
|
1442
|
+
try { window.__DFV_PRUNE = (window.__DFV_PRUNE || []) ; window.__DFV_PRUNE.push({ cwd: cwd, before: files.list.length, after: kept.length }) } catch (_) {}
|
|
1443
|
+
if (kept.length === files.list.length) return files
|
|
1444
|
+
var activeKept = kept.some(function (f) { return f.path === files.active })
|
|
1445
|
+
var active = files.active
|
|
1446
|
+
if (!activeKept) active = kept.length ? kept[kept.length - 1].path : null
|
|
1447
|
+
return { list: kept, active: active }
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function setFiles(patch) {
|
|
1451
|
+
setState({ files: Object.assign({}, state.files, patch) })
|
|
1452
|
+
try {
|
|
1453
|
+
var f = state.files
|
|
1454
|
+
window.__DFV_DBG = { n: f.list.length, active: f.active, ops: ((window.__DFV_DBG && window.__DFV_DBG.ops) || 0) + 1 }
|
|
1455
|
+
} catch (_) {}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function findFileEntry(path) {
|
|
1459
|
+
for (var i = 0; i < state.files.list.length; i++) {
|
|
1460
|
+
if (state.files.list[i].path === path) return state.files.list[i]
|
|
1461
|
+
}
|
|
1462
|
+
return null
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/** 点击原生 header 的「预览」tab,把 view ring 切到预览视图。 */
|
|
1466
|
+
function activateFilesView() {
|
|
1467
|
+
try {
|
|
1468
|
+
var tabs = document.querySelectorAll('[role="tab"]')
|
|
1469
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
1470
|
+
if (tabs[i].textContent === FILES_TAB_LABEL()) { tabs[i].click(); return true }
|
|
1471
|
+
}
|
|
1472
|
+
} catch (err) {}
|
|
1473
|
+
return false
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
async function openFileInTab(path) {
|
|
1477
|
+
setState({ menu: null })
|
|
1478
|
+
var entry = findFileEntry(path)
|
|
1479
|
+
if (!entry) {
|
|
1480
|
+
entry = {
|
|
1481
|
+
path: path, name: baseName(path),
|
|
1482
|
+
loading: true, loaded: false, error: '',
|
|
1483
|
+
kind: null, content: '', dataUrl: '', data: '',
|
|
1484
|
+
truncated: false, size: 0,
|
|
1485
|
+
}
|
|
1486
|
+
// FIFO:最多同时 5 个预览,新开的挤掉最早打开的
|
|
1487
|
+
var list = state.files.list.slice()
|
|
1488
|
+
while (list.length >= FILES_MAX_OPEN) list.shift()
|
|
1489
|
+
list.push(entry)
|
|
1490
|
+
setFiles({ list: list, active: path })
|
|
1491
|
+
} else if (entry.loaded) {
|
|
1492
|
+
setFiles({ active: path })
|
|
1493
|
+
activateFilesView()
|
|
1494
|
+
return
|
|
1495
|
+
} else {
|
|
1496
|
+
setFiles({ active: path })
|
|
1497
|
+
}
|
|
1498
|
+
activateFilesView()
|
|
1499
|
+
var res = await rpc('read', { path: path, sessionId: activeSessionId })
|
|
1500
|
+
if (!res.ok && /超出允许范围/.test(res.error || '')) {
|
|
1501
|
+
await new Promise(function (r) { setTimeout(r, 600) })
|
|
1502
|
+
res = await rpc('read', { path: path, sessionId: activeSessionId })
|
|
1503
|
+
}
|
|
1504
|
+
var cur = findFileEntry(path)
|
|
1505
|
+
if (!cur) return
|
|
1506
|
+
cur.loading = false
|
|
1507
|
+
if (!res || !res.ok) {
|
|
1508
|
+
cur.error = (res && res.error) || T('explorer.readFail')
|
|
1509
|
+
} else {
|
|
1510
|
+
cur.error = ''
|
|
1511
|
+
cur.loaded = true
|
|
1512
|
+
cur.kind = res.kind
|
|
1513
|
+
cur.size = res.size || 0
|
|
1514
|
+
if (res.kind === 'text') { cur.content = res.content; cur.truncated = !!res.truncated }
|
|
1515
|
+
else if (res.kind === 'image') { cur.dataUrl = 'data:' + res.mime + ';base64,' + res.data }
|
|
1516
|
+
else if (res.kind === 'pdf') { cur.data = res.data }
|
|
1517
|
+
}
|
|
1518
|
+
setFiles({ list: state.files.list.slice(), active: path })
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
async function reloadFile(path) {
|
|
1522
|
+
var entry = findFileEntry(path)
|
|
1523
|
+
if (!entry) return
|
|
1524
|
+
// 复位 loaded,让 openFileInTab 走重读路径而不是「已打开直接激活」。
|
|
1525
|
+
entry.loaded = false
|
|
1526
|
+
await openFileInTab(path)
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function closeFileTab(path) {
|
|
1530
|
+
var list = state.files.list
|
|
1531
|
+
var idx = -1
|
|
1532
|
+
for (var i = 0; i < list.length; i++) if (list[i].path === path) { idx = i; break }
|
|
1533
|
+
if (idx < 0) return
|
|
1534
|
+
var next = list.slice(0, idx).concat(list.slice(idx + 1))
|
|
1535
|
+
var active = state.files.active
|
|
1536
|
+
if (active === path) {
|
|
1537
|
+
var neighbor = next[Math.min(idx, next.length - 1)]
|
|
1538
|
+
active = neighbor ? neighbor.path : null
|
|
1539
|
+
}
|
|
1540
|
+
setFiles({ list: next, active: active })
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function setActiveFile(path) {
|
|
1544
|
+
setFiles({ active: path })
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// ------------------------------------------------------------------
|
|
1548
|
+
// ▓▓ 区域二 · 会话内预览 · 🫚 姜(渲染器)
|
|
1549
|
+
// 按扩展名分派 md/html/pdf/notebook/json/csv + 图片/纯文本兜底 + FileContent + FilesView 子标签条。
|
|
1550
|
+
// ------------------------------------------------------------------
|
|
1551
|
+
// 文件视图(conversation.view 'dsh-soup-files'):
|
|
1552
|
+
// 子 tab 条 + 只读预览区(markdown/html/json/csv 渲染 / 图片预览 / 二进制提示)。
|
|
1553
|
+
// ------------------------------------------------------------------
|
|
1554
|
+
// ------------------------------------------------------------------
|
|
1555
|
+
// 预览渲染器:按扩展名分派(信任模型参考 JupyterLab viewer——
|
|
1556
|
+
// markdown 走不可信安全渲染管线,html 进无 allow-* 的沙箱 iframe)。
|
|
1557
|
+
// ------------------------------------------------------------------
|
|
1558
|
+
/** 取路径小写扩展名。 */
|
|
1559
|
+
function extOfName(p) {
|
|
1560
|
+
var m = /\.([A-Za-z0-9]+)$/.exec(String(p || ''))
|
|
1561
|
+
return m ? m[1].toLowerCase() : ''
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/** 预览渲染器类型:markdown | html | json | csv | tsv | notebook | code | text。 */
|
|
1565
|
+
function previewKind(path) {
|
|
1566
|
+
var ext = extOfName(path)
|
|
1567
|
+
if (ext === 'md' || ext === 'markdown') return 'markdown'
|
|
1568
|
+
if (ext === 'html' || ext === 'htm') return 'html'
|
|
1569
|
+
if (ext === 'json') return 'json'
|
|
1570
|
+
if (ext === 'ipynb') return 'notebook'
|
|
1571
|
+
if (ext === 'csv') return 'csv'
|
|
1572
|
+
if (ext === 'tsv') return 'tsv'
|
|
1573
|
+
return 'text'
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// 扩展名 → Shiki 语言提示。对齐 DSH 读卡片的 LANG_BY_EXTENSION
|
|
1577
|
+
// (packages/fs/tool-fs read-render.ts),json/md/markdown/html/htm 除外——
|
|
1578
|
+
// 它们走上方专属渲染器。
|
|
1579
|
+
var CODE_LANG_BY_EXT = {
|
|
1580
|
+
ts: 'ts', tsx: 'tsx', mts: 'ts', cts: 'ts',
|
|
1581
|
+
js: 'js', jsx: 'jsx', mjs: 'js', cjs: 'js',
|
|
1582
|
+
jsonc: 'json',
|
|
1583
|
+
py: 'py', rb: 'rb', go: 'go', rs: 'rs', java: 'java',
|
|
1584
|
+
c: 'c', h: 'c', cc: 'cpp', cpp: 'cpp', hpp: 'cpp', cxx: 'cpp',
|
|
1585
|
+
cs: 'cs', kt: 'kotlin', swift: 'swift', php: 'php',
|
|
1586
|
+
sh: 'sh', bash: 'sh', zsh: 'sh',
|
|
1587
|
+
yaml: 'yaml', yml: 'yaml', toml: 'toml', ini: 'ini',
|
|
1588
|
+
mdx: 'mdx', css: 'css', scss: 'scss', less: 'less',
|
|
1589
|
+
sql: 'sql', xml: 'xml', lua: 'lua',
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** 超过该字符数的代码文件不做高亮(Shiki 首次高亮的耗时保护)。 */
|
|
1593
|
+
var CODE_HIGHLIGHT_MAX_CHARS = 400000
|
|
1594
|
+
|
|
1595
|
+
/** RFC 4180 风格的分隔符解析(引号感知,"" 转义),返回行数组。 */
|
|
1596
|
+
function parseDelimited(text, delim) {
|
|
1597
|
+
var rows = []
|
|
1598
|
+
var row = []
|
|
1599
|
+
var field = ''
|
|
1600
|
+
var inQuotes = false
|
|
1601
|
+
var n = text.length
|
|
1602
|
+
for (var i = 0; i < n; i++) {
|
|
1603
|
+
var ch = text[i]
|
|
1604
|
+
if (inQuotes) {
|
|
1605
|
+
if (ch === '"') {
|
|
1606
|
+
if (text[i + 1] === '"') { field += '"'; i++ } else { inQuotes = false }
|
|
1607
|
+
} else {
|
|
1608
|
+
field += ch
|
|
1609
|
+
}
|
|
1610
|
+
} else if (ch === '"') {
|
|
1611
|
+
inQuotes = true
|
|
1612
|
+
} else if (ch === delim) {
|
|
1613
|
+
row.push(field); field = ''
|
|
1614
|
+
} else if (ch === '\n') {
|
|
1615
|
+
row.push(field); rows.push(row); row = []; field = ''
|
|
1616
|
+
} else if (ch !== '\r') {
|
|
1617
|
+
field += ch
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (field !== '' || row.length > 0) { row.push(field); rows.push(row) }
|
|
1621
|
+
return rows
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/** 把 pretty JSON 切成着色 token 数组(纯文本/React 节点,无 innerHTML)。 */
|
|
1377
1625
|
function jsonColorNodes(pretty) {
|
|
1378
1626
|
var out = []
|
|
1379
1627
|
var last = 0
|
|
@@ -1832,229 +2080,9 @@ window.__ModuleLoader__.load({
|
|
|
1832
2080
|
}
|
|
1833
2081
|
}
|
|
1834
2082
|
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
// 客户端会话快照是本会话 cwd 的可信来源(与官方 DetailsPanel 同源,
|
|
1839
|
-
// 始终是当前会话所在项目目录,而非宿主启动时的根目录)。
|
|
1840
|
-
var useSessions = props && props.useSessions
|
|
1841
|
-
var liveCwd = useSessions ? useSessions(function (list) {
|
|
1842
|
-
return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
|
|
1843
|
-
}) : undefined
|
|
1844
|
-
var cwdRef = React.useRef(liveCwd)
|
|
1845
|
-
cwdRef.current = liveCwd || cwdRef.current
|
|
1846
|
-
var panelRef = React.useRef(null)
|
|
1847
|
-
var frameRef = React.useRef(null)
|
|
1848
|
-
var widthRef = React.useRef(DETAILS_DEFAULT)
|
|
1849
|
-
var dragRef = React.useRef({ startX: 0, startW: 0, active: false })
|
|
1850
|
-
|
|
1851
|
-
React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
|
|
1852
|
-
|
|
1853
|
-
var setFrameWidth = React.useCallback(function (w) {
|
|
1854
|
-
var frame = frameRef.current
|
|
1855
|
-
if (!frame) return
|
|
1856
|
-
try {
|
|
1857
|
-
var gtc = frame.style.gridTemplateColumns
|
|
1858
|
-
if (!gtc) return
|
|
1859
|
-
var parts = gtc.trim().split(/\s+/)
|
|
1860
|
-
if (parts.length < 3) return
|
|
1861
|
-
parts[parts.length - 1] = Math.round(w) + 'px'
|
|
1862
|
-
frame.style.gridTemplateColumns = parts.join(' ')
|
|
1863
|
-
} catch (e) {}
|
|
1864
|
-
}, [])
|
|
1865
|
-
|
|
1866
|
-
React.useEffect(function () {
|
|
1867
|
-
var el = panelRef.current
|
|
1868
|
-
if (!el) return
|
|
1869
|
-
var mo = null
|
|
1870
|
-
var cancelled = false
|
|
1871
|
-
function attach() {
|
|
1872
|
-
var frame = findFrameEl(el)
|
|
1873
|
-
if (!frame || mo) return
|
|
1874
|
-
frameRef.current = frame
|
|
1875
|
-
function sync() {
|
|
1876
|
-
try {
|
|
1877
|
-
var gtc = frame.style.gridTemplateColumns
|
|
1878
|
-
if (!gtc) return
|
|
1879
|
-
var parts = gtc.trim().split(/\s+/)
|
|
1880
|
-
if (parts.length < 3) return
|
|
1881
|
-
var last = parseFloat(parts[parts.length - 1])
|
|
1882
|
-
if (isNaN(last) || last <= 1) { setState({ open: false }); return }
|
|
1883
|
-
setState({ open: true })
|
|
1884
|
-
if (Math.round(last) !== Math.round(widthRef.current)) {
|
|
1885
|
-
parts[parts.length - 1] = Math.round(widthRef.current) + 'px'
|
|
1886
|
-
frame.style.gridTemplateColumns = parts.join(' ')
|
|
1887
|
-
}
|
|
1888
|
-
} catch (e) {}
|
|
1889
|
-
}
|
|
1890
|
-
mo = new MutationObserver(sync)
|
|
1891
|
-
mo.observe(frame, { attributes: true, attributeFilter: ['style'] })
|
|
1892
|
-
sync()
|
|
1893
|
-
}
|
|
1894
|
-
attach()
|
|
1895
|
-
if (!mo) {
|
|
1896
|
-
var tries = 0
|
|
1897
|
-
var t = setInterval(function () {
|
|
1898
|
-
if (cancelled || mo) { clearInterval(t); return }
|
|
1899
|
-
attach()
|
|
1900
|
-
if (++tries > 40) clearInterval(t)
|
|
1901
|
-
}, 100)
|
|
1902
|
-
}
|
|
1903
|
-
return function () { cancelled = true; if (mo) mo.disconnect() }
|
|
1904
|
-
}, [])
|
|
1905
|
-
|
|
1906
|
-
function onResizeDown(e) {
|
|
1907
|
-
e.preventDefault()
|
|
1908
|
-
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) {}
|
|
1909
|
-
dragRef.current = { startX: e.clientX, startW: widthRef.current, active: true }
|
|
1910
|
-
if (frameRef.current) { try { frameRef.current.style.transition = 'none' } catch (e) {} }
|
|
1911
|
-
}
|
|
1912
|
-
function onResizeMove(e) {
|
|
1913
|
-
if (!dragRef.current.active) return
|
|
1914
|
-
var w = clampW(dragRef.current.startW - (e.clientX - dragRef.current.startX))
|
|
1915
|
-
widthRef.current = w
|
|
1916
|
-
setFrameWidth(w)
|
|
1917
|
-
}
|
|
1918
|
-
function onResizeUp(e) {
|
|
1919
|
-
dragRef.current.active = false
|
|
1920
|
-
try { e.currentTarget.releasePointerCapture(e.pointerId) } catch (err) {}
|
|
1921
|
-
if (frameRef.current) { try { frameRef.current.style.transition = '' } catch (e) {} }
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
visibleRows.length = 0
|
|
1925
|
-
|
|
1926
|
-
var bodyChildren = []
|
|
1927
|
-
if (state.newItem && state.newItem.parent === state.cwd) {
|
|
1928
|
-
bodyChildren.push(create('div', { key: '__new__', className: 'expl-row' },
|
|
1929
|
-
create('div', { className: 'expl-row-main', style: { paddingLeft: 8 } },
|
|
1930
|
-
create('span', { className: 'expl-icon' }, state.newItem.isDir ? '📁' : '📄'),
|
|
1931
|
-
create(NameInput, {
|
|
1932
|
-
initial: '',
|
|
1933
|
-
selectBase: false,
|
|
1934
|
-
onCommit: function (name) { commitNew(state.newItem.parent, name, state.newItem.isDir) },
|
|
1935
|
-
onCancel: function () { setState({ newItem: null }) },
|
|
1936
|
-
}),
|
|
1937
|
-
),
|
|
1938
|
-
))
|
|
1939
|
-
}
|
|
1940
|
-
if (s.tree.length === 0 && !(state.newItem && state.newItem.parent === state.cwd)) {
|
|
1941
|
-
bodyChildren.push(create('div', { key: '__empty__', className: 'expl-muted' }, T('explorer.emptyDir')))
|
|
1942
|
-
} else {
|
|
1943
|
-
s.tree.forEach(function (node) { bodyChildren.push(create(Row, { key: node.path, node: node, depth: 0 })) })
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
var container = create('div', {
|
|
1947
|
-
className: 'expl-body' + (state.dropTarget === state.cwd ? ' drop-target' : ''),
|
|
1948
|
-
onClick: onBlankClick,
|
|
1949
|
-
onContextMenu: onBlankContext,
|
|
1950
|
-
onDragOver: function (e) { e.preventDefault(); e.stopPropagation(); if (state.dropTarget !== state.cwd) setState({ dropTarget: state.cwd }) },
|
|
1951
|
-
onDragLeave: function (e) { if (state.dropTarget === state.cwd) setState({ dropTarget: null }) },
|
|
1952
|
-
onDrop: onBodyDrop,
|
|
1953
|
-
}, bodyChildren)
|
|
1954
|
-
|
|
1955
|
-
return create('div', { ref: panelRef, className: 'expl-panel' },
|
|
1956
|
-
create('div', { className: 'expl-resize', onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }),
|
|
1957
|
-
create('div', { className: 'expl-head' },
|
|
1958
|
-
create('span', { className: 'expl-title' }, T('explorer.title')),
|
|
1959
|
-
create('div', { className: 'expl-head-btns' },
|
|
1960
|
-
create('button', {
|
|
1961
|
-
className: 'expl-btn',
|
|
1962
|
-
onClick: function () { refreshAll() },
|
|
1963
|
-
title: T('explorer.refreshTitle'),
|
|
1964
|
-
'aria-label': T('explorer.refresh'),
|
|
1965
|
-
}, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } })),
|
|
1966
|
-
create('button', {
|
|
1967
|
-
className: 'expl-btn',
|
|
1968
|
-
onClick: function () { if (uploadInputEl) uploadInputEl.click() },
|
|
1969
|
-
title: T('explorer.upload'),
|
|
1970
|
-
'aria-label': T('explorer.upload'),
|
|
1971
|
-
}, '⬆'),
|
|
1972
|
-
create('button', {
|
|
1973
|
-
className: 'expl-btn',
|
|
1974
|
-
style: { fontSize: '18px' },
|
|
1975
|
-
onClick: function (event) {
|
|
1976
|
-
try { layout.closeDetails() } catch (e) {}
|
|
1977
|
-
setHeroDetailsWidth(event.currentTarget, 0)
|
|
1978
|
-
},
|
|
1979
|
-
title: T('explorer.close'),
|
|
1980
|
-
'aria-label': T('explorer.closePanel'),
|
|
1981
|
-
}, '✕'),
|
|
1982
|
-
),
|
|
1983
|
-
),
|
|
1984
|
-
create('div', { className: 'expl-path', title: s.cwd || '' }, s.cwd || '…'),
|
|
1985
|
-
(s.uploads && s.uploads.length) ? create('div', { className: 'expl-uploads' },
|
|
1986
|
-
s.uploads.map(function (u, i) {
|
|
1987
|
-
var pct = u.done ? null : (u.total ? Math.round((u.loaded / u.total) * 100) : 0)
|
|
1988
|
-
return create('div', { key: i, className: 'expl-upload-row' + (u.done ? ' expl-upload-done' : '') },
|
|
1989
|
-
create('span', { className: 'expl-upload-glyph' }, u.done ? '✓' : (u.down ? '⬇' : '⬆')),
|
|
1990
|
-
create('span', { className: 'expl-upload-text' },
|
|
1991
|
-
u.done ? u.name
|
|
1992
|
-
: (u.down ? T('explorer.downloading') : T('explorer.uploading')) + (u.totalFiles > 1 ? '(' + u.idx + '/' + u.totalFiles + ') ' : '') + u.name,
|
|
1993
|
-
),
|
|
1994
|
-
!u.done && u.total ? create('span', { className: 'expl-upload-pct' }, pct + '%') : null,
|
|
1995
|
-
)
|
|
1996
|
-
}),
|
|
1997
|
-
) : null,
|
|
1998
|
-
create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange: onUploadPicker }),
|
|
1999
|
-
s.error ? create('div', { className: 'expl-error' }, s.error) : null,
|
|
2000
|
-
s.notice ? create('div', { className: 'expl-notice' }, s.notice) : null,
|
|
2001
|
-
s.selected.size > 1
|
|
2002
|
-
? create('div', { className: 'expl-bulk' }, T('explorer.bulkSelected', { n: s.selected.size }))
|
|
2003
|
-
: null,
|
|
2004
|
-
container,
|
|
2005
|
-
s.menu ? create('div', { className: 'expl-menu-mask', onMouseDown: function () { setState({ menu: null }) }, onContextMenu: function (e) { e.preventDefault(); setState({ menu: null }) } }) : null,
|
|
2006
|
-
s.menu ? create(Menu, { menu: s.menu }) : null,
|
|
2007
|
-
)
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
function HeaderAction(props) {
|
|
2011
|
-
var s = useStore()
|
|
2012
|
-
var sessionId = props && props.sessionId
|
|
2013
|
-
var useSessions = props && props.useSessions
|
|
2014
|
-
var liveCwd = useSessions ? useSessions(function (list) {
|
|
2015
|
-
return (sessionId && list.byId[sessionId]) ? list.byId[sessionId].cwd : undefined
|
|
2016
|
-
}) : undefined
|
|
2017
|
-
var cwdRef = React.useRef(liveCwd)
|
|
2018
|
-
cwdRef.current = liveCwd || cwdRef.current
|
|
2019
|
-
React.useEffect(function () { trackSession(sessionId, cwdRef.current) }, [sessionId])
|
|
2020
|
-
return create('button', {
|
|
2021
|
-
type: 'button',
|
|
2022
|
-
className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
|
|
2023
|
-
onClick: function () { try { if (s.open) layout.closeDetails(); else layout.openDetails() } catch (e) {} },
|
|
2024
|
-
title: T('explorer.label'),
|
|
2025
|
-
'aria-label': T('explorer.label'),
|
|
2026
|
-
}, '📁')
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
function HeroAction(props) {
|
|
2030
|
-
var s = useStore()
|
|
2031
|
-
var session = props && props.session
|
|
2032
|
-
var useSessions = props && props.useSessions
|
|
2033
|
-
var liveCwd = useSessions ? useSessions(function (list) {
|
|
2034
|
-
return (props.sessionId && list.byId[props.sessionId]) ? list.byId[props.sessionId].cwd : undefined
|
|
2035
|
-
}) : undefined
|
|
2036
|
-
var blank = !!(session && session.blank && session.composerPhase === 'blank')
|
|
2037
|
-
React.useEffect(function () {
|
|
2038
|
-
if (blank) trackSession(props.sessionId, liveCwd)
|
|
2039
|
-
}, [blank, props.sessionId])
|
|
2040
|
-
if (!blank) return null
|
|
2041
|
-
return create('div', { className: 'expl-hero-dock' },
|
|
2042
|
-
create('button', {
|
|
2043
|
-
type: 'button',
|
|
2044
|
-
className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
|
|
2045
|
-
onClick: function (event) {
|
|
2046
|
-
if (s.open) {
|
|
2047
|
-
try { layout.closeDetails() } catch (e) {}
|
|
2048
|
-
setHeroDetailsWidth(event.currentTarget, 0)
|
|
2049
|
-
} else if (!setHeroDetailsWidth(event.currentTarget, DETAILS_DEFAULT)) {
|
|
2050
|
-
try { layout.openDetails() } catch (e) {}
|
|
2051
|
-
}
|
|
2052
|
-
},
|
|
2053
|
-
title: T('explorer.label'),
|
|
2054
|
-
'aria-label': T('explorer.label'),
|
|
2055
|
-
}, '📁'))
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2083
|
+
// ------------------------------------------------------------------
|
|
2084
|
+
// ▓▓ 区域三 · 速率徽标 · 🧂 盐
|
|
2085
|
+
// 输入框上方实时 t/s 吞吐徽标,数据来自 llm/stream 真实流;timerRef 为共享模块级计时引用。
|
|
2058
2086
|
// ------------------------------------------------------------------
|
|
2059
2087
|
// 速度徽标:DOM 注入到 Deep diving(role=status)旁。
|
|
2060
2088
|
// 不改 DSH 源码。用 MutationObserver 定位消息流里的 Deep diving,
|
|
@@ -2202,6 +2230,10 @@ window.__ModuleLoader__.load({
|
|
|
2202
2230
|
|
|
2203
2231
|
// ------------------------------------------------------------------
|
|
2204
2232
|
// ------------------------------------------------------------------
|
|
2233
|
+
// ------------------------------------------------------------------
|
|
2234
|
+
// ▓▓ 区域四 · 多行 GoalBar · 🧄 蒜
|
|
2235
|
+
// 复用原生 goal projection 与动作动词,多行完整展示 + textarea 编辑。
|
|
2236
|
+
// ------------------------------------------------------------------
|
|
2205
2237
|
// GoalBar 多行版:复用 DSH 原生 GoalBar 的 goal projection 与动作动词
|
|
2206
2238
|
// (onEdit/onPause/onResume/onClear),仅在展示上把单行截断放开为多行、
|
|
2207
2239
|
// 编辑用 textarea。通过 conversation.input.dock 同 id 'goal' + 更低 priority
|
|
@@ -2403,6 +2435,10 @@ window.__ModuleLoader__.load({
|
|
|
2403
2435
|
}
|
|
2404
2436
|
|
|
2405
2437
|
/** 应用浏览器半区:注册会话头部/空白会话切换按钮、details 右列面板、GoalBar、速度徽标。 */
|
|
2438
|
+
// ------------------------------------------------------------------
|
|
2439
|
+
// ▓▓ 区域五 · 应用注册(apply)
|
|
2440
|
+
// 把上述各区域组件注册进 slots / inject;导出插件浏览器半区入口 apply/inject。
|
|
2441
|
+
// ------------------------------------------------------------------
|
|
2406
2442
|
function apply(ctx) {
|
|
2407
2443
|
var slots = ctx.slots
|
|
2408
2444
|
layout = ctx.layout
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyhue1991/dsh-soup",
|
|
3
3
|
"description": "DSH 项目资源管理器:右侧 details 列文件树(并列网格、宽度与左侧工作区一致 264-420px),有 Session log 时在其右侧、空白新会话在输入框上方右端提供 📁 切换按钮;支持多选、双击展开/打开、右键菜单(打开/系统打开/重命名/废纸篓/新建/复制路径)、拖拽移动、上传。文件「打开」在会话区新增「文件」标签页(原生 conversation.view,与对话/轨迹同级):文本可编辑 ⌘S 保存、图片预览、二进制提示。附带模型吞吐徽标:输入框正上方显示「正在等待模型...」与彩色 t/s 徽标(数据来自 llm/stream 真实流)。附带 GoalBar 多行展示:把原生单行截断 GoalBar 放开为多行完整显示,编辑用 textarea,工具与数据仍走原生 goal。",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|