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.
@@ -1,9 +1,10 @@
1
1
  // “ZCode Pro 增强设置”弹窗:功能开关 + 样式调整 + 运行状态。
2
2
  // 顶部标签页切换(视觉参考侧栏“分组/项目”切换);配置写入 helper(~/.zcode/zcodepro.json)。
3
- import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale } 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';
7
+ import { migrationCard } from './alias-migration.js';
7
8
  import { applyStyles, STYLE_DEFAULTS, showStyleHighlight, hideStyleHighlight } from './styles.js';
8
9
  import { copyToClipboard, hCopyIcon } from './copy-path.js';
9
10
 
@@ -12,6 +13,9 @@ function openExternal(url) {
12
13
  try { void window.zcode.openExternal(url); } catch { /* ignore */ }
13
14
  }
14
15
 
16
+ // 插件市场仓库(设置页“自动更新”行的仓库名链接指向它)
17
+ const PLUGINS_REPO_URL = 'https://github.com/duanluan/zcode-plugins';
18
+
15
19
  // 本体升级交互(Headroom 的 pip 与 rtk 的 GitHub Releases 共用):
16
20
  // 检查更新 → 升级(helper 后台任务,轮询进度,输出取最后一行、超长省略中间)→
17
21
  // 进行中可停止;关弹窗不中断升级,重开恢复显示。endpoint 为 /headroom 或 /rtk,
@@ -236,10 +240,85 @@ export function openSettingsDialog() {
236
240
  );
237
241
 
238
242
  const rows = h('div', { class: 'divide-y divide-border rounded-xl border border-border' });
243
+ const pluginsStatusLine = h('span', { class: 'max-w-40 truncate text-ui-xs/relaxed text-foreground-subtle' }, '…');
244
+ const pluginsBtn = btnSmall(L.pluginsCheckNow, () => { void runPluginsUpdate(); });
245
+ let pluginsUpdating = false;
246
+ const runPluginsUpdate = async () => {
247
+ // 并发守卫:打开弹窗的自动触发与手动点击可能撞在一起,第二个直接忽略
248
+ if (pluginsUpdating) return;
249
+ pluginsUpdating = true;
250
+ pluginsBtn.disabled = true;
251
+ pluginsStatusLine.textContent = L.pluginsUpdating;
252
+ const res = await rpc('/plugins/update', { method: 'POST', body: {} });
253
+ pluginsUpdating = false;
254
+ pluginsBtn.disabled = false;
255
+ if (!res.ok) {
256
+ pluginsStatusLine.textContent = '';
257
+ showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
258
+ return;
259
+ }
260
+ pluginsStatusLine.textContent = res.updated > 0
261
+ ? L.pluginsUpdatedDone.replaceAll('{n}', String(res.updated))
262
+ : L.pluginsUpToDate;
263
+ showToast(pluginsStatusLine.textContent, 'success');
264
+ };
265
+ void (async () => {
266
+ // 打开设置弹窗即自动更新(与启动时同语义):开关开着且有新版时直接更,
267
+ // 状态行如实显示过程与结果;开关关闭时只检查并显示“N 个可更新”
268
+ const cfg = await getConfig(true).catch(() => null);
269
+ const res = await rpc('/plugins/status');
270
+ if (!res.ok) { pluginsStatusLine.textContent = ''; return; }
271
+ const n = (res.updates || []).length;
272
+ if (cfg && (cfg.features || {}).autoUpdatePlugins === true && n > 0) {
273
+ void runPluginsUpdate();
274
+ return;
275
+ }
276
+ pluginsStatusLine.textContent = n > 0
277
+ ? L.pluginsUpdatesFound.replaceAll('{n}', String(n))
278
+ : L.pluginsUpToDate;
279
+ })();
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
+
239
315
  const refreshRows = () => {
240
316
  const f = config.features || {};
241
317
  rows.replaceChildren(
242
- settingRow(L.featureProjectMenu, L.featureProjectMenuDesc, f.projectMenu !== false, async () => {
318
+ // 3.16.1 起官方“编辑项目”支持修改名称:别名停用,此行只描述其余三项;
319
+ // 仍有存量别名时在最上方放迁移卡片(卡片自管移除,这里每次重建)
320
+ ...(isNewProjectSidebar() ? [migrationCard()] : []),
321
+ settingRow(L.featureProjectMenu, isNewProjectSidebar() ? L.featureProjectMenuDescNew : L.featureProjectMenuDesc, f.projectMenu !== false, async () => {
243
322
  const next = !(f.projectMenu !== false);
244
323
  if (await setFeature('projectMenu', next)) f.projectMenu = next;
245
324
  refreshRows();
@@ -251,7 +330,7 @@ export function openSettingsDialog() {
251
330
  if (await setFeature('taskOrder', next)) f.taskOrder = next;
252
331
  refreshRows();
253
332
  }),
254
- settingRow(L.featureSessionSwitch, L.featureSessionSwitchDesc, f.sessionSwitch !== false, async () => {
333
+ settingRow(L.featureSessionSwitch, sessionSwitchDesc(), f.sessionSwitch !== false, async () => {
255
334
  const next = !(f.sessionSwitch !== false);
256
335
  if (await setFeature('sessionSwitch', next)) f.sessionSwitch = next;
257
336
  refreshRows();
@@ -281,16 +360,31 @@ export function openSettingsDialog() {
281
360
  if (await setFeature('wsRunningSpin', next)) f.wsRunningSpin = next;
282
361
  refreshRows();
283
362
  }),
284
- settingRow(L.featurePinnedExpand, L.featurePinnedExpandDesc, f.pinnedKeepCollapsed !== false, async () => {
285
- const next = !(f.pinnedKeepCollapsed !== false);
286
- if (await setFeature('pinnedKeepCollapsed', next)) f.pinnedKeepCollapsed = next;
287
- refreshRows();
288
- }),
289
- settingRow(L.featureAutoUpdatePlugins, L.featureAutoUpdatePluginsDesc, f.autoUpdatePlugins === true, async () => {
290
- const next = !(f.autoUpdatePlugins === true);
291
- if (await setFeature('autoUpdatePlugins', next)) f.autoUpdatePlugins = next;
292
- refreshRows();
293
- }),
363
+ (() => {
364
+ // 标题 = “自动更新 duanluan/zcode-plugins 插件”,仓库名可点击跳转仓库
365
+ const repoLink = h('span', {
366
+ class: 'cursor-pointer underline-offset-4 hover:underline',
367
+ title: PLUGINS_REPO_URL,
368
+ onClick: (ev) => { ev.stopPropagation(); openExternal(PLUGINS_REPO_URL); },
369
+ }, L.featureAutoUpdatePluginsRepo);
370
+ const row = settingRow('', '', f.autoUpdatePlugins === true, async () => {
371
+ const next = !(f.autoUpdatePlugins === true);
372
+ if (await setFeature('autoUpdatePlugins', next)) f.autoUpdatePlugins = next;
373
+ refreshRows();
374
+ }, h('div', { class: 'flex items-center gap-2' }, pluginsBtn));
375
+ // 标题行:自动更新 <链接> 插件;描述位放状态字(检查结果/更新进度)
376
+ const nameEl = row.querySelector('.text-ui-sm.font-medium');
377
+ if (nameEl) {
378
+ nameEl.replaceChildren(
379
+ document.createTextNode(L.featureAutoUpdatePlugins + ' '),
380
+ repoLink,
381
+ document.createTextNode(' 插件'),
382
+ );
383
+ }
384
+ const descEl = row.querySelector('[data-zcodepro-row-desc]');
385
+ if (descEl) descEl.replaceChildren(pluginsStatusLine);
386
+ return row;
387
+ })(),
294
388
  );
295
389
  };
296
390
  refreshRows();
@@ -333,56 +427,108 @@ export function openSettingsDialog() {
333
427
  let activeTab = 'features';
334
428
  // 插件更新卡片:显示 duanluan-zcode-plugins 市场状态,一键更新(helper /plugins/* 端点);
335
429
  // 状态异步加载,打开弹窗不等待
336
- const pluginsStatusLine = h('span', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, '…');
337
- const pluginsBtn = btnSmall(L.pluginsCheckNow, () => { void runPluginsUpdate(); });
338
- const runPluginsUpdate = async () => {
339
- pluginsBtn.disabled = true;
340
- pluginsStatusLine.textContent = L.pluginsUpdating;
341
- const res = await rpc('/plugins/update', { method: 'POST', body: {} });
342
- pluginsBtn.disabled = false;
343
- if (!res.ok) {
344
- pluginsStatusLine.textContent = '';
345
- showToast(L.pluginsUpdateFailed + ': ' + errText(res), 'error');
346
- return;
347
- }
348
- pluginsStatusLine.textContent = res.updated > 0
349
- ? L.pluginsUpdatedDone.replaceAll('{n}', String(res.updated))
350
- : L.pluginsUpToDate;
351
- showToast(pluginsStatusLine.textContent, 'success');
352
- };
353
- void (async () => {
354
- const res = await rpc('/plugins/status');
355
- if (!res.ok) { pluginsStatusLine.textContent = ''; return; }
356
- pluginsStatusLine.textContent = (res.updates || []).length > 0
357
- ? L.pluginsUpdatesFound.replaceAll('{n}', String(res.updates.length))
358
- : L.pluginsUpToDate;
359
- })();
360
- // 标题中的 zcode-plugins 可点击跳转插件市场仓库(依赖宿主 openExternal 打开系统浏览器)
361
- const PLUGINS_REPO_URL = 'https://github.com/duanluan/zcode-plugins';
362
- const repoName = 'zcode-plugins';
363
- const titleParts = L.pluginsUpdateTitle.split(repoName);
364
- const pluginsTitle = h('div', { class: 'truncate text-ui-sm font-medium text-foreground' });
365
- if (titleParts.length === 2 && typeof window.zcode?.openExternal === 'function') {
366
- pluginsTitle.append(titleParts[0],
367
- h('span', {
368
- class: 'cursor-pointer underline-offset-4 hover:underline',
369
- title: PLUGINS_REPO_URL,
370
- onClick: () => openExternal(PLUGINS_REPO_URL),
371
- }, repoName),
372
- titleParts[1]);
373
- } else {
374
- pluginsTitle.append(L.pluginsUpdateTitle);
375
- }
376
- const pluginsCard = h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
377
- h('div', { class: 'min-w-0' },
378
- pluginsTitle,
379
- h('div', { class: 'mt-0.5 truncate text-ui-xs/relaxed text-foreground-subtle' }, pluginsStatusLine)),
380
- pluginsBtn);
430
+
381
431
  const paneFeatures = h('div', { role: 'tabpanel', class: 'mt-4' },
382
432
  h('div', {}, rows),
383
- pluginsCard,
384
433
  communityCards,
385
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
+ );
386
532
  const paneStyles = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
387
533
  const panePerf = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
388
534
  const paneAgents = h('div', { role: 'tabpanel', class: 'mt-2', style: 'display:none' });
@@ -393,9 +539,10 @@ export function openSettingsDialog() {
393
539
  const paneHeadroom = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
394
540
  const paneTitleGen = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
395
541
  const paneDebug = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
396
- 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 };
397
543
  const tabDefs = [
398
544
  ['features', L.tabFeatures],
545
+ ['shortcuts', L.tabShortcuts],
399
546
  ['agents', L.tabBehavior],
400
547
  ['styles', L.tabStyles],
401
548
  ['perf', L.tabPerformance],
@@ -418,6 +565,8 @@ export function openSettingsDialog() {
418
565
  }, label)));
419
566
  const switchTab = (name) => {
420
567
  activeTab = name;
568
+ // 切走“快捷键”标签页时结束键位录制:否则在其它标签页打字会被录制监听吞掉
569
+ if (name !== 'shortcuts' && hkRecording) stopHkRecording();
421
570
  for (const [id, pane] of Object.entries(panes)) pane.style.display = id === name ? '' : 'none';
422
571
  renderTabs();
423
572
  };
@@ -497,6 +646,7 @@ export function openSettingsDialog() {
497
646
  cellPadH.el, h('span', { class: 'w-3 text-ui-xs text-foreground-subtle' }, 'px'))),
498
647
  };
499
648
  const cells = [
649
+ // 侧栏间距双结构:3.15.x 旧容器/行 + 3.16.1 新项目行(virtual-project-row)与任务行
500
650
  styleCell(L.sidebarProjectSpacingName, L.sidebarProjectSpacingDesc, 'sidebarProjectSpacing', { max: 24 }),
501
651
  styleCell(L.sidebarTaskSpacingName, L.sidebarTaskSpacingDesc, 'sidebarTaskSpacing', { max: 24 }),
502
652
  widthCell,
@@ -1853,6 +2003,7 @@ export function openSettingsDialog() {
1853
2003
  const hrStatusLine = h('div', { class: 'flex items-center gap-2' },
1854
2004
  h('span', { class: 'text-ui-sm text-foreground-subtle/70' }, '…'));
1855
2005
  const hrFacts = h('div', { class: 'mt-1.5 grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-3' });
2006
+ const hrInstList = h('div', { class: 'mt-1.5 space-y-1', style: 'display:none' });
1856
2007
  const hrHintLine = h('p', { class: 'mt-1.5 text-ui-xs/relaxed text-amber-500', style: 'display:none' });
1857
2008
  const hrRefresh = async () => {
1858
2009
  const res = await rpc('/headroom/status');
@@ -1860,6 +2011,8 @@ export function openSettingsDialog() {
1860
2011
  hrStatusLine.replaceChildren(
1861
2012
  h('span', { class: 'text-ui-sm text-destructive' }, L.headroomStatusLoadFailed + ': ' + errText(res)));
1862
2013
  hrFacts.replaceChildren();
2014
+ hrInstList.replaceChildren();
2015
+ hrInstList.style.display = 'none';
1863
2016
  hrHintLine.style.display = 'none';
1864
2017
  return;
1865
2018
  }
@@ -1876,9 +2029,18 @@ export function openSettingsDialog() {
1876
2029
  fact(L.headroomCurrentBackend, hrDevLabel(s.backend)),
1877
2030
  fact(L.headroomDesiredBackend, hrDevLabel(s.desiredBackend)),
1878
2031
  fact(L.headroomPowerState, HR_PM[s.powerMode] || s.powerMode),
1879
- fact(L.headroomWatcher, s.watcherRunning ? L.headroomWatcherOn : L.headroomWatcherOff),
2032
+ fact(L.headroomWatcher, s.watcherRunning == null ? '—' : (s.watcherRunning ? L.headroomWatcherOn : L.headroomWatcherOff)),
1880
2033
  fact(L.headroomSaverDetect, HR_DETECT[s.saverDetect] || s.saverDetect),
1881
2034
  );
2035
+ // 多实例模式:列出各压缩实例(端口/格式/上游/供应商)与运行状态
2036
+ const insts = Array.isArray(s.instances) ? s.instances : [];
2037
+ hrInstList.replaceChildren(...insts.map((it) => h('div', { class: 'flex min-w-0 items-center gap-2' },
2038
+ h('span', { class: 'shrink-0 font-mono text-ui-xs text-foreground-subtle' }, `:${it.port}`),
2039
+ h('span', { class: 'min-w-0 flex-1 truncate text-ui-xs text-foreground-subtle', title: `${it.upstream} | ${it.providers}` },
2040
+ `${it.format} → ${it.upstream}`),
2041
+ h('span', { class: 'max-w-[40%] shrink-0 truncate text-ui-xs text-foreground-subtle/70', title: it.providers }, it.providers),
2042
+ h('span', { class: 'shrink-0 text-ui-xs ' + (it.up ? 'text-emerald-600' : 'text-destructive') }, it.up ? L.hrInstRunning : L.hrInstStopped))));
2043
+ hrInstList.style.display = insts.length ? '' : 'none';
1882
2044
  hrHintLine.textContent = s.saverHint || '';
1883
2045
  hrHintLine.style.display = s.saverHint ? '' : 'none';
1884
2046
  };
@@ -2052,6 +2214,7 @@ export function openSettingsDialog() {
2052
2214
  h('div', { class: 'flex items-center gap-1.5' }, hrRefreshBtn, hrStartBtn, hrRestartBtn, hrStopBtn)),
2053
2215
  hrStatusLine,
2054
2216
  hrFacts,
2217
+ hrInstList,
2055
2218
  hrHintLine),
2056
2219
  h('div', {
2057
2220
  class: 'mt-2 rounded-xl border border-border p-1',
@@ -2227,6 +2390,7 @@ export function openSettingsDialog() {
2227
2390
  statusLine,
2228
2391
  tablist,
2229
2392
  paneFeatures,
2393
+ paneShortcuts,
2230
2394
  paneAgents,
2231
2395
  paneStyles,
2232
2396
  panePerf,
@@ -214,6 +214,9 @@ function buildCss(styles) {
214
214
  `${LIST} [data-testid^="workspace-item-"] > div:first-child{align-self:stretch !important;}`,
215
215
  `${SCROLL} [data-slot="collapsible-trigger"],${SCROLL} .flex.h-7{height:auto !important;}`,
216
216
  `${SCROLL} [data-slot="collapsible-trigger"]{min-height:${28 + 2 * pad}px !important;padding-block:${pad}px !important;padding-top:${pad}px !important;padding-bottom:${pad}px !important;}`,
217
+ // 3.16.1 新结构:项目行(virtual-project-row,固定 h-8)按视觉总量放行内留白;
218
+ // 分区间距由新侧栏容器自己的布局管理,不动
219
+ `section[data-project-id] [data-testid="virtual-project-row"]{height:auto !important;min-height:${28 + 2 * pad}px !important;padding-block:${Math.round(pad)}px !important;}`,
217
220
  );
218
221
  }
219
222
  const sts = styles.sidebarTaskSpacing;
@@ -228,6 +231,9 @@ function buildCss(styles) {
228
231
  `ul.space-y-0\\.5:has(> li[data-task-item-key]) > li{padding-block:${pad}px !important;padding-top:${pad}px !important;padding-bottom:${pad}px !important;}`,
229
232
  `${SCROLL} .flex.flex-col.gap-2{gap:${m}px !important;row-gap:${m}px !important;}`,
230
233
  `${SCROLL} div.cursor-pointer[class*="pl-8"]{padding-block:${pad}px !important;padding-top:${pad}px !important;padding-bottom:${pad}px !important;}`,
234
+ // 3.16.1 新结构:任务行(li[data-task-item-key])行内留白 + 相邻行间距
235
+ `li[data-task-item-key]{padding-block:${Math.max(1, Math.round(pad))}px !important;}`,
236
+ `li[data-task-item-key] + li[data-task-item-key]{margin-block-start:${m}px !important;}`,
231
237
  );
232
238
  }
233
239
  return parts.join('');
@@ -275,7 +281,8 @@ const HL_SELECTORS = {
275
281
  thinkingLineHeight: `[data-testid^="chat-assistant-history-content"]`,
276
282
  userLineHeight: `${CONV} [class*="user-row"] .whitespace-pre-wrap`,
277
283
  contentWidth: '[data-v4-timeline-content-column]',
278
- sidebarProjectSpacing: '[data-testid^="workspace-item-"]',
284
+ // 双结构:新侧栏项目行 / 旧 testid 行
285
+ sidebarProjectSpacing: 'section[data-project-id] [data-testid="virtual-project-row"], [data-testid^="workspace-item-"]',
279
286
  sidebarTaskSpacing: 'li[data-task-item-key]',
280
287
  };
281
288
 
@@ -28,6 +28,7 @@ const REVERIFY_MS = 3000; // 回写后的再复核:纠正数据库写
28
28
  const MAP_REFRESH_MS = 60000; // 任务行变动后至多一次的延迟表刷新:让新重命名会话进入锁定范围
29
29
 
30
30
  let installed = false;
31
+ const seenRows = new WeakSet(); // 已见过的任务行;新上屏的行立即钉回(不等复核)
31
32
  let enabled = true; // titleLock 开关(/pinned-titles 响应带来)
32
33
  let pinned = null; // Map(task_id → 重命名标题);null = 尚未取到/功能关闭
33
34
  let resolved = false; // 首次成功通信(无论开关状态)后不再重试启动拉取
@@ -100,7 +101,19 @@ function writeTitle(el, text) {
100
101
  }
101
102
 
102
103
  // 失配 → 排期复核;到期后行可能已被 React 重挂载,按键重新定位
103
- function scheduleEnforce(key) {
104
+ function scheduleEnforce(key, { immediate = false } = {}) {
105
+ if (immediate) {
106
+ // 新上屏的行(项目展开/滚动复现)带着旧名出现,用户此刻正看着列表——
107
+ // 立即用缓存表钉回,不等复核(编辑中的行不会到这:isMismatch 已排除 input)
108
+ if (enabled && pinned) {
109
+ const want = pinned.get(sessIdOf(key));
110
+ if (want !== undefined) {
111
+ for (const item of findRows(key)) {
112
+ for (const el of titleElsOf(item)) writeTitle(el, want);
113
+ }
114
+ }
115
+ }
116
+ }
104
117
  if (pendingKeys.has(key)) return;
105
118
  pendingKeys.add(key);
106
119
  setTimeout(() => {
@@ -138,7 +151,12 @@ function scan() {
138
151
  if (!enabled || !pinned || pinned.size === 0) return;
139
152
  for (const item of document.querySelectorAll(TASK_SEL)) {
140
153
  const key = isMismatch(item);
141
- if (key) scheduleEnforce(key);
154
+ if (!key) { seenRows.add(item); continue; }
155
+ // 新上屏的失配行立即钉回(项目展开时旧名闪现 1 秒+,用户不知道点哪个);
156
+ // 已在屏上的失配走原路径(复核后写,避免把刚提交的重命名改回旧名)
157
+ const fresh = !seenRows.has(item);
158
+ seenRows.add(item);
159
+ scheduleEnforce(key, { immediate: fresh });
142
160
  }
143
161
  }
144
162