dsh-activity-pane 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +367 -104
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/scripts/acceptance.mjs +9 -4
- package/scripts/check.mjs +322 -60
- package/src/client.mjs +180 -68
- package/src/core.mjs +150 -36
- package/src/navigation.mjs +37 -0
package/.dsh-plugin/client.js
CHANGED
|
@@ -9,7 +9,7 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
//
|
|
10
10
|
// 数据源约定(来自 @deepseek-ai/dsh-client-runtime 的 sessions.list 快照):
|
|
11
11
|
// ids: SessionId[] —— 宿主列表顺序(祖先在前的 lineage 顺序)
|
|
12
|
-
// byId[id]: { id, displayTitle, title?, cwd?, parentId?, running, completed?,
|
|
12
|
+
// byId[id]: { id, displayTitle, title?, cwd?, parentId?, origin?, running, completed?,
|
|
13
13
|
// pendingInteraction?: 'approval'|'plan-review'|'question', blank, ... }
|
|
14
14
|
// current: SessionId | undefined
|
|
15
15
|
// subagentsByParent: { [parentId]: { entries: [{ id, label, ... }] } }
|
|
@@ -17,8 +17,8 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
// 以及 workspaces.list 的 items: [{ title, path, sessionIds }]。
|
|
18
18
|
//
|
|
19
19
|
// 展示规则:
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// 主会话(非有效子代理行):running || completed || pendingInteraction 都显示;
|
|
21
|
+
// 子代理(origin === 'subagent' 且 parentId 有效):仅 running || pendingInteraction 时显示(结束后即消失);
|
|
22
22
|
// pendingInteraction 总是优先视为"等待用户行动"(即使在 running 中)。
|
|
23
23
|
|
|
24
24
|
const PENDING_LABELS = {
|
|
@@ -1073,6 +1073,31 @@ function usageSummary({ uncachedInputTokens = null, cacheReadTokens = null, cach
|
|
|
1073
1073
|
return { inputTokens, cacheHitPct };
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
1076
|
+
/** 从 sessions.list 的投影统一归一卡片统计:运行卡与最近卡共享同一口径。 */
|
|
1077
|
+
function statsFromProjection(projection = null, elapsedMs = null) {
|
|
1078
|
+
const usage = isRecord(projection?.tokenUsage) ? projection.tokenUsage : {};
|
|
1079
|
+
const sessionStats = isRecord(projection?.sessionStats) ? projection.sessionStats : {};
|
|
1080
|
+
const rateTokS = Number.isFinite(sessionStats.decodeMs) && sessionStats.decodeMs > 0
|
|
1081
|
+
? sessionStats.decodeTokens / (sessionStats.decodeMs / 1000)
|
|
1082
|
+
: null;
|
|
1083
|
+
return {
|
|
1084
|
+
...runtimeStats({ elapsedMs, outputTokens: usage.outputTokens, rateTokS }),
|
|
1085
|
+
...usageSummary(usage),
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** 统计字段按字段合并:当前值有效时优先,否则回退到上一份已知值;不合并 elapsedMs。 */
|
|
1090
|
+
function mergeRuntimeStats(current = null, fallback = null) {
|
|
1091
|
+
const currentStats = isRecord(current) ? current : {};
|
|
1092
|
+
const fallbackStats = isRecord(fallback) ? fallback : {};
|
|
1093
|
+
return {
|
|
1094
|
+
outputTokens: currentStats.outputTokens ?? fallbackStats.outputTokens ?? null,
|
|
1095
|
+
inputTokens: currentStats.inputTokens ?? fallbackStats.inputTokens ?? null,
|
|
1096
|
+
cacheHitPct: currentStats.cacheHitPct ?? fallbackStats.cacheHitPct ?? null,
|
|
1097
|
+
rateTokS: currentStats.rateTokS ?? fallbackStats.rateTokS ?? null,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1076
1101
|
/** 需要用户行动的种类的展示文案。 */
|
|
1077
1102
|
function pendingText(kind) {
|
|
1078
1103
|
return PENDING_LABELS[kind] ?? PENDING_UNKNOWN_LABEL;
|
|
@@ -1174,8 +1199,8 @@ function escapeCssString(value) {
|
|
|
1174
1199
|
* windowComplete(R-01-009/AC-06、R-01-012/AC-12 冷窗口兜底):快照已就绪但窗口缺
|
|
1175
1200
|
* 锚点数据(开放回合起点或可锚用户行在窗口外)时为 false——此时仍发起一次 history
|
|
1176
1201
|
* 补读,供进度锚点与指令锚行兜底。previewFallbackNeeded 表示最近卡的快照预览
|
|
1177
|
-
* 不完整,同样补读一次 history(R-01-013/AC-03、AC-04);durationFallbackNeeded
|
|
1178
|
-
* 需要在已加载的旧 history 之后再取一次最新回合边界(R-01-009/AC-12)。 */
|
|
1202
|
+
* 不完整,同样补读一次 history(R-01-013/AC-03、AC-04);durationFallbackNeeded 表示等待卡或最近卡
|
|
1203
|
+
* 需要在已加载的旧 history 之后再取一次最新回合边界(R-01-009/AC-12、R-01-013/AC-12)。 */
|
|
1179
1204
|
function detailLoadPlan({
|
|
1180
1205
|
detail = {},
|
|
1181
1206
|
isSubagent = false,
|
|
@@ -1256,11 +1281,11 @@ function workspaceInfoForSession(sessionId, workspaceItems, byId = {}) {
|
|
|
1256
1281
|
}
|
|
1257
1282
|
|
|
1258
1283
|
/**
|
|
1259
|
-
*
|
|
1284
|
+
* 工作区徽标基色色相(R-01-003/AC-08、AC-09):以工作区身份为唯一输入的纯函数——
|
|
1260
1285
|
* djb2 哈希经雪崩终混(异或右移 + 乘法,把高位熵折入低位,消除 djb2 低位分布
|
|
1261
1286
|
* 聚集;每步 >>> 0 保持无符号,异或结果可能带符号位)后在避开红色警戒区的
|
|
1262
1287
|
* 色相弧 [30°,320°] 上均匀取色(30 + hash % 291),输出 [30,320] 整数。
|
|
1263
|
-
*
|
|
1288
|
+
* 同一身份恒得同一基色色相,与工作区列表顺序、会话状态及持久化存储无关,页面
|
|
1264
1289
|
* 刷新后不变;空身份返回 null。
|
|
1265
1290
|
*/
|
|
1266
1291
|
function workspaceHue(key) {
|
|
@@ -1279,34 +1304,99 @@ function workspaceHue(key) {
|
|
|
1279
1304
|
return 30 + (hash % 291);
|
|
1280
1305
|
}
|
|
1281
1306
|
|
|
1282
|
-
const
|
|
1307
|
+
const WORKSPACE_COLOR_SLOTS = Object.freeze([
|
|
1308
|
+
{ slot: 0, hue: 55, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1309
|
+
{ slot: 1, hue: 100, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1310
|
+
{ slot: 2, hue: 145, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1311
|
+
{ slot: 3, hue: 190, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1312
|
+
{ slot: 4, hue: 235, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1313
|
+
{ slot: 5, hue: 280, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1314
|
+
{ slot: 6, hue: 325, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1315
|
+
{ slot: 7, hue: 77, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1316
|
+
{ slot: 8, hue: 122, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1317
|
+
{ slot: 9, hue: 167, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1318
|
+
{ slot: 10, hue: 257, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1319
|
+
{ slot: 11, hue: 302, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1320
|
+
].map(Object.freeze));
|
|
1321
|
+
|
|
1322
|
+
const WORKSPACE_BACKGROUND_SLOTS = Object.freeze([
|
|
1323
|
+
{
|
|
1324
|
+
slot: 0,
|
|
1325
|
+
dark: Object.freeze({ l: 0.30, c: 0.05, mix: "26%", borderMix: "46%" }),
|
|
1326
|
+
light: Object.freeze({ l: 0.92, c: 0.05, mix: "20%", borderMix: "34%" }),
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
slot: 1,
|
|
1330
|
+
dark: Object.freeze({ l: 0.38, c: 0.06, mix: "31%", borderMix: "52%" }),
|
|
1331
|
+
light: Object.freeze({ l: 0.84, c: 0.06, mix: "27%", borderMix: "42%" }),
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
slot: 2,
|
|
1335
|
+
dark: Object.freeze({ l: 0.46, c: 0.07, mix: "36%", borderMix: "58%" }),
|
|
1336
|
+
light: Object.freeze({ l: 0.76, c: 0.07, mix: "34%", borderMix: "50%" }),
|
|
1337
|
+
},
|
|
1338
|
+
].map(Object.freeze));
|
|
1339
|
+
|
|
1340
|
+
const WORKSPACE_PRIMARY_SLOT_COUNT = 7;
|
|
1341
|
+
const WORKSPACE_SECONDARY_SLOT_COUNT = WORKSPACE_COLOR_SLOTS.length - WORKSPACE_PRIMARY_SLOT_COUNT;
|
|
1342
|
+
|
|
1343
|
+
function workspaceSlotProbe(start, count, step) {
|
|
1344
|
+
return Array.from({ length: count }, (_, offset) => (start + offset * step) % count);
|
|
1345
|
+
}
|
|
1283
1346
|
|
|
1284
1347
|
/**
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1348
|
+
* 同屏工作区复合颜色槽位消解(R-01-003/AC-08、AC-12):前七个身份沿原七色
|
|
1349
|
+
* OKLCH 主槽位分配,主槽位占满后再使用五个受控明度/色相补充槽位;每个前景槽位
|
|
1350
|
+
* 内优先使用未用的三个背景变体;超过三十六个复合身份后按槽位使用次数均衡复用。
|
|
1288
1351
|
*/
|
|
1289
|
-
function
|
|
1352
|
+
function resolveWorkspaceColors(keys) {
|
|
1290
1353
|
const identities = [...new Set((Array.isArray(keys) ? keys : []).map(cleanText).filter(Boolean))].sort();
|
|
1291
|
-
const uses =
|
|
1354
|
+
const uses = WORKSPACE_COLOR_SLOTS.map(() => 0);
|
|
1355
|
+
const backgroundUses = WORKSPACE_COLOR_SLOTS.map(() => WORKSPACE_BACKGROUND_SLOTS.map(() => 0));
|
|
1292
1356
|
const resolved = new Map();
|
|
1293
1357
|
for (const identity of identities) {
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1358
|
+
const baseHue = workspaceHue(identity);
|
|
1359
|
+
const primaryOrder = workspaceSlotProbe(
|
|
1360
|
+
(baseHue - 30) % WORKSPACE_PRIMARY_SLOT_COUNT,
|
|
1361
|
+
WORKSPACE_PRIMARY_SLOT_COUNT,
|
|
1362
|
+
3,
|
|
1363
|
+
);
|
|
1364
|
+
const secondaryOrder = workspaceSlotProbe(
|
|
1365
|
+
(baseHue - 30) % WORKSPACE_SECONDARY_SLOT_COUNT,
|
|
1366
|
+
WORKSPACE_SECONDARY_SLOT_COUNT,
|
|
1367
|
+
2,
|
|
1368
|
+
).map((index) => WORKSPACE_PRIMARY_SLOT_COUNT + index);
|
|
1369
|
+
const order = [...primaryOrder, ...secondaryOrder];
|
|
1370
|
+
const backgroundOrder = workspaceSlotProbe(
|
|
1371
|
+
(baseHue - 30) % WORKSPACE_BACKGROUND_SLOTS.length,
|
|
1372
|
+
WORKSPACE_BACKGROUND_SLOTS.length,
|
|
1373
|
+
2,
|
|
1374
|
+
);
|
|
1375
|
+
const availableForegroundOrder = order.filter((index) => backgroundOrder.some((backgroundSlot) => backgroundUses[index][backgroundSlot] === 0));
|
|
1376
|
+
const foregroundOrder = availableForegroundOrder.length > 0 ? availableForegroundOrder : order;
|
|
1377
|
+
let foregroundSlot = foregroundOrder.find((index) => uses[index] === 0);
|
|
1378
|
+
if (foregroundSlot === undefined) {
|
|
1379
|
+
foregroundSlot = foregroundOrder.reduce(
|
|
1380
|
+
(best, index) => (uses[index] < uses[best] ? index : best),
|
|
1381
|
+
foregroundOrder[0],
|
|
1382
|
+
);
|
|
1303
1383
|
}
|
|
1304
|
-
uses[
|
|
1305
|
-
|
|
1384
|
+
uses[foregroundSlot] += 1;
|
|
1385
|
+
const backgroundUsesForForeground = backgroundUses[foregroundSlot];
|
|
1386
|
+
const backgroundSlot = backgroundOrder.reduce(
|
|
1387
|
+
(best, index) => (backgroundUsesForForeground[index] < backgroundUsesForForeground[best] ? index : best),
|
|
1388
|
+
backgroundOrder[0],
|
|
1389
|
+
);
|
|
1390
|
+
backgroundUsesForForeground[backgroundSlot] += 1;
|
|
1391
|
+
resolved.set(identity, {
|
|
1392
|
+
foreground: WORKSPACE_COLOR_SLOTS[foregroundSlot],
|
|
1393
|
+
background: WORKSPACE_BACKGROUND_SLOTS[backgroundSlot],
|
|
1394
|
+
});
|
|
1306
1395
|
}
|
|
1307
1396
|
return resolved;
|
|
1308
1397
|
}
|
|
1309
1398
|
|
|
1399
|
+
|
|
1310
1400
|
/** 主会话按左侧工作区顺序排序的权重;不在任何 workspace 的排在最后保持 lineage 顺序。 */
|
|
1311
1401
|
function workspaceRank(workspaceItems) {
|
|
1312
1402
|
const wsIndex = new Map();
|
|
@@ -1361,6 +1451,7 @@ function mainTitle(byId, id) {
|
|
|
1361
1451
|
* - 主会话 running(且无 pending)或处于委托周期(含后代耗尽空窗)→ 'running'
|
|
1362
1452
|
* - 主会话 pendingInteraction / completed / errorReminder → 'awaiting'(等待用户行动)
|
|
1363
1453
|
* - 子代理 running / pending 或存在活动后代 → 'subagent',自身与后代均不活动则不显示
|
|
1454
|
+
* - 普通 fork 虽有 parentId,但 origin 缺失,仍按独立主会话处理
|
|
1364
1455
|
* completions(完成确认与错误提醒,R-01-002/AC-05、AC-13、R-01-010/AC-06):Map id →
|
|
1365
1456
|
* { lastTurnEnd, lastTurnEndKind, lastTurnEndError, ackedAt },由渲染层从宿主侧 ack 状态
|
|
1366
1457
|
* 通道注入;其中完成提醒成立(completionReminder)或错误提醒成立(errorReminder)的
|
|
@@ -1368,21 +1459,38 @@ function mainTitle(byId, id) {
|
|
|
1368
1459
|
* 记账派生):集合内会话视同处于委托周期——后代耗尽至 settle 处理回合启动的
|
|
1369
1460
|
* 空窗内仍保持运行呈现、完成/错误提醒不生效;条目的 descendantActive 字段
|
|
1370
1461
|
* 始终为当帧原始后代活性(供进度锚点记账判定耗尽),不受 delegatingIds 影响。
|
|
1462
|
+
* archivedIds(工作区服务的注册表全局归档集合):归档会话及其有效子代理子树不产出活动条目,
|
|
1463
|
+
* 普通 fork 仅因共享来源 parentId 不受影响;避免会话服务仍保留旧行或完成提醒时在窗格中滞留。
|
|
1371
1464
|
*/
|
|
1372
|
-
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null) {
|
|
1465
|
+
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = []) {
|
|
1373
1466
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1374
1467
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1375
1468
|
const current = snapshot?.current ?? null;
|
|
1376
1469
|
const subagentsByParent = isRecord(snapshot?.subagentsByParent)
|
|
1377
1470
|
? snapshot.subagentsByParent
|
|
1378
1471
|
: {};
|
|
1472
|
+
const archived = archivedIds instanceof Set
|
|
1473
|
+
? new Set([...archivedIds].map((id) => String(id)))
|
|
1474
|
+
: new Set((archivedIds ?? []).map((id) => String(id)));
|
|
1475
|
+
const isArchived = (id) => {
|
|
1476
|
+
const seen = new Set();
|
|
1477
|
+
let currentId = id;
|
|
1478
|
+
while (currentId !== undefined && currentId !== null) {
|
|
1479
|
+
const key = String(currentId);
|
|
1480
|
+
if (archived.has(key) || seen.has(key)) return archived.has(key);
|
|
1481
|
+
seen.add(key);
|
|
1482
|
+
currentId = subagentParentId(byId[key], byId);
|
|
1483
|
+
}
|
|
1484
|
+
return false;
|
|
1485
|
+
};
|
|
1379
1486
|
const rank = workspaceRank(workspaceItems ?? []);
|
|
1380
|
-
const descendantIds = descendantActiveIds(byId);
|
|
1487
|
+
const descendantIds = descendantActiveIds(byId, isArchived);
|
|
1381
1488
|
// 第一遍:层级关系 + 显示判定(show = 自身活动 || 委托周期 || 完成提醒,单点实现避免漂移)。
|
|
1382
1489
|
const rootIds = [];
|
|
1383
1490
|
const childIds = new Map();
|
|
1384
1491
|
const meta = new Map();
|
|
1385
1492
|
for (const id of ids) {
|
|
1493
|
+
if (isArchived(id)) continue;
|
|
1386
1494
|
const row = byId[id];
|
|
1387
1495
|
if (!isRecord(row)) continue;
|
|
1388
1496
|
const hasParent = isSubagentRow(row, byId);
|
|
@@ -1600,10 +1708,15 @@ function listLoadState(snapshot) {
|
|
|
1600
1708
|
return "ready";
|
|
1601
1709
|
}
|
|
1602
1710
|
|
|
1603
|
-
/**
|
|
1711
|
+
/** 会话行是否为某主会话的直属子代理:需同时满足 origin 与有效父会话。 */
|
|
1604
1712
|
function isSubagentRow(row, byId = {}) {
|
|
1605
1713
|
const id = row?.parentId;
|
|
1606
|
-
return id !== undefined && id !== null && isRecord(byId[id]);
|
|
1714
|
+
return row?.origin === "subagent" && id !== undefined && id !== null && isRecord(byId[id]);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/** 取实际子代理的直属母会话 id;普通 fork 的 parentId 只是来源关系,不参与子代理树。 */
|
|
1718
|
+
function subagentParentId(row, byId = {}) {
|
|
1719
|
+
return isSubagentRow(row, byId) ? row.parentId : undefined;
|
|
1607
1720
|
}
|
|
1608
1721
|
|
|
1609
1722
|
/** 会话行是否满足自身状态的活动判定,不含后代活动继承。
|
|
@@ -1617,31 +1730,32 @@ function isOwnActiveRow(row, byId = {}) {
|
|
|
1617
1730
|
return running || pending;
|
|
1618
1731
|
}
|
|
1619
1732
|
|
|
1620
|
-
/**
|
|
1733
|
+
/** 沿实际子代理关系的有效 parentId 链上溯收集会话 id:includeSelf 含活动会话自身,
|
|
1621
1734
|
* 否则只收祖先(存在活动后代的母会话)。活动区与历史区显示判定的单点实现。 */
|
|
1622
|
-
function lineageActiveIds(byId, includeSelf) {
|
|
1735
|
+
function lineageActiveIds(byId, includeSelf, isExcluded = null) {
|
|
1623
1736
|
const ids = new Set();
|
|
1624
1737
|
for (const [id, row] of Object.entries(byId)) {
|
|
1625
|
-
if (!isOwnActiveRow(row, byId)) continue;
|
|
1738
|
+
if (isExcluded?.(id) || !isOwnActiveRow(row, byId)) continue;
|
|
1626
1739
|
const seen = new Set();
|
|
1627
|
-
let currentId = includeSelf ? id : row
|
|
1740
|
+
let currentId = includeSelf ? id : subagentParentId(row, byId);
|
|
1628
1741
|
while (currentId !== undefined && currentId !== null && isRecord(byId[currentId]) && !seen.has(String(currentId))) {
|
|
1742
|
+
if (isExcluded?.(currentId)) break;
|
|
1629
1743
|
seen.add(String(currentId));
|
|
1630
1744
|
ids.add(String(currentId));
|
|
1631
|
-
currentId = byId[currentId]
|
|
1745
|
+
currentId = subagentParentId(byId[currentId], byId);
|
|
1632
1746
|
}
|
|
1633
1747
|
}
|
|
1634
1748
|
return ids;
|
|
1635
1749
|
}
|
|
1636
1750
|
|
|
1637
|
-
/**
|
|
1751
|
+
/** 沿实际子代理关系补齐活动祖先(含活动会话自身),供历史区显示判定。 */
|
|
1638
1752
|
function activeSessionIds(byId = {}) {
|
|
1639
1753
|
return lineageActiveIds(byId, true);
|
|
1640
1754
|
}
|
|
1641
1755
|
|
|
1642
1756
|
/** 「存在活动后代」的母会话集合(不含活动会话自身),供 buildEntries 判定委托周期(R-01-003/AC-05)。 */
|
|
1643
|
-
function descendantActiveIds(byId = {}) {
|
|
1644
|
-
return lineageActiveIds(byId, false);
|
|
1757
|
+
function descendantActiveIds(byId = {}, isExcluded = null) {
|
|
1758
|
+
return lineageActiveIds(byId, false, isExcluded);
|
|
1645
1759
|
}
|
|
1646
1760
|
|
|
1647
1761
|
/** 判断活动条目是否需要建立轮内状态订阅:以宿主 running 为准、与呈现 kind 解耦——
|
|
@@ -2083,6 +2197,43 @@ function openSession(sessions, sessionId) {
|
|
|
2083
2197
|
}
|
|
2084
2198
|
}
|
|
2085
2199
|
|
|
2200
|
+
/**
|
|
2201
|
+
* 以最小必要距离把卡片滚入窗格滚动视口;不会居中,也不会滚动外层页面。
|
|
2202
|
+
* 默认使用原生 smooth scroll,调用方可传 auto 适配降低动效偏好;缺少 scrollTo 或 options 不受支持时回退为同步滚动
|
|
2203
|
+
* (R-01-006/AC-02)。
|
|
2204
|
+
*/
|
|
2205
|
+
function scrollCardIntoView(scroll, card, behavior = "smooth") {
|
|
2206
|
+
if (typeof scroll?.getBoundingClientRect !== "function" || typeof card?.getBoundingClientRect !== "function")
|
|
2207
|
+
return false;
|
|
2208
|
+
const viewport = scroll.getBoundingClientRect();
|
|
2209
|
+
const rect = card.getBoundingClientRect();
|
|
2210
|
+
const delta = rect.top < viewport.top
|
|
2211
|
+
? rect.top - viewport.top
|
|
2212
|
+
: rect.bottom > viewport.bottom
|
|
2213
|
+
? rect.bottom - viewport.bottom
|
|
2214
|
+
: 0;
|
|
2215
|
+
if (delta === 0) return false;
|
|
2216
|
+
const currentTop = Number(scroll.scrollTop);
|
|
2217
|
+
if (!Number.isFinite(currentTop)) return false;
|
|
2218
|
+
const scrollHeight = Number(scroll.scrollHeight);
|
|
2219
|
+
const clientHeight = Number(scroll.clientHeight);
|
|
2220
|
+
const maxTop = Number.isFinite(scrollHeight) && Number.isFinite(clientHeight)
|
|
2221
|
+
? Math.max(0, scrollHeight - clientHeight)
|
|
2222
|
+
: Infinity;
|
|
2223
|
+
const nextTop = Math.min(maxTop, Math.max(0, currentTop + delta));
|
|
2224
|
+
if (!Number.isFinite(nextTop) || nextTop === currentTop) return false;
|
|
2225
|
+
if (typeof scroll.scrollTo === "function") {
|
|
2226
|
+
try {
|
|
2227
|
+
scroll.scrollTo({ top: nextTop, behavior });
|
|
2228
|
+
if (behavior !== "auto" || Number(scroll.scrollTop) !== currentTop) return true;
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
if (!(error instanceof TypeError)) throw error;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
scroll.scrollTop = nextTop;
|
|
2234
|
+
return Number(scroll.scrollTop) !== currentTop;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2086
2237
|
/** 原生会话输入框(dsh-client-ui-conversation composer 的 textarea)。 */
|
|
2087
2238
|
const COMPOSER_SELECTOR = "textarea[data-phase]";
|
|
2088
2239
|
|
|
@@ -2595,23 +2746,41 @@ const CSS = `
|
|
|
2595
2746
|
/* 复审修复(C-040、C-043):状态点光晕三类同强度、仅换色相——阻塞金、完成绿与错误红光晕一致。 */
|
|
2596
2747
|
/* 工作区徽标「图标+文本」双段:文件夹图标与左边栏工作区条目同源(R-01-003/AC-06);
|
|
2597
2748
|
名称字号不低于 10.5px(AC-07),行高保持 14px 以维持胶囊与卡片高度。
|
|
2598
|
-
着色(AC-08~AC-
|
|
2599
|
-
|
|
2600
|
-
|
|
2749
|
+
着色(AC-08~AC-12):核心映射提供 12 个前景身份槽与每槽 3 个背景变体;
|
|
2750
|
+
深色/浅色主题分别使用前景与背景槽位的 L/C 参数,文字使用前景调色板色,
|
|
2751
|
+
底色使用独立背景源色、描边使用前景调色板色,分别在 OKLCH 空间按变体混合强度呈现;前景与背景保持同色相族。 */
|
|
2601
2752
|
[data-dsh-activity-pane] .dap-workspace {
|
|
2602
2753
|
width: fit-content; max-width: 100%; display: flex; align-items: center; gap: 3px;
|
|
2603
2754
|
overflow: hidden;
|
|
2604
2755
|
font-size: 10.5px; line-height: 14px;
|
|
2605
|
-
--dap-workspace-color: oklch(
|
|
2756
|
+
--dap-workspace-color: oklch(
|
|
2757
|
+
var(--dap-workspace-dark-l, 0.78)
|
|
2758
|
+
var(--dap-workspace-dark-c, 0.16)
|
|
2759
|
+
var(--dap-workspace-hue, 235)
|
|
2760
|
+
);
|
|
2761
|
+
--dap-workspace-background-color: oklch(
|
|
2762
|
+
var(--dap-workspace-bg-dark-l, 0.38)
|
|
2763
|
+
var(--dap-workspace-bg-dark-c, 0.06)
|
|
2764
|
+
var(--dap-workspace-hue, 235)
|
|
2765
|
+
);
|
|
2606
2766
|
color: var(--dap-workspace-color);
|
|
2607
|
-
background: color-mix(in oklch, var(--dap-workspace-color)
|
|
2608
|
-
border: 1px solid color-mix(in oklch, var(--dap-workspace-color)
|
|
2767
|
+
background: color-mix(in oklch, var(--dap-workspace-background-color) var(--dap-workspace-bg-dark-mix, 31%), transparent);
|
|
2768
|
+
border: 1px solid color-mix(in oklch, var(--dap-workspace-color) var(--dap-workspace-bg-dark-border-mix, 52%), transparent);
|
|
2609
2769
|
border-radius: 999px; padding: 0 7px;
|
|
2610
2770
|
}
|
|
2611
2771
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
2612
|
-
--dap-workspace-color: oklch(
|
|
2613
|
-
|
|
2614
|
-
|
|
2772
|
+
--dap-workspace-color: oklch(
|
|
2773
|
+
var(--dap-workspace-light-l, 0.48)
|
|
2774
|
+
var(--dap-workspace-light-c, 0.15)
|
|
2775
|
+
var(--dap-workspace-hue, 235)
|
|
2776
|
+
);
|
|
2777
|
+
--dap-workspace-background-color: oklch(
|
|
2778
|
+
var(--dap-workspace-bg-light-l, 0.84)
|
|
2779
|
+
var(--dap-workspace-bg-light-c, 0.06)
|
|
2780
|
+
var(--dap-workspace-hue, 235)
|
|
2781
|
+
);
|
|
2782
|
+
background: color-mix(in oklch, var(--dap-workspace-background-color) var(--dap-workspace-bg-light-mix, 27%), transparent);
|
|
2783
|
+
border-color: color-mix(in oklch, var(--dap-workspace-color) var(--dap-workspace-bg-light-border-mix, 42%), transparent);
|
|
2615
2784
|
}
|
|
2616
2785
|
[data-dsh-activity-pane] .dap-workspace-icon { flex: none; display: inline-flex; }
|
|
2617
2786
|
[data-dsh-activity-pane] .dap-workspace-icon svg { display: block; }
|
|
@@ -2649,14 +2818,10 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
2649
2818
|
display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
|
|
2650
2819
|
min-width: 0; margin: 2px 0 0;
|
|
2651
2820
|
}
|
|
2652
|
-
/*
|
|
2821
|
+
/* 等待类型胶囊独立同行:耗时回到统计行最右侧,不参与等待脉冲。 */
|
|
2653
2822
|
[data-dsh-activity-pane] .dap-await-head {
|
|
2654
2823
|
display: flex; align-items: center; gap: 8px; align-self: stretch; min-width: 0;
|
|
2655
2824
|
}
|
|
2656
|
-
[data-dsh-activity-pane] .dap-await-head .dap-token-time {
|
|
2657
|
-
margin-left: auto; flex: none; font-size: 10px; line-height: 14px;
|
|
2658
|
-
color: #8f9aaa; font-variant-numeric: tabular-nums;
|
|
2659
|
-
}
|
|
2660
2825
|
[data-dsh-activity-pane] .dap-note-row {
|
|
2661
2826
|
display: flex; align-items: center; gap: 6px; min-width: 0; align-self: stretch;
|
|
2662
2827
|
}
|
|
@@ -2675,7 +2840,7 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
2675
2840
|
color: color-mix(in srgb, currentColor 82%, transparent);
|
|
2676
2841
|
background: color-mix(in srgb, currentColor 14%, transparent);
|
|
2677
2842
|
}
|
|
2678
|
-
/*
|
|
2843
|
+
/* 运行卡与最近卡统计(对齐 answer-pet 卡片;MIT 参考,见 README)。 */
|
|
2679
2844
|
[data-dsh-activity-pane] .dap-progress {
|
|
2680
2845
|
display: flex; align-items: center; gap: 7px; min-width: 0;
|
|
2681
2846
|
}
|
|
@@ -3158,16 +3323,18 @@ function apply(ctx) {
|
|
|
3158
3323
|
/** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
|
|
3159
3324
|
let pulseSignature = "";
|
|
3160
3325
|
// E2E-only deterministic seams(T-089/T-090):显式 URL fragment 最多把本页首次列表 ready 呈现
|
|
3161
|
-
// 或正式 models RPC 延后 1s
|
|
3326
|
+
// 或正式 models RPC 延后 1s;列表延迟从首个窗格 render 开始计时,避免宿主启动耗时
|
|
3327
|
+
// 把 pending 观察窗口提前消耗;默认路径为 0,不伪造服务响应、快照或生产时序。
|
|
3162
3328
|
const e2eParams = new URLSearchParams(window.location.hash.slice(1));
|
|
3163
3329
|
const requestedListDelay = Number(e2eParams.get("dap-e2e-list-delay"));
|
|
3164
|
-
const
|
|
3165
|
-
?
|
|
3330
|
+
const e2eListDelayMs = Number.isFinite(requestedListDelay) && requestedListDelay > 0
|
|
3331
|
+
? Math.min(requestedListDelay, 1_000)
|
|
3166
3332
|
: 0;
|
|
3167
3333
|
const requestedModelDelay = Number(e2eParams.get("dap-e2e-model-delay"));
|
|
3168
3334
|
const e2eModelDelayMs = Number.isFinite(requestedModelDelay) && requestedModelDelay > 0
|
|
3169
3335
|
? Math.min(requestedModelDelay, 1_000)
|
|
3170
3336
|
: 0;
|
|
3337
|
+
let e2eListReadyAt = 0;
|
|
3171
3338
|
let e2eListReleaseTimer = null;
|
|
3172
3339
|
const e2eModelDelayWaiters = new Map();
|
|
3173
3340
|
function delayedModelCall(call) {
|
|
@@ -3188,6 +3355,9 @@ function apply(ctx) {
|
|
|
3188
3355
|
let paneWidth = readStoredPaneWidth();
|
|
3189
3356
|
/** 用户最近一次激活的卡片 id;打开重试链被更新的激活意图取代即取消。 */
|
|
3190
3357
|
let lastActivatedId = null;
|
|
3358
|
+
/** 最近一次已处理的当前卡片;同一卡片的运行时重绘不反复打断用户手动滚动。 */
|
|
3359
|
+
let autoScrolledCurrentId = null;
|
|
3360
|
+
let autoScrolledCurrentCard = null;
|
|
3191
3361
|
/** 完成确认状态(R-01-002/AC-03、AC-10~AC-12):id → { lastTurnEnd, ackedAt },
|
|
3192
3362
|
* 来自宿主侧 ack 通道(SSE 全量快照),随每次推送整体替换。 */
|
|
3193
3363
|
const completeAcksById = new Map();
|
|
@@ -3214,7 +3384,7 @@ function apply(ctx) {
|
|
|
3214
3384
|
const livenessById = new Map();
|
|
3215
3385
|
/** 委托周期进度锚点记账:id → progressAnchor 状态(R-01-009/AC-06)。 */
|
|
3216
3386
|
const progressAnchorById = new Map();
|
|
3217
|
-
/**
|
|
3387
|
+
/** 订阅停止后保留最近快照与最后已知运行统计,供 awaiting/recent 卡继续显示上下文。 */
|
|
3218
3388
|
const sessionDetailsById = new Map();
|
|
3219
3389
|
/** 会话跳转的单一重试链;避免重复点击叠加 refresh/timer。 */
|
|
3220
3390
|
const openRetryStates = new Map();
|
|
@@ -3889,7 +4059,10 @@ function apply(ctx) {
|
|
|
3889
4059
|
const agentLabel = makeEl("span", "dap-history-label");
|
|
3890
4060
|
agentLabel.textContent = "助手";
|
|
3891
4061
|
agentLine.append(agentIcon, agentLabel, makeEl("span", "dap-history-separator"), makeEl("span", "dap-history-text"));
|
|
3892
|
-
|
|
4062
|
+
const statsRow = makeEl("div", "dap-token-stats");
|
|
4063
|
+
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
4064
|
+
statsRow.hidden = true;
|
|
4065
|
+
return [head, row, userLine, agentLine, statsRow, makeEl("div", "dap-note")];
|
|
3893
4066
|
}
|
|
3894
4067
|
if (kind === "awaiting") {
|
|
3895
4068
|
const row = makeEl("div", "dap-row");
|
|
@@ -3903,10 +4076,13 @@ function apply(ctx) {
|
|
|
3903
4076
|
const noteRow = makeEl("div", "dap-note-row");
|
|
3904
4077
|
noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
|
|
3905
4078
|
const awaitHead = makeEl("div", "dap-await-head");
|
|
3906
|
-
awaitHead.append(capsule
|
|
4079
|
+
awaitHead.append(capsule);
|
|
4080
|
+
const statsRow = makeEl("div", "dap-token-stats");
|
|
4081
|
+
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
4082
|
+
statsRow.hidden = true;
|
|
3907
4083
|
const foot = makeEl("div", "dap-foot");
|
|
3908
4084
|
foot.append(awaitHead, noteRow);
|
|
3909
|
-
return [head, row, makeEl("div", "dap-trace"), foot];
|
|
4085
|
+
return [head, row, makeEl("div", "dap-trace"), statsRow, foot];
|
|
3910
4086
|
}
|
|
3911
4087
|
const row = makeEl("div", "dap-row");
|
|
3912
4088
|
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
@@ -4267,14 +4443,19 @@ function apply(ctx) {
|
|
|
4267
4443
|
}
|
|
4268
4444
|
}
|
|
4269
4445
|
|
|
4270
|
-
/**
|
|
4446
|
+
/** 统计行渲染:运行卡、等待卡与最近卡写入速率、token 与耗时,旧骨架缺节点时就地补齐。 */
|
|
4271
4447
|
function renderTokenStats(el, entry) {
|
|
4272
4448
|
let stats = el.querySelector(".dap-token-stats");
|
|
4273
4449
|
if (stats === null) {
|
|
4274
4450
|
stats = makeEl("div", "dap-token-stats");
|
|
4275
4451
|
stats.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
4276
4452
|
stats.hidden = true;
|
|
4277
|
-
el.
|
|
4453
|
+
const foot = entry.kind === "awaiting" ? el.querySelector(".dap-foot") : null;
|
|
4454
|
+
if (foot !== null) el.insertBefore(stats, foot);
|
|
4455
|
+
else el.append(stats);
|
|
4456
|
+
} else if (entry.kind === "awaiting") {
|
|
4457
|
+
const foot = el.querySelector(".dap-foot");
|
|
4458
|
+
if (foot !== null && stats.nextElementSibling !== foot) el.insertBefore(stats, foot);
|
|
4278
4459
|
}
|
|
4279
4460
|
let mainTextEl = stats.querySelector(".dap-token-main");
|
|
4280
4461
|
let timeEl = stats.querySelector(".dap-token-time");
|
|
@@ -4296,25 +4477,9 @@ function apply(ctx) {
|
|
|
4296
4477
|
if (stats.hidden !== statsHidden) stats.hidden = statsHidden;
|
|
4297
4478
|
}
|
|
4298
4479
|
|
|
4299
|
-
/**
|
|
4300
|
-
function
|
|
4301
|
-
|
|
4302
|
-
if (foot === null) return;
|
|
4303
|
-
let head = foot.querySelector(".dap-await-head");
|
|
4304
|
-
if (head === null) {
|
|
4305
|
-
const capsule = foot.querySelector(".dap-capsule");
|
|
4306
|
-
if (capsule === null) return;
|
|
4307
|
-
head = makeEl("div", "dap-await-head");
|
|
4308
|
-
capsule.replaceWith(head);
|
|
4309
|
-
head.append(capsule);
|
|
4310
|
-
}
|
|
4311
|
-
let time = head.querySelector(".dap-token-time");
|
|
4312
|
-
if (time === null) {
|
|
4313
|
-
time = makeEl("span", "dap-token-time");
|
|
4314
|
-
head.append(time);
|
|
4315
|
-
}
|
|
4316
|
-
const text = Number.isFinite(entry.elapsedMs) && entry.elapsedMs >= 0 ? fmtElapsedMs(entry.elapsedMs) : "";
|
|
4317
|
-
if (time.textContent !== text) time.textContent = text;
|
|
4480
|
+
/** 旧热装等待骨架清理:耗时已迁回统计行,去掉胶囊同行残留的时长节点。 */
|
|
4481
|
+
function removeAwaitingHeadDuration(el) {
|
|
4482
|
+
el.querySelector(".dap-await-head .dap-token-time")?.remove();
|
|
4318
4483
|
}
|
|
4319
4484
|
|
|
4320
4485
|
/** 时间线区加载指示:数据在途且尚无工作项时显示活动图标行(R-01-014/AC-02)。 */
|
|
@@ -4341,22 +4506,40 @@ function apply(ctx) {
|
|
|
4341
4506
|
renderTrace(container, entry.timeline, { lastOnly });
|
|
4342
4507
|
}
|
|
4343
4508
|
|
|
4344
|
-
function renderCardInto(el, entry,
|
|
4509
|
+
function renderCardInto(el, entry, colorByWorkspace) {
|
|
4345
4510
|
const workspaceLabel = el.querySelector(".dap-workspace");
|
|
4346
4511
|
if (workspaceLabel !== null) {
|
|
4347
4512
|
const workspaceText = workspaceLabel.querySelector(".dap-workspace-text");
|
|
4348
|
-
|
|
4513
|
+
const color = colorByWorkspace.get(entry.workspaceKey);
|
|
4514
|
+
const foreground = color?.foreground;
|
|
4515
|
+
const background = color?.background;
|
|
4516
|
+
const colorVariables = {
|
|
4517
|
+
"--dap-workspace-hue": foreground?.hue,
|
|
4518
|
+
"--dap-workspace-dark-l": foreground?.darkL,
|
|
4519
|
+
"--dap-workspace-dark-c": foreground?.darkC,
|
|
4520
|
+
"--dap-workspace-light-l": foreground?.lightL,
|
|
4521
|
+
"--dap-workspace-light-c": foreground?.lightC,
|
|
4522
|
+
"--dap-workspace-bg-dark-l": background?.dark?.l,
|
|
4523
|
+
"--dap-workspace-bg-dark-c": background?.dark?.c,
|
|
4524
|
+
"--dap-workspace-bg-dark-mix": background?.dark?.mix,
|
|
4525
|
+
"--dap-workspace-bg-dark-border-mix": background?.dark?.borderMix,
|
|
4526
|
+
"--dap-workspace-bg-light-l": background?.light?.l,
|
|
4527
|
+
"--dap-workspace-bg-light-c": background?.light?.c,
|
|
4528
|
+
"--dap-workspace-bg-light-mix": background?.light?.mix,
|
|
4529
|
+
"--dap-workspace-bg-light-border-mix": background?.light?.borderMix,
|
|
4530
|
+
};
|
|
4531
|
+
if (entry.workspaceTitle !== "" && foreground !== undefined && background !== undefined) {
|
|
4349
4532
|
if (workspaceText !== null) restoreTextField(workspaceText, entry.workspaceTitle);
|
|
4350
|
-
const
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
if (
|
|
4354
|
-
else workspaceLabel.style.setProperty(
|
|
4533
|
+
for (const [property, value] of Object.entries(colorVariables)) {
|
|
4534
|
+
const next = value === undefined ? "" : String(value);
|
|
4535
|
+
if (workspaceLabel.style.getPropertyValue(property) === next) continue;
|
|
4536
|
+
if (next === "") workspaceLabel.style.removeProperty(property);
|
|
4537
|
+
else workspaceLabel.style.setProperty(property, next);
|
|
4355
4538
|
}
|
|
4356
4539
|
workspaceLabel.removeAttribute("hidden");
|
|
4357
4540
|
} else {
|
|
4358
4541
|
if (workspaceText !== null) restoreTextField(workspaceText, "");
|
|
4359
|
-
workspaceLabel.style.removeProperty(
|
|
4542
|
+
for (const property of Object.keys(colorVariables)) workspaceLabel.style.removeProperty(property);
|
|
4360
4543
|
workspaceLabel.setAttribute("hidden", "");
|
|
4361
4544
|
}
|
|
4362
4545
|
}
|
|
@@ -4449,12 +4632,14 @@ function apply(ctx) {
|
|
|
4449
4632
|
restoreTextField(text, previews[i]);
|
|
4450
4633
|
}
|
|
4451
4634
|
}
|
|
4635
|
+
renderTokenStats(el, entry);
|
|
4452
4636
|
}
|
|
4453
4637
|
|
|
4454
4638
|
if (entry.kind === "awaiting") {
|
|
4455
4639
|
const traceContainer = el.querySelector(".dap-trace");
|
|
4456
4640
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
4457
|
-
|
|
4641
|
+
removeAwaitingHeadDuration(el);
|
|
4642
|
+
renderTokenStats(el, entry);
|
|
4458
4643
|
const confirm = el.querySelector(".dap-confirm");
|
|
4459
4644
|
if (confirm !== null) {
|
|
4460
4645
|
// 激活锚点:只在结构重建时绑一次(卡片按 id 复用,kind 变化会重建骨架)。
|
|
@@ -4733,7 +4918,7 @@ function apply(ctx) {
|
|
|
4733
4918
|
|
|
4734
4919
|
/** 渲染某一张卡片进指定列表容器(活动/历史通用)。index 是条目在卡片序列中的
|
|
4735
4920
|
* 序号,offset 是容器内首个卡片前的非卡片子节点数(活动区有轨道层、历史区有段头)。 */
|
|
4736
|
-
function renderCardIntoList(list, entry, reuseMap, index, offset,
|
|
4921
|
+
function renderCardIntoList(list, entry, reuseMap, index, offset, colorByWorkspace) {
|
|
4737
4922
|
let rec = reuseMap.get(entry.id);
|
|
4738
4923
|
if (rec === undefined) {
|
|
4739
4924
|
const el = document.createElement("div");
|
|
@@ -4790,7 +4975,7 @@ function apply(ctx) {
|
|
|
4790
4975
|
recentTimeText ? "," + recentTimeText : ""
|
|
4791
4976
|
}`,
|
|
4792
4977
|
);
|
|
4793
|
-
renderCardInto(rec.el, entry,
|
|
4978
|
+
renderCardInto(rec.el, entry, colorByWorkspace);
|
|
4794
4979
|
// 只有顺序/归属真正变化时才移动 DOM:每次渲染无条件 appendChild 会把所有
|
|
4795
4980
|
// 卡片瞬时移除再插回——按下/抬起之间经过的移动让浏览器取消 click;焦点卡
|
|
4796
4981
|
// 被瞬时断开而失焦;悬停卡的 :hover 也随之丢失且不再补发。会话活跃期间
|
|
@@ -4800,6 +4985,37 @@ function apply(ctx) {
|
|
|
4800
4985
|
return true;
|
|
4801
4986
|
}
|
|
4802
4987
|
|
|
4988
|
+
/** 原生侧栏切换当前会话后,只把已呈现且未完整可见的当前卡片滚入窗格;降低动效偏好时不平滑。 */
|
|
4989
|
+
function ensureCurrentCardVisible(scroll, currentId) {
|
|
4990
|
+
const id = currentId === null || currentId === undefined ? null : String(currentId);
|
|
4991
|
+
if (id === null || id === "") {
|
|
4992
|
+
autoScrolledCurrentId = null;
|
|
4993
|
+
autoScrolledCurrentCard = null;
|
|
4994
|
+
return;
|
|
4995
|
+
}
|
|
4996
|
+
// 当前会话卡片只有在活动列表中才参与自动定位;完成确认后同一 current
|
|
4997
|
+
// 会话会进入历史区,不能因仍带 data-current 把窗格滚到历史卡片。
|
|
4998
|
+
const card = scroll?.querySelector?.(`.${LIST_CLASS} .${CARD_CLASS}[data-current]`) ?? null;
|
|
4999
|
+
if (
|
|
5000
|
+
card === null ||
|
|
5001
|
+
card.dataset.sessionId !== id ||
|
|
5002
|
+
scroll.clientHeight <= 0 ||
|
|
5003
|
+
card.getBoundingClientRect().height <= 0
|
|
5004
|
+
)
|
|
5005
|
+
return;
|
|
5006
|
+
if (autoScrolledCurrentId === id && autoScrolledCurrentCard === card) return;
|
|
5007
|
+
if (scrollCardIntoView(scroll, card, prefersReducedMotion() ? "auto" : "smooth")) {
|
|
5008
|
+
autoScrolledCurrentId = id;
|
|
5009
|
+
autoScrolledCurrentCard = card;
|
|
5010
|
+
return;
|
|
5011
|
+
}
|
|
5012
|
+
const viewport = scroll.getBoundingClientRect();
|
|
5013
|
+
const rect = card.getBoundingClientRect();
|
|
5014
|
+
if (rect.height <= viewport.height && (rect.top < viewport.top || rect.bottom > viewport.bottom)) return;
|
|
5015
|
+
autoScrolledCurrentId = id;
|
|
5016
|
+
autoScrolledCurrentCard = card;
|
|
5017
|
+
}
|
|
5018
|
+
|
|
4803
5019
|
function pruneCards(reuseMap, alive) {
|
|
4804
5020
|
for (const [id, rec] of reuseMap) {
|
|
4805
5021
|
if (alive.has(id)) continue;
|
|
@@ -4983,6 +5199,8 @@ function apply(ctx) {
|
|
|
4983
5199
|
recentTotal = 0;
|
|
4984
5200
|
recentHasMore = false;
|
|
4985
5201
|
recentAppendQueued = false;
|
|
5202
|
+
autoScrolledCurrentId = null;
|
|
5203
|
+
autoScrolledCurrentCard = null;
|
|
4986
5204
|
// 旧窗格已脱离文档:其在飞平移的 transitionend 不再触发,逐元素取消避免残留。
|
|
4987
5205
|
for (const el of [...shiftCleanups.keys()]) cancelShift(el);
|
|
4988
5206
|
}
|
|
@@ -4991,6 +5209,7 @@ function apply(ctx) {
|
|
|
4991
5209
|
const recentSection = pane.querySelector(`.${RECENT_CLASS}`);
|
|
4992
5210
|
if (activeList === null || recentSection === null) return;
|
|
4993
5211
|
|
|
5212
|
+
if (e2eListReadyAt === 0 && e2eListDelayMs > 0) e2eListReadyAt = Date.now() + e2eListDelayMs;
|
|
4994
5213
|
let snapshot = getSnapshot(sessions, "list");
|
|
4995
5214
|
if (e2eListReadyAt > Date.now() && snapshot?.phase !== "error") {
|
|
4996
5215
|
snapshot = { phase: "pending" };
|
|
@@ -5015,7 +5234,7 @@ function apply(ctx) {
|
|
|
5015
5234
|
for (const [id, state] of progressAnchorById) {
|
|
5016
5235
|
if (delegationActive(state, now)) delegatingIds.add(id);
|
|
5017
5236
|
}
|
|
5018
|
-
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds);
|
|
5237
|
+
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds);
|
|
5019
5238
|
// 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
|
|
5020
5239
|
// 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
|
|
5021
5240
|
const runLikeIds = new Set(
|
|
@@ -5099,11 +5318,11 @@ function apply(ctx) {
|
|
|
5099
5318
|
// token/速率取自 sessions.list 条目的投影(tokenUsage/sessionStats),
|
|
5100
5319
|
// 复用既有列表订阅,无新增轮询(R-01-009/AC-05、R-02-004/AC-02)。
|
|
5101
5320
|
const projection = snapshot?.byId?.[entry.id]?.projectionValues;
|
|
5102
|
-
const
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5321
|
+
const projectionStats = statsFromProjection(projection);
|
|
5322
|
+
if (entry.kind === "awaiting") {
|
|
5323
|
+
// 等待期间冻结运行停止前的最后已知统计;刷新/无留存时回退当前列表投影。
|
|
5324
|
+
Object.assign(entry, mergeRuntimeStats(detail?.lastRuntimeStats, projectionStats));
|
|
5325
|
+
}
|
|
5107
5326
|
// 委托周期锚点(R-01-009/AC-06):全部活动条目逐帧记账——锚点不因呈现翻转
|
|
5108
5327
|
// (委托期与 awaiting 互转)或瞬时不可见而丢失,仅 dispose 时整体清除;周期内
|
|
5109
5328
|
// 进度连续(含 settle 处理回合),周期外由宿主回合起点驱动(新回合归零)。
|
|
@@ -5119,13 +5338,15 @@ function apply(ctx) {
|
|
|
5119
5338
|
progressAnchorById.set(entry.id, anchor);
|
|
5120
5339
|
if (entry.kind === "running") {
|
|
5121
5340
|
const elapsedMs = Number.isFinite(anchor.anchor) ? Math.max(0, now - anchor.anchor) : null;
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5341
|
+
Object.assign(entry, projectionStats, { elapsedMs });
|
|
5342
|
+
if (detail) {
|
|
5343
|
+
// 列表投影短暂缺字段时,不覆盖此前已知的有效统计。
|
|
5344
|
+
detail.lastRuntimeStats = mergeRuntimeStats(projectionStats, detail.lastRuntimeStats);
|
|
5345
|
+
}
|
|
5125
5346
|
// 回合进度:纯时间驱动 y = t/(t+k),半衰期每帧按最新实测速率现算,固定
|
|
5126
5347
|
// k 下单调性由函数本身保证;k 变化时允许进度随速率回落而回退(委托周期
|
|
5127
5348
|
// 连续、周期外回合切换归零,R-01-009/AC-06,C-014、C-044)。
|
|
5128
|
-
entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS }) });
|
|
5349
|
+
entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS }) });
|
|
5129
5350
|
}
|
|
5130
5351
|
}
|
|
5131
5352
|
// 历史区时间精化(R-01-010/AC-08、AC-09):从保留快照的 turnTimings 与已拉取的
|
|
@@ -5146,6 +5367,23 @@ function apply(ctx) {
|
|
|
5146
5367
|
recentTotal = recentCandidates.length;
|
|
5147
5368
|
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
5148
5369
|
recentHasMore = recent.length < recentTotal;
|
|
5370
|
+
// 最近卡统计复用运行卡的列表投影口径;耗时从保留快照或已读 history 取最近完整回合,
|
|
5371
|
+
// 缺边界时仅为当前可见历史卡安排一次既有 history 补读(R-01-013/AC-12)。
|
|
5372
|
+
const recentDurationFallbackIds = new Set();
|
|
5373
|
+
for (const entry of recent) {
|
|
5374
|
+
const detail = sessionDetailsById.get(entry.id);
|
|
5375
|
+
const detailSnapshot = livenessById.get(entry.id)?.snapshot ?? detail?.snapshot ?? null;
|
|
5376
|
+
const elapsedMs = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history: detail?.history });
|
|
5377
|
+
const stats = statsFromProjection(snapshot?.byId?.[entry.id]?.projectionValues, elapsedMs);
|
|
5378
|
+
const retained = detail?.lastRuntimeStats;
|
|
5379
|
+
Object.assign(entry, stats, retained ? {
|
|
5380
|
+
outputTokens: retained.outputTokens ?? stats.outputTokens,
|
|
5381
|
+
inputTokens: retained.inputTokens ?? stats.inputTokens,
|
|
5382
|
+
cacheHitPct: retained.cacheHitPct ?? stats.cacheHitPct,
|
|
5383
|
+
rateTokS: retained.rateTokS ?? stats.rateTokS,
|
|
5384
|
+
} : null);
|
|
5385
|
+
if (elapsedMs === null) recentDurationFallbackIds.add(entry.id);
|
|
5386
|
+
}
|
|
5149
5387
|
syncRecentTimeClock(recent.length > 0);
|
|
5150
5388
|
// 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
|
|
5151
5389
|
// 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
|
|
@@ -5166,7 +5404,10 @@ function apply(ctx) {
|
|
|
5166
5404
|
entry.loadingPreviews = (!entry.userPreview || !entry.agentPreview) && historyLoads.has(entry.id);
|
|
5167
5405
|
}
|
|
5168
5406
|
// 补充数据读取优先级:当前会话最优先,活动区先于当前已显示历史页(区内按显示顺序)。
|
|
5169
|
-
const durationFallbackIds = new Set(
|
|
5407
|
+
const durationFallbackIds = new Set([
|
|
5408
|
+
...active.filter((entry) => entry.kind === "awaiting").map((entry) => entry.id),
|
|
5409
|
+
...recentDurationFallbackIds,
|
|
5410
|
+
]);
|
|
5170
5411
|
const detailIds = [...active, ...recent].map((entry) => entry.id);
|
|
5171
5412
|
detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
|
|
5172
5413
|
loadNativeDetails(detailIds, previewFallbackIds, durationFallbackIds);
|
|
@@ -5191,12 +5432,32 @@ function apply(ctx) {
|
|
|
5191
5432
|
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
5192
5433
|
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);
|
|
5193
5434
|
if (sig === lastSig) return;
|
|
5194
|
-
const
|
|
5435
|
+
const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
|
|
5195
5436
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
|
5196
5437
|
const migrations = [
|
|
5197
5438
|
...movedToRecentIds(prevRenderedActiveIds, active, recent).map((id) => ({ id, from: cardsById, to: recentCardsById })),
|
|
5198
5439
|
...movedToActiveIds(prevRenderedRecentIds, active, recent).map((id) => ({ id, from: recentCardsById, to: cardsById })),
|
|
5199
5440
|
];
|
|
5441
|
+
// 活动卡带焦点迁入历史时,把键盘焦点交给其上一个仍在活动区的卡片;
|
|
5442
|
+
// 若迁移卡已在活动区首位,则退到下一张,避免焦点落到历史区或 body。
|
|
5443
|
+
let focusAfterMigrationId = null;
|
|
5444
|
+
const focusedElement = document.activeElement;
|
|
5445
|
+
const activeIds = new Set(active.map((entry) => String(entry.id)));
|
|
5446
|
+
for (const { id, from } of migrations) {
|
|
5447
|
+
if (from !== cardsById) continue;
|
|
5448
|
+
const source = from.get(id)?.el;
|
|
5449
|
+
if (source === undefined || !source.contains(focusedElement)) continue;
|
|
5450
|
+
const activeCards = [...activeList.querySelectorAll(`.${CARD_CLASS}`)];
|
|
5451
|
+
const sourceIndex = activeCards.indexOf(source);
|
|
5452
|
+
if (sourceIndex < 0) continue;
|
|
5453
|
+
const target = activeCards
|
|
5454
|
+
.slice(0, sourceIndex)
|
|
5455
|
+
.reverse()
|
|
5456
|
+
.find((card) => activeIds.has(String(card.dataset.sessionId)))
|
|
5457
|
+
?? activeCards.slice(sourceIndex + 1).find((card) => activeIds.has(String(card.dataset.sessionId)));
|
|
5458
|
+
focusAfterMigrationId = target?.dataset.sessionId ?? null;
|
|
5459
|
+
break;
|
|
5460
|
+
}
|
|
5200
5461
|
const movePlans = prepareMoveGhosts(migrations);
|
|
5201
5462
|
// 迁移帧内位置受影响的其它卡片与历史区段头:DOM 写入前量取当前视觉矩形
|
|
5202
5463
|
// (R-01-010/AC-10);reduced-motion 下 movePlans 为空,FLIP 量取整体跳过。
|
|
@@ -5210,7 +5471,7 @@ function apply(ctx) {
|
|
|
5210
5471
|
const aliveActive = new Set();
|
|
5211
5472
|
for (const [index, entry] of active.entries()) {
|
|
5212
5473
|
try {
|
|
5213
|
-
renderCardIntoList(activeList, entry, cardsById, index, 1,
|
|
5474
|
+
renderCardIntoList(activeList, entry, cardsById, index, 1, colorByWorkspace);
|
|
5214
5475
|
} catch (error) {
|
|
5215
5476
|
renderOk = false;
|
|
5216
5477
|
logCardRenderError(entry.id, error);
|
|
@@ -5227,7 +5488,7 @@ function apply(ctx) {
|
|
|
5227
5488
|
// 历史区容器首个子节点是段头(.dap-recent-head),卡片从 offset 1 开始。
|
|
5228
5489
|
for (const [index, entry] of recent.entries()) {
|
|
5229
5490
|
try {
|
|
5230
|
-
renderCardIntoList(recentSection, entry, recentCardsById, index, 1,
|
|
5491
|
+
renderCardIntoList(recentSection, entry, recentCardsById, index, 1, colorByWorkspace);
|
|
5231
5492
|
} catch (error) {
|
|
5232
5493
|
renderOk = false;
|
|
5233
5494
|
logCardRenderError(entry.id, error);
|
|
@@ -5246,6 +5507,8 @@ function apply(ctx) {
|
|
|
5246
5507
|
}
|
|
5247
5508
|
runMoveGhosts(movePlans);
|
|
5248
5509
|
if (shiftRects !== null) runShiftAnimations(shiftRects);
|
|
5510
|
+
ensureCurrentCardVisible(pane.querySelector(".dap-scroll"), snapshot?.current ?? null);
|
|
5511
|
+
if (focusAfterMigrationId !== null) cardsById.get(focusAfterMigrationId)?.el.focus();
|
|
5249
5512
|
// 区域已有条目但列表仍在途时,在区头部显示行内加载指示(R-01-014/AC-01)。
|
|
5250
5513
|
const headerEl = pane.querySelector(".dap-header");
|
|
5251
5514
|
const recentHeadEl = recentSection?.querySelector(".dap-recent-head") ?? null;
|