dsh-activity-pane 0.10.1 → 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 +111 -31
- package/README.md +10 -7
- package/README.zh-CN.md +9 -6
- package/package.json +1 -1
- package/scripts/acceptance.mjs +4 -4
- package/scripts/check.mjs +93 -21
- package/src/client.mjs +68 -19
- package/src/core.mjs +43 -12
package/.dsh-plugin/client.js
CHANGED
|
@@ -138,11 +138,11 @@ function normalizeDensity(raw) {
|
|
|
138
138
|
return raw === "compact" || raw === "medium" || raw === "full" ? raw : "medium";
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/**
|
|
142
|
-
const DENSITY_ORDER = ["
|
|
141
|
+
/** 显示档位的循环次序:紧凑 → 中间 → 完整 → 紧凑——信息量自小到大(R-01-021/AC-01)。 */
|
|
142
|
+
const DENSITY_ORDER = ["compact", "medium", "full"];
|
|
143
143
|
|
|
144
144
|
/**
|
|
145
|
-
*
|
|
145
|
+
* 返回循环切换后的下一显示档位:紧凑 → 中间 → 完整 → 紧凑;
|
|
146
146
|
* 输入先经 normalizeDensity 归一,非法值视作中间档(R-01-021/AC-01)。
|
|
147
147
|
*/
|
|
148
148
|
function nextDensity(value) {
|
|
@@ -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'(等待用户行动)
|
|
@@ -1553,8 +1553,16 @@ function mainTitle(byId, id) {
|
|
|
1553
1553
|
* 始终为当帧原始后代活性(供进度锚点记账判定耗尽),不受 delegatingIds 影响。
|
|
1554
1554
|
* archivedIds(工作区服务的注册表全局归档集合):归档会话及其有效子代理子树不产出活动条目,
|
|
1555
1555
|
* 普通 fork 仅因共享来源 parentId 不受影响;避免会话服务仍保留旧行或完成提醒时在窗格中滞留。
|
|
1556
|
+
* waitingStarts(进入阻塞等待的时刻,R-01-001/AC-07):Map id → 毫秒时刻,取宿主回合记账的
|
|
1557
|
+
* `openWaitStart`(approval/asked 与 ask_user_question tool/call 边界);阻塞等待发生时本回合
|
|
1558
|
+
* 尚未结束、`lastTurnEnd` 仍是上一回合的旧时刻,不能作为等待进行中会话的排序键(T-141);
|
|
1559
|
+
* 非 Map、缺失记录或值非有限数字(含 null/空串等 Number 归一为 0 的形状)均视为无数据,
|
|
1560
|
+
* 回落宿主列表时间。
|
|
1561
|
+
* stateAt(进入当前等待行动状态的时刻,R-01-002/AC-14):仅 awaiting 条目携带,与排序键
|
|
1562
|
+
* 共用 enterStateAt 单点口径——pending 取 waitingStarts,完成/错误提醒取 completions.lastTurnEnd;
|
|
1563
|
+
* 显示侧不回落宿主列表时间——排序键缺失时的回落仅用于排序,显示以 null(不显示)承载。
|
|
1556
1564
|
*/
|
|
1557
|
-
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = []) {
|
|
1565
|
+
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null) {
|
|
1558
1566
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1559
1567
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1560
1568
|
const current = snapshot?.current ?? null;
|
|
@@ -1607,19 +1615,35 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1607
1615
|
meta.set(id, { row, running, pending, isSub, show, done, err, descendantActive, delegating, depth: 0 });
|
|
1608
1616
|
}
|
|
1609
1617
|
|
|
1610
|
-
// 主会话分两组排序(R-01-001/AC-07
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
// 两组相同时间均回落宿主列表出现顺序。工作区顺序不参与排序,仅承载卡片徽标与名称。
|
|
1618
|
+
// 主会话分两组排序(R-01-001/AC-07,键口径契约详见上方 JSDoc):运行中主会话置顶、
|
|
1619
|
+
// 组内按宿主列表时间从新到旧;等待/完成组排后、组内按进入状态时刻从新到旧;
|
|
1620
|
+
// 两组相同时间均回落宿主列表出现顺序,工作区顺序不参与排序。
|
|
1614
1621
|
const isRunningEntry = (id) => {
|
|
1615
1622
|
const m = meta.get(id);
|
|
1616
1623
|
return m !== undefined && !m.pending && (m.running || m.delegating);
|
|
1617
1624
|
};
|
|
1625
|
+
const waitingStartTime = (id) => {
|
|
1626
|
+
if (!(waitingStarts instanceof Map)) return null;
|
|
1627
|
+
const value = waitingStarts.get(String(id));
|
|
1628
|
+
// 仅接受真实数字时刻:Number(null)/Number("") 均为 0,会把「无数据」误判为
|
|
1629
|
+
// epoch 最旧时刻(与 normalizeBusyMs 的空值防护同理),一律回落宿主列表时间。
|
|
1630
|
+
if (typeof value !== "number") return null;
|
|
1631
|
+
return Number.isFinite(value) ? value : null;
|
|
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
|
+
};
|
|
1618
1641
|
const sortTime = (id) => {
|
|
1619
1642
|
if (isRunningEntry(id)) return instructionTime(byId[id]);
|
|
1620
|
-
const
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1643
|
+
const m = meta.get(id);
|
|
1644
|
+
const entered = m !== undefined ? enterStateAt(id, m.pending) : null;
|
|
1645
|
+
// 排序键缺失回落宿主列表时间;回落仅用于排序,显示侧以 null 承载(见 stateAt)。
|
|
1646
|
+
return entered ?? instructionTime(byId[id]);
|
|
1623
1647
|
};
|
|
1624
1648
|
rootIds.sort((a, b) => {
|
|
1625
1649
|
const byGroup = Number(isRunningEntry(b)) - Number(isRunningEntry(a));
|
|
@@ -1653,6 +1677,10 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1653
1677
|
const doneWait = !m.pending && m.done && !m.running && !m.delegating;
|
|
1654
1678
|
const errWait = !m.pending && m.err && !m.running && !m.delegating;
|
|
1655
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;
|
|
1656
1684
|
const questionPreview =
|
|
1657
1685
|
m.pending && m.row.pendingInteraction === "question" ? timelineQuestionPreview(timeline) : undefined;
|
|
1658
1686
|
entries.push({
|
|
@@ -1697,6 +1725,7 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1697
1725
|
: doneWait
|
|
1698
1726
|
? ROUND_DONE_NOTE
|
|
1699
1727
|
: undefined,
|
|
1728
|
+
stateAt,
|
|
1700
1729
|
questionPreview: m.pending && m.row.pendingInteraction === "question" ? (questionPreview ?? null) : undefined,
|
|
1701
1730
|
});
|
|
1702
1731
|
}
|
|
@@ -1791,6 +1820,8 @@ function cardSignature(entries) {
|
|
|
1791
1820
|
entry.waitClass ?? null,
|
|
1792
1821
|
entry.noteText ?? null,
|
|
1793
1822
|
entry.questionPreview ?? null,
|
|
1823
|
+
// 进入状态时刻参与签名(R-01-002/AC-14):数据到达/更替(回填、SSE)驱动重绘。
|
|
1824
|
+
entry.stateAt ?? null,
|
|
1794
1825
|
entry.activityAt ?? null,
|
|
1795
1826
|
entry.progress ?? null,
|
|
1796
1827
|
entry.loadingModel ?? null,
|
|
@@ -2657,7 +2688,7 @@ const CLOCK_MS = 1000;
|
|
|
2657
2688
|
* 10Hz——事件率随宿主流式 chunk 数增长,显示粒度(秒级时长、块级时间线)无感,
|
|
2658
2689
|
* 而渲染与 O(日志窗口) 派生不再随刷新率(移动端 120Hz)与事件率线性放大(T-127)。 */
|
|
2659
2690
|
const SYNC_MIN_INTERVAL_MS = 100;
|
|
2660
|
-
/**
|
|
2691
|
+
/** 历史卡相对时间与等待卡状态年龄的刷新周期;无需每秒重绘整列。 */
|
|
2661
2692
|
const RECENT_TIME_REFRESH_MS = 60_000;
|
|
2662
2693
|
/** 冷数据读取并发池上限:慢网下避免几十张卡片的 models/history 一次性挤占通道。 */
|
|
2663
2694
|
const LOAD_CONCURRENCY = 3;
|
|
@@ -2690,8 +2721,9 @@ const CSS = `
|
|
|
2690
2721
|
font-weight: 700;
|
|
2691
2722
|
letter-spacing: 0.02em;
|
|
2692
2723
|
}
|
|
2693
|
-
/*
|
|
2694
|
-
|
|
2724
|
+
/* 标题行拆为三部分:最左仓库入口独立区(T-144,与工具区按钮分处标题行两端)、左侧标题区
|
|
2725
|
+
(flex:1 占满剩余宽度)整体即收起控件,悬停/聚焦高亮只覆盖标题区
|
|
2726
|
+
(R-01-011/AC-03、AC-07、R-01-008/AC-02);右侧工具区独立,
|
|
2695
2727
|
不参与标题区的悬停高亮与折叠激活。 */
|
|
2696
2728
|
[data-dsh-activity-pane] .dap-titlebar {
|
|
2697
2729
|
flex: 1;
|
|
@@ -2830,7 +2862,11 @@ const CSS = `
|
|
|
2830
2862
|
[data-dsh-activity-pane] .dap-density:focus-visible {
|
|
2831
2863
|
background: #262932;
|
|
2832
2864
|
}
|
|
2833
|
-
/*
|
|
2865
|
+
/* 仓库入口移至标题行最左独立区(T-144,R-01-022/AC-01):与右侧工具区的档位切换按钮
|
|
2866
|
+
分处标题行两端以消除触屏误触;无描边(T-139),视觉强度弱于带描边的档位切换按钮,仅以不透明
|
|
2867
|
+
底色圆形呈现。原依赖 .dap-tools 的 padding 由自身 margin 承担(圆形底色盒不可用
|
|
2868
|
+
padding 扩容);左右 margin 各 12px——右缘与标题区的接缝间距对齐工具区的接缝间距
|
|
2869
|
+
(tools padding-left 12px),标题区悬停高亮两侧留白对称(T-146)。 */
|
|
2834
2870
|
[data-dsh-activity-pane] .dap-repo {
|
|
2835
2871
|
display: flex;
|
|
2836
2872
|
align-items: center;
|
|
@@ -2842,6 +2878,7 @@ const CSS = `
|
|
|
2842
2878
|
color: inherit;
|
|
2843
2879
|
cursor: pointer;
|
|
2844
2880
|
text-decoration: none;
|
|
2881
|
+
margin: 0 12px;
|
|
2845
2882
|
}
|
|
2846
2883
|
[data-dsh-activity-pane] .dap-repo:hover,
|
|
2847
2884
|
[data-dsh-activity-pane] .dap-repo:focus-visible {
|
|
@@ -3278,6 +3315,13 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3278
3315
|
[data-dsh-activity-pane] .dap-await-head {
|
|
3279
3316
|
display: flex; align-items: center; gap: 8px; align-self: stretch; min-width: 0;
|
|
3280
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; }
|
|
3281
3325
|
[data-dsh-activity-pane] .dap-note-row {
|
|
3282
3326
|
display: flex; align-items: center; gap: 6px; min-width: 0; align-self: stretch;
|
|
3283
3327
|
}
|
|
@@ -3549,8 +3593,8 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3549
3593
|
}
|
|
3550
3594
|
/* 移动端抽屉透明遮罩:抽屉打开时铺满视口、点击收起抽屉(R-01-008/AC-03)。
|
|
3551
3595
|
完全透明不占布局;z-index 介于主会话与抽屉(2147482990)之间;抽屉打开期间
|
|
3552
|
-
浮动开关隐藏(见 .dap-toggle[data-drawer-open],R-01-008/AC-05
|
|
3553
|
-
|
|
3596
|
+
浮动开关隐藏(见 .dap-toggle[data-drawer-open],R-01-008/AC-05),关闭由
|
|
3597
|
+
标题行激活(无独立 × 关闭按钮,T-137)与遮罩承担;桌面断点外由媒体查询保持隐藏。 */
|
|
3554
3598
|
.dap-backdrop {
|
|
3555
3599
|
position: fixed; inset: 0;
|
|
3556
3600
|
z-index: 2147482989;
|
|
@@ -3697,7 +3741,7 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-track {
|
|
|
3697
3741
|
body:not([data-ds-dark-theme]) .dap-toggle {
|
|
3698
3742
|
background: var(--dsw-alias-button-floating-fill, rgba(255, 255, 255, 0.94));
|
|
3699
3743
|
}
|
|
3700
|
-
/*
|
|
3744
|
+
/* 「回到顶部」与标题行档位按钮及左侧仓库入口的浅色覆盖:不透明层-2 底色与外壳描边别名
|
|
3701
3745
|
(R-01-018/AC-05、R-01-021/AC-05、R-01-022/AC-01);仓库入口无描边(T-139),只并入底色组。 */
|
|
3702
3746
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top,
|
|
3703
3747
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density,
|
|
@@ -3774,6 +3818,12 @@ function fmtRecentTime(ts, now = Date.now()) {
|
|
|
3774
3818
|
}
|
|
3775
3819
|
}
|
|
3776
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
|
+
|
|
3777
3827
|
/** 读取持久化列宽:缺失/非法/越界值经 clampPaneWidth 归一;localStorage 不可用(隐私模式)静默回退默认(R-01-015/AC-04)。 */
|
|
3778
3828
|
function readStoredPaneWidth() {
|
|
3779
3829
|
try {
|
|
@@ -4733,7 +4783,7 @@ function apply(ctx) {
|
|
|
4733
4783
|
const onTopClick = () => {
|
|
4734
4784
|
scroll?.scrollTo({ top: 0, behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
4735
4785
|
};
|
|
4736
|
-
// 显示档位循环切换(R-01-021/AC-01
|
|
4786
|
+
// 显示档位循环切换(R-01-021/AC-01):紧凑→中间→完整→紧凑,形态写窗格根属性
|
|
4737
4787
|
// 驱动纯 CSS 呈现,持久化于 localStorage(AC-06),会话状态变化不触碰已选档位(AC-07)。
|
|
4738
4788
|
const densityBtn = pane.querySelector(".dap-density");
|
|
4739
4789
|
const applyDensity = () => {
|
|
@@ -4805,14 +4855,14 @@ function apply(ctx) {
|
|
|
4805
4855
|
center.insertBefore(pane, seat);
|
|
4806
4856
|
pane.innerHTML = `
|
|
4807
4857
|
<div class="dap-header">
|
|
4858
|
+
<a class="dap-repo" href="https://github.com/ccll/dsh-activity-pane" target="_blank" rel="noreferrer noopener" aria-label="报告问题" title="报告问题,点赞收藏"></a>
|
|
4808
4859
|
<span class="dap-titlebar" role="button" tabindex="0" aria-expanded="true" aria-label="收起活动会话窗格" title="收起">
|
|
4809
4860
|
<span>活动会话</span>
|
|
4810
4861
|
<span class="dap-count" role="status" aria-live="polite"></span>
|
|
4811
4862
|
<span class="dap-collapse-hint" aria-hidden="true"></span>
|
|
4812
4863
|
</span>
|
|
4813
4864
|
<span class="dap-tools">
|
|
4814
|
-
<
|
|
4815
|
-
<button class="dap-density" type="button" aria-label="切换为紧凑显示" title="紧凑显示"></button>
|
|
4865
|
+
<button class="dap-density" type="button" aria-label="切换为完整显示" title="完整显示"></button>
|
|
4816
4866
|
</span>
|
|
4817
4867
|
</div>
|
|
4818
4868
|
<div class="dap-scroll">
|
|
@@ -4835,7 +4885,7 @@ function apply(ctx) {
|
|
|
4835
4885
|
// 紧凑显示切换按钮常显于标题行右侧工具区(R-01-021/AC-05);切换时以 data-density
|
|
4836
4886
|
// 驱动纯 CSS 呈现,骨架重建后由 bindPaneControls 的 applyDensity 恢复形态。
|
|
4837
4887
|
pane.querySelector(".dap-density").append(createDensityIcon());
|
|
4838
|
-
//
|
|
4888
|
+
// 仓库入口常显于标题行最左独立区(R-01-022/AC-01,T-144);图标在创建时注入。
|
|
4839
4889
|
pane.querySelector(".dap-repo").append(createRepoIcon());
|
|
4840
4890
|
// 收起方向图标为标题行悬停/聚焦的可见性提示(R-01-011/AC-07),图标在创建时注入。
|
|
4841
4891
|
pane.querySelector(".dap-collapse-hint").append(createCollapseIcon());
|
|
@@ -4897,9 +4947,12 @@ function apply(ctx) {
|
|
|
4897
4947
|
noteRow.querySelector(".dap-badge")?.remove();
|
|
4898
4948
|
const capsule = makeEl("div", "dap-capsule");
|
|
4899
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"));
|
|
4900
4953
|
const foot = makeEl("div", "dap-foot");
|
|
4901
4954
|
noteRow.replaceWith(foot);
|
|
4902
|
-
foot.append(
|
|
4955
|
+
foot.append(awaitHead, noteRow);
|
|
4903
4956
|
}
|
|
4904
4957
|
|
|
4905
4958
|
/** 进度行骨架(运行卡与子代理卡共用,R-01-009/AC-06、AC-14):可伸缩轨道 + 固定宽百分比。 */
|
|
@@ -4970,7 +5023,8 @@ function apply(ctx) {
|
|
|
4970
5023
|
const noteRow = makeEl("div", "dap-note-row");
|
|
4971
5024
|
noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
|
|
4972
5025
|
const awaitHead = makeEl("div", "dap-await-head");
|
|
4973
|
-
|
|
5026
|
+
// 状态年龄(R-01-002/AC-14):进入状态时刻的裸相对时间,胶囊右侧静态显示。
|
|
5027
|
+
awaitHead.append(capsule, makeEl("span", "dap-await-age"));
|
|
4974
5028
|
const foot = makeEl("div", "dap-foot");
|
|
4975
5029
|
foot.append(awaitHead, noteRow);
|
|
4976
5030
|
return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];
|
|
@@ -5042,7 +5096,7 @@ function apply(ctx) {
|
|
|
5042
5096
|
});
|
|
5043
5097
|
}
|
|
5044
5098
|
|
|
5045
|
-
/**
|
|
5099
|
+
/** 标题行左侧独立区的仓库入口图标(R-01-022/AC-01,T-144):GitHub Octicon `mark-github` 字形
|
|
5046
5100
|
* (MIT 许可,Primer Octicons),与工具区既有图标同用 14px 字形盒。 */
|
|
5047
5101
|
function createRepoIcon() {
|
|
5048
5102
|
return createInlineIcon({
|
|
@@ -5552,6 +5606,21 @@ function apply(ctx) {
|
|
|
5552
5606
|
iconHolder.dataset.kind = iconKind;
|
|
5553
5607
|
iconHolder.replaceChildren(...(iconKind === "" ? [] : [createCapsuleIcon(iconKind)]));
|
|
5554
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
|
+
}
|
|
5555
5624
|
}
|
|
5556
5625
|
|
|
5557
5626
|
if (entry.kind === "running") {
|
|
@@ -5716,7 +5785,7 @@ function apply(ctx) {
|
|
|
5716
5785
|
}
|
|
5717
5786
|
}
|
|
5718
5787
|
|
|
5719
|
-
/**
|
|
5788
|
+
/** 历史卡相对时间与等待卡状态年龄只需分钟级刷新;两者皆无时停止定时器,避免空窗格常驻唤醒。 */
|
|
5720
5789
|
function syncRecentTimeClock(wanted) {
|
|
5721
5790
|
if (wanted && recentTimeTimer === null) {
|
|
5722
5791
|
recentTimeTimer = setInterval(() => queueSync(), RECENT_TIME_REFRESH_MS);
|
|
@@ -6218,7 +6287,12 @@ function apply(ctx) {
|
|
|
6218
6287
|
for (const [id, state] of progressAnchorById) {
|
|
6219
6288
|
if (delegationActive(state, now)) delegatingIds.add(id);
|
|
6220
6289
|
}
|
|
6221
|
-
|
|
6290
|
+
// 排序键注入(契约见 buildEntries JSDoc):openWaitStart 已在 busy SSE 快照归一为毫秒数或 null。
|
|
6291
|
+
const waitingStarts = new Map();
|
|
6292
|
+
for (const [id, record] of busyById) {
|
|
6293
|
+
if (typeof record?.openWaitStart === "number") waitingStarts.set(String(id), record.openWaitStart);
|
|
6294
|
+
}
|
|
6295
|
+
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds, waitingStarts);
|
|
6222
6296
|
// 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
|
|
6223
6297
|
// 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
|
|
6224
6298
|
const runLikeIds = new Set(
|
|
@@ -6299,6 +6373,8 @@ function apply(ctx) {
|
|
|
6299
6373
|
if (entry.kind === "awaiting" && detail) {
|
|
6300
6374
|
entry.elapsedMs = memoTurnDuration(detail);
|
|
6301
6375
|
}
|
|
6376
|
+
// 状态年龄(R-01-002/AC-14):帧内单点派生一次,渲染与签名共用同一文案与时钟源。
|
|
6377
|
+
if (entry.kind === "awaiting") entry.awaitAge = awaitAgeText(entry, now);
|
|
6302
6378
|
if (detail?.model) {
|
|
6303
6379
|
entry.model = detail.model.model;
|
|
6304
6380
|
entry.reasoning = detail.model.reasoning;
|
|
@@ -6411,7 +6487,8 @@ function apply(ctx) {
|
|
|
6411
6487
|
} : null);
|
|
6412
6488
|
if (elapsedMs === null) recentDurationFallbackIds.add(entry.id);
|
|
6413
6489
|
}
|
|
6414
|
-
|
|
6490
|
+
// 状态年龄(R-01-002/AC-14)与历史卡相对时间同为分钟级:任一存在即保持定时器。
|
|
6491
|
+
syncRecentTimeClock(recent.length > 0 || active.some((entry) => entry.kind === "awaiting" && entry.awaitAge));
|
|
6415
6492
|
// 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
|
|
6416
6493
|
// 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
|
|
6417
6494
|
const previewFallbackIds = new Set();
|
|
@@ -6474,9 +6551,12 @@ function apply(ctx) {
|
|
|
6474
6551
|
// listState 参与签名:空列表从 pending/error → ready 时卡集合不变,若只比较卡片
|
|
6475
6552
|
// 会被提前返回冻结在「加载中」/「列表加载失败」;数量胶囊可见面变化同样需要
|
|
6476
6553
|
// 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
|
|
6477
|
-
//
|
|
6554
|
+
// 历史卡的相对活动时间与等待卡状态年龄随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
|
|
6478
6555
|
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
6479
|
-
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]);
|
|
6480
6560
|
if (sig === lastSig) return;
|
|
6481
6561
|
const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
|
|
6482
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>
|
|
@@ -36,7 +36,7 @@ The npm package ships prebuilt, so no local build step is needed. If the pane do
|
|
|
36
36
|
|
|
37
37
|
## Requirements
|
|
38
38
|
|
|
39
|
-
- DSH (DeepSeek Harness) web, tested against `@deepseek-ai/dsh@0.1.
|
|
39
|
+
- DSH (DeepSeek Harness) web, tested against `@deepseek-ai/dsh@0.1.5-rc.1`.
|
|
40
40
|
- No third-party plugin dependencies: the pane only consumes DSH's native session and workspace services, and uninstalling is fully reversible.
|
|
41
41
|
|
|
42
42
|
## Acknowledgments & Disclaimers
|
|
@@ -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>
|
|
@@ -36,7 +36,7 @@ npm 包内置预构建产物,无需本地构建步骤。安装后如窗格未
|
|
|
36
36
|
|
|
37
37
|
## 环境要求
|
|
38
38
|
|
|
39
|
-
- DSH (DeepSeek Harness) Web,经 `@deepseek-ai/dsh@0.1.
|
|
39
|
+
- DSH (DeepSeek Harness) Web,经 `@deepseek-ai/dsh@0.1.5-rc.1` 实测验证。
|
|
40
40
|
- 无第三方插件依赖:窗格只消费 DSH 原生会话与工作区服务,卸载可逆。
|
|
41
41
|
|
|
42
42
|
## 感谢与声明
|
|
@@ -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/acceptance.mjs
CHANGED
|
@@ -200,8 +200,8 @@ const steps = [
|
|
|
200
200
|
"准备至少 21 个已完成且已移入历史的主会话:首次打开历史区只显示按最近活动时间排序的 10 张卡,并在列表底部显示「加载更多...」按钮;将窗格滚动到底部,确认不会自动追加,点击按钮后才追加后 10 张且前 10 张保留、无重复;再次点击按钮确认只追加最后 1 张、按钮随后隐藏,继续滚动不再增加;追加过程中活动区仍与历史区互斥,窗格滚动不带动主会话页面,桌面与移动端抽屉均可正常继续浏览。另以少于 10 张历史会话确认首屏全部显示、不显示加载按钮且不会自动补页。",
|
|
201
201
|
// R-01-018/AC-02、AC-04、AC-05 观感残留(键盘/显隐/回顶/窄条/抽屉行为断言已迁 e2e/specs/back-to-top.mjs)
|
|
202
202
|
"确认平滑滚动动画流畅自然;切到 <=767px 移动视口打开抽屉,确认抽屉内按钮不遮挡标题行;深浅主题各验一次不透明底色与图标可辨。",
|
|
203
|
-
// R-01-011/AC-03
|
|
204
|
-
"
|
|
203
|
+
// R-01-011/AC-03 标题区整体收起(T-144:标题区界定不含最左仓库入口区)
|
|
204
|
+
"桌面宽度下点击「活动会话」标题区(标题与计数徽标一侧,不含最左仓库入口区与右侧工具区),确认窗格折叠为不占用主会话宽度的窄条;点击工具区切换按钮仅切换档位、不触发折叠;键盘聚焦标题区按 Enter/Space 亦可收起。",
|
|
205
205
|
// R-01-011/AC-04 窄条竖排标题整体展开
|
|
206
206
|
"折叠态下确认窄条以竖排显示「活动会话」标题与活动计数徽标,悬停窄条出现与展开态标题行对等的高亮反馈,点击窄条任意位置展开并恢复活动区+历史区显示。",
|
|
207
207
|
// R-01-011/AC-06 移动端标题行即收起抽屉
|
|
@@ -211,11 +211,11 @@ const steps = [
|
|
|
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 相对位置与图标辨识残留人工)
|
|
214
|
-
"
|
|
214
|
+
"连续点击「活动会话」标题行右侧工具区的切换按钮:确认卡片按紧凑→中间→完整→紧凑循环,中间档保留工作区徽标行、标题行、等待胶囊与正文(等待卡)或用户/助手预览行(历史卡)、隐藏时间线与统计;每次切换后当前选中卡片的顶部在屏幕上位置稳定不跳变;等待行动卡与运行卡的卡面类别底色在中间与紧凑下仍一眼可辨、与完整呈现一致,深浅主题各验一次。",
|
|
215
215
|
// R-01-021/AC-05 抽屉形态观感
|
|
216
216
|
"切到 <=767px 移动视口打开抽屉,确认切换按钮同样常显于抽屉内标题行右侧工具区,图标与命中区在触屏下可辨可用;折叠为桌面窄条后按钮不显示。",
|
|
217
217
|
// R-01-022/AC-01 仓库入口观感(链接目标/激活隔离/窄条隐藏 → e2e/specs/repo-entry.mjs)
|
|
218
|
-
"
|
|
218
|
+
"确认「活动会话」标题行最左侧独立区的 GitHub 图标按钮与右侧工具区的档位切换按钮分处标题行两端、无边框弱化呈现,与标题区的接缝间距和标题区与档位按钮的接缝间距对称(悬停高亮两侧留白对称、不贴任一按钮),悬停提示为「报告问题,点赞收藏」,深浅主题下图标与底色协调、命中区在触屏与鼠标下可辨可用;点击后在新标签页打开插件仓库页,原会话页保持不动(当前选中会话与呈现档位不变);切到 <=767px 移动视口打开抽屉,确认仓库入口同样常显于抽屉内标题行最左且点按档位按钮不致误触;桌面折叠为窄条后不显示。",
|
|
219
219
|
];
|
|
220
220
|
|
|
221
221
|
const line = "=".repeat(64);
|
package/scripts/check.mjs
CHANGED
|
@@ -876,26 +876,32 @@ assert.deepEqual(
|
|
|
876
876
|
"全运行中场景按宿主列表时间从新到旧;相同时间保持宿主列表出现顺序;缺失视为最旧;子代理不参与排序、始终跟随其母会话(R-01-001/AC-07)",
|
|
877
877
|
);
|
|
878
878
|
|
|
879
|
-
// ---- R-01-001/AC-07
|
|
879
|
+
// ---- R-01-001/AC-07 运行中置顶;等待/完成组按进入状态时刻倒序
|
|
880
|
+
// (阻塞等待取回合内等待边界开启时刻 T-141;完成/错误提醒取回合结束登记时刻) ----
|
|
880
881
|
const mixedWorkspace = [{ title: "Ops", path: "/srv/ops", sessionIds: ["sRunOld"] }];
|
|
881
882
|
const mixedActivity = {
|
|
882
883
|
ids: ["sDone1", "sRunOld", "sWait", "sDone2", "sWaitNoRec"],
|
|
883
884
|
byId: {
|
|
884
885
|
sDone1: { id: "sDone1", displayTitle: "完成一", running: false, updatedAt: 500 },
|
|
885
886
|
sRunOld: { id: "sRunOld", displayTitle: "运行旧指令", running: true, updatedAt: 1_000 },
|
|
886
|
-
|
|
887
|
+
// T-141 复现形态(docsim 实测):等待进行中的会话 lastTurnEnd 停留在上一回合
|
|
888
|
+
// (1_500,早于全部完成提醒),进入等待时刻(9_000)晚于全部完成提醒;宿主列表
|
|
889
|
+
// 时间(3_500)介于两个完成提醒之间,用于区分「用 openWaitStart」与「回落」两条路径。
|
|
890
|
+
sWait: { id: "sWait", displayTitle: "阻塞等待", running: false, pendingInteraction: "approval", updatedAt: 3_500 },
|
|
887
891
|
sDone2: { id: "sDone2", displayTitle: "完成二", running: false, updatedAt: 700 },
|
|
892
|
+
// 无等待记账记录的阻塞等待会话:回落宿主列表时间(最后用户指令时刻)。
|
|
888
893
|
sWaitNoRec: { id: "sWaitNoRec", displayTitle: "无登记等待", running: false, pendingInteraction: "question", updatedAt: 10_000 },
|
|
889
894
|
},
|
|
890
895
|
current: null,
|
|
891
896
|
};
|
|
892
897
|
const mixedCompletions = new Map([
|
|
893
898
|
["sDone1", { lastTurnEnd: 4_000, lastTurnEndKind: "completed", ackedAt: null }],
|
|
894
|
-
["sWait", { lastTurnEnd:
|
|
899
|
+
["sWait", { lastTurnEnd: 1_500, lastTurnEndKind: "blocked", ackedAt: null }],
|
|
895
900
|
["sDone2", { lastTurnEnd: 3_000, lastTurnEndKind: "completed", ackedAt: null }],
|
|
896
901
|
]);
|
|
902
|
+
const mixedWaitingStarts = new Map([["sWait", 9_000]]);
|
|
897
903
|
assert.deepEqual(
|
|
898
|
-
buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions).map((entry) => [entry.id, entry.kind]),
|
|
904
|
+
buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions, null, [], mixedWaitingStarts).map((entry) => [entry.id, entry.kind]),
|
|
899
905
|
[
|
|
900
906
|
["sRunOld", "running"],
|
|
901
907
|
["sWaitNoRec", "awaiting"],
|
|
@@ -903,7 +909,73 @@ assert.deepEqual(
|
|
|
903
909
|
["sDone1", "awaiting"],
|
|
904
910
|
["sDone2", "awaiting"],
|
|
905
911
|
],
|
|
906
|
-
"
|
|
912
|
+
"运行中置顶(即便其指令时间更旧);等待/完成组按进入状态时刻倒序——阻塞等待按等待边界开启时刻排前(不被上一回合旧登记压后),无记账回落宿主列表时间,完成/错误提醒按回合结束登记倒序(R-01-001/AC-07,T-141)",
|
|
913
|
+
);
|
|
914
|
+
// waitingStarts 缺失(旧调用/数据在途帧):阻塞等待会话回落宿主列表时间而非上一回合旧登记——
|
|
915
|
+
// sWait 键取 updatedAt=3_500,排在 sDone1(4_000) 之后、sDone2(3_000) 之前,不按 1_500 沉底。
|
|
916
|
+
assert.deepEqual(
|
|
917
|
+
buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions).map((entry) => [entry.id, entry.kind]),
|
|
918
|
+
[
|
|
919
|
+
["sRunOld", "running"],
|
|
920
|
+
["sWaitNoRec", "awaiting"],
|
|
921
|
+
["sDone1", "awaiting"],
|
|
922
|
+
["sWait", "awaiting"],
|
|
923
|
+
["sDone2", "awaiting"],
|
|
924
|
+
],
|
|
925
|
+
"waitingStarts 缺失时阻塞等待回落宿主列表时间(仍非上一回合旧登记),完成/错误提醒不受影响(R-01-001/AC-07,T-141)",
|
|
926
|
+
);
|
|
927
|
+
// 非 Map 入参视为无数据,与上一断言同路径。
|
|
928
|
+
assert.deepEqual(
|
|
929
|
+
buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions, null, [], "not-a-map").map((entry) => [entry.id, entry.kind]),
|
|
930
|
+
buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions).map((entry) => [entry.id, entry.kind]),
|
|
931
|
+
"waitingStarts 非 Map 视为无数据(R-01-001/AC-07,T-141)",
|
|
932
|
+
);
|
|
933
|
+
// 记录值非法(null/空串经 Number 归一为 0 的形状)同样按无数据处理,回落宿主列表时间而非最旧时刻 0。
|
|
934
|
+
assert.deepEqual(
|
|
935
|
+
buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions, null, [], new Map([["sWait", null], ["sWaitNoRec", ""]])).map((entry) => [entry.id, entry.kind]),
|
|
936
|
+
buildEntries(mixedActivity, mixedWorkspace, {}, mixedCompletions).map((entry) => [entry.id, entry.kind]),
|
|
937
|
+
"waitingStarts 非法值(null/空串)不作数、回落宿主列表时间,不误判为最旧时刻 0(R-01-001/AC-07,T-141 复审)",
|
|
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)",
|
|
907
979
|
);
|
|
908
980
|
assert.deepEqual(trackRuns([{ kind: "subagent", parentId: "p", depth: 1 }]), [], "无 id 条目不产生轨道");
|
|
909
981
|
assert.deepEqual(
|
|
@@ -3442,10 +3514,10 @@ assert.equal(normalizeDensity("full"), "full", "完整持久化值恢复为完
|
|
|
3442
3514
|
assert.equal(normalizeDensity(null), "medium", "无持久化记录回退默认中间档");
|
|
3443
3515
|
assert.equal(normalizeDensity(""), "medium", "空串回退默认中间档");
|
|
3444
3516
|
assert.equal(normalizeDensity("abc"), "medium", "非法持久化值回退默认中间档");
|
|
3445
|
-
assert.equal(nextDensity("full"), "
|
|
3446
|
-
assert.equal(nextDensity("medium"), "
|
|
3447
|
-
assert.equal(nextDensity("compact"), "
|
|
3448
|
-
assert.equal(nextDensity("junk"), "
|
|
3517
|
+
assert.equal(nextDensity("full"), "compact", "完整档的下一档为紧凑——循环自小到大(R-01-021/AC-01)");
|
|
3518
|
+
assert.equal(nextDensity("medium"), "full", "中间档的下一档为完整(R-01-021/AC-01)");
|
|
3519
|
+
assert.equal(nextDensity("compact"), "medium", "紧凑档的下一档为中间(R-01-021/AC-01)");
|
|
3520
|
+
assert.equal(nextDensity("junk"), "full", "非法值经归一视作默认中间档再循环(R-01-021/AC-01)");
|
|
3449
3521
|
|
|
3450
3522
|
// ---- 重建 client bundle 并校验产物契约 ----
|
|
3451
3523
|
await mkdir(join(root, ".dsh-plugin"), { recursive: true });
|
|
@@ -3539,20 +3611,20 @@ assert.ok(
|
|
|
3539
3611
|
bundle.includes('[data-dsh-activity-pane] .dap-density {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 22px;'),
|
|
3540
3612
|
"切换按钮常显于标题行右侧工具区(R-01-021/AC-05)",
|
|
3541
3613
|
);
|
|
3542
|
-
// R-01-022/AC-01
|
|
3614
|
+
// R-01-022/AC-01 标题行左侧独立区仓库入口(GitHub 链接、新标签页打开)
|
|
3543
3615
|
assert.ok(
|
|
3544
3616
|
bundle.includes('<a class="dap-repo" href="https://github.com/ccll/dsh-activity-pane" target="_blank" rel="noreferrer noopener"'),
|
|
3545
|
-
"
|
|
3617
|
+
"仓库入口随窗格骨架创建于标题行左侧独立区,指向 GitHub 仓库页并以 target=_blank 与 noreferrer noopener 在新标签页打开(R-01-022/AC-01)",
|
|
3546
3618
|
);
|
|
3547
3619
|
assert.ok(
|
|
3548
3620
|
bundle.includes('[data-dsh-activity-pane] .dap-repo {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border-radius: 999px;\n background: #1d1f25;'),
|
|
3549
|
-
"
|
|
3621
|
+
"仓库入口与档位切换按钮同视觉语言且无边框(22px 圆形纯图标、视觉强度弱于带描边的档位切换按钮,R-01-022/AC-01、T-139)",
|
|
3550
3622
|
);
|
|
3551
|
-
// T-
|
|
3623
|
+
// T-144:仓库入口移至标题行最左独立区、与档位切换按钮分处标题行两端——模板顺序上仓库入口最先出现。
|
|
3552
3624
|
assert.ok(
|
|
3553
|
-
bundle.includes('aria-label="报告问题" title="
|
|
3625
|
+
bundle.includes('aria-label="报告问题" title="报告问题,点赞收藏"')
|
|
3554
3626
|
&& bundle.indexOf('<a class="dap-repo"') < bundle.indexOf('<button class="dap-density"'),
|
|
3555
|
-
"
|
|
3627
|
+
"仓库入口位于标题行最左独立区、与档位切换按钮分处标题行两端且悬停提示为「报告问题,点赞收藏」(T-139、T-144、T-145)",
|
|
3556
3628
|
);
|
|
3557
3629
|
// R-01-015/AC-03 折叠窄条与移动端抽屉不提供拖拽
|
|
3558
3630
|
assert.ok(bundle.includes('[data-collapsed="true"] .dap-resize { display: none; }'), "折叠窄条不提供拖拽调宽");
|
|
@@ -3566,8 +3638,8 @@ assert.ok(bundle.includes("notifyLayoutChange"), "布局变化通知 sibling ove
|
|
|
3566
3638
|
assert.ok(bundle.includes('window.dispatchEvent(new Event("resize"))'), "布局变化派发标准 resize 通知");
|
|
3567
3639
|
assert.ok(bundle.includes("pane !== renderedPane"), "新窗格实例必须重置渲染签名");
|
|
3568
3640
|
assert.ok(
|
|
3569
|
-
clientSource.includes("const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature, densityLevel]);"),
|
|
3570
|
-
"列表 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)",
|
|
3571
3643
|
);
|
|
3572
3644
|
assert.ok(
|
|
3573
3645
|
clientSource.includes("scroll?.querySelector?.(`.${LIST_CLASS} .${CARD_CLASS}[data-current]`)") &&
|
|
@@ -3649,8 +3721,8 @@ assert.ok(
|
|
|
3649
3721
|
"等待卡复用统计行并清理胶囊同行的旧耗时节点(R-01-009/AC-12、AC-13)",
|
|
3650
3722
|
);
|
|
3651
3723
|
assert.ok(
|
|
3652
|
-
bundle.includes("function removeAwaitingHeadDuration") && bundle.includes(
|
|
3653
|
-
"
|
|
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)",
|
|
3654
3726
|
);
|
|
3655
3727
|
assert.ok(bundle.includes("lastTurnDuration({"), "等待卡耗时由最近完整回合边界派生(R-01-009/AC-12)");
|
|
3656
3728
|
assert.ok(bundle.includes("`输入 ${fmtTokens("), "统计行含输入/输出中文短标签(R-01-009/AC-05)");
|
|
@@ -3737,9 +3809,9 @@ assert.ok(
|
|
|
3737
3809
|
assert.ok(bundle.includes("function activeSessionIds(byId = {})"), "活动子代理沿 parentId 链补齐活动祖先");
|
|
3738
3810
|
// ---- R-01-016/AC-01 等待卡保留最近工作项时间线 ----
|
|
3739
3811
|
assert.ok(
|
|
3740
|
-
bundle.includes(
|
|
3812
|
+
bundle.includes('awaitHead.append(capsule, makeEl("span", "dap-await-age"));') &&
|
|
3741
3813
|
bundle.includes('return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];'),
|
|
3742
|
-
"awaiting
|
|
3814
|
+
"awaiting 骨架在标题行与统计行、末行两段(胶囊+正文)之间含时间线,末行首行胶囊右侧携带状态年龄(R-01-002/AC-14),固定耗时由统计行承载(R-01-016/AC-01、R-01-009/AC-12,C-043)",
|
|
3743
3815
|
);
|
|
3744
3816
|
// ---- R-01-002/AC-10 完成提醒卡「移入历史」按钮 ----
|
|
3745
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;
|
|
@@ -78,8 +78,9 @@ const CSS = `
|
|
|
78
78
|
font-weight: 700;
|
|
79
79
|
letter-spacing: 0.02em;
|
|
80
80
|
}
|
|
81
|
-
/*
|
|
82
|
-
|
|
81
|
+
/* 标题行拆为三部分:最左仓库入口独立区(T-144,与工具区按钮分处标题行两端)、左侧标题区
|
|
82
|
+
(flex:1 占满剩余宽度)整体即收起控件,悬停/聚焦高亮只覆盖标题区
|
|
83
|
+
(R-01-011/AC-03、AC-07、R-01-008/AC-02);右侧工具区独立,
|
|
83
84
|
不参与标题区的悬停高亮与折叠激活。 */
|
|
84
85
|
[data-dsh-activity-pane] .dap-titlebar {
|
|
85
86
|
flex: 1;
|
|
@@ -218,7 +219,11 @@ const CSS = `
|
|
|
218
219
|
[data-dsh-activity-pane] .dap-density:focus-visible {
|
|
219
220
|
background: #262932;
|
|
220
221
|
}
|
|
221
|
-
/*
|
|
222
|
+
/* 仓库入口移至标题行最左独立区(T-144,R-01-022/AC-01):与右侧工具区的档位切换按钮
|
|
223
|
+
分处标题行两端以消除触屏误触;无描边(T-139),视觉强度弱于带描边的档位切换按钮,仅以不透明
|
|
224
|
+
底色圆形呈现。原依赖 .dap-tools 的 padding 由自身 margin 承担(圆形底色盒不可用
|
|
225
|
+
padding 扩容);左右 margin 各 12px——右缘与标题区的接缝间距对齐工具区的接缝间距
|
|
226
|
+
(tools padding-left 12px),标题区悬停高亮两侧留白对称(T-146)。 */
|
|
222
227
|
[data-dsh-activity-pane] .dap-repo {
|
|
223
228
|
display: flex;
|
|
224
229
|
align-items: center;
|
|
@@ -230,6 +235,7 @@ const CSS = `
|
|
|
230
235
|
color: inherit;
|
|
231
236
|
cursor: pointer;
|
|
232
237
|
text-decoration: none;
|
|
238
|
+
margin: 0 12px;
|
|
233
239
|
}
|
|
234
240
|
[data-dsh-activity-pane] .dap-repo:hover,
|
|
235
241
|
[data-dsh-activity-pane] .dap-repo:focus-visible {
|
|
@@ -666,6 +672,13 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
666
672
|
[data-dsh-activity-pane] .dap-await-head {
|
|
667
673
|
display: flex; align-items: center; gap: 8px; align-self: stretch; min-width: 0;
|
|
668
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; }
|
|
669
682
|
[data-dsh-activity-pane] .dap-note-row {
|
|
670
683
|
display: flex; align-items: center; gap: 6px; min-width: 0; align-self: stretch;
|
|
671
684
|
}
|
|
@@ -937,8 +950,8 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
937
950
|
}
|
|
938
951
|
/* 移动端抽屉透明遮罩:抽屉打开时铺满视口、点击收起抽屉(R-01-008/AC-03)。
|
|
939
952
|
完全透明不占布局;z-index 介于主会话与抽屉(2147482990)之间;抽屉打开期间
|
|
940
|
-
浮动开关隐藏(见 .dap-toggle[data-drawer-open],R-01-008/AC-05
|
|
941
|
-
|
|
953
|
+
浮动开关隐藏(见 .dap-toggle[data-drawer-open],R-01-008/AC-05),关闭由
|
|
954
|
+
标题行激活(无独立 × 关闭按钮,T-137)与遮罩承担;桌面断点外由媒体查询保持隐藏。 */
|
|
942
955
|
.dap-backdrop {
|
|
943
956
|
position: fixed; inset: 0;
|
|
944
957
|
z-index: 2147482989;
|
|
@@ -1085,7 +1098,7 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-track {
|
|
|
1085
1098
|
body:not([data-ds-dark-theme]) .dap-toggle {
|
|
1086
1099
|
background: var(--dsw-alias-button-floating-fill, rgba(255, 255, 255, 0.94));
|
|
1087
1100
|
}
|
|
1088
|
-
/*
|
|
1101
|
+
/* 「回到顶部」与标题行档位按钮及左侧仓库入口的浅色覆盖:不透明层-2 底色与外壳描边别名
|
|
1089
1102
|
(R-01-018/AC-05、R-01-021/AC-05、R-01-022/AC-01);仓库入口无描边(T-139),只并入底色组。 */
|
|
1090
1103
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top,
|
|
1091
1104
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-density,
|
|
@@ -1162,6 +1175,12 @@ function fmtRecentTime(ts, now = Date.now()) {
|
|
|
1162
1175
|
}
|
|
1163
1176
|
}
|
|
1164
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
|
+
|
|
1165
1184
|
/** 读取持久化列宽:缺失/非法/越界值经 clampPaneWidth 归一;localStorage 不可用(隐私模式)静默回退默认(R-01-015/AC-04)。 */
|
|
1166
1185
|
function readStoredPaneWidth() {
|
|
1167
1186
|
try {
|
|
@@ -2121,7 +2140,7 @@ function apply(ctx) {
|
|
|
2121
2140
|
const onTopClick = () => {
|
|
2122
2141
|
scroll?.scrollTo({ top: 0, behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
2123
2142
|
};
|
|
2124
|
-
// 显示档位循环切换(R-01-021/AC-01
|
|
2143
|
+
// 显示档位循环切换(R-01-021/AC-01):紧凑→中间→完整→紧凑,形态写窗格根属性
|
|
2125
2144
|
// 驱动纯 CSS 呈现,持久化于 localStorage(AC-06),会话状态变化不触碰已选档位(AC-07)。
|
|
2126
2145
|
const densityBtn = pane.querySelector(".dap-density");
|
|
2127
2146
|
const applyDensity = () => {
|
|
@@ -2193,14 +2212,14 @@ function apply(ctx) {
|
|
|
2193
2212
|
center.insertBefore(pane, seat);
|
|
2194
2213
|
pane.innerHTML = `
|
|
2195
2214
|
<div class="dap-header">
|
|
2215
|
+
<a class="dap-repo" href="https://github.com/ccll/dsh-activity-pane" target="_blank" rel="noreferrer noopener" aria-label="报告问题" title="报告问题,点赞收藏"></a>
|
|
2196
2216
|
<span class="dap-titlebar" role="button" tabindex="0" aria-expanded="true" aria-label="收起活动会话窗格" title="收起">
|
|
2197
2217
|
<span>活动会话</span>
|
|
2198
2218
|
<span class="dap-count" role="status" aria-live="polite"></span>
|
|
2199
2219
|
<span class="dap-collapse-hint" aria-hidden="true"></span>
|
|
2200
2220
|
</span>
|
|
2201
2221
|
<span class="dap-tools">
|
|
2202
|
-
<
|
|
2203
|
-
<button class="dap-density" type="button" aria-label="切换为紧凑显示" title="紧凑显示"></button>
|
|
2222
|
+
<button class="dap-density" type="button" aria-label="切换为完整显示" title="完整显示"></button>
|
|
2204
2223
|
</span>
|
|
2205
2224
|
</div>
|
|
2206
2225
|
<div class="dap-scroll">
|
|
@@ -2223,7 +2242,7 @@ function apply(ctx) {
|
|
|
2223
2242
|
// 紧凑显示切换按钮常显于标题行右侧工具区(R-01-021/AC-05);切换时以 data-density
|
|
2224
2243
|
// 驱动纯 CSS 呈现,骨架重建后由 bindPaneControls 的 applyDensity 恢复形态。
|
|
2225
2244
|
pane.querySelector(".dap-density").append(createDensityIcon());
|
|
2226
|
-
//
|
|
2245
|
+
// 仓库入口常显于标题行最左独立区(R-01-022/AC-01,T-144);图标在创建时注入。
|
|
2227
2246
|
pane.querySelector(".dap-repo").append(createRepoIcon());
|
|
2228
2247
|
// 收起方向图标为标题行悬停/聚焦的可见性提示(R-01-011/AC-07),图标在创建时注入。
|
|
2229
2248
|
pane.querySelector(".dap-collapse-hint").append(createCollapseIcon());
|
|
@@ -2285,9 +2304,12 @@ function apply(ctx) {
|
|
|
2285
2304
|
noteRow.querySelector(".dap-badge")?.remove();
|
|
2286
2305
|
const capsule = makeEl("div", "dap-capsule");
|
|
2287
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"));
|
|
2288
2310
|
const foot = makeEl("div", "dap-foot");
|
|
2289
2311
|
noteRow.replaceWith(foot);
|
|
2290
|
-
foot.append(
|
|
2312
|
+
foot.append(awaitHead, noteRow);
|
|
2291
2313
|
}
|
|
2292
2314
|
|
|
2293
2315
|
/** 进度行骨架(运行卡与子代理卡共用,R-01-009/AC-06、AC-14):可伸缩轨道 + 固定宽百分比。 */
|
|
@@ -2358,7 +2380,8 @@ function apply(ctx) {
|
|
|
2358
2380
|
const noteRow = makeEl("div", "dap-note-row");
|
|
2359
2381
|
noteRow.append(makeEl("div", "dap-note"), makeConfirmButton());
|
|
2360
2382
|
const awaitHead = makeEl("div", "dap-await-head");
|
|
2361
|
-
|
|
2383
|
+
// 状态年龄(R-01-002/AC-14):进入状态时刻的裸相对时间,胶囊右侧静态显示。
|
|
2384
|
+
awaitHead.append(capsule, makeEl("span", "dap-await-age"));
|
|
2362
2385
|
const foot = makeEl("div", "dap-foot");
|
|
2363
2386
|
foot.append(awaitHead, noteRow);
|
|
2364
2387
|
return [head, row, makeEl("div", "dap-trace"), makeStatsRow(), foot];
|
|
@@ -2430,7 +2453,7 @@ function apply(ctx) {
|
|
|
2430
2453
|
});
|
|
2431
2454
|
}
|
|
2432
2455
|
|
|
2433
|
-
/**
|
|
2456
|
+
/** 标题行左侧独立区的仓库入口图标(R-01-022/AC-01,T-144):GitHub Octicon `mark-github` 字形
|
|
2434
2457
|
* (MIT 许可,Primer Octicons),与工具区既有图标同用 14px 字形盒。 */
|
|
2435
2458
|
function createRepoIcon() {
|
|
2436
2459
|
return createInlineIcon({
|
|
@@ -2940,6 +2963,21 @@ function apply(ctx) {
|
|
|
2940
2963
|
iconHolder.dataset.kind = iconKind;
|
|
2941
2964
|
iconHolder.replaceChildren(...(iconKind === "" ? [] : [createCapsuleIcon(iconKind)]));
|
|
2942
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
|
+
}
|
|
2943
2981
|
}
|
|
2944
2982
|
|
|
2945
2983
|
if (entry.kind === "running") {
|
|
@@ -3104,7 +3142,7 @@ function apply(ctx) {
|
|
|
3104
3142
|
}
|
|
3105
3143
|
}
|
|
3106
3144
|
|
|
3107
|
-
/**
|
|
3145
|
+
/** 历史卡相对时间与等待卡状态年龄只需分钟级刷新;两者皆无时停止定时器,避免空窗格常驻唤醒。 */
|
|
3108
3146
|
function syncRecentTimeClock(wanted) {
|
|
3109
3147
|
if (wanted && recentTimeTimer === null) {
|
|
3110
3148
|
recentTimeTimer = setInterval(() => queueSync(), RECENT_TIME_REFRESH_MS);
|
|
@@ -3606,7 +3644,12 @@ function apply(ctx) {
|
|
|
3606
3644
|
for (const [id, state] of progressAnchorById) {
|
|
3607
3645
|
if (delegationActive(state, now)) delegatingIds.add(id);
|
|
3608
3646
|
}
|
|
3609
|
-
|
|
3647
|
+
// 排序键注入(契约见 buildEntries JSDoc):openWaitStart 已在 busy SSE 快照归一为毫秒数或 null。
|
|
3648
|
+
const waitingStarts = new Map();
|
|
3649
|
+
for (const [id, record] of busyById) {
|
|
3650
|
+
if (typeof record?.openWaitStart === "number") waitingStarts.set(String(id), record.openWaitStart);
|
|
3651
|
+
}
|
|
3652
|
+
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds, waitingStarts);
|
|
3610
3653
|
// 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
|
|
3611
3654
|
// 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
|
|
3612
3655
|
const runLikeIds = new Set(
|
|
@@ -3687,6 +3730,8 @@ function apply(ctx) {
|
|
|
3687
3730
|
if (entry.kind === "awaiting" && detail) {
|
|
3688
3731
|
entry.elapsedMs = memoTurnDuration(detail);
|
|
3689
3732
|
}
|
|
3733
|
+
// 状态年龄(R-01-002/AC-14):帧内单点派生一次,渲染与签名共用同一文案与时钟源。
|
|
3734
|
+
if (entry.kind === "awaiting") entry.awaitAge = awaitAgeText(entry, now);
|
|
3690
3735
|
if (detail?.model) {
|
|
3691
3736
|
entry.model = detail.model.model;
|
|
3692
3737
|
entry.reasoning = detail.model.reasoning;
|
|
@@ -3799,7 +3844,8 @@ function apply(ctx) {
|
|
|
3799
3844
|
} : null);
|
|
3800
3845
|
if (elapsedMs === null) recentDurationFallbackIds.add(entry.id);
|
|
3801
3846
|
}
|
|
3802
|
-
|
|
3847
|
+
// 状态年龄(R-01-002/AC-14)与历史卡相对时间同为分钟级:任一存在即保持定时器。
|
|
3848
|
+
syncRecentTimeClock(recent.length > 0 || active.some((entry) => entry.kind === "awaiting" && entry.awaitAge));
|
|
3803
3849
|
// 预览只对当前显示的 recent 卡计算(活动卡不显示预览);快照/历史引用不变时命中缓存。
|
|
3804
3850
|
// 完成瞬间的窗口快照可能先有用户消息、后到 agent reply;缺任一预览时补读一次 history。
|
|
3805
3851
|
const previewFallbackIds = new Set();
|
|
@@ -3862,9 +3908,12 @@ function apply(ctx) {
|
|
|
3862
3908
|
// listState 参与签名:空列表从 pending/error → ready 时卡集合不变,若只比较卡片
|
|
3863
3909
|
// 会被提前返回冻结在「加载中」/「列表加载失败」;数量胶囊可见面变化同样需要
|
|
3864
3910
|
// 进入渲染,以便与当前可见等待卡末行重新对相(R-01-002/AC-07)。
|
|
3865
|
-
//
|
|
3911
|
+
// 历史卡的相对活动时间与等待卡状态年龄随分钟级时钟变化,纳入签名后只在文案实际变化时重绘。
|
|
3866
3912
|
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
3867
|
-
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]);
|
|
3868
3917
|
if (sig === lastSig) return;
|
|
3869
3918
|
const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
|
|
3870
3919
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
package/src/core.mjs
CHANGED
|
@@ -132,11 +132,11 @@ export function normalizeDensity(raw) {
|
|
|
132
132
|
return raw === "compact" || raw === "medium" || raw === "full" ? raw : "medium";
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/**
|
|
136
|
-
const DENSITY_ORDER = ["
|
|
135
|
+
/** 显示档位的循环次序:紧凑 → 中间 → 完整 → 紧凑——信息量自小到大(R-01-021/AC-01)。 */
|
|
136
|
+
const DENSITY_ORDER = ["compact", "medium", "full"];
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
139
|
+
* 返回循环切换后的下一显示档位:紧凑 → 中间 → 完整 → 紧凑;
|
|
140
140
|
* 输入先经 normalizeDensity 归一,非法值视作中间档(R-01-021/AC-01)。
|
|
141
141
|
*/
|
|
142
142
|
export function nextDensity(value) {
|
|
@@ -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'(等待用户行动)
|
|
@@ -1547,8 +1547,16 @@ export function mainTitle(byId, id) {
|
|
|
1547
1547
|
* 始终为当帧原始后代活性(供进度锚点记账判定耗尽),不受 delegatingIds 影响。
|
|
1548
1548
|
* archivedIds(工作区服务的注册表全局归档集合):归档会话及其有效子代理子树不产出活动条目,
|
|
1549
1549
|
* 普通 fork 仅因共享来源 parentId 不受影响;避免会话服务仍保留旧行或完成提醒时在窗格中滞留。
|
|
1550
|
+
* waitingStarts(进入阻塞等待的时刻,R-01-001/AC-07):Map id → 毫秒时刻,取宿主回合记账的
|
|
1551
|
+
* `openWaitStart`(approval/asked 与 ask_user_question tool/call 边界);阻塞等待发生时本回合
|
|
1552
|
+
* 尚未结束、`lastTurnEnd` 仍是上一回合的旧时刻,不能作为等待进行中会话的排序键(T-141);
|
|
1553
|
+
* 非 Map、缺失记录或值非有限数字(含 null/空串等 Number 归一为 0 的形状)均视为无数据,
|
|
1554
|
+
* 回落宿主列表时间。
|
|
1555
|
+
* stateAt(进入当前等待行动状态的时刻,R-01-002/AC-14):仅 awaiting 条目携带,与排序键
|
|
1556
|
+
* 共用 enterStateAt 单点口径——pending 取 waitingStarts,完成/错误提醒取 completions.lastTurnEnd;
|
|
1557
|
+
* 显示侧不回落宿主列表时间——排序键缺失时的回落仅用于排序,显示以 null(不显示)承载。
|
|
1550
1558
|
*/
|
|
1551
|
-
export function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = []) {
|
|
1559
|
+
export function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null) {
|
|
1552
1560
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1553
1561
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1554
1562
|
const current = snapshot?.current ?? null;
|
|
@@ -1601,19 +1609,35 @@ export function buildEntries(snapshot, workspaceItems, detailsById = {}, complet
|
|
|
1601
1609
|
meta.set(id, { row, running, pending, isSub, show, done, err, descendantActive, delegating, depth: 0 });
|
|
1602
1610
|
}
|
|
1603
1611
|
|
|
1604
|
-
// 主会话分两组排序(R-01-001/AC-07
|
|
1605
|
-
//
|
|
1606
|
-
//
|
|
1607
|
-
// 两组相同时间均回落宿主列表出现顺序。工作区顺序不参与排序,仅承载卡片徽标与名称。
|
|
1612
|
+
// 主会话分两组排序(R-01-001/AC-07,键口径契约详见上方 JSDoc):运行中主会话置顶、
|
|
1613
|
+
// 组内按宿主列表时间从新到旧;等待/完成组排后、组内按进入状态时刻从新到旧;
|
|
1614
|
+
// 两组相同时间均回落宿主列表出现顺序,工作区顺序不参与排序。
|
|
1608
1615
|
const isRunningEntry = (id) => {
|
|
1609
1616
|
const m = meta.get(id);
|
|
1610
1617
|
return m !== undefined && !m.pending && (m.running || m.delegating);
|
|
1611
1618
|
};
|
|
1619
|
+
const waitingStartTime = (id) => {
|
|
1620
|
+
if (!(waitingStarts instanceof Map)) return null;
|
|
1621
|
+
const value = waitingStarts.get(String(id));
|
|
1622
|
+
// 仅接受真实数字时刻:Number(null)/Number("") 均为 0,会把「无数据」误判为
|
|
1623
|
+
// epoch 最旧时刻(与 normalizeBusyMs 的空值防护同理),一律回落宿主列表时间。
|
|
1624
|
+
if (typeof value !== "number") return null;
|
|
1625
|
+
return Number.isFinite(value) ? value : null;
|
|
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
|
+
};
|
|
1612
1635
|
const sortTime = (id) => {
|
|
1613
1636
|
if (isRunningEntry(id)) return instructionTime(byId[id]);
|
|
1614
|
-
const
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1637
|
+
const m = meta.get(id);
|
|
1638
|
+
const entered = m !== undefined ? enterStateAt(id, m.pending) : null;
|
|
1639
|
+
// 排序键缺失回落宿主列表时间;回落仅用于排序,显示侧以 null 承载(见 stateAt)。
|
|
1640
|
+
return entered ?? instructionTime(byId[id]);
|
|
1617
1641
|
};
|
|
1618
1642
|
rootIds.sort((a, b) => {
|
|
1619
1643
|
const byGroup = Number(isRunningEntry(b)) - Number(isRunningEntry(a));
|
|
@@ -1647,6 +1671,10 @@ export function buildEntries(snapshot, workspaceItems, detailsById = {}, complet
|
|
|
1647
1671
|
const doneWait = !m.pending && m.done && !m.running && !m.delegating;
|
|
1648
1672
|
const errWait = !m.pending && m.err && !m.running && !m.delegating;
|
|
1649
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;
|
|
1650
1678
|
const questionPreview =
|
|
1651
1679
|
m.pending && m.row.pendingInteraction === "question" ? timelineQuestionPreview(timeline) : undefined;
|
|
1652
1680
|
entries.push({
|
|
@@ -1691,6 +1719,7 @@ export function buildEntries(snapshot, workspaceItems, detailsById = {}, complet
|
|
|
1691
1719
|
: doneWait
|
|
1692
1720
|
? ROUND_DONE_NOTE
|
|
1693
1721
|
: undefined,
|
|
1722
|
+
stateAt,
|
|
1694
1723
|
questionPreview: m.pending && m.row.pendingInteraction === "question" ? (questionPreview ?? null) : undefined,
|
|
1695
1724
|
});
|
|
1696
1725
|
}
|
|
@@ -1785,6 +1814,8 @@ export function cardSignature(entries) {
|
|
|
1785
1814
|
entry.waitClass ?? null,
|
|
1786
1815
|
entry.noteText ?? null,
|
|
1787
1816
|
entry.questionPreview ?? null,
|
|
1817
|
+
// 进入状态时刻参与签名(R-01-002/AC-14):数据到达/更替(回填、SSE)驱动重绘。
|
|
1818
|
+
entry.stateAt ?? null,
|
|
1788
1819
|
entry.activityAt ?? null,
|
|
1789
1820
|
entry.progress ?? null,
|
|
1790
1821
|
entry.loadingModel ?? null,
|