dsh-vscode-mode 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
5
5
  "icon": "assets/icon.svg",
6
6
  "keywords": [
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import React from 'react'
17
17
  import './styles/editor.css'
18
+ import './styles/quickmenu.css'
18
19
  import { EditorView } from './ui/EditorView.js'
19
20
  import { DiffBadge } from './ui/DiffBadge.js'
20
21
  import { PerfSizeBadge } from './ui/PerfSizeBadge.js'
@@ -43,6 +44,12 @@ import { SideEditorTab } from './ui/SideEditorTab.js'
43
44
  import { OfficialSideTab } from './ui/OfficialSideTab.js'
44
45
  import { createClaimRouter } from './ui/ClaimRouter.js'
45
46
  import { createAddToConversation } from './addToConversation.js'
47
+ import { createMenuStore } from './quickMenuStore.js'
48
+ import { QuickMenuLauncher } from './ui/QuickMenuLauncher.js'
49
+ import type { QuickMenuHost, QuickSendResult } from './quickMenuHost.js'
50
+ import type { RuleListSnapshot } from '../shared/quickMenu.js'
51
+ import type { SkillCatalog } from '../shared/skillCatalog.js'
52
+ import { toastDom } from './externalOpen.js'
46
53
  import { createSidebarPanelRegistry } from './sidebar/registry.js'
47
54
  import { createFilePanel } from './sidebar/panels/index.js'
48
55
  import { createSearchPanel } from './sidebar/panels/index.js'
@@ -285,6 +292,144 @@ export async function apply(ctx: any): Promise<void> {
285
292
  /** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
286
293
  const addToConversation = createAddToConversation(ctx)
287
294
 
295
+ /**
296
+ * 读取规则列表(rules.list;失败抛错,由快捷菜单按「未校验」降级)。
297
+ * @author ddj 2026年10月30号
298
+ * @returns 用户规则 + 项目规则
299
+ */
300
+ const loadRules = async (): Promise<RuleListSnapshot> => {
301
+ const result = await rpc('rules.list', {})
302
+ if (!result.ok) throw new Error(result.error)
303
+ return { user: result.user, projects: result.projects }
304
+ }
305
+
306
+ /**
307
+ * 会话技能枚举服务(remote.skills 动态注入;旧版 DSH 无该服务时保持 null = 「未校验」)。
308
+ * @author ddj 2026年10月30号
309
+ */
310
+ let skillEnumerator: { list?: (req: { sessionId: string }) => Promise<{ skills?: Array<{ name?: string }> }> } | null = null
311
+
312
+ /**
313
+ * 读取会话可见技能名(服务不可用时返回 null,界面按「未校验」处理)。
314
+ * @author ddj 2026年10月30号
315
+ * @param sessionId 会话 id
316
+ * @returns 技能名清单或 null
317
+ */
318
+ const loadSkillNames = async (sessionId: string): Promise<string[] | null> => {
319
+ const list = skillEnumerator?.list
320
+ if (typeof list !== 'function') return null
321
+ const result = await list.call(skillEnumerator, { sessionId })
322
+ // remote 命名空间返回 Typert 信封 { ok, value },旧版也可能直接给裸值,两种都吃。
323
+ const payload = (result as { value?: { skills?: Array<{ name?: string }> } }).value ?? (result as { skills?: Array<{ name?: string }> })
324
+ const names = (payload?.skills ?? []).map((entry) => entry.name).filter((name): name is string => typeof name === 'string')
325
+ return names
326
+ }
327
+
328
+ /**
329
+ * 读取技能候选(全局技能 + 当前工作区技能;只读枚举,失败返回 null)。
330
+ * @author ddj 2026年10月30号
331
+ * @param cwd 目标工作区(缺省取当前会话作用域)
332
+ * @returns 技能候选快照;host 无该 RPC 或读取失败时为 null
333
+ */
334
+ const loadSkillCatalog = async (cwd?: string): Promise<SkillCatalog | null> => {
335
+ const target = cwd ?? readSessionScope(ctx).cwd
336
+ try {
337
+ const result = await rpc('skills.list', target ? { cwd: target } : {})
338
+ if (!result.ok) return null
339
+ return {
340
+ user: result.user,
341
+ project: result.project,
342
+ ...(result.projectRoot ? { projectRoot: result.projectRoot } : {}),
343
+ }
344
+ } catch {
345
+ return null
346
+ }
347
+ }
348
+
349
+ /**
350
+ * 确保存在可写入的会话(无会话时新建独立会话)。
351
+ * @author ddj 2026年10月30号
352
+ * @returns 会话 id 或错误文案
353
+ */
354
+ const ensureSession = async (): Promise<{ id: string } | { error: string }> => {
355
+ const scope = readSessionScope(ctx)
356
+ if (scope.sessionId) return { id: scope.sessionId }
357
+ const draft = await addToConversation.startDraftSession(scope.cwd)
358
+ if (!draft.ok || !draft.id) return { error: '当前没有可用会话,新建会话也失败了' }
359
+ return { id: draft.id }
360
+ }
361
+
362
+ /**
363
+ * 发送快捷菜单文本(优先发到当前会话,会话忙碌时如实回报)。
364
+ * @author ddj 2026年10月30号
365
+ * @param text 合成后的消息文本
366
+ * @returns 是否发送成功
367
+ */
368
+ const sendQuickText = async (text: string): Promise<QuickSendResult> => {
369
+ const session = await ensureSession()
370
+ if ('error' in session) return { ok: false, message: session.error }
371
+ const sent = await addToConversation.sendTask(session.id, text)
372
+ return sent ? { ok: true } : { ok: false, message: '发送被拒绝(会话可能正忙,稍后再点)' }
373
+ }
374
+
375
+ /**
376
+ * 只把快捷菜单文本填入输入框。
377
+ * @author ddj 2026年10月30号
378
+ * @param text 合成后的消息文本
379
+ * @returns 是否填入成功
380
+ */
381
+ const fillQuickText = async (text: string): Promise<QuickSendResult> => {
382
+ const session = await ensureSession()
383
+ if ('error' in session) return { ok: false, message: session.error }
384
+ const outcome = await addToConversation.appendText(session.id, text)
385
+ if (outcome === 'ok' || outcome === 'busy') return { ok: true }
386
+ return { ok: false, message: '写入输入框失败(' + outcome + '),请重试' }
387
+ }
388
+
389
+ /** 快捷菜单宿主桥接:菜单组件不直接摸 ctx,规则/技能/发送/工作区都从这里注入。 */
390
+ const quickMenuHost: QuickMenuHost = {
391
+ store: createMenuStore({
392
+ rules: loadRules,
393
+ skills: loadSkillNames,
394
+ scope: () => {
395
+ const scope = readSessionScope(ctx)
396
+ return { sessionId: scope.sessionId, cwd: scope.cwd }
397
+ },
398
+ }),
399
+ scope: () => {
400
+ const scope = readSessionScope(ctx)
401
+ return { sessionId: scope.sessionId, cwd: scope.cwd }
402
+ },
403
+ send: sendQuickText,
404
+ fill: fillQuickText,
405
+ rules: loadRules,
406
+ skills: async () => {
407
+ const scope = readSessionScope(ctx)
408
+ return scope.sessionId ? loadSkillNames(scope.sessionId) : null
409
+ },
410
+ skillCatalog: loadSkillCatalog,
411
+ readRule: async (binding) => {
412
+ const result = await rpc('rules.read', {
413
+ scope: binding.scope,
414
+ file: binding.file,
415
+ ...(binding.workspacePath ? { workspacePath: binding.workspacePath } : {}),
416
+ ...(binding.origin ? { origin: binding.origin } : {}),
417
+ })
418
+ if (!result.ok) throw new Error(result.error)
419
+ return result.content
420
+ },
421
+ workspaces: () => (workspaces?.list.getSnapshot().items ?? []).map((item: { path: string; title: string }) => ({ path: item.path, title: item.title })),
422
+ notify: (message: string) => { toastDom(message) },
423
+ }
424
+
425
+ // 技能枚举是可选服务:按需动态注入,缺失时不影响悬浮按钮与其余功能(旧版 DSH 自然降级)。
426
+ if (typeof ctx.inject === 'function') {
427
+ ctx.inject(['remote.skills'], (scope: { remote?: { skills?: typeof skillEnumerator } }) => {
428
+ skillEnumerator = scope.remote?.skills ?? null
429
+ void quickMenuHost.store.refreshBindings(true)
430
+ })
431
+ }
432
+
288
433
  /**
289
434
  * Build applicable client diagnostics from live installation state.
290
435
  * @private @author ddj 2026年10月13号
@@ -696,6 +841,27 @@ export async function apply(ctx: any): Promise<void> {
696
841
  order: 40,
697
842
  }, (props: unknown) => React.createElement(PluginDetailSection, Object.assign({}, props)))
698
843
 
844
+ // 快捷菜单悬浮按钮:挂在 shell.overlay(全框浮层,层本身 click-through,条目自己接管指针)。
845
+ // 旧版 DSH 没有该槽时 slots.inject 会一直等待而不报错——按钮自然不出现即为「不支持」,
846
+ // 因此这里不做版本号判断,只按能力探测。
847
+ ctx.effect(() => {
848
+ const dispose = registerSlotSafely(ctx, {
849
+ name: 'shell.overlay',
850
+ id: 'edrv-quick-menu',
851
+ order: 120,
852
+ label: '快捷菜单',
853
+ }, () => React.createElement(QuickMenuLauncher, { host: quickMenuHost }))
854
+ return () => {
855
+ dispose?.()
856
+ void quickMenuHost.store.flushLauncher()
857
+ }
858
+ }, 'vscode-mode: quick menu launcher')
859
+
860
+ // 切换会话/工作区后重算绑定状态(规则与技能都跟会话可见性有关)
861
+ ctx.effect(() => subscribeScope(ctx, () => {
862
+ void quickMenuHost.store.refreshBindings(true)
863
+ }), 'vscode-mode: quick menu scope refresh')
864
+
699
865
  // 卸载收尾(G4,DSH 0.1.6-alpha.2 起支持运行时卸载/重载):
700
866
  // 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
701
867
  // 不做会导致重载后重复注册(补全/跳转/hover 各翻倍)与陈旧开关残留。
@@ -0,0 +1,46 @@
1
+ /**
2
+ * dsh-vscode-mode client — 快捷菜单与宿主能力的桥接契约。
3
+ *
4
+ * 菜单组件不直接依赖 ctx:规则/技能/工作区列表与「发送到对话」都由 client 入口注入,
5
+ * 这样菜单可以在无会话、无远程服务时优雅降级(返回 null 或空列表)。
6
+ * 作者 ddj 2026年10月30号
7
+ */
8
+ import type { QuickMenuStore, QuickMenuScope } from './quickMenuStore.js'
9
+ import type { RuleBinding, RuleListSnapshot } from '../shared/quickMenu.js'
10
+ import type { SkillCatalog } from '../shared/skillCatalog.js'
11
+
12
+ /** 宿主提供的发送结果。 */
13
+ export interface QuickSendResult {
14
+ ok: boolean
15
+ /** 失败/跳过的说明(直接给用户看)。 */
16
+ message?: string
17
+ }
18
+
19
+ /** 工作区精简视图(限定工作区选择用)。 */
20
+ export interface QuickWorkspace {
21
+ path: string
22
+ title: string
23
+ }
24
+
25
+ /** 菜单组件可用的宿主能力。 */
26
+ export interface QuickMenuHost {
27
+ store: QuickMenuStore
28
+ /** 当前会话与工作区(每次调用取最新值)。 */
29
+ scope(): QuickMenuScope
30
+ /** 发送文本到当前会话(内部负责新建会话兜底)。 */
31
+ send(text: string): Promise<QuickSendResult>
32
+ /** 只把文本填入当前输入框(不发送)。 */
33
+ fill(text: string): Promise<QuickSendResult>
34
+ /** 规则列表(rules.list)。 */
35
+ rules(): Promise<RuleListSnapshot>
36
+ /** 当前会话可见技能名;无会话返回 null(供 validateSkill 判定,与候选列表分开)。 */
37
+ skills(): Promise<string[] | null>
38
+ /** 技能候选(全局技能 + 当前工作区技能);枚举失败返回 null。 */
39
+ skillCatalog(cwd?: string): Promise<SkillCatalog | null>
40
+ /** 读取一条规则的正文(内联注入用)。 */
41
+ readRule(binding: RuleBinding): Promise<string>
42
+ /** 已注册工作区列表。 */
43
+ workspaces(): QuickWorkspace[]
44
+ /** 轻提示。 */
45
+ notify(text: string): void
46
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * dsh-vscode-mode client — 快捷菜单 store(文档 + 绑定快照 + 坐标防抖)。
3
+ *
4
+ * 为什么客户端要 store:菜单在多列里同时展示,多列共享同一份文档与绑定校验结果;
5
+ * 用外部 store + useSyncExternalStore,避免把整棵树塞进一个组件的 state。
6
+ * 写入一律「先改内存 → 整体提交 → host 归一后回传」;冲突(其他窗口改过)时重拉,
7
+ * 不拿本地旧版本覆盖磁盘。
8
+ * 作者 ddj 2026年10月30号
9
+ */
10
+ import { defaultDoc, descendantIds, newQuickId, normalizeDoc } from '../shared/quickMenu.js'
11
+ import type { QuickMenuDoc, QuickMenuItem, RuleListSnapshot, ValidateContext } from '../shared/quickMenu.js'
12
+ import { rpc } from './rpc.js'
13
+
14
+ /** 菜单加载状态。 */
15
+ export interface QuickMenuSnapshot {
16
+ status: 'idle' | 'loading' | 'ready' | 'error'
17
+ /** 当前文档(loading/error 时为默认或上次成功的文档)。 */
18
+ doc: QuickMenuDoc
19
+ /** 读取/保存失败文案。 */
20
+ error?: string
21
+ /** host 读取告警(配置损坏已重置等)。 */
22
+ warning?: string
23
+ }
24
+
25
+ /** 当前会话与工作区快照。 */
26
+ export interface QuickMenuScope {
27
+ sessionId?: string
28
+ cwd?: string
29
+ }
30
+
31
+ /** 外部数据加载器(由 client 入口注入,避免 store 依赖 ctx 细节)。 */
32
+ export interface QuickMenuLoaders {
33
+ /** 读取规则列表(rules.list)。 */
34
+ rules(): Promise<RuleListSnapshot>
35
+ /** 读取会话可见技能名;无法枚举时返回 null。 */
36
+ skills(sessionId: string): Promise<string[] | null>
37
+ /** 当前会话与工作区(每次调用取最新值)。 */
38
+ scope(): QuickMenuScope
39
+ }
40
+
41
+ /** 一次写操作结果。 */
42
+ export interface QuickMenuMutation {
43
+ ok: boolean
44
+ error?: string
45
+ }
46
+
47
+ /** store 对外能力。 */
48
+ export interface QuickMenuStore {
49
+ subscribe(listener: () => void): () => void
50
+ /** 版本号(useSyncExternalStore 的快照值)。 */
51
+ getVersion(): number
52
+ getState(): QuickMenuSnapshot
53
+ /** 当前校验上下文(绑定失效判定用)。 */
54
+ getContext(): ValidateContext
55
+ load(force?: boolean): Promise<void>
56
+ save(doc: QuickMenuDoc): Promise<QuickMenuMutation>
57
+ createItem(draft: QuickMenuItem): Promise<QuickMenuMutation>
58
+ updateItem(next: QuickMenuItem): Promise<QuickMenuMutation>
59
+ removeItem(id: string): Promise<QuickMenuMutation>
60
+ /** 记录悬浮按钮坐标(500ms 防抖落盘)。 */
61
+ patchLauncher(x: number | null, y: number | null): void
62
+ /** 立即落盘待写坐标(松手/卸载时调用)。 */
63
+ flushLauncher(): Promise<void>
64
+ refreshBindings(force?: boolean): Promise<void>
65
+ /** 悬浮按钮是否已挂载(= 当前 DSH 具备 shell.overlay)。 */
66
+ markMounted(mounted: boolean): void
67
+ isMounted(): boolean
68
+ /** 生成新项 id。 */
69
+ nextId(type: 'group' | 'button'): string
70
+ }
71
+
72
+ /** 绑定快照有效期(菜单打开/聚焦时刷新,期间复用)。 */
73
+ const BINDINGS_TTL_MS = 60_000
74
+ /** 坐标落盘防抖。 */
75
+ const LAUNCHER_DEBOUNCE_MS = 500
76
+
77
+ /**
78
+ * 创建快捷菜单 store。
79
+ * @author ddj 2026年10月30号
80
+ * @param loaders 外部数据加载器
81
+ * @returns store 实例
82
+ */
83
+ export function createMenuStore(loaders: QuickMenuLoaders): QuickMenuStore {
84
+ let state: QuickMenuSnapshot = { status: 'idle', doc: defaultDoc() }
85
+ let version = 0
86
+ let mounted = false
87
+ const listeners = new Set<() => void>()
88
+ /** 绑定快照(cwd/sessionId 变化或超期即重拉)。 */
89
+ let bindings: { at: number; cwd?: string; sessionId?: string; rules?: RuleListSnapshot; skillNames?: string[] | null } = { at: 0 }
90
+ /** 待落盘坐标与定时器。 */
91
+ let pending: { x: number | null; y: number | null } | null = null
92
+ let timer: ReturnType<typeof setTimeout> | null = null
93
+
94
+ /**
95
+ * 写入状态并通知订阅者。
96
+ * @author ddj 2026年10月30号
97
+ * @param next 新状态
98
+ */
99
+ function publish(next: QuickMenuSnapshot): void {
100
+ state = next
101
+ version += 1
102
+ for (const listener of listeners) listener()
103
+ }
104
+
105
+ /**
106
+ * 合并补丁到当前状态。
107
+ * @author ddj 2026年10月30号
108
+ * @param patch 局部更新
109
+ */
110
+ function patch(patchState: Partial<QuickMenuSnapshot>): void {
111
+ publish({ ...state, ...patchState })
112
+ }
113
+
114
+ /**
115
+ * 读取规则与技能快照(失败保留旧值,不打断菜单)。
116
+ * @author ddj 2026年10月30号
117
+ * @param force 是否强制重拉
118
+ */
119
+ async function refreshBindings(force = false): Promise<void> {
120
+ const scope = loaders.scope()
121
+ const fresh = !force && Date.now() - bindings.at < BINDINGS_TTL_MS
122
+ if (fresh && bindings.cwd === scope.cwd && bindings.sessionId === scope.sessionId) return
123
+ bindings = { ...bindings, at: Date.now(), cwd: scope.cwd, sessionId: scope.sessionId }
124
+ try {
125
+ bindings.rules = await loaders.rules()
126
+ } catch {
127
+ // 规则服务不可用:保持 undefined(界面显示「未校验」,不误报失效)
128
+ bindings.rules = undefined
129
+ }
130
+ try {
131
+ bindings.skillNames = scope.sessionId ? await loaders.skills(scope.sessionId) : null
132
+ } catch {
133
+ bindings.skillNames = null
134
+ }
135
+ version += 1
136
+ for (const listener of listeners) listener()
137
+ }
138
+
139
+ /**
140
+ * 拉取文档(并顺带刷新绑定快照)。
141
+ * @author ddj 2026年10月30号
142
+ * @param force 是否强制重拉
143
+ */
144
+ async function load(force = false): Promise<void> {
145
+ if (state.status === 'loading') return
146
+ patch({ status: 'loading' })
147
+ try {
148
+ const result = await rpc('quickMenu.get', {})
149
+ if (result.ok) patch({ status: 'ready', doc: result.doc, warning: result.warning, error: undefined })
150
+ else patch({ status: 'error', error: result.error })
151
+ } catch (error) {
152
+ patch({ status: 'error', error: '读取快捷菜单失败:' + String(error) })
153
+ }
154
+ await refreshBindings(force)
155
+ }
156
+
157
+ /**
158
+ * 提交整份文档(host 归一后回传,冲突时重拉)。
159
+ * @author ddj 2026年10月30号
160
+ * @param doc 待提交文档
161
+ * @returns 写结果
162
+ */
163
+ async function save(doc: QuickMenuDoc): Promise<QuickMenuMutation> {
164
+ try {
165
+ const result = await rpc('quickMenu.save', { doc, baseRevision: state.doc.revision })
166
+ if (result.ok) {
167
+ patch({ status: 'ready', doc: result.doc, error: undefined, warning: undefined })
168
+ return { ok: true }
169
+ }
170
+ await load(true)
171
+ return { ok: false, error: result.error }
172
+ } catch (error) {
173
+ return { ok: false, error: '保存快捷菜单失败:' + String(error) }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * 新增或替换一项(按 id 去重)。
179
+ * @author ddj 2026年10月30号
180
+ * @param draft 菜单项
181
+ * @returns 写结果
182
+ */
183
+ async function upsert(draft: QuickMenuItem): Promise<QuickMenuMutation> {
184
+ const items = state.doc.items.filter((item) => item.id !== draft.id)
185
+ items.push(draft)
186
+ return save(normalizeDoc({ ...state.doc, items }))
187
+ }
188
+
189
+ /**
190
+ * 删除一项及其全部子项。
191
+ * @author ddj 2026年10月30号
192
+ * @param id 项 id
193
+ * @returns 写结果
194
+ */
195
+ async function removeItem(id: string): Promise<QuickMenuMutation> {
196
+ const doomed = descendantIds(state.doc, id)
197
+ const items = state.doc.items.filter((item) => !doomed.includes(item.id))
198
+ return save(normalizeDoc({ ...state.doc, items }))
199
+ }
200
+
201
+ /**
202
+ * 定时把待写坐标落盘。
203
+ * @author ddj 2026年10月30号
204
+ */
205
+ async function flushLauncher(): Promise<void> {
206
+ if (timer !== null) {
207
+ clearTimeout(timer)
208
+ timer = null
209
+ }
210
+ const target = pending
211
+ pending = null
212
+ if (!target) return
213
+ try {
214
+ const result = await rpc('quickMenu.launcher', target)
215
+ if (result.ok) patch({ doc: result.doc })
216
+ } catch {
217
+ // 位置保存失败无碍主流程:下次拖动会重试
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 记录坐标并防抖落盘。
223
+ * @author ddj 2026年10月30号
224
+ * @param x 横坐标
225
+ * @param y 纵坐标
226
+ */
227
+ function patchLauncher(x: number | null, y: number | null): void {
228
+ pending = { x, y }
229
+ if (timer !== null) clearTimeout(timer)
230
+ timer = setTimeout(() => { void flushLauncher() }, LAUNCHER_DEBOUNCE_MS)
231
+ }
232
+
233
+ return {
234
+ subscribe(listener: () => void): () => void {
235
+ listeners.add(listener)
236
+ return () => { listeners.delete(listener) }
237
+ },
238
+ getVersion(): number { return version },
239
+ getState(): QuickMenuSnapshot { return state },
240
+ getContext(): ValidateContext {
241
+ const scope = loaders.scope()
242
+ return { cwd: scope.cwd, rules: bindings.rules, skillNames: bindings.skillNames ?? null }
243
+ },
244
+ load,
245
+ save,
246
+ createItem: upsert,
247
+ updateItem: upsert,
248
+ removeItem,
249
+ patchLauncher,
250
+ flushLauncher,
251
+ refreshBindings,
252
+ markMounted(value: boolean): void {
253
+ mounted = value
254
+ version += 1
255
+ for (const listener of listeners) listener()
256
+ },
257
+ isMounted(): boolean { return mounted },
258
+ nextId(type: 'group' | 'button'): string { return newQuickId(type) },
259
+ }
260
+ }
@@ -1,5 +1,6 @@
1
1
  /** Source-aware metadata projection; official patches never become legacy hunks. @author ddj 2026年10月26号 */
2
2
  import { PENDING_PREFIX } from '../../shared/pendingReview.js'
3
+ import { pathKeyFor } from '../../shared/pathKey.js'
3
4
  import type { ReviewPage } from '../../shared/review.js'
4
5
  import type { RecordView } from '../../shared/types.js'
5
6
  import type { FileSummary, Summary } from './records.js'
@@ -14,11 +15,15 @@ export interface DiffFile extends FileSummary {
14
15
  }
15
16
  export interface DiffSummary extends Summary { files: DiffFile[]; pendingFiles: DiffFile[] }
16
17
 
17
- /** @public @author ddj 2026年10月26号 @param path Functional local or remote identity. @returns Comparison key without rewriting remote case. */
18
+ /**
19
+ * @public
20
+ * @author ddj 2026年10月26号
21
+ * @description: 客户端路径比较口径:浏览器里没有宿主平台信息,非盘符路径不折叠大小写。
22
+ * @param path 文件身份或相对路径
23
+ * @returns 比较键
24
+ */
18
25
  export function diffPathKey(path: string): string {
19
- if (/^[a-z]+:\/\//i.test(path)) return path
20
- const normalized = path.replace(/\\/g, '/')
21
- return /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized
26
+ return pathKeyFor(path, false)
22
27
  }
23
28
 
24
29
  /** @public @author ddj 2026年10月26号 @param path Current editor file. @param page Authoritative metadata, including decided paths. @returns Whether legacy actions must be hidden for this file. */