dsh-activity-pane 0.4.0 → 0.7.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 +287 -62
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance.mjs +4 -2
- package/scripts/check.mjs +274 -53
- package/src/client.mjs +123 -26
- package/src/core.mjs +127 -36
- package/src/navigation.mjs +37 -0
package/.dsh-plugin/client.js
CHANGED
|
@@ -9,7 +9,7 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
//
|
|
10
10
|
// 数据源约定(来自 @deepseek-ai/dsh-client-runtime 的 sessions.list 快照):
|
|
11
11
|
// ids: SessionId[] —— 宿主列表顺序(祖先在前的 lineage 顺序)
|
|
12
|
-
// byId[id]: { id, displayTitle, title?, cwd?, parentId?, running, completed?,
|
|
12
|
+
// byId[id]: { id, displayTitle, title?, cwd?, parentId?, origin?, running, completed?,
|
|
13
13
|
// pendingInteraction?: 'approval'|'plan-review'|'question', blank, ... }
|
|
14
14
|
// current: SessionId | undefined
|
|
15
15
|
// subagentsByParent: { [parentId]: { entries: [{ id, label, ... }] } }
|
|
@@ -17,8 +17,8 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
// 以及 workspaces.list 的 items: [{ title, path, sessionIds }]。
|
|
18
18
|
//
|
|
19
19
|
// 展示规则:
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// 主会话(非有效子代理行):running || completed || pendingInteraction 都显示;
|
|
21
|
+
// 子代理(origin === 'subagent' 且 parentId 有效):仅 running || pendingInteraction 时显示(结束后即消失);
|
|
22
22
|
// pendingInteraction 总是优先视为"等待用户行动"(即使在 running 中)。
|
|
23
23
|
|
|
24
24
|
const PENDING_LABELS = {
|
|
@@ -1281,11 +1281,11 @@ function workspaceInfoForSession(sessionId, workspaceItems, byId = {}) {
|
|
|
1281
1281
|
}
|
|
1282
1282
|
|
|
1283
1283
|
/**
|
|
1284
|
-
*
|
|
1284
|
+
* 工作区徽标基色色相(R-01-003/AC-08、AC-09):以工作区身份为唯一输入的纯函数——
|
|
1285
1285
|
* djb2 哈希经雪崩终混(异或右移 + 乘法,把高位熵折入低位,消除 djb2 低位分布
|
|
1286
1286
|
* 聚集;每步 >>> 0 保持无符号,异或结果可能带符号位)后在避开红色警戒区的
|
|
1287
1287
|
* 色相弧 [30°,320°] 上均匀取色(30 + hash % 291),输出 [30,320] 整数。
|
|
1288
|
-
*
|
|
1288
|
+
* 同一身份恒得同一基色色相,与工作区列表顺序、会话状态及持久化存储无关,页面
|
|
1289
1289
|
* 刷新后不变;空身份返回 null。
|
|
1290
1290
|
*/
|
|
1291
1291
|
function workspaceHue(key) {
|
|
@@ -1304,34 +1304,99 @@ function workspaceHue(key) {
|
|
|
1304
1304
|
return 30 + (hash % 291);
|
|
1305
1305
|
}
|
|
1306
1306
|
|
|
1307
|
-
const
|
|
1307
|
+
const WORKSPACE_COLOR_SLOTS = Object.freeze([
|
|
1308
|
+
{ slot: 0, hue: 55, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1309
|
+
{ slot: 1, hue: 100, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1310
|
+
{ slot: 2, hue: 145, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1311
|
+
{ slot: 3, hue: 190, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1312
|
+
{ slot: 4, hue: 235, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1313
|
+
{ slot: 5, hue: 280, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1314
|
+
{ slot: 6, hue: 325, darkL: 0.78, darkC: 0.16, lightL: 0.48, lightC: 0.15 },
|
|
1315
|
+
{ slot: 7, hue: 77, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1316
|
+
{ slot: 8, hue: 122, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1317
|
+
{ slot: 9, hue: 167, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1318
|
+
{ slot: 10, hue: 257, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1319
|
+
{ slot: 11, hue: 302, darkL: 0.64, darkC: 0.15, lightL: 0.36, lightC: 0.15 },
|
|
1320
|
+
].map(Object.freeze));
|
|
1321
|
+
|
|
1322
|
+
const WORKSPACE_BACKGROUND_SLOTS = Object.freeze([
|
|
1323
|
+
{
|
|
1324
|
+
slot: 0,
|
|
1325
|
+
dark: Object.freeze({ l: 0.30, c: 0.05, mix: "26%", borderMix: "46%" }),
|
|
1326
|
+
light: Object.freeze({ l: 0.92, c: 0.05, mix: "20%", borderMix: "34%" }),
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
slot: 1,
|
|
1330
|
+
dark: Object.freeze({ l: 0.38, c: 0.06, mix: "31%", borderMix: "52%" }),
|
|
1331
|
+
light: Object.freeze({ l: 0.84, c: 0.06, mix: "27%", borderMix: "42%" }),
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
slot: 2,
|
|
1335
|
+
dark: Object.freeze({ l: 0.46, c: 0.07, mix: "36%", borderMix: "58%" }),
|
|
1336
|
+
light: Object.freeze({ l: 0.76, c: 0.07, mix: "34%", borderMix: "50%" }),
|
|
1337
|
+
},
|
|
1338
|
+
].map(Object.freeze));
|
|
1339
|
+
|
|
1340
|
+
const WORKSPACE_PRIMARY_SLOT_COUNT = 7;
|
|
1341
|
+
const WORKSPACE_SECONDARY_SLOT_COUNT = WORKSPACE_COLOR_SLOTS.length - WORKSPACE_PRIMARY_SLOT_COUNT;
|
|
1342
|
+
|
|
1343
|
+
function workspaceSlotProbe(start, count, step) {
|
|
1344
|
+
return Array.from({ length: count }, (_, offset) => (start + offset * step) % count);
|
|
1345
|
+
}
|
|
1308
1346
|
|
|
1309
1347
|
/**
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1348
|
+
* 同屏工作区复合颜色槽位消解(R-01-003/AC-08、AC-12):前七个身份沿原七色
|
|
1349
|
+
* OKLCH 主槽位分配,主槽位占满后再使用五个受控明度/色相补充槽位;每个前景槽位
|
|
1350
|
+
* 内优先使用未用的三个背景变体;超过三十六个复合身份后按槽位使用次数均衡复用。
|
|
1313
1351
|
*/
|
|
1314
|
-
function
|
|
1352
|
+
function resolveWorkspaceColors(keys) {
|
|
1315
1353
|
const identities = [...new Set((Array.isArray(keys) ? keys : []).map(cleanText).filter(Boolean))].sort();
|
|
1316
|
-
const uses =
|
|
1354
|
+
const uses = WORKSPACE_COLOR_SLOTS.map(() => 0);
|
|
1355
|
+
const backgroundUses = WORKSPACE_COLOR_SLOTS.map(() => WORKSPACE_BACKGROUND_SLOTS.map(() => 0));
|
|
1317
1356
|
const resolved = new Map();
|
|
1318
1357
|
for (const identity of identities) {
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1358
|
+
const baseHue = workspaceHue(identity);
|
|
1359
|
+
const primaryOrder = workspaceSlotProbe(
|
|
1360
|
+
(baseHue - 30) % WORKSPACE_PRIMARY_SLOT_COUNT,
|
|
1361
|
+
WORKSPACE_PRIMARY_SLOT_COUNT,
|
|
1362
|
+
3,
|
|
1363
|
+
);
|
|
1364
|
+
const secondaryOrder = workspaceSlotProbe(
|
|
1365
|
+
(baseHue - 30) % WORKSPACE_SECONDARY_SLOT_COUNT,
|
|
1366
|
+
WORKSPACE_SECONDARY_SLOT_COUNT,
|
|
1367
|
+
2,
|
|
1368
|
+
).map((index) => WORKSPACE_PRIMARY_SLOT_COUNT + index);
|
|
1369
|
+
const order = [...primaryOrder, ...secondaryOrder];
|
|
1370
|
+
const backgroundOrder = workspaceSlotProbe(
|
|
1371
|
+
(baseHue - 30) % WORKSPACE_BACKGROUND_SLOTS.length,
|
|
1372
|
+
WORKSPACE_BACKGROUND_SLOTS.length,
|
|
1373
|
+
2,
|
|
1374
|
+
);
|
|
1375
|
+
const availableForegroundOrder = order.filter((index) => backgroundOrder.some((backgroundSlot) => backgroundUses[index][backgroundSlot] === 0));
|
|
1376
|
+
const foregroundOrder = availableForegroundOrder.length > 0 ? availableForegroundOrder : order;
|
|
1377
|
+
let foregroundSlot = foregroundOrder.find((index) => uses[index] === 0);
|
|
1378
|
+
if (foregroundSlot === undefined) {
|
|
1379
|
+
foregroundSlot = foregroundOrder.reduce(
|
|
1380
|
+
(best, index) => (uses[index] < uses[best] ? index : best),
|
|
1381
|
+
foregroundOrder[0],
|
|
1382
|
+
);
|
|
1328
1383
|
}
|
|
1329
|
-
uses[
|
|
1330
|
-
|
|
1384
|
+
uses[foregroundSlot] += 1;
|
|
1385
|
+
const backgroundUsesForForeground = backgroundUses[foregroundSlot];
|
|
1386
|
+
const backgroundSlot = backgroundOrder.reduce(
|
|
1387
|
+
(best, index) => (backgroundUsesForForeground[index] < backgroundUsesForForeground[best] ? index : best),
|
|
1388
|
+
backgroundOrder[0],
|
|
1389
|
+
);
|
|
1390
|
+
backgroundUsesForForeground[backgroundSlot] += 1;
|
|
1391
|
+
resolved.set(identity, {
|
|
1392
|
+
foreground: WORKSPACE_COLOR_SLOTS[foregroundSlot],
|
|
1393
|
+
background: WORKSPACE_BACKGROUND_SLOTS[backgroundSlot],
|
|
1394
|
+
});
|
|
1331
1395
|
}
|
|
1332
1396
|
return resolved;
|
|
1333
1397
|
}
|
|
1334
1398
|
|
|
1399
|
+
|
|
1335
1400
|
/** 主会话按左侧工作区顺序排序的权重;不在任何 workspace 的排在最后保持 lineage 顺序。 */
|
|
1336
1401
|
function workspaceRank(workspaceItems) {
|
|
1337
1402
|
const wsIndex = new Map();
|
|
@@ -1386,6 +1451,7 @@ function mainTitle(byId, id) {
|
|
|
1386
1451
|
* - 主会话 running(且无 pending)或处于委托周期(含后代耗尽空窗)→ 'running'
|
|
1387
1452
|
* - 主会话 pendingInteraction / completed / errorReminder → 'awaiting'(等待用户行动)
|
|
1388
1453
|
* - 子代理 running / pending 或存在活动后代 → 'subagent',自身与后代均不活动则不显示
|
|
1454
|
+
* - 普通 fork 虽有 parentId,但 origin 缺失,仍按独立主会话处理
|
|
1389
1455
|
* completions(完成确认与错误提醒,R-01-002/AC-05、AC-13、R-01-010/AC-06):Map id →
|
|
1390
1456
|
* { lastTurnEnd, lastTurnEndKind, lastTurnEndError, ackedAt },由渲染层从宿主侧 ack 状态
|
|
1391
1457
|
* 通道注入;其中完成提醒成立(completionReminder)或错误提醒成立(errorReminder)的
|
|
@@ -1393,21 +1459,38 @@ function mainTitle(byId, id) {
|
|
|
1393
1459
|
* 记账派生):集合内会话视同处于委托周期——后代耗尽至 settle 处理回合启动的
|
|
1394
1460
|
* 空窗内仍保持运行呈现、完成/错误提醒不生效;条目的 descendantActive 字段
|
|
1395
1461
|
* 始终为当帧原始后代活性(供进度锚点记账判定耗尽),不受 delegatingIds 影响。
|
|
1462
|
+
* archivedIds(工作区服务的注册表全局归档集合):归档会话及其有效子代理子树不产出活动条目,
|
|
1463
|
+
* 普通 fork 仅因共享来源 parentId 不受影响;避免会话服务仍保留旧行或完成提醒时在窗格中滞留。
|
|
1396
1464
|
*/
|
|
1397
|
-
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null) {
|
|
1465
|
+
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = []) {
|
|
1398
1466
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1399
1467
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1400
1468
|
const current = snapshot?.current ?? null;
|
|
1401
1469
|
const subagentsByParent = isRecord(snapshot?.subagentsByParent)
|
|
1402
1470
|
? snapshot.subagentsByParent
|
|
1403
1471
|
: {};
|
|
1472
|
+
const archived = archivedIds instanceof Set
|
|
1473
|
+
? new Set([...archivedIds].map((id) => String(id)))
|
|
1474
|
+
: new Set((archivedIds ?? []).map((id) => String(id)));
|
|
1475
|
+
const isArchived = (id) => {
|
|
1476
|
+
const seen = new Set();
|
|
1477
|
+
let currentId = id;
|
|
1478
|
+
while (currentId !== undefined && currentId !== null) {
|
|
1479
|
+
const key = String(currentId);
|
|
1480
|
+
if (archived.has(key) || seen.has(key)) return archived.has(key);
|
|
1481
|
+
seen.add(key);
|
|
1482
|
+
currentId = subagentParentId(byId[key], byId);
|
|
1483
|
+
}
|
|
1484
|
+
return false;
|
|
1485
|
+
};
|
|
1404
1486
|
const rank = workspaceRank(workspaceItems ?? []);
|
|
1405
|
-
const descendantIds = descendantActiveIds(byId);
|
|
1487
|
+
const descendantIds = descendantActiveIds(byId, isArchived);
|
|
1406
1488
|
// 第一遍:层级关系 + 显示判定(show = 自身活动 || 委托周期 || 完成提醒,单点实现避免漂移)。
|
|
1407
1489
|
const rootIds = [];
|
|
1408
1490
|
const childIds = new Map();
|
|
1409
1491
|
const meta = new Map();
|
|
1410
1492
|
for (const id of ids) {
|
|
1493
|
+
if (isArchived(id)) continue;
|
|
1411
1494
|
const row = byId[id];
|
|
1412
1495
|
if (!isRecord(row)) continue;
|
|
1413
1496
|
const hasParent = isSubagentRow(row, byId);
|
|
@@ -1625,10 +1708,15 @@ function listLoadState(snapshot) {
|
|
|
1625
1708
|
return "ready";
|
|
1626
1709
|
}
|
|
1627
1710
|
|
|
1628
|
-
/**
|
|
1711
|
+
/** 会话行是否为某主会话的直属子代理:需同时满足 origin 与有效父会话。 */
|
|
1629
1712
|
function isSubagentRow(row, byId = {}) {
|
|
1630
1713
|
const id = row?.parentId;
|
|
1631
|
-
return id !== undefined && id !== null && isRecord(byId[id]);
|
|
1714
|
+
return row?.origin === "subagent" && id !== undefined && id !== null && isRecord(byId[id]);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/** 取实际子代理的直属母会话 id;普通 fork 的 parentId 只是来源关系,不参与子代理树。 */
|
|
1718
|
+
function subagentParentId(row, byId = {}) {
|
|
1719
|
+
return isSubagentRow(row, byId) ? row.parentId : undefined;
|
|
1632
1720
|
}
|
|
1633
1721
|
|
|
1634
1722
|
/** 会话行是否满足自身状态的活动判定,不含后代活动继承。
|
|
@@ -1642,31 +1730,32 @@ function isOwnActiveRow(row, byId = {}) {
|
|
|
1642
1730
|
return running || pending;
|
|
1643
1731
|
}
|
|
1644
1732
|
|
|
1645
|
-
/**
|
|
1733
|
+
/** 沿实际子代理关系的有效 parentId 链上溯收集会话 id:includeSelf 含活动会话自身,
|
|
1646
1734
|
* 否则只收祖先(存在活动后代的母会话)。活动区与历史区显示判定的单点实现。 */
|
|
1647
|
-
function lineageActiveIds(byId, includeSelf) {
|
|
1735
|
+
function lineageActiveIds(byId, includeSelf, isExcluded = null) {
|
|
1648
1736
|
const ids = new Set();
|
|
1649
1737
|
for (const [id, row] of Object.entries(byId)) {
|
|
1650
|
-
if (!isOwnActiveRow(row, byId)) continue;
|
|
1738
|
+
if (isExcluded?.(id) || !isOwnActiveRow(row, byId)) continue;
|
|
1651
1739
|
const seen = new Set();
|
|
1652
|
-
let currentId = includeSelf ? id : row
|
|
1740
|
+
let currentId = includeSelf ? id : subagentParentId(row, byId);
|
|
1653
1741
|
while (currentId !== undefined && currentId !== null && isRecord(byId[currentId]) && !seen.has(String(currentId))) {
|
|
1742
|
+
if (isExcluded?.(currentId)) break;
|
|
1654
1743
|
seen.add(String(currentId));
|
|
1655
1744
|
ids.add(String(currentId));
|
|
1656
|
-
currentId = byId[currentId]
|
|
1745
|
+
currentId = subagentParentId(byId[currentId], byId);
|
|
1657
1746
|
}
|
|
1658
1747
|
}
|
|
1659
1748
|
return ids;
|
|
1660
1749
|
}
|
|
1661
1750
|
|
|
1662
|
-
/**
|
|
1751
|
+
/** 沿实际子代理关系补齐活动祖先(含活动会话自身),供历史区显示判定。 */
|
|
1663
1752
|
function activeSessionIds(byId = {}) {
|
|
1664
1753
|
return lineageActiveIds(byId, true);
|
|
1665
1754
|
}
|
|
1666
1755
|
|
|
1667
1756
|
/** 「存在活动后代」的母会话集合(不含活动会话自身),供 buildEntries 判定委托周期(R-01-003/AC-05)。 */
|
|
1668
|
-
function descendantActiveIds(byId = {}) {
|
|
1669
|
-
return lineageActiveIds(byId, false);
|
|
1757
|
+
function descendantActiveIds(byId = {}, isExcluded = null) {
|
|
1758
|
+
return lineageActiveIds(byId, false, isExcluded);
|
|
1670
1759
|
}
|
|
1671
1760
|
|
|
1672
1761
|
/** 判断活动条目是否需要建立轮内状态订阅:以宿主 running 为准、与呈现 kind 解耦——
|
|
@@ -1904,12 +1993,14 @@ function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedId
|
|
|
1904
1993
|
return entries;
|
|
1905
1994
|
}
|
|
1906
1995
|
|
|
1907
|
-
/** 毫秒时长的人性化短格式,例如 "47s"、"3m12s"。 */
|
|
1996
|
+
/** 毫秒时长的人性化短格式,例如 "47s"、"3m12s"、"1h2m3s"。 */
|
|
1908
1997
|
function fmtElapsedMs(ms) {
|
|
1909
1998
|
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
1910
1999
|
const s = Math.round(ms / 1000);
|
|
1911
2000
|
if (s < 60) return `${s}s`;
|
|
1912
|
-
|
|
2001
|
+
const m = Math.floor(s / 60);
|
|
2002
|
+
if (m < 60) return `${m}m${s % 60}s`;
|
|
2003
|
+
return `${Math.floor(m / 60)}h${m % 60}m${s % 60}s`;
|
|
1913
2004
|
}
|
|
1914
2005
|
|
|
1915
2006
|
/** 历史卡相对活动时间:按分钟、小时、天、周、月、年分级;负值返回空。 */
|
|
@@ -2108,6 +2199,43 @@ function openSession(sessions, sessionId) {
|
|
|
2108
2199
|
}
|
|
2109
2200
|
}
|
|
2110
2201
|
|
|
2202
|
+
/**
|
|
2203
|
+
* 以最小必要距离把卡片滚入窗格滚动视口;不会居中,也不会滚动外层页面。
|
|
2204
|
+
* 默认使用原生 smooth scroll,调用方可传 auto 适配降低动效偏好;缺少 scrollTo 或 options 不受支持时回退为同步滚动
|
|
2205
|
+
* (R-01-006/AC-02)。
|
|
2206
|
+
*/
|
|
2207
|
+
function scrollCardIntoView(scroll, card, behavior = "smooth") {
|
|
2208
|
+
if (typeof scroll?.getBoundingClientRect !== "function" || typeof card?.getBoundingClientRect !== "function")
|
|
2209
|
+
return false;
|
|
2210
|
+
const viewport = scroll.getBoundingClientRect();
|
|
2211
|
+
const rect = card.getBoundingClientRect();
|
|
2212
|
+
const delta = rect.top < viewport.top
|
|
2213
|
+
? rect.top - viewport.top
|
|
2214
|
+
: rect.bottom > viewport.bottom
|
|
2215
|
+
? rect.bottom - viewport.bottom
|
|
2216
|
+
: 0;
|
|
2217
|
+
if (delta === 0) return false;
|
|
2218
|
+
const currentTop = Number(scroll.scrollTop);
|
|
2219
|
+
if (!Number.isFinite(currentTop)) return false;
|
|
2220
|
+
const scrollHeight = Number(scroll.scrollHeight);
|
|
2221
|
+
const clientHeight = Number(scroll.clientHeight);
|
|
2222
|
+
const maxTop = Number.isFinite(scrollHeight) && Number.isFinite(clientHeight)
|
|
2223
|
+
? Math.max(0, scrollHeight - clientHeight)
|
|
2224
|
+
: Infinity;
|
|
2225
|
+
const nextTop = Math.min(maxTop, Math.max(0, currentTop + delta));
|
|
2226
|
+
if (!Number.isFinite(nextTop) || nextTop === currentTop) return false;
|
|
2227
|
+
if (typeof scroll.scrollTo === "function") {
|
|
2228
|
+
try {
|
|
2229
|
+
scroll.scrollTo({ top: nextTop, behavior });
|
|
2230
|
+
if (behavior !== "auto" || Number(scroll.scrollTop) !== currentTop) return true;
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
if (!(error instanceof TypeError)) throw error;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
scroll.scrollTop = nextTop;
|
|
2236
|
+
return Number(scroll.scrollTop) !== currentTop;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2111
2239
|
/** 原生会话输入框(dsh-client-ui-conversation composer 的 textarea)。 */
|
|
2112
2240
|
const COMPOSER_SELECTOR = "textarea[data-phase]";
|
|
2113
2241
|
|
|
@@ -2620,23 +2748,41 @@ const CSS = `
|
|
|
2620
2748
|
/* 复审修复(C-040、C-043):状态点光晕三类同强度、仅换色相——阻塞金、完成绿与错误红光晕一致。 */
|
|
2621
2749
|
/* 工作区徽标「图标+文本」双段:文件夹图标与左边栏工作区条目同源(R-01-003/AC-06);
|
|
2622
2750
|
名称字号不低于 10.5px(AC-07),行高保持 14px 以维持胶囊与卡片高度。
|
|
2623
|
-
着色(AC-08~AC-
|
|
2624
|
-
|
|
2625
|
-
|
|
2751
|
+
着色(AC-08~AC-12):核心映射提供 12 个前景身份槽与每槽 3 个背景变体;
|
|
2752
|
+
深色/浅色主题分别使用前景与背景槽位的 L/C 参数,文字使用前景调色板色,
|
|
2753
|
+
底色使用独立背景源色、描边使用前景调色板色,分别在 OKLCH 空间按变体混合强度呈现;前景与背景保持同色相族。 */
|
|
2626
2754
|
[data-dsh-activity-pane] .dap-workspace {
|
|
2627
2755
|
width: fit-content; max-width: 100%; display: flex; align-items: center; gap: 3px;
|
|
2628
2756
|
overflow: hidden;
|
|
2629
2757
|
font-size: 10.5px; line-height: 14px;
|
|
2630
|
-
--dap-workspace-color: oklch(
|
|
2758
|
+
--dap-workspace-color: oklch(
|
|
2759
|
+
var(--dap-workspace-dark-l, 0.78)
|
|
2760
|
+
var(--dap-workspace-dark-c, 0.16)
|
|
2761
|
+
var(--dap-workspace-hue, 235)
|
|
2762
|
+
);
|
|
2763
|
+
--dap-workspace-background-color: oklch(
|
|
2764
|
+
var(--dap-workspace-bg-dark-l, 0.38)
|
|
2765
|
+
var(--dap-workspace-bg-dark-c, 0.06)
|
|
2766
|
+
var(--dap-workspace-hue, 235)
|
|
2767
|
+
);
|
|
2631
2768
|
color: var(--dap-workspace-color);
|
|
2632
|
-
background: color-mix(in oklch, var(--dap-workspace-color)
|
|
2633
|
-
border: 1px solid color-mix(in oklch, var(--dap-workspace-color)
|
|
2769
|
+
background: color-mix(in oklch, var(--dap-workspace-background-color) var(--dap-workspace-bg-dark-mix, 31%), transparent);
|
|
2770
|
+
border: 1px solid color-mix(in oklch, var(--dap-workspace-color) var(--dap-workspace-bg-dark-border-mix, 52%), transparent);
|
|
2634
2771
|
border-radius: 999px; padding: 0 7px;
|
|
2635
2772
|
}
|
|
2636
2773
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
2637
|
-
--dap-workspace-color: oklch(
|
|
2638
|
-
|
|
2639
|
-
|
|
2774
|
+
--dap-workspace-color: oklch(
|
|
2775
|
+
var(--dap-workspace-light-l, 0.48)
|
|
2776
|
+
var(--dap-workspace-light-c, 0.15)
|
|
2777
|
+
var(--dap-workspace-hue, 235)
|
|
2778
|
+
);
|
|
2779
|
+
--dap-workspace-background-color: oklch(
|
|
2780
|
+
var(--dap-workspace-bg-light-l, 0.84)
|
|
2781
|
+
var(--dap-workspace-bg-light-c, 0.06)
|
|
2782
|
+
var(--dap-workspace-hue, 235)
|
|
2783
|
+
);
|
|
2784
|
+
background: color-mix(in oklch, var(--dap-workspace-background-color) var(--dap-workspace-bg-light-mix, 27%), transparent);
|
|
2785
|
+
border-color: color-mix(in oklch, var(--dap-workspace-color) var(--dap-workspace-bg-light-border-mix, 42%), transparent);
|
|
2640
2786
|
}
|
|
2641
2787
|
[data-dsh-activity-pane] .dap-workspace-icon { flex: none; display: inline-flex; }
|
|
2642
2788
|
[data-dsh-activity-pane] .dap-workspace-icon svg { display: block; }
|
|
@@ -3179,16 +3325,18 @@ function apply(ctx) {
|
|
|
3179
3325
|
/** 等待条目 id/类别队列签名:变化时统一重启数量胶囊与等待卡末行动画对相(R-01-002/AC-07、AC-08)。 */
|
|
3180
3326
|
let pulseSignature = "";
|
|
3181
3327
|
// E2E-only deterministic seams(T-089/T-090):显式 URL fragment 最多把本页首次列表 ready 呈现
|
|
3182
|
-
// 或正式 models RPC 延后 1s
|
|
3328
|
+
// 或正式 models RPC 延后 1s;列表延迟从首个窗格 render 开始计时,避免宿主启动耗时
|
|
3329
|
+
// 把 pending 观察窗口提前消耗;默认路径为 0,不伪造服务响应、快照或生产时序。
|
|
3183
3330
|
const e2eParams = new URLSearchParams(window.location.hash.slice(1));
|
|
3184
3331
|
const requestedListDelay = Number(e2eParams.get("dap-e2e-list-delay"));
|
|
3185
|
-
const
|
|
3186
|
-
?
|
|
3332
|
+
const e2eListDelayMs = Number.isFinite(requestedListDelay) && requestedListDelay > 0
|
|
3333
|
+
? Math.min(requestedListDelay, 1_000)
|
|
3187
3334
|
: 0;
|
|
3188
3335
|
const requestedModelDelay = Number(e2eParams.get("dap-e2e-model-delay"));
|
|
3189
3336
|
const e2eModelDelayMs = Number.isFinite(requestedModelDelay) && requestedModelDelay > 0
|
|
3190
3337
|
? Math.min(requestedModelDelay, 1_000)
|
|
3191
3338
|
: 0;
|
|
3339
|
+
let e2eListReadyAt = 0;
|
|
3192
3340
|
let e2eListReleaseTimer = null;
|
|
3193
3341
|
const e2eModelDelayWaiters = new Map();
|
|
3194
3342
|
function delayedModelCall(call) {
|
|
@@ -3209,6 +3357,9 @@ function apply(ctx) {
|
|
|
3209
3357
|
let paneWidth = readStoredPaneWidth();
|
|
3210
3358
|
/** 用户最近一次激活的卡片 id;打开重试链被更新的激活意图取代即取消。 */
|
|
3211
3359
|
let lastActivatedId = null;
|
|
3360
|
+
/** 最近一次已处理的当前卡片;同一卡片的运行时重绘不反复打断用户手动滚动。 */
|
|
3361
|
+
let autoScrolledCurrentId = null;
|
|
3362
|
+
let autoScrolledCurrentCard = null;
|
|
3212
3363
|
/** 完成确认状态(R-01-002/AC-03、AC-10~AC-12):id → { lastTurnEnd, ackedAt },
|
|
3213
3364
|
* 来自宿主侧 ack 通道(SSE 全量快照),随每次推送整体替换。 */
|
|
3214
3365
|
const completeAcksById = new Map();
|
|
@@ -4357,22 +4508,40 @@ function apply(ctx) {
|
|
|
4357
4508
|
renderTrace(container, entry.timeline, { lastOnly });
|
|
4358
4509
|
}
|
|
4359
4510
|
|
|
4360
|
-
function renderCardInto(el, entry,
|
|
4511
|
+
function renderCardInto(el, entry, colorByWorkspace) {
|
|
4361
4512
|
const workspaceLabel = el.querySelector(".dap-workspace");
|
|
4362
4513
|
if (workspaceLabel !== null) {
|
|
4363
4514
|
const workspaceText = workspaceLabel.querySelector(".dap-workspace-text");
|
|
4364
|
-
|
|
4515
|
+
const color = colorByWorkspace.get(entry.workspaceKey);
|
|
4516
|
+
const foreground = color?.foreground;
|
|
4517
|
+
const background = color?.background;
|
|
4518
|
+
const colorVariables = {
|
|
4519
|
+
"--dap-workspace-hue": foreground?.hue,
|
|
4520
|
+
"--dap-workspace-dark-l": foreground?.darkL,
|
|
4521
|
+
"--dap-workspace-dark-c": foreground?.darkC,
|
|
4522
|
+
"--dap-workspace-light-l": foreground?.lightL,
|
|
4523
|
+
"--dap-workspace-light-c": foreground?.lightC,
|
|
4524
|
+
"--dap-workspace-bg-dark-l": background?.dark?.l,
|
|
4525
|
+
"--dap-workspace-bg-dark-c": background?.dark?.c,
|
|
4526
|
+
"--dap-workspace-bg-dark-mix": background?.dark?.mix,
|
|
4527
|
+
"--dap-workspace-bg-dark-border-mix": background?.dark?.borderMix,
|
|
4528
|
+
"--dap-workspace-bg-light-l": background?.light?.l,
|
|
4529
|
+
"--dap-workspace-bg-light-c": background?.light?.c,
|
|
4530
|
+
"--dap-workspace-bg-light-mix": background?.light?.mix,
|
|
4531
|
+
"--dap-workspace-bg-light-border-mix": background?.light?.borderMix,
|
|
4532
|
+
};
|
|
4533
|
+
if (entry.workspaceTitle !== "" && foreground !== undefined && background !== undefined) {
|
|
4365
4534
|
if (workspaceText !== null) restoreTextField(workspaceText, entry.workspaceTitle);
|
|
4366
|
-
const
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
if (
|
|
4370
|
-
else workspaceLabel.style.setProperty(
|
|
4535
|
+
for (const [property, value] of Object.entries(colorVariables)) {
|
|
4536
|
+
const next = value === undefined ? "" : String(value);
|
|
4537
|
+
if (workspaceLabel.style.getPropertyValue(property) === next) continue;
|
|
4538
|
+
if (next === "") workspaceLabel.style.removeProperty(property);
|
|
4539
|
+
else workspaceLabel.style.setProperty(property, next);
|
|
4371
4540
|
}
|
|
4372
4541
|
workspaceLabel.removeAttribute("hidden");
|
|
4373
4542
|
} else {
|
|
4374
4543
|
if (workspaceText !== null) restoreTextField(workspaceText, "");
|
|
4375
|
-
workspaceLabel.style.removeProperty(
|
|
4544
|
+
for (const property of Object.keys(colorVariables)) workspaceLabel.style.removeProperty(property);
|
|
4376
4545
|
workspaceLabel.setAttribute("hidden", "");
|
|
4377
4546
|
}
|
|
4378
4547
|
}
|
|
@@ -4751,7 +4920,7 @@ function apply(ctx) {
|
|
|
4751
4920
|
|
|
4752
4921
|
/** 渲染某一张卡片进指定列表容器(活动/历史通用)。index 是条目在卡片序列中的
|
|
4753
4922
|
* 序号,offset 是容器内首个卡片前的非卡片子节点数(活动区有轨道层、历史区有段头)。 */
|
|
4754
|
-
function renderCardIntoList(list, entry, reuseMap, index, offset,
|
|
4923
|
+
function renderCardIntoList(list, entry, reuseMap, index, offset, colorByWorkspace) {
|
|
4755
4924
|
let rec = reuseMap.get(entry.id);
|
|
4756
4925
|
if (rec === undefined) {
|
|
4757
4926
|
const el = document.createElement("div");
|
|
@@ -4808,7 +4977,7 @@ function apply(ctx) {
|
|
|
4808
4977
|
recentTimeText ? "," + recentTimeText : ""
|
|
4809
4978
|
}`,
|
|
4810
4979
|
);
|
|
4811
|
-
renderCardInto(rec.el, entry,
|
|
4980
|
+
renderCardInto(rec.el, entry, colorByWorkspace);
|
|
4812
4981
|
// 只有顺序/归属真正变化时才移动 DOM:每次渲染无条件 appendChild 会把所有
|
|
4813
4982
|
// 卡片瞬时移除再插回——按下/抬起之间经过的移动让浏览器取消 click;焦点卡
|
|
4814
4983
|
// 被瞬时断开而失焦;悬停卡的 :hover 也随之丢失且不再补发。会话活跃期间
|
|
@@ -4818,6 +4987,37 @@ function apply(ctx) {
|
|
|
4818
4987
|
return true;
|
|
4819
4988
|
}
|
|
4820
4989
|
|
|
4990
|
+
/** 原生侧栏切换当前会话后,只把已呈现且未完整可见的当前卡片滚入窗格;降低动效偏好时不平滑。 */
|
|
4991
|
+
function ensureCurrentCardVisible(scroll, currentId) {
|
|
4992
|
+
const id = currentId === null || currentId === undefined ? null : String(currentId);
|
|
4993
|
+
if (id === null || id === "") {
|
|
4994
|
+
autoScrolledCurrentId = null;
|
|
4995
|
+
autoScrolledCurrentCard = null;
|
|
4996
|
+
return;
|
|
4997
|
+
}
|
|
4998
|
+
// 当前会话卡片只有在活动列表中才参与自动定位;完成确认后同一 current
|
|
4999
|
+
// 会话会进入历史区,不能因仍带 data-current 把窗格滚到历史卡片。
|
|
5000
|
+
const card = scroll?.querySelector?.(`.${LIST_CLASS} .${CARD_CLASS}[data-current]`) ?? null;
|
|
5001
|
+
if (
|
|
5002
|
+
card === null ||
|
|
5003
|
+
card.dataset.sessionId !== id ||
|
|
5004
|
+
scroll.clientHeight <= 0 ||
|
|
5005
|
+
card.getBoundingClientRect().height <= 0
|
|
5006
|
+
)
|
|
5007
|
+
return;
|
|
5008
|
+
if (autoScrolledCurrentId === id && autoScrolledCurrentCard === card) return;
|
|
5009
|
+
if (scrollCardIntoView(scroll, card, prefersReducedMotion() ? "auto" : "smooth")) {
|
|
5010
|
+
autoScrolledCurrentId = id;
|
|
5011
|
+
autoScrolledCurrentCard = card;
|
|
5012
|
+
return;
|
|
5013
|
+
}
|
|
5014
|
+
const viewport = scroll.getBoundingClientRect();
|
|
5015
|
+
const rect = card.getBoundingClientRect();
|
|
5016
|
+
if (rect.height <= viewport.height && (rect.top < viewport.top || rect.bottom > viewport.bottom)) return;
|
|
5017
|
+
autoScrolledCurrentId = id;
|
|
5018
|
+
autoScrolledCurrentCard = card;
|
|
5019
|
+
}
|
|
5020
|
+
|
|
4821
5021
|
function pruneCards(reuseMap, alive) {
|
|
4822
5022
|
for (const [id, rec] of reuseMap) {
|
|
4823
5023
|
if (alive.has(id)) continue;
|
|
@@ -5001,6 +5201,8 @@ function apply(ctx) {
|
|
|
5001
5201
|
recentTotal = 0;
|
|
5002
5202
|
recentHasMore = false;
|
|
5003
5203
|
recentAppendQueued = false;
|
|
5204
|
+
autoScrolledCurrentId = null;
|
|
5205
|
+
autoScrolledCurrentCard = null;
|
|
5004
5206
|
// 旧窗格已脱离文档:其在飞平移的 transitionend 不再触发,逐元素取消避免残留。
|
|
5005
5207
|
for (const el of [...shiftCleanups.keys()]) cancelShift(el);
|
|
5006
5208
|
}
|
|
@@ -5009,6 +5211,7 @@ function apply(ctx) {
|
|
|
5009
5211
|
const recentSection = pane.querySelector(`.${RECENT_CLASS}`);
|
|
5010
5212
|
if (activeList === null || recentSection === null) return;
|
|
5011
5213
|
|
|
5214
|
+
if (e2eListReadyAt === 0 && e2eListDelayMs > 0) e2eListReadyAt = Date.now() + e2eListDelayMs;
|
|
5012
5215
|
let snapshot = getSnapshot(sessions, "list");
|
|
5013
5216
|
if (e2eListReadyAt > Date.now() && snapshot?.phase !== "error") {
|
|
5014
5217
|
snapshot = { phase: "pending" };
|
|
@@ -5033,7 +5236,7 @@ function apply(ctx) {
|
|
|
5033
5236
|
for (const [id, state] of progressAnchorById) {
|
|
5034
5237
|
if (delegationActive(state, now)) delegatingIds.add(id);
|
|
5035
5238
|
}
|
|
5036
|
-
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds);
|
|
5239
|
+
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds);
|
|
5037
5240
|
// 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
|
|
5038
5241
|
// 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
|
|
5039
5242
|
const runLikeIds = new Set(
|
|
@@ -5231,12 +5434,32 @@ function apply(ctx) {
|
|
|
5231
5434
|
const recentTimeSignature = recent.map((entry) => fmtRecentTime(entry.activityAt));
|
|
5232
5435
|
const sig = JSON.stringify([listState, cardSignature(visibleEntries), pulseSurface, recentTimeSignature]);
|
|
5233
5436
|
if (sig === lastSig) return;
|
|
5234
|
-
const
|
|
5437
|
+
const colorByWorkspace = resolveWorkspaceColors(visibleEntries.map((entry) => entry.workspaceKey));
|
|
5235
5438
|
// 跨区迁移(双向,R-01-010/AC-07):DOM 写入前量取旧卡矩形并克隆 ghost。
|
|
5236
5439
|
const migrations = [
|
|
5237
5440
|
...movedToRecentIds(prevRenderedActiveIds, active, recent).map((id) => ({ id, from: cardsById, to: recentCardsById })),
|
|
5238
5441
|
...movedToActiveIds(prevRenderedRecentIds, active, recent).map((id) => ({ id, from: recentCardsById, to: cardsById })),
|
|
5239
5442
|
];
|
|
5443
|
+
// 活动卡带焦点迁入历史时,把键盘焦点交给其上一个仍在活动区的卡片;
|
|
5444
|
+
// 若迁移卡已在活动区首位,则退到下一张,避免焦点落到历史区或 body。
|
|
5445
|
+
let focusAfterMigrationId = null;
|
|
5446
|
+
const focusedElement = document.activeElement;
|
|
5447
|
+
const activeIds = new Set(active.map((entry) => String(entry.id)));
|
|
5448
|
+
for (const { id, from } of migrations) {
|
|
5449
|
+
if (from !== cardsById) continue;
|
|
5450
|
+
const source = from.get(id)?.el;
|
|
5451
|
+
if (source === undefined || !source.contains(focusedElement)) continue;
|
|
5452
|
+
const activeCards = [...activeList.querySelectorAll(`.${CARD_CLASS}`)];
|
|
5453
|
+
const sourceIndex = activeCards.indexOf(source);
|
|
5454
|
+
if (sourceIndex < 0) continue;
|
|
5455
|
+
const target = activeCards
|
|
5456
|
+
.slice(0, sourceIndex)
|
|
5457
|
+
.reverse()
|
|
5458
|
+
.find((card) => activeIds.has(String(card.dataset.sessionId)))
|
|
5459
|
+
?? activeCards.slice(sourceIndex + 1).find((card) => activeIds.has(String(card.dataset.sessionId)));
|
|
5460
|
+
focusAfterMigrationId = target?.dataset.sessionId ?? null;
|
|
5461
|
+
break;
|
|
5462
|
+
}
|
|
5240
5463
|
const movePlans = prepareMoveGhosts(migrations);
|
|
5241
5464
|
// 迁移帧内位置受影响的其它卡片与历史区段头:DOM 写入前量取当前视觉矩形
|
|
5242
5465
|
// (R-01-010/AC-10);reduced-motion 下 movePlans 为空,FLIP 量取整体跳过。
|
|
@@ -5250,7 +5473,7 @@ function apply(ctx) {
|
|
|
5250
5473
|
const aliveActive = new Set();
|
|
5251
5474
|
for (const [index, entry] of active.entries()) {
|
|
5252
5475
|
try {
|
|
5253
|
-
renderCardIntoList(activeList, entry, cardsById, index, 1,
|
|
5476
|
+
renderCardIntoList(activeList, entry, cardsById, index, 1, colorByWorkspace);
|
|
5254
5477
|
} catch (error) {
|
|
5255
5478
|
renderOk = false;
|
|
5256
5479
|
logCardRenderError(entry.id, error);
|
|
@@ -5267,7 +5490,7 @@ function apply(ctx) {
|
|
|
5267
5490
|
// 历史区容器首个子节点是段头(.dap-recent-head),卡片从 offset 1 开始。
|
|
5268
5491
|
for (const [index, entry] of recent.entries()) {
|
|
5269
5492
|
try {
|
|
5270
|
-
renderCardIntoList(recentSection, entry, recentCardsById, index, 1,
|
|
5493
|
+
renderCardIntoList(recentSection, entry, recentCardsById, index, 1, colorByWorkspace);
|
|
5271
5494
|
} catch (error) {
|
|
5272
5495
|
renderOk = false;
|
|
5273
5496
|
logCardRenderError(entry.id, error);
|
|
@@ -5286,6 +5509,8 @@ function apply(ctx) {
|
|
|
5286
5509
|
}
|
|
5287
5510
|
runMoveGhosts(movePlans);
|
|
5288
5511
|
if (shiftRects !== null) runShiftAnimations(shiftRects);
|
|
5512
|
+
ensureCurrentCardVisible(pane.querySelector(".dap-scroll"), snapshot?.current ?? null);
|
|
5513
|
+
if (focusAfterMigrationId !== null) cardsById.get(focusAfterMigrationId)?.el.focus();
|
|
5289
5514
|
// 区域已有条目但列表仍在途时,在区头部显示行内加载指示(R-01-014/AC-01)。
|
|
5290
5515
|
const headerEl = pane.querySelector(".dap-header");
|
|
5291
5516
|
const recentHeadEl = recentSection?.querySelector(".dap-recent-head") ?? null;
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ The npm package ships prebuilt, so no local build step is needed. If the pane do
|
|
|
53
53
|
- [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.
|
|
54
54
|
- [x] **Workspace names displayed and factored into ordering**: session cards show a workspace badge with a stable color, and session ordering follows the workspace order in the left sidebar.
|
|
55
55
|
- [x] **Current work and run stats**: active cards show the latest instruction, thinking, and tool calls in a collapsed timeline of at most 4 rows; running cards also show round progress, output rate, cache hit rate, input/output tokens, and run duration, while completed, blocked, and error waits retain the previous round's duration at the end of the token stats row alongside last-known output stats; recent history cards retain the latest completed round's available stats between the reply preview and activity time.
|
|
56
|
-
- [x] **Session navigation**: clicking or keyboard-activating a session card jumps to that session's page,
|
|
56
|
+
- [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.
|
|
57
57
|
- [x] **Session metadata**: session cards show the current model name and reasoning level.
|
|
58
58
|
- [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.
|
|
59
59
|
|
package/README.zh-CN.md
CHANGED
|
@@ -53,7 +53,7 @@ npm 包内置预构建产物,无需本地构建步骤。安装后如窗格未
|
|
|
53
53
|
- [x] **显示子/孙会话层级**:子代理以连接线和紧凑卡片嵌套在母会话下;母会话自身回合结束但仍有活动后代时继续按运行中呈现,子代理结束且没有活动后代后从活动区消失;历史区只保留主会话。
|
|
54
54
|
- [x] **显示工作区名称并参与排序**:会话卡片显示带稳定色彩的工作区徽标,会话排序与左侧边栏中的工作区顺序保持一致。
|
|
55
55
|
- [x] **展示当前工作与运行统计**:活动卡以最多 4 行折叠时间线展示最近指令、思考与工具调用;运行中卡片还显示回合进度、输出速率、缓存命中率、输入/输出 token 与运行时长,进入完成、阻塞或错误等待后在统计行末尾与 tok/s 等内容并列保留上一轮耗时及最后已知统计;迁入最近历史后,历史卡在助手预览与活动时间之间继续保留最后一轮的可用统计。
|
|
56
|
-
- [x]
|
|
56
|
+
- [x] **加入会话导航跳转**:点击或键盘激活会话卡片可跳转到对应会话页面,当前会话保持高亮;从 DSH 原生左侧栏选择会话时,对应窗格卡片会滚动到完整可见,不强制居中。
|
|
57
57
|
- [x] **增加会话元信息**:会话卡片中显示当前使用的模型名称和推理级别。
|
|
58
58
|
- [x] **完善桌面与移动交互**:桌面窗格可折叠、拖拽调宽并记忆宽度;移动端使用不挤压主会话布局的固定抽屉;长列表提供独立滚动与回到顶部按钮。
|
|
59
59
|
|
package/package.json
CHANGED
package/scripts/acceptance.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// - R-01-002/AC-05、AC-10、AC-11、AC-12 完成提醒路径 → e2e/specs/completion-sync.mjs
|
|
18
18
|
// - R-01-002/AC-05、AC-06、AC-09、AC-10、AC-12、AC-13 错误提醒路径 → e2e/specs/error-reminder.mjs
|
|
19
19
|
// T-084 起第二批:
|
|
20
|
-
// - R-01-005/AC-01 点击/Enter/Space、R-01-006/AC-01 → e2e/specs/navigation.mjs
|
|
20
|
+
// - R-01-005/AC-01 点击/Enter/Space、R-01-006/AC-01~AC-02 → e2e/specs/navigation.mjs、e2e/specs/long-list.mjs
|
|
21
21
|
// - R-01-015/AC-01、AC-02、AC-04、AC-03 桌面折叠态 → e2e/specs/resize.mjs;AC-03 移动端无手柄 → e2e/specs/mobile-drawer.mjs
|
|
22
22
|
// - R-01-018/AC-01、AC-03、AC-04、AC-05、AC-02 点击/键盘/reduced-motion → e2e/specs/back-to-top.mjs(AC-02 平滑观感残留人工)
|
|
23
23
|
// - R-01-003/AC-03、AC-08~AC-12 浏览器着色、R-01-012/AC-01、AC-05、R-01-013/AC-01、R-01-013/AC-03、R-01-013/AC-04、AC-07、AC-08、AC-10、AC-11 → e2e/specs/card-content.mjs(AC-02 结构断言已迁,多工作区感知区分与「标题随宿主更新」半句仍人工)
|
|
@@ -37,7 +37,9 @@ const steps = [
|
|
|
37
37
|
// R-01-003/AC-06、AC-07 工作区徽标图标与字号
|
|
38
38
|
"确认活动卡与最近卡上的工作区名称文字前都有文件夹图标,且与左边栏工作区条目的图标相同(一眼可辨为工作区归属);名称字号明显可读(不低于 10.5px),深浅主题下图标颜色与文字协调;把窗格拖窄使名称超长,确认仍以省略号截断而图标完整不被挤压。",
|
|
39
39
|
// R-01-003/AC-08、AC-09、AC-10、AC-11、AC-12 工作区徽标色彩区分与协调
|
|
40
|
-
|
|
40
|
+
// R-01-003/AC-10:深浅主题前景、底色与描边均可辨。
|
|
41
|
+
// R-01-003/AC-11:颜色槽位保持 OKLCH 同色相单色系。
|
|
42
|
+
"同时保持归属不同工作区的多个会话卡片可见,确认同一工作区的所有徽标(图标、文字、底色与描边)使用同一颜色槽位;当可见工作区达到 8~12 个时,主色相与明度/色相补充槽仍能一眼区分;当某个前景槽位发生复用时,3 档背景变体能拉开同前景色的底色/描边层次;颜色分布覆盖橙/黄绿/绿/青/蓝/紫/洋红等色区,且任何徽标都不是红色警戒色;重点确认 docsim、dsh-activity-pane、ops 与至少一个补充槽位工作区彼此可区分;每枚徽标的前景(图标+文字)与背景为 OKLCH 同色相单色系,文字鲜明不发灰、明度对比拉开;同一可见工作区集合下刷新页面后各工作区颜色槽位保持不变;切换深浅主题后徽标仍协调且文字可辨,胶囊尺寸与字号不变。",
|
|
41
43
|
// R-01-005/AC-01 活跃期连续点击(回归:渲染不得移动卡片 DOM 吃掉 click/焦点/悬停)
|
|
42
44
|
"在某会话持续流式运行期间,连续点击当前会话活动卡与各历史卡:每次 click 都应生效(当前卡获得并保持焦点、其它卡完成跳转),悬停高亮不丢失;运行结束后行为保持一致。",
|
|
43
45
|
// R-01-005/AC-01 移动端切换不弹软键盘(回归:抑制原生 composer 自动聚焦)
|