dsh-code-server-app 0.3.20 → 0.3.22

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 (42) hide show
  1. package/README.en.md +82 -19
  2. package/README.md +74 -21
  3. package/assets/extensions/dshcs-editor-bridge/extension.js +120 -64
  4. package/assets/extensions/dshcs-editor-bridge/lib/ask-panel.js +343 -174
  5. package/assets/extensions/dshcs-editor-bridge/lib/bridge-client.js +30 -4
  6. package/assets/extensions/dshcs-editor-bridge/package.json +2 -2
  7. package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +21 -0
  8. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
  9. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
  10. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
  11. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
  12. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
  13. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
  14. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
  15. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
  16. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
  17. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
  18. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
  19. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
  20. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
  21. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
  22. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
  23. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
  24. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
  25. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
  26. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
  27. package/assets/extensions/dshcs-editor-bridge/webview/fonts/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
  28. package/assets/extensions/dshcs-editor-bridge/webview/src/app.jsx +189 -0
  29. package/assets/extensions/dshcs-editor-bridge/webview/src/approval.jsx +53 -0
  30. package/assets/extensions/dshcs-editor-bridge/webview/src/official-tokens.css +8 -0
  31. package/assets/extensions/dshcs-editor-bridge/webview/src/panel.css +283 -0
  32. package/assets/extensions/dshcs-editor-bridge/webview/src/thread.jsx +72 -0
  33. package/assets/extensions/dshcs-editor-bridge/webview/thread.css +1 -0
  34. package/assets/extensions/dshcs-editor-bridge/webview/thread.js +462 -0
  35. package/lib/bridge-approval.mjs +189 -0
  36. package/lib/bridge-session.mjs +3 -1
  37. package/lib/bridge-thread.mjs +345 -0
  38. package/lib/bridge.mjs +15 -6
  39. package/lib/index.js +142 -17
  40. package/package.json +29 -6
  41. package/vendor/VENDOR.json +1 -1
  42. package/lib/bridge-answer.mjs +0 -155
package/lib/index.js CHANGED
@@ -53,8 +53,10 @@ import {
53
53
  import { bridgeEndpointPath, startBridgeListener } from './bridge-ipc.mjs';
54
54
  import { registerEditorPrompt, registerEditorTools, setPromptLiveProbe } from './bridge-tools.mjs';
55
55
  import { deliverEditorPrompt } from './bridge-session.mjs';
56
+ import { dshEntry } from './dsh-resolve.mjs';
56
57
  import { registerBridgeObserver } from './bridge-observe.mjs';
57
- import { createAnswerBoard, registerBridgeAnswers } from './bridge-answer.mjs';
58
+ import { createThreadRegistry } from './bridge-thread.mjs';
59
+ import { createApprovalBoard, createApprovalInterceptor, DEFAULT_HOLD_MS, PANEL_OUTCOMES } from './bridge-approval.mjs';
58
60
 
59
61
  // schemastery 由 DSH 部署自带(官方核心依赖),仿 auto-open-web 的解析策略:
60
62
  // 常规 import 优先,不可用时回退到全局 npm 布局的 DSH 部署副本。
@@ -723,14 +725,75 @@ export async function apply(ctx, config) {
723
725
  let bridgePromptDispose = null;
724
726
  /** agent 写操作观察器的 disposer。 */
725
727
  let bridgeObserveDispose = null;
726
- /** agent 回复同步(提问框)的 disposer。 */
728
+ /** 会话流订阅 / 授权拦截的 disposer。 */
727
729
  let bridgeAnswerDispose = null;
728
730
  /** 推给编辑器的事件环形缓冲(tools/result → 扩展轮询)。 */
729
731
  const bridgeEvents = createEventRing();
730
732
  /** 编辑器状态缓存:扩展在每次 /sync 里推上来,agent 的工具调用来读它。 */
731
733
  const bridgeContext = createContextCache();
732
- /** 「问过 DSH 的会话」的最新回复(随 /sync 的 answers 同步回编辑器提问框)。 */
733
- const bridgeAnswers = createAnswerBoard();
734
+ /** 被面板观看的会话 → 对话条目流(只渲染新内容;见 lib/bridge-thread.mjs)。 */
735
+ const bridgeThread = createThreadRegistry({
736
+ resolveController: () => {
737
+ try {
738
+ return ctx.get('sessionController');
739
+ } catch {
740
+ return undefined;
741
+ }
742
+ },
743
+ log: (message) => console.warn(`[code-server] 编辑器桥会话流:${message}`),
744
+ });
745
+ /** 面板的授权决策登记表 + agent 作用域拦截器(见 lib/bridge-approval.mjs)。 */
746
+ const bridgeApprovalBoard = createApprovalBoard();
747
+ /** 面板最近一次声明 watch 的时间/id:据此判断"面板是不是正在看"(关掉就不拦截授权)。 */
748
+ const bridgeWatch = { ids: [], at: 0 };
749
+ const bridgeApproval = createApprovalInterceptor({
750
+ board: bridgeApprovalBoard,
751
+ holdMs: DEFAULT_HOLD_MS,
752
+ hasPanel: () => bridgeWatch.ids.length > 0 && Date.now() - bridgeWatch.at < 5000,
753
+ log: (message) => console.warn(`[code-server] 编辑器桥授权:${message}`),
754
+ });
755
+
756
+ /** 本部署的 DSH 版本(诊断用;面板要的是下面那个"界面版本")。 */
757
+ function dshVersion() {
758
+ try {
759
+ const entry = dshEntry();
760
+ if (entry === undefined || entry === null) return null;
761
+ const manifest = JSON.parse(fs.readFileSync(path.join(path.dirname(entry), 'package.json'), 'utf8'));
762
+ return typeof manifest.version === 'string' ? manifest.version : null;
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+
768
+ /**
769
+ * 本部署**界面**的版本(0.3.22)。
770
+ *
771
+ * 面板里的 markdown 渲染器是从 `@deepseek-ai/dsh-client-ui-primitives` 打包进去的,而那份
772
+ * 渲染器就装在 `@deepseek-ai/dsh-web-frontend` 里(DSH Web 的前端包)。面板拿这个版本和
773
+ * 自己打包时的版本比一比,不一致就在面板顶部显示一行提示 —— 版本错配的排版差异不许静默。
774
+ */
775
+ function uiVersion() {
776
+ try {
777
+ const entry = dshEntry();
778
+ if (entry === undefined || entry === null) return null;
779
+ const dir = path.dirname(entry);
780
+ const candidates = [
781
+ path.join(dir, 'node_modules', '@deepseek-ai', 'dsh-web-frontend', 'package.json'),
782
+ path.join(dir, '..', 'dsh-web-frontend', 'package.json'),
783
+ ];
784
+ for (const candidate of candidates) {
785
+ try {
786
+ const manifest = JSON.parse(fs.readFileSync(candidate, 'utf8'));
787
+ if (typeof manifest.version === 'string') return manifest.version;
788
+ } catch {
789
+ // 下一个候选
790
+ }
791
+ }
792
+ return null;
793
+ } catch {
794
+ return null;
795
+ }
796
+ }
734
797
 
735
798
  /** 本次启动生成的新桥令牌;null = 本实例没有令牌(adopt 旧实例时会回读 bridge.json)。 */
736
799
  let bridgeToken = null;
@@ -783,14 +846,16 @@ export async function apply(ctx, config) {
783
846
  return handle.close().catch(() => {});
784
847
  }
785
848
 
786
- /** 桥的四条路由(后缀 → Fetch 风格 handler)。做成函数而非常量:handler 声明在文件后段,
787
- * 函数声明会提升,而常量在挂载时可能还在 TDZ。 */
849
+ /** 桥的路由(后缀 → Fetch 风格 handler)。做成函数而非常量:handler 声明在文件后段,
850
+ * 函数声明会提升,而常量在挂载时可能还在 TDZ。
851
+ * `/approve` 是唯一的非只读路由(只回答既有授权请求,见 handleBridgeApprove)。 */
788
852
  function bridgeRouteTable() {
789
853
  return [
790
854
  { suffix: '/health', methods: ['GET'], fetch: handleBridgeHealth },
791
855
  { suffix: '/sync', methods: ['POST'], fetch: handleBridgeSync },
792
856
  { suffix: '/ask', methods: ['POST'], fetch: handleBridgeAsk },
793
857
  { suffix: '/event', methods: ['POST'], fetch: handleBridgeEvent },
858
+ { suffix: '/approve', methods: ['POST'], fetch: handleBridgeApprove },
794
859
  ];
795
860
  }
796
861
 
@@ -855,11 +920,15 @@ export async function apply(ctx, config) {
855
920
  isLive: () => bridgeMeta !== null && !bridgeContext.isStale(),
856
921
  });
857
922
 
858
- // 把 agent 的回复同步给编辑器里的「提问框」(0.3.19):正文随 /sync 的 answers 字段回去。
859
- bridgeAnswerDispose = registerBridgeAnswers(ctx, {
860
- board: bridgeAnswers,
861
- log: (message) => console.warn(`[code-server] 编辑器桥回复同步:${message}`),
862
- });
923
+ // 会话流与授权拦截都是**按会话**建立的(在 /ask 成功时 watch + intercept),
924
+ // 这里只登记卸载钩子:面板关掉 / IDE 停掉时把订阅与 pending 授权一起收干净。
925
+ bridgeAnswerDispose = () => {
926
+ bridgeThread.dispose();
927
+ bridgeApproval.dispose();
928
+ bridgeApprovalBoard.clear();
929
+ bridgeWatch.ids = [];
930
+ bridgeWatch.at = 0;
931
+ };
863
932
 
864
933
  /** 桥配置要写的目录(可能有**两个**):
865
934
  *
@@ -1672,9 +1741,27 @@ export async function apply(ctx, config) {
1672
1741
  }
1673
1742
  const events = bridgeEvents.since(since);
1674
1743
  bridgeEvents.reset();
1675
- // 回复走独立字段(不是事件环形缓冲):每个会话只给**最新一条完整正文**,
1676
- // 这样丢中间态无所谓、也不会把 agent-edit 事件挤掉(见 lib/bridge-answer.mjs)。
1677
- return jsonResponse({ ok: true, events, answers: bridgeAnswers.snapshot() });
1744
+ // 面板声明它要看哪些会话(数组;空 = 关掉了)。据此对齐订阅 + 判断"面板在不在看"
1745
+ // (授权拦截只在面板看着的时候才抢答,否则一律交给官方链路)。
1746
+ const watch = Array.isArray(body.watch) ? body.watch.filter((id) => typeof id === 'string' && id !== '') : [];
1747
+ bridgeWatch.ids = watch.slice(0, 4);
1748
+ bridgeWatch.at = Date.now();
1749
+ bridgeThread.sync(bridgeWatch.ids);
1750
+ // 对话流走独立字段:每个会话给有界条目(见 lib/bridge-thread.mjs),
1751
+ // 授权待决列表也一并回去(面板显示卡片)。
1752
+ return jsonResponse({
1753
+ ok: true,
1754
+ events,
1755
+ thread: bridgeThread.supported()
1756
+ ? bridgeThread.snapshot(bridgeWatch.ids)
1757
+ : { available: false, sessionId: null, cursor: null, entries: [], error: '宿主没有 sessionController.follow(DSH 版本过旧)' },
1758
+ approvals: bridgeApprovalBoard.snapshot(),
1759
+ // 授权卡片要在面板上倒计时,窗口长度由 host 给(面板不猜)。
1760
+ approvalHoldMs: DEFAULT_HOLD_MS,
1761
+ dshVersion: dshVersion(),
1762
+ // 界面版本:面板据此提示"内置渲染器与当前 DSH 界面版本不一致"。
1763
+ uiVersion: uiVersion(),
1764
+ });
1678
1765
  }
1679
1766
 
1680
1767
  async function handleBridgeAsk(request) {
@@ -1702,11 +1789,49 @@ export async function apply(ctx, config) {
1702
1789
  selection,
1703
1790
  languageId: typeof body.languageId === 'string' ? body.languageId : null,
1704
1791
  });
1705
- // 登记这个会话:之后它的回复会随 /sync 同步回编辑器(见 lib/bridge-answer.mjs)。
1792
+ // 会话流 + 授权拦截都在这里建立(只针对**被问过的**那个会话,agent 作用域):
1793
+ // - thread:面板像 DSH 对话那样显示"新内容"(见 lib/bridge-thread.mjs);
1794
+ // - approval:该会话的敏感动作(工作区外写入 / 命令执行)先在面板里问一句,
1795
+ // 8 秒没人答就交给官方链路(见 lib/bridge-approval.mjs)。
1706
1796
  if (result.ok === true && typeof result.sessionId === 'string' && result.sessionId !== '') {
1707
- bridgeAnswers.track(result.sessionId);
1797
+ const ok = bridgeThread.watch(result.sessionId);
1798
+ if (!ok) console.warn('[code-server] 编辑器桥:会话订阅未建立(宿主没有 sessionController.follow)');
1799
+ bridgeApproval.intercept(result.agent, result.sessionId);
1800
+ }
1801
+ // `agent` 是内部句柄,不能进 JSON。
1802
+ const { agent: _agent, ...payload } = result;
1803
+ void _agent;
1804
+ return jsonResponse(payload, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
1805
+ }
1806
+
1807
+ /**
1808
+ * 面板对授权请求的决策(0.3.22)。
1809
+ *
1810
+ * **这是命名空间里唯一的非只读路由**,约束写死在实现里:
1811
+ * - 只能回答**本 host 发起、仍未决**的请求(单次使用,用后即废);
1812
+ * - outcome 只接受 `allowed-once` / `rejected`;
1813
+ * - 不接受任何自由文本、路径或命令参数 —— 它只能"回答问题",不能"发起动作"。
1814
+ */
1815
+ async function handleBridgeApprove(request) {
1816
+ const denied = bridgeRejection(request);
1817
+ if (denied !== null) return denied;
1818
+ let body;
1819
+ try {
1820
+ body = await readJsonBody(request);
1821
+ } catch {
1822
+ return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
1823
+ }
1824
+ const id = typeof body.id === 'string' ? body.id : '';
1825
+ const outcome = typeof body.outcome === 'string' ? body.outcome : '';
1826
+ if (id === '') return jsonResponse({ ok: false, error: '需要 id 字段' }, 400);
1827
+ if (!PANEL_OUTCOMES.includes(outcome)) {
1828
+ return jsonResponse({ ok: false, error: `outcome 只能取 ${PANEL_OUTCOMES.join(' | ')}` }, 400);
1829
+ }
1830
+ if (bridgeApprovalBoard.get(id) === null) {
1831
+ return jsonResponse({ ok: false, error: '未知或已处理的授权请求' }, 409);
1708
1832
  }
1709
- return jsonResponse(result, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
1833
+ const ok = bridgeApprovalBoard.answer(id, outcome);
1834
+ return jsonResponse({ ok, outcome: ok ? outcome : null }, ok ? 200 : 409);
1710
1835
  }
1711
1836
 
1712
1837
  async function handleBridgeEvent(request) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code-server-app",
3
- "version": "0.3.20",
4
- "description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs). Since 0.3.0 the bundle also ships an editor bridge (assets/extensions/dshcs-editor-bridge): a read-only channel between the in-tree VS Code extension host and DSH, giving the agent what only the editor knows (unsaved buffers, language-server diagnostics, the active selection) and letting editor gestures drive the session. The tab claims DSH file addresses (dsh-resource://file/**) by file type (setting claimExtensions), so the product's own produced-file chips, delivered-file previews and inline prose mentions open in the workbench. The IDE is a resident surface moved with Element.moveBefore instead of being remounted, so switching sidebar tabs no longer reloads it. Opening the tab switches the right sidebar to fullscreen by default (setting fullscreenOnOpen). Following a workspace switch is lightweight: the workbench re-navigates with the new ?folder= and the IDE process is not restarted (since 0.2.12). Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions get a single upgrade notice on the settings page and no other UI. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). No code-server Node layer, no argon2, no C++ toolchain.",
3
+ "version": "0.3.22",
4
+ "description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs). Since 0.3.0 the bundle also ships an editor bridge (assets/extensions/dshcs-editor-bridge): a read-only channel between the in-tree VS Code extension host and DSH, giving the agent what only the editor knows (unsaved buffers, language-server diagnostics, the active selection) and letting editor gestures drive the session. Since 0.3.22 the ask panel renders that session's new content with DSH's own Markdown renderer (pinned to the deployed DSH UI version) and can answer approval requests in place — the bridge's only non-read-only route (POST /approve, constrained to pending in-process requests and the outcomes allowed-once/rejected). The tab claims DSH file addresses (dsh-resource://file/**) by file type (setting claimExtensions), so the product's own produced-file chips, delivered-file previews and inline prose mentions open in the workbench. The IDE is a resident surface moved with Element.moveBefore instead of being remounted, so switching sidebar tabs no longer reloads it. Opening the tab switches the right sidebar to fullscreen by default (setting fullscreenOnOpen). Following a workspace switch is lightweight: the workbench re-navigates with the new ?folder= and the IDE process is not restarted (since 0.2.12). Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions get a single upgrade notice on the settings page and no other UI. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). No code-server Node layer, no argon2, no C++ toolchain.",
5
5
  "homepage": "https://github.com/jinsiyu/dsh-code-server-app",
6
6
  "repository": {
7
7
  "type": "git",
@@ -43,7 +43,8 @@
43
43
  "lib/bridge-tools.mjs",
44
44
  "lib/bridge-session.mjs",
45
45
  "lib/bridge-observe.mjs",
46
- "lib/bridge-answer.mjs",
46
+ "lib/bridge-thread.mjs",
47
+ "lib/bridge-approval.mjs",
47
48
  "lib/dsh-resolve.mjs",
48
49
  "lib/launcher.mjs",
49
50
  "lib/serve-dsh.mjs",
@@ -63,7 +64,28 @@
63
64
  "README.en.md"
64
65
  ],
65
66
  "devDependencies": {
66
- "esbuild": "^0.25.0"
67
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
68
+ "@deepseek-ai/dsh-client-ui-theme": "0.1.5-rc.2",
69
+ "@shikijs/langs": "4.3.1",
70
+ "anser": "2.3.5",
71
+ "clsx": "2.0.0",
72
+ "esbuild": "^0.25.0",
73
+ "katex": "0.16.47",
74
+ "mdast-util-from-markdown": "2.0.3",
75
+ "mdast-util-gfm": "3.1.0",
76
+ "mdast-util-math": "3.0.0",
77
+ "micromark-core-commonmark": "2.0.3",
78
+ "micromark-extension-gfm": "3.0.0",
79
+ "micromark-extension-math": "3.1.0",
80
+ "micromark-factory-space": "2.0.1",
81
+ "micromark-util-character": "2.1.1",
82
+ "micromark-util-classify-character": "2.0.1",
83
+ "micromark-util-sanitize-uri": "2.0.1",
84
+ "micromark-util-symbol": "2.0.1",
85
+ "micromark-util-types": "2.0.2",
86
+ "react": "18.3.1",
87
+ "react-dom": "18.3.1",
88
+ "shiki": "4.3.1"
67
89
  },
68
90
  "license": "MIT",
69
91
  "dependencies": {
@@ -90,7 +112,6 @@
90
112
  "http-proxy-agent": "7.0.0",
91
113
  "https-proxy-agent": "7.0.2",
92
114
  "jschardet": "3.1.4",
93
- "katex": "0.16.22",
94
115
  "minimist": "1.2.8",
95
116
  "node-addon-api": "6.1.0",
96
117
  "tar": "7.5.22",
@@ -119,12 +140,14 @@
119
140
  "publish:plugin": "node scripts/publish-plugin.mjs",
120
141
  "promote": "node scripts/promote.mjs",
121
142
  "build:client": "node scripts/build-client.mjs",
143
+ "build:webview": "node scripts/build-webview.mjs",
122
144
  "test:apply": "node scripts/test-plugin-apply.mjs",
123
145
  "test:launcher-routes": "node scripts/test-launcher-routes.mjs",
124
146
  "test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
125
147
  "test:claim-types": "node scripts/test-claim-types.mjs",
126
148
  "test:workspace-switch": "node scripts/test-workspace-switch.mjs",
127
149
  "test:bridge-routes": "node scripts/test-bridge-routes.mjs",
128
- "test:bridge-extension": "node scripts/test-bridge-extension.mjs"
150
+ "test:bridge-extension": "node scripts/test-bridge-extension.mjs",
151
+ "test:webview": "node scripts/test-webview-bundle.mjs"
129
152
  }
130
153
  }
@@ -3,7 +3,7 @@
3
3
  "vscodeVersion": "1.137.0",
4
4
  "productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
5
5
  "layout": "vscode-only",
6
- "preparedAt": "2026-09-13T06:41:44.563Z",
6
+ "preparedAt": "2026-09-13T08:28:20.966Z",
7
7
  "source": "registry",
8
8
  "node": "v24.21.0",
9
9
  "platform": "win32",
@@ -1,155 +0,0 @@
1
- /**
2
- * lib/bridge-answer.mjs — 把 agent 的回复同步回编辑器(0.3.19)。
3
- *
4
- * 方向:DSH → 编辑器。用户在编辑器里问了一句之后,当然想**在原地**看到回答,而不是切回 DSH 界面。
5
- * 桥本来就是"扩展每 600ms 拉一次 `/sync`",所以回复走同一趟,新增一个字段而不是挤进事件环形缓冲:
6
- *
7
- * `/sync` 响应 = `{ ok, events, answers }`
8
- * `answers` = `[{ sessionId, text, done, at }]` —— **每个会话只保留最新一条**,
9
- * 且每次都是"到目前为止的完整正文"(不是增量)。这样:
10
- * · 事件环形缓冲(64 条)不被正文挤爆 —— `agent-edit` 那些提示不会因此丢掉;
11
- * · 丢中间态无所谓 —— 面板永远渲染最新那条完整正文,不需要做增量合并;
12
- *
13
- * 数据来源(都是 agent 作用域的事件,根上下文监听器能看到所有 agent):
14
- * - `agent/assistant-stream` 的 `text-delta` 帧 → 累积正文(**不含 reasoning**);
15
- * - `agent/status` → `idle`(这一轮跑完了)→ 标记 `done:true`。
16
- *
17
- * 只同步"**被编辑器问过**的会话"(ask 时登记 sessionId):别的会话在 DSH 界面里聊得再多,
18
- * 也不该往桥里塞数据。会话数有上限,超了丢最旧的。
19
- */
20
-
21
- /** 每个会话保留的最新回复;超过这么多会话就丢最旧的(编辑器里问过的会话不会有几个)。 */
22
- export const MAX_TRACKED_SESSIONS = 8;
23
-
24
- /** 回复正文上限:面板只用来显示,超长截断(避免一次 sync 把响应撑大)。 */
25
- export const MAX_ANSWER_CHARS = 20000;
26
-
27
- /** 累积器的实现:`registerBridgeAnswers` 用它,测试也直接用它。 */
28
- export function createAnswerBoard({ maxSessions = MAX_TRACKED_SESSIONS } = {}) {
29
- /** sessionId → { text, done, at }(插入顺序即新旧顺序) */
30
- const entries = new Map();
31
-
32
- return {
33
- /** ask 成功时登记:之后这个会话的回复才会同步给编辑器。 */
34
- track(sessionId) {
35
- if (typeof sessionId !== 'string' || sessionId === '') return false;
36
- entries.delete(sessionId);
37
- entries.set(sessionId, { text: '', done: false, at: Date.now() });
38
- while (entries.size > maxSessions) entries.delete(entries.keys().next().value);
39
- return true;
40
- },
41
- /** 追加一段正文(只在已登记的会话上生效)。 */
42
- append(sessionId, text) {
43
- if (typeof text !== 'string' || text === '' || !entries.has(sessionId)) return false;
44
- const current = entries.get(sessionId);
45
- const next = current.text + text;
46
- entries.set(sessionId, {
47
- text: next.length > MAX_ANSWER_CHARS ? next.slice(-MAX_ANSWER_CHARS) : next,
48
- done: false,
49
- at: Date.now(),
50
- });
51
- return true;
52
- },
53
- /** 一轮跑完(或出错):定稿。 */
54
- finish(sessionId) {
55
- const current = entries.get(sessionId);
56
- if (current === undefined) return false;
57
- entries.set(sessionId, { ...current, done: true, at: Date.now() });
58
- return true;
59
- },
60
- /** 新的一轮开始:清掉上一次的正文,避免新旧回答拼在一起。 */
61
- restart(sessionId) {
62
- if (!entries.has(sessionId)) return false;
63
- entries.set(sessionId, { text: '', done: false, at: Date.now() });
64
- return true;
65
- },
66
- /** 给 `/sync` 的快照(数组顺序 = 登记顺序)。 */
67
- snapshot() {
68
- return [...entries.entries()].map(([sessionId, value]) => ({ sessionId, ...value }));
69
- },
70
- /** 某会话当前状态(诊断/测试用)。 */
71
- get(sessionId) {
72
- return entries.get(sessionId) ?? null;
73
- },
74
- size() {
75
- return entries.size;
76
- },
77
- };
78
- }
79
-
80
- /** 从 agent 对象取会话 id(与 lib/bridge-session.mjs 同一套取值规则)。 */
81
- export function sessionIdOf(agent) {
82
- if (agent === null || agent === undefined) return null;
83
- if (agent.session !== undefined && agent.session !== null && typeof agent.session === 'object') {
84
- return agent.session.id ?? agent.id ?? null;
85
- }
86
- return agent.id ?? null;
87
- }
88
-
89
- /**
90
- * 订阅 agent 事件,把回复喂进 board。
91
- *
92
- * @param {object} ctx cordis 上下文(需要 `on`)
93
- * @param {{board: ReturnType<typeof createAnswerBoard>, log?: (message: string) => void}} deps
94
- * @returns {() => void} 注销函数(永不抛)
95
- */
96
- export function registerBridgeAnswers(ctx, deps) {
97
- const { board } = deps;
98
- const log = deps.log ?? (() => {});
99
- if (ctx === undefined || ctx === null || typeof ctx.on !== 'function') return () => {};
100
- const disposers = [];
101
- /** sessionId → 上次看到的 turn(换轮时重新累积正文)。 */
102
- const lastTurn = new Map();
103
- const on = (event, handler) => {
104
- try {
105
- const dispose = ctx.on(event, handler);
106
- if (typeof dispose === 'function') disposers.push(dispose);
107
- } catch (err) {
108
- log(`订阅 ${event} 失败:${err && err.message ? err.message : String(err)}`);
109
- }
110
- };
111
-
112
- on('agent/assistant-stream', (payload) => {
113
- try {
114
- const agent = payload?.agent;
115
- const frame = payload?.frame;
116
- if (frame?.type !== 'chunk') return;
117
- const chunk = frame.chunk;
118
- // 只同步**正文**:reasoning 是模型的内心戏,不该出现在编辑器的提问框里。
119
- if (chunk?.type !== 'text-delta' || typeof chunk.text !== 'string' || chunk.text === '') return;
120
- const sessionId = sessionIdOf(agent);
121
- if (sessionId === null) return;
122
- // 换了一轮(turn 变了)就重新累积:新问题给的是新回答,不该和上一轮拼在一起。
123
- const turn = Number.isSafeInteger(frame.turn) ? frame.turn : null;
124
- if (turn !== null && lastTurn.get(sessionId) !== turn) {
125
- lastTurn.set(sessionId, turn);
126
- board.restart(sessionId);
127
- }
128
- board.append(sessionId, chunk.text);
129
- } catch (err) {
130
- // 同步失败绝不影响 agent 的一轮(这与 lib/bridge-observe.mjs 同一策略)。
131
- log(`同步回复失败:${err && err.message ? err.message : String(err)}`);
132
- }
133
- });
134
-
135
- on('agent/status', (payload) => {
136
- try {
137
- if (payload?.status !== 'idle') return;
138
- const sessionId = sessionIdOf(payload?.agent);
139
- if (sessionId === null) return;
140
- board.finish(sessionId);
141
- } catch (err) {
142
- log(`收尾回复失败:${err && err.message ? err.message : String(err)}`);
143
- }
144
- });
145
-
146
- return () => {
147
- for (const dispose of disposers) {
148
- try {
149
- dispose();
150
- } catch {
151
- // 注销失败无关正确性
152
- }
153
- }
154
- };
155
- }