zcode-pro 1.0.0 → 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 +4 -4
- package/README.md +4 -4
- package/dist/inject.js +1540 -1173
- package/package.json +1 -1
- package/src/host/helper.mjs +10 -1
- package/src/inject/core.js +80 -10
- package/src/inject/features/sandbox-reclaim.js +121 -0
- package/src/inject/features/session-switch.js +159 -24
- package/src/inject/features/settings-dialog.js +138 -3
- package/src/inject/features/ws-running.js +7 -0
- package/src/inject/ui.js +14 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// “ZCode Pro 增强设置”弹窗:功能开关 + 样式调整 + 运行状态。
|
|
2
2
|
// 顶部标签页切换(视觉参考侧栏“分组/项目”切换);配置写入 helper(~/.zcode/zcodepro.json)。
|
|
3
|
-
import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale, isNewProjectSidebar } from '../core.js';
|
|
3
|
+
import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale, isNewProjectSidebar, DEFAULT_SHORTCUTS } from '../core.js';
|
|
4
4
|
import { openDialog, dialogFooter, btnPrimary, btnSecondary, btnSmall, settingRow, ensureStyle, showToast, numberField, unitField } from '../ui.js';
|
|
5
5
|
import { comboField, providerOptionsOf, modelOptionsOf, modelPlaceholderOf, isManualConn } from '../provider-fields.js';
|
|
6
6
|
import { refreshAliases } from './alias.js';
|
|
@@ -278,6 +278,40 @@ export function openSettingsDialog() {
|
|
|
278
278
|
: L.pluginsUpToDate;
|
|
279
279
|
})();
|
|
280
280
|
|
|
281
|
+
// 会话快捷切换:入口键位(“快捷键”标签页可改);此处定义在 refreshRows 之前,
|
|
282
|
+
// 功能行的描述与快捷键面板共用同一份当前键位
|
|
283
|
+
const HK_ACTIONS = [
|
|
284
|
+
['switchLast', 'hkAltZ'],
|
|
285
|
+
['switchPrev', 'hkAltX'],
|
|
286
|
+
['switchNext', 'hkAltC'],
|
|
287
|
+
['switchRecent', 'hkAltE'],
|
|
288
|
+
['switchAttention', 'hkAltR'],
|
|
289
|
+
];
|
|
290
|
+
const hkNames = () => ({
|
|
291
|
+
switchLast: L.hkNameLast, switchPrev: L.hkNamePrev, switchNext: L.hkNameNext,
|
|
292
|
+
switchRecent: L.hkNameRecent, switchAttention: L.hkNameAttention,
|
|
293
|
+
});
|
|
294
|
+
// 当前生效键位:配置里合法的单字符覆盖默认,其余回退默认
|
|
295
|
+
const hkKeys = () => {
|
|
296
|
+
const sc = config.shortcuts || {};
|
|
297
|
+
const keys = {};
|
|
298
|
+
for (const [a] of HK_ACTIONS) {
|
|
299
|
+
keys[a] = /^[a-z0-9]$/.test(sc[a]) ? sc[a] : DEFAULT_SHORTCUTS[a];
|
|
300
|
+
}
|
|
301
|
+
return keys;
|
|
302
|
+
};
|
|
303
|
+
// 开关描述按当前键位渲染(改键后 refreshRows 重绘即更新);
|
|
304
|
+
// prev/next 出现在“按住 alt 再按 {prev}/{next}”语境,用裸键位避免 alt 重复
|
|
305
|
+
const sessionSwitchDesc = () => {
|
|
306
|
+
const k = hkKeys();
|
|
307
|
+
return L.featureSessionSwitchDesc
|
|
308
|
+
.replaceAll('{last}', 'alt+' + k.switchLast)
|
|
309
|
+
.replaceAll('{prev}', k.switchPrev)
|
|
310
|
+
.replaceAll('{next}', k.switchNext)
|
|
311
|
+
.replaceAll('{recent}', 'alt+' + k.switchRecent)
|
|
312
|
+
.replaceAll('{attention}', 'alt+' + k.switchAttention);
|
|
313
|
+
};
|
|
314
|
+
|
|
281
315
|
const refreshRows = () => {
|
|
282
316
|
const f = config.features || {};
|
|
283
317
|
rows.replaceChildren(
|
|
@@ -296,7 +330,7 @@ export function openSettingsDialog() {
|
|
|
296
330
|
if (await setFeature('taskOrder', next)) f.taskOrder = next;
|
|
297
331
|
refreshRows();
|
|
298
332
|
}),
|
|
299
|
-
settingRow(L.featureSessionSwitch,
|
|
333
|
+
settingRow(L.featureSessionSwitch, sessionSwitchDesc(), f.sessionSwitch !== false, async () => {
|
|
300
334
|
const next = !(f.sessionSwitch !== false);
|
|
301
335
|
if (await setFeature('sessionSwitch', next)) f.sessionSwitch = next;
|
|
302
336
|
refreshRows();
|
|
@@ -398,6 +432,103 @@ export function openSettingsDialog() {
|
|
|
398
432
|
h('div', {}, rows),
|
|
399
433
|
communityCards,
|
|
400
434
|
);
|
|
435
|
+
// “快捷键”:五个入口键可改(点“修改”后按下新键,字母/数字,Esc 取消),
|
|
436
|
+
// 弹窗内通用键(↑/↓、Enter、Esc、松开 alt)固定只读;开关仍在“功能”标签页。
|
|
437
|
+
// 键位与名称定义(HK_ACTIONS/hkNames/hkKeys)在 refreshRows 前共用
|
|
438
|
+
let hkRecording = null; // 正在录制新键的动作(null = 未在录制)
|
|
439
|
+
let hkKeyHandler = null;
|
|
440
|
+
const hkCard = h('div', { class: 'divide-y divide-border rounded-xl border border-border' });
|
|
441
|
+
const hkCell = (keyLabel, desc, extra) => h('div', { class: 'flex items-start gap-3 px-3 py-2.5' },
|
|
442
|
+
h('kbd', {
|
|
443
|
+
class: 'shrink-0 min-w-20 rounded-md border border-border px-2 py-0.5 text-center text-ui-xs text-foreground-subtle',
|
|
444
|
+
style: 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; line-height: 1.5;',
|
|
445
|
+
}, keyLabel),
|
|
446
|
+
h('div', { class: 'min-w-0 flex-1 text-ui-sm text-foreground-subtle' }, desc),
|
|
447
|
+
extra ? h('div', { class: 'ml-auto shrink-0 pl-3' }, extra) : null);
|
|
448
|
+
const renderShortcuts = () => {
|
|
449
|
+
const keys = hkKeys();
|
|
450
|
+
const names = hkNames();
|
|
451
|
+
hkCard.replaceChildren(
|
|
452
|
+
...HK_ACTIONS.map(([a, descKey]) => {
|
|
453
|
+
const recording = hkRecording === a;
|
|
454
|
+
return hkCell(
|
|
455
|
+
recording ? L.hkRecording : 'alt + ' + keys[a],
|
|
456
|
+
L[descKey],
|
|
457
|
+
btnSmall(recording ? L.hkRecording : L.hkModify, () => { recording ? stopHkRecording() : startHkRecording(a); }));
|
|
458
|
+
}),
|
|
459
|
+
hkCell('↑ / ↓', L.hkArrow.replaceAll('{prev}', keys.switchPrev).replaceAll('{next}', keys.switchNext)),
|
|
460
|
+
hkCell('Enter', L.hkEnter),
|
|
461
|
+
hkCell('Esc', L.hkEsc),
|
|
462
|
+
hkCell(L.hkReleaseKey, L.hkRelease));
|
|
463
|
+
};
|
|
464
|
+
const stopHkRecording = () => {
|
|
465
|
+
hkRecording = null;
|
|
466
|
+
if (hkKeyHandler) {
|
|
467
|
+
document.removeEventListener('keydown', hkKeyHandler, true);
|
|
468
|
+
hkKeyHandler = null;
|
|
469
|
+
}
|
|
470
|
+
renderShortcuts();
|
|
471
|
+
};
|
|
472
|
+
const startHkRecording = (action) => {
|
|
473
|
+
hkRecording = action;
|
|
474
|
+
if (!hkKeyHandler) {
|
|
475
|
+
// 绑定本次弹窗实例:弹窗关闭(哪怕马上重开新弹窗)后旧监听器自摘,
|
|
476
|
+
// 不会吞掉新弹窗里的按键
|
|
477
|
+
const myOverlay = document.querySelector('.zcodepro-overlay');
|
|
478
|
+
hkKeyHandler = (e) => {
|
|
479
|
+
if (!myOverlay || !myOverlay.isConnected) { stopHkRecording(); return; }
|
|
480
|
+
if (e.isComposing) return;
|
|
481
|
+
e.preventDefault();
|
|
482
|
+
e.stopImmediatePropagation();
|
|
483
|
+
if (e.key === 'Escape') { stopHkRecording(); return; }
|
|
484
|
+
const k = e.key.toLowerCase();
|
|
485
|
+
if (!/^[a-z0-9]$/.test(k)) { showToast(L.hkInvalid, 'error'); return; }
|
|
486
|
+
const keys = hkKeys();
|
|
487
|
+
const names = hkNames();
|
|
488
|
+
const conflict = HK_ACTIONS.find(([a]) => a !== hkRecording && keys[a] === k);
|
|
489
|
+
if (conflict) {
|
|
490
|
+
showToast(L.hkConflict.replaceAll('{k}', k).replaceAll('{name}', names[conflict[0]]), 'error');
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const a = hkRecording;
|
|
494
|
+
stopHkRecording();
|
|
495
|
+
void (async () => {
|
|
496
|
+
const res = await rpc('/config', { method: 'POST', body: { shortcuts: { [a]: k } } });
|
|
497
|
+
clearConfigCache();
|
|
498
|
+
window.dispatchEvent(new CustomEvent('zcodepro:config-changed'));
|
|
499
|
+
if (!res.ok) { showToast(L.failed + ': ' + errText(res), 'error'); renderShortcuts(); return; }
|
|
500
|
+
if (res.config && res.config.shortcuts) config.shortcuts = res.config.shortcuts;
|
|
501
|
+
showToast(L.hkSaved, 'success');
|
|
502
|
+
refreshRows(); // 功能标签页的开关描述跟随新键位
|
|
503
|
+
renderShortcuts();
|
|
504
|
+
})();
|
|
505
|
+
};
|
|
506
|
+
document.addEventListener('keydown', hkKeyHandler, true);
|
|
507
|
+
}
|
|
508
|
+
renderShortcuts();
|
|
509
|
+
};
|
|
510
|
+
renderShortcuts();
|
|
511
|
+
// 重置为默认:五个入口键一次性写回默认值,保存路径与单个改键一致
|
|
512
|
+
const resetShortcuts = async () => {
|
|
513
|
+
stopHkRecording(); // 录制进行中点重置:先结束录制再写回默认
|
|
514
|
+
const all = {};
|
|
515
|
+
for (const [a] of HK_ACTIONS) all[a] = DEFAULT_SHORTCUTS[a];
|
|
516
|
+
const res = await rpc('/config', { method: 'POST', body: { shortcuts: all } });
|
|
517
|
+
clearConfigCache();
|
|
518
|
+
window.dispatchEvent(new CustomEvent('zcodepro:config-changed'));
|
|
519
|
+
if (!res.ok) { showToast(L.failed + ': ' + errText(res), 'error'); return; }
|
|
520
|
+
if (res.config && res.config.shortcuts) config.shortcuts = res.config.shortcuts;
|
|
521
|
+
showToast(L.hkResetDone, 'success');
|
|
522
|
+
refreshRows(); // 功能标签页的开关描述跟随默认键位
|
|
523
|
+
renderShortcuts();
|
|
524
|
+
};
|
|
525
|
+
const paneShortcuts = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' },
|
|
526
|
+
h('div', { class: 'mb-2 text-ui-sm font-medium' }, L.hkTitle),
|
|
527
|
+
h('div', { class: 'mb-2 flex items-center gap-2' },
|
|
528
|
+
h('div', { class: 'min-w-0 flex-1 text-ui-xs/relaxed text-foreground-subtle' }, L.hkEditHint),
|
|
529
|
+
h('div', { class: 'shrink-0' }, btnSmall(L.hkReset, () => { void resetShortcuts(); }))),
|
|
530
|
+
hkCard,
|
|
531
|
+
);
|
|
401
532
|
const paneStyles = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
402
533
|
const panePerf = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
403
534
|
const paneAgents = h('div', { role: 'tabpanel', class: 'mt-2', style: 'display:none' });
|
|
@@ -408,9 +539,10 @@ export function openSettingsDialog() {
|
|
|
408
539
|
const paneHeadroom = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
409
540
|
const paneTitleGen = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
410
541
|
const paneDebug = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
|
|
411
|
-
const panes = { features: paneFeatures, styles: paneStyles, perf: panePerf, agents: paneAgents, proxy: paneProxy, sync: paneSync, vision: paneVision, headroom: paneHeadroom, rtk: paneRtk, debug: paneDebug };
|
|
542
|
+
const panes = { features: paneFeatures, shortcuts: paneShortcuts, styles: paneStyles, perf: panePerf, agents: paneAgents, proxy: paneProxy, sync: paneSync, vision: paneVision, headroom: paneHeadroom, rtk: paneRtk, debug: paneDebug };
|
|
412
543
|
const tabDefs = [
|
|
413
544
|
['features', L.tabFeatures],
|
|
545
|
+
['shortcuts', L.tabShortcuts],
|
|
414
546
|
['agents', L.tabBehavior],
|
|
415
547
|
['styles', L.tabStyles],
|
|
416
548
|
['perf', L.tabPerformance],
|
|
@@ -433,6 +565,8 @@ export function openSettingsDialog() {
|
|
|
433
565
|
}, label)));
|
|
434
566
|
const switchTab = (name) => {
|
|
435
567
|
activeTab = name;
|
|
568
|
+
// 切走“快捷键”标签页时结束键位录制:否则在其它标签页打字会被录制监听吞掉
|
|
569
|
+
if (name !== 'shortcuts' && hkRecording) stopHkRecording();
|
|
436
570
|
for (const [id, pane] of Object.entries(panes)) pane.style.display = id === name ? '' : 'none';
|
|
437
571
|
renderTabs();
|
|
438
572
|
};
|
|
@@ -2256,6 +2390,7 @@ export function openSettingsDialog() {
|
|
|
2256
2390
|
statusLine,
|
|
2257
2391
|
tablist,
|
|
2258
2392
|
paneFeatures,
|
|
2393
|
+
paneShortcuts,
|
|
2259
2394
|
paneAgents,
|
|
2260
2395
|
paneStyles,
|
|
2261
2396
|
panePerf,
|
|
@@ -640,6 +640,13 @@ export function wsRunningSnapshot() {
|
|
|
640
640
|
};
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
+
// 当前所有运行中会话键的集合(会话切换的 alt+r 以此筛“正在运行”的任务)
|
|
644
|
+
export function wsRunningKeys() {
|
|
645
|
+
const keys = new Set();
|
|
646
|
+
for (const set of runningByWs.values()) for (const k of set) keys.add(k);
|
|
647
|
+
return keys;
|
|
648
|
+
}
|
|
649
|
+
|
|
643
650
|
// 无感复核指定工作区当前是否仍有运行中会话(展开读行后原样收起)。
|
|
644
651
|
// 返回 true = 确认无运行;false = 有运行、窗口隐藏或行不可达(一律视为忙,调用方不得回收)
|
|
645
652
|
export async function verifyWorkspaceNotRunning(ws) {
|
package/src/inject/ui.js
CHANGED
|
@@ -616,6 +616,20 @@ export function ensureStyle() {
|
|
|
616
616
|
background-color: color-mix(in oklab, var(--color-primary, #111) 12%, transparent);
|
|
617
617
|
color: var(--color-foreground-subtle, #666);
|
|
618
618
|
}
|
|
619
|
+
/* attention 弹窗(alt+r)的行状态标签:运行中绿、未查看琥珀 */
|
|
620
|
+
.zcodepro-switcher-tag-run {
|
|
621
|
+
background-color: color-mix(in oklab, var(--color-emerald-500, #10b981) 20%, transparent);
|
|
622
|
+
}
|
|
623
|
+
.zcodepro-switcher-tag-new {
|
|
624
|
+
background-color: color-mix(in oklab, var(--color-amber-500, #f59e0b) 22%, transparent);
|
|
625
|
+
}
|
|
626
|
+
/* 列表底部“还有 N 条”提示行 */
|
|
627
|
+
.zcodepro-switcher-more {
|
|
628
|
+
padding: 6px 12px 2px;
|
|
629
|
+
font-size: 11px;
|
|
630
|
+
text-align: center;
|
|
631
|
+
color: var(--color-muted-foreground, #888);
|
|
632
|
+
}
|
|
619
633
|
.zcodepro-switcher-ws {
|
|
620
634
|
font-size: 11px;
|
|
621
635
|
color: var(--color-muted-foreground, #888);
|