codexmate 0.0.53 → 0.0.55

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/web-ui/app.js CHANGED
@@ -79,6 +79,8 @@ document.addEventListener('DOMContentLoaded', () => {
79
79
  projectPathOptionsLoading: false,
80
80
  showSkillsModal: false,
81
81
  showHealthCheckModal: false,
82
+ showProviderCacheModal: false,
83
+ showProviderCacheAnnouncementModal: false,
82
84
  showCodexBridgePoolModal: false,
83
85
  showClaudeBridgePoolModal: false,
84
86
  showWebhookModal: false,
@@ -378,6 +380,14 @@ document.addEventListener('DOMContentLoaded', () => {
378
380
  codexDownloadLoading: false,
379
381
  codexDownloadProgress: 0,
380
382
  codexDownloadTimer: null,
383
+ providerCacheRecords: { root: '', generatedAt: '', groups: [] },
384
+ providerCacheLoadedOnce: false,
385
+ providerCacheLoadedAt: '',
386
+ providerCacheLoading: false,
387
+ providerCacheSyncing: false,
388
+ providerCacheSyncMessage: '',
389
+ providerCacheError: '',
390
+ providerCacheRequestSeq: 0,
381
391
  settingsTab: 'general',
382
392
  toolConfigPermissions: (function() {
383
393
  try {
@@ -506,6 +516,18 @@ document.addEventListener('DOMContentLoaded', () => {
506
516
  if (typeof this.loadWebhookSettings === 'function') {
507
517
  this.loadWebhookSettings();
508
518
  }
519
+ if (typeof this.loadWebUiPreferences === 'function') {
520
+ const applyPreferenceNavigation = (() => {
521
+ try {
522
+ const url = new URL(window.location.href);
523
+ if (url.pathname === '/session') return false;
524
+ return !String(url.searchParams.get('tab') || '').trim();
525
+ } catch (_) {
526
+ return true;
527
+ }
528
+ })();
529
+ void this.loadWebUiPreferences({ applyNavigation: applyPreferenceNavigation });
530
+ }
509
531
  if (typeof this.t === 'function') {
510
532
  this.confirmDialogConfirmText = this.t('confirm.ok');
511
533
  this.confirmDialogCancelText = this.t('confirm.cancel');
@@ -693,8 +715,14 @@ document.addEventListener('DOMContentLoaded', () => {
693
715
  if (typeof this.loadAppVersionStatus === 'function') {
694
716
  void this.loadAppVersionStatus({ silent: true });
695
717
  }
718
+ if (typeof this.hydrateClaudeConfigsFromProviderCache === 'function') {
719
+ await this.hydrateClaudeConfigsFromProviderCache({ silent: true });
720
+ }
696
721
  void this.refreshClaudeSelectionFromSettings({ silent: true });
697
722
  void this.syncDefaultOpenclawConfigEntry({ silent: true });
723
+ if (typeof this.loadProviderCacheRecords === 'function') {
724
+ void this.loadProviderCacheRecords({ background: true });
725
+ }
698
726
  };
699
727
  if (typeof requestAnimationFrame === 'function') {
700
728
  this._initialLoadRafId = requestAnimationFrame(() => {
@@ -743,6 +771,10 @@ document.addEventListener('DOMContentLoaded', () => {
743
771
  clearTimeout(this._initialLoadTimer);
744
772
  this._initialLoadTimer = 0;
745
773
  }
774
+ if (this.__webUiPreferencesPersistTimer) {
775
+ clearTimeout(this.__webUiPreferencesPersistTimer);
776
+ this.__webUiPreferencesPersistTimer = 0;
777
+ }
746
778
  window.removeEventListener('resize', this.onWindowResize);
747
779
  window.removeEventListener('keydown', this.handleGlobalKeydown);
748
780
  window.removeEventListener('beforeunload', this.handleBeforeUnload);
@@ -767,6 +799,9 @@ document.addEventListener('DOMContentLoaded', () => {
767
799
  try {
768
800
  localStorage.setItem('codexmate_prompts_sub_tab', newVal);
769
801
  } catch (_) {}
802
+ if (typeof this.persistWebUiPreferences === 'function') {
803
+ this.persistWebUiPreferences({ promptsSubTab: newVal });
804
+ }
770
805
  if (this.mainTab === 'prompts' && typeof this.loadPromptsContent === 'function') {
771
806
  this.loadPromptsContent();
772
807
  }
@@ -779,6 +814,9 @@ document.addEventListener('DOMContentLoaded', () => {
779
814
  localStorage.removeItem('codexmate_project_claude_md_path');
780
815
  }
781
816
  } catch (_) {}
817
+ if (typeof this.persistWebUiPreferences === 'function') {
818
+ this.persistWebUiPreferences({ projectClaudeMdPath: newPath || '' });
819
+ }
782
820
  }
783
821
  },
784
822
 
@@ -191,6 +191,14 @@ export function matchClaudeConfigFromSettings(claudeConfigs = {}, env = {}) {
191
191
  if (normalizedSettings.apiKey && normalizedConfig.apiKey === normalizedSettings.apiKey) {
192
192
  return name;
193
193
  }
194
+ if (normalizedSettings.apiKey
195
+ && normalizedConfig.apiKey === ''
196
+ && config
197
+ && typeof config.providerCacheRef === 'string'
198
+ && config.providerCacheRef.trim()
199
+ && config.hasKey === true) {
200
+ return name;
201
+ }
194
202
  if (!normalizedSettings.apiKey
195
203
  && normalizedConfig.apiKey === ''
196
204
  && normalizedConfig.externalCredentialType
@@ -20,6 +20,7 @@ function getClaudeConfigValidationForContext(vm, mode = 'add') {
20
20
  const draft = mode === 'edit' ? vm.editingConfig : vm.newClaudeConfig;
21
21
  const name = normalizeClaudeText(draft && draft.name);
22
22
  const apiKey = normalizeClaudeText(draft && draft.apiKey);
23
+ const providerCacheRef = normalizeClaudeText(draft && draft.providerCacheRef);
23
24
  const externalCredentialType = normalizeClaudeText(draft && draft.externalCredentialType);
24
25
  const baseUrl = normalizeClaudeBaseUrl(draft && draft.baseUrl);
25
26
  const model = normalizeClaudeText(draft && draft.model);
@@ -40,7 +41,7 @@ function getClaudeConfigValidationForContext(vm, mode = 'add') {
40
41
  errors.name = vm.t('validation.claude.nameExists');
41
42
  }
42
43
 
43
- if (!apiKey && !externalCredentialType && targetApi !== 'ollama') {
44
+ if (!apiKey && !externalCredentialType && !providerCacheRef && targetApi !== 'ollama') {
44
45
  errors.apiKey = vm.t('validation.claude.apiKeyRequired');
45
46
  }
46
47
 
@@ -58,6 +59,7 @@ function getClaudeConfigValidationForContext(vm, mode = 'add') {
58
59
  mode,
59
60
  name,
60
61
  apiKey,
62
+ providerCacheRef,
61
63
  externalCredentialType,
62
64
  baseUrl,
63
65
  model,
@@ -112,6 +114,64 @@ export function createClaudeConfigMethods(options = {}) {
112
114
  this.syncClaudeBridgeProviders();
113
115
  },
114
116
 
117
+ async hydrateClaudeConfigsFromProviderCache(options = {}) {
118
+ const silent = options && options.silent === true;
119
+ try {
120
+ const res = await api('get-claude-provider-cache-configs');
121
+ if (!res || res.error) {
122
+ if (!silent) this.showMessage((res && res.error) || this.t('toast.claude.loadSettingsFail'), 'error');
123
+ return false;
124
+ }
125
+ const providers = Array.isArray(res.providers) ? res.providers : [];
126
+ if (providers.length === 0) return true;
127
+ const configs = this.claudeConfigs && typeof this.claudeConfigs === 'object' ? this.claudeConfigs : {};
128
+ let changed = false;
129
+ let firstCachedName = '';
130
+ for (const provider of providers) {
131
+ if (!provider || typeof provider !== 'object') continue;
132
+ const name = normalizeClaudeText(provider.name);
133
+ const baseUrl = normalizeClaudeBaseUrl(provider.baseUrl);
134
+ const model = normalizeClaudeText(provider.model);
135
+ if (!name || !baseUrl || !model) continue;
136
+ if (!firstCachedName) firstCachedName = name;
137
+ const cachedConfig = {
138
+ apiKey: '',
139
+ baseUrl,
140
+ model,
141
+ hasKey: provider.hasKey === true,
142
+ providerCacheRef: normalizeClaudeText(provider.providerCacheRef) || name,
143
+ source: 'provider-cache',
144
+ targetApi: normalizeClaudeText(provider.targetApi) || 'responses'
145
+ };
146
+ const existing = configs[name];
147
+ if (existing && existing.source !== 'provider-cache' && existing.providerCacheRef !== cachedConfig.providerCacheRef) {
148
+ continue;
149
+ }
150
+ if (JSON.stringify(existing || {}) !== JSON.stringify(cachedConfig)) {
151
+ configs[name] = cachedConfig;
152
+ changed = true;
153
+ }
154
+ }
155
+ this.claudeConfigs = configs;
156
+ if (firstCachedName) {
157
+ let savedCurrent = '';
158
+ try { savedCurrent = localStorage.getItem('currentClaudeConfig') || ''; } catch (_) {}
159
+ const current = normalizeClaudeText(this.currentClaudeConfig);
160
+ const currentConfig = current && configs[current] ? configs[current] : null;
161
+ if (!savedCurrent && (!current || (currentConfig && currentConfig.hasKey === false && !currentConfig.providerCacheRef))) {
162
+ this.currentClaudeConfig = firstCachedName;
163
+ try { localStorage.setItem('currentClaudeConfig', firstCachedName); } catch (_) {}
164
+ changed = true;
165
+ }
166
+ }
167
+ if (changed) this.saveClaudeConfigs();
168
+ return true;
169
+ } catch (e) {
170
+ if (!silent) this.showMessage(e && e.message ? e.message : this.t('toast.claude.loadSettingsFail'), 'error');
171
+ return false;
172
+ }
173
+ },
174
+
115
175
  async syncClaudeBridgeProviders() {
116
176
  try { await api('claude-local-bridge-sync-providers', { providers: this.claudeConfigs || {} }); } catch (_) {}
117
177
  },
@@ -154,6 +214,7 @@ export function createClaudeConfigMethods(options = {}) {
154
214
  model: config.model || '',
155
215
  targetApi: config.targetApi || 'responses'
156
216
  };
217
+ if (config.providerCacheRef) this.editingConfig.providerCacheRef = config.providerCacheRef;
157
218
  this.showEditClaudeConfigKey = false;
158
219
  this.showEditConfigModal = true;
159
220
  },
@@ -165,6 +226,8 @@ export function createClaudeConfigMethods(options = {}) {
165
226
  }
166
227
  const name = validation.name;
167
228
  this.editingConfig.apiKey = validation.apiKey;
229
+ if (validation.providerCacheRef) this.editingConfig.providerCacheRef = validation.providerCacheRef;
230
+ else delete this.editingConfig.providerCacheRef;
168
231
  this.editingConfig.externalCredentialType = validation.externalCredentialType;
169
232
  this.editingConfig.baseUrl = validation.baseUrl;
170
233
  this.editingConfig.model = validation.model;
@@ -195,6 +258,8 @@ export function createClaudeConfigMethods(options = {}) {
195
258
  }
196
259
  const name = validation.name;
197
260
  this.editingConfig.apiKey = validation.apiKey;
261
+ if (validation.providerCacheRef) this.editingConfig.providerCacheRef = validation.providerCacheRef;
262
+ else delete this.editingConfig.providerCacheRef;
198
263
  this.editingConfig.externalCredentialType = validation.externalCredentialType;
199
264
  this.editingConfig.baseUrl = validation.baseUrl;
200
265
  this.editingConfig.model = validation.model;
@@ -203,7 +268,7 @@ export function createClaudeConfigMethods(options = {}) {
203
268
  this.saveClaudeConfigs();
204
269
 
205
270
  const config = this.claudeConfigs[name];
206
- if (!config.apiKey && config.targetApi !== 'ollama') {
271
+ if (!config.apiKey && !config.providerCacheRef && config.targetApi !== 'ollama') {
207
272
  this.showMessage(this.t('toast.claude.savedWithoutKey'), 'info');
208
273
  this.closeEditConfigModal();
209
274
  if (name === this.currentClaudeConfig) {
@@ -212,7 +277,7 @@ export function createClaudeConfigMethods(options = {}) {
212
277
  return;
213
278
  }
214
279
 
215
- const _claudeKey = `${name}|${config.apiKey || ""}|${config.baseUrl || ""}|${config.model || ""}|${config.targetApi || "responses"}`;
280
+ const _claudeKey = `${name}|${config.apiKey || ""}|${config.providerCacheRef || ""}|${config.baseUrl || ""}|${config.model || ""}|${config.targetApi || "responses"}`;
216
281
  try {
217
282
  const res = await api('apply-claude-config', { config: { ...config, name } });
218
283
  if (res.error || res.success === false) {
@@ -238,6 +303,8 @@ export function createClaudeConfigMethods(options = {}) {
238
303
  }
239
304
  this.newClaudeConfig.name = validation.name;
240
305
  this.newClaudeConfig.apiKey = validation.apiKey;
306
+ if (validation.providerCacheRef) this.newClaudeConfig.providerCacheRef = validation.providerCacheRef;
307
+ else delete this.newClaudeConfig.providerCacheRef;
241
308
  this.newClaudeConfig.externalCredentialType = validation.externalCredentialType;
242
309
  this.newClaudeConfig.baseUrl = validation.baseUrl;
243
310
  this.newClaudeConfig.model = validation.model;
@@ -284,14 +351,14 @@ export function createClaudeConfigMethods(options = {}) {
284
351
  this.refreshClaudeModelContext();
285
352
  const config = this.claudeConfigs[name];
286
353
 
287
- if (!config.apiKey && config.targetApi !== 'ollama') {
354
+ if (!config.apiKey && !config.providerCacheRef && config.targetApi !== 'ollama') {
288
355
  if (config.externalCredentialType) {
289
356
  return this.showMessage(this.t('toast.claude.externalAuth'), 'info');
290
357
  }
291
358
  return this.showMessage(this.t('toast.claude.apiKeyRequired'), 'error');
292
359
  }
293
360
 
294
- const _claudeKey2 = `${name}|${config.apiKey || ""}|${config.baseUrl || ""}|${config.model || ""}|${config.targetApi || "responses"}`;
361
+ const _claudeKey2 = `${name}|${config.apiKey || ""}|${config.providerCacheRef || ""}|${config.baseUrl || ""}|${config.model || ""}|${config.targetApi || "responses"}`;
295
362
  try {
296
363
  const res = await api('apply-claude-config', { config: { ...config, name } });
297
364
  if (res.error || res.success === false) {
@@ -21,6 +21,7 @@ import { createOpenclawEditingMethods } from './app.methods.openclaw-editing.mjs
21
21
  import { createOpenclawPersistMethods } from './app.methods.openclaw-persist.mjs';
22
22
  import { createOpencodeConfigMethods } from './app.methods.opencode-config.mjs';
23
23
  import { createProvidersMethods } from './app.methods.providers.mjs';
24
+ import { createProviderCacheMethods } from './app.methods.provider-cache.mjs';
24
25
  import { createRuntimeMethods } from './app.methods.runtime.mjs';
25
26
  import { createToolConfigPermissionMethods } from './app.methods.tool-config-permissions.mjs';
26
27
  import { createTaskOrchestrationMethods } from './app.methods.task-orchestration.mjs';
@@ -33,6 +34,7 @@ import { createSkillsMethods } from './skills.methods.mjs';
33
34
  import { createPluginsMethods } from './plugins.methods.mjs';
34
35
  import { createI18nMethods } from './i18n.mjs';
35
36
  import { createWebhookMethods } from './app.methods.webhook.mjs';
37
+ import { createWebUiPreferencesMethods } from './app.methods.web-ui-preferences.mjs';
36
38
  import {
37
39
  CONFIG_MODE_SET,
38
40
  getProviderConfigModeMeta
@@ -83,6 +85,8 @@ export function createAppMethods() {
83
85
  ...createPluginsMethods(),
84
86
  ...createAgentsMethods({ api, apiWithMeta }),
85
87
  ...createProvidersMethods({ api }),
88
+ ...createProviderCacheMethods({ api }),
89
+ ...createWebUiPreferencesMethods({ api }),
86
90
  ...createClaudeConfigMethods({ api }),
87
91
  ...createToolConfigPermissionMethods({ api }),
88
92
  ...createOpenclawCoreMethods(),
@@ -108,6 +108,9 @@
108
108
  try {
109
109
  localStorage.setItem(NAV_STATE_STORAGE_KEY, JSON.stringify(snapshot));
110
110
  } catch (_) {}
111
+ if (typeof vm.persistWebUiPreferences === 'function') {
112
+ vm.persistWebUiPreferences({ navigation: snapshot });
113
+ }
111
114
  };
112
115
 
113
116
  return {
@@ -0,0 +1,223 @@
1
+ export function createProviderCacheMethods(options = {}) {
2
+ const { api } = options;
3
+
4
+ return {
5
+ async openProviderCacheModal(options = {}) {
6
+ this.showProviderCacheModal = true;
7
+ if (options.forceRefresh === true || !this.providerCacheLoadedOnce) {
8
+ await this.loadProviderCacheRecords({ forceRefresh: options.forceRefresh === true });
9
+ }
10
+ },
11
+
12
+ closeProviderCacheModal() {
13
+ this.showProviderCacheModal = false;
14
+ },
15
+
16
+ async openProviderCacheAnnouncementModal() {
17
+ this.showProviderCacheAnnouncementModal = true;
18
+ if (!this.providerCacheLoadedOnce) {
19
+ await this.loadProviderCacheRecords();
20
+ }
21
+ },
22
+
23
+ closeProviderCacheAnnouncementModal() {
24
+ this.showProviderCacheAnnouncementModal = false;
25
+ },
26
+
27
+ async openProviderCacheDetailsFromAnnouncement() {
28
+ this.showProviderCacheAnnouncementModal = false;
29
+ await this.openProviderCacheModal({ forceRefresh: false });
30
+ },
31
+
32
+ async loadProviderCacheRecords(options = {}) {
33
+ const forceRefresh = options && options.forceRefresh === true;
34
+ const background = options && options.background === true;
35
+ if (this.providerCacheLoading && !forceRefresh) return;
36
+ const requestSeq = (Number(this.providerCacheRequestSeq) || 0) + 1;
37
+ this.providerCacheRequestSeq = requestSeq;
38
+ const isLatestRequest = () => requestSeq === Number(this.providerCacheRequestSeq || 0);
39
+ if (!background) {
40
+ this.providerCacheLoading = true;
41
+ }
42
+ this.providerCacheError = '';
43
+ try {
44
+ const res = await api('get-provider-cache-records');
45
+ if (!isLatestRequest()) return;
46
+ if (res && res.error) {
47
+ this.providerCacheError = res.error;
48
+ return;
49
+ }
50
+ this.providerCacheRecords = res && typeof res === 'object' ? res : { groups: [] };
51
+ this.providerCacheLoadedOnce = true;
52
+ this.providerCacheLoadedAt = this.providerCacheRecords.generatedAt || new Date().toISOString();
53
+ } catch (e) {
54
+ if (!isLatestRequest()) return;
55
+ this.providerCacheError = e && e.message ? e.message : this.t('modal.providerCache.loadFailed');
56
+ } finally {
57
+ if (isLatestRequest() && !background) {
58
+ this.providerCacheLoading = false;
59
+ }
60
+ }
61
+ },
62
+
63
+ async syncProviderCacheRecords() {
64
+ if (this.providerCacheSyncing) return;
65
+ this.providerCacheSyncing = true;
66
+ this.providerCacheError = '';
67
+ this.providerCacheSyncMessage = '';
68
+ try {
69
+ const res = await api('sync-provider-cache-records');
70
+ if (res && res.error) {
71
+ this.providerCacheError = res.errorKey ? this.t(res.errorKey) : res.error;
72
+ return;
73
+ }
74
+ const summary = res && res.summary && typeof res.summary === 'object' ? res.summary : {};
75
+ const providerCount = Number(summary.providerCount || 0);
76
+ const fileCount = Number(summary.fileCount || 0);
77
+ this.providerCacheSyncMessage = this.t('modal.providerCache.syncSucceeded', { count: providerCount, fileCount });
78
+ if (res && res.records && typeof res.records === 'object') {
79
+ this.providerCacheRecords = res.records;
80
+ this.providerCacheLoadedOnce = true;
81
+ this.providerCacheLoadedAt = this.providerCacheRecords.generatedAt || new Date().toISOString();
82
+ }
83
+ await this.loadProviderCacheRecords({ forceRefresh: true });
84
+ } catch (e) {
85
+ this.providerCacheError = e && e.message ? e.message : this.t('modal.providerCache.syncFailed');
86
+ } finally {
87
+ this.providerCacheSyncing = false;
88
+ }
89
+ },
90
+
91
+ getProviderCacheGroups() {
92
+ const records = this.providerCacheRecords && typeof this.providerCacheRecords === 'object'
93
+ ? this.providerCacheRecords
94
+ : {};
95
+ return Array.isArray(records.groups) ? records.groups : [];
96
+ },
97
+
98
+ getProviderCacheAnnouncementSummary() {
99
+ const groups = this.getProviderCacheGroups();
100
+ let fileCount = 0;
101
+ let providerCount = 0;
102
+ for (const group of groups) {
103
+ const files = this.getProviderCacheExistingFiles(group);
104
+ fileCount += files.length;
105
+ for (const file of files) {
106
+ const count = Number(file && file.providerCount);
107
+ if (Number.isFinite(count) && count > 0) {
108
+ providerCount += count;
109
+ } else {
110
+ providerCount += this.getProviderCacheFileProviders(file).length;
111
+ }
112
+ }
113
+ }
114
+ return {
115
+ groupCount: groups.length,
116
+ fileCount,
117
+ providerCount,
118
+ loadedAt: this.providerCacheLoadedAt || (this.providerCacheRecords && this.providerCacheRecords.generatedAt) || ''
119
+ };
120
+ },
121
+
122
+ getProviderCacheAnnouncementGroups() {
123
+ return this.getProviderCacheGroups().map((group) => {
124
+ const existingFiles = this.getProviderCacheExistingFiles(group);
125
+ return {
126
+ key: group && group.key ? group.key : '',
127
+ label: group && group.label ? group.label : '',
128
+ existingCount: existingFiles.length,
129
+ providerCount: existingFiles.reduce((sum, file) => {
130
+ const count = Number(file && file.providerCount);
131
+ if (Number.isFinite(count) && count > 0) return sum + count;
132
+ return sum + this.getProviderCacheFileProviders(file).length;
133
+ }, 0)
134
+ };
135
+ });
136
+ },
137
+
138
+ getProviderCacheExistingFiles(group) {
139
+ const files = group && Array.isArray(group.files) ? group.files : [];
140
+ return files.filter((file) => file && file.exists);
141
+ },
142
+
143
+ hasProviderCacheExistingFiles(group) {
144
+ return this.getProviderCacheExistingFiles(group).length > 0;
145
+ },
146
+
147
+ getProviderCacheFileKey(file) {
148
+ if (!file) return '';
149
+ return file.displayPath || file.path || file.name || '';
150
+ },
151
+
152
+ getProviderCacheFilePath(file) {
153
+ if (!file) return '';
154
+ return file.displayPath || file.path || file.name || '';
155
+ },
156
+
157
+ getProviderCacheFileSummary(file) {
158
+ if (!file || !file.exists) return '';
159
+ const count = Number(file.providerCount || 0);
160
+ if (count > 0) return this.t('modal.providerCache.providerCount', { count });
161
+ if (file.tooLarge) return this.t('modal.providerCache.tooLarge');
162
+ if (file.ok === false) return this.t('modal.providerCache.parseFailed');
163
+ return this.t('modal.providerCache.rawJsonOnly');
164
+ },
165
+
166
+ getProviderCacheFileProviders(file) {
167
+ const providers = file && Array.isArray(file.providers) ? file.providers : [];
168
+ return providers.filter((provider) => provider && typeof provider === 'object');
169
+ },
170
+
171
+ hasProviderCacheProviders(file) {
172
+ return this.getProviderCacheFileProviders(file).length > 0;
173
+ },
174
+
175
+ getProviderCacheProviderMeta(provider) {
176
+ if (!provider || typeof provider !== 'object') return [];
177
+ const fields = [
178
+ ['baseUrl', 'base_url'],
179
+ ['wireApi', 'wire_api'],
180
+ ['authMethod', 'auth'],
181
+ ['model', 'model']
182
+ ];
183
+ return fields
184
+ .map(([key, label]) => {
185
+ const value = provider[key];
186
+ if (value === undefined || value === null || value === '') return null;
187
+ return { label, value: String(value) };
188
+ })
189
+ .filter(Boolean);
190
+ },
191
+
192
+ getProviderCacheProviderText(provider) {
193
+ if (!provider || typeof provider !== 'object') return '';
194
+ try {
195
+ return JSON.stringify(provider.data === undefined ? provider : provider.data, null, 2);
196
+ } catch (_) {
197
+ return String(provider.name || '');
198
+ }
199
+ },
200
+
201
+ getProviderCacheRecordText(record) {
202
+ if (!record || !record.exists) return '';
203
+ if (record.ok === false) {
204
+ return record.error || '';
205
+ }
206
+ try {
207
+ return JSON.stringify(record.data === undefined ? null : record.data, null, 2);
208
+ } catch (_) {
209
+ return String(record.data || '');
210
+ }
211
+ },
212
+
213
+ formatProviderCacheFileSize(size) {
214
+ const bytes = Number(size);
215
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
216
+ if (bytes < 1024) return `${Math.floor(bytes)} B`;
217
+ const kib = bytes / 1024;
218
+ if (kib < 1024) return `${kib.toFixed(kib >= 10 ? 0 : 1)} KiB`;
219
+ const mib = kib / 1024;
220
+ return `${mib.toFixed(mib >= 10 ? 1 : 2)} MiB`;
221
+ }
222
+ };
223
+ }
@@ -281,6 +281,9 @@ export function createSessionActionMethods(options = {}) {
281
281
  try {
282
282
  localStorage.setItem('codexmateSessionTrashEnabled', enabled ? 'true' : 'false');
283
283
  } catch (_) {}
284
+ if (typeof this.persistWebUiPreferences === 'function') {
285
+ this.persistWebUiPreferences({ sessionTrashEnabled: enabled });
286
+ }
284
287
  },
285
288
 
286
289
  setSessionTimelineStyle(style) {
@@ -289,12 +292,18 @@ export function createSessionActionMethods(options = {}) {
289
292
  try {
290
293
  localStorage.setItem('codexmateSessionTimelineStyle', normalized);
291
294
  } catch (_) {}
295
+ if (typeof this.persistWebUiPreferences === 'function') {
296
+ this.persistWebUiPreferences({ sessionTimelineStyle: normalized });
297
+ }
292
298
  },
293
299
 
294
300
  setConfigTemplateDiffConfirmEnabled(value) {
295
301
  const enabled = this.normalizeConfigTemplateDiffConfirmEnabled(value);
296
302
  this.configTemplateDiffConfirmEnabled = enabled;
297
303
  persistConfigTemplateDiffConfirmEnabledToStorage(enabled);
304
+ if (typeof this.persistWebUiPreferences === 'function') {
305
+ this.persistWebUiPreferences({ configTemplateDiffConfirmEnabled: enabled });
306
+ }
298
307
  },
299
308
 
300
309
  getShareCommandPrefixInvocation() {
@@ -308,6 +317,9 @@ export function createSessionActionMethods(options = {}) {
308
317
  try {
309
318
  localStorage.setItem('codexmateShareCommandPrefix', normalized);
310
319
  } catch (_) {}
320
+ if (typeof this.persistWebUiPreferences === 'function') {
321
+ this.persistWebUiPreferences({ shareCommandPrefix: normalized });
322
+ }
311
323
  },
312
324
 
313
325
  fallbackCopyText(text) {
@@ -803,6 +803,9 @@ export function createSessionBrowserMethods(options = {}) {
803
803
  const range = normalized === 'all' ? 'all' : (normalized === '30d' ? '30d' : '7d');
804
804
  this.sessionsUsageTimeRange = range;
805
805
  try { localStorage.setItem('sessionsUsageTimeRange', range); } catch (_) {}
806
+ if (typeof this.persistWebUiPreferences === 'function') {
807
+ this.persistWebUiPreferences({ sessionsUsageTimeRange: range });
808
+ }
806
809
  if (range === 'all') {
807
810
  this.sessionsUsageCompareEnabled = false;
808
811
  }
@@ -311,6 +311,9 @@ export function createSessionTrashMethods(options = {}) {
311
311
  const normalized = this.normalizeSessionTrashRetentionDays(days);
312
312
  this.sessionTrashRetentionDays = normalized;
313
313
  try { localStorage.setItem('codexmateSessionTrashRetentionDays', String(normalized)); } catch (_) {}
314
+ if (typeof this.persistWebUiPreferences === 'function') {
315
+ this.persistWebUiPreferences({ sessionTrashRetentionDays: normalized });
316
+ }
314
317
  },
315
318
 
316
319
  getSessionTrashActionKey(item) {
@@ -260,17 +260,23 @@ export function createStartupClaudeMethods(options = {}) {
260
260
  mergeClaudeConfig(existing = {}, updates = {}) {
261
261
  const previous = this.normalizeClaudeConfig(existing);
262
262
  const next = this.normalizeClaudeConfig({ ...existing, ...updates });
263
+ const raw = { ...existing, ...updates };
264
+ const providerCacheRef = typeof raw.providerCacheRef === 'string' ? raw.providerCacheRef.trim() : '';
265
+ const source = raw.source === 'provider-cache' ? 'provider-cache' : (existing.source === 'provider-cache' && providerCacheRef ? 'provider-cache' : '');
263
266
  const externalCredentialType = next.apiKey
264
267
  ? ''
265
268
  : (next.externalCredentialType || previous.externalCredentialType || '');
266
- return {
269
+ const merged = {
267
270
  apiKey: next.apiKey,
268
271
  baseUrl: next.baseUrl,
269
272
  model: next.model || previous.model || 'glm-4.7',
270
- hasKey: !!(next.apiKey || externalCredentialType),
273
+ hasKey: !!(next.apiKey || externalCredentialType || providerCacheRef || raw.hasKey === true),
271
274
  externalCredentialType,
272
275
  targetApi: next.targetApi || previous.targetApi || 'responses'
273
276
  };
277
+ if (providerCacheRef) merged.providerCacheRef = providerCacheRef;
278
+ if (source) merged.source = source;
279
+ return merged;
274
280
  },
275
281
 
276
282
  buildClaudeImportedConfigName(baseUrl) {
@@ -459,6 +465,9 @@ export function createStartupClaudeMethods(options = {}) {
459
465
  const currentConfigName = typeof this.currentClaudeConfig === 'string' ? this.currentClaudeConfig.trim() : '';
460
466
  const baseUrl = (config.baseUrl || '').trim();
461
467
  const apiKey = (config.apiKey || '').trim();
468
+ const providerCacheRef = typeof config.providerCacheRef === 'string'
469
+ ? config.providerCacheRef.trim()
470
+ : '';
462
471
  const externalCredentialType = typeof config.externalCredentialType === 'string'
463
472
  ? config.externalCredentialType.trim()
464
473
  : '';
@@ -468,7 +477,7 @@ export function createStartupClaudeMethods(options = {}) {
468
477
  return;
469
478
  }
470
479
  const localCatalog = getClaudeModelCatalogForBaseUrl(baseUrl);
471
- if (!apiKey && externalCredentialType) {
480
+ if (!apiKey && (externalCredentialType || providerCacheRef)) {
472
481
  this.claudeModels = localCatalog;
473
482
  this.claudeModelsSource = localCatalog.length ? 'catalog' : 'unlimited';
474
483
  if (localCatalog.length) {
@@ -520,6 +529,7 @@ export function createStartupClaudeMethods(options = {}) {
520
529
  }
521
530
  return (latestConfig.baseUrl || '').trim() === baseUrl
522
531
  && (latestConfig.apiKey || '').trim() === apiKey
532
+ && (typeof latestConfig.providerCacheRef === 'string' ? latestConfig.providerCacheRef.trim() : '') === providerCacheRef
523
533
  && (typeof latestConfig.externalCredentialType === 'string' ? latestConfig.externalCredentialType.trim() : '') === externalCredentialType;
524
534
  };
525
535
  if (cachedOk) {