zcode-pro 0.16.0__py3-none-any.whl
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.
- zcode_pro/__init__.py +3 -0
- zcode_pro/__main__.py +4 -0
- zcode_pro/app/cli.mjs +9 -0
- zcode_pro/app/dist/inject.js +5821 -0
- zcode_pro/app/src/host/cdp.mjs +126 -0
- zcode_pro/app/src/host/helper.mjs +2134 -0
- zcode_pro/app/src/host/main.mjs +262 -0
- zcode_pro/app/src/host/paths.mjs +73 -0
- zcode_pro/app/src/host/pickFolder.mjs +152 -0
- zcode_pro/app/src/host/settings.mjs +61 -0
- zcode_pro/app/src/host/taskIndex.mjs +255 -0
- zcode_pro/app/src/host/taskOrder.mjs +117 -0
- zcode_pro/app/src/host/webdav.mjs +105 -0
- zcode_pro/app/src/inject/core.js +751 -0
- zcode_pro/app/src/inject/features/alias.js +211 -0
- zcode_pro/app/src/inject/features/copy-path.js +78 -0
- zcode_pro/app/src/inject/features/file-menu.js +147 -0
- zcode_pro/app/src/inject/features/idle-reclaim.js +179 -0
- zcode_pro/app/src/inject/features/image-menu.js +215 -0
- zcode_pro/app/src/inject/features/open-folder.js +43 -0
- zcode_pro/app/src/inject/features/pinned-collapse.js +132 -0
- zcode_pro/app/src/inject/features/pinned-expand.js +142 -0
- zcode_pro/app/src/inject/features/plugin-updates.js +28 -0
- zcode_pro/app/src/inject/features/project-menu.js +58 -0
- zcode_pro/app/src/inject/features/relocate-dialog.js +156 -0
- zcode_pro/app/src/inject/features/session-switch.js +244 -0
- zcode_pro/app/src/inject/features/settings-dialog.js +1503 -0
- zcode_pro/app/src/inject/features/settings-entry.js +21 -0
- zcode_pro/app/src/inject/features/styles.js +311 -0
- zcode_pro/app/src/inject/features/task-order.js +78 -0
- zcode_pro/app/src/inject/features/title-lock.js +242 -0
- zcode_pro/app/src/inject/features/toolbar-icons.js +243 -0
- zcode_pro/app/src/inject/features/ws-running.js +472 -0
- zcode_pro/app/src/inject/index.js +55 -0
- zcode_pro/app/src/inject/ui.js +734 -0
- zcode_pro/cli.py +71 -0
- zcode_pro-0.16.0.dist-info/METADATA +45 -0
- zcode_pro-0.16.0.dist-info/RECORD +41 -0
- zcode_pro-0.16.0.dist-info/WHEEL +4 -0
- zcode_pro-0.16.0.dist-info/entry_points.txt +2 -0
- zcode_pro-0.16.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// 折叠项目运行提示:项目折叠后,若其中仍有会话在运行,项目图标旋转提示,
|
|
2
|
+
// 全部结束或项目展开后恢复静止。应用本身没有跨折叠的运行信号——运行状态只
|
|
3
|
+
// 体现在侧栏已挂载会话行的 loader 图标上,项目一折叠这些行就被卸载
|
|
4
|
+
// (任务索引库、窗口标题、window.zcode 桥均不实时,实测确认),因此分三层:
|
|
5
|
+
// 1) 扫描在场的会话行,维护“工作区 → 运行中会话键”;
|
|
6
|
+
// 2) 折叠瞬间从 MutationObserver 的 removedNodes 读各行 loader(已移除的节点
|
|
7
|
+
// 仍可查询),把最后状态并入表:折叠保留、展开着的移除视为任务被删;
|
|
8
|
+
// 3) 对标记运行中的折叠项目做“无感探查”复核:点项目行展开(应用的该点击会
|
|
9
|
+
// 把主视图切到该项目,故点完立刻同帧点回当前会话行,两次提交都在绘制前
|
|
10
|
+
// 完成、视图与选中不受影响),新增的会话行由 style 元素按会话键前缀
|
|
11
|
+
// display:none(挂载当帧即生效,不依赖观察器时序与列表容器结构——应用
|
|
12
|
+
// 3.14.4 已无旧代码依赖的 empty:hidden 容器),原项目行用 position:fixed
|
|
13
|
+
// 冻结克隆盖住,读完后收起、等行真正卸载再撤隐藏(收起后行有数百毫秒的
|
|
14
|
+
// 卸载延迟,提前撤隐藏会让行在收起瞬间闪现)。单次约 0.3 秒。
|
|
15
|
+
// 探查调度:只在“捕获到运行中”后复核(20 秒首查、其后每 60 秒)。不做启动
|
|
16
|
+
// 普查:应用重启后所有会话本就处于停止状态,无运行可发现;且启动期界面还在
|
|
17
|
+
// 恢复,探查会打扰(实测首启会出现项目逐个展开又收起的干扰)。
|
|
18
|
+
// 开关:helper wsRunningSpin(默认 true)。
|
|
19
|
+
import { rpc } from '../core.js';
|
|
20
|
+
|
|
21
|
+
let installed = false;
|
|
22
|
+
let enabled = true;
|
|
23
|
+
|
|
24
|
+
const SCAN_DEBOUNCE_MS = 150; // 相关 DOM 变动后的去抖
|
|
25
|
+
const SCAN_FALLBACK_MS = 5000; // 兜底轮询周期(观察器覆盖不到的路径)
|
|
26
|
+
const PEEK_FIRST_MS = 20000; // 折叠捕获到运行后的首次复核
|
|
27
|
+
const PEEK_REPEAT_MS = 60000; // 仍运行中的复核间隔
|
|
28
|
+
const PEEK_SETTLE_MS = 160; // 行数连续两次采样相同视为渲染完成
|
|
29
|
+
const PEEK_MAX_MS = 1500; // 单次探查兜底(行始终没出现 = 项目无会话)
|
|
30
|
+
const PEEK_UNMOUNT_WAIT_MS = 800; // 收起后等行真正卸载的最长时间(实测约 0.4 秒)
|
|
31
|
+
|
|
32
|
+
const CONFIG_POLL_MIN_MS = 5000; // 配置未变化时逐次拉长轮询间隔,减轻常驻开销
|
|
33
|
+
const CONFIG_POLL_MAX_MS = 60000;
|
|
34
|
+
|
|
35
|
+
const TASK_SEL = '[data-task-item-key]';
|
|
36
|
+
const WS_SEL = '[data-testid^="workspace-item-"]';
|
|
37
|
+
|
|
38
|
+
const runningByWs = new Map(); // 工作区路径(原样,来自会话键)→ Set(会话键)
|
|
39
|
+
const peekTimers = new Map(); // 工作区 → 复核定时器
|
|
40
|
+
const activePeeks = new Set(); // 正在探查的工作区
|
|
41
|
+
const peekedWs = new Set(); // 已探查过的项目:之后统一按复查节奏(探查自身的收起动作
|
|
42
|
+
// 也会触发一次折叠捕获,避免把节奏重置回首查间隔)
|
|
43
|
+
let wsWithRows = new Set(); // 最近一次扫描时仍有会话行在 DOM 的工作区
|
|
44
|
+
|
|
45
|
+
// —— 工具 ——
|
|
46
|
+
|
|
47
|
+
function workspaceOf(key) {
|
|
48
|
+
// 会话键格式 <workspace_path>:<sess_id>;sess 段固定以 "sess_" 开头,
|
|
49
|
+
// 按它定位分隔符(Windows 盘符路径里也有冒号,不能按第一个 ':' 切)
|
|
50
|
+
const i = key.indexOf(':sess_');
|
|
51
|
+
return i > 0 ? key.slice(0, i) : key;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findWsRow(ws) {
|
|
55
|
+
return document.querySelector(`[data-testid="workspace-item-${CSS.escape(ws)}"]`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function wsHead(row) {
|
|
59
|
+
return row.matches('[aria-expanded]') ? row : row.querySelector('[aria-expanded]');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 运行中的行首图标是 loader;空闲行该位置为空 span(无 svg)。只认行首图标位
|
|
63
|
+
// (span 的直接子级),行内操作按钮等处将来即使出现 loader 也不算
|
|
64
|
+
function isRunningRow(el) {
|
|
65
|
+
const holder = el.firstElementChild && el.firstElementChild.querySelector(':scope > span');
|
|
66
|
+
if (!holder) return false;
|
|
67
|
+
return [...holder.children].some((c) => c.tagName === 'svg' && (c.getAttribute('class') || '').includes('lucide-loader'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function domRowsOf(ws) {
|
|
71
|
+
return [...document.querySelectorAll(TASK_SEL)]
|
|
72
|
+
.filter((e) => workspaceOf(e.getAttribute('data-task-item-key') || '') === ws);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 当前选中的会话行(bg-selected 类,session-switch.js 同款标记)
|
|
76
|
+
function currentTaskRow() {
|
|
77
|
+
for (const el of document.querySelectorAll(TASK_SEL)) {
|
|
78
|
+
if ((el.className + '').includes('bg-selected')) return el;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const synthClick = (el) => {
|
|
84
|
+
try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } catch { /* ignore */ }
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// 无副作用的展开/收起:应用把项目行点击当作“切换到该项目”(实测当前会话会被
|
|
88
|
+
// 取消选中、主视图跳进该项目的新建任务)。点完项目行后立刻点回当前会话行,
|
|
89
|
+
// 两次提交都在浏览器绘制前完成——折叠照常生效,视图与选中状态不被带走。
|
|
90
|
+
// 还原行必须在点项目行之前就记住:点击会把当前行取消选中,事后再按选中标记
|
|
91
|
+
// 找就找不到了(3.14.4 实测,回点落空会把用户留在该项目的新会话页);
|
|
92
|
+
// 元素被 React 重挂载时按记住的会话键重找
|
|
93
|
+
function toggleNavSafe(head) {
|
|
94
|
+
const cur = currentTaskRow();
|
|
95
|
+
const backKey = cur ? cur.getAttribute('data-task-item-key') : '';
|
|
96
|
+
synthClick(head);
|
|
97
|
+
if (!backKey) return;
|
|
98
|
+
const back = (cur && cur.isConnected)
|
|
99
|
+
? cur
|
|
100
|
+
: document.querySelector(`${TASK_SEL}[data-task-item-key="${CSS.escape(backKey)}"]`);
|
|
101
|
+
if (back) synthClick(back);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// —— 状态维护 ——
|
|
105
|
+
|
|
106
|
+
function scan() {
|
|
107
|
+
wsWithRows = new Set();
|
|
108
|
+
for (const el of document.querySelectorAll(TASK_SEL)) {
|
|
109
|
+
const key = el.getAttribute('data-task-item-key') || '';
|
|
110
|
+
const ws = workspaceOf(key);
|
|
111
|
+
if (!ws) continue;
|
|
112
|
+
wsWithRows.add(ws);
|
|
113
|
+
const run = isRunningRow(el);
|
|
114
|
+
let set = runningByWs.get(ws);
|
|
115
|
+
if (run) {
|
|
116
|
+
if (!set) { set = new Set(); runningByWs.set(ws, set); }
|
|
117
|
+
set.add(key);
|
|
118
|
+
} else if (set) {
|
|
119
|
+
set.delete(key);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 无条件复核探查调度:行从有到无(折叠/卸载)时 scan 本身看不到变化,
|
|
123
|
+
// 但 wsWithRows 已更新,正是需要(重新)排复核的时机
|
|
124
|
+
syncPeeks();
|
|
125
|
+
apply();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 折叠/收起移除会话行时,从被移除节点读最后状态(removedNodes 子树仍可查询)
|
|
129
|
+
function handleRemovals(muts) {
|
|
130
|
+
const removed = [];
|
|
131
|
+
for (const m of muts) {
|
|
132
|
+
if (m.type !== 'childList') continue;
|
|
133
|
+
for (const n of m.removedNodes) {
|
|
134
|
+
if (!(n instanceof Element)) continue;
|
|
135
|
+
if (n.matches(TASK_SEL)) removed.push(n);
|
|
136
|
+
else for (const r of n.querySelectorAll(TASK_SEL)) removed.push(r);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!removed.length) return;
|
|
140
|
+
const infos = removed
|
|
141
|
+
.map((el) => {
|
|
142
|
+
const key = el.getAttribute('data-task-item-key') || '';
|
|
143
|
+
const ws = workspaceOf(key);
|
|
144
|
+
return key && ws ? { key, ws, run: isRunningRow(el) } : null;
|
|
145
|
+
})
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
if (!infos.length) return;
|
|
148
|
+
// 展开态判定等本批渲染全部落地后再做(aria-expanded 可能与行移除分两次提交)
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
for (const info of infos) {
|
|
151
|
+
const row = findWsRow(info.ws);
|
|
152
|
+
const head = row && wsHead(row);
|
|
153
|
+
const collapsed = !row || !head || head.getAttribute('aria-expanded') === 'false';
|
|
154
|
+
let set = runningByWs.get(info.ws);
|
|
155
|
+
if (collapsed && info.run) {
|
|
156
|
+
if (!set) { set = new Set(); runningByWs.set(info.ws, set); }
|
|
157
|
+
set.add(info.key);
|
|
158
|
+
} else if (set) {
|
|
159
|
+
set.delete(info.key); // 展开着却被移除 = 任务被删/归档;折叠但已结束 = 清除
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
syncPeeks();
|
|
163
|
+
apply();
|
|
164
|
+
}, 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// —— 应用到项目行 ——
|
|
168
|
+
|
|
169
|
+
function apply() {
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
for (const row of document.querySelectorAll(WS_SEL)) {
|
|
172
|
+
const path = row.getAttribute('data-testid').slice('workspace-item-'.length);
|
|
173
|
+
seen.add(path);
|
|
174
|
+
const head = wsHead(row);
|
|
175
|
+
const collapsed = !head || head.getAttribute('aria-expanded') === 'false';
|
|
176
|
+
const running = !!(enabled && collapsed && runningByWs.get(path) && runningByWs.get(path).size);
|
|
177
|
+
// 类挂在图标的外层 span 上:展开/收起时 React 会重建 svg,span 是稳定节点
|
|
178
|
+
const icon = row.firstElementChild && row.firstElementChild.querySelector(':scope > span');
|
|
179
|
+
if (icon) icon.classList.toggle('zcodepro-ws-running', running);
|
|
180
|
+
}
|
|
181
|
+
// 侧栏里已不存在的项目清掉状态
|
|
182
|
+
for (const ws of [...runningByWs.keys()]) if (!seen.has(ws)) runningByWs.delete(ws);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// —— 无感探查 ——
|
|
186
|
+
|
|
187
|
+
function schedulePeek(ws, delay) {
|
|
188
|
+
clearTimeout(peekTimers.get(ws));
|
|
189
|
+
peekTimers.set(ws, setTimeout(() => {
|
|
190
|
+
peekTimers.delete(ws);
|
|
191
|
+
void peek(ws);
|
|
192
|
+
}, delay));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function clearPeek(ws) {
|
|
196
|
+
const t = peekTimers.get(ws);
|
|
197
|
+
if (t) { clearTimeout(t); peekTimers.delete(ws); }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function syncPeeks() {
|
|
201
|
+
for (const [ws, set] of runningByWs) {
|
|
202
|
+
if (!enabled || !set.size) { clearPeek(ws); continue; }
|
|
203
|
+
const row = findWsRow(ws);
|
|
204
|
+
const head = row && wsHead(row);
|
|
205
|
+
// 项目展开着、或它的会话行在别处可见(置顶区):现场信号已足够,不探查
|
|
206
|
+
if (!row || !head || head.getAttribute('aria-expanded') !== 'false' || wsWithRows.has(ws)) {
|
|
207
|
+
clearPeek(ws);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (!peekTimers.has(ws) && !activePeeks.has(ws)) schedulePeek(ws, peekedWs.has(ws) ? PEEK_REPEAT_MS : PEEK_FIRST_MS);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 探查串行链:同一时间最多一个探查在跑(多项复核同时到期时,重叠会互相干扰)
|
|
215
|
+
let peekChain = Promise.resolve();
|
|
216
|
+
|
|
217
|
+
async function peek(ws, opts) {
|
|
218
|
+
const run = () => peekInner(ws, opts);
|
|
219
|
+
const p = peekChain.then(run, run);
|
|
220
|
+
peekChain = p.catch(() => {});
|
|
221
|
+
return p;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function peekInner(ws, opts) {
|
|
225
|
+
// manual:idle-reclaim 的复核调用,不受本功能开关限制(调度侧已按 enabled 过滤)。
|
|
226
|
+
// 返回 true = 完成了一次真实读取(结果已并入 runningByWs);false/undefined = 未验证
|
|
227
|
+
if ((!opts?.manual && !enabled) || document.hidden || activePeeks.has(ws)) return false;
|
|
228
|
+
const row = findWsRow(ws);
|
|
229
|
+
const head = row && wsHead(row);
|
|
230
|
+
if (!row || !head || head.getAttribute('aria-expanded') !== 'false') return false;
|
|
231
|
+
// 无可还原的当前会话时放弃本次探查(点了项目行会把主视图切到该项目的新会话
|
|
232
|
+
// 页,没有可回点的行就还原不回来——新建任务视图同样不放行),60 秒后再试——
|
|
233
|
+
// 探查绝不能把用户的当前视图带走
|
|
234
|
+
if (!currentTaskRow()) {
|
|
235
|
+
schedulePeek(ws, PEEK_REPEAT_MS);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
activePeeks.add(ws);
|
|
240
|
+
peekedWs.add(ws);
|
|
241
|
+
// 冻结克隆盖住项目行:探查期间图标(folder↔folder-open)等重渲染不可见;
|
|
242
|
+
// 克隆保留 zcodepro-ws-running 类,转圈不中断
|
|
243
|
+
const rect = row.getBoundingClientRect();
|
|
244
|
+
const frozen = row.cloneNode(true);
|
|
245
|
+
frozen.classList.add('zcodepro-ws-frozen');
|
|
246
|
+
frozen.style.left = rect.left + 'px';
|
|
247
|
+
frozen.style.top = rect.top + 'px';
|
|
248
|
+
frozen.style.width = rect.width + 'px';
|
|
249
|
+
frozen.style.height = rect.height + 'px';
|
|
250
|
+
document.body.append(frozen);
|
|
251
|
+
row.style.visibility = 'hidden';
|
|
252
|
+
// 会话行隐藏改走 style 元素(见 installPeekHider):挂载当帧即 display:none,
|
|
253
|
+
// 与观察器回调时序、列表容器结构均无关
|
|
254
|
+
const hider = installPeekHider(ws);
|
|
255
|
+
try {
|
|
256
|
+
toggleNavSafe(head);
|
|
257
|
+
await settleAndRead(ws);
|
|
258
|
+
// 读完后收起;用户在探查期间点了项目行就不再干预
|
|
259
|
+
const rowNow = findWsRow(ws) || row;
|
|
260
|
+
const headNow = wsHead(rowNow);
|
|
261
|
+
if (headNow && headNow.getAttribute('aria-expanded') === 'true' && !userTouched(rowNow)) {
|
|
262
|
+
toggleNavSafe(headNow);
|
|
263
|
+
// 收起后行并非立刻卸载(有数百毫秒延迟),期间保持隐藏再撤,
|
|
264
|
+
// 否则行会在收起瞬间闪现——即“项目先展开再缩起”被看见的原因
|
|
265
|
+
await waitForRowsGone(ws);
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
removePeekHider(hider);
|
|
269
|
+
row.style.visibility = '';
|
|
270
|
+
frozen.remove();
|
|
271
|
+
activePeeks.delete(ws);
|
|
272
|
+
}
|
|
273
|
+
apply();
|
|
274
|
+
const set = runningByWs.get(ws);
|
|
275
|
+
if (set && set.size && enabled) schedulePeek(ws, PEEK_REPEAT_MS);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// —— 供 idle-reclaim 复用 ——
|
|
280
|
+
|
|
281
|
+
// 当前运行状态快照:running 为仍有运行中会话的工作区列表,current 为当前选中会话的工作区
|
|
282
|
+
export function wsRunningSnapshot() {
|
|
283
|
+
const curKey = currentTaskRow()?.getAttribute('data-task-item-key') || '';
|
|
284
|
+
return {
|
|
285
|
+
running: [...runningByWs.entries()].filter(([, set]) => set.size > 0).map(([ws]) => ws),
|
|
286
|
+
current: curKey ? (workspaceOf(curKey) || null) : null,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 无感复核指定工作区当前是否仍有运行中会话(展开读行后原样收起)。
|
|
291
|
+
// 返回 true = 确认无运行;false = 有运行、窗口隐藏或行不可达(一律视为忙,调用方不得回收)
|
|
292
|
+
export async function verifyWorkspaceNotRunning(ws) {
|
|
293
|
+
if (!ws || document.hidden) return false;
|
|
294
|
+
const row = findWsRow(ws);
|
|
295
|
+
const head = row && wsHead(row);
|
|
296
|
+
if (!row || !head) return false;
|
|
297
|
+
if (head.getAttribute('aria-expanded') !== 'false') {
|
|
298
|
+
// 已展开:行在场,直接读
|
|
299
|
+
let run = false;
|
|
300
|
+
for (const el of domRowsOf(ws)) if (isRunningRow(el)) run = true;
|
|
301
|
+
return !run;
|
|
302
|
+
}
|
|
303
|
+
let verified = false;
|
|
304
|
+
try { verified = (await peek(ws, { manual: true })) === true; } catch { /* 未验证成 */ }
|
|
305
|
+
if (!verified) return false;
|
|
306
|
+
const set = runningByWs.get(ws);
|
|
307
|
+
return !(set && set.size);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 等该项目的会话行渲染稳定后读取运行状态;行始终没出现视为项目无会话,清除记录
|
|
311
|
+
function settleAndRead(ws) {
|
|
312
|
+
return new Promise((resolve) => {
|
|
313
|
+
const t0 = Date.now();
|
|
314
|
+
let lastCount = -1;
|
|
315
|
+
let stableAt = 0;
|
|
316
|
+
const timer = setInterval(() => {
|
|
317
|
+
const rows = domRowsOf(ws);
|
|
318
|
+
const elapsed = Date.now() - t0;
|
|
319
|
+
if (rows.length !== lastCount) { lastCount = rows.length; stableAt = Date.now(); }
|
|
320
|
+
const settled = rows.length > 0 && rows.length === lastCount && Date.now() - stableAt >= PEEK_SETTLE_MS;
|
|
321
|
+
if (!settled && elapsed < PEEK_MAX_MS) return;
|
|
322
|
+
clearInterval(timer);
|
|
323
|
+
if (lastCount === 0) {
|
|
324
|
+
runningByWs.delete(ws);
|
|
325
|
+
} else {
|
|
326
|
+
let set = runningByWs.get(ws);
|
|
327
|
+
if (!set) { set = new Set(); runningByWs.set(ws, set); }
|
|
328
|
+
for (const el of rows) {
|
|
329
|
+
const key = el.getAttribute('data-task-item-key') || '';
|
|
330
|
+
if (isRunningRow(el)) set.add(key);
|
|
331
|
+
else set.delete(key);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
resolve();
|
|
335
|
+
}, 80);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// —— 探查期间的会话行隐藏 ——
|
|
340
|
+
// style 元素按会话键前缀隐藏:CSS 在节点挂载当帧即生效,不依赖观察器回调
|
|
341
|
+
// 时序,也与列表容器结构无关(旧代码依赖的 empty:hidden 容器在应用 3.14.4
|
|
342
|
+
// 已不存在;现列表容器仅 space-y 间距,行全隐时无高度,无需藏容器)。
|
|
343
|
+
// 探查开始前已在场的同键行(如置顶区)打豁免类,不受影响。
|
|
344
|
+
|
|
345
|
+
const PEEK_EXEMPT_CLASS = 'zcodepro-peek-exempt';
|
|
346
|
+
const cssAttr = (v) => String(v).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
347
|
+
|
|
348
|
+
function installPeekHider(ws) {
|
|
349
|
+
const rowSel = `${TASK_SEL}[data-task-item-key^="${cssAttr(ws + ':')}"]`;
|
|
350
|
+
for (const r of document.querySelectorAll(rowSel)) r.classList.add(PEEK_EXEMPT_CLASS);
|
|
351
|
+
const style = document.createElement('style');
|
|
352
|
+
// 只隐藏会话行本身:容器(ul/列表外壳/折叠动画层)不能藏——应用的展开动画
|
|
353
|
+
// 会测量内容高度,藏容器会导致收起后行不卸载反而重新挂载(实测)
|
|
354
|
+
style.textContent = `${rowSel}:not(.${PEEK_EXEMPT_CLASS}){display:none!important}`;
|
|
355
|
+
document.head.append(style);
|
|
356
|
+
return { style, sel: rowSel };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function removePeekHider(hider) {
|
|
360
|
+
hider.style.remove();
|
|
361
|
+
for (const r of document.querySelectorAll('.' + PEEK_EXEMPT_CLASS)) r.classList.remove(PEEK_EXEMPT_CLASS);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 收起后等该项目的会话行全部离开 DOM(卸载有数百毫秒延迟),超时兜底放行
|
|
365
|
+
function waitForRowsGone(ws) {
|
|
366
|
+
const sel = `${TASK_SEL}[data-task-item-key^="${cssAttr(ws + ':')}"]:not(.${PEEK_EXEMPT_CLASS})`;
|
|
367
|
+
return new Promise((resolve) => {
|
|
368
|
+
const t0 = Date.now();
|
|
369
|
+
const timer = setInterval(() => {
|
|
370
|
+
if (!document.querySelector(sel) || Date.now() - t0 >= PEEK_UNMOUNT_WAIT_MS) {
|
|
371
|
+
clearInterval(timer);
|
|
372
|
+
resolve();
|
|
373
|
+
}
|
|
374
|
+
}, 40);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// —— 用户主动操作项目行的标记(探查收起前 800 毫秒内用户点过就不干预) ——
|
|
379
|
+
// 记录在模块内 WeakMap(不挂在元素属性上):真实点击(isTrusted)才记录,
|
|
380
|
+
// 本功能自己派发的合成点击不算,React 重挂载后的新行也不带旧记录
|
|
381
|
+
|
|
382
|
+
const TOUCH_WINDOW_MS = 800;
|
|
383
|
+
const wsTouchAt = new WeakMap();
|
|
384
|
+
|
|
385
|
+
function userTouched(row) {
|
|
386
|
+
const at = wsTouchAt.get(row);
|
|
387
|
+
return !!at && Date.now() - at < TOUCH_WINDOW_MS;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// —— 变动过滤 ——
|
|
391
|
+
|
|
392
|
+
function inSel(node, sel) {
|
|
393
|
+
return !!(node instanceof Element && node.closest && node.closest(sel));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function relevant(muts) {
|
|
397
|
+
// 只观察 childList:本功能关心的全是结构变动(loader 图标增删、会话/项目行
|
|
398
|
+
// 增删、React 重建行内元素),聊天流式输出的 characterData/class 高频变动
|
|
399
|
+
// 一概不进回调(实测后台态全页 5 秒仅 5 个变动、全为非 childList)
|
|
400
|
+
for (const m of muts) {
|
|
401
|
+
if (inSel(m.target, TASK_SEL) || inSel(m.target, WS_SEL)) return true;
|
|
402
|
+
for (const n of m.addedNodes) {
|
|
403
|
+
if (n instanceof Element && (n.matches(TASK_SEL) || n.matches(WS_SEL) || n.querySelector(TASK_SEL) || n.querySelector(WS_SEL))) return true;
|
|
404
|
+
}
|
|
405
|
+
for (const n of m.removedNodes) {
|
|
406
|
+
if (n instanceof Element && (n.matches(TASK_SEL) || n.matches(WS_SEL) || n.querySelector(TASK_SEL))) return true;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// —— 配置(自适应轮询,模式同 pinned-expand.js) ——
|
|
413
|
+
|
|
414
|
+
let pollMs = CONFIG_POLL_MIN_MS;
|
|
415
|
+
|
|
416
|
+
async function refreshConfig() {
|
|
417
|
+
try {
|
|
418
|
+
const res = await rpc('/config');
|
|
419
|
+
if (res && res.ok && res.config && res.config.features) {
|
|
420
|
+
const next = res.config.features.wsRunningSpin !== false;
|
|
421
|
+
if (next !== enabled) {
|
|
422
|
+
enabled = next;
|
|
423
|
+
if (!next) {
|
|
424
|
+
for (const ws of [...peekTimers.keys()]) clearPeek(ws);
|
|
425
|
+
apply();
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} catch { /* helper 未就绪时保持默认 */ }
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function pollConfig() {
|
|
435
|
+
setTimeout(async () => {
|
|
436
|
+
if (document.hidden) pollMs = CONFIG_POLL_MAX_MS;
|
|
437
|
+
else pollMs = (await refreshConfig()) ? CONFIG_POLL_MIN_MS : Math.min(Math.round(pollMs * 1.5), CONFIG_POLL_MAX_MS);
|
|
438
|
+
pollConfig();
|
|
439
|
+
}, pollMs);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// —— 入口 ——
|
|
443
|
+
|
|
444
|
+
let scanDebounce = 0;
|
|
445
|
+
|
|
446
|
+
export function startWsRunningSpin() {
|
|
447
|
+
if (installed || typeof document === 'undefined') return;
|
|
448
|
+
installed = true;
|
|
449
|
+
void refreshConfig();
|
|
450
|
+
pollConfig();
|
|
451
|
+
|
|
452
|
+
const observer = new MutationObserver((muts) => {
|
|
453
|
+
if (!relevant(muts)) return;
|
|
454
|
+
handleRemovals(muts);
|
|
455
|
+
if (!scanDebounce) {
|
|
456
|
+
scanDebounce = setTimeout(() => { scanDebounce = 0; scan(); }, SCAN_DEBOUNCE_MS);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
460
|
+
|
|
461
|
+
scan();
|
|
462
|
+
setInterval(() => { if (!document.hidden && enabled) scan(); }, SCAN_FALLBACK_MS);
|
|
463
|
+
document.addEventListener('visibilitychange', () => { if (!document.hidden) scan(); });
|
|
464
|
+
|
|
465
|
+
// 用户点项目行 = 主动展开/收起,探查不与其抢操作(捕获阶段,先于应用处理)。
|
|
466
|
+
// 只认真实点击:探查自己派发的合成点击(isTrusted=false)不算用户操作
|
|
467
|
+
document.addEventListener('click', (e) => {
|
|
468
|
+
if (!e.isTrusted) return;
|
|
469
|
+
const row = e.target instanceof Element && e.target.closest(WS_SEL);
|
|
470
|
+
if (row) wsTouchAt.set(row, Date.now());
|
|
471
|
+
}, true);
|
|
472
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// ZCode Pro 注入脚本入口(由 esbuild 打包为 dist/inject.js,launcher 拼上启动参数后注入页面)。
|
|
2
|
+
// 幂等守卫:页面刷新/重复注入时只初始化一次。
|
|
3
|
+
import { observeRadixPopups } from './core.js';
|
|
4
|
+
import { handleProjectMenu } from './features/project-menu.js';
|
|
5
|
+
import { handleFileMenu } from './features/file-menu.js';
|
|
6
|
+
import { startImageMenu } from './features/image-menu.js';
|
|
7
|
+
import { startSettingsEntry } from './features/settings-entry.js';
|
|
8
|
+
import { startAliasWatcher } from './features/alias.js';
|
|
9
|
+
import { startTaskOrderWatcher } from './features/task-order.js';
|
|
10
|
+
import { startSessionSwitch } from './features/session-switch.js';
|
|
11
|
+
import { startTitleLock } from './features/title-lock.js';
|
|
12
|
+
import { startPinnedExpandSuppression } from './features/pinned-expand.js';
|
|
13
|
+
import { startWsRunningSpin } from './features/ws-running.js';
|
|
14
|
+
import { startIdleReclaim } from './features/idle-reclaim.js';
|
|
15
|
+
import { startToolbarIcons } from './features/toolbar-icons.js';
|
|
16
|
+
import { startPinnedCollapse } from './features/pinned-collapse.js';
|
|
17
|
+
import { startStyleAdjustments } from './features/styles.js';
|
|
18
|
+
import { startPluginUpdateCheck } from './features/plugin-updates.js';
|
|
19
|
+
import { ensureStyle } from './ui.js';
|
|
20
|
+
|
|
21
|
+
(function zcodeproInject() {
|
|
22
|
+
if (typeof window === 'undefined') return;
|
|
23
|
+
if (window.__zcodeproInjected) return;
|
|
24
|
+
window.__zcodeproInjected = true;
|
|
25
|
+
|
|
26
|
+
// 注意:本脚本可能在 document start 阶段执行(head 尚未就绪),
|
|
27
|
+
// 因此所有 DOM 操作都推迟到 DOMContentLoaded 之后。
|
|
28
|
+
const start = () => {
|
|
29
|
+
ensureStyle();
|
|
30
|
+
observeRadixPopups((content) => {
|
|
31
|
+
try { handleProjectMenu(content); } catch { /* 单个功能失败不影响其他 */ }
|
|
32
|
+
try { handleFileMenu(content); } catch { /* ignore */ }
|
|
33
|
+
});
|
|
34
|
+
try { void startAliasWatcher(); } catch { /* ignore */ }
|
|
35
|
+
try { startTaskOrderWatcher(); } catch { /* ignore */ }
|
|
36
|
+
try { startSessionSwitch(); } catch { /* ignore */ }
|
|
37
|
+
try { startTitleLock(); } catch { /* ignore */ }
|
|
38
|
+
try { startImageMenu(); } catch { /* ignore */ }
|
|
39
|
+
try { startPinnedExpandSuppression(); } catch { /* ignore */ }
|
|
40
|
+
try { startWsRunningSpin(); } catch { /* ignore */ }
|
|
41
|
+
try { startIdleReclaim(); } catch { /* ignore */ }
|
|
42
|
+
try { startToolbarIcons(); } catch { /* ignore */ }
|
|
43
|
+
try { startPinnedCollapse(); } catch { /* ignore */ }
|
|
44
|
+
try { startStyleAdjustments(); } catch { /* ignore */ }
|
|
45
|
+
try { startSettingsEntry(); } catch { /* ignore */ }
|
|
46
|
+
// 插件更新检查慢(可能 git pull),异步进行,不阻塞其他功能
|
|
47
|
+
try { void startPluginUpdateCheck(); } catch { /* ignore */ }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (document.readyState === 'loading') {
|
|
51
|
+
document.addEventListener('DOMContentLoaded', () => start(), { once: true });
|
|
52
|
+
} else {
|
|
53
|
+
start();
|
|
54
|
+
}
|
|
55
|
+
})();
|