codexmate 0.0.56 → 0.1.1

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 +2 -2
  2. package/README.vi.md +1 -0
  3. package/README.zh.md +2 -2
  4. package/cli/openai-bridge-retry.js +62 -0
  5. package/cli/openai-bridge-runtime.js +1819 -0
  6. package/cli/openai-bridge.js +137 -2048
  7. package/cli.js +749 -133
  8. package/lib/task-orchestrator.js +90 -21
  9. package/lib/task-workspace-chat.js +292 -0
  10. package/package.json +2 -2
  11. package/web-ui/app.js +67 -131
  12. package/web-ui/modules/app.computed.main-tabs.mjs +304 -7
  13. package/web-ui/modules/app.methods.agents.mjs +192 -1
  14. package/web-ui/modules/app.methods.claude-config.mjs +65 -25
  15. package/web-ui/modules/app.methods.navigation.mjs +67 -83
  16. package/web-ui/modules/app.methods.openclaw-editing.mjs +3 -6
  17. package/web-ui/modules/app.methods.providers.mjs +40 -10
  18. package/web-ui/modules/app.methods.session-actions.mjs +1 -12
  19. package/web-ui/modules/app.methods.session-browser.mjs +23 -54
  20. package/web-ui/modules/app.methods.session-trash.mjs +0 -1
  21. package/web-ui/modules/app.methods.startup-claude.mjs +16 -31
  22. package/web-ui/modules/app.methods.task-orchestration.mjs +347 -8
  23. package/web-ui/modules/app.methods.tool-config-permissions.mjs +0 -3
  24. package/web-ui/modules/app.methods.web-ui-preferences.mjs +442 -68
  25. package/web-ui/modules/config-template-confirm-pref.mjs +2 -3
  26. package/web-ui/modules/i18n/locales/en.mjs +181 -30
  27. package/web-ui/modules/i18n/locales/ja.mjs +178 -27
  28. package/web-ui/modules/i18n/locales/vi.mjs +180 -29
  29. package/web-ui/modules/i18n/locales/zh-tw.mjs +181 -30
  30. package/web-ui/modules/i18n/locales/zh.mjs +183 -32
  31. package/web-ui/modules/i18n.mjs +5 -12
  32. package/web-ui/modules/sessions-filters-url.mjs +1 -2
  33. package/web-ui/partials/index/layout-header.html +44 -38
  34. package/web-ui/partials/index/modals-basic.html +26 -0
  35. package/web-ui/partials/index/panel-orchestration.html +489 -282
  36. package/web-ui/partials/index/panel-prompts.html +66 -3
  37. package/web-ui/res/web-ui-render.precompiled.js +1181 -604
  38. package/web-ui/styles/layout-shell.css +157 -1
  39. package/web-ui/styles/modals-core.css +173 -0
  40. package/web-ui/styles/navigation-panels.css +11 -0
  41. package/web-ui/styles/responsive.css +32 -0
  42. package/web-ui/styles/task-orchestration.css +2161 -4
@@ -8,8 +8,6 @@ function normalizeClaudeBaseUrl(value) {
8
8
  return normalizeClaudeText(value).replace(/\/+$/g, '');
9
9
  }
10
10
 
11
- const DELETED_CLAUDE_SETTINGS_IMPORTS_KEY = 'deletedClaudeSettingsImports';
12
-
13
11
  function buildDeletedClaudeSettingsFingerprint(config = {}) {
14
12
  const safe = config && typeof config === 'object' ? config : {};
15
13
  const baseUrl = normalizeClaudeBaseUrl(safe.baseUrl);
@@ -92,10 +90,41 @@ export function createClaudeConfigMethods(options = {}) {
92
90
  return {
93
91
  switchClaudeConfig(name) {
94
92
  this.currentClaudeConfig = name;
95
- try { localStorage.setItem('currentClaudeConfig', name || ''); } catch (_) {}
93
+ if (typeof this.persistWebUiPreferences === 'function') {
94
+ this.persistWebUiPreferences({ currentClaudeConfig: name || '' });
95
+ }
96
96
  this.refreshClaudeModelContext();
97
97
  },
98
98
 
99
+ normalizeStoredClaudeConfigs() {
100
+ const configs = this.claudeConfigs && typeof this.claudeConfigs === 'object' && !Array.isArray(this.claudeConfigs)
101
+ ? this.claudeConfigs
102
+ : {};
103
+ let changed = configs !== this.claudeConfigs;
104
+ for (const config of Object.values(configs)) {
105
+ if (!config || typeof config !== 'object') continue;
106
+ if (config.apiKey && config.apiKey.includes('****')) {
107
+ config.apiKey = '';
108
+ config.hasKey = false;
109
+ changed = true;
110
+ }
111
+ const targetApiRaw = typeof config.targetApi === 'string' ? config.targetApi.trim().toLowerCase() : '';
112
+ const previousTargetApi = config.targetApi;
113
+ if (targetApiRaw === 'chat_completions' || targetApiRaw === 'chat-completions' || targetApiRaw === 'chat/completions') {
114
+ config.targetApi = 'chat_completions';
115
+ } else if (targetApiRaw === 'ollama') {
116
+ config.targetApi = 'ollama';
117
+ } else {
118
+ config.targetApi = 'responses';
119
+ }
120
+ if (config.targetApi !== previousTargetApi) {
121
+ changed = true;
122
+ }
123
+ }
124
+ this.claudeConfigs = configs;
125
+ return changed;
126
+ },
127
+
99
128
  onClaudeModelChange() {
100
129
  const name = this.currentClaudeConfig;
101
130
  if (!name || name === 'claude-local') {
@@ -124,9 +153,14 @@ export function createClaudeConfigMethods(options = {}) {
124
153
  },
125
154
 
126
155
  saveClaudeConfigs() {
127
- try { localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs)); } catch (_) {}
128
- if (this.currentClaudeConfig) {
129
- try { localStorage.setItem('currentClaudeConfig', this.currentClaudeConfig); } catch (_) {}
156
+ if (typeof this.normalizeStoredClaudeConfigs === 'function') {
157
+ this.normalizeStoredClaudeConfigs();
158
+ }
159
+ if (typeof this.persistWebUiPreferences === 'function') {
160
+ this.persistWebUiPreferences({
161
+ claudeConfigs: this.claudeConfigs,
162
+ currentClaudeConfig: this.currentClaudeConfig || ''
163
+ });
130
164
  }
131
165
  this.syncClaudeBridgeProviders();
132
166
  },
@@ -134,18 +168,17 @@ export function createClaudeConfigMethods(options = {}) {
134
168
  rememberDeletedClaudeSettingsImport(config) {
135
169
  const fingerprint = buildDeletedClaudeSettingsFingerprint(config);
136
170
  if (!fingerprint) return;
137
- try {
138
- const raw = localStorage.getItem(DELETED_CLAUDE_SETTINGS_IMPORTS_KEY);
139
- const parsed = raw ? JSON.parse(raw) : [];
140
- const entries = Array.isArray(parsed) ? parsed : [];
141
- const deduped = entries.filter((entry) => {
142
- if (!entry || typeof entry !== 'object') return false;
143
- return normalizeClaudeBaseUrl(entry.baseUrl) !== fingerprint.baseUrl
144
- || normalizeClaudeText(entry.model) !== fingerprint.model;
145
- });
146
- deduped.push(fingerprint);
147
- localStorage.setItem(DELETED_CLAUDE_SETTINGS_IMPORTS_KEY, JSON.stringify(deduped.slice(-50)));
148
- } catch (_) {}
171
+ const entries = Array.isArray(this.deletedClaudeSettingsImports) ? this.deletedClaudeSettingsImports : [];
172
+ const deduped = entries.filter((entry) => {
173
+ if (!entry || typeof entry !== 'object') return false;
174
+ return normalizeClaudeBaseUrl(entry.baseUrl) !== fingerprint.baseUrl
175
+ || normalizeClaudeText(entry.model) !== fingerprint.model;
176
+ });
177
+ deduped.push(fingerprint);
178
+ this.deletedClaudeSettingsImports = deduped.slice(-50);
179
+ if (typeof this.persistWebUiPreferences === 'function') {
180
+ this.persistWebUiPreferences({ deletedClaudeSettingsImports: this.deletedClaudeSettingsImports });
181
+ }
149
182
  },
150
183
 
151
184
  async applyCurrentClaudeConfigSilently() {
@@ -227,15 +260,18 @@ export function createClaudeConfigMethods(options = {}) {
227
260
  const current = normalizeClaudeText(this.currentClaudeConfig);
228
261
  if (current && !configs[current]) {
229
262
  this.currentClaudeConfig = firstCachedName || Object.keys(configs)[0] || '';
230
- try { localStorage.setItem('currentClaudeConfig', this.currentClaudeConfig); } catch (_) {}
231
263
  changed = true;
232
264
  } else if (firstCachedName) {
233
- let savedCurrent = '';
234
- try { savedCurrent = localStorage.getItem('currentClaudeConfig') || ''; } catch (_) {}
235
265
  const currentConfig = current && configs[current] ? configs[current] : null;
236
- if (!savedCurrent && (!current || (currentConfig && currentConfig.hasKey === false && !currentConfig.providerCacheRef))) {
266
+ const currentApplyable = currentConfig && (
267
+ currentConfig.hasKey === true
268
+ || !!normalizeClaudeText(currentConfig.apiKey)
269
+ || !!normalizeClaudeText(currentConfig.providerCacheRef)
270
+ || !!normalizeClaudeText(currentConfig.externalCredentialType)
271
+ || normalizeClaudeText(currentConfig.targetApi) === 'ollama'
272
+ );
273
+ if (!currentApplyable) {
237
274
  this.currentClaudeConfig = firstCachedName;
238
- try { localStorage.setItem('currentClaudeConfig', firstCachedName); } catch (_) {}
239
275
  changed = true;
240
276
  }
241
277
  }
@@ -458,7 +494,9 @@ export function createClaudeConfigMethods(options = {}) {
458
494
  const silentSuccess = silent || !!(options && options.silentSuccess);
459
495
  const silentError = silent || !!(options && options.silentError);
460
496
  this.currentClaudeConfig = name;
461
- try { localStorage.setItem('currentClaudeConfig', name || ''); } catch (_) {}
497
+ if (typeof this.persistWebUiPreferences === 'function') {
498
+ this.persistWebUiPreferences({ currentClaudeConfig: name || '' });
499
+ }
462
500
  this.refreshClaudeModelContext();
463
501
  const config = this.claudeConfigs[name];
464
502
 
@@ -572,7 +610,9 @@ export function createClaudeConfigMethods(options = {}) {
572
610
 
573
611
  async applyClaudeLocalBridge() {
574
612
  this.currentClaudeConfig = 'claude-local';
575
- try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {}
613
+ if (typeof this.persistWebUiPreferences === 'function') {
614
+ this.persistWebUiPreferences({ currentClaudeConfig: 'claude-local' });
615
+ }
576
616
  this.refreshClaudeModelContext();
577
617
 
578
618
  const candidates = this.claudeLocalBridgeCandidateProviders();
@@ -4,8 +4,7 @@
4
4
  switchMainTabHelper,
5
5
  loadMoreSessionMessagesHelper
6
6
  } = options;
7
- const NAV_STATE_STORAGE_KEY = 'codexmateNavState.v1';
8
- const MAIN_TAB_SET = new Set([
7
+ const MAIN_TAB_ORDER = [
9
8
  'dashboard',
10
9
  'config',
11
10
  'sessions',
@@ -17,7 +16,39 @@
17
16
  'settings',
18
17
  'trash',
19
18
  'prompts'
20
- ]);
19
+ ];
20
+ const MAIN_TAB_SET = new Set(MAIN_TAB_ORDER);
21
+ const DISABLED_MAIN_TAB_SET = new Set(['orchestration']);
22
+ const normalizeMainTab = (tab) => typeof tab === 'string'
23
+ ? tab.trim().toLowerCase()
24
+ : '';
25
+ const isMainTabSelectable = (tab) => {
26
+ const normalized = normalizeMainTab(tab);
27
+ return !!(normalized && MAIN_TAB_SET.has(normalized) && !DISABLED_MAIN_TAB_SET.has(normalized));
28
+ };
29
+ const getFirstSelectableMainTab = () => MAIN_TAB_ORDER.find((tab) => isMainTabSelectable(tab)) || 'dashboard';
30
+ const resolveSelectableMainTab = (tab) => {
31
+ const normalized = normalizeMainTab(tab);
32
+ if (isMainTabSelectable(normalized)) return normalized;
33
+ return getFirstSelectableMainTab();
34
+ };
35
+ const resolveSwitchMainTabTarget = (tab) => {
36
+ const normalized = normalizeMainTab(tab);
37
+ if (!normalized) return '';
38
+ if (isMainTabSelectable(normalized)) return normalized;
39
+ if (MAIN_TAB_SET.has(normalized) && DISABLED_MAIN_TAB_SET.has(normalized)) {
40
+ return getFirstSelectableMainTab();
41
+ }
42
+ return '';
43
+ };
44
+ const cancelDisabledMainTabEvent = (event) => {
45
+ if (event && typeof event.preventDefault === 'function') {
46
+ event.preventDefault();
47
+ }
48
+ if (event && typeof event.stopPropagation === 'function') {
49
+ event.stopPropagation();
50
+ }
51
+ };
21
52
  const loadDoctorOverview = async (vm, options = {}) => {
22
53
  if (!vm || typeof vm !== 'object') return false;
23
54
  if (vm.__doctorLoading) return false;
@@ -41,23 +72,6 @@
41
72
  }
42
73
  }
43
74
  };
44
- const readNavState = () => {
45
- if (typeof localStorage === 'undefined') return null;
46
- let raw = '';
47
- try {
48
- raw = localStorage.getItem(NAV_STATE_STORAGE_KEY) || '';
49
- } catch (_) {
50
- raw = '';
51
- }
52
- if (!raw) return null;
53
- try {
54
- const parsed = JSON.parse(raw);
55
- return parsed && typeof parsed === 'object' ? parsed : null;
56
- } catch (_) {
57
- return null;
58
- }
59
- };
60
-
61
75
  const canonicalizeWebUiRuntimeUrl = () => {
62
76
  if (typeof window === 'undefined' || !window.location) return;
63
77
  try {
@@ -85,7 +99,6 @@
85
99
 
86
100
  const persistNavState = (vm, overrides = null) => {
87
101
  if (!vm || vm.__navStateRestoring) return;
88
- if (typeof localStorage === 'undefined') return;
89
102
  const resolvedOverrides = overrides && typeof overrides === 'object' ? overrides : null;
90
103
  const mainTabSource = resolvedOverrides && typeof resolvedOverrides.mainTab === 'string'
91
104
  ? resolvedOverrides.mainTab
@@ -93,76 +106,36 @@
93
106
  const configModeSource = resolvedOverrides && typeof resolvedOverrides.configMode === 'string'
94
107
  ? resolvedOverrides.configMode
95
108
  : vm.configMode;
96
- const mainTab = typeof mainTabSource === 'string' ? mainTabSource.trim().toLowerCase() : '';
109
+ const mainTab = normalizeMainTab(mainTabSource);
97
110
  const configMode = typeof configModeSource === 'string' ? configModeSource.trim().toLowerCase() : '';
98
111
  const settingsTab = typeof vm.settingsTab === 'string' ? vm.settingsTab.trim().toLowerCase() : 'general';
99
112
  const skillsTargetApp = typeof vm.skillsTargetApp === 'string' && (vm.skillsTargetApp === 'codex' || vm.skillsTargetApp === 'claude') ? vm.skillsTargetApp : 'codex';
100
113
  const promptTemplatesMode = typeof vm.promptTemplatesMode === 'string' && (vm.promptTemplatesMode === 'compose' || vm.promptTemplatesMode === 'manage') ? vm.promptTemplatesMode : 'compose';
101
114
  const snapshot = {
102
115
  settingsTab: settingsTab === 'data' ? 'data' : 'general',
103
- mainTab: MAIN_TAB_SET.has(mainTab) ? mainTab : 'dashboard',
116
+ mainTab: resolveSelectableMainTab(mainTab),
104
117
  configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex',
105
118
  skillsTargetApp,
106
119
  promptTemplatesMode
107
120
  };
108
- try {
109
- localStorage.setItem(NAV_STATE_STORAGE_KEY, JSON.stringify(snapshot));
110
- } catch (_) {}
111
121
  if (typeof vm.persistWebUiPreferences === 'function') {
112
122
  vm.persistWebUiPreferences({ navigation: snapshot });
113
123
  }
114
124
  };
115
125
 
116
126
  return {
127
+ toggleSidebarCollapsed() {
128
+ this.sidebarCollapsed = !this.sidebarCollapsed;
129
+ if (typeof this.persistWebUiPreferences === 'function') {
130
+ this.persistWebUiPreferences({ sidebarCollapsed: this.sidebarCollapsed });
131
+ }
132
+ },
133
+
117
134
  saveNavState() {
118
135
  persistNavState(this);
119
136
  },
120
137
  restoreNavStateFromStorage() {
121
- if (this.__navStateRestoring) return false;
122
- const restored = readNavState();
123
- if (!restored) return false;
124
- const nextMainTab = restored && typeof restored.mainTab === 'string'
125
- ? restored.mainTab.trim().toLowerCase()
126
- : '';
127
- const nextConfigMode = restored && typeof restored.configMode === 'string'
128
- ? restored.configMode.trim().toLowerCase()
129
- : '';
130
- const shouldUpdateConfigMode = !!(nextConfigMode && configModeSet && configModeSet.has(nextConfigMode));
131
- const shouldUpdateMainTab = !!(nextMainTab && MAIN_TAB_SET.has(nextMainTab) && nextMainTab !== this.mainTab);
132
- const nextSettingsTab = restored && typeof restored.settingsTab === 'string'
133
- ? restored.settingsTab.trim().toLowerCase()
134
- : '';
135
- const shouldUpdateSettingsTab = !!(nextSettingsTab && (nextSettingsTab === 'general' || nextSettingsTab === 'data') && nextSettingsTab !== this.settingsTab);
136
- const nextSkillsTargetApp = restored && typeof restored.skillsTargetApp === 'string' && (restored.skillsTargetApp === 'codex' || restored.skillsTargetApp === 'claude')
137
- ? restored.skillsTargetApp : '';
138
- const shouldUpdateSkillsTargetApp = !!(nextSkillsTargetApp && nextSkillsTargetApp !== this.skillsTargetApp);
139
- const nextPromptTemplatesMode = restored && typeof restored.promptTemplatesMode === 'string' && (restored.promptTemplatesMode === 'compose' || restored.promptTemplatesMode === 'manage')
140
- ? restored.promptTemplatesMode : '';
141
- const shouldUpdatePromptTemplatesMode = !!(nextPromptTemplatesMode && nextPromptTemplatesMode !== this.promptTemplatesMode);
142
- if (!shouldUpdateConfigMode && !shouldUpdateMainTab && !shouldUpdateSettingsTab && !shouldUpdateSkillsTargetApp && !shouldUpdatePromptTemplatesMode) {
143
- return false;
144
- }
145
- this.__navStateRestoring = true;
146
- try {
147
- if (shouldUpdateConfigMode) {
148
- this.configMode = nextConfigMode;
149
- }
150
- if (shouldUpdateSettingsTab) {
151
- this.settingsTab = nextSettingsTab;
152
- }
153
- if (shouldUpdateMainTab) {
154
- this.switchMainTab(nextMainTab);
155
- }
156
- if (shouldUpdateSkillsTargetApp) {
157
- this.skillsTargetApp = nextSkillsTargetApp;
158
- }
159
- if (shouldUpdatePromptTemplatesMode) {
160
- this.promptTemplatesMode = nextPromptTemplatesMode;
161
- }
162
- } finally {
163
- this.__navStateRestoring = false;
164
- }
165
- return true;
138
+ return false;
166
139
  },
167
140
  switchConfigMode(mode) {
168
141
  const normalizedMode = typeof mode === 'string'
@@ -353,6 +326,10 @@
353
326
  }
354
327
  const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
355
328
  if (!normalizedTab) return;
329
+ if (!isMainTabSelectable(normalizedTab)) {
330
+ cancelDisabledMainTabEvent(event);
331
+ return;
332
+ }
356
333
  persistNavState(this, { mainTab: normalizedTab });
357
334
  this.setMainTabSwitchIntent(normalizedTab);
358
335
  this.applyImmediateNavIntent(normalizedTab);
@@ -394,8 +371,13 @@
394
371
  this.switchConfigMode(normalizedMode);
395
372
  },
396
373
  onMainTabClick(tab) {
374
+ const event = arguments.length > 1 ? arguments[1] : null;
397
375
  const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
398
376
  if (!normalizedTab) return;
377
+ if (!isMainTabSelectable(normalizedTab)) {
378
+ cancelDisabledMainTabEvent(event);
379
+ return;
380
+ }
399
381
  if (this.consumePointerNavCommit('main', normalizedTab)) return;
400
382
  this.switchMainTab(normalizedTab);
401
383
  },
@@ -437,16 +419,20 @@
437
419
  }
438
420
  return this.configMode === mode;
439
421
  },
422
+ isMainTabDisabled(tab) {
423
+ const normalizedTab = normalizeMainTab(tab);
424
+ return !!(normalizedTab && MAIN_TAB_SET.has(normalizedTab) && DISABLED_MAIN_TAB_SET.has(normalizedTab));
425
+ },
426
+ getFirstSelectableMainTab() {
427
+ return getFirstSelectableMainTab();
428
+ },
440
429
  switchMainTab(tab) {
441
430
  const normalizedTab = typeof tab === 'string'
442
431
  ? tab.trim().toLowerCase()
443
432
  : '';
444
- const targetTab = normalizedTab || tab;
433
+ const targetTab = resolveSwitchMainTabTarget(normalizedTab || tab);
445
434
  if (!targetTab) return;
446
435
  canonicalizeWebUiRuntimeUrl();
447
- if (targetTab === 'orchestration' && this.taskOrchestrationTabEnabled !== true) {
448
- return this.switchMainTab('config');
449
- }
450
436
  persistNavState(this, {
451
437
  mainTab: targetTab,
452
438
  configMode: targetTab === 'config' ? this.configMode : this.configMode
@@ -473,13 +459,11 @@
473
459
  switchState.ticket += 1;
474
460
  switchState.pendingTarget = '';
475
461
  if (targetTab === 'dashboard' && !this.__doctorLoadedOnce) {
476
- if (targetTab === 'trash' && !this.sessionTrashLoadedOnce) {
477
- if (typeof this.loadSessionTrash === 'function') {
478
- void this.loadSessionTrash({ forceRefresh: false });
479
- }
480
- }
481
462
  void loadDoctorOverview(this);
482
463
  }
464
+ if (targetTab === 'trash' && !this.sessionTrashLoadedOnce && typeof this.loadSessionTrash === 'function') {
465
+ void this.loadSessionTrash({ forceRefresh: false });
466
+ }
483
467
  if (
484
468
  targetTab === 'sessions'
485
469
  && typeof this.prepareSessionTabRender === 'function'
@@ -488,7 +472,7 @@
488
472
  this.prepareSessionTabRender();
489
473
  }
490
474
  this.scheduleAfterFrame(() => {
491
- this.clearMainTabSwitchIntent(normalizedTab);
475
+ this.clearMainTabSwitchIntent(targetTab);
492
476
  });
493
477
  return;
494
478
  }
@@ -510,7 +494,7 @@
510
494
  void loadDoctorOverview(this);
511
495
  }
512
496
  this.scheduleAfterFrame(() => {
513
- this.clearMainTabSwitchIntent(normalizedTab);
497
+ this.clearMainTabSwitchIntent(targetTab);
514
498
  });
515
499
  return result;
516
500
  }
@@ -527,7 +511,7 @@
527
511
  if (pendingTarget === 'dashboard') {
528
512
  void loadDoctorOverview(this);
529
513
  }
530
- this.clearMainTabSwitchIntent(normalizedTab);
514
+ this.clearMainTabSwitchIntent(pendingTarget);
531
515
  });
532
516
  },
533
517
 
@@ -360,13 +360,10 @@ export function createOpenclawEditingMethods() {
360
360
  },
361
361
 
362
362
  saveOpenclawConfigs() {
363
- try {
364
- localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
365
- return true;
366
- } catch (_) {
367
- this.showMessage('保存本地 OpenClaw 配置失败', 'error');
368
- return false;
363
+ if (typeof this.persistWebUiPreferences === 'function') {
364
+ this.persistWebUiPreferences({ openclawConfigs: this.openclawConfigs });
369
365
  }
366
+ return true;
370
367
  },
371
368
 
372
369
  // Accordion stepper methods
@@ -41,6 +41,13 @@ function findProviderByName(list, name) {
41
41
  return (Array.isArray(list) ? list : []).find((item) => item && normalizeText(item.name) === target) || null;
42
42
  }
43
43
 
44
+ function normalizeBridgeMaxRetries(value, fallback = 2) {
45
+ const raw = Number(value);
46
+ const fallbackRaw = Number(fallback);
47
+ const base = Number.isFinite(raw) ? raw : (Number.isFinite(fallbackRaw) ? fallbackRaw : 2);
48
+ return Math.min(10, Math.max(2, Math.floor(base)));
49
+ }
50
+
44
51
  function normalizeProviderDraftState(target) {
45
52
  if (!target || typeof target !== 'object') return;
46
53
  if (typeof target.name === 'string') {
@@ -55,6 +62,9 @@ function normalizeProviderDraftState(target) {
55
62
  if (typeof target.key === 'string') {
56
63
  target.key = target.key.trim();
57
64
  }
65
+ if (target.useTransform || target.openaiBridgeMaxRetries !== undefined) {
66
+ target.openaiBridgeMaxRetries = normalizeBridgeMaxRetries(target.openaiBridgeMaxRetries);
67
+ }
58
68
  }
59
69
 
60
70
  function maskKeyLocal(key) {
@@ -76,11 +86,14 @@ function getProviderValidationForContext(vm, mode = 'add') {
76
86
  const url = normalizeProviderUrl(draft && draft.url);
77
87
  const model = normalizeText(draft && draft.model);
78
88
  const key = normalizeText(draft && draft.key);
89
+ const useTransform = !!(draft && draft.useTransform);
90
+ const openaiBridgeMaxRetries = normalizeBridgeMaxRetries(draft && draft.openaiBridgeMaxRetries);
79
91
  const errors = {
80
92
  name: '',
81
93
  url: '',
82
94
  key: '',
83
- model: ''
95
+ model: '',
96
+ openaiBridgeMaxRetries: ''
84
97
  };
85
98
 
86
99
  if (mode === 'add') {
@@ -111,14 +124,20 @@ function getProviderValidationForContext(vm, mode = 'add') {
111
124
  errors.model = '模型名称必填';
112
125
  }
113
126
 
127
+ if (useTransform && openaiBridgeMaxRetries < 2) {
128
+ errors.openaiBridgeMaxRetries = '重试次数最小为 2';
129
+ }
130
+
114
131
  return {
115
132
  mode,
116
133
  name,
117
134
  url,
118
135
  key,
119
136
  model,
137
+ useTransform,
138
+ openaiBridgeMaxRetries,
120
139
  errors,
121
- ok: !errors.name && !errors.url && !errors.key && !errors.model
140
+ ok: !errors.name && !errors.url && !errors.key && !errors.model && !errors.openaiBridgeMaxRetries
122
141
  };
123
142
  }
124
143
 
@@ -172,7 +191,7 @@ export function createProvidersMethods(options = {}) {
172
191
  normalizeProviderDraftState(this.newProvider);
173
192
  const validation = getProviderValidationForContext(this, 'add');
174
193
  if (!validation.ok) {
175
- return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.key || validation.errors.model || this.t('toast.provider.fieldsRequired'), 'error');
194
+ return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.key || validation.errors.model || validation.errors.openaiBridgeMaxRetries || this.t('toast.provider.fieldsRequired'), 'error');
176
195
  }
177
196
 
178
197
  try {
@@ -184,6 +203,7 @@ export function createProvidersMethods(options = {}) {
184
203
  };
185
204
  if (this.newProvider && this.newProvider.useTransform) {
186
205
  payload.useTransform = true;
206
+ payload.openaiBridgeMaxRetries = validation.openaiBridgeMaxRetries;
187
207
  }
188
208
  const suggestedModel = validation.model;
189
209
  const res = await api('add-provider', payload);
@@ -198,6 +218,7 @@ export function createProvidersMethods(options = {}) {
198
218
  url: validation.url,
199
219
  upstreamUrl: '',
200
220
  codexmate_bridge: payload.useTransform ? 'openai' : '',
221
+ openaiBridgeMaxRetries: payload.useTransform ? validation.openaiBridgeMaxRetries : undefined,
201
222
  key: maskKeyLocal(payload.key),
202
223
  hasKey: !!payload.key,
203
224
  models: suggestedModel ? [{ id: suggestedModel, name: suggestedModel, cost: null, contextWindow: undefined, maxTokens: undefined }] : [],
@@ -323,7 +344,8 @@ export function createProvidersMethods(options = {}) {
323
344
  url: cloneUrl,
324
345
  key: '',
325
346
  model: '',
326
- useTransform: isTransform
347
+ useTransform: isTransform,
348
+ openaiBridgeMaxRetries: normalizeBridgeMaxRetries(provider.openaiBridgeMaxRetries)
327
349
  };
328
350
  this.showAddProviderKey = false;
329
351
  this.showAddModal = true;
@@ -335,7 +357,8 @@ export function createProvidersMethods(options = {}) {
335
357
  url: '',
336
358
  key: '',
337
359
  model: '',
338
- useTransform: false
360
+ useTransform: false,
361
+ openaiBridgeMaxRetries: 2
339
362
  };
340
363
  this.showAddProviderKey = false;
341
364
  this.showAddModal = true;
@@ -363,7 +386,8 @@ export function createProvidersMethods(options = {}) {
363
386
  nonEditable: typeof provider.nonEditable === 'boolean'
364
387
  ? provider.nonEditable
365
388
  : this.isNonDeletableProvider(provider),
366
- useTransform: isTransformProvider
389
+ useTransform: isTransformProvider,
390
+ openaiBridgeMaxRetries: normalizeBridgeMaxRetries(provider.openaiBridgeMaxRetries)
367
391
  };
368
392
  this._editProviderOriginalKey = '';
369
393
  this._editProviderRealKeyLoaded = false;
@@ -404,6 +428,9 @@ export function createProvidersMethods(options = {}) {
404
428
  && res.baseUrl.trim()
405
429
  ) {
406
430
  this.editingProvider.url = normalizeProviderUrl(res.baseUrl);
431
+ if (res.maxRetries !== undefined) {
432
+ this.editingProvider.openaiBridgeMaxRetries = normalizeBridgeMaxRetries(res.maxRetries);
433
+ }
407
434
  }
408
435
  } catch (_) {
409
436
  // ignore
@@ -420,12 +447,13 @@ export function createProvidersMethods(options = {}) {
420
447
  normalizeProviderDraftState(this.editingProvider);
421
448
  const validation = getProviderValidationForContext(this, 'edit');
422
449
  if (!validation.ok) {
423
- return this.showMessage(validation.errors.name || validation.errors.url || this.t('toast.provider.urlRequired'), 'error');
450
+ return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.openaiBridgeMaxRetries || this.t('toast.provider.urlRequired'), 'error');
424
451
  }
425
452
 
426
453
  const params = { name: validation.name, url: validation.url };
427
454
  if (this.editingProvider && this.editingProvider.useTransform) {
428
455
  params.useTransform = true;
456
+ params.openaiBridgeMaxRetries = validation.openaiBridgeMaxRetries;
429
457
  }
430
458
  if (this._editProviderRealKeyLoaded) {
431
459
  const currentKey = typeof this.editingProvider.key === 'string' ? this.editingProvider.key : '';
@@ -449,7 +477,9 @@ export function createProvidersMethods(options = {}) {
449
477
  ...p,
450
478
  url: validation.url,
451
479
  key: keyUpdated ? maskKeyLocal(params.key) : p.key,
452
- hasKey: keyUpdated ? !!params.key : p.hasKey
480
+ hasKey: keyUpdated ? !!params.key : p.hasKey,
481
+ codexmate_bridge: params.useTransform ? 'openai' : p.codexmate_bridge,
482
+ openaiBridgeMaxRetries: params.useTransform ? validation.openaiBridgeMaxRetries : p.openaiBridgeMaxRetries
453
483
  };
454
484
  }
455
485
  return p;
@@ -467,7 +497,7 @@ export function createProvidersMethods(options = {}) {
467
497
  this.showEditProviderKey = false;
468
498
  this._editProviderOriginalKey = '';
469
499
  this._editProviderRealKeyLoaded = false;
470
- this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false, useTransform: false };
500
+ this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false, useTransform: false, openaiBridgeMaxRetries: 2 };
471
501
  },
472
502
 
473
503
  toggleEditProviderKey() {
@@ -550,7 +580,7 @@ export function createProvidersMethods(options = {}) {
550
580
  closeAddModal() {
551
581
  this.showAddModal = false;
552
582
  this.showAddProviderKey = false;
553
- this.newProvider = { name: nextCodexProviderName(this.providersList), url: '', key: '', model: '', useTransform: false };
583
+ this.newProvider = { name: nextCodexProviderName(this.providersList), url: '', key: '', model: '', useTransform: false, openaiBridgeMaxRetries: 2 };
554
584
  },
555
585
 
556
586
  toggleAddProviderKey() {
@@ -1,6 +1,5 @@
1
1
  import {
2
- normalizeConfigTemplateDiffConfirmEnabled,
3
- persistConfigTemplateDiffConfirmEnabledToStorage
2
+ normalizeConfigTemplateDiffConfirmEnabled
4
3
  } from './config-template-confirm-pref.mjs';
5
4
 
6
5
  export function createSessionActionMethods(options = {}) {
@@ -302,9 +301,6 @@ export function createSessionActionMethods(options = {}) {
302
301
  setSessionTrashEnabled(value) {
303
302
  const enabled = this.normalizeSessionTrashEnabled(value);
304
303
  this.sessionTrashEnabled = enabled;
305
- try {
306
- localStorage.setItem('codexmateSessionTrashEnabled', enabled ? 'true' : 'false');
307
- } catch (_) {}
308
304
  if (typeof this.persistWebUiPreferences === 'function') {
309
305
  this.persistWebUiPreferences({ sessionTrashEnabled: enabled });
310
306
  }
@@ -313,9 +309,6 @@ export function createSessionActionMethods(options = {}) {
313
309
  setSessionTimelineStyle(style) {
314
310
  const normalized = style === 'bar' ? 'bar' : 'dots';
315
311
  this.sessionTimelineStyle = normalized;
316
- try {
317
- localStorage.setItem('codexmateSessionTimelineStyle', normalized);
318
- } catch (_) {}
319
312
  if (typeof this.persistWebUiPreferences === 'function') {
320
313
  this.persistWebUiPreferences({ sessionTimelineStyle: normalized });
321
314
  }
@@ -324,7 +317,6 @@ export function createSessionActionMethods(options = {}) {
324
317
  setConfigTemplateDiffConfirmEnabled(value) {
325
318
  const enabled = this.normalizeConfigTemplateDiffConfirmEnabled(value);
326
319
  this.configTemplateDiffConfirmEnabled = enabled;
327
- persistConfigTemplateDiffConfirmEnabledToStorage(enabled);
328
320
  if (typeof this.persistWebUiPreferences === 'function') {
329
321
  this.persistWebUiPreferences({ configTemplateDiffConfirmEnabled: enabled });
330
322
  }
@@ -338,9 +330,6 @@ export function createSessionActionMethods(options = {}) {
338
330
  setShareCommandPrefix(value) {
339
331
  const normalized = this.normalizeShareCommandPrefix(value);
340
332
  this.shareCommandPrefix = normalized;
341
- try {
342
- localStorage.setItem('codexmateShareCommandPrefix', normalized);
343
- } catch (_) {}
344
333
  if (typeof this.persistWebUiPreferences === 'function') {
345
334
  this.persistWebUiPreferences({ shareCommandPrefix: normalized });
346
335
  }