@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/lib/client.js +456 -420
  3. 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.0(当前)** —
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
- var FILES_TAB_LABEL = function () { return T('files.tab') }
1000
-
1001
- /** 当前活跃会话:list/read/download 携带给宿主,供围栏直查该会话 cwd。 */
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
- // 预览渲染器:按扩展名分派(信任模型参考 JupyterLab viewer——
1308
- // markdown 走不可信安全渲染管线,html 进无 allow-* 的沙箱 iframe)。
1185
+ // ▓▓ 区域一 · 资源管理器 · 🧅 葱(面板与入口)
1186
+ // Panel(右列 details 面板)/ HeaderAction(Session log 侧 📁)/ HeroAction(空白会话 📁)。
1309
1187
  // ------------------------------------------------------------------
1310
- /** 取路径小写扩展名。 */
1311
- function extOfName(p) {
1312
- var m = /\.([A-Za-z0-9]+)$/.exec(String(p || ''))
1313
- return m ? m[1].toLowerCase() : ''
1314
- }
1315
-
1316
- /** 预览渲染器类型:markdown | html | json | csv | tsv | notebook | code | text。 */
1317
- function previewKind(path) {
1318
- var ext = extOfName(path)
1319
- if (ext === 'md' || ext === 'markdown') return 'markdown'
1320
- if (ext === 'html' || ext === 'htm') return 'html'
1321
- if (ext === 'json') return 'json'
1322
- if (ext === 'ipynb') return 'notebook'
1323
- if (ext === 'csv') return 'csv'
1324
- if (ext === 'tsv') return 'tsv'
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
- // 扩展名 → Shiki 语言提示。对齐 DSH 读卡片的 LANG_BY_EXTENSION
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
- /** 超过该字符数的代码文件不做高亮(Shiki 首次高亮的耗时保护)。 */
1345
- var CODE_HIGHLIGHT_MAX_CHARS = 400000
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
- /** RFC 4180 风格的分隔符解析(引号感知,"" 转义),返回行数组。 */
1348
- function parseDelimited(text, delim) {
1349
- var rows = []
1350
- var row = []
1351
- var field = ''
1352
- var inQuotes = false
1353
- var n = text.length
1354
- for (var i = 0; i < n; i++) {
1355
- var ch = text[i]
1356
- if (inQuotes) {
1357
- if (ch === '"') {
1358
- if (text[i + 1] === '"') { field += '"'; i++ } else { inQuotes = false }
1359
- } else {
1360
- field += ch
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
- } else if (ch === '"') {
1363
- inQuotes = true
1364
- } else if (ch === delim) {
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
- if (field !== '' || row.length > 0) { row.push(field); rows.push(row) }
1373
- return rows
1374
- }
1375
-
1376
- /** 把 pretty JSON 切成着色 token 数组(纯文本/React 节点,无 innerHTML)。 */
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
- function Panel(props) {
1836
- var s = useStore()
1837
- var sessionId = props && props.sessionId
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.0",
4
+ "version": "0.4.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {