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.
- package/README.md +34 -25
- package/lib/client.js +538 -98
- package/lib/client.js.map +1 -1
- package/lib/index.js +4 -1
- package/lib/index.js.map +1 -1
- package/package.json +5 -2
- package/src/client/events.ts +8 -4
- package/src/client/index.ts +36 -9
- package/src/client/monaco/diffRender.ts +3 -0
- package/src/client/sidebar/panels/FileExplorer.ts +12 -2
- package/src/client/sidebarBridge.ts +247 -0
- package/src/client/styles/editor.css +11 -0
- package/src/client/ui/ContextMenu.ts +37 -12
- package/src/client/ui/DiffBadge.ts +5 -3
- package/src/client/ui/EditorView.ts +108 -43
- package/src/client/ui/SideEditorTab.ts +48 -0
- package/src/client/ui/menuPosition.ts +63 -0
- package/src/rpc.ts +7 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH 上的类 VSCode 编码体验:Monaco
|
|
3
|
+
"version": "0.1.25",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -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",
|
package/src/client/events.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 跨组件/跨 slot 的窗口事件助手。
|
|
3
|
-
* 迁移自原 src/client/index.ts 的 openEditorView
|
|
3
|
+
* 迁移自原 src/client/index.ts 的 openEditorView 与事件派发,语义不改;
|
|
4
|
+
* 侧边栏形态(sidebarBridge 已注册路由)优先,否则回退 DOM 页签点击。
|
|
4
5
|
* 作者 ddj 2026-08-20
|
|
5
6
|
*/
|
|
7
|
+
import { routeSideEditor } from './sidebarBridge.js'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
* 选择中央「文件编辑」页签(DOM 级,无需 store actions
|
|
10
|
+
* 选择中央「文件编辑」页签(DOM 级,无需 store actions)。仅旧页签形态使用。
|
|
9
11
|
* @author ddj 2026年08月26号
|
|
10
12
|
*/
|
|
11
13
|
function selectEditorTab(): void {
|
|
@@ -19,11 +21,12 @@ function selectEditorTab(): void {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
24
|
+
* 打开「文件编辑」:侧边栏形态路由优先,旧形态回退中央页签。
|
|
23
25
|
* @author ddj 2026年08月26号
|
|
24
26
|
* @param path 要打开的路径(可空)
|
|
25
27
|
*/
|
|
26
28
|
export function openEditorView(path: string | null): void {
|
|
29
|
+
if (routeSideEditor(path ?? null, false)) return
|
|
27
30
|
selectEditorTab()
|
|
28
31
|
const target = path ?? null
|
|
29
32
|
setTimeout(() => {
|
|
@@ -32,11 +35,12 @@ export function openEditorView(path: string | null): void {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
*
|
|
38
|
+
* 打开文件编辑页并聚焦指定文件的首个差异(侧边栏形态自动展开面板)。
|
|
36
39
|
* @author ddj 2026年08月26号
|
|
37
40
|
* @param path 待聚焦的差异文件路径
|
|
38
41
|
*/
|
|
39
42
|
export function openDiffView(path: string): void {
|
|
43
|
+
if (routeSideEditor(path, true)) return
|
|
40
44
|
selectEditorTab()
|
|
41
45
|
setTimeout(() => {
|
|
42
46
|
window.dispatchEvent(new CustomEvent('edrv:open-editor', {
|
package/src/client/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 浏览器半入口:slot 注册 + 装配。
|
|
3
|
-
* 挂点:
|
|
3
|
+
* 挂点:betterSidebar「文件编辑」Tab(侧边栏形态,对话+编辑同屏;未装 dsh-better-sidebar 时
|
|
4
|
+
* 回退 conversation.view 中央页签)+ conversation.input.dock 差异条 + header 差异角标。
|
|
4
5
|
* 与 Host 通信:同源 fetch('/edrv/rpc')(shared/rpc 契约)。
|
|
5
6
|
*
|
|
6
7
|
* ⚠️ 跨版本 slot 装配(2026-08-21):新版 DSH 的 slots 系统要求 slot 必须由父 entry
|
|
@@ -25,6 +26,8 @@ import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
|
25
26
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
26
27
|
import { SettingsContext } from './settingsContext.js'
|
|
27
28
|
import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat.js'
|
|
29
|
+
import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
|
|
30
|
+
import { SideEditorTab } from './ui/SideEditorTab.js'
|
|
28
31
|
import { createAddToConversation } from './addToConversation.js'
|
|
29
32
|
import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
30
33
|
import { createFilePanel } from './sidebar/panels/index.js'
|
|
@@ -72,6 +75,8 @@ export function apply(ctx: any): void {
|
|
|
72
75
|
const binder = pickSettingsBinder(ctx)
|
|
73
76
|
const settings = binder.scope
|
|
74
77
|
let selected = autoValue('auto')
|
|
78
|
+
/** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
|
|
79
|
+
const sideService = detectSidebarService(ctx)
|
|
75
80
|
/** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
|
|
76
81
|
const addToConversation = createAddToConversation(ctx)
|
|
77
82
|
|
|
@@ -80,6 +85,7 @@ export function apply(ctx: any): void {
|
|
|
80
85
|
{ name: '设置桥', active: settings !== undefined, note: settings !== undefined ? '使用 ' + binder.service + ' 桥' : '未绑定设置服务(fileOpenTool 持久化不可用)' },
|
|
81
86
|
{ name: '文件打开路由(workspaces.openPath)', active: Boolean(workspaces?.openPath), note: 'vscode 打开器优先,失败回退系统打开' },
|
|
82
87
|
{ name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
|
|
88
|
+
{ name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ')', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(对话+编辑同屏)' : '未检测到;安装 dsh-better-sidebar 后刷新启用侧边栏形态(' + SIDEBAR_INSTALL_CMD + ')' },
|
|
83
89
|
]
|
|
84
90
|
|
|
85
91
|
ctx.provide('fileOpeners', registry)
|
|
@@ -134,14 +140,35 @@ export function apply(ctx: any): void {
|
|
|
134
140
|
}), 'vscode-mode: file link routing')
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
// 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态可用时编辑器住 betterSidebar Tab,
|
|
144
|
+
// 本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
|
|
145
|
+
const registerLegacyTab = (): void => {
|
|
146
|
+
registerSlotSafely(ctx, {
|
|
147
|
+
name: 'conversation.view',
|
|
148
|
+
id: 'edrv-editor',
|
|
149
|
+
order: 5,
|
|
150
|
+
label: '文件编辑',
|
|
151
|
+
inject: (sessionId: string) => ({ sessionId }),
|
|
152
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (sideService) {
|
|
156
|
+
// 侧边栏形态:注册「文件编辑」Tab(单实例、可按会话持久化、内容打开自动展开面板)
|
|
157
|
+
ctx.effect(() => installSideEditor({
|
|
158
|
+
service: sideService,
|
|
159
|
+
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })),
|
|
160
|
+
activeSession: () => {
|
|
161
|
+
const snapshot = sessions?.list?.getSnapshot?.() as { current?: string; byId?: Record<string, { cwd?: string }> } | undefined
|
|
162
|
+
const sessionId = snapshot?.current
|
|
163
|
+
if (!sessionId) return undefined
|
|
164
|
+
return { sessionId, cwd: snapshot?.byId?.[sessionId]?.cwd }
|
|
165
|
+
},
|
|
166
|
+
registerLegacyFallback: registerLegacyTab,
|
|
167
|
+
}), 'vscode-mode: sidebar editor tab')
|
|
168
|
+
} else {
|
|
169
|
+
setEnsureSideEditor(null)
|
|
170
|
+
registerLegacyTab()
|
|
171
|
+
}
|
|
145
172
|
|
|
146
173
|
// 对话输入框上方差异 dock:普通对话显示单文案按钮,文件编辑页由 EditorView 隐藏
|
|
147
174
|
registerSlotSafely(ctx, {
|
|
@@ -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 变化下次打开生效)。
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 侧边栏编辑区桥:可选探测 dsh-better-sidebar 的
|
|
3
|
+
* ctx.betterSidebar 服务,并注册「文件编辑」Tab(单实例)。
|
|
4
|
+
* 可选依赖模式:服务缺失时调用方回退到中央页签形态;本模块不注册任何东西。
|
|
5
|
+
* 纯逻辑(探测/初始打开解析/角标计数)可单测,DOM/事件仅存在于 install 路径。
|
|
6
|
+
* 作者 ddj 2026年08月25号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
|
|
11
|
+
/** betterSidebar 服务名(ctx.get 用;不要加进 inject——缺失会让插件停靠等待)。 */
|
|
12
|
+
export const BETTER_SIDEBAR_SERVICE = 'betterSidebar'
|
|
13
|
+
/** 本插件在侧边栏注册的 Tab 类型 id(亦是 SidebarTab.type)。 */
|
|
14
|
+
export const SIDE_TAB_ID = 'edrv-editor'
|
|
15
|
+
/** 侧边栏 Tab 标题。 */
|
|
16
|
+
export const SIDE_TAB_TITLE = '文件编辑'
|
|
17
|
+
/** 推荐安装命令(提示条/兼容性报告共用)。 */
|
|
18
|
+
export const SIDEBAR_INSTALL_CMD = 'dsh plugin --profile web add dsh-better-sidebar'
|
|
19
|
+
|
|
20
|
+
/** Tab 元数据(openTab 种子随 Tab 持久化,创建路径用它恢复初始打开)。 */
|
|
21
|
+
export interface SideEditorMeta {
|
|
22
|
+
/** 待打开的工作区相对/绝对路径。 */
|
|
23
|
+
openPath?: string
|
|
24
|
+
/** 打开后聚焦首个差异。 */
|
|
25
|
+
focusDiff?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* betterSidebar 服务的最小结构面(结构性探测,不 import 第三方类型)。
|
|
30
|
+
*/
|
|
31
|
+
export interface SidebarServiceLike {
|
|
32
|
+
registerTab: (descriptor: unknown) => () => void
|
|
33
|
+
openTab: (seed: { type: string; title?: string; path?: string; meta?: unknown }, scope?: { sessionId: string; cwd?: string }) => void
|
|
34
|
+
isTabEnabled?: (id: string) => boolean
|
|
35
|
+
features?: readonly string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 解析 Tab 挂载时的初始打开请求(创建路径读 tab.path/meta,聚焦路径走窗口事件)。
|
|
40
|
+
* @author ddj 2026年08月25号
|
|
41
|
+
* @param tab 侧边栏 Tab(path/meta 为可选字段)
|
|
42
|
+
* @returns 待打开路径与是否聚焦差异
|
|
43
|
+
*/
|
|
44
|
+
export function resolveInitialOpen(tab: { path?: string; meta?: unknown } | null | undefined): { path: string | null; focusDiff: boolean } {
|
|
45
|
+
const meta = (tab?.meta ?? {}) as SideEditorMeta
|
|
46
|
+
const path = typeof tab?.path === 'string' && tab.path ? tab.path : (typeof meta?.openPath === 'string' ? meta.openPath : null)
|
|
47
|
+
return { path: path || null, focusDiff: meta?.focusDiff === true }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 会话级待处理差异文件数缓存(Tab 角标同步读取;EditorView 侧栏形态写入)。 */
|
|
51
|
+
const sidePending = new Map<string, number>()
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 写入会话的待处理差异文件数(0 视为清除,不显示角标)。
|
|
55
|
+
* @author ddj 2026年08月25号
|
|
56
|
+
* @param sessionId 会话 id
|
|
57
|
+
* @param count 待处理差异文件数
|
|
58
|
+
*/
|
|
59
|
+
export function setSidePending(sessionId: string, count: number): void {
|
|
60
|
+
if (!sessionId) return
|
|
61
|
+
const value = Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0
|
|
62
|
+
if (value > 0) sidePending.set(sessionId, value)
|
|
63
|
+
else sidePending.delete(sessionId)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 读取会话的待处理差异文件数(角标用)。
|
|
68
|
+
* @author ddj 2026年08月25号
|
|
69
|
+
* @param sessionId 会话 id
|
|
70
|
+
* @returns 非负计数
|
|
71
|
+
*/
|
|
72
|
+
export function getSidePending(sessionId?: string): number {
|
|
73
|
+
if (!sessionId) return 0
|
|
74
|
+
return sidePending.get(sessionId) ?? 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 模块级侧栏打开路由(events.ts 优先走它;未注册 = 旧页签形态)。 */
|
|
78
|
+
let ensureSideEditor: ((path: string | null, focusDiff: boolean) => boolean) | null = null
|
|
79
|
+
|
|
80
|
+
/** 待消费的初始打开(Tab 组件尚未挂载时暂存;按会话匹配,挂载时取走)。 */
|
|
81
|
+
let pendingOpen: { sessionId: string; path: string | null; focusDiff: boolean } | null = null
|
|
82
|
+
/** Tab 组件当前是否有挂载实例(有则直接投递窗口事件,无则暂存待消费)。 */
|
|
83
|
+
let sideEditorMounted = false
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 注册/撤销侧栏打开路由。
|
|
87
|
+
* @author ddj 2026年08月25号
|
|
88
|
+
* @param fn 路由函数或 null
|
|
89
|
+
*/
|
|
90
|
+
export function setEnsureSideEditor(fn: ((path: string | null, focusDiff: boolean) => boolean) | null): void {
|
|
91
|
+
ensureSideEditor = fn
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 标记侧栏编辑器组件挂载状态(SideEditorTab 挂载/卸载时调用)。
|
|
96
|
+
* @author ddj 2026年08月25号
|
|
97
|
+
* @param mounted 是否已挂载
|
|
98
|
+
*/
|
|
99
|
+
export function setSideEditorMounted(mounted: boolean): void {
|
|
100
|
+
sideEditorMounted = mounted
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 暂存指定会话的待消费初始打开(Tab 组件未挂载时由 install 路径调用)。
|
|
105
|
+
* @author ddj 2026年08月25号
|
|
106
|
+
* @param sessionId 会话 id
|
|
107
|
+
* @param path 待打开路径(可空)
|
|
108
|
+
* @param focusDiff 是否聚焦首个差异
|
|
109
|
+
*/
|
|
110
|
+
export function stagePendingSideOpen(sessionId: string, path: string | null, focusDiff: boolean): void {
|
|
111
|
+
if (!sessionId) return
|
|
112
|
+
pendingOpen = { sessionId, path: path || null, focusDiff: focusDiff === true }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 取走指定会话的待消费初始打开(SideEditorTab 挂载时读取;不存在返回 null)。
|
|
117
|
+
* @author ddj 2026年08月25号
|
|
118
|
+
* @param sessionId 会话 id
|
|
119
|
+
* @returns 待打开请求或 null
|
|
120
|
+
*/
|
|
121
|
+
export function takePendingSideOpen(sessionId?: string): { path: string | null; focusDiff: boolean } | null {
|
|
122
|
+
if (!pendingOpen || !sessionId || pendingOpen.sessionId !== sessionId) return null
|
|
123
|
+
const value = { path: pendingOpen.path, focusDiff: pendingOpen.focusDiff }
|
|
124
|
+
pendingOpen = null
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 打开/聚焦侧栏编辑器(打开路径可选;返回是否已被侧栏形态接管)。
|
|
130
|
+
* @author ddj 2026年08月25号
|
|
131
|
+
* @param path 待打开路径(可空)
|
|
132
|
+
* @param focusDiff 是否聚焦首个差异
|
|
133
|
+
* @returns 是否路由成功
|
|
134
|
+
*/
|
|
135
|
+
export function routeSideEditor(path: string | null, focusDiff: boolean): boolean {
|
|
136
|
+
if (typeof ensureSideEditor !== 'function') return false
|
|
137
|
+
try {
|
|
138
|
+
return ensureSideEditor(path, focusDiff) === true
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.warn('[dsh-vscode-mode] 侧栏编辑器打开失败(' + String(error) + '),已回退')
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 结构化探测 betterSidebar 服务(可选依赖:缺失/降级返回 undefined)。
|
|
147
|
+
* @author ddj 2026年08月25号
|
|
148
|
+
* @param ctx 客户端服务上下文
|
|
149
|
+
* @returns 服务结构面或 undefined
|
|
150
|
+
*/
|
|
151
|
+
export function detectSidebarService(ctx: { get: (name: string) => unknown }): SidebarServiceLike | undefined {
|
|
152
|
+
try {
|
|
153
|
+
const service = ctx.get(BETTER_SIDEBAR_SERVICE) as SidebarServiceLike | undefined
|
|
154
|
+
if (!service || typeof service.registerTab !== 'function' || typeof service.openTab !== 'function') return undefined
|
|
155
|
+
return service
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 注册侧边栏「文件编辑」Tab 并接管打开路由。
|
|
163
|
+
* Tab 被用户在 better-sidebar 设置里禁用时,打开路由降级为回退回调(中央页签)。
|
|
164
|
+
* @author ddj 2026年08月25号
|
|
165
|
+
* @param options 装配参数(组件依赖 + 活动会话解析 + 回退注册)
|
|
166
|
+
* @returns 卸载器(反注册 Tab + 撤销路由)
|
|
167
|
+
*/
|
|
168
|
+
export function installSideEditor(options: {
|
|
169
|
+
service: SidebarServiceLike
|
|
170
|
+
renderTab: (props: Record<string, unknown>) => unknown
|
|
171
|
+
activeSession: () => { sessionId?: string; cwd?: string } | undefined
|
|
172
|
+
registerLegacyFallback: () => void
|
|
173
|
+
}): () => void {
|
|
174
|
+
const { service, renderTab, activeSession, registerLegacyFallback } = options
|
|
175
|
+
let fallbackInstalled = false
|
|
176
|
+
const open = (path: string | null, focusDiff: boolean): boolean => {
|
|
177
|
+
if (typeof service.isTabEnabled === 'function' && !service.isTabEnabled(SIDE_TAB_ID)) {
|
|
178
|
+
if (!fallbackInstalled) {
|
|
179
|
+
fallbackInstalled = true
|
|
180
|
+
try { registerLegacyFallback() } catch (error) { console.warn('[dsh-vscode-mode] 回退注册失败(' + String(error) + ')') }
|
|
181
|
+
}
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
const scope = activeSession()
|
|
185
|
+
const sessionId = scope?.sessionId
|
|
186
|
+
if (!sessionId) return false
|
|
187
|
+
try {
|
|
188
|
+
service.openTab({
|
|
189
|
+
type: SIDE_TAB_ID,
|
|
190
|
+
title: SIDE_TAB_TITLE,
|
|
191
|
+
...(path ? { path } : {}),
|
|
192
|
+
meta: { openPath: path || undefined, focusDiff },
|
|
193
|
+
}, { sessionId, cwd: scope?.cwd })
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.warn('[dsh-vscode-mode] openTab 失败(' + String(error) + ')')
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
if (sideEditorMounted) {
|
|
199
|
+
// 已挂载:窗口事件直达 EditorView 监听器
|
|
200
|
+
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path, focusDiff } }))
|
|
201
|
+
} else {
|
|
202
|
+
// 未挂载(面板折叠卸载/首次创建):暂存待消费,挂载时由 SideEditorTab 取走
|
|
203
|
+
stagePendingSideOpen(sessionId, path, focusDiff)
|
|
204
|
+
}
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
let tabDisposer: (() => void) | null = null
|
|
208
|
+
try {
|
|
209
|
+
tabDisposer = service.registerTab({
|
|
210
|
+
id: SIDE_TAB_ID,
|
|
211
|
+
title: SIDE_TAB_TITLE,
|
|
212
|
+
icon: codeIcon,
|
|
213
|
+
single: true,
|
|
214
|
+
badge: (_ctx: unknown, scope: { sessionId?: string }) => getSidePending(scope?.sessionId) || null,
|
|
215
|
+
component: renderTab,
|
|
216
|
+
})
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.warn('[dsh-vscode-mode] 侧边栏 Tab 注册失败(' + String(error) + ')')
|
|
219
|
+
// 注册失败 = 侧栏形态不可用:立即回退中央页签,不接管打开路由
|
|
220
|
+
if (!fallbackInstalled) {
|
|
221
|
+
fallbackInstalled = true
|
|
222
|
+
try { registerLegacyFallback() } catch (inner) { console.warn('[dsh-vscode-mode] 回退注册失败(' + String(inner) + ')') }
|
|
223
|
+
}
|
|
224
|
+
return () => {}
|
|
225
|
+
}
|
|
226
|
+
setEnsureSideEditor(open)
|
|
227
|
+
return () => {
|
|
228
|
+
if (typeof tabDisposer === 'function') tabDisposer()
|
|
229
|
+
setEnsureSideEditor(null)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 侧边栏 Tab 图标(内联 SVG 代码括号,不依赖图标包)。
|
|
235
|
+
* @author ddj 2026年08月25号
|
|
236
|
+
* @param size 边长(px)
|
|
237
|
+
* @returns 图标元素
|
|
238
|
+
*/
|
|
239
|
+
function codeIcon(size: number): unknown {
|
|
240
|
+
return React.createElement('svg', {
|
|
241
|
+
width: size, height: size, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true, focusable: false,
|
|
242
|
+
},
|
|
243
|
+
React.createElement('path', {
|
|
244
|
+
d: 'M5.5 4.5 2.5 8l3 3.5M10.5 4.5 13.5 8l-3 3.5',
|
|
245
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
246
|
+
}))
|
|
247
|
+
}
|
|
@@ -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); }
|
|
@@ -232,3 +233,13 @@
|
|
|
232
233
|
[data-edrv-view] .edrv-side-resize:hover { background: var(--dsw-alias-brand-primary, #0f9d58); opacity: .35; }
|
|
233
234
|
[data-edrv-view] .edrv-chip-btn.edrv-chip-on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.12)); color: var(--dsw-alias-label-primary, #1f2933); }
|
|
234
235
|
|
|
236
|
+
/* ===== 侧栏形态(betterSidebar 面板内编辑区) ===== */
|
|
237
|
+
[data-edrv-view][data-edrv-layout='side'] { box-sizing: border-box; height: 100%; }
|
|
238
|
+
|
|
239
|
+
/* ===== 侧边栏引导条(旧页签形态、未装 betterSidebar 时) ===== */
|
|
240
|
+
[data-edrv-view] .edrv-side-hint { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; flex-wrap: wrap; padding: 6px 12px; font-size: 12px; color: var(--dsw-alias-label-secondary, #52606d); background: var(--dsw-specific-tip, var(--dsw-alias-bg-layer-2, #f0f4f4)); border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); }
|
|
241
|
+
[data-edrv-view] .edrv-side-hint-text { white-space: nowrap; }
|
|
242
|
+
[data-edrv-view] .edrv-side-hint-cmd { font-size: 11px; padding: 2px 6px; border-radius: 6px; background: var(--dsw-alias-bg-layer-1, #ffffff); border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); color: var(--dsw-alias-label-primary, #1f2933); white-space: nowrap; }
|
|
243
|
+
[data-edrv-view] .edrv-side-hint-close { margin-left: auto; border: none; background: transparent; color: var(--dsw-alias-label-tertiary, #9aa5b1); cursor: pointer; font-size: 14px; line-height: 1; padding: 2px 6px; border-radius: 6px; flex-shrink: 0; }
|
|
244
|
+
[data-edrv-view] .edrv-side-hint-close:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); color: var(--dsw-alias-label-primary, #1f2933); }
|
|
245
|
+
|
|
@@ -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
|
}
|
|
@@ -14,7 +14,7 @@ import { summarize } from '../state/records.js'
|
|
|
14
14
|
*/
|
|
15
15
|
export function DiffBadge(props) {
|
|
16
16
|
const sessionId = props?.sessionId
|
|
17
|
-
const [
|
|
17
|
+
const [summary, setSummary] = React.useState(null)
|
|
18
18
|
const seq = React.useRef(0)
|
|
19
19
|
|
|
20
20
|
const load = React.useCallback(() => {
|
|
@@ -22,7 +22,7 @@ export function DiffBadge(props) {
|
|
|
22
22
|
const s = ++seq.current
|
|
23
23
|
rpc('edrv.list', { sessionId }).then((res) => {
|
|
24
24
|
if (s !== seq.current || !res || !res.ok || !Array.isArray(res.records)) return
|
|
25
|
-
|
|
25
|
+
setSummary(summarize(res.records))
|
|
26
26
|
}).catch(() => {})
|
|
27
27
|
}, [sessionId])
|
|
28
28
|
|
|
@@ -34,10 +34,12 @@ export function DiffBadge(props) {
|
|
|
34
34
|
return () => { clearInterval(t); window.removeEventListener('edrv:refresh', onRefresh) }
|
|
35
35
|
}, [load])
|
|
36
36
|
|
|
37
|
+
const n = summary?.totalFiles ?? 0
|
|
37
38
|
if (!n) return null
|
|
38
39
|
|
|
39
40
|
const click = () => {
|
|
40
|
-
|
|
41
|
+
// 侧栏形态:带首个待处理文件路径(内容打开自动展开面板);旧形态行为不变
|
|
42
|
+
openEditorView(summary?.pendingFiles?.[0]?.path ?? null)
|
|
41
43
|
setTimeout(() => { emitShowLauncher() }, 120)
|
|
42
44
|
}
|
|
43
45
|
|