zcode-pro 0.18.0 → 0.20.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.
@@ -2,6 +2,7 @@
2
2
  // 顶部标签页切换(视觉参考侧栏“分组/项目”切换);配置写入 helper(~/.zcode/zcodepro.json)。
3
3
  import { h, t, rpc, getConfig, clearConfigCache, errText, HELPER_URL, isZhLocale } from '../core.js';
4
4
  import { openDialog, dialogFooter, btnPrimary, btnSecondary, btnSmall, settingRow, ensureStyle, showToast, numberField, unitField } from '../ui.js';
5
+ import { comboField, providerOptionsOf, modelOptionsOf, modelPlaceholderOf, isManualConn } from '../provider-fields.js';
5
6
  import { refreshAliases } from './alias.js';
6
7
  import { applyStyles, STYLE_DEFAULTS, showStyleHighlight, hideStyleHighlight } from './styles.js';
7
8
  import { copyToClipboard, hCopyIcon } from './copy-path.js';
@@ -24,7 +25,7 @@ function makeUpgradeControls({ endpoint, metaRefresh, idleText }) {
24
25
  const hasIdle = () => typeof idleText === 'function';
25
26
  const setState = (text, kind = '') => {
26
27
  const shown = text || (hasIdle() ? (idleText() || '') : '');
27
- // 「 · 」前缀:与前面的版本行无缝拼成「插件 x · rtk y · 状态/路径」一行;
28
+ // “ · ”前缀:与前面的版本行直接拼成“插件 x · rtk y · 状态/路径”一行;
28
29
  // 错误信息不加前缀——它是独立提示,拼在版本行后会读成版本的一部分
29
30
  state.textContent = shown ? (kind === 'error' ? shown : ` · ${shown}`) : '';
30
31
  state.className = 'min-w-0 flex-1 truncate text-left text-ui-xs/relaxed '
@@ -149,8 +150,10 @@ export function openSettingsDialog() {
149
150
  openDialog({
150
151
  title: L.settingsTitle,
151
152
  // 宽度类必须用宿主样式表已有的工具类(注入的类名不会生成 CSS):
152
- // sm:max-w-3xl 不存在会失去约束拉满全屏;max-w-2xl=42rem 存在且尺寸合适
153
- width: 'max-w-2xl',
153
+ // sm:max-w-3xl 不存在会失去约束拉满全屏;max-w-3xl=48rem 存在——英文界面
154
+ // 标签栏较长,42rem 会挤出一块空白并触发横向滚动
155
+ width: 'max-w-3xl',
156
+ bodyClass: 'mt-1',
154
157
  overlay: 'none',
155
158
  draggable: true,
156
159
  posKey: 'settings',
@@ -202,7 +205,11 @@ export function openSettingsDialog() {
202
205
  const config = await getConfig(true);
203
206
  const agentsRes = await rpc('/agents');
204
207
  const visionRes = await rpc('/vision');
205
- const visionProvidersRes = await rpc('/vision/providers');
208
+ const titleGenRes = await rpc('/title-gen');
209
+ const titleGenProvidersRes = await rpc('/title-gen/providers');
210
+ // 供应商表(含各供应商可选模型):视觉代理的模型下拉也用这份
211
+ const titleGenProviders = titleGenProvidersRes.ok && Array.isArray(titleGenProvidersRes.providers)
212
+ ? titleGenProvidersRes.providers : [];
206
213
  const rtkRes = await rpc('/rtk');
207
214
  const headroomRes = await rpc('/headroom');
208
215
 
@@ -225,7 +232,7 @@ export function openSettingsDialog() {
225
232
  'data-zcodepro-status': '1',
226
233
  },
227
234
  h('span', { class: 'inline-block size-2 rounded-full bg-emerald-500' }),
228
- h('span', { class: 'text-foreground-subtle/70' }, `${L.version} ${health.version} · ${HELPER_URL}`)
235
+ h('span', { class: 'text-foreground-subtle/70' }, `${health.version} · ${HELPER_URL}`)
229
236
  );
230
237
 
231
238
  const rows = h('div', { class: 'divide-y divide-border rounded-xl border border-border' });
@@ -378,19 +385,20 @@ export function openSettingsDialog() {
378
385
  );
379
386
  const paneStyles = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
380
387
  const panePerf = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
381
- const paneAgents = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
388
+ const paneAgents = h('div', { role: 'tabpanel', class: 'mt-2', style: 'display:none' });
382
389
  const paneProxy = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
383
390
  const paneSync = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
384
391
  const paneVision = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
385
392
  const paneRtk = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
386
393
  const paneHeadroom = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
394
+ const paneTitleGen = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
387
395
  const paneDebug = h('div', { role: 'tabpanel', class: 'mt-4', style: 'display:none' });
388
396
  const panes = { features: paneFeatures, styles: paneStyles, perf: panePerf, agents: paneAgents, proxy: paneProxy, sync: paneSync, vision: paneVision, headroom: paneHeadroom, rtk: paneRtk, debug: paneDebug };
389
397
  const tabDefs = [
390
398
  ['features', L.tabFeatures],
399
+ ['agents', L.tabBehavior],
391
400
  ['styles', L.tabStyles],
392
401
  ['perf', L.tabPerformance],
393
- ['agents', L.tabAgents],
394
402
  ['proxy', L.tabProxy],
395
403
  ['sync', L.tabSync],
396
404
  ['vision', L.tabVision],
@@ -400,7 +408,7 @@ export function openSettingsDialog() {
400
408
  // 普通用户不可见
401
409
  ...(config.devMode === true || config.devActive === true ? [['debug', L.tabDebug]] : []),
402
410
  ];
403
- const tablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist mt-4' });
411
+ const tablist = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist mt-1' });
404
412
  const renderTabs = () => tablist.replaceChildren(...tabDefs.map(([id, label]) =>
405
413
  h('button', {
406
414
  type: 'button', role: 'tab', class: 'zcodepro-tab',
@@ -581,7 +589,7 @@ export function openSettingsDialog() {
581
589
  // “全局提示词”:编辑 ~/.zcode/AGENTS.md(helper /agents 端点读写)。
582
590
  // 大段文本不做即存即生效——显式保存;未修改时保存按钮禁用
583
591
  const agentsArea = h('textarea', {
584
- class: 'zcodepro-textarea',
592
+ class: 'zcodepro-textarea zcodepro-textarea-lg',
585
593
  placeholder: L.agentsPlaceholder,
586
594
  spellcheck: 'false',
587
595
  });
@@ -610,7 +618,11 @@ export function openSettingsDialog() {
610
618
  agentsSaveBtn.disabled = false;
611
619
  }
612
620
  };
613
- paneAgents.append(
621
+ // “行为”标签页:二级标签切换“全局提示词 / 任务名”。全局提示词编辑
622
+ // ~/.zcode/AGENTS.md;任务名(智能起名)面板复用 paneTitleGen(原本是
623
+ // 一级标签页,收进来后不再单独出现在顶部标签栏)
624
+ const agentsPromptPane = h('div', { class: 'mt-4' });
625
+ agentsPromptPane.append(
614
626
  h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.agentsDesc),
615
627
  ...(agentsFailed
616
628
  ? [h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.agentsLoadFailed + ': ' + errText(agentsRes))]
@@ -618,6 +630,27 @@ export function openSettingsDialog() {
618
630
  h('div', { class: 'mt-3' }, agentsArea),
619
631
  h('div', { class: 'mt-3 flex justify-end' }, agentsSaveBtn),
620
632
  );
633
+ let agentsSubTab = 'prompt';
634
+ const agentsSubPanes = { prompt: agentsPromptPane, titleGen: paneTitleGen };
635
+ const agentsSubList = h('div', { role: 'tablist', 'aria-orientation': 'horizontal', class: 'zcodepro-tablist' });
636
+ const renderAgentsSubTabs = () => {
637
+ agentsSubList.replaceChildren(...[
638
+ ['prompt', L.tabAgents],
639
+ ['titleGen', L.tabTitleGen],
640
+ ].map(([id, label]) => h('button', {
641
+ type: 'button', role: 'tab', class: 'zcodepro-tab',
642
+ 'aria-selected': String(agentsSubTab === id),
643
+ 'data-state': agentsSubTab === id ? 'active' : 'inactive',
644
+ onClick: () => {
645
+ agentsSubTab = id;
646
+ for (const [pid, pane] of Object.entries(agentsSubPanes)) pane.style.display = pid === id ? '' : 'none';
647
+ renderAgentsSubTabs();
648
+ },
649
+ }, label)));
650
+ };
651
+ for (const [pid, pane] of Object.entries(agentsSubPanes)) pane.style.display = pid === agentsSubTab ? '' : 'none';
652
+ renderAgentsSubTabs();
653
+ paneAgents.append(agentsSubList, agentsPromptPane, paneTitleGen);
621
654
 
622
655
  // “代理”:为 helper 发起的网络访问(插件市场更新/安装、headroom 本体升级)设
623
656
  // HTTP 代理;保存即生效(helper 子进程环境变量注入),不影响 ZCode 应用与模型请求。
@@ -648,7 +681,7 @@ export function openSettingsDialog() {
648
681
  const proxyTestBtn = btnSmall(L.proxyTest, () => { void runProxyTest(); }, 'shrink-0');
649
682
  const runProxyTest = async () => {
650
683
  if (proxyTesting) return;
651
- // 空地址不发请求(等网络超时才报「检测失败」的体验太差),立即提示
684
+ // 空地址不发请求(等网络超时才报“检测失败”的体验太差),立即提示
652
685
  if (!proxyInput.value.trim()) {
653
686
  proxyTestState.className = 'mt-2 text-ui-xs/relaxed text-amber-500';
654
687
  proxyTestState.textContent = L.proxyTestEmpty;
@@ -766,19 +799,37 @@ export function openSettingsDialog() {
766
799
  // “同步”:把 ZCode Pro 设置与模型设置手动备份到 WebDAV / 从 WebDAV 恢复
767
800
  // (交互参考 Tampermonkey:URL/登录/密码 + 手动触发,无自动同步)。仅 WebDAV
768
801
  // 一种后端且本就只在点击时传输,不设“类型/禁用”下拉。表单显式保存(旁有
769
- // 「测试」用当前表单值验证连通);同步内容开关即时保存;下载先弹云端版本
802
+ // “测试”用当前表单值验证连通);同步内容开关即时保存;下载先弹云端版本
770
803
  // 列表(固定名 = 最新 + 带时间戳的历史),任选一个导入,导入会覆盖本机、
771
804
  // 先弹二次确认
772
805
  const savedSync = (config.sync && typeof config.sync === 'object') ? config.sync
773
806
  : { url: '', dir: 'zcode-pro', login: '', password: '', include: { zcodepro: true, model: true }, lastPushAt: null, lastPullAt: null };
774
- const syncInputCls = 'h-8 w-full rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
807
+ // 宽度分开拼:定宽控件(历史版本数字框)不能带 w-full——与 w-20 同用时
808
+ // w-full 在样式表里靠后、会胜出,把整行撑爆
809
+ const syncInputBase = 'h-8 rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
810
+ const syncInputCls = syncInputBase + ' w-full';
811
+ const syncLabel = (text) => h('span', { class: 'w-20 shrink-0 text-ui-sm font-medium text-foreground' }, text);
775
812
  const syncFormRow = (labelText, node) => h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
776
- h('span', { class: 'w-20 shrink-0 text-ui-sm font-medium text-foreground' }, labelText), node);
813
+ syncLabel(labelText), node);
777
814
  const syncUrlInput = h('input', { type: 'text', value: savedSync.url || '', placeholder: L.syncUrlPlaceholder, spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
778
815
  // 上传目录:拼在 URL 之后的文件夹(默认 zcode-pro,避免直接写进网盘根目录),可多级
779
816
  const syncDirInput = h('input', { type: 'text', value: savedSync.dir || 'zcode-pro', placeholder: L.syncDirPlaceholder, spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
780
817
  const syncLoginInput = h('input', { type: 'text', value: savedSync.login || '', spellcheck: 'false', autocomplete: 'off', class: syncInputCls + ' min-w-0 flex-1' });
781
818
  const syncPasswordInput = h('input', { type: 'password', value: savedSync.password || '', autocomplete: 'new-password', class: syncInputCls + ' min-w-0 flex-1' });
819
+ // 历史版本保留个数:上传成功后自动删除云端更早的历史版本(固定名最新版不受
820
+ // 影响);0 = 不留历史版本。非法输入按默认 10 提交(服务端还会再校验)
821
+ const syncHistoryInput = h('input', {
822
+ type: 'number', min: '0', max: '100', step: '1',
823
+ value: Number.isInteger(savedSync.historyLimit) ? savedSync.historyLimit : 10,
824
+ title: L.syncHistoryLimitHint, class: syncInputBase + ' w-20 shrink-0 text-center',
825
+ });
826
+ const syncHistoryValue = () => {
827
+ // 清空按默认 10 提交,不当成 0——0 会清掉云端全部历史版,误清空代价太大
828
+ const s = syncHistoryInput.value.trim();
829
+ if (!s) return 10;
830
+ const n = Number(s);
831
+ return Number.isFinite(n) ? Math.min(100, Math.max(0, Math.round(n))) : 10;
832
+ };
782
833
  let syncFormOriginal = null;
783
834
  const syncSaveBtn = btnSmall(L.syncSave, () => { void saveSyncForm(); }, 'shrink-0', 'primary');
784
835
  const syncFormValues = () => ({
@@ -786,15 +837,17 @@ export function openSettingsDialog() {
786
837
  dir: syncDirInput.value.trim(),
787
838
  login: syncLoginInput.value.trim(),
788
839
  password: syncPasswordInput.value,
840
+ historyLimit: syncHistoryValue(),
789
841
  });
790
842
  const syncCheckDirty = () => {
791
843
  const c = syncFormValues();
792
844
  syncSaveBtn.disabled = c.url === syncFormOriginal.url && c.dir === syncFormOriginal.dir
793
- && c.login === syncFormOriginal.login && c.password === syncFormOriginal.password;
845
+ && c.login === syncFormOriginal.login && c.password === syncFormOriginal.password
846
+ && c.historyLimit === syncFormOriginal.historyLimit;
794
847
  };
795
848
  syncFormOriginal = syncFormValues();
796
849
  syncSaveBtn.disabled = true;
797
- for (const el of [syncUrlInput, syncDirInput, syncLoginInput, syncPasswordInput]) {
850
+ for (const el of [syncUrlInput, syncDirInput, syncLoginInput, syncPasswordInput, syncHistoryInput]) {
798
851
  el.addEventListener('input', syncCheckDirty);
799
852
  }
800
853
  const saveSyncForm = async () => {
@@ -805,18 +858,20 @@ export function openSettingsDialog() {
805
858
  url: res.sync.url,
806
859
  dir: res.sync.dir,
807
860
  login: res.sync.login, password: res.sync.password,
861
+ historyLimit: Number.isInteger(res.sync.historyLimit) ? res.sync.historyLimit : 10,
808
862
  };
809
863
  syncUrlInput.value = res.sync.url;
810
864
  syncDirInput.value = res.sync.dir;
811
865
  syncLoginInput.value = res.sync.login;
812
866
  syncPasswordInput.value = res.sync.password;
867
+ syncHistoryInput.value = syncFormOriginal.historyLimit;
813
868
  showToast(L.syncSaved);
814
869
  } else {
815
870
  showToast(L.failed + ': ' + errText(res), 'error');
816
871
  syncCheckDirty();
817
872
  }
818
873
  };
819
- // 「测试」:用表单当前值(先测再存)验证 WebDAV:连接与凭据 → 上传目录
874
+ // “测试”:用表单当前值(先测再存)验证 WebDAV:连接与凭据 → 上传目录
820
875
  // (缺则自动建,等同上传时的行为)→ 文件列表;结果仿“代理”标签页的检测,
821
876
  // 一行显示各步耗时与云端备份文件数
822
877
  const syncTestState = h('p', { class: 'mt-2 text-ui-xs/relaxed text-foreground-subtle' });
@@ -900,7 +955,7 @@ export function openSettingsDialog() {
900
955
  renderSyncStatus(savedSync);
901
956
  const syncUploadBtn = btnSmall(L.syncUpload, () => { void runSyncPush(); }, 'shrink-0', 'primary');
902
957
  const syncDownloadBtn = btnSmall(L.syncDownload, () => { openSyncVersionPicker(); }, 'shrink-0');
903
- // 「从云端下载」先弹版本列表(备份管理器的样子):列出云端目录里的备份文件
958
+ // “从云端下载”先弹版本列表(备份管理器的样子):列出云端目录里的备份文件
904
959
  // (固定名 = 最新一版 + 带时间戳的历史),每行可导入到本机 / 从云端删除 /
905
960
  // 另存到本机磁盘;导入按文件名前缀决定覆盖哪一项,不受“同步内容”勾选影响
906
961
  const openSyncVersionPicker = () => {
@@ -1083,16 +1138,21 @@ export function openSettingsDialog() {
1083
1138
  return;
1084
1139
  }
1085
1140
  renderSyncStatus(res.sync || {});
1086
- showToast(L.syncUploaded, 'success');
1141
+ showToast(res.pruned ? L.syncUploaded + (isZhLocale() ? ';' : '; ') + L.syncPruned.replace('{n}', res.pruned) : L.syncUploaded, 'success');
1087
1142
  };
1088
1143
  paneSync.append(
1089
1144
  h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.syncDesc),
1090
1145
  h('div', { class: 'mt-3 rounded-xl border border-border p-1' },
1091
1146
  syncFormRow('URL', syncUrlInput),
1092
- syncFormRow(L.syncDir, syncDirInput),
1093
- syncFormRow(L.syncLogin, syncLoginInput),
1094
- syncFormRow(L.syncPassword, syncPasswordInput),
1095
- h('div', { class: 'flex items-start justify-between gap-2 px-2 py-1.5' },
1147
+ // 登录与密码一行、上传目录与历史版本一行:短字段成对排,少占纵向空间
1148
+ h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
1149
+ syncLabel(L.syncLogin), syncLoginInput,
1150
+ syncLabel(L.syncPassword), syncPasswordInput),
1151
+ h('div', { class: 'flex items-center gap-3 px-2 py-1.5' },
1152
+ syncLabel(L.syncDir), syncDirInput,
1153
+ syncLabel(L.syncHistoryLimit), syncHistoryInput,
1154
+ h('span', { class: 'shrink-0 text-ui-xs text-foreground-subtle' }, L.syncHistoryLimitUnit)),
1155
+ h('div', { class: 'flex items-center justify-between gap-2 px-2 py-1.5' },
1096
1156
  h('div', { class: 'min-w-0' },
1097
1157
  h('p', { class: 'text-ui-xs/relaxed text-foreground-subtle' }, L.syncFormHint),
1098
1158
  syncRecLine),
@@ -1168,8 +1228,6 @@ export function openSettingsDialog() {
1168
1228
  skipMinutes: 30,
1169
1229
  forceIntercept: true,
1170
1230
  };
1171
- // 供应商下拉数据:合并 zcode 两张供应商表(config.json 的 provider 与 provider_config.json 的 providerRules)
1172
- const visionProviders = visionProvidersRes.ok && Array.isArray(visionProvidersRes.providers) ? visionProvidersRes.providers : [];
1173
1231
  let visionCfg = visionRes.ok && visionRes.config && typeof visionRes.config === 'object'
1174
1232
  ? structuredClone(visionRes.config)
1175
1233
  : null;
@@ -1226,7 +1284,7 @@ export function openSettingsDialog() {
1226
1284
  }
1227
1285
  // 插件未装优先显示安装卡——判断只看钩子(visionRes.hook),与配置文件是否存在无关:
1228
1286
  // 配置是钩子消费的,没钩子就没功能;老用户卸载插件后 ~/.zcode/zcode-vision.json 仍在,
1229
- // 若按配置存在与否分流会误入完整配置界面而毫无「未安装」提示
1287
+ // 若按配置存在与否分流会误入完整配置界面而毫无“未安装”提示
1230
1288
  if (!visionRes.hook) {
1231
1289
  paneVision.replaceChildren(
1232
1290
  h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.visionDesc),
@@ -1283,6 +1341,8 @@ export function openSettingsDialog() {
1283
1341
  return h('label', { class: 'flex shrink-0 items-center gap-1.5', title },
1284
1342
  h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, label), n);
1285
1343
  };
1344
+ // 接口地址 / API Key 是否显示走 provider-fields 的共用规则
1345
+ //(isManualConn:不跟随或供应商已删才显示,连接其余由跟随对象负责)
1286
1346
  const cards = visionCfg.proxies.map((p, i) => h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
1287
1347
  h('div', { class: 'flex flex-wrap items-center gap-2' },
1288
1348
  h('span', { class: 'shrink-0 rounded-md bg-surface px-1.5 py-0.5 text-ui-xs tabular-nums text-foreground-subtle' }, String(i + 1)),
@@ -1313,43 +1373,35 @@ export function openSettingsDialog() {
1313
1373
  smallBtn(L.visionRemove, () => { visionCfg.proxies.splice(i, 1); void persistVision(); renderVision(); }),
1314
1374
  ),
1315
1375
  h('div', { class: 'mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2' },
1316
- field(L.visionUseProvider, (() => {
1317
- // 组合框:下拉(不跟随/session/全部供应商)+ 手动输入;手输失焦校验,未知供应商给提示
1318
- const wrap = h('div', { class: 'flex flex-col gap-1' });
1319
- const input = h('input', { type: 'text', value: p.useProvider || '', placeholder: L.visionUseProviderHint, class: visionInputCls });
1320
- const warn = h('p', { class: 'text-ui-xs text-destructive', style: 'display:none' }, L.visionUseProviderUnknown);
1321
- const isKnown = (v) => !v || v === 'session'
1322
- || visionProviders.some((pr) => pr.id === v || pr.name === v || (pr.aliases || []).includes(v));
1323
- const sel = h('select', { class: visionInputCls });
1324
- const syncSelect = () => {
1325
- const cur = (p.useProvider || '').trim();
1326
- const opts = [h('option', { value: '' }, L.visionUseProviderNone), h('option', { value: 'session' }, L.visionUseProviderSession)];
1327
- for (const pr of visionProviders) {
1328
- opts.push(h('option', { value: pr.id }, pr.name && pr.name !== pr.id ? `${pr.name}(${pr.id})` : pr.id));
1329
- }
1330
- if (cur && !opts.some((o) => o.value === cur)) opts.push(h('option', { value: cur }, `${cur}(${L.visionUseProvider})`));
1331
- sel.replaceChildren(...opts);
1332
- sel.value = cur;
1333
- };
1334
- sel.addEventListener('change', () => {
1335
- input.value = sel.value;
1336
- p.useProvider = sel.value.trim();
1337
- warn.style.display = 'none';
1376
+ // 供应商与模型走 provider-fields 共用逻辑:跟随会话时模型下拉为
1377
+ // 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注“已删除”
1378
+ field(L.visionUseProvider, comboField(p.useProvider || '',
1379
+ providerOptionsOf(titleGenProviders, L),
1380
+ (v, src) => {
1381
+ p.useProvider = v;
1338
1382
  saveVisionSoon();
1339
- });
1340
- input.addEventListener('input', () => { p.useProvider = input.value.trim(); saveVisionSoon(); });
1341
- input.addEventListener('blur', () => {
1342
- p.useProvider = input.value.trim();
1343
- warn.style.display = isKnown(p.useProvider) ? 'none' : '';
1344
- syncSelect();
1345
- });
1346
- syncSelect();
1347
- wrap.append(sel, input, warn);
1348
- return wrap;
1349
- })()),
1350
- field(L.visionModel, textIn(p.model, (v) => { p.model = v; saveVisionSoon(); })),
1351
- field(L.visionBaseUrl, textIn(p.baseUrl, (v) => { p.baseUrl = v; saveVisionSoon(); })),
1352
- field(L.visionApiKey + ' · ' + L.visionApiKeyHint, textIn(p.apiKey, (v) => { p.apiKey = v; saveVisionSoon(); }, { type: 'password' })),
1383
+ // 下拉切换后重渲染整个面板:模型下拉选项随新供应商变化,接口
1384
+ // 地址/API Key 字段按“不跟随或供应商已删才显示”重新显隐
1385
+ if (src === 'select') renderVision();
1386
+ },
1387
+ { placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted })),
1388
+ // 跟随会话时模型下拉头部有“跟随当前会话当前模型”(值为空,
1389
+ // 插件 ≥1.6.1 在调用时解析为会话当前模型);具体供应商为其模型列表
1390
+ field(L.visionModel, comboField(p.model || '',
1391
+ modelOptionsOf(p.useProvider, titleGenProviders, {
1392
+ followSession: (p.useProvider || '').trim() === 'session',
1393
+ followSessionLabel: L.titleGenModelFollowSession,
1394
+ }),
1395
+ (v) => { p.model = v; saveVisionSoon(); },
1396
+ {
1397
+ placeholder: modelPlaceholderOf(p.useProvider, L),
1398
+ emptyLabel: (p.useProvider || '').trim() === 'session'
1399
+ ? L.titleGenModelFollowSession : L.titleGenModelEmpty,
1400
+ })),
1401
+ ...(isManualConn(p.useProvider, titleGenProviders) ? [
1402
+ field(L.visionBaseUrl, textIn(p.baseUrl, (v) => { p.baseUrl = v; saveVisionSoon(); })),
1403
+ field(L.visionApiKey + ' · ' + L.visionApiKeyHint, textIn(p.apiKey, (v) => { p.apiKey = v; saveVisionSoon(); }, { type: 'password' })),
1404
+ ] : []),
1353
1405
  ),
1354
1406
  (() => {
1355
1407
  const n = h('textarea', { class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '3', placeholder: L.visionPrompt });
@@ -2011,13 +2063,164 @@ export function openSettingsDialog() {
2011
2063
  };
2012
2064
  renderHeadroomPane();
2013
2065
 
2066
+ // “任务名提示词”(位于“行为”标签页的二级标签):新会话开始作答时 →
2067
+ // 按自定义供应商/提示词生成标题并按手动重命名写库。配置读写走 /title-gen
2068
+ //(存 zcodepro.json);供应商下拉数据走 /title-gen/providers(与视觉代理
2069
+ // 同源,但只列 helper 能直连调用的条目)。
2070
+ // 结构性改动(开关/供应商/协议)即时保存,提示词与文本输入防抖保存。
2071
+ let titleGenCfg = titleGenRes.ok && titleGenRes.titleGen && typeof titleGenRes.titleGen === 'object'
2072
+ ? structuredClone(titleGenRes.titleGen) : null;
2073
+ const titleGenDefaultPrompt = titleGenRes.ok && typeof titleGenRes.defaultPrompt === 'string'
2074
+ ? titleGenRes.defaultPrompt : '';
2075
+ let titleGenSaveTimer = null;
2076
+ const persistTitleGen = async () => {
2077
+ const res = await rpc('/title-gen', { method: 'POST', body: { titleGen: titleGenCfg } });
2078
+ clearConfigCache();
2079
+ if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
2080
+ return res.ok;
2081
+ };
2082
+ const saveTitleGenSoon = () => {
2083
+ clearTimeout(titleGenSaveTimer);
2084
+ titleGenSaveTimer = setTimeout(() => { void persistTitleGen(); }, 500);
2085
+ };
2086
+ const renderTitleGenPane = () => {
2087
+ if (!titleGenCfg) {
2088
+ paneTitleGen.replaceChildren(
2089
+ h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.titleGenShortDesc),
2090
+ h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(titleGenRes)),
2091
+ );
2092
+ return;
2093
+ }
2094
+ const inputCls = 'h-8 w-full rounded-lg border border-border bg-input px-2.5 text-ui-sm text-foreground outline-none transition-shadow placeholder:text-foreground-subtle focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40';
2095
+ const field = (labelText, node) => h('label', { class: 'block min-w-0' },
2096
+ h('span', { class: 'mb-1 block text-ui-xs font-medium text-foreground-subtle' }, labelText), node);
2097
+ // 供应商 / 模型 / 地址密钥共用 provider-fields 的规则(详见该模块说明)。
2098
+ // 换供应商后重渲染:模型下拉换成新供应商的模型、地址字段按模式显隐;
2099
+ // 手动输入过的模型保留(不在新列表里时显示为“手动输入”项,是否改选
2100
+ // 由用户决定)
2101
+ const showInlineConn = isManualConn(titleGenCfg.provider, titleGenProviders);
2102
+ const providerCombo = comboField(
2103
+ titleGenCfg.provider || '',
2104
+ providerOptionsOf(titleGenProviders, L),
2105
+ (v, src) => {
2106
+ titleGenCfg.provider = v;
2107
+ if (src === 'select') { void persistTitleGen(); renderTitleGenPane(); }
2108
+ else saveTitleGenSoon();
2109
+ },
2110
+ { placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted });
2111
+ const modelCombo = comboField(
2112
+ titleGenCfg.model || '',
2113
+ // 跟随会话时选项表头部有“跟随当前会话当前模型”(值为空,生成时用
2114
+ // 会话当前模型),选了具体模型后也能再切回它
2115
+ modelOptionsOf(titleGenCfg.provider, titleGenProviders, {
2116
+ followSession: true,
2117
+ followSessionLabel: L.titleGenModelFollowSession,
2118
+ }),
2119
+ (v) => { titleGenCfg.model = v; saveTitleGenSoon(); },
2120
+ {
2121
+ placeholder: modelPlaceholderOf(titleGenCfg.provider, L),
2122
+ emptyLabel: (titleGenCfg.provider || '').trim() === 'session'
2123
+ ? L.titleGenModelFollowSession : L.titleGenModelEmpty,
2124
+ });
2125
+ // 接口地址 / API Key / 协议(显隐见上方 showInlineConn):不跟随时由
2126
+ // 用户填写;供应商被删时展示保存的副本,仍可直连或改填
2127
+ const textIn = (value, onInput, { type = 'text', placeholder = '' } = {}) => {
2128
+ const n = h('input', { type, value: value || '', placeholder, class: inputCls });
2129
+ n.addEventListener('input', () => onInput(n.value));
2130
+ return n;
2131
+ };
2132
+ const formatSel = h('select', { class: inputCls },
2133
+ h('option', { value: 'anthropic' }, 'anthropic'),
2134
+ h('option', { value: 'openai' }, 'openai'));
2135
+ formatSel.value = titleGenCfg.format === 'openai' ? 'openai' : 'anthropic';
2136
+ formatSel.addEventListener('change', () => {
2137
+ titleGenCfg.format = formatSel.value;
2138
+ void persistTitleGen();
2139
+ });
2140
+ const inlineConnCard = h('div', {
2141
+ class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-3',
2142
+ style: showInlineConn ? '' : 'display:none',
2143
+ },
2144
+ field(L.titleGenBaseUrl, textIn(titleGenCfg.baseUrl, (v) => { titleGenCfg.baseUrl = v; saveTitleGenSoon(); }, { placeholder: 'https://…' })),
2145
+ field(L.titleGenApiKey, textIn(titleGenCfg.apiKey, (v) => { titleGenCfg.apiKey = v; saveTitleGenSoon(); }, { type: 'password' })),
2146
+ field(L.titleGenFormat, formatSel));
2147
+ // 提示词:留空 = 内置默认;“查看默认提示词”把默认值载入编辑框供修改
2148
+ const promptArea = h('textarea', {
2149
+ class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '8',
2150
+ placeholder: L.titleGenPromptPlaceholder,
2151
+ });
2152
+ promptArea.value = titleGenCfg.prompt || '';
2153
+ promptArea.addEventListener('input', () => { titleGenCfg.prompt = promptArea.value; saveTitleGenSoon(); });
2154
+ // 测试:用示例首条消息实际生成一次(不写库),验证供应商/模型/提示词
2155
+ // 连通;示例文本随界面语言取中英文版
2156
+ const sampleText = isZhLocale()
2157
+ ? '帮我看下这个报错是什么原因:NullPointerException at com.example.OrderService.calculate(OrderService.java:87)'
2158
+ : 'Why does my Spring Boot app throw NullPointerException intermittently in OrderService.calculate?';
2159
+ const sampleInput = h('input', {
2160
+ type: 'text', class: inputCls + ' min-w-0 flex-1',
2161
+ value: sampleText,
2162
+ });
2163
+ let testBtn;
2164
+ const testState = h('span', { class: 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-foreground-subtle' });
2165
+ testBtn = btnSmall(L.titleGenTest, async () => {
2166
+ testBtn.disabled = true;
2167
+ testBtn.textContent = L.titleGenTesting;
2168
+ testState.textContent = L.titleGenTesting;
2169
+ const res = await rpc('/title-gen/run', {
2170
+ method: 'POST',
2171
+ body: { text: sampleInput.value, test: true },
2172
+ });
2173
+ testBtn.disabled = false;
2174
+ testBtn.textContent = L.titleGenTest;
2175
+ if (res && res.ok) {
2176
+ testState.textContent = L.titleGenTestOk + res.title;
2177
+ showToast(res.title, 'success');
2178
+ } else {
2179
+ testState.textContent = (res && errText(res)) || L.titleGenTestFailed;
2180
+ testState.className = 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-destructive';
2181
+ showToast(L.titleGenTestFailed + ': ' + (res ? errText(res) : ''), 'error');
2182
+ }
2183
+ }, 'shrink-0');
2184
+ paneTitleGen.replaceChildren(
2185
+ // 信息卡:一句话简介在左,启用开关右对齐
2186
+ h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
2187
+ h('span', { class: 'min-w-0 text-ui-sm/relaxed text-foreground-subtle' }, L.titleGenShortDesc),
2188
+ h('div', { class: 'shrink-0' }, settingRow(L.titleGenEnabled, '', titleGenCfg.enabled === true, async () => {
2189
+ titleGenCfg.enabled = !(titleGenCfg.enabled === true);
2190
+ await persistTitleGen();
2191
+ renderTitleGenPane();
2192
+ }))),
2193
+ // 供应商 + 模型
2194
+ h('div', { class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-2' },
2195
+ field(L.titleGenProvider, providerCombo),
2196
+ field(L.titleGenModel, modelCombo)),
2197
+ inlineConnCard,
2198
+ // 提示词 + 载入默认
2199
+ h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
2200
+ h('div', { class: 'mb-1 flex items-center justify-between gap-2' },
2201
+ h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.titleGenPrompt),
2202
+ btnSmall(L.titleGenPromptReset, () => {
2203
+ promptArea.value = titleGenDefaultPrompt;
2204
+ titleGenCfg.prompt = titleGenDefaultPrompt;
2205
+ void persistTitleGen();
2206
+ promptArea.focus();
2207
+ })),
2208
+ promptArea),
2209
+ // 测试
2210
+ h('div', { class: 'mt-2 flex items-center gap-2 rounded-xl border border-border p-2' },
2211
+ sampleInput, testBtn),
2212
+ testState,
2213
+ );
2214
+ };
2215
+ renderTitleGenPane();
2216
+
2014
2217
  body.append(
2015
2218
  statusLine,
2016
2219
  tablist,
2017
2220
  paneFeatures,
2221
+ paneAgents,
2018
2222
  paneStyles,
2019
2223
  panePerf,
2020
- paneAgents,
2021
2224
  paneProxy,
2022
2225
  paneSync,
2023
2226
  paneVision,
@@ -140,7 +140,7 @@ function buildCss(styles) {
140
140
  }
141
141
  const tlh = styles.thinkingLineHeight;
142
142
  if (typeof tlh === 'number' && Number.isFinite(tlh) && tlh >= 0.8) {
143
- // 「思考行高」管“已工作/工作中 N 秒”过程视图的行距(读取/编辑/查阅/终端等
143
+ // “思考行高”管“已工作/工作中 N 秒”过程视图的行距(读取/编辑/查阅/终端等
144
144
  // 条目行、流式思考预览,默认 1.5):行高挂在历史内容容器上由各行继承;
145
145
  // 过程里的叙述正文走 .space-y-4(回答行高,选择器更具体压过这里的继承)。
146
146
  // 单独展开的“思考·持续了 N 秒”块内内容不在此设置范围内
@@ -29,7 +29,7 @@ export function startTaskOrderWatcher() {
29
29
  if (!row || row.getAttribute('data-task-item-key') === dragKey) return;
30
30
  e.preventDefault();
31
31
  if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
32
- // 实时预览:把拖动行移动到目标行前/后(拖动期间 React 不重渲染;落库后刷新重建)
32
+ // 实时预览:把拖动行移动到目标行前/后(拖动期间 React 不重渲染;写入数据库后刷新重建)
33
33
  const rect = row.getBoundingClientRect();
34
34
  const before = e.clientY < rect.top + rect.height / 2;
35
35
  const src = [...row.parentElement.querySelectorAll('[data-task-item-key]')]