dsh-panel-control 0.8.3 → 0.9.1

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 的模块图解析,`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.1
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`、包名标签的位置与开关、清空恢复、窄侧栏不出标签、重复名称仍能按位置映射、未知包回退到入口 id,以及 `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 模块图解析,接口不可用时标签回退为入口 id。
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.1';
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,84 @@ 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
+ const pkg = packageState.map[id];
1042
+ return packageLabel(pkg) || packageLabel(id);
1043
+ }
1044
+
1045
+ /**
1046
+ * Ask the Host which package each visible row belongs to.
1047
+ *
1048
+ * Scans are de-duplicated by the exact id set, so the engine's many passes and
1049
+ * every bridge re-sync cost one call per set rather than one per pass.
1050
+ */
1051
+ async function ensurePackages(force) {
1052
+ const records = [];
1053
+ const seen = new Set();
1054
+ const add = (id, slot) => {
1055
+ if (typeof id !== 'string' || id === '' || seen.has(slot + ':' + id)) return;
1056
+ seen.add(slot + ':' + id);
1057
+ records.push({ id, slot });
1058
+ };
1059
+ for (const entry of store.state.entries) {
1060
+ if (entry.id.startsWith(BRIDGE_PREFIX)) continue;
1061
+ add(entry.id, SLOT_PANELS);
1062
+ }
1063
+ for (const record of bridgeState.entries.values()) {
1064
+ if (record && typeof record.sectionId === 'string') add(record.sectionId, SLOT_SETTINGS);
1065
+ }
1066
+ if (records.length === 0) return;
1067
+ records.sort((left, right) => (left.slot + ':' + left.id).localeCompare(right.slot + ':' + right.id));
1068
+ const key = records.map((row) => row.slot + ':' + row.id).join('|');
1069
+ if (!force && key === packageState.key) return;
1070
+ if (packageState.loading) {
1071
+ packageState.pending = key;
1072
+ return;
1073
+ }
1074
+ packageState.key = key;
1075
+ packageState.loading = true;
1076
+ try {
1077
+ const value = await hostCall('packages', { ids: records });
1078
+ const packages = value && value.packages;
1079
+ if (!packages || typeof packages !== 'object') return;
1080
+ let changed = false;
1081
+ for (const [id, pkg] of Object.entries(packages)) {
1082
+ if (typeof pkg !== 'string' || pkg === '') continue;
1083
+ if (packageState.map[id] === pkg) continue;
1084
+ packageState.map[id] = pkg;
1085
+ changed = true;
1086
+ }
1087
+ // A bridge row's own id is synthetic; its package is the section's.
1088
+ for (const [bridgeId, record] of bridgeState.entries) {
1089
+ const pkg = record && packageState.map[record.sectionId];
1090
+ if (pkg && packageState.map[bridgeId] !== pkg) {
1091
+ packageState.map[bridgeId] = pkg;
1092
+ changed = true;
1093
+ }
1094
+ }
1095
+ if (changed && engineRef) engineRef.schedule();
1096
+ } finally {
1097
+ packageState.loading = false;
1098
+ const next = packageState.pending;
1099
+ packageState.pending = '';
1100
+ if (next && next !== packageState.key) void ensurePackages();
1101
+ }
1102
+ }
1103
+
966
1104
  function registrantOf(entry) {
967
1105
  const options = (entry && entry.options) || {};
968
1106
  const value = (entry && entry.registrant) || options.registrant;
@@ -1111,7 +1249,7 @@ function build(require) {
1111
1249
  for (const candidate of candidates) {
1112
1250
  if (bridgeState.entries.has(candidate.bridgeId)) continue;
1113
1251
  const slotName = BRIDGE_SLOT_PREFIX + candidate.sectionId;
1114
- const record = { disposers: [] };
1252
+ const record = { sectionId: candidate.sectionId, disposers: [] };
1115
1253
  try {
1116
1254
  record.disposers.push(
1117
1255
  slots.register(
@@ -1189,6 +1327,9 @@ function build(require) {
1189
1327
  // re-render it would keep showing the value from before the sync.
1190
1328
  emit();
1191
1329
  }
1330
+ // New bridge rows (or a first scan that finally sees the real rows) need
1331
+ // their package names; the call is keyed, so repeated scans are free.
1332
+ void ensurePackages();
1192
1333
  } catch (error) {
1193
1334
  console.warn(`[${PACKAGE}] bridge sync failed`, error);
1194
1335
  } finally {
@@ -1316,7 +1457,9 @@ function build(require) {
1316
1457
  const used = new Set();
1317
1458
  const byLabel = new Map();
1318
1459
  for (const button of buttons) {
1319
- const label = button.getAttribute('aria-label') || '';
1460
+ // A renamed row keeps its original label in `data-dpc-label`: the visible
1461
+ // text and aria-label change, but the entry it belongs to does not.
1462
+ const label = button.getAttribute('data-dpc-label') || button.getAttribute('aria-label') || '';
1320
1463
  if (!byLabel.has(label)) byLabel.set(label, []);
1321
1464
  byLabel.get(label).push(button);
1322
1465
  }
@@ -1335,7 +1478,7 @@ function build(require) {
1335
1478
  // Labels that agree prove the two lists are the same list; a button with no
1336
1479
  // label at all is no evidence either way, so it may be filled positionally.
1337
1480
  const sameOrder = loose.every((entry, index) => {
1338
- const label = rest[index].getAttribute('aria-label') || '';
1481
+ const label = rest[index].getAttribute('data-dpc-label') || rest[index].getAttribute('aria-label') || '';
1339
1482
  return label === '' || label === entry.label;
1340
1483
  });
1341
1484
  // A build that labels rows in wording this plugin never sees ("IM 机器人"
@@ -1447,7 +1590,10 @@ function build(require) {
1447
1590
  if (!entry.label) continue;
1448
1591
  let button = null;
1449
1592
  try {
1450
- button = document.querySelector(`button[aria-label="${cssEscape(entry.label)}"]`);
1593
+ const label = cssEscape(entry.label);
1594
+ button = document.querySelector(
1595
+ `button[data-dpc-label="${label}"], button[aria-label="${label}"]`,
1596
+ );
1451
1597
  } catch {
1452
1598
  button = null;
1453
1599
  }
@@ -1595,6 +1741,74 @@ function build(require) {
1595
1741
  }
1596
1742
  };
1597
1743
 
1744
+ /** The title node of a host panel row: the only direct span that is not the
1745
+ * aria-hidden glyph and not the original-name tag this plugin adds. */
1746
+ const titleSpanOf = (button) => {
1747
+ for (const child of Array.from(button.children || [])) {
1748
+ if (child.tagName !== 'SPAN') continue;
1749
+ const className = typeof child.className === 'string' ? child.className : '';
1750
+ if (className.split(/\s+/).includes('dpc-nameTag')) continue;
1751
+ if (child.getAttribute('aria-hidden') === 'true') continue;
1752
+ return child;
1753
+ }
1754
+ return null;
1755
+ };
1756
+
1757
+ /**
1758
+ * Rename rows in the host sidebar, and keep the original label visible in a
1759
+ * small tag. The host owns this DOM, so this is a pure display overlay: the
1760
+ * entry id, its slot registration and every other preference are untouched.
1761
+ */
1762
+ const applyNames = (map, plan, wide) => {
1763
+ const tagsOn = plan.prefs.tags !== false;
1764
+ const byId = new Map(plan.entries.map((entry) => [entry.id, entry]));
1765
+ for (const [id, button] of map) {
1766
+ try {
1767
+ const entry = byId.get(id);
1768
+ if (!entry) continue;
1769
+ const custom = customName(plan.prefs, id);
1770
+ const shown = custom || entry.label;
1771
+ // Keep the original label addressable: mapButtons() must keep matching
1772
+ // the same row after the user renames it.
1773
+ if (button.getAttribute('data-dpc-label') !== entry.label) {
1774
+ button.setAttribute('data-dpc-label', entry.label);
1775
+ }
1776
+ if (button.getAttribute('aria-label') !== shown) button.setAttribute('aria-label', shown);
1777
+ const title = titleSpanOf(button);
1778
+ if (title && title.textContent !== shown) title.textContent = shown;
1779
+ let tag = null;
1780
+ for (const child of Array.from(button.children || [])) {
1781
+ const className = typeof child.className === 'string' ? child.className : '';
1782
+ if (className.split(/\s+/).includes('dpc-nameTag')) tag = child;
1783
+ }
1784
+ // Only a renamed row needs the tag: it identifies the plugin by its
1785
+ // package (`dsh-` trimmed), which a custom display name can hide.
1786
+ const tagText = packageTagFor(id);
1787
+ const wantTag = Boolean(
1788
+ wide &&
1789
+ tagsOn &&
1790
+ custom &&
1791
+ custom !== entry.label &&
1792
+ tagText &&
1793
+ tagText.toLowerCase() !== shown.toLowerCase(),
1794
+ );
1795
+ if (wantTag) {
1796
+ if (!tag) {
1797
+ tag = document.createElement('span');
1798
+ tag.className = 'dpc-nameTag';
1799
+ tag.setAttribute('aria-hidden', 'true');
1800
+ button.appendChild(tag);
1801
+ }
1802
+ if (tag.textContent !== tagText) tag.textContent = tagText;
1803
+ } else if (tag) {
1804
+ tag.remove();
1805
+ }
1806
+ } catch {
1807
+ /* a host build with another row shape: leave that row alone */
1808
+ }
1809
+ }
1810
+ };
1811
+
1598
1812
  const apply = () => {
1599
1813
  if (destroyed) return;
1600
1814
  const plan = readPlan();
@@ -1623,10 +1837,21 @@ function build(require) {
1623
1837
  const folded = folding ? new Set(plan.prefs.collapsed || []) : new Set();
1624
1838
  diag.folded = Array.from(folded).join(',');
1625
1839
 
1840
+ // Renames are display state, like the height cap: apply them before the
1841
+ // signature shortcut so a pass that only renames still reaches the DOM.
1842
+ const buttons = Array.from(list.querySelectorAll(':scope > button'));
1843
+ const map = mapButtons(plan.entries, buttons);
1844
+ applyNames(map, plan, wide);
1845
+ // Package names label renamed rows; the first pass that knows the entries
1846
+ // asks the Host once, and later passes are deduplicated by the id set.
1847
+ void ensurePackages();
1848
+
1626
1849
  const nextSignature = JSON.stringify({
1627
1850
  blocks: plan.blocks,
1628
1851
  hidden: plan.hidden ? Array.from(plan.hidden).sort() : [],
1629
1852
  entries: plan.entries.map((entry) => entry.id + ':' + entry.label),
1853
+ names: plan.prefs.names || {},
1854
+ tags: plan.prefs.tags !== false,
1630
1855
  wide,
1631
1856
  folded: Array.from(folded),
1632
1857
  });
@@ -1640,10 +1865,8 @@ function build(require) {
1640
1865
 
1641
1866
  pause();
1642
1867
  try {
1643
- const buttons = Array.from(list.querySelectorAll(':scope > button'));
1644
1868
  for (const node of Array.from(list.querySelectorAll('[data-dpc-sep]'))) node.remove();
1645
1869
 
1646
- const map = mapButtons(plan.entries, buttons);
1647
1870
  const managed = new Set(map.values());
1648
1871
 
1649
1872
  // Anything we do not manage (the "More" launcher and its wrapper) keeps
@@ -1781,6 +2004,18 @@ function build(require) {
1781
2004
  node.style.removeProperty('display');
1782
2005
  }
1783
2006
  }
2007
+ // Hand the host rows back exactly as they were: the original label, the
2008
+ // original title text, and no tag of ours.
2009
+ for (const button of Array.from(document.querySelectorAll('button[data-dpc-label]'))) {
2010
+ const label = button.getAttribute('data-dpc-label');
2011
+ if (label) {
2012
+ button.setAttribute('aria-label', label);
2013
+ const title = titleSpanOf(button);
2014
+ if (title) title.textContent = label;
2015
+ button.removeAttribute('data-dpc-label');
2016
+ }
2017
+ for (const tag of Array.from(button.querySelectorAll('.dpc-nameTag'))) tag.remove();
2018
+ }
1784
2019
  for (const node of Array.from(document.querySelectorAll('[data-dpc-list]'))) {
1785
2020
  node.removeAttribute('data-dpc-list');
1786
2021
  node.removeAttribute('data-dpc-unbounded');
@@ -1868,7 +2103,9 @@ function build(require) {
1868
2103
  }
1869
2104
 
1870
2105
  function sortByName() {
1871
- const labelOf = new Map((store.state.entries || []).map((entry) => [entry.id, entry.label]));
2106
+ const prefs = prefsNow();
2107
+ const entries = store.state.entries || [];
2108
+ const labelOf = new Map(entries.map((entry) => [entry.id, displayName(prefs, entry.id, entry.label)]));
1872
2109
  const locale = activeLang() === 'zh' ? 'zh-Hans-CN' : 'en';
1873
2110
  const order = orderNow()
1874
2111
  .slice()
@@ -2113,6 +2350,7 @@ function build(require) {
2113
2350
  plus: 'M12 6v12M6 12h12',
2114
2351
  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
2352
  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',
2353
+ edit: 'M4 20h4l10.5-10.5a2.12 2.12 0 0 0-3-3L5 17v3Z',
2116
2354
  back: 'M14.5 6.5 9 12l5.5 5.5',
2117
2355
  };
2118
2356
 
@@ -2373,6 +2611,9 @@ function build(require) {
2373
2611
  // check runs again on focus, and the answer must not pop back up.
2374
2612
  const [, forceRender] = React.useReducer((n) => n + 1, 0);
2375
2613
  const [groupDrag, setGroupDrag] = React.useState({ from: null, over: null });
2614
+ const [editing, setEditing] = React.useState(null);
2615
+ const [draft, setDraft] = React.useState('');
2616
+ const cancelRenameRef = React.useRef(false);
2376
2617
  /**
2377
2618
  * Dragging works the same for a row and for a section: the handle starts it,
2378
2619
  * the target highlights, dropping commits "move before this one". Only the
@@ -2436,12 +2677,44 @@ function build(require) {
2436
2677
  }, [prefs.auto, prefs.autoFiled, entries.length, state.autoState && state.autoState.status]);
2437
2678
 
2438
2679
  const order = normalize(prefs, entries);
2439
- const labelOf = new Map(entries.map((entry) => [entry.id, entry.label]));
2680
+ const rawOf = new Map(entries.map((entry) => [entry.id, entry.label]));
2681
+ const labelOf = new Map(entries.map((entry) => [entry.id, displayName(prefs, entry.id, entry.label)]));
2440
2682
  const hidden = new Set(prefs.hidden || []);
2441
2683
  const groupsOf = prefs.groups || [];
2442
2684
  const autoOn = prefs.auto === true;
2443
2685
  const autoState = state.autoState || { status: 'idle', message: '', groups: 0 };
2444
2686
 
2687
+ const startRename = (id) => {
2688
+ cancelRenameRef.current = false;
2689
+ setEditing(id);
2690
+ setDraft(customName(prefsNow(), id));
2691
+ };
2692
+
2693
+ const finishRename = (id) => {
2694
+ // Escape unmounts the input; a trailing blur must not save the draft.
2695
+ if (cancelRenameRef.current) {
2696
+ cancelRenameRef.current = false;
2697
+ setEditing(null);
2698
+ setDraft('');
2699
+ return;
2700
+ }
2701
+ const original = rawOf.get(id) || id;
2702
+ const name = cleanName(draft);
2703
+ const names = { ...(prefsNow().names || {}) };
2704
+ if (!name || name === original) delete names[id];
2705
+ else names[id] = name;
2706
+ setEditing(null);
2707
+ setDraft('');
2708
+ commit({ ...prefsNow(), names });
2709
+ };
2710
+
2711
+ const cancelRename = () => {
2712
+ cancelRenameRef.current = true;
2713
+ setEditing(null);
2714
+ setDraft('');
2715
+ };
2716
+
2717
+
2445
2718
  const head = h(
2446
2719
  'header',
2447
2720
  { className: inSection ? 'dpc-head dpc-head--section' : 'dpc-head' },
@@ -2599,6 +2872,7 @@ function build(require) {
2599
2872
 
2600
2873
  const rows = order.map((id, position) => {
2601
2874
  const label = labelOf.get(id) || id;
2875
+ const rawLabel = rawOf.get(id) || id;
2602
2876
  const isHidden = hidden.has(id);
2603
2877
  const locked = id === PANEL_KEY;
2604
2878
  return h(
@@ -2616,8 +2890,32 @@ function build(require) {
2616
2890
  h(
2617
2891
  'span',
2618
2892
  { className: 'dpc-rowText' },
2619
- h('span', { className: 'dpc-name', title: label }, label),
2620
- h('code', { className: 'dpc-key', title: id }, id),
2893
+ editing === id
2894
+ ? h('input', {
2895
+ className: 'dpc-input dpc-nameInput',
2896
+ // autoFocus is enough: opening the editor is an explicit click.
2897
+ autoFocus: true,
2898
+ value: draft,
2899
+ placeholder: rawLabel,
2900
+ 'aria-label': t('renameIt'),
2901
+ onChange: (event) => setDraft(event.target.value),
2902
+ onKeyDown: (event) => {
2903
+ if (event.key === 'Enter') {
2904
+ event.preventDefault();
2905
+ finishRename(id);
2906
+ } else if (event.key === 'Escape') {
2907
+ event.preventDefault();
2908
+ cancelRename();
2909
+ }
2910
+ },
2911
+ onBlur: () => finishRename(id),
2912
+ })
2913
+ : h(
2914
+ 'span',
2915
+ { className: 'dpc-name', title: label === rawLabel ? rawLabel : label + ' · ' + rawLabel },
2916
+ label,
2917
+ ),
2918
+ editing === id ? null : h('code', { className: 'dpc-key', title: id }, id),
2621
2919
  ),
2622
2920
  h(
2623
2921
  'span',
@@ -2635,6 +2933,9 @@ function build(require) {
2635
2933
  ungrouped: t('ungrouped'),
2636
2934
  onPick: (next) => setGroup(id, next),
2637
2935
  }),
2936
+ iconButton('edit', t('renameIt'), () => startRename(id), false, {
2937
+ 'aria-pressed': editing === id ? 'true' : 'false',
2938
+ }),
2638
2939
  iconButton(
2639
2940
  isHidden ? 'eyeOff' : 'eye',
2640
2941
  locked ? t('lockedHint') : isHidden ? t('showIt') : t('hideIt'),
@@ -2676,6 +2977,7 @@ function build(require) {
2676
2977
  const layoutOn = prefs.guard !== false;
2677
2978
  const foldingOn = prefs.collapse !== false;
2678
2979
  const bridgeOn = prefs.bridge !== false;
2980
+ const tagsOn = prefs.tags !== false;
2679
2981
  const capNow = typeof prefs.panelMax === 'number' ? prefs.panelMax : 45;
2680
2982
  const setLayout = (patch) => commit({ ...prefsNow(), ...patch });
2681
2983
  const layoutSwitch = (on, label, onClick) =>
@@ -2726,6 +3028,11 @@ function build(require) {
2726
3028
  t('layoutBridgeHint'),
2727
3029
  layoutSwitch(bridgeOn, t('layoutBridge'), () => setLayout({ bridge: !bridgeOn })),
2728
3030
  ),
3031
+ layoutRow(
3032
+ t('layoutTags'),
3033
+ t('layoutTagsHint'),
3034
+ layoutSwitch(tagsOn, t('layoutTags'), () => setLayout({ tags: !tagsOn })),
3035
+ ),
2729
3036
  layoutRow(
2730
3037
  t('layoutMax'),
2731
3038
  null,
@@ -2928,6 +3235,7 @@ function build(require) {
2928
3235
  .dpc-glyph:empty::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.4}
2929
3236
  .dpc-rowText{display:flex;flex:1 1 auto;flex-direction:row;align-items:baseline;gap:8px;min-width:0}
2930
3237
  .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}
3238
+ .dpc-nameInput{flex:1 1 120px;max-width:320px;font-size:13px;line-height:18px}
2931
3239
  .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
3240
  .dpc-tags{display:flex;flex:none;align-items:center;gap:8px}
2933
3241
  .dpc-tag{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,rgba(127,127,127,.8));white-space:nowrap}
@@ -2942,7 +3250,9 @@ function build(require) {
2942
3250
  .dpc-pickerText{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2943
3251
  .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
3252
  .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)}
3253
+ /* Opaque popover surface: --dsw-alias-bg-layer-2 is the host's own elevated
3254
+ layer, so the menu never lets the card behind it show through. */
3255
+ .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
3256
  .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
3257
  .dpc-option:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.18));color:var(--dsw-alias-label-primary,#8a8a8a)}
2948
3258
  .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 +3355,10 @@ function build(require) {
3045
3355
  [data-dpc-list]>button{flex:none}
3046
3356
  [data-dpc-region]{flex:1 1 auto;min-height:min(120px,18vh)}
3047
3357
 
3358
+ /* The original name of a renamed row. It sits after the host title, pushed
3359
+ to the far right; the host row stays a single click target. */
3360
+ .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}
3361
+
3048
3362
  .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
3363
  .dpc-sep:hover{color:var(--dsw-alias-label-secondary,rgba(127,127,127,.95))}
3050
3364
  .dpc-sep:focus-visible{outline:2px solid var(--dsw-focus-ring-color,rgba(127,127,127,.65));outline-offset:1px}
@@ -3244,6 +3558,9 @@ function build(require) {
3244
3558
  }
3245
3559
  ctx.effect(() => () => disposeBridges(), `${PACKAGE}: bridge cleanup`);
3246
3560
  kickBridge();
3561
+ // Renames show the owning package; the first scan answers the current rows,
3562
+ // later bridge scans refresh the set when more settings pages register.
3563
+ void ensurePackages();
3247
3564
  try { window.__dpcApplied = true; } catch { /* ignore */ }
3248
3565
  }
3249
3566