dsh-activity-pane 0.15.0 → 0.16.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 +707 -270
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -2
- package/scripts/acceptance.mjs +8 -3
- package/scripts/check.mjs +142 -53
- package/src/client.mjs +649 -248
- package/src/core.mjs +50 -17
- package/src/navigation.mjs +8 -5
package/.dsh-plugin/client.js
CHANGED
|
@@ -7,13 +7,16 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
// dsh-activity-pane 核心纯函数:把 DSH 原生 sessions/workspaces 客户端服务
|
|
8
8
|
// 快照映射成窗格里的"活动会话条目"。这一层不触碰 DOM,可单测。
|
|
9
9
|
//
|
|
10
|
-
// 数据源约定(来自 @deepseek-ai/dsh-
|
|
10
|
+
// 数据源约定(来自 @deepseek-ai/dsh-api-session-controller 0.2.0 的 sessions.list 快照):
|
|
11
11
|
// ids: SessionId[] —— 宿主列表顺序(祖先在前的 lineage 顺序)
|
|
12
|
-
// byId[id]: { id, displayTitle, title?, cwd?, parentId?, origin?, running,
|
|
13
|
-
//
|
|
14
|
-
// current
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// byId[id]: { id, displayTitle, title?, cwd?, parentId?, origin?, running,
|
|
13
|
+
// retainedBy: { [source]: count }, blank, updatedAt, ... }
|
|
14
|
+
// current —— 0.2.0 起不再携带:以行上 retainedBy.mainView > 0
|
|
15
|
+
// 推导(宿主外壳 DocumentTitle / UiSession
|
|
16
|
+
// .publishMain / isMain 同款口径,currentIdOf)
|
|
17
|
+
// projectionsBySession[id]: { values: { subagentCatalog?: SubagentCatalogEntry[] },
|
|
18
|
+
// state: 'idle'|'loading'|'ready'|'error' }
|
|
19
|
+
// jobsBySession: { [sessionId]: JobView[] }(来自 jobs 服务 state.rows,watchRows 订阅)
|
|
17
20
|
// 以及 workspaces.list 的 items: [{ title, path, sessionIds }]。
|
|
18
21
|
//
|
|
19
22
|
// 展示规则:
|
|
@@ -1526,12 +1529,42 @@ function instructionTime(row) {
|
|
|
1526
1529
|
return Number.isFinite(time) ? time : -1;
|
|
1527
1530
|
}
|
|
1528
1531
|
|
|
1529
|
-
/**
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1532
|
+
/**
|
|
1533
|
+
* 当前会话 id 推导(dsh 0.2.0 起 sessions.list 不再携带 current):取行上
|
|
1534
|
+
* `retainedBy.mainView` 引用计数 > 0 的首行。与宿主外壳同款口径——client-ui-layout
|
|
1535
|
+
* DocumentTitle、client-ui-session publishMain/isMain 均以该判定取当前会话。
|
|
1536
|
+
* 快照非记录、行缺 retainedBy 或计数非有限数字均按未命中处理,不抛错。
|
|
1537
|
+
*/
|
|
1538
|
+
function currentIdOf(list) {
|
|
1539
|
+
const byId = isRecord(list) && isRecord(list.byId) ? list.byId : {};
|
|
1540
|
+
for (const row of Object.values(byId)) {
|
|
1541
|
+
if (!isRecord(row) || !isRecord(row.retainedBy)) continue;
|
|
1542
|
+
const count = Number(row.retainedBy.mainView);
|
|
1543
|
+
if (Number.isFinite(count) && count > 0) return typeof row?.id === "string" ? row.id : null;
|
|
1544
|
+
}
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* 母会话子代理目录条目读取(dsh 0.2.0 起 subagentsByParent 移除):取列表快照
|
|
1550
|
+
* `projectionsBySession[parentId].values.subagentCatalog`(SubagentCatalogEntry[]
|
|
1551
|
+
* —— `{ id, createdAt, mode: 'one-shot'|'continuable'|'unknown', label? }`)。
|
|
1552
|
+
* 投影未加载(无该键、state 非就绪或 values 缺键)按目录不可得返回 undefined,
|
|
1553
|
+
* 调用方经 sessions.refreshProjections(parentId) 拉取后重试。
|
|
1554
|
+
*/
|
|
1555
|
+
function subagentCatalogOf(projectionsBySession, parentId) {
|
|
1556
|
+
if (!isRecord(projectionsBySession)) return undefined;
|
|
1557
|
+
const snapshot = projectionsBySession[String(parentId)];
|
|
1558
|
+
const entries = isRecord(snapshot) ? snapshot.values?.subagentCatalog : undefined;
|
|
1559
|
+
return Array.isArray(entries) ? entries : undefined;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/** 子代理的展示标题:优先目录 label,其次 displayTitle,兜底 "子任务"。
|
|
1563
|
+
* 第 4 参为 0.2.0 的列表快照 `projectionsBySession`(旧 subagentsByParent 移除)。 */
|
|
1564
|
+
function subagentTitle(parentId, id, byId, projectionsBySession = {}) {
|
|
1565
|
+
const entry = subagentCatalogOf(projectionsBySession, parentId)?.find(
|
|
1566
|
+
(candidate) => String(candidate?.id) === String(id),
|
|
1567
|
+
);
|
|
1535
1568
|
const label = cleanText(entry?.label);
|
|
1536
1569
|
if (label) return label;
|
|
1537
1570
|
const display = cleanText(byId[id]?.displayTitle);
|
|
@@ -1580,9 +1613,9 @@ function mainTitle(byId, id) {
|
|
|
1580
1613
|
function buildEntries(snapshot, workspaceItems, detailsById = {}, completions = null, delegatingIds = null, archivedIds = [], waitingStarts = null, jobsBySession = null) {
|
|
1581
1614
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
1582
1615
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
1583
|
-
const current = snapshot
|
|
1584
|
-
const
|
|
1585
|
-
? snapshot.
|
|
1616
|
+
const current = currentIdOf(snapshot);
|
|
1617
|
+
const projectionsBySession = isRecord(snapshot?.projectionsBySession)
|
|
1618
|
+
? snapshot.projectionsBySession
|
|
1586
1619
|
: {};
|
|
1587
1620
|
const archived = archivedIds instanceof Set
|
|
1588
1621
|
? new Set([...archivedIds].map((id) => String(id)))
|
|
@@ -1716,7 +1749,7 @@ function buildEntries(snapshot, workspaceItems, detailsById = {}, completions =
|
|
|
1716
1749
|
: "awaiting",
|
|
1717
1750
|
descendantActive: m.descendantActive,
|
|
1718
1751
|
title: m.isSub
|
|
1719
|
-
? subagentTitle(parentId, id, byId,
|
|
1752
|
+
? subagentTitle(parentId, id, byId, projectionsBySession)
|
|
1720
1753
|
: mainTitle(byId, id),
|
|
1721
1754
|
workspaceTitle: workspace.title,
|
|
1722
1755
|
workspaceKey: workspace.key,
|
|
@@ -2407,7 +2440,7 @@ function createSessionEventSerializer() {
|
|
|
2407
2440
|
function buildRecent(snapshot, workspaceItems, now, detailsById = {}, archivedIds = [], completions = null, delegatingIds = null, turnEnds = null, jobsBySession = null) {
|
|
2408
2441
|
const byId = isRecord(snapshot) && isRecord(snapshot.byId) ? snapshot.byId : {};
|
|
2409
2442
|
const ids = Array.isArray(snapshot?.ids) ? snapshot.ids : [];
|
|
2410
|
-
const current = snapshot
|
|
2443
|
+
const current = currentIdOf(snapshot);
|
|
2411
2444
|
const items = Array.isArray(workspaceItems) ? workspaceItems : [];
|
|
2412
2445
|
const activeIds = activeSessionIds(byId);
|
|
2413
2446
|
const archived = archivedIds instanceof Set ? archivedIds : new Set(archivedIds ?? []);
|
|
@@ -2654,13 +2687,16 @@ function shouldDismissDrawerOnActivation({ targetId, currentId, mobile, drawerOp
|
|
|
2654
2687
|
}
|
|
2655
2688
|
|
|
2656
2689
|
/**
|
|
2657
|
-
* 调用 DSH
|
|
2658
|
-
*
|
|
2690
|
+
* 调用 DSH 原生会话导航(dsh 0.2.0,T-159):经 uiWorkspace.openSession 发起
|
|
2691
|
+
* 「选中会话并显示其 Conversation」的导航动作(内部以 source "mainView" retain
|
|
2692
|
+
* 并驱动外壳 selection;替代 0.2.0 已删除的 sessions.open)。发起为 void 且可被
|
|
2693
|
+
* 后续导航取代,是否到达由调用方的 currentId 重试链判定;target 亦接受子代理
|
|
2694
|
+
* 持久父地址(SessionTarget)。服务缺失返回 false,由调用方决定重试策略。
|
|
2659
2695
|
*/
|
|
2660
|
-
function openSession(
|
|
2661
|
-
if (typeof
|
|
2696
|
+
function openSession(uiWorkspace, target) {
|
|
2697
|
+
if (typeof uiWorkspace?.openSession !== "function") return false;
|
|
2662
2698
|
try {
|
|
2663
|
-
|
|
2699
|
+
uiWorkspace.openSession(target);
|
|
2664
2700
|
return true;
|
|
2665
2701
|
} catch {
|
|
2666
2702
|
return false;
|
|
@@ -2757,15 +2793,26 @@ function bindBackdropDismiss(backdrop, dismiss) {
|
|
|
2757
2793
|
//
|
|
2758
2794
|
// 数据来源:DSH 原生 `sessions` / `workspaces` 客户端服务(推送式快照)+ `remote`
|
|
2759
2795
|
// 服务的部署级模型目录(session/modelCatalog)与会话日志分页(session/page,
|
|
2760
|
-
// Session 格式 V3
|
|
2761
|
-
//
|
|
2762
|
-
//
|
|
2796
|
+
// Session 格式 V3——快照不再携带会话内容、connection.api 门面移除)+ `jobs` 服务的
|
|
2797
|
+
// 会话任务 roster(watchRows/state.rows,0.2.0 起 jobsBySession 自列表快照移除)+
|
|
2798
|
+
// `uiSession.sessionStatus` 统一状态面(running/pendingInteraction/completionUnread,
|
|
2799
|
+
// 0.2.0 取代 pendingInteractions)+ 运行中会话的原生订阅(retain 引用的 binding,
|
|
2800
|
+
// 运行状态面与 eventSource 日志窗口)+ 可选 `modelDirectories` 目录 store 订阅
|
|
2801
|
+
// (模型选择实时更新),不依赖任何第三方插件数据路由,也不做状态轮询。
|
|
2802
|
+
//
|
|
2803
|
+
// 会话代际生命周期(dsh 0.2.0,T-170):`binding()` 只借用已保留代际,插件经
|
|
2804
|
+
// `sessions.retain(target, { source: "dsh-activity-pane" })` 显式持有可见卡片的
|
|
2805
|
+
// 会话引用(子代理以 `sessions.subagentAddress()` 解析的持久父地址为 target),
|
|
2806
|
+
// `reference.ready` 落定后接线 binding.eventSource 日志窗口与 session 运行态订阅;
|
|
2807
|
+
// 引用随可见性 prune、紧凑档拆窗、服务代际更替与卸载释放。
|
|
2763
2808
|
|
|
2764
2809
|
const name = "dsh-activity-pane";
|
|
2765
|
-
// remote.session
|
|
2766
|
-
//
|
|
2767
|
-
// 子命名空间即可解析,无需再注入父面)。
|
|
2768
|
-
|
|
2810
|
+
// remote.session:会话 RPC(目录/日志分页)经 api-remotes 的点分命名空间注入
|
|
2811
|
+
// (cordis 代理对未注入属性直接抛错,父服务声明不代表子命名空间可用;仅声明
|
|
2812
|
+
// 子命名空间即可解析,无需再注入父面)。jobs/uiWorkspace:0.2.0 起后台任务
|
|
2813
|
+
// roster 与会话导航分别由 jobs 服务(dsh-api-job-controller client 半面)与
|
|
2814
|
+
// uiWorkspace.openSession(dsh-client-ui-workspace)承载。
|
|
2815
|
+
const inject = ["sessions", "workspaces", "uiSession", "remote.session", "jobs", "uiWorkspace"];
|
|
2769
2816
|
|
|
2770
2817
|
const CONVERSATION_SELECTOR = "#root [data-slot=\"main\"]";
|
|
2771
2818
|
const PANE_ATTR = "data-dsh-activity-pane";
|
|
@@ -2888,8 +2935,10 @@ const CSS = `
|
|
|
2888
2935
|
-webkit-overflow-scrolling: touch;
|
|
2889
2936
|
padding: 0 0 10px;
|
|
2890
2937
|
/* Keep the native scrollbar just left of the edge handle, as in the host
|
|
2891
|
-
sidebar; stable keeps the list geometry unchanged when overflow appears.
|
|
2892
|
-
|
|
2938
|
+
sidebar; stable keeps the list geometry unchanged when overflow appears.
|
|
2939
|
+
max() 下限 = 调宽手柄宽度(6px):dsh 0.2.0 起宿主 scrollbar-width token
|
|
2940
|
+
收窄到 5px,裸 token 会让手柄压住滚动条命中区(T-170)。 */
|
|
2941
|
+
margin-right: max(var(--dsh-scrollbar-width, 8px), 6px);
|
|
2893
2942
|
scrollbar-gutter: stable;
|
|
2894
2943
|
}
|
|
2895
2944
|
/* 滚动条在滚动或鼠标进入窗格时显示(R-01-004/AC-03,与外壳侧栏一致):thumb 默认透明,
|
|
@@ -3489,17 +3538,27 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3489
3538
|
border-color: rgba(255, 255, 255, 0.13);
|
|
3490
3539
|
filter: none;
|
|
3491
3540
|
}
|
|
3492
|
-
/* 任务卡行 1(R-01-023/AC-05):状态点 + 工具名称 + 右缘随时钟时长;工具名不可得时
|
|
3493
|
-
|
|
3541
|
+
/* 任务卡行 1(R-01-023/AC-05、AC-09):状态点 + 工具名称 + 右缘随时钟时长;工具名不可得时
|
|
3542
|
+
文本段隐藏,仅保留状态点与时长。状态点活性脉冲(AC-09):与运行中主会话卡标题状态点
|
|
3543
|
+
同源同节奏(dap-pulse 1.2s),光晕与着色同色相——运行中任务蓝、停止中琥珀。 */
|
|
3494
3544
|
[data-dsh-activity-pane] .dap-card[data-kind="job"] .dap-job-kind {
|
|
3495
3545
|
flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
3496
3546
|
font-size: 11px; line-height: 15px;
|
|
3497
3547
|
}
|
|
3498
3548
|
[data-dsh-activity-pane] .dap-job-dot {
|
|
3499
3549
|
flex: none; width: 6px; height: 6px; border-radius: 50%;
|
|
3550
|
+
}
|
|
3551
|
+
[data-dsh-activity-pane] .dap-job-dot[data-status="running"] {
|
|
3500
3552
|
background: #65a0ff;
|
|
3553
|
+
box-shadow: 0 0 6px rgba(101, 160, 255, 0.8);
|
|
3554
|
+
}
|
|
3555
|
+
[data-dsh-activity-pane] .dap-job-dot[data-status="stopping"] { background: #f5a524; box-shadow: 0 0 6px rgba(245, 165, 36, 0.8); }
|
|
3556
|
+
/* 活性门控(AC-09):着色、光晕与脉冲只作用于 running/stopping 两个在跑状态,AC 条件由
|
|
3557
|
+
CSS 显式表达,不依赖「liveJobs 只收 running/stopping」的跨层隐式前提(双轴复审修复)。 */
|
|
3558
|
+
[data-dsh-activity-pane] .dap-job-dot[data-status="running"],
|
|
3559
|
+
[data-dsh-activity-pane] .dap-job-dot[data-status="stopping"] {
|
|
3560
|
+
animation: dap-pulse 1.2s ease-in-out infinite;
|
|
3501
3561
|
}
|
|
3502
|
-
[data-dsh-activity-pane] .dap-job-dot[data-status="stopping"] { background: #f5a524; }
|
|
3503
3562
|
[data-dsh-activity-pane] .dap-job-elapsed {
|
|
3504
3563
|
flex: none; margin-left: auto; font-size: 10px; line-height: 15px;
|
|
3505
3564
|
color: color-mix(in srgb, currentColor 55%, transparent); font-variant-numeric: tabular-nums;
|
|
@@ -3721,26 +3780,67 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3721
3780
|
.dap-toggle .dap-toggle-count .dap-spinner {
|
|
3722
3781
|
vertical-align: middle;
|
|
3723
3782
|
}
|
|
3724
|
-
/*
|
|
3725
|
-
|
|
3783
|
+
/* 移动端「活动」开关按钮:仅在窄屏显示(桌面隐藏)。呈紧凑形态(仅计数徽标,
|
|
3784
|
+
无图标与文字标签,T-162)——面板图标被东家误读为边栏按钮,撤销)。常态嵌入
|
|
3785
|
+
宿主头部的标题行行首作为布局子项参与排布(R-01-008/AC-04,T-163;视觉上位于
|
|
3786
|
+
左边栏切换按钮右侧,其余头部控件被真实后挤);标题行不可得且宿主侧栏展开按钮
|
|
3787
|
+
在屏时落位 hero 形态贴靠该按钮右侧(T-167)。落位由 placeToggle 驱动
|
|
3788
|
+
(data-embedded / data-hero 属性承载);未落位的开关不进入 DOM(不可嵌即不
|
|
3789
|
+
渲染,C-093)。本基规则不再携带 fixed 定位与高层级 z-index:按钮处于 flex
|
|
3790
|
+
容器时其 z-index 即使 static 也生效(CSS Flexbox 规范),高层级声明曾使嵌入
|
|
3791
|
+
态整体逃逸宿主抽屉之上(T-167 根因)。按钮本体无外层胶囊壳:不携带底色、
|
|
3792
|
+
描边与投影(T-168,东家指示只留内部涂色的计数胶囊),可见形态即
|
|
3793
|
+
.dap-toggle-count 徽标本体;border:none 与 background:none 显式压掉 UA
|
|
3794
|
+
默认按钮壳——appearance:none 不摘除 UA 声明的边框与底色,WebKit 探针实测
|
|
3795
|
+
漏出 2px 灰壳;外层命中盒最小 44×38px,内层计数徽标最小 36×24px,
|
|
3796
|
+
计数文字 12px、水平 padding 8px(R-01-008/AC-07、C-095)。
|
|
3797
|
+
width:max-content + appearance:none + max-width:64px 显式钉宽并钳硬上限;
|
|
3798
|
+
两位计数时外层命中盒保持至少 44px 宽,较长计数沿既有上限处理。 */
|
|
3726
3799
|
.dap-toggle {
|
|
3727
|
-
position: fixed; top: 12px; left: 44px; z-index: 2147482991;
|
|
3728
3800
|
display: none;
|
|
3729
|
-
align-items: center; gap:
|
|
3730
|
-
min-height:
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
color: currentColor;
|
|
3801
|
+
align-items: center; gap: 5px;
|
|
3802
|
+
min-width: 44px; height: 38px; min-height: 38px; padding: 0;
|
|
3803
|
+
appearance: none; white-space: nowrap; width: max-content; max-width: 64px;
|
|
3804
|
+
box-sizing: border-box;
|
|
3805
|
+
border: none; background: none;
|
|
3735
3806
|
font-size: 12px; font-weight: 600;
|
|
3736
3807
|
cursor: pointer;
|
|
3737
|
-
|
|
3808
|
+
}
|
|
3809
|
+
/* 嵌入态:回归文档流参与标题行排布;flex:none + width:max-content +
|
|
3810
|
+
max-width:64px 在此重复声明是特异度保险——宿主样式表的 (0,1,1) 级后代规则
|
|
3811
|
+
可压过 0-1-0 的基规则,嵌入态处于宿主子树内必须自带 (0,2,0) 钉宽;min-width
|
|
3812
|
+
44px、height 38px 与宿主移动控件对齐;右缘 8px 与标题簇分隔。z-index:1 为层级归位
|
|
3813
|
+
(T-167):flex 子项的 z-index 即使 static 也生效,置 1 使按钮略高于行内
|
|
3814
|
+
兄弟、仍远低于宿主任何覆盖层(移动壳层抽屉 1300 / 遮罩 1250 / 菜单 1100),
|
|
3815
|
+
抽屉滑入即被正常遮挡;同时免疫宿主 header 内 absolute 装饰的意外覆盖。 */
|
|
3816
|
+
.dap-toggle[data-embedded] {
|
|
3817
|
+
position: static;
|
|
3818
|
+
margin-right: 8px;
|
|
3819
|
+
flex: none;
|
|
3820
|
+
width: max-content; max-width: 64px; min-width: 44px;
|
|
3821
|
+
height: 38px; min-height: 38px;
|
|
3822
|
+
z-index: 1;
|
|
3823
|
+
}
|
|
3824
|
+
/* hero 形态(T-167):标题行不可得(宿主 data-phase="hero",英雄页/空白会话)
|
|
3825
|
+
时贴靠宿主侧栏展开按钮(dsh-web-mobile ⊡,data-mobile-nav="fab"/"toggle")
|
|
3826
|
+
右侧 8px 并垂直居中,几何由 placeToggle 内联钉死(T-164 策略),此处承载
|
|
3827
|
+
position 缺省。z-index:1060 等同页面内容层:低于壳层遮罩(1250)、宿主抽屉
|
|
3828
|
+
(1300)与宿主菜单(1100,抽屉打开期间壳层把宿主浮层抬至 1400 同样盖住它),
|
|
3829
|
+
抽屉滑入即被逐步遮挡;数值耦合 dsh-web-mobile 层契约,壳层改版会漂移,记录
|
|
3830
|
+
为边界(同 T-165 判据耦合)。 */
|
|
3831
|
+
.dap-toggle[data-hero] {
|
|
3832
|
+
position: absolute;
|
|
3833
|
+
z-index: 1060;
|
|
3738
3834
|
}
|
|
3739
3835
|
.dap-toggle .dap-toggle-count {
|
|
3740
|
-
|
|
3836
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
3837
|
+
box-sizing: border-box; min-width: 36px; max-width: 64px; height: 24px;
|
|
3838
|
+
line-height: 24px; text-align: center; border-radius: 999px;
|
|
3839
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
3741
3840
|
background: color-mix(in srgb, currentColor 16%, transparent);
|
|
3742
|
-
padding: 0
|
|
3841
|
+
padding: 0 8px; font-size: 12px; font-weight: 700;
|
|
3743
3842
|
}
|
|
3843
|
+
/* 徽标等待态:执行色底 + 呼吸动画(有待执行行动时点亮)。 */
|
|
3744
3844
|
.dap-toggle[data-awaiting] .dap-toggle-count {
|
|
3745
3845
|
background: rgba(46, 42, 26, 0.97);
|
|
3746
3846
|
animation: dap-await-pulse 1.2s ease-in-out infinite;
|
|
@@ -3803,12 +3903,18 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-workspace {
|
|
|
3803
3903
|
body,不落本规则,浮动开关徽标脉冲(R-01-002/AC-06、AC-07)照常。 */
|
|
3804
3904
|
[data-dsh-activity-pane]:not([data-open="true"]) { content-visibility: hidden; }
|
|
3805
3905
|
.dap-backdrop[data-drawer-open] { display: block; }
|
|
3806
|
-
|
|
3807
|
-
|
|
3906
|
+
/* 开关显隐改为落位属性驱动(T-167):嵌入态/hero 态各自 display:flex,
|
|
3907
|
+
未落位(不在 DOM)自然不显示;fixed 兜底形态已去除。 */
|
|
3908
|
+
.dap-toggle[data-embedded] { display: flex; }
|
|
3909
|
+
.dap-toggle[data-hero] { display: flex; }
|
|
3910
|
+
/* 插件抽屉打开时开关留位隐藏:visibility 不可见且不可命中(满足 AC-05 隐藏),
|
|
3911
|
+
标题行不因开合重排(T-160 闸口决策);pointer-events 同步摘除,不可见盒
|
|
3912
|
+
不拦截宿主抽屉点击;hero 态 absolute 脱流,留位语义等价。 */
|
|
3913
|
+
.dap-toggle[data-drawer-open] { display: flex; visibility: hidden; pointer-events: none; }
|
|
3808
3914
|
}
|
|
3809
3915
|
/* 浅色主题适配:外壳以 body 上 data-ds-dark-theme 属性标记深色(缺省即浅色),
|
|
3810
3916
|
并在两个作用域下翻转整套 --dsw-alias-* 变量。本块只覆盖上文暗色专用的硬编码
|
|
3811
|
-
|
|
3917
|
+
颜色(卡片底色/描边/阴影、时间线文字与轨道、进度轨道),深色主题下
|
|
3812
3918
|
全部规则保持原值;状态色(绿/蓝/橙/红)与 currentColor 派生色两主题通用,不覆盖。
|
|
3813
3919
|
::before 状态圆点不覆盖:基色本就被各 data-status 规则接管,覆盖反而会以更高
|
|
3814
3920
|
优先级压掉运行/完成/错误状态色。 */
|
|
@@ -3901,9 +4007,6 @@ body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-trace-item[data-ico
|
|
|
3901
4007
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-track {
|
|
3902
4008
|
background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, 0.08));
|
|
3903
4009
|
}
|
|
3904
|
-
body:not([data-ds-dark-theme]) .dap-toggle {
|
|
3905
|
-
background: var(--dsw-alias-button-floating-fill, rgba(255, 255, 255, 0.94));
|
|
3906
|
-
}
|
|
3907
4010
|
/* 「回到顶部」与标题行档位按钮及左侧仓库入口的浅色覆盖:不透明层-2 底色与外壳描边别名
|
|
3908
4011
|
(R-01-018/AC-05、R-01-021/AC-05、R-01-022/AC-01);仓库入口无描边(T-139),只并入底色组。 */
|
|
3909
4012
|
body:not([data-ds-dark-theme]) [data-dsh-activity-pane] .dap-top,
|
|
@@ -4029,9 +4132,11 @@ function apply(ctx) {
|
|
|
4029
4132
|
let sessions = null;
|
|
4030
4133
|
let workspaces = null;
|
|
4031
4134
|
let uiSession = null;
|
|
4135
|
+
let jobs = null;
|
|
4032
4136
|
let sessionUnsubscribe = null;
|
|
4033
4137
|
let workspaceUnsubscribe = null;
|
|
4034
4138
|
let pendingUnsubscribe = null;
|
|
4139
|
+
let jobsUnsubscribe = null;
|
|
4035
4140
|
let clockTimer = null;
|
|
4036
4141
|
let recentTimeTimer = null;
|
|
4037
4142
|
let syncScheduled = false;
|
|
@@ -4077,8 +4182,13 @@ function apply(ctx) {
|
|
|
4077
4182
|
* 重挂载后保留(R-01-021/AC-06)。 */
|
|
4078
4183
|
let densityLevel = readStoredDensity();
|
|
4079
4184
|
/** 待执行的锚定补偿:档位切换时登记当前选中卡片顶部的视口相对位置,本轮渲染
|
|
4080
|
-
* 提交后量测该卡新位置并补偿 scrollTop(R-01-021/AC-01)。
|
|
4185
|
+
* 提交后量测该卡新位置并补偿 scrollTop(R-01-021/AC-01)。
|
|
4186
|
+
* dsh 0.2.0(T-170 审核):切回更高档位时窗口经 retain→ready 异步重建,时间线
|
|
4187
|
+
* 内容分多帧长回——补偿在沉降窗内逐帧执行,位置稳定或超窗即撤(单帧补偿
|
|
4188
|
+
* 会漏掉后期增长,锚点漂移)。 */
|
|
4081
4189
|
let pendingDensityAnchor = null;
|
|
4190
|
+
/** 锚定补偿沉降窗截止时刻:窗内逐帧补偿,位置稳定提前撤,超窗强制撤。 */
|
|
4191
|
+
let densityAnchorDeadline = 0;
|
|
4082
4192
|
/** 用户最近一次激活的卡片 id;打开重试链被更新的激活意图取代即取消。 */
|
|
4083
4193
|
let lastActivatedId = null;
|
|
4084
4194
|
/** 最近一次已处理的当前卡片;同一卡片的运行时重绘不反复打断用户手动滚动。 */
|
|
@@ -4132,7 +4242,6 @@ function apply(ctx) {
|
|
|
4132
4242
|
let catalogGroups = [];
|
|
4133
4243
|
/** 部署级 modelCatalog 一次性读取的在途 Promise:成功即缓存、失败允许重试。 */
|
|
4134
4244
|
let catalogPromise = null;
|
|
4135
|
-
/** native session.open() requests in flight; avoid duplicate cold history reads. */
|
|
4136
4245
|
/** 冷数据读取并发池:队列顺序即优先级(调用方已排序),逐个完成逐个重绘。 */
|
|
4137
4246
|
const loadQueue = [];
|
|
4138
4247
|
let loadInflight = 0;
|
|
@@ -4153,29 +4262,146 @@ function apply(ctx) {
|
|
|
4153
4262
|
pumpDetailLoads();
|
|
4154
4263
|
return promise;
|
|
4155
4264
|
}
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4265
|
+
/** 插件持有的会话引用登记(dsh 0.2.0 retain 生命周期,T-170):String(id) →
|
|
4266
|
+
* { reference, session, eventSource, pending }。retain 以独立
|
|
4267
|
+
* source `dsh-activity-pane` 持有可见卡片的会话代际(binding 只借用已保留代际,
|
|
4268
|
+
* 旧打开/释放调用已移出会话面);释放点 = 可见性 prune / 紧凑档拆窗 /
|
|
4269
|
+
* 服务代际更替 / 卸载。 */
|
|
4270
|
+
const sessionRefs = new Map();
|
|
4271
|
+
/** 插件 retain source:与宿主 mainView/workspaceOperation 并列的独立消费方标识。 */
|
|
4272
|
+
const PLUGIN_RETAIN_SOURCE = "dsh-activity-pane";
|
|
4273
|
+
/** jobs 服务按会话 roster 订阅(dsh 0.2.0 起 jobsBySession 移除,R-01-023):
|
|
4274
|
+
* String(id) → stop();对列表内全会话订 roster,随列表 prune。 */
|
|
4275
|
+
const jobWatchById = new Map();
|
|
4276
|
+
/** 母会话投影按需拉取记账(T-151 后继):parentId 在途记账,完成即除名允许重试。 */
|
|
4277
|
+
const projectionRefreshes = new Set();
|
|
4159
4278
|
|
|
4160
4279
|
const style = document.createElement("style");
|
|
4161
4280
|
style.id = STYLE_ID;
|
|
4162
4281
|
style.textContent = CSS;
|
|
4163
4282
|
document.head.appendChild(style);
|
|
4164
4283
|
|
|
4165
|
-
// 移动端浮动开关按钮(桌面隐藏见 CSS
|
|
4284
|
+
// 移动端浮动开关按钮(桌面隐藏见 CSS):紧凑形骨架——仅计数徽标,无图标与
|
|
4285
|
+
// 文字标签(R-01-008/AC-04,T-162;面板图标被东家误读为边栏按钮,撤销);
|
|
4286
|
+
// 可访问名称由 aria-label 承担。不预挂 DOM:开关仅在 placeToggle 落位时进入
|
|
4287
|
+
// 文档,未落位即不渲染(C-093,fail-visible——嵌入失败表现为按钮消失)。
|
|
4166
4288
|
const toggle = document.createElement("button");
|
|
4167
4289
|
toggle.className = "dap-toggle";
|
|
4168
4290
|
toggle.type = "button";
|
|
4169
4291
|
toggle.setAttribute("aria-label", "切换活动会话窗格");
|
|
4170
|
-
toggle.innerHTML =
|
|
4171
|
-
"<span>活动</span><span class=\"dap-toggle-count\"></span>";
|
|
4172
|
-
document.body.appendChild(toggle);
|
|
4292
|
+
toggle.innerHTML = "<span class=\"dap-toggle-count\"></span>";
|
|
4173
4293
|
|
|
4174
4294
|
// 移动端抽屉透明遮罩:仅窄屏且抽屉打开时显示,点击收起抽屉(R-01-008/AC-03)。
|
|
4175
4295
|
const backdrop = document.createElement("div");
|
|
4176
4296
|
backdrop.className = "dap-backdrop";
|
|
4177
4297
|
document.body.appendChild(backdrop);
|
|
4178
4298
|
|
|
4299
|
+
// 移动端「活动」开关落位守卫(T-163/T-167/T-171,R-01-008/AC-04):常态嵌入宿主
|
|
4300
|
+
// 头部参与布局——东家指示(左边栏弹出按钮的右侧,其余控件被真实后挤)。0.2.0 起
|
|
4301
|
+
// 头部结构代差:header.firstElementChild 只是左侧窄条 headerLeading;无壳层开关
|
|
4302
|
+
// 占座时仍嵌该行行首;移动壳层开关(data-mobile-nav="toggle")占座时改嵌会话头
|
|
4303
|
+
// 标题行行首(座位右缘)。hero 页头部被壳层隐藏时几何感知回落 hero 形态。
|
|
4304
|
+
// 插入点取行内首个内容簇(标题 crumb 导航所在顶层簇)之前,无簇时行首 prepend;
|
|
4305
|
+
// 幂等:已是行首元素时不动 DOM;宿主重渲染丢弃嵌入节点后由下一轮渲染守卫重插
|
|
4306
|
+
// (头部在 seat 子树内,conversationObserver 的 subtree 监听会经 queueSync
|
|
4307
|
+
// 唤醒本守卫)。
|
|
4308
|
+
// 标题行不可得(宿主 data-phase="hero":英雄页/空白会话;或会话视图未挂载
|
|
4309
|
+
// 窗口期)时落位 hero 形态:贴靠宿主侧栏展开按钮(dsh-web-mobile ⊡,语义属性
|
|
4310
|
+
// data-mobile-nav="toggle"/"fab",互斥、任一时刻至多一个在屏)右侧 8px 并垂直
|
|
4311
|
+
// 居中,absolute 挂宿主 frame([data-shell-overlay] 的 parentElement,与壳层
|
|
4312
|
+
// findFrame 同源,东家确认;hero 页无布局流容器可嵌,C-093)。两落位均不可得时
|
|
4313
|
+
// 摘除开关不渲染——静默浮层降级已去除(T-167,fail-visible)。
|
|
4314
|
+
// 在屏判据阈值:元素 x 超过 -10px 视为在屏内(移动壳层屏外收起抽屉 x 约 -300)
|
|
4315
|
+
const ON_SCREEN_X = -10;
|
|
4316
|
+
function placeToggle() {
|
|
4317
|
+
const seat = document.querySelector(CONVERSATION_SELECTOR);
|
|
4318
|
+
const header = seat !== null ? (seat.querySelector("header") ?? null) : null;
|
|
4319
|
+
const row = header !== null ? (header.firstElementChild ?? null) : null;
|
|
4320
|
+
// 几何感知(T-171):0.2.0 起 hero 页 header.firstElementChild 存在(左侧窄条
|
|
4321
|
+
// headerLeading),但移动壳层对 headerBlank/headerHidden 头部整条 display:none
|
|
4322
|
+
// ——行存在不等于行可见,嵌入其中会被连带隐藏(0.2.0 移动空白页入口消失根因)。
|
|
4323
|
+
// 可见性判据取 header 包围盒:headerLeading 自身可为 0 宽(未嵌入时槽内容被壳
|
|
4324
|
+
// 层隐藏,列宽塌缩),header 可见即行可嵌。宽高归零即回落 hero 形态贴靠 ⊡。
|
|
4325
|
+
const headerRect = header !== null ? header.getBoundingClientRect() : null;
|
|
4326
|
+
const headerVisible = headerRect !== null && headerRect.width > 0 && headerRect.height > 0;
|
|
4327
|
+
if (row !== null && headerVisible) {
|
|
4328
|
+
// 壳层左座检测(T-171):移动壳层在头部左缘 absolute 挂自己的目录开关
|
|
4329
|
+
// (data-mobile-nav="toggle",实测 [8,6,28,28]),嵌入 headerLeading 行首
|
|
4330
|
+
// 会与之同位叠置(0.2.0 active 页双开关冲突根因)。检出在屏壳层开关时改嵌
|
|
4331
|
+
// 会话头标题行(conversation.session.header 槽渲染的 titleRow,壳层以
|
|
4332
|
+
// padding-left:32 让出座位)行首——开关落座位右缘,标题被真实后挤;
|
|
4333
|
+
// 标题行不可得时维持 headerLeading 旧落位(叠置边界记录 T-171)。
|
|
4334
|
+
const shellToggle = [...header.querySelectorAll('button[data-mobile-nav="toggle"]')].find((el) => {
|
|
4335
|
+
const rect = el.getBoundingClientRect();
|
|
4336
|
+
return rect.width > 0 && rect.x > ON_SCREEN_X;
|
|
4337
|
+
});
|
|
4338
|
+
let target = row;
|
|
4339
|
+
if (shellToggle !== undefined) {
|
|
4340
|
+
const titleRow = header.querySelector('[data-slot="conversation.session.header"]')?.firstElementChild ?? null;
|
|
4341
|
+
if (titleRow !== null) target = titleRow;
|
|
4342
|
+
}
|
|
4343
|
+
// 插入点取行内首个内容簇(标题簇)之前:行首即视觉上位于左侧边栏切换按钮
|
|
4344
|
+
// 的右侧,其余头部控件被真实后挤(相对顺序契约,不断言行首)。簇内含 crumb
|
|
4345
|
+
// 导航即认定标题簇(nav 必在 head 子树内,顶层簇即 head 本身)。
|
|
4346
|
+
const head = target.firstElementChild;
|
|
4347
|
+
const cluster = head !== null && (head.tagName === "NAV" || head.querySelector("nav") !== null)
|
|
4348
|
+
? head
|
|
4349
|
+
: null;
|
|
4350
|
+
const settled = toggle.parentElement === target && toggle.hasAttribute("data-embedded") &&
|
|
4351
|
+
(cluster !== null ? toggle.nextSibling === cluster : target.firstElementChild === toggle);
|
|
4352
|
+
if (settled) return;
|
|
4353
|
+
if (cluster !== null) target.insertBefore(toggle, cluster);
|
|
4354
|
+
else target.prepend(toggle);
|
|
4355
|
+
toggle.setAttribute("data-embedded", "");
|
|
4356
|
+
toggle.removeAttribute("data-hero");
|
|
4357
|
+
// 定位关键量内联钉死:嵌入态回归文档流不依赖样式表状态(东家真机
|
|
4358
|
+
// 疑似样式与脚本状态失配,T-164);样式表只承担外观。
|
|
4359
|
+
toggle.style.position = "static";
|
|
4360
|
+
toggle.style.top = "";
|
|
4361
|
+
toggle.style.left = "";
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
// hero 形态:锚定宿主侧栏展开按钮(⊡)右侧。toggle/fab 属性值互斥(会话头
|
|
4365
|
+
// toggle 挂 header.actions 槽、hero 页 header 为空壳不渲染槽内容),统一
|
|
4366
|
+
// 寻址取首个在屏者;抽屉打开期间壳层在 drawerOpen 翻转的同一 reconciler 遍
|
|
4367
|
+
// 内移除 ⊡(overlay-backdrop-fab ensure),锚不可测走保留分支——开关已被
|
|
4368
|
+
// 抽屉与遮罩在层级上盖住,保留原位即「一直在抽屉后面被挡住」(T-168)。
|
|
4369
|
+
let fabEl = null;
|
|
4370
|
+
for (const fab of document.querySelectorAll('button[data-mobile-nav="toggle"], button[data-mobile-nav="fab"]')) {
|
|
4371
|
+
const rect = fab.getBoundingClientRect();
|
|
4372
|
+
if (rect.width > 0 && rect.x > ON_SCREEN_X) {
|
|
4373
|
+
fabEl = fab;
|
|
4374
|
+
break;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
const frame = document.querySelector("[data-shell-overlay]")?.parentElement ?? null;
|
|
4378
|
+
if (fabEl === null || frame === null) {
|
|
4379
|
+
// 锚或挂载点不可得:已落位开关保留原位几何——宿主抽屉打开窗口内壳层移除
|
|
4380
|
+
// ⊡,开关由抽屉(1300)与遮罩(1250)遮挡,摘除会在关抽屉后经复检凭空
|
|
4381
|
+
// 重现(T-168 东家否决);随失效行/旧 frame detach 的开关不可见,由下一
|
|
4382
|
+
// 次成功落位回收。未落位开关不在此进入文档(不渲染,C-093);壳层结构
|
|
4383
|
+
// 缺失(frame 不可得)时仍摘除已挂载开关。宿主改版致锚长期缺失时开关
|
|
4384
|
+
// 残留于最后有效位置,记录为边界(T-168 验证矩阵)。
|
|
4385
|
+
if (frame === null && toggle.parentElement !== null) toggle.remove();
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
if (toggle.parentElement !== frame) frame.appendChild(toggle);
|
|
4389
|
+
toggle.removeAttribute("data-embedded");
|
|
4390
|
+
toggle.setAttribute("data-hero", "");
|
|
4391
|
+
// 定位关键量内联钉死(T-164 策略):absolute 几何不依赖样式表状态;
|
|
4392
|
+
// 38 为样式表 height 缺省,量测失败时保底。
|
|
4393
|
+
// 几何取锚的布局坐标(offsetLeft/Top/Width/Height)而非视口 rect(T-172):
|
|
4394
|
+
// 两者同为 frame 直系子项、offsetParent 同为 frame,布局坐标即 frame 内
|
|
4395
|
+
// absolute 坐标本身;视口 rect 含 frame 自身位移(iOS rubber-band/滚动期
|
|
4396
|
+
// 合成层位移),烘进 frame 内坐标等于把该位移重复计入一次——位移期间每次
|
|
4397
|
+
// 重落位都让开关相对 ⊡ 多漂一份位移(实测 40px 位移重落位后总位移 80px),
|
|
4398
|
+
// 且离散重落位呈顿挫。布局坐标不受祖先 transform 影响,重落位幂等稳定。
|
|
4399
|
+
const toggleHeight = toggle.offsetHeight || 38;
|
|
4400
|
+
toggle.style.position = "absolute";
|
|
4401
|
+
toggle.style.top = `${Math.round(fabEl.offsetTop + (fabEl.offsetHeight - toggleHeight) / 2)}px`;
|
|
4402
|
+
toggle.style.left = `${Math.round(fabEl.offsetLeft + fabEl.offsetWidth + 8)}px`;
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4179
4405
|
// 桌面判定与"真实参与布局":中间列切为行方向,窗格固定宽、会话根弹性填充
|
|
4180
4406
|
// 剩余宽度(flex:1 + min-width:0),让会话内容(标题/tabs/滚动区/输入框)
|
|
4181
4407
|
// 被真实挤到窗格右侧,且聊天列在增宽的会话带内自动居中。
|
|
@@ -4491,34 +4717,37 @@ function apply(ctx) {
|
|
|
4491
4717
|
throw response?.error ?? new Error("remote request failed");
|
|
4492
4718
|
}
|
|
4493
4719
|
|
|
4494
|
-
/** 母会话子代理目录条目查找(T-151
|
|
4495
|
-
*
|
|
4496
|
-
function subagentCatalogEntry(
|
|
4497
|
-
const entries =
|
|
4498
|
-
return
|
|
4499
|
-
? entries.find((candidate) => String(candidate?.id) === String(id))
|
|
4500
|
-
: undefined;
|
|
4720
|
+
/** 母会话子代理目录条目查找(T-151 单源;T-170 改读 0.2.0 投影):open 地址与
|
|
4721
|
+
* 深翻分页地址共用同一取法,防条目形状或键名演化时两处漂移。 */
|
|
4722
|
+
function subagentCatalogEntry(projectionsBySession, parentId, id) {
|
|
4723
|
+
const entries = subagentCatalogOf(projectionsBySession, parentId);
|
|
4724
|
+
return entries?.find((candidate) => String(candidate?.id) === String(id));
|
|
4501
4725
|
}
|
|
4502
4726
|
|
|
4503
4727
|
/** 会话日志分页地址(R-01-012):主会话 `{kind:"session", sessionId}`;子代理
|
|
4504
|
-
* `{kind:"subagent", parentSessionId, childSessionId, mode}
|
|
4505
|
-
*
|
|
4506
|
-
*
|
|
4507
|
-
*
|
|
4508
|
-
*
|
|
4509
|
-
|
|
4728
|
+
* `{kind:"subagent", parentSessionId, childSessionId, mode}`。mode 必须与子代理
|
|
4729
|
+
* 描述符一致,不一致被宿主以 subagent/unauthorized 拒绝(T-151):优先取母会话
|
|
4730
|
+
* 目录条目的 mode;目录条目缺失(未加载、在途空窗或无该子条目)时以 `unknown`
|
|
4731
|
+
* 兜底——0.2.0 SessionAddress 新增该合法值(旧行 `continuable` 字段已随 0.2.0
|
|
4732
|
+
* 行形状移除,启发不再可靠;读取失败仍由 pagedHistoryEvents 以 null 收敛为空白
|
|
4733
|
+
* 详情,R-01-013)。 */
|
|
4734
|
+
/** 子代理行的母会话 id(T-170 审核发现 7):0.2.0 行形状 parentSessionId,
|
|
4735
|
+
* 兼容旧 parentId 键,单点解析避免三处漂移。 */
|
|
4736
|
+
function parentSessionIdOf(row) {
|
|
4737
|
+
return row?.parentSessionId ?? row?.parentId;
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
function sessionPageAddress(id, byId, projectionsBySession = null) {
|
|
4510
4741
|
const row = byId[id] ?? {};
|
|
4511
4742
|
if (row?.origin === "subagent") {
|
|
4512
|
-
const parentId = row
|
|
4743
|
+
const parentId = parentSessionIdOf(row);
|
|
4513
4744
|
if (parentId !== undefined && parentId !== null) {
|
|
4514
|
-
const entry = subagentCatalogEntry(
|
|
4745
|
+
const entry = subagentCatalogEntry(projectionsBySession, String(parentId), id);
|
|
4515
4746
|
return {
|
|
4516
4747
|
kind: "subagent",
|
|
4517
4748
|
parentSessionId: String(parentId),
|
|
4518
4749
|
childSessionId: String(id),
|
|
4519
|
-
mode: typeof entry?.mode === "string" && entry.mode !== ""
|
|
4520
|
-
? entry.mode
|
|
4521
|
-
: row.continuable === true ? "continuable" : "one-shot",
|
|
4750
|
+
mode: typeof entry?.mode === "string" && entry.mode !== "" ? entry.mode : "unknown",
|
|
4522
4751
|
};
|
|
4523
4752
|
}
|
|
4524
4753
|
}
|
|
@@ -4662,7 +4891,21 @@ function apply(ctx) {
|
|
|
4662
4891
|
if (deepReadNeeded && !historyLoads.has(id)) {
|
|
4663
4892
|
detail.previewFallbackLoaded = true;
|
|
4664
4893
|
detail.durationFallbackLoaded = true;
|
|
4665
|
-
|
|
4894
|
+
// 子代理目录未加载时先拉母会话投影再重试(T-170 审核):0.2.0 起
|
|
4895
|
+
// 子代理分页地址的 mode 取自 projectionsBySession 目录,缺目录时
|
|
4896
|
+
// 以 mode:"unknown" 贸然翻页可能被宿主拒绝、首读收敛为空白;
|
|
4897
|
+
// 与 retain 路径同款单发拉取(ensureParentProjections),下一轮
|
|
4898
|
+
// 渲染目录就位后按真实 mode 重试。
|
|
4899
|
+
const rowById = byId[id] ?? {};
|
|
4900
|
+
if (rowById?.origin === "subagent") {
|
|
4901
|
+
const rowParentId = parentSessionIdOf(rowById);
|
|
4902
|
+
if (rowParentId != null && subagentCatalogOf(listSnap?.projectionsBySession ?? null, String(rowParentId)) === undefined) {
|
|
4903
|
+
ensureParentProjections(String(rowParentId));
|
|
4904
|
+
queueSync();
|
|
4905
|
+
continue;
|
|
4906
|
+
}
|
|
4907
|
+
}
|
|
4908
|
+
const address = sessionPageAddress(id, byId, listSnap?.projectionsBySession ?? null);
|
|
4666
4909
|
const promise = enqueueDetailLoad(() => Promise.resolve()
|
|
4667
4910
|
.then(async () => {
|
|
4668
4911
|
const { events, error } = await pagedHistoryEvents({
|
|
@@ -4716,15 +4959,16 @@ function apply(ctx) {
|
|
|
4716
4959
|
* 不 dispose、不拆订阅(由本函数在渲染期持续执法:其切走后的下一轮渲染
|
|
4717
4960
|
* 照常释放);当前会话不可判定(列表快照未就绪)时整轮跳过,不误释放。
|
|
4718
4961
|
* 子代理日志窗口承载标题行可见的模型上下文,保留。切回更高档位时经既有
|
|
4719
|
-
* 渐进路径(captureSessionLog 重订阅 +
|
|
4962
|
+
* 渐进路径(captureSessionLog 重订阅 + retain 重新水合)就绪,
|
|
4720
4963
|
* 不新增恢复机制。 */
|
|
4721
4964
|
function enforceDensityDataDiscipline() {
|
|
4722
4965
|
if (densityLevel !== "compact") return;
|
|
4723
4966
|
const listSnap = getSnapshot(sessions, "list");
|
|
4724
|
-
|
|
4967
|
+
const currentId = currentIdOf(listSnap);
|
|
4968
|
+
if (currentId == null) return;
|
|
4725
4969
|
for (const [id, unsubscribe] of [...logSourceSubs]) {
|
|
4726
4970
|
if (!logWindowSuppressedFor(id, listSnap?.byId ?? {})) continue;
|
|
4727
|
-
if (String(id) === String(
|
|
4971
|
+
if (String(id) === String(currentId)) continue;
|
|
4728
4972
|
try {
|
|
4729
4973
|
unsubscribe?.();
|
|
4730
4974
|
} catch {
|
|
@@ -4733,11 +4977,18 @@ function apply(ctx) {
|
|
|
4733
4977
|
logSourceSubs.delete(id);
|
|
4734
4978
|
const detail = sessionDetailsById.get(id) ?? null;
|
|
4735
4979
|
if (detail !== null) detail.log = null;
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4980
|
+
// dsh 0.2.0 retain 生命周期(T-170):拆窗即释放插件引用——旧
|
|
4981
|
+
// sessions.binding(id).session.dispose() 随 open/dispose 移出会话面而删除。
|
|
4982
|
+
releaseSessionRef(id);
|
|
4983
|
+
}
|
|
4984
|
+
// 无窗口引用的兜底释放(T-170 审核):引用可先于窗口存在(轮内状态订阅
|
|
4985
|
+
// 借用、切档在途),被抑制且非当前会话的残留引用一并释放,不留常驻
|
|
4986
|
+
// retain(R-01-025/AC-01)。
|
|
4987
|
+
for (const id of [...sessionRefs.keys()]) {
|
|
4988
|
+
if (logSourceSubs.has(id)) continue;
|
|
4989
|
+
if (!logWindowSuppressedFor(id, listSnap?.byId ?? {})) continue;
|
|
4990
|
+
if (String(id) === String(currentId)) continue;
|
|
4991
|
+
releaseSessionRef(id);
|
|
4741
4992
|
}
|
|
4742
4993
|
}
|
|
4743
4994
|
|
|
@@ -4765,33 +5016,149 @@ function apply(ctx) {
|
|
|
4765
5016
|
// 当前选择),不可得时保持空白,不以日志历史取值或部署默认冒充(R-01-012/AC-18)。
|
|
4766
5017
|
}
|
|
4767
5018
|
|
|
4768
|
-
/**
|
|
4769
|
-
*
|
|
4770
|
-
*
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
5019
|
+
/** 会话引用获取(dsh 0.2.0 retain 生命周期,T-170):为可见卡片以独立 source
|
|
5020
|
+
* 持有会话代际。子代理行先经 sessions.subagentAddress 解析持久父地址(旧
|
|
5021
|
+
* 会话面地址安装路径随重构删除);地址不可得时先拉取母会话投影
|
|
5022
|
+
* 并返回 null 下轮重试。reference.ready 落定后接线 binding.eventSource 与
|
|
5023
|
+
* session 面(ObservableSnapshot 半面)。返回 { session, eventSource } 或
|
|
5024
|
+
* null(在途/不可得——调用方下轮渲染重试)。 */
|
|
5025
|
+
function liveSessionFace(id) {
|
|
5026
|
+
const key = String(id);
|
|
5027
|
+
const existing = sessionRefs.get(key);
|
|
5028
|
+
if (existing !== undefined) return existing.pending ? null : existing;
|
|
5029
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
5030
|
+
const byId = listSnap?.byId ?? {};
|
|
5031
|
+
let target = id;
|
|
5032
|
+
if (isSubagentRow(byId[id], byId)) {
|
|
5033
|
+
const parentId = String(parentSessionIdOf(byId[id]));
|
|
5034
|
+
let address = null;
|
|
5035
|
+
try {
|
|
5036
|
+
address = sessions?.subagentAddress?.(String(id)) ?? null;
|
|
5037
|
+
} catch {
|
|
5038
|
+
address = null;
|
|
5039
|
+
}
|
|
5040
|
+
if (address === null || address === undefined) {
|
|
5041
|
+
ensureParentProjections(parentId);
|
|
5042
|
+
return null;
|
|
5043
|
+
}
|
|
5044
|
+
target = address;
|
|
5045
|
+
}
|
|
5046
|
+
let reference = null;
|
|
5047
|
+
try {
|
|
5048
|
+
reference = sessions?.retain?.(target, { source: PLUGIN_RETAIN_SOURCE }) ?? null;
|
|
5049
|
+
} catch {
|
|
5050
|
+
reference = null;
|
|
5051
|
+
}
|
|
5052
|
+
if (reference === null) return null;
|
|
5053
|
+
const record = { reference, session: null, eventSource: null, pending: true };
|
|
5054
|
+
sessionRefs.set(key, record);
|
|
5055
|
+
Promise.resolve(reference.ready)
|
|
5056
|
+
.then(() => {
|
|
5057
|
+
if (disposed) {
|
|
5058
|
+
try {
|
|
5059
|
+
reference.release();
|
|
5060
|
+
} catch {
|
|
5061
|
+
/* 已卸载:引用兜底释放 */
|
|
5062
|
+
}
|
|
5063
|
+
return;
|
|
5064
|
+
}
|
|
5065
|
+
let binding = null;
|
|
5066
|
+
try {
|
|
5067
|
+
binding = reference.binding ?? null;
|
|
5068
|
+
} catch {
|
|
5069
|
+
binding = null; // 代际已终结:下次渲染重新 retain
|
|
5070
|
+
}
|
|
5071
|
+
if (binding !== null) {
|
|
5072
|
+
record.session = binding.session ?? null;
|
|
5073
|
+
record.eventSource = binding.eventSource ?? null;
|
|
5074
|
+
}
|
|
5075
|
+
record.pending = false;
|
|
5076
|
+
queueSync();
|
|
5077
|
+
})
|
|
5078
|
+
.catch(() => {
|
|
5079
|
+
record.pending = false;
|
|
5080
|
+
if (!disposed) queueSync();
|
|
5081
|
+
});
|
|
5082
|
+
return null;
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
/** 只借用已接线会话面,不发起 retain(T-170 审核发现 1):运行状态订阅的
|
|
5086
|
+
* 面必须来自既有引用(日志窗口或外壳主视图 retain 的会话)。无引用时返回
|
|
5087
|
+
* null,调用方降级到统一状态面(sessionStatus)与 busy 记账,不得为
|
|
5088
|
+
* 轮内状态订阅单独水合紧凑档非当前主会话(R-01-025/AC-01)。 */
|
|
5089
|
+
function peekSessionFace(id) {
|
|
5090
|
+
const record = sessionRefs.get(String(id));
|
|
5091
|
+
if (record === undefined || record.pending) return null;
|
|
5092
|
+
const session = record.session;
|
|
5093
|
+
const eventSource = record.eventSource;
|
|
5094
|
+
if ((session === null || session === undefined) && (eventSource === null || eventSource === undefined)) return null;
|
|
5095
|
+
return record;
|
|
5096
|
+
}
|
|
5097
|
+
|
|
5098
|
+
/** 释放一个会话引用:release retain 引用(dsh 0.2.0),断流由宿主随引用计数收敛。 */
|
|
5099
|
+
function releaseSessionRef(id) {
|
|
5100
|
+
const key = String(id);
|
|
5101
|
+
const record = sessionRefs.get(key);
|
|
5102
|
+
if (record === undefined) return;
|
|
5103
|
+
sessionRefs.delete(key);
|
|
4775
5104
|
try {
|
|
4776
|
-
|
|
5105
|
+
record.reference?.release?.();
|
|
4777
5106
|
} catch {
|
|
4778
|
-
|
|
5107
|
+
/* 引用已释放 */
|
|
4779
5108
|
}
|
|
4780
|
-
|
|
5109
|
+
}
|
|
5110
|
+
|
|
5111
|
+
/** 母会话投影按需拉取(T-170):0.2.0 起子代理目录随列表快照的
|
|
5112
|
+
* projectionsBySession 下发,未加载时经 refreshProjections 拉取后重试。 */
|
|
5113
|
+
function ensureParentProjections(parentId) {
|
|
5114
|
+
if (typeof sessions?.refreshProjections !== "function") return;
|
|
5115
|
+
const key = String(parentId);
|
|
5116
|
+
if (projectionRefreshes.has(key)) return;
|
|
5117
|
+
projectionRefreshes.add(key);
|
|
5118
|
+
try {
|
|
5119
|
+
Promise.resolve(sessions.refreshProjections(key))
|
|
5120
|
+
.catch(() => {})
|
|
5121
|
+
.finally(() => {
|
|
5122
|
+
projectionRefreshes.delete(key);
|
|
5123
|
+
if (!disposed) queueSync();
|
|
5124
|
+
});
|
|
5125
|
+
} catch {
|
|
5126
|
+
projectionRefreshes.delete(key);
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
// 绑定会话并水合事件源(dsh 0.2.0 retain 生命周期,T-170):retain 的 ready
|
|
5131
|
+
// 承载共享初始历史打开(旧显式会话打开调用随 open 移出会话面而删除),
|
|
5132
|
+
// binding.eventSource 打开即收完整日志窗口 + 实时尾(原生 Conversation 同源)。
|
|
5133
|
+
// 日志窗口快照引用变化即重派生详情(时间线/预览/模型),窗口由宿主按消息对齐分页。
|
|
5134
|
+
function captureSessionLog(id, { cwd } = {}) {
|
|
4781
5135
|
// 档位资源纪律(R-01-025/AC-01):紧凑档主会话不建立或保持会话日志窗口——
|
|
4782
|
-
//
|
|
5136
|
+
// 不订阅事件流、不发起水合、不读窗口快照。判定必须先于 liveSessionFace:
|
|
5137
|
+
// dsh 0.2.0 起 retain.ready 承载共享初始历史打开,先 retain 后判档会让
|
|
5138
|
+
// 紧凑档非当前主会话被水合(T-170 审核发现 1)。判定在函数入口用时重读,
|
|
4783
5139
|
// 切档在途的回调与重试不落地新窗口。子代理日志窗口承载标题行可见的
|
|
4784
5140
|
// 模型上下文(R-01-012/AC-17),恒不裁剪(单点判定见 logWindowSuppressedFor)。
|
|
4785
5141
|
if (logWindowSuppressedFor(id)) return;
|
|
5142
|
+
const detail = sessionDetailsById.get(id) ?? {};
|
|
5143
|
+
sessionDetailsById.set(id, detail);
|
|
5144
|
+
const live = liveSessionFace(id);
|
|
5145
|
+
if (live === null) return; // retain 在途/子代理地址未就绪:下轮渲染重试
|
|
5146
|
+
const session = live.session;
|
|
5147
|
+
const eventSource = live.eventSource;
|
|
5148
|
+
if ((session === null || session === undefined) && (eventSource === null || eventSource === undefined)) {
|
|
5149
|
+
// 代际已终结(binding 访问失败):释放登记,下轮渲染重新 retain。
|
|
5150
|
+
releaseSessionRef(id);
|
|
5151
|
+
return;
|
|
5152
|
+
}
|
|
4786
5153
|
// 日志窗口无效化订阅(事件驱动,不构成轮询,R-02-004):等待/历史卡无会话状态
|
|
4787
5154
|
// 订阅(syncLiveness 只订阅运行中),窗口推进(回合收尾、流式尾)必须经此回调
|
|
4788
5155
|
// 重读快照并重绘;否则窗口更新只能靠渲染期重读兜底——渲染一旦静默(一次性
|
|
4789
5156
|
// load 修复后),完成提醒耗时等窗口派生数据将永久停摆。随可见性 prune。
|
|
4790
|
-
if (typeof
|
|
5157
|
+
if (typeof eventSource?.subscribe === "function" && !logSourceSubs.has(id)) {
|
|
4791
5158
|
try {
|
|
4792
5159
|
logSourceSubs.set(
|
|
4793
5160
|
id,
|
|
4794
|
-
|
|
5161
|
+
eventSource.subscribe(() => {
|
|
4795
5162
|
if (disposed) return;
|
|
4796
5163
|
const listSnap = getSnapshot(sessions, "list");
|
|
4797
5164
|
captureSessionLog(id, { cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
@@ -4802,24 +5169,7 @@ function apply(ctx) {
|
|
|
4802
5169
|
// 订阅不可用:回退渲染期重读。
|
|
4803
5170
|
}
|
|
4804
5171
|
}
|
|
4805
|
-
|
|
4806
|
-
// 日志窗口缺席才发起 open:非订阅会话(等待/历史卡)无快照推送,openState 永远
|
|
4807
|
-
// 不可知,只以 openState 把关会对已水合会话逐帧重发 open,settle→delete→重发
|
|
4808
|
-
// 的在途翻转同样驱动加载指示抖动(docsim 卡闪烁根因之一)。
|
|
4809
|
-
if (!detail.log && detail.snapshot?.openState !== "open" && !sessionOpenLoads.has(id) && typeof session.open === "function") {
|
|
4810
|
-
// 子代理事件流仅接受持久父地址:open 前先安装(T-151),否则宿主拒绝、窗口永不水合。
|
|
4811
|
-
ensureSubagentAddress(id, session);
|
|
4812
|
-
const opening = Promise.resolve(session.open()).catch(() => {});
|
|
4813
|
-
sessionOpenLoads.set(id, opening);
|
|
4814
|
-
opening.finally(() => {
|
|
4815
|
-
if (sessionOpenLoads.get(id) === opening) sessionOpenLoads.delete(id);
|
|
4816
|
-
queueSync();
|
|
4817
|
-
});
|
|
4818
|
-
}
|
|
4819
|
-
} catch {
|
|
4820
|
-
// open 不可用:保持当前窗口。
|
|
4821
|
-
}
|
|
4822
|
-
const log = session.eventSource?.getSnapshot?.() ?? null;
|
|
5172
|
+
const log = eventSource?.getSnapshot?.() ?? null;
|
|
4823
5173
|
if (log === detail.log) return;
|
|
4824
5174
|
detail.log = log;
|
|
4825
5175
|
// 流式派生合并(T-127):事件到达只更新引用并标脏,applyLogEvents 的 O(日志窗口)
|
|
@@ -4848,53 +5198,6 @@ function apply(ctx) {
|
|
|
4848
5198
|
queueSync();
|
|
4849
5199
|
}
|
|
4850
5200
|
|
|
4851
|
-
/** 触发母会话子代理目录单发读取(T-151):目录条目是地址 mode 的唯一可靠来源,
|
|
4852
|
-
* 未加载时先拉取,目录随下一轮快照到达;每次拉取经原生 sessions.refreshSubagents
|
|
4853
|
-
* 走既有 remote 通道,单父会话单飞、不重试,不构成轮询(R-02-004)。 */
|
|
4854
|
-
function requestSubagentCatalog(parentId) {
|
|
4855
|
-
if (typeof sessions?.refreshSubagents !== "function") return;
|
|
4856
|
-
if (subagentCatalogRequests.has(parentId)) return;
|
|
4857
|
-
subagentCatalogRequests.add(parentId);
|
|
4858
|
-
try {
|
|
4859
|
-
Promise.resolve(sessions?.refreshSubagents?.(parentId))
|
|
4860
|
-
.catch(() => {})
|
|
4861
|
-
.finally(() => {
|
|
4862
|
-
if (!disposed) queueSync();
|
|
4863
|
-
});
|
|
4864
|
-
} catch {
|
|
4865
|
-
subagentCatalogRequests.delete(parentId);
|
|
4866
|
-
}
|
|
4867
|
-
}
|
|
4868
|
-
|
|
4869
|
-
/** 子代理会话事件流打开前置(T-151 缺陷修复):dsh 0.1.5 起宿主按地址校验会话事件
|
|
4870
|
-
* 流路由——子代理会话仅接受持久父地址,且地址 mode 必须与其描述符一致(不一致被
|
|
4871
|
-
* subagent/unauthorized 拒绝),持久地址仅经原生导航留存。mode 唯一可靠来源是母
|
|
4872
|
-
* 会话的子代理目录条目:在册即经 configureSubagent 安装(与原生 selectSubagent
|
|
4873
|
-
* 同构,但不切换当前会话);目录未加载时先单发拉取并跳过本轮安装,避免装入必被
|
|
4874
|
-
* 拒绝的错误 mode 地址;目录拉取失败沿用既有空白详情路径(深翻以 null 收敛),
|
|
4875
|
-
* 不阻断其余卡片。非子代理行或会话对象缺 configureSubagent 时为无操作。 */
|
|
4876
|
-
function ensureSubagentAddress(id, session) {
|
|
4877
|
-
const listSnap = getSnapshot(sessions, "list");
|
|
4878
|
-
const byId = listSnap?.byId ?? {};
|
|
4879
|
-
if (!isSubagentRow(byId[id], byId)) return;
|
|
4880
|
-
if (typeof session?.configureSubagent !== "function") return;
|
|
4881
|
-
const parentId = String(byId[id].parentSessionId ?? byId[id].parentId);
|
|
4882
|
-
const catalog = isRecord(listSnap?.subagentsByParent) ? listSnap.subagentsByParent[parentId] : null;
|
|
4883
|
-
const entry = subagentCatalogEntry(listSnap?.subagentsByParent ?? null, parentId, id);
|
|
4884
|
-
if (!isRecord(entry) || typeof entry.mode !== "string" || entry.mode === "") {
|
|
4885
|
-
requestSubagentCatalog(parentId);
|
|
4886
|
-
return;
|
|
4887
|
-
}
|
|
4888
|
-
try {
|
|
4889
|
-
session.configureSubagent(
|
|
4890
|
-
{ kind: "subagent", parentSessionId: parentId, childSessionId: String(id), mode: entry.mode },
|
|
4891
|
-
typeof catalog?.parentAvailable === "boolean" ? catalog.parentAvailable : undefined,
|
|
4892
|
-
);
|
|
4893
|
-
} catch {
|
|
4894
|
-
// 安装失败沿用既有空白详情路径,不阻断其余卡片。
|
|
4895
|
-
}
|
|
4896
|
-
}
|
|
4897
|
-
|
|
4898
5201
|
/** 部署级模型目录一次性读取(R-01-012/AC-01):dsh 0.1.5 起 per-session models
|
|
4899
5202
|
* RPC 移除,目录经 `remote.session.modelCatalog()` 无参数读取;成功即缓存不再
|
|
4900
5203
|
* 重发,失败留待下轮详情读取重试。 */
|
|
@@ -4911,18 +5214,43 @@ function apply(ctx) {
|
|
|
4911
5214
|
});
|
|
4912
5215
|
}
|
|
4913
5216
|
|
|
5217
|
+
/** 释放全部会话引用并停流 jobs roster 订阅(T-170 审核发现 5):服务代际更替
|
|
5218
|
+
* 与卸载两个释放点共用同一形状,抽单点避免漂移。 */
|
|
5219
|
+
function releaseAllSessionRefs() {
|
|
5220
|
+
for (const id of [...sessionRefs.keys()]) releaseSessionRef(id);
|
|
5221
|
+
}
|
|
5222
|
+
|
|
5223
|
+
function stopAllJobWatches() {
|
|
5224
|
+
for (const [, stop] of jobWatchById) {
|
|
5225
|
+
try {
|
|
5226
|
+
stop?.();
|
|
5227
|
+
} catch {
|
|
5228
|
+
/* 流已随旧代际关闭或已卸载 */
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
jobWatchById.clear();
|
|
5232
|
+
}
|
|
5233
|
+
|
|
4914
5234
|
function installServiceSubscriptions() {
|
|
4915
5235
|
const nextSessions = ctx.get("sessions");
|
|
4916
5236
|
const nextWorkspaces = ctx.get("workspaces");
|
|
4917
5237
|
const nextUiSession = ctx.get("uiSession");
|
|
4918
|
-
|
|
5238
|
+
const nextJobs = ctx.get("jobs");
|
|
5239
|
+
if (nextSessions === sessions && nextWorkspaces === workspaces && nextUiSession === uiSession && nextJobs === jobs) return;
|
|
5240
|
+
|
|
5241
|
+
// 服务代际更替:旧代际上登记的 retain 引用与 jobs roster 订阅一并释放——
|
|
5242
|
+
// 引用的 binding 在代际终结后访问即失败(dsh 0.2.0 retain 语义)。
|
|
5243
|
+
releaseAllSessionRefs();
|
|
5244
|
+
stopAllJobWatches();
|
|
4919
5245
|
|
|
4920
5246
|
sessionUnsubscribe?.();
|
|
4921
5247
|
workspaceUnsubscribe?.();
|
|
4922
5248
|
pendingUnsubscribe?.();
|
|
5249
|
+
jobsUnsubscribe?.();
|
|
4923
5250
|
sessions = nextSessions ?? null;
|
|
4924
5251
|
workspaces = nextWorkspaces ?? null;
|
|
4925
5252
|
uiSession = nextUiSession ?? null;
|
|
5253
|
+
jobs = nextJobs ?? null;
|
|
4926
5254
|
try {
|
|
4927
5255
|
sessionUnsubscribe = sessions?.list?.subscribe?.(queueSync) ?? null;
|
|
4928
5256
|
} catch {
|
|
@@ -4934,13 +5262,20 @@ function apply(ctx) {
|
|
|
4934
5262
|
workspaceUnsubscribe = null;
|
|
4935
5263
|
}
|
|
4936
5264
|
try {
|
|
4937
|
-
// 0.
|
|
4938
|
-
//
|
|
5265
|
+
// dsh 0.2.0(T-170):统一会话状态面 sessionStatus 取代 pendingInteractions
|
|
5266
|
+
// (Map<sessionId, {running, pendingInteraction, completionUnread}>),
|
|
4939
5267
|
// 订阅其快照变化驱动等待卡重算(R-01-002/AC-03)。
|
|
4940
|
-
pendingUnsubscribe = uiSession?.
|
|
5268
|
+
pendingUnsubscribe = uiSession?.sessionStatus?.subscribe?.(queueSync) ?? null;
|
|
4941
5269
|
} catch {
|
|
4942
5270
|
pendingUnsubscribe = null;
|
|
4943
5271
|
}
|
|
5272
|
+
try {
|
|
5273
|
+
// dsh 0.2.0(T-170):后台任务 roster 经 jobs 服务的 state 快照推送
|
|
5274
|
+
// (rows: Record<sessionId, JobView[]>),订阅驱动在跑任务卡重算(R-01-023)。
|
|
5275
|
+
jobsUnsubscribe = jobs?.state?.subscribe?.(queueSync) ?? null;
|
|
5276
|
+
} catch {
|
|
5277
|
+
jobsUnsubscribe = null;
|
|
5278
|
+
}
|
|
4944
5279
|
|
|
4945
5280
|
queueSync();
|
|
4946
5281
|
}
|
|
@@ -4955,6 +5290,41 @@ function apply(ctx) {
|
|
|
4955
5290
|
toggle.toggleAttribute("data-drawer-open", open);
|
|
4956
5291
|
queueSync();
|
|
4957
5292
|
}
|
|
5293
|
+
// 帧状态观察(T-167/T-171):键盘(Escape)与滑动手势关抽屉不派发 pointer 事件;
|
|
5294
|
+
// 壳层抽屉状态机本体是 frame 的 data-sidebar-collapsed 属性,观察其翻转是
|
|
5295
|
+
// 关抽屉后 ⊡ 重挂、hero 落位重锚的必然唤醒点——回调仅在状态翻转与 frame
|
|
5296
|
+
// 直系子节点变动(锚/开关挂载摘除,T-171)时发生,无轮询成本。按元素记挂:
|
|
5297
|
+
// frame 就绪前延迟挂载,外壳重挂载出新 frame 时换挂。
|
|
5298
|
+
// 开关在锚缺失期间保留原位(T-168),翻转即检与 400ms 复检均为同值重写或
|
|
5299
|
+
// 重锚,无显隐突变;宿主抽屉隐藏检测(T-165)随层级归位失去必要性,整体
|
|
5300
|
+
// 摘除(T-168)。
|
|
5301
|
+
let frameObserver = null;
|
|
5302
|
+
let observedFrame = null;
|
|
5303
|
+
function armFrameObserver() {
|
|
5304
|
+
if (disposed) return;
|
|
5305
|
+
const frame = document.querySelector("[data-shell-overlay]")?.parentElement;
|
|
5306
|
+
if (frame == null) return;
|
|
5307
|
+
if (frameObserver !== null && observedFrame === frame) return;
|
|
5308
|
+
frameObserver?.disconnect();
|
|
5309
|
+
frameObserver = new MutationObserver(() => {
|
|
5310
|
+
if (disposed) return;
|
|
5311
|
+
// 翻转即检 + 400ms 复检:壳层重挂 ⊡ 晚于属性翻转(reconcile 异步),
|
|
5312
|
+
// 仅翻转即检会扑空且再无唤醒点;开关未摘除,复检为重锚同值。
|
|
5313
|
+
placeToggle();
|
|
5314
|
+
setTimeout(() => {
|
|
5315
|
+
if (disposed) return;
|
|
5316
|
+
placeToggle();
|
|
5317
|
+
}, 400);
|
|
5318
|
+
});
|
|
5319
|
+
// childList(T-171):锚(⊡/壳层开关)与开关自身的挂载摘除都是 frame 直接
|
|
5320
|
+
// 子节点变动——锚在静默页面上出现(壳层延迟挂 ⊡、e2e 注入)没有任何其它
|
|
5321
|
+
// 唤醒点(seat 突变、抽屉翻转、订阅均不发生),落位会悬到下一个任意时钟。
|
|
5322
|
+
// 观察直系子节点变动即检,落位从事件驱动变为确定;回调幂等无反馈环。
|
|
5323
|
+
frameObserver.observe(frame, { attributes: true, attributeFilter: ["data-sidebar-collapsed"], childList: true });
|
|
5324
|
+
observedFrame = frame;
|
|
5325
|
+
}
|
|
5326
|
+
armFrameObserver();
|
|
5327
|
+
placeToggle();
|
|
4958
5328
|
function notifyLayoutChange() {
|
|
4959
5329
|
try {
|
|
4960
5330
|
window.dispatchEvent(new Event("resize"));
|
|
@@ -5097,9 +5467,10 @@ function apply(ctx) {
|
|
|
5097
5467
|
enforceDensityDataDiscipline();
|
|
5098
5468
|
applyDensity();
|
|
5099
5469
|
// 档位已纳入渲染签名:触发一轮同步让时间线按新档位以 lastOnly 重建;
|
|
5100
|
-
//
|
|
5470
|
+
// 锚定补偿在渲染提交后逐帧执行至沉降窗结束,此时量测才含新行高
|
|
5101
5471
|
//(R-01-021/AC-01、AC-08)。
|
|
5102
5472
|
pendingDensityAnchor = currentCard && anchorTop !== null ? anchorTop : null;
|
|
5473
|
+
densityAnchorDeadline = Date.now() + 1500;
|
|
5103
5474
|
queueSync();
|
|
5104
5475
|
};
|
|
5105
5476
|
// 档位按钮位于标题行右侧的工具区(标题区折叠控件的兄弟节点),激活天然不会
|
|
@@ -5133,6 +5504,10 @@ function apply(ctx) {
|
|
|
5133
5504
|
|
|
5134
5505
|
// ---- 窗格容器(conversation 槽座的前置兄弟列;外壳重挂载后重插) ----
|
|
5135
5506
|
function ensurePane() {
|
|
5507
|
+
// 开关落位守卫先于窗格守卫:头部行存在与否独立于窗格挂载态(T-160)。
|
|
5508
|
+
placeToggle();
|
|
5509
|
+
// 帧观察器在 frame 就绪后补挂(外壳重挂载后 frame 为新元素,须重挂)。
|
|
5510
|
+
armFrameObserver();
|
|
5136
5511
|
const seat = document.querySelector(CONVERSATION_SELECTOR);
|
|
5137
5512
|
if (seat === null || seat.parentElement === null) return null;
|
|
5138
5513
|
// [data-slot="conversation"] 的父级是 AppFrame 的中间列(flex column)。
|
|
@@ -6095,72 +6470,65 @@ function apply(ctx) {
|
|
|
6095
6470
|
}
|
|
6096
6471
|
|
|
6097
6472
|
// ---- 轮内状态订阅生命周期(R-01-009、R-02-004) ----
|
|
6473
|
+
// dsh 0.2.0(T-170 审核发现 1):状态订阅只借用既有引用的会话面——日志窗口
|
|
6474
|
+
// 豁免(高档位/当前/子代理)与外壳主视图已 retain 的会话免费获得;紧凑档
|
|
6475
|
+
// 非当前主会话不 retain(retain.ready 即共享初始历史打开,等价水合,
|
|
6476
|
+
// R-01-025/AC-01),其运行呈现降级到统一状态面 sessionStatus(running,
|
|
6477
|
+
// 宿主级单订阅已随 installServiceSubscriptions 建立)与 busy 记账
|
|
6478
|
+
// openTurnStart(回合起点,供运行卡时钟)。
|
|
6098
6479
|
function syncLiveness(runningIds, clockWanted) {
|
|
6099
6480
|
for (const id of runningIds) {
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6481
|
+
const existing = livenessById.get(id);
|
|
6482
|
+
const face = peekSessionFace(id);
|
|
6483
|
+
// 幂等调和:已订阅的记录不动;降级记录(无订阅无快照)在引用出现时
|
|
6484
|
+
// 升级为会话订阅,引用缺席时保持降级。
|
|
6485
|
+
if (existing !== undefined && ((existing.unsubscribe ?? null) !== null || existing.snapshot !== null || face === null)) continue;
|
|
6486
|
+
if (face !== null) {
|
|
6487
|
+
const session = face.session;
|
|
6488
|
+
let unsubscribe = null;
|
|
6489
|
+
if (session !== null && session !== undefined) {
|
|
6490
|
+
try {
|
|
6491
|
+
unsubscribe = session.subscribe(() => {
|
|
6492
|
+
if (disposed) return;
|
|
6493
|
+
const snapshot = getSessionSnapshot(session);
|
|
6494
|
+
const live = livenessFromSnapshot(snapshot);
|
|
6495
|
+
livenessById.set(id, { unsubscribe, liveness: live, snapshot });
|
|
6496
|
+
const detail = sessionDetailsById.get(id) ?? {};
|
|
6497
|
+
detail.snapshot = snapshot;
|
|
6498
|
+
sessionDetailsById.set(id, detail);
|
|
6499
|
+
// dsh 0.1.5 起快照不再携带会话内容:时间线经 eventSource 日志窗口
|
|
6500
|
+
// 就地重派生(R-01-009)。
|
|
6501
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
6502
|
+
captureSessionLog(id, { cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
6503
|
+
queueSync();
|
|
6504
|
+
});
|
|
6505
|
+
} catch {
|
|
6506
|
+
unsubscribe = null; // 订阅不可用:降级到统一状态面路径
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
const snapshot = face.session ? getSessionSnapshot(face.session) : null;
|
|
6510
|
+
if (snapshot !== null) {
|
|
6511
|
+
livenessById.set(id, {
|
|
6512
|
+
unsubscribe,
|
|
6513
|
+
liveness: livenessFromSnapshot(snapshot),
|
|
6514
|
+
snapshot,
|
|
6515
|
+
});
|
|
6115
6516
|
const detail = sessionDetailsById.get(id) ?? {};
|
|
6116
6517
|
detail.snapshot = snapshot;
|
|
6117
6518
|
sessionDetailsById.set(id, detail);
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
const listSnap = getSnapshot(sessions, "list");
|
|
6121
|
-
captureSessionLog(id, { cwd: listSnap?.byId?.[id]?.cwd ?? "" });
|
|
6122
|
-
queueSync();
|
|
6123
|
-
});
|
|
6124
|
-
} catch {
|
|
6125
|
-
continue; // 订阅失败:本次跳过,下次渲染重试
|
|
6126
|
-
}
|
|
6127
|
-
// 子代理事件流仅接受持久父地址:open 前先安装(T-151),否则宿主拒绝、窗口永不水合。
|
|
6128
|
-
ensureSubagentAddress(id, session);
|
|
6129
|
-
// 档位资源纪律(R-01-025/AC-01):紧凑档主会话不发起 open 水合——轮内状态
|
|
6130
|
-
// 订阅(本函数职责,R-02-004)保留,liveness 记账与正常路径共用下方代码,
|
|
6131
|
-
// 日志窗口水合在切回更高档位后经渲染期重试重建。子代理模型上下文依赖其
|
|
6132
|
-
// 窗口,不裁剪(单点判定见 logWindowSuppressedFor)。
|
|
6133
|
-
const livenessSuppressed = logWindowSuppressedFor(id);
|
|
6134
|
-
let opening = null;
|
|
6135
|
-
if (!livenessSuppressed) {
|
|
6136
|
-
try {
|
|
6137
|
-
opening = session.open?.();
|
|
6138
|
-
} catch {
|
|
6139
|
-
opening = null;
|
|
6519
|
+
} else {
|
|
6520
|
+
livenessById.set(id, { unsubscribe: null, liveness: { startTime: null }, snapshot: null });
|
|
6140
6521
|
}
|
|
6522
|
+
} else {
|
|
6523
|
+
// 无引用(紧凑档非当前主会话):不 retain、不订阅。运行起点由消费点
|
|
6524
|
+
// 逐帧从 busy 记账现取(hostStartTime 回退链既有路径),起点不落记录
|
|
6525
|
+
// 冻结;快照派生退回 history 补读路径(既有 fallback)。
|
|
6526
|
+
livenessById.set(id, {
|
|
6527
|
+
unsubscribe: null,
|
|
6528
|
+
liveness: { startTime: null },
|
|
6529
|
+
snapshot: null,
|
|
6530
|
+
});
|
|
6141
6531
|
}
|
|
6142
|
-
if (opening && typeof opening.then === "function") {
|
|
6143
|
-
const tracked = Promise.resolve(opening).then(
|
|
6144
|
-
() => {
|
|
6145
|
-
sessionOpenLoads.delete(id);
|
|
6146
|
-
queueSync();
|
|
6147
|
-
},
|
|
6148
|
-
() => {
|
|
6149
|
-
sessionOpenLoads.delete(id);
|
|
6150
|
-
queueSync();
|
|
6151
|
-
},
|
|
6152
|
-
);
|
|
6153
|
-
sessionOpenLoads.set(id, tracked);
|
|
6154
|
-
}
|
|
6155
|
-
const snapshot = getSessionSnapshot(session);
|
|
6156
|
-
livenessById.set(id, {
|
|
6157
|
-
unsubscribe,
|
|
6158
|
-
liveness: livenessFromSnapshot(snapshot),
|
|
6159
|
-
snapshot,
|
|
6160
|
-
});
|
|
6161
|
-
const detail = sessionDetailsById.get(id) ?? {};
|
|
6162
|
-
detail.snapshot = snapshot;
|
|
6163
|
-
sessionDetailsById.set(id, detail);
|
|
6164
6532
|
}
|
|
6165
6533
|
for (const [id, rec] of livenessById) {
|
|
6166
6534
|
if (runningIds.has(id)) continue;
|
|
@@ -6188,6 +6556,33 @@ function apply(ctx) {
|
|
|
6188
6556
|
}
|
|
6189
6557
|
}
|
|
6190
6558
|
|
|
6559
|
+
/** jobs roster 订阅同步(R-01-023,T-170):dsh 0.2.0 起任务 roster 经 jobs 服务
|
|
6560
|
+
* 按会话 watchRows 拉取(引用计数共享流),不再随列表快照推送。对列表全会话
|
|
6561
|
+
* 建立订阅(与旧 jobsBySession 全量推送同覆盖),离开列表即停流;幂等,每帧
|
|
6562
|
+
* 重入只处理差集。服务缺失时整轮跳过。 */
|
|
6563
|
+
function syncJobsWatches(listSnapshot) {
|
|
6564
|
+
if (typeof jobs?.watchRows !== "function") return;
|
|
6565
|
+
const ids = Array.isArray(listSnapshot?.ids) ? listSnapshot.ids : [];
|
|
6566
|
+
const wanted = new Set(ids.map((id) => String(id)));
|
|
6567
|
+
for (const [key, stop] of [...jobWatchById]) {
|
|
6568
|
+
if (wanted.has(key)) continue;
|
|
6569
|
+
jobWatchById.delete(key);
|
|
6570
|
+
try {
|
|
6571
|
+
stop?.();
|
|
6572
|
+
} catch {
|
|
6573
|
+
/* 流已断 */
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6576
|
+
for (const id of wanted) {
|
|
6577
|
+
if (jobWatchById.has(id)) continue;
|
|
6578
|
+
try {
|
|
6579
|
+
jobWatchById.set(id, jobs.watchRows(id));
|
|
6580
|
+
} catch {
|
|
6581
|
+
/* 单会话订流失败:下帧重试 */
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
|
|
6191
6586
|
function getSessionSnapshot(session) {
|
|
6192
6587
|
try {
|
|
6193
6588
|
return session?.getSnapshot?.() ?? null;
|
|
@@ -6361,7 +6756,7 @@ function apply(ctx) {
|
|
|
6361
6756
|
// 按条目 id 复用,同一 card 元素不会跨 kind 复用——创建时按 entry.kind 一次性
|
|
6362
6757
|
// 决定是否绑激活是安全的;若 id 规则变化须同步复核此处。
|
|
6363
6758
|
const unbind = entry.kind === "job" ? null : bindCardActivation(el, (sessionId) => {
|
|
6364
|
-
if (typeof
|
|
6759
|
+
if (typeof ctx.get("uiWorkspace")?.openSession !== "function") return;
|
|
6365
6760
|
lastActivatedId = sessionId;
|
|
6366
6761
|
// 新激活意图取代一切旧重试链,避免过期链条稍后把当前会话拽回旧目标;
|
|
6367
6762
|
// 收起抽屉的分支同样是最新意图,必须先取消挂起链条再 return。
|
|
@@ -6371,7 +6766,7 @@ function apply(ctx) {
|
|
|
6371
6766
|
if (
|
|
6372
6767
|
shouldDismissDrawerOnActivation({
|
|
6373
6768
|
targetId: sessionId,
|
|
6374
|
-
currentId: getSnapshot(sessions, "list")
|
|
6769
|
+
currentId: currentIdOf(getSnapshot(sessions, "list")),
|
|
6375
6770
|
mobile: window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT})`).matches,
|
|
6376
6771
|
drawerOpen:
|
|
6377
6772
|
document.querySelector(`[${PANE_ATTR}]`)?.getAttribute("data-open") === "true",
|
|
@@ -6667,16 +7062,18 @@ function apply(ctx) {
|
|
|
6667
7062
|
}
|
|
6668
7063
|
}
|
|
6669
7064
|
const listState = listLoadState(snapshot);
|
|
6670
|
-
// 0.
|
|
6671
|
-
//
|
|
6672
|
-
//
|
|
6673
|
-
|
|
6674
|
-
|
|
7065
|
+
// dsh 0.2.0(T-170):统一会话状态面 sessionStatus(Map<sessionId,
|
|
7066
|
+
// {running, pendingInteraction, completionUnread}>)取代 pendingInteractions:
|
|
7067
|
+
// 从状态快照把 pendingInteraction.kind 回填到行副本上,等待卡分类
|
|
7068
|
+
// (awaiting/blocked/提问中)保持既有单一口径(R-01-002/AC-03)。
|
|
7069
|
+
const statusSnapshot = uiSession?.sessionStatus?.getSnapshot?.() ?? null;
|
|
7070
|
+
if (statusSnapshot instanceof Map && statusSnapshot.size > 0 && snapshot?.byId) {
|
|
6675
7071
|
const byId = { ...snapshot.byId };
|
|
6676
|
-
for (const [id,
|
|
7072
|
+
for (const [id, status] of statusSnapshot) {
|
|
6677
7073
|
const row = byId[id];
|
|
6678
|
-
|
|
6679
|
-
|
|
7074
|
+
const kind = status?.pendingInteraction?.kind;
|
|
7075
|
+
if (!isRecord(row) || typeof kind !== "string") continue;
|
|
7076
|
+
byId[id] = { ...row, pendingInteraction: kind };
|
|
6680
7077
|
}
|
|
6681
7078
|
snapshot = { ...snapshot, byId };
|
|
6682
7079
|
}
|
|
@@ -6698,9 +7095,12 @@ function apply(ctx) {
|
|
|
6698
7095
|
for (const [id, record] of busyById) {
|
|
6699
7096
|
if (typeof record?.openWaitStart === "number") waitingStarts.set(String(id), record.openWaitStart);
|
|
6700
7097
|
}
|
|
6701
|
-
// 在跑后台任务(R-01-023):jobsBySession
|
|
6702
|
-
//
|
|
6703
|
-
|
|
7098
|
+
// 在跑后台任务(R-01-023,T-170):dsh 0.2.0 起 jobsBySession 自列表快照移除,
|
|
7099
|
+
// 改由 jobs 服务的 state.rows(Record<sessionId, JobView[]>)注入
|
|
7100
|
+
// buildEntries/buildRecent——liveJobs 非空的主会话保留活动区、归运行组并抑制
|
|
7101
|
+
// 完成/错误提醒;roster 订阅经 syncJobsWatches 对列表全会话建立。
|
|
7102
|
+
syncJobsWatches(snapshot);
|
|
7103
|
+
const jobsBySession = jobs?.state?.getSnapshot?.()?.rows ?? null;
|
|
6704
7104
|
const active = buildEntries(snapshot, workspaceItems, sessionDetailsById, completeAcksById, delegatingIds, archivedSessionIds, waitingStarts, jobsBySession);
|
|
6705
7105
|
// 轮内订阅仅对"运行中"会话建立(主会话 + 运行中的子代理),保持在运行中的订阅
|
|
6706
7106
|
// 数量 == 运行中会话数量(R-02-004/AC-01);暂停等待的子代理只显示标题。
|
|
@@ -6808,7 +7208,7 @@ function apply(ctx) {
|
|
|
6808
7208
|
entry.loadingModel = !detail?.model && (modelLoads.has(entry.id) || (entry.kind === "subagent" && historyLoads.has(entry.id)));
|
|
6809
7209
|
entry.loadingTimeline =
|
|
6810
7210
|
entry.timeline.length === 0 &&
|
|
6811
|
-
(historyLoads.has(entry.id) ||
|
|
7211
|
+
(historyLoads.has(entry.id) || sessionRefs.get(String(entry.id))?.pending === true || (runLikeIds.has(entry.id) && !liveRecord));
|
|
6812
7212
|
// token/速率取自 sessions.list 条目的投影(tokenUsage/sessionStats),
|
|
6813
7213
|
// 复用既有列表订阅,无新增轮询(R-01-009/AC-05、R-02-004/AC-02)。
|
|
6814
7214
|
const projection = snapshot?.byId?.[entry.id]?.projectionValues;
|
|
@@ -6937,7 +7337,8 @@ function apply(ctx) {
|
|
|
6937
7337
|
if (chatLatestAssistantSettled(detailSnapshot)) subagentModelReadIds.add(entry.id);
|
|
6938
7338
|
}
|
|
6939
7339
|
const detailIds = [...active, ...recent].map((entry) => entry.id);
|
|
6940
|
-
|
|
7340
|
+
const currentId = currentIdOf(snapshot);
|
|
7341
|
+
detailIds.sort((a, b) => Number(String(b) === String(currentId)) - Number(String(a) === String(currentId)));
|
|
6941
7342
|
// 档位资源纪律(R-01-025/AC-01):渲染期持续执法——进入紧凑档即拆主会话窗口;
|
|
6942
7343
|
// 拆窗时被豁免的当前会话在切走后的下一轮渲染照常释放。幂等:主会话拆窗后
|
|
6943
7344
|
// 不在 logSourceSubs,重复执法为无操作。
|
|
@@ -6950,12 +7351,17 @@ function apply(ctx) {
|
|
|
6950
7351
|
// 详情与 loads 记账同生命周期:离开可见集合即放行,重回可见时允许重拉/重试。
|
|
6951
7352
|
// 锚点记账不随可见性 prune:瞬时 loading 空帧不得误清(进度重置);陈旧条目靠
|
|
6952
7353
|
// 耗尽宽限与 turnStart 不匹配自校正,仅在 dispose 时整体清除。
|
|
6953
|
-
pruneInvisibleEntries([sessionDetailsById, modelLoads, historyLoads
|
|
7354
|
+
pruneInvisibleEntries([sessionDetailsById, modelLoads, historyLoads], visibleIds);
|
|
6954
7355
|
// 模型目录订阅同生命周期:不可见即先 unsubscribe 再除名,监听器不残留(R-01-012/AC-16)。
|
|
6955
7356
|
pruneSubscriptions(modelDirectorySubs, visibleIds);
|
|
6956
7357
|
pruneSubscriptions(logSourceSubs, visibleIds);
|
|
7358
|
+
// 会话引用同生命周期(T-170):离开可见集合即 release retain 引用——retain 的
|
|
7359
|
+
// 共享代际与流随引用计数收敛,对应旧 dispose 语义。
|
|
7360
|
+
for (const refId of [...sessionRefs.keys()]) {
|
|
7361
|
+
if (!visibleIds.has(refId)) releaseSessionRef(refId);
|
|
7362
|
+
}
|
|
6957
7363
|
// 重试链目标已成为当前会话(他途到达)即取消,避免过期链条拽回会话。
|
|
6958
|
-
cancelStaleOpenRetries({ currentId
|
|
7364
|
+
cancelStaleOpenRetries({ currentId, activatedId: lastActivatedId });
|
|
6959
7365
|
|
|
6960
7366
|
const visibleEntries = [...active, ...recent];
|
|
6961
7367
|
const pulseSurface = desktopQuery.matches
|
|
@@ -7053,7 +7459,7 @@ function apply(ctx) {
|
|
|
7053
7459
|
}
|
|
7054
7460
|
runMoveGhosts(movePlans);
|
|
7055
7461
|
if (shiftRects !== null) runShiftAnimations(shiftRects);
|
|
7056
|
-
ensureCurrentCardVisible(pane.querySelector(".dap-scroll"), snapshot
|
|
7462
|
+
ensureCurrentCardVisible(pane.querySelector(".dap-scroll"), currentIdOf(snapshot));
|
|
7057
7463
|
if (focusAfterMigrationId !== null) cardsById.get(focusAfterMigrationId)?.el.focus();
|
|
7058
7464
|
// 区域已有条目但列表仍在途时,在区头部显示行内加载指示(R-01-014/AC-01)。
|
|
7059
7465
|
const headerEl = pane.querySelector(".dap-titlebar");
|
|
@@ -7126,7 +7532,10 @@ function apply(ctx) {
|
|
|
7126
7532
|
}
|
|
7127
7533
|
// 锚定补偿(R-01-021/AC-01):档位切换的渲染落地后量测当前选中卡的新位置并
|
|
7128
7534
|
// 补偿 scrollTop,使当前卡顶部回到切换前相对视口的位置;补偿量超滚动边界时
|
|
7129
|
-
//
|
|
7535
|
+
// 由浏览器钳制(以当前卡不滚出可视范围为准)。沉降窗内逐帧补偿:dsh 0.2.0
|
|
7536
|
+
// 起切回高档位的窗口经 retain→ready 异步重建,时间线内容分多帧长回,单帧
|
|
7537
|
+
// 补偿会漏掉后期增长(T-170 审核修复回归);位置稳定(残差 ≤1px)或超窗
|
|
7538
|
+
// (1500ms)即撤,避免与用户手动滚动长期对抗。
|
|
7130
7539
|
if (pendingDensityAnchor !== null) {
|
|
7131
7540
|
const scrollEl = pane.querySelector(".dap-scroll");
|
|
7132
7541
|
// 补偿按当前 DOM 的 data-current 卡实时定位:跨区迁移等渲染可能重建卡片
|
|
@@ -7135,13 +7544,21 @@ function apply(ctx) {
|
|
|
7135
7544
|
if (scrollEl !== null && currentCard !== null) {
|
|
7136
7545
|
const viewportTop = scrollEl.getBoundingClientRect().top;
|
|
7137
7546
|
const shiftedTop = currentCard.getBoundingClientRect().top - viewportTop;
|
|
7547
|
+
// 仅按沉降窗超时撤除(T-170):窗口重建分多帧长回且成簇到达,帧间
|
|
7548
|
+
// 空隙的「稳定帧」不可作为撤除依据——提前撤除会让后续突增失偿。
|
|
7138
7549
|
scrollEl.scrollTop += shiftedTop - pendingDensityAnchor;
|
|
7550
|
+
if (Date.now() > densityAnchorDeadline) {
|
|
7551
|
+
pendingDensityAnchor = null;
|
|
7552
|
+
densityAnchorDeadline = 0;
|
|
7553
|
+
}
|
|
7554
|
+
} else {
|
|
7555
|
+
pendingDensityAnchor = null;
|
|
7556
|
+
densityAnchorDeadline = 0;
|
|
7139
7557
|
}
|
|
7140
|
-
pendingDensityAnchor = null;
|
|
7141
7558
|
}
|
|
7142
7559
|
}
|
|
7143
7560
|
|
|
7144
|
-
// ----
|
|
7561
|
+
// ---- 打开会话(dsh 0.2.0 经 uiWorkspace.openSession 导航,currentId 驱动重试链) ----
|
|
7145
7562
|
const MAX_OPEN_ATTEMPTS = 60;
|
|
7146
7563
|
function cardElFor(sessionId) {
|
|
7147
7564
|
return document.querySelector(`[${PANE_ATTR}] [data-session-id="${escapeCssString(sessionId)}"]`);
|
|
@@ -7184,31 +7601,44 @@ function apply(ctx) {
|
|
|
7184
7601
|
}, 500);
|
|
7185
7602
|
});
|
|
7186
7603
|
}
|
|
7604
|
+
// dsh 0.2.0 导航目标(T-170):子代理行以持久父地址寻址(T-151 语义后继——宿主
|
|
7605
|
+
// 仍按地址校验子代理会话路由);地址不可得(母会话投影未拉取)时以裸 id 发起,
|
|
7606
|
+
// 重试链在投影到达后自然命中。
|
|
7607
|
+
function openTargetFor(sessionId) {
|
|
7608
|
+
const listSnap = getSnapshot(sessions, "list");
|
|
7609
|
+
const byId = listSnap?.byId ?? {};
|
|
7610
|
+
if (!isSubagentRow(byId[sessionId], byId)) return sessionId;
|
|
7611
|
+
try {
|
|
7612
|
+
return sessions?.subagentAddress?.(String(sessionId)) ?? sessionId;
|
|
7613
|
+
} catch {
|
|
7614
|
+
return sessionId;
|
|
7615
|
+
}
|
|
7616
|
+
}
|
|
7187
7617
|
function attemptOpen(sessionId, attempt) {
|
|
7188
|
-
// 过期链条(目标已到达 / 已被新激活意图取代)不再发起 open
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
currentId: getSnapshot(sessions, "list")?.current ?? null,
|
|
7193
|
-
activatedId: lastActivatedId,
|
|
7194
|
-
})
|
|
7195
|
-
) {
|
|
7618
|
+
// 过期链条(目标已到达 / 已被新激活意图取代)不再发起 open,防止拽回会话;
|
|
7619
|
+
// 到达(currentId 命中本目标)即视为导航成功并收链。
|
|
7620
|
+
const currentId = currentIdOf(getSnapshot(sessions, "list"));
|
|
7621
|
+
if (shouldCancelOpenRetry({ targetId: sessionId, currentId, activatedId: lastActivatedId })) {
|
|
7196
7622
|
cancelOpenRetry(sessionId);
|
|
7197
7623
|
return;
|
|
7198
7624
|
}
|
|
7199
7625
|
const el = cardElFor(sessionId);
|
|
7200
7626
|
if (el !== null) el.setAttribute("data-opening", "");
|
|
7201
7627
|
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7628
|
+
// dsh 0.2.0(T-170):导航经 uiWorkspace.openSession(内部以 source "mainView"
|
|
7629
|
+
// retain 并驱动外壳 selection,替代已删除的 sessions.open)。发起不等于到达:
|
|
7630
|
+
// 到达判定交给 currentId 驱动的取消链(同步循环每帧 cancelStaleOpenRetries
|
|
7631
|
+
// 与本重试链共用 shouldCancelOpenRetry),data-opening 随取消/到达摘除;
|
|
7632
|
+
// 未到达则 500ms 后重发导航(MAX_OPEN_ATTEMPTS 封顶)。
|
|
7633
|
+
// 移动端抑制(R-01-005/AC-01):宿主 composer 聚焦 effect 与 selection 同一
|
|
7634
|
+
// 提交周期发生,抑制必须在发起导航时即武装(blur + 捕获期 focusin 拦截窗),
|
|
7635
|
+
// 不能等到达判定——0.1.5 的「成功后抑制」时序在 0.2.0 会晚于宿主聚焦。
|
|
7636
|
+
// 窗宽分档(T-170 审核发现 5):首发沿用 1200ms 全窗;重试只武装 60ms
|
|
7637
|
+
// 窄窗——失败的尝试不触发 selection 变更、宿主不聚焦,宽窗只会在失败链
|
|
7638
|
+
// 期间连续吞掉用户聚焦;窄窗仍覆盖成功尝试的同一提交周期聚焦 effect。
|
|
7639
|
+
const suppressFocus = !desktopQuery.matches;
|
|
7640
|
+
openSession(ctx.get("uiWorkspace"), openTargetFor(sessionId));
|
|
7641
|
+
if (suppressFocus) suppressComposerAutofocus(document, setTimeout, attempt === 0 ? 1200 : 60);
|
|
7212
7642
|
if (attempt < MAX_OPEN_ATTEMPTS) scheduleOpenRetry(sessionId, attempt + 1);
|
|
7213
7643
|
else cancelOpenRetry(sessionId);
|
|
7214
7644
|
}
|
|
@@ -7324,6 +7754,9 @@ function apply(ctx) {
|
|
|
7324
7754
|
|
|
7325
7755
|
const cleanup = () => {
|
|
7326
7756
|
disposed = true;
|
|
7757
|
+
frameObserver?.disconnect();
|
|
7758
|
+
frameObserver = null;
|
|
7759
|
+
observedFrame = null;
|
|
7327
7760
|
sessionUnsubscribe?.();
|
|
7328
7761
|
workspaceUnsubscribe?.();
|
|
7329
7762
|
pendingUnsubscribe?.();
|
|
@@ -7363,8 +7796,12 @@ function apply(ctx) {
|
|
|
7363
7796
|
// 卸载即全量退订:空可见集合驱动 pruneSubscriptions 先 unsubscribe 再除名。
|
|
7364
7797
|
pruneSubscriptions(modelDirectorySubs, new Set());
|
|
7365
7798
|
pruneSubscriptions(logSourceSubs, new Set());
|
|
7799
|
+
// 会话引用与 jobs roster 订阅随卸载全量释放(dsh 0.2.0 retain/流语义,T-170)。
|
|
7800
|
+
releaseAllSessionRefs();
|
|
7801
|
+
stopAllJobWatches();
|
|
7802
|
+
jobsUnsubscribe?.();
|
|
7803
|
+
jobsUnsubscribe = null;
|
|
7366
7804
|
progressAnchorById.clear();
|
|
7367
|
-
sessionOpenLoads.clear();
|
|
7368
7805
|
loadQueue.length = 0;
|
|
7369
7806
|
loadInflight = 0;
|
|
7370
7807
|
for (const rec of cardsById.values()) rec.unbind?.();
|