dsh-code-server-app 0.3.57 → 0.3.59

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/lib/index.js CHANGED
@@ -337,6 +337,31 @@ export function installBundledExtensions(extensionsDir, userDataDir, options = {
337
337
  }
338
338
  };
339
339
  walkDst('');
340
+ // 文件删完还要**收掉空目录**:0.3.59 删掉了整个 webview/ 子目录,只删文件的话会在用户的
341
+ // VS Code 树里留下一串空目录(实测:webview/、webview/src、webview/fonts 各 0 字节)。
342
+ // 自底向上收,只收"确实空了"的目录(非空的目录说明里面有别的东西,不动)。
343
+ const pruneEmptyDirs = (rel) => {
344
+ const abs = rel === '' ? dst : path.join(dst, rel);
345
+ let entries;
346
+ try {
347
+ entries = fs.readdirSync(abs, { withFileTypes: true });
348
+ } catch {
349
+ return;
350
+ }
351
+ for (const ent of entries) {
352
+ if (ent.isDirectory()) pruneEmptyDirs(rel === '' ? ent.name : `${rel}/${ent.name}`);
353
+ }
354
+ if (rel === '') return;
355
+ try {
356
+ if (fs.readdirSync(abs).length === 0) {
357
+ fs.rmdirSync(abs);
358
+ removed.push(`${rel}/`);
359
+ }
360
+ } catch {
361
+ // 竞态(别的东西刚写进来)或权限问题:保持原样,不影响主流程
362
+ }
363
+ };
364
+ pruneEmptyDirs('');
340
365
  }
341
366
  // 清理放错位置的副本:编辑器桥的旧用户级副本(0.3.11 及更早)/ dshcs-open-file 的旧用户级副本。
342
367
  const wrong = extensionTarget(extensionsDir, ext.name, ext.placement === 'builtin' ? 'user' : 'builtin', treeRoot);
@@ -755,22 +780,19 @@ export async function apply(ctx, config) {
755
780
  const bridgeApprovalBoard = createApprovalBoard();
756
781
  /**
757
782
  * 谁在"看着这个会话" —— 授权拦截据此决定要不要抢答(没人看就直接交给官方链路):
758
- * - `ids`:编辑器扩展面板声明要看的会话(每趟 /sync 上报);
759
783
  * - `dialogIds`:DSH 页面里的对话框(0.3.24)声明要看的会话;
760
- * - `at`:最近一次声明的时间(超过 5s 视为没人看)。
784
+ * - `at`:最近一次声明的时间(诊断用)。
785
+ *
786
+ * 0.3.59:编辑器扩展的 webview 面板退役后只剩对话框一路 —— 以前还有 `ids`(扩展每趟 /sync 上报的
787
+ * 观看列表),两个来源求并集、还要按心跳新鲜度区分,现在都不需要了。
761
788
  */
762
- const bridgeWatch = { ids: [], dialogIds: [], at: 0 };
763
- /** 上次回给扩展的对话流修订号(0.3.27:没变化就不重传整份快照)。 */
764
- let bridgeThreadRevSent = -1;
789
+ const bridgeWatch = { dialogIds: [], at: 0 };
765
790
  /** 上次记录过的"有没有人在看"(0.3.30:状态翻转才写诊断日志)。 */
766
791
  let bridgeWatchLogged = false;
767
792
  /** 首帧是否已写过诊断(0.3.31:"一直没人看"也要留下证据)。 */
768
793
  let bridgeWatchWroteFirst = false;
769
794
  /** 上次写过的 hasPanel 判据原文(变了才写,避免每 600ms 刷屏)。 */
770
795
  let bridgeHasPanelLine = '';
771
- /** 客户端(self-declared)是否能把授权卡片画出来(0.3.30)。 */
772
- let bridgeApprovalsUi = false;
773
- let bridgeApprovalsUiLogged = false;
774
796
 
775
797
  /**
776
798
  * 授权/看护状态的**诊断日志**(0.3.30):写到 <home>/.dsh/code-server/bridge-approval.log。
@@ -795,36 +817,30 @@ export async function apply(ctx, config) {
795
817
 
796
818
  /** 当前所有"在看"的会话(去重)。 */
797
819
  function watchedSessions() {
798
- const all = [...bridgeWatch.ids, ...bridgeWatch.dialogIds];
799
- return [...new Set(all.filter((id) => typeof id === 'string' && id !== ''))].slice(0, 4);
820
+ return [...new Set(bridgeWatch.dialogIds.filter((id) => typeof id === 'string' && id !== ''))].slice(0, 4);
800
821
  }
801
822
  /** 现在有没有人在看(授权拦截的唯一判据)。 */
802
823
  function hasWatcher() {
803
- const sessions = watchedSessions();
804
- if (sessions.length === 0) return false;
805
- // **对话框是宿主侧状态**(只在 /ask/close 时消失):不拿轮询新鲜度去判它 ——
806
- // 浏览器把后台标签的定时器节流到分钟级时,900ms 的心跳一断、5 秒窗口就过期,
824
+ // 0.3.59 起只有对话框一路。**对话框是宿主侧状态**(只在 /ask/close 时消失),所以不拿轮询新鲜度
825
+ // 去判它 —— 浏览器把后台标签的定时器节流到分钟级时,900ms 的心跳一断、5 秒窗口就过期,
807
826
  // 我们会误判"没人看"而放弃拦截(用户实测:授权卡片因此跑到 DSH 界面)。
808
- if (bridgeWatch.dialogIds.length > 0) return true;
809
- // 编辑器面板靠 /sync 的 600ms 心跳;窗口放宽到 60 秒(正常心跳远小于它)。
810
- return Date.now() - bridgeWatch.at < 60000;
827
+ return watchedSessions().length > 0;
811
828
  }
812
829
 
813
830
  const bridgeApproval = createApprovalInterceptor({
814
831
  board: bridgeApprovalBoard,
815
832
  holdMs: DEFAULT_HOLD_MS,
816
- // **只在客户端能画卡片时才抢答**(0.3.30):面板/对话框在看 **且** 它自己声明 approvalsUi。
833
+ // **只在对话框真的能显示卡片时才抢答**(0.3.30 / 0.3.59 收紧)。
817
834
  // 反例(用户实测):面板处于"宿主没有对话流能力"的坏状态时我们仍然抢答 ⇒ 官方卡片不出现、
818
835
  // 面板也不画 ⇒ 用户看到的是"没弹出授权",请求在宿主里干等 5 分钟。
819
- // 客户端能画卡片 = 编辑器面板自报可以的 **或** 对话框开着且有会话(0.3.33)。
820
- // "活着"由 /ask/state 轮询证明)。0.3.31 的日志给出了反例:用户从**对话框**提问 ⇒
821
- // 编辑器面板是 null ⇒ 旧判据误判成"画不出卡片",授权被让给了 DSH 界面。
836
+ // 0.3.59 起编辑器面板退役,"能显示卡片"只剩**对话框开着 + 有人看 + 客户端半部在轮询**一条路
837
+ // (客户端自己声明 approvalsUi 那条也随面板一起删了 —— 声明者已经不存在)。
822
838
  hasPanel: () => {
823
839
  const watcher = hasWatcher();
824
840
  const dialogCapable = askDialogLive();
825
- const verdict = watcher && (bridgeApprovalsUi || dialogCapable);
841
+ const verdict = watcher && dialogCapable;
826
842
  // 判据的**原始值**一起落日志(0.3.35):只记结论的话,出问题还是只能猜。
827
- const line = 'hasPanel -> ' + verdict + ' (hasWatcher=' + watcher + ' approvalsUi=' + bridgeApprovalsUi
843
+ const line = 'hasPanel -> ' + verdict + ' (hasWatcher=' + watcher
828
844
  + ' dialogLive=' + dialogCapable + ' polls=' + askDialog.polls + ' open=' + askDialog.open
829
845
  + ' session=' + String(askDialog.sessionId) + ')';
830
846
  if (line !== bridgeHasPanelLine) { bridgeHasPanelLine = line; bridgeDiag(line); }
@@ -839,11 +855,15 @@ export async function apply(ctx, config) {
839
855
  /**
840
856
  * 「问 DSH」对话框的宿主侧状态(0.3.24)。
841
857
  *
842
- * 对话渲染从编辑器的 webview 面板搬到了 **DSH 页面里的浮动对话框**(client 半部,见 src/factory.js):
843
- * 面板是编辑器里的一个 tab/一列,而用户要的是"像 DSH 那样的对话框"。host 这边只负责:
858
+ * 对话只有一个宿主:**DSH 页面里的浮动对话框**(client 半部,见 lib/client.js)。host 这边只负责:
844
859
  * - 打开/关闭状态(编辑器扩展右键 → 桥 `/event` 上报 `ask-open`);
845
860
  * - 状态与动作的 HTTP 面(`/api/code-server/ask/*`,与 DSH 同源、吃它的 cookie/Origin 校验);
846
- * - 把面板产物(thread.js / thread.css)交给对话框注入(Lib 版本与 DSH 界面同版本,见 build-webview.mjs)。
861
+ * - 把"被看的会话"投影成有界条目流(见 lib/bridge-thread.mjs)与待决授权快照。
862
+ *
863
+ * **0.3.59 起不再往页面里送产物,也不再服务编辑器面板**:面板就在 `lib/client.js` 里,渲染器走
864
+ * DSH 页面的模块表(`react-dom/client` + `@deepseek-ai/dsh-client-ui-primitives`);
865
+ * `/ask/bundle` 路由、`askBundle()` 与编辑器里的 webview 面板在同一版里一起删掉
866
+ * (thread.js / thread.css / build-webview.mjs / esbuild 那一整套)。
847
867
  */
848
868
  const askDialog = { open: false, armed: false, mode: 'selection', at: 0, sessionId: null, polls: 0, lastPollAt: 0 };
849
869
 
@@ -852,8 +872,8 @@ export async function apply(ctx, config) {
852
872
  *
853
873
  * 0.3.24 的教训:宿主无条件报 `askDialog: true`,扩展于是不再开编辑器面板;而客户端半部只要没跑起来
854
874
  * (浏览器还缓存着旧 bundle / 没硬刷新),就**没有任何一方在看这个会话** ⇒ 授权悄悄落到 DSH 界面的
855
- * 官方卡片,用户在编辑器里什么都看不到。现在只有**真的收到过 `/ask/state` 轮询**(5 秒内)才算有
856
- * 这个能力;否则扩展退回编辑器面板 —— 那是验证过、能看能点的 UI。
875
+ * 官方卡片,用户在编辑器里什么都看不到。现在只有**真的收到过 `/ask/state` 轮询**(10 分钟内)才算有
876
+ * 这个能力;0.3.59 起不满足时扩展也不再退编辑器面板(那一半已退役),而是明确提示用户刷新/打开页面。
857
877
  */
858
878
  function askDialogLive() {
859
879
  // 对话框每 900ms 问一次 /ask/state —— 那就是"活跃心跳"。心跳一旦建立,在 TTL 内
@@ -922,7 +942,8 @@ export async function apply(ctx, config) {
922
942
  statusText: '',
923
943
  error: null,
924
944
  sessionId: snapshot.sessionId ?? askDialog.sessionId ?? null,
925
- uiVersion: uiVersion(),
945
+ // 0.3.59:`uiVersion` 只服务过"面板内置渲染器与界面版本比对"那行告警 —— 面板退役后删掉。
946
+ // `dshVersion` 留着:诊断时一眼看出这套宿主跑在哪个 DSH 上。
926
947
  dshVersion: dshVersion(),
927
948
  };
928
949
  }
@@ -932,10 +953,12 @@ export async function apply(ctx, config) {
932
953
  return bridgeThread.rev(watchedSessions()) * 8 + bridgeApprovalBoard.rev() * 8 + (askDialog.open ? 1 : 0) + (askDialog.mode === 'file' ? 2 : 0);
933
954
  }
934
955
 
935
- /** 对话框刷新它的"我在看"时间戳(每趟 /state 都算一次)。 */
956
+ /** 对话框刷新它的"我在看"时间戳(每趟 /state 都算一次)。
957
+ * 0.3.59 起这里也负责对齐会话流订阅:以前是扩展每 600ms 的 `/sync` 顺带做的。 */
936
958
  function refreshDialogWatch() {
937
959
  bridgeWatch.dialogIds = askDialog.open && askDialog.sessionId !== null ? [askDialog.sessionId] : [];
938
960
  bridgeWatch.at = Date.now();
961
+ bridgeThread.sync(watchedSessions());
939
962
  }
940
963
 
941
964
  /**
@@ -952,29 +975,6 @@ export async function apply(ctx, config) {
952
975
  return { status: ok ? 200 : 409, body: { ok, outcome: ok ? outcome : null } };
953
976
  }
954
977
 
955
- /** 面板产物(thread.js / thread.css):对话框把它们注入 DSH 页面,所以由 host 读盘给文本。 */
956
- let askBundleCache = null;
957
- function askBundle() {
958
- if (askBundleCache !== null) return askBundleCache;
959
- const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'assets', 'extensions', 'dshcs-editor-bridge', 'webview');
960
- const read = (name) => {
961
- try {
962
- return fs.readFileSync(path.join(dir, name), 'utf8');
963
- } catch {
964
- return null;
965
- }
966
- };
967
- const js = read('thread.js');
968
- const css = read('thread.css');
969
- // 产物缺失(打包漏了 build:webview)→ 明确报出来,不返回半个面板。
970
- if (js === null || css === null) {
971
- console.warn(`[code-server] 「问 DSH」对话框的产物缺失:${dir}(请在源码里跑 pnpm run build:webview)`);
972
- return null;
973
- }
974
- askBundleCache = { js, css };
975
- return askBundleCache;
976
- }
977
-
978
978
 
979
979
  /** 本部署的 DSH 版本(诊断用;面板要的是下面那个"界面版本")。 */
980
980
  function dshVersion() {
@@ -988,36 +988,6 @@ export async function apply(ctx, config) {
988
988
  }
989
989
  }
990
990
 
991
- /**
992
- * 本部署**界面**的版本(0.3.22)。
993
- *
994
- * 面板里的 markdown 渲染器是从 `@deepseek-ai/dsh-client-ui-primitives` 打包进去的,而那份
995
- * 渲染器就装在 `@deepseek-ai/dsh-web-frontend` 里(DSH Web 的前端包)。面板拿这个版本和
996
- * 自己打包时的版本比一比,不一致就在面板顶部显示一行提示 —— 版本错配的排版差异不许静默。
997
- */
998
- function uiVersion() {
999
- try {
1000
- const entry = dshEntry();
1001
- if (entry === undefined || entry === null) return null;
1002
- const dir = path.dirname(entry);
1003
- const candidates = [
1004
- path.join(dir, 'node_modules', '@deepseek-ai', 'dsh-web-frontend', 'package.json'),
1005
- path.join(dir, '..', 'dsh-web-frontend', 'package.json'),
1006
- ];
1007
- for (const candidate of candidates) {
1008
- try {
1009
- const manifest = JSON.parse(fs.readFileSync(candidate, 'utf8'));
1010
- if (typeof manifest.version === 'string') return manifest.version;
1011
- } catch {
1012
- // 下一个候选
1013
- }
1014
- }
1015
- return null;
1016
- } catch {
1017
- return null;
1018
- }
1019
- }
1020
-
1021
991
  /** 本次启动生成的新桥令牌;null = 本实例没有令牌(adopt 旧实例时会回读 bridge.json)。 */
1022
992
  let bridgeToken = null;
1023
993
  /** 桥的本机 IPC 监听口(0.3.13)。null = 未起(未启用/起失败)。*/
@@ -1084,15 +1054,17 @@ export async function apply(ctx, config) {
1084
1054
 
1085
1055
  /** 桥的路由(后缀 → Fetch 风格 handler)。做成函数而非常量:handler 声明在文件后段,
1086
1056
  * 函数声明会提升,而常量在挂载时可能还在 TDZ。
1087
- * `/approve` 是唯一的非只读路由(只回答既有授权请求,见 handleBridgeApprove)。 */
1057
+ *
1058
+ * **四条路由全部只读**:/health 探活、/sync 上报状态+取事件、/old 取写前原文、/event 上报意图
1059
+ * (请宿主打开对话框等)。提问与授权**不在这里** —— 它们走 DSH 同源的
1060
+ * `/api/code-server/ask/{send,approve}`,调用方是 DSH 页面里的插件客户端(client 半部),
1061
+ * 吃 DSH 自己的 cookie/Origin 校验。 */
1088
1062
  function bridgeRouteTable() {
1089
1063
  return [
1090
1064
  { suffix: '/health', methods: ['GET'], fetch: handleBridgeHealth },
1091
1065
  { suffix: '/sync', methods: ['POST'], fetch: handleBridgeSync },
1092
1066
  { suffix: '/old', methods: ['GET'], fetch: handleBridgeOld },
1093
- { suffix: '/ask', methods: ['POST'], fetch: handleBridgeAsk },
1094
1067
  { suffix: '/event', methods: ['POST'], fetch: handleBridgeEvent },
1095
- { suffix: '/approve', methods: ['POST'], fetch: handleBridgeApprove },
1096
1068
  ];
1097
1069
  }
1098
1070
 
@@ -1159,12 +1131,12 @@ export async function apply(ctx, config) {
1159
1131
  });
1160
1132
 
1161
1133
  // 会话流与授权拦截都是**按会话**建立的(在 /ask 成功时 watch + intercept),
1162
- // 这里只登记卸载钩子:面板关掉 / IDE 停掉时把订阅与 pending 授权一起收干净。
1134
+ // 这里只登记卸载钩子:对话框关掉 / IDE 停掉时把订阅与 pending 授权一起收干净。
1163
1135
  bridgeAnswerDispose = () => {
1164
1136
  bridgeThread.dispose();
1165
1137
  bridgeApproval.dispose();
1166
1138
  bridgeApprovalBoard.clear();
1167
- bridgeWatch.ids = [];
1139
+ bridgeWatch.dialogIds = [];
1168
1140
  bridgeWatch.at = 0;
1169
1141
  };
1170
1142
 
@@ -1518,7 +1490,7 @@ export async function apply(ctx, config) {
1518
1490
 
1519
1491
  /** 已运行实例切换工作区:**只改"当前 workbench 目录",不重启进程**。
1520
1492
  *
1521
- * 为什么够用:工作区目录由客户端 URL 的 `&folder=<绝对路径>` 决定(见 src/factory.js 的
1493
+ * 为什么够用:工作区目录由客户端 URL 的 `&folder=<绝对路径>` 决定(见 lib/client.js 的
1522
1494
  * buildPageUrl),服务进程自己的 cwd 只影响它 spawn 时对相对路径的解析;真正"换目录"是
1523
1495
  * workbench 拿新 folder 重新导航(客户端导航 iframe)。
1524
1496
  * 旧行为(0.2.11 及以前)在这里 stop('restart') + 重新 launch:切一次工作区就整进程重启,
@@ -2021,17 +1993,7 @@ export async function apply(ctx, config) {
2021
1993
  // 把当前最大 seq 一并回去:客户端据此自查"我的游标是不是跑到宿主前面去了"(跨版本升级、
2022
1994
  // 接管了别的宿主留下的游标文件等),发现后退回 since=0 重新对齐,免得永久失聪。
2023
1995
  const lastSeq = bridgeEvents.lastSeq();
2024
- // 面板声明它要看哪些会话(数组;空 = 关掉了)。据此对齐订阅 + 判断"面板在不在看"
2025
- // (授权拦截只在面板看着的时候才抢答,否则一律交给官方链路)。
2026
- const watch = Array.isArray(body.watch) ? body.watch.filter((id) => typeof id === 'string' && id !== '') : [];
2027
- bridgeWatch.ids = watch.slice(0, 4);
2028
- bridgeApprovalsUi = body.approvalsUi === true;
2029
- if (bridgeApprovalsUi !== bridgeApprovalsUiLogged) {
2030
- bridgeApprovalsUiLogged = bridgeApprovalsUi;
2031
- bridgeDiag('approvalsUi → ' + bridgeApprovalsUi + '(客户端可否显示授权卡片)');
2032
- }
2033
1996
  bridgeWatch.at = Date.now();
2034
- bridgeThread.sync(watchedSessions());
2035
1997
  // 看护状态变化就写一行;并且**首帧必写**(0.3.31:只记"翻转"会让"一直没人看"这种情况在日志里
2036
1998
  // 完全空白,事后无从判断 —— 上一轮就是这样被拖了一整轮)。
2037
1999
  const watchingNow = hasWatcher();
@@ -2039,118 +2001,30 @@ export async function apply(ctx, config) {
2039
2001
  bridgeWatchLogged = watchingNow;
2040
2002
  bridgeWatchWroteFirst = true;
2041
2003
  bridgeDiag('hasWatcher → ' + watchingNow
2042
- + ' (面板=[' + bridgeWatch.ids.join(',') + '] 对话框=[' + bridgeWatch.dialogIds.join(',') + ']'
2043
- + ' approvalsUi=' + bridgeApprovalsUi + ')');
2004
+ + ' (对话框=[' + bridgeWatch.dialogIds.join(',') + '])');
2044
2005
  }
2045
2006
  // 请求过对话框、客户端半部却从没来问过 → 说清楚原因(多半是浏览器还缓存着旧的客户端 bundle)。
2046
2007
  if (askDialog.open && !askDialogLive() && askDialog.warned !== true) {
2047
2008
  askDialog.warned = true;
2048
2009
  console.warn('[code-server] 请求了「问 DSH」对话框,但客户端半部没有轮询 /ask/state:'
2049
- + '浏览器可能还在用旧的客户端 bundle(硬刷新 / 清 Code Cache)。'
2050
- + '此期间提问会退回编辑器面板,授权也在面板里批。');
2010
+ + '浏览器可能还在用旧的客户端 bundle(硬刷新 / 清 Code Cache)。');
2051
2011
  }
2052
- // **对话流只在变化时回传**(0.3.27):以前每趟 /sync 都塞整份快照(≤120 条 × ≤8KB,可达 ~1MB / 600ms),
2053
- // 实测把 /sync 拖成 3 秒超时 —— 扩展收不到响应 ⇒ 面板看不到 approvals ⇒ 授权卡片永远不出现
2054
- // (卡片在宿主里干等到窗口结束才交给 DSH 界面)。现在给修订号,没变化就不重传。
2055
- const threadRev = bridgeThread.rev(watchedSessions());
2056
- const snapshot = bridgeThread.supported()
2057
- ? bridgeThread.snapshot(watchedSessions())
2058
- : { available: false, sessionId: null, cursor: null, rev: 0, entries: [], error: '宿主没有 sessionController.follow(DSH 版本过旧)' };
2059
- // **按"客户端已经拿到哪一版"决定发不发**(0.3.29 修):0.3.28 比的是宿主自己上次发过的版本,
2060
- // 于是"面板是后开的"这种情形下宿主认为没变化、什么都不发,面板手里一份快照都没有 ⇒ 旧模型把它
2061
- // 当成"宿主没有对话流能力"(用户实测到的那条误报)。现在由客户端用 threadRev 声明自己持有的版本;
2062
- // 不带这个字段的旧扩展一律收整份(不省流量,但绝不误报)。
2063
- const peerRev = Number.isSafeInteger(body.threadRev) ? body.threadRev : null;
2064
- const threadChanged = peerRev === null || peerRev !== threadRev || snapshot.available === false;
2065
- bridgeThreadRevSent = threadRev;
2066
- // 对话流走独立字段:每个会话给有界条目(见 lib/bridge-thread.mjs),
2067
- // 授权待决列表也一并回去(面板显示卡片)。
2012
+ // **对话流不再经这里**(0.3.59):那是编辑器 webview 面板时代的分工 —— 面板退役后这条
2013
+ // 每 600ms 一趟的通道上驮着 ≤1MB 的快照(0.3.27 还专门为它做过"没变化就不重传"的省流量),
2014
+ // 现在全归 `/api/code-server/ask/state`:对话框每 900ms 只问修订号,没变化只回一个数字。
2068
2015
  return jsonResponse({
2069
2016
  ok: true,
2070
2017
  events,
2071
2018
  // 事件序号的高水位:扩展用它自查游标是否超前(超前 ⇒ 退回 since=0 重新对齐,避免永久失聪)。
2072
2019
  lastSeq,
2073
- // thread 缺省 = 没变化(扩展沿用上一份);threadRev 用来区分"没变化"与"旧宿主没有这个字段"。
2074
- threadRev,
2075
- thread: threadChanged ? snapshot : undefined,
2076
- approvals: bridgeApprovalBoard.snapshot(),
2077
- // 授权卡片要在面板上倒计时,窗口长度由 host 给(面板不猜)。
2078
- approvalHoldMs: DEFAULT_HOLD_MS,
2079
- dshVersion: dshVersion(),
2080
- // 界面版本:面板据此提示"内置渲染器与当前 DSH 界面版本不一致"。
2081
- uiVersion: uiVersion(),
2082
- // 能力探测(0.3.24;0.3.25 起以"客户端半部真的在轮询"为准):
2083
- // 只有对话框活着的时候才让扩展走 ask-open;否则扩展开编辑器面板(那里也能批授权)。
2020
+ // 能力探测(0.3.24;0.3.25 起以"客户端半部真的在轮询 /ask/state"为准):
2021
+ // 只有对话框活着的时候才让扩展走 ask-open;0.3.59 起不满足时扩展给一条提示(不再退编辑器面板)。
2084
2022
  askDialog: askDialogLive(),
2085
2023
  // 诊断用:请求过对话框但客户端没响应(浏览器缓存旧 bundle?)。
2086
2024
  askDialogRequested: askDialog.open,
2087
2025
  });
2088
2026
  }
2089
2027
 
2090
- async function handleBridgeAsk(request) {
2091
- const denied = bridgeRejection(request);
2092
- if (denied !== null) return denied;
2093
- let body;
2094
- try {
2095
- body = await readJsonBody(request);
2096
- } catch {
2097
- return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
2098
- }
2099
- const text = typeof body.text === 'string' ? body.text : '';
2100
- if (text === '') return jsonResponse({ ok: false, error: '需要 text 字段(要问 DSH 的话)' }, 400);
2101
- const file = typeof body.file === 'string' && body.file !== '' ? body.file : null;
2102
- const lineStart = Number.isSafeInteger(body.lineStart) ? body.lineStart : null;
2103
- const lineEnd = Number.isSafeInteger(body.lineEnd) ? body.lineEnd : null;
2104
- const selection = typeof body.selection === 'string' && body.selection !== '' ? body.selection : null;
2105
- // 交给 bridge-session.mjs(agent 投递);它内部特性探测 agents,缺失时返回
2106
- // {ok:false, code:'NO_AGENT'} —— 这里原样透传,并把 NO_AGENT 映射成 409。
2107
- const result = await deliverEditorPrompt(ctx, {
2108
- text,
2109
- file,
2110
- lineStart,
2111
- lineEnd,
2112
- selection,
2113
- languageId: typeof body.languageId === 'string' ? body.languageId : null,
2114
- });
2115
- // 会话流 + 授权拦截都在这里建立(只针对**被问过的**那个会话,agent 作用域):
2116
- // - thread:面板像 DSH 对话那样显示"新内容"(见 lib/bridge-thread.mjs);
2117
- // - approval:该会话的敏感动作(工作区外写入 / 命令执行)先在面板里问一句,
2118
- // 8 秒没人答就交给官方链路(见 lib/bridge-approval.mjs)。
2119
- if (result.ok === true && typeof result.sessionId === 'string' && result.sessionId !== '') {
2120
- const ok = bridgeThread.watch(result.sessionId);
2121
- if (!ok) console.warn('[code-server] 编辑器桥:会话订阅未建立(宿主没有 sessionController.follow)');
2122
- bridgeApproval.intercept(result.agent, result.sessionId);
2123
- }
2124
- // `agent` 是内部句柄,不能进 JSON。
2125
- const { agent: _agent, ...payload } = result;
2126
- void _agent;
2127
- return jsonResponse(payload, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
2128
- }
2129
-
2130
- /**
2131
- * 面板对授权请求的决策(0.3.22)。
2132
- *
2133
- * **这是命名空间里唯一的非只读路由**,约束写死在实现里:
2134
- * - 只能回答**本 host 发起、仍未决**的请求(单次使用,用后即废);
2135
- * - outcome 只接受 `allowed-once` / `rejected`;
2136
- * - 不接受任何自由文本、路径或命令参数 —— 它只能"回答问题",不能"发起动作"。
2137
- */
2138
- async function handleBridgeApprove(request) {
2139
- const denied = bridgeRejection(request);
2140
- if (denied !== null) return denied;
2141
- let body;
2142
- try {
2143
- body = await readJsonBody(request);
2144
- } catch {
2145
- return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
2146
- }
2147
- const verdict = answerApproval(
2148
- typeof body.id === 'string' ? body.id : '',
2149
- typeof body.outcome === 'string' ? body.outcome : '',
2150
- );
2151
- return jsonResponse(verdict.body, verdict.status);
2152
- }
2153
-
2154
2028
  // ---------------------------------------------------------------- 「问 DSH」对话框(client 半部)
2155
2029
 
2156
2030
  /** 对话框每 900ms 问一次:给修订号,没变就只回一个数字(不重传几十 KB 的对话流)。 */
@@ -2193,6 +2067,9 @@ export async function apply(ctx, config) {
2193
2067
  if (text === '') return jsonResponse({ ok: false, error: '需要 text 字段(要问 DSH 的话)' }, 400);
2194
2068
  const mode = body.mode === 'file' ? 'file' : askDialog.mode;
2195
2069
  const context = askContextFromCache(mode);
2070
+ // 投递方式(0.3.59):默认跟着 DSH 自己的设置 `ui-conversation.busyEnter` 走(见
2071
+ // lib/bridge-session.mjs 的 pickBusyEnter)—— 面板按 Enter 与主界面按 Enter 是同一个手势,
2072
+ // 行为必须一致。显式传 `delivery: 'queue'|'steer'` 可覆盖(测试用,或将来在面板上做"插队"按钮)。
2196
2073
  const result = await deliverEditorPrompt(ctx, {
2197
2074
  text,
2198
2075
  file: context === null ? null : context.file,
@@ -2200,7 +2077,7 @@ export async function apply(ctx, config) {
2200
2077
  lineEnd: context === null ? null : context.lineEnd,
2201
2078
  selection: context === null ? null : context.selection,
2202
2079
  languageId: context === null ? null : context.languageId,
2203
- });
2080
+ }, { delivery: typeof body.delivery === 'string' ? body.delivery : undefined });
2204
2081
  if (result.ok === true && typeof result.sessionId === 'string' && result.sessionId !== '') {
2205
2082
  askDialog.sessionId = result.sessionId;
2206
2083
  askDialog.mode = mode;
@@ -2213,7 +2090,7 @@ export async function apply(ctx, config) {
2213
2090
  return jsonResponse(payload, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
2214
2091
  }
2215
2092
 
2216
- /** 对话框回答授权(与桥的 /approve 同一套校验)。 */
2093
+ /** 对话框回答授权(与宿主内部 answerApproval 同一套校验:白名单 outcome + 只认仍未决的 id)。 */
2217
2094
  async function handleAskApprove(request) {
2218
2095
  let body;
2219
2096
  try {
@@ -2239,15 +2116,6 @@ export async function apply(ctx, config) {
2239
2116
  return jsonResponse({ ok: true });
2240
2117
  }
2241
2118
 
2242
- /** 面板产物(thread.js / thread.css)给对话框注入用。 */
2243
- async function handleAskBundle() {
2244
- const bundle = askBundle();
2245
- if (bundle === null) {
2246
- return jsonResponse({ ok: false, error: '面板产物缺失:请在插件源码里跑 pnpm run build:webview,或重装插件' }, 503);
2247
- }
2248
- return jsonResponse({ ok: true, rev: askRevision(), ...bundle });
2249
- }
2250
-
2251
2119
  async function handleBridgeEvent(request) {
2252
2120
  const denied = bridgeRejection(request);
2253
2121
  if (denied !== null) return denied;
@@ -2283,7 +2151,7 @@ export async function apply(ctx, config) {
2283
2151
  { path: `${API_BASE}/ask/send`, methods: ['POST'], fetch: handleAskSend },
2284
2152
  { path: `${API_BASE}/ask/approve`, methods: ['POST'], fetch: handleAskApprove },
2285
2153
  { path: `${API_BASE}/ask/close`, methods: ['POST'], fetch: handleAskClose },
2286
- { path: `${API_BASE}/ask/bundle`, methods: ['GET'], fetch: handleAskBundle },
2154
+ // 0.3.59 删掉了 `/ask/bundle`(面板产物不再存在:面板本体在 lib/client.js 里,渲染器走页面模块表)。
2287
2155
  // ---- 编辑器桥的 4 条路由**不在 /api 下**(0.3.9 修正)----
2288
2156
  // 原因:Connection 给 `/api` 装了 cookie fence(无 cookie → 401),而扩展宿主是 Node 进程、
2289
2157
  // 永远拿不到浏览器 cookie ⇒ 挂在这里的路由根本到不了(实测:带令牌也被 401/405 挡回)。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code-server-app",
3
- "version": "0.3.57",
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); since 0.3.23 it is a full-width dialog with collapsible thinking rows 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; panel-first for 5 minutes, handed back to the official card the moment the panel closes). 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). Since 0.3.55 the diff the bridge opens always has the real pre-write text on the left, even for files that are not open in the editor: the host reads the execution-local result.value.before in tools/post-execute (the durable tools/result projection deliberately omits value) into a bounded snapshot cache and the extension fetches it by opaque key over the read-only GET /old route; paths are resolved against the session cwd first, so relative file_path arguments are no longer dropped by the editor's workspace check. Since 0.3.56 the bridge actually delivers every diff: the event ring's sequence is monotonic for the lifetime of a host process (0.3.9-0.3.55 rewound it after every /sync, so at most one agent-edit per IDE session was ever delivered), /sync returns lastSeq, and the extension re-aligns its cursor by itself when it notices it ran ahead. No code-server Node layer, no argon2, no C++ toolchain.",
3
+ "version": "0.3.59",
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), plus an editor bridge (assets/extensions/dshcs-editor-bridge) that gives the agent what only the editor knows (unsaved buffers, language-server diagnostics, the active selection) and lets editor gestures drive the session. The bridge is completely read-only: all four of its routes (/health, /sync, /old, /event) are reads. Asking DSH from the editor opens a floating dialog inside the DSH page (draggable, resizable, bottom-right): the plugin's client half sends the question as user input (POST /api/code-server/ask/send) and renders that session's new content with DSH's own Markdown renderer - the same @deepseek-ai/dsh-client-ui-primitives instance the UI uses, required from the page's module table, so typography, highlighting and math match the interface and a renderer version mismatch is impossible. Thinking appears as collapsed rows and approval requests (writing outside the workspace / running commands) can be answered in place: whole-plugin write route POST /api/code-server/ask/approve, constrained to pending in-process requests and the outcomes allowed-once/rejected; dialog-first for 5 minutes, handed back to the official card the moment the dialog closes, never auto-approved. There is no build step anywhere: lib/client.js (client half including the dialog panel) and the extension are committed hand-written source, devDependencies holds a single test-only entry, and the published package carries no frontend bundle. Also: the tab claims DSH file addresses (dsh-resource://file/**) by file type (setting claimExtensions), so 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 does not reload it; opening the tab switches the right sidebar to fullscreen by default (setting fullscreenOnOpen); a workspace switch re-navigates the workbench with the new ?folder= without restarting the IDE process. Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions only get an upgrade notice on the settings page. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). The diff the bridge opens always has the real pre-write text on the left, taken from result.value.before in tools/post-execute into a bounded snapshot cache and fetched by opaque key over the read-only GET /old, so files that are not open in the editor work too. 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",
@@ -67,30 +67,7 @@
67
67
  "README.en.md"
68
68
  ],
69
69
  "devDependencies": {
70
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.1",
71
- "@deepseek-ai/dsh-client-ui-theme": "0.1.6-alpha.1",
72
- "@deepseek-ai/schemastery": "3.18.2",
73
- "@shikijs/langs": "4.3.1",
74
- "anser": "2.3.5",
75
- "clsx": "2.0.0",
76
- "diff": "9.0.0",
77
- "esbuild": "^0.25.0",
78
- "katex": "0.16.47",
79
- "mdast-util-from-markdown": "2.0.3",
80
- "mdast-util-gfm": "3.1.0",
81
- "mdast-util-math": "3.0.0",
82
- "micromark-core-commonmark": "2.0.3",
83
- "micromark-extension-gfm": "3.0.0",
84
- "micromark-extension-math": "3.1.0",
85
- "micromark-factory-space": "2.0.1",
86
- "micromark-util-character": "2.1.1",
87
- "micromark-util-classify-character": "2.0.1",
88
- "micromark-util-sanitize-uri": "2.0.1",
89
- "micromark-util-symbol": "2.0.1",
90
- "micromark-util-types": "2.0.2",
91
- "react": "18.3.1",
92
- "react-dom": "18.3.1",
93
- "shiki": "4.3.1"
70
+ "@deepseek-ai/schemastery": "3.18.2"
94
71
  },
95
72
  "license": "MIT",
96
73
  "dependencies": {
@@ -183,8 +160,6 @@
183
160
  "publish:repacks": "node scripts/publish-repacks.mjs",
184
161
  "publish:plugin": "node scripts/publish-plugin.mjs",
185
162
  "promote": "node scripts/promote.mjs",
186
- "build:client": "node scripts/build-client.mjs",
187
- "build:webview": "node scripts/build-webview.mjs",
188
163
  "test:apply": "node scripts/test-plugin-apply.mjs",
189
164
  "test:launcher-routes": "node scripts/test-launcher-routes.mjs",
190
165
  "test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
@@ -193,6 +168,8 @@
193
168
  "test:workspace-cwd": "node scripts/test-workspace-cwd.mjs",
194
169
  "test:client-cwd": "node scripts/test-client-bundle-cwd.mjs",
195
170
  "test:client-tabs": "node scripts/test-client-bundle-tabs.mjs",
171
+ "test:client-entry": "node scripts/test-client-entry.mjs",
172
+ "test:ask-panel": "node scripts/test-ask-panel-inline.mjs",
196
173
  "test:client-seat": "node scripts/test-client-settings-seat.mjs",
197
174
  "test:dsh-resolve": "node scripts/test-dsh-resolve.mjs",
198
175
  "test:child-node": "node scripts/test-child-node.mjs",
@@ -200,7 +177,7 @@
200
177
  "test:bridge-routes": "node scripts/test-bridge-routes.mjs",
201
178
  "test:edit-snapshot": "node scripts/test-edit-snapshot.mjs",
202
179
  "test:bridge-extension": "node scripts/test-bridge-extension.mjs",
203
- "test:webview": "node scripts/test-webview-bundle.mjs",
180
+ "test:ask-dialog": "node scripts/test-ask-dialog.mjs",
204
181
  "test:vendored": "node scripts/test-vendored-table.mjs"
205
182
  }
206
183
  }
@@ -3,7 +3,7 @@
3
3
  "vscodeVersion": "1.137.0",
4
4
  "productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
5
5
  "layout": "vscode-only",
6
- "preparedAt": "2026-09-19T12:45:33.503Z",
6
+ "preparedAt": "2026-09-20T15:36:43.387Z",
7
7
  "source": "registry",
8
8
  "node": "v24.20.0",
9
9
  "platform": "linux",