dsh-vscode-mode 0.1.48 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.48",
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",
@@ -11,7 +11,7 @@ export const SIDEBAR_PLUGIN = 'dsh-better-sidebar'
11
11
 
12
12
  /** 设置 scope 的跨组件上下文形状(与官方 settingsScope 绑定的最小面)。 */
13
13
  export interface SettingsScopeLike {
14
- getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown; keybindings?: Record<string, string> }; writable?: boolean }
14
+ getSnapshot: () => { status?: string; value?: { fileOpenTool?: unknown; keybindings?: Record<string, string>; sidebarMinWidth?: unknown }; writable?: boolean }
15
15
  subscribe: (listener: () => void) => () => void
16
16
  set: (field: string, value: unknown) => Promise<void>
17
17
  }
@@ -33,12 +33,14 @@ import { createAddToConversation } from './addToConversation.js'
33
33
  import { createSidebarPanelRegistry } from './sidebar/registry.js'
34
34
  import { createFilePanel } from './sidebar/panels/index.js'
35
35
  import { createSearchPanel } from './sidebar/panels/index.js'
36
+ import { createRulesPanel } from './sidebar/panels/index.js'
36
37
  import { createTreeMenuRegistry } from './sidebar/contextMenu.js'
37
38
  import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
38
39
  import { createOutlinePanel } from './outline/index.js'
39
40
  import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
40
41
  import { createLspOutlineSource } from './outline/lspSource.js'
41
42
  import { keybindingsApply } from './keybindings.js'
43
+ import { sidebarMinApply } from './sidebarMin.js'
42
44
  import { setupLsp, setSession } from './monaco/lsp/index.js'
43
45
  import type { CompatAdapter } from '../shared/compat.js'
44
46
 
@@ -114,6 +116,8 @@ export function apply(ctx: any): void {
114
116
  ctx.effect(() => sidebarPanels.register(createFilePanel()), 'vscode-mode: sidebar panel')
115
117
  // 搜索面板:活动栏「搜索」页签(Ctrl+Shift+F 唤起,VSCode 搜索视图形态)
116
118
  ctx.effect(() => sidebarPanels.register(createSearchPanel()), 'vscode-mode: sidebar panel search')
119
+ // 规则面板:活动栏「规则」页签(Codebuddy 规则管理形态:用户/项目规则 + 启用开关,host 注入生效)
120
+ ctx.effect(() => sidebarPanels.register(createRulesPanel()), 'vscode-mode: sidebar panel rules')
117
121
  // 文件右键菜单项注册表(对外 provide,供本插件/第三方注册;内置「在文件浏览器中打开」)
118
122
  const fileMenuItems = createTreeMenuRegistry()
119
123
  ctx.provide('edrvFileContextMenuItems', fileMenuItems)
@@ -147,12 +151,15 @@ export function apply(ctx: any): void {
147
151
  return settings.subscribe(sync)
148
152
  }, 'vscode-mode: file opener setting sync')
149
153
  // 快捷键配置同步:设置提交后立即刷新键位匹配(编辑器/QuickOpen 按事件时读取)
154
+ // 同一订阅里顺带同步侧边栏最小宽度(sidebarMinWidth)→ 派发 edrv:sidebar-min-width 通知编辑器重夹
150
155
  ctx.effect(() => {
151
156
  if (!settings) return undefined
152
157
  const sync = (): void => {
153
158
  const snapshot = settings.getSnapshot()
154
159
  if (snapshot.status === 'loading') return
155
160
  keybindingsApply(snapshot.value?.keybindings)
161
+ const minW = sidebarMinApply(snapshot.value?.sidebarMinWidth)
162
+ window.dispatchEvent(new CustomEvent('edrv:sidebar-min-width', { detail: { value: minW } }))
156
163
  }
157
164
  sync()
158
165
  return settings.subscribe(sync)
@@ -181,7 +188,7 @@ export function apply(ctx: any): void {
181
188
  order: 5,
182
189
  label: '文件编辑',
183
190
  inject: (sessionId: string) => ({ sessionId }),
184
- }, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })))
191
+ }, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { layout: 'tab', sideHint: SIDEBAR_INSTALL_CMD, schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })))
185
192
  }
186
193
 
187
194
  // 侧边栏/中央页签两形态互斥切换:sideDisposer(侧边栏 Tab)与 legacyDisposer(中央页签)只保留其一。
@@ -197,7 +204,7 @@ export function apply(ctx: any): void {
197
204
  if (legacyDisposer !== null) { legacyDisposer(); legacyDisposer = null }
198
205
  sideDisposer = ctx.effect(() => installSideEditor({
199
206
  service: sideService as NonNullable<typeof sideService>,
200
- renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })),
207
+ renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })),
201
208
  activeSession: () => {
202
209
  const snapshot = sessions?.list?.getSnapshot?.() as { current?: string; byId?: Record<string, { cwd?: string }> } | undefined
203
210
  const sessionId = snapshot?.current
@@ -24,6 +24,8 @@ export const CACHE_KEY = {
24
24
  sideHint: 'edrv.side-hint-dismissed',
25
25
  /** 「性能优化」页工作区栏目展开状态(workspaceFoldCache v1,全局不按会话)。 */
26
26
  workspaceFold: 'edrv.ws-fold.v1.',
27
+ /** 规则面板 UI 状态(RulesPanel v1:Tab 与项目工作区选择,全局不按会话)。 */
28
+ rules: 'edrv.rules.v1.',
27
29
  } as const
28
30
 
29
31
  /** 按前缀拼会话键(侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
@@ -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
@@ -1,15 +1,22 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * dsh-vscode-mode client — 侧边栏容器(活动栏 + 面板区 + 拖拽调宽)。
4
- * 面板列表来自注册表(活动栏图标/徽标/激活态),渲染当前激活面板的 render(ctx);
5
- * 右侧手柄拖拽调宽(clamp 180–560),底部按钮收起侧边栏。
6
- * 作者 ddj 2026-08-26
3
+ * dsh-vscode-mode client — 侧边栏容器(活动栏常显 + 可隐藏面板区 + 拖拽调宽)。
4
+ * 面板列表来自注册表(活动栏图标/徽标/激活态);最左侧图标列(活动栏)常显,
5
+ * 右侧详情面板区可拖拽调宽/隐藏:visible=false 时仅保留图标列(宽 = 图标列宽),
6
+ * 点击任一图标重新展开面板(onShow),底部按钮在 ◀ 收起 / ▶ 展开间切换。
7
+ * 拖拽调宽下限 = minWidth prop(通用设置 sidebarMinWidth,默认 300),低于下限直接收起面板区。
8
+ * 作者 ddj 2026-08-26 / 2026-09-04
7
9
  */
8
10
  import React from 'react'
9
11
  import type { SidebarCtx } from './types.js'
10
12
 
11
- const W_MIN = 180
12
13
  const W_MAX = 560
14
+ /** 面板区隐藏时容器的宽度(= 图标列宽,与 editor.css .edrv-rail 对齐)。 */
15
+ const RAIL_ONLY_W = 44
16
+ /** 隐藏态接缝拖拽条宽度(与 editor.css .edrv-side-resize 对齐)。 */
17
+ const SEAM_W = 5
18
+ /** 隐藏态拖出面板区的触发阈值(px,拖过即展开并继续实时调宽)。 */
19
+ const PULL_OUT_THRESHOLD = 12
13
20
 
14
21
  /**
15
22
  * 侧边栏容器。
@@ -17,18 +24,24 @@ const W_MAX = 560
17
24
  * @param props.ctx 面板共享上下文
18
25
  * @param props.activePanel 当前激活面板 id
19
26
  * @param props.onActive 激活面板变化回调
27
+ * @param props.visible 面板区是否可见(false = 只显示活动栏)
28
+ * @param props.onShow 重新展开面板区回调(活动栏图标点击/底部按钮)
20
29
  * @param props.width 面板区宽度
21
30
  * @param props.onWidth 宽度变化回调
22
- * @param props.onHide 收起侧边栏回调
31
+ * @param props.onHide 收起面板区回调
32
+ * @param props.minWidth 最小宽度(通用设置 sidebarMinWidth;拖拽低于它触发隐藏)
23
33
  */
24
34
  export function SidebarView(props) {
25
35
  const panels = props.registry?.list?.() ?? []
26
36
  const ctx = props.ctx
27
37
  const activePanel = props.activePanel
28
38
  const onActive = props.onActive
39
+ const visible = props.visible !== false
40
+ const onShow = props.onShow
29
41
  const width = props.width
30
42
  const onWidth = props.onWidth
31
43
  const onHide = props.onHide
44
+ const wMin = Math.max(120, Math.min(W_MAX, Math.round(Number(props.minWidth) || 300)))
32
45
 
33
46
  const active = panels.find((p) => p.id === activePanel) || panels[0]
34
47
 
@@ -36,14 +49,51 @@ export function SidebarView(props) {
36
49
  e.preventDefault()
37
50
  const startX = e.clientX
38
51
  const startW = width
52
+ const cleanup = () => {
53
+ window.removeEventListener('pointermove', onMove)
54
+ window.removeEventListener('pointerup', onUp)
55
+ }
56
+ const onUp = () => cleanup()
39
57
  const onMove = (ev) => {
40
- const w = Math.max(W_MIN, Math.min(W_MAX, startW + (ev.clientX - startX)))
41
- onWidth(w)
58
+ const raw = startW + (ev.clientX - startX)
59
+ // 低于最小宽度:清理监听并直接收起面板区(不夹住)
60
+ if (raw < wMin) {
61
+ cleanup()
62
+ onHide()
63
+ return
64
+ }
65
+ onWidth(Math.min(W_MAX, raw))
42
66
  }
43
- const onUp = () => {
67
+ window.addEventListener('pointermove', onMove)
68
+ window.addEventListener('pointerup', onUp)
69
+
70
+ }
71
+
72
+ /**
73
+ * 隐藏态接缝拖拽:向右拖过阈值把面板区拉出来(以记忆宽度展开),继续拖动实时调宽。
74
+ * @param e pointerdown 事件
75
+ */
76
+ const onSeamPointerDown = (e) => {
77
+ e.preventDefault()
78
+ const startX = e.clientX
79
+ const baseW = Math.max(wMin, Math.min(W_MAX, Math.round(Number(width) || wMin)))
80
+ let expanded = false
81
+ let expandStartX = 0
82
+ const cleanup = () => {
44
83
  window.removeEventListener('pointermove', onMove)
45
84
  window.removeEventListener('pointerup', onUp)
46
85
  }
86
+ const onUp = () => cleanup()
87
+ const onMove = (ev) => {
88
+ if (!expanded) {
89
+ if (ev.clientX - startX < PULL_OUT_THRESHOLD) return
90
+ expanded = true
91
+ expandStartX = ev.clientX
92
+ onShow()
93
+ return
94
+ }
95
+ onWidth(Math.max(wMin, Math.min(W_MAX, baseW + (ev.clientX - expandStartX))))
96
+ }
47
97
  window.addEventListener('pointermove', onMove)
48
98
  window.addEventListener('pointerup', onUp)
49
99
  }
@@ -54,20 +104,37 @@ export function SidebarView(props) {
54
104
  const count = typeof p.badge === 'function' ? p.badge(ctx) : null
55
105
  return React.createElement('button', {
56
106
  key: p.id,
57
- className: 'edrv-rail-btn' + (active && p.id === active.id ? ' edrv-rail-on' : ''),
107
+ className: 'edrv-rail-btn' + (visible && active && p.id === active.id ? ' edrv-rail-on' : ''),
58
108
  title: p.title,
59
- onClick: () => onActive(p.id),
109
+ onClick: () => {
110
+ onActive(p.id)
111
+ // 面板区隐藏时点击图标 = 展开到该面板(VSCode 活动栏行为)
112
+ if (!visible && typeof onShow === 'function') onShow()
113
+ },
60
114
  },
61
115
  React.createElement('span', { className: 'edrv-rail-icon' }, p.icon),
62
116
  count > 0
63
117
  ? React.createElement('span', { className: 'edrv-rail-badge' }, String(count))
64
118
  : null)
65
119
  })),
66
- React.createElement('button', { className: 'edrv-rail-btn', title: '隐藏侧边栏 (Ctrl+B)', onClick: onHide }, '◀'))
120
+ React.createElement('button', {
121
+ className: 'edrv-rail-btn',
122
+ title: (visible ? '隐藏面板区' : '展开面板区') + ' (Ctrl+B)',
123
+ onClick: () => (visible ? onHide() : onShow?.()),
124
+ }, visible ? '◀' : '▶'))
67
125
 
68
- return React.createElement('div', { className: 'edrv-sidebar', style: { width } },
126
+ return React.createElement('div', {
127
+ className: 'edrv-sidebar' + (visible ? '' : ' edrv-sidebar-rail-only'),
128
+ // maxWidth 兜底:持久化宽度超过容器可用空间时也不允许布局宽溢出(配合 flex-shrink:1 收缩)
129
+ style: { width: visible ? width : RAIL_ONLY_W + SEAM_W, maxWidth: '100%' },
130
+ },
69
131
  rail,
70
- React.createElement('div', { className: 'edrv-side-body' },
71
- active ? active.render(ctx) : null),
72
- React.createElement('div', { className: 'edrv-side-resize', onPointerDown: onPointerDown }))
132
+ visible
133
+ ? React.createElement('div', { className: 'edrv-side-body' },
134
+ active ? active.render(ctx) : null)
135
+ : null,
136
+ visible
137
+ ? React.createElement('div', { className: 'edrv-side-resize', onPointerDown: onPointerDown })
138
+ // 隐藏态接缝:悬浮显示拖拽光标与高亮,向右拖把面板区拉出来
139
+ : React.createElement('div', { className: 'edrv-side-resize', title: '拖拽展开面板区', onPointerDown: onSeamPointerDown }))
73
140
  }