zcode-pro 0.20.3 → 1.1.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/README.en.md +9 -6
- package/README.md +8 -5
- package/dist/inject.js +2658 -1318
- package/package.json +1 -1
- package/src/host/helper.mjs +156 -60
- package/src/host/main.mjs +7 -4
- package/src/host/taskIndex.mjs +97 -0
- package/src/host/version.mjs +51 -0
- package/src/inject/core.js +177 -24
- package/src/inject/features/alias-migration.js +470 -0
- package/src/inject/features/alias.js +6 -1
- package/src/inject/features/idle-reclaim.js +23 -48
- package/src/inject/features/pinned-collapse.js +4 -3
- package/src/inject/features/project-menu.js +209 -19
- package/src/inject/features/sandbox-reclaim.js +121 -0
- package/src/inject/features/session-switch.js +164 -25
- package/src/inject/features/settings-dialog.js +225 -61
- package/src/inject/features/styles.js +8 -1
- package/src/inject/features/title-lock.js +20 -2
- package/src/inject/features/toolbar-icons.js +85 -45
- package/src/inject/features/ws-rows.js +62 -0
- package/src/inject/features/ws-running.js +391 -27
- package/src/inject/index.js +3 -2
- package/src/inject/ui.js +34 -11
- package/src/inject/features/pinned-expand.js +0 -142
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
// 会话快捷切换:alt+z 立即切到上次使用的会话(再按切回);按住 alt 按 x/c 打开
|
|
2
|
-
// MRU 弹窗前后移动高亮(x 更早 / c
|
|
3
|
-
//
|
|
2
|
+
// MRU 弹窗前后移动高亮(x 更早 / c 更新,c 到底且还有更多时加载 5 条并选中新
|
|
3
|
+
// 加载的第一条),alt+e 直接打开同一弹窗(初始显示最近 10 条),alt+r 打开
|
|
4
|
+
// “运行中 / 运行完未查看”任务弹窗,松开 alt 切换,Esc / 点弹窗外取消,点行即切。
|
|
5
|
+
// MRU(最近使用序)从注入起记录(不回填历史库,上限 50 条),
|
|
4
6
|
// 与侧栏展示顺序无关;当前会话靠侧栏条目的 bg-selected 类识别(实测标记),
|
|
5
7
|
// 事件驱动扫描(会话行增删/选中标记移动才触发)+ 低频兜底轮询——
|
|
6
8
|
// 点击、新建任务自动跳转等任何方式的切换都能捕捉,聊天区流式输出不产生扫描。
|
|
7
9
|
// 目标条目在折叠项目下时先展开项目行再点击(展开先例见 pinned-expand.js)。
|
|
8
|
-
import { h, t, getConfig } from '../core.js';
|
|
10
|
+
import { h, t, getConfig, DEFAULT_SHORTCUTS } from '../core.js';
|
|
9
11
|
import { ensureStyle, showToast } from '../ui.js';
|
|
12
|
+
import { findProjectRow } from './ws-rows.js';
|
|
13
|
+
import { wsRunningKeys } from './ws-running.js';
|
|
10
14
|
|
|
11
15
|
const MRU_LIMIT = 50;
|
|
12
16
|
const SCAN_DEBOUNCE_MS = 150; // 相关 DOM 变动后的去抖,合并连续变动
|
|
13
17
|
const SCAN_FALLBACK_MS = 5000; // 兜底轮询周期:仅补观察器覆盖不到的路径
|
|
18
|
+
const INITIAL_COUNT = 10; // 弹窗初始渲染条数(alt+e“最近 10 条”同此)
|
|
19
|
+
const PAGE_SIZE = 5; // c 到底再按时加载的条数
|
|
20
|
+
const UNSEEN_LIMIT = 50;
|
|
14
21
|
|
|
15
22
|
let mru = []; // 会话键(data-task-item-key),最近使用的在最前
|
|
16
23
|
let current = null; // 当前会话键
|
|
@@ -19,6 +26,24 @@ let popupEl = null; // 打开的切换弹窗(null = 未打开)
|
|
|
19
26
|
let aliasesCache = {}; // 项目路径 → 别名(打开弹窗时预取,renderPopup 同步使用)
|
|
20
27
|
let highlight = 0;
|
|
21
28
|
let scanDebounce = 0;
|
|
29
|
+
let popupMode = 'mru'; // 'mru':最近使用(x/c/e);'attention':运行中 + 未查看(r)
|
|
30
|
+
let visibleCount = INITIAL_COUNT;
|
|
31
|
+
let unseen = new Set(); // 运行结束以来未再切入查看的会话键
|
|
32
|
+
let lastRunKeys = null; // 上次采样的运行键集合(用于识别“运行→结束”的转变)
|
|
33
|
+
let movedSinceOpen = false; // 打开弹窗后是否移动过高亮(决定松开 alt 是提交还是保持)
|
|
34
|
+
// 键位(alt+字母/数字):默认见 core.js,可在设置 → 快捷键修改;
|
|
35
|
+
// 设置保存时会派发 zcodepro:config-changed,这里刷新缓存
|
|
36
|
+
let hotkeys = { ...DEFAULT_SHORTCUTS };
|
|
37
|
+
function loadHotkeys() {
|
|
38
|
+
return getConfig(true).then((cfg) => {
|
|
39
|
+
const sc = cfg && cfg.shortcuts;
|
|
40
|
+
if (sc && typeof sc === 'object') {
|
|
41
|
+
for (const k of Object.keys(DEFAULT_SHORTCUTS)) {
|
|
42
|
+
if (typeof sc[k] === 'string' && /^[a-z0-9]$/.test(sc[k])) hotkeys[k] = sc[k];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}).catch(() => {});
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
function firstLineOf(item) {
|
|
24
49
|
// 条目文本含相对时间等杂项,取首个截断类标题元素,兜底 innerText 首行
|
|
@@ -40,8 +65,9 @@ function findCurrent() {
|
|
|
40
65
|
return null;
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
// 记录一次“切到 key
|
|
68
|
+
// 记录一次“切到 key”;顺带刷新在场条目的标题缓存。切入即视为已查看
|
|
44
69
|
function record(key) {
|
|
70
|
+
if (key) unseen.delete(key);
|
|
45
71
|
if (key && key !== current) {
|
|
46
72
|
current = key;
|
|
47
73
|
mru = mru.filter((k) => k !== key);
|
|
@@ -93,7 +119,9 @@ async function switchTo(key) {
|
|
|
93
119
|
let item = find();
|
|
94
120
|
if (!item) {
|
|
95
121
|
const ws = workspaceOf(key);
|
|
96
|
-
|
|
122
|
+
// 项目行按路径定位:旧结构查 testid,新结构(3.16.1 起)按 data-project-id
|
|
123
|
+
// 分区匹配(分区与路径的关联需该项目本次会话展开过一次,见 ws-rows.js)
|
|
124
|
+
const header = findProjectRow(ws);
|
|
97
125
|
const expander = header && header.querySelector('[aria-expanded]');
|
|
98
126
|
if (expander && expander.getAttribute('aria-expanded') === 'false') {
|
|
99
127
|
expander.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
@@ -108,13 +136,35 @@ async function switchTo(key) {
|
|
|
108
136
|
|
|
109
137
|
// —— 切换弹窗(类 alt+tab):居中列表卡,x/c 移动高亮,松开 alt / 点击提交 ——
|
|
110
138
|
|
|
139
|
+
// attention 弹窗的列表:正在运行或运行完未查看的任务。MRU 只记录切入过的
|
|
140
|
+
// 会话,而“未查看”的任务可能从未切入,不在 MRU 里——这部分(先运行中、
|
|
141
|
+
// 再未查看,各按发现序)追加在 MRU 命中项之后
|
|
142
|
+
function attentionList() {
|
|
143
|
+
const run = wsRunningKeys();
|
|
144
|
+
const listed = new Set();
|
|
145
|
+
const head = [];
|
|
146
|
+
for (const k of mru) {
|
|
147
|
+
if (run.has(k) || unseen.has(k)) { head.push(k); listed.add(k); }
|
|
148
|
+
}
|
|
149
|
+
const tail = [];
|
|
150
|
+
for (const k of run) if (!listed.has(k)) { tail.push(k); listed.add(k); }
|
|
151
|
+
for (const k of unseen) if (!listed.has(k)) { tail.push(k); listed.add(k); }
|
|
152
|
+
return [...head, ...tail];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 弹窗当前列表:普通模式为 MRU 全量;attention 模式为运行中 + 未查看
|
|
156
|
+
function activeList() {
|
|
157
|
+
return popupMode === 'attention' ? attentionList() : mru;
|
|
158
|
+
}
|
|
159
|
+
|
|
111
160
|
function closePopup(commit) {
|
|
112
161
|
if (!popupEl) return;
|
|
113
162
|
const idx = highlight;
|
|
163
|
+
const list = activeList();
|
|
114
164
|
popupEl.remove();
|
|
115
165
|
popupEl = null;
|
|
116
|
-
if (commit &&
|
|
117
|
-
void switchTo(
|
|
166
|
+
if (commit && list[idx] && list[idx] !== current) {
|
|
167
|
+
void switchTo(list[idx]).then((ok) => { if (!ok) showToast(t().switcherFailed, 'error'); });
|
|
118
168
|
}
|
|
119
169
|
}
|
|
120
170
|
|
|
@@ -136,51 +186,94 @@ function renderPopup() {
|
|
|
136
186
|
if (!popupEl) return;
|
|
137
187
|
const L = t();
|
|
138
188
|
const list = popupEl.querySelector('[data-zcodepro-switcher-list]');
|
|
139
|
-
|
|
189
|
+
const full = activeList();
|
|
190
|
+
const shown = full.slice(0, visibleCount);
|
|
191
|
+
// attention 模式每行标状态:运行中(绿)/ 未查看(琥珀);普通模式不加
|
|
192
|
+
const run = popupMode === 'attention' ? wsRunningKeys() : null;
|
|
193
|
+
list.replaceChildren(...shown.map((key) => {
|
|
140
194
|
const proj = projLabelOf(key, aliasesCache);
|
|
141
195
|
return h('div', {
|
|
142
196
|
class: 'zcodepro-switcher-row',
|
|
143
|
-
'data-active':
|
|
144
|
-
onClick: () => { highlight =
|
|
197
|
+
'data-active': full[highlight] === key ? '' : undefined,
|
|
198
|
+
onClick: () => { highlight = full.indexOf(key); closePopup(true); },
|
|
145
199
|
},
|
|
146
200
|
// 标题文本包 span:截断省略作用在文本节点上(flex 容器自身 ellipsis 无效)
|
|
147
201
|
h('div', { class: 'zcodepro-switcher-title' },
|
|
148
|
-
|
|
202
|
+
key === current ? h('span', { class: 'zcodepro-switcher-tag' }, L.switcherCurrent) : null,
|
|
203
|
+
run && run.has(key) ? h('span', { class: 'zcodepro-switcher-tag zcodepro-switcher-tag-run' }, L.switcherRunning) : null,
|
|
204
|
+
run && !run.has(key) ? h('span', { class: 'zcodepro-switcher-tag zcodepro-switcher-tag-new' }, L.switcherUnseen) : null,
|
|
149
205
|
h('span', null, findTitle(key))),
|
|
150
206
|
// 无项目归属(如置顶)时不渲染空行
|
|
151
207
|
proj ? h('div', { class: 'zcodepro-switcher-ws' }, proj) : null);
|
|
152
208
|
}));
|
|
209
|
+
if (full.length > shown.length) {
|
|
210
|
+
list.append(h('div', { class: 'zcodepro-switcher-more' },
|
|
211
|
+
L.switcherMore.replaceAll('{n}', String(full.length - shown.length))));
|
|
212
|
+
}
|
|
153
213
|
}
|
|
154
214
|
|
|
155
|
-
async function openPopup() {
|
|
215
|
+
async function openPopup(mode = 'mru') {
|
|
156
216
|
if (popupEl) return;
|
|
217
|
+
popupMode = mode;
|
|
218
|
+
visibleCount = INITIAL_COUNT;
|
|
219
|
+
highlight = 0;
|
|
220
|
+
movedSinceOpen = false;
|
|
157
221
|
const cfg = await getConfig().catch(() => null);
|
|
158
222
|
aliasesCache = (cfg && cfg.aliases && typeof cfg.aliases === 'object') ? cfg.aliases : {};
|
|
159
223
|
ensureStyle();
|
|
160
224
|
const L = t();
|
|
161
|
-
highlight = 0;
|
|
162
225
|
popupEl = h('div', {
|
|
163
226
|
class: 'zcodepro-switcher-overlay',
|
|
164
227
|
// 点弹窗外 = 取消(不切)
|
|
165
228
|
onMousedown: (e) => { if (e.target === popupEl) closePopup(false); },
|
|
166
229
|
},
|
|
167
230
|
h('div', { class: 'zcodepro-switcher' },
|
|
168
|
-
|
|
231
|
+
// 提示行键位跟随当前配置(改键后新开的弹窗即用新键位)
|
|
232
|
+
h('div', { class: 'zcodepro-switcher-hint' },
|
|
233
|
+
(mode === 'attention' ? L.switcherAttentionHint : L.switcherHint)
|
|
234
|
+
.replaceAll('{prev}', hotkeys.switchPrev)
|
|
235
|
+
.replaceAll('{next}', hotkeys.switchNext)),
|
|
169
236
|
h('div', { 'data-zcodepro-switcher-list': '1', class: 'zcodepro-switcher-list' })));
|
|
170
237
|
document.body.append(popupEl);
|
|
171
238
|
renderPopup();
|
|
172
239
|
}
|
|
173
240
|
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
highlight = (highlight + delta + mru.length) % mru.length;
|
|
177
|
-
renderPopup();
|
|
178
|
-
const row = popupEl.querySelector('[data-active]');
|
|
241
|
+
function scrollHighlight() {
|
|
242
|
+
const row = popupEl && popupEl.querySelector('[data-active]');
|
|
179
243
|
if (row) row.scrollIntoView({ block: 'nearest' });
|
|
180
244
|
}
|
|
181
245
|
|
|
246
|
+
function moveUp() {
|
|
247
|
+
const len = Math.min(activeList().length, visibleCount);
|
|
248
|
+
if (len < 2) return;
|
|
249
|
+
highlight = highlight > 0 ? highlight - 1 : len - 1; // 到顶循环回已加载的末尾
|
|
250
|
+
movedSinceOpen = true;
|
|
251
|
+
renderPopup();
|
|
252
|
+
scrollHighlight();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function moveDown() {
|
|
256
|
+
const full = activeList();
|
|
257
|
+
const len = Math.min(full.length, visibleCount);
|
|
258
|
+
if (len < 2) return;
|
|
259
|
+
if (highlight < len - 1) {
|
|
260
|
+
highlight += 1;
|
|
261
|
+
} else if (visibleCount < full.length) {
|
|
262
|
+
// 到底且还有更多:加载 5 条,高亮落在新加载的第一条
|
|
263
|
+
visibleCount = Math.min(visibleCount + PAGE_SIZE, full.length);
|
|
264
|
+
highlight = len;
|
|
265
|
+
} else {
|
|
266
|
+
highlight = 0; // 全部加载完后再到底:循环回顶部
|
|
267
|
+
}
|
|
268
|
+
movedSinceOpen = true;
|
|
269
|
+
renderPopup();
|
|
270
|
+
scrollHighlight();
|
|
271
|
+
}
|
|
272
|
+
|
|
182
273
|
export function startSessionSwitch() {
|
|
183
274
|
scan();
|
|
275
|
+
void loadHotkeys();
|
|
276
|
+
window.addEventListener('zcodepro:config-changed', () => { void loadHotkeys(); });
|
|
184
277
|
// 事件驱动为主:任务条目相关变动(去抖 150ms)立即重扫
|
|
185
278
|
new MutationObserver((muts) => { if (taskRowMutations(muts)) scheduleScan(); })
|
|
186
279
|
.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
|
@@ -188,6 +281,21 @@ export function startSessionSwitch() {
|
|
|
188
281
|
setInterval(() => { if (!document.hidden) scan(); }, SCAN_FALLBACK_MS);
|
|
189
282
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) scan(); });
|
|
190
283
|
|
|
284
|
+
// “运行完未查看”跟踪:周期采样运行键集合(ws-running 维护的实时状态),
|
|
285
|
+
// 从运行集合消失(转终态)且结束时不是当前会话的记为未查看;
|
|
286
|
+
// 用户切入该会话(record)即清除。注入前已结束的任务无从判定,不追溯
|
|
287
|
+
setInterval(() => {
|
|
288
|
+
if (document.hidden) return;
|
|
289
|
+
const now = wsRunningKeys();
|
|
290
|
+
if (lastRunKeys) {
|
|
291
|
+
for (const k of lastRunKeys) {
|
|
292
|
+
if (!now.has(k) && k !== current) unseen.add(k);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
lastRunKeys = now;
|
|
296
|
+
if (unseen.size > UNSEEN_LIMIT) unseen = new Set([...unseen].slice(-UNSEEN_LIMIT));
|
|
297
|
+
}, 10_000);
|
|
298
|
+
|
|
191
299
|
// 点击兜底:bg-selected 标记不可用/未出现时,点条目(含本功能派发的合成点击)即记录
|
|
192
300
|
document.addEventListener('click', (e) => {
|
|
193
301
|
const item = e.target instanceof Element && e.target.closest('[data-task-item-key]');
|
|
@@ -202,17 +310,29 @@ export function startSessionSwitch() {
|
|
|
202
310
|
closePopup(false);
|
|
203
311
|
return;
|
|
204
312
|
}
|
|
313
|
+
// 弹窗开着时方向键 / Enter 也可操作(此时用户往往已松开 alt):
|
|
314
|
+
// ↑/↓ 与 x/c 同规则(下到底加载 5 条),Enter 提交高亮项
|
|
315
|
+
if (popupEl && (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Enter')) {
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
e.stopImmediatePropagation();
|
|
318
|
+
if (e.key === 'ArrowUp') moveUp();
|
|
319
|
+
else if (e.key === 'ArrowDown') moveDown();
|
|
320
|
+
else if (!e.repeat) closePopup(true);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
205
323
|
if (!e.altKey || e.ctrlKey || e.metaKey || e.isComposing) return;
|
|
206
324
|
// 我们的设置弹窗开着时不抢键(它有自己的 Esc/焦点逻辑)
|
|
207
325
|
if (document.querySelector('.zcodepro-overlay')) return;
|
|
208
326
|
const k = e.key.toLowerCase();
|
|
209
|
-
|
|
327
|
+
const isHot = k === hotkeys.switchLast || k === hotkeys.switchPrev || k === hotkeys.switchNext
|
|
328
|
+
|| k === hotkeys.switchRecent || k === hotkeys.switchAttention;
|
|
329
|
+
if (!isHot) return;
|
|
210
330
|
e.preventDefault();
|
|
211
331
|
e.stopImmediatePropagation();
|
|
212
332
|
void (async () => {
|
|
213
333
|
const cfg = await getConfig(); // 带 3s 缓存
|
|
214
334
|
if (cfg.features && cfg.features.sessionSwitch === false) return;
|
|
215
|
-
if (k ===
|
|
335
|
+
if (k === hotkeys.switchLast) {
|
|
216
336
|
if (e.repeat) return;
|
|
217
337
|
// 即时切换:关掉可能开着的弹窗,切到最近使用的会话(上次使用的会话)
|
|
218
338
|
closePopup(false);
|
|
@@ -228,15 +348,34 @@ export function startSessionSwitch() {
|
|
|
228
348
|
void switchTo(target).then((ok) => { if (!ok) showToast(t().switcherFailed, 'error'); });
|
|
229
349
|
return;
|
|
230
350
|
}
|
|
231
|
-
//
|
|
351
|
+
// r:运行中 / 运行完未查看的任务(attention 弹窗)
|
|
352
|
+
if (k === hotkeys.switchAttention) {
|
|
353
|
+
if (e.repeat) return;
|
|
354
|
+
closePopup(false);
|
|
355
|
+
if (!attentionList().length) {
|
|
356
|
+
showToast(t().switcherAttentionEmpty);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
await openPopup('attention');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// x/c/e:最近使用弹窗(无可导航会话时与 z 一致轻提示)
|
|
232
363
|
if (mru.length < 2) { showToast(t().switcherEmpty); return; }
|
|
233
|
-
|
|
234
|
-
|
|
364
|
+
// e 只负责打开(初始显示最近 10 条,c 可继续加载更早的)
|
|
365
|
+
if (k === hotkeys.switchRecent) {
|
|
366
|
+
if (!e.repeat && !popupEl) await openPopup('mru');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (!popupEl) await openPopup('mru');
|
|
370
|
+
// 键位对调:alt+x 上移一位(更靠上/更近的键管向上),alt+c 下移
|
|
371
|
+
if (k === hotkeys.switchPrev) moveUp(); else moveDown();
|
|
235
372
|
})();
|
|
236
373
|
}, true);
|
|
237
374
|
|
|
238
375
|
document.addEventListener('keyup', (e) => {
|
|
239
|
-
|
|
376
|
+
// 松开 alt:移动过高亮则提交(alt+tab 流);从未移动则保持弹窗打开,
|
|
377
|
+
// 让用户继续用方向键选择(alt+e / alt+r 打开后松手查看的场景)
|
|
378
|
+
if (e.key === 'Alt' && popupEl && movedSinceOpen) closePopup(true);
|
|
240
379
|
}, true);
|
|
241
380
|
|
|
242
381
|
// 窗口失焦(alt+tab 到别的应用等)视同取消
|