dsh-vscode-mode 0.1.23 → 0.1.25
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 +8 -5
- package/lib/client.js +97 -28
- package/lib/client.js.map +1 -1
- package/lib/index.js +4 -1
- package/lib/index.js.map +1 -1
- package/package.json +4 -1
- package/src/client/monaco/diffRender.ts +3 -0
- package/src/client/sidebar/panels/FileExplorer.ts +12 -2
- package/src/client/styles/editor.css +1 -0
- package/src/client/ui/ContextMenu.ts +37 -12
- package/src/client/ui/EditorView.ts +20 -1
- package/src/client/ui/menuPosition.ts +63 -0
- package/src/rpc.ts +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -51,13 +51,16 @@
|
|
|
51
51
|
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
|
|
52
52
|
"cordis": ">=4.0.0-rc <5",
|
|
53
53
|
"react": "^18.2.0",
|
|
54
|
+
"react-dom": "^18.2.0",
|
|
54
55
|
"schemastery": "^3.18.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@deepseek-ai/dsh-settings": "0.1.0-rc.8",
|
|
58
59
|
"@types/node": "^24.13.3",
|
|
59
60
|
"@types/react": "^18.3.1",
|
|
61
|
+
"@types/react-dom": "^18.3.5",
|
|
60
62
|
"react": "^18.3.1",
|
|
63
|
+
"react-dom": "^18.3.1",
|
|
61
64
|
"schemastery": "3.18.0",
|
|
62
65
|
"tsdown": "^0.22.14",
|
|
63
66
|
"typescript": "^5.9.0",
|
|
@@ -21,6 +21,9 @@ export function createDiffRenderer(log) {
|
|
|
21
21
|
* @param sessionId 会话 id
|
|
22
22
|
*/
|
|
23
23
|
const render = (monaco, editor, pendingRegions, sessionId) => {
|
|
24
|
+
// 空渲染且无残留(5s 轮询 setRecords 常触发):无需清理时直接跳过,
|
|
25
|
+
// 不打 begin 日志、不做无谓 DOM 操作;有 zone/decoration 残留时仍需走清理。
|
|
26
|
+
if (!pendingRegions.length && viewZoneIds.length === 0 && decorations.length === 0) return
|
|
24
27
|
const renderT0 = Date.now()
|
|
25
28
|
const callIdAttr = (callId, idx) => String(callId) + ':' + String(idx)
|
|
26
29
|
log(sessionId, '[diff-render] begin regs=' + pendingRegions.length + ' ' + pendingRegions.map((r) => callIdAttr(r.callId, r.idx) + '@' + (r.start ?? '?') + '-' + (r.end ?? '?') + '(-' + (r.oldLines ? r.oldLines.length : 0) + '/+' + (r.newLines ? r.newLines.length : 0) + ')').join('|'))
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import React from 'react'
|
|
11
11
|
import { rpc } from '../../rpc.js'
|
|
12
12
|
import { ContextMenu } from '../../ui/ContextMenu.js'
|
|
13
|
+
import { rowMenuPosition } from '../../ui/menuPosition.js'
|
|
13
14
|
import { buildTreeMenu } from '../contextMenu.js'
|
|
14
15
|
import type { SidebarCtx } from '../types.js'
|
|
15
16
|
|
|
@@ -74,6 +75,7 @@ export function FileExplorer(props) {
|
|
|
74
75
|
const refresh = () => {
|
|
75
76
|
const keep = Object.keys(expandedRef.current).filter((k) => expandedRef.current[k] === true)
|
|
76
77
|
tokensRef.current = {}
|
|
78
|
+
setMenu(null)
|
|
77
79
|
setDirs({})
|
|
78
80
|
setLoading({})
|
|
79
81
|
setError(null)
|
|
@@ -94,6 +96,7 @@ export function FileExplorer(props) {
|
|
|
94
96
|
setExpanded({})
|
|
95
97
|
setLoading({})
|
|
96
98
|
setError(null)
|
|
99
|
+
setMenu(null)
|
|
97
100
|
setRoot(null)
|
|
98
101
|
void loadDir('')
|
|
99
102
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -111,17 +114,22 @@ export function FileExplorer(props) {
|
|
|
111
114
|
const rowEl = (e, depth, isDir, isOpen) => {
|
|
112
115
|
const pending = isDir ? 0 : (pendingByPath[e.path] ?? 0)
|
|
113
116
|
const active = !isDir && e.path === activePath
|
|
117
|
+
const contextTarget = Boolean(menu && menuEntries.length && menu.target.path === e.path)
|
|
114
118
|
const dim = e.type === 'other'
|
|
115
119
|
return React.createElement('div', {
|
|
116
120
|
key: e.path,
|
|
117
|
-
className: 'edrv-tree-row'
|
|
121
|
+
className: 'edrv-tree-row'
|
|
122
|
+
+ (active ? ' edrv-tree-active' : '')
|
|
123
|
+
+ (contextTarget ? ' edrv-tree-context' : ''),
|
|
118
124
|
title: e.path,
|
|
119
125
|
style: { paddingLeft: 6 + depth * 14 },
|
|
120
126
|
onClick: () => { if (isDir) toggle(e.path); else openFile(e.path) },
|
|
121
127
|
onContextMenu: (ev) => {
|
|
122
128
|
ev.preventDefault()
|
|
123
129
|
ev.stopPropagation()
|
|
124
|
-
|
|
130
|
+
const rect = ev.currentTarget?.getBoundingClientRect?.()
|
|
131
|
+
const position = rowMenuPosition(rect, ev.clientX, ev.clientY)
|
|
132
|
+
setMenu({ x: position.left, y: position.top, target: { path: e.path, type: e.type } })
|
|
125
133
|
},
|
|
126
134
|
},
|
|
127
135
|
React.createElement('span', { className: 'edrv-tree-chev' },
|
|
@@ -158,6 +166,8 @@ export function FileExplorer(props) {
|
|
|
158
166
|
// 面板区右键(空白处)→ 工作区根目录菜单;行内右键已在 rowEl 阻止冒泡。
|
|
159
167
|
const onPanelContext = (ev) => {
|
|
160
168
|
ev.preventDefault()
|
|
169
|
+
ev.stopPropagation()
|
|
170
|
+
if (!ev.currentTarget.contains(ev.target)) return
|
|
161
171
|
setMenu({ x: ev.clientX, y: ev.clientY, target: { path: '', type: 'directory' } })
|
|
162
172
|
}
|
|
163
173
|
// 当前右键目标的可视菜单项(构建时过滤/排序,registry 变化下次打开生效)。
|
|
@@ -213,6 +213,7 @@
|
|
|
213
213
|
[data-edrv-view] .edrv-tree-row { display: flex; align-items: center; gap: 4px; height: 22px; padding-right: 6px; border-radius: 5px; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; white-space: nowrap; }
|
|
214
214
|
[data-edrv-view] .edrv-tree-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
215
215
|
[data-edrv-view] .edrv-tree-row.edrv-tree-active { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); }
|
|
216
|
+
[data-edrv-view] .edrv-tree-row.edrv-tree-context { box-shadow: inset 0 0 0 1px var(--dsw-alias-brand-primary, #0f9d58); background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.12)); }
|
|
216
217
|
[data-edrv-view] .edrv-tree-chev { width: 12px; flex-shrink: 0; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 10px; text-align: center; }
|
|
217
218
|
[data-edrv-view] .edrv-tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
218
219
|
[data-edrv-view] .edrv-tree-name.edrv-tree-dim { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 通用浮动右键菜单(createElement 风格、类型化)。
|
|
3
|
-
*
|
|
3
|
+
* 非阻塞式关闭:window capture 监听(点击菜单外/右键菜单外/Esc 关闭),
|
|
4
|
+
* 不拦截菜单外事件,允许同一事件内由发起方切换/重开菜单。
|
|
4
5
|
* 条目支持 danger/disabled/separator,复用既有 edrv-ctxmenu* 样式类。
|
|
5
6
|
* 作者 ddj 2026-08-27
|
|
6
7
|
*/
|
|
7
8
|
import React from 'react'
|
|
9
|
+
import { createPortal } from 'react-dom'
|
|
10
|
+
import { clampMenuPosition } from './menuPosition.js'
|
|
8
11
|
|
|
9
12
|
/** 单条菜单项(展示层形状;业务侧由 buildTreeMenu 映射而来)。 */
|
|
10
13
|
export interface ContextMenuEntry {
|
|
@@ -37,17 +40,43 @@ const MENU_H = 176
|
|
|
37
40
|
*/
|
|
38
41
|
export function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
|
39
42
|
const { x, y, entries, onClose } = props
|
|
43
|
+
const menuRef = React.useRef<HTMLDivElement | null>(null)
|
|
40
44
|
|
|
41
45
|
React.useEffect(() => {
|
|
42
46
|
const onKey = (e: KeyboardEvent): void => {
|
|
43
47
|
if (e.key === 'Escape') onClose()
|
|
44
48
|
}
|
|
49
|
+
const onPointerDown = (e: PointerEvent): void => {
|
|
50
|
+
if (e.button !== 0) return
|
|
51
|
+
if (menuRef.current && menuRef.current.contains(e.target as Node)) return
|
|
52
|
+
onClose()
|
|
53
|
+
}
|
|
54
|
+
const onContextMenu = (e: MouseEvent): void => {
|
|
55
|
+
if (menuRef.current && menuRef.current.contains(e.target as Node)) {
|
|
56
|
+
e.preventDefault()
|
|
57
|
+
}
|
|
58
|
+
onClose()
|
|
59
|
+
}
|
|
45
60
|
window.addEventListener('keydown', onKey, true)
|
|
46
|
-
|
|
61
|
+
window.addEventListener('pointerdown', onPointerDown, true)
|
|
62
|
+
window.addEventListener('contextmenu', onContextMenu, true)
|
|
63
|
+
return () => {
|
|
64
|
+
window.removeEventListener('keydown', onKey, true)
|
|
65
|
+
window.removeEventListener('pointerdown', onPointerDown, true)
|
|
66
|
+
window.removeEventListener('contextmenu', onContextMenu, true)
|
|
67
|
+
}
|
|
47
68
|
}, [onClose])
|
|
48
69
|
|
|
49
|
-
const
|
|
50
|
-
const
|
|
70
|
+
const safeX = Number.isFinite(x) ? x : 4
|
|
71
|
+
const safeY = Number.isFinite(y) ? y : 4
|
|
72
|
+
const position = clampMenuPosition(
|
|
73
|
+
safeX,
|
|
74
|
+
safeY,
|
|
75
|
+
window.innerWidth || 800,
|
|
76
|
+
window.innerHeight || 600,
|
|
77
|
+
MENU_W,
|
|
78
|
+
MENU_H,
|
|
79
|
+
)
|
|
51
80
|
|
|
52
81
|
const children: React.ReactNode[] = []
|
|
53
82
|
for (const entry of entries) {
|
|
@@ -71,12 +100,8 @@ export function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
|
|
71
100
|
}, entry.label))
|
|
72
101
|
}
|
|
73
102
|
|
|
74
|
-
|
|
75
|
-
React.createElement('div', {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
onClick: onClose,
|
|
79
|
-
onContextMenu: (e: React.MouseEvent) => { e.preventDefault(); onClose() },
|
|
80
|
-
}),
|
|
81
|
-
React.createElement('div', { className: 'edrv-ctxmenu', style: { left, top } }, ...children))
|
|
103
|
+
const overlay = React.createElement('div', { 'data-edrv-view': '1' },
|
|
104
|
+
React.createElement('div', { className: 'edrv-ctxmenu', ref: menuRef, style: position }, ...children))
|
|
105
|
+
|
|
106
|
+
return typeof document === 'undefined' ? overlay : createPortal(overlay, document.body)
|
|
82
107
|
}
|
|
@@ -131,6 +131,25 @@ export function EditorView(props) {
|
|
|
131
131
|
})
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* 轮询去重:前后记录内容一致时返回原引用,React 跳过重渲染,
|
|
136
|
+
* 阻断 5s 轮询 → memo 链 → diff 重渲染/日志 的空转。
|
|
137
|
+
* @author ddj 2026年08月26号
|
|
138
|
+
* @param prev 当前 records
|
|
139
|
+
* @param next 轮询新结果
|
|
140
|
+
* @returns 内容一致返回 true
|
|
141
|
+
*/
|
|
142
|
+
const sameRecords = (prev, next) => {
|
|
143
|
+
const pk = Object.keys(prev)
|
|
144
|
+
const nk = Object.keys(next)
|
|
145
|
+
if (pk.length !== nk.length) return false
|
|
146
|
+
for (const k of pk) {
|
|
147
|
+
if (!(k in next)) return false
|
|
148
|
+
if (JSON.stringify(prev[k]) !== JSON.stringify(next[k])) return false
|
|
149
|
+
}
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
|
|
134
153
|
const refreshRecords = (skipStale) => {
|
|
135
154
|
if (!sessionId) return
|
|
136
155
|
rpc('edrv.list', { sessionId, ...(skipStale ? { skipStale: true } : {}) }).then((res) => {
|
|
@@ -140,7 +159,7 @@ export function EditorView(props) {
|
|
|
140
159
|
if (r.archived === true) continue
|
|
141
160
|
map[r.callId] = r
|
|
142
161
|
}
|
|
143
|
-
setRecords(map)
|
|
162
|
+
setRecords((prev) => (sameRecords(prev, map) ? prev : map))
|
|
144
163
|
}).catch((e) => setError('list异常:' + String(e)))
|
|
145
164
|
}
|
|
146
165
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 右键菜单 viewport 定位纯函数。
|
|
3
|
+
* 将目标行锚点与视口边界计算从 React 渲染层拆出,便于复用和单测。
|
|
4
|
+
* 作者 ddj 2026-08-28
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface MenuPosition {
|
|
8
|
+
left: number
|
|
9
|
+
top: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MenuAnchorRect {
|
|
13
|
+
top: number
|
|
14
|
+
right: number
|
|
15
|
+
width: number
|
|
16
|
+
height: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 将菜单坐标限制在 viewport 内。
|
|
21
|
+
* @author ddj 2026年08月28号
|
|
22
|
+
* @param x 期望的 viewport x 坐标
|
|
23
|
+
* @param y 期望的 viewport y 坐标
|
|
24
|
+
* @param viewportWidth viewport 宽度
|
|
25
|
+
* @param viewportHeight viewport 高度
|
|
26
|
+
* @param menuWidth 菜单估算宽度
|
|
27
|
+
* @param menuHeight 菜单估算高度
|
|
28
|
+
* @returns 限制后的菜单位置
|
|
29
|
+
*/
|
|
30
|
+
export function clampMenuPosition(
|
|
31
|
+
x: number,
|
|
32
|
+
y: number,
|
|
33
|
+
viewportWidth: number,
|
|
34
|
+
viewportHeight: number,
|
|
35
|
+
menuWidth: number,
|
|
36
|
+
menuHeight: number,
|
|
37
|
+
): MenuPosition {
|
|
38
|
+
const maxLeft = Math.max(4, viewportWidth - menuWidth)
|
|
39
|
+
const maxTop = Math.max(4, viewportHeight - menuHeight)
|
|
40
|
+
return {
|
|
41
|
+
left: Math.max(4, Math.min(x, maxLeft)),
|
|
42
|
+
top: Math.max(4, Math.min(y, maxTop)),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 将文件树行右键菜单锚定到目标行右侧;无法测量时回退到鼠标坐标。
|
|
48
|
+
* @author ddj 2026年08月28号
|
|
49
|
+
* @param rect 目标行的布局矩形
|
|
50
|
+
* @param fallbackX 鼠标 viewport x 坐标
|
|
51
|
+
* @param fallbackY 鼠标 viewport y 坐标
|
|
52
|
+
* @returns 菜单初始位置
|
|
53
|
+
*/
|
|
54
|
+
export function rowMenuPosition(
|
|
55
|
+
rect: MenuAnchorRect | null | undefined,
|
|
56
|
+
fallbackX: number,
|
|
57
|
+
fallbackY: number,
|
|
58
|
+
): MenuPosition {
|
|
59
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
60
|
+
return { left: fallbackX, top: fallbackY }
|
|
61
|
+
}
|
|
62
|
+
return { left: rect.right + 4, top: rect.top }
|
|
63
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -34,6 +34,8 @@ import { revealInExplorer } from './reveal.js'
|
|
|
34
34
|
|
|
35
35
|
/** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
|
|
36
36
|
const debugWriteQueues = new Map<string, Promise<void>>()
|
|
37
|
+
/** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
|
|
38
|
+
const DEBUG_LOG_CAP = 512 * 1024
|
|
37
39
|
|
|
38
40
|
/** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
|
|
39
41
|
function recView(record: DiffRecord): RecordView {
|
|
@@ -300,7 +302,11 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
|
|
|
300
302
|
const target = await fs.resolve('.dsh-edit-review-debug.log', { cwd: sc.cwd })
|
|
301
303
|
const old = await fs.readText(target).catch(() => '')
|
|
302
304
|
const line = new Date().toISOString() + ' ' + text + '\n'
|
|
303
|
-
|
|
305
|
+
// 超上限时截断保留尾部:单文件体积有界,每次追加的读改写成本不随会话时长增长
|
|
306
|
+
const next = old.length + line.length > DEBUG_LOG_CAP
|
|
307
|
+
? old.slice(-Math.floor(DEBUG_LOG_CAP / 2)) + line
|
|
308
|
+
: old + line
|
|
309
|
+
await fs.writeText(target, next, void 0, void 0, policyOf(ctx, sc.session))
|
|
304
310
|
} catch (e) { /* 写日志失败忽略 */ }
|
|
305
311
|
})
|
|
306
312
|
debugWriteQueues.set(sc.cwd, task)
|