dsh-activity-pane 0.2.2 → 0.4.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 +267 -85
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/scripts/acceptance.mjs +14 -7
- package/scripts/bench.mjs +2 -2
- package/scripts/check.mjs +199 -34
- package/src/client.mjs +189 -64
- package/src/core.mjs +78 -21
package/.dsh-plugin/client.js
CHANGED
|
@@ -1073,6 +1073,31 @@ function usageSummary({ uncachedInputTokens = null, cacheReadTokens = null, cach
|
|
|
1073
1073
|
return { inputTokens, cacheHitPct };
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
1076
|
+
/** 从 sessions.list 的投影统一归一卡片统计:运行卡与最近卡共享同一口径。 */
|
|
1077
|
+
function statsFromProjection(projection = null, elapsedMs = null) {
|
|
1078
|
+
const usage = isRecord(projection?.tokenUsage) ? projection.tokenUsage : {};
|
|
1079
|
+
const sessionStats = isRecord(projection?.sessionStats) ? projection.sessionStats : {};
|
|
1080
|
+
const rateTokS = Number.isFinite(sessionStats.decodeMs) && sessionStats.decodeMs > 0
|
|
1081
|
+
? sessionStats.decodeTokens / (sessionStats.decodeMs / 1000)
|
|
1082
|
+
: null;
|
|
1083
|
+
return {
|
|
1084
|
+
...runtimeStats({ elapsedMs, outputTokens: usage.outputTokens, rateTokS }),
|
|
1085
|
+
...usageSummary(usage),
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** 统计字段按字段合并:当前值有效时优先,否则回退到上一份已知值;不合并 elapsedMs。 */
|
|
1090
|
+
function mergeRuntimeStats(current = null, fallback = null) {
|
|
1091
|
+
const currentStats = isRecord(current) ? current : {};
|
|
1092
|
+
const fallbackStats = isRecord(fallback) ? fallback : {};
|
|
1093
|
+
return {
|
|
1094
|
+
outputTokens: currentStats.outputTokens ?? fallbackStats.outputTokens ?? null,
|
|
1095
|
+
inputTokens: currentStats.inputTokens ?? fallbackStats.inputTokens ?? null,
|
|
1096
|
+
cacheHitPct: currentStats.cacheHitPct ?? fallbackStats.cacheHitPct ?? null,
|
|
1097
|
+
rateTokS: currentStats.rateTokS ?? fallbackStats.rateTokS ?? null,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1076
1101
|
/** 需要用户行动的种类的展示文案。 */
|
|
1077
1102
|
function pendingText(kind) {
|
|
1078
1103
|
return PENDING_LABELS[kind] ?? PENDING_UNKNOWN_LABEL;
|
|
@@ -1174,8 +1199,8 @@ function escapeCssString(value) {
|
|
|
1174
1199
|
* windowComplete(R-01-009/AC-06、R-01-012/AC-12 冷窗口兜底):快照已就绪但窗口缺
|
|
1175
1200
|
* 锚点数据(开放回合起点或可锚用户行在窗口外)时为 false——此时仍发起一次 history
|
|
1176
1201
|
* 补读,供进度锚点与指令锚行兜底。previewFallbackNeeded 表示最近卡的快照预览
|
|
1177
|
-
* 不完整,同样补读一次 history(R-01-013/AC-03、AC-04);durationFallbackNeeded
|
|
1178
|
-
* 需要在已加载的旧 history 之后再取一次最新回合边界(R-01-009/AC-12)。 */
|
|
1202
|
+
* 不完整,同样补读一次 history(R-01-013/AC-03、AC-04);durationFallbackNeeded 表示等待卡或最近卡
|
|
1203
|
+
* 需要在已加载的旧 history 之后再取一次最新回合边界(R-01-009/AC-12、R-01-013/AC-12)。 */
|
|
1179
1204
|
function detailLoadPlan({
|
|
1180
1205
|
detail = {},
|
|
1181
1206
|
isSubagent = false,
|
|
@@ -1600,11 +1625,6 @@ function listLoadState(snapshot) {
|
|
|
1600
1625
|
return "ready";
|
|
1601
1626
|
}
|
|
1602
1627
|
|
|
1603
|
-
/** 历史窗口:会话最后一次活动距现在不超过该毫秒数则视为"最近使用过"。 */
|
|
1604
|
-
const HISTORY_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
1605
|
-
/** 历史区最多展示的最近会话条数。 */
|
|
1606
|
-
const HISTORY_MAX = 20;
|
|
1607
|
-
|
|
1608
1628
|
/** 会话行是否为某主会话的直属子代理。 */
|
|
1609
1629
|
function isSubagentRow(row, byId = {}) {
|
|
1610
1630
|
const id = row?.parentId;
|
|
@@ -1708,7 +1728,7 @@ function entryErrorNote(completion) {
|
|
|
1708
1728
|
|
|
1709
1729
|
/**
|
|
1710
1730
|
* 活动区→历史区迁移检测(R-01-010/AC-07):上一帧活动区 id 在本帧离开活动区且出现于
|
|
1711
|
-
*
|
|
1731
|
+
* 历史区即判定为一次迁移;彻底消失(归档、不再被会话服务列出)不判定。prevActiveIds 为上一帧
|
|
1712
1732
|
* 已渲染的活动区 id 集合,active/recent 为本帧派生条目。
|
|
1713
1733
|
*/
|
|
1714
1734
|
function movedToRecentIds(prevActiveIds, active, recent) {
|
|
@@ -1724,7 +1744,7 @@ function movedToRecentIds(prevActiveIds, active, recent) {
|
|
|
1724
1744
|
|
|
1725
1745
|
/**
|
|
1726
1746
|
* 历史区→活动区迁移检测(R-01-010/AC-07):上一帧历史区 id 在本帧离开历史区且出现于
|
|
1727
|
-
*
|
|
1747
|
+
* 活动区即判定为一次反向迁移;彻底消失(归档、不再被会话服务列出)不判定。prevRecentIds 为上一帧
|
|
1728
1748
|
* 已渲染的历史区 id 集合,active/recent 为本帧派生条目。与 movedToRecentIds 镜像对称。
|
|
1729
1749
|
*/
|
|
1730
1750
|
function movedToActiveIds(prevRecentIds, active, recent) {
|
|
@@ -1824,16 +1844,12 @@ function lastTurnDuration({ turnTimings = null, history = [] } = {}) {
|
|
|
1824
1844
|
}
|
|
1825
1845
|
|
|
1826
1846
|
/**
|
|
1827
|
-
*
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
* 一张点了回落到新会话界面的死卡。完成确认中的会话留在活动区,不入历史区;
|
|
1832
|
-
* 委托周期(含耗尽空窗)中的会话同样留在活动区(delegatingIds,分区不变量)。
|
|
1833
|
-
* 窗口候选判定用宿主列表时间(下界);turnEnds(id → 已知回合结束时刻)驱动
|
|
1834
|
-
* 时间精化:条目 activityAt 取宿主列表时间与回合结束时刻的较新者(R-01-010/AC-08、AC-09)。
|
|
1847
|
+
* 构建历史区条目:当前非活动的**主会话**,按最后活动时间从新到旧返回完整候选集合。
|
|
1848
|
+
* 子代理是临时工作单元,不入历史区;归档、空会话、完成/错误提醒与委托周期中的会话
|
|
1849
|
+
* 也不入历史区。历史区不再按时间窗口或条数截断;turnEnds(id → 已知回合结束时刻)驱动
|
|
1850
|
+
* activityAt 精化(R-01-010、R-01-019)。
|
|
1835
1851
|
*/
|
|
1836
|
-
function buildRecent(snapshot, workspaceItems, now,
|
|
1852
|
+
function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedIds = [], completions = null, delegatingIds = null, turnEnds = null) {
|
|
1837
1853
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1838
1854
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1839
1855
|
const current = snapshot?.current ?? null;
|
|
@@ -1854,7 +1870,7 @@ function buildRecent(snapshot, workspaceItems, now, windowMs = HISTORY_WINDOW_MS
|
|
|
1854
1870
|
if (isActiveRow(row, byId, activeIds)) continue;
|
|
1855
1871
|
const updatedAt = Number(row.updatedAt);
|
|
1856
1872
|
if (!Number.isFinite(updatedAt)) continue;
|
|
1857
|
-
if (
|
|
1873
|
+
if (Number.isFinite(now) && updatedAt > now) continue;
|
|
1858
1874
|
const turnEnd = Number(mapValue(turnEnds, id));
|
|
1859
1875
|
const activityAt = Number.isFinite(turnEnd) ? Math.max(updatedAt, turnEnd) : updatedAt;
|
|
1860
1876
|
const details = mapValue(detailsById, id) ?? {};
|
|
@@ -1878,8 +1894,14 @@ function buildRecent(snapshot, workspaceItems, now, windowMs = HISTORY_WINDOW_MS
|
|
|
1878
1894
|
});
|
|
1879
1895
|
}
|
|
1880
1896
|
|
|
1881
|
-
entries.sort((a, b) =>
|
|
1882
|
-
|
|
1897
|
+
entries.sort((a, b) => {
|
|
1898
|
+
const byActivity = b.activityAt - a.activityAt;
|
|
1899
|
+
if (byActivity !== 0) return byActivity;
|
|
1900
|
+
const aId = String(a.id);
|
|
1901
|
+
const bId = String(b.id);
|
|
1902
|
+
return aId < bId ? -1 : aId > bId ? 1 : 0;
|
|
1903
|
+
});
|
|
1904
|
+
return entries;
|
|
1883
1905
|
}
|
|
1884
1906
|
|
|
1885
1907
|
/** 毫秒时长的人性化短格式,例如 "47s"、"3m12s"。 */
|
|
@@ -1890,6 +1912,41 @@ function fmtElapsedMs(ms) {
|
|
|
1890
1912
|
return `${Math.floor(s / 60)}m${s % 60}s`;
|
|
1891
1913
|
}
|
|
1892
1914
|
|
|
1915
|
+
/** 历史卡相对活动时间:按分钟、小时、天、周、月、年分级;负值返回空。 */
|
|
1916
|
+
function fmtRelativeAge(ageMs) {
|
|
1917
|
+
if (!Number.isFinite(ageMs) || ageMs < 0) return "";
|
|
1918
|
+
const minute = 60_000;
|
|
1919
|
+
const hour = 60 * minute;
|
|
1920
|
+
const day = 24 * hour;
|
|
1921
|
+
if (ageMs < minute) return "刚刚";
|
|
1922
|
+
if (ageMs < hour) return `${Math.floor(ageMs / minute)}分钟前`;
|
|
1923
|
+
if (ageMs < day) return `${Math.floor(ageMs / hour)}小时前`;
|
|
1924
|
+
if (ageMs < 7 * day) return `${Math.floor(ageMs / day)}天前`;
|
|
1925
|
+
if (ageMs < 30 * day) return `${Math.floor(ageMs / (7 * day))}周前`;
|
|
1926
|
+
if (ageMs < 365 * day) return `${Math.floor(ageMs / (30 * day))}个月前`;
|
|
1927
|
+
return `${Math.floor(ageMs / (365 * day))}年前`;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/** 历史卡绝对活动时间:使用浏览器/宿主本地时区,当前年份省略年份,跨年补年份。 */
|
|
1931
|
+
function fmtAbsoluteDateTime(ts, now = Date.now()) {
|
|
1932
|
+
if (ts === null || ts === undefined || now === null || now === undefined) return "";
|
|
1933
|
+
try {
|
|
1934
|
+
const value = Number(ts);
|
|
1935
|
+
const current = Number(now);
|
|
1936
|
+
const date = new Date(value);
|
|
1937
|
+
const today = new Date(current);
|
|
1938
|
+
if (!Number.isFinite(value) || !Number.isFinite(current) || !Number.isFinite(date.getTime()) || !Number.isFinite(today.getTime())) return "";
|
|
1939
|
+
const dateOptions = { month: "2-digit", day: "2-digit" };
|
|
1940
|
+
if (date.getFullYear() !== today.getFullYear()) dateOptions.year = "numeric";
|
|
1941
|
+
return `${date.toLocaleDateString([], dateOptions)} ${date.toLocaleTimeString([], {
|
|
1942
|
+
hour: "2-digit",
|
|
1943
|
+
minute: "2-digit",
|
|
1944
|
+
})}`;
|
|
1945
|
+
} catch {
|
|
1946
|
+
return "";
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1893
1950
|
/** token 计数的人性化短格式,例如 "847"、"1.2k";非有限非负时返回 null。 */
|
|
1894
1951
|
/** token 计数紧凑缩写,镜像原生统计行 formatTokens:847 / 12.2K / 517K / 2.8M——
|
|
1895
1952
|
* 千以下原样;K/M 档缩写值百位以上取整、不足百位保留一位小数;非法输入返回 null 不展示。 */
|
|
@@ -2128,8 +2185,12 @@ const INDENT_PX = 16;
|
|
|
2128
2185
|
const MOBILE_BREAKPOINT = "767px";
|
|
2129
2186
|
/** 运行卡时钟:只要存在运行中会话,就以该周期刷新时长显示。 */
|
|
2130
2187
|
const CLOCK_MS = 1000;
|
|
2188
|
+
/** 历史卡相对时间刷新周期;无需每秒重绘整列。 */
|
|
2189
|
+
const RECENT_TIME_REFRESH_MS = 60_000;
|
|
2131
2190
|
/** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
|
|
2132
2191
|
const LOAD_CONCURRENCY = 3;
|
|
2192
|
+
/** 历史分页每批数量;后续批次只由底部按钮显式加载。 */
|
|
2193
|
+
const RECENT_PAGE_SIZE = 10;
|
|
2133
2194
|
/** 「回到顶部」悬浮按钮显隐阈值:scrollTop 超过该值(px)时显示(R-01-018/AC-01)。 */
|
|
2134
2195
|
const TOP_THRESHOLD = 200;
|
|
2135
2196
|
|
|
@@ -2213,21 +2274,34 @@ const CSS = `
|
|
|
2213
2274
|
touch-action: pan-y;
|
|
2214
2275
|
-webkit-overflow-scrolling: touch;
|
|
2215
2276
|
padding: 0 0 10px;
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2277
|
+
/* Keep the native scrollbar just left of the edge handle, as in the host
|
|
2278
|
+
sidebar; stable keeps the list geometry unchanged when overflow appears. */
|
|
2279
|
+
margin-right: var(--dsh-scrollbar-width, 8px);
|
|
2280
|
+
scrollbar-gutter: stable;
|
|
2281
|
+
}
|
|
2282
|
+
/* 滚动条在滚动或鼠标进入窗格时显示(R-01-004/AC-03,与外壳侧栏一致):thumb 默认透明,
|
|
2283
|
+
经 data-scrolling/data-pointer-inside 显示,悬停时读取主题 hover token。Firefox 路径必须在
|
|
2284
|
+
@supports 门内——非 auto 的 scrollbar-color 会让 Chromium 丢弃该元素的 ::-webkit-scrollbar 规则。 */
|
|
2220
2285
|
[data-dsh-activity-pane] .dap-scroll::-webkit-scrollbar-thumb {
|
|
2221
2286
|
background: transparent;
|
|
2222
2287
|
}
|
|
2223
2288
|
[data-dsh-activity-pane] .dap-scroll[data-scrolling]::-webkit-scrollbar-thumb {
|
|
2224
2289
|
background: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent));
|
|
2225
2290
|
}
|
|
2291
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb {
|
|
2292
|
+
background: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent));
|
|
2293
|
+
}
|
|
2294
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb:hover {
|
|
2295
|
+
background: var(--dsh-scrollbar-thumb-hover, color-mix(in srgb, currentColor 40%, transparent));
|
|
2296
|
+
}
|
|
2226
2297
|
@supports not selector(::-webkit-scrollbar) {
|
|
2227
2298
|
[data-dsh-activity-pane] .dap-scroll { scrollbar-color: transparent transparent; }
|
|
2228
2299
|
[data-dsh-activity-pane] .dap-scroll[data-scrolling] {
|
|
2229
2300
|
scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
|
|
2230
2301
|
}
|
|
2302
|
+
[data-dsh-activity-pane][data-pointer-inside] .dap-scroll {
|
|
2303
|
+
scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
|
|
2304
|
+
}
|
|
2231
2305
|
}
|
|
2232
2306
|
/* 「回到顶部」悬浮图标按钮(R-01-018):窗格内右下角圆形按钮,纯图标无文字、不透明底色;
|
|
2233
2307
|
默认 hidden,scrollTop 超阈值时由滚动监听揭隐。基类 display:flex 会压过 UA 的
|
|
@@ -2284,6 +2358,29 @@ const CSS = `
|
|
|
2284
2358
|
color: color-mix(in srgb, currentColor 52%, transparent);
|
|
2285
2359
|
text-transform: uppercase;
|
|
2286
2360
|
}
|
|
2361
|
+
/* 历史分页只在用户明确激活底部按钮后追加(R-01-019/AC-02)。 */
|
|
2362
|
+
[data-dsh-activity-pane] .dap-recent-more {
|
|
2363
|
+
align-self: center;
|
|
2364
|
+
flex: none;
|
|
2365
|
+
min-width: 132px;
|
|
2366
|
+
padding: 5px 10px;
|
|
2367
|
+
border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
|
|
2368
|
+
border-radius: 6px;
|
|
2369
|
+
background: color-mix(in srgb, currentColor 7%, transparent);
|
|
2370
|
+
color: color-mix(in srgb, currentColor 70%, transparent);
|
|
2371
|
+
font: inherit;
|
|
2372
|
+
font-size: 11px;
|
|
2373
|
+
line-height: 16px;
|
|
2374
|
+
cursor: pointer;
|
|
2375
|
+
}
|
|
2376
|
+
[data-dsh-activity-pane] .dap-recent-more:hover,
|
|
2377
|
+
[data-dsh-activity-pane] .dap-recent-more:focus-visible {
|
|
2378
|
+
background: color-mix(in srgb, currentColor 13%, transparent);
|
|
2379
|
+
color: inherit;
|
|
2380
|
+
}
|
|
2381
|
+
[data-dsh-activity-pane] .dap-recent-more:focus-visible { outline: 2px solid color-mix(in srgb, currentColor 45%, transparent); outline-offset: 1px; }
|
|
2382
|
+
[data-dsh-activity-pane] .dap-recent-more[disabled] { cursor: wait; opacity: 0.6; }
|
|
2383
|
+
[data-dsh-activity-pane] .dap-recent-more[hidden] { display: none; }
|
|
2287
2384
|
/* 折叠:仅桌面生效;窄条 + 竖排标题与计数(R-01-011/AC-04)。 */
|
|
2288
2385
|
[data-dsh-activity-pane] .dap-rail {
|
|
2289
2386
|
display: none;
|
|
@@ -2322,7 +2419,8 @@ const CSS = `
|
|
|
2322
2419
|
background: rgba(46, 42, 26, 0.97);
|
|
2323
2420
|
animation: dap-await-pulse 1.2s ease-in-out infinite;
|
|
2324
2421
|
}
|
|
2325
|
-
/* 桌面拖拽调宽手柄(R-01-015):右缘 6px
|
|
2422
|
+
/* 桌面拖拽调宽手柄(R-01-015):右缘 6px 透明命中区,拖拽实时写入 --dap-width;
|
|
2423
|
+
滚动区在其左侧留出 native scrollbar 命中带,手柄只改变光标、不绘制 hover 高亮;
|
|
2326
2424
|
折叠窄条与移动端抽屉不提供拖拽(下方两处媒体查询隐藏)。 */
|
|
2327
2425
|
[data-dsh-activity-pane] .dap-resize {
|
|
2328
2426
|
position: absolute;
|
|
@@ -2332,10 +2430,6 @@ const CSS = `
|
|
|
2332
2430
|
touch-action: none;
|
|
2333
2431
|
z-index: 6;
|
|
2334
2432
|
}
|
|
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
2433
|
/* 桌面:窗格作为中间列内的真实 flex 行元素参与布局——中间列被置为行方向,
|
|
2340
2434
|
窗格固定宽、会话区弹性收缩,整个会话内容被真实挤到右边;折叠时收窄为窄条。 */
|
|
2341
2435
|
@media (min-width: 768px) {
|
|
@@ -2580,14 +2674,10 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
2580
2674
|
display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
|
|
2581
2675
|
min-width: 0; margin: 2px 0 0;
|
|
2582
2676
|
}
|
|
2583
|
-
/*
|
|
2677
|
+
/* 等待类型胶囊独立同行:耗时回到统计行最右侧,不参与等待脉冲。 */
|
|
2584
2678
|
[data-dsh-activity-pane] .dap-await-head {
|
|
2585
2679
|
display: flex; align-items: center; gap: 8px; align-self: stretch; min-width: 0;
|
|
2586
2680
|
}
|
|
2587
|
-
[data-dsh-activity-pane] .dap-await-head .dap-token-time {
|
|
2588
|
-
margin-left: auto; flex: none; font-size: 10px; line-height: 14px;
|
|
2589
|
-
color: #8f9aaa; font-variant-numeric: tabular-nums;
|
|
2590
|
-
}
|
|
2591
2681
|
[data-dsh-activity-pane] .dap-note-row {
|
|
2592
2682
|
display: flex; align-items: center; gap: 6px; min-width: 0; align-self: stretch;
|
|
2593
2683
|
}
|
|
@@ -2606,7 +2696,7 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
2606
2696
|
color: color-mix(in srgb, currentColor 82%, transparent);
|
|
2607
2697
|
background: color-mix(in srgb, currentColor 14%, transparent);
|
|
2608
2698
|
}
|
|
2609
|
-
/*
|
|
2699
|
+
/* 运行卡与最近卡统计(对齐 answer-pet 卡片;MIT 参考,见 README)。 */
|
|
2610
2700
|
[data-dsh-activity-pane] .dap-progress {
|
|
2611
2701
|
display: flex; align-items: center; gap: 7px; min-width: 0;
|
|
2612
2702
|
}
|
|
@@ -3041,12 +3131,15 @@ function livenessFromSnapshot(snap) {
|
|
|
3041
3131
|
return { startTime };
|
|
3042
3132
|
}
|
|
3043
3133
|
|
|
3044
|
-
|
|
3134
|
+
/** 历史卡时间同时给出本地绝对日期时分与相对年龄;跨年份的绝对日期带年份。 */
|
|
3135
|
+
function fmtRecentTime(ts, now = Date.now()) {
|
|
3045
3136
|
try {
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3137
|
+
const value = Number(ts);
|
|
3138
|
+
const current = Number(now);
|
|
3139
|
+
const absolute = fmtAbsoluteDateTime(ts, now);
|
|
3140
|
+
if (!absolute) return "";
|
|
3141
|
+
const relative = fmtRelativeAge(current - value);
|
|
3142
|
+
return relative ? `最后活动 · ${absolute} · ${relative}` : `最后活动 · ${absolute}`;
|
|
3050
3143
|
} catch {
|
|
3051
3144
|
return "";
|
|
3052
3145
|
}
|
|
@@ -3080,6 +3173,7 @@ function apply(ctx) {
|
|
|
3080
3173
|
let sessionUnsubscribe = null;
|
|
3081
3174
|
let workspaceUnsubscribe = null;
|
|
3082
3175
|
let clockTimer = null;
|
|
3176
|
+
let recentTimeTimer = null;
|
|
3083
3177
|
let syncScheduled = false;
|
|
3084
3178
|
let lastSig = "";
|
|
3085
3179
|
/** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
|
|
@@ -3132,11 +3226,16 @@ function apply(ctx) {
|
|
|
3132
3226
|
const cardsById = new Map();
|
|
3133
3227
|
/** 历史区卡片 id → { el } 复用表。 */
|
|
3134
3228
|
const recentCardsById = new Map();
|
|
3229
|
+
/** 历史区渐进呈现状态:候选由核心完整派生,DOM 与详情读取只覆盖当前前缀。 */
|
|
3230
|
+
let recentVisibleCount = RECENT_PAGE_SIZE;
|
|
3231
|
+
let recentTotal = 0;
|
|
3232
|
+
let recentHasMore = false;
|
|
3233
|
+
let recentAppendQueued = false;
|
|
3135
3234
|
/** 运行中会话原生快照订阅:id → { unsubscribe, liveness, snapshot }。 */
|
|
3136
3235
|
const livenessById = new Map();
|
|
3137
3236
|
/** 委托周期进度锚点记账:id → progressAnchor 状态(R-01-009/AC-06)。 */
|
|
3138
3237
|
const progressAnchorById = new Map();
|
|
3139
|
-
/**
|
|
3238
|
+
/** 订阅停止后保留最近快照与最后已知运行统计,供 awaiting/recent 卡继续显示上下文。 */
|
|
3140
3239
|
const sessionDetailsById = new Map();
|
|
3141
3240
|
/** 会话跳转的单一重试链;避免重复点击叠加 refresh/timer。 */
|
|
3142
3241
|
const openRetryStates = new Map();
|
|
@@ -3545,12 +3644,25 @@ function apply(ctx) {
|
|
|
3545
3644
|
window.dispatchEvent(new Event("resize"));
|
|
3546
3645
|
} catch {}
|
|
3547
3646
|
}
|
|
3647
|
+
function loadMoreRecent() {
|
|
3648
|
+
if (disposed || !recentHasMore || recentAppendQueued) return false;
|
|
3649
|
+
const nextCount = Math.min(recentVisibleCount + RECENT_PAGE_SIZE, recentTotal);
|
|
3650
|
+
if (nextCount <= recentVisibleCount) return false;
|
|
3651
|
+
recentAppendQueued = true;
|
|
3652
|
+
recentVisibleCount = nextCount;
|
|
3653
|
+
queueSync();
|
|
3654
|
+
return true;
|
|
3655
|
+
}
|
|
3548
3656
|
function bindPaneControls(pane) {
|
|
3549
3657
|
const header = pane.querySelector(".dap-header");
|
|
3550
3658
|
const rail = pane.querySelector(".dap-rail");
|
|
3551
3659
|
const resize = pane.querySelector(".dap-resize");
|
|
3552
3660
|
const scroll = pane.querySelector(".dap-scroll");
|
|
3553
3661
|
const topBtn = pane.querySelector(".dap-top");
|
|
3662
|
+
const recentMore = pane.querySelector(".dap-recent-more");
|
|
3663
|
+
const onRecentMoreClick = () => {
|
|
3664
|
+
if (loadMoreRecent() && recentMore !== null) recentMore.disabled = true;
|
|
3665
|
+
};
|
|
3554
3666
|
// 标题行整体即收起控件,两端断点一致:桌面折叠为窄条(R-01-011/AC-03);
|
|
3555
3667
|
// 移动端断点解释为收起抽屉,而非折叠窄条(R-01-008/AC-02、R-01-011/AC-06)。
|
|
3556
3668
|
const onHeaderActivate = () => {
|
|
@@ -3573,6 +3685,15 @@ function apply(ctx) {
|
|
|
3573
3685
|
const syncTopBtn = () => {
|
|
3574
3686
|
if (topBtn !== null && scroll !== null) topBtn.hidden = scroll.scrollTop <= TOP_THRESHOLD;
|
|
3575
3687
|
};
|
|
3688
|
+
// 鼠标进入窗格即显示 scrollbar(R-01-004/AC-03);触摸指针不改变桌面滚动条可见状态。
|
|
3689
|
+
const onPanePointerEnter = (event) => {
|
|
3690
|
+
if (event.pointerType !== "mouse") return;
|
|
3691
|
+
pane.setAttribute("data-pointer-inside", "");
|
|
3692
|
+
};
|
|
3693
|
+
const onPanePointerLeave = (event) => {
|
|
3694
|
+
if (event.pointerType !== "mouse") return;
|
|
3695
|
+
pane.removeAttribute("data-pointer-inside");
|
|
3696
|
+
};
|
|
3576
3697
|
const onRailClick = () => {
|
|
3577
3698
|
collapsed = false;
|
|
3578
3699
|
pane.setAttribute("data-collapsed", "false");
|
|
@@ -3615,8 +3736,9 @@ function apply(ctx) {
|
|
|
3615
3736
|
resize.addEventListener("pointercancel", onResizeUp);
|
|
3616
3737
|
resize.setPointerCapture(event.pointerId);
|
|
3617
3738
|
};
|
|
3618
|
-
//
|
|
3619
|
-
//
|
|
3739
|
+
// 滚动条随滚动或窗格内鼠标指针显示(R-01-004/AC-03):滚动即置位,停滚 600ms 后收口;
|
|
3740
|
+
// 指针离开后若仍在滚动拖尾内则保持显示,拖尾结束后隐藏。同一监听承载「回到顶部」按钮显隐
|
|
3741
|
+
//(R-01-018/AC-01、AC-03):回顶后的收口不经额外事件,
|
|
3620
3742
|
// 由平滑滚动触发的 scroll 事件自然完成。
|
|
3621
3743
|
let scrollHideTimer = null;
|
|
3622
3744
|
const onScroll = () => {
|
|
@@ -3637,14 +3759,21 @@ function apply(ctx) {
|
|
|
3637
3759
|
header?.addEventListener("click", onHeaderActivate);
|
|
3638
3760
|
header?.addEventListener("keydown", onHeaderKeydown);
|
|
3639
3761
|
rail?.addEventListener("click", onRailClick);
|
|
3762
|
+
pane.addEventListener("pointerenter", onPanePointerEnter);
|
|
3763
|
+
pane.addEventListener("pointerleave", onPanePointerLeave);
|
|
3640
3764
|
scroll?.addEventListener("scroll", onScroll, { passive: true });
|
|
3765
|
+
recentMore?.addEventListener("click", onRecentMoreClick);
|
|
3641
3766
|
topBtn?.addEventListener("click", onTopClick);
|
|
3642
3767
|
resize?.addEventListener("pointerdown", onResizeDown);
|
|
3643
3768
|
return () => {
|
|
3644
3769
|
header?.removeEventListener("click", onHeaderActivate);
|
|
3645
3770
|
header?.removeEventListener("keydown", onHeaderKeydown);
|
|
3646
3771
|
rail?.removeEventListener("click", onRailClick);
|
|
3772
|
+
pane.removeEventListener("pointerenter", onPanePointerEnter);
|
|
3773
|
+
pane.removeEventListener("pointerleave", onPanePointerLeave);
|
|
3774
|
+
pane.removeAttribute("data-pointer-inside");
|
|
3647
3775
|
scroll?.removeEventListener("scroll", onScroll);
|
|
3776
|
+
recentMore?.removeEventListener("click", onRecentMoreClick);
|
|
3648
3777
|
if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);
|
|
3649
3778
|
topBtn?.removeEventListener("click", onTopClick);
|
|
3650
3779
|
resize?.removeEventListener("pointerdown", onResizeDown);
|
|
@@ -3674,7 +3803,8 @@ function apply(ctx) {
|
|
|
3674
3803
|
<div class="dap-scroll">
|
|
3675
3804
|
<div class="dap-list" tabindex="-1"><div class="dap-tracks" aria-hidden="true"></div></div>
|
|
3676
3805
|
<div class="dap-recent">
|
|
3677
|
-
<div class="dap-recent-head"><span
|
|
3806
|
+
<div class="dap-recent-head"><span>最近历史</span></div>
|
|
3807
|
+
<button class="dap-recent-more" type="button" hidden>加载更多...</button>
|
|
3678
3808
|
</div>
|
|
3679
3809
|
</div>
|
|
3680
3810
|
<button class="dap-top" type="button" aria-label="回到顶部" title="回到顶部" hidden></button>
|
|
@@ -3780,7 +3910,10 @@ function apply(ctx) {
|
|
|
3780
3910
|
const agentLabel = makeEl("span", "dap-history-label");
|
|
3781
3911
|
agentLabel.textContent = "助手";
|
|
3782
3912
|
agentLine.append(agentIcon, agentLabel, makeEl("span", "dap-history-separator"), makeEl("span", "dap-history-text"));
|
|
3783
|
-
|
|
3913
|
+
const statsRow = makeEl("div", "dap-token-stats");
|
|
3914
|
+
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
3915
|
+
statsRow.hidden = true;
|
|
3916
|
+
return [head, row, userLine, agentLine, statsRow, makeEl("div", "dap-note")];
|
|
3784
3917
|
}
|
|
3785
3918
|
if (kind === "awaiting") {
|
|
3786
3919
|
const row = makeEl("div", "dap-row");
|
|
@@ -3794,10 +3927,13 @@ function apply(ctx) {
|
|
|
3794
3927
|
const noteRow = makeEl("div", "dap-note-row");
|
|
3795
3928
|
noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
|
|
3796
3929
|
const awaitHead = makeEl("div", "dap-await-head");
|
|
3797
|
-
awaitHead.append(capsule
|
|
3930
|
+
awaitHead.append(capsule);
|
|
3931
|
+
const statsRow = makeEl("div", "dap-token-stats");
|
|
3932
|
+
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
3933
|
+
statsRow.hidden = true;
|
|
3798
3934
|
const foot = makeEl("div", "dap-foot");
|
|
3799
3935
|
foot.append(awaitHead, noteRow);
|
|
3800
|
-
return [head, row, makeEl("div", "dap-trace"), foot];
|
|
3936
|
+
return [head, row, makeEl("div", "dap-trace"), statsRow, foot];
|
|
3801
3937
|
}
|
|
3802
3938
|
const row = makeEl("div", "dap-row");
|
|
3803
3939
|
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
@@ -4158,14 +4294,19 @@ function apply(ctx) {
|
|
|
4158
4294
|
}
|
|
4159
4295
|
}
|
|
4160
4296
|
|
|
4161
|
-
/**
|
|
4297
|
+
/** 统计行渲染:运行卡、等待卡与最近卡写入速率、token 与耗时,旧骨架缺节点时就地补齐。 */
|
|
4162
4298
|
function renderTokenStats(el, entry) {
|
|
4163
4299
|
let stats = el.querySelector(".dap-token-stats");
|
|
4164
4300
|
if (stats === null) {
|
|
4165
4301
|
stats = makeEl("div", "dap-token-stats");
|
|
4166
4302
|
stats.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
4167
4303
|
stats.hidden = true;
|
|
4168
|
-
el.
|
|
4304
|
+
const foot = entry.kind === "awaiting" ? el.querySelector(".dap-foot") : null;
|
|
4305
|
+
if (foot !== null) el.insertBefore(stats, foot);
|
|
4306
|
+
else el.append(stats);
|
|
4307
|
+
} else if (entry.kind === "awaiting") {
|
|
4308
|
+
const foot = el.querySelector(".dap-foot");
|
|
4309
|
+
if (foot !== null && stats.nextElementSibling !== foot) el.insertBefore(stats, foot);
|
|
4169
4310
|
}
|
|
4170
4311
|
let mainTextEl = stats.querySelector(".dap-token-main");
|
|
4171
4312
|
let timeEl = stats.querySelector(".dap-token-time");
|
|
@@ -4187,25 +4328,9 @@ function apply(ctx) {
|
|
|
4187
4328
|
if (stats.hidden !== statsHidden) stats.hidden = statsHidden;
|
|
4188
4329
|
}
|
|
4189
4330
|
|
|
4190
|
-
/**
|
|
4191
|
-
function
|
|
4192
|
-
|
|
4193
|
-
if (foot === null) return;
|
|
4194
|
-
let head = foot.querySelector(".dap-await-head");
|
|
4195
|
-
if (head === null) {
|
|
4196
|
-
const capsule = foot.querySelector(".dap-capsule");
|
|
4197
|
-
if (capsule === null) return;
|
|
4198
|
-
head = makeEl("div", "dap-await-head");
|
|
4199
|
-
capsule.replaceWith(head);
|
|
4200
|
-
head.append(capsule);
|
|
4201
|
-
}
|
|
4202
|
-
let time = head.querySelector(".dap-token-time");
|
|
4203
|
-
if (time === null) {
|
|
4204
|
-
time = makeEl("span", "dap-token-time");
|
|
4205
|
-
head.append(time);
|
|
4206
|
-
}
|
|
4207
|
-
const text = Number.isFinite(entry.elapsedMs) && entry.elapsedMs >= 0 ? fmtElapsedMs(entry.elapsedMs) : "";
|
|
4208
|
-
if (time.textContent !== text) time.textContent = text;
|
|
4331
|
+
/** 旧热装等待骨架清理:耗时已迁回统计行,去掉胶囊同行残留的时长节点。 */
|
|
4332
|
+
function removeAwaitingHeadDuration(el) {
|
|
4333
|
+
el.querySelector(".dap-await-head .dap-token-time")?.remove();
|
|
4209
4334
|
}
|
|
4210
4335
|
|
|
4211
4336
|
/** 时间线区加载指示:数据在途且尚无工作项时显示活动图标行(R-01-014/AC-02)。 */
|
|
@@ -4340,12 +4465,14 @@ function apply(ctx) {
|
|
|
4340
4465
|
restoreTextField(text, previews[i]);
|
|
4341
4466
|
}
|
|
4342
4467
|
}
|
|
4468
|
+
renderTokenStats(el, entry);
|
|
4343
4469
|
}
|
|
4344
4470
|
|
|
4345
4471
|
if (entry.kind === "awaiting") {
|
|
4346
4472
|
const traceContainer = el.querySelector(".dap-trace");
|
|
4347
4473
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
4348
|
-
|
|
4474
|
+
removeAwaitingHeadDuration(el);
|
|
4475
|
+
renderTokenStats(el, entry);
|
|
4349
4476
|
const confirm = el.querySelector(".dap-confirm");
|
|
4350
4477
|
if (confirm !== null) {
|
|
4351
4478
|
// 激活锚点:只在结构重建时绑一次(卡片按 id 复用,kind 变化会重建骨架)。
|
|
@@ -4454,6 +4581,16 @@ function apply(ctx) {
|
|
|
4454
4581
|
}
|
|
4455
4582
|
}
|
|
4456
4583
|
|
|
4584
|
+
/** 历史卡相对时间只需分钟级刷新;无历史卡时停止定时器,避免空窗格常驻唤醒。 */
|
|
4585
|
+
function syncRecentTimeClock(wanted) {
|
|
4586
|
+
if (wanted && recentTimeTimer === null) {
|
|
4587
|
+
recentTimeTimer = setInterval(() => queueSync(), RECENT_TIME_REFRESH_MS);
|
|
4588
|
+
} else if (!wanted && recentTimeTimer !== null) {
|
|
4589
|
+
clearInterval(recentTimeTimer);
|
|
4590
|
+
recentTimeTimer = null;
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4457
4594
|
function getSessionSnapshot(session) {
|
|
4458
4595
|
try {
|
|
4459
4596
|
return session?.getSnapshot?.() ?? null;
|
|
@@ -4662,11 +4799,14 @@ function apply(ctx) {
|
|
|
4662
4799
|
if (entry.waitClass === "blocked" || entry.waitClass === "done" || entry.waitClass === "error")
|
|
4663
4800
|
rec.el.setAttribute("data-wait", entry.waitClass);
|
|
4664
4801
|
else rec.el.removeAttribute("data-wait");
|
|
4802
|
+
const recentTimeText = entry.kind === "recent" ? fmtRecentTime(entry.activityAt) : "";
|
|
4665
4803
|
rec.el.setAttribute(
|
|
4666
4804
|
"aria-label",
|
|
4667
4805
|
`${entry.workspaceTitle ? entry.workspaceTitle + " - " : ""}${entry.title}${
|
|
4668
4806
|
entry.pendingText ? "," + entry.pendingText : ""
|
|
4669
|
-
}${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}
|
|
4807
|
+
}${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}${
|
|
4808
|
+
recentTimeText ? "," + recentTimeText : ""
|
|
4809
|
+
}`,
|
|
4670
4810
|
);
|
|
4671
4811
|
renderCardInto(rec.el, entry, hueByWorkspace);
|
|
4672
4812
|
// 只有顺序/归属真正变化时才移动 DOM:每次渲染无条件 appendChild 会把所有
|
|
@@ -4857,6 +4997,10 @@ function apply(ctx) {
|
|
|
4857
4997
|
pulseSignature = "";
|
|
4858
4998
|
prevRenderedActiveIds = new Set();
|
|
4859
4999
|
prevRenderedRecentIds = new Set();
|
|
5000
|
+
recentVisibleCount = RECENT_PAGE_SIZE;
|
|
5001
|
+
recentTotal = 0;
|
|
5002
|
+
recentHasMore = false;
|
|
5003
|
+
recentAppendQueued = false;
|
|
4860
5004
|
// 旧窗格已脱离文档:其在飞平移的 transitionend 不再触发,逐元素取消避免残留。
|
|
4861
5005
|
for (const el of [...shiftCleanups.keys()]) cancelShift(el);
|
|
4862
5006
|
}
|
|
@@ -4876,6 +5020,8 @@ function apply(ctx) {
|
|
|
4876
5020
|
}
|
|
4877
5021
|
}
|
|
4878
5022
|
const listState = listLoadState(snapshot);
|
|
5023
|
+
// 只消费上一轮追加请求;列表短暂 pending 时保留已展开页,ready 后继续从同一前缀呈现。
|
|
5024
|
+
recentAppendQueued = false;
|
|
4879
5025
|
const workspaceSnapshot = getSnapshot(workspaces, "list");
|
|
4880
5026
|
const workspaceItems = workspaceSnapshot?.items ?? [];
|
|
4881
5027
|
const archivedSessionIds = workspaceSnapshot?.archivedSessionIds ?? [];
|
|
@@ -4971,11 +5117,11 @@ function apply(ctx) {
|
|
|
4971
5117
|
// token/速率取自 sessions.list 条目的投影(tokenUsage/sessionStats),
|
|
4972
5118
|
// 复用既有列表订阅,无新增轮询(R-01-009/AC-05、R-02-004/AC-02)。
|
|
4973
5119
|
const projection = snapshot?.byId?.[entry.id]?.projectionValues;
|
|
4974
|
-
const
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5120
|
+
const projectionStats = statsFromProjection(projection);
|
|
5121
|
+
if (entry.kind === "awaiting") {
|
|
5122
|
+
// 等待期间冻结运行停止前的最后已知统计;刷新/无留存时回退当前列表投影。
|
|
5123
|
+
Object.assign(entry, mergeRuntimeStats(detail?.lastRuntimeStats, projectionStats));
|
|
5124
|
+
}
|
|
4979
5125
|
// 委托周期锚点(R-01-009/AC-06):全部活动条目逐帧记账——锚点不因呈现翻转
|
|
4980
5126
|
// (委托期与 awaiting 互转)或瞬时不可见而丢失,仅 dispose 时整体清除;周期内
|
|
4981
5127
|
// 进度连续(含 settle 处理回合),周期外由宿主回合起点驱动(新回合归零)。
|
|
@@ -4991,13 +5137,15 @@ function apply(ctx) {
|
|
|
4991
5137
|
progressAnchorById.set(entry.id, anchor);
|
|
4992
5138
|
if (entry.kind === "running") {
|
|
4993
5139
|
const elapsedMs = Number.isFinite(anchor.anchor) ? Math.max(0, now - anchor.anchor) : null;
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
5140
|
+
Object.assign(entry, projectionStats, { elapsedMs });
|
|
5141
|
+
if (detail) {
|
|
5142
|
+
// 列表投影短暂缺字段时,不覆盖此前已知的有效统计。
|
|
5143
|
+
detail.lastRuntimeStats = mergeRuntimeStats(projectionStats, detail.lastRuntimeStats);
|
|
5144
|
+
}
|
|
4997
5145
|
// 回合进度:纯时间驱动 y = t/(t+k),半衰期每帧按最新实测速率现算,固定
|
|
4998
5146
|
// k 下单调性由函数本身保证;k 变化时允许进度随速率回落而回退(委托周期
|
|
4999
5147
|
// 连续、周期外回合切换归零,R-01-009/AC-06,C-014、C-044)。
|
|
5000
|
-
entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS }) });
|
|
5148
|
+
entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS }) });
|
|
5001
5149
|
}
|
|
5002
5150
|
}
|
|
5003
5151
|
// 历史区时间精化(R-01-010/AC-08、AC-09):从保留快照的 turnTimings 与已拉取的
|
|
@@ -5014,8 +5162,29 @@ function apply(ctx) {
|
|
|
5014
5162
|
}
|
|
5015
5163
|
if (detail.memoTurnEnd != null) turnEnds[id] = detail.memoTurnEnd;
|
|
5016
5164
|
}
|
|
5017
|
-
const
|
|
5018
|
-
|
|
5165
|
+
const recentCandidates = buildRecent(snapshot, workspaceItems, now, sessionDetailsById, archivedSessionIds, completeAcksById, delegatingIds, turnEnds);
|
|
5166
|
+
recentTotal = recentCandidates.length;
|
|
5167
|
+
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
5168
|
+
recentHasMore = recent.length < recentTotal;
|
|
5169
|
+
// 最近卡统计复用运行卡的列表投影口径;耗时从保留快照或已读 history 取最近完整回合,
|
|
5170
|
+
// 缺边界时仅为当前可见历史卡安排一次既有 history 补读(R-01-013/AC-12)。
|
|
5171
|
+
const recentDurationFallbackIds = new Set();
|
|
5172
|
+
for (const entry of recent) {
|
|
5173
|
+
const detail = sessionDetailsById.get(entry.id);
|
|
5174
|
+
const detailSnapshot = livenessById.get(entry.id)?.snapshot ?? detail?.snapshot ?? null;
|
|
5175
|
+
const elapsedMs = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history: detail?.history });
|
|
5176
|
+
const stats = statsFromProjection(snapshot?.byId?.[entry.id]?.projectionValues, elapsedMs);
|
|
5177
|
+
const retained = detail?.lastRuntimeStats;
|
|
5178
|
+
Object.assign(entry, stats, retained ? {
|
|
5179
|
+
outputTokens: retained.outputTokens ?? stats.outputTokens,
|
|
5180
|
+
inputTokens: retained.inputTokens ?? stats.inputTokens,
|
|
5181
|
+
cacheHitPct: retained.cacheHitPct ?? stats.cacheHitPct,
|
|
5182
|
+
rateTokS: retained.rateTokS ?? stats.rateTokS,
|
|
5183
|
+
} : null);
|
|
5184
|
+
if (elapsedMs === null) recentDurationFallbackIds.add(entry.id);
|
|
5185
|
+
}
|
|
5186
|
+
syncRecentTimeClock(recent.length > 0);
|
|
5187
|
+
// 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
|
|
5019
5188
|
// 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
|
|
5020
5189
|
const previewFallbackIds = new Set();
|
|
5021
5190
|
for (const entry of recent) {
|
|
@@ -5033,8 +5202,11 @@ function apply(ctx) {
|
|
|
5033
5202
|
if (!entry.userPreview || !entry.agentPreview) previewFallbackIds.add(entry.id);
|
|
5034
5203
|
entry.loadingPreviews = (!entry.userPreview || !entry.agentPreview) && historyLoads.has(entry.id);
|
|
5035
5204
|
}
|
|
5036
|
-
//
|
|
5037
|
-
const durationFallbackIds = new Set(
|
|
5205
|
+
// 补充数据读取优先级:当前会话最优先,活动区先于当前已显示历史页(区内按显示顺序)。
|
|
5206
|
+
const durationFallbackIds = new Set([
|
|
5207
|
+
...active.filter((entry) => entry.kind === "awaiting").map((entry) => entry.id),
|
|
5208
|
+
...recentDurationFallbackIds,
|
|
5209
|
+
]);
|
|
5038
5210
|
const detailIds = [...active, ...recent].map((entry) => entry.id);
|
|
5039
5211
|
detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
|
|
5040
5212
|
loadNativeDetails(detailIds, previewFallbackIds, durationFallbackIds);
|
|
@@ -5055,7 +5227,9 @@ function apply(ctx) {
|
|
|
5055
5227
|
// listState 参与签名:空列表从 pending/error → ready 时卡集合不变,若只比较卡片
|
|
5056
5228
|
// 会被提前返回冻结在「加载中」/「列表加载失败」;数量胶囊可见面变化同样需要
|
|
5057
5229
|
// 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
|
|
5058
|
-
|
|
5230
|
+
// 历史卡的相对活动时间随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
|
|
5231
|
+
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
5232
|
+
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);
|
|
5059
5233
|
if (sig === lastSig) return;
|
|
5060
5234
|
const hueByWorkspace = resolveWorkspaceHues(visibleEntries.map((entry) => entry.workspaceKey));
|
|
5061
5235
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
|
@@ -5104,6 +5278,11 @@ function apply(ctx) {
|
|
|
5104
5278
|
if (recentSection !== null) {
|
|
5105
5279
|
recentSection.hidden = listState === "ready" && recent.length === 0;
|
|
5106
5280
|
ensureListStatus(recentSection, listState !== "ready" && recent.length === 0, listState === "loading" ? "加载中…" : "列表加载失败", { loading: listState === "loading" });
|
|
5281
|
+
const recentMore = recentSection.querySelector(".dap-recent-more");
|
|
5282
|
+
if (recentMore !== null) {
|
|
5283
|
+
recentMore.hidden = !recentHasMore;
|
|
5284
|
+
recentMore.disabled = recentAppendQueued;
|
|
5285
|
+
}
|
|
5107
5286
|
}
|
|
5108
5287
|
runMoveGhosts(movePlans);
|
|
5109
5288
|
if (shiftRects !== null) runShiftAnimations(shiftRects);
|
|
@@ -5164,6 +5343,8 @@ function apply(ctx) {
|
|
|
5164
5343
|
const headerExpanded = collapsed ? "false" : "true";
|
|
5165
5344
|
if (headerEl !== null && headerEl.getAttribute("aria-expanded") !== headerExpanded)
|
|
5166
5345
|
headerEl.setAttribute("aria-expanded", headerExpanded);
|
|
5346
|
+
// 历史分页只由底部按钮显式触发;追加只排队一帧,避免重复点击造成重复增长
|
|
5347
|
+
//(R-01-019/AC-01~AC-03)。
|
|
5167
5348
|
|
|
5168
5349
|
// 渲染签名在整轮 DOM 写入全部成功后提交:任何一步失败都保留下一轮
|
|
5169
5350
|
// 同步重试的机会,避免故障被签名吞掉后卡片永久滞留(R-01-013/AC-02)。
|
|
@@ -5348,6 +5529,7 @@ function apply(ctx) {
|
|
|
5348
5529
|
window.removeEventListener("pageshow", onPageShow);
|
|
5349
5530
|
completeAcksById.clear();
|
|
5350
5531
|
if (clockTimer !== null) clearInterval(clockTimer);
|
|
5532
|
+
if (recentTimeTimer !== null) clearInterval(recentTimeTimer);
|
|
5351
5533
|
if (e2eListReleaseTimer !== null) clearTimeout(e2eListReleaseTimer);
|
|
5352
5534
|
for (const [timer, resolve] of e2eModelDelayWaiters) {
|
|
5353
5535
|
clearTimeout(timer);
|