dsh-code-ui 1.3.2 → 1.4.0

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
@@ -4,7 +4,6 @@
4
4
  // UI's file / reference / note operations, and a model tool get_referenced_snippets.
5
5
 
6
6
  import { defineTool } from "@deepseek-ai/dsh-tools";
7
- import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
8
7
  import z from "@deepseek-ai/schemastery";
9
8
  import { spawn } from "node:child_process";
10
9
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
@@ -36,6 +35,65 @@ const SELF_MANIFEST = readJsonSafe(join(HERE, "..", "package.json")) || { name:
36
35
  const SELF_NAME = SELF_MANIFEST.name;
37
36
  const SELF_VERSION = SELF_MANIFEST.version;
38
37
 
38
+ // —— highlight.js 静态资产:宿主 webServer exact 路由下发 ——
39
+ // /plugins 组合脚本只承载 client.js 本体,vendor 产物(全量包 + 作用域化主题 CSS)无从经其
40
+ // 下发;exact 路由无路径参数,天然无目录穿越面。产物由 tools/vendor-hljs.mjs 生成
41
+ // (升级:node tools/vendor-hljs.mjs <版本号>;只补主题:--themes-only)。
42
+ const VENDOR_ASSETS = {
43
+ "/dsh-code-ui/vendor/highlight.full.min.js": {
44
+ file: join(HERE, "vendor", "highlight.full.min.js"),
45
+ type: "text/javascript; charset=utf-8",
46
+ },
47
+ "/dsh-code-ui/vendor/hljs-themes.css": {
48
+ file: join(HERE, "vendor", "hljs-themes.css"),
49
+ type: "text/css; charset=utf-8",
50
+ },
51
+ };
52
+ // 语法配色可取值(与 lib/client.js 的 SYN_THEMES 表、tools/vendor-hljs.mjs 的 THEMES 一一对应)
53
+ const SYN_THEME_IDS = [
54
+ "auto", "github", "github-dark", "atom-one-dark", "atom-one-light",
55
+ "monokai", "dracula", "nord", "vs", "vs2015", "tokyo-night-dark",
56
+ ];
57
+ function registerVendorRoutes(ctx) {
58
+ // webServer 为可选宿主服务(web profile 提供):无该服务的宿主上回调不运行,
59
+ // 编辑器自动回退自研 tokenizer,不影响其它功能
60
+ ctx.inject(["webServer"], (c) => {
61
+ const disposers = [];
62
+ for (const [path, asset] of Object.entries(VENDOR_ASSETS)) {
63
+ disposers.push(
64
+ c.webServer.register({
65
+ kind: "exact",
66
+ path,
67
+ handler: (req, res) => {
68
+ if (req.method !== "GET" && req.method !== "HEAD") {
69
+ res.writeHead(405, { allow: "GET, HEAD", "content-length": "0" });
70
+ res.end();
71
+ return;
72
+ }
73
+ let body = null;
74
+ try {
75
+ if (existsSync(asset.file)) body = readFileSync(asset.file);
76
+ } catch (_) {}
77
+ if (body === null) {
78
+ res.writeHead(404, { "cache-control": "no-store" });
79
+ res.end();
80
+ return;
81
+ }
82
+ res.writeHead(200, {
83
+ "content-type": asset.type,
84
+ "content-length": String(body.length),
85
+ // 客户端以 ?v=<插件版本> 作 cache-bust:同版本字节不变,可放心长缓存
86
+ "cache-control": "public, max-age=31536000, immutable",
87
+ });
88
+ res.end(req.method === "HEAD" ? undefined : body);
89
+ },
90
+ }),
91
+ );
92
+ }
93
+ c.effect(() => () => { for (const d of disposers) { try { d(); } catch (_) {} } }, "dsh-code-ui: vendor assets");
94
+ });
95
+ }
96
+
39
97
  // profile 根 = 向上找 package.json 含 dsh.profile.bundles 的目录(不数层级、不硬编码路径)
40
98
  function findProfileRoot() {
41
99
  let dir = HERE;
@@ -90,7 +148,15 @@ const REGISTRY_MIRROR = "https://registry.npmmirror.com";
90
148
  // -- settings 命名空间:「插件配置」页按 host 服务的命名空间分发卡片 --
91
149
  // 卡片的 key 必须是一个 host 实际注册的 settings 命名空间,否则卡片永不渲染
92
150
  //(ui-settings-plugins 的 tab-store 只分发 key ∈ 已服务命名空间的卡片,照 dshmarket 的 settings.ts 做法)。
93
- const CODE_UI_SETTINGS_NS = settingsNamespace("dsh-code-ui");
151
+ // 不 import @deepseek-ai/dsh-settings:该包 0.1.2 起删除了 installSettingsSection /
152
+ // settingsNamespace 具名导出(ESM 缺失具名导出 = 模块求值期 SyntaxError,宿主直接退出 1),
153
+ // 而服务本身 sctx.settings.register(ns, schema, { base }) 从 0.1.0-rc.7 到 0.1.2 完全一致。
154
+ // 照 dshmarket settings.js 的做法内联等价接线;下为旧 settingsNamespace() 的校验规则。
155
+ const SETTINGS_NS_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
156
+ const CODE_UI_SETTINGS_NS = "dsh-code-ui";
157
+ if (!SETTINGS_NS_PATTERN.test(CODE_UI_SETTINGS_NS)) {
158
+ throw new TypeError(`settings namespace "${CODE_UI_SETTINGS_NS}" must match ${SETTINGS_NS_PATTERN}`);
159
+ }
94
160
  const CodeUiSettingsSchema = z.object({
95
161
  // 检查更新用的 registry(只影响在线「检查」;安装仍走 profile 自己的 .npmrc)
96
162
  updateRegistry: z.string().default(REGISTRY_DEFAULT),
@@ -104,37 +170,42 @@ const CodeUiSettingsSchema = z.object({
104
170
  uiMode: z.string().default("native"),
105
171
  // 切换按钮样式:bar(悬浮模式条,默认) / header(会话头 ⌨ 按钮)
106
172
  toggle: z.string().default("bar"),
173
+ // 语法配色:auto(默认,随编辑器明暗 GitHub/GitHub Dark) 或 SYN_THEME_IDS 之一
174
+ synTheme: z.string().default("auto"),
107
175
  });
108
176
  // 解析后的当前值:settings 服务未挂载/未写入时保持默认
109
- const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "native", toggle: "bar" };
177
+ const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "native", toggle: "bar", synTheme: "auto" };
110
178
  // settings 服务引用(「启用/禁用」开关写入用;由 installSelfSettings 捕获)
111
179
  let settingsServiceRef = null;
112
180
  function installSelfSettings(ctx) {
113
- let source = () => ({ updateRegistry: selfSettings.updateRegistry, enabled: selfSettings.enabled, theme: selfSettings.theme, layout: selfSettings.layout, uiMode: selfSettings.uiMode, toggle: selfSettings.toggle });
114
- installSettingsSection(
115
- ctx,
116
- CODE_UI_SETTINGS_NS,
117
- CodeUiSettingsSchema,
118
- { updateRegistry: selfSettings.updateRegistry, enabled: selfSettings.enabled, theme: selfSettings.theme, layout: selfSettings.layout, uiMode: selfSettings.uiMode, toggle: selfSettings.toggle },
119
- {
120
- setSource: (current) => { source = current; },
121
- onChange: () => {
122
- try {
123
- const v = source() || {};
124
- if (typeof v.updateRegistry === "string" && /^https?:\/\//.test(v.updateRegistry)) {
125
- selfSettings.updateRegistry = v.updateRegistry.replace(/\/+$/, "");
126
- }
127
- if (typeof v.enabled === "boolean") selfSettings.enabled = v.enabled;
128
- if (v.theme === "dsh" || v.theme === "system" || v.theme === "light" || v.theme === "dark") selfSettings.theme = v.theme;
129
- if (v.layout === "dock" || v.layout === "float") selfSettings.layout = v.layout;
130
- if (v.uiMode === "codeui" || v.uiMode === "native") selfSettings.uiMode = v.uiMode;
131
- if (v.toggle === "header" || v.toggle === "bar") selfSettings.toggle = v.toggle;
132
- } catch (_) {}
133
- },
134
- },
135
- );
136
- // 捕获 settings 服务实例:卡片的「启用/禁用」开关经 set-enabled RPC 写入本命名空间
137
- ctx.inject(["settings"], (sctx) => { settingsServiceRef = sctx.settings; });
181
+ // 组合基线(base):settings 服务缺席/卸载时的回落取值
182
+ const entry = { updateRegistry: selfSettings.updateRegistry, enabled: selfSettings.enabled, theme: selfSettings.theme, layout: selfSettings.layout, uiMode: selfSettings.uiMode, toggle: selfSettings.toggle, synTheme: selfSettings.synTheme };
183
+ let source = () => entry;
184
+ const onChange = () => {
185
+ try {
186
+ const v = source() || {};
187
+ if (typeof v.updateRegistry === "string" && /^https?:\/\//.test(v.updateRegistry)) {
188
+ selfSettings.updateRegistry = v.updateRegistry.replace(/\/+$/, "");
189
+ }
190
+ if (typeof v.enabled === "boolean") selfSettings.enabled = v.enabled;
191
+ if (v.theme === "dsh" || v.theme === "system" || v.theme === "light" || v.theme === "dark") selfSettings.theme = v.theme;
192
+ if (v.layout === "dock" || v.layout === "float") selfSettings.layout = v.layout;
193
+ if (v.uiMode === "codeui" || v.uiMode === "native") selfSettings.uiMode = v.uiMode;
194
+ if (v.toggle === "header" || v.toggle === "bar") selfSettings.toggle = v.toggle;
195
+ if (typeof v.synTheme === "string" && SYN_THEME_IDS.indexOf(v.synTheme) >= 0) selfSettings.synTheme = v.synTheme;
196
+ } catch (_) {}
197
+ };
198
+ // inject 是优雅降级边界:无 settings 服务的宿主上回调不运行,基线值照常生效
199
+ //(等价于旧 installSettingsSection:inject + register + watch + 卸载回落,照 dshmarket settings.js)
200
+ ctx.inject(["settings"], (sctx) => {
201
+ settingsServiceRef = sctx.settings; // 写路径(set-* RPC)用服务级 update(ns, patch),新旧版签名一致
202
+ const scope = sctx.settings.register(CODE_UI_SETTINGS_NS, CodeUiSettingsSchema, { base: entry });
203
+ source = () => scope.get();
204
+ // 卸载恢复基线取值,避免路由读到无人可见/可改的值
205
+ sctx.effect(() => () => { source = () => entry; onChange(); });
206
+ onChange();
207
+ scope.watch(onChange);
208
+ });
138
209
  }
139
210
 
140
211
  async function fetchLatestVersion(name, registry) {
@@ -350,7 +421,10 @@ function unwatchAllFiles() {
350
421
  function rpcHandler(ctx) {
351
422
  const fs = ctx.get("fs");
352
423
  let currentRootRef = detectWorkspaceRoot(ctx); // 最近一次解析的根:list-dir 的兜底
353
- let manualRootRef = null; // 用户「以此文件夹为根」的显式选择:一旦设置不再跟随会话
424
+ // 每会话根目录记忆:sessionId -> 手动设置的根(「以此文件夹为根」)。
425
+ // 按会话隔离——手动根只影响设置它的那个会话,不再全局污染其它会话的自动解析;
426
+ // 重启 DSH 即清空,各会话回到各自 cwd 解析。自动解析结果不缓存(会话 cwd 可变,保持实时)。
427
+ const sessionRoots = new Map();
354
428
 
355
429
  function detectWorkspaceRoot(_ctx) {
356
430
  // 1) 当前会话的真实工作区根(项目根):优先取正在发起调用的 agent 的 session cwd。
@@ -384,20 +458,45 @@ function rpcHandler(ctx) {
384
458
  const args = payload && typeof payload === "object" ? payload : {};
385
459
 
386
460
  if (endpoint === "get-root") {
387
- // 优先级:手动设置的根 > 浏览器上报的当前会话 cwd(agent/session id 共享,agents.get 可查)
388
- // > initiator 会话 cwd(agent 驱动中)> 部署回退根 > 进程 cwd
389
- let root = manualRootRef;
390
- if (!root && args.sessionId) {
461
+ // 优先级(按会话隔离):该会话手动设置的根 > 该会话的 agent cwd(agent 已启动,
462
+ // agent/session id 共享,agents.get 可查)> 该会话的 SessionHeader.cwd(新会话未发
463
+ // 消息时 agents 里还没有记录,但会话创建时 cwd 已从所选工作区写入不可变 header,
464
+ // 经 sessions 服务可查)> initiator 会话 cwd > 部署回退根 > 进程 cwd。
465
+ // 手动根仅对本会话生效,其它会话不受污染。
466
+ const sid = args.sessionId ? String(args.sessionId) : "";
467
+ let root = sid ? sessionRoots.get(sid) || null : null;
468
+ if (!root && sid) {
391
469
  const agentsSvc = ctx.get("agents");
392
470
  if (agentsSvc) {
393
471
  try {
394
- const a = agentsSvc.get(String(args.sessionId));
472
+ const a = agentsSvc.get(sid);
395
473
  if (a && a.session && a.session.header && a.session.header.cwd) {
396
474
  root = a.session.header.cwd;
397
475
  }
398
476
  } catch (_) {}
399
477
  }
400
478
  }
479
+ // 新会话(尚未发消息、agent 未启动):从会话存储取 SessionHeader.cwd
480
+ if (!root && sid) {
481
+ const sessionsSvc = ctx.get("sessions");
482
+ if (sessionsSvc && typeof sessionsSvc.get === "function") {
483
+ try {
484
+ const s = sessionsSvc.get(sid);
485
+ if (s && s.header && s.header.cwd) root = s.header.cwd;
486
+ } catch (_) {}
487
+ }
488
+ }
489
+ // 会话已持久化但未在内存存储(如重启 DSH 后重开旧对话、agent 尚未恢复):
490
+ // 经 sessionController.inspect 读持久化 header(官方语义:不激活 agent)
491
+ if (!root && sid) {
492
+ const sc = ctx.get("sessionController");
493
+ if (sc && typeof sc.inspect === "function") {
494
+ try {
495
+ const r = await sc.inspect(sid);
496
+ if (r && r.meta && r.meta.cwd) root = r.meta.cwd;
497
+ } catch (_) {}
498
+ }
499
+ }
401
500
  if (!root) root = detectWorkspaceRoot(ctx);
402
501
  if (root) currentRootRef = root;
403
502
  return ok({ root });
@@ -405,9 +504,12 @@ function rpcHandler(ctx) {
405
504
 
406
505
  if (endpoint === "set-root") {
407
506
  if (!args.path) return ok({ error: "no-path" });
408
- manualRootRef = String(args.path);
409
- currentRootRef = manualRootRef;
410
- return ok({ root: currentRootRef });
507
+ // 手动根按会话记录:只影响发起设置的会话;无会话上下文时退化为全局兜底
508
+ const path = String(args.path);
509
+ const sid = args.sessionId ? String(args.sessionId) : "";
510
+ if (sid) sessionRoots.set(sid, path);
511
+ currentRootRef = path;
512
+ return ok({ root: path });
411
513
  }
412
514
 
413
515
  if (endpoint === "list-dir") {
@@ -464,9 +566,17 @@ function rpcHandler(ctx) {
464
566
  if (endpoint === "write-file") {
465
567
  if (!args.path) return ok({ error: "no-path" });
466
568
  if (typeof args.content !== "string") return ok({ error: "no-content" });
467
- writeSuppress.set(args.path, Date.now() + 1500); // 自己的保存不进事件队列(回声抑制)
468
- await fs.writeText(await fs.resolve(args.path), args.content);
469
- return ok({ ok: true });
569
+ try {
570
+ writeSuppress.set(args.path, Date.now() + 1500); // 自己的保存不进事件队列(回声抑制)
571
+ // 原生写入:code UI 的 Ctrl+S 是用户显式编辑动作,不受会话沙箱限制
572
+ // (与文件树右键新建/重命名/删除一致;沙箱 fs 只允许写会话工作区,
573
+ // 工作区之外的文件会报 file access denied under workspace-write mode)
574
+ writeFileSync(args.path, args.content, "utf8");
575
+ return ok({ ok: true });
576
+ } catch (err) {
577
+ writeSuppress.delete(args.path);
578
+ return ok({ error: "保存失败:" + ((err && err.message) || String(err)) });
579
+ }
470
580
  }
471
581
 
472
582
  if (endpoint === "watch-file") {
@@ -618,6 +728,7 @@ function rpcHandler(ctx) {
618
728
  layout: selfSettings.layout,
619
729
  uiMode: selfSettings.uiMode,
620
730
  toggle: selfSettings.toggle,
731
+ synTheme: selfSettings.synTheme,
621
732
  });
622
733
  }
623
734
 
@@ -681,6 +792,21 @@ function rpcHandler(ctx) {
681
792
  }
682
793
  }
683
794
 
795
+ if (endpoint === "set-syn-theme") {
796
+ const v = args.synTheme;
797
+ if (SYN_THEME_IDS.indexOf(v) < 0) return ok({ error: "synTheme 需为 auto 或受支持的主题 id" });
798
+ if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
799
+ return ok({ error: "settings 服务不可用,重启 DSH 后再试" });
800
+ }
801
+ try {
802
+ await settingsServiceRef.update(CODE_UI_SETTINGS_NS, { synTheme: v });
803
+ selfSettings.synTheme = v;
804
+ return ok({ ok: true, synTheme: v });
805
+ } catch (err) {
806
+ return ok({ error: "写入设置失败:" + ((err && err.message) || String(err)) });
807
+ }
808
+ }
809
+
684
810
  if (endpoint === "set-enabled") {
685
811
  if (typeof args.enabled !== "boolean") return ok({ error: "enabled 参数需为布尔值" });
686
812
  if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
@@ -812,6 +938,10 @@ export function apply(ctx) {
812
938
  // 无 settings 服务的宿主上静默不运行,不影响其它功能)
813
939
  installSelfSettings(ctx);
814
940
 
941
+ // vendor 静态资产路由(highlight.js 全量包 + 主题 CSS):webServer 服务存在时注册 exact 路由,
942
+ // 客户端编辑器懒加载;无 webServer 的宿主上静默跳过(编辑器回退自带 tokenizer)
943
+ registerVendorRoutes(ctx);
944
+
815
945
  // 注册模型工具:通过 ctx.inject 拿 tools 服务(避免 Cordis guard 直接访问 ctx.tools)
816
946
  ctx.inject(["tools"], (c) => {
817
947
  registerSnippetsTool(c.tools);