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.
@@ -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-08-26
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(240) // 侧边栏宽度
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(180, Math.min(560, saved.width)))
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: '切换侧边栏 (' + (chordOf('edrv.toggleSidebar') ?? 'Ctrl+B') + ')', onClick: () => setSidebarOn((v) => !v) }, '☰'),
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
- (sidebarOn && sidebarPanels
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 next = settings?.getSnapshot?.().value?.fileOpenTool
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
 
@@ -44,5 +44,6 @@ export function SideEditorTab(props) {
44
44
  sidebarPanels: props?.sidebarPanels,
45
45
  outlineSources: props?.outlineSources,
46
46
  fileMenuItems: props?.fileMenuItems,
47
+ sessions: props?.sessions,
47
48
  })
48
49
  }