dsh-activity-pane 0.7.0 → 0.8.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 +943 -203
- package/package.json +1 -1
- package/scripts/acceptance.mjs +4 -0
- package/scripts/check.mjs +545 -103
- package/src/client.mjs +565 -142
- package/src/core.mjs +375 -59
- package/src/host.mjs +197 -4
- package/src/navigation.mjs +3 -2
package/src/client.mjs
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
// dsh-activity-pane 浏览器运行时。
|
|
2
2
|
//
|
|
3
|
-
// 挂载策略:把窗格作为 AppFrame 中 `
|
|
4
|
-
// (`#root [data-slot="
|
|
5
|
-
//
|
|
3
|
+
// 挂载策略:把窗格作为 AppFrame 中 keyed `main` 槽容器的前置兄弟列插入
|
|
4
|
+
// (`#root [data-slot="main"] || .parentElement` 即中列 flex,dsh 0.1.5 起原
|
|
5
|
+
// `conversation` Slot 迁移为 `main` 的 `conversation` key),让外壳的让步链
|
|
6
|
+
// 挤压中间栏;窄屏(<=767px)转为固定抽屉 + 浮动开关按钮。
|
|
6
7
|
//
|
|
7
|
-
// 数据来源:DSH 原生 `sessions` / `workspaces` 客户端服务(推送式快照)+
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
//
|
|
8
|
+
// 数据来源:DSH 原生 `sessions` / `workspaces` 客户端服务(推送式快照)+ `remote`
|
|
9
|
+
// 服务的部署级模型目录(session/modelCatalog)与会话日志分页(session/page,
|
|
10
|
+
// Session 格式 V3——dsh 0.1.5 起快照不再携带会话内容、connection.api 门面移除)+
|
|
11
|
+
// 运行中会话的原生订阅(binding().session,运行状态面)+ 可选 `modelDirectories`
|
|
12
|
+
// 目录 store 订阅(模型选择实时更新),不依赖任何第三方插件数据路由,也不做状态轮询。
|
|
11
13
|
|
|
12
14
|
const name = "dsh-activity-pane";
|
|
13
|
-
|
|
15
|
+
// remote.session:dsh 0.1.5 起会话 RPC(目录/日志分页)经 api-remotes 的点分命名空间
|
|
16
|
+
// 注入(cordis 代理对未注入属性直接抛错,父服务声明不代表子命名空间可用;仅声明
|
|
17
|
+
// 子命名空间即可解析,无需再注入父面)。
|
|
18
|
+
const inject = ["sessions", "workspaces", "uiSession", "remote.session"];
|
|
14
19
|
|
|
15
|
-
const CONVERSATION_SELECTOR = "#root [data-slot=\"
|
|
20
|
+
const CONVERSATION_SELECTOR = "#root [data-slot=\"main\"]";
|
|
16
21
|
const PANE_ATTR = "data-dsh-activity-pane";
|
|
17
22
|
const PANE_CLASS = "dap-pane";
|
|
18
23
|
const LIST_CLASS = "dap-list";
|
|
@@ -24,7 +29,7 @@ const INSTANCE_KEY = "__dshActivityPaneCleanup";
|
|
|
24
29
|
const WIDTH_STORAGE_KEY = "dsh-activity-pane:width";
|
|
25
30
|
const COLLAPSED_WIDTH = 34;
|
|
26
31
|
/** 宿主侧完成确认 API 前缀(C-030):acks 快照 / SSE 推送 / ack 写回,同源受信。 */
|
|
27
|
-
const
|
|
32
|
+
const PANE_API_BASE = "/dsh-activity-pane/api";
|
|
28
33
|
// 缩进槽宽:与连接线 CSS 几何耦合(left:-8px = INDENT_PX/2 缩进槽中线,
|
|
29
34
|
// top:-6px/bottom:-2px 对应 .dap-list 的 gap:6px),改任一数值须三处同步;
|
|
30
35
|
// scripts/check.mjs 有钉住断言。
|
|
@@ -436,6 +441,13 @@ const CSS = `
|
|
|
436
441
|
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
437
442
|
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight: 700;
|
|
438
443
|
}
|
|
444
|
+
/* 标题行最右侧的累计运行时长(R-01-020/AC-01):固定占位不参与标题挤压,
|
|
445
|
+
标题过长时以自身省略号让位;色调弱于标题,不与状态点抢视觉。 */
|
|
446
|
+
[data-dsh-activity-pane] .dap-total-time {
|
|
447
|
+
flex: none; margin-left: auto; font-size: 12px; line-height: 16px;
|
|
448
|
+
color: #8a94a3; white-space: nowrap;
|
|
449
|
+
}
|
|
450
|
+
[data-dsh-activity-pane] .dap-total-time[hidden] { display: none; }
|
|
439
451
|
/* 最近历史卡标题降为常规字重:历史区不抢占视觉强调(R-01-013/AC-09)。 */
|
|
440
452
|
[data-dsh-activity-pane] .dap-card[data-kind="recent"] .dap-title {
|
|
441
453
|
font-weight: 400;
|
|
@@ -565,6 +577,7 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
565
577
|
[data-dsh-activity-pane] .dap-progress {
|
|
566
578
|
display: flex; align-items: center; gap: 7px; min-width: 0;
|
|
567
579
|
}
|
|
580
|
+
[data-dsh-activity-pane] .dap-progress[hidden] { display: none; }
|
|
568
581
|
[data-dsh-activity-pane] .dap-progress .dap-track { flex: 1 1 auto; min-width: 0; }
|
|
569
582
|
[data-dsh-activity-pane] .dap-pct {
|
|
570
583
|
flex: none; width: 5ch; font-size: 12px; line-height: 15px; font-weight: 700; text-align: right;
|
|
@@ -1035,8 +1048,10 @@ function apply(ctx) {
|
|
|
1035
1048
|
let disposed = false;
|
|
1036
1049
|
let sessions = null;
|
|
1037
1050
|
let workspaces = null;
|
|
1051
|
+
let uiSession = null;
|
|
1038
1052
|
let sessionUnsubscribe = null;
|
|
1039
1053
|
let workspaceUnsubscribe = null;
|
|
1054
|
+
let pendingUnsubscribe = null;
|
|
1040
1055
|
let clockTimer = null;
|
|
1041
1056
|
let recentTimeTimer = null;
|
|
1042
1057
|
let syncScheduled = false;
|
|
@@ -1109,12 +1124,24 @@ function apply(ctx) {
|
|
|
1109
1124
|
const sessionDetailsById = new Map();
|
|
1110
1125
|
/** 会话跳转的单一重试链;避免重复点击叠加 refresh/timer。 */
|
|
1111
1126
|
const openRetryStates = new Map();
|
|
1112
|
-
/**
|
|
1127
|
+
/** 主会话目录一次性 load 的在途记账:id → promise(字段级加载指示消费)。 */
|
|
1113
1128
|
const modelLoads = new Map();
|
|
1129
|
+
/** native cold-session log reads, one promise per session and no polling. */
|
|
1114
1130
|
const historyLoads = new Map();
|
|
1115
1131
|
/** 模型目录订阅(R-01-012/AC-16):id → unsubscribe;模型选择切换经原生
|
|
1116
1132
|
* modelDirectories store 推送即时到达,随可见性清理/卸载先 unsubscribe 再除名。 */
|
|
1117
1133
|
const modelDirectorySubs = new Map();
|
|
1134
|
+
const logSourceSubs = new Map();
|
|
1135
|
+
/** 模型目录分组的 modelId → {name, reasoning} 索引(R-01-012/AC-17):同一部署的
|
|
1136
|
+
* 目录分组在主/子会话间共享,经主会话的目录订阅与一次性 models RPC 就地收割,
|
|
1137
|
+
* 渲染时解析显示名与 effort 回退。无原型对象:模型 id 可能恰为 "constructor" 等
|
|
1138
|
+
* 继承键名,不得穿透回退。 */
|
|
1139
|
+
const catalogEntries = Object.create(null);
|
|
1140
|
+
/** 部署级目录分组(dsh 0.1.5 起模型上下文的显示名/effort 解析来源):由
|
|
1141
|
+
* remote.sessions.modelCatalog 与 modelDirectories 订阅推送共同收割。 */
|
|
1142
|
+
let catalogGroups = [];
|
|
1143
|
+
/** 部署级 modelCatalog 一次性读取的在途 Promise:成功即缓存、失败允许重试。 */
|
|
1144
|
+
let catalogPromise = null;
|
|
1118
1145
|
/** native session.open() requests in flight; avoid duplicate cold history reads. */
|
|
1119
1146
|
/** 冷数据读取并发池:队列顺序即优先级(调用方已排序),逐个完成逐个重绘。 */
|
|
1120
1147
|
const loadQueue = [];
|
|
@@ -1281,7 +1308,7 @@ function apply(ctx) {
|
|
|
1281
1308
|
acksSource = null;
|
|
1282
1309
|
if (disposed || typeof window.EventSource !== "function") return;
|
|
1283
1310
|
try {
|
|
1284
|
-
const source = new window.EventSource(`${
|
|
1311
|
+
const source = new window.EventSource(`${PANE_API_BASE}/acks/stream`);
|
|
1285
1312
|
source.addEventListener("state", (event) => applyAcksState(event.data ?? ""));
|
|
1286
1313
|
acksSource = source;
|
|
1287
1314
|
} catch {
|
|
@@ -1311,7 +1338,7 @@ function apply(ctx) {
|
|
|
1311
1338
|
completeAcksById.set(id, { ...prev, lastTurnEnd: prev?.lastTurnEnd ?? null, ackedAt: Date.now() });
|
|
1312
1339
|
queueSync();
|
|
1313
1340
|
try {
|
|
1314
|
-
const response = await fetch(`${
|
|
1341
|
+
const response = await fetch(`${PANE_API_BASE}/ack`, {
|
|
1315
1342
|
method: "POST",
|
|
1316
1343
|
headers: { "Content-Type": "application/json" },
|
|
1317
1344
|
body: JSON.stringify({ sessionId: id }),
|
|
@@ -1325,8 +1352,137 @@ function apply(ctx) {
|
|
|
1325
1352
|
}
|
|
1326
1353
|
}
|
|
1327
1354
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1355
|
+
// ---- 累计运行时长通道(R-01-020,C-074) ----
|
|
1356
|
+
// SSE 订阅宿主侧回合统计:连接即收全量快照(刷新/重连恢复),此后每次变更(回合
|
|
1357
|
+
// 边界、回填完成)推送新全量。无 EventSource 环境静默降级为不显示累计时长。
|
|
1358
|
+
// 可见会话的懒回填经 GET /busy?ids= 触发(fire-and-forget),完成后经同一 SSE 广播。
|
|
1359
|
+
const busyById = new Map(); // sessionId -> { busyMs, openTurnStart }
|
|
1360
|
+
const busyRequestedIds = new Set(); // 已触发过回填的会话 id(宿主侧单飞,本地防重复请求)
|
|
1361
|
+
const busyRetryAtById = new Map(); // sessionId -> 回填失败后的最早重试时刻(30s 退避,不逼近轮询)
|
|
1362
|
+
let busySource = null;
|
|
1363
|
+
let busyFetchInflight = false;
|
|
1364
|
+
|
|
1365
|
+
/** SSE 全量快照应用:接受已解析对象或原始 JSON 文本;解析失败静默丢弃。 */
|
|
1366
|
+
function applyBusyState(input) {
|
|
1367
|
+
if (disposed) return;
|
|
1368
|
+
let state = typeof input === "object" ? input : null;
|
|
1369
|
+
if (state === null) {
|
|
1370
|
+
try {
|
|
1371
|
+
state = JSON.parse(input);
|
|
1372
|
+
} catch {
|
|
1373
|
+
state = null;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (state === null || typeof state !== "object") return;
|
|
1377
|
+
busyById.clear();
|
|
1378
|
+
for (const [id, record] of Object.entries(state)) {
|
|
1379
|
+
busyById.set(String(id), {
|
|
1380
|
+
busyMs: normalizeBusyMs(record?.busyMs),
|
|
1381
|
+
openTurnStart: normalizeBusyMs(record?.openTurnStart),
|
|
1382
|
+
waitedMs: normalizeBusyMs(record?.waitedMs),
|
|
1383
|
+
openWaitStart: normalizeBusyMs(record?.openWaitStart),
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
queueSync();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** 累计运行时长注入(R-01-020/AC-01、AC-03、AC-07):已完成回合累计 + 开放回合实时
|
|
1390
|
+
* 已耗时(扣除等待区间);渲染期按 now 合成,阻塞等待期间公式自冻结;子代理条目
|
|
1391
|
+
* 不注入(entry.totalBusyMs 保持 undefined → 节点隐藏)。 */
|
|
1392
|
+
function applyTotalBusy(entry, now) {
|
|
1393
|
+
const busyRecord = busyById.get(entry.id);
|
|
1394
|
+
entry.totalBusyMs = totalBusyDisplayMs({
|
|
1395
|
+
busyMs: busyRecord?.busyMs ?? null,
|
|
1396
|
+
openTurnStart: busyRecord?.openTurnStart ?? null,
|
|
1397
|
+
waitedMs: busyRecord?.waitedMs ?? null,
|
|
1398
|
+
openWaitStart: busyRecord?.openWaitStart ?? null,
|
|
1399
|
+
now,
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/** 触发可见主会话的懒回填:只对未请求过的 id 发一次 GET;在途时跳过(SSE 广播兜底)。 */
|
|
1404
|
+
function requestBusyBackfill(ids) {
|
|
1405
|
+
if (disposed || typeof fetch !== "function") return;
|
|
1406
|
+
const pending = ids.filter((id) => !busyRequestedIds.has(id) && !busyById.has(id) && Date.now() >= (busyRetryAtById.get(id) ?? 0));
|
|
1407
|
+
if (pending.length === 0 || busyFetchInflight) return;
|
|
1408
|
+
for (const id of pending) busyRequestedIds.add(id);
|
|
1409
|
+
busyFetchInflight = true;
|
|
1410
|
+
fetch(`${PANE_API_BASE}/busy?ids=${encodeURIComponent(pending.join(","))}`)
|
|
1411
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
1412
|
+
.then((state) => {
|
|
1413
|
+
if (state !== null && typeof state === "object") applyBusyState(state);
|
|
1414
|
+
})
|
|
1415
|
+
.catch(() => {
|
|
1416
|
+
// 回填触发失败:30s 退避后允许重试,不逐帧逼近轮询形态。
|
|
1417
|
+
const retryAt = Date.now() + 30_000;
|
|
1418
|
+
for (const id of pending) {
|
|
1419
|
+
busyRequestedIds.delete(id);
|
|
1420
|
+
busyRetryAtById.set(id, retryAt);
|
|
1421
|
+
}
|
|
1422
|
+
})
|
|
1423
|
+
.finally(() => {
|
|
1424
|
+
busyFetchInflight = false;
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/** (重)建 SSE 连接:先关闭旧连接再新建;连接即收宿主侧全量快照。 */
|
|
1429
|
+
function connectBusyStream() {
|
|
1430
|
+
try {
|
|
1431
|
+
busySource?.close();
|
|
1432
|
+
} catch {}
|
|
1433
|
+
busySource = null;
|
|
1434
|
+
if (disposed || typeof window.EventSource !== "function") return;
|
|
1435
|
+
try {
|
|
1436
|
+
const source = new window.EventSource(`${PANE_API_BASE}/busy/stream`);
|
|
1437
|
+
source.addEventListener("state", (event) => applyBusyState(event.data ?? ""));
|
|
1438
|
+
busySource = source;
|
|
1439
|
+
} catch {
|
|
1440
|
+
busySource = null;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
// 回到前台(含 bfcache 还原)时 busy 通道自愈:重建 SSE,借连接全量快照收敛;
|
|
1444
|
+
// 具名 handler 使 cleanup 能移除监听(与 acks 通道同模式,R-02-003)。
|
|
1445
|
+
const onBusyVisibilityResume = () => {
|
|
1446
|
+
if (document.visibilityState === "visible" && !disposed) connectBusyStream();
|
|
1447
|
+
};
|
|
1448
|
+
const onBusyPageShow = (event) => {
|
|
1449
|
+
if (event?.persisted === true && !disposed) connectBusyStream();
|
|
1450
|
+
};
|
|
1451
|
+
document.addEventListener("visibilitychange", onBusyVisibilityResume);
|
|
1452
|
+
window.addEventListener("pageshow", onBusyPageShow);
|
|
1453
|
+
connectBusyStream();
|
|
1454
|
+
|
|
1455
|
+
function remoteValue(response) {
|
|
1456
|
+
if (response?.ok === true) return response.value;
|
|
1457
|
+
throw response?.error ?? new Error("remote request failed");
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/** 会话日志分页地址(R-01-012):主会话 `{kind:"session", sessionId}`;子代理
|
|
1461
|
+
* `{kind:"subagent", parentSessionId, childSessionId, mode}`——母会话 id 兼容
|
|
1462
|
+
* `parentSessionId` / `parentId` 两种条目键名,mode 无条目标注时按一次性子代理
|
|
1463
|
+
* 处理、读取失败由 pagedHistoryEvents 以 null 收敛为空白详情(R-01-013)。 */
|
|
1464
|
+
function sessionPageAddress(id, byId) {
|
|
1465
|
+
const row = byId[id] ?? {};
|
|
1466
|
+
if (row?.origin === "subagent") {
|
|
1467
|
+
const parentId = row.parentSessionId ?? row.parentId;
|
|
1468
|
+
if (parentId !== undefined && parentId !== null) {
|
|
1469
|
+
return {
|
|
1470
|
+
kind: "subagent",
|
|
1471
|
+
parentSessionId: String(parentId),
|
|
1472
|
+
childSessionId: String(id),
|
|
1473
|
+
mode: row.continuable === true ? "continuable" : "one-shot",
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return { kind: "session", sessionId: String(id) };
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/** 部署级目录分组收割(R-01-012/AC-01):modelCatalog RPC 与 modelDirectories
|
|
1481
|
+
* 目录订阅推送共享同一部署目录,后到覆盖(分组集合一致);展平索引进
|
|
1482
|
+
* `catalogEntries` 供子代理溯源 id 解析显示名。空分组不覆盖既有缓存。 */
|
|
1483
|
+
function harvestCatalog(groups) {
|
|
1484
|
+
if (Array.isArray(groups) && groups.length > 0) catalogGroups = groups;
|
|
1485
|
+
Object.assign(catalogEntries, catalogModelEntries(groups));
|
|
1330
1486
|
}
|
|
1331
1487
|
|
|
1332
1488
|
/** 同帧重启等待提醒动画:数量胶囊保持亮度呼吸,卡片末行保持 opacity 脉冲,
|
|
@@ -1351,16 +1507,17 @@ function apply(ctx) {
|
|
|
1351
1507
|
try {
|
|
1352
1508
|
directory = ctx.get("modelDirectories")?.directoryFor?.(id) ?? null;
|
|
1353
1509
|
} catch {
|
|
1354
|
-
directory = null; // 会话无 scope
|
|
1510
|
+
directory = null; // 会话无 scope:回落日志提取
|
|
1355
1511
|
}
|
|
1356
1512
|
if (directory === null) return;
|
|
1357
1513
|
const syncFromDirectory = () => {
|
|
1358
1514
|
if (disposed) return;
|
|
1359
1515
|
const snap = directory.store?.getSnapshot?.();
|
|
1360
1516
|
if (!snap?.current) return; // 目录未就绪:不覆写既有取值
|
|
1517
|
+
harvestCatalog(snap.groups);
|
|
1361
1518
|
detail.models = { current: snap.current, groups: snap.groups ?? [] };
|
|
1362
1519
|
detail.model = modelMetadata(detail.models);
|
|
1363
|
-
//
|
|
1520
|
+
// 订阅已产值标记:晚到的一次性快照不得回写切换前的旧值。
|
|
1364
1521
|
detail.modelLive = true;
|
|
1365
1522
|
queueSync();
|
|
1366
1523
|
};
|
|
@@ -1368,17 +1525,48 @@ function apply(ctx) {
|
|
|
1368
1525
|
try {
|
|
1369
1526
|
unsubscribe = directory.store.subscribe(syncFromDirectory);
|
|
1370
1527
|
} catch {
|
|
1371
|
-
return; //
|
|
1528
|
+
return; // 订阅失败:保持日志提取结果
|
|
1372
1529
|
}
|
|
1373
1530
|
modelDirectorySubs.set(id, unsubscribe);
|
|
1374
|
-
syncFromDirectory(); //
|
|
1531
|
+
syncFromDirectory(); // 目录已被主窗口加载时立即同步,该会话免发一次性读取
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/** 目录 store 一次性 load(dsh 0.1.5 起惰性加载:不 load 不产出当前选择,原生模型
|
|
1535
|
+
* 选择器同样先 load)。load 与 select() 的 generation 竞争以「最新操作胜出」,用户
|
|
1536
|
+
* 切换后到仍胜出。e2e 接缝(dap-e2e-model-delay)延迟 load 发起,使模型上下文
|
|
1537
|
+
* 严格晚于时间线呈现,渐进渲染可观察(R-01-014/AC-03)。 */
|
|
1538
|
+
function loadDirectoryOnce(id) {
|
|
1539
|
+
if (disposed || !modelDirectorySubs.has(id) || modelLoads.has(id)) return;
|
|
1540
|
+
let directory = null;
|
|
1541
|
+
try {
|
|
1542
|
+
directory = ctx.get("modelDirectories")?.directoryFor?.(id) ?? null;
|
|
1543
|
+
} catch {
|
|
1544
|
+
directory = null;
|
|
1545
|
+
}
|
|
1546
|
+
if (directory === null) return;
|
|
1547
|
+
try {
|
|
1548
|
+
const loadPromise = delayedModelCall(() => (typeof directory.load === "function" ? directory.load() : null));
|
|
1549
|
+
if (loadPromise && typeof loadPromise.finally === "function") {
|
|
1550
|
+
modelLoads.set(id, loadPromise);
|
|
1551
|
+
// 守卫只在失败时释放(放行下一次渲染重试);成功后保持到可见性 prune,
|
|
1552
|
+
// 兑现一次性 load 语义。若 settle 即释放,目录 store 的每次广播
|
|
1553
|
+
// (syncInputs 无条件以新引用 set)都会再驱动 render → load → 广播,
|
|
1554
|
+
// 形成 rAF 速率反馈环——空闲期 60fps 满载,手机端表现为持续发烫。
|
|
1555
|
+
loadPromise.catch(() => {
|
|
1556
|
+
if (modelLoads.get(id) === loadPromise) modelLoads.delete(id);
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
loadPromise?.catch(() => {});
|
|
1560
|
+
} catch {
|
|
1561
|
+
// load 不可用:保持订阅,等待主窗口加载后的推送。
|
|
1562
|
+
}
|
|
1375
1563
|
}
|
|
1376
1564
|
|
|
1377
|
-
function loadNativeDetails(ids, previewFallbackIds = new Set(), durationFallbackIds = new Set()) {
|
|
1378
|
-
const
|
|
1379
|
-
if (!
|
|
1565
|
+
function loadNativeDetails({ ids, previewFallbackIds = new Set(), durationFallbackIds = new Set(), subagentModelReadIds = new Set() }) {
|
|
1566
|
+
const sessionRemote = ctx.get("remote.session") ?? null;
|
|
1567
|
+
if (!sessionRemote) return;
|
|
1568
|
+
ensureCatalogGroups(sessionRemote);
|
|
1380
1569
|
const byId = getSnapshot(sessions, "list")?.byId ?? {};
|
|
1381
|
-
const modelPromises = [];
|
|
1382
1570
|
const historyPromises = [];
|
|
1383
1571
|
for (const id of ids) {
|
|
1384
1572
|
const detail = sessionDetailsById.get(id) ?? {};
|
|
@@ -1386,80 +1574,55 @@ function apply(ctx) {
|
|
|
1386
1574
|
if (liveSnapshot) detail.snapshot = liveSnapshot;
|
|
1387
1575
|
sessionDetailsById.set(id, detail);
|
|
1388
1576
|
const subagent = isSubagentRow(byId[id], byId);
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
const plan = detailLoadPlan({
|
|
1405
|
-
detail,
|
|
1406
|
-
isSubagent: subagent,
|
|
1407
|
-
snapshotReady,
|
|
1408
|
-
historyNeeded: needsHistorySnapshot(detail.snapshot),
|
|
1409
|
-
previewFallbackNeeded: previewFallbackIds.has(id),
|
|
1410
|
-
durationFallbackNeeded,
|
|
1411
|
-
windowComplete,
|
|
1412
|
-
modelInflight: modelLoads.has(id),
|
|
1413
|
-
historyInflight: historyLoads.has(id) || sessionOpenLoads.has(id),
|
|
1414
|
-
});
|
|
1415
|
-
if (plan.subagent) {
|
|
1416
|
-
// 子代理的 models 读取必被宿主以 agent-busy 拒绝:直接留空,不发注定失败的 RPC。
|
|
1417
|
-
detail.model ??= { model: "", reasoning: "" };
|
|
1418
|
-
} else if (plan.model && typeof api.models === "function") {
|
|
1419
|
-
const promise = enqueueDetailLoad(() => delayedModelCall(() => api.models({ sessionId: id }))
|
|
1420
|
-
.then((response) => {
|
|
1421
|
-
const value = apiValue(response);
|
|
1422
|
-
if (!value) {
|
|
1423
|
-
if (!detail.modelLive) detail.model = { model: "", reasoning: "" };
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
// 目录订阅已产出更新的当前选择时,晚到的 RPC 快照不得回写旧值(R-01-012/AC-16)。
|
|
1427
|
-
if (detail.modelLive) return;
|
|
1428
|
-
detail.models = value;
|
|
1429
|
-
detail.model = modelMetadata(value);
|
|
1430
|
-
})
|
|
1431
|
-
.catch((error) => {
|
|
1432
|
-
if (!detail.modelLive) detail.model = { model: "", reasoning: "" };
|
|
1433
|
-
detail.modelError = error instanceof Error ? error.message : String(error);
|
|
1434
|
-
}));
|
|
1435
|
-
modelLoads.set(id, promise);
|
|
1436
|
-
// settle 即移除记账:在途判定驱动加载指示,残留会让空字段永久误报加载。
|
|
1437
|
-
promise.finally(() => {
|
|
1438
|
-
if (modelLoads.get(id) === promise) modelLoads.delete(id);
|
|
1439
|
-
});
|
|
1440
|
-
modelPromises.push(promise);
|
|
1577
|
+
// 主会话建立模型目录订阅:dsh 0.1.5 起目录 store 惰性加载(不 load 不产出当前
|
|
1578
|
+
// 选择),订阅与一次性 load 一并触发;子代理的目录不可用(宿主以 agent-busy
|
|
1579
|
+
// 拒绝其模型 RPC),不建立订阅。e2e 接缝把订阅与 load 整体延后,使模型上下文
|
|
1580
|
+
// 严格晚于时间线呈现(R-01-014/AC-03 渐进语义可观察)。
|
|
1581
|
+
if (!subagent) {
|
|
1582
|
+
if (e2eModelDelayMs === 0) {
|
|
1583
|
+
subscribeModelDirectory(id, detail);
|
|
1584
|
+
loadDirectoryOnce(id);
|
|
1585
|
+
} else {
|
|
1586
|
+
delayedModelCall(() => {
|
|
1587
|
+
subscribeModelDirectory(id, detail);
|
|
1588
|
+
loadDirectoryOnce(id);
|
|
1589
|
+
return null;
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1441
1592
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1593
|
+
captureSessionLog(id, { subagent, cwd: byId[id]?.cwd ?? "" });
|
|
1594
|
+
// 长会话深读兜底(R-01-013/AC-03):最近卡预览/子代理溯源不在尾页日志窗口内
|
|
1595
|
+
// 且宿主标记 hasMore 时,按 beforeSeq 向前回溯翻页(默认无页数上限)。
|
|
1596
|
+
const windowEntries = Array.isArray(detail.log?.entries) ? detail.log.entries : [];
|
|
1597
|
+
const lastSeq = Number(windowEntries.at(-1)?.event?.seq);
|
|
1598
|
+
const deepReadNeeded =
|
|
1599
|
+
(previewFallbackIds.has(id) || durationFallbackIds.has(id) || (subagent && subagentModelReadIds.has(id))) &&
|
|
1600
|
+
detail.log?.hasMore === true &&
|
|
1601
|
+
Number.isFinite(lastSeq) &&
|
|
1602
|
+
detail.historyDeepReadDone !== true;
|
|
1603
|
+
// 在途守卫:深翻未在途才入队。渲染逐帧发生而 deepReadNeeded 在深翻完成前恒真,
|
|
1604
|
+
// 无守卫时每帧重复入队新 job,队列以 LOAD_CONCURRENCY 并发放大发——同一会话的
|
|
1605
|
+
// page RPC 风暴,且 historyLoads 的 set/delete 随 job 翻转驱动加载指示高频闪烁。
|
|
1606
|
+
if (deepReadNeeded && !historyLoads.has(id)) {
|
|
1607
|
+
detail.previewFallbackLoaded = true;
|
|
1608
|
+
detail.durationFallbackLoaded = true;
|
|
1609
|
+
const address = sessionPageAddress(id, byId);
|
|
1445
1610
|
const promise = enqueueDetailLoad(() => Promise.resolve()
|
|
1446
1611
|
.then(async () => {
|
|
1447
|
-
// 单池任务内串行回溯深翻(默认无页数上限):向前翻到命中最近一条
|
|
1448
|
-
// 用户消息或翻尽为止——超长会话的最后用户消息可能在尾页窗口之外,
|
|
1449
|
-
// 固定页数上限会让历史卡用户预览永久缺失(R-01-013/AC-03 回溯承诺)。
|
|
1450
|
-
// 运行会话缺窗口内回合起点时要求深翻至命中开放回合 turn/start
|
|
1451
|
-
// (R-01-009/AC-06 冷窗口兜底)。
|
|
1452
1612
|
const { events, error } = await pagedHistoryEvents({
|
|
1453
|
-
fetchPage: async (beforeSeq) =>
|
|
1454
|
-
|
|
1613
|
+
fetchPage: async (beforeSeq) => {
|
|
1614
|
+
const value = remoteValue(
|
|
1615
|
+
await sessionRemote.page({ address, throughSeq: lastSeq, beforeSeq, maxMessages: 50 }),
|
|
1616
|
+
);
|
|
1617
|
+
if (!value) return null;
|
|
1618
|
+
return { events: Array.isArray(value.records) ? value.records : [], hasMore: value.hasMore === true };
|
|
1619
|
+
},
|
|
1455
1620
|
});
|
|
1456
1621
|
if (error) detail.historyError = error instanceof Error ? error.message : String(error);
|
|
1457
1622
|
detail.history = events;
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
detail.previews = messagePreviews({ history: events });
|
|
1462
|
-
}))
|
|
1623
|
+
detail.historyDeepReadDone = true;
|
|
1624
|
+
applyLogEvents(id, detail, events, { subagent, cwd: byId[id]?.cwd ?? "", planSubagentModelRead: subagent });
|
|
1625
|
+
}));
|
|
1463
1626
|
historyLoads.set(id, promise);
|
|
1464
1627
|
promise.finally(() => {
|
|
1465
1628
|
if (historyLoads.get(id) === promise) historyLoads.delete(id);
|
|
@@ -1467,7 +1630,7 @@ function apply(ctx) {
|
|
|
1467
1630
|
historyPromises.push(promise);
|
|
1468
1631
|
}
|
|
1469
1632
|
}
|
|
1470
|
-
const pending =
|
|
1633
|
+
const pending = historyPromises;
|
|
1471
1634
|
if (pending.length > 0) {
|
|
1472
1635
|
// 逐个完成即重绘(先就绪先显示,不等待全部,R-01-014/AC-03);
|
|
1473
1636
|
// 并立即重绘一次让加载指示在数据返回前出现。
|
|
@@ -1476,15 +1639,115 @@ function apply(ctx) {
|
|
|
1476
1639
|
}
|
|
1477
1640
|
}
|
|
1478
1641
|
|
|
1642
|
+
/** 日志页落地后的详情派生(R-01-012、R-01-017):时间线折叠、消息预览与子代理模型上下文。
|
|
1643
|
+
* 子代理沿用溯源链(assistant/message source.model + 请求头 effort,T-122);主会话模型
|
|
1644
|
+
* 不经日志提取(目录链订阅 + 一次性 load 承载,见 installServiceSubscriptions 注)。
|
|
1645
|
+
* 无请求头/无溯源时保持空白,不以部署默认或其它会话取值冒充(R-01-012/AC-18)。 */
|
|
1646
|
+
function applyLogEvents(id, detail, events, { subagent, cwd = "", planSubagentModelRead = false }) {
|
|
1647
|
+
detail.history = events;
|
|
1648
|
+
detail.timeline = foldedHistoryTimeline(events, 4, cwd);
|
|
1649
|
+
detail.previews = messagePreviews({ history: events });
|
|
1650
|
+
if (subagent) {
|
|
1651
|
+
if (planSubagentModelRead) {
|
|
1652
|
+
detail.modelReadDone = true;
|
|
1653
|
+
if (!detail.model) detail.model = modelFromHistoryEvents(events);
|
|
1654
|
+
} else if (!detail.model) {
|
|
1655
|
+
detail.model = modelFromHistoryEvents(events);
|
|
1656
|
+
}
|
|
1657
|
+
if (detail.model && !detail.model.reasoning) {
|
|
1658
|
+
detail.model.reasoning = reasoningEffortFromHistoryEvents(events) ?? "";
|
|
1659
|
+
}
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
// 主会话模型上下文不经日志提取:以 modelDirectories 目录订阅为准(实时推送
|
|
1663
|
+
// 当前选择),不可得时保持空白,不以日志历史取值或部署默认冒充(R-01-012/AC-18)。
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/** 绑定会话并水合事件源(dsh 0.1.5 起会话内容经 eventSource 流式下发:打开即收
|
|
1667
|
+
* 完整日志窗口 + 实时尾,原生 Conversation 同源)。冷会话补一次 open(),日志窗口
|
|
1668
|
+
* 快照引用变化即重派生详情(时间线/预览/模型),窗口由宿主按消息对齐分页。 */
|
|
1669
|
+
function captureSessionLog(id, { subagent, cwd }) {
|
|
1670
|
+
const detail = sessionDetailsById.get(id) ?? {};
|
|
1671
|
+
sessionDetailsById.set(id, detail);
|
|
1672
|
+
let session = null;
|
|
1673
|
+
try {
|
|
1674
|
+
session = sessions?.binding?.(id)?.session ?? null;
|
|
1675
|
+
} catch {
|
|
1676
|
+
session = null;
|
|
1677
|
+
}
|
|
1678
|
+
if (session === null) return;
|
|
1679
|
+
// 日志窗口无效化订阅(事件驱动,不构成轮询,R-02-004):等待/历史卡无会话状态
|
|
1680
|
+
// 订阅(syncLiveness 只订阅运行中),窗口推进(回合收尾、流式尾)必须经此回调
|
|
1681
|
+
// 重读快照并重绘;否则窗口更新只能靠渲染期重读兜底——渲染一旦静默(一次性
|
|
1682
|
+
// load 修复后),完成提醒耗时等窗口派生数据将永久停摆。随可见性 prune。
|
|
1683
|
+
if (typeof session.eventSource?.subscribe === "function" && !logSourceSubs.has(id)) {
|
|
1684
|
+
try {
|
|
1685
|
+
logSourceSubs.set(
|
|
1686
|
+
id,
|
|
1687
|
+
session.eventSource.subscribe(() => {
|
|
1688
|
+
if (disposed) return;
|
|
1689
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
1690
|
+
captureSessionLog(id, {
|
|
1691
|
+
subagent: isSubagentRow(listSnap?.byId?.[id], listSnap ?? {}),
|
|
1692
|
+
cwd: listSnap?.byId?.[id]?.cwd ?? "",
|
|
1693
|
+
});
|
|
1694
|
+
queueSync();
|
|
1695
|
+
}),
|
|
1696
|
+
);
|
|
1697
|
+
} catch {
|
|
1698
|
+
// 订阅不可用:回退渲染期重读。
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
// 日志窗口缺席才发起 open:非订阅会话(等待/历史卡)无快照推送,openState 永远
|
|
1703
|
+
// 不可知,只以 openState 把关会对已水合会话逐帧重发 open,settle→delete→重发
|
|
1704
|
+
// 的在途翻转同样驱动加载指示抖动(docsim 卡闪烁根因之一)。
|
|
1705
|
+
if (!detail.log && detail.snapshot?.openState !== "open" && !sessionOpenLoads.has(id) && typeof session.open === "function") {
|
|
1706
|
+
const opening = Promise.resolve(session.open()).catch(() => {});
|
|
1707
|
+
sessionOpenLoads.set(id, opening);
|
|
1708
|
+
opening.finally(() => {
|
|
1709
|
+
if (sessionOpenLoads.get(id) === opening) sessionOpenLoads.delete(id);
|
|
1710
|
+
queueSync();
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
} catch {
|
|
1714
|
+
// open 不可用:保持当前窗口。
|
|
1715
|
+
}
|
|
1716
|
+
const log = session.eventSource?.getSnapshot?.() ?? null;
|
|
1717
|
+
if (log === detail.log) return;
|
|
1718
|
+
detail.log = log;
|
|
1719
|
+
const entries = Array.isArray(log?.entries) ? log.entries : [];
|
|
1720
|
+
applyLogEvents(id, detail, entries, { subagent, cwd });
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** 部署级模型目录一次性读取(R-01-012/AC-01):dsh 0.1.5 起 per-session models
|
|
1724
|
+
* RPC 移除,目录经 `remote.session.modelCatalog()` 无参数读取;成功即缓存不再
|
|
1725
|
+
* 重发,失败留待下轮详情读取重试。 */
|
|
1726
|
+
function ensureCatalogGroups(sessionRemote) {
|
|
1727
|
+
if (catalogGroups.length > 0 || catalogPromise !== null) return;
|
|
1728
|
+
catalogPromise = Promise.resolve()
|
|
1729
|
+
.then(() => remoteValue(sessionRemote.modelCatalog()))
|
|
1730
|
+
.then((value) => {
|
|
1731
|
+
if (value && Array.isArray(value.groups)) harvestCatalog(value.groups);
|
|
1732
|
+
})
|
|
1733
|
+
.catch(() => {})
|
|
1734
|
+
.finally(() => {
|
|
1735
|
+
catalogPromise = null;
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1479
1739
|
function installServiceSubscriptions() {
|
|
1480
1740
|
const nextSessions = ctx.get("sessions");
|
|
1481
1741
|
const nextWorkspaces = ctx.get("workspaces");
|
|
1482
|
-
|
|
1742
|
+
const nextUiSession = ctx.get("uiSession");
|
|
1743
|
+
if (nextSessions === sessions && nextWorkspaces === workspaces && nextUiSession === uiSession) return;
|
|
1483
1744
|
|
|
1484
1745
|
sessionUnsubscribe?.();
|
|
1485
1746
|
workspaceUnsubscribe?.();
|
|
1747
|
+
pendingUnsubscribe?.();
|
|
1486
1748
|
sessions = nextSessions ?? null;
|
|
1487
1749
|
workspaces = nextWorkspaces ?? null;
|
|
1750
|
+
uiSession = nextUiSession ?? null;
|
|
1488
1751
|
try {
|
|
1489
1752
|
sessionUnsubscribe = sessions?.list?.subscribe?.(queueSync) ?? null;
|
|
1490
1753
|
} catch {
|
|
@@ -1495,6 +1758,14 @@ function apply(ctx) {
|
|
|
1495
1758
|
} catch {
|
|
1496
1759
|
workspaceUnsubscribe = null;
|
|
1497
1760
|
}
|
|
1761
|
+
try {
|
|
1762
|
+
// 0.1.5 起 sessions 行不再承载 pendingInteraction:待确认/待审查/待回复改由
|
|
1763
|
+
// uiSession.pendingInteractions(Map<sessionId, interaction>)独立承载,
|
|
1764
|
+
// 订阅其快照变化驱动等待卡重算(R-01-002/AC-03)。
|
|
1765
|
+
pendingUnsubscribe = uiSession?.pendingInteractions?.subscribe?.(queueSync) ?? null;
|
|
1766
|
+
} catch {
|
|
1767
|
+
pendingUnsubscribe = null;
|
|
1768
|
+
}
|
|
1498
1769
|
|
|
1499
1770
|
queueSync();
|
|
1500
1771
|
}
|
|
@@ -1750,6 +2021,23 @@ function apply(ctx) {
|
|
|
1750
2021
|
foot.append(capsule, noteRow);
|
|
1751
2022
|
}
|
|
1752
2023
|
|
|
2024
|
+
/** 进度行骨架(运行卡与子代理卡共用,R-01-009/AC-06、AC-14):可伸缩轨道 + 固定宽百分比。 */
|
|
2025
|
+
function makeProgressRow() {
|
|
2026
|
+
const track = makeEl("div", "dap-track");
|
|
2027
|
+
track.append(makeEl("div", "dap-fill"));
|
|
2028
|
+
const progressRow = makeEl("div", "dap-progress");
|
|
2029
|
+
progressRow.append(track, makeEl("span", "dap-pct"));
|
|
2030
|
+
return progressRow;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/** 统计行骨架:左列 token/速率/命中率(超长省略号截断),时长固定最右(R-01-009/AC-05)。 */
|
|
2034
|
+
function makeStatsRow() {
|
|
2035
|
+
const statsRow = makeEl("div", "dap-token-stats");
|
|
2036
|
+
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
2037
|
+
statsRow.hidden = true;
|
|
2038
|
+
return statsRow;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
1753
2041
|
/** 静态骨架卡片;动态文本一律走 textContent,规避 HTML 注入。 */
|
|
1754
2042
|
function cardChildren(kind) {
|
|
1755
2043
|
const head = makeEl("div", "dap-card-head");
|
|
@@ -1757,15 +2045,22 @@ function apply(ctx) {
|
|
|
1757
2045
|
const workspaceIcon = makeEl("span", "dap-workspace-icon");
|
|
1758
2046
|
workspaceIcon.append(createWorkspaceFolderIcon());
|
|
1759
2047
|
workspace.append(workspaceIcon, makeEl("span", "dap-workspace-text"));
|
|
1760
|
-
|
|
2048
|
+
const model = makeEl("div", "dap-model");
|
|
2049
|
+
head.append(workspace, model);
|
|
1761
2050
|
if (kind === "subagent") {
|
|
1762
2051
|
const row = makeEl("div", "dap-row");
|
|
1763
|
-
|
|
1764
|
-
|
|
2052
|
+
// 模型名与标题同行右对齐(东家反馈):子代理卡无工作区徽标与累计时长,
|
|
2053
|
+
// 标题行右侧即模型位;空值时区域隐藏不占位(R-01-012/AC-17、AC-18)。
|
|
2054
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), model, makeEl("span", "dap-total-time"));
|
|
2055
|
+
// 运行中子代理卡与主会话运行卡同构的进度行与统计行(R-01-009/AC-14);
|
|
2056
|
+
// 非运行时进度行隐藏、统计行冻结(R-01-009/AC-15)。
|
|
2057
|
+
const progressRow = makeProgressRow();
|
|
2058
|
+
progressRow.hidden = true;
|
|
2059
|
+
return [row, makeEl("div", "dap-subtrace"), progressRow, makeStatsRow()];
|
|
1765
2060
|
}
|
|
1766
2061
|
if (kind === "recent") {
|
|
1767
2062
|
const row = makeEl("div", "dap-row");
|
|
1768
|
-
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
2063
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-total-time"));
|
|
1769
2064
|
const userLine = makeEl("div", "dap-history-line");
|
|
1770
2065
|
userLine.dataset.role = "user";
|
|
1771
2066
|
const userIcon = makeEl("span", "dap-history-icon");
|
|
@@ -1780,14 +2075,11 @@ function apply(ctx) {
|
|
|
1780
2075
|
const agentLabel = makeEl("span", "dap-history-label");
|
|
1781
2076
|
agentLabel.textContent = "助手";
|
|
1782
2077
|
agentLine.append(agentIcon, agentLabel, makeEl("span", "dap-history-separator"), makeEl("span", "dap-history-text"));
|
|
1783
|
-
|
|
1784
|
-
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
1785
|
-
statsRow.hidden = true;
|
|
1786
|
-
return [head, row, userLine, agentLine, statsRow, makeEl("div", "dap-note")];
|
|
2078
|
+
return [head, row, userLine, agentLine, makeStatsRow(), makeEl("div", "dap-note")];
|
|
1787
2079
|
}
|
|
1788
2080
|
if (kind === "awaiting") {
|
|
1789
2081
|
const row = makeEl("div", "dap-row");
|
|
1790
|
-
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
2082
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-total-time"));
|
|
1791
2083
|
// 等待三类末行结构(R-01-002/AC-08、AC-09,C-043):首行「类型胶囊」(圆底类型
|
|
1792
2084
|
// 图标 + 类型文字),其下为正文行——阻塞/错误的说明文字,完成提醒的
|
|
1793
2085
|
// 「继续对话,或移入历史」+ 行尾「移入历史」按钮。
|
|
@@ -1798,24 +2090,14 @@ function apply(ctx) {
|
|
|
1798
2090
|
noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
|
|
1799
2091
|
const awaitHead = makeEl("div", "dap-await-head");
|
|
1800
2092
|
awaitHead.append(capsule);
|
|
1801
|
-
const statsRow = makeEl("div", "dap-token-stats");
|
|
1802
|
-
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
1803
|
-
statsRow.hidden = true;
|
|
1804
2093
|
const foot = makeEl("div", "dap-foot");
|
|
1805
2094
|
foot.append(awaitHead, noteRow);
|
|
1806
|
-
return [head, row, makeEl("div", "dap-trace"),
|
|
2095
|
+
return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];
|
|
1807
2096
|
}
|
|
1808
2097
|
const row = makeEl("div", "dap-row");
|
|
1809
|
-
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
const progressRow = makeEl("div", "dap-progress");
|
|
1813
|
-
progressRow.append(track, makeEl("span", "dap-pct"));
|
|
1814
|
-
// 统计行双段结构:左列 token/速率/命中率(超长省略号截断),时长固定最右(R-01-009/AC-05)。
|
|
1815
|
-
const statsRow = makeEl("div", "dap-token-stats");
|
|
1816
|
-
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
1817
|
-
statsRow.hidden = true;
|
|
1818
|
-
return [head, row, makeEl("div", "dap-trace"), progressRow, statsRow];
|
|
2098
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-total-time"));
|
|
2099
|
+
const progressRow = makeProgressRow();
|
|
2100
|
+
return [head, row, makeEl("div", "dap-trace"), progressRow, makeStatsRow()];
|
|
1819
2101
|
}
|
|
1820
2102
|
|
|
1821
2103
|
function createInlineIcon({ viewBox, width = 12, height = 12, parts }) {
|
|
@@ -2164,6 +2446,28 @@ function apply(ctx) {
|
|
|
2164
2446
|
}
|
|
2165
2447
|
}
|
|
2166
2448
|
|
|
2449
|
+
/** 进度行写入(运行卡与子代理卡共用,R-01-009/AC-06、AC-14):百分比与填充宽度,未变化跳过 DOM 写。 */
|
|
2450
|
+
function renderProgressRow(el, progress) {
|
|
2451
|
+
const pct = el.querySelector(".dap-pct");
|
|
2452
|
+
if (pct !== null) pct.textContent = `${Math.round(progress ?? 0)}%`;
|
|
2453
|
+
const fill = el.querySelector(".dap-fill");
|
|
2454
|
+
if (fill !== null) {
|
|
2455
|
+
const width = `${Math.min(100, Math.max(0, progress ?? 0))}%`;
|
|
2456
|
+
if (fill.style.width !== width) fill.style.width = width;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/** 最近回合耗时 memo(等待卡与暂停子代理卡共用,R-01-009/AC-12、AC-15):快照/历史引用不变即命中缓存。 */
|
|
2461
|
+
function memoTurnDuration(detail, detailSnapshot) {
|
|
2462
|
+
const history = detail.history ?? null;
|
|
2463
|
+
if (detail.memoTurnDurationSnapshotOf !== detailSnapshot || detail.memoTurnDurationHistoryOf !== history) {
|
|
2464
|
+
detail.memoTurnDurationSnapshotOf = detailSnapshot;
|
|
2465
|
+
detail.memoTurnDurationHistoryOf = history;
|
|
2466
|
+
detail.memoTurnDuration = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history });
|
|
2467
|
+
}
|
|
2468
|
+
return detail.memoTurnDuration ?? null;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2167
2471
|
/** 统计行渲染:运行卡、等待卡与最近卡写入速率、token 与耗时,旧骨架缺节点时就地补齐。 */
|
|
2168
2472
|
function renderTokenStats(el, entry) {
|
|
2169
2473
|
let stats = el.querySelector(".dap-token-stats");
|
|
@@ -2278,6 +2582,15 @@ function apply(ctx) {
|
|
|
2278
2582
|
const title = el.querySelector(".dap-title");
|
|
2279
2583
|
if (title !== null && title.textContent !== entry.title)
|
|
2280
2584
|
title.textContent = entry.title;
|
|
2585
|
+
// 标题行最右侧的累计运行时长(R-01-020/AC-01、AC-06):有数据才显示,
|
|
2586
|
+
// 无数据(含子代理卡)隐藏节点,不以 0 冒充。
|
|
2587
|
+
const totalTime = el.querySelector(".dap-total-time");
|
|
2588
|
+
if (totalTime !== null) {
|
|
2589
|
+
const totalText = Number.isFinite(entry.totalBusyMs) && entry.totalBusyMs >= 0 ? fmtElapsedMs(entry.totalBusyMs) : "";
|
|
2590
|
+
if (totalTime.textContent !== totalText) totalTime.textContent = totalText;
|
|
2591
|
+
const totalHidden = totalText === "";
|
|
2592
|
+
if (totalTime.hidden !== totalHidden) totalTime.hidden = totalHidden;
|
|
2593
|
+
}
|
|
2281
2594
|
|
|
2282
2595
|
const capsule = el.querySelector(".dap-capsule");
|
|
2283
2596
|
// 陈旧骨架就地迁移(C-043 热装兼容):旧版「正文行 + 行尾类型徽标」骨架升级为
|
|
@@ -2316,16 +2629,9 @@ function apply(ctx) {
|
|
|
2316
2629
|
}
|
|
2317
2630
|
|
|
2318
2631
|
if (entry.kind === "running") {
|
|
2319
|
-
|
|
2320
|
-
if (pct !== null)
|
|
2321
|
-
pct.textContent = `${Math.round(entry.progress ?? 0)}%`;
|
|
2632
|
+
renderProgressRow(el, entry.progress);
|
|
2322
2633
|
const traceContainer = el.querySelector(".dap-trace");
|
|
2323
2634
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
2324
|
-
const fill = el.querySelector(".dap-fill");
|
|
2325
|
-
if (fill !== null) {
|
|
2326
|
-
const width = `${Math.min(100, Math.max(0, entry.progress ?? 0))}%`;
|
|
2327
|
-
if (fill.style.width !== width) fill.style.width = width;
|
|
2328
|
-
}
|
|
2329
2635
|
renderTokenStats(el, entry);
|
|
2330
2636
|
return;
|
|
2331
2637
|
}
|
|
@@ -2333,6 +2639,17 @@ function apply(ctx) {
|
|
|
2333
2639
|
if (entry.kind === "subagent") {
|
|
2334
2640
|
const traceContainer = el.querySelector(".dap-subtrace");
|
|
2335
2641
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: true });
|
|
2642
|
+
// 运行中呈现与运行卡同构的进度行;锚点空闲(暂停等待)时整行隐藏(R-01-009/AC-15)。
|
|
2643
|
+
const progressRow = el.querySelector(".dap-progress");
|
|
2644
|
+
if (progressRow !== null) {
|
|
2645
|
+
if (Number.isFinite(entry.progress)) {
|
|
2646
|
+
renderProgressRow(el, entry.progress);
|
|
2647
|
+
if (progressRow.hidden) progressRow.hidden = false;
|
|
2648
|
+
} else if (!progressRow.hidden) {
|
|
2649
|
+
progressRow.hidden = true;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
renderTokenStats(el, entry);
|
|
2336
2653
|
return;
|
|
2337
2654
|
}
|
|
2338
2655
|
|
|
@@ -2420,6 +2737,10 @@ function apply(ctx) {
|
|
|
2420
2737
|
const detail = sessionDetailsById.get(id) ?? {};
|
|
2421
2738
|
detail.snapshot = snapshot;
|
|
2422
2739
|
sessionDetailsById.set(id, detail);
|
|
2740
|
+
// dsh 0.1.5 起快照不再携带会话内容:时间线经 eventSource 日志窗口
|
|
2741
|
+
// 就地重派生(R-01-009)。
|
|
2742
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
2743
|
+
captureSessionLog(id, { subagent: isSubagentRow(listSnap?.byId?.[id], listSnap ?? {}), cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
2423
2744
|
queueSync();
|
|
2424
2745
|
});
|
|
2425
2746
|
} catch {
|
|
@@ -2942,6 +3263,19 @@ function apply(ctx) {
|
|
|
2942
3263
|
}
|
|
2943
3264
|
}
|
|
2944
3265
|
const listState = listLoadState(snapshot);
|
|
3266
|
+
// 0.1.5 sessions 行不再承载 pendingInteraction:从 uiSession.pendingInteractions
|
|
3267
|
+
// 快照(Map<sessionId, interaction>,kind ∈ approval/plan-review/question)回填到
|
|
3268
|
+
// 行副本上,等待卡分类(awaiting/blocked/提问中)保持既有单一口径(R-01-002/AC-03)。
|
|
3269
|
+
const pendingSnapshot = uiSession?.pendingInteractions?.getSnapshot?.() ?? null;
|
|
3270
|
+
if (pendingSnapshot instanceof Map && pendingSnapshot.size > 0 && snapshot?.byId) {
|
|
3271
|
+
const byId = { ...snapshot.byId };
|
|
3272
|
+
for (const [id, interaction] of pendingSnapshot) {
|
|
3273
|
+
const row = byId[id];
|
|
3274
|
+
if (!isRecord(row) || typeof interaction?.kind !== "string") continue;
|
|
3275
|
+
byId[id] = { ...row, pendingInteraction: interaction.kind };
|
|
3276
|
+
}
|
|
3277
|
+
snapshot = { ...snapshot, byId };
|
|
3278
|
+
}
|
|
2945
3279
|
// 只消费上一轮追加请求;列表短暂 pending 时保留已展开页,ready 后继续从同一前缀呈现。
|
|
2946
3280
|
recentAppendQueued = false;
|
|
2947
3281
|
const workspaceSnapshot = getSnapshot(workspaces, "list");
|
|
@@ -2961,24 +3295,42 @@ function apply(ctx) {
|
|
|
2961
3295
|
const runLikeIds = new Set(
|
|
2962
3296
|
active.filter((entry) => shouldSubscribeToSession(entry, snapshot?.byId ?? {})).map((entry) => entry.id),
|
|
2963
3297
|
);
|
|
2964
|
-
|
|
3298
|
+
// 运行卡时钟:运行中子代理卡同样承载逐秒推进的进度与时长(R-01-009/AC-14)。
|
|
3299
|
+
syncLiveness(
|
|
3300
|
+
runLikeIds,
|
|
3301
|
+
active.some((entry) => entry.kind === "running" || (entry.kind === "subagent" && runLikeIds.has(entry.id))),
|
|
3302
|
+
);
|
|
2965
3303
|
for (const entry of active) {
|
|
2966
3304
|
const liveRecord = livenessById.get(entry.id);
|
|
2967
3305
|
const live = liveRecord?.liveness ?? null;
|
|
2968
3306
|
const detail = sessionDetailsById.get(entry.id);
|
|
2969
3307
|
if (entry.kind === "running" && detail) detail.durationFallbackLoaded = false;
|
|
3308
|
+
// 累计运行时长(R-01-020/AC-01、AC-03):主会话条目注入显示值——渲染期合成,
|
|
3309
|
+
// 含开放回合的实时已耗时并随时钟逐秒推进;子代理卡片不显示。
|
|
3310
|
+
if (entry.kind !== "subagent") applyTotalBusy(entry, now);
|
|
2970
3311
|
const detailSnapshot = liveRecord?.snapshot ?? detail?.snapshot ?? null;
|
|
2971
3312
|
if (detail && detail.memoHistoryAnchorOf !== (detail.history ?? null)) {
|
|
2972
3313
|
detail.memoHistoryAnchorOf = detail.history ?? null;
|
|
2973
3314
|
detail.memoHistoryAnchor = historyInstructionAnchor(detail.history);
|
|
2974
3315
|
}
|
|
3316
|
+
// 等待/暂停呈现(pendingText 存在)且自身快照为冻结值时,残留 running 行全部落定;
|
|
3317
|
+
// 存在活动后代时保留尾部提升的「agent 工作中」呈现(R-01-009/AC-10 委托周期语义)。
|
|
3318
|
+
const entryIdle = (entry.pendingText ?? null) !== null && entry.descendantActive !== true;
|
|
3319
|
+
const entryCwd = snapshot?.byId?.[entry.id]?.cwd ?? "";
|
|
3320
|
+
// log 派生 memo:history 引用 / idle / cwd 变化才重算。落定在折叠前生效
|
|
3321
|
+
// (foldedHistoryTimeline 的 settleIdle),组标题由已定案成员派生——阻塞等待卡
|
|
3322
|
+
// 呈现「运行了命令」+「等待回答」摘要,而非「正在运行」蓝闪(R-01-009/AC-09)。
|
|
3323
|
+
const historyRef = detail?.history ?? null;
|
|
3324
|
+
const logReady = Array.isArray(historyRef) && historyRef.length > 0;
|
|
3325
|
+
if (detail && logReady && (detail.memoLogTimelineOf !== historyRef || detail.memoLogTimelineIdle !== entryIdle || detail.memoLogTimelineCwd !== entryCwd)) {
|
|
3326
|
+
detail.memoLogTimelineOf = historyRef;
|
|
3327
|
+
detail.memoLogTimelineIdle = entryIdle;
|
|
3328
|
+
detail.memoLogTimelineCwd = entryCwd;
|
|
3329
|
+
detail.memoLogTimeline = foldedHistoryTimeline(historyRef, 4, entryCwd, entryIdle);
|
|
3330
|
+
}
|
|
2975
3331
|
if (detail && detailSnapshot) {
|
|
2976
3332
|
// 按快照引用 memo:引用不变(时钟 tick、无关推送)时命中缓存,
|
|
2977
3333
|
// 长会话不再每次渲染全序扫描。
|
|
2978
|
-
const entryCwd = snapshot?.byId?.[entry.id]?.cwd ?? "";
|
|
2979
|
-
// 等待/暂停呈现(pendingText 存在)且自身快照为冻结值时,残留 running 行全部落定;
|
|
2980
|
-
// 存在活动后代时保留尾部提升的「agent 工作中」呈现(R-01-009/AC-10 委托周期语义)。
|
|
2981
|
-
const entryIdle = (entry.pendingText ?? null) !== null && entry.descendantActive !== true;
|
|
2982
3334
|
if (detail.memoTimelineOf !== detailSnapshot || detail.memoTimelineCwd !== entryCwd || detail.memoTimelineDescendantActive !== (entry.descendantActive === true) || detail.memoTimelineIdle !== entryIdle || detail.memoTimelineAnchor !== (detail.memoHistoryAnchor ?? null)) {
|
|
2983
3335
|
detail.memoTimelineOf = detailSnapshot;
|
|
2984
3336
|
detail.memoTimelineCwd = entryCwd;
|
|
@@ -2990,7 +3342,11 @@ function apply(ctx) {
|
|
|
2990
3342
|
// 出现在输出(窗口行或锚行),反之需 history 补读。
|
|
2991
3343
|
detail.snapshotHasAnchorableUserRow = detail.memoTimeline.some(isAnchorableUserRow);
|
|
2992
3344
|
}
|
|
2993
|
-
|
|
3345
|
+
// V3 快照不再承载 chat/partial:活动时间线以 log 派生(eventSource 窗口,含
|
|
3346
|
+
// live-chunk 流式行)为主源;快照派生仅在 log 未就绪时兜底(锚行回退)。
|
|
3347
|
+
entry.timeline = logReady ? detail.memoLogTimeline : detail.memoTimeline ?? [];
|
|
3348
|
+
} else if (logReady) {
|
|
3349
|
+
entry.timeline = detail.memoLogTimeline;
|
|
2994
3350
|
} else {
|
|
2995
3351
|
entry.timeline = detail?.timeline ?? entry.timeline ?? [];
|
|
2996
3352
|
}
|
|
@@ -3012,18 +3368,19 @@ function apply(ctx) {
|
|
|
3012
3368
|
}
|
|
3013
3369
|
}
|
|
3014
3370
|
if (entry.kind === "awaiting" && detail) {
|
|
3015
|
-
|
|
3016
|
-
if (detail.memoTurnDurationSnapshotOf !== detailSnapshot || detail.memoTurnDurationHistoryOf !== history) {
|
|
3017
|
-
detail.memoTurnDurationSnapshotOf = detailSnapshot;
|
|
3018
|
-
detail.memoTurnDurationHistoryOf = history;
|
|
3019
|
-
detail.memoTurnDuration = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history });
|
|
3020
|
-
}
|
|
3021
|
-
entry.elapsedMs = detail.memoTurnDuration ?? null;
|
|
3371
|
+
entry.elapsedMs = memoTurnDuration(detail, detailSnapshot);
|
|
3022
3372
|
}
|
|
3023
3373
|
if (detail?.model) {
|
|
3024
3374
|
entry.model = detail.model.model;
|
|
3025
3375
|
entry.reasoning = detail.model.reasoning;
|
|
3026
3376
|
}
|
|
3377
|
+
// 子代理溯源得到的是 provider 模型 id:经目录分组解析为显示名,effort 缺失时
|
|
3378
|
+
// 以同一目录条目回退(R-01-012/AC-17);目录未覆盖该 id 时保留原始回退(AC-18)。
|
|
3379
|
+
if (entry.kind === "subagent" && detail?.model) {
|
|
3380
|
+
const catalogEntry = catalogEntries[detail.model.model];
|
|
3381
|
+
if (catalogEntry?.name) entry.model = catalogEntry.name;
|
|
3382
|
+
if (!entry.reasoning && catalogEntry?.reasoning) entry.reasoning = catalogEntry.reasoning;
|
|
3383
|
+
}
|
|
3027
3384
|
// 待回复卡列表补全(C-040、C-064):buildEntries 运行时快照时间线可能仍为空,
|
|
3028
3385
|
// 而上面的 memo 才在本帧算出结构化提问预览;等待卡静止后常无下一帧,因此在此
|
|
3029
3386
|
// 立即补入 questionPreview,由 cardSignature 驱动本帧 DOM 写入。
|
|
@@ -3032,7 +3389,9 @@ function apply(ctx) {
|
|
|
3032
3389
|
if (question !== null) entry.questionPreview = question;
|
|
3033
3390
|
}
|
|
3034
3391
|
// 字段级加载指示(R-01-014/AC-02):补充数据在途时卡片对应位置显示活动图标。
|
|
3035
|
-
|
|
3392
|
+
// 主会话模型经目录 store 一次性 load 到达(modelLoads 记账),子代理模型经
|
|
3393
|
+
// 日志读取提取(R-01-012/AC-17)。
|
|
3394
|
+
entry.loadingModel = !detail?.model && (modelLoads.has(entry.id) || (entry.kind === "subagent" && historyLoads.has(entry.id)));
|
|
3036
3395
|
entry.loadingTimeline =
|
|
3037
3396
|
entry.timeline.length === 0 &&
|
|
3038
3397
|
(historyLoads.has(entry.id) || sessionOpenLoads.has(entry.id) || (runLikeIds.has(entry.id) && !liveRecord));
|
|
@@ -3053,7 +3412,8 @@ function apply(ctx) {
|
|
|
3053
3412
|
// 之外)时,取 history 深翻提取的开放回合起点。
|
|
3054
3413
|
const anchor = progressAnchor(progressAnchorById.get(entry.id) ?? null, {
|
|
3055
3414
|
descendantActive: entry.descendantActive === true,
|
|
3056
|
-
|
|
3415
|
+
// dsh 0.1.5 起快照不再携带 turnTimings:开放回合起点由宿主侧 busy 记账兜底。
|
|
3416
|
+
hostStartTime: live?.startTime ?? busyById.get(entry.id)?.openTurnStart ?? detail?.memoOpenTurnStart ?? null,
|
|
3057
3417
|
now,
|
|
3058
3418
|
});
|
|
3059
3419
|
progressAnchorById.set(entry.id, anchor);
|
|
@@ -3069,6 +3429,21 @@ function apply(ctx) {
|
|
|
3069
3429
|
// 连续、周期外回合切换归零,R-01-009/AC-06,C-014、C-044)。
|
|
3070
3430
|
entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS }) });
|
|
3071
3431
|
}
|
|
3432
|
+
if (entry.kind === "subagent") {
|
|
3433
|
+
// 运行中子代理卡与主会话运行卡同口径:同一锚点状态机与进度曲线(R-01-009/AC-14)。
|
|
3434
|
+
const elapsedMs = Number.isFinite(anchor.anchor) ? Math.max(0, now - anchor.anchor) : null;
|
|
3435
|
+
if (elapsedMs !== null) {
|
|
3436
|
+
Object.assign(entry, projectionStats, { elapsedMs });
|
|
3437
|
+
if (detail) detail.lastRuntimeStats = mergeRuntimeStats(projectionStats, detail.lastRuntimeStats);
|
|
3438
|
+
entry.progress = progressOf({ elapsedMs, halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS }) });
|
|
3439
|
+
} else {
|
|
3440
|
+
// 非运行(暂停等待):冻结最后已知统计与最近回合耗时,progress 置空隐藏
|
|
3441
|
+
// 进度条(R-01-009/AC-15);刷新/无留存时回退当前列表投影。
|
|
3442
|
+
Object.assign(entry, mergeRuntimeStats(detail?.lastRuntimeStats, projectionStats));
|
|
3443
|
+
entry.elapsedMs = detail ? memoTurnDuration(detail, detailSnapshot) : null;
|
|
3444
|
+
entry.progress = null;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3072
3447
|
}
|
|
3073
3448
|
// 历史区时间精化(R-01-010/AC-08、AC-09):从保留快照的 turnTimings 与已拉取的
|
|
3074
3449
|
// history 同批事件提取最后回合结束时刻(取两者较新者),按引用 memo;均无则不提供,
|
|
@@ -3095,6 +3470,8 @@ function apply(ctx) {
|
|
|
3095
3470
|
const detail = sessionDetailsById.get(entry.id);
|
|
3096
3471
|
const detailSnapshot = livenessById.get(entry.id)?.snapshot ?? detail?.snapshot ?? null;
|
|
3097
3472
|
const elapsedMs = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history: detail?.history });
|
|
3473
|
+
// 累计运行时长(R-01-020/AC-01):最近历史卡同为标题行右侧显示;历史卡无开放回合。
|
|
3474
|
+
applyTotalBusy(entry, now);
|
|
3098
3475
|
const stats = statsFromProjection(snapshot?.byId?.[entry.id]?.projectionValues, elapsedMs);
|
|
3099
3476
|
const retained = detail?.lastRuntimeStats;
|
|
3100
3477
|
Object.assign(entry, stats, retained ? {
|
|
@@ -3129,9 +3506,27 @@ function apply(ctx) {
|
|
|
3129
3506
|
...active.filter((entry) => entry.kind === "awaiting").map((entry) => entry.id),
|
|
3130
3507
|
...recentDurationFallbackIds,
|
|
3131
3508
|
]);
|
|
3509
|
+
// 子代理模型溯源读取时机(R-01-012/AC-17):运行中看快照最新助手节点是否已
|
|
3510
|
+
// 定案(事件已落宿主日志,尾页读取必命中;工具密集期 4 行折叠窗口可能不含
|
|
3511
|
+
// 助手行,故直接读快照而非折叠时间线);不在运行中则整段日志已冻结,直接读取。
|
|
3512
|
+
// 开局只有流式 partial 时不读,避免早读扑空。
|
|
3513
|
+
const subagentModelReadIds = new Set();
|
|
3514
|
+
for (const entry of active) {
|
|
3515
|
+
if (entry.kind !== "subagent") continue;
|
|
3516
|
+
if (!runLikeIds.has(entry.id)) {
|
|
3517
|
+
subagentModelReadIds.add(entry.id);
|
|
3518
|
+
continue;
|
|
3519
|
+
}
|
|
3520
|
+
const detail = sessionDetailsById.get(entry.id);
|
|
3521
|
+
const detailSnapshot = livenessById.get(entry.id)?.snapshot ?? detail?.snapshot ?? null;
|
|
3522
|
+
if (chatLatestAssistantSettled(detailSnapshot)) subagentModelReadIds.add(entry.id);
|
|
3523
|
+
}
|
|
3132
3524
|
const detailIds = [...active, ...recent].map((entry) => entry.id);
|
|
3133
3525
|
detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
|
|
3134
|
-
loadNativeDetails(detailIds, previewFallbackIds, durationFallbackIds);
|
|
3526
|
+
loadNativeDetails({ ids: detailIds, previewFallbackIds, durationFallbackIds, subagentModelReadIds });
|
|
3527
|
+
// 累计运行时长懒回填(R-01-020/AC-05):对可见主会话触发宿主侧存量回合补齐;
|
|
3528
|
+
// 宿主侧每会话单飞,重复触发由宿主合并;完成后经 busy SSE 广播推送。
|
|
3529
|
+
requestBusyBackfill([...active, ...recent].filter((entry) => entry.kind !== "subagent").map((entry) => entry.id));
|
|
3135
3530
|
const visibleIds = new Set([...active, ...recent].map((entry) => entry.id));
|
|
3136
3531
|
// 详情与 loads 记账同生命周期:离开可见集合即放行,重回可见时允许重拉/重试。
|
|
3137
3532
|
// 锚点记账不随可见性 prune:瞬时 loading 空帧不得误清(进度重置);陈旧条目靠
|
|
@@ -3139,6 +3534,7 @@ function apply(ctx) {
|
|
|
3139
3534
|
pruneInvisibleEntries([sessionDetailsById, modelLoads, historyLoads, sessionOpenLoads], visibleIds);
|
|
3140
3535
|
// 模型目录订阅同生命周期:不可见即先 unsubscribe 再除名,监听器不残留(R-01-012/AC-16)。
|
|
3141
3536
|
pruneSubscriptions(modelDirectorySubs, visibleIds);
|
|
3537
|
+
pruneSubscriptions(logSourceSubs, visibleIds);
|
|
3142
3538
|
// 重试链目标已成为当前会话(他途到达)即取消,避免过期链条拽回会话。
|
|
3143
3539
|
cancelStaleOpenRetries({ currentId: snapshot?.current ?? null, activatedId: lastActivatedId });
|
|
3144
3540
|
|
|
@@ -3427,7 +3823,16 @@ function apply(ctx) {
|
|
|
3427
3823
|
// 只观察 center 的直接子节点,捕获 seat/pane 重挂载,不观察 pane 子树。
|
|
3428
3824
|
centerObserver = new MutationObserver(() => {
|
|
3429
3825
|
const nextSeat = document.querySelector(CONVERSATION_SELECTOR);
|
|
3430
|
-
if (nextSeat?.parentElement !== center)
|
|
3826
|
+
if (nextSeat?.parentElement !== center) {
|
|
3827
|
+
installFrameObserver();
|
|
3828
|
+
} else if (nextSeat !== seat) {
|
|
3829
|
+
// 中间列未换但槽容器被宿主原地替换:流式观察者换绑到新节点,
|
|
3830
|
+
// 不整层重装(旧节点已脱离 DOM,留着会让流式 childList 更新丢失)。
|
|
3831
|
+
conversationObserver?.disconnect();
|
|
3832
|
+
conversationObserver = new MutationObserver(queueSync);
|
|
3833
|
+
conversationObserver.observe(nextSeat, { childList: true, subtree: true });
|
|
3834
|
+
observedSeat = nextSeat;
|
|
3835
|
+
}
|
|
3431
3836
|
queueSync();
|
|
3432
3837
|
});
|
|
3433
3838
|
centerObserver.observe(center, { childList: true });
|
|
@@ -3450,8 +3855,17 @@ function apply(ctx) {
|
|
|
3450
3855
|
|
|
3451
3856
|
// ---- 交互:移动端抽屉开关、弹窗收起、桌面折叠 ----
|
|
3452
3857
|
function onToggleClick() {
|
|
3453
|
-
|
|
3454
|
-
|
|
3858
|
+
let pane = document.querySelector(`[${PANE_ATTR}]`);
|
|
3859
|
+
if (pane === null) {
|
|
3860
|
+
// 宿主视图替换窗口期/绑定失败时窗格可能缺失:先补绑再开合,开关不得静默无响应
|
|
3861
|
+
// (真机宿主重渲染节奏与桌面不同,此窗口期在触屏上更易被命中,R-01-008/AC-01)。
|
|
3862
|
+
pane = ensurePane();
|
|
3863
|
+
if (pane === null) {
|
|
3864
|
+
console.warn("[dsh-activity-pane] 窗格挂载点不存在(main 槽容器未就绪),已忽略本次开关点击");
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
const open = pane.getAttribute("data-open") !== "true";
|
|
3455
3869
|
togglePane(open);
|
|
3456
3870
|
}
|
|
3457
3871
|
toggle.addEventListener("click", onToggleClick);
|
|
@@ -3467,11 +3881,19 @@ function apply(ctx) {
|
|
|
3467
3881
|
disposed = true;
|
|
3468
3882
|
sessionUnsubscribe?.();
|
|
3469
3883
|
workspaceUnsubscribe?.();
|
|
3884
|
+
pendingUnsubscribe?.();
|
|
3470
3885
|
acksSource?.close();
|
|
3471
3886
|
acksSource = null;
|
|
3887
|
+
busySource?.close();
|
|
3888
|
+
busySource = null;
|
|
3889
|
+
document.removeEventListener("visibilitychange", onBusyVisibilityResume);
|
|
3890
|
+
window.removeEventListener("pageshow", onBusyPageShow);
|
|
3472
3891
|
document.removeEventListener("visibilitychange", onVisibilityResume);
|
|
3473
3892
|
window.removeEventListener("pageshow", onPageShow);
|
|
3474
3893
|
completeAcksById.clear();
|
|
3894
|
+
busyById.clear();
|
|
3895
|
+
busyRequestedIds.clear();
|
|
3896
|
+
busyRetryAtById.clear();
|
|
3475
3897
|
if (clockTimer !== null) clearInterval(clockTimer);
|
|
3476
3898
|
if (recentTimeTimer !== null) clearInterval(recentTimeTimer);
|
|
3477
3899
|
if (e2eListReleaseTimer !== null) clearTimeout(e2eListReleaseTimer);
|
|
@@ -3488,6 +3910,7 @@ function apply(ctx) {
|
|
|
3488
3910
|
livenessById.clear();
|
|
3489
3911
|
// 卸载即全量退订:空可见集合驱动 pruneSubscriptions 先 unsubscribe 再除名。
|
|
3490
3912
|
pruneSubscriptions(modelDirectorySubs, new Set());
|
|
3913
|
+
pruneSubscriptions(logSourceSubs, new Set());
|
|
3491
3914
|
progressAnchorById.clear();
|
|
3492
3915
|
sessionOpenLoads.clear();
|
|
3493
3916
|
loadQueue.length = 0;
|