dsh-activity-pane 0.11.0 → 0.13.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.
@@ -642,11 +642,11 @@ function buildFoldRow(run) {
642
642
  kind = "context";
643
643
  icon = "context";
644
644
  } else if (runningTool !== null) {
645
- label = "正在运行";
645
+ label = "运行";
646
646
  kind = "tool";
647
- icon = "bash";
647
+ icon = "bash";
648
648
  } else if (runningThink !== null) {
649
- label = "正在思考";
649
+ label = "思考";
650
650
  kind = "assistant";
651
651
  icon = "assistant";
652
652
  } else if (tools.length > 0) {
@@ -840,7 +840,7 @@ function foldedConversationTimeline(snapshot, limit = 4, cwd = "", descendantAct
840
840
  const max = Math.max(0, limit);
841
841
  if (max === 0) return [];
842
842
  // 非执行呈现(渲染层 idle 判定或快照 pending)且非委托周期:落定在分组之前——
843
- // 组标题/状态由已定案成员派生(避免 done 圆点配「正在思考」标题),尾部提升同时跳过。
843
+ // 组标题/状态由已定案成员派生(避免 done 圆点配「思考」标题),尾部提升同时跳过。
844
844
  const settle = (idle === true || snapshotIdle(snapshot)) && descendantActive !== true;
845
845
  for (const want of [max * 3, max * 8, Number.MAX_SAFE_INTEGER]) {
846
846
  const items = rawTailItems(snapshot, want, cwd, true);
@@ -1051,7 +1051,7 @@ function conversationTimelineFromHistory(history, limit = 4, cwd = "") {
1051
1051
  /** 冷 history 折叠分组时间线(R-01-017、R-01-012/AC-12~AC-15):页内全部事件映射折叠后
1052
1052
  * 套用与快照路径同一窗口/锚行选择(selectTimelineRows),最近用户消息滚动触顶后停留为
1053
1053
  * 首行锚行。settleIdle:阻塞等待呈现(pendingText 存在)下折叠前把残留 running 行落定,
1054
- * 组标题/状态由已定案成员派生(「运行了命令」而非「正在运行」蓝闪),与快照路径的
1054
+ * 组标题/状态由已定案成员派生(「运行了命令」而非「运行」蓝闪),与快照路径的
1055
1055
  * settleWhenIdle 前置语义一致。 */
1056
1056
  function foldedHistoryTimeline(history, limit = 4, cwd = "", settleIdle = false) {
1057
1057
  const max = Math.max(0, limit);
@@ -1538,7 +1538,7 @@ function mainTitle(byId, id) {
1538
1538
  * 把 sessions/workspaces 快照构建成窗格条目列表(有序、已含层级与显示过滤)。
1539
1539
  * 返回数组的每一项:
1540
1540
  * { id, parentId?, depth, kind: 'running'|'awaiting'|'subagent', title, workspaceTitle, workspaceKey,
1541
- * isCurrent, pendingText?, descendantActive? }
1541
+ * isCurrent, pendingText?, descendantActive?, stateAt? }
1542
1542
  * kind 规则:
1543
1543
  * - 主会话 running(且无 pending)或处于委托周期(含后代耗尽空窗)→ 'running'
1544
1544
  * - 主会话 pendingInteraction / completed / errorReminder → 'awaiting'(等待用户行动)
@@ -1558,6 +1558,9 @@ function mainTitle(byId, id) {
1558
1558
  * 尚未结束、`lastTurnEnd` 仍是上一回合的旧时刻,不能作为等待进行中会话的排序键(T-141);
1559
1559
  * 非 Map、缺失记录或值非有限数字(含 null/空串等 Number 归一为 0 的形状)均视为无数据,
1560
1560
  * 回落宿主列表时间。
1561
+ * stateAt(进入当前等待行动状态的时刻,R-01-002/AC-14):仅 awaiting 条目携带,与排序键
1562
+ * 共用 enterStateAt 单点口径——pending 取 waitingStarts,完成/错误提醒取 completions.lastTurnEnd;
1563
+ * 显示侧不回落宿主列表时间——排序键缺失时的回落仅用于排序,显示以 null(不显示)承载。
1561
1564
  */
1562
1565
  function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null) {
1563
1566
  const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
@@ -1627,16 +1630,20 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
1627
1630
  if (typeof value !== "number") return null;
1628
1631
  return Number.isFinite(value) ? value : null;
1629
1632
  };
1633
+ // 进入状态时刻单点(R-01-002/AC-14,排序键 R-01-001/AC-07 同源):阻塞等待取
1634
+ // waitingStarts,完成/错误提醒取最近一次回合结束登记时刻;有效性口径单点收紧——
1635
+ // 仅真实数字作数,Number(null)/Number("") 归一的 0 陷阱两侧同样不作数。
1636
+ const enterStateAt = (id, pending) => {
1637
+ if (pending) return waitingStartTime(id);
1638
+ const end = completionFor(id, completions)?.lastTurnEnd;
1639
+ return typeof end === "number" && Number.isFinite(end) ? end : null;
1640
+ };
1630
1641
  const sortTime = (id) => {
1631
1642
  if (isRunningEntry(id)) return instructionTime(byId[id]);
1632
1643
  const m = meta.get(id);
1633
- if (m !== undefined && m.pending) {
1634
- const start = waitingStartTime(id);
1635
- return start ?? instructionTime(byId[id]);
1636
- }
1637
- const record = completionFor(id, completions);
1638
- const end = isRecord(record) ? Number(record.lastTurnEnd) : NaN;
1639
- return Number.isFinite(end) ? end : instructionTime(byId[id]);
1644
+ const entered = m !== undefined ? enterStateAt(id, m.pending) : null;
1645
+ // 排序键缺失回落宿主列表时间;回落仅用于排序,显示侧以 null 承载(见 stateAt)。
1646
+ return entered ?? instructionTime(byId[id]);
1640
1647
  };
1641
1648
  rootIds.sort((a, b) => {
1642
1649
  const byGroup = Number(isRunningEntry(b)) - Number(isRunningEntry(a));
@@ -1670,6 +1677,10 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
1670
1677
  const doneWait = !m.pending && m.done && !m.running && !m.delegating;
1671
1678
  const errWait = !m.pending && m.err && !m.running && !m.delegating;
1672
1679
  const errorNote = entryErrorNote(completionFor(id, completions));
1680
+ // 进入状态时刻(R-01-002/AC-14):仅 awaiting 条目携带,与排序键共用 enterStateAt
1681
+ // 单点口径;显示侧不回落宿主列表时间,不可得即为 null(节点隐藏),避免把
1682
+ // 回退值冒充真实进入时刻。
1683
+ const stateAt = m.pending || doneWait || errWait ? enterStateAt(id, m.pending) : undefined;
1673
1684
  const questionPreview =
1674
1685
  m.pending && m.row.pendingInteraction === "question" ? timelineQuestionPreview(timeline) : undefined;
1675
1686
  entries.push({
@@ -1714,6 +1725,7 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
1714
1725
  : doneWait
1715
1726
  ? ROUND_DONE_NOTE
1716
1727
  : undefined,
1728
+ stateAt,
1717
1729
  questionPreview: m.pending && m.row.pendingInteraction === "question" ? (questionPreview ?? null) : undefined,
1718
1730
  });
1719
1731
  }
@@ -1808,6 +1820,8 @@ function cardSignature(entries) {
1808
1820
  entry.waitClass ?? null,
1809
1821
  entry.noteText ?? null,
1810
1822
  entry.questionPreview ?? null,
1823
+ // 进入状态时刻参与签名(R-01-002/AC-14):数据到达/更替(回填、SSE)驱动重绘。
1824
+ entry.stateAt ?? null,
1811
1825
  entry.activityAt ?? null,
1812
1826
  entry.progress ?? null,
1813
1827
  entry.loadingModel ?? null,
@@ -2674,7 +2688,7 @@ const CLOCK_MS = 1000;
2674
2688
  * 10Hz——事件率随宿主流式 chunk 数增长,显示粒度(秒级时长、块级时间线)无感,
2675
2689
  * 而渲染与 O(日志窗口) 派生不再随刷新率(移动端 120Hz)与事件率线性放大(T-127)。 */
2676
2690
  const SYNC_MIN_INTERVAL_MS = 100;
2677
- /** 历史卡相对时间刷新周期;无需每秒重绘整列。 */
2691
+ /** 历史卡相对时间与等待卡状态年龄的刷新周期;无需每秒重绘整列。 */
2678
2692
  const RECENT_TIME_REFRESH_MS = 60_000;
2679
2693
  /** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
2680
2694
  const LOAD_CONCURRENCY = 3;
@@ -3301,6 +3315,13 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
3301
3315
  [data-dsh-activity-pane] .dap-await-head {
3302
3316
  display: flex; align-items: center; gap: 8px; align-self: stretch; min-width: 0;
3303
3317
  }
3318
+ /* 进入状态相对年龄(R-01-002/AC-14):胶囊右侧裸相对时间,弱化色调不与胶囊争抢,
3319
+ 不参与等待脉冲;进入状态时刻不可得时隐藏节点,不以虚假时刻冒充。 */
3320
+ [data-dsh-activity-pane] .dap-await-age {
3321
+ flex: none; font-size: 10px; line-height: 14px; white-space: nowrap;
3322
+ color: color-mix(in srgb, currentColor 55%, transparent);
3323
+ }
3324
+ [data-dsh-activity-pane] .dap-await-age[hidden] { display: none; }
3304
3325
  [data-dsh-activity-pane] .dap-note-row {
3305
3326
  display: flex; align-items: center; gap: 6px; min-width: 0; align-self: stretch;
3306
3327
  }
@@ -3797,6 +3818,12 @@ function fmtRecentTime(ts, now = Date.now()) {
3797
3818
  }
3798
3819
  }
3799
3820
 
3821
+ /** 等待卡状态年龄文案(R-01-002/AC-14):进入状态时刻的裸相对时间,单点派生;
3822
+ * 时刻不可得或差值为负(时钟偏差)返回空串(节点隐藏)。 */
3823
+ function awaitAgeText(entry, now = Date.now()) {
3824
+ return Number.isFinite(entry?.stateAt) ? fmtRelativeAge(now - entry.stateAt) : "";
3825
+ }
3826
+
3800
3827
  /** 读取持久化列宽:缺失/非法/越界值经 clampPaneWidth 归一;localStorage 不可用(隐私模式)静默回退默认(R-01-015/AC-04)。 */
3801
3828
  function readStoredPaneWidth() {
3802
3829
  try {
@@ -4920,9 +4947,12 @@ function apply(ctx) {
4920
4947
  noteRow.querySelector(".dap-badge")?.remove();
4921
4948
  const capsule = makeEl("div", "dap-capsule");
4922
4949
  capsule.append(makeEl("span", "dap-capsule-icon"), makeEl("span", "dap-capsule-text"));
4950
+ // 与新版骨架同形:胶囊行含类型胶囊与状态年龄段(R-01-002/AC-14)。
4951
+ const awaitHead = makeEl("div", "dap-await-head");
4952
+ awaitHead.append(capsule, makeEl("span", "dap-await-age"));
4923
4953
  const foot = makeEl("div", "dap-foot");
4924
4954
  noteRow.replaceWith(foot);
4925
- foot.append(capsule, noteRow);
4955
+ foot.append(awaitHead, noteRow);
4926
4956
  }
4927
4957
 
4928
4958
  /** 进度行骨架(运行卡与子代理卡共用,R-01-009/AC-06、AC-14):可伸缩轨道 + 固定宽百分比。 */
@@ -4993,7 +5023,8 @@ function apply(ctx) {
4993
5023
  const noteRow = makeEl("div", "dap-note-row");
4994
5024
  noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
4995
5025
  const awaitHead = makeEl("div", "dap-await-head");
4996
- awaitHead.append(capsule);
5026
+ // 状态年龄(R-01-002/AC-14):进入状态时刻的裸相对时间,胶囊右侧静态显示。
5027
+ awaitHead.append(capsule, makeEl("span", "dap-await-age"));
4997
5028
  const foot = makeEl("div", "dap-foot");
4998
5029
  foot.append(awaitHead, noteRow);
4999
5030
  return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];
@@ -5575,6 +5606,21 @@ function apply(ctx) {
5575
5606
  iconHolder.dataset.kind = iconKind;
5576
5607
  iconHolder.replaceChildren(...(iconKind === "" ? [] : [createCapsuleIcon(iconKind)]));
5577
5608
  }
5609
+ // 状态年龄(R-01-002/AC-14):进入状态时刻的裸相对时间,紧随胶囊之后;文案由
5610
+ // 帧内 enrichment 单点派生(entry.awaitAge,与签名同时钟源)。陈旧骨架就地
5611
+ // 补建年龄节点(C-043 热装兼容同惯例)。
5612
+ const capsuleEl = el.querySelector(".dap-capsule");
5613
+ if (capsuleEl !== null) {
5614
+ let ageEl = capsuleEl.nextElementSibling;
5615
+ if (ageEl === null || !ageEl.classList.contains("dap-await-age")) {
5616
+ ageEl = makeEl("span", "dap-await-age");
5617
+ capsuleEl.insertAdjacentElement("afterend", ageEl);
5618
+ }
5619
+ const ageText = entry.awaitAge ?? "";
5620
+ if (ageEl.textContent !== ageText) ageEl.textContent = ageText;
5621
+ const ageHidden = ageText === "";
5622
+ if (ageEl.hidden !== ageHidden) ageEl.hidden = ageHidden;
5623
+ }
5578
5624
  }
5579
5625
 
5580
5626
  if (entry.kind === "running") {
@@ -5739,7 +5785,7 @@ function apply(ctx) {
5739
5785
  }
5740
5786
  }
5741
5787
 
5742
- /** 历史卡相对时间只需分钟级刷新;无历史卡时停止定时器,避免空窗格常驻唤醒。 */
5788
+ /** 历史卡相对时间与等待卡状态年龄只需分钟级刷新;两者皆无时停止定时器,避免空窗格常驻唤醒。 */
5743
5789
  function syncRecentTimeClock(wanted) {
5744
5790
  if (wanted && recentTimeTimer === null) {
5745
5791
  recentTimeTimer = setInterval(() => queueSync(), RECENT_TIME_REFRESH_MS);
@@ -6276,7 +6322,7 @@ function apply(ctx) {
6276
6322
  const entryCwd = snapshot?.byId?.[entry.id]?.cwd ?? "";
6277
6323
  // log 派生 memo:history 引用 / idle / cwd 变化才重算。落定在折叠前生效
6278
6324
  // (foldedHistoryTimeline 的 settleIdle),组标题由已定案成员派生——阻塞等待卡
6279
- // 呈现「运行了命令」+「等待回答」摘要,而非「正在运行」蓝闪(R-01-009/AC-09)。
6325
+ // 呈现「运行了命令」+「等待回答」摘要,而非「运行」蓝闪(R-01-009/AC-09)。
6280
6326
  const historyRef = detail?.history ?? null;
6281
6327
  const logReady = Array.isArray(historyRef) && historyRef.length > 0;
6282
6328
  if (detail && logReady && (detail.memoLogTimelineOf !== historyRef || detail.memoLogTimelineIdle !== entryIdle || detail.memoLogTimelineCwd !== entryCwd)) {
@@ -6327,6 +6373,8 @@ function apply(ctx) {
6327
6373
  if (entry.kind === "awaiting" && detail) {
6328
6374
  entry.elapsedMs = memoTurnDuration(detail);
6329
6375
  }
6376
+ // 状态年龄(R-01-002/AC-14):帧内单点派生一次,渲染与签名共用同一文案与时钟源。
6377
+ if (entry.kind === "awaiting") entry.awaitAge = awaitAgeText(entry, now);
6330
6378
  if (detail?.model) {
6331
6379
  entry.model = detail.model.model;
6332
6380
  entry.reasoning = detail.model.reasoning;
@@ -6439,7 +6487,8 @@ function apply(ctx) {
6439
6487
  } : null);
6440
6488
  if (elapsedMs === null) recentDurationFallbackIds.add(entry.id);
6441
6489
  }
6442
- syncRecentTimeClock(recent.length > 0);
6490
+ // 状态年龄(R-01-002/AC-14)与历史卡相对时间同为分钟级:任一存在即保持定时器。
6491
+ syncRecentTimeClock(recent.length > 0 || active.some((entry) => entry.kind === "awaiting" && entry.awaitAge));
6443
6492
  // 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
6444
6493
  // 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
6445
6494
  const previewFallbackIds = new Set();
@@ -6502,9 +6551,12 @@ function apply(ctx) {
6502
6551
  // listState 参与签名:空列表从 pending/error → ready 时卡集合不变,若只比较卡片
6503
6552
  // 会被提前返回冻结在「加载中」/「列表加载失败」;数量胶囊可见面变化同样需要
6504
6553
  // 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
6505
- // 历史卡的相对活动时间随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
6554
+ // 历史卡的相对活动时间与等待卡状态年龄随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
6506
6555
  const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
6507
- const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, densityLevel]);
6556
+ const awaitAgeSignature = active
6557
+ .filter((entry) => entry.kind === "awaiting")
6558
+ .map((entry) => entry.awaitAge ?? "");
6559
+ const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, awaitAgeSignature, densityLevel]);
6508
6560
  if (sig === lastSig) return;
6509
6561
  const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
6510
6562
  // 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
package/README.md CHANGED
@@ -21,7 +21,7 @@ This plugin attempts to answer these questions by providing an **activity sessio
21
21
  <p align="center">
22
22
  <picture>
23
23
  <source media="(prefers-color-scheme: dark)" srcset="assets/screenshot-desktop-dark.png">
24
- <img src="assets/screenshot-desktop-light.png" width="1000" alt="Activity session overview pane in an isolated demo environment: showing run stats, sub-agent hierarchy, question prompts, completion reminders, error reminders, and recent history">
24
+ <img src="assets/screenshot-desktop-light.png" width="1000" alt="Activity session overview pane in an isolated demo environment at the default medium display tier: showing live session status with sub-agent hierarchy, question prompts, completion reminders, error reminders, and recent history">
25
25
  </picture>
26
26
  </p>
27
27
  <p align="center"><sub>Mobile <a href="assets/screenshot-mobile-dark.png">dark drawer</a> · <a href="assets/screenshot-mobile-light.png">light drawer</a> of the same clean isolated environment running a simulated coding task</sub></p>
@@ -53,11 +53,14 @@ The npm package ships prebuilt, so no local build step is needed. If the pane do
53
53
  - [x] **From floating overlay to docked pane**: on desktop, a persistent edge-docked column is added to the right of the left-sidebar workspaces; on mobile, a fixed drawer hidden by default is expanded via the "Activity" button in the session header, without squeezing the main conversation layout.
54
54
  - [x] **No pet icon features**: pet-related features are not supported; the UI focuses on session activity itself.
55
55
  - [x] **Native data-source subscription**: directly subscribes to the push snapshots of DSH's native `sessions` / `workspaces` services; the timeline shows at most 4 collapsed work-item rows, keeping the latest user instruction and the work item actually being executed.
56
- - [x] **Recent session list**: the pane is split into "Active sessions" and "Recent history" areas; inactive main sessions are shown in activity-time batches, and a "Load more..." button at the bottom lets users explicitly reveal older sessions.
57
- - [x] **Stronger waiting-for-action reminders**: blocked waits, completion reminders, and error reminders are marked with gold, green, and red cards respectively; questions are previewed directly as a question list, and completion reminders are explicitly acknowledged via the "Move to history" button on the card; the state is persisted on the host side and synced across all clients, so refreshing the page or opening another window never loses unacknowledged completion reminders or error reminders not yet overwritten by a new round.
58
- - [x] **Sub/grandchild session hierarchy**: sub-agents are nested under their parent session with connector lines and compact cards; a parent whose own round has ended but that still has active descendants keeps rendering as running, and disappears from the active area once its sub-agents have ended and no active descendants remain; the history area keeps main sessions only.
59
- - [x] **Workspace names displayed and factored into ordering**: session cards show a workspace badge with a stable color, and session ordering follows the workspace order in the left sidebar.
60
- - [x] **Current work and run stats**: active cards show the latest instruction, thinking, and tool calls in a collapsed timeline of at most 4 rows; running cards also show round progress, output rate, cache hit rate, input/output tokens, and run duration, while completed, blocked, and error waits retain the previous round's duration at the end of the token stats row alongside last-known output stats; recent history cards retain the latest completed round's available stats between the reply preview and activity time.
56
+ - [x] **Recent session list**: the pane is split into "Active sessions" and "Recent history" areas; inactive main sessions are shown in activity-time batches with both the absolute date-time and the relative activity time, and a "Load more..." button at the bottom lets users explicitly reveal older sessions.
57
+ - [x] **Stronger waiting-for-action reminders**: blocked waits, completion reminders, and error reminders are marked with gold, green, and red cards respectively; questions are previewed directly as a question list, and completion reminders are explicitly acknowledged via the "Move to history" button on the card; the state is persisted on the host side and synced across all clients, so refreshing the page or opening another window never loses unacknowledged completion reminders or error reminders not yet overwritten by a new round. Each waiting card also shows the relative age of its current state (e.g. "5 min ago") to the right of the status pill, refreshed every minute and hidden when the entry time is unavailable.
58
+ - [x] **Sub/grandchild session hierarchy**: sub-agents are nested under their parent session with connector lines and compact cards; a parent whose own round has ended but that still has active descendants keeps rendering as running, and disappears from the active area once its sub-agents have ended and no active descendants remain; the history area keeps main sessions only. Sub-agent cards show model provenance, the reasoning effort, and round progress.
59
+ - [x] **Workspace badges with stable identity colors**: session cards show a workspace badge whose foreground/background color pair is derived from the workspace identity adding, removing, or changing other workspaces or refreshing the page never reshuffles existing colors.
60
+ - [x] **Recency-first activity ordering**: running sessions are pinned to the top and sorted by the time of the latest user instruction (newest first); waiting-for-action sessions are sorted by when they entered their waiting state (newest first).
61
+ - [x] **Current work and run stats**: active cards show the latest instruction, thinking, and tool calls in a collapsed timeline of at most 4 rows, with the cumulative run duration on the title row; running cards in the full tier also show round progress, output rate, cache hit rate, and input/output tokens, while completed, blocked, and error waits retain the previous round's duration at the end of the token stats row alongside last-known output stats; recent history cards retain the latest completed round's available stats between the reply preview and activity time.
62
+ - [x] **Three-tier display density**: the toggle in the title-bar tool area cycles every card through compact → medium → full (ascending information density), defaulting to medium — medium keeps the title, workspace badge, the latest timeline row (live for running sessions), and waiting bodies (completion reminders collapse to a single row), while compact keeps title rows only; switching anchors the selected card in place, and the choice persists across refreshes.
63
+ - [x] **Redesigned title bar**: the GitHub repo entry sits in its own area at the far left (hover tip: "Report issues, star & fork"), the collapse-hint icon appears on hover at the right end of the title area, and the display-density toggle lives in the tool area on the right — the two buttons are placed at opposite ends of the title bar to prevent touch mis-taps.
61
64
  - [x] **Session navigation**: clicking or keyboard-activating a session card jumps to that session's page, the current session stays highlighted, and selecting a session from DSH's native sidebar brings its pane card fully into view without forced centering.
62
65
  - [x] **Session metadata**: session cards show the current model name and reasoning level.
63
66
  - [x] **Polished desktop & mobile interactions**: the desktop pane can collapse, be resized by dragging, and remember its width; mobile uses a fixed drawer that does not squeeze the main conversation layout; long lists get independent scrolling and a back-to-top button.
package/README.zh-CN.md CHANGED
@@ -21,7 +21,7 @@ DSH (DeepSeek Harness) 一大痛点是缺少活动会话与历史会话的管理
21
21
  <p align="center">
22
22
  <picture>
23
23
  <source media="(prefers-color-scheme: dark)" srcset="assets/screenshot-desktop-dark.png">
24
- <img src="assets/screenshot-desktop-light.png" width="1000" alt="隔离演示环境中的活动会话总览窗格:展示运行统计、子代理层级、提问等待、完成提醒、错误提醒和最近历史">
24
+ <img src="assets/screenshot-desktop-light.png" width="1000" alt="隔离演示环境中的活动会话总览窗格(默认中间显示档):展示实时会话状态、子代理层级、提问等待、完成提醒、错误提醒和最近历史">
25
25
  </picture>
26
26
  </p>
27
27
  <p align="center"><sub>同一干净隔离环境中模拟编程任务的 <a href="assets/screenshot-mobile-dark.png">移动端深色抽屉</a> · <a href="assets/screenshot-mobile-light.png">移动端浅色抽屉</a></sub></p>
@@ -54,10 +54,13 @@ npm 包内置预构建产物,无需本地构建步骤。安装后如窗格未
54
54
  - [x] **去除宠物图标功能**:不支持宠物相关功能,界面聚焦于会话活动本身。
55
55
  - [x] **原生数据源订阅**:直接订阅 DSH 原生 `sessions` / `workspaces` 服务的推送式快照;时间线最多显示 4 个折叠工作项行,保留最近用户指令与真实执行中的工作项。
56
56
  - [x] **增加历史会话列表**:窗格分为「活动会话」和「最近历史」两个区域,非活动主会话按最近活动时间分批呈现;历史区底部提供「加载更多...」按钮,点击后才继续找回更早会话;卡片同时显示绝对日期时间与相对活动时间。
57
- - [x] **强化等待行动提醒**:阻塞等待、完成提醒与错误提醒分别以金色、绿色和红色卡片标识;提问直接预览问题列表,完成提醒经卡片上的「移入历史」按钮显式确认;状态由宿主侧持久化并在所有客户端间同步,刷新页面或另开窗口不会丢失未确认的完成提醒和尚未被新回合覆盖的错误提醒。
58
- - [x] **显示子/孙会话层级**:子代理以连接线和紧凑卡片嵌套在母会话下;母会话自身回合结束但仍有活动后代时继续按运行中呈现,子代理结束且没有活动后代后从活动区消失;历史区只保留主会话。
59
- - [x] **显示工作区名称并参与排序**:会话卡片显示带稳定色彩的工作区徽标,会话排序与左侧边栏中的工作区顺序保持一致。
60
- - [x] **展示当前工作与运行统计**:活动卡以最多 4 行折叠时间线展示最近指令、思考与工具调用;运行中卡片还显示回合进度、输出速率、缓存命中率、输入/输出 token 与运行时长,进入完成、阻塞或错误等待后在统计行末尾与 tok/s 等内容并列保留上一轮耗时及最后已知统计;迁入最近历史后,历史卡在助手预览与活动时间之间继续保留最后一轮的可用统计。
57
+ - [x] **强化等待行动提醒**:阻塞等待、完成提醒与错误提醒分别以金色、绿色和红色卡片标识;提问直接预览问题列表,完成提醒经卡片上的「移入历史」按钮显式确认;状态由宿主侧持久化并在所有客户端间同步,刷新页面或另开窗口不会丢失未确认的完成提醒和尚未被新回合覆盖的错误提醒。状态胶囊右侧还会显示进入当前状态的相对时间(如「5 分钟前」),随分钟级时钟更新,进入时刻不可得时不显示。
58
+ - [x] **显示子/孙会话层级**:子代理以连接线和紧凑卡片嵌套在母会话下;母会话自身回合结束但仍有活动后代时继续按运行中呈现,子代理结束且没有活动后代后从活动区消失;历史区只保留主会话。子代理卡显示模型溯源、reasoning effort 与回合进度。
59
+ - [x] **工作区徽标稳定配色**:会话卡片显示工作区徽标,前景/背景颜色按工作区身份稳定派生——增删或变更其它工作区、页面刷新均不改变既有配色。
60
+ - [x] **活动会话新近度排序**:运行中会话置顶并按最后用户指令时间从新到旧排列;等待行动会话按进入等待状态的时刻从新到旧排列。
61
+ - [x] **展示当前工作与运行统计**:活动卡以最多 4 行折叠时间线展示最近指令、思考与工具调用,标题行显示累计运行时长(超一小时按时分秒显示);完整呈现档的运行中卡片还显示回合进度、输出速率、缓存命中率、输入/输出 token 与运行时长,进入完成、阻塞或错误等待后在统计行末尾与 tok/s 等内容并列保留上一轮耗时及最后已知统计;迁入最近历史后,历史卡在助手预览与活动时间之间继续保留最后一轮的可用统计。
62
+ - [x] **三档显示密度一键切换**:标题行工具区的切换按钮按紧凑→中间→完整循环切换全部卡片的呈现密度,默认中间档——中间档保留标题、工作区徽标、时间线最新一行(运行中实时更新)与等待正文(完成提醒收合为单行),紧凑档仅保留标题行;切换时滚动锚定当前卡片,档位持久化、刷新后恢复。
63
+ - [x] **标题行三区布局**:最左独立区常显 GitHub 仓库入口(悬停提示「报告问题,点赞收藏」),中部标题区悬停显现收起方向图标,右侧工具区常显档位切换按钮——两按钮分处标题行两端,消除触屏误触。
61
64
  - [x] **加入会话导航跳转**:点击或键盘激活会话卡片可跳转到对应会话页面,当前会话保持高亮;从 DSH 原生左侧栏选择会话时,对应窗格卡片会滚动到完整可见,不强制居中。
62
65
  - [x] **增加会话元信息**:会话卡片中显示当前使用的模型名称和推理级别。
63
66
  - [x] **完善桌面与移动交互**:桌面窗格可折叠、拖拽调宽并记忆宽度;移动端使用不挤压主会话布局的固定抽屉;长列表提供独立滚动与回到顶部按钮。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-activity-pane",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -207,7 +207,7 @@ const steps = [
207
207
  // R-01-011/AC-06 移动端标题行即收起抽屉
208
208
  "切到 <=767px 移动视口打开抽屉,确认点击抽屉内标题行与桌面为同一交互路径:抽屉收起、不出现折叠窄条;桌面宽度下点击标题行仍折叠为窄条,行为不回归。",
209
209
  // R-01-017/AC-01 折叠时间线呈现质量(「无 dsh-auto-collapse 不降级」已由 e2e/specs/auto-update.mjs 自动化覆盖)
210
- "运行一个多工具+思考回合的会话:确认活动卡时间线呈折叠分组(如「正在运行/运行了命令/编辑了文件/已思考」,组摘要为推理文本内容)。",
210
+ "运行一个多工具+思考回合的会话:确认活动卡时间线呈折叠分组(如「运行/运行了命令/编辑了文件/已思考」,组摘要为推理文本内容)。",
211
211
  // R-02-004/AC-01 订阅断开
212
212
  "在 DevTools 中观察:会话停止运行后,其轮内状态订阅被断开(无残留监听/继续更新);卸载插件后订阅归零、控制台干净。",
213
213
  // R-01-021/AC-01、R-01-021/AC-02、R-01-021/AC-03、R-01-021/AC-04、R-01-021/AC-05、R-01-021/AC-06、R-01-021/AC-07、R-01-021/AC-08 交互与刷新恢复 → e2e/specs/compact-density.mjs(AC-03 类别底色观感、AC-05 相对位置与图标辨识残留人工)
package/scripts/check.mjs CHANGED
@@ -626,8 +626,8 @@ assert.equal(askQuestionsPreview(undefined), null);
626
626
  const timelineQuestions = { items: [{ index: 1, text: "要合并回 main 吗?" }, { index: 2, text: "需要先跑测试吗?" }], omitted: false };
627
627
  assert.deepEqual(
628
628
  timelineQuestionPreview([
629
- { fold: true, label: "正在运行", question: null },
630
- { fold: true, label: "正在运行", question: timelineQuestions },
629
+ { fold: true, label: "运行", question: null },
630
+ { fold: true, label: "运行", question: timelineQuestions },
631
631
  ]),
632
632
  timelineQuestions,
633
633
  "结构化提问预览穿透折叠分组上浮组行",
@@ -936,6 +936,47 @@ assert.deepEqual(
936
936
  buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions).map((entry) => [entry.id, entry.kind]),
937
937
  "waitingStarts 非法值(null/空串)不作数、回落宿主列表时间,不误判为最旧时刻 0(R-01-001/AC-07,T-141 复审)",
938
938
  );
939
+ // ---- R-01-002/AC-14 等待卡状态年龄:awaiting 条目携带进入状态时刻 stateAt ----
940
+ // 与排序键共用 enterStateAt 单点口径:阻塞等待取 waitingStarts(openWaitStart),
941
+ // 完成/错误提醒取 completions.lastTurnEnd;显示口径不回落宿主列表时间,
942
+ // 缺失/非法为 null(节点隐藏)。
943
+ {
944
+ const stateEntries = buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions, null, [], mixedWaitingStarts);
945
+ const stateById = new Map(stateEntries.map((entry) => [entry.id, entry]));
946
+ assert.equal(stateById.get("sWait").stateAt, 9_000, "阻塞等待条目 stateAt 取等待边界开启时刻,与排序键同源(R-01-002/AC-14)");
947
+ assert.equal(stateById.get("sWaitNoRec").stateAt, null, "waitingStarts 缺失的阻塞等待条目 stateAt 为 null,不回落宿主列表时间(R-01-002/AC-14)");
948
+ assert.equal(stateById.get("sDone1").stateAt, 4_000, "完成提醒条目 stateAt 取回合结束登记时刻(R-01-002/AC-14)");
949
+ assert.equal(stateById.get("sRunOld").stateAt, undefined, "运行中条目不携带 stateAt(R-01-002/AC-14)");
950
+ }
951
+ assert.equal(
952
+ buildEntries(
953
+ { ids: ["sErr"], byId: { sErr: { id: "sErr", displayTitle: "错误提醒", running: false, updatedAt: 500 } }, current: null },
954
+ [],
955
+ {},
956
+ new Map([["sErr", { lastTurnEnd: 12_345, lastTurnEndKind: "error", lastTurnEndError: "boom", ackedAt: null }]]),
957
+ )[0].stateAt,
958
+ 12_345,
959
+ "错误提醒条目 stateAt 取回合结束登记时刻(R-01-002/AC-14)",
960
+ );
961
+ // 子代理条目不携带 stateAt(R-01-002/AC-14):状态年龄仅主会话等待卡承载。
962
+ {
963
+ const subStateActivity = {
964
+ ids: ["sParent", "sSub"],
965
+ byId: {
966
+ sParent: { id: "sParent", displayTitle: "母会话", running: false, updatedAt: 500 },
967
+ sSub: { id: "sSub", parentId: "sParent", origin: "subagent", running: true, updatedAt: 600 },
968
+ },
969
+ current: null,
970
+ };
971
+ const subEntry = buildEntries(subStateActivity, [], {}, null).find((entry) => entry.id === "sSub");
972
+ assert.equal(subEntry?.kind, "subagent", "前置:运行中子代理为 subagent 条目");
973
+ assert.equal(subEntry?.stateAt, undefined, "子代理条目不携带 stateAt(R-01-002/AC-14)");
974
+ }
975
+ assert.notEqual(
976
+ cardSignature(buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions, null, [], mixedWaitingStarts)),
977
+ cardSignature(buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions, null, [], null)),
978
+ "stateAt 到达/更替驱动渲染签名(R-01-002/AC-14)",
979
+ );
939
980
  assert.deepEqual(trackRuns([{ kind: "subagent", parentId: "p", depth: 1 }]), [], "无 id 条目不产生轨道");
940
981
  assert.deepEqual(
941
982
  trackRuns([...hierarchyEntries, { id: "X", kind: "subagent", parentId: "root", depth: 3 }]),
@@ -1124,7 +1165,7 @@ const questionPreview = {
1124
1165
  omitted: false,
1125
1166
  };
1126
1167
  const questionEntries = buildEntries(questionSnap, [], {
1127
- sQ: { timeline: [{ fold: true, label: "正在运行", summary: "等待回答", question: questionPreview }] },
1168
+ sQ: { timeline: [{ fold: true, label: "运行", summary: "等待回答", question: questionPreview }] },
1128
1169
  });
1129
1170
  assert.equal(questionEntries[0].pendingText, "提问中");
1130
1171
  assert.equal(questionEntries[0].pendingKind, "question");
@@ -1747,7 +1788,7 @@ const runTool = foldedConversationTimeline({
1747
1788
  ["r2", { key: "r2", kind: "tool-call", data: { root: { kind: "tool-call", callId: "r2", call: { name: "grep", argsRaw: '{"pattern":"x"}' } } } }],
1748
1789
  ]).get(key) } },
1749
1790
  });
1750
- assert.equal(runTool[0].label, "正在运行", "运行中工具组标题为正在运行");
1791
+ assert.equal(runTool[0].label, "运行", "运行中工具组标题为运行(R-01-017/AC-03)");
1751
1792
  assert.equal(runTool[0].status, "running", "运行中状态聚合");
1752
1793
  assert.equal(runTool[0].icon, "bash", "运行中 tool 组行图标同为命令图标");
1753
1794
  // R-01-017/AC-03 双运行成员的优先序:running tool 标题/摘要优先于 running think(评审对齐 vendor updateChip)。
@@ -1757,12 +1798,12 @@ const bothRunning = foldedConversationTimeline({
1757
1798
  ["br2", { key: "br2", kind: "assistant-step", data: { status: "running", turn: 1, step: 0, blocks: [{ kind: "reasoning", text: "思考中\n最新想法" }] } }],
1758
1799
  ]).get(key) } },
1759
1800
  });
1760
- assert.equal(bothRunning[0].label, "正在运行", "tool 与 think 同时运行时标题取正在运行");
1801
+ assert.equal(bothRunning[0].label, "运行", "tool 与 think 同时运行时标题取运行");
1761
1802
  assert.equal(bothRunning[0].summary, "make", "同时运行时的组摘要取执行中工具摘要(AC-04 限定于无执行中工具的分组)");
1762
1803
  const runThink = foldedConversationTimeline({
1763
1804
  chat: { order: ["rt"], nodes: { get: () => ({ kind: "assistant-step", data: { status: "running", turn: 1, step: 0, blocks: [{ kind: "reasoning", text: "第一行\n最新行" }] } }) } },
1764
1805
  });
1765
- assert.equal(runThink[0].label, "正在思考", "运行中思考组标题为正在思考(R-01-017/AC-03)");
1806
+ assert.equal(runThink[0].label, "思考", "运行中思考组标题为思考(R-01-017/AC-03)");
1766
1807
  assert.equal(runThink[0].summary, "最新行", "流式思考摘要取最新行(R-01-017/AC-04)");
1767
1808
  // R-01-017/AC-03 编辑了文件 / 上下文注入 标题判定
1768
1809
  const editGroup = foldedConversationTimeline({
@@ -2034,10 +2075,10 @@ assert.ok(settledStream.every((row) => row.status !== "running"), "回合落定
2034
2075
  // 阻塞等待呈现(settleIdle):折叠前落定残留 running 行,组标题/状态由已定案成员派生
2035
2076
  // (快照路径 settleWhenIdle 前置语义对齐,R-01-009/AC-09「仅执行中行闪烁」)。
2036
2077
  const blockedAskRunning = foldedHistoryTimeline([hUser(1, "指令"), hToolCall(2, "ha")], 10);
2037
- assert.equal(blockedAskRunning.at(-1).label, "正在运行", "未落定呈现保留「正在运行」组标题");
2078
+ assert.equal(blockedAskRunning.at(-1).label, "运行", "未落定呈现保留「运行」组标题");
2038
2079
  const blockedAskSettled = foldedHistoryTimeline([hUser(1, "指令"), hToolCall(2, "ha")], 10, "", true);
2039
2080
  assert.equal(blockedAskSettled.at(-1).status, "done", "阻塞等待呈现下折叠前落定残留 running 行");
2040
- assert.equal(blockedAskSettled.at(-1).label, "运行了命令", "落定后组标题由已定案成员派生(非「正在运行」)");
2081
+ assert.equal(blockedAskSettled.at(-1).label, "运行了命令", "落定后组标题由已定案成员派生(非「运行」)");
2041
2082
  // historyInstructionAnchor:尾扫最近一条非空文本真实用户消息(R-01-012/AC-12 快照窗口外兜底)
2042
2083
  assert.equal(historyInstructionAnchor([hUser(1, "旧指令"), hAgent(2, "回复"), hUser(3, "新指令")])?.text, "新指令", "锚行取最近一条用户消息");
2043
2084
  assert.equal(historyInstructionAnchor([hUser(1, " "), hAgent(2, "回复")]), null, "空文本用户消息不作锚");
@@ -2457,8 +2498,8 @@ assert.equal(delegatingFold[0].status, "running", "委托周期中尾部已定
2457
2498
  // pending + 活动后代:快照级 idle 落定让位于委托语义(R-01-016 例外、R-01-009/AC-10)。
2458
2499
  const pendingDescendant = foldedConversationTimeline({ ...frozenSnap, pending: [{ kind: "approval" }] }, 4, "", true);
2459
2500
  assert.ok(pendingDescendant.some((row) => row.status === "running"), "pending 且后代活跃时快照 idle 落定不生效(保留在飞呈现)");
2460
- // 落定在分组之前:组标题由已定案成员派生,不出现 done 圆点配「正在思考」(R-01-017/AC-03)。
2461
- assert.equal(frozenIdle.at(-1)?.label, "已思考", "idle 落定后组标题随成员落定(不再显示「正在思考」)");
2501
+ // 落定在分组之前:组标题由已定案成员派生,不出现 done 圆点配「思考」(R-01-017/AC-03)。
2502
+ assert.equal(frozenIdle.at(-1)?.label, "已思考", "idle 落定后组标题随成员落定(不再显示「思考」)");
2462
2503
  const nonDelegatingFold = foldedConversationTimeline({ ...idleGapSnapshot, running: false }, 4);
2463
2504
  assert.equal(nonDelegatingFold[0].status, "done", "非运行且非委托周期尾部不提升");
2464
2505
  const errorTail = conversationWorkItems({
@@ -3597,8 +3638,8 @@ assert.ok(bundle.includes("notifyLayoutChange"), "布局变化通知 sibling ove
3597
3638
  assert.ok(bundle.includes('window.dispatchEvent(new Event("resize"))'), "布局变化派发标准 resize 通知");
3598
3639
  assert.ok(bundle.includes("pane !== renderedPane"), "新窗格实例必须重置渲染签名");
3599
3640
  assert.ok(
3600
- clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, densityLevel]);"),
3601
- "列表 phase、历史时间文案与显示档位必须参与结构化渲染签名,空列表不得冻结在加载/失败状态(T-087),档位切换触发时间线按新档位重建(R-01-021/AC-08)",
3641
+ clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, awaitAgeSignature, densityLevel]);"),
3642
+ "列表 phase、历史时间文案、等待卡状态年龄与显示档位必须参与结构化渲染签名,空列表不得冻结在加载/失败状态(T-087),档位切换触发时间线按新档位重建(R-01-021/AC-08),状态年龄随分钟级时钟重绘(R-01-002/AC-14)",
3602
3643
  );
3603
3644
  assert.ok(
3604
3645
  clientSource.includes("scroll?.querySelector?.(`.${LIST_CLASS} .${CARD_CLASS}[data-current]`)") &&
@@ -3680,8 +3721,8 @@ assert.ok(
3680
3721
  "等待卡复用统计行并清理胶囊同行的旧耗时节点(R-01-009/AC-12、AC-13)",
3681
3722
  );
3682
3723
  assert.ok(
3683
- bundle.includes("function removeAwaitingHeadDuration") && bundle.includes("awaitHead.append(capsule);"),
3684
- "等待类型胶囊独立成行,固定耗时回到统计行(R-01-009/AC-12)",
3724
+ bundle.includes("function removeAwaitingHeadDuration") && bundle.includes('awaitHead.append(capsule, makeEl("span", "dap-await-age"));'),
3725
+ "等待类型胶囊独立成行且右侧携带状态年龄(R-01-002/AC-14),固定耗时回到统计行(R-01-009/AC-12)",
3685
3726
  );
3686
3727
  assert.ok(bundle.includes("lastTurnDuration({"), "等待卡耗时由最近完整回合边界派生(R-01-009/AC-12)");
3687
3728
  assert.ok(bundle.includes("`输入 ${fmtTokens("), "统计行含输入/输出中文短标签(R-01-009/AC-05)");
@@ -3768,9 +3809,9 @@ assert.ok(
3768
3809
  assert.ok(bundle.includes("function activeSessionIds(byId = {})"), "活动子代理沿 parentId 链补齐活动祖先");
3769
3810
  // ---- R-01-016/AC-01 等待卡保留最近工作项时间线 ----
3770
3811
  assert.ok(
3771
- bundle.includes("awaitHead.append(capsule);") &&
3812
+ bundle.includes('awaitHead.append(capsule, makeEl("span", "dap-await-age"));') &&
3772
3813
  bundle.includes('return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];'),
3773
- "awaiting 骨架在标题行与统计行、末行两段(胶囊+正文)之间含时间线,固定耗时由统计行承载(R-01-016/AC-01、R-01-009/AC-12,C-043)",
3814
+ "awaiting 骨架在标题行与统计行、末行两段(胶囊+正文)之间含时间线,末行首行胶囊右侧携带状态年龄(R-01-002/AC-14),固定耗时由统计行承载(R-01-016/AC-01、R-01-009/AC-12,C-043)",
3774
3815
  );
3775
3816
  // ---- R-01-002/AC-10 完成提醒卡「移入历史」按钮 ----
3776
3817
  assert.ok(
package/src/client.mjs CHANGED
@@ -45,7 +45,7 @@ const CLOCK_MS = 1000;
45
45
  * 10Hz——事件率随宿主流式 chunk 数增长,显示粒度(秒级时长、块级时间线)无感,
46
46
  * 而渲染与 O(日志窗口) 派生不再随刷新率(移动端 120Hz)与事件率线性放大(T-127)。 */
47
47
  const SYNC_MIN_INTERVAL_MS = 100;
48
- /** 历史卡相对时间刷新周期;无需每秒重绘整列。 */
48
+ /** 历史卡相对时间与等待卡状态年龄的刷新周期;无需每秒重绘整列。 */
49
49
  const RECENT_TIME_REFRESH_MS = 60_000;
50
50
  /** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
51
51
  const LOAD_CONCURRENCY = 3;
@@ -672,6 +672,13 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
672
672
  [data-dsh-activity-pane] .dap-await-head {
673
673
  display: flex; align-items: center; gap: 8px; align-self: stretch; min-width: 0;
674
674
  }
675
+ /* 进入状态相对年龄(R-01-002/AC-14):胶囊右侧裸相对时间,弱化色调不与胶囊争抢,
676
+ 不参与等待脉冲;进入状态时刻不可得时隐藏节点,不以虚假时刻冒充。 */
677
+ [data-dsh-activity-pane] .dap-await-age {
678
+ flex: none; font-size: 10px; line-height: 14px; white-space: nowrap;
679
+ color: color-mix(in srgb, currentColor 55%, transparent);
680
+ }
681
+ [data-dsh-activity-pane] .dap-await-age[hidden] { display: none; }
675
682
  [data-dsh-activity-pane] .dap-note-row {
676
683
  display: flex; align-items: center; gap: 6px; min-width: 0; align-self: stretch;
677
684
  }
@@ -1168,6 +1175,12 @@ function fmtRecentTime(ts, now = Date.now()) {
1168
1175
  }
1169
1176
  }
1170
1177
 
1178
+ /** 等待卡状态年龄文案(R-01-002/AC-14):进入状态时刻的裸相对时间,单点派生;
1179
+ * 时刻不可得或差值为负(时钟偏差)返回空串(节点隐藏)。 */
1180
+ function awaitAgeText(entry, now = Date.now()) {
1181
+ return Number.isFinite(entry?.stateAt) ? fmtRelativeAge(now - entry.stateAt) : "";
1182
+ }
1183
+
1171
1184
  /** 读取持久化列宽:缺失/非法/越界值经 clampPaneWidth 归一;localStorage 不可用(隐私模式)静默回退默认(R-01-015/AC-04)。 */
1172
1185
  function readStoredPaneWidth() {
1173
1186
  try {
@@ -2291,9 +2304,12 @@ function apply(ctx) {
2291
2304
  noteRow.querySelector(".dap-badge")?.remove();
2292
2305
  const capsule = makeEl("div", "dap-capsule");
2293
2306
  capsule.append(makeEl("span", "dap-capsule-icon"), makeEl("span", "dap-capsule-text"));
2307
+ // 与新版骨架同形:胶囊行含类型胶囊与状态年龄段(R-01-002/AC-14)。
2308
+ const awaitHead = makeEl("div", "dap-await-head");
2309
+ awaitHead.append(capsule, makeEl("span", "dap-await-age"));
2294
2310
  const foot = makeEl("div", "dap-foot");
2295
2311
  noteRow.replaceWith(foot);
2296
- foot.append(capsule, noteRow);
2312
+ foot.append(awaitHead, noteRow);
2297
2313
  }
2298
2314
 
2299
2315
  /** 进度行骨架(运行卡与子代理卡共用,R-01-009/AC-06、AC-14):可伸缩轨道 + 固定宽百分比。 */
@@ -2364,7 +2380,8 @@ function apply(ctx) {
2364
2380
  const noteRow = makeEl("div", "dap-note-row");
2365
2381
  noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
2366
2382
  const awaitHead = makeEl("div", "dap-await-head");
2367
- awaitHead.append(capsule);
2383
+ // 状态年龄(R-01-002/AC-14):进入状态时刻的裸相对时间,胶囊右侧静态显示。
2384
+ awaitHead.append(capsule, makeEl("span", "dap-await-age"));
2368
2385
  const foot = makeEl("div", "dap-foot");
2369
2386
  foot.append(awaitHead, noteRow);
2370
2387
  return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];
@@ -2946,6 +2963,21 @@ function apply(ctx) {
2946
2963
  iconHolder.dataset.kind = iconKind;
2947
2964
  iconHolder.replaceChildren(...(iconKind === "" ? [] : [createCapsuleIcon(iconKind)]));
2948
2965
  }
2966
+ // 状态年龄(R-01-002/AC-14):进入状态时刻的裸相对时间,紧随胶囊之后;文案由
2967
+ // 帧内 enrichment 单点派生(entry.awaitAge,与签名同时钟源)。陈旧骨架就地
2968
+ // 补建年龄节点(C-043 热装兼容同惯例)。
2969
+ const capsuleEl = el.querySelector(".dap-capsule");
2970
+ if (capsuleEl !== null) {
2971
+ let ageEl = capsuleEl.nextElementSibling;
2972
+ if (ageEl === null || !ageEl.classList.contains("dap-await-age")) {
2973
+ ageEl = makeEl("span", "dap-await-age");
2974
+ capsuleEl.insertAdjacentElement("afterend", ageEl);
2975
+ }
2976
+ const ageText = entry.awaitAge ?? "";
2977
+ if (ageEl.textContent !== ageText) ageEl.textContent = ageText;
2978
+ const ageHidden = ageText === "";
2979
+ if (ageEl.hidden !== ageHidden) ageEl.hidden = ageHidden;
2980
+ }
2949
2981
  }
2950
2982
 
2951
2983
  if (entry.kind === "running") {
@@ -3110,7 +3142,7 @@ function apply(ctx) {
3110
3142
  }
3111
3143
  }
3112
3144
 
3113
- /** 历史卡相对时间只需分钟级刷新;无历史卡时停止定时器,避免空窗格常驻唤醒。 */
3145
+ /** 历史卡相对时间与等待卡状态年龄只需分钟级刷新;两者皆无时停止定时器,避免空窗格常驻唤醒。 */
3114
3146
  function syncRecentTimeClock(wanted) {
3115
3147
  if (wanted && recentTimeTimer === null) {
3116
3148
  recentTimeTimer = setInterval(() => queueSync(), RECENT_TIME_REFRESH_MS);
@@ -3647,7 +3679,7 @@ function apply(ctx) {
3647
3679
  const entryCwd = snapshot?.byId?.[entry.id]?.cwd ?? "";
3648
3680
  // log 派生 memo:history 引用 / idle / cwd 变化才重算。落定在折叠前生效
3649
3681
  // (foldedHistoryTimeline 的 settleIdle),组标题由已定案成员派生——阻塞等待卡
3650
- // 呈现「运行了命令」+「等待回答」摘要,而非「正在运行」蓝闪(R-01-009/AC-09)。
3682
+ // 呈现「运行了命令」+「等待回答」摘要,而非「运行」蓝闪(R-01-009/AC-09)。
3651
3683
  const historyRef = detail?.history ?? null;
3652
3684
  const logReady = Array.isArray(historyRef) && historyRef.length > 0;
3653
3685
  if (detail && logReady && (detail.memoLogTimelineOf !== historyRef || detail.memoLogTimelineIdle !== entryIdle || detail.memoLogTimelineCwd !== entryCwd)) {
@@ -3698,6 +3730,8 @@ function apply(ctx) {
3698
3730
  if (entry.kind === "awaiting" && detail) {
3699
3731
  entry.elapsedMs = memoTurnDuration(detail);
3700
3732
  }
3733
+ // 状态年龄(R-01-002/AC-14):帧内单点派生一次,渲染与签名共用同一文案与时钟源。
3734
+ if (entry.kind === "awaiting") entry.awaitAge = awaitAgeText(entry, now);
3701
3735
  if (detail?.model) {
3702
3736
  entry.model = detail.model.model;
3703
3737
  entry.reasoning = detail.model.reasoning;
@@ -3810,7 +3844,8 @@ function apply(ctx) {
3810
3844
  } : null);
3811
3845
  if (elapsedMs === null) recentDurationFallbackIds.add(entry.id);
3812
3846
  }
3813
- syncRecentTimeClock(recent.length > 0);
3847
+ // 状态年龄(R-01-002/AC-14)与历史卡相对时间同为分钟级:任一存在即保持定时器。
3848
+ syncRecentTimeClock(recent.length > 0 || active.some((entry) => entry.kind === "awaiting" && entry.awaitAge));
3814
3849
  // 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
3815
3850
  // 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
3816
3851
  const previewFallbackIds = new Set();
@@ -3873,9 +3908,12 @@ function apply(ctx) {
3873
3908
  // listState 参与签名:空列表从 pending/error → ready 时卡集合不变,若只比较卡片
3874
3909
  // 会被提前返回冻结在「加载中」/「列表加载失败」;数量胶囊可见面变化同样需要
3875
3910
  // 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
3876
- // 历史卡的相对活动时间随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
3911
+ // 历史卡的相对活动时间与等待卡状态年龄随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
3877
3912
  const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
3878
- const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, densityLevel]);
3913
+ const awaitAgeSignature = active
3914
+ .filter((entry) => entry.kind === "awaiting")
3915
+ .map((entry) => entry.awaitAge ?? "");
3916
+ const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, awaitAgeSignature, densityLevel]);
3879
3917
  if (sig === lastSig) return;
3880
3918
  const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
3881
3919
  // 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
package/src/core.mjs CHANGED
@@ -636,11 +636,11 @@ function buildFoldRow(run) {
636
636
  kind = "context";
637
637
  icon = "context";
638
638
  } else if (runningTool !== null) {
639
- label = "正在运行";
639
+ label = "运行";
640
640
  kind = "tool";
641
- icon = "bash";
641
+ icon = "bash";
642
642
  } else if (runningThink !== null) {
643
- label = "正在思考";
643
+ label = "思考";
644
644
  kind = "assistant";
645
645
  icon = "assistant";
646
646
  } else if (tools.length > 0) {
@@ -834,7 +834,7 @@ export function foldedConversationTimeline(snapshot, limit = 4, cwd = "", descen
834
834
  const max = Math.max(0, limit);
835
835
  if (max === 0) return [];
836
836
  // 非执行呈现(渲染层 idle 判定或快照 pending)且非委托周期:落定在分组之前——
837
- // 组标题/状态由已定案成员派生(避免 done 圆点配「正在思考」标题),尾部提升同时跳过。
837
+ // 组标题/状态由已定案成员派生(避免 done 圆点配「思考」标题),尾部提升同时跳过。
838
838
  const settle = (idle === true || snapshotIdle(snapshot)) && descendantActive !== true;
839
839
  for (const want of [max * 3, max * 8, Number.MAX_SAFE_INTEGER]) {
840
840
  const items = rawTailItems(snapshot, want, cwd, true);
@@ -1045,7 +1045,7 @@ export function conversationTimelineFromHistory(history, limit = 4, cwd = "") {
1045
1045
  /** 冷 history 折叠分组时间线(R-01-017、R-01-012/AC-12~AC-15):页内全部事件映射折叠后
1046
1046
  * 套用与快照路径同一窗口/锚行选择(selectTimelineRows),最近用户消息滚动触顶后停留为
1047
1047
  * 首行锚行。settleIdle:阻塞等待呈现(pendingText 存在)下折叠前把残留 running 行落定,
1048
- * 组标题/状态由已定案成员派生(「运行了命令」而非「正在运行」蓝闪),与快照路径的
1048
+ * 组标题/状态由已定案成员派生(「运行了命令」而非「运行」蓝闪),与快照路径的
1049
1049
  * settleWhenIdle 前置语义一致。 */
1050
1050
  export function foldedHistoryTimeline(history, limit = 4, cwd = "", settleIdle = false) {
1051
1051
  const max = Math.max(0, limit);
@@ -1532,7 +1532,7 @@ export function mainTitle(byId, id) {
1532
1532
  * 把 sessions/workspaces 快照构建成窗格条目列表(有序、已含层级与显示过滤)。
1533
1533
  * 返回数组的每一项:
1534
1534
  * { id, parentId?, depth, kind: 'running'|'awaiting'|'subagent', title, workspaceTitle, workspaceKey,
1535
- * isCurrent, pendingText?, descendantActive? }
1535
+ * isCurrent, pendingText?, descendantActive?, stateAt? }
1536
1536
  * kind 规则:
1537
1537
  * - 主会话 running(且无 pending)或处于委托周期(含后代耗尽空窗)→ 'running'
1538
1538
  * - 主会话 pendingInteraction / completed / errorReminder → 'awaiting'(等待用户行动)
@@ -1552,6 +1552,9 @@ export function mainTitle(byId, id) {
1552
1552
  * 尚未结束、`lastTurnEnd` 仍是上一回合的旧时刻,不能作为等待进行中会话的排序键(T-141);
1553
1553
  * 非 Map、缺失记录或值非有限数字(含 null/空串等 Number 归一为 0 的形状)均视为无数据,
1554
1554
  * 回落宿主列表时间。
1555
+ * stateAt(进入当前等待行动状态的时刻,R-01-002/AC-14):仅 awaiting 条目携带,与排序键
1556
+ * 共用 enterStateAt 单点口径——pending 取 waitingStarts,完成/错误提醒取 completions.lastTurnEnd;
1557
+ * 显示侧不回落宿主列表时间——排序键缺失时的回落仅用于排序,显示以 null(不显示)承载。
1555
1558
  */
1556
1559
  export function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null) {
1557
1560
  const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
@@ -1621,16 +1624,20 @@ export function buildEntries(snapshot, workspaceItems, detailsById = {}, complet
1621
1624
  if (typeof value !== "number") return null;
1622
1625
  return Number.isFinite(value) ? value : null;
1623
1626
  };
1627
+ // 进入状态时刻单点(R-01-002/AC-14,排序键 R-01-001/AC-07 同源):阻塞等待取
1628
+ // waitingStarts,完成/错误提醒取最近一次回合结束登记时刻;有效性口径单点收紧——
1629
+ // 仅真实数字作数,Number(null)/Number("") 归一的 0 陷阱两侧同样不作数。
1630
+ const enterStateAt = (id, pending) => {
1631
+ if (pending) return waitingStartTime(id);
1632
+ const end = completionFor(id, completions)?.lastTurnEnd;
1633
+ return typeof end === "number" && Number.isFinite(end) ? end : null;
1634
+ };
1624
1635
  const sortTime = (id) => {
1625
1636
  if (isRunningEntry(id)) return instructionTime(byId[id]);
1626
1637
  const m = meta.get(id);
1627
- if (m !== undefined && m.pending) {
1628
- const start = waitingStartTime(id);
1629
- return start ?? instructionTime(byId[id]);
1630
- }
1631
- const record = completionFor(id, completions);
1632
- const end = isRecord(record) ? Number(record.lastTurnEnd) : NaN;
1633
- return Number.isFinite(end) ? end : instructionTime(byId[id]);
1638
+ const entered = m !== undefined ? enterStateAt(id, m.pending) : null;
1639
+ // 排序键缺失回落宿主列表时间;回落仅用于排序,显示侧以 null 承载(见 stateAt)。
1640
+ return entered ?? instructionTime(byId[id]);
1634
1641
  };
1635
1642
  rootIds.sort((a, b) => {
1636
1643
  const byGroup = Number(isRunningEntry(b)) - Number(isRunningEntry(a));
@@ -1664,6 +1671,10 @@ export function buildEntries(snapshot, workspaceItems, detailsById = {}, complet
1664
1671
  const doneWait = !m.pending && m.done && !m.running && !m.delegating;
1665
1672
  const errWait = !m.pending && m.err && !m.running && !m.delegating;
1666
1673
  const errorNote = entryErrorNote(completionFor(id, completions));
1674
+ // 进入状态时刻(R-01-002/AC-14):仅 awaiting 条目携带,与排序键共用 enterStateAt
1675
+ // 单点口径;显示侧不回落宿主列表时间,不可得即为 null(节点隐藏),避免把
1676
+ // 回退值冒充真实进入时刻。
1677
+ const stateAt = m.pending || doneWait || errWait ? enterStateAt(id, m.pending) : undefined;
1667
1678
  const questionPreview =
1668
1679
  m.pending && m.row.pendingInteraction === "question" ? timelineQuestionPreview(timeline) : undefined;
1669
1680
  entries.push({
@@ -1708,6 +1719,7 @@ export function buildEntries(snapshot, workspaceItems, detailsById = {}, complet
1708
1719
  : doneWait
1709
1720
  ? ROUND_DONE_NOTE
1710
1721
  : undefined,
1722
+ stateAt,
1711
1723
  questionPreview: m.pending && m.row.pendingInteraction === "question" ? (questionPreview ?? null) : undefined,
1712
1724
  });
1713
1725
  }
@@ -1802,6 +1814,8 @@ export function cardSignature(entries) {
1802
1814
  entry.waitClass ?? null,
1803
1815
  entry.noteText ?? null,
1804
1816
  entry.questionPreview ?? null,
1817
+ // 进入状态时刻参与签名(R-01-002/AC-14):数据到达/更替(回填、SSE)驱动重绘。
1818
+ entry.stateAt ?? null,
1805
1819
  entry.activityAt ?? null,
1806
1820
  entry.progress ?? null,
1807
1821
  entry.loadingModel ?? null,