codexmate 0.1.16 → 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
  };
@@ -1247,6 +1249,7 @@ function writePiModels(params = {}) {
1247
1249
  fs.mkdirSync(dir, { recursive: true });
1248
1250
  const filePath = path.join(dir, 'models.json');
1249
1251
  if (file) {
1252
+ historyBackup('pi-models', filePath);
1250
1253
  fs.writeFileSync(filePath, JSON.stringify(file, null, 2), 'utf-8');
1251
1254
  return { success: true };
1252
1255
  }
@@ -1312,6 +1315,7 @@ function writePiSettings(params = {}) {
1312
1315
  fs.mkdirSync(dir, { recursive: true });
1313
1316
  const filePath = path.join(dir, 'settings.json');
1314
1317
  if (fullSettings) {
1318
+ historyBackup('pi-settings', filePath);
1315
1319
  fs.writeFileSync(filePath, JSON.stringify(fullSettings, null, 2), 'utf-8');
1316
1320
  return { success: true, settings: fullSettings };
1317
1321
  }
@@ -1332,6 +1336,43 @@ function writePiSettings(params = {}) {
1332
1336
  }
1333
1337
  }
1334
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
+
1335
1376
  function setToolConfigPermission(params = {}) {
1336
1377
  const target = normalizeToolConfigTarget(params && params.target);
1337
1378
  if (!target) return { error: '未知配置对象' };
@@ -13315,6 +13356,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13315
13356
  result = writePiSettings(params || {});
13316
13357
  break;
13317
13358
  }
13359
+ case 'apply-pi-config-history': {
13360
+ result = applyPiConfigHistory(params || {});
13361
+ break;
13362
+ }
13318
13363
  case 'fetch-pi-remote-models': {
13319
13364
  result = await fetchPiRemoteModels(params || {});
13320
13365
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.16",
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
@@ -153,6 +153,13 @@ document.addEventListener('DOMContentLoaded', () => {
153
153
  piModelsJsonDraft: '',
154
154
  piModelsJsonError: '',
155
155
  piFileJsonSaving: false,
156
+ piHistoryTarget: '',
157
+ piHistoryLoading: false,
158
+ piHistoryItems: [],
159
+ piHistoryPreviewId: '',
160
+ piHistoryPreviewContent: '',
161
+ piHistoryError: '',
162
+ piHistoryApplying: false,
156
163
  // Plugins
157
164
  pluginsActiveId: 'prompt-templates',
158
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',
@@ -475,6 +475,86 @@ export function createPiConfigMethods({ api: apiClient }) {
475
475
  piEditorJsonDraftInput() {
476
476
  const draft = this.piParseEditorJsonDraft();
477
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
+ }
478
558
  }
479
559
  };
480
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',
@@ -1358,6 +1364,10 @@ const en = Object.freeze({
1358
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.',
1359
1365
  'pi.files.settingsTitle': 'settings.json (active settings)',
1360
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.',
1361
1371
  'pi.providers.models.remove': 'Remove',
1362
1372
  'pi.providers.models.search': 'Search available models…',
1363
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': '総時間',
@@ -1359,6 +1365,10 @@ const ja = Object.freeze({
1359
1365
  'pi.files.desc': 'Pi の settings.json と models.json を全文表示して直接編集します。保存前に JSON の正当性を検証し、不正な内容は書き込みません。',
1360
1366
  'pi.files.settingsTitle': 'settings.json(有効な設定)',
1361
1367
  'pi.files.modelsTitle': 'models.json(プロバイダーとモデル)',
1368
+ 'pi.history.apply': 'この履歴を適用',
1369
+ 'pi.history.applied': '履歴を適用しました',
1370
+ 'pi.history.applyFailed': '履歴の適用に失敗しました',
1371
+ 'pi.history.confirm': 'この履歴をファイルに適用しますか?現在の内容は先にスナップショット保存されます。',
1362
1372
  'pi.providers.models.remove': '削除',
1363
1373
  'pi.providers.models.search': '利用可能なモデルを検索…',
1364
1374
  'pi.providers.models.remoteCount': '{count} 個のモデルを取得しました。クリックで追加',
@@ -1000,6 +1000,12 @@ const vi = Object.freeze({
1000
1000
  'usage.summary.avgMessagesPerSession': 'TB tin nhắn/phiên',
1001
1001
  'usage.summary.busiestDay': 'Ngày bận nhất',
1002
1002
  'usage.summary.busiestHour': 'Giờ cao điểm',
1003
+ 'usage.kpi.tokens': 'Tổng token',
1004
+ 'usage.kpi.sessions': 'Số phiên',
1005
+ 'usage.kpi.messages': 'Số tin nhắn',
1006
+ 'usage.kpi.dailyAvg': 'Token trung bình/ngày',
1007
+ 'usage.kpi.busiestDay': 'Ngày bận nhất',
1008
+ 'usage.wave.summary': 'Tổng {total} · đỉnh {peakDay} ({peakValue})',
1003
1009
  'usage.currentSession.title': 'Phiên hiện tại',
1004
1010
  'usage.currentSession.apiDuration': 'Thời gian API',
1005
1011
  'usage.currentSession.totalDuration': 'Tổng thời gian',
@@ -1327,6 +1333,10 @@ const vi = Object.freeze({
1327
1333
  '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
1334
  'pi.files.settingsTitle': 'settings.json (cài đặt đang dùng)',
1329
1335
  'pi.files.modelsTitle': 'models.json (nhà cung cấp và mô hình)',
1336
+ 'pi.history.apply': 'Áp dụng bản ghi này',
1337
+ 'pi.history.applied': 'Đã áp dụng lịch sử',
1338
+ 'pi.history.applyFailed': 'Áp dụng lịch sử thất bại',
1339
+ 'pi.history.confirm': 'Áp dụng bản ghi lịch sử này vào tệp? Nội dung hiện tại sẽ được chụp nhanh trước.',
1330
1340
  'pi.providers.models.remove': 'Xóa',
1331
1341
  'pi.providers.models.search': 'Tìm kiếm mô hình khả dụng…',
1332
1342
  'pi.providers.models.remoteCount': 'Đã lấy {count} mô hình, bấm để thêm',
@@ -826,6 +826,12 @@ const zhTw = Object.freeze({
826
826
  'usage.summary.avgMessagesPerSession': '平均每會話消息',
827
827
  'usage.summary.busiestDay': '最忙日',
828
828
  'usage.summary.busiestHour': '高峰時段',
829
+ 'usage.kpi.tokens': '總 Token',
830
+ 'usage.kpi.sessions': '會話數',
831
+ 'usage.kpi.messages': '訊息數',
832
+ 'usage.kpi.dailyAvg': '日均 Token',
833
+ 'usage.kpi.busiestDay': '最忙日',
834
+ 'usage.wave.summary': '本期共 {total} · 峰值 {peakDay}({peakValue})',
829
835
  'usage.currentSession.title': '目前會話',
830
836
  'usage.currentSession.apiDuration': 'API時長',
831
837
  'usage.currentSession.totalDuration': '總時長',
@@ -1356,6 +1362,10 @@ const zhTw = Object.freeze({
1356
1362
  'pi.files.desc': '完整展示並直接編輯 Pi 的 settings.json 與 models.json;儲存前校驗 JSON 合法性,非法內容不會寫入。',
1357
1363
  'pi.files.settingsTitle': 'settings.json(生效設定)',
1358
1364
  'pi.files.modelsTitle': 'models.json(供應商與模型)',
1365
+ 'pi.history.apply': '套用此紀錄',
1366
+ 'pi.history.applied': '已套用歷史紀錄',
1367
+ 'pi.history.applyFailed': '套用歷史紀錄失敗',
1368
+ 'pi.history.confirm': '將此歷史紀錄套用到檔案?目前檔案內容會先被快照儲存。',
1359
1369
  'pi.providers.models.remove': '刪除',
1360
1370
  'pi.providers.models.search': '搜尋可用模型…',
1361
1371
  'pi.providers.models.remoteCount': '已取得 {count} 個可用模型,點擊新增',
@@ -826,6 +826,12 @@ const zh = Object.freeze({
826
826
  'usage.summary.avgMessagesPerSession': '平均每会话消息',
827
827
  'usage.summary.busiestDay': '最忙日',
828
828
  'usage.summary.busiestHour': '高峰时段',
829
+ 'usage.kpi.tokens': '总 Token',
830
+ 'usage.kpi.sessions': '会话数',
831
+ 'usage.kpi.messages': '消息数',
832
+ 'usage.kpi.dailyAvg': '日均 Token',
833
+ 'usage.kpi.busiestDay': '最忙日',
834
+ 'usage.wave.summary': '本期共 {total} · 峰值 {peakDay}({peakValue})',
829
835
  'usage.currentSession.title': '当前会话',
830
836
  'usage.currentSession.apiDuration': 'API时长',
831
837
  'usage.currentSession.totalDuration': '总时长',
@@ -1356,6 +1362,10 @@ const zh = Object.freeze({
1356
1362
  'pi.files.desc': '完整展示并直接编辑 Pi 的 settings.json 与 models.json;保存前校验 JSON 合法性,非法内容不会写入。',
1357
1363
  'pi.files.settingsTitle': 'settings.json(生效设置)',
1358
1364
  'pi.files.modelsTitle': 'models.json(供应商与模型)',
1365
+ 'pi.history.apply': '应用此记录',
1366
+ 'pi.history.applied': '已应用历史记录',
1367
+ 'pi.history.applyFailed': '应用历史记录失败',
1368
+ 'pi.history.confirm': '将此历史记录应用到文件?当前文件内容会先被快照保存。',
1359
1369
  'pi.providers.models.remove': '删除',
1360
1370
  'pi.providers.models.search': '搜索可用模型…',
1361
1371
  'pi.providers.models.remoteCount': '已拉取 {count} 个可用模型,点击添加',
@@ -1,10 +1,14 @@
1
1
  export function createSkillsComputed() {
2
2
  return {
3
3
  skillsTargetLabel() {
4
- return this.skillsTargetApp === 'claude' ? 'Claude Code' : 'Codex';
4
+ if (this.skillsTargetApp === 'claude') return 'Claude Code';
5
+ if (this.skillsTargetApp === 'pi') return 'Pi';
6
+ return 'Codex';
5
7
  },
6
8
  skillsDefaultRootPath() {
7
- return this.skillsTargetApp === 'claude' ? '~/.claude/skills' : '~/.codex/skills';
9
+ if (this.skillsTargetApp === 'claude') return '~/.claude/skills';
10
+ if (this.skillsTargetApp === 'pi') return '~/.pi/agent/skills';
11
+ return '~/.codex/skills';
8
12
  },
9
13
  filteredSkillsList() {
10
14
  const list = Array.isArray(this.skillsList) ? this.skillsList : [];
@@ -12,7 +12,7 @@ export function createSkillsMethods({ api }) {
12
12
  if (app == null) {
13
13
  return 'codex';
14
14
  }
15
- if (app === 'codex' || app === 'claude') {
15
+ if (app === 'codex' || app === 'claude' || app === 'pi') {
16
16
  return app;
17
17
  }
18
18
  throw createUnsupportedSkillsTargetAppError(app);
@@ -23,6 +23,14 @@
23
23
  @click="setSkillsTargetApp('claude', { silent: false })">
24
24
  Claude Code
25
25
  </button>
26
+ <button
27
+ type="button"
28
+ :class="['market-target-chip', { active: skillsTargetApp === 'pi' }]"
29
+ :aria-pressed="skillsTargetApp === 'pi'"
30
+ :disabled="loading || !!initError || skillsMarketBusy"
31
+ @click="setSkillsTargetApp('pi', { silent: false })">
32
+ Pi
33
+ </button>
26
34
  </div>
27
35
  <button class="btn-mini" @click="refreshSkillsList({ silent: false })" :disabled="skillsLoading || skillsDeleting || skillsScanningImports || skillsImporting || skillsZipImporting || skillsExporting">
28
36
  {{ skillsLoading ? t('common.refreshing') : t('common.refresh') }}
@@ -195,7 +195,29 @@
195
195
  <p v-if="piSettingsJsonError" class="config-template-hint error-text">{{ piSettingsJsonError }}</p>
196
196
  <div class="settings-tab-actions">
197
197
  <button type="button" class="btn-tool" @click="savePiSettingsJson" :disabled="!isToolConfigWriteAllowed('pi') || piSaving || piFileJsonSaving || !!piSettingsJsonError">{{ t('common.save') }}</button>
198
+ <button type="button" class="btn-mini" @click="openPiConfigHistory('settings')" :disabled="piHistoryLoading">{{ t('common.history') }}</button>
198
199
  </div>
200
+ <details v-if="piHistoryTarget === 'settings'" class="prompt-history-panel" open>
201
+ <summary class="prompt-history-summary">
202
+ <span>{{ t('common.history') }}</span>
203
+ <button type="button" class="btn-mini prompt-history-close" @click="closePiConfigHistory" :title="t('common.close')">✕</button>
204
+ </summary>
205
+ <div class="prompt-history-body">
206
+ <ul v-if="!piHistoryLoading && !piHistoryError && piHistoryItems.length" class="prompt-history-list">
207
+ <li v-for="item in piHistoryItems" :key="item.id" :class="['prompt-history-item', { active: piHistoryPreviewId === item.id }]" @click="viewPiConfigHistoryItem(item)">
208
+ <span class="prompt-history-time">{{ item.id }}</span>
209
+ <span class="prompt-history-size">{{ Math.round(item.size / 1024 * 10) / 10 }} KB</span>
210
+ </li>
211
+ </ul>
212
+ <div v-if="piHistoryLoading" class="state-message">{{ t('diff.generating') }}</div>
213
+ <div v-else-if="piHistoryError" class="state-message error">{{ piHistoryError }}</div>
214
+ <div v-else-if="!piHistoryItems.length" class="state-message">{{ t('toast.history.empty') }}</div>
215
+ <div v-if="piHistoryPreviewContent" class="prompt-history-preview-wrap">
216
+ <pre class="prompt-history-preview">{{ piHistoryPreviewContent }}</pre>
217
+ <button type="button" class="btn-mini btn-confirm-mini" @click="applyPiConfigHistory" :disabled="piHistoryApplying || !isToolConfigWriteAllowed('pi')">{{ t('pi.history.apply') }}</button>
218
+ </div>
219
+ </div>
220
+ </details>
199
221
  <label class="form-label">{{ t('pi.files.modelsTitle') }}</label>
200
222
  <textarea
201
223
  class="template-textarea config-json-editor"
@@ -208,7 +230,29 @@
208
230
  <p v-if="piModelsJsonError" class="config-template-hint error-text">{{ piModelsJsonError }}</p>
209
231
  <div class="settings-tab-actions">
210
232
  <button type="button" class="btn-tool" @click="savePiModelsJson" :disabled="!isToolConfigWriteAllowed('pi') || piSaving || piFileJsonSaving || !!piModelsJsonError">{{ t('common.save') }}</button>
233
+ <button type="button" class="btn-mini" @click="openPiConfigHistory('models')" :disabled="piHistoryLoading">{{ t('common.history') }}</button>
211
234
  </div>
235
+ <details v-if="piHistoryTarget === 'models'" class="prompt-history-panel" open>
236
+ <summary class="prompt-history-summary">
237
+ <span>{{ t('common.history') }}</span>
238
+ <button type="button" class="btn-mini prompt-history-close" @click="closePiConfigHistory" :title="t('common.close')">✕</button>
239
+ </summary>
240
+ <div class="prompt-history-body">
241
+ <ul v-if="!piHistoryLoading && !piHistoryError && piHistoryItems.length" class="prompt-history-list">
242
+ <li v-for="item in piHistoryItems" :key="item.id" :class="['prompt-history-item', { active: piHistoryPreviewId === item.id }]" @click="viewPiConfigHistoryItem(item)">
243
+ <span class="prompt-history-time">{{ item.id }}</span>
244
+ <span class="prompt-history-size">{{ Math.round(item.size / 1024 * 10) / 10 }} KB</span>
245
+ </li>
246
+ </ul>
247
+ <div v-if="piHistoryLoading" class="state-message">{{ t('diff.generating') }}</div>
248
+ <div v-else-if="piHistoryError" class="state-message error">{{ piHistoryError }}</div>
249
+ <div v-else-if="!piHistoryItems.length" class="state-message">{{ t('toast.history.empty') }}</div>
250
+ <div v-if="piHistoryPreviewContent" class="prompt-history-preview-wrap">
251
+ <pre class="prompt-history-preview">{{ piHistoryPreviewContent }}</pre>
252
+ <button type="button" class="btn-mini btn-confirm-mini" @click="applyPiConfigHistory" :disabled="piHistoryApplying || !isToolConfigWriteAllowed('pi')">{{ t('pi.history.apply') }}</button>
253
+ </div>
254
+ </div>
255
+ </details>
212
256
  </section>
213
257
  <div v-if="!isToolConfigWriteAllowed('pi')" class="tool-config-write-overlay">
214
258
  <div class="tool-config-write-overlay-card">