dsh-vscode-mode 0.1.62 → 0.2.0

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.
@@ -5,10 +5,11 @@
5
5
  * host 树索引,命中近乎零成本);挂载/切会话从缓存即时恢复展开树;⟳/edrv:refresh
6
6
  * 保留旧条目强制后台重列(force);每次用户发起加载后预取 ≤4 个子目录(排除重型
7
7
  * 目录、缓存新鲜跳过、不级联);10s 轻量跟随已展开目录(命中索引,RPC 近零成本)。
8
- * 差异角标/右键菜单/展开状态持久化行为不变。
9
- * 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31
8
+ * 差异角标/右键菜单/展开状态持久化行为不变;行图标用官方原语(文件夹图标 + 文件类型图标)。
9
+ * 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31 / 2026-09-10
10
10
  */
11
11
  import React from 'react'
12
+ import { FileTypeIcon, IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, classifyFileType } from '@deepseek-ai/dsh-client-ui-primitives'
12
13
  import { rpc } from '../../rpc.js'
13
14
  import { ContextMenu } from '../../ui/ContextMenu.js'
14
15
  import { buildTreeMenu } from '../contextMenu.js'
@@ -23,6 +24,58 @@ const FOLLOW_INTERVAL_MS = 10_000
23
24
  const PREFETCH_MAX = 4
24
25
  const PREFETCH_EXCLUDED = new Set(['node_modules', '.git', '.hg', '.svn', '.pnpm', '.pnpm-store'])
25
26
 
27
+ // --region 行图标(官方原语:目录文件夹图标 + 文件类型图标;缺失时回落纯文本)
28
+
29
+ /**
30
+ * 目录图标元素(官方 IconFolderOpen16/Close16)。
31
+ * @author ddj 2026年09月10号
32
+ * @param isOpen 目录是否展开
33
+ * @returns 图标元素;原语缺失返回 null
34
+ */
35
+ function dirIconEl(isOpen) {
36
+ const Icon = isOpen ? IconFolderOpen16 : IconFolderClose16
37
+ if (typeof Icon !== 'function') return null
38
+ return React.createElement(Icon, { size: 16, className: 'edrv-tree-icon' })
39
+ }
40
+
41
+ /**
42
+ * 文件图标元素(官方 FileTypeIcon + classifyFileType 分类,未知类型为 other)。
43
+ * @author ddj 2026年09月10号
44
+ * @param name 文件名(含扩展名)
45
+ * @returns 图标元素;原语缺失或分类异常返回 null
46
+ */
47
+ function fileIconEl(name) {
48
+ if (typeof FileTypeIcon !== 'function' || typeof classifyFileType !== 'function') return null
49
+ try {
50
+ return React.createElement(FileTypeIcon, { kind: classifyFileType(String(name ?? '')), size: 16, className: 'edrv-tree-icon' })
51
+ } catch (error) {
52
+ return null
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 行图标分派:目录 → 文件夹图标,文件 → 官方类型图标。
58
+ * @author ddj 2026年09月10号
59
+ * @param e 目录条目(name/type)
60
+ * @param isDir 是否目录
61
+ * @param isOpen 目录是否展开
62
+ * @returns 图标元素或 null
63
+ */
64
+ function rowIconEl(e, isDir, isOpen) {
65
+ return isDir ? dirIconEl(isOpen) : fileIconEl(e?.name)
66
+ }
67
+
68
+ /**
69
+ * 刷新按钮图标(官方 IconRefreshOutline16;原语缺失回落文本 ⟳)。
70
+ * @author ddj 2026年09月10号
71
+ * @returns 图标元素或回落文本
72
+ */
73
+ function refreshIconEl() {
74
+ if (typeof IconRefreshOutline16 !== 'function') return '⟳'
75
+ return React.createElement(IconRefreshOutline16, { size: 14 })
76
+ }
77
+ // --endregion
78
+
26
79
  /**
27
80
  * 目录树面板主体(SWR:有缓存先渲染,无缓存才显示加载态;加载总在后台)。
28
81
  * @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath/fileMenuItems/notify)
@@ -227,8 +280,9 @@ export function FileExplorer(props) {
227
280
  },
228
281
  React.createElement('span', { className: 'edrv-tree-chev' },
229
282
  isDir ? (isOpen ? '▾' : '▸') : ''),
283
+ rowIconEl(e, isDir, isOpen),
230
284
  React.createElement('span', { className: 'edrv-tree-name' + (dim ? ' edrv-tree-dim' : '') },
231
- isDir ? (isOpen ? '📂' : '📁') : '📄', ' ', e.name),
285
+ e.name),
232
286
  (pending > 0
233
287
  ? React.createElement('span', { className: 'edrv-tree-badge' }, String(pending))
234
288
  : null))
@@ -283,7 +337,7 @@ export function FileExplorer(props) {
283
337
  React.createElement('span', { className: 'edrv-side-title' }, '资源管理器'),
284
338
  React.createElement('span', { className: 'edrv-side-root', title: root || '' }, rootName),
285
339
  React.createElement('span', { style: { flex: 1 } }),
286
- React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: refresh }, '⟳')),
340
+ React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: refresh }, refreshIconEl())),
287
341
  React.createElement('div', { className: 'edrv-tree' },
288
342
  (errorText
289
343
  ? React.createElement('div', { className: 'edrv-tree-error' },
@@ -1,9 +1,11 @@
1
1
  // @ts-nocheck
2
2
  /**
3
3
  * dsh-vscode-mode client — 侧边栏面板定义(文件管理 + 搜索 + 规则)。
4
- * 作者 ddj 2026-08-26 / 2026-09-03
4
+ * 活动栏图标用官方 UI 原语(跟随 DSH 主题与皮肤);原语缺失时由 SidebarView 回落文本渲染。
5
+ * 作者 ddj 2026-08-26 / 2026-09-03 / 2026-09-10
5
6
  */
6
7
  import React from 'react'
8
+ import { IconFolderOpenOutline16, IconListPenOutline16, IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
7
9
  import { FileExplorer } from './FileExplorer.js'
8
10
  import { SearchPanel } from './SearchPanel.js'
9
11
  import { RulesPanel } from './RulesPanel.js'
@@ -11,14 +13,14 @@ import type { SidebarPanelDef, SidebarCtx } from '../types.js'
11
13
 
12
14
  /**
13
15
  * 构造文件管理面板定义。
14
- * @author ddj 2026年08月26号
15
- * @returns 面板定义(含活动栏徽标 = 差异文件总数)
16
+ * @author ddj 2026年08月26号 / 2026年09月10号
17
+ * @returns 面板定义(含活动栏徽标 = 差异文件总数;图标 = 官方 IconFolderOpenOutline16)
16
18
  */
17
19
  export function createFilePanel(): SidebarPanelDef {
18
20
  return {
19
21
  id: 'explorer',
20
22
  title: '文件管理',
21
- icon: '📁',
23
+ icon: IconFolderOpenOutline16,
22
24
  order: 10,
23
25
  badge: (ctx: SidebarCtx) => (ctx.sum && ctx.sum.totalFiles > 0 ? ctx.sum.totalFiles : null),
24
26
  render: (ctx: SidebarCtx) => React.createElement(FileExplorer, { ctx }),
@@ -27,14 +29,14 @@ export function createFilePanel(): SidebarPanelDef {
27
29
 
28
30
  /**
29
31
  * 构造搜索面板定义。
30
- * @author ddj 2026年08月30号
31
- * @returns 面板定义(无徽标;活动栏图标 🔍)
32
+ * @author ddj 2026年08月30号 / 2026年09月10号
33
+ * @returns 面板定义(无徽标;图标 = 官方 IconSearchOutline16)
32
34
  */
33
35
  export function createSearchPanel(): SidebarPanelDef {
34
36
  return {
35
37
  id: 'search',
36
38
  title: '搜索',
37
- icon: '🔍',
39
+ icon: IconSearchOutline16,
38
40
  order: 15,
39
41
  render: (ctx: SidebarCtx) => React.createElement(SearchPanel, { ctx }),
40
42
  }
@@ -42,14 +44,14 @@ export function createSearchPanel(): SidebarPanelDef {
42
44
 
43
45
  /**
44
46
  * 构造规则管理面板定义(Codebuddy 规则形态:用户规则/项目规则双 Tab + 开关)。
45
- * @author ddj 2026年09月03号
46
- * @returns 面板定义(无徽标;活动栏图标 📏,与大纲 📜 区分)
47
+ * @author ddj 2026年09月03号 / 2026年09月10号
48
+ * @returns 面板定义(无徽标;图标 = 官方 IconListPenOutline16)
47
49
  */
48
50
  export function createRulesPanel(): SidebarPanelDef {
49
51
  return {
50
52
  id: 'rules',
51
53
  title: '规则',
52
- icon: '📏',
54
+ icon: IconListPenOutline16,
53
55
  order: 20,
54
56
  render: (ctx: SidebarCtx) => React.createElement(RulesPanel, { ctx }),
55
57
  }
@@ -40,8 +40,8 @@ export interface SidebarCtx {
40
40
  export interface SidebarPanelDef {
41
41
  id: string
42
42
  title: string
43
- /** 活动栏图标(文本/SVG 均可,横向居中显示)。 */
44
- icon: string
43
+ /** 活动栏图标:官方图标组件(跟随官方主题)或文本/emoji(回落),横向居中显示。 */
44
+ icon: string | ((props: { size?: number }) => unknown)
45
45
  order?: number
46
46
  /** 活动栏徽标:返回数字/null;缺省不显示。 */
47
47
  badge?: (ctx: SidebarCtx) => number | null
@@ -1,7 +1,9 @@
1
1
  /* dsh-vscode-mode 编辑区样式(原 ensureEdrvStyles 字符串,落盘为真 .css,由 tsdown CSS-inline 注入) */
2
- /* 浅色主题(对齐参考图:#f8f8f8 近白底 + 薄荷绿药丸 + 青绿文字 + 三文鱼 undo + 浅灰边框):
3
- [data-edrv-view] 作用域内重定义 --dsw-alias-*,级联命中组件内联样式与 CSS。 */
4
- [data-edrv-view] { box-sizing: border-box; --edrv-tab-h: 34px; --dsw-alias-bg-base: #f8f8f8; --dsw-alias-bg-layer-1: #ffffff; --dsw-alias-bg-layer-2: #f0f4f4; --dsw-alias-bg-overlay: #ffffff; --dsw-alias-label-primary: #1f2933; --dsw-alias-label-secondary: #52606d; --dsw-alias-label-tertiary: #9aa5b1; --dsw-alias-border-l1: #e0e6e8; --dsw-alias-border-l2: #cbd2d9; --dsw-alias-brand-primary: #0f9d58; --dsw-alias-state-success-primary: #0f9d58; --dsw-alias-state-error-primary: #d9534f; --dsw-alias-state-warn-primary: #d97706; --dsw-alias-interactive-bg-hover: rgba(15,157,88,.08); }
2
+ /* 主题:跟随官方 --dsw-alias-* 令牌(DSH 明暗与第三方皮肤生效)。
3
+ 本作用域不再重定义官方令牌(旧实现曾在此硬覆盖浅色盘,导致插件不跟随 DSH 主题);
4
+ 旧环境/缺令牌场景由每条声明的 var(--dsw-alias-*, 回落值) 兜底。
5
+ @author ddj 2026-09-10 */
6
+ [data-edrv-view] { box-sizing: border-box; --edrv-tab-h: 34px; }
5
7
 
6
8
  [data-edrv-view] .edrv-tabs { display: flex; align-items: center; gap: 2px; padding: 2px 8px 0; background: var(--dsw-alias-bg-layer-1, transparent); overflow-x: auto; scrollbar-width: thin; flex-shrink: 0; }
7
9
  [data-edrv-view] .edrv-tab { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 8px 0 12px; border: none; border-radius: 6px 6px 0 0; background: transparent; color: var(--dsw-alias-label-secondary, #aaa); font-size: 12px; cursor: pointer; white-space: nowrap; flex-shrink: 0; }
@@ -9,7 +11,7 @@
9
11
  [data-edrv-view] .edrv-tab.edrv-tab-active { background: var(--dsw-alias-bg-base, #111); color: var(--dsw-alias-label-primary, #eee); box-shadow: inset 0 -2px 0 var(--dsw-alias-brand-primary, #4f8cff); }
10
12
  [data-edrv-view] .edrv-tab .edrv-tab-star { color: var(--dsw-alias-brand-primary, #4f8cff); font-size: 13px; font-weight: 700; line-height: 1; flex-shrink: 0; } /* 未保存修改星号 @author ddj 2026年09月09号 */
11
13
  [data-edrv-view] .edrv-tab .edrv-tab-x { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 4px; color: var(--dsw-alias-label-tertiary, #888); font-size: 11px; }
12
- [data-edrv-view] .edrv-tab .edrv-tab-x:hover { background: rgba(255,255,255,.12); color: var(--dsw-alias-label-primary, #eee); }
14
+ [data-edrv-view] .edrv-tab .edrv-tab-x:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.12)); color: var(--dsw-alias-label-primary, #eee); }
13
15
  [data-edrv-view] .edrv-tab-add { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-left: 2px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, #aaa); font-size: 14px; cursor: pointer; flex-shrink: 0; }
14
16
  [data-edrv-view] .edrv-tab-add:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
15
17
  [data-edrv-view] .edrv-pathbar { display: flex; align-items: center; gap: 8px; height: 26px; padding: 0 10px; background: var(--dsw-alias-bg-layer-1, #ffffff); border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); font-size: 12px; flex-shrink: 0; overflow: hidden; }
@@ -27,7 +29,7 @@
27
29
  [data-edrv-view] .edrv-search-item .n { color: var(--dsw-alias-label-primary, #eee); font-size: 12px; word-break: break-all; }
28
30
  [data-edrv-view] .edrv-search-item .d { color: var(--dsw-alias-label-tertiary, #888); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
29
31
  [data-edrv-view] .edrv-search-empty { padding: 8px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #888); }
30
- [data-edrv-view] .edrv-btn-mini { font-size: 11px; padding: 2px 9px; border: none; border-radius: 999px; cursor: pointer; color: #fff; font-weight: 600; white-space: nowrap; }
32
+ [data-edrv-view] .edrv-btn-mini { font-size: 11px; padding: 2px 9px; border: none; border-radius: 999px; cursor: pointer; color: var(--dsw-alias-label-primary-inverted, #fff); font-weight: 600; white-space: nowrap; }
31
33
  [data-edrv-view] .edrv-btn-keep { background: var(--dsw-alias-state-success-primary, #2e9e44); }
32
34
  [data-edrv-view] .edrv-btn-undo { background: var(--dsw-alias-state-warn-primary, #d97706); }
33
35
  [data-edrv-view] .edrv-btn-mini:disabled { opacity: .45; cursor: default; }
@@ -47,15 +49,15 @@
47
49
  [data-edrv-view] .edrv-chip-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
48
50
  [data-edrv-view] .edrv-chip-btn:disabled { opacity: .35; cursor: default; }
49
51
  [data-edrv-view] .edrv-chip-btn:disabled:hover { background: transparent; }
50
- [data-edrv-view] .edrv-mn-add-line { background: rgba(15,157,88,.10); }
51
- [data-edrv-view] .edrv-mn-gutter-add { color: #0f9d58 !important; font-weight: 700; display: flex !important; align-items: center; justify-content: center; }
52
+ [data-edrv-view] .edrv-mn-add-line { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.10)); }
53
+ [data-edrv-view] .edrv-mn-gutter-add { color: var(--dsw-alias-state-success-primary, #0f9d58) !important; font-weight: 700; display: flex !important; align-items: center; justify-content: center; }
52
54
  [data-edrv-view] .edrv-mn-gutter-add::before { content: "+"; }
53
- [data-edrv-view] .edrv-del-zone { background: #fdeaea; border-top: 1px solid rgba(217,83,79,.35); border-bottom: 1px solid rgba(217,83,79,.35); box-shadow: inset 0 0 0 1px rgba(217,83,79,.12); }
54
- [data-edrv-view] .edrv-del-row { white-space: pre; font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 12.5px; line-height: 20px; color: #c0392b; }
55
+ [data-edrv-view] .edrv-del-zone { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217,83,79,.14)); border-top: 1px solid var(--dsw-alias-state-error-primary, rgba(217,83,79,.35)); border-bottom: 1px solid var(--dsw-alias-state-error-primary, rgba(217,83,79,.35)); box-shadow: inset 0 0 0 1px var(--dsw-alias-interactive-bg-hover-danger, rgba(217,83,79,.12)); }
56
+ [data-edrv-view] .edrv-del-row { white-space: pre; font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 12.5px; line-height: 20px; color: var(--dsw-alias-state-error-primary, #c0392b); }
55
57
  [data-edrv-view] .edrv-del-text { display: inline-block; white-space: pre; }
56
58
  [data-edrv-view] .edrv-minus-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; max-width: 100%; overflow: visible; pointer-events: none; z-index: 6; }
57
59
  [data-edrv-view] .edrv-minus-item { position: absolute; box-sizing: border-box; max-width: 100%; overflow: hidden; }
58
- [data-edrv-view] .edrv-minus-item::before { content: ''; position: absolute; left: 4px; top: 50%; width: 8px; height: 2px; transform: translateY(-50%); border-radius: 2px; background: #d9534f; }
60
+ [data-edrv-view] .edrv-minus-item::before { content: ''; position: absolute; left: 4px; top: 50%; width: 8px; height: 2px; transform: translateY(-50%); border-radius: 2px; background: var(--dsw-alias-state-error-primary, #d9534f); }
59
61
  [data-edrv-view] .edrv-monaco-host { flex: 1 1 auto; min-width: 0; min-height: 0; position: relative; overflow: hidden; }
60
62
  [data-edrv-view] .edrv-monaco-host > div { position: absolute; inset: 0; min-width: 0; min-height: 0; }
61
63
  [data-edrv-view] .edrv-imgview { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
@@ -83,9 +85,9 @@
83
85
  [data-edrv-view] .edrv-diffbar-count.edrv-count-file { min-width: 64px; cursor: default; }
84
86
  [data-edrv-view] .edrv-diffbar-file { flex: 1; min-width: 0; font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
85
87
  [data-edrv-view] .edrv-pill { font-size: 11px; padding: 3px 10px; border: none; border-radius: 999px; cursor: pointer; white-space: nowrap; font-weight: 600; }
86
- [data-edrv-view] .edrv-pill-keep { background: #e8f8e8; color: #0f7a45; border: 1px solid rgba(15,157,88,.4); }
87
- [data-edrv-view] .edrv-pill-undo { background: #fdeaea; color: #c0392b; border: 1px solid rgba(217,83,79,.4); }
88
- [data-edrv-view] .edrv-pill-ghost { background: #ffffff; color: var(--dsw-alias-label-secondary, #52606d); border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); }
88
+ [data-edrv-view] .edrv-pill-keep { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-state-success-primary, #0f7a45); border: 1px solid var(--dsw-alias-state-success-primary, rgba(15,157,88,.4)); }
89
+ [data-edrv-view] .edrv-pill-undo { background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217,83,79,.14)); color: var(--dsw-alias-state-error-primary, #c0392b); border: 1px solid var(--dsw-alias-state-error-primary, rgba(217,83,79,.4)); }
90
+ [data-edrv-view] .edrv-pill-ghost { background: var(--dsw-alias-bg-layer-1, #ffffff); color: var(--dsw-alias-label-secondary, #52606d); border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); }
89
91
  [data-edrv-view] .edrv-pill:disabled { opacity: .45; cursor: default; }
90
92
  [data-edrv-view] .edrv-diffbar-menu { position: absolute; right: 8px; bottom: calc(100% + 6px); z-index: 45; min-width: 160px; border-radius: 10px; background: var(--dsw-alias-bg-overlay, #ffffff); border: 1px solid var(--dsw-alias-border-l2, #cbd2d9); box-shadow: 0 6px 20px rgba(31,41,51,.16); padding: 4px; display: flex; flex-direction: column; gap: 2px; }
91
93
  [data-edrv-view] .edrv-diffmenu-item { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 6px 10px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; text-align: left; }
@@ -107,7 +109,7 @@
107
109
  [data-edrv-view] .edrv-diffrow-o { font-size: 11px; color: var(--dsw-alias-state-error-primary, #d9534f); font-family: var(--ds-font-family-code, ui-monospace, monospace); }
108
110
  [data-edrv-view] .edrv-diffrow-n { font-size: 11px; color: var(--dsw-alias-state-success-primary, #0f9d58); font-family: var(--ds-font-family-code, ui-monospace, monospace); }
109
111
  [data-edrv-view] .edrv-diffrow-tag { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
110
- [data-edrv-view] .edrv-diffrow-stale { border-color: rgba(217,119,6,.5); cursor: default; }
112
+ [data-edrv-view] .edrv-diffrow-stale { border-color: var(--dsw-alias-state-warn-primary, rgba(217,119,6,.5)); cursor: default; }
111
113
  [data-edrv-view] .edrv-diffbox-others { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 4px 2px 0; border-top: 1px solid var(--dsw-alias-border-l1, #e0e6e8); margin-top: 2px; }
112
114
  [data-edrv-view] .edrv-diffrow-file { font-size: 11px; padding: 2px 10px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); cursor: pointer; white-space: nowrap; max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
113
115
  [data-edrv-view] .edrv-diffrow-file:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); color: var(--dsw-alias-label-primary, #1f2933); }
@@ -228,8 +230,11 @@
228
230
  [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; }
229
231
  [data-edrv-view] .edrv-rail-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
230
232
  [data-edrv-view] .edrv-rail-btn.edrv-rail-on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.12)); color: var(--dsw-alias-label-primary, #1f2933); box-shadow: inset 2px 0 0 var(--dsw-alias-brand-primary, #0f9d58); }
231
- [data-edrv-view] .edrv-rail-icon { font-size: 15px; line-height: 1; }
232
- [data-edrv-view] .edrv-rail-badge { position: absolute; top: 2px; right: 2px; min-width: 14px; height: 14px; padding: 0 3px; border-radius: 999px; background: var(--dsw-alias-brand-primary, #0f9d58); color: #fff; font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; }
233
+ [data-edrv-view] .edrv-rail-icon { font-size: 15px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; }
234
+ /* 树行图标:官方原语(文件夹/文件类型),尺寸与行高对齐 @author ddj 2026-09-10 */
235
+ [data-edrv-view] .edrv-tree-icon { flex-shrink: 0; display: inline-flex; align-items: center; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
236
+ [data-edrv-view] .edrv-tree-row.edrv-tree-active .edrv-tree-icon { color: var(--dsw-alias-label-secondary, #52606d); }
237
+ [data-edrv-view] .edrv-rail-badge { position: absolute; top: 2px; right: 2px; min-width: 14px; height: 14px; padding: 0 3px; border-radius: 999px; background: var(--dsw-alias-brand-primary, #0f9d58); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; }
233
238
  [data-edrv-view] .edrv-side-body { flex: 1; min-width: 0; min-height: 0; display: flex; overflow: hidden; }
234
239
  [data-edrv-view] .edrv-side-panel { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; position: relative; }
235
240
  [data-edrv-view] .edrv-side-head { display: flex; align-items: center; gap: 8px; height: 34px; padding: 0 10px 0 12px; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); flex-shrink: 0; }
@@ -245,9 +250,9 @@
245
250
  [data-edrv-view] .edrv-tree-chev { width: 12px; flex-shrink: 0; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 10px; text-align: center; }
246
251
  [data-edrv-view] .edrv-tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
247
252
  [data-edrv-view] .edrv-tree-name.edrv-tree-dim { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
248
- [data-edrv-view] .edrv-tree-badge { min-width: 14px; height: 14px; padding: 0 4px; border-radius: 999px; background: var(--dsw-alias-state-warn-primary, #d97706); color: #fff; font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; flex-shrink: 0; }
253
+ [data-edrv-view] .edrv-tree-badge { min-width: 14px; height: 14px; padding: 0 4px; border-radius: 999px; background: var(--dsw-alias-state-warn-primary, #d97706); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; flex-shrink: 0; }
249
254
  [data-edrv-view] .edrv-tree-loading { height: 22px; line-height: 22px; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; }
250
- [data-edrv-view] .edrv-tree-error { display: flex; align-items: center; gap: 8px; margin: 4px 2px 6px; 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; }
255
+ [data-edrv-view] .edrv-tree-error { display: flex; align-items: center; gap: 8px; margin: 4px 2px 6px; padding: 6px 8px; border: 1px solid var(--dsw-alias-state-error-primary, rgba(217,83,79,.4)); border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217,83,79,.12)); color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
251
256
  /* 大纲面板:kind 字形 / detail / 行号 */
252
257
  [data-edrv-view] .edrv-outline-kind { display: inline-flex; align-items: center; justify-content: center; width: 14px; flex-shrink: 0; font-size: 12px; font-weight: 600; text-align: center; }
253
258
  [data-edrv-view] .edrv-outline-kind.edrv-ol-func { color: var(--dsw-alias-brand-primary, #0f9d58); }
@@ -295,10 +300,10 @@
295
300
  [data-edrv-view] .edrv-search-line:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
296
301
  [data-edrv-view] .edrv-search-ln { flex-shrink: 0; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; min-width: 26px; text-align: right; user-select: none; }
297
302
  [data-edrv-view] .edrv-search-text { overflow: hidden; text-overflow: ellipsis; white-space: pre; font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 11px; }
298
- [data-edrv-view] .edrv-search-hit { background: var(--dsw-alias-state-warn-primary, #e5b93f); color: #1f2933; border-radius: 2px; padding: 0 1px; }
303
+ [data-edrv-view] .edrv-search-hit { background: var(--dsw-alias-state-warn-primary, #e5b93f); color: var(--dsw-alias-label-primary-inverted, #1f2933); border-radius: 2px; padding: 0 1px; }
299
304
  [data-edrv-view] .edrv-search-empty { padding: 8px 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
300
- [data-edrv-view] .edrv-search-error { 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; }
301
- [data-edrv-view] .edrv-search-warn { padding: 6px 8px; border: 1px solid rgba(240,173,78,.45); border-radius: 6px; background: #fdf6e3; color: #b8860b; font-size: 11px; word-break: break-all; }
305
+ [data-edrv-view] .edrv-search-error { padding: 6px 8px; border: 1px solid var(--dsw-alias-state-error-primary, rgba(217,83,79,.4)); border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217,83,79,.12)); color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
306
+ [data-edrv-view] .edrv-search-warn { padding: 6px 8px; border: 1px solid var(--dsw-alias-state-warn-primary, rgba(240,173,78,.45)); border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover, rgba(240,173,78,.12)); color: var(--dsw-alias-state-warn-primary, #b8860b); font-size: 11px; word-break: break-all; }
302
307
 
303
308
  /* include/exclude filter rows (collapsible) */
304
309
  [data-edrv-view] .edrv-search-rows { display: flex; flex-direction: column; gap: 4px; padding: 2px 0 4px; }
@@ -333,20 +338,20 @@
333
338
  [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; }
334
339
  [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; }
335
340
  [data-edrv-view] .edrv-rules-type { flex-shrink: 0; height: 15px; padding: 0 5px; border-radius: 999px; font-size: 10px; line-height: 15px; }
336
- [data-edrv-view] .edrv-rules-type-always { background: rgba(15,157,88,.15); color: #0b7c46; }
337
- [data-edrv-view] .edrv-rules-type-auto { background: rgba(79,140,255,.16); color: #3367c9; }
341
+ [data-edrv-view] .edrv-rules-type-always { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.15)); color: var(--dsw-alias-state-success-primary, #0b7c46); }
342
+ [data-edrv-view] .edrv-rules-type-auto { background: var(--dsw-alias-interactive-bg-hover-accent, rgba(79,140,255,.16)); color: var(--dsw-alias-link, #3367c9); }
338
343
  [data-edrv-view] .edrv-rules-type-manual { background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-secondary, #52606d); }
339
344
  [data-edrv-view] .edrv-rules-offtag { font-size: 10px; color: var(--dsw-alias-label-tertiary, #9aa5b1); flex-shrink: 0; }
340
345
  [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; }
341
346
  [data-edrv-view] .edrv-rules-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; margin-left: auto; }
342
347
  [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; }
343
348
  [data-edrv-view] .edrv-rules-act:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
344
- [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; }
345
- [data-edrv-view] .edrv-rules-switch.on { background: #7c5cff; }
346
- [data-edrv-view] .edrv-rules-knob { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: #fff; transition: left .15s; }
349
+ [data-edrv-view] .edrv-rules-switch { position: relative; width: 30px; height: 16px; border-radius: 999px; border: none; background: var(--dsw-alias-border-l2, #b9c4cc); cursor: pointer; transition: background .15s; flex-shrink: 0; padding: 0; }
350
+ [data-edrv-view] .edrv-rules-switch.on { background: var(--dsw-alias-brand-primary, #7c5cff); }
351
+ [data-edrv-view] .edrv-rules-knob { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--dsw-alias-bg-layer-1, #fff); transition: left .15s; }
347
352
  [data-edrv-view] .edrv-rules-switch.on .edrv-rules-knob { left: 16px; }
348
353
  [data-edrv-view] .edrv-rules-empty { padding: 10px 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); line-height: 1.6; }
349
- [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; }
354
+ [data-edrv-view] .edrv-rules-err { padding: 6px 8px; border: 1px solid var(--dsw-alias-state-error-primary, rgba(217,83,79,.4)); border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217,83,79,.12)); color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; word-break: break-all; }
350
355
  [data-edrv-view] .edrv-rules-editor { display: flex; flex-direction: column; min-height: 0; flex: 1; gap: 6px; }
351
356
  [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; }
352
357
  [data-edrv-view] .edrv-rules-crumb { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -361,8 +366,22 @@
361
366
  [data-edrv-view] .edrv-rules-body:focus { border-color: var(--dsw-alias-brand-primary, #4f8cff); }
362
367
  [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; }
363
368
  [data-edrv-view] .edrv-rules-editor-foot { display: flex; justify-content: flex-end; flex: 0 0 auto; }
364
- [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; }
369
+ [data-edrv-view] .edrv-rules-save { height: 24px; padding: 0 14px; border: none; border-radius: 6px; background: var(--dsw-alias-brand-primary, #0f9d58); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 12px; cursor: pointer; }
365
370
  [data-edrv-view] .edrv-rules-save:disabled { opacity: .5; cursor: default; }
366
371
 
372
+ /* 命令栏(Ctrl+Shift+P / F1):body 级 portal 浮层,故不依赖编辑区几何 */
373
+ [data-edrv-view] .edrv-palette-mask { position: fixed; inset: 0; z-index: 190; background: rgba(0, 0, 0, .28); }
374
+ [data-edrv-view] .edrv-palette { position: fixed; top: 12vh; left: 50%; transform: translateX(-50%); z-index: 191; width: min(580px, calc(100vw - 32px)); max-height: 64vh; display: flex; flex-direction: column; border-radius: 10px; background: var(--dsw-alias-bg-overlay, #ffffff); border: 1px solid var(--dsw-alias-border-l2, #cbd2d9); box-shadow: 0 12px 34px rgba(31, 41, 51, .24); overflow: hidden; }
375
+ [data-edrv-view] .edrv-palette-input { height: 38px; flex: 0 0 auto; padding: 0 12px; border: none; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); background: transparent; color: var(--dsw-alias-label-primary, #1f2933); font-size: 13px; outline: none; }
376
+ [data-edrv-view] .edrv-palette-list { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 4px; }
377
+ [data-edrv-view] .edrv-palette-row { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 6px; font-size: 12px; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; }
378
+ [data-edrv-view] .edrv-palette-row.edrv-palette-sel { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 157, 88, .10)); }
379
+ [data-edrv-view] .edrv-palette-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
380
+ [data-edrv-view] .edrv-palette-cat { flex: 0 0 auto; font-size: 10px; padding: 1px 7px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); color: var(--dsw-alias-label-tertiary, #7b8794); }
381
+ [data-edrv-view] .edrv-palette-key { flex: 0 0 auto; font-family: var(--ds-font-family-code, ui-monospace, monospace); font-size: 10px; padding: 1px 6px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 5px; background: var(--dsw-alias-bg-layer-1, #f7f9fa); color: var(--dsw-alias-label-secondary, #52606d); }
382
+ [data-edrv-view] .edrv-palette-empty { padding: 14px 10px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #7b8794); text-align: center; }
383
+ [data-edrv-view] .edrv-palette-notice { flex: 0 0 auto; padding: 6px 10px; border-top: 1px solid var(--dsw-alias-border-l1, #e0e6e8); font-size: 11px; color: var(--dsw-alias-state-warn-primary, #b7791f); }
384
+
385
+
367
386
 
368
387
 
@@ -0,0 +1,96 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — ClaimRouter:官方 file 认领页签的中转正文。
4
+ * 官方为每个 `dsh-resource://file/**` 地址开一个认领页签;本组件只做转发:
5
+ * 解析地址 → 送进单一编辑器页签(文件分页由编辑器自带页签栏接管)→ 关闭本中转页签。
6
+ * 转发失败(seat 未就绪等瞬时态)按 3 次 ×150ms 重试;仍失败则就地渲染兜底正文
7
+ * (旧行为:每文件一套编辑器),保证退化路径不白屏。本组件自身不创建 Monaco 实例。
8
+ * 作者 ddj 2026年09月10号
9
+ */
10
+ import React from 'react'
11
+ import { forwardToEditor, parseOfficialFileAddress, resolveNavLine } from '../officialSidebar.js'
12
+ import { log } from '../log.js'
13
+
14
+ /** 转发尝试次数(含首次)与重试间隔。 */
15
+ const FORWARD_ATTEMPTS = 3
16
+ const FORWARD_DELAY_MS = 150
17
+
18
+ /**
19
+ * 读 slot 框架注入的 tab 记录(hook 缺失或抛错时返回 undefined)。
20
+ * @author ddj 2026年09月10号
21
+ * @param props slot 框架注入的组件 props
22
+ * @returns tab 记录(含 navigation 与 actions),或 undefined
23
+ */
24
+ function readTab(props) {
25
+ const useTabInfo = props?.useTabInfo
26
+ if (typeof useTabInfo !== 'function') return undefined
27
+ try {
28
+ return useTabInfo()?.tab
29
+ } catch {
30
+ return undefined
31
+ }
32
+ }
33
+
34
+ /**
35
+ * 关闭本中转页签(失败仅告警:目标文件已在编辑器打开,残留页签不阻塞功能)。
36
+ * @author ddj 2026年09月10号
37
+ * @param tab slot 框架注入的 tab 记录
38
+ */
39
+ function closeTab(tab) {
40
+ try {
41
+ tab?.actions?.close?.()
42
+ } catch (error) {
43
+ log.warn('中转页签关闭失败(' + String(error) + ')')
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 组装 file 认领转发正文。
49
+ * @author ddj 2026年09月10号
50
+ * @param spec 装配依赖(service 官方导航服务 / schedule 延时调度 / fallback 兜底正文)
51
+ * @returns 正文组件(注册进 keyed slot sidebar.right.pane.tab)
52
+ */
53
+ export function createClaimRouter(spec) {
54
+ const service = spec?.service
55
+ const schedule = spec?.schedule
56
+ const fallback = spec?.fallback
57
+
58
+ /**
59
+ * 中转正文:转发成功即关闭自身;重试耗尽后渲染兜底正文。
60
+ * @param props slot 框架注入(useTabInfo)+ 装配依赖
61
+ * @returns 兜底正文元素,或 null(转发中 / 已转发)
62
+ */
63
+ function ClaimRouter(props) {
64
+ const tab = readTab(props)
65
+ const navigation = tab?.navigation
66
+ const revision = typeof navigation?.revision === 'number' ? navigation.revision : -1
67
+ const [failed, setFailed] = React.useState(false)
68
+ const doneRef = React.useRef(-1)
69
+
70
+ React.useEffect(() => {
71
+ if (revision === doneRef.current) return
72
+ doneRef.current = revision
73
+ const parsed = parseOfficialFileAddress(navigation?.address)
74
+ if (!parsed) {
75
+ setFailed(true)
76
+ return
77
+ }
78
+ const line = resolveNavLine(navigation?.params)
79
+ let left = FORWARD_ATTEMPTS
80
+ const attempt = () => {
81
+ if (forwardToEditor(service, parsed.path, line)) {
82
+ closeTab(tab)
83
+ return
84
+ }
85
+ left -= 1
86
+ if (left > 0 && typeof schedule === 'function') schedule(attempt, FORWARD_DELAY_MS)
87
+ else setFailed(true)
88
+ }
89
+ attempt()
90
+ }, [revision])
91
+
92
+ if (!failed || typeof fallback !== 'function') return null
93
+ return React.createElement(fallback, props)
94
+ }
95
+ return ClaimRouter
96
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * dsh-vscode-mode client — 命令栏浮层(Ctrl+Shift+P / F1)。
3
+ * 订阅 commandPaletteStore 的开关状态;候选来自注入的指令注册表(window.dsh.edrvCommands),
4
+ * 过滤/排序复用 commandSearch 纯函数;↑↓ 选择、Enter 执行、Esc 或点击遮罩关闭。
5
+ * 浮层经 createPortal 渲染到 body,故不受编辑区三种形态(官方侧栏/better-sidebar/中央页签)影响;
6
+ * 宿主单实例由 store 的 claimPaletteHost 保证(另一个宿主自动返回 null)。
7
+ * 作者 ddj 2026年09月10号
8
+ */
9
+ import React from 'react'
10
+ import { createPortal } from 'react-dom'
11
+ import { REGISTRY_GLOBAL } from '../commandGlobals.js'
12
+ import { filterCommands } from '../commandSearch.js'
13
+ import { createCommandRegistry } from '../commandRegistry.js'
14
+ import type { CommandRegistry } from '../commandRegistry.js'
15
+ import { chordOf } from '../keybindings.js'
16
+ import {
17
+ claimPaletteHost, closeCommandPalette, isPaletteOpen, registryRef, releasePaletteHost,
18
+ runPaletteCommand, subscribePalette,
19
+ } from '../commandPaletteStore.js'
20
+ import type { CommandDef } from './commandCatalog.js'
21
+
22
+ /** 空注册表(宿主未注入时的安全降级:命令栏可开但无候选)。 */
23
+ let fallbackRegistry: CommandRegistry | null = null
24
+
25
+ /**
26
+ * 读取指令注册表:① 装配期存入的模块引用(首选,不依赖全局)
27
+ * ② window[REGISTRY_GLOBAL] 镜像(第三方/调试场景)③ 空表降级(仅装配异常时)。
28
+ * @author ddj 2026年09月10号
29
+ * @returns 指令注册表
30
+ */
31
+ function useRegistry(): CommandRegistry {
32
+ const injected = registryRef()
33
+ if (injected) return injected
34
+ const mirrored = typeof window === 'undefined'
35
+ ? undefined
36
+ : (window as unknown as Record<string, CommandRegistry | undefined>)[REGISTRY_GLOBAL]
37
+ if (mirrored) return mirrored
38
+ if (!fallbackRegistry) fallbackRegistry = createCommandRegistry()
39
+ return fallbackRegistry
40
+ }
41
+
42
+ /** 命令栏每一行的展示信息。 */
43
+ interface PaletteRow {
44
+ command: CommandDef
45
+ chord: string | null
46
+ }
47
+
48
+ /**
49
+ * 组装候选行(先按可用性过滤,再按查询排序)。
50
+ * @author ddj 2026年09月10号
51
+ * @param registry 指令注册表
52
+ * @param query 用户输入
53
+ * @returns 候选行
54
+ */
55
+ function rowsOf(registry: CommandRegistry, query: string): PaletteRow[] {
56
+ return filterCommands(registry.available(), query)
57
+ .map((command) => ({ command, chord: chordOf(command.id) }))
58
+ }
59
+
60
+ /**
61
+ * 命令栏浮层(未展开/非宿主时渲染 null)。
62
+ * @author ddj 2026年09月10号
63
+ * @returns 浮层 React 元素或 null
64
+ */
65
+ export function CommandPalette(): React.ReactElement | null {
66
+ const open = React.useSyncExternalStore(subscribePalette, isPaletteOpen)
67
+ const registry = useRegistry()
68
+ const [token] = React.useState(() => (claimPaletteHost() ? {} : null))
69
+ const [query, setQuery] = React.useState('')
70
+ const [selected, setSelected] = React.useState(0)
71
+ const [notice, setNotice] = React.useState('')
72
+ const inputRef = React.useRef<HTMLInputElement | null>(null)
73
+ const host = token !== null
74
+
75
+ React.useEffect(() => () => releasePaletteHost(token), [token])
76
+ React.useEffect(() => {
77
+ if (!open) return undefined
78
+ setQuery('')
79
+ setSelected(0)
80
+ setNotice('')
81
+ const timer = setTimeout(() => inputRef.current?.focus(), 0)
82
+ return () => clearTimeout(timer)
83
+ }, [open])
84
+
85
+ const rows = React.useMemo(
86
+ () => (open && host ? rowsOf(registry, query) : []),
87
+ [open, host, registry, query],
88
+ )
89
+ const active = rows.length ? Math.min(selected, rows.length - 1) : 0
90
+
91
+ React.useEffect(() => {
92
+ if (!open) return
93
+ const node = document.querySelector('.edrv-palette-row.edrv-palette-sel')
94
+ const scroll = (node as HTMLElement | null)?.scrollIntoView
95
+ if (typeof scroll === 'function') scroll.call(node, { block: 'nearest' })
96
+ }, [open, active, rows.length])
97
+
98
+ /** 执行候选行(执行器内部会做可用性校验与异常上报)。 */
99
+ const execute = (row: PaletteRow | undefined): void => {
100
+ if (!row) return
101
+ closeCommandPalette()
102
+ if (!runPaletteCommand(row.command.id)) setNotice('命令未执行:' + row.command.label)
103
+ }
104
+
105
+ const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
106
+ event.stopPropagation()
107
+ if (event.key === 'Escape') {
108
+ event.preventDefault()
109
+ closeCommandPalette()
110
+ return
111
+ }
112
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
113
+ event.preventDefault()
114
+ if (!rows.length) return
115
+ const step = event.key === 'ArrowDown' ? 1 : rows.length - 1
116
+ setSelected((value) => (Math.min(value, rows.length - 1) + step) % rows.length)
117
+ return
118
+ }
119
+ if (event.key === 'Enter') {
120
+ event.preventDefault()
121
+ execute(rows[active])
122
+ }
123
+ }
124
+
125
+ if (!open || !host || typeof document === 'undefined') return null
126
+ const list = rows.length
127
+ ? rows.map((row, index) => React.createElement('div', {
128
+ key: row.command.id,
129
+ className: 'edrv-palette-row' + (index === active ? ' edrv-palette-sel' : ''),
130
+ onMouseEnter: () => setSelected(index),
131
+ onClick: () => execute(row),
132
+ },
133
+ React.createElement('span', { className: 'edrv-palette-label' }, row.command.label),
134
+ React.createElement('span', { className: 'edrv-palette-cat' }, row.command.category),
135
+ row.chord ? React.createElement('kbd', { className: 'edrv-palette-key' }, row.chord) : null))
136
+ : React.createElement('div', { className: 'edrv-palette-empty' }, '无匹配命令')
137
+
138
+ const panel = React.createElement('div', { className: 'edrv-palette', 'data-edrv-palette': '1', onKeyDown },
139
+ React.createElement('input', {
140
+ ref: inputRef,
141
+ className: 'edrv-palette-input',
142
+ value: query,
143
+ placeholder: '输入命令名称…(↑↓ 选择 · Enter 执行 · Esc 关闭)',
144
+ spellCheck: false,
145
+ onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
146
+ setQuery(event.target.value)
147
+ setSelected(0)
148
+ setNotice('')
149
+ },
150
+ }),
151
+ React.createElement('div', { className: 'edrv-palette-list' }, list),
152
+ notice ? React.createElement('div', { className: 'edrv-palette-notice' }, notice) : null)
153
+
154
+ const overlay = React.createElement('div', { 'data-edrv-view': '1' },
155
+ React.createElement('div', { className: 'edrv-palette-mask', onClick: () => closeCommandPalette() }),
156
+ panel)
157
+ return createPortal(overlay, document.body)
158
+ }