codexmate 0.0.27 → 0.0.29

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 (51) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/cli/builtin-proxy.js +430 -4
  4. package/cli/openai-bridge.js +498 -13
  5. package/cli.js +130 -41
  6. package/lib/cli-models-utils.js +71 -10
  7. package/lib/cli-webhook.js +126 -0
  8. package/package.json +76 -74
  9. package/plugins/prompt-templates/computed.mjs +1 -1
  10. package/plugins/prompt-templates/methods.mjs +0 -66
  11. package/plugins/prompt-templates/overview.mjs +1 -0
  12. package/web-ui/app.js +21 -16
  13. package/web-ui/index.html +1 -0
  14. package/web-ui/logic.codex.mjs +69 -0
  15. package/web-ui/modules/app.computed.dashboard.mjs +54 -0
  16. package/web-ui/modules/app.computed.session.mjs +22 -17
  17. package/web-ui/modules/app.methods.claude-config.mjs +24 -8
  18. package/web-ui/modules/app.methods.codex-config.mjs +35 -3
  19. package/web-ui/modules/app.methods.index.mjs +2 -0
  20. package/web-ui/modules/app.methods.navigation.mjs +21 -3
  21. package/web-ui/modules/app.methods.providers.mjs +96 -7
  22. package/web-ui/modules/app.methods.session-actions.mjs +3 -6
  23. package/web-ui/modules/app.methods.session-browser.mjs +1 -6
  24. package/web-ui/modules/app.methods.session-trash.mjs +6 -7
  25. package/web-ui/modules/app.methods.startup-claude.mjs +8 -1
  26. package/web-ui/modules/app.methods.webhook.mjs +79 -0
  27. package/web-ui/modules/i18n.dict.mjs +1104 -104
  28. package/web-ui/modules/i18n.mjs +9 -3
  29. package/web-ui/modules/provider-url-display.mjs +17 -0
  30. package/web-ui/partials/index/layout-header.html +25 -0
  31. package/web-ui/partials/index/modals-basic.html +0 -3
  32. package/web-ui/partials/index/panel-config-claude.html +10 -3
  33. package/web-ui/partials/index/panel-config-codex.html +44 -4
  34. package/web-ui/partials/index/panel-plugins.html +3 -29
  35. package/web-ui/partials/index/panel-sessions.html +0 -10
  36. package/web-ui/partials/index/panel-settings.html +93 -177
  37. package/web-ui/partials/index/panel-trash.html +88 -0
  38. package/web-ui/session-helpers.mjs +2 -2
  39. package/web-ui/styles/base-theme.css +47 -34
  40. package/web-ui/styles/controls-forms.css +27 -28
  41. package/web-ui/styles/docs-panel.css +63 -39
  42. package/web-ui/styles/layout-shell.css +69 -46
  43. package/web-ui/styles/modals-core.css +12 -10
  44. package/web-ui/styles/navigation-panels.css +36 -35
  45. package/web-ui/styles/responsive.css +4 -4
  46. package/web-ui/styles/sessions-list.css +10 -6
  47. package/web-ui/styles/settings-panel.css +197 -33
  48. package/web-ui/styles/titles-cards.css +90 -26
  49. package/web-ui/styles/trash-panel.css +90 -0
  50. package/web-ui/styles/webhook.css +81 -0
  51. package/web-ui/styles.css +2 -0
@@ -43,6 +43,16 @@ export function createClaudeConfigMethods(options = {}) {
43
43
  }
44
44
  },
45
45
 
46
+ openCloneClaudeConfigModal(name, config) {
47
+ this.newClaudeConfig = {
48
+ name: '',
49
+ apiKey: config.apiKey || '',
50
+ baseUrl: config.baseUrl || '',
51
+ model: config.model || ''
52
+ };
53
+ this.showClaudeConfigModal = true;
54
+ },
55
+
46
56
  openEditConfigModal(name) {
47
57
  const config = this.claudeConfigs[name];
48
58
  this.editingConfig = {
@@ -77,7 +87,7 @@ export function createClaudeConfigMethods(options = {}) {
77
87
 
78
88
  const config = this.claudeConfigs[name];
79
89
  if (!config.apiKey) {
80
- this.showMessage('已保存,未应用', 'info');
90
+ this.showMessage('已保存(未填写 API Key)', 'info');
81
91
  this.closeEditConfigModal();
82
92
  if (name === this.currentClaudeConfig) {
83
93
  this.refreshClaudeModelContext();
@@ -85,14 +95,17 @@ export function createClaudeConfigMethods(options = {}) {
85
95
  return;
86
96
  }
87
97
 
98
+ const _claudeKey = `${name}|${config.apiKey || ""}|${config.baseUrl || ""}|${config.model || ""}`;
88
99
  try {
89
100
  const res = await api('apply-claude-config', { config });
90
101
  if (res.error || res.success === false) {
91
102
  this.showMessage(res.error || '应用配置失败', 'error');
92
103
  } else {
93
104
  this.currentClaudeConfig = name;
94
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
95
- this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
105
+ if (this._lastAppliedClaudeKey !== _claudeKey) {
106
+ this.showMessage('Claude 配置已生效', 'success');
107
+ this._lastAppliedClaudeKey = _claudeKey;
108
+ }
96
109
  this.closeEditConfigModal();
97
110
  this.refreshClaudeModelContext();
98
111
  }
@@ -153,18 +166,21 @@ export function createClaudeConfigMethods(options = {}) {
153
166
 
154
167
  if (!config.apiKey) {
155
168
  if (config.externalCredentialType) {
156
- return this.showMessage('检测到外部 Claude 认证状态;当前仅支持展示,若需由 codexmate 接管请补充 API Key', 'info');
169
+ return this.showMessage('使用外部认证,无需 API Key', 'info');
157
170
  }
158
171
  return this.showMessage('请先配置 API Key', 'error');
159
172
  }
160
173
 
174
+ const _claudeKey2 = `${name}|${config.apiKey || ""}|${config.baseUrl || ""}|${config.model || ""}`;
161
175
  try {
162
176
  const res = await api('apply-claude-config', { config });
163
177
  if (res.error || res.success === false) {
164
178
  this.showMessage(res.error || '应用配置失败', 'error');
165
179
  } else {
166
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
167
- this.showMessage(`已应用配置到 Claude 设置: ${name}${targetTip}`, 'success');
180
+ if (this._lastAppliedClaudeKey !== _claudeKey2) {
181
+ this.showMessage('配置已应用', 'success');
182
+ this._lastAppliedClaudeKey = _claudeKey2;
183
+ }
168
184
  }
169
185
  } catch (_) {
170
186
  this.showMessage('应用配置失败', 'error');
@@ -176,8 +192,8 @@ export function createClaudeConfigMethods(options = {}) {
176
192
  this.newClaudeConfig = {
177
193
  name: '',
178
194
  apiKey: '',
179
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
180
- model: 'glm-4.7'
195
+ baseUrl: '',
196
+ model: ''
181
197
  };
182
198
  }
183
199
  };
@@ -180,7 +180,19 @@ export function createCodexConfigMethods(options = {}) {
180
180
  const previousModels = Array.isArray(this.models) ? [...this.models] : [];
181
181
  const previousModelsSource = this.modelsSource;
182
182
  const previousModelsHasCurrent = this.modelsHasCurrent;
183
+ // 切走前把上一个 provider 的 model 落到内存字典,避免切回时显示抖动。
184
+ if (previousProvider && typeof previousModel === 'string' && previousModel.trim() && previousModel !== '未设置') {
185
+ if (!this.currentModels || typeof this.currentModels !== 'object') this.currentModels = {};
186
+ this.currentModels[previousProvider] = previousModel.trim();
187
+ }
183
188
  this.currentProvider = name;
189
+ // 立即按字典预填,让 UI 不出现空白;远端 /models 后台异步补齐。
190
+ const dictModel = typeof this.activeProviderModel === 'function'
191
+ ? this.activeProviderModel(name)
192
+ : '';
193
+ if (dictModel) {
194
+ this.currentModel = dictModel;
195
+ }
184
196
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
185
197
 
186
198
  // 不要把“切换提供商”强绑定到 /models 成功与否:
@@ -195,7 +207,11 @@ export function createCodexConfigMethods(options = {}) {
195
207
 
196
208
  await Promise.race([modelsTask, delay(250)]);
197
209
 
198
- if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
210
+ // 只在“字典中没有该 provider 记录”时才允许 remote 首项覆盖,否则尊重用户上次选择。
211
+ if (!dictModel
212
+ && this.modelsSource === 'remote'
213
+ && this.models.length > 0
214
+ && !this.models.includes(this.currentModel)) {
199
215
  this.currentModel = this.models[0];
200
216
  this.modelsHasCurrent = true;
201
217
  }
@@ -208,7 +224,10 @@ export function createCodexConfigMethods(options = {}) {
208
224
  await modelsTask;
209
225
 
210
226
  if (this.currentProvider === name) {
211
- if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
227
+ if (!dictModel
228
+ && this.modelsSource === 'remote'
229
+ && this.models.length > 0
230
+ && !this.models.includes(this.currentModel)) {
212
231
  this.currentModel = this.models[0];
213
232
  this.modelsHasCurrent = true;
214
233
  if (getProviderConfigModeMeta(this.configMode)) {
@@ -261,6 +280,12 @@ export function createCodexConfigMethods(options = {}) {
261
280
  },
262
281
 
263
282
  async onModelChange() {
283
+ const name = String(this.currentProvider || '').trim();
284
+ const model = typeof this.currentModel === 'string' ? this.currentModel.trim() : '';
285
+ if (name && model) {
286
+ if (!this.currentModels || typeof this.currentModels !== 'object') this.currentModels = {};
287
+ this.currentModels[name] = model;
288
+ }
264
289
  await this.applyCodexConfigDirect();
265
290
  },
266
291
 
@@ -629,6 +654,8 @@ export function createCodexConfigMethods(options = {}) {
629
654
  this.modelContextWindowInput = modelContextWindow.text;
630
655
  this.modelAutoCompactTokenLimitInput = modelAutoCompactTokenLimit.text;
631
656
 
657
+ const _codexKey = `${provider}|${model}|${this.serviceTier || ""}|${this.modelReasoningEffort || ""}|${modelContextWindow.value}|${modelAutoCompactTokenLimit.value}`;
658
+
632
659
  this.codexApplying = true;
633
660
  try {
634
661
  const tplRes = await api('get-config-template', {
@@ -665,7 +692,12 @@ export function createCodexConfigMethods(options = {}) {
665
692
  }
666
693
 
667
694
  if (options.silent !== true) {
668
- this.showMessage('配置已应用', 'success');
695
+ if (this._lastAppliedCodexKey !== _codexKey) {
696
+ this.showMessage('配置已应用', 'success');
697
+ this._lastAppliedCodexKey = _codexKey;
698
+ }
699
+ } else {
700
+ this._lastAppliedCodexKey = _codexKey;
669
701
  }
670
702
 
671
703
  const refreshOptions = options.silent === true
@@ -29,6 +29,7 @@ import { createStartupClaudeMethods } from './app.methods.startup-claude.mjs';
29
29
  import { createSkillsMethods } from './skills.methods.mjs';
30
30
  import { createPluginsMethods } from './plugins.methods.mjs';
31
31
  import { createI18nMethods } from './i18n.mjs';
32
+ import { createWebhookMethods } from './app.methods.webhook.mjs';
32
33
  import {
33
34
  CONFIG_MODE_SET,
34
35
  getProviderConfigModeMeta
@@ -43,6 +44,7 @@ import {
43
44
  export function createAppMethods() {
44
45
  return {
45
46
  ...createI18nMethods(),
47
+ ...createWebhookMethods(),
46
48
  ...createStartupClaudeMethods({
47
49
  api,
48
50
  defaultModelContextWindow: DEFAULT_MODEL_CONTEXT_WINDOW,
@@ -1,4 +1,4 @@
1
- export function createNavigationMethods(options = {}) {
1
+ export function createNavigationMethods(options = {}) {
2
2
  const {
3
3
  configModeSet,
4
4
  switchMainTabHelper,
@@ -14,7 +14,8 @@ export function createNavigationMethods(options = {}) {
14
14
  'market',
15
15
  'plugins',
16
16
  'docs',
17
- 'settings'
17
+ 'settings',
18
+ 'trash'
18
19
  ]);
19
20
  const loadDoctorOverview = async (vm, options = {}) => {
20
21
  if (!vm || typeof vm !== 'object') return false;
@@ -67,7 +68,9 @@ export function createNavigationMethods(options = {}) {
67
68
  : vm.configMode;
68
69
  const mainTab = typeof mainTabSource === 'string' ? mainTabSource.trim().toLowerCase() : '';
69
70
  const configMode = typeof configModeSource === 'string' ? configModeSource.trim().toLowerCase() : '';
71
+ const settingsTab = typeof vm.settingsTab === 'string' ? vm.settingsTab.trim().toLowerCase() : 'general';
70
72
  const snapshot = {
73
+ settingsTab: settingsTab === 'data' ? 'data' : 'general',
71
74
  mainTab: MAIN_TAB_SET.has(mainTab) ? mainTab : 'dashboard',
72
75
  configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex'
73
76
  };
@@ -77,6 +80,9 @@ export function createNavigationMethods(options = {}) {
77
80
  };
78
81
 
79
82
  return {
83
+ saveNavState() {
84
+ persistNavState(this);
85
+ },
80
86
  restoreNavStateFromStorage() {
81
87
  if (this.__navStateRestoring) return false;
82
88
  const restored = readNavState();
@@ -89,7 +95,11 @@ export function createNavigationMethods(options = {}) {
89
95
  : '';
90
96
  const shouldUpdateConfigMode = !!(nextConfigMode && configModeSet && configModeSet.has(nextConfigMode));
91
97
  const shouldUpdateMainTab = !!(nextMainTab && MAIN_TAB_SET.has(nextMainTab) && nextMainTab !== this.mainTab);
92
- if (!shouldUpdateConfigMode && !shouldUpdateMainTab) {
98
+ const nextSettingsTab = restored && typeof restored.settingsTab === 'string'
99
+ ? restored.settingsTab.trim().toLowerCase()
100
+ : '';
101
+ const shouldUpdateSettingsTab = !!(nextSettingsTab && (nextSettingsTab === 'general' || nextSettingsTab === 'data') && nextSettingsTab !== this.settingsTab);
102
+ if (!shouldUpdateConfigMode && !shouldUpdateMainTab && !shouldUpdateSettingsTab) {
93
103
  return false;
94
104
  }
95
105
  this.__navStateRestoring = true;
@@ -97,6 +107,9 @@ export function createNavigationMethods(options = {}) {
97
107
  if (shouldUpdateConfigMode) {
98
108
  this.configMode = nextConfigMode;
99
109
  }
110
+ if (shouldUpdateSettingsTab) {
111
+ this.settingsTab = nextSettingsTab;
112
+ }
100
113
  if (shouldUpdateMainTab) {
101
114
  this.switchMainTab(nextMainTab);
102
115
  }
@@ -411,6 +424,11 @@ export function createNavigationMethods(options = {}) {
411
424
  switchState.ticket += 1;
412
425
  switchState.pendingTarget = '';
413
426
  if (targetTab === 'dashboard' && !this.__doctorLoadedOnce) {
427
+ if (targetTab === 'trash' && !this.sessionTrashLoadedOnce) {
428
+ if (typeof this.loadSessionTrash === 'function') {
429
+ void this.loadSessionTrash({ forceRefresh: false });
430
+ }
431
+ }
414
432
  void loadDoctorOverview(this);
415
433
  }
416
434
  if (
@@ -48,6 +48,12 @@ function normalizeProviderDraftState(target) {
48
48
  }
49
49
  }
50
50
 
51
+ function maskKeyLocal(key) {
52
+ if (!key) return '';
53
+ if (key.length <= 8) return '****';
54
+ return key.substring(0, 4) + '...' + key.substring(key.length - 4);
55
+ }
56
+
51
57
  function getProviderValidationForContext(vm, mode = 'add') {
52
58
  const draft = mode === 'edit' ? vm.editingProvider : vm.newProvider;
53
59
  const editingName = mode === 'edit' ? normalizeText(draft && draft.name) : '';
@@ -149,15 +155,38 @@ export function createProvidersMethods(options = {}) {
149
155
  if (this.newProvider && this.newProvider.useTransform) {
150
156
  payload.useTransform = true;
151
157
  }
158
+ const suggestedModel = typeof this.newProvider._suggestedModel === 'string'
159
+ ? this.newProvider._suggestedModel.trim()
160
+ : '';
152
161
  const res = await api('add-provider', payload);
153
162
  if (res.error) {
154
163
  this.showMessage(res.error, 'error');
155
164
  return;
156
165
  }
157
166
 
167
+ // 本地更新:构造新 provider 对象并追加到列表
168
+ const newProvider = {
169
+ name: validation.name,
170
+ url: validation.url,
171
+ upstreamUrl: '',
172
+ codexmate_bridge: payload.useTransform ? 'openai' : '',
173
+ key: maskKeyLocal(payload.key),
174
+ hasKey: !!payload.key,
175
+ models: [],
176
+ current: false,
177
+ readOnly: false,
178
+ nonDeletable: false,
179
+ nonEditable: false
180
+ };
181
+ this.providersList = [...this.providersList, newProvider];
182
+
158
183
  this.showMessage('操作成功', 'success');
159
184
  this.closeAddModal();
160
- await this.loadAll();
185
+
186
+ if (suggestedModel) {
187
+ if (!this.currentModels || typeof this.currentModels !== 'object') this.currentModels = {};
188
+ this.currentModels[validation.name] = suggestedModel;
189
+ }
161
190
  } catch (e) {
162
191
  this.showMessage('添加失败', 'error');
163
192
  }
@@ -229,17 +258,42 @@ export function createProvidersMethods(options = {}) {
229
258
  this.showMessage(res.error, 'error');
230
259
  return;
231
260
  }
261
+
262
+ // 本地更新:从列表中移除
263
+ this.providersList = this.providersList.filter(p => p.name !== name);
264
+
265
+ // 清理 currentModels
266
+ if (this.currentModels && this.currentModels[name]) {
267
+ delete this.currentModels[name];
268
+ }
269
+
232
270
  if (res.switched && res.provider) {
271
+ this.currentProvider = res.provider;
272
+ if (res.model) this.currentModel = res.model;
273
+ // 更新 current 标记
274
+ this.providersList = this.providersList.map(p => ({
275
+ ...p,
276
+ current: p.name === res.provider
277
+ }));
233
278
  this.showMessage(`已删除提供商,自动切换到 ${res.provider}${res.model ? ` / ${res.model}` : ''}`, 'success');
234
279
  } else {
235
280
  this.showMessage('操作成功', 'success');
236
281
  }
237
- await this.loadAll();
238
282
  } catch (_) {
239
283
  this.showMessage('删除失败', 'error');
240
284
  }
241
285
  },
242
286
 
287
+ openCloneProviderModal(provider) {
288
+ this.newProvider = {
289
+ name: '',
290
+ url: normalizeProviderUrl(provider.url || ''),
291
+ key: '',
292
+ useTransform: !!(provider.codexmate_bridge || '').trim() || /\/bridge\/openai\//.test(provider.url || '')
293
+ };
294
+ this.showAddModal = true;
295
+ },
296
+
243
297
  async openEditModal(provider) {
244
298
  const requestId = Symbol('openEditModal');
245
299
  this._openEditModalRequestId = requestId;
@@ -311,9 +365,22 @@ export function createProvidersMethods(options = {}) {
311
365
  this.showMessage(res.error, 'error');
312
366
  return;
313
367
  }
368
+
369
+ // 本地更新:更新列表中对应 provider 的 url 和 key
370
+ this.providersList = this.providersList.map(p => {
371
+ if (p.name === validation.name) {
372
+ return {
373
+ ...p,
374
+ url: validation.url,
375
+ key: params.key ? maskKeyLocal(params.key) : p.key,
376
+ hasKey: params.key ? true : p.hasKey
377
+ };
378
+ }
379
+ return p;
380
+ });
381
+
314
382
  this.closeEditModal();
315
383
  this.showMessage('操作成功', 'success');
316
- await this.loadAll();
317
384
  } catch (e) {
318
385
  this.showMessage('更新失败', 'error');
319
386
  }
@@ -348,13 +415,26 @@ export function createProvidersMethods(options = {}) {
348
415
  return this.showMessage('请输入模型', 'error');
349
416
  }
350
417
  try {
351
- const res = await api('add-model', { model: this.newModelName.trim() });
418
+ const modelName = this.newModelName.trim();
419
+ const res = await api('add-model', { model: modelName });
352
420
  if (res.error) {
353
421
  this.showMessage(res.error, 'error');
354
422
  } else {
423
+ // 本地更新:在当前 provider 的 models 中追加
424
+ this.providersList = this.providersList.map(p => {
425
+ if (p.name === this.currentProvider) {
426
+ const exists = p.models.some(m => m.id === modelName);
427
+ if (!exists) {
428
+ return {
429
+ ...p,
430
+ models: [...p.models, { id: modelName, name: modelName, cost: null, contextWindow: undefined, maxTokens: undefined }]
431
+ };
432
+ }
433
+ }
434
+ return p;
435
+ });
355
436
  this.showMessage('操作成功', 'success');
356
437
  this.closeModelModal();
357
- await this.loadAll();
358
438
  }
359
439
  } catch (_) {
360
440
  this.showMessage('新增模型失败', 'error');
@@ -367,8 +447,17 @@ export function createProvidersMethods(options = {}) {
367
447
  if (res.error) {
368
448
  this.showMessage(res.error, 'error');
369
449
  } else {
450
+ // 本地更新:从当前 provider 的 models 中移除
451
+ this.providersList = this.providersList.map(p => {
452
+ if (p.name === this.currentProvider) {
453
+ return {
454
+ ...p,
455
+ models: p.models.filter(m => m.id !== model)
456
+ };
457
+ }
458
+ return p;
459
+ });
370
460
  this.showMessage('操作成功', 'success');
371
- await this.loadAll();
372
461
  }
373
462
  } catch (_) {
374
463
  this.showMessage('删除模型失败', 'error');
@@ -377,7 +466,7 @@ export function createProvidersMethods(options = {}) {
377
466
 
378
467
  closeAddModal() {
379
468
  this.showAddModal = false;
380
- this.newProvider = { name: '', url: '', key: '', useTransform: false };
469
+ this.newProvider = { name: '', url: '', key: '', useTransform: false, _suggestedModel: '' };
381
470
  },
382
471
 
383
472
  closeModelModal() {
@@ -181,12 +181,9 @@ export function createSessionActionMethods(options = {}) {
181
181
  return `gemini -r ${arg}`;
182
182
  }
183
183
  if (source === 'claude') {
184
- return `claude -r ${arg}`;
184
+ return `claude --dangerously-skip-permissions -r ${arg}`;
185
185
  }
186
- if (this.sessionResumeWithYolo) {
187
- return `codex --yolo resume ${arg}`;
188
- }
189
- return `codex resume ${arg}`;
186
+ return `codex --yolo resume ${arg}`;
190
187
  },
191
188
 
192
189
  extractClaudeResumeKeyFromFilePath(filePath) {
@@ -254,7 +251,7 @@ export function createSessionActionMethods(options = {}) {
254
251
 
255
252
  getShareCommandPrefixInvocation() {
256
253
  const prefix = this.normalizeShareCommandPrefix(this.shareCommandPrefix);
257
- return prefix === 'codexmate' ? 'codexmate' : 'npm start';
254
+ return prefix === 'codexmate' ? 'codexmate' : 'npm start --';
258
255
  },
259
256
 
260
257
  setShareCommandPrefix(value) {
@@ -249,11 +249,6 @@ export function createSessionBrowserMethods(options = {}) {
249
249
  }
250
250
  },
251
251
 
252
- onSessionResumeYoloChange() {
253
- const value = this.sessionResumeWithYolo ? '1' : '0';
254
- localStorage.setItem('codexmateSessionResumeYolo', value);
255
- },
256
-
257
252
  normalizeSessionSortMode(value) {
258
253
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
259
254
  return normalized === 'hot' ? 'hot' : 'time';
@@ -698,7 +693,7 @@ export function createSessionBrowserMethods(options = {}) {
698
693
  for (const session of visible) {
699
694
  if (!session || typeof session !== 'object') continue;
700
695
  const messageCountRaw = Number(session.messageCount);
701
- const shouldHydrate = !Number.isFinite(messageCountRaw) || messageCountRaw === 0;
696
+ const shouldHydrate = !Number.isFinite(messageCountRaw) || (messageCountRaw === 0 && !session.__messageCountExact);
702
697
  if (!shouldHydrate) continue;
703
698
  const key = this.getSessionExportKey(session);
704
699
  if (!key) continue;
@@ -208,10 +208,10 @@ export function createSessionTrashMethods(options = {}) {
208
208
  },
209
209
 
210
210
  normalizeSettingsTab(tab) {
211
- if (tab === 'trash' || tab === 'device') {
211
+ if (tab === 'general' || tab === 'data') {
212
212
  return tab;
213
213
  }
214
- return 'backup';
214
+ return 'general';
215
215
  },
216
216
 
217
217
  async onSettingsTabClick(tab) {
@@ -221,12 +221,11 @@ export function createSessionTrashMethods(options = {}) {
221
221
  async switchSettingsTab(tab, options = {}) {
222
222
  const nextTab = this.normalizeSettingsTab(tab);
223
223
  this.settingsTab = nextTab;
224
- if (nextTab !== 'trash') {
225
- return;
224
+ if (typeof this.saveNavState === 'function') {
225
+ this.saveNavState();
226
226
  }
227
- const forceRefresh = options.forceRefresh === true;
228
- if (forceRefresh || !this.sessionTrashLoadedOnce) {
229
- await this.loadSessionTrash({ forceRefresh });
227
+ if (nextTab !== 'data') {
228
+ return;
230
229
  }
231
230
  },
232
231
 
@@ -60,7 +60,14 @@ export function createStartupClaudeMethods(options = {}) {
60
60
  return false;
61
61
  }
62
62
  this.currentProvider = statusRes.provider;
63
- this.currentModel = statusRes.model;
63
+ this.currentModels = statusRes.currentModels && typeof statusRes.currentModels === 'object'
64
+ ? { ...statusRes.currentModels }
65
+ : {};
66
+ const dictModelForCurrent = this.currentProvider
67
+ && typeof this.currentModels[this.currentProvider] === 'string'
68
+ ? this.currentModels[this.currentProvider].trim()
69
+ : '';
70
+ this.currentModel = dictModelForCurrent || statusRes.model;
64
71
  try {
65
72
  const installRes = await withTimeout(api('install-status'), Math.max(0, Math.min(1200, timeLeftMs())));
66
73
  if (installRes && !installRes.error) {
@@ -0,0 +1,79 @@
1
+ import { api } from './api.mjs';
2
+
3
+ export function createWebhookMethods() {
4
+ return {
5
+ async loadWebhookSettings() {
6
+ try {
7
+ const data = await api('get-webhook-config');
8
+ if (data && typeof data === 'object' && !data.error) {
9
+ this.webhookConfig = {
10
+ enabled: !!data.enabled,
11
+ url: typeof data.url === 'string' ? data.url : '',
12
+ events: Array.isArray(data.events) && data.events.length
13
+ ? data.events.slice()
14
+ : this.webhookEventOptions.slice()
15
+ };
16
+ }
17
+ } catch (e) {
18
+ this.webhookTestResult = { ok: false, error: e && e.message ? e.message : String(e) };
19
+ }
20
+ },
21
+
22
+ async saveWebhookSettings() {
23
+ this.webhookSaving = true;
24
+ try {
25
+ const cfg = {
26
+ enabled: !!this.webhookConfig.enabled,
27
+ url: typeof this.webhookConfig.url === 'string' ? this.webhookConfig.url.trim() : '',
28
+ events: Array.isArray(this.webhookConfig.events) ? this.webhookConfig.events.slice() : []
29
+ };
30
+ const saved = await api('set-webhook-config', { config: cfg });
31
+ if (saved && typeof saved === 'object' && !saved.error) {
32
+ this.webhookConfig = {
33
+ enabled: !!saved.enabled,
34
+ url: typeof saved.url === 'string' ? saved.url : '',
35
+ events: Array.isArray(saved.events) ? saved.events.slice() : []
36
+ };
37
+ this.webhookTestResult = { ok: true, status: 'saved' };
38
+ } else {
39
+ this.webhookTestResult = { ok: false, error: (saved && saved.error) || 'save failed' };
40
+ }
41
+ } catch (e) {
42
+ this.webhookTestResult = { ok: false, error: e && e.message ? e.message : String(e) };
43
+ } finally {
44
+ this.webhookSaving = false;
45
+ }
46
+ },
47
+
48
+ async testWebhook() {
49
+ this.webhookTesting = true;
50
+ try {
51
+ const cfg = {
52
+ enabled: true,
53
+ url: typeof this.webhookConfig.url === 'string' ? this.webhookConfig.url.trim() : '',
54
+ events: Array.isArray(this.webhookConfig.events) && this.webhookConfig.events.length
55
+ ? this.webhookConfig.events.slice()
56
+ : this.webhookEventOptions.slice()
57
+ };
58
+ const r = await api('test-webhook', { config: cfg });
59
+ this.webhookTestResult = r || { ok: false, error: 'no result' };
60
+ } catch (e) {
61
+ this.webhookTestResult = { ok: false, error: e && e.message ? e.message : String(e) };
62
+ } finally {
63
+ this.webhookTesting = false;
64
+ }
65
+ },
66
+
67
+ toggleWebhookEvent(eventName) {
68
+ if (!Array.isArray(this.webhookConfig.events)) {
69
+ this.webhookConfig.events = [];
70
+ }
71
+ const idx = this.webhookConfig.events.indexOf(eventName);
72
+ if (idx === -1) {
73
+ this.webhookConfig.events.push(eventName);
74
+ } else {
75
+ this.webhookConfig.events.splice(idx, 1);
76
+ }
77
+ }
78
+ };
79
+ }