codexmate 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -1222,28 +1222,34 @@ function readPiModels(params = {}) {
1222
1222
  const dir = getPiAgentDir();
1223
1223
  const filePath = path.join(dir, 'models.json');
1224
1224
  if (!fs.existsSync(filePath)) {
1225
- return { providers: {} };
1225
+ return { providers: {}, file: {} };
1226
1226
  }
1227
1227
  const raw = fs.readFileSync(filePath, 'utf-8');
1228
1228
  const data = JSON.parse(raw);
1229
1229
  const providers = (data && typeof data === 'object' && data.providers && typeof data.providers === 'object')
1230
1230
  ? data.providers
1231
1231
  : {};
1232
- return { providers };
1232
+ const file = data && typeof data === 'object' && !Array.isArray(data) ? data : { providers };
1233
+ return { providers, file };
1233
1234
  } catch (e) {
1234
1235
  return { error: e && e.message ? e.message : '读取 Pi models.json 失败' };
1235
1236
  }
1236
1237
  }
1237
1238
 
1238
1239
  function writePiModels(params = {}) {
1240
+ const file = params && typeof params.file === 'object' && params.file !== null && !Array.isArray(params.file) ? params.file : null;
1239
1241
  const providers = params && typeof params.providers === 'object' ? params.providers : null;
1240
- if (providers === null) {
1241
- return { error: 'providers 不能为空' };
1242
+ if (!file && providers === null) {
1243
+ return { error: '缺少可写入的内容' };
1242
1244
  }
1243
1245
  try {
1244
1246
  const dir = getPiAgentDir();
1245
1247
  fs.mkdirSync(dir, { recursive: true });
1246
1248
  const filePath = path.join(dir, 'models.json');
1249
+ if (file) {
1250
+ fs.writeFileSync(filePath, JSON.stringify(file, null, 2), 'utf-8');
1251
+ return { success: true };
1252
+ }
1247
1253
  let existing = {};
1248
1254
  if (fs.existsSync(filePath)) {
1249
1255
  try {
@@ -1294,16 +1300,21 @@ function readPiSettings(params = {}) {
1294
1300
  }
1295
1301
 
1296
1302
  function writePiSettings(params = {}) {
1303
+ const fullSettings = params && typeof params.settings === 'object' && params.settings !== null && !Array.isArray(params.settings) ? params.settings : null;
1297
1304
  const updates = {};
1298
1305
  if (typeof (params && params.defaultProvider) === 'string') updates.defaultProvider = params.defaultProvider;
1299
1306
  if (typeof (params && params.defaultModel) === 'string') updates.defaultModel = params.defaultModel;
1300
- if (Object.keys(updates).length === 0) {
1307
+ if (!fullSettings && Object.keys(updates).length === 0) {
1301
1308
  return { error: '缺少可写入的设置项' };
1302
1309
  }
1303
1310
  try {
1304
1311
  const dir = getPiAgentDir();
1305
1312
  fs.mkdirSync(dir, { recursive: true });
1306
1313
  const filePath = path.join(dir, 'settings.json');
1314
+ if (fullSettings) {
1315
+ fs.writeFileSync(filePath, JSON.stringify(fullSettings, null, 2), 'utf-8');
1316
+ return { success: true, settings: fullSettings };
1317
+ }
1307
1318
  let existing = {};
1308
1319
  if (fs.existsSync(filePath)) {
1309
1320
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -148,6 +148,11 @@ document.addEventListener('DOMContentLoaded', () => {
148
148
  piActiveProvider: '',
149
149
  piActiveModel: '',
150
150
  piShowKey: false,
151
+ piSettingsJsonDraft: '',
152
+ piSettingsJsonError: '',
153
+ piModelsJsonDraft: '',
154
+ piModelsJsonError: '',
155
+ piFileJsonSaving: false,
151
156
  // Plugins
152
157
  pluginsActiveId: 'prompt-templates',
153
158
  pluginsLoading: false,
@@ -819,10 +824,7 @@ document.addEventListener('DOMContentLoaded', () => {
819
824
  if (typeof this.persistWebUiPreferences === 'function') {
820
825
  this.persistWebUiPreferences({ sysPromptMode: newVal });
821
826
  }
822
- }
823
- },
824
-
825
- watch: {
827
+ },
826
828
  configMode(newMode) {
827
829
  if (newMode === 'pi' && typeof this.loadPiSources === 'function') {
828
830
  this.loadPiSources();
@@ -471,6 +471,18 @@ export function createAgentsMethods(options = {}) {
471
471
  .filter(Boolean)
472
472
  .filter(function (v, i, a) { return a.indexOf(v) === i; });
473
473
  }
474
+ if (this.promptsSubTab === 'claude-project'
475
+ && this.mainTab === 'prompts'
476
+ && Array.isArray(this.projectPathOptions)
477
+ && this.projectPathOptions.length) {
478
+ const currentPath = (this.projectClaudeMdPath || '').trim();
479
+ if (currentPath && this.projectPathOptions.indexOf(currentPath) !== -1) {
480
+ this.projectClaudeMdPath = currentPath;
481
+ } else {
482
+ this.projectClaudeMdPath = this.projectPathOptions[0];
483
+ }
484
+ if (typeof this.loadPromptsContent === 'function') this.loadPromptsContent();
485
+ }
474
486
  } catch (_) {
475
487
  // silent
476
488
  } finally {
@@ -1015,7 +1027,11 @@ export function createAgentsMethods(options = {}) {
1015
1027
  if (!this.projectPathOptions.length && !this.projectPathOptionsLoading && typeof this.loadProjectPathOptions === 'function') {
1016
1028
  this.loadProjectPathOptions();
1017
1029
  }
1018
- const projectPath = (this.projectClaudeMdPath || '').trim();
1030
+ let projectPath = (this.projectClaudeMdPath || '').trim();
1031
+ if (!projectPath && Array.isArray(this.projectPathOptions) && this.projectPathOptions.length) {
1032
+ this.projectClaudeMdPath = this.projectPathOptions[0];
1033
+ projectPath = this.projectClaudeMdPath.trim();
1034
+ }
1019
1035
  if (projectPath) {
1020
1036
  rpcParams.baseDir = projectPath;
1021
1037
  }
@@ -66,7 +66,8 @@ export function createPiConfigMethods({ api: apiClient }) {
66
66
  try {
67
67
  const [providers] = await Promise.all([
68
68
  this.loadPiProviders(),
69
- this.loadPiSettings()
69
+ this.loadPiSettings(),
70
+ this.loadPiFileJsons()
70
71
  ]);
71
72
  this.piProviders = providers || {};
72
73
  this.piProviderIds = Object.keys(this.piProviders || {}).sort();
@@ -81,24 +82,13 @@ export function createPiConfigMethods({ api: apiClient }) {
81
82
  this.piSaving = true;
82
83
  try {
83
84
  const providerId = this.editingPiProvider.id;
84
- const values = this.editingPiProvider.form;
85
- const extras = this.editingPiProvider.extras || {};
86
- const provider = this.piProviders[providerId] || {};
87
-
88
- const merged = {
89
- ...extras,
90
- baseUrl: values.baseUrl,
91
- api: values.api,
92
- apiKey: values.apiKey || '',
93
- models: (Array.isArray(values.models) ? values.models : []).map((model) => ({
94
- ...model,
95
- id: model.id || '',
96
- name: model.id || ''
97
- }))
98
- };
99
- if (isPiPlainObject(provider.headers)) merged.headers = provider.headers;
100
- if (typeof provider.title === 'string') merged.title = provider.title;
101
- else if (typeof provider.name === 'string') merged.name = provider.name;
85
+ const { merged, ok, error } = this.piBuildMergedEditorRecord();
86
+ if (!ok || !merged) {
87
+ this.piEditorJsonError = error || '配置 JSON 不合法';
88
+ this.message = this.piEditorJsonError;
89
+ this.messageType = 'error';
90
+ return;
91
+ }
102
92
 
103
93
  await this.persistPiProvider(providerId, merged);
104
94
  this.piProviders = { ...this.piProviders, [providerId]: merged };
@@ -232,6 +222,7 @@ export function createPiConfigMethods({ api: apiClient }) {
232
222
  this.piRemoteModels = [];
233
223
  this.piRemoteModelError = '';
234
224
  this.piModelSearch = '';
225
+ this.piEditorJsonError = '';
235
226
  const provider = this.piProviders[providerId] || {};
236
227
  const knownKeys = ['id', 'name', 'title', 'baseUrl', 'api', 'apiKey', 'models', 'headers', 'configJson'];
237
228
  const extras = {};
@@ -269,6 +260,7 @@ export function createPiConfigMethods({ api: apiClient }) {
269
260
  this.piRemoteModels = [];
270
261
  this.piRemoteModelError = '';
271
262
  this.piModelSearch = '';
263
+ this.piEditorJsonError = '';
272
264
  },
273
265
  removePiProviderModel(index) {
274
266
  if (!this.editingPiProvider) return;
@@ -350,5 +342,139 @@ export function createPiConfigMethods({ api: apiClient }) {
350
342
  const api = provider.api || '';
351
343
  return [baseUrl, api].filter(Boolean).join(' • ') || 'Piper provider';
352
344
  }
345
+ ,
346
+ piParseFileJsonDraft(draft, label) {
347
+ const name = label || 'JSON';
348
+ const text = String(draft || '').trim();
349
+ if (!text) return { ok: false, value: null, error: name + ' 内容不能为空' };
350
+ try {
351
+ const parsed = JSON.parse(text);
352
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object') {
353
+ return { ok: false, value: null, error: name + ' 必须是 JSON 对象' };
354
+ }
355
+ return { ok: true, value: parsed, error: '' };
356
+ } catch (e) {
357
+ return { ok: false, value: null, error: name + ' 解析失败:' + (e && e.message ? e.message : '') };
358
+ }
359
+ },
360
+ async loadPiFileJsons() {
361
+ const [modelsResult, settingsResult] = await Promise.all([
362
+ apiClient('read-pi-models'),
363
+ apiClient('read-pi-settings')
364
+ ]);
365
+ const modelsFile = isPiPlainObject(modelsResult && modelsResult.file) ? modelsResult.file : {};
366
+ const settings = isPiPlainObject(settingsResult && settingsResult.settings) ? settingsResult.settings : {};
367
+ this.piModelsJsonDraft = JSON.stringify(modelsFile, null, 2);
368
+ this.piSettingsJsonDraft = JSON.stringify(settings, null, 2);
369
+ this.piModelsJsonError = '';
370
+ this.piSettingsJsonError = '';
371
+ },
372
+ piSettingsJsonInput() {
373
+ const parsed = this.piParseFileJsonDraft(this.piSettingsJsonDraft, 'settings.json');
374
+ this.piSettingsJsonError = parsed.ok ? '' : parsed.error;
375
+ },
376
+ piModelsJsonInput() {
377
+ const parsed = this.piParseFileJsonDraft(this.piModelsJsonDraft, 'models.json');
378
+ this.piModelsJsonError = parsed.ok ? '' : parsed.error;
379
+ },
380
+ async savePiSettingsJson() {
381
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving || this.piFileJsonSaving) return;
382
+ const parsed = this.piParseFileJsonDraft(this.piSettingsJsonDraft, 'settings.json');
383
+ if (!parsed.ok) {
384
+ this.piSettingsJsonError = parsed.error;
385
+ this.message = parsed.error;
386
+ this.messageType = 'error';
387
+ return;
388
+ }
389
+ this.piFileJsonSaving = true;
390
+ try {
391
+ const result = await apiClient('write-pi-settings', { settings: parsed.value });
392
+ if (result && result.error) throw new Error(result.error);
393
+ this.piSettingsJsonError = '';
394
+ this.piSettingsJsonDraft = JSON.stringify(parsed.value, null, 2);
395
+ await this.loadPiSettings();
396
+ this.message = 'settings.json 已保存';
397
+ this.messageType = 'success';
398
+ } catch (e) {
399
+ this.message = e && e.message ? e.message : '保存 settings.json 失败';
400
+ this.messageType = 'error';
401
+ } finally {
402
+ this.piFileJsonSaving = false;
403
+ }
404
+ },
405
+ async savePiModelsJson() {
406
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving || this.piFileJsonSaving) return;
407
+ const parsed = this.piParseFileJsonDraft(this.piModelsJsonDraft, 'models.json');
408
+ if (!parsed.ok) {
409
+ this.piModelsJsonError = parsed.error;
410
+ this.message = parsed.error;
411
+ this.messageType = 'error';
412
+ return;
413
+ }
414
+ this.piFileJsonSaving = true;
415
+ try {
416
+ const result = await apiClient('write-pi-models', { file: parsed.value });
417
+ if (result && result.error) throw new Error(result.error);
418
+ await this.loadPiSources();
419
+ this.message = 'models.json 已保存';
420
+ this.messageType = 'success';
421
+ } catch (e) {
422
+ this.message = e && e.message ? e.message : '保存 models.json 失败';
423
+ this.messageType = 'error';
424
+ } finally {
425
+ this.piFileJsonSaving = false;
426
+ }
427
+ },
428
+ piParseEditorJsonDraft() {
429
+ const provider = this.editingPiProvider;
430
+ const draft = provider && provider.form ? String(provider.form.configJsonDraft || '').trim() : '';
431
+ if (!draft) return { ok: true, extras: {} };
432
+ try {
433
+ const parsed = JSON.parse(draft);
434
+ if (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object') {
435
+ return { ok: false, extras: {}, error: '配置 JSON 必须是对象' };
436
+ }
437
+ return { ok: true, extras: parsed };
438
+ } catch (e) {
439
+ return { ok: false, extras: {}, error: e && e.message ? '配置 JSON 解析失败:' + e.message : '配置 JSON 解析失败' };
440
+ }
441
+ },
442
+ piBuildMergedEditorRecord() {
443
+ const provider = this.editingPiProvider;
444
+ if (!provider || !provider.form) return { merged: null, ok: false, error: '未在编辑供应商', extras: {} };
445
+ const draft = this.piParseEditorJsonDraft();
446
+ if (!draft.ok) return { merged: null, ok: false, error: draft.error, extras: {} };
447
+ const values = provider.form || {};
448
+ const extras = draft.extras || {};
449
+ const orig = this.piProviders[provider.id] || {};
450
+ const mapModels = (xs) => (Array.isArray(xs) ? xs : []).map((model) => ({
451
+ ...model,
452
+ id: (model && model.id) || '',
453
+ name: (model && (model.name || model.id)) || ''
454
+ }));
455
+ const merged = { ...extras };
456
+ if (merged.baseUrl === undefined) merged.baseUrl = values.baseUrl || '';
457
+ if (merged.api === undefined) merged.api = values.api || '';
458
+ if (merged.apiKey === undefined) merged.apiKey = values.apiKey || '';
459
+ if (merged.models === undefined) merged.models = mapModels(values.models);
460
+ if (isPiPlainObject(orig.headers)) merged.headers = orig.headers;
461
+ if (typeof orig.title === 'string') merged.title = orig.title;
462
+ else if (typeof orig.name === 'string') merged.name = orig.name;
463
+ if (isPiPlainObject(orig.configJson)) merged.configJson = orig.configJson;
464
+ return { merged, ok: true, error: '', extras };
465
+ },
466
+ piProviderPreviewJson() {
467
+ const { merged, ok } = this.piBuildMergedEditorRecord();
468
+ if (!ok || !merged) return '{}';
469
+ try {
470
+ return JSON.stringify(merged, null, 2);
471
+ } catch (_) {
472
+ return '{}';
473
+ }
474
+ },
475
+ piEditorJsonDraftInput() {
476
+ const draft = this.piParseEditorJsonDraft();
477
+ this.piEditorJsonError = draft.ok ? '' : draft.error;
478
+ }
353
479
  };
354
480
  }
@@ -1030,6 +1030,10 @@ const en = Object.freeze({
1030
1030
  'status.opencodeConfig': 'OpenCode config',
1031
1031
  'status.workspaceFile': 'Workspace file',
1032
1032
  'status.configMode': 'Config mode',
1033
+ 'status.kilocodeProvider': 'Kilocode provider',
1034
+ 'status.kilocodeModel': 'Kilocode model',
1035
+ 'status.piProvider': 'Pi provider',
1036
+ 'status.piModel': 'Pi model',
1033
1037
  'status.currentProvider': 'Current provider',
1034
1038
  'status.currentModel': 'Current model',
1035
1039
  'status.quickSwitchProvider': 'Quick switch provider',
@@ -1346,6 +1350,14 @@ const en = Object.freeze({
1346
1350
  'pi.providers.fields.apiKey': 'API Key',
1347
1351
  'pi.providers.fields.models': 'Models',
1348
1352
  'pi.providers.fields.configJson': 'Config JSON',
1353
+ 'pi.providers.json.title': 'Config JSON',
1354
+ 'pi.providers.json.desc': 'Live preview of the final provider record merged from form changes and extras; edits are merged into models.json on save.',
1355
+ 'pi.providers.json.previewTitle': 'Preview',
1356
+ 'pi.providers.json.editTitle': 'Edit JSON',
1357
+ 'pi.files.title': 'File JSON',
1358
+ 'pi.files.desc': 'View and edit Pi settings.json and models.json in full; JSON validity is checked before saving and invalid content is never written.',
1359
+ 'pi.files.settingsTitle': 'settings.json (active settings)',
1360
+ 'pi.files.modelsTitle': 'models.json (providers and models)',
1349
1361
  'pi.providers.models.remove': 'Remove',
1350
1362
  'pi.providers.models.search': 'Search available models…',
1351
1363
  'pi.providers.models.remoteCount': '{count} models available, click to add',
@@ -1033,6 +1033,10 @@ const ja = Object.freeze({
1033
1033
  'status.opencodeConfig': 'OpenCode 設定',
1034
1034
  'status.workspaceFile': 'ワークスペースファイル',
1035
1035
  'status.configMode': '設定モード',
1036
+ 'status.kilocodeProvider': 'Kilocode プロバイダー',
1037
+ 'status.kilocodeModel': 'Kilocode モデル',
1038
+ 'status.piProvider': 'Pi プロバイダー',
1039
+ 'status.piModel': 'Pi モデル',
1036
1040
  'status.currentProvider': '現在の Provider',
1037
1041
  'status.currentModel': '現在のモデル',
1038
1042
  'status.quickSwitchProvider': 'プロバイダーをクイック切替',
@@ -1347,6 +1351,14 @@ const ja = Object.freeze({
1347
1351
  'pi.providers.fields.apiKey': 'API Key',
1348
1352
  'pi.providers.fields.models': 'モデル',
1349
1353
  'pi.providers.fields.configJson': '設定 JSON',
1354
+ 'pi.providers.json.title': '設定 JSON',
1355
+ 'pi.providers.json.desc': 'フォーム変更と拡張フィールドを統合した最終レコードをリアルタイムにプレビュー。保存時に models.json へ統合します。',
1356
+ 'pi.providers.json.previewTitle': 'プレビュー',
1357
+ 'pi.providers.json.editTitle': 'JSON を編集',
1358
+ 'pi.files.title': 'ファイル JSON',
1359
+ 'pi.files.desc': 'Pi の settings.json と models.json を全文表示して直接編集します。保存前に JSON の正当性を検証し、不正な内容は書き込みません。',
1360
+ 'pi.files.settingsTitle': 'settings.json(有効な設定)',
1361
+ 'pi.files.modelsTitle': 'models.json(プロバイダーとモデル)',
1350
1362
  'pi.providers.models.remove': '削除',
1351
1363
  'pi.providers.models.search': '利用可能なモデルを検索…',
1352
1364
  'pi.providers.models.remoteCount': '{count} 個のモデルを取得しました。クリックで追加',
@@ -1128,6 +1128,10 @@ const vi = Object.freeze({
1128
1128
  'status.openclawConfig': 'Cấu hình OpenClaw',
1129
1129
  'status.workspaceFile': 'File không gian làm việc',
1130
1130
  'status.configMode': 'Chế độ cấu hình',
1131
+ 'status.kilocodeProvider': 'Nhà cung cấp Kilocode',
1132
+ 'status.kilocodeModel': 'Mô hình Kilocode',
1133
+ 'status.piProvider': 'Nhà cung cấp Pi',
1134
+ 'status.piModel': 'Mô hình Pi',
1131
1135
  'status.currentProvider': 'Nhà cung cấp hiện tại',
1132
1136
  'status.currentModel': 'Model hiện tại',
1133
1137
  'status.quickSwitchProvider': 'Chuyển nhanh nhà cung cấp',
@@ -1315,6 +1319,14 @@ const vi = Object.freeze({
1315
1319
  'pi.providers.fields.apiKey': 'API Key',
1316
1320
  'pi.providers.fields.models': 'Mô hình',
1317
1321
  'pi.providers.fields.configJson': 'Cấu hình JSON',
1322
+ 'pi.providers.json.title': 'Cấu hình JSON',
1323
+ 'pi.providers.json.desc': 'Xem trước bản ghi cuối từ thay đổi biểu mẫu; các chỉnh sửa được gộp vào models.json khi lưu.',
1324
+ 'pi.providers.json.previewTitle': 'Xem trước',
1325
+ 'pi.providers.json.editTitle': 'Chỉnh sửa JSON',
1326
+ 'pi.files.title': 'JSON tập tin',
1327
+ 'pi.files.desc': 'Hiển thị đầy đủ và chỉnh sửa trực tiếp settings.json và models.json của Pi; kiểm tra tính hợp lệ của JSON trước khi lưu, nội dung không hợp lệ sẽ không được ghi.',
1328
+ 'pi.files.settingsTitle': 'settings.json (cài đặt đang dùng)',
1329
+ 'pi.files.modelsTitle': 'models.json (nhà cung cấp và mô hình)',
1318
1330
  'pi.providers.models.remove': 'Xóa',
1319
1331
  'pi.providers.models.search': 'Tìm kiếm mô hình khả dụng…',
1320
1332
  'pi.providers.models.remoteCount': 'Đã lấy {count} mô hình, bấm để thêm',
@@ -1029,6 +1029,10 @@ const zhTw = Object.freeze({
1029
1029
  'status.opencodeConfig': 'OpenCode 設定',
1030
1030
  'status.workspaceFile': '工作區文件',
1031
1031
  'status.configMode': '設定模式',
1032
+ 'status.kilocodeProvider': 'Kilocode 供應商',
1033
+ 'status.kilocodeModel': 'Kilocode 模型',
1034
+ 'status.piProvider': 'Pi 供應商',
1035
+ 'status.piModel': 'Pi 模型',
1032
1036
  'status.currentProvider': '目前 Provider',
1033
1037
  'status.currentModel': '目前模型',
1034
1038
  'status.quickSwitchProvider': '快速切換提供商',
@@ -1344,6 +1348,14 @@ const zhTw = Object.freeze({
1344
1348
  'pi.providers.fields.apiKey': 'API Key',
1345
1349
  'pi.providers.fields.models': '模型',
1346
1350
  'pi.providers.fields.configJson': '組態 JSON',
1351
+ 'pi.providers.json.title': '設定 JSON',
1352
+ 'pi.providers.json.desc': '即時預覽表單異動合併後的最終紀錄;儲存時併入 models.json。',
1353
+ 'pi.providers.json.previewTitle': '預覽',
1354
+ 'pi.providers.json.editTitle': '編輯 JSON',
1355
+ 'pi.files.title': '檔案 JSON',
1356
+ 'pi.files.desc': '完整展示並直接編輯 Pi 的 settings.json 與 models.json;儲存前校驗 JSON 合法性,非法內容不會寫入。',
1357
+ 'pi.files.settingsTitle': 'settings.json(生效設定)',
1358
+ 'pi.files.modelsTitle': 'models.json(供應商與模型)',
1347
1359
  'pi.providers.models.remove': '刪除',
1348
1360
  'pi.providers.models.search': '搜尋可用模型…',
1349
1361
  'pi.providers.models.remoteCount': '已取得 {count} 個可用模型,點擊新增',
@@ -1029,6 +1029,10 @@ const zh = Object.freeze({
1029
1029
  'status.opencodeConfig': 'OpenCode 配置',
1030
1030
  'status.workspaceFile': '工作区文件',
1031
1031
  'status.configMode': '配置模式',
1032
+ 'status.kilocodeProvider': 'Kilocode 提供商',
1033
+ 'status.kilocodeModel': 'Kilocode 模型',
1034
+ 'status.piProvider': 'Pi 提供商',
1035
+ 'status.piModel': 'Pi 模型',
1032
1036
  'status.currentProvider': '当前 Provider',
1033
1037
  'status.currentModel': '当前模型',
1034
1038
  'status.quickSwitchProvider': '快速切换提供商',
@@ -1344,6 +1348,14 @@ const zh = Object.freeze({
1344
1348
  'pi.providers.fields.apiKey': 'API Key',
1345
1349
  'pi.providers.fields.models': '模型',
1346
1350
  'pi.providers.fields.configJson': '配置 JSON',
1351
+ 'pi.providers.json.title': '配置 JSON',
1352
+ 'pi.providers.json.desc': '实时预览表单改动合并后的最终记录;保存时并入 models.json。',
1353
+ 'pi.providers.json.previewTitle': '预览',
1354
+ 'pi.providers.json.editTitle': '编辑 JSON',
1355
+ 'pi.files.title': '文件 JSON',
1356
+ 'pi.files.desc': '完整展示并直接编辑 Pi 的 settings.json 与 models.json;保存前校验 JSON 合法性,非法内容不会写入。',
1357
+ 'pi.files.settingsTitle': 'settings.json(生效设置)',
1358
+ 'pi.files.modelsTitle': 'models.json(供应商与模型)',
1347
1359
  'pi.providers.models.remove': '删除',
1348
1360
  'pi.providers.models.search': '搜索可用模型…',
1349
1361
  'pi.providers.models.remoteCount': '已拉取 {count} 个可用模型,点击添加',
@@ -471,6 +471,26 @@
471
471
  <span class="value">{{ opencodeConfigPath || t('common.notSelected') }}</span>
472
472
  </div>
473
473
  </template>
474
+ <template v-else-if="configMode === 'kilocode'">
475
+ <div class="status-chip">
476
+ <span class="label">{{ t('status.kilocodeProvider') }}</span>
477
+ <span class="value">{{ kilocodeProvider || t('common.notSelected') }}</span>
478
+ </div>
479
+ <div class="status-chip">
480
+ <span class="label">{{ t('status.kilocodeModel') }}</span>
481
+ <span class="value">{{ kilocodeModel || t('common.notSelected') }}</span>
482
+ </div>
483
+ </template>
484
+ <template v-else-if="configMode === 'pi'">
485
+ <div class="status-chip">
486
+ <span class="label">{{ t('status.piProvider') }}</span>
487
+ <span class="value">{{ piActiveProvider || t('common.notSelected') }}</span>
488
+ </div>
489
+ <div class="status-chip">
490
+ <span class="label">{{ t('status.piModel') }}</span>
491
+ <span class="value">{{ piActiveModel || t('common.notSelected') }}</span>
492
+ </div>
493
+ </template>
474
494
  <template v-else>
475
495
  <div class="status-chip">
476
496
  <span class="label">{{ t('status.configMode') }}</span>
@@ -117,6 +117,25 @@
117
117
  <div class="settings-tab-actions">
118
118
  <button type="button" class="btn-tool" @click="confirmDeletePiProvider" :disabled="!isToolConfigWriteAllowed('pi') || piSaving">{{ t('pi.providers.delete') }}</button>
119
119
  </div>
120
+
121
+ <section class="selector-section">
122
+ <div class="selector-header">
123
+ <span class="selector-title">{{ t('pi.providers.json.title') }}</span>
124
+ </div>
125
+ <p class="config-template-hint">{{ t('pi.providers.json.desc') }}</p>
126
+ <label class="form-label">{{ t('pi.providers.json.previewTitle') }}</label>
127
+ <pre class="config-json-preview">{{ piProviderPreviewJson() }}</pre>
128
+ <label class="form-label">{{ t('pi.providers.json.editTitle') }}</label>
129
+ <textarea
130
+ class="template-textarea config-json-editor"
131
+ v-model="editingPiProvider.form.configJsonDraft"
132
+ spellcheck="false"
133
+ autocomplete="off"
134
+ :disabled="!isToolConfigWriteAllowed('pi')"
135
+ @input="piEditorJsonDraftInput"
136
+ placeholder="{}"></textarea>
137
+ <p v-if="piEditorJsonError" class="config-template-hint error-text">{{ piEditorJsonError }}</p>
138
+ </section>
120
139
  </section>
121
140
 
122
141
  <section class="selector-section" v-if="showAddPiProviderModal">
@@ -159,6 +178,38 @@
159
178
  </div>
160
179
  </div>
161
180
  </section>
181
+ <section class="selector-section">
182
+ <div class="selector-header">
183
+ <span class="selector-title">{{ t('pi.files.title') }}</span>
184
+ </div>
185
+ <div class="config-template-hint">{{ t('pi.files.desc') }}</div>
186
+ <label class="form-label">{{ t('pi.files.settingsTitle') }}</label>
187
+ <textarea
188
+ class="template-textarea config-json-editor"
189
+ v-model="piSettingsJsonDraft"
190
+ spellcheck="false"
191
+ autocomplete="off"
192
+ :disabled="!isToolConfigWriteAllowed('pi')"
193
+ @input="piSettingsJsonInput"
194
+ placeholder="{}"></textarea>
195
+ <p v-if="piSettingsJsonError" class="config-template-hint error-text">{{ piSettingsJsonError }}</p>
196
+ <div class="settings-tab-actions">
197
+ <button type="button" class="btn-tool" @click="savePiSettingsJson" :disabled="!isToolConfigWriteAllowed('pi') || piSaving || piFileJsonSaving || !!piSettingsJsonError">{{ t('common.save') }}</button>
198
+ </div>
199
+ <label class="form-label">{{ t('pi.files.modelsTitle') }}</label>
200
+ <textarea
201
+ class="template-textarea config-json-editor"
202
+ v-model="piModelsJsonDraft"
203
+ spellcheck="false"
204
+ autocomplete="off"
205
+ :disabled="!isToolConfigWriteAllowed('pi')"
206
+ @input="piModelsJsonInput"
207
+ placeholder="{}"></textarea>
208
+ <p v-if="piModelsJsonError" class="config-template-hint error-text">{{ piModelsJsonError }}</p>
209
+ <div class="settings-tab-actions">
210
+ <button type="button" class="btn-tool" @click="savePiModelsJson" :disabled="!isToolConfigWriteAllowed('pi') || piSaving || piFileJsonSaving || !!piModelsJsonError">{{ t('common.save') }}</button>
211
+ </div>
212
+ </section>
162
213
  <div v-if="!isToolConfigWriteAllowed('pi')" class="tool-config-write-overlay">
163
214
  <div class="tool-config-write-overlay-card">
164
215
  <div class="tool-config-write-overlay-title">{{ t('toolConfig.pi.lockedTitle') }}</div>
@@ -661,13 +661,35 @@ return function render(_ctx, _cache) {
661
661
  _createElementVNode("span", { class: "value" }, _toDisplayString(_ctx.opencodeConfigPath || _ctx.t('common.notSelected')), 1 /* TEXT */)
662
662
  ])
663
663
  ], 64 /* STABLE_FRAGMENT */))
664
- : (_openBlock(), _createElementBlock("div", {
665
- key: 4,
666
- class: "status-chip"
667
- }, [
668
- _createElementVNode("span", { class: "label" }, _toDisplayString(_ctx.t('status.configMode')), 1 /* TEXT */),
669
- _createElementVNode("span", { class: "value" }, _toDisplayString(_ctx.t('common.notSelected')), 1 /* TEXT */)
670
- ]))
664
+ : (_ctx.configMode === 'kilocode')
665
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 4 }, [
666
+ _createElementVNode("div", { class: "status-chip" }, [
667
+ _createElementVNode("span", { class: "label" }, _toDisplayString(_ctx.t('status.kilocodeProvider')), 1 /* TEXT */),
668
+ _createElementVNode("span", { class: "value" }, _toDisplayString(_ctx.kilocodeProvider || _ctx.t('common.notSelected')), 1 /* TEXT */)
669
+ ]),
670
+ _createElementVNode("div", { class: "status-chip" }, [
671
+ _createElementVNode("span", { class: "label" }, _toDisplayString(_ctx.t('status.kilocodeModel')), 1 /* TEXT */),
672
+ _createElementVNode("span", { class: "value" }, _toDisplayString(_ctx.kilocodeModel || _ctx.t('common.notSelected')), 1 /* TEXT */)
673
+ ])
674
+ ], 64 /* STABLE_FRAGMENT */))
675
+ : (_ctx.configMode === 'pi')
676
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 5 }, [
677
+ _createElementVNode("div", { class: "status-chip" }, [
678
+ _createElementVNode("span", { class: "label" }, _toDisplayString(_ctx.t('status.piProvider')), 1 /* TEXT */),
679
+ _createElementVNode("span", { class: "value" }, _toDisplayString(_ctx.piActiveProvider || _ctx.t('common.notSelected')), 1 /* TEXT */)
680
+ ]),
681
+ _createElementVNode("div", { class: "status-chip" }, [
682
+ _createElementVNode("span", { class: "label" }, _toDisplayString(_ctx.t('status.piModel')), 1 /* TEXT */),
683
+ _createElementVNode("span", { class: "value" }, _toDisplayString(_ctx.piActiveModel || _ctx.t('common.notSelected')), 1 /* TEXT */)
684
+ ])
685
+ ], 64 /* STABLE_FRAGMENT */))
686
+ : (_openBlock(), _createElementBlock("div", {
687
+ key: 6,
688
+ class: "status-chip"
689
+ }, [
690
+ _createElementVNode("span", { class: "label" }, _toDisplayString(_ctx.t('status.configMode')), 1 /* TEXT */),
691
+ _createElementVNode("span", { class: "value" }, _toDisplayString(_ctx.t('common.notSelected')), 1 /* TEXT */)
692
+ ]))
671
693
  ]))
672
694
  : (!_ctx.sessionStandalone && _ctx.mainTab === 'sessions')
673
695
  ? (_openBlock(), _createElementBlock("div", {
@@ -3458,6 +3480,32 @@ return function render(_ctx, _cache) {
3458
3480
  onClick: _ctx.confirmDeletePiProvider,
3459
3481
  disabled: !_ctx.isToolConfigWriteAllowed('pi') || _ctx.piSaving
3460
3482
  }, _toDisplayString(_ctx.t('pi.providers.delete')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])
3483
+ ]),
3484
+ _createElementVNode("section", { class: "selector-section" }, [
3485
+ _createElementVNode("div", { class: "selector-header" }, [
3486
+ _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('pi.providers.json.title')), 1 /* TEXT */)
3487
+ ]),
3488
+ _createElementVNode("p", { class: "config-template-hint" }, _toDisplayString(_ctx.t('pi.providers.json.desc')), 1 /* TEXT */),
3489
+ _createElementVNode("label", { class: "form-label" }, _toDisplayString(_ctx.t('pi.providers.json.previewTitle')), 1 /* TEXT */),
3490
+ _createElementVNode("pre", { class: "config-json-preview" }, _toDisplayString(_ctx.piProviderPreviewJson()), 1 /* TEXT */),
3491
+ _createElementVNode("label", { class: "form-label" }, _toDisplayString(_ctx.t('pi.providers.json.editTitle')), 1 /* TEXT */),
3492
+ _withDirectives(_createElementVNode("textarea", {
3493
+ class: "template-textarea config-json-editor",
3494
+ "onUpdate:modelValue": $event => ((_ctx.editingPiProvider.form.configJsonDraft) = $event),
3495
+ spellcheck: "false",
3496
+ autocomplete: "off",
3497
+ disabled: !_ctx.isToolConfigWriteAllowed('pi'),
3498
+ onInput: _ctx.piEditorJsonDraftInput,
3499
+ placeholder: "{}"
3500
+ }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "disabled", "onInput"]), [
3501
+ [_vModelText, _ctx.editingPiProvider.form.configJsonDraft]
3502
+ ]),
3503
+ (_ctx.piEditorJsonError)
3504
+ ? (_openBlock(), _createElementBlock("p", {
3505
+ key: 0,
3506
+ class: "config-template-hint error-text"
3507
+ }, _toDisplayString(_ctx.piEditorJsonError), 1 /* TEXT */))
3508
+ : _createCommentVNode("v-if", true)
3461
3509
  ])
3462
3510
  ]))
3463
3511
  : _createCommentVNode("v-if", true),
@@ -3572,6 +3620,64 @@ return function render(_ctx, _cache) {
3572
3620
  ])
3573
3621
  ]))
3574
3622
  : _createCommentVNode("v-if", true),
3623
+ _createElementVNode("section", { class: "selector-section" }, [
3624
+ _createElementVNode("div", { class: "selector-header" }, [
3625
+ _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('pi.files.title')), 1 /* TEXT */)
3626
+ ]),
3627
+ _createElementVNode("div", { class: "config-template-hint" }, _toDisplayString(_ctx.t('pi.files.desc')), 1 /* TEXT */),
3628
+ _createElementVNode("label", { class: "form-label" }, _toDisplayString(_ctx.t('pi.files.settingsTitle')), 1 /* TEXT */),
3629
+ _withDirectives(_createElementVNode("textarea", {
3630
+ class: "template-textarea config-json-editor",
3631
+ "onUpdate:modelValue": $event => ((_ctx.piSettingsJsonDraft) = $event),
3632
+ spellcheck: "false",
3633
+ autocomplete: "off",
3634
+ disabled: !_ctx.isToolConfigWriteAllowed('pi'),
3635
+ onInput: _ctx.piSettingsJsonInput,
3636
+ placeholder: "{}"
3637
+ }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "disabled", "onInput"]), [
3638
+ [_vModelText, _ctx.piSettingsJsonDraft]
3639
+ ]),
3640
+ (_ctx.piSettingsJsonError)
3641
+ ? (_openBlock(), _createElementBlock("p", {
3642
+ key: 0,
3643
+ class: "config-template-hint error-text"
3644
+ }, _toDisplayString(_ctx.piSettingsJsonError), 1 /* TEXT */))
3645
+ : _createCommentVNode("v-if", true),
3646
+ _createElementVNode("div", { class: "settings-tab-actions" }, [
3647
+ _createElementVNode("button", {
3648
+ type: "button",
3649
+ class: "btn-tool",
3650
+ onClick: _ctx.savePiSettingsJson,
3651
+ disabled: !_ctx.isToolConfigWriteAllowed('pi') || _ctx.piSaving || _ctx.piFileJsonSaving || !!_ctx.piSettingsJsonError
3652
+ }, _toDisplayString(_ctx.t('common.save')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])
3653
+ ]),
3654
+ _createElementVNode("label", { class: "form-label" }, _toDisplayString(_ctx.t('pi.files.modelsTitle')), 1 /* TEXT */),
3655
+ _withDirectives(_createElementVNode("textarea", {
3656
+ class: "template-textarea config-json-editor",
3657
+ "onUpdate:modelValue": $event => ((_ctx.piModelsJsonDraft) = $event),
3658
+ spellcheck: "false",
3659
+ autocomplete: "off",
3660
+ disabled: !_ctx.isToolConfigWriteAllowed('pi'),
3661
+ onInput: _ctx.piModelsJsonInput,
3662
+ placeholder: "{}"
3663
+ }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "disabled", "onInput"]), [
3664
+ [_vModelText, _ctx.piModelsJsonDraft]
3665
+ ]),
3666
+ (_ctx.piModelsJsonError)
3667
+ ? (_openBlock(), _createElementBlock("p", {
3668
+ key: 1,
3669
+ class: "config-template-hint error-text"
3670
+ }, _toDisplayString(_ctx.piModelsJsonError), 1 /* TEXT */))
3671
+ : _createCommentVNode("v-if", true),
3672
+ _createElementVNode("div", { class: "settings-tab-actions" }, [
3673
+ _createElementVNode("button", {
3674
+ type: "button",
3675
+ class: "btn-tool",
3676
+ onClick: _ctx.savePiModelsJson,
3677
+ disabled: !_ctx.isToolConfigWriteAllowed('pi') || _ctx.piSaving || _ctx.piFileJsonSaving || !!_ctx.piModelsJsonError
3678
+ }, _toDisplayString(_ctx.t('common.save')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])
3679
+ ])
3680
+ ]),
3575
3681
  (!_ctx.isToolConfigWriteAllowed('pi'))
3576
3682
  ? (_openBlock(), _createElementBlock("div", {
3577
3683
  key: 2,
@@ -352,6 +352,28 @@
352
352
  margin-top: 2px;
353
353
  }
354
354
 
355
+ .config-json-preview {
356
+ width: 100%;
357
+ max-height: 240px;
358
+ overflow: auto;
359
+ margin: 0 0 10px;
360
+ padding: 12px;
361
+ border: 1px solid var(--color-border);
362
+ border-radius: var(--radius-md);
363
+ background: rgba(0, 0, 0, 0.04);
364
+ color: var(--color-text-secondary);
365
+ font-family: var(--font-mono);
366
+ font-size: 12px;
367
+ line-height: 1.55;
368
+ white-space: pre;
369
+ box-shadow: var(--shadow-subtle);
370
+ }
371
+
372
+ .config-json-editor {
373
+ min-height: 180px;
374
+ margin-bottom: 8px;
375
+ }
376
+
355
377
  /* ============================================
356
378
  按钮
357
379
  ============================================ */