dsh-activity-pane 0.4.0 → 0.7.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 +287 -62
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance.mjs +4 -2
- package/scripts/check.mjs +274 -53
- package/src/client.mjs +123 -26
- package/src/core.mjs +127 -36
- package/src/navigation.mjs +37 -0
package/scripts/check.mjs
CHANGED
|
@@ -73,7 +73,9 @@ import {
|
|
|
73
73
|
summarizeToolArguments,
|
|
74
74
|
usageSummary,
|
|
75
75
|
workspaceHue,
|
|
76
|
-
|
|
76
|
+
WORKSPACE_COLOR_SLOTS,
|
|
77
|
+
WORKSPACE_BACKGROUND_SLOTS,
|
|
78
|
+
resolveWorkspaceColors,
|
|
77
79
|
workspaceInfoForSession,
|
|
78
80
|
} from "../src/core.mjs";
|
|
79
81
|
import {
|
|
@@ -81,6 +83,7 @@ import {
|
|
|
81
83
|
bindBackdropDismiss,
|
|
82
84
|
bindCardActivation,
|
|
83
85
|
openSession,
|
|
86
|
+
scrollCardIntoView,
|
|
84
87
|
shouldDismissDrawerOnActivation,
|
|
85
88
|
suppressComposerAutofocus,
|
|
86
89
|
} from "../src/navigation.mjs";
|
|
@@ -114,6 +117,56 @@ assert.equal(
|
|
|
114
117
|
"sessions.open 失败时交给调用方进入 refresh/retry",
|
|
115
118
|
);
|
|
116
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
|
+
|
|
117
170
|
// ---- R-01-005/AC-01、R-02-003/AC-01 card 自身 click/键盘监听与卸载 ----
|
|
118
171
|
const cardListeners = new Map();
|
|
119
172
|
const card = {
|
|
@@ -221,7 +274,62 @@ assert.equal(escapeCssString("a\fb"), "a\\c b", "换页按 CSS 字符串码位
|
|
|
221
274
|
assert.equal(escapeCssString("a\0b"), "a�b", "NUL 归一为替换字符");
|
|
222
275
|
// R-01-012/AC-01
|
|
223
276
|
assert.equal(isSubagentRow({ parentId: "ghost" }, {}), false, "父级不在列表时按主会话处理(仍允许 models 读取)");
|
|
224
|
-
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 及其真实子代理子树");
|
|
225
333
|
// models/history 加载决策行为链:首读 → 在途不重发 → 失败置空后可见期内不热重试 → 离开可见清理后重回可重试
|
|
226
334
|
const loadDetail = {};
|
|
227
335
|
assert.deepEqual(
|
|
@@ -530,56 +638,86 @@ const hueSpread = new Set();
|
|
|
530
638
|
for (let i = 0; i < 50; i += 1) hueSpread.add(workspaceHue(`/home/user/proj/ws-${i}`));
|
|
531
639
|
assert.ok(hueSpread.size >= 40, `50 个长前缀身份分散到 ${hueSpread.size} 个不同色相(≥40,R-01-003/AC-09)`);
|
|
532
640
|
|
|
533
|
-
// ---- R-01-003/AC-12 OKLCH
|
|
534
|
-
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);
|
|
535
644
|
const realWorkspaceCluster = [
|
|
536
645
|
"/home/cailei/ops",
|
|
537
646
|
"/home/cailei/proj/docsim",
|
|
538
647
|
"/home/cailei/proj/dsh-activity-pane",
|
|
539
648
|
"/home/cailei/proj/dsh-control-center",
|
|
540
649
|
];
|
|
541
|
-
const resolvedCluster =
|
|
650
|
+
const resolvedCluster = resolveWorkspaceColors(realWorkspaceCluster);
|
|
542
651
|
assert.deepEqual(
|
|
543
|
-
[...resolvedCluster.entries()],
|
|
652
|
+
[...resolvedCluster.entries()].map(([key, color]) => [key, color.foreground.hue]),
|
|
544
653
|
[
|
|
545
654
|
["/home/cailei/ops", 280],
|
|
546
655
|
["/home/cailei/proj/docsim", 55],
|
|
547
656
|
["/home/cailei/proj/dsh-activity-pane", 190],
|
|
548
657
|
["/home/cailei/proj/dsh-control-center", 100],
|
|
549
658
|
],
|
|
550
|
-
"
|
|
551
|
-
);
|
|
552
|
-
assert.deepEqual(
|
|
553
|
-
[...
|
|
554
|
-
[...resolvedCluster.entries()],
|
|
555
|
-
"
|
|
556
|
-
);
|
|
557
|
-
assert.deepEqual([...
|
|
558
|
-
const
|
|
559
|
-
assert.deepEqual([...
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
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)];
|
|
564
675
|
};
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
assert.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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)");
|
|
575
713
|
|
|
576
714
|
// ---- R-01-001/AC-01 活动卡片逐条显示 | R-01-003/AC-01 子代理嵌套 | R-01-003/AC-02 子代理结束即消失 | R-01-006/AC-01 当前会话 ----
|
|
577
715
|
const snapshot = {
|
|
578
716
|
ids: ["sA", "sA-c1", "sA-c2", "sB", "sX"],
|
|
579
717
|
byId: {
|
|
580
718
|
sA: { id: "sA", displayTitle: "主A", running: true, completed: false },
|
|
581
|
-
"sA-c1": { id: "sA-c1", displayTitle: "子1", running: true, parentId: "sA" },
|
|
582
|
-
"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" },
|
|
583
721
|
sB: { id: "sB", displayTitle: "主B", running: false, completed: true },
|
|
584
722
|
sX: { id: "sX", displayTitle: "主X", running: false, completed: false },
|
|
585
723
|
},
|
|
@@ -685,8 +823,8 @@ const inheritedActivity = {
|
|
|
685
823
|
ids: ["root", "parent", "child"],
|
|
686
824
|
byId: {
|
|
687
825
|
root: { id: "root", displayTitle: "根母会话", running: false, completed: false, updatedAt: 1900 },
|
|
688
|
-
parent: { id: "parent", displayTitle: "中间母会话", running: false, completed: false, parentId: "root", updatedAt: 1900 },
|
|
689
|
-
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" },
|
|
690
828
|
},
|
|
691
829
|
current: null,
|
|
692
830
|
};
|
|
@@ -716,7 +854,7 @@ const delegCompleted = {
|
|
|
716
854
|
ids: ["root", "child"],
|
|
717
855
|
byId: {
|
|
718
856
|
root: { id: "root", displayTitle: "母会话", running: false, updatedAt: 1900 },
|
|
719
|
-
child: { id: "child", displayTitle: "活动子会话", running: true, parentId: "root" },
|
|
857
|
+
child: { id: "child", displayTitle: "活动子会话", running: true, parentId: "root", origin: "subagent" },
|
|
720
858
|
},
|
|
721
859
|
current: null,
|
|
722
860
|
};
|
|
@@ -897,6 +1035,11 @@ assert.deepEqual(
|
|
|
897
1035
|
);
|
|
898
1036
|
assert.equal(fmtElapsedMs(47_000), "47s", "时长短格式");
|
|
899
1037
|
assert.equal(fmtElapsedMs(193_000), "3m13s", "时长分秒格式");
|
|
1038
|
+
// R-01-009/AC-05 超过 1 小时按时分秒显示
|
|
1039
|
+
assert.equal(fmtElapsedMs(3_723_000), "1h2m3s", "时长时分秒格式");
|
|
1040
|
+
assert.equal(fmtElapsedMs(9_772_000), "2h42m52s", "多小时格式");
|
|
1041
|
+
assert.equal(fmtElapsedMs(3_600_000), "1h0m0s", "整小时保留零段");
|
|
1042
|
+
assert.equal(fmtElapsedMs(3_599_500), "1h0m0s", "秒数四舍五入进位到小时");
|
|
900
1043
|
// R-01-009/AC-03
|
|
901
1044
|
assert.equal(fmtElapsedMs(NaN), "", "NaN 时长归一为空");
|
|
902
1045
|
assert.equal(fmtElapsedMs(Infinity), "", "Infinity 时长归一为空");
|
|
@@ -2474,7 +2617,7 @@ assert.equal(completionReminder(holdBase, { lastTurnEnd: 1000, ackedAt: 1000 },
|
|
|
2474
2617
|
assert.equal(completionReminder(holdBase, { lastTurnEnd: 1000, ackedAt: 2000 }, false), false, "ackedAt 晚于 lastTurnEnd 不成立");
|
|
2475
2618
|
assert.equal(completionReminder(holdBase, null, false), false, "无完成登记不成立(升级不回溯补发提醒)");
|
|
2476
2619
|
assert.equal(completionReminder(holdBase, { lastTurnEnd: 0, ackedAt: null }, false), false, "lastTurnEnd 非法不作数");
|
|
2477
|
-
assert.equal(completionReminder({ id: "m-c1", parentId: "m", displayTitle: "子S" }, { lastTurnEnd: 1000, ackedAt: null }, true), false, "子代理不产生完成提醒");
|
|
2620
|
+
assert.equal(completionReminder({ id: "m-c1", parentId: "m", origin: "subagent", displayTitle: "子S" }, { lastTurnEnd: 1000, ackedAt: null }, true), false, "子代理不产生完成提醒");
|
|
2478
2621
|
// 打开/切换当前会话不解除(AC-05):完成提醒成立与 current 无关。
|
|
2479
2622
|
// R-01-002/AC-05 打开或切换当前会话不解除完成提醒:判定与 current 无关(C-030)。
|
|
2480
2623
|
const holdSnap = { ids: ["sB"], byId: { sB: holdBase }, current: "sA" };
|
|
@@ -2512,7 +2655,7 @@ assert.deepEqual(
|
|
|
2512
2655
|
// 新回合隐式更替:lastTurnEnd 前移后旧确认游标不再覆盖新回合(仍未确认则对新回合成立)。
|
|
2513
2656
|
assert.equal(completionReminder(holdBase, { lastTurnEnd: 2000, ackedAt: 1500 }, false), true, "新回合完成后提醒针对新回合重新成立");
|
|
2514
2657
|
// 委托周期抑制:后代活动期间完成提醒不生效(呈现不依赖宿主 completed)。
|
|
2515
|
-
const delegDoneMix = { ids: ["root", "root-c1"], byId: { root: holdBase, "root-c1": { id: "root-c1", displayTitle: "子S", parentId: "root", running: true } }, current: null };
|
|
2658
|
+
const delegDoneMix = { ids: ["root", "root-c1"], byId: { root: holdBase, "root-c1": { id: "root-c1", displayTitle: "子S", parentId: "root", origin: "subagent", running: true } }, current: null };
|
|
2516
2659
|
assert.deepEqual(
|
|
2517
2660
|
buildEntries(delegDoneMix, [], {}, acks(1000), new Set(["root"])).map((e) => [e.id, e.pendingText ?? null]),
|
|
2518
2661
|
[["root", null], ["root-c1", null]],
|
|
@@ -2534,7 +2677,7 @@ assert.equal(errorReminder({ id: "sB", displayTitle: "旧B" }, { lastTurnEndKind
|
|
|
2534
2677
|
assert.equal(errorReminder({ id: "sB", displayTitle: "旧B" }, { lastTurnEndKind: "completed" }, false), false, "正常回合不成立");
|
|
2535
2678
|
assert.equal(errorReminder({ id: "sB", displayTitle: "旧B" }, { lastTurnEndKind: "error", ackedAt: 99999 }, false), true, "错误提醒不消费 ack 游标(ackedAt 不影响成立)");
|
|
2536
2679
|
assert.equal(errorReminder({ id: "sB", displayTitle: "旧B" }, null, false), false, "无登记不成立(升级不回溯补发错误提醒)");
|
|
2537
|
-
assert.equal(errorReminder({ id: "m-c1", parentId: "m", displayTitle: "子S" }, { lastTurnEndKind: "error" }, true), false, "子代理不产生错误提醒");
|
|
2680
|
+
assert.equal(errorReminder({ id: "m-c1", parentId: "m", origin: "subagent", displayTitle: "子S" }, { lastTurnEndKind: "error" }, true), false, "子代理不产生错误提醒");
|
|
2538
2681
|
const errEntries = buildEntries(holdSnap, [], {}, errAcks("error", "The engine is currently overloaded, please try again later"));
|
|
2539
2682
|
assert.deepEqual(
|
|
2540
2683
|
errEntries.map((e) => [e.id, e.kind, e.pendingText ?? null, e.waitClass, e.noteText]),
|
|
@@ -2586,7 +2729,7 @@ assert.deepEqual(
|
|
|
2586
2729
|
const clearedErrAcked = buildEntries(holdSnap, [], {}, errAcks("completed", null, 2000));
|
|
2587
2730
|
assert.deepEqual(clearedErrAcked.map((e) => [e.id, e.kind]), [], "新回合正常结束且已确认后完全退出活动区(错误提醒无确认按钮语义)");
|
|
2588
2731
|
// 委托周期抑制:后代活动期间错误提醒不生效。
|
|
2589
|
-
const delegErrMix = { ids: ["root", "root-c1"], byId: { root: holdBase, "root-c1": { id: "root-c1", displayTitle: "子S", parentId: "root", running: true } }, current: null };
|
|
2732
|
+
const delegErrMix = { ids: ["root", "root-c1"], byId: { root: holdBase, "root-c1": { id: "root-c1", displayTitle: "子S", parentId: "root", origin: "subagent", running: true } }, current: null };
|
|
2590
2733
|
const delegErrAcks = new Map([["root", { lastTurnEnd: 1000, lastTurnEndKind: "error", lastTurnEndError: "boom", ackedAt: null }]]);
|
|
2591
2734
|
assert.deepEqual(
|
|
2592
2735
|
buildEntries(delegErrMix, [], {}, delegErrAcks, new Set(["root"])).map((e) => e.id),
|
|
@@ -2628,7 +2771,7 @@ const recentSubSnap = {
|
|
|
2628
2771
|
ids: ["m", "m-c1"],
|
|
2629
2772
|
byId: {
|
|
2630
2773
|
m: { id: "m", displayTitle: "主M", running: false, completed: false, updatedAt: NOW - 1_000 },
|
|
2631
|
-
"m-c1": { id: "m-c1", displayTitle: "子S", running: false, completed: false, parentId: "m", updatedAt: NOW - 500 },
|
|
2774
|
+
"m-c1": { id: "m-c1", displayTitle: "子S", running: false, completed: false, parentId: "m", origin: "subagent", updatedAt: NOW - 500 },
|
|
2632
2775
|
},
|
|
2633
2776
|
current: null,
|
|
2634
2777
|
};
|
|
@@ -2677,6 +2820,67 @@ assert.deepEqual(
|
|
|
2677
2820
|
"归档集同样接受 Set 形态",
|
|
2678
2821
|
);
|
|
2679
2822
|
|
|
2823
|
+
// R-01-010/AC-01:归档集合同时驱动活动区过滤;未确认完成提醒也不得在归档后滞留。
|
|
2824
|
+
const activeArchivedSnap = {
|
|
2825
|
+
ids: ["sKeep", "sGone"],
|
|
2826
|
+
byId: {
|
|
2827
|
+
sKeep: { id: "sKeep", displayTitle: "保留K", running: true },
|
|
2828
|
+
sGone: { id: "sGone", displayTitle: "归档G", running: false },
|
|
2829
|
+
},
|
|
2830
|
+
current: null,
|
|
2831
|
+
};
|
|
2832
|
+
assert.deepEqual(
|
|
2833
|
+
buildEntries(activeArchivedSnap, [], {}, new Map([["sGone", { lastTurnEnd: NOW, ackedAt: null }]]), null, ["sGone"]).map((e) => e.id),
|
|
2834
|
+
["sKeep"],
|
|
2835
|
+
"归档会话从活动区消失,不因未确认完成提醒继续滞留",
|
|
2836
|
+
);
|
|
2837
|
+
const archivedStatusSnap = {
|
|
2838
|
+
ids: ["sRunning", "sDone", "sError", "sDelegating"],
|
|
2839
|
+
byId: {
|
|
2840
|
+
sRunning: { id: "sRunning", displayTitle: "运行中归档", running: true },
|
|
2841
|
+
sDone: { id: "sDone", displayTitle: "完成归档", running: false },
|
|
2842
|
+
sError: { id: "sError", displayTitle: "错误归档", running: false },
|
|
2843
|
+
sDelegating: { id: "sDelegating", displayTitle: "委托归档", running: false },
|
|
2844
|
+
},
|
|
2845
|
+
current: null,
|
|
2846
|
+
};
|
|
2847
|
+
assert.deepEqual(
|
|
2848
|
+
buildEntries(
|
|
2849
|
+
archivedStatusSnap,
|
|
2850
|
+
[],
|
|
2851
|
+
{},
|
|
2852
|
+
new Map([
|
|
2853
|
+
["sDone", { lastTurnEnd: NOW, ackedAt: null }],
|
|
2854
|
+
["sError", { lastTurnEnd: NOW, lastTurnEndKind: "error", lastTurnEndError: "boom", ackedAt: null }],
|
|
2855
|
+
]),
|
|
2856
|
+
new Set(["sDelegating"]),
|
|
2857
|
+
new Set(["sRunning", "sDone", "sError", "sDelegating"]),
|
|
2858
|
+
).map((e) => e.id),
|
|
2859
|
+
[],
|
|
2860
|
+
"Set 归档集合同时过滤运行、完成提醒、错误提醒与委托周期中的会话",
|
|
2861
|
+
);
|
|
2862
|
+
const archivedLineageSnap = {
|
|
2863
|
+
ids: ["root", "root-child", "root-grandchild", "keep-root", "keep-child"],
|
|
2864
|
+
byId: {
|
|
2865
|
+
root: { id: "root", displayTitle: "归档母会话", running: false },
|
|
2866
|
+
"root-child": { id: "root-child", displayTitle: "归档子代理", parentId: "root", origin: "subagent", running: true },
|
|
2867
|
+
"root-grandchild": { id: "root-grandchild", displayTitle: "归档孙代理", parentId: "root-child", origin: "subagent", running: true },
|
|
2868
|
+
"keep-root": { id: "keep-root", displayTitle: "保留母会话", running: false },
|
|
2869
|
+
"keep-child": { id: "keep-child", displayTitle: "被归档子代理", parentId: "keep-root", origin: "subagent", running: true },
|
|
2870
|
+
},
|
|
2871
|
+
current: null,
|
|
2872
|
+
};
|
|
2873
|
+
assert.deepEqual(
|
|
2874
|
+
buildEntries(archivedLineageSnap, [], {}, null, null, ["root"]).map((e) => e.id),
|
|
2875
|
+
["keep-root", "keep-child"],
|
|
2876
|
+
"归档母会话时其子会话与更深层后代均不产出活动条目",
|
|
2877
|
+
);
|
|
2878
|
+
assert.deepEqual(
|
|
2879
|
+
buildEntries(archivedLineageSnap, [], {}, null, null, ["keep-child"]).map((e) => e.id),
|
|
2880
|
+
["root", "root-child", "root-grandchild"],
|
|
2881
|
+
"归档子会话后不因残留后代活动把未归档母会话留在活动区",
|
|
2882
|
+
);
|
|
2883
|
+
|
|
2680
2884
|
// ---- R-01-014/AC-01 列表在途显示加载指示而非空态 ----
|
|
2681
2885
|
assert.equal(listLoadState(null), "loading", "快照缺失视为列表在途");
|
|
2682
2886
|
assert.equal(listLoadState({ phase: "pending" }), "loading", "phase 为 pending 视为列表在途");
|
|
@@ -2810,6 +3014,16 @@ assert.ok(
|
|
|
2810
3014
|
clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);"),
|
|
2811
3015
|
"列表 phase 与历史时间文案必须参与结构化渲染签名,空列表不得冻结在加载/失败状态(T-087)",
|
|
2812
3016
|
);
|
|
3017
|
+
assert.ok(
|
|
3018
|
+
clientSource.includes("scroll?.querySelector?.(`.${LIST_CLASS} .${CARD_CLASS}[data-current]`)") &&
|
|
3019
|
+
!clientSource.includes("scroll?.querySelector?.(`.${CARD_CLASS}[data-current]`)"),
|
|
3020
|
+
"当前卡片自动滚动仅定位活动列表,完成卡迁入历史后不追随历史区(R-01-006/AC-02)",
|
|
3021
|
+
);
|
|
3022
|
+
assert.ok(
|
|
3023
|
+
clientSource.includes("source.contains(focusedElement)") &&
|
|
3024
|
+
clientSource.includes("cardsById.get(focusAfterMigrationId)?.el.focus()"),
|
|
3025
|
+
"活动卡迁移时将已有键盘焦点交给仍在活动区的相邻卡片(R-01-005/AC-01)",
|
|
3026
|
+
);
|
|
2813
3027
|
// R-01-013/AC-02 回归:卡片标题必须随快照更新——单卡渲染异常不得冻结其余卡片
|
|
2814
3028
|
// (此前渲染签名先于卡片循环提交且无异常隔离,故障卡及其后全部卡片永久滞留旧标题,
|
|
2815
3029
|
// 历史卡因此停在首条消息形态的 fallback 标题,与左侧栏脱节)。
|
|
@@ -2961,7 +3175,7 @@ assert.ok(
|
|
|
2961
3175
|
assert.ok(
|
|
2962
3176
|
clientSource.includes("const INDENT_PX = 16;") &&
|
|
2963
3177
|
bundle.includes("Math.round(parent.left + indentPx / 2 + 1)") &&
|
|
2964
|
-
bundle.includes("renderCardIntoList(activeList, entry, cardsById, index, 1,
|
|
3178
|
+
bundle.includes("renderCardIntoList(activeList, entry, cardsById, index, 1, colorByWorkspace)"),
|
|
2965
3179
|
"几何耦合钉住:INDENT_PX=16、轨道 left 由母会话卡片左缘测量推导(+半槽+1px border,取整后与横线起笔相接)与活动区卡片 offset=1(轨道层为首子节点),改任一必须同步",
|
|
2966
3180
|
);
|
|
2967
3181
|
// R-01-003/AC-05
|
|
@@ -3443,20 +3657,27 @@ assert.ok(bundle.includes('workspace.append(workspaceIcon, makeEl("span", "dap-w
|
|
|
3443
3657
|
assert.ok(bundle.includes("if (workspaceText !== null) restoreTextField(workspaceText, entry.workspaceTitle)"), "热装旧骨架无文本段时容空跳过,不中断渲染(R-01-003/AC-06 健壮性)");
|
|
3444
3658
|
|
|
3445
3659
|
// R-01-003/AC-08、AC-09、AC-10、AC-11、AC-12
|
|
3446
|
-
//
|
|
3447
|
-
// CSS 以 OKLCH
|
|
3448
|
-
assert.ok(bundle.includes("
|
|
3449
|
-
assert.ok(bundle.includes("
|
|
3450
|
-
assert.ok(bundle.includes('
|
|
3451
|
-
assert.ok(bundle.includes(
|
|
3452
|
-
assert.ok(bundle.includes("--dap-workspace-
|
|
3660
|
+
// 工作区徽标按身份派生基色、经同屏前景/背景复合槽位消解后着色:渲染层写入
|
|
3661
|
+
// 前景 hue/L/C 与背景变体主题参数,CSS 以 OKLCH 前景色呈现文字、独立背景源色混合底色/描边;胶囊几何与字号不变。
|
|
3662
|
+
assert.ok(bundle.includes("resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey))"), "渲染层按同帧可见身份集合消解十二槽颜色(R-01-003/AC-12)");
|
|
3663
|
+
assert.ok(bundle.includes("colorByWorkspace.get(entry.workspaceKey)"), "每张卡使用集合消解后的工作区颜色槽位(R-01-003/AC-08、AC-12)");
|
|
3664
|
+
assert.ok(bundle.includes('entry.workspaceTitle !== "" && foreground !== undefined && background !== undefined'), "缺少工作区身份或背景变体时隐藏徽标而不使用默认颜色(R-01-003/AC-08)");
|
|
3665
|
+
assert.ok(bundle.includes("style.setProperty(property, next)"), "渲染层把槽位颜色变量写入徽标(R-01-003/AC-08、AC-12)");
|
|
3666
|
+
assert.ok(bundle.includes('"--dap-workspace-dark-l": foreground?.darkL'), "渲染层把前景深色主题 L 写入徽标(R-01-003/AC-10、AC-11)");
|
|
3667
|
+
assert.ok(bundle.includes('"--dap-workspace-light-l": foreground?.lightL'), "渲染层把前景浅色主题 L 写入徽标(R-01-003/AC-10、AC-11)");
|
|
3668
|
+
assert.ok(bundle.includes('"--dap-workspace-bg-dark-l": background?.dark?.l'), "渲染层把背景变体深色主题 L 写入徽标(R-01-003/AC-10、AC-11)");
|
|
3669
|
+
assert.ok(bundle.includes('"--dap-workspace-bg-light-l": background?.light?.l'), "渲染层把背景变体浅色主题 L 写入徽标(R-01-003/AC-10、AC-11)");
|
|
3670
|
+
assert.ok(bundle.includes("for (const property of Object.keys(colorVariables)) workspaceLabel.style.removeProperty(property)"), "徽标隐藏时移除全部前景/背景颜色变量,不留陈旧着色(R-01-003/AC-08)");
|
|
3671
|
+
assert.ok(bundle.includes("var(--dap-workspace-dark-l, 0.78)"), "深色主题使用槽位 OKLCH 明度变量(R-01-003/AC-11)");
|
|
3672
|
+
assert.ok(bundle.includes("var(--dap-workspace-dark-c, 0.16)"), "深色主题使用槽位 OKLCH 彩度变量(R-01-003/AC-11)");
|
|
3453
3673
|
assert.ok(bundle.includes("color: var(--dap-workspace-color)"), "徽标文字直接使用调色板色、不混 currentColor(R-01-003/AC-11)");
|
|
3454
|
-
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-color)
|
|
3455
|
-
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-color)
|
|
3674
|
+
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)");
|
|
3675
|
+
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)");
|
|
3456
3676
|
assert.ok(bundle.includes("body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {"), "浅色主题单独校准徽标配色(R-01-003/AC-10、AC-11)");
|
|
3457
|
-
assert.ok(bundle.includes("--dap-workspace-
|
|
3458
|
-
assert.ok(bundle.includes("
|
|
3459
|
-
assert.ok(bundle.includes("color-mix(in oklch, var(--dap-workspace-color)
|
|
3677
|
+
assert.ok(bundle.includes("var(--dap-workspace-light-l, 0.48)"), "浅色主题使用槽位 OKLCH 明度变量(R-01-003/AC-11)");
|
|
3678
|
+
assert.ok(bundle.includes("var(--dap-workspace-light-c, 0.15)"), "浅色主题使用槽位 OKLCH 彩度变量(R-01-003/AC-11)");
|
|
3679
|
+
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)");
|
|
3680
|
+
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)");
|
|
3460
3681
|
assert.ok(!bundle.includes("color-mix(in srgb, var(--dap-workspace-color) 92%, currentColor)"), "工作区文字不得再以 currentColor 冲淡调色板色(R-01-003/AC-11)");
|
|
3461
3682
|
|
|
3462
3683
|
// R-01-010/AC-01、R-01-010/AC-05
|