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/README.md +21 -11
- package/lib/client.js +445 -20
- package/lib/client.js.map +1 -1
- package/package.json +2 -2
- package/src/client/fileOpeners.ts +41 -1
- package/src/client/index.ts +91 -23
- package/src/client/officialSidebar.ts +281 -0
- package/src/client/ui/EditorView.ts +1 -1
- package/src/client/ui/OfficialSideTab.ts +73 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻
|
|
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
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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: '
|
|
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
|
|
226
|
-
//
|
|
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
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// bundle
|
|
240
|
-
//
|
|
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
|
-
|
|
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 (
|
|
330
|
+
if (officialService !== undefined || sideRetries >= 15) return
|
|
270
331
|
sideRetries += 1
|
|
271
332
|
schedule(() => {
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
log.info('
|
|
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
|
-
|
|
278
|
-
|
|
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' }, '
|
|
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
|
+
}
|