codexmate 0.1.16 → 0.1.18

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.
Files changed (33) hide show
  1. package/README.md +1 -0
  2. package/README.vi.md +1 -0
  3. package/README.zh.md +1 -0
  4. package/cli/skills.js +18 -1
  5. package/cli.js +176 -4
  6. package/package.json +1 -1
  7. package/plugins/prompt-templates/computed.mjs +5 -0
  8. package/web-ui/app.js +12 -10
  9. package/web-ui/index.html +1 -0
  10. package/web-ui/modules/app.computed.session.mjs +83 -12
  11. package/web-ui/modules/app.methods.navigation.mjs +1 -1
  12. package/web-ui/modules/app.methods.pi-config.mjs +288 -120
  13. package/web-ui/modules/app.methods.web-ui-preferences.mjs +3 -3
  14. package/web-ui/modules/i18n/locales/en.mjs +23 -0
  15. package/web-ui/modules/i18n/locales/ja.mjs +23 -0
  16. package/web-ui/modules/i18n/locales/vi.mjs +23 -0
  17. package/web-ui/modules/i18n/locales/zh-tw.mjs +23 -0
  18. package/web-ui/modules/i18n/locales/zh.mjs +23 -0
  19. package/web-ui/modules/pi-provider-templates.mjs +26 -0
  20. package/web-ui/modules/skills.computed.mjs +6 -2
  21. package/web-ui/modules/skills.methods.mjs +1 -1
  22. package/web-ui/partials/index/modal-skills.html +8 -0
  23. package/web-ui/partials/index/modals-pi-provider.html +96 -0
  24. package/web-ui/partials/index/panel-config-pi.html +44 -122
  25. package/web-ui/partials/index/panel-docs.html +8 -22
  26. package/web-ui/partials/index/panel-market.html +8 -0
  27. package/web-ui/partials/index/panel-plugins.html +14 -13
  28. package/web-ui/partials/index/panel-usage.html +27 -25
  29. package/web-ui/res/web-ui-render.precompiled.js +795 -619
  30. package/web-ui/styles/docs-panel.css +9 -56
  31. package/web-ui/styles/plugins-panel.css +18 -18
  32. package/web-ui/styles/sessions-usage.css +104 -91
  33. package/web-ui/styles/settings-panel.css +12 -0
@@ -1,9 +1,37 @@
1
1
  import { api } from './api.mjs';
2
+ import { PI_PROVIDER_TEMPLATE_GROUPS } from './pi-provider-templates.mjs';
2
3
 
3
4
  function isPiPlainObject(value) {
4
5
  return value && typeof value === 'object' && !Array.isArray(value);
5
6
  }
6
7
 
8
+ function normalizePiModelInput(value) {
9
+ if (!Array.isArray(value)) return [];
10
+ return value.filter((item) => item === 'text' || item === 'image');
11
+ }
12
+
13
+ function normalizePiModelNumber(value) {
14
+ if (typeof value === 'number') return Number.isFinite(value) ? value : '';
15
+ if (typeof value === 'string' && value.trim() !== '') {
16
+ const num = Number(value);
17
+ return Number.isFinite(num) ? num : '';
18
+ }
19
+ return '';
20
+ }
21
+
22
+ function normalizePiModelRecord(model) {
23
+ const record = model && typeof model === 'object' ? model : {};
24
+ const contextWindow = record.contextWindow !== undefined ? record.contextWindow : record.context_window;
25
+ const maxTokens = record.maxTokens !== undefined ? record.maxTokens : record.max_tokens;
26
+ return {
27
+ id: record.id == null ? '' : String(record.id),
28
+ name: record.name == null ? '' : String(record.name),
29
+ reasoning: !!record.reasoning,
30
+ contextWindow: normalizePiModelNumber(contextWindow),
31
+ maxTokens: normalizePiModelNumber(maxTokens),
32
+ input: normalizePiModelInput(record.input)
33
+ };
34
+ }
7
35
  export function createPiConfigMethods({ api: apiClient }) {
8
36
  return {
9
37
  async loadPiProviders() {
@@ -79,9 +107,18 @@ export function createPiConfigMethods({ api: apiClient }) {
79
107
  }
80
108
  },
81
109
  async savePiProvider() {
110
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
111
+ const editor = this.editingPiProvider;
112
+ if (!editor || !editor.form) return;
113
+ const isNew = !!editor.isNew;
114
+ const baseUrl = (editor.form.baseUrl || '').trim();
115
+ if (isNew && !baseUrl) {
116
+ this.message = 'Base URL 不能为空';
117
+ this.messageType = 'error';
118
+ return;
119
+ }
82
120
  this.piSaving = true;
83
121
  try {
84
- const providerId = this.editingPiProvider.id;
85
122
  const { merged, ok, error } = this.piBuildMergedEditorRecord();
86
123
  if (!ok || !merged) {
87
124
  this.piEditorJsonError = error || '配置 JSON 不合法';
@@ -89,12 +126,23 @@ export function createPiConfigMethods({ api: apiClient }) {
89
126
  this.messageType = 'error';
90
127
  return;
91
128
  }
92
-
129
+ const providerId = isNew ? this.derivePiProviderId(baseUrl) : editor.id;
130
+ if (isNew) {
131
+ merged.id = providerId;
132
+ if (merged.name === undefined) merged.name = providerId;
133
+ }
93
134
  await this.persistPiProvider(providerId, merged);
94
135
  this.piProviders = { ...this.piProviders, [providerId]: merged };
95
136
  this.piProviderIds = Object.keys(this.piProviders).sort();
96
137
  this.resetPiProviderEditing();
97
- this.message = '配置已保存';
138
+ if (isNew) {
139
+ this.showAddPiProviderModal = false;
140
+ this.piProviderPickerQuery = '';
141
+ this.piSelectedProviderTemplate = '';
142
+ this.message = '供应商已添加';
143
+ } else {
144
+ this.message = '配置已保存';
145
+ }
98
146
  this.messageType = 'success';
99
147
  } catch (e) {
100
148
  this.message = e && e.message ? e.message : '保存 Pi 配置失败';
@@ -103,45 +151,6 @@ export function createPiConfigMethods({ api: apiClient }) {
103
151
  this.piSaving = false;
104
152
  }
105
153
  },
106
- async addPiProviderFromModal() {
107
- let createdId = null;
108
- this.piSaving = true;
109
- try {
110
- const baseUrl = (this.addingPiProviderBaseUrl || '').trim();
111
- if (!baseUrl) throw new Error('Base URL 不能为空');
112
- const providerId = this.derivePiProviderId(baseUrl);
113
- const modelId = (this.addingPiProviderModel || '').trim();
114
- const values = {
115
- id: providerId,
116
- name: providerId,
117
- baseUrl,
118
- api: this.addingPiProviderApi || 'openai-completions',
119
- apiKey: (this.addingPiProviderApiKey || '').trim(),
120
- models: modelId ? [{
121
- id: modelId,
122
- name: modelId,
123
- reasoning: false,
124
- contextWindow: 128000,
125
- maxTokens: 32000,
126
- input: ''
127
- }] : []
128
- };
129
-
130
- await this.persistPiProvider(providerId, values);
131
- this.piProviders = { ...this.piProviders, [providerId]: values };
132
- this.piProviderIds = Object.keys(this.piProviders).sort();
133
- this.cancelAddPiProviderModal();
134
- this.message = '供应商已添加';
135
- this.messageType = 'success';
136
- createdId = providerId;
137
- } catch (e) {
138
- this.message = e && e.message ? e.message : '添加 Pi 供应商失败';
139
- this.messageType = 'error';
140
- } finally {
141
- this.piSaving = false;
142
- if (createdId) this.openEditPiProvider(createdId);
143
- }
144
- },
145
154
  derivePiProviderId(baseUrl) {
146
155
  let candidate = String(Date.now());
147
156
  while (this.piProviders[candidate]) candidate = String(Number(candidate) + 1);
@@ -150,79 +159,156 @@ export function createPiConfigMethods({ api: apiClient }) {
150
159
  piProviderUrlTitle() {
151
160
  return '';
152
161
  },
162
+ resetPiEditorTransientState() {
163
+ this.piRemoteModels = [];
164
+ this.piRemoteModelError = '';
165
+ this.piRemoteChecked = {};
166
+ this.piCatalogFillError = '';
167
+ this.piCatalogFillIndex = -1;
168
+ this.piModelSearch = '';
169
+ this.piEditorJsonError = '';
170
+ },
153
171
  startAddPiProvider() {
154
172
  if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
155
- this.addingPiProviderId = '';
156
- this.addingPiProviderName = '';
157
- this.addingPiProviderBaseUrl = '';
158
- this.addingPiProviderApi = 'openai-completions';
159
- this.addingPiProviderApiKey = '';
160
- this.addingPiProviderModel = '';
161
- this.addingPiRemoteModels = [];
162
- this.addingPiRemoteLoading = false;
163
- this.addingPiRemoteError = '';
164
- this.showAddPiProviderModal = true;
165
173
  this.editingPiProvider = null;
174
+ this.showAddPiProviderModal = true;
175
+ this.piProviderPickerQuery = '';
176
+ this.piSelectedProviderTemplate = '';
177
+ this.resetPiEditorTransientState();
166
178
  },
167
179
  cancelAddPiProviderModal() {
168
180
  this.showAddPiProviderModal = false;
169
- this.addingPiProviderId = '';
170
- this.addingPiProviderName = '';
171
- this.addingPiProviderBaseUrl = '';
172
- this.addingPiProviderApi = '';
173
- this.addingPiProviderApiKey = '';
174
- this.addingPiProviderModel = '';
175
- this.addingPiRemoteModels = [];
176
- this.addingPiRemoteLoading = false;
177
- this.addingPiRemoteError = '';
178
- },
179
- async fetchAddingPiRemoteModels() {
180
- const baseUrl = (this.addingPiProviderBaseUrl || '').trim();
181
- if (!baseUrl || !/^https?:\/\//i.test(baseUrl)) {
182
- this.addingPiRemoteModels = [];
183
- this.addingPiRemoteError = '';
184
- return;
181
+ this.piProviderPickerQuery = '';
182
+ this.piSelectedProviderTemplate = '';
183
+ if (this.editingPiProvider && this.editingPiProvider.isNew) this.editingPiProvider = null;
184
+ this.resetPiEditorTransientState();
185
+ },
186
+ piProviderTemplateGroups() {
187
+ const query = (this.piProviderPickerQuery || '').trim().toLowerCase();
188
+ if (!query) return PI_PROVIDER_TEMPLATE_GROUPS;
189
+ return PI_PROVIDER_TEMPLATE_GROUPS
190
+ .map((group) => ({
191
+ id: group.id,
192
+ i18nKey: group.i18nKey,
193
+ templates: group.templates.filter((tpl) =>
194
+ tpl.name.toLowerCase().includes(query) || tpl.id.toLowerCase().includes(query))
195
+ }))
196
+ .filter((group) => group.templates.length > 0);
197
+ },
198
+ selectPiProviderTemplate(tplId) {
199
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
200
+ const tpl = PI_PROVIDER_TEMPLATE_GROUPS
201
+ .flatMap((group) => group.templates)
202
+ .find((item) => item.id === tplId);
203
+ if (!tpl) return;
204
+ this.piSelectedProviderTemplate = tpl.id;
205
+ this.startPiProviderDraft(tpl);
206
+ },
207
+ startPiProviderDraft(tpl) {
208
+ const template = isPiPlainObject(tpl) ? tpl : {};
209
+ this.resetPiEditorTransientState();
210
+ this.editingPiProvider = {
211
+ id: '',
212
+ isNew: true,
213
+ form: {
214
+ id: '',
215
+ baseUrl: template.baseUrl || '',
216
+ api: template.api || 'openai-completions',
217
+ apiKey: '',
218
+ configJsonDraft: '',
219
+ models: []
220
+ },
221
+ extras: {},
222
+ original: null
223
+ };
224
+ this.fetchPiRemoteModels();
225
+ },
226
+ resetPiProviderTemplateSelection() {
227
+ this.piSelectedProviderTemplate = '';
228
+ if (this.editingPiProvider && this.editingPiProvider.isNew) this.editingPiProvider = null;
229
+ },
230
+ piRemoteSelectableModels() {
231
+ const base = Array.isArray(this.piRemoteModels) ? this.piRemoteModels : [];
232
+ const form = this.editingPiProvider && this.editingPiProvider.form;
233
+ const existing = Array.isArray(form && form.models) ? form.models : [];
234
+ const added = new Set(existing.map((m) => String(m && m.id || '').trim().toLowerCase()));
235
+ const q = (this.piModelSearch || '').trim().toLowerCase();
236
+ return base
237
+ .filter((id) => {
238
+ if (added.has(String(id).trim().toLowerCase())) return false;
239
+ return !q || String(id).toLowerCase().includes(q);
240
+ })
241
+ .slice(0, 50);
242
+ },
243
+ isPiRemoteChecked(id) {
244
+ return !!this.piRemoteChecked[String(id)];
245
+ },
246
+ togglePiRemoteChecked(id) {
247
+ const key = String(id);
248
+ const next = { ...this.piRemoteChecked };
249
+ if (next[key]) delete next[key];
250
+ else next[key] = true;
251
+ this.piRemoteChecked = next;
252
+ },
253
+ piCheckedRemoteCount() {
254
+ return this.piRemoteSelectableModels().filter((id) => this.isPiRemoteChecked(id)).length;
255
+ },
256
+ piCreateModelRecord(id) {
257
+ const t = String(id || '').trim();
258
+ return {
259
+ id: t,
260
+ name: t,
261
+ reasoning: false,
262
+ contextWindow: 128000,
263
+ maxTokens: 32000,
264
+ input: []
265
+ };
266
+ },
267
+ addSelectedPiRemoteModels() {
268
+ if (!this.isToolConfigWriteAllowed('pi') || !this.editingPiProvider) return;
269
+ const form = this.editingPiProvider.form;
270
+ if (!form || !Array.isArray(form.models)) return;
271
+ const existing = new Set(form.models.map((m) => String(m && m.id || '').trim().toLowerCase()));
272
+ for (const id of this.piRemoteSelectableModels()) {
273
+ if (!this.isPiRemoteChecked(id)) continue;
274
+ const key = String(id).trim().toLowerCase();
275
+ if (existing.has(key)) continue;
276
+ form.models.push(this.piCreateModelRecord(id));
277
+ existing.add(key);
185
278
  }
186
- this.addingPiRemoteLoading = true;
187
- this.addingPiRemoteError = '';
279
+ this.piRemoteChecked = {};
280
+ this.piModelSearch = '';
281
+ },
282
+ async fillPiModelFromCatalog(index) {
283
+ if (!this.isToolConfigWriteAllowed('pi') || !this.editingPiProvider) return;
284
+ const models = this.editingPiProvider.form && this.editingPiProvider.form.models;
285
+ const model = Array.isArray(models) ? models[index] : null;
286
+ if (!model || !String(model.id || '').trim()) return;
287
+ if (this.piCatalogFillIndex !== -1) return;
288
+ this.piCatalogFillIndex = index;
289
+ this.piCatalogFillError = '';
188
290
  try {
189
- const result = await apiClient('fetch-pi-remote-models', {
190
- baseUrl,
191
- apiKey: (this.addingPiProviderApiKey || '').trim()
291
+ const result = await apiClient('pi-models-catalog', {
292
+ modelId: String(model.id).trim(),
293
+ providerId: this.editingPiProvider.id,
294
+ baseUrl: this.editingPiProvider.form.baseUrl || ''
192
295
  });
193
- if (!this.showAddPiProviderModal) return;
194
- if (result && Array.isArray(result.models) && result.models.length > 0) {
195
- this.addingPiRemoteModels = result.models;
296
+ if (result && result.ok && isPiPlainObject(result.model)) {
297
+ const info = result.model;
298
+ if (model.name === '' && typeof info.name === 'string') model.name = info.name;
299
+ if (model.contextWindow === '' && Number.isFinite(info.contextWindow)) model.contextWindow = info.contextWindow;
300
+ if (model.maxTokens === '' && Number.isFinite(info.maxTokens)) model.maxTokens = info.maxTokens;
301
+ model.reasoning = !!info.reasoning;
196
302
  } else {
197
- this.addingPiRemoteModels = [];
198
- this.addingPiRemoteError = (result && result.error) || '未获取到可用模型';
303
+ this.piCatalogFillError = (result && result.error) || 'models.dev 目录中未找到该模型';
199
304
  }
200
- } catch (e) {
201
- if (!this.showAddPiProviderModal) return;
202
- this.addingPiRemoteModels = [];
203
- this.addingPiRemoteError = e && e.message ? e.message : '模型列表获取失败';
204
305
  } finally {
205
- if (this.showAddPiProviderModal) this.addingPiRemoteLoading = false;
306
+ this.piCatalogFillIndex = -1;
206
307
  }
207
308
  },
208
- onAddingPiEndpointChange() {
209
- if (this.addingPiRemoteDebounce) clearTimeout(this.addingPiRemoteDebounce);
210
- this.addingPiRemoteDebounce = setTimeout(() => {
211
- this.addingPiRemoteDebounce = null;
212
- this.fetchAddingPiRemoteModels();
213
- }, 600);
214
- },
215
- piFilteredAddingRemoteModels() {
216
- const query = (this.addingPiProviderModel || '').trim().toLowerCase();
217
- if (!query) return [];
218
- return this.addingPiRemoteModels.filter((id) => id.toLowerCase().includes(query)).slice(0, 50);
219
- },
220
309
  openEditPiProvider(providerId) {
221
310
  if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
222
- this.piRemoteModels = [];
223
- this.piRemoteModelError = '';
224
- this.piModelSearch = '';
225
- this.piEditorJsonError = '';
311
+ this.resetPiEditorTransientState();
226
312
  const provider = this.piProviders[providerId] || {};
227
313
  const knownKeys = ['id', 'name', 'title', 'baseUrl', 'api', 'apiKey', 'models', 'headers', 'configJson'];
228
314
  const extras = {};
@@ -236,14 +322,7 @@ export function createPiConfigMethods({ api: apiClient }) {
236
322
  apiKey: provider.apiKey || '',
237
323
  configJsonDraft: JSON.stringify(extras, null, 2),
238
324
  models: Array.isArray(provider.models) && provider.models.length
239
- ? provider.models.map((model) => ({
240
- id: model?.id || '',
241
- name: model?.name || '',
242
- reasoning: model?.reasoning || '',
243
- contextWindow: model?.contextWindow ?? model?.context_window ?? '',
244
- maxTokens: model?.maxTokens ?? model?.max_tokens ?? '',
245
- input: model?.input || ''
246
- }))
325
+ ? provider.models.map((model) => ({ ...model, ...normalizePiModelRecord(model) }))
247
326
  : []
248
327
  };
249
328
  this.editingPiProvider = {
@@ -259,9 +338,19 @@ export function createPiConfigMethods({ api: apiClient }) {
259
338
  this.editingPiProvider = null;
260
339
  this.piRemoteModels = [];
261
340
  this.piRemoteModelError = '';
341
+ this.piRemoteChecked = {};
342
+ this.piCatalogFillError = '';
343
+ this.piCatalogFillIndex = -1;
262
344
  this.piModelSearch = '';
263
345
  this.piEditorJsonError = '';
264
346
  },
347
+ closePiProviderEditor() {
348
+ const editor = this.editingPiProvider;
349
+ if (!editor) return;
350
+ const wasNew = !!editor.isNew;
351
+ this.resetPiProviderEditing();
352
+ if (wasNew) this.piSelectedProviderTemplate = '';
353
+ },
265
354
  removePiProviderModel(index) {
266
355
  if (!this.editingPiProvider) return;
267
356
  const list = this.editingPiProvider.form.models;
@@ -280,13 +369,16 @@ export function createPiConfigMethods({ api: apiClient }) {
280
369
  if (this.editingPiProvider !== provider) return;
281
370
  if (result && Array.isArray(result.models) && result.models.length > 0) {
282
371
  this.piRemoteModels = result.models;
372
+ this.piRemoteChecked = {};
283
373
  } else {
284
374
  this.piRemoteModels = [];
375
+ this.piRemoteChecked = {};
285
376
  this.piRemoteModelError = (result && result.error) || '未获取到可用模型';
286
377
  }
287
378
  } catch (e) {
288
379
  if (this.editingPiProvider !== provider) return;
289
380
  this.piRemoteModels = [];
381
+ this.piRemoteChecked = {};
290
382
  this.piRemoteModelError = e && e.message ? e.message : '模型列表获取失败';
291
383
  } finally {
292
384
  if (this.editingPiProvider === provider) this.piRemoteModelsLoading = false;
@@ -303,14 +395,10 @@ export function createPiConfigMethods({ api: apiClient }) {
303
395
  if (!trimmed) return;
304
396
  const list = this.editingPiProvider.form.models;
305
397
  if (!Array.isArray(list)) return;
306
- list.splice(0, list.length, {
307
- id: trimmed,
308
- name: trimmed,
309
- reasoning: false,
310
- contextWindow: 128000,
311
- maxTokens: 32000,
312
- input: ''
313
- });
398
+ const key = trimmed.toLowerCase();
399
+ const duplicated = list.some((m) => String(m && m.id || '').trim().toLowerCase() === key);
400
+ if (duplicated) return;
401
+ list.push(this.piCreateModelRecord(trimmed));
314
402
  this.piModelSearch = '';
315
403
  },
316
404
  async confirmDeletePiProvider() {
@@ -447,11 +535,11 @@ export function createPiConfigMethods({ api: apiClient }) {
447
535
  const values = provider.form || {};
448
536
  const extras = draft.extras || {};
449
537
  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
- }));
538
+ const mapModels = (xs) => (Array.isArray(xs) ? xs : []).map((model) => {
539
+ const record = { ...(model && typeof model === 'object' ? model : {}), ...normalizePiModelRecord(model) };
540
+ if (!record.name) record.name = record.id;
541
+ return record;
542
+ });
455
543
  const merged = { ...extras };
456
544
  if (merged.baseUrl === undefined) merged.baseUrl = values.baseUrl || '';
457
545
  if (merged.api === undefined) merged.api = values.api || '';
@@ -475,6 +563,86 @@ export function createPiConfigMethods({ api: apiClient }) {
475
563
  piEditorJsonDraftInput() {
476
564
  const draft = this.piParseEditorJsonDraft();
477
565
  this.piEditorJsonError = draft.ok ? '' : draft.error;
566
+ },
567
+ piHistoryBucketFor(target) {
568
+ return target === 'settings' ? 'pi-settings' : 'pi-models';
569
+ },
570
+ async openPiConfigHistory(target) {
571
+ if (this.piHistoryLoading) return;
572
+ const tgt = target === 'settings' ? 'settings' : (target === 'models' ? 'models' : '');
573
+ if (!tgt) return;
574
+ this.piHistoryTarget = tgt;
575
+ this.piHistoryItems = [];
576
+ this.piHistoryPreviewId = '';
577
+ this.piHistoryPreviewContent = '';
578
+ this.piHistoryError = '';
579
+ this.piHistoryLoading = true;
580
+ try {
581
+ const res = await api('list-prompt-history', { bucket: this.piHistoryBucketFor(tgt) });
582
+ if (res && res.error) {
583
+ this.piHistoryError = res.error;
584
+ return;
585
+ }
586
+ this.piHistoryItems = Array.isArray(res) ? res : [];
587
+ } catch (e) {
588
+ this.piHistoryError = this.t('toast.load.fail');
589
+ } finally {
590
+ this.piHistoryLoading = false;
591
+ }
592
+ },
593
+ closePiConfigHistory() {
594
+ this.piHistoryTarget = '';
595
+ this.piHistoryItems = [];
596
+ this.piHistoryPreviewId = '';
597
+ this.piHistoryPreviewContent = '';
598
+ this.piHistoryError = '';
599
+ this.piHistoryLoading = false;
600
+ },
601
+ async viewPiConfigHistoryItem(item) {
602
+ if (!item || !item.id) return;
603
+ if (this.piHistoryLoading) return;
604
+ if (this.piHistoryPreviewId === item.id && this.piHistoryPreviewContent) return;
605
+ this.piHistoryPreviewId = item.id;
606
+ this.piHistoryPreviewContent = '';
607
+ try {
608
+ const res = await api('get-prompt-history', { bucket: this.piHistoryBucketFor(this.piHistoryTarget), id: item.id });
609
+ if (res && res.error) {
610
+ this.piHistoryError = res.error;
611
+ this.piHistoryPreviewId = '';
612
+ return;
613
+ }
614
+ this.piHistoryPreviewContent = typeof res.content === 'string' ? res.content : '';
615
+ } catch (e) {
616
+ this.piHistoryError = this.t('toast.load.fail');
617
+ this.piHistoryPreviewId = '';
618
+ }
619
+ },
620
+ async applyPiConfigHistory() {
621
+ if (this.piHistoryApplying || this.piHistoryLoading) return;
622
+ if (!this.piHistoryTarget || !this.piHistoryPreviewId) return;
623
+ const confirmed = await this.requestConfirmDialog({
624
+ title: this.t('common.history'),
625
+ message: this.t('pi.history.confirm'),
626
+ confirmText: this.t('confirm.ok'),
627
+ cancelText: this.t('confirm.cancel'),
628
+ danger: true
629
+ });
630
+ if (!confirmed) return;
631
+ this.piHistoryApplying = true;
632
+ try {
633
+ const res = await api('apply-pi-config-history', { target: this.piHistoryTarget, id: this.piHistoryPreviewId });
634
+ if (res && res.error) {
635
+ this.showMessage(res.error, 'error');
636
+ return;
637
+ }
638
+ this.showMessage(this.t('pi.history.applied'), 'success');
639
+ this.closePiConfigHistory();
640
+ await this.loadPiSources();
641
+ } catch (e) {
642
+ this.showMessage(this.t('pi.history.applyFailed'), 'error');
643
+ } finally {
644
+ this.piHistoryApplying = false;
645
+ }
478
646
  }
479
647
  };
480
648
  }
@@ -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,12 +1364,29 @@ 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',
1364
1374
  'pi.providers.models.remoteLoading': 'Fetching remote model list…',
1365
1375
  'pi.providers.models.remoteFailed': 'You can still type model IDs manually.',
1366
1376
  'pi.providers.models.noMatch': 'No matching models',
1377
+ 'pi.providers.models.importTitle': 'Import models from endpoint',
1378
+ 'pi.providers.models.addSelected': 'Add selected ({count})',
1379
+ 'pi.providers.models.name': 'Name',
1380
+ 'pi.providers.models.reasoning': 'Supports reasoning',
1381
+ 'pi.providers.models.contextWindow': 'Context window',
1382
+ 'pi.providers.models.maxTokens': 'Max output',
1383
+ 'pi.providers.models.catalogFill': 'Auto-fill',
1384
+ 'pi.providers.models.catalogFilling': 'Filling…',
1385
+ 'pi.picker.searchPlaceholder': 'Search providers…',
1386
+ 'pi.picker.group.custom': 'Custom endpoint',
1387
+ 'pi.picker.group.apiKey': 'API key providers',
1388
+ 'pi.picker.empty': 'No matching provider templates',
1389
+ 'pi.picker.back': 'Back to template selection',
1367
1390
  'pi.providers.delete': 'Delete Provider',
1368
1391
  'pi.providers.addModal.title': 'Add Provider',
1369
1392
  'pi.providers.addModal.subtitle': 'Create a new Piper provider',
@@ -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,12 +1365,29 @@ 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} 個のモデルを取得しました。クリックで追加',
1365
1375
  'pi.providers.models.remoteLoading': 'リモートモデル一覧を取得中…',
1366
1376
  'pi.providers.models.remoteFailed': 'モデル ID は手動入力できます。',
1367
1377
  'pi.providers.models.noMatch': '一致するモデルがありません',
1378
+ 'pi.providers.models.importTitle': 'エンドポイントからモデルをインポート',
1379
+ 'pi.providers.models.addSelected': '選択済みを追加({count})',
1380
+ 'pi.providers.models.name': '名前',
1381
+ 'pi.providers.models.reasoning': '推論をサポート',
1382
+ 'pi.providers.models.contextWindow': 'コンテキストウィンドウ',
1383
+ 'pi.providers.models.maxTokens': '最大出力',
1384
+ 'pi.providers.models.catalogFill': '自動入力',
1385
+ 'pi.providers.models.catalogFilling': '入力中…',
1386
+ 'pi.picker.searchPlaceholder': 'プロバイダーを検索…',
1387
+ 'pi.picker.group.custom': 'カスタムエンドポイント',
1388
+ 'pi.picker.group.apiKey': 'API キープロバイダー',
1389
+ 'pi.picker.empty': '一致するプロバイダーテンプレートがありません',
1390
+ 'pi.picker.back': 'テンプレート選択に戻る',
1368
1391
  'pi.providers.delete': 'プロバイダを削除',
1369
1392
  'pi.providers.addModal.title': 'プロバイダを追加',
1370
1393
  'pi.providers.addModal.subtitle': '新しい Piper プロバイダを作成',