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.
@@ -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, L.featureSessionSwitchDesc, f.sessionSwitch !== false, async () => {
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);