dsh-activity-pane 0.3.0 → 0.6.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 +367 -104
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/scripts/acceptance.mjs +9 -4
- package/scripts/check.mjs +322 -60
- package/src/client.mjs +180 -68
- package/src/core.mjs +150 -36
- package/src/navigation.mjs +37 -0
package/src/client.mjs
CHANGED
|
@@ -467,23 +467,41 @@ const CSS = `
|
|
|
467
467
|
/* 复审修复(C-040、C-043):状态点光晕三类同强度、仅换色相——阻塞金、完成绿与错误红光晕一致。 */
|
|
468
468
|
/* 工作区徽标「图标+文本」双段:文件夹图标与左边栏工作区条目同源(R-01-003/AC-06);
|
|
469
469
|
名称字号不低于 10.5px(AC-07),行高保持 14px 以维持胶囊与卡片高度。
|
|
470
|
-
着色(AC-08~AC-
|
|
471
|
-
|
|
472
|
-
|
|
470
|
+
着色(AC-08~AC-12):核心映射提供 12 个前景身份槽与每槽 3 个背景变体;
|
|
471
|
+
深色/浅色主题分别使用前景与背景槽位的 L/C 参数,文字使用前景调色板色,
|
|
472
|
+
底色使用独立背景源色、描边使用前景调色板色,分别在 OKLCH 空间按变体混合强度呈现;前景与背景保持同色相族。 */
|
|
473
473
|
[data-dsh-activity-pane] .dap-workspace {
|
|
474
474
|
width: fit-content; max-width: 100%; display: flex; align-items: center; gap: 3px;
|
|
475
475
|
overflow: hidden;
|
|
476
476
|
font-size: 10.5px; line-height: 14px;
|
|
477
|
-
--dap-workspace-color: oklch(
|
|
477
|
+
--dap-workspace-color: oklch(
|
|
478
|
+
var(--dap-workspace-dark-l, 0.78)
|
|
479
|
+
var(--dap-workspace-dark-c, 0.16)
|
|
480
|
+
var(--dap-workspace-hue, 235)
|
|
481
|
+
);
|
|
482
|
+
--dap-workspace-background-color: oklch(
|
|
483
|
+
var(--dap-workspace-bg-dark-l, 0.38)
|
|
484
|
+
var(--dap-workspace-bg-dark-c, 0.06)
|
|
485
|
+
var(--dap-workspace-hue, 235)
|
|
486
|
+
);
|
|
478
487
|
color: var(--dap-workspace-color);
|
|
479
|
-
background: color-mix(in oklch, var(--dap-workspace-color)
|
|
480
|
-
border: 1px solid color-mix(in oklch, var(--dap-workspace-color)
|
|
488
|
+
background: color-mix(in oklch, var(--dap-workspace-background-color) var(--dap-workspace-bg-dark-mix, 31%), transparent);
|
|
489
|
+
border: 1px solid color-mix(in oklch, var(--dap-workspace-color) var(--dap-workspace-bg-dark-border-mix, 52%), transparent);
|
|
481
490
|
border-radius: 999px; padding: 0 7px;
|
|
482
491
|
}
|
|
483
492
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
484
|
-
--dap-workspace-color: oklch(
|
|
485
|
-
|
|
486
|
-
|
|
493
|
+
--dap-workspace-color: oklch(
|
|
494
|
+
var(--dap-workspace-light-l, 0.48)
|
|
495
|
+
var(--dap-workspace-light-c, 0.15)
|
|
496
|
+
var(--dap-workspace-hue, 235)
|
|
497
|
+
);
|
|
498
|
+
--dap-workspace-background-color: oklch(
|
|
499
|
+
var(--dap-workspace-bg-light-l, 0.84)
|
|
500
|
+
var(--dap-workspace-bg-light-c, 0.06)
|
|
501
|
+
var(--dap-workspace-hue, 235)
|
|
502
|
+
);
|
|
503
|
+
background: color-mix(in oklch, var(--dap-workspace-background-color) var(--dap-workspace-bg-light-mix, 27%), transparent);
|
|
504
|
+
border-color: color-mix(in oklch, var(--dap-workspace-color) var(--dap-workspace-bg-light-border-mix, 42%), transparent);
|
|
487
505
|
}
|
|
488
506
|
[data-dsh-activity-pane] .dap-workspace-icon { flex: none; display: inline-flex; }
|
|
489
507
|
[data-dsh-activity-pane] .dap-workspace-icon svg { display: block; }
|
|
@@ -521,14 +539,10 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
521
539
|
display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
|
|
522
540
|
min-width: 0; margin: 2px 0 0;
|
|
523
541
|
}
|
|
524
|
-
/*
|
|
542
|
+
/* 等待类型胶囊独立同行:耗时回到统计行最右侧,不参与等待脉冲。 */
|
|
525
543
|
[data-dsh-activity-pane] .dap-await-head {
|
|
526
544
|
display: flex; align-items: center; gap: 8px; align-self: stretch; min-width: 0;
|
|
527
545
|
}
|
|
528
|
-
[data-dsh-activity-pane] .dap-await-head .dap-token-time {
|
|
529
|
-
margin-left: auto; flex: none; font-size: 10px; line-height: 14px;
|
|
530
|
-
color: #8f9aaa; font-variant-numeric: tabular-nums;
|
|
531
|
-
}
|
|
532
546
|
[data-dsh-activity-pane] .dap-note-row {
|
|
533
547
|
display: flex; align-items: center; gap: 6px; min-width: 0; align-self: stretch;
|
|
534
548
|
}
|
|
@@ -547,7 +561,7 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
547
561
|
color: color-mix(in srgb, currentColor 82%, transparent);
|
|
548
562
|
background: color-mix(in srgb, currentColor 14%, transparent);
|
|
549
563
|
}
|
|
550
|
-
/*
|
|
564
|
+
/* 运行卡与最近卡统计(对齐 answer-pet 卡片;MIT 参考,见 README)。 */
|
|
551
565
|
[data-dsh-activity-pane] .dap-progress {
|
|
552
566
|
display: flex; align-items: center; gap: 7px; min-width: 0;
|
|
553
567
|
}
|
|
@@ -1030,16 +1044,18 @@ function apply(ctx) {
|
|
|
1030
1044
|
/** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
|
|
1031
1045
|
let pulseSignature = "";
|
|
1032
1046
|
// E2E-only deterministic seams(T-089/T-090):显式 URL fragment 最多把本页首次列表 ready 呈现
|
|
1033
|
-
// 或正式 models RPC 延后 1s
|
|
1047
|
+
// 或正式 models RPC 延后 1s;列表延迟从首个窗格 render 开始计时,避免宿主启动耗时
|
|
1048
|
+
// 把 pending 观察窗口提前消耗;默认路径为 0,不伪造服务响应、快照或生产时序。
|
|
1034
1049
|
const e2eParams = new URLSearchParams(window.location.hash.slice(1));
|
|
1035
1050
|
const requestedListDelay = Number(e2eParams.get("dap-e2e-list-delay"));
|
|
1036
|
-
const
|
|
1037
|
-
?
|
|
1051
|
+
const e2eListDelayMs = Number.isFinite(requestedListDelay) && requestedListDelay > 0
|
|
1052
|
+
? Math.min(requestedListDelay, 1_000)
|
|
1038
1053
|
: 0;
|
|
1039
1054
|
const requestedModelDelay = Number(e2eParams.get("dap-e2e-model-delay"));
|
|
1040
1055
|
const e2eModelDelayMs = Number.isFinite(requestedModelDelay) && requestedModelDelay > 0
|
|
1041
1056
|
? Math.min(requestedModelDelay, 1_000)
|
|
1042
1057
|
: 0;
|
|
1058
|
+
let e2eListReadyAt = 0;
|
|
1043
1059
|
let e2eListReleaseTimer = null;
|
|
1044
1060
|
const e2eModelDelayWaiters = new Map();
|
|
1045
1061
|
function delayedModelCall(call) {
|
|
@@ -1060,6 +1076,9 @@ function apply(ctx) {
|
|
|
1060
1076
|
let paneWidth = readStoredPaneWidth();
|
|
1061
1077
|
/** 用户最近一次激活的卡片 id;打开重试链被更新的激活意图取代即取消。 */
|
|
1062
1078
|
let lastActivatedId = null;
|
|
1079
|
+
/** 最近一次已处理的当前卡片;同一卡片的运行时重绘不反复打断用户手动滚动。 */
|
|
1080
|
+
let autoScrolledCurrentId = null;
|
|
1081
|
+
let autoScrolledCurrentCard = null;
|
|
1063
1082
|
/** 完成确认状态(R-01-002/AC-03、AC-10~AC-12):id → { lastTurnEnd, ackedAt },
|
|
1064
1083
|
* 来自宿主侧 ack 通道(SSE 全量快照),随每次推送整体替换。 */
|
|
1065
1084
|
const completeAcksById = new Map();
|
|
@@ -1086,7 +1105,7 @@ function apply(ctx) {
|
|
|
1086
1105
|
const livenessById = new Map();
|
|
1087
1106
|
/** 委托周期进度锚点记账:id → progressAnchor 状态(R-01-009/AC-06)。 */
|
|
1088
1107
|
const progressAnchorById = new Map();
|
|
1089
|
-
/**
|
|
1108
|
+
/** 订阅停止后保留最近快照与最后已知运行统计,供 awaiting/recent 卡继续显示上下文。 */
|
|
1090
1109
|
const sessionDetailsById = new Map();
|
|
1091
1110
|
/** 会话跳转的单一重试链;避免重复点击叠加 refresh/timer。 */
|
|
1092
1111
|
const openRetryStates = new Map();
|
|
@@ -1761,7 +1780,10 @@ function apply(ctx) {
|
|
|
1761
1780
|
const agentLabel = makeEl("span", "dap-history-label");
|
|
1762
1781
|
agentLabel.textContent = "助手";
|
|
1763
1782
|
agentLine.append(agentIcon, agentLabel, makeEl("span", "dap-history-separator"), makeEl("span", "dap-history-text"));
|
|
1764
|
-
|
|
1783
|
+
const statsRow = makeEl("div", "dap-token-stats");
|
|
1784
|
+
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
1785
|
+
statsRow.hidden = true;
|
|
1786
|
+
return [head, row, userLine, agentLine, statsRow, makeEl("div", "dap-note")];
|
|
1765
1787
|
}
|
|
1766
1788
|
if (kind === "awaiting") {
|
|
1767
1789
|
const row = makeEl("div", "dap-row");
|
|
@@ -1775,10 +1797,13 @@ function apply(ctx) {
|
|
|
1775
1797
|
const noteRow = makeEl("div", "dap-note-row");
|
|
1776
1798
|
noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
|
|
1777
1799
|
const awaitHead = makeEl("div", "dap-await-head");
|
|
1778
|
-
awaitHead.append(capsule
|
|
1800
|
+
awaitHead.append(capsule);
|
|
1801
|
+
const statsRow = makeEl("div", "dap-token-stats");
|
|
1802
|
+
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
1803
|
+
statsRow.hidden = true;
|
|
1779
1804
|
const foot = makeEl("div", "dap-foot");
|
|
1780
1805
|
foot.append(awaitHead, noteRow);
|
|
1781
|
-
return [head, row, makeEl("div", "dap-trace"), foot];
|
|
1806
|
+
return [head, row, makeEl("div", "dap-trace"), statsRow, foot];
|
|
1782
1807
|
}
|
|
1783
1808
|
const row = makeEl("div", "dap-row");
|
|
1784
1809
|
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
@@ -2139,14 +2164,19 @@ function apply(ctx) {
|
|
|
2139
2164
|
}
|
|
2140
2165
|
}
|
|
2141
2166
|
|
|
2142
|
-
/**
|
|
2167
|
+
/** 统计行渲染:运行卡、等待卡与最近卡写入速率、token 与耗时,旧骨架缺节点时就地补齐。 */
|
|
2143
2168
|
function renderTokenStats(el, entry) {
|
|
2144
2169
|
let stats = el.querySelector(".dap-token-stats");
|
|
2145
2170
|
if (stats === null) {
|
|
2146
2171
|
stats = makeEl("div", "dap-token-stats");
|
|
2147
2172
|
stats.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
2148
2173
|
stats.hidden = true;
|
|
2149
|
-
el.
|
|
2174
|
+
const foot = entry.kind === "awaiting" ? el.querySelector(".dap-foot") : null;
|
|
2175
|
+
if (foot !== null) el.insertBefore(stats, foot);
|
|
2176
|
+
else el.append(stats);
|
|
2177
|
+
} else if (entry.kind === "awaiting") {
|
|
2178
|
+
const foot = el.querySelector(".dap-foot");
|
|
2179
|
+
if (foot !== null && stats.nextElementSibling !== foot) el.insertBefore(stats, foot);
|
|
2150
2180
|
}
|
|
2151
2181
|
let mainTextEl = stats.querySelector(".dap-token-main");
|
|
2152
2182
|
let timeEl = stats.querySelector(".dap-token-time");
|
|
@@ -2168,25 +2198,9 @@ function apply(ctx) {
|
|
|
2168
2198
|
if (stats.hidden !== statsHidden) stats.hidden = statsHidden;
|
|
2169
2199
|
}
|
|
2170
2200
|
|
|
2171
|
-
/**
|
|
2172
|
-
function
|
|
2173
|
-
|
|
2174
|
-
if (foot === null) return;
|
|
2175
|
-
let head = foot.querySelector(".dap-await-head");
|
|
2176
|
-
if (head === null) {
|
|
2177
|
-
const capsule = foot.querySelector(".dap-capsule");
|
|
2178
|
-
if (capsule === null) return;
|
|
2179
|
-
head = makeEl("div", "dap-await-head");
|
|
2180
|
-
capsule.replaceWith(head);
|
|
2181
|
-
head.append(capsule);
|
|
2182
|
-
}
|
|
2183
|
-
let time = head.querySelector(".dap-token-time");
|
|
2184
|
-
if (time === null) {
|
|
2185
|
-
time = makeEl("span", "dap-token-time");
|
|
2186
|
-
head.append(time);
|
|
2187
|
-
}
|
|
2188
|
-
const text = Number.isFinite(entry.elapsedMs) && entry.elapsedMs >= 0 ? fmtElapsedMs(entry.elapsedMs) : "";
|
|
2189
|
-
if (time.textContent !== text) time.textContent = text;
|
|
2201
|
+
/** 旧热装等待骨架清理:耗时已迁回统计行,去掉胶囊同行残留的时长节点。 */
|
|
2202
|
+
function removeAwaitingHeadDuration(el) {
|
|
2203
|
+
el.querySelector(".dap-await-head .dap-token-time")?.remove();
|
|
2190
2204
|
}
|
|
2191
2205
|
|
|
2192
2206
|
/** 时间线区加载指示:数据在途且尚无工作项时显示活动图标行(R-01-014/AC-02)。 */
|
|
@@ -2213,22 +2227,40 @@ function apply(ctx) {
|
|
|
2213
2227
|
renderTrace(container, entry.timeline, { lastOnly });
|
|
2214
2228
|
}
|
|
2215
2229
|
|
|
2216
|
-
function renderCardInto(el, entry,
|
|
2230
|
+
function renderCardInto(el, entry, colorByWorkspace) {
|
|
2217
2231
|
const workspaceLabel = el.querySelector(".dap-workspace");
|
|
2218
2232
|
if (workspaceLabel !== null) {
|
|
2219
2233
|
const workspaceText = workspaceLabel.querySelector(".dap-workspace-text");
|
|
2220
|
-
|
|
2234
|
+
const color = colorByWorkspace.get(entry.workspaceKey);
|
|
2235
|
+
const foreground = color?.foreground;
|
|
2236
|
+
const background = color?.background;
|
|
2237
|
+
const colorVariables = {
|
|
2238
|
+
"--dap-workspace-hue": foreground?.hue,
|
|
2239
|
+
"--dap-workspace-dark-l": foreground?.darkL,
|
|
2240
|
+
"--dap-workspace-dark-c": foreground?.darkC,
|
|
2241
|
+
"--dap-workspace-light-l": foreground?.lightL,
|
|
2242
|
+
"--dap-workspace-light-c": foreground?.lightC,
|
|
2243
|
+
"--dap-workspace-bg-dark-l": background?.dark?.l,
|
|
2244
|
+
"--dap-workspace-bg-dark-c": background?.dark?.c,
|
|
2245
|
+
"--dap-workspace-bg-dark-mix": background?.dark?.mix,
|
|
2246
|
+
"--dap-workspace-bg-dark-border-mix": background?.dark?.borderMix,
|
|
2247
|
+
"--dap-workspace-bg-light-l": background?.light?.l,
|
|
2248
|
+
"--dap-workspace-bg-light-c": background?.light?.c,
|
|
2249
|
+
"--dap-workspace-bg-light-mix": background?.light?.mix,
|
|
2250
|
+
"--dap-workspace-bg-light-border-mix": background?.light?.borderMix,
|
|
2251
|
+
};
|
|
2252
|
+
if (entry.workspaceTitle !== "" && foreground !== undefined && background !== undefined) {
|
|
2221
2253
|
if (workspaceText !== null) restoreTextField(workspaceText, entry.workspaceTitle);
|
|
2222
|
-
const
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
if (
|
|
2226
|
-
else workspaceLabel.style.setProperty(
|
|
2254
|
+
for (const [property, value] of Object.entries(colorVariables)) {
|
|
2255
|
+
const next = value === undefined ? "" : String(value);
|
|
2256
|
+
if (workspaceLabel.style.getPropertyValue(property) === next) continue;
|
|
2257
|
+
if (next === "") workspaceLabel.style.removeProperty(property);
|
|
2258
|
+
else workspaceLabel.style.setProperty(property, next);
|
|
2227
2259
|
}
|
|
2228
2260
|
workspaceLabel.removeAttribute("hidden");
|
|
2229
2261
|
} else {
|
|
2230
2262
|
if (workspaceText !== null) restoreTextField(workspaceText, "");
|
|
2231
|
-
workspaceLabel.style.removeProperty(
|
|
2263
|
+
for (const property of Object.keys(colorVariables)) workspaceLabel.style.removeProperty(property);
|
|
2232
2264
|
workspaceLabel.setAttribute("hidden", "");
|
|
2233
2265
|
}
|
|
2234
2266
|
}
|
|
@@ -2321,12 +2353,14 @@ function apply(ctx) {
|
|
|
2321
2353
|
restoreTextField(text, previews[i]);
|
|
2322
2354
|
}
|
|
2323
2355
|
}
|
|
2356
|
+
renderTokenStats(el, entry);
|
|
2324
2357
|
}
|
|
2325
2358
|
|
|
2326
2359
|
if (entry.kind === "awaiting") {
|
|
2327
2360
|
const traceContainer = el.querySelector(".dap-trace");
|
|
2328
2361
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
2329
|
-
|
|
2362
|
+
removeAwaitingHeadDuration(el);
|
|
2363
|
+
renderTokenStats(el, entry);
|
|
2330
2364
|
const confirm = el.querySelector(".dap-confirm");
|
|
2331
2365
|
if (confirm !== null) {
|
|
2332
2366
|
// 激活锚点:只在结构重建时绑一次(卡片按 id 复用,kind 变化会重建骨架)。
|
|
@@ -2605,7 +2639,7 @@ function apply(ctx) {
|
|
|
2605
2639
|
|
|
2606
2640
|
/** 渲染某一张卡片进指定列表容器(活动/历史通用)。index 是条目在卡片序列中的
|
|
2607
2641
|
* 序号,offset 是容器内首个卡片前的非卡片子节点数(活动区有轨道层、历史区有段头)。 */
|
|
2608
|
-
function renderCardIntoList(list, entry, reuseMap, index, offset,
|
|
2642
|
+
function renderCardIntoList(list, entry, reuseMap, index, offset, colorByWorkspace) {
|
|
2609
2643
|
let rec = reuseMap.get(entry.id);
|
|
2610
2644
|
if (rec === undefined) {
|
|
2611
2645
|
const el = document.createElement("div");
|
|
@@ -2662,7 +2696,7 @@ function apply(ctx) {
|
|
|
2662
2696
|
recentTimeText ? "," + recentTimeText : ""
|
|
2663
2697
|
}`,
|
|
2664
2698
|
);
|
|
2665
|
-
renderCardInto(rec.el, entry,
|
|
2699
|
+
renderCardInto(rec.el, entry, colorByWorkspace);
|
|
2666
2700
|
// 只有顺序/归属真正变化时才移动 DOM:每次渲染无条件 appendChild 会把所有
|
|
2667
2701
|
// 卡片瞬时移除再插回——按下/抬起之间经过的移动让浏览器取消 click;焦点卡
|
|
2668
2702
|
// 被瞬时断开而失焦;悬停卡的 :hover 也随之丢失且不再补发。会话活跃期间
|
|
@@ -2672,6 +2706,37 @@ function apply(ctx) {
|
|
|
2672
2706
|
return true;
|
|
2673
2707
|
}
|
|
2674
2708
|
|
|
2709
|
+
/** 原生侧栏切换当前会话后,只把已呈现且未完整可见的当前卡片滚入窗格;降低动效偏好时不平滑。 */
|
|
2710
|
+
function ensureCurrentCardVisible(scroll, currentId) {
|
|
2711
|
+
const id = currentId === null || currentId === undefined ? null : String(currentId);
|
|
2712
|
+
if (id === null || id === "") {
|
|
2713
|
+
autoScrolledCurrentId = null;
|
|
2714
|
+
autoScrolledCurrentCard = null;
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
// 当前会话卡片只有在活动列表中才参与自动定位;完成确认后同一 current
|
|
2718
|
+
// 会话会进入历史区,不能因仍带 data-current 把窗格滚到历史卡片。
|
|
2719
|
+
const card = scroll?.querySelector?.(`.${LIST_CLASS} .${CARD_CLASS}[data-current]`) ?? null;
|
|
2720
|
+
if (
|
|
2721
|
+
card === null ||
|
|
2722
|
+
card.dataset.sessionId !== id ||
|
|
2723
|
+
scroll.clientHeight <= 0 ||
|
|
2724
|
+
card.getBoundingClientRect().height <= 0
|
|
2725
|
+
)
|
|
2726
|
+
return;
|
|
2727
|
+
if (autoScrolledCurrentId === id && autoScrolledCurrentCard === card) return;
|
|
2728
|
+
if (scrollCardIntoView(scroll, card, prefersReducedMotion() ? "auto" : "smooth")) {
|
|
2729
|
+
autoScrolledCurrentId = id;
|
|
2730
|
+
autoScrolledCurrentCard = card;
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
const viewport = scroll.getBoundingClientRect();
|
|
2734
|
+
const rect = card.getBoundingClientRect();
|
|
2735
|
+
if (rect.height <= viewport.height && (rect.top < viewport.top || rect.bottom > viewport.bottom)) return;
|
|
2736
|
+
autoScrolledCurrentId = id;
|
|
2737
|
+
autoScrolledCurrentCard = card;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2675
2740
|
function pruneCards(reuseMap, alive) {
|
|
2676
2741
|
for (const [id, rec] of reuseMap) {
|
|
2677
2742
|
if (alive.has(id)) continue;
|
|
@@ -2855,6 +2920,8 @@ function apply(ctx) {
|
|
|
2855
2920
|
recentTotal = 0;
|
|
2856
2921
|
recentHasMore = false;
|
|
2857
2922
|
recentAppendQueued = false;
|
|
2923
|
+
autoScrolledCurrentId = null;
|
|
2924
|
+
autoScrolledCurrentCard = null;
|
|
2858
2925
|
// 旧窗格已脱离文档:其在飞平移的 transitionend 不再触发,逐元素取消避免残留。
|
|
2859
2926
|
for (const el of [...shiftCleanups.keys()]) cancelShift(el);
|
|
2860
2927
|
}
|
|
@@ -2863,6 +2930,7 @@ function apply(ctx) {
|
|
|
2863
2930
|
const recentSection = pane.querySelector(`.${RECENT_CLASS}`);
|
|
2864
2931
|
if (activeList === null || recentSection === null) return;
|
|
2865
2932
|
|
|
2933
|
+
if (e2eListReadyAt === 0 && e2eListDelayMs > 0) e2eListReadyAt = Date.now() + e2eListDelayMs;
|
|
2866
2934
|
let snapshot = getSnapshot(sessions, "list");
|
|
2867
2935
|
if (e2eListReadyAt > Date.now() && snapshot?.phase !== "error") {
|
|
2868
2936
|
snapshot = { phase: "pending" };
|
|
@@ -2887,7 +2955,7 @@ function apply(ctx) {
|
|
|
2887
2955
|
for (const [id, state] of progressAnchorById) {
|
|
2888
2956
|
if (delegationActive(state, now)) delegatingIds.add(id);
|
|
2889
2957
|
}
|
|
2890
|
-
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds);
|
|
2958
|
+
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds);
|
|
2891
2959
|
// 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
|
|
2892
2960
|
// 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
|
|
2893
2961
|
const runLikeIds = new Set(
|
|
@@ -2971,11 +3039,11 @@ function apply(ctx) {
|
|
|
2971
3039
|
// token/速率取自 sessions.list 条目的投影(tokenUsage/sessionStats),
|
|
2972
3040
|
// 复用既有列表订阅,无新增轮询(R-01-009/AC-05、R-02-004/AC-02)。
|
|
2973
3041
|
const projection = snapshot?.byId?.[entry.id]?.projectionValues;
|
|
2974
|
-
const
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
3042
|
+
const projectionStats = statsFromProjection(projection);
|
|
3043
|
+
if (entry.kind === "awaiting") {
|
|
3044
|
+
// 等待期间冻结运行停止前的最后已知统计;刷新/无留存时回退当前列表投影。
|
|
3045
|
+
Object.assign(entry, mergeRuntimeStats(detail?.lastRuntimeStats, projectionStats));
|
|
3046
|
+
}
|
|
2979
3047
|
// 委托周期锚点(R-01-009/AC-06):全部活动条目逐帧记账——锚点不因呈现翻转
|
|
2980
3048
|
// (委托期与 awaiting 互转)或瞬时不可见而丢失,仅 dispose 时整体清除;周期内
|
|
2981
3049
|
// 进度连续(含 settle 处理回合),周期外由宿主回合起点驱动(新回合归零)。
|
|
@@ -2991,13 +3059,15 @@ function apply(ctx) {
|
|
|
2991
3059
|
progressAnchorById.set(entry.id, anchor);
|
|
2992
3060
|
if (entry.kind === "running") {
|
|
2993
3061
|
const elapsedMs = Number.isFinite(anchor.anchor) ? Math.max(0, now - anchor.anchor) : null;
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
3062
|
+
Object.assign(entry, projectionStats, { elapsedMs });
|
|
3063
|
+
if (detail) {
|
|
3064
|
+
// 列表投影短暂缺字段时,不覆盖此前已知的有效统计。
|
|
3065
|
+
detail.lastRuntimeStats = mergeRuntimeStats(projectionStats, detail.lastRuntimeStats);
|
|
3066
|
+
}
|
|
2997
3067
|
// 回合进度:纯时间驱动 y = t/(t+k),半衰期每帧按最新实测速率现算,固定
|
|
2998
3068
|
// k 下单调性由函数本身保证;k 变化时允许进度随速率回落而回退(委托周期
|
|
2999
3069
|
// 连续、周期外回合切换归零,R-01-009/AC-06,C-014、C-044)。
|
|
3000
|
-
entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS }) });
|
|
3070
|
+
entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS }) });
|
|
3001
3071
|
}
|
|
3002
3072
|
}
|
|
3003
3073
|
// 历史区时间精化(R-01-010/AC-08、AC-09):从保留快照的 turnTimings 与已拉取的
|
|
@@ -3018,6 +3088,23 @@ function apply(ctx) {
|
|
|
3018
3088
|
recentTotal = recentCandidates.length;
|
|
3019
3089
|
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
3020
3090
|
recentHasMore = recent.length < recentTotal;
|
|
3091
|
+
// 最近卡统计复用运行卡的列表投影口径;耗时从保留快照或已读 history 取最近完整回合,
|
|
3092
|
+
// 缺边界时仅为当前可见历史卡安排一次既有 history 补读(R-01-013/AC-12)。
|
|
3093
|
+
const recentDurationFallbackIds = new Set();
|
|
3094
|
+
for (const entry of recent) {
|
|
3095
|
+
const detail = sessionDetailsById.get(entry.id);
|
|
3096
|
+
const detailSnapshot = livenessById.get(entry.id)?.snapshot ?? detail?.snapshot ?? null;
|
|
3097
|
+
const elapsedMs = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history: detail?.history });
|
|
3098
|
+
const stats = statsFromProjection(snapshot?.byId?.[entry.id]?.projectionValues, elapsedMs);
|
|
3099
|
+
const retained = detail?.lastRuntimeStats;
|
|
3100
|
+
Object.assign(entry, stats, retained ? {
|
|
3101
|
+
outputTokens: retained.outputTokens ?? stats.outputTokens,
|
|
3102
|
+
inputTokens: retained.inputTokens ?? stats.inputTokens,
|
|
3103
|
+
cacheHitPct: retained.cacheHitPct ?? stats.cacheHitPct,
|
|
3104
|
+
rateTokS: retained.rateTokS ?? stats.rateTokS,
|
|
3105
|
+
} : null);
|
|
3106
|
+
if (elapsedMs === null) recentDurationFallbackIds.add(entry.id);
|
|
3107
|
+
}
|
|
3021
3108
|
syncRecentTimeClock(recent.length > 0);
|
|
3022
3109
|
// 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
|
|
3023
3110
|
// 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
|
|
@@ -3038,7 +3125,10 @@ function apply(ctx) {
|
|
|
3038
3125
|
entry.loadingPreviews = (!entry.userPreview || !entry.agentPreview) && historyLoads.has(entry.id);
|
|
3039
3126
|
}
|
|
3040
3127
|
// 补充数据读取优先级:当前会话最优先,活动区先于当前已显示历史页(区内按显示顺序)。
|
|
3041
|
-
const durationFallbackIds = new Set(
|
|
3128
|
+
const durationFallbackIds = new Set([
|
|
3129
|
+
...active.filter((entry) => entry.kind === "awaiting").map((entry) => entry.id),
|
|
3130
|
+
...recentDurationFallbackIds,
|
|
3131
|
+
]);
|
|
3042
3132
|
const detailIds = [...active, ...recent].map((entry) => entry.id);
|
|
3043
3133
|
detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
|
|
3044
3134
|
loadNativeDetails(detailIds, previewFallbackIds, durationFallbackIds);
|
|
@@ -3063,12 +3153,32 @@ function apply(ctx) {
|
|
|
3063
3153
|
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
3064
3154
|
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);
|
|
3065
3155
|
if (sig === lastSig) return;
|
|
3066
|
-
const
|
|
3156
|
+
const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
|
|
3067
3157
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
|
3068
3158
|
const migrations = [
|
|
3069
3159
|
...movedToRecentIds(prevRenderedActiveIds, active, recent).map((id) => ({ id, from: cardsById, to: recentCardsById })),
|
|
3070
3160
|
...movedToActiveIds(prevRenderedRecentIds, active, recent).map((id) => ({ id, from: recentCardsById, to: cardsById })),
|
|
3071
3161
|
];
|
|
3162
|
+
// 活动卡带焦点迁入历史时,把键盘焦点交给其上一个仍在活动区的卡片;
|
|
3163
|
+
// 若迁移卡已在活动区首位,则退到下一张,避免焦点落到历史区或 body。
|
|
3164
|
+
let focusAfterMigrationId = null;
|
|
3165
|
+
const focusedElement = document.activeElement;
|
|
3166
|
+
const activeIds = new Set(active.map((entry) => String(entry.id)));
|
|
3167
|
+
for (const { id, from } of migrations) {
|
|
3168
|
+
if (from !== cardsById) continue;
|
|
3169
|
+
const source = from.get(id)?.el;
|
|
3170
|
+
if (source === undefined || !source.contains(focusedElement)) continue;
|
|
3171
|
+
const activeCards = [...activeList.querySelectorAll(`.${CARD_CLASS}`)];
|
|
3172
|
+
const sourceIndex = activeCards.indexOf(source);
|
|
3173
|
+
if (sourceIndex < 0) continue;
|
|
3174
|
+
const target = activeCards
|
|
3175
|
+
.slice(0, sourceIndex)
|
|
3176
|
+
.reverse()
|
|
3177
|
+
.find((card) => activeIds.has(String(card.dataset.sessionId)))
|
|
3178
|
+
?? activeCards.slice(sourceIndex + 1).find((card) => activeIds.has(String(card.dataset.sessionId)));
|
|
3179
|
+
focusAfterMigrationId = target?.dataset.sessionId ?? null;
|
|
3180
|
+
break;
|
|
3181
|
+
}
|
|
3072
3182
|
const movePlans = prepareMoveGhosts(migrations);
|
|
3073
3183
|
// 迁移帧内位置受影响的其它卡片与历史区段头:DOM 写入前量取当前视觉矩形
|
|
3074
3184
|
// (R-01-010/AC-10);reduced-motion 下 movePlans 为空,FLIP 量取整体跳过。
|
|
@@ -3082,7 +3192,7 @@ function apply(ctx) {
|
|
|
3082
3192
|
const aliveActive = new Set();
|
|
3083
3193
|
for (const [index, entry] of active.entries()) {
|
|
3084
3194
|
try {
|
|
3085
|
-
renderCardIntoList(activeList, entry, cardsById, index, 1,
|
|
3195
|
+
renderCardIntoList(activeList, entry, cardsById, index, 1, colorByWorkspace);
|
|
3086
3196
|
} catch (error) {
|
|
3087
3197
|
renderOk = false;
|
|
3088
3198
|
logCardRenderError(entry.id, error);
|
|
@@ -3099,7 +3209,7 @@ function apply(ctx) {
|
|
|
3099
3209
|
// 历史区容器首个子节点是段头(.dap-recent-head),卡片从 offset 1 开始。
|
|
3100
3210
|
for (const [index, entry] of recent.entries()) {
|
|
3101
3211
|
try {
|
|
3102
|
-
renderCardIntoList(recentSection, entry, recentCardsById, index, 1,
|
|
3212
|
+
renderCardIntoList(recentSection, entry, recentCardsById, index, 1, colorByWorkspace);
|
|
3103
3213
|
} catch (error) {
|
|
3104
3214
|
renderOk = false;
|
|
3105
3215
|
logCardRenderError(entry.id, error);
|
|
@@ -3118,6 +3228,8 @@ function apply(ctx) {
|
|
|
3118
3228
|
}
|
|
3119
3229
|
runMoveGhosts(movePlans);
|
|
3120
3230
|
if (shiftRects !== null) runShiftAnimations(shiftRects);
|
|
3231
|
+
ensureCurrentCardVisible(pane.querySelector(".dap-scroll"), snapshot?.current ?? null);
|
|
3232
|
+
if (focusAfterMigrationId !== null) cardsById.get(focusAfterMigrationId)?.el.focus();
|
|
3121
3233
|
// 区域已有条目但列表仍在途时,在区头部显示行内加载指示(R-01-014/AC-01)。
|
|
3122
3234
|
const headerEl = pane.querySelector(".dap-header");
|
|
3123
3235
|
const recentHeadEl = recentSection?.querySelector(".dap-recent-head") ?? null;
|