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/README.md
CHANGED
|
@@ -48,11 +48,11 @@ 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.
|
|
55
|
-
- [x] **Current work and run stats**: active cards show the latest instruction, thinking, and tool calls in a collapsed timeline of at most 4 rows; running cards also show round progress, output rate, cache hit rate, input/output tokens, and run duration, while completed, blocked, and error waits retain the previous round's duration.
|
|
55
|
+
- [x] **Current work and run stats**: active cards show the latest instruction, thinking, and tool calls in a collapsed timeline of at most 4 rows; running cards also show round progress, output rate, cache hit rate, input/output tokens, and run duration, while completed, blocked, and error waits retain the previous round's duration at the end of the token stats row alongside last-known output stats; recent history cards retain the latest completed round's available stats between the reply preview and activity time.
|
|
56
56
|
- [x] **Session navigation**: clicking or keyboard-activating a session card jumps to that session's page, and the current session stays highlighted.
|
|
57
57
|
- [x] **Session metadata**: session cards show the current model name and reasoning level.
|
|
58
58
|
- [x] **Polished desktop & mobile interactions**: the desktop pane can collapse, be resized by dragging, and remember its width; mobile uses a fixed drawer that does not squeeze the main conversation layout; long lists get independent scrolling and a back-to-top button.
|
package/README.zh-CN.md
CHANGED
|
@@ -48,11 +48,11 @@ 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] **显示工作区名称并参与排序**:会话卡片显示带稳定色彩的工作区徽标,会话排序与左侧边栏中的工作区顺序保持一致。
|
|
55
|
-
- [x] **展示当前工作与运行统计**:活动卡以最多 4 行折叠时间线展示最近指令、思考与工具调用;运行中卡片还显示回合进度、输出速率、缓存命中率、输入/输出 token
|
|
55
|
+
- [x] **展示当前工作与运行统计**:活动卡以最多 4 行折叠时间线展示最近指令、思考与工具调用;运行中卡片还显示回合进度、输出速率、缓存命中率、输入/输出 token 与运行时长,进入完成、阻塞或错误等待后在统计行末尾与 tok/s 等内容并列保留上一轮耗时及最后已知统计;迁入最近历史后,历史卡在助手预览与活动时间之间继续保留最后一轮的可用统计。
|
|
56
56
|
- [x] **加入会话导航跳转**:点击或键盘激活会话卡片可跳转到对应会话页面,当前会话保持高亮。
|
|
57
57
|
- [x] **增加会话元信息**:会话卡片中显示当前使用的模型名称和推理级别。
|
|
58
58
|
- [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,8 @@
|
|
|
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 结构断言已迁,多工作区感知区分与「标题随宿主更新」半句仍人工)
|
|
24
|
+
// - R-01-013/AC-12 → e2e/specs/session-lifecycle.mjs(最近卡统计行迁移与位置)
|
|
23
25
|
// T-089 起:
|
|
24
26
|
// - R-01-009/AC-02、AC-03 的 tool→等待→stream 实时更新 → e2e/specs/auto-update.mjs(AC-01 进行中 tool partial 仍由 unit/人工承接)。
|
|
25
27
|
// - R-01-014/AC-01、AC-03、AC-06 的列表 pending→ready、detail 渐进补齐与计数切换 → e2e/specs/loading-ready.mjs。
|
|
@@ -64,8 +66,8 @@ const steps = [
|
|
|
64
66
|
"让包含 todo_write/ask_user_question/cordis_define/失败工具调用/上下文注入的会话在选中与非选中态间切换,确认时间线中每个动作的标题与摘要两态完全一致(todo 显示 x/y 进度、ask 显示等待/已答/已取消、失败动作显示错误首行、上下文注入显示「上下文注入 · 来源标识」而非注入内容原文)。",
|
|
65
67
|
// R-01-009/AC-05 统计行布局与缓存命中率
|
|
66
68
|
"运行中会话流式输出时,确认进度条下方统计行左侧依次显示「tok/s 速率 · 缓存命中率 · 输入 · 输出」(与会话主窗口同序),字段间以小圆点区隔且速率无约等于符号、命中率为百分比;本回合时长固定在该行最右侧;数据缺失的字段隐藏,两段皆空时整行不残留。",
|
|
67
|
-
// R-01-009/AC-12
|
|
68
|
-
"
|
|
69
|
+
// R-01-009/AC-12、R-01-009/AC-13 等待状态保留上一轮耗时与统计
|
|
70
|
+
"让已有至少一个完整已结束回合的会话分别进入完成提醒、待确认/待审查/待回复阻塞等待与错误提醒,确认三类等待卡的时间线之后均继续显示上一轮最后已知的「tok/s · 缓存 · 输入 · 输出」可用统计,且耗时与上述统计处于同一行最右侧、等待类型胶囊同行不重复显示;等待数分钟后统计与耗时均不增长;没有可用统计或完整上一轮时不显示虚假字段。",
|
|
69
71
|
// R-01-009/AC-06 回合进度条(时间驱动、半衰期每帧按最新实测速率校准,C-014、C-025、C-044)
|
|
70
72
|
"确认运行卡标题后不再显示进度百分比;百分比改为与进度条同行并紧跟其右侧,在固定占位内向右对齐,其文字右缘与下一行最右侧的本回合耗时文字右缘对齐,并相对进度条中心略微上移 1px、视觉上不再偏下;9% 到 100% 的位数变化不改变其占位宽度或右缘,窄卡宽下进度条可收缩而百分比保持完整可见。观察进度条随本回合已耗时平滑爬升(不再出现 10% 级跳变):新会话起步期(无实测速率)按 20 tok/s 保守基准慢爬(约 2 分钟到 18%、6 分钟到 40%);速率实测后进度增速随统计行显示的累计平均速率动态调整——平均速率上升时增速加快(如 ≈45 tok/s 时 4 分钟到 50%),平均速率回落时进度可随之小幅回退(实时估计语义,不承诺单调);回合切换后归零重爬;中途才接入的会话进度从该回合已耗时对应值起算;回退时条纹动画不中断。",
|
|
71
73
|
// R-01-009/AC-07 工作项时间线状态与摘要(无行级耗时)
|
|
@@ -125,9 +127,12 @@ const steps = [
|
|
|
125
127
|
// R-01-013/AC-02 标题随宿主更新(回归:单卡渲染异常不得冻结其余卡片)
|
|
126
128
|
"重命名一个历史会话后,确认窗格历史卡标题与左边栏同步变为新标题;对之后获得自动生成标题的会话,确认卡片不停留在首条消息形态的旧标题。",
|
|
127
129
|
// R-01-010/AC-01 归档会话不入最近历史
|
|
128
|
-
"
|
|
130
|
+
"在左边栏归档一个历史会话后,确认其卡片从最近历史区消失(不再出现点了回落到新会话界面的死卡);其余历史卡不受影响。",
|
|
129
131
|
// R-01-010/AC-08、R-01-010/AC-09 历史区时间口径为最后活动时间
|
|
130
|
-
|
|
132
|
+
// R-01-013/AC-05 历史卡同时显示绝对日期时间与相对活动时间
|
|
133
|
+
"找一个发指令后 agent 运行了较久才结束的会话,待其落入最近历史区后,确认卡片第五行同时显示本地绝对日期时间(跨年份带年份)与相对活动时间,且绝对时间对应最后回复结束时刻(而非发指令时刻);历史区仍按该时刻从新到旧排序。刷新页面(冷会话)后确认时间先按宿主列表时间显示、数据到达后精化为回复结束时刻,相对时间会按分钟级推进。",
|
|
134
|
+
// R-01-013/AC-12 最近历史卡保留最近回合统计
|
|
135
|
+
"让一个已有完整回合的会话结束并经「移入历史」进入最近历史区,确认助手预览行之后、活动时间之前的倒数第二行仍显示最后回合的「tok/s · 缓存 · 输入 · 输出」可用统计,耗时固定在该行最右侧;缺失字段不造假且全部为空时不留空行。刷新页面后确认可从 native 列表投影与回合边界恢复。",
|
|
131
136
|
// R-01-013/AC-09 最近卡标题常规字重
|
|
132
137
|
"确认最近历史卡的会话标题为常规字重(不加粗),与活动卡的加粗标题形成明显区分。",
|
|
133
138
|
// R-01-013/AC-10 最近卡整体淡化
|
|
@@ -177,8 +182,10 @@ const steps = [
|
|
|
177
182
|
"重复上述两个方向的迁移并观察迁移卡以外的卡片:确认所有位置受影响的卡片(活动区剩余卡片、历史区其余卡片)与「最近历史」段头均以平滑滑动过渡到新位置,无任何卡片或段头瞬间跳变。",
|
|
178
183
|
// R-01-010/AC-05 分隔线留白
|
|
179
184
|
"确认活动区与最近历史区之间的分隔线上下各有约 10px 留白,两区呼吸感明确。",
|
|
180
|
-
// R-01-004/AC-03
|
|
181
|
-
"
|
|
185
|
+
// R-01-004/AC-03 滚动条指针显隐与 thumb hover 高亮
|
|
186
|
+
"将鼠标移入窗格(包括右缘调宽命中区)确认滚动条出现;调宽命中区只改变光标、不绘制高亮;将鼠标悬停在 scrollbar thumb 上,确认 thumb 变为更醒目的主题 hover 色;再向左移出调宽区后拖动 native scrollbar 确认列表滚动而 pane 宽度不变;鼠标离开且停止滚动约 0.6s 后滚动条隐藏,主会话区域滚动行为不受影响。",
|
|
187
|
+
// R-01-019/AC-01、R-01-019/AC-02、R-01-019/AC-03、R-01-019/AC-04 历史会话手动分页
|
|
188
|
+
"准备至少 21 个已完成且已移入历史的主会话:首次打开历史区只显示按最近活动时间排序的 10 张卡,并在列表底部显示「加载更多...」按钮;将窗格滚动到底部,确认不会自动追加,点击按钮后才追加后 10 张且前 10 张保留、无重复;再次点击按钮确认只追加最后 1 张、按钮随后隐藏,继续滚动不再增加;追加过程中活动区仍与历史区互斥,窗格滚动不带动主会话页面,桌面与移动端抽屉均可正常继续浏览。另以少于 10 张历史会话确认首屏全部显示、不显示加载按钮且不会自动补页。",
|
|
182
189
|
// R-01-018/AC-02、AC-04、AC-05 观感残留(键盘/显隐/回顶/窄条/抽屉行为断言已迁 e2e/specs/back-to-top.mjs)
|
|
183
190
|
"确认平滑滚动动画流畅自然;切到 <=767px 移动视口打开抽屉,确认抽屉内按钮不遮挡标题行;深浅主题各验一次不透明底色与图标可辨。",
|
|
184
191
|
// 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;
|
package/scripts/check.mjs
CHANGED
|
@@ -33,7 +33,9 @@ import {
|
|
|
33
33
|
openTurnStartMissing,
|
|
34
34
|
escapeCssString,
|
|
35
35
|
firstPhysicalLine,
|
|
36
|
+
fmtAbsoluteDateTime,
|
|
36
37
|
fmtElapsedMs,
|
|
38
|
+
fmtRelativeAge,
|
|
37
39
|
fmtTokens,
|
|
38
40
|
isActiveRow,
|
|
39
41
|
shouldSubscribeToSession,
|
|
@@ -64,6 +66,8 @@ import {
|
|
|
64
66
|
pruneInvisibleEntries,
|
|
65
67
|
pruneSubscriptions,
|
|
66
68
|
runtimeStats,
|
|
69
|
+
statsFromProjection,
|
|
70
|
+
mergeRuntimeStats,
|
|
67
71
|
shouldCancelOpenRetry,
|
|
68
72
|
subagentTitle,
|
|
69
73
|
summarizeToolArguments,
|
|
@@ -754,11 +758,11 @@ const drainGapIdle = {
|
|
|
754
758
|
current: null,
|
|
755
759
|
};
|
|
756
760
|
assert.equal(
|
|
757
|
-
buildRecent(drainGapIdle, [], 2000,
|
|
761
|
+
buildRecent(drainGapIdle, [], 2000, {}, [], null, new Set(["root"])).length,
|
|
758
762
|
0,
|
|
759
763
|
"耗尽空窗内委托周期会话不入最近历史(R-01-010 分区不变量)",
|
|
760
764
|
);
|
|
761
|
-
assert.equal(buildRecent(drainGapIdle, [], 2000,
|
|
765
|
+
assert.equal(buildRecent(drainGapIdle, [], 2000, {}, [], null).length, 1, "委托周期结束后才入最近历史");
|
|
762
766
|
// delegationActive:耗尽宽限内视为委托周期(空窗保持),超时退出。
|
|
763
767
|
assert.equal(delegationActive({ mode: "delegating", anchor: 1000, turnStart: 9000, drainedAt: null }, 50000), true, "委托周期中视为活动");
|
|
764
768
|
assert.equal(delegationActive({ mode: "delegating", anchor: 1000, turnStart: 9000, drainedAt: 30000 }, 31000), true, "耗尽宽限内视为活动(空窗保持运行呈现)");
|
|
@@ -897,6 +901,35 @@ assert.equal(fmtElapsedMs(193_000), "3m13s", "时长分秒格式");
|
|
|
897
901
|
assert.equal(fmtElapsedMs(NaN), "", "NaN 时长归一为空");
|
|
898
902
|
assert.equal(fmtElapsedMs(Infinity), "", "Infinity 时长归一为空");
|
|
899
903
|
assert.equal(fmtElapsedMs(-1), "", "负时长归一为空");
|
|
904
|
+
// R-01-013/AC-05:历史卡同时显示本地绝对日期时间与相对年龄,异常输入不制造虚假时间。
|
|
905
|
+
const relativeMinute = 60_000;
|
|
906
|
+
const relativeHour = 60 * relativeMinute;
|
|
907
|
+
const relativeDay = 24 * relativeHour;
|
|
908
|
+
assert.equal(fmtRelativeAge(0), "刚刚", "小于一分钟显示刚刚");
|
|
909
|
+
assert.equal(fmtRelativeAge(2 * relativeMinute + 1), "2分钟前", "分钟级相对时间");
|
|
910
|
+
assert.equal(fmtRelativeAge(3 * relativeHour + 1), "3小时前", "小时级相对时间");
|
|
911
|
+
assert.equal(fmtRelativeAge(2 * relativeDay + 1), "2天前", "天级相对时间");
|
|
912
|
+
assert.equal(fmtRelativeAge(2 * 7 * relativeDay + 1), "2周前", "周级相对时间");
|
|
913
|
+
assert.equal(fmtRelativeAge(2 * 30 * relativeDay + 1), "2个月前", "月级相对时间");
|
|
914
|
+
assert.equal(fmtRelativeAge(2 * 365 * relativeDay + 1), "2年前", "年级相对时间");
|
|
915
|
+
assert.equal(fmtRelativeAge(NaN), "", "NaN 相对时间归一为空");
|
|
916
|
+
assert.equal(fmtRelativeAge(Infinity), "", "Infinity 相对时间归一为空");
|
|
917
|
+
assert.equal(fmtRelativeAge(-1), "", "负相对时间归一为空");
|
|
918
|
+
const sameYearActivity = new Date(2026, 7, 31, 14, 32);
|
|
919
|
+
const sameYearNow = new Date(2026, 8, 1, 9, 0);
|
|
920
|
+
assert.equal(
|
|
921
|
+
fmtAbsoluteDateTime(sameYearActivity.getTime(), sameYearNow.getTime()),
|
|
922
|
+
`${sameYearActivity.toLocaleDateString([], { month: "2-digit", day: "2-digit" })} ${sameYearActivity.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`,
|
|
923
|
+
"当前年份绝对时间省略年份但保留月日时分",
|
|
924
|
+
);
|
|
925
|
+
const priorYearActivity = new Date(2025, 11, 31, 23, 50);
|
|
926
|
+
assert.equal(
|
|
927
|
+
fmtAbsoluteDateTime(priorYearActivity.getTime(), sameYearNow.getTime()),
|
|
928
|
+
`${priorYearActivity.toLocaleDateString([], { year: "numeric", month: "2-digit", day: "2-digit" })} ${priorYearActivity.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`,
|
|
929
|
+
"跨年份绝对时间补充年份",
|
|
930
|
+
);
|
|
931
|
+
assert.equal(fmtAbsoluteDateTime(null, sameYearNow.getTime()), "", "缺失绝对时间归一为空");
|
|
932
|
+
assert.equal(fmtAbsoluteDateTime(NaN, sameYearNow.getTime()), "", "非法绝对时间归一为空");
|
|
900
933
|
|
|
901
934
|
// R-01-012/AC-01
|
|
902
935
|
// R-01-012/AC-02
|
|
@@ -1471,6 +1504,33 @@ assert.deepEqual(
|
|
|
1471
1504
|
{ elapsedMs: 47_000, outputTokens: 0, rateTokS: null },
|
|
1472
1505
|
"零速率归一为空,token 统计仍可放在进度条下方",
|
|
1473
1506
|
);
|
|
1507
|
+
assert.deepEqual(
|
|
1508
|
+
statsFromProjection(
|
|
1509
|
+
{
|
|
1510
|
+
tokenUsage: { uncachedInputTokens: 100, cacheReadTokens: 20, cacheWriteTokens: 10, outputTokens: 144 },
|
|
1511
|
+
sessionStats: { decodeTokens: 144, decodeMs: 3_000 },
|
|
1512
|
+
},
|
|
1513
|
+
47_000,
|
|
1514
|
+
),
|
|
1515
|
+
{ elapsedMs: 47_000, outputTokens: 144, rateTokS: 48, inputTokens: 130, cacheHitPct: 15 },
|
|
1516
|
+
"最近卡复用列表投影派生 tok/s、缓存、输入/输出与固定耗时(R-01-013/AC-12)",
|
|
1517
|
+
);
|
|
1518
|
+
assert.deepEqual(
|
|
1519
|
+
mergeRuntimeStats(
|
|
1520
|
+
{ outputTokens: null, inputTokens: 24, cacheHitPct: null, rateTokS: null },
|
|
1521
|
+
{ outputTokens: 96, inputTokens: 12, cacheHitPct: 40, rateTokS: 8 },
|
|
1522
|
+
),
|
|
1523
|
+
{ outputTokens: 96, inputTokens: 24, cacheHitPct: 40, rateTokS: 8 },
|
|
1524
|
+
"等待/运行统计按字段回退,缺失投影不覆盖最后已知有效值(R-01-009/AC-13)",
|
|
1525
|
+
);
|
|
1526
|
+
assert.deepEqual(
|
|
1527
|
+
mergeRuntimeStats(
|
|
1528
|
+
{ outputTokens: 0, inputTokens: 0, cacheHitPct: 0, rateTokS: null },
|
|
1529
|
+
{ outputTokens: 96, inputTokens: 12, cacheHitPct: 40, rateTokS: 8 },
|
|
1530
|
+
),
|
|
1531
|
+
{ outputTokens: 0, inputTokens: 0, cacheHitPct: 0, rateTokS: 8 },
|
|
1532
|
+
"统计合并保留有效的零值,仅对缺失字段回退(R-01-009/AC-13)",
|
|
1533
|
+
);
|
|
1474
1534
|
assert.deepEqual(
|
|
1475
1535
|
usageSummary({ uncachedInputTokens: 100, cacheReadTokens: 700, cacheWriteTokens: 200 }),
|
|
1476
1536
|
{ inputTokens: 1_000, cacheHitPct: 70 },
|
|
@@ -2069,7 +2129,8 @@ assert.notEqual(
|
|
|
2069
2129
|
cardSignature(entries),
|
|
2070
2130
|
"缓存命中率变化签名必变",
|
|
2071
2131
|
);
|
|
2072
|
-
// ---- R-01-010/AC-01
|
|
2132
|
+
// ---- R-01-010/AC-01 非活动主会话→最近历史区(不设时间窗口) ----
|
|
2133
|
+
const RECENT_PAGE_SIZE = 10;
|
|
2073
2134
|
const NOW = 2_000_000_000_000; // 固定时钟便于确定性断言
|
|
2074
2135
|
// 注意:completed:true 的主会话是"待打开"的活动卡(在活动区),不属历史区;
|
|
2075
2136
|
// 真实"最近历史"是已处理(running:false 且无未确认完成)的非活动会话;
|
|
@@ -2090,19 +2151,19 @@ const recentSnap = {
|
|
|
2090
2151
|
const recentUncompleted = buildRecent(recentSnap, [], NOW);
|
|
2091
2152
|
assert.deepEqual(
|
|
2092
2153
|
recentUncompleted.map((e) => e.id),
|
|
2093
|
-
["sAwait", "sB"],
|
|
2094
|
-
"completed
|
|
2154
|
+
["sAwait", "sB", "sOld"],
|
|
2155
|
+
"completed 边沿标志不参与活动/历史判定;无完成登记时所有非活动主会话均入历史区(C-030)",
|
|
2095
2156
|
);
|
|
2096
2157
|
// 完成登记存在时(未确认完成):待开 E 显示为完成提醒并排除出历史区(AC-03、R-01-010/AC-06)。
|
|
2097
2158
|
const recentAcks = new Map([
|
|
2098
2159
|
["sAwait", { lastTurnEnd: NOW - 1_000, ackedAt: null }],
|
|
2099
2160
|
["sAwait2", { lastTurnEnd: NOW - 900, ackedAt: NOW - 2_000 }],
|
|
2100
2161
|
]);
|
|
2101
|
-
const recent = buildRecent(recentSnap, [], NOW,
|
|
2162
|
+
const recent = buildRecent(recentSnap, [], NOW, {}, [], recentAcks);
|
|
2102
2163
|
assert.deepEqual(
|
|
2103
2164
|
recent.map((e) => e.id),
|
|
2104
|
-
["sB"],
|
|
2105
|
-
"
|
|
2165
|
+
["sB", "sOld"],
|
|
2166
|
+
"完成登记表中未确认完成的会话留在活动区、排除出历史区;已确认的不再排除",
|
|
2106
2167
|
);
|
|
2107
2168
|
assert.deepEqual(
|
|
2108
2169
|
[recent[0].model, recent[0].reasoning, recent[0].userPreview, recent[0].agentPreview],
|
|
@@ -2117,7 +2178,7 @@ assert.deepEqual(messagePreviews({ history: [] }), { userPreview: "", agentPrevi
|
|
|
2117
2178
|
// R-01-013/AC-04
|
|
2118
2179
|
// R-01-013/AC-05
|
|
2119
2180
|
// R-01-013/AC-06
|
|
2120
|
-
const recentWithPreviews = buildRecent(recentSnap, [], NOW,
|
|
2181
|
+
const recentWithPreviews = buildRecent(recentSnap, [], NOW, {
|
|
2121
2182
|
sB: {
|
|
2122
2183
|
model: { model: "Model M", reasoning: "High" },
|
|
2123
2184
|
previews: { userPreview: "用户首行", agentPreview: "回复首行" },
|
|
@@ -2169,7 +2230,7 @@ const viaRunToIdle = buildEntries(
|
|
|
2169
2230
|
[],
|
|
2170
2231
|
);
|
|
2171
2232
|
assert.deepEqual(viaRunToIdle.map((e) => e.id), ["sB"], "运行中会话在活动区");
|
|
2172
|
-
const recentAfterIdle = buildRecent(recentSnap, [], NOW,
|
|
2233
|
+
const recentAfterIdle = buildRecent(recentSnap, [], NOW, {}, [], recentAcks);
|
|
2173
2234
|
assert.ok(recentAfterIdle.some((e) => e.id === "sB"), "转为非活动后进入历史区");
|
|
2174
2235
|
|
|
2175
2236
|
// ---- R-01-010/AC-03 历史区按最后活动时间从新到旧 ----
|
|
@@ -2192,6 +2253,52 @@ assert.deepEqual(
|
|
|
2192
2253
|
"历史区按最后活动时间倒序",
|
|
2193
2254
|
);
|
|
2194
2255
|
|
|
2256
|
+
// ---- R-01-019/AC-01~AC-03 历史候选不再截断,按 10 条分批呈现 ----
|
|
2257
|
+
const manyRecentIds = Array.from({ length: RECENT_PAGE_SIZE * 2 + 1 }, (_, index) => `page-${index}`);
|
|
2258
|
+
const manyRecent = buildRecent(
|
|
2259
|
+
{
|
|
2260
|
+
ids: manyRecentIds,
|
|
2261
|
+
byId: Object.fromEntries(manyRecentIds.map((id, index) => [id, {
|
|
2262
|
+
id,
|
|
2263
|
+
displayTitle: id,
|
|
2264
|
+
running: false,
|
|
2265
|
+
updatedAt: NOW - index,
|
|
2266
|
+
}])),
|
|
2267
|
+
current: null,
|
|
2268
|
+
},
|
|
2269
|
+
[],
|
|
2270
|
+
NOW,
|
|
2271
|
+
);
|
|
2272
|
+
assert.equal(manyRecent.length, manyRecentIds.length, "历史候选超过 20 条时不再被核心截断(R-01-019/AC-01)");
|
|
2273
|
+
assert.deepEqual(
|
|
2274
|
+
manyRecent.slice(0, RECENT_PAGE_SIZE).map((entry) => entry.id),
|
|
2275
|
+
manyRecentIds.slice(0, RECENT_PAGE_SIZE),
|
|
2276
|
+
"历史首批按最近顺序取 10 条(R-01-019/AC-01)",
|
|
2277
|
+
);
|
|
2278
|
+
assert.deepEqual(
|
|
2279
|
+
manyRecent.slice(RECENT_PAGE_SIZE, RECENT_PAGE_SIZE * 2).map((entry) => entry.id),
|
|
2280
|
+
manyRecentIds.slice(RECENT_PAGE_SIZE, RECENT_PAGE_SIZE * 2),
|
|
2281
|
+
"历史第二批紧接首批取后续 10 条(R-01-019/AC-02)",
|
|
2282
|
+
);
|
|
2283
|
+
assert.deepEqual(
|
|
2284
|
+
manyRecent.slice(RECENT_PAGE_SIZE * 2).map((entry) => entry.id),
|
|
2285
|
+
manyRecentIds.slice(RECENT_PAGE_SIZE * 2),
|
|
2286
|
+
"历史末批只取剩余会话且不会越界重复(R-01-019/AC-03)",
|
|
2287
|
+
);
|
|
2288
|
+
const tiedRecent = buildRecent(
|
|
2289
|
+
{
|
|
2290
|
+
ids: ["tie-z", "tie-a"],
|
|
2291
|
+
byId: {
|
|
2292
|
+
"tie-z": { id: "tie-z", displayTitle: "Z", running: false, updatedAt: NOW },
|
|
2293
|
+
"tie-a": { id: "tie-a", displayTitle: "A", running: false, updatedAt: NOW },
|
|
2294
|
+
},
|
|
2295
|
+
current: null,
|
|
2296
|
+
},
|
|
2297
|
+
[],
|
|
2298
|
+
NOW,
|
|
2299
|
+
);
|
|
2300
|
+
assert.deepEqual(tiedRecent.map((entry) => entry.id), ["tie-a", "tie-z"], "相同 activityAt 时以会话 id 稳定排序");
|
|
2301
|
+
|
|
2195
2302
|
// ---- R-01-010/AC-08 最后活动时间:turn/end 提取与 max 归一 ----
|
|
2196
2303
|
assert.equal(lastTurnEndFromEvents([]), null, "空 history 无回合结束时刻");
|
|
2197
2304
|
assert.equal(
|
|
@@ -2326,7 +2433,7 @@ const refineSnap = {
|
|
|
2326
2433
|
},
|
|
2327
2434
|
current: null,
|
|
2328
2435
|
};
|
|
2329
|
-
const refined = buildRecent(refineSnap, [], NOW,
|
|
2436
|
+
const refined = buildRecent(refineSnap, [], NOW, {}, [], null, null, {
|
|
2330
2437
|
sTurn: NOW - 2_000, // 回合结束晚于宿主时间 → 精化为回合结束时刻
|
|
2331
2438
|
sPrompt: NOW - 3_000, // 回合结束早于宿主时间(消息未处理)→ 取较新者
|
|
2332
2439
|
});
|
|
@@ -2343,19 +2450,18 @@ assert.deepEqual(
|
|
|
2343
2450
|
[["sPrompt", NOW - 1_000], ["sNone", NOW - 5_000], ["sTurn", NOW - 10_000]],
|
|
2344
2451
|
"回合结束时刻在途时先按宿主列表时间判定、排序与显示(R-01-010/AC-09)",
|
|
2345
2452
|
);
|
|
2346
|
-
//
|
|
2453
|
+
// 无时间窗口语义:宿主时间较早的非活动会话仍进入历史区,已知 turn/end 继续精化排序(C-068)。
|
|
2347
2454
|
const crossWindow = buildRecent(
|
|
2348
2455
|
{ ids: ["sLong"], byId: { sLong: { id: "sLong", displayTitle: "长回合", running: false, updatedAt: NOW - 25 * 3_600_000 } }, current: null },
|
|
2349
2456
|
[],
|
|
2350
2457
|
NOW,
|
|
2351
|
-
undefined,
|
|
2352
2458
|
{},
|
|
2353
2459
|
[],
|
|
2354
2460
|
null,
|
|
2355
2461
|
null,
|
|
2356
2462
|
{ sLong: NOW - 1_000 },
|
|
2357
2463
|
);
|
|
2358
|
-
assert.
|
|
2464
|
+
assert.deepEqual(crossWindow.map((entry) => [entry.id, entry.activityAt]), [["sLong", NOW - 1_000]], "较早宿主时间的非活动会话仍进入历史区(C-068)");
|
|
2359
2465
|
|
|
2360
2466
|
// ---- R-01-002/AC-03、AC-05、AC-10~AC-12、R-01-010/AC-06 完成确认:未确认完成提醒保留活动卡;
|
|
2361
2467
|
// 显式确认(按钮)或新回合隐式更替后解除;打开/切走/刷新不解除(C-030)----
|
|
@@ -2384,7 +2490,7 @@ assert.deepEqual(
|
|
|
2384
2490
|
"完成提醒计入徽标等待分子但不计阻塞(R-01-002/AC-06)",
|
|
2385
2491
|
);
|
|
2386
2492
|
assert.deepEqual(
|
|
2387
|
-
buildRecent(holdSnap, [], NOW,
|
|
2493
|
+
buildRecent(holdSnap, [], NOW, {}, [], acks(1000)).map((e) => e.id),
|
|
2388
2494
|
[],
|
|
2389
2495
|
"未确认完成提醒不入历史区(分区不变量)",
|
|
2390
2496
|
);
|
|
@@ -2399,7 +2505,7 @@ assert.deepEqual(
|
|
|
2399
2505
|
"确认后完成提醒解除、退出活动区",
|
|
2400
2506
|
);
|
|
2401
2507
|
assert.deepEqual(
|
|
2402
|
-
buildRecent(holdSnap, [], NOW,
|
|
2508
|
+
buildRecent(holdSnap, [], NOW, {}, [], ackedMap).map((e) => e.id),
|
|
2403
2509
|
["sB"],
|
|
2404
2510
|
"确认后会话进入历史区",
|
|
2405
2511
|
);
|
|
@@ -2457,7 +2563,7 @@ assert.deepEqual(
|
|
|
2457
2563
|
"错误提醒计入徽标等待分子但不计阻塞(R-01-002/AC-06)",
|
|
2458
2564
|
);
|
|
2459
2565
|
assert.deepEqual(
|
|
2460
|
-
buildRecent(holdSnap, [], NOW,
|
|
2566
|
+
buildRecent(holdSnap, [], NOW, {}, [], errAcks("error", "boom")).map((e) => e.id),
|
|
2461
2567
|
[],
|
|
2462
2568
|
"错误提醒中会话不入历史区(分区不变量,R-01-010/AC-06)",
|
|
2463
2569
|
);
|
|
@@ -2503,7 +2609,7 @@ assert.deepEqual(
|
|
|
2503
2609
|
["sB"],
|
|
2504
2610
|
"上一帧活动区 id 离开活动区且出现于历史区判定为迁移",
|
|
2505
2611
|
);
|
|
2506
|
-
assert.deepEqual(movedToRecentIds(new Set(["sB"]), [], []), [], "
|
|
2612
|
+
assert.deepEqual(movedToRecentIds(new Set(["sB"]), [], []), [], "彻底消失(归档/不再被会话服务列出)不判定为迁移");
|
|
2507
2613
|
assert.deepEqual(movedToRecentIds(new Set(), [{ id: "sB" }], []), [], "上一帧不在活动区不判定为迁移");
|
|
2508
2614
|
assert.deepEqual(movedToRecentIds(new Set(["sB"]), [{ id: "sB" }], []), [], "仍在活动区不判定为迁移");
|
|
2509
2615
|
|
|
@@ -2513,7 +2619,7 @@ assert.deepEqual(
|
|
|
2513
2619
|
["rB"],
|
|
2514
2620
|
"上一帧历史区 id 离开历史区且出现于活动区判定为反向迁移",
|
|
2515
2621
|
);
|
|
2516
|
-
assert.deepEqual(movedToActiveIds(new Set(["rB"]), [], []), [], "
|
|
2622
|
+
assert.deepEqual(movedToActiveIds(new Set(["rB"]), [], []), [], "彻底消失(归档/不再被会话服务列出)不判定为反向迁移");
|
|
2517
2623
|
assert.deepEqual(movedToActiveIds(new Set(), [{ id: "rB" }], []), [], "上一帧不在历史区不判定为反向迁移");
|
|
2518
2624
|
assert.deepEqual(movedToActiveIds(new Set(["rB"]), [], [{ id: "rB" }]), [], "仍在历史区不判定为反向迁移");
|
|
2519
2625
|
|
|
@@ -2544,14 +2650,13 @@ const recentArchived = buildRecent(
|
|
|
2544
2650
|
},
|
|
2545
2651
|
[],
|
|
2546
2652
|
NOW,
|
|
2547
|
-
undefined,
|
|
2548
2653
|
{},
|
|
2549
2654
|
["sGone"],
|
|
2550
2655
|
);
|
|
2551
2656
|
assert.deepEqual(
|
|
2552
2657
|
recentArchived.map((e) => e.id),
|
|
2553
2658
|
["sKeep"],
|
|
2554
|
-
"
|
|
2659
|
+
"归档会话即使仍被会话服务列出也不入最近历史",
|
|
2555
2660
|
);
|
|
2556
2661
|
assert.deepEqual(
|
|
2557
2662
|
buildRecent(
|
|
@@ -2565,7 +2670,6 @@ assert.deepEqual(
|
|
|
2565
2670
|
},
|
|
2566
2671
|
[],
|
|
2567
2672
|
NOW,
|
|
2568
|
-
undefined,
|
|
2569
2673
|
{},
|
|
2570
2674
|
new Set(["sGone"]),
|
|
2571
2675
|
).map((e) => e.id),
|
|
@@ -2703,8 +2807,8 @@ assert.ok(bundle.includes("notifyLayoutChange"), "布局变化通知 sibling ove
|
|
|
2703
2807
|
assert.ok(bundle.includes('window.dispatchEvent(new Event("resize"))'), "布局变化派发标准 resize 通知");
|
|
2704
2808
|
assert.ok(bundle.includes("pane !== renderedPane"), "新窗格实例必须重置渲染签名");
|
|
2705
2809
|
assert.ok(
|
|
2706
|
-
clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface]);"),
|
|
2707
|
-
"列表 phase
|
|
2810
|
+
clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);"),
|
|
2811
|
+
"列表 phase 与历史时间文案必须参与结构化渲染签名,空列表不得冻结在加载/失败状态(T-087)",
|
|
2708
2812
|
);
|
|
2709
2813
|
// R-01-013/AC-02 回归:卡片标题必须随快照更新——单卡渲染异常不得冻结其余卡片
|
|
2710
2814
|
// (此前渲染签名先于卡片循环提交且无异常隔离,故障卡及其后全部卡片永久滞留旧标题,
|
|
@@ -2740,13 +2844,13 @@ assert.ok(
|
|
|
2740
2844
|
bundle.includes("list.insertBefore(rec.el, ref)"),
|
|
2741
2845
|
"卡片仅在顺序/归属变化时移动 DOM(insertBefore 位置守卫)",
|
|
2742
2846
|
);
|
|
2743
|
-
// ---- R-01-009/AC-02、R-01-009/AC-05、R-01-012/AC-01..04、R-01-013/AC-01..06 ----
|
|
2847
|
+
// ---- R-01-009/AC-02、R-01-009/AC-05、R-01-012/AC-01..04、R-01-013/AC-01..06、AC-12 ----
|
|
2744
2848
|
assert.ok(bundle.includes("foldedConversationTimeline"), "活动卡时间线由折叠分组唯一来源派生(R-01-012、R-01-017)");
|
|
2745
2849
|
assert.ok(!bundle.includes("dap-trace-time"), "工作项时间线不渲染行级耗时元素,对齐主会话窗口(R-01-009/AC-07、C-012)");
|
|
2746
2850
|
assert.ok(!bundle.includes("PROGRESS_THINK_BASE") && !bundle.includes("progressFloor"), "回合进度纯时间驱动,无思考基线/单调下限残留(R-01-009/AC-06、C-014)");
|
|
2747
2851
|
assert.ok(bundle.includes("progressHalfLifeSec"), "bundle 含半衰期速率校准函数(R-01-009/AC-06、C-025、C-044)");
|
|
2748
2852
|
assert.ok(
|
|
2749
|
-
bundle.includes("halfLifeSec: progressHalfLifeSec({ rateTokS })"),
|
|
2853
|
+
bundle.includes("halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS })"),
|
|
2750
2854
|
"进度赋值每帧按最新实测速率现算半衰期(R-01-009/AC-06、C-044)",
|
|
2751
2855
|
);
|
|
2752
2856
|
assert.ok(
|
|
@@ -2766,9 +2870,18 @@ assert.ok(
|
|
|
2766
2870
|
"统计行双段结构:左列文本 + 右置时长(R-01-009/AC-05)",
|
|
2767
2871
|
);
|
|
2768
2872
|
assert.ok(bundle.includes("function renderTokenStats"), "运行卡统计行继续复用既有渲染逻辑(R-01-009/AC-05)");
|
|
2873
|
+
// R-01-009/AC-13 等待卡保留进入等待前最后已知的 token 统计
|
|
2874
|
+
assert.ok(
|
|
2875
|
+
bundle.includes('return [head, row, makeEl("div", "dap-trace"), statsRow, foot];'),
|
|
2876
|
+
"等待卡骨架在时间线后保留统计行(R-01-009/AC-13)",
|
|
2877
|
+
);
|
|
2769
2878
|
assert.ok(
|
|
2770
|
-
bundle.includes("
|
|
2771
|
-
"
|
|
2879
|
+
bundle.includes("renderTokenStats(el, entry);") && bundle.includes("removeAwaitingHeadDuration(el);"),
|
|
2880
|
+
"等待卡复用统计行并清理胶囊同行的旧耗时节点(R-01-009/AC-12、AC-13)",
|
|
2881
|
+
);
|
|
2882
|
+
assert.ok(
|
|
2883
|
+
bundle.includes("function removeAwaitingHeadDuration") && bundle.includes("awaitHead.append(capsule);"),
|
|
2884
|
+
"等待类型胶囊独立成行,固定耗时回到统计行(R-01-009/AC-12)",
|
|
2772
2885
|
);
|
|
2773
2886
|
assert.ok(bundle.includes("lastTurnDuration({"), "等待卡耗时由最近完整回合边界派生(R-01-009/AC-12)");
|
|
2774
2887
|
assert.ok(bundle.includes("`输入 ${fmtTokens("), "统计行含输入/输出中文短标签(R-01-009/AC-05)");
|
|
@@ -2783,6 +2896,14 @@ assert.ok(
|
|
|
2783
2896
|
);
|
|
2784
2897
|
assert.ok(!bundle.includes("≈"), "速率不再携带约等于符号(R-01-009/AC-05)");
|
|
2785
2898
|
assert.ok(bundle.includes("dap-history-line"), "历史卡包含用户/agent 两条消息预览行");
|
|
2899
|
+
// R-01-013/AC-12 最近历史卡保留最近回合统计
|
|
2900
|
+
assert.ok(
|
|
2901
|
+
bundle.includes('return [head, row, userLine, agentLine, statsRow, makeEl("div", "dap-note")];'),
|
|
2902
|
+
"历史卡统计行位于助手预览后、活动时间前",
|
|
2903
|
+
);
|
|
2904
|
+
assert.ok(bundle.includes("statsFromProjection"), "历史卡统计复用列表投影派生函数(R-01-013/AC-12)");
|
|
2905
|
+
assert.ok(bundle.includes("lastRuntimeStats"), "会话结束时保留最后已知运行统计(R-01-013/AC-12)");
|
|
2906
|
+
assert.ok(bundle.includes("recentDurationFallbackIds"), "历史卡缺回合边界时复用 history 补读(R-01-013/AC-12)");
|
|
2786
2907
|
// R-01-003/AC-04
|
|
2787
2908
|
assert.ok(bundle.includes("parentId: m.isSub ? String(parentId) : null"), "活动卡条目保留直属母会话 id");
|
|
2788
2909
|
assert.ok(bundle.includes("function trackRuns(entries)"), "母会话轨道拓扑由纯函数 trackRuns 一次求出");
|
|
@@ -2847,9 +2968,9 @@ assert.ok(
|
|
|
2847
2968
|
assert.ok(bundle.includes("function activeSessionIds(byId = {})"), "活动子代理沿 parentId 链补齐活动祖先");
|
|
2848
2969
|
// ---- R-01-016/AC-01 等待卡保留最近工作项时间线 ----
|
|
2849
2970
|
assert.ok(
|
|
2850
|
-
bundle.includes(
|
|
2851
|
-
bundle.includes('return [head, row, makeEl("div", "dap-trace"), foot];'),
|
|
2852
|
-
"awaiting
|
|
2971
|
+
bundle.includes("awaitHead.append(capsule);") &&
|
|
2972
|
+
bundle.includes('return [head, row, makeEl("div", "dap-trace"), statsRow, foot];'),
|
|
2973
|
+
"awaiting 骨架在标题行与统计行、末行两段(胶囊+正文)之间含时间线,固定耗时由统计行承载(R-01-016/AC-01、R-01-009/AC-12,C-043)",
|
|
2853
2974
|
);
|
|
2854
2975
|
// ---- R-01-002/AC-10 完成提醒卡「移入历史」按钮 ----
|
|
2855
2976
|
assert.ok(
|
|
@@ -2950,7 +3071,8 @@ assert.ok(!bundle.includes("serviceTimer"), "服务发现不得保留后台定
|
|
|
2950
3071
|
assert.ok(!bundle.includes("frameProbeTimer"), "宿主 frame 发现不得保留后台定时器");
|
|
2951
3072
|
assert.ok(bundle.includes("conversationObserver"), "流式 DOM 观察绑定到 conversation seat");
|
|
2952
3073
|
assert.ok(bundle.includes("centerObserver"), "宿主结构变化通过 center 直接子节点通知处理");
|
|
2953
|
-
assert.ok(bundle.includes("setInterval(() => queueSync(), CLOCK_MS)"), "
|
|
3074
|
+
assert.ok(bundle.includes("setInterval(() => queueSync(), CLOCK_MS)"), "运行卡时长继续使用独立的 1 秒时钟");
|
|
3075
|
+
assert.ok(bundle.includes("RECENT_TIME_REFRESH_MS") && bundle.includes("syncRecentTimeClock"), "历史卡相对时间使用独立的分钟级刷新时钟(R-01-013/AC-05)");
|
|
2954
3076
|
// R-01-014/AC-01
|
|
2955
3077
|
// R-01-014/AC-02
|
|
2956
3078
|
// R-01-014/AC-03
|
|
@@ -3144,6 +3266,7 @@ assert.ok(
|
|
|
3144
3266
|
assert.ok(bundle.includes("lastTurnEndFromEvents") && bundle.includes("lastTurnEndFromTimings"), "回合结束时刻提取进入 bundle(R-01-010/AC-08)");
|
|
3145
3267
|
assert.ok(bundle.includes("delegatingIds, turnEnds)"), "turnEnds 注入 buildRecent(R-01-010/AC-09)");
|
|
3146
3268
|
assert.ok(bundle.includes("fmtRecentTime(entry.activityAt)"), "最近卡渲染读取精化后的 activityAt(R-01-010/AC-08)");
|
|
3269
|
+
assert.ok(bundle.includes("fmtRelativeAge") && bundle.includes("最后活动 ·"), "最近卡同时显示绝对日期时间与相对活动时间(R-01-013/AC-05)");
|
|
3147
3270
|
|
|
3148
3271
|
assert.ok(
|
|
3149
3272
|
bundle.indexOf('[data-kind="recent"] {') < bundle.indexOf(".dap-card[data-opening]"),
|
|
@@ -3390,12 +3513,54 @@ assert.ok(
|
|
|
3390
3513
|
);
|
|
3391
3514
|
|
|
3392
3515
|
// R-01-004/AC-03
|
|
3393
|
-
//
|
|
3516
|
+
// 滚动条随滚动或鼠标指针进入窗格显示:thumb 默认透明、data-scrolling/data-pointer-inside 时显示;
|
|
3517
|
+
// Firefox 路径在 @supports 门内,native scrollbar 与右缘调宽手柄保持可分别命中。
|
|
3394
3518
|
assert.ok(bundle.includes(".dap-scroll::-webkit-scrollbar-thumb {\n background: transparent;"), "滚动条 thumb 默认透明(不滚动时不显示)");
|
|
3395
3519
|
assert.ok(bundle.includes(".dap-scroll[data-scrolling]::-webkit-scrollbar-thumb"), "滚动中经 data-scrolling 显示滚动条");
|
|
3520
|
+
assert.ok(bundle.includes("[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb"), "鼠标进入窗格经 data-pointer-inside 显示滚动条");
|
|
3521
|
+
assert.ok(bundle.includes("[data-dsh-activity-pane][data-pointer-inside] .dap-scroll::-webkit-scrollbar-thumb:hover"), "鼠标悬停 native thumb 使用高 specificity hover 规则");
|
|
3522
|
+
assert.ok(bundle.includes("var(--dsh-scrollbar-thumb-hover"), "native thumb hover 读取 DSH 主题 hover token");
|
|
3523
|
+
assert.ok(bundle.includes("scrollbar-gutter: stable;"), "滚动区稳定预留 native scrollbar 槽位");
|
|
3524
|
+
assert.ok(bundle.includes("margin-right: var(--dsh-scrollbar-width, 8px);"), "滚动区右侧按 DSH scrollbar 宽度留出调宽边界");
|
|
3525
|
+
assert.ok(
|
|
3526
|
+
bundle.includes("[data-dsh-activity-pane] .dap-resize {\n position: absolute;\n top: 0; right: 0; bottom: 0;"),
|
|
3527
|
+
"调宽手柄保持在 pane 右缘",
|
|
3528
|
+
);
|
|
3529
|
+
assert.ok(!bundle.includes(".dap-resize:hover"), "调宽手柄悬停不绘制高亮区域");
|
|
3530
|
+
assert.ok(bundle.includes('pane.addEventListener("pointerenter", onPanePointerEnter)'), "鼠标进入窗格绑定滚动条显示");
|
|
3531
|
+
assert.ok(bundle.includes('pane.addEventListener("pointerleave", onPanePointerLeave)'), "鼠标离开窗格绑定滚动条隐藏");
|
|
3532
|
+
assert.ok(bundle.includes('pane.removeEventListener("pointerenter", onPanePointerEnter)') && bundle.includes('pane.removeEventListener("pointerleave", onPanePointerLeave)'), "unbind 清理窗格指针监听(R-02-003/AC-02)");
|
|
3396
3533
|
assert.ok(bundle.includes("@supports not selector(::-webkit-scrollbar)") && bundle.includes("scrollbar-color: transparent transparent"), "Firefox 路径以 @supports 门隔离(防 Chromium 丢弃伪元素规则)");
|
|
3534
|
+
assert.ok(bundle.includes("[data-dsh-activity-pane][data-pointer-inside] .dap-scroll {\n scrollbar-color:"), "Firefox 路径经 data-pointer-inside 显示滚动条");
|
|
3397
3535
|
assert.ok(bundle.includes('scroll?.addEventListener("scroll", onScroll, { passive: true })'), "滚动监听置位 data-scrolling");
|
|
3398
|
-
assert.ok(
|
|
3536
|
+
assert.ok(
|
|
3537
|
+
bundle.includes('scroll?.removeEventListener("scroll", onScroll);') && bundle.includes('if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);'),
|
|
3538
|
+
"unbind 同步清理滚动监听与隐藏定时器(R-02-003/AC-02)",
|
|
3539
|
+
);
|
|
3540
|
+
assert.ok(
|
|
3541
|
+
bundle.includes('recentMore?.removeEventListener("click", onRecentMoreClick);') && !bundle.includes("recentObserver?.disconnect();"),
|
|
3542
|
+
"unbind 同步清理加载更多按钮监听,历史分页不再保留 observer(R-02-003/AC-02)",
|
|
3543
|
+
);
|
|
3544
|
+
|
|
3545
|
+
// R-01-019/AC-01~AC-04 历史候选与手动分页渲染契约
|
|
3546
|
+
assert.equal(RECENT_PAGE_SIZE, 10, "历史分页批次固定为 10 条(R-01-019/AC-01)");
|
|
3547
|
+
assert.ok(bundle.includes("const RECENT_PAGE_SIZE = 10;"), "生成 bundle 保留 10 条历史分页批次配置(R-01-019/AC-01)");
|
|
3548
|
+
assert.ok(bundle.includes('const recent = recentCandidates.slice(0, recentVisibleCount);'), "历史区只渲染当前可见候选前缀(R-01-019/AC-01)");
|
|
3549
|
+
assert.ok(bundle.includes('const nextCount = Math.min(recentVisibleCount + RECENT_PAGE_SIZE, recentTotal);'), "按钮激活后追加最多一个 10 条批次(R-01-019/AC-02)");
|
|
3550
|
+
assert.ok(
|
|
3551
|
+
bundle.includes('<button class="dap-recent-more" type="button" hidden>加载更多...</button>') &&
|
|
3552
|
+
bundle.includes('recentMore?.addEventListener("click", onRecentMoreClick);'),
|
|
3553
|
+
"历史区底部提供原生加载更多按钮并绑定显式激活(R-01-019/AC-02)",
|
|
3554
|
+
);
|
|
3555
|
+
assert.ok(
|
|
3556
|
+
!bundle.includes("new window.IntersectionObserver") && !bundle.includes('class="dap-recent-tail"') && !bundle.includes("maybeLoadMoreRecent"),
|
|
3557
|
+
"历史分页不再由 IntersectionObserver、尾部 sentinel 或滚动检查自动触发(R-01-019/AC-01、AC-02)",
|
|
3558
|
+
);
|
|
3559
|
+
assert.ok(
|
|
3560
|
+
bundle.includes("recentMore.hidden = !recentHasMore;") && bundle.includes("recentMore.disabled = recentAppendQueued;"),
|
|
3561
|
+
"仍有候选时显示按钮、追加期间禁用,候选耗尽后隐藏(R-01-019/AC-03)",
|
|
3562
|
+
);
|
|
3563
|
+
assert.ok(bundle.includes('recentHasMore = recent.length < recentTotal;'), "历史候选耗尽后停止追加(R-01-019/AC-03)");
|
|
3399
3564
|
|
|
3400
3565
|
// R-01-018 回到顶部悬浮图标按钮
|
|
3401
3566
|
// R-01-018/AC-01、R-01-018/AC-03:骨架按钮默认 hidden,滚动监听按 TOP_THRESHOLD 阈值揭隐/隐藏;
|