zcode-pro 0.18.0 → 0.19.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.
@@ -206,11 +206,94 @@ export async function listOverriddenTitles(dataRoot) {
206
206
  return { titles };
207
207
  }
208
208
 
209
+ // 智能起名写入:把生成标题按“手动重命名”存入索引(title_overridden = 1),
210
+ // 之后官方自动标题不会再覆盖这条记录。仅在会话未删除、且从未被手动重命名过
211
+ // (title_overridden = 0)时写入——手动重命名永远优先。返回 { written, title };
212
+ // written = false 表示没写(已重命名或会话不在索引中)。库被占用时抛 index-busy。
213
+ export async function setTaskTitleIfNotOverridden(dataRoot, sessId, title) {
214
+ const dbFile = taskIndexPath(dataRoot);
215
+ if (!existsSync(dbFile)) return { written: false, title };
216
+ const driver = await loadDriver();
217
+ if (!driver) {
218
+ const err = new Error('缺少 SQLite 支持(需要 Node ≥23.4 或 sqlite3 命令)');
219
+ err.code = 'index-error';
220
+ throw err;
221
+ }
222
+ try {
223
+ if (driver.kind === 'node') {
224
+ const db = new driver.DatabaseSync(dbFile);
225
+ try {
226
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
227
+ db.exec('BEGIN IMMEDIATE');
228
+ try {
229
+ const r = db.prepare(
230
+ 'UPDATE tasks SET title = ?, title_overridden = 1 WHERE task_id = ? AND deleted = 0 AND title_overridden = 0'
231
+ ).run(title, sessId);
232
+ db.exec('COMMIT');
233
+ return { written: r.changes > 0, title };
234
+ } catch (err) {
235
+ try { db.exec('ROLLBACK'); } catch { /* 已回滚/连接失效 */ }
236
+ throw err;
237
+ }
238
+ } finally { db.close(); }
239
+ }
240
+ const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
241
+ const r = runCli(dbFile,
242
+ 'BEGIN IMMEDIATE;\n'
243
+ + `UPDATE tasks SET title = ${esc(title)}, title_overridden = 1`
244
+ + ` WHERE task_id = ${esc(sessId)} AND deleted = 0 AND title_overridden = 0;\n`
245
+ + 'SELECT changes();\nCOMMIT;');
246
+ return { written: Number(r.stdout.trim()) > 0, title };
247
+ } catch (err) {
248
+ if (isBusyError(err)) {
249
+ const e = new Error('任务索引正被占用(ZCode 可能正在写入)');
250
+ e.code = 'index-busy';
251
+ throw e;
252
+ }
253
+ const e = new Error('写入任务标题失败: ' + (err?.message || err));
254
+ e.code = 'index-error';
255
+ throw e;
256
+ }
257
+ }
258
+
209
259
  function isBusyError(err) {
210
260
  const s = String(err?.code || '') + String(err?.message || '');
211
261
  return /SQLITE_BUSY|database is locked/i.test(s);
212
262
  }
213
263
 
264
+ // 读某会话记录的模型信息(智能起名“跟随当前会话”用)。model 列存
265
+ // “<供应商id>/<模型id>”(如 be0a90cb…/glm-5.3),拆开后返回
266
+ // { providerId, modelId };没有斜线时 providerId 退回 provider 列的值。
267
+ // 会话不存在返回 null;读取失败按 null 处理(调用方换明确报错)
268
+ export async function getTaskProviderModel(dataRoot, sessId) {
269
+ const dbFile = taskIndexPath(dataRoot);
270
+ if (!existsSync(dbFile)) return null;
271
+ const driver = await loadDriver();
272
+ if (!driver) return null;
273
+ let row = null;
274
+ try {
275
+ if (driver.kind === 'node') {
276
+ const db = new driver.DatabaseSync(dbFile);
277
+ try {
278
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
279
+ row = db.prepare('SELECT provider, model FROM tasks WHERE task_id = ? AND deleted = 0').get(sessId) || null;
280
+ } finally { db.close(); }
281
+ } else {
282
+ const esc = (v) => "'" + String(v).replace(/'/g, "''") + "'";
283
+ const r = runCli(dbFile,
284
+ `SELECT coalesce(json_object('provider', provider, 'model', model), 'null') FROM tasks WHERE task_id = ${esc(sessId)} AND deleted = 0;`);
285
+ const parsed = JSON.parse((r.stdout.trim() || 'null'));
286
+ row = parsed && typeof parsed === 'object' ? parsed : null;
287
+ }
288
+ } catch { /* 读不了就当没有 */ }
289
+ if (!row) return null;
290
+ const model = String(row.model || '');
291
+ const slash = model.indexOf('/');
292
+ return slash > 0
293
+ ? { providerId: model.slice(0, slash), modelId: model.slice(slash + 1) }
294
+ : { providerId: String(row.provider || ''), modelId: model };
295
+ }
296
+
214
297
  // 任务(会话)ID → 工作区路径(无尾分隔符):空闲回收在 Windows 上的后台命令
215
298
  // 归属判定用(命令行里只带会话 ID,需经索引反查它属于哪个项目)。
216
299
  // 读取失败返回空表,调用方按“无法归属”保守处理
@@ -43,6 +43,7 @@ const zh = {
43
43
  tabFeatures: '功能',
44
44
  tabStyles: '样式',
45
45
  tabProxy: '代理',
46
+ tabBehavior: '行为',
46
47
  tabAgents: '全局提示词',
47
48
  agentsDesc: '写入 ~/.zcode/AGENTS.md,作为默认指令用于所有项目的每次会话;保存后从新会话起生效,项目内的 AGENTS.md 可补充或覆盖。内容超过 100KB 会被应用截断。',
48
49
  agentsPlaceholder: '填写希望所有项目默认遵循的指令;清空并保存即移除全局提示词',
@@ -154,6 +155,30 @@ const zh = {
154
155
  featureSessionSwitchDesc: 'alt+z 在当前与上次会话间来回切换;按住 alt 再按 x/c 弹出最近会话列表前后选择,松开 alt 切换(类 alt+tab)。',
155
156
  featureTitleLock: '重命名会话名锁定',
156
157
  featureTitleLockDesc: '手动重命名过的会话不再被自动标题覆盖:会话有新回答后界面可能改显自动标题,重启后又变回,本功能让侧栏始终显示重命名后的名字。',
158
+ tabTitleGen: '任务名提示词',
159
+ titleGenShortDesc: '新会话开始作答时,按所选供应商与提示词生成任务名并按手动重命名保存,官方自动标题不再覆盖。',
160
+ titleGenEnabled: '启用',
161
+ titleGenProvider: '供应商',
162
+ titleGenProviderNone: '不跟随(自填地址)',
163
+ titleGenProviderSession: '跟随当前会话',
164
+ titleGenProviderDeleted: '(已删除)',
165
+ titleGenModel: '模型',
166
+ titleGenModelHint: '从列表选择,或直接填写模型 id',
167
+ titleGenModelEmpty: '(未选择)',
168
+ titleGenModelSessionHint: '从列表选择模型;列表第一项为跟随会话当前模型',
169
+ titleGenModelFollowSession: '跟随当前会话当前模型',
170
+ titleGenModelManualHint: '输入模型 id',
171
+ titleGenBaseUrl: '接口地址',
172
+ titleGenApiKey: 'API Key',
173
+ titleGenFormat: '协议',
174
+ titleGenPrompt: '提示词',
175
+ titleGenPromptPlaceholder: '留空使用内置默认提示词(点击下方按钮可查看)',
176
+ titleGenPromptReset: '使用默认提示词',
177
+ titleGenSample: '测试消息',
178
+ titleGenTest: '测试生成',
179
+ titleGenTesting: '生成中…',
180
+ titleGenTestOk: '生成结果:',
181
+ titleGenTestFailed: '生成失败',
157
182
  featureWsRunning: '折叠项目运行提示(实验性)',
158
183
  featureWsRunningDesc: '折叠的项目里仍有会话在运行时,项目图标旋转提示;全部结束或展开项目后恢复。期间每 60 秒在后台短暂展开该项目复核运行状态再收起,会话行不显示,仅项目位置可能有约四分之一秒的轻微高度起伏。',
159
184
  tabPerformance: '性能',
@@ -228,10 +253,6 @@ const zh = {
228
253
  visionMoveDown: '下移',
229
254
  visionName: '名称',
230
255
  visionUseProvider: '供应商',
231
- visionUseProviderHint: '下拉选择或手动输入:session=跟随当前会话供应商,或供应商名/ID;设置后 baseUrl/API Key/格式自动取该供应商',
232
- visionUseProviderNone: '不跟随(自填地址)',
233
- visionUseProviderSession: '跟随当前会话(session)',
234
- visionUseProviderUnknown: '未找到该供应商,识别时会报错;请从下拉选择,或输入 session',
235
256
  visionSkipAfterFailures: '连续失败次数',
236
257
  visionSkipAfterFailuresHint: '某代理连续失败满次数后暂时跳过它,直接试下一个;0 = 不跳过',
237
258
  visionSkipMinutes: '跳过分钟数',
@@ -340,7 +361,6 @@ const zh = {
340
361
  upgradeDone: '已更新到 {v}',
341
362
  upgradeFinished: '升级完成',
342
363
  upgradeCheckFailed: '检查更新失败',
343
- version: '版本',
344
364
  close: '关闭',
345
365
  cancel: '取消',
346
366
  pathLabel: '路径',
@@ -385,6 +405,7 @@ const en = {
385
405
  tabFeatures: 'Features',
386
406
  tabStyles: 'Styles',
387
407
  tabProxy: 'Proxy',
408
+ tabBehavior: 'Behavior',
388
409
  tabAgents: 'Global Prompt',
389
410
  agentsDesc: 'Written to ~/.zcode/AGENTS.md and used as default instructions in every session across all projects. Takes effect for new sessions; per-project AGENTS.md can extend or override it. Content over 100 KB is truncated by the app.',
390
411
  agentsPlaceholder: 'Instructions followed by all projects by default; save empty to remove the global prompt',
@@ -496,6 +517,30 @@ const en = {
496
517
  featureSessionSwitchDesc: 'alt+z toggles between the current and the last session; hold alt and press x/c to move the highlight across recently used sessions, release alt to switch (like alt+tab).',
497
518
  featureTitleLock: 'Keep renamed session titles',
498
519
  featureTitleLockDesc: 'Sessions you renamed keep their custom title: after new replies the app may show an auto-generated title instead, which reverts after a restart; this keeps the renamed title in the sidebar.',
520
+ tabTitleGen: 'Task name prompt',
521
+ titleGenShortDesc: 'When a new session starts answering, generates the task name with the chosen provider and prompt and saves it as a manual rename the built-in title never overrides.',
522
+ titleGenEnabled: 'Enable',
523
+ titleGenProvider: 'Provider',
524
+ titleGenProviderNone: 'Custom endpoint (no provider)',
525
+ titleGenProviderSession: 'Follow current session',
526
+ titleGenProviderDeleted: ' (deleted)',
527
+ titleGenModel: 'Model',
528
+ titleGenModelHint: 'Pick from the list, or type a model id',
529
+ titleGenModelEmpty: '(not set)',
530
+ titleGenModelSessionHint: 'Pick from the list; the first entry follows the session model',
531
+ titleGenModelFollowSession: 'Follow the session model',
532
+ titleGenModelManualHint: 'Type a model id',
533
+ titleGenBaseUrl: 'Base URL',
534
+ titleGenApiKey: 'API Key',
535
+ titleGenFormat: 'Protocol',
536
+ titleGenPrompt: 'Prompt',
537
+ titleGenPromptPlaceholder: 'Leave empty for the built-in default prompt (button below shows it)',
538
+ titleGenPromptReset: 'Use default prompt',
539
+ titleGenSample: 'Test message',
540
+ titleGenTest: 'Test',
541
+ titleGenTesting: 'Generating…',
542
+ titleGenTestOk: 'Result: ',
543
+ titleGenTestFailed: 'Generation failed',
499
544
  featureWsRunning: 'Running indicator on collapsed projects (experimental)',
500
545
  featureWsRunningDesc: 'Spins a collapsed project\'s icon while any of its sessions is still running; stops when they all finish or the project is expanded. While spinning, the project is briefly re-expanded in the background every 60 s to re-check and then collapsed — rows stay hidden, with only a subtle quarter-second height ripple at the project.',
501
546
  tabPerformance: 'Performance',
@@ -570,10 +615,6 @@ const en = {
570
615
  visionMoveDown: 'Down',
571
616
  visionName: 'Name',
572
617
  visionUseProvider: 'Provider',
573
- visionUseProviderHint: 'Pick from the list or type: session = follow the current session provider, or a provider name/ID; baseUrl/API key/format follow that provider',
574
- visionUseProviderNone: 'Not following (enter base URL)',
575
- visionUseProviderSession: 'Follow session provider (session)',
576
- visionUseProviderUnknown: 'Unknown provider; recognition will fail. Pick from the list or type session',
577
618
  visionSkipAfterFailures: 'Skip after failures',
578
619
  visionSkipAfterFailuresHint: 'After this many consecutive failures a proxy is skipped and the next one is tried; 0 = never skip',
579
620
  visionSkipMinutes: 'Skip minutes',
@@ -682,7 +723,6 @@ const en = {
682
723
  upgradeDone: 'Updated to {v}',
683
724
  upgradeFinished: 'Update finished',
684
725
  upgradeCheckFailed: 'Update check failed',
685
- version: 'Version',
686
726
  close: 'Close',
687
727
  cancel: 'Cancel',
688
728
  pathLabel: 'Path',
@@ -8,6 +8,7 @@
8
8
  // 触发器/菜单的 React fiber props(OpenSplitButton 场景 target/fileLink/row.path)。
9
9
  import { t, closeRadixMenu, itemsOf, itemText, rpc } from '../core.js';
10
10
  import { getConfig } from '../core.js';
11
+ import { showToast } from '../ui.js';
11
12
 
12
13
  const COPY_ABS_TEXTS = ['复制绝对路径', 'Copy absolute path'];
13
14
  const NO_APPS_TEXTS = ['未找到可用 App', 'No apps found'];
@@ -126,6 +127,12 @@ export function handleFileMenu(content) {
126
127
  const path = resolveTargetPath(content);
127
128
  if (!path) return;
128
129
  const L = t();
130
+ // 打开失败(路径不存在等)弹 toast 报错,不再无声无息
131
+ const fire = (endpoint, label) => {
132
+ rpc(endpoint, { method: 'POST', body: { path } })
133
+ .then((r) => { if (r && r.ok === false) showToast(`${label}失败:${r.error || '未知错误'}`, 'error'); })
134
+ .catch(() => { /* 网络异常时 helper 多半未运行,此时菜单不会注入,静默 */ });
135
+ };
129
136
  // “默认应用打开”锚在编辑器列表末尾(无可用编辑器时即“未找到可用 App”禁用项)之上;
130
137
  // 找不到该组时退而锚在“复制绝对路径”上,保持“打开方式在前、路径操作在后”的次序
131
138
  const noApps = items.find((el) => NO_APPS_TEXTS.some((x) => itemText(el) === x))
@@ -135,13 +142,13 @@ export function handleFileMenu(content) {
135
142
  marker: 'file-open-default',
136
143
  label: L.fileOpenDefault,
137
144
  icon: ICON_OPEN_DEFAULT,
138
- onPick: () => { try { void rpc('/open-path', { method: 'POST', body: { path } }); } catch { /* ignore */ } },
145
+ onPick: () => { void fire('/open-path', L.fileOpenDefault); },
139
146
  });
140
147
  appendMenuItem(content, copyAbs, copyAbs, {
141
148
  marker: 'file-reveal',
142
149
  label: L.fileReveal,
143
150
  icon: ICON_REVEAL,
144
- onPick: () => { try { void rpc('/reveal-path', { method: 'POST', body: { path } }); } catch { /* ignore */ } },
151
+ onPick: () => { void fire('/reveal-path', L.fileReveal); },
145
152
  });
146
153
  })();
147
154
  }
@@ -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';
@@ -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 应用与模型请求。
@@ -1168,8 +1201,6 @@ export function openSettingsDialog() {
1168
1201
  skipMinutes: 30,
1169
1202
  forceIntercept: true,
1170
1203
  };
1171
- // 供应商下拉数据:合并 zcode 两张供应商表(config.json 的 provider 与 provider_config.json 的 providerRules)
1172
- const visionProviders = visionProvidersRes.ok && Array.isArray(visionProvidersRes.providers) ? visionProvidersRes.providers : [];
1173
1204
  let visionCfg = visionRes.ok && visionRes.config && typeof visionRes.config === 'object'
1174
1205
  ? structuredClone(visionRes.config)
1175
1206
  : null;
@@ -1283,6 +1314,8 @@ export function openSettingsDialog() {
1283
1314
  return h('label', { class: 'flex shrink-0 items-center gap-1.5', title },
1284
1315
  h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, label), n);
1285
1316
  };
1317
+ // 接口地址 / API Key 是否显示走 provider-fields 的共用规则
1318
+ //(isManualConn:不跟随或供应商已删才显示,连接其余由跟随对象负责)
1286
1319
  const cards = visionCfg.proxies.map((p, i) => h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
1287
1320
  h('div', { class: 'flex flex-wrap items-center gap-2' },
1288
1321
  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 +1346,35 @@ export function openSettingsDialog() {
1313
1346
  smallBtn(L.visionRemove, () => { visionCfg.proxies.splice(i, 1); void persistVision(); renderVision(); }),
1314
1347
  ),
1315
1348
  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';
1349
+ // 供应商与模型走 provider-fields 共用逻辑:跟随会话时模型下拉为
1350
+ // 全部供应商模型汇总,具体供应商为其模型列表;已删 id 标注「已删除」
1351
+ field(L.visionUseProvider, comboField(p.useProvider || '',
1352
+ providerOptionsOf(titleGenProviders, L),
1353
+ (v, src) => {
1354
+ p.useProvider = v;
1338
1355
  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' })),
1356
+ // 下拉切换后重渲染整个面板:模型下拉选项随新供应商变化,接口
1357
+ // 地址/API Key 字段按「不跟随或供应商已删才显示」重新显隐
1358
+ if (src === 'select') renderVision();
1359
+ },
1360
+ { placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted })),
1361
+ // 跟随会话时模型下拉头部有「跟随当前会话当前模型」(值为空,
1362
+ // 插件 ≥1.6.1 在调用时解析为会话当前模型);具体供应商为其模型列表
1363
+ field(L.visionModel, comboField(p.model || '',
1364
+ modelOptionsOf(p.useProvider, titleGenProviders, {
1365
+ followSession: (p.useProvider || '').trim() === 'session',
1366
+ followSessionLabel: L.titleGenModelFollowSession,
1367
+ }),
1368
+ (v) => { p.model = v; saveVisionSoon(); },
1369
+ {
1370
+ placeholder: modelPlaceholderOf(p.useProvider, L),
1371
+ emptyLabel: (p.useProvider || '').trim() === 'session'
1372
+ ? L.titleGenModelFollowSession : L.titleGenModelEmpty,
1373
+ })),
1374
+ ...(isManualConn(p.useProvider, titleGenProviders) ? [
1375
+ field(L.visionBaseUrl, textIn(p.baseUrl, (v) => { p.baseUrl = v; saveVisionSoon(); })),
1376
+ field(L.visionApiKey + ' · ' + L.visionApiKeyHint, textIn(p.apiKey, (v) => { p.apiKey = v; saveVisionSoon(); }, { type: 'password' })),
1377
+ ] : []),
1353
1378
  ),
1354
1379
  (() => {
1355
1380
  const n = h('textarea', { class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '3', placeholder: L.visionPrompt });
@@ -2011,13 +2036,164 @@ export function openSettingsDialog() {
2011
2036
  };
2012
2037
  renderHeadroomPane();
2013
2038
 
2039
+ // “任务名提示词”(位于“行为”标签页的二级标签):新会话开始作答时 →
2040
+ // 按自定义供应商/提示词生成标题并按手动重命名写库。配置读写走 /title-gen
2041
+ //(存 zcodepro.json);供应商下拉数据走 /title-gen/providers(与视觉代理
2042
+ // 同源,但只列 helper 能直连调用的条目)。
2043
+ // 结构性改动(开关/供应商/协议)即时保存,提示词与文本输入防抖保存。
2044
+ let titleGenCfg = titleGenRes.ok && titleGenRes.titleGen && typeof titleGenRes.titleGen === 'object'
2045
+ ? structuredClone(titleGenRes.titleGen) : null;
2046
+ const titleGenDefaultPrompt = titleGenRes.ok && typeof titleGenRes.defaultPrompt === 'string'
2047
+ ? titleGenRes.defaultPrompt : '';
2048
+ let titleGenSaveTimer = null;
2049
+ const persistTitleGen = async () => {
2050
+ const res = await rpc('/title-gen', { method: 'POST', body: { titleGen: titleGenCfg } });
2051
+ clearConfigCache();
2052
+ if (!res.ok) showToast(L.failed + ': ' + errText(res), 'error');
2053
+ return res.ok;
2054
+ };
2055
+ const saveTitleGenSoon = () => {
2056
+ clearTimeout(titleGenSaveTimer);
2057
+ titleGenSaveTimer = setTimeout(() => { void persistTitleGen(); }, 500);
2058
+ };
2059
+ const renderTitleGenPane = () => {
2060
+ if (!titleGenCfg) {
2061
+ paneTitleGen.replaceChildren(
2062
+ h('p', { class: 'text-ui-sm/relaxed text-foreground-subtle' }, L.titleGenShortDesc),
2063
+ h('p', { class: 'mt-2 text-ui-sm text-destructive' }, L.visionLoadFailed + ': ' + errText(titleGenRes)),
2064
+ );
2065
+ return;
2066
+ }
2067
+ 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';
2068
+ const field = (labelText, node) => h('label', { class: 'block min-w-0' },
2069
+ h('span', { class: 'mb-1 block text-ui-xs font-medium text-foreground-subtle' }, labelText), node);
2070
+ // 供应商 / 模型 / 地址密钥共用 provider-fields 的规则(详见该模块说明)。
2071
+ // 换供应商后重渲染:模型下拉换成新供应商的模型、地址字段按模式显隐;
2072
+ // 手动输入过的模型保留(不在新列表里时显示为「手动输入」项,是否改选
2073
+ // 由用户决定)
2074
+ const showInlineConn = isManualConn(titleGenCfg.provider, titleGenProviders);
2075
+ const providerCombo = comboField(
2076
+ titleGenCfg.provider || '',
2077
+ providerOptionsOf(titleGenProviders, L),
2078
+ (v, src) => {
2079
+ titleGenCfg.provider = v;
2080
+ if (src === 'select') { void persistTitleGen(); renderTitleGenPane(); }
2081
+ else saveTitleGenSoon();
2082
+ },
2083
+ { placeholder: L.titleGenProviderSession, staleLabel: L.titleGenProviderDeleted });
2084
+ const modelCombo = comboField(
2085
+ titleGenCfg.model || '',
2086
+ // 跟随会话时选项表头部有“跟随当前会话当前模型”(值为空,生成时用
2087
+ // 会话当前模型),选了具体模型后也能再切回它
2088
+ modelOptionsOf(titleGenCfg.provider, titleGenProviders, {
2089
+ followSession: true,
2090
+ followSessionLabel: L.titleGenModelFollowSession,
2091
+ }),
2092
+ (v) => { titleGenCfg.model = v; saveTitleGenSoon(); },
2093
+ {
2094
+ placeholder: modelPlaceholderOf(titleGenCfg.provider, L),
2095
+ emptyLabel: (titleGenCfg.provider || '').trim() === 'session'
2096
+ ? L.titleGenModelFollowSession : L.titleGenModelEmpty,
2097
+ });
2098
+ // 接口地址 / API Key / 协议(显隐见上方 showInlineConn):不跟随时由
2099
+ // 用户填写;供应商被删时展示保存的副本,仍可直连或改填
2100
+ const textIn = (value, onInput, { type = 'text', placeholder = '' } = {}) => {
2101
+ const n = h('input', { type, value: value || '', placeholder, class: inputCls });
2102
+ n.addEventListener('input', () => onInput(n.value));
2103
+ return n;
2104
+ };
2105
+ const formatSel = h('select', { class: inputCls },
2106
+ h('option', { value: 'anthropic' }, 'anthropic'),
2107
+ h('option', { value: 'openai' }, 'openai'));
2108
+ formatSel.value = titleGenCfg.format === 'openai' ? 'openai' : 'anthropic';
2109
+ formatSel.addEventListener('change', () => {
2110
+ titleGenCfg.format = formatSel.value;
2111
+ void persistTitleGen();
2112
+ });
2113
+ const inlineConnCard = h('div', {
2114
+ class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-3',
2115
+ style: showInlineConn ? '' : 'display:none',
2116
+ },
2117
+ field(L.titleGenBaseUrl, textIn(titleGenCfg.baseUrl, (v) => { titleGenCfg.baseUrl = v; saveTitleGenSoon(); }, { placeholder: 'https://…' })),
2118
+ field(L.titleGenApiKey, textIn(titleGenCfg.apiKey, (v) => { titleGenCfg.apiKey = v; saveTitleGenSoon(); }, { type: 'password' })),
2119
+ field(L.titleGenFormat, formatSel));
2120
+ // 提示词:留空 = 内置默认;「查看默认提示词」把默认值载入编辑框供修改
2121
+ const promptArea = h('textarea', {
2122
+ class: 'zcodepro-textarea zcodepro-textarea-sm', rows: '8',
2123
+ placeholder: L.titleGenPromptPlaceholder,
2124
+ });
2125
+ promptArea.value = titleGenCfg.prompt || '';
2126
+ promptArea.addEventListener('input', () => { titleGenCfg.prompt = promptArea.value; saveTitleGenSoon(); });
2127
+ // 测试:用示例首条消息实际生成一次(不写库),验证供应商/模型/提示词
2128
+ // 连通;示例文本随界面语言取中英文版
2129
+ const sampleText = isZhLocale()
2130
+ ? '帮我看下这个报错是什么原因:NullPointerException at com.example.OrderService.calculate(OrderService.java:87)'
2131
+ : 'Why does my Spring Boot app throw NullPointerException intermittently in OrderService.calculate?';
2132
+ const sampleInput = h('input', {
2133
+ type: 'text', class: inputCls + ' min-w-0 flex-1',
2134
+ value: sampleText,
2135
+ });
2136
+ let testBtn;
2137
+ const testState = h('span', { class: 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-foreground-subtle' });
2138
+ testBtn = btnSmall(L.titleGenTest, async () => {
2139
+ testBtn.disabled = true;
2140
+ testBtn.textContent = L.titleGenTesting;
2141
+ testState.textContent = L.titleGenTesting;
2142
+ const res = await rpc('/title-gen/run', {
2143
+ method: 'POST',
2144
+ body: { text: sampleInput.value, test: true },
2145
+ });
2146
+ testBtn.disabled = false;
2147
+ testBtn.textContent = L.titleGenTest;
2148
+ if (res && res.ok) {
2149
+ testState.textContent = L.titleGenTestOk + res.title;
2150
+ showToast(res.title, 'success');
2151
+ } else {
2152
+ testState.textContent = (res && errText(res)) || L.titleGenTestFailed;
2153
+ testState.className = 'min-w-0 flex-1 truncate text-ui-xs/relaxed text-destructive';
2154
+ showToast(L.titleGenTestFailed + ': ' + (res ? errText(res) : ''), 'error');
2155
+ }
2156
+ }, 'shrink-0');
2157
+ paneTitleGen.replaceChildren(
2158
+ // 信息卡:一句话简介在左,启用开关右对齐
2159
+ h('div', { class: 'mt-3 flex items-center justify-between gap-2 rounded-xl border border-border p-2' },
2160
+ h('span', { class: 'min-w-0 text-ui-sm/relaxed text-foreground-subtle' }, L.titleGenShortDesc),
2161
+ h('div', { class: 'shrink-0' }, settingRow(L.titleGenEnabled, '', titleGenCfg.enabled === true, async () => {
2162
+ titleGenCfg.enabled = !(titleGenCfg.enabled === true);
2163
+ await persistTitleGen();
2164
+ renderTitleGenPane();
2165
+ }))),
2166
+ // 供应商 + 模型
2167
+ h('div', { class: 'mt-2 grid grid-cols-1 gap-2 rounded-xl border border-border p-2 sm:grid-cols-2' },
2168
+ field(L.titleGenProvider, providerCombo),
2169
+ field(L.titleGenModel, modelCombo)),
2170
+ inlineConnCard,
2171
+ // 提示词 + 载入默认
2172
+ h('div', { class: 'mt-2 rounded-xl border border-border p-2' },
2173
+ h('div', { class: 'mb-1 flex items-center justify-between gap-2' },
2174
+ h('span', { class: 'text-ui-xs font-medium text-foreground-subtle' }, L.titleGenPrompt),
2175
+ btnSmall(L.titleGenPromptReset, () => {
2176
+ promptArea.value = titleGenDefaultPrompt;
2177
+ titleGenCfg.prompt = titleGenDefaultPrompt;
2178
+ void persistTitleGen();
2179
+ promptArea.focus();
2180
+ })),
2181
+ promptArea),
2182
+ // 测试
2183
+ h('div', { class: 'mt-2 flex items-center gap-2 rounded-xl border border-border p-2' },
2184
+ sampleInput, testBtn),
2185
+ testState,
2186
+ );
2187
+ };
2188
+ renderTitleGenPane();
2189
+
2014
2190
  body.append(
2015
2191
  statusLine,
2016
2192
  tablist,
2017
2193
  paneFeatures,
2194
+ paneAgents,
2018
2195
  paneStyles,
2019
2196
  panePerf,
2020
- paneAgents,
2021
2197
  paneProxy,
2022
2198
  paneSync,
2023
2199
  paneVision,