codexmate 0.0.22 → 0.0.24

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 (66) hide show
  1. package/README.md +5 -3
  2. package/README.zh.md +8 -5
  3. package/cli/auth-profiles.js +23 -7
  4. package/cli/doctor-core.js +903 -0
  5. package/cli/import-skills-url.js +334 -0
  6. package/cli.js +304 -208
  7. package/lib/cli-models-utils.js +0 -40
  8. package/lib/cli-network-utils.js +28 -2
  9. package/package.json +5 -2
  10. package/plugins/README.md +20 -0
  11. package/plugins/README.zh-CN.md +20 -0
  12. package/plugins/prompt-templates/comment-polish/index.mjs +25 -0
  13. package/plugins/prompt-templates/computed.mjs +253 -0
  14. package/plugins/prompt-templates/index.mjs +8 -0
  15. package/plugins/prompt-templates/manifest.mjs +15 -0
  16. package/plugins/prompt-templates/methods.mjs +619 -0
  17. package/plugins/prompt-templates/overview.mjs +90 -0
  18. package/plugins/prompt-templates/ownership.mjs +19 -0
  19. package/plugins/prompt-templates/rule-ack/index.mjs +21 -0
  20. package/plugins/prompt-templates/storage.mjs +64 -0
  21. package/plugins/registry.mjs +16 -0
  22. package/res/logo-pack.webp +0 -0
  23. package/web-ui/app.js +68 -34
  24. package/web-ui/index.html +4 -3
  25. package/web-ui/modules/app.computed.dashboard.mjs +22 -22
  26. package/web-ui/modules/app.computed.main-tabs.mjs +9 -2
  27. package/web-ui/modules/app.methods.agents.mjs +91 -3
  28. package/web-ui/modules/app.methods.codex-config.mjs +153 -164
  29. package/web-ui/modules/app.methods.install.mjs +16 -0
  30. package/web-ui/modules/app.methods.navigation.mjs +76 -0
  31. package/web-ui/modules/app.methods.runtime.mjs +24 -2
  32. package/web-ui/modules/app.methods.session-browser.mjs +73 -1
  33. package/web-ui/modules/app.methods.startup-claude.mjs +12 -0
  34. package/web-ui/modules/app.methods.task-orchestration.mjs +96 -11
  35. package/web-ui/modules/config-mode.computed.mjs +1 -3
  36. package/web-ui/modules/i18n.dict.mjs +2039 -0
  37. package/web-ui/modules/i18n.mjs +2 -1555
  38. package/web-ui/modules/plugins.computed.mjs +2 -219
  39. package/web-ui/modules/plugins.methods.mjs +2 -619
  40. package/web-ui/modules/plugins.storage.mjs +11 -37
  41. package/web-ui/modules/sessions-filters-url.mjs +85 -0
  42. package/web-ui/partials/index/layout-header.html +38 -34
  43. package/web-ui/partials/index/modal-config-template-agents.html +3 -4
  44. package/web-ui/partials/index/modal-health-check.html +33 -60
  45. package/web-ui/partials/index/panel-config-claude.html +56 -15
  46. package/web-ui/partials/index/panel-config-codex.html +68 -19
  47. package/web-ui/partials/index/panel-config-openclaw.html +8 -3
  48. package/web-ui/partials/index/panel-dashboard.html +186 -0
  49. package/web-ui/partials/index/panel-docs.html +1 -1
  50. package/web-ui/partials/index/panel-market.html +3 -0
  51. package/web-ui/partials/index/panel-orchestration.html +105 -111
  52. package/web-ui/partials/index/panel-plugins.html +48 -12
  53. package/web-ui/partials/index/panel-sessions.html +12 -3
  54. package/web-ui/partials/index/panel-settings.html +1 -1
  55. package/web-ui/partials/index/panel-usage.html +7 -6
  56. package/web-ui/styles/controls-forms.css +16 -2
  57. package/web-ui/styles/dashboard.css +274 -0
  58. package/web-ui/styles/layout-shell.css +11 -5
  59. package/web-ui/styles/navigation-panels.css +8 -0
  60. package/web-ui/styles/plugins-panel.css +5 -0
  61. package/web-ui/styles/sessions-list.css +3 -3
  62. package/web-ui/styles/sessions-usage.css +37 -0
  63. package/web-ui/styles/skills-market.css +12 -2
  64. package/web-ui/styles/task-orchestration.css +57 -11
  65. package/web-ui/styles.css +1 -0
  66. package/res/logo.png +0 -0
@@ -193,8 +193,8 @@ export function createAgentsMethods(options = {}) {
193
193
  const fileName = (options.fileName || this.openclawWorkspaceFileName || 'AGENTS.md').trim();
194
194
  this.agentsContext = 'openclaw-workspace';
195
195
  this.agentsWorkspaceFileName = fileName;
196
- this.agentsModalTitle = `OpenClaw 工作区文件: ${fileName}`;
197
- this.agentsModalHint = `保存后会写入 OpenClaw Workspace 下的 ${fileName}。`;
196
+ this.agentsModalTitle = tr('modal.agents.title.openclawWorkspaceFile', `OpenClaw 工作区文件: ${fileName}`, { fileName });
197
+ this.agentsModalHint = tr('modal.agents.hint.openclawWorkspaceFile', `保存后会写入 OpenClaw Workspace 下的 ${fileName}。`, { fileName });
198
198
  return;
199
199
  }
200
200
  this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
@@ -224,7 +224,47 @@ export function createAgentsMethods(options = {}) {
224
224
  this._agentsDiffPreviewRequestToken = null;
225
225
  },
226
226
  handleGlobalKeydown(event) {
227
- if (!event || event.key !== 'Escape') {
227
+ if (!event) {
228
+ return;
229
+ }
230
+ const isCmdLike = !!(event.metaKey || event.ctrlKey);
231
+ const key = typeof event.key === 'string' ? event.key : '';
232
+ const isSearchHotkey = isCmdLike && !event.altKey && (key === 'k' || key === 'K');
233
+ if (isSearchHotkey) {
234
+ const target = event.target;
235
+ const tag = target && target.tagName ? String(target.tagName).toUpperCase() : '';
236
+ const isTypingTarget = !!(
237
+ tag === 'INPUT'
238
+ || tag === 'TEXTAREA'
239
+ || tag === 'SELECT'
240
+ || (target && target.isContentEditable)
241
+ );
242
+ if (!isTypingTarget) {
243
+ event.preventDefault();
244
+ event.stopPropagation();
245
+ try {
246
+ const focusSelector = (() => {
247
+ if (this.showSkillsModal) return '.skills-filter-row input.form-input';
248
+ if (this.mainTab === 'sessions') return '#panel-sessions .session-query-input';
249
+ if (this.mainTab === 'plugins' && this.pluginsActiveId === 'prompt-templates' && this.promptTemplatesMode !== 'compose') {
250
+ return '#panel-plugins .prompt-templates-toolbar input.form-input';
251
+ }
252
+ return '';
253
+ })();
254
+ if (focusSelector) {
255
+ const el = document.querySelector(focusSelector);
256
+ if (el && typeof el.focus === 'function') {
257
+ el.focus();
258
+ if (typeof el.select === 'function') {
259
+ el.select();
260
+ }
261
+ }
262
+ }
263
+ } catch (_) {}
264
+ }
265
+ return;
266
+ }
267
+ if (key !== 'Escape') {
228
268
  return;
229
269
  }
230
270
  if (this.showConfirmDialog) {
@@ -233,6 +273,54 @@ export function createAgentsMethods(options = {}) {
233
273
  this.resolveConfirmDialog(false);
234
274
  return;
235
275
  }
276
+ if (this.showSkillsModal && typeof this.closeSkillsModal === 'function') {
277
+ event.preventDefault();
278
+ event.stopPropagation();
279
+ this.closeSkillsModal();
280
+ return;
281
+ }
282
+ if (this.showOpenclawConfigModal && typeof this.closeOpenclawConfigModal === 'function') {
283
+ event.preventDefault();
284
+ event.stopPropagation();
285
+ this.closeOpenclawConfigModal();
286
+ return;
287
+ }
288
+ if (this.showConfigTemplateModal && typeof this.closeConfigTemplateModal === 'function') {
289
+ event.preventDefault();
290
+ event.stopPropagation();
291
+ this.closeConfigTemplateModal();
292
+ return;
293
+ }
294
+ if (this.showEditConfigModal && typeof this.closeEditConfigModal === 'function') {
295
+ event.preventDefault();
296
+ event.stopPropagation();
297
+ this.closeEditConfigModal();
298
+ return;
299
+ }
300
+ if (this.showClaudeConfigModal && typeof this.closeClaudeConfigModal === 'function') {
301
+ event.preventDefault();
302
+ event.stopPropagation();
303
+ this.closeClaudeConfigModal();
304
+ return;
305
+ }
306
+ if (this.showModelModal && typeof this.closeModelModal === 'function') {
307
+ event.preventDefault();
308
+ event.stopPropagation();
309
+ this.closeModelModal();
310
+ return;
311
+ }
312
+ if (this.showAddModal && typeof this.closeAddModal === 'function') {
313
+ event.preventDefault();
314
+ event.stopPropagation();
315
+ this.closeAddModal();
316
+ return;
317
+ }
318
+ if (this.showEditModal && typeof this.closeEditModal === 'function') {
319
+ event.preventDefault();
320
+ event.stopPropagation();
321
+ this.closeEditModal();
322
+ return;
323
+ }
236
324
  if (!this.showAgentsModal) {
237
325
  return;
238
326
  }
@@ -271,53 +271,164 @@ export function createCodexConfigMethods(options = {}) {
271
271
  });
272
272
  },
273
273
 
274
- async runHealthCheck() {
274
+ async runHealthCheck(options = {}) {
275
275
  this.healthCheckLoading = true;
276
276
  this.healthCheckResult = null;
277
- let shouldRunClaudeSpeedTests = false;
277
+ this.healthCheckBatchTotal = 0;
278
+ this.healthCheckBatchDone = 0;
279
+ this.healthCheckBatchFailed = 0;
278
280
  try {
279
- const res = await api('config-health-check', {
280
- remote: this.configMode === 'codex'
281
- });
281
+ const silent = !!(options && options.silent);
282
+ const forceRefresh = !!(options && options.forceRefresh);
283
+ const useDoctor = !!(options && options.doctor);
284
+ if (useDoctor) {
285
+ const res = await api('doctor', {
286
+ lang: this.lang,
287
+ remote: true,
288
+ range: this.sessionsUsageTimeRange,
289
+ targetApp: this.skillsTargetApp,
290
+ includeUsage: true,
291
+ includeTasks: true,
292
+ includeSkills: true,
293
+ includeInstall: true,
294
+ forceRefresh
295
+ });
296
+ if (hasResponseError(res)) {
297
+ this.healthCheckResult = null;
298
+ if (!silent) {
299
+ this.showMessage(getResponseMessage(res, '检查失败'), 'error');
300
+ }
301
+ return;
302
+ }
303
+ if (res && typeof res === 'object') {
304
+ this.healthCheckResult = res;
305
+ const report = res.report && typeof res.report === 'object' ? res.report : null;
306
+ const summary = report && report.summary && typeof report.summary === 'object' ? report.summary : null;
307
+ const total = summary && Number.isFinite(Number(summary.total))
308
+ ? Math.max(0, Math.floor(Number(summary.total)))
309
+ : (Array.isArray(res.issues) ? res.issues.length : 0);
310
+ const errors = summary && Number.isFinite(Number(summary.error)) ? Math.max(0, Math.floor(Number(summary.error))) : 0;
311
+ const warns = summary && Number.isFinite(Number(summary.warn)) ? Math.max(0, Math.floor(Number(summary.warn))) : 0;
312
+ this.healthCheckBatchTotal = total;
313
+ this.healthCheckBatchDone = total;
314
+ this.healthCheckBatchFailed = errors + warns;
315
+ if (!silent && res.ok) {
316
+ this.showMessage('检查通过', 'success');
317
+ }
318
+ return;
319
+ }
320
+ this.healthCheckResult = null;
321
+ if (!silent) {
322
+ this.showMessage('检查失败', 'error');
323
+ }
324
+ return;
325
+ }
326
+
327
+ if (this.configMode === 'claude') {
328
+ const entries = Object.entries(this.claudeConfigs || {});
329
+ this.healthCheckBatchTotal = entries.length;
330
+
331
+ const speedTasks = entries.map(([name, config]) => this.runClaudeSpeedTest(name, config)
332
+ .then((result) => {
333
+ if (!result || result.ok !== true) {
334
+ this.healthCheckBatchFailed += 1;
335
+ }
336
+ return { name, result };
337
+ })
338
+ .catch((err) => {
339
+ this.healthCheckBatchFailed += 1;
340
+ return {
341
+ name,
342
+ result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
343
+ };
344
+ })
345
+ .finally(() => {
346
+ this.healthCheckBatchDone += 1;
347
+ })
348
+ );
349
+
350
+ const pairs = await Promise.all(speedTasks);
351
+ const results = {};
352
+ const issues = [];
353
+ for (const pair of pairs) {
354
+ results[pair.name] = pair.result || null;
355
+ if (typeof this.buildSpeedTestIssue === 'function') {
356
+ const issue = this.buildSpeedTestIssue(pair.name, pair.result);
357
+ if (issue) issues.push(issue);
358
+ }
359
+ }
360
+ const ok = issues.length === 0 && this.healthCheckBatchFailed === 0;
361
+ this.healthCheckResult = {
362
+ ok,
363
+ issues,
364
+ remote: {
365
+ type: 'speed-test',
366
+ speedTests: results
367
+ }
368
+ };
369
+ if (ok && !silent) {
370
+ this.showMessage('检查通过', 'success');
371
+ }
372
+ return;
373
+ }
374
+
375
+ const shouldRunSpeedTests = this.configMode === 'codex';
376
+ const speedTimeoutMs = shouldRunSpeedTests ? 3500 : 0;
377
+ const providers = shouldRunSpeedTests
378
+ ? (this.providersList || [])
379
+ .map((provider) => typeof provider === 'string'
380
+ ? provider.trim()
381
+ : String((provider && provider.name) || '').trim())
382
+ .filter(Boolean)
383
+ : [];
384
+ const currentProvider = String(this.currentProvider || '').trim();
385
+ const orderedProviders = currentProvider && providers.includes(currentProvider)
386
+ ? [currentProvider, ...providers.filter((name) => name !== currentProvider)]
387
+ : providers;
388
+ this.healthCheckBatchTotal = orderedProviders.length;
389
+
390
+ const speedTasks = orderedProviders.map((provider) => this.runSpeedTest(provider, { silent: true, timeoutMs: speedTimeoutMs })
391
+ .then((result) => {
392
+ if (!result || result.ok !== true) {
393
+ this.healthCheckBatchFailed += 1;
394
+ }
395
+ return { name: provider, result };
396
+ })
397
+ .catch((err) => {
398
+ this.healthCheckBatchFailed += 1;
399
+ return {
400
+ name: provider,
401
+ result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
402
+ };
403
+ })
404
+ .finally(() => {
405
+ this.healthCheckBatchDone += 1;
406
+ })
407
+ );
408
+
409
+ const configTask = api('config-health-check', { remote: this.configMode === 'codex' });
410
+ const [res, pairs] = await Promise.all([
411
+ configTask,
412
+ Promise.all(speedTasks)
413
+ ]);
282
414
  if (hasResponseError(res)) {
283
415
  this.healthCheckResult = null;
284
- this.showMessage(getResponseMessage(res, '检查失败'), 'error');
416
+ if (!silent) {
417
+ this.showMessage(getResponseMessage(res, '检查失败'), 'error');
418
+ }
285
419
  } else if (res && typeof res === 'object') {
286
- shouldRunClaudeSpeedTests = true;
287
420
  const issues = Array.isArray(res.issues) ? [...res.issues] : [];
288
421
  let remote = res.remote || null;
289
- {
290
- const providers = (this.providersList || [])
291
- .map((provider) => typeof provider === 'string'
292
- ? provider.trim()
293
- : String((provider && provider.name) || '').trim())
294
- .filter(Boolean);
295
- const tasks = providers.map(provider =>
296
- this.runSpeedTest(provider, { silent: true })
297
- .then(result => ({ name: provider, result }))
298
- .catch(err => ({
299
- name: provider,
300
- result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
301
- }))
302
- );
303
- const pairs = await Promise.all(tasks);
422
+ if (shouldRunSpeedTests) {
304
423
  const results = {};
305
424
  for (const pair of pairs) {
306
425
  results[pair.name] = pair.result || null;
307
426
  const issue = this.buildSpeedTestIssue(pair.name, pair.result);
308
427
  if (issue) issues.push(issue);
309
428
  }
310
- if (remote && typeof remote === 'object') {
311
- remote = {
312
- ...remote,
313
- speedTests: results
314
- };
315
- } else {
316
- remote = {
317
- type: 'speed-test',
318
- speedTests: results
319
- };
320
- }
429
+ remote = remote && typeof remote === 'object'
430
+ ? { ...remote, speedTests: results }
431
+ : { type: 'speed-test', speedTests: results };
321
432
  }
322
433
 
323
434
  const ok = issues.length === 0;
@@ -327,146 +438,24 @@ export function createCodexConfigMethods(options = {}) {
327
438
  issues,
328
439
  remote
329
440
  };
330
- if (ok) {
441
+ if (ok && !silent) {
331
442
  this.showMessage('检查通过', 'success');
332
443
  }
333
444
  } else {
334
445
  this.healthCheckResult = null;
335
- this.showMessage('检查失败', 'error');
446
+ if (!silent) {
447
+ this.showMessage('检查失败', 'error');
448
+ }
336
449
  }
337
450
  } catch (e) {
338
451
  this.healthCheckResult = null;
339
- this.showMessage('检查失败', 'error');
340
- } finally {
341
- if (shouldRunClaudeSpeedTests && this.configMode === 'claude') {
342
- try {
343
- const entries = Object.entries(this.claudeConfigs || {});
344
- await Promise.all(entries.map(([name, config]) => this.runClaudeSpeedTest(name, config)));
345
- } catch (e) {}
346
- }
347
- this.healthCheckLoading = false;
348
- }
349
- },
350
-
351
- buildDefaultHealthCheckPrompt() {
352
- return '请简短回复:连接正常。';
353
- },
354
-
355
- openHealthCheckDialog(options = {}) {
356
- const providerName = typeof options.providerName === 'string'
357
- ? options.providerName.trim()
358
- : '';
359
- const locked = !!options.locked && !!providerName;
360
- if (locked && providerName && providerName !== String(this.currentProvider || '').trim()) {
361
- if (typeof this.showMessage === 'function') {
362
- this.showMessage('请先切换到该提供商再进行健康聊天测试', 'info');
363
- }
364
- return;
365
- }
366
- const nextProvider = providerName
367
- || String(this.healthCheckDialogSelectedProvider || '').trim()
368
- || String(this.currentProvider || '').trim()
369
- || String(((this.displayProvidersList || [])[0] || {}).name || '').trim();
370
-
371
- this.showHealthCheckDialog = true;
372
- this.healthCheckDialogLockedProvider = locked ? nextProvider : '';
373
- this.healthCheckDialogSelectedProvider = nextProvider;
374
- this.healthCheckDialogPrompt = this.buildDefaultHealthCheckPrompt();
375
- this.healthCheckDialogMessages = [];
376
- this.healthCheckDialogLastResult = null;
377
- },
378
-
379
- closeHealthCheckDialog(options = {}) {
380
- if (this.healthCheckDialogSending && !options.force) {
381
- return;
382
- }
383
- this.showHealthCheckDialog = false;
384
- this.healthCheckDialogLockedProvider = '';
385
- this.healthCheckDialogSelectedProvider = '';
386
- this.healthCheckDialogPrompt = this.buildDefaultHealthCheckPrompt();
387
- this.healthCheckDialogMessages = [];
388
- this.healthCheckDialogLastResult = null;
389
- },
390
-
391
- async sendHealthCheckDialogMessage() {
392
- if (this.healthCheckDialogSending) {
393
- return;
394
- }
395
-
396
- const provider = String(
397
- this.healthCheckDialogLockedProvider || this.healthCheckDialogSelectedProvider || ''
398
- ).trim();
399
- const prompt = String(this.healthCheckDialogPrompt || '').trim();
400
- if (!provider) {
401
- this.showMessage('请先选择提供商', 'error');
402
- return;
403
- }
404
- if (!prompt) {
405
- this.showMessage('请输入消息内容', 'error');
406
- return;
407
- }
408
-
409
- this.healthCheckDialogMessages.push({
410
- id: `user-${Date.now()}`,
411
- role: 'user',
412
- text: prompt
413
- });
414
- this.healthCheckDialogSending = true;
415
- this.healthCheckDialogLastResult = null;
416
-
417
- try {
418
- const res = await api('provider-chat-check', {
419
- name: provider,
420
- prompt
421
- });
422
- this.healthCheckDialogLastResult = res;
423
-
424
- if (hasResponseError(res) || res.ok === false) {
425
- const message = getResponseMessage(res, '健康聊天测试失败');
426
- this.healthCheckDialogMessages.push({
427
- id: `assistant-${Date.now()}`,
428
- role: 'assistant',
429
- text: message,
430
- ok: false,
431
- status: Number.isFinite(res && res.status) ? res.status : 0,
432
- durationMs: Number.isFinite(res && res.durationMs) ? res.durationMs : 0,
433
- model: typeof (res && res.model) === 'string' ? res.model : '',
434
- rawPreview: typeof (res && res.rawPreview) === 'string' ? res.rawPreview : ''
435
- });
436
- this.showMessage(message, 'error');
437
- return;
452
+ if (!(options && options.silent)) {
453
+ this.showMessage('检查失败', 'error');
438
454
  }
439
-
440
- const reply = typeof res.reply === 'string' && res.reply.trim()
441
- ? res.reply.trim()
442
- : '已收到回复,但未解析到可展示文本。';
443
- this.healthCheckDialogMessages.push({
444
- id: `assistant-${Date.now()}`,
445
- role: 'assistant',
446
- text: reply,
447
- ok: true,
448
- status: Number.isFinite(res.status) ? res.status : 0,
449
- durationMs: Number.isFinite(res.durationMs) ? res.durationMs : 0,
450
- model: typeof res.model === 'string' ? res.model : '',
451
- rawPreview: typeof res.rawPreview === 'string' ? res.rawPreview : ''
452
- });
453
- this.healthCheckDialogPrompt = '';
454
- } catch (e) {
455
- const message = e && e.message ? e.message : '健康聊天测试失败';
456
- this.healthCheckDialogMessages.push({
457
- id: `assistant-${Date.now()}`,
458
- role: 'assistant',
459
- text: message,
460
- ok: false,
461
- status: 0,
462
- durationMs: 0,
463
- model: '',
464
- rawPreview: ''
465
- });
466
- this.healthCheckDialogLastResult = { ok: false, error: message };
467
- this.showMessage(message, 'error');
468
455
  } finally {
469
- this.healthCheckDialogSending = false;
456
+ this.healthCheckBatchTotal = this.healthCheckBatchTotal || 0;
457
+ this.healthCheckBatchDone = Math.min(this.healthCheckBatchDone || 0, this.healthCheckBatchTotal || 0);
458
+ this.healthCheckLoading = false;
470
459
  }
471
460
  },
472
461
 
@@ -156,6 +156,22 @@ export function createInstallMethods() {
156
156
 
157
157
  setInstallRegistryPreset(presetName) {
158
158
  this.installRegistryPreset = this.normalizeInstallRegistryPreset(presetName);
159
+ },
160
+
161
+ getInstallStatusTarget(targetId) {
162
+ const key = typeof targetId === 'string' ? targetId.trim() : '';
163
+ if (!key) return null;
164
+ const list = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : [];
165
+ return list.find((item) => item && item.id === key) || null;
166
+ },
167
+
168
+ isInstallTargetInstalled(targetId) {
169
+ const target = this.getInstallStatusTarget(targetId);
170
+ return !!(target && target.installed === true);
171
+ },
172
+
173
+ shouldShowCliInstallPlaceholder(targetId) {
174
+ return Array.isArray(this.installStatusTargets) && !this.isInstallTargetInstalled(targetId);
159
175
  }
160
176
  };
161
177
  }
@@ -4,6 +4,70 @@ export function createNavigationMethods(options = {}) {
4
4
  switchMainTabHelper,
5
5
  loadMoreSessionMessagesHelper
6
6
  } = options;
7
+ const NAV_STATE_STORAGE_KEY = 'codexmateNavState.v1';
8
+ const MAIN_TAB_SET = new Set([
9
+ 'dashboard',
10
+ 'config',
11
+ 'sessions',
12
+ 'usage',
13
+ 'orchestration',
14
+ 'market',
15
+ 'plugins',
16
+ 'docs',
17
+ 'settings'
18
+ ]);
19
+ const loadDoctorOverview = async (vm, options = {}) => {
20
+ if (!vm || typeof vm !== 'object') return false;
21
+ if (vm.__doctorLoading) return false;
22
+ const forceRefresh = !!(options && options.forceRefresh);
23
+ vm.__doctorLoading = true;
24
+ let ok = true;
25
+ try {
26
+ if (typeof vm.runHealthCheck === 'function') {
27
+ await vm.runHealthCheck({ doctor: true, silent: true, forceRefresh });
28
+ }
29
+ vm.__doctorLoadedOnce = true;
30
+ return true;
31
+ } catch (_) {
32
+ ok = false;
33
+ vm.__doctorLoadedOnce = true;
34
+ return false;
35
+ } finally {
36
+ vm.__doctorLoading = false;
37
+ if (!ok) {
38
+ vm.__doctorLoadedOnce = true;
39
+ }
40
+ }
41
+ };
42
+ const readNavState = () => {
43
+ if (typeof localStorage === 'undefined') return null;
44
+ let raw = '';
45
+ try {
46
+ raw = localStorage.getItem(NAV_STATE_STORAGE_KEY) || '';
47
+ } catch (_) {
48
+ raw = '';
49
+ }
50
+ if (!raw) return null;
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ return parsed && typeof parsed === 'object' ? parsed : null;
54
+ } catch (_) {
55
+ return null;
56
+ }
57
+ };
58
+ const persistNavState = (vm) => {
59
+ if (!vm || vm.__navStateRestoring) return;
60
+ if (typeof localStorage === 'undefined') return;
61
+ const mainTab = typeof vm.mainTab === 'string' ? vm.mainTab.trim().toLowerCase() : '';
62
+ const configMode = typeof vm.configMode === 'string' ? vm.configMode.trim().toLowerCase() : '';
63
+ const snapshot = {
64
+ mainTab: MAIN_TAB_SET.has(mainTab) ? mainTab : 'dashboard',
65
+ configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex'
66
+ };
67
+ try {
68
+ localStorage.setItem(NAV_STATE_STORAGE_KEY, JSON.stringify(snapshot));
69
+ } catch (_) {}
70
+ };
7
71
 
8
72
  return {
9
73
  switchConfigMode(mode) {
@@ -34,6 +98,7 @@ export function createNavigationMethods(options = {}) {
34
98
  this.scheduleAfterFrame(() => {
35
99
  this.clearMainTabSwitchIntent('config');
36
100
  });
101
+ persistNavState(this);
37
102
  return;
38
103
  }
39
104
  this.switchMainTab('config');
@@ -300,6 +365,9 @@ export function createNavigationMethods(options = {}) {
300
365
  if (targetTab === previousTab) {
301
366
  switchState.ticket += 1;
302
367
  switchState.pendingTarget = '';
368
+ if (targetTab === 'dashboard' && !this.__doctorLoadedOnce) {
369
+ void loadDoctorOverview(this);
370
+ }
303
371
  if (
304
372
  targetTab === 'sessions'
305
373
  && typeof this.prepareSessionTabRender === 'function'
@@ -324,6 +392,10 @@ export function createNavigationMethods(options = {}) {
324
392
  switchState.ticket += 1;
325
393
  switchState.pendingTarget = '';
326
394
  const result = switchMainTabHelper.call(this, targetTab);
395
+ persistNavState(this);
396
+ if (targetTab === 'dashboard') {
397
+ void loadDoctorOverview(this);
398
+ }
327
399
  this.scheduleAfterFrame(() => {
328
400
  this.clearMainTabSwitchIntent(normalizedTab);
329
401
  });
@@ -338,6 +410,10 @@ export function createNavigationMethods(options = {}) {
338
410
  const pendingTarget = liveState.pendingTarget || targetTab;
339
411
  liveState.pendingTarget = '';
340
412
  switchMainTabHelper.call(this, pendingTarget);
413
+ persistNavState(this);
414
+ if (pendingTarget === 'dashboard') {
415
+ void loadDoctorOverview(this);
416
+ }
341
417
  this.clearMainTabSwitchIntent(normalizedTab);
342
418
  });
343
419
  },
@@ -37,7 +37,12 @@ export function createRuntimeMethods(options = {}) {
37
37
  const silent = !!options.silent;
38
38
  this.speedLoading[name] = true;
39
39
  try {
40
- const res = await api('speed-test', { name });
40
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1000, Number(options.timeoutMs)) : 0;
41
+ const payload = { name };
42
+ if (timeoutMs) {
43
+ payload.timeoutMs = timeoutMs;
44
+ }
45
+ const res = await api('speed-test', payload);
41
46
  if (res.error) {
42
47
  this.speedResults[name] = { ok: false, error: res.error };
43
48
  if (!silent) {
@@ -66,6 +71,8 @@ export function createRuntimeMethods(options = {}) {
66
71
  async runClaudeSpeedTest(name, config) {
67
72
  if (!name || this.claudeSpeedLoading[name]) return null;
68
73
  const baseUrl = config && typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
74
+ const apiKey = config && typeof config.apiKey === 'string' ? config.apiKey.trim() : '';
75
+ const model = config && typeof config.model === 'string' ? config.model.trim() : '';
69
76
  this.claudeSpeedLoading[name] = true;
70
77
  try {
71
78
  if (!baseUrl) {
@@ -73,7 +80,22 @@ export function createRuntimeMethods(options = {}) {
73
80
  this.claudeSpeedResults[name] = res;
74
81
  return res;
75
82
  }
76
- const res = await api('speed-test', { url: baseUrl });
83
+ if (!apiKey) {
84
+ const res = { ok: false, error: 'Missing API key' };
85
+ this.claudeSpeedResults[name] = res;
86
+ return res;
87
+ }
88
+ if (!model) {
89
+ const res = { ok: false, error: 'Missing model' };
90
+ this.claudeSpeedResults[name] = res;
91
+ return res;
92
+ }
93
+ const res = await api('speed-test', {
94
+ kind: 'claude',
95
+ url: baseUrl,
96
+ apiKey,
97
+ model
98
+ });
77
99
  if (res.error) {
78
100
  this.claudeSpeedResults[name] = { ok: false, error: res.error };
79
101
  return { ok: false, error: res.error };