dsh-vscode-mode 0.1.59 → 0.1.60

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,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.59",
4
- "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
3
+ "version": "0.1.60",
4
+ "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * dsh-vscode-mode client — 文件打开器注册表与自动选择。
3
- * @author ddj 2026年08月24号
3
+ * @author ddj 2026年08月24号 / 2026年09月09号
4
4
  */
5
5
  import { SIDEBAR_PLUGIN } from './compat.js'
6
+ import { buildFileAddress } from './officialSidebar.js'
6
7
 
7
8
  export const AUTO_OPEN_TOOL = 'auto'
8
9
  export const SYSTEM_OPEN_TOOL = 'system'
9
10
  export const VSCODE_OPEN_TOOL = 'dsh-vscode-mode'
10
11
  export const SIDEBAR_OPEN_TOOL = SIDEBAR_PLUGIN
12
+ /** 官方右侧侧边栏打开器 id(「文件链接使用工具」下拉的官方选项;DSH 0.1.5+)。 */
13
+ export const OFFICIAL_OPEN_TOOL = 'dsh-official-sidebar'
11
14
 
12
15
  export interface FileOpenContext {
13
16
  sessionId?: string
@@ -171,3 +174,40 @@ export function baseName(path: string): string {
171
174
  const normalized = path.replace(/\\/g, '/')
172
175
  return normalized.slice(normalized.lastIndexOf('/') + 1) || path
173
176
  }
177
+
178
+ /** 官方 sidebarRight.openResource 的最小结构面(结构性使用,不 import 官方类型)。 */
179
+ export interface OfficialOpenResourceService {
180
+ openResource?: (address: string, options?: { params?: unknown }) => void
181
+ }
182
+
183
+ /**
184
+ * 文件链接是否由本插件认领官方 file 地址:自动/VSCodeMode 档接管(链接进本插件编辑器),
185
+ * 官方侧边栏/system/better-sidebar 档不认领(官方查看器或旧版路由生效)。
186
+ * @author ddj 2026年09月09号
187
+ * @param selected 「文件链接使用工具」当前值
188
+ * @returns 是否认领
189
+ */
190
+ export function shouldClaimFiles(selected: unknown): boolean {
191
+ return selected === AUTO_OPEN_TOOL || selected === VSCODE_OPEN_TOOL
192
+ }
193
+
194
+ /**
195
+ * 创建「官方侧边栏」文件打开器(DSH 0.1.5+,经 ctx.sidebarRight.openResource 打开官方查看器)。
196
+ * 仅官方服务探测命中时注册;下拉选项随注册动态出现(McpSettings 动态渲染)。
197
+ * @author ddj 2026年09月09号
198
+ * @param service 官方导航控制器
199
+ * @returns 文件打开器
200
+ */
201
+ export function officialSidebarOpener(service: OfficialOpenResourceService): FileOpener {
202
+ return {
203
+ id: OFFICIAL_OPEN_TOOL,
204
+ label: '官方侧边栏',
205
+ description: '在 DSH 官方右侧侧边栏查看器中打开(0.1.5+)',
206
+ priority: 90,
207
+ isAvailable: () => typeof service.openResource === 'function',
208
+ open: (path, context) => {
209
+ if (typeof service.openResource !== 'function') throw new Error('官方侧边栏 openResource 能力不可用')
210
+ service.openResource(buildFileAddress(path, context.sessionId))
211
+ },
212
+ }
213
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
3
- * 挂点:betterSidebar「文件编辑」Tab(侧边栏形态,对话+编辑同屏;未装 dsh-better-sidebar 时
4
- * 回退 conversation.view 中央页签)+ conversation.input.dock 差异条 + header 差异角标。
3
+ * 挂点:官方右侧 Sidebar「文件编辑」Tab(DSH 0.1.5+,对话+编辑同屏)> betterSidebar Tab
4
+ * (归档,仅旧版 DSH 回退)> conversation.view 中央页签 + conversation.input.dock 差异条 + header 差异角标。
5
5
  * 与 Host 通信:同源 fetch('/edrv/rpc')(shared/rpc 契约)。
6
6
  *
7
7
  * ⚠️ 跨版本 slot 装配(2026-08-21):新版 DSH 的 slots 系统要求 slot 必须由父 entry
@@ -22,7 +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 { createFileOpenerRegistry, scanSidebar, type FileOpenContext } from './fileOpeners.js'
25
+ import { createFileOpenerRegistry, scanSidebar, officialSidebarOpener, shouldClaimFiles, type FileOpenContext } from './fileOpeners.js'
26
26
  import type { FileOpenerRegistry } from './fileOpeners.js'
27
27
  import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
28
28
  import { patchRemoteOpen, probeRemoteOpen } from './remoteOpenRouter.js'
@@ -30,7 +30,9 @@ import { setupExtOpen } from './externalOpen.js'
30
30
  import { SettingsContext } from './settingsContext.js'
31
31
  import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat.js'
32
32
  import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
33
+ import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_TITLE } from './officialSidebar.js'
33
34
  import { SideEditorTab } from './ui/SideEditorTab.js'
35
+ import { OfficialSideTab } from './ui/OfficialSideTab.js'
34
36
  import { createAddToConversation } from './addToConversation.js'
35
37
  import { createSidebarPanelRegistry } from './sidebar/registry.js'
36
38
  import { createFilePanel } from './sidebar/panels/index.js'
@@ -113,6 +115,10 @@ export function apply(ctx: any): void {
113
115
  }
114
116
  /** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
115
117
  let sideService = detectSidebarService(ctx)
118
+ /** 可选探测官方右侧 Sidebar(DSH 0.1.5-alpha.1+;探测命中 ≡ 版本判定,不进 inject 同上)。 */
119
+ let officialService = detectOfficial(ctx)
120
+ /** 官方 file 地址认领的当前注销器(null=未认领;由 syncFileClaim 依设置值切换)。 */
121
+ let claimDisposer: (() => void) | null = null
116
122
  /** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
117
123
  const addToConversation = createAddToConversation(ctx)
118
124
 
@@ -122,7 +128,9 @@ export function apply(ctx: any): void {
122
128
  { name: '文件打开路由(workspaces.openPath)', active: Boolean(workspaces?.openPath), note: 'vscode 打开器优先,失败回退系统打开(0.1.2 及更早 DSH 的对话文件链接主路径)' },
123
129
  { name: '会话文件链接路由(remote.session.openWorkspacePath)', active: remoteOpenInstalled, note: '0.1.3+ 对话文件引用/产物打开优先走插件打开器,失败回退系统打开' },
124
130
  { name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
125
- { name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ')', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(对话+编辑同屏)' : '未检测到;安装 dsh-better-sidebar 后刷新启用侧边栏形态(' + SIDEBAR_INSTALL_CMD + ')' },
131
+ { name: '侧边栏编辑区(官方 Sidebar)', active: officialService !== undefined, note: officialService !== undefined ? '编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)' : '官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)' },
132
+ { name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ',归档)', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(旧版 DSH 回退形态)' : '未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(' + SIDEBAR_INSTALL_CMD + ')' },
133
+ { name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '聊天文件链接由本插件编辑器接管(官方侧边栏内打开)' : '链接走官方查看器或旧版路由(未认领)' },
126
134
  ]
127
135
 
128
136
  ctx.provide('fileOpeners', registry)
@@ -157,12 +165,29 @@ export function apply(ctx: any): void {
157
165
  const sidebar = scanSidebar(ctx)
158
166
  return sidebar ? registry.register(sidebar) : undefined
159
167
  }, 'vscode-mode: sidebar file opener')
168
+ // 官方侧边栏正文组件装配(页类型与 file 认领两类 Tab 共用同一 EditorView 形态)
169
+ const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
170
+ /** 官方 file 地址认领同步:自动/VSCodeMode 档认领(链接进本插件编辑器),其余交官方查看器。 */
171
+ const syncFileClaim = (): void => {
172
+ const official = officialService
173
+ const want = official !== undefined && shouldClaimFiles(selected)
174
+ if (want && claimDisposer === null && official) {
175
+ const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab: officialRenderTab })
176
+ if (disposer !== null) claimDisposer = ctx.effect(() => disposer, 'vscode-mode: official file claim')
177
+ return
178
+ }
179
+ if (!want && claimDisposer !== null) {
180
+ claimDisposer()
181
+ claimDisposer = null
182
+ }
183
+ }
160
184
  ctx.effect(() => {
161
185
  if (!settings) return undefined
162
186
  const sync = (): void => {
163
187
  const snapshot = settings.getSnapshot()
164
188
  if (snapshot.status === 'loading') return
165
189
  selected = autoValue(snapshot.value?.fileOpenTool)
190
+ syncFileClaim()
166
191
  window.dispatchEvent(new CustomEvent('edrv:file-open-tool-change', { detail: { value: selected } }))
167
192
  }
168
193
  sync()
@@ -222,8 +247,8 @@ export function apply(ctx: any): void {
222
247
  }
223
248
  retryRemoteOpen()
224
249
 
225
- // 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态可用时编辑器住 betterSidebar Tab,
226
- // 本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
250
+ // 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态(官方/better-sidebar)可用时
251
+ // 编辑器住侧边栏 Tab,本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
227
252
  const registerLegacyTab = (): (() => void) | null => {
228
253
  return registerSlotSafely(ctx, {
229
254
  name: 'conversation.view',
@@ -234,16 +259,51 @@ export function apply(ctx: any): void {
234
259
  }, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })))
235
260
  }
236
261
 
237
- // 侧边栏/中央页签两形态互斥切换:sideDisposer(侧边栏 Tab)与 legacyDisposer(中央页签)只保留其一。
238
- // ⚠️ betterSidebar 服务由 dsh-better-sidebar 的 client bundle(1MB+)在页面加载时提供,本插件
239
- // bundle 较小往往先执行完启动检测——一次性探测会误报「未检测到」并永久停在中央页签形态(实测
240
- // 竞态:better-sidebar 已装,但「文件编辑」Tab 未注册)。故启动后按 2s 间隔重试(窗口 ~30s),
241
- // 命中即自动切换侧边栏形态;始终未命中则保持中央页签回退。
262
+ // 侧边栏三形态互斥切换:官方(DSH 0.1.5+)> better-sidebar(归档,仅旧版回退)> 中央页签。
263
+ // officialDisposer / sideDisposer / legacyDisposer 任一时刻只保留其一(避免双实例 Monaco 抢占)。
264
+ // ⚠️ 两个可选服务都可能晚于本插件就绪(better-sidebar bundle 1MB+ 后加载;官方服务旧 DSH
265
+ // 不存在),一次性探测会误报——故启动后按 2s 间隔重试(窗口 ~30s):每 tick 先探官方再探
266
+ // better-sidebar;better-sidebar 先命中而官方后到时自动切换(官方优先),窗口结束仍未命中
267
+ // 则保持中央页签回退。
268
+ let officialDisposer: (() => void) | null = null
242
269
  let sideDisposer: (() => void) | null = null
243
270
  let legacyDisposer: (() => void) | null = null
271
+ const installOfficialForm = (): void => {
272
+ const official = officialService
273
+ if (officialDisposer !== null || !official) return
274
+ if (sideDisposer !== null) { sideDisposer(); sideDisposer = null }
275
+ if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
276
+ const outcome = installOfficial({
277
+ tabs: official.tabs,
278
+ service: official.service,
279
+ slots: ctx.slots,
280
+ renderTab: officialRenderTab,
281
+ guide: {
282
+ title: () => OFFICIAL_TAB_TITLE,
283
+ description: () => 'Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)',
284
+ },
285
+ })
286
+ if (outcome === null) {
287
+ // 官方类型注册失败(API 变更等):撤下官方标记,回退旧形态链
288
+ officialService = undefined
289
+ applySideForm()
290
+ return
291
+ }
292
+ officialDisposer = ctx.effect(() => outcome, 'vscode-mode: official sidebar editor tab')
293
+ // 官方打开器(下拉「官方侧边栏」选项,动态出现;openResource 能力缺失时跳过)+ 按当前设置同步 file 地址认领
294
+ if (typeof official.service.openResource === 'function') {
295
+ ctx.effect(() => registry.register(officialSidebarOpener(official.service)), 'vscode-mode: official sidebar opener')
296
+ }
297
+ syncFileClaim()
298
+ }
244
299
  const applySideForm = (): void => {
300
+ if (officialService) {
301
+ installOfficialForm()
302
+ return
303
+ }
245
304
  if (sideService) {
246
305
  if (sideDisposer !== null) return
306
+ if (officialDisposer !== null) { officialDisposer(); officialDisposer = null }
247
307
  if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
248
308
  sideDisposer = ctx.effect(() => installSideEditor({
249
309
  service: sideService as NonNullable<typeof sideService>,
@@ -256,27 +316,35 @@ export function apply(ctx: any): void {
256
316
  },
257
317
  registerLegacyFallback: registerLegacyTab,
258
318
  }), 'vscode-mode: sidebar editor tab')
259
- } else {
260
- if (legacyDisposer !== null) return
261
- if (sideDisposer !== null) { sideDisposer(); sideDisposer = null }
262
- setEnsureSideEditor(null)
263
- legacyDisposer = registerLegacyTab()
319
+ return
264
320
  }
321
+ if (legacyDisposer !== null) return
322
+ if (officialDisposer !== null) { officialDisposer(); officialDisposer = null }
323
+ if (sideDisposer !== null) { sideDisposer(); sideDisposer = null }
324
+ setEnsureSideEditor(null)
325
+ legacyDisposer = registerLegacyTab()
265
326
  }
266
327
  applySideForm()
267
328
  let sideRetries = 0
268
329
  const retrySideService = (): void => {
269
- if (sideService !== undefined || sideRetries >= 15) return
330
+ if (officialService !== undefined || sideRetries >= 15) return
270
331
  sideRetries += 1
271
332
  schedule(() => {
272
- if (sideService !== undefined) return
273
- sideService = detectSidebarService(ctx)
274
- if (sideService !== undefined) {
275
- log.info('检测到 ' + SIDEBAR_PLUGIN + ',切换侧边栏编辑形态')
333
+ if (officialService !== undefined) return
334
+ officialService = detectOfficial(ctx)
335
+ if (officialService !== undefined) {
336
+ log.info('检测到官方右侧 Sidebar(DSH 0.1.5+),切换官方侧边栏编辑形态')
276
337
  applySideForm()
277
- } else {
278
- retrySideService()
338
+ return
339
+ }
340
+ if (sideService === undefined) {
341
+ sideService = detectSidebarService(ctx)
342
+ if (sideService !== undefined) {
343
+ log.info('检测到 ' + SIDEBAR_PLUGIN + ',切换侧边栏编辑形态(官方未探测到,后续仍优先官方)')
344
+ applySideForm()
345
+ }
279
346
  }
347
+ retrySideService()
280
348
  }, 2000)
281
349
  }
282
350
  retrySideService()
@@ -0,0 +1,281 @@
1
+ /**
2
+ * dsh-vscode-mode client — 官方右侧 Sidebar 桥:探测 DSH 0.1.5-alpha.1+ 的
3
+ * ctx.sidebarRightTabs / ctx.sidebarRight 服务,两段式注册「文件编辑」Tab
4
+ * (类型定义进注册表,正文进 keyed slot sidebar.right.pane.tab)。
5
+ * 可选依赖模式:服务缺失时调用方回退 better-sidebar / 中央页签;本模块不注册任何东西。
6
+ * 维护约定:官方侧边栏(DSH 0.1.5+)是侧边栏编辑形态的唯一维护面;
7
+ * dsh-better-sidebar 桥(sidebarBridge.ts)已归档,仅作旧版 DSH 回退,不再新增能力。
8
+ * 纯逻辑(探测/导航参数解析)可单测,注册装配仅存在于 install 路径。
9
+ * 作者 ddj 2026年09月09号
10
+ */
11
+
12
+ import { log } from './log.js'
13
+ import { setEnsureSideEditor } from './sidebarBridge.js'
14
+
15
+ /** 官方 Tab 类型注册表服务名(ctx.get 用;不进 inject——旧版 DSH 无此包,inject 会停等)。 */
16
+ export const OFFICIAL_TABS_SERVICE = 'sidebarRightTabs'
17
+ /** 官方导航控制器服务名(ctx.get 用)。 */
18
+ export const OFFICIAL_SERVICE = 'sidebarRight'
19
+ /** 官方 keyed slot 名(Tab 正文挂点,由官方侧边栏的 rightbar seat 声明)。 */
20
+ export const OFFICIAL_SLOT_NAME = 'sidebar.right.pane.tab'
21
+ /** 本插件 Tab 类型定义 id(官方约定用包名;正文 slot 的 key 与之相同)。 */
22
+ export const OFFICIAL_TAB_ID = 'dsh-vscode-mode'
23
+ /** 本插件 Tab 的类型判别符(ctx.sidebarRight.openTab 的 kind)。 */
24
+ export const OFFICIAL_TAB_KIND = 'edrvEditor'
25
+ /** Tab 标题(与 better-sidebar 形态一致)。 */
26
+ export const OFFICIAL_TAB_TITLE = '文件编辑'
27
+ /** 官方 file 资源地址前缀(0.1.5+ 聊天文件链接/文件树打开的统一地址语法)。 */
28
+ export const OFFICIAL_FILE_PREFIX = 'dsh-resource://file/'
29
+ /** file 地址认领的 patterns(extension 档接管官方 textpreview 的 fallback 认领,注销自动回落)。 */
30
+ export const OFFICIAL_FILE_PATTERN = 'dsh-resource://file/**'
31
+ /** file 资源类型的定义 id(正文 slot 的 key)。 */
32
+ export const OFFICIAL_FILE_TAB_ID = 'dsh-vscode-mode/file'
33
+ /** file 资源类型的 kind(与页类型 edrvEditor 区分:同 kind 只允许一份 extension)。 */
34
+ export const OFFICIAL_FILE_KIND = 'edrvEditorFile'
35
+
36
+ /** 官方 Tab 打开参数(官方运行时不校验的 JSON,正文经 navigation.params 读回)。 */
37
+ export interface OfficialTabParams {
38
+ openPath?: string
39
+ focusDiff?: boolean
40
+ }
41
+
42
+ /** sidebarRightTabs 服务的最小结构面(结构性探测,不 import 官方类型)。 */
43
+ export interface SidebarRightTabsLike {
44
+ register: (definition: unknown) => () => void
45
+ }
46
+
47
+ /** sidebarRight 服务的最小结构面(openTab 页路由 + openResource 资源路由)。 */
48
+ export interface SidebarRightServiceLike {
49
+ openTab: (kind: string, options?: { params?: unknown }) => void
50
+ openResource?: (address: string, options?: { params?: unknown }) => void
51
+ }
52
+
53
+ /** slots 服务的最小结构面(等待声明 + 注册)。 */
54
+ export interface SlotsLike {
55
+ inject: (name: string, register: () => unknown) => unknown
56
+ register: (spec: object, render: unknown) => unknown
57
+ }
58
+
59
+ /** 探测命中的一对官方服务。 */
60
+ export interface OfficialSidebar {
61
+ tabs: SidebarRightTabsLike
62
+ service: SidebarRightServiceLike
63
+ }
64
+
65
+ /**
66
+ * 结构化探测官方右侧 Sidebar 服务(可选依赖:缺失/降级返回 undefined)。
67
+ * 服务仅存在于 DSH 0.1.5-alpha.1+,探测命中 ≡ 版本判定成立。
68
+ * @author ddj 2026年09月09号
69
+ * @param ctx 客户端服务上下文
70
+ * @returns 注册表与控制器对,或 undefined
71
+ */
72
+ export function detectOfficial(ctx: { get: (name: string) => unknown }): OfficialSidebar | undefined {
73
+ try {
74
+ const tabs = ctx.get(OFFICIAL_TABS_SERVICE) as SidebarRightTabsLike | undefined
75
+ const service = ctx.get(OFFICIAL_SERVICE) as SidebarRightServiceLike | undefined
76
+ if (!tabs || typeof tabs.register !== 'function') return undefined
77
+ if (!service || typeof service.openTab !== 'function') return undefined
78
+ return { tabs, service }
79
+ } catch {
80
+ return undefined
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 从官方导航参数安全解析打开请求(缺字段/坏类型一律降级为空请求,不抛错)。
86
+ * @author ddj 2026年09月09号
87
+ * @param params openTab 传入的 params(正文经 navigation.params 读回)
88
+ * @returns 待打开路径与是否聚焦首个差异
89
+ */
90
+ export function resolveNavOpen(params: unknown): { path: string | null; focusDiff: boolean } {
91
+ const value = (params ?? {}) as OfficialTabParams
92
+ const path = typeof value?.openPath === 'string' && value.openPath ? value.openPath : null
93
+ return { path, focusDiff: value?.focusDiff === true }
94
+ }
95
+
96
+ /** 逐段 component 解码(失败返回 null,对齐官方 parseFileAddress 的容错语义)。 */
97
+ function decodeSegment(raw: string | undefined): string | null {
98
+ if (raw === undefined) return null
99
+ try {
100
+ return decodeURIComponent(raw)
101
+ } catch {
102
+ return null
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 解析官方 file 资源地址(镜像 dsh-util-workspace-path 的 parseFileAddress 语法,不 import 官方包)。
108
+ * session 域:`dsh-resource://file/session/<sessionId>/<逐段编码的相对路径>`;
109
+ * absolute 域:`dsh-resource://file/absolute/<去前导 / 的绝对路径>`(POSIX 补回前导 /,
110
+ * Windows 盘符 `C:/` 原样,UNC 保留空首段 `//server/share`)。
111
+ * @author ddj 2026年09月09号
112
+ * @param address 候选地址
113
+ * @returns 文件路径(session 域附带 sessionId);非 file 地址/畸形/解码失败返回 null
114
+ */
115
+ export function parseOfficialFileAddress(address: unknown): { path: string; sessionId?: string } | null {
116
+ if (typeof address !== 'string' || !address.startsWith(OFFICIAL_FILE_PREFIX)) return null
117
+ const [scope, ...tail] = address.slice(OFFICIAL_FILE_PREFIX.length).split('/')
118
+ const kind = decodeSegment(scope)
119
+ if (kind === 'session') {
120
+ const sessionId = decodeSegment(tail[0])
121
+ const segments = tail.slice(1).map(decodeSegment)
122
+ if (!sessionId || segments.length === 0 || segments.some((s) => s === null)) return null
123
+ const path = (segments as string[]).join('/')
124
+ return path ? { path, sessionId } : null
125
+ }
126
+ if (kind === 'absolute') {
127
+ const unc = tail[0] === '' && tail.length > 1
128
+ const segments = (unc ? tail.slice(1) : tail).map(decodeSegment)
129
+ if (segments.length === 0 || (segments as string[])[0] === '' || segments.some((s) => s === null)) return null
130
+ const list = segments as string[]
131
+ if (unc) return { path: '//' + list.join('/') }
132
+ if (/^[A-Za-z]:$/.test(list[0])) return { path: list.join('/') }
133
+ return { path: '/' + list.join('/') }
134
+ }
135
+ return null
136
+ }
137
+
138
+ /**
139
+ * file 地址的 Tab chip 标题:解码后的 basename(解码失败回退原始串切片)。
140
+ * @author ddj 2026年09月09号
141
+ * @param address file 资源地址
142
+ * @returns 标题文本
143
+ */
144
+ export function officialFileTitle(address: unknown): string {
145
+ const parsed = parseOfficialFileAddress(address)
146
+ const source = parsed?.path ?? (typeof address === 'string' ? address : '')
147
+ const normalized = source.replace(/\\/g, '/')
148
+ return normalized.slice(normalized.lastIndexOf('/') + 1) || source
149
+ }
150
+
151
+ /** component 编码一段(对齐官方 encodeSegment:盘符冒号保持字面)。 */
152
+ function encodeSegment(segment: string): string {
153
+ return encodeURIComponent(segment).replace(/%3A/gi, ':')
154
+ }
155
+
156
+ /**
157
+ * 构造官方 file 资源地址(镜像 fileAddressFor 的分派:绝对路径→absolute 域,相对→session 域)。
158
+ * @author ddj 2026年09月09号
159
+ * @param path 绝对或工作区相对路径(两种分隔符均可)
160
+ * @param sessionId 会话 id(相对路径必需;绝对路径可省)
161
+ * @returns `dsh-resource://file/…` 地址
162
+ * @throws 路径为空,或相对路径缺 sessionId
163
+ */
164
+ export function buildFileAddress(path: string, sessionId?: string): string {
165
+ const posix = (typeof path === 'string' ? path : '').replace(/\\/g, '/')
166
+ if (!posix.trim()) throw new Error('buildFileAddress:路径为空')
167
+ const isAbsolute = posix.startsWith('/') || posix.startsWith('//') || /^[A-Za-z]:[/\\]/.test(posix)
168
+ if (isAbsolute) {
169
+ const unc = posix.startsWith('//')
170
+ const body = posix.replace(/^\/+/, '')
171
+ return OFFICIAL_FILE_PREFIX + 'absolute/' + (unc ? '/' : '') + body.split('/').map(encodeSegment).join('/')
172
+ }
173
+ if (!sessionId) throw new Error('buildFileAddress:相对路径需要会话 id')
174
+ const relative = posix.replace(/^(?:\.\/)+/, '').replace(/^\/+/, '')
175
+ return OFFICIAL_FILE_PREFIX + 'session/' + encodeSegment(sessionId) + '/' + relative.split('/').map(encodeSegment).join('/')
176
+ }
177
+
178
+ /**
179
+ * 注册官方 Tab 正文(keyed slot,key=定义 id;slots.inject 等待 rightbar seat 声明)。
180
+ * @author ddj 2026年09月09号
181
+ * @param slots slots 服务
182
+ * @param key 正文注册键(= 类型定义 id)
183
+ * @param renderTab 正文组件(包装后的 OfficialSideTab)
184
+ * @returns 注销函数;注册抛错返回 null
185
+ */
186
+ function registerBodySlot(slots: SlotsLike, key: string, renderTab: unknown): (() => void) | null {
187
+ try {
188
+ const disposer = slots.inject(OFFICIAL_SLOT_NAME, () => slots.register(
189
+ { name: OFFICIAL_SLOT_NAME, key },
190
+ renderTab,
191
+ ))
192
+ return typeof disposer === 'function' ? (disposer as () => void) : null
193
+ } catch (error) {
194
+ log.warn('官方侧边栏正文 slot 注册失败(' + String(error) + ')')
195
+ return null
196
+ }
197
+ }
198
+
199
+ /**
200
+ * 注册官方右侧 Sidebar「文件编辑」Tab 并接管打开路由(两段式 + 路由)。
201
+ * 调用方须把返回值挂进 ctx.effect;类型注册抛错时返回 null,由调用方回退旧形态。
202
+ * @author ddj 2026年09月09号
203
+ * @param options 装配参数(探测命中的一对服务 + slots + 正文组件 + guide 文案)
204
+ * @returns 卸载器(反注册类型与正文 + 撤销路由);类型注册失败返回 null
205
+ */
206
+ export function installOfficial(options: {
207
+ tabs: SidebarRightTabsLike
208
+ service: SidebarRightServiceLike
209
+ slots: SlotsLike
210
+ renderTab: unknown
211
+ guide: { title: () => string; description: () => string }
212
+ }): (() => void) | null {
213
+ const { tabs, service, slots, renderTab, guide } = options
214
+ const open = (path: string | null, focusDiff: boolean): boolean => {
215
+ try {
216
+ // 官方语义:打开即展开侧栏;seat 未挂载(无会话画面)时抛错,降级其他打开器
217
+ service.openTab(OFFICIAL_TAB_KIND, { params: { openPath: path || undefined, focusDiff } })
218
+ } catch (error) {
219
+ log.warn('官方侧边栏打开失败(' + String(error) + ')')
220
+ return false
221
+ }
222
+ return true
223
+ }
224
+ let typeDisposer: (() => void) | null = null
225
+ try {
226
+ // extension 档(默认且最高):与官方内置类型同 kind 时接管,卸载后官方恢复
227
+ typeDisposer = tabs.register({
228
+ id: OFFICIAL_TAB_ID,
229
+ kind: OFFICIAL_TAB_KIND,
230
+ priority: 'extension',
231
+ title: () => OFFICIAL_TAB_TITLE,
232
+ guide: [{ order: 20, title: guide.title, description: guide.description }],
233
+ })
234
+ } catch (error) {
235
+ log.warn('官方侧边栏 Tab 类型注册失败(' + String(error) + '),回退旧形态')
236
+ return null
237
+ }
238
+ setEnsureSideEditor(open)
239
+ const bodyDisposer = registerBodySlot(slots, OFFICIAL_TAB_ID, renderTab)
240
+ return () => {
241
+ if (typeof typeDisposer === 'function') typeDisposer()
242
+ if (typeof bodyDisposer === 'function') bodyDisposer()
243
+ setEnsureSideEditor(null)
244
+ }
245
+ }
246
+
247
+ /**
248
+ * 注册官方 file 资源地址认领(extension 档接管 `dsh-resource://file/**`,
249
+ * 聊天文件链接/文件树打开改落本插件编辑器;注销后官方 textpreview 自动恢复)。
250
+ * 认领与否由「文件链接使用工具」设置驱动(见 fileOpeners.shouldClaimFiles)。
251
+ * @author ddj 2026年09月09号
252
+ * @param options 装配参数(探测命中的注册表 + slots + 正文组件)
253
+ * @returns 卸载器(反注册类型与正文);类型注册抛错返回 null(回落官方查看器)
254
+ */
255
+ export function registerOfficialFileClaim(options: {
256
+ tabs: SidebarRightTabsLike
257
+ slots: SlotsLike
258
+ renderTab: unknown
259
+ }): (() => void) | null {
260
+ const { tabs, slots, renderTab } = options
261
+ let typeDisposer: (() => void) | null = null
262
+ try {
263
+ typeDisposer = tabs.register({
264
+ id: OFFICIAL_FILE_TAB_ID,
265
+ kind: OFFICIAL_FILE_KIND,
266
+ patterns: [OFFICIAL_FILE_PATTERN],
267
+ priority: 'extension',
268
+ // 畸形/解码失败地址否决:回落官方 textpreview,不产生白屏 Tab
269
+ canOpen: (address: string) => parseOfficialFileAddress(address) !== null,
270
+ title: (address: string) => officialFileTitle(address),
271
+ })
272
+ } catch (error) {
273
+ log.warn('官方 file 地址认领注册失败(' + String(error) + '),链接走官方查看器')
274
+ return null
275
+ }
276
+ const bodyDisposer = registerBodySlot(slots, OFFICIAL_FILE_TAB_ID, renderTab)
277
+ return () => {
278
+ if (typeof typeDisposer === 'function') typeDisposer()
279
+ if (typeof bodyDisposer === 'function') bodyDisposer()
280
+ }
281
+ }
@@ -1713,7 +1713,7 @@ export function EditorView(props) {
1713
1713
  }
1714
1714
  const sideHintEl = (layout === 'tab' && sideHint && !hintDismissed)
1715
1715
  ? React.createElement('div', { className: 'edrv-side-hint' },
1716
- React.createElement('span', { className: 'edrv-side-hint-text' }, '安装 dsh-better-sidebar 后可启用侧边栏编辑(对话+编辑同屏):'),
1716
+ React.createElement('span', { className: 'edrv-side-hint-text' }, 'DSH 0.1.5+ 自带官方右侧侧边栏(升级即用);旧版可安装 dsh-better-sidebar 启用侧边栏编辑(对话+编辑同屏):'),
1717
1717
  React.createElement('code', { className: 'edrv-side-hint-cmd' }, SIDEBAR_INSTALL_CMD),
1718
1718
  React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
1719
1719
  React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
@@ -0,0 +1,73 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — OfficialSideTab:DSH 官方右侧 Sidebar「文件编辑」Tab 正文包装。
4
+ * 双用途:页类型 Tab(edrvEditor)经 navigation.params(openPath/focusDiff)打开;
5
+ * file 资源类型 Tab(edrvEditorFile,认领 dsh-resource://file/**)经 navigation.address
6
+ * 解码出真实路径并透传 params.line 行号定位。派发既有 edrv:open-editor 事件复用
7
+ * EditorView 打开流;按会话 key 保证切会话干净重建。
8
+ * Tab 正文切换后重挂载时按最新导航恢复打开(对齐 better-sidebar 形态的种子行为)。
9
+ * 作者 ddj 2026年09月09号
10
+ */
11
+ import React from 'react'
12
+ import { EditorView } from './EditorView.js'
13
+ import { setSideEditorMounted } from '../sidebarBridge.js'
14
+ import { parseOfficialFileAddress, resolveNavOpen } from '../officialSidebar.js'
15
+
16
+ /**
17
+ * 官方侧边栏 Tab 正文组件(keyed slot sidebar.right.pane.tab 装配)。
18
+ * @param props slot 框架注入(useTabInfo/sessionId)+ 装配依赖(schedule 等)
19
+ * @returns EditorView 侧栏形态
20
+ */
21
+ export function OfficialSideTab(props) {
22
+ const sessionId = props?.sessionId
23
+ const useTabInfo = props?.useTabInfo
24
+ let navigation
25
+ // useTabInfo 是 slot 框架按组件实例生成的 hook,须在组件顶层无条件调用
26
+ if (typeof useTabInfo === 'function') {
27
+ try {
28
+ navigation = useTabInfo()?.tab?.navigation
29
+ } catch {
30
+ navigation = undefined
31
+ }
32
+ }
33
+ const revision = typeof navigation?.revision === 'number' ? navigation.revision : -1
34
+ const revisionRef = React.useRef(-1)
35
+
36
+ React.useEffect(() => {
37
+ setSideEditorMounted(true)
38
+ return () => setSideEditorMounted(false)
39
+ }, [])
40
+
41
+ React.useEffect(() => {
42
+ if (revision === revisionRef.current) return
43
+ revisionRef.current = revision
44
+ // revision 0 = 无地址打开(种子/撤销恢复),没有可执行的打开请求
45
+ if (revision <= 0) return
46
+ // file 资源 Tab:地址即文件(解码出真实路径,透传 params.line 行号定位)
47
+ const fileHit = parseOfficialFileAddress(navigation?.address)
48
+ if (fileHit) {
49
+ const line = navigation?.params?.line
50
+ window.dispatchEvent(new CustomEvent('edrv:open-editor', {
51
+ detail: { path: fileHit.path, line: typeof line === 'number' ? line : undefined },
52
+ }))
53
+ return
54
+ }
55
+ // 页类型 Tab:params 携带打开请求
56
+ const request = resolveNavOpen(navigation?.params)
57
+ window.dispatchEvent(new CustomEvent('edrv:open-editor', {
58
+ detail: { path: request.path, focusDiff: request.focusDiff },
59
+ }))
60
+ })
61
+
62
+ return React.createElement(EditorView, {
63
+ key: sessionId,
64
+ sessionId,
65
+ layout: 'side',
66
+ schedule: props?.schedule,
67
+ addToConversation: props?.addToConversation,
68
+ sidebarPanels: props?.sidebarPanels,
69
+ outlineSources: props?.outlineSources,
70
+ fileMenuItems: props?.fileMenuItems,
71
+ sessions: props?.sessions,
72
+ })
73
+ }