@konnga/dsh-hud 0.7.1 → 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/README.md +77 -59
- package/docs/images/capsules.webp +0 -0
- package/docs/images/panel.webp +0 -0
- package/lib/client.js +536 -249
- package/lib/index.js +272 -4
- package/package.json +14 -4
package/lib/client.js
CHANGED
|
@@ -44,7 +44,16 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
* 那些 chip 是"扫一眼"的信息(哪个会话脏了),不值得跟面板一样频繁;宿主侧还按目录
|
|
45
45
|
* 缓存 + 去重,多会话共用一个目录只跑一次 git。
|
|
46
46
|
*/
|
|
47
|
-
|
|
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
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
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
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
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": "
|
|
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
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
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
|
-
"
|
|
229
|
-
"
|
|
230
|
-
"
|
|
231
|
-
"
|
|
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}
|
|
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
|
-
|
|
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(--
|
|
307
|
-
untracked: "var(--dsw-alias-label-
|
|
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
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
|
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
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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,84 +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) return null;
|
|
2373
|
-
/*
|
|
2374
|
-
* 常亮档**永远有标记**:干净、非仓库、读取失败各有各的写法。
|
|
2375
|
-
* 之前"干净就整枚不画",结果是用户以为功能没生效 —— 常亮的意义就是"扫一眼总能看到"。
|
|
2376
|
-
*/
|
|
2377
|
-
var token = null;
|
|
2378
|
-
// 优先级 = 价值顺序:本会话确定改过 → 命令旁听的可能相关 → 工作区脏 → 读取失败 → 干净 → 非仓库。
|
|
2379
|
-
if (summary.mine > 0) token = { text: "●" + compactCount(summary.mine), kind: "mine" };
|
|
2380
|
-
else if (summary.possible > 0) token = { text: "◐" + compactCount(summary.possible), kind: "possible" };
|
|
2381
|
-
else if (summary.dirty) token = { text: "!" + compactCount(summary.changed), kind: "dirty" };
|
|
2382
|
-
else if (summary.row.error !== null) token = { text: "?", kind: "error" };
|
|
2383
|
-
else if (summary.row.repo === true) token = { text: "✓", kind: "clean" };
|
|
2384
|
-
else token = { text: "—", kind: "clean" };
|
|
2385
|
-
probe.log("row-token", { sessionId: sessionId, text: token.text, kind: token.kind });
|
|
2386
|
-
return h("span", {
|
|
2387
|
-
className: "dshh-rowtoken dshh-rowtoken-" + token.kind,
|
|
2388
|
-
"data-dsh-hud": "row-token",
|
|
2389
|
-
title: rowChipTitle(summary.row, t),
|
|
2390
|
-
}, token.text);
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
/**
|
|
2394
|
-
* 悬停动条上那枚(座位 `sidebar.workspaces.session.row.action`)。
|
|
2395
|
-
*
|
|
2396
|
-
* 动作条只有 32px 高、平时就放一两个图标按钮,所以只放"图标级"宽度:
|
|
2397
|
-
* `!` 脏 + 改动总数 + `●n`/`◐n` 本会话改动数;明细在悬停里。
|
|
2398
|
-
*/
|
|
2399
|
-
function SessionRowChip(props) {
|
|
2400
|
-
var t = props.t || function (key) { return key; };
|
|
2401
|
-
var sessionId = props.sessionId;
|
|
2402
|
-
var store = props.store;
|
|
2403
|
-
var state = useStore(store);
|
|
2404
|
-
if (!sessionId || state.sidebarMode !== "hover") return null;
|
|
2405
|
-
var row = state.rows && state.rows.byId ? state.rows.byId[sessionId] : undefined;
|
|
2406
|
-
var summary = rowSummaryOf(row);
|
|
2407
|
-
if (summary === null) return null;
|
|
2408
|
-
|
|
2409
|
-
var children = [];
|
|
2410
|
-
if (summary.row.repo === true) {
|
|
2411
|
-
var head = summary.dirty ? "!" : "";
|
|
2412
|
-
if (summary.changed > 0) head += String(summary.changed);
|
|
2413
|
-
if (head !== "") children.push(h("span", { key: "git" }, head));
|
|
2414
|
-
}
|
|
2415
|
-
if (summary.mine > 0) children.push(h("span", { key: "mine", className: "dshh-rowchipMine" }, "●" + String(summary.mine)));
|
|
2416
|
-
else if (summary.possible > 0) children.push(h("span", { key: "possible", className: "dshh-rowchipPossible" }, "◐" + String(summary.possible)));
|
|
2417
|
-
|
|
2418
|
-
// 干净:给一个 ✓,别让这一格空着(空着和"没做这个功能"长得一样)。
|
|
2419
|
-
if (children.length === 0) children.push(h("span", { key: "clean", className: "dshh-rowchipClean" }, "✓"));
|
|
2420
|
-
probe.log("row-chip", {
|
|
2421
|
-
sessionId: sessionId,
|
|
2422
|
-
repo: summary.row.repo === true,
|
|
2423
|
-
dirty: summary.dirty,
|
|
2424
|
-
changed: summary.changed,
|
|
2425
|
-
mine: summary.mine,
|
|
2426
|
-
possible: summary.possible,
|
|
2427
|
-
});
|
|
2428
|
-
return h("span", {
|
|
2429
|
-
className: "dshh-rowchip",
|
|
2430
|
-
"data-dsh-hud": "row-chip",
|
|
2431
|
-
title: rowChipTitle(summary.row, t),
|
|
2432
|
-
}, children);
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
2548
|
/**
|
|
2436
2549
|
* 侧栏会话行**悬停卡片**里的那一块(座位 `sidebar.session.row.hover`)。
|
|
2437
2550
|
*
|
|
@@ -2444,7 +2557,7 @@ window.__ModuleLoader__.load({
|
|
|
2444
2557
|
var sessionId = props.sessionId;
|
|
2445
2558
|
var store = props.store;
|
|
2446
2559
|
var state = useStore(store);
|
|
2447
|
-
if (!sessionId
|
|
2560
|
+
if (!sessionId) return null;
|
|
2448
2561
|
var row = state.rows && state.rows.byId ? state.rows.byId[sessionId] : undefined;
|
|
2449
2562
|
var summary = rowSummaryOf(row);
|
|
2450
2563
|
if (summary === null) return null;
|
|
@@ -2473,7 +2586,9 @@ window.__ModuleLoader__.load({
|
|
|
2473
2586
|
if (row.error) lines.push(t("rows.error", { code: String(row.error) }));
|
|
2474
2587
|
var own = [];
|
|
2475
2588
|
if (mineCount > 0) own.push(t("session.pill", { n: String(mineCount) }));
|
|
2476
|
-
if (possibleCount > 0)
|
|
2589
|
+
if (possibleCount > 0) {
|
|
2590
|
+
own.push(t("rows.possible", { n: String(possibleCount), now: String(summary.possibleNow) }));
|
|
2591
|
+
}
|
|
2477
2592
|
// 和第二行对齐:没有改动记录时也明说一句 —— 否则卡片上会缺一行,看着像没加载出来。
|
|
2478
2593
|
lines.push(own.length === 0 ? t("rows.ownNone") : own.join(" · "));
|
|
2479
2594
|
if (lines.length === 0) return null;
|
|
@@ -2486,35 +2601,6 @@ window.__ModuleLoader__.load({
|
|
|
2486
2601
|
}));
|
|
2487
2602
|
}
|
|
2488
2603
|
|
|
2489
|
-
/** 行内 chip 的悬停说明:分支、上游、各类计数、以及这个会话自己的改动。 */
|
|
2490
|
-
function rowChipTitle(row, t) {
|
|
2491
|
-
var lines = [];
|
|
2492
|
-
if (row.repo === true) {
|
|
2493
|
-
var head = branchLabel(row, t);
|
|
2494
|
-
if (row.dirty === true) head += "*";
|
|
2495
|
-
if (row.ahead > 0) head += " ↑" + String(row.ahead);
|
|
2496
|
-
if (row.behind > 0) head += " ↓" + String(row.behind);
|
|
2497
|
-
lines.push(head);
|
|
2498
|
-
var shorthands = countShorthands(row.counts);
|
|
2499
|
-
if (shorthands.length > 0) {
|
|
2500
|
-
lines.push(shorthands.map(function (item) {
|
|
2501
|
-
return t("count." + item.key) + " " + String(item.value);
|
|
2502
|
-
}).join(" · "));
|
|
2503
|
-
}
|
|
2504
|
-
if (row.cwd) lines.push(row.cwd);
|
|
2505
|
-
} else if (row.repo === false && row.error === null) {
|
|
2506
|
-
// 不在仓库里也要说清(不能只丢一个路径 —— 用户看不出"为什么这一行没有分支")。
|
|
2507
|
-
lines.push(t("rows.noRepo"));
|
|
2508
|
-
if (row.cwd) lines.push(row.cwd);
|
|
2509
|
-
}
|
|
2510
|
-
if (row.error) lines.push(t("rows.error", { code: String(row.error) }));
|
|
2511
|
-
var mineCount = typeof row.mine === "number" ? row.mine : 0;
|
|
2512
|
-
var possibleCount = typeof row.possible === "number" ? row.possible : 0;
|
|
2513
|
-
if (mineCount > 0) lines.push(t("session.pill", { n: String(mineCount) }));
|
|
2514
|
-
if (possibleCount > 0) lines.push(t("rows.possible", { n: String(possibleCount) }));
|
|
2515
|
-
return lines.join("\n");
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
2604
|
function HudDockPill(props) {
|
|
2519
2605
|
var t = props.t || function (key) { return key; };
|
|
2520
2606
|
var sessionId = props.sessionId;
|
|
@@ -2536,6 +2622,24 @@ window.__ModuleLoader__.load({
|
|
|
2536
2622
|
|
|
2537
2623
|
var state = useStore(store);
|
|
2538
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
|
+
|
|
2539
2643
|
React.useEffect(function () {
|
|
2540
2644
|
if (!mainView) return;
|
|
2541
2645
|
store.bindSession(sessionId, cwd);
|
|
@@ -2550,18 +2654,30 @@ window.__ModuleLoader__.load({
|
|
|
2550
2654
|
: null;
|
|
2551
2655
|
|
|
2552
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
|
+
}
|
|
2553
2664
|
if (state.phase === "error") {
|
|
2554
|
-
children.push("
|
|
2665
|
+
children.push(h("span", { key: "gitWraperr", className: "dshh-gitWrap" }, "git:(" + t("pill.error") + ")"));
|
|
2555
2666
|
} else if (state.phase === "loading" || state.phase === "idle") {
|
|
2556
|
-
children.push("
|
|
2667
|
+
children.push(h("span", { key: "gitWrapload", className: "dshh-gitWrap" }, "git:(" + t("pill.loading") + ")"));
|
|
2557
2668
|
} else if (!state.status || !state.status.repo) {
|
|
2558
|
-
children.push("
|
|
2669
|
+
children.push(h("span", { key: "gitWrapnorepo", className: "dshh-gitWrap" }, "git:(" + t("pill.notRepo") + ")"));
|
|
2559
2670
|
} else {
|
|
2560
2671
|
var status = state.status;
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
children.push(
|
|
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)));
|
|
2565
2681
|
|
|
2566
2682
|
// 简写符:只列非零项,按固定顺序,各自上色(M 黄 / A 绿 / D 红 / ? 灰 …)。
|
|
2567
2683
|
// 与侧栏行内 chip 共用 `countShorthands` —— 两处各写一套算法一定会漂。
|
|
@@ -2608,7 +2724,132 @@ window.__ModuleLoader__.load({
|
|
|
2608
2724
|
onClick: function () {
|
|
2609
2725
|
store.togglePanel();
|
|
2610
2726
|
},
|
|
2611
|
-
}, 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");
|
|
2612
2853
|
}
|
|
2613
2854
|
|
|
2614
2855
|
/* ------------------------------------------------------- 悬浮面板 */
|
|
@@ -2634,13 +2875,24 @@ window.__ModuleLoader__.load({
|
|
|
2634
2875
|
* @param layout - `panelLayout()` 的结果。
|
|
2635
2876
|
* @param diffWidth - 差异栏的目标宽度。
|
|
2636
2877
|
*/
|
|
2637
|
-
function panelWidthFor(layout, diffWidth) {
|
|
2638
|
-
var
|
|
2639
|
-
|
|
2640
|
-
|
|
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;
|
|
2641
2894
|
var available = window.innerWidth - 2 * VIEWPORT_MARGIN;
|
|
2642
|
-
|
|
2643
|
-
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);
|
|
2644
2896
|
}
|
|
2645
2897
|
|
|
2646
2898
|
/** 把位置夹回视口内,避免拖到屏幕外再也点不到。 */
|
|
@@ -2700,7 +2952,7 @@ window.__ModuleLoader__.load({
|
|
|
2700
2952
|
// 从 store 现读,别用可能过期的渲染闭包(模式切换会改宽度)。
|
|
2701
2953
|
function currentPanelWidth() {
|
|
2702
2954
|
var snapshot = store.getSnapshot();
|
|
2703
|
-
return panelWidthFor(panelLayout(snapshot), snapshot.diffWidth);
|
|
2955
|
+
return panelWidthFor(panelLayout(snapshot), snapshot.diffWidth, snapshot.commitWidth);
|
|
2704
2956
|
}
|
|
2705
2957
|
|
|
2706
2958
|
// Esc 收起面板。用捕获阶段监听:焦点可能在任何输入框里,捕获阶段先拿到。
|
|
@@ -2833,7 +3085,8 @@ window.__ModuleLoader__.load({
|
|
|
2833
3085
|
if (!grip) return undefined;
|
|
2834
3086
|
var onMove = function (event) {
|
|
2835
3087
|
if (event.pointerId !== grip.pointerId) return;
|
|
2836
|
-
store.
|
|
3088
|
+
if (grip.kind === "commit") store.setCommitWidth(grip.startWidth - (event.clientX - grip.startX));
|
|
3089
|
+
else store.setDiffWidth(grip.startWidth - (event.clientX - grip.startX));
|
|
2837
3090
|
};
|
|
2838
3091
|
var finish = function (event) {
|
|
2839
3092
|
if (event.pointerId !== grip.pointerId) return;
|
|
@@ -2852,15 +3105,21 @@ window.__ModuleLoader__.load({
|
|
|
2852
3105
|
|
|
2853
3106
|
if (!state.panelOpen) return null;
|
|
2854
3107
|
|
|
2855
|
-
|
|
3108
|
+
/**
|
|
3109
|
+
* 抓握按下。
|
|
3110
|
+
*
|
|
3111
|
+
* `kind` 决定它调哪一栏:`diff` = 面板右边缘那条(调差异栏);`commit` = 提交栏右边那条
|
|
3112
|
+
* (调提交明细栏)。只有提交栏、没有差异栏时,右边缘那条也调提交栏 —— 它是右边界上唯一
|
|
3113
|
+
* 还能变宽的东西,不该按下去没反应。
|
|
3114
|
+
*/
|
|
3115
|
+
function onGripPointerDown(kind, event) {
|
|
2856
3116
|
if (event.button !== 0) return;
|
|
2857
3117
|
var snapshot = store.getSnapshot();
|
|
2858
3118
|
event.preventDefault();
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
});
|
|
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 });
|
|
2864
3123
|
}
|
|
2865
3124
|
|
|
2866
3125
|
function onHeaderPointerDown(event) {
|
|
@@ -2897,7 +3156,7 @@ window.__ModuleLoader__.load({
|
|
|
2897
3156
|
position: "fixed",
|
|
2898
3157
|
zIndex: 30,
|
|
2899
3158
|
boxSizing: "border-box",
|
|
2900
|
-
width: panelWidthFor(layout, state.diffWidth),
|
|
3159
|
+
width: panelWidthFor(layout, state.diffWidth, state.commitWidth),
|
|
2901
3160
|
maxWidth: "calc(100vw - 16px)",
|
|
2902
3161
|
display: "flex",
|
|
2903
3162
|
flexDirection: "column",
|
|
@@ -2976,8 +3235,6 @@ window.__ModuleLoader__.load({
|
|
|
2976
3235
|
onToggleLatest: function () { store.toggleOnlyLatest(); },
|
|
2977
3236
|
review: reviewView(state),
|
|
2978
3237
|
onClearReviewed: function () { store.clearReviewed(); },
|
|
2979
|
-
sidebarMode: state.sidebarMode,
|
|
2980
|
-
onCycleSidebarMode: function () { store.cycleSidebarMode(); },
|
|
2981
3238
|
}),
|
|
2982
3239
|
// 顺序:改动概览 → **改动清单** → 最近提交。清单是"现在要处理的",
|
|
2983
3240
|
// 提交是背景信息;清单放前面,一屏不够时先看到的是眼前的事。
|
|
@@ -3005,6 +3262,24 @@ window.__ModuleLoader__.load({
|
|
|
3005
3262
|
// 再点一次打开着的那条 = 收起明细(打开提交不改左栏,只高亮它)。
|
|
3006
3263
|
onOpen: function (commit) { store.toggleCommit(commit); },
|
|
3007
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
|
+
}),
|
|
3008
3283
|
];
|
|
3009
3284
|
listBody = h.apply(null, ["div", { className: "dshh-body" }].concat(repoChildren));
|
|
3010
3285
|
} else {
|
|
@@ -3024,7 +3299,15 @@ window.__ModuleLoader__.load({
|
|
|
3024
3299
|
onOpenFile: function (file) { store.openCommitDiff(file); },
|
|
3025
3300
|
activeFile: commitOpenFile,
|
|
3026
3301
|
cursor: state.cursor,
|
|
3027
|
-
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); },
|
|
3028
3311
|
})
|
|
3029
3312
|
: null,
|
|
3030
3313
|
diffMode ? h(DiffView, { t: t, diff: state.diff, store: store }) : null)
|
|
@@ -3075,9 +3358,17 @@ window.__ModuleLoader__.load({
|
|
|
3075
3358
|
className: "dshh-grip",
|
|
3076
3359
|
"data-dsh-hud": "grip",
|
|
3077
3360
|
title: t("diff.resize"),
|
|
3078
|
-
onPointerDown: onGripPointerDown,
|
|
3361
|
+
onPointerDown: function (event) { onGripPointerDown("diff", event); },
|
|
3079
3362
|
})
|
|
3080
|
-
:
|
|
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,
|
|
3081
3372
|
h("div", { className: "dshh-footer" },
|
|
3082
3373
|
h("span", null, t("panel.readonly")),
|
|
3083
3374
|
h("span", null, state.updatedAt ? t("panel.updated") + timeLabel(state.updatedAt) : "")));
|
|
@@ -3093,7 +3384,7 @@ window.__ModuleLoader__.load({
|
|
|
3093
3384
|
// dock 座位给的是整栏宽的一行,而输入卡片有自己的最大宽度并居中
|
|
3094
3385
|
// (--dsh-composer-card-max-width)。外层容器复刻框架自带 dock 占位元素
|
|
3095
3386
|
// (队列公告)的几何,胶囊才会真正贴在输入卡片上方左端,而不是飘到栏的最左。
|
|
3096
|
-
".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;",
|
|
3097
3388
|
"width:calc(100% - var(--dsh-composer-side-clearance,16px) - var(--dsh-composer-side-clearance,16px)",
|
|
3098
3389
|
" - var(--dsh-composer-dock-inset,8px) - var(--dsh-composer-dock-inset,8px));",
|
|
3099
3390
|
"max-width:calc(var(--dsh-composer-card-max-width,952px) - var(--dsh-composer-dock-inset,8px) - var(--dsh-composer-dock-inset,8px));",
|
|
@@ -3146,37 +3437,43 @@ window.__ModuleLoader__.load({
|
|
|
3146
3437
|
"color:var(--dsw-alias-brand-primary,#4b8bf5);}",
|
|
3147
3438
|
".dshh-markerPossible{color:var(--dsw-alias-state-warn-primary,#d9a63c);opacity:.75;}",
|
|
3148
3439
|
// 行尾的审阅标记:`○` 未审 / `✓` 这一版已审 / `✓`(黄) 改动已变。
|
|
3149
|
-
// 侧栏会话行里那枚 chip:一行只有 32px 高、悬停才露出来,所以它必须很小。
|
|
3150
|
-
// 侧栏会话行里那枚 chip:一行只有 32px 高、悬停动作条很窄,所以它必须小且不许撑宽。
|
|
3151
|
-
".dshh-rowchip{display:inline-flex;align-items:center;gap:2px;flex:none;max-width:72px;",
|
|
3152
|
-
"font-family:ui-monospace,Consolas,monospace;font-size:10.5px;line-height:1;",
|
|
3153
|
-
"white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-secondary,#8a8f98);}",
|
|
3154
|
-
".dshh-rowchipMine{color:var(--dsw-alias-brand-primary,#4b8bf5);}",
|
|
3155
|
-
// 行首常亮那枚:产品给的格子只有 16px,所以最多两个字符、不许换行。
|
|
3156
|
-
".dshh-rowtoken{font-family:ui-monospace,Consolas,monospace;font-size:10px;line-height:1;",
|
|
3157
|
-
"white-space:nowrap;flex:none;letter-spacing:-0.02em;}",
|
|
3158
3440
|
// `⋯` 菜单里那条只读摘要:像一条说明,不像一个可点的菜单项。
|
|
3159
3441
|
".dshh-menureadout{display:flex;flex-direction:column;gap:2px;padding:4px 10px 6px;",
|
|
3160
3442
|
"font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary,var(--dsw-alias-label-secondary,#8a8f98));",
|
|
3161
3443
|
"cursor:default;user-select:text;max-width:260px;}",
|
|
3162
3444
|
".dshh-menureadoutLine{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
|
|
3163
|
-
".dshh-rowtoken-mine{color:var(--dsw-alias-brand-primary,#4b8bf5);}",
|
|
3164
|
-
".dshh-rowtoken-possible{color:var(--dsw-alias-state-warn-primary,#d9a63c);}",
|
|
3165
|
-
".dshh-rowtoken-dirty{color:var(--dsw-alias-state-warn-primary,#d9a63c);opacity:.75;}",
|
|
3166
|
-
".dshh-rowtoken-clean{color:var(--dsw-alias-label-tertiary,rgba(138,143,152,0.6));}",
|
|
3167
|
-
".dshh-rowtoken-error{color:var(--dsw-alias-state-warn-primary,#d9a63c);}",
|
|
3168
|
-
".dshh-rowchipClean{color:var(--dsw-alias-state-success-primary,#3fb950);opacity:.8;}",
|
|
3169
3445
|
// 悬停卡片里那块详情:跟着卡片自己的字号,只做行距与弱化。
|
|
3170
3446
|
".dshh-rowhover{display:flex;flex-direction:column;gap:2px;font-size:11px;",
|
|
3171
3447
|
"color:var(--dsw-alias-label-secondary,#8a8f98);min-width:0;}",
|
|
3172
3448
|
".dshh-rowhoverLine{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}",
|
|
3173
|
-
".dshh-rowchipPossible{color:var(--dsw-alias-state-warn-primary,#d9a63c);}",
|
|
3174
3449
|
".dshh-review{flex:none;margin-left:auto;padding:0 2px;font-size:10.5px;cursor:pointer;",
|
|
3175
3450
|
"color:var(--dsw-alias-label-dimmed,rgba(138,143,152,0.65));}",
|
|
3176
3451
|
".dshh-review:hover{color:var(--dsw-alias-label-primary,#e5e7eb);}",
|
|
3177
3452
|
".dshh-reviewOn{color:var(--dsw-alias-state-success-primary,#3fb950);}",
|
|
3178
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);}",
|
|
3179
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);}",
|
|
3180
3477
|
".dshh-section{display:flex;flex-direction:column;gap:4px;}",
|
|
3181
3478
|
".dshh-list{display:flex;flex-direction:column;gap:1px;max-height:30vh;overflow:auto;",
|
|
3182
3479
|
"border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,0.16));padding-top:4px;}",
|
|
@@ -3242,6 +3539,9 @@ window.__ModuleLoader__.load({
|
|
|
3242
3539
|
"box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary,#4b8bf5);}",
|
|
3243
3540
|
".dshh-hunkMark{text-align:right;}",
|
|
3244
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;}",
|
|
3245
3545
|
".dshh-grip:hover{background:var(--dsw-alias-border-l2,rgba(127,127,127,0.35));}",
|
|
3246
3546
|
".dshh-diffview{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;}",
|
|
3247
3547
|
".dshh-toolbar{display:flex;align-items:center;gap:4px;padding:5px 8px;flex:none;overflow:hidden;",
|
|
@@ -3380,6 +3680,17 @@ window.__ModuleLoader__.load({
|
|
|
3380
3680
|
};
|
|
3381
3681
|
}, "dsh-hud: session rows poll");
|
|
3382
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
|
+
|
|
3383
3694
|
// `⋯` 菜单里的一条只读摘要:行尾拿不到常亮座位,这里是最实在的"完整信息"落点。
|
|
3384
3695
|
ctx.slots.inject("sidebar.workspaces.session.menu.item", function () {
|
|
3385
3696
|
return ctx.slots.register({
|
|
@@ -3393,31 +3704,6 @@ window.__ModuleLoader__.load({
|
|
|
3393
3704
|
}, SessionMenuSummary);
|
|
3394
3705
|
});
|
|
3395
3706
|
|
|
3396
|
-
// 行首那格(常亮档):产品只给 16px,所以是一枚双字符标记。
|
|
3397
|
-
ctx.slots.inject("sidebar.session.row.leading", function () {
|
|
3398
|
-
return ctx.slots.register({
|
|
3399
|
-
name: "sidebar.session.row.leading",
|
|
3400
|
-
id: "dsh-hud-session-leading",
|
|
3401
|
-
order: 30,
|
|
3402
|
-
locale: NS,
|
|
3403
|
-
inject: function () {
|
|
3404
|
-
return { store: store };
|
|
3405
|
-
},
|
|
3406
|
-
}, SessionLeadingToken);
|
|
3407
|
-
});
|
|
3408
|
-
|
|
3409
|
-
// 侧栏每条会话行悬停时的那组动作:产品自己的动作也只是普通条目,这里挂一枚只读状态。
|
|
3410
|
-
ctx.slots.inject("sidebar.workspaces.session.row.action", function () {
|
|
3411
|
-
return ctx.slots.register({
|
|
3412
|
-
name: "sidebar.workspaces.session.row.action",
|
|
3413
|
-
id: "dsh-hud-session-status",
|
|
3414
|
-
order: 30,
|
|
3415
|
-
locale: NS,
|
|
3416
|
-
inject: function () {
|
|
3417
|
-
return { store: store };
|
|
3418
|
-
},
|
|
3419
|
-
}, SessionRowChip);
|
|
3420
|
-
});
|
|
3421
3707
|
|
|
3422
3708
|
// 悬停卡片里那块详情(宽敞的地方;动作条那枚放不下的东西都在这儿)。
|
|
3423
3709
|
ctx.slots.inject("sidebar.session.row.hover", function () {
|
|
@@ -3432,6 +3718,7 @@ window.__ModuleLoader__.load({
|
|
|
3432
3718
|
}, SessionRowHoverChip);
|
|
3433
3719
|
});
|
|
3434
3720
|
|
|
3721
|
+
|
|
3435
3722
|
ctx.slots.inject("conversation.input.dock", function () {
|
|
3436
3723
|
return ctx.slots.register({
|
|
3437
3724
|
name: "conversation.input.dock",
|