dsh-vscode-mode 0.1.59 → 0.1.61

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.61",
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_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab } 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'
@@ -96,6 +98,24 @@ export function apply(ctx: any): void {
96
98
  setSession(list.getSnapshot()?.current)
97
99
  }), 'vscode-mode: lsp session sync')
98
100
  }
101
+ // 会话切换 → 编辑 Tab 跨会话自动恢复:上一会话编辑区激活时,新会话自动展开并激活
102
+ //(官方停靠面按会话隔离,新会话默认收起;先试 openTabIn 直写新会话面,未 adopt 走有界重试)。
103
+ // 注意:首次打开某会话时通知早于 current 字段落定,故通知只调度延迟任务,执行时再读最新快照
104
+ if (monacoList && typeof monacoList.subscribe === 'function') {
105
+ const list = monacoList as { getSnapshot: () => { current?: string }; subscribe: (listener: () => void) => () => void }
106
+ ctx.effect(() => {
107
+ let lastCurrent = list.getSnapshot?.()?.current
108
+ const check = () => {
109
+ const current = list.getSnapshot?.()?.current
110
+ if (!current || current === lastCurrent) return
111
+ lastCurrent = current
112
+ if (!isEditorTabActive() || officialService === undefined) return
113
+ try { officialService.service.openTabIn?.(current, OFFICIAL_TAB_KIND, {}) } catch { /* 未 adopt 等 seat 就绪走重试 */ }
114
+ restoreEditorTab(officialService.service, schedule)
115
+ }
116
+ return list.subscribe(() => schedule(check, 0))
117
+ }, 'vscode-mode: editor tab restore')
118
+ }
99
119
  const originalOpenPath = workspaces?.openPath
100
120
  const binder = pickSettingsBinder(ctx)
101
121
  const settings = binder.scope
@@ -113,6 +133,10 @@ export function apply(ctx: any): void {
113
133
  }
114
134
  /** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
115
135
  let sideService = detectSidebarService(ctx)
136
+ /** 可选探测官方右侧 Sidebar(DSH 0.1.5-alpha.1+;探测命中 ≡ 版本判定,不进 inject 同上)。 */
137
+ let officialService = detectOfficial(ctx)
138
+ /** 官方 file 地址认领的当前注销器(null=未认领;由 syncFileClaim 依设置值切换)。 */
139
+ let claimDisposer: (() => void) | null = null
116
140
  /** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
117
141
  const addToConversation = createAddToConversation(ctx)
118
142
 
@@ -122,7 +146,9 @@ export function apply(ctx: any): void {
122
146
  { name: '文件打开路由(workspaces.openPath)', active: Boolean(workspaces?.openPath), note: 'vscode 打开器优先,失败回退系统打开(0.1.2 及更早 DSH 的对话文件链接主路径)' },
123
147
  { name: '会话文件链接路由(remote.session.openWorkspacePath)', active: remoteOpenInstalled, note: '0.1.3+ 对话文件引用/产物打开优先走插件打开器,失败回退系统打开' },
124
148
  { 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 + ')' },
149
+ { name: '侧边栏编辑区(官方 Sidebar)', active: officialService !== undefined, note: officialService !== undefined ? '编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)' : '官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)' },
150
+ { 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 ? '聊天文件链接由本插件编辑器接管(官方侧边栏内打开)' : '链接走官方查看器或旧版路由(未认领)' },
126
152
  ]
127
153
 
128
154
  ctx.provide('fileOpeners', registry)
@@ -157,12 +183,29 @@ export function apply(ctx: any): void {
157
183
  const sidebar = scanSidebar(ctx)
158
184
  return sidebar ? registry.register(sidebar) : undefined
159
185
  }, 'vscode-mode: sidebar file opener')
186
+ // 官方侧边栏正文组件装配(页类型与 file 认领两类 Tab 共用同一 EditorView 形态)
187
+ const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
188
+ /** 官方 file 地址认领同步:自动/VSCodeMode 档认领(链接进本插件编辑器),其余交官方查看器。 */
189
+ const syncFileClaim = (): void => {
190
+ const official = officialService
191
+ const want = official !== undefined && shouldClaimFiles(selected)
192
+ if (want && claimDisposer === null && official) {
193
+ const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab: officialRenderTab })
194
+ if (disposer !== null) claimDisposer = ctx.effect(() => disposer, 'vscode-mode: official file claim')
195
+ return
196
+ }
197
+ if (!want && claimDisposer !== null) {
198
+ claimDisposer()
199
+ claimDisposer = null
200
+ }
201
+ }
160
202
  ctx.effect(() => {
161
203
  if (!settings) return undefined
162
204
  const sync = (): void => {
163
205
  const snapshot = settings.getSnapshot()
164
206
  if (snapshot.status === 'loading') return
165
207
  selected = autoValue(snapshot.value?.fileOpenTool)
208
+ syncFileClaim()
166
209
  window.dispatchEvent(new CustomEvent('edrv:file-open-tool-change', { detail: { value: selected } }))
167
210
  }
168
211
  sync()
@@ -222,8 +265,8 @@ export function apply(ctx: any): void {
222
265
  }
223
266
  retryRemoteOpen()
224
267
 
225
- // 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态可用时编辑器住 betterSidebar Tab,
226
- // 本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
268
+ // 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态(官方/better-sidebar)可用时
269
+ // 编辑器住侧边栏 Tab,本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
227
270
  const registerLegacyTab = (): (() => void) | null => {
228
271
  return registerSlotSafely(ctx, {
229
272
  name: 'conversation.view',
@@ -234,16 +277,51 @@ export function apply(ctx: any): void {
234
277
  }, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })))
235
278
  }
236
279
 
237
- // 侧边栏/中央页签两形态互斥切换:sideDisposer(侧边栏 Tab)与 legacyDisposer(中央页签)只保留其一。
238
- // ⚠️ betterSidebar 服务由 dsh-better-sidebar 的 client bundle(1MB+)在页面加载时提供,本插件
239
- // bundle 较小往往先执行完启动检测——一次性探测会误报「未检测到」并永久停在中央页签形态(实测
240
- // 竞态:better-sidebar 已装,但「文件编辑」Tab 未注册)。故启动后按 2s 间隔重试(窗口 ~30s),
241
- // 命中即自动切换侧边栏形态;始终未命中则保持中央页签回退。
280
+ // 侧边栏三形态互斥切换:官方(DSH 0.1.5+)> better-sidebar(归档,仅旧版回退)> 中央页签。
281
+ // officialDisposer / sideDisposer / legacyDisposer 任一时刻只保留其一(避免双实例 Monaco 抢占)。
282
+ // ⚠️ 两个可选服务都可能晚于本插件就绪(better-sidebar bundle 1MB+ 后加载;官方服务旧 DSH
283
+ // 不存在),一次性探测会误报——故启动后按 2s 间隔重试(窗口 ~30s):每 tick 先探官方再探
284
+ // better-sidebar;better-sidebar 先命中而官方后到时自动切换(官方优先),窗口结束仍未命中
285
+ // 则保持中央页签回退。
286
+ let officialDisposer: (() => void) | null = null
242
287
  let sideDisposer: (() => void) | null = null
243
288
  let legacyDisposer: (() => void) | null = null
289
+ const installOfficialForm = (): void => {
290
+ const official = officialService
291
+ if (officialDisposer !== null || !official) return
292
+ if (sideDisposer !== null) { sideDisposer(); sideDisposer = null }
293
+ if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
294
+ const outcome = installOfficial({
295
+ tabs: official.tabs,
296
+ service: official.service,
297
+ slots: ctx.slots,
298
+ renderTab: officialRenderTab,
299
+ guide: {
300
+ title: () => OFFICIAL_TAB_TITLE,
301
+ description: () => 'Monaco 文件编辑器:对话与编辑同屏(差异审查/跳转/大纲)',
302
+ },
303
+ })
304
+ if (outcome === null) {
305
+ // 官方类型注册失败(API 变更等):撤下官方标记,回退旧形态链
306
+ officialService = undefined
307
+ applySideForm()
308
+ return
309
+ }
310
+ officialDisposer = ctx.effect(() => outcome, 'vscode-mode: official sidebar editor tab')
311
+ // 官方打开器(下拉「官方侧边栏」选项,动态出现;openResource 能力缺失时跳过)+ 按当前设置同步 file 地址认领
312
+ if (typeof official.service.openResource === 'function') {
313
+ ctx.effect(() => registry.register(officialSidebarOpener(official.service)), 'vscode-mode: official sidebar opener')
314
+ }
315
+ syncFileClaim()
316
+ }
244
317
  const applySideForm = (): void => {
318
+ if (officialService) {
319
+ installOfficialForm()
320
+ return
321
+ }
245
322
  if (sideService) {
246
323
  if (sideDisposer !== null) return
324
+ if (officialDisposer !== null) { officialDisposer(); officialDisposer = null }
247
325
  if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
248
326
  sideDisposer = ctx.effect(() => installSideEditor({
249
327
  service: sideService as NonNullable<typeof sideService>,
@@ -256,27 +334,35 @@ export function apply(ctx: any): void {
256
334
  },
257
335
  registerLegacyFallback: registerLegacyTab,
258
336
  }), '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()
337
+ return
264
338
  }
339
+ if (legacyDisposer !== null) return
340
+ if (officialDisposer !== null) { officialDisposer(); officialDisposer = null }
341
+ if (sideDisposer !== null) { sideDisposer(); sideDisposer = null }
342
+ setEnsureSideEditor(null)
343
+ legacyDisposer = registerLegacyTab()
265
344
  }
266
345
  applySideForm()
267
346
  let sideRetries = 0
268
347
  const retrySideService = (): void => {
269
- if (sideService !== undefined || sideRetries >= 15) return
348
+ if (officialService !== undefined || sideRetries >= 15) return
270
349
  sideRetries += 1
271
350
  schedule(() => {
272
- if (sideService !== undefined) return
273
- sideService = detectSidebarService(ctx)
274
- if (sideService !== undefined) {
275
- log.info('检测到 ' + SIDEBAR_PLUGIN + ',切换侧边栏编辑形态')
351
+ if (officialService !== undefined) return
352
+ officialService = detectOfficial(ctx)
353
+ if (officialService !== undefined) {
354
+ log.info('检测到官方右侧 Sidebar(DSH 0.1.5+),切换官方侧边栏编辑形态')
276
355
  applySideForm()
277
- } else {
278
- retrySideService()
356
+ return
357
+ }
358
+ if (sideService === undefined) {
359
+ sideService = detectSidebarService(ctx)
360
+ if (sideService !== undefined) {
361
+ log.info('检测到 ' + SIDEBAR_PLUGIN + ',切换侧边栏编辑形态(官方未探测到,后续仍优先官方)')
362
+ applySideForm()
363
+ }
279
364
  }
365
+ retrySideService()
280
366
  }, 2000)
281
367
  }
282
368
  retrySideService()
@@ -4,7 +4,7 @@
4
4
  * 后退回到上一处焦点,前进回到下一处,跨文件生效。
5
5
  * 数据模型:past(末位 = 当前)+ future 两个栈;record 压栈并清空 future;
6
6
  * 与栈顶同 path+line+column 视为同一位置:仅刷新 viewState 快照,不压栈、不清 future。
7
- * 纯逻辑无 DOM 依赖,可单测。
7
+ * 纯逻辑无 DOM 依赖,可单测;navHistoryFor 提供按工作区作用域的实例仓(跨会话复用)。
8
8
  * 作者 ddj 2026年09月04号
9
9
  */
10
10
 
@@ -41,6 +41,12 @@ export interface NavHistory {
41
41
  /** 后退栈默认容量上限(超出逐出最旧)。 */
42
42
  export const NAV_HISTORY_CAP = 200
43
43
 
44
+ /** 工作区历史实例缓存上限(FIFO 逐出最久未用作用域,防长期驻留泄漏)。 */
45
+ export const NAV_SCOPE_CAP = 6
46
+
47
+ /** scope → 导航历史实例(同工作区跨会话复用同一份历史)。 */
48
+ const scopeHistories = new Map<string, NavHistory>()
49
+
44
50
  /** 路径一致(容忍相对路径大小写差异,对齐既有 sameFile 语义常用写法)。 */
45
51
  function samePath(a: string, b: string): boolean {
46
52
  if (a === b) return true
@@ -102,3 +108,24 @@ export function createNavHistory(cap = NAV_HISTORY_CAP): NavHistory {
102
108
  },
103
109
  }
104
110
  }
111
+
112
+ /**
113
+ * 取(或建)某工作区作用域的导航历史实例:同 scope 跨会话复用同一实例,
114
+ * 后退/前进历史在切换对话后保留(对齐「同一工作区同一编辑区」体感)。
115
+ * 超出 NAV_SCOPE_CAP 时逐出最旧的实例(Map 插入序近似 LRU)。
116
+ * @author ddj 2026年09月09号
117
+ * @param scope 作用域键(scopeStore.workspaceScopeOf 产物)
118
+ * @param cap 后退栈容量上限(透传 createNavHistory)
119
+ * @returns 导航历史实例
120
+ */
121
+ export function navHistoryFor(scope: string, cap = NAV_HISTORY_CAP): NavHistory {
122
+ const existing = scopeHistories.get(scope)
123
+ if (existing) return existing
124
+ const created = createNavHistory(cap)
125
+ scopeHistories.set(scope, created)
126
+ if (scopeHistories.size > NAV_SCOPE_CAP) {
127
+ const oldest = scopeHistories.keys().next().value
128
+ if (typeof oldest === 'string') scopeHistories.delete(oldest)
129
+ }
130
+ return created
131
+ }