@lyhue1991/dsh-soup 0.7.0 → 0.7.2

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 / 编辑器)修改时,预览内容几秒内自动更新;若你正在编辑,则保留草稿并提示冲突。
@@ -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 }
@@ -29,6 +29,21 @@ export function previewKind(path) {
29
29
  return 'text'
30
30
  }
31
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
+
32
47
  /** RFC 4180 风格的分隔符解析(引号感知,"" 转义),返回行数组。 */
33
48
  export function parseDelimited(text, delim) {
34
49
  var rows = []
@@ -72,6 +87,7 @@ export function isEditableTextFile(path) {
72
87
  var kind = previewKind(path)
73
88
  var ext = extOfName(path)
74
89
  return kind === 'markdown' || kind === 'html' || kind === 'json'
90
+ || EDITABLE_FILE_NAMES.has(baseNameOf(path))
75
91
  || Object.prototype.hasOwnProperty.call(CODE_LANG_BY_EXT, ext)
76
92
  }
77
93
 
@@ -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
@@ -30963,6 +30963,110 @@
30963
30963
  window.__DSH_SOUP_HTML_PREVIEW__ = { createHtmlPreview };
30964
30964
  }
30965
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
+
30966
31070
  // lib/client/styles.js
30967
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>';
30968
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>';
@@ -31488,51 +31592,6 @@
31488
31592
  }
31489
31593
  var setiTag;
31490
31594
 
31491
- // lib/client/file-address.js
31492
- var FILE_ADDRESS_PREFIX = "dsh-resource://file/";
31493
- function decodeSegmentSafe(segment) {
31494
- try {
31495
- return decodeURIComponent(segment);
31496
- } catch (e) {
31497
- return null;
31498
- }
31499
- }
31500
- function parseFileAddress(address) {
31501
- if (typeof address !== "string" || address.indexOf(FILE_ADDRESS_PREFIX) !== 0) return null;
31502
- var end = address.search(/[?#]/);
31503
- var body = address.slice(FILE_ADDRESS_PREFIX.length, end === -1 ? void 0 : end);
31504
- var parts = body.split("/");
31505
- if (parts[0] === "session") {
31506
- var sessionId = decodeSegmentSafe(parts[1] || "");
31507
- if (!sessionId) return null;
31508
- var segments = [];
31509
- for (var i = 2; i < parts.length; i++) {
31510
- var seg = decodeSegmentSafe(parts[i]);
31511
- if (seg === null) return null;
31512
- segments.push(seg);
31513
- }
31514
- if (!segments.length) return null;
31515
- return { scope: "session", sessionId, path: segments.join("/") };
31516
- }
31517
- if (parts[0] === "absolute") {
31518
- var absolute = [];
31519
- for (var j = 1; j < parts.length; j++) {
31520
- var abs = decodeSegmentSafe(parts[j]);
31521
- if (abs === null) return null;
31522
- absolute.push(abs);
31523
- }
31524
- if (!absolute.length) return null;
31525
- return { scope: "absolute", path: "/" + absolute.join("/") };
31526
- }
31527
- return null;
31528
- }
31529
- function fileAddressMatches(address, sessionIdHint, activeSessionId) {
31530
- var parsed = parseFileAddress(address);
31531
- if (!parsed) return false;
31532
- if (parsed.scope === "session" && (sessionIdHint || parsed.sessionId) !== activeSessionId) return false;
31533
- return true;
31534
- }
31535
-
31536
31595
  // lib/client/preview-renderers.js
31537
31596
  function extOfName2(p) {
31538
31597
  var m = /\.([A-Za-z0-9]+)$/.exec(String(p || ""));
@@ -31548,6 +31607,35 @@
31548
31607
  if (ext === "tsv") return "tsv";
31549
31608
  return "text";
31550
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
+ }
31551
31639
  function parseDelimited(text, delim) {
31552
31640
  var rows = [];
31553
31641
  var row = [];
@@ -31597,7 +31685,7 @@
31597
31685
  function isEditableTextFile(path) {
31598
31686
  var kind = previewKind(path);
31599
31687
  var ext = extOfName2(path);
31600
- 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);
31601
31689
  }
31602
31690
  function createPreviewRenderers(ctx) {
31603
31691
  var React = ctx.React;
@@ -34980,7 +35068,8 @@
34980
35068
  document.removeEventListener("click", onOfficialExpand, true);
34981
35069
  });
34982
35070
  }
34983
- var bridgePatched = false;
35071
+ var bridgeDisposers = [];
35072
+ var bridgedServices = typeof WeakSet === "function" ? /* @__PURE__ */ new WeakSet() : [];
34984
35073
  function resolveSidebarRight() {
34985
35074
  try {
34986
35075
  if (ctx.sidebarRight) return ctx.sidebarRight;
@@ -35002,42 +35091,43 @@
35002
35091
  }
35003
35092
  return null;
35004
35093
  }
35005
- function patchSidebarRightBridge() {
35006
- if (bridgePatched) return true;
35007
- var service = resolveSidebarRight();
35008
- if (!service || typeof service.openResource !== "function") return false;
35009
- if (!service.__dshSoupFileBridge) {
35010
- var origResource = service.openResource;
35011
- var origResourceIn = service.openResourceIn;
35012
- service.openResource = function(address, options) {
35013
- if (fileAddressMatches(address, null, activeSessionId)) {
35014
- openFileAddressInSoup(address, null);
35015
- return;
35016
- }
35017
- return origResource.call(service, address, options);
35018
- };
35019
- if (typeof origResourceIn === "function") {
35020
- service.openResourceIn = function(sessionId, address, options) {
35021
- if (fileAddressMatches(address, sessionId, activeSessionId)) {
35022
- openFileAddressInSoup(address, sessionId);
35023
- return;
35024
- }
35025
- return origResourceIn.call(service, sessionId, address, options);
35026
- };
35027
- }
35028
- try {
35029
- Object.defineProperty(service, "__dshSoupFileBridge", { value: true });
35030
- } catch (e) {
35031
- service.__dshSoupFileBridge = true;
35032
- }
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;
35033
35107
  }
35034
- 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);
35035
35117
  try {
35036
35118
  if (typeof window !== "undefined") window.__DSH_SOUP_FILE_BRIDGE = true;
35037
35119
  } catch (e) {
35038
35120
  }
35039
35121
  return true;
35040
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
+ }
35041
35131
  patchSidebarRightBridge();
35042
35132
  var bridgeTries = 0;
35043
35133
  var bridgeTimer = setInterval(function() {
@@ -35047,6 +35137,14 @@
35047
35137
  disposers.push(function() {
35048
35138
  clearInterval(bridgeTimer);
35049
35139
  });
35140
+ disposers.push(function() {
35141
+ bridgeDisposers.forEach(function(disposeBridge) {
35142
+ try {
35143
+ disposeBridge();
35144
+ } catch (e) {
35145
+ }
35146
+ });
35147
+ });
35050
35148
  if (ctx.locale && typeof ctx.locale.register === "function") {
35051
35149
  ctx.effect(function() {
35052
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'
@@ -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/lib/host/speed.js CHANGED
@@ -47,10 +47,25 @@ function extractChunk(chunk) {
47
47
  }
48
48
  }
49
49
  }
50
+ const appendToolCalls = (calls) => {
51
+ if (!Array.isArray(calls)) return
52
+ for (const call of calls) {
53
+ // OpenAI-style delta: { function: { arguments: '...' } }. Some adapters
54
+ // use `arguments` directly on the call object.
55
+ appendText(call?.function?.arguments)
56
+ appendText(call?.arguments)
57
+ appendText(call?.argumentsDelta)
58
+ }
59
+ }
50
60
  try {
51
61
  if (!chunk) return { text, realTokens }
52
62
  appendText(chunk.text)
53
63
  appendText(chunk.content)
64
+ // Tool-call argument deltas are generated output too. DSH's abstract
65
+ // StreamChunk carries them as `argumentsDelta`; when the model writes a
66
+ // file the whole stream is tool-call deltas, so without this the content
67
+ // clock never advances and the badge degrades to the waiting text.
68
+ appendText(chunk.argumentsDelta)
54
69
  // Reasoning streams use different names across providers/adapters. They
55
70
  // are still generated output and must keep the throughput window alive.
56
71
  appendText(chunk.reasoning_content)
@@ -64,8 +79,10 @@ function extractChunk(chunk) {
64
79
  appendText(chunk.delta.reasoningContent)
65
80
  appendText(chunk.delta.reasoning)
66
81
  appendText(chunk.delta.thinking)
82
+ appendToolCalls(chunk.delta.tool_calls)
67
83
  realTokens = outputTokens(chunk.delta.usage) ?? realTokens
68
84
  }
85
+ appendToolCalls(chunk.tool_calls)
69
86
  realTokens = outputTokens(chunk.usage) ?? realTokens
70
87
  if (Array.isArray(chunk.choices)) {
71
88
  for (const choice of chunk.choices) {
@@ -75,6 +92,7 @@ function extractChunk(chunk) {
75
92
  appendText(choice?.delta?.reasoningContent)
76
93
  appendText(choice?.delta?.reasoning)
77
94
  appendText(choice?.delta?.thinking)
95
+ appendToolCalls(choice?.delta?.tool_calls)
78
96
  realTokens = outputTokens(choice?.delta?.usage) ?? realTokens
79
97
  }
80
98
  }
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.7.0",
4
+ "version": "0.7.2",
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/html-preview.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",