dsh-activity-pane 0.9.0 → 0.10.1
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 +513 -147
- package/README.md +9 -0
- package/README.zh-CN.md +9 -0
- package/package.json +1 -1
- package/scripts/acceptance.mjs +16 -4
- package/scripts/check.mjs +279 -55
- package/src/client.mjs +393 -52
- package/src/core.mjs +120 -95
- package/src/host.mjs +45 -39
package/.dsh-plugin/client.js
CHANGED
|
@@ -130,6 +130,26 @@ function clampPaneWidth(raw) {
|
|
|
130
130
|
return Math.min(PANE_WIDTH_MAX, Math.max(PANE_WIDTH_MIN, Math.round(value)));
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* 把任意输入(localStorage 字符串等)归一为合法卡片显示档位:
|
|
135
|
+
* 仅 'full'/'medium'/'compact' 为合法档位,其余(含缺失/非法值)回退默认中间档(R-01-021/AC-06)。
|
|
136
|
+
*/
|
|
137
|
+
function normalizeDensity(raw) {
|
|
138
|
+
return raw === "compact" || raw === "medium" || raw === "full" ? raw : "medium";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 显示档位的循环次序:完整 → 中间 → 紧凑 → 完整(R-01-021/AC-01)。 */
|
|
142
|
+
const DENSITY_ORDER = ["full", "medium", "compact"];
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 返回循环切换后的下一显示档位:完整 → 中间 → 紧凑 → 完整;
|
|
146
|
+
* 输入先经 normalizeDensity 归一,非法值视作中间档(R-01-021/AC-01)。
|
|
147
|
+
*/
|
|
148
|
+
function nextDensity(value) {
|
|
149
|
+
const index = DENSITY_ORDER.indexOf(normalizeDensity(value));
|
|
150
|
+
return DENSITY_ORDER[(index + 1) % DENSITY_ORDER.length];
|
|
151
|
+
}
|
|
152
|
+
|
|
133
153
|
function isRecord(value) {
|
|
134
154
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
135
155
|
}
|
|
@@ -1293,25 +1313,25 @@ function awaitBadgeStats(entries) {
|
|
|
1293
1313
|
|
|
1294
1314
|
/** 数量标识呈现态(R-01-014/AC-06):列表在途(loading)时不冒充计数——归一为
|
|
1295
1315
|
* loading 呈现(加载指示 + 加载中 aria 文案,不等待、不脉冲);否则归一为 count
|
|
1296
|
-
* 呈现(n/m 文本 + 计数 aria
|
|
1316
|
+
* 呈现(n/m 文本 + 计数 aria 文案 + 悬停 tips 文案)。分子为正在运行的主会话数
|
|
1317
|
+
* (running = total − waiting)——随会话逐一完成递减至 0,与等待行动数互补
|
|
1318
|
+
* (R-01-001/AC-04、AC-05);awaiting 表达「存在等待行动」——底色经
|
|
1297
1319
|
* awaitBadgeTone(错误 > 阻塞 > 完成,红/金/绿)与脉冲门控同一信号:任一等待行动
|
|
1298
1320
|
* (阻塞等待、完成提醒或错误提醒)即脉冲(R-01-002/AC-06,C-037、C-043)。
|
|
1299
1321
|
* blocked 入参只用于 aria 文案的计数说明,不再驱动门控。错误轴不算在途,维持计数呈现。 */
|
|
1300
1322
|
function countBadgeState(listState, waiting, total, blocked = 0) {
|
|
1301
|
-
if (listState === "loading") return { mode: "loading", text: "", ariaText: "活动会话计数加载中", awaiting: false };
|
|
1323
|
+
if (listState === "loading") return { mode: "loading", text: "", ariaText: "活动会话计数加载中", tip: "", awaiting: false };
|
|
1302
1324
|
const awaiting = waiting > 0;
|
|
1303
1325
|
const hasBlocked = blocked > 0;
|
|
1304
1326
|
const doneCount = waiting - (hasBlocked ? blocked : 0);
|
|
1327
|
+
const running = total - waiting;
|
|
1305
1328
|
return {
|
|
1306
1329
|
mode: "count",
|
|
1307
|
-
text: `${
|
|
1308
|
-
ariaText:
|
|
1309
|
-
?
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
: awaiting
|
|
1313
|
-
? `${total} 个活动会话,${waiting} 个已完成`
|
|
1314
|
-
: `${total} 个活动会话`,
|
|
1330
|
+
text: `${running}/${total}`,
|
|
1331
|
+
ariaText: `${total} 个活动会话,${running} 个正在运行`
|
|
1332
|
+
+ (hasBlocked ? `,${blocked} 个等待你答复` : "")
|
|
1333
|
+
+ (doneCount > 0 ? `,${doneCount} 个已完成` : ""),
|
|
1334
|
+
tip: `运行中的会话 ${running} / 总活动会话 ${total}`,
|
|
1315
1335
|
awaiting,
|
|
1316
1336
|
};
|
|
1317
1337
|
}
|
|
@@ -1386,16 +1406,11 @@ function workspaceInfoForSession(sessionId, workspaceItems, byId = {}) {
|
|
|
1386
1406
|
}
|
|
1387
1407
|
|
|
1388
1408
|
/**
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
* 色相弧 [30°,320°] 上均匀取色(30 + hash % 291),输出 [30,320] 整数。
|
|
1393
|
-
* 同一身份恒得同一基色色相,与工作区列表顺序、会话状态及持久化存储无关,页面
|
|
1394
|
-
* 刷新后不变;空身份返回 null。
|
|
1409
|
+
* 身份字符串 → 32 位无符号哈希:djb2 经雪崩终混(见 C-029 决策记录)。
|
|
1410
|
+
* 异或右移 + 乘法把高位熵折入低位,消除 djb2 低位分布聚集;每步 >>> 0 保持
|
|
1411
|
+
* 无符号,异或结果可能带符号位。
|
|
1395
1412
|
*/
|
|
1396
|
-
function
|
|
1397
|
-
const text = cleanText(key);
|
|
1398
|
-
if (!text) return null;
|
|
1413
|
+
function identityHash(text) {
|
|
1399
1414
|
let hash = 5381;
|
|
1400
1415
|
for (let i = 0; i < text.length; i += 1)
|
|
1401
1416
|
hash = ((hash << 5) + hash + text.charCodeAt(i)) >>> 0;
|
|
@@ -1405,8 +1420,24 @@ function workspaceHue(key) {
|
|
|
1405
1420
|
// 乘法的乘积(最大约 5×10^18)超出 double 精确整数上限 2^53,低 32 位
|
|
1406
1421
|
// 会丢失精度。
|
|
1407
1422
|
hash = Math.imul(hash, 0x45d9f3b) >>> 0;
|
|
1408
|
-
|
|
1409
|
-
|
|
1423
|
+
return (hash ^ (hash >>> 16)) >>> 0;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// 避红基色色相弧 [30°,320°] 的起点与取值数(30 + hash % 291);背景变体
|
|
1427
|
+
// 以同一弧宽的商派生,共享常量避免两处口径漂移(R-01-003/AC-09、C-077)。
|
|
1428
|
+
const WORKSPACE_HUE_ARC_START = 30;
|
|
1429
|
+
const WORKSPACE_HUE_ARC_SIZE = 291;
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* 工作区徽标基色色相(R-01-003/AC-08、AC-09):以工作区身份为唯一输入的纯函数,
|
|
1433
|
+
* 在避开红色警戒区的色相弧 [30°,320°] 上均匀取色,输出 [30,320] 整数;哈希
|
|
1434
|
+
* 机制见 {@link identityHash}。同一身份恒得同一基色色相,与工作区列表顺序、
|
|
1435
|
+
* 会话状态及持久化存储无关,页面刷新后不变;空身份返回 null。
|
|
1436
|
+
*/
|
|
1437
|
+
function workspaceHue(key) {
|
|
1438
|
+
const text = cleanText(key);
|
|
1439
|
+
if (!text) return null;
|
|
1440
|
+
return WORKSPACE_HUE_ARC_START + (identityHash(text) % WORKSPACE_HUE_ARC_SIZE);
|
|
1410
1441
|
}
|
|
1411
1442
|
|
|
1412
1443
|
const WORKSPACE_COLOR_SLOTS = Object.freeze([
|
|
@@ -1442,90 +1473,46 @@ const WORKSPACE_BACKGROUND_SLOTS = Object.freeze([
|
|
|
1442
1473
|
},
|
|
1443
1474
|
].map(Object.freeze));
|
|
1444
1475
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
return Array.from({ length: count }, (_, offset) => (start + offset * step) % count);
|
|
1476
|
+
/** 环形色相距离(度,0–180)。 */
|
|
1477
|
+
function hueDistance(a, b) {
|
|
1478
|
+
const d = Math.abs(a - b) % 360;
|
|
1479
|
+
return Math.min(d, 360 - d);
|
|
1450
1480
|
}
|
|
1451
1481
|
|
|
1452
1482
|
/**
|
|
1453
|
-
* 同屏工作区复合颜色槽位消解(R-01-003/AC-08、AC-12
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1483
|
+
* 同屏工作区复合颜色槽位消解(R-01-003/AC-08、AC-12):逐身份纯映射——每个
|
|
1484
|
+
* 身份独立地以其 32 位雪崩哈希派生前景与背景槽位,身份之间互不影响,创建、
|
|
1485
|
+
* 移除或变更其它工作区不改变既有工作区的颜色(C-077);前景取基色色相在 12 个
|
|
1486
|
+
* 前景槽位中环形距离最近者(平局取低槽位),背景变体取 floor(hash / 弧宽) % 3。
|
|
1487
|
+
* 均匀哈希使不同身份的前景碰撞概率保持在约 1/12、复合碰撞约 1/36 的最小水平。
|
|
1456
1488
|
*/
|
|
1457
1489
|
function resolveWorkspaceColors(keys) {
|
|
1458
1490
|
const identities = [...new Set((Array.isArray(keys) ? keys : []).map(cleanText).filter(Boolean))].sort();
|
|
1459
|
-
const uses = WORKSPACE_COLOR_SLOTS.map(() => 0);
|
|
1460
|
-
const backgroundUses = WORKSPACE_COLOR_SLOTS.map(() => WORKSPACE_BACKGROUND_SLOTS.map(() => 0));
|
|
1461
1491
|
const resolved = new Map();
|
|
1462
1492
|
for (const identity of identities) {
|
|
1463
|
-
const
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
).map((index) => WORKSPACE_PRIMARY_SLOT_COUNT + index);
|
|
1474
|
-
const order = [...primaryOrder, ...secondaryOrder];
|
|
1475
|
-
const backgroundOrder = workspaceSlotProbe(
|
|
1476
|
-
(baseHue - 30) % WORKSPACE_BACKGROUND_SLOTS.length,
|
|
1477
|
-
WORKSPACE_BACKGROUND_SLOTS.length,
|
|
1478
|
-
2,
|
|
1479
|
-
);
|
|
1480
|
-
const availableForegroundOrder = order.filter((index) => backgroundOrder.some((backgroundSlot) => backgroundUses[index][backgroundSlot] === 0));
|
|
1481
|
-
const foregroundOrder = availableForegroundOrder.length > 0 ? availableForegroundOrder : order;
|
|
1482
|
-
let foregroundSlot = foregroundOrder.find((index) => uses[index] === 0);
|
|
1483
|
-
if (foregroundSlot === undefined) {
|
|
1484
|
-
foregroundSlot = foregroundOrder.reduce(
|
|
1485
|
-
(best, index) => (uses[index] < uses[best] ? index : best),
|
|
1486
|
-
foregroundOrder[0],
|
|
1487
|
-
);
|
|
1493
|
+
const hash = identityHash(identity);
|
|
1494
|
+
const baseHue = WORKSPACE_HUE_ARC_START + (hash % WORKSPACE_HUE_ARC_SIZE);
|
|
1495
|
+
let foregroundSlot = 0;
|
|
1496
|
+
let nearest = hueDistance(baseHue, WORKSPACE_COLOR_SLOTS[0].hue);
|
|
1497
|
+
for (let slot = 1; slot < WORKSPACE_COLOR_SLOTS.length; slot += 1) {
|
|
1498
|
+
const distance = hueDistance(baseHue, WORKSPACE_COLOR_SLOTS[slot].hue);
|
|
1499
|
+
if (distance < nearest) {
|
|
1500
|
+
nearest = distance;
|
|
1501
|
+
foregroundSlot = slot;
|
|
1502
|
+
}
|
|
1488
1503
|
}
|
|
1489
|
-
uses[foregroundSlot] += 1;
|
|
1490
|
-
const backgroundUsesForForeground = backgroundUses[foregroundSlot];
|
|
1491
|
-
const backgroundSlot = backgroundOrder.reduce(
|
|
1492
|
-
(best, index) => (backgroundUsesForForeground[index] < backgroundUsesForForeground[best] ? index : best),
|
|
1493
|
-
backgroundOrder[0],
|
|
1494
|
-
);
|
|
1495
|
-
backgroundUsesForForeground[backgroundSlot] += 1;
|
|
1496
1504
|
resolved.set(identity, {
|
|
1497
1505
|
foreground: WORKSPACE_COLOR_SLOTS[foregroundSlot],
|
|
1498
|
-
background: WORKSPACE_BACKGROUND_SLOTS[
|
|
1506
|
+
background: WORKSPACE_BACKGROUND_SLOTS[Math.floor(hash / WORKSPACE_HUE_ARC_SIZE) % WORKSPACE_BACKGROUND_SLOTS.length],
|
|
1499
1507
|
});
|
|
1500
1508
|
}
|
|
1501
1509
|
return resolved;
|
|
1502
1510
|
}
|
|
1503
1511
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const posIndex = new Map();
|
|
1509
|
-
for (const workspace of workspaceItems ?? []) {
|
|
1510
|
-
if (!isRecord(workspace)) continue;
|
|
1511
|
-
const sessionIds = Array.isArray(workspace.sessionIds)
|
|
1512
|
-
? workspace.sessionIds
|
|
1513
|
-
: [];
|
|
1514
|
-
sessionIds.forEach((sid, p) => {
|
|
1515
|
-
const key = String(sid);
|
|
1516
|
-
if (!wsIndex.has(key)) {
|
|
1517
|
-
wsIndex.set(key, wsIndex.size);
|
|
1518
|
-
posIndex.set(key, p);
|
|
1519
|
-
}
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
return (id) => {
|
|
1523
|
-
const key = String(id);
|
|
1524
|
-
return {
|
|
1525
|
-
ws: wsIndex.get(key) ?? Number.MAX_SAFE_INTEGER,
|
|
1526
|
-
pos: posIndex.get(key) ?? Number.MAX_SAFE_INTEGER,
|
|
1527
|
-
};
|
|
1528
|
-
};
|
|
1512
|
+
/** 主会话活动区排序用的最后用户指令时刻:宿主列表时间,缺失/非法视为最旧。 */
|
|
1513
|
+
function instructionTime(row) {
|
|
1514
|
+
const time = Number(row?.updatedAt);
|
|
1515
|
+
return Number.isFinite(time) ? time : -1;
|
|
1529
1516
|
}
|
|
1530
1517
|
|
|
1531
1518
|
/** 子代理的展示标题:优先目录 label,其次 displayTitle,兜底 "子任务"。 */
|
|
@@ -1588,7 +1575,6 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1588
1575
|
}
|
|
1589
1576
|
return false;
|
|
1590
1577
|
};
|
|
1591
|
-
const rank = workspaceRank(workspaceItems ?? []);
|
|
1592
1578
|
const descendantIds = descendantActiveIds(byId, isArchived);
|
|
1593
1579
|
// 第一遍:层级关系 + 显示判定(show = 自身活动 || 委托周期 || 完成提醒,单点实现避免漂移)。
|
|
1594
1580
|
const rootIds = [];
|
|
@@ -1621,12 +1607,25 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1621
1607
|
meta.set(id, { row, running, pending, isSub, show, done, err, descendantActive, delegating, depth: 0 });
|
|
1622
1608
|
}
|
|
1623
1609
|
|
|
1624
|
-
//
|
|
1610
|
+
// 主会话分两组排序(R-01-001/AC-07):运行中主会话置顶,组内按最后一次用户指令
|
|
1611
|
+
// 时间(宿主列表时间)从新到旧;等待/完成组(阻塞等待、完成提醒、错误提醒)排后,
|
|
1612
|
+
// 组内按进入该状态的时刻(最近一次回合结束登记时刻,缺失回落宿主列表时间)从新到旧。
|
|
1613
|
+
// 两组相同时间均回落宿主列表出现顺序。工作区顺序不参与排序,仅承载卡片徽标与名称。
|
|
1614
|
+
const isRunningEntry = (id) => {
|
|
1615
|
+
const m = meta.get(id);
|
|
1616
|
+
return m !== undefined && !m.pending && (m.running || m.delegating);
|
|
1617
|
+
};
|
|
1618
|
+
const sortTime = (id) => {
|
|
1619
|
+
if (isRunningEntry(id)) return instructionTime(byId[id]);
|
|
1620
|
+
const record = completionFor(id, completions);
|
|
1621
|
+
const end = isRecord(record) ? Number(record.lastTurnEnd) : NaN;
|
|
1622
|
+
return Number.isFinite(end) ? end : instructionTime(byId[id]);
|
|
1623
|
+
};
|
|
1625
1624
|
rootIds.sort((a, b) => {
|
|
1626
|
-
const
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
if (
|
|
1625
|
+
const byGroup = Number(isRunningEntry(b)) - Number(isRunningEntry(a));
|
|
1626
|
+
if (byGroup !== 0) return byGroup;
|
|
1627
|
+
const byTime = sortTime(b) - sortTime(a);
|
|
1628
|
+
if (byTime !== 0) return byTime;
|
|
1630
1629
|
return ids.indexOf(a) - ids.indexOf(b);
|
|
1631
1630
|
});
|
|
1632
1631
|
|
|
@@ -2237,6 +2236,32 @@ function reconcileTurnStats(current, records, { closeOpenTurn = false, forceFres
|
|
|
2237
2236
|
return { ...state, watermarkSeq, watermarkTime };
|
|
2238
2237
|
}
|
|
2239
2238
|
|
|
2239
|
+
/**
|
|
2240
|
+
* 会话级登记串行化(R-01-020/AC-02):宿主存储域的 `KvTable.put` 先异步落盘、落盘完成
|
|
2241
|
+
* 后才更新内存快照,因此同一会话的两个边界事件 handler 并发执行时,后到事件会在先到
|
|
2242
|
+
* 事件落盘前读到旧状态,其效果被当作「无效果」永久丢弃(实测:`ask_user_question` 的
|
|
2243
|
+
* `tool/call` 与 11ms 后的 `tool/result` 交错,等待区间悬挂到 `turn/end` 强制结算,
|
|
2244
|
+
* 总耗时系统性缺记整段运行过程)。返回 `(id, run) => tail`:同一会话前一个 run 完成
|
|
2245
|
+
* (含其 put 的内存生效)后才执行下一个 run,不同会话互不阻塞。run 内部须自行捕获
|
|
2246
|
+
* 错误、不得让拒绝外泄(调用方可安全丢弃返回的 tail);tail 供调用方等待与测试断言。
|
|
2247
|
+
* 链空闲即清理,不随会话数增长。
|
|
2248
|
+
*/
|
|
2249
|
+
function createSessionEventSerializer() {
|
|
2250
|
+
const tails = new Map();
|
|
2251
|
+
return (id, run) => {
|
|
2252
|
+
const prev = tails.get(id) ?? Promise.resolve();
|
|
2253
|
+
const tail = prev.then(run, run);
|
|
2254
|
+
tails.set(id, tail);
|
|
2255
|
+
tail.then(
|
|
2256
|
+
() => {
|
|
2257
|
+
if (tails.get(id) === tail) tails.delete(id);
|
|
2258
|
+
},
|
|
2259
|
+
() => {},
|
|
2260
|
+
);
|
|
2261
|
+
return tail;
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2240
2265
|
/**
|
|
2241
2266
|
* 构建历史区条目:当前非活动的**主会话**,按最后活动时间从新到旧返回完整候选集合。
|
|
2242
2267
|
* 子代理是临时工作单元,不入历史区;归档、空会话、完成/错误提醒与委托周期中的会话
|
|
@@ -2614,6 +2639,10 @@ const STYLE_ID = "dsh-activity-pane-style";
|
|
|
2614
2639
|
const INSTANCE_KEY = "__dshActivityPaneCleanup";
|
|
2615
2640
|
/** 拖拽调宽的 localStorage 持久化键(R-01-015/AC-04)。 */
|
|
2616
2641
|
const WIDTH_STORAGE_KEY = "dsh-activity-pane:width";
|
|
2642
|
+
/** 卡片紧凑显示的 localStorage 持久化键(R-01-021/AC-06)。 */
|
|
2643
|
+
const DENSITY_STORAGE_KEY = "dsh-activity-pane:density";
|
|
2644
|
+
/** 三档显示的可访问名称用中文标签,键为档位值(R-01-021/AC-01)。 */
|
|
2645
|
+
const DENSITY_LABELS = { full: "完整", medium: "中间", compact: "紧凑" };
|
|
2617
2646
|
const COLLAPSED_WIDTH = 34;
|
|
2618
2647
|
/** 宿主侧完成确认 API 前缀(C-030):acks 快照 / SSE 推送 / ack 写回,同源受信。 */
|
|
2619
2648
|
const PANE_API_BASE = "/dsh-activity-pane/api";
|
|
@@ -2624,6 +2653,10 @@ const INDENT_PX = 16;
|
|
|
2624
2653
|
const MOBILE_BREAKPOINT = "767px";
|
|
2625
2654
|
/** 运行卡时钟:只要存在运行中会话,就以该周期刷新时长显示。 */
|
|
2626
2655
|
const CLOCK_MS = 1000;
|
|
2656
|
+
/** 渲染与流式派生的最小合并间隔:事件密集(流式输出)时把渲染/派生频率硬顶在
|
|
2657
|
+
* 10Hz——事件率随宿主流式 chunk 数增长,显示粒度(秒级时长、块级时间线)无感,
|
|
2658
|
+
* 而渲染与 O(日志窗口) 派生不再随刷新率(移动端 120Hz)与事件率线性放大(T-127)。 */
|
|
2659
|
+
const SYNC_MIN_INTERVAL_MS = 100;
|
|
2627
2660
|
/** 历史卡相对时间刷新周期;无需每秒重绘整列。 */
|
|
2628
2661
|
const RECENT_TIME_REFRESH_MS = 60_000;
|
|
2629
2662
|
/** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
|
|
@@ -2653,25 +2686,27 @@ const CSS = `
|
|
|
2653
2686
|
[data-dsh-activity-pane] .dap-header {
|
|
2654
2687
|
display: flex;
|
|
2655
2688
|
align-items: center;
|
|
2656
|
-
gap: 8px;
|
|
2657
|
-
padding: 10px 12px;
|
|
2658
2689
|
font-size: 12px;
|
|
2659
2690
|
font-weight: 700;
|
|
2660
2691
|
letter-spacing: 0.02em;
|
|
2661
2692
|
}
|
|
2662
|
-
/*
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
[data-dsh-activity-pane] .dap-
|
|
2666
|
-
|
|
2693
|
+
/* 标题行拆为两部分:左侧标题区(flex:1 占满剩余宽度)整体即收起控件,悬停/聚焦
|
|
2694
|
+
高亮只覆盖标题区(R-01-011/AC-03、AC-07、R-01-008/AC-02);右侧工具区独立,
|
|
2695
|
+
不参与标题区的悬停高亮与折叠激活。 */
|
|
2696
|
+
[data-dsh-activity-pane] .dap-titlebar {
|
|
2697
|
+
flex: 1;
|
|
2698
|
+
min-width: 0;
|
|
2699
|
+
display: flex;
|
|
2700
|
+
align-items: center;
|
|
2701
|
+
gap: 8px;
|
|
2702
|
+
padding: 10px 8px 10px 12px;
|
|
2703
|
+
cursor: pointer;
|
|
2667
2704
|
}
|
|
2668
|
-
[data-dsh-activity-pane] .dap-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
margin-left: auto;
|
|
2672
|
-
color: color-mix(in srgb, currentColor 45%, transparent);
|
|
2673
|
-
font-size: 13px;
|
|
2705
|
+
[data-dsh-activity-pane] .dap-titlebar:hover,
|
|
2706
|
+
[data-dsh-activity-pane] .dap-titlebar:focus-visible {
|
|
2707
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
2674
2708
|
}
|
|
2709
|
+
[data-dsh-activity-pane] .dap-titlebar:focus-visible { outline: none; }
|
|
2675
2710
|
[data-dsh-activity-pane] .dap-count {
|
|
2676
2711
|
flex: none;
|
|
2677
2712
|
font-size: 10px;
|
|
@@ -2742,13 +2777,13 @@ const CSS = `
|
|
|
2742
2777
|
scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
|
|
2743
2778
|
}
|
|
2744
2779
|
}
|
|
2745
|
-
/*
|
|
2746
|
-
默认 hidden,scrollTop
|
|
2780
|
+
/* 「回到顶部」(R-01-018)悬浮图标按钮:右缘对齐、28px 圆形、不透明底色。
|
|
2781
|
+
默认 hidden,scrollTop 超阈值时由滚动监听揭隐;基类 display:flex 会压过 UA 的
|
|
2747
2782
|
[hidden] 规则,故显式补 [hidden] 隐藏。 */
|
|
2748
2783
|
[data-dsh-activity-pane] .dap-top {
|
|
2749
2784
|
position: absolute;
|
|
2750
|
-
bottom: 12px;
|
|
2751
2785
|
right: 12px;
|
|
2786
|
+
bottom: 12px;
|
|
2752
2787
|
z-index: 6;
|
|
2753
2788
|
display: flex;
|
|
2754
2789
|
align-items: center;
|
|
@@ -2767,6 +2802,109 @@ const CSS = `
|
|
|
2767
2802
|
[data-dsh-activity-pane] .dap-top:focus-visible {
|
|
2768
2803
|
background: #262932;
|
|
2769
2804
|
}
|
|
2805
|
+
/* 标题行右侧工具按钮区(R-01-021/AC-05):固定于行尾、常显档位切换按钮,未来新
|
|
2806
|
+
工具按钮统一加入此区;宽度不随悬停态变化,收起方向图标的显隐不挤动本区按钮;
|
|
2807
|
+
左缘 padding 12px 与标题区保持充分间隔。收起方向图标(R-01-011/AC-07)是标题区
|
|
2808
|
+
的行尾提示:常态宽度为 0 不占位,鼠标悬停或键盘聚焦标题区时在标题区最右端即时
|
|
2809
|
+
显现(无过渡动画),离开即隐藏。 */
|
|
2810
|
+
[data-dsh-activity-pane] .dap-tools {
|
|
2811
|
+
display: flex;
|
|
2812
|
+
align-items: center;
|
|
2813
|
+
gap: 4px;
|
|
2814
|
+
padding: 10px 12px;
|
|
2815
|
+
}
|
|
2816
|
+
[data-dsh-activity-pane] .dap-density {
|
|
2817
|
+
display: flex;
|
|
2818
|
+
align-items: center;
|
|
2819
|
+
justify-content: center;
|
|
2820
|
+
width: 22px;
|
|
2821
|
+
height: 22px;
|
|
2822
|
+
padding: 0;
|
|
2823
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
2824
|
+
border-radius: 999px;
|
|
2825
|
+
background: #1d1f25;
|
|
2826
|
+
color: inherit;
|
|
2827
|
+
cursor: pointer;
|
|
2828
|
+
}
|
|
2829
|
+
[data-dsh-activity-pane] .dap-density:hover,
|
|
2830
|
+
[data-dsh-activity-pane] .dap-density:focus-visible {
|
|
2831
|
+
background: #262932;
|
|
2832
|
+
}
|
|
2833
|
+
/* 仓库入口无描边(T-139):视觉强度弱于带描边的档位切换按钮,仅以不透明底色圆形呈现。 */
|
|
2834
|
+
[data-dsh-activity-pane] .dap-repo {
|
|
2835
|
+
display: flex;
|
|
2836
|
+
align-items: center;
|
|
2837
|
+
justify-content: center;
|
|
2838
|
+
width: 22px;
|
|
2839
|
+
height: 22px;
|
|
2840
|
+
border-radius: 999px;
|
|
2841
|
+
background: #1d1f25;
|
|
2842
|
+
color: inherit;
|
|
2843
|
+
cursor: pointer;
|
|
2844
|
+
text-decoration: none;
|
|
2845
|
+
}
|
|
2846
|
+
[data-dsh-activity-pane] .dap-repo:hover,
|
|
2847
|
+
[data-dsh-activity-pane] .dap-repo:focus-visible {
|
|
2848
|
+
background: #262932;
|
|
2849
|
+
}
|
|
2850
|
+
[data-dsh-activity-pane] .dap-collapse-hint {
|
|
2851
|
+
margin-left: auto;
|
|
2852
|
+
display: flex;
|
|
2853
|
+
align-items: center;
|
|
2854
|
+
justify-content: center;
|
|
2855
|
+
width: 0;
|
|
2856
|
+
height: 22px;
|
|
2857
|
+
opacity: 0;
|
|
2858
|
+
overflow: hidden;
|
|
2859
|
+
}
|
|
2860
|
+
[data-dsh-activity-pane] .dap-titlebar:hover .dap-collapse-hint,
|
|
2861
|
+
[data-dsh-activity-pane] .dap-titlebar:focus-visible .dap-collapse-hint {
|
|
2862
|
+
width: 22px;
|
|
2863
|
+
opacity: 1;
|
|
2864
|
+
}
|
|
2865
|
+
/* 卡片显示档位(R-01-021):中间档保留标题行、工作区徽标行、等待末行与最近卡
|
|
2866
|
+
消息预览行、经渲染层 lastOnly 单行渲染时间线(仅最新一行,AC-08);完成提醒卡
|
|
2867
|
+
末行在中间档收合为单行(见下方 data-wait="done" 作用域规则);紧凑档在
|
|
2868
|
+
中间档基础上再隐藏工作区徽标行、时间线末行、等待末行与消息预览行,仅保留
|
|
2869
|
+
标题行(AC-02)——激活跳转逻辑不感知档位,渲染签名含显示档位分量(档位切换
|
|
2870
|
+
经 queueSync 触发一轮重渲染)(R-01-021/AC-04)。 */
|
|
2871
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card :is(
|
|
2872
|
+
.dap-progress,
|
|
2873
|
+
.dap-token-stats
|
|
2874
|
+
) {
|
|
2875
|
+
display: none;
|
|
2876
|
+
}
|
|
2877
|
+
[data-dsh-activity-pane][data-density="compact"] .dap-card :is(
|
|
2878
|
+
.dap-card-head,
|
|
2879
|
+
.dap-trace,
|
|
2880
|
+
.dap-subtrace,
|
|
2881
|
+
.dap-progress,
|
|
2882
|
+
.dap-token-stats,
|
|
2883
|
+
.dap-foot,
|
|
2884
|
+
.dap-history-line,
|
|
2885
|
+
.dap-note
|
|
2886
|
+
) {
|
|
2887
|
+
display: none;
|
|
2888
|
+
}
|
|
2889
|
+
/* 中间档完成提醒卡末行收合为单行(R-01-021/AC-08,东家反馈):末行两行内容
|
|
2890
|
+
(「已完成」胶囊行 +「继续对话,或移入历史」与「移入历史」按钮行)冗余,收合为
|
|
2891
|
+
单行——「已完成」胶囊居左、按钮 margin-left:auto 居右、正文不再显示;两段包裹层
|
|
2892
|
+
以 display: contents 释放为同行 flex 项。选择器以卡片根 data-wait="done" 作用域,
|
|
2893
|
+
阻塞/错误提醒卡与完整呈现档的两行结构不受影响,紧凑档仍整体隐藏末行。 */
|
|
2894
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card[data-kind="awaiting"][data-wait="done"] .dap-foot {
|
|
2895
|
+
flex-direction: row;
|
|
2896
|
+
align-items: center;
|
|
2897
|
+
gap: 6px;
|
|
2898
|
+
}
|
|
2899
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card[data-kind="awaiting"][data-wait="done"] :is(.dap-await-head, .dap-note-row) {
|
|
2900
|
+
display: contents;
|
|
2901
|
+
}
|
|
2902
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card[data-kind="awaiting"][data-wait="done"] .dap-note {
|
|
2903
|
+
display: none;
|
|
2904
|
+
}
|
|
2905
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card[data-kind="awaiting"][data-wait="done"] .dap-confirm {
|
|
2906
|
+
margin-left: auto;
|
|
2907
|
+
}
|
|
2770
2908
|
[data-dsh-activity-pane] .dap-list {
|
|
2771
2909
|
display: flex;
|
|
2772
2910
|
flex-direction: column;
|
|
@@ -3328,17 +3466,30 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3328
3466
|
[data-dsh-activity-pane] .dap-fill {
|
|
3329
3467
|
position: absolute; inset: 0 auto 0 0; width: 0%;
|
|
3330
3468
|
border-radius: 6px;
|
|
3331
|
-
|
|
3332
|
-
background-size: 200% 100%;
|
|
3469
|
+
overflow: hidden;
|
|
3333
3470
|
box-shadow: 0 0 7px rgba(88, 201, 143, 0.5);
|
|
3334
3471
|
transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1);
|
|
3335
3472
|
/* 进度条仅存于运行卡骨架:会话运行全程持续向右滚动条纹,作为活动标志
|
|
3336
3473
|
(对齐 answer-pet 的 ap-stripes,R-01-009/AC-08)。 */
|
|
3474
|
+
/* 条带载体 ::after 与滚动动画分离(T-128):background-position 不可合成,
|
|
3475
|
+
每帧重绘;改由伪元素 transform 平移承载滚动,fill 只保留 width 过渡与
|
|
3476
|
+
裁切,滚动帧全程合成器驱动。 */
|
|
3477
|
+
}
|
|
3478
|
+
[data-dsh-activity-pane] .dap-fill::after {
|
|
3479
|
+
content: "";
|
|
3480
|
+
position: absolute;
|
|
3481
|
+
inset: 0 auto 0 0;
|
|
3482
|
+
/* 覆盖 fill 宽度 + 一次位移量:平移全程右缘不落后于 fill 右缘,无缝循环。 */
|
|
3483
|
+
width: calc(100% + 40px);
|
|
3484
|
+
/* 周期 20px(色带 10px)与原 background-position 实现一致——px 色标不受
|
|
3485
|
+
background-size 拉伸(T-128 双轴实测);translateX(-40px) 恰为 2 个周期,
|
|
3486
|
+
无缝且速度 40px/0.8s 与原实现一致。 */
|
|
3487
|
+
background: repeating-linear-gradient(90deg, #58c98f 0 10px, #3fbf86 10px 20px);
|
|
3337
3488
|
animation: dap-stripes 0.8s linear infinite;
|
|
3338
3489
|
}
|
|
3339
3490
|
@keyframes dap-stripes {
|
|
3340
|
-
from {
|
|
3341
|
-
to {
|
|
3491
|
+
from { transform: translateX(-40px); }
|
|
3492
|
+
to { transform: translateX(0); }
|
|
3342
3493
|
}
|
|
3343
3494
|
@media (prefers-reduced-motion: reduce) {
|
|
3344
3495
|
/* answer-pet 保留状态脉冲/进度条纹;仅关闭宽度过渡,避免状态反馈消失。 */
|
|
@@ -3448,6 +3599,11 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3448
3599
|
touch-action: none;
|
|
3449
3600
|
}
|
|
3450
3601
|
[data-dsh-activity-pane][data-open="true"] { transform: translateX(0); }
|
|
3602
|
+
/* 屏外休眠(T-127):抽屉关闭(含初始未开,data-open 缺省视同关闭)时子树整体
|
|
3603
|
+
跳过渲染——无限动画与样式失效不再产生渲染开销;布局状态保留,scrollTop 不归零
|
|
3604
|
+
(区别于 display:none,T-027);打开瞬间恢复渲染,滑入过渡不变。开关与遮罩挂
|
|
3605
|
+
body,不落本规则,浮动开关徽标脉冲(R-01-002/AC-06、AC-07)照常。 */
|
|
3606
|
+
[data-dsh-activity-pane]:not([data-open="true"]) { content-visibility: hidden; }
|
|
3451
3607
|
.dap-backdrop[data-drawer-open] { display: block; }
|
|
3452
3608
|
.dap-toggle { display: flex; }
|
|
3453
3609
|
.dap-toggle[data-drawer-open] { display: none; }
|
|
@@ -3541,13 +3697,23 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-track {
|
|
|
3541
3697
|
body:not([data-ds-dark-theme]) .dap-toggle {
|
|
3542
3698
|
background: var(--dsw-alias-button-floating-fill, rgba(255, 255, 255, 0.94));
|
|
3543
3699
|
}
|
|
3544
|
-
/*
|
|
3545
|
-
|
|
3700
|
+
/* 「回到顶部」与标题行工具区档位/仓库入口按钮的浅色覆盖:不透明层-2 底色与外壳描边别名
|
|
3701
|
+
(R-01-018/AC-05、R-01-021/AC-05、R-01-022/AC-01);仓库入口无描边(T-139),只并入底色组。 */
|
|
3702
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top,
|
|
3703
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density,
|
|
3704
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-repo {
|
|
3546
3705
|
background: var(--dsw-alias-bg-layer-2, #ffffff);
|
|
3706
|
+
}
|
|
3707
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top,
|
|
3708
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density {
|
|
3547
3709
|
border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
3548
3710
|
}
|
|
3549
3711
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top:hover,
|
|
3550
|
-
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top:focus-visible
|
|
3712
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top:focus-visible,
|
|
3713
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density:hover,
|
|
3714
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density:focus-visible,
|
|
3715
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-repo:hover,
|
|
3716
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-repo:focus-visible {
|
|
3551
3717
|
background: var(--dsw-alias-bg-layer-3, #eceef1);
|
|
3552
3718
|
}
|
|
3553
3719
|
`;
|
|
@@ -3623,6 +3789,22 @@ function writeStoredPaneWidth(width) {
|
|
|
3623
3789
|
} catch {}
|
|
3624
3790
|
}
|
|
3625
3791
|
|
|
3792
|
+
/** 读取持久化卡片显示档位:缺失/非法值经 normalizeDensity 归一为默认中间档;
|
|
3793
|
+
* localStorage 不可用(隐私模式)静默回退中间档(R-01-021/AC-06)。 */
|
|
3794
|
+
function readStoredDensity() {
|
|
3795
|
+
try {
|
|
3796
|
+
return normalizeDensity(window.localStorage.getItem(DENSITY_STORAGE_KEY));
|
|
3797
|
+
} catch {
|
|
3798
|
+
return "medium";
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
/** 切换时持久化卡片显示档位;localStorage 不可用时静默跳过(R-01-021/AC-06)。 */
|
|
3802
|
+
function writeStoredDensity(value) {
|
|
3803
|
+
try {
|
|
3804
|
+
window.localStorage.setItem(DENSITY_STORAGE_KEY, value);
|
|
3805
|
+
} catch {}
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3626
3808
|
function apply(ctx) {
|
|
3627
3809
|
const previousCleanup = document[INSTANCE_KEY] ?? globalThis[INSTANCE_KEY];
|
|
3628
3810
|
if (typeof previousCleanup === "function") previousCleanup();
|
|
@@ -3640,6 +3822,10 @@ function apply(ctx) {
|
|
|
3640
3822
|
let clockTimer = null;
|
|
3641
3823
|
let recentTimeTimer = null;
|
|
3642
3824
|
let syncScheduled = false;
|
|
3825
|
+
/** 渲染节流状态(T-127):lastSyncAt 取 0 保证首轮立即渲染;syncThrottleTimer 为
|
|
3826
|
+
* 节流窗口尾的在途 timer,交付或卸载时清空。 */
|
|
3827
|
+
let lastSyncAt = 0;
|
|
3828
|
+
let syncThrottleTimer = null;
|
|
3643
3829
|
let lastSig = "";
|
|
3644
3830
|
/** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
|
|
3645
3831
|
let pulseSignature = "";
|
|
@@ -3674,6 +3860,12 @@ function apply(ctx) {
|
|
|
3674
3860
|
let collapsed = false;
|
|
3675
3861
|
/** 当前桌面列宽:启动时从 localStorage 恢复,拖拽实时更新,重挂载后保留(R-01-015)。 */
|
|
3676
3862
|
let paneWidth = readStoredPaneWidth();
|
|
3863
|
+
/** 卡片显示档位(full/medium/compact):启动时从 localStorage 恢复,切换实时更新,
|
|
3864
|
+
* 重挂载后保留(R-01-021/AC-06)。 */
|
|
3865
|
+
let densityLevel = readStoredDensity();
|
|
3866
|
+
/** 待执行的锚定补偿:档位切换时登记当前选中卡片顶部的视口相对位置,本轮渲染
|
|
3867
|
+
* 提交后量测该卡新位置并补偿 scrollTop(R-01-021/AC-01)。 */
|
|
3868
|
+
let pendingDensityAnchor = null;
|
|
3677
3869
|
/** 用户最近一次激活的卡片 id;打开重试链被更新的激活意图取代即取消。 */
|
|
3678
3870
|
let lastActivatedId = null;
|
|
3679
3871
|
/** 最近一次已处理的当前卡片;同一卡片的运行时重绘不反复打断用户手动滚动。 */
|
|
@@ -3841,13 +4033,23 @@ function apply(ctx) {
|
|
|
3841
4033
|
if (changed) notifyLayoutChange();
|
|
3842
4034
|
}
|
|
3843
4035
|
|
|
4036
|
+
function deliverSync() {
|
|
4037
|
+
syncScheduled = false;
|
|
4038
|
+
syncThrottleTimer = null;
|
|
4039
|
+
if (disposed) return;
|
|
4040
|
+
lastSyncAt = Date.now();
|
|
4041
|
+
render();
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
/** 渲染入口合帧 + 节流(T-127):距上次渲染落点不足 SYNC_MIN_INTERVAL_MS 时,
|
|
4045
|
+
* 本轮请求合并到窗口尾由 timer 交付(在途请求只此一个);达到间隔走原 rAF
|
|
4046
|
+
* 合帧立即渲染。空闲期无事件即无排队,不引入常驻唤醒。 */
|
|
3844
4047
|
function queueSync() {
|
|
3845
4048
|
if (disposed || syncScheduled) return;
|
|
3846
4049
|
syncScheduled = true;
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
});
|
|
4050
|
+
const wait = SYNC_MIN_INTERVAL_MS - (Date.now() - lastSyncAt);
|
|
4051
|
+
if (wait > 0) syncThrottleTimer = setTimeout(deliverSync, wait);
|
|
4052
|
+
else schedule(deliverSync);
|
|
3851
4053
|
}
|
|
3852
4054
|
|
|
3853
4055
|
// ---- 完成确认通道(R-01-002/AC-10~AC-12、R-01-010/AC-06,C-030) ----
|
|
@@ -4037,6 +4239,36 @@ function apply(ctx) {
|
|
|
4037
4239
|
window.addEventListener("pageshow", onBusyPageShow);
|
|
4038
4240
|
connectBusyStream();
|
|
4039
4241
|
|
|
4242
|
+
// ---- 回到前台渲染管线自愈(R-01-001/AC-03 回归修复) ----
|
|
4243
|
+
// iOS 后台挂起会丢弃在途 setTimeout:queueSync 的节流窗口尾与流式派生窗口若在
|
|
4244
|
+
// 挂起前排队,恢复后永不交付——syncScheduled/logDeriveTimer 永久滞留,此后一切
|
|
4245
|
+
// 更新(store 订阅推送、SSE 快照、时钟 tick)都被 queueSync 早退吞掉,窗格停留
|
|
4246
|
+
// 在挂起前状态(会话已完成仍显示运行中)。回前台时无条件清掉两类在途 timer 并
|
|
4247
|
+
// 立即交付一轮渲染(rAF 路径挂起安全),与 acks/busy 通道回前台重建同模式。
|
|
4248
|
+
function resumeRenderPipeline() {
|
|
4249
|
+
if (disposed) return;
|
|
4250
|
+
if (syncThrottleTimer !== null) {
|
|
4251
|
+
clearTimeout(syncThrottleTimer);
|
|
4252
|
+
syncThrottleTimer = null;
|
|
4253
|
+
}
|
|
4254
|
+
syncScheduled = false;
|
|
4255
|
+
for (const detail of sessionDetailsById.values()) {
|
|
4256
|
+
if (detail.logDeriveTimer && detail.logDeriveFlush) {
|
|
4257
|
+
clearTimeout(detail.logDeriveTimer);
|
|
4258
|
+
detail.logDeriveFlush();
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
schedule(deliverSync);
|
|
4262
|
+
}
|
|
4263
|
+
const onSyncVisibilityResume = () => {
|
|
4264
|
+
if (document.visibilityState === "visible") resumeRenderPipeline();
|
|
4265
|
+
};
|
|
4266
|
+
const onSyncPageShow = (event) => {
|
|
4267
|
+
if (event?.persisted === true) resumeRenderPipeline();
|
|
4268
|
+
};
|
|
4269
|
+
document.addEventListener("visibilitychange", onSyncVisibilityResume);
|
|
4270
|
+
window.addEventListener("pageshow", onSyncPageShow);
|
|
4271
|
+
|
|
4040
4272
|
function remoteValue(response) {
|
|
4041
4273
|
if (response?.ok === true) return response.value;
|
|
4042
4274
|
throw response?.error ?? new Error("remote request failed");
|
|
@@ -4175,7 +4407,7 @@ function apply(ctx) {
|
|
|
4175
4407
|
});
|
|
4176
4408
|
}
|
|
4177
4409
|
}
|
|
4178
|
-
captureSessionLog(id, {
|
|
4410
|
+
captureSessionLog(id, { cwd: byId[id]?.cwd ?? "" });
|
|
4179
4411
|
// 长会话深读兜底(R-01-013/AC-03):最近卡预览/子代理溯源不在尾页日志窗口内
|
|
4180
4412
|
// 且宿主标记 hasMore 时,按 beforeSeq 向前回溯翻页(默认无页数上限)。
|
|
4181
4413
|
const windowEntries = Array.isArray(detail.log?.entries) ? detail.log.entries : [];
|
|
@@ -4251,7 +4483,7 @@ function apply(ctx) {
|
|
|
4251
4483
|
/** 绑定会话并水合事件源(dsh 0.1.5 起会话内容经 eventSource 流式下发:打开即收
|
|
4252
4484
|
* 完整日志窗口 + 实时尾,原生 Conversation 同源)。冷会话补一次 open(),日志窗口
|
|
4253
4485
|
* 快照引用变化即重派生详情(时间线/预览/模型),窗口由宿主按消息对齐分页。 */
|
|
4254
|
-
function captureSessionLog(id, {
|
|
4486
|
+
function captureSessionLog(id, { cwd } = {}) {
|
|
4255
4487
|
const detail = sessionDetailsById.get(id) ?? {};
|
|
4256
4488
|
sessionDetailsById.set(id, detail);
|
|
4257
4489
|
let session = null;
|
|
@@ -4272,10 +4504,7 @@ function apply(ctx) {
|
|
|
4272
4504
|
session.eventSource.subscribe(() => {
|
|
4273
4505
|
if (disposed) return;
|
|
4274
4506
|
const listSnap = getSnapshot(sessions, "list");
|
|
4275
|
-
captureSessionLog(id, {
|
|
4276
|
-
subagent: isSubagentRow(listSnap?.byId?.[id], listSnap ?? {}),
|
|
4277
|
-
cwd: listSnap?.byId?.[id]?.cwd ?? "",
|
|
4278
|
-
});
|
|
4507
|
+
captureSessionLog(id, { cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
4279
4508
|
queueSync();
|
|
4280
4509
|
}),
|
|
4281
4510
|
);
|
|
@@ -4301,8 +4530,30 @@ function apply(ctx) {
|
|
|
4301
4530
|
const log = session.eventSource?.getSnapshot?.() ?? null;
|
|
4302
4531
|
if (log === detail.log) return;
|
|
4303
4532
|
detail.log = log;
|
|
4304
|
-
|
|
4305
|
-
|
|
4533
|
+
// 流式派生合并(T-127):事件到达只更新引用并标脏,applyLogEvents 的 O(日志窗口)
|
|
4534
|
+
// 全量折叠/预览/模型提取合并进 SYNC_MIN_INTERVAL_MS 窗口执行——事件率与派生成本
|
|
4535
|
+
// 解耦,消化时读到的即最新窗口。subagent/cwd 在回调内经 list 快照现取(timer 在途
|
|
4536
|
+
// 期间行属性可能突变,建窗入参不代表消化时刻;与 logSourceSubs 回调同模式),
|
|
4537
|
+
// cwd 在快照不可得时回退建窗入参(subagent 现取即权威,无建窗回退)。
|
|
4538
|
+
// 深翻路径为一次性同步调用,不经本窗口。交付体同时挂在 detail.logDeriveFlush
|
|
4539
|
+
// 上:回前台自愈据此补交付被挂起丢弃的窗口(resumeRenderPipeline)。
|
|
4540
|
+
if (!detail.logDeriveTimer && typeof setTimeout === "function") {
|
|
4541
|
+
const flushDerive = () => {
|
|
4542
|
+
detail.logDeriveTimer = null;
|
|
4543
|
+
detail.logDeriveFlush = null;
|
|
4544
|
+
if (disposed) return;
|
|
4545
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
4546
|
+
const entries = Array.isArray(detail.log?.entries) ? detail.log.entries : [];
|
|
4547
|
+
applyLogEvents(id, detail, entries, {
|
|
4548
|
+
subagent: isSubagentRow(listSnap?.byId?.[id], listSnap?.byId ?? {}),
|
|
4549
|
+
cwd: listSnap?.byId?.[id]?.cwd ?? cwd,
|
|
4550
|
+
});
|
|
4551
|
+
queueSync();
|
|
4552
|
+
};
|
|
4553
|
+
detail.logDeriveFlush = flushDerive;
|
|
4554
|
+
detail.logDeriveTimer = setTimeout(flushDerive, SYNC_MIN_INTERVAL_MS);
|
|
4555
|
+
}
|
|
4556
|
+
queueSync();
|
|
4306
4557
|
}
|
|
4307
4558
|
|
|
4308
4559
|
/** 部署级模型目录一次性读取(R-01-012/AC-01):dsh 0.1.5 起 per-session models
|
|
@@ -4380,7 +4631,7 @@ function apply(ctx) {
|
|
|
4380
4631
|
return true;
|
|
4381
4632
|
}
|
|
4382
4633
|
function bindPaneControls(pane) {
|
|
4383
|
-
const header = pane.querySelector(".dap-
|
|
4634
|
+
const header = pane.querySelector(".dap-titlebar");
|
|
4384
4635
|
const rail = pane.querySelector(".dap-rail");
|
|
4385
4636
|
const resize = pane.querySelector(".dap-resize");
|
|
4386
4637
|
const scroll = pane.querySelector(".dap-scroll");
|
|
@@ -4482,6 +4733,37 @@ function apply(ctx) {
|
|
|
4482
4733
|
const onTopClick = () => {
|
|
4483
4734
|
scroll?.scrollTo({ top: 0, behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
4484
4735
|
};
|
|
4736
|
+
// 显示档位循环切换(R-01-021/AC-01):完整→中间→紧凑→完整,形态写窗格根属性
|
|
4737
|
+
// 驱动纯 CSS 呈现,持久化于 localStorage(AC-06),会话状态变化不触碰已选档位(AC-07)。
|
|
4738
|
+
const densityBtn = pane.querySelector(".dap-density");
|
|
4739
|
+
const applyDensity = () => {
|
|
4740
|
+
pane.setAttribute("data-density", densityLevel);
|
|
4741
|
+
if (densityBtn !== null) {
|
|
4742
|
+
const next = nextDensity(densityLevel);
|
|
4743
|
+
densityBtn.setAttribute("aria-label", `切换为${DENSITY_LABELS[next]}显示`);
|
|
4744
|
+
densityBtn.title = `${DENSITY_LABELS[next]}显示`;
|
|
4745
|
+
}
|
|
4746
|
+
};
|
|
4747
|
+
const onDensityClick = () => {
|
|
4748
|
+
// 滚动锚定(R-01-021/AC-01):记录切换前当前选中卡片顶部相对滚动视口的
|
|
4749
|
+
// 位置,档位翻转后把 scrollTop 补偿回该相对位置,使卡片顶部在屏幕上不动。
|
|
4750
|
+
const scrollEl = scroll ?? pane.querySelector(".dap-scroll");
|
|
4751
|
+
const currentCard = scrollEl?.querySelector(".dap-card[data-current]") ?? null;
|
|
4752
|
+
const viewportTop = scrollEl?.getBoundingClientRect().top ?? 0;
|
|
4753
|
+
const anchorTop = currentCard ? currentCard.getBoundingClientRect().top - viewportTop : null;
|
|
4754
|
+
densityLevel = nextDensity(densityLevel);
|
|
4755
|
+
writeStoredDensity(densityLevel);
|
|
4756
|
+
applyDensity();
|
|
4757
|
+
// 档位已纳入渲染签名:触发一轮同步让时间线按新档位以 lastOnly 重建;
|
|
4758
|
+
// 锚定补偿在本轮渲染提交后执行,此时量测才含新行高
|
|
4759
|
+
//(R-01-021/AC-01、AC-08)。
|
|
4760
|
+
pendingDensityAnchor = currentCard && anchorTop !== null ? anchorTop : null;
|
|
4761
|
+
queueSync();
|
|
4762
|
+
};
|
|
4763
|
+
// 档位按钮位于标题行右侧的工具区(标题区折叠控件的兄弟节点),激活天然不会
|
|
4764
|
+
// 冒泡为标题区折叠,无需额外阻断。
|
|
4765
|
+
densityBtn?.addEventListener("click", onDensityClick);
|
|
4766
|
+
applyDensity();
|
|
4485
4767
|
header?.addEventListener("click", onHeaderActivate);
|
|
4486
4768
|
header?.addEventListener("keydown", onHeaderKeydown);
|
|
4487
4769
|
rail?.addEventListener("click", onRailClick);
|
|
@@ -4502,6 +4784,7 @@ function apply(ctx) {
|
|
|
4502
4784
|
recentMore?.removeEventListener("click", onRecentMoreClick);
|
|
4503
4785
|
if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);
|
|
4504
4786
|
topBtn?.removeEventListener("click", onTopClick);
|
|
4787
|
+
densityBtn?.removeEventListener("click", onDensityClick);
|
|
4505
4788
|
resize?.removeEventListener("pointerdown", onResizeDown);
|
|
4506
4789
|
};
|
|
4507
4790
|
}
|
|
@@ -4521,10 +4804,16 @@ function apply(ctx) {
|
|
|
4521
4804
|
pane.className = PANE_CLASS;
|
|
4522
4805
|
center.insertBefore(pane, seat);
|
|
4523
4806
|
pane.innerHTML = `
|
|
4524
|
-
<div class="dap-header"
|
|
4525
|
-
<span
|
|
4526
|
-
|
|
4527
|
-
|
|
4807
|
+
<div class="dap-header">
|
|
4808
|
+
<span class="dap-titlebar" role="button" tabindex="0" aria-expanded="true" aria-label="收起活动会话窗格" title="收起">
|
|
4809
|
+
<span>活动会话</span>
|
|
4810
|
+
<span class="dap-count" role="status" aria-live="polite"></span>
|
|
4811
|
+
<span class="dap-collapse-hint" aria-hidden="true"></span>
|
|
4812
|
+
</span>
|
|
4813
|
+
<span class="dap-tools">
|
|
4814
|
+
<a class="dap-repo" href="https://github.com/ccll/dsh-activity-pane" target="_blank" rel="noreferrer noopener" aria-label="报告问题" title="报告问题"></a>
|
|
4815
|
+
<button class="dap-density" type="button" aria-label="切换为紧凑显示" title="紧凑显示"></button>
|
|
4816
|
+
</span>
|
|
4528
4817
|
</div>
|
|
4529
4818
|
<div class="dap-scroll">
|
|
4530
4819
|
<div class="dap-list" tabindex="-1"><div class="dap-tracks" aria-hidden="true"></div></div>
|
|
@@ -4543,6 +4832,13 @@ function apply(ctx) {
|
|
|
4543
4832
|
pane.style.setProperty("--dap-width", `${paneWidth}px`);
|
|
4544
4833
|
// 「回到顶部」按钮为纯图标呈现(R-01-018/AC-05):骨架无文字,图标在创建时注入。
|
|
4545
4834
|
pane.querySelector(".dap-top").append(createTopIcon());
|
|
4835
|
+
// 紧凑显示切换按钮常显于标题行右侧工具区(R-01-021/AC-05);切换时以 data-density
|
|
4836
|
+
// 驱动纯 CSS 呈现,骨架重建后由 bindPaneControls 的 applyDensity 恢复形态。
|
|
4837
|
+
pane.querySelector(".dap-density").append(createDensityIcon());
|
|
4838
|
+
// 仓库入口常显于标题行右侧工具区(R-01-022/AC-01);图标在创建时注入。
|
|
4839
|
+
pane.querySelector(".dap-repo").append(createRepoIcon());
|
|
4840
|
+
// 收起方向图标为标题行悬停/聚焦的可见性提示(R-01-011/AC-07),图标在创建时注入。
|
|
4841
|
+
pane.querySelector(".dap-collapse-hint").append(createCollapseIcon());
|
|
4546
4842
|
}
|
|
4547
4843
|
if (pane !== boundPane) {
|
|
4548
4844
|
unbindPaneControls?.();
|
|
@@ -4715,6 +5011,50 @@ function apply(ctx) {
|
|
|
4715
5011
|
});
|
|
4716
5012
|
}
|
|
4717
5013
|
|
|
5014
|
+
/** 紧凑显示切换按钮的「多行收拢为单行」密度图标:上下两个指向中线的箭头夹一条
|
|
5015
|
+
* 标题行横线,与「回到顶部」的单向箭头、标题行的收起方向图标均可区分
|
|
5016
|
+
* (canonical 图标集无现成字形,14 盒 stroke 风格与 createTopIcon 一致,R-01-021/AC-05)。 */
|
|
5017
|
+
function createDensityIcon() {
|
|
5018
|
+
return createInlineIcon({
|
|
5019
|
+
viewBox: "0 0 14 14",
|
|
5020
|
+
width: 14,
|
|
5021
|
+
height: 14,
|
|
5022
|
+
parts: [
|
|
5023
|
+
{ attrs: { d: "m3.5 4.5 3.5-3.5 3.5 3.5", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5024
|
+
{ attrs: { d: "M3 7h8", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5025
|
+
{ attrs: { d: "m3.5 9.5 3.5 3.5 3.5-3.5", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5026
|
+
],
|
|
5027
|
+
});
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
/** 标题行的「收起」方向图标(R-01-011/AC-07):左侧竖杠 + 向左箭头指向竖杠,
|
|
5031
|
+
* 表达窗格收缩到左侧;随 .dap-collapse-hint 在标题行悬停/聚焦时显现。 */
|
|
5032
|
+
function createCollapseIcon() {
|
|
5033
|
+
return createInlineIcon({
|
|
5034
|
+
viewBox: "0 0 14 14",
|
|
5035
|
+
width: 14,
|
|
5036
|
+
height: 14,
|
|
5037
|
+
parts: [
|
|
5038
|
+
{ attrs: { d: "M3 2.5v9", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5039
|
+
{ attrs: { d: "M11.5 7H5", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5040
|
+
{ attrs: { d: "M8.25 3.75 5 7l3.25 3.25", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5041
|
+
],
|
|
5042
|
+
});
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
/** 标题行工具区的仓库入口图标(R-01-022/AC-01):GitHub Octicon `mark-github` 字形
|
|
5046
|
+
* (MIT 许可,Primer Octicons),与工具区既有图标同用 14px 字形盒。 */
|
|
5047
|
+
function createRepoIcon() {
|
|
5048
|
+
return createInlineIcon({
|
|
5049
|
+
viewBox: "0 0 16 16",
|
|
5050
|
+
width: 14,
|
|
5051
|
+
height: 14,
|
|
5052
|
+
parts: [
|
|
5053
|
+
{ attrs: { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.28.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z", fill: "currentColor" } },
|
|
5054
|
+
],
|
|
5055
|
+
});
|
|
5056
|
+
}
|
|
5057
|
+
|
|
4718
5058
|
function createUserIcon() {
|
|
4719
5059
|
return createInlineIcon({
|
|
4720
5060
|
viewBox: "0 0 16 16",
|
|
@@ -5217,7 +5557,7 @@ function apply(ctx) {
|
|
|
5217
5557
|
if (entry.kind === "running") {
|
|
5218
5558
|
renderProgressRow(el, entry.progress);
|
|
5219
5559
|
const traceContainer = el.querySelector(".dap-trace");
|
|
5220
|
-
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
5560
|
+
if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
|
|
5221
5561
|
renderTokenStats(el, entry);
|
|
5222
5562
|
return;
|
|
5223
5563
|
}
|
|
@@ -5261,7 +5601,7 @@ function apply(ctx) {
|
|
|
5261
5601
|
|
|
5262
5602
|
if (entry.kind === "awaiting") {
|
|
5263
5603
|
const traceContainer = el.querySelector(".dap-trace");
|
|
5264
|
-
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
5604
|
+
if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
|
|
5265
5605
|
removeAwaitingHeadDuration(el);
|
|
5266
5606
|
renderTokenStats(el, entry);
|
|
5267
5607
|
const confirm = el.querySelector(".dap-confirm");
|
|
@@ -5326,7 +5666,7 @@ function apply(ctx) {
|
|
|
5326
5666
|
// dsh 0.1.5 起快照不再携带会话内容:时间线经 eventSource 日志窗口
|
|
5327
5667
|
// 就地重派生(R-01-009)。
|
|
5328
5668
|
const listSnap = getSnapshot(sessions, "list");
|
|
5329
|
-
captureSessionLog(id, {
|
|
5669
|
+
captureSessionLog(id, { cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
5330
5670
|
queueSync();
|
|
5331
5671
|
});
|
|
5332
5672
|
} catch {
|
|
@@ -5417,15 +5757,18 @@ function apply(ctx) {
|
|
|
5417
5757
|
}
|
|
5418
5758
|
|
|
5419
5759
|
/** 数量标识内容写入(R-01-014/AC-06):加载态显示活动指示——已是指示则不重写,
|
|
5420
|
-
* 避免每轮 replaceChildren 重启动画抖动;计数态恢复文本写入,textContent 赋值自动摘除指示。
|
|
5760
|
+
* 避免每轮 replaceChildren 重启动画抖动;计数态恢复文本写入,textContent 赋值自动摘除指示。
|
|
5761
|
+
* 计数态同时写入悬停 tips(R-01-001/AC-08):说明分子/分母口径;加载态无计数可解释,摘除。 */
|
|
5421
5762
|
function setCountBadgeContent(el, badge) {
|
|
5422
5763
|
if (badge.mode === "loading") {
|
|
5423
5764
|
const spinner = el.firstElementChild;
|
|
5424
5765
|
if (!(el.childNodes.length === 1 && spinner !== null && spinner.classList.contains("dap-spinner")))
|
|
5425
5766
|
el.replaceChildren(makeEl("span", "dap-spinner"));
|
|
5426
|
-
|
|
5767
|
+
if (el.getAttribute("title") !== null) el.removeAttribute("title");
|
|
5768
|
+
} else {
|
|
5427
5769
|
// 值未变不写文本节点:aria-live 下相同赋值也会触发替换与重复播报。
|
|
5428
|
-
el.textContent = badge.text;
|
|
5770
|
+
if (el.textContent !== badge.text) el.textContent = badge.text;
|
|
5771
|
+
if (el.getAttribute("title") !== badge.tip) el.setAttribute("title", badge.tip);
|
|
5429
5772
|
}
|
|
5430
5773
|
}
|
|
5431
5774
|
|
|
@@ -6133,7 +6476,7 @@ function apply(ctx) {
|
|
|
6133
6476
|
// 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
|
|
6134
6477
|
// 历史卡的相对活动时间随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
|
|
6135
6478
|
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
6136
|
-
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);
|
|
6479
|
+
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, densityLevel]);
|
|
6137
6480
|
if (sig === lastSig) return;
|
|
6138
6481
|
const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
|
|
6139
6482
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
|
@@ -6213,7 +6556,7 @@ function apply(ctx) {
|
|
|
6213
6556
|
ensureCurrentCardVisible(pane.querySelector(".dap-scroll"), snapshot?.current ?? null);
|
|
6214
6557
|
if (focusAfterMigrationId !== null) cardsById.get(focusAfterMigrationId)?.el.focus();
|
|
6215
6558
|
// 区域已有条目但列表仍在途时,在区头部显示行内加载指示(R-01-014/AC-01)。
|
|
6216
|
-
const headerEl = pane.querySelector(".dap-
|
|
6559
|
+
const headerEl = pane.querySelector(".dap-titlebar");
|
|
6217
6560
|
const recentHeadEl = recentSection?.querySelector(".dap-recent-head") ?? null;
|
|
6218
6561
|
for (const [head, hasItems] of [[headerEl, active.length > 0], [recentHeadEl, recent.length > 0]]) {
|
|
6219
6562
|
if (head === null) continue;
|
|
@@ -6225,11 +6568,12 @@ function apply(ctx) {
|
|
|
6225
6568
|
}
|
|
6226
6569
|
}
|
|
6227
6570
|
|
|
6228
|
-
// 计数与折叠:n/m
|
|
6571
|
+
// 计数与折叠:n/m 只统计主会话——分子为运行中数、分母为其加等待行动主会话之和
|
|
6229
6572
|
// (R-01-001/AC-04、AC-05);空态同样显示 0/0(AC-06)。列表在途时不冒充计数,
|
|
6230
|
-
// 三处数量标识显示加载指示(R-01-014/AC-06
|
|
6231
|
-
//
|
|
6232
|
-
//
|
|
6573
|
+
// 三处数量标识显示加载指示(R-01-014/AC-06)。悬停 tips 说明分子/分母口径(AC-08)。
|
|
6574
|
+
// 脉冲由 data-awaiting 承载:任一等待行动(阻塞等待、完成提醒或错误提醒)即脉冲
|
|
6575
|
+
// (R-01-002/AC-06,C-037);底色经 data-tone 跟随等待构成——错误 > 阻塞 > 完成
|
|
6576
|
+
// 优先级取红/金/绿(C-040、C-043)。
|
|
6233
6577
|
const count = pane.querySelector(".dap-count");
|
|
6234
6578
|
const railCount = pane.querySelector(".dap-rail-count");
|
|
6235
6579
|
const { waiting, blocked, total } = awaitBadgeStats(active);
|
|
@@ -6280,6 +6624,21 @@ function apply(ctx) {
|
|
|
6280
6624
|
prevRenderedActiveIds = new Set(active.map((entry) => String(entry.id)));
|
|
6281
6625
|
prevRenderedRecentIds = new Set(recent.map((entry) => String(entry.id)));
|
|
6282
6626
|
}
|
|
6627
|
+
// 锚定补偿(R-01-021/AC-01):档位切换的渲染落地后量测当前选中卡的新位置并
|
|
6628
|
+
// 补偿 scrollTop,使当前卡顶部回到切换前相对视口的位置;补偿量超滚动边界时
|
|
6629
|
+
// 由浏览器钳制(以当前卡不滚出可视范围为准)。
|
|
6630
|
+
if (pendingDensityAnchor !== null) {
|
|
6631
|
+
const scrollEl = pane.querySelector(".dap-scroll");
|
|
6632
|
+
// 补偿按当前 DOM 的 data-current 卡实时定位:跨区迁移等渲染可能重建卡片
|
|
6633
|
+
// 元素,不持有旧引用(量测恒为 attached 节点)。
|
|
6634
|
+
const currentCard = scrollEl?.querySelector(".dap-card[data-current]") ?? null;
|
|
6635
|
+
if (scrollEl !== null && currentCard !== null) {
|
|
6636
|
+
const viewportTop = scrollEl.getBoundingClientRect().top;
|
|
6637
|
+
const shiftedTop = currentCard.getBoundingClientRect().top - viewportTop;
|
|
6638
|
+
scrollEl.scrollTop += shiftedTop - pendingDensityAnchor;
|
|
6639
|
+
}
|
|
6640
|
+
pendingDensityAnchor = null;
|
|
6641
|
+
}
|
|
6283
6642
|
}
|
|
6284
6643
|
|
|
6285
6644
|
// ---- 打开会话(让 sessions.open 自己校验列表,失败时 refresh + 重试) ----
|
|
@@ -6476,12 +6835,19 @@ function apply(ctx) {
|
|
|
6476
6835
|
window.removeEventListener("pageshow", onBusyPageShow);
|
|
6477
6836
|
document.removeEventListener("visibilitychange", onVisibilityResume);
|
|
6478
6837
|
window.removeEventListener("pageshow", onPageShow);
|
|
6838
|
+
document.removeEventListener("visibilitychange", onSyncVisibilityResume);
|
|
6839
|
+
window.removeEventListener("pageshow", onSyncPageShow);
|
|
6479
6840
|
completeAcksById.clear();
|
|
6480
6841
|
busyById.clear();
|
|
6481
6842
|
busyRequestedIds.clear();
|
|
6482
6843
|
busyRetryAtById.clear();
|
|
6483
6844
|
if (clockTimer !== null) clearInterval(clockTimer);
|
|
6484
6845
|
if (recentTimeTimer !== null) clearInterval(recentTimeTimer);
|
|
6846
|
+
if (syncThrottleTimer !== null) clearTimeout(syncThrottleTimer);
|
|
6847
|
+
// 流式派生合并窗口:逐 detail 清理在途 timer(T-127)。
|
|
6848
|
+
for (const detail of sessionDetailsById.values()) {
|
|
6849
|
+
if (detail.logDeriveTimer) clearTimeout(detail.logDeriveTimer);
|
|
6850
|
+
}
|
|
6485
6851
|
if (e2eListReleaseTimer !== null) clearTimeout(e2eListReleaseTimer);
|
|
6486
6852
|
for (const [timer, resolve] of e2eModelDelayWaiters) {
|
|
6487
6853
|
clearTimeout(timer);
|