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/scripts/check.mjs
CHANGED
|
@@ -66,12 +66,16 @@ import {
|
|
|
66
66
|
pruneInvisibleEntries,
|
|
67
67
|
pruneSubscriptions,
|
|
68
68
|
runtimeStats,
|
|
69
|
+
statsFromProjection,
|
|
70
|
+
mergeRuntimeStats,
|
|
69
71
|
shouldCancelOpenRetry,
|
|
70
72
|
subagentTitle,
|
|
71
73
|
summarizeToolArguments,
|
|
72
74
|
usageSummary,
|
|
73
75
|
workspaceHue,
|
|
74
|
-
|
|
76
|
+
WORKSPACE_COLOR_SLOTS,
|
|
77
|
+
WORKSPACE_BACKGROUND_SLOTS,
|
|
78
|
+
resolveWorkspaceColors,
|
|
75
79
|
workspaceInfoForSession,
|
|
76
80
|
} from "../src/core.mjs";
|
|
77
81
|
import {
|
|
@@ -79,6 +83,7 @@ import {
|
|
|
79
83
|
bindBackdropDismiss,
|
|
80
84
|
bindCardActivation,
|
|
81
85
|
openSession,
|
|
86
|
+
scrollCardIntoView,
|
|
82
87
|
shouldDismissDrawerOnActivation,
|
|
83
88
|
suppressComposerAutofocus,
|
|
84
89
|
} from "../src/navigation.mjs";
|
|
@@ -112,6 +117,56 @@ assert.equal(
|
|
|
112
117
|
"sessions.open 失败时交给调用方进入 refresh/retry",
|
|
113
118
|
);
|
|
114
119
|
|
|
120
|
+
// ---- R-01-006/AC-02 当前卡片最小滚动:只调整越界方向,不居中 ----
|
|
121
|
+
const viewport = { top: 10, bottom: 110 };
|
|
122
|
+
const makeScrollBox = (scrollTop, smooth = true) => {
|
|
123
|
+
const scroll = {
|
|
124
|
+
scrollTop,
|
|
125
|
+
scrollHeight: 300,
|
|
126
|
+
clientHeight: 100,
|
|
127
|
+
getBoundingClientRect: () => viewport,
|
|
128
|
+
};
|
|
129
|
+
if (smooth) {
|
|
130
|
+
scroll.scrollCalls = [];
|
|
131
|
+
scroll.scrollTo = ({ top, behavior }) => {
|
|
132
|
+
scroll.scrollCalls.push({ top, behavior });
|
|
133
|
+
scroll.scrollTop = top;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return scroll;
|
|
137
|
+
};
|
|
138
|
+
const makeCardBox = (top, bottom) => ({
|
|
139
|
+
getBoundingClientRect: () => ({ top, bottom }),
|
|
140
|
+
});
|
|
141
|
+
const belowScroll = makeScrollBox(0);
|
|
142
|
+
assert.equal(scrollCardIntoView(belowScroll, makeCardBox(120, 160)), true, "卡片底部越界时执行向下定位");
|
|
143
|
+
assert.equal(belowScroll.scrollTop, 50, "向下只滚动卡片底部超出的最小距离");
|
|
144
|
+
assert.deepEqual(belowScroll.scrollCalls, [{ top: 50, behavior: "smooth" }], "向下定位使用快速平滑滚动");
|
|
145
|
+
const reducedMotionScroll = makeScrollBox(0);
|
|
146
|
+
assert.equal(scrollCardIntoView(reducedMotionScroll, makeCardBox(120, 160), "auto"), true, "降低动效偏好时仍执行定位");
|
|
147
|
+
assert.deepEqual(reducedMotionScroll.scrollCalls, [{ top: 50, behavior: "auto" }], "降低动效偏好时跳过平滑过渡");
|
|
148
|
+
const fallbackScroll = makeScrollBox(0, false);
|
|
149
|
+
assert.equal(scrollCardIntoView(fallbackScroll, makeCardBox(120, 160)), true, "缺少 scrollTo 时保留直接滚动 fallback");
|
|
150
|
+
assert.equal(fallbackScroll.scrollTop, 50, "scrollTo fallback 仍使用最小滚动距离");
|
|
151
|
+
const throwingScroll = makeScrollBox(0);
|
|
152
|
+
throwingScroll.scrollTo = () => { throw new TypeError("unsupported options"); };
|
|
153
|
+
assert.equal(scrollCardIntoView(throwingScroll, makeCardBox(120, 160)), true, "scrollTo options 不受支持时回退为直接滚动");
|
|
154
|
+
assert.equal(throwingScroll.scrollTop, 50, "scrollTo fallback 仍完成最小滚动");
|
|
155
|
+
const unexpectedScroll = makeScrollBox(0);
|
|
156
|
+
unexpectedScroll.scrollTo = () => { throw new Error("unexpected failure"); };
|
|
157
|
+
assert.throws(() => scrollCardIntoView(unexpectedScroll, makeCardBox(120, 160)), /unexpected failure/, "非预期滚动异常继续暴露");
|
|
158
|
+
const aboveScroll = makeScrollBox(100);
|
|
159
|
+
assert.equal(scrollCardIntoView(aboveScroll, makeCardBox(-20, 20)), true, "卡片顶部越界时执行向上定位");
|
|
160
|
+
assert.equal(aboveScroll.scrollTop, 70, "向上只滚动卡片顶部超出的最小距离");
|
|
161
|
+
const visibleScroll = makeScrollBox(42);
|
|
162
|
+
assert.equal(scrollCardIntoView(visibleScroll, makeCardBox(20, 90)), false, "卡片完整可见时不滚动");
|
|
163
|
+
assert.equal(visibleScroll.scrollTop, 42, "卡片完整可见时保持原滚动位置");
|
|
164
|
+
const topBoundaryScroll = makeScrollBox(0);
|
|
165
|
+
assert.equal(scrollCardIntoView(topBoundaryScroll, makeCardBox(-20, 20)), false, "顶部边界不能继续向上滚动");
|
|
166
|
+
const bottomBoundaryScroll = makeScrollBox(200);
|
|
167
|
+
assert.equal(scrollCardIntoView(bottomBoundaryScroll, makeCardBox(120, 160)), false, "底部边界不能继续向下滚动");
|
|
168
|
+
assert.equal(scrollCardIntoView(null, makeCardBox(0, 1)), false, "缺少滚动容器时静默跳过");
|
|
169
|
+
|
|
115
170
|
// ---- R-01-005/AC-01、R-02-003/AC-01 card 自身 click/键盘监听与卸载 ----
|
|
116
171
|
const cardListeners = new Map();
|
|
117
172
|
const card = {
|
|
@@ -219,7 +274,62 @@ assert.equal(escapeCssString("a\fb"), "a\\c b", "换页按 CSS 字符串码位
|
|
|
219
274
|
assert.equal(escapeCssString("a\0b"), "a�b", "NUL 归一为替换字符");
|
|
220
275
|
// R-01-012/AC-01
|
|
221
276
|
assert.equal(isSubagentRow({ parentId: "ghost" }, {}), false, "父级不在列表时按主会话处理(仍允许 models 读取)");
|
|
222
|
-
assert.equal(isSubagentRow({ parentId: "p" }, { p: { id: "p" } }),
|
|
277
|
+
assert.equal(isSubagentRow({ parentId: "p" }, { p: { id: "p" } }), false, "普通 fork 的 parentId 不足以判定为子代理");
|
|
278
|
+
assert.equal(isSubagentRow({ parentId: "p", origin: "subagent" }, { p: { id: "p" } }), true, "origin 为 subagent 且父级有效时命中子代理判定");
|
|
279
|
+
assert.equal(isSubagentRow({ origin: "subagent" }, { p: { id: "p" } }), false, "缺少有效 parentId 时不判定为子代理");
|
|
280
|
+
// R-01-003/AC-01、R-01-010/AC-01:普通 fork 虽有 parentId,仍按独立主会话处理。
|
|
281
|
+
const forkWorkspace = [{ title: "Fork 工作区", path: "/srv/fork", sessionIds: ["fork-root", "fork-child"] }];
|
|
282
|
+
const forkActivity = {
|
|
283
|
+
ids: ["fork-root", "fork-child"],
|
|
284
|
+
byId: {
|
|
285
|
+
"fork-root": { id: "fork-root", displayTitle: "来源会话", running: false, completed: false, updatedAt: 1_000 },
|
|
286
|
+
"fork-child": { id: "fork-child", displayTitle: "分支会话", running: true, completed: false, parentId: "fork-root" },
|
|
287
|
+
},
|
|
288
|
+
current: null,
|
|
289
|
+
};
|
|
290
|
+
assert.equal(isSubagentRow(forkActivity.byId["fork-child"], forkActivity.byId), false, "普通 fork 的 parentId 不改变主会话身份");
|
|
291
|
+
assert.deepEqual([...activeSessionIds(forkActivity.byId)], ["fork-child"], "活动 fork 不把来源会话算作活动后代母会话");
|
|
292
|
+
assert.deepEqual(
|
|
293
|
+
buildEntries(forkActivity, forkWorkspace).map((entry) => [entry.id, entry.kind, entry.depth, entry.parentId, entry.workspaceTitle]),
|
|
294
|
+
[["fork-child", "running", 0, null, "Fork 工作区"]],
|
|
295
|
+
"活动 fork 保持独立主会话条目与工作区身份");
|
|
296
|
+
const forkRecent = {
|
|
297
|
+
...forkActivity,
|
|
298
|
+
byId: {
|
|
299
|
+
"fork-root": { ...forkActivity.byId["fork-root"], updatedAt: 1_000 },
|
|
300
|
+
"fork-child": { ...forkActivity.byId["fork-child"], running: false, updatedAt: 1_500 },
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
assert.deepEqual(
|
|
304
|
+
buildRecent(forkRecent, forkWorkspace, 2_000).map((entry) => [entry.id, entry.workspaceTitle]),
|
|
305
|
+
[["fork-child", "Fork 工作区"], ["fork-root", "Fork 工作区"]],
|
|
306
|
+
"结束 fork 可进入最近历史且保留工作区身份");
|
|
307
|
+
const trueSubagentActivity = {
|
|
308
|
+
...forkActivity,
|
|
309
|
+
byId: { ...forkActivity.byId, "fork-child": { ...forkActivity.byId["fork-child"], origin: "subagent" } },
|
|
310
|
+
};
|
|
311
|
+
assert.deepEqual(
|
|
312
|
+
buildEntries(trueSubagentActivity, forkWorkspace).map((entry) => [entry.id, entry.kind, entry.depth, entry.parentId]),
|
|
313
|
+
[["fork-root", "running", 0, null], ["fork-child", "subagent", 1, "fork-root"]],
|
|
314
|
+
"origin 为 subagent 的真实子代理仍保持嵌套与母会话委托呈现");
|
|
315
|
+
const subagentUnderFork = {
|
|
316
|
+
ids: ["source", "fork", "nested-subagent"],
|
|
317
|
+
byId: {
|
|
318
|
+
source: { id: "source", displayTitle: "来源会话", running: false },
|
|
319
|
+
fork: { id: "fork", displayTitle: "独立分支", parentId: "source", running: false },
|
|
320
|
+
"nested-subagent": { id: "nested-subagent", displayTitle: "分支中的子代理", parentId: "fork", origin: "subagent", running: true },
|
|
321
|
+
},
|
|
322
|
+
current: null,
|
|
323
|
+
};
|
|
324
|
+
assert.deepEqual([...activeSessionIds(subagentUnderFork.byId)].sort(), ["fork", "nested-subagent"], "分支下真实子代理的活动祖先不越过独立 fork 来源");
|
|
325
|
+
assert.deepEqual(
|
|
326
|
+
buildEntries(subagentUnderFork, [{ title: "Fork 工作区", path: "/srv/fork", sessionIds: ["source", "fork", "nested-subagent"] }]).map((entry) => [entry.id, entry.kind, entry.depth, entry.parentId]),
|
|
327
|
+
[["fork", "running", 0, null], ["nested-subagent", "subagent", 1, "fork"]],
|
|
328
|
+
"分支下真实子代理只嵌套到 fork,不把来源会话变成母会话");
|
|
329
|
+
assert.deepEqual(
|
|
330
|
+
buildEntries(subagentUnderFork, [], {}, null, null, ["source"]).map((entry) => entry.id),
|
|
331
|
+
["fork", "nested-subagent"],
|
|
332
|
+
"归档 fork 来源不影响独立 fork 及其真实子代理子树");
|
|
223
333
|
// models/history 加载决策行为链:首读 → 在途不重发 → 失败置空后可见期内不热重试 → 离开可见清理后重回可重试
|
|
224
334
|
const loadDetail = {};
|
|
225
335
|
assert.deepEqual(
|
|
@@ -528,56 +638,86 @@ const hueSpread = new Set();
|
|
|
528
638
|
for (let i = 0; i < 50; i += 1) hueSpread.add(workspaceHue(`/home/user/proj/ws-${i}`));
|
|
529
639
|
assert.ok(hueSpread.size >= 40, `50 个长前缀身份分散到 ${hueSpread.size} 个不同色相(≥40,R-01-003/AC-09)`);
|
|
530
640
|
|
|
531
|
-
// ---- R-01-003/AC-12 OKLCH
|
|
532
|
-
const
|
|
641
|
+
// ---- R-01-003/AC-12 OKLCH 十二前景槽、三档背景变体、主题距离与复合容量 ----
|
|
642
|
+
const workspacePrimaryHues = [55, 100, 145, 190, 235, 280, 325];
|
|
643
|
+
const workspacePaletteHues = WORKSPACE_COLOR_SLOTS.map((slot) => slot.hue);
|
|
533
644
|
const realWorkspaceCluster = [
|
|
534
645
|
"/home/cailei/ops",
|
|
535
646
|
"/home/cailei/proj/docsim",
|
|
536
647
|
"/home/cailei/proj/dsh-activity-pane",
|
|
537
648
|
"/home/cailei/proj/dsh-control-center",
|
|
538
649
|
];
|
|
539
|
-
const resolvedCluster =
|
|
650
|
+
const resolvedCluster = resolveWorkspaceColors(realWorkspaceCluster);
|
|
540
651
|
assert.deepEqual(
|
|
541
|
-
[...resolvedCluster.entries()],
|
|
652
|
+
[...resolvedCluster.entries()].map(([key, color]) => [key, color.foreground.hue]),
|
|
542
653
|
[
|
|
543
654
|
["/home/cailei/ops", 280],
|
|
544
655
|
["/home/cailei/proj/docsim", 55],
|
|
545
656
|
["/home/cailei/proj/dsh-activity-pane", 190],
|
|
546
657
|
["/home/cailei/proj/dsh-control-center", 100],
|
|
547
658
|
],
|
|
548
|
-
"
|
|
549
|
-
);
|
|
550
|
-
assert.deepEqual(
|
|
551
|
-
[...
|
|
552
|
-
[...resolvedCluster.entries()],
|
|
553
|
-
"
|
|
554
|
-
);
|
|
555
|
-
assert.deepEqual([...
|
|
556
|
-
const
|
|
557
|
-
assert.deepEqual([...
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
659
|
+
"真实撞槽子集保持 +3 探测拆分到蓝紫/橙/青/黄绿明显色区(R-01-003/AC-12)",
|
|
660
|
+
);
|
|
661
|
+
assert.deepEqual(
|
|
662
|
+
[...resolveWorkspaceColors([...realWorkspaceCluster].reverse(), "", " ", realWorkspaceCluster[0]).entries()].map(([key, color]) => [key, color.foreground.slot, color.background.slot]),
|
|
663
|
+
[...resolvedCluster.entries()].map(([key, color]) => [key, color.foreground.slot, color.background.slot]),
|
|
664
|
+
"输入顺序、重复项与空白身份不影响前景/背景复合槽位映射(R-01-003/AC-08、AC-12)",
|
|
665
|
+
);
|
|
666
|
+
assert.deepEqual([...resolveWorkspaceColors(null).entries()], [], "无身份集合返回空映射(R-01-003/AC-12)");
|
|
667
|
+
const sevenColors = [...resolveWorkspaceColors(Array.from({ length: 7 }, (_, index) => `/home/user/proj/seven-${index}`)).values()];
|
|
668
|
+
assert.deepEqual([...sevenColors].map((color) => color.foreground.hue).sort((a, b) => a - b), workspacePrimaryHues, "七个工作区仍恰占满七个主色相槽位(R-01-003/AC-12)");
|
|
669
|
+
assert.ok([...sevenColors].every((color) => color.foreground.slot < workspacePrimaryHues.length), "七个工作区不提前使用补充槽位(R-01-003/AC-12)");
|
|
670
|
+
const oklabPoint = (color, theme) => {
|
|
671
|
+
const hue = color.hue * Math.PI / 180;
|
|
672
|
+
const lightness = theme === "dark" ? color.darkL : color.lightL;
|
|
673
|
+
const chroma = theme === "dark" ? color.darkC : color.lightC;
|
|
674
|
+
return [lightness, chroma * Math.cos(hue), chroma * Math.sin(hue)];
|
|
562
675
|
};
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
assert.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
676
|
+
const oklabDistance = (colorA, colorB, theme) => {
|
|
677
|
+
const a = oklabPoint(colorA, theme);
|
|
678
|
+
const b = oklabPoint(colorB, theme);
|
|
679
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
680
|
+
};
|
|
681
|
+
const twelveColors = [...resolveWorkspaceColors(Array.from({ length: 12 }, (_, index) => `/home/user/proj/twelve-${index}`)).values()];
|
|
682
|
+
assert.deepEqual(
|
|
683
|
+
[...twelveColors].map((color) => color.foreground.slot).sort((a, b) => a - b),
|
|
684
|
+
Array.from({ length: 12 }, (_, index) => index),
|
|
685
|
+
"十二个工作区恰好使用十二个唯一前景颜色槽位(R-01-003/AC-12)",
|
|
686
|
+
);
|
|
687
|
+
assert.equal(new Set(workspacePaletteHues).size, workspacePaletteHues.length, "十二槽 hue 不重复(R-01-003/AC-12)");
|
|
688
|
+
assert.ok(workspacePaletteHues.every((hue) => hue > 30 && hue < 330), "十二槽 hue 均避开红色警戒环段(R-01-003/AC-09、AC-12)");
|
|
689
|
+
assert.deepEqual(
|
|
690
|
+
WORKSPACE_BACKGROUND_SLOTS.map((slot) => slot.slot),
|
|
691
|
+
[0, 1, 2],
|
|
692
|
+
"每个前景槽位提供三个背景变体槽(R-01-003/AC-12)",
|
|
693
|
+
);
|
|
694
|
+
assert.equal(new Set(WORKSPACE_BACKGROUND_SLOTS.map((slot) => slot.dark.l)).size, 3, "深色背景变体具有三档独立明度(R-01-003/AC-10、AC-11)");
|
|
695
|
+
assert.equal(new Set(WORKSPACE_BACKGROUND_SLOTS.map((slot) => slot.light.l)).size, 3, "浅色背景变体具有三档独立明度(R-01-003/AC-10、AC-11)");
|
|
696
|
+
for (const theme of ["dark", "light"])
|
|
697
|
+
for (let i = 0; i < twelveColors.length; i += 1)
|
|
698
|
+
for (let j = i + 1; j < twelveColors.length; j += 1)
|
|
699
|
+
assert.ok(oklabDistance(twelveColors[i].foreground, twelveColors[j].foreground, theme) >= 0.11, `${theme} 主题十二槽任意两前景色 OKLab 距离至少 0.11(R-01-003/AC-12)`);
|
|
700
|
+
const thirtySixColors = [...resolveWorkspaceColors(Array.from({ length: 36 }, (_, index) => `/home/user/proj/thirty-six-${index}`)).values()];
|
|
701
|
+
const pairKeys = thirtySixColors.map((color) => `${color.foreground.slot}/${color.background.slot}`);
|
|
702
|
+
assert.equal(new Set(pairKeys).size, 36, "三十六个工作区恰好使用三十六个唯一前景/背景复合槽位(R-01-003/AC-12)");
|
|
703
|
+
for (const foregroundSlot of WORKSPACE_COLOR_SLOTS.map((slot) => slot.slot)) {
|
|
704
|
+
const backgrounds = thirtySixColors.filter((color) => color.foreground.slot === foregroundSlot).map((color) => color.background.slot);
|
|
705
|
+
assert.deepEqual(backgrounds.sort((a, b) => a - b), [0, 1, 2], `前景槽 ${foregroundSlot} 先使用三个背景变体(R-01-003/AC-12)`);
|
|
706
|
+
}
|
|
707
|
+
const crowdedColors = resolveWorkspaceColors(Array.from({ length: 40 }, (_, index) => `/home/user/proj/crowded-${index}`));
|
|
708
|
+
assert.equal(crowdedColors.size, 40, "超容量集合仍为每个身份返回复合颜色槽位并有限终止(R-01-003/AC-12)");
|
|
709
|
+
assert.ok([...crowdedColors.values()].every((color) => color.foreground.slot >= 0 && color.foreground.slot < WORKSPACE_COLOR_SLOTS.length), "超容量时仍只使用十二个前景颜色槽位(R-01-003/AC-12)");
|
|
710
|
+
assert.ok([...crowdedColors.values()].every((color) => color.background.slot >= 0 && color.background.slot < WORKSPACE_BACKGROUND_SLOTS.length), "超容量时仍只使用三个背景变体槽位(R-01-003/AC-12)");
|
|
711
|
+
const pairUses = [...new Set([...crowdedColors.values()].map((color) => `${color.foreground.slot}/${color.background.slot}`))].map((pair) => [...crowdedColors.values()].filter((color) => `${color.foreground.slot}/${color.background.slot}` === pair).length);
|
|
712
|
+
assert.ok(Math.max(...pairUses) - Math.min(...pairUses) <= 1, "超容量时三十六个复合槽位复用计数差不超过 1(R-01-003/AC-12)");
|
|
573
713
|
|
|
574
714
|
// ---- R-01-001/AC-01 活动卡片逐条显示 | R-01-003/AC-01 子代理嵌套 | R-01-003/AC-02 子代理结束即消失 | R-01-006/AC-01 当前会话 ----
|
|
575
715
|
const snapshot = {
|
|
576
716
|
ids: ["sA", "sA-c1", "sA-c2", "sB", "sX"],
|
|
577
717
|
byId: {
|
|
578
718
|
sA: { id: "sA", displayTitle: "主A", running: true, completed: false },
|
|
579
|
-
"sA-c1": { id: "sA-c1", displayTitle: "子1", running: true, parentId: "sA" },
|
|
580
|
-
"sA-c2": { id: "sA-c2", displayTitle: "子2", running: false, completed: true, parentId: "sA" },
|
|
719
|
+
"sA-c1": { id: "sA-c1", displayTitle: "子1", running: true, parentId: "sA", origin: "subagent" },
|
|
720
|
+
"sA-c2": { id: "sA-c2", displayTitle: "子2", running: false, completed: true, parentId: "sA", origin: "subagent" },
|
|
581
721
|
sB: { id: "sB", displayTitle: "主B", running: false, completed: true },
|
|
582
722
|
sX: { id: "sX", displayTitle: "主X", running: false, completed: false },
|
|
583
723
|
},
|
|
@@ -683,8 +823,8 @@ const inheritedActivity = {
|
|
|
683
823
|
ids: ["root", "parent", "child"],
|
|
684
824
|
byId: {
|
|
685
825
|
root: { id: "root", displayTitle: "根母会话", running: false, completed: false, updatedAt: 1900 },
|
|
686
|
-
parent: { id: "parent", displayTitle: "中间母会话", running: false, completed: false, parentId: "root", updatedAt: 1900 },
|
|
687
|
-
child: { id: "child", displayTitle: "活动子会话", running: true, parentId: "parent" },
|
|
826
|
+
parent: { id: "parent", displayTitle: "中间母会话", running: false, completed: false, parentId: "root", origin: "subagent", updatedAt: 1900 },
|
|
827
|
+
child: { id: "child", displayTitle: "活动子会话", running: true, parentId: "parent", origin: "subagent" },
|
|
688
828
|
},
|
|
689
829
|
current: null,
|
|
690
830
|
};
|
|
@@ -714,7 +854,7 @@ const delegCompleted = {
|
|
|
714
854
|
ids: ["root", "child"],
|
|
715
855
|
byId: {
|
|
716
856
|
root: { id: "root", displayTitle: "母会话", running: false, updatedAt: 1900 },
|
|
717
|
-
child: { id: "child", displayTitle: "活动子会话", running: true, parentId: "root" },
|
|
857
|
+
child: { id: "child", displayTitle: "活动子会话", running: true, parentId: "root", origin: "subagent" },
|
|
718
858
|
},
|
|
719
859
|
current: null,
|
|
720
860
|
};
|
|
@@ -1502,6 +1642,33 @@ assert.deepEqual(
|
|
|
1502
1642
|
{ elapsedMs: 47_000, outputTokens: 0, rateTokS: null },
|
|
1503
1643
|
"零速率归一为空,token 统计仍可放在进度条下方",
|
|
1504
1644
|
);
|
|
1645
|
+
assert.deepEqual(
|
|
1646
|
+
statsFromProjection(
|
|
1647
|
+
{
|
|
1648
|
+
tokenUsage: { uncachedInputTokens: 100, cacheReadTokens: 20, cacheWriteTokens: 10, outputTokens: 144 },
|
|
1649
|
+
sessionStats: { decodeTokens: 144, decodeMs: 3_000 },
|
|
1650
|
+
},
|
|
1651
|
+
47_000,
|
|
1652
|
+
),
|
|
1653
|
+
{ elapsedMs: 47_000, outputTokens: 144, rateTokS: 48, inputTokens: 130, cacheHitPct: 15 },
|
|
1654
|
+
"最近卡复用列表投影派生 tok/s、缓存、输入/输出与固定耗时(R-01-013/AC-12)",
|
|
1655
|
+
);
|
|
1656
|
+
assert.deepEqual(
|
|
1657
|
+
mergeRuntimeStats(
|
|
1658
|
+
{ outputTokens: null, inputTokens: 24, cacheHitPct: null, rateTokS: null },
|
|
1659
|
+
{ outputTokens: 96, inputTokens: 12, cacheHitPct: 40, rateTokS: 8 },
|
|
1660
|
+
),
|
|
1661
|
+
{ outputTokens: 96, inputTokens: 24, cacheHitPct: 40, rateTokS: 8 },
|
|
1662
|
+
"等待/运行统计按字段回退,缺失投影不覆盖最后已知有效值(R-01-009/AC-13)",
|
|
1663
|
+
);
|
|
1664
|
+
assert.deepEqual(
|
|
1665
|
+
mergeRuntimeStats(
|
|
1666
|
+
{ outputTokens: 0, inputTokens: 0, cacheHitPct: 0, rateTokS: null },
|
|
1667
|
+
{ outputTokens: 96, inputTokens: 12, cacheHitPct: 40, rateTokS: 8 },
|
|
1668
|
+
),
|
|
1669
|
+
{ outputTokens: 0, inputTokens: 0, cacheHitPct: 0, rateTokS: 8 },
|
|
1670
|
+
"统计合并保留有效的零值,仅对缺失字段回退(R-01-009/AC-13)",
|
|
1671
|
+
);
|
|
1505
1672
|
assert.deepEqual(
|
|
1506
1673
|
usageSummary({ uncachedInputTokens: 100, cacheReadTokens: 700, cacheWriteTokens: 200 }),
|
|
1507
1674
|
{ inputTokens: 1_000, cacheHitPct: 70 },
|
|
@@ -2445,7 +2612,7 @@ assert.equal(completionReminder(holdBase, { lastTurnEnd: 1000, ackedAt: 1000 },
|
|
|
2445
2612
|
assert.equal(completionReminder(holdBase, { lastTurnEnd: 1000, ackedAt: 2000 }, false), false, "ackedAt 晚于 lastTurnEnd 不成立");
|
|
2446
2613
|
assert.equal(completionReminder(holdBase, null, false), false, "无完成登记不成立(升级不回溯补发提醒)");
|
|
2447
2614
|
assert.equal(completionReminder(holdBase, { lastTurnEnd: 0, ackedAt: null }, false), false, "lastTurnEnd 非法不作数");
|
|
2448
|
-
assert.equal(completionReminder({ id: "m-c1", parentId: "m", displayTitle: "子S" }, { lastTurnEnd: 1000, ackedAt: null }, true), false, "子代理不产生完成提醒");
|
|
2615
|
+
assert.equal(completionReminder({ id: "m-c1", parentId: "m", origin: "subagent", displayTitle: "子S" }, { lastTurnEnd: 1000, ackedAt: null }, true), false, "子代理不产生完成提醒");
|
|
2449
2616
|
// 打开/切换当前会话不解除(AC-05):完成提醒成立与 current 无关。
|
|
2450
2617
|
// R-01-002/AC-05 打开或切换当前会话不解除完成提醒:判定与 current 无关(C-030)。
|
|
2451
2618
|
const holdSnap = { ids: ["sB"], byId: { sB: holdBase }, current: "sA" };
|
|
@@ -2483,7 +2650,7 @@ assert.deepEqual(
|
|
|
2483
2650
|
// 新回合隐式更替:lastTurnEnd 前移后旧确认游标不再覆盖新回合(仍未确认则对新回合成立)。
|
|
2484
2651
|
assert.equal(completionReminder(holdBase, { lastTurnEnd: 2000, ackedAt: 1500 }, false), true, "新回合完成后提醒针对新回合重新成立");
|
|
2485
2652
|
// 委托周期抑制:后代活动期间完成提醒不生效(呈现不依赖宿主 completed)。
|
|
2486
|
-
const delegDoneMix = { ids: ["root", "root-c1"], byId: { root: holdBase, "root-c1": { id: "root-c1", displayTitle: "子S", parentId: "root", running: true } }, current: null };
|
|
2653
|
+
const delegDoneMix = { ids: ["root", "root-c1"], byId: { root: holdBase, "root-c1": { id: "root-c1", displayTitle: "子S", parentId: "root", origin: "subagent", running: true } }, current: null };
|
|
2487
2654
|
assert.deepEqual(
|
|
2488
2655
|
buildEntries(delegDoneMix, [], {}, acks(1000), new Set(["root"])).map((e) => [e.id, e.pendingText ?? null]),
|
|
2489
2656
|
[["root", null], ["root-c1", null]],
|
|
@@ -2505,7 +2672,7 @@ assert.equal(errorReminder({ id: "sB", displayTitle: "旧B" }, { lastTurnEndKind
|
|
|
2505
2672
|
assert.equal(errorReminder({ id: "sB", displayTitle: "旧B" }, { lastTurnEndKind: "completed" }, false), false, "正常回合不成立");
|
|
2506
2673
|
assert.equal(errorReminder({ id: "sB", displayTitle: "旧B" }, { lastTurnEndKind: "error", ackedAt: 99999 }, false), true, "错误提醒不消费 ack 游标(ackedAt 不影响成立)");
|
|
2507
2674
|
assert.equal(errorReminder({ id: "sB", displayTitle: "旧B" }, null, false), false, "无登记不成立(升级不回溯补发错误提醒)");
|
|
2508
|
-
assert.equal(errorReminder({ id: "m-c1", parentId: "m", displayTitle: "子S" }, { lastTurnEndKind: "error" }, true), false, "子代理不产生错误提醒");
|
|
2675
|
+
assert.equal(errorReminder({ id: "m-c1", parentId: "m", origin: "subagent", displayTitle: "子S" }, { lastTurnEndKind: "error" }, true), false, "子代理不产生错误提醒");
|
|
2509
2676
|
const errEntries = buildEntries(holdSnap, [], {}, errAcks("error", "The engine is currently overloaded, please try again later"));
|
|
2510
2677
|
assert.deepEqual(
|
|
2511
2678
|
errEntries.map((e) => [e.id, e.kind, e.pendingText ?? null, e.waitClass, e.noteText]),
|
|
@@ -2557,7 +2724,7 @@ assert.deepEqual(
|
|
|
2557
2724
|
const clearedErrAcked = buildEntries(holdSnap, [], {}, errAcks("completed", null, 2000));
|
|
2558
2725
|
assert.deepEqual(clearedErrAcked.map((e) => [e.id, e.kind]), [], "新回合正常结束且已确认后完全退出活动区(错误提醒无确认按钮语义)");
|
|
2559
2726
|
// 委托周期抑制:后代活动期间错误提醒不生效。
|
|
2560
|
-
const delegErrMix = { ids: ["root", "root-c1"], byId: { root: holdBase, "root-c1": { id: "root-c1", displayTitle: "子S", parentId: "root", running: true } }, current: null };
|
|
2727
|
+
const delegErrMix = { ids: ["root", "root-c1"], byId: { root: holdBase, "root-c1": { id: "root-c1", displayTitle: "子S", parentId: "root", origin: "subagent", running: true } }, current: null };
|
|
2561
2728
|
const delegErrAcks = new Map([["root", { lastTurnEnd: 1000, lastTurnEndKind: "error", lastTurnEndError: "boom", ackedAt: null }]]);
|
|
2562
2729
|
assert.deepEqual(
|
|
2563
2730
|
buildEntries(delegErrMix, [], {}, delegErrAcks, new Set(["root"])).map((e) => e.id),
|
|
@@ -2599,7 +2766,7 @@ const recentSubSnap = {
|
|
|
2599
2766
|
ids: ["m", "m-c1"],
|
|
2600
2767
|
byId: {
|
|
2601
2768
|
m: { id: "m", displayTitle: "主M", running: false, completed: false, updatedAt: NOW - 1_000 },
|
|
2602
|
-
"m-c1": { id: "m-c1", displayTitle: "子S", running: false, completed: false, parentId: "m", updatedAt: NOW - 500 },
|
|
2769
|
+
"m-c1": { id: "m-c1", displayTitle: "子S", running: false, completed: false, parentId: "m", origin: "subagent", updatedAt: NOW - 500 },
|
|
2603
2770
|
},
|
|
2604
2771
|
current: null,
|
|
2605
2772
|
};
|
|
@@ -2648,6 +2815,67 @@ assert.deepEqual(
|
|
|
2648
2815
|
"归档集同样接受 Set 形态",
|
|
2649
2816
|
);
|
|
2650
2817
|
|
|
2818
|
+
// R-01-010/AC-01:归档集合同时驱动活动区过滤;未确认完成提醒也不得在归档后滞留。
|
|
2819
|
+
const activeArchivedSnap = {
|
|
2820
|
+
ids: ["sKeep", "sGone"],
|
|
2821
|
+
byId: {
|
|
2822
|
+
sKeep: { id: "sKeep", displayTitle: "保留K", running: true },
|
|
2823
|
+
sGone: { id: "sGone", displayTitle: "归档G", running: false },
|
|
2824
|
+
},
|
|
2825
|
+
current: null,
|
|
2826
|
+
};
|
|
2827
|
+
assert.deepEqual(
|
|
2828
|
+
buildEntries(activeArchivedSnap, [], {}, new Map([["sGone", { lastTurnEnd: NOW, ackedAt: null }]]), null, ["sGone"]).map((e) => e.id),
|
|
2829
|
+
["sKeep"],
|
|
2830
|
+
"归档会话从活动区消失,不因未确认完成提醒继续滞留",
|
|
2831
|
+
);
|
|
2832
|
+
const archivedStatusSnap = {
|
|
2833
|
+
ids: ["sRunning", "sDone", "sError", "sDelegating"],
|
|
2834
|
+
byId: {
|
|
2835
|
+
sRunning: { id: "sRunning", displayTitle: "运行中归档", running: true },
|
|
2836
|
+
sDone: { id: "sDone", displayTitle: "完成归档", running: false },
|
|
2837
|
+
sError: { id: "sError", displayTitle: "错误归档", running: false },
|
|
2838
|
+
sDelegating: { id: "sDelegating", displayTitle: "委托归档", running: false },
|
|
2839
|
+
},
|
|
2840
|
+
current: null,
|
|
2841
|
+
};
|
|
2842
|
+
assert.deepEqual(
|
|
2843
|
+
buildEntries(
|
|
2844
|
+
archivedStatusSnap,
|
|
2845
|
+
[],
|
|
2846
|
+
{},
|
|
2847
|
+
new Map([
|
|
2848
|
+
["sDone", { lastTurnEnd: NOW, ackedAt: null }],
|
|
2849
|
+
["sError", { lastTurnEnd: NOW, lastTurnEndKind: "error", lastTurnEndError: "boom", ackedAt: null }],
|
|
2850
|
+
]),
|
|
2851
|
+
new Set(["sDelegating"]),
|
|
2852
|
+
new Set(["sRunning", "sDone", "sError", "sDelegating"]),
|
|
2853
|
+
).map((e) => e.id),
|
|
2854
|
+
[],
|
|
2855
|
+
"Set 归档集合同时过滤运行、完成提醒、错误提醒与委托周期中的会话",
|
|
2856
|
+
);
|
|
2857
|
+
const archivedLineageSnap = {
|
|
2858
|
+
ids: ["root", "root-child", "root-grandchild", "keep-root", "keep-child"],
|
|
2859
|
+
byId: {
|
|
2860
|
+
root: { id: "root", displayTitle: "归档母会话", running: false },
|
|
2861
|
+
"root-child": { id: "root-child", displayTitle: "归档子代理", parentId: "root", origin: "subagent", running: true },
|
|
2862
|
+
"root-grandchild": { id: "root-grandchild", displayTitle: "归档孙代理", parentId: "root-child", origin: "subagent", running: true },
|
|
2863
|
+
"keep-root": { id: "keep-root", displayTitle: "保留母会话", running: false },
|
|
2864
|
+
"keep-child": { id: "keep-child", displayTitle: "被归档子代理", parentId: "keep-root", origin: "subagent", running: true },
|
|
2865
|
+
},
|
|
2866
|
+
current: null,
|
|
2867
|
+
};
|
|
2868
|
+
assert.deepEqual(
|
|
2869
|
+
buildEntries(archivedLineageSnap, [], {}, null, null, ["root"]).map((e) => e.id),
|
|
2870
|
+
["keep-root", "keep-child"],
|
|
2871
|
+
"归档母会话时其子会话与更深层后代均不产出活动条目",
|
|
2872
|
+
);
|
|
2873
|
+
assert.deepEqual(
|
|
2874
|
+
buildEntries(archivedLineageSnap, [], {}, null, null, ["keep-child"]).map((e) => e.id),
|
|
2875
|
+
["root", "root-child", "root-grandchild"],
|
|
2876
|
+
"归档子会话后不因残留后代活动把未归档母会话留在活动区",
|
|
2877
|
+
);
|
|
2878
|
+
|
|
2651
2879
|
// ---- R-01-014/AC-01 列表在途显示加载指示而非空态 ----
|
|
2652
2880
|
assert.equal(listLoadState(null), "loading", "快照缺失视为列表在途");
|
|
2653
2881
|
assert.equal(listLoadState({ phase: "pending" }), "loading", "phase 为 pending 视为列表在途");
|
|
@@ -2781,6 +3009,16 @@ assert.ok(
|
|
|
2781
3009
|
clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);"),
|
|
2782
3010
|
"列表 phase 与历史时间文案必须参与结构化渲染签名,空列表不得冻结在加载/失败状态(T-087)",
|
|
2783
3011
|
);
|
|
3012
|
+
assert.ok(
|
|
3013
|
+
clientSource.includes("scroll?.querySelector?.(`.${LIST_CLASS} .${CARD_CLASS}[data-current]`)") &&
|
|
3014
|
+
!clientSource.includes("scroll?.querySelector?.(`.${CARD_CLASS}[data-current]`)"),
|
|
3015
|
+
"当前卡片自动滚动仅定位活动列表,完成卡迁入历史后不追随历史区(R-01-006/AC-02)",
|
|
3016
|
+
);
|
|
3017
|
+
assert.ok(
|
|
3018
|
+
clientSource.includes("source.contains(focusedElement)") &&
|
|
3019
|
+
clientSource.includes("cardsById.get(focusAfterMigrationId)?.el.focus()"),
|
|
3020
|
+
"活动卡迁移时将已有键盘焦点交给仍在活动区的相邻卡片(R-01-005/AC-01)",
|
|
3021
|
+
);
|
|
2784
3022
|
// R-01-013/AC-02 回归:卡片标题必须随快照更新——单卡渲染异常不得冻结其余卡片
|
|
2785
3023
|
// (此前渲染签名先于卡片循环提交且无异常隔离,故障卡及其后全部卡片永久滞留旧标题,
|
|
2786
3024
|
// 历史卡因此停在首条消息形态的 fallback 标题,与左侧栏脱节)。
|
|
@@ -2815,13 +3053,13 @@ assert.ok(
|
|
|
2815
3053
|
bundle.includes("list.insertBefore(rec.el, ref)"),
|
|
2816
3054
|
"卡片仅在顺序/归属变化时移动 DOM(insertBefore 位置守卫)",
|
|
2817
3055
|
);
|
|
2818
|
-
// ---- R-01-009/AC-02、R-01-009/AC-05、R-01-012/AC-01..04、R-01-013/AC-01..06 ----
|
|
3056
|
+
// ---- R-01-009/AC-02、R-01-009/AC-05、R-01-012/AC-01..04、R-01-013/AC-01..06、AC-12 ----
|
|
2819
3057
|
assert.ok(bundle.includes("foldedConversationTimeline"), "活动卡时间线由折叠分组唯一来源派生(R-01-012、R-01-017)");
|
|
2820
3058
|
assert.ok(!bundle.includes("dap-trace-time"), "工作项时间线不渲染行级耗时元素,对齐主会话窗口(R-01-009/AC-07、C-012)");
|
|
2821
3059
|
assert.ok(!bundle.includes("PROGRESS_THINK_BASE") && !bundle.includes("progressFloor"), "回合进度纯时间驱动,无思考基线/单调下限残留(R-01-009/AC-06、C-014)");
|
|
2822
3060
|
assert.ok(bundle.includes("progressHalfLifeSec"), "bundle 含半衰期速率校准函数(R-01-009/AC-06、C-025、C-044)");
|
|
2823
3061
|
assert.ok(
|
|
2824
|
-
bundle.includes("halfLifeSec: progressHalfLifeSec({ rateTokS })"),
|
|
3062
|
+
bundle.includes("halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS })"),
|
|
2825
3063
|
"进度赋值每帧按最新实测速率现算半衰期(R-01-009/AC-06、C-044)",
|
|
2826
3064
|
);
|
|
2827
3065
|
assert.ok(
|
|
@@ -2841,9 +3079,18 @@ assert.ok(
|
|
|
2841
3079
|
"统计行双段结构:左列文本 + 右置时长(R-01-009/AC-05)",
|
|
2842
3080
|
);
|
|
2843
3081
|
assert.ok(bundle.includes("function renderTokenStats"), "运行卡统计行继续复用既有渲染逻辑(R-01-009/AC-05)");
|
|
3082
|
+
// R-01-009/AC-13 等待卡保留进入等待前最后已知的 token 统计
|
|
3083
|
+
assert.ok(
|
|
3084
|
+
bundle.includes('return [head, row, makeEl("div", "dap-trace"), statsRow, foot];'),
|
|
3085
|
+
"等待卡骨架在时间线后保留统计行(R-01-009/AC-13)",
|
|
3086
|
+
);
|
|
2844
3087
|
assert.ok(
|
|
2845
|
-
bundle.includes("
|
|
2846
|
-
"
|
|
3088
|
+
bundle.includes("renderTokenStats(el, entry);") && bundle.includes("removeAwaitingHeadDuration(el);"),
|
|
3089
|
+
"等待卡复用统计行并清理胶囊同行的旧耗时节点(R-01-009/AC-12、AC-13)",
|
|
3090
|
+
);
|
|
3091
|
+
assert.ok(
|
|
3092
|
+
bundle.includes("function removeAwaitingHeadDuration") && bundle.includes("awaitHead.append(capsule);"),
|
|
3093
|
+
"等待类型胶囊独立成行,固定耗时回到统计行(R-01-009/AC-12)",
|
|
2847
3094
|
);
|
|
2848
3095
|
assert.ok(bundle.includes("lastTurnDuration({"), "等待卡耗时由最近完整回合边界派生(R-01-009/AC-12)");
|
|
2849
3096
|
assert.ok(bundle.includes("`输入 ${fmtTokens("), "统计行含输入/输出中文短标签(R-01-009/AC-05)");
|
|
@@ -2858,6 +3105,14 @@ assert.ok(
|
|
|
2858
3105
|
);
|
|
2859
3106
|
assert.ok(!bundle.includes("≈"), "速率不再携带约等于符号(R-01-009/AC-05)");
|
|
2860
3107
|
assert.ok(bundle.includes("dap-history-line"), "历史卡包含用户/agent 两条消息预览行");
|
|
3108
|
+
// R-01-013/AC-12 最近历史卡保留最近回合统计
|
|
3109
|
+
assert.ok(
|
|
3110
|
+
bundle.includes('return [head, row, userLine, agentLine, statsRow, makeEl("div", "dap-note")];'),
|
|
3111
|
+
"历史卡统计行位于助手预览后、活动时间前",
|
|
3112
|
+
);
|
|
3113
|
+
assert.ok(bundle.includes("statsFromProjection"), "历史卡统计复用列表投影派生函数(R-01-013/AC-12)");
|
|
3114
|
+
assert.ok(bundle.includes("lastRuntimeStats"), "会话结束时保留最后已知运行统计(R-01-013/AC-12)");
|
|
3115
|
+
assert.ok(bundle.includes("recentDurationFallbackIds"), "历史卡缺回合边界时复用 history 补读(R-01-013/AC-12)");
|
|
2861
3116
|
// R-01-003/AC-04
|
|
2862
3117
|
assert.ok(bundle.includes("parentId: m.isSub ? String(parentId) : null"), "活动卡条目保留直属母会话 id");
|
|
2863
3118
|
assert.ok(bundle.includes("function trackRuns(entries)"), "母会话轨道拓扑由纯函数 trackRuns 一次求出");
|
|
@@ -2915,16 +3170,16 @@ assert.ok(
|
|
|
2915
3170
|
assert.ok(
|
|
2916
3171
|
clientSource.includes("const INDENT_PX = 16;") &&
|
|
2917
3172
|
bundle.includes("Math.round(parent.left + indentPx / 2 + 1)") &&
|
|
2918
|
-
bundle.includes("renderCardIntoList(activeList, entry, cardsById, index, 1,
|
|
3173
|
+
bundle.includes("renderCardIntoList(activeList, entry, cardsById, index, 1, colorByWorkspace)"),
|
|
2919
3174
|
"几何耦合钉住:INDENT_PX=16、轨道 left 由母会话卡片左缘测量推导(+半槽+1px border,取整后与横线起笔相接)与活动区卡片 offset=1(轨道层为首子节点),改任一必须同步",
|
|
2920
3175
|
);
|
|
2921
3176
|
// R-01-003/AC-05
|
|
2922
3177
|
assert.ok(bundle.includes("function activeSessionIds(byId = {})"), "活动子代理沿 parentId 链补齐活动祖先");
|
|
2923
3178
|
// ---- R-01-016/AC-01 等待卡保留最近工作项时间线 ----
|
|
2924
3179
|
assert.ok(
|
|
2925
|
-
bundle.includes(
|
|
2926
|
-
bundle.includes('return [head, row, makeEl("div", "dap-trace"), foot];'),
|
|
2927
|
-
"awaiting
|
|
3180
|
+
bundle.includes("awaitHead.append(capsule);") &&
|
|
3181
|
+
bundle.includes('return [head, row, makeEl("div", "dap-trace"), statsRow, foot];'),
|
|
3182
|
+
"awaiting 骨架在标题行与统计行、末行两段(胶囊+正文)之间含时间线,固定耗时由统计行承载(R-01-016/AC-01、R-01-009/AC-12,C-043)",
|
|
2928
3183
|
);
|
|
2929
3184
|
// ---- R-01-002/AC-10 完成提醒卡「移入历史」按钮 ----
|
|
2930
3185
|
assert.ok(
|
|
@@ -3397,20 +3652,27 @@ assert.ok(bundle.includes('workspace.append(workspaceIcon, makeEl("span", "dap-w
|
|
|
3397
3652
|
assert.ok(bundle.includes("if (workspaceText !== null) restoreTextField(workspaceText, entry.workspaceTitle)"), "热装旧骨架无文本段时容空跳过,不中断渲染(R-01-003/AC-06 健壮性)");
|
|
3398
3653
|
|
|
3399
3654
|
// R-01-003/AC-08、AC-09、AC-10、AC-11、AC-12
|
|
3400
|
-
//
|
|
3401
|
-
// CSS 以 OKLCH
|
|
3402
|
-
assert.ok(bundle.includes("
|
|
3403
|
-
assert.ok(bundle.includes("
|
|
3404
|
-
assert.ok(bundle.includes('
|
|
3405
|
-
assert.ok(bundle.includes(
|
|
3406
|
-
assert.ok(bundle.includes("--dap-workspace-
|
|
3655
|
+
// 工作区徽标按身份派生基色、经同屏前景/背景复合槽位消解后着色:渲染层写入
|
|
3656
|
+
// 前景 hue/L/C 与背景变体主题参数,CSS 以 OKLCH 前景色呈现文字、独立背景源色混合底色/描边;胶囊几何与字号不变。
|
|
3657
|
+
assert.ok(bundle.includes("resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey))"), "渲染层按同帧可见身份集合消解十二槽颜色(R-01-003/AC-12)");
|
|
3658
|
+
assert.ok(bundle.includes("colorByWorkspace.get(entry.workspaceKey)"), "每张卡使用集合消解后的工作区颜色槽位(R-01-003/AC-08、AC-12)");
|
|
3659
|
+
assert.ok(bundle.includes('entry.workspaceTitle !== "" && foreground !== undefined && background !== undefined'), "缺少工作区身份或背景变体时隐藏徽标而不使用默认颜色(R-01-003/AC-08)");
|
|
3660
|
+
assert.ok(bundle.includes("style.setProperty(property, next)"), "渲染层把槽位颜色变量写入徽标(R-01-003/AC-08、AC-12)");
|
|
3661
|
+
assert.ok(bundle.includes('"--dap-workspace-dark-l": foreground?.darkL'), "渲染层把前景深色主题 L 写入徽标(R-01-003/AC-10、AC-11)");
|
|
3662
|
+
assert.ok(bundle.includes('"--dap-workspace-light-l": foreground?.lightL'), "渲染层把前景浅色主题 L 写入徽标(R-01-003/AC-10、AC-11)");
|
|
3663
|
+
assert.ok(bundle.includes('"--dap-workspace-bg-dark-l": background?.dark?.l'), "渲染层把背景变体深色主题 L 写入徽标(R-01-003/AC-10、AC-11)");
|
|
3664
|
+
assert.ok(bundle.includes('"--dap-workspace-bg-light-l": background?.light?.l'), "渲染层把背景变体浅色主题 L 写入徽标(R-01-003/AC-10、AC-11)");
|
|
3665
|
+
assert.ok(bundle.includes("for (const property of Object.keys(colorVariables)) workspaceLabel.style.removeProperty(property)"), "徽标隐藏时移除全部前景/背景颜色变量,不留陈旧着色(R-01-003/AC-08)");
|
|
3666
|
+
assert.ok(bundle.includes("var(--dap-workspace-dark-l, 0.78)"), "深色主题使用槽位 OKLCH 明度变量(R-01-003/AC-11)");
|
|
3667
|
+
assert.ok(bundle.includes("var(--dap-workspace-dark-c, 0.16)"), "深色主题使用槽位 OKLCH 彩度变量(R-01-003/AC-11)");
|
|
3407
3668
|
assert.ok(bundle.includes("color: var(--dap-workspace-color)"), "徽标文字直接使用调色板色、不混 currentColor(R-01-003/AC-11)");
|
|
3408
|
-
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-color)
|
|
3409
|
-
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-color)
|
|
3669
|
+
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-background-color) var(--dap-workspace-bg-dark-mix, 31%), transparent)"), "深色主题底色使用加强的独立背景变体混合(R-01-003/AC-10、AC-11)");
|
|
3670
|
+
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-color) var(--dap-workspace-bg-dark-border-mix, 52%), transparent)"), "深色主题描边使用高对比前景色混合(R-01-003/AC-10、AC-11)");
|
|
3410
3671
|
assert.ok(bundle.includes("body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {"), "浅色主题单独校准徽标配色(R-01-003/AC-10、AC-11)");
|
|
3411
|
-
assert.ok(bundle.includes("--dap-workspace-
|
|
3412
|
-
assert.ok(bundle.includes("
|
|
3413
|
-
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-color)
|
|
3672
|
+
assert.ok(bundle.includes("var(--dap-workspace-light-l, 0.48)"), "浅色主题使用槽位 OKLCH 明度变量(R-01-003/AC-11)");
|
|
3673
|
+
assert.ok(bundle.includes("var(--dap-workspace-light-c, 0.15)"), "浅色主题使用槽位 OKLCH 彩度变量(R-01-003/AC-11)");
|
|
3674
|
+
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-background-color) var(--dap-workspace-bg-light-mix, 27%), transparent)"), "浅色主题底色使用加强的独立背景变体混合(R-01-003/AC-10、AC-11)");
|
|
3675
|
+
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-color) var(--dap-workspace-bg-light-border-mix, 42%), transparent)"), "浅色主题描边使用高对比前景色混合(R-01-003/AC-10、AC-11)");
|
|
3414
3676
|
assert.ok(!bundle.includes("color-mix(in srgb, var(--dap-workspace-color) 92%, currentColor)"), "工作区文字不得再以 currentColor 冲淡调色板色(R-01-003/AC-11)");
|
|
3415
3677
|
|
|
3416
3678
|
// R-01-010/AC-01、R-01-010/AC-05
|