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/README.md +5 -3
- package/lib/client.js +3370 -85
- package/lib/client.js.map +2 -2
- package/lib/index.js +1167 -266
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +166 -0
- package/src/client/quickMenuHost.ts +46 -0
- package/src/client/quickMenuStore.ts +260 -0
- package/src/client/state/diffSummary.ts +9 -4
- package/src/client/styles/quickmenu.css +1049 -0
- package/src/client/ui/QuickMenuDialog.tsx +835 -0
- package/src/client/ui/QuickMenuLauncher.tsx +792 -0
- package/src/client/ui/QuickMenuPickers.ts +577 -0
- package/src/index.ts +2 -2
- package/src/paths.ts +10 -0
- package/src/pending/capture.ts +104 -75
- package/src/pending/persist.ts +6 -1
- package/src/pending/store.ts +10 -5
- package/src/pending/vcs.ts +2 -2
- package/src/pendingCompat.ts +3 -8
- package/src/pendingRpc.ts +140 -58
- package/src/quickMenu.ts +269 -0
- package/src/rpc.ts +37 -0
- package/src/shared/pathKey.ts +54 -0
- package/src/shared/quickMenu.ts +1009 -0
- package/src/shared/quickPick.ts +67 -0
- package/src/shared/rpc.ts +10 -0
- package/src/shared/skillCatalog.ts +60 -0
- package/src/skillCatalog.ts +138 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 快捷菜单配置弹窗的通用选择控件。
|
|
3
|
+
*
|
|
4
|
+
* 提供三件控件:多选下拉(技能/规则)、限定工作区下拉(全部/当前/更多)、设置项开关与分段。
|
|
5
|
+
* 弹层用 position: fixed 挂在 body 之上的坐标,因此不受弹窗 body 的 overflow 裁剪;
|
|
6
|
+
* 组件仍用 React.createElement(项目内无 JSX),样式落在 quickmenu.css 的 edrv-qm-pick* 类上。
|
|
7
|
+
* 作者 ddj 2026年10月30号
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { selectionLabel, toggleWorkspace, workspaceLabel } from '../../shared/quickPick.js'
|
|
11
|
+
|
|
12
|
+
/** 下拉里一个可选项。 */
|
|
13
|
+
export interface PickOption {
|
|
14
|
+
/** 稳定键:技能名 / 工作区绝对路径 / 规则 bindingKey。 */
|
|
15
|
+
key: string
|
|
16
|
+
label: string
|
|
17
|
+
/** 次要说明(来源目录、绝对路径等)。 */
|
|
18
|
+
hint?: string
|
|
19
|
+
/** 校验状态:ok 不显示圆点,warn / error 显示对应颜色圆点。 */
|
|
20
|
+
state?: 'ok' | 'warn' | 'error'
|
|
21
|
+
/** 状态原因(作为 title 提示)。 */
|
|
22
|
+
reason?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 一组可选项(技能按来源、规则按作用域分组)。 */
|
|
26
|
+
export interface PickGroup {
|
|
27
|
+
key: string
|
|
28
|
+
label: string
|
|
29
|
+
options: PickOption[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 一个工作区选项。 */
|
|
33
|
+
export interface PickWorkspaceItem {
|
|
34
|
+
path: string
|
|
35
|
+
title: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 分段控件中的一个值。 */
|
|
39
|
+
export interface PickSegOption {
|
|
40
|
+
value: string
|
|
41
|
+
label: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 弹层位置:靠下展开给 top,向上展开给 bottom。 */
|
|
45
|
+
interface PanelPos {
|
|
46
|
+
left: number
|
|
47
|
+
width: number
|
|
48
|
+
maxHeight: number
|
|
49
|
+
top?: number
|
|
50
|
+
bottom?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 定位弹层需要的句柄。 */
|
|
54
|
+
interface Anchored {
|
|
55
|
+
open: boolean
|
|
56
|
+
style: React.CSSProperties
|
|
57
|
+
triggerRef: React.RefObject<HTMLButtonElement>
|
|
58
|
+
panelRef: React.RefObject<HTMLDivElement>
|
|
59
|
+
toggle(): void
|
|
60
|
+
close(): void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 弹层最小/最大高度(决定向上还是向下展开)。 */
|
|
64
|
+
const PANEL_MIN = 180
|
|
65
|
+
const PANEL_MAX = 320
|
|
66
|
+
/** 弹层与触发器/视口边缘的间距。 */
|
|
67
|
+
const PANEL_GAP = 6
|
|
68
|
+
const EDGE_GAP = 8
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 拼接 className,过滤假值。
|
|
72
|
+
* @author ddj 2026年10月30号
|
|
73
|
+
* @param names 候选类名(false / undefined 会被忽略)
|
|
74
|
+
* @returns 空格分隔的 className
|
|
75
|
+
*/
|
|
76
|
+
function classes(...names: Array<string | false | undefined>): string {
|
|
77
|
+
return names.filter(Boolean).join(' ')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 按触发器位置计算弹层坐标,空间不足时向上展开。
|
|
82
|
+
* @author ddj 2026年10月30号
|
|
83
|
+
* @param trigger 触发器元素
|
|
84
|
+
* @returns 弹层定位参数
|
|
85
|
+
*/
|
|
86
|
+
function placePanel(trigger: HTMLElement): PanelPos {
|
|
87
|
+
const rect = trigger.getBoundingClientRect()
|
|
88
|
+
const width = Math.max(rect.width, 280)
|
|
89
|
+
const below = window.innerHeight - rect.bottom - PANEL_GAP - EDGE_GAP
|
|
90
|
+
const above = rect.top - PANEL_GAP - EDGE_GAP
|
|
91
|
+
const useAbove = below < PANEL_MIN && above > below
|
|
92
|
+
const maxHeight = Math.max(PANEL_MIN, Math.min(PANEL_MAX, useAbove ? above : below))
|
|
93
|
+
const maxLeft = Math.max(EDGE_GAP, window.innerWidth - width - EDGE_GAP)
|
|
94
|
+
const left = Math.min(Math.max(EDGE_GAP, rect.left), maxLeft)
|
|
95
|
+
|
|
96
|
+
if (useAbove) {
|
|
97
|
+
return { left, width, maxHeight, bottom: window.innerHeight - rect.top + PANEL_GAP }
|
|
98
|
+
}
|
|
99
|
+
return { left, width, maxHeight, top: rect.bottom + PANEL_GAP }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 受控弹层的位置与开合状态。
|
|
104
|
+
*
|
|
105
|
+
* Esc 的处理依赖「子组件 effect 先于父组件 effect 执行」:本 hook 在 ModalShell 内部的
|
|
106
|
+
* 子组件里挂载,因此先注册 window 捕获监听,才能在弹层打开时拦下 Esc,避免连外层弹窗一起关掉。
|
|
107
|
+
* @author ddj 2026年10月30号
|
|
108
|
+
* @returns 弹层句柄
|
|
109
|
+
*/
|
|
110
|
+
function useAnchored(): Anchored {
|
|
111
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
112
|
+
const panelRef = React.useRef<HTMLDivElement>(null)
|
|
113
|
+
const [open, setOpen] = React.useState(false)
|
|
114
|
+
const [pos, setPos] = React.useState<PanelPos | null>(null)
|
|
115
|
+
const openRef = React.useRef(false)
|
|
116
|
+
openRef.current = open
|
|
117
|
+
|
|
118
|
+
const close = React.useCallback(() => {
|
|
119
|
+
setOpen(false)
|
|
120
|
+
setPos(null)
|
|
121
|
+
}, [])
|
|
122
|
+
|
|
123
|
+
const toggle = React.useCallback(() => {
|
|
124
|
+
if (openRef.current) {
|
|
125
|
+
close()
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const trigger = triggerRef.current
|
|
130
|
+
setPos(trigger ? placePanel(trigger) : null)
|
|
131
|
+
setOpen(true)
|
|
132
|
+
}, [close])
|
|
133
|
+
|
|
134
|
+
React.useEffect(() => {
|
|
135
|
+
const onKey = (event: KeyboardEvent) => {
|
|
136
|
+
if (event.key !== 'Escape' || !openRef.current) return
|
|
137
|
+
// 只收掉弹层:ModalShell 同样在 window 捕获阶段监听 Esc,必须阻止它继续执行
|
|
138
|
+
event.stopImmediatePropagation()
|
|
139
|
+
event.preventDefault()
|
|
140
|
+
close()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
window.addEventListener('keydown', onKey, true)
|
|
144
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
145
|
+
}, [close])
|
|
146
|
+
|
|
147
|
+
React.useEffect(() => {
|
|
148
|
+
if (!open) return
|
|
149
|
+
|
|
150
|
+
const reposition = () => {
|
|
151
|
+
const trigger = triggerRef.current
|
|
152
|
+
if (trigger) setPos(placePanel(trigger))
|
|
153
|
+
}
|
|
154
|
+
const onDown = (event: MouseEvent) => {
|
|
155
|
+
const target = event.target as Node | null
|
|
156
|
+
if (!target) return
|
|
157
|
+
if (triggerRef.current?.contains(target)) return
|
|
158
|
+
if (panelRef.current?.contains(target)) return
|
|
159
|
+
close()
|
|
160
|
+
}
|
|
161
|
+
const onScroll = (event: Event) => {
|
|
162
|
+
const target = event.target as Node | null
|
|
163
|
+
if (target && panelRef.current?.contains(target)) return
|
|
164
|
+
reposition()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
window.addEventListener('mousedown', onDown, true)
|
|
168
|
+
window.addEventListener('resize', reposition)
|
|
169
|
+
window.addEventListener('scroll', onScroll, true)
|
|
170
|
+
return () => {
|
|
171
|
+
window.removeEventListener('mousedown', onDown, true)
|
|
172
|
+
window.removeEventListener('resize', reposition)
|
|
173
|
+
window.removeEventListener('scroll', onScroll, true)
|
|
174
|
+
}
|
|
175
|
+
}, [open, close])
|
|
176
|
+
|
|
177
|
+
const style: React.CSSProperties = pos
|
|
178
|
+
? { left: pos.left, top: pos.top, bottom: pos.bottom, width: pos.width, maxHeight: pos.maxHeight }
|
|
179
|
+
: {}
|
|
180
|
+
|
|
181
|
+
return { open, style, triggerRef, panelRef, toggle, close }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 收集所有分组的选项键→标签映射。
|
|
186
|
+
* @author ddj 2026年10月30号
|
|
187
|
+
* @param groups 分组选项
|
|
188
|
+
* @returns 选项键到标签的映射
|
|
189
|
+
*/
|
|
190
|
+
function labelsOf(groups: PickGroup[]): Map<string, string> {
|
|
191
|
+
const labels = new Map<string, string>()
|
|
192
|
+
for (const group of groups) {
|
|
193
|
+
for (const option of group.options) labels.set(option.key, option.label)
|
|
194
|
+
}
|
|
195
|
+
return labels
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 统计已选项里带警告/错误状态的数量。
|
|
200
|
+
* @author ddj 2026年10月30号
|
|
201
|
+
* @param groups 分组选项
|
|
202
|
+
* @param selected 已选键
|
|
203
|
+
* @returns 异常状态条目数
|
|
204
|
+
*/
|
|
205
|
+
function issueCount(groups: PickGroup[], selected: string[]): number {
|
|
206
|
+
const picked = new Set(selected)
|
|
207
|
+
let count = 0
|
|
208
|
+
for (const group of groups) {
|
|
209
|
+
for (const option of group.options) {
|
|
210
|
+
if (picked.has(option.key) && option.state && option.state !== 'ok') count += 1
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return count
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 触发器按钮的属性。 */
|
|
217
|
+
interface TriggerProps {
|
|
218
|
+
triggerRef: React.RefObject<HTMLButtonElement>
|
|
219
|
+
buttonId?: string
|
|
220
|
+
summary: string
|
|
221
|
+
open: boolean
|
|
222
|
+
disabled: boolean
|
|
223
|
+
issueCount?: number
|
|
224
|
+
hint?: string
|
|
225
|
+
onClick(): void
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 下拉触发器:显示当前选择摘要与展开箭头。
|
|
230
|
+
* @author ddj 2026年10月30号
|
|
231
|
+
* @param props 触发器属性
|
|
232
|
+
* @returns 触发器按钮
|
|
233
|
+
*/
|
|
234
|
+
function PickTrigger(props: TriggerProps) {
|
|
235
|
+
return React.createElement(
|
|
236
|
+
'button',
|
|
237
|
+
{
|
|
238
|
+
ref: props.triggerRef,
|
|
239
|
+
id: props.buttonId,
|
|
240
|
+
type: 'button',
|
|
241
|
+
className: classes('edrv-qm-pick-trigger', props.open && 'edrv-qm-pick-trigger--open'),
|
|
242
|
+
disabled: props.disabled,
|
|
243
|
+
title: props.hint,
|
|
244
|
+
'aria-haspopup': 'listbox',
|
|
245
|
+
'aria-expanded': props.open,
|
|
246
|
+
onClick: props.onClick,
|
|
247
|
+
},
|
|
248
|
+
React.createElement('span', { className: 'edrv-qm-pick-summary' }, props.summary),
|
|
249
|
+
props.issueCount
|
|
250
|
+
? React.createElement('span', { className: 'edrv-qm-pick-badge' }, String(props.issueCount))
|
|
251
|
+
: null,
|
|
252
|
+
React.createElement('span', { className: 'edrv-qm-pick-caret', 'aria-hidden': 'true' }),
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 弹层属性。 */
|
|
257
|
+
interface PanelProps {
|
|
258
|
+
panelRef: React.RefObject<HTMLDivElement>
|
|
259
|
+
style: React.CSSProperties
|
|
260
|
+
labelledBy?: string
|
|
261
|
+
children?: React.ReactNode
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 绝对定位的弹层容器。
|
|
266
|
+
* @author ddj 2026年10月30号
|
|
267
|
+
* @param props 弹层属性
|
|
268
|
+
* @returns 弹层节点
|
|
269
|
+
*/
|
|
270
|
+
function PickPanel(props: PanelProps) {
|
|
271
|
+
return React.createElement(
|
|
272
|
+
'div',
|
|
273
|
+
{
|
|
274
|
+
ref: props.panelRef,
|
|
275
|
+
className: 'edrv-qm-pick-panel',
|
|
276
|
+
style: props.style,
|
|
277
|
+
role: 'listbox',
|
|
278
|
+
'aria-labelledby': props.labelledBy,
|
|
279
|
+
},
|
|
280
|
+
props.children,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** 单个可选项的属性。 */
|
|
285
|
+
interface OptionProps {
|
|
286
|
+
option: PickOption
|
|
287
|
+
picked: boolean
|
|
288
|
+
onToggle(key: string): void
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 可勾选的多选项行。
|
|
293
|
+
* @author ddj 2026年10月30号
|
|
294
|
+
* @param props 选项属性
|
|
295
|
+
* @returns 选项按钮
|
|
296
|
+
*/
|
|
297
|
+
function PickOptionRow(props: OptionProps) {
|
|
298
|
+
const option = props.option
|
|
299
|
+
const state = option.state ?? 'ok'
|
|
300
|
+
|
|
301
|
+
return React.createElement(
|
|
302
|
+
'button',
|
|
303
|
+
{
|
|
304
|
+
type: 'button',
|
|
305
|
+
role: 'option',
|
|
306
|
+
className: classes('edrv-qm-pick-option', props.picked && 'edrv-qm-pick-option--on'),
|
|
307
|
+
'aria-selected': props.picked,
|
|
308
|
+
title: option.reason ?? option.hint,
|
|
309
|
+
onClick: () => props.onToggle(option.key),
|
|
310
|
+
},
|
|
311
|
+
React.createElement(
|
|
312
|
+
'span',
|
|
313
|
+
{
|
|
314
|
+
className: classes('edrv-qm-pick-check', props.picked && 'edrv-qm-pick-check--on'),
|
|
315
|
+
'aria-hidden': 'true',
|
|
316
|
+
},
|
|
317
|
+
props.picked ? '✓' : '',
|
|
318
|
+
),
|
|
319
|
+
React.createElement('span', { className: 'edrv-qm-pick-name' }, option.label),
|
|
320
|
+
state === 'ok'
|
|
321
|
+
? null
|
|
322
|
+
: React.createElement('span', {
|
|
323
|
+
className: classes('edrv-qm-pick-dot', 'edrv-qm-pick-dot--' + state),
|
|
324
|
+
'aria-hidden': 'true',
|
|
325
|
+
}),
|
|
326
|
+
option.hint ? React.createElement('span', { className: 'edrv-qm-pick-hint' }, option.hint) : null,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** 多选下拉的属性。 */
|
|
331
|
+
export interface PickMultiProps {
|
|
332
|
+
buttonId?: string
|
|
333
|
+
groups: PickGroup[]
|
|
334
|
+
selected: string[]
|
|
335
|
+
/** 摘要计数单位,如「个技能」「条规则」。 */
|
|
336
|
+
unit: string
|
|
337
|
+
emptyText: string
|
|
338
|
+
disabled?: boolean
|
|
339
|
+
disabledHint?: string
|
|
340
|
+
onToggle(key: string): void
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 多选下拉:技能与规则共用,按来源分组展示。
|
|
345
|
+
* @author ddj 2026年10月30号
|
|
346
|
+
* @param props 多选下拉属性
|
|
347
|
+
* @returns 多选下拉组件
|
|
348
|
+
*/
|
|
349
|
+
export function PickMulti(props: PickMultiProps) {
|
|
350
|
+
const anchor = useAnchored()
|
|
351
|
+
const labels = labelsOf(props.groups)
|
|
352
|
+
const summaryNames = props.selected.map((key) => labels.get(key) ?? key)
|
|
353
|
+
const summary = selectionLabel(summaryNames, props.unit)
|
|
354
|
+
const issues = issueCount(props.groups, props.selected)
|
|
355
|
+
const groups = props.groups.filter((group) => group.options.length > 0)
|
|
356
|
+
|
|
357
|
+
const rows = groups.map((group) =>
|
|
358
|
+
React.createElement(
|
|
359
|
+
'div',
|
|
360
|
+
{ key: group.key, className: 'edrv-qm-pick-group' },
|
|
361
|
+
React.createElement('div', { className: 'edrv-qm-pick-group-title' }, group.label),
|
|
362
|
+
group.options.map((option) =>
|
|
363
|
+
React.createElement(PickOptionRow, {
|
|
364
|
+
key: option.key,
|
|
365
|
+
option,
|
|
366
|
+
picked: props.selected.includes(option.key),
|
|
367
|
+
onToggle: props.onToggle,
|
|
368
|
+
}),
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return React.createElement(
|
|
374
|
+
'div',
|
|
375
|
+
{ className: 'edrv-qm-pick' },
|
|
376
|
+
React.createElement(PickTrigger, {
|
|
377
|
+
triggerRef: anchor.triggerRef,
|
|
378
|
+
buttonId: props.buttonId,
|
|
379
|
+
summary,
|
|
380
|
+
open: anchor.open,
|
|
381
|
+
disabled: props.disabled === true,
|
|
382
|
+
issueCount: issues,
|
|
383
|
+
hint: props.disabled ? props.disabledHint : undefined,
|
|
384
|
+
onClick: anchor.toggle,
|
|
385
|
+
}),
|
|
386
|
+
anchor.open
|
|
387
|
+
? React.createElement(
|
|
388
|
+
PickPanel,
|
|
389
|
+
{ panelRef: anchor.panelRef, style: anchor.style, labelledBy: props.buttonId },
|
|
390
|
+
rows.length > 0
|
|
391
|
+
? React.createElement('div', { className: 'edrv-qm-pick-list' }, rows)
|
|
392
|
+
: React.createElement('div', { className: 'edrv-qm-pick-empty' }, props.emptyText),
|
|
393
|
+
)
|
|
394
|
+
: null,
|
|
395
|
+
)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 限定工作区下拉的属性。 */
|
|
399
|
+
export interface PickWorkspaceProps {
|
|
400
|
+
buttonId?: string
|
|
401
|
+
workspaces: PickWorkspaceItem[]
|
|
402
|
+
selected: string[] | null
|
|
403
|
+
cwd?: string
|
|
404
|
+
inherited?: boolean
|
|
405
|
+
inheritedHint?: string
|
|
406
|
+
onChange(next: string[] | null): void
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 限定工作区下拉:全部 / 当前 / 更多(更多展开全部工作区多选)。
|
|
411
|
+
* @author ddj 2026年10月30号
|
|
412
|
+
* @param props 工作区下拉属性
|
|
413
|
+
* @returns 工作区下拉组件
|
|
414
|
+
*/
|
|
415
|
+
export function PickWorkspace(props: PickWorkspaceProps) {
|
|
416
|
+
const anchor = useAnchored()
|
|
417
|
+
const [more, setMore] = React.useState(false)
|
|
418
|
+
const summary = workspaceLabel(props.selected, props.cwd)
|
|
419
|
+
const picked = props.selected ?? []
|
|
420
|
+
const disabled = props.inherited === true
|
|
421
|
+
const currentPath = props.cwd
|
|
422
|
+
|
|
423
|
+
// 全部/当前是「一个值」的快捷项;「更多」不再高亮,避免与下方多选冲突
|
|
424
|
+
const isAll = picked.length === 0
|
|
425
|
+
const isCurrent = picked.length === 1 && !!currentPath && summary.mode === 'current'
|
|
426
|
+
|
|
427
|
+
const onPickAll = () => {
|
|
428
|
+
props.onChange(null)
|
|
429
|
+
anchor.close()
|
|
430
|
+
}
|
|
431
|
+
const onPickCurrent = () => {
|
|
432
|
+
if (!currentPath) return
|
|
433
|
+
props.onChange([currentPath])
|
|
434
|
+
anchor.close()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const quick = [
|
|
438
|
+
React.createElement(PickOptionRow, {
|
|
439
|
+
key: '__all__',
|
|
440
|
+
option: { key: '__all__', label: '全部工作区', hint: '菜单在所有工作区可见' },
|
|
441
|
+
picked: isAll,
|
|
442
|
+
onToggle: onPickAll,
|
|
443
|
+
}),
|
|
444
|
+
React.createElement(PickOptionRow, {
|
|
445
|
+
key: '__current__',
|
|
446
|
+
option: {
|
|
447
|
+
key: '__current__',
|
|
448
|
+
label: '当前工作区',
|
|
449
|
+
hint: currentPath || '当前没有活跃工作区',
|
|
450
|
+
},
|
|
451
|
+
picked: isCurrent,
|
|
452
|
+
onToggle: onPickCurrent,
|
|
453
|
+
}),
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
const rows = props.workspaces.map((workspace) =>
|
|
457
|
+
React.createElement(PickOptionRow, {
|
|
458
|
+
key: workspace.path,
|
|
459
|
+
option: { key: workspace.path, label: workspace.title, hint: workspace.path },
|
|
460
|
+
picked: picked.some((path) => path === workspace.path),
|
|
461
|
+
onToggle: (path: string) => props.onChange(toggleWorkspace(props.selected, path)),
|
|
462
|
+
}),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
const moreToggle = React.createElement(
|
|
466
|
+
'button',
|
|
467
|
+
{
|
|
468
|
+
type: 'button',
|
|
469
|
+
className: classes('edrv-qm-pick-more', more && 'edrv-qm-pick-more--on'),
|
|
470
|
+
'aria-expanded': more,
|
|
471
|
+
onClick: () => setMore(!more),
|
|
472
|
+
},
|
|
473
|
+
more ? '收起工作区列表' : '更多工作区(' + String(props.workspaces.length) + ')',
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return React.createElement(
|
|
477
|
+
'div',
|
|
478
|
+
{ className: 'edrv-qm-pick' },
|
|
479
|
+
React.createElement(PickTrigger, {
|
|
480
|
+
triggerRef: anchor.triggerRef,
|
|
481
|
+
buttonId: props.buttonId,
|
|
482
|
+
summary: summary.label,
|
|
483
|
+
open: anchor.open,
|
|
484
|
+
disabled,
|
|
485
|
+
hint: disabled ? props.inheritedHint : '点开选择生效范围',
|
|
486
|
+
onClick: anchor.toggle,
|
|
487
|
+
}),
|
|
488
|
+
anchor.open
|
|
489
|
+
? React.createElement(
|
|
490
|
+
PickPanel,
|
|
491
|
+
{ panelRef: anchor.panelRef, style: anchor.style, labelledBy: props.buttonId },
|
|
492
|
+
React.createElement('div', { className: 'edrv-qm-pick-list' }, quick),
|
|
493
|
+
moreToggle,
|
|
494
|
+
more ? React.createElement('div', { className: 'edrv-qm-pick-list' }, rows) : null,
|
|
495
|
+
)
|
|
496
|
+
: null,
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** 开关属性。 */
|
|
501
|
+
export interface PickSwitchProps {
|
|
502
|
+
buttonId?: string
|
|
503
|
+
label: string
|
|
504
|
+
hint?: string
|
|
505
|
+
checked: boolean
|
|
506
|
+
disabled?: boolean
|
|
507
|
+
onChange(next: boolean): void
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* 开关行:左侧标题与说明,右侧滑动开关。
|
|
512
|
+
* @author ddj 2026年10月30号
|
|
513
|
+
* @param props 开关属性
|
|
514
|
+
* @returns 开关行
|
|
515
|
+
*/
|
|
516
|
+
export function PickSwitch(props: PickSwitchProps) {
|
|
517
|
+
return React.createElement(
|
|
518
|
+
'div',
|
|
519
|
+
{ className: 'edrv-qm-switch-row' },
|
|
520
|
+
React.createElement(
|
|
521
|
+
'div',
|
|
522
|
+
{ className: 'edrv-qm-switch-text' },
|
|
523
|
+
React.createElement('div', { className: 'edrv-qm-pick-name' }, props.label),
|
|
524
|
+
props.hint ? React.createElement('div', { className: 'edrv-qm-pick-hint' }, props.hint) : null,
|
|
525
|
+
),
|
|
526
|
+
React.createElement(
|
|
527
|
+
'button',
|
|
528
|
+
{
|
|
529
|
+
id: props.buttonId,
|
|
530
|
+
type: 'button',
|
|
531
|
+
role: 'switch',
|
|
532
|
+
className: classes('edrv-qm-switch', props.checked && 'edrv-qm-switch--on'),
|
|
533
|
+
'aria-checked': props.checked,
|
|
534
|
+
'aria-label': props.label,
|
|
535
|
+
disabled: props.disabled === true,
|
|
536
|
+
onClick: () => props.onChange(!props.checked),
|
|
537
|
+
},
|
|
538
|
+
React.createElement('span', { className: 'edrv-qm-switch-knob', 'aria-hidden': 'true' }),
|
|
539
|
+
),
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** 分段控件属性。 */
|
|
544
|
+
export interface PickSegProps {
|
|
545
|
+
options: PickSegOption[]
|
|
546
|
+
value: string
|
|
547
|
+
disabled?: boolean
|
|
548
|
+
label?: string
|
|
549
|
+
onChange(next: string): void
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* 分段控件:等宽按钮组,用于 1–5 层数这类小范围枚举。
|
|
554
|
+
* @author ddj 2026年10月30号
|
|
555
|
+
* @param props 分段控件属性
|
|
556
|
+
* @returns 分段控件
|
|
557
|
+
*/
|
|
558
|
+
export function PickSeg(props: PickSegProps) {
|
|
559
|
+
return React.createElement(
|
|
560
|
+
'div',
|
|
561
|
+
{ className: 'edrv-qm-seg', role: 'group', 'aria-label': props.label },
|
|
562
|
+
props.options.map((option) =>
|
|
563
|
+
React.createElement(
|
|
564
|
+
'button',
|
|
565
|
+
{
|
|
566
|
+
key: option.value,
|
|
567
|
+
type: 'button',
|
|
568
|
+
className: classes('edrv-qm-seg-btn', option.value === props.value && 'edrv-qm-seg-btn--on'),
|
|
569
|
+
'aria-pressed': option.value === props.value,
|
|
570
|
+
disabled: props.disabled === true,
|
|
571
|
+
onClick: () => props.onChange(option.value),
|
|
572
|
+
},
|
|
573
|
+
option.label,
|
|
574
|
+
),
|
|
575
|
+
),
|
|
576
|
+
)
|
|
577
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { registerRoutes } from './routes.js'
|
|
|
3
3
|
import { captureToolResult } from './capture.js'
|
|
4
4
|
import { installCapture } from './officialCapture.js'
|
|
5
5
|
import { installEvidence } from './reviewCapture.js'
|
|
6
|
-
import {
|
|
6
|
+
import { installPending } from './pending/capture.js'
|
|
7
7
|
import { recordWrite } from './pending/store.js'
|
|
8
8
|
import { trackPendingCover } from './pendingCompat.js'
|
|
9
9
|
import { closeArchiveDbs } from './archiveDb.js'
|
|
@@ -75,7 +75,7 @@ function attachRuntime(ctx: Ctx, runtime: Runtime): void {
|
|
|
75
75
|
const evidence = installEvidence(ctx)
|
|
76
76
|
const stopCapture = installCapture(ctx, (cwd, snapshot) => { evidence.persist(cwd, snapshot); return true })
|
|
77
77
|
// 待审累计基线:只有写后内容确实变化才落库,异常一律不打断用户工具调用
|
|
78
|
-
const pending =
|
|
78
|
+
const pending = installPending(ctx, (write) => {
|
|
79
79
|
try { trackPendingCover(write.cwd, write.path, true) } catch { /* 内存登记失败不阻断采集 */ }
|
|
80
80
|
try { recordWrite(write) } catch (error) { log.warn('待审差异落库失败:' + String(error)) }
|
|
81
81
|
})
|
package/src/paths.ts
CHANGED
|
@@ -204,6 +204,16 @@ export function archiveDbFile(cwd: string, home = dshHome()): string {
|
|
|
204
204
|
return join(home, PLUGIN_ID, 'data', 'archive', hashOf(cwd) + '.sqlite')
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* 快捷菜单配置文本(用户数据,不受缓存清理预算约束,跨工作区共享一份)。
|
|
209
|
+
* @author ddj 2026年10月30号
|
|
210
|
+
* @param home DSH home(测试可注入)
|
|
211
|
+
* @returns 快捷菜单配置绝对路径
|
|
212
|
+
*/
|
|
213
|
+
export function quickMenuFile(home = dshHome()): string {
|
|
214
|
+
return join(home, PLUGIN_ID, 'data', 'quick-menu.json')
|
|
215
|
+
}
|
|
216
|
+
|
|
207
217
|
/** 插件包 assets 目录(import.meta.url 派生;config.imageDir 可覆盖,见 imageDirOf)。 */
|
|
208
218
|
export function assetsDirOf(moduleUrl: string): string {
|
|
209
219
|
return join(dirname(fileURLToPath(moduleUrl)), '..', 'assets')
|