@lyhue1991/dsh-soup 0.6.0 → 0.7.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 CHANGED
@@ -24,7 +24,7 @@ DSH(DeepSeek Harness)功能很强:会话、工具、轨迹、Goal 一应
24
24
 
25
25
  ### 🧅 右侧文件树(主打 · 葱 · 提鲜点缀)
26
26
 
27
- 一个真正和左侧工作区并列的文件浏览器——不悬浮、不遮挡,配色与宽度都与左侧一致(默认 360px,拖左缘在 300–520px 间调整)。
27
+ 一个真正和左侧工作区并列的文件浏览器——不悬浮、不遮挡,配色与宽度都与左侧一致(默认 300px,拖左缘在 300–520px 间调整)。
28
28
 
29
29
  - **跟随会话**:自动展示当前会话的工作目录,切会话/工作区时跟着走。
30
30
  - **右键菜单**:预览、用系统打开、重命名、移到废纸篓、复制路径、下载;目录多出新建文件、新建文件夹、上传。
@@ -50,7 +50,7 @@ DSH(DeepSeek Harness)功能很强:会话、工具、轨迹、Goal 一应
50
50
  - **Notebook(.ipynb)**:GitHub 风格静态渲染(代码高亮 + 图片/输出内联)。
51
51
  - **图片**:png/jpg/gif/webp/svg 等内联展示(≤8MB)。
52
52
  - **其余**:二进制给出提示,超大文本截断为纯文本。
53
- - **文本编辑**:Markdown / Python / JavaScript / TypeScript / JSON / HTML / CSS / Shell 等常见文本文件可点「编辑」,支持 ⌘/Ctrl+S 立即保存、未保存标记与返回预览;Python / JSON 等纯代码文件在编辑态保留与预览一致的左侧行号与语法配色,Markdown 返回预览时展示当前草稿。
53
+ - **文本编辑**:Markdown / Python / JavaScript / TypeScript / JSON / HTML / CSS / Shell,以及 `.gitignore`、`Dockerfile`、`Makefile`、`LICENSE` 等常见文本文件可点「编辑」,支持 ⌘/Ctrl+S 立即保存、未保存标记与返回预览;Python / JSON 等纯代码文件在编辑态保留与预览一致的左侧行号与语法配色,Markdown 返回预览时展示当前草稿。
54
54
  - **自动保存**:参考 VS Code `files.autoSaveDelay`,停止输入 5 秒后自动保存;退出编辑时也会保存。`⌘S / Ctrl+S` 保存成功后工具栏会短暂显示“已保存”。编辑态默认覆盖磁盘上的其他更改,工具栏不显示保存/取消按钮。
55
55
  - **跟随会话**:切到别的项目时,范围外的预览自动关闭。
56
56
  - **自动重读**:正在看的文件被外部(agent / 编辑器)修改时,预览内容几秒内自动更新;若你正在编辑,则保留草稿并提示冲突。
@@ -20,7 +20,7 @@ export function createExplorerView(ctx) {
20
20
  var uploadTargetDir = null
21
21
  var DETAILS_MIN = 300
22
22
  var DETAILS_MAX = 520
23
- var DETAILS_DEFAULT = 360
23
+ var DETAILS_DEFAULT = 300
24
24
  function clampW(w) { return Math.max(DETAILS_MIN, Math.min(DETAILS_MAX, w)) }
25
25
 
26
26
  // Hook 规则安全兜底:宿主未注入 useSessions 时也必须调用恰好一个 hook,
@@ -0,0 +1,68 @@
1
+ import { fileAddressMatches } from './file-address.js'
2
+
3
+ /**
4
+ * Wrap the official Sidebar resource opener so file resources keep working
5
+ * when dsh-soup owns the right-column surface. The service is intentionally
6
+ * passed in by the caller: it can be provided after this plugin's apply()
7
+ * lifecycle has already started, and may be replaced on a host reconnect.
8
+ */
9
+ export function bridgeFileAddressOpener(service, ctx) {
10
+ if (!service || typeof service.openResource !== 'function') return null
11
+ if (service.__dshSoupFileBridge) return null
12
+
13
+ var originalOpenResource = service.openResource
14
+ var originalOpenResourceIn = service.openResourceIn
15
+ var openFileAddressInSoup = ctx.openFileAddressInSoup
16
+ var getActiveSessionId = ctx.getActiveSessionId
17
+
18
+ function openInSoup(address, sessionIdHint) {
19
+ if (!fileAddressMatches(address, sessionIdHint, getActiveSessionId())) return false
20
+ try {
21
+ var task = openFileAddressInSoup(address, sessionIdHint)
22
+ // Sidebar actions are synchronous. Keep asynchronous preview failures
23
+ // from becoming unhandled rejections in the host click handler.
24
+ if (task && typeof task.catch === 'function') task.catch(function () {})
25
+ return true
26
+ } catch (err) {
27
+ return false
28
+ }
29
+ }
30
+
31
+ var bridgedOpenResource = function (address, options) {
32
+ if (openInSoup(address, null)) return undefined
33
+ return originalOpenResource.call(this, address, options)
34
+ }
35
+ var bridgedOpenResourceIn = typeof originalOpenResourceIn === 'function'
36
+ ? function (sessionId, address, options) {
37
+ if (openInSoup(address, sessionId)) return undefined
38
+ return originalOpenResourceIn.call(this, sessionId, address, options)
39
+ }
40
+ : null
41
+
42
+ try {
43
+ service.openResource = bridgedOpenResource
44
+ if (bridgedOpenResourceIn) service.openResourceIn = bridgedOpenResourceIn
45
+ if (service.openResource !== bridgedOpenResource ||
46
+ (bridgedOpenResourceIn && service.openResourceIn !== bridgedOpenResourceIn)) return null
47
+ try { Object.defineProperty(service, '__dshSoupFileBridge', { value: true, configurable: true }) }
48
+ catch (err) { service.__dshSoupFileBridge = true }
49
+ } catch (err) {
50
+ try {
51
+ if (service.openResource === bridgedOpenResource) service.openResource = originalOpenResource
52
+ if (bridgedOpenResourceIn && service.openResourceIn === bridgedOpenResourceIn) {
53
+ service.openResourceIn = originalOpenResourceIn
54
+ }
55
+ } catch (restoreErr) {}
56
+ return null
57
+ }
58
+
59
+ return function () {
60
+ if (service.openResource === bridgedOpenResource) service.openResource = originalOpenResource
61
+ if (bridgedOpenResourceIn && service.openResourceIn === bridgedOpenResourceIn) {
62
+ service.openResourceIn = originalOpenResourceIn
63
+ }
64
+ try { delete service.__dshSoupFileBridge } catch (err) {}
65
+ }
66
+ }
67
+
68
+ if (typeof window !== 'undefined') window.__DSH_SOUP_FILE_ADDRESS_BRIDGE__ = { bridgeFileAddressOpener }
@@ -7,6 +7,13 @@
7
7
  const ASSET_MAX_BYTES = 4 * 1024 * 1024
8
8
  const TOTAL_MAX_BYTES = 32 * 1024 * 1024
9
9
  const MAX_ASSETS = 64
10
+ const IMAGE_MAX_COUNT = 64
11
+ /** Same whitelist as the host inline-image route (host/file-view.js IMAGE_MIME). */
12
+ const IMAGE_DATA_MIME = {
13
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
14
+ webp: 'image/webp', svg: 'image/svg+xml', bmp: 'image/bmp', ico: 'image/x-icon',
15
+ }
16
+ const CSS_URL_RE = /url\(\s*('([^']*)'|"([^"]*)"|([^)'"]*))\s*\)/gi
10
17
 
11
18
  function base64ToText(data) {
12
19
  const raw = atob(data)
@@ -27,6 +34,21 @@ function assetPath(reference) {
27
34
  return cut === -1 ? String(reference || '') : String(reference || '').slice(0, cut)
28
35
  }
29
36
 
37
+ function imageMimeOf(reference) {
38
+ const match = /\.([A-Za-z0-9]+)$/.exec(assetPath(reference))
39
+ return match ? IMAGE_DATA_MIME[match[1].toLowerCase()] : undefined
40
+ }
41
+
42
+ function relativeKey(reference) {
43
+ const out = []
44
+ for (const segment of String(reference || '').split('/')) {
45
+ if (segment === '' || segment === '.') continue
46
+ if (segment === '..') out.pop()
47
+ else out.push(segment)
48
+ }
49
+ return out.join('/')
50
+ }
51
+
30
52
  /** Preserve legacy WeChat document compatibility in every rendering mode. */
31
53
  export function prepareHtmlText(text) {
32
54
  let next = String(text || '')
@@ -100,7 +122,104 @@ export async function packHtmlPreview({ text, rootPath, sessionId, rpc, signal }
100
122
  assets.push({ kind, reference, text: assetText })
101
123
  } catch { /* malformed UTF-8 dependency is skipped */ }
102
124
  }
103
- return { html: source, assets }
125
+
126
+ // ------------------------------------------------------------------
127
+ // 相对路径图片内联为 data URL。srcdoc iframe 的文档 base URL 继承宿主
128
+ // 应用页面,`article_files/img.jpg` 之类的相对 <img> 会被解析到应用自身
129
+ // 路由而 404;且沙箱(无 allow-same-origin)发出的子资源请求 Origin 为
130
+ // opaque,无法走 /api/dsh-soup/img 的同源校验。因此与 script/css 同路:
131
+ // 经 read-related(限定文档目录内)读出后内联,彻底不依赖网络解析。
132
+ // ------------------------------------------------------------------
133
+ const imageCache = new Map()
134
+ function loadImage(reference) {
135
+ let bare = assetPath(reference)
136
+ try { bare = decodeURIComponent(bare) } catch { /* keep raw path */ }
137
+ if (!imageMimeOf(bare)) return Promise.resolve(null)
138
+ const key = relativeKey(bare)
139
+ let entry = imageCache.get(key)
140
+ if (!entry) {
141
+ if (imageCache.size >= IMAGE_MAX_COUNT) return Promise.resolve(null)
142
+ entry = Promise.resolve(rpc('read-related', { path: rootPath, relativePath: bare, sessionId }))
143
+ .then((response) => {
144
+ if (!response?.ok || response.size > ASSET_MAX_BYTES) return null
145
+ const bytes = Math.ceil(response.data.length * 3 / 4)
146
+ if (total + bytes > TOTAL_MAX_BYTES) return null
147
+ total += bytes
148
+ return { url: 'data:' + imageMimeOf(bare) + ';base64,' + response.data, bytes }
149
+ })
150
+ .catch(() => null)
151
+ imageCache.set(key, entry)
152
+ }
153
+ return entry
154
+ }
155
+
156
+ let mutated = false
157
+ await Promise.all(
158
+ Array.from(template.content.querySelectorAll('img[src],source[src],video[poster],input[type="image"][src]'))
159
+ .map(async (element) => {
160
+ const attribute = element.localName === 'video' ? 'poster' : 'src'
161
+ const reference = element.getAttribute(attribute) || ''
162
+ if (!relativeReference(reference) || !imageMimeOf(reference)) return
163
+ signal.throwIfAborted?.()
164
+ const image = await loadImage(reference)
165
+ if (image) { element.setAttribute(attribute, image.url); mutated = true }
166
+ }),
167
+ )
168
+
169
+ // 响应式图片:srcset 是「url 描述符, url 描述符」列表,逐候选改写。
170
+ for (const element of template.content.querySelectorAll('img[srcset],source[srcset]')) {
171
+ signal.throwIfAborted?.()
172
+ const value = element.getAttribute('srcset') || ''
173
+ if (!value) continue
174
+ const parts = await Promise.all(value.split(',').map(async (part) => {
175
+ const trimmed = part.trim()
176
+ if (!trimmed) return part
177
+ const cut = trimmed.search(/\s/)
178
+ const url = cut === -1 ? trimmed : trimmed.slice(0, cut)
179
+ const rest = cut === -1 ? '' : trimmed.slice(cut)
180
+ if (!relativeReference(url)) return part
181
+ const image = await loadImage(url)
182
+ return image ? image.url + rest : part
183
+ }))
184
+ const next = parts.join(',')
185
+ if (next !== value) { element.setAttribute('srcset', next); mutated = true }
186
+ }
187
+
188
+ // 内联 <style> 与 style 属性里的相对 url(...) 背景图(仅图片扩展名)。
189
+ async function rewriteCssUrls(text) {
190
+ if (!text || String(text).indexOf('url(') === -1) return text
191
+ const refs = new Set()
192
+ for (const match of String(text).matchAll(CSS_URL_RE)) {
193
+ const raw = match[2] !== undefined ? match[2] : match[3] !== undefined ? match[3] : match[4]
194
+ const reference = String(raw || '').trim()
195
+ if (reference && relativeReference(reference) && imageMimeOf(reference)) refs.add(reference)
196
+ }
197
+ if (!refs.size) return text
198
+ const resolved = new Map()
199
+ for (const reference of refs) {
200
+ const image = await loadImage(reference)
201
+ if (image) resolved.set(reference, image.url)
202
+ }
203
+ if (!resolved.size) return text
204
+ return String(text).replace(CSS_URL_RE, (match, quote, single, double, bare) => {
205
+ const url = resolved.get(String(single !== undefined ? single : double !== undefined ? double : bare).trim())
206
+ return url ? 'url(' + url + ')' : match
207
+ })
208
+ }
209
+ for (const element of template.content.querySelectorAll('style')) {
210
+ signal.throwIfAborted?.()
211
+ const next = await rewriteCssUrls(element.textContent)
212
+ if (next !== element.textContent) { element.textContent = next; mutated = true }
213
+ }
214
+ for (const element of template.content.querySelectorAll('[style]')) {
215
+ signal.throwIfAborted?.()
216
+ const next = await rewriteCssUrls(element.getAttribute('style'))
217
+ if (next !== element.getAttribute('style')) { element.setAttribute('style', next); mutated = true }
218
+ }
219
+
220
+ // 图片已直接写入模板;仅在发生内联时回传序列化结果,
221
+ // 纯外链文档保持原始 srcDoc 上下文不变。
222
+ return { html: mutated ? template.innerHTML : source, assets }
104
223
  }
105
224
 
106
225
  /** Build the React component while retaining DSH runtime dependencies at the caller boundary. */
@@ -8,7 +8,6 @@
8
8
  * 编辑动作)由 main 经工厂参数注入;纯辅助函数同时导出供 main 复用。
9
9
  */
10
10
 
11
- import { hostBase } from './rpc.js'
12
11
  import { CODE_LANG_BY_EXT } from './file-icons.js'
13
12
  import { ICON_REFRESH, FILE_ICON_EDIT, FILE_ICON_PREVIEW } from './styles.js'
14
13
 
@@ -30,6 +29,21 @@ export function previewKind(path) {
30
29
  return 'text'
31
30
  }
32
31
 
32
+ /** 常见无扩展名/点文件本身就是文本配置,应允许直接编辑。 */
33
+ var EDITABLE_FILE_NAMES = new Set([
34
+ '.gitignore', '.gitattributes', '.gitmodules', '.dockerignore',
35
+ '.editorconfig', '.npmrc', '.yarnrc', '.yarnrc.yml',
36
+ 'dockerfile', 'makefile', 'justfile', 'taskfile', 'procfile',
37
+ 'vagrantfile', 'gemfile', 'rakefile', 'brewfile',
38
+ 'license', 'licence', 'notice', 'authors', 'contributors',
39
+ ])
40
+
41
+ function baseNameOf(path) {
42
+ var value = String(path || '').replace(/[\\/]+$/, '')
43
+ var slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'))
44
+ return value.slice(slash + 1).toLowerCase()
45
+ }
46
+
33
47
  /** RFC 4180 风格的分隔符解析(引号感知,"" 转义),返回行数组。 */
34
48
  export function parseDelimited(text, delim) {
35
49
  var rows = []
@@ -73,6 +87,7 @@ export function isEditableTextFile(path) {
73
87
  var kind = previewKind(path)
74
88
  var ext = extOfName(path)
75
89
  return kind === 'markdown' || kind === 'html' || kind === 'json'
90
+ || EDITABLE_FILE_NAMES.has(baseNameOf(path))
76
91
  || Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, ext)
77
92
  }
78
93
 
@@ -264,13 +279,16 @@ export function createPreviewRenderers(ctx) {
264
279
  var CSV_MAX_COLS = 64
265
280
 
266
281
  // ------------------------------------------------------------------
267
- // Markdown 相对图片 → dsh-soup 同源 img 端点绝对 URL。
268
- // 预览 markdown 时,DSH 的 MarkdownText 渲染器「禁相对链接/相对资源」,
269
- // `![x](view.jpg)` 这类相对路径图片会被直接丢弃。因此把 src 改写成
270
- // 本插件同名端点(GET /api/dsh-soup/img)的同源绝对地址,交给 <img>
271
- // 从磁盘内联加载。绝对(http/https/data 等)与根相对路径放行不处理。
282
+ // Markdown 相对图片 → 官方 /api/file 文件路由词汇表(pathImages)。
283
+ // DSH 的 MarkdownText 渲染器只放行 http/https/mailto URL 与
284
+ // pathImages/fileImages 词汇表改写结果:桌面端 WebView 的页面基址是
285
+ // 自定义协议 dsh-app://app,把 src 改写成自家 img 端点会被
286
+ // sanitizeUrl 直接丢弃(alt 文本兜底);浏览器打开时才是 http。
287
+ // 因此改为向 MarkdownText 提供 pathImages 词汇表:相对路径以预览
288
+ // 文件所在目录为基解析成绝对路径,经官方 api/file 路由(浏览器为
289
+ // http 同源、桌面为 dsh-app://app/api/file,均在官方白名单内,
290
+ // 鉴权 cookie 由协议转发器注入)返回给 <img> 加载。
272
291
  // ------------------------------------------------------------------
273
- var MD_IMG_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g
274
292
  function posixResolve(dir, rel) {
275
293
  var combined = (String(dir || '/').replace(/\/+$/, '') || '/') + '/' + String(rel)
276
294
  var out = []
@@ -281,17 +299,24 @@ export function createPreviewRenderers(ctx) {
281
299
  })
282
300
  return '/' + out.join('/')
283
301
  }
284
- function imgUrl(absPath) {
285
- return hostBase() + '/api/dsh-soup/img?p=' + encodeURIComponent(absPath)
286
- }
287
- function absolutizeMarkdownImages(text, baseDir) {
288
- if (!text) return text
289
- var dir = baseDir || '/'
290
- return text.replace(MD_IMG_RE, function (m0, alt, src) {
291
- var s = String(src || '').trim()
292
- if (/^(?:[a-z][a-z0-9+.-]*:|#|\/)/i.test(s)) return m0
293
- return '![' + alt + '](' + imgUrl(posixResolve(dir, s)) + ')'
294
- })
302
+ /** pathImages.resolve:本地路径 → 官方 api/file URL;非本地路径返回 undefined。 */
303
+ function localPathImageUrl(baseDir, value) {
304
+ var raw = String(value || '')
305
+ // 与 DSH localPathMediaUrl 同规:剥离 ?# 后缀,URL 转义解码一次。
306
+ var bare = raw.split(/[?#]/)[0]
307
+ var decoded
308
+ try { decoded = decodeURIComponent(bare) } catch (e) { return undefined }
309
+ if (!decoded || /[\u0000-\u001f\u007f]/.test(decoded)) return undefined
310
+ // 协议 URL(http/https/data 等)与 UNC 路径不属于本地路径词汇表,
311
+ // 留给官方 imageSource 处理;Windows 盘符路径按官方例外放行。
312
+ if (/^[a-z][a-z\d+.-]*:/i.test(decoded) && !/^[a-z]:[\\/]/i.test(decoded)) return undefined
313
+ if (/^[/\\]{2}/.test(decoded)) return undefined
314
+ var abs = decoded.charAt(0) === '/' || /^[a-z]:[\\/]/i.test(decoded)
315
+ ? decoded
316
+ : posixResolve(baseDir || '/', decoded)
317
+ try {
318
+ return new URL('api/file?path=' + encodeURIComponent(abs), document.baseURI).href
319
+ } catch (e) { return undefined }
295
320
  }
296
321
 
297
322
  var MarkdownRenderBoundary = React.Component
@@ -403,7 +428,11 @@ export function createPreviewRenderers(ctx) {
403
428
  if (MarkdownText) {
404
429
  // Keep the original MarkdownText source contract visible for host integrations.
405
430
  // create(MarkdownText, { text: md })
406
- var md = props.baseDir ? absolutizeMarkdownImages(text, props.baseDir) : text
431
+ // 相对图片经 pathImages 词汇表交给官方 api/file 路由(见 localPathImageUrl 注释)。
432
+ var pathImages = props.baseDir
433
+ ? { resolve: function (value) { return localPathImageUrl(props.baseDir, value) } }
434
+ : undefined
435
+ var md = text
407
436
  var lines = String(md || '').split(/\r?\n/)
408
437
  var chunks = []
409
438
  var normal = []
@@ -411,7 +440,7 @@ export function createPreviewRenderers(ctx) {
411
440
  function pushNormal() {
412
441
  if (!normal.length) return
413
442
  var value = normal.join('\n')
414
- if (value.trim() !== '') chunks.push(create(MarkdownRenderBoundary, { key: 'md-' + chunks.length, text: value }, create(MarkdownText, { text: value })))
443
+ if (value.trim() !== '') chunks.push(create(MarkdownRenderBoundary, { key: 'md-' + chunks.length, text: value }, create(MarkdownText, { text: value, pathImages: pathImages })))
415
444
  normal = []
416
445
  }
417
446
  function pushTable(headerLine) {
@@ -421,7 +450,7 @@ export function createPreviewRenderers(ctx) {
421
450
  }
422
451
  var headers = splitRow(headerLine)
423
452
  var renderCell = function (cell, key) {
424
- return create(MarkdownRenderBoundary, { key: key, text: cell }, create(MarkdownText, { text: cell }))
453
+ return create(MarkdownRenderBoundary, { key: key, text: cell }, create(MarkdownText, { text: cell, pathImages: pathImages }))
425
454
  }
426
455
  var head = create('tr', null, headers.map(function (cell, hi) {
427
456
  return create('th', { key: hi, className: 'dfv-th' }, renderCell(cell, 'th-' + hi))
@@ -808,7 +837,7 @@ export function createPreviewRenderers(ctx) {
808
837
  return {
809
838
  codeMirrorLanguage: codeMirrorLanguage,
810
839
  jsonColorNodes: jsonColorNodes,
811
- absolutizeMarkdownImages: absolutizeMarkdownImages,
840
+ localPathImageUrl: localPathImageUrl,
812
841
  TextEditor: TextEditor,
813
842
  MarkdownPreview: MarkdownPreview,
814
843
  PdfPreview: PdfPreview,
@@ -1,4 +1,5 @@
1
1
  import './codemirror-modules.js'
2
2
  import './client/rpc.js'
3
3
  import './client/html-preview.js'
4
+ import './client/file-address-bridge.js'
4
5
  import './client.main.js'
package/lib/client.js CHANGED
@@ -30734,6 +30734,18 @@
30734
30734
  var ASSET_MAX_BYTES = 4 * 1024 * 1024;
30735
30735
  var TOTAL_MAX_BYTES = 32 * 1024 * 1024;
30736
30736
  var MAX_ASSETS = 64;
30737
+ var IMAGE_MAX_COUNT = 64;
30738
+ var IMAGE_DATA_MIME = {
30739
+ png: "image/png",
30740
+ jpg: "image/jpeg",
30741
+ jpeg: "image/jpeg",
30742
+ gif: "image/gif",
30743
+ webp: "image/webp",
30744
+ svg: "image/svg+xml",
30745
+ bmp: "image/bmp",
30746
+ ico: "image/x-icon"
30747
+ };
30748
+ var CSS_URL_RE = /url\(\s*('([^']*)'|"([^"]*)"|([^)'"]*))\s*\)/gi;
30737
30749
  function base64ToText(data2) {
30738
30750
  const raw = atob(data2);
30739
30751
  const bytes = new Uint8Array(raw.length);
@@ -30747,6 +30759,19 @@
30747
30759
  const cut = String(reference || "").search(/[?#]/);
30748
30760
  return cut === -1 ? String(reference || "") : String(reference || "").slice(0, cut);
30749
30761
  }
30762
+ function imageMimeOf(reference) {
30763
+ const match = /\.([A-Za-z0-9]+)$/.exec(assetPath(reference));
30764
+ return match ? IMAGE_DATA_MIME[match[1].toLowerCase()] : void 0;
30765
+ }
30766
+ function relativeKey(reference) {
30767
+ const out = [];
30768
+ for (const segment of String(reference || "").split("/")) {
30769
+ if (segment === "" || segment === ".") continue;
30770
+ if (segment === "..") out.pop();
30771
+ else out.push(segment);
30772
+ }
30773
+ return out.join("/");
30774
+ }
30750
30775
  function prepareHtmlText(text) {
30751
30776
  let next = String(text || "");
30752
30777
  if (next.includes("js_content") || next.includes("data-src=")) {
@@ -30814,7 +30839,100 @@
30814
30839
  } catch {
30815
30840
  }
30816
30841
  }
30817
- return { html: source, assets };
30842
+ const imageCache = /* @__PURE__ */ new Map();
30843
+ function loadImage(reference) {
30844
+ let bare = assetPath(reference);
30845
+ try {
30846
+ bare = decodeURIComponent(bare);
30847
+ } catch {
30848
+ }
30849
+ if (!imageMimeOf(bare)) return Promise.resolve(null);
30850
+ const key = relativeKey(bare);
30851
+ let entry = imageCache.get(key);
30852
+ if (!entry) {
30853
+ if (imageCache.size >= IMAGE_MAX_COUNT) return Promise.resolve(null);
30854
+ entry = Promise.resolve(rpc("read-related", { path: rootPath, relativePath: bare, sessionId })).then((response) => {
30855
+ if (!response?.ok || response.size > ASSET_MAX_BYTES) return null;
30856
+ const bytes = Math.ceil(response.data.length * 3 / 4);
30857
+ if (total + bytes > TOTAL_MAX_BYTES) return null;
30858
+ total += bytes;
30859
+ return { url: "data:" + imageMimeOf(bare) + ";base64," + response.data, bytes };
30860
+ }).catch(() => null);
30861
+ imageCache.set(key, entry);
30862
+ }
30863
+ return entry;
30864
+ }
30865
+ let mutated = false;
30866
+ await Promise.all(
30867
+ Array.from(template.content.querySelectorAll('img[src],source[src],video[poster],input[type="image"][src]')).map(async (element) => {
30868
+ const attribute = element.localName === "video" ? "poster" : "src";
30869
+ const reference = element.getAttribute(attribute) || "";
30870
+ if (!relativeReference(reference) || !imageMimeOf(reference)) return;
30871
+ signal.throwIfAborted?.();
30872
+ const image = await loadImage(reference);
30873
+ if (image) {
30874
+ element.setAttribute(attribute, image.url);
30875
+ mutated = true;
30876
+ }
30877
+ })
30878
+ );
30879
+ for (const element of template.content.querySelectorAll("img[srcset],source[srcset]")) {
30880
+ signal.throwIfAborted?.();
30881
+ const value = element.getAttribute("srcset") || "";
30882
+ if (!value) continue;
30883
+ const parts = await Promise.all(value.split(",").map(async (part) => {
30884
+ const trimmed = part.trim();
30885
+ if (!trimmed) return part;
30886
+ const cut = trimmed.search(/\s/);
30887
+ const url = cut === -1 ? trimmed : trimmed.slice(0, cut);
30888
+ const rest = cut === -1 ? "" : trimmed.slice(cut);
30889
+ if (!relativeReference(url)) return part;
30890
+ const image = await loadImage(url);
30891
+ return image ? image.url + rest : part;
30892
+ }));
30893
+ const next = parts.join(",");
30894
+ if (next !== value) {
30895
+ element.setAttribute("srcset", next);
30896
+ mutated = true;
30897
+ }
30898
+ }
30899
+ async function rewriteCssUrls(text2) {
30900
+ if (!text2 || String(text2).indexOf("url(") === -1) return text2;
30901
+ const refs = /* @__PURE__ */ new Set();
30902
+ for (const match of String(text2).matchAll(CSS_URL_RE)) {
30903
+ const raw = match[2] !== void 0 ? match[2] : match[3] !== void 0 ? match[3] : match[4];
30904
+ const reference = String(raw || "").trim();
30905
+ if (reference && relativeReference(reference) && imageMimeOf(reference)) refs.add(reference);
30906
+ }
30907
+ if (!refs.size) return text2;
30908
+ const resolved = /* @__PURE__ */ new Map();
30909
+ for (const reference of refs) {
30910
+ const image = await loadImage(reference);
30911
+ if (image) resolved.set(reference, image.url);
30912
+ }
30913
+ if (!resolved.size) return text2;
30914
+ return String(text2).replace(CSS_URL_RE, (match, quote, single, double, bare) => {
30915
+ const url = resolved.get(String(single !== void 0 ? single : double !== void 0 ? double : bare).trim());
30916
+ return url ? "url(" + url + ")" : match;
30917
+ });
30918
+ }
30919
+ for (const element of template.content.querySelectorAll("style")) {
30920
+ signal.throwIfAborted?.();
30921
+ const next = await rewriteCssUrls(element.textContent);
30922
+ if (next !== element.textContent) {
30923
+ element.textContent = next;
30924
+ mutated = true;
30925
+ }
30926
+ }
30927
+ for (const element of template.content.querySelectorAll("[style]")) {
30928
+ signal.throwIfAborted?.();
30929
+ const next = await rewriteCssUrls(element.getAttribute("style"));
30930
+ if (next !== element.getAttribute("style")) {
30931
+ element.setAttribute("style", next);
30932
+ mutated = true;
30933
+ }
30934
+ }
30935
+ return { html: mutated ? template.innerHTML : source, assets };
30818
30936
  }
30819
30937
  function createHtmlPreview({ React, create, rpc, translate }) {
30820
30938
  return function HtmlPreview(props) {
@@ -30845,6 +30963,110 @@
30845
30963
  window.__DSH_SOUP_HTML_PREVIEW__ = { createHtmlPreview };
30846
30964
  }
30847
30965
 
30966
+ // lib/client/file-address.js
30967
+ var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
30968
+ function decodeSegmentSafe(segment) {
30969
+ try {
30970
+ return decodeURIComponent(segment);
30971
+ } catch (e) {
30972
+ return null;
30973
+ }
30974
+ }
30975
+ function parseFileAddress(address) {
30976
+ if (typeof address !== "string" || address.indexOf(FILE_ADDRESS_PREFIX) !== 0) return null;
30977
+ var end = address.search(/[?#]/);
30978
+ var body = address.slice(FILE_ADDRESS_PREFIX.length, end === -1 ? void 0 : end);
30979
+ var parts = body.split("/");
30980
+ if (parts[0] === "session") {
30981
+ var sessionId = decodeSegmentSafe(parts[1] || "");
30982
+ if (!sessionId) return null;
30983
+ var segments = [];
30984
+ for (var i = 2; i < parts.length; i++) {
30985
+ var seg = decodeSegmentSafe(parts[i]);
30986
+ if (seg === null) return null;
30987
+ segments.push(seg);
30988
+ }
30989
+ if (!segments.length) return null;
30990
+ return { scope: "session", sessionId, path: segments.join("/") };
30991
+ }
30992
+ if (parts[0] === "absolute") {
30993
+ var absolute = [];
30994
+ for (var j = 1; j < parts.length; j++) {
30995
+ var abs = decodeSegmentSafe(parts[j]);
30996
+ if (abs === null) return null;
30997
+ absolute.push(abs);
30998
+ }
30999
+ if (!absolute.length) return null;
31000
+ return { scope: "absolute", path: "/" + absolute.join("/") };
31001
+ }
31002
+ return null;
31003
+ }
31004
+ function fileAddressMatches(address, sessionIdHint, activeSessionId) {
31005
+ var parsed = parseFileAddress(address);
31006
+ if (!parsed) return false;
31007
+ if (parsed.scope === "session" && (sessionIdHint || parsed.sessionId) !== activeSessionId) return false;
31008
+ return true;
31009
+ }
31010
+
31011
+ // lib/client/file-address-bridge.js
31012
+ function bridgeFileAddressOpener(service, ctx) {
31013
+ if (!service || typeof service.openResource !== "function") return null;
31014
+ if (service.__dshSoupFileBridge) return null;
31015
+ var originalOpenResource = service.openResource;
31016
+ var originalOpenResourceIn = service.openResourceIn;
31017
+ var openFileAddressInSoup = ctx.openFileAddressInSoup;
31018
+ var getActiveSessionId = ctx.getActiveSessionId;
31019
+ function openInSoup(address, sessionIdHint) {
31020
+ if (!fileAddressMatches(address, sessionIdHint, getActiveSessionId())) return false;
31021
+ try {
31022
+ var task = openFileAddressInSoup(address, sessionIdHint);
31023
+ if (task && typeof task.catch === "function") task.catch(function() {
31024
+ });
31025
+ return true;
31026
+ } catch (err) {
31027
+ return false;
31028
+ }
31029
+ }
31030
+ var bridgedOpenResource = function(address, options) {
31031
+ if (openInSoup(address, null)) return void 0;
31032
+ return originalOpenResource.call(this, address, options);
31033
+ };
31034
+ var bridgedOpenResourceIn = typeof originalOpenResourceIn === "function" ? function(sessionId, address, options) {
31035
+ if (openInSoup(address, sessionId)) return void 0;
31036
+ return originalOpenResourceIn.call(this, sessionId, address, options);
31037
+ } : null;
31038
+ try {
31039
+ service.openResource = bridgedOpenResource;
31040
+ if (bridgedOpenResourceIn) service.openResourceIn = bridgedOpenResourceIn;
31041
+ if (service.openResource !== bridgedOpenResource || bridgedOpenResourceIn && service.openResourceIn !== bridgedOpenResourceIn) return null;
31042
+ try {
31043
+ Object.defineProperty(service, "__dshSoupFileBridge", { value: true, configurable: true });
31044
+ } catch (err) {
31045
+ service.__dshSoupFileBridge = true;
31046
+ }
31047
+ } catch (err) {
31048
+ try {
31049
+ if (service.openResource === bridgedOpenResource) service.openResource = originalOpenResource;
31050
+ if (bridgedOpenResourceIn && service.openResourceIn === bridgedOpenResourceIn) {
31051
+ service.openResourceIn = originalOpenResourceIn;
31052
+ }
31053
+ } catch (restoreErr) {
31054
+ }
31055
+ return null;
31056
+ }
31057
+ return function() {
31058
+ if (service.openResource === bridgedOpenResource) service.openResource = originalOpenResource;
31059
+ if (bridgedOpenResourceIn && service.openResourceIn === bridgedOpenResourceIn) {
31060
+ service.openResourceIn = originalOpenResourceIn;
31061
+ }
31062
+ try {
31063
+ delete service.__dshSoupFileBridge;
31064
+ } catch (err) {
31065
+ }
31066
+ };
31067
+ }
31068
+ if (typeof window !== "undefined") window.__DSH_SOUP_FILE_ADDRESS_BRIDGE__ = { bridgeFileAddressOpener };
31069
+
30848
31070
  // lib/client/styles.js
30849
31071
  var ICON_REFRESH = '<svg xmlns="http://www.w3.org/2000/svg" width="16" viewBox="0 0 18 18"><path fill="currentColor" d="M9 13.5c-2.49 0-4.5-2.01-4.5-4.5S6.51 4.5 9 4.5c1.24 0 2.36.52 3.17 1.33L10 8h5V3l-1.76 1.76A6 6 0 0 0 9 3C5.69 3 3.01 5.69 3.01 9S5.69 15 9 15a5.98 5.98 0 0 0 5.9-5h-1.52c-.46 2-2.24 3.5-4.38 3.5"/></svg>';
30850
31072
  var ICON_NEW_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M20 6h-8l-2-2H4c-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-2zm-2 7h-3v3h-2v-3h-3v-2h3V8h2v3h3v2z"/></svg>';
@@ -31370,51 +31592,6 @@
31370
31592
  }
31371
31593
  var setiTag;
31372
31594
 
31373
- // lib/client/file-address.js
31374
- var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
31375
- function decodeSegmentSafe(segment) {
31376
- try {
31377
- return decodeURIComponent(segment);
31378
- } catch (e) {
31379
- return null;
31380
- }
31381
- }
31382
- function parseFileAddress(address) {
31383
- if (typeof address !== "string" || address.indexOf(FILE_ADDRESS_PREFIX) !== 0) return null;
31384
- var end = address.search(/[?#]/);
31385
- var body = address.slice(FILE_ADDRESS_PREFIX.length, end === -1 ? void 0 : end);
31386
- var parts = body.split("/");
31387
- if (parts[0] === "session") {
31388
- var sessionId = decodeSegmentSafe(parts[1] || "");
31389
- if (!sessionId) return null;
31390
- var segments = [];
31391
- for (var i = 2; i < parts.length; i++) {
31392
- var seg = decodeSegmentSafe(parts[i]);
31393
- if (seg === null) return null;
31394
- segments.push(seg);
31395
- }
31396
- if (!segments.length) return null;
31397
- return { scope: "session", sessionId, path: segments.join("/") };
31398
- }
31399
- if (parts[0] === "absolute") {
31400
- var absolute = [];
31401
- for (var j = 1; j < parts.length; j++) {
31402
- var abs = decodeSegmentSafe(parts[j]);
31403
- if (abs === null) return null;
31404
- absolute.push(abs);
31405
- }
31406
- if (!absolute.length) return null;
31407
- return { scope: "absolute", path: "/" + absolute.join("/") };
31408
- }
31409
- return null;
31410
- }
31411
- function fileAddressMatches(address, sessionIdHint, activeSessionId) {
31412
- var parsed = parseFileAddress(address);
31413
- if (!parsed) return false;
31414
- if (parsed.scope === "session" && (sessionIdHint || parsed.sessionId) !== activeSessionId) return false;
31415
- return true;
31416
- }
31417
-
31418
31595
  // lib/client/preview-renderers.js
31419
31596
  function extOfName2(p) {
31420
31597
  var m = /\.([A-Za-z0-9]+)$/.exec(String(p || ""));
@@ -31430,6 +31607,35 @@
31430
31607
  if (ext === "tsv") return "tsv";
31431
31608
  return "text";
31432
31609
  }
31610
+ var EDITABLE_FILE_NAMES = /* @__PURE__ */ new Set([
31611
+ ".gitignore",
31612
+ ".gitattributes",
31613
+ ".gitmodules",
31614
+ ".dockerignore",
31615
+ ".editorconfig",
31616
+ ".npmrc",
31617
+ ".yarnrc",
31618
+ ".yarnrc.yml",
31619
+ "dockerfile",
31620
+ "makefile",
31621
+ "justfile",
31622
+ "taskfile",
31623
+ "procfile",
31624
+ "vagrantfile",
31625
+ "gemfile",
31626
+ "rakefile",
31627
+ "brewfile",
31628
+ "license",
31629
+ "licence",
31630
+ "notice",
31631
+ "authors",
31632
+ "contributors"
31633
+ ]);
31634
+ function baseNameOf(path) {
31635
+ var value = String(path || "").replace(/[\\/]+$/, "");
31636
+ var slash3 = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"));
31637
+ return value.slice(slash3 + 1).toLowerCase();
31638
+ }
31433
31639
  function parseDelimited(text, delim) {
31434
31640
  var rows = [];
31435
31641
  var row = [];
@@ -31479,7 +31685,7 @@
31479
31685
  function isEditableTextFile(path) {
31480
31686
  var kind = previewKind(path);
31481
31687
  var ext = extOfName2(path);
31482
- return kind === "markdown" || kind === "html" || kind === "json" || Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, ext);
31688
+ return kind === "markdown" || kind === "html" || kind === "json" || EDITABLE_FILE_NAMES.has(baseNameOf(path)) || Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, ext);
31483
31689
  }
31484
31690
  function createPreviewRenderers(ctx) {
31485
31691
  var React = ctx.React;
@@ -31639,7 +31845,6 @@
31639
31845
  }
31640
31846
  var CSV_MAX_ROWS = 500;
31641
31847
  var CSV_MAX_COLS = 64;
31642
- var MD_IMG_RE = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
31643
31848
  function posixResolve(dir, rel) {
31644
31849
  var combined = (String(dir || "/").replace(/\/+$/, "") || "/") + "/" + String(rel);
31645
31850
  var out = [];
@@ -31653,17 +31858,24 @@
31653
31858
  });
31654
31859
  return "/" + out.join("/");
31655
31860
  }
31656
- function imgUrl(absPath) {
31657
- return hostBase() + "/api/dsh-soup/img?p=" + encodeURIComponent(absPath);
31658
- }
31659
- function absolutizeMarkdownImages(text, baseDir) {
31660
- if (!text) return text;
31661
- var dir = baseDir || "/";
31662
- return text.replace(MD_IMG_RE, function(m0, alt, src) {
31663
- var s = String(src || "").trim();
31664
- if (/^(?:[a-z][a-z0-9+.-]*:|#|\/)/i.test(s)) return m0;
31665
- return "![" + alt + "](" + imgUrl(posixResolve(dir, s)) + ")";
31666
- });
31861
+ function localPathImageUrl(baseDir, value) {
31862
+ var raw = String(value || "");
31863
+ var bare = raw.split(/[?#]/)[0];
31864
+ var decoded;
31865
+ try {
31866
+ decoded = decodeURIComponent(bare);
31867
+ } catch (e) {
31868
+ return void 0;
31869
+ }
31870
+ if (!decoded || /[\u0000-\u001f\u007f]/.test(decoded)) return void 0;
31871
+ if (/^[a-z][a-z\d+.-]*:/i.test(decoded) && !/^[a-z]:[\\/]/i.test(decoded)) return void 0;
31872
+ if (/^[/\\]{2}/.test(decoded)) return void 0;
31873
+ var abs = decoded.charAt(0) === "/" || /^[a-z]:[\\/]/i.test(decoded) ? decoded : posixResolve(baseDir || "/", decoded);
31874
+ try {
31875
+ return new URL("api/file?path=" + encodeURIComponent(abs), document.baseURI).href;
31876
+ } catch (e) {
31877
+ return void 0;
31878
+ }
31667
31879
  }
31668
31880
  var MarkdownRenderBoundary = React.Component ? class MarkdownRenderBoundary extends React.Component {
31669
31881
  constructor(props) {
@@ -31785,7 +31997,7 @@
31785
31997
  let pushNormal = function() {
31786
31998
  if (!normal.length) return;
31787
31999
  var value = normal.join("\n");
31788
- if (value.trim() !== "") chunks.push(create(MarkdownRenderBoundary, { key: "md-" + chunks.length, text: value }, create(MarkdownText, { text: value })));
32000
+ if (value.trim() !== "") chunks.push(create(MarkdownRenderBoundary, { key: "md-" + chunks.length, text: value }, create(MarkdownText, { text: value, pathImages })));
31789
32001
  normal = [];
31790
32002
  }, pushTable = function(headerLine) {
31791
32003
  var splitRow = function(value) {
@@ -31796,7 +32008,7 @@
31796
32008
  };
31797
32009
  var headers = splitRow(headerLine);
31798
32010
  var renderCell = function(cell, key) {
31799
- return create(MarkdownRenderBoundary, { key, text: cell }, create(MarkdownText, { text: cell }));
32011
+ return create(MarkdownRenderBoundary, { key, text: cell }, create(MarkdownText, { text: cell, pathImages }));
31800
32012
  };
31801
32013
  var head = create("tr", null, headers.map(function(cell, hi) {
31802
32014
  return create("th", { key: hi, className: "dfv-th" }, renderCell(cell, "th-" + hi));
@@ -31824,7 +32036,10 @@
31824
32036
  )
31825
32037
  ));
31826
32038
  };
31827
- var md = props.baseDir ? absolutizeMarkdownImages(text, props.baseDir) : text;
32039
+ var pathImages = props.baseDir ? { resolve: function(value) {
32040
+ return localPathImageUrl(props.baseDir, value);
32041
+ } } : void 0;
32042
+ var md = text;
31828
32043
  var lines = String(md || "").split(/\r?\n/);
31829
32044
  var chunks = [];
31830
32045
  var normal = [];
@@ -32234,7 +32449,7 @@
32234
32449
  return {
32235
32450
  codeMirrorLanguage,
32236
32451
  jsonColorNodes,
32237
- absolutizeMarkdownImages,
32452
+ localPathImageUrl,
32238
32453
  TextEditor,
32239
32454
  MarkdownPreview,
32240
32455
  PdfPreview,
@@ -33807,7 +34022,7 @@
33807
34022
  var uploadTargetDir = null;
33808
34023
  var DETAILS_MIN = 300;
33809
34024
  var DETAILS_MAX = 520;
33810
- var DETAILS_DEFAULT = 360;
34025
+ var DETAILS_DEFAULT = 300;
33811
34026
  function clampW(w) {
33812
34027
  return Math.max(DETAILS_MIN, Math.min(DETAILS_MAX, w));
33813
34028
  }
@@ -34853,7 +35068,8 @@
34853
35068
  document.removeEventListener("click", onOfficialExpand, true);
34854
35069
  });
34855
35070
  }
34856
- var bridgePatched = false;
35071
+ var bridgeDisposers = [];
35072
+ var bridgedServices = typeof WeakSet === "function" ? /* @__PURE__ */ new WeakSet() : [];
34857
35073
  function resolveSidebarRight() {
34858
35074
  try {
34859
35075
  if (ctx.sidebarRight) return ctx.sidebarRight;
@@ -34875,42 +35091,43 @@
34875
35091
  }
34876
35092
  return null;
34877
35093
  }
34878
- function patchSidebarRightBridge() {
34879
- if (bridgePatched) return true;
34880
- var service = resolveSidebarRight();
34881
- if (!service || typeof service.openResource !== "function") return false;
34882
- if (!service.__dshSoupFileBridge) {
34883
- var origResource = service.openResource;
34884
- var origResourceIn = service.openResourceIn;
34885
- service.openResource = function(address, options) {
34886
- if (fileAddressMatches(address, null, activeSessionId)) {
34887
- openFileAddressInSoup(address, null);
34888
- return;
34889
- }
34890
- return origResource.call(service, address, options);
34891
- };
34892
- if (typeof origResourceIn === "function") {
34893
- service.openResourceIn = function(sessionId, address, options) {
34894
- if (fileAddressMatches(address, sessionId, activeSessionId)) {
34895
- openFileAddressInSoup(address, sessionId);
34896
- return;
34897
- }
34898
- return origResourceIn.call(service, sessionId, address, options);
34899
- };
34900
- }
34901
- try {
34902
- Object.defineProperty(service, "__dshSoupFileBridge", { value: true });
34903
- } catch (e) {
34904
- service.__dshSoupFileBridge = true;
34905
- }
35094
+ function hasBridgedService(service) {
35095
+ return bridgedServices instanceof WeakSet ? bridgedServices.has(service) : bridgedServices.indexOf(service) !== -1;
35096
+ }
35097
+ function rememberBridgedService(service) {
35098
+ if (bridgedServices instanceof WeakSet) bridgedServices.add(service);
35099
+ else bridgedServices.push(service);
35100
+ }
35101
+ function patchSidebarRightBridge(service) {
35102
+ service = service || resolveSidebarRight();
35103
+ if (!service || hasBridgedService(service)) return !!service;
35104
+ if (service.__dshSoupFileBridge) {
35105
+ rememberBridgedService(service);
35106
+ return true;
34906
35107
  }
34907
- bridgePatched = true;
35108
+ var disposeBridge = bridgeFileAddressOpener(service, {
35109
+ openFileAddressInSoup,
35110
+ getActiveSessionId: function() {
35111
+ return activeSessionId;
35112
+ }
35113
+ });
35114
+ if (!disposeBridge) return false;
35115
+ rememberBridgedService(service);
35116
+ bridgeDisposers.push(disposeBridge);
34908
35117
  try {
34909
35118
  if (typeof window !== "undefined") window.__DSH_SOUP_FILE_BRIDGE = true;
34910
35119
  } catch (e) {
34911
35120
  }
34912
35121
  return true;
34913
35122
  }
35123
+ if (typeof ctx.inject === "function") {
35124
+ try {
35125
+ ctx.inject(["sidebarRight"], function(scope) {
35126
+ patchSidebarRightBridge(scope && scope.sidebarRight);
35127
+ });
35128
+ } catch (e) {
35129
+ }
35130
+ }
34914
35131
  patchSidebarRightBridge();
34915
35132
  var bridgeTries = 0;
34916
35133
  var bridgeTimer = setInterval(function() {
@@ -34920,6 +35137,14 @@
34920
35137
  disposers.push(function() {
34921
35138
  clearInterval(bridgeTimer);
34922
35139
  });
35140
+ disposers.push(function() {
35141
+ bridgeDisposers.forEach(function(disposeBridge) {
35142
+ try {
35143
+ disposeBridge();
35144
+ } catch (e) {
35145
+ }
35146
+ });
35147
+ });
34923
35148
  if (ctx.locale && typeof ctx.locale.register === "function") {
34924
35149
  ctx.effect(function() {
34925
35150
  return ctx.locale.register(NS, DICT);
@@ -3,7 +3,8 @@ import { NS, DICT } from './client/i18n.js'
3
3
  import { iconSvgFor } from './client/file-icons.js'
4
4
  import { createRpc, hostBase } from './client/rpc.js'
5
5
  import { createHtmlPreview } from './client/html-preview.js'
6
- import { parseFileAddress, fileAddressMatches } from './client/file-address.js'
6
+ import { parseFileAddress } from './client/file-address.js'
7
+ import { bridgeFileAddressOpener } from './client/file-address-bridge.js'
7
8
  import { createPreviewRenderers, isEditableTextFile } from './client/preview-renderers.js'
8
9
  import { createSpeedBadge } from './client/speed-badge.js'
9
10
  import { createGoalBar } from './client/goal-bar.js'
@@ -17,7 +18,7 @@ import { createExplorerView } from './client/explorer-view.js'
17
18
  * 布局:注册进 shell 原生的 `details` 右列(并列网格,非悬浮),
18
19
  * 有 Session log 时在它右侧放 📁;空白新会话则在输入框上方、
19
20
  * workspace/preset 控件行的右端提供同一入口。
20
- * 宽度对齐宿主 details 合同:默认 360px、可拖 300–520px(直接改写 shell 网格
21
+ * 宽度对齐宿主 details 合同:默认 300px、可拖 300–520px(直接改写 shell 网格
21
22
  * 的 details 轨,并用 MutationObserver 在 shell 重渲染后维持宽度)。
22
23
  *
23
24
  * 与宿主通信:走同源 HTTP 路由 `/api/dsh-soup`(POST JSON),
@@ -385,10 +386,11 @@ window.__ModuleLoader__.load({
385
386
  document.addEventListener('click', onOfficialExpand, true)
386
387
  disposers.push(function () { document.removeEventListener('click', onOfficialExpand, true) })
387
388
  }
388
- // 官方产物链接桥接:包装 sidebarRight 服务(见 parseFileAddress 处注释)。
389
- // 优先走可选注入的 ctx.sidebarRight(框架保证就绪),并保留
390
- // ctx.get / ctx.reflect.get 多条解析路径与短轮询兜底。
391
- var bridgePatched = false
389
+ // 官方产物链接桥接:产物卡片并不把 file 地址放进 DOM,而是直接调
390
+ // sidebarRight.openResource()。rightbar 槽位被替换后仍需接住该调用。
391
+ // 首选 ctx.inject 订阅服务就绪/重建;反射轮询仅保留给旧版宿主兼容。
392
+ var bridgeDisposers = []
393
+ var bridgedServices = typeof WeakSet === 'function' ? new WeakSet() : []
392
394
  function resolveSidebarRight() {
393
395
  try { if (ctx.sidebarRight) return ctx.sidebarRight } catch (e) {}
394
396
  try {
@@ -405,30 +407,37 @@ window.__ModuleLoader__.load({
405
407
  } catch (e) {}
406
408
  return null
407
409
  }
408
- function patchSidebarRightBridge() {
409
- if (bridgePatched) return true
410
- var service = resolveSidebarRight()
411
- if (!service || typeof service.openResource !== 'function') return false
412
- if (!service.__dshSoupFileBridge) {
413
- var origResource = service.openResource
414
- var origResourceIn = service.openResourceIn
415
- service.openResource = function (address, options) {
416
- if (fileAddressMatches(address, null, activeSessionId)) { openFileAddressInSoup(address, null); return }
417
- return origResource.call(service, address, options)
418
- }
419
- if (typeof origResourceIn === 'function') {
420
- service.openResourceIn = function (sessionId, address, options) {
421
- if (fileAddressMatches(address, sessionId, activeSessionId)) { openFileAddressInSoup(address, sessionId); return }
422
- return origResourceIn.call(service, sessionId, address, options)
423
- }
424
- }
425
- try { Object.defineProperty(service, '__dshSoupFileBridge', { value: true }) }
426
- catch (e) { service.__dshSoupFileBridge = true }
410
+ function hasBridgedService(service) {
411
+ return bridgedServices instanceof WeakSet ? bridgedServices.has(service) : bridgedServices.indexOf(service) !== -1
412
+ }
413
+ function rememberBridgedService(service) {
414
+ if (bridgedServices instanceof WeakSet) bridgedServices.add(service)
415
+ else bridgedServices.push(service)
416
+ }
417
+ function patchSidebarRightBridge(service) {
418
+ service = service || resolveSidebarRight()
419
+ if (!service || hasBridgedService(service)) return !!service
420
+ if (service.__dshSoupFileBridge) {
421
+ rememberBridgedService(service)
422
+ return true
427
423
  }
428
- bridgePatched = true
424
+ var disposeBridge = bridgeFileAddressOpener(service, {
425
+ openFileAddressInSoup: openFileAddressInSoup,
426
+ getActiveSessionId: function () { return activeSessionId },
427
+ })
428
+ if (!disposeBridge) return false
429
+ rememberBridgedService(service)
430
+ bridgeDisposers.push(disposeBridge)
429
431
  try { if (typeof window !== 'undefined') window.__DSH_SOUP_FILE_BRIDGE = true } catch (e) {}
430
432
  return true
431
433
  }
434
+ if (typeof ctx.inject === 'function') {
435
+ try {
436
+ ctx.inject(['sidebarRight'], function (scope) {
437
+ patchSidebarRightBridge(scope && scope.sidebarRight)
438
+ })
439
+ } catch (e) {}
440
+ }
432
441
  patchSidebarRightBridge()
433
442
  var bridgeTries = 0
434
443
  var bridgeTimer = setInterval(function () {
@@ -436,6 +445,9 @@ window.__ModuleLoader__.load({
436
445
  if (patchSidebarRightBridge() || bridgeTries >= 10) clearInterval(bridgeTimer)
437
446
  }, 1000)
438
447
  disposers.push(function () { clearInterval(bridgeTimer) })
448
+ disposers.push(function () {
449
+ bridgeDisposers.forEach(function (disposeBridge) { try { disposeBridge() } catch (e) {} })
450
+ })
439
451
  // i18n:注册双语词典;模块级 T 供组件外回调(菜单/错误横幅)取当前语言文案。
440
452
  if (ctx.locale && typeof ctx.locale.register === 'function') {
441
453
  ctx.effect(function () { return ctx.locale.register(NS, DICT) }, 'dsh-soup: dictionaries')
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@lyhue1991/dsh-soup",
3
3
  "description": "dsh 体验增强四件套 = Jupyter风格SideBar + 中央文件预览Tab + 多行GoalBar + 实时Token流速徽标",
4
- "version": "0.6.0",
4
+ "version": "0.7.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "scripts": {
8
8
  "build": "esbuild lib/client.entry.js --bundle --format=iife --platform=browser --target=es2020 --outfile=lib/client.js",
9
9
  "prepublishOnly": "npm run build",
10
- "test": "npm run build && node --test test/client-goal.spec.mjs test/host.spec.mjs"
10
+ "test": "npm run build && node --test test/client-goal.spec.mjs test/file-address-bridge.spec.mjs test/html-preview.spec.mjs test/host.spec.mjs"
11
11
  },
12
12
  "exports": {
13
13
  ".": "./lib/index.js",
@@ -31,7 +31,8 @@
31
31
  "@lezer/highlight": "^1.0.0"
32
32
  },
33
33
  "devDependencies": {
34
- "esbuild": "^0.25.12"
34
+ "esbuild": "^0.25.12",
35
+ "jsdom": "^27.0.1"
35
36
  },
36
37
  "dsh": {
37
38
  "bundle": {