codexmate 0.1.10 → 0.1.13

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 (40) hide show
  1. package/README.md +5 -7
  2. package/README.vi.md +4 -7
  3. package/README.zh.md +5 -7
  4. package/cli/agents-files.js +27 -3
  5. package/cli/prompt-history.js +171 -0
  6. package/cli/system-prompt-files.js +20 -2
  7. package/cli.js +162 -5
  8. package/package.json +1 -1
  9. package/web-ui/app.js +95 -4
  10. package/web-ui/index.html +2 -1
  11. package/web-ui/modules/app.methods.agents.mjs +90 -0
  12. package/web-ui/modules/app.methods.index.mjs +2 -0
  13. package/web-ui/modules/app.methods.navigation.mjs +7 -1
  14. package/web-ui/modules/app.methods.pi-config.mjs +371 -0
  15. package/web-ui/modules/app.methods.session-actions.mjs +57 -0
  16. package/web-ui/modules/app.methods.startup-claude.mjs +2 -1
  17. package/web-ui/modules/app.methods.system-prompt.mjs +81 -0
  18. package/web-ui/modules/app.methods.tool-config-permissions.mjs +3 -2
  19. package/web-ui/modules/app.methods.web-ui-preferences.mjs +8 -1
  20. package/web-ui/modules/config-mode.computed.mjs +2 -1
  21. package/web-ui/modules/i18n/locales/en.mjs +52 -0
  22. package/web-ui/modules/i18n/locales/ja.mjs +52 -0
  23. package/web-ui/modules/i18n/locales/vi.mjs +52 -0
  24. package/web-ui/modules/i18n/locales/zh-tw.mjs +52 -0
  25. package/web-ui/modules/i18n/locales/zh.mjs +52 -0
  26. package/web-ui/partials/index/layout-header.html +20 -0
  27. package/web-ui/partials/index/panel-config-claude.html +3 -3
  28. package/web-ui/partials/index/panel-config-codex.html +3 -3
  29. package/web-ui/partials/index/panel-config-kilocode.html +5 -5
  30. package/web-ui/partials/index/panel-config-openclaw.html +3 -3
  31. package/web-ui/partials/index/panel-config-opencode.html +5 -5
  32. package/web-ui/partials/index/panel-config-pi.html +171 -0
  33. package/web-ui/partials/index/panel-prompts.html +52 -0
  34. package/web-ui/partials/index/panel-sessions.html +12 -0
  35. package/web-ui/partials/index/panel-settings.html +42 -0
  36. package/web-ui/res/web-ui-render.precompiled.js +1101 -252
  37. package/web-ui/styles/modals-core.css +86 -0
  38. package/web-ui/styles/sessions-preview.css +38 -0
  39. package/web-ui/styles/settings-panel.css +7 -0
  40. package/web-ui/styles/titles-cards.css +2 -2
@@ -0,0 +1,371 @@
1
+ import { api } from './api.mjs';
2
+
3
+ function isPiPlainObject(value) {
4
+ return value && typeof value === 'object' && !Array.isArray(value);
5
+ }
6
+
7
+ export function createPiConfigMethods({ api: apiClient }) {
8
+ return {
9
+ async loadPiProviders() {
10
+ const result = await apiClient('read-pi-models');
11
+ const providers = isPiPlainObject(result && result.providers) ? result.providers : {};
12
+ return providers;
13
+ },
14
+ async loadPiSettings() {
15
+ const result = await apiClient('read-pi-settings');
16
+ const settings = isPiPlainObject(result && result.settings) ? result.settings : {};
17
+ this.piActiveProvider = typeof settings.defaultProvider === 'string' ? settings.defaultProvider : '';
18
+ this.piActiveModel = typeof settings.defaultModel === 'string' ? settings.defaultModel : '';
19
+ return settings;
20
+ },
21
+ async switchPiActiveProvider(providerId) {
22
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
23
+ const provider = this.piProviders[providerId];
24
+ if (!isPiPlainObject(provider)) return;
25
+ const firstModel = Array.isArray(provider.models) && provider.models.length > 0
26
+ ? (provider.models[0] && provider.models[0].id) || ''
27
+ : '';
28
+ const updates = { defaultProvider: providerId };
29
+ if (firstModel) updates.defaultModel = firstModel;
30
+ try {
31
+ const result = await apiClient('write-pi-settings', updates);
32
+ if (result && result.error) throw new Error(result.error);
33
+ await this.loadPiSettings();
34
+ this.message = '已切换为使用该供应商';
35
+ this.messageType = 'success';
36
+ } catch (e) {
37
+ this.message = e && e.message ? e.message : '切换默认供应商失败';
38
+ this.messageType = 'error';
39
+ }
40
+ },
41
+ async savePiProviders(providers) {
42
+ return apiClient('write-pi-models', { providers });
43
+ },
44
+ async removePiProvider(providerId) {
45
+ const providers = await this.loadPiProviders();
46
+ const existing = providers[providerId] || {};
47
+ const updated = { ...providers };
48
+ delete updated[providerId];
49
+ await this.savePiProviders(updated);
50
+ return { success: true, provider: existing };
51
+ },
52
+ async persistPiProvider(providerId, values) {
53
+ const providers = await this.loadPiProviders();
54
+ const existing = providers[providerId] || {};
55
+ const current = isPiPlainObject(values) ? values : {};
56
+
57
+ const updatedProvider = {
58
+ ...existing,
59
+ ...current
60
+ };
61
+
62
+ return this.savePiProviders({ ...providers, [providerId]: updatedProvider });
63
+ },
64
+ async loadPiSources() {
65
+ this.piProviderLoading = true;
66
+ try {
67
+ const [providers] = await Promise.all([
68
+ this.loadPiProviders(),
69
+ this.loadPiSettings()
70
+ ]);
71
+ this.piProviders = providers || {};
72
+ this.piProviderIds = Object.keys(this.piProviders || {}).sort();
73
+ } catch (e) {
74
+ this.message = e && e.message ? e.message : '读取 Pi 配置失败';
75
+ this.messageType = 'error';
76
+ } finally {
77
+ this.piProviderLoading = false;
78
+ }
79
+ },
80
+ async savePiProvider() {
81
+ this.piSaving = true;
82
+ try {
83
+ 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;
102
+
103
+ await this.persistPiProvider(providerId, merged);
104
+ this.piProviders = { ...this.piProviders, [providerId]: merged };
105
+ this.piProviderIds = Object.keys(this.piProviders).sort();
106
+ this.resetPiProviderEditing();
107
+ this.message = '配置已保存';
108
+ this.messageType = 'success';
109
+ } catch (e) {
110
+ this.message = e && e.message ? e.message : '保存 Pi 配置失败';
111
+ this.messageType = 'error';
112
+ } finally {
113
+ this.piSaving = false;
114
+ }
115
+ },
116
+ async addPiProviderFromModal() {
117
+ let createdId = null;
118
+ this.piSaving = true;
119
+ try {
120
+ const baseUrl = (this.addingPiProviderBaseUrl || '').trim();
121
+ if (!baseUrl) throw new Error('Base URL 不能为空');
122
+ const providerId = this.derivePiProviderId(baseUrl);
123
+ const modelId = (this.addingPiProviderModel || '').trim();
124
+ const values = {
125
+ baseUrl,
126
+ api: this.addingPiProviderApi || 'openai-completions',
127
+ apiKey: (this.addingPiProviderApiKey || '').trim(),
128
+ models: modelId ? [{
129
+ id: modelId,
130
+ name: modelId,
131
+ reasoning: false,
132
+ contextWindow: 128000,
133
+ maxTokens: 32000,
134
+ input: ''
135
+ }] : []
136
+ };
137
+
138
+ await this.persistPiProvider(providerId, values);
139
+ this.piProviders = { ...this.piProviders, [providerId]: values };
140
+ this.piProviderIds = Object.keys(this.piProviders).sort();
141
+ this.cancelAddPiProviderModal();
142
+ this.message = '供应商已添加';
143
+ this.messageType = 'success';
144
+ createdId = providerId;
145
+ } catch (e) {
146
+ this.message = e && e.message ? e.message : '添加 Pi 供应商失败';
147
+ this.messageType = 'error';
148
+ } finally {
149
+ this.piSaving = false;
150
+ if (createdId) this.openEditPiProvider(createdId);
151
+ }
152
+ },
153
+ derivePiProviderId(baseUrl) {
154
+ let base = '';
155
+ try {
156
+ base = new URL(baseUrl).hostname.replace(/^www\./, '');
157
+ } catch (_) {
158
+ base = '';
159
+ }
160
+ const candidate = (base || 'pi-provider').replace(/[^a-zA-Z0-9-]/g, '-')
161
+ .replace(/-+/g, '-').replace(/^-|-$/g, '') || 'pi-provider';
162
+ if (!this.piProviders[candidate]) return candidate;
163
+ let suffix = 2;
164
+ while (this.piProviders[`${candidate}-${suffix}`]) suffix += 1;
165
+ return `${candidate}-${suffix}`;
166
+ },
167
+ startAddPiProvider() {
168
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
169
+ this.addingPiProviderId = '';
170
+ this.addingPiProviderName = '';
171
+ this.addingPiProviderBaseUrl = '';
172
+ this.addingPiProviderApi = 'openai-completions';
173
+ this.addingPiProviderApiKey = '';
174
+ this.addingPiProviderModel = '';
175
+ this.addingPiRemoteModels = [];
176
+ this.addingPiRemoteLoading = false;
177
+ this.addingPiRemoteError = '';
178
+ this.showAddPiProviderModal = true;
179
+ this.editingPiProvider = null;
180
+ },
181
+ cancelAddPiProviderModal() {
182
+ this.showAddPiProviderModal = false;
183
+ this.addingPiProviderId = '';
184
+ this.addingPiProviderName = '';
185
+ this.addingPiProviderBaseUrl = '';
186
+ this.addingPiProviderApi = '';
187
+ this.addingPiProviderApiKey = '';
188
+ this.addingPiProviderModel = '';
189
+ this.addingPiRemoteModels = [];
190
+ this.addingPiRemoteLoading = false;
191
+ this.addingPiRemoteError = '';
192
+ },
193
+ async fetchAddingPiRemoteModels() {
194
+ const baseUrl = (this.addingPiProviderBaseUrl || '').trim();
195
+ if (!baseUrl || !/^https?:\/\//i.test(baseUrl)) {
196
+ this.addingPiRemoteModels = [];
197
+ this.addingPiRemoteError = '';
198
+ return;
199
+ }
200
+ this.addingPiRemoteLoading = true;
201
+ this.addingPiRemoteError = '';
202
+ try {
203
+ const result = await apiClient('fetch-pi-remote-models', {
204
+ baseUrl,
205
+ apiKey: (this.addingPiProviderApiKey || '').trim()
206
+ });
207
+ if (!this.showAddPiProviderModal) return;
208
+ if (result && Array.isArray(result.models) && result.models.length > 0) {
209
+ this.addingPiRemoteModels = result.models;
210
+ } else {
211
+ this.addingPiRemoteModels = [];
212
+ this.addingPiRemoteError = (result && result.error) || '未获取到可用模型';
213
+ }
214
+ } catch (e) {
215
+ if (!this.showAddPiProviderModal) return;
216
+ this.addingPiRemoteModels = [];
217
+ this.addingPiRemoteError = e && e.message ? e.message : '模型列表获取失败';
218
+ } finally {
219
+ if (this.showAddPiProviderModal) this.addingPiRemoteLoading = false;
220
+ }
221
+ },
222
+ onAddingPiEndpointChange() {
223
+ if (this.addingPiRemoteDebounce) clearTimeout(this.addingPiRemoteDebounce);
224
+ this.addingPiRemoteDebounce = setTimeout(() => {
225
+ this.addingPiRemoteDebounce = null;
226
+ this.fetchAddingPiRemoteModels();
227
+ }, 600);
228
+ },
229
+ piFilteredAddingRemoteModels() {
230
+ const query = (this.addingPiProviderModel || '').trim().toLowerCase();
231
+ if (!query) return [];
232
+ return this.addingPiRemoteModels.filter((id) => id.toLowerCase().includes(query)).slice(0, 50);
233
+ },
234
+ openEditPiProvider(providerId) {
235
+ if (!this.isToolConfigWriteAllowed('pi') || this.piSaving) return;
236
+ this.piRemoteModels = [];
237
+ this.piRemoteModelError = '';
238
+ this.piModelSearch = '';
239
+ const provider = this.piProviders[providerId] || {};
240
+ const knownKeys = ['id', 'name', 'title', 'baseUrl', 'api', 'apiKey', 'models', 'headers', 'configJson'];
241
+ const extras = {};
242
+ Object.keys(provider).forEach((key) => {
243
+ if (!knownKeys.includes(key)) extras[key] = provider[key];
244
+ });
245
+ const form = {
246
+ id: provider.id || providerId,
247
+ baseUrl: provider.baseUrl || '',
248
+ api: provider.api || '',
249
+ apiKey: provider.apiKey || '',
250
+ configJsonDraft: JSON.stringify(extras, null, 2),
251
+ models: Array.isArray(provider.models) && provider.models.length
252
+ ? provider.models.map((model) => ({
253
+ id: model?.id || '',
254
+ name: model?.name || '',
255
+ reasoning: model?.reasoning || '',
256
+ contextWindow: model?.contextWindow ?? model?.context_window ?? '',
257
+ maxTokens: model?.maxTokens ?? model?.max_tokens ?? '',
258
+ input: model?.input || ''
259
+ }))
260
+ : []
261
+ };
262
+ this.editingPiProvider = {
263
+ id: providerId,
264
+ form,
265
+ extras,
266
+ original: provider
267
+ };
268
+ this.showAddPiProviderModal = false;
269
+ this.fetchPiRemoteModels();
270
+ },
271
+ resetPiProviderEditing() {
272
+ this.editingPiProvider = null;
273
+ this.piRemoteModels = [];
274
+ this.piRemoteModelError = '';
275
+ this.piModelSearch = '';
276
+ },
277
+ removePiProviderModel(index) {
278
+ if (!this.editingPiProvider) return;
279
+ const list = this.editingPiProvider.form.models;
280
+ if (Array.isArray(list)) list.splice(index, 1);
281
+ },
282
+ async fetchPiRemoteModels() {
283
+ const provider = this.editingPiProvider;
284
+ if (!provider) return;
285
+ this.piRemoteModelsLoading = true;
286
+ this.piRemoteModelError = '';
287
+ try {
288
+ const result = await apiClient('fetch-pi-remote-models', {
289
+ baseUrl: provider.form.baseUrl || '',
290
+ apiKey: provider.form.apiKey || ''
291
+ });
292
+ if (this.editingPiProvider !== provider) return;
293
+ if (result && Array.isArray(result.models) && result.models.length > 0) {
294
+ this.piRemoteModels = result.models;
295
+ } else {
296
+ this.piRemoteModels = [];
297
+ this.piRemoteModelError = (result && result.error) || '未获取到可用模型';
298
+ }
299
+ } catch (e) {
300
+ if (this.editingPiProvider !== provider) return;
301
+ this.piRemoteModels = [];
302
+ this.piRemoteModelError = e && e.message ? e.message : '模型列表获取失败';
303
+ } finally {
304
+ if (this.editingPiProvider === provider) this.piRemoteModelsLoading = false;
305
+ }
306
+ },
307
+ piFilteredRemoteModels() {
308
+ const query = (this.piModelSearch || '').trim().toLowerCase();
309
+ if (!query) return [];
310
+ return this.piRemoteModels.filter((id) => id.toLowerCase().includes(query)).slice(0, 50);
311
+ },
312
+ addPiRemoteModel(modelId) {
313
+ if (!this.isToolConfigWriteAllowed('pi') || !this.editingPiProvider) return;
314
+ const trimmed = typeof modelId === 'string' ? modelId.trim() : '';
315
+ if (!trimmed) return;
316
+ const list = this.editingPiProvider.form.models;
317
+ if (!Array.isArray(list)) return;
318
+ list.splice(0, list.length, {
319
+ id: trimmed,
320
+ name: trimmed,
321
+ reasoning: false,
322
+ contextWindow: 128000,
323
+ maxTokens: 32000,
324
+ input: ''
325
+ });
326
+ this.piModelSearch = '';
327
+ },
328
+ async confirmDeletePiProvider() {
329
+ if (!this.editingPiProvider) return;
330
+ const providerId = this.editingPiProvider.id;
331
+ const confirmed = await this.requestConfirmDialog({
332
+ title: this.t('pi.providers.deleteTitle'),
333
+ message: this.t('pi.providers.deleteMessage', { name: this.piProviderName(providerId) }),
334
+ confirmText: this.t('confirm.ok'),
335
+ cancelText: this.t('confirm.cancel'),
336
+ danger: true
337
+ });
338
+ if (!confirmed) return;
339
+ await this.removePiProvider(providerId);
340
+ const updated = { ...this.piProviders };
341
+ delete updated[providerId];
342
+ this.piProviders = updated;
343
+ this.piProviderIds = Object.keys(this.piProviders).sort();
344
+ this.resetPiProviderEditing();
345
+ this.message = '供应商已删除';
346
+ this.messageType = 'success';
347
+ },
348
+ piProviderName(providerId) {
349
+ const provider = this.piProviders[providerId] || {};
350
+ if (provider.title || provider.name) return provider.title || provider.name;
351
+ const urlTitle = this.piProviderUrlTitle(provider);
352
+ return urlTitle || providerId;
353
+ },
354
+ piProviderUrlTitle(provider) {
355
+ const baseUrl = provider && provider.baseUrl ? String(provider.baseUrl) : '';
356
+ if (!baseUrl) return '';
357
+ try {
358
+ const hostname = new URL(baseUrl).hostname;
359
+ return hostname.replace(/^www\./, '');
360
+ } catch (e) {
361
+ return '';
362
+ }
363
+ },
364
+ piProviderSummary(providerId) {
365
+ const provider = this.piProviders[providerId] || {};
366
+ const baseUrl = provider.baseUrl || '';
367
+ const api = provider.api || '';
368
+ return [baseUrl, api].filter(Boolean).join(' • ') || 'Piper provider';
369
+ }
370
+ };
371
+ }
@@ -357,6 +357,42 @@ export function createSessionActionMethods(options = {}) {
357
357
  }
358
358
  },
359
359
 
360
+ normalizeConfigModeVisibility(value) {
361
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
362
+ const result = {};
363
+ for (const mode of ['codex', 'claude', 'openclaw', 'opencode', 'kilocode', 'pi']) {
364
+ result[mode] = typeof source[mode] === 'boolean' ? source[mode] : true;
365
+ }
366
+ return result;
367
+ },
368
+
369
+ isConfigModeVisible(mode) {
370
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
371
+ if (!this.configModeVisibility) return true;
372
+ return this.configModeVisibility[normalizedMode] !== false;
373
+ },
374
+
375
+ setConfigModeVisibility(mode, visible) {
376
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
377
+ if (!normalizedMode || !['codex', 'claude', 'openclaw', 'opencode', 'kilocode', 'pi'].includes(normalizedMode)) return;
378
+ const visibility = this.configModeVisibility || { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true, pi: true };
379
+ const next = { ...visibility, [normalizedMode]: !!visible };
380
+ const visibleCount = Object.values(next).filter(Boolean).length;
381
+ if (visibleCount === 0) {
382
+ this.showMessage(typeof this.t === 'function' ? this.t('settings.configTabs.minRequired') : 'At least one tab must remain visible', 'info');
383
+ return;
384
+ }
385
+ this.configModeVisibility = next;
386
+ if (typeof this.persistWebUiPreferences === 'function') {
387
+ this.persistWebUiPreferences({ configModeVisibility: next });
388
+ }
389
+ if (this.mainTab === 'config' && this.configMode === normalizedMode && !next[normalizedMode]) {
390
+ const fallback = ['codex', 'claude', 'openclaw', 'opencode', 'kilocode', 'pi'].find(m => next[m] !== false);
391
+ if (fallback) this.switchConfigMode(fallback);
392
+ }
393
+ },
394
+
395
+
360
396
  getShareCommandPrefixInvocation() {
361
397
  const prefix = this.normalizeShareCommandPrefix(this.shareCommandPrefix);
362
398
  return prefix === 'codexmate' ? 'codexmate' : 'npm start --';
@@ -393,6 +429,27 @@ export function createSessionActionMethods(options = {}) {
393
429
  }
394
430
  },
395
431
 
432
+ async copySessionMessage(msg) {
433
+ const text = msg && typeof msg.text === 'string' ? msg.text : '';
434
+ if (!text) {
435
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.empty') : 'Nothing to copy', 'info');
436
+ return;
437
+ }
438
+ const ok = this.fallbackCopyText(text);
439
+ if (ok) {
440
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
441
+ return;
442
+ }
443
+ try {
444
+ if (navigator.clipboard && window.isSecureContext) {
445
+ await navigator.clipboard.writeText(text);
446
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
447
+ return;
448
+ }
449
+ } catch (_) {}
450
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.fail') : 'Copy failed', 'error');
451
+ },
452
+
396
453
  copyAgentsContent() {
397
454
  const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
398
455
  if (!text) {
@@ -145,7 +145,8 @@ export function createStartupClaudeMethods(options = {}) {
145
145
  claude: statusRes.toolConfigPermissions.claude === true,
146
146
  opencode: statusRes.toolConfigPermissions.opencode === true,
147
147
  kilocode: statusRes.toolConfigPermissions.kilocode === true,
148
- openclaw: statusRes.toolConfigPermissions.openclaw === true
148
+ openclaw: statusRes.toolConfigPermissions.openclaw === true,
149
+ pi: statusRes.toolConfigPermissions.pi === true
149
150
  };
150
151
  }
151
152
  this.providersList = listRes.providers;
@@ -223,6 +223,9 @@ export function createSystemPromptMethods(options = {}) {
223
223
  this.showMessage(res.error, 'error');
224
224
  return;
225
225
  }
226
+ if (typeof res.historyBucket === 'string' && res.historyBucket) {
227
+ this.sysHistoryBucket = res.historyBucket;
228
+ }
226
229
  this.showMessage(this.t('sysPrompt.toast.saved'), 'success');
227
230
  await this.loadSystemPrompt();
228
231
  } catch (e) {
@@ -289,6 +292,84 @@ export function createSystemPromptMethods(options = {}) {
289
292
  return { text: this.t('sysPrompt.hint.twoStepSave'), warn: false };
290
293
  }
291
294
  return null;
295
+ },
296
+
297
+ resolveSysHistoryBucket() {
298
+ if (typeof this.sysHistoryBucket === 'string' && this.sysHistoryBucket) {
299
+ return this.sysHistoryBucket;
300
+ }
301
+ const scope = this.sysPromptScope || 'global';
302
+ return 'system_' + scope + '_global';
303
+ },
304
+
305
+ async openSysHistory() {
306
+ if (this.sysHistoryLoading) return;
307
+ const bucket = this.resolveSysHistoryBucket();
308
+ this.sysHistoryVisible = true;
309
+ this.sysHistoryError = '';
310
+ await this.loadSysHistory(bucket);
311
+ },
312
+
313
+ async loadSysHistory(bucketArg) {
314
+ if (this.sysHistoryLoading) return;
315
+ const bucket = typeof bucketArg === 'string' && bucketArg.trim()
316
+ ? bucketArg.trim()
317
+ : this.resolveSysHistoryBucket();
318
+ this.sysHistoryBucket = bucket;
319
+ this.sysHistoryLoading = true;
320
+ this.sysHistoryError = '';
321
+ this.sysHistoryItems = [];
322
+ this.sysHistoryPreviewId = '';
323
+ this.sysHistoryPreviewContent = '';
324
+ try {
325
+ const res = await api('list-prompt-history', { bucket });
326
+ if (res && res.error) {
327
+ this.sysHistoryError = res.error;
328
+ return;
329
+ }
330
+ this.sysHistoryItems = Array.isArray(res) ? res : [];
331
+ } catch (e) {
332
+ this.sysHistoryError = this.t('toast.load.fail');
333
+ } finally {
334
+ this.sysHistoryLoading = false;
335
+ }
336
+ },
337
+
338
+ closeSysHistory() {
339
+ this.sysHistoryVisible = false;
340
+ this.sysHistoryPreviewId = '';
341
+ this.sysHistoryPreviewContent = '';
342
+ this.sysHistoryError = '';
343
+ },
344
+
345
+ async viewSysHistoryItem(item) {
346
+ if (!item || !item.id) return;
347
+ if (this.sysHistoryLoading) return;
348
+ if (this.sysHistoryPreviewId === item.id && this.sysHistoryPreviewContent) return;
349
+ this.sysHistoryPreviewId = item.id;
350
+ this.sysHistoryPreviewContent = '';
351
+ try {
352
+ const res = await api('get-prompt-history', { bucket: this.sysHistoryBucket, id: item.id });
353
+ if (res && res.error) {
354
+ this.sysHistoryError = res.error;
355
+ this.sysHistoryPreviewId = '';
356
+ return;
357
+ }
358
+ this.sysHistoryPreviewContent = typeof res.content === 'string' ? res.content : '';
359
+ } catch (e) {
360
+ this.sysHistoryError = this.t('toast.load.fail');
361
+ this.sysHistoryPreviewId = '';
362
+ }
363
+ },
364
+
365
+ applySysHistoryToEditor() {
366
+ if (!this.sysHistoryPreviewContent) return;
367
+ if (this.sysPromptSaving || this.sysPromptDiffVisible) return;
368
+ this.sysPromptContent = this.sysHistoryPreviewContent;
369
+ this.sysHistoryVisible = false;
370
+ this.sysHistoryPreviewId = '';
371
+ this.sysHistoryPreviewContent = '';
372
+ this.showMessage(this.t('toast.history.restored'), 'success');
292
373
  }
293
374
  };
294
375
  }
@@ -3,7 +3,7 @@ export function createToolConfigPermissionMethods(options = {}) {
3
3
 
4
4
  function normalizeTarget(value) {
5
5
  const target = typeof value === 'string' ? value.trim().toLowerCase() : '';
6
- return target === 'codex' || target === 'claude' || target === 'opencode' || target === 'kilocode' || target === 'openclaw' ? target : '';
6
+ return target === 'codex' || target === 'claude' || target === 'opencode' || target === 'kilocode' || target === 'openclaw' || target === 'pi' ? target : '';
7
7
  }
8
8
 
9
9
  function normalizePermissions(value) {
@@ -13,7 +13,8 @@ export function createToolConfigPermissionMethods(options = {}) {
13
13
  claude: source.claude === true,
14
14
  opencode: source.opencode === true,
15
15
  kilocode: source.kilocode === true,
16
- openclaw: source.openclaw === true
16
+ openclaw: source.openclaw === true,
17
+ pi: source.pi === true
17
18
  };
18
19
  }
19
20
 
@@ -347,6 +347,9 @@ export function createWebUiPreferencesMethods(options = {}) {
347
347
  ? source.projectClaudeMdPath
348
348
  : (typeof this.projectClaudeMdPath === 'string' ? this.projectClaudeMdPath : ''),
349
349
  sidebarCollapsed: normalizeBoolean(hasOwn(source, 'sidebarCollapsed') ? source.sidebarCollapsed : this.sidebarCollapsed, false),
350
+ configModeVisibility: typeof this.normalizeConfigModeVisibility === 'function'
351
+ ? this.normalizeConfigModeVisibility(hasOwn(source, 'configModeVisibility') ? source.configModeVisibility : this.configModeVisibility)
352
+ : { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true },
350
353
  starPrompted: normalizeBoolean(hasOwn(source, 'starPrompted') ? source.starPrompted : this.starPrompted, false),
351
354
  sessionLoadNativeDialog: normalizeBoolean(hasOwn(source, 'sessionLoadNativeDialog') ? source.sessionLoadNativeDialog : this.sessionLoadNativeDialog, false),
352
355
  language: typeof source.language === 'string'
@@ -412,6 +415,9 @@ export function createWebUiPreferencesMethods(options = {}) {
412
415
  if (hasOwn(source, 'sidebarCollapsed')) {
413
416
  this.sidebarCollapsed = normalizeBoolean(source.sidebarCollapsed, false);
414
417
  }
418
+ if (hasOwn(source, 'configModeVisibility') && typeof this.normalizeConfigModeVisibility === 'function') {
419
+ this.configModeVisibility = this.normalizeConfigModeVisibility(source.configModeVisibility);
420
+ }
415
421
  if (hasOwn(source, 'starPrompted')) {
416
422
  this.starPrompted = normalizeBoolean(source.starPrompted, false);
417
423
  }
@@ -451,7 +457,8 @@ export function createWebUiPreferencesMethods(options = {}) {
451
457
  claude: source.toolConfigPermissions.claude === true,
452
458
  opencode: source.toolConfigPermissions.opencode === true,
453
459
  kilocode: source.toolConfigPermissions.kilocode === true,
454
- openclaw: source.toolConfigPermissions.openclaw === true
460
+ openclaw: source.toolConfigPermissions.openclaw === true,
461
+ pi: source.toolConfigPermissions.pi === true
455
462
  };
456
463
  }
457
464
  if (Array.isArray(source.deletedClaudeSettingsImports)) {
@@ -12,7 +12,8 @@ export const CONFIG_MODE_SET = new Set([
12
12
  'claude',
13
13
  'openclaw',
14
14
  'opencode',
15
- 'kilocode'
15
+ 'kilocode',
16
+ 'pi'
16
17
  ]);
17
18
 
18
19
  export function getProviderConfigModeMeta(mode) {