codexmate 0.0.15 → 0.0.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "codexmate",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Codex/Claude Code 配置与会话管理 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -12,7 +12,9 @@
12
12
  normalizeSessionPathFilter,
13
13
  buildSessionFilterCacheState,
14
14
  buildSessionTimelineNodes,
15
- normalizeSessionMessageRole
15
+ normalizeSessionMessageRole,
16
+ runLatestOnlyQueue,
17
+ shouldForceCompactLayoutMode
16
18
  } from './logic.mjs';
17
19
  import {
18
20
  CONFIG_MODE_SET,
@@ -170,6 +172,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
170
172
  claudeSpeedLoading: {},
171
173
  claudeShareLoading: {},
172
174
  providerShareLoading: {},
175
+ providerSwitchInProgress: false,
176
+ pendingProviderSwitch: '',
173
177
  installPackageManager: 'npm',
174
178
  installCommandAction: 'install',
175
179
  installRegistryPreset: 'default',
@@ -291,11 +295,13 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
291
295
  proxyStarting: false,
292
296
  proxyStopping: false,
293
297
  proxyApplying: false,
294
- showProxyAdvanced: false
298
+ showProxyAdvanced: false,
299
+ forceCompactLayout: false
295
300
  }
296
301
  },
297
302
  mounted() {
298
303
  this.initSessionStandalone();
304
+ this.updateCompactLayoutMode();
299
305
  const savedSessionYolo = localStorage.getItem('codexmateSessionResumeYolo');
300
306
  if (savedSessionYolo === '0' || savedSessionYolo === 'false') {
301
307
  this.sessionResumeWithYolo = false;
@@ -343,6 +349,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
343
349
  this.cancelSessionTimelineSync();
344
350
  this.disconnectSessionPreviewHeaderResizeObserver();
345
351
  window.removeEventListener('resize', this.onWindowResize);
352
+ this.applyCompactLayoutClass(false);
346
353
  this.sessionPreviewScrollEl = null;
347
354
  this.sessionPreviewContainerEl = null;
348
355
  this.sessionPreviewHeaderEl = null;
@@ -542,7 +549,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
542
549
  }
543
550
  },
544
551
 
545
- async loadModelsForProvider(providerName) {
552
+ async loadModelsForProvider(providerName, options = {}) {
553
+ const silentError = !!options.silentError;
546
554
  this.codexModelsLoading = true;
547
555
  if (!providerName) {
548
556
  this.models = [];
@@ -560,7 +568,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
560
568
  return;
561
569
  }
562
570
  if (res.error) {
563
- this.showMessage('获取模型列表失败', 'error');
571
+ if (!silentError) {
572
+ this.showMessage('获取模型列表失败', 'error');
573
+ }
564
574
  this.models = [];
565
575
  this.modelsSource = 'error';
566
576
  this.modelsHasCurrent = true;
@@ -571,7 +581,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
571
581
  this.modelsSource = res.source || 'remote';
572
582
  this.modelsHasCurrent = !!this.currentModel && list.includes(this.currentModel);
573
583
  } catch (e) {
574
- this.showMessage('获取模型列表失败', 'error');
584
+ if (!silentError) {
585
+ this.showMessage('获取模型列表失败', 'error');
586
+ }
575
587
  this.models = [];
576
588
  this.modelsSource = 'error';
577
589
  this.modelsHasCurrent = true;
@@ -603,15 +615,58 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
603
615
  return findDuplicateClaudeConfigName(this.claudeConfigs, config);
604
616
  },
605
617
 
606
- async refreshClaudeSelectionFromSettings(options = {}) {
607
- const configNames = Object.keys(this.claudeConfigs || {});
608
- if (configNames.length === 0) {
609
- this.currentClaudeConfig = '';
610
- this.currentClaudeModel = '';
611
- this.resetClaudeModelsState();
612
- return;
618
+ mergeClaudeConfig(existing = {}, updates = {}) {
619
+ const previous = this.normalizeClaudeConfig(existing);
620
+ const next = this.normalizeClaudeConfig({ ...existing, ...updates });
621
+ const externalCredentialType = next.apiKey
622
+ ? ''
623
+ : (next.externalCredentialType || previous.externalCredentialType || '');
624
+ return {
625
+ apiKey: next.apiKey,
626
+ baseUrl: next.baseUrl,
627
+ model: next.model || previous.model || 'glm-4.7',
628
+ hasKey: !!(next.apiKey || externalCredentialType),
629
+ externalCredentialType
630
+ };
631
+ },
632
+
633
+ buildClaudeImportedConfigName(baseUrl) {
634
+ const normalizedUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
635
+ if (!normalizedUrl) return '导入配置';
636
+ try {
637
+ const parsed = new URL(normalizedUrl);
638
+ const host = typeof parsed.host === 'string' ? parsed.host.trim() : '';
639
+ if (host) return `导入-${host}`;
640
+ } catch (_) {
641
+ // keep generic fallback name
642
+ }
643
+ return '导入配置';
644
+ },
645
+
646
+ ensureClaudeConfigFromSettings(env = {}) {
647
+ const normalized = this.normalizeClaudeSettingsEnv(env);
648
+ const hasCredential = !!(normalized.apiKey || normalized.authToken || normalized.useKey);
649
+ if (!normalized.baseUrl || !hasCredential) return '';
650
+
651
+ const duplicateName = this.findDuplicateClaudeConfigName(normalized);
652
+ if (duplicateName) return duplicateName;
653
+
654
+ const preferredName = this.buildClaudeImportedConfigName(normalized.baseUrl);
655
+ let candidateName = preferredName;
656
+ let suffix = 2;
657
+ while (this.claudeConfigs[candidateName]) {
658
+ candidateName = `${preferredName}-${suffix}`;
659
+ suffix += 1;
613
660
  }
661
+
662
+ this.claudeConfigs[candidateName] = this.mergeClaudeConfig({}, normalized);
663
+ this.saveClaudeConfigs();
664
+ return candidateName;
665
+ },
666
+
667
+ async refreshClaudeSelectionFromSettings(options = {}) {
614
668
  const silent = !!options.silent;
669
+ const silentModelError = !!options.silentModelError || silent;
615
670
  try {
616
671
  const res = await api('get-claude-settings');
617
672
  if (res && res.error) {
@@ -625,7 +680,18 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
625
680
  if (this.currentClaudeConfig !== matchName) {
626
681
  this.currentClaudeConfig = matchName;
627
682
  }
628
- this.refreshClaudeModelContext();
683
+ this.refreshClaudeModelContext({ silentError: silentModelError });
684
+ return;
685
+ }
686
+ const importedName = this.ensureClaudeConfigFromSettings((res && res.env) || {});
687
+ if (importedName) {
688
+ if (this.currentClaudeConfig !== importedName) {
689
+ this.currentClaudeConfig = importedName;
690
+ }
691
+ this.refreshClaudeModelContext({ silentError: silentModelError });
692
+ if (!silent) {
693
+ this.showMessage(`检测到外部 Claude 配置,已自动导入:${importedName}`, 'success');
694
+ }
629
695
  return;
630
696
  }
631
697
  this.currentClaudeConfig = '';
@@ -649,9 +715,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
649
715
  this.currentClaudeModel = config && config.model ? config.model : '';
650
716
  },
651
717
 
652
- refreshClaudeModelContext() {
718
+ refreshClaudeModelContext(options = {}) {
653
719
  this.syncClaudeModelFromConfig();
654
- this.loadClaudeModels();
720
+ return this.loadClaudeModels(options);
655
721
  },
656
722
 
657
723
  resetClaudeModelsState() {
@@ -666,7 +732,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
666
732
  this.claudeModelsHasCurrent = !!currentModel && this.claudeModels.includes(currentModel);
667
733
  },
668
734
 
669
- async loadClaudeModels() {
735
+ async loadClaudeModels(options = {}) {
736
+ const silentError = !!options.silentError;
670
737
  const config = this.getCurrentClaudeConfig();
671
738
  if (!config) {
672
739
  this.resetClaudeModelsState();
@@ -674,11 +741,20 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
674
741
  }
675
742
  const baseUrl = (config.baseUrl || '').trim();
676
743
  const apiKey = (config.apiKey || '').trim();
744
+ const externalCredentialType = typeof config.externalCredentialType === 'string'
745
+ ? config.externalCredentialType.trim()
746
+ : '';
677
747
 
678
748
  if (!baseUrl) {
679
749
  this.resetClaudeModelsState();
680
750
  return;
681
751
  }
752
+ if (!apiKey && externalCredentialType) {
753
+ this.claudeModels = [];
754
+ this.claudeModelsSource = 'unlimited';
755
+ this.claudeModelsHasCurrent = true;
756
+ return;
757
+ }
682
758
 
683
759
  this.claudeModelsLoading = true;
684
760
  try {
@@ -690,7 +766,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
690
766
  return;
691
767
  }
692
768
  if (res.error) {
693
- this.showMessage('获取模型列表失败', 'error');
769
+ if (!silentError) {
770
+ this.showMessage('获取模型列表失败', 'error');
771
+ }
694
772
  this.claudeModels = [];
695
773
  this.claudeModelsSource = 'error';
696
774
  this.claudeModelsHasCurrent = true;
@@ -701,7 +779,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
701
779
  this.claudeModelsSource = res.source || 'remote';
702
780
  this.updateClaudeModelsCurrent();
703
781
  } catch (e) {
704
- this.showMessage('获取模型列表失败', 'error');
782
+ if (!silentError) {
783
+ this.showMessage('获取模型列表失败', 'error');
784
+ }
705
785
  this.claudeModels = [];
706
786
  this.claudeModelsSource = 'error';
707
787
  this.claudeModelsHasCurrent = true;
@@ -996,6 +1076,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
996
1076
  const name = typeof payload.name === 'string' ? payload.name.trim() : '';
997
1077
  const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl.trim() : '';
998
1078
  const apiKey = typeof payload.apiKey === 'string' ? payload.apiKey : '';
1079
+ const model = typeof payload.model === 'string' ? payload.model.trim() : '';
999
1080
  if (!name || !baseUrl) return '';
1000
1081
 
1001
1082
  const nameArg = this.quoteShellArg(name);
@@ -1005,7 +1086,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1005
1086
  const addCmd = apiKey
1006
1087
  ? `codexmate add ${nameArg} ${urlArg} ${keyArg}`
1007
1088
  : `codexmate add ${nameArg} ${urlArg}`;
1008
- return `${addCmd} && ${switchCmd}`;
1089
+ const modelCmd = model ? ` && codexmate use ${this.quoteShellArg(model)}` : '';
1090
+ return `${addCmd} && ${switchCmd}${modelCmd}`;
1009
1091
  },
1010
1092
 
1011
1093
  buildClaudeShareCommand(payload) {
@@ -1399,9 +1481,59 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1399
1481
  this.scheduleSessionTimelineSync();
1400
1482
  },
1401
1483
  onWindowResize() {
1484
+ this.updateCompactLayoutMode();
1402
1485
  this.updateSessionTimelineOffset();
1403
1486
  this.scheduleSessionTimelineSync();
1404
1487
  },
1488
+ shouldForceCompactLayout() {
1489
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
1490
+ return false;
1491
+ }
1492
+ const doc = typeof document !== 'undefined' ? document : null;
1493
+ const viewportWidth = Math.max(
1494
+ 0,
1495
+ Number(window.innerWidth || 0),
1496
+ Number(doc && doc.documentElement ? doc.documentElement.clientWidth : 0)
1497
+ );
1498
+ const screenWidth = Number(window.screen && window.screen.width ? window.screen.width : 0);
1499
+ const screenHeight = Number(window.screen && window.screen.height ? window.screen.height : 0);
1500
+ const shortEdge = screenWidth > 0 && screenHeight > 0
1501
+ ? Math.min(screenWidth, screenHeight)
1502
+ : 0;
1503
+ const touchPoints = Number(navigator.maxTouchPoints || 0);
1504
+ const userAgent = String(navigator.userAgent || '');
1505
+ const isMobileUa = /(Android|iPhone|iPad|iPod|Mobile)/i.test(userAgent);
1506
+ let coarsePointer = false;
1507
+ let noHover = false;
1508
+ try {
1509
+ coarsePointer = !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
1510
+ } catch (_) {}
1511
+ try {
1512
+ noHover = !!(window.matchMedia && window.matchMedia('(hover: none)').matches);
1513
+ } catch (_) {}
1514
+ return shouldForceCompactLayoutMode({
1515
+ viewportWidth,
1516
+ screenWidth,
1517
+ screenHeight,
1518
+ shortEdge,
1519
+ maxTouchPoints: touchPoints,
1520
+ userAgent,
1521
+ isMobileUa,
1522
+ coarsePointer,
1523
+ noHover
1524
+ });
1525
+ },
1526
+ applyCompactLayoutClass(enabled) {
1527
+ if (typeof document === 'undefined' || !document.body) {
1528
+ return;
1529
+ }
1530
+ document.body.classList.toggle('force-compact', !!enabled);
1531
+ },
1532
+ updateCompactLayoutMode() {
1533
+ const enabled = this.shouldForceCompactLayout();
1534
+ this.forceCompactLayout = enabled;
1535
+ this.applyCompactLayoutClass(enabled);
1536
+ },
1405
1537
  syncSessionTimelineActiveFromScroll() {
1406
1538
  const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
1407
1539
  if (!nodes.length) {
@@ -1751,17 +1883,75 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1751
1883
  }
1752
1884
  },
1753
1885
 
1754
- async switchProvider(name) {
1886
+ async quickSwitchProvider(name) {
1887
+ const target = String(name || '').trim();
1888
+ if (!target || target === this.pendingProviderSwitch) {
1889
+ return;
1890
+ }
1891
+ if (!this.providerSwitchInProgress && target === this.currentProvider) {
1892
+ return;
1893
+ }
1894
+ await this.switchProvider(target);
1895
+ },
1896
+
1897
+ async waitForCodexApplyIdle(maxWaitMs = 20000) {
1898
+ const startedAt = Date.now();
1899
+ while (this.codexApplying) {
1900
+ if ((Date.now() - startedAt) > maxWaitMs) {
1901
+ throw new Error('等待配置应用完成超时');
1902
+ }
1903
+ await new Promise((resolve) => setTimeout(resolve, 50));
1904
+ }
1905
+ },
1906
+
1907
+ async performProviderSwitch(name) {
1908
+ await this.waitForCodexApplyIdle();
1755
1909
  this.currentProvider = name;
1756
1910
  await this.loadModelsForProvider(name);
1757
1911
  if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
1758
1912
  this.currentModel = this.models[0];
1759
1913
  }
1760
1914
  if (getProviderConfigModeMeta(this.configMode)) {
1915
+ await this.waitForCodexApplyIdle();
1761
1916
  await this.applyCodexConfigDirect({ silent: true });
1762
1917
  }
1763
1918
  },
1764
1919
 
1920
+ async switchProvider(name) {
1921
+ const target = String(name || '').trim();
1922
+ if (!target) {
1923
+ return;
1924
+ }
1925
+ if (this.providerSwitchInProgress) {
1926
+ this.pendingProviderSwitch = target;
1927
+ return;
1928
+ }
1929
+ this.providerSwitchInProgress = true;
1930
+ let lastError = '';
1931
+ try {
1932
+ this.pendingProviderSwitch = '';
1933
+ const result = await runLatestOnlyQueue(target, {
1934
+ perform: async (queuedTarget) => {
1935
+ await this.performProviderSwitch(queuedTarget);
1936
+ },
1937
+ consumePending: () => {
1938
+ const queued = this.pendingProviderSwitch;
1939
+ this.pendingProviderSwitch = '';
1940
+ return queued;
1941
+ }
1942
+ });
1943
+ if (result && typeof result.lastError === 'string') {
1944
+ lastError = result.lastError;
1945
+ }
1946
+ } finally {
1947
+ this.providerSwitchInProgress = false;
1948
+ this.pendingProviderSwitch = '';
1949
+ }
1950
+ if (lastError) {
1951
+ this.showMessage(lastError, 'error');
1952
+ }
1953
+ },
1954
+
1765
1955
  async onModelChange() {
1766
1956
  await this.applyCodexConfigDirect();
1767
1957
  },
@@ -1963,8 +2153,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1963
2153
  }
1964
2154
  },
1965
2155
 
1966
- ...createSkillsMethods({ api }),
1967
-
2156
+ ...createSkillsMethods({ api }),
2157
+
1968
2158
  async openOpenclawAgentsEditor() {
1969
2159
  this.setAgentsModalContext('openclaw');
1970
2160
  this.agentsLoading = true;
@@ -2341,12 +2531,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2341
2531
  }
2342
2532
  const existing = this.claudeConfigs[name] || {};
2343
2533
  this.currentClaudeModel = model;
2344
- this.claudeConfigs[name] = {
2345
- apiKey: existing.apiKey || '',
2346
- baseUrl: existing.baseUrl || '',
2347
- model: model,
2348
- hasKey: !!existing.apiKey
2349
- };
2534
+ this.claudeConfigs[name] = this.mergeClaudeConfig(existing, { model });
2350
2535
  this.saveClaudeConfigs();
2351
2536
  this.updateClaudeModelsCurrent();
2352
2537
  if (!this.claudeConfigs[name].apiKey) {
@@ -2373,12 +2558,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2373
2558
 
2374
2559
  updateConfig() {
2375
2560
  const name = this.editingConfig.name;
2376
- this.claudeConfigs[name] = {
2377
- apiKey: this.editingConfig.apiKey,
2378
- baseUrl: this.editingConfig.baseUrl,
2379
- model: this.editingConfig.model,
2380
- hasKey: !!this.editingConfig.apiKey
2381
- };
2561
+ this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
2382
2562
  this.saveClaudeConfigs();
2383
2563
  this.showMessage('操作成功', 'success');
2384
2564
  this.closeEditConfigModal();
@@ -2394,12 +2574,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2394
2574
 
2395
2575
  async saveAndApplyConfig() {
2396
2576
  const name = this.editingConfig.name;
2397
- this.claudeConfigs[name] = {
2398
- apiKey: this.editingConfig.apiKey,
2399
- baseUrl: this.editingConfig.baseUrl,
2400
- model: this.editingConfig.model,
2401
- hasKey: !!this.editingConfig.apiKey
2402
- };
2577
+ this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
2403
2578
  this.saveClaudeConfigs();
2404
2579
 
2405
2580
  const config = this.claudeConfigs[name];
@@ -2438,12 +2613,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2438
2613
  return this.showMessage('配置已存在', 'info');
2439
2614
  }
2440
2615
 
2441
- this.claudeConfigs[name] = {
2442
- apiKey: this.newClaudeConfig.apiKey,
2443
- baseUrl: this.newClaudeConfig.baseUrl,
2444
- model: this.newClaudeConfig.model,
2445
- hasKey: !!this.newClaudeConfig.apiKey
2446
- };
2616
+ this.claudeConfigs[name] = this.mergeClaudeConfig({}, this.newClaudeConfig);
2447
2617
 
2448
2618
  this.currentClaudeConfig = name;
2449
2619
  this.saveClaudeConfigs();
@@ -2474,6 +2644,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2474
2644
  const config = this.claudeConfigs[name];
2475
2645
 
2476
2646
  if (!config.apiKey) {
2647
+ if (config.externalCredentialType) {
2648
+ return this.showMessage('检测到外部 Claude 认证状态;当前仅支持展示,若需由 codexmate 接管请补充 API Key', 'info');
2649
+ }
2477
2650
  return this.showMessage('请先配置 API Key', 'error');
2478
2651
  }
2479
2652
 
package/web-ui/index.html CHANGED
@@ -282,6 +282,20 @@
282
282
  <span class="value">{{ sessionsList.length }}</span>
283
283
  </div>
284
284
  </div>
285
+ <div
286
+ v-if="!sessionStandalone && mainTab === 'config' && isProviderConfigMode && forceCompactLayout && !loading && !initError && providersList.length > 1"
287
+ class="provider-fast-switch">
288
+ <label class="provider-fast-switch-label" for="provider-fast-switch-select">快速切换提供商</label>
289
+ <select
290
+ id="provider-fast-switch-select"
291
+ class="provider-fast-switch-select"
292
+ :value="currentProvider"
293
+ @change="quickSwitchProvider($event.target.value)">
294
+ <option v-for="provider in providersList" :key="'quick-switch-' + provider.name" :value="provider.name">
295
+ {{ provider.name }}
296
+ </option>
297
+ </select>
298
+ </div>
285
299
 
286
300
  <div v-if="false && mainTab === 'config' && !sessionStandalone" class="config-subtabs">
287
301
  <button :class="['config-subtab', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
@@ -672,7 +686,7 @@
672
686
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
673
687
  </svg>
674
688
  </button>
675
- <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" title="分享导入命令" aria-label="Share import command">
689
+ <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled title="分享导入命令(暂时禁用)" aria-label="Share import command">
676
690
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
677
691
  <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
678
692
  <path d="M16 6l-4-4-4 4"/>
package/web-ui/logic.mjs CHANGED
@@ -8,33 +8,63 @@ export function normalizeClaudeConfig(config) {
8
8
  return {
9
9
  apiKey: normalizeClaudeValue(safe.apiKey),
10
10
  baseUrl: normalizeClaudeValue(safe.baseUrl),
11
- model: normalizeClaudeValue(safe.model)
11
+ model: normalizeClaudeValue(safe.model),
12
+ authToken: normalizeClaudeValue(safe.authToken),
13
+ useKey: normalizeClaudeValue(safe.useKey),
14
+ externalCredentialType: normalizeClaudeValue(safe.externalCredentialType)
12
15
  };
13
16
  }
14
17
 
15
18
  export function normalizeClaudeSettingsEnv(env) {
16
19
  const safe = env && typeof env === 'object' ? env : {};
20
+ const apiKey = normalizeClaudeValue(safe.ANTHROPIC_API_KEY);
21
+ const authToken = normalizeClaudeValue(safe.ANTHROPIC_AUTH_TOKEN);
22
+ const useKey = normalizeClaudeValue(safe.CLAUDE_CODE_USE_KEY);
17
23
  return {
18
- apiKey: normalizeClaudeValue(safe.ANTHROPIC_API_KEY),
24
+ apiKey,
19
25
  baseUrl: normalizeClaudeValue(safe.ANTHROPIC_BASE_URL),
20
- model: normalizeClaudeValue(safe.ANTHROPIC_MODEL)
26
+ model: normalizeClaudeValue(safe.ANTHROPIC_MODEL) || 'glm-4.7',
27
+ authToken,
28
+ useKey,
29
+ externalCredentialType: apiKey
30
+ ? ''
31
+ : (authToken ? 'auth-token' : (useKey ? 'claude-code-use-key' : ''))
21
32
  };
22
33
  }
23
34
 
35
+ function normalizeClaudeComparableUrl(value) {
36
+ const trimmed = normalizeClaudeValue(value);
37
+ if (!trimmed) return '';
38
+ return trimmed.replace(/\/+$/g, '');
39
+ }
40
+
41
+ function hasClaudeCredential(config = {}) {
42
+ return !!(config.apiKey || config.authToken || config.useKey);
43
+ }
44
+
24
45
  export function matchClaudeConfigFromSettings(claudeConfigs = {}, env = {}) {
25
46
  const normalizedSettings = normalizeClaudeSettingsEnv(env);
26
- if (!normalizedSettings.apiKey || !normalizedSettings.baseUrl || !normalizedSettings.model) {
47
+ if (!normalizedSettings.baseUrl || !normalizedSettings.model || !hasClaudeCredential(normalizedSettings)) {
27
48
  return '';
28
49
  }
50
+ const comparableSettingsUrl = normalizeClaudeComparableUrl(normalizedSettings.baseUrl);
29
51
  const entries = Object.entries(claudeConfigs || {});
30
52
  for (const [name, config] of entries) {
31
53
  const normalizedConfig = normalizeClaudeConfig(config);
32
- if (!normalizedConfig.apiKey || !normalizedConfig.baseUrl || !normalizedConfig.model) {
54
+ if (!normalizedConfig.baseUrl || !normalizedConfig.model) {
33
55
  continue;
34
56
  }
35
- if (normalizedConfig.apiKey === normalizedSettings.apiKey
36
- && normalizedConfig.baseUrl === normalizedSettings.baseUrl
37
- && normalizedConfig.model === normalizedSettings.model) {
57
+ if (normalizeClaudeComparableUrl(normalizedConfig.baseUrl) !== comparableSettingsUrl
58
+ || normalizedConfig.model !== normalizedSettings.model) {
59
+ continue;
60
+ }
61
+ if (normalizedSettings.apiKey && normalizedConfig.apiKey === normalizedSettings.apiKey) {
62
+ return name;
63
+ }
64
+ if (!normalizedSettings.apiKey
65
+ && normalizedConfig.apiKey === ''
66
+ && normalizedConfig.externalCredentialType
67
+ && normalizedConfig.externalCredentialType === normalizedSettings.externalCredentialType) {
38
68
  return name;
39
69
  }
40
70
  }
@@ -43,18 +73,30 @@ export function matchClaudeConfigFromSettings(claudeConfigs = {}, env = {}) {
43
73
 
44
74
  export function findDuplicateClaudeConfigName(claudeConfigs = {}, config) {
45
75
  const normalized = normalizeClaudeConfig(config);
46
- if (!normalized.apiKey || !normalized.baseUrl || !normalized.model) {
76
+ if (!normalized.baseUrl || !normalized.model) {
77
+ return '';
78
+ }
79
+ const comparableUrl = normalizeClaudeComparableUrl(normalized.baseUrl);
80
+ const isExternal = !normalized.apiKey && !!normalized.externalCredentialType;
81
+ if (!normalized.apiKey && !isExternal) {
47
82
  return '';
48
83
  }
49
84
  const entries = Object.entries(claudeConfigs || {});
50
85
  for (const [name, existing] of entries) {
51
86
  const normalizedExisting = normalizeClaudeConfig(existing);
52
- if (!normalizedExisting.apiKey || !normalizedExisting.baseUrl || !normalizedExisting.model) {
87
+ if (!normalizedExisting.baseUrl || !normalizedExisting.model) {
88
+ continue;
89
+ }
90
+ if (normalizeClaudeComparableUrl(normalizedExisting.baseUrl) !== comparableUrl
91
+ || normalizedExisting.model !== normalized.model) {
53
92
  continue;
54
93
  }
55
- if (normalizedExisting.apiKey === normalized.apiKey
56
- && normalizedExisting.baseUrl === normalized.baseUrl
57
- && normalizedExisting.model === normalized.model) {
94
+ if (normalized.apiKey && normalizedExisting.apiKey === normalized.apiKey) {
95
+ return name;
96
+ }
97
+ if (isExternal
98
+ && !normalizedExisting.apiKey
99
+ && normalizedExisting.externalCredentialType === normalized.externalCredentialType) {
58
100
  return name;
59
101
  }
60
102
  }
@@ -126,6 +168,64 @@ export function buildSpeedTestIssue(name, result) {
126
168
  return null;
127
169
  }
128
170
 
171
+ export async function runLatestOnlyQueue(initialTarget, options = {}) {
172
+ const perform = typeof options.perform === 'function'
173
+ ? options.perform
174
+ : async () => {};
175
+ const consumePending = typeof options.consumePending === 'function'
176
+ ? options.consumePending
177
+ : () => '';
178
+ let currentTarget = typeof initialTarget === 'string' ? initialTarget.trim() : '';
179
+ let lastError = '';
180
+
181
+ while (currentTarget) {
182
+ try {
183
+ await perform(currentTarget);
184
+ lastError = '';
185
+ } catch (e) {
186
+ lastError = e && e.message ? e.message : 'queue task failed';
187
+ }
188
+ const queued = String(consumePending() || '').trim();
189
+ if (!queued || queued === currentTarget) {
190
+ break;
191
+ }
192
+ currentTarget = queued;
193
+ }
194
+
195
+ return {
196
+ lastTarget: currentTarget,
197
+ lastError
198
+ };
199
+ }
200
+
201
+ export function shouldForceCompactLayoutMode(options = {}) {
202
+ const viewportWidth = Number(options.viewportWidth || 0);
203
+ const screenWidth = Number(options.screenWidth || 0);
204
+ const screenHeight = Number(options.screenHeight || 0);
205
+ const shortEdge = Number(options.shortEdge || (screenWidth > 0 && screenHeight > 0 ? Math.min(screenWidth, screenHeight) : 0));
206
+ const maxTouchPoints = Number(options.maxTouchPoints || 0);
207
+ const userAgent = typeof options.userAgent === 'string' ? options.userAgent : '';
208
+ const isMobileUa = typeof options.isMobileUa === 'boolean'
209
+ ? options.isMobileUa
210
+ : /(Android|iPhone|iPad|iPod|Mobile)/i.test(userAgent);
211
+ const coarsePointer = !!options.coarsePointer;
212
+ const noHover = !!options.noHover;
213
+ const isSmallPhysicalScreen = shortEdge > 0 && shortEdge <= 920;
214
+ const isNarrowViewport = viewportWidth > 0 && viewportWidth <= 960;
215
+ const pointerSuggestsTouchOnly = coarsePointer && noHover;
216
+
217
+ if (isMobileUa) {
218
+ return isNarrowViewport || isSmallPhysicalScreen;
219
+ }
220
+ if (!pointerSuggestsTouchOnly) {
221
+ return false;
222
+ }
223
+ if (maxTouchPoints <= 0) {
224
+ return false;
225
+ }
226
+ return isSmallPhysicalScreen;
227
+ }
228
+
129
229
  // Session filtering helpers
130
230
  export function isSessionQueryEnabled(source) {
131
231
  const normalized = normalizeSessionSource(source, '');