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/.dsh-plugin/client.js
CHANGED
|
@@ -1600,11 +1600,6 @@ function listLoadState(snapshot) {
|
|
|
1600
1600
|
return "ready";
|
|
1601
1601
|
}
|
|
1602
1602
|
|
|
1603
|
-
/** 历史窗口:会话最后一次活动距现在不超过该毫秒数则视为"最近使用过"。 */
|
|
1604
|
-
const HISTORY_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
1605
|
-
/** 历史区最多展示的最近会话条数。 */
|
|
1606
|
-
const HISTORY_MAX = 20;
|
|
1607
|
-
|
|
1608
1603
|
/** 会话行是否为某主会话的直属子代理。 */
|
|
1609
1604
|
function isSubagentRow(row, byId = {}) {
|
|
1610
1605
|
const id = row?.parentId;
|
|
@@ -1708,7 +1703,7 @@ function entryErrorNote(completion) {
|
|
|
1708
1703
|
|
|
1709
1704
|
/**
|
|
1710
1705
|
* 活动区→历史区迁移检测(R-01-010/AC-07):上一帧活动区 id 在本帧离开活动区且出现于
|
|
1711
|
-
*
|
|
1706
|
+
* 历史区即判定为一次迁移;彻底消失(归档、不再被会话服务列出)不判定。prevActiveIds 为上一帧
|
|
1712
1707
|
* 已渲染的活动区 id 集合,active/recent 为本帧派生条目。
|
|
1713
1708
|
*/
|
|
1714
1709
|
function movedToRecentIds(prevActiveIds, active, recent) {
|
|
@@ -1724,7 +1719,7 @@ function movedToRecentIds(prevActiveIds, active, recent) {
|
|
|
1724
1719
|
|
|
1725
1720
|
/**
|
|
1726
1721
|
* 历史区→活动区迁移检测(R-01-010/AC-07):上一帧历史区 id 在本帧离开历史区且出现于
|
|
1727
|
-
*
|
|
1722
|
+
* 活动区即判定为一次反向迁移;彻底消失(归档、不再被会话服务列出)不判定。prevRecentIds 为上一帧
|
|
1728
1723
|
* 已渲染的历史区 id 集合,active/recent 为本帧派生条目。与 movedToRecentIds 镜像对称。
|
|
1729
1724
|
*/
|
|
1730
1725
|
function movedToActiveIds(prevRecentIds, active, recent) {
|
|
@@ -1824,16 +1819,12 @@ function lastTurnDuration({ turnTimings = null, history = [] } = {}) {
|
|
|
1824
1819
|
}
|
|
1825
1820
|
|
|
1826
1821
|
/**
|
|
1827
|
-
*
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
* 一张点了回落到新会话界面的死卡。完成确认中的会话留在活动区,不入历史区;
|
|
1832
|
-
* 委托周期(含耗尽空窗)中的会话同样留在活动区(delegatingIds,分区不变量)。
|
|
1833
|
-
* 窗口候选判定用宿主列表时间(下界);turnEnds(id → 已知回合结束时刻)驱动
|
|
1834
|
-
* 时间精化:条目 activityAt 取宿主列表时间与回合结束时刻的较新者(R-01-010/AC-08、AC-09)。
|
|
1822
|
+
* 构建历史区条目:当前非活动的**主会话**,按最后活动时间从新到旧返回完整候选集合。
|
|
1823
|
+
* 子代理是临时工作单元,不入历史区;归档、空会话、完成/错误提醒与委托周期中的会话
|
|
1824
|
+
* 也不入历史区。历史区不再按时间窗口或条数截断;turnEnds(id → 已知回合结束时刻)驱动
|
|
1825
|
+
* activityAt 精化(R-01-010、R-01-019)。
|
|
1835
1826
|
*/
|
|
1836
|
-
function buildRecent(snapshot, workspaceItems, now,
|
|
1827
|
+
function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedIds = [], completions = null, delegatingIds = null, turnEnds = null) {
|
|
1837
1828
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1838
1829
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1839
1830
|
const current = snapshot?.current ?? null;
|
|
@@ -1854,7 +1845,7 @@ function buildRecent(snapshot, workspaceItems, now, windowMs = HISTORY_WINDOW_MS
|
|
|
1854
1845
|
if (isActiveRow(row, byId, activeIds)) continue;
|
|
1855
1846
|
const updatedAt = Number(row.updatedAt);
|
|
1856
1847
|
if (!Number.isFinite(updatedAt)) continue;
|
|
1857
|
-
if (
|
|
1848
|
+
if (Number.isFinite(now) && updatedAt > now) continue;
|
|
1858
1849
|
const turnEnd = Number(mapValue(turnEnds, id));
|
|
1859
1850
|
const activityAt = Number.isFinite(turnEnd) ? Math.max(updatedAt, turnEnd) : updatedAt;
|
|
1860
1851
|
const details = mapValue(detailsById, id) ?? {};
|
|
@@ -1878,8 +1869,14 @@ function buildRecent(snapshot, workspaceItems, now, windowMs = HISTORY_WINDOW_MS
|
|
|
1878
1869
|
});
|
|
1879
1870
|
}
|
|
1880
1871
|
|
|
1881
|
-
entries.sort((a, b) =>
|
|
1882
|
-
|
|
1872
|
+
entries.sort((a, b) => {
|
|
1873
|
+
const byActivity = b.activityAt - a.activityAt;
|
|
1874
|
+
if (byActivity !== 0) return byActivity;
|
|
1875
|
+
const aId = String(a.id);
|
|
1876
|
+
const bId = String(b.id);
|
|
1877
|
+
return aId < bId ? -1 : aId > bId ? 1 : 0;
|
|
1878
|
+
});
|
|
1879
|
+
return entries;
|
|
1883
1880
|
}
|
|
1884
1881
|
|
|
1885
1882
|
/** 毫秒时长的人性化短格式,例如 "47s"、"3m12s"。 */
|
|
@@ -1890,6 +1887,41 @@ function fmtElapsedMs(ms) {
|
|
|
1890
1887
|
return `${Math.floor(s / 60)}m${s % 60}s`;
|
|
1891
1888
|
}
|
|
1892
1889
|
|
|
1890
|
+
/** 历史卡相对活动时间:按分钟、小时、天、周、月、年分级;负值返回空。 */
|
|
1891
|
+
function fmtRelativeAge(ageMs) {
|
|
1892
|
+
if (!Number.isFinite(ageMs) || ageMs < 0) return "";
|
|
1893
|
+
const minute = 60_000;
|
|
1894
|
+
const hour = 60 * minute;
|
|
1895
|
+
const day = 24 * hour;
|
|
1896
|
+
if (ageMs < minute) return "刚刚";
|
|
1897
|
+
if (ageMs < hour) return `${Math.floor(ageMs / minute)}分钟前`;
|
|
1898
|
+
if (ageMs < day) return `${Math.floor(ageMs / hour)}小时前`;
|
|
1899
|
+
if (ageMs < 7 * day) return `${Math.floor(ageMs / day)}天前`;
|
|
1900
|
+
if (ageMs < 30 * day) return `${Math.floor(ageMs / (7 * day))}周前`;
|
|
1901
|
+
if (ageMs < 365 * day) return `${Math.floor(ageMs / (30 * day))}个月前`;
|
|
1902
|
+
return `${Math.floor(ageMs / (365 * day))}年前`;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/** 历史卡绝对活动时间:使用浏览器/宿主本地时区,当前年份省略年份,跨年补年份。 */
|
|
1906
|
+
function fmtAbsoluteDateTime(ts, now = Date.now()) {
|
|
1907
|
+
if (ts === null || ts === undefined || now === null || now === undefined) return "";
|
|
1908
|
+
try {
|
|
1909
|
+
const value = Number(ts);
|
|
1910
|
+
const current = Number(now);
|
|
1911
|
+
const date = new Date(value);
|
|
1912
|
+
const today = new Date(current);
|
|
1913
|
+
if (!Number.isFinite(value) || !Number.isFinite(current) || !Number.isFinite(date.getTime()) || !Number.isFinite(today.getTime())) return "";
|
|
1914
|
+
const dateOptions = { month: "2-digit", day: "2-digit" };
|
|
1915
|
+
if (date.getFullYear() !== today.getFullYear()) dateOptions.year = "numeric";
|
|
1916
|
+
return `${date.toLocaleDateString([], dateOptions)} ${date.toLocaleTimeString([], {
|
|
1917
|
+
hour: "2-digit",
|
|
1918
|
+
minute: "2-digit",
|
|
1919
|
+
})}`;
|
|
1920
|
+
} catch {
|
|
1921
|
+
return "";
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1893
1925
|
/** token 计数的人性化短格式,例如 "847"、"1.2k";非有限非负时返回 null。 */
|
|
1894
1926
|
/** token 计数紧凑缩写,镜像原生统计行 formatTokens:847 / 12.2K / 517K / 2.8M——
|
|
1895
1927
|
* 千以下原样;K/M 档缩写值百位以上取整、不足百位保留一位小数;非法输入返回 null 不展示。 */
|
|
@@ -2128,8 +2160,12 @@ const INDENT_PX = 16;
|
|
|
2128
2160
|
const MOBILE_BREAKPOINT = "767px";
|
|
2129
2161
|
/** 运行卡时钟:只要存在运行中会话,就以该周期刷新时长显示。 */
|
|
2130
2162
|
const CLOCK_MS = 1000;
|
|
2163
|
+
/** 历史卡相对时间刷新周期;无需每秒重绘整列。 */
|
|
2164
|
+
const RECENT_TIME_REFRESH_MS = 60_000;
|
|
2131
2165
|
/** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
|
|
2132
2166
|
const LOAD_CONCURRENCY = 3;
|
|
2167
|
+
/** 历史分页每批数量;后续批次只由底部按钮显式加载。 */
|
|
2168
|
+
const RECENT_PAGE_SIZE = 10;
|
|
2133
2169
|
/** 「回到顶部」悬浮按钮显隐阈值:scrollTop 超过该值(px)时显示(R-01-018/AC-01)。 */
|
|
2134
2170
|
const TOP_THRESHOLD = 200;
|
|
2135
2171
|
|
|
@@ -2213,21 +2249,34 @@ const CSS = `
|
|
|
2213
2249
|
touch-action: pan-y;
|
|
2214
2250
|
-webkit-overflow-scrolling: touch;
|
|
2215
2251
|
padding: 0 0 10px;
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2252
|
+
/* Keep the native scrollbar just left of the edge handle, as in the host
|
|
2253
|
+
sidebar; stable keeps the list geometry unchanged when overflow appears. */
|
|
2254
|
+
margin-right: var(--dsh-scrollbar-width, 8px);
|
|
2255
|
+
scrollbar-gutter: stable;
|
|
2256
|
+
}
|
|
2257
|
+
/* 滚动条在滚动或鼠标进入窗格时显示(R-01-004/AC-03,与外壳侧栏一致):thumb 默认透明,
|
|
2258
|
+
经 data-scrolling/data-pointer-inside 显示,悬停时读取主题 hover token。Firefox 路径必须在
|
|
2259
|
+
@supports 门内——非 auto 的 scrollbar-color 会让 Chromium 丢弃该元素的 ::-webkit-scrollbar 规则。 */
|
|
2220
2260
|
[data-dsh-activity-pane] .dap-scroll::-webkit-scrollbar-thumb {
|
|
2221
2261
|
background: transparent;
|
|
2222
2262
|
}
|
|
2223
2263
|
[data-dsh-activity-pane] .dap-scroll[data-scrolling]::-webkit-scrollbar-thumb {
|
|
2224
2264
|
background: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent));
|
|
2225
2265
|
}
|
|
2266
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb {
|
|
2267
|
+
background: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent));
|
|
2268
|
+
}
|
|
2269
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb:hover {
|
|
2270
|
+
background: var(--dsh-scrollbar-thumb-hover, color-mix(in srgb, currentColor 40%, transparent));
|
|
2271
|
+
}
|
|
2226
2272
|
@supports not selector(::-webkit-scrollbar) {
|
|
2227
2273
|
[data-dsh-activity-pane] .dap-scroll { scrollbar-color: transparent transparent; }
|
|
2228
2274
|
[data-dsh-activity-pane] .dap-scroll[data-scrolling] {
|
|
2229
2275
|
scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
|
|
2230
2276
|
}
|
|
2277
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll {
|
|
2278
|
+
scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
|
|
2279
|
+
}
|
|
2231
2280
|
}
|
|
2232
2281
|
/* 「回到顶部」悬浮图标按钮(R-01-018):窗格内右下角圆形按钮,纯图标无文字、不透明底色;
|
|
2233
2282
|
默认 hidden,scrollTop 超阈值时由滚动监听揭隐。基类 display:flex 会压过 UA 的
|
|
@@ -2284,6 +2333,29 @@ const CSS = `
|
|
|
2284
2333
|
color: color-mix(in srgb, currentColor 52%, transparent);
|
|
2285
2334
|
text-transform: uppercase;
|
|
2286
2335
|
}
|
|
2336
|
+
/* 历史分页只在用户明确激活底部按钮后追加(R-01-019/AC-02)。 */
|
|
2337
|
+
[data-dsh-activity-pane] .dap-recent-more {
|
|
2338
|
+
align-self: center;
|
|
2339
|
+
flex: none;
|
|
2340
|
+
min-width: 132px;
|
|
2341
|
+
padding: 5px 10px;
|
|
2342
|
+
border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
|
|
2343
|
+
border-radius: 6px;
|
|
2344
|
+
background: color-mix(in srgb, currentColor 7%, transparent);
|
|
2345
|
+
color: color-mix(in srgb, currentColor 70%, transparent);
|
|
2346
|
+
font: inherit;
|
|
2347
|
+
font-size: 11px;
|
|
2348
|
+
line-height: 16px;
|
|
2349
|
+
cursor: pointer;
|
|
2350
|
+
}
|
|
2351
|
+
[data-dsh-activity-pane] .dap-recent-more:hover,
|
|
2352
|
+
[data-dsh-activity-pane] .dap-recent-more:focus-visible {
|
|
2353
|
+
background: color-mix(in srgb, currentColor 13%, transparent);
|
|
2354
|
+
color: inherit;
|
|
2355
|
+
}
|
|
2356
|
+
[data-dsh-activity-pane] .dap-recent-more:focus-visible { outline: 2px solid color-mix(in srgb, currentColor 45%, transparent); outline-offset: 1px; }
|
|
2357
|
+
[data-dsh-activity-pane] .dap-recent-more[disabled] { cursor: wait; opacity: 0.6; }
|
|
2358
|
+
[data-dsh-activity-pane] .dap-recent-more[hidden] { display: none; }
|
|
2287
2359
|
/* 折叠:仅桌面生效;窄条 + 竖排标题与计数(R-01-011/AC-04)。 */
|
|
2288
2360
|
[data-dsh-activity-pane] .dap-rail {
|
|
2289
2361
|
display: none;
|
|
@@ -2322,7 +2394,8 @@ const CSS = `
|
|
|
2322
2394
|
background: rgba(46, 42, 26, 0.97);
|
|
2323
2395
|
animation: dap-await-pulse 1.2s ease-in-out infinite;
|
|
2324
2396
|
}
|
|
2325
|
-
/* 桌面拖拽调宽手柄(R-01-015):右缘 6px
|
|
2397
|
+
/* 桌面拖拽调宽手柄(R-01-015):右缘 6px 透明命中区,拖拽实时写入 --dap-width;
|
|
2398
|
+
滚动区在其左侧留出 native scrollbar 命中带,手柄只改变光标、不绘制 hover 高亮;
|
|
2326
2399
|
折叠窄条与移动端抽屉不提供拖拽(下方两处媒体查询隐藏)。 */
|
|
2327
2400
|
[data-dsh-activity-pane] .dap-resize {
|
|
2328
2401
|
position: absolute;
|
|
@@ -2332,10 +2405,6 @@ const CSS = `
|
|
|
2332
2405
|
touch-action: none;
|
|
2333
2406
|
z-index: 6;
|
|
2334
2407
|
}
|
|
2335
|
-
[data-dsh-activity-pane] .dap-resize:hover,
|
|
2336
|
-
[data-dsh-activity-pane] .dap-resize[data-dragging] {
|
|
2337
|
-
background: color-mix(in srgb, currentColor 18%, transparent);
|
|
2338
|
-
}
|
|
2339
2408
|
/* 桌面:窗格作为中间列内的真实 flex 行元素参与布局——中间列被置为行方向,
|
|
2340
2409
|
窗格固定宽、会话区弹性收缩,整个会话内容被真实挤到右边;折叠时收窄为窄条。 */
|
|
2341
2410
|
@media (min-width: 768px) {
|
|
@@ -3041,12 +3110,15 @@ function livenessFromSnapshot(snap) {
|
|
|
3041
3110
|
return { startTime };
|
|
3042
3111
|
}
|
|
3043
3112
|
|
|
3044
|
-
|
|
3113
|
+
/** 历史卡时间同时给出本地绝对日期时分与相对年龄;跨年份的绝对日期带年份。 */
|
|
3114
|
+
function fmtRecentTime(ts, now = Date.now()) {
|
|
3045
3115
|
try {
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3116
|
+
const value = Number(ts);
|
|
3117
|
+
const current = Number(now);
|
|
3118
|
+
const absolute = fmtAbsoluteDateTime(ts, now);
|
|
3119
|
+
if (!absolute) return "";
|
|
3120
|
+
const relative = fmtRelativeAge(current - value);
|
|
3121
|
+
return relative ? `最后活动 · ${absolute} · ${relative}` : `最后活动 · ${absolute}`;
|
|
3050
3122
|
} catch {
|
|
3051
3123
|
return "";
|
|
3052
3124
|
}
|
|
@@ -3080,6 +3152,7 @@ function apply(ctx) {
|
|
|
3080
3152
|
let sessionUnsubscribe = null;
|
|
3081
3153
|
let workspaceUnsubscribe = null;
|
|
3082
3154
|
let clockTimer = null;
|
|
3155
|
+
let recentTimeTimer = null;
|
|
3083
3156
|
let syncScheduled = false;
|
|
3084
3157
|
let lastSig = "";
|
|
3085
3158
|
/** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
|
|
@@ -3132,6 +3205,11 @@ function apply(ctx) {
|
|
|
3132
3205
|
const cardsById = new Map();
|
|
3133
3206
|
/** 历史区卡片 id → { el } 复用表。 */
|
|
3134
3207
|
const recentCardsById = new Map();
|
|
3208
|
+
/** 历史区渐进呈现状态:候选由核心完整派生,DOM 与详情读取只覆盖当前前缀。 */
|
|
3209
|
+
let recentVisibleCount = RECENT_PAGE_SIZE;
|
|
3210
|
+
let recentTotal = 0;
|
|
3211
|
+
let recentHasMore = false;
|
|
3212
|
+
let recentAppendQueued = false;
|
|
3135
3213
|
/** 运行中会话原生快照订阅:id → { unsubscribe, liveness, snapshot }。 */
|
|
3136
3214
|
const livenessById = new Map();
|
|
3137
3215
|
/** 委托周期进度锚点记账:id → progressAnchor 状态(R-01-009/AC-06)。 */
|
|
@@ -3545,12 +3623,25 @@ function apply(ctx) {
|
|
|
3545
3623
|
window.dispatchEvent(new Event("resize"));
|
|
3546
3624
|
} catch {}
|
|
3547
3625
|
}
|
|
3626
|
+
function loadMoreRecent() {
|
|
3627
|
+
if (disposed || !recentHasMore || recentAppendQueued) return false;
|
|
3628
|
+
const nextCount = Math.min(recentVisibleCount + RECENT_PAGE_SIZE, recentTotal);
|
|
3629
|
+
if (nextCount <= recentVisibleCount) return false;
|
|
3630
|
+
recentAppendQueued = true;
|
|
3631
|
+
recentVisibleCount = nextCount;
|
|
3632
|
+
queueSync();
|
|
3633
|
+
return true;
|
|
3634
|
+
}
|
|
3548
3635
|
function bindPaneControls(pane) {
|
|
3549
3636
|
const header = pane.querySelector(".dap-header");
|
|
3550
3637
|
const rail = pane.querySelector(".dap-rail");
|
|
3551
3638
|
const resize = pane.querySelector(".dap-resize");
|
|
3552
3639
|
const scroll = pane.querySelector(".dap-scroll");
|
|
3553
3640
|
const topBtn = pane.querySelector(".dap-top");
|
|
3641
|
+
const recentMore = pane.querySelector(".dap-recent-more");
|
|
3642
|
+
const onRecentMoreClick = () => {
|
|
3643
|
+
if (loadMoreRecent() && recentMore !== null) recentMore.disabled = true;
|
|
3644
|
+
};
|
|
3554
3645
|
// 标题行整体即收起控件,两端断点一致:桌面折叠为窄条(R-01-011/AC-03);
|
|
3555
3646
|
// 移动端断点解释为收起抽屉,而非折叠窄条(R-01-008/AC-02、R-01-011/AC-06)。
|
|
3556
3647
|
const onHeaderActivate = () => {
|
|
@@ -3573,6 +3664,15 @@ function apply(ctx) {
|
|
|
3573
3664
|
const syncTopBtn = () => {
|
|
3574
3665
|
if (topBtn !== null && scroll !== null) topBtn.hidden = scroll.scrollTop <= TOP_THRESHOLD;
|
|
3575
3666
|
};
|
|
3667
|
+
// 鼠标进入窗格即显示 scrollbar(R-01-004/AC-03);触摸指针不改变桌面滚动条可见状态。
|
|
3668
|
+
const onPanePointerEnter = (event) => {
|
|
3669
|
+
if (event.pointerType !== "mouse") return;
|
|
3670
|
+
pane.setAttribute("data-pointer-inside", "");
|
|
3671
|
+
};
|
|
3672
|
+
const onPanePointerLeave = (event) => {
|
|
3673
|
+
if (event.pointerType !== "mouse") return;
|
|
3674
|
+
pane.removeAttribute("data-pointer-inside");
|
|
3675
|
+
};
|
|
3576
3676
|
const onRailClick = () => {
|
|
3577
3677
|
collapsed = false;
|
|
3578
3678
|
pane.setAttribute("data-collapsed", "false");
|
|
@@ -3615,8 +3715,9 @@ function apply(ctx) {
|
|
|
3615
3715
|
resize.addEventListener("pointercancel", onResizeUp);
|
|
3616
3716
|
resize.setPointerCapture(event.pointerId);
|
|
3617
3717
|
};
|
|
3618
|
-
//
|
|
3619
|
-
//
|
|
3718
|
+
// 滚动条随滚动或窗格内鼠标指针显示(R-01-004/AC-03):滚动即置位,停滚 600ms 后收口;
|
|
3719
|
+
// 指针离开后若仍在滚动拖尾内则保持显示,拖尾结束后隐藏。同一监听承载「回到顶部」按钮显隐
|
|
3720
|
+
//(R-01-018/AC-01、AC-03):回顶后的收口不经额外事件,
|
|
3620
3721
|
// 由平滑滚动触发的 scroll 事件自然完成。
|
|
3621
3722
|
let scrollHideTimer = null;
|
|
3622
3723
|
const onScroll = () => {
|
|
@@ -3637,14 +3738,21 @@ function apply(ctx) {
|
|
|
3637
3738
|
header?.addEventListener("click", onHeaderActivate);
|
|
3638
3739
|
header?.addEventListener("keydown", onHeaderKeydown);
|
|
3639
3740
|
rail?.addEventListener("click", onRailClick);
|
|
3741
|
+
pane.addEventListener("pointerenter", onPanePointerEnter);
|
|
3742
|
+
pane.addEventListener("pointerleave", onPanePointerLeave);
|
|
3640
3743
|
scroll?.addEventListener("scroll", onScroll, { passive: true });
|
|
3744
|
+
recentMore?.addEventListener("click", onRecentMoreClick);
|
|
3641
3745
|
topBtn?.addEventListener("click", onTopClick);
|
|
3642
3746
|
resize?.addEventListener("pointerdown", onResizeDown);
|
|
3643
3747
|
return () => {
|
|
3644
3748
|
header?.removeEventListener("click", onHeaderActivate);
|
|
3645
3749
|
header?.removeEventListener("keydown", onHeaderKeydown);
|
|
3646
3750
|
rail?.removeEventListener("click", onRailClick);
|
|
3751
|
+
pane.removeEventListener("pointerenter", onPanePointerEnter);
|
|
3752
|
+
pane.removeEventListener("pointerleave", onPanePointerLeave);
|
|
3753
|
+
pane.removeAttribute("data-pointer-inside");
|
|
3647
3754
|
scroll?.removeEventListener("scroll", onScroll);
|
|
3755
|
+
recentMore?.removeEventListener("click", onRecentMoreClick);
|
|
3648
3756
|
if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);
|
|
3649
3757
|
topBtn?.removeEventListener("click", onTopClick);
|
|
3650
3758
|
resize?.removeEventListener("pointerdown", onResizeDown);
|
|
@@ -3674,7 +3782,8 @@ function apply(ctx) {
|
|
|
3674
3782
|
<div class="dap-scroll">
|
|
3675
3783
|
<div class="dap-list" tabindex="-1"><div class="dap-tracks" aria-hidden="true"></div></div>
|
|
3676
3784
|
<div class="dap-recent">
|
|
3677
|
-
<div class="dap-recent-head"><span
|
|
3785
|
+
<div class="dap-recent-head"><span>最近历史</span></div>
|
|
3786
|
+
<button class="dap-recent-more" type="button" hidden>加载更多...</button>
|
|
3678
3787
|
</div>
|
|
3679
3788
|
</div>
|
|
3680
3789
|
<button class="dap-top" type="button" aria-label="回到顶部" title="回到顶部" hidden></button>
|
|
@@ -4454,6 +4563,16 @@ function apply(ctx) {
|
|
|
4454
4563
|
}
|
|
4455
4564
|
}
|
|
4456
4565
|
|
|
4566
|
+
/** 历史卡相对时间只需分钟级刷新;无历史卡时停止定时器,避免空窗格常驻唤醒。 */
|
|
4567
|
+
function syncRecentTimeClock(wanted) {
|
|
4568
|
+
if (wanted && recentTimeTimer === null) {
|
|
4569
|
+
recentTimeTimer = setInterval(() => queueSync(), RECENT_TIME_REFRESH_MS);
|
|
4570
|
+
} else if (!wanted && recentTimeTimer !== null) {
|
|
4571
|
+
clearInterval(recentTimeTimer);
|
|
4572
|
+
recentTimeTimer = null;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4457
4576
|
function getSessionSnapshot(session) {
|
|
4458
4577
|
try {
|
|
4459
4578
|
return session?.getSnapshot?.() ?? null;
|
|
@@ -4662,11 +4781,14 @@ function apply(ctx) {
|
|
|
4662
4781
|
if (entry.waitClass === "blocked" || entry.waitClass === "done" || entry.waitClass === "error")
|
|
4663
4782
|
rec.el.setAttribute("data-wait", entry.waitClass);
|
|
4664
4783
|
else rec.el.removeAttribute("data-wait");
|
|
4784
|
+
const recentTimeText = entry.kind === "recent" ? fmtRecentTime(entry.activityAt) : "";
|
|
4665
4785
|
rec.el.setAttribute(
|
|
4666
4786
|
"aria-label",
|
|
4667
4787
|
`${entry.workspaceTitle ? entry.workspaceTitle + " - " : ""}${entry.title}${
|
|
4668
4788
|
entry.pendingText ? "," + entry.pendingText : ""
|
|
4669
|
-
}${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}
|
|
4789
|
+
}${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}${
|
|
4790
|
+
recentTimeText ? "," + recentTimeText : ""
|
|
4791
|
+
}`,
|
|
4670
4792
|
);
|
|
4671
4793
|
renderCardInto(rec.el, entry, hueByWorkspace);
|
|
4672
4794
|
// 只有顺序/归属真正变化时才移动 DOM:每次渲染无条件 appendChild 会把所有
|
|
@@ -4857,6 +4979,10 @@ function apply(ctx) {
|
|
|
4857
4979
|
pulseSignature = "";
|
|
4858
4980
|
prevRenderedActiveIds = new Set();
|
|
4859
4981
|
prevRenderedRecentIds = new Set();
|
|
4982
|
+
recentVisibleCount = RECENT_PAGE_SIZE;
|
|
4983
|
+
recentTotal = 0;
|
|
4984
|
+
recentHasMore = false;
|
|
4985
|
+
recentAppendQueued = false;
|
|
4860
4986
|
// 旧窗格已脱离文档:其在飞平移的 transitionend 不再触发,逐元素取消避免残留。
|
|
4861
4987
|
for (const el of [...shiftCleanups.keys()]) cancelShift(el);
|
|
4862
4988
|
}
|
|
@@ -4876,6 +5002,8 @@ function apply(ctx) {
|
|
|
4876
5002
|
}
|
|
4877
5003
|
}
|
|
4878
5004
|
const listState = listLoadState(snapshot);
|
|
5005
|
+
// 只消费上一轮追加请求;列表短暂 pending 时保留已展开页,ready 后继续从同一前缀呈现。
|
|
5006
|
+
recentAppendQueued = false;
|
|
4879
5007
|
const workspaceSnapshot = getSnapshot(workspaces, "list");
|
|
4880
5008
|
const workspaceItems = workspaceSnapshot?.items ?? [];
|
|
4881
5009
|
const archivedSessionIds = workspaceSnapshot?.archivedSessionIds ?? [];
|
|
@@ -5014,8 +5142,12 @@ function apply(ctx) {
|
|
|
5014
5142
|
}
|
|
5015
5143
|
if (detail.memoTurnEnd != null) turnEnds[id] = detail.memoTurnEnd;
|
|
5016
5144
|
}
|
|
5017
|
-
const
|
|
5018
|
-
|
|
5145
|
+
const recentCandidates = buildRecent(snapshot, workspaceItems, now, sessionDetailsById, archivedSessionIds, completeAcksById, delegatingIds, turnEnds);
|
|
5146
|
+
recentTotal = recentCandidates.length;
|
|
5147
|
+
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
5148
|
+
recentHasMore = recent.length < recentTotal;
|
|
5149
|
+
syncRecentTimeClock(recent.length > 0);
|
|
5150
|
+
// 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
|
|
5019
5151
|
// 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
|
|
5020
5152
|
const previewFallbackIds = new Set();
|
|
5021
5153
|
for (const entry of recent) {
|
|
@@ -5033,7 +5165,7 @@ function apply(ctx) {
|
|
|
5033
5165
|
if (!entry.userPreview || !entry.agentPreview) previewFallbackIds.add(entry.id);
|
|
5034
5166
|
entry.loadingPreviews = (!entry.userPreview || !entry.agentPreview) && historyLoads.has(entry.id);
|
|
5035
5167
|
}
|
|
5036
|
-
//
|
|
5168
|
+
// 补充数据读取优先级:当前会话最优先,活动区先于当前已显示历史页(区内按显示顺序)。
|
|
5037
5169
|
const durationFallbackIds = new Set(active.filter((entry) => entry.kind === "awaiting").map((entry) => entry.id));
|
|
5038
5170
|
const detailIds = [...active, ...recent].map((entry) => entry.id);
|
|
5039
5171
|
detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
|
|
@@ -5055,7 +5187,9 @@ function apply(ctx) {
|
|
|
5055
5187
|
// listState 参与签名:空列表从 pending/error → ready 时卡集合不变,若只比较卡片
|
|
5056
5188
|
// 会被提前返回冻结在「加载中」/「列表加载失败」;数量胶囊可见面变化同样需要
|
|
5057
5189
|
// 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
|
|
5058
|
-
|
|
5190
|
+
// 历史卡的相对活动时间随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
|
|
5191
|
+
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
5192
|
+
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);
|
|
5059
5193
|
if (sig === lastSig) return;
|
|
5060
5194
|
const hueByWorkspace = resolveWorkspaceHues(visibleEntries.map((entry) => entry.workspaceKey));
|
|
5061
5195
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
|
@@ -5104,6 +5238,11 @@ function apply(ctx) {
|
|
|
5104
5238
|
if (recentSection !== null) {
|
|
5105
5239
|
recentSection.hidden = listState === "ready" && recent.length === 0;
|
|
5106
5240
|
ensureListStatus(recentSection, listState !== "ready" && recent.length === 0, listState === "loading" ? "加载中…" : "列表加载失败", { loading: listState === "loading" });
|
|
5241
|
+
const recentMore = recentSection.querySelector(".dap-recent-more");
|
|
5242
|
+
if (recentMore !== null) {
|
|
5243
|
+
recentMore.hidden = !recentHasMore;
|
|
5244
|
+
recentMore.disabled = recentAppendQueued;
|
|
5245
|
+
}
|
|
5107
5246
|
}
|
|
5108
5247
|
runMoveGhosts(movePlans);
|
|
5109
5248
|
if (shiftRects !== null) runShiftAnimations(shiftRects);
|
|
@@ -5164,6 +5303,8 @@ function apply(ctx) {
|
|
|
5164
5303
|
const headerExpanded = collapsed ? "false" : "true";
|
|
5165
5304
|
if (headerEl !== null && headerEl.getAttribute("aria-expanded") !== headerExpanded)
|
|
5166
5305
|
headerEl.setAttribute("aria-expanded", headerExpanded);
|
|
5306
|
+
// 历史分页只由底部按钮显式触发;追加只排队一帧,避免重复点击造成重复增长
|
|
5307
|
+
//(R-01-019/AC-01~AC-03)。
|
|
5167
5308
|
|
|
5168
5309
|
// 渲染签名在整轮 DOM 写入全部成功后提交:任何一步失败都保留下一轮
|
|
5169
5310
|
// 同步重试的机会,避免故障被签名吞掉后卡片永久滞留(R-01-013/AC-02)。
|
|
@@ -5348,6 +5489,7 @@ function apply(ctx) {
|
|
|
5348
5489
|
window.removeEventListener("pageshow", onPageShow);
|
|
5349
5490
|
completeAcksById.clear();
|
|
5350
5491
|
if (clockTimer !== null) clearInterval(clockTimer);
|
|
5492
|
+
if (recentTimeTimer !== null) clearInterval(recentTimeTimer);
|
|
5351
5493
|
if (e2eListReleaseTimer !== null) clearTimeout(e2eListReleaseTimer);
|
|
5352
5494
|
for (const [timer, resolve] of e2eModelDelayWaiters) {
|
|
5353
5495
|
clearTimeout(timer);
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ The npm package ships prebuilt, so no local build step is needed. If the pane do
|
|
|
48
48
|
- [x] **From floating overlay to docked pane**: on desktop, a persistent edge-docked column is added to the right of the left-sidebar workspaces; on mobile, a fixed drawer hidden by default is expanded via the "Activity" button in the session header, without squeezing the main conversation layout.
|
|
49
49
|
- [x] **No pet icon features**: pet-related features are not supported; the UI focuses on session activity itself.
|
|
50
50
|
- [x] **Native data-source subscription**: directly subscribes to the push snapshots of DSH's native `sessions` / `workspaces` services; the timeline shows at most 4 collapsed work-item rows, keeping the latest user instruction and the work item actually being executed.
|
|
51
|
-
- [x] **Recent session list**: the pane is split into "Active sessions" and "Recent history" areas; main sessions
|
|
51
|
+
- [x] **Recent session list**: the pane is split into "Active sessions" and "Recent history" areas; inactive main sessions are shown in activity-time batches, and a "Load more..." button at the bottom lets users explicitly reveal older sessions.
|
|
52
52
|
- [x] **Stronger waiting-for-action reminders**: blocked waits, completion reminders, and error reminders are marked with gold, green, and red cards respectively; questions are previewed directly as a question list, and completion reminders are explicitly acknowledged via the "Move to history" button on the card; the state is persisted on the host side and synced across all clients, so refreshing the page or opening another window never loses unacknowledged completion reminders or error reminders not yet overwritten by a new round.
|
|
53
53
|
- [x] **Sub/grandchild session hierarchy**: sub-agents are nested under their parent session with connector lines and compact cards; a parent whose own round has ended but that still has active descendants keeps rendering as running, and disappears from the active area once its sub-agents have ended and no active descendants remain; the history area keeps main sessions only.
|
|
54
54
|
- [x] **Workspace names displayed and factored into ordering**: session cards show a workspace badge with a stable color, and session ordering follows the workspace order in the left sidebar.
|
package/README.zh-CN.md
CHANGED
|
@@ -48,7 +48,7 @@ npm 包内置预构建产物,无需本地构建步骤。安装后如窗格未
|
|
|
48
48
|
- [x] **从浮层改为固定窗格**:桌面端在左边栏工作区的右侧增加常驻贴边列;移动端使用默认隐藏的固定抽屉,通过会话头部的「活动」按钮展开,不挤压主会话布局。
|
|
49
49
|
- [x] **去除宠物图标功能**:不支持宠物相关功能,界面聚焦于会话活动本身。
|
|
50
50
|
- [x] **原生数据源订阅**:直接订阅 DSH 原生 `sessions` / `workspaces` 服务的推送式快照;时间线最多显示 4 个折叠工作项行,保留最近用户指令与真实执行中的工作项。
|
|
51
|
-
- [x]
|
|
51
|
+
- [x] **增加历史会话列表**:窗格分为「活动会话」和「最近历史」两个区域,非活动主会话按最近活动时间分批呈现;历史区底部提供「加载更多...」按钮,点击后才继续找回更早会话;卡片同时显示绝对日期时间与相对活动时间。
|
|
52
52
|
- [x] **强化等待行动提醒**:阻塞等待、完成提醒与错误提醒分别以金色、绿色和红色卡片标识;提问直接预览问题列表,完成提醒经卡片上的「移入历史」按钮显式确认;状态由宿主侧持久化并在所有客户端间同步,刷新页面或另开窗口不会丢失未确认的完成提醒和尚未被新回合覆盖的错误提醒。
|
|
53
53
|
- [x] **显示子/孙会话层级**:子代理以连接线和紧凑卡片嵌套在母会话下;母会话自身回合结束但仍有活动后代时继续按运行中呈现,子代理结束且没有活动后代后从活动区消失;历史区只保留主会话。
|
|
54
54
|
- [x] **显示工作区名称并参与排序**:会话卡片显示带稳定色彩的工作区徽标,会话排序与左侧边栏中的工作区顺序保持一致。
|
package/package.json
CHANGED
package/scripts/acceptance.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// 真实设备与复杂编排类条目。迁移映射:
|
|
10
10
|
// - R-01-001/AC-03~AC-06、R-01-002/AC-06~AC-07(空态/完成/阻塞及三处徽标)、R-01-017/AC-01、R-02-002/AC-01、AC-02 → e2e/specs/auto-update.mjs
|
|
11
11
|
// - R-01-004/AC-01、AC-02 → e2e/specs/long-list.mjs
|
|
12
|
+
// - R-01-019/AC-01~AC-04 → e2e/specs/recent-infinite-scroll.mjs(首批/手动追加/末尾停止/互斥与独立滚动)
|
|
12
13
|
// - R-01-007/AC-01、AC-02、R-01-011/AC-05 → e2e/specs/desktop-layout.mjs
|
|
13
14
|
// - R-01-008/AC-01、AC-02 点击/键盘、AC-03 鼠标、AC-04 文案/相对位置、AC-05、AC-06 点击/键盘 → e2e/specs/mobile-drawer.mjs(AC-03 真触摸、AC-04 精确位置与遮挡观感残留人工)
|
|
14
15
|
// - R-01-010/AC-04 → e2e/specs/auto-update.mjs 与 e2e/specs/session-lifecycle.mjs
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
// - R-01-005/AC-01 点击/Enter/Space、R-01-006/AC-01 → e2e/specs/navigation.mjs
|
|
20
21
|
// - R-01-015/AC-01、AC-02、AC-04、AC-03 桌面折叠态 → e2e/specs/resize.mjs;AC-03 移动端无手柄 → e2e/specs/mobile-drawer.mjs
|
|
21
22
|
// - R-01-018/AC-01、AC-03、AC-04、AC-05、AC-02 点击/键盘/reduced-motion → e2e/specs/back-to-top.mjs(AC-02 平滑观感残留人工)
|
|
22
|
-
// - R-01-003/AC-03、AC-08~AC-12 浏览器着色、R-01-012/AC-01、AC-05、R-01-013/AC-01、AC-03
|
|
23
|
+
// - R-01-003/AC-03、AC-08~AC-12 浏览器着色、R-01-012/AC-01、AC-05、R-01-013/AC-01、R-01-013/AC-03、R-01-013/AC-04、AC-07、AC-08、AC-10、AC-11 → e2e/specs/card-content.mjs(AC-02 结构断言已迁,多工作区感知区分与「标题随宿主更新」半句仍人工)
|
|
23
24
|
// T-089 起:
|
|
24
25
|
// - R-01-009/AC-02、AC-03 的 tool→等待→stream 实时更新 → e2e/specs/auto-update.mjs(AC-01 进行中 tool partial 仍由 unit/人工承接)。
|
|
25
26
|
// - R-01-014/AC-01、AC-03、AC-06 的列表 pending→ready、detail 渐进补齐与计数切换 → e2e/specs/loading-ready.mjs。
|
|
@@ -125,9 +126,10 @@ const steps = [
|
|
|
125
126
|
// R-01-013/AC-02 标题随宿主更新(回归:单卡渲染异常不得冻结其余卡片)
|
|
126
127
|
"重命名一个历史会话后,确认窗格历史卡标题与左边栏同步变为新标题;对之后获得自动生成标题的会话,确认卡片不停留在首条消息形态的旧标题。",
|
|
127
128
|
// R-01-010/AC-01 归档会话不入最近历史
|
|
128
|
-
"
|
|
129
|
+
"在左边栏归档一个历史会话后,确认其卡片从最近历史区消失(不再出现点了回落到新会话界面的死卡);其余历史卡不受影响。",
|
|
129
130
|
// R-01-010/AC-08、R-01-010/AC-09 历史区时间口径为最后活动时间
|
|
130
|
-
|
|
131
|
+
// R-01-013/AC-05 历史卡同时显示绝对日期时间与相对活动时间
|
|
132
|
+
"找一个发指令后 agent 运行了较久才结束的会话,待其落入最近历史区后,确认卡片第五行同时显示本地绝对日期时间(跨年份带年份)与相对活动时间,且绝对时间对应最后回复结束时刻(而非发指令时刻);历史区仍按该时刻从新到旧排序。刷新页面(冷会话)后确认时间先按宿主列表时间显示、数据到达后精化为回复结束时刻,相对时间会按分钟级推进。",
|
|
131
133
|
// R-01-013/AC-09 最近卡标题常规字重
|
|
132
134
|
"确认最近历史卡的会话标题为常规字重(不加粗),与活动卡的加粗标题形成明显区分。",
|
|
133
135
|
// R-01-013/AC-10 最近卡整体淡化
|
|
@@ -177,8 +179,10 @@ const steps = [
|
|
|
177
179
|
"重复上述两个方向的迁移并观察迁移卡以外的卡片:确认所有位置受影响的卡片(活动区剩余卡片、历史区其余卡片)与「最近历史」段头均以平滑滑动过渡到新位置,无任何卡片或段头瞬间跳变。",
|
|
178
180
|
// R-01-010/AC-05 分隔线留白
|
|
179
181
|
"确认活动区与最近历史区之间的分隔线上下各有约 10px 留白,两区呼吸感明确。",
|
|
180
|
-
// R-01-004/AC-03
|
|
181
|
-
"
|
|
182
|
+
// R-01-004/AC-03 滚动条指针显隐与 thumb hover 高亮
|
|
183
|
+
"将鼠标移入窗格(包括右缘调宽命中区)确认滚动条出现;调宽命中区只改变光标、不绘制高亮;将鼠标悬停在 scrollbar thumb 上,确认 thumb 变为更醒目的主题 hover 色;再向左移出调宽区后拖动 native scrollbar 确认列表滚动而 pane 宽度不变;鼠标离开且停止滚动约 0.6s 后滚动条隐藏,主会话区域滚动行为不受影响。",
|
|
184
|
+
// R-01-019/AC-01、R-01-019/AC-02、R-01-019/AC-03、R-01-019/AC-04 历史会话手动分页
|
|
185
|
+
"准备至少 21 个已完成且已移入历史的主会话:首次打开历史区只显示按最近活动时间排序的 10 张卡,并在列表底部显示「加载更多...」按钮;将窗格滚动到底部,确认不会自动追加,点击按钮后才追加后 10 张且前 10 张保留、无重复;再次点击按钮确认只追加最后 1 张、按钮随后隐藏,继续滚动不再增加;追加过程中活动区仍与历史区互斥,窗格滚动不带动主会话页面,桌面与移动端抽屉均可正常继续浏览。另以少于 10 张历史会话确认首屏全部显示、不显示加载按钮且不会自动补页。",
|
|
182
186
|
// R-01-018/AC-02、AC-04、AC-05 观感残留(键盘/显隐/回顶/窄条/抽屉行为断言已迁 e2e/specs/back-to-top.mjs)
|
|
183
187
|
"确认平滑滚动动画流畅自然;切到 <=767px 移动视口打开抽屉,确认抽屉内按钮不遮挡标题行;深浅主题各验一次不透明底色与图标可辨。",
|
|
184
188
|
// R-01-011/AC-03 标题行整体收起
|
package/scripts/bench.mjs
CHANGED
|
@@ -75,7 +75,7 @@ function renderPass() {
|
|
|
75
75
|
entry.timeline = detail.memoTimeline;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
const recent = buildRecent(listSnapshot, [], Date.now(),
|
|
78
|
+
const recent = buildRecent(listSnapshot, [], Date.now(), detailsById);
|
|
79
79
|
for (const entry of recent) {
|
|
80
80
|
const detail = detailsById.get(entry.id);
|
|
81
81
|
if (!detail) continue;
|
|
@@ -115,7 +115,7 @@ function legacyRenderPass() {
|
|
|
115
115
|
legacyScanAll(detail.snapshot); // 旧渲染循环 timeline 重算
|
|
116
116
|
legacyScanAll(detail.snapshot); // 旧渲染循环 previews 重算
|
|
117
117
|
}
|
|
118
|
-
const recent = buildRecent(listSnapshot, [], Date.now(),
|
|
118
|
+
const recent = buildRecent(listSnapshot, [], Date.now(), detailsById);
|
|
119
119
|
for (const entry of recent) {
|
|
120
120
|
const detail = detailsById.get(entry.id);
|
|
121
121
|
if (!detail?.snapshot) continue;
|