dsh-sessions-manager 3.7.5 → 3.7.7
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/assets/screenshot-dark-controls.png +0 -0
- package/assets/screenshot-id-copied.png +0 -0
- package/assets/screenshot-id-idle.png +0 -0
- package/lib/client.js +341 -27
- package/lib/client.js.map +2 -2
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/src/client/index.jsx +356 -33
- package/src/client/logic.js +93 -0
package/lib/index.js
CHANGED
|
@@ -2039,7 +2039,7 @@ function emptyScanCandidate(sizeBytes) {
|
|
|
2039
2039
|
}
|
|
2040
2040
|
|
|
2041
2041
|
// src/index.js
|
|
2042
|
-
var BUILD_STAMP = true ? "3.7.
|
|
2042
|
+
var BUILD_STAMP = true ? "3.7.7+89abb735" : "dev";
|
|
2043
2043
|
var name = "dsh-sessions-manager";
|
|
2044
2044
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
2045
2045
|
var MAX_TITLE = 80;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sessions-manager",
|
|
3
3
|
"description": "DSH 设置面板会话管理器:归档 / 恢复 / 彻底删除 / 移动到其他工作区,带工作区标签与会话日期;统一「会话管理」面板。Session manager for the DeepSeek Harness settings panel — archive / restore / permanently delete / move sessions across workspaces, with workspace tags & session dates in one unified panel.",
|
|
4
|
-
"version": "3.7.
|
|
4
|
+
"version": "3.7.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/client/index.jsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
14
|
-
import { applyTagFilter, canDropOnWorkspace, dotStateFor, effectiveTitleOf, filterShapeFromSaved, filterSnapshotOf, foldBranches, foldSubagents, noticeToastPlan, openSubagentToast, pathTail, sessionForNodes, shortId, starredOf, tagDeleteConfirm, titleBackfillDecision, toastDurationFor, workspaceForNodes } from './logic.js'
|
|
14
|
+
import { aliveSessionCount, applyTagFilter, canDropOnWorkspace, countRenderedVisible, dotStateFor, effectiveTitleOf, filterShapeFromSaved, filterSnapshotOf, foldBranches, foldSubagents, noticeToastPlan, openSubagentToast, overflowPlan, pathTail, remainingRows, sessionForNodes, shortId, starredOf, tagDeleteConfirm, titleBackfillDecision, toastDurationFor, workspaceForNodes } from './logic.js'
|
|
15
15
|
|
|
16
16
|
export const inject = ['slots']
|
|
17
17
|
|
|
@@ -21,6 +21,21 @@ function loadPanelPrefs() {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const CSS = `
|
|
24
|
+
/* 兼容 shim(2026-09-30 对 0.2.0-rc.2 实测):当前 runtime 的设计令牌不再提供
|
|
25
|
+
fill-elevated / fill-subtle / fill-secondary / label-inverted / state-warning-primary
|
|
26
|
+
这几个 alias,缺位会让输入框、下拉框、卡片、徽标整体回退成透明。这里把它们重映射
|
|
27
|
+
到 runtime 现有 token(官方 Input/Pill 分别用 bg-layer-1 / bg-layer-2)。选择器用
|
|
28
|
+
:where(:root)(零特异性):runtime 将来恢复官方定义时自动胜出,shim 只在缺位时兜底。
|
|
29
|
+
state-warning-primary 是旧名,现名为 state-warn-primary。 */
|
|
30
|
+
:where(:root){--dsw-alias-fill-elevated:var(--dsw-alias-bg-layer-2);--dsw-alias-fill-subtle:var(--dsw-alias-bg-layer-1);--dsw-alias-fill-secondary:var(--dsw-alias-bg-layer-2);--dsw-alias-label-inverted:var(--dsw-alias-label-primary-foreground);--dsw-alias-state-warning-primary:var(--dsw-alias-state-warn-primary)}
|
|
31
|
+
/* 原生 <select> 的弹出列表由 Chromium 按 color-scheme 绘制,不声明时暗色主题下
|
|
32
|
+
仍是白底,叠加未适配的文字色导致不可读;跟随 DSH 的暗色主题标记切换。 */
|
|
33
|
+
.archv{color-scheme:light}
|
|
34
|
+
body[data-ds-dark-theme] .archv{color-scheme:dark}
|
|
35
|
+
/* 兜底:原生弹层行的配色直接写在 option 上(Chromium 按 option 自身的
|
|
36
|
+
background/color 绘制弹层),即使 color-scheme 判定被扩展或环境干扰,
|
|
37
|
+
暗色下也不会出现白底配浅色文字的不可读组合。 */
|
|
38
|
+
.archv option{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
|
|
24
39
|
.archv{--dsm-radius-tag:9px;--dsm-radius-ctl:9px;--dsm-radius-sheet:10px;--dsm-radius-card:12px;display:flex;flex-direction:column;gap:4px;max-width:800px;padding:8px 2px 28px}
|
|
25
40
|
.archv-head{display:flex;align-items:center;gap:10px;margin:0 0 2px}
|
|
26
41
|
.archv-title{font-size:16px;font-weight:650;color:var(--dsw-alias-label-primary);letter-spacing:-0.01em;margin:0}
|
|
@@ -97,7 +112,9 @@ const CSS = `
|
|
|
97
112
|
分支(绿)/空白(灰),名字超长省略号;+N 计数 chip 中性色。 */
|
|
98
113
|
.dsm-tagchip{display:inline-flex;align-items:center;flex:none;max-width:9em;min-height:22px;padding:0 8px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#EAB308) 45%,transparent);border-radius:var(--dsm-radius-tag);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#EAB308) 10%,transparent);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
99
114
|
.dsm-tagchip-more{max-width:none;color:var(--dsw-alias-label-tertiary);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle)}
|
|
100
|
-
.archv-id{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;color:var(--dsw-alias-label-tertiary);flex:none;margin-left:auto;white-space:nowrap}
|
|
115
|
+
.archv-id{appearance:none;border:none;background:none;padding:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;color:var(--dsw-alias-label-tertiary);flex:none;margin-left:auto;white-space:nowrap;cursor:pointer;transition:color .12s ease}
|
|
116
|
+
.archv-id:hover{color:var(--dsw-alias-label-secondary)}
|
|
117
|
+
.archv-id-copied{color:var(--dsw-alias-state-business-primary)}
|
|
101
118
|
.archv-dot{color:var(--dsw-alias-border-l3);flex:none}
|
|
102
119
|
.archv-check{width:15px;height:15px;accent-color:var(--dsw-alias-state-business-primary);flex:none;cursor:pointer}
|
|
103
120
|
.archv-star{appearance:none;width:22px;height:22px;flex:none;display:inline-flex;align-items:center;justify-content:center;border:none;background:0 0;padding:0;line-height:0;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:50%;transition:color .15s ease,transform .12s ease}
|
|
@@ -921,6 +938,37 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
921
938
|
}
|
|
922
939
|
}
|
|
923
940
|
|
|
941
|
+
// 点击行尾短 ID 复制完整会话 ID。clipboard API 优先(desktop/web 的页面都来自
|
|
942
|
+
// localhost,属安全上下文),失败回退 execCommand;成功把短 ID 短暂换成「已复制」
|
|
943
|
+
// 作就近反馈,两条路径全失败时把完整 ID 亮进 toast 供手抄(与上方徽章的失败终点
|
|
944
|
+
// 同一思路)。
|
|
945
|
+
const copiedTimer = useRef(null)
|
|
946
|
+
const [copiedId, setCopiedId] = useState(null)
|
|
947
|
+
const copySessionId = (id) => {
|
|
948
|
+
const markCopied = () => {
|
|
949
|
+
setCopiedId(id)
|
|
950
|
+
if (copiedTimer.current) clearTimeout(copiedTimer.current)
|
|
951
|
+
copiedTimer.current = setTimeout(() => setCopiedId(null), 1400)
|
|
952
|
+
}
|
|
953
|
+
const viaExecCommand = () => {
|
|
954
|
+
try {
|
|
955
|
+
const ta = document.createElement('textarea')
|
|
956
|
+
ta.value = id
|
|
957
|
+
ta.setAttribute('readonly', '')
|
|
958
|
+
ta.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0'
|
|
959
|
+
document.body.appendChild(ta)
|
|
960
|
+
ta.select()
|
|
961
|
+
const ok = document.execCommand('copy')
|
|
962
|
+
ta.remove()
|
|
963
|
+
if (ok) markCopied()
|
|
964
|
+
else showToast('复制失败,完整 ID:' + id, 'err')
|
|
965
|
+
} catch (e) { showToast('复制失败,完整 ID:' + id, 'err') }
|
|
966
|
+
}
|
|
967
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
968
|
+
navigator.clipboard.writeText(id).then(markCopied).catch(viaExecCommand)
|
|
969
|
+
} else viaExecCommand()
|
|
970
|
+
}
|
|
971
|
+
|
|
924
972
|
const refresh = () => {
|
|
925
973
|
setError(null)
|
|
926
974
|
Promise.all([
|
|
@@ -2147,7 +2195,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
2147
2195
|
{tagChips(it.sessionId)}
|
|
2148
2196
|
{kidsBadge(it.sessionId)}
|
|
2149
2197
|
{branchGroupBadge(it.sessionId)}
|
|
2150
|
-
<
|
|
2198
|
+
<button type="button" className={'archv-id' + (copiedId === it.sessionId ? ' archv-id-copied' : '')} title={it.sessionId + '(点击复制完整 ID)'} aria-label={'复制会话 ID ' + it.sessionId} onClick={(e) => { e.stopPropagation(); copySessionId(it.sessionId) }}>{copiedId === it.sessionId ? '已复制' : shortId(it.sessionId)}</button>
|
|
2151
2199
|
</div>
|
|
2152
2200
|
<div className="archv-meta">
|
|
2153
2201
|
{it.archived ? <span className="archv-wtag archv-wgone">已归档</span> : <span className="archv-wtag archv-active">活动</span>}
|
|
@@ -2431,7 +2479,7 @@ const SIDEBAR_AUG_CSS = `
|
|
|
2431
2479
|
.dsm-toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:2147483601;background:Canvas;color:CanvasText;border:1px solid color-mix(in srgb,CanvasText 25%,transparent);padding:9px 16px;border-radius:999px;font-size:12px;box-shadow:0 8px 24px rgb(0 0 0/.25);max-width:min(92vw,460px);cursor:pointer}
|
|
2432
2480
|
.dsm-toast-long{border-radius:14px;text-align:left;line-height:18px}
|
|
2433
2481
|
.dsm-toast-err{background:#4A1D1D;color:#FFD9D9;border:1px solid var(--dsw-alias-state-error-primary)}
|
|
2434
|
-
.dsm-sub{position:fixed;z-index:1100;box-sizing:border-box;min-width:190px;max-width:320px;padding:4px;display:flex;flex-direction:column;gap:0;background:var(--dsw-specific-menu);border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;box-shadow:var(--dsw-shadow-lv3)}
|
|
2482
|
+
.dsm-sub{position:fixed;z-index:1100;box-sizing:border-box;min-width:190px;max-width:320px;padding:4px;display:flex;flex-direction:column;gap:0;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%));border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;box-shadow:var(--dsw-shadow-lv3)}
|
|
2435
2483
|
.dsm-sub-loading,.dsm-sub-empty{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:6px 10px}
|
|
2436
2484
|
.dsm-sub-err{font-size:12px;color:var(--dsw-alias-state-error-primary);padding:6px 10px}
|
|
2437
2485
|
.dsm-sub-item{display:flex;align-items:center;gap:8px;width:100%;min-height:36px;padding:6px 10px;border:none;border-radius:8px;background:transparent;cursor:pointer;font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary);text-align:left}
|
|
@@ -2678,6 +2726,24 @@ function dsmTeardownSidebarAug() {
|
|
|
2678
2726
|
for (const sel of selectors) {
|
|
2679
2727
|
try { document.querySelectorAll(sel).forEach((el) => el.remove()) } catch (e) { /* best-effort */ }
|
|
2680
2728
|
}
|
|
2729
|
+
try {
|
|
2730
|
+
// issue #16:被隐藏的会话行与展开按钮在卸载时还原——adapter 停用(上游
|
|
2731
|
+
// 改版)或插件停用后,不能把「看不见的会话」留在官方侧栏上。
|
|
2732
|
+
document.querySelectorAll('[data-dsm-hidden]').forEach((row) => {
|
|
2733
|
+
row.style.display = ''
|
|
2734
|
+
row.removeAttribute('data-dsm-hidden')
|
|
2735
|
+
})
|
|
2736
|
+
document.querySelectorAll('[data-dsm-overflow-hidden], [data-dsm-overflow-base]').forEach((btn) => {
|
|
2737
|
+
// 文案可能被我们改写过:dataset 里存着上游原文,卸载时连文案一起还原。
|
|
2738
|
+
const base = btn.getAttribute('data-dsm-overflow-base')
|
|
2739
|
+
if (base != null && btn.textContent !== base) btn.textContent = base
|
|
2740
|
+
btn.style.display = ''
|
|
2741
|
+
btn.removeAttribute('data-dsm-overflow-hidden')
|
|
2742
|
+
btn.removeAttribute('data-dsm-overflow-base')
|
|
2743
|
+
btn.removeAttribute('data-dsm-overflow-write')
|
|
2744
|
+
btn.removeAttribute('data-dsm-overflow-ver')
|
|
2745
|
+
})
|
|
2746
|
+
} catch (e) { /* best-effort */ }
|
|
2681
2747
|
try {
|
|
2682
2748
|
document.querySelectorAll('[data-dsm-group-hidden]').forEach((owner) => {
|
|
2683
2749
|
owner.style.display = owner.dataset.dsmPrevDisplay || ''
|
|
@@ -2722,11 +2788,26 @@ function createSidebarAdapter() {
|
|
|
2722
2788
|
}
|
|
2723
2789
|
return out
|
|
2724
2790
|
}
|
|
2791
|
+
// 只取 `group` 的静默识别:命中即停,**不参与 miss 预算**。展开计数的
|
|
2792
|
+
// workspace 行识别属非关键路径(拿不到就保持上游原文案),不能让它的落空
|
|
2793
|
+
// 把整套侧栏增强熔断;命中即返也省掉继续上溯整条 fiber 链。
|
|
2794
|
+
const recognizeGroupQuiet = (row) => {
|
|
2795
|
+
if (disabled || !row || typeof row !== 'object') return null
|
|
2796
|
+
let f = findFiber(row)
|
|
2797
|
+
let guard = 0
|
|
2798
|
+
while (f && guard++ < 300) {
|
|
2799
|
+
const props = f.memoizedProps
|
|
2800
|
+
if (props && props.group && typeof props.group === 'object') return props.group
|
|
2801
|
+
f = f.return
|
|
2802
|
+
}
|
|
2803
|
+
return null
|
|
2804
|
+
}
|
|
2725
2805
|
return {
|
|
2726
2806
|
version: SIDEBAR_ADAPTER_VERSION,
|
|
2727
2807
|
get disabled() { return disabled },
|
|
2728
2808
|
findFiber,
|
|
2729
2809
|
recognize,
|
|
2810
|
+
recognizeGroupQuiet,
|
|
2730
2811
|
}
|
|
2731
2812
|
}
|
|
2732
2813
|
const sidebarAdapter = createSidebarAdapter()
|
|
@@ -3012,6 +3093,25 @@ function installSidebarSessionMenuAug() {
|
|
|
3012
3093
|
function installSidebarStatusDots() {
|
|
3013
3094
|
if (typeof document === 'undefined') return
|
|
3014
3095
|
const DOT = 'data-dsm-dot'
|
|
3096
|
+
// issue #16:被插件隐藏的行打标记;展开计数的扣减统计靠它(数据层不可用时兜底)。
|
|
3097
|
+
const DSM_HIDDEN_ATTR = 'data-dsm-hidden'
|
|
3098
|
+
// 空分组:整组被我们藏起时打在**分组容器**上的标记(见 dsmSyncEmptyGroups)。
|
|
3099
|
+
const GROUP_UNIT_ATTR = 'data-dsm-group-hidden'
|
|
3100
|
+
// 展开按钮上的数据集:上游原文(基线)、上次写入值、口径版本、是否被我们藏起。
|
|
3101
|
+
const DSM_OVERFLOW_BASE = 'data-dsm-overflow-base'
|
|
3102
|
+
const DSM_OVERFLOW_WRITE = 'data-dsm-overflow-write'
|
|
3103
|
+
const DSM_OVERFLOW_VER = 'data-dsm-overflow-ver'
|
|
3104
|
+
const DSM_OVERFLOW_HIDDEN = 'data-dsm-overflow-hidden'
|
|
3105
|
+
let overflowDebugged = false
|
|
3106
|
+
const debugOverflowOnce = (msg) => {
|
|
3107
|
+
if (overflowDebugged) return
|
|
3108
|
+
overflowDebugged = true
|
|
3109
|
+
try { console.debug('[dsh-sessions-manager] 展开计数:' + msg) } catch (e) { /* 控制台不可用 */ }
|
|
3110
|
+
}
|
|
3111
|
+
// 展开按钮的文案基线 / 上次写入值存在 dataset(而非 WeakMap):HMR / 插件
|
|
3112
|
+
// 重装后新闭包里 WeakMap 是空的,会把「已扣减的文案」再当上游原文扣一次;
|
|
3113
|
+
// dataset 跟随节点存活,卸载时还能据此还原原文案。data-dsm-* 不在
|
|
3114
|
+
// MutationObserver 的 attributeFilter 里,不会自触发重绘。
|
|
3015
3115
|
if (!document.querySelector('style[data-dsm=aug]')) {
|
|
3016
3116
|
const s = document.createElement('style')
|
|
3017
3117
|
s.dataset.dsm = 'aug'
|
|
@@ -3045,37 +3145,250 @@ function installSidebarStatusDots() {
|
|
|
3045
3145
|
// 行;官方(重命名等)改写文本后所有权移交,绝不覆盖 DSH Core 的实时更新。
|
|
3046
3146
|
const titleBaselines = new WeakMap()
|
|
3047
3147
|
// 空分组兜底:DSH 会把「日志已不在原处」的会话重新归到「未分组」组里(删
|
|
3048
|
-
//
|
|
3049
|
-
//
|
|
3050
|
-
//
|
|
3051
|
-
//
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
}
|
|
3070
|
-
if (owner.style.display !== 'none') owner.style.display = 'none'
|
|
3071
|
-
} else if (owner.hasAttribute('data-dsm-group-hidden')) {
|
|
3072
|
-
const prev = owner.dataset.dsmPrevDisplay || ''
|
|
3073
|
-
if (owner.style.display !== prev) owner.style.display = prev
|
|
3074
|
-
owner.removeAttribute('data-dsm-group-hidden')
|
|
3075
|
-
delete owner.dataset.dsmPrevDisplay
|
|
3148
|
+
// 除后最明显)。我们把行隐藏了,但空组标题会留下来——所以组内没有**可能
|
|
3149
|
+
// 出现**的会话时,把整个分组容器一起隐藏;重新出现时自动还原。
|
|
3150
|
+
//
|
|
3151
|
+
// 容器定位:各版本的顶层分组容器都没有稳定的 role/class —— `role="group"`
|
|
3152
|
+
// 从 0.1.7 到 0.2.1 只出现在**子分组**容器上(一个包着整段子分组的 div),
|
|
3153
|
+
// 顶层分组是无 role 的普通 div(`groupSection`)。所以以
|
|
3154
|
+
// `data-row-key="workspace:*"`(0.1.7 起稳定)为锚点,取它的父元素当容器:
|
|
3155
|
+
// 本组的会话行与展开按钮都是这个容器的子节点。
|
|
3156
|
+
//
|
|
3157
|
+
// 判定走数据层(与展开计数同一口径):只有 group.sessions 里「非空白、且
|
|
3158
|
+
// 不在回收站/墓碑/空白集合」的条数确实为 0 才藏。只看 DOM 会把「上游折叠
|
|
3159
|
+
// 后还没渲染出来的可见会话」误判成空组,连标题带展开按钮一起藏掉——那些
|
|
3160
|
+
// 会话就再也点不到了。
|
|
3161
|
+
//
|
|
3162
|
+
// 原 display 记在容器的 dataset 上(而不是 WeakMap):还原路径无法遍历
|
|
3163
|
+
// WeakMap,dataset 让「谁被我们藏过、原来是什么」随时可查可还原。
|
|
3164
|
+
const setGroupHidden = (scope, hidden) => {
|
|
3165
|
+
if (hidden) {
|
|
3166
|
+
if (!scope.hasAttribute(GROUP_UNIT_ATTR)) {
|
|
3167
|
+
scope.dataset.dsmPrevDisplay = scope.style.display || ''
|
|
3168
|
+
scope.setAttribute(GROUP_UNIT_ATTR, '')
|
|
3076
3169
|
}
|
|
3170
|
+
if (scope.style.display !== 'none') scope.style.display = 'none'
|
|
3171
|
+
return
|
|
3172
|
+
}
|
|
3173
|
+
if (!scope.hasAttribute(GROUP_UNIT_ATTR)) return
|
|
3174
|
+
const prev = scope.dataset.dsmPrevDisplay || ''
|
|
3175
|
+
if (scope.style.display !== prev) scope.style.display = prev
|
|
3176
|
+
scope.removeAttribute(GROUP_UNIT_ATTR)
|
|
3177
|
+
delete scope.dataset.dsmPrevDisplay
|
|
3178
|
+
}
|
|
3179
|
+
// 元素自身到 stopAt(不含)之间有没有被藏起来。只看我们自己的两处标记与
|
|
3180
|
+
// style.display:不碰 getComputedStyle(每帧调用的代价太高)。
|
|
3181
|
+
const isShownWithin = (el, stopAt) => {
|
|
3182
|
+
let node = el
|
|
3183
|
+
while (node && node !== stopAt) {
|
|
3184
|
+
if (node.style && node.style.display === 'none') return false
|
|
3185
|
+
if (node.hasAttribute && (node.hasAttribute(GROUP_UNIT_ATTR) || node.hasAttribute(DSM_HIDDEN_ATTR))) return false
|
|
3186
|
+
node = node.parentElement
|
|
3187
|
+
}
|
|
3188
|
+
return true
|
|
3189
|
+
}
|
|
3190
|
+
function dsmSyncEmptyGroups(purgedNow) {
|
|
3191
|
+
let rows = null
|
|
3192
|
+
try { rows = document.querySelectorAll('[role="treeitem"][data-row-key^="workspace:"]') } catch (e) { return }
|
|
3193
|
+
if (!rows || !rows.length) return
|
|
3194
|
+
const purged = purgedNow instanceof Set ? purgedNow : new Set()
|
|
3195
|
+
// 被插件藏起来的 id 集合只在真的需要判定时建一次(绝大多数帧用不到)。
|
|
3196
|
+
let hiddenIds = null
|
|
3197
|
+
const hiddenIdSet = () => {
|
|
3198
|
+
if (hiddenIds) return hiddenIds
|
|
3199
|
+
hiddenIds = new Set()
|
|
3200
|
+
if (dsmTrashIds) for (const id of dsmTrashIds) hiddenIds.add(id)
|
|
3201
|
+
for (const id of purged) hiddenIds.add(id)
|
|
3202
|
+
for (const [id, info] of dsmLineage.entries()) if (info && info.empty) hiddenIds.add(id)
|
|
3203
|
+
return hiddenIds
|
|
3204
|
+
}
|
|
3205
|
+
rows.forEach((row) => {
|
|
3206
|
+
try {
|
|
3207
|
+
const scope = row.parentElement
|
|
3208
|
+
if (!scope || scope === document.body) return
|
|
3209
|
+
// 容器必须是无 role 的普通元素(顶层分组)。子分组容器 role="group"
|
|
3210
|
+
// 里装的是整段子分组,不能当组容器用——拿它当容器会把整段子分组藏掉。
|
|
3211
|
+
if (typeof scope.getAttribute !== 'function' || scope.getAttribute('role')) return
|
|
3212
|
+
// 组内还有没被我们藏起来的会话行 → 绝不藏(也顺手还原)。
|
|
3213
|
+
let rendered = 0
|
|
3214
|
+
for (const el of scope.children) {
|
|
3215
|
+
if (el === row || !el.getAttribute) continue
|
|
3216
|
+
if (el.getAttribute('role') !== 'treeitem') continue
|
|
3217
|
+
if (!(el.getAttribute('data-row-key') || '').startsWith('session:')) continue
|
|
3218
|
+
if (el.hasAttribute(DSM_HIDDEN_ATTR)) continue
|
|
3219
|
+
rendered++
|
|
3220
|
+
}
|
|
3221
|
+
if (rendered > 0) { setGroupHidden(scope, false); return }
|
|
3222
|
+
// 容器里可能还挂着子分组(上游把嵌套工作区渲染在 role="group" 的
|
|
3223
|
+
// div 里)。只要还有子分组对用户可见,就绝不能藏父容器——那会把整
|
|
3224
|
+
// 段子分组一起带走。
|
|
3225
|
+
for (const nested of scope.querySelectorAll('[data-row-key^="workspace:"]')) {
|
|
3226
|
+
if (nested === row) continue
|
|
3227
|
+
if (!isShownWithin(nested, scope)) continue
|
|
3228
|
+
setGroupHidden(scope, false)
|
|
3229
|
+
return
|
|
3230
|
+
}
|
|
3231
|
+
// 渲染行全被我们藏了(或一行都没渲染):只有数据层能证明「真空」才藏。
|
|
3232
|
+
// 拿不到 group.sessions / 数组为空 = 不知道 → 按非空处理,保持显示。
|
|
3233
|
+
const group = sidebarAdapter.recognizeGroupQuiet(row)
|
|
3234
|
+
const sessions = group && Array.isArray(group.sessions) ? group.sessions : null
|
|
3235
|
+
setGroupHidden(scope, aliveSessionCount(sessions, hiddenIdSet()) === 0)
|
|
3236
|
+
} catch (e) { /* 单组失败不影响其它组 */ }
|
|
3077
3237
|
})
|
|
3078
3238
|
}
|
|
3239
|
+
// issue #16:「展开其余 N 个会话」的 N 是上游按数据层算的(0.2.0-rc.2 叫
|
|
3240
|
+
// collapsedSessionRows,0.2.1-rc.2 改叫 watermarkRows),它看不到插件用
|
|
3241
|
+
// display:none 藏起来的回收站/空白行,于是 N 虚高;点开只是把更多已隐藏行
|
|
3242
|
+
// 塞进 DOM,画面不动,很像「会话丢了」。
|
|
3243
|
+
//
|
|
3244
|
+
// 只改**文案与显隐**,不碰上游的展开状态机(watermark / 点击行为),也不改
|
|
3245
|
+
// 任何行自身的可见性判定。
|
|
3246
|
+
//
|
|
3247
|
+
// 口径(F1):上游 N 是「水位线之外未渲染的行数」,和「整组被插件藏起来的
|
|
3248
|
+
// 行数」不重叠,相减会重复扣减、点开一次就翻负,把仍有可见行的按钮藏掉
|
|
3249
|
+
// (上游只有这一个展开入口 → 会话不可达)。这里改用
|
|
3250
|
+
// 还能出现几行 = 本组可见总数(非 blank、且不会被插件藏)- 已渲染可见行数
|
|
3251
|
+
// 空白行两边都排除(F2):上游 watermarkRows 里 `if (session.blank) return true`
|
|
3252
|
+
// —— 空白行永远保留、从不计入 N。
|
|
3253
|
+
//
|
|
3254
|
+
// 数据层拿不到 group.sessions 时**什么都不改**(F6):DOM 兜底在数学上不成立,
|
|
3255
|
+
// 写一个看起来合理却会算错的数比不改更糟。
|
|
3256
|
+
const OVERFLOW_SIGS = new WeakMap()
|
|
3257
|
+
// 签名只看「便宜且能反映变化」的量:本组已渲染可见会话行(数量 + 首尾 id)、
|
|
3258
|
+
// 按钮自身文案与显隐、三个集合的大小。流式输出时 paint 每帧都跑,但侧栏结构
|
|
3259
|
+
// 绝大多数帧没变 —— 命中签名就直接跳过 fiber 走链(F4)。
|
|
3260
|
+
const overflowSignature = (scope, btn, purgedNow) => {
|
|
3261
|
+
let n = 0
|
|
3262
|
+
let firstKey = ''
|
|
3263
|
+
let lastKey = ''
|
|
3264
|
+
for (const el of scope.children) {
|
|
3265
|
+
if (el === btn || !el.getAttribute) continue
|
|
3266
|
+
const k = el.getAttribute('data-row-key') || ''
|
|
3267
|
+
if (!k.startsWith('session:')) continue
|
|
3268
|
+
if (el.hasAttribute(DSM_HIDDEN_ATTR)) continue
|
|
3269
|
+
n++
|
|
3270
|
+
if (!firstKey) firstKey = k
|
|
3271
|
+
lastKey = k
|
|
3272
|
+
}
|
|
3273
|
+
return n + '|' + firstKey + '|' + lastKey + '|' + (btn.textContent || '') + '|' + (btn.style.display || '') +
|
|
3274
|
+
'|' + (dsmTrashIds ? dsmTrashIds.size : -1) + '|' + purgedNow.size + '|' + dsmLineage.size
|
|
3275
|
+
}
|
|
3276
|
+
function dsmSyncOverflowLabels(purgedNow) {
|
|
3277
|
+
let buttons = null
|
|
3278
|
+
try { buttons = document.querySelectorAll('[data-row-key^="overflow:"]') } catch (e) { return }
|
|
3279
|
+
if (!buttons || !buttons.length) return
|
|
3280
|
+
try {
|
|
3281
|
+
buttons.forEach((btn) => {
|
|
3282
|
+
// 工作区标题行与本组的会话行同属一个 groupSection(上游 renderGroup 的
|
|
3283
|
+
// 直接子节点;子分组在另一个 role=group 容器里)→ 只按直接子节点找,
|
|
3284
|
+
// 不再做全文档属性前缀扫描。
|
|
3285
|
+
const scope = btn.parentElement
|
|
3286
|
+
if (!scope || !scope.children) return
|
|
3287
|
+
const sig = overflowSignature(scope, btn, purgedNow)
|
|
3288
|
+
if (OVERFLOW_SIGS.get(btn) === sig) return
|
|
3289
|
+
const key = (btn.getAttribute('data-row-key') || '').slice('overflow:'.length)
|
|
3290
|
+
let wsRow = null
|
|
3291
|
+
for (const el of scope.children) {
|
|
3292
|
+
if (el.getAttribute && el.getAttribute('data-row-key') === 'workspace:' + key) { wsRow = el; break }
|
|
3293
|
+
}
|
|
3294
|
+
const group = wsRow ? sidebarAdapter.recognizeGroupQuiet(wsRow) : null
|
|
3295
|
+
const nodes = group && Array.isArray(group.sessions) ? group.sessions : null
|
|
3296
|
+
if (!nodes) {
|
|
3297
|
+
// 数据层不可用:还原成上游原文案并放开按钮,绝不猜数字。
|
|
3298
|
+
restoreOverflowButton(btn)
|
|
3299
|
+
OVERFLOW_SIGS.set(btn, overflowSignature(scope, btn, purgedNow))
|
|
3300
|
+
return
|
|
3301
|
+
}
|
|
3302
|
+
// 本组「上游会列出、且不会被插件藏起来」的会话集合。
|
|
3303
|
+
const ids = new Set()
|
|
3304
|
+
for (const node of nodes) {
|
|
3305
|
+
if (!node || node.blank) continue
|
|
3306
|
+
const id = node.id != null ? String(node.id) : ''
|
|
3307
|
+
if (!id) continue
|
|
3308
|
+
if ((dsmTrashIds && dsmTrashIds.has(id)) || purgedNow.has(id)) continue
|
|
3309
|
+
const li = dsmLineage.get(id)
|
|
3310
|
+
if (li && li.empty) continue
|
|
3311
|
+
ids.add(id)
|
|
3312
|
+
}
|
|
3313
|
+
// 已渲染进 DOM 的可见行:只认本组直接子节点里不在隐藏标记中的会话行。
|
|
3314
|
+
// 用 `data-dsm-hidden` 而非 style.display 判定——上游折叠父级时行自身
|
|
3315
|
+
// 的 display 仍是空串,按 style 判会把看不见的行算成已渲染。
|
|
3316
|
+
const rendered = []
|
|
3317
|
+
for (const el of scope.children) {
|
|
3318
|
+
if (el === btn || !el.getAttribute) continue
|
|
3319
|
+
if (el.getAttribute('role') !== 'treeitem') continue
|
|
3320
|
+
const k = el.getAttribute('data-row-key') || ''
|
|
3321
|
+
if (!k.startsWith('session:')) continue
|
|
3322
|
+
rendered.push({ id: k.slice('session:'.length), pluginHidden: el.hasAttribute(DSM_HIDDEN_ATTR) })
|
|
3323
|
+
}
|
|
3324
|
+
// 空集/口径不匹配的保护:数据层说「一条都没有」但 DOM 里明明有会话行,
|
|
3325
|
+
// 说明 group.sessions 的语义变了(懒加载、上游又换了过滤条件)。此时按
|
|
3326
|
+
// 未知处理,绝不能算出 0 把按钮藏掉 —— 那是把可见会话变成不可达。
|
|
3327
|
+
if (!nodes.length || (!ids.size && rendered.length)) {
|
|
3328
|
+
restoreOverflowButton(btn)
|
|
3329
|
+
OVERFLOW_SIGS.set(btn, overflowSignature(scope, btn, purgedNow))
|
|
3330
|
+
return
|
|
3331
|
+
}
|
|
3332
|
+
const renderedVisible = countRenderedVisible(rendered, ids)
|
|
3333
|
+
const more = remainingRows(ids.size, renderedVisible)
|
|
3334
|
+
applyOverflowPlan(btn, more, '可见 ' + ids.size + ' / 已渲染 ' + renderedVisible)
|
|
3335
|
+
OVERFLOW_SIGS.set(btn, overflowSignature(scope, btn, purgedNow))
|
|
3336
|
+
})
|
|
3337
|
+
} catch (e) {
|
|
3338
|
+
// 非关键路径:失败就保持上游原样,不影响本次 paint 的其它注入(F5)。
|
|
3339
|
+
try { console.debug('[dsh-sessions-manager] 展开计数校准跳过:' + (e && e.message)) } catch (e2) {}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
// 把计划落到按钮上:文案基线 / 上次写入值 / 口径版本都记在 dataset
|
|
3343
|
+
// (F8:跨版本残留的旧基线不会延续到下个版本),HMR 重装后也不会二次处理。
|
|
3344
|
+
function applyOverflowPlan(btn, more, detail) {
|
|
3345
|
+
const raw = btn.textContent || ''
|
|
3346
|
+
const ver = String(SIDEBAR_ADAPTER_VERSION)
|
|
3347
|
+
const fresh = btn.getAttribute(DSM_OVERFLOW_VER) === ver
|
|
3348
|
+
const last = fresh ? btn.getAttribute(DSM_OVERFLOW_WRITE) : null
|
|
3349
|
+
let baseline = fresh ? btn.getAttribute(DSM_OVERFLOW_BASE) : null
|
|
3350
|
+
// 首次见到,或上游重渲染换了文案(含点击展开后的新 N)→ 以当前文案为新基准。
|
|
3351
|
+
if (baseline == null || raw !== last) baseline = raw
|
|
3352
|
+
btn.setAttribute(DSM_OVERFLOW_BASE, baseline)
|
|
3353
|
+
btn.setAttribute(DSM_OVERFLOW_VER, ver)
|
|
3354
|
+
// F3:上游 N 藏在基线文案里。若我们算出的「还能出现几行」反而比上游 N 大,
|
|
3355
|
+
// 说明数据层与 DOM 的口径已经不一致(上游改了 group.sessions 的过滤语义、
|
|
3356
|
+
// 或将来插件改用官方归档 API)——收敛回上游 N 并留一条 debug,把「静默
|
|
3357
|
+
// 失效」变成可观测。注意必须用**基线**里的 N:当前文案可能是我们写过的值。
|
|
3358
|
+
const bm = baseline.match(/(\d[\d,]*)/)
|
|
3359
|
+
const upstream = bm ? Number(bm[1].replace(/,/g, '')) : null
|
|
3360
|
+
if (more != null && upstream != null && upstream > 0 && more > upstream) {
|
|
3361
|
+
more = upstream
|
|
3362
|
+
debugOverflowOnce((detail || '') + ',超过上游计数 ' + upstream + ',已按上游计数收敛')
|
|
3363
|
+
}
|
|
3364
|
+
const plan = overflowPlan(baseline, more)
|
|
3365
|
+
if (plan.hide) {
|
|
3366
|
+
if (btn.style.display !== 'none') btn.style.display = 'none'
|
|
3367
|
+
if (!btn.hasAttribute(DSM_OVERFLOW_HIDDEN)) btn.setAttribute(DSM_OVERFLOW_HIDDEN, '')
|
|
3368
|
+
btn.setAttribute(DSM_OVERFLOW_WRITE, baseline)
|
|
3369
|
+
return
|
|
3370
|
+
}
|
|
3371
|
+
if (btn.hasAttribute(DSM_OVERFLOW_HIDDEN)) {
|
|
3372
|
+
btn.style.display = ''
|
|
3373
|
+
btn.removeAttribute(DSM_OVERFLOW_HIDDEN)
|
|
3374
|
+
}
|
|
3375
|
+
if (plan.changed) {
|
|
3376
|
+
// 同值不写:写入会触发 characterData 观察 → 重绘,必须保持幂等。
|
|
3377
|
+
if (btn.textContent !== plan.text) btn.textContent = plan.text
|
|
3378
|
+
btn.setAttribute(DSM_OVERFLOW_WRITE, plan.text)
|
|
3379
|
+
} else {
|
|
3380
|
+
if (btn.textContent !== baseline) btn.textContent = baseline
|
|
3381
|
+
btn.setAttribute(DSM_OVERFLOW_WRITE, baseline)
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
function restoreOverflowButton(btn) {
|
|
3385
|
+
const verbatim = btn.getAttribute(DSM_OVERFLOW_BASE)
|
|
3386
|
+
if (verbatim != null && btn.textContent !== verbatim) btn.textContent = verbatim
|
|
3387
|
+
if (btn.hasAttribute(DSM_OVERFLOW_HIDDEN)) {
|
|
3388
|
+
btn.style.display = ''
|
|
3389
|
+
btn.removeAttribute(DSM_OVERFLOW_HIDDEN)
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3079
3392
|
const paint = () => {
|
|
3080
3393
|
if (sidebarAdapter.disabled) return
|
|
3081
3394
|
// Recompute the active session on every paint so the active row is never
|
|
@@ -3121,6 +3434,10 @@ function installSidebarStatusDots() {
|
|
|
3121
3434
|
const purged = purgedNow
|
|
3122
3435
|
if ((dsmTrashIds && dsmTrashIds.has(id)) || purged.has(id)) {
|
|
3123
3436
|
if (row.style.display !== 'none') row.style.display = 'none'
|
|
3437
|
+
// issue #16:标记「这一行是插件隐藏的」,供展开计数扣减使用(数据层
|
|
3438
|
+
// 拿不到 group.sessions 时也能按 DOM 兜底统计)。属性不进 observer 的
|
|
3439
|
+
// attributeFilter,不会自触发重绘。
|
|
3440
|
+
if (row.getAttribute(DSM_HIDDEN_ATTR) !== 'trash') row.setAttribute(DSM_HIDDEN_ATTR, 'trash')
|
|
3124
3441
|
const d = row.querySelector('[' + DOT + ']'); if (d) d.remove()
|
|
3125
3442
|
dsmDropSubList(row, id)
|
|
3126
3443
|
return
|
|
@@ -3153,11 +3470,15 @@ function installSidebarStatusDots() {
|
|
|
3153
3470
|
}
|
|
3154
3471
|
if (hideLineage) {
|
|
3155
3472
|
if (row.style.display !== 'none') row.style.display = 'none'
|
|
3473
|
+
if (row.getAttribute(DSM_HIDDEN_ATTR) !== 'empty') row.setAttribute(DSM_HIDDEN_ATTR, 'empty')
|
|
3156
3474
|
const d = row.querySelector('[' + DOT + ']'); if (d) d.remove()
|
|
3157
3475
|
dsmDropSubList(row, id)
|
|
3158
3476
|
return
|
|
3159
3477
|
}
|
|
3160
3478
|
if (row.style.display === 'none') row.style.display = ''
|
|
3479
|
+
// 行重新可见(已从回收站恢复 / 空白状态消失):摘掉隐藏标记,展开计数
|
|
3480
|
+
// 立刻把这一条还回「可展开」的数量里。
|
|
3481
|
+
if (row.hasAttribute(DSM_HIDDEN_ATTR)) row.removeAttribute(DSM_HIDDEN_ATTR)
|
|
3161
3482
|
if (!isEmpty && li && li.parentSession && li.origin !== 'subagent') {
|
|
3162
3483
|
// fork 分支行:绿「⑂ 分支」chip,点击滚动并高亮父会话行。
|
|
3163
3484
|
// v3.6.2 #3:chip 每次 paint 幂等同步(存在才不建,但 tooltip/父 id
|
|
@@ -3293,7 +3614,9 @@ function installSidebarStatusDots() {
|
|
|
3293
3614
|
}
|
|
3294
3615
|
dot.style.background = color
|
|
3295
3616
|
})
|
|
3296
|
-
dsmSyncEmptyGroups()
|
|
3617
|
+
dsmSyncEmptyGroups(purgedNow)
|
|
3618
|
+
// issue #16:等隐藏标记落定后再校准展开计数(数据层优先,DOM 标记兜底)。
|
|
3619
|
+
dsmSyncOverflowLabels(purgedNow)
|
|
3297
3620
|
}
|
|
3298
3621
|
dsmRepaintDots = paint
|
|
3299
3622
|
const paintToast = (msg, kind) => {
|
package/src/client/logic.js
CHANGED
|
@@ -462,3 +462,96 @@ export function filterShapeFromSaved(item) {
|
|
|
462
462
|
if (tag === '' && f.tag !== '') degraded.push('tag')
|
|
463
463
|
return { view, workspace, sort, tag, degraded }
|
|
464
464
|
}
|
|
465
|
+
|
|
466
|
+
// issue #16:侧栏「展开其余 N 个会话」的 N 由上游按**数据层**计算
|
|
467
|
+
// (0.2.0-rc.2 `collapsedSessionRows` → 0.2.1-rc.2 `watermarkRows`),
|
|
468
|
+
// 看不到插件用 display:none 隐藏的回收站/空白行,于是数字虚高;点展开只是
|
|
469
|
+
// 把更多已隐藏行塞进 DOM,可见行数不变——按钮在动、画面不动,容易被误读成
|
|
470
|
+
// 「会话丢了」。
|
|
471
|
+
//
|
|
472
|
+
// 注意**不能**用「N − 本组被插件隐藏的条数」:上游的 N 数是「水位线之外
|
|
473
|
+
// 还没渲染的行」,隐藏数数的是「整组被藏起来的行」,两个口径不重叠——被藏的
|
|
474
|
+
// 行可能就在已渲染窗口内,减它属重复扣减,点开一次就翻负,进而把仍有可见行
|
|
475
|
+
// 的按钮藏掉(上游只有这一个展开入口 → 会话不可达)。
|
|
476
|
+
//
|
|
477
|
+
// 正确口径由 `remainingRows` 给出:还能出现几行 = 本组可见总数 − 已渲染的
|
|
478
|
+
// 可见行数;`overflowPlan` 只负责把它写进文案、并决定是否藏按钮。
|
|
479
|
+
export function remainingRows(visibleTotal, renderedVisible) {
|
|
480
|
+
// Number(null) === 0:null / undefined 必须当作「不知道」,不能当成 0。
|
|
481
|
+
if (visibleTotal == null || visibleTotal === '') return null
|
|
482
|
+
const total = Number(visibleTotal)
|
|
483
|
+
if (!Number.isFinite(total)) return null
|
|
484
|
+
const shown = Number(renderedVisible)
|
|
485
|
+
const shownCount = Number.isFinite(shown) ? Math.max(0, Math.floor(shown)) : 0
|
|
486
|
+
return Math.max(0, Math.max(0, Math.floor(total)) - shownCount)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 本组已渲染的可见会话行数。`rows` 是直接子节点里会话行的描述
|
|
490
|
+
// ({ id, pluginHidden }),`visibleIds` 是「上游会列出且插件不会藏」的 id 集合。
|
|
491
|
+
// 插件隐藏的行(回收站 / 空白)与不在集合里的行(空白会话 / 上游过滤口径差异)
|
|
492
|
+
// 都不计入 —— 与 `remainingRows` 的「可见总数」保持同一口径。
|
|
493
|
+
export function countRenderedVisible(rows, visibleIds) {
|
|
494
|
+
if (!Array.isArray(rows) || !visibleIds || typeof visibleIds.has !== 'function') return 0
|
|
495
|
+
let n = 0
|
|
496
|
+
for (const row of rows) {
|
|
497
|
+
if (!row || row.pluginHidden) continue
|
|
498
|
+
if (row.id && visibleIds.has(row.id)) n++
|
|
499
|
+
}
|
|
500
|
+
return n
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 空分组判定:`group.sessions` 里「还会出现在侧栏」的条数。
|
|
504
|
+
// - 空白会话不计:上游 `watermarkRows` 里 `if (session.blank) return true`,
|
|
505
|
+
// 空白行永远保留、从不计入可展开数;插件也一律把它藏起来。
|
|
506
|
+
// - `hiddenIds` = 回收站 / 墓碑 / 空白血缘的行 id(与展开计数同一份口径)。
|
|
507
|
+
// - 返回 null 表示**不知道**(拿不到数据层,或该组还没加载出会话):调用方
|
|
508
|
+
// 必须按「非空」处理——宁可留一个空标题,也不能把仍有会话的组藏掉。
|
|
509
|
+
export function aliveSessionCount(sessions, hiddenIds) {
|
|
510
|
+
if (!Array.isArray(sessions) || !sessions.length) return null
|
|
511
|
+
let alive = 0
|
|
512
|
+
let seen = 0
|
|
513
|
+
for (const node of sessions) {
|
|
514
|
+
if (!node || node.id == null) continue
|
|
515
|
+
const id = String(node.id)
|
|
516
|
+
if (!id) continue
|
|
517
|
+
seen++
|
|
518
|
+
if (node.blank) continue
|
|
519
|
+
if (hiddenIds && typeof hiddenIds.has === 'function' && hiddenIds.has(id)) continue
|
|
520
|
+
alive++
|
|
521
|
+
}
|
|
522
|
+
// 一条带 id 的都没有 = 结构不认识(不是「组空」),照未知处理。
|
|
523
|
+
return seen === 0 ? null : alive
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 把「还能出现几行」落到展开按钮上:
|
|
527
|
+
// - `more` 未知(拿不到数据层)→ 一律不动作。DOM 兜底在数学上不成立
|
|
528
|
+
// (窗口内/外的隐藏数口径不同),宁可不改也不写一个看起来合理却会算错的数;
|
|
529
|
+
// - 文案没有数字(展开态的「收起」、或上游改成纯图标)→ 不改也不藏。无法确认
|
|
530
|
+
// 是不是计数文案时保持沉默;
|
|
531
|
+
// - 只替换第一个数字(各语言文案都把计数放在数字位,如「展开其余 12 个会话」
|
|
532
|
+
// /「Show 12 more」),支持千分位写法;
|
|
533
|
+
// - `more === 0` 才藏按钮:此时确实一行都不会再出现,留着只会让人反复点。
|
|
534
|
+
export function overflowPlan(text, more) {
|
|
535
|
+
const raw = typeof text === 'string' ? text : ''
|
|
536
|
+
const unknown = more == null || !Number.isFinite(Number(more))
|
|
537
|
+
const remaining = unknown ? 0 : Math.max(0, Math.floor(Number(more)))
|
|
538
|
+
const m = raw.match(/(\d[\d,]*)/)
|
|
539
|
+
const base = m ? Number(m[1].replace(/,/g, '')) : null
|
|
540
|
+
if (base === null || unknown) {
|
|
541
|
+
return { text: raw, hide: false, base, more: unknown ? null : remaining, changed: false }
|
|
542
|
+
}
|
|
543
|
+
if (remaining === 0) {
|
|
544
|
+
// base<=0 是上游没给出可信计数(理论上不会出现):不据此隐藏,避免误判。
|
|
545
|
+
return { text: raw, hide: base > 0, base, more: 0, changed: false }
|
|
546
|
+
}
|
|
547
|
+
if (remaining === base) {
|
|
548
|
+
return { text: raw, hide: false, base, more: remaining, changed: false }
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
text: raw.slice(0, m.index) + String(remaining) + raw.slice(m.index + m[1].length),
|
|
552
|
+
hide: false,
|
|
553
|
+
base,
|
|
554
|
+
more: remaining,
|
|
555
|
+
changed: true,
|
|
556
|
+
}
|
|
557
|
+
}
|