codexmate 0.1.15 → 0.1.17

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/skills.js CHANGED
@@ -24,6 +24,7 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
24
24
  const CODEX_SKILLS_DIR = path.join(CONFIG_DIR, 'skills');
25
25
  const CLAUDE_SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
26
26
  const AGENTS_SKILLS_DIR = path.join(os.homedir(), '.agents', 'skills');
27
+ const PI_SKILLS_DIR = path.join(os.homedir(), '.pi', 'agent', 'skills');
27
28
 
28
29
  const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
29
30
  const MAX_SKILLS_ZIP_UPLOAD_SIZE = 20 * 1024 * 1024;
@@ -72,9 +73,24 @@ function getClaudeSkillsDir() {
72
73
  return resolveExistingDir([homeConfigDir], CLAUDE_SKILLS_DIR);
73
74
  }
74
75
 
76
+ function getPiSkillsDir() {
77
+ const joinPath = (basePath, ...segments) => {
78
+ const base = typeof basePath === 'string' ? basePath.trim() : '';
79
+ const pathApi = base.includes('/') && !base.includes('\\') && path.posix ? path.posix : path;
80
+ return pathApi.join(base, ...segments);
81
+ };
82
+ const envPiDir = typeof process.env.PI_CODING_AGENT_DIR === 'string' ? process.env.PI_CODING_AGENT_DIR.trim() : '';
83
+ if (envPiDir) {
84
+ const target = joinPath(envPiDir, 'skills');
85
+ return resolveExistingDir([target], target);
86
+ }
87
+ return resolveExistingDir([PI_SKILLS_DIR], PI_SKILLS_DIR);
88
+ }
89
+
75
90
  const SKILL_TARGETS = Object.freeze([
76
91
  Object.freeze({ app: 'codex', label: 'Codex', dir: getCodexSkillsDir() }),
77
- Object.freeze({ app: 'claude', label: 'Claude Code', dir: getClaudeSkillsDir() })
92
+ Object.freeze({ app: 'claude', label: 'Claude Code', dir: getClaudeSkillsDir() }),
93
+ Object.freeze({ app: 'pi', label: 'Pi', dir: getPiSkillsDir() })
78
94
  ]);
79
95
 
80
96
  const SKILL_IMPORT_SOURCES = Object.freeze([
@@ -1119,6 +1135,7 @@ module.exports = {
1119
1135
  SKILL_IMPORT_SOURCES,
1120
1136
  getCodexSkillsDir,
1121
1137
  getClaudeSkillsDir,
1138
+ getPiSkillsDir,
1122
1139
  normalizeSkillTargetApp,
1123
1140
  getSkillTargetByApp,
1124
1141
  resolveSkillTarget,
package/cli.js CHANGED
@@ -1094,7 +1094,9 @@ function normalizeWebUiPreferences(value = {}) {
1094
1094
  mainTab: normalizeMainTabPreference(navigation.mainTab),
1095
1095
  configMode: normalizeConfigModePreference(navigation.configMode),
1096
1096
  settingsTab: normalizeSettingsTabPreference(navigation.settingsTab),
1097
- skillsTargetApp: navigation.skillsTargetApp === 'claude' ? 'claude' : 'codex',
1097
+ skillsTargetApp: navigation.skillsTargetApp === 'claude' || navigation.skillsTargetApp === 'pi'
1098
+ ? navigation.skillsTargetApp
1099
+ : 'codex',
1098
1100
  promptTemplatesMode: navigation.promptTemplatesMode === 'manage' ? 'manage' : 'compose'
1099
1101
  }
1100
1102
  };
@@ -1222,28 +1224,35 @@ function readPiModels(params = {}) {
1222
1224
  const dir = getPiAgentDir();
1223
1225
  const filePath = path.join(dir, 'models.json');
1224
1226
  if (!fs.existsSync(filePath)) {
1225
- return { providers: {} };
1227
+ return { providers: {}, file: {} };
1226
1228
  }
1227
1229
  const raw = fs.readFileSync(filePath, 'utf-8');
1228
1230
  const data = JSON.parse(raw);
1229
1231
  const providers = (data && typeof data === 'object' && data.providers && typeof data.providers === 'object')
1230
1232
  ? data.providers
1231
1233
  : {};
1232
- return { providers };
1234
+ const file = data && typeof data === 'object' && !Array.isArray(data) ? data : { providers };
1235
+ return { providers, file };
1233
1236
  } catch (e) {
1234
1237
  return { error: e && e.message ? e.message : '读取 Pi models.json 失败' };
1235
1238
  }
1236
1239
  }
1237
1240
 
1238
1241
  function writePiModels(params = {}) {
1242
+ const file = params && typeof params.file === 'object' && params.file !== null && !Array.isArray(params.file) ? params.file : null;
1239
1243
  const providers = params && typeof params.providers === 'object' ? params.providers : null;
1240
- if (providers === null) {
1241
- return { error: 'providers 不能为空' };
1244
+ if (!file && providers === null) {
1245
+ return { error: '缺少可写入的内容' };
1242
1246
  }
1243
1247
  try {
1244
1248
  const dir = getPiAgentDir();
1245
1249
  fs.mkdirSync(dir, { recursive: true });
1246
1250
  const filePath = path.join(dir, 'models.json');
1251
+ if (file) {
1252
+ historyBackup('pi-models', filePath);
1253
+ fs.writeFileSync(filePath, JSON.stringify(file, null, 2), 'utf-8');
1254
+ return { success: true };
1255
+ }
1247
1256
  let existing = {};
1248
1257
  if (fs.existsSync(filePath)) {
1249
1258
  try {
@@ -1294,16 +1303,22 @@ function readPiSettings(params = {}) {
1294
1303
  }
1295
1304
 
1296
1305
  function writePiSettings(params = {}) {
1306
+ const fullSettings = params && typeof params.settings === 'object' && params.settings !== null && !Array.isArray(params.settings) ? params.settings : null;
1297
1307
  const updates = {};
1298
1308
  if (typeof (params && params.defaultProvider) === 'string') updates.defaultProvider = params.defaultProvider;
1299
1309
  if (typeof (params && params.defaultModel) === 'string') updates.defaultModel = params.defaultModel;
1300
- if (Object.keys(updates).length === 0) {
1310
+ if (!fullSettings && Object.keys(updates).length === 0) {
1301
1311
  return { error: '缺少可写入的设置项' };
1302
1312
  }
1303
1313
  try {
1304
1314
  const dir = getPiAgentDir();
1305
1315
  fs.mkdirSync(dir, { recursive: true });
1306
1316
  const filePath = path.join(dir, 'settings.json');
1317
+ if (fullSettings) {
1318
+ historyBackup('pi-settings', filePath);
1319
+ fs.writeFileSync(filePath, JSON.stringify(fullSettings, null, 2), 'utf-8');
1320
+ return { success: true, settings: fullSettings };
1321
+ }
1307
1322
  let existing = {};
1308
1323
  if (fs.existsSync(filePath)) {
1309
1324
  try {
@@ -1321,6 +1336,43 @@ function writePiSettings(params = {}) {
1321
1336
  }
1322
1337
  }
1323
1338
 
1339
+ function applyPiConfigHistory(params = {}) {
1340
+ const target = typeof params.target === 'string' ? params.target.trim() : '';
1341
+ if (target !== 'settings' && target !== 'models') {
1342
+ return { error: '无效的历史记录目标' };
1343
+ }
1344
+ const bucket = target === 'settings' ? 'pi-settings' : 'pi-models';
1345
+ const fileName = target === 'settings' ? 'settings.json' : 'models.json';
1346
+ let entry;
1347
+ try {
1348
+ entry = readPromptHistory(bucket, String(params.id || ''));
1349
+ } catch (_) {
1350
+ return { error: '历史记录不存在或读取失败' };
1351
+ }
1352
+ const content = entry && typeof entry.content === 'string' ? entry.content : '';
1353
+ let parsed = null;
1354
+ if (content) {
1355
+ try {
1356
+ parsed = JSON.parse(content);
1357
+ } catch (_) {
1358
+ parsed = null;
1359
+ }
1360
+ }
1361
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
1362
+ return { error: '历史记录内容不是有效的 JSON 对象' };
1363
+ }
1364
+ try {
1365
+ const filePath = path.join(getPiAgentDir(), fileName);
1366
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1367
+ historyBackup(bucket, filePath);
1368
+ fs.writeFileSync(filePath, JSON.stringify(parsed, null, 2), 'utf-8');
1369
+ } catch (e) {
1370
+ return { error: (e && e.message) || '应用历史记录失败' };
1371
+ }
1372
+ if (target === 'settings') return { success: true, settings: parsed };
1373
+ return { success: true, file: parsed };
1374
+ }
1375
+
1324
1376
  function setToolConfigPermission(params = {}) {
1325
1377
  const target = normalizeToolConfigTarget(params && params.target);
1326
1378
  if (!target) return { error: '未知配置对象' };
@@ -13304,6 +13356,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13304
13356
  result = writePiSettings(params || {});
13305
13357
  break;
13306
13358
  }
13359
+ case 'apply-pi-config-history': {
13360
+ result = applyPiConfigHistory(params || {});
13361
+ break;
13362
+ }
13307
13363
  case 'fetch-pi-remote-models': {
13308
13364
  result = await fetchPiRemoteModels(params || {});
13309
13365
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -167,6 +167,11 @@ export function createPluginsComputed() {
167
167
  .map((meta) => localizePluginMeta(meta, this.t));
168
168
  },
169
169
 
170
+ pluginsSidebarVisible() {
171
+ const catalog = Array.isArray(this.pluginsCatalog) ? this.pluginsCatalog : [];
172
+ return catalog.length > 1;
173
+ },
174
+
170
175
  pluginsActiveMeta() {
171
176
  const id = typeof this.pluginsActiveId === 'string' ? this.pluginsActiveId.trim() : '';
172
177
  const entry = pluginsRegistry.find((item) => item && item.id === id) || null;
package/web-ui/app.js CHANGED
@@ -148,6 +148,18 @@ document.addEventListener('DOMContentLoaded', () => {
148
148
  piActiveProvider: '',
149
149
  piActiveModel: '',
150
150
  piShowKey: false,
151
+ piSettingsJsonDraft: '',
152
+ piSettingsJsonError: '',
153
+ piModelsJsonDraft: '',
154
+ piModelsJsonError: '',
155
+ piFileJsonSaving: false,
156
+ piHistoryTarget: '',
157
+ piHistoryLoading: false,
158
+ piHistoryItems: [],
159
+ piHistoryPreviewId: '',
160
+ piHistoryPreviewContent: '',
161
+ piHistoryError: '',
162
+ piHistoryApplying: false,
151
163
  // Plugins
152
164
  pluginsActiveId: 'prompt-templates',
153
165
  pluginsLoading: false,
@@ -896,26 +896,97 @@ export function createSessionComputed() {
896
896
  };
897
897
  },
898
898
 
899
- usageHeroMainValue() {
899
+ usageKpiCards() {
900
900
  const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
901
901
  ? this.sessionUsageCharts.summary
902
902
  : null;
903
- if (!summary) return '0';
904
- return formatCompactUsageSummaryNumber(summary.totalTokens || 0);
903
+ if (!summary) return [];
904
+ const t = typeof this.t === 'function' ? this.t : null;
905
+ const totalTokens = summary.totalTokens || 0;
906
+ const activeDays = summary.activeDays || 0;
907
+ const dailyAvgTokens = activeDays > 0 ? Math.round(totalTokens / activeDays) : 0;
908
+ const noneLabel = t ? t('common.none') : '暂无';
909
+ const busiestDay = summary.busiestDay;
910
+ return [
911
+ {
912
+ key: 'tokens',
913
+ label: t ? t('usage.kpi.tokens') : '总 Token',
914
+ value: formatCompactUsageSummaryNumber(totalTokens),
915
+ title: formatUsageSummaryNumber(totalTokens),
916
+ delta: this.usageHeroDelta,
917
+ deltaClass: this.usageHeroDeltaClass
918
+ },
919
+ {
920
+ key: 'sessions',
921
+ label: t ? t('usage.kpi.sessions') : '会话数',
922
+ value: formatUsageSummaryNumber(summary.totalSessions || 0)
923
+ },
924
+ {
925
+ key: 'messages',
926
+ label: t ? t('usage.kpi.messages') : '消息数',
927
+ value: formatUsageSummaryNumber(summary.totalMessages || 0)
928
+ },
929
+ {
930
+ key: 'daily-avg',
931
+ label: t ? t('usage.kpi.dailyAvg') : '日均 Token',
932
+ value: formatCompactUsageSummaryNumber(dailyAvgTokens),
933
+ title: formatUsageSummaryNumber(dailyAvgTokens)
934
+ },
935
+ {
936
+ key: 'busiest-day',
937
+ label: t ? t('usage.kpi.busiestDay') : '最忙日',
938
+ value: busiestDay && busiestDay.totalSessions > 0
939
+ ? `${busiestDay.label} · ${busiestDay.totalSessions}`
940
+ : noneLabel
941
+ }
942
+ ];
905
943
  },
906
-
907
- usageHeroSubLabel() {
908
- const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
909
- ? this.sessionUsageCharts.summary
944
+ usageWaveHeaderSummary() {
945
+ const daily = this.sessionUsageDaily && typeof this.sessionUsageDaily === 'object'
946
+ ? this.sessionUsageDaily
910
947
  : null;
911
- if (!summary) return '';
948
+ const rows = daily && Array.isArray(daily.rows) ? daily.rows : [];
949
+ if (!rows.length) return '';
950
+ let totalTokens = 0;
951
+ let peakRow = rows[0];
952
+ for (const row of rows) {
953
+ const tokens = Number.isFinite(Number(row && row.tokenTotal)) ? Number(row.tokenTotal) : 0;
954
+ totalTokens += tokens;
955
+ if (tokens > (Number(peakRow.tokenTotal) || 0)) peakRow = row;
956
+ }
912
957
  const t = typeof this.t === 'function' ? this.t : null;
913
- const sessionCount = summary.totalSessions || 0;
914
- const rangeLabel = this.sessionsUsageTimeRange === '30d' ? '30天' : (this.sessionsUsageTimeRange === 'all' ? '全部' : '7天');
915
- const rangeText = t ? t('usage.range.' + this.sessionsUsageTimeRange) : rangeLabel;
916
- return `${formatUsageSummaryNumber(sessionCount)} sessions · ${rangeText}`;
958
+ const params = {
959
+ total: formatCompactUsageSummaryNumber(totalTokens),
960
+ peakDay: peakRow.key || '',
961
+ peakValue: formatCompactUsageSummaryNumber(peakRow.tokenTotal || 0)
962
+ };
963
+ return t
964
+ ? t('usage.wave.summary', params)
965
+ : `本期共 ${params.total} · 峰值 ${params.peakDay}(${params.peakValue})`;
917
966
  },
918
967
 
968
+ usageRankedLists() {
969
+ const charts = this.sessionUsageCharts && typeof this.sessionUsageCharts === 'object'
970
+ ? this.sessionUsageCharts
971
+ : {};
972
+ const withBars = (list, pickValue) => {
973
+ const items = Array.isArray(list) ? list : [];
974
+ const max = items.reduce((acc, item) => Math.max(acc, pickValue(item) || 0), 0);
975
+ return items.map((item) => ({
976
+ ...item,
977
+ barPercent: max > 0
978
+ ? Math.max(6, Math.round(((pickValue(item) || 0) / max) * 1000) / 10)
979
+ : 0
980
+ }));
981
+ };
982
+ return {
983
+ topSessions: withBars(charts.topSessionsByMessages, (item) => item && item.messageCount),
984
+ recentSessions: withBars(charts.recentSessions, (item) => item && item.messageCount),
985
+ topPaths: withBars(charts.topPaths, (item) => item && item.count)
986
+ };
987
+ },
988
+
989
+
919
990
  usageHeroDelta() {
920
991
  const range = this.sessionsUsageTimeRange;
921
992
  if (range === 'all') return null;
@@ -108,7 +108,7 @@
108
108
  const mainTab = normalizeMainTab(mainTabSource);
109
109
  const configMode = typeof configModeSource === 'string' ? configModeSource.trim().toLowerCase() : '';
110
110
  const settingsTab = typeof vm.settingsTab === 'string' ? vm.settingsTab.trim().toLowerCase() : 'general';
111
- const skillsTargetApp = typeof vm.skillsTargetApp === 'string' && (vm.skillsTargetApp === 'codex' || vm.skillsTargetApp === 'claude') ? vm.skillsTargetApp : 'codex';
111
+ const skillsTargetApp = typeof vm.skillsTargetApp === 'string' && (vm.skillsTargetApp === 'codex' || vm.skillsTargetApp === 'claude' || vm.skillsTargetApp === 'pi') ? vm.skillsTargetApp : 'codex';
112
112
  const promptTemplatesMode = typeof vm.promptTemplatesMode === 'string' && (vm.promptTemplatesMode === 'compose' || vm.promptTemplatesMode === 'manage') ? vm.promptTemplatesMode : 'compose';
113
113
  const snapshot = {
114
114
  settingsTab: settingsTab === 'data' ? 'data' : 'general',
@@ -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();
@@ -342,6 +343,88 @@ export function createPiConfigMethods({ api: apiClient }) {
342
343
  return [baseUrl, api].filter(Boolean).join(' • ') || 'Piper provider';
343
344
  }
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
+ },
345
428
  piParseEditorJsonDraft() {
346
429
  const provider = this.editingPiProvider;
347
430
  const draft = provider && provider.form ? String(provider.form.configJsonDraft || '').trim() : '';
@@ -392,6 +475,86 @@ export function createPiConfigMethods({ api: apiClient }) {
392
475
  piEditorJsonDraftInput() {
393
476
  const draft = this.piParseEditorJsonDraft();
394
477
  this.piEditorJsonError = draft.ok ? '' : draft.error;
478
+ },
479
+ piHistoryBucketFor(target) {
480
+ return target === 'settings' ? 'pi-settings' : 'pi-models';
481
+ },
482
+ async openPiConfigHistory(target) {
483
+ if (this.piHistoryLoading) return;
484
+ const tgt = target === 'settings' ? 'settings' : (target === 'models' ? 'models' : '');
485
+ if (!tgt) return;
486
+ this.piHistoryTarget = tgt;
487
+ this.piHistoryItems = [];
488
+ this.piHistoryPreviewId = '';
489
+ this.piHistoryPreviewContent = '';
490
+ this.piHistoryError = '';
491
+ this.piHistoryLoading = true;
492
+ try {
493
+ const res = await api('list-prompt-history', { bucket: this.piHistoryBucketFor(tgt) });
494
+ if (res && res.error) {
495
+ this.piHistoryError = res.error;
496
+ return;
497
+ }
498
+ this.piHistoryItems = Array.isArray(res) ? res : [];
499
+ } catch (e) {
500
+ this.piHistoryError = this.t('toast.load.fail');
501
+ } finally {
502
+ this.piHistoryLoading = false;
503
+ }
504
+ },
505
+ closePiConfigHistory() {
506
+ this.piHistoryTarget = '';
507
+ this.piHistoryItems = [];
508
+ this.piHistoryPreviewId = '';
509
+ this.piHistoryPreviewContent = '';
510
+ this.piHistoryError = '';
511
+ this.piHistoryLoading = false;
512
+ },
513
+ async viewPiConfigHistoryItem(item) {
514
+ if (!item || !item.id) return;
515
+ if (this.piHistoryLoading) return;
516
+ if (this.piHistoryPreviewId === item.id && this.piHistoryPreviewContent) return;
517
+ this.piHistoryPreviewId = item.id;
518
+ this.piHistoryPreviewContent = '';
519
+ try {
520
+ const res = await api('get-prompt-history', { bucket: this.piHistoryBucketFor(this.piHistoryTarget), id: item.id });
521
+ if (res && res.error) {
522
+ this.piHistoryError = res.error;
523
+ this.piHistoryPreviewId = '';
524
+ return;
525
+ }
526
+ this.piHistoryPreviewContent = typeof res.content === 'string' ? res.content : '';
527
+ } catch (e) {
528
+ this.piHistoryError = this.t('toast.load.fail');
529
+ this.piHistoryPreviewId = '';
530
+ }
531
+ },
532
+ async applyPiConfigHistory() {
533
+ if (this.piHistoryApplying || this.piHistoryLoading) return;
534
+ if (!this.piHistoryTarget || !this.piHistoryPreviewId) return;
535
+ const confirmed = await this.requestConfirmDialog({
536
+ title: this.t('common.history'),
537
+ message: this.t('pi.history.confirm'),
538
+ confirmText: this.t('confirm.ok'),
539
+ cancelText: this.t('confirm.cancel'),
540
+ danger: true
541
+ });
542
+ if (!confirmed) return;
543
+ this.piHistoryApplying = true;
544
+ try {
545
+ const res = await api('apply-pi-config-history', { target: this.piHistoryTarget, id: this.piHistoryPreviewId });
546
+ if (res && res.error) {
547
+ this.showMessage(res.error, 'error');
548
+ return;
549
+ }
550
+ this.showMessage(this.t('pi.history.applied'), 'success');
551
+ this.closePiConfigHistory();
552
+ await this.loadPiSources();
553
+ } catch (e) {
554
+ this.showMessage(this.t('pi.history.applyFailed'), 'error');
555
+ } finally {
556
+ this.piHistoryApplying = false;
557
+ }
395
558
  }
396
559
  };
397
560
  }
@@ -102,13 +102,13 @@ function normalizeSessionSortMode(value) {
102
102
  }
103
103
 
104
104
  function normalizeNavigationSnapshot(vm, source = {}) {
105
- const currentSkillsTargetApp = vm.skillsTargetApp === 'claude' ? 'claude' : 'codex';
105
+ const currentSkillsTargetApp = vm.skillsTargetApp === 'claude' || vm.skillsTargetApp === 'pi' ? vm.skillsTargetApp : 'codex';
106
106
  const currentPromptTemplatesMode = vm.promptTemplatesMode === 'manage' ? 'manage' : 'compose';
107
107
  return {
108
108
  mainTab: typeof source.mainTab === 'string' ? source.mainTab : vm.mainTab,
109
109
  configMode: typeof source.configMode === 'string' ? source.configMode : vm.configMode,
110
110
  settingsTab: typeof source.settingsTab === 'string' ? source.settingsTab : vm.settingsTab,
111
- skillsTargetApp: source.skillsTargetApp === 'claude' || source.skillsTargetApp === 'codex'
111
+ skillsTargetApp: source.skillsTargetApp === 'claude' || source.skillsTargetApp === 'codex' || source.skillsTargetApp === 'pi'
112
112
  ? source.skillsTargetApp
113
113
  : currentSkillsTargetApp,
114
114
  promptTemplatesMode: source.promptTemplatesMode === 'manage' || source.promptTemplatesMode === 'compose'
@@ -477,7 +477,7 @@ export function createWebUiPreferencesMethods(options = {}) {
477
477
  this.mainTab = nav.mainTab;
478
478
  }
479
479
  }
480
- if (nav.skillsTargetApp === 'codex' || nav.skillsTargetApp === 'claude') this.skillsTargetApp = nav.skillsTargetApp;
480
+ if (nav.skillsTargetApp === 'codex' || nav.skillsTargetApp === 'claude' || nav.skillsTargetApp === 'pi') this.skillsTargetApp = nav.skillsTargetApp;
481
481
  if (nav.promptTemplatesMode === 'compose' || nav.promptTemplatesMode === 'manage') this.promptTemplatesMode = nav.promptTemplatesMode;
482
482
  if (typeof this.saveNavState === 'function') this.saveNavState();
483
483
  }
@@ -827,6 +827,12 @@ const en = Object.freeze({
827
827
  'usage.summary.avgMessagesPerSession': 'Avg messages/session',
828
828
  'usage.summary.busiestDay': 'Busiest day',
829
829
  'usage.summary.busiestHour': 'Peak hour',
830
+ 'usage.kpi.tokens': 'Total tokens',
831
+ 'usage.kpi.sessions': 'Sessions',
832
+ 'usage.kpi.messages': 'Messages',
833
+ 'usage.kpi.dailyAvg': 'Avg tokens / day',
834
+ 'usage.kpi.busiestDay': 'Busiest day',
835
+ 'usage.wave.summary': '{total} total · peak {peakDay} ({peakValue})',
830
836
  'usage.currentSession.title': 'Current session',
831
837
  'usage.currentSession.apiDuration': 'API duration',
832
838
  'usage.currentSession.totalDuration': 'Total duration',
@@ -1030,6 +1036,10 @@ const en = Object.freeze({
1030
1036
  'status.opencodeConfig': 'OpenCode config',
1031
1037
  'status.workspaceFile': 'Workspace file',
1032
1038
  'status.configMode': 'Config mode',
1039
+ 'status.kilocodeProvider': 'Kilocode provider',
1040
+ 'status.kilocodeModel': 'Kilocode model',
1041
+ 'status.piProvider': 'Pi provider',
1042
+ 'status.piModel': 'Pi model',
1033
1043
  'status.currentProvider': 'Current provider',
1034
1044
  'status.currentModel': 'Current model',
1035
1045
  'status.quickSwitchProvider': 'Quick switch provider',
@@ -1350,6 +1360,14 @@ const en = Object.freeze({
1350
1360
  '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.',
1351
1361
  'pi.providers.json.previewTitle': 'Preview',
1352
1362
  'pi.providers.json.editTitle': 'Edit JSON',
1363
+ 'pi.files.title': 'File JSON',
1364
+ '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.',
1365
+ 'pi.files.settingsTitle': 'settings.json (active settings)',
1366
+ 'pi.files.modelsTitle': 'models.json (providers and models)',
1367
+ 'pi.history.apply': 'Apply this record',
1368
+ 'pi.history.applied': 'History record applied',
1369
+ 'pi.history.applyFailed': 'Failed to apply history record',
1370
+ 'pi.history.confirm': 'Apply this history record to the file? The current file content will be snapshotted first.',
1353
1371
  'pi.providers.models.remove': 'Remove',
1354
1372
  'pi.providers.models.search': 'Search available models…',
1355
1373
  'pi.providers.models.remoteCount': '{count} models available, click to add',
@@ -830,6 +830,12 @@ const ja = Object.freeze({
830
830
  'usage.summary.avgMessagesPerSession': 'セッションあたり平均メッセージ',
831
831
  'usage.summary.busiestDay': '最も忙しい日',
832
832
  'usage.summary.busiestHour': 'ピーク時間帯',
833
+ 'usage.kpi.tokens': '合計 Token',
834
+ 'usage.kpi.sessions': 'セッション数',
835
+ 'usage.kpi.messages': 'メッセージ数',
836
+ 'usage.kpi.dailyAvg': '1 日あたり Token',
837
+ 'usage.kpi.busiestDay': '最も忙しい日',
838
+ 'usage.wave.summary': '合計 {total} · ピーク {peakDay}({peakValue})',
833
839
  'usage.currentSession.title': '現在のセッション',
834
840
  'usage.currentSession.apiDuration': 'API 時間',
835
841
  'usage.currentSession.totalDuration': '総時間',
@@ -1033,6 +1039,10 @@ const ja = Object.freeze({
1033
1039
  'status.opencodeConfig': 'OpenCode 設定',
1034
1040
  'status.workspaceFile': 'ワークスペースファイル',
1035
1041
  'status.configMode': '設定モード',
1042
+ 'status.kilocodeProvider': 'Kilocode プロバイダー',
1043
+ 'status.kilocodeModel': 'Kilocode モデル',
1044
+ 'status.piProvider': 'Pi プロバイダー',
1045
+ 'status.piModel': 'Pi モデル',
1036
1046
  'status.currentProvider': '現在の Provider',
1037
1047
  'status.currentModel': '現在のモデル',
1038
1048
  'status.quickSwitchProvider': 'プロバイダーをクイック切替',
@@ -1351,6 +1361,14 @@ const ja = Object.freeze({
1351
1361
  'pi.providers.json.desc': 'フォーム変更と拡張フィールドを統合した最終レコードをリアルタイムにプレビュー。保存時に models.json へ統合します。',
1352
1362
  'pi.providers.json.previewTitle': 'プレビュー',
1353
1363
  'pi.providers.json.editTitle': 'JSON を編集',
1364
+ 'pi.files.title': 'ファイル JSON',
1365
+ 'pi.files.desc': 'Pi の settings.json と models.json を全文表示して直接編集します。保存前に JSON の正当性を検証し、不正な内容は書き込みません。',
1366
+ 'pi.files.settingsTitle': 'settings.json(有効な設定)',
1367
+ 'pi.files.modelsTitle': 'models.json(プロバイダーとモデル)',
1368
+ 'pi.history.apply': 'この履歴を適用',
1369
+ 'pi.history.applied': '履歴を適用しました',
1370
+ 'pi.history.applyFailed': '履歴の適用に失敗しました',
1371
+ 'pi.history.confirm': 'この履歴をファイルに適用しますか?現在の内容は先にスナップショット保存されます。',
1354
1372
  'pi.providers.models.remove': '削除',
1355
1373
  'pi.providers.models.search': '利用可能なモデルを検索…',
1356
1374
  'pi.providers.models.remoteCount': '{count} 個のモデルを取得しました。クリックで追加',