codexmate 0.0.23 → 0.0.25

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 (73) hide show
  1. package/README.md +32 -9
  2. package/README.zh.md +33 -9
  3. package/cli/auth-profiles.js +23 -7
  4. package/cli/builtin-proxy.js +35 -0
  5. package/cli/claude-proxy.js +24 -0
  6. package/cli/doctor-core.js +903 -0
  7. package/cli/import-skills-url.js +356 -0
  8. package/cli/openai-bridge.js +51 -4
  9. package/cli/session-usage.js +8 -2
  10. package/cli.js +1921 -399
  11. package/lib/automation.js +404 -0
  12. package/lib/cli-models-utils.js +0 -40
  13. package/lib/cli-network-utils.js +28 -2
  14. package/lib/cli-path-utils.js +21 -5
  15. package/lib/cli-sessions.js +32 -1
  16. package/lib/download-artifacts.js +17 -2
  17. package/lib/mcp-stdio.js +13 -0
  18. package/package.json +3 -3
  19. package/plugins/README.md +20 -0
  20. package/plugins/README.zh-CN.md +20 -0
  21. package/plugins/prompt-templates/comment-polish/index.mjs +25 -0
  22. package/plugins/prompt-templates/computed.mjs +253 -0
  23. package/plugins/prompt-templates/index.mjs +8 -0
  24. package/plugins/prompt-templates/manifest.mjs +15 -0
  25. package/plugins/prompt-templates/methods.mjs +619 -0
  26. package/plugins/prompt-templates/overview.mjs +90 -0
  27. package/plugins/prompt-templates/ownership.mjs +19 -0
  28. package/plugins/prompt-templates/rule-ack/index.mjs +21 -0
  29. package/plugins/prompt-templates/storage.mjs +64 -0
  30. package/plugins/registry.mjs +16 -0
  31. package/web-ui/app.js +21 -35
  32. package/web-ui/index.html +4 -3
  33. package/web-ui/logic.sessions.mjs +2 -2
  34. package/web-ui/modules/app.computed.dashboard.mjs +24 -22
  35. package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
  36. package/web-ui/modules/app.computed.session.mjs +17 -0
  37. package/web-ui/modules/app.methods.agents.mjs +91 -3
  38. package/web-ui/modules/app.methods.codex-config.mjs +153 -164
  39. package/web-ui/modules/app.methods.install.mjs +28 -0
  40. package/web-ui/modules/app.methods.navigation.mjs +34 -1
  41. package/web-ui/modules/app.methods.runtime.mjs +24 -2
  42. package/web-ui/modules/app.methods.session-actions.mjs +8 -1
  43. package/web-ui/modules/app.methods.session-browser.mjs +37 -6
  44. package/web-ui/modules/app.methods.session-trash.mjs +4 -2
  45. package/web-ui/modules/config-mode.computed.mjs +1 -3
  46. package/web-ui/modules/i18n.dict.mjs +2055 -0
  47. package/web-ui/modules/i18n.mjs +2 -1769
  48. package/web-ui/partials/index/layout-header.html +48 -34
  49. package/web-ui/partials/index/modal-config-template-agents.html +3 -4
  50. package/web-ui/partials/index/modal-health-check.html +33 -60
  51. package/web-ui/partials/index/panel-config-claude.html +35 -15
  52. package/web-ui/partials/index/panel-config-codex.html +47 -19
  53. package/web-ui/partials/index/panel-config-openclaw.html +8 -3
  54. package/web-ui/partials/index/panel-dashboard.html +186 -0
  55. package/web-ui/partials/index/panel-docs.html +1 -1
  56. package/web-ui/partials/index/panel-market.html +3 -0
  57. package/web-ui/partials/index/panel-orchestration.html +3 -0
  58. package/web-ui/partials/index/panel-plugins.html +16 -10
  59. package/web-ui/partials/index/panel-sessions.html +8 -3
  60. package/web-ui/partials/index/panel-settings.html +1 -1
  61. package/web-ui/partials/index/panel-usage.html +9 -1
  62. package/web-ui/res/logo-pack.webp +0 -0
  63. package/web-ui/styles/controls-forms.css +58 -4
  64. package/web-ui/styles/dashboard.css +274 -0
  65. package/web-ui/styles/layout-shell.css +3 -2
  66. package/web-ui/styles/responsive.css +0 -2
  67. package/web-ui/styles/sessions-list.css +5 -7
  68. package/web-ui/styles/sessions-toolbar-trash.css +4 -4
  69. package/web-ui/styles/sessions-usage.css +33 -0
  70. package/web-ui/styles.css +1 -0
  71. package/res/logo.png +0 -0
  72. /package/{res → web-ui/res}/json5.min.js +0 -0
  73. /package/{res → web-ui/res}/vue.global.prod.js +0 -0
@@ -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
 
@@ -104,6 +104,16 @@ export function createInstallMethods() {
104
104
  update: '',
105
105
  uninstall: ''
106
106
  },
107
+ codebuddy: {
108
+ install: '',
109
+ update: '',
110
+ uninstall: ''
111
+ },
112
+ gemini: {
113
+ install: '',
114
+ update: '',
115
+ uninstall: ''
116
+ },
107
117
  codex: {
108
118
  install: '',
109
119
  update: '',
@@ -114,6 +124,12 @@ export function createInstallMethods() {
114
124
  matrix.claude.install = 'pnpm add -g @anthropic-ai/claude-code';
115
125
  matrix.claude.update = 'pnpm up -g @anthropic-ai/claude-code';
116
126
  matrix.claude.uninstall = 'pnpm remove -g @anthropic-ai/claude-code';
127
+ matrix.codebuddy.install = 'pnpm add -g @tencent-ai/codebuddy-code';
128
+ matrix.codebuddy.update = 'pnpm up -g @tencent-ai/codebuddy-code';
129
+ matrix.codebuddy.uninstall = 'pnpm remove -g @tencent-ai/codebuddy-code';
130
+ matrix.gemini.install = 'pnpm add -g @google/gemini-cli';
131
+ matrix.gemini.update = 'pnpm up -g @google/gemini-cli';
132
+ matrix.gemini.uninstall = 'pnpm remove -g @google/gemini-cli';
117
133
  matrix.codex.install = `pnpm add -g ${codexInstallPackage}`;
118
134
  matrix.codex.update = `pnpm up -g ${codexPackage}`;
119
135
  matrix.codex.uninstall = `pnpm remove -g ${codexPackage}`;
@@ -123,6 +139,12 @@ export function createInstallMethods() {
123
139
  matrix.claude.install = 'bun add -g @anthropic-ai/claude-code';
124
140
  matrix.claude.update = 'bun update -g @anthropic-ai/claude-code';
125
141
  matrix.claude.uninstall = 'bun remove -g @anthropic-ai/claude-code';
142
+ matrix.codebuddy.install = 'bun add -g @tencent-ai/codebuddy-code';
143
+ matrix.codebuddy.update = 'bun update -g @tencent-ai/codebuddy-code';
144
+ matrix.codebuddy.uninstall = 'bun remove -g @tencent-ai/codebuddy-code';
145
+ matrix.gemini.install = 'bun add -g @google/gemini-cli';
146
+ matrix.gemini.update = 'bun update -g @google/gemini-cli';
147
+ matrix.gemini.uninstall = 'bun remove -g @google/gemini-cli';
126
148
  matrix.codex.install = `bun add -g ${codexInstallPackage}`;
127
149
  matrix.codex.update = `bun update -g ${codexPackage}`;
128
150
  matrix.codex.uninstall = `bun remove -g ${codexPackage}`;
@@ -131,6 +153,12 @@ export function createInstallMethods() {
131
153
  matrix.claude.install = 'npm install -g @anthropic-ai/claude-code';
132
154
  matrix.claude.update = 'npm update -g @anthropic-ai/claude-code';
133
155
  matrix.claude.uninstall = 'npm uninstall -g @anthropic-ai/claude-code';
156
+ matrix.codebuddy.install = 'npm install -g @tencent-ai/codebuddy-code';
157
+ matrix.codebuddy.update = 'npm update -g @tencent-ai/codebuddy-code';
158
+ matrix.codebuddy.uninstall = 'npm uninstall -g @tencent-ai/codebuddy-code';
159
+ matrix.gemini.install = 'npm install -g @google/gemini-cli';
160
+ matrix.gemini.update = 'npm update -g @google/gemini-cli';
161
+ matrix.gemini.uninstall = 'npm uninstall -g @google/gemini-cli';
134
162
  matrix.codex.install = `npm install -g ${codexInstallPackage}`;
135
163
  matrix.codex.update = platform === 'termux'
136
164
  ? `npm install -g ${codexInstallPackage}`
@@ -6,6 +6,7 @@ export function createNavigationMethods(options = {}) {
6
6
  } = options;
7
7
  const NAV_STATE_STORAGE_KEY = 'codexmateNavState.v1';
8
8
  const MAIN_TAB_SET = new Set([
9
+ 'dashboard',
9
10
  'config',
10
11
  'sessions',
11
12
  'usage',
@@ -15,6 +16,29 @@ export function createNavigationMethods(options = {}) {
15
16
  'docs',
16
17
  'settings'
17
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
+ };
18
42
  const readNavState = () => {
19
43
  if (typeof localStorage === 'undefined') return null;
20
44
  let raw = '';
@@ -37,7 +61,7 @@ export function createNavigationMethods(options = {}) {
37
61
  const mainTab = typeof vm.mainTab === 'string' ? vm.mainTab.trim().toLowerCase() : '';
38
62
  const configMode = typeof vm.configMode === 'string' ? vm.configMode.trim().toLowerCase() : '';
39
63
  const snapshot = {
40
- mainTab: MAIN_TAB_SET.has(mainTab) ? mainTab : 'docs',
64
+ mainTab: MAIN_TAB_SET.has(mainTab) ? mainTab : 'dashboard',
41
65
  configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex'
42
66
  };
43
67
  try {
@@ -341,6 +365,9 @@ export function createNavigationMethods(options = {}) {
341
365
  if (targetTab === previousTab) {
342
366
  switchState.ticket += 1;
343
367
  switchState.pendingTarget = '';
368
+ if (targetTab === 'dashboard' && !this.__doctorLoadedOnce) {
369
+ void loadDoctorOverview(this);
370
+ }
344
371
  if (
345
372
  targetTab === 'sessions'
346
373
  && typeof this.prepareSessionTabRender === 'function'
@@ -366,6 +393,9 @@ export function createNavigationMethods(options = {}) {
366
393
  switchState.pendingTarget = '';
367
394
  const result = switchMainTabHelper.call(this, targetTab);
368
395
  persistNavState(this);
396
+ if (targetTab === 'dashboard') {
397
+ void loadDoctorOverview(this);
398
+ }
369
399
  this.scheduleAfterFrame(() => {
370
400
  this.clearMainTabSwitchIntent(normalizedTab);
371
401
  });
@@ -381,6 +411,9 @@ export function createNavigationMethods(options = {}) {
381
411
  liveState.pendingTarget = '';
382
412
  switchMainTabHelper.call(this, pendingTarget);
383
413
  persistNavState(this);
414
+ if (pendingTarget === 'dashboard') {
415
+ void loadDoctorOverview(this);
416
+ }
384
417
  this.clearMainTabSwitchIntent(normalizedTab);
385
418
  });
386
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 };
@@ -117,7 +117,7 @@ export function createSessionActionMethods(options = {}) {
117
117
  if (!session) return false;
118
118
  const source = String(session.source || '').trim().toLowerCase();
119
119
  const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
120
- return source === 'codex' && !!sessionId;
120
+ return (source === 'codex' || source === 'codebuddy' || source === 'gemini') && !!sessionId;
121
121
  },
122
122
 
123
123
  isCloneAvailable(session) {
@@ -138,8 +138,15 @@ export function createSessionActionMethods(options = {}) {
138
138
  },
139
139
 
140
140
  buildResumeCommand(session) {
141
+ const source = session && session.source ? String(session.source).trim().toLowerCase() : '';
141
142
  const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
142
143
  const arg = this.quoteResumeArg(sessionId);
144
+ if (source === 'codebuddy') {
145
+ return `codebuddy -r ${arg}`;
146
+ }
147
+ if (source === 'gemini') {
148
+ return `gemini -r ${arg}`;
149
+ }
143
150
  if (this.sessionResumeWithYolo) {
144
151
  return `codex --yolo resume ${arg}`;
145
152
  }
@@ -127,7 +127,9 @@ export function createSessionBrowserMethods(options = {}) {
127
127
  },
128
128
 
129
129
  syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
130
- const targetSource = source === 'claude' ? 'claude' : (source === 'all' ? 'all' : 'codex');
130
+ const targetSource = source === 'claude'
131
+ ? 'claude'
132
+ : (source === 'gemini' ? 'gemini' : (source === 'all' ? 'all' : 'codex'));
131
133
  const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
132
134
  ? this.sessionPathOptionsMap[targetSource]
133
135
  : [];
@@ -142,7 +144,9 @@ export function createSessionBrowserMethods(options = {}) {
142
144
  },
143
145
 
144
146
  refreshSessionPathOptions(source) {
145
- const targetSource = source === 'claude' ? 'claude' : (source === 'all' ? 'all' : 'codex');
147
+ const targetSource = source === 'claude'
148
+ ? 'claude'
149
+ : (source === 'gemini' ? 'gemini' : (source === 'all' ? 'all' : 'codex'));
146
150
  const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
147
151
  ? [...this.sessionPathOptionsMap[targetSource]]
148
152
  : [];
@@ -164,7 +168,9 @@ export function createSessionBrowserMethods(options = {}) {
164
168
  },
165
169
 
166
170
  async loadSessionPathOptions(options = {}) {
167
- const source = options.source === 'claude' ? 'claude' : (options.source === 'all' ? 'all' : 'codex');
171
+ const source = options.source === 'claude'
172
+ ? 'claude'
173
+ : (options.source === 'gemini' ? 'gemini' : (options.source === 'all' ? 'all' : 'codex'));
168
174
  const forceRefresh = !!options.forceRefresh;
169
175
  const loaded = !!this.sessionPathOptionsLoadedMap[source];
170
176
  if (!forceRefresh && loaded) {
@@ -252,6 +258,9 @@ export function createSessionBrowserMethods(options = {}) {
252
258
  const urlState = readSessionsFilterUrlState();
253
259
  if (urlState) {
254
260
  applySessionsFilterUrlState(this, urlState);
261
+ if (this.mainTab === 'sessions' && typeof this.loadSessions === 'function') {
262
+ void this.loadSessions();
263
+ }
255
264
  return;
256
265
  }
257
266
  const sourceCache = localStorage.getItem('codexmateSessionFilterSource');
@@ -266,6 +275,16 @@ export function createSessionBrowserMethods(options = {}) {
266
275
  this.sessionRoleFilter = normalizeSessionRoleFilter(roleCache);
267
276
  this.sessionTimePreset = normalizeSessionTimePreset(timeCache);
268
277
  this.refreshSessionPathOptions(this.sessionFilterSource);
278
+ if (this.mainTab === 'sessions' && typeof this.loadSessions === 'function') {
279
+ const shouldReload = cached.source !== 'all'
280
+ || !!cached.pathFilter
281
+ || !!(this.sessionQuery && isSessionQueryEnabled(cached.source))
282
+ || (this.sessionRoleFilter && this.sessionRoleFilter !== 'all')
283
+ || (this.sessionTimePreset && this.sessionTimePreset !== 'all');
284
+ if (shouldReload) {
285
+ void this.loadSessions();
286
+ }
287
+ }
269
288
  },
270
289
 
271
290
  persistSessionFilterCache() {
@@ -412,7 +431,12 @@ export function createSessionBrowserMethods(options = {}) {
412
431
  this.persistSessionPinnedMap();
413
432
  },
414
433
 
415
- async onSessionSourceChange() {
434
+ async onSessionSourceChange(event) {
435
+ const rawValue = event && event.target && typeof event.target.value === 'string'
436
+ ? event.target.value
437
+ : this.sessionFilterSource;
438
+ const cached = buildSessionFilterCacheState(rawValue, this.sessionPathFilter);
439
+ this.sessionFilterSource = cached.source;
416
440
  this.refreshSessionPathOptions(this.sessionFilterSource);
417
441
  this.persistSessionFilterCache();
418
442
  syncSessionsFilterUrl(this);
@@ -442,12 +466,19 @@ export function createSessionBrowserMethods(options = {}) {
442
466
  await this.onSessionSourceChange();
443
467
  },
444
468
 
445
- copySessionsFilterShareUrl() {
469
+ async copySessionsFilterShareUrl() {
446
470
  const url = buildSessionsFilterShareUrl(this);
447
471
  if (!url) {
448
- this.showMessage('无法生成链接', 'error');
472
+ this.showMessage(typeof this.t === 'function' ? this.t('sessions.filters.urlBuildFail') : 'Failed to build link', 'error');
449
473
  return;
450
474
  }
475
+ try {
476
+ if (navigator.clipboard && window.isSecureContext) {
477
+ await navigator.clipboard.writeText(url);
478
+ this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
479
+ return;
480
+ }
481
+ } catch (_) {}
451
482
  const ok = typeof this.fallbackCopyText === 'function' ? this.fallbackCopyText(url) : false;
452
483
  if (ok) {
453
484
  this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');