codexmate 0.0.54 → 0.0.56

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 (42) hide show
  1. package/README.md +3 -0
  2. package/README.vi.md +172 -0
  3. package/README.zh.md +2 -0
  4. package/cli/local-bridge.js +221 -22
  5. package/cli.js +666 -6
  6. package/package.json +1 -1
  7. package/web-ui/app.js +40 -0
  8. package/web-ui/logic.claude.mjs +8 -0
  9. package/web-ui/modules/app.computed.session.mjs +210 -0
  10. package/web-ui/modules/app.methods.claude-config.mjs +196 -13
  11. package/web-ui/modules/app.methods.codex-config.mjs +294 -65
  12. package/web-ui/modules/app.methods.index.mjs +4 -0
  13. package/web-ui/modules/app.methods.navigation.mjs +7 -1
  14. package/web-ui/modules/app.methods.provider-cache.mjs +223 -0
  15. package/web-ui/modules/app.methods.providers.mjs +15 -1
  16. package/web-ui/modules/app.methods.runtime.mjs +7 -2
  17. package/web-ui/modules/app.methods.session-actions.mjs +36 -0
  18. package/web-ui/modules/app.methods.session-browser.mjs +3 -0
  19. package/web-ui/modules/app.methods.session-trash.mjs +3 -0
  20. package/web-ui/modules/app.methods.startup-claude.mjs +47 -4
  21. package/web-ui/modules/app.methods.web-ui-preferences.mjs +161 -0
  22. package/web-ui/modules/i18n/locales/en.mjs +106 -3
  23. package/web-ui/modules/i18n/locales/ja.mjs +103 -3
  24. package/web-ui/modules/i18n/locales/vi.mjs +950 -0
  25. package/web-ui/modules/i18n/locales/zh-tw.mjs +103 -3
  26. package/web-ui/modules/i18n/locales/zh.mjs +103 -3
  27. package/web-ui/modules/provider-default-names.mjs +25 -0
  28. package/web-ui/partials/index/layout-header.html +18 -5
  29. package/web-ui/partials/index/modal-health-check.html +69 -5
  30. package/web-ui/partials/index/modals-basic.html +182 -0
  31. package/web-ui/partials/index/panel-config-codex.html +2 -2
  32. package/web-ui/partials/index/panel-sessions.html +99 -19
  33. package/web-ui/partials/index/panel-settings.html +20 -0
  34. package/web-ui/res/web-ui-render.precompiled.js +742 -113
  35. package/web-ui/session-helpers.mjs +4 -1
  36. package/web-ui/styles/layout-shell.css +51 -1
  37. package/web-ui/styles/responsive.css +98 -0
  38. package/web-ui/styles/sessions-preview.css +212 -2
  39. package/web-ui/styles/sessions-toolbar-trash.css +61 -18
  40. package/web-ui/styles/settings-panel.css +398 -0
  41. package/web-ui/styles/skills-list.css +122 -0
  42. package/web-ui/styles/titles-cards.css +52 -0
@@ -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
+ }
@@ -1,3 +1,5 @@
1
+ import { nextCodexProviderName } from './provider-default-names.mjs';
2
+
1
3
  const PROVIDER_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
2
4
  const RESERVED_PROXY_PROVIDER_NAME = 'codexmate-proxy';
3
5
  const RESERVED_LOCAL_PROVIDER_NAME = 'local';
@@ -327,6 +329,18 @@ export function createProvidersMethods(options = {}) {
327
329
  this.showAddModal = true;
328
330
  },
329
331
 
332
+ openAddProviderModal() {
333
+ this.newProvider = {
334
+ name: nextCodexProviderName(this.providersList),
335
+ url: '',
336
+ key: '',
337
+ model: '',
338
+ useTransform: false
339
+ };
340
+ this.showAddProviderKey = false;
341
+ this.showAddModal = true;
342
+ },
343
+
330
344
  async openEditModal(provider) {
331
345
  const requestId = Symbol('openEditModal');
332
346
  this._openEditModalRequestId = requestId;
@@ -536,7 +550,7 @@ export function createProvidersMethods(options = {}) {
536
550
  closeAddModal() {
537
551
  this.showAddModal = false;
538
552
  this.showAddProviderKey = false;
539
- this.newProvider = { name: '', url: '', key: '', model: '', useTransform: false };
553
+ this.newProvider = { name: nextCodexProviderName(this.providersList), url: '', key: '', model: '', useTransform: false };
540
554
  },
541
555
 
542
556
  toggleAddProviderKey() {
@@ -154,6 +154,10 @@ export function createRuntimeMethods(options = {}) {
154
154
  const baseUrl = config && typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
155
155
  const apiKey = config && typeof config.apiKey === 'string' ? config.apiKey.trim() : '';
156
156
  const model = config && typeof config.model === 'string' ? config.model.trim() : '';
157
+ const targetApiRaw = config && typeof config.targetApi === 'string' ? config.targetApi.trim().toLowerCase() : '';
158
+ const targetApi = targetApiRaw === 'ollama'
159
+ ? 'ollama'
160
+ : (targetApiRaw === 'chat_completions' || targetApiRaw === 'chat-completions' || targetApiRaw === 'chat/completions' ? 'chat_completions' : 'responses');
157
161
  this.claudeSpeedLoading[name] = true;
158
162
  try {
159
163
  if (!baseUrl) {
@@ -161,7 +165,7 @@ export function createRuntimeMethods(options = {}) {
161
165
  this.claudeSpeedResults[name] = res;
162
166
  return res;
163
167
  }
164
- if (!apiKey) {
168
+ if (!apiKey && targetApi !== 'ollama') {
165
169
  const res = { ok: false, error: 'Missing API key' };
166
170
  this.claudeSpeedResults[name] = res;
167
171
  return res;
@@ -175,7 +179,8 @@ export function createRuntimeMethods(options = {}) {
175
179
  kind: 'claude',
176
180
  url: baseUrl,
177
181
  apiKey,
178
- model
182
+ model,
183
+ targetApi
179
184
  });
180
185
  if (res.error) {
181
186
  this.claudeSpeedResults[name] = { ok: false, error: res.error };
@@ -165,6 +165,30 @@ export function createSessionActionMethods(options = {}) {
165
165
  this.showMessage(this.t('toast.copy.fail'), 'error');
166
166
  },
167
167
 
168
+ async copySessionWorkspaceBrief() {
169
+ const summary = this.activeSessionWorkspaceSummary;
170
+ const text = summary && typeof summary.briefText === 'string'
171
+ ? summary.briefText.trim()
172
+ : '';
173
+ if (!text) {
174
+ this.showMessage(this.t('sessions.workspace.copy.empty'), 'error');
175
+ return;
176
+ }
177
+ const ok = this.fallbackCopyText(text);
178
+ if (ok) {
179
+ this.showMessage(this.t('sessions.workspace.copy.success'), 'success');
180
+ return;
181
+ }
182
+ try {
183
+ if (navigator.clipboard && window.isSecureContext) {
184
+ await navigator.clipboard.writeText(text);
185
+ this.showMessage(this.t('sessions.workspace.copy.success'), 'success');
186
+ return;
187
+ }
188
+ } catch (_) {}
189
+ this.showMessage(this.t('toast.copy.fail'), 'error');
190
+ },
191
+
168
192
  getSessionExportKey(session) {
169
193
  return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
170
194
  },
@@ -281,6 +305,9 @@ export function createSessionActionMethods(options = {}) {
281
305
  try {
282
306
  localStorage.setItem('codexmateSessionTrashEnabled', enabled ? 'true' : 'false');
283
307
  } catch (_) {}
308
+ if (typeof this.persistWebUiPreferences === 'function') {
309
+ this.persistWebUiPreferences({ sessionTrashEnabled: enabled });
310
+ }
284
311
  },
285
312
 
286
313
  setSessionTimelineStyle(style) {
@@ -289,12 +316,18 @@ export function createSessionActionMethods(options = {}) {
289
316
  try {
290
317
  localStorage.setItem('codexmateSessionTimelineStyle', normalized);
291
318
  } catch (_) {}
319
+ if (typeof this.persistWebUiPreferences === 'function') {
320
+ this.persistWebUiPreferences({ sessionTimelineStyle: normalized });
321
+ }
292
322
  },
293
323
 
294
324
  setConfigTemplateDiffConfirmEnabled(value) {
295
325
  const enabled = this.normalizeConfigTemplateDiffConfirmEnabled(value);
296
326
  this.configTemplateDiffConfirmEnabled = enabled;
297
327
  persistConfigTemplateDiffConfirmEnabledToStorage(enabled);
328
+ if (typeof this.persistWebUiPreferences === 'function') {
329
+ this.persistWebUiPreferences({ configTemplateDiffConfirmEnabled: enabled });
330
+ }
298
331
  },
299
332
 
300
333
  getShareCommandPrefixInvocation() {
@@ -308,6 +341,9 @@ export function createSessionActionMethods(options = {}) {
308
341
  try {
309
342
  localStorage.setItem('codexmateShareCommandPrefix', normalized);
310
343
  } catch (_) {}
344
+ if (typeof this.persistWebUiPreferences === 'function') {
345
+ this.persistWebUiPreferences({ shareCommandPrefix: normalized });
346
+ }
311
347
  },
312
348
 
313
349
  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) {
@@ -8,6 +8,31 @@ import {
8
8
  normalizeClaudeSettingsEnv,
9
9
  normalizeClaudeValue
10
10
  } from '../logic.mjs';
11
+ import { nextClaudeConfigName } from './provider-default-names.mjs';
12
+
13
+ const DELETED_CLAUDE_SETTINGS_IMPORTS_STORAGE_KEY = 'deletedClaudeSettingsImports';
14
+
15
+ function normalizeDeletedClaudeImportUrl(value) {
16
+ return typeof value === 'string' ? value.trim().replace(/\/+$/g, '') : '';
17
+ }
18
+
19
+ function shouldSuppressDeletedClaudeSettingsImport(env = {}) {
20
+ const normalized = normalizeClaudeSettingsEnv(env);
21
+ const baseUrl = normalizeDeletedClaudeImportUrl(normalized.baseUrl);
22
+ const model = normalizeClaudeValue(normalized.model);
23
+ if (!baseUrl || !model) return false;
24
+ try {
25
+ const raw = localStorage.getItem(DELETED_CLAUDE_SETTINGS_IMPORTS_STORAGE_KEY);
26
+ const parsed = raw ? JSON.parse(raw) : [];
27
+ const entries = Array.isArray(parsed) ? parsed : [];
28
+ return entries.some((entry) => entry
29
+ && typeof entry === 'object'
30
+ && normalizeDeletedClaudeImportUrl(entry.baseUrl) === baseUrl
31
+ && normalizeClaudeValue(entry.model) === model);
32
+ } catch (_) {
33
+ return false;
34
+ }
35
+ }
11
36
 
12
37
  export function createStartupClaudeMethods(options = {}) {
13
38
  const {
@@ -250,7 +275,7 @@ export function createStartupClaudeMethods(options = {}) {
250
275
  },
251
276
 
252
277
  shouldSuppressClaudeSettingsImport(env) {
253
- return isLikelyBuiltinClaudeProxySettingsEnv(env);
278
+ return isLikelyBuiltinClaudeProxySettingsEnv(env) || shouldSuppressDeletedClaudeSettingsImport(env);
254
279
  },
255
280
 
256
281
  findDuplicateClaudeConfigName(config) {
@@ -260,17 +285,23 @@ export function createStartupClaudeMethods(options = {}) {
260
285
  mergeClaudeConfig(existing = {}, updates = {}) {
261
286
  const previous = this.normalizeClaudeConfig(existing);
262
287
  const next = this.normalizeClaudeConfig({ ...existing, ...updates });
288
+ const raw = { ...existing, ...updates };
289
+ const providerCacheRef = typeof raw.providerCacheRef === 'string' ? raw.providerCacheRef.trim() : '';
290
+ const source = raw.source === 'provider-cache' ? 'provider-cache' : (existing.source === 'provider-cache' && providerCacheRef ? 'provider-cache' : '');
263
291
  const externalCredentialType = next.apiKey
264
292
  ? ''
265
293
  : (next.externalCredentialType || previous.externalCredentialType || '');
266
- return {
294
+ const merged = {
267
295
  apiKey: next.apiKey,
268
296
  baseUrl: next.baseUrl,
269
297
  model: next.model || previous.model || 'glm-4.7',
270
- hasKey: !!(next.apiKey || externalCredentialType),
298
+ hasKey: !!(next.apiKey || externalCredentialType || providerCacheRef || raw.hasKey === true),
271
299
  externalCredentialType,
272
300
  targetApi: next.targetApi || previous.targetApi || 'responses'
273
301
  };
302
+ if (providerCacheRef) merged.providerCacheRef = providerCacheRef;
303
+ if (source) merged.source = source;
304
+ return merged;
274
305
  },
275
306
 
276
307
  buildClaudeImportedConfigName(baseUrl) {
@@ -459,6 +490,9 @@ export function createStartupClaudeMethods(options = {}) {
459
490
  const currentConfigName = typeof this.currentClaudeConfig === 'string' ? this.currentClaudeConfig.trim() : '';
460
491
  const baseUrl = (config.baseUrl || '').trim();
461
492
  const apiKey = (config.apiKey || '').trim();
493
+ const providerCacheRef = typeof config.providerCacheRef === 'string'
494
+ ? config.providerCacheRef.trim()
495
+ : '';
462
496
  const externalCredentialType = typeof config.externalCredentialType === 'string'
463
497
  ? config.externalCredentialType.trim()
464
498
  : '';
@@ -468,7 +502,7 @@ export function createStartupClaudeMethods(options = {}) {
468
502
  return;
469
503
  }
470
504
  const localCatalog = getClaudeModelCatalogForBaseUrl(baseUrl);
471
- if (!apiKey && externalCredentialType) {
505
+ if (!apiKey && (externalCredentialType || providerCacheRef)) {
472
506
  this.claudeModels = localCatalog;
473
507
  this.claudeModelsSource = localCatalog.length ? 'catalog' : 'unlimited';
474
508
  if (localCatalog.length) {
@@ -520,6 +554,7 @@ export function createStartupClaudeMethods(options = {}) {
520
554
  }
521
555
  return (latestConfig.baseUrl || '').trim() === baseUrl
522
556
  && (latestConfig.apiKey || '').trim() === apiKey
557
+ && (typeof latestConfig.providerCacheRef === 'string' ? latestConfig.providerCacheRef.trim() : '') === providerCacheRef
523
558
  && (typeof latestConfig.externalCredentialType === 'string' ? latestConfig.externalCredentialType.trim() : '') === externalCredentialType;
524
559
  };
525
560
  if (cachedOk) {
@@ -563,6 +598,14 @@ export function createStartupClaudeMethods(options = {}) {
563
598
  },
564
599
 
565
600
  openClaudeConfigModal() {
601
+ this.newClaudeConfig = {
602
+ name: nextClaudeConfigName(this.claudeConfigs),
603
+ apiKey: '',
604
+ externalCredentialType: '',
605
+ baseUrl: '',
606
+ model: '',
607
+ targetApi: 'responses'
608
+ };
566
609
  this.showAddClaudeConfigKey = false;
567
610
  this.showClaudeConfigModal = true;
568
611
  },
@@ -0,0 +1,161 @@
1
+ export function createWebUiPreferencesMethods(options = {}) {
2
+ const api = typeof options.api === 'function' ? options.api : async () => ({});
3
+ const storageOverride = options.storage && typeof options.storage === 'object' ? options.storage : null;
4
+
5
+ const getLocalStorage = () => {
6
+ const storage = storageOverride || (typeof globalThis !== 'undefined' ? globalThis.localStorage : null);
7
+ return storage && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function'
8
+ ? storage
9
+ : null;
10
+ };
11
+
12
+ const setLocalStorageValue = (key, value) => {
13
+ const storage = getLocalStorage();
14
+ if (!storage) return;
15
+ try {
16
+ if (value === null || value === undefined || value === '') {
17
+ storage.removeItem(key);
18
+ } else {
19
+ storage.setItem(key, String(value));
20
+ }
21
+ } catch (_) {}
22
+ };
23
+
24
+ const normalizeUsageTimeRange = (value) => {
25
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
26
+ if (normalized === 'all' || normalized === '30d') return normalized;
27
+ return '7d';
28
+ };
29
+
30
+ const normalizePromptsSubTab = (value) => {
31
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
32
+ return normalized === 'claude-project' ? 'claude-project' : 'codex';
33
+ };
34
+
35
+ const normalizeNavigationSnapshot = (vm, source = {}) => {
36
+ const currentSkillsTargetApp = vm.skillsTargetApp === 'claude' ? 'claude' : 'codex';
37
+ const currentPromptTemplatesMode = vm.promptTemplatesMode === 'manage' ? 'manage' : 'compose';
38
+ return {
39
+ mainTab: typeof source.mainTab === 'string' ? source.mainTab : vm.mainTab,
40
+ configMode: typeof source.configMode === 'string' ? source.configMode : vm.configMode,
41
+ settingsTab: typeof source.settingsTab === 'string' ? source.settingsTab : vm.settingsTab,
42
+ skillsTargetApp: source.skillsTargetApp === 'claude' || source.skillsTargetApp === 'codex'
43
+ ? source.skillsTargetApp
44
+ : currentSkillsTargetApp,
45
+ promptTemplatesMode: source.promptTemplatesMode === 'manage' || source.promptTemplatesMode === 'compose'
46
+ ? source.promptTemplatesMode
47
+ : currentPromptTemplatesMode
48
+ };
49
+ };
50
+
51
+ return {
52
+ buildWebUiPreferencesSnapshot(overrides = {}) {
53
+ const navigationOverride = overrides && typeof overrides.navigation === 'object' && overrides.navigation
54
+ ? overrides.navigation
55
+ : null;
56
+ return {
57
+ shareCommandPrefix: typeof this.normalizeShareCommandPrefix === 'function'
58
+ ? this.normalizeShareCommandPrefix(overrides.shareCommandPrefix || this.shareCommandPrefix)
59
+ : (this.shareCommandPrefix || 'npm start'),
60
+ sessionTrashEnabled: typeof this.normalizeSessionTrashEnabled === 'function'
61
+ ? this.normalizeSessionTrashEnabled(Object.prototype.hasOwnProperty.call(overrides, 'sessionTrashEnabled') ? overrides.sessionTrashEnabled : this.sessionTrashEnabled)
62
+ : this.sessionTrashEnabled !== false,
63
+ sessionTrashRetentionDays: typeof this.normalizeSessionTrashRetentionDays === 'function'
64
+ ? this.normalizeSessionTrashRetentionDays(Object.prototype.hasOwnProperty.call(overrides, 'sessionTrashRetentionDays') ? overrides.sessionTrashRetentionDays : this.sessionTrashRetentionDays)
65
+ : 30,
66
+ sessionTimelineStyle: typeof this.normalizeSessionTimelineStyle === 'function'
67
+ ? this.normalizeSessionTimelineStyle(overrides.sessionTimelineStyle || this.sessionTimelineStyle)
68
+ : (this.sessionTimelineStyle === 'bar' ? 'bar' : 'dots'),
69
+ configTemplateDiffConfirmEnabled: typeof this.normalizeConfigTemplateDiffConfirmEnabled === 'function'
70
+ ? this.normalizeConfigTemplateDiffConfirmEnabled(Object.prototype.hasOwnProperty.call(overrides, 'configTemplateDiffConfirmEnabled') ? overrides.configTemplateDiffConfirmEnabled : this.configTemplateDiffConfirmEnabled)
71
+ : this.configTemplateDiffConfirmEnabled !== false,
72
+ sessionsUsageTimeRange: normalizeUsageTimeRange(overrides.sessionsUsageTimeRange || this.sessionsUsageTimeRange),
73
+ promptsSubTab: normalizePromptsSubTab(overrides.promptsSubTab || this.promptsSubTab),
74
+ projectClaudeMdPath: typeof overrides.projectClaudeMdPath === 'string'
75
+ ? overrides.projectClaudeMdPath
76
+ : (typeof this.projectClaudeMdPath === 'string' ? this.projectClaudeMdPath : ''),
77
+ navigation: normalizeNavigationSnapshot(this, navigationOverride || {})
78
+ };
79
+ },
80
+
81
+ applyWebUiPreferences(preferences = {}, options = {}) {
82
+ const source = preferences && typeof preferences === 'object' ? preferences : {};
83
+ const shouldApplyNavigation = !(options && options.applyNavigation === false);
84
+ this.__webUiPreferencesApplying = true;
85
+ try {
86
+ if (typeof source.shareCommandPrefix === 'string' && typeof this.normalizeShareCommandPrefix === 'function') {
87
+ this.shareCommandPrefix = this.normalizeShareCommandPrefix(source.shareCommandPrefix);
88
+ setLocalStorageValue('codexmateShareCommandPrefix', this.shareCommandPrefix);
89
+ }
90
+ if (Object.prototype.hasOwnProperty.call(source, 'sessionTrashEnabled') && typeof this.normalizeSessionTrashEnabled === 'function') {
91
+ this.sessionTrashEnabled = this.normalizeSessionTrashEnabled(source.sessionTrashEnabled);
92
+ setLocalStorageValue('codexmateSessionTrashEnabled', this.sessionTrashEnabled ? 'true' : 'false');
93
+ }
94
+ if (Object.prototype.hasOwnProperty.call(source, 'sessionTrashRetentionDays') && typeof this.normalizeSessionTrashRetentionDays === 'function') {
95
+ this.sessionTrashRetentionDays = this.normalizeSessionTrashRetentionDays(source.sessionTrashRetentionDays);
96
+ setLocalStorageValue('codexmateSessionTrashRetentionDays', this.sessionTrashRetentionDays);
97
+ }
98
+ if (typeof source.sessionTimelineStyle === 'string' && typeof this.normalizeSessionTimelineStyle === 'function') {
99
+ this.sessionTimelineStyle = this.normalizeSessionTimelineStyle(source.sessionTimelineStyle);
100
+ setLocalStorageValue('codexmateSessionTimelineStyle', this.sessionTimelineStyle);
101
+ }
102
+ if (Object.prototype.hasOwnProperty.call(source, 'configTemplateDiffConfirmEnabled') && typeof this.normalizeConfigTemplateDiffConfirmEnabled === 'function') {
103
+ this.configTemplateDiffConfirmEnabled = this.normalizeConfigTemplateDiffConfirmEnabled(source.configTemplateDiffConfirmEnabled);
104
+ setLocalStorageValue('codexmateConfigTemplateDiffConfirmEnabled', this.configTemplateDiffConfirmEnabled ? 'true' : 'false');
105
+ }
106
+ if (typeof source.sessionsUsageTimeRange === 'string') {
107
+ this.sessionsUsageTimeRange = normalizeUsageTimeRange(source.sessionsUsageTimeRange);
108
+ setLocalStorageValue('sessionsUsageTimeRange', this.sessionsUsageTimeRange);
109
+ }
110
+ if (typeof source.promptsSubTab === 'string') {
111
+ this.promptsSubTab = normalizePromptsSubTab(source.promptsSubTab);
112
+ setLocalStorageValue('codexmate_prompts_sub_tab', this.promptsSubTab);
113
+ }
114
+ if (typeof source.projectClaudeMdPath === 'string') {
115
+ this.projectClaudeMdPath = source.projectClaudeMdPath;
116
+ setLocalStorageValue('codexmate_project_claude_md_path', this.projectClaudeMdPath);
117
+ }
118
+ if (shouldApplyNavigation && source.navigation && typeof source.navigation === 'object') {
119
+ const nav = source.navigation;
120
+ if (typeof nav.settingsTab === 'string' && typeof this.normalizeSettingsTab === 'function') {
121
+ this.settingsTab = this.normalizeSettingsTab(nav.settingsTab);
122
+ }
123
+ if (typeof nav.configMode === 'string') this.configMode = nav.configMode;
124
+ if (typeof nav.mainTab === 'string') {
125
+ if (typeof this.switchMainTab === 'function') {
126
+ this.switchMainTab(nav.mainTab);
127
+ } else {
128
+ this.mainTab = nav.mainTab;
129
+ }
130
+ }
131
+ if (nav.skillsTargetApp === 'codex' || nav.skillsTargetApp === 'claude') this.skillsTargetApp = nav.skillsTargetApp;
132
+ if (nav.promptTemplatesMode === 'compose' || nav.promptTemplatesMode === 'manage') this.promptTemplatesMode = nav.promptTemplatesMode;
133
+ if (typeof this.saveNavState === 'function') this.saveNavState();
134
+ }
135
+ } finally {
136
+ this.__webUiPreferencesApplying = false;
137
+ }
138
+ },
139
+
140
+ async loadWebUiPreferences(options = {}) {
141
+ try {
142
+ const res = await api('get-web-ui-preferences');
143
+ if (res && res.preferences && typeof res.preferences === 'object') {
144
+ this.applyWebUiPreferences(res.preferences, options && typeof options === 'object' ? options : {});
145
+ }
146
+ } catch (_) {}
147
+ },
148
+
149
+ persistWebUiPreferences(overrides = {}) {
150
+ if (this.__webUiPreferencesApplying) return;
151
+ const snapshot = this.buildWebUiPreferencesSnapshot(overrides && typeof overrides === 'object' ? overrides : {});
152
+ if (this.__webUiPreferencesPersistTimer) {
153
+ clearTimeout(this.__webUiPreferencesPersistTimer);
154
+ }
155
+ this.__webUiPreferencesPersistTimer = setTimeout(() => {
156
+ this.__webUiPreferencesPersistTimer = 0;
157
+ api('set-web-ui-preferences', { preferences: snapshot }).catch(() => {});
158
+ }, 120);
159
+ }
160
+ };
161
+ }