dsh-vscode-mode 0.1.62 → 0.1.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -22,6 +22,7 @@ import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
22
22
  import { McpSettings } from './ui/McpSettings.js'
23
23
  import { rpc } from './rpc.js'
24
24
  import { loadMonaco } from './monaco/loader.js'
25
+ import { observeScheme, schemeOfSnapshot } from './monaco/theme.js'
25
26
  import { createFileOpenerRegistry, scanSidebar, officialSidebarOpener, shouldClaimFiles, type FileOpenContext } from './fileOpeners.js'
26
27
  import type { FileOpenerRegistry } from './fileOpeners.js'
27
28
  import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
@@ -33,6 +34,7 @@ import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_I
33
34
  import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab } from './officialSidebar.js'
34
35
  import { SideEditorTab } from './ui/SideEditorTab.js'
35
36
  import { OfficialSideTab } from './ui/OfficialSideTab.js'
37
+ import { createClaimRouter } from './ui/ClaimRouter.js'
36
38
  import { createAddToConversation } from './addToConversation.js'
37
39
  import { createSidebarPanelRegistry } from './sidebar/registry.js'
38
40
  import { createFilePanel } from './sidebar/panels/index.js'
@@ -148,7 +150,7 @@ export function apply(ctx: any): void {
148
150
  { name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
149
151
  { name: '侧边栏编辑区(官方 Sidebar)', active: officialService !== undefined, note: officialService !== undefined ? '编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)' : '官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)' },
150
152
  { name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ',归档)', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(旧版 DSH 回退形态)' : '未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(' + SIDEBAR_INSTALL_CMD + ')' },
151
- { name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '聊天文件链接由本插件编辑器接管(官方侧边栏内打开)' : '链接走官方查看器或旧版路由(未认领)' },
153
+ { name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '聊天文件链接转发进单一编辑器页签(文件分页归编辑器自带页签栏)' : '链接走官方查看器或旧版路由(未认领)' },
152
154
  ]
153
155
 
154
156
  ctx.provide('fileOpeners', registry)
@@ -183,14 +185,16 @@ export function apply(ctx: any): void {
183
185
  const sidebar = scanSidebar(ctx)
184
186
  return sidebar ? registry.register(sidebar) : undefined
185
187
  }, 'vscode-mode: sidebar file opener')
186
- // 官方侧边栏正文组件装配(页类型与 file 认领两类 Tab 共用同一 EditorView 形态)
188
+ // 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
187
189
  const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
188
- /** 官方 file 地址认领同步:自动/VSCodeMode 档认领(链接进本插件编辑器),其余交官方查看器。 */
190
+ /** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
189
191
  const syncFileClaim = (): void => {
190
192
  const official = officialService
191
193
  const want = official !== undefined && shouldClaimFiles(selected)
192
194
  if (want && claimDisposer === null && official) {
193
- const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab: officialRenderTab })
195
+ // 认领正文只做转发:文件分页收归编辑器自带页签栏,官方侧栏不再按文件分裂编辑器实例
196
+ const renderTab = createClaimRouter({ service: official.service, schedule, fallback: officialRenderTab })
197
+ const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab })
194
198
  if (disposer !== null) claimDisposer = ctx.effect(() => disposer, 'vscode-mode: official file claim')
195
199
  return
196
200
  }
@@ -265,6 +269,21 @@ export function apply(ctx: any): void {
265
269
  }
266
270
  retryRemoteOpen()
267
271
 
272
+ // 官方主题跟随(DSH 0.1.5+):ctx.theme 快照事件优先,其次明暗标记观察(observeScheme),
273
+ // 都不可用时 theme.ts 的 detectColorScheme 兜底;统一广播 edrv:theme-change 触发 Monaco 主题重刷。
274
+ const themeService = ctx.get('theme') as { getTheme?: () => unknown } | undefined
275
+ const emitTheme = (): void => {
276
+ const snapshot = typeof themeService?.getTheme === 'function' ? themeService.getTheme() : undefined
277
+ window.dispatchEvent(new CustomEvent('edrv:theme-change', { detail: { scheme: schemeOfSnapshot(snapshot) } }))
278
+ }
279
+ ctx.effect(() => {
280
+ if (!themeService || typeof ctx.on !== 'function') return undefined
281
+ const disposer = ctx.on('theme/change', emitTheme)
282
+ return typeof disposer === 'function' ? disposer : undefined
283
+ }, 'vscode-mode: official theme event')
284
+ ctx.effect(() => observeScheme(emitTheme), 'vscode-mode: theme attribute watch')
285
+ emitTheme()
286
+
268
287
  // 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态(官方/better-sidebar)可用时
269
288
  // 编辑器住侧边栏 Tab,本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
270
289
  const registerLegacyTab = (): (() => void) | null => {
@@ -25,6 +25,8 @@ export const COMMANDS: Array<{ id: string; label: string }> = [
25
25
  { id: 'edrv.searchInFiles', label: '在工作区中搜索' },
26
26
  { id: 'edrv.navigateBack', label: '后退(导航历史)' },
27
27
  { id: 'edrv.navigateForward', label: '前进(导航历史)' },
28
+ { id: 'edrv.nextTab', label: '下一个页签' },
29
+ { id: 'edrv.prevTab', label: '上一个页签' },
28
30
  ]
29
31
 
30
32
  const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
@@ -4,7 +4,7 @@
4
4
  * 迁移自原 src/client/index.ts 的 MONACO_BASE/LANG_BY_EXT/langOf/loadMonaco,语义不改。
5
5
  * 作者 ddj 2026-08-20
6
6
  */
7
- import { applyTheme, registerThemes } from './theme.js'
7
+ import { applyOfficial, registerThemes } from './theme.js'
8
8
 
9
9
  export const MONACO_BASE = '/edrv/vendor/monaco/vs'
10
10
  let monacoPromise = null
@@ -102,10 +102,10 @@ export function loadMonaco(onProgress) {
102
102
  publishStage('core', MONACO_STAGES.core.progress, MONACO_STAGES.core.message)
103
103
  window.require(['vs/editor/editor.main'], () => {
104
104
  publishStage('ready', MONACO_STAGES.ready.progress, MONACO_STAGES.ready.message)
105
- // 分色主题注册 + 应用(rich token 配色,替掉内置基础 vs 的少层次着色)
105
+ // 主题:注册现役双套(幂等)+ 应用跟随官方的令牌主题(令牌缺失时回落现役)
106
106
  try {
107
107
  registerThemes(window.monaco)
108
- applyTheme(window.monaco)
108
+ applyOfficial(window.monaco)
109
109
  } catch (error) { /* 主题失败不阻塞编辑器 */ }
110
110
  resolve(window.monaco)
111
111
  }, (err) => fail(new Error('Monaco 模块加载失败:' + String(err))))
@@ -1,10 +1,10 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * dsh-vscode-mode client — Monaco 语法分色主题(rich token 配色)。
4
- * 编辑器原先硬编码 'vs'(内置基础主题),token 分色层次少;
5
- * 这里定义 edrv-dark / edrv-light 两套完整 token 配色(对齐 VSCode Dark+ 语义分层),
6
- * 并随 DSH 明暗主题自动切换(跟随文档根 data 主题属性 / prefers-color-scheme)。
7
- * 作者 ddj 2026-08-28
3
+ * dsh-vscode-mode client — Monaco 主题:官方令牌跟随 + 现役双套回落。
4
+ * 优先级:官方代码/UI 令牌(--shiki-* / --dsw-alias-*,DSH 0.1.5+ 主题体系,
5
+ * 含第三方皮肤)→ edrv-dark / edrv-light 两套内置配色(旧版 DSH 或令牌缺失)。
6
+ * 明暗判定:body[data-ds-dark-theme](官方)→ 根 data-theme(旧皮肤)→ prefers-color-scheme。
7
+ * 作者 ddj 2026-08-28 / 2026-09-10
8
8
  */
9
9
 
10
10
  export const EDRV_DARK = 'edrv-dark'
@@ -185,12 +185,14 @@ export function registerThemes(monaco) {
185
185
  }
186
186
 
187
187
  /**
188
- * 探测当前 DSH 明暗(文档根 data 主题属性 → prefers-color-scheme → 暗)。
189
- * @author ddj 2026年08月28号
188
+ * 探测当前 DSH 明暗(官方 data-ds-dark-theme → 旧皮肤 data-theme → prefers-color-scheme → 暗)。
189
+ * @author ddj 2026年08月28号 / 2026年09月10号
190
190
  * @returns 'dark' | 'light'
191
191
  */
192
192
  export function detectColorScheme() {
193
193
  try {
194
+ const official = document.body?.getAttribute?.('data-ds-dark-theme')
195
+ if (official !== null && official !== undefined && official !== 'false') return 'dark'
194
196
  const attr = document.documentElement?.getAttribute?.('data-theme')
195
197
  ?? document.body?.getAttribute?.('data-theme')
196
198
  const value = String(attr ?? '').toLowerCase()
@@ -227,3 +229,307 @@ export function applyTheme(monaco) {
227
229
  /* setTheme 失败忽略 */
228
230
  }
229
231
  }
232
+
233
+ // --region 官方主题跟随(--shiki-* / --dsw-alias-* 令牌)
234
+
235
+ /** 动态官方主题 id 前缀(每次应用带序号重建,保证 Monaco 感知重定义)。 */
236
+ const EDRV_OFFICIAL = 'edrv-official'
237
+
238
+ /** 官方代码配色令牌 → Monaco 规则 token 前缀(粗粒度;未覆盖的规则保留现役语义分层)。 */
239
+ const RULE_TOKEN_MAP = [
240
+ ['--shiki-token-comment', ['comment']],
241
+ ['--shiki-token-keyword', ['keyword']],
242
+ ['--shiki-token-string', ['string']],
243
+ ['--shiki-token-string-expression', ['string.escape']],
244
+ ['--shiki-token-constant', ['number', 'constant']],
245
+ ['--shiki-token-function', ['function', 'method', 'member']],
246
+ ['--shiki-token-parameter', ['parameter', 'variable.parameter']],
247
+ ['--shiki-token-punctuation', ['delimiter', 'operator', 'punctuation']],
248
+ ['--shiki-token-link', ['annotation', 'metatag']],
249
+ ]
250
+
251
+ /**
252
+ * 官方 UI 令牌 → Monaco 颜色键(每键给候选令牌,按序取首个可解析值)。
253
+ * 依据运行态实测:--dsw-alias-* 定义在 body 上;--shiki-background/-foreground 未定义,
254
+ * 故代码面底色用 markdown-code-block 近似;selection 不覆盖(沿用现役可见选框)。
255
+ */
256
+ const COLOR_TOKEN_MAP = [
257
+ [['--dsw-alias-markdown-code-block', '--dsw-alias-bg-layer-2'], ['editor.background', 'editorGutter.background', 'minimap.background']],
258
+ [['--dsw-alias-label-primary', '--shiki-foreground'], ['editor.foreground']],
259
+ [['--dsw-alias-bg-layer-1', '--dsw-alias-bg-layer-3'], ['editorWidget.background', 'editorHoverWidget.background']],
260
+ [['--dsw-alias-label-tertiary'], ['editorLineNumber.foreground']],
261
+ [['--dsw-alias-label-secondary'], ['editorLineNumber.activeForeground']],
262
+ [['--dsw-alias-interactive-bg-hover'], ['editor.lineHighlightBackground', 'editor.inactiveSelectionBackground']],
263
+ ]
264
+
265
+ /** 已应用的官方主题序号(模块级计数,保证主题 id 每次唯一)。 */
266
+ let officialSeq = 0
267
+
268
+ /**
269
+ * 读官方 CSS 变量(先 body 后文档根;未定义/空值/未解析 var() 返回 undefined)。
270
+ * 依据运行态实测:DSH 的 --dsw-alias-* 定义在 body 上,仅 --shiki-token-* 在根。
271
+ * @author ddj 2026年09月10号
272
+ * @param name CSS 变量名(含 -- 前缀)
273
+ * @returns 计算样式值,或 undefined
274
+ */
275
+ export function readCssVar(name) {
276
+ try {
277
+ if (typeof window?.getComputedStyle !== 'function') return undefined
278
+ const hosts = [document?.body, document?.documentElement]
279
+ for (const host of hosts) {
280
+ if (!host) continue
281
+ const raw = window.getComputedStyle(host).getPropertyValue(name)
282
+ const value = String(raw ?? '').trim()
283
+ if (value && !value.startsWith('var(')) return value
284
+ }
285
+ return undefined
286
+ } catch (error) {
287
+ return undefined
288
+ }
289
+ }
290
+
291
+ /**
292
+ * 数值 → 两位十六进制(0-255 夹取)。
293
+ * @author ddj 2026年09月10号
294
+ * @param value 通道值
295
+ * @returns 两位小写十六进制
296
+ */
297
+ function hex2(value) {
298
+ const text = Math.max(0, Math.min(255, Math.round(Number(value) || 0))).toString(16)
299
+ return text.length === 1 ? '0' + text : text
300
+ }
301
+
302
+ /**
303
+ * CSS 颜色归一为 Monaco 可用值(#rgb/#rrggbb/#rrggbbaa 原样,rgb()/hsl() 转 hex,其余 undefined)。
304
+ * @author ddj 2026年09月10号
305
+ * @param value CSS 计算样式颜色值
306
+ * @returns Monaco 颜色字符串,或 undefined(不可识别)
307
+ */
308
+ export function toMonacoColor(value) {
309
+ const text = String(value ?? '').trim().toLowerCase()
310
+ if (!text || text.startsWith('var(')) return undefined
311
+ if (/^#[0-9a-f]{3}$/.test(text)) return '#' + text.slice(1).split('').map((c) => c + c).join('')
312
+ if (/^#[0-9a-f]{6}$/.test(text) || /^#[0-9a-f]{8}$/.test(text)) return text
313
+ return channelColor(text)
314
+ }
315
+
316
+ /**
317
+ * rgb()/rgba()/hsl()/hsla() → Monaco 颜色(兼容逗号与空格/斜杠两种参数语法)。
318
+ * @author ddj 2026年09月10号
319
+ * @param text 小写颜色文本
320
+ * @returns Monaco 颜色,或 undefined(不可识别)
321
+ */
322
+ function channelColor(text) {
323
+ const body = /^(?:rgba?|hsla?)\(([^)]+)\)$/.exec(text)?.[1]
324
+ if (!body) return undefined
325
+ const parts = body.split(/[\s,/]+/).filter(Boolean)
326
+ if (parts.length < 3) return undefined
327
+ let channels
328
+ if (text.startsWith('hsl')) {
329
+ const h = Number(parts[0])
330
+ const s = percentOf(parts[1])
331
+ const l = percentOf(parts[2])
332
+ if (![h, s, l].every(Number.isFinite)) return undefined
333
+ channels = hslToRgb(h, s, l)
334
+ } else {
335
+ channels = parts.slice(0, 3).map(Number)
336
+ }
337
+ if (channels.some((n) => !Number.isFinite(n))) return undefined
338
+ const alpha = parts.length > 3 ? alphaOf(parts[3]) : 1
339
+ const tail = Number.isFinite(alpha) && alpha < 1 ? hex2(alpha * 255) : ''
340
+ return '#' + channels.map(hex2).join('') + tail
341
+ }
342
+
343
+ /**
344
+ * 百分数文本 → 数值('5%' → 5,'0.5' → 0.5)。
345
+ * @author ddj 2026年09月10号
346
+ * @param value 参数文本
347
+ * @returns 数值(非数字为 NaN)
348
+ */
349
+ function percentOf(value) {
350
+ const text = String(value ?? '')
351
+ return text.endsWith('%') ? Number(text.slice(0, -1)) : Number(text)
352
+ }
353
+
354
+ /**
355
+ * alpha 参数归一('5%' → 0.05,'0.5' → 0.5)。
356
+ * @author ddj 2026年09月10号
357
+ * @param value 参数文本
358
+ * @returns 0-1 的 alpha(非数字为 NaN)
359
+ */
360
+ function alphaOf(value) {
361
+ const text = String(value ?? '')
362
+ return text.endsWith('%') ? Number(text.slice(0, -1)) / 100 : Number(text)
363
+ }
364
+
365
+ /**
366
+ * HSL → RGB 通道(h 度、s/l 为 0-100 百分数)。
367
+ * @author ddj 2026年09月10号
368
+ * @param h 色相(度)
369
+ * @param s 饱和度(0-100)
370
+ * @param l 亮度(0-100)
371
+ * @returns [r, g, b](0-255)
372
+ */
373
+ function hslToRgb(h, s, l) {
374
+ const hue = ((h % 360) + 360) % 360
375
+ const sat = Math.max(0, Math.min(100, s)) / 100
376
+ const lum = Math.max(0, Math.min(100, l)) / 100
377
+ const c = (1 - Math.abs(2 * lum - 1)) * sat
378
+ const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
379
+ const m = lum - c / 2
380
+ const table = [[c, x, 0], [x, c, 0], [0, c, x], [0, x, c], [x, 0, c], [c, 0, x]]
381
+ return table[Math.min(5, Math.floor(hue / 60))].map((v) => (v + m) * 255)
382
+ }
383
+
384
+ /**
385
+ * 官方令牌 → Monaco 颜色键覆盖表(候选令牌按序取首个可解析值;全缺则不写入,保持现役回落)。
386
+ * @author ddj 2026年09月10号
387
+ * @returns 颜色键 → 颜色值
388
+ */
389
+ function tokenColors() {
390
+ const out = {}
391
+ for (const entry of COLOR_TOKEN_MAP) {
392
+ const color = firstColor(entry[0])
393
+ if (color === undefined) continue
394
+ for (const key of entry[1]) out[key] = color
395
+ }
396
+ return out
397
+ }
398
+
399
+ /**
400
+ * 候选令牌中首个可解析为 Monaco 颜色的值。
401
+ * @author ddj 2026年09月10号
402
+ * @param names CSS 变量名候选(按优先级)
403
+ * @returns Monaco 颜色,或 undefined(全部缺失/不可解析)
404
+ */
405
+ function firstColor(names) {
406
+ for (const name of names) {
407
+ const color = toMonacoColor(readCssVar(name))
408
+ if (color !== undefined) return color
409
+ }
410
+ return undefined
411
+ }
412
+
413
+ /**
414
+ * 官方令牌 → Monaco 规则前缀覆盖表(值去掉 # 前缀,Monaco rules 约定)。
415
+ * @author ddj 2026年09月10号
416
+ * @returns token 前缀 → 十六进制色(无 #)
417
+ */
418
+ function tokenRules() {
419
+ const out = {}
420
+ for (const entry of RULE_TOKEN_MAP) {
421
+ const color = toMonacoColor(readCssVar(entry[0]))
422
+ if (color === undefined) continue
423
+ for (const prefix of entry[1]) out[prefix] = color.replace('#', '')
424
+ }
425
+ return out
426
+ }
427
+
428
+ /**
429
+ * 现役规则叠加令牌覆盖(前缀命中即换色,未命中保持语义分层)。
430
+ * @author ddj 2026年09月10号
431
+ * @param baseRules 现役规则表(edrv-dark / edrv-light)
432
+ * @param overrides token 前缀 → 颜色
433
+ * @returns 覆盖后的规则表(无覆盖时原样返回)
434
+ */
435
+ function withOverrides(baseRules, overrides) {
436
+ const prefixes = Object.keys(overrides)
437
+ if (!prefixes.length) return baseRules
438
+ return baseRules.map((rule) => {
439
+ const hit = prefixes.find((p) => rule.token === p || rule.token.startsWith(p + '.'))
440
+ return hit === undefined ? rule : Object.assign({}, rule, { foreground: overrides[hit] })
441
+ })
442
+ }
443
+
444
+ /**
445
+ * 基础规则(token 为空串)前景色跟随官方 editor.foreground。
446
+ * Monaco 的 '' 规则匹配所有未被更具体规则命中的 token,不改它则内置 #1e1e1e 会盖住
447
+ * editor.foreground 令牌色(实测 .mtk1 仍为内置值的原因)。
448
+ * @author ddj 2026年09月10号
449
+ * @param rules 现役规则表
450
+ * @param color 已解析的 editor.foreground(含 #,缺省则不覆盖)
451
+ * @returns 基础规则换色后的规则表
452
+ */
453
+ export function withBaseForeground(rules, color) {
454
+ if (!color) return rules
455
+ const foreground = String(color).replace('#', '')
456
+ return rules.map((rule) => (rule.token === '' ? Object.assign({}, rule, { foreground }) : rule))
457
+ }
458
+
459
+ /**
460
+ * 构建跟随官方的 Monaco 主题(令牌全缺时 hasTokens=false,调用方回落现役双套)。
461
+ * @author ddj 2026年09月10号
462
+ * @param scheme 明暗('light' 之外一律按暗色)
463
+ * @returns 主题定义(base/rules/colors/hasTokens)
464
+ */
465
+ export function officialThemeOf(scheme) {
466
+ const dark = scheme !== 'light'
467
+ const colors = tokenColors()
468
+ const rules = tokenRules()
469
+ const layered = withOverrides(dark ? DARK_RULES : LIGHT_RULES, rules)
470
+ return {
471
+ base: dark ? 'vs-dark' : 'vs',
472
+ rules: withBaseForeground(layered, colors['editor.foreground']),
473
+ colors: Object.assign({}, dark ? DARK_COLORS : LIGHT_COLORS, colors),
474
+ hasTokens: Object.keys(colors).length > 0 || Object.keys(rules).length > 0,
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 应用跟随官方的主题;令牌读不到或定义失败时回落现役双套。
480
+ * @author ddj 2026年09月10号
481
+ * @param monaco Monaco 实例
482
+ * @returns 实际应用的 Monaco 主题 id
483
+ */
484
+ export function applyOfficial(monaco) {
485
+ if (!monaco?.editor?.setTheme || !monaco?.editor?.defineTheme) return themeNameOf()
486
+ const theme = officialThemeOf(detectColorScheme())
487
+ if (!theme.hasTokens) {
488
+ applyTheme(monaco)
489
+ return themeNameOf()
490
+ }
491
+ officialSeq += 1
492
+ const id = EDRV_OFFICIAL + '-' + officialSeq
493
+ try {
494
+ monaco.editor.defineTheme(id, { base: theme.base, inherit: true, rules: theme.rules, colors: theme.colors })
495
+ monaco.editor.setTheme(id)
496
+ return id
497
+ } catch (error) {
498
+ applyTheme(monaco)
499
+ return themeNameOf()
500
+ }
501
+ }
502
+
503
+ /**
504
+ * 观察官方明暗标记变化(body / 根节点的 data-ds-dark-theme 与 data-theme)。
505
+ * @author ddj 2026年09月10号
506
+ * @param onChange 标记变化回调
507
+ * @returns 停止观察函数(非浏览器环境返回空函数)
508
+ */
509
+ export function observeScheme(onChange) {
510
+ try {
511
+ if (typeof MutationObserver !== 'function' || !document?.body) return () => {}
512
+ const observer = new MutationObserver(() => onChange())
513
+ const filter = ['data-ds-dark-theme', 'data-theme']
514
+ observer.observe(document.body, { attributes: true, attributeFilter: filter })
515
+ if (document.documentElement) {
516
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: filter })
517
+ }
518
+ return () => observer.disconnect()
519
+ } catch (error) {
520
+ return () => {}
521
+ }
522
+ }
523
+
524
+ /**
525
+ * 官方主题快照 → 明暗(ThemeRuntime.getTheme() 快照;字段缺失/未知返回 undefined)。
526
+ * @author ddj 2026年09月10号
527
+ * @param snapshot 官方主题快照
528
+ * @returns 'light' | 'dark' | undefined
529
+ */
530
+ export function schemeOfSnapshot(snapshot) {
531
+ const value = snapshot ?? {}
532
+ const scheme = value.active?.colorScheme ?? value.colorScheme
533
+ return scheme === 'light' || scheme === 'dark' ? scheme : undefined
534
+ }
535
+ // --endregion
@@ -37,6 +37,8 @@ export const OFFICIAL_FILE_KIND = 'edrvEditorFile'
37
37
  export interface OfficialTabParams {
38
38
  openPath?: string
39
39
  focusDiff?: boolean
40
+ /** 目标行号(行引用/工具行跳转透传;缺省仅打开文件)。 */
41
+ line?: number
40
42
  }
41
43
 
42
44
  /** sidebarRightTabs 服务的最小结构面(结构性探测,不 import 官方类型)。 */
@@ -97,6 +99,19 @@ export function resolveNavOpen(params: unknown): { path: string | null; focusDif
97
99
  return { path, focusDiff: value?.focusDiff === true }
98
100
  }
99
101
 
102
+ /**
103
+ * 从官方导航参数安全解析行号(缺字段/坏类型/非正数一律视为未指定)。
104
+ * @author ddj 2026年09月10号
105
+ * @param params openTab/openResource 传入的 params(正文经 navigation.params 读回)
106
+ * @returns 正整数行号,或 undefined
107
+ */
108
+ export function resolveNavLine(params: unknown): number | undefined {
109
+ const value = (params ?? {}) as OfficialTabParams
110
+ const line = value?.line
111
+ if (typeof line !== 'number' || !Number.isFinite(line) || line <= 0) return undefined
112
+ return Math.floor(line)
113
+ }
114
+
100
115
  /** 逐段 component 解码(失败返回 null,对齐官方 parseFileAddress 的容错语义)。 */
101
116
  function decodeSegment(raw: string | undefined): string | null {
102
117
  if (raw === undefined) return null
@@ -179,6 +194,36 @@ export function buildFileAddress(path: string, sessionId?: string): string {
179
194
  return OFFICIAL_FILE_PREFIX + 'session/' + encodeSegment(sessionId) + '/' + relative.split('/').map(encodeSegment).join('/')
180
195
  }
181
196
 
197
+ // --region file 认领转发(文件分页收归编辑器自带页签栏)
198
+
199
+ /**
200
+ * 把 file 打开请求转发进单一编辑器页签(文件分页由编辑器自带页签栏接管,
201
+ * 官方侧栏不再按文件分裂出多套编辑器实例)。
202
+ * @author ddj 2026年09月10号
203
+ * @param service 官方 sidebarRight 服务(缺失/缺 openTab 返回 false)
204
+ * @param path 目标文件路径(空值返回 false)
205
+ * @param line 可选行号(透传给编辑器页签导航参数)
206
+ * @returns 转发是否成功(openTab 抛错返回 false,由调用方决定重试或兜底)
207
+ */
208
+ export function forwardToEditor(
209
+ service: SidebarRightServiceLike | undefined | null,
210
+ path: string | null,
211
+ line?: number,
212
+ ): boolean {
213
+ if (!service || typeof service.openTab !== 'function' || !path) return false
214
+ const params: OfficialTabParams = { openPath: path }
215
+ if (line !== undefined) params.line = line
216
+ try {
217
+ // 官方语义:页类型恒去重,故转发只会聚焦既有编辑器页签,不产生新实例
218
+ service.openTab(OFFICIAL_TAB_KIND, { params })
219
+ return true
220
+ } catch (error) {
221
+ log.warn('file 认领转发进编辑器页签失败(' + String(error) + ')')
222
+ return false
223
+ }
224
+ }
225
+ // --endregion
226
+
182
227
  /**
183
228
  * 注册官方 Tab 正文(keyed slot,key=定义 id;slots.inject 等待 rightbar seat 声明)。
184
229
  * @author ddj 2026年09月09号
@@ -5,19 +5,20 @@
5
5
  * 作者 ddj 2026-08-27
6
6
  */
7
7
  import React from 'react'
8
+ import { IconCodeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
8
9
  import { OutlinePanel } from './OutlinePanel.js'
9
10
  import type { SidebarPanelDef, SidebarCtx } from '../sidebar/types.js'
10
11
 
11
12
  /**
12
13
  * 构造大纲面板定义。
13
- * @author ddj 2026年08月27号
14
- * @returns 面板定义(无徽标;活动栏图标 📜)
14
+ * @author ddj 2026年08月27号 / 2026年09月10号
15
+ * @returns 面板定义(无徽标;活动栏图标 = 官方 IconCodeOutline16)
15
16
  */
16
17
  export function createOutlinePanel(): SidebarPanelDef {
17
18
  return {
18
19
  id: 'outline',
19
20
  title: '大纲',
20
- icon: '📜',
21
+ icon: IconCodeOutline16,
21
22
  order: 20,
22
23
  render: (ctx: SidebarCtx) => React.createElement(OutlinePanel, { ctx }),
23
24
  }
@@ -18,6 +18,21 @@ const SEAM_W = 5
18
18
  /** 隐藏态拖出面板区的触发阈值(px,拖过即展开并继续实时调宽)。 */
19
19
  const PULL_OUT_THRESHOLD = 12
20
20
 
21
+ /**
22
+ * 活动栏图标内容:面板可给官方图标组件(跟随官方主题),也可给文本/emoji(回落)。
23
+ * @author ddj 2026年09月10号
24
+ * @param icon 面板定义的 icon 字段(组件或文本)
25
+ * @returns 图标元素或文本;组件渲染异常返回 null
26
+ */
27
+ function railIconEl(icon) {
28
+ if (typeof icon !== 'function') return icon
29
+ try {
30
+ return React.createElement(icon, { size: 16 })
31
+ } catch (error) {
32
+ return null
33
+ }
34
+ }
35
+
21
36
  /**
22
37
  * 侧边栏容器。
23
38
  * @param props.registry 面板注册表(list() 提供面板顺序)
@@ -112,7 +127,7 @@ export function SidebarView(props) {
112
127
  if (!visible && typeof onShow === 'function') onShow()
113
128
  },
114
129
  },
115
- React.createElement('span', { className: 'edrv-rail-icon' }, p.icon),
130
+ React.createElement('span', { className: 'edrv-rail-icon' }, railIconEl(p.icon)),
116
131
  count > 0
117
132
  ? React.createElement('span', { className: 'edrv-rail-badge' }, String(count))
118
133
  : null)
@@ -5,10 +5,11 @@
5
5
  * host 树索引,命中近乎零成本);挂载/切会话从缓存即时恢复展开树;⟳/edrv:refresh
6
6
  * 保留旧条目强制后台重列(force);每次用户发起加载后预取 ≤4 个子目录(排除重型
7
7
  * 目录、缓存新鲜跳过、不级联);10s 轻量跟随已展开目录(命中索引,RPC 近零成本)。
8
- * 差异角标/右键菜单/展开状态持久化行为不变。
9
- * 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31
8
+ * 差异角标/右键菜单/展开状态持久化行为不变;行图标用官方原语(文件夹图标 + 文件类型图标)。
9
+ * 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31 / 2026-09-10
10
10
  */
11
11
  import React from 'react'
12
+ import { FileTypeIcon, IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, classifyFileType } from '@deepseek-ai/dsh-client-ui-primitives'
12
13
  import { rpc } from '../../rpc.js'
13
14
  import { ContextMenu } from '../../ui/ContextMenu.js'
14
15
  import { buildTreeMenu } from '../contextMenu.js'
@@ -23,6 +24,58 @@ const FOLLOW_INTERVAL_MS = 10_000
23
24
  const PREFETCH_MAX = 4
24
25
  const PREFETCH_EXCLUDED = new Set(['node_modules', '.git', '.hg', '.svn', '.pnpm', '.pnpm-store'])
25
26
 
27
+ // --region 行图标(官方原语:目录文件夹图标 + 文件类型图标;缺失时回落纯文本)
28
+
29
+ /**
30
+ * 目录图标元素(官方 IconFolderOpen16/Close16)。
31
+ * @author ddj 2026年09月10号
32
+ * @param isOpen 目录是否展开
33
+ * @returns 图标元素;原语缺失返回 null
34
+ */
35
+ function dirIconEl(isOpen) {
36
+ const Icon = isOpen ? IconFolderOpen16 : IconFolderClose16
37
+ if (typeof Icon !== 'function') return null
38
+ return React.createElement(Icon, { size: 16, className: 'edrv-tree-icon' })
39
+ }
40
+
41
+ /**
42
+ * 文件图标元素(官方 FileTypeIcon + classifyFileType 分类,未知类型为 other)。
43
+ * @author ddj 2026年09月10号
44
+ * @param name 文件名(含扩展名)
45
+ * @returns 图标元素;原语缺失或分类异常返回 null
46
+ */
47
+ function fileIconEl(name) {
48
+ if (typeof FileTypeIcon !== 'function' || typeof classifyFileType !== 'function') return null
49
+ try {
50
+ return React.createElement(FileTypeIcon, { kind: classifyFileType(String(name ?? '')), size: 16, className: 'edrv-tree-icon' })
51
+ } catch (error) {
52
+ return null
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 行图标分派:目录 → 文件夹图标,文件 → 官方类型图标。
58
+ * @author ddj 2026年09月10号
59
+ * @param e 目录条目(name/type)
60
+ * @param isDir 是否目录
61
+ * @param isOpen 目录是否展开
62
+ * @returns 图标元素或 null
63
+ */
64
+ function rowIconEl(e, isDir, isOpen) {
65
+ return isDir ? dirIconEl(isOpen) : fileIconEl(e?.name)
66
+ }
67
+
68
+ /**
69
+ * 刷新按钮图标(官方 IconRefreshOutline16;原语缺失回落文本 ⟳)。
70
+ * @author ddj 2026年09月10号
71
+ * @returns 图标元素或回落文本
72
+ */
73
+ function refreshIconEl() {
74
+ if (typeof IconRefreshOutline16 !== 'function') return '⟳'
75
+ return React.createElement(IconRefreshOutline16, { size: 14 })
76
+ }
77
+ // --endregion
78
+
26
79
  /**
27
80
  * 目录树面板主体(SWR:有缓存先渲染,无缓存才显示加载态;加载总在后台)。
28
81
  * @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath/fileMenuItems/notify)
@@ -227,8 +280,9 @@ export function FileExplorer(props) {
227
280
  },
228
281
  React.createElement('span', { className: 'edrv-tree-chev' },
229
282
  isDir ? (isOpen ? '▾' : '▸') : ''),
283
+ rowIconEl(e, isDir, isOpen),
230
284
  React.createElement('span', { className: 'edrv-tree-name' + (dim ? ' edrv-tree-dim' : '') },
231
- isDir ? (isOpen ? '📂' : '📁') : '📄', ' ', e.name),
285
+ e.name),
232
286
  (pending > 0
233
287
  ? React.createElement('span', { className: 'edrv-tree-badge' }, String(pending))
234
288
  : null))
@@ -283,7 +337,7 @@ export function FileExplorer(props) {
283
337
  React.createElement('span', { className: 'edrv-side-title' }, '资源管理器'),
284
338
  React.createElement('span', { className: 'edrv-side-root', title: root || '' }, rootName),
285
339
  React.createElement('span', { style: { flex: 1 } }),
286
- React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: refresh }, '⟳')),
340
+ React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: refresh }, refreshIconEl())),
287
341
  React.createElement('div', { className: 'edrv-tree' },
288
342
  (errorText
289
343
  ? React.createElement('div', { className: 'edrv-tree-error' },