dsh-panel-control 0.8.3 → 0.9.2

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 CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  - **排序** — 拖动行首把手任意换位;一键按名称 A→Z;一键恢复注册时的默认顺序。
8
8
  - **隐藏** — 行右侧的眼睛图标。隐藏项仍留在列表里,随时放回;不影响插件是否加载,也不写插件自己的配置。
9
+ - **重命名** — 行右侧的铅笔图标把名称改成你熟悉的叫法,`Enter` 或失焦保存,`Esc` 取消,清空即恢复原名。改名只改侧栏显示,不动插件自己的配置;宽侧栏下改过名的行会在最右侧显示插件包名小标签(省略 `dsh-` 前缀,可在「侧栏布局」里关掉),避免时间久了忘记它原本是谁。
9
10
  - **分组** — 新建分组(如「创作」「系统」)并把入口分进去,侧栏会在组与组之间插入带标题的分隔线。组的先后由设置里的分组列表拖动决定,与注册顺序、分类结果无关。
10
11
  - **分组折叠** — 点分组标题收起该组,只留标题与计数;`Enter` / 空格同样可切换,状态写进配置。侧栏收成图标条时不显示标题,也不折叠。
11
12
  - **侧栏溢出兜底** — 导航项变多时,官方侧栏会把下方的会话/工作区列表压到 0 高且无法滚动([讨论 #12083](https://github.com/deepseek-ai/deepseek-harness/discussions/12083))。本插件给导航列表加上限高与滚动,给下方区域留最小高度。
@@ -25,13 +26,17 @@
25
26
 
26
27
  补出来的设置页入口也出现在同一个侧栏列表里,排在普通入口之后、本插件自己之前;标题沿用设置页自己的名称,重复时附带设置页 id 以区分。
27
28
 
29
+ 改过名的入口仍把原来的注册名留在按钮属性里,引擎靠它把自己映射回同一条注册,所以宿主的排序、重渲染和后续重扫都不受影响;行尾标签显示的是插件包名(由 Host 从安装目录的 `package.json` 解析,`dsh-` 前缀已省略),屏幕阅读器与工具提示读到的则是你设定的显示名。
30
+
28
31
  ## 安装
29
32
 
33
+ 要求 DeepSeek Harness `>= 0.2.1-rc.3`(更旧的版本缺少本插件依赖的槽与模块图接口,宿主会拒绝加载)。
34
+
30
35
  从 npm 安装(推荐):
31
36
 
32
37
  ```bash
33
38
  cd C:/Users/<you>/.dsh/profiles/desktop
34
- npm install dsh-panel-control@0.8.3
39
+ npm install dsh-panel-control@0.9.2
35
40
  ```
36
41
 
37
42
  把包名加进同一目录 `package.json` 的 `dsh.profile.bundles`:
@@ -68,11 +73,12 @@ cmd //c mklink /J node_modules\dsh-panel-control D:\Github\dsh-panel-control
68
73
  - 高度兜底开关,以及 35 / 45 / 55 / 不限 四档上限;
69
74
  - 分组折叠开关,以及「全部收起 / 全部展开」;
70
75
  - 设置页入口开关(默认开)。关掉后补出来的行会连同注册一起撤掉,不只是藏起来;
76
+ - 包名标签开关(默认开)。关掉后改名仍然生效,只是行尾不再显示包名;
71
77
  - 折叠状态按分组 id 记在配置里,重启后保留。
72
78
 
73
79
  ## 配置与存储
74
80
 
75
- 布局写在 Host 侧:`~/.dsh/panel-control/prefs.json`(`~` 是这个 profile 的用户目录)。包含顺序、隐藏项、分组、折叠状态、布局开关、设置页入口开关与分类结果。
81
+ 布局写在 Host 侧:`~/.dsh/panel-control/prefs.json`(`~` 是这个 profile 的用户目录)。包含顺序、隐藏项、分组、自定义名称、折叠状态、布局开关、设置页入口开关、包名标签开关与分类结果。
76
82
 
77
83
  页面 origin 是 Host 的 loopback 端口,每次启动都会换端口,`localStorage` 会因此"看起来每次清空"——所以真实存储放在 Host 侧,`localStorage` 只留镜像;读不到 Host 时才回退到镜像。配置按安装维度生效,不跟随工作区。
78
84
 
@@ -86,7 +92,7 @@ cmd //c mklink /J node_modules\dsh-panel-control D:\Github\dsh-panel-control
86
92
  ## 开发与测试
87
93
 
88
94
  ```bash
89
- npm run check # 语法检查 + 宿主路由 smoke + 布局/桥接静态与行为断言
95
+ npm run check # 语法检查 + 宿主路由 smoke + 布局/桥接/改名静态与行为断言
90
96
  npm run smoke # 只跑宿主路由用例
91
97
  npm run doctor # 体检 profile 安装状态
92
98
  ```
@@ -95,14 +101,16 @@ npm run doctor # 体检 profile 安装状态
95
101
  - `scripts/host-smoke.mjs`:驱动宿主侧路由,覆盖设置读写、分类请求与 bundle 兜底。
96
102
  - `scripts/layout-smoke.mjs`:断言侧栏布局的 CSS 钩子与关键数值,并校验 `BUILD_STAMP` 与版本号一致。
97
103
  - `scripts/bridge-smoke.mjs`:在假 ctx 上跑 `client.js`,断言只给「有设置页、没有侧栏入口」的第三方插件补行,且成对注册 main、开关关闭后整体撤回、桥接页经自有子槽交给宿主渲染并能返回对话。
104
+ - `scripts/names-smoke.mjs`:用迷你 DOM 驱动真实引擎,断言改名覆盖宿主标题与 `aria-label`、包名标签的位置与开关、清空恢复、窄侧栏不出标签、重复名称仍能按位置映射、未知包不显示标签,以及 `dispose` 后 DOM 原样归还。
98
105
 
99
- 进一步材料:[docs/sidebar-layout.md](docs/sidebar-layout.md)(布局与折叠实现)、[docs/bridged-settings-pages.md](docs/bridged-settings-pages.md)(设置页补入口的识别与渲染)、[docs/ui-design-notes.md](docs/ui-design-notes.md)(界面规范与踩坑)、[docs/discussion-bundle-evaluated-twice.md](docs/discussion-bundle-evaluated-twice.md)(bundle 被求值两次的事故复盘)。
106
+ 进一步材料:[docs/sidebar-layout.md](docs/sidebar-layout.md)(布局与折叠实现)、[docs/renaming-panels.md](docs/renaming-panels.md)(重命名与包名标签)、[docs/bridged-settings-pages.md](docs/bridged-settings-pages.md)(设置页补入口的识别与渲染)、[docs/ui-design-notes.md](docs/ui-design-notes.md)(界面规范与踩坑)、[docs/discussion-bundle-evaluated-twice.md](docs/discussion-bundle-evaluated-twice.md)(bundle 被求值两次的事故复盘)。
100
107
 
101
108
  ## 边界
102
109
 
103
110
  - 自动分类会把入口名称发给当前 profile 配置的模型服务商;不想要就关掉开关,分组仍可手动编辑。
104
111
  - 分类结果只是建议,可以整体重排或改名。
105
112
  - 依赖宿主侧栏的 DOM 结构(`[class*="panelList"]` 与面板按钮的 `aria-label`),官方大改版时需要跟进。
113
+ - 重命名只覆盖侧栏的显示层:宿主标题文本、`aria-label` 与包名标签都是本插件写的,插件自己的配置和入口 id 不变;宿主重渲染后由下一次扫描补回。图标条模式没有标题文本,只改可访问名称,也不显示包名标签。包名由 Host 从模块所在安装目录的 `package.json` 解析(模块 id 不等于包名,例如 `dsh-enhance-ui` → `@wufeng0105/dsh-enhance`),接口不可用或解析不到时该行不显示标签。
106
114
  - 只写 `style.order`、`style.display` 与 `data-dpc-*` 属性,不移动 React 节点,不改宿主代码。
107
115
  - 排序、隐藏、分组都只影响本机这份安装。
108
116
  - 设置页入口按「section id / label 是否已被某个面板占用」识别归属,不依赖插件声明的注册者(client runner 会把多个插件报成同一个名字);代价是启发式:与面板同名的第三方设置页会被视为已有入口,新增内置设置页需要更新 `CORE_SECTION_IDS`。第三方设置页按宿主渲染器原位渲染,若它自己依赖设置弹窗的私有上下文,可能出现布局差异。
package/client.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // Everything below therefore lives inside this closure. Re-evaluation must stay possible: when the
6
6
  // plugin is switched off the loader drops our factory, so switching it back on re-executes this
7
7
  // script and needs the registration that follows.
8
- const BUILD_STAMP = 'dsh-panel-control/0.8.3';
8
+ const BUILD_STAMP = 'dsh-panel-control/0.9.2';
9
9
  try { window.__dpcEvaluated = BUILD_STAMP; } catch { /* ignore */ }
10
10
  /**
11
11
  * Client half of dsh-panel-control.
@@ -348,9 +348,10 @@ function build(require) {
348
348
  diagYes: { zh: '是', en: 'yes' },
349
349
  diagNo: { zh: '否', en: 'no' },
350
350
  hint: {
351
- zh: '拖行首的把手排序;隐藏的入口仍留在列表里,随时可以放回来。',
352
- en: 'Drag the handle at the start of a row to reorder it; a hidden entry stays in this list and can come back at any time.',
351
+ zh: '拖行首的把手排序;点铅笔可以改显示名,改过名的入口会在侧栏右侧显示包名。隐藏的入口仍留在列表里,随时可以放回来。',
352
+ en: 'Drag the handle at the start of a row to reorder it; use the pencil to rename an entry, and a renamed row shows its package name on the right in the sidebar. A hidden entry stays in this list and can come back at any time.',
353
353
  },
354
+ renameIt: { zh: '修改显示名', en: 'Rename entry' },
354
355
  dragHint: { zh: '拖动排序', en: 'Drag to reorder' },
355
356
  locked: { zh: '固定显示', en: 'always shown' },
356
357
  lockedHint: {
@@ -395,6 +396,11 @@ function build(require) {
395
396
  bridgeFrom: { zh: '来自 {p}', en: 'From {p}' },
396
397
  bridgeMissing: { zh: '这个设置页已经不可用。', en: 'This settings page is no longer available.' },
397
398
  diagBridge: { zh: '补全入口', en: 'bridged entries' },
399
+ layoutTags: { zh: '包名标签', en: 'Package tag' },
400
+ layoutTagsHint: {
401
+ zh: '改过显示名的入口会在侧栏行右侧显示插件包名(省略 dsh- 前缀);关掉后全部隐藏。',
402
+ en: 'A renamed entry shows its plugin package name (with the dsh- prefix trimmed) in a small tag on the right of its sidebar row. Turn this off to hide them all.',
403
+ },
398
404
  layoutCollapseAll: { zh: '全部收起', en: 'Fold all' },
399
405
  layoutExpandAll: { zh: '全部展开', en: 'Unfold all' },
400
406
  layoutCollapseOne: { zh: '点此收起', en: 'click to fold' },
@@ -487,6 +493,8 @@ function build(require) {
487
493
  guard: true,
488
494
  collapse: true,
489
495
  bridge: true,
496
+ names: {},
497
+ tags: true,
490
498
  panelMax: 45,
491
499
  });
492
500
 
@@ -498,7 +506,17 @@ function build(require) {
498
506
  * would then build that many separators. The numbers here are far above any
499
507
  * real sidebar and turn a hostile or corrupted store into a cheap no-op.
500
508
  */
501
- const LIMITS = { order: 200, hidden: 200, autoFiled: 400, collapsed: 200, groups: 24, assign: 400, name: 40, key: 120 };
509
+ const LIMITS = { order: 200, hidden: 200, autoFiled: 400, collapsed: 200, groups: 24, assign: 400, names: 200, name: 40, key: 120 };
510
+
511
+ /** Trim, flatten and cap a user-visible name. Returns '' when it is unusable. */
512
+ function cleanName(value) {
513
+ if (typeof value !== 'string') return '';
514
+ return value
515
+ .replace(/[\u0000-\u001f\u007f]/g, '')
516
+ .replace(/\s+/g, ' ')
517
+ .trim()
518
+ .slice(0, LIMITS.name);
519
+ }
502
520
 
503
521
  /** Dedupe, drop anything that is not a usable id, and stop at `cap`. */
504
522
  function uniqueIds(raw, cap) {
@@ -553,6 +571,20 @@ function build(require) {
553
571
  kept += 1;
554
572
  }
555
573
  }
574
+ if (raw.names && typeof raw.names === 'object') {
575
+ let kept = 0;
576
+ for (const [panelId, value] of Object.entries(raw.names)) {
577
+ if (kept >= LIMITS.names) break;
578
+ if (typeof panelId !== 'string' || panelId === '' || panelId.length > LIMITS.key) continue;
579
+ const name = cleanName(value);
580
+ if (!name) continue;
581
+ out.names[panelId] = name;
582
+ kept += 1;
583
+ }
584
+ }
585
+ // Renames are opt-in per entry, so a missing field means "no custom names".
586
+ // The tag switch is the opposite: old files never had it and must get it on.
587
+ out.tags = raw.tags !== false;
556
588
  out.auto = raw.auto === true;
557
589
  return out;
558
590
  }
@@ -587,6 +619,8 @@ function build(require) {
587
619
  prefs.guard === false ||
588
620
  prefs.collapse === false ||
589
621
  prefs.bridge === false ||
622
+ prefs.tags === false ||
623
+ Object.keys(prefs.names || {}).length > 0 ||
590
624
  (typeof prefs.panelMax === 'number' && prefs.panelMax !== 45));
591
625
 
592
626
  /** One call to the Host half, or null when this Client cannot reach it. */
@@ -898,6 +932,32 @@ function build(require) {
898
932
  return fallback;
899
933
  }
900
934
 
935
+ /** The user's own name for an entry, or '' when it was never renamed. */
936
+ function customName(prefs, id) {
937
+ const names = prefs && prefs.names;
938
+ if (!names || typeof names !== 'object') return '';
939
+ const value = names[id];
940
+ return typeof value === 'string' ? cleanName(value) : '';
941
+ }
942
+
943
+ /** What the sidebar and the manager row should show for this entry. */
944
+ function displayName(prefs, id, fallback) {
945
+ return customName(prefs, id) || fallback;
946
+ }
947
+
948
+ /**
949
+ * A package name shortened into a tag: scope gone, `dsh-` gone, `client-ui-`
950
+ * gone. `dsh-panel-control` and `dshmarket` both read as their product name.
951
+ */
952
+ function packageLabel(name) {
953
+ if (typeof name !== 'string') return '';
954
+ return name
955
+ .replace(/^@[^/]+\//, '')
956
+ .replace(/^dsh-?/, '')
957
+ .replace(/^client-ui-/, '')
958
+ .trim();
959
+ }
960
+
901
961
  /**
902
962
  * The sidebar's own list: every registered entry, sorted exactly the way the
903
963
  * sidebar sorts it (`order`, then registration order).
@@ -963,6 +1023,86 @@ function build(require) {
963
1023
  unsub: null,
964
1024
  };
965
1025
 
1026
+ /**
1027
+ * Panel/section id -> the package that registers it.
1028
+ *
1029
+ * The slot entry has no package field and its `registrant` is shared by most
1030
+ * plugins, so the Host resolves ownership from its module graph. The map stays
1031
+ * empty until that answer arrives; the tag falls back to the id meanwhile.
1032
+ */
1033
+ const packageState = {
1034
+ map: Object.create(null),
1035
+ loading: false,
1036
+ key: '',
1037
+ pending: '',
1038
+ };
1039
+
1040
+ function packageTagFor(id) {
1041
+ // No fallback to the row id: a bridge row's id is `dpc-bridge-*` and an
1042
+ // unresolved panel id is not a package name. An empty tag is better than
1043
+ // a second, wrong spelling of the same plugin.
1044
+ return packageLabel(packageState.map[id]);
1045
+ }
1046
+
1047
+ /**
1048
+ * Ask the Host which package each visible row belongs to.
1049
+ *
1050
+ * Scans are de-duplicated by the exact id set, so the engine's many passes and
1051
+ * every bridge re-sync cost one call per set rather than one per pass.
1052
+ */
1053
+ async function ensurePackages(force) {
1054
+ const records = [];
1055
+ const seen = new Set();
1056
+ const add = (id, slot) => {
1057
+ if (typeof id !== 'string' || id === '' || seen.has(slot + ':' + id)) return;
1058
+ seen.add(slot + ':' + id);
1059
+ records.push({ id, slot });
1060
+ };
1061
+ for (const entry of store.state.entries) {
1062
+ if (entry.id.startsWith(BRIDGE_PREFIX)) continue;
1063
+ add(entry.id, SLOT_PANELS);
1064
+ }
1065
+ for (const record of bridgeState.entries.values()) {
1066
+ if (record && typeof record.sectionId === 'string') add(record.sectionId, SLOT_SETTINGS);
1067
+ }
1068
+ if (records.length === 0) return;
1069
+ records.sort((left, right) => (left.slot + ':' + left.id).localeCompare(right.slot + ':' + right.id));
1070
+ const key = records.map((row) => row.slot + ':' + row.id).join('|');
1071
+ if (!force && key === packageState.key) return;
1072
+ if (packageState.loading) {
1073
+ packageState.pending = key;
1074
+ return;
1075
+ }
1076
+ packageState.key = key;
1077
+ packageState.loading = true;
1078
+ try {
1079
+ const value = await hostCall('packages', { ids: records });
1080
+ const packages = value && value.packages;
1081
+ if (!packages || typeof packages !== 'object') return;
1082
+ let changed = false;
1083
+ for (const [id, pkg] of Object.entries(packages)) {
1084
+ if (typeof pkg !== 'string' || pkg === '') continue;
1085
+ if (packageState.map[id] === pkg) continue;
1086
+ packageState.map[id] = pkg;
1087
+ changed = true;
1088
+ }
1089
+ // A bridge row's own id is synthetic; its package is the section's.
1090
+ for (const [bridgeId, record] of bridgeState.entries) {
1091
+ const pkg = record && packageState.map[record.sectionId];
1092
+ if (pkg && packageState.map[bridgeId] !== pkg) {
1093
+ packageState.map[bridgeId] = pkg;
1094
+ changed = true;
1095
+ }
1096
+ }
1097
+ if (changed && engineRef) engineRef.schedule();
1098
+ } finally {
1099
+ packageState.loading = false;
1100
+ const next = packageState.pending;
1101
+ packageState.pending = '';
1102
+ if (next && next !== packageState.key) void ensurePackages();
1103
+ }
1104
+ }
1105
+
966
1106
  function registrantOf(entry) {
967
1107
  const options = (entry && entry.options) || {};
968
1108
  const value = (entry && entry.registrant) || options.registrant;
@@ -1111,7 +1251,7 @@ function build(require) {
1111
1251
  for (const candidate of candidates) {
1112
1252
  if (bridgeState.entries.has(candidate.bridgeId)) continue;
1113
1253
  const slotName = BRIDGE_SLOT_PREFIX + candidate.sectionId;
1114
- const record = { disposers: [] };
1254
+ const record = { sectionId: candidate.sectionId, disposers: [] };
1115
1255
  try {
1116
1256
  record.disposers.push(
1117
1257
  slots.register(
@@ -1189,6 +1329,9 @@ function build(require) {
1189
1329
  // re-render it would keep showing the value from before the sync.
1190
1330
  emit();
1191
1331
  }
1332
+ // New bridge rows (or a first scan that finally sees the real rows) need
1333
+ // their package names; the call is keyed, so repeated scans are free.
1334
+ void ensurePackages();
1192
1335
  } catch (error) {
1193
1336
  console.warn(`[${PACKAGE}] bridge sync failed`, error);
1194
1337
  } finally {
@@ -1316,7 +1459,9 @@ function build(require) {
1316
1459
  const used = new Set();
1317
1460
  const byLabel = new Map();
1318
1461
  for (const button of buttons) {
1319
- const label = button.getAttribute('aria-label') || '';
1462
+ // A renamed row keeps its original label in `data-dpc-label`: the visible
1463
+ // text and aria-label change, but the entry it belongs to does not.
1464
+ const label = button.getAttribute('data-dpc-label') || button.getAttribute('aria-label') || '';
1320
1465
  if (!byLabel.has(label)) byLabel.set(label, []);
1321
1466
  byLabel.get(label).push(button);
1322
1467
  }
@@ -1335,7 +1480,7 @@ function build(require) {
1335
1480
  // Labels that agree prove the two lists are the same list; a button with no
1336
1481
  // label at all is no evidence either way, so it may be filled positionally.
1337
1482
  const sameOrder = loose.every((entry, index) => {
1338
- const label = rest[index].getAttribute('aria-label') || '';
1483
+ const label = rest[index].getAttribute('data-dpc-label') || rest[index].getAttribute('aria-label') || '';
1339
1484
  return label === '' || label === entry.label;
1340
1485
  });
1341
1486
  // A build that labels rows in wording this plugin never sees ("IM 机器人"
@@ -1447,7 +1592,10 @@ function build(require) {
1447
1592
  if (!entry.label) continue;
1448
1593
  let button = null;
1449
1594
  try {
1450
- button = document.querySelector(`button[aria-label="${cssEscape(entry.label)}"]`);
1595
+ const label = cssEscape(entry.label);
1596
+ button = document.querySelector(
1597
+ `button[data-dpc-label="${label}"], button[aria-label="${label}"]`,
1598
+ );
1451
1599
  } catch {
1452
1600
  button = null;
1453
1601
  }
@@ -1595,6 +1743,74 @@ function build(require) {
1595
1743
  }
1596
1744
  };
1597
1745
 
1746
+ /** The title node of a host panel row: the only direct span that is not the
1747
+ * aria-hidden glyph and not the original-name tag this plugin adds. */
1748
+ const titleSpanOf = (button) => {
1749
+ for (const child of Array.from(button.children || [])) {
1750
+ if (child.tagName !== 'SPAN') continue;
1751
+ const className = typeof child.className === 'string' ? child.className : '';
1752
+ if (className.split(/\s+/).includes('dpc-nameTag')) continue;
1753
+ if (child.getAttribute('aria-hidden') === 'true') continue;
1754
+ return child;
1755
+ }
1756
+ return null;
1757
+ };
1758
+
1759
+ /**
1760
+ * Rename rows in the host sidebar, and keep the original label visible in a
1761
+ * small tag. The host owns this DOM, so this is a pure display overlay: the
1762
+ * entry id, its slot registration and every other preference are untouched.
1763
+ */
1764
+ const applyNames = (map, plan, wide) => {
1765
+ const tagsOn = plan.prefs.tags !== false;
1766
+ const byId = new Map(plan.entries.map((entry) => [entry.id, entry]));
1767
+ for (const [id, button] of map) {
1768
+ try {
1769
+ const entry = byId.get(id);
1770
+ if (!entry) continue;
1771
+ const custom = customName(plan.prefs, id);
1772
+ const shown = custom || entry.label;
1773
+ // Keep the original label addressable: mapButtons() must keep matching
1774
+ // the same row after the user renames it.
1775
+ if (button.getAttribute('data-dpc-label') !== entry.label) {
1776
+ button.setAttribute('data-dpc-label', entry.label);
1777
+ }
1778
+ if (button.getAttribute('aria-label') !== shown) button.setAttribute('aria-label', shown);
1779
+ const title = titleSpanOf(button);
1780
+ if (title && title.textContent !== shown) title.textContent = shown;
1781
+ let tag = null;
1782
+ for (const child of Array.from(button.children || [])) {
1783
+ const className = typeof child.className === 'string' ? child.className : '';
1784
+ if (className.split(/\s+/).includes('dpc-nameTag')) tag = child;
1785
+ }
1786
+ // Only a renamed row needs the tag: it identifies the plugin by its
1787
+ // package (`dsh-` trimmed), which a custom display name can hide.
1788
+ const tagText = packageTagFor(id);
1789
+ const wantTag = Boolean(
1790
+ wide &&
1791
+ tagsOn &&
1792
+ custom &&
1793
+ custom !== entry.label &&
1794
+ tagText &&
1795
+ tagText.toLowerCase() !== shown.toLowerCase(),
1796
+ );
1797
+ if (wantTag) {
1798
+ if (!tag) {
1799
+ tag = document.createElement('span');
1800
+ tag.className = 'dpc-nameTag';
1801
+ tag.setAttribute('aria-hidden', 'true');
1802
+ button.appendChild(tag);
1803
+ }
1804
+ if (tag.textContent !== tagText) tag.textContent = tagText;
1805
+ } else if (tag) {
1806
+ tag.remove();
1807
+ }
1808
+ } catch {
1809
+ /* a host build with another row shape: leave that row alone */
1810
+ }
1811
+ }
1812
+ };
1813
+
1598
1814
  const apply = () => {
1599
1815
  if (destroyed) return;
1600
1816
  const plan = readPlan();
@@ -1623,10 +1839,21 @@ function build(require) {
1623
1839
  const folded = folding ? new Set(plan.prefs.collapsed || []) : new Set();
1624
1840
  diag.folded = Array.from(folded).join(',');
1625
1841
 
1842
+ // Renames are display state, like the height cap: apply them before the
1843
+ // signature shortcut so a pass that only renames still reaches the DOM.
1844
+ const buttons = Array.from(list.querySelectorAll(':scope > button'));
1845
+ const map = mapButtons(plan.entries, buttons);
1846
+ applyNames(map, plan, wide);
1847
+ // Package names label renamed rows; the first pass that knows the entries
1848
+ // asks the Host once, and later passes are deduplicated by the id set.
1849
+ void ensurePackages();
1850
+
1626
1851
  const nextSignature = JSON.stringify({
1627
1852
  blocks: plan.blocks,
1628
1853
  hidden: plan.hidden ? Array.from(plan.hidden).sort() : [],
1629
1854
  entries: plan.entries.map((entry) => entry.id + ':' + entry.label),
1855
+ names: plan.prefs.names || {},
1856
+ tags: plan.prefs.tags !== false,
1630
1857
  wide,
1631
1858
  folded: Array.from(folded),
1632
1859
  });
@@ -1640,10 +1867,8 @@ function build(require) {
1640
1867
 
1641
1868
  pause();
1642
1869
  try {
1643
- const buttons = Array.from(list.querySelectorAll(':scope > button'));
1644
1870
  for (const node of Array.from(list.querySelectorAll('[data-dpc-sep]'))) node.remove();
1645
1871
 
1646
- const map = mapButtons(plan.entries, buttons);
1647
1872
  const managed = new Set(map.values());
1648
1873
 
1649
1874
  // Anything we do not manage (the "More" launcher and its wrapper) keeps
@@ -1781,6 +2006,18 @@ function build(require) {
1781
2006
  node.style.removeProperty('display');
1782
2007
  }
1783
2008
  }
2009
+ // Hand the host rows back exactly as they were: the original label, the
2010
+ // original title text, and no tag of ours.
2011
+ for (const button of Array.from(document.querySelectorAll('button[data-dpc-label]'))) {
2012
+ const label = button.getAttribute('data-dpc-label');
2013
+ if (label) {
2014
+ button.setAttribute('aria-label', label);
2015
+ const title = titleSpanOf(button);
2016
+ if (title) title.textContent = label;
2017
+ button.removeAttribute('data-dpc-label');
2018
+ }
2019
+ for (const tag of Array.from(button.querySelectorAll('.dpc-nameTag'))) tag.remove();
2020
+ }
1784
2021
  for (const node of Array.from(document.querySelectorAll('[data-dpc-list]'))) {
1785
2022
  node.removeAttribute('data-dpc-list');
1786
2023
  node.removeAttribute('data-dpc-unbounded');
@@ -1868,7 +2105,9 @@ function build(require) {
1868
2105
  }
1869
2106
 
1870
2107
  function sortByName() {
1871
- const labelOf = new Map((store.state.entries || []).map((entry) => [entry.id, entry.label]));
2108
+ const prefs = prefsNow();
2109
+ const entries = store.state.entries || [];
2110
+ const labelOf = new Map(entries.map((entry) => [entry.id, displayName(prefs, entry.id, entry.label)]));
1872
2111
  const locale = activeLang() === 'zh' ? 'zh-Hans-CN' : 'en';
1873
2112
  const order = orderNow()
1874
2113
  .slice()
@@ -2113,6 +2352,7 @@ function build(require) {
2113
2352
  plus: 'M12 6v12M6 12h12',
2114
2353
  eye: 'M2.5 12S6 6.5 12 6.5 21.5 12 21.5 12 18 17.5 12 17.5 2.5 12 2.5 12Z',
2115
2354
  eyeOff: 'M2.5 12S6 6.5 12 6.5c1.6 0 3 .45 4.2 1.1M21.5 12S18 17.5 12 17.5c-1.6 0-3-.45-4.2-1.1M4 4l16 16',
2355
+ edit: 'M4 20h4l10.5-10.5a2.12 2.12 0 0 0-3-3L5 17v3Z',
2116
2356
  back: 'M14.5 6.5 9 12l5.5 5.5',
2117
2357
  };
2118
2358
 
@@ -2258,7 +2498,15 @@ function build(require) {
2258
2498
  'span',
2259
2499
  { className: 'dpc-bridgeTags' },
2260
2500
  h('span', { className: 'dpc-bridgeTag' }, t('bridgeTag')),
2261
- h('span', { className: 'dpc-bridgeFrom' }, fill(t('bridgeFrom'), { p: candidate.sectionId })),
2501
+ h(
2502
+ 'span',
2503
+ { className: 'dpc-bridgeFrom' },
2504
+ fill(t('bridgeFrom'), {
2505
+ // The same package short name the row tag shows; the section
2506
+ // id is only a fallback while the Host lookup is in flight.
2507
+ p: packageTagFor(candidate.bridgeId) || candidate.sectionId,
2508
+ }),
2509
+ ),
2262
2510
  ),
2263
2511
  ),
2264
2512
  node
@@ -2373,6 +2621,9 @@ function build(require) {
2373
2621
  // check runs again on focus, and the answer must not pop back up.
2374
2622
  const [, forceRender] = React.useReducer((n) => n + 1, 0);
2375
2623
  const [groupDrag, setGroupDrag] = React.useState({ from: null, over: null });
2624
+ const [editing, setEditing] = React.useState(null);
2625
+ const [draft, setDraft] = React.useState('');
2626
+ const cancelRenameRef = React.useRef(false);
2376
2627
  /**
2377
2628
  * Dragging works the same for a row and for a section: the handle starts it,
2378
2629
  * the target highlights, dropping commits "move before this one". Only the
@@ -2436,12 +2687,44 @@ function build(require) {
2436
2687
  }, [prefs.auto, prefs.autoFiled, entries.length, state.autoState && state.autoState.status]);
2437
2688
 
2438
2689
  const order = normalize(prefs, entries);
2439
- const labelOf = new Map(entries.map((entry) => [entry.id, entry.label]));
2690
+ const rawOf = new Map(entries.map((entry) => [entry.id, entry.label]));
2691
+ const labelOf = new Map(entries.map((entry) => [entry.id, displayName(prefs, entry.id, entry.label)]));
2440
2692
  const hidden = new Set(prefs.hidden || []);
2441
2693
  const groupsOf = prefs.groups || [];
2442
2694
  const autoOn = prefs.auto === true;
2443
2695
  const autoState = state.autoState || { status: 'idle', message: '', groups: 0 };
2444
2696
 
2697
+ const startRename = (id) => {
2698
+ cancelRenameRef.current = false;
2699
+ setEditing(id);
2700
+ setDraft(customName(prefsNow(), id));
2701
+ };
2702
+
2703
+ const finishRename = (id) => {
2704
+ // Escape unmounts the input; a trailing blur must not save the draft.
2705
+ if (cancelRenameRef.current) {
2706
+ cancelRenameRef.current = false;
2707
+ setEditing(null);
2708
+ setDraft('');
2709
+ return;
2710
+ }
2711
+ const original = rawOf.get(id) || id;
2712
+ const name = cleanName(draft);
2713
+ const names = { ...(prefsNow().names || {}) };
2714
+ if (!name || name === original) delete names[id];
2715
+ else names[id] = name;
2716
+ setEditing(null);
2717
+ setDraft('');
2718
+ commit({ ...prefsNow(), names });
2719
+ };
2720
+
2721
+ const cancelRename = () => {
2722
+ cancelRenameRef.current = true;
2723
+ setEditing(null);
2724
+ setDraft('');
2725
+ };
2726
+
2727
+
2445
2728
  const head = h(
2446
2729
  'header',
2447
2730
  { className: inSection ? 'dpc-head dpc-head--section' : 'dpc-head' },
@@ -2599,6 +2882,7 @@ function build(require) {
2599
2882
 
2600
2883
  const rows = order.map((id, position) => {
2601
2884
  const label = labelOf.get(id) || id;
2885
+ const rawLabel = rawOf.get(id) || id;
2602
2886
  const isHidden = hidden.has(id);
2603
2887
  const locked = id === PANEL_KEY;
2604
2888
  return h(
@@ -2616,8 +2900,32 @@ function build(require) {
2616
2900
  h(
2617
2901
  'span',
2618
2902
  { className: 'dpc-rowText' },
2619
- h('span', { className: 'dpc-name', title: label }, label),
2620
- h('code', { className: 'dpc-key', title: id }, id),
2903
+ editing === id
2904
+ ? h('input', {
2905
+ className: 'dpc-input dpc-nameInput',
2906
+ // autoFocus is enough: opening the editor is an explicit click.
2907
+ autoFocus: true,
2908
+ value: draft,
2909
+ placeholder: rawLabel,
2910
+ 'aria-label': t('renameIt'),
2911
+ onChange: (event) => setDraft(event.target.value),
2912
+ onKeyDown: (event) => {
2913
+ if (event.key === 'Enter') {
2914
+ event.preventDefault();
2915
+ finishRename(id);
2916
+ } else if (event.key === 'Escape') {
2917
+ event.preventDefault();
2918
+ cancelRename();
2919
+ }
2920
+ },
2921
+ onBlur: () => finishRename(id),
2922
+ })
2923
+ : h(
2924
+ 'span',
2925
+ { className: 'dpc-name', title: label === rawLabel ? rawLabel : label + ' · ' + rawLabel },
2926
+ label,
2927
+ ),
2928
+ editing === id ? null : h('code', { className: 'dpc-key', title: id }, id),
2621
2929
  ),
2622
2930
  h(
2623
2931
  'span',
@@ -2635,6 +2943,9 @@ function build(require) {
2635
2943
  ungrouped: t('ungrouped'),
2636
2944
  onPick: (next) => setGroup(id, next),
2637
2945
  }),
2946
+ iconButton('edit', t('renameIt'), () => startRename(id), false, {
2947
+ 'aria-pressed': editing === id ? 'true' : 'false',
2948
+ }),
2638
2949
  iconButton(
2639
2950
  isHidden ? 'eyeOff' : 'eye',
2640
2951
  locked ? t('lockedHint') : isHidden ? t('showIt') : t('hideIt'),
@@ -2676,6 +2987,7 @@ function build(require) {
2676
2987
  const layoutOn = prefs.guard !== false;
2677
2988
  const foldingOn = prefs.collapse !== false;
2678
2989
  const bridgeOn = prefs.bridge !== false;
2990
+ const tagsOn = prefs.tags !== false;
2679
2991
  const capNow = typeof prefs.panelMax === 'number' ? prefs.panelMax : 45;
2680
2992
  const setLayout = (patch) => commit({ ...prefsNow(), ...patch });
2681
2993
  const layoutSwitch = (on, label, onClick) =>
@@ -2726,6 +3038,11 @@ function build(require) {
2726
3038
  t('layoutBridgeHint'),
2727
3039
  layoutSwitch(bridgeOn, t('layoutBridge'), () => setLayout({ bridge: !bridgeOn })),
2728
3040
  ),
3041
+ layoutRow(
3042
+ t('layoutTags'),
3043
+ t('layoutTagsHint'),
3044
+ layoutSwitch(tagsOn, t('layoutTags'), () => setLayout({ tags: !tagsOn })),
3045
+ ),
2729
3046
  layoutRow(
2730
3047
  t('layoutMax'),
2731
3048
  null,
@@ -2928,6 +3245,7 @@ function build(require) {
2928
3245
  .dpc-glyph:empty::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.4}
2929
3246
  .dpc-rowText{display:flex;flex:1 1 auto;flex-direction:row;align-items:baseline;gap:8px;min-width:0}
2930
3247
  .dpc-name{font-size:13px;font-weight:500;line-height:18px;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3248
+ .dpc-nameInput{flex:1 1 120px;max-width:320px;font-size:13px;line-height:18px}
2931
3249
  .dpc-key{font-family:var(--dsw-font-family-code,ui-monospace,monospace);font-size:11px;line-height:16px;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.7));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2932
3250
  .dpc-tags{display:flex;flex:none;align-items:center;gap:8px}
2933
3251
  .dpc-tag{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.8));white-space:nowrap}
@@ -2942,7 +3260,9 @@ function build(require) {
2942
3260
  .dpc-pickerText{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2943
3261
  .dpc-caret{flex:none;width:6px;height:6px;margin-top:-2px;border-right:1.2px solid currentColor;border-bottom:1.2px solid currentColor;transform:rotate(45deg);opacity:.7;transition:transform 120ms cubic-bezier(.16,1,.3,1)}
2944
3262
  .dpc-picker[data-open="true"] .dpc-caret{margin-top:2px;transform:rotate(225deg)}
2945
- .dpc-menu{position:absolute;z-index:50;top:calc(100% + 4px);right:0;display:flex;flex-direction:column;gap:2px;min-width:144px;max-width:240px;padding:4px;background:var(--dsw-alias-bg-elevated,rgba(127,127,127,.18));border:.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.24));border-radius:var(--dsw-radius-md,8px);box-shadow:0 8px 24px rgba(0,0,0,.28)}
3263
+ /* Opaque popover surface: --dsw-alias-bg-layer-2 is the host's own elevated
3264
+ layer, so the menu never lets the card behind it show through. */
3265
+ .dpc-menu{position:absolute;z-index:50;top:calc(100% + 4px);right:0;display:flex;flex-direction:column;gap:2px;min-width:144px;max-width:240px;padding:4px;background-color:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#1f1f1f));border:.5px solid var(--dsw-alias-border-l2,rgba(127,127,127,.24));border-radius:var(--dsw-radius-md,8px);box-shadow:0 8px 24px rgba(0,0,0,.28)}
2946
3266
  .dpc-option{appearance:none;border:0;padding:4px 8px;border-radius:var(--dsw-radius-xs,4px);background:transparent;color:var(--dsw-alias-label-secondary,rgba(127,127,127,.95));font:inherit;font-size:12px;line-height:16px;text-align:left;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2947
3267
  .dpc-option:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.18));color:var(--dsw-alias-label-primary,#8a8a8a)}
2948
3268
  .dpc-option[aria-selected="true"]{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#5865f2) 16%,transparent);color:var(--dsw-alias-label-primary-bluish,#7f8fd8)}
@@ -3045,6 +3365,10 @@ function build(require) {
3045
3365
  [data-dpc-list]>button{flex:none}
3046
3366
  [data-dpc-region]{flex:1 1 auto;min-height:min(120px,18vh)}
3047
3367
 
3368
+ /* The original name of a renamed row. It sits after the host title, pushed
3369
+ to the far right; the host row stays a single click target. */
3370
+ .dpc-nameTag{display:inline-block;flex:none;margin-left:auto;max-width:50%;height:16px;padding:0 6px;border-radius:999px;background:var(--dsw-alias-bg-l2,rgba(127,127,127,.14));color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.8));font-size:10px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}
3371
+
3048
3372
  .dpc-sep{display:flex;align-items:center;gap:6px;flex:none;padding:12px 8px 4px;font-size:11px;font-weight:500;line-height:14px;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.78));white-space:nowrap;overflow:hidden;cursor:pointer;border-radius:var(--dsw-radius-sm,6px);user-select:none}
3049
3373
  .dpc-sep:hover{color:var(--dsw-alias-label-secondary,rgba(127,127,127,.95))}
3050
3374
  .dpc-sep:focus-visible{outline:2px solid var(--dsw-focus-ring-color,rgba(127,127,127,.65));outline-offset:1px}
@@ -3244,6 +3568,9 @@ function build(require) {
3244
3568
  }
3245
3569
  ctx.effect(() => () => disposeBridges(), `${PACKAGE}: bridge cleanup`);
3246
3570
  kickBridge();
3571
+ // Renames show the owning package; the first scan answers the current rows,
3572
+ // later bridge scans refresh the set when more settings pages register.
3573
+ void ensurePackages();
3247
3574
  try { window.__dpcApplied = true; } catch { /* ignore */ }
3248
3575
  }
3249
3576
 
@@ -44,8 +44,8 @@
44
44
  `props.renderSlot(slotName, { close: back }, {})` 渲染它。
45
45
 
46
46
  这样 section 的 `inject`、`store`、locale `t`、`useStore` 和 error boundary 仍由宿主照常提供;直接调用
47
- section 组件会缺这些。面板只负责外壳:顶部一条 40px 的栏(「返回对话」+ 来源 section id),
48
- 下面一块随页面滚动的主体。`close` 与设置壳里一样只做「关掉这一页」,这里落回对话
47
+ section 组件会缺这些。面板只负责外壳:顶部一条 40px 的栏(「返回对话」+ 来源包名短名,
48
+ 查不到包名时退回 section id),下面一块随页面滚动的主体。`close` 与设置壳里一样只做「关掉这一页」,这里落回对话
49
49
  (`layout.selectPanel(null)`)。
50
50
 
51
51
  桥接面板不常驻:开关关闭、section 消失、插件卸载时,`dispose()` 成对撤掉 `main` 与