dsh-vscode-mode 0.1.48 → 0.1.49
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 +10 -1
- package/lib/client.js +557 -32
- package/lib/client.js.map +1 -1
- package/lib/index.js +562 -3
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +1 -1
- package/src/client/index.ts +9 -2
- package/src/client/paths.ts +2 -0
- package/src/client/sidebar/SidebarView.ts +82 -16
- package/src/client/sidebar/panels/RulesPanel.ts +284 -0
- package/src/client/sidebar/panels/index.ts +18 -2
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/sidebarMin.ts +48 -0
- package/src/client/styles/editor.css +43 -0
- package/src/client/ui/EditorView.ts +22 -4
- package/src/client/ui/McpSettings.ts +37 -2
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/fileOpenSettings.ts +2 -0
- package/src/index.ts +5 -1
- package/src/rpc.ts +37 -0
- package/src/rules.ts +528 -0
- package/src/shared/rpc.ts +11 -0
- package/src/shared/rules.ts +60 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
package/src/client/compat.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const SIDEBAR_PLUGIN = 'dsh-better-sidebar'
|
|
|
11
11
|
|
|
12
12
|
/** 设置 scope 的跨组件上下文形状(与官方 settingsScope 绑定的最小面)。 */
|
|
13
13
|
export interface SettingsScopeLike {
|
|
14
|
-
getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown; keybindings?: Record<string, string
|
|
14
|
+
getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown; keybindings?: Record<string, string>; sidebarMinWidth?: unknown }; writable?: boolean }
|
|
15
15
|
subscribe: (listener: () => void) => () => void
|
|
16
16
|
set: (field: string, value: unknown) => Promise<void>
|
|
17
17
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -33,12 +33,14 @@ import { createAddToConversation } from './addToConversation.js'
|
|
|
33
33
|
import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
34
34
|
import { createFilePanel } from './sidebar/panels/index.js'
|
|
35
35
|
import { createSearchPanel } from './sidebar/panels/index.js'
|
|
36
|
+
import { createRulesPanel } from './sidebar/panels/index.js'
|
|
36
37
|
import { createTreeMenuRegistry } from './sidebar/contextMenu.js'
|
|
37
38
|
import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
|
|
38
39
|
import { createOutlinePanel } from './outline/index.js'
|
|
39
40
|
import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
|
|
40
41
|
import { createLspOutlineSource } from './outline/lspSource.js'
|
|
41
42
|
import { keybindingsApply } from './keybindings.js'
|
|
43
|
+
import { sidebarMinApply } from './sidebarMin.js'
|
|
42
44
|
import { setupLsp, setSession } from './monaco/lsp/index.js'
|
|
43
45
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
44
46
|
|
|
@@ -114,6 +116,8 @@ export function apply(ctx: any): void {
|
|
|
114
116
|
ctx.effect(() => sidebarPanels.register(createFilePanel()), 'vscode-mode: sidebar panel')
|
|
115
117
|
// 搜索面板:活动栏「搜索」页签(Ctrl+Shift+F 唤起,VSCode 搜索视图形态)
|
|
116
118
|
ctx.effect(() => sidebarPanels.register(createSearchPanel()), 'vscode-mode: sidebar panel search')
|
|
119
|
+
// 规则面板:活动栏「规则」页签(Codebuddy 规则管理形态:用户/项目规则 + 启用开关,host 注入生效)
|
|
120
|
+
ctx.effect(() => sidebarPanels.register(createRulesPanel()), 'vscode-mode: sidebar panel rules')
|
|
117
121
|
// 文件右键菜单项注册表(对外 provide,供本插件/第三方注册;内置「在文件浏览器中打开」)
|
|
118
122
|
const fileMenuItems = createTreeMenuRegistry()
|
|
119
123
|
ctx.provide('edrvFileContextMenuItems', fileMenuItems)
|
|
@@ -147,12 +151,15 @@ export function apply(ctx: any): void {
|
|
|
147
151
|
return settings.subscribe(sync)
|
|
148
152
|
}, 'vscode-mode: file opener setting sync')
|
|
149
153
|
// 快捷键配置同步:设置提交后立即刷新键位匹配(编辑器/QuickOpen 按事件时读取)
|
|
154
|
+
// 同一订阅里顺带同步侧边栏最小宽度(sidebarMinWidth)→ 派发 edrv:sidebar-min-width 通知编辑器重夹
|
|
150
155
|
ctx.effect(() => {
|
|
151
156
|
if (!settings) return undefined
|
|
152
157
|
const sync = (): void => {
|
|
153
158
|
const snapshot = settings.getSnapshot()
|
|
154
159
|
if (snapshot.status === 'loading') return
|
|
155
160
|
keybindingsApply(snapshot.value?.keybindings)
|
|
161
|
+
const minW = sidebarMinApply(snapshot.value?.sidebarMinWidth)
|
|
162
|
+
window.dispatchEvent(new CustomEvent('edrv:sidebar-min-width', { detail: { value: minW } }))
|
|
156
163
|
}
|
|
157
164
|
sync()
|
|
158
165
|
return settings.subscribe(sync)
|
|
@@ -181,7 +188,7 @@ export function apply(ctx: any): void {
|
|
|
181
188
|
order: 5,
|
|
182
189
|
label: '文件编辑',
|
|
183
190
|
inject: (sessionId: string) => ({ sessionId }),
|
|
184
|
-
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })))
|
|
191
|
+
}, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })))
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
// 侧边栏/中央页签两形态互斥切换:sideDisposer(侧边栏 Tab)与 legacyDisposer(中央页签)只保留其一。
|
|
@@ -197,7 +204,7 @@ export function apply(ctx: any): void {
|
|
|
197
204
|
if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
|
|
198
205
|
sideDisposer = ctx.effect(() => installSideEditor({
|
|
199
206
|
service: sideService as NonNullable<typeof sideService>,
|
|
200
|
-
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })),
|
|
207
|
+
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })),
|
|
201
208
|
activeSession: () => {
|
|
202
209
|
const snapshot = sessions?.list?.getSnapshot?.() as { current?: string; byId?: Record<string, { cwd?: string }> } | undefined
|
|
203
210
|
const sessionId = snapshot?.current
|
package/src/client/paths.ts
CHANGED
|
@@ -24,6 +24,8 @@ export const CACHE_KEY = {
|
|
|
24
24
|
sideHint: 'edrv.side-hint-dismissed',
|
|
25
25
|
/** 「性能优化」页工作区栏目展开状态(workspaceFoldCache v1,全局不按会话)。 */
|
|
26
26
|
workspaceFold: 'edrv.ws-fold.v1.',
|
|
27
|
+
/** 规则面板 UI 状态(RulesPanel v1:Tab 与项目工作区选择,全局不按会话)。 */
|
|
28
|
+
rules: 'edrv.rules.v1.',
|
|
27
29
|
} as const
|
|
28
30
|
|
|
29
31
|
/** 按前缀拼会话键(侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* dsh-vscode-mode client —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* dsh-vscode-mode client — 侧边栏容器(活动栏常显 + 可隐藏面板区 + 拖拽调宽)。
|
|
4
|
+
* 面板列表来自注册表(活动栏图标/徽标/激活态);最左侧图标列(活动栏)常显,
|
|
5
|
+
* 右侧详情面板区可拖拽调宽/隐藏:visible=false 时仅保留图标列(宽 = 图标列宽),
|
|
6
|
+
* 点击任一图标重新展开面板(onShow),底部按钮在 ◀ 收起 / ▶ 展开间切换。
|
|
7
|
+
* 拖拽调宽下限 = minWidth prop(通用设置 sidebarMinWidth,默认 300),低于下限直接收起面板区。
|
|
8
|
+
* 作者 ddj 2026-08-26 / 2026-09-04
|
|
7
9
|
*/
|
|
8
10
|
import React from 'react'
|
|
9
11
|
import type { SidebarCtx } from './types.js'
|
|
10
12
|
|
|
11
|
-
const W_MIN = 180
|
|
12
13
|
const W_MAX = 560
|
|
14
|
+
/** 面板区隐藏时容器的宽度(= 图标列宽,与 editor.css .edrv-rail 对齐)。 */
|
|
15
|
+
const RAIL_ONLY_W = 44
|
|
16
|
+
/** 隐藏态接缝拖拽条宽度(与 editor.css .edrv-side-resize 对齐)。 */
|
|
17
|
+
const SEAM_W = 5
|
|
18
|
+
/** 隐藏态拖出面板区的触发阈值(px,拖过即展开并继续实时调宽)。 */
|
|
19
|
+
const PULL_OUT_THRESHOLD = 12
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* 侧边栏容器。
|
|
@@ -17,18 +24,24 @@ const W_MAX = 560
|
|
|
17
24
|
* @param props.ctx 面板共享上下文
|
|
18
25
|
* @param props.activePanel 当前激活面板 id
|
|
19
26
|
* @param props.onActive 激活面板变化回调
|
|
27
|
+
* @param props.visible 面板区是否可见(false = 只显示活动栏)
|
|
28
|
+
* @param props.onShow 重新展开面板区回调(活动栏图标点击/底部按钮)
|
|
20
29
|
* @param props.width 面板区宽度
|
|
21
30
|
* @param props.onWidth 宽度变化回调
|
|
22
|
-
* @param props.onHide
|
|
31
|
+
* @param props.onHide 收起面板区回调
|
|
32
|
+
* @param props.minWidth 最小宽度(通用设置 sidebarMinWidth;拖拽低于它触发隐藏)
|
|
23
33
|
*/
|
|
24
34
|
export function SidebarView(props) {
|
|
25
35
|
const panels = props.registry?.list?.() ?? []
|
|
26
36
|
const ctx = props.ctx
|
|
27
37
|
const activePanel = props.activePanel
|
|
28
38
|
const onActive = props.onActive
|
|
39
|
+
const visible = props.visible !== false
|
|
40
|
+
const onShow = props.onShow
|
|
29
41
|
const width = props.width
|
|
30
42
|
const onWidth = props.onWidth
|
|
31
43
|
const onHide = props.onHide
|
|
44
|
+
const wMin = Math.max(120, Math.min(W_MAX, Math.round(Number(props.minWidth) || 300)))
|
|
32
45
|
|
|
33
46
|
const active = panels.find((p) => p.id === activePanel) || panels[0]
|
|
34
47
|
|
|
@@ -36,14 +49,51 @@ export function SidebarView(props) {
|
|
|
36
49
|
e.preventDefault()
|
|
37
50
|
const startX = e.clientX
|
|
38
51
|
const startW = width
|
|
52
|
+
const cleanup = () => {
|
|
53
|
+
window.removeEventListener('pointermove', onMove)
|
|
54
|
+
window.removeEventListener('pointerup', onUp)
|
|
55
|
+
}
|
|
56
|
+
const onUp = () => cleanup()
|
|
39
57
|
const onMove = (ev) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
58
|
+
const raw = startW + (ev.clientX - startX)
|
|
59
|
+
// 低于最小宽度:清理监听并直接收起面板区(不夹住)
|
|
60
|
+
if (raw < wMin) {
|
|
61
|
+
cleanup()
|
|
62
|
+
onHide()
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
onWidth(Math.min(W_MAX, raw))
|
|
42
66
|
}
|
|
43
|
-
|
|
67
|
+
window.addEventListener('pointermove', onMove)
|
|
68
|
+
window.addEventListener('pointerup', onUp)
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 隐藏态接缝拖拽:向右拖过阈值把面板区拉出来(以记忆宽度展开),继续拖动实时调宽。
|
|
74
|
+
* @param e pointerdown 事件
|
|
75
|
+
*/
|
|
76
|
+
const onSeamPointerDown = (e) => {
|
|
77
|
+
e.preventDefault()
|
|
78
|
+
const startX = e.clientX
|
|
79
|
+
const baseW = Math.max(wMin, Math.min(W_MAX, Math.round(Number(width) || wMin)))
|
|
80
|
+
let expanded = false
|
|
81
|
+
let expandStartX = 0
|
|
82
|
+
const cleanup = () => {
|
|
44
83
|
window.removeEventListener('pointermove', onMove)
|
|
45
84
|
window.removeEventListener('pointerup', onUp)
|
|
46
85
|
}
|
|
86
|
+
const onUp = () => cleanup()
|
|
87
|
+
const onMove = (ev) => {
|
|
88
|
+
if (!expanded) {
|
|
89
|
+
if (ev.clientX - startX < PULL_OUT_THRESHOLD) return
|
|
90
|
+
expanded = true
|
|
91
|
+
expandStartX = ev.clientX
|
|
92
|
+
onShow()
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
onWidth(Math.max(wMin, Math.min(W_MAX, baseW + (ev.clientX - expandStartX))))
|
|
96
|
+
}
|
|
47
97
|
window.addEventListener('pointermove', onMove)
|
|
48
98
|
window.addEventListener('pointerup', onUp)
|
|
49
99
|
}
|
|
@@ -54,20 +104,36 @@ export function SidebarView(props) {
|
|
|
54
104
|
const count = typeof p.badge === 'function' ? p.badge(ctx) : null
|
|
55
105
|
return React.createElement('button', {
|
|
56
106
|
key: p.id,
|
|
57
|
-
className: 'edrv-rail-btn' + (active && p.id === active.id ? ' edrv-rail-on' : ''),
|
|
107
|
+
className: 'edrv-rail-btn' + (visible && active && p.id === active.id ? ' edrv-rail-on' : ''),
|
|
58
108
|
title: p.title,
|
|
59
|
-
onClick: () =>
|
|
109
|
+
onClick: () => {
|
|
110
|
+
onActive(p.id)
|
|
111
|
+
// 面板区隐藏时点击图标 = 展开到该面板(VSCode 活动栏行为)
|
|
112
|
+
if (!visible && typeof onShow === 'function') onShow()
|
|
113
|
+
},
|
|
60
114
|
},
|
|
61
115
|
React.createElement('span', { className: 'edrv-rail-icon' }, p.icon),
|
|
62
116
|
count > 0
|
|
63
117
|
? React.createElement('span', { className: 'edrv-rail-badge' }, String(count))
|
|
64
118
|
: null)
|
|
65
119
|
})),
|
|
66
|
-
React.createElement('button', {
|
|
120
|
+
React.createElement('button', {
|
|
121
|
+
className: 'edrv-rail-btn',
|
|
122
|
+
title: (visible ? '隐藏面板区' : '展开面板区') + ' (Ctrl+B)',
|
|
123
|
+
onClick: () => (visible ? onHide() : onShow?.()),
|
|
124
|
+
}, visible ? '◀' : '▶'))
|
|
67
125
|
|
|
68
|
-
return React.createElement('div', {
|
|
126
|
+
return React.createElement('div', {
|
|
127
|
+
className: 'edrv-sidebar' + (visible ? '' : ' edrv-sidebar-rail-only'),
|
|
128
|
+
style: { width: visible ? width : RAIL_ONLY_W + SEAM_W },
|
|
129
|
+
},
|
|
69
130
|
rail,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
131
|
+
visible
|
|
132
|
+
? React.createElement('div', { className: 'edrv-side-body' },
|
|
133
|
+
active ? active.render(ctx) : null)
|
|
134
|
+
: null,
|
|
135
|
+
visible
|
|
136
|
+
? React.createElement('div', { className: 'edrv-side-resize', onPointerDown: onPointerDown })
|
|
137
|
+
// 隐藏态接缝:悬浮显示拖拽光标与高亮,向右拖把面板区拉出来
|
|
138
|
+
: React.createElement('div', { className: 'edrv-side-resize', title: '拖拽展开面板区', onPointerDown: onSeamPointerDown }))
|
|
73
139
|
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 侧边栏「规则」面板(Codebuddy 规则管理形态)。
|
|
4
|
+
* 双 Tab:用户规则(~/.dsh/rules/)与项目规则(<工作区>/.dsh/rules/);
|
|
5
|
+
* 列表行 = 文件名 + 相对提示 + 类型徽标(总是/自动/手动)+ 描述 + 编辑/删除/启用开关;
|
|
6
|
+
* 新建/编辑 = 内嵌表单(文件名 + 原始 .mdc 文本域,frontmatter 可见),保存走 rules.save。
|
|
7
|
+
* 作者 ddj 2026年09月03号
|
|
8
|
+
*/
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { rpc } from '../../rpc.js'
|
|
11
|
+
import type { SidebarCtx } from '../types.js'
|
|
12
|
+
import { CACHE_KEY } from '../../paths.js'
|
|
13
|
+
|
|
14
|
+
/** 新建规则模板(frontmatter 字段与 host parseRuleMdc 对齐)。 */
|
|
15
|
+
const NEW_TEMPLATE = '---\ndescription: \nalwaysApply: true\n---\n\n'
|
|
16
|
+
const TYPE_LABEL = { always: '总是', auto: '自动', manual: '手动' }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 读 localStorage(损坏/不可用安全)。
|
|
20
|
+
* @author ddj 2026年09月03号
|
|
21
|
+
* @param key 键
|
|
22
|
+
* @returns 值或 null
|
|
23
|
+
*/
|
|
24
|
+
function readLocal(key) {
|
|
25
|
+
try { return localStorage.getItem(key) } catch (e) { return null }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 写 localStorage(不可用安全)。
|
|
30
|
+
* @author ddj 2026年09月03号
|
|
31
|
+
* @param key 键
|
|
32
|
+
* @param value 值
|
|
33
|
+
*/
|
|
34
|
+
function writeLocal(key, value) {
|
|
35
|
+
try { localStorage.setItem(key, value) } catch (e) { /* 忽略 */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 类型徽标(总是=绿 / 自动=蓝 / 手动=灰)。
|
|
40
|
+
* @author ddj 2026年09月03号
|
|
41
|
+
* @param type 规则类型
|
|
42
|
+
* @returns 徽标元素
|
|
43
|
+
*/
|
|
44
|
+
function typeBadge(type) {
|
|
45
|
+
return React.createElement('span', { className: 'edrv-rules-type edrv-rules-type-' + type }, TYPE_LABEL[type] ?? type)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --region 列表行
|
|
49
|
+
/**
|
|
50
|
+
* 渲染一条规则行:文件名 + 相对提示 + 类型徽标,第二行描述;右侧编辑/删除/开关。
|
|
51
|
+
* @author ddj 2026年09月03号
|
|
52
|
+
* @param props.rule 规则元信息
|
|
53
|
+
* @param props.onEdit 编辑回调
|
|
54
|
+
* @param props.onRemove 删除回调
|
|
55
|
+
* @param props.onToggle 开关回调
|
|
56
|
+
* @returns 行元素
|
|
57
|
+
*/
|
|
58
|
+
function RuleRow(props) {
|
|
59
|
+
const rule = props.rule
|
|
60
|
+
const desc = [rule.error ? '解析失败:' + rule.error : null, rule.description || (rule.type === 'auto' ? 'globs: ' + rule.globs.join(', ') : '')]
|
|
61
|
+
.filter(Boolean).join(' · ')
|
|
62
|
+
return React.createElement('div', { className: 'edrv-rules-row' + (rule.enabled ? '' : ' off') },
|
|
63
|
+
React.createElement('div', { className: 'edrv-rules-main', onClick: () => props.onEdit(rule), title: rule.absPath },
|
|
64
|
+
React.createElement('span', { className: 'edrv-rules-file' }, rule.file),
|
|
65
|
+
React.createElement('span', { className: 'edrv-rules-path' }, '[' + rule.relHint + rule.file + ']'),
|
|
66
|
+
typeBadge(rule.type),
|
|
67
|
+
!rule.enabled ? React.createElement('span', { className: 'edrv-rules-offtag' }, '已停用') : null),
|
|
68
|
+
React.createElement('div', { className: 'edrv-rules-desc', title: desc }, desc ? '描述: ' + desc : '描述: —'),
|
|
69
|
+
React.createElement('div', { className: 'edrv-rules-actions' },
|
|
70
|
+
React.createElement('button', { className: 'edrv-rules-act', title: '编辑', onClick: () => props.onEdit(rule) }, '✏️'),
|
|
71
|
+
React.createElement('button', { className: 'edrv-rules-act', title: '删除', onClick: () => props.onRemove(rule) }, '🗑'),
|
|
72
|
+
React.createElement('button', {
|
|
73
|
+
className: 'edrv-rules-switch' + (rule.enabled ? ' on' : ''), role: 'switch', 'aria-checked': rule.enabled,
|
|
74
|
+
title: rule.enabled ? '已启用(点击停用)' : '已停用(点击启用)',
|
|
75
|
+
onClick: () => props.onToggle(rule),
|
|
76
|
+
}, React.createElement('span', { className: 'edrv-rules-knob' }))))
|
|
77
|
+
}
|
|
78
|
+
// --endregion
|
|
79
|
+
|
|
80
|
+
// --region 内嵌编辑器
|
|
81
|
+
/**
|
|
82
|
+
* 新建/编辑表单:文件名(仅新建可改)+ 原始 .mdc 文本域 + 保存/取消。
|
|
83
|
+
* @author ddj 2026年09月03号
|
|
84
|
+
* @param props.form 表单状态(mode/scope/workspacePath/file/content/error/saving)
|
|
85
|
+
* @param props.setForm 表单更新函数
|
|
86
|
+
* @param props.onSave 保存回调
|
|
87
|
+
* @param props.onCancel 取消回调
|
|
88
|
+
* @returns 表单元素
|
|
89
|
+
*/
|
|
90
|
+
function RuleEditor(props) {
|
|
91
|
+
const form = props.form
|
|
92
|
+
const isNew = form.mode === 'new'
|
|
93
|
+
return React.createElement('div', { className: 'edrv-rules-editor' },
|
|
94
|
+
React.createElement('div', { className: 'edrv-rules-editor-head' },
|
|
95
|
+
isNew ? '新建' + (form.scope === 'project' ? '项目' : '用户') + '规则' : '编辑:' + form.file,
|
|
96
|
+
React.createElement('button', { className: 'edrv-rules-act', title: '取消', onClick: props.onCancel }, '✕')),
|
|
97
|
+
React.createElement('div', { className: 'edrv-rules-editor-file' },
|
|
98
|
+
React.createElement('label', null, '文件名'),
|
|
99
|
+
React.createElement('input', {
|
|
100
|
+
className: 'edrv-rules-input', value: form.file, disabled: !isNew, spellCheck: false,
|
|
101
|
+
placeholder: 'my-rule.mdc', onChange: (e) => props.setForm({ ...form, file: e.target.value }),
|
|
102
|
+
})),
|
|
103
|
+
React.createElement('textarea', {
|
|
104
|
+
className: 'edrv-rules-body', spellCheck: false,
|
|
105
|
+
value: form.content,
|
|
106
|
+
onChange: (e) => props.setForm({ ...form, content: e.target.value }),
|
|
107
|
+
}),
|
|
108
|
+
React.createElement('div', { className: 'edrv-rules-editor-hint' },
|
|
109
|
+
'frontmatter 支持 description / alwaysApply / globs / enabled;无 frontmatter 的纯 markdown 视为「手动」规则'),
|
|
110
|
+
form.error ? React.createElement('div', { className: 'edrv-rules-err' }, form.error) : null,
|
|
111
|
+
React.createElement('div', { className: 'edrv-rules-editor-foot' },
|
|
112
|
+
React.createElement('button', { className: 'edrv-rules-save', disabled: form.saving, onClick: props.onSave }, form.saving ? '保存中…' : '保存')))
|
|
113
|
+
}
|
|
114
|
+
// --endregion
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 规则面板主体。
|
|
118
|
+
* @param props.ctx 面板共享上下文(notify 可选)
|
|
119
|
+
*/
|
|
120
|
+
export function RulesPanel(props) {
|
|
121
|
+
const ctx = props?.ctx
|
|
122
|
+
const notify = ctx?.notify ?? ((message) => {})
|
|
123
|
+
// 当前会话工作区(EditorView 从 sessions 快照注入;项目 Tab 自动匹配,不提供手动切换)
|
|
124
|
+
const cwd = ctx?.cwd ?? null
|
|
125
|
+
const [tab, setTab] = React.useState(() => (readLocal(CACHE_KEY.rules + 'tab') === 'project' ? 'project' : 'user'))
|
|
126
|
+
const [data, setData] = React.useState(null) // { user: RuleInfo[], projects: RuleProject[] }
|
|
127
|
+
const [error, setError] = React.useState('')
|
|
128
|
+
const [form, setForm] = React.useState(null) // 编辑器表单状态;null=列表态
|
|
129
|
+
const mountedRef = React.useRef(true)
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 当前工作区对应的项目聚合(按 ctx.cwd 自动匹配;无会话/未注册返回 null)。
|
|
133
|
+
* @returns RuleProject 或 null
|
|
134
|
+
*/
|
|
135
|
+
const activeProject = () => {
|
|
136
|
+
if (!cwd) return null
|
|
137
|
+
return (data?.projects ?? []).find((p) => p.workspacePath === cwd) ?? null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 拉取规则总列表(rules.list)。 */
|
|
141
|
+
const load = () => {
|
|
142
|
+
rpc('rules.list', {}).then((res) => {
|
|
143
|
+
if (!mountedRef.current) return
|
|
144
|
+
if (res && res.ok) { setData(res); setError('') } else setError(res?.error ?? '读取规则失败')
|
|
145
|
+
}).catch((e) => { if (mountedRef.current) setError('读取规则异常: ' + String(e)) })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
mountedRef.current = true
|
|
150
|
+
load()
|
|
151
|
+
return () => { mountedRef.current = false }
|
|
152
|
+
}, [])
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 切换 Tab(记忆到 localStorage)。
|
|
156
|
+
* @param next 目标 Tab
|
|
157
|
+
*/
|
|
158
|
+
const switchTab = (next) => {
|
|
159
|
+
setTab(next)
|
|
160
|
+
writeLocal(CACHE_KEY.rules + 'tab', next)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 打开新建表单(按当前 Tab 决定作用域;项目规则跟随当前会话工作区)。
|
|
165
|
+
*/
|
|
166
|
+
const openNew = () => {
|
|
167
|
+
const scope = tab === 'project' ? 'project' : 'user'
|
|
168
|
+
if (scope === 'project' && !cwd) { notify('当前会话没有工作区,无法新建项目规则'); return }
|
|
169
|
+
setForm({ mode: 'new', scope, workspacePath: scope === 'project' ? cwd : undefined, file: '', content: NEW_TEMPLATE, error: '', saving: false })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 打开编辑表单:先拉全文再进入编辑态。
|
|
174
|
+
* @param rule 目标规则
|
|
175
|
+
*/
|
|
176
|
+
const openEdit = (rule) => {
|
|
177
|
+
rpc('rules.read', { scope: rule.scope, workspacePath: rule.scope === 'project' ? rule.workspacePath : undefined, file: rule.file })
|
|
178
|
+
.then((res) => {
|
|
179
|
+
if (res && res.ok) {
|
|
180
|
+
setForm({ mode: 'edit', scope: rule.scope, workspacePath: rule.workspacePath, file: rule.file, content: res.content, error: '', saving: false })
|
|
181
|
+
} else notify(res?.error ?? '读取规则失败')
|
|
182
|
+
}).catch((e) => notify('读取规则异常: ' + String(e)))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 保存表单(rules.save;文件名仅新建校验非空)。 */
|
|
186
|
+
const saveForm = () => {
|
|
187
|
+
const next = { ...form, saving: true, error: '' }
|
|
188
|
+
const file = String(form.file ?? '').trim()
|
|
189
|
+
if (!file) { setForm({ ...next, saving: false, error: '文件名不能为空' }); return }
|
|
190
|
+
setForm(next)
|
|
191
|
+
rpc('rules.save', { scope: form.scope, workspacePath: form.workspacePath, file, content: form.content })
|
|
192
|
+
.then((res) => {
|
|
193
|
+
if (res && res.ok) {
|
|
194
|
+
setForm(null)
|
|
195
|
+
notify('规则已保存:' + file)
|
|
196
|
+
load()
|
|
197
|
+
} else setForm((prev) => ({ ...prev, saving: false, error: res?.error ?? '保存失败' }))
|
|
198
|
+
}).catch((e) => setForm((prev) => ({ ...prev, saving: false, error: '保存异常: ' + String(e) })))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 删除规则(confirm 后走 rules.remove)。
|
|
203
|
+
* @param rule 目标规则
|
|
204
|
+
*/
|
|
205
|
+
const removeRule = (rule) => {
|
|
206
|
+
if (!window.confirm('删除规则 ' + rule.file + '?(不可恢复)')) return
|
|
207
|
+
rpc('rules.remove', { scope: rule.scope, workspacePath: rule.workspacePath, file: rule.file })
|
|
208
|
+
.then((res) => {
|
|
209
|
+
if (res && res.ok) { notify('已删除:' + rule.file); load() } else notify(res?.error ?? '删除失败')
|
|
210
|
+
}).catch((e) => notify('删除异常: ' + String(e)))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 切换启用开关:本地乐观翻转(按 workspacePath+file 定位,失败回滚)。
|
|
215
|
+
* @param rule 目标规则(project 已注入 workspacePath)
|
|
216
|
+
*/
|
|
217
|
+
const toggleRule = (rule) => {
|
|
218
|
+
const enabled = !rule.enabled
|
|
219
|
+
const applyLocal = (on) => setData((prev) => prev ? {
|
|
220
|
+
...prev,
|
|
221
|
+
user: prev.user.map((r) => (r.scope === 'user' && r.file === rule.file ? { ...r, enabled: on } : r)),
|
|
222
|
+
projects: prev.projects.map((p) => (p.workspacePath === rule.workspacePath
|
|
223
|
+
? { ...p, rules: p.rules.map((r) => (r.file === rule.file ? { ...r, enabled: on } : r)) }
|
|
224
|
+
: p)),
|
|
225
|
+
} : prev)
|
|
226
|
+
applyLocal(enabled)
|
|
227
|
+
rpc('rules.toggle', { scope: rule.scope, workspacePath: rule.workspacePath, file: rule.file, enabled })
|
|
228
|
+
.then((res) => {
|
|
229
|
+
if (res && res.ok) { applyLocal(res.rule?.enabled === true); notify(res.rule?.enabled === false ? '已停用:' + rule.file : '已启用:' + rule.file) }
|
|
230
|
+
else { applyLocal(!enabled); notify(res?.error ?? '切换失败') }
|
|
231
|
+
}).catch((e) => { applyLocal(!enabled); notify('切换异常: ' + String(e)) })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 当前列表数据(按 Tab 取用户规则或当前工作区的项目规则)。 */
|
|
235
|
+
const rowsOf = () => {
|
|
236
|
+
if (!data) return []
|
|
237
|
+
if (tab === 'user') return data.user
|
|
238
|
+
return activeProject()?.rules ?? []
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const renderTabs = () => React.createElement('div', { className: 'edrv-rules-tabs' },
|
|
242
|
+
React.createElement('button', { className: 'edrv-rules-tab' + (tab === 'user' ? ' on' : ''), onClick: () => switchTab('user') }, '用户规则'),
|
|
243
|
+
React.createElement('button', { className: 'edrv-rules-tab' + (tab === 'project' ? ' on' : ''), onClick: () => switchTab('project') }, '项目规则'),
|
|
244
|
+
React.createElement('button', { className: 'edrv-rules-new', title: '新建规则', onClick: openNew }, '+ 新建规则'))
|
|
245
|
+
|
|
246
|
+
/** 项目 Tab 的工作区展示(不可编辑,自动跟随当前会话)。 */
|
|
247
|
+
const renderWsLine = () => {
|
|
248
|
+
if (tab !== 'project') return null
|
|
249
|
+
return React.createElement('div', { className: 'edrv-rules-ws-line', title: cwd ?? '' },
|
|
250
|
+
'工作区:' + (cwd ?? '当前会话未打开工作区'))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const renderEmpty = () => {
|
|
254
|
+
if (tab === 'project') {
|
|
255
|
+
if (!cwd) return '当前会话没有工作区,项目规则不可用(项目规则跟随会话所在工作区)'
|
|
256
|
+
if (!data?.projects?.length) return '当前 DSH 没有已注册的工作区;在目标工作区打开一次会话后再来管理'
|
|
257
|
+
if (!activeProject()) return '当前工作区未注册为 DSH workspace,暂不能管理其项目规则'
|
|
258
|
+
return '<工作区>/.dsh/rules/ 还没有规则,点击「+ 新建规则」创建'
|
|
259
|
+
}
|
|
260
|
+
return '还没有用户规则(~/.dsh/rules/),点击「+ 新建规则」创建'
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const renderBody = () => {
|
|
264
|
+
if (form) return React.createElement(RuleEditor, { form, setForm, onSave: saveForm, onCancel: () => setForm(null) })
|
|
265
|
+
if (!data) return React.createElement('div', { className: 'edrv-rules-empty' }, '加载中…')
|
|
266
|
+
if (error) return React.createElement('div', { className: 'edrv-rules-err' }, error)
|
|
267
|
+
const rows = rowsOf()
|
|
268
|
+
if (!rows.length) return React.createElement('div', { className: 'edrv-rules-empty' }, renderEmpty())
|
|
269
|
+
const common = {
|
|
270
|
+
onEdit: (rule) => openEdit({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
271
|
+
onRemove: (rule) => removeRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
272
|
+
onToggle: (rule) => toggleRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
273
|
+
}
|
|
274
|
+
return React.createElement('div', { className: 'edrv-rules-list' },
|
|
275
|
+
rows.map((rule) => React.createElement(RuleRow, { key: rule.scope + ':' + rule.file, rule, ...common })))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return React.createElement('div', { className: 'edrv-rules-panel' },
|
|
279
|
+
renderTabs(),
|
|
280
|
+
renderWsLine(),
|
|
281
|
+
React.createElement('div', { className: 'edrv-rules-hint' },
|
|
282
|
+
tab === 'user' ? '用户规则对所有会话生效(存放于 ~/.dsh/rules/)' : '项目规则仅对所在工作区的会话生效(存放于 <工作区>/.dsh/rules/)'),
|
|
283
|
+
renderBody())
|
|
284
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* dsh-vscode-mode client — 侧边栏面板定义(文件管理 +
|
|
4
|
-
* 作者 ddj 2026-08-26 / 2026-
|
|
3
|
+
* dsh-vscode-mode client — 侧边栏面板定义(文件管理 + 搜索 + 规则)。
|
|
4
|
+
* 作者 ddj 2026-08-26 / 2026-09-03
|
|
5
5
|
*/
|
|
6
6
|
import React from 'react'
|
|
7
7
|
import { FileExplorer } from './FileExplorer.js'
|
|
8
8
|
import { SearchPanel } from './SearchPanel.js'
|
|
9
|
+
import { RulesPanel } from './RulesPanel.js'
|
|
9
10
|
import type { SidebarPanelDef, SidebarCtx } from '../types.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -38,3 +39,18 @@ export function createSearchPanel(): SidebarPanelDef {
|
|
|
38
39
|
render: (ctx: SidebarCtx) => React.createElement(SearchPanel, { ctx }),
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 构造规则管理面板定义(Codebuddy 规则形态:用户规则/项目规则双 Tab + 开关)。
|
|
45
|
+
* @author ddj 2026年09月03号
|
|
46
|
+
* @returns 面板定义(无徽标;活动栏图标 📏,与大纲 📜 区分)
|
|
47
|
+
*/
|
|
48
|
+
export function createRulesPanel(): SidebarPanelDef {
|
|
49
|
+
return {
|
|
50
|
+
id: 'rules',
|
|
51
|
+
title: '规则',
|
|
52
|
+
icon: '📏',
|
|
53
|
+
order: 20,
|
|
54
|
+
render: (ctx: SidebarCtx) => React.createElement(RulesPanel, { ctx }),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -10,6 +10,8 @@ import type { TreeMenuRegistry } from './contextMenu.js'
|
|
|
10
10
|
/** 面板可用的共享上下文(由 SidebarView 从 EditorView 注入,面板不直碰其内部)。 */
|
|
11
11
|
export interface SidebarCtx {
|
|
12
12
|
sessionId?: string
|
|
13
|
+
/** 当前会话工作区 cwd(来自 sessions 快照;规则面板项目 Tab 自动匹配用)。 */
|
|
14
|
+
cwd?: string | null
|
|
13
15
|
openFile: (path: string) => void
|
|
14
16
|
/** 打开文件并跳转到指定行/列(搜索面板命中跳转;缺省仅打开)。 */
|
|
15
17
|
openFileAt?: (path: string, line?: number, column?: number) => void
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 侧边栏最小宽度共享状态。
|
|
3
|
+
* 设置来源:dsh-vscode-mode 命名空间的 sidebarMinWidth 字段(通用设置页可调);
|
|
4
|
+
* 客户端 settings 订阅经 sidebarMinApply 写入,编辑器/侧边栏容器经 getSidebarMinWidth 读取,
|
|
5
|
+
* 变更时由 client/index.ts 派发 edrv:sidebar-min-width 窗口事件通知编辑器重夹。
|
|
6
|
+
* 作者 ddj 2026年09月04号
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 默认最小宽度(设置缺失/非法时的兜底值)。 */
|
|
10
|
+
export const SIDEBAR_MIN_DEFAULT = 300
|
|
11
|
+
/** 最小宽度允许下界(与旧版拖拽下限一致)。 */
|
|
12
|
+
export const SIDEBAR_MIN_FLOOR = 180
|
|
13
|
+
/** 最小宽度允许上界(与侧边栏最大宽度 W_MAX 一致,避免区间倒挂)。 */
|
|
14
|
+
export const SIDEBAR_MIN_CEIL = 560
|
|
15
|
+
|
|
16
|
+
let currentMin = SIDEBAR_MIN_DEFAULT
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 归一化最小宽度:非法(NaN/非有限/负数)回退默认,越界夹取到 [180, 560]。
|
|
20
|
+
* @author ddj 2026年09月04号
|
|
21
|
+
* @param value 原始值(设置文档来的任意 JSON 值)
|
|
22
|
+
* @returns 合法最小宽度
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeSidebarMinWidth(value: unknown): number {
|
|
25
|
+
const n = typeof value === 'number' ? value : Number(value)
|
|
26
|
+
if (!Number.isFinite(n) || n <= 0) return SIDEBAR_MIN_DEFAULT
|
|
27
|
+
return Math.max(SIDEBAR_MIN_FLOOR, Math.min(SIDEBAR_MIN_CEIL, Math.round(n)))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 应用设置值(client/index.ts 设置订阅同步调用)。
|
|
32
|
+
* @author ddj 2026年09月04号
|
|
33
|
+
* @param value 设置文档中的 sidebarMinWidth
|
|
34
|
+
* @returns 归一化后的生效值
|
|
35
|
+
*/
|
|
36
|
+
export function sidebarMinApply(value: unknown): number {
|
|
37
|
+
currentMin = normalizeSidebarMinWidth(value)
|
|
38
|
+
return currentMin
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 读取当前生效的最小宽度(编辑器初始/恢复/拖拽夹取共用)。
|
|
43
|
+
* @author ddj 2026年09月04号
|
|
44
|
+
* @returns 当前最小宽度
|
|
45
|
+
*/
|
|
46
|
+
export function getSidebarMinWidth(): number {
|
|
47
|
+
return currentMin
|
|
48
|
+
}
|