dsh-code-server-app 0.2.14 → 0.3.6

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
@@ -38,6 +38,21 @@ import {
38
38
  import { aliasNodePathDirs, ensureRuntimeLayout, resolveRuntime, runtimePackageName, verifyNatives } from './native.js';
39
39
  import { MOUNT_PATH, mountOnWebServer } from './serve-dsh.mjs';
40
40
  import { DEFAULT_CLAIM_EXTENSIONS, describeClaimPolicy, normalizeClaimExtensions } from './claim-types.js';
41
+ import {
42
+ BRIDGE_DIRNAME,
43
+ bodyWithinLimit,
44
+ bridgeGuard,
45
+ bridgeUrl,
46
+ createContextCache,
47
+ createEventRing,
48
+ mintBridgeToken,
49
+ readBridgeConfig,
50
+ removeBridgeConfig,
51
+ writeBridgeConfig,
52
+ } from './bridge.mjs';
53
+ import { registerEditorPrompt, registerEditorTools, setPromptLiveProbe } from './bridge-tools.mjs';
54
+ import { deliverEditorPrompt } from './bridge-session.mjs';
55
+ import { registerBridgeObserver } from './bridge-observe.mjs';
41
56
 
42
57
  // schemastery 由 DSH 部署自带(官方核心依赖),仿 auto-open-web 的解析策略:
43
58
  // 常规 import 优先,不可用时回退到全局 npm 布局的 DSH 部署副本。
@@ -88,6 +103,11 @@ export const Config = z.object({
88
103
  * 客户端把右侧栏切到全屏(铺满窗口);false 则保持 DSH 默认的 push(与对话并排)。
89
104
  * 只影响"打开那一刻":之后用户点「退出全屏」不会被抢回去。 */
90
105
  fullscreenOnOpen: z.boolean().default(true),
106
+ /** editorBridge=true(默认):启用「编辑器桥」(0.3.0)—— 树内扩展 dshcs-editor-bridge 与
107
+ * host 之间建立**只读**通道,给 agent 提供 only-the-editor-knows 的上下文(未保存缓冲区、
108
+ * 诊断、活动选区),并让编辑器里的动作能驱动 DSH。关闭后不写 bridge.json、不注册编辑器工具,
109
+ * 扩展会休眠(它读不到配置就不做任何事)。安全模型见 lib/bridge.mjs 顶部。 */
110
+ editorBridge: z.boolean().default(true),
91
111
  });
92
112
 
93
113
  const DEFAULT_CONFIG = {
@@ -102,6 +122,8 @@ const DEFAULT_CONFIG = {
102
122
  extensionsDir: '',
103
123
  locale: '',
104
124
  readyTimeoutMs: 60000,
125
+ /** 编辑器桥(0.3.0):见 Config.editorBridge。行配置与设置文档都可关;设置卡片暂不提供行。 */
126
+ editorBridge: true,
105
127
  };
106
128
 
107
129
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
@@ -159,56 +181,72 @@ function launcherPath() {
159
181
  return path.join(path.dirname(fileURLToPath(import.meta.url)), 'launcher.mjs');
160
182
  }
161
183
 
162
- /** 扩展安装目标:VS Code「内置扩展」目录 = <树>/lib/vscode/extensions
163
- * (位于程序内置目录的扩展被 VS Code 视为内置——用户视图显示为"内置",不可卸载;
164
- * --extensions-dir 的是用户级扩展,可被用户禁用/卸载。)
165
- * 返回 { dst, builtin }——builtin=true 时为核心路径;找不到树则回退用户级。 */
166
- function extensionTarget(extensionsDir) {
167
- try {
168
- const root = vsRoot();
169
- if (root !== null) {
170
- const vscodeExt = path.join(root, 'lib', 'vscode', 'extensions');
171
- if (fs.existsSync(vscodeExt)) {
172
- return { dst: path.join(vscodeExt, 'dshcs-open-file'), builtin: true };
184
+ /** 扩展安装目标:`placement: 'builtin'` 时优先 VS Code「内置扩展」目录 = <树>/lib/vscode/extensions
185
+ * (位于程序内置目录的扩展被 VS Code 视为内置——用户视图显示为"内置",**不能卸载**);
186
+ * `placement: 'user'` 时只用 --extensions-dir(用户级扩展,可被用户禁用/卸载)
187
+ * 返回 { dst, builtin }——builtin=true 时是核心路径;找不到树时回退用户级。 */
188
+ function extensionTarget(extensionsDir, name, placement = 'builtin') {
189
+ if (placement === 'builtin') {
190
+ try {
191
+ const root = vsRoot();
192
+ if (root !== null) {
193
+ const vscodeExt = path.join(root, 'lib', 'vscode', 'extensions');
194
+ if (fs.existsSync(vscodeExt)) {
195
+ return { dst: path.join(vscodeExt, name), builtin: true };
196
+ }
173
197
  }
174
- }
175
- } catch { /* fall through */ }
176
- return { dst: path.join(extensionsDir, 'dshcs-open-file'), builtin: false };
198
+ } catch { /* fall through */ }
199
+ }
200
+ return { dst: path.join(extensionsDir, name), builtin: false };
177
201
  }
178
202
 
179
- /** 内置扩展安装:dshcs-open-file(host 信号文件 → VS Code 打开文件,可带行号)。
180
- * 优先装进 code-server 内置扩展目录(不可卸载);同时清理用户级旧副本。
181
- * 每次启动调用:缺失**或内容有变化**即同步(自愈,且插件升级后能更新已装的旧副本) */
182
- function installBundledExtension(extensionsDir, userDataDir) {
183
- try {
184
- const here = path.dirname(fileURLToPath(import.meta.url));
185
- const src = path.join(here, '..', 'assets', 'extensions', 'dshcs-open-file');
186
- if (!fs.existsSync(path.join(src, 'package.json'))) return;
187
- const target = extensionTarget(extensionsDir);
188
- const dst = target.dst;
189
- const files = ['package.json', 'extension.js'];
190
- const stale = files.filter((name) => {
191
- const to = path.join(dst, name);
192
- if (!fs.existsSync(to)) return true;
193
- try {
194
- return fs.readFileSync(path.join(src, name), 'utf8') !== fs.readFileSync(to, 'utf8');
195
- } catch {
196
- return true;
203
+ /** 树内自带扩展清单。
204
+ * - dshcs-open-file:host 信号文件 → VS Code 打开文件(可带行号)。**内置**(用户不需要看到它)。
205
+ * - dshcs-editor-bridge(0.3.0):编辑器桥的扩展侧。**必须用户级** —— 它是可选能力,
206
+ * 用户得有办法一键禁用;内置扩展按 VS Code 语义不能禁用。代价是用户可能卸载它,
207
+ * 这没关系:host 侧本来就按"桥不可用"降级。 */
208
+ const BUNDLED_EXTENSIONS = [
209
+ { name: 'dshcs-open-file', placement: 'builtin' },
210
+ { name: 'dshcs-editor-bridge', placement: 'user' },
211
+ ];
212
+
213
+ /** 内置扩展安装:每次启动调用 —— 缺失**或内容有变化**即同步(自愈,且插件升级后能更新已装的旧副本)。
214
+ * 同时清理"放错位置"的旧副本(用户级/内置两份同时存在会让 VS Code 打架) */
215
+ function installBundledExtensions(extensionsDir, userDataDir) {
216
+ const here = path.dirname(fileURLToPath(import.meta.url));
217
+ for (const ext of BUNDLED_EXTENSIONS) {
218
+ try {
219
+ const src = path.join(here, '..', 'assets', 'extensions', ext.name);
220
+ const manifest = path.join(src, 'package.json');
221
+ if (!fs.existsSync(manifest)) continue;
222
+ const files = ['package.json', 'extension.js'];
223
+ const target = extensionTarget(extensionsDir, ext.name, ext.placement);
224
+ const dst = target.dst;
225
+ const stale = files.filter((name) => {
226
+ const to = path.join(dst, name);
227
+ if (!fs.existsSync(to)) return true;
228
+ try {
229
+ return fs.readFileSync(path.join(src, name), 'utf8') !== fs.readFileSync(to, 'utf8');
230
+ } catch {
231
+ return true;
232
+ }
233
+ });
234
+ if (stale.length > 0) {
235
+ fs.mkdirSync(dst, { recursive: true });
236
+ for (const name of stale) fs.copyFileSync(path.join(src, name), path.join(dst, name));
197
237
  }
198
- });
199
- if (stale.length > 0) {
200
- fs.mkdirSync(dst, { recursive: true });
201
- for (const name of stale) fs.copyFileSync(path.join(src, name), path.join(dst, name));
202
- }
203
- // 清理用户级旧副本(避免重复/可卸载副本)
204
- const legacy = path.join(extensionsDir, 'dshcs-open-file');
205
- if (legacy !== dst && fs.existsSync(legacy)) {
206
- fs.rmSync(legacy, { recursive: true, force: true });
238
+ // 清理放错位置的副本:dshcs-open-file 的旧用户级副本 / 编辑器桥的错误内置副本。
239
+ const wrong = extensionTarget(extensionsDir, ext.name, ext.placement === 'builtin' ? 'user' : 'builtin');
240
+ if (wrong.dst !== dst && fs.existsSync(wrong.dst)) {
241
+ fs.rmSync(wrong.dst, { recursive: true, force: true });
242
+ console.log(`[code-server] 清理放错位置的扩展副本 ${wrong.dst}(${ext.name} 应装在${target.builtin ? '内置' : '用户级'}目录)`);
243
+ }
244
+ console.log(`[code-server] bundled extension ${ext.name} -> ${dst}${target.builtin ? ' (内置,不可卸载)' : ' (用户级,可禁用)'}${stale.length > 0 ? ` [更新 ${stale.join(',')}]` : ''}`);
245
+ } catch (err) {
246
+ console.warn(`[code-server] bundled extension ${ext.name} install failed:`, err && err.message ? err.message : String(err));
207
247
  }
208
- console.log(`[code-server] bundled extension dshcs-open-file -> ${dst}${target.builtin ? ' (内置,不可卸载)' : ' (用户级回退)'}${stale.length > 0 ? ` [更新 ${stale.join(',')}]` : ''}`);
209
- } catch (err) {
210
- console.warn('[code-server] bundled extension install failed:', err && err.message ? err.message : String(err));
211
248
  }
249
+ void userDataDir;
212
250
  }
213
251
 
214
252
  /** 打开文件信号文件:<user-data>/User/dshcs-open.json(扩展轮询此文件)。 */
@@ -429,6 +467,8 @@ export async function apply(ctx, config) {
429
467
  let keepResident = true; // 客户端常驻预热(0.2.2)
430
468
  let claimExtensions = DEFAULT_CLAIM_EXTENSIONS; // 认领类型清单(0.2.11,取代 fileOpenScope)
431
469
  let fullscreenOnOpen = true; // 客户端"打开标签即全屏右侧栏"(0.2.9)
470
+ /** 编辑器桥开关(0.3.0):行配置 cfg.editorBridge 是种子,设置文档里可改并实时生效。 */
471
+ let bridgeSetting = cfg.editorBridge !== false;
432
472
  if (settingsSvc !== undefined && typeof settingsSvc.register === 'function') {
433
473
  try {
434
474
  const scope = settingsSvc.register(SETTINGS_NS, Config);
@@ -441,6 +481,7 @@ export async function apply(ctx, config) {
441
481
  ? normalizeClaimExtensions(resolved.claimExtensions)
442
482
  : DEFAULT_CLAIM_EXTENSIONS;
443
483
  fullscreenOnOpen = resolved && typeof resolved.fullscreenOnOpen === 'boolean' ? resolved.fullscreenOnOpen : true;
484
+ bridgeSetting = resolved && typeof resolved.editorBridge === 'boolean' ? resolved.editorBridge : bridgeSetting;
444
485
  }
445
486
  scope.watch((next) => {
446
487
  if (next != null && (next.serve === 'dsh' || next.serve === 'loopback')) {
@@ -449,6 +490,19 @@ export async function apply(ctx, config) {
449
490
  console.log(`[code-server] serve updated: ${serveSetting}(下次启动生效)`);
450
491
  }
451
492
  }
493
+ if (next != null && typeof next.editorBridge === 'boolean') {
494
+ if (next.editorBridge !== bridgeSetting) {
495
+ bridgeSetting = next.editorBridge;
496
+ console.log(`[code-server] editorBridge updated: ${bridgeSetting}`);
497
+ // 关掉 → 立刻下线(删配置 + 注销工具);开启 → 若 IDE 在跑就补一份配置。
498
+ if (bridgeSetting === false) {
499
+ clearBridgeRuntime();
500
+ } else if (state.status === 'running' && state.serve === 'loopback' && Number.isSafeInteger(state.port) && state.port > 0) {
501
+ bridgeToken ??= mintBridgeToken();
502
+ syncBridgeRuntime({ host: cfg.host, port: state.port, pid: state.pid, startedAt: state.startedAt });
503
+ }
504
+ }
505
+ }
452
506
  if (next != null && typeof next.keepResident === 'boolean') {
453
507
  keepResident = next.keepResident;
454
508
  console.log(`[code-server] keepResident updated: ${keepResident}`);
@@ -519,6 +573,134 @@ export async function apply(ctx, config) {
519
573
  let pollTimer = null;
520
574
  let disposeKilled = false;
521
575
 
576
+ // ---- 编辑器桥(0.3.0):配置/凭据写在 <extensionsDir>/.dshcs-bridge/bridge.json ----
577
+ // 这里只算路径;是否启用还要看 cfg.editorBridge 与 setting 值。详见 lib/bridge.mjs 顶部。
578
+ const bridgeRoot = dataRoot(cfg);
579
+ const bridgeUserDataDir = cfg.userDataDir || path.join(bridgeRoot, 'user-data');
580
+ const bridgeExtensionsDir = cfg.extensionsDir || path.join(bridgeRoot, 'extensions');
581
+ const bridgeMetaDir = path.join(bridgeExtensionsDir, BRIDGE_DIRNAME);
582
+ /** 已写入的桥配置(供桥路由与 /status 读回);null = 未启用/未就绪。 */
583
+ let bridgeMeta = null;
584
+ /** 桥工具(editor_context / editor_diagnostics)的 disposer;null = 未注册。 */
585
+ let bridgeToolDispose = null;
586
+ /** 提示词段落的 disposer(只注册一次;文本按桥是否存活渲染)。 */
587
+ let bridgePromptDispose = null;
588
+ /** agent 写操作观察器的 disposer。 */
589
+ let bridgeObserveDispose = null;
590
+ /** 推给编辑器的事件环形缓冲(tools/result → 扩展轮询)。 */
591
+ const bridgeEvents = createEventRing();
592
+ /** 编辑器状态缓存:扩展在每次 /sync 里推上来,agent 的工具调用来读它。 */
593
+ const bridgeContext = createContextCache();
594
+
595
+ /** 本次启动生成的新桥令牌;null = 本实例没有令牌(adopt 旧实例时会回读 bridge.json)。 */
596
+ let bridgeToken = null;
597
+
598
+ /** 桥是否启用(行配置为种子,设置文档里可实时改)。 */
599
+ function bridgeEnabled() {
600
+ return bridgeSetting !== false;
601
+ }
602
+
603
+ /** 提示词段落在插件激活时注册一次:文本按"桥是否存活"渲染(桥停时为空串 → DSH 丢弃该段),
604
+ * 所以在 IDE 从未启动的部署里它也只是一段空注册,不产生任何提示词开销。 */
605
+ setPromptLiveProbe(() => bridgeMeta !== null);
606
+ bridgePromptDispose = registerEditorPrompt(ctx);
607
+ if (bridgePromptDispose === null && bridgeEnabled()) {
608
+ console.warn('[code-server] 编辑器桥:systemPrompt 服务不可用,提示词段落未注册(工具仍可用)');
609
+ }
610
+
611
+ // 观察 agent 的写操作(→ 编辑器 diff 提示)并在写脏文件前附一条提醒。
612
+ // 与 IDE 是否在跑无关:桥没起来时 isLive() 为 false,dirty 检查直接跳过。
613
+ bridgeObserveDispose = registerBridgeObserver(ctx, {
614
+ emit: (kind, fields) => bridgeEvents.push(kind, fields),
615
+ context: () => bridgeContext.get(),
616
+ isLive: () => bridgeMeta !== null && !bridgeContext.isStale(),
617
+ });
618
+
619
+ /** 同步桥运行时:写入/更新 bridge.json,并(就绪时)注册编辑器工具。
620
+ * 端口、令牌、pid 三者任一变化都重写 —— 扩展每 5s 重读,故不需要任何推送。 */
621
+ function syncBridgeRuntime({ host, port, pid, startedAt }) {
622
+ if (!bridgeEnabled()) return;
623
+ if (typeof host !== 'string' || !Number.isSafeInteger(port) || port <= 0) return;
624
+ bridgeToken ??= mintBridgeToken();
625
+ const base = bridgeUrl(host, port);
626
+ const changed = bridgeMeta === null || bridgeMeta.url !== base || bridgeMeta.token !== bridgeToken || bridgeMeta.pid !== pid;
627
+ bridgeMeta = { url: base, token: bridgeToken, pid, startedAt: startedAt ?? null, host, port };
628
+ try {
629
+ writeBridgeConfig(bridgeExtensionsDir, { url: base, token: bridgeToken, pid, startedAt: startedAt ?? null });
630
+ } catch (err) {
631
+ console.warn(`[code-server] 编辑器桥配置写入失败(${bridgeExtensionsDir}):${err && err.message ? err.message : err}`);
632
+ return;
633
+ }
634
+ if (changed) {
635
+ // 只打印"已启用",不打印令牌本身(与 path-token 同一决策)。
636
+ console.log(`[code-server] 编辑器桥:已启用(${base},令牌文件 ${path.join(bridgeMetaDir, 'bridge.json')})`);
637
+ }
638
+ ensureBridgeTools();
639
+ }
640
+
641
+ /**
642
+ * 接管实例时对齐桥令牌:磁盘上已有配置且基址一致 → 沿用(避免无谓轮换打乱正在运行的扩展);
643
+ * 否则 mint 新的(扩展下次轮询就会读到新的 bridge.json,一次请求的失败无所谓)。
644
+ */
645
+ function adoptBridgeRuntime(host, port, pid, startedAt) {
646
+ if (!bridgeEnabled() || typeof host !== 'string' || !Number.isSafeInteger(port) || port <= 0) return;
647
+ const existing = readBridgeConfig(bridgeExtensionsDir);
648
+ const base = bridgeUrl(host, port);
649
+ bridgeToken = existing !== null && existing.url === base && TOKEN_RE.test(String(existing.token))
650
+ ? String(existing.token)
651
+ : mintBridgeToken();
652
+ syncBridgeRuntime({ host, port, pid, startedAt });
653
+ }
654
+
655
+ /** 注销桥运行时(停止 IDE / 插件卸载):删配置 + 注销工具 + 清缓存,扩展随即休眠。 */
656
+ function clearBridgeRuntime() {
657
+ bridgeToolDispose = disposeSafely(bridgeToolDispose);
658
+ bridgeContext.clear();
659
+ bridgeEvents.reset();
660
+ if (bridgeMeta === null) return;
661
+ bridgeMeta = null;
662
+ try {
663
+ removeBridgeConfig(bridgeExtensionsDir);
664
+ } catch {
665
+ // 配置删不掉不影响正确性(扩展会因 IDE 不可达而休眠)
666
+ }
667
+ }
668
+
669
+ function disposeSafely(dispose) {
670
+ if (typeof dispose !== 'function') return null;
671
+ try {
672
+ dispose();
673
+ } catch (err) {
674
+ console.warn(`[code-server] 桥工具注销失败:${err && err.message ? err.message : err}`);
675
+ }
676
+ return null;
677
+ }
678
+
679
+ /** 桥就绪时注册编辑器工具;`tools` / `defineTool` 缺失时静默退化为"只有 HTTP 面"。 */
680
+ function ensureBridgeTools() {
681
+ if (bridgeMeta === null || bridgeToolDispose !== null) return;
682
+ Promise.resolve(registerEditorTools(ctx, {
683
+ target: () => bridgeMeta,
684
+ cache: () => bridgeContext,
685
+ }))
686
+ .then((dispose) => {
687
+ if (dispose === null) {
688
+ console.log('[code-server] 编辑器桥:工具服务不可用,仅提供 HTTP 面(editor_context/editor_diagnostics 未注册)');
689
+ return;
690
+ }
691
+ // 期间桥可能已经被停掉(IDE 退出):立刻回滚,避免留下永远不可用的工具。
692
+ if (bridgeMeta === null) {
693
+ disposeSafely(dispose);
694
+ return;
695
+ }
696
+ bridgeToolDispose = dispose;
697
+ console.log('[code-server] 编辑器桥:已注册 editor_context / editor_diagnostics');
698
+ })
699
+ .catch((err) => {
700
+ console.warn(`[code-server] 编辑器桥工具注册失败:${err && err.message ? err.message : err}`);
701
+ });
702
+ }
703
+
522
704
  // ---- DSH 同源挂载(serve=dsh):把 /code-server 注册到 DSH 自己的 webServer ----
523
705
  // webServer 只在 web profile 存在(desktop 显式禁用该行)→ 用 ctx.inject 特性检测,
524
706
  // 缺失时 requestedServe() 自动回退 loopback。
@@ -578,6 +760,15 @@ export async function apply(ctx, config) {
578
760
  claimExtensions, // 客户端据此按扩展名决定认领哪些文件(0.2.11,取代 fileOpenScope)
579
761
  fullscreenOnOpen, // 客户端据此决定"打开标签即全屏右侧栏"(0.2.9)
580
762
  sidebarUi: state.sidebarUi,
763
+ /** 编辑器桥状态(0.3.0,只读诊断面;**不含令牌** —— 令牌只在 bridge.json 里)。 */
764
+ bridge: {
765
+ enabled: bridgeEnabled(),
766
+ live: bridgeMeta !== null,
767
+ toolsRegistered: bridgeToolDispose !== null,
768
+ supported: state.serve === 'loopback',
769
+ url: bridgeMeta === null ? null : bridgeMeta.url,
770
+ file: path.join(bridgeMetaDir, 'bridge.json'),
771
+ },
581
772
  env: state.env,
582
773
  setup: {
583
774
  running: state.setup.running,
@@ -674,6 +865,7 @@ export async function apply(ctx, config) {
674
865
  await killTree(pid);
675
866
  }
676
867
  removePidFile(cfg);
868
+ clearBridgeRuntime(); // 桥在 IDE 停止时同步下线:配置删除,扩展随即休眠
677
869
  state.status = 'stopped';
678
870
  state.pid = null;
679
871
  state.cwd = null;
@@ -797,6 +989,7 @@ export async function apply(ctx, config) {
797
989
  state.launchCwd = record.launchCwd ?? record.cwd ?? null;
798
990
  state.startedAt = record.startedAt ?? null;
799
991
  state.adopted = true;
992
+ adoptBridgeRuntime(cfg.host, cfg.port, record.pid, state.startedAt);
800
993
  return snapshot();
801
994
  }
802
995
  state.status = 'error';
@@ -812,8 +1005,9 @@ export async function apply(ctx, config) {
812
1005
  fs.mkdirSync(userDataDir, { recursive: true });
813
1006
  fs.mkdirSync(extensionsDir, { recursive: true });
814
1007
 
815
- // 安装内置扩展(dshcs-open-file:host 信号文件 → VS Code 打开文件)
816
- installBundledExtension(extensionsDir, userDataDir);
1008
+ // 安装树内自带扩展(dshcs-open-file:host 信号文件 → VS Code 打开文件;
1009
+ // dshcs-editor-bridge:编辑器桥的扩展侧,0.3.0)
1010
+ installBundledExtensions(extensionsDir, userDataDir);
817
1011
 
818
1012
  const args = launch.kind === 'launcher'
819
1013
  ? [
@@ -934,11 +1128,24 @@ export async function apply(ctx, config) {
934
1128
  writeInstanceRecord();
935
1129
  console.log(`[code-server] 随机端口: ${state.port}(host=${endpoint.host}, pid=${endpoint.pid})`);
936
1130
  }
1131
+ // 编辑器桥:端口已定(loopback + 已知端口)时写入配置,扩展随即能连上来。
1132
+ // 新启动会轮换桥令牌 —— 与路径令牌同一时机(每次新启动都换)。
1133
+ // dsh 模式(无独立端口)不启用:桥的 Host 白名单假设是 127.0.0.1:<实际端口>,管道模式没有端口。
1134
+ if (serve === 'loopback' && (cfg.port !== 0 || state.port !== null)) {
1135
+ bridgeToken = mintBridgeToken();
1136
+ syncBridgeRuntime({
1137
+ host: cfg.host,
1138
+ port: state.port,
1139
+ pid: proc.pid ?? null,
1140
+ startedAt: state.startedAt,
1141
+ });
1142
+ }
937
1143
 
938
1144
  proc.on('error', (err) => {
939
1145
  if (child !== proc) return;
940
1146
  child = null;
941
1147
  removePidFile(cfg);
1148
+ clearBridgeRuntime();
942
1149
  state.status = 'error';
943
1150
  state.error = `code-server 启动失败: ${err && err.message ? err.message : String(err)}\n${state.logTail.slice(-1000)}`;
944
1151
  });
@@ -947,6 +1154,7 @@ export async function apply(ctx, config) {
947
1154
  if (child !== proc) return; // 已被 stop/dispose 接管
948
1155
  child = null;
949
1156
  removePidFile(cfg);
1157
+ clearBridgeRuntime();
950
1158
  if (disposeKilled) return;
951
1159
  state.status = 'error';
952
1160
  state.error = `code-server 意外退出${code !== null ? `(exit ${code})` : signal ? `(signal ${signal})` : ''}:\n${state.logTail.slice(-1500)}`;
@@ -1109,6 +1317,95 @@ export async function apply(ctx, config) {
1109
1317
  }
1110
1318
  }
1111
1319
 
1320
+ // ---- 编辑器桥路由(0.3.0)----
1321
+ // 这些路由**不依赖** DSH 的 cookie 认证:扩展宿主是 Node 进程,拿不到浏览器 cookie,
1322
+ // 所以自带独立令牌(见 lib/bridge.mjs 顶部的安全不变量)。命名空间永久只读。
1323
+
1324
+ /** 每个桥路由都先过 guard;返回 Response 表示拒绝,调用方直接返回它。 */
1325
+ function bridgeRejection(request) {
1326
+ return bridgeGuard(request, bridgeMeta === null ? null : bridgeMeta.token);
1327
+ }
1328
+
1329
+ /** 桥不可达时的统一 503(扩展侧没跑 / IDE 刚起还没加载扩展)。 */
1330
+ function bridgeDown(reason) {
1331
+ return jsonResponse({ ok: false, error: reason }, 503);
1332
+ }
1333
+
1334
+ async function handleBridgeHealth() {
1335
+ // 无鉴权:只回一句"桥活着吗",不含任何编辑器数据(便于重启后一眼确认)。
1336
+ return jsonResponse({
1337
+ ok: true,
1338
+ bridge: bridgeMeta !== null,
1339
+ pid: state.pid,
1340
+ url: bridgeMeta === null ? null : bridgeMeta.url,
1341
+ });
1342
+ }
1343
+
1344
+ async function handleBridgeSync(request) {
1345
+ const denied = bridgeRejection(request);
1346
+ if (denied !== null) return denied;
1347
+ let body;
1348
+ try {
1349
+ body = await readJsonBody(request);
1350
+ } catch {
1351
+ return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
1352
+ }
1353
+ if (!bodyWithinLimit(body)) return jsonResponse({ ok: false, error: '上报内容过大' }, 413);
1354
+ bridgeContext.update(body);
1355
+ // 取事件 + 推进游标:与上报同一趟来回,扩展不需要第二个定时器。
1356
+ let since = 0;
1357
+ try {
1358
+ const raw = new URL(request.url).searchParams.get('since');
1359
+ const parsed = raw === null ? 0 : Number.parseInt(raw, 10);
1360
+ if (Number.isSafeInteger(parsed) && parsed > 0) since = parsed;
1361
+ } catch {
1362
+ since = 0;
1363
+ }
1364
+ const events = bridgeEvents.since(since);
1365
+ bridgeEvents.reset();
1366
+ return jsonResponse({ ok: true, events });
1367
+ }
1368
+
1369
+ async function handleBridgeAsk(request) {
1370
+ const denied = bridgeRejection(request);
1371
+ if (denied !== null) return denied;
1372
+ let body;
1373
+ try {
1374
+ body = await readJsonBody(request);
1375
+ } catch {
1376
+ return jsonResponse({ ok: false, error: '请求体不是合法 JSON' }, 400);
1377
+ }
1378
+ const text = typeof body.text === 'string' ? body.text : '';
1379
+ if (text === '') return jsonResponse({ ok: false, error: '需要 text 字段(要问 DSH 的话)' }, 400);
1380
+ const file = typeof body.file === 'string' && body.file !== '' ? body.file : null;
1381
+ const lineStart = Number.isSafeInteger(body.lineStart) ? body.lineStart : null;
1382
+ const lineEnd = Number.isSafeInteger(body.lineEnd) ? body.lineEnd : null;
1383
+ const selection = typeof body.selection === 'string' && body.selection !== '' ? body.selection : null;
1384
+ // 交给 bridge-session.mjs(agent 投递);它内部特性探测 agents,缺失时返回
1385
+ // {ok:false, code:'NO_AGENT'} —— 这里原样透传,并把 NO_AGENT 映射成 409。
1386
+ const result = await deliverEditorPrompt(ctx, {
1387
+ text,
1388
+ file,
1389
+ lineStart,
1390
+ lineEnd,
1391
+ selection,
1392
+ languageId: typeof body.languageId === 'string' ? body.languageId : null,
1393
+ });
1394
+ return jsonResponse(result, result.ok === true ? 200 : (result.code === 'NO_AGENT' ? 409 : 200));
1395
+ }
1396
+
1397
+ async function handleBridgeEvent(request) {
1398
+ const denied = bridgeRejection(request);
1399
+ if (denied !== null) return denied;
1400
+ const body = await readJsonBody(request);
1401
+ const kind = typeof body.kind === 'string' ? body.kind : '';
1402
+ if (kind === '') return jsonResponse({ ok: false, error: '需要 kind 字段' }, 400);
1403
+ // 扩展上报的编辑器侧事件(打开/关闭文件等):目前只进日志尾,供诊断时回看。
1404
+ appendLog(`[bridge] ${kind}${typeof body.path === 'string' ? ` ${body.path}` : ''}\n`);
1405
+ return jsonResponse({ ok: true });
1406
+ }
1407
+
1408
+
1112
1409
  // 每条操作一条 exact Fetch 路由。desktop 的 assetHandler 只把 /api/* 交给
1113
1410
  // createSharedFetchHandler('/api'),所以路径必须落在 /api 下。
1114
1411
  const disposers = [ { path: `${API_BASE}/status`, methods: ['GET'], fetch: handleStatus },
@@ -1117,6 +1414,13 @@ export async function apply(ctx, config) {
1117
1414
  { path: `${API_BASE}/setup`, methods: ['POST'], fetch: handleSetup },
1118
1415
  { path: `${API_BASE}/open-file`, methods: ['POST'], fetch: handleOpenFile },
1119
1416
  { path: `${API_BASE}/ui-mode`, methods: ['POST'], fetch: handleUiMode },
1417
+ // ---- 编辑器桥(0.3.0):扩展侧的 HTTP 面。全部自带令牌鉴权,**命名空间永久只读**。----
1418
+ // 只有 4 条:扩展宿主没有 HTTP 服务器,所以"拿编辑器状态"由扩展在 /sync 里推上来,
1419
+ // host 缓存后供 agent 工具读取(见 lib/bridge.mjs 顶部的通道说明)。
1420
+ { path: `${API_BASE}/bridge/health`, methods: ['GET'], fetch: handleBridgeHealth },
1421
+ { path: `${API_BASE}/bridge/sync`, methods: ['POST'], fetch: handleBridgeSync },
1422
+ { path: `${API_BASE}/bridge/ask`, methods: ['POST'], fetch: handleBridgeAsk },
1423
+ { path: `${API_BASE}/bridge/event`, methods: ['POST'], fetch: handleBridgeEvent },
1120
1424
  ].map(route => connection.fetch.register({
1121
1425
  path: route.path,
1122
1426
  methods: route.methods,
@@ -1128,6 +1432,9 @@ export async function apply(ctx, config) {
1128
1432
  return () => {
1129
1433
  disposeKilled = true;
1130
1434
  stopPolling();
1435
+ bridgeObserveDispose = disposeSafely(bridgeObserveDispose);
1436
+ bridgePromptDispose = disposeSafely(bridgePromptDispose);
1437
+ clearBridgeRuntime();
1131
1438
  for (const d of disposers) {
1132
1439
  try {
1133
1440
  // connection.fetch.register 的 disposer 是异步的(返回 Promise);
@@ -1207,6 +1514,7 @@ export async function apply(ctx, config) {
1207
1514
  state.startedAt = record.startedAt ?? null;
1208
1515
  state.adopted = true;
1209
1516
  console.log(`[code-server] adopted running instance pid=${record.pid} port=${adoptPort}(令牌已启用)`);
1517
+ adoptBridgeRuntime(cfg.host, adoptPort, record.pid, state.startedAt);
1210
1518
  } else {
1211
1519
  removePidFile(cfg);
1212
1520
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code-server-app",
3
- "version": "0.2.14",
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). 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.6",
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.",
5
5
  "homepage": "https://github.com/jinsiyu/dsh-code-server-app",
6
6
  "repository": {
7
7
  "type": "git",
@@ -41,6 +41,11 @@
41
41
  "lib/index.js",
42
42
  "lib/client.js",
43
43
  "lib/claim-types.js",
44
+ "lib/bridge.mjs",
45
+ "lib/bridge-tools.mjs",
46
+ "lib/bridge-session.mjs",
47
+ "lib/bridge-observe.mjs",
48
+ "lib/dsh-resolve.mjs",
44
49
  "lib/launcher.mjs",
45
50
  "lib/serve-dsh.mjs",
46
51
  "lib/vendor.js",
@@ -48,6 +53,7 @@
48
53
  "assets/favicon.ico",
49
54
  "assets/favicon.svg",
50
55
  "assets/extensions/dshcs-open-file",
56
+ "assets/extensions/dshcs-editor-bridge",
51
57
  "scripts/vendor-vscode-server.mjs",
52
58
  "scripts/vendor-code-server.mjs",
53
59
  "scripts/vendor-repacks.mjs",
@@ -117,6 +123,8 @@
117
123
  "test:launcher-routes": "node scripts/test-launcher-routes.mjs",
118
124
  "test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
119
125
  "test:claim-types": "node scripts/test-claim-types.mjs",
120
- "test:workspace-switch": "node scripts/test-workspace-switch.mjs"
126
+ "test:workspace-switch": "node scripts/test-workspace-switch.mjs",
127
+ "test:bridge-routes": "node scripts/test-bridge-routes.mjs",
128
+ "test:bridge-extension": "node scripts/test-bridge-extension.mjs"
121
129
  }
122
130
  }
@@ -3,7 +3,7 @@
3
3
  "vscodeVersion": "1.137.0",
4
4
  "productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
5
5
  "layout": "vscode-only",
6
- "preparedAt": "2026-09-11T14:42:28.333Z",
6
+ "preparedAt": "2026-09-11T15:40:32.321Z",
7
7
  "source": "registry",
8
8
  "node": "v24.13.1",
9
9
  "platform": "win32",