dsh-vscode-mode 0.1.17 → 0.1.20
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 +17 -7
- package/lib/client.js +1261 -280
- package/lib/client.js.map +1 -1
- package/lib/index.js +169 -23
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +211 -0
- package/src/client/diffDock.ts +52 -0
- package/src/client/diffDockStore.ts +87 -0
- package/src/client/editorLayout.ts +19 -0
- package/src/client/events.ts +40 -11
- package/src/client/index.ts +48 -5
- package/src/client/sidebar/SidebarView.ts +73 -0
- package/src/client/sidebar/panels/FileExplorer.ts +161 -0
- package/src/client/sidebar/panels/index.ts +24 -0
- package/src/client/sidebar/registry.ts +52 -0
- package/src/client/sidebar/types.ts +31 -0
- package/src/client/styles/editor.css +127 -5
- package/src/client/ui/ConversationDiffDock.ts +84 -0
- package/src/client/ui/DiffBox.ts +78 -29
- package/src/client/ui/EditorView.ts +375 -68
- package/src/rpc.ts +84 -18
- package/src/shared/rpc.ts +29 -1
- package/src/store.ts +6 -2
- package/src/tree.ts +62 -0
|
@@ -13,9 +13,11 @@ import { createDiffRenderer } from '../monaco/diffRender.js'
|
|
|
13
13
|
import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
|
|
14
14
|
import { diffRegions } from '../state/regions.js'
|
|
15
15
|
import { QuickOpen } from './QuickOpen.js'
|
|
16
|
-
import { DiffBox } from './DiffBox.js'
|
|
17
|
-
import { DiffBarEmpty } from './DiffBarEmpty.js'
|
|
18
16
|
import { DiffLauncher } from './DiffLauncher.js'
|
|
17
|
+
import { SidebarView } from '../sidebar/SidebarView.js'
|
|
18
|
+
import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
|
|
19
|
+
import { displayDiffTotal, editorDockMode } from '../diffDock.js'
|
|
20
|
+
import { editorHeight } from '../editorLayout.js'
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
|
|
@@ -26,6 +28,7 @@ import { DiffLauncher } from './DiffLauncher.js'
|
|
|
26
28
|
export function EditorView(props) {
|
|
27
29
|
const sessionId = props?.sessionId
|
|
28
30
|
const schedule = props.schedule
|
|
31
|
+
const addToConversation = props.addToConversation
|
|
29
32
|
const [monaco, setMonaco] = React.useState(null)
|
|
30
33
|
const [monacoErr, setMonacoErr] = React.useState(null)
|
|
31
34
|
const [records, setRecords] = React.useState({})
|
|
@@ -33,6 +36,7 @@ export function EditorView(props) {
|
|
|
33
36
|
const [active, setActive] = React.useState(null)
|
|
34
37
|
const [dirtyMap, setDirtyMap] = React.useState({})
|
|
35
38
|
const [content, setContent] = React.useState(null)
|
|
39
|
+
const [contentPath, setContentPath] = React.useState(null)
|
|
36
40
|
const [status, setStatus] = React.useState('')
|
|
37
41
|
const [error, setError] = React.useState(null)
|
|
38
42
|
const [loadError, setLoadError] = React.useState(null)
|
|
@@ -45,7 +49,13 @@ export function EditorView(props) {
|
|
|
45
49
|
const [hoverAct, setHoverAct] = React.useState(null) // { region, top } 编辑区 hover 差异块的 Keep/Undo 浮层
|
|
46
50
|
const [diffIdx, setDiffIdx] = React.useState(0) // 当前文件内差异位置(x/x 显示)
|
|
47
51
|
const [fileIdx, setFileIdx] = React.useState(0) // 全局差异文件位置(x/x 文件 显示)
|
|
52
|
+
const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
|
|
53
|
+
const [sidebarOn, setSidebarOn] = React.useState(true) // 侧边栏显隐
|
|
54
|
+
const [sidebarW, setSidebarW] = React.useState(240) // 侧边栏宽度
|
|
55
|
+
const [activePanel, setActivePanel] = React.useState('explorer') // 激活面板 id
|
|
56
|
+
const [focusRequest, setFocusRequest] = React.useState(0) // 外部差异聚焦请求版本
|
|
48
57
|
const editorRef = React.useRef(null)
|
|
58
|
+
const viewRootRef = React.useRef(null)
|
|
49
59
|
const monacoRef = React.useRef(null)
|
|
50
60
|
const modelsRef = React.useRef(new Map())
|
|
51
61
|
const saveTimerRef = React.useRef(null)
|
|
@@ -62,6 +72,7 @@ export function EditorView(props) {
|
|
|
62
72
|
const hoverEditorRef = React.useRef(false) // 鼠标是否仍在编辑器命中区
|
|
63
73
|
const hoverPanelRef = React.useRef(false) // 鼠标是否已进入 Keep/Undo 浮层
|
|
64
74
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
75
|
+
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
65
76
|
const diffRendererRef = React.useRef(null)
|
|
66
77
|
if (!diffRendererRef.current) diffRendererRef.current = createDiffRenderer((sid, t) => dbg(sid, t))
|
|
67
78
|
|
|
@@ -71,7 +82,8 @@ export function EditorView(props) {
|
|
|
71
82
|
return list
|
|
72
83
|
}, [records, active])
|
|
73
84
|
|
|
74
|
-
const
|
|
85
|
+
const contentReady = content !== null && contentPath === active
|
|
86
|
+
const regions = React.useMemo(() => diffRegions(currentRecords, contentReady ? content : null).filter((r) => !r.superseded), [currentRecords, content, contentPath, active])
|
|
75
87
|
// useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
|
|
76
88
|
const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
|
|
77
89
|
const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
|
|
@@ -108,9 +120,9 @@ export function EditorView(props) {
|
|
|
108
120
|
})
|
|
109
121
|
}
|
|
110
122
|
|
|
111
|
-
const refreshRecords = () => {
|
|
123
|
+
const refreshRecords = (skipStale) => {
|
|
112
124
|
if (!sessionId) return
|
|
113
|
-
rpc('edrv.list', { sessionId }).then((res) => {
|
|
125
|
+
rpc('edrv.list', { sessionId, ...(skipStale ? { skipStale: true } : {}) }).then((res) => {
|
|
114
126
|
if (!res || !res.ok || !Array.isArray(res.records)) return
|
|
115
127
|
const map = {}
|
|
116
128
|
for (const r of res.records) {
|
|
@@ -129,6 +141,7 @@ export function EditorView(props) {
|
|
|
129
141
|
if (seq !== loadSeqRef.current || path !== active) return
|
|
130
142
|
if (res && res.ok) {
|
|
131
143
|
setContent(res.content)
|
|
144
|
+
setContentPath(path)
|
|
132
145
|
setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
|
|
133
146
|
setStatus('已加载')
|
|
134
147
|
} else {
|
|
@@ -159,9 +172,18 @@ export function EditorView(props) {
|
|
|
159
172
|
React.useEffect(() => {
|
|
160
173
|
const onOpen = (e) => {
|
|
161
174
|
const p = e?.detail?.path
|
|
162
|
-
if (p)
|
|
175
|
+
if (!p) return
|
|
176
|
+
if (e?.detail?.focusDiff === true) {
|
|
177
|
+
pendingFocusRef.current = { path: p, region: null }
|
|
178
|
+
setFocusRequest((value) => value + 1)
|
|
179
|
+
}
|
|
180
|
+
addTab(p, true)
|
|
181
|
+
}
|
|
182
|
+
const onShowLauncher = (event) => {
|
|
183
|
+
const tab = event?.detail?.tab
|
|
184
|
+
if (tab === 'pending' || tab === 'archive') setLauncherTab(tab)
|
|
185
|
+
setLauncherOpen(true)
|
|
163
186
|
}
|
|
164
|
-
const onShowLauncher = () => setLauncherOpen(true)
|
|
165
187
|
window.addEventListener('edrv:open-editor', onOpen)
|
|
166
188
|
window.addEventListener('edrv:show-launcher', onShowLauncher)
|
|
167
189
|
return () => {
|
|
@@ -170,6 +192,64 @@ export function EditorView(props) {
|
|
|
170
192
|
}
|
|
171
193
|
}, [sessionId])
|
|
172
194
|
|
|
195
|
+
// 对话输入区的唯一差异 dock 读取此会话快照;编辑器卸载时只清理自己的发布者令牌。
|
|
196
|
+
const dockSourceRef = React.useRef({})
|
|
197
|
+
React.useEffect(() => () => clearDiffDock(sessionId, dockSourceRef.current), [sessionId])
|
|
198
|
+
|
|
199
|
+
// DSH composer 保持原生布局;编辑区只读取几何边界并同步自己的高度。
|
|
200
|
+
React.useLayoutEffect(() => {
|
|
201
|
+
const root = viewRootRef.current
|
|
202
|
+
const scroll = root?.closest?.('[data-conversation-scroll]')
|
|
203
|
+
if (!root || !scroll) return
|
|
204
|
+
let seat = null
|
|
205
|
+
let frame = null
|
|
206
|
+
let resizeObserver = null
|
|
207
|
+
const update = () => {
|
|
208
|
+
frame = null
|
|
209
|
+
const rootRect = root.getBoundingClientRect()
|
|
210
|
+
const scrollRect = scroll.getBoundingClientRect()
|
|
211
|
+
const seatRect = seat?.getBoundingClientRect?.()
|
|
212
|
+
const height = editorHeight(rootRect.top, scrollRect.bottom, seatRect?.top)
|
|
213
|
+
root.style.setProperty('--edrv-editor-height', height + 'px')
|
|
214
|
+
}
|
|
215
|
+
const scheduleUpdate = () => {
|
|
216
|
+
if (frame !== null) return
|
|
217
|
+
frame = requestAnimationFrame(update)
|
|
218
|
+
}
|
|
219
|
+
const attachSeat = () => {
|
|
220
|
+
const next = scroll.querySelector?.('[data-composer-seat]')
|
|
221
|
+
if (next === seat) {
|
|
222
|
+
scheduleUpdate()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
resizeObserver?.disconnect()
|
|
226
|
+
seat = next
|
|
227
|
+
resizeObserver = null
|
|
228
|
+
if (seat && typeof ResizeObserver === 'function') {
|
|
229
|
+
resizeObserver = new ResizeObserver(scheduleUpdate)
|
|
230
|
+
resizeObserver.observe(seat)
|
|
231
|
+
}
|
|
232
|
+
scheduleUpdate()
|
|
233
|
+
}
|
|
234
|
+
attachSeat()
|
|
235
|
+
const mutationObserver = typeof MutationObserver === 'function'
|
|
236
|
+
? new MutationObserver(attachSeat)
|
|
237
|
+
: null
|
|
238
|
+
mutationObserver?.observe(scroll, { childList: true })
|
|
239
|
+
const scrollObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(scheduleUpdate) : null
|
|
240
|
+
scrollObserver?.observe(scroll)
|
|
241
|
+
window.addEventListener('resize', scheduleUpdate)
|
|
242
|
+
scheduleUpdate()
|
|
243
|
+
return () => {
|
|
244
|
+
if (frame !== null) cancelAnimationFrame(frame)
|
|
245
|
+
resizeObserver?.disconnect()
|
|
246
|
+
scrollObserver?.disconnect()
|
|
247
|
+
mutationObserver?.disconnect()
|
|
248
|
+
window.removeEventListener('resize', scheduleUpdate)
|
|
249
|
+
root.style.removeProperty('--edrv-editor-height')
|
|
250
|
+
}
|
|
251
|
+
}, [sessionId])
|
|
252
|
+
|
|
173
253
|
// localStorage v2 恢复页签
|
|
174
254
|
React.useEffect(() => {
|
|
175
255
|
if (bootRef.current || !sessionId) return
|
|
@@ -192,15 +272,64 @@ export function EditorView(props) {
|
|
|
192
272
|
catch (e) { /* 忽略 */ }
|
|
193
273
|
}, [tabs, active, sessionId])
|
|
194
274
|
|
|
275
|
+
// 侧边栏状态:恢复(显隐/宽度/激活面板)
|
|
276
|
+
React.useEffect(() => {
|
|
277
|
+
if (!sessionId) return
|
|
278
|
+
try {
|
|
279
|
+
const raw = localStorage.getItem('edrv.sidebar.' + String(sessionId))
|
|
280
|
+
if (raw) {
|
|
281
|
+
const saved = JSON.parse(raw)
|
|
282
|
+
if (typeof saved.on === 'boolean') setSidebarOn(saved.on)
|
|
283
|
+
if (typeof saved.width === 'number') setSidebarW(Math.max(180, Math.min(560, saved.width)))
|
|
284
|
+
if (typeof saved.panel === 'string') setActivePanel(saved.panel)
|
|
285
|
+
}
|
|
286
|
+
} catch (e) { /* 损坏忽略 */ }
|
|
287
|
+
}, [sessionId])
|
|
288
|
+
|
|
289
|
+
// 侧边栏状态持久化
|
|
290
|
+
React.useEffect(() => {
|
|
291
|
+
if (!sessionId) return
|
|
292
|
+
try { localStorage.setItem('edrv.sidebar.' + String(sessionId), JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
|
|
293
|
+
catch (e) { /* 忽略 */ }
|
|
294
|
+
}, [sidebarOn, sidebarW, activePanel, sessionId])
|
|
295
|
+
|
|
296
|
+
// Ctrl+B 切换侧边栏(capture 抢占,避免与 DSH 全局冲突)
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
const onKey = (e) => {
|
|
299
|
+
if ((e.ctrlKey || e.metaKey) && String(e.key).toLowerCase() === 'b') {
|
|
300
|
+
e.preventDefault(); e.stopPropagation()
|
|
301
|
+
setSidebarOn((v) => !v)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
window.addEventListener('keydown', onKey, true)
|
|
305
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
306
|
+
}, [])
|
|
307
|
+
|
|
308
|
+
// 文件 → 待处理差异数 映射(侧边栏角标)
|
|
309
|
+
const pendingByPath = React.useMemo(() => {
|
|
310
|
+
const out = {}
|
|
311
|
+
for (const f of sum.files) if (f.pending > 0) out[f.path] = f.pending
|
|
312
|
+
return out
|
|
313
|
+
}, [sum])
|
|
314
|
+
|
|
195
315
|
React.useEffect(() => {
|
|
196
316
|
if (!active) return
|
|
197
317
|
setContent(null)
|
|
318
|
+
setContentPath(null)
|
|
198
319
|
setLoadError(null)
|
|
199
320
|
setLoadStage({ progress: 10, message: '准备读取文件…' })
|
|
200
321
|
setStatus('加载中…')
|
|
201
322
|
loadContent(active, sessionId)
|
|
202
323
|
}, [active, sessionId])
|
|
203
324
|
|
|
325
|
+
// Tab 右键菜单打开时 Esc 关闭
|
|
326
|
+
React.useEffect(() => {
|
|
327
|
+
if (!tabMenu) return
|
|
328
|
+
const onKey = (e) => { if (e.key === 'Escape') dismissMenus() }
|
|
329
|
+
window.addEventListener('keydown', onKey, true)
|
|
330
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
331
|
+
}, [tabMenu])
|
|
332
|
+
|
|
204
333
|
React.useEffect(() => {
|
|
205
334
|
if (monaco || monacoErr) return
|
|
206
335
|
let alive = true
|
|
@@ -247,6 +376,7 @@ export function EditorView(props) {
|
|
|
247
376
|
if (res && res.ok) {
|
|
248
377
|
setStatus('已保存 ' + new Date().toTimeString().slice(0, 8))
|
|
249
378
|
setContent(text)
|
|
379
|
+
setContentPath(active)
|
|
250
380
|
setDirtyMap((d) => Object.assign({}, d, { [active]: false }))
|
|
251
381
|
refreshRecords()
|
|
252
382
|
emitRefresh()
|
|
@@ -327,6 +457,40 @@ export function EditorView(props) {
|
|
|
327
457
|
ed.onDidChangeCursorPosition((e) => {
|
|
328
458
|
setCursor('Ln ' + e.position.lineNumber + ', Col ' + e.position.column)
|
|
329
459
|
})
|
|
460
|
+
// 编辑区右键 → 注入 Monaco 原生 context menu(与 Go to Definition 等同菜单,避免分离浮层)。
|
|
461
|
+
// 有选区时额外显示「选中内容」项:context key edrvSelection 由光标选区变化驱动。
|
|
462
|
+
// 路径/选区在点击时从 model/editor 实时读取(不依赖闭包里的过期 active)。
|
|
463
|
+
const menuHandlers = () => menuHandlersRef.current
|
|
464
|
+
const pathOf = (edx) => {
|
|
465
|
+
const model = edx?.getModel?.()
|
|
466
|
+
const uriPath = model?.uri?.path
|
|
467
|
+
return uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
|
|
468
|
+
}
|
|
469
|
+
const selectionOf = (edx) => {
|
|
470
|
+
const s = edx?.getSelection?.()
|
|
471
|
+
if (!s) return null
|
|
472
|
+
if (s.startLineNumber === s.endLineNumber && s.startColumn === s.endColumn) return null
|
|
473
|
+
return { startLine: s.startLineNumber, endLine: s.endLineNumber }
|
|
474
|
+
}
|
|
475
|
+
const selKey = ed.createContextKey('edrvSelection', false)
|
|
476
|
+
ed.onDidChangeCursorSelection((e) => {
|
|
477
|
+
const s = e?.selection
|
|
478
|
+
selKey.set(!!(s && (s.startLineNumber !== s.endLineNumber || s.startColumn !== s.endColumn)))
|
|
479
|
+
})
|
|
480
|
+
ed.addAction({
|
|
481
|
+
id: 'edrv.addFileRef', label: '添加文件到对话', contextMenuGroupId: '1_edrv',
|
|
482
|
+
precondition: 'editorTextFocus',
|
|
483
|
+
run: (edx) => menuHandlers()?.addRefToChat(pathOf(edx)),
|
|
484
|
+
})
|
|
485
|
+
ed.addAction({
|
|
486
|
+
id: 'edrv.addSelectionRef', label: '添加选中内容为引用', contextMenuGroupId: '1_edrv',
|
|
487
|
+
precondition: 'edrvSelection',
|
|
488
|
+
run: (edx) => {
|
|
489
|
+
const s = selectionOf(edx)
|
|
490
|
+
const p = pathOf(edx)
|
|
491
|
+
if (s && p) menuHandlers()?.addRefToChat(p, { startLine: s.startLine, endLine: s.endLine })
|
|
492
|
+
},
|
|
493
|
+
})
|
|
330
494
|
// hover 差异块 → 浮出 Keep/Undo(req:鼠标移到编辑区差异块时显示)
|
|
331
495
|
// 防闪烁:① 区域不变不 setState(浮窗锚定差异块起始行,不跟随鼠标);② 延迟隐藏;
|
|
332
496
|
// ③ 浮窗自身 onMouseEnter 取消隐藏计时(鼠标在浮窗与编辑器间移动不闪)。
|
|
@@ -373,7 +537,7 @@ export function EditorView(props) {
|
|
|
373
537
|
ed.revealLineInCenter(Math.max(1, target.start ?? 1))
|
|
374
538
|
ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
|
|
375
539
|
ed.focus()
|
|
376
|
-
}, [active, content, pendingRegions])
|
|
540
|
+
}, [active, content, pendingRegions, focusRequest])
|
|
377
541
|
|
|
378
542
|
const jumpTo = (region) => {
|
|
379
543
|
if (editorRef.current) {
|
|
@@ -410,10 +574,16 @@ export function EditorView(props) {
|
|
|
410
574
|
openFile(sum.pendingFiles[next].path, true)
|
|
411
575
|
}
|
|
412
576
|
|
|
413
|
-
const
|
|
577
|
+
const openNextFile = () => {
|
|
578
|
+
if (!sum.pendingFiles.length) return
|
|
579
|
+
if (sum.pendingFiles.some((file) => file.path === active)) gotoFile(1)
|
|
580
|
+
else openFile(sum.pendingFiles[0].path, true)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const reloadFile = (skipStale) => {
|
|
414
584
|
if (!active) return
|
|
415
585
|
loadContent(active, sessionId)
|
|
416
|
-
refreshRecords()
|
|
586
|
+
refreshRecords(skipStale === true)
|
|
417
587
|
}
|
|
418
588
|
|
|
419
589
|
/**
|
|
@@ -454,8 +624,64 @@ export function EditorView(props) {
|
|
|
454
624
|
})
|
|
455
625
|
}
|
|
456
626
|
|
|
457
|
-
|
|
458
|
-
|
|
627
|
+
/**
|
|
628
|
+
* 批量采纳/不采纳:一次 RPC 处理多个差异(Keep 整文件 / Keep All / Undo All)。
|
|
629
|
+
* 单次 setRecords 合并全部结果,避免逐条往返读写整个 sidecar。
|
|
630
|
+
* @author ddj 2026年08月25号
|
|
631
|
+
* @param items 决策项数组(callId/scope/hunkIndex/decision)
|
|
632
|
+
* @returns Promise<{ok:number; fail:number}> 成功/失败计数
|
|
633
|
+
*/
|
|
634
|
+
const actMany = (items) => {
|
|
635
|
+
if (!items.length) return Promise.resolve({ ok: 0, fail: 0 })
|
|
636
|
+
return rpc('edrv.decideBatch', { sessionId, items }).then((res) => {
|
|
637
|
+
if (!res || !res.ok || !Array.isArray(res.results)) {
|
|
638
|
+
setError(res?.error ? String(res.error) : '批量处理失败')
|
|
639
|
+
return { ok: 0, fail: items.length }
|
|
640
|
+
}
|
|
641
|
+
let ok = 0
|
|
642
|
+
let fail = 0
|
|
643
|
+
const next = {}
|
|
644
|
+
for (const item of res.results) {
|
|
645
|
+
if (item && item.ok) {
|
|
646
|
+
ok++
|
|
647
|
+
if (item.record) next[item.callId] = item.record
|
|
648
|
+
} else {
|
|
649
|
+
fail++
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
setRecords((prev) => Object.assign({}, prev, next))
|
|
653
|
+
return { ok, fail }
|
|
654
|
+
}).catch((e) => {
|
|
655
|
+
setError('批量处理异常:' + String(e))
|
|
656
|
+
return { ok: 0, fail: items.length }
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* 决策项构造(与单条 actHunk 的 scope 语义一致:create 记录走 call 作用域)。
|
|
662
|
+
* @author ddj 2026年08月25号
|
|
663
|
+
* @param r 差异区域/待处理项(callId/idx/create)
|
|
664
|
+
* @param reject true=不采纳
|
|
665
|
+
* @returns decideBatch items 元素
|
|
666
|
+
*/
|
|
667
|
+
const itemOf = (r, reject) => ({ callId: r.callId, scope: r.create ? 'call' : 'hunk', hunkIndex: r.idx, decision: reject ? 'rejected' : 'accepted' })
|
|
668
|
+
|
|
669
|
+
const acceptFile = () => {
|
|
670
|
+
actMany(pendingRegions.map((r) => itemOf(r, false))).then(({ ok, fail }) => {
|
|
671
|
+
reloadFile(true)
|
|
672
|
+
emitRefresh()
|
|
673
|
+
setStatus('已采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
|
|
674
|
+
if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
const undoFile = () => {
|
|
678
|
+
actMany([...pendingRegions].reverse().map((r) => itemOf(r, true))).then(({ ok, fail }) => {
|
|
679
|
+
reloadFile(true)
|
|
680
|
+
emitRefresh()
|
|
681
|
+
setStatus('已不采纳 ' + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
|
|
682
|
+
if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
683
|
+
})
|
|
684
|
+
}
|
|
459
685
|
|
|
460
686
|
// 所有差异文件的待处理 hunk 列表(二级菜单 Keep All / Undo All 用)
|
|
461
687
|
const allPending = React.useMemo(() => {
|
|
@@ -473,7 +699,7 @@ export function EditorView(props) {
|
|
|
473
699
|
}, [records])
|
|
474
700
|
|
|
475
701
|
/**
|
|
476
|
-
* 批量处理所有差异文件:采纳全部 / 不采纳全部。不采纳按 at
|
|
702
|
+
* 批量处理所有差异文件:采纳全部 / 不采纳全部。不采纳按 at 降序(新改先回滚)排序后一次 RPC。
|
|
477
703
|
* @author ddj 2026年08月20号
|
|
478
704
|
* @param reject true=全部不采纳(回滚),false=全部采纳
|
|
479
705
|
*/
|
|
@@ -481,19 +707,13 @@ export function EditorView(props) {
|
|
|
481
707
|
if (batchBusyRef.current || !allPending.length) return
|
|
482
708
|
batchBusyRef.current = true
|
|
483
709
|
const list = reject ? [...allPending].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : b.idx - a.idx)) : allPending
|
|
484
|
-
|
|
485
|
-
const run = async () => {
|
|
486
|
-
for (const r of list) {
|
|
487
|
-
if (await actHunk(r, reject, true)) ok++
|
|
488
|
-
else fail++
|
|
489
|
-
}
|
|
710
|
+
actMany(list.map((r) => itemOf(r, reject))).then(({ ok, fail }) => {
|
|
490
711
|
batchBusyRef.current = false
|
|
491
|
-
reloadFile()
|
|
712
|
+
reloadFile(true)
|
|
492
713
|
emitRefresh()
|
|
493
714
|
setStatus((reject ? '已不采纳 ' : '已采纳 ') + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
|
|
494
715
|
if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
495
|
-
}
|
|
496
|
-
run()
|
|
716
|
+
}).catch(() => { batchBusyRef.current = false })
|
|
497
717
|
}
|
|
498
718
|
const acceptAllFiles = () => actAllPending(false)
|
|
499
719
|
const undoAllFiles = () => actAllPending(true)
|
|
@@ -508,6 +728,32 @@ export function EditorView(props) {
|
|
|
508
728
|
}).catch((e) => setError('回滚异常:' + String(e)))
|
|
509
729
|
}
|
|
510
730
|
|
|
731
|
+
/**
|
|
732
|
+
* 关闭 Tab 右键菜单。
|
|
733
|
+
* @author ddj 2026年08月25号
|
|
734
|
+
*/
|
|
735
|
+
const dismissMenus = () => {
|
|
736
|
+
setTabMenu(null)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** 把「添加到对话」返回状态映射为状态栏文案。
|
|
740
|
+
* @author ddj 2026年08月25号 */
|
|
741
|
+
const statusOfAdd = (outcome, okText) => {
|
|
742
|
+
if (outcome === 'ok') return okText
|
|
743
|
+
if (outcome === 'busy') return okText + '(输入框忙,已降级纯文本)'
|
|
744
|
+
return '无法添加到对话(无会话或输入框不可用)'
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** 添加文件/选中区引用到对话(异步,状态栏反馈)。 */
|
|
748
|
+
const addRefToChat = (path, range) => {
|
|
749
|
+
if (!path) { setStatus('无活动文件'); return }
|
|
750
|
+
if (!addToConversation) { setStatus('添加到对话不可用'); return }
|
|
751
|
+
addToConversation.appendReference(sessionId, path, range).then((o) => setStatus(statusOfAdd(o, '已添加文件引用')))
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// 供 Monaco 原生右键菜单 addAction 读取的最新动作闭包(空依赖回调不随渲染重建)
|
|
755
|
+
menuHandlersRef.current = { addRefToChat }
|
|
756
|
+
|
|
511
757
|
const openFile = (path, focusDiff) => {
|
|
512
758
|
if (!path) return
|
|
513
759
|
addTab(path, true)
|
|
@@ -532,6 +778,11 @@ export function EditorView(props) {
|
|
|
532
778
|
className: 'edrv-tab' + (t.path === active ? ' edrv-tab-active' : ''),
|
|
533
779
|
title: t.path,
|
|
534
780
|
onClick: () => { if (t.path !== active) { flushSave(); setActive(t.path) } },
|
|
781
|
+
onContextMenu: (e) => {
|
|
782
|
+
e.preventDefault()
|
|
783
|
+
e.stopPropagation()
|
|
784
|
+
setTabMenu({ x: e.clientX, y: e.clientY, path: t.path })
|
|
785
|
+
},
|
|
535
786
|
},
|
|
536
787
|
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 } }, t.path.split(/[\\/]/).pop() || t.path),
|
|
537
788
|
(dirtyMap[t.path] ? React.createElement('span', { className: 'edrv-tab-dot' }) : null),
|
|
@@ -542,22 +793,19 @@ export function EditorView(props) {
|
|
|
542
793
|
|
|
543
794
|
const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
|
|
544
795
|
React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
|
|
545
|
-
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (Ctrl+P) 打开文件')
|
|
796
|
+
React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (Ctrl+P) 打开文件'),
|
|
797
|
+
(active && langOf(active) ? React.createElement('span', { className: 'edrv-pb-meta' }, langOf(active)) : null),
|
|
798
|
+
(cursor ? React.createElement('span', { className: 'edrv-pb-meta' }, cursor) : null),
|
|
799
|
+
(status ? React.createElement('span', { className: 'edrv-pb-meta edrv-pb-status' }, status) : null))
|
|
546
800
|
|
|
547
801
|
const tabRow = React.createElement('div', { style: { display: 'flex', alignItems: 'center', background: 'var(--dsw-alias-bg-layer-1,transparent)', flexShrink: 0 } },
|
|
548
802
|
tabsEl,
|
|
549
|
-
React.createElement(QuickOpen, { sessionId, onOpen: (p) => openFile(p, false) }))
|
|
550
|
-
|
|
551
|
-
const statusBar = React.createElement('div', { className: 'edrv-statusbar' },
|
|
552
|
-
React.createElement('span', null, '编辑'),
|
|
553
|
-
React.createElement('span', null, active ? langOf(active) : ''),
|
|
554
|
-
React.createElement('span', null, cursor),
|
|
555
|
-
(status ? React.createElement('span', null, status) : null),
|
|
556
|
-
React.createElement('span', { style: { flex: 1 } }),
|
|
557
803
|
(sum.totalFiles > 0
|
|
558
804
|
? React.createElement('button', { className: 'edrv-diffchip', title: '打开差异总览/归档', onClick: () => setLauncherOpen((o) => !o) }, '⚠ 差异 ' + sum.totalFiles + ' 文件')
|
|
559
805
|
: null),
|
|
560
|
-
React.createElement('button', { className: 'edrv-chip-btn', title: '
|
|
806
|
+
React.createElement('button', { className: 'edrv-chip-btn' + (sidebarOn ? ' edrv-chip-on' : ''), title: '切换侧边栏 (Ctrl+B)', onClick: () => setSidebarOn((v) => !v) }, '☰'),
|
|
807
|
+
React.createElement('button', { className: 'edrv-chip-btn', title: '刷新', onClick: reloadFile }, '⟳'),
|
|
808
|
+
React.createElement(QuickOpen, { sessionId, onOpen: (p) => openFile(p, false) }))
|
|
561
809
|
|
|
562
810
|
const otherFiles = sum.pendingFiles.filter((f) => f.path !== active)
|
|
563
811
|
|
|
@@ -598,6 +846,7 @@ export function EditorView(props) {
|
|
|
598
846
|
body = loadingBody('文件加载失败:' + loadError, 0, () => {
|
|
599
847
|
setLoadError(null)
|
|
600
848
|
setContent(null)
|
|
849
|
+
setContentPath(null)
|
|
601
850
|
setLoadStage({ progress: 10, message: '重新读取文件…' })
|
|
602
851
|
loadContent(active, sessionId)
|
|
603
852
|
})
|
|
@@ -630,46 +879,104 @@ export function EditorView(props) {
|
|
|
630
879
|
React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
|
|
631
880
|
: null
|
|
632
881
|
|
|
882
|
+
React.useEffect(() => {
|
|
883
|
+
if (!sessionId) return
|
|
884
|
+
const mode = editorDockMode(active)
|
|
885
|
+
publishDiffDock(sessionId, {
|
|
886
|
+
mode,
|
|
887
|
+
pendingRegions,
|
|
888
|
+
staleRegions,
|
|
889
|
+
onAct: actHunk,
|
|
890
|
+
onAcceptFile: acceptFile,
|
|
891
|
+
onUndoFile: undoFile,
|
|
892
|
+
onAcceptAllFiles: acceptAllFiles,
|
|
893
|
+
onUndoAllFiles: undoAllFiles,
|
|
894
|
+
allPendingCount: allPending.length,
|
|
895
|
+
onRollback: rollbackFile,
|
|
896
|
+
onJump: jumpTo,
|
|
897
|
+
otherFiles,
|
|
898
|
+
onOpenOther: (path) => openFile(path, true),
|
|
899
|
+
onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
|
|
900
|
+
onRefresh: reloadFile,
|
|
901
|
+
activePath: active,
|
|
902
|
+
diffIdx: contentReady ? diffIdx : 0,
|
|
903
|
+
diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => file.path === active)?.pending ?? 0),
|
|
904
|
+
fileIdx,
|
|
905
|
+
fileTotal: sum.pendingFiles.length,
|
|
906
|
+
onPrevDiff: () => gotoDiff(-1),
|
|
907
|
+
onNextDiff: () => gotoDiff(1),
|
|
908
|
+
onPrevFile: () => gotoFile(-1),
|
|
909
|
+
onNextFile: () => gotoFile(1),
|
|
910
|
+
onOpenNextFile: openNextFile,
|
|
911
|
+
}, dockSourceRef.current)
|
|
912
|
+
})
|
|
913
|
+
|
|
633
914
|
const overlay = launcherOpen
|
|
634
915
|
? React.createElement(React.Fragment, null,
|
|
635
916
|
React.createElement('div', { style: { position: 'fixed', inset: 0, zIndex: 30 }, onClick: () => setLauncherOpen(false) }),
|
|
636
917
|
React.createElement(DiffLauncher, { sessionId, sum, tab: launcherTab, onClose: () => setLauncherOpen(false), onOpenFile: (p) => { openFile(p, true); setLauncherOpen(false) } }))
|
|
637
|
-
:
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
: (
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
918
|
+
: null
|
|
919
|
+
|
|
920
|
+
// Tab 右键菜单浮层:全屏遮罩(点击/右键关闭)+ 菜单(固定定位,clamp 防越界)。
|
|
921
|
+
// 编辑区右键走 Monaco 原生 context menu(editor.addAction),不在此渲染浮层。
|
|
922
|
+
/**
|
|
923
|
+
* 计算右键菜单固定定位(clamp 防越出视口)。
|
|
924
|
+
* @author ddj 2026年08月25号
|
|
925
|
+
*/
|
|
926
|
+
const menuPos = (x, y) => ({
|
|
927
|
+
left: Math.max(4, Math.min(x, (window.innerWidth || 800) - 224)),
|
|
928
|
+
top: Math.max(4, Math.min(y, (window.innerHeight || 600) - 176)),
|
|
929
|
+
})
|
|
930
|
+
const menuBackdrop = tabMenu
|
|
931
|
+
? React.createElement('div', {
|
|
932
|
+
style: { position: 'fixed', inset: 0, zIndex: 70 },
|
|
933
|
+
onClick: dismissMenus,
|
|
934
|
+
onContextMenu: (e) => { e.preventDefault(); dismissMenus() },
|
|
935
|
+
})
|
|
936
|
+
: null
|
|
937
|
+
const tabMenuEl = tabMenu
|
|
938
|
+
? React.createElement('div', { className: 'edrv-ctxmenu', style: menuPos(tabMenu.x, tabMenu.y) },
|
|
939
|
+
React.createElement('button', { className: 'edrv-ctxmenu-item', onClick: () => { addRefToChat(tabMenu.path); dismissMenus() } }, '添加文件到对话'),
|
|
940
|
+
React.createElement('div', { className: 'edrv-ctxmenu-sep' }),
|
|
941
|
+
React.createElement('button', { className: 'edrv-ctxmenu-item edrv-ctxmenu-danger', onClick: () => { closeTab(tabMenu.path); dismissMenus() } }, '关闭标签页'))
|
|
942
|
+
: null
|
|
943
|
+
|
|
944
|
+
const sidebarPanels = props.sidebarPanels
|
|
945
|
+
const sidebarCtx = {
|
|
946
|
+
sessionId,
|
|
947
|
+
openFile: (p) => openFile(p, false),
|
|
948
|
+
activePath: active,
|
|
949
|
+
pendingByPath,
|
|
950
|
+
sum: { totalFiles: sum.totalFiles, files: sum.files },
|
|
951
|
+
refreshRecords: () => refreshRecords(),
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
|
|
955
|
+
const editorArea = React.createElement('div', { className: 'edrv-editor-area' },
|
|
956
|
+
body,
|
|
957
|
+
hoverEl,
|
|
958
|
+
overlay)
|
|
959
|
+
const mainCol = React.createElement('div', { className: 'edrv-main-col', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
668
960
|
pathBar,
|
|
669
961
|
tabRow,
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
962
|
+
editorArea)
|
|
963
|
+
// 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
|
|
964
|
+
const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
|
|
965
|
+
(sidebarOn && sidebarPanels
|
|
966
|
+
? React.createElement(SidebarView, {
|
|
967
|
+
registry: sidebarPanels,
|
|
968
|
+
ctx: sidebarCtx,
|
|
969
|
+
activePanel,
|
|
970
|
+
onActive: setActivePanel,
|
|
971
|
+
width: sidebarW,
|
|
972
|
+
onWidth: setSidebarW,
|
|
973
|
+
onHide: () => setSidebarOn(false),
|
|
974
|
+
})
|
|
975
|
+
: null),
|
|
976
|
+
mainCol)
|
|
977
|
+
|
|
978
|
+
return React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', style: { height: 'var(--edrv-editor-height, 100%)', maxHeight: 'var(--edrv-editor-height, 100%)', minHeight: 0, display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base,transparent)', overflow: 'hidden' } },
|
|
979
|
+
editorRow,
|
|
980
|
+
menuBackdrop,
|
|
981
|
+
tabMenuEl)
|
|
675
982
|
}
|