dsh-activity-pane 0.13.0 → 0.14.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 +590 -33
- package/package.json +1 -1
- package/scripts/acceptance.mjs +4 -0
- package/scripts/check.mjs +233 -14
- package/src/client.mjs +381 -20
- package/src/core.mjs +209 -13
- package/src/host.mjs +143 -1
package/.dsh-plugin/client.js
CHANGED
|
@@ -18,6 +18,8 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
//
|
|
19
19
|
// 展示规则:
|
|
20
20
|
// 主会话(非有效子代理行):running || completed || pendingInteraction 都显示;
|
|
21
|
+
// 存活在跑后台任务(jobsBySession 中 status ∈ {running, stopping},R-01-023)的主会话
|
|
22
|
+
// 同样按运行态显示并抑制完成/错误提醒;
|
|
21
23
|
// 子代理(origin === 'subagent' 且 parentId 有效):仅 running || pendingInteraction 时显示(结束后即消失);
|
|
22
24
|
// pendingInteraction 总是优先视为"等待用户行动"(即使在 running 中)。
|
|
23
25
|
|
|
@@ -1561,8 +1563,12 @@ function mainTitle(byId, id) {
|
|
|
1561
1563
|
* stateAt(进入当前等待行动状态的时刻,R-01-002/AC-14):仅 awaiting 条目携带,与排序键
|
|
1562
1564
|
* 共用 enterStateAt 单点口径——pending 取 waitingStarts,完成/错误提醒取 completions.lastTurnEnd;
|
|
1563
1565
|
* 显示侧不回落宿主列表时间——排序键缺失时的回落仅用于排序,显示以 null(不显示)承载。
|
|
1566
|
+
* jobsBySession(R-01-023):快照携带的 `jobsBySession` 任务视图映射——存在在跑后台任务
|
|
1567
|
+
* (status ∈ {running, stopping})的主会话获得第三种自身活动来源:保留在活动区归入运行组、
|
|
1568
|
+
* 完成提醒与错误提醒被抑制(AC-01、AC-02);条目携带 liveJobs(startedAt 升序)供渲染层
|
|
1569
|
+
* 标注数量与构建任务行(R-01-024)。子代理行不派生 liveJobs(后台任务当前仅主会话呈现)。
|
|
1564
1570
|
*/
|
|
1565
|
-
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null) {
|
|
1571
|
+
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null, jobsBySession = null) {
|
|
1566
1572
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1567
1573
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1568
1574
|
const current = snapshot?.current ?? null;
|
|
@@ -1603,16 +1609,20 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1603
1609
|
const running = row.running === true;
|
|
1604
1610
|
const pending = row.pendingInteraction !== undefined;
|
|
1605
1611
|
const isSub = hasParent;
|
|
1612
|
+
// 在跑后台任务(R-01-023):主会话的第三种自身活动来源——liveJobs 非空期间
|
|
1613
|
+
// 完成/错误提醒被抑制(AC-02)、会话保持运行态呈现(AC-01)。
|
|
1614
|
+
const liveJobs = isSub ? [] : liveJobsOf(jobsBySession, id);
|
|
1615
|
+
const hasLive = liveJobs.length > 0;
|
|
1606
1616
|
// 完成确认(R-01-002/AC-03、AC-05、R-01-010/AC-06):未确认的完成提醒按自身活动计入。
|
|
1607
|
-
const done = completionReminder(row, completionFor(id, completions), isSub);
|
|
1617
|
+
const done = !hasLive && completionReminder(row, completionFor(id, completions), isSub);
|
|
1608
1618
|
// 错误提醒(R-01-002/AC-13,C-043):最近回合以错误结束的按自身活动计入。
|
|
1609
|
-
const err = errorReminder(row, completionFor(id, completions), isSub);
|
|
1619
|
+
const err = !hasLive && errorReminder(row, completionFor(id, completions), isSub);
|
|
1610
1620
|
// 子代理完成且没有活动后代时消失;主会话完成后保留为"等待打开";母会话在委托周期保持运行呈现(R-01-003/AC-05)。
|
|
1611
|
-
const selfActive = isOwnActiveRow(row, byId);
|
|
1621
|
+
const selfActive = isOwnActiveRow(row, byId) || hasLive;
|
|
1612
1622
|
const descendantActive = descendantIds.has(String(id));
|
|
1613
1623
|
const delegating = descendantActive || (delegatingIds instanceof Set && delegatingIds.has(String(id)));
|
|
1614
1624
|
const show = selfActive || delegating || done || err;
|
|
1615
|
-
meta.set(id, { row, running, pending, isSub, show, done, err, descendantActive, delegating, depth: 0 });
|
|
1625
|
+
meta.set(id, { row, running, pending, isSub, show, done, err, descendantActive, delegating, hasLive, liveJobs, depth: 0 });
|
|
1616
1626
|
}
|
|
1617
1627
|
|
|
1618
1628
|
// 主会话分两组排序(R-01-001/AC-07,键口径契约详见上方 JSDoc):运行中主会话置顶、
|
|
@@ -1620,7 +1630,8 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1620
1630
|
// 两组相同时间均回落宿主列表出现顺序,工作区顺序不参与排序。
|
|
1621
1631
|
const isRunningEntry = (id) => {
|
|
1622
1632
|
const m = meta.get(id);
|
|
1623
|
-
|
|
1633
|
+
// 在跑后台任务视同运行组(R-01-023/AC-01):排序键沿用宿主列表时间。
|
|
1634
|
+
return m !== undefined && !m.pending && (m.running || m.delegating || m.hasLive);
|
|
1624
1635
|
};
|
|
1625
1636
|
const waitingStartTime = (id) => {
|
|
1626
1637
|
if (!(waitingStarts instanceof Map)) return null;
|
|
@@ -1691,7 +1702,7 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1691
1702
|
? "subagent"
|
|
1692
1703
|
: m.pending
|
|
1693
1704
|
? "awaiting"
|
|
1694
|
-
: m.running || m.delegating
|
|
1705
|
+
: m.running || m.delegating || m.hasLive
|
|
1695
1706
|
? "running"
|
|
1696
1707
|
: "awaiting",
|
|
1697
1708
|
descendantActive: m.descendantActive,
|
|
@@ -1725,9 +1736,41 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1725
1736
|
: doneWait
|
|
1726
1737
|
? ROUND_DONE_NOTE
|
|
1727
1738
|
: undefined,
|
|
1739
|
+
// 在跑后台任务(R-01-023/AC-01):仅 liveJobs 非空时携带(startedAt 升序),
|
|
1740
|
+
// 渲染层据此在标题行标注数量;任务本体以下一线条目呈现(kind: "job")。
|
|
1741
|
+
liveJobs: m.hasLive ? m.liveJobs : undefined,
|
|
1742
|
+
selfRunning: m.row.running === true,
|
|
1728
1743
|
stateAt,
|
|
1729
1744
|
questionPreview: m.pending && m.row.pendingInteraction === "question" ? (questionPreview ?? null) : undefined,
|
|
1730
1745
|
});
|
|
1746
|
+
// 后台任务子条目(R-01-024):与子代理同形的缩进子卡,跟随母会话在其全部
|
|
1747
|
+
// 子代理之前(即母亲条目的直接后继);结束即随 liveJobs 清空而消失(R-01-023/AC-03)。
|
|
1748
|
+
// 复合 id 在去重视野(entries/visited/cardsById)中唯一,且携 jobs 前缀与子代理
|
|
1749
|
+
// 会话 id 空间天然隔离;层级连接线与子代理同规则参与 trackRuns(R-01-003/AC-04),
|
|
1750
|
+
// 仍不参与徽标计数(kind 过滤,R-01-023/AC-04)。
|
|
1751
|
+
if (m.hasLive && !m.isSub) {
|
|
1752
|
+
for (const job of m.liveJobs) {
|
|
1753
|
+
entries.push({
|
|
1754
|
+
id: `job:${id}:${job.id}`,
|
|
1755
|
+
parentId: id,
|
|
1756
|
+
depth: depth + 1,
|
|
1757
|
+
kind: "job",
|
|
1758
|
+
title: job.label,
|
|
1759
|
+
jobKind: job.kind,
|
|
1760
|
+
workspaceTitle: "",
|
|
1761
|
+
workspaceKey: "",
|
|
1762
|
+
model: "",
|
|
1763
|
+
reasoning: "",
|
|
1764
|
+
timeline: [],
|
|
1765
|
+
userPreview: "",
|
|
1766
|
+
agentPreview: "",
|
|
1767
|
+
isCurrent: false,
|
|
1768
|
+
jobId: job.id,
|
|
1769
|
+
jobStatus: job.status,
|
|
1770
|
+
jobStartedAt: job.startedAt,
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1731
1774
|
}
|
|
1732
1775
|
for (const child of childIds.get(id) ?? []) visit(child, depth + 1);
|
|
1733
1776
|
};
|
|
@@ -1737,10 +1780,10 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1737
1780
|
return entries;
|
|
1738
1781
|
}
|
|
1739
1782
|
/**
|
|
1740
|
-
* 把活动条目压成母会话轨道运行(R-01-003/AC-04
|
|
1741
|
-
*
|
|
1742
|
-
*
|
|
1743
|
-
*
|
|
1783
|
+
* 把活动条目压成母会话轨道运行(R-01-003/AC-04):每个拥有可见直属子代理或后台
|
|
1784
|
+
* 任务子卡的母会话一条,记录全部可见直属子级 id(有序,末位即末级)与子级深度,
|
|
1785
|
+
* 供渲染层测量后绘制整条连续轨道与接入横线。条目按 preorder 排列,同一直属
|
|
1786
|
+
* 子级组天然连续。直属性按「条目深度 = 母会话条目深度 + 1」判定(与条目
|
|
1744
1787
|
* 顺序无关);无 id、无母会话条目或非直属的条目一律跳过。
|
|
1745
1788
|
*/
|
|
1746
1789
|
function trackRuns(entries) {
|
|
@@ -1752,7 +1795,8 @@ function trackRuns(entries) {
|
|
|
1752
1795
|
const runs = new Map();
|
|
1753
1796
|
for (const entry of list) {
|
|
1754
1797
|
if (entry?.id == null || entry?.parentId == null || (entry.depth ?? 0) < 1) continue;
|
|
1755
|
-
|
|
1798
|
+
// 子代理与后台任务子卡一视同仁(R-01-003/AC-04):两类直属子级同规则上轨。
|
|
1799
|
+
if (entry.kind !== "subagent" && entry.kind !== "job") continue;
|
|
1756
1800
|
const pid = String(entry.parentId);
|
|
1757
1801
|
const parentDepth = depthById.get(pid);
|
|
1758
1802
|
if (parentDepth === undefined || entry.depth !== parentDepth + 1) continue;
|
|
@@ -1830,6 +1874,13 @@ function cardSignature(entries) {
|
|
|
1830
1874
|
entry.tokenStats ?? [entry.outputTokens ?? null, entry.inputTokens ?? null, entry.cacheHitPct ?? null, entry.rateTokS ?? null, entry.elapsedMs ?? null],
|
|
1831
1875
|
// 累计运行时长参与签名(R-01-020):回填/SSE 推送与逐秒推进都要驱动重绘。
|
|
1832
1876
|
entry.totalBusyMs ?? null,
|
|
1877
|
+
// 在跑后台任务(R-01-023):任务视图随快照推送帧刷新,进入/退出在跑态驱动重绘;
|
|
1878
|
+
// job 子卡状态翻转与秒桶(渲染期注入的任务行时长推进)同入签名。
|
|
1879
|
+
entry.liveJobs ?? null,
|
|
1880
|
+
entry.jobStatus ?? null,
|
|
1881
|
+
// jobKind 不入签名(R-01-023/AC-05):同一 jobId 的工具类型由发起方定死恒定,
|
|
1882
|
+
// 签名分量永不变化;任务视图整体(liveJobs,含 kind)已随推送帧入签名。
|
|
1883
|
+
entry.jobsAgeSec ?? null,
|
|
1833
1884
|
]),
|
|
1834
1885
|
);
|
|
1835
1886
|
}
|
|
@@ -1952,6 +2003,149 @@ function entryErrorNote(completion) {
|
|
|
1952
2003
|
return typeof message === "string" && message !== "" ? message : ERROR_NOTE_FALLBACK;
|
|
1953
2004
|
}
|
|
1954
2005
|
|
|
2006
|
+
// ---- 后台任务(R-01-023、R-01-024):在跑活性归一与 job_output 读取回放 ----
|
|
2007
|
+
|
|
2008
|
+
/** 后台任务输出回放的字符上限(R-01-024/AC-04):模型每次 job_output 读取的
|
|
2009
|
+
* 已定案文本拼接后超出即截断并置 truncated;与单条错误信息截断同量级考虑。 */
|
|
2010
|
+
const JOB_OUTPUT_MAX_CHARS = 20000;
|
|
2011
|
+
|
|
2012
|
+
/** 在跑后台任务的活性状态全集(R-01-023):stopping 视同在跑——停止请求已发出但
|
|
2013
|
+
* 任务尚未结束,呈现与提醒抑制口径与 running 一致。 */
|
|
2014
|
+
const LIVE_JOB_STATUSES = new Set(["running", "stopping"]);
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* 归一会话的在跑后台任务列表(R-01-023/AC-01):取快照 `jobsBySession[id]` 中
|
|
2018
|
+
* status ∈ {running, stopping} 的任务视图,按 startedAt 升序(最早在前)。
|
|
2019
|
+
* jobsBySession 缺失、非记录、条目非记录或字段非法均按无任务/剔除处理,不抛错。
|
|
2020
|
+
* 返回 `{ id, kind, label, status, startedAt }` 的新数组(不泄漏宿主对象引用;
|
|
2021
|
+
* `kind` 为任务发起工具的类型原文,供任务卡工具名行消费,R-01-023/AC-05)。
|
|
2022
|
+
*/
|
|
2023
|
+
function liveJobsOf(jobsBySession, id) {
|
|
2024
|
+
if (!isRecord(jobsBySession)) return [];
|
|
2025
|
+
const list = jobsBySession[String(id)];
|
|
2026
|
+
if (!Array.isArray(list)) return [];
|
|
2027
|
+
const live = [];
|
|
2028
|
+
for (const job of list) {
|
|
2029
|
+
if (!isRecord(job)) continue;
|
|
2030
|
+
if (!LIVE_JOB_STATUSES.has(job.status)) continue;
|
|
2031
|
+
live.push({
|
|
2032
|
+
id: typeof job.id === "string" ? job.id : "",
|
|
2033
|
+
kind: typeof job.kind === "string" ? job.kind : "",
|
|
2034
|
+
// label 纯空白视为任务内容不可得(R-01-023/AC-06 的数据源头):不渲染空白行,
|
|
2035
|
+
// 不以占位文本补位;非空白保留原文(不额外 trim,显示层忠实原文)。
|
|
2036
|
+
label: typeof job.label === "string" && job.label.trim() !== "" ? job.label : "",
|
|
2037
|
+
status: job.status,
|
|
2038
|
+
startedAt: Number.isFinite(Number(job.startedAt)) ? Number(job.startedAt) : 0,
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
live.sort((a, b) => a.startedAt - b.startedAt);
|
|
2042
|
+
return live;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/** 后台任务工具名的友好显示映射(R-01-023/AC-05):bash→Bash、pwsh→PowerShell、
|
|
2046
|
+
* subagent→子代理;未知 kind 原样显示,非字符串或空串视为不可得(返回空串)。
|
|
2047
|
+
* 与 client 渲染层 JOB_STATUS_LABELS(状态词文案)分层:本表居 core 因核心与
|
|
2048
|
+
* 渲染两层共用且需 Node 单测钉住映射,状态词仅渲染层消费。 */
|
|
2049
|
+
const JOB_KIND_LABELS = { bash: "Bash", pwsh: "PowerShell", subagent: "子代理" };
|
|
2050
|
+
|
|
2051
|
+
function jobKindLabel(kind) {
|
|
2052
|
+
if (typeof kind !== "string" || kind === "") return "";
|
|
2053
|
+
return JOB_KIND_LABELS[kind] ?? kind;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/** 从 tool-result 消息提取纯文本:tool-result 内容块内的 text 片段按换行拼接;
|
|
2057
|
+
* 非数组内容或无文本块返回 undefined(错误结果是否纳入由调用方按 isError 判定)。 */
|
|
2058
|
+
function jobResultText(message) {
|
|
2059
|
+
if (!Array.isArray(message.content)) return undefined;
|
|
2060
|
+
const parts = [];
|
|
2061
|
+
for (const block of message.content) {
|
|
2062
|
+
if (!isRecord(block) || block.type !== "tool-result") continue;
|
|
2063
|
+
if (!Array.isArray(block.content)) continue;
|
|
2064
|
+
for (const item of block.content) {
|
|
2065
|
+
if (isRecord(item) && item.type === "text" && typeof item.text === "string") parts.push(item.text);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
return parts.length > 0 ? parts.join("\n") : undefined;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/** tool-result 是否为错误结果(内层 tool-result 块的 isError 标志)。 */
|
|
2072
|
+
function jobResultIsError(message) {
|
|
2073
|
+
if (!Array.isArray(message.content)) return false;
|
|
2074
|
+
return message.content.some((block) => isRecord(block) && block.type === "tool-result" && block.isError === true);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* 从事件记录列表提取 job_output 读写轨迹(R-01-024):`tool/call` 名为 `job_output`
|
|
2079
|
+
* 的行经 `arguments.job_id`(JSON 字符串或已解析对象)登记 callId → jobId 映射,
|
|
2080
|
+
* `tool/result` 行按 `message.source.callId` 配对并携带模型收到的定案文本与错误标志。
|
|
2081
|
+
* 兼容包装记录 `{ seq, event }` 与平面事件两种形状(seq 取事件或记录顶层);seq 非法的
|
|
2082
|
+
* 事件跳过。返回按 seq 升序的去重轨迹数组(同 seq 后到者胜——实时镜像与日志重放合并
|
|
2083
|
+
* 时的自然口径)。
|
|
2084
|
+
*/
|
|
2085
|
+
function jobOutputTraces(records) {
|
|
2086
|
+
const bySeq = new Map();
|
|
2087
|
+
for (const record of Array.isArray(records) ? records : []) {
|
|
2088
|
+
const event = eventOf(record);
|
|
2089
|
+
if (!isRecord(event) || (event.type !== "tool/call" && event.type !== "tool/result")) continue;
|
|
2090
|
+
const seq = Number(event.seq ?? (isRecord(record) ? record.seq : undefined));
|
|
2091
|
+
if (!Number.isFinite(seq)) continue;
|
|
2092
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
2093
|
+
if (event.type === "tool/call") {
|
|
2094
|
+
if (data.name !== "job_output" || typeof data.callId !== "string") continue;
|
|
2095
|
+
let jobId = null;
|
|
2096
|
+
if (typeof data.arguments === "string") {
|
|
2097
|
+
try {
|
|
2098
|
+
const parsed = JSON.parse(data.arguments);
|
|
2099
|
+
if (isRecord(parsed) && typeof parsed.job_id === "string") jobId = parsed.job_id;
|
|
2100
|
+
} catch {
|
|
2101
|
+
// 参数不可解析:不构成有效的 job_output 调用轨迹。
|
|
2102
|
+
}
|
|
2103
|
+
} else if (isRecord(data.arguments) && typeof data.arguments.job_id === "string") {
|
|
2104
|
+
jobId = data.arguments.job_id;
|
|
2105
|
+
}
|
|
2106
|
+
if (jobId === null) continue;
|
|
2107
|
+
bySeq.set(seq, { seq, kind: "call", callId: data.callId, jobId });
|
|
2108
|
+
} else {
|
|
2109
|
+
const message = isRecord(data.message) ? data.message : null;
|
|
2110
|
+
const callId = typeof message?.source?.callId === "string" ? message.source.callId : null;
|
|
2111
|
+
if (callId === null) continue;
|
|
2112
|
+
bySeq.set(seq, { seq, kind: "result", callId, text: jobResultText(message), isError: jobResultIsError(message) });
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
/**
|
|
2119
|
+
* 从轨迹回放指定后台任务的模型已读输出(R-01-024/AC-01):按 seq 序配对 call → result,
|
|
2120
|
+
* 拼接归属 jobId 的全部读取文本;剔除错误结果与 `(no new output)` 占位(模型视角的
|
|
2121
|
+
* 「无新内容」对人无展示价值)。返回 `{ text, truncated, read }`——read = 存在已配对
|
|
2122
|
+
* 读取(无配对时 text 为空,客户端以「尚未被读取」承接,R-01-024/AC-02);超出
|
|
2123
|
+
* limit 按字符截断并置 truncated(R-01-024/AC-04)。同 seq 轨迹去重(后到者胜),
|
|
2124
|
+
* 供日志重放与实时镜像两源合并。jobId 非法返回未读取空结果。
|
|
2125
|
+
*/
|
|
2126
|
+
function jobOutputFromTraces(traces, jobId, limit = JOB_OUTPUT_MAX_CHARS) {
|
|
2127
|
+
if (typeof jobId !== "string" || jobId === "") return { text: "", truncated: false, read: false };
|
|
2128
|
+
const bySeq = new Map();
|
|
2129
|
+
for (const trace of Array.isArray(traces) ? traces : []) {
|
|
2130
|
+
if (isRecord(trace) && Number.isFinite(Number(trace?.seq))) bySeq.set(Number(trace.seq), trace);
|
|
2131
|
+
}
|
|
2132
|
+
const jobOf = new Map();
|
|
2133
|
+
const parts = [];
|
|
2134
|
+
let read = false;
|
|
2135
|
+
for (const trace of [...bySeq.values()].sort((a, b) => a.seq - b.seq)) {
|
|
2136
|
+
if (trace?.kind === "call") {
|
|
2137
|
+
if (typeof trace.jobId === "string") jobOf.set(trace.callId, trace.jobId);
|
|
2138
|
+
} else if (jobOf.get(trace.callId) === jobId) {
|
|
2139
|
+
read = true;
|
|
2140
|
+
if (trace.isError !== true && typeof trace.text === "string" && !trace.text.startsWith("(no new output)")) {
|
|
2141
|
+
parts.push(trace.text);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
const text = parts.join("\n");
|
|
2146
|
+
return { text: text.length > limit ? text.slice(0, limit) : text, truncated: text.length > limit, read };
|
|
2147
|
+
}
|
|
2148
|
+
|
|
1955
2149
|
/**
|
|
1956
2150
|
* 活动区→历史区迁移检测(R-01-010/AC-07):上一帧活动区 id 在本帧离开活动区且出现于
|
|
1957
2151
|
* 历史区即判定为一次迁移;彻底消失(归档、不再被会话服务列出)不判定。prevActiveIds 为上一帧
|
|
@@ -2299,7 +2493,7 @@ function createSessionEventSerializer() {
|
|
|
2299
2493
|
* 也不入历史区。历史区不再按时间窗口或条数截断;turnEnds(id → 已知回合结束时刻)驱动
|
|
2300
2494
|
* activityAt 精化(R-01-010、R-01-019)。
|
|
2301
2495
|
*/
|
|
2302
|
-
function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedIds = [], completions = null, delegatingIds = null, turnEnds = null) {
|
|
2496
|
+
function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedIds = [], completions = null, delegatingIds = null, turnEnds = null, jobsBySession = null) {
|
|
2303
2497
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
2304
2498
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
2305
2499
|
const current = snapshot?.current ?? null;
|
|
@@ -2317,6 +2511,8 @@ function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedId
|
|
|
2317
2511
|
if (completionReminder(row, completionFor(id, completions), false)) continue; // 完成确认中,留在活动区
|
|
2318
2512
|
if (errorReminder(row, completionFor(id, completions), false)) continue; // 错误提醒中,留在活动区
|
|
2319
2513
|
if (delegatingIds instanceof Set && delegatingIds.has(String(id))) continue; // 委托周期中(含耗尽空窗),留在活动区
|
|
2514
|
+
// 在跑后台任务(R-01-023/AC-01、AC-03):留在活动区,不落入最近历史。
|
|
2515
|
+
if (liveJobsOf(jobsBySession, id).length > 0) continue;
|
|
2320
2516
|
if (isActiveRow(row, byId, activeIds)) continue;
|
|
2321
2517
|
const updatedAt = Number(row.updatedAt);
|
|
2322
2518
|
if (!Number.isFinite(updatedAt)) continue;
|
|
@@ -2919,7 +3115,8 @@ const CSS = `
|
|
|
2919
3115
|
.dap-token-stats,
|
|
2920
3116
|
.dap-foot,
|
|
2921
3117
|
.dap-history-line,
|
|
2922
|
-
.dap-note
|
|
3118
|
+
.dap-note,
|
|
3119
|
+
.dap-job-content
|
|
2923
3120
|
) {
|
|
2924
3121
|
display: none;
|
|
2925
3122
|
}
|
|
@@ -3077,7 +3274,7 @@ const CSS = `
|
|
|
3077
3274
|
gap: 4px;
|
|
3078
3275
|
cursor: pointer;
|
|
3079
3276
|
}
|
|
3080
|
-
/*
|
|
3277
|
+
/* 子代理与后台任务子卡层级连接线(R-01-003/AC-04):竖轨与横线全部由轨道层整体绘制——
|
|
3081
3278
|
syncTracks 测量各卡片浮点矩形,trackBoxes 统一取整到 CSS 像素后写入:
|
|
3082
3279
|
每个母会话一条连续竖轨 .dap-conn-track(母会话底缘 → 末级子卡中心,
|
|
3083
3280
|
含收口行),每个子卡一条横线 .dap-conn-stub(竖轨右缘 → 子卡左缘)。
|
|
@@ -3361,6 +3558,64 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3361
3558
|
}
|
|
3362
3559
|
[data-dsh-activity-pane] .dap-token-time { flex: none; }
|
|
3363
3560
|
[data-dsh-activity-pane] .dap-token-stats[hidden] { display: none; }
|
|
3561
|
+
/* 后台任务数量注(R-01-023/AC-01):母卡标题行内「后台 ×N」小注,弱化色调不抢标题。 */
|
|
3562
|
+
[data-dsh-activity-pane] .dap-jobs-chip {
|
|
3563
|
+
flex: none; font-size: 10px; line-height: 15px;
|
|
3564
|
+
color: color-mix(in srgb, currentColor 55%, transparent);
|
|
3565
|
+
font-variant-numeric: tabular-nums;
|
|
3566
|
+
}
|
|
3567
|
+
[data-dsh-activity-pane] .dap-jobs-chip[hidden] { display: none; }
|
|
3568
|
+
/* 后台任务子卡(R-01-023/AC-05、AC-07):与子代理卡同构的两行卡面,底色在子代理卡
|
|
3569
|
+
底色上轻染任务状态点同族的蓝以相互可辨;状态点色随任务状态翻转。 */
|
|
3570
|
+
[data-dsh-activity-pane] .dap-card[data-kind="job"] {
|
|
3571
|
+
padding: 6px 10px;
|
|
3572
|
+
border-radius: 12px;
|
|
3573
|
+
background: color-mix(in srgb, #65a0ff 8%, rgba(25, 27, 32, 0.95));
|
|
3574
|
+
cursor: pointer;
|
|
3575
|
+
}
|
|
3576
|
+
/* 任务卡行 1(R-01-023/AC-05):状态点 + 工具名称 + 右缘随时钟时长;工具名不可得时
|
|
3577
|
+
文本段隐藏,仅保留状态点与时长。 */
|
|
3578
|
+
[data-dsh-activity-pane] .dap-card[data-kind="job"] .dap-job-kind {
|
|
3579
|
+
flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
3580
|
+
font-size: 11px; line-height: 15px;
|
|
3581
|
+
}
|
|
3582
|
+
[data-dsh-activity-pane] .dap-job-dot {
|
|
3583
|
+
flex: none; width: 6px; height: 6px; border-radius: 50%;
|
|
3584
|
+
background: #65a0ff;
|
|
3585
|
+
}
|
|
3586
|
+
[data-dsh-activity-pane] .dap-job-dot[data-status="stopping"] { background: #f5a524; }
|
|
3587
|
+
[data-dsh-activity-pane] .dap-job-elapsed {
|
|
3588
|
+
flex: none; margin-left: auto; font-size: 10px; line-height: 15px;
|
|
3589
|
+
color: color-mix(in srgb, currentColor 55%, transparent); font-variant-numeric: tabular-nums;
|
|
3590
|
+
}
|
|
3591
|
+
/* 任务内容行(R-01-023/AC-05、AC-06):mono 原文单行省略,原生 tooltip 承载完整原文;
|
|
3592
|
+
内容不可得时整行 hidden([hidden] 显式覆盖 display:flex),紧凑档经密度规则隐藏。 */
|
|
3593
|
+
[data-dsh-activity-pane] .dap-job-content {
|
|
3594
|
+
display: flex; align-items: baseline; min-width: 0;
|
|
3595
|
+
margin-top: 1px;
|
|
3596
|
+
}
|
|
3597
|
+
[data-dsh-activity-pane] .dap-job-content[hidden] { display: none; }
|
|
3598
|
+
[data-dsh-activity-pane] .dap-card[data-kind="job"] .dap-job-label {
|
|
3599
|
+
flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
3600
|
+
font-family: var(--dsh-font-mono, monospace); font-size: 11px; line-height: 15px;
|
|
3601
|
+
}
|
|
3602
|
+
/* 子卡输出区(R-01-024/AC-01):展开时追加于卡内底部,终端风回放。 */
|
|
3603
|
+
[data-dsh-activity-pane] .dap-jobout {
|
|
3604
|
+
min-width: 0; max-height: 160px; overflow: auto;
|
|
3605
|
+
margin: 4px 0 0;
|
|
3606
|
+
background: color-mix(in srgb, currentColor 7%, transparent);
|
|
3607
|
+
border-radius: 6px; padding: 4px 6px;
|
|
3608
|
+
}
|
|
3609
|
+
[data-dsh-activity-pane] .dap-jobout-pre {
|
|
3610
|
+
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3611
|
+
font-family: var(--dsh-font-mono, monospace); font-size: 10px; line-height: 14px;
|
|
3612
|
+
color: color-mix(in srgb, currentColor 78%, transparent);
|
|
3613
|
+
}
|
|
3614
|
+
[data-dsh-activity-pane] .dap-jobout-hint {
|
|
3615
|
+
font-size: 10px; line-height: 14px;
|
|
3616
|
+
color: color-mix(in srgb, currentColor 55%, transparent);
|
|
3617
|
+
}
|
|
3618
|
+
[data-dsh-activity-pane] .dap-jobout-hint:empty { display: none; }
|
|
3364
3619
|
[data-dsh-activity-pane] .dap-history-line {
|
|
3365
3620
|
display: flex; align-items: center; gap: 4px; height: 15px;
|
|
3366
3621
|
min-width: 0; overflow: hidden; white-space: nowrap;
|
|
@@ -3670,6 +3925,10 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card:hover {
|
|
|
3670
3925
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="subagent"] {
|
|
3671
3926
|
background: var(--dsw-specific-sidebar-fill, rgb(249, 250, 251));
|
|
3672
3927
|
}
|
|
3928
|
+
/* 后台任务子卡浅色主题与子代理卡同源(R-01-024):淡侧栏填充底,轻染同族蓝与子代理卡区分(R-01-023/AC-07)。 */
|
|
3929
|
+
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="job"] {
|
|
3930
|
+
background: color-mix(in srgb, #65a0ff 8%, var(--dsw-specific-sidebar-fill, rgb(249, 250, 251)));
|
|
3931
|
+
}
|
|
3673
3932
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="recent"] {
|
|
3674
3933
|
/* 暗于活动卡的 --dsw-alias-bg-layer-2 纯白、深于窗格底色(R-01-013/AC-10、AC-11)。 */
|
|
3675
3934
|
background: rgb(243, 244, 246);
|
|
@@ -3991,6 +4250,8 @@ function apply(ctx) {
|
|
|
3991
4250
|
return promise;
|
|
3992
4251
|
}
|
|
3993
4252
|
const sessionOpenLoads = new Map();
|
|
4253
|
+
/** 子代理目录按需拉取记账(T-151):parentId → 已触发;失败不热重试(行为有界)。 */
|
|
4254
|
+
const subagentCatalogRequests = new Set();
|
|
3994
4255
|
|
|
3995
4256
|
const style = document.createElement("style");
|
|
3996
4257
|
style.id = STYLE_ID;
|
|
@@ -4153,20 +4414,69 @@ function apply(ctx) {
|
|
|
4153
4414
|
}
|
|
4154
4415
|
}
|
|
4155
4416
|
|
|
4156
|
-
/** 回到前台(含 bfcache 还原)时 ack
|
|
4157
|
-
|
|
4158
|
-
|
|
4417
|
+
/** 回到前台(含 bfcache 还原)时 ack 与任务输出通道自愈:重建 SSE(jobs 无全量快照,
|
|
4418
|
+
* 重连后由下一次轨迹通知或重新选中收敛;选中输出随卡片渲染帧刷新)。 */
|
|
4419
|
+
function resumePushChannels() {
|
|
4420
|
+
if (!disposed) {
|
|
4421
|
+
connectAcksStream();
|
|
4422
|
+
connectJobsStream();
|
|
4423
|
+
}
|
|
4159
4424
|
}
|
|
4160
4425
|
const onVisibilityResume = () => {
|
|
4161
|
-
if (document.visibilityState === "visible")
|
|
4426
|
+
if (document.visibilityState === "visible") resumePushChannels();
|
|
4162
4427
|
};
|
|
4163
4428
|
const onPageShow = (event) => {
|
|
4164
|
-
if (event?.persisted === true)
|
|
4429
|
+
if (event?.persisted === true) resumePushChannels();
|
|
4165
4430
|
};
|
|
4166
4431
|
document.addEventListener("visibilitychange", onVisibilityResume);
|
|
4167
4432
|
window.addEventListener("pageshow", onPageShow);
|
|
4168
4433
|
connectAcksStream();
|
|
4169
4434
|
|
|
4435
|
+
// ---- 后台任务输出通道(R-01-024) ----
|
|
4436
|
+
// SSE 订阅宿主侧 job_output 轨迹通知:连接即发空快照(轨迹不入库、无全量快照语义),
|
|
4437
|
+
// 此后每个新轨迹广播 { sessionId, jobId };客户端仅对「已选中该任务」的可见卡片
|
|
4438
|
+
// 回读输出(R-01-024/AC-03)。无 EventSource 环境静默降级为选中时不自动刷新,
|
|
4439
|
+
// 不引入轮询。
|
|
4440
|
+
let jobsSource = null;
|
|
4441
|
+
|
|
4442
|
+
/** 轨迹通知应用:命中已展开该任务的 job 子卡即置脏并回读一次;其余通知忽略。 */
|
|
4443
|
+
function applyJobTraceNotice(raw) {
|
|
4444
|
+
if (disposed) return;
|
|
4445
|
+
let notice = null;
|
|
4446
|
+
try {
|
|
4447
|
+
notice = JSON.parse(raw);
|
|
4448
|
+
} catch {
|
|
4449
|
+
notice = null;
|
|
4450
|
+
}
|
|
4451
|
+
const sessionId = typeof notice?.sessionId === "string" ? notice.sessionId : null;
|
|
4452
|
+
const jobId = typeof notice?.jobId === "string" ? notice.jobId : null;
|
|
4453
|
+
if (sessionId === null || jobId === null) return;
|
|
4454
|
+
for (const [, card] of cardsById) {
|
|
4455
|
+
const el = card?.el;
|
|
4456
|
+
if (el?.dataset?.kind !== "job" || el.dataset.jobOwner !== sessionId || el.dataset.jobId !== jobId) continue;
|
|
4457
|
+
// 脏标记 + 回读钩子:仅展开中的输出区需要拉取新轨迹(R-01-024/AC-03)。
|
|
4458
|
+
el.__jobOutDirty = true;
|
|
4459
|
+
el.__jobOutReload?.();
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
/** (重)建 jobs SSE 连接:与 acks 通道同模式的断连自愈口径。 */
|
|
4464
|
+
function connectJobsStream() {
|
|
4465
|
+
try {
|
|
4466
|
+
jobsSource?.close();
|
|
4467
|
+
} catch {}
|
|
4468
|
+
jobsSource = null;
|
|
4469
|
+
if (disposed || typeof window.EventSource !== "function") return;
|
|
4470
|
+
try {
|
|
4471
|
+
const source = new window.EventSource(`${PANE_API_BASE}/jobs/stream`);
|
|
4472
|
+
source.addEventListener("state", (event) => applyJobTraceNotice(event.data ?? ""));
|
|
4473
|
+
jobsSource = source;
|
|
4474
|
+
} catch {
|
|
4475
|
+
jobsSource = null;
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
connectJobsStream();
|
|
4479
|
+
|
|
4170
4480
|
/** 确认写回(R-01-002/AC-10~AC-12):乐观更新本地游标(签名驱动即时解除),
|
|
4171
4481
|
* 再 POST 宿主侧持久化并广播;写回失败回滚本地游标(提醒恢复),不吞异常。 */
|
|
4172
4482
|
async function ackCompletion(sessionId) {
|
|
@@ -4324,20 +4634,34 @@ function apply(ctx) {
|
|
|
4324
4634
|
throw response?.error ?? new Error("remote request failed");
|
|
4325
4635
|
}
|
|
4326
4636
|
|
|
4637
|
+
/** 母会话子代理目录条目查找(T-151 单源):open 地址与深翻分页地址共用同一取法,
|
|
4638
|
+
* 防条目形状或键名演化时两处漂移。 */
|
|
4639
|
+
function subagentCatalogEntry(subagentsByParent, parentId, id) {
|
|
4640
|
+
const entries = subagentsByParent?.[String(parentId)]?.entries;
|
|
4641
|
+
return Array.isArray(entries)
|
|
4642
|
+
? entries.find((candidate) => String(candidate?.id) === String(id))
|
|
4643
|
+
: undefined;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4327
4646
|
/** 会话日志分页地址(R-01-012):主会话 `{kind:"session", sessionId}`;子代理
|
|
4328
4647
|
* `{kind:"subagent", parentSessionId, childSessionId, mode}`——母会话 id 兼容
|
|
4329
|
-
* `parentSessionId` / `parentId`
|
|
4330
|
-
*
|
|
4331
|
-
|
|
4648
|
+
* `parentSessionId` / `parentId` 两种条目键名。mode 必须与子代理描述符一致,
|
|
4649
|
+
* 不一致被宿主以 subagent/unauthorized 拒绝(T-151):优先取母会话目录条目的
|
|
4650
|
+
* mode,目录条目缺失(未加载、在途空窗或无该子条目)时回退行 `continuable`
|
|
4651
|
+
* 启发(读取失败由 pagedHistoryEvents 以 null 收敛为空白详情,R-01-013)。 */
|
|
4652
|
+
function sessionPageAddress(id, byId, subagentsByParent = null) {
|
|
4332
4653
|
const row = byId[id] ?? {};
|
|
4333
4654
|
if (row?.origin === "subagent") {
|
|
4334
4655
|
const parentId = row.parentSessionId ?? row.parentId;
|
|
4335
4656
|
if (parentId !== undefined && parentId !== null) {
|
|
4657
|
+
const entry = subagentCatalogEntry(subagentsByParent, String(parentId), id);
|
|
4336
4658
|
return {
|
|
4337
4659
|
kind: "subagent",
|
|
4338
4660
|
parentSessionId: String(parentId),
|
|
4339
4661
|
childSessionId: String(id),
|
|
4340
|
-
mode:
|
|
4662
|
+
mode: typeof entry?.mode === "string" && entry.mode !== ""
|
|
4663
|
+
? entry.mode
|
|
4664
|
+
: row.continuable === true ? "continuable" : "one-shot",
|
|
4341
4665
|
};
|
|
4342
4666
|
}
|
|
4343
4667
|
}
|
|
@@ -4433,7 +4757,8 @@ function apply(ctx) {
|
|
|
4433
4757
|
const sessionRemote = ctx.get("remote.session") ?? null;
|
|
4434
4758
|
if (!sessionRemote) return;
|
|
4435
4759
|
ensureCatalogGroups(sessionRemote);
|
|
4436
|
-
const
|
|
4760
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
4761
|
+
const byId = listSnap?.byId ?? {};
|
|
4437
4762
|
const historyPromises = [];
|
|
4438
4763
|
for (const id of ids) {
|
|
4439
4764
|
const detail = sessionDetailsById.get(id) ?? {};
|
|
@@ -4473,7 +4798,7 @@ function apply(ctx) {
|
|
|
4473
4798
|
if (deepReadNeeded && !historyLoads.has(id)) {
|
|
4474
4799
|
detail.previewFallbackLoaded = true;
|
|
4475
4800
|
detail.durationFallbackLoaded = true;
|
|
4476
|
-
const address = sessionPageAddress(id, byId);
|
|
4801
|
+
const address = sessionPageAddress(id, byId, listSnap?.subagentsByParent ?? null);
|
|
4477
4802
|
const promise = enqueueDetailLoad(() => Promise.resolve()
|
|
4478
4803
|
.then(async () => {
|
|
4479
4804
|
const { events, error } = await pagedHistoryEvents({
|
|
@@ -4567,6 +4892,8 @@ function apply(ctx) {
|
|
|
4567
4892
|
// 不可知,只以 openState 把关会对已水合会话逐帧重发 open,settle→delete→重发
|
|
4568
4893
|
// 的在途翻转同样驱动加载指示抖动(docsim 卡闪烁根因之一)。
|
|
4569
4894
|
if (!detail.log && detail.snapshot?.openState !== "open" && !sessionOpenLoads.has(id) && typeof session.open === "function") {
|
|
4895
|
+
// 子代理事件流仅接受持久父地址:open 前先安装(T-151),否则宿主拒绝、窗口永不水合。
|
|
4896
|
+
ensureSubagentAddress(id, session);
|
|
4570
4897
|
const opening = Promise.resolve(session.open()).catch(() => {});
|
|
4571
4898
|
sessionOpenLoads.set(id, opening);
|
|
4572
4899
|
opening.finally(() => {
|
|
@@ -4606,6 +4933,53 @@ function apply(ctx) {
|
|
|
4606
4933
|
queueSync();
|
|
4607
4934
|
}
|
|
4608
4935
|
|
|
4936
|
+
/** 触发母会话子代理目录单发读取(T-151):目录条目是地址 mode 的唯一可靠来源,
|
|
4937
|
+
* 未加载时先拉取,目录随下一轮快照到达;每次拉取经原生 sessions.refreshSubagents
|
|
4938
|
+
* 走既有 remote 通道,单父会话单飞、不重试,不构成轮询(R-02-004)。 */
|
|
4939
|
+
function requestSubagentCatalog(parentId) {
|
|
4940
|
+
if (typeof sessions?.refreshSubagents !== "function") return;
|
|
4941
|
+
if (subagentCatalogRequests.has(parentId)) return;
|
|
4942
|
+
subagentCatalogRequests.add(parentId);
|
|
4943
|
+
try {
|
|
4944
|
+
Promise.resolve(sessions?.refreshSubagents?.(parentId))
|
|
4945
|
+
.catch(() => {})
|
|
4946
|
+
.finally(() => {
|
|
4947
|
+
if (!disposed) queueSync();
|
|
4948
|
+
});
|
|
4949
|
+
} catch {
|
|
4950
|
+
subagentCatalogRequests.delete(parentId);
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4954
|
+
/** 子代理会话事件流打开前置(T-151 缺陷修复):dsh 0.1.5 起宿主按地址校验会话事件
|
|
4955
|
+
* 流路由——子代理会话仅接受持久父地址,且地址 mode 必须与其描述符一致(不一致被
|
|
4956
|
+
* subagent/unauthorized 拒绝),持久地址仅经原生导航留存。mode 唯一可靠来源是母
|
|
4957
|
+
* 会话的子代理目录条目:在册即经 configureSubagent 安装(与原生 selectSubagent
|
|
4958
|
+
* 同构,但不切换当前会话);目录未加载时先单发拉取并跳过本轮安装,避免装入必被
|
|
4959
|
+
* 拒绝的错误 mode 地址;目录拉取失败沿用既有空白详情路径(深翻以 null 收敛),
|
|
4960
|
+
* 不阻断其余卡片。非子代理行或会话对象缺 configureSubagent 时为无操作。 */
|
|
4961
|
+
function ensureSubagentAddress(id, session) {
|
|
4962
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
4963
|
+
const byId = listSnap?.byId ?? {};
|
|
4964
|
+
if (!isSubagentRow(byId[id], byId)) return;
|
|
4965
|
+
if (typeof session?.configureSubagent !== "function") return;
|
|
4966
|
+
const parentId = String(byId[id].parentSessionId ?? byId[id].parentId);
|
|
4967
|
+
const catalog = isRecord(listSnap?.subagentsByParent) ? listSnap.subagentsByParent[parentId] : null;
|
|
4968
|
+
const entry = subagentCatalogEntry(listSnap?.subagentsByParent ?? null, parentId, id);
|
|
4969
|
+
if (!isRecord(entry) || typeof entry.mode !== "string" || entry.mode === "") {
|
|
4970
|
+
requestSubagentCatalog(parentId);
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
try {
|
|
4974
|
+
session.configureSubagent(
|
|
4975
|
+
{ kind: "subagent", parentSessionId: parentId, childSessionId: String(id), mode: entry.mode },
|
|
4976
|
+
typeof catalog?.parentAvailable === "boolean" ? catalog.parentAvailable : undefined,
|
|
4977
|
+
);
|
|
4978
|
+
} catch {
|
|
4979
|
+
// 安装失败沿用既有空白详情路径,不阻断其余卡片。
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4609
4983
|
/** 部署级模型目录一次性读取(R-01-012/AC-01):dsh 0.1.5 起 per-session models
|
|
4610
4984
|
* RPC 移除,目录经 `remote.session.modelCatalog()` 无参数读取;成功即缓存不再
|
|
4611
4985
|
* 重发,失败留待下轮详情读取重试。 */
|
|
@@ -5029,8 +5403,17 @@ function apply(ctx) {
|
|
|
5029
5403
|
foot.append(awaitHead, noteRow);
|
|
5030
5404
|
return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];
|
|
5031
5405
|
}
|
|
5406
|
+
if (kind === "job") {
|
|
5407
|
+
// 后台任务子卡(R-01-023/AC-05、R-01-024):行 1 = 状态点 + 工具名称 + 随时钟
|
|
5408
|
+
// 时长;行 2 = 任务内容原文(mono 省略 + tooltip);输出区在展开时追加。
|
|
5409
|
+
const row = makeEl("div", "dap-row");
|
|
5410
|
+
row.append(makeEl("span", "dap-job-dot"), makeEl("span", "dap-job-kind"), makeEl("span", "dap-job-elapsed"));
|
|
5411
|
+
const content = makeEl("div", "dap-job-content");
|
|
5412
|
+
content.append(makeEl("span", "dap-job-label"));
|
|
5413
|
+
return [row, content];
|
|
5414
|
+
}
|
|
5032
5415
|
const row = makeEl("div", "dap-row");
|
|
5033
|
-
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-total-time"));
|
|
5416
|
+
row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-jobs-chip"), makeEl("span", "dap-total-time"));
|
|
5034
5417
|
const progressRow = makeProgressRow();
|
|
5035
5418
|
return [head, row, makeEl("div", "dap-trace"), progressRow, makeStatsRow()];
|
|
5036
5419
|
}
|
|
@@ -5511,6 +5894,139 @@ function apply(ctx) {
|
|
|
5511
5894
|
renderTrace(container, entry.timeline, { lastOnly });
|
|
5512
5895
|
}
|
|
5513
5896
|
|
|
5897
|
+
/** 后台任务状态的两字中文短语(R-01-023 呈现):与原生 ui-jobs 状态词同口径。 */
|
|
5898
|
+
const JOB_STATUS_LABELS = { running: "运行中", stopping: "停止中", completed: "已完成", killed: "已终止", failed: "失败" };
|
|
5899
|
+
|
|
5900
|
+
/** 后台任务数量注(R-01-023/AC-01):母卡标题行内 `后台 ×N` 小注;任务本体以 job
|
|
5901
|
+
* 子卡呈现(R-01-024),本注仅承载数量语义。无 liveJobs 时隐藏节点。 */
|
|
5902
|
+
function renderJobsChip(el, entry) {
|
|
5903
|
+
const chip = el.querySelector(".dap-jobs-chip");
|
|
5904
|
+
if (chip === null) return;
|
|
5905
|
+
const count = Array.isArray(entry.liveJobs) ? entry.liveJobs.length : 0;
|
|
5906
|
+
const text = count > 0 ? `后台 ×${count}` : "";
|
|
5907
|
+
if (chip.textContent !== text) chip.textContent = text;
|
|
5908
|
+
const hidden = count === 0;
|
|
5909
|
+
if (chip.hidden !== hidden) chip.hidden = hidden;
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5912
|
+
/** 后台任务子卡的输出展开/收起(R-01-024/AC-01):激活 job 卡即在卡内展开输出区,
|
|
5913
|
+
* 再次激活收起;同一母会话下至多展开一张(点击新卡先收兄弟,稳定性与共享坞等价)。 */
|
|
5914
|
+
function toggleJobExpanded(el) {
|
|
5915
|
+
if (el.hasAttribute("data-expanded")) {
|
|
5916
|
+
el.removeAttribute("data-expanded");
|
|
5917
|
+
el.querySelector(".dap-jobout")?.remove();
|
|
5918
|
+
delete el.dataset.loadedFor;
|
|
5919
|
+
el.__jobOutReload = null;
|
|
5920
|
+
return;
|
|
5921
|
+
}
|
|
5922
|
+
const ownerId = el.dataset.jobOwner ?? "";
|
|
5923
|
+
for (const [, card] of cardsById) {
|
|
5924
|
+
const other = card?.el;
|
|
5925
|
+
if (other === el || other?.dataset?.kind !== "job" || other.dataset.jobOwner !== ownerId) continue;
|
|
5926
|
+
other.removeAttribute("data-expanded");
|
|
5927
|
+
other.querySelector(".dap-jobout")?.remove();
|
|
5928
|
+
delete other.dataset.loadedFor;
|
|
5929
|
+
other.__jobOutReload = null;
|
|
5930
|
+
}
|
|
5931
|
+
el.setAttribute("data-expanded", "");
|
|
5932
|
+
// 展开视为显式回读请求:置脏令 renderJobOutput 必发一次(含重复点选的重试语义)。
|
|
5933
|
+
el.__jobOutDirty = true;
|
|
5934
|
+
renderJobOutput(el);
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
/** 输出区装载/刷新(R-01-024/AC-01~AC-04):按卡上归属与任务 id 回读一次模型已读
|
|
5938
|
+
* 输出——未被读取显示「尚未被读取」(AC-02),超限截断提示(AC-04),失败诚实降级。
|
|
5939
|
+
* 回读仅在展开、选中切换(=重新展开)或轨迹通知置脏时发出(R-01-024/AC-03):
|
|
5940
|
+
* loadedFor 未变且未置脏时渲染帧直达 return,不构成周期性拉取(R-02-004)。
|
|
5941
|
+
* token 使迟到的响应失效(快速切换不串内容)。 */
|
|
5942
|
+
function renderJobOutput(el) {
|
|
5943
|
+
if (!el.hasAttribute("data-expanded")) return;
|
|
5944
|
+
const sessionId = el.dataset.jobOwner ?? null;
|
|
5945
|
+
const jobId = el.dataset.jobId ?? null;
|
|
5946
|
+
if (sessionId === null || sessionId === "" || jobId === null || jobId === "") return;
|
|
5947
|
+
let out = el.querySelector(".dap-jobout");
|
|
5948
|
+
if (out === null) {
|
|
5949
|
+
out = makeEl("div", "dap-jobout");
|
|
5950
|
+
out.append(makeEl("pre", "dap-jobout-pre"), makeEl("div", "dap-jobout-hint"));
|
|
5951
|
+
el.append(out);
|
|
5952
|
+
}
|
|
5953
|
+
const loadedFor = el.dataset.loadedFor;
|
|
5954
|
+
const key = `${sessionId}/${jobId}`;
|
|
5955
|
+
if (loadedFor === key && el.__jobOutDirty !== true) return;
|
|
5956
|
+
el.dataset.loadedFor = key;
|
|
5957
|
+
el.__jobOutDirty = false;
|
|
5958
|
+
const pre = out.querySelector(".dap-jobout-pre");
|
|
5959
|
+
const hint = out.querySelector(".dap-jobout-hint");
|
|
5960
|
+
const token = `${sessionId} ${jobId} ${Date.now()}`;
|
|
5961
|
+
el.__jobOutToken = token;
|
|
5962
|
+
// jobs SSE 轨迹通知按此钩子触发回读(R-01-024/AC-03);随卡片重建自然失效。
|
|
5963
|
+
el.__jobOutReload = () => renderJobOutput(el);
|
|
5964
|
+
pre.textContent = "";
|
|
5965
|
+
hint.textContent = "读取中…";
|
|
5966
|
+
fetch(`${PANE_API_BASE}/jobs-output?sessionId=${encodeURIComponent(sessionId)}&jobId=${encodeURIComponent(jobId)}`)
|
|
5967
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
5968
|
+
.then((payload) => {
|
|
5969
|
+
if (el.__jobOutToken !== token || disposed) return;
|
|
5970
|
+
if (payload === null || typeof payload !== "object") {
|
|
5971
|
+
hint.textContent = "输出读取失败";
|
|
5972
|
+
return;
|
|
5973
|
+
}
|
|
5974
|
+
pre.textContent = typeof payload.text === "string" ? payload.text : "";
|
|
5975
|
+
hint.textContent =
|
|
5976
|
+
payload.read === false
|
|
5977
|
+
? "尚未被读取"
|
|
5978
|
+
: payload.truncated === true
|
|
5979
|
+
? "输出过长,已截断"
|
|
5980
|
+
: "";
|
|
5981
|
+
})
|
|
5982
|
+
.catch(() => {
|
|
5983
|
+
if (el.__jobOutToken === token && !disposed) hint.textContent = "输出读取失败";
|
|
5984
|
+
});
|
|
5985
|
+
}
|
|
5986
|
+
|
|
5987
|
+
/** 后台任务子卡渲染(R-01-023/AC-01、R-01-024/AC-01):状态点着色、标题与随时钟
|
|
5988
|
+
* 推进的已运行时长;展开态下装载/刷新输出区。任务消失由条目派生侧收卡(随
|
|
5989
|
+
* liveJobs 清空整个条目消失),此处只呈现当帧状态。 */
|
|
5990
|
+
function renderJobCardInto(el, entry) {
|
|
5991
|
+
const dot = el.querySelector(".dap-job-dot");
|
|
5992
|
+
if (dot !== null && dot.dataset.status !== entry.jobStatus) dot.dataset.status = entry.jobStatus;
|
|
5993
|
+
// 工具名称(R-01-023/AC-05):核心友好映射;不可得时隐藏文本段,仅保留状态点与时长。
|
|
5994
|
+
const kindEl = el.querySelector(".dap-job-kind");
|
|
5995
|
+
if (kindEl !== null) {
|
|
5996
|
+
const kindText = jobKindLabel(entry.jobKind);
|
|
5997
|
+
if (kindEl.textContent !== kindText) kindEl.textContent = kindText;
|
|
5998
|
+
const kindHidden = kindText === "";
|
|
5999
|
+
if (kindEl.hidden !== kindHidden) kindEl.hidden = kindHidden;
|
|
6000
|
+
}
|
|
6001
|
+
// 任务内容行(R-01-023/AC-05、AC-06):mono 原文单行省略,完整原文以原生 tooltip
|
|
6002
|
+
// 显示(沿用 R-01-024 呈现细化语义,tooltip 归内容行);内容不可得时整行隐藏,
|
|
6003
|
+
// 不补空白或占位。
|
|
6004
|
+
const contentRow = el.querySelector(".dap-job-content");
|
|
6005
|
+
const labelText = String(entry.title ?? "");
|
|
6006
|
+
if (contentRow !== null) {
|
|
6007
|
+
const contentHidden = labelText === "";
|
|
6008
|
+
if (contentRow.hidden !== contentHidden) contentRow.hidden = contentHidden;
|
|
6009
|
+
if (!contentHidden) {
|
|
6010
|
+
const label = contentRow.querySelector(".dap-job-label");
|
|
6011
|
+
if (label !== null) {
|
|
6012
|
+
if (label.textContent !== labelText) label.textContent = labelText;
|
|
6013
|
+
if (label.title !== labelText) label.title = labelText;
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
const elapsed = el.querySelector(".dap-job-elapsed");
|
|
6018
|
+
if (elapsed !== null) {
|
|
6019
|
+
const elapsedText =
|
|
6020
|
+
Number.isFinite(entry.jobStartedAt) && entry.jobStartedAt > 0
|
|
6021
|
+
? fmtElapsedMs(Math.max(0, Date.now() - entry.jobStartedAt))
|
|
6022
|
+
: "";
|
|
6023
|
+
if (elapsed.textContent !== elapsedText) elapsed.textContent = elapsedText;
|
|
6024
|
+
}
|
|
6025
|
+
if (el.dataset.jobOwner !== String(entry.parentId)) el.dataset.jobOwner = String(entry.parentId);
|
|
6026
|
+
if (el.dataset.jobId !== String(entry.jobId)) el.dataset.jobId = String(entry.jobId);
|
|
6027
|
+
renderJobOutput(el);
|
|
6028
|
+
}
|
|
6029
|
+
|
|
5514
6030
|
function renderCardInto(el, entry, colorByWorkspace) {
|
|
5515
6031
|
const workspaceLabel = el.querySelector(".dap-workspace");
|
|
5516
6032
|
if (workspaceLabel !== null) {
|
|
@@ -5624,13 +6140,30 @@ function apply(ctx) {
|
|
|
5624
6140
|
}
|
|
5625
6141
|
|
|
5626
6142
|
if (entry.kind === "running") {
|
|
5627
|
-
|
|
6143
|
+
// 仅在跑后台任务(R-01-023/AC-01):呈现冻结——隐藏进度行(进度/条纹为回合运行
|
|
6144
|
+
// 语义,不冒充回合执行),时间线保留最后已知状态;任务本体以 job 子卡呈现。
|
|
6145
|
+
const jobsOnly = Array.isArray(entry.liveJobs) && entry.liveJobs.length > 0 && entry.selfRunning !== true;
|
|
6146
|
+
renderJobsChip(el, entry);
|
|
6147
|
+
const progressRow = el.querySelector(".dap-progress");
|
|
6148
|
+
if (progressRow !== null) {
|
|
6149
|
+
if (jobsOnly) {
|
|
6150
|
+
if (!progressRow.hidden) progressRow.hidden = true;
|
|
6151
|
+
} else {
|
|
6152
|
+
renderProgressRow(el, entry.progress);
|
|
6153
|
+
if (progressRow.hidden) progressRow.hidden = false;
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
5628
6156
|
const traceContainer = el.querySelector(".dap-trace");
|
|
5629
6157
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
|
|
5630
6158
|
renderTokenStats(el, entry);
|
|
5631
6159
|
return;
|
|
5632
6160
|
}
|
|
5633
6161
|
|
|
6162
|
+
if (entry.kind === "job") {
|
|
6163
|
+
renderJobCardInto(el, entry);
|
|
6164
|
+
return;
|
|
6165
|
+
}
|
|
6166
|
+
|
|
5634
6167
|
if (entry.kind === "subagent") {
|
|
5635
6168
|
const traceContainer = el.querySelector(".dap-subtrace");
|
|
5636
6169
|
if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: true });
|
|
@@ -5741,6 +6274,9 @@ function apply(ctx) {
|
|
|
5741
6274
|
} catch {
|
|
5742
6275
|
continue; // 订阅失败:本次跳过,下次渲染重试
|
|
5743
6276
|
}
|
|
6277
|
+
// 子代理事件流仅接受持久父地址:open 前先安装(T-151),否则宿主拒绝、窗口永不水合。
|
|
6278
|
+
ensureSubagentAddress(id, session);
|
|
6279
|
+
let opening = null;
|
|
5744
6280
|
try {
|
|
5745
6281
|
opening = session.open?.();
|
|
5746
6282
|
} catch {
|
|
@@ -5964,6 +6500,12 @@ function apply(ctx) {
|
|
|
5964
6500
|
const el = document.createElement("div");
|
|
5965
6501
|
el.className = CARD_CLASS;
|
|
5966
6502
|
const unbind = bindCardActivation(el, (sessionId) => {
|
|
6503
|
+
// 后台任务子卡(R-01-024/AC-01):激活即切换卡内输出展开,不发起会话跳转
|
|
6504
|
+
//(job 复合 id 非会话 id,无可打开目标)。
|
|
6505
|
+
if (el.dataset.kind === "job") {
|
|
6506
|
+
toggleJobExpanded(el);
|
|
6507
|
+
return;
|
|
6508
|
+
}
|
|
5967
6509
|
if (typeof sessions?.open !== "function") return;
|
|
5968
6510
|
lastActivatedId = sessionId;
|
|
5969
6511
|
// 新激活意图取代一切旧重试链,避免过期链条稍后把当前会话拽回旧目标;
|
|
@@ -6007,10 +6549,13 @@ function apply(ctx) {
|
|
|
6007
6549
|
rec.el.setAttribute("data-wait", entry.waitClass);
|
|
6008
6550
|
else rec.el.removeAttribute("data-wait");
|
|
6009
6551
|
const recentTimeText = entry.kind === "recent" ? fmtRecentTime(entry.activityAt) : "";
|
|
6552
|
+
const jobStatusText = entry.kind === "job" ? JOB_STATUS_LABELS[entry.jobStatus] ?? "" : "";
|
|
6553
|
+
const jobKindText = entry.kind === "job" ? jobKindLabel(entry.jobKind) : "";
|
|
6010
6554
|
rec.el.setAttribute(
|
|
6011
6555
|
"aria-label",
|
|
6012
|
-
`${entry.workspaceTitle ? entry.workspaceTitle + " - " : ""}${entry.title}${
|
|
6013
|
-
|
|
6556
|
+
`${entry.workspaceTitle ? entry.workspaceTitle + " - " : ""}${jobKindText ? jobKindText + "," : ""}${entry.title}${
|
|
6557
|
+
jobStatusText ? "," + jobStatusText : ""
|
|
6558
|
+
}${entry.pendingText ? "," + entry.pendingText : ""
|
|
6014
6559
|
}${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}${
|
|
6015
6560
|
recentTimeText ? "," + recentTimeText : ""
|
|
6016
6561
|
}`,
|
|
@@ -6292,7 +6837,10 @@ function apply(ctx) {
|
|
|
6292
6837
|
for (const [id, record] of busyById) {
|
|
6293
6838
|
if (typeof record?.openWaitStart === "number") waitingStarts.set(String(id), record.openWaitStart);
|
|
6294
6839
|
}
|
|
6295
|
-
|
|
6840
|
+
// 在跑后台任务(R-01-023):jobsBySession 快照注入 buildEntries/buildRecent——
|
|
6841
|
+
// liveJobs 非空的主会话保留活动区、归运行组并抑制完成/错误提醒。
|
|
6842
|
+
const jobsBySession = isRecord(snapshot) && isRecord(snapshot.jobsBySession) ? snapshot.jobsBySession : null;
|
|
6843
|
+
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds, waitingStarts, jobsBySession);
|
|
6296
6844
|
// 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
|
|
6297
6845
|
// 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
|
|
6298
6846
|
const runLikeIds = new Set(
|
|
@@ -6301,7 +6849,7 @@ function apply(ctx) {
|
|
|
6301
6849
|
// 运行卡时钟:运行中子代理卡同样承载逐秒推进的进度与时长(R-01-009/AC-14)。
|
|
6302
6850
|
syncLiveness(
|
|
6303
6851
|
runLikeIds,
|
|
6304
|
-
active.some((entry) => entry.kind === "running" || (entry.kind === "subagent" && runLikeIds.has(entry.id))),
|
|
6852
|
+
active.some((entry) => entry.kind === "running" || entry.kind === "job" || (entry.kind === "subagent" && runLikeIds.has(entry.id))),
|
|
6305
6853
|
);
|
|
6306
6854
|
for (const entry of active) {
|
|
6307
6855
|
const liveRecord = livenessById.get(entry.id);
|
|
@@ -6464,7 +7012,7 @@ function apply(ctx) {
|
|
|
6464
7012
|
}
|
|
6465
7013
|
if (detail.memoTurnEnd != null) turnEnds[id] = detail.memoTurnEnd;
|
|
6466
7014
|
}
|
|
6467
|
-
const recentCandidates = buildRecent(snapshot, workspaceItems, now, sessionDetailsById, archivedSessionIds, completeAcksById, delegatingIds, turnEnds);
|
|
7015
|
+
const recentCandidates = buildRecent(snapshot, workspaceItems, now, sessionDetailsById, archivedSessionIds, completeAcksById, delegatingIds, turnEnds, jobsBySession);
|
|
6468
7016
|
recentTotal = recentCandidates.length;
|
|
6469
7017
|
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
6470
7018
|
recentHasMore = recent.length < recentTotal;
|
|
@@ -6556,6 +7104,13 @@ function apply(ctx) {
|
|
|
6556
7104
|
const awaitAgeSignature = active
|
|
6557
7105
|
.filter((entry) => entry.kind === "awaiting")
|
|
6558
7106
|
.map((entry) => entry.awaitAge ?? "");
|
|
7107
|
+
// job 子卡的任务行时长随 1 秒时钟推进(R-01-023/AC-01):秒桶注入渲染签名,
|
|
7108
|
+
// 与运行卡 totalBusyMs 的逐秒重绘同节奏,不新增时钟。
|
|
7109
|
+
for (const entry of visibleEntries) {
|
|
7110
|
+
if (entry.kind === "job") {
|
|
7111
|
+
entry.jobsAgeSec = Math.floor(Date.now() / 1000);
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
6559
7114
|
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, awaitAgeSignature, densityLevel]);
|
|
6560
7115
|
if (sig === lastSig) return;
|
|
6561
7116
|
const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
|
|
@@ -6909,6 +7464,8 @@ function apply(ctx) {
|
|
|
6909
7464
|
pendingUnsubscribe?.();
|
|
6910
7465
|
acksSource?.close();
|
|
6911
7466
|
acksSource = null;
|
|
7467
|
+
jobsSource?.close();
|
|
7468
|
+
jobsSource = null;
|
|
6912
7469
|
busySource?.close();
|
|
6913
7470
|
busySource = null;
|
|
6914
7471
|
document.removeEventListener("visibilitychange", onBusyVisibilityResume);
|