dsh-vscode-mode 0.1.22 → 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.
@@ -19,17 +19,22 @@ import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
19
19
  import { displayDiffTotal, editorDockMode } from '../diffDock.js'
20
20
  import { editorHeight } from '../editorLayout.js'
21
21
  import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
22
+ import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
22
23
 
23
24
  /**
24
25
  * 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
25
26
  * 底部差异条(DiffBox/空态)+ 全局差异下拉(DiffLauncher)+ 状态栏。
26
27
  * @param props.sessionId 会话 id
27
28
  * @param props.schedule 延时调度(ctx.timeout)
29
+ * @param props.layout 布局形态:'tab'(中央页签,默认)| 'side'(侧边栏面板)
30
+ * @param props.sideHint 旧页签形态下显示侧边栏引导(安装命令)
28
31
  */
29
32
  export function EditorView(props) {
30
33
  const sessionId = props?.sessionId
31
34
  const schedule = props.schedule
32
35
  const addToConversation = props.addToConversation
36
+ const layout = props?.layout === 'side' ? 'side' : 'tab'
37
+ const sideHint = props?.sideHint
33
38
  const [monaco, setMonaco] = React.useState(null)
34
39
  const [monacoErr, setMonacoErr] = React.useState(null)
35
40
  const [records, setRecords] = React.useState({})
@@ -51,10 +56,13 @@ export function EditorView(props) {
51
56
  const [diffIdx, setDiffIdx] = React.useState(0) // 当前文件内差异位置(x/x 显示)
52
57
  const [fileIdx, setFileIdx] = React.useState(0) // 全局差异文件位置(x/x 文件 显示)
53
58
  const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
54
- const [sidebarOn, setSidebarOn] = React.useState(true) // 侧边栏显隐
59
+ const [sidebarOn, setSidebarOn] = React.useState(layout !== 'side') // 侧边栏显隐(侧栏形态默认收起)
55
60
  const [sidebarW, setSidebarW] = React.useState(240) // 侧边栏宽度
56
61
  const [activePanel, setActivePanel] = React.useState('explorer') // 激活面板 id
57
62
  const [focusRequest, setFocusRequest] = React.useState(0) // 外部差异聚焦请求版本
63
+ const [hintDismissed, setHintDismissed] = React.useState(() => {
64
+ try { return localStorage.getItem('edrv.side-hint-dismissed') === '1' } catch { return false }
65
+ }) // 侧边栏引导条是否已关闭
58
66
  const editorRef = React.useRef(null)
59
67
  const viewRootRef = React.useRef(null)
60
68
  const monacoRef = React.useRef(null)
@@ -75,6 +83,8 @@ export function EditorView(props) {
75
83
  const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
76
84
  const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
77
85
  const diffRendererRef = React.useRef(null)
86
+ const layoutRef = React.useRef(layout) // ensureEditor 空依赖闭包读取的稳定布局
87
+ layoutRef.current = layout
78
88
  if (!diffRendererRef.current) diffRendererRef.current = createDiffRenderer((sid, t) => dbg(sid, t))
79
89
 
80
90
  const currentRecords = React.useMemo(() => {
@@ -121,6 +131,25 @@ export function EditorView(props) {
121
131
  })
122
132
  }
123
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
+
124
153
  const refreshRecords = (skipStale) => {
125
154
  if (!sessionId) return
126
155
  rpc('edrv.list', { sessionId, ...(skipStale ? { skipStale: true } : {}) }).then((res) => {
@@ -130,7 +159,7 @@ export function EditorView(props) {
130
159
  if (r.archived === true) continue
131
160
  map[r.callId] = r
132
161
  }
133
- setRecords(map)
162
+ setRecords((prev) => (sameRecords(prev, map) ? prev : map))
134
163
  }).catch((e) => setError('list异常:' + String(e)))
135
164
  }
136
165
 
@@ -195,10 +224,15 @@ export function EditorView(props) {
195
224
 
196
225
  // 对话输入区的唯一差异 dock 读取此会话快照;编辑器卸载时只清理自己的发布者令牌。
197
226
  const dockSourceRef = React.useRef({})
198
- React.useEffect(() => () => clearDiffDock(sessionId, dockSourceRef.current), [sessionId])
227
+ React.useEffect(() => () => {
228
+ clearDiffDock(sessionId, dockSourceRef.current)
229
+ if (layout === 'side') setSidePending(sessionId, 0)
230
+ }, [sessionId])
199
231
 
200
232
  // DSH composer 保持原生布局;编辑区只读取几何边界并同步自己的高度。
233
+ // 侧栏形态:面板容器自带高度,跳过 composer 几何同步(面板内无会话滚动区)。
201
234
  React.useLayoutEffect(() => {
235
+ if (layout === 'side') return
202
236
  const root = viewRootRef.current
203
237
  const scroll = root?.closest?.('[data-conversation-scroll]')
204
238
  if (!root || !scroll) return
@@ -249,7 +283,7 @@ export function EditorView(props) {
249
283
  window.removeEventListener('resize', scheduleUpdate)
250
284
  root.style.removeProperty('--edrv-editor-height')
251
285
  }
252
- }, [sessionId])
286
+ }, [sessionId, layout])
253
287
 
254
288
  // localStorage v2 恢复页签
255
289
  React.useEffect(() => {
@@ -273,11 +307,12 @@ export function EditorView(props) {
273
307
  catch (e) { /* 忽略 */ }
274
308
  }, [tabs, active, sessionId])
275
309
 
276
- // 侧边栏状态:恢复(显隐/宽度/激活面板)
310
+ // 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
311
+ const sidebarKey = 'edrv.sidebar.' + (layout === 'side' ? 'side.' : '') + String(sessionId)
277
312
  React.useEffect(() => {
278
313
  if (!sessionId) return
279
314
  try {
280
- const raw = localStorage.getItem('edrv.sidebar.' + String(sessionId))
315
+ const raw = localStorage.getItem(sidebarKey)
281
316
  if (raw) {
282
317
  const saved = JSON.parse(raw)
283
318
  if (typeof saved.on === 'boolean') setSidebarOn(saved.on)
@@ -285,14 +320,14 @@ export function EditorView(props) {
285
320
  if (typeof saved.panel === 'string') setActivePanel(saved.panel)
286
321
  }
287
322
  } catch (e) { /* 损坏忽略 */ }
288
- }, [sessionId])
323
+ }, [sessionId, sidebarKey])
289
324
 
290
325
  // 侧边栏状态持久化
291
326
  React.useEffect(() => {
292
327
  if (!sessionId) return
293
- try { localStorage.setItem('edrv.sidebar.' + String(sessionId), JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
328
+ try { localStorage.setItem(sidebarKey, JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
294
329
  catch (e) { /* 忽略 */ }
295
- }, [sidebarOn, sidebarW, activePanel, sessionId])
330
+ }, [sidebarOn, sidebarW, activePanel, sessionId, sidebarKey])
296
331
 
297
332
  // Ctrl+B 切换侧边栏(capture 抢占,避免与 DSH 全局冲突)
298
333
  React.useEffect(() => {
@@ -431,6 +466,7 @@ export function EditorView(props) {
431
466
  }
432
467
  if (editorRef.current || !monacoRef.current) return
433
468
  const m = monacoRef.current
469
+ const side = layoutRef.current === 'side'
434
470
  const ed = m.editor.create(node, {
435
471
  value: '',
436
472
  language: 'plaintext',
@@ -438,7 +474,7 @@ export function EditorView(props) {
438
474
  fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
439
475
  fontSize: 13,
440
476
  lineHeight: 20,
441
- minimap: { enabled: true, scale: 1 },
477
+ minimap: { enabled: !side, scale: 1 },
442
478
  glyphMargin: true,
443
479
  lineDecorationsWidth: 16,
444
480
  automaticLayout: true,
@@ -448,7 +484,7 @@ export function EditorView(props) {
448
484
  renderWhitespace: 'selection',
449
485
  smoothScrolling: true,
450
486
  cursorBlinking: 'smooth',
451
- padding: { top: 8 },
487
+ padding: { top: side ? 6 : 8 },
452
488
  })
453
489
  ed.addCommand(m.KeyMod.CtrlCmd | m.KeyCode.KeyS, () => { flushSave(); doSave(false) })
454
490
  ed.onDidChangeModelContent(() => {
@@ -901,36 +937,41 @@ export function EditorView(props) {
901
937
  React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
902
938
  : null
903
939
 
940
+ /** 组装差异 dock 快照(发布给 conversation.input.dock 的唯一 DiffBox 实例)。 */
941
+ const buildDockSnapshot = () => ({
942
+ mode: editorDockMode(active),
943
+ pendingRegions,
944
+ staleRegions,
945
+ onAct: actHunk,
946
+ onAcceptFile: acceptFile,
947
+ onUndoFile: undoFile,
948
+ onAcceptAllFiles: acceptAllFiles,
949
+ onUndoAllFiles: undoAllFiles,
950
+ allPendingCount: allPending.length,
951
+ onRollback: rollbackFile,
952
+ onJump: jumpTo,
953
+ otherFiles,
954
+ onOpenOther: (path) => openFile(path, true),
955
+ onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
956
+ onRefresh: reloadFile,
957
+ activePath: active,
958
+ diffIdx: contentReady ? diffIdx : 0,
959
+ diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => file.path === active)?.pending ?? 0),
960
+ fileIdx,
961
+ fileTotal: sum.pendingFiles.length,
962
+ onPrevDiff: () => gotoDiff(-1),
963
+ onNextDiff: () => gotoDiff(1),
964
+ onPrevFile: () => gotoFile(-1),
965
+ onNextFile: () => gotoFile(1),
966
+ onOpenNextFile: openNextFile,
967
+ })
968
+
904
969
  React.useEffect(() => {
905
970
  if (!sessionId) return
906
- const mode = editorDockMode(active)
907
- publishDiffDock(sessionId, {
908
- mode,
909
- pendingRegions,
910
- staleRegions,
911
- onAct: actHunk,
912
- onAcceptFile: acceptFile,
913
- onUndoFile: undoFile,
914
- onAcceptAllFiles: acceptAllFiles,
915
- onUndoAllFiles: undoAllFiles,
916
- allPendingCount: allPending.length,
917
- onRollback: rollbackFile,
918
- onJump: jumpTo,
919
- otherFiles,
920
- onOpenOther: (path) => openFile(path, true),
921
- onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
922
- onRefresh: reloadFile,
923
- activePath: active,
924
- diffIdx: contentReady ? diffIdx : 0,
925
- diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => file.path === active)?.pending ?? 0),
926
- fileIdx,
927
- fileTotal: sum.pendingFiles.length,
928
- onPrevDiff: () => gotoDiff(-1),
929
- onNextDiff: () => gotoDiff(1),
930
- onPrevFile: () => gotoFile(-1),
931
- onNextFile: () => gotoFile(1),
932
- onOpenNextFile: openNextFile,
933
- }, dockSourceRef.current)
971
+ publishDiffDock(sessionId, buildDockSnapshot(), dockSourceRef.current)
972
+ // 侧栏形态同步 Tab 角标计数(仅本形态写入;页签形态清零避免残留)
973
+ if (layout === 'side') setSidePending(sessionId, sum.totalFiles)
974
+ else setSidePending(sessionId, 0)
934
975
  })
935
976
 
936
977
  const overlay = launcherOpen
@@ -982,11 +1023,28 @@ export function EditorView(props) {
982
1023
  body,
983
1024
  hoverEl,
984
1025
  overlay)
1026
+ // 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
1027
+ const dismissHint = () => {
1028
+ setHintDismissed(true)
1029
+ try { localStorage.setItem('edrv.side-hint-dismissed', '1') } catch (e) { /* 忽略 */ }
1030
+ }
1031
+ const copyInstallCmd = () => {
1032
+ if (navigator.clipboard?.writeText) navigator.clipboard.writeText(SIDEBAR_INSTALL_CMD).catch(() => {})
1033
+ }
1034
+ const sideHintEl = (layout === 'tab' && sideHint && !hintDismissed)
1035
+ ? React.createElement('div', { className: 'edrv-side-hint' },
1036
+ React.createElement('span', { className: 'edrv-side-hint-text' }, '安装 dsh-better-sidebar 后可启用侧边栏编辑(对话+编辑同屏):'),
1037
+ React.createElement('code', { className: 'edrv-side-hint-cmd' }, SIDEBAR_INSTALL_CMD),
1038
+ React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
1039
+ React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
1040
+ : null
985
1041
  const mainCol = React.createElement('div', { className: 'edrv-main-col', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
986
1042
  pathBar,
987
1043
  tabRow,
1044
+ sideHintEl,
988
1045
  editorArea)
989
1046
  // 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
1047
+ // 侧栏形态由面板容器给高(100%),不做 composer 几何同步。
990
1048
  const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
991
1049
  (sidebarOn && sidebarPanels
992
1050
  ? React.createElement(SidebarView, {
@@ -1001,8 +1059,15 @@ export function EditorView(props) {
1001
1059
  : null),
1002
1060
  mainCol)
1003
1061
 
1004
- 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' } },
1005
- editorRow,
1006
- menuBackdrop,
1007
- tabMenuEl)
1062
+ const baseStyle = { minHeight: 0, display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base,transparent)', overflow: 'hidden' }
1063
+ const rootEl = layout === 'side'
1064
+ ? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
1065
+ editorRow,
1066
+ menuBackdrop,
1067
+ tabMenuEl)
1068
+ : React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', style: Object.assign({}, baseStyle, { height: 'var(--edrv-editor-height, 100%)', maxHeight: 'var(--edrv-editor-height, 100%)' }) },
1069
+ editorRow,
1070
+ menuBackdrop,
1071
+ tabMenuEl)
1072
+ return rootEl
1008
1073
  }
@@ -0,0 +1,48 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — SideEditorTab:betterSidebar「文件编辑」Tab 的组件包装。
4
+ * 把 TabComponentProps(scope/tab/visible)映射为 EditorView 侧栏形态 props,
5
+ * 并按会话 key 保证切会话状态干净重建;创建种子的初始打开在挂载后投递一次。
6
+ * 作者 ddj 2026年08月25号
7
+ */
8
+ import React from 'react'
9
+ import { EditorView } from './EditorView.js'
10
+ import { resolveInitialOpen, setSideEditorMounted, takePendingSideOpen } from '../sidebarBridge.js'
11
+
12
+ /**
13
+ * 侧边栏 Tab 组件(TabDescriptor.component 装配)。
14
+ * @param props TabComponentProps(scope.sessionId、tab.path/meta 等)
15
+ * @returns EditorView 侧栏形态
16
+ */
17
+ export function SideEditorTab(props) {
18
+ const sessionId = props?.scope?.sessionId
19
+ const initial = React.useMemo(() => resolveInitialOpen(props?.tab), [props?.tab])
20
+ const dispatchedRef = React.useRef(false)
21
+
22
+ React.useEffect(() => {
23
+ setSideEditorMounted(true)
24
+ return () => setSideEditorMounted(false)
25
+ }, [])
26
+
27
+ React.useEffect(() => {
28
+ if (dispatchedRef.current) return
29
+ dispatchedRef.current = true
30
+ // 待消费打开(挂载前暂存的聚焦请求)优先于创建种子;两者都走同事件,EditorView 幂等处理
31
+ const pending = takePendingSideOpen(sessionId)
32
+ const request = pending ?? initial
33
+ window.dispatchEvent(new CustomEvent('edrv:open-editor', {
34
+ detail: { path: request?.path ?? null, focusDiff: request?.focusDiff === true },
35
+ }))
36
+ }, [initial.path, initial.focusDiff, sessionId])
37
+
38
+ return React.createElement(EditorView, {
39
+ key: sessionId,
40
+ sessionId,
41
+ layout: 'side',
42
+ schedule: props?.schedule,
43
+ addToConversation: props?.addToConversation,
44
+ sidebarPanels: props?.sidebarPanels,
45
+ outlineSources: props?.outlineSources,
46
+ fileMenuItems: props?.fileMenuItems,
47
+ })
48
+ }
@@ -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
- await fs.writeText(target, (old || '') + line, void 0, void 0, policyOf(ctx, sc.session))
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)