@konnga/dsh-hud 0.7.0 → 0.8.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.
package/lib/client.js CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  window.__ModuleLoader__.load({
18
- id: 'dsh-hud',
18
+ id: '@konnga/dsh-hud',
19
19
  factory: (require) => {
20
20
  var module = { exports: {} };
21
21
  var exports = module.exports;
@@ -44,7 +44,16 @@ window.__ModuleLoader__.load({
44
44
  * 那些 chip 是"扫一眼"的信息(哪个会话脏了),不值得跟面板一样频繁;宿主侧还按目录
45
45
  * 缓存 + 去重,多会话共用一个目录只跑一次 git。
46
46
  */
47
- var ROWS_POLL_MS = 12000;
47
+ // 侧栏标记删掉之后,这份数据只剩"悬停卡片 / ⋯ 菜单"两处**按需展示**在用 ——
48
+ // 所以从 12s 放宽到 30s:按下不表的东西没必要那么勤地去问 git。
49
+ var ROWS_POLL_MS = 30000;
50
+ /**
51
+ * 工具活动的节拍。比面板那 8s 密得多:宿主这一端是**内存读**(不碰 git、不跑子进程),
52
+ * 密一点才能让"正在跑什么"真的看到;代价用"内容没变就不 set"抵掉。
53
+ */
54
+ var TOOLS_POLL_MS = 3000;
55
+ /** 胶囊上最多铺几个技能名(其余收成 `+k`,完整清单在悬停里)。 */
56
+ var SKILLS_SHOWN = 2;
48
57
  /** 面板默认宽度与“停靠在输入框上方”的锚点高度。 */
49
58
  var PANEL_WIDTH = 380;
50
59
  var COMPOSER_OFFSET = 168;
@@ -58,6 +67,8 @@ window.__ModuleLoader__.load({
58
67
  * 它夹在左栏与差异栏之间,是固定的——右边缘那个抓握仍只调整差异栏。
59
68
  */
60
69
  var COMMIT_LIST_WIDTH = 240;
70
+ /** 提交明细栏的最小宽度(它装的是文件路径,不能太窄;默认 240 只是起点,可拖)。 */
71
+ var COMMIT_MIN_WIDTH = 180;
61
72
 
62
73
 
63
74
  /** diff 模式的面板:更宽,且用固定高度(避免 bottom 锚定下"内容变高就跳位")。 */
@@ -93,12 +104,15 @@ window.__ModuleLoader__.load({
93
104
  "panel.clickForDiff": "点击一行查看该文件的差异",
94
105
  "panel.rowKeys": "在面板里按过之后再按 ↑/↓ 选行、Enter 打开",
95
106
  "panel.commits": "最近提交",
96
- "time.now": "刚刚",
97
- "time.minutes": " 分钟前",
98
- "time.hours": " 小时前",
99
- "time.days": " 天前",
107
+ "panel.tags": "标签",
108
+ "panel.tagsEmpty": "这个仓库还没有标签",
109
+ "panel.tagsCapped": "只显示最近 {n} 个",
110
+ "tag.click": "点一下看它指向的提交",
111
+ "tag.annotated": "附注标签",
112
+ "tag.lightweight": "轻量标签",
100
113
  "diff.close": "关闭差异",
101
114
  "diff.resize": "拖动右边缘调整差异宽度",
115
+ "diff.resizeCommit": "拖动调整提交明细栏宽度",
102
116
  "diff.worktree": "未暂存",
103
117
  "diff.index": "已暂存",
104
118
  "diff.wrap": "自动换行",
@@ -119,14 +133,15 @@ window.__ModuleLoader__.load({
119
133
  "commit.click": "点击查看这次提交改了什么",
120
134
  // ── 会话归因(本会话改过哪些文件) ──
121
135
  "session.line": "本会话改过 {n} 个文件",
122
- "rows.mode.always": "侧栏 常亮",
123
- "rows.mode.hover": "侧栏 悬停",
124
- "rows.mode.off": "侧栏 关",
125
- "rows.modeTitle": "点一下切换:常亮(行首小标记)→ 悬停(动条上完整那枚)→ 关。当前:{mode}",
136
+ "tools.now": "现在在跑:{name}",
137
+ "tools.total": "本会话共 {n} 次工具调用",
138
+ "tools.byTool": "各工具:",
139
+ "tools.skills": "skills:",
140
+ "tools.recent": "最近:",
126
141
  "rows.ownNone": "本会话没有改动记录",
127
142
  "rows.noRepo": "不在 git 仓库里",
128
143
  "rows.error": "读取失败({code})",
129
- "rows.possible": "另有 {n} 个可能相关(命令旁听)",
144
+ "rows.possible": "命令旁听到 {n} 个可能改动的路径({now} 个现在有差异)",
130
145
  "review.count": "已审 {n}/{total}",
131
146
  "review.staleCount": "{n} 个改动已变,需要重看",
132
147
  "review.mark": "标成「这一版我审过了」(键盘:v)",
@@ -200,12 +215,15 @@ window.__ModuleLoader__.load({
200
215
  "panel.clickForDiff": "Click a file to see its diff",
201
216
  "panel.rowKeys": "Once you have clicked in the panel: ↑/↓ to select a row, Enter to open it",
202
217
  "panel.commits": "Recent commits",
203
- "time.now": "just now",
204
- "time.minutes": "m ago",
205
- "time.hours": "h ago",
206
- "time.days": "d ago",
218
+ "panel.tags": "Tags",
219
+ "panel.tagsEmpty": "No tags in this repository yet",
220
+ "panel.tagsCapped": "Showing the newest {n}",
221
+ "tag.click": "Click to open the commit it points at",
222
+ "tag.annotated": "Annotated tag",
223
+ "tag.lightweight": "Lightweight tag",
207
224
  "diff.close": "Close diff",
208
225
  "diff.resize": "Drag the right edge to resize the diff",
226
+ "diff.resizeCommit": "Drag to resize the commit pane",
209
227
  "diff.worktree": "Unstaged",
210
228
  "diff.index": "Staged",
211
229
  "diff.wrap": "Wrap lines",
@@ -225,14 +243,15 @@ window.__ModuleLoader__.load({
225
243
  "commit.unknown": "Commit not found: it may have been rebased or amended away — refresh the panel",
226
244
  "commit.click": "Click to see what this commit changed",
227
245
  "session.line": "{n} file(s) changed by this session",
228
- "rows.mode.always": "Sidebar on",
229
- "rows.mode.hover": "Sidebar hover",
230
- "rows.mode.off": "Sidebar off",
231
- "rows.modeTitle": "Click to cycle: always (leading token) → hover (chip in the action strip) → off. Now: {mode}",
246
+ "tools.now": "Running now: {name}",
247
+ "tools.total": "{n} tool call(s) this session",
248
+ "tools.byTool": "By tool:",
249
+ "tools.skills": "skills:",
250
+ "tools.recent": "Recent:",
232
251
  "rows.ownNone": "this session has no recorded changes",
233
252
  "rows.noRepo": "not in a git repository",
234
253
  "rows.error": "read failed ({code})",
235
- "rows.possible": "{n} more possibly related (from commands)",
254
+ "rows.possible": "{n} path(s) commands may have touched ({now} still differ)",
236
255
  "review.count": "{n}/{total} reviewed",
237
256
  "review.staleCount": "{n} changed since reviewed",
238
257
  "review.mark": "Mark this revision as reviewed (key: v)",
@@ -301,10 +320,12 @@ window.__ModuleLoader__.load({
301
320
  modified: "var(--dsw-alias-state-warn-primary, #d9a63c)",
302
321
  added: "var(--dsw-alias-state-success-primary, #3fb950)",
303
322
  deleted: "var(--dsw-alias-state-error-primary, #f06a6a)",
304
- renamed: "var(--dsw-alias-brand-primary, #4b8bf5)",
323
+ // 与 codex-hud 的 `statEntries` 一一对应:R→cyan、C→brightBlue(我们的品牌蓝就是这个位)、
324
+ // T→magenta、?→dim。M/A/D/! 本来就一致(yellow/green/red/critical)。
325
+ renamed: "var(--dsh-hud-cyan, #22d3ee)",
305
326
  copied: "var(--dsw-alias-brand-primary, #4b8bf5)",
306
- typeChanged: "var(--dsw-alias-state-idle-primary, #8a8f98)",
307
- untracked: "var(--dsw-alias-label-secondary, #8a8f98)",
327
+ typeChanged: "var(--dsh-hud-magenta, #d946ef)",
328
+ untracked: "var(--dsw-alias-label-dimmed, #8a8f98)",
308
329
  conflicted: "var(--dsw-alias-state-error-primary, #f06a6a)",
309
330
  };
310
331
 
@@ -325,6 +346,8 @@ window.__ModuleLoader__.load({
325
346
 
326
347
  /** 胶囊里分支名的显示上限;超出就中段省略,别把简写符挤出可见区。 */
327
348
  var PILL_BRANCH_MAX = 28;
349
+ /** 胶囊里工作区名最多几个字符(它在分支前面,不该抢分支的位置)。 */
350
+ var PILL_WORKSPACE_MAX = 20;
328
351
 
329
352
  /**
330
353
  * 差异行的底色:半透明,压在面板表面色之上,因此亮/暗主题都可读
@@ -355,9 +378,6 @@ window.__ModuleLoader__.load({
355
378
  }
356
379
  }
357
380
 
358
- /** 侧栏状态的三个档位(顺序就是点一下的循环顺序)。 */
359
- var SIDEBAR_MODES = ["always", "hover", "off"];
360
-
361
381
  /** 「审阅进度」的存档键:按"工作区 + 文件"记下审过的那一版签名。 */
362
382
  var REVIEWED_KEY = "dsh-hud.reviewed.v1";
363
383
  /**
@@ -644,8 +664,17 @@ window.__ModuleLoader__.load({
644
664
  /** 最近提交;`commitsOid` 记住它是哪个 HEAD 取的,oid 变了才重取。 */
645
665
  commits: null,
646
666
  commitsOid: null,
667
+ /**
668
+ * 标签列表。与提交不同,**新 tag 可以打在老提交上**(HEAD 不动),所以不能只按 oid 判断;
669
+ * 改用"上次取的时刻"做新鲜度窗口:面板开着时每次刷新都看一眼,但 10s 内不重复问 git。
670
+ */
671
+ tags: null,
672
+ tagsAt: 0,
673
+ tagsTruncated: false,
647
674
  /** 差异栏宽度(面板右边缘可拖),持久化。 */
648
675
  diffWidth: typeof ui.diffWidth === "number" ? ui.diffWidth : DIFF_DEFAULT_WIDTH,
676
+ /** 提交明细栏宽度(它与差异栏之间那条抓握可拖),持久化。 */
677
+ commitWidth: typeof ui.commitWidth === "number" ? ui.commitWidth : COMMIT_LIST_WIDTH,
649
678
  /**
650
679
  * 会话归因:本会话改过哪些文件(宿主半按 fs 事件记的)。
651
680
  *
@@ -657,13 +686,14 @@ window.__ModuleLoader__.load({
657
686
  onlySession: ui.onlySession === true,
658
687
  /** 「只看最近一轮」开关:把清单收窄到最近这一轮动过的文件。 */
659
688
  onlyLatest: ui.onlyLatest === true,
660
- /**
661
- * 侧栏状态显示档位:`always` 常亮(行首 16px 格放紧凑标记)/ `hover` 悬停(动条上放完整那枚)/
662
- * `off` 一处都不画。默认常亮 —— "哪个会话脏了"这件事本来就该扫一眼看到。
663
- */
664
- sidebarMode: SIDEBAR_MODES.indexOf(ui.sidebarMode) === -1 ? "always" : ui.sidebarMode,
665
689
  /** 这一版每个改动文件的签名(`file → signature`);空表表示"这次没要"。 */
666
690
  review: {},
691
+ /**
692
+ * 本会话的工具活动:`active`(正在跑哪个)+ `recent` + 计数。
693
+ *
694
+ * 实时秒数由客户端本地算(`active.startedAt`),不靠把轮询压到亚秒级。
695
+ */
696
+ tools: { phase: "idle", active: null, recent: [], counts: [], skills: [], skillsDropped: 0, total: 0, updatedAt: null },
667
697
  /** 侧栏行内 chip 要的摘要:一次批量取回,按 sessionId 索引。 */
668
698
  rows: { phase: "idle", byId: {}, unavailable: false, truncated: false, updatedAt: null },
669
699
  /**
@@ -686,6 +716,8 @@ window.__ModuleLoader__.load({
686
716
  var commitInflight = null;
687
717
  /** 会话归因那次请求(在途合并)。 */
688
718
  var sessionInflight = null;
719
+ /** 工具活动那次请求(在途合并)。 */
720
+ var toolsInflight = null;
689
721
  /** 侧栏摘要那次请求(在途合并)。 */
690
722
  var rowsInflight = null;
691
723
  /** 回合投影的订阅:`{ sessionId, dispose }`;`null` 表示没接。 */
@@ -719,14 +751,22 @@ window.__ModuleLoader__.load({
719
751
  writeUi({
720
752
  onlySession: state.onlySession === true,
721
753
  onlyLatest: state.onlyLatest === true,
722
- sidebarMode: state.sidebarMode,
723
754
  open: state.panelOpen,
724
755
  pos: state.pos,
725
756
  wrap: state.diff ? state.diff.wrap === true : false,
726
757
  diffWidth: state.diffWidth,
758
+ commitWidth: state.commitWidth,
727
759
  });
728
760
  }
729
761
 
762
+ /** 拖动提交栏右边那条抓握时只改内存;松手才落盘。 */
763
+ function setCommitWidth(width) {
764
+ if (typeof width !== "number" || !Number.isFinite(width)) return;
765
+ var next = Math.max(COMMIT_MIN_WIDTH, Math.round(width));
766
+ if (state.commitWidth === next) return;
767
+ set({ commitWidth: next });
768
+ }
769
+
730
770
  /** 拖动右边缘时只改内存;松手才落盘(persistLayout)。 */
731
771
  function setDiffWidth(width) {
732
772
  if (typeof width !== "number" || !Number.isFinite(width)) return;
@@ -1122,9 +1162,34 @@ window.__ModuleLoader__.load({
1122
1162
  });
1123
1163
  }
1124
1164
 
1165
+ /** 取标签列表(面板开着时才取)。失败只让这一段不显示。 */
1166
+ function loadTags(cwd) {
1167
+ // 10s 新鲜度窗口:面板每 8s 刷新一次,这里避免每次都对 git 跑一遍 for-each-ref;
1168
+ // 又能让"刚打的新 tag"在十来秒内出现(HEAD 没动也得看到)。
1169
+ if (state.tags !== null && Date.now() - state.tagsAt < 10000) return Promise.resolve();
1170
+ return Promise.resolve()
1171
+ .then(function () {
1172
+ return ctx.connection.rpc.call(CHANNEL, "tags", { args: { cwd: cwd } });
1173
+ })
1174
+ .then(function (result) {
1175
+ if (state.cwd !== cwd) return;
1176
+ var value = result && result.ok === true ? result.value : null;
1177
+ var tags = value && Array.isArray(value.tags) ? value.tags : [];
1178
+ probe.log("tags", { count: tags.length, truncated: value ? value.truncated === true : false });
1179
+ set({ tags: tags, tagsAt: Date.now(), tagsTruncated: value ? value.truncated === true : false });
1180
+ })
1181
+ .catch(function () {
1182
+ if (state.cwd !== cwd) return;
1183
+ probe.log("tags-failed", { code: "transport" });
1184
+ set({ tags: [] , tagsAt: Date.now(), tagsTruncated: false });
1185
+ });
1186
+ }
1187
+
1125
1188
  function refresh() {
1126
1189
  // 会话归因跟 status 同一条命(两个都是"当前会话 + 当前工作区"的只读事实)。
1127
1190
  loadSessionChanges();
1191
+ // 工具活动只跟会话有关(没有工作区也该显示"正在跑什么")。
1192
+ loadTools();
1128
1193
  // 回合投影:会话还没物化时这里会安静地不接,下一次刷新再试。
1129
1194
  bindTurns(state.sessionId);
1130
1195
  if (!state.cwd) return Promise.resolve();
@@ -1158,6 +1223,8 @@ window.__ModuleLoader__.load({
1158
1223
  && (state.commits === null || state.commitsOid !== statusValue.oid)) {
1159
1224
  loadCommits(cwd, statusValue.oid);
1160
1225
  }
1226
+ // 标签与 HEAD 无关(新 tag 可以打在老提交上),所以只要面板开着就问一次(10s 窗口)。
1227
+ if (statusValue && state.panelOpen) loadTags(cwd);
1161
1228
  return;
1162
1229
  }
1163
1230
  var code = result && result.error && result.error.details ? result.error.details.code : null;
@@ -1182,6 +1249,9 @@ window.__ModuleLoader__.load({
1182
1249
  errorCode: null,
1183
1250
  commits: null,
1184
1251
  commitsOid: null,
1252
+ tags: null,
1253
+ tagsAt: 0,
1254
+ tagsTruncated: false,
1185
1255
  // 差异与提交明细都绑定在某个工作区上:换了工作区就把它们收起,
1186
1256
  // 否则会显示另一个仓库的文件或提交。键盘光标同理,它指向的是清单里的行。
1187
1257
  diff: null,
@@ -1189,6 +1259,8 @@ window.__ModuleLoader__.load({
1189
1259
  cursor: null,
1190
1260
  phase: cwd ? "loading" : "idle",
1191
1261
  });
1262
+ // 工具活动同理:换会话就清空,别显示上一个会话正在跑的工具。
1263
+ set({ tools: { phase: "idle", active: null, recent: [], counts: [], skills: [], skillsDropped: 0, total: 0, updatedAt: null } });
1192
1264
  // 归因是"这个会话"的事实:换会话就先清空,别把上一个会话的标记留在新会话的清单上。
1193
1265
  set({ sessionChanges: { phase: "idle", files: null, possible: null, commands: [], since: null, dropped: 0, possibleDropped: 0, errorCode: null } });
1194
1266
  // 回合信息同理:换会话就按新会话重新接(旧的先解掉)。
@@ -1396,6 +1468,61 @@ window.__ModuleLoader__.load({
1396
1468
  return rowsInflight;
1397
1469
  }
1398
1470
 
1471
+ /** 取本会话的工具活动(正在跑哪个 / 跑过哪些 / 各几次)。 */
1472
+ function loadTools() {
1473
+ var sessionId = state.sessionId;
1474
+ if (!sessionId) return Promise.resolve();
1475
+ if (toolsInflight) return toolsInflight;
1476
+ toolsInflight = Promise.resolve()
1477
+ .then(function () {
1478
+ return ctx.connection.rpc.call(CHANNEL, "session-tools", { args: { sessionId: sessionId } });
1479
+ })
1480
+ .then(function (result) {
1481
+ toolsInflight = null;
1482
+ if (state.sessionId !== sessionId) return;
1483
+ if (result && result.ok === true && result.value) {
1484
+ var value = result.value;
1485
+ var tools = {
1486
+ phase: "ready",
1487
+ active: value.active && typeof value.active === "object" ? value.active : null,
1488
+ recent: Array.isArray(value.recent) ? value.recent : [],
1489
+ counts: Array.isArray(value.counts) ? value.counts : [],
1490
+ skills: Array.isArray(value.skills) ? value.skills : [],
1491
+ skillsDropped: typeof value.skillsDropped === "number" ? value.skillsDropped : 0,
1492
+ total: typeof value.total === "number" ? value.total : 0,
1493
+ updatedAt: Date.now(),
1494
+ };
1495
+ // 内容没变就不要 set:这个节拍很密(见 TOOLS_POLL_MS),无谓的重渲染会到处都是。
1496
+ var previous = state.tools;
1497
+ var same = previous !== null && previous.active !== null && tools.active !== null
1498
+ ? previous.active.callId === tools.active.callId && previous.total === tools.total
1499
+ : previous !== null && previous.active === null && tools.active === null
1500
+ && previous.total === tools.total
1501
+ && previous.recent.length === tools.recent.length
1502
+ && (previous.recent.length === 0 || previous.recent[0].at === tools.recent[0].at);
1503
+ if (same) return;
1504
+ probe.log("session-tools", {
1505
+ active: tools.active === null ? null : tools.active.name,
1506
+ total: tools.total,
1507
+ kinds: tools.counts.length,
1508
+ // 技能单独露面:排查"技能名有没有记到"时不必再靠猜(这一版新增的字段)。
1509
+ skills: tools.skills.length,
1510
+ });
1511
+ set({ tools: tools });
1512
+ return;
1513
+ }
1514
+ var code = result && result.error && result.error.details ? result.error.details.code : null;
1515
+ probe.log("session-tools-failed", { code: code || "transport" });
1516
+ set({ tools: { phase: "error", active: null, recent: [], counts: [], skills: [], skillsDropped: 0, total: 0, updatedAt: Date.now() } });
1517
+ })
1518
+ .catch(function () {
1519
+ toolsInflight = null;
1520
+ probe.log("session-tools-failed", { code: "transport" });
1521
+ set({ tools: { phase: "error", active: null, recent: [], counts: [], skills: [], skillsDropped: 0, total: 0, updatedAt: Date.now() } });
1522
+ });
1523
+ return toolsInflight;
1524
+ }
1525
+
1399
1526
  /** 审阅存档的键:**按工作区分开**,同一个文件名在别的仓库里是另一码事。 */
1400
1527
  function reviewedKeyOf(file) {
1401
1528
  return reviewKeyOf(state.cwd, file);
@@ -1443,14 +1570,6 @@ window.__ModuleLoader__.load({
1443
1570
  set({ reviewed: writeReviewed(next) });
1444
1571
  }
1445
1572
 
1446
- /** 侧栏状态档位:常亮 → 悬停 → 不显示,点一下走一档。 */
1447
- function cycleSidebarMode() {
1448
- var index = SIDEBAR_MODES.indexOf(state.sidebarMode);
1449
- var next = SIDEBAR_MODES[(index + 1) % SIDEBAR_MODES.length];
1450
- set({ sidebarMode: next });
1451
- persist();
1452
- }
1453
-
1454
1573
  /** 「只看最近一轮」开关:过滤发生在 `visibleChangeRows`(渲染与键盘共用)。 */
1455
1574
  function toggleOnlyLatest() {
1456
1575
  set({ onlyLatest: state.onlyLatest !== true });
@@ -1502,6 +1621,7 @@ window.__ModuleLoader__.load({
1502
1621
  resetPanel: resetPanel,
1503
1622
  setAnchorTop: setAnchorTop,
1504
1623
  setDiffWidth: setDiffWidth,
1624
+ setCommitWidth: setCommitWidth,
1505
1625
  persistLayout: persist,
1506
1626
  openDiff: openDiff,
1507
1627
  closeDiff: closeDiff,
@@ -1519,7 +1639,6 @@ window.__ModuleLoader__.load({
1519
1639
  toggleOnlySession: toggleOnlySession,
1520
1640
  toggleOnlyLatest: toggleOnlyLatest,
1521
1641
  refreshRows: refreshRows,
1522
- cycleSidebarMode: cycleSidebarMode,
1523
1642
  toggleReviewed: toggleReviewed,
1524
1643
  clearReviewed: clearReviewed,
1525
1644
  };
@@ -1781,15 +1900,6 @@ window.__ModuleLoader__.load({
1781
1900
  onClick: props.onClearReviewed,
1782
1901
  }, t("review.clearShort"))
1783
1902
  : null,
1784
- // 侧栏状态档位:常亮 → 悬停 → 关(默认常亮)。
1785
- h("button", {
1786
- type: "button",
1787
- className: "dshh-sessionToggle" + (props.sidebarMode === "always" ? " dshh-sessionToggleOn" : ""),
1788
- "data-session-filter": "sidebar",
1789
- "data-sidebar-mode": props.sidebarMode,
1790
- title: t("rows.modeTitle", { mode: t("rows.mode." + props.sidebarMode) }),
1791
- onClick: props.onCycleSidebarMode,
1792
- }, props.sidebarMode === "always" ? "◉ " : (props.sidebarMode === "hover" ? "◑ " : "○ "), t("rows.mode." + props.sidebarMode)),
1793
1903
  // 第二枚开关只在"这一轮的归属拿得到"时出现:拿不到就整枚不画,别给一个按不动的按钮。
1794
1904
  view === null || view.latestTurn === null
1795
1905
  ? null
@@ -1957,15 +2067,24 @@ window.__ModuleLoader__.load({
1957
2067
  /** 相对时间;超过 30 天直接给提交自己的日期。 */
1958
2068
  function commitWhen(commit, t) {
1959
2069
  if (typeof commit.committedAt !== "number") return commit.date || "";
1960
- var seconds = Math.max(0, Math.round((Date.now() - commit.committedAt) / 1000));
1961
- if (seconds < 60) return t("time.now");
1962
- var minutes = Math.round(seconds / 60);
1963
- if (minutes < 60) return String(minutes) + t("time.minutes");
1964
- var hours = Math.round(minutes / 60);
1965
- if (hours < 24) return String(hours) + t("time.hours");
1966
- var days = Math.round(hours / 24);
1967
- if (days < 30) return String(days) + t("time.days");
1968
- return commit.date || "";
2070
+ return fullWhen(commit.committedAt);
2071
+ }
2072
+
2073
+ /**
2074
+ * 时刻一律给完整年月日 + 时分(本地时区):`2026-10-16 14:18`。
2075
+ *
2076
+ * 两级简化都被否掉了:先是从"2 小时前"改成具体时间(相对时间在对账场景不够用),
2077
+ * 再是"今天只给 `14:18`"也被否 —— 用户要**年月日都在**。所以不再分档,一视同仁。
2078
+ * 只到分钟:提交/标签不需要秒,列一排在面板里也塞得下。
2079
+ */
2080
+ function fullWhen(at) {
2081
+ var then = new Date(at);
2082
+ return String(then.getFullYear()) + "-" + pad2(then.getMonth() + 1) + "-" + pad2(then.getDate())
2083
+ + " " + pad2(then.getHours()) + ":" + pad2(then.getMinutes());
2084
+ }
2085
+
2086
+ function pad2(value) {
2087
+ return value < 10 ? "0" + String(value) : String(value);
1969
2088
  }
1970
2089
 
1971
2090
  /**
@@ -1982,7 +2101,8 @@ window.__ModuleLoader__.load({
1982
2101
  h("div", { className: "dshh-commits" }, commits.map(function (commit, index) {
1983
2102
  var refs = commit.refs || [];
1984
2103
  var isHead = refs.some(function (ref) { return ref.indexOf("HEAD") === 0; });
1985
- var tip = [t("commit.click"), "", commit.subject, commit.author + " · " + commit.date].concat(refs).join("\n");
2104
+ var when = typeof commit.committedAt === "number" ? fullWhen(commit.committedAt) : (commit.date || "");
2105
+ var tip = [t("commit.click"), "", commit.subject, commit.author + " · " + when].concat(refs).join("\n");
1986
2106
  // 整行是按钮:点了在左栏换成这次提交的改动清单(只读)。
1987
2107
  var open = sameCommit(commit.sha, props.activeSha);
1988
2108
  return h("button", {
@@ -2001,6 +2121,58 @@ window.__ModuleLoader__.load({
2001
2121
  })));
2002
2122
  }
2003
2123
 
2124
+ /**
2125
+ * 标签列表:一行一个,tag 名 + 它指向的提交 sha + 相对时间;指向当前 HEAD 的标 HEAD。
2126
+ * 点一行 = 用右栏的提交明细看这个 tag 指向的那次提交(复用同一条管道,不新增界面)。
2127
+ */
2128
+ function TagList(props) {
2129
+ // 原样保留 `null`("还没取到")—— 写成 `props.tags || []` 会把"没取到"和"确实没有"合并,
2130
+ // 于是面板一打开就先闪一行"还没有标签"。
2131
+ var tags = props.tags;
2132
+ var t = props.t;
2133
+ // `null` = 还没取到(先不画,避免闪一下"还没有标签");`[]` = 取回了、确实一个都没有 →
2134
+ // 这时要**画一行写明**:整段不画的话,用户没法区分"这个仓库没有 tag"与"这个功能没做"(实测踩过)。
2135
+ if (!Array.isArray(tags)) return null;
2136
+ if (tags.length === 0) {
2137
+ return h("div", { className: "dshh-section" },
2138
+ h("div", { className: "dshh-sectionTitle" }, t("panel.tags")),
2139
+ h("div", { className: "dshh-note", "data-tags-empty": "1" }, t("panel.tagsEmpty")));
2140
+ }
2141
+ var headSha = props.headSha;
2142
+
2143
+ return h("div", { className: "dshh-section" },
2144
+ h("div", { className: "dshh-sectionTitle" }, t("panel.tags")),
2145
+ h("div", { className: "dshh-commits" }, tags.map(function (tag, index) {
2146
+ var isHead = typeof headSha === "string" && headSha.length > 0
2147
+ && (tag.sha === headSha || headSha.indexOf(tag.sha) === 0);
2148
+ var tip = [
2149
+ t("tag.click"),
2150
+ "",
2151
+ tag.subject || "",
2152
+ t(tag.annotated ? "tag.annotated" : "tag.lightweight") + " · " + tag.sha + " · "
2153
+ + (typeof tag.committedAt === "number" ? fullWhen(tag.committedAt) : (tag.date || "")),
2154
+ ].join("\n");
2155
+ var open = sameCommit(tag.sha, props.activeSha);
2156
+ return h("button", {
2157
+ type: "button",
2158
+ className: "dshh-commit" + (open ? " dshh-commitActive" : ""),
2159
+ "data-tag-row": tag.name,
2160
+ key: tag.name + ":" + String(index),
2161
+ title: tip,
2162
+ onClick: function () {
2163
+ if (props.onOpen) props.onOpen(tag);
2164
+ },
2165
+ },
2166
+ h("span", { className: "dshh-ctag" }, tag.name),
2167
+ h("span", { className: "dshh-csha" }, tag.sha),
2168
+ isHead ? h("span", { className: "dshh-chead" }, "HEAD") : null,
2169
+ h("span", { className: "dshh-cwhen" }, commitWhen(tag, t)));
2170
+ })),
2171
+ props.truncated === true
2172
+ ? h("div", { className: "dshh-note" }, t("panel.tagsCapped", { n: String(tags.length) }))
2173
+ : null);
2174
+ }
2175
+
2004
2176
  /**
2005
2177
  * 一次提交的那一栏(**中间**栏,固定宽度):元信息 + 这次提交的改动清单。
2006
2178
  *
@@ -2224,12 +2396,33 @@ window.__ModuleLoader__.load({
2224
2396
 
2225
2397
  /** 分支名过长时中段省略,保留头尾信息量。 */
2226
2398
  function shortBranch(name) {
2227
- if (name.length <= PILL_BRANCH_MAX) return name;
2228
- var head = Math.ceil((PILL_BRANCH_MAX - 1) / 2);
2229
- var tail = PILL_BRANCH_MAX - 1 - head;
2399
+ return middleEllipsis(name, PILL_BRANCH_MAX);
2400
+ }
2401
+
2402
+ /** 从中间省略:保留头尾(路径/分支名两头的区分度都比中间高)。 */
2403
+ function middleEllipsis(name, max) {
2404
+ if (name.length <= max) return name;
2405
+ var head = Math.ceil((max - 1) / 2);
2406
+ var tail = max - 1 - head;
2230
2407
  return name.slice(0, head) + "…" + name.slice(name.length - tail);
2231
2408
  }
2232
2409
 
2410
+ /**
2411
+ * 工作区名 = 会话 cwd 的目录名(侧栏给工作区起的名字就是它)。
2412
+ *
2413
+ * 取的是 **cwd** 而不是仓库顶层:会话开在子目录时,用户认的是那个子目录;
2414
+ * 完整路径在悬停与面板里,胶囊上只要一个能认出来的短名字。
2415
+ */
2416
+ function workspaceNameOf(cwd) {
2417
+ if (typeof cwd !== 'string' || cwd.length === 0) return null;
2418
+ var trimmed = cwd.replace(/[/\\]+$/, '');
2419
+ if (trimmed.length === 0) return null;
2420
+ var index = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
2421
+ var name = index === -1 ? trimmed : trimmed.slice(index + 1);
2422
+ if (name.length === 0) return null;
2423
+ return middleEllipsis(name, PILL_WORKSPACE_MAX);
2424
+ }
2425
+
2233
2426
  /**
2234
2427
  * 改动计数 → 简写符列表 `[{ key, mark, value }]`(只列非零项,顺序固定)。
2235
2428
  *
@@ -2290,7 +2483,8 @@ window.__ModuleLoader__.load({
2290
2483
  return {
2291
2484
  row: row,
2292
2485
  mine: mineCount,
2293
- possible: possibleCount,
2486
+ possibleNow: typeof row.possibleNow === "number" ? row.possibleNow : 0,
2487
+ possible: possibleCount,
2294
2488
  shorthands: shorthands,
2295
2489
  changed: changed,
2296
2490
  dirty: row.repo === true && row.dirty === true,
@@ -2298,11 +2492,6 @@ window.__ModuleLoader__.load({
2298
2492
  };
2299
2493
  }
2300
2494
 
2301
- /** 两位数以上收成 `+`(行首那格只容得下两个字符)。 */
2302
- function compactCount(value) {
2303
- return value >= 10 ? "+" : String(value);
2304
- }
2305
-
2306
2495
  /**
2307
2496
  * `⋯` 菜单里的只读摘要(座位 `sidebar.workspaces.session.menu.item`)。
2308
2497
  *
@@ -2318,7 +2507,7 @@ window.__ModuleLoader__.load({
2318
2507
  var sessionId = props.sessionId;
2319
2508
  var store = props.store;
2320
2509
  var state = useStore(store);
2321
- if (!sessionId || state.sidebarMode === "off") return null;
2510
+ if (!sessionId) return null;
2322
2511
  var row = state.rows && state.rows.byId ? state.rows.byId[sessionId] : undefined;
2323
2512
  var summary = rowSummaryOf(row);
2324
2513
  if (summary === null) return null;
@@ -2341,7 +2530,9 @@ window.__ModuleLoader__.load({
2341
2530
 
2342
2531
  var own = [];
2343
2532
  if (summary.mine > 0) own.push(t("session.pill", { n: String(summary.mine) }));
2344
- if (summary.possible > 0) own.push(t("rows.possible", { n: String(summary.possible) }));
2533
+ if (summary.possible > 0) {
2534
+ own.push(t("rows.possible", { n: String(summary.possible), now: String(summary.possibleNow) }));
2535
+ }
2345
2536
  var ownLine = own.length === 0 ? t("rows.ownNone") : own.join(" · ");
2346
2537
 
2347
2538
  return h("div", {
@@ -2354,76 +2545,6 @@ window.__ModuleLoader__.load({
2354
2545
  ]);
2355
2546
  }
2356
2547
 
2357
- /**
2358
- * 常亮那一枚(座位 `sidebar.session.row.leading`)。
2359
- *
2360
- * 产品只给 16px 宽(`.slot{width:16px;flex:none}`),所以这里只放**一个双字符标记**;
2361
- * 优先级 = 价值顺序:先报"本会话改过几个"(只有我们有),没有就报"这个工作区脏不脏"。
2362
- * 产品自己在那一格显示"会话状态点"时这个座位不会被渲染 —— 那时退回悬停档。
2363
- */
2364
- function SessionLeadingToken(props) {
2365
- var t = props.t || function (key) { return key; };
2366
- var sessionId = props.sessionId;
2367
- var store = props.store;
2368
- var state = useStore(store);
2369
- if (!sessionId || state.sidebarMode !== "always") return null;
2370
- var row = state.rows && state.rows.byId ? state.rows.byId[sessionId] : undefined;
2371
- var summary = rowSummaryOf(row);
2372
- if (summary === null || !summary.hasContent) return null;
2373
- var token = null;
2374
- if (summary.mine > 0) token = { text: "●" + compactCount(summary.mine), kind: "mine" };
2375
- else if (summary.possible > 0) token = { text: "◐" + compactCount(summary.possible), kind: "possible" };
2376
- else if (summary.dirty) token = { text: "!" + compactCount(summary.changed), kind: "dirty" };
2377
- if (token === null) return null;
2378
- probe.log("row-token", { sessionId: sessionId, text: token.text, kind: token.kind });
2379
- return h("span", {
2380
- className: "dshh-rowtoken dshh-rowtoken-" + token.kind,
2381
- "data-dsh-hud": "row-token",
2382
- title: rowChipTitle(summary.row, t),
2383
- }, token.text);
2384
- }
2385
-
2386
- /**
2387
- * 悬停动条上那枚(座位 `sidebar.workspaces.session.row.action`)。
2388
- *
2389
- * 动作条只有 32px 高、平时就放一两个图标按钮,所以只放"图标级"宽度:
2390
- * `!` 脏 + 改动总数 + `●n`/`◐n` 本会话改动数;明细在悬停里。
2391
- */
2392
- function SessionRowChip(props) {
2393
- var t = props.t || function (key) { return key; };
2394
- var sessionId = props.sessionId;
2395
- var store = props.store;
2396
- var state = useStore(store);
2397
- if (!sessionId || state.sidebarMode !== "hover") return null;
2398
- var row = state.rows && state.rows.byId ? state.rows.byId[sessionId] : undefined;
2399
- var summary = rowSummaryOf(row);
2400
- if (summary === null || !summary.hasContent) return null;
2401
-
2402
- var children = [];
2403
- if (summary.row.repo === true) {
2404
- var head = summary.dirty ? "!" : "";
2405
- if (summary.changed > 0) head += String(summary.changed);
2406
- if (head !== "") children.push(h("span", { key: "git" }, head));
2407
- }
2408
- if (summary.mine > 0) children.push(h("span", { key: "mine", className: "dshh-rowchipMine" }, "●" + String(summary.mine)));
2409
- else if (summary.possible > 0) children.push(h("span", { key: "possible", className: "dshh-rowchipPossible" }, "◐" + String(summary.possible)));
2410
-
2411
- if (children.length === 0) return null;
2412
- probe.log("row-chip", {
2413
- sessionId: sessionId,
2414
- repo: summary.row.repo === true,
2415
- dirty: summary.dirty,
2416
- changed: summary.changed,
2417
- mine: summary.mine,
2418
- possible: summary.possible,
2419
- });
2420
- return h("span", {
2421
- className: "dshh-rowchip",
2422
- "data-dsh-hud": "row-chip",
2423
- title: rowChipTitle(summary.row, t),
2424
- }, children);
2425
- }
2426
-
2427
2548
  /**
2428
2549
  * 侧栏会话行**悬停卡片**里的那一块(座位 `sidebar.session.row.hover`)。
2429
2550
  *
@@ -2436,10 +2557,10 @@ window.__ModuleLoader__.load({
2436
2557
  var sessionId = props.sessionId;
2437
2558
  var store = props.store;
2438
2559
  var state = useStore(store);
2439
- if (!sessionId || state.sidebarMode === "off") return null;
2560
+ if (!sessionId) return null;
2440
2561
  var row = state.rows && state.rows.byId ? state.rows.byId[sessionId] : undefined;
2441
2562
  var summary = rowSummaryOf(row);
2442
- if (summary === null || !summary.hasContent) return null;
2563
+ if (summary === null) return null;
2443
2564
  var mineCount = summary.mine;
2444
2565
  var possibleCount = summary.possible;
2445
2566
  var shorthands = summary.shorthands;
@@ -2454,6 +2575,9 @@ window.__ModuleLoader__.load({
2454
2575
  head += " · " + shorthands.map(function (item) {
2455
2576
  return t("count." + item.key) + " " + String(item.value);
2456
2577
  }).join(" ");
2578
+ } else {
2579
+ // 干净也要说出来 —— 与悬停说明、⋯ 菜单同一个口径(空着看不出是"干净"还是"没读到")。
2580
+ head += " · " + t("panel.clean");
2457
2581
  }
2458
2582
  lines.push(head);
2459
2583
  } else if (row.repo === false && row.error === null) {
@@ -2462,7 +2586,9 @@ window.__ModuleLoader__.load({
2462
2586
  if (row.error) lines.push(t("rows.error", { code: String(row.error) }));
2463
2587
  var own = [];
2464
2588
  if (mineCount > 0) own.push(t("session.pill", { n: String(mineCount) }));
2465
- if (possibleCount > 0) own.push(t("rows.possible", { n: String(possibleCount) }));
2589
+ if (possibleCount > 0) {
2590
+ own.push(t("rows.possible", { n: String(possibleCount), now: String(summary.possibleNow) }));
2591
+ }
2466
2592
  // 和第二行对齐:没有改动记录时也明说一句 —— 否则卡片上会缺一行,看着像没加载出来。
2467
2593
  lines.push(own.length === 0 ? t("rows.ownNone") : own.join(" · "));
2468
2594
  if (lines.length === 0) return null;
@@ -2475,35 +2601,6 @@ window.__ModuleLoader__.load({
2475
2601
  }));
2476
2602
  }
2477
2603
 
2478
- /** 行内 chip 的悬停说明:分支、上游、各类计数、以及这个会话自己的改动。 */
2479
- function rowChipTitle(row, t) {
2480
- var lines = [];
2481
- if (row.repo === true) {
2482
- var head = branchLabel(row, t);
2483
- if (row.dirty === true) head += "*";
2484
- if (row.ahead > 0) head += " ↑" + String(row.ahead);
2485
- if (row.behind > 0) head += " ↓" + String(row.behind);
2486
- lines.push(head);
2487
- var shorthands = countShorthands(row.counts);
2488
- if (shorthands.length > 0) {
2489
- lines.push(shorthands.map(function (item) {
2490
- return t("count." + item.key) + " " + String(item.value);
2491
- }).join(" · "));
2492
- }
2493
- if (row.cwd) lines.push(row.cwd);
2494
- } else if (row.repo === false && row.error === null) {
2495
- // 不在仓库里也要说清(不能只丢一个路径 —— 用户看不出"为什么这一行没有分支")。
2496
- lines.push(t("rows.noRepo"));
2497
- if (row.cwd) lines.push(row.cwd);
2498
- }
2499
- if (row.error) lines.push(t("rows.error", { code: String(row.error) }));
2500
- var mineCount = typeof row.mine === "number" ? row.mine : 0;
2501
- var possibleCount = typeof row.possible === "number" ? row.possible : 0;
2502
- if (mineCount > 0) lines.push(t("session.pill", { n: String(mineCount) }));
2503
- if (possibleCount > 0) lines.push(t("rows.possible", { n: String(possibleCount) }));
2504
- return lines.join("\n");
2505
- }
2506
-
2507
2604
  function HudDockPill(props) {
2508
2605
  var t = props.t || function (key) { return key; };
2509
2606
  var sessionId = props.sessionId;
@@ -2525,6 +2622,24 @@ window.__ModuleLoader__.load({
2525
2622
 
2526
2623
  var state = useStore(store);
2527
2624
 
2625
+ /**
2626
+ * 工具胶囊的"实时秒数"滴答。
2627
+ *
2628
+ * **必须在组件体里、无条件、固定顺序地调用**:早先放在普通函数 `toolPillNode` 里、还带
2629
+ * `if (now === undefined)` 的条件,就是条件钩子 —— React 会抛错,导致整行 dock 渲染失败,
2630
+ * 两枚胶囊一起消失(用户实测"胶囊都不显示了")。回调里再判断要不要开定时器就没这问题。
2631
+ */
2632
+ var tick = React.useState(Date.now());
2633
+ var tickSet = tick[1];
2634
+ var toolsTickKey = state.tools && state.tools.active !== null && state.tools.active !== undefined
2635
+ ? String(state.tools.active.callId)
2636
+ : null;
2637
+ React.useEffect(function () {
2638
+ if (toolsTickKey === null) return undefined;
2639
+ var timer = window.setInterval(function () { tickSet(Date.now()); }, 500);
2640
+ return function () { window.clearInterval(timer); };
2641
+ }, [toolsTickKey]);
2642
+
2528
2643
  React.useEffect(function () {
2529
2644
  if (!mainView) return;
2530
2645
  store.bindSession(sessionId, cwd);
@@ -2539,18 +2654,30 @@ window.__ModuleLoader__.load({
2539
2654
  : null;
2540
2655
 
2541
2656
  var children = [];
2657
+ // 工作区名放在最前、且与分支用 `·` 隔开:写成 `git:(dsh-hud/main*)` 会被读成"分支叫 dsh-hud/main"。
2658
+ // 三种"非仓库"状态也带上它 —— 工作区跟 git 没关系,没仓库时它反而更有用。
2659
+ var workspace = workspaceNameOf(cwd);
2660
+ if (workspace !== null) {
2661
+ children.push(h("span", { key: "workspace", className: "dshh-workspace" }, workspace));
2662
+ children.push(h("span", { key: "workspaceSep", className: "dshh-workspaceSep" }, " · "));
2663
+ }
2542
2664
  if (state.phase === "error") {
2543
- children.push("⎇ " + t("pill.error"));
2665
+ children.push(h("span", { key: "gitWraperr", className: "dshh-gitWrap" }, "git:(" + t("pill.error") + ")"));
2544
2666
  } else if (state.phase === "loading" || state.phase === "idle") {
2545
- children.push("⎇ " + t("pill.loading"));
2667
+ children.push(h("span", { key: "gitWrapload", className: "dshh-gitWrap" }, "git:(" + t("pill.loading") + ")"));
2546
2668
  } else if (!state.status || !state.status.repo) {
2547
- children.push("⎇ " + t("pill.notRepo"));
2669
+ children.push(h("span", { key: "gitWrapnorepo", className: "dshh-gitWrap" }, "git:(" + t("pill.notRepo") + ")"));
2548
2670
  } else {
2549
2671
  var status = state.status;
2550
- var head = "⎇ " + shortBranch(branchLabel(status, t)) + (status.dirty ? "*" : "");
2551
- if (status.ahead > 0) head += " ↑" + String(status.ahead);
2552
- if (status.behind > 0) head += " ↓" + String(status.behind);
2553
- children.push(head);
2672
+ // `git:(branch*)` —— codex-hud 的 gitSegment 就是这么拼的,配色也照它:
2673
+ // `git:(` 与 `)` 品红(colors.git)、分支名与 ↑↓ 青(colors.gitBranch),
2674
+ // 而 `*` 在括号**里面**、与分支名同色。
2675
+ children.push(h("span", { key: "gitOpen", className: "dshh-gitWrap" }, "git:("));
2676
+ children.push(h("span", { key: "branch", className: "dshh-branch" },
2677
+ shortBranch(branchLabel(status, t)) + (status.dirty ? "*" : "")));
2678
+ children.push(h("span", { key: "gitClose", className: "dshh-gitWrap" }, ")"));
2679
+ if (status.ahead > 0) children.push(h("span", { key: "ahead", className: "dshh-ahead" }, " ↑" + String(status.ahead)));
2680
+ if (status.behind > 0) children.push(h("span", { key: "behind", className: "dshh-ahead" }, " ↓" + String(status.behind)));
2554
2681
 
2555
2682
  // 简写符:只列非零项,按固定顺序,各自上色(M 黄 / A 绿 / D 红 / ? 灰 …)。
2556
2683
  // 与侧栏行内 chip 共用 `countShorthands` —— 两处各写一套算法一定会漂。
@@ -2597,7 +2724,132 @@ window.__ModuleLoader__.load({
2597
2724
  onClick: function () {
2598
2725
  store.togglePanel();
2599
2726
  },
2600
- }, children));
2727
+ }, children),
2728
+ // 工具活动那一枚并排在后面 —— dock 是竖列,独立注册会另起一整行(用户实测"换行了")。
2729
+ toolPillNode(state, { t: t, now: props.now === undefined ? tick[0] : props.now }));
2730
+ }
2731
+
2732
+ /**
2733
+ * 本会话工具活动的胶囊(座位 `conversation.input.dock`,与 Git 胶囊各自一行)。
2734
+ *
2735
+ * 照 codex-hud 那条 `Tools:` 的口径:**正在跑**就报工具名与实时秒数(秒数本地算,不靠轮询);
2736
+ * 空闲就报最近一次是哪个工具、本会话一共几次(失败打叉)。完全没跑过工具时整枚不画 ——
2737
+ * 空白注册会白占一行(dock 是竖列,一条注册就是一整行)。
2738
+ */
2739
+ /**
2740
+ * 工具活动那一枚(**与 Git 胶囊同一行**,所以它只是那一行里的第二个节点,不是独立注册)。
2741
+ *
2742
+ * 照 codex-hud 那条 `Tools:` 的口径:**正在跑**就报工具名与实时秒数(秒数本地算,不靠轮询);
2743
+ * 空闲就报最近一次是哪个工具、本会话一共几次(失败打叉)。完全没跑过工具时返回 null ——
2744
+ * 不画它,但那一行仍然在(Git 胶囊自己不消失,界面不会跳)。
2745
+ */
2746
+ function toolPillNode(state, options) {
2747
+ var t = options.t;
2748
+ var tools = state.tools;
2749
+ if (!tools || (tools.active === null && tools.total === 0)) return null;
2750
+ var now = options.now;
2751
+
2752
+ var children = [];
2753
+ if (tools.active !== null) {
2754
+ // 配色对齐 codex-hud 的 activity-lines:`◐` 黄 + 工具名青 + 目标/耗时用默认正文色。
2755
+ var elapsed = toolElapsedLabel(tools.active, now);
2756
+ children.push(h("span", { key: "glyph", className: "dshh-toolsGlyph dshh-toolsGlyphRun" }, "\u25d0"));
2757
+ children.push(h("span", { key: "name", className: "dshh-toolsName" },
2758
+ " " + String(tools.active.name === undefined ? "?" : tools.active.name)));
2759
+ // 带上参数摘要(codex-hud 就是 `◐ exec_command: pnpm test`)—— 技能调用因此能看出是哪个技能。
2760
+ if (typeof tools.active.summary === "string" && tools.active.summary.length > 0) {
2761
+ children.push(": " + tools.active.summary);
2762
+ }
2763
+ if (elapsed !== null) children.push(" " + elapsed);
2764
+ } else {
2765
+ // 空闲时把**每个工具各自几次**铺出来(codex-hud 那条 `Tools:` 就是这么给的):
2766
+ // `✓ bash ×16 │ ✓ apply_patch ×6 │ ✗ write_stdin ×3`。名字与数字贴在一起,
2767
+ // 就不会再有"×54 是总数还是这个工具的次数"那种误读。
2768
+ //
2769
+ // 最多铺 3 个(按次数排序,宿主已经排好),其余收成 `+k` —— 悬停里有完整清单与最近几条。
2770
+ // `skill` 工具本身对用户没信息量(`skill ×2` 说明不了是哪个技能)—— codex-hud 也是直接列技能名。
2771
+ // 所以把它从计数里剔掉,改由下面那段技能清单承担;总次数不受影响。
2772
+ var toolCounts = tools.counts.filter(function (item) { return item.name !== "skill"; });
2773
+ var shown = toolCounts.slice(0, 3);
2774
+ for (var i = 0; i < shown.length; i += 1) {
2775
+ var item = shown[i];
2776
+ var bad = item.ok === false;
2777
+ if (i > 0) children.push(" \u2502 ");
2778
+ children.push(h("span", {
2779
+ key: "glyph" + String(i),
2780
+ className: "dshh-toolsGlyph" + (bad ? " dshh-toolsGlyphBad" : " dshh-toolsGlyphOk"),
2781
+ }, bad ? "\u2717" : "\u2713"));
2782
+ children.push(" " + String(item.name) + " \u00d7" + String(item.count));
2783
+ }
2784
+ // 技能清单:名字直接铺出来("会话用了哪些 skill"要能一眼看到,不只是次数)。
2785
+ // 标签用字面的 `skills:`(不翻译)—— 与 `git:(` 同一套:它是约定写法,不是描述。
2786
+ var skills = tools.skills || [];
2787
+ if (skills.length > 0) {
2788
+ if (children.length > 0) children.push(" \u2502 ");
2789
+ children.push(t("tools.skills") + " ");
2790
+ for (var k = 0; k < skills.length && k < SKILLS_SHOWN; k += 1) {
2791
+ if (k > 0) children.push(" \u00b7 ");
2792
+ children.push(h("span", {
2793
+ key: "skill" + String(k),
2794
+ className: skills[k].ok === false ? "dshh-toolsGlyphBad" : "dshh-skillName",
2795
+ }, String(skills[k].name) + " \u00d7" + String(skills[k].count)));
2796
+ }
2797
+ if (skills.length > SKILLS_SHOWN) children.push(" +" + String(skills.length - SKILLS_SHOWN));
2798
+ if (tools.skillsDropped > 0) children.push(" +" + String(tools.skillsDropped));
2799
+ }
2800
+ if (toolCounts.length > shown.length) children.push(" +" + String(toolCounts.length - shown.length));
2801
+ }
2802
+
2803
+ // 渲染现场:画没画、画的是哪一档(与 row-chip/row-token 同一套排查手段)。
2804
+ probe.log("tools-pill", {
2805
+ active: tools.active === null ? null : String(tools.active.name),
2806
+ total: tools.total,
2807
+ });
2808
+ return h("span", {
2809
+ className: "dshh-pill dshh-toolsPill",
2810
+ "data-dsh-hud": "tools-pill",
2811
+ key: "tools",
2812
+ title: toolsTitle(tools, t),
2813
+ }, children);
2814
+ }
2815
+
2816
+ /** 已经跑了多久:`1.2s` / `12s`(拿不到开始时刻就返回 null,不编一个数)。 */
2817
+ function toolElapsedLabel(active, now) {
2818
+ if (active === null || typeof active !== "object") return null;
2819
+ if (typeof active.startedAt !== "number") return null;
2820
+ var seconds = Math.max(0, (now - active.startedAt) / 1000);
2821
+ return seconds < 10 ? seconds.toFixed(1) + "s" : String(Math.round(seconds)) + "s";
2822
+ }
2823
+
2824
+ /** 工具胶囊的悬停说明:在跑什么、一共几次、各工具几次、最近几条的结果与耗时。 */
2825
+ function toolsTitle(tools, t) {
2826
+ var lines = [];
2827
+ if (tools.active !== null) {
2828
+ lines.push(t("tools.now", { name: String(tools.active.name) }));
2829
+ if (typeof tools.active.summary === "string" && tools.active.summary.length > 0) lines.push(tools.active.summary);
2830
+ }
2831
+ lines.push(t("tools.total", { n: String(tools.total) }));
2832
+ if (tools.counts.length > 0) {
2833
+ lines.push(t("tools.byTool") + " " + tools.counts.map(function (item) {
2834
+ return String(item.name) + " \u00d7" + String(item.count);
2835
+ }).join(" \u00b7 "));
2836
+ }
2837
+ if (tools.skills && tools.skills.length > 0) {
2838
+ lines.push(t("tools.skills") + " " + tools.skills.map(function (item) {
2839
+ return String(item.name) + " \u00d7" + String(item.count) + (item.ok === false ? " (\u2717)" : "");
2840
+ }).join(" \u00b7 "));
2841
+ }
2842
+ var recent = tools.recent.slice(0, 3);
2843
+ if (recent.length > 0) {
2844
+ lines.push(t("tools.recent"));
2845
+ lines.push(recent.map(function (item) {
2846
+ var mark = item.ok === false ? "\u2717" : "\u2713";
2847
+ var ms = typeof item.ms === "number" ? " " + (item.ms / 1000).toFixed(1) + "s" : "";
2848
+ var summary = typeof item.summary === "string" && item.summary.length > 0 ? " \u00b7 " + item.summary : "";
2849
+ return mark + " " + String(item.name) + ms + summary;
2850
+ }).join("\n"));
2851
+ }
2852
+ return lines.join("\n");
2601
2853
  }
2602
2854
 
2603
2855
  /* ------------------------------------------------------- 悬浮面板 */
@@ -2623,13 +2875,24 @@ window.__ModuleLoader__.load({
2623
2875
  * @param layout - `panelLayout()` 的结果。
2624
2876
  * @param diffWidth - 差异栏的目标宽度。
2625
2877
  */
2626
- function panelWidthFor(layout, diffWidth) {
2627
- var middle = layout.commit === true ? COMMIT_LIST_WIDTH : 0;
2628
- if (layout.diff !== true && layout.commit !== true) return PANEL_WIDTH;
2629
- var right = layout.diff === true ? RESIZE_GRIP_WIDTH + (typeof diffWidth === "number" ? diffWidth : DIFF_DEFAULT_WIDTH) : 0;
2878
+ function panelWidthFor(layout, diffWidth, commitWidth) {
2879
+ var hasCommit = layout.commit === true;
2880
+ var hasDiff = layout.diff === true;
2881
+ if (!hasCommit && !hasDiff) return PANEL_WIDTH;
2882
+ // **逐栏钳到自己的下限**:只对总宽加一个 floor 是不够的 —— 存档里写着 40px 时,
2883
+ // 总宽仍然会按 40 算(floor 只保证"不小于最小总和",但差额会被算到别的栏头上)。
2884
+ var middle = hasCommit
2885
+ ? Math.max(COMMIT_MIN_WIDTH, typeof commitWidth === "number" ? commitWidth : COMMIT_LIST_WIDTH)
2886
+ : 0;
2887
+ var right = hasDiff
2888
+ ? Math.max(DIFF_MIN_WIDTH, typeof diffWidth === "number" ? diffWidth : DIFF_DEFAULT_WIDTH)
2889
+ : 0;
2890
+ // 两条抓握:面板右边缘那条(调差异栏;只有提交栏时调提交栏)—— 它是绝对定位、**不占宽**,
2891
+ // 但宽度里要预留 6px 让位,否则它压在内容上;栏间那条是真实的一格,占宽。
2892
+ var outerGrip = RESIZE_GRIP_WIDTH;
2893
+ var innerGrip = hasCommit && hasDiff ? RESIZE_GRIP_WIDTH : 0;
2630
2894
  var available = window.innerWidth - 2 * VIEWPORT_MARGIN;
2631
- var min = PANEL_WIDTH + middle + (layout.diff === true ? RESIZE_GRIP_WIDTH + DIFF_MIN_WIDTH : 0);
2632
- return Math.max(Math.min(min, available), Math.min(PANEL_WIDTH + middle + right, available));
2895
+ return Math.min(PANEL_WIDTH + middle + innerGrip + right + outerGrip, available);
2633
2896
  }
2634
2897
 
2635
2898
  /** 把位置夹回视口内,避免拖到屏幕外再也点不到。 */
@@ -2689,7 +2952,7 @@ window.__ModuleLoader__.load({
2689
2952
  // 从 store 现读,别用可能过期的渲染闭包(模式切换会改宽度)。
2690
2953
  function currentPanelWidth() {
2691
2954
  var snapshot = store.getSnapshot();
2692
- return panelWidthFor(panelLayout(snapshot), snapshot.diffWidth);
2955
+ return panelWidthFor(panelLayout(snapshot), snapshot.diffWidth, snapshot.commitWidth);
2693
2956
  }
2694
2957
 
2695
2958
  // Esc 收起面板。用捕获阶段监听:焦点可能在任何输入框里,捕获阶段先拿到。
@@ -2822,7 +3085,8 @@ window.__ModuleLoader__.load({
2822
3085
  if (!grip) return undefined;
2823
3086
  var onMove = function (event) {
2824
3087
  if (event.pointerId !== grip.pointerId) return;
2825
- store.setDiffWidth(grip.startWidth - (event.clientX - grip.startX));
3088
+ if (grip.kind === "commit") store.setCommitWidth(grip.startWidth - (event.clientX - grip.startX));
3089
+ else store.setDiffWidth(grip.startWidth - (event.clientX - grip.startX));
2826
3090
  };
2827
3091
  var finish = function (event) {
2828
3092
  if (event.pointerId !== grip.pointerId) return;
@@ -2841,15 +3105,21 @@ window.__ModuleLoader__.load({
2841
3105
 
2842
3106
  if (!state.panelOpen) return null;
2843
3107
 
2844
- function onGripPointerDown(event) {
3108
+ /**
3109
+ * 抓握按下。
3110
+ *
3111
+ * `kind` 决定它调哪一栏:`diff` = 面板右边缘那条(调差异栏);`commit` = 提交栏右边那条
3112
+ * (调提交明细栏)。只有提交栏、没有差异栏时,右边缘那条也调提交栏 —— 它是右边界上唯一
3113
+ * 还能变宽的东西,不该按下去没反应。
3114
+ */
3115
+ function onGripPointerDown(kind, event) {
2845
3116
  if (event.button !== 0) return;
2846
3117
  var snapshot = store.getSnapshot();
2847
3118
  event.preventDefault();
2848
- setGrip({
2849
- pointerId: event.pointerId,
2850
- startX: event.clientX,
2851
- startWidth: typeof snapshot.diffWidth === "number" ? snapshot.diffWidth : DIFF_DEFAULT_WIDTH,
2852
- });
3119
+ var start = kind === "commit"
3120
+ ? (typeof snapshot.commitWidth === "number" ? snapshot.commitWidth : COMMIT_LIST_WIDTH)
3121
+ : (typeof snapshot.diffWidth === "number" ? snapshot.diffWidth : DIFF_DEFAULT_WIDTH);
3122
+ setGrip({ kind: kind, pointerId: event.pointerId, startX: event.clientX, startWidth: start });
2853
3123
  }
2854
3124
 
2855
3125
  function onHeaderPointerDown(event) {
@@ -2886,7 +3156,7 @@ window.__ModuleLoader__.load({
2886
3156
  position: "fixed",
2887
3157
  zIndex: 30,
2888
3158
  boxSizing: "border-box",
2889
- width: panelWidthFor(layout, state.diffWidth),
3159
+ width: panelWidthFor(layout, state.diffWidth, state.commitWidth),
2890
3160
  maxWidth: "calc(100vw - 16px)",
2891
3161
  display: "flex",
2892
3162
  flexDirection: "column",
@@ -2965,8 +3235,6 @@ window.__ModuleLoader__.load({
2965
3235
  onToggleLatest: function () { store.toggleOnlyLatest(); },
2966
3236
  review: reviewView(state),
2967
3237
  onClearReviewed: function () { store.clearReviewed(); },
2968
- sidebarMode: state.sidebarMode,
2969
- onCycleSidebarMode: function () { store.cycleSidebarMode(); },
2970
3238
  }),
2971
3239
  // 顺序:改动概览 → **改动清单** → 最近提交。清单是"现在要处理的",
2972
3240
  // 提交是背景信息;清单放前面,一屏不够时先看到的是眼前的事。
@@ -2994,6 +3262,24 @@ window.__ModuleLoader__.load({
2994
3262
  // 再点一次打开着的那条 = 收起明细(打开提交不改左栏,只高亮它)。
2995
3263
  onOpen: function (commit) { store.toggleCommit(commit); },
2996
3264
  }),
3265
+ h(TagList, {
3266
+ tags: state.tags,
3267
+ truncated: state.tagsTruncated,
3268
+ t: t,
3269
+ headSha: state.status && state.status.oid ? state.status.oid : null,
3270
+ activeSha: commitMode ? state.commit.sha : null,
3271
+ // 点一个 tag = 看它指向的那次提交(tag 自己不是提交,所以把提交信息填进去)。
3272
+ onOpen: function (tag) {
3273
+ store.toggleCommit({
3274
+ sha: tag.sha,
3275
+ subject: tag.subject || "",
3276
+ author: "",
3277
+ date: tag.date || "",
3278
+ committedAt: typeof tag.committedAt === "number" ? tag.committedAt : null,
3279
+ refs: [tag.name],
3280
+ });
3281
+ },
3282
+ }),
2997
3283
  ];
2998
3284
  listBody = h.apply(null, ["div", { className: "dshh-body" }].concat(repoChildren));
2999
3285
  } else {
@@ -3013,7 +3299,15 @@ window.__ModuleLoader__.load({
3013
3299
  onOpenFile: function (file) { store.openCommitDiff(file); },
3014
3300
  activeFile: commitOpenFile,
3015
3301
  cursor: state.cursor,
3016
- width: COMMIT_LIST_WIDTH,
3302
+ width: typeof state.commitWidth === "number" ? state.commitWidth : COMMIT_LIST_WIDTH,
3303
+ })
3304
+ : null,
3305
+ commitMode && diffMode
3306
+ ? h("div", {
3307
+ className: "dshh-grip dshh-gripBetween",
3308
+ "data-dsh-hud": "grip-commit",
3309
+ title: t("diff.resizeCommit"),
3310
+ onPointerDown: function (event) { onGripPointerDown("commit", event); },
3017
3311
  })
3018
3312
  : null,
3019
3313
  diffMode ? h(DiffView, { t: t, diff: state.diff, store: store }) : null)
@@ -3064,9 +3358,17 @@ window.__ModuleLoader__.load({
3064
3358
  className: "dshh-grip",
3065
3359
  "data-dsh-hud": "grip",
3066
3360
  title: t("diff.resize"),
3067
- onPointerDown: onGripPointerDown,
3361
+ onPointerDown: function (event) { onGripPointerDown("diff", event); },
3068
3362
  })
3069
- : null,
3363
+ : commitMode
3364
+ // 只有提交栏:右边缘这条改成调它(否则按下去没反应)。
3365
+ ? h("div", {
3366
+ className: "dshh-grip",
3367
+ "data-dsh-hud": "grip-commit",
3368
+ title: t("diff.resizeCommit"),
3369
+ onPointerDown: function (event) { onGripPointerDown("commit", event); },
3370
+ })
3371
+ : null,
3070
3372
  h("div", { className: "dshh-footer" },
3071
3373
  h("span", null, t("panel.readonly")),
3072
3374
  h("span", null, state.updatedAt ? t("panel.updated") + timeLabel(state.updatedAt) : "")));
@@ -3082,7 +3384,7 @@ window.__ModuleLoader__.load({
3082
3384
  // dock 座位给的是整栏宽的一行,而输入卡片有自己的最大宽度并居中
3083
3385
  // (--dsh-composer-card-max-width)。外层容器复刻框架自带 dock 占位元素
3084
3386
  // (队列公告)的几何,胶囊才会真正贴在输入卡片上方左端,而不是飘到栏的最左。
3085
- ".dshh-dock{box-sizing:border-box;display:flex;justify-content:flex-start;",
3387
+ ".dshh-dock{box-sizing:border-box;display:flex;align-items:center;gap:6px;justify-content:flex-start;",
3086
3388
  "width:calc(100% - var(--dsh-composer-side-clearance,16px) - var(--dsh-composer-side-clearance,16px)",
3087
3389
  " - var(--dsh-composer-dock-inset,8px) - var(--dsh-composer-dock-inset,8px));",
3088
3390
  "max-width:calc(var(--dsh-composer-card-max-width,952px) - var(--dsh-composer-dock-inset,8px) - var(--dsh-composer-dock-inset,8px));",
@@ -3135,34 +3437,43 @@ window.__ModuleLoader__.load({
3135
3437
  "color:var(--dsw-alias-brand-primary,#4b8bf5);}",
3136
3438
  ".dshh-markerPossible{color:var(--dsw-alias-state-warn-primary,#d9a63c);opacity:.75;}",
3137
3439
  // 行尾的审阅标记:`○` 未审 / `✓` 这一版已审 / `✓`(黄) 改动已变。
3138
- // 侧栏会话行里那枚 chip:一行只有 32px 高、悬停才露出来,所以它必须很小。
3139
- // 侧栏会话行里那枚 chip:一行只有 32px 高、悬停动作条很窄,所以它必须小且不许撑宽。
3140
- ".dshh-rowchip{display:inline-flex;align-items:center;gap:2px;flex:none;max-width:72px;",
3141
- "font-family:ui-monospace,Consolas,monospace;font-size:10.5px;line-height:1;",
3142
- "white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-secondary,#8a8f98);}",
3143
- ".dshh-rowchipMine{color:var(--dsw-alias-brand-primary,#4b8bf5);}",
3144
- // 行首常亮那枚:产品给的格子只有 16px,所以最多两个字符、不许换行。
3145
- ".dshh-rowtoken{font-family:ui-monospace,Consolas,monospace;font-size:10px;line-height:1;",
3146
- "white-space:nowrap;flex:none;letter-spacing:-0.02em;}",
3147
3440
  // `⋯` 菜单里那条只读摘要:像一条说明,不像一个可点的菜单项。
3148
3441
  ".dshh-menureadout{display:flex;flex-direction:column;gap:2px;padding:4px 10px 6px;",
3149
3442
  "font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary,#8a8f98));",
3150
3443
  "cursor:default;user-select:text;max-width:260px;}",
3151
3444
  ".dshh-menureadoutLine{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
3152
- ".dshh-rowtoken-mine{color:var(--dsw-alias-brand-primary,#4b8bf5);}",
3153
- ".dshh-rowtoken-possible{color:var(--dsw-alias-state-warn-primary,#d9a63c);}",
3154
- ".dshh-rowtoken-dirty{color:var(--dsw-alias-state-warn-primary,#d9a63c);opacity:.75;}",
3155
3445
  // 悬停卡片里那块详情:跟着卡片自己的字号,只做行距与弱化。
3156
3446
  ".dshh-rowhover{display:flex;flex-direction:column;gap:2px;font-size:11px;",
3157
3447
  "color:var(--dsw-alias-label-secondary,#8a8f98);min-width:0;}",
3158
3448
  ".dshh-rowhoverLine{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
3159
- ".dshh-rowchipPossible{color:var(--dsw-alias-state-warn-primary,#d9a63c);}",
3160
3449
  ".dshh-review{flex:none;margin-left:auto;padding:0 2px;font-size:10.5px;cursor:pointer;",
3161
3450
  "color:var(--dsw-alias-label-dimmed,rgba(138,143,152,0.65));}",
3162
3451
  ".dshh-review:hover{color:var(--dsw-alias-label-primary,#e5e7eb);}",
3163
3452
  ".dshh-reviewOn{color:var(--dsw-alias-state-success-primary,#3fb950);}",
3164
3453
  ".dshh-reviewStale{color:var(--dsw-alias-state-warn-primary,#d9a63c);}",
3454
+ ".dshh-ctag{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
3455
+ "font-family:ui-monospace,Consolas,monospace;font-size:11px;color:var(--dsw-alias-state-warn-primary,#d9a63c);}",
3165
3456
  ".dshh-markerOther{color:var(--dsw-alias-label-dimmed,rgba(138,143,152,0.6));}",
3457
+ // 工具活动胶囊:与 Git 胶囊同形,但它是只读读数,指针不该是“可点”。
3458
+ // 与 codex-hud 对齐的几支色(它的默认配色表:git=magenta、gitBranch=cyan、project=yellow):
3459
+ // 产品没有青/品红的语义变量(只有 button-info-*,那是按钮用的),所以这两支写死值,
3460
+ // 但都走变量、可被覆盖。
3461
+ ".dshh-workspace{color:var(--dsw-alias-state-warn-primary,#d9a63c);flex:none;}",
3462
+ ".dshh-gitWrap{color:var(--dsh-hud-magenta,#d946ef);flex:none;}",
3463
+ ".dshh-branch{color:var(--dsh-hud-cyan,#22d3ee);flex:none;}",
3464
+ ".dshh-ahead{color:var(--dsh-hud-cyan,#22d3ee);flex:none;}",
3465
+ ".dshh-workspaceSep{color:var(--dsw-alias-label-tertiary,rgba(138,143,152,0.6));flex:none;}",
3466
+ ".dshh-toolsPill{cursor:default;flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
3467
+ // 配色对齐 codex-hud 的 activity-lines:只有状态符上色,名字/次数/分隔符都用正文色。
3468
+ ".dshh-toolsGlyph{flex:none;}",
3469
+ ".dshh-toolsGlyphOk{color:var(--dsw-alias-state-success-primary,#3fb950);}",
3470
+ ".dshh-toolsGlyphRun{color:var(--dsw-alias-state-warn-primary,#d9a63c);}",
3471
+ ".dshh-toolsGlyphBad{color:var(--dsw-alias-state-error-primary,#f06a6a);}",
3472
+ // 唯一一处写死的颜色:codex-hud 把"正在跑的工具名"画成青色,产品没有对应的语义变量
3473
+ // (只有 button-info-*,那是按钮用的)。留 `--dsh-hud-tool-name` 作为覆盖钩子。
3474
+ ".dshh-toolsName{color:var(--dsh-hud-cyan,#22d3ee);}",
3475
+ // 技能名与工具名同色系但不同支:工具那支是青,技能走品牌色(一眼能分开)。
3476
+ ".dshh-skillName{color:var(--dsw-alias-brand-primary,#4b8bf5);}",
3166
3477
  ".dshh-section{display:flex;flex-direction:column;gap:4px;}",
3167
3478
  ".dshh-list{display:flex;flex-direction:column;gap:1px;max-height:30vh;overflow:auto;",
3168
3479
  "border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,0.16));padding-top:4px;}",
@@ -3228,6 +3539,9 @@ window.__ModuleLoader__.load({
3228
3539
  "box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary,#4b8bf5);}",
3229
3540
  ".dshh-hunkMark{text-align:right;}",
3230
3541
  ".dshh-grip{position:absolute;top:0;right:0;bottom:0;width:6px;cursor:col-resize;z-index:5;}",
3542
+ // 栏间那条抓握**必须占宽**:它是提交栏与差异栏之间的真实一格。绝对定位会让它贴到面板右边缘,
3543
+ // 与外面那条叠在一起(面板右边缘那条不占宽,靠宽度公式里预留的 6px 让位)。
3544
+ ".dshh-gripBetween{position:static;flex:none;width:6px;}",
3231
3545
  ".dshh-grip:hover{background:var(--dsw-alias-border-l2,rgba(127,127,127,0.35));}",
3232
3546
  ".dshh-diffview{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;}",
3233
3547
  ".dshh-toolbar{display:flex;align-items:center;gap:4px;padding:5px 8px;flex:none;overflow:hidden;",
@@ -3277,7 +3591,8 @@ window.__ModuleLoader__.load({
3277
3591
  var tag = typeof document.querySelector === "function" ? document.querySelector(selector) : null;
3278
3592
  if (!tag) {
3279
3593
  tag = document.createElement("style");
3280
- tag.setAttribute("data-plugin", "dsh-hud");
3594
+ // 值必须等于客户端模块 id(= 包名):客户端的 `removeOwnedStyles(id)` 靠它回收样式。
3595
+ tag.setAttribute("data-plugin", "@konnga/dsh-hud");
3281
3596
  tag.setAttribute("data-plugin-css", CSS_TAG_ID);
3282
3597
  tag.setAttribute("data-dsh-hud", "styles");
3283
3598
  document.head.appendChild(tag);
@@ -3365,6 +3680,17 @@ window.__ModuleLoader__.load({
3365
3680
  };
3366
3681
  }, "dsh-hud: session rows poll");
3367
3682
 
3683
+ // 工具活动单独一条节拍(3s;宿主是内存读,密一点才看得到"正在跑")。
3684
+ ctx.effect(function () {
3685
+ var id = window.setInterval(function () {
3686
+ if (document.visibilityState === "hidden") return;
3687
+ loadTools();
3688
+ }, TOOLS_POLL_MS);
3689
+ return function () {
3690
+ window.clearInterval(id);
3691
+ };
3692
+ }, "dsh-hud: tool activity poll");
3693
+
3368
3694
  // `⋯` 菜单里的一条只读摘要:行尾拿不到常亮座位,这里是最实在的"完整信息"落点。
3369
3695
  ctx.slots.inject("sidebar.workspaces.session.menu.item", function () {
3370
3696
  return ctx.slots.register({
@@ -3378,31 +3704,6 @@ window.__ModuleLoader__.load({
3378
3704
  }, SessionMenuSummary);
3379
3705
  });
3380
3706
 
3381
- // 行首那格(常亮档):产品只给 16px,所以是一枚双字符标记。
3382
- ctx.slots.inject("sidebar.session.row.leading", function () {
3383
- return ctx.slots.register({
3384
- name: "sidebar.session.row.leading",
3385
- id: "dsh-hud-session-leading",
3386
- order: 30,
3387
- locale: NS,
3388
- inject: function () {
3389
- return { store: store };
3390
- },
3391
- }, SessionLeadingToken);
3392
- });
3393
-
3394
- // 侧栏每条会话行悬停时的那组动作:产品自己的动作也只是普通条目,这里挂一枚只读状态。
3395
- ctx.slots.inject("sidebar.workspaces.session.row.action", function () {
3396
- return ctx.slots.register({
3397
- name: "sidebar.workspaces.session.row.action",
3398
- id: "dsh-hud-session-status",
3399
- order: 30,
3400
- locale: NS,
3401
- inject: function () {
3402
- return { store: store };
3403
- },
3404
- }, SessionRowChip);
3405
- });
3406
3707
 
3407
3708
  // 悬停卡片里那块详情(宽敞的地方;动作条那枚放不下的东西都在这儿)。
3408
3709
  ctx.slots.inject("sidebar.session.row.hover", function () {
@@ -3417,6 +3718,7 @@ window.__ModuleLoader__.load({
3417
3718
  }, SessionRowHoverChip);
3418
3719
  });
3419
3720
 
3721
+
3420
3722
  ctx.slots.inject("conversation.input.dock", function () {
3421
3723
  return ctx.slots.register({
3422
3724
  name: "conversation.input.dock",