dsh-activity-pane 0.11.0 → 0.12.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 +67 -15
- package/README.md +9 -6
- package/README.zh-CN.md +8 -5
- package/package.json +1 -1
- package/scripts/check.mjs +47 -6
- package/src/client.mjs +45 -7
- package/src/core.mjs +22 -8
package/.dsh-plugin/client.js
CHANGED
|
@@ -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
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
60
|
-
- [x] **
|
|
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]
|
|
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
package/scripts/check.mjs
CHANGED
|
@@ -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 }]),
|
|
@@ -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
|
|
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(
|
|
3684
|
-
"
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
|
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
|
@@ -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
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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,
|