dsh-activity-pane 0.7.0 → 0.9.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 +978 -250
- package/package.json +1 -1
- package/scripts/acceptance.mjs +6 -2
- package/scripts/check.mjs +632 -164
- package/src/client.mjs +572 -150
- package/src/core.mjs +403 -98
- package/src/host.mjs +231 -4
- package/src/navigation.mjs +3 -2
package/.dsh-plugin/client.js
CHANGED
|
@@ -441,6 +441,23 @@ function chatNodeAt(nodes, key) {
|
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
/** 子代理模型读取的触发信号(R-01-012/AC-17):快照最新一个助手节点已定案
|
|
445
|
+
* (status === "settled",即其 assistant/message 事件已落宿主日志,尾页读取必命中)
|
|
446
|
+
* 时为 true。只检查最新一个助手节点——它随流式推送翻转为 settled 的那一刻即产生
|
|
447
|
+
* 触发,尾扫即停、近 O(1),完全挂在既有订阅推送上(R-02-004);工具密集期 4 行折叠窗口
|
|
448
|
+
* 可能不含助手行,故不看折叠时间线而直接读快照。流式未定案/中断/无快照为 false。 */
|
|
449
|
+
function chatLatestAssistantSettled(snapshot) {
|
|
450
|
+
const chat = snapshot?.chat;
|
|
451
|
+
const order = Array.isArray(chat?.order) ? chat.order : [];
|
|
452
|
+
const nodes = chat?.nodes;
|
|
453
|
+
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
454
|
+
const node = chatNodeAt(nodes, order[i]);
|
|
455
|
+
if (!isRecord(node) || node.visibility === "hidden" || node.kind !== "assistant-step") continue;
|
|
456
|
+
return isRecord(node.data) && node.data.status === "settled";
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
444
461
|
/** 尾部反向收集原始工作项(不含 live 合并),取够 want 个可转换项或耗尽 order 即停。
|
|
445
462
|
* continueToUser:取够后以廉价结构检查(isUserChatNode:非 hidden 的 user/steering 且含非空文本块)继续前走至最近一个未收集的用户节点(含
|
|
446
463
|
* steering),命中才转换并入队首——供指令锚行派生(R-01-012/AC-12),不为找锚做全序转换。 */
|
|
@@ -819,12 +836,54 @@ function foldedConversationTimeline(snapshot, limit = 4, cwd = "", descendantAct
|
|
|
819
836
|
return [];
|
|
820
837
|
}
|
|
821
838
|
|
|
839
|
+
/** user/message 非用户 source 的 provenance 投影(对齐宿主 dsh-client-ui-chat
|
|
840
|
+
* contextProvenance):recall = 跨会话召回,其余为注入;label 取各 source 形态的
|
|
841
|
+
* 稳定标识,未知 kind 直接以 kind 呈现。 */
|
|
842
|
+
function contextProvenanceOf(source) {
|
|
843
|
+
if (!isRecord(source)) return { role: "inject", label: null };
|
|
844
|
+
const kind = typeof source.kind === "string" ? source.kind : null;
|
|
845
|
+
if (kind === null) return { role: "inject", label: null };
|
|
846
|
+
if (kind === "session-reference") return { role: "recall", label: joinedSourceLabels(source.references, "label") ?? kind };
|
|
847
|
+
if (kind === "agent-instructions") return { role: "inject", label: joinedSourceLabels(source.changes, "path") ?? kind };
|
|
848
|
+
if (kind === "plugin") return { role: "inject", label: typeof source.plugin === "string" ? source.plugin : kind };
|
|
849
|
+
if (kind === "skill-invocation") return { role: "inject", label: typeof source.name === "string" ? source.name : kind };
|
|
850
|
+
return { role: "inject", label: kind };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** source 数组成员的字段去重收集(首见顺序),逗号拼接为单行标签。 */
|
|
854
|
+
function joinedSourceLabels(list, field) {
|
|
855
|
+
if (!Array.isArray(list)) return null;
|
|
856
|
+
const seen = [];
|
|
857
|
+
for (const entry of list) {
|
|
858
|
+
const value = isRecord(entry) && typeof entry[field] === "string" ? entry[field] : null;
|
|
859
|
+
if (value !== null && !seen.includes(value)) seen.push(value);
|
|
860
|
+
}
|
|
861
|
+
return seen.length > 0 ? seen.join(", ") : null;
|
|
862
|
+
}
|
|
863
|
+
|
|
822
864
|
function timelineItemFromEvent(entry, cwd = "") {
|
|
823
865
|
const event = isRecord(entry?.event) ? entry.event : entry;
|
|
824
866
|
const data = isRecord(event?.data) ? event.data : {};
|
|
825
867
|
if (!event || typeof event.type !== "string") return null;
|
|
826
|
-
if (event.type === "user/message"
|
|
827
|
-
|
|
868
|
+
if (event.type === "user/message") {
|
|
869
|
+
if (data.source?.kind === "user") {
|
|
870
|
+
return { id: `user:${event.seq}`, kind: "user", icon: "user", label: "用户", text: contentText(data.content), detail: null, status: "done" };
|
|
871
|
+
}
|
|
872
|
+
// V3 log 路径的注入上下文行:非用户 source 的 user/message 即宿主 ContextMessageNode
|
|
873
|
+
// (0.1.5 无独立 context 事件),镜像原生 ContextInjectionRow 的角色文案。
|
|
874
|
+
const text = contentText(data.content);
|
|
875
|
+
if (text === "") return null;
|
|
876
|
+
const provenance = contextProvenanceOf(data.source);
|
|
877
|
+
return {
|
|
878
|
+
id: `context:${event.seq}`,
|
|
879
|
+
kind: "context",
|
|
880
|
+
icon: "context",
|
|
881
|
+
label: provenance.role === "recall" ? "跨会话召回" : "上下文注入",
|
|
882
|
+
text,
|
|
883
|
+
summary: provenance.label ?? "",
|
|
884
|
+
detail: null,
|
|
885
|
+
status: "done",
|
|
886
|
+
};
|
|
828
887
|
}
|
|
829
888
|
if (event.type === "assistant/message") {
|
|
830
889
|
const text = contentText(data.message?.content);
|
|
@@ -866,20 +925,15 @@ function historyToolResultRoot(data, resultView, callInfo = null) {
|
|
|
866
925
|
};
|
|
867
926
|
}
|
|
868
927
|
|
|
869
|
-
/** 判断 session window 是否尚未 hydrate,需用 native history 补齐。 */
|
|
870
|
-
function needsHistorySnapshot(snapshot) {
|
|
871
|
-
return !snapshot || !Array.isArray(snapshot.chat?.order) || snapshot.chat.order.length === 0;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
928
|
/** 冷会话 history 回溯深翻:自尾页起按 beforeSeq 向前翻页,直至命中最近一条用户消息
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
929
|
+
* (messagePreviews 的 userPreview 非空,R-01-013/AC-03)、或翻尽
|
|
930
|
+
* (hasMore=false/无更多事件/业务错误 null);requireOpenTurnStart 为 true 时
|
|
931
|
+
* (运行会话开放回合起点兜底,R-01-009/AC-06)命中用户消息后开放回合起点未命中
|
|
932
|
+
* 仍继续深翻直至起点命中或翻尽。maxPages 仅作显式护栏(默认 Infinity 即不设页数
|
|
933
|
+
* 上限——用户消息必然存在于会话最早段,翻尽必终止,无需预置页数界)。fetchPage
|
|
934
|
+
* (beforeSeq) 注入实际读取(返回 `{events, hasMore}` 或 null),便于纯函数单测;
|
|
935
|
+
* 中途异常保留已得事件并以 error 返回。返回 `{ events, error }`(events 按时间
|
|
936
|
+
* 正序,新页在后)。 */
|
|
883
937
|
async function pagedHistoryEvents({ fetchPage, maxPages = Infinity, requireOpenTurnStart = false }) {
|
|
884
938
|
const allEvents = [];
|
|
885
939
|
let beforeSeq;
|
|
@@ -908,17 +962,45 @@ async function pagedHistoryEvents({ fetchPage, maxPages = Infinity, requireOpenT
|
|
|
908
962
|
return { events: allEvents, error };
|
|
909
963
|
}
|
|
910
964
|
|
|
911
|
-
/**
|
|
912
|
-
*
|
|
913
|
-
*
|
|
965
|
+
/** V3 log 窗口的扁平工作项映射:供没有 ChatSnapshot 的活动/历史会话折叠分组使用。
|
|
966
|
+
* eventSource 快照含 type === "transient" 的 `assistant/live-chunk` 条目(agent-stream
|
|
967
|
+
* 增量帧,seq 为插值小数,仅存在于流式期间):按 attemptId 原位累积为单个 running
|
|
968
|
+
* assistant 行(对齐快照路径 mergeLiveItems 的 partial 行语义);回合落定后 transient
|
|
969
|
+
* 条目随 attempt 出窗,durable assistant/message 自然接管。tool-call-delta 不折叠——
|
|
970
|
+
* tool/call 以 durable 事件落定后经既有 call/result 配对路径呈现。
|
|
914
971
|
* tool/result 落定同 callId 的 call 项(原位替换,name/arguments/callView 由 call 事件补齐):
|
|
915
972
|
* history 是冻结过去,call 事件单独留存会成为永久 running 幽灵行(R-01-016/AC-01)。 */
|
|
916
973
|
function conversationTimelineFromHistory(history, limit = 4, cwd = "") {
|
|
917
974
|
const items = [];
|
|
918
975
|
const inflightCalls = new Map(); // callId → { index, data, callView }:等待结果落定的 tool/call 事件
|
|
976
|
+
const liveAttempts = new Map(); // attemptId → { index, text, reasoning }:流式期间累积的 live 行
|
|
919
977
|
for (const entry of Array.isArray(history) ? history : []) {
|
|
920
978
|
const event = eventOf(entry);
|
|
921
979
|
const data = isRecord(event?.data) ? event.data : {};
|
|
980
|
+
if (event?.type === "assistant/live-chunk") {
|
|
981
|
+
const attemptId = typeof data.attemptId === "string" ? data.attemptId : "";
|
|
982
|
+
const text = typeof data.chunk?.text === "string" ? data.chunk.text : "";
|
|
983
|
+
if (attemptId === "" || text === "" || !isRecord(data.chunk)) continue;
|
|
984
|
+
const attempt = liveAttempts.get(attemptId) ?? (() => {
|
|
985
|
+
const created = { index: items.length, text: "", reasoning: "" };
|
|
986
|
+
liveAttempts.set(attemptId, created);
|
|
987
|
+
items.push({ id: `live:${attemptId}`, kind: "assistant", icon: "assistant", text: "", detail: null, status: "running", live: true });
|
|
988
|
+
return created;
|
|
989
|
+
})();
|
|
990
|
+
if (data.chunk.type === "reasoning-delta") attempt.reasoning += text;
|
|
991
|
+
else if (data.chunk.type === "text-delta") attempt.text += text;
|
|
992
|
+
else continue;
|
|
993
|
+
items[attempt.index] = {
|
|
994
|
+
...items[attempt.index],
|
|
995
|
+
text: attempt.text,
|
|
996
|
+
detail: attempt.reasoning || null,
|
|
997
|
+
// 镜像原生 ReasoningRow:流式思考显示尾部最新行,避免与已定案首行摘要漂移。
|
|
998
|
+
summary: attempt.reasoning ? latestLineOf(attempt.reasoning) : attempt.text,
|
|
999
|
+
status: "running",
|
|
1000
|
+
live: true,
|
|
1001
|
+
};
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
922
1004
|
if (event?.type === "tool/result") {
|
|
923
1005
|
const callId = toolResultCallId(data);
|
|
924
1006
|
const pending = callId !== undefined ? inflightCalls.get(callId) : undefined;
|
|
@@ -948,12 +1030,15 @@ function conversationTimelineFromHistory(history, limit = 4, cwd = "") {
|
|
|
948
1030
|
|
|
949
1031
|
/** 冷 history 折叠分组时间线(R-01-017、R-01-012/AC-12~AC-15):页内全部事件映射折叠后
|
|
950
1032
|
* 套用与快照路径同一窗口/锚行选择(selectTimelineRows),最近用户消息滚动触顶后停留为
|
|
951
|
-
* 首行锚行。
|
|
952
|
-
|
|
1033
|
+
* 首行锚行。settleIdle:阻塞等待呈现(pendingText 存在)下折叠前把残留 running 行落定,
|
|
1034
|
+
* 组标题/状态由已定案成员派生(「运行了命令」而非「正在运行」蓝闪),与快照路径的
|
|
1035
|
+
* settleWhenIdle 前置语义一致。 */
|
|
1036
|
+
function foldedHistoryTimeline(history, limit = 4, cwd = "", settleIdle = false) {
|
|
953
1037
|
const max = Math.max(0, limit);
|
|
954
1038
|
if (max === 0) return [];
|
|
955
1039
|
const items = conversationTimelineFromHistory(history, Number.MAX_SAFE_INTEGER, cwd);
|
|
956
|
-
|
|
1040
|
+
const settled = settleIdle ? settleWhenIdle(items, true) : items;
|
|
1041
|
+
return selectTimelineRows(foldWorkGroups(settled, Number.MAX_SAFE_INTEGER), max);
|
|
957
1042
|
}
|
|
958
1043
|
|
|
959
1044
|
/** history 指令锚行提取(R-01-012/AC-12 快照窗口外兜底):尾部反向取最近一条非空文本的
|
|
@@ -971,14 +1056,6 @@ function historyInstructionAnchor(history) {
|
|
|
971
1056
|
}
|
|
972
1057
|
|
|
973
1058
|
|
|
974
|
-
/** 开放回合起点缺口判定(R-01-009/AC-06 冷窗口兜底触发口径):快照就绪、宿主判定运行中、
|
|
975
|
-
* 轮内订阅已建立,但快照 turnTimings 无开放回合起点(liveStartTime 为 null)——超长回合
|
|
976
|
-
* 的 turn/start 在尾页窗口之外。等待/空闲会话(非运行或无 liveness 记录)不算缺口,
|
|
977
|
-
* 不触发 history 补读。 */
|
|
978
|
-
function openTurnStartMissing({ snapshotReady = false, running = false, hasLiveness = false, liveStartTime = null } = {}) {
|
|
979
|
-
return snapshotReady === true && running === true && hasLiveness === true && liveStartTime == null;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
1059
|
/** 开放回合起点兜底提取(R-01-009/AC-06):history 事件尾部反向扫描,最近一条边界事件
|
|
983
1060
|
* 为 turn/start 即存在开放回合、返回其时刻;为 turn/end 则无开放回合返回 null。
|
|
984
1061
|
* minTurn:快照已知的最晚回合号——history 开放回合落后于此(拉取后已切换新回合)时
|
|
@@ -1051,6 +1128,64 @@ function modelMetadata(models) {
|
|
|
1051
1128
|
};
|
|
1052
1129
|
}
|
|
1053
1130
|
|
|
1131
|
+
/** 子代理模型溯源(R-01-012/AC-17):从 history 事件流尾扫最近一条携带模型溯源的
|
|
1132
|
+
* `assistant/message` 事件,取 `message.source.model`。返回值 reasoning 恒为空串,
|
|
1133
|
+
* effort 由调用方以 `reasoningEffortFromHistoryEvents` 同页折叠;无命中(空事件、
|
|
1134
|
+
* 无助手消息或溯源缺失)返回 null,调用方保持模型区空白、不以 preset 或母会话
|
|
1135
|
+
* 模型冒充(R-01-012/AC-18)。 */
|
|
1136
|
+
function modelFromHistoryEvents(history) {
|
|
1137
|
+
const entries = Array.isArray(history) ? history : [];
|
|
1138
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1139
|
+
const event = eventOf(entries[i]);
|
|
1140
|
+
if (event?.type !== "assistant/message") continue;
|
|
1141
|
+
const source = isRecord(event.data?.message?.source) ? event.data.message.source : null;
|
|
1142
|
+
if (source !== null && typeof source.model === "string" && source.model !== "") {
|
|
1143
|
+
return { model: source.model, reasoning: "" };
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** 子代理 reasoning effort(R-01-012/AC-17):history 尾扫最新一条 `request/header`
|
|
1150
|
+
* 事件,取 `config.reasoningEffort`——宿主的会话选择即从该折叠读取。命中最新请求头
|
|
1151
|
+
* 即停:更早请求头属已废弃纪元,即使声明过 effort 也不再回扫。最新请求头未声明
|
|
1152
|
+
* effort 或页内无请求头(超长子会话的 header 在日志开头、可能落在尾页窗口之外)
|
|
1153
|
+
* 返回 null,调用方回落目录条目 `reasoning` 后保持空值(R-01-012/AC-18)。 */
|
|
1154
|
+
function reasoningEffortFromHistoryEvents(history) {
|
|
1155
|
+
const entries = Array.isArray(history) ? history : [];
|
|
1156
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
1157
|
+
const event = eventOf(entries[i]);
|
|
1158
|
+
if (event?.type !== "request/header") continue;
|
|
1159
|
+
const config = isRecord(event.data?.header?.config) ? event.data.header.config : null;
|
|
1160
|
+
if (config !== null && typeof config.reasoningEffort === "string" && config.reasoningEffort !== "") {
|
|
1161
|
+
return config.reasoningEffort;
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/** 模型目录分组的 modelId → {name, reasoning} 索引(R-01-012/AC-17):溯源
|
|
1169
|
+
* `source.model` 是 provider 侧 id,显示名需经目录分组解析;同一部署的目录分组在
|
|
1170
|
+
* 主/子会话间共享,条目 `reasoning` 是请求头 effort 不可得时的回退来源。畸形条目
|
|
1171
|
+
* 跳过;目录缺失返回空索引,调用方回退显示原始溯源 id(R-01-012/AC-18)。 */
|
|
1172
|
+
function catalogModelEntries(groups) {
|
|
1173
|
+
const entries = {};
|
|
1174
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
1175
|
+
if (!isRecord(group)) continue;
|
|
1176
|
+
for (const model of Array.isArray(group.models) ? group.models : []) {
|
|
1177
|
+
if (!isRecord(model)) continue;
|
|
1178
|
+
if (typeof model.id === "string" && model.id !== "" && typeof model.name === "string" && model.name !== "") {
|
|
1179
|
+
entries[model.id] = {
|
|
1180
|
+
name: model.name,
|
|
1181
|
+
reasoning: typeof model.reasoning === "string" && model.reasoning !== "" ? model.reasoning : "",
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return entries;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1054
1189
|
/** 只提供卡片底部所需的原始统计字段,不拼接当前动作文案。 */
|
|
1055
1190
|
function runtimeStats({ elapsedMs = null, outputTokens = null, rateTokS = null } = {}) {
|
|
1056
1191
|
return {
|
|
@@ -1193,36 +1328,6 @@ function escapeCssString(value) {
|
|
|
1193
1328
|
.replace(/\0/g, "�");
|
|
1194
1329
|
}
|
|
1195
1330
|
|
|
1196
|
-
/** 冷会话补充数据读取决策(单次渲染内是否发起 models/history 读取)。
|
|
1197
|
-
* 失败路径会写入空 model/history 使决策转为「不读」(可见期内不热重试);
|
|
1198
|
-
* 详情与记账随可见性清理(pruneInvisibleEntries)一起移除后,决策自然恢复为「读取」。
|
|
1199
|
-
* windowComplete(R-01-009/AC-06、R-01-012/AC-12 冷窗口兜底):快照已就绪但窗口缺
|
|
1200
|
-
* 锚点数据(开放回合起点或可锚用户行在窗口外)时为 false——此时仍发起一次 history
|
|
1201
|
-
* 补读,供进度锚点与指令锚行兜底。previewFallbackNeeded 表示最近卡的快照预览
|
|
1202
|
-
* 不完整,同样补读一次 history(R-01-013/AC-03、AC-04);durationFallbackNeeded 表示等待卡或最近卡
|
|
1203
|
-
* 需要在已加载的旧 history 之后再取一次最新回合边界(R-01-009/AC-12、R-01-013/AC-12)。 */
|
|
1204
|
-
function detailLoadPlan({
|
|
1205
|
-
detail = {},
|
|
1206
|
-
isSubagent = false,
|
|
1207
|
-
snapshotReady = false,
|
|
1208
|
-
historyNeeded = false,
|
|
1209
|
-
previewFallbackNeeded = false,
|
|
1210
|
-
durationFallbackNeeded = false,
|
|
1211
|
-
windowComplete = true,
|
|
1212
|
-
modelInflight = false,
|
|
1213
|
-
historyInflight = false,
|
|
1214
|
-
} = {}) {
|
|
1215
|
-
return {
|
|
1216
|
-
subagent: isSubagent === true,
|
|
1217
|
-
model: !isSubagent && !detail.model && !modelInflight,
|
|
1218
|
-
history:
|
|
1219
|
-
!historyInflight &&
|
|
1220
|
-
((durationFallbackNeeded && detail.durationFallbackLoaded !== true) ||
|
|
1221
|
-
(previewFallbackNeeded && detail.previewFallbackLoaded !== true) ||
|
|
1222
|
-
(!detail.history && ((!snapshotReady && historyNeeded) || (snapshotReady === true && windowComplete === false)))),
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
1331
|
/** 打开重试链是否应取消:目标已成为当前会话(已到达),或用户已激活其它卡片(被新意图取代)。 */
|
|
1227
1332
|
function shouldCancelOpenRetry({ targetId, currentId = null, activatedId = null } = {}) {
|
|
1228
1333
|
if (targetId === undefined || targetId === null) return true;
|
|
@@ -1693,6 +1798,8 @@ function cardSignature(entries) {
|
|
|
1693
1798
|
entry.loadingTimeline ?? null,
|
|
1694
1799
|
entry.loadingPreviews ?? null,
|
|
1695
1800
|
entry.tokenStats ?? [entry.outputTokens ?? null, entry.inputTokens ?? null, entry.cacheHitPct ?? null, entry.rateTokS ?? null, entry.elapsedMs ?? null],
|
|
1801
|
+
// 累计运行时长参与签名(R-01-020):回填/SSE 推送与逐秒推进都要驱动重绘。
|
|
1802
|
+
entry.totalBusyMs ?? null,
|
|
1696
1803
|
]),
|
|
1697
1804
|
);
|
|
1698
1805
|
}
|
|
@@ -1878,58 +1985,256 @@ function durationTime(value) {
|
|
|
1878
1985
|
return Number.isFinite(time) ? time : null;
|
|
1879
1986
|
}
|
|
1880
1987
|
|
|
1881
|
-
/**
|
|
1882
|
-
|
|
1883
|
-
|
|
1988
|
+
/**
|
|
1989
|
+
* 从 history 重放回合/等待边界事件,提取最近一个已结束回合的运行过程耗时(busy 口径:
|
|
1990
|
+
* 起止差值扣除回合内阻塞等待,与 R-01-020 累计口径一致,恒不大于该回合墙钟时长)。
|
|
1991
|
+
* 回合起止不完整、时间逆序或回合运行段不为正时忽略该回合;无可得回合返回 null。
|
|
1992
|
+
*/
|
|
1993
|
+
function lastTurnBusyFromEvents(events) {
|
|
1994
|
+
let state = emptyTurnStats();
|
|
1884
1995
|
let latest = null;
|
|
1885
1996
|
for (const entry of Array.isArray(events) ? events : []) {
|
|
1886
|
-
const
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
if (
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
}
|
|
1894
|
-
if (event.type !== "turn/end") continue;
|
|
1895
|
-
const start = starts.get(turn);
|
|
1896
|
-
if (start === undefined || time < start) continue;
|
|
1897
|
-
if (latest === null || time > latest.end) latest = { end: time, duration: time - start };
|
|
1997
|
+
const time = durationTime(eventOf(entry)?.time);
|
|
1998
|
+
const prev = state;
|
|
1999
|
+
state = applyTurnEventToStats(state, entry);
|
|
2000
|
+
if (prev.openTurnStart === null || state.openTurnStart !== null) continue;
|
|
2001
|
+
// 能走到回合闭合的必为时刻有效的 turn/end(applyTurnEventToStats 对无效时刻无效果)。
|
|
2002
|
+
const activeMs = (state.busyMs ?? 0) - (prev.busyMs ?? 0);
|
|
2003
|
+
if (activeMs <= 0) continue;
|
|
2004
|
+
latest = { end: time, duration: activeMs };
|
|
1898
2005
|
}
|
|
1899
|
-
return latest;
|
|
2006
|
+
return latest?.duration ?? null;
|
|
1900
2007
|
}
|
|
1901
2008
|
|
|
1902
|
-
/**
|
|
1903
|
-
function
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2009
|
+
/** 最近完整回合的固定耗时(busy 口径):从 history 事件重放派生。 */
|
|
2010
|
+
function lastTurnDuration({ history = [] } = {}) {
|
|
2011
|
+
return lastTurnBusyFromEvents(history);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/** ask_user_question 工具名:提问/计划审查等待的开启边界(tool/call)与配对结算(tool/result)。 */
|
|
2015
|
+
const QUESTION_TOOL_NAME = "ask_user_question";
|
|
2016
|
+
|
|
2017
|
+
/** 回合/等待边界事件类型全集:busy 记账只消费这些事件,其余事件对记账无效果。 */
|
|
2018
|
+
const BOUNDARY_EVENT_TYPES = new Set(["turn/start", "turn/end", "approval/asked", "approval/decided", "tool/call", "tool/result"]);
|
|
2019
|
+
|
|
2020
|
+
/** 边界事件判定(宿主实时登记入口与记账转移共用,新增边界只改 BOUNDARY_EVENT_TYPES 一处)。 */
|
|
2021
|
+
function isBusyBoundaryEvent(type) {
|
|
2022
|
+
return BOUNDARY_EVENT_TYPES.has(type);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
/** 空记账状态(无任何有效计时)。 */
|
|
2026
|
+
function emptyTurnStats() {
|
|
2027
|
+
return { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null, watermarkTime: null };
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/** 从任意来源拷贝记账状态:缺失字段归一为 null(旧记录/部分记录兼容)。 */
|
|
2031
|
+
function turnStatsFrom(state) {
|
|
2032
|
+
return {
|
|
2033
|
+
busyMs: state?.busyMs ?? null,
|
|
2034
|
+
openTurnStart: state?.openTurnStart ?? null,
|
|
2035
|
+
openWaitStart: state?.openWaitStart ?? null,
|
|
2036
|
+
openWaitKind: state?.openWaitKind ?? null,
|
|
2037
|
+
openWaitId: state?.openWaitId ?? null,
|
|
2038
|
+
waitedMs: state?.waitedMs ?? null,
|
|
2039
|
+
watermarkTime: state?.watermarkTime ?? null,
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* 记账状态逐字段相等(不含 watermarkSeq——它不是状态字段,由宿主侧随写入一并落盘;
|
|
2045
|
+
* watermarkTime 为水位处已覆盖事件的时刻,参与相等判定以让补写该字段的收敛写入生效)。
|
|
2046
|
+
*/
|
|
2047
|
+
function turnStatsEqual(a, b) {
|
|
2048
|
+
return (
|
|
2049
|
+
a === b ||
|
|
2050
|
+
((a?.busyMs ?? null) === (b?.busyMs ?? null) &&
|
|
2051
|
+
(a?.openTurnStart ?? null) === (b?.openTurnStart ?? null) &&
|
|
2052
|
+
(a?.openWaitStart ?? null) === (b?.openWaitStart ?? null) &&
|
|
2053
|
+
(a?.openWaitKind ?? null) === (b?.openWaitKind ?? null) &&
|
|
2054
|
+
(a?.openWaitId ?? null) === (b?.openWaitId ?? null) &&
|
|
2055
|
+
(a?.waitedMs ?? null) === (b?.waitedMs ?? null) &&
|
|
2056
|
+
(a?.watermarkTime ?? null) === (b?.watermarkTime ?? null))
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
/** 结算当前未配对等待区间:时刻有效则累加 waitedMs,随后清空等待态。 */
|
|
2061
|
+
function settleOpenWait(next, time) {
|
|
2062
|
+
if (next.openWaitStart !== null && time >= next.openWaitStart) {
|
|
2063
|
+
next.waitedMs = (next.waitedMs ?? 0) + (time - next.openWaitStart);
|
|
2064
|
+
}
|
|
2065
|
+
next.openWaitStart = null;
|
|
2066
|
+
next.openWaitKind = null;
|
|
2067
|
+
next.openWaitId = null;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/** 强制关闭开放回合(turn/end 与启动扫描共用):先按时刻结算未配对等待(等待尾部不
|
|
2071
|
+
* 计入运行时长),再把 `time − openTurnStart − waitedMs` 的运行部分累加进 busyMs,
|
|
2072
|
+
* 最后清空回合态;时间逆序时跳过累加、仅清空。 */
|
|
2073
|
+
function settleTurnClose(next, time) {
|
|
2074
|
+
if (next.openWaitStart !== null) settleOpenWait(next, time);
|
|
2075
|
+
if (next.openTurnStart !== null && time >= next.openTurnStart) {
|
|
2076
|
+
const activeMs = time - next.openTurnStart - (next.waitedMs ?? 0);
|
|
2077
|
+
if (activeMs > 0) next.busyMs = (next.busyMs ?? 0) + activeMs;
|
|
1911
2078
|
}
|
|
1912
|
-
|
|
2079
|
+
next.openTurnStart = null;
|
|
2080
|
+
next.waitedMs = null;
|
|
1913
2081
|
}
|
|
1914
2082
|
|
|
1915
|
-
/**
|
|
1916
|
-
|
|
1917
|
-
|
|
2083
|
+
/**
|
|
2084
|
+
* 会话回合统计记账的单步转移(R-01-020):对单个回合/等待边界事件应用后返回新状态。
|
|
2085
|
+
* 状态 `{ busyMs, openTurnStart, openWaitStart, openWaitKind, openWaitId, waitedMs }`——
|
|
2086
|
+
* busyMs 为运行过程时长的累计(null 表示尚无任何有效回合计时),openTurnStart 为当前
|
|
2087
|
+
* 开放回合起点,openWaitStart/openWaitKind/openWaitId 为当前未配对等待区间(回合内
|
|
2088
|
+
* 阻塞等待:'approval' 审批 / 'question' 提问,id 为审批 id 或提问 callId),waitedMs
|
|
2089
|
+
* 为本回合内已配对等待时长的累计;completed/blocked/max-tokens/aborted/error 全部结束
|
|
2090
|
+
* 原因均计入,回合间空闲与回合内等待不计入。等待边界:`approval/asked` 与
|
|
2091
|
+
* `ask_user_question` 的 `tool/call` 开启(记录 id/callId),`approval/decided` 与同
|
|
2092
|
+
* id/callId 的 `tool/result` 结算(id 不匹配的结算事件忽略);等待串行不嵌套(已有
|
|
2093
|
+
* 未配对等待时新边界忽略),`turn/end` 先按事件时刻强制结算未配对等待再以
|
|
2094
|
+
* `time − openTurnStart − waitedMs` 结算回合。start 覆盖式登记(串行回合下最后一个
|
|
2095
|
+
* start 为当前回合),end 配对最近 start;起点缺失或时间逆序的回合跳过累加、仅清空
|
|
2096
|
+
* 状态。event 兼容 history 条目包装与裸事件(eventOf 解包),宿主实时登记与回填共用
|
|
2097
|
+
* 同一转移,保证口径一致(R-01-020/AC-02)。
|
|
2098
|
+
*/
|
|
2099
|
+
function applyTurnEventToStats(state, entry) {
|
|
2100
|
+
const event = eventOf(entry);
|
|
2101
|
+
const time = durationTime(event?.time);
|
|
2102
|
+
const type = event?.type;
|
|
2103
|
+
if (time === null || !isBusyBoundaryEvent(type)) return state;
|
|
2104
|
+
const next = turnStatsFrom(state);
|
|
2105
|
+
if (type === "turn/start") {
|
|
2106
|
+
next.openTurnStart = time;
|
|
2107
|
+
next.openWaitStart = null;
|
|
2108
|
+
next.openWaitKind = null;
|
|
2109
|
+
next.openWaitId = null;
|
|
2110
|
+
next.waitedMs = null;
|
|
2111
|
+
return next;
|
|
2112
|
+
}
|
|
2113
|
+
if (type === "turn/end") {
|
|
2114
|
+
settleTurnClose(next, time);
|
|
2115
|
+
return next;
|
|
2116
|
+
}
|
|
2117
|
+
if (type === "approval/asked") {
|
|
2118
|
+
if (next.openTurnStart === null || next.openWaitStart !== null) return state;
|
|
2119
|
+
next.openWaitStart = time;
|
|
2120
|
+
next.openWaitKind = "approval";
|
|
2121
|
+
next.openWaitId = typeof event?.data?.id === "string" ? event.data.id : null;
|
|
2122
|
+
return next;
|
|
2123
|
+
}
|
|
2124
|
+
if (type === "approval/decided") {
|
|
2125
|
+
if (next.openWaitStart === null || next.openWaitKind !== "approval") return state;
|
|
2126
|
+
if (next.openWaitId !== null && event?.data?.id !== next.openWaitId) return state;
|
|
2127
|
+
settleOpenWait(next, time);
|
|
2128
|
+
return next;
|
|
2129
|
+
}
|
|
2130
|
+
if (type === "tool/call") {
|
|
2131
|
+
if (event?.data?.name !== QUESTION_TOOL_NAME) return state;
|
|
2132
|
+
if (next.openTurnStart === null || next.openWaitStart !== null) return state;
|
|
2133
|
+
next.openWaitStart = time;
|
|
2134
|
+
next.openWaitKind = "question";
|
|
2135
|
+
next.openWaitId = typeof event?.data?.callId === "string" ? event.data.callId : null;
|
|
2136
|
+
return next;
|
|
2137
|
+
}
|
|
2138
|
+
// tool/result:仅结算开启中的提问等待,且 callId 与开启边界一致——其余 result 对
|
|
2139
|
+
// 记账无效果(等待串行,同 callId 的下一个 result 即配对边界)。
|
|
2140
|
+
if (next.openWaitStart === null || next.openWaitKind !== "question") return state;
|
|
2141
|
+
const resultCallId = event?.data?.message?.source?.callId;
|
|
2142
|
+
if (next.openWaitId !== null && resultCallId !== next.openWaitId) return state;
|
|
2143
|
+
settleOpenWait(next, time);
|
|
2144
|
+
return next;
|
|
1918
2145
|
}
|
|
1919
2146
|
|
|
1920
|
-
/**
|
|
1921
|
-
|
|
1922
|
-
|
|
2147
|
+
/**
|
|
2148
|
+
* busy 记账字段归一:null/undefined 直通为 null(「无数据」语义),仅真实有限数字
|
|
2149
|
+
* 保留。必须先判空再走 Number()——Number(null) === 0 会把「无开放回合」误归一为
|
|
2150
|
+
* epoch 0,令合成值膨胀为 now−0(R-01-020/AC-06 回归)。
|
|
2151
|
+
*/
|
|
2152
|
+
function normalizeBusyMs(value) {
|
|
2153
|
+
if (value === null || value === undefined) return null;
|
|
2154
|
+
const n = Number(value);
|
|
2155
|
+
return Number.isFinite(n) ? n : null;
|
|
1923
2156
|
}
|
|
1924
2157
|
|
|
1925
|
-
/**
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2158
|
+
/**
|
|
2159
|
+
* 标题行累计运行时长的显示合成(R-01-020/AC-01、AC-03、AC-06、AC-07):已完成回合
|
|
2160
|
+
* 累计加开放回合的实时已耗时(扣除回合内已配对等待与进行中的未配对等待);两者皆
|
|
2161
|
+
* 不可得时返回 null(调用方不显示,不以 0 冒充)。now 缺失或无效时不推进实时增量,
|
|
2162
|
+
* 只返回已完成累计。等待期间 `(now − openTurnStart)` 与 `(now − openWaitStart)` 两项
|
|
2163
|
+
* 随 now 同步增长相互抵消,显示值自冻结;等待结算时 waitedMs 接管同一增量,恢复
|
|
2164
|
+
* 运行后从冻结值继续,回合结算处连续无跳变。
|
|
2165
|
+
*/
|
|
2166
|
+
function totalBusyDisplayMs({ busyMs = null, openTurnStart = null, waitedMs = null, openWaitStart = null, now = null } = {}) {
|
|
2167
|
+
let total = typeof busyMs === "number" && Number.isFinite(busyMs) && busyMs >= 0 ? busyMs : null;
|
|
2168
|
+
if (openTurnStart !== null && Number.isFinite(openTurnStart) && Number.isFinite(now) && now > openTurnStart) {
|
|
2169
|
+
const waited = typeof waitedMs === "number" && Number.isFinite(waitedMs) && waitedMs >= 0 ? waitedMs : 0;
|
|
2170
|
+
const waitingNow = openWaitStart !== null && Number.isFinite(openWaitStart) && now > openWaitStart ? now - openWaitStart : 0;
|
|
2171
|
+
total = (total ?? 0) + Math.max(0, now - openTurnStart - waited - waitingNow);
|
|
1931
2172
|
}
|
|
1932
|
-
return
|
|
2173
|
+
return total;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/**
|
|
2177
|
+
* 回合统计记账的统一收敛(R-01-020/AC-04、AC-05):对全会话事件列表重放出下一份
|
|
2178
|
+
* 记账,宿主侧懒回填与启动扫描共用。路径选择——无记录、强制重放、持久化水位非法、
|
|
2179
|
+
* 或水位超前于日志最大 seq(事件 seq 空间被重编,如 dsh 0.1.5 V3 迁移)时从空状态
|
|
2180
|
+
* 全量重放(超前水位会把后续全部实时事件封死在守卫之外,openTurnStart 永不清空、
|
|
2181
|
+
* 总耗时无限增长);否则从持久化记账出发仅增量应用 `seq > watermarkSeq` 的事件。
|
|
2182
|
+
* 水位推进处同步记录 `watermarkTime`(水位处已覆盖事件的时刻,与实时登记按效果事件
|
|
2183
|
+
* 写入的口径互为保守——检测只要求该值不超过真实边界事件时刻)——宿主实时登记据此
|
|
2184
|
+
* 识别 seq 空间重编(低 seq 事件携带比水位更新的时刻,增量口径已失效)。
|
|
2185
|
+
* `closeOpenTurn`(宿主启动扫描):重放后仍存在的开放回合按日志最后事件时刻强制
|
|
2186
|
+
* 结算关闭——宿主重启后不存在仍在运行的回合,残留起点只会令总耗时无限增长;尾部
|
|
2187
|
+
* 未配对等待一并按同刻结算(不落到运行时长里)。
|
|
2188
|
+
*/
|
|
2189
|
+
function reconcileTurnStats(current, records, { closeOpenTurn = false, forceFresh = false } = {}) {
|
|
2190
|
+
const list = Array.isArray(records) ? records : [];
|
|
2191
|
+
let maxSeq = null;
|
|
2192
|
+
let lastTime = null;
|
|
2193
|
+
for (const record of list) {
|
|
2194
|
+
const seq = Number(record?.seq);
|
|
2195
|
+
if (Number.isFinite(seq) && (maxSeq === null || seq > maxSeq)) maxSeq = seq;
|
|
2196
|
+
const time = durationTime(eventOf(record)?.time);
|
|
2197
|
+
if (time !== null && (lastTime === null || time > lastTime)) lastTime = time;
|
|
2198
|
+
}
|
|
2199
|
+
const fresh = forceFresh || !isRecord(current) || !Number.isFinite(current.watermarkSeq) || (maxSeq !== null && Number(current.watermarkSeq) > maxSeq);
|
|
2200
|
+
let state = emptyTurnStats();
|
|
2201
|
+
let watermarkSeq = null;
|
|
2202
|
+
let watermarkTime = null;
|
|
2203
|
+
// 水位推进:seq 更大即前推水位;事件时刻有效才更新 watermarkTime(无效不前推)。
|
|
2204
|
+
const advance = (record) => {
|
|
2205
|
+
const seq = Number(record?.seq);
|
|
2206
|
+
if (!Number.isFinite(seq) || (watermarkSeq !== null && seq <= watermarkSeq)) return;
|
|
2207
|
+
watermarkSeq = seq;
|
|
2208
|
+
const time = durationTime(eventOf(record)?.time);
|
|
2209
|
+
if (time !== null) watermarkTime = time;
|
|
2210
|
+
};
|
|
2211
|
+
if (fresh) {
|
|
2212
|
+
for (const record of list) {
|
|
2213
|
+
state = applyTurnEventToStats(state, record);
|
|
2214
|
+
advance(record);
|
|
2215
|
+
}
|
|
2216
|
+
} else {
|
|
2217
|
+
state = turnStatsFrom(current);
|
|
2218
|
+
watermarkTime = state.watermarkTime;
|
|
2219
|
+
watermarkSeq = Number(current.watermarkSeq);
|
|
2220
|
+
for (const record of list) {
|
|
2221
|
+
const seq = Number(record?.seq);
|
|
2222
|
+
if (!Number.isFinite(seq) || seq <= watermarkSeq) continue;
|
|
2223
|
+
state = applyTurnEventToStats(state, record);
|
|
2224
|
+
advance(record);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
if (closeOpenTurn && state.openTurnStart !== null) {
|
|
2228
|
+
// 启动扫描:宿主重启后不存在仍在运行的回合——按日志最后事件时刻强制结算关闭;
|
|
2229
|
+
// 日志不可得(空/缺失)时仅清除回合态,不制造虚假运行时长。
|
|
2230
|
+
if (lastTime !== null) settleTurnClose(state, lastTime);
|
|
2231
|
+
state.openTurnStart = null;
|
|
2232
|
+
state.openWaitStart = null;
|
|
2233
|
+
state.openWaitKind = null;
|
|
2234
|
+
state.openWaitId = null;
|
|
2235
|
+
state.waitedMs = null;
|
|
2236
|
+
}
|
|
2237
|
+
return { ...state, watermarkSeq, watermarkTime };
|
|
1933
2238
|
}
|
|
1934
2239
|
|
|
1935
2240
|
/**
|
|
@@ -2236,8 +2541,9 @@ function scrollCardIntoView(scroll, card, behavior = "smooth") {
|
|
|
2236
2541
|
return Number(scroll.scrollTop) !== currentTop;
|
|
2237
2542
|
}
|
|
2238
2543
|
|
|
2239
|
-
/**
|
|
2240
|
-
|
|
2544
|
+
/** 原生会话输入框:dsh-client-ui-conversation 的 composer 输入面(Lexical contenteditable div,
|
|
2545
|
+
* 宿主以 data-composer-input 显式标记;宿主 0.1.5 重写前为 textarea[data-phase])。 */
|
|
2546
|
+
const COMPOSER_SELECTOR = "[data-composer-input]";
|
|
2241
2547
|
|
|
2242
2548
|
/**
|
|
2243
2549
|
* 抑制切换会话后原生 composer 的自动聚焦(R-01-005/AC-01 移动端回归)。
|
|
@@ -2281,19 +2587,24 @@ function bindBackdropDismiss(backdrop, dismiss) {
|
|
|
2281
2587
|
|
|
2282
2588
|
// dsh-activity-pane 浏览器运行时。
|
|
2283
2589
|
//
|
|
2284
|
-
// 挂载策略:把窗格作为 AppFrame 中 `
|
|
2285
|
-
// (`#root [data-slot="
|
|
2286
|
-
//
|
|
2590
|
+
// 挂载策略:把窗格作为 AppFrame 中 keyed `main` 槽容器的前置兄弟列插入
|
|
2591
|
+
// (`#root [data-slot="main"] || .parentElement` 即中列 flex,dsh 0.1.5 起原
|
|
2592
|
+
// `conversation` Slot 迁移为 `main` 的 `conversation` key),让外壳的让步链
|
|
2593
|
+
// 挤压中间栏;窄屏(<=767px)转为固定抽屉 + 浮动开关按钮。
|
|
2287
2594
|
//
|
|
2288
|
-
// 数据来源:DSH 原生 `sessions` / `workspaces` 客户端服务(推送式快照)+
|
|
2289
|
-
//
|
|
2290
|
-
|
|
2291
|
-
//
|
|
2595
|
+
// 数据来源:DSH 原生 `sessions` / `workspaces` 客户端服务(推送式快照)+ `remote`
|
|
2596
|
+
// 服务的部署级模型目录(session/modelCatalog)与会话日志分页(session/page,
|
|
2597
|
+
// Session 格式 V3——dsh 0.1.5 起快照不再携带会话内容、connection.api 门面移除)+
|
|
2598
|
+
// 运行中会话的原生订阅(binding().session,运行状态面)+ 可选 `modelDirectories`
|
|
2599
|
+
// 目录 store 订阅(模型选择实时更新),不依赖任何第三方插件数据路由,也不做状态轮询。
|
|
2292
2600
|
|
|
2293
2601
|
const name = "dsh-activity-pane";
|
|
2294
|
-
|
|
2602
|
+
// remote.session:dsh 0.1.5 起会话 RPC(目录/日志分页)经 api-remotes 的点分命名空间
|
|
2603
|
+
// 注入(cordis 代理对未注入属性直接抛错,父服务声明不代表子命名空间可用;仅声明
|
|
2604
|
+
// 子命名空间即可解析,无需再注入父面)。
|
|
2605
|
+
const inject = ["sessions", "workspaces", "uiSession", "remote.session"];
|
|
2295
2606
|
|
|
2296
|
-
const CONVERSATION_SELECTOR = "#root [data-slot=\"
|
|
2607
|
+
const CONVERSATION_SELECTOR = "#root [data-slot=\"main\"]";
|
|
2297
2608
|
const PANE_ATTR = "data-dsh-activity-pane";
|
|
2298
2609
|
const PANE_CLASS = "dap-pane";
|
|
2299
2610
|
const LIST_CLASS = "dap-list";
|
|
@@ -2305,7 +2616,7 @@ const INSTANCE_KEY = "__dshActivityPaneCleanup";
|
|
|
2305
2616
|
const WIDTH_STORAGE_KEY = "dsh-activity-pane:width";
|
|
2306
2617
|
const COLLAPSED_WIDTH = 34;
|
|
2307
2618
|
/** 宿主侧完成确认 API 前缀(C-030):acks 快照 / SSE 推送 / ack 写回,同源受信。 */
|
|
2308
|
-
const
|
|
2619
|
+
const PANE_API_BASE = "/dsh-activity-pane/api";
|
|
2309
2620
|
// 缩进槽宽:与连接线 CSS 几何耦合(left:-8px = INDENT_PX/2 缩进槽中线,
|
|
2310
2621
|
// top:-6px/bottom:-2px 对应 .dap-list 的 gap:6px),改任一数值须三处同步;
|
|
2311
2622
|
// scripts/check.mjs 有钉住断言。
|
|
@@ -2713,14 +3024,19 @@ const CSS = `
|
|
|
2713
3024
|
box-shadow: none;
|
|
2714
3025
|
animation: none;
|
|
2715
3026
|
}
|
|
3027
|
+
/* 会话卡标题(活动卡、子代理卡与最近卡共用 .dap-title)统一常规字重,不加粗
|
|
3028
|
+
(R-01-013/AC-09 及东家 2026-09-11 视觉反馈);无按卡类的字重覆盖。 */
|
|
2716
3029
|
[data-dsh-activity-pane] .dap-title {
|
|
2717
3030
|
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
2718
|
-
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight:
|
|
3031
|
+
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight: 400;
|
|
2719
3032
|
}
|
|
2720
|
-
/*
|
|
2721
|
-
|
|
2722
|
-
|
|
3033
|
+
/* 标题行最右侧的累计运行时长(R-01-020/AC-01):固定占位不参与标题挤压,
|
|
3034
|
+
标题过长时以自身省略号让位;色调弱于标题,不与状态点抢视觉。 */
|
|
3035
|
+
[data-dsh-activity-pane] .dap-total-time {
|
|
3036
|
+
flex: none; margin-left: auto; font-size: 12px; line-height: 16px;
|
|
3037
|
+
color: #8a94a3; white-space: nowrap;
|
|
2723
3038
|
}
|
|
3039
|
+
[data-dsh-activity-pane] .dap-total-time[hidden] { display: none; }
|
|
2724
3040
|
/* 等待卡末行首行「类型胶囊」(R-01-002/AC-01、AC-02、AC-09、AC-13,C-043):圆底类型
|
|
2725
3041
|
图标 + 类型文字,色相随等待类别(--dap-wait-color)——阻塞金/完成绿/错误红;
|
|
2726
3042
|
胶囊为行内元素不自占满宽,随文字内容收缩。 */
|
|
@@ -2846,6 +3162,7 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
2846
3162
|
[data-dsh-activity-pane] .dap-progress {
|
|
2847
3163
|
display: flex; align-items: center; gap: 7px; min-width: 0;
|
|
2848
3164
|
}
|
|
3165
|
+
[data-dsh-activity-pane] .dap-progress[hidden] { display: none; }
|
|
2849
3166
|
[data-dsh-activity-pane] .dap-progress .dap-track { flex: 1 1 auto; min-width: 0; }
|
|
2850
3167
|
[data-dsh-activity-pane] .dap-pct {
|
|
2851
3168
|
flex: none; width: 5ch; font-size: 12px; line-height: 15px; font-weight: 700; text-align: right;
|
|
@@ -3316,8 +3633,10 @@ function apply(ctx) {
|
|
|
3316
3633
|
let disposed = false;
|
|
3317
3634
|
let sessions = null;
|
|
3318
3635
|
let workspaces = null;
|
|
3636
|
+
let uiSession = null;
|
|
3319
3637
|
let sessionUnsubscribe = null;
|
|
3320
3638
|
let workspaceUnsubscribe = null;
|
|
3639
|
+
let pendingUnsubscribe = null;
|
|
3321
3640
|
let clockTimer = null;
|
|
3322
3641
|
let recentTimeTimer = null;
|
|
3323
3642
|
let syncScheduled = false;
|
|
@@ -3390,12 +3709,24 @@ function apply(ctx) {
|
|
|
3390
3709
|
const sessionDetailsById = new Map();
|
|
3391
3710
|
/** 会话跳转的单一重试链;避免重复点击叠加 refresh/timer。 */
|
|
3392
3711
|
const openRetryStates = new Map();
|
|
3393
|
-
/**
|
|
3712
|
+
/** 主会话目录一次性 load 的在途记账:id → promise(字段级加载指示消费)。 */
|
|
3394
3713
|
const modelLoads = new Map();
|
|
3714
|
+
/** native cold-session log reads, one promise per session and no polling. */
|
|
3395
3715
|
const historyLoads = new Map();
|
|
3396
3716
|
/** 模型目录订阅(R-01-012/AC-16):id → unsubscribe;模型选择切换经原生
|
|
3397
3717
|
* modelDirectories store 推送即时到达,随可见性清理/卸载先 unsubscribe 再除名。 */
|
|
3398
3718
|
const modelDirectorySubs = new Map();
|
|
3719
|
+
const logSourceSubs = new Map();
|
|
3720
|
+
/** 模型目录分组的 modelId → {name, reasoning} 索引(R-01-012/AC-17):同一部署的
|
|
3721
|
+
* 目录分组在主/子会话间共享,经主会话的目录订阅与一次性 models RPC 就地收割,
|
|
3722
|
+
* 渲染时解析显示名与 effort 回退。无原型对象:模型 id 可能恰为 "constructor" 等
|
|
3723
|
+
* 继承键名,不得穿透回退。 */
|
|
3724
|
+
const catalogEntries = Object.create(null);
|
|
3725
|
+
/** 部署级目录分组(dsh 0.1.5 起模型上下文的显示名/effort 解析来源):由
|
|
3726
|
+
* remote.sessions.modelCatalog 与 modelDirectories 订阅推送共同收割。 */
|
|
3727
|
+
let catalogGroups = [];
|
|
3728
|
+
/** 部署级 modelCatalog 一次性读取的在途 Promise:成功即缓存、失败允许重试。 */
|
|
3729
|
+
let catalogPromise = null;
|
|
3399
3730
|
/** native session.open() requests in flight; avoid duplicate cold history reads. */
|
|
3400
3731
|
/** 冷数据读取并发池:队列顺序即优先级(调用方已排序),逐个完成逐个重绘。 */
|
|
3401
3732
|
const loadQueue = [];
|
|
@@ -3562,7 +3893,7 @@ function apply(ctx) {
|
|
|
3562
3893
|
acksSource = null;
|
|
3563
3894
|
if (disposed || typeof window.EventSource !== "function") return;
|
|
3564
3895
|
try {
|
|
3565
|
-
const source = new window.EventSource(`${
|
|
3896
|
+
const source = new window.EventSource(`${PANE_API_BASE}/acks/stream`);
|
|
3566
3897
|
source.addEventListener("state", (event) => applyAcksState(event.data ?? ""));
|
|
3567
3898
|
acksSource = source;
|
|
3568
3899
|
} catch {
|
|
@@ -3592,7 +3923,7 @@ function apply(ctx) {
|
|
|
3592
3923
|
completeAcksById.set(id, { ...prev, lastTurnEnd: prev?.lastTurnEnd ?? null, ackedAt: Date.now() });
|
|
3593
3924
|
queueSync();
|
|
3594
3925
|
try {
|
|
3595
|
-
const response = await fetch(`${
|
|
3926
|
+
const response = await fetch(`${PANE_API_BASE}/ack`, {
|
|
3596
3927
|
method: "POST",
|
|
3597
3928
|
headers: { "Content-Type": "application/json" },
|
|
3598
3929
|
body: JSON.stringify({ sessionId: id }),
|
|
@@ -3606,8 +3937,137 @@ function apply(ctx) {
|
|
|
3606
3937
|
}
|
|
3607
3938
|
}
|
|
3608
3939
|
|
|
3609
|
-
|
|
3610
|
-
|
|
3940
|
+
// ---- 累计运行时长通道(R-01-020,C-074) ----
|
|
3941
|
+
// SSE 订阅宿主侧回合统计:连接即收全量快照(刷新/重连恢复),此后每次变更(回合
|
|
3942
|
+
// 边界、回填完成)推送新全量。无 EventSource 环境静默降级为不显示累计时长。
|
|
3943
|
+
// 可见会话的懒回填经 GET /busy?ids= 触发(fire-and-forget),完成后经同一 SSE 广播。
|
|
3944
|
+
const busyById = new Map(); // sessionId -> { busyMs, openTurnStart }
|
|
3945
|
+
const busyRequestedIds = new Set(); // 已触发过回填的会话 id(宿主侧单飞,本地防重复请求)
|
|
3946
|
+
const busyRetryAtById = new Map(); // sessionId -> 回填失败后的最早重试时刻(30s 退避,不逼近轮询)
|
|
3947
|
+
let busySource = null;
|
|
3948
|
+
let busyFetchInflight = false;
|
|
3949
|
+
|
|
3950
|
+
/** SSE 全量快照应用:接受已解析对象或原始 JSON 文本;解析失败静默丢弃。 */
|
|
3951
|
+
function applyBusyState(input) {
|
|
3952
|
+
if (disposed) return;
|
|
3953
|
+
let state = typeof input === "object" ? input : null;
|
|
3954
|
+
if (state === null) {
|
|
3955
|
+
try {
|
|
3956
|
+
state = JSON.parse(input);
|
|
3957
|
+
} catch {
|
|
3958
|
+
state = null;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
if (state === null || typeof state !== "object") return;
|
|
3962
|
+
busyById.clear();
|
|
3963
|
+
for (const [id, record] of Object.entries(state)) {
|
|
3964
|
+
busyById.set(String(id), {
|
|
3965
|
+
busyMs: normalizeBusyMs(record?.busyMs),
|
|
3966
|
+
openTurnStart: normalizeBusyMs(record?.openTurnStart),
|
|
3967
|
+
waitedMs: normalizeBusyMs(record?.waitedMs),
|
|
3968
|
+
openWaitStart: normalizeBusyMs(record?.openWaitStart),
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
queueSync();
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
/** 累计运行时长注入(R-01-020/AC-01、AC-03、AC-07):已完成回合累计 + 开放回合实时
|
|
3975
|
+
* 已耗时(扣除等待区间);渲染期按 now 合成,阻塞等待期间公式自冻结;子代理条目
|
|
3976
|
+
* 不注入(entry.totalBusyMs 保持 undefined → 节点隐藏)。 */
|
|
3977
|
+
function applyTotalBusy(entry, now) {
|
|
3978
|
+
const busyRecord = busyById.get(entry.id);
|
|
3979
|
+
entry.totalBusyMs = totalBusyDisplayMs({
|
|
3980
|
+
busyMs: busyRecord?.busyMs ?? null,
|
|
3981
|
+
openTurnStart: busyRecord?.openTurnStart ?? null,
|
|
3982
|
+
waitedMs: busyRecord?.waitedMs ?? null,
|
|
3983
|
+
openWaitStart: busyRecord?.openWaitStart ?? null,
|
|
3984
|
+
now,
|
|
3985
|
+
});
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
/** 触发可见主会话的懒回填:只对未请求过的 id 发一次 GET;在途时跳过(SSE 广播兜底)。 */
|
|
3989
|
+
function requestBusyBackfill(ids) {
|
|
3990
|
+
if (disposed || typeof fetch !== "function") return;
|
|
3991
|
+
const pending = ids.filter((id) => !busyRequestedIds.has(id) && !busyById.has(id) && Date.now() >= (busyRetryAtById.get(id) ?? 0));
|
|
3992
|
+
if (pending.length === 0 || busyFetchInflight) return;
|
|
3993
|
+
for (const id of pending) busyRequestedIds.add(id);
|
|
3994
|
+
busyFetchInflight = true;
|
|
3995
|
+
fetch(`${PANE_API_BASE}/busy?ids=${encodeURIComponent(pending.join(","))}`)
|
|
3996
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
3997
|
+
.then((state) => {
|
|
3998
|
+
if (state !== null && typeof state === "object") applyBusyState(state);
|
|
3999
|
+
})
|
|
4000
|
+
.catch(() => {
|
|
4001
|
+
// 回填触发失败:30s 退避后允许重试,不逐帧逼近轮询形态。
|
|
4002
|
+
const retryAt = Date.now() + 30_000;
|
|
4003
|
+
for (const id of pending) {
|
|
4004
|
+
busyRequestedIds.delete(id);
|
|
4005
|
+
busyRetryAtById.set(id, retryAt);
|
|
4006
|
+
}
|
|
4007
|
+
})
|
|
4008
|
+
.finally(() => {
|
|
4009
|
+
busyFetchInflight = false;
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
/** (重)建 SSE 连接:先关闭旧连接再新建;连接即收宿主侧全量快照。 */
|
|
4014
|
+
function connectBusyStream() {
|
|
4015
|
+
try {
|
|
4016
|
+
busySource?.close();
|
|
4017
|
+
} catch {}
|
|
4018
|
+
busySource = null;
|
|
4019
|
+
if (disposed || typeof window.EventSource !== "function") return;
|
|
4020
|
+
try {
|
|
4021
|
+
const source = new window.EventSource(`${PANE_API_BASE}/busy/stream`);
|
|
4022
|
+
source.addEventListener("state", (event) => applyBusyState(event.data ?? ""));
|
|
4023
|
+
busySource = source;
|
|
4024
|
+
} catch {
|
|
4025
|
+
busySource = null;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
// 回到前台(含 bfcache 还原)时 busy 通道自愈:重建 SSE,借连接全量快照收敛;
|
|
4029
|
+
// 具名 handler 使 cleanup 能移除监听(与 acks 通道同模式,R-02-003)。
|
|
4030
|
+
const onBusyVisibilityResume = () => {
|
|
4031
|
+
if (document.visibilityState === "visible" && !disposed) connectBusyStream();
|
|
4032
|
+
};
|
|
4033
|
+
const onBusyPageShow = (event) => {
|
|
4034
|
+
if (event?.persisted === true && !disposed) connectBusyStream();
|
|
4035
|
+
};
|
|
4036
|
+
document.addEventListener("visibilitychange", onBusyVisibilityResume);
|
|
4037
|
+
window.addEventListener("pageshow", onBusyPageShow);
|
|
4038
|
+
connectBusyStream();
|
|
4039
|
+
|
|
4040
|
+
function remoteValue(response) {
|
|
4041
|
+
if (response?.ok === true) return response.value;
|
|
4042
|
+
throw response?.error ?? new Error("remote request failed");
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
/** 会话日志分页地址(R-01-012):主会话 `{kind:"session", sessionId}`;子代理
|
|
4046
|
+
* `{kind:"subagent", parentSessionId, childSessionId, mode}`——母会话 id 兼容
|
|
4047
|
+
* `parentSessionId` / `parentId` 两种条目键名,mode 无条目标注时按一次性子代理
|
|
4048
|
+
* 处理、读取失败由 pagedHistoryEvents 以 null 收敛为空白详情(R-01-013)。 */
|
|
4049
|
+
function sessionPageAddress(id, byId) {
|
|
4050
|
+
const row = byId[id] ?? {};
|
|
4051
|
+
if (row?.origin === "subagent") {
|
|
4052
|
+
const parentId = row.parentSessionId ?? row.parentId;
|
|
4053
|
+
if (parentId !== undefined && parentId !== null) {
|
|
4054
|
+
return {
|
|
4055
|
+
kind: "subagent",
|
|
4056
|
+
parentSessionId: String(parentId),
|
|
4057
|
+
childSessionId: String(id),
|
|
4058
|
+
mode: row.continuable === true ? "continuable" : "one-shot",
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
return { kind: "session", sessionId: String(id) };
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
/** 部署级目录分组收割(R-01-012/AC-01):modelCatalog RPC 与 modelDirectories
|
|
4066
|
+
* 目录订阅推送共享同一部署目录,后到覆盖(分组集合一致);展平索引进
|
|
4067
|
+
* `catalogEntries` 供子代理溯源 id 解析显示名。空分组不覆盖既有缓存。 */
|
|
4068
|
+
function harvestCatalog(groups) {
|
|
4069
|
+
if (Array.isArray(groups) && groups.length > 0) catalogGroups = groups;
|
|
4070
|
+
Object.assign(catalogEntries, catalogModelEntries(groups));
|
|
3611
4071
|
}
|
|
3612
4072
|
|
|
3613
4073
|
/** 同帧重启等待提醒动画:数量胶囊保持亮度呼吸,卡片末行保持 opacity 脉冲,
|
|
@@ -3632,16 +4092,17 @@ function apply(ctx) {
|
|
|
3632
4092
|
try {
|
|
3633
4093
|
directory = ctx.get("modelDirectories")?.directoryFor?.(id) ?? null;
|
|
3634
4094
|
} catch {
|
|
3635
|
-
directory = null; // 会话无 scope
|
|
4095
|
+
directory = null; // 会话无 scope:回落日志提取
|
|
3636
4096
|
}
|
|
3637
4097
|
if (directory === null) return;
|
|
3638
4098
|
const syncFromDirectory = () => {
|
|
3639
4099
|
if (disposed) return;
|
|
3640
4100
|
const snap = directory.store?.getSnapshot?.();
|
|
3641
4101
|
if (!snap?.current) return; // 目录未就绪:不覆写既有取值
|
|
4102
|
+
harvestCatalog(snap.groups);
|
|
3642
4103
|
detail.models = { current: snap.current, groups: snap.groups ?? [] };
|
|
3643
4104
|
detail.model = modelMetadata(detail.models);
|
|
3644
|
-
//
|
|
4105
|
+
// 订阅已产值标记:晚到的一次性快照不得回写切换前的旧值。
|
|
3645
4106
|
detail.modelLive = true;
|
|
3646
4107
|
queueSync();
|
|
3647
4108
|
};
|
|
@@ -3649,17 +4110,48 @@ function apply(ctx) {
|
|
|
3649
4110
|
try {
|
|
3650
4111
|
unsubscribe = directory.store.subscribe(syncFromDirectory);
|
|
3651
4112
|
} catch {
|
|
3652
|
-
return; //
|
|
4113
|
+
return; // 订阅失败:保持日志提取结果
|
|
3653
4114
|
}
|
|
3654
4115
|
modelDirectorySubs.set(id, unsubscribe);
|
|
3655
|
-
syncFromDirectory(); //
|
|
4116
|
+
syncFromDirectory(); // 目录已被主窗口加载时立即同步,该会话免发一次性读取
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
/** 目录 store 一次性 load(dsh 0.1.5 起惰性加载:不 load 不产出当前选择,原生模型
|
|
4120
|
+
* 选择器同样先 load)。load 与 select() 的 generation 竞争以「最新操作胜出」,用户
|
|
4121
|
+
* 切换后到仍胜出。e2e 接缝(dap-e2e-model-delay)延迟 load 发起,使模型上下文
|
|
4122
|
+
* 严格晚于时间线呈现,渐进渲染可观察(R-01-014/AC-03)。 */
|
|
4123
|
+
function loadDirectoryOnce(id) {
|
|
4124
|
+
if (disposed || !modelDirectorySubs.has(id) || modelLoads.has(id)) return;
|
|
4125
|
+
let directory = null;
|
|
4126
|
+
try {
|
|
4127
|
+
directory = ctx.get("modelDirectories")?.directoryFor?.(id) ?? null;
|
|
4128
|
+
} catch {
|
|
4129
|
+
directory = null;
|
|
4130
|
+
}
|
|
4131
|
+
if (directory === null) return;
|
|
4132
|
+
try {
|
|
4133
|
+
const loadPromise = delayedModelCall(() => (typeof directory.load === "function" ? directory.load() : null));
|
|
4134
|
+
if (loadPromise && typeof loadPromise.finally === "function") {
|
|
4135
|
+
modelLoads.set(id, loadPromise);
|
|
4136
|
+
// 守卫只在失败时释放(放行下一次渲染重试);成功后保持到可见性 prune,
|
|
4137
|
+
// 兑现一次性 load 语义。若 settle 即释放,目录 store 的每次广播
|
|
4138
|
+
// (syncInputs 无条件以新引用 set)都会再驱动 render → load → 广播,
|
|
4139
|
+
// 形成 rAF 速率反馈环——空闲期 60fps 满载,手机端表现为持续发烫。
|
|
4140
|
+
loadPromise.catch(() => {
|
|
4141
|
+
if (modelLoads.get(id) === loadPromise) modelLoads.delete(id);
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
loadPromise?.catch(() => {});
|
|
4145
|
+
} catch {
|
|
4146
|
+
// load 不可用:保持订阅,等待主窗口加载后的推送。
|
|
4147
|
+
}
|
|
3656
4148
|
}
|
|
3657
4149
|
|
|
3658
|
-
function loadNativeDetails(ids, previewFallbackIds = new Set(), durationFallbackIds = new Set()) {
|
|
3659
|
-
const
|
|
3660
|
-
if (!
|
|
4150
|
+
function loadNativeDetails({ ids, previewFallbackIds = new Set(), durationFallbackIds = new Set(), subagentModelReadIds = new Set() }) {
|
|
4151
|
+
const sessionRemote = ctx.get("remote.session") ?? null;
|
|
4152
|
+
if (!sessionRemote) return;
|
|
4153
|
+
ensureCatalogGroups(sessionRemote);
|
|
3661
4154
|
const byId = getSnapshot(sessions, "list")?.byId ?? {};
|
|
3662
|
-
const modelPromises = [];
|
|
3663
4155
|
const historyPromises = [];
|
|
3664
4156
|
for (const id of ids) {
|
|
3665
4157
|
const detail = sessionDetailsById.get(id) ?? {};
|
|
@@ -3667,80 +4159,55 @@ function apply(ctx) {
|
|
|
3667
4159
|
if (liveSnapshot) detail.snapshot = liveSnapshot;
|
|
3668
4160
|
sessionDetailsById.set(id, detail);
|
|
3669
4161
|
const subagent = isSubagentRow(byId[id], byId);
|
|
3670
|
-
//
|
|
3671
|
-
//
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
const plan = detailLoadPlan({
|
|
3686
|
-
detail,
|
|
3687
|
-
isSubagent: subagent,
|
|
3688
|
-
snapshotReady,
|
|
3689
|
-
historyNeeded: needsHistorySnapshot(detail.snapshot),
|
|
3690
|
-
previewFallbackNeeded: previewFallbackIds.has(id),
|
|
3691
|
-
durationFallbackNeeded,
|
|
3692
|
-
windowComplete,
|
|
3693
|
-
modelInflight: modelLoads.has(id),
|
|
3694
|
-
historyInflight: historyLoads.has(id) || sessionOpenLoads.has(id),
|
|
3695
|
-
});
|
|
3696
|
-
if (plan.subagent) {
|
|
3697
|
-
// 子代理的 models 读取必被宿主以 agent-busy 拒绝:直接留空,不发注定失败的 RPC。
|
|
3698
|
-
detail.model ??= { model: "", reasoning: "" };
|
|
3699
|
-
} else if (plan.model && typeof api.models === "function") {
|
|
3700
|
-
const promise = enqueueDetailLoad(() => delayedModelCall(() => api.models({ sessionId: id }))
|
|
3701
|
-
.then((response) => {
|
|
3702
|
-
const value = apiValue(response);
|
|
3703
|
-
if (!value) {
|
|
3704
|
-
if (!detail.modelLive) detail.model = { model: "", reasoning: "" };
|
|
3705
|
-
return;
|
|
3706
|
-
}
|
|
3707
|
-
// 目录订阅已产出更新的当前选择时,晚到的 RPC 快照不得回写旧值(R-01-012/AC-16)。
|
|
3708
|
-
if (detail.modelLive) return;
|
|
3709
|
-
detail.models = value;
|
|
3710
|
-
detail.model = modelMetadata(value);
|
|
3711
|
-
})
|
|
3712
|
-
.catch((error) => {
|
|
3713
|
-
if (!detail.modelLive) detail.model = { model: "", reasoning: "" };
|
|
3714
|
-
detail.modelError = error instanceof Error ? error.message : String(error);
|
|
3715
|
-
}));
|
|
3716
|
-
modelLoads.set(id, promise);
|
|
3717
|
-
// settle 即移除记账:在途判定驱动加载指示,残留会让空字段永久误报加载。
|
|
3718
|
-
promise.finally(() => {
|
|
3719
|
-
if (modelLoads.get(id) === promise) modelLoads.delete(id);
|
|
3720
|
-
});
|
|
3721
|
-
modelPromises.push(promise);
|
|
4162
|
+
// 主会话建立模型目录订阅:dsh 0.1.5 起目录 store 惰性加载(不 load 不产出当前
|
|
4163
|
+
// 选择),订阅与一次性 load 一并触发;子代理的目录不可用(宿主以 agent-busy
|
|
4164
|
+
// 拒绝其模型 RPC),不建立订阅。e2e 接缝把订阅与 load 整体延后,使模型上下文
|
|
4165
|
+
// 严格晚于时间线呈现(R-01-014/AC-03 渐进语义可观察)。
|
|
4166
|
+
if (!subagent) {
|
|
4167
|
+
if (e2eModelDelayMs === 0) {
|
|
4168
|
+
subscribeModelDirectory(id, detail);
|
|
4169
|
+
loadDirectoryOnce(id);
|
|
4170
|
+
} else {
|
|
4171
|
+
delayedModelCall(() => {
|
|
4172
|
+
subscribeModelDirectory(id, detail);
|
|
4173
|
+
loadDirectoryOnce(id);
|
|
4174
|
+
return null;
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
3722
4177
|
}
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
4178
|
+
captureSessionLog(id, { subagent, cwd: byId[id]?.cwd ?? "" });
|
|
4179
|
+
// 长会话深读兜底(R-01-013/AC-03):最近卡预览/子代理溯源不在尾页日志窗口内
|
|
4180
|
+
// 且宿主标记 hasMore 时,按 beforeSeq 向前回溯翻页(默认无页数上限)。
|
|
4181
|
+
const windowEntries = Array.isArray(detail.log?.entries) ? detail.log.entries : [];
|
|
4182
|
+
const lastSeq = Number(windowEntries.at(-1)?.event?.seq);
|
|
4183
|
+
const deepReadNeeded =
|
|
4184
|
+
(previewFallbackIds.has(id) || durationFallbackIds.has(id) || (subagent && subagentModelReadIds.has(id))) &&
|
|
4185
|
+
detail.log?.hasMore === true &&
|
|
4186
|
+
Number.isFinite(lastSeq) &&
|
|
4187
|
+
detail.historyDeepReadDone !== true;
|
|
4188
|
+
// 在途守卫:深翻未在途才入队。渲染逐帧发生而 deepReadNeeded 在深翻完成前恒真,
|
|
4189
|
+
// 无守卫时每帧重复入队新 job,队列以 LOAD_CONCURRENCY 并发放大发——同一会话的
|
|
4190
|
+
// page RPC 风暴,且 historyLoads 的 set/delete 随 job 翻转驱动加载指示高频闪烁。
|
|
4191
|
+
if (deepReadNeeded && !historyLoads.has(id)) {
|
|
4192
|
+
detail.previewFallbackLoaded = true;
|
|
4193
|
+
detail.durationFallbackLoaded = true;
|
|
4194
|
+
const address = sessionPageAddress(id, byId);
|
|
3726
4195
|
const promise = enqueueDetailLoad(() => Promise.resolve()
|
|
3727
4196
|
.then(async () => {
|
|
3728
|
-
// 单池任务内串行回溯深翻(默认无页数上限):向前翻到命中最近一条
|
|
3729
|
-
// 用户消息或翻尽为止——超长会话的最后用户消息可能在尾页窗口之外,
|
|
3730
|
-
// 固定页数上限会让历史卡用户预览永久缺失(R-01-013/AC-03 回溯承诺)。
|
|
3731
|
-
// 运行会话缺窗口内回合起点时要求深翻至命中开放回合 turn/start
|
|
3732
|
-
// (R-01-009/AC-06 冷窗口兜底)。
|
|
3733
4197
|
const { events, error } = await pagedHistoryEvents({
|
|
3734
|
-
fetchPage: async (beforeSeq) =>
|
|
3735
|
-
|
|
4198
|
+
fetchPage: async (beforeSeq) => {
|
|
4199
|
+
const value = remoteValue(
|
|
4200
|
+
await sessionRemote.page({ address, throughSeq: lastSeq, beforeSeq, maxMessages: 50 }),
|
|
4201
|
+
);
|
|
4202
|
+
if (!value) return null;
|
|
4203
|
+
return { events: Array.isArray(value.records) ? value.records : [], hasMore: value.hasMore === true };
|
|
4204
|
+
},
|
|
3736
4205
|
});
|
|
3737
4206
|
if (error) detail.historyError = error instanceof Error ? error.message : String(error);
|
|
3738
4207
|
detail.history = events;
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
detail.previews = messagePreviews({ history: events });
|
|
3743
|
-
}))
|
|
4208
|
+
detail.historyDeepReadDone = true;
|
|
4209
|
+
applyLogEvents(id, detail, events, { subagent, cwd: byId[id]?.cwd ?? "", planSubagentModelRead: subagent });
|
|
4210
|
+
}));
|
|
3744
4211
|
historyLoads.set(id, promise);
|
|
3745
4212
|
promise.finally(() => {
|
|
3746
4213
|
if (historyLoads.get(id) === promise) historyLoads.delete(id);
|
|
@@ -3748,7 +4215,7 @@ function apply(ctx) {
|
|
|
3748
4215
|
historyPromises.push(promise);
|
|
3749
4216
|
}
|
|
3750
4217
|
}
|
|
3751
|
-
const pending =
|
|
4218
|
+
const pending = historyPromises;
|
|
3752
4219
|
if (pending.length > 0) {
|
|
3753
4220
|
// 逐个完成即重绘(先就绪先显示,不等待全部,R-01-014/AC-03);
|
|
3754
4221
|
// 并立即重绘一次让加载指示在数据返回前出现。
|
|
@@ -3757,15 +4224,115 @@ function apply(ctx) {
|
|
|
3757
4224
|
}
|
|
3758
4225
|
}
|
|
3759
4226
|
|
|
4227
|
+
/** 日志页落地后的详情派生(R-01-012、R-01-017):时间线折叠、消息预览与子代理模型上下文。
|
|
4228
|
+
* 子代理沿用溯源链(assistant/message source.model + 请求头 effort,T-122);主会话模型
|
|
4229
|
+
* 不经日志提取(目录链订阅 + 一次性 load 承载,见 installServiceSubscriptions 注)。
|
|
4230
|
+
* 无请求头/无溯源时保持空白,不以部署默认或其它会话取值冒充(R-01-012/AC-18)。 */
|
|
4231
|
+
function applyLogEvents(id, detail, events, { subagent, cwd = "", planSubagentModelRead = false }) {
|
|
4232
|
+
detail.history = events;
|
|
4233
|
+
detail.timeline = foldedHistoryTimeline(events, 4, cwd);
|
|
4234
|
+
detail.previews = messagePreviews({ history: events });
|
|
4235
|
+
if (subagent) {
|
|
4236
|
+
if (planSubagentModelRead) {
|
|
4237
|
+
detail.modelReadDone = true;
|
|
4238
|
+
if (!detail.model) detail.model = modelFromHistoryEvents(events);
|
|
4239
|
+
} else if (!detail.model) {
|
|
4240
|
+
detail.model = modelFromHistoryEvents(events);
|
|
4241
|
+
}
|
|
4242
|
+
if (detail.model && !detail.model.reasoning) {
|
|
4243
|
+
detail.model.reasoning = reasoningEffortFromHistoryEvents(events) ?? "";
|
|
4244
|
+
}
|
|
4245
|
+
return;
|
|
4246
|
+
}
|
|
4247
|
+
// 主会话模型上下文不经日志提取:以 modelDirectories 目录订阅为准(实时推送
|
|
4248
|
+
// 当前选择),不可得时保持空白,不以日志历史取值或部署默认冒充(R-01-012/AC-18)。
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
/** 绑定会话并水合事件源(dsh 0.1.5 起会话内容经 eventSource 流式下发:打开即收
|
|
4252
|
+
* 完整日志窗口 + 实时尾,原生 Conversation 同源)。冷会话补一次 open(),日志窗口
|
|
4253
|
+
* 快照引用变化即重派生详情(时间线/预览/模型),窗口由宿主按消息对齐分页。 */
|
|
4254
|
+
function captureSessionLog(id, { subagent, cwd }) {
|
|
4255
|
+
const detail = sessionDetailsById.get(id) ?? {};
|
|
4256
|
+
sessionDetailsById.set(id, detail);
|
|
4257
|
+
let session = null;
|
|
4258
|
+
try {
|
|
4259
|
+
session = sessions?.binding?.(id)?.session ?? null;
|
|
4260
|
+
} catch {
|
|
4261
|
+
session = null;
|
|
4262
|
+
}
|
|
4263
|
+
if (session === null) return;
|
|
4264
|
+
// 日志窗口无效化订阅(事件驱动,不构成轮询,R-02-004):等待/历史卡无会话状态
|
|
4265
|
+
// 订阅(syncLiveness 只订阅运行中),窗口推进(回合收尾、流式尾)必须经此回调
|
|
4266
|
+
// 重读快照并重绘;否则窗口更新只能靠渲染期重读兜底——渲染一旦静默(一次性
|
|
4267
|
+
// load 修复后),完成提醒耗时等窗口派生数据将永久停摆。随可见性 prune。
|
|
4268
|
+
if (typeof session.eventSource?.subscribe === "function" && !logSourceSubs.has(id)) {
|
|
4269
|
+
try {
|
|
4270
|
+
logSourceSubs.set(
|
|
4271
|
+
id,
|
|
4272
|
+
session.eventSource.subscribe(() => {
|
|
4273
|
+
if (disposed) return;
|
|
4274
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
4275
|
+
captureSessionLog(id, {
|
|
4276
|
+
subagent: isSubagentRow(listSnap?.byId?.[id], listSnap ?? {}),
|
|
4277
|
+
cwd: listSnap?.byId?.[id]?.cwd ?? "",
|
|
4278
|
+
});
|
|
4279
|
+
queueSync();
|
|
4280
|
+
}),
|
|
4281
|
+
);
|
|
4282
|
+
} catch {
|
|
4283
|
+
// 订阅不可用:回退渲染期重读。
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
try {
|
|
4287
|
+
// 日志窗口缺席才发起 open:非订阅会话(等待/历史卡)无快照推送,openState 永远
|
|
4288
|
+
// 不可知,只以 openState 把关会对已水合会话逐帧重发 open,settle→delete→重发
|
|
4289
|
+
// 的在途翻转同样驱动加载指示抖动(docsim 卡闪烁根因之一)。
|
|
4290
|
+
if (!detail.log && detail.snapshot?.openState !== "open" && !sessionOpenLoads.has(id) && typeof session.open === "function") {
|
|
4291
|
+
const opening = Promise.resolve(session.open()).catch(() => {});
|
|
4292
|
+
sessionOpenLoads.set(id, opening);
|
|
4293
|
+
opening.finally(() => {
|
|
4294
|
+
if (sessionOpenLoads.get(id) === opening) sessionOpenLoads.delete(id);
|
|
4295
|
+
queueSync();
|
|
4296
|
+
});
|
|
4297
|
+
}
|
|
4298
|
+
} catch {
|
|
4299
|
+
// open 不可用:保持当前窗口。
|
|
4300
|
+
}
|
|
4301
|
+
const log = session.eventSource?.getSnapshot?.() ?? null;
|
|
4302
|
+
if (log === detail.log) return;
|
|
4303
|
+
detail.log = log;
|
|
4304
|
+
const entries = Array.isArray(log?.entries) ? log.entries : [];
|
|
4305
|
+
applyLogEvents(id, detail, entries, { subagent, cwd });
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
/** 部署级模型目录一次性读取(R-01-012/AC-01):dsh 0.1.5 起 per-session models
|
|
4309
|
+
* RPC 移除,目录经 `remote.session.modelCatalog()` 无参数读取;成功即缓存不再
|
|
4310
|
+
* 重发,失败留待下轮详情读取重试。 */
|
|
4311
|
+
function ensureCatalogGroups(sessionRemote) {
|
|
4312
|
+
if (catalogGroups.length > 0 || catalogPromise !== null) return;
|
|
4313
|
+
catalogPromise = Promise.resolve()
|
|
4314
|
+
.then(() => remoteValue(sessionRemote.modelCatalog()))
|
|
4315
|
+
.then((value) => {
|
|
4316
|
+
if (value && Array.isArray(value.groups)) harvestCatalog(value.groups);
|
|
4317
|
+
})
|
|
4318
|
+
.catch(() => {})
|
|
4319
|
+
.finally(() => {
|
|
4320
|
+
catalogPromise = null;
|
|
4321
|
+
});
|
|
4322
|
+
}
|
|
4323
|
+
|
|
3760
4324
|
function installServiceSubscriptions() {
|
|
3761
4325
|
const nextSessions = ctx.get("sessions");
|
|
3762
4326
|
const nextWorkspaces = ctx.get("workspaces");
|
|
3763
|
-
|
|
4327
|
+
const nextUiSession = ctx.get("uiSession");
|
|
4328
|
+
if (nextSessions === sessions && nextWorkspaces === workspaces && nextUiSession === uiSession) return;
|
|
3764
4329
|
|
|
3765
4330
|
sessionUnsubscribe?.();
|
|
3766
4331
|
workspaceUnsubscribe?.();
|
|
4332
|
+
pendingUnsubscribe?.();
|
|
3767
4333
|
sessions = nextSessions ?? null;
|
|
3768
4334
|
workspaces = nextWorkspaces ?? null;
|
|
4335
|
+
uiSession = nextUiSession ?? null;
|
|
3769
4336
|
try {
|
|
3770
4337
|
sessionUnsubscribe = sessions?.list?.subscribe?.(queueSync) ?? null;
|
|
3771
4338
|
} catch {
|
|
@@ -3776,6 +4343,14 @@ function apply(ctx) {
|
|
|
3776
4343
|
} catch {
|
|
3777
4344
|
workspaceUnsubscribe = null;
|
|
3778
4345
|
}
|
|
4346
|
+
try {
|
|
4347
|
+
// 0.1.5 起 sessions 行不再承载 pendingInteraction:待确认/待审查/待回复改由
|
|
4348
|
+
// uiSession.pendingInteractions(Map<sessionId, interaction>)独立承载,
|
|
4349
|
+
// 订阅其快照变化驱动等待卡重算(R-01-002/AC-03)。
|
|
4350
|
+
pendingUnsubscribe = uiSession?.pendingInteractions?.subscribe?.(queueSync) ?? null;
|
|
4351
|
+
} catch {
|
|
4352
|
+
pendingUnsubscribe = null;
|
|
4353
|
+
}
|
|
3779
4354
|
|
|
3780
4355
|
queueSync();
|
|
3781
4356
|
}
|
|
@@ -4031,6 +4606,23 @@ function apply(ctx) {
|
|
|
4031
4606
|
foot.append(capsule, noteRow);
|
|
4032
4607
|
}
|
|
4033
4608
|
|
|
4609
|
+
/** 进度行骨架(运行卡与子代理卡共用,R-01-009/AC-06、AC-14):可伸缩轨道 + 固定宽百分比。 */
|
|
4610
|
+
function makeProgressRow() {
|
|
4611
|
+
const track = makeEl("div", "dap-track");
|
|
4612
|
+
track.append(makeEl("div", "dap-fill"));
|
|
4613
|
+
const progressRow = makeEl("div", "dap-progress");
|
|
4614
|
+
progressRow.append(track, makeEl("span", "dap-pct"));
|
|
4615
|
+
return progressRow;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
/** 统计行骨架:左列 token/速率/命中率(超长省略号截断),时长固定最右(R-01-009/AC-05)。 */
|
|
4619
|
+
function makeStatsRow() {
|
|
4620
|
+
const statsRow = makeEl("div", "dap-token-stats");
|
|
4621
|
+
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
4622
|
+
statsRow.hidden = true;
|
|
4623
|
+
return statsRow;
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4034
4626
|
/** 静态骨架卡片;动态文本一律走 textContent,规避 HTML 注入。 */
|
|
4035
4627
|
function cardChildren(kind) {
|
|
4036
4628
|
const head = makeEl("div", "dap-card-head");
|
|
@@ -4038,15 +4630,22 @@ function apply(ctx) {
|
|
|
4038
4630
|
const workspaceIcon = makeEl("span", "dap-workspace-icon");
|
|
4039
4631
|
workspaceIcon.append(createWorkspaceFolderIcon());
|
|
4040
4632
|
workspace.append(workspaceIcon, makeEl("span", "dap-workspace-text"));
|
|
4041
|
-
|
|
4633
|
+
const model = makeEl("div", "dap-model");
|
|
4634
|
+
head.append(workspace, model);
|
|
4042
4635
|
if (kind === "subagent") {
|
|
4043
4636
|
const row = makeEl("div", "dap-row");
|
|
4044
|
-
|
|
4045
|
-
|
|
4637
|
+
// 模型名与标题同行右对齐(东家反馈):子代理卡无工作区徽标与累计时长,
|
|
4638
|
+
// 标题行右侧即模型位;空值时区域隐藏不占位(R-01-012/AC-17、AC-18)。
|
|
4639
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), model, makeEl("span", "dap-total-time"));
|
|
4640
|
+
// 运行中子代理卡与主会话运行卡同构的进度行与统计行(R-01-009/AC-14);
|
|
4641
|
+
// 非运行时进度行隐藏、统计行冻结(R-01-009/AC-15)。
|
|
4642
|
+
const progressRow = makeProgressRow();
|
|
4643
|
+
progressRow.hidden = true;
|
|
4644
|
+
return [row, makeEl("div", "dap-subtrace"), progressRow, makeStatsRow()];
|
|
4046
4645
|
}
|
|
4047
4646
|
if (kind === "recent") {
|
|
4048
4647
|
const row = makeEl("div", "dap-row");
|
|
4049
|
-
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
4648
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-total-time"));
|
|
4050
4649
|
const userLine = makeEl("div", "dap-history-line");
|
|
4051
4650
|
userLine.dataset.role = "user";
|
|
4052
4651
|
const userIcon = makeEl("span", "dap-history-icon");
|
|
@@ -4061,14 +4660,11 @@ function apply(ctx) {
|
|
|
4061
4660
|
const agentLabel = makeEl("span", "dap-history-label");
|
|
4062
4661
|
agentLabel.textContent = "助手";
|
|
4063
4662
|
agentLine.append(agentIcon, agentLabel, makeEl("span", "dap-history-separator"), makeEl("span", "dap-history-text"));
|
|
4064
|
-
|
|
4065
|
-
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
4066
|
-
statsRow.hidden = true;
|
|
4067
|
-
return [head, row, userLine, agentLine, statsRow, makeEl("div", "dap-note")];
|
|
4663
|
+
return [head, row, userLine, agentLine, makeStatsRow(), makeEl("div", "dap-note")];
|
|
4068
4664
|
}
|
|
4069
4665
|
if (kind === "awaiting") {
|
|
4070
4666
|
const row = makeEl("div", "dap-row");
|
|
4071
|
-
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
4667
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-total-time"));
|
|
4072
4668
|
// 等待三类末行结构(R-01-002/AC-08、AC-09,C-043):首行「类型胶囊」(圆底类型
|
|
4073
4669
|
// 图标 + 类型文字),其下为正文行——阻塞/错误的说明文字,完成提醒的
|
|
4074
4670
|
// 「继续对话,或移入历史」+ 行尾「移入历史」按钮。
|
|
@@ -4079,24 +4675,14 @@ function apply(ctx) {
|
|
|
4079
4675
|
noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
|
|
4080
4676
|
const awaitHead = makeEl("div", "dap-await-head");
|
|
4081
4677
|
awaitHead.append(capsule);
|
|
4082
|
-
const statsRow = makeEl("div", "dap-token-stats");
|
|
4083
|
-
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
4084
|
-
statsRow.hidden = true;
|
|
4085
4678
|
const foot = makeEl("div", "dap-foot");
|
|
4086
4679
|
foot.append(awaitHead, noteRow);
|
|
4087
|
-
return [head, row, makeEl("div", "dap-trace"),
|
|
4680
|
+
return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];
|
|
4088
4681
|
}
|
|
4089
4682
|
const row = makeEl("div", "dap-row");
|
|
4090
|
-
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"));
|
|
4091
|
-
const
|
|
4092
|
-
|
|
4093
|
-
const progressRow = makeEl("div", "dap-progress");
|
|
4094
|
-
progressRow.append(track, makeEl("span", "dap-pct"));
|
|
4095
|
-
// 统计行双段结构:左列 token/速率/命中率(超长省略号截断),时长固定最右(R-01-009/AC-05)。
|
|
4096
|
-
const statsRow = makeEl("div", "dap-token-stats");
|
|
4097
|
-
statsRow.append(makeEl("span", "dap-token-main"), makeEl("span", "dap-token-time"));
|
|
4098
|
-
statsRow.hidden = true;
|
|
4099
|
-
return [head, row, makeEl("div", "dap-trace"), progressRow, statsRow];
|
|
4683
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-total-time"));
|
|
4684
|
+
const progressRow = makeProgressRow();
|
|
4685
|
+
return [head, row, makeEl("div", "dap-trace"), progressRow, makeStatsRow()];
|
|
4100
4686
|
}
|
|
4101
4687
|
|
|
4102
4688
|
function createInlineIcon({ viewBox, width = 12, height = 12, parts }) {
|
|
@@ -4445,6 +5031,29 @@ function apply(ctx) {
|
|
|
4445
5031
|
}
|
|
4446
5032
|
}
|
|
4447
5033
|
|
|
5034
|
+
/** 进度行写入(运行卡与子代理卡共用,R-01-009/AC-06、AC-14):百分比与填充宽度,未变化跳过 DOM 写。 */
|
|
5035
|
+
function renderProgressRow(el, progress) {
|
|
5036
|
+
const pct = el.querySelector(".dap-pct");
|
|
5037
|
+
if (pct !== null) pct.textContent = `${Math.round(progress ?? 0)}%`;
|
|
5038
|
+
const fill = el.querySelector(".dap-fill");
|
|
5039
|
+
if (fill !== null) {
|
|
5040
|
+
const width = `${Math.min(100, Math.max(0, progress ?? 0))}%`;
|
|
5041
|
+
if (fill.style.width !== width) fill.style.width = width;
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
/** 最近回合耗时 memo(等待卡与暂停子代理卡共用,R-01-009/AC-12、AC-15):history 引用
|
|
5046
|
+
* 不变即命中缓存。busy 口径(起止差值扣除回合内阻塞等待),与标题行总耗时同口径,
|
|
5047
|
+
* 保证恒不大于累计值(R-01-020)。 */
|
|
5048
|
+
function memoTurnDuration(detail) {
|
|
5049
|
+
const history = detail.history ?? null;
|
|
5050
|
+
if (detail.memoTurnDurationHistoryOf !== history) {
|
|
5051
|
+
detail.memoTurnDurationHistoryOf = history;
|
|
5052
|
+
detail.memoTurnDuration = lastTurnDuration({ history });
|
|
5053
|
+
}
|
|
5054
|
+
return detail.memoTurnDuration ?? null;
|
|
5055
|
+
}
|
|
5056
|
+
|
|
4448
5057
|
/** 统计行渲染:运行卡、等待卡与最近卡写入速率、token 与耗时,旧骨架缺节点时就地补齐。 */
|
|
4449
5058
|
function renderTokenStats(el, entry) {
|
|
4450
5059
|
let stats = el.querySelector(".dap-token-stats");
|
|
@@ -4559,6 +5168,15 @@ function apply(ctx) {
|
|
|
4559
5168
|
const title = el.querySelector(".dap-title");
|
|
4560
5169
|
if (title !== null && title.textContent !== entry.title)
|
|
4561
5170
|
title.textContent = entry.title;
|
|
5171
|
+
// 标题行最右侧的累计运行时长(R-01-020/AC-01、AC-06):有数据才显示,
|
|
5172
|
+
// 无数据(含子代理卡)隐藏节点,不以 0 冒充。
|
|
5173
|
+
const totalTime = el.querySelector(".dap-total-time");
|
|
5174
|
+
if (totalTime !== null) {
|
|
5175
|
+
const totalText = Number.isFinite(entry.totalBusyMs) && entry.totalBusyMs >= 0 ? fmtElapsedMs(entry.totalBusyMs) : "";
|
|
5176
|
+
if (totalTime.textContent !== totalText) totalTime.textContent = totalText;
|
|
5177
|
+
const totalHidden = totalText === "";
|
|
5178
|
+
if (totalTime.hidden !== totalHidden) totalTime.hidden = totalHidden;
|
|
5179
|
+
}
|
|
4562
5180
|
|
|
4563
5181
|
const capsule = el.querySelector(".dap-capsule");
|
|
4564
5182
|
// 陈旧骨架就地迁移(C-043 热装兼容):旧版「正文行 + 行尾类型徽标」骨架升级为
|
|
@@ -4597,16 +5215,9 @@ function apply(ctx) {
|
|
|
4597
5215
|
}
|
|
4598
5216
|
|
|
4599
5217
|
if (entry.kind === "running") {
|
|
4600
|
-
|
|
4601
|
-
if (pct !== null)
|
|
4602
|
-
pct.textContent = `${Math.round(entry.progress ?? 0)}%`;
|
|
5218
|
+
renderProgressRow(el, entry.progress);
|
|
4603
5219
|
const traceContainer = el.querySelector(".dap-trace");
|
|
4604
5220
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry);
|
|
4605
|
-
const fill = el.querySelector(".dap-fill");
|
|
4606
|
-
if (fill !== null) {
|
|
4607
|
-
const width = `${Math.min(100, Math.max(0, entry.progress ?? 0))}%`;
|
|
4608
|
-
if (fill.style.width !== width) fill.style.width = width;
|
|
4609
|
-
}
|
|
4610
5221
|
renderTokenStats(el, entry);
|
|
4611
5222
|
return;
|
|
4612
5223
|
}
|
|
@@ -4614,6 +5225,17 @@ function apply(ctx) {
|
|
|
4614
5225
|
if (entry.kind === "subagent") {
|
|
4615
5226
|
const traceContainer = el.querySelector(".dap-subtrace");
|
|
4616
5227
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: true });
|
|
5228
|
+
// 运行中呈现与运行卡同构的进度行;锚点空闲(暂停等待)时整行隐藏(R-01-009/AC-15)。
|
|
5229
|
+
const progressRow = el.querySelector(".dap-progress");
|
|
5230
|
+
if (progressRow !== null) {
|
|
5231
|
+
if (Number.isFinite(entry.progress)) {
|
|
5232
|
+
renderProgressRow(el, entry.progress);
|
|
5233
|
+
if (progressRow.hidden) progressRow.hidden = false;
|
|
5234
|
+
} else if (!progressRow.hidden) {
|
|
5235
|
+
progressRow.hidden = true;
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
renderTokenStats(el, entry);
|
|
4617
5239
|
return;
|
|
4618
5240
|
}
|
|
4619
5241
|
|
|
@@ -4701,6 +5323,10 @@ function apply(ctx) {
|
|
|
4701
5323
|
const detail = sessionDetailsById.get(id) ?? {};
|
|
4702
5324
|
detail.snapshot = snapshot;
|
|
4703
5325
|
sessionDetailsById.set(id, detail);
|
|
5326
|
+
// dsh 0.1.5 起快照不再携带会话内容:时间线经 eventSource 日志窗口
|
|
5327
|
+
// 就地重派生(R-01-009)。
|
|
5328
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
5329
|
+
captureSessionLog(id, { subagent: isSubagentRow(listSnap?.byId?.[id], listSnap ?? {}), cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
4704
5330
|
queueSync();
|
|
4705
5331
|
});
|
|
4706
5332
|
} catch {
|
|
@@ -5223,6 +5849,19 @@ function apply(ctx) {
|
|
|
5223
5849
|
}
|
|
5224
5850
|
}
|
|
5225
5851
|
const listState = listLoadState(snapshot);
|
|
5852
|
+
// 0.1.5 sessions 行不再承载 pendingInteraction:从 uiSession.pendingInteractions
|
|
5853
|
+
// 快照(Map<sessionId, interaction>,kind ∈ approval/plan-review/question)回填到
|
|
5854
|
+
// 行副本上,等待卡分类(awaiting/blocked/提问中)保持既有单一口径(R-01-002/AC-03)。
|
|
5855
|
+
const pendingSnapshot = uiSession?.pendingInteractions?.getSnapshot?.() ?? null;
|
|
5856
|
+
if (pendingSnapshot instanceof Map && pendingSnapshot.size > 0 && snapshot?.byId) {
|
|
5857
|
+
const byId = { ...snapshot.byId };
|
|
5858
|
+
for (const [id, interaction] of pendingSnapshot) {
|
|
5859
|
+
const row = byId[id];
|
|
5860
|
+
if (!isRecord(row) || typeof interaction?.kind !== "string") continue;
|
|
5861
|
+
byId[id] = { ...row, pendingInteraction: interaction.kind };
|
|
5862
|
+
}
|
|
5863
|
+
snapshot = { ...snapshot, byId };
|
|
5864
|
+
}
|
|
5226
5865
|
// 只消费上一轮追加请求;列表短暂 pending 时保留已展开页,ready 后继续从同一前缀呈现。
|
|
5227
5866
|
recentAppendQueued = false;
|
|
5228
5867
|
const workspaceSnapshot = getSnapshot(workspaces, "list");
|
|
@@ -5242,24 +5881,42 @@ function apply(ctx) {
|
|
|
5242
5881
|
const runLikeIds = new Set(
|
|
5243
5882
|
active.filter((entry) => shouldSubscribeToSession(entry, snapshot?.byId ?? {})).map((entry) => entry.id),
|
|
5244
5883
|
);
|
|
5245
|
-
|
|
5884
|
+
// 运行卡时钟:运行中子代理卡同样承载逐秒推进的进度与时长(R-01-009/AC-14)。
|
|
5885
|
+
syncLiveness(
|
|
5886
|
+
runLikeIds,
|
|
5887
|
+
active.some((entry) => entry.kind === "running" || (entry.kind === "subagent" && runLikeIds.has(entry.id))),
|
|
5888
|
+
);
|
|
5246
5889
|
for (const entry of active) {
|
|
5247
5890
|
const liveRecord = livenessById.get(entry.id);
|
|
5248
5891
|
const live = liveRecord?.liveness ?? null;
|
|
5249
5892
|
const detail = sessionDetailsById.get(entry.id);
|
|
5250
5893
|
if (entry.kind === "running" && detail) detail.durationFallbackLoaded = false;
|
|
5894
|
+
// 累计运行时长(R-01-020/AC-01、AC-03):主会话条目注入显示值——渲染期合成,
|
|
5895
|
+
// 含开放回合的实时已耗时并随时钟逐秒推进;子代理卡片不显示。
|
|
5896
|
+
if (entry.kind !== "subagent") applyTotalBusy(entry, now);
|
|
5251
5897
|
const detailSnapshot = liveRecord?.snapshot ?? detail?.snapshot ?? null;
|
|
5252
5898
|
if (detail && detail.memoHistoryAnchorOf !== (detail.history ?? null)) {
|
|
5253
5899
|
detail.memoHistoryAnchorOf = detail.history ?? null;
|
|
5254
5900
|
detail.memoHistoryAnchor = historyInstructionAnchor(detail.history);
|
|
5255
5901
|
}
|
|
5902
|
+
// 等待/暂停呈现(pendingText 存在)且自身快照为冻结值时,残留 running 行全部落定;
|
|
5903
|
+
// 存在活动后代时保留尾部提升的「agent 工作中」呈现(R-01-009/AC-10 委托周期语义)。
|
|
5904
|
+
const entryIdle = (entry.pendingText ?? null) !== null && entry.descendantActive !== true;
|
|
5905
|
+
const entryCwd = snapshot?.byId?.[entry.id]?.cwd ?? "";
|
|
5906
|
+
// log 派生 memo:history 引用 / idle / cwd 变化才重算。落定在折叠前生效
|
|
5907
|
+
// (foldedHistoryTimeline 的 settleIdle),组标题由已定案成员派生——阻塞等待卡
|
|
5908
|
+
// 呈现「运行了命令」+「等待回答」摘要,而非「正在运行」蓝闪(R-01-009/AC-09)。
|
|
5909
|
+
const historyRef = detail?.history ?? null;
|
|
5910
|
+
const logReady = Array.isArray(historyRef) && historyRef.length > 0;
|
|
5911
|
+
if (detail && logReady && (detail.memoLogTimelineOf !== historyRef || detail.memoLogTimelineIdle !== entryIdle || detail.memoLogTimelineCwd !== entryCwd)) {
|
|
5912
|
+
detail.memoLogTimelineOf = historyRef;
|
|
5913
|
+
detail.memoLogTimelineIdle = entryIdle;
|
|
5914
|
+
detail.memoLogTimelineCwd = entryCwd;
|
|
5915
|
+
detail.memoLogTimeline = foldedHistoryTimeline(historyRef, 4, entryCwd, entryIdle);
|
|
5916
|
+
}
|
|
5256
5917
|
if (detail && detailSnapshot) {
|
|
5257
5918
|
// 按快照引用 memo:引用不变(时钟 tick、无关推送)时命中缓存,
|
|
5258
5919
|
// 长会话不再每次渲染全序扫描。
|
|
5259
|
-
const entryCwd = snapshot?.byId?.[entry.id]?.cwd ?? "";
|
|
5260
|
-
// 等待/暂停呈现(pendingText 存在)且自身快照为冻结值时,残留 running 行全部落定;
|
|
5261
|
-
// 存在活动后代时保留尾部提升的「agent 工作中」呈现(R-01-009/AC-10 委托周期语义)。
|
|
5262
|
-
const entryIdle = (entry.pendingText ?? null) !== null && entry.descendantActive !== true;
|
|
5263
5920
|
if (detail.memoTimelineOf !== detailSnapshot || detail.memoTimelineCwd !== entryCwd || detail.memoTimelineDescendantActive !== (entry.descendantActive === true) || detail.memoTimelineIdle !== entryIdle || detail.memoTimelineAnchor !== (detail.memoHistoryAnchor ?? null)) {
|
|
5264
5921
|
detail.memoTimelineOf = detailSnapshot;
|
|
5265
5922
|
detail.memoTimelineCwd = entryCwd;
|
|
@@ -5271,7 +5928,11 @@ function apply(ctx) {
|
|
|
5271
5928
|
// 出现在输出(窗口行或锚行),反之需 history 补读。
|
|
5272
5929
|
detail.snapshotHasAnchorableUserRow = detail.memoTimeline.some(isAnchorableUserRow);
|
|
5273
5930
|
}
|
|
5274
|
-
|
|
5931
|
+
// V3 快照不再承载 chat/partial:活动时间线以 log 派生(eventSource 窗口,含
|
|
5932
|
+
// live-chunk 流式行)为主源;快照派生仅在 log 未就绪时兜底(锚行回退)。
|
|
5933
|
+
entry.timeline = logReady ? detail.memoLogTimeline : detail.memoTimeline ?? [];
|
|
5934
|
+
} else if (logReady) {
|
|
5935
|
+
entry.timeline = detail.memoLogTimeline;
|
|
5275
5936
|
} else {
|
|
5276
5937
|
entry.timeline = detail?.timeline ?? entry.timeline ?? [];
|
|
5277
5938
|
}
|
|
@@ -5293,18 +5954,19 @@ function apply(ctx) {
|
|
|
5293
5954
|
}
|
|
5294
5955
|
}
|
|
5295
5956
|
if (entry.kind === "awaiting" && detail) {
|
|
5296
|
-
|
|
5297
|
-
if (detail.memoTurnDurationSnapshotOf !== detailSnapshot || detail.memoTurnDurationHistoryOf !== history) {
|
|
5298
|
-
detail.memoTurnDurationSnapshotOf = detailSnapshot;
|
|
5299
|
-
detail.memoTurnDurationHistoryOf = history;
|
|
5300
|
-
detail.memoTurnDuration = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history });
|
|
5301
|
-
}
|
|
5302
|
-
entry.elapsedMs = detail.memoTurnDuration ?? null;
|
|
5957
|
+
entry.elapsedMs = memoTurnDuration(detail);
|
|
5303
5958
|
}
|
|
5304
5959
|
if (detail?.model) {
|
|
5305
5960
|
entry.model = detail.model.model;
|
|
5306
5961
|
entry.reasoning = detail.model.reasoning;
|
|
5307
5962
|
}
|
|
5963
|
+
// 子代理溯源得到的是 provider 模型 id:经目录分组解析为显示名,effort 缺失时
|
|
5964
|
+
// 以同一目录条目回退(R-01-012/AC-17);目录未覆盖该 id 时保留原始回退(AC-18)。
|
|
5965
|
+
if (entry.kind === "subagent" && detail?.model) {
|
|
5966
|
+
const catalogEntry = catalogEntries[detail.model.model];
|
|
5967
|
+
if (catalogEntry?.name) entry.model = catalogEntry.name;
|
|
5968
|
+
if (!entry.reasoning && catalogEntry?.reasoning) entry.reasoning = catalogEntry.reasoning;
|
|
5969
|
+
}
|
|
5308
5970
|
// 待回复卡列表补全(C-040、C-064):buildEntries 运行时快照时间线可能仍为空,
|
|
5309
5971
|
// 而上面的 memo 才在本帧算出结构化提问预览;等待卡静止后常无下一帧,因此在此
|
|
5310
5972
|
// 立即补入 questionPreview,由 cardSignature 驱动本帧 DOM 写入。
|
|
@@ -5313,7 +5975,9 @@ function apply(ctx) {
|
|
|
5313
5975
|
if (question !== null) entry.questionPreview = question;
|
|
5314
5976
|
}
|
|
5315
5977
|
// 字段级加载指示(R-01-014/AC-02):补充数据在途时卡片对应位置显示活动图标。
|
|
5316
|
-
|
|
5978
|
+
// 主会话模型经目录 store 一次性 load 到达(modelLoads 记账),子代理模型经
|
|
5979
|
+
// 日志读取提取(R-01-012/AC-17)。
|
|
5980
|
+
entry.loadingModel = !detail?.model && (modelLoads.has(entry.id) || (entry.kind === "subagent" && historyLoads.has(entry.id)));
|
|
5317
5981
|
entry.loadingTimeline =
|
|
5318
5982
|
entry.timeline.length === 0 &&
|
|
5319
5983
|
(historyLoads.has(entry.id) || sessionOpenLoads.has(entry.id) || (runLikeIds.has(entry.id) && !liveRecord));
|
|
@@ -5334,7 +5998,8 @@ function apply(ctx) {
|
|
|
5334
5998
|
// 之外)时,取 history 深翻提取的开放回合起点。
|
|
5335
5999
|
const anchor = progressAnchor(progressAnchorById.get(entry.id) ?? null, {
|
|
5336
6000
|
descendantActive: entry.descendantActive === true,
|
|
5337
|
-
|
|
6001
|
+
// dsh 0.1.5 起快照不再携带 turnTimings:开放回合起点由宿主侧 busy 记账兜底。
|
|
6002
|
+
hostStartTime: live?.startTime ?? busyById.get(entry.id)?.openTurnStart ?? detail?.memoOpenTurnStart ?? null,
|
|
5338
6003
|
now,
|
|
5339
6004
|
});
|
|
5340
6005
|
progressAnchorById.set(entry.id, anchor);
|
|
@@ -5350,6 +6015,21 @@ function apply(ctx) {
|
|
|
5350
6015
|
// 连续、周期外回合切换归零,R-01-009/AC-06,C-014、C-044)。
|
|
5351
6016
|
entry.progress = progressOf({ elapsedMs: elapsedMs ?? 0, halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS }) });
|
|
5352
6017
|
}
|
|
6018
|
+
if (entry.kind === "subagent") {
|
|
6019
|
+
// 运行中子代理卡与主会话运行卡同口径:同一锚点状态机与进度曲线(R-01-009/AC-14)。
|
|
6020
|
+
const elapsedMs = Number.isFinite(anchor.anchor) ? Math.max(0, now - anchor.anchor) : null;
|
|
6021
|
+
if (elapsedMs !== null) {
|
|
6022
|
+
Object.assign(entry, projectionStats, { elapsedMs });
|
|
6023
|
+
if (detail) detail.lastRuntimeStats = mergeRuntimeStats(projectionStats, detail.lastRuntimeStats);
|
|
6024
|
+
entry.progress = progressOf({ elapsedMs, halfLifeSec: progressHalfLifeSec({ rateTokS: projectionStats.rateTokS }) });
|
|
6025
|
+
} else {
|
|
6026
|
+
// 非运行(暂停等待):冻结最后已知统计与最近回合耗时,progress 置空隐藏
|
|
6027
|
+
// 进度条(R-01-009/AC-15);刷新/无留存时回退当前列表投影。
|
|
6028
|
+
Object.assign(entry, mergeRuntimeStats(detail?.lastRuntimeStats, projectionStats));
|
|
6029
|
+
entry.elapsedMs = detail ? memoTurnDuration(detail) : null;
|
|
6030
|
+
entry.progress = null;
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
5353
6033
|
}
|
|
5354
6034
|
// 历史区时间精化(R-01-010/AC-08、AC-09):从保留快照的 turnTimings 与已拉取的
|
|
5355
6035
|
// history 同批事件提取最后回合结束时刻(取两者较新者),按引用 memo;均无则不提供,
|
|
@@ -5369,13 +6049,15 @@ function apply(ctx) {
|
|
|
5369
6049
|
recentTotal = recentCandidates.length;
|
|
5370
6050
|
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
5371
6051
|
recentHasMore = recent.length < recentTotal;
|
|
5372
|
-
//
|
|
5373
|
-
//
|
|
6052
|
+
// 最近卡统计复用运行卡的列表投影口径;耗时从已读 history 取最近完整回合的运行
|
|
6053
|
+
// 过程时长(busy 口径,与标题行总耗时一致,R-01-013/AC-12),缺边界时仅为当前
|
|
6054
|
+
// 可见历史卡安排一次既有 history 补读。
|
|
5374
6055
|
const recentDurationFallbackIds = new Set();
|
|
5375
6056
|
for (const entry of recent) {
|
|
5376
6057
|
const detail = sessionDetailsById.get(entry.id);
|
|
5377
|
-
const
|
|
5378
|
-
|
|
6058
|
+
const elapsedMs = lastTurnDuration({ history: detail?.history ?? null });
|
|
6059
|
+
// 累计运行时长(R-01-020/AC-01):最近历史卡同为标题行右侧显示;历史卡无开放回合。
|
|
6060
|
+
applyTotalBusy(entry, now);
|
|
5379
6061
|
const stats = statsFromProjection(snapshot?.byId?.[entry.id]?.projectionValues, elapsedMs);
|
|
5380
6062
|
const retained = detail?.lastRuntimeStats;
|
|
5381
6063
|
Object.assign(entry, stats, retained ? {
|
|
@@ -5410,9 +6092,27 @@ function apply(ctx) {
|
|
|
5410
6092
|
...active.filter((entry) => entry.kind === "awaiting").map((entry) => entry.id),
|
|
5411
6093
|
...recentDurationFallbackIds,
|
|
5412
6094
|
]);
|
|
6095
|
+
// 子代理模型溯源读取时机(R-01-012/AC-17):运行中看快照最新助手节点是否已
|
|
6096
|
+
// 定案(事件已落宿主日志,尾页读取必命中;工具密集期 4 行折叠窗口可能不含
|
|
6097
|
+
// 助手行,故直接读快照而非折叠时间线);不在运行中则整段日志已冻结,直接读取。
|
|
6098
|
+
// 开局只有流式 partial 时不读,避免早读扑空。
|
|
6099
|
+
const subagentModelReadIds = new Set();
|
|
6100
|
+
for (const entry of active) {
|
|
6101
|
+
if (entry.kind !== "subagent") continue;
|
|
6102
|
+
if (!runLikeIds.has(entry.id)) {
|
|
6103
|
+
subagentModelReadIds.add(entry.id);
|
|
6104
|
+
continue;
|
|
6105
|
+
}
|
|
6106
|
+
const detail = sessionDetailsById.get(entry.id);
|
|
6107
|
+
const detailSnapshot = livenessById.get(entry.id)?.snapshot ?? detail?.snapshot ?? null;
|
|
6108
|
+
if (chatLatestAssistantSettled(detailSnapshot)) subagentModelReadIds.add(entry.id);
|
|
6109
|
+
}
|
|
5413
6110
|
const detailIds = [...active, ...recent].map((entry) => entry.id);
|
|
5414
6111
|
detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
|
|
5415
|
-
loadNativeDetails(detailIds, previewFallbackIds, durationFallbackIds);
|
|
6112
|
+
loadNativeDetails({ ids: detailIds, previewFallbackIds, durationFallbackIds, subagentModelReadIds });
|
|
6113
|
+
// 累计运行时长懒回填(R-01-020/AC-05):对可见主会话触发宿主侧存量回合补齐;
|
|
6114
|
+
// 宿主侧每会话单飞,重复触发由宿主合并;完成后经 busy SSE 广播推送。
|
|
6115
|
+
requestBusyBackfill([...active, ...recent].filter((entry) => entry.kind !== "subagent").map((entry) => entry.id));
|
|
5416
6116
|
const visibleIds = new Set([...active, ...recent].map((entry) => entry.id));
|
|
5417
6117
|
// 详情与 loads 记账同生命周期:离开可见集合即放行,重回可见时允许重拉/重试。
|
|
5418
6118
|
// 锚点记账不随可见性 prune:瞬时 loading 空帧不得误清(进度重置);陈旧条目靠
|
|
@@ -5420,6 +6120,7 @@ function apply(ctx) {
|
|
|
5420
6120
|
pruneInvisibleEntries([sessionDetailsById, modelLoads, historyLoads, sessionOpenLoads], visibleIds);
|
|
5421
6121
|
// 模型目录订阅同生命周期:不可见即先 unsubscribe 再除名,监听器不残留(R-01-012/AC-16)。
|
|
5422
6122
|
pruneSubscriptions(modelDirectorySubs, visibleIds);
|
|
6123
|
+
pruneSubscriptions(logSourceSubs, visibleIds);
|
|
5423
6124
|
// 重试链目标已成为当前会话(他途到达)即取消,避免过期链条拽回会话。
|
|
5424
6125
|
cancelStaleOpenRetries({ currentId: snapshot?.current ?? null, activatedId: lastActivatedId });
|
|
5425
6126
|
|
|
@@ -5708,7 +6409,16 @@ function apply(ctx) {
|
|
|
5708
6409
|
// 只观察 center 的直接子节点,捕获 seat/pane 重挂载,不观察 pane 子树。
|
|
5709
6410
|
centerObserver = new MutationObserver(() => {
|
|
5710
6411
|
const nextSeat = document.querySelector(CONVERSATION_SELECTOR);
|
|
5711
|
-
if (nextSeat?.parentElement !== center)
|
|
6412
|
+
if (nextSeat?.parentElement !== center) {
|
|
6413
|
+
installFrameObserver();
|
|
6414
|
+
} else if (nextSeat !== seat) {
|
|
6415
|
+
// 中间列未换但槽容器被宿主原地替换:流式观察者换绑到新节点,
|
|
6416
|
+
// 不整层重装(旧节点已脱离 DOM,留着会让流式 childList 更新丢失)。
|
|
6417
|
+
conversationObserver?.disconnect();
|
|
6418
|
+
conversationObserver = new MutationObserver(queueSync);
|
|
6419
|
+
conversationObserver.observe(nextSeat, { childList: true, subtree: true });
|
|
6420
|
+
observedSeat = nextSeat;
|
|
6421
|
+
}
|
|
5712
6422
|
queueSync();
|
|
5713
6423
|
});
|
|
5714
6424
|
centerObserver.observe(center, { childList: true });
|
|
@@ -5731,8 +6441,17 @@ function apply(ctx) {
|
|
|
5731
6441
|
|
|
5732
6442
|
// ---- 交互:移动端抽屉开关、弹窗收起、桌面折叠 ----
|
|
5733
6443
|
function onToggleClick() {
|
|
5734
|
-
|
|
5735
|
-
|
|
6444
|
+
let pane = document.querySelector(`[${PANE_ATTR}]`);
|
|
6445
|
+
if (pane === null) {
|
|
6446
|
+
// 宿主视图替换窗口期/绑定失败时窗格可能缺失:先补绑再开合,开关不得静默无响应
|
|
6447
|
+
// (真机宿主重渲染节奏与桌面不同,此窗口期在触屏上更易被命中,R-01-008/AC-01)。
|
|
6448
|
+
pane = ensurePane();
|
|
6449
|
+
if (pane === null) {
|
|
6450
|
+
console.warn("[dsh-activity-pane] 窗格挂载点不存在(main 槽容器未就绪),已忽略本次开关点击");
|
|
6451
|
+
return;
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
const open = pane.getAttribute("data-open") !== "true";
|
|
5736
6455
|
togglePane(open);
|
|
5737
6456
|
}
|
|
5738
6457
|
toggle.addEventListener("click", onToggleClick);
|
|
@@ -5748,11 +6467,19 @@ function apply(ctx) {
|
|
|
5748
6467
|
disposed = true;
|
|
5749
6468
|
sessionUnsubscribe?.();
|
|
5750
6469
|
workspaceUnsubscribe?.();
|
|
6470
|
+
pendingUnsubscribe?.();
|
|
5751
6471
|
acksSource?.close();
|
|
5752
6472
|
acksSource = null;
|
|
6473
|
+
busySource?.close();
|
|
6474
|
+
busySource = null;
|
|
6475
|
+
document.removeEventListener("visibilitychange", onBusyVisibilityResume);
|
|
6476
|
+
window.removeEventListener("pageshow", onBusyPageShow);
|
|
5753
6477
|
document.removeEventListener("visibilitychange", onVisibilityResume);
|
|
5754
6478
|
window.removeEventListener("pageshow", onPageShow);
|
|
5755
6479
|
completeAcksById.clear();
|
|
6480
|
+
busyById.clear();
|
|
6481
|
+
busyRequestedIds.clear();
|
|
6482
|
+
busyRetryAtById.clear();
|
|
5756
6483
|
if (clockTimer !== null) clearInterval(clockTimer);
|
|
5757
6484
|
if (recentTimeTimer !== null) clearInterval(recentTimeTimer);
|
|
5758
6485
|
if (e2eListReleaseTimer !== null) clearTimeout(e2eListReleaseTimer);
|
|
@@ -5769,6 +6496,7 @@ function apply(ctx) {
|
|
|
5769
6496
|
livenessById.clear();
|
|
5770
6497
|
// 卸载即全量退订:空可见集合驱动 pruneSubscriptions 先 unsubscribe 再除名。
|
|
5771
6498
|
pruneSubscriptions(modelDirectorySubs, new Set());
|
|
6499
|
+
pruneSubscriptions(logSourceSubs, new Set());
|
|
5772
6500
|
progressAnchorById.clear();
|
|
5773
6501
|
sessionOpenLoads.clear();
|
|
5774
6502
|
loadQueue.length = 0;
|