dsh-vscode-mode 0.1.22 → 0.1.23

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(() => {
@@ -195,10 +205,15 @@ export function EditorView(props) {
195
205
 
196
206
  // 对话输入区的唯一差异 dock 读取此会话快照;编辑器卸载时只清理自己的发布者令牌。
197
207
  const dockSourceRef = React.useRef({})
198
- React.useEffect(() => () => clearDiffDock(sessionId, dockSourceRef.current), [sessionId])
208
+ React.useEffect(() => () => {
209
+ clearDiffDock(sessionId, dockSourceRef.current)
210
+ if (layout === 'side') setSidePending(sessionId, 0)
211
+ }, [sessionId])
199
212
 
200
213
  // DSH composer 保持原生布局;编辑区只读取几何边界并同步自己的高度。
214
+ // 侧栏形态:面板容器自带高度,跳过 composer 几何同步(面板内无会话滚动区)。
201
215
  React.useLayoutEffect(() => {
216
+ if (layout === 'side') return
202
217
  const root = viewRootRef.current
203
218
  const scroll = root?.closest?.('[data-conversation-scroll]')
204
219
  if (!root || !scroll) return
@@ -249,7 +264,7 @@ export function EditorView(props) {
249
264
  window.removeEventListener('resize', scheduleUpdate)
250
265
  root.style.removeProperty('--edrv-editor-height')
251
266
  }
252
- }, [sessionId])
267
+ }, [sessionId, layout])
253
268
 
254
269
  // localStorage v2 恢复页签
255
270
  React.useEffect(() => {
@@ -273,11 +288,12 @@ export function EditorView(props) {
273
288
  catch (e) { /* 忽略 */ }
274
289
  }, [tabs, active, sessionId])
275
290
 
276
- // 侧边栏状态:恢复(显隐/宽度/激活面板)
291
+ // 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
292
+ const sidebarKey = 'edrv.sidebar.' + (layout === 'side' ? 'side.' : '') + String(sessionId)
277
293
  React.useEffect(() => {
278
294
  if (!sessionId) return
279
295
  try {
280
- const raw = localStorage.getItem('edrv.sidebar.' + String(sessionId))
296
+ const raw = localStorage.getItem(sidebarKey)
281
297
  if (raw) {
282
298
  const saved = JSON.parse(raw)
283
299
  if (typeof saved.on === 'boolean') setSidebarOn(saved.on)
@@ -285,14 +301,14 @@ export function EditorView(props) {
285
301
  if (typeof saved.panel === 'string') setActivePanel(saved.panel)
286
302
  }
287
303
  } catch (e) { /* 损坏忽略 */ }
288
- }, [sessionId])
304
+ }, [sessionId, sidebarKey])
289
305
 
290
306
  // 侧边栏状态持久化
291
307
  React.useEffect(() => {
292
308
  if (!sessionId) return
293
- try { localStorage.setItem('edrv.sidebar.' + String(sessionId), JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
309
+ try { localStorage.setItem(sidebarKey, JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
294
310
  catch (e) { /* 忽略 */ }
295
- }, [sidebarOn, sidebarW, activePanel, sessionId])
311
+ }, [sidebarOn, sidebarW, activePanel, sessionId, sidebarKey])
296
312
 
297
313
  // Ctrl+B 切换侧边栏(capture 抢占,避免与 DSH 全局冲突)
298
314
  React.useEffect(() => {
@@ -431,6 +447,7 @@ export function EditorView(props) {
431
447
  }
432
448
  if (editorRef.current || !monacoRef.current) return
433
449
  const m = monacoRef.current
450
+ const side = layoutRef.current === 'side'
434
451
  const ed = m.editor.create(node, {
435
452
  value: '',
436
453
  language: 'plaintext',
@@ -438,7 +455,7 @@ export function EditorView(props) {
438
455
  fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
439
456
  fontSize: 13,
440
457
  lineHeight: 20,
441
- minimap: { enabled: true, scale: 1 },
458
+ minimap: { enabled: !side, scale: 1 },
442
459
  glyphMargin: true,
443
460
  lineDecorationsWidth: 16,
444
461
  automaticLayout: true,
@@ -448,7 +465,7 @@ export function EditorView(props) {
448
465
  renderWhitespace: 'selection',
449
466
  smoothScrolling: true,
450
467
  cursorBlinking: 'smooth',
451
- padding: { top: 8 },
468
+ padding: { top: side ? 6 : 8 },
452
469
  })
453
470
  ed.addCommand(m.KeyMod.CtrlCmd | m.KeyCode.KeyS, () => { flushSave(); doSave(false) })
454
471
  ed.onDidChangeModelContent(() => {
@@ -901,36 +918,41 @@ export function EditorView(props) {
901
918
  React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
902
919
  : null
903
920
 
921
+ /** 组装差异 dock 快照(发布给 conversation.input.dock 的唯一 DiffBox 实例)。 */
922
+ const buildDockSnapshot = () => ({
923
+ mode: editorDockMode(active),
924
+ pendingRegions,
925
+ staleRegions,
926
+ onAct: actHunk,
927
+ onAcceptFile: acceptFile,
928
+ onUndoFile: undoFile,
929
+ onAcceptAllFiles: acceptAllFiles,
930
+ onUndoAllFiles: undoAllFiles,
931
+ allPendingCount: allPending.length,
932
+ onRollback: rollbackFile,
933
+ onJump: jumpTo,
934
+ otherFiles,
935
+ onOpenOther: (path) => openFile(path, true),
936
+ onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
937
+ onRefresh: reloadFile,
938
+ activePath: active,
939
+ diffIdx: contentReady ? diffIdx : 0,
940
+ diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => file.path === active)?.pending ?? 0),
941
+ fileIdx,
942
+ fileTotal: sum.pendingFiles.length,
943
+ onPrevDiff: () => gotoDiff(-1),
944
+ onNextDiff: () => gotoDiff(1),
945
+ onPrevFile: () => gotoFile(-1),
946
+ onNextFile: () => gotoFile(1),
947
+ onOpenNextFile: openNextFile,
948
+ })
949
+
904
950
  React.useEffect(() => {
905
951
  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)
952
+ publishDiffDock(sessionId, buildDockSnapshot(), dockSourceRef.current)
953
+ // 侧栏形态同步 Tab 角标计数(仅本形态写入;页签形态清零避免残留)
954
+ if (layout === 'side') setSidePending(sessionId, sum.totalFiles)
955
+ else setSidePending(sessionId, 0)
934
956
  })
935
957
 
936
958
  const overlay = launcherOpen
@@ -982,11 +1004,28 @@ export function EditorView(props) {
982
1004
  body,
983
1005
  hoverEl,
984
1006
  overlay)
1007
+ // 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
1008
+ const dismissHint = () => {
1009
+ setHintDismissed(true)
1010
+ try { localStorage.setItem('edrv.side-hint-dismissed', '1') } catch (e) { /* 忽略 */ }
1011
+ }
1012
+ const copyInstallCmd = () => {
1013
+ if (navigator.clipboard?.writeText) navigator.clipboard.writeText(SIDEBAR_INSTALL_CMD).catch(() => {})
1014
+ }
1015
+ const sideHintEl = (layout === 'tab' && sideHint && !hintDismissed)
1016
+ ? React.createElement('div', { className: 'edrv-side-hint' },
1017
+ React.createElement('span', { className: 'edrv-side-hint-text' }, '安装 dsh-better-sidebar 后可启用侧边栏编辑(对话+编辑同屏):'),
1018
+ React.createElement('code', { className: 'edrv-side-hint-cmd' }, SIDEBAR_INSTALL_CMD),
1019
+ React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
1020
+ React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
1021
+ : null
985
1022
  const mainCol = React.createElement('div', { className: 'edrv-main-col', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
986
1023
  pathBar,
987
1024
  tabRow,
1025
+ sideHintEl,
988
1026
  editorArea)
989
1027
  // 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
1028
+ // 侧栏形态由面板容器给高(100%),不做 composer 几何同步。
990
1029
  const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
991
1030
  (sidebarOn && sidebarPanels
992
1031
  ? React.createElement(SidebarView, {
@@ -1001,8 +1040,15 @@ export function EditorView(props) {
1001
1040
  : null),
1002
1041
  mainCol)
1003
1042
 
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)
1043
+ const baseStyle = { minHeight: 0, display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base,transparent)', overflow: 'hidden' }
1044
+ const rootEl = layout === 'side'
1045
+ ? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
1046
+ editorRow,
1047
+ menuBackdrop,
1048
+ tabMenuEl)
1049
+ : 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%)' }) },
1050
+ editorRow,
1051
+ menuBackdrop,
1052
+ tabMenuEl)
1053
+ return rootEl
1008
1054
  }
@@ -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
+ }