dsh-activity-pane 0.8.0 → 0.10.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 +551 -223
- package/README.md +9 -0
- package/README.zh-CN.md +9 -0
- package/package.json +1 -1
- package/scripts/acceptance.mjs +18 -6
- package/scripts/check.mjs +330 -116
- package/src/client.mjs +408 -68
- package/src/core.mjs +143 -155
- package/src/host.mjs +46 -12
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
|
|
|
@@ -1985,58 +1984,30 @@ function durationTime(value) {
|
|
|
1985
1984
|
return Number.isFinite(time) ? time : null;
|
|
1986
1985
|
}
|
|
1987
1986
|
|
|
1988
|
-
/**
|
|
1989
|
-
|
|
1990
|
-
|
|
1987
|
+
/**
|
|
1988
|
+
* 从 history 重放回合/等待边界事件,提取最近一个已结束回合的运行过程耗时(busy 口径:
|
|
1989
|
+
* 起止差值扣除回合内阻塞等待,与 R-01-020 累计口径一致,恒不大于该回合墙钟时长)。
|
|
1990
|
+
* 回合起止不完整、时间逆序或回合运行段不为正时忽略该回合;无可得回合返回 null。
|
|
1991
|
+
*/
|
|
1992
|
+
function lastTurnBusyFromEvents(events) {
|
|
1993
|
+
let state = emptyTurnStats();
|
|
1991
1994
|
let latest = null;
|
|
1992
1995
|
for (const entry of Array.isArray(events) ? events : []) {
|
|
1993
|
-
const
|
|
1994
|
-
const
|
|
1995
|
-
|
|
1996
|
-
if (
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
}
|
|
2001
|
-
if (event.type !== "turn/end") continue;
|
|
2002
|
-
const start = starts.get(turn);
|
|
2003
|
-
if (start === undefined || time < start) continue;
|
|
2004
|
-
if (latest === null || time > latest.end) latest = { end: time, duration: time - start };
|
|
1996
|
+
const time = durationTime(eventOf(entry)?.time);
|
|
1997
|
+
const prev = state;
|
|
1998
|
+
state = applyTurnEventToStats(state, entry);
|
|
1999
|
+
if (prev.openTurnStart === null || state.openTurnStart !== null) continue;
|
|
2000
|
+
// 能走到回合闭合的必为时刻有效的 turn/end(applyTurnEventToStats 对无效时刻无效果)。
|
|
2001
|
+
const activeMs = (state.busyMs ?? 0) - (prev.busyMs ?? 0);
|
|
2002
|
+
if (activeMs <= 0) continue;
|
|
2003
|
+
latest = { end: time, duration: activeMs };
|
|
2005
2004
|
}
|
|
2006
|
-
return latest;
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
/** 从 turnTimings 提取最近完整回合的结束时刻与固定耗时;两者始终来自同一回合。 */
|
|
2010
|
-
function lastTurnDurationCandidateFromTimings(turnTimings) {
|
|
2011
|
-
if (!(turnTimings instanceof Map)) return null;
|
|
2012
|
-
let latest = null;
|
|
2013
|
-
for (const timing of turnTimings.values()) {
|
|
2014
|
-
const start = durationTime(timing?.startTime);
|
|
2015
|
-
const end = durationTime(timing?.endTime);
|
|
2016
|
-
if (start === null || end === null || end < start) continue;
|
|
2017
|
-
if (latest === null || end > latest.end) latest = { end, duration: end - start };
|
|
2018
|
-
}
|
|
2019
|
-
return latest;
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
/** 从 history 提取最近完整回合的固定耗时;回合起止不完整或逆序时忽略该回合。 */
|
|
2023
|
-
function lastTurnDurationFromEvents(events) {
|
|
2024
|
-
return lastTurnDurationCandidateFromEvents(events)?.duration ?? null;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
/** 从 turnTimings 提取最近完整回合的固定耗时;全部回合未结束或无效时返回 null。 */
|
|
2028
|
-
function lastTurnDurationFromTimings(turnTimings) {
|
|
2029
|
-
return lastTurnDurationCandidateFromTimings(turnTimings)?.duration ?? null;
|
|
2005
|
+
return latest?.duration ?? null;
|
|
2030
2006
|
}
|
|
2031
2007
|
|
|
2032
|
-
/**
|
|
2033
|
-
function lastTurnDuration({
|
|
2034
|
-
|
|
2035
|
-
let latest = null;
|
|
2036
|
-
for (const candidate of candidates) {
|
|
2037
|
-
if (latest === null || candidate.end > latest.end) latest = candidate;
|
|
2038
|
-
}
|
|
2039
|
-
return latest?.duration ?? null;
|
|
2008
|
+
/** 最近完整回合的固定耗时(busy 口径):从 history 事件重放派生。 */
|
|
2009
|
+
function lastTurnDuration({ history = [] } = {}) {
|
|
2010
|
+
return lastTurnBusyFromEvents(history);
|
|
2040
2011
|
}
|
|
2041
2012
|
|
|
2042
2013
|
/** ask_user_question 工具名:提问/计划审查等待的开启边界(tool/call)与配对结算(tool/result)。 */
|
|
@@ -2052,7 +2023,7 @@ function isBusyBoundaryEvent(type) {
|
|
|
2052
2023
|
|
|
2053
2024
|
/** 空记账状态(无任何有效计时)。 */
|
|
2054
2025
|
function emptyTurnStats() {
|
|
2055
|
-
return { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null };
|
|
2026
|
+
return { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null, watermarkTime: null };
|
|
2056
2027
|
}
|
|
2057
2028
|
|
|
2058
2029
|
/** 从任意来源拷贝记账状态:缺失字段归一为 null(旧记录/部分记录兼容)。 */
|
|
@@ -2064,10 +2035,14 @@ function turnStatsFrom(state) {
|
|
|
2064
2035
|
openWaitKind: state?.openWaitKind ?? null,
|
|
2065
2036
|
openWaitId: state?.openWaitId ?? null,
|
|
2066
2037
|
waitedMs: state?.waitedMs ?? null,
|
|
2038
|
+
watermarkTime: state?.watermarkTime ?? null,
|
|
2067
2039
|
};
|
|
2068
2040
|
}
|
|
2069
2041
|
|
|
2070
|
-
/**
|
|
2042
|
+
/**
|
|
2043
|
+
* 记账状态逐字段相等(不含 watermarkSeq——它不是状态字段,由宿主侧随写入一并落盘;
|
|
2044
|
+
* watermarkTime 为水位处已覆盖事件的时刻,参与相等判定以让补写该字段的收敛写入生效)。
|
|
2045
|
+
*/
|
|
2071
2046
|
function turnStatsEqual(a, b) {
|
|
2072
2047
|
return (
|
|
2073
2048
|
a === b ||
|
|
@@ -2076,7 +2051,8 @@ function turnStatsEqual(a, b) {
|
|
|
2076
2051
|
(a?.openWaitStart ?? null) === (b?.openWaitStart ?? null) &&
|
|
2077
2052
|
(a?.openWaitKind ?? null) === (b?.openWaitKind ?? null) &&
|
|
2078
2053
|
(a?.openWaitId ?? null) === (b?.openWaitId ?? null) &&
|
|
2079
|
-
(a?.waitedMs ?? null) === (b?.waitedMs ?? null)
|
|
2054
|
+
(a?.waitedMs ?? null) === (b?.waitedMs ?? null) &&
|
|
2055
|
+
(a?.watermarkTime ?? null) === (b?.watermarkTime ?? null))
|
|
2080
2056
|
);
|
|
2081
2057
|
}
|
|
2082
2058
|
|
|
@@ -2198,15 +2174,18 @@ function totalBusyDisplayMs({ busyMs = null, openTurnStart = null, waitedMs = nu
|
|
|
2198
2174
|
|
|
2199
2175
|
/**
|
|
2200
2176
|
* 回合统计记账的统一收敛(R-01-020/AC-04、AC-05):对全会话事件列表重放出下一份
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2177
|
+
* 记账,宿主侧懒回填与启动扫描共用。路径选择——无记录、强制重放、持久化水位非法、
|
|
2178
|
+
* 或水位超前于日志最大 seq(事件 seq 空间被重编,如 dsh 0.1.5 V3 迁移)时从空状态
|
|
2179
|
+
* 全量重放(超前水位会把后续全部实时事件封死在守卫之外,openTurnStart 永不清空、
|
|
2180
|
+
* 总耗时无限增长);否则从持久化记账出发仅增量应用 `seq > watermarkSeq` 的事件。
|
|
2181
|
+
* 水位推进处同步记录 `watermarkTime`(水位处已覆盖事件的时刻,与实时登记按效果事件
|
|
2182
|
+
* 写入的口径互为保守——检测只要求该值不超过真实边界事件时刻)——宿主实时登记据此
|
|
2183
|
+
* 识别 seq 空间重编(低 seq 事件携带比水位更新的时刻,增量口径已失效)。
|
|
2205
2184
|
* `closeOpenTurn`(宿主启动扫描):重放后仍存在的开放回合按日志最后事件时刻强制
|
|
2206
2185
|
* 结算关闭——宿主重启后不存在仍在运行的回合,残留起点只会令总耗时无限增长;尾部
|
|
2207
2186
|
* 未配对等待一并按同刻结算(不落到运行时长里)。
|
|
2208
2187
|
*/
|
|
2209
|
-
function reconcileTurnStats(current, records, { closeOpenTurn = false } = {}) {
|
|
2188
|
+
function reconcileTurnStats(current, records, { closeOpenTurn = false, forceFresh = false } = {}) {
|
|
2210
2189
|
const list = Array.isArray(records) ? records : [];
|
|
2211
2190
|
let maxSeq = null;
|
|
2212
2191
|
let lastTime = null;
|
|
@@ -2216,23 +2195,32 @@ function reconcileTurnStats(current, records, { closeOpenTurn = false } = {}) {
|
|
|
2216
2195
|
const time = durationTime(eventOf(record)?.time);
|
|
2217
2196
|
if (time !== null && (lastTime === null || time > lastTime)) lastTime = time;
|
|
2218
2197
|
}
|
|
2219
|
-
const fresh = !isRecord(current) || !Number.isFinite(current.watermarkSeq) || (maxSeq !== null && Number(current.watermarkSeq) > maxSeq);
|
|
2198
|
+
const fresh = forceFresh || !isRecord(current) || !Number.isFinite(current.watermarkSeq) || (maxSeq !== null && Number(current.watermarkSeq) > maxSeq);
|
|
2220
2199
|
let state = emptyTurnStats();
|
|
2221
2200
|
let watermarkSeq = null;
|
|
2201
|
+
let watermarkTime = null;
|
|
2202
|
+
// 水位推进:seq 更大即前推水位;事件时刻有效才更新 watermarkTime(无效不前推)。
|
|
2203
|
+
const advance = (record) => {
|
|
2204
|
+
const seq = Number(record?.seq);
|
|
2205
|
+
if (!Number.isFinite(seq) || (watermarkSeq !== null && seq <= watermarkSeq)) return;
|
|
2206
|
+
watermarkSeq = seq;
|
|
2207
|
+
const time = durationTime(eventOf(record)?.time);
|
|
2208
|
+
if (time !== null) watermarkTime = time;
|
|
2209
|
+
};
|
|
2222
2210
|
if (fresh) {
|
|
2223
2211
|
for (const record of list) {
|
|
2224
2212
|
state = applyTurnEventToStats(state, record);
|
|
2225
|
-
|
|
2226
|
-
if (Number.isFinite(seq) && (watermarkSeq === null || seq > watermarkSeq)) watermarkSeq = seq;
|
|
2213
|
+
advance(record);
|
|
2227
2214
|
}
|
|
2228
2215
|
} else {
|
|
2229
2216
|
state = turnStatsFrom(current);
|
|
2217
|
+
watermarkTime = state.watermarkTime;
|
|
2230
2218
|
watermarkSeq = Number(current.watermarkSeq);
|
|
2231
2219
|
for (const record of list) {
|
|
2232
2220
|
const seq = Number(record?.seq);
|
|
2233
2221
|
if (!Number.isFinite(seq) || seq <= watermarkSeq) continue;
|
|
2234
2222
|
state = applyTurnEventToStats(state, record);
|
|
2235
|
-
|
|
2223
|
+
advance(record);
|
|
2236
2224
|
}
|
|
2237
2225
|
}
|
|
2238
2226
|
if (closeOpenTurn && state.openTurnStart !== null) {
|
|
@@ -2245,7 +2233,7 @@ function reconcileTurnStats(current, records, { closeOpenTurn = false } = {}) {
|
|
|
2245
2233
|
state.openWaitId = null;
|
|
2246
2234
|
state.waitedMs = null;
|
|
2247
2235
|
}
|
|
2248
|
-
return { ...state, watermarkSeq };
|
|
2236
|
+
return { ...state, watermarkSeq, watermarkTime };
|
|
2249
2237
|
}
|
|
2250
2238
|
|
|
2251
2239
|
/**
|
|
@@ -2625,6 +2613,10 @@ const STYLE_ID = "dsh-activity-pane-style";
|
|
|
2625
2613
|
const INSTANCE_KEY = "__dshActivityPaneCleanup";
|
|
2626
2614
|
/** 拖拽调宽的 localStorage 持久化键(R-01-015/AC-04)。 */
|
|
2627
2615
|
const WIDTH_STORAGE_KEY = "dsh-activity-pane:width";
|
|
2616
|
+
/** 卡片紧凑显示的 localStorage 持久化键(R-01-021/AC-06)。 */
|
|
2617
|
+
const DENSITY_STORAGE_KEY = "dsh-activity-pane:density";
|
|
2618
|
+
/** 三档显示的可访问名称用中文标签,键为档位值(R-01-021/AC-01)。 */
|
|
2619
|
+
const DENSITY_LABELS = { full: "完整", medium: "中间", compact: "紧凑" };
|
|
2628
2620
|
const COLLAPSED_WIDTH = 34;
|
|
2629
2621
|
/** 宿主侧完成确认 API 前缀(C-030):acks 快照 / SSE 推送 / ack 写回,同源受信。 */
|
|
2630
2622
|
const PANE_API_BASE = "/dsh-activity-pane/api";
|
|
@@ -2635,6 +2627,10 @@ const INDENT_PX = 16;
|
|
|
2635
2627
|
const MOBILE_BREAKPOINT = "767px";
|
|
2636
2628
|
/** 运行卡时钟:只要存在运行中会话,就以该周期刷新时长显示。 */
|
|
2637
2629
|
const CLOCK_MS = 1000;
|
|
2630
|
+
/** 渲染与流式派生的最小合并间隔:事件密集(流式输出)时把渲染/派生频率硬顶在
|
|
2631
|
+
* 10Hz——事件率随宿主流式 chunk 数增长,显示粒度(秒级时长、块级时间线)无感,
|
|
2632
|
+
* 而渲染与 O(日志窗口) 派生不再随刷新率(移动端 120Hz)与事件率线性放大(T-127)。 */
|
|
2633
|
+
const SYNC_MIN_INTERVAL_MS = 100;
|
|
2638
2634
|
/** 历史卡相对时间刷新周期;无需每秒重绘整列。 */
|
|
2639
2635
|
const RECENT_TIME_REFRESH_MS = 60_000;
|
|
2640
2636
|
/** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
|
|
@@ -2664,25 +2660,27 @@ const CSS = `
|
|
|
2664
2660
|
[data-dsh-activity-pane] .dap-header {
|
|
2665
2661
|
display: flex;
|
|
2666
2662
|
align-items: center;
|
|
2667
|
-
gap: 8px;
|
|
2668
|
-
padding: 10px 12px;
|
|
2669
2663
|
font-size: 12px;
|
|
2670
2664
|
font-weight: 700;
|
|
2671
2665
|
letter-spacing: 0.02em;
|
|
2672
2666
|
}
|
|
2673
|
-
/*
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
[data-dsh-activity-pane] .dap-
|
|
2677
|
-
|
|
2667
|
+
/* 标题行拆为两部分:左侧标题区(flex:1 占满剩余宽度)整体即收起控件,悬停/聚焦
|
|
2668
|
+
高亮只覆盖标题区(R-01-011/AC-03、AC-07、R-01-008/AC-02);右侧工具区独立,
|
|
2669
|
+
不参与标题区的悬停高亮与折叠激活。 */
|
|
2670
|
+
[data-dsh-activity-pane] .dap-titlebar {
|
|
2671
|
+
flex: 1;
|
|
2672
|
+
min-width: 0;
|
|
2673
|
+
display: flex;
|
|
2674
|
+
align-items: center;
|
|
2675
|
+
gap: 8px;
|
|
2676
|
+
padding: 10px 8px 10px 12px;
|
|
2677
|
+
cursor: pointer;
|
|
2678
2678
|
}
|
|
2679
|
-
[data-dsh-activity-pane] .dap-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
margin-left: auto;
|
|
2683
|
-
color: color-mix(in srgb, currentColor 45%, transparent);
|
|
2684
|
-
font-size: 13px;
|
|
2679
|
+
[data-dsh-activity-pane] .dap-titlebar:hover,
|
|
2680
|
+
[data-dsh-activity-pane] .dap-titlebar:focus-visible {
|
|
2681
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
2685
2682
|
}
|
|
2683
|
+
[data-dsh-activity-pane] .dap-titlebar:focus-visible { outline: none; }
|
|
2686
2684
|
[data-dsh-activity-pane] .dap-count {
|
|
2687
2685
|
flex: none;
|
|
2688
2686
|
font-size: 10px;
|
|
@@ -2753,13 +2751,13 @@ const CSS = `
|
|
|
2753
2751
|
scrollbar-color: var(--dsh-scrollbar-thumb, color-mix(in srgb, currentColor 25%, transparent)) transparent;
|
|
2754
2752
|
}
|
|
2755
2753
|
}
|
|
2756
|
-
/*
|
|
2757
|
-
默认 hidden,scrollTop
|
|
2754
|
+
/* 「回到顶部」(R-01-018)悬浮图标按钮:右缘对齐、28px 圆形、不透明底色。
|
|
2755
|
+
默认 hidden,scrollTop 超阈值时由滚动监听揭隐;基类 display:flex 会压过 UA 的
|
|
2758
2756
|
[hidden] 规则,故显式补 [hidden] 隐藏。 */
|
|
2759
2757
|
[data-dsh-activity-pane] .dap-top {
|
|
2760
2758
|
position: absolute;
|
|
2761
|
-
bottom: 12px;
|
|
2762
2759
|
right: 12px;
|
|
2760
|
+
bottom: 12px;
|
|
2763
2761
|
z-index: 6;
|
|
2764
2762
|
display: flex;
|
|
2765
2763
|
align-items: center;
|
|
@@ -2778,6 +2776,109 @@ const CSS = `
|
|
|
2778
2776
|
[data-dsh-activity-pane] .dap-top:focus-visible {
|
|
2779
2777
|
background: #262932;
|
|
2780
2778
|
}
|
|
2779
|
+
/* 标题行右侧工具按钮区(R-01-021/AC-05):固定于行尾、常显档位切换按钮,未来新
|
|
2780
|
+
工具按钮统一加入此区;宽度不随悬停态变化,收起方向图标的显隐不挤动本区按钮;
|
|
2781
|
+
左缘 padding 12px 与标题区保持充分间隔。收起方向图标(R-01-011/AC-07)是标题区
|
|
2782
|
+
的行尾提示:常态宽度为 0 不占位,鼠标悬停或键盘聚焦标题区时在标题区最右端即时
|
|
2783
|
+
显现(无过渡动画),离开即隐藏。 */
|
|
2784
|
+
[data-dsh-activity-pane] .dap-tools {
|
|
2785
|
+
display: flex;
|
|
2786
|
+
align-items: center;
|
|
2787
|
+
gap: 4px;
|
|
2788
|
+
padding: 10px 12px;
|
|
2789
|
+
}
|
|
2790
|
+
[data-dsh-activity-pane] .dap-density {
|
|
2791
|
+
display: flex;
|
|
2792
|
+
align-items: center;
|
|
2793
|
+
justify-content: center;
|
|
2794
|
+
width: 22px;
|
|
2795
|
+
height: 22px;
|
|
2796
|
+
padding: 0;
|
|
2797
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
2798
|
+
border-radius: 999px;
|
|
2799
|
+
background: #1d1f25;
|
|
2800
|
+
color: inherit;
|
|
2801
|
+
cursor: pointer;
|
|
2802
|
+
}
|
|
2803
|
+
[data-dsh-activity-pane] .dap-density:hover,
|
|
2804
|
+
[data-dsh-activity-pane] .dap-density:focus-visible {
|
|
2805
|
+
background: #262932;
|
|
2806
|
+
}
|
|
2807
|
+
/* 仓库入口无描边(T-139):视觉强度弱于带描边的档位切换按钮,仅以不透明底色圆形呈现。 */
|
|
2808
|
+
[data-dsh-activity-pane] .dap-repo {
|
|
2809
|
+
display: flex;
|
|
2810
|
+
align-items: center;
|
|
2811
|
+
justify-content: center;
|
|
2812
|
+
width: 22px;
|
|
2813
|
+
height: 22px;
|
|
2814
|
+
border-radius: 999px;
|
|
2815
|
+
background: #1d1f25;
|
|
2816
|
+
color: inherit;
|
|
2817
|
+
cursor: pointer;
|
|
2818
|
+
text-decoration: none;
|
|
2819
|
+
}
|
|
2820
|
+
[data-dsh-activity-pane] .dap-repo:hover,
|
|
2821
|
+
[data-dsh-activity-pane] .dap-repo:focus-visible {
|
|
2822
|
+
background: #262932;
|
|
2823
|
+
}
|
|
2824
|
+
[data-dsh-activity-pane] .dap-collapse-hint {
|
|
2825
|
+
margin-left: auto;
|
|
2826
|
+
display: flex;
|
|
2827
|
+
align-items: center;
|
|
2828
|
+
justify-content: center;
|
|
2829
|
+
width: 0;
|
|
2830
|
+
height: 22px;
|
|
2831
|
+
opacity: 0;
|
|
2832
|
+
overflow: hidden;
|
|
2833
|
+
}
|
|
2834
|
+
[data-dsh-activity-pane] .dap-titlebar:hover .dap-collapse-hint,
|
|
2835
|
+
[data-dsh-activity-pane] .dap-titlebar:focus-visible .dap-collapse-hint {
|
|
2836
|
+
width: 22px;
|
|
2837
|
+
opacity: 1;
|
|
2838
|
+
}
|
|
2839
|
+
/* 卡片显示档位(R-01-021):中间档保留标题行、工作区徽标行、等待末行与最近卡
|
|
2840
|
+
消息预览行、经渲染层 lastOnly 单行渲染时间线(仅最新一行,AC-08);完成提醒卡
|
|
2841
|
+
末行在中间档收合为单行(见下方 data-wait="done" 作用域规则);紧凑档在
|
|
2842
|
+
中间档基础上再隐藏工作区徽标行、时间线末行、等待末行与消息预览行,仅保留
|
|
2843
|
+
标题行(AC-02)——激活跳转逻辑不感知档位,渲染签名含显示档位分量(档位切换
|
|
2844
|
+
经 queueSync 触发一轮重渲染)(R-01-021/AC-04)。 */
|
|
2845
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card :is(
|
|
2846
|
+
.dap-progress,
|
|
2847
|
+
.dap-token-stats
|
|
2848
|
+
) {
|
|
2849
|
+
display: none;
|
|
2850
|
+
}
|
|
2851
|
+
[data-dsh-activity-pane][data-density="compact"] .dap-card :is(
|
|
2852
|
+
.dap-card-head,
|
|
2853
|
+
.dap-trace,
|
|
2854
|
+
.dap-subtrace,
|
|
2855
|
+
.dap-progress,
|
|
2856
|
+
.dap-token-stats,
|
|
2857
|
+
.dap-foot,
|
|
2858
|
+
.dap-history-line,
|
|
2859
|
+
.dap-note
|
|
2860
|
+
) {
|
|
2861
|
+
display: none;
|
|
2862
|
+
}
|
|
2863
|
+
/* 中间档完成提醒卡末行收合为单行(R-01-021/AC-08,东家反馈):末行两行内容
|
|
2864
|
+
(「已完成」胶囊行 +「继续对话,或移入历史」与「移入历史」按钮行)冗余,收合为
|
|
2865
|
+
单行——「已完成」胶囊居左、按钮 margin-left:auto 居右、正文不再显示;两段包裹层
|
|
2866
|
+
以 display: contents 释放为同行 flex 项。选择器以卡片根 data-wait="done" 作用域,
|
|
2867
|
+
阻塞/错误提醒卡与完整呈现档的两行结构不受影响,紧凑档仍整体隐藏末行。 */
|
|
2868
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card[data-kind="awaiting"][data-wait="done"] .dap-foot {
|
|
2869
|
+
flex-direction: row;
|
|
2870
|
+
align-items: center;
|
|
2871
|
+
gap: 6px;
|
|
2872
|
+
}
|
|
2873
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card[data-kind="awaiting"][data-wait="done"] :is(.dap-await-head, .dap-note-row) {
|
|
2874
|
+
display: contents;
|
|
2875
|
+
}
|
|
2876
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card[data-kind="awaiting"][data-wait="done"] .dap-note {
|
|
2877
|
+
display: none;
|
|
2878
|
+
}
|
|
2879
|
+
[data-dsh-activity-pane][data-density="medium"] .dap-card[data-kind="awaiting"][data-wait="done"] .dap-confirm {
|
|
2880
|
+
margin-left: auto;
|
|
2881
|
+
}
|
|
2781
2882
|
[data-dsh-activity-pane] .dap-list {
|
|
2782
2883
|
display: flex;
|
|
2783
2884
|
flex-direction: column;
|
|
@@ -3035,9 +3136,11 @@ const CSS = `
|
|
|
3035
3136
|
box-shadow: none;
|
|
3036
3137
|
animation: none;
|
|
3037
3138
|
}
|
|
3139
|
+
/* 会话卡标题(活动卡、子代理卡与最近卡共用 .dap-title)统一常规字重,不加粗
|
|
3140
|
+
(R-01-013/AC-09 及东家 2026-09-11 视觉反馈);无按卡类的字重覆盖。 */
|
|
3038
3141
|
[data-dsh-activity-pane] .dap-title {
|
|
3039
3142
|
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
3040
|
-
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight:
|
|
3143
|
+
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight: 400;
|
|
3041
3144
|
}
|
|
3042
3145
|
/* 标题行最右侧的累计运行时长(R-01-020/AC-01):固定占位不参与标题挤压,
|
|
3043
3146
|
标题过长时以自身省略号让位;色调弱于标题,不与状态点抢视觉。 */
|
|
@@ -3046,10 +3149,6 @@ const CSS = `
|
|
|
3046
3149
|
color: #8a94a3; white-space: nowrap;
|
|
3047
3150
|
}
|
|
3048
3151
|
[data-dsh-activity-pane] .dap-total-time[hidden] { display: none; }
|
|
3049
|
-
/* 最近历史卡标题降为常规字重:历史区不抢占视觉强调(R-01-013/AC-09)。 */
|
|
3050
|
-
[data-dsh-activity-pane] .dap-card[data-kind="recent"] .dap-title {
|
|
3051
|
-
font-weight: 400;
|
|
3052
|
-
}
|
|
3053
3152
|
/* 等待卡末行首行「类型胶囊」(R-01-002/AC-01、AC-02、AC-09、AC-13,C-043):圆底类型
|
|
3054
3153
|
图标 + 类型文字,色相随等待类别(--dap-wait-color)——阻塞金/完成绿/错误红;
|
|
3055
3154
|
胶囊为行内元素不自占满宽,随文字内容收缩。 */
|
|
@@ -3341,17 +3440,30 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3341
3440
|
[data-dsh-activity-pane] .dap-fill {
|
|
3342
3441
|
position: absolute; inset: 0 auto 0 0; width: 0%;
|
|
3343
3442
|
border-radius: 6px;
|
|
3344
|
-
|
|
3345
|
-
background-size: 200% 100%;
|
|
3443
|
+
overflow: hidden;
|
|
3346
3444
|
box-shadow: 0 0 7px rgba(88, 201, 143, 0.5);
|
|
3347
3445
|
transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1);
|
|
3348
3446
|
/* 进度条仅存于运行卡骨架:会话运行全程持续向右滚动条纹,作为活动标志
|
|
3349
3447
|
(对齐 answer-pet 的 ap-stripes,R-01-009/AC-08)。 */
|
|
3448
|
+
/* 条带载体 ::after 与滚动动画分离(T-128):background-position 不可合成,
|
|
3449
|
+
每帧重绘;改由伪元素 transform 平移承载滚动,fill 只保留 width 过渡与
|
|
3450
|
+
裁切,滚动帧全程合成器驱动。 */
|
|
3451
|
+
}
|
|
3452
|
+
[data-dsh-activity-pane] .dap-fill::after {
|
|
3453
|
+
content: "";
|
|
3454
|
+
position: absolute;
|
|
3455
|
+
inset: 0 auto 0 0;
|
|
3456
|
+
/* 覆盖 fill 宽度 + 一次位移量:平移全程右缘不落后于 fill 右缘,无缝循环。 */
|
|
3457
|
+
width: calc(100% + 40px);
|
|
3458
|
+
/* 周期 20px(色带 10px)与原 background-position 实现一致——px 色标不受
|
|
3459
|
+
background-size 拉伸(T-128 双轴实测);translateX(-40px) 恰为 2 个周期,
|
|
3460
|
+
无缝且速度 40px/0.8s 与原实现一致。 */
|
|
3461
|
+
background: repeating-linear-gradient(90deg, #58c98f 0 10px, #3fbf86 10px 20px);
|
|
3350
3462
|
animation: dap-stripes 0.8s linear infinite;
|
|
3351
3463
|
}
|
|
3352
3464
|
@keyframes dap-stripes {
|
|
3353
|
-
from {
|
|
3354
|
-
to {
|
|
3465
|
+
from { transform: translateX(-40px); }
|
|
3466
|
+
to { transform: translateX(0); }
|
|
3355
3467
|
}
|
|
3356
3468
|
@media (prefers-reduced-motion: reduce) {
|
|
3357
3469
|
/* answer-pet 保留状态脉冲/进度条纹;仅关闭宽度过渡,避免状态反馈消失。 */
|
|
@@ -3461,6 +3573,11 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3461
3573
|
touch-action: none;
|
|
3462
3574
|
}
|
|
3463
3575
|
[data-dsh-activity-pane][data-open="true"] { transform: translateX(0); }
|
|
3576
|
+
/* 屏外休眠(T-127):抽屉关闭(含初始未开,data-open 缺省视同关闭)时子树整体
|
|
3577
|
+
跳过渲染——无限动画与样式失效不再产生渲染开销;布局状态保留,scrollTop 不归零
|
|
3578
|
+
(区别于 display:none,T-027);打开瞬间恢复渲染,滑入过渡不变。开关与遮罩挂
|
|
3579
|
+
body,不落本规则,浮动开关徽标脉冲(R-01-002/AC-06、AC-07)照常。 */
|
|
3580
|
+
[data-dsh-activity-pane]:not([data-open="true"]) { content-visibility: hidden; }
|
|
3464
3581
|
.dap-backdrop[data-drawer-open] { display: block; }
|
|
3465
3582
|
.dap-toggle { display: flex; }
|
|
3466
3583
|
.dap-toggle[data-drawer-open] { display: none; }
|
|
@@ -3554,13 +3671,23 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-track {
|
|
|
3554
3671
|
body:not([data-ds-dark-theme]) .dap-toggle {
|
|
3555
3672
|
background: var(--dsw-alias-button-floating-fill, rgba(255, 255, 255, 0.94));
|
|
3556
3673
|
}
|
|
3557
|
-
/*
|
|
3558
|
-
|
|
3674
|
+
/* 「回到顶部」与标题行工具区档位/仓库入口按钮的浅色覆盖:不透明层-2 底色与外壳描边别名
|
|
3675
|
+
(R-01-018/AC-05、R-01-021/AC-05、R-01-022/AC-01);仓库入口无描边(T-139),只并入底色组。 */
|
|
3676
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top,
|
|
3677
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density,
|
|
3678
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-repo {
|
|
3559
3679
|
background: var(--dsw-alias-bg-layer-2, #ffffff);
|
|
3680
|
+
}
|
|
3681
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top,
|
|
3682
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density {
|
|
3560
3683
|
border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
3561
3684
|
}
|
|
3562
3685
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top:hover,
|
|
3563
|
-
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top:focus-visible
|
|
3686
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top:focus-visible,
|
|
3687
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density:hover,
|
|
3688
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density:focus-visible,
|
|
3689
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-repo:hover,
|
|
3690
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-repo:focus-visible {
|
|
3564
3691
|
background: var(--dsw-alias-bg-layer-3, #eceef1);
|
|
3565
3692
|
}
|
|
3566
3693
|
`;
|
|
@@ -3636,6 +3763,22 @@ function writeStoredPaneWidth(width) {
|
|
|
3636
3763
|
} catch {}
|
|
3637
3764
|
}
|
|
3638
3765
|
|
|
3766
|
+
/** 读取持久化卡片显示档位:缺失/非法值经 normalizeDensity 归一为默认中间档;
|
|
3767
|
+
* localStorage 不可用(隐私模式)静默回退中间档(R-01-021/AC-06)。 */
|
|
3768
|
+
function readStoredDensity() {
|
|
3769
|
+
try {
|
|
3770
|
+
return normalizeDensity(window.localStorage.getItem(DENSITY_STORAGE_KEY));
|
|
3771
|
+
} catch {
|
|
3772
|
+
return "medium";
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
/** 切换时持久化卡片显示档位;localStorage 不可用时静默跳过(R-01-021/AC-06)。 */
|
|
3776
|
+
function writeStoredDensity(value) {
|
|
3777
|
+
try {
|
|
3778
|
+
window.localStorage.setItem(DENSITY_STORAGE_KEY, value);
|
|
3779
|
+
} catch {}
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3639
3782
|
function apply(ctx) {
|
|
3640
3783
|
const previousCleanup = document[INSTANCE_KEY] ?? globalThis[INSTANCE_KEY];
|
|
3641
3784
|
if (typeof previousCleanup === "function") previousCleanup();
|
|
@@ -3653,6 +3796,10 @@ function apply(ctx) {
|
|
|
3653
3796
|
let clockTimer = null;
|
|
3654
3797
|
let recentTimeTimer = null;
|
|
3655
3798
|
let syncScheduled = false;
|
|
3799
|
+
/** 渲染节流状态(T-127):lastSyncAt 取 0 保证首轮立即渲染;syncThrottleTimer 为
|
|
3800
|
+
* 节流窗口尾的在途 timer,交付或卸载时清空。 */
|
|
3801
|
+
let lastSyncAt = 0;
|
|
3802
|
+
let syncThrottleTimer = null;
|
|
3656
3803
|
let lastSig = "";
|
|
3657
3804
|
/** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
|
|
3658
3805
|
let pulseSignature = "";
|
|
@@ -3687,6 +3834,12 @@ function apply(ctx) {
|
|
|
3687
3834
|
let collapsed = false;
|
|
3688
3835
|
/** 当前桌面列宽:启动时从 localStorage 恢复,拖拽实时更新,重挂载后保留(R-01-015)。 */
|
|
3689
3836
|
let paneWidth = readStoredPaneWidth();
|
|
3837
|
+
/** 卡片显示档位(full/medium/compact):启动时从 localStorage 恢复,切换实时更新,
|
|
3838
|
+
* 重挂载后保留(R-01-021/AC-06)。 */
|
|
3839
|
+
let densityLevel = readStoredDensity();
|
|
3840
|
+
/** 待执行的锚定补偿:档位切换时登记当前选中卡片顶部的视口相对位置,本轮渲染
|
|
3841
|
+
* 提交后量测该卡新位置并补偿 scrollTop(R-01-021/AC-01)。 */
|
|
3842
|
+
let pendingDensityAnchor = null;
|
|
3690
3843
|
/** 用户最近一次激活的卡片 id;打开重试链被更新的激活意图取代即取消。 */
|
|
3691
3844
|
let lastActivatedId = null;
|
|
3692
3845
|
/** 最近一次已处理的当前卡片;同一卡片的运行时重绘不反复打断用户手动滚动。 */
|
|
@@ -3854,13 +4007,23 @@ function apply(ctx) {
|
|
|
3854
4007
|
if (changed) notifyLayoutChange();
|
|
3855
4008
|
}
|
|
3856
4009
|
|
|
4010
|
+
function deliverSync() {
|
|
4011
|
+
syncScheduled = false;
|
|
4012
|
+
syncThrottleTimer = null;
|
|
4013
|
+
if (disposed) return;
|
|
4014
|
+
lastSyncAt = Date.now();
|
|
4015
|
+
render();
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
/** 渲染入口合帧 + 节流(T-127):距上次渲染落点不足 SYNC_MIN_INTERVAL_MS 时,
|
|
4019
|
+
* 本轮请求合并到窗口尾由 timer 交付(在途请求只此一个);达到间隔走原 rAF
|
|
4020
|
+
* 合帧立即渲染。空闲期无事件即无排队,不引入常驻唤醒。 */
|
|
3857
4021
|
function queueSync() {
|
|
3858
4022
|
if (disposed || syncScheduled) return;
|
|
3859
4023
|
syncScheduled = true;
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
});
|
|
4024
|
+
const wait = SYNC_MIN_INTERVAL_MS - (Date.now() - lastSyncAt);
|
|
4025
|
+
if (wait > 0) syncThrottleTimer = setTimeout(deliverSync, wait);
|
|
4026
|
+
else schedule(deliverSync);
|
|
3864
4027
|
}
|
|
3865
4028
|
|
|
3866
4029
|
// ---- 完成确认通道(R-01-002/AC-10~AC-12、R-01-010/AC-06,C-030) ----
|
|
@@ -4050,6 +4213,36 @@ function apply(ctx) {
|
|
|
4050
4213
|
window.addEventListener("pageshow", onBusyPageShow);
|
|
4051
4214
|
connectBusyStream();
|
|
4052
4215
|
|
|
4216
|
+
// ---- 回到前台渲染管线自愈(R-01-001/AC-03 回归修复) ----
|
|
4217
|
+
// iOS 后台挂起会丢弃在途 setTimeout:queueSync 的节流窗口尾与流式派生窗口若在
|
|
4218
|
+
// 挂起前排队,恢复后永不交付——syncScheduled/logDeriveTimer 永久滞留,此后一切
|
|
4219
|
+
// 更新(store 订阅推送、SSE 快照、时钟 tick)都被 queueSync 早退吞掉,窗格停留
|
|
4220
|
+
// 在挂起前状态(会话已完成仍显示运行中)。回前台时无条件清掉两类在途 timer 并
|
|
4221
|
+
// 立即交付一轮渲染(rAF 路径挂起安全),与 acks/busy 通道回前台重建同模式。
|
|
4222
|
+
function resumeRenderPipeline() {
|
|
4223
|
+
if (disposed) return;
|
|
4224
|
+
if (syncThrottleTimer !== null) {
|
|
4225
|
+
clearTimeout(syncThrottleTimer);
|
|
4226
|
+
syncThrottleTimer = null;
|
|
4227
|
+
}
|
|
4228
|
+
syncScheduled = false;
|
|
4229
|
+
for (const detail of sessionDetailsById.values()) {
|
|
4230
|
+
if (detail.logDeriveTimer && detail.logDeriveFlush) {
|
|
4231
|
+
clearTimeout(detail.logDeriveTimer);
|
|
4232
|
+
detail.logDeriveFlush();
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
schedule(deliverSync);
|
|
4236
|
+
}
|
|
4237
|
+
const onSyncVisibilityResume = () => {
|
|
4238
|
+
if (document.visibilityState === "visible") resumeRenderPipeline();
|
|
4239
|
+
};
|
|
4240
|
+
const onSyncPageShow = (event) => {
|
|
4241
|
+
if (event?.persisted === true) resumeRenderPipeline();
|
|
4242
|
+
};
|
|
4243
|
+
document.addEventListener("visibilitychange", onSyncVisibilityResume);
|
|
4244
|
+
window.addEventListener("pageshow", onSyncPageShow);
|
|
4245
|
+
|
|
4053
4246
|
function remoteValue(response) {
|
|
4054
4247
|
if (response?.ok === true) return response.value;
|
|
4055
4248
|
throw response?.error ?? new Error("remote request failed");
|
|
@@ -4188,7 +4381,7 @@ function apply(ctx) {
|
|
|
4188
4381
|
});
|
|
4189
4382
|
}
|
|
4190
4383
|
}
|
|
4191
|
-
captureSessionLog(id, {
|
|
4384
|
+
captureSessionLog(id, { cwd: byId[id]?.cwd ?? "" });
|
|
4192
4385
|
// 长会话深读兜底(R-01-013/AC-03):最近卡预览/子代理溯源不在尾页日志窗口内
|
|
4193
4386
|
// 且宿主标记 hasMore 时,按 beforeSeq 向前回溯翻页(默认无页数上限)。
|
|
4194
4387
|
const windowEntries = Array.isArray(detail.log?.entries) ? detail.log.entries : [];
|
|
@@ -4264,7 +4457,7 @@ function apply(ctx) {
|
|
|
4264
4457
|
/** 绑定会话并水合事件源(dsh 0.1.5 起会话内容经 eventSource 流式下发:打开即收
|
|
4265
4458
|
* 完整日志窗口 + 实时尾,原生 Conversation 同源)。冷会话补一次 open(),日志窗口
|
|
4266
4459
|
* 快照引用变化即重派生详情(时间线/预览/模型),窗口由宿主按消息对齐分页。 */
|
|
4267
|
-
function captureSessionLog(id, {
|
|
4460
|
+
function captureSessionLog(id, { cwd } = {}) {
|
|
4268
4461
|
const detail = sessionDetailsById.get(id) ?? {};
|
|
4269
4462
|
sessionDetailsById.set(id, detail);
|
|
4270
4463
|
let session = null;
|
|
@@ -4285,10 +4478,7 @@ function apply(ctx) {
|
|
|
4285
4478
|
session.eventSource.subscribe(() => {
|
|
4286
4479
|
if (disposed) return;
|
|
4287
4480
|
const listSnap = getSnapshot(sessions, "list");
|
|
4288
|
-
captureSessionLog(id, {
|
|
4289
|
-
subagent: isSubagentRow(listSnap?.byId?.[id], listSnap ?? {}),
|
|
4290
|
-
cwd: listSnap?.byId?.[id]?.cwd ?? "",
|
|
4291
|
-
});
|
|
4481
|
+
captureSessionLog(id, { cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
4292
4482
|
queueSync();
|
|
4293
4483
|
}),
|
|
4294
4484
|
);
|
|
@@ -4314,8 +4504,30 @@ function apply(ctx) {
|
|
|
4314
4504
|
const log = session.eventSource?.getSnapshot?.() ?? null;
|
|
4315
4505
|
if (log === detail.log) return;
|
|
4316
4506
|
detail.log = log;
|
|
4317
|
-
|
|
4318
|
-
|
|
4507
|
+
// 流式派生合并(T-127):事件到达只更新引用并标脏,applyLogEvents 的 O(日志窗口)
|
|
4508
|
+
// 全量折叠/预览/模型提取合并进 SYNC_MIN_INTERVAL_MS 窗口执行——事件率与派生成本
|
|
4509
|
+
// 解耦,消化时读到的即最新窗口。subagent/cwd 在回调内经 list 快照现取(timer 在途
|
|
4510
|
+
// 期间行属性可能突变,建窗入参不代表消化时刻;与 logSourceSubs 回调同模式),
|
|
4511
|
+
// cwd 在快照不可得时回退建窗入参(subagent 现取即权威,无建窗回退)。
|
|
4512
|
+
// 深翻路径为一次性同步调用,不经本窗口。交付体同时挂在 detail.logDeriveFlush
|
|
4513
|
+
// 上:回前台自愈据此补交付被挂起丢弃的窗口(resumeRenderPipeline)。
|
|
4514
|
+
if (!detail.logDeriveTimer && typeof setTimeout === "function") {
|
|
4515
|
+
const flushDerive = () => {
|
|
4516
|
+
detail.logDeriveTimer = null;
|
|
4517
|
+
detail.logDeriveFlush = null;
|
|
4518
|
+
if (disposed) return;
|
|
4519
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
4520
|
+
const entries = Array.isArray(detail.log?.entries) ? detail.log.entries : [];
|
|
4521
|
+
applyLogEvents(id, detail, entries, {
|
|
4522
|
+
subagent: isSubagentRow(listSnap?.byId?.[id], listSnap?.byId ?? {}),
|
|
4523
|
+
cwd: listSnap?.byId?.[id]?.cwd ?? cwd,
|
|
4524
|
+
});
|
|
4525
|
+
queueSync();
|
|
4526
|
+
};
|
|
4527
|
+
detail.logDeriveFlush = flushDerive;
|
|
4528
|
+
detail.logDeriveTimer = setTimeout(flushDerive, SYNC_MIN_INTERVAL_MS);
|
|
4529
|
+
}
|
|
4530
|
+
queueSync();
|
|
4319
4531
|
}
|
|
4320
4532
|
|
|
4321
4533
|
/** 部署级模型目录一次性读取(R-01-012/AC-01):dsh 0.1.5 起 per-session models
|
|
@@ -4393,7 +4605,7 @@ function apply(ctx) {
|
|
|
4393
4605
|
return true;
|
|
4394
4606
|
}
|
|
4395
4607
|
function bindPaneControls(pane) {
|
|
4396
|
-
const header = pane.querySelector(".dap-
|
|
4608
|
+
const header = pane.querySelector(".dap-titlebar");
|
|
4397
4609
|
const rail = pane.querySelector(".dap-rail");
|
|
4398
4610
|
const resize = pane.querySelector(".dap-resize");
|
|
4399
4611
|
const scroll = pane.querySelector(".dap-scroll");
|
|
@@ -4495,6 +4707,37 @@ function apply(ctx) {
|
|
|
4495
4707
|
const onTopClick = () => {
|
|
4496
4708
|
scroll?.scrollTo({ top: 0, behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
4497
4709
|
};
|
|
4710
|
+
// 显示档位循环切换(R-01-021/AC-01):完整→中间→紧凑→完整,形态写窗格根属性
|
|
4711
|
+
// 驱动纯 CSS 呈现,持久化于 localStorage(AC-06),会话状态变化不触碰已选档位(AC-07)。
|
|
4712
|
+
const densityBtn = pane.querySelector(".dap-density");
|
|
4713
|
+
const applyDensity = () => {
|
|
4714
|
+
pane.setAttribute("data-density", densityLevel);
|
|
4715
|
+
if (densityBtn !== null) {
|
|
4716
|
+
const next = nextDensity(densityLevel);
|
|
4717
|
+
densityBtn.setAttribute("aria-label", `切换为${DENSITY_LABELS[next]}显示`);
|
|
4718
|
+
densityBtn.title = `${DENSITY_LABELS[next]}显示`;
|
|
4719
|
+
}
|
|
4720
|
+
};
|
|
4721
|
+
const onDensityClick = () => {
|
|
4722
|
+
// 滚动锚定(R-01-021/AC-01):记录切换前当前选中卡片顶部相对滚动视口的
|
|
4723
|
+
// 位置,档位翻转后把 scrollTop 补偿回该相对位置,使卡片顶部在屏幕上不动。
|
|
4724
|
+
const scrollEl = scroll ?? pane.querySelector(".dap-scroll");
|
|
4725
|
+
const currentCard = scrollEl?.querySelector(".dap-card[data-current]") ?? null;
|
|
4726
|
+
const viewportTop = scrollEl?.getBoundingClientRect().top ?? 0;
|
|
4727
|
+
const anchorTop = currentCard ? currentCard.getBoundingClientRect().top - viewportTop : null;
|
|
4728
|
+
densityLevel = nextDensity(densityLevel);
|
|
4729
|
+
writeStoredDensity(densityLevel);
|
|
4730
|
+
applyDensity();
|
|
4731
|
+
// 档位已纳入渲染签名:触发一轮同步让时间线按新档位以 lastOnly 重建;
|
|
4732
|
+
// 锚定补偿在本轮渲染提交后执行,此时量测才含新行高
|
|
4733
|
+
//(R-01-021/AC-01、AC-08)。
|
|
4734
|
+
pendingDensityAnchor = currentCard && anchorTop !== null ? anchorTop : null;
|
|
4735
|
+
queueSync();
|
|
4736
|
+
};
|
|
4737
|
+
// 档位按钮位于标题行右侧的工具区(标题区折叠控件的兄弟节点),激活天然不会
|
|
4738
|
+
// 冒泡为标题区折叠,无需额外阻断。
|
|
4739
|
+
densityBtn?.addEventListener("click", onDensityClick);
|
|
4740
|
+
applyDensity();
|
|
4498
4741
|
header?.addEventListener("click", onHeaderActivate);
|
|
4499
4742
|
header?.addEventListener("keydown", onHeaderKeydown);
|
|
4500
4743
|
rail?.addEventListener("click", onRailClick);
|
|
@@ -4515,6 +4758,7 @@ function apply(ctx) {
|
|
|
4515
4758
|
recentMore?.removeEventListener("click", onRecentMoreClick);
|
|
4516
4759
|
if (scrollHideTimer !== null) clearTimeout(scrollHideTimer);
|
|
4517
4760
|
topBtn?.removeEventListener("click", onTopClick);
|
|
4761
|
+
densityBtn?.removeEventListener("click", onDensityClick);
|
|
4518
4762
|
resize?.removeEventListener("pointerdown", onResizeDown);
|
|
4519
4763
|
};
|
|
4520
4764
|
}
|
|
@@ -4534,10 +4778,16 @@ function apply(ctx) {
|
|
|
4534
4778
|
pane.className = PANE_CLASS;
|
|
4535
4779
|
center.insertBefore(pane, seat);
|
|
4536
4780
|
pane.innerHTML = `
|
|
4537
|
-
<div class="dap-header"
|
|
4538
|
-
<span
|
|
4539
|
-
|
|
4540
|
-
|
|
4781
|
+
<div class="dap-header">
|
|
4782
|
+
<span class="dap-titlebar" role="button" tabindex="0" aria-expanded="true" aria-label="收起活动会话窗格" title="收起">
|
|
4783
|
+
<span>活动会话</span>
|
|
4784
|
+
<span class="dap-count" role="status" aria-live="polite"></span>
|
|
4785
|
+
<span class="dap-collapse-hint" aria-hidden="true"></span>
|
|
4786
|
+
</span>
|
|
4787
|
+
<span class="dap-tools">
|
|
4788
|
+
<a class="dap-repo" href="https://github.com/ccll/dsh-activity-pane" target="_blank" rel="noreferrer noopener" aria-label="报告问题" title="报告问题"></a>
|
|
4789
|
+
<button class="dap-density" type="button" aria-label="切换为紧凑显示" title="紧凑显示"></button>
|
|
4790
|
+
</span>
|
|
4541
4791
|
</div>
|
|
4542
4792
|
<div class="dap-scroll">
|
|
4543
4793
|
<div class="dap-list" tabindex="-1"><div class="dap-tracks" aria-hidden="true"></div></div>
|
|
@@ -4556,6 +4806,13 @@ function apply(ctx) {
|
|
|
4556
4806
|
pane.style.setProperty("--dap-width", `${paneWidth}px`);
|
|
4557
4807
|
// 「回到顶部」按钮为纯图标呈现(R-01-018/AC-05):骨架无文字,图标在创建时注入。
|
|
4558
4808
|
pane.querySelector(".dap-top").append(createTopIcon());
|
|
4809
|
+
// 紧凑显示切换按钮常显于标题行右侧工具区(R-01-021/AC-05);切换时以 data-density
|
|
4810
|
+
// 驱动纯 CSS 呈现,骨架重建后由 bindPaneControls 的 applyDensity 恢复形态。
|
|
4811
|
+
pane.querySelector(".dap-density").append(createDensityIcon());
|
|
4812
|
+
// 仓库入口常显于标题行右侧工具区(R-01-022/AC-01);图标在创建时注入。
|
|
4813
|
+
pane.querySelector(".dap-repo").append(createRepoIcon());
|
|
4814
|
+
// 收起方向图标为标题行悬停/聚焦的可见性提示(R-01-011/AC-07),图标在创建时注入。
|
|
4815
|
+
pane.querySelector(".dap-collapse-hint").append(createCollapseIcon());
|
|
4559
4816
|
}
|
|
4560
4817
|
if (pane !== boundPane) {
|
|
4561
4818
|
unbindPaneControls?.();
|
|
@@ -4728,6 +4985,50 @@ function apply(ctx) {
|
|
|
4728
4985
|
});
|
|
4729
4986
|
}
|
|
4730
4987
|
|
|
4988
|
+
/** 紧凑显示切换按钮的「多行收拢为单行」密度图标:上下两个指向中线的箭头夹一条
|
|
4989
|
+
* 标题行横线,与「回到顶部」的单向箭头、标题行的收起方向图标均可区分
|
|
4990
|
+
* (canonical 图标集无现成字形,14 盒 stroke 风格与 createTopIcon 一致,R-01-021/AC-05)。 */
|
|
4991
|
+
function createDensityIcon() {
|
|
4992
|
+
return createInlineIcon({
|
|
4993
|
+
viewBox: "0 0 14 14",
|
|
4994
|
+
width: 14,
|
|
4995
|
+
height: 14,
|
|
4996
|
+
parts: [
|
|
4997
|
+
{ 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" } },
|
|
4998
|
+
{ attrs: { d: "M3 7h8", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
4999
|
+
{ 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" } },
|
|
5000
|
+
],
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
|
|
5004
|
+
/** 标题行的「收起」方向图标(R-01-011/AC-07):左侧竖杠 + 向左箭头指向竖杠,
|
|
5005
|
+
* 表达窗格收缩到左侧;随 .dap-collapse-hint 在标题行悬停/聚焦时显现。 */
|
|
5006
|
+
function createCollapseIcon() {
|
|
5007
|
+
return createInlineIcon({
|
|
5008
|
+
viewBox: "0 0 14 14",
|
|
5009
|
+
width: 14,
|
|
5010
|
+
height: 14,
|
|
5011
|
+
parts: [
|
|
5012
|
+
{ attrs: { d: "M3 2.5v9", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5013
|
+
{ attrs: { d: "M11.5 7H5", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5014
|
+
{ attrs: { d: "M8.25 3.75 5 7l3.25 3.25", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" } },
|
|
5015
|
+
],
|
|
5016
|
+
});
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
/** 标题行工具区的仓库入口图标(R-01-022/AC-01):GitHub Octicon `mark-github` 字形
|
|
5020
|
+
* (MIT 许可,Primer Octicons),与工具区既有图标同用 14px 字形盒。 */
|
|
5021
|
+
function createRepoIcon() {
|
|
5022
|
+
return createInlineIcon({
|
|
5023
|
+
viewBox: "0 0 16 16",
|
|
5024
|
+
width: 14,
|
|
5025
|
+
height: 14,
|
|
5026
|
+
parts: [
|
|
5027
|
+
{ 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" } },
|
|
5028
|
+
],
|
|
5029
|
+
});
|
|
5030
|
+
}
|
|
5031
|
+
|
|
4731
5032
|
function createUserIcon() {
|
|
4732
5033
|
return createInlineIcon({
|
|
4733
5034
|
viewBox: "0 0 16 16",
|
|
@@ -5055,13 +5356,14 @@ function apply(ctx) {
|
|
|
5055
5356
|
}
|
|
5056
5357
|
}
|
|
5057
5358
|
|
|
5058
|
-
/** 最近回合耗时 memo(等待卡与暂停子代理卡共用,R-01-009/AC-12、AC-15
|
|
5059
|
-
|
|
5359
|
+
/** 最近回合耗时 memo(等待卡与暂停子代理卡共用,R-01-009/AC-12、AC-15):history 引用
|
|
5360
|
+
* 不变即命中缓存。busy 口径(起止差值扣除回合内阻塞等待),与标题行总耗时同口径,
|
|
5361
|
+
* 保证恒不大于累计值(R-01-020)。 */
|
|
5362
|
+
function memoTurnDuration(detail) {
|
|
5060
5363
|
const history = detail.history ?? null;
|
|
5061
|
-
if (detail.
|
|
5062
|
-
detail.memoTurnDurationSnapshotOf = detailSnapshot;
|
|
5364
|
+
if (detail.memoTurnDurationHistoryOf !== history) {
|
|
5063
5365
|
detail.memoTurnDurationHistoryOf = history;
|
|
5064
|
-
detail.memoTurnDuration = lastTurnDuration({
|
|
5366
|
+
detail.memoTurnDuration = lastTurnDuration({ history });
|
|
5065
5367
|
}
|
|
5066
5368
|
return detail.memoTurnDuration ?? null;
|
|
5067
5369
|
}
|
|
@@ -5229,7 +5531,7 @@ function apply(ctx) {
|
|
|
5229
5531
|
if (entry.kind === "running") {
|
|
5230
5532
|
renderProgressRow(el, entry.progress);
|
|
5231
5533
|
const traceContainer = el.querySelector(".dap-trace");
|
|
5232
|
-
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
5534
|
+
if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
|
|
5233
5535
|
renderTokenStats(el, entry);
|
|
5234
5536
|
return;
|
|
5235
5537
|
}
|
|
@@ -5273,7 +5575,7 @@ function apply(ctx) {
|
|
|
5273
5575
|
|
|
5274
5576
|
if (entry.kind === "awaiting") {
|
|
5275
5577
|
const traceContainer = el.querySelector(".dap-trace");
|
|
5276
|
-
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
5578
|
+
if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
|
|
5277
5579
|
removeAwaitingHeadDuration(el);
|
|
5278
5580
|
renderTokenStats(el, entry);
|
|
5279
5581
|
const confirm = el.querySelector(".dap-confirm");
|
|
@@ -5338,7 +5640,7 @@ function apply(ctx) {
|
|
|
5338
5640
|
// dsh 0.1.5 起快照不再携带会话内容:时间线经 eventSource 日志窗口
|
|
5339
5641
|
// 就地重派生(R-01-009)。
|
|
5340
5642
|
const listSnap = getSnapshot(sessions, "list");
|
|
5341
|
-
captureSessionLog(id, {
|
|
5643
|
+
captureSessionLog(id, { cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
5342
5644
|
queueSync();
|
|
5343
5645
|
});
|
|
5344
5646
|
} catch {
|
|
@@ -5429,15 +5731,18 @@ function apply(ctx) {
|
|
|
5429
5731
|
}
|
|
5430
5732
|
|
|
5431
5733
|
/** 数量标识内容写入(R-01-014/AC-06):加载态显示活动指示——已是指示则不重写,
|
|
5432
|
-
* 避免每轮 replaceChildren 重启动画抖动;计数态恢复文本写入,textContent 赋值自动摘除指示。
|
|
5734
|
+
* 避免每轮 replaceChildren 重启动画抖动;计数态恢复文本写入,textContent 赋值自动摘除指示。
|
|
5735
|
+
* 计数态同时写入悬停 tips(R-01-001/AC-08):说明分子/分母口径;加载态无计数可解释,摘除。 */
|
|
5433
5736
|
function setCountBadgeContent(el, badge) {
|
|
5434
5737
|
if (badge.mode === "loading") {
|
|
5435
5738
|
const spinner = el.firstElementChild;
|
|
5436
5739
|
if (!(el.childNodes.length === 1 && spinner !== null && spinner.classList.contains("dap-spinner")))
|
|
5437
5740
|
el.replaceChildren(makeEl("span", "dap-spinner"));
|
|
5438
|
-
|
|
5741
|
+
if (el.getAttribute("title") !== null) el.removeAttribute("title");
|
|
5742
|
+
} else {
|
|
5439
5743
|
// 值未变不写文本节点:aria-live 下相同赋值也会触发替换与重复播报。
|
|
5440
|
-
el.textContent = badge.text;
|
|
5744
|
+
if (el.textContent !== badge.text) el.textContent = badge.text;
|
|
5745
|
+
if (el.getAttribute("title") !== badge.tip) el.setAttribute("title", badge.tip);
|
|
5441
5746
|
}
|
|
5442
5747
|
}
|
|
5443
5748
|
|
|
@@ -5966,7 +6271,7 @@ function apply(ctx) {
|
|
|
5966
6271
|
}
|
|
5967
6272
|
}
|
|
5968
6273
|
if (entry.kind === "awaiting" && detail) {
|
|
5969
|
-
entry.elapsedMs = memoTurnDuration(detail
|
|
6274
|
+
entry.elapsedMs = memoTurnDuration(detail);
|
|
5970
6275
|
}
|
|
5971
6276
|
if (detail?.model) {
|
|
5972
6277
|
entry.model = detail.model.model;
|
|
@@ -6038,7 +6343,7 @@ function apply(ctx) {
|
|
|
6038
6343
|
// 非运行(暂停等待):冻结最后已知统计与最近回合耗时,progress 置空隐藏
|
|
6039
6344
|
// 进度条(R-01-009/AC-15);刷新/无留存时回退当前列表投影。
|
|
6040
6345
|
Object.assign(entry, mergeRuntimeStats(detail?.lastRuntimeStats, projectionStats));
|
|
6041
|
-
entry.elapsedMs = detail ? memoTurnDuration(detail
|
|
6346
|
+
entry.elapsedMs = detail ? memoTurnDuration(detail) : null;
|
|
6042
6347
|
entry.progress = null;
|
|
6043
6348
|
}
|
|
6044
6349
|
}
|
|
@@ -6061,13 +6366,13 @@ function apply(ctx) {
|
|
|
6061
6366
|
recentTotal = recentCandidates.length;
|
|
6062
6367
|
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
6063
6368
|
recentHasMore = recent.length < recentTotal;
|
|
6064
|
-
//
|
|
6065
|
-
//
|
|
6369
|
+
// 最近卡统计复用运行卡的列表投影口径;耗时从已读 history 取最近完整回合的运行
|
|
6370
|
+
// 过程时长(busy 口径,与标题行总耗时一致,R-01-013/AC-12),缺边界时仅为当前
|
|
6371
|
+
// 可见历史卡安排一次既有 history 补读。
|
|
6066
6372
|
const recentDurationFallbackIds = new Set();
|
|
6067
6373
|
for (const entry of recent) {
|
|
6068
6374
|
const detail = sessionDetailsById.get(entry.id);
|
|
6069
|
-
const
|
|
6070
|
-
const elapsedMs = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history: detail?.history });
|
|
6375
|
+
const elapsedMs = lastTurnDuration({ history: detail?.history ?? null });
|
|
6071
6376
|
// 累计运行时长(R-01-020/AC-01):最近历史卡同为标题行右侧显示;历史卡无开放回合。
|
|
6072
6377
|
applyTotalBusy(entry, now);
|
|
6073
6378
|
const stats = statsFromProjection(snapshot?.byId?.[entry.id]?.projectionValues, elapsedMs);
|
|
@@ -6145,7 +6450,7 @@ function apply(ctx) {
|
|
|
6145
6450
|
// 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
|
|
6146
6451
|
// 历史卡的相对活动时间随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
|
|
6147
6452
|
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
6148
|
-
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);
|
|
6453
|
+
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, densityLevel]);
|
|
6149
6454
|
if (sig === lastSig) return;
|
|
6150
6455
|
const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
|
|
6151
6456
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
|
@@ -6225,7 +6530,7 @@ function apply(ctx) {
|
|
|
6225
6530
|
ensureCurrentCardVisible(pane.querySelector(".dap-scroll"), snapshot?.current ?? null);
|
|
6226
6531
|
if (focusAfterMigrationId !== null) cardsById.get(focusAfterMigrationId)?.el.focus();
|
|
6227
6532
|
// 区域已有条目但列表仍在途时,在区头部显示行内加载指示(R-01-014/AC-01)。
|
|
6228
|
-
const headerEl = pane.querySelector(".dap-
|
|
6533
|
+
const headerEl = pane.querySelector(".dap-titlebar");
|
|
6229
6534
|
const recentHeadEl = recentSection?.querySelector(".dap-recent-head") ?? null;
|
|
6230
6535
|
for (const [head, hasItems] of [[headerEl, active.length > 0], [recentHeadEl, recent.length > 0]]) {
|
|
6231
6536
|
if (head === null) continue;
|
|
@@ -6237,11 +6542,12 @@ function apply(ctx) {
|
|
|
6237
6542
|
}
|
|
6238
6543
|
}
|
|
6239
6544
|
|
|
6240
|
-
// 计数与折叠:n/m
|
|
6545
|
+
// 计数与折叠:n/m 只统计主会话——分子为运行中数、分母为其加等待行动主会话之和
|
|
6241
6546
|
// (R-01-001/AC-04、AC-05);空态同样显示 0/0(AC-06)。列表在途时不冒充计数,
|
|
6242
|
-
// 三处数量标识显示加载指示(R-01-014/AC-06
|
|
6243
|
-
//
|
|
6244
|
-
//
|
|
6547
|
+
// 三处数量标识显示加载指示(R-01-014/AC-06)。悬停 tips 说明分子/分母口径(AC-08)。
|
|
6548
|
+
// 脉冲由 data-awaiting 承载:任一等待行动(阻塞等待、完成提醒或错误提醒)即脉冲
|
|
6549
|
+
// (R-01-002/AC-06,C-037);底色经 data-tone 跟随等待构成——错误 > 阻塞 > 完成
|
|
6550
|
+
// 优先级取红/金/绿(C-040、C-043)。
|
|
6245
6551
|
const count = pane.querySelector(".dap-count");
|
|
6246
6552
|
const railCount = pane.querySelector(".dap-rail-count");
|
|
6247
6553
|
const { waiting, blocked, total } = awaitBadgeStats(active);
|
|
@@ -6292,6 +6598,21 @@ function apply(ctx) {
|
|
|
6292
6598
|
prevRenderedActiveIds = new Set(active.map((entry) => String(entry.id)));
|
|
6293
6599
|
prevRenderedRecentIds = new Set(recent.map((entry) => String(entry.id)));
|
|
6294
6600
|
}
|
|
6601
|
+
// 锚定补偿(R-01-021/AC-01):档位切换的渲染落地后量测当前选中卡的新位置并
|
|
6602
|
+
// 补偿 scrollTop,使当前卡顶部回到切换前相对视口的位置;补偿量超滚动边界时
|
|
6603
|
+
// 由浏览器钳制(以当前卡不滚出可视范围为准)。
|
|
6604
|
+
if (pendingDensityAnchor !== null) {
|
|
6605
|
+
const scrollEl = pane.querySelector(".dap-scroll");
|
|
6606
|
+
// 补偿按当前 DOM 的 data-current 卡实时定位:跨区迁移等渲染可能重建卡片
|
|
6607
|
+
// 元素,不持有旧引用(量测恒为 attached 节点)。
|
|
6608
|
+
const currentCard = scrollEl?.querySelector(".dap-card[data-current]") ?? null;
|
|
6609
|
+
if (scrollEl !== null && currentCard !== null) {
|
|
6610
|
+
const viewportTop = scrollEl.getBoundingClientRect().top;
|
|
6611
|
+
const shiftedTop = currentCard.getBoundingClientRect().top - viewportTop;
|
|
6612
|
+
scrollEl.scrollTop += shiftedTop - pendingDensityAnchor;
|
|
6613
|
+
}
|
|
6614
|
+
pendingDensityAnchor = null;
|
|
6615
|
+
}
|
|
6295
6616
|
}
|
|
6296
6617
|
|
|
6297
6618
|
// ---- 打开会话(让 sessions.open 自己校验列表,失败时 refresh + 重试) ----
|
|
@@ -6488,12 +6809,19 @@ function apply(ctx) {
|
|
|
6488
6809
|
window.removeEventListener("pageshow", onBusyPageShow);
|
|
6489
6810
|
document.removeEventListener("visibilitychange", onVisibilityResume);
|
|
6490
6811
|
window.removeEventListener("pageshow", onPageShow);
|
|
6812
|
+
document.removeEventListener("visibilitychange", onSyncVisibilityResume);
|
|
6813
|
+
window.removeEventListener("pageshow", onSyncPageShow);
|
|
6491
6814
|
completeAcksById.clear();
|
|
6492
6815
|
busyById.clear();
|
|
6493
6816
|
busyRequestedIds.clear();
|
|
6494
6817
|
busyRetryAtById.clear();
|
|
6495
6818
|
if (clockTimer !== null) clearInterval(clockTimer);
|
|
6496
6819
|
if (recentTimeTimer !== null) clearInterval(recentTimeTimer);
|
|
6820
|
+
if (syncThrottleTimer !== null) clearTimeout(syncThrottleTimer);
|
|
6821
|
+
// 流式派生合并窗口:逐 detail 清理在途 timer(T-127)。
|
|
6822
|
+
for (const detail of sessionDetailsById.values()) {
|
|
6823
|
+
if (detail.logDeriveTimer) clearTimeout(detail.logDeriveTimer);
|
|
6824
|
+
}
|
|
6497
6825
|
if (e2eListReleaseTimer !== null) clearTimeout(e2eListReleaseTimer);
|
|
6498
6826
|
for (const [timer, resolve] of e2eModelDelayWaiters) {
|
|
6499
6827
|
clearTimeout(timer);
|