dsh-activity-pane 0.13.0 → 0.15.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.
@@ -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
 
@@ -150,6 +152,15 @@ function nextDensity(value) {
150
152
  return DENSITY_ORDER[(index + 1) % DENSITY_ORDER.length];
151
153
  }
152
154
 
155
+ /**
156
+ * 档位资源纪律谓词(R-01-025/AC-01):紧凑呈现档下主会话日志窗口不建立或保持。
157
+ * 子代理日志窗口承载标题行可见的模型上下文(R-01-012/AC-17),恒不裁剪;
158
+ * 输入档位经 normalizeDensity 归一,非法值视作中间档即不裁剪(R-01-025/AC-01)。
159
+ */
160
+ function logWindowSuppressed(densityLevel, isSubagent) {
161
+ return normalizeDensity(densityLevel) === "compact" && isSubagent !== true;
162
+ }
163
+
153
164
  function isRecord(value) {
154
165
  return value !== null && typeof value === "object" && !Array.isArray(value);
155
166
  }
@@ -1561,8 +1572,12 @@ function mainTitle(byId, id) {
1561
1572
  * stateAt(进入当前等待行动状态的时刻,R-01-002/AC-14):仅 awaiting 条目携带,与排序键
1562
1573
  * 共用 enterStateAt 单点口径——pending 取 waitingStarts,完成/错误提醒取 completions.lastTurnEnd;
1563
1574
  * 显示侧不回落宿主列表时间——排序键缺失时的回落仅用于排序,显示以 null(不显示)承载。
1575
+ * jobsBySession(R-01-023):快照携带的 `jobsBySession` 任务视图映射——存在在跑后台任务
1576
+ * (status ∈ {running, stopping})的主会话获得第三种自身活动来源:保留在活动区归入运行组、
1577
+ * 完成提醒与错误提醒被抑制(AC-01、AC-02);条目携带 liveJobs(startedAt 升序)供渲染层
1578
+ * 标注数量与构建任务行(R-01-023)。子代理行不派生 liveJobs(后台任务当前仅主会话呈现)。
1564
1579
  */
1565
- function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null) {
1580
+ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null, jobsBySession = null) {
1566
1581
  const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
1567
1582
  const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
1568
1583
  const current = snapshot?.current ?? null;
@@ -1603,16 +1618,20 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
1603
1618
  const running = row.running === true;
1604
1619
  const pending = row.pendingInteraction !== undefined;
1605
1620
  const isSub = hasParent;
1621
+ // 在跑后台任务(R-01-023):主会话的第三种自身活动来源——liveJobs 非空期间
1622
+ // 完成/错误提醒被抑制(AC-02)、会话保持运行态呈现(AC-01)。
1623
+ const liveJobs = isSub ? [] : liveJobsOf(jobsBySession, id);
1624
+ const hasLive = liveJobs.length > 0;
1606
1625
  // 完成确认(R-01-002/AC-03、AC-05、R-01-010/AC-06):未确认的完成提醒按自身活动计入。
1607
- const done = completionReminder(row, completionFor(id, completions), isSub);
1626
+ const done = !hasLive && completionReminder(row, completionFor(id, completions), isSub);
1608
1627
  // 错误提醒(R-01-002/AC-13,C-043):最近回合以错误结束的按自身活动计入。
1609
- const err = errorReminder(row, completionFor(id, completions), isSub);
1628
+ const err = !hasLive && errorReminder(row, completionFor(id, completions), isSub);
1610
1629
  // 子代理完成且没有活动后代时消失;主会话完成后保留为"等待打开";母会话在委托周期保持运行呈现(R-01-003/AC-05)。
1611
- const selfActive = isOwnActiveRow(row, byId);
1630
+ const selfActive = isOwnActiveRow(row, byId) || hasLive;
1612
1631
  const descendantActive = descendantIds.has(String(id));
1613
1632
  const delegating = descendantActive || (delegatingIds instanceof Set && delegatingIds.has(String(id)));
1614
1633
  const show = selfActive || delegating || done || err;
1615
- meta.set(id, { row, running, pending, isSub, show, done, err, descendantActive, delegating, depth: 0 });
1634
+ meta.set(id, { row, running, pending, isSub, show, done, err, descendantActive, delegating, hasLive, liveJobs, depth: 0 });
1616
1635
  }
1617
1636
 
1618
1637
  // 主会话分两组排序(R-01-001/AC-07,键口径契约详见上方 JSDoc):运行中主会话置顶、
@@ -1620,7 +1639,8 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
1620
1639
  // 两组相同时间均回落宿主列表出现顺序,工作区顺序不参与排序。
1621
1640
  const isRunningEntry = (id) => {
1622
1641
  const m = meta.get(id);
1623
- return m !== undefined && !m.pending && (m.running || m.delegating);
1642
+ // 在跑后台任务视同运行组(R-01-023/AC-01):排序键沿用宿主列表时间。
1643
+ return m !== undefined && !m.pending && (m.running || m.delegating || m.hasLive);
1624
1644
  };
1625
1645
  const waitingStartTime = (id) => {
1626
1646
  if (!(waitingStarts instanceof Map)) return null;
@@ -1691,7 +1711,7 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
1691
1711
  ? "subagent"
1692
1712
  : m.pending
1693
1713
  ? "awaiting"
1694
- : m.running || m.delegating
1714
+ : m.running || m.delegating || m.hasLive
1695
1715
  ? "running"
1696
1716
  : "awaiting",
1697
1717
  descendantActive: m.descendantActive,
@@ -1725,9 +1745,40 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
1725
1745
  : doneWait
1726
1746
  ? ROUND_DONE_NOTE
1727
1747
  : undefined,
1748
+ // 在跑后台任务(R-01-023/AC-01):仅 liveJobs 非空时携带(startedAt 升序),
1749
+ // 渲染层据此在标题行标注数量;任务本体以下一线条目呈现(kind: "job")。
1750
+ liveJobs: m.hasLive ? m.liveJobs : undefined,
1751
+ selfRunning: m.row.running === true,
1728
1752
  stateAt,
1729
1753
  questionPreview: m.pending && m.row.pendingInteraction === "question" ? (questionPreview ?? null) : undefined,
1730
1754
  });
1755
+ // 后台任务子条目(R-01-023):与子代理同形的缩进子卡,跟随母会话在其全部
1756
+ // 子代理之前(即母亲条目的直接后继);结束即随 liveJobs 清空而消失(R-01-023/AC-03)。
1757
+ // 复合 id 在去重视野(entries/visited/cardsById)中唯一,且携 jobs 前缀与子代理
1758
+ // 会话 id 空间天然隔离;层级连接线与子代理同规则参与 trackRuns(R-01-003/AC-04),
1759
+ // 仍不参与徽标计数(kind 过滤,R-01-023/AC-04)。
1760
+ if (m.hasLive && !m.isSub) {
1761
+ for (const job of m.liveJobs) {
1762
+ entries.push({
1763
+ id: `job:${id}:${job.id}`,
1764
+ parentId: id,
1765
+ depth: depth + 1,
1766
+ kind: "job",
1767
+ title: job.label,
1768
+ jobKind: job.kind,
1769
+ workspaceTitle: "",
1770
+ workspaceKey: "",
1771
+ model: "",
1772
+ reasoning: "",
1773
+ timeline: [],
1774
+ userPreview: "",
1775
+ agentPreview: "",
1776
+ isCurrent: false,
1777
+ jobStatus: job.status,
1778
+ jobStartedAt: job.startedAt,
1779
+ });
1780
+ }
1781
+ }
1731
1782
  }
1732
1783
  for (const child of childIds.get(id) ?? []) visit(child, depth + 1);
1733
1784
  };
@@ -1737,10 +1788,10 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
1737
1788
  return entries;
1738
1789
  }
1739
1790
  /**
1740
- * 把活动条目压成母会话轨道运行(R-01-003/AC-04):每个拥有可见直属子代理的
1741
- * 母会话一条,记录全部可见直属子代理 id(有序,末位即末级)与子级深度,供
1742
- * 渲染层测量后绘制整条连续轨道与接入横线。条目按 preorder 排列,同一直属
1743
- * 子代理组天然连续。直属性按「条目深度 = 母会话条目深度 + 1」判定(与条目
1791
+ * 把活动条目压成母会话轨道运行(R-01-003/AC-04):每个拥有可见直属子代理或后台
1792
+ * 任务子卡的母会话一条,记录全部可见直属子级 id(有序,末位即末级)与子级深度,
1793
+ * 供渲染层测量后绘制整条连续轨道与接入横线。条目按 preorder 排列,同一直属
1794
+ * 子级组天然连续。直属性按「条目深度 = 母会话条目深度 + 1」判定(与条目
1744
1795
  * 顺序无关);无 id、无母会话条目或非直属的条目一律跳过。
1745
1796
  */
1746
1797
  function trackRuns(entries) {
@@ -1752,7 +1803,8 @@ function trackRuns(entries) {
1752
1803
  const runs = new Map();
1753
1804
  for (const entry of list) {
1754
1805
  if (entry?.id == null || entry?.parentId == null || (entry.depth ?? 0) < 1) continue;
1755
- if (entry.kind !== "subagent") continue;
1806
+ // 子代理与后台任务子卡一视同仁(R-01-003/AC-04):两类直属子级同规则上轨。
1807
+ if (entry.kind !== "subagent" && entry.kind !== "job") continue;
1756
1808
  const pid = String(entry.parentId);
1757
1809
  const parentDepth = depthById.get(pid);
1758
1810
  if (parentDepth === undefined || entry.depth !== parentDepth + 1) continue;
@@ -1830,6 +1882,13 @@ function cardSignature(entries) {
1830
1882
  entry.tokenStats ?? [entry.outputTokens ?? null, entry.inputTokens ?? null, entry.cacheHitPct ?? null, entry.rateTokS ?? null, entry.elapsedMs ?? null],
1831
1883
  // 累计运行时长参与签名(R-01-020):回填/SSE 推送与逐秒推进都要驱动重绘。
1832
1884
  entry.totalBusyMs ?? null,
1885
+ // 在跑后台任务(R-01-023):任务视图随快照推送帧刷新,进入/退出在跑态驱动重绘;
1886
+ // job 子卡状态翻转与秒桶(渲染期注入的任务行时长推进)同入签名。
1887
+ entry.liveJobs ?? null,
1888
+ entry.jobStatus ?? null,
1889
+ // jobKind 不入签名(R-01-023/AC-05):同一 jobId 的工具类型由发起方定死恒定,
1890
+ // 签名分量永不变化;任务视图整体(liveJobs,含 kind)已随推送帧入签名。
1891
+ entry.jobsAgeSec ?? null,
1833
1892
  ]),
1834
1893
  );
1835
1894
  }
@@ -1952,6 +2011,52 @@ function entryErrorNote(completion) {
1952
2011
  return typeof message === "string" && message !== "" ? message : ERROR_NOTE_FALLBACK;
1953
2012
  }
1954
2013
 
2014
+ // ---- 后台任务(R-01-023):在跑活性归一 ----
2015
+
2016
+ /** 在跑后台任务的活性状态全集(R-01-023):stopping 视同在跑——停止请求已发出但
2017
+ * 任务尚未结束,呈现与提醒抑制口径与 running 一致。 */
2018
+ const LIVE_JOB_STATUSES = new Set(["running", "stopping"]);
2019
+
2020
+ /**
2021
+ * 归一会话的在跑后台任务列表(R-01-023/AC-01):取快照 `jobsBySession[id]` 中
2022
+ * status ∈ {running, stopping} 的任务视图,按 startedAt 升序(最早在前)。
2023
+ * jobsBySession 缺失、非记录、条目非记录或字段非法均按无任务/剔除处理,不抛错。
2024
+ * 返回 `{ id, kind, label, status, startedAt }` 的新数组(不泄漏宿主对象引用;
2025
+ * `kind` 为任务发起工具的类型原文,供任务卡工具名行消费,R-01-023/AC-05)。
2026
+ */
2027
+ function liveJobsOf(jobsBySession, id) {
2028
+ if (!isRecord(jobsBySession)) return [];
2029
+ const list = jobsBySession[String(id)];
2030
+ if (!Array.isArray(list)) return [];
2031
+ const live = [];
2032
+ for (const job of list) {
2033
+ if (!isRecord(job)) continue;
2034
+ if (!LIVE_JOB_STATUSES.has(job.status)) continue;
2035
+ live.push({
2036
+ id: typeof job.id === "string" ? job.id : "",
2037
+ kind: typeof job.kind === "string" ? job.kind : "",
2038
+ // label 纯空白视为任务内容不可得(R-01-023/AC-06 的数据源头):不渲染空白行,
2039
+ // 不以占位文本补位;非空白保留原文(不额外 trim,显示层忠实原文)。
2040
+ label: typeof job.label === "string" && job.label.trim() !== "" ? job.label : "",
2041
+ status: job.status,
2042
+ startedAt: Number.isFinite(Number(job.startedAt)) ? Number(job.startedAt) : 0,
2043
+ });
2044
+ }
2045
+ live.sort((a, b) => a.startedAt - b.startedAt);
2046
+ return live;
2047
+ }
2048
+
2049
+ /** 后台任务工具名的友好显示映射(R-01-023/AC-05):bash→Bash、pwsh→PowerShell、
2050
+ * subagent→子代理;未知 kind 原样显示,非字符串或空串视为不可得(返回空串)。
2051
+ * 与 client 渲染层 JOB_STATUS_LABELS(状态词文案)分层:本表居 core 因核心与
2052
+ * 渲染两层共用且需 Node 单测钉住映射,状态词仅渲染层消费。 */
2053
+ const JOB_KIND_LABELS = { bash: "Bash", pwsh: "PowerShell", subagent: "子代理" };
2054
+
2055
+ function jobKindLabel(kind) {
2056
+ if (typeof kind !== "string" || kind === "") return "";
2057
+ return JOB_KIND_LABELS[kind] ?? kind;
2058
+ }
2059
+
1955
2060
  /**
1956
2061
  * 活动区→历史区迁移检测(R-01-010/AC-07):上一帧活动区 id 在本帧离开活动区且出现于
1957
2062
  * 历史区即判定为一次迁移;彻底消失(归档、不再被会话服务列出)不判定。prevActiveIds 为上一帧
@@ -2299,7 +2404,7 @@ function createSessionEventSerializer() {
2299
2404
  * 也不入历史区。历史区不再按时间窗口或条数截断;turnEnds(id → 已知回合结束时刻)驱动
2300
2405
  * activityAt 精化(R-01-010、R-01-019)。
2301
2406
  */
2302
- function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedIds = [], completions = null, delegatingIds = null, turnEnds = null) {
2407
+ function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedIds = [], completions = null, delegatingIds = null, turnEnds = null, jobsBySession = null) {
2303
2408
  const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
2304
2409
  const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
2305
2410
  const current = snapshot?.current ?? null;
@@ -2317,6 +2422,8 @@ function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedId
2317
2422
  if (completionReminder(row, completionFor(id, completions), false)) continue; // 完成确认中,留在活动区
2318
2423
  if (errorReminder(row, completionFor(id, completions), false)) continue; // 错误提醒中,留在活动区
2319
2424
  if (delegatingIds instanceof Set && delegatingIds.has(String(id))) continue; // 委托周期中(含耗尽空窗),留在活动区
2425
+ // 在跑后台任务(R-01-023/AC-01、AC-03):留在活动区,不落入最近历史。
2426
+ if (liveJobsOf(jobsBySession, id).length > 0) continue;
2320
2427
  if (isActiveRow(row, byId, activeIds)) continue;
2321
2428
  const updatedAt = Number(row.updatedAt);
2322
2429
  if (!Number.isFinite(updatedAt)) continue;
@@ -2919,7 +3026,8 @@ const CSS = `
2919
3026
  .dap-token-stats,
2920
3027
  .dap-foot,
2921
3028
  .dap-history-line,
2922
- .dap-note
3029
+ .dap-note,
3030
+ .dap-job-content
2923
3031
  ) {
2924
3032
  display: none;
2925
3033
  }
@@ -3077,7 +3185,7 @@ const CSS = `
3077
3185
  gap: 4px;
3078
3186
  cursor: pointer;
3079
3187
  }
3080
- /* 子代理层级连接线(R-01-003/AC-04):竖轨与横线全部由轨道层整体绘制——
3188
+ /* 子代理与后台任务子卡层级连接线(R-01-003/AC-04):竖轨与横线全部由轨道层整体绘制——
3081
3189
  syncTracks 测量各卡片浮点矩形,trackBoxes 统一取整到 CSS 像素后写入:
3082
3190
  每个母会话一条连续竖轨 .dap-conn-track(母会话底缘 → 末级子卡中心,
3083
3191
  含收口行),每个子卡一条横线 .dap-conn-stub(竖轨右缘 → 子卡左缘)。
@@ -3361,6 +3469,52 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
3361
3469
  }
3362
3470
  [data-dsh-activity-pane] .dap-token-time { flex: none; }
3363
3471
  [data-dsh-activity-pane] .dap-token-stats[hidden] { display: none; }
3472
+ /* 后台任务数量注(R-01-023/AC-01):母卡标题行内「后台 ×N」小注,弱化色调不抢标题。 */
3473
+ [data-dsh-activity-pane] .dap-jobs-chip {
3474
+ flex: none; font-size: 10px; line-height: 15px;
3475
+ color: color-mix(in srgb, currentColor 55%, transparent);
3476
+ font-variant-numeric: tabular-nums;
3477
+ }
3478
+ [data-dsh-activity-pane] .dap-jobs-chip[hidden] { display: none; }
3479
+ /* 后台任务子卡(R-01-023/AC-05、AC-07):与子代理卡同构的两行卡面,底色在子代理卡
3480
+ 底色上轻染任务状态点同族的蓝以相互可辨;状态点色随任务状态翻转。
3481
+ 子卡为纯展示(R-01-023/AC-08):默认光标,悬停不提供可点反馈。 */
3482
+ [data-dsh-activity-pane] .dap-card[data-kind="job"] {
3483
+ padding: 6px 10px;
3484
+ border-radius: 12px;
3485
+ background: color-mix(in srgb, #65a0ff 8%, rgba(25, 27, 32, 0.95));
3486
+ cursor: default;
3487
+ }
3488
+ [data-dsh-activity-pane] .dap-card[data-kind="job"]:hover {
3489
+ border-color: rgba(255, 255, 255, 0.13);
3490
+ filter: none;
3491
+ }
3492
+ /* 任务卡行 1(R-01-023/AC-05):状态点 + 工具名称 + 右缘随时钟时长;工具名不可得时
3493
+ 文本段隐藏,仅保留状态点与时长。 */
3494
+ [data-dsh-activity-pane] .dap-card[data-kind="job"] .dap-job-kind {
3495
+ flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
3496
+ font-size: 11px; line-height: 15px;
3497
+ }
3498
+ [data-dsh-activity-pane] .dap-job-dot {
3499
+ flex: none; width: 6px; height: 6px; border-radius: 50%;
3500
+ background: #65a0ff;
3501
+ }
3502
+ [data-dsh-activity-pane] .dap-job-dot[data-status="stopping"] { background: #f5a524; }
3503
+ [data-dsh-activity-pane] .dap-job-elapsed {
3504
+ flex: none; margin-left: auto; font-size: 10px; line-height: 15px;
3505
+ color: color-mix(in srgb, currentColor 55%, transparent); font-variant-numeric: tabular-nums;
3506
+ }
3507
+ /* 任务内容行(R-01-023/AC-05、AC-06):mono 原文单行省略,原生 tooltip 承载完整原文;
3508
+ 内容不可得时整行 hidden([hidden] 显式覆盖 display:flex),紧凑档经密度规则隐藏。 */
3509
+ [data-dsh-activity-pane] .dap-job-content {
3510
+ display: flex; align-items: baseline; min-width: 0;
3511
+ margin-top: 1px;
3512
+ }
3513
+ [data-dsh-activity-pane] .dap-job-content[hidden] { display: none; }
3514
+ [data-dsh-activity-pane] .dap-card[data-kind="job"] .dap-job-label {
3515
+ flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
3516
+ font-family: var(--dsh-font-mono, monospace); font-size: 11px; line-height: 15px;
3517
+ }
3364
3518
  [data-dsh-activity-pane] .dap-history-line {
3365
3519
  display: flex; align-items: center; gap: 4px; height: 15px;
3366
3520
  min-width: 0; overflow: hidden; white-space: nowrap;
@@ -3670,6 +3824,15 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card:hover {
3670
3824
  body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="subagent"] {
3671
3825
  background: var(--dsw-specific-sidebar-fill, rgb(249, 250, 251));
3672
3826
  }
3827
+ /* 后台任务子卡浅色主题与子代理卡同源(R-01-023):淡侧栏填充底,轻染同族蓝与子代理卡区分(R-01-023/AC-07)。 */
3828
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="job"] {
3829
+ background: color-mix(in srgb, #65a0ff 8%, var(--dsw-specific-sidebar-fill, rgb(249, 250, 251)));
3830
+ }
3831
+ /* 纯展示子卡(R-01-023/AC-08):浅色主题同样不提供悬停反馈(回归基态描边)。 */
3832
+ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="job"]:hover {
3833
+ border-color: var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
3834
+ filter: none;
3835
+ }
3673
3836
  body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-card[data-kind="recent"] {
3674
3837
  /* 暗于活动卡的 --dsw-alias-bg-layer-2 纯白、深于窗格底色(R-01-013/AC-10、AC-11)。 */
3675
3838
  background: rgb(243, 244, 246);
@@ -3991,6 +4154,8 @@ function apply(ctx) {
3991
4154
  return promise;
3992
4155
  }
3993
4156
  const sessionOpenLoads = new Map();
4157
+ /** 子代理目录按需拉取记账(T-151):parentId → 已触发;失败不热重试(行为有界)。 */
4158
+ const subagentCatalogRequests = new Set();
3994
4159
 
3995
4160
  const style = document.createElement("style");
3996
4161
  style.id = STYLE_ID;
@@ -4153,15 +4318,17 @@ function apply(ctx) {
4153
4318
  }
4154
4319
  }
4155
4320
 
4156
- /** 回到前台(含 bfcache 还原)时 ack 通道自愈:重建 SSE,借连接全量快照收敛。 */
4157
- function resumeAcksChannel() {
4158
- if (!disposed) connectAcksStream();
4321
+ /** 回到前台(含 bfcache 还原)时 ack 通道自愈:重建 SSE(连接即发全量,状态必然收敛)。 */
4322
+ function resumePushChannels() {
4323
+ if (!disposed) {
4324
+ connectAcksStream();
4325
+ }
4159
4326
  }
4160
4327
  const onVisibilityResume = () => {
4161
- if (document.visibilityState === "visible") resumeAcksChannel();
4328
+ if (document.visibilityState === "visible") resumePushChannels();
4162
4329
  };
4163
4330
  const onPageShow = (event) => {
4164
- if (event?.persisted === true) resumeAcksChannel();
4331
+ if (event?.persisted === true) resumePushChannels();
4165
4332
  };
4166
4333
  document.addEventListener("visibilitychange", onVisibilityResume);
4167
4334
  window.addEventListener("pageshow", onPageShow);
@@ -4324,20 +4491,34 @@ function apply(ctx) {
4324
4491
  throw response?.error ?? new Error("remote request failed");
4325
4492
  }
4326
4493
 
4494
+ /** 母会话子代理目录条目查找(T-151 单源):open 地址与深翻分页地址共用同一取法,
4495
+ * 防条目形状或键名演化时两处漂移。 */
4496
+ function subagentCatalogEntry(subagentsByParent, parentId, id) {
4497
+ const entries = subagentsByParent?.[String(parentId)]?.entries;
4498
+ return Array.isArray(entries)
4499
+ ? entries.find((candidate) => String(candidate?.id) === String(id))
4500
+ : undefined;
4501
+ }
4502
+
4327
4503
  /** 会话日志分页地址(R-01-012):主会话 `{kind:"session", sessionId}`;子代理
4328
4504
  * `{kind:"subagent", parentSessionId, childSessionId, mode}`——母会话 id 兼容
4329
- * `parentSessionId` / `parentId` 两种条目键名,mode 无条目标注时按一次性子代理
4330
- * 处理、读取失败由 pagedHistoryEvents 以 null 收敛为空白详情(R-01-013)。 */
4331
- function sessionPageAddress(id, byId) {
4505
+ * `parentSessionId` / `parentId` 两种条目键名。mode 必须与子代理描述符一致,
4506
+ * 不一致被宿主以 subagent/unauthorized 拒绝(T-151):优先取母会话目录条目的
4507
+ * mode,目录条目缺失(未加载、在途空窗或无该子条目)时回退行 `continuable`
4508
+ * 启发(读取失败由 pagedHistoryEvents 以 null 收敛为空白详情,R-01-013)。 */
4509
+ function sessionPageAddress(id, byId, subagentsByParent = null) {
4332
4510
  const row = byId[id] ?? {};
4333
4511
  if (row?.origin === "subagent") {
4334
4512
  const parentId = row.parentSessionId ?? row.parentId;
4335
4513
  if (parentId !== undefined && parentId !== null) {
4514
+ const entry = subagentCatalogEntry(subagentsByParent, String(parentId), id);
4336
4515
  return {
4337
4516
  kind: "subagent",
4338
4517
  parentSessionId: String(parentId),
4339
4518
  childSessionId: String(id),
4340
- mode: row.continuable === true ? "continuable" : "one-shot",
4519
+ mode: typeof entry?.mode === "string" && entry.mode !== ""
4520
+ ? entry.mode
4521
+ : row.continuable === true ? "continuable" : "one-shot",
4341
4522
  };
4342
4523
  }
4343
4524
  }
@@ -4433,7 +4614,8 @@ function apply(ctx) {
4433
4614
  const sessionRemote = ctx.get("remote.session") ?? null;
4434
4615
  if (!sessionRemote) return;
4435
4616
  ensureCatalogGroups(sessionRemote);
4436
- const byId = getSnapshot(sessions, "list")?.byId ?? {};
4617
+ const listSnap = getSnapshot(sessions, "list");
4618
+ const byId = listSnap?.byId ?? {};
4437
4619
  const historyPromises = [];
4438
4620
  for (const id of ids) {
4439
4621
  const detail = sessionDetailsById.get(id) ?? {};
@@ -4445,7 +4627,10 @@ function apply(ctx) {
4445
4627
  // 选择),订阅与一次性 load 一并触发;子代理的目录不可用(宿主以 agent-busy
4446
4628
  // 拒绝其模型 RPC),不建立订阅。e2e 接缝把订阅与 load 整体延后,使模型上下文
4447
4629
  // 严格晚于时间线呈现(R-01-014/AC-03 渐进语义可观察)。
4448
- if (!subagent) {
4630
+ // 档位资源纪律(R-01-025/AC-06):紧凑档主会话的模型区域在头部行内、整体
4631
+ // 隐藏——模型上下文无可见消费者,不订阅不 load;切回更高档位后经既有
4632
+ // 一次性读取语义补齐。logWindowSuppressed 单点判定。
4633
+ if (!subagent && !logWindowSuppressedFor(id, byId)) {
4449
4634
  if (e2eModelDelayMs === 0) {
4450
4635
  subscribeModelDirectory(id, detail);
4451
4636
  loadDirectoryOnce(id);
@@ -4464,6 +4649,10 @@ function apply(ctx) {
4464
4649
  const lastSeq = Number(windowEntries.at(-1)?.event?.seq);
4465
4650
  const deepReadNeeded =
4466
4651
  (previewFallbackIds.has(id) || durationFallbackIds.has(id) || (subagent && subagentModelReadIds.has(id))) &&
4652
+ // 档位资源纪律(R-01-025/AC-03):紧凑档主会话不发起日志分页深翻——
4653
+ // 预览行与最近回合耗时在该档位均不可见,最近历史排序以既有可用时刻
4654
+ // 承载(R-01-010/AC-09 档位限定);子代理模型溯源读取不受影响。
4655
+ (subagent || !logWindowSuppressed(densityLevel, subagent)) &&
4467
4656
  detail.log?.hasMore === true &&
4468
4657
  Number.isFinite(lastSeq) &&
4469
4658
  detail.historyDeepReadDone !== true;
@@ -4473,11 +4662,15 @@ function apply(ctx) {
4473
4662
  if (deepReadNeeded && !historyLoads.has(id)) {
4474
4663
  detail.previewFallbackLoaded = true;
4475
4664
  detail.durationFallbackLoaded = true;
4476
- const address = sessionPageAddress(id, byId);
4665
+ const address = sessionPageAddress(id, byId, listSnap?.subagentsByParent ?? null);
4477
4666
  const promise = enqueueDetailLoad(() => Promise.resolve()
4478
4667
  .then(async () => {
4479
4668
  const { events, error } = await pagedHistoryEvents({
4480
4669
  fetchPage: async (beforeSeq) => {
4670
+ // 档位资源纪律(R-01-025/AC-03):分页响应点用时重读档位——
4671
+ // 切入紧凑档后,在途深翻不再发起下一页;中止时已取页不落地、
4672
+ // 保持既有详情,切回后重新入队重取。子代理模型溯源读取不受影响。
4673
+ if (logWindowSuppressed(densityLevel, subagent)) return null;
4481
4674
  const value = remoteValue(
4482
4675
  await sessionRemote.page({ address, throughSeq: lastSeq, beforeSeq, maxMessages: 50 }),
4483
4676
  );
@@ -4485,6 +4678,10 @@ function apply(ctx) {
4485
4678
  return { events: Array.isArray(value.records) ? value.records : [], hasMore: value.hasMore === true };
4486
4679
  },
4487
4680
  });
4681
+ // 切档中止的在途深翻不落地、不标记完成——historyDeepReadDone 无复位
4682
+ // 路径,中止即置位会让切回高档位后的预览/耗时补齐永久失效;中止时
4683
+ // 保持既有详情不变,切回后经 deepReadNeeded 重新入队(R-01-025/AC-01)。
4684
+ if (logWindowSuppressed(densityLevel, subagent)) return;
4488
4685
  if (error) detail.historyError = error instanceof Error ? error.message : String(error);
4489
4686
  detail.history = events;
4490
4687
  detail.historyDeepReadDone = true;
@@ -4506,6 +4703,44 @@ function apply(ctx) {
4506
4703
  }
4507
4704
  }
4508
4705
 
4706
+ /** 档位资源纪律单点判定(R-01-025):档位谓词与子代理判定的唯一组合。
4707
+ * byId 缺省时读当帧列表快照;调用方已持有快照时传入以免重复读取。 */
4708
+ function logWindowSuppressedFor(id, byId = getSnapshot(sessions, "list")?.byId ?? {}) {
4709
+ return logWindowSuppressed(densityLevel, isSubagentRow(byId[id], byId));
4710
+ }
4711
+
4712
+ /** 档位资源纪律拆窗(R-01-025/AC-01):对非当前主会话的日志窗口订阅先
4713
+ * unsubscribe 再除名并清除窗口引用,并 dispose 会话运行时的 live 源
4714
+ * (openState 归 cold,底层 session/follow 流终止)——订阅回调移除不终止
4715
+ * 共享传输流,dispose 才真正停带宽。当前会话的窗口由外壳会话视图持有,
4716
+ * 不 dispose、不拆订阅(由本函数在渲染期持续执法:其切走后的下一轮渲染
4717
+ * 照常释放);当前会话不可判定(列表快照未就绪)时整轮跳过,不误释放。
4718
+ * 子代理日志窗口承载标题行可见的模型上下文,保留。切回更高档位时经既有
4719
+ * 渐进路径(captureSessionLog 重订阅 + session.open 重新水合)就绪,
4720
+ * 不新增恢复机制。 */
4721
+ function enforceDensityDataDiscipline() {
4722
+ if (densityLevel !== "compact") return;
4723
+ const listSnap = getSnapshot(sessions, "list");
4724
+ if (listSnap?.current == null) return;
4725
+ for (const [id, unsubscribe] of [...logSourceSubs]) {
4726
+ if (!logWindowSuppressedFor(id, listSnap?.byId ?? {})) continue;
4727
+ if (String(id) === String(listSnap.current)) continue;
4728
+ try {
4729
+ unsubscribe?.();
4730
+ } catch {
4731
+ /* 监听器已失效:除名即可 */
4732
+ }
4733
+ logSourceSubs.delete(id);
4734
+ const detail = sessionDetailsById.get(id) ?? null;
4735
+ if (detail !== null) detail.log = null;
4736
+ try {
4737
+ sessions?.binding?.(id)?.session?.dispose?.();
4738
+ } catch {
4739
+ /* dispose 不可用:订阅已断即达成本档位纪律的可用部分 */
4740
+ }
4741
+ }
4742
+ }
4743
+
4509
4744
  /** 日志页落地后的详情派生(R-01-012、R-01-017):时间线折叠、消息预览与子代理模型上下文。
4510
4745
  * 子代理沿用溯源链(assistant/message source.model + 请求头 effort,T-122);主会话模型
4511
4746
  * 不经日志提取(目录链订阅 + 一次性 load 承载,见 installServiceSubscriptions 注)。
@@ -4543,6 +4778,11 @@ function apply(ctx) {
4543
4778
  session = null;
4544
4779
  }
4545
4780
  if (session === null) return;
4781
+ // 档位资源纪律(R-01-025/AC-01):紧凑档主会话不建立或保持会话日志窗口——
4782
+ // 不订阅事件流、不发起 open 水合、不读窗口快照。判定在函数入口用时重读,
4783
+ // 切档在途的回调与重试不落地新窗口。子代理日志窗口承载标题行可见的
4784
+ // 模型上下文(R-01-012/AC-17),恒不裁剪(单点判定见 logWindowSuppressedFor)。
4785
+ if (logWindowSuppressedFor(id)) return;
4546
4786
  // 日志窗口无效化订阅(事件驱动,不构成轮询,R-02-004):等待/历史卡无会话状态
4547
4787
  // 订阅(syncLiveness 只订阅运行中),窗口推进(回合收尾、流式尾)必须经此回调
4548
4788
  // 重读快照并重绘;否则窗口更新只能靠渲染期重读兜底——渲染一旦静默(一次性
@@ -4567,6 +4807,8 @@ function apply(ctx) {
4567
4807
  // 不可知,只以 openState 把关会对已水合会话逐帧重发 open,settle→delete→重发
4568
4808
  // 的在途翻转同样驱动加载指示抖动(docsim 卡闪烁根因之一)。
4569
4809
  if (!detail.log && detail.snapshot?.openState !== "open" && !sessionOpenLoads.has(id) && typeof session.open === "function") {
4810
+ // 子代理事件流仅接受持久父地址:open 前先安装(T-151),否则宿主拒绝、窗口永不水合。
4811
+ ensureSubagentAddress(id, session);
4570
4812
  const opening = Promise.resolve(session.open()).catch(() => {});
4571
4813
  sessionOpenLoads.set(id, opening);
4572
4814
  opening.finally(() => {
@@ -4606,6 +4848,53 @@ function apply(ctx) {
4606
4848
  queueSync();
4607
4849
  }
4608
4850
 
4851
+ /** 触发母会话子代理目录单发读取(T-151):目录条目是地址 mode 的唯一可靠来源,
4852
+ * 未加载时先拉取,目录随下一轮快照到达;每次拉取经原生 sessions.refreshSubagents
4853
+ * 走既有 remote 通道,单父会话单飞、不重试,不构成轮询(R-02-004)。 */
4854
+ function requestSubagentCatalog(parentId) {
4855
+ if (typeof sessions?.refreshSubagents !== "function") return;
4856
+ if (subagentCatalogRequests.has(parentId)) return;
4857
+ subagentCatalogRequests.add(parentId);
4858
+ try {
4859
+ Promise.resolve(sessions?.refreshSubagents?.(parentId))
4860
+ .catch(() => {})
4861
+ .finally(() => {
4862
+ if (!disposed) queueSync();
4863
+ });
4864
+ } catch {
4865
+ subagentCatalogRequests.delete(parentId);
4866
+ }
4867
+ }
4868
+
4869
+ /** 子代理会话事件流打开前置(T-151 缺陷修复):dsh 0.1.5 起宿主按地址校验会话事件
4870
+ * 流路由——子代理会话仅接受持久父地址,且地址 mode 必须与其描述符一致(不一致被
4871
+ * subagent/unauthorized 拒绝),持久地址仅经原生导航留存。mode 唯一可靠来源是母
4872
+ * 会话的子代理目录条目:在册即经 configureSubagent 安装(与原生 selectSubagent
4873
+ * 同构,但不切换当前会话);目录未加载时先单发拉取并跳过本轮安装,避免装入必被
4874
+ * 拒绝的错误 mode 地址;目录拉取失败沿用既有空白详情路径(深翻以 null 收敛),
4875
+ * 不阻断其余卡片。非子代理行或会话对象缺 configureSubagent 时为无操作。 */
4876
+ function ensureSubagentAddress(id, session) {
4877
+ const listSnap = getSnapshot(sessions, "list");
4878
+ const byId = listSnap?.byId ?? {};
4879
+ if (!isSubagentRow(byId[id], byId)) return;
4880
+ if (typeof session?.configureSubagent !== "function") return;
4881
+ const parentId = String(byId[id].parentSessionId ?? byId[id].parentId);
4882
+ const catalog = isRecord(listSnap?.subagentsByParent) ? listSnap.subagentsByParent[parentId] : null;
4883
+ const entry = subagentCatalogEntry(listSnap?.subagentsByParent ?? null, parentId, id);
4884
+ if (!isRecord(entry) || typeof entry.mode !== "string" || entry.mode === "") {
4885
+ requestSubagentCatalog(parentId);
4886
+ return;
4887
+ }
4888
+ try {
4889
+ session.configureSubagent(
4890
+ { kind: "subagent", parentSessionId: parentId, childSessionId: String(id), mode: entry.mode },
4891
+ typeof catalog?.parentAvailable === "boolean" ? catalog.parentAvailable : undefined,
4892
+ );
4893
+ } catch {
4894
+ // 安装失败沿用既有空白详情路径,不阻断其余卡片。
4895
+ }
4896
+ }
4897
+
4609
4898
  /** 部署级模型目录一次性读取(R-01-012/AC-01):dsh 0.1.5 起 per-session models
4610
4899
  * RPC 移除,目录经 `remote.session.modelCatalog()` 无参数读取;成功即缓存不再
4611
4900
  * 重发,失败留待下轮详情读取重试。 */
@@ -4803,6 +5092,9 @@ function apply(ctx) {
4803
5092
  const anchorTop = currentCard ? currentCard.getBoundingClientRect().top - viewportTop : null;
4804
5093
  densityLevel = nextDensity(densityLevel);
4805
5094
  writeStoredDensity(densityLevel);
5095
+ // 档位资源纪律(R-01-025/AC-01):进入紧凑档即拆主会话日志窗口订阅;
5096
+ // captureSessionLog 入口门控阻止在途回调重建。
5097
+ enforceDensityDataDiscipline();
4806
5098
  applyDensity();
4807
5099
  // 档位已纳入渲染签名:触发一轮同步让时间线按新档位以 lastOnly 重建;
4808
5100
  // 锚定补偿在本轮渲染提交后执行,此时量测才含新行高
@@ -5029,8 +5321,17 @@ function apply(ctx) {
5029
5321
  foot.append(awaitHead, noteRow);
5030
5322
  return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];
5031
5323
  }
5324
+ if (kind === "job") {
5325
+ // 后台任务子卡(R-01-023/AC-05):行 1 = 状态点 + 工具名称 + 随时钟时长;
5326
+ // 行 2 = 任务内容原文(mono 省略 + tooltip)。
5327
+ const row = makeEl("div", "dap-row");
5328
+ row.append(makeEl("span", "dap-job-dot"), makeEl("span", "dap-job-kind"), makeEl("span", "dap-job-elapsed"));
5329
+ const content = makeEl("div", "dap-job-content");
5330
+ content.append(makeEl("span", "dap-job-label"));
5331
+ return [row, content];
5332
+ }
5032
5333
  const row = makeEl("div", "dap-row");
5033
- row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-total-time"));
5334
+ row.append(makeEl("span", "dap-dot"), makeEl("span", "dap-title"), makeEl("span", "dap-jobs-chip"), makeEl("span", "dap-total-time"));
5034
5335
  const progressRow = makeProgressRow();
5035
5336
  return [head, row, makeEl("div", "dap-trace"), progressRow, makeStatsRow()];
5036
5337
  }
@@ -5511,6 +5812,60 @@ function apply(ctx) {
5511
5812
  renderTrace(container, entry.timeline, { lastOnly });
5512
5813
  }
5513
5814
 
5815
+ /** 后台任务状态的两字中文短语(R-01-023 呈现):与原生 ui-jobs 状态词同口径。 */
5816
+ const JOB_STATUS_LABELS = { running: "运行中", stopping: "停止中", completed: "已完成", killed: "已终止", failed: "失败" };
5817
+
5818
+ /** 后台任务数量注(R-01-023/AC-01):母卡标题行内 `后台 ×N` 小注;任务本体以 job
5819
+ * 子卡呈现(R-01-023),本注仅承载数量语义。无 liveJobs 时隐藏节点。 */
5820
+ function renderJobsChip(el, entry) {
5821
+ const chip = el.querySelector(".dap-jobs-chip");
5822
+ if (chip === null) return;
5823
+ const count = Array.isArray(entry.liveJobs) ? entry.liveJobs.length : 0;
5824
+ const text = count > 0 ? `后台 ×${count}` : "";
5825
+ if (chip.textContent !== text) chip.textContent = text;
5826
+ const hidden = count === 0;
5827
+ if (chip.hidden !== hidden) chip.hidden = hidden;
5828
+ }
5829
+
5830
+ /** 后台任务子卡渲染(R-01-023/AC-01、AC-05):状态点着色、标题与随时钟推进的
5831
+ * 已运行时长。任务消失由条目派生侧收卡(随 liveJobs 清空整个条目消失),
5832
+ * 此处只呈现当帧状态。 */
5833
+ function renderJobCardInto(el, entry) {
5834
+ const dot = el.querySelector(".dap-job-dot");
5835
+ if (dot !== null && dot.dataset.status !== entry.jobStatus) dot.dataset.status = entry.jobStatus;
5836
+ // 工具名称(R-01-023/AC-05):核心友好映射;不可得时隐藏文本段,仅保留状态点与时长。
5837
+ const kindEl = el.querySelector(".dap-job-kind");
5838
+ if (kindEl !== null) {
5839
+ const kindText = jobKindLabel(entry.jobKind);
5840
+ if (kindEl.textContent !== kindText) kindEl.textContent = kindText;
5841
+ const kindHidden = kindText === "";
5842
+ if (kindEl.hidden !== kindHidden) kindEl.hidden = kindHidden;
5843
+ }
5844
+ // 任务内容行(R-01-023/AC-05、AC-06):mono 原文单行省略,完整原文以原生 tooltip
5845
+ // 显示(tooltip 归内容行);内容不可得时整行隐藏,不补空白或占位。
5846
+ const contentRow = el.querySelector(".dap-job-content");
5847
+ const labelText = String(entry.title ?? "");
5848
+ if (contentRow !== null) {
5849
+ const contentHidden = labelText === "";
5850
+ if (contentRow.hidden !== contentHidden) contentRow.hidden = contentHidden;
5851
+ if (!contentHidden) {
5852
+ const label = contentRow.querySelector(".dap-job-label");
5853
+ if (label !== null) {
5854
+ if (label.textContent !== labelText) label.textContent = labelText;
5855
+ if (label.title !== labelText) label.title = labelText;
5856
+ }
5857
+ }
5858
+ }
5859
+ const elapsed = el.querySelector(".dap-job-elapsed");
5860
+ if (elapsed !== null) {
5861
+ const elapsedText =
5862
+ Number.isFinite(entry.jobStartedAt) && entry.jobStartedAt > 0
5863
+ ? fmtElapsedMs(Math.max(0, Date.now() - entry.jobStartedAt))
5864
+ : "";
5865
+ if (elapsed.textContent !== elapsedText) elapsed.textContent = elapsedText;
5866
+ }
5867
+ }
5868
+
5514
5869
  function renderCardInto(el, entry, colorByWorkspace) {
5515
5870
  const workspaceLabel = el.querySelector(".dap-workspace");
5516
5871
  if (workspaceLabel !== null) {
@@ -5624,19 +5979,43 @@ function apply(ctx) {
5624
5979
  }
5625
5980
 
5626
5981
  if (entry.kind === "running") {
5627
- renderProgressRow(el, entry.progress);
5982
+ // 仅在跑后台任务(R-01-023/AC-01):呈现冻结——隐藏进度行(进度/条纹为回合运行
5983
+ // 语义,不冒充回合执行),时间线保留最后已知状态;任务本体以 job 子卡呈现。
5984
+ const jobsOnly = Array.isArray(entry.liveJobs) && entry.liveJobs.length > 0 && entry.selfRunning !== true;
5985
+ renderJobsChip(el, entry);
5986
+ const progressRow = el.querySelector(".dap-progress");
5987
+ // 档位资源纪律(R-01-025/AC-05):进度行在中间/紧凑档被 CSS 隐藏,不写入;
5988
+ // 切回完整档后随档位签名重渲染恢复。
5989
+ if (progressRow !== null && densityLevel === "full") {
5990
+ if (jobsOnly) {
5991
+ if (!progressRow.hidden) progressRow.hidden = true;
5992
+ } else {
5993
+ renderProgressRow(el, entry.progress);
5994
+ if (progressRow.hidden) progressRow.hidden = false;
5995
+ }
5996
+ }
5628
5997
  const traceContainer = el.querySelector(".dap-trace");
5629
- if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
5630
- renderTokenStats(el, entry);
5998
+ // 档位资源纪律(R-01-025/AC-05):紧凑档不构建被隐藏的时间线内容;
5999
+ // 中间档 lastOnly 单行语义不变(R-01-021/AC-08)。切回后随档位签名重渲染恢复。
6000
+ if (traceContainer !== null && densityLevel !== "compact") renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
6001
+ if (densityLevel === "full") renderTokenStats(el, entry);
6002
+ return;
6003
+ }
6004
+
6005
+ if (entry.kind === "job") {
6006
+ renderJobCardInto(el, entry);
5631
6007
  return;
5632
6008
  }
5633
6009
 
5634
6010
  if (entry.kind === "subagent") {
5635
6011
  const traceContainer = el.querySelector(".dap-subtrace");
5636
- if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: true });
6012
+ // 档位资源纪律(R-01-025/AC-05):紧凑档不构建被隐藏的子卡时间线内容;
6013
+ // 标题行内联模型上下文(R-01-012/AC-17)不受影响。
6014
+ if (traceContainer !== null && densityLevel !== "compact") renderTimelineArea(traceContainer, entry, { lastOnly: true });
5637
6015
  // 运行中呈现与运行卡同构的进度行;锚点空闲(暂停等待)时整行隐藏(R-01-009/AC-15)。
5638
6016
  const progressRow = el.querySelector(".dap-progress");
5639
- if (progressRow !== null) {
6017
+ // 档位资源纪律(R-01-025/AC-05):进度行与统计行在中间/紧凑档不写入。
6018
+ if (progressRow !== null && densityLevel === "full") {
5640
6019
  if (Number.isFinite(entry.progress)) {
5641
6020
  renderProgressRow(el, entry.progress);
5642
6021
  if (progressRow.hidden) progressRow.hidden = false;
@@ -5644,7 +6023,7 @@ function apply(ctx) {
5644
6023
  progressRow.hidden = true;
5645
6024
  }
5646
6025
  }
5647
- renderTokenStats(el, entry);
6026
+ if (densityLevel === "full") renderTokenStats(el, entry);
5648
6027
  return;
5649
6028
  }
5650
6029
 
@@ -5665,14 +6044,18 @@ function apply(ctx) {
5665
6044
  restoreTextField(text, previews[i]);
5666
6045
  }
5667
6046
  }
5668
- renderTokenStats(el, entry);
6047
+ // 档位资源纪律(R-01-025/AC-05):最近卡统计行在中间/紧凑档不写入。
6048
+ if (densityLevel === "full") renderTokenStats(el, entry);
5669
6049
  }
5670
6050
 
5671
6051
  if (entry.kind === "awaiting") {
5672
6052
  const traceContainer = el.querySelector(".dap-trace");
5673
- if (traceContainer !== null) renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
6053
+ // 档位资源纪律(R-01-025/AC-05):紧凑档不构建被隐藏的时间线内容;
6054
+ // 中间档 lastOnly 单行语义不变(R-01-021/AC-08)。
6055
+ if (traceContainer !== null && densityLevel !== "compact") renderTimelineArea(traceContainer, entry, { lastOnly: densityLevel === "medium" });
5674
6056
  removeAwaitingHeadDuration(el);
5675
- renderTokenStats(el, entry);
6057
+ // 档位资源纪律(R-01-025/AC-05):等待卡统计行在中间/紧凑档不写入。
6058
+ if (densityLevel === "full") renderTokenStats(el, entry);
5676
6059
  const confirm = el.querySelector(".dap-confirm");
5677
6060
  if (confirm !== null) {
5678
6061
  // 激活锚点:只在结构重建时绑一次(卡片按 id 复用,kind 变化会重建骨架)。
@@ -5741,10 +6124,20 @@ function apply(ctx) {
5741
6124
  } catch {
5742
6125
  continue; // 订阅失败:本次跳过,下次渲染重试
5743
6126
  }
5744
- try {
5745
- opening = session.open?.();
5746
- } catch {
5747
- opening = null;
6127
+ // 子代理事件流仅接受持久父地址:open 前先安装(T-151),否则宿主拒绝、窗口永不水合。
6128
+ ensureSubagentAddress(id, session);
6129
+ // 档位资源纪律(R-01-025/AC-01):紧凑档主会话不发起 open 水合——轮内状态
6130
+ // 订阅(本函数职责,R-02-004)保留,liveness 记账与正常路径共用下方代码,
6131
+ // 日志窗口水合在切回更高档位后经渲染期重试重建。子代理模型上下文依赖其
6132
+ // 窗口,不裁剪(单点判定见 logWindowSuppressedFor)。
6133
+ const livenessSuppressed = logWindowSuppressedFor(id);
6134
+ let opening = null;
6135
+ if (!livenessSuppressed) {
6136
+ try {
6137
+ opening = session.open?.();
6138
+ } catch {
6139
+ opening = null;
6140
+ }
5748
6141
  }
5749
6142
  if (opening && typeof opening.then === "function") {
5750
6143
  const tracked = Promise.resolve(opening).then(
@@ -5963,7 +6356,11 @@ function apply(ctx) {
5963
6356
  if (rec === undefined) {
5964
6357
  const el = document.createElement("div");
5965
6358
  el.className = CARD_CLASS;
5966
- const unbind = bindCardActivation(el, (sessionId) => {
6359
+ // 后台任务子卡为纯展示(R-01-023/AC-08):不绑激活监听,job 复合 id 亦非会话
6360
+ // id,不存在可跳转目标。job 复合 id 与会话 id 空间隔离(`job:` 前缀),reuseMap
6361
+ // 按条目 id 复用,同一 card 元素不会跨 kind 复用——创建时按 entry.kind 一次性
6362
+ // 决定是否绑激活是安全的;若 id 规则变化须同步复核此处。
6363
+ const unbind = entry.kind === "job" ? null : bindCardActivation(el, (sessionId) => {
5967
6364
  if (typeof sessions?.open !== "function") return;
5968
6365
  lastActivatedId = sessionId;
5969
6366
  // 新激活意图取代一切旧重试链,避免过期链条稍后把当前会话拽回旧目标;
@@ -5992,8 +6389,14 @@ function apply(ctx) {
5992
6389
  rec.el.dataset.kind = entry.kind;
5993
6390
  rec.el.dataset.sessionId = entry.id;
5994
6391
  rec.el.dataset.depth = String(entry.depth ?? 0);
5995
- rec.el.setAttribute("role", "button");
5996
- rec.el.tabIndex = 0;
6392
+ // 纯展示子卡(R-01-023/AC-08)不进入按钮语义与 tab 序。
6393
+ if (entry.kind === "job") {
6394
+ rec.el.removeAttribute("role");
6395
+ rec.el.removeAttribute("tabindex");
6396
+ } else {
6397
+ rec.el.setAttribute("role", "button");
6398
+ rec.el.tabIndex = 0;
6399
+ }
5997
6400
  rec.el.replaceChildren(...cardChildren(entry.kind));
5998
6401
  rec.kind = entry.kind;
5999
6402
  }
@@ -6007,10 +6410,13 @@ function apply(ctx) {
6007
6410
  rec.el.setAttribute("data-wait", entry.waitClass);
6008
6411
  else rec.el.removeAttribute("data-wait");
6009
6412
  const recentTimeText = entry.kind === "recent" ? fmtRecentTime(entry.activityAt) : "";
6413
+ const jobStatusText = entry.kind === "job" ? JOB_STATUS_LABELS[entry.jobStatus] ?? "" : "";
6414
+ const jobKindText = entry.kind === "job" ? jobKindLabel(entry.jobKind) : "";
6010
6415
  rec.el.setAttribute(
6011
6416
  "aria-label",
6012
- `${entry.workspaceTitle ? entry.workspaceTitle + " - " : ""}${entry.title}${
6013
- entry.pendingText ? "," + entry.pendingText : ""
6417
+ `${entry.workspaceTitle ? entry.workspaceTitle + " - " : ""}${jobKindText ? jobKindText + "," : ""}${entry.title}${
6418
+ jobStatusText ? "," + jobStatusText : ""
6419
+ }${entry.pendingText ? "," + entry.pendingText : ""
6014
6420
  }${(entry.waitClass === "done" || entry.waitClass === "error") && entry.noteText ? "," + entry.noteText : ""}${
6015
6421
  recentTimeText ? "," + recentTimeText : ""
6016
6422
  }`,
@@ -6292,7 +6698,10 @@ function apply(ctx) {
6292
6698
  for (const [id, record] of busyById) {
6293
6699
  if (typeof record?.openWaitStart === "number") waitingStarts.set(String(id), record.openWaitStart);
6294
6700
  }
6295
- const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds, waitingStarts);
6701
+ // 在跑后台任务(R-01-023):jobsBySession 快照注入 buildEntries/buildRecent——
6702
+ // liveJobs 非空的主会话保留活动区、归运行组并抑制完成/错误提醒。
6703
+ const jobsBySession = isRecord(snapshot) && isRecord(snapshot.jobsBySession) ? snapshot.jobsBySession : null;
6704
+ const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds, waitingStarts, jobsBySession);
6296
6705
  // 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
6297
6706
  // 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
6298
6707
  const runLikeIds = new Set(
@@ -6301,7 +6710,7 @@ function apply(ctx) {
6301
6710
  // 运行卡时钟:运行中子代理卡同样承载逐秒推进的进度与时长(R-01-009/AC-14)。
6302
6711
  syncLiveness(
6303
6712
  runLikeIds,
6304
- active.some((entry) => entry.kind === "running" || (entry.kind === "subagent" && runLikeIds.has(entry.id))),
6713
+ active.some((entry) => entry.kind === "running" || entry.kind === "job" || (entry.kind === "subagent" && runLikeIds.has(entry.id))),
6305
6714
  );
6306
6715
  for (const entry of active) {
6307
6716
  const liveRecord = livenessById.get(entry.id);
@@ -6464,7 +6873,7 @@ function apply(ctx) {
6464
6873
  }
6465
6874
  if (detail.memoTurnEnd != null) turnEnds[id] = detail.memoTurnEnd;
6466
6875
  }
6467
- const recentCandidates = buildRecent(snapshot, workspaceItems, now, sessionDetailsById, archivedSessionIds, completeAcksById, delegatingIds, turnEnds);
6876
+ const recentCandidates = buildRecent(snapshot, workspaceItems, now, sessionDetailsById, archivedSessionIds, completeAcksById, delegatingIds, turnEnds, jobsBySession);
6468
6877
  recentTotal = recentCandidates.length;
6469
6878
  const recent = recentCandidates.slice(0, recentVisibleCount);
6470
6879
  recentHasMore = recent.length < recentTotal;
@@ -6529,6 +6938,10 @@ function apply(ctx) {
6529
6938
  }
6530
6939
  const detailIds = [...active, ...recent].map((entry) => entry.id);
6531
6940
  detailIds.sort((a, b) => Number(String(b) === String(snapshot?.current)) - Number(String(a) === String(snapshot?.current)));
6941
+ // 档位资源纪律(R-01-025/AC-01):渲染期持续执法——进入紧凑档即拆主会话窗口;
6942
+ // 拆窗时被豁免的当前会话在切走后的下一轮渲染照常释放。幂等:主会话拆窗后
6943
+ // 不在 logSourceSubs,重复执法为无操作。
6944
+ enforceDensityDataDiscipline();
6532
6945
  loadNativeDetails({ ids: detailIds, previewFallbackIds, durationFallbackIds, subagentModelReadIds });
6533
6946
  // 累计运行时长懒回填(R-01-020/AC-05):对可见主会话触发宿主侧存量回合补齐;
6534
6947
  // 宿主侧每会话单飞,重复触发由宿主合并;完成后经 busy SSE 广播推送。
@@ -6556,6 +6969,13 @@ function apply(ctx) {
6556
6969
  const awaitAgeSignature = active
6557
6970
  .filter((entry) => entry.kind === "awaiting")
6558
6971
  .map((entry) => entry.awaitAge ?? "");
6972
+ // job 子卡的任务行时长随 1 秒时钟推进(R-01-023/AC-01):秒桶注入渲染签名,
6973
+ // 与运行卡 totalBusyMs 的逐秒重绘同节奏,不新增时钟。
6974
+ for (const entry of visibleEntries) {
6975
+ if (entry.kind === "job") {
6976
+ entry.jobsAgeSec = Math.floor(Date.now() / 1000);
6977
+ }
6978
+ }
6559
6979
  const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, awaitAgeSignature, densityLevel]);
6560
6980
  if (sig === lastSig) return;
6561
6981
  const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));