dsh-activity-pane 0.2.2 → 0.3.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/.dsh-plugin/client.js +183 -41
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance.mjs +9 -5
- package/scripts/bench.mjs +2 -2
- package/scripts/check.mjs +146 -27
- package/src/client.mjs +132 -22
- package/src/core.mjs +51 -19
package/src/client.mjs
CHANGED
|
@@ -32,8 +32,12 @@ const INDENT_PX = 16;
|
|
|
32
32
|
const MOBILE_BREAKPOINT = "767px";
|
|
33
33
|
/** 运行卡时钟:只要存在运行中会话,就以该周期刷新时长显示。 */
|
|
34
34
|
const CLOCK_MS = 1000;
|
|
35
|
+
/** 历史卡相对时间刷新周期;无需每秒重绘整列。 */
|
|
36
|
+
const RECENT_TIME_REFRESH_MS = 60_000;
|
|
35
37
|
/** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
|
|
36
38
|
const LOAD_CONCURRENCY = 3;
|
|
39
|
+
/** 历史分页每批数量;后续批次只由底部按钮显式加载。 */
|
|
40
|
+
const RECENT_PAGE_SIZE = 10;
|
|
37
41
|
/** 「回到顶部」悬浮按钮显隐阈值:scrollTop 超过该值(px)时显示(R-01-018/AC-01)。 */
|
|
38
42
|
const TOP_THRESHOLD = 200;
|
|
39
43
|
|
|
@@ -117,21 +121,34 @@ const CSS = `
|
|
|
117
121
|
touch-action: pan-y;
|
|
118
122
|
-webkit-overflow-scrolling: touch;
|
|
119
123
|
padding: 0 0 10px;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
/* Keep the native scrollbar just left of the edge handle, as in the host
|
|
125
|
+
sidebar; stable keeps the list geometry unchanged when overflow appears. */
|
|
126
|
+
margin-right: var(--dsh-scrollbar-width, 8px);
|
|
127
|
+
scrollbar-gutter: stable;
|
|
128
|
+
}
|
|
129
|
+
/* 滚动条在滚动或鼠标进入窗格时显示(R-01-004/AC-03,与外壳侧栏一致):thumb 默认透明,
|
|
130
|
+
经 data-scrolling/data-pointer-inside 显示,悬停时读取主题 hover token。Firefox 路径必须在
|
|
131
|
+
@supports 门内——非 auto 的 scrollbar-color 会让 Chromium 丢弃该元素的 ::-webkit-scrollbar 规则。 */
|
|
124
132
|
[data-dsh-activity-pane] .dap-scroll::-webkit-scrollbar-thumb {
|
|
125
133
|
background: transparent;
|
|
126
134
|
}
|
|
127
135
|
[data-dsh-activity-pane] .dap-scroll[data-scrolling]::-webkit-scrollbar-thumb {
|
|
128
136
|
background: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent));
|
|
129
137
|
}
|
|
138
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb {
|
|
139
|
+
background: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent));
|
|
140
|
+
}
|
|
141
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb:hover {
|
|
142
|
+
background: var(--dsh-scrollbar-thumb-hover, color-mix(in srgb, currentColor 40%, transparent));
|
|
143
|
+
}
|
|
130
144
|
@supports not selector(::-webkit-scrollbar) {
|
|
131
145
|
[data-dsh-activity-pane] .dap-scroll { scrollbar-color: transparent transparent; }
|
|
132
146
|
[data-dsh-activity-pane] .dap-scroll[data-scrolling] {
|
|
133
147
|
scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
|
|
134
148
|
}
|
|
149
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll {
|
|
150
|
+
scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
|
|
151
|
+
}
|
|
135
152
|
}
|
|
136
153
|
/* 「回到顶部」悬浮图标按钮(R-01-018):窗格内右下角圆形按钮,纯图标无文字、不透明底色;
|
|
137
154
|
默认 hidden,scrollTop 超阈值时由滚动监听揭隐。基类 display:flex 会压过 UA 的
|
|
@@ -188,6 +205,29 @@ const CSS = `
|
|
|
188
205
|
color: color-mix(in srgb, currentColor 52%, transparent);
|
|
189
206
|
text-transform: uppercase;
|
|
190
207
|
}
|
|
208
|
+
/* 历史分页只在用户明确激活底部按钮后追加(R-01-019/AC-02)。 */
|
|
209
|
+
[data-dsh-activity-pane] .dap-recent-more {
|
|
210
|
+
align-self: center;
|
|
211
|
+
flex: none;
|
|
212
|
+
min-width: 132px;
|
|
213
|
+
padding: 5px 10px;
|
|
214
|
+
border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
|
|
215
|
+
border-radius: 6px;
|
|
216
|
+
background: color-mix(in srgb, currentColor 7%, transparent);
|
|
217
|
+
color: color-mix(in srgb, currentColor 70%, transparent);
|
|
218
|
+
font: inherit;
|
|
219
|
+
font-size: 11px;
|
|
220
|
+
line-height: 16px;
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
}
|
|
223
|
+
[data-dsh-activity-pane] .dap-recent-more:hover,
|
|
224
|
+
[data-dsh-activity-pane] .dap-recent-more:focus-visible {
|
|
225
|
+
background: color-mix(in srgb, currentColor 13%, transparent);
|
|
226
|
+
color: inherit;
|
|
227
|
+
}
|
|
228
|
+
[data-dsh-activity-pane] .dap-recent-more:focus-visible { outline: 2px solid color-mix(in srgb, currentColor 45%, transparent); outline-offset: 1px; }
|
|
229
|
+
[data-dsh-activity-pane] .dap-recent-more[disabled] { cursor: wait; opacity: 0.6; }
|
|
230
|
+
[data-dsh-activity-pane] .dap-recent-more[hidden] { display: none; }
|
|
191
231
|
/* 折叠:仅桌面生效;窄条 + 竖排标题与计数(R-01-011/AC-04)。 */
|
|
192
232
|
[data-dsh-activity-pane] .dap-rail {
|
|
193
233
|
display: none;
|
|
@@ -226,7 +266,8 @@ const CSS = `
|
|
|
226
266
|
background: rgba(46, 42, 26, 0.97);
|
|
227
267
|
animation: dap-await-pulse 1.2s ease-in-out infinite;
|
|
228
268
|
}
|
|
229
|
-
/* 桌面拖拽调宽手柄(R-01-015):右缘 6px
|
|
269
|
+
/* 桌面拖拽调宽手柄(R-01-015):右缘 6px 透明命中区,拖拽实时写入 --dap-width;
|
|
270
|
+
滚动区在其左侧留出 native scrollbar 命中带,手柄只改变光标、不绘制 hover 高亮;
|
|
230
271
|
折叠窄条与移动端抽屉不提供拖拽(下方两处媒体查询隐藏)。 */
|
|
231
272
|
[data-dsh-activity-pane] .dap-resize {
|
|
232
273
|
position: absolute;
|
|
@@ -236,10 +277,6 @@ const CSS = `
|
|
|
236
277
|
touch-action: none;
|
|
237
278
|
z-index: 6;
|
|
238
279
|
}
|
|
239
|
-
[data-dsh-activity-pane] .dap-resize:hover,
|
|
240
|
-
[data-dsh-activity-pane] .dap-resize[data-dragging] {
|
|
241
|
-
background: color-mix(in srgb, currentColor 18%, transparent);
|
|
242
|
-
}
|
|
243
280
|
/* 桌面:窗格作为中间列内的真实 flex 行元素参与布局——中间列被置为行方向,
|
|
244
281
|
窗格固定宽、会话区弹性收缩,整个会话内容被真实挤到右边;折叠时收窄为窄条。 */
|
|
245
282
|
@media (min-width: 768px) {
|
|
@@ -945,12 +982,15 @@ function livenessFromSnapshot(snap) {
|
|
|
945
982
|
return { startTime };
|
|
946
983
|
}
|
|
947
984
|
|
|
948
|
-
|
|
985
|
+
/** 历史卡时间同时给出本地绝对日期时分与相对年龄;跨年份的绝对日期带年份。 */
|
|
986
|
+
function fmtRecentTime(ts, now = Date.now()) {
|
|
949
987
|
try {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
988
|
+
const value = Number(ts);
|
|
989
|
+
const current = Number(now);
|
|
990
|
+
const absolute = fmtAbsoluteDateTime(ts, now);
|
|
991
|
+
if (!absolute) return "";
|
|
992
|
+
const relative = fmtRelativeAge(current - value);
|
|
993
|
+
return relative ? `最后活动 · ${absolute} · ${relative}` : `最后活动 · ${absolute}`;
|
|
954
994
|
} catch {
|
|
955
995
|
return "";
|
|
956
996
|
}
|
|
@@ -984,6 +1024,7 @@ function apply(ctx) {
|
|
|
984
1024
|
let sessionUnsubscribe = null;
|
|
985
1025
|
let workspaceUnsubscribe = null;
|
|
986
1026
|
let clockTimer = null;
|
|
1027
|
+
let recentTimeTimer = null;
|
|
987
1028
|
let syncScheduled = false;
|
|
988
1029
|
let lastSig = "";
|
|
989
1030
|
/** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
|
|
@@ -1036,6 +1077,11 @@ function apply(ctx) {
|
|
|
1036
1077
|
const cardsById = new Map();
|
|
1037
1078
|
/** 历史区卡片 id → { el } 复用表。 */
|
|
1038
1079
|
const recentCardsById = new Map();
|
|
1080
|
+
/** 历史区渐进呈现状态:候选由核心完整派生,DOM 与详情读取只覆盖当前前缀。 */
|
|
1081
|
+
let recentVisibleCount = RECENT_PAGE_SIZE;
|
|
1082
|
+
let recentTotal = 0;
|
|
1083
|
+
let recentHasMore = false;
|
|
1084
|
+
let recentAppendQueued = false;
|
|
1039
1085
|
/** 运行中会话原生快照订阅:id → { unsubscribe, liveness, snapshot }。 */
|
|
1040
1086
|
const livenessById = new Map();
|
|
1041
1087
|
/** 委托周期进度锚点记账:id → progressAnchor 状态(R-01-009/AC-06)。 */
|
|
@@ -1449,12 +1495,25 @@ function apply(ctx) {
|
|
|
1449
1495
|
window.dispatchEvent(new Event("resize"));
|
|
1450
1496
|
} catch {}
|
|
1451
1497
|
}
|
|
1498
|
+
function loadMoreRecent() {
|
|
1499
|
+
if (disposed || !recentHasMore || recentAppendQueued) return false;
|
|
1500
|
+
const nextCount = Math.min(recentVisibleCount + RECENT_PAGE_SIZE, recentTotal);
|
|
1501
|
+
if (nextCount <= recentVisibleCount) return false;
|
|
1502
|
+
recentAppendQueued = true;
|
|
1503
|
+
recentVisibleCount = nextCount;
|
|
1504
|
+
queueSync();
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1452
1507
|
function bindPaneControls(pane) {
|
|
1453
1508
|
const header = pane.querySelector(".dap-header");
|
|
1454
1509
|
const rail = pane.querySelector(".dap-rail");
|
|
1455
1510
|
const resize = pane.querySelector(".dap-resize");
|
|
1456
1511
|
const scroll = pane.querySelector(".dap-scroll");
|
|
1457
1512
|
const topBtn = pane.querySelector(".dap-top");
|
|
1513
|
+
const recentMore = pane.querySelector(".dap-recent-more");
|
|
1514
|
+
const onRecentMoreClick = () => {
|
|
1515
|
+
if (loadMoreRecent() && recentMore !== null) recentMore.disabled = true;
|
|
1516
|
+
};
|
|
1458
1517
|
// 标题行整体即收起控件,两端断点一致:桌面折叠为窄条(R-01-011/AC-03);
|
|
1459
1518
|
// 移动端断点解释为收起抽屉,而非折叠窄条(R-01-008/AC-02、R-01-011/AC-06)。
|
|
1460
1519
|
const onHeaderActivate = () => {
|
|
@@ -1477,6 +1536,15 @@ function apply(ctx) {
|
|
|
1477
1536
|
const syncTopBtn = () => {
|
|
1478
1537
|
if (topBtn !== null && scroll !== null) topBtn.hidden = scroll.scrollTop <= TOP_THRESHOLD;
|
|
1479
1538
|
};
|
|
1539
|
+
// 鼠标进入窗格即显示 scrollbar(R-01-004/AC-03);触摸指针不改变桌面滚动条可见状态。
|
|
1540
|
+
const onPanePointerEnter = (event) => {
|
|
1541
|
+
if (event.pointerType !== "mouse") return;
|
|
1542
|
+
pane.setAttribute("data-pointer-inside", "");
|
|
1543
|
+
};
|
|
1544
|
+
const onPanePointerLeave = (event) => {
|
|
1545
|
+
if (event.pointerType !== "mouse") return;
|
|
1546
|
+
pane.removeAttribute("data-pointer-inside");
|
|
1547
|
+
};
|
|
1480
1548
|
const onRailClick = () => {
|
|
1481
1549
|
collapsed = false;
|
|
1482
1550
|
pane.setAttribute("data-collapsed", "false");
|
|
@@ -1519,8 +1587,9 @@ function apply(ctx) {
|
|
|
1519
1587
|
resize.addEventListener("pointercancel", onResizeUp);
|
|
1520
1588
|
resize.setPointerCapture(event.pointerId);
|
|
1521
1589
|
};
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1590
|
+
// 滚动条随滚动或窗格内鼠标指针显示(R-01-004/AC-03):滚动即置位,停滚 600ms 后收口;
|
|
1591
|
+
// 指针离开后若仍在滚动拖尾内则保持显示,拖尾结束后隐藏。同一监听承载「回到顶部」按钮显隐
|
|
1592
|
+
//(R-01-018/AC-01、AC-03):回顶后的收口不经额外事件,
|
|
1524
1593
|
// 由平滑滚动触发的 scroll 事件自然完成。
|
|
1525
1594
|
let scrollHideTimer = null;
|
|
1526
1595
|
const onScroll = () => {
|
|
@@ -1541,14 +1610,21 @@ function apply(ctx) {
|
|
|
1541
1610
|
header?.addEventListener("click", onHeaderActivate);
|
|
1542
1611
|
header?.addEventListener("keydown", onHeaderKeydown);
|
|
1543
1612
|
rail?.addEventListener("click", onRailClick);
|
|
1613
|
+
pane.addEventListener("pointerenter", onPanePointerEnter);
|
|
1614
|
+
pane.addEventListener("pointerleave", onPanePointerLeave);
|
|
1544
1615
|
scroll?.addEventListener("scroll", onScroll, { passive: true });
|
|
1616
|
+
recentMore?.addEventListener("click", onRecentMoreClick);
|
|
1545
1617
|
topBtn?.addEventListener("click", onTopClick);
|
|
1546
1618
|
resize?.addEventListener("pointerdown", onResizeDown);
|
|
1547
1619
|
return () => {
|
|
1548
1620
|
header?.removeEventListener("click", onHeaderActivate);
|
|
1549
1621
|
header?.removeEventListener("keydown", onHeaderKeydown);
|
|
1550
1622
|
rail?.removeEventListener("click", onRailClick);
|
|
1623
|
+
pane.removeEventListener("pointerenter", onPanePointerEnter);
|
|
1624
|
+
pane.removeEventListener("pointerleave", onPanePointerLeave);
|
|
1625
|
+
pane.removeAttribute("data-pointer-inside");
|
|
1551
1626
|
scroll?.removeEventListener("scroll", onScroll);
|
|
1627
|
+
recentMore?.removeEventListener("click", onRecentMoreClick);
|
|
1552
1628
|
if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);
|
|
1553
1629
|
topBtn?.removeEventListener("click", onTopClick);
|
|
1554
1630
|
resize?.removeEventListener("pointerdown", onResizeDown);
|
|
@@ -1578,7 +1654,8 @@ function apply(ctx) {
|
|
|
1578
1654
|
<div class="dap-scroll">
|
|
1579
1655
|
<div class="dap-list" tabindex="-1"><div class="dap-tracks" aria-hidden="true"></div></div>
|
|
1580
1656
|
<div class="dap-recent">
|
|
1581
|
-
<div class="dap-recent-head"><span
|
|
1657
|
+
<div class="dap-recent-head"><span>最近历史</span></div>
|
|
1658
|
+
<button class="dap-recent-more" type="button" hidden>加载更多...</button>
|
|
1582
1659
|
</div>
|
|
1583
1660
|
</div>
|
|
1584
1661
|
<button class="dap-top" type="button" aria-label="回到顶部" title="回到顶部" hidden></button>
|
|
@@ -2358,6 +2435,16 @@ function apply(ctx) {
|
|
|
2358
2435
|
}
|
|
2359
2436
|
}
|
|
2360
2437
|
|
|
2438
|
+
/** 历史卡相对时间只需分钟级刷新;无历史卡时停止定时器,避免空窗格常驻唤醒。 */
|
|
2439
|
+
function syncRecentTimeClock(wanted) {
|
|
2440
|
+
if (wanted && recentTimeTimer === null) {
|
|
2441
|
+
recentTimeTimer = setInterval(() => queueSync(), RECENT_TIME_REFRESH_MS);
|
|
2442
|
+
} else if (!wanted && recentTimeTimer !== null) {
|
|
2443
|
+
clearInterval(recentTimeTimer);
|
|
2444
|
+
recentTimeTimer = null;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2361
2448
|
function getSessionSnapshot(session) {
|
|
2362
2449
|
try {
|
|
2363
2450
|
return session?.getSnapshot?.() ?? null;
|
|
@@ -2566,11 +2653,14 @@ function apply(ctx) {
|
|
|
2566
2653
|
if (entry.waitClass === "blocked" || entry.waitClass === "done" || entry.waitClass === "error")
|
|
2567
2654
|
rec.el.setAttribute("data-wait", entry.waitClass);
|
|
2568
2655
|
else rec.el.removeAttribute("data-wait");
|
|
2656
|
+
const recentTimeText = entry.kind === "recent" ? fmtRecentTime(entry.activityAt) : "";
|
|
2569
2657
|
rec.el.setAttribute(
|
|
2570
2658
|
"aria-label",
|
|
2571
2659
|
`${entry.workspaceTitle ? entry.workspaceTitle + " - " : ""}${entry.title}${
|
|
2572
2660
|
entry.pendingText ? "," + entry.pendingText : ""
|
|
2573
|
-
}${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}
|
|
2661
|
+
}${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}${
|
|
2662
|
+
recentTimeText ? "," + recentTimeText : ""
|
|
2663
|
+
}`,
|
|
2574
2664
|
);
|
|
2575
2665
|
renderCardInto(rec.el, entry, hueByWorkspace);
|
|
2576
2666
|
// 只有顺序/归属真正变化时才移动 DOM:每次渲染无条件 appendChild 会把所有
|
|
@@ -2761,6 +2851,10 @@ function apply(ctx) {
|
|
|
2761
2851
|
pulseSignature = "";
|
|
2762
2852
|
prevRenderedActiveIds = new Set();
|
|
2763
2853
|
prevRenderedRecentIds = new Set();
|
|
2854
|
+
recentVisibleCount = RECENT_PAGE_SIZE;
|
|
2855
|
+
recentTotal = 0;
|
|
2856
|
+
recentHasMore = false;
|
|
2857
|
+
recentAppendQueued = false;
|
|
2764
2858
|
// 旧窗格已脱离文档:其在飞平移的 transitionend 不再触发,逐元素取消避免残留。
|
|
2765
2859
|
for (const el of [...shiftCleanups.keys()]) cancelShift(el);
|
|
2766
2860
|
}
|
|
@@ -2780,6 +2874,8 @@ function apply(ctx) {
|
|
|
2780
2874
|
}
|
|
2781
2875
|
}
|
|
2782
2876
|
const listState = listLoadState(snapshot);
|
|
2877
|
+
// 只消费上一轮追加请求;列表短暂 pending 时保留已展开页,ready 后继续从同一前缀呈现。
|
|
2878
|
+
recentAppendQueued = false;
|
|
2783
2879
|
const workspaceSnapshot = getSnapshot(workspaces, "list");
|
|
2784
2880
|
const workspaceItems = workspaceSnapshot?.items ?? [];
|
|
2785
2881
|
const archivedSessionIds = workspaceSnapshot?.archivedSessionIds ?? [];
|
|
@@ -2918,8 +3014,12 @@ function apply(ctx) {
|
|
|
2918
3014
|
}
|
|
2919
3015
|
if (detail.memoTurnEnd != null) turnEnds[id] = detail.memoTurnEnd;
|
|
2920
3016
|
}
|
|
2921
|
-
const
|
|
2922
|
-
|
|
3017
|
+
const recentCandidates = buildRecent(snapshot, workspaceItems, now, sessionDetailsById, archivedSessionIds, completeAcksById, delegatingIds, turnEnds);
|
|
3018
|
+
recentTotal = recentCandidates.length;
|
|
3019
|
+
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
3020
|
+
recentHasMore = recent.length < recentTotal;
|
|
3021
|
+
syncRecentTimeClock(recent.length > 0);
|
|
3022
|
+
// 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
|
|
2923
3023
|
// 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
|
|
2924
3024
|
const previewFallbackIds = new Set();
|
|
2925
3025
|
for (const entry of recent) {
|
|
@@ -2937,7 +3037,7 @@ function apply(ctx) {
|
|
|
2937
3037
|
if (!entry.userPreview || !entry.agentPreview) previewFallbackIds.add(entry.id);
|
|
2938
3038
|
entry.loadingPreviews = (!entry.userPreview || !entry.agentPreview) && historyLoads.has(entry.id);
|
|
2939
3039
|
}
|
|
2940
|
-
//
|
|
3040
|
+
// 补充数据读取优先级:当前会话最优先,活动区先于当前已显示历史页(区内按显示顺序)。
|
|
2941
3041
|
const durationFallbackIds = new Set(active.filter((entry) => entry.kind === "awaiting").map((entry) => entry.id));
|
|
2942
3042
|
const detailIds = [...active, ...recent].map((entry) => entry.id);
|
|
2943
3043
|
detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
|
|
@@ -2959,7 +3059,9 @@ function apply(ctx) {
|
|
|
2959
3059
|
// listState 参与签名:空列表从 pending/error → ready 时卡集合不变,若只比较卡片
|
|
2960
3060
|
// 会被提前返回冻结在「加载中」/「列表加载失败」;数量胶囊可见面变化同样需要
|
|
2961
3061
|
// 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
|
|
2962
|
-
|
|
3062
|
+
// 历史卡的相对活动时间随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
|
|
3063
|
+
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
3064
|
+
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);
|
|
2963
3065
|
if (sig === lastSig) return;
|
|
2964
3066
|
const hueByWorkspace = resolveWorkspaceHues(visibleEntries.map((entry) => entry.workspaceKey));
|
|
2965
3067
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
|
@@ -3008,6 +3110,11 @@ function apply(ctx) {
|
|
|
3008
3110
|
if (recentSection !== null) {
|
|
3009
3111
|
recentSection.hidden = listState === "ready" && recent.length === 0;
|
|
3010
3112
|
ensureListStatus(recentSection, listState !== "ready" && recent.length === 0, listState === "loading" ? "加载中…" : "列表加载失败", { loading: listState === "loading" });
|
|
3113
|
+
const recentMore = recentSection.querySelector(".dap-recent-more");
|
|
3114
|
+
if (recentMore !== null) {
|
|
3115
|
+
recentMore.hidden = !recentHasMore;
|
|
3116
|
+
recentMore.disabled = recentAppendQueued;
|
|
3117
|
+
}
|
|
3011
3118
|
}
|
|
3012
3119
|
runMoveGhosts(movePlans);
|
|
3013
3120
|
if (shiftRects !== null) runShiftAnimations(shiftRects);
|
|
@@ -3068,6 +3175,8 @@ function apply(ctx) {
|
|
|
3068
3175
|
const headerExpanded = collapsed ? "false" : "true";
|
|
3069
3176
|
if (headerEl !== null && headerEl.getAttribute("aria-expanded") !== headerExpanded)
|
|
3070
3177
|
headerEl.setAttribute("aria-expanded", headerExpanded);
|
|
3178
|
+
// 历史分页只由底部按钮显式触发;追加只排队一帧,避免重复点击造成重复增长
|
|
3179
|
+
//(R-01-019/AC-01~AC-03)。
|
|
3071
3180
|
|
|
3072
3181
|
// 渲染签名在整轮 DOM 写入全部成功后提交:任何一步失败都保留下一轮
|
|
3073
3182
|
// 同步重试的机会,避免故障被签名吞掉后卡片永久滞留(R-01-013/AC-02)。
|
|
@@ -3252,6 +3361,7 @@ function apply(ctx) {
|
|
|
3252
3361
|
window.removeEventListener("pageshow", onPageShow);
|
|
3253
3362
|
completeAcksById.clear();
|
|
3254
3363
|
if (clockTimer !== null) clearInterval(clockTimer);
|
|
3364
|
+
if (recentTimeTimer !== null) clearInterval(recentTimeTimer);
|
|
3255
3365
|
if (e2eListReleaseTimer !== null) clearTimeout(e2eListReleaseTimer);
|
|
3256
3366
|
for (const [timer, resolve] of e2eModelDelayWaiters) {
|
|
3257
3367
|
clearTimeout(timer);
|
package/src/core.mjs
CHANGED
|
@@ -1594,11 +1594,6 @@ export function listLoadState(snapshot) {
|
|
|
1594
1594
|
return "ready";
|
|
1595
1595
|
}
|
|
1596
1596
|
|
|
1597
|
-
/** 历史窗口:会话最后一次活动距现在不超过该毫秒数则视为"最近使用过"。 */
|
|
1598
|
-
export const HISTORY_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
1599
|
-
/** 历史区最多展示的最近会话条数。 */
|
|
1600
|
-
export const HISTORY_MAX = 20;
|
|
1601
|
-
|
|
1602
1597
|
/** 会话行是否为某主会话的直属子代理。 */
|
|
1603
1598
|
export function isSubagentRow(row, byId = {}) {
|
|
1604
1599
|
const id = row?.parentId;
|
|
@@ -1702,7 +1697,7 @@ function entryErrorNote(completion) {
|
|
|
1702
1697
|
|
|
1703
1698
|
/**
|
|
1704
1699
|
* 活动区→历史区迁移检测(R-01-010/AC-07):上一帧活动区 id 在本帧离开活动区且出现于
|
|
1705
|
-
*
|
|
1700
|
+
* 历史区即判定为一次迁移;彻底消失(归档、不再被会话服务列出)不判定。prevActiveIds 为上一帧
|
|
1706
1701
|
* 已渲染的活动区 id 集合,active/recent 为本帧派生条目。
|
|
1707
1702
|
*/
|
|
1708
1703
|
export function movedToRecentIds(prevActiveIds, active, recent) {
|
|
@@ -1718,7 +1713,7 @@ export function movedToRecentIds(prevActiveIds, active, recent) {
|
|
|
1718
1713
|
|
|
1719
1714
|
/**
|
|
1720
1715
|
* 历史区→活动区迁移检测(R-01-010/AC-07):上一帧历史区 id 在本帧离开历史区且出现于
|
|
1721
|
-
*
|
|
1716
|
+
* 活动区即判定为一次反向迁移;彻底消失(归档、不再被会话服务列出)不判定。prevRecentIds 为上一帧
|
|
1722
1717
|
* 已渲染的历史区 id 集合,active/recent 为本帧派生条目。与 movedToRecentIds 镜像对称。
|
|
1723
1718
|
*/
|
|
1724
1719
|
export function movedToActiveIds(prevRecentIds, active, recent) {
|
|
@@ -1818,16 +1813,12 @@ export function lastTurnDuration({ turnTimings = null, history = [] } = {}) {
|
|
|
1818
1813
|
}
|
|
1819
1814
|
|
|
1820
1815
|
/**
|
|
1821
|
-
*
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1825
|
-
* 一张点了回落到新会话界面的死卡。完成确认中的会话留在活动区,不入历史区;
|
|
1826
|
-
* 委托周期(含耗尽空窗)中的会话同样留在活动区(delegatingIds,分区不变量)。
|
|
1827
|
-
* 窗口候选判定用宿主列表时间(下界);turnEnds(id → 已知回合结束时刻)驱动
|
|
1828
|
-
* 时间精化:条目 activityAt 取宿主列表时间与回合结束时刻的较新者(R-01-010/AC-08、AC-09)。
|
|
1816
|
+
* 构建历史区条目:当前非活动的**主会话**,按最后活动时间从新到旧返回完整候选集合。
|
|
1817
|
+
* 子代理是临时工作单元,不入历史区;归档、空会话、完成/错误提醒与委托周期中的会话
|
|
1818
|
+
* 也不入历史区。历史区不再按时间窗口或条数截断;turnEnds(id → 已知回合结束时刻)驱动
|
|
1819
|
+
* activityAt 精化(R-01-010、R-01-019)。
|
|
1829
1820
|
*/
|
|
1830
|
-
export function buildRecent(snapshot, workspaceItems, now,
|
|
1821
|
+
export function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedIds = [], completions = null, delegatingIds = null, turnEnds = null) {
|
|
1831
1822
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1832
1823
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1833
1824
|
const current = snapshot?.current ?? null;
|
|
@@ -1848,7 +1839,7 @@ export function buildRecent(snapshot, workspaceItems, now, windowMs = HISTORY_WI
|
|
|
1848
1839
|
if (isActiveRow(row, byId, activeIds)) continue;
|
|
1849
1840
|
const updatedAt = Number(row.updatedAt);
|
|
1850
1841
|
if (!Number.isFinite(updatedAt)) continue;
|
|
1851
|
-
if (
|
|
1842
|
+
if (Number.isFinite(now) && updatedAt > now) continue;
|
|
1852
1843
|
const turnEnd = Number(mapValue(turnEnds, id));
|
|
1853
1844
|
const activityAt = Number.isFinite(turnEnd) ? Math.max(updatedAt, turnEnd) : updatedAt;
|
|
1854
1845
|
const details = mapValue(detailsById, id) ?? {};
|
|
@@ -1872,8 +1863,14 @@ export function buildRecent(snapshot, workspaceItems, now, windowMs = HISTORY_WI
|
|
|
1872
1863
|
});
|
|
1873
1864
|
}
|
|
1874
1865
|
|
|
1875
|
-
entries.sort((a, b) =>
|
|
1876
|
-
|
|
1866
|
+
entries.sort((a, b) => {
|
|
1867
|
+
const byActivity = b.activityAt - a.activityAt;
|
|
1868
|
+
if (byActivity !== 0) return byActivity;
|
|
1869
|
+
const aId = String(a.id);
|
|
1870
|
+
const bId = String(b.id);
|
|
1871
|
+
return aId < bId ? -1 : aId > bId ? 1 : 0;
|
|
1872
|
+
});
|
|
1873
|
+
return entries;
|
|
1877
1874
|
}
|
|
1878
1875
|
|
|
1879
1876
|
/** 毫秒时长的人性化短格式,例如 "47s"、"3m12s"。 */
|
|
@@ -1884,6 +1881,41 @@ export function fmtElapsedMs(ms) {
|
|
|
1884
1881
|
return `${Math.floor(s / 60)}m${s % 60}s`;
|
|
1885
1882
|
}
|
|
1886
1883
|
|
|
1884
|
+
/** 历史卡相对活动时间:按分钟、小时、天、周、月、年分级;负值返回空。 */
|
|
1885
|
+
export function fmtRelativeAge(ageMs) {
|
|
1886
|
+
if (!Number.isFinite(ageMs) || ageMs < 0) return "";
|
|
1887
|
+
const minute = 60_000;
|
|
1888
|
+
const hour = 60 * minute;
|
|
1889
|
+
const day = 24 * hour;
|
|
1890
|
+
if (ageMs < minute) return "刚刚";
|
|
1891
|
+
if (ageMs < hour) return `${Math.floor(ageMs / minute)}分钟前`;
|
|
1892
|
+
if (ageMs < day) return `${Math.floor(ageMs / hour)}小时前`;
|
|
1893
|
+
if (ageMs < 7 * day) return `${Math.floor(ageMs / day)}天前`;
|
|
1894
|
+
if (ageMs < 30 * day) return `${Math.floor(ageMs / (7 * day))}周前`;
|
|
1895
|
+
if (ageMs < 365 * day) return `${Math.floor(ageMs / (30 * day))}个月前`;
|
|
1896
|
+
return `${Math.floor(ageMs / (365 * day))}年前`;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/** 历史卡绝对活动时间:使用浏览器/宿主本地时区,当前年份省略年份,跨年补年份。 */
|
|
1900
|
+
export function fmtAbsoluteDateTime(ts, now = Date.now()) {
|
|
1901
|
+
if (ts === null || ts === undefined || now === null || now === undefined) return "";
|
|
1902
|
+
try {
|
|
1903
|
+
const value = Number(ts);
|
|
1904
|
+
const current = Number(now);
|
|
1905
|
+
const date = new Date(value);
|
|
1906
|
+
const today = new Date(current);
|
|
1907
|
+
if (!Number.isFinite(value) || !Number.isFinite(current) || !Number.isFinite(date.getTime()) || !Number.isFinite(today.getTime())) return "";
|
|
1908
|
+
const dateOptions = { month: "2-digit", day: "2-digit" };
|
|
1909
|
+
if (date.getFullYear() !== today.getFullYear()) dateOptions.year = "numeric";
|
|
1910
|
+
return `${date.toLocaleDateString([], dateOptions)} ${date.toLocaleTimeString([], {
|
|
1911
|
+
hour: "2-digit",
|
|
1912
|
+
minute: "2-digit",
|
|
1913
|
+
})}`;
|
|
1914
|
+
} catch {
|
|
1915
|
+
return "";
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1887
1919
|
/** token 计数的人性化短格式,例如 "847"、"1.2k";非有限非负时返回 null。 */
|
|
1888
1920
|
/** token 计数紧凑缩写,镜像原生统计行 formatTokens:847 / 12.2K / 517K / 2.8M——
|
|
1889
1921
|
* 千以下原样;K/M 档缩写值百位以上取整、不足百位保留一位小数;非法输入返回 null 不展示。 */
|