codexmate 0.0.7 → 0.0.8

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.
package/web-ui.html CHANGED
@@ -13,10 +13,10 @@
13
13
  设计系统 - Design Tokens
14
14
  ============================================ */
15
15
  :root {
16
- /* 色彩系统 */
17
- --color-brand: #C95E4B;
18
- --color-brand-dark: #A94637;
19
- --color-brand-light: rgba(201, 94, 75, 0.14);
16
+ /* 色彩系统:去除杂纹,强调干净留白与温柔橙红 */
17
+ --color-brand: #D0583A;
18
+ --color-brand-dark: #B8442B;
19
+ --color-brand-light: rgba(208, 88, 58, 0.14);
20
20
  --color-brand-subtle: rgba(201, 94, 75, 0.2);
21
21
 
22
22
  --color-bg: #F6EFE6;
@@ -32,8 +32,8 @@
32
32
  --color-border-soft: rgba(216, 201, 184, 0.45);
33
33
  --color-border-strong: rgba(216, 201, 184, 0.8);
34
34
 
35
- --color-success: #4E8B66;
36
- --color-error: #C1483B;
35
+ --color-success: #4B8B6A;
36
+ --color-error: #C44536;
37
37
 
38
38
  --bg-warm-gradient:
39
39
  radial-gradient(circle at 16% 10%, rgba(201, 94, 75, 0.18), transparent 45%),
@@ -133,6 +133,9 @@
133
133
  overflow-x: hidden;
134
134
  }
135
135
 
136
+ /* ============================================
137
+ 容器
138
+ ============================================ */
136
139
  body::before {
137
140
  content: "";
138
141
  position: fixed;
@@ -146,18 +149,230 @@
146
149
  z-index: 0;
147
150
  }
148
151
 
152
+ /* 背景网格 */
153
+ body::after {
154
+ content: "";
155
+ position: fixed;
156
+ inset: 0;
157
+ background-image:
158
+ linear-gradient(90deg, rgba(255, 255, 255, 0.15) 1px, transparent 1px),
159
+ linear-gradient(0deg, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
160
+ background-size: 120px 120px;
161
+ opacity: 0.5;
162
+ pointer-events: none;
163
+ z-index: 0;
164
+ }
165
+
149
166
  /* ============================================
150
167
  容器
151
168
  ============================================ */
152
169
  .container {
153
170
  width: 100%;
154
- max-width: 1160px;
171
+ max-width: 1200px;
155
172
  margin: 0 auto;
156
- padding-bottom: var(--spacing-lg);
173
+ padding: var(--spacing-md) var(--spacing-sm) var(--spacing-lg);
157
174
  position: relative;
158
175
  z-index: 1;
159
176
  }
160
177
 
178
+ /* ============================================
179
+ 布局:单列居中
180
+ ============================================ */
181
+ .app-shell {
182
+ display: grid;
183
+ grid-template-columns: 1fr;
184
+ gap: var(--spacing-md);
185
+ align-items: flex-start;
186
+ }
187
+
188
+ .side-nav {
189
+ display: none;
190
+ }
191
+
192
+ .brand-block {
193
+ margin-bottom: var(--spacing-md);
194
+ }
195
+
196
+ .brand-title {
197
+ font-size: 30px;
198
+ line-height: 1.05;
199
+ font-family: var(--font-family-display);
200
+ color: var(--color-text-primary);
201
+ letter-spacing: -0.02em;
202
+ }
203
+
204
+ .brand-title .accent {
205
+ color: var(--color-brand);
206
+ }
207
+
208
+ .brand-subtitle {
209
+ margin-top: 8px;
210
+ font-size: var(--font-size-secondary);
211
+ color: var(--color-text-tertiary);
212
+ line-height: 1.45;
213
+ }
214
+
215
+ .main-tabs {
216
+ display: flex;
217
+ gap: 10px;
218
+ }
219
+
220
+ .main-tab-btn {
221
+ flex: 1;
222
+ text-align: center;
223
+ border: 1px solid rgba(216, 201, 184, 0.55);
224
+ background: rgba(255, 255, 255, 0.95);
225
+ border-radius: var(--radius-lg);
226
+ padding: 12px 14px;
227
+ cursor: pointer;
228
+ color: var(--color-text-secondary);
229
+ font-size: var(--font-size-body);
230
+ font-weight: var(--font-weight-secondary);
231
+ box-shadow: var(--shadow-subtle);
232
+ transition: all var(--transition-normal) var(--ease-spring);
233
+ }
234
+
235
+ .main-tab-btn:hover {
236
+ border-color: var(--color-brand);
237
+ color: var(--color-text-primary);
238
+ transform: translateY(-1px);
239
+ }
240
+
241
+ .main-tab-btn.active {
242
+ border-color: var(--color-brand);
243
+ box-shadow: 0 10px 24px rgba(27, 23, 20, 0.08);
244
+ color: var(--color-text-primary);
245
+ background: linear-gradient(135deg, rgba(201, 94, 75, 0.12), rgba(255, 255, 255, 0.95));
246
+ }
247
+
248
+ .main-panel {
249
+ min-width: 0;
250
+ background: rgba(255, 255, 255, 0.9);
251
+ border: 1px solid rgba(255, 255, 255, 0.65);
252
+ border-radius: 18px;
253
+ box-shadow: 0 12px 30px rgba(27, 23, 20, 0.08);
254
+ padding: var(--spacing-md) var(--spacing-lg);
255
+ backdrop-filter: blur(8px);
256
+ }
257
+
258
+ .panel-header {
259
+ margin-bottom: 12px;
260
+ text-align: left;
261
+ }
262
+
263
+ .hero {
264
+ margin-bottom: var(--spacing-sm);
265
+ }
266
+
267
+ .hero-title {
268
+ font-size: 48px;
269
+ line-height: 1.05;
270
+ font-family: var(--font-family-display);
271
+ color: var(--color-text-primary);
272
+ letter-spacing: -0.02em;
273
+ }
274
+
275
+ .hero-title .accent {
276
+ color: var(--color-brand);
277
+ }
278
+
279
+ .hero-subtitle {
280
+ margin-top: 8px;
281
+ font-size: var(--font-size-body);
282
+ color: var(--color-text-tertiary);
283
+ line-height: 1.5;
284
+ }
285
+
286
+ .top-tabs {
287
+ margin: 14px 0 18px;
288
+ background: rgba(255, 255, 255, 0.92);
289
+ border: 1px solid rgba(255, 255, 255, 0.7);
290
+ border-radius: 14px;
291
+ padding: 6px;
292
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.06);
293
+ display: grid;
294
+ grid-template-columns: repeat(4, 1fr);
295
+ gap: 8px;
296
+ backdrop-filter: blur(6px);
297
+ }
298
+
299
+ .top-tab {
300
+ border: 1px solid rgba(216, 201, 184, 0.55);
301
+ border-radius: 12px;
302
+ background: rgba(255, 255, 255, 0.96);
303
+ padding: 11px 10px;
304
+ font-size: var(--font-size-body);
305
+ color: var(--color-text-secondary);
306
+ text-align: center;
307
+ cursor: pointer;
308
+ transition: all var(--transition-normal) var(--ease-spring);
309
+ box-shadow: var(--shadow-subtle);
310
+ }
311
+
312
+ .top-tab:hover {
313
+ border-color: var(--color-brand);
314
+ color: var(--color-text-primary);
315
+ transform: translateY(-1px);
316
+ }
317
+
318
+ .top-tab.active {
319
+ border-color: var(--color-brand);
320
+ color: var(--color-text-primary);
321
+ background: linear-gradient(135deg, rgba(201, 94, 75, 0.12), rgba(255, 255, 255, 0.95));
322
+ box-shadow: 0 10px 24px rgba(27, 23, 20, 0.08);
323
+ }
324
+
325
+ .config-subtabs {
326
+ display: flex;
327
+ gap: 8px;
328
+ margin-bottom: 16px;
329
+ padding: 6px;
330
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.7));
331
+ border-radius: var(--radius-lg);
332
+ border: 1px solid rgba(255, 255, 255, 0.7);
333
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.05);
334
+ }
335
+
336
+ .config-subtab {
337
+ border: 1px solid var(--color-border-soft);
338
+ border-radius: var(--radius-lg);
339
+ padding: 10px 14px;
340
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 250, 245, 0.9));
341
+ color: var(--color-text-secondary);
342
+ cursor: pointer;
343
+ font-size: var(--font-size-body);
344
+ font-weight: var(--font-weight-secondary);
345
+ transition: all var(--transition-normal) var(--ease-spring);
346
+ box-shadow: var(--shadow-subtle);
347
+ }
348
+
349
+ .config-subtab:hover {
350
+ border-color: var(--color-border-strong);
351
+ color: var(--color-text-primary);
352
+ }
353
+
354
+ .config-subtab.active {
355
+ border-color: var(--color-brand);
356
+ color: var(--color-text-primary);
357
+ background: linear-gradient(135deg, rgba(201, 94, 75, 0.18), rgba(255, 255, 255, 0.95));
358
+ box-shadow: var(--shadow-card);
359
+ }
360
+
361
+ .content-wrapper {
362
+ background: rgba(255, 255, 255, 0.9);
363
+ border: 1px solid rgba(255, 255, 255, 0.7);
364
+ border-radius: var(--radius-lg);
365
+ box-shadow: var(--shadow-card);
366
+ padding: var(--spacing-md);
367
+ }
368
+
369
+ .mode-content {
370
+ border-radius: var(--radius-lg);
371
+ background: rgba(255, 255, 255, 0.85);
372
+ box-shadow: var(--shadow-subtle);
373
+ padding: var(--spacing-sm);
374
+ }
375
+
161
376
  /* ============================================
162
377
  主标题
163
378
  ============================================ */
@@ -195,14 +410,14 @@
195
410
  ============================================ */
196
411
  .segmented-control {
197
412
  display: flex;
198
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0.55) 100%);
199
- border-radius: var(--radius-lg);
200
- padding: 5px;
201
- margin-bottom: 16px;
413
+ background: rgba(255, 255, 255, 0.92);
414
+ border-radius: var(--radius-xl);
415
+ padding: 6px;
416
+ margin-bottom: 20px;
202
417
  position: relative;
203
- box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.08);
418
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.06);
204
419
  border: 1px solid rgba(255, 255, 255, 0.7);
205
- backdrop-filter: blur(10px);
420
+ backdrop-filter: blur(6px);
206
421
  }
207
422
 
208
423
  .segment {
@@ -245,7 +460,7 @@
245
460
  卡片
246
461
  ============================================ */
247
462
  .card {
248
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
463
+ background: linear-gradient(180deg, #fffdf9 0%, #fff8f2 100%);
249
464
  border-radius: var(--radius-lg);
250
465
  padding: var(--spacing-sm);
251
466
  display: flex;
@@ -256,17 +471,17 @@
256
471
  transform var(--transition-normal) var(--ease-spring),
257
472
  box-shadow var(--transition-normal) var(--ease-spring),
258
473
  background-color var(--transition-fast) var(--ease-smooth);
259
- box-shadow: var(--shadow-card);
474
+ box-shadow: 0 10px 24px rgba(27, 23, 20, 0.08);
260
475
  user-select: none;
261
476
  will-change: transform;
262
- border: 1px solid var(--color-border-soft);
477
+ border: 1px solid rgba(216, 201, 184, 0.55);
263
478
  position: relative;
264
479
  overflow: hidden;
265
480
  }
266
481
 
267
482
  .card:hover {
268
483
  transform: translateY(-2px) scale(1.005);
269
- box-shadow: var(--shadow-float);
484
+ box-shadow: 0 16px 32px rgba(27, 23, 20, 0.1);
270
485
  }
271
486
 
272
487
  .card::before,
@@ -393,6 +608,11 @@
393
608
  transform: translateX(0);
394
609
  }
395
610
 
611
+ .mode-cards .card-actions {
612
+ opacity: 1;
613
+ transform: translateX(0);
614
+ }
615
+
396
616
  .card-action-btn {
397
617
  width: 28px;
398
618
  height: 28px;
@@ -524,54 +744,6 @@
524
744
  gap: var(--spacing-xs);
525
745
  }
526
746
 
527
- .recent-list {
528
- display: flex;
529
- flex-wrap: wrap;
530
- gap: 8px;
531
- margin-top: 8px;
532
- }
533
-
534
- .recent-item {
535
- border: 1px solid var(--color-border-soft);
536
- background: var(--color-surface-elevated);
537
- border-radius: var(--radius-md);
538
- padding: 10px 12px;
539
- min-width: 170px;
540
- text-align: left;
541
- cursor: pointer;
542
- transition: all var(--transition-fast) var(--ease-spring);
543
- box-shadow: 0 2px 6px rgba(27, 23, 20, 0.06);
544
- }
545
-
546
- .recent-item:hover {
547
- transform: translateY(-1px);
548
- box-shadow: 0 4px 12px rgba(27, 23, 20, 0.12);
549
- border-color: var(--color-border);
550
- }
551
-
552
- .recent-item:disabled {
553
- opacity: 0.6;
554
- cursor: not-allowed;
555
- }
556
-
557
- .recent-provider {
558
- font-size: var(--font-size-body);
559
- font-weight: var(--font-weight-secondary);
560
- color: var(--color-text-primary);
561
- margin-bottom: 4px;
562
- }
563
-
564
- .recent-model {
565
- font-size: var(--font-size-caption);
566
- color: var(--color-text-tertiary);
567
- line-height: 1.4;
568
- }
569
-
570
- .recent-empty {
571
- font-size: var(--font-size-caption);
572
- color: var(--color-text-tertiary);
573
- }
574
-
575
747
  .health-report {
576
748
  margin-top: 10px;
577
749
  padding: 10px 12px;
@@ -657,7 +829,7 @@
657
829
  outline: none;
658
830
  cursor: pointer;
659
831
  appearance: none;
660
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%235C5046' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
832
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23505A66' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
661
833
  background-repeat: no-repeat;
662
834
  background-position: right 14px center;
663
835
  background-size: 12px;
@@ -807,6 +979,32 @@
807
979
  justify-content: flex-end;
808
980
  }
809
981
 
982
+ .session-toolbar-footer {
983
+ display: flex;
984
+ align-items: center;
985
+ justify-content: flex-end;
986
+ gap: var(--spacing-xs);
987
+ margin-top: -2px;
988
+ padding-top: 6px;
989
+ margin-bottom: 12px;
990
+ border-top: 1px dashed var(--color-border-soft);
991
+ }
992
+
993
+ .session-toolbar-footer .quick-option {
994
+ margin: 0;
995
+ padding: 6px 10px;
996
+ border-radius: var(--radius-sm);
997
+ border: 1px solid var(--color-border-soft);
998
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 100%);
999
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
1000
+ transition: all var(--transition-fast) var(--ease-spring);
1001
+ line-height: 1.2;
1002
+ }
1003
+
1004
+ .session-toolbar-footer .quick-option:hover {
1005
+ border-color: var(--color-border-strong);
1006
+ }
1007
+
810
1008
  .session-source-select,
811
1009
  .session-path-select,
812
1010
  .session-query-input,
@@ -906,37 +1104,10 @@
906
1104
  margin-left: auto;
907
1105
  }
908
1106
 
909
- .btn-session-export {
910
- border: 1px solid var(--color-border-soft);
911
- border-radius: var(--radius-sm);
912
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
913
- color: var(--color-text-secondary);
914
- padding: 8px 12px;
915
- font-size: var(--font-size-secondary);
916
- font-weight: var(--font-weight-secondary);
917
- cursor: pointer;
918
- transition: all var(--transition-fast) var(--ease-spring);
919
- white-space: nowrap;
920
- box-shadow: var(--shadow-subtle);
921
- letter-spacing: -0.01em;
922
- }
923
-
924
- .btn-session-open {
925
- border: 1px solid var(--color-border-soft);
926
- border-radius: var(--radius-sm);
927
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
928
- color: var(--color-text-secondary);
929
- padding: 8px 12px;
930
- font-size: var(--font-size-secondary);
931
- font-weight: var(--font-weight-secondary);
932
- cursor: pointer;
933
- transition: all var(--transition-fast) var(--ease-spring);
934
- white-space: nowrap;
935
- box-shadow: var(--shadow-subtle);
936
- letter-spacing: -0.01em;
937
- }
938
-
939
- .btn-session-clone {
1107
+ .btn-session-export,
1108
+ .btn-session-open,
1109
+ .btn-session-clone,
1110
+ .btn-session-refresh {
940
1111
  border: 1px solid var(--color-border-soft);
941
1112
  border-radius: var(--radius-sm);
942
1113
  background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
@@ -952,7 +1123,7 @@
952
1123
  }
953
1124
 
954
1125
  .btn-session-delete {
955
- border: 1px solid rgba(189, 70, 68, 0.4);
1126
+ border: 1px solid rgba(189, 70, 68, 0.45);
956
1127
  border-radius: var(--radius-sm);
957
1128
  background: linear-gradient(to bottom, rgba(255, 245, 245, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%);
958
1129
  color: #b74545;
@@ -966,21 +1137,6 @@
966
1137
  letter-spacing: -0.01em;
967
1138
  }
968
1139
 
969
- .btn-session-refresh {
970
- border: 1px solid var(--color-border-soft);
971
- border-radius: var(--radius-sm);
972
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
973
- color: var(--color-text-secondary);
974
- padding: 8px 12px;
975
- font-size: var(--font-size-secondary);
976
- font-weight: var(--font-weight-secondary);
977
- cursor: pointer;
978
- transition: all var(--transition-fast) var(--ease-spring);
979
- white-space: nowrap;
980
- box-shadow: var(--shadow-subtle);
981
- letter-spacing: -0.01em;
982
- }
983
-
984
1140
  .btn-session-refresh:hover {
985
1141
  border-color: var(--color-brand);
986
1142
  color: var(--color-brand);
@@ -1062,38 +1218,38 @@
1062
1218
  min-height: 520px;
1063
1219
  }
1064
1220
 
1065
- .session-layout.session-standalone {
1066
- grid-template-columns: minmax(0, 1fr);
1067
- }
1068
-
1069
- .session-standalone-page {
1070
- max-width: 960px;
1071
- margin: 0 auto;
1072
- padding: var(--spacing-sm) 0;
1073
- }
1074
-
1075
- .session-standalone-title {
1076
- font-size: var(--font-size-title);
1077
- font-weight: var(--font-weight-title);
1078
- color: var(--color-text-primary);
1079
- margin-bottom: var(--spacing-sm);
1080
- letter-spacing: -0.01em;
1081
- }
1082
-
1083
- .session-standalone-text {
1084
- white-space: pre-wrap;
1085
- font-family: var(--font-family-body);
1086
- font-size: var(--font-size-body);
1087
- line-height: 1.7;
1088
- color: var(--color-text-primary);
1089
- word-break: break-word;
1090
- }
1091
-
1092
- .session-list {
1093
- display: flex;
1094
- flex-direction: column;
1095
- gap: var(--spacing-xs);
1096
- position: sticky;
1221
+ .session-layout.session-standalone {
1222
+ grid-template-columns: minmax(0, 1fr);
1223
+ }
1224
+
1225
+ .session-standalone-page {
1226
+ max-width: 960px;
1227
+ margin: 0 auto;
1228
+ padding: var(--spacing-sm) 0;
1229
+ }
1230
+
1231
+ .session-standalone-title {
1232
+ font-size: var(--font-size-title);
1233
+ font-weight: var(--font-weight-title);
1234
+ color: var(--color-text-primary);
1235
+ margin-bottom: var(--spacing-sm);
1236
+ letter-spacing: -0.01em;
1237
+ }
1238
+
1239
+ .session-standalone-text {
1240
+ white-space: pre-wrap;
1241
+ font-family: var(--font-family-body);
1242
+ font-size: var(--font-size-body);
1243
+ line-height: 1.7;
1244
+ color: var(--color-text-primary);
1245
+ word-break: break-word;
1246
+ }
1247
+
1248
+ .session-list {
1249
+ display: flex;
1250
+ flex-direction: column;
1251
+ gap: var(--spacing-xs);
1252
+ position: sticky;
1097
1253
  top: 12px;
1098
1254
  height: 100%;
1099
1255
  max-height: none;
@@ -1207,21 +1363,6 @@
1207
1363
  transition: all var(--transition-fast) var(--ease-spring);
1208
1364
  }
1209
1365
 
1210
- .session-item-delete {
1211
- border: 1px solid rgba(189, 70, 68, 0.35);
1212
- background: rgba(189, 70, 68, 0.08);
1213
- color: #b74545;
1214
- width: 28px;
1215
- height: 28px;
1216
- border-radius: 8px;
1217
- display: inline-flex;
1218
- align-items: center;
1219
- justify-content: center;
1220
- cursor: pointer;
1221
- flex-shrink: 0;
1222
- transition: all var(--transition-fast) var(--ease-spring);
1223
- }
1224
-
1225
1366
  .session-item-copy:hover {
1226
1367
  border-color: rgba(70, 86, 110, 0.7);
1227
1368
  background: rgba(70, 86, 110, 0.16);
@@ -1229,35 +1370,17 @@
1229
1370
  transform: translateY(-1px);
1230
1371
  }
1231
1372
 
1232
- .session-item-delete:hover {
1233
- border-color: rgba(189, 70, 68, 0.7);
1234
- background: rgba(189, 70, 68, 0.18);
1235
- color: #9f3b3b;
1236
- transform: translateY(-1px);
1237
- }
1238
-
1239
1373
  .session-item-copy:disabled {
1240
1374
  opacity: 0.5;
1241
1375
  cursor: not-allowed;
1242
1376
  transform: none;
1243
1377
  }
1244
1378
 
1245
- .session-item-delete:disabled {
1246
- opacity: 0.5;
1247
- cursor: not-allowed;
1248
- transform: none;
1249
- }
1250
-
1251
1379
  .session-item-copy svg {
1252
1380
  width: 16px;
1253
1381
  height: 16px;
1254
1382
  }
1255
1383
 
1256
- .session-item-delete svg {
1257
- width: 16px;
1258
- height: 16px;
1259
- }
1260
-
1261
1384
  .session-item-sub {
1262
1385
  font-size: var(--font-size-caption);
1263
1386
  color: var(--color-text-tertiary);
@@ -1510,6 +1633,10 @@
1510
1633
  justify-content: flex-start;
1511
1634
  }
1512
1635
 
1636
+ .session-toolbar-footer {
1637
+ justify-content: flex-start;
1638
+ }
1639
+
1513
1640
  .session-list {
1514
1641
  position: static;
1515
1642
  max-height: 300px;
@@ -2221,6 +2348,19 @@
2221
2348
  outline-offset: 2px;
2222
2349
  }
2223
2350
 
2351
+ @media (max-width: 960px) {
2352
+ .container {
2353
+ padding: var(--spacing-sm);
2354
+ }
2355
+ .main-panel {
2356
+ padding: var(--spacing-sm) var(--spacing-sm);
2357
+ border-radius: 14px;
2358
+ }
2359
+ .top-tabs {
2360
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2361
+ }
2362
+ }
2363
+
2224
2364
  @media (max-width: 720px) {
2225
2365
  .main-title {
2226
2366
  font-size: 40px;
@@ -2236,37 +2376,93 @@
2236
2376
  gap: 6px;
2237
2377
  }
2238
2378
  }
2379
+
2380
+ @media (max-width: 540px) {
2381
+ body {
2382
+ padding: var(--spacing-md) var(--spacing-sm);
2383
+ }
2384
+ .container {
2385
+ padding: 0 var(--spacing-sm) var(--spacing-md);
2386
+ }
2387
+ .hero-title {
2388
+ font-size: 36px;
2389
+ }
2390
+ .hero-subtitle {
2391
+ font-size: var(--font-size-secondary);
2392
+ }
2393
+ .top-tabs {
2394
+ grid-template-columns: repeat(1, minmax(0, 1fr));
2395
+ }
2396
+ .main-panel {
2397
+ padding: var(--spacing-sm);
2398
+ }
2399
+ .card {
2400
+ padding: 12px;
2401
+ }
2402
+ .session-layout {
2403
+ grid-template-columns: 1fr;
2404
+ height: auto;
2405
+ min-height: 0;
2406
+ }
2407
+ }
2239
2408
  </style>
2240
2409
  </head>
2241
2410
  <body>
2242
2411
  <div id="app" class="container" v-cloak>
2243
- <!-- 主标题 -->
2244
- <h1 v-if="!sessionStandalone" class="main-title">
2245
- Codex<br>
2246
- <span class="accent">Mate.</span>
2247
- </h1>
2248
- <p v-if="!sessionStandalone" class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</p>
2249
-
2250
- <!-- 模式切换器 -->
2251
- <div v-if="!sessionStandalone" class="segmented-control">
2252
- <button :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
2253
- Codex 配置
2254
- </button>
2255
- <button :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
2256
- Claude Code 配置
2257
- </button>
2258
- <button :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
2259
- OpenClaw 配置
2260
- </button>
2261
- <button :class="['segment', { active: configMode === 'sessions' }]" @click="switchConfigMode('sessions')">
2262
- 会话浏览
2263
- </button>
2412
+ <div class="hero" v-if="!sessionStandalone">
2413
+ <div class="hero-title">
2414
+ Codex <span class="accent">Mate.</span>
2415
+ </div>
2416
+ <div class="hero-subtitle">
2417
+ 本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。
2418
+ </div>
2419
+ </div>
2420
+
2421
+ <div v-if="!sessionStandalone" class="top-tabs">
2422
+ <button class="top-tab"
2423
+ :class="{ active: mainTab === 'config' && configMode === 'codex' }"
2424
+ @click="switchConfigMode('codex')">Codex 配置</button>
2425
+ <button class="top-tab"
2426
+ :class="{ active: mainTab === 'config' && configMode === 'claude' }"
2427
+ @click="switchConfigMode('claude')">Claude Code 配置</button>
2428
+ <button class="top-tab"
2429
+ :class="{ active: mainTab === 'config' && configMode === 'openclaw' }"
2430
+ @click="switchConfigMode('openclaw')">OpenClaw 配置</button>
2431
+ <button class="top-tab"
2432
+ :class="{ active: mainTab === 'sessions' }"
2433
+ @click="switchMainTab('sessions')">会话浏览</button>
2264
2434
  </div>
2265
2435
 
2266
- <!-- 内容包裹器 - 稳定布局 -->
2267
- <div class="content-wrapper">
2268
- <!-- Codex 配置模式 -->
2269
- <div v-show="configMode === 'codex'" class="mode-content">
2436
+ <div :class="['app-shell', { standalone: sessionStandalone }]">
2437
+ <main class="main-panel">
2438
+ <div class="panel-header" v-if="!sessionStandalone">
2439
+ <h1 class="main-title">
2440
+ {{ mainTab === 'config' ? '配置中心' : '会话浏览' }}
2441
+ </h1>
2442
+ <p class="subtitle" v-if="mainTab === 'config'">
2443
+ 本地配置中枢,统一管理 Codex / Claude Code / OpenClaw。
2444
+ </p>
2445
+ <p class="subtitle" v-else>
2446
+ 浏览、导出或独立查看 Codex / Claude 会话记录。
2447
+ </p>
2448
+ </div>
2449
+
2450
+ <div v-if="false && mainTab === 'config' && !sessionStandalone" class="config-subtabs">
2451
+ <button :class="['config-subtab', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
2452
+ Codex 配置
2453
+ </button>
2454
+ <button :class="['config-subtab', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
2455
+ Claude Code 配置
2456
+ </button>
2457
+ <button :class="['config-subtab', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
2458
+ OpenClaw 配置
2459
+ </button>
2460
+ </div>
2461
+
2462
+ <!-- 内容包裹器 - 稳定布局 -->
2463
+ <div class="content-wrapper">
2464
+ <!-- Codex 配置模式 -->
2465
+ <div v-show="mainTab === 'config' && configMode === 'codex'" class="mode-content mode-cards">
2270
2466
  <!-- 添加提供商按钮 -->
2271
2467
  <button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
2272
2468
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -2320,22 +2516,14 @@
2320
2516
 
2321
2517
  <div class="selector-section">
2322
2518
  <div class="selector-header">
2323
- <span class="selector-title">最近使用</span>
2324
- <span v-if="recentLoading" class="selector-title">加载中...</span>
2325
- </div>
2326
- <div v-if="recentConfigs.length === 0" class="recent-empty">
2327
- 暂无记录
2519
+ <span class="selector-title">服务档位</span>
2328
2520
  </div>
2329
- <div v-else class="recent-list">
2330
- <button
2331
- v-for="item in recentConfigs"
2332
- :key="item.provider + '::' + item.model + '::' + (item.usedAt || '')"
2333
- class="recent-item"
2334
- @click="applyRecentConfig(item)"
2335
- :disabled="loading || !!initError">
2336
- <div class="recent-provider">{{ item.provider }}</div>
2337
- <div class="recent-model">{{ item.model }}</div>
2338
- </button>
2521
+ <select class="model-select" v-model="serviceTier" @change="onServiceTierChange">
2522
+ <option value="fast">fast(默认)</option>
2523
+ <option value="standard">standard</option>
2524
+ </select>
2525
+ <div class="config-template-hint">
2526
+ 仅 fast 会写入 <code>service_tier</code>。
2339
2527
  </div>
2340
2528
  </div>
2341
2529
 
@@ -2384,6 +2572,13 @@
2384
2572
  <path d="M13 2L3 14h7l-1 8 12-14h-7l-1-6z"/>
2385
2573
  </svg>
2386
2574
  </button>
2575
+ <button class="card-action-btn" :class="{ loading: providerShareLoading[provider.name] }" @click="copyProviderShareCommand(provider)" title="分享导入命令">
2576
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2577
+ <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
2578
+ <path d="M16 6l-4-4-4 4"/>
2579
+ <path d="M12 2v14"/>
2580
+ </svg>
2581
+ </button>
2387
2582
  <button class="card-action-btn" @click="openEditModal(provider)" title="编辑">
2388
2583
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2389
2584
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
@@ -2403,7 +2598,7 @@
2403
2598
  </div>
2404
2599
 
2405
2600
  <!-- Claude Code 配置模式 -->
2406
- <div v-show="configMode === 'claude'" class="mode-content">
2601
+ <div v-show="mainTab === 'config' && configMode === 'claude'" class="mode-content mode-cards">
2407
2602
  <!-- 添加提供商按钮 -->
2408
2603
  <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError">
2409
2604
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -2419,7 +2614,16 @@
2419
2614
  <div class="selector-header">
2420
2615
  <span class="selector-title">模型</span>
2421
2616
  </div>
2617
+ <select
2618
+ v-if="claudeModelHasList"
2619
+ class="model-select"
2620
+ v-model="currentClaudeModel"
2621
+ @change="onClaudeModelChange"
2622
+ >
2623
+ <option v-for="model in claudeModelOptions" :key="model" :value="model">{{ model }}</option>
2624
+ </select>
2422
2625
  <input
2626
+ v-else
2423
2627
  class="model-input"
2424
2628
  v-model="currentClaudeModel"
2425
2629
  @blur="onClaudeModelChange"
@@ -2431,6 +2635,15 @@
2431
2635
  </div>
2432
2636
  </div>
2433
2637
 
2638
+ <div class="selector-section">
2639
+ <div class="selector-header">
2640
+ <span class="selector-title">配置健康检查</span>
2641
+ </div>
2642
+ <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
2643
+ {{ healthCheckLoading ? '检查中...' : '运行检查' }}
2644
+ </button>
2645
+ </div>
2646
+
2434
2647
  <div class="card-list">
2435
2648
  <div v-for="(config, name) in claudeConfigs" :key="name"
2436
2649
  :class="['card', { active: currentClaudeConfig === name }]"
@@ -2446,6 +2659,9 @@
2446
2659
  <span :class="['pill', config.hasKey ? 'configured' : 'empty']">
2447
2660
  {{ config.hasKey ? '已配置' : '未配置' }}
2448
2661
  </span>
2662
+ <span v-if="claudeSpeedResults[name]" :class="['latency', claudeSpeedResults[name].ok ? 'ok' : 'error']">
2663
+ {{ formatLatency(claudeSpeedResults[name]) }}
2664
+ </span>
2449
2665
  <div class="card-actions" @click.stop>
2450
2666
  <button class="card-action-btn" @click="openEditConfigModal(name)" title="编辑">
2451
2667
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -2453,6 +2669,13 @@
2453
2669
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
2454
2670
  </svg>
2455
2671
  </button>
2672
+ <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" title="分享导入命令" aria-label="Share import command">
2673
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2674
+ <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
2675
+ <path d="M16 6l-4-4-4 4"/>
2676
+ <path d="M12 2v14"/>
2677
+ </svg>
2678
+ </button>
2456
2679
  <button class="card-action-btn delete" @click="deleteClaudeConfig(name)" title="删除">
2457
2680
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2458
2681
  <path d="M3 6h18"/>
@@ -2466,7 +2689,7 @@
2466
2689
  </div>
2467
2690
 
2468
2691
  <!-- OpenClaw 配置模式 -->
2469
- <div v-show="configMode === 'openclaw'" class="mode-content">
2692
+ <div v-show="mainTab === 'config' && configMode === 'openclaw'" class="mode-content mode-cards">
2470
2693
  <button class="btn-add" @click="openOpenclawAddModal" v-if="!loading && !initError">
2471
2694
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
2472
2695
  <path d="M10 4v12M4 10h12"/>
@@ -2539,29 +2762,29 @@
2539
2762
  </div>
2540
2763
  </div>
2541
2764
 
2542
- <!-- 会话浏览模式 -->
2543
- <div v-show="configMode === 'sessions'" class="mode-content">
2544
- <div v-if="sessionStandalone" class="session-standalone-page">
2545
- <div v-if="sessionStandaloneLoading" class="state-message">
2546
- 加载中...
2547
- </div>
2548
- <div v-else-if="sessionStandaloneError" class="state-message error">
2549
- {{ sessionStandaloneError }}
2550
- </div>
2551
- <div v-else>
2552
- <div class="session-standalone-title">
2553
- {{ sessionStandaloneTitle }}
2554
- <span v-if="sessionStandaloneSourceLabel"> · {{ sessionStandaloneSourceLabel }}</span>
2555
- </div>
2556
- <pre class="session-standalone-text">{{ sessionStandaloneText }}</pre>
2557
- </div>
2558
- </div>
2559
-
2560
- <div v-else>
2561
- <div v-if="!sessionStandalone" class="selector-section">
2562
- <div class="selector-header">
2563
- <span class="selector-title">会话来源</span>
2564
- </div>
2765
+ <!-- 会话浏览模式 -->
2766
+ <div v-show="mainTab === 'sessions'" class="mode-content">
2767
+ <div v-if="sessionStandalone" class="session-standalone-page">
2768
+ <div v-if="sessionStandaloneLoading" class="state-message">
2769
+ 加载中...
2770
+ </div>
2771
+ <div v-else-if="sessionStandaloneError" class="state-message error">
2772
+ {{ sessionStandaloneError }}
2773
+ </div>
2774
+ <div v-else>
2775
+ <div class="session-standalone-title">
2776
+ {{ sessionStandaloneTitle }}
2777
+ <span v-if="sessionStandaloneSourceLabel"> · {{ sessionStandaloneSourceLabel }}</span>
2778
+ </div>
2779
+ <pre class="session-standalone-text">{{ sessionStandaloneText }}</pre>
2780
+ </div>
2781
+ </div>
2782
+
2783
+ <div v-else>
2784
+ <div v-if="!sessionStandalone" class="selector-section">
2785
+ <div class="selector-header">
2786
+ <span class="selector-title">会话来源</span>
2787
+ </div>
2565
2788
  <div class="session-toolbar">
2566
2789
  <div class="session-toolbar-group">
2567
2790
  <select class="session-source-select" v-model="sessionFilterSource" @change="onSessionSourceChange" :disabled="sessionsLoading">
@@ -2617,10 +2840,15 @@
2617
2840
  </button>
2618
2841
  </div>
2619
2842
  </div>
2620
- <div class="session-hint">
2621
- 关键词检索仅 Codex 可用;<br>
2622
- 角色/时间筛选暂不可用;<br>
2623
- 仅支持来源与路径筛选,右侧仅查看/导出。
2843
+ <div class="session-toolbar-footer">
2844
+ <label class="quick-option">
2845
+ <input
2846
+ type="checkbox"
2847
+ v-model="sessionResumeWithYolo"
2848
+ @change="onSessionResumeYoloChange"
2849
+ >
2850
+ 复制恢复命令附带 --yolo
2851
+ </label>
2624
2852
  </div>
2625
2853
  </div>
2626
2854
 
@@ -2661,21 +2889,6 @@
2661
2889
  <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
2662
2890
  </svg>
2663
2891
  </button>
2664
- <button
2665
- v-if="isDeleteAvailable(session)"
2666
- class="session-item-delete"
2667
- @click.stop="deleteSession(session)"
2668
- :disabled="sessionsLoading || sessionDeleting[getSessionExportKey(session)]"
2669
- aria-label="删除会话"
2670
- title="删除会话">
2671
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2672
- <path d="M3 6h18"></path>
2673
- <path d="M8 6V4h8v2"></path>
2674
- <path d="M19 6l-1 14H6L5 6"></path>
2675
- <path d="M10 11v6"></path>
2676
- <path d="M14 11v6"></path>
2677
- </svg>
2678
- </button>
2679
2892
  </div>
2680
2893
  </div>
2681
2894
  <div class="session-item-meta">
@@ -2704,7 +2917,7 @@
2704
2917
  <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
2705
2918
  </div>
2706
2919
  </div>
2707
- <div v-if="!sessionStandalone" class="session-actions">
2920
+ <div v-if="!sessionStandalone" class="session-actions">
2708
2921
  <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession">
2709
2922
  {{ sessionDetailLoading ? '加载中...' : '刷新内容' }}
2710
2923
  </button>
@@ -2770,14 +2983,14 @@
2770
2983
  </div>
2771
2984
  </template>
2772
2985
 
2773
- <div v-else class="session-preview-empty">
2774
- <span v-if="sessionStandaloneError">{{ sessionStandaloneError }}</span>
2775
- <span v-else>请先在左侧选择一个会话</span>
2776
- </div>
2777
- </div>
2778
- </div>
2779
- </div>
2780
- </div>
2986
+ <div v-else class="session-preview-empty">
2987
+ <span v-if="sessionStandaloneError">{{ sessionStandaloneError }}</span>
2988
+ <span v-else>请先在左侧选择一个会话</span>
2989
+ </div>
2990
+ </div>
2991
+ </div>
2992
+ </div>
2993
+ </div>
2781
2994
 
2782
2995
  <!-- 加载状态 -->
2783
2996
  <div v-if="loading" class="state-message">
@@ -2790,6 +3003,9 @@
2790
3003
  </div>
2791
3004
  </div>
2792
3005
 
3006
+ </main>
3007
+ </div>
3008
+
2793
3009
  <!-- 添加提供商模态框 -->
2794
3010
  <div v-if="showAddModal" class="modal-overlay" @click.self="showAddModal = false">
2795
3011
  <div class="modal">
@@ -3278,9 +3494,9 @@
3278
3494
 
3279
3495
  <script>
3280
3496
  const { createApp } = Vue;
3281
- const API_BASE = (location && location.origin && location.origin !== 'null')
3282
- ? location.origin
3283
- : 'http://localhost:3737';
3497
+ const API_BASE = (location && location.origin && location.origin !== 'null')
3498
+ ? location.origin
3499
+ : 'http://localhost:3737';
3284
3500
  const DEFAULT_OPENCLAW_TEMPLATE = `{
3285
3501
  // OpenClaw config (JSON5)
3286
3502
  agent: {
@@ -3305,9 +3521,11 @@
3305
3521
  const app = createApp({
3306
3522
  data() {
3307
3523
  return {
3524
+ mainTab: 'config',
3308
3525
  configMode: 'codex',
3309
3526
  currentProvider: '',
3310
3527
  currentModel: '',
3528
+ serviceTier: 'fast',
3311
3529
  providersList: [],
3312
3530
  models: [],
3313
3531
  codexModelsLoading: false,
@@ -3348,6 +3566,7 @@
3348
3566
  sessionQuery: '',
3349
3567
  sessionRoleFilter: 'all',
3350
3568
  sessionTimePreset: 'all',
3569
+ sessionResumeWithYolo: true,
3351
3570
  sessionPathOptions: [],
3352
3571
  sessionPathOptionsLoading: false,
3353
3572
  sessionPathOptionsMap: {
@@ -3368,17 +3587,21 @@
3368
3587
  activeSessionMessages: [],
3369
3588
  activeSessionDetailError: '',
3370
3589
  activeSessionDetailClipped: false,
3371
- sessionDetailLoading: false,
3372
- sessionDetailRequestSeq: 0,
3373
- sessionStandalone: false,
3374
- sessionStandaloneError: '',
3375
- sessionStandaloneText: '',
3376
- sessionStandaloneTitle: '',
3377
- sessionStandaloneSourceLabel: '',
3378
- sessionStandaloneLoading: false,
3379
- sessionStandaloneRequestSeq: 0,
3380
- speedResults: {},
3590
+ sessionDetailLoading: false,
3591
+ sessionDetailRequestSeq: 0,
3592
+ sessionStandalone: false,
3593
+ sessionStandaloneError: '',
3594
+ sessionStandaloneText: '',
3595
+ sessionStandaloneTitle: '',
3596
+ sessionStandaloneSourceLabel: '',
3597
+ sessionStandaloneLoading: false,
3598
+ sessionStandaloneRequestSeq: 0,
3599
+ speedResults: {},
3381
3600
  speedLoading: {},
3601
+ claudeSpeedResults: {},
3602
+ claudeSpeedLoading: {},
3603
+ claudeShareLoading: {},
3604
+ providerShareLoading: {},
3382
3605
  newProvider: { name: '', url: '', key: '' },
3383
3606
  editingProvider: { name: '', url: '', key: '' },
3384
3607
  newModelName: '',
@@ -3444,8 +3667,6 @@
3444
3667
  openclawAgentsList: [],
3445
3668
  openclawProviders: [],
3446
3669
  openclawMissingProviders: [],
3447
- recentConfigs: [],
3448
- recentLoading: false,
3449
3670
  healthCheckLoading: false,
3450
3671
  healthCheckResult: null,
3451
3672
  healthCheckRemote: false
@@ -3453,6 +3674,12 @@
3453
3674
  },
3454
3675
  mounted() {
3455
3676
  this.initSessionStandalone();
3677
+ const savedSessionYolo = localStorage.getItem('codexmateSessionResumeYolo');
3678
+ if (savedSessionYolo === '0' || savedSessionYolo === 'false') {
3679
+ this.sessionResumeWithYolo = false;
3680
+ } else if (savedSessionYolo === '1' || savedSessionYolo === 'true') {
3681
+ this.sessionResumeWithYolo = true;
3682
+ }
3456
3683
  const savedConfigs = localStorage.getItem('claudeConfigs');
3457
3684
  if (savedConfigs) {
3458
3685
  try {
@@ -3464,22 +3691,11 @@
3464
3691
  }
3465
3692
  }
3466
3693
  localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
3467
-
3468
- const configNames = Object.keys(this.claudeConfigs);
3469
- if (configNames.length > 0) {
3470
- this.currentClaudeConfig = configNames[0];
3471
- }
3472
3694
  } catch (e) {
3473
3695
  console.error('加载 Claude 配置失败:', e);
3474
3696
  }
3475
3697
  }
3476
- if (!this.currentClaudeConfig) {
3477
- const configNames = Object.keys(this.claudeConfigs);
3478
- if (configNames.length > 0) {
3479
- this.currentClaudeConfig = configNames[0];
3480
- }
3481
- }
3482
- this.syncClaudeModelFromConfig();
3698
+ void this.refreshClaudeSelectionFromSettings({ silent: true });
3483
3699
  const savedOpenclawConfigs = localStorage.getItem('openclawConfigs');
3484
3700
  if (savedOpenclawConfigs) {
3485
3701
  try {
@@ -3506,6 +3722,17 @@
3506
3722
  },
3507
3723
  sessionQueryPlaceholder() {
3508
3724
  return this.isSessionQueryEnabled ? '关键词检索' : '仅 Codex 支持关键词检索';
3725
+ },
3726
+ claudeModelHasList() {
3727
+ return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
3728
+ },
3729
+ claudeModelOptions() {
3730
+ const list = Array.isArray(this.claudeModels) ? [...this.claudeModels] : [];
3731
+ const current = (this.currentClaudeModel || '').trim();
3732
+ if (current && !list.includes(current)) {
3733
+ list.unshift(current);
3734
+ }
3735
+ return list;
3509
3736
  }
3510
3737
  },
3511
3738
  methods: {
@@ -3521,6 +3748,12 @@
3521
3748
  } else {
3522
3749
  this.currentProvider = statusRes.provider;
3523
3750
  this.currentModel = statusRes.model;
3751
+ {
3752
+ const tier = typeof statusRes.serviceTier === 'string'
3753
+ ? statusRes.serviceTier.trim().toLowerCase()
3754
+ : '';
3755
+ this.serviceTier = tier === 'fast' ? 'fast' : (tier ? 'standard' : 'fast');
3756
+ }
3524
3757
  this.providersList = listRes.providers;
3525
3758
  await this.loadModelsForProvider(this.currentProvider);
3526
3759
  if (statusRes.configReady === false) {
@@ -3531,7 +3764,6 @@
3531
3764
  }
3532
3765
  this.maybeShowStarPrompt();
3533
3766
  }
3534
- await this.loadRecentConfigs();
3535
3767
  } catch (e) {
3536
3768
  this.initError = '连接失败: ' + e.message;
3537
3769
  } finally {
@@ -3582,6 +3814,109 @@
3582
3814
  return this.claudeConfigs[this.currentClaudeConfig] || null;
3583
3815
  },
3584
3816
 
3817
+ normalizeClaudeValue(value) {
3818
+ return typeof value === 'string' ? value.trim() : '';
3819
+ },
3820
+
3821
+ normalizeClaudeConfig(config) {
3822
+ const safe = config && typeof config === 'object' ? config : {};
3823
+ return {
3824
+ apiKey: this.normalizeClaudeValue(safe.apiKey),
3825
+ baseUrl: this.normalizeClaudeValue(safe.baseUrl),
3826
+ model: this.normalizeClaudeValue(safe.model)
3827
+ };
3828
+ },
3829
+
3830
+ normalizeClaudeSettingsEnv(env) {
3831
+ const safe = env && typeof env === 'object' ? env : {};
3832
+ return {
3833
+ apiKey: this.normalizeClaudeValue(safe.ANTHROPIC_API_KEY),
3834
+ baseUrl: this.normalizeClaudeValue(safe.ANTHROPIC_BASE_URL),
3835
+ model: this.normalizeClaudeValue(safe.ANTHROPIC_MODEL)
3836
+ };
3837
+ },
3838
+
3839
+ matchClaudeConfigFromSettings(env) {
3840
+ const normalizedSettings = this.normalizeClaudeSettingsEnv(env);
3841
+ if (!normalizedSettings.apiKey || !normalizedSettings.baseUrl || !normalizedSettings.model) {
3842
+ return '';
3843
+ }
3844
+ const entries = Object.entries(this.claudeConfigs || {});
3845
+ for (const [name, config] of entries) {
3846
+ const normalizedConfig = this.normalizeClaudeConfig(config);
3847
+ if (!normalizedConfig.apiKey || !normalizedConfig.baseUrl || !normalizedConfig.model) {
3848
+ continue;
3849
+ }
3850
+ if (normalizedConfig.apiKey === normalizedSettings.apiKey
3851
+ && normalizedConfig.baseUrl === normalizedSettings.baseUrl
3852
+ && normalizedConfig.model === normalizedSettings.model) {
3853
+ return name;
3854
+ }
3855
+ }
3856
+ return '';
3857
+ },
3858
+
3859
+ findDuplicateClaudeConfigName(config) {
3860
+ const normalized = this.normalizeClaudeConfig(config);
3861
+ if (!normalized.apiKey || !normalized.baseUrl || !normalized.model) {
3862
+ return '';
3863
+ }
3864
+ const entries = Object.entries(this.claudeConfigs || {});
3865
+ for (const [name, existing] of entries) {
3866
+ const normalizedExisting = this.normalizeClaudeConfig(existing);
3867
+ if (!normalizedExisting.apiKey || !normalizedExisting.baseUrl || !normalizedExisting.model) {
3868
+ continue;
3869
+ }
3870
+ if (normalizedExisting.apiKey === normalized.apiKey
3871
+ && normalizedExisting.baseUrl === normalized.baseUrl
3872
+ && normalizedExisting.model === normalized.model) {
3873
+ return name;
3874
+ }
3875
+ }
3876
+ return '';
3877
+ },
3878
+
3879
+ async refreshClaudeSelectionFromSettings(options = {}) {
3880
+ const configNames = Object.keys(this.claudeConfigs || {});
3881
+ if (configNames.length === 0) {
3882
+ this.currentClaudeConfig = '';
3883
+ this.currentClaudeModel = '';
3884
+ this.resetClaudeModelsState();
3885
+ return;
3886
+ }
3887
+ const silent = !!options.silent;
3888
+ try {
3889
+ const res = await api('get-claude-settings');
3890
+ if (res && res.error) {
3891
+ if (!silent) {
3892
+ this.showMessage('读取 Claude 配置失败: ' + res.error, 'error');
3893
+ }
3894
+ return;
3895
+ }
3896
+ const matchName = this.matchClaudeConfigFromSettings((res && res.env) || {});
3897
+ if (matchName) {
3898
+ if (this.currentClaudeConfig !== matchName) {
3899
+ this.currentClaudeConfig = matchName;
3900
+ }
3901
+ this.refreshClaudeModelContext();
3902
+ return;
3903
+ }
3904
+ this.currentClaudeConfig = '';
3905
+ this.currentClaudeModel = '';
3906
+ this.resetClaudeModelsState();
3907
+ if (!silent) {
3908
+ const tip = res && res.exists
3909
+ ? '当前 Claude settings.json 与本地配置不匹配,已取消选中'
3910
+ : '未检测到 Claude settings.json,已取消选中';
3911
+ this.showMessage(tip, 'info');
3912
+ }
3913
+ } catch (e) {
3914
+ if (!silent) {
3915
+ this.showMessage('读取 Claude 配置失败: ' + e.message, 'error');
3916
+ }
3917
+ }
3918
+ },
3919
+
3585
3920
  syncClaudeModelFromConfig() {
3586
3921
  const config = this.getCurrentClaudeConfig();
3587
3922
  this.currentClaudeModel = config && config.model ? config.model : '';
@@ -3606,6 +3941,10 @@
3606
3941
 
3607
3942
  async loadClaudeModels() {
3608
3943
  const config = this.getCurrentClaudeConfig();
3944
+ if (!config) {
3945
+ this.resetClaudeModelsState();
3946
+ return;
3947
+ }
3609
3948
  const baseUrl = (config.baseUrl || '').trim();
3610
3949
  const apiKey = (config.apiKey || '').trim();
3611
3950
 
@@ -3658,13 +3997,21 @@
3658
3997
  },
3659
3998
 
3660
3999
  switchConfigMode(mode) {
4000
+ this.mainTab = 'config';
3661
4001
  this.configMode = mode;
3662
4002
  if (mode === 'claude') {
3663
4003
  this.refreshClaudeModelContext();
3664
4004
  }
3665
- if (mode === 'sessions' && this.sessionsList.length === 0) {
4005
+ },
4006
+
4007
+ switchMainTab(tab) {
4008
+ this.mainTab = tab;
4009
+ if (tab === 'sessions' && this.sessionsList.length === 0) {
3666
4010
  this.loadSessions();
3667
4011
  }
4012
+ if (tab === 'config' && this.configMode === 'claude') {
4013
+ this.refreshClaudeModelContext();
4014
+ }
3668
4015
  },
3669
4016
 
3670
4017
  getSessionStandaloneContext() {
@@ -3710,7 +4057,7 @@
3710
4057
  if (!context.requested) return;
3711
4058
 
3712
4059
  this.sessionStandalone = true;
3713
- this.configMode = 'sessions';
4060
+ this.mainTab = 'sessions';
3714
4061
 
3715
4062
  if (context.error || !context.params) {
3716
4063
  this.sessionStandaloneError = `会话链接参数不完整:${context.error || '参数解析失败'}`;
@@ -3718,22 +4065,22 @@
3718
4065
  }
3719
4066
 
3720
4067
  const sourceLabel = context.params.source === 'codex' ? 'Codex' : 'Claude Code';
3721
- this.activeSession = {
3722
- source: context.params.source,
3723
- sourceLabel,
3724
- sessionId: context.params.sessionId,
3725
- filePath: context.params.filePath,
3726
- title: context.params.sessionId || context.params.filePath || '会话'
3727
- };
3728
- this.activeSessionMessages = [];
3729
- this.activeSessionDetailError = '';
3730
- this.activeSessionDetailClipped = false;
3731
- this.sessionStandaloneError = '';
3732
- this.sessionStandaloneText = '';
3733
- this.sessionStandaloneTitle = this.activeSession.title || '会话';
3734
- this.sessionStandaloneSourceLabel = sourceLabel;
3735
- this.loadSessionStandalonePlain();
3736
- },
4068
+ this.activeSession = {
4069
+ source: context.params.source,
4070
+ sourceLabel,
4071
+ sessionId: context.params.sessionId,
4072
+ filePath: context.params.filePath,
4073
+ title: context.params.sessionId || context.params.filePath || '会话'
4074
+ };
4075
+ this.activeSessionMessages = [];
4076
+ this.activeSessionDetailError = '';
4077
+ this.activeSessionDetailClipped = false;
4078
+ this.sessionStandaloneError = '';
4079
+ this.sessionStandaloneText = '';
4080
+ this.sessionStandaloneTitle = this.activeSession.title || '会话';
4081
+ this.sessionStandaloneSourceLabel = sourceLabel;
4082
+ this.loadSessionStandalonePlain();
4083
+ },
3737
4084
 
3738
4085
  buildSessionStandaloneUrl(session) {
3739
4086
  if (!session) return '';
@@ -3791,10 +4138,14 @@
3791
4138
 
3792
4139
  buildResumeCommand(session) {
3793
4140
  const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
3794
- return `codex resume ${this.quoteResumeArg(sessionId)}`;
4141
+ const arg = this.quoteResumeArg(sessionId);
4142
+ if (this.sessionResumeWithYolo) {
4143
+ return `codex --yolo resume ${arg}`;
4144
+ }
4145
+ return `codex resume ${arg}`;
3795
4146
  },
3796
4147
 
3797
- quoteResumeArg(value) {
4148
+ quoteShellArg(value) {
3798
4149
  const text = typeof value === 'string' ? value : String(value || '');
3799
4150
  if (!text) return "''";
3800
4151
  if (/^[a-zA-Z0-9._-]+$/.test(text)) return text;
@@ -3802,6 +4153,10 @@
3802
4153
  return `'${escaped}'`;
3803
4154
  },
3804
4155
 
4156
+ quoteResumeArg(value) {
4157
+ return this.quoteShellArg(value);
4158
+ },
4159
+
3805
4160
  fallbackCopyText(text) {
3806
4161
  let textarea = null;
3807
4162
  try {
@@ -3862,6 +4217,121 @@
3862
4217
  this.showMessage('复制失败,请手动复制命令', 'error');
3863
4218
  },
3864
4219
 
4220
+ buildProviderShareCommand(payload) {
4221
+ if (!payload || typeof payload !== 'object') return '';
4222
+ const name = typeof payload.name === 'string' ? payload.name.trim() : '';
4223
+ const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl.trim() : '';
4224
+ const apiKey = typeof payload.apiKey === 'string' ? payload.apiKey : '';
4225
+ if (!name || !baseUrl) return '';
4226
+
4227
+ const nameArg = this.quoteShellArg(name);
4228
+ const urlArg = this.quoteShellArg(baseUrl);
4229
+ const keyArg = apiKey ? this.quoteShellArg(apiKey) : '';
4230
+ const switchCmd = `codexmate switch ${nameArg}`;
4231
+ const addCmd = apiKey
4232
+ ? `codexmate add ${nameArg} ${urlArg} ${keyArg}`
4233
+ : `codexmate add ${nameArg} ${urlArg}`;
4234
+ return `${addCmd} && ${switchCmd}`;
4235
+ },
4236
+
4237
+ buildClaudeShareCommand(payload) {
4238
+ if (!payload || typeof payload !== 'object') return '';
4239
+ const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl.trim() : '';
4240
+ const apiKey = typeof payload.apiKey === 'string' ? payload.apiKey : '';
4241
+ const model = typeof payload.model === 'string' && payload.model.trim()
4242
+ ? payload.model.trim()
4243
+ : 'glm-4.7';
4244
+ if (!baseUrl || !apiKey) return '';
4245
+ const urlArg = this.quoteShellArg(baseUrl);
4246
+ const keyArg = this.quoteShellArg(apiKey);
4247
+ const modelArg = this.quoteShellArg(model);
4248
+ return `codexmate claude ${urlArg} ${keyArg} ${modelArg}`;
4249
+ },
4250
+
4251
+ async copyProviderShareCommand(provider) {
4252
+ const name = provider && typeof provider.name === 'string' ? provider.name.trim() : '';
4253
+ if (!name) {
4254
+ this.showMessage('提供商名称无效', 'error');
4255
+ return;
4256
+ }
4257
+ if (this.providerShareLoading[name]) {
4258
+ return;
4259
+ }
4260
+ this.providerShareLoading[name] = true;
4261
+ try {
4262
+ const res = await api('export-provider', { name });
4263
+ if (res && res.error) {
4264
+ this.showMessage(res.error, 'error');
4265
+ return;
4266
+ }
4267
+ const command = this.buildProviderShareCommand(res && res.payload ? res.payload : null);
4268
+ if (!command) {
4269
+ this.showMessage('分享命令生成失败', 'error');
4270
+ return;
4271
+ }
4272
+ const ok = this.fallbackCopyText(command);
4273
+ if (ok) {
4274
+ this.showMessage('已复制分享命令', 'success');
4275
+ return;
4276
+ }
4277
+ try {
4278
+ if (navigator.clipboard && window.isSecureContext) {
4279
+ await navigator.clipboard.writeText(command);
4280
+ this.showMessage('已复制分享命令', 'success');
4281
+ return;
4282
+ }
4283
+ } catch (e) {
4284
+ // keep fallback failure message
4285
+ }
4286
+ this.showMessage('复制失败,请手动复制命令', 'error');
4287
+ } catch (e) {
4288
+ this.showMessage('生成分享命令失败: ' + e.message, 'error');
4289
+ } finally {
4290
+ this.providerShareLoading[name] = false;
4291
+ }
4292
+ },
4293
+
4294
+ async copyClaudeShareCommand(name) {
4295
+ const config = this.claudeConfigs[name];
4296
+ if (!config) {
4297
+ this.showMessage('配置不存在', 'error');
4298
+ return;
4299
+ }
4300
+ if (this.claudeShareLoading[name]) return;
4301
+ this.claudeShareLoading[name] = true;
4302
+ try {
4303
+ const res = await api('export-claude-share', { config });
4304
+ if (res && res.error) {
4305
+ this.showMessage(res.error, 'error');
4306
+ return;
4307
+ }
4308
+ const command = this.buildClaudeShareCommand(res && res.payload ? res.payload : null);
4309
+ if (!command) {
4310
+ this.showMessage('分享命令生成失败', 'error');
4311
+ return;
4312
+ }
4313
+ const ok = this.fallbackCopyText(command);
4314
+ if (ok) {
4315
+ this.showMessage('已复制分享命令', 'success');
4316
+ return;
4317
+ }
4318
+ try {
4319
+ if (navigator.clipboard && window.isSecureContext) {
4320
+ await navigator.clipboard.writeText(command);
4321
+ this.showMessage('已复制分享命令', 'success');
4322
+ return;
4323
+ }
4324
+ } catch (e) {
4325
+ // fall through
4326
+ }
4327
+ this.showMessage('复制失败,请手动复制命令', 'error');
4328
+ } catch (e) {
4329
+ this.showMessage('生成分享命令失败: ' + e.message, 'error');
4330
+ } finally {
4331
+ this.claudeShareLoading[name] = false;
4332
+ }
4333
+ },
4334
+
3865
4335
  async cloneSession(session) {
3866
4336
  if (!this.isCloneAvailable(session)) {
3867
4337
  this.showMessage('当前会话不支持克隆', 'error');
@@ -4036,6 +4506,11 @@
4036
4506
  }
4037
4507
  },
4038
4508
 
4509
+ onSessionResumeYoloChange() {
4510
+ const value = this.sessionResumeWithYolo ? '1' : '0';
4511
+ localStorage.setItem('codexmateSessionResumeYolo', value);
4512
+ },
4513
+
4039
4514
  async onSessionSourceChange() {
4040
4515
  this.refreshSessionPathOptions(this.sessionFilterSource);
4041
4516
  await this.loadSessions();
@@ -4140,66 +4615,66 @@
4140
4615
  }
4141
4616
  },
4142
4617
 
4143
- async selectSession(session) {
4144
- if (!session) return;
4145
- if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
4146
- this.activeSession = session;
4147
- this.activeSessionMessages = [];
4148
- this.activeSessionDetailError = '';
4149
- this.activeSessionDetailClipped = false;
4150
- await this.loadActiveSessionDetail();
4151
- },
4152
-
4153
- async loadSessionStandalonePlain() {
4154
- if (!this.activeSession) {
4155
- this.sessionStandaloneText = '';
4156
- this.sessionStandaloneTitle = '会话';
4157
- this.sessionStandaloneSourceLabel = '';
4158
- this.sessionStandaloneError = '';
4159
- return;
4160
- }
4161
-
4162
- const requestSeq = ++this.sessionStandaloneRequestSeq;
4163
- this.sessionStandaloneLoading = true;
4164
- this.sessionStandaloneError = '';
4165
- try {
4166
- const res = await api('session-plain', {
4167
- source: this.activeSession.source,
4168
- sessionId: this.activeSession.sessionId,
4169
- filePath: this.activeSession.filePath
4170
- });
4171
-
4172
- if (requestSeq !== this.sessionStandaloneRequestSeq) {
4173
- return;
4174
- }
4175
-
4176
- if (res.error) {
4177
- this.sessionStandaloneText = '';
4178
- this.sessionStandaloneError = res.error;
4179
- return;
4180
- }
4181
-
4182
- this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
4183
- this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
4184
- this.sessionStandaloneText = typeof res.text === 'string' ? res.text : '';
4185
- } catch (e) {
4186
- if (requestSeq !== this.sessionStandaloneRequestSeq) {
4187
- return;
4188
- }
4189
- this.sessionStandaloneText = '';
4190
- this.sessionStandaloneError = '加载会话内容失败: ' + e.message;
4191
- } finally {
4192
- if (requestSeq === this.sessionStandaloneRequestSeq) {
4193
- this.sessionStandaloneLoading = false;
4194
- }
4195
- }
4196
- },
4197
-
4198
- async loadActiveSessionDetail() {
4199
- if (!this.activeSession) {
4200
- this.activeSessionMessages = [];
4201
- this.activeSessionDetailError = '';
4202
- this.activeSessionDetailClipped = false;
4618
+ async selectSession(session) {
4619
+ if (!session) return;
4620
+ if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
4621
+ this.activeSession = session;
4622
+ this.activeSessionMessages = [];
4623
+ this.activeSessionDetailError = '';
4624
+ this.activeSessionDetailClipped = false;
4625
+ await this.loadActiveSessionDetail();
4626
+ },
4627
+
4628
+ async loadSessionStandalonePlain() {
4629
+ if (!this.activeSession) {
4630
+ this.sessionStandaloneText = '';
4631
+ this.sessionStandaloneTitle = '会话';
4632
+ this.sessionStandaloneSourceLabel = '';
4633
+ this.sessionStandaloneError = '';
4634
+ return;
4635
+ }
4636
+
4637
+ const requestSeq = ++this.sessionStandaloneRequestSeq;
4638
+ this.sessionStandaloneLoading = true;
4639
+ this.sessionStandaloneError = '';
4640
+ try {
4641
+ const res = await api('session-plain', {
4642
+ source: this.activeSession.source,
4643
+ sessionId: this.activeSession.sessionId,
4644
+ filePath: this.activeSession.filePath
4645
+ });
4646
+
4647
+ if (requestSeq !== this.sessionStandaloneRequestSeq) {
4648
+ return;
4649
+ }
4650
+
4651
+ if (res.error) {
4652
+ this.sessionStandaloneText = '';
4653
+ this.sessionStandaloneError = res.error;
4654
+ return;
4655
+ }
4656
+
4657
+ this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
4658
+ this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
4659
+ this.sessionStandaloneText = typeof res.text === 'string' ? res.text : '';
4660
+ } catch (e) {
4661
+ if (requestSeq !== this.sessionStandaloneRequestSeq) {
4662
+ return;
4663
+ }
4664
+ this.sessionStandaloneText = '';
4665
+ this.sessionStandaloneError = '加载会话内容失败: ' + e.message;
4666
+ } finally {
4667
+ if (requestSeq === this.sessionStandaloneRequestSeq) {
4668
+ this.sessionStandaloneLoading = false;
4669
+ }
4670
+ }
4671
+ },
4672
+
4673
+ async loadActiveSessionDetail() {
4674
+ if (!this.activeSession) {
4675
+ this.activeSessionMessages = [];
4676
+ this.activeSessionDetailError = '';
4677
+ this.activeSessionDetailClipped = false;
4203
4678
  return;
4204
4679
  }
4205
4680
 
@@ -4285,22 +4760,22 @@
4285
4760
  sessionId: session.sessionId,
4286
4761
  filePath: session.filePath
4287
4762
  });
4288
- if (res.error) {
4289
- this.showMessage(res.error, 'error');
4290
- return;
4291
- }
4292
-
4293
- const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
4294
- this.downloadTextFile(fileName, res.content || '');
4295
- if (res.truncated) {
4296
- const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
4297
- this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
4298
- } else {
4299
- this.showMessage('会话导出完成', 'success');
4300
- }
4301
- } catch (e) {
4302
- this.showMessage('导出失败: ' + e.message, 'error');
4303
- } finally {
4763
+ if (res.error) {
4764
+ this.showMessage(res.error, 'error');
4765
+ return;
4766
+ }
4767
+
4768
+ const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
4769
+ this.downloadTextFile(fileName, res.content || '');
4770
+ if (res.truncated) {
4771
+ const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
4772
+ this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
4773
+ } else {
4774
+ this.showMessage('会话导出完成', 'success');
4775
+ }
4776
+ } catch (e) {
4777
+ this.showMessage('导出失败: ' + e.message, 'error');
4778
+ } finally {
4304
4779
  this.sessionExporting[key] = false;
4305
4780
  }
4306
4781
  },
@@ -4315,32 +4790,8 @@
4315
4790
  await this.openConfigTemplateEditor();
4316
4791
  },
4317
4792
 
4318
- async loadRecentConfigs() {
4319
- this.recentLoading = true;
4320
- try {
4321
- const res = await api('get-recent-configs');
4322
- if (res && Array.isArray(res.items)) {
4323
- this.recentConfigs = res.items;
4324
- } else {
4325
- this.recentConfigs = [];
4326
- }
4327
- } catch (e) {
4328
- this.recentConfigs = [];
4329
- } finally {
4330
- this.recentLoading = false;
4331
- }
4332
- },
4333
-
4334
- async applyRecentConfig(item) {
4335
- if (!item || !item.provider || !item.model) {
4336
- this.showMessage('最近配置无效,无法应用', 'error');
4337
- return;
4338
- }
4339
- this.currentProvider = item.provider;
4340
- this.currentModel = item.model;
4341
- await this.openConfigTemplateEditor({
4342
- appendHint: '最近使用配置,确认后将写入 config.toml'
4343
- });
4793
+ async onServiceTierChange() {
4794
+ await this.openConfigTemplateEditor();
4344
4795
  },
4345
4796
 
4346
4797
  async runHealthCheck() {
@@ -4395,6 +4846,12 @@
4395
4846
  this.healthCheckResult = null;
4396
4847
  this.showMessage('健康检查失败: ' + e.message, 'error');
4397
4848
  } finally {
4849
+ if (this.configMode === 'claude') {
4850
+ try {
4851
+ const entries = Object.entries(this.claudeConfigs || {});
4852
+ await Promise.all(entries.map(([name, config]) => this.runClaudeSpeedTest(name, config)));
4853
+ } catch (e) {}
4854
+ }
4398
4855
  this.healthCheckLoading = false;
4399
4856
  }
4400
4857
  },
@@ -4409,7 +4866,8 @@
4409
4866
  try {
4410
4867
  const res = await api('get-config-template', {
4411
4868
  provider: this.currentProvider,
4412
- model: this.currentModel
4869
+ model: this.currentModel,
4870
+ serviceTier: this.serviceTier
4413
4871
  });
4414
4872
  if (res.error) {
4415
4873
  this.showMessage(res.error, 'error');
@@ -4818,6 +5276,10 @@
4818
5276
  if (this.claudeConfigs[name]) {
4819
5277
  return this.showMessage('配置名称已存在', 'error');
4820
5278
  }
5279
+ const duplicateName = this.findDuplicateClaudeConfigName(this.newClaudeConfig);
5280
+ if (duplicateName) {
5281
+ return this.showMessage('已存在相同配置,已忽略添加', 'info');
5282
+ }
4821
5283
 
4822
5284
  this.claudeConfigs[name] = {
4823
5285
  apiKey: this.newClaudeConfig.apiKey,
@@ -5912,6 +6374,33 @@
5912
6374
  }
5913
6375
  },
5914
6376
 
6377
+ async runClaudeSpeedTest(name, config) {
6378
+ if (!name || this.claudeSpeedLoading[name]) return null;
6379
+ const baseUrl = config && typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
6380
+ this.claudeSpeedLoading[name] = true;
6381
+ try {
6382
+ if (!baseUrl) {
6383
+ const res = { ok: false, error: 'Missing base URL' };
6384
+ this.claudeSpeedResults[name] = res;
6385
+ return res;
6386
+ }
6387
+ const res = await api('speed-test', { url: baseUrl });
6388
+ if (res.error) {
6389
+ this.claudeSpeedResults[name] = { ok: false, error: res.error };
6390
+ return { ok: false, error: res.error };
6391
+ }
6392
+ this.claudeSpeedResults[name] = res;
6393
+ return res;
6394
+ } catch (e) {
6395
+ const message = e && e.message ? e.message : 'Speed test failed';
6396
+ const res = { ok: false, error: message };
6397
+ this.claudeSpeedResults[name] = res;
6398
+ return res;
6399
+ } finally {
6400
+ this.claudeSpeedLoading[name] = false;
6401
+ }
6402
+ },
6403
+
5915
6404
  showMessage(text, type) {
5916
6405
  this.message = text;
5917
6406
  this.messageType = type || 'info';