dsh-vscode-mode 0.1.49 → 0.1.50
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 +6 -3
- package/lib/client.js +362 -34
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/rulesMdc.ts +141 -0
- package/src/client/sidebar/SidebarView.ts +2 -1
- package/src/client/sidebar/panels/RulesPanel.ts +190 -29
- package/src/client/styles/editor.css +20 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 规则 frontmatter 纯函数(解析 + 按类型改写)。
|
|
3
|
+
* 与 host parseRuleMdc/ruleTypeOf 语义对齐的浏览器端实现:编辑器类型下拉与
|
|
4
|
+
* 原始 .mdc 文本域双向同步共用;纯字符串运算,禁 node/react 导入。
|
|
5
|
+
* 作者 ddj 2026年09月07号
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 规则生效类型(与 shared/rules.ts RuleType 一致,此处独立声明避免引入 host 依赖)。 */
|
|
9
|
+
export type RuleFmType = 'always' | 'auto' | 'manual'
|
|
10
|
+
|
|
11
|
+
/** 一条 .mdc 文本的解析结果(改写 frontmatter 所需的全部上下文)。 */
|
|
12
|
+
export interface RuleFmParsed {
|
|
13
|
+
type: RuleFmType
|
|
14
|
+
description: string
|
|
15
|
+
alwaysApply: boolean
|
|
16
|
+
globs: string[]
|
|
17
|
+
enabled: boolean
|
|
18
|
+
/** 是否存在规则 frontmatter(首行 --- 且有闭合)。 */
|
|
19
|
+
hasFm: boolean
|
|
20
|
+
/** 闭合 --- 之后的正文(按 \n 归一,与 host 解析一致)。 */
|
|
21
|
+
body: string
|
|
22
|
+
/** 已知键之外的 frontmatter 原行(如 updatedAt),改写时原样保留。 */
|
|
23
|
+
fmExtra: string[]
|
|
24
|
+
/** 原文是否带 BOM(改写后回填)。 */
|
|
25
|
+
bom: boolean
|
|
26
|
+
/** 原文 frontmatter 是否显式写了 enabled 行(改写时保持该行存在与否)。 */
|
|
27
|
+
hasEnabledLine: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 类型改写入参:下拉/描述/globs 控件的最新值。 */
|
|
31
|
+
export interface RuleMetaNext {
|
|
32
|
+
type: RuleFmType
|
|
33
|
+
description: string
|
|
34
|
+
globs: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 去除值两侧成对引号(与 host stripQuotes 一致)。
|
|
39
|
+
* @author ddj 2026年09月07号
|
|
40
|
+
* @param raw 原始值
|
|
41
|
+
* @returns 去引号后的值
|
|
42
|
+
*/
|
|
43
|
+
function stripQuotes(raw: string): string {
|
|
44
|
+
const value = raw.trim()
|
|
45
|
+
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
46
|
+
return value.slice(1, -1)
|
|
47
|
+
}
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --region 解析
|
|
52
|
+
/**
|
|
53
|
+
* 解析 .mdc 全文:首行 --- 且 100 行内有闭合 --- 才视为 frontmatter;
|
|
54
|
+
* 已知键 description/alwaysApply/globs/enabled,其余原行进 fmExtra;永不抛错。
|
|
55
|
+
* @author ddj 2026年09月07号
|
|
56
|
+
* @param text 文件全文
|
|
57
|
+
* @returns 解析结果
|
|
58
|
+
*/
|
|
59
|
+
export function parseRuleFm(text: string): RuleFmParsed {
|
|
60
|
+
const bom = text.startsWith('\uFEFF')
|
|
61
|
+
const lines = text.replace(/^\uFEFF/, '').split(/\r?\n/)
|
|
62
|
+
const none: RuleFmParsed = { type: 'manual', description: '', alwaysApply: false, globs: [], enabled: true, hasFm: false, body: text.replace(/^\uFEFF/, ''), fmExtra: [], bom, hasEnabledLine: false }
|
|
63
|
+
if (lines[0]?.trim() !== '---') return none
|
|
64
|
+
let close = -1
|
|
65
|
+
for (let i = 1; i < lines.length && i <= 101; i++) {
|
|
66
|
+
if (lines[i].trim() === '---') { close = i; break }
|
|
67
|
+
}
|
|
68
|
+
if (close < 0) return none
|
|
69
|
+
const parsed: RuleFmParsed = { type: 'manual', description: '', alwaysApply: false, globs: [], enabled: true, hasFm: true, body: lines.slice(close + 1).join('\n'), fmExtra: [], bom, hasEnabledLine: false }
|
|
70
|
+
for (let i = 1; i < close; i++) {
|
|
71
|
+
const match = /^([A-Za-z_-]+)[ \t]*:[ \t]?(.*)$/.exec(lines[i])
|
|
72
|
+
if (!match) { parsed.fmExtra.push(lines[i]); continue }
|
|
73
|
+
const [, key, raw] = match
|
|
74
|
+
if (key === 'description') parsed.description = stripQuotes(raw)
|
|
75
|
+
else if (key === 'alwaysApply') parsed.alwaysApply = stripQuotes(raw).toLowerCase() === 'true'
|
|
76
|
+
else if (key === 'enabled') { parsed.enabled = stripQuotes(raw).toLowerCase() !== 'false'; parsed.hasEnabledLine = true }
|
|
77
|
+
else if (key === 'globs') parsed.globs = parseGlobsValue(raw, lines, i, close)
|
|
78
|
+
else parsed.fmExtra.push(lines[i])
|
|
79
|
+
}
|
|
80
|
+
parsed.type = fmTypeOf(parsed)
|
|
81
|
+
return parsed
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 解析 globs 值:内联逗号分隔串,或空值后接 YAML 短横列表(与 host parseGlobs 一致)。
|
|
86
|
+
* @author ddj 2026年09月07号
|
|
87
|
+
* @param value 内联值
|
|
88
|
+
* @param lines 全文行
|
|
89
|
+
* @param start globs 行下标
|
|
90
|
+
* @param close 闭合 --- 下标
|
|
91
|
+
* @returns glob 数组
|
|
92
|
+
*/
|
|
93
|
+
function parseGlobsValue(value: string, lines: string[], start: number, close: number): string[] {
|
|
94
|
+
const inline = stripQuotes(value)
|
|
95
|
+
if (inline) return inline.split(',').map((item) => stripQuotes(item)).filter(Boolean)
|
|
96
|
+
const list: string[] = []
|
|
97
|
+
for (let j = start + 1; j < close; j++) {
|
|
98
|
+
const item = /^[ \t]*-[ \t]*(.+)$/.exec(lines[j])
|
|
99
|
+
if (!item) break
|
|
100
|
+
list.push(stripQuotes(item[1]))
|
|
101
|
+
}
|
|
102
|
+
return list
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 由 frontmatter 推导类型:alwaysApply=true → 总是;有 globs → 自动;否则手动。
|
|
107
|
+
* @author ddj 2026年09月07号
|
|
108
|
+
* @param parsed 解析结果
|
|
109
|
+
* @returns 规则类型
|
|
110
|
+
*/
|
|
111
|
+
function fmTypeOf(parsed: RuleFmParsed): RuleFmType {
|
|
112
|
+
if (parsed.alwaysApply) return 'always'
|
|
113
|
+
if (parsed.globs.length) return 'auto'
|
|
114
|
+
return 'manual'
|
|
115
|
+
}
|
|
116
|
+
// --endregion
|
|
117
|
+
|
|
118
|
+
// --region 改写
|
|
119
|
+
/**
|
|
120
|
+
* 按最新控件值重写 frontmatter:known 键固定顺序 + fmExtra + 闭合 --- + 原 body;
|
|
121
|
+
* 无 frontmatter 且目标为手动时:有描述则新建仅含描述的 frontmatter,否则原文返回。
|
|
122
|
+
* @author ddj 2026年09月07号
|
|
123
|
+
* @param parsed parseRuleFm 的结果(必须来自同一文本)
|
|
124
|
+
* @param next 控件最新值
|
|
125
|
+
* @returns 重写后的全文
|
|
126
|
+
*/
|
|
127
|
+
export function applyRuleMeta(parsed: RuleFmParsed, next: RuleMetaNext): string {
|
|
128
|
+
if (!parsed.hasFm && next.type === 'manual') {
|
|
129
|
+
if (!next.description.trim()) return (parsed.bom ? '\uFEFF' : '') + parsed.body
|
|
130
|
+
return (parsed.bom ? '\uFEFF' : '') + ['---', 'description: ' + next.description.trim(), '---'].join('\n') + '\n' + parsed.body
|
|
131
|
+
}
|
|
132
|
+
const lines: string[] = ['---']
|
|
133
|
+
lines.push('description: ' + next.description.trim())
|
|
134
|
+
if (next.type === 'always') lines.push('alwaysApply: true')
|
|
135
|
+
if (next.type === 'auto') lines.push('globs: ' + next.globs.join(', '))
|
|
136
|
+
if (parsed.hasEnabledLine) lines.push('enabled: ' + (parsed.enabled ? 'true' : 'false'))
|
|
137
|
+
lines.push(...parsed.fmExtra)
|
|
138
|
+
lines.push('---')
|
|
139
|
+
return (parsed.bom ? '\uFEFF' : '') + lines.join('\n') + '\n' + parsed.body
|
|
140
|
+
}
|
|
141
|
+
// --endregion
|
|
@@ -125,7 +125,8 @@ export function SidebarView(props) {
|
|
|
125
125
|
|
|
126
126
|
return React.createElement('div', {
|
|
127
127
|
className: 'edrv-sidebar' + (visible ? '' : ' edrv-sidebar-rail-only'),
|
|
128
|
-
|
|
128
|
+
// maxWidth 兜底:持久化宽度超过容器可用空间时也不允许布局宽溢出(配合 flex-shrink:1 收缩)
|
|
129
|
+
style: { width: visible ? width : RAIL_ONLY_W + SEAM_W, maxWidth: '100%' },
|
|
129
130
|
},
|
|
130
131
|
rail,
|
|
131
132
|
visible
|
|
@@ -2,18 +2,30 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — 侧边栏「规则」面板(Codebuddy 规则管理形态)。
|
|
4
4
|
* 双 Tab:用户规则(~/.dsh/rules/)与项目规则(<工作区>/.dsh/rules/);
|
|
5
|
-
* 列表行 = 文件名 +
|
|
6
|
-
* 新建/编辑 =
|
|
7
|
-
*
|
|
5
|
+
* 列表行 = 文件名 + 相对提示,第二行类型徽标与描述,右侧 编辑/删除 图标 + 滑动启用开关;
|
|
6
|
+
* 新建/编辑 = 顶部面包屑 + 规则类型下拉(总是/自动/手动)+ 描述/globs 控件,与原始 .mdc
|
|
7
|
+
* 文本域双向同步(控件改写 frontmatter,文本域回读控件),保存走 rules.save。
|
|
8
|
+
* 作者 ddj 2026年09月03号(2026年09月07号 交互性调整)
|
|
8
9
|
*/
|
|
9
10
|
import React from 'react'
|
|
10
11
|
import { rpc } from '../../rpc.js'
|
|
11
12
|
import type { SidebarCtx } from '../types.js'
|
|
12
13
|
import { CACHE_KEY } from '../../paths.js'
|
|
14
|
+
import { parseRuleFm, applyRuleMeta } from '../../rulesMdc.js'
|
|
13
15
|
|
|
14
16
|
/** 新建规则模板(frontmatter 字段与 host parseRuleMdc 对齐)。 */
|
|
15
17
|
const NEW_TEMPLATE = '---\ndescription: \nalwaysApply: true\n---\n\n'
|
|
16
18
|
const TYPE_LABEL = { always: '总是', auto: '自动', manual: '手动' }
|
|
19
|
+
/** 列表行描述的基础显示上限(字符数;宽面板下的封顶值,超出省略号)。 */
|
|
20
|
+
const DESC_MAX = 24
|
|
21
|
+
/** 动态字数下限(极窄面板的保底可见字符数)。 */
|
|
22
|
+
const DESC_CAP_MIN = 8
|
|
23
|
+
/** 动态字数估算:描述之外的行内固定占用 px(动作区/类型标签/徽标/间距/省略余量)。 */
|
|
24
|
+
const DESC_RESERVE_PX = 210
|
|
25
|
+
/** 动态字数估算:单字符平均宽度 px(11px 字号,中英混排)。 */
|
|
26
|
+
const DESC_CHAR_PX = 10
|
|
27
|
+
/** 窄面板阈值:低于此宽度隐藏路径提示(编辑界面面包屑与悬停仍有完整路径)。 */
|
|
28
|
+
const NARROW_W = 420
|
|
17
29
|
|
|
18
30
|
/**
|
|
19
31
|
* 读 localStorage(损坏/不可用安全)。
|
|
@@ -45,9 +57,85 @@ function typeBadge(type) {
|
|
|
45
57
|
return React.createElement('span', { className: 'edrv-rules-type edrv-rules-type-' + type }, TYPE_LABEL[type] ?? type)
|
|
46
58
|
}
|
|
47
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 按字符上限截断列表行描述,超出以省略号收尾(上限随面板宽度动态变化;
|
|
62
|
+
* 窄面板下 CSS 省略号仍会按像素提前截断,二者取更小)。
|
|
63
|
+
* @author ddj 2026年09月07号
|
|
64
|
+
* @param text 完整描述
|
|
65
|
+
* @param cap 本帧字符上限
|
|
66
|
+
* @returns 截断后的展示文本
|
|
67
|
+
*/
|
|
68
|
+
function clampDesc(text, cap) {
|
|
69
|
+
const chars = Array.from(String(text ?? '')) // 按码位切分,避免把 emoji 等代理对从中间截断
|
|
70
|
+
const limit = Math.max(4, Number(cap) || DESC_MAX)
|
|
71
|
+
return chars.length > limit ? chars.slice(0, limit).join('') + '…' : chars.join('')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 由面板宽度估算描述可用字符数:随宽度动态变化,并与基础上限取小、极窄时保底。
|
|
76
|
+
* @author ddj 2026年09月07号
|
|
77
|
+
* @param panelWidth 面板内容宽度(px)
|
|
78
|
+
* @returns 本帧描述字符上限
|
|
79
|
+
*/
|
|
80
|
+
function descCapOf(panelWidth) {
|
|
81
|
+
const byWidth = Math.floor((Number(panelWidth) - DESC_RESERVE_PX) / DESC_CHAR_PX)
|
|
82
|
+
return Math.max(DESC_CAP_MIN, Math.min(DESC_MAX, byWidth))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 编辑图标(线性铅笔,与 DiffBox chevron 同风格:currentColor 描边装饰图)。
|
|
87
|
+
* @author ddj 2026年09月07号
|
|
88
|
+
* @returns 13×13 装饰性 SVG
|
|
89
|
+
*/
|
|
90
|
+
function pencilIcon() {
|
|
91
|
+
return React.createElement('svg', {
|
|
92
|
+
width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true, focusable: false,
|
|
93
|
+
}, React.createElement('path', {
|
|
94
|
+
d: 'M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z',
|
|
95
|
+
stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
96
|
+
}))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 删除图标(线性垃圾桶,风格同 pencilIcon)。
|
|
101
|
+
* @author ddj 2026年09月07号
|
|
102
|
+
* @returns 13×13 装饰性 SVG
|
|
103
|
+
*/
|
|
104
|
+
function trashIcon() {
|
|
105
|
+
return React.createElement('svg', {
|
|
106
|
+
width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true, focusable: false,
|
|
107
|
+
}, React.createElement('path', {
|
|
108
|
+
d: 'M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14zM10 11v6M14 11v6',
|
|
109
|
+
stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
110
|
+
}))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 逗号分隔的 globs 输入串 → 数组(去空项)。
|
|
115
|
+
* @author ddj 2026年09月07号
|
|
116
|
+
* @param text 输入串
|
|
117
|
+
* @returns glob 数组
|
|
118
|
+
*/
|
|
119
|
+
function globList(text) {
|
|
120
|
+
return String(text ?? '').split(',').map((item) => item.trim()).filter(Boolean)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 由控件值重建 .mdc 全文:frontmatter 按类型/描述/globs 重写,正文与其余键不动。
|
|
125
|
+
* @author ddj 2026年09月07号
|
|
126
|
+
* @param base 当前全文
|
|
127
|
+
* @param fmType 目标类型(always/auto/manual)
|
|
128
|
+
* @param desc 描述文本
|
|
129
|
+
* @param globsText globs 输入串
|
|
130
|
+
* @returns 重写后的全文
|
|
131
|
+
*/
|
|
132
|
+
function rebuildContent(base, fmType, desc, globsText) {
|
|
133
|
+
return applyRuleMeta(parseRuleFm(base), { type: fmType, description: desc, globs: globList(globsText) })
|
|
134
|
+
}
|
|
135
|
+
|
|
48
136
|
// --region 列表行
|
|
49
137
|
/**
|
|
50
|
-
*
|
|
138
|
+
* 渲染一条规则行:左侧两行(文件名+相对提示 / 类型徽标+描述),右侧 编辑/删除 图标 + 滑动开关。
|
|
51
139
|
* @author ddj 2026年09月03号
|
|
52
140
|
* @param props.rule 规则元信息
|
|
53
141
|
* @param props.onEdit 编辑回调
|
|
@@ -61,14 +149,17 @@ function RuleRow(props) {
|
|
|
61
149
|
.filter(Boolean).join(' · ')
|
|
62
150
|
return React.createElement('div', { className: 'edrv-rules-row' + (rule.enabled ? '' : ' off') },
|
|
63
151
|
React.createElement('div', { className: 'edrv-rules-main', onClick: () => props.onEdit(rule), title: rule.absPath },
|
|
64
|
-
React.createElement('
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
152
|
+
React.createElement('div', { className: 'edrv-rules-line1' },
|
|
153
|
+
React.createElement('span', { className: 'edrv-rules-file' }, rule.file),
|
|
154
|
+
!props.narrow ? React.createElement('span', { className: 'edrv-rules-path' }, '[' + rule.relHint + rule.file + ']') : null,
|
|
155
|
+
!rule.enabled ? React.createElement('span', { className: 'edrv-rules-offtag' }, '已停用') : null),
|
|
156
|
+
React.createElement('div', { className: 'edrv-rules-sub' },
|
|
157
|
+
React.createElement('span', { className: 'edrv-rules-sublabel' }, '类型:'),
|
|
158
|
+
typeBadge(rule.type),
|
|
159
|
+
desc ? React.createElement('span', { className: 'edrv-rules-desc', title: desc }, clampDesc(desc, props.descCap)) : null)),
|
|
69
160
|
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) },
|
|
161
|
+
React.createElement('button', { className: 'edrv-rules-act', title: '编辑', onClick: () => props.onEdit(rule) }, pencilIcon()),
|
|
162
|
+
React.createElement('button', { className: 'edrv-rules-act', title: '删除', onClick: () => props.onRemove(rule) }, trashIcon()),
|
|
72
163
|
React.createElement('button', {
|
|
73
164
|
className: 'edrv-rules-switch' + (rule.enabled ? ' on' : ''), role: 'switch', 'aria-checked': rule.enabled,
|
|
74
165
|
title: rule.enabled ? '已启用(点击停用)' : '已停用(点击启用)',
|
|
@@ -79,10 +170,13 @@ function RuleRow(props) {
|
|
|
79
170
|
|
|
80
171
|
// --region 内嵌编辑器
|
|
81
172
|
/**
|
|
82
|
-
*
|
|
173
|
+
* 新建/编辑表单:面包屑头部 + 规则类型下拉/描述/globs 控件行 + 原始 .mdc 文本域 + 保存/取消。
|
|
83
174
|
* @author ddj 2026年09月03号
|
|
84
|
-
* @param props.form 表单状态(mode/scope/workspacePath/file/content/error/saving)
|
|
85
|
-
* @param props.
|
|
175
|
+
* @param props.form 表单状态(mode/scope/workspacePath/relHint/file/content/fmType/desc/globs/error/saving)
|
|
176
|
+
* @param props.onType 类型下拉变更回调
|
|
177
|
+
* @param props.onDesc 描述输入回调
|
|
178
|
+
* @param props.onGlobs globs 输入回调
|
|
179
|
+
* @param props.onBody 文本域编辑回调
|
|
86
180
|
* @param props.onSave 保存回调
|
|
87
181
|
* @param props.onCancel 取消回调
|
|
88
182
|
* @returns 表单元素
|
|
@@ -90,23 +184,33 @@ function RuleRow(props) {
|
|
|
90
184
|
function RuleEditor(props) {
|
|
91
185
|
const form = props.form
|
|
92
186
|
const isNew = form.mode === 'new'
|
|
187
|
+
const crumb = (form.scope === 'project' ? '项目规则' : '用户规则') + ' > ' + form.relHint + ' > ' + (form.file || '新建规则.mdc')
|
|
93
188
|
return React.createElement('div', { className: 'edrv-rules-editor' },
|
|
94
189
|
React.createElement('div', { className: 'edrv-rules-editor-head' },
|
|
95
|
-
|
|
190
|
+
React.createElement('span', { className: 'edrv-rules-crumb', title: crumb }, crumb),
|
|
96
191
|
React.createElement('button', { className: 'edrv-rules-act', title: '取消', onClick: props.onCancel }, '✕')),
|
|
97
|
-
React.createElement('div', { className: 'edrv-rules-editor-
|
|
98
|
-
React.createElement('label',
|
|
192
|
+
React.createElement('div', { className: 'edrv-rules-editor-ctl' },
|
|
193
|
+
React.createElement('label', { className: 'edrv-rules-ctl-label' }, '规则类型'),
|
|
194
|
+
React.createElement('select', { className: 'edrv-rules-select', value: form.fmType, onChange: props.onType },
|
|
195
|
+
Object.keys(TYPE_LABEL).map((key) => React.createElement('option', { key, value: key }, TYPE_LABEL[key]))),
|
|
196
|
+
React.createElement('label', { className: 'edrv-rules-ctl-label' }, '描述'),
|
|
99
197
|
React.createElement('input', {
|
|
100
|
-
className: 'edrv-rules-input', value: form.
|
|
101
|
-
placeholder: '
|
|
198
|
+
className: 'edrv-rules-input edrv-rules-desc-input', value: form.desc, spellCheck: false,
|
|
199
|
+
placeholder: isNew ? '规则用途说明(写入 frontmatter description)' : '规则用途说明', onChange: props.onDesc,
|
|
102
200
|
})),
|
|
201
|
+
form.fmType === 'auto' ? React.createElement('div', { className: 'edrv-rules-editor-ctl' },
|
|
202
|
+
React.createElement('label', { className: 'edrv-rules-ctl-label' }, 'globs'),
|
|
203
|
+
React.createElement('input', {
|
|
204
|
+
className: 'edrv-rules-input', value: form.globs, spellCheck: false,
|
|
205
|
+
placeholder: 'src/**/*.ts, *.md(逗号分隔,命中才注入)', onChange: props.onGlobs,
|
|
206
|
+
})) : null,
|
|
103
207
|
React.createElement('textarea', {
|
|
104
208
|
className: 'edrv-rules-body', spellCheck: false,
|
|
105
209
|
value: form.content,
|
|
106
|
-
onChange:
|
|
210
|
+
onChange: props.onBody,
|
|
107
211
|
}),
|
|
108
212
|
React.createElement('div', { className: 'edrv-rules-editor-hint' },
|
|
109
|
-
'
|
|
213
|
+
'类型/描述/globs 与 frontmatter 双向同步;enabled 与其余键原样保留;无 frontmatter 的纯 markdown 视为「手动」规则'),
|
|
110
214
|
form.error ? React.createElement('div', { className: 'edrv-rules-err' }, form.error) : null,
|
|
111
215
|
React.createElement('div', { className: 'edrv-rules-editor-foot' },
|
|
112
216
|
React.createElement('button', { className: 'edrv-rules-save', disabled: form.saving, onClick: props.onSave }, form.saving ? '保存中…' : '保存')))
|
|
@@ -125,6 +229,8 @@ export function RulesPanel(props) {
|
|
|
125
229
|
const [tab, setTab] = React.useState(() => (readLocal(CACHE_KEY.rules + 'tab') === 'project' ? 'project' : 'user'))
|
|
126
230
|
const [data, setData] = React.useState(null) // { user: RuleInfo[], projects: RuleProject[] }
|
|
127
231
|
const [error, setError] = React.useState('')
|
|
232
|
+
const panelRef = React.useRef(null)
|
|
233
|
+
const [panelW, setPanelW] = React.useState(0) // 面板内容宽度(ResizeObserver 跟踪;0=未测得)
|
|
128
234
|
const [form, setForm] = React.useState(null) // 编辑器表单状态;null=列表态
|
|
129
235
|
const mountedRef = React.useRef(true)
|
|
130
236
|
|
|
@@ -151,6 +257,20 @@ export function RulesPanel(props) {
|
|
|
151
257
|
return () => { mountedRef.current = false }
|
|
152
258
|
}, [])
|
|
153
259
|
|
|
260
|
+
// 面板宽度跟踪:描述字数上限与窄面板模式都随宽度动态变化
|
|
261
|
+
React.useEffect(() => {
|
|
262
|
+
const el = panelRef.current
|
|
263
|
+
if (!el || typeof ResizeObserver === 'undefined') return undefined
|
|
264
|
+
const observer = new ResizeObserver(() => setPanelW(el.clientWidth))
|
|
265
|
+
observer.observe(el)
|
|
266
|
+
setPanelW(el.clientWidth)
|
|
267
|
+
return () => observer.disconnect()
|
|
268
|
+
}, [])
|
|
269
|
+
|
|
270
|
+
// 描述字数上限与窄面板模式(未测得宽度前用基础上限/完整展示)
|
|
271
|
+
const descCap = panelW > 0 ? descCapOf(panelW) : DESC_MAX
|
|
272
|
+
const narrow = panelW > 0 && panelW < NARROW_W
|
|
273
|
+
|
|
154
274
|
/**
|
|
155
275
|
* 切换 Tab(记忆到 localStorage)。
|
|
156
276
|
* @param next 目标 Tab
|
|
@@ -166,27 +286,61 @@ export function RulesPanel(props) {
|
|
|
166
286
|
const openNew = () => {
|
|
167
287
|
const scope = tab === 'project' ? 'project' : 'user'
|
|
168
288
|
if (scope === 'project' && !cwd) { notify('当前会话没有工作区,无法新建项目规则'); return }
|
|
169
|
-
|
|
289
|
+
const parsed = parseRuleFm(NEW_TEMPLATE)
|
|
290
|
+
setForm({
|
|
291
|
+
mode: 'new', scope, workspacePath: scope === 'project' ? cwd : undefined,
|
|
292
|
+
relHint: scope === 'project' ? '.dsh/rules/' : 'rules/', file: '', content: NEW_TEMPLATE,
|
|
293
|
+
fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', '), error: '', saving: false,
|
|
294
|
+
})
|
|
170
295
|
}
|
|
171
296
|
|
|
172
297
|
/**
|
|
173
|
-
*
|
|
298
|
+
* 打开编辑表单:先拉全文,再解析 frontmatter 回填类型/描述/globs 控件。
|
|
174
299
|
* @param rule 目标规则
|
|
175
300
|
*/
|
|
176
301
|
const openEdit = (rule) => {
|
|
177
302
|
rpc('rules.read', { scope: rule.scope, workspacePath: rule.scope === 'project' ? rule.workspacePath : undefined, file: rule.file })
|
|
178
303
|
.then((res) => {
|
|
179
|
-
if (res
|
|
180
|
-
|
|
181
|
-
|
|
304
|
+
if (!res || !res.ok) { notify(res?.error ?? '读取规则失败'); return }
|
|
305
|
+
const parsed = parseRuleFm(res.content)
|
|
306
|
+
setForm({
|
|
307
|
+
mode: 'edit', scope: rule.scope, workspacePath: rule.workspacePath, relHint: rule.relHint,
|
|
308
|
+
file: rule.file, content: res.content,
|
|
309
|
+
fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', '), error: '', saving: false,
|
|
310
|
+
})
|
|
182
311
|
}).catch((e) => notify('读取规则异常: ' + String(e)))
|
|
183
312
|
}
|
|
184
313
|
|
|
185
|
-
/**
|
|
314
|
+
/** 类型下拉变更:frontmatter 按新类型重写,控件同步。 */
|
|
315
|
+
const onTypeChange = (e) => {
|
|
316
|
+
const nextType = e.target.value
|
|
317
|
+
setForm((prev) => ({ ...prev, fmType: nextType, content: rebuildContent(prev.content, nextType, prev.desc, prev.globs), error: '' }))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 描述输入变更:同步 frontmatter description 行。 */
|
|
321
|
+
const onDescChange = (e) => {
|
|
322
|
+
const nextDesc = e.target.value
|
|
323
|
+
setForm((prev) => ({ ...prev, desc: nextDesc, content: rebuildContent(prev.content, prev.fmType, nextDesc, prev.globs), error: '' }))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** globs 输入变更(自动规则):同步 frontmatter globs 行。 */
|
|
327
|
+
const onGlobsChange = (e) => {
|
|
328
|
+
const nextGlobs = e.target.value
|
|
329
|
+
setForm((prev) => ({ ...prev, globs: nextGlobs, content: rebuildContent(prev.content, prev.fmType, prev.desc, nextGlobs), error: '' }))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** 文本域直接编辑:回读 frontmatter,同步类型/描述/globs 控件(文本域是唯一真源)。 */
|
|
333
|
+
const onBodyChange = (e) => {
|
|
334
|
+
const parsed = parseRuleFm(e.target.value)
|
|
335
|
+
setForm((prev) => ({ ...prev, content: e.target.value, fmType: parsed.type, desc: parsed.description, globs: parsed.globs.join(', ') }))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** 保存表单(rules.save;文件名非空 + 自动规则必须有 globs)。 */
|
|
186
339
|
const saveForm = () => {
|
|
187
340
|
const next = { ...form, saving: true, error: '' }
|
|
188
341
|
const file = String(form.file ?? '').trim()
|
|
189
342
|
if (!file) { setForm({ ...next, saving: false, error: '文件名不能为空' }); return }
|
|
343
|
+
if (form.fmType === 'auto' && globList(form.globs).length === 0) { setForm({ ...next, saving: false, error: '自动规则需要至少一个 glob(逗号分隔)' }); return }
|
|
190
344
|
setForm(next)
|
|
191
345
|
rpc('rules.save', { scope: form.scope, workspacePath: form.workspacePath, file, content: form.content })
|
|
192
346
|
.then((res) => {
|
|
@@ -261,12 +415,19 @@ export function RulesPanel(props) {
|
|
|
261
415
|
}
|
|
262
416
|
|
|
263
417
|
const renderBody = () => {
|
|
264
|
-
if (form)
|
|
418
|
+
if (form) {
|
|
419
|
+
return React.createElement(RuleEditor, {
|
|
420
|
+
form, onType: onTypeChange, onDesc: onDescChange, onGlobs: onGlobsChange, onBody: onBodyChange,
|
|
421
|
+
onSave: saveForm, onCancel: () => setForm(null),
|
|
422
|
+
})
|
|
423
|
+
}
|
|
265
424
|
if (!data) return React.createElement('div', { className: 'edrv-rules-empty' }, '加载中…')
|
|
266
425
|
if (error) return React.createElement('div', { className: 'edrv-rules-err' }, error)
|
|
267
426
|
const rows = rowsOf()
|
|
268
427
|
if (!rows.length) return React.createElement('div', { className: 'edrv-rules-empty' }, renderEmpty())
|
|
269
428
|
const common = {
|
|
429
|
+
descCap,
|
|
430
|
+
narrow,
|
|
270
431
|
onEdit: (rule) => openEdit({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
271
432
|
onRemove: (rule) => removeRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
272
433
|
onToggle: (rule) => toggleRule({ ...rule, workspacePath: rule.scope === 'project' ? cwd : undefined }),
|
|
@@ -275,7 +436,7 @@ export function RulesPanel(props) {
|
|
|
275
436
|
rows.map((rule) => React.createElement(RuleRow, { key: rule.scope + ':' + rule.file, rule, ...common })))
|
|
276
437
|
}
|
|
277
438
|
|
|
278
|
-
return React.createElement('div', { className: 'edrv-rules-panel' },
|
|
439
|
+
return React.createElement('div', { className: 'edrv-rules-panel', ref: panelRef },
|
|
279
440
|
renderTabs(),
|
|
280
441
|
renderWsLine(),
|
|
281
442
|
React.createElement('div', { className: 'edrv-rules-hint' },
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
[data-edrv-view] .edrv-main-col > .edrv-pathbar,
|
|
207
207
|
[data-edrv-view] .edrv-main-col > .edrv-tabs { flex: 0 0 auto; }
|
|
208
208
|
[data-edrv-view] .edrv-editor-area { position: relative; display: flex; flex: 1 1 auto; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
|
|
209
|
-
[data-edrv-view] .edrv-sidebar { display: flex; flex-direction: row; flex-shrink:
|
|
209
|
+
[data-edrv-view] .edrv-sidebar { display: flex; flex-direction: row; flex-shrink: 1; min-width: 0; height: 100%; background: var(--dsw-alias-bg-layer-1, #ffffff); border-right: 1px solid var(--dsw-alias-border-l1, #e0e6e8); overflow: hidden; }
|
|
210
210
|
[data-edrv-view] .edrv-rail { display: flex; flex-direction: column; align-items: center; width: 44px; flex-shrink: 0; padding: 6px 0; background: var(--dsw-alias-bg-layer-2, #f0f4f4); border-right: 1px solid var(--dsw-alias-border-l1, #e0e6e8); gap: 4px; }
|
|
211
211
|
[data-edrv-view] .edrv-rail-btn { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: none; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, #52606d); cursor: pointer; flex-shrink: 0; }
|
|
212
212
|
[data-edrv-view] .edrv-rail-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
[data-edrv-view] .edrv-search-toggle:disabled { opacity: .4; cursor: default; }
|
|
297
297
|
|
|
298
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; }
|
|
299
|
+
[data-edrv-view] .edrv-rules-panel { display: flex; flex-direction: column; min-width: 0; min-height: 0; flex: 1; padding: 6px; gap: 4px; }
|
|
300
300
|
[data-edrv-view] .edrv-rules-tabs { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
|
|
301
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
302
|
[data-edrv-view] .edrv-rules-tab:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
@@ -306,19 +306,23 @@
|
|
|
306
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
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
308
|
[data-edrv-view] .edrv-rules-list { flex: 1; min-height: 0; overflow: auto; }
|
|
309
|
-
[data-edrv-view] .edrv-rules-row {
|
|
310
|
-
[data-edrv-view] .edrv-rules-row
|
|
311
|
-
[data-edrv-view] .edrv-rules-
|
|
309
|
+
[data-edrv-view] .edrv-rules-row { display: flex; align-items: center; gap: 4px; padding: 6px 4px; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; }
|
|
310
|
+
[data-edrv-view] .edrv-rules-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
311
|
+
[data-edrv-view] .edrv-rules-row.off .edrv-rules-main { opacity: .5; }
|
|
312
|
+
[data-edrv-view] .edrv-rules-main { flex: 1; min-width: 0; cursor: pointer; overflow: hidden; }
|
|
313
|
+
[data-edrv-view] .edrv-rules-line1 { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
314
|
+
[data-edrv-view] .edrv-rules-sub { display: flex; align-items: center; gap: 5px; margin-top: 3px; min-width: 0; }
|
|
315
|
+
[data-edrv-view] .edrv-rules-sublabel { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); flex-shrink: 0; }
|
|
312
316
|
[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-
|
|
317
|
+
[data-edrv-view] .edrv-rules-path { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
314
318
|
[data-edrv-view] .edrv-rules-type { flex-shrink: 0; height: 15px; padding: 0 5px; border-radius: 999px; font-size: 10px; line-height: 15px; }
|
|
315
319
|
[data-edrv-view] .edrv-rules-type-always { background: rgba(15,157,88,.15); color: #0b7c46; }
|
|
316
320
|
[data-edrv-view] .edrv-rules-type-auto { background: rgba(79,140,255,.16); color: #3367c9; }
|
|
317
321
|
[data-edrv-view] .edrv-rules-type-manual { background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); }
|
|
318
322
|
[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 {
|
|
320
|
-
[data-edrv-view] .edrv-rules-actions {
|
|
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); }
|
|
323
|
+
[data-edrv-view] .edrv-rules-desc { font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 0 1 auto; }
|
|
324
|
+
[data-edrv-view] .edrv-rules-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; margin-left: auto; }
|
|
325
|
+
[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); display: inline-flex; align-items: center; justify-content: center; }
|
|
322
326
|
[data-edrv-view] .edrv-rules-act:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
|
|
323
327
|
[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
328
|
[data-edrv-view] .edrv-rules-switch.on { background: #7c5cff; }
|
|
@@ -327,8 +331,13 @@
|
|
|
327
331
|
[data-edrv-view] .edrv-rules-empty { padding: 10px 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); line-height: 1.6; }
|
|
328
332
|
[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
333
|
[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-
|
|
334
|
+
[data-edrv-view] .edrv-rules-editor-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #1f2933); flex: 0 0 auto; }
|
|
335
|
+
[data-edrv-view] .edrv-rules-crumb { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
336
|
+
[data-edrv-view] .edrv-rules-editor-ctl { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
|
|
337
|
+
[data-edrv-view] .edrv-rules-ctl-label { font-size: 11px; color: var(--dsw-alias-label-secondary, #52606d); flex-shrink: 0; }
|
|
338
|
+
[data-edrv-view] .edrv-rules-select { height: 24px; padding: 0 4px; 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; cursor: pointer; flex-shrink: 0; }
|
|
339
|
+
[data-edrv-view] .edrv-rules-select:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
340
|
+
[data-edrv-view] .edrv-rules-desc-input { flex: 1; min-width: 0; }
|
|
332
341
|
[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
342
|
[data-edrv-view] .edrv-rules-input:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
|
|
334
343
|
[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); }
|