dsh-vscode-mode 0.1.47 → 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 +729 -205
- 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/addToConversation.ts +32 -19
- 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/menuItems.ts +23 -2
- package/src/client/sidebar/panels/FileExplorer.ts +2 -4
- package/src/client/sidebar/panels/RulesPanel.ts +284 -0
- package/src/client/sidebar/panels/index.ts +18 -2
- package/src/client/sidebar/types.ts +5 -0
- package/src/client/sidebarMin.ts +48 -0
- package/src/client/styles/editor.css +43 -0
- package/src/client/ui/EditorView.ts +25 -13
- package/src/client/ui/McpSettings.ts +37 -2
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/client/ui/menuPosition.ts +2 -28
- 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
|
@@ -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
|
+
}
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* 作者 ddj 2026-08-26
|
|
5
5
|
*/
|
|
6
6
|
import type { OutlineSourceRegistry } from '../outline/types.js'
|
|
7
|
+
import type { AddToConversation } from '../addToConversation.js'
|
|
7
8
|
import type { TreeMenuRegistry } from './contextMenu.js'
|
|
8
9
|
|
|
9
10
|
/** 面板可用的共享上下文(由 SidebarView 从 EditorView 注入,面板不直碰其内部)。 */
|
|
10
11
|
export interface SidebarCtx {
|
|
11
12
|
sessionId?: string
|
|
13
|
+
/** 当前会话工作区 cwd(来自 sessions 快照;规则面板项目 Tab 自动匹配用)。 */
|
|
14
|
+
cwd?: string | null
|
|
12
15
|
openFile: (path: string) => void
|
|
13
16
|
/** 打开文件并跳转到指定行/列(搜索面板命中跳转;缺省仅打开)。 */
|
|
14
17
|
openFileAt?: (path: string, line?: number, column?: number) => void
|
|
@@ -25,6 +28,8 @@ export interface SidebarCtx {
|
|
|
25
28
|
outlineSources?: OutlineSourceRegistry
|
|
26
29
|
/** 文件右键菜单项注册表(公开 provide 为 edrvFileContextMenuItems;文件管理面板构建菜单用)。 */
|
|
27
30
|
fileMenuItems?: TreeMenuRegistry
|
|
31
|
+
/** 「添加到对话」动作集(文件/文件夹引用注入对话输入框;缺省时菜单项降级提示)。 */
|
|
32
|
+
addToConversation?: AddToConversation
|
|
28
33
|
/** 面板动作反馈(如右键菜单操作结果 → 编辑区路径栏状态)。 */
|
|
29
34
|
notify?: (message: string) => void
|
|
30
35
|
}
|
|
@@ -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
|
+
}
|
|
@@ -295,5 +295,48 @@
|
|
|
295
295
|
[data-edrv-view] .edrv-search-toggle.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); border-color: var(--dsw-alias-brand-primary, #0f9d58); }
|
|
296
296
|
[data-edrv-view] .edrv-search-toggle:disabled { opacity: .4; cursor: default; }
|
|
297
297
|
|
|
298
|
+
/* ===== rules panel (sidebar, Codebuddy rules view) ===== */
|
|
299
|
+
[data-edrv-view] .edrv-rules-panel { display: flex; flex-direction: column; min-height: 0; flex: 1; padding: 6px; gap: 4px; }
|
|
300
|
+
[data-edrv-view] .edrv-rules-tabs { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
|
|
301
|
+
[data-edrv-view] .edrv-rules-tab { height: 24px; padding: 0 10px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, #52606d); font-size: 12px; cursor: pointer; }
|
|
302
|
+
[data-edrv-view] .edrv-rules-tab:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
303
|
+
[data-edrv-view] .edrv-rules-tab.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); font-weight: 600; }
|
|
304
|
+
[data-edrv-view] .edrv-rules-new { margin-left: auto; height: 24px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #cbd2d9); border-radius: 6px; background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #1f2933); font-size: 11px; cursor: pointer; flex-shrink: 0; }
|
|
305
|
+
[data-edrv-view] .edrv-rules-new:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
306
|
+
[data-edrv-view] .edrv-rules-ws-line { font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); padding: 0 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 0 auto; user-select: none; }
|
|
307
|
+
[data-edrv-view] .edrv-rules-hint { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); padding: 0 2px; flex: 0 0 auto; }
|
|
308
|
+
[data-edrv-view] .edrv-rules-list { flex: 1; min-height: 0; overflow: auto; }
|
|
309
|
+
[data-edrv-view] .edrv-rules-row { position: relative; padding: 7px 100px 6px 4px; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); }
|
|
310
|
+
[data-edrv-view] .edrv-rules-row.off .edrv-rules-file, [data-edrv-view] .edrv-rules-row.off .edrv-rules-desc { opacity: .5; }
|
|
311
|
+
[data-edrv-view] .edrv-rules-main { display: flex; align-items: center; gap: 6px; min-width: 0; cursor: pointer; }
|
|
312
|
+
[data-edrv-view] .edrv-rules-file { font-weight: 600; font-size: 12px; color: var(--dsw-alias-label-primary, #1f2933); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
313
|
+
[data-edrv-view] .edrv-rules-path { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
314
|
+
[data-edrv-view] .edrv-rules-type { flex-shrink: 0; height: 15px; padding: 0 5px; border-radius: 999px; font-size: 10px; line-height: 15px; }
|
|
315
|
+
[data-edrv-view] .edrv-rules-type-always { background: rgba(15,157,88,.15); color: #0b7c46; }
|
|
316
|
+
[data-edrv-view] .edrv-rules-type-auto { background: rgba(79,140,255,.16); color: #3367c9; }
|
|
317
|
+
[data-edrv-view] .edrv-rules-type-manual { background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); }
|
|
318
|
+
[data-edrv-view] .edrv-rules-offtag { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); flex-shrink: 0; }
|
|
319
|
+
[data-edrv-view] .edrv-rules-desc { margin-top: 3px; font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
320
|
+
[data-edrv-view] .edrv-rules-actions { position: absolute; top: 5px; right: 4px; display: flex; align-items: center; gap: 2px; }
|
|
321
|
+
[data-edrv-view] .edrv-rules-act { min-width: 22px; height: 22px; padding: 0 4px; border: none; border-radius: 5px; background: transparent; font-size: 12px; line-height: 1; cursor: pointer; color: var(--dsw-alias-label-secondary, #52606d); }
|
|
322
|
+
[data-edrv-view] .edrv-rules-act:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
323
|
+
[data-edrv-view] .edrv-rules-switch { position: relative; width: 30px; height: 16px; border-radius: 999px; border: none; background: #b9c4cc; cursor: pointer; transition: background .15s; flex-shrink: 0; padding: 0; }
|
|
324
|
+
[data-edrv-view] .edrv-rules-switch.on { background: #7c5cff; }
|
|
325
|
+
[data-edrv-view] .edrv-rules-knob { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: #fff; transition: left .15s; }
|
|
326
|
+
[data-edrv-view] .edrv-rules-switch.on .edrv-rules-knob { left: 16px; }
|
|
327
|
+
[data-edrv-view] .edrv-rules-empty { padding: 10px 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); line-height: 1.6; }
|
|
328
|
+
[data-edrv-view] .edrv-rules-err { padding: 6px 8px; border: 1px solid rgba(217,83,79,.4); border-radius: 6px; background: #fdeaea; color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; word-break: break-all; }
|
|
329
|
+
[data-edrv-view] .edrv-rules-editor { display: flex; flex-direction: column; min-height: 0; flex: 1; gap: 6px; }
|
|
330
|
+
[data-edrv-view] .edrv-rules-editor-head { display: flex; align-items: center; justify-content: space-between; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #1f2933); flex: 0 0 auto; }
|
|
331
|
+
[data-edrv-view] .edrv-rules-editor-file { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); flex: 0 0 auto; }
|
|
332
|
+
[data-edrv-view] .edrv-rules-input { flex: 1; min-width: 0; height: 24px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 6px; background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); font-size: 11px; outline: none; font-family: var(--ds-font-family-code, ui-monospace, monospace); }
|
|
333
|
+
[data-edrv-view] .edrv-rules-input:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
334
|
+
[data-edrv-view] .edrv-rules-body { flex: 1; min-height: 0; resize: none; padding: 8px; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 6px; background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); font-size: 12px; line-height: 1.5; outline: none; font-family: var(--ds-font-family-code, ui-monospace, monospace); }
|
|
335
|
+
[data-edrv-view] .edrv-rules-body:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
336
|
+
[data-edrv-view] .edrv-rules-editor-hint { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); line-height: 1.5; flex: 0 0 auto; }
|
|
337
|
+
[data-edrv-view] .edrv-rules-editor-foot { display: flex; justify-content: flex-end; flex: 0 0 auto; }
|
|
338
|
+
[data-edrv-view] .edrv-rules-save { height: 24px; padding: 0 14px; border: none; border-radius: 6px; background: var(--dsw-alias-brand-primary, #0f9d58); color: #fff; font-size: 12px; cursor: pointer; }
|
|
339
|
+
[data-edrv-view] .edrv-rules-save:disabled { opacity: .5; cursor: default; }
|
|
340
|
+
|
|
298
341
|
|
|
299
342
|
|
|
@@ -23,7 +23,9 @@ import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
|
|
|
23
23
|
import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
|
|
24
24
|
import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewStateCache.js'
|
|
25
25
|
import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
|
|
26
|
+
import { getSidebarMinWidth } from '../sidebarMin.js'
|
|
26
27
|
import { createNavHistory } from '../navHistory.js'
|
|
28
|
+
import { statusOfAdd } from '../addToConversation.js'
|
|
27
29
|
import { CACHE_KEY } from '../paths.js'
|
|
28
30
|
import { bindLspEditor, runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
|
|
29
31
|
import { bindLspUnderline } from '../monaco/lsp/underline.js'
|
|
@@ -66,7 +68,7 @@ export function EditorView(props) {
|
|
|
66
68
|
const [fileIdx, setFileIdx] = React.useState(0) // 全局差异文件位置(x/x 文件 显示)
|
|
67
69
|
const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
|
|
68
70
|
const [sidebarOn, setSidebarOn] = React.useState(layout !== 'side') // 侧边栏显隐(侧栏形态默认收起)
|
|
69
|
-
const [sidebarW, setSidebarW] = React.useState(
|
|
71
|
+
const [sidebarW, setSidebarW] = React.useState(() => getSidebarMinWidth()) // 侧边栏宽度(初值 = 最小宽度,默认 300)
|
|
70
72
|
const [activePanel, setActivePanel] = React.useState('explorer') // 激活面板 id
|
|
71
73
|
const [focusRequest, setFocusRequest] = React.useState(0) // 外部差异聚焦请求版本
|
|
72
74
|
const kbVersion = useKeybindingsVersion() // 快捷键配置版本(tooltip 文案随键位刷新)
|
|
@@ -507,6 +509,7 @@ export function EditorView(props) {
|
|
|
507
509
|
}, [tabs, active, sessionId])
|
|
508
510
|
|
|
509
511
|
// 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
|
|
512
|
+
// 宽度下限来自通用设置 sidebarMinWidth(默认 300),恢复值按其重夹
|
|
510
513
|
const sidebarKey = CACHE_KEY.sidebar + (layout === 'side' ? 'side.' : '') + String(sessionId)
|
|
511
514
|
React.useEffect(() => {
|
|
512
515
|
if (!sessionId) return
|
|
@@ -515,12 +518,23 @@ export function EditorView(props) {
|
|
|
515
518
|
if (raw) {
|
|
516
519
|
const saved = JSON.parse(raw)
|
|
517
520
|
if (typeof saved.on === 'boolean') setSidebarOn(saved.on)
|
|
518
|
-
if (typeof saved.width === 'number') setSidebarW(Math.max(
|
|
521
|
+
if (typeof saved.width === 'number') setSidebarW(Math.max(getSidebarMinWidth(), Math.min(560, saved.width)))
|
|
519
522
|
if (typeof saved.panel === 'string') setActivePanel(saved.panel)
|
|
520
523
|
}
|
|
521
524
|
} catch (e) { /* 损坏忽略 */ }
|
|
522
525
|
}, [sessionId, sidebarKey])
|
|
523
526
|
|
|
527
|
+
// 侧边栏最小宽度设置变更(edrv:sidebar-min-width)→ 更新 minW 并重夹当前宽度
|
|
528
|
+
const [sidebarMinW, setSidebarMinW] = React.useState(() => getSidebarMinWidth())
|
|
529
|
+
React.useEffect(() => {
|
|
530
|
+
const onMinChange = () => {
|
|
531
|
+
setSidebarMinW(getSidebarMinWidth())
|
|
532
|
+
setSidebarW((w) => Math.max(getSidebarMinWidth(), w))
|
|
533
|
+
}
|
|
534
|
+
window.addEventListener('edrv:sidebar-min-width', onMinChange)
|
|
535
|
+
return () => window.removeEventListener('edrv:sidebar-min-width', onMinChange)
|
|
536
|
+
}, [])
|
|
537
|
+
|
|
524
538
|
// 侧边栏状态持久化
|
|
525
539
|
React.useEffect(() => {
|
|
526
540
|
if (!sessionId) return
|
|
@@ -1119,15 +1133,7 @@ export function EditorView(props) {
|
|
|
1119
1133
|
setTabMenu(null)
|
|
1120
1134
|
}
|
|
1121
1135
|
|
|
1122
|
-
/**
|
|
1123
|
-
* @author ddj 2026年08月25号 */
|
|
1124
|
-
const statusOfAdd = (outcome, okText) => {
|
|
1125
|
-
if (outcome === 'ok') return okText
|
|
1126
|
-
if (outcome === 'busy') return okText + '(输入框忙,已降级纯文本)'
|
|
1127
|
-
return '无法添加到对话(无会话或输入框不可用)'
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
/** 添加文件/选中区引用到对话(异步,状态栏反馈)。 */
|
|
1136
|
+
/** 添加文件/选中区引用到对话(异步,状态栏反馈;状态文案走共享 statusOfAdd)。 */
|
|
1131
1137
|
const addRefToChat = (path, range) => {
|
|
1132
1138
|
if (!path) { setStatus('无活动文件'); return }
|
|
1133
1139
|
if (!addToConversation) { setStatus('添加到对话不可用'); return }
|
|
@@ -1251,7 +1257,7 @@ export function EditorView(props) {
|
|
|
1251
1257
|
'aria-label': '前进',
|
|
1252
1258
|
onClick: navForward,
|
|
1253
1259
|
}, '→'),
|
|
1254
|
-
React.createElement('button', { className: 'edrv-chip-btn' + (sidebarOn ? ' edrv-chip-on' : ''), title: '
|
|
1260
|
+
React.createElement('button', { className: 'edrv-chip-btn' + (sidebarOn ? ' edrv-chip-on' : ''), title: '切换面板区 (' + (chordOf('edrv.toggleSidebar') ?? 'Ctrl+B') + '),图标列常显', onClick: () => setSidebarOn((v) => !v) }, '☰'),
|
|
1255
1261
|
React.createElement('button', { className: 'edrv-chip-btn', title: '刷新', onClick: reloadFile }, '⟳'),
|
|
1256
1262
|
React.createElement(QuickOpen, { sessionId, onOpen: (p) => openFile(p, false) }))
|
|
1257
1263
|
|
|
@@ -1397,6 +1403,8 @@ export function EditorView(props) {
|
|
|
1397
1403
|
const sidebarPanels = props.sidebarPanels
|
|
1398
1404
|
const sidebarCtx = {
|
|
1399
1405
|
sessionId,
|
|
1406
|
+
// 当前会话工作区(sessions 快照 byId[sessionId].cwd;规则面板项目 Tab 自动匹配)
|
|
1407
|
+
cwd: props.sessions?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd ?? null,
|
|
1400
1408
|
openFile: (p) => openFile(p, false),
|
|
1401
1409
|
openFileAt,
|
|
1402
1410
|
activePath: active,
|
|
@@ -1406,6 +1414,7 @@ export function EditorView(props) {
|
|
|
1406
1414
|
editor: () => editorRef.current,
|
|
1407
1415
|
outlineSources: props.outlineSources,
|
|
1408
1416
|
fileMenuItems: props.fileMenuItems,
|
|
1417
|
+
addToConversation,
|
|
1409
1418
|
notify: (message) => setStatus(message),
|
|
1410
1419
|
}
|
|
1411
1420
|
|
|
@@ -1438,15 +1447,18 @@ export function EditorView(props) {
|
|
|
1438
1447
|
// 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
|
|
1439
1448
|
// 侧栏形态由面板容器给高(100%),不做 composer 几何同步。
|
|
1440
1449
|
const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
|
|
1441
|
-
(
|
|
1450
|
+
(sidebarPanels
|
|
1442
1451
|
? React.createElement(SidebarView, {
|
|
1443
1452
|
registry: sidebarPanels,
|
|
1444
1453
|
ctx: sidebarCtx,
|
|
1445
1454
|
activePanel,
|
|
1446
1455
|
onActive: setActivePanel,
|
|
1456
|
+
visible: sidebarOn,
|
|
1457
|
+
onShow: () => setSidebarOn(true),
|
|
1447
1458
|
width: sidebarW,
|
|
1448
1459
|
onWidth: setSidebarW,
|
|
1449
1460
|
onHide: () => setSidebarOn(false),
|
|
1461
|
+
minWidth: sidebarMinW,
|
|
1450
1462
|
})
|
|
1451
1463
|
: null),
|
|
1452
1464
|
mainCol)
|
|
@@ -10,6 +10,7 @@ import type { MpcProject, MpcServer } from '../../shared/mcp.js'
|
|
|
10
10
|
import '../styles/mcp.css'
|
|
11
11
|
import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
|
|
12
12
|
import { SettingsContext } from '../settingsContext.js'
|
|
13
|
+
import { normalizeSidebarMinWidth, SIDEBAR_MIN_DEFAULT } from '../sidebarMin.js'
|
|
13
14
|
import { KeybindingsPanel } from './KeybindingsPanel.js'
|
|
14
15
|
import { LspSettings } from './LspSettings.js'
|
|
15
16
|
import { PerfSettings } from './PerfSettings.js'
|
|
@@ -122,7 +123,7 @@ function ProjectGroup({ project, busy, onAdd, onRefresh, onToggle, onRemove }) {
|
|
|
122
123
|
return React.createElement('section', { className: 'vsm-project' }, head, body)
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
/** 通用设置:选择当前对话文件链接的打开器 +
|
|
126
|
+
/** 通用设置:选择当前对话文件链接的打开器 + 开发形态开关 + 编辑器布局(侧边栏最小宽度)。 */
|
|
126
127
|
function GeneralSettings({ registry }) {
|
|
127
128
|
const [tool, setTool] = React.useState(AUTO_OPEN_TOOL)
|
|
128
129
|
const [busy, setBusy] = React.useState(false)
|
|
@@ -130,6 +131,8 @@ function GeneralSettings({ registry }) {
|
|
|
130
131
|
const [devForm, setDevForm] = React.useState(null)
|
|
131
132
|
const [devBusy, setDevBusy] = React.useState(false)
|
|
132
133
|
const [devMessage, setDevMessage] = React.useState('')
|
|
134
|
+
const [minW, setMinW] = React.useState(SIDEBAR_MIN_DEFAULT)
|
|
135
|
+
const [minDraft, setMinDraft] = React.useState(null) // 输入草稿(null=跟随已保存值;blur/Enter 提交)
|
|
133
136
|
const settings = React.useContext(SettingsContext)
|
|
134
137
|
const snapshot = settings?.getSnapshot?.()
|
|
135
138
|
const loading = !snapshot || snapshot.status === 'loading'
|
|
@@ -137,8 +140,10 @@ function GeneralSettings({ registry }) {
|
|
|
137
140
|
const notReady = snapshot?.status !== 'ready'
|
|
138
141
|
React.useEffect(() => {
|
|
139
142
|
const onChange = () => {
|
|
140
|
-
const
|
|
143
|
+
const snap = settings?.getSnapshot?.()
|
|
144
|
+
const next = snap?.value?.fileOpenTool
|
|
141
145
|
if (typeof next === 'string') setTool(next)
|
|
146
|
+
setMinW(normalizeSidebarMinWidth(snap?.value?.sidebarMinWidth))
|
|
142
147
|
}
|
|
143
148
|
onChange()
|
|
144
149
|
return settings?.subscribe?.(onChange)
|
|
@@ -153,6 +158,19 @@ function GeneralSettings({ registry }) {
|
|
|
153
158
|
if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
|
|
154
159
|
settings.set('fileOpenTool', value).catch((e) => setError(String(e))).finally(() => setBusy(false))
|
|
155
160
|
}
|
|
161
|
+
/** 提交侧边栏最小宽度(归一化夹取 180–560 后持久化)。 */
|
|
162
|
+
const saveMinW = (raw) => {
|
|
163
|
+
const next = normalizeSidebarMinWidth(raw)
|
|
164
|
+
setMinW(next); setBusy(true); setError('')
|
|
165
|
+
if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
|
|
166
|
+
settings.set('sidebarMinWidth', next).catch((e) => setError(String(e))).finally(() => setBusy(false))
|
|
167
|
+
}
|
|
168
|
+
/** 结束输入(blur/Enter)时提交草稿。 */
|
|
169
|
+
const commitMinW = () => {
|
|
170
|
+
if (minDraft === null) return
|
|
171
|
+
setMinDraft(null)
|
|
172
|
+
saveMinW(minDraft)
|
|
173
|
+
}
|
|
156
174
|
const closeDevForm = () => {
|
|
157
175
|
if (!window.confirm('关闭开发形态:插件将切换为正式版安装(版本依赖 + 删除工作区链接),pnpm 装配后需重启 DSH 生效。确认关闭?')) return
|
|
158
176
|
setDevBusy(true)
|
|
@@ -184,6 +202,23 @@ function GeneralSettings({ registry }) {
|
|
|
184
202
|
devFormRow,
|
|
185
203
|
),
|
|
186
204
|
),
|
|
205
|
+
React.createElement('section', { className: 'vsm-panel' },
|
|
206
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '编辑器'),
|
|
207
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
208
|
+
React.createElement('label', { className: 'vsm-general-row' },
|
|
209
|
+
React.createElement('span', null, '侧边栏最小宽度'),
|
|
210
|
+
React.createElement('input', {
|
|
211
|
+
type: 'number', min: 180, max: 560, step: 10,
|
|
212
|
+
value: minDraft ?? minW,
|
|
213
|
+
disabled: loading || unavailable || notReady || busy || snapshot?.writable === false,
|
|
214
|
+
title: '180–560 px;拖拽侧边栏低于该宽度时自动隐藏',
|
|
215
|
+
onChange: (event) => setMinDraft(event.target.value),
|
|
216
|
+
onBlur: commitMinW,
|
|
217
|
+
onKeyDown: (event) => { if (event.key === 'Enter') commitMinW() },
|
|
218
|
+
}),
|
|
219
|
+
React.createElement('small', null, '180–560 px;拖拽低于该宽度自动隐藏')),
|
|
220
|
+
),
|
|
221
|
+
),
|
|
187
222
|
)
|
|
188
223
|
}
|
|
189
224
|
|