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.
- package/README.md +26 -20
- package/lib/client.js +443 -72
- package/lib/client.js.map +1 -1
- package/package.json +2 -2
- package/src/client/events.ts +8 -4
- package/src/client/index.ts +36 -9
- package/src/client/sidebarBridge.ts +247 -0
- package/src/client/styles/editor.css +10 -0
- package/src/client/ui/DiffBadge.ts +5 -3
- package/src/client/ui/EditorView.ts +88 -42
- package/src/client/ui/SideEditorTab.ts +48 -0
|
@@ -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(
|
|
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(() => () =>
|
|
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(
|
|
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(
|
|
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:
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
+
}
|