codexmate 0.0.16 → 0.0.18

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/app.js CHANGED
@@ -4,16 +4,27 @@
4
4
  normalizeClaudeSettingsEnv,
5
5
  matchClaudeConfigFromSettings,
6
6
  findDuplicateClaudeConfigName,
7
+ buildAgentsDiffPreview,
8
+ buildAgentsDiffPreviewRequest,
9
+ isAgentsDiffPreviewPayloadTooLarge,
10
+ shouldApplyAgentsDiffPreviewResponse,
7
11
  formatLatency,
8
12
  buildSpeedTestIssue,
9
13
  isSessionQueryEnabled,
10
- buildSessionListParams,
11
14
  normalizeSessionSource,
12
15
  normalizeSessionPathFilter,
13
16
  buildSessionFilterCacheState,
14
17
  buildSessionTimelineNodes,
15
- normalizeSessionMessageRole
18
+ normalizeSessionMessageRole,
19
+ runLatestOnlyQueue,
20
+ shouldForceCompactLayoutMode
16
21
  } from './logic.mjs';
22
+ import {
23
+ switchMainTab as switchMainTabHelper,
24
+ loadSessions as loadSessionsHelper,
25
+ loadActiveSessionDetail as loadActiveSessionDetailHelper,
26
+ loadMoreSessionMessages as loadMoreSessionMessagesHelper
27
+ } from './session-helpers.mjs';
17
28
  import {
18
29
  CONFIG_MODE_SET,
19
30
  getProviderConfigModeMeta,
@@ -41,6 +52,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
41
52
  const API_BASE = (location && location.origin && location.origin !== 'null')
42
53
  ? location.origin
43
54
  : 'http://localhost:3737';
55
+ const SESSION_TRASH_LIST_LIMIT = 500;
56
+ const SESSION_TRASH_PAGE_SIZE = 200;
44
57
  const DEFAULT_OPENCLAW_TEMPLATE = `{
45
58
  // OpenClaw config (JSON5)
46
59
  agent: {
@@ -53,15 +66,45 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
53
66
  }
54
67
  }`;
55
68
 
56
- async function api(action, params = {}) {
57
- const res = await fetch(`${API_BASE}/api`, {
69
+ async function postApi(action, params = {}) {
70
+ return await fetch(`${API_BASE}/api`, {
58
71
  method: 'POST',
59
72
  headers: { 'Content-Type': 'application/json' },
60
73
  body: JSON.stringify({ action, params })
61
74
  });
75
+ }
76
+
77
+ async function api(action, params = {}) {
78
+ const res = await postApi(action, params);
62
79
  return await res.json();
63
80
  }
64
81
 
82
+ async function apiWithMeta(action, params = {}) {
83
+ const res = await postApi(action, params);
84
+ const contentType = String(res.headers.get('content-type') || '').toLowerCase();
85
+ if (contentType.includes('application/json')) {
86
+ try {
87
+ const payload = await res.json();
88
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
89
+ return { ...payload, ok: res.ok, status: res.status };
90
+ }
91
+ return { ok: res.ok, status: res.status, data: payload };
92
+ } catch (error) {
93
+ if (res.status === 413) {
94
+ return { ok: false, status: 413, errorCode: 'payload-too-large' };
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+ const error = await res.text();
100
+ return {
101
+ ok: res.ok,
102
+ status: res.status,
103
+ error,
104
+ errorCode: res.status === 413 ? 'payload-too-large' : ''
105
+ };
106
+ }
107
+
65
108
  const app = createApp({
66
109
  data() {
67
110
  return {
@@ -95,6 +138,13 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
95
138
  showAgentsModal: false,
96
139
  showSkillsModal: false,
97
140
  showInstallModal: false,
141
+ showConfirmDialog: false,
142
+ confirmDialogTitle: '',
143
+ confirmDialogMessage: '',
144
+ confirmDialogConfirmText: '确认',
145
+ confirmDialogCancelText: '取消',
146
+ confirmDialogDanger: false,
147
+ confirmDialogResolver: null,
98
148
  configTemplateContent: '',
99
149
  configTemplateApplying: false,
100
150
  codexApplying: false,
@@ -104,6 +154,19 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
104
154
  agentsLineEnding: '\n',
105
155
  agentsLoading: false,
106
156
  agentsSaving: false,
157
+ agentsOriginalContent: '',
158
+ agentsDiffVisible: false,
159
+ agentsDiffLoading: false,
160
+ agentsDiffError: '',
161
+ agentsDiffLines: [],
162
+ agentsDiffStats: {
163
+ added: 0,
164
+ removed: 0,
165
+ unchanged: 0
166
+ },
167
+ agentsDiffTruncated: false,
168
+ agentsDiffHasChangesValue: false,
169
+ agentsDiffFingerprint: '',
107
170
  agentsContext: 'codex',
108
171
  agentsModalTitle: 'AGENTS.md 编辑器',
109
172
  agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
@@ -120,7 +183,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
120
183
  skillsImporting: false,
121
184
  skillsZipImporting: false,
122
185
  skillsExporting: false,
186
+ sessionPinnedMap: {},
123
187
  sessionsList: [],
188
+ sessionsLoadedOnce: false,
124
189
  sessionsLoading: false,
125
190
  sessionFilterSource: 'all',
126
191
  sessionPathFilter: '',
@@ -150,13 +215,31 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
150
215
  activeSessionDetailClipped: false,
151
216
  sessionDetailLoading: false,
152
217
  sessionDetailRequestSeq: 0,
218
+ sessionDetailInitialMessageLimit: 80,
219
+ sessionDetailFetchStep: 80,
220
+ sessionDetailMessageLimit: 80,
221
+ sessionDetailMessageLimitCap: 1000,
153
222
  sessionTimelineActiveKey: '',
154
223
  sessionTimelineRafId: 0,
224
+ sessionTimelineLastSyncAt: 0,
225
+ sessionTimelineLastScrollTop: 0,
226
+ sessionTimelineLastAnchorY: 0,
227
+ sessionTimelineLastDirection: 0,
228
+ sessionTimelineEnabled: true,
155
229
  sessionMessageRefMap: Object.create(null),
230
+ sessionMessageRefBinderMap: Object.create(null),
156
231
  sessionPreviewScrollEl: null,
157
232
  sessionPreviewContainerEl: null,
158
233
  sessionPreviewHeaderEl: null,
159
234
  sessionPreviewHeaderResizeObserver: null,
235
+ sessionListRenderEnabled: false,
236
+ sessionPreviewRenderEnabled: false,
237
+ sessionTabRenderTicket: 0,
238
+ sessionPreviewVisibleCount: 0,
239
+ sessionPreviewInitialBatchSize: 12,
240
+ sessionPreviewLoadStep: 24,
241
+ sessionPreviewPendingVisibleCount: 0,
242
+ sessionPreviewLoadingMore: false,
160
243
  sessionStandalone: false,
161
244
  sessionStandaloneError: '',
162
245
  sessionStandaloneText: '',
@@ -170,6 +253,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
170
253
  claudeSpeedLoading: {},
171
254
  claudeShareLoading: {},
172
255
  providerShareLoading: {},
256
+ providerSwitchInProgress: false,
257
+ pendingProviderSwitch: '',
173
258
  installPackageManager: 'npm',
174
259
  installCommandAction: 'install',
175
260
  installRegistryPreset: 'default',
@@ -271,6 +356,22 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
271
356
  codexDownloadLoading: false,
272
357
  codexDownloadProgress: 0,
273
358
  codexDownloadTimer: null,
359
+ settingsTab: 'backup',
360
+ sessionTrashItems: [],
361
+ sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
362
+ sessionTrashTotalCount: 0,
363
+ sessionTrashCountLoadedOnce: false,
364
+ sessionTrashLoadedOnce: false,
365
+ sessionTrashLastLoadFailed: false,
366
+ sessionTrashCountRequestToken: 0,
367
+ sessionTrashListRequestToken: 0,
368
+ sessionTrashCountPendingOptions: null,
369
+ sessionTrashPendingOptions: null,
370
+ sessionTrashCountLoading: false,
371
+ sessionTrashLoading: false,
372
+ sessionTrashRestoring: {},
373
+ sessionTrashPurging: {},
374
+ sessionTrashClearing: false,
274
375
  claudeImportLoading: false,
275
376
  codexImportLoading: false,
276
377
  codexAuthProfiles: [],
@@ -291,11 +392,13 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
291
392
  proxyStarting: false,
292
393
  proxyStopping: false,
293
394
  proxyApplying: false,
294
- showProxyAdvanced: false
395
+ showProxyAdvanced: false,
396
+ forceCompactLayout: false
295
397
  }
296
398
  },
297
399
  mounted() {
298
400
  this.initSessionStandalone();
401
+ this.updateCompactLayoutMode();
299
402
  const savedSessionYolo = localStorage.getItem('codexmateSessionResumeYolo');
300
403
  if (savedSessionYolo === '0' || savedSessionYolo === 'false') {
301
404
  this.sessionResumeWithYolo = false;
@@ -303,7 +406,10 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
303
406
  this.sessionResumeWithYolo = true;
304
407
  }
305
408
  this.restoreSessionFilterCache();
409
+ this.restoreSessionPinnedMap();
306
410
  window.addEventListener('resize', this.onWindowResize);
411
+ window.addEventListener('keydown', this.handleGlobalKeydown);
412
+ window.addEventListener('beforeunload', this.handleBeforeUnload);
307
413
  const savedConfigs = localStorage.getItem('claudeConfigs');
308
414
  if (savedConfigs) {
309
415
  try {
@@ -340,24 +446,104 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
340
446
  this.loadAll();
341
447
  },
342
448
  beforeUnmount() {
343
- this.cancelSessionTimelineSync();
449
+ this.teardownSessionTabRender();
450
+ this.cancelScheduledSessionTabDeferredTeardown();
344
451
  this.disconnectSessionPreviewHeaderResizeObserver();
345
452
  window.removeEventListener('resize', this.onWindowResize);
453
+ window.removeEventListener('keydown', this.handleGlobalKeydown);
454
+ window.removeEventListener('beforeunload', this.handleBeforeUnload);
455
+ this.applyCompactLayoutClass(false);
346
456
  this.sessionPreviewScrollEl = null;
347
457
  this.sessionPreviewContainerEl = null;
348
458
  this.sessionPreviewHeaderEl = null;
349
- this.sessionMessageRefMap = Object.create(null);
459
+ this.clearSessionTimelineRefs();
350
460
  },
351
461
 
352
462
  computed: {
353
463
  isSessionQueryEnabled() {
354
464
  return isSessionQueryEnabled(this.sessionFilterSource);
355
465
  },
466
+ activeSessionExportKey() {
467
+ return this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
468
+ },
469
+ sortedSessionsList() {
470
+ const list = Array.isArray(this.sessionsList) ? this.sessionsList : [];
471
+ if (list.length === 0) return [];
472
+ const pinnedMap = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
473
+ ? this.sessionPinnedMap
474
+ : {};
475
+ let hasPinned = false;
476
+ const decorated = list.map((session, index) => {
477
+ const key = session ? this.getSessionExportKey(session) : '';
478
+ const rawPinnedAt = key ? pinnedMap[key] : 0;
479
+ const pinnedAt = Number.isFinite(Number(rawPinnedAt))
480
+ ? Math.floor(Number(rawPinnedAt))
481
+ : 0;
482
+ const isPinned = pinnedAt > 0;
483
+ if (isPinned) {
484
+ hasPinned = true;
485
+ }
486
+ return { session, index, pinnedAt, isPinned };
487
+ });
488
+ if (!hasPinned) return list;
489
+ decorated.sort((a, b) => {
490
+ if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1;
491
+ if (a.isPinned && a.pinnedAt !== b.pinnedAt) return b.pinnedAt - a.pinnedAt;
492
+ return a.index - b.index;
493
+ });
494
+ return decorated.map(item => item.session);
495
+ },
496
+ activeSessionVisibleMessages() {
497
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
498
+ return [];
499
+ }
500
+ const list = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages : [];
501
+ const rawCount = Number(this.sessionPreviewVisibleCount);
502
+ const visibleCount = Number.isFinite(rawCount)
503
+ ? Math.max(0, Math.floor(rawCount))
504
+ : 0;
505
+ if (visibleCount <= 0) {
506
+ if (!list.length) return [];
507
+ // Defensive fallback: avoid getting stuck in "正在渲染会话内容..."
508
+ // when visible count has not been primed yet.
509
+ return list.slice(0, Math.min(8, list.length));
510
+ }
511
+ if (visibleCount >= list.length) return list;
512
+ return list.slice(0, visibleCount);
513
+ },
514
+ canLoadMoreSessionMessages() {
515
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
516
+ return false;
517
+ }
518
+ const total = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages.length : 0;
519
+ const visible = Array.isArray(this.activeSessionVisibleMessages) ? this.activeSessionVisibleMessages.length : 0;
520
+ return total > visible;
521
+ },
522
+ sessionPreviewRemainingCount() {
523
+ const total = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages.length : 0;
524
+ const visible = Array.isArray(this.activeSessionVisibleMessages) ? this.activeSessionVisibleMessages.length : 0;
525
+ return Math.max(0, total - visible);
526
+ },
356
527
  sessionTimelineNodes() {
357
- return buildSessionTimelineNodes(this.activeSessionMessages, {
528
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
529
+ return [];
530
+ }
531
+ return buildSessionTimelineNodes(this.activeSessionVisibleMessages, {
358
532
  getKey: (message, index) => this.getRecordRenderKey(message, index)
359
533
  });
360
534
  },
535
+ sessionTimelineNodeKeyMap() {
536
+ const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
537
+ if (!nodes.length) {
538
+ return Object.create(null);
539
+ }
540
+ const map = Object.create(null);
541
+ for (const node of nodes) {
542
+ if (!node || !node.key) continue;
543
+ map[node.key] = true;
544
+ }
545
+ return map;
546
+ },
361
547
  sessionTimelineActiveTitle() {
362
548
  if (!this.sessionTimelineActiveKey) return '';
363
549
  const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
@@ -370,6 +556,15 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
370
556
  }
371
557
  return '当前来源暂不支持关键词检索';
372
558
  },
559
+ agentsDiffHasChanges() {
560
+ if (this.agentsDiffTruncated) {
561
+ return !!this.agentsDiffHasChangesValue;
562
+ }
563
+ const stats = this.agentsDiffStats || {};
564
+ const added = Number(stats.added || 0);
565
+ const removed = Number(stats.removed || 0);
566
+ return added > 0 || removed > 0;
567
+ },
373
568
  claudeModelHasList() {
374
569
  return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
375
570
  },
@@ -409,6 +604,29 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
409
604
  installRegistryPreview() {
410
605
  return this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
411
606
  },
607
+ visibleSessionTrashItems() {
608
+ const items = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
609
+ const visibleCount = Number(this.sessionTrashVisibleCount);
610
+ const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0
611
+ ? Math.floor(visibleCount)
612
+ : SESSION_TRASH_PAGE_SIZE;
613
+ return items.slice(0, safeVisibleCount);
614
+ },
615
+ sessionTrashHasMoreItems() {
616
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
617
+ return this.visibleSessionTrashItems.length < totalItems;
618
+ },
619
+ sessionTrashHiddenCount() {
620
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
621
+ return Math.max(0, totalItems - this.visibleSessionTrashItems.length);
622
+ },
623
+ sessionTrashCount() {
624
+ const totalCount = Number(this.sessionTrashTotalCount);
625
+ if (Number.isFinite(totalCount) && totalCount >= 0) {
626
+ return Math.max(0, Math.floor(totalCount));
627
+ }
628
+ return Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
629
+ },
412
630
  ...createSkillsComputed(),
413
631
 
414
632
  ...createConfigModeComputed(),
@@ -542,7 +760,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
542
760
  }
543
761
  },
544
762
 
545
- async loadModelsForProvider(providerName) {
763
+ async loadModelsForProvider(providerName, options = {}) {
764
+ const silentError = !!options.silentError;
546
765
  this.codexModelsLoading = true;
547
766
  if (!providerName) {
548
767
  this.models = [];
@@ -560,7 +779,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
560
779
  return;
561
780
  }
562
781
  if (res.error) {
563
- this.showMessage('获取模型列表失败', 'error');
782
+ if (!silentError) {
783
+ this.showMessage('获取模型列表失败', 'error');
784
+ }
564
785
  this.models = [];
565
786
  this.modelsSource = 'error';
566
787
  this.modelsHasCurrent = true;
@@ -571,7 +792,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
571
792
  this.modelsSource = res.source || 'remote';
572
793
  this.modelsHasCurrent = !!this.currentModel && list.includes(this.currentModel);
573
794
  } catch (e) {
574
- this.showMessage('获取模型列表失败', 'error');
795
+ if (!silentError) {
796
+ this.showMessage('获取模型列表失败', 'error');
797
+ }
575
798
  this.models = [];
576
799
  this.modelsSource = 'error';
577
800
  this.modelsHasCurrent = true;
@@ -603,15 +826,58 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
603
826
  return findDuplicateClaudeConfigName(this.claudeConfigs, config);
604
827
  },
605
828
 
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;
829
+ mergeClaudeConfig(existing = {}, updates = {}) {
830
+ const previous = this.normalizeClaudeConfig(existing);
831
+ const next = this.normalizeClaudeConfig({ ...existing, ...updates });
832
+ const externalCredentialType = next.apiKey
833
+ ? ''
834
+ : (next.externalCredentialType || previous.externalCredentialType || '');
835
+ return {
836
+ apiKey: next.apiKey,
837
+ baseUrl: next.baseUrl,
838
+ model: next.model || previous.model || 'glm-4.7',
839
+ hasKey: !!(next.apiKey || externalCredentialType),
840
+ externalCredentialType
841
+ };
842
+ },
843
+
844
+ buildClaudeImportedConfigName(baseUrl) {
845
+ const normalizedUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
846
+ if (!normalizedUrl) return '导入配置';
847
+ try {
848
+ const parsed = new URL(normalizedUrl);
849
+ const host = typeof parsed.host === 'string' ? parsed.host.trim() : '';
850
+ if (host) return `导入-${host}`;
851
+ } catch (_) {
852
+ // keep generic fallback name
613
853
  }
854
+ return '导入配置';
855
+ },
856
+
857
+ ensureClaudeConfigFromSettings(env = {}) {
858
+ const normalized = this.normalizeClaudeSettingsEnv(env);
859
+ const hasCredential = !!(normalized.apiKey || normalized.authToken || normalized.useKey);
860
+ if (!normalized.baseUrl || !hasCredential) return '';
861
+
862
+ const duplicateName = this.findDuplicateClaudeConfigName(normalized);
863
+ if (duplicateName) return duplicateName;
864
+
865
+ const preferredName = this.buildClaudeImportedConfigName(normalized.baseUrl);
866
+ let candidateName = preferredName;
867
+ let suffix = 2;
868
+ while (this.claudeConfigs[candidateName]) {
869
+ candidateName = `${preferredName}-${suffix}`;
870
+ suffix += 1;
871
+ }
872
+
873
+ this.claudeConfigs[candidateName] = this.mergeClaudeConfig({}, normalized);
874
+ this.saveClaudeConfigs();
875
+ return candidateName;
876
+ },
877
+
878
+ async refreshClaudeSelectionFromSettings(options = {}) {
614
879
  const silent = !!options.silent;
880
+ const silentModelError = !!options.silentModelError || silent;
615
881
  try {
616
882
  const res = await api('get-claude-settings');
617
883
  if (res && res.error) {
@@ -625,7 +891,18 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
625
891
  if (this.currentClaudeConfig !== matchName) {
626
892
  this.currentClaudeConfig = matchName;
627
893
  }
628
- this.refreshClaudeModelContext();
894
+ this.refreshClaudeModelContext({ silentError: silentModelError });
895
+ return;
896
+ }
897
+ const importedName = this.ensureClaudeConfigFromSettings((res && res.env) || {});
898
+ if (importedName) {
899
+ if (this.currentClaudeConfig !== importedName) {
900
+ this.currentClaudeConfig = importedName;
901
+ }
902
+ this.refreshClaudeModelContext({ silentError: silentModelError });
903
+ if (!silent) {
904
+ this.showMessage(`检测到外部 Claude 配置,已自动导入:${importedName}`, 'success');
905
+ }
629
906
  return;
630
907
  }
631
908
  this.currentClaudeConfig = '';
@@ -649,9 +926,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
649
926
  this.currentClaudeModel = config && config.model ? config.model : '';
650
927
  },
651
928
 
652
- refreshClaudeModelContext() {
929
+ refreshClaudeModelContext(options = {}) {
653
930
  this.syncClaudeModelFromConfig();
654
- this.loadClaudeModels();
931
+ return this.loadClaudeModels(options);
655
932
  },
656
933
 
657
934
  resetClaudeModelsState() {
@@ -666,7 +943,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
666
943
  this.claudeModelsHasCurrent = !!currentModel && this.claudeModels.includes(currentModel);
667
944
  },
668
945
 
669
- async loadClaudeModels() {
946
+ async loadClaudeModels(options = {}) {
947
+ const silentError = !!options.silentError;
670
948
  const config = this.getCurrentClaudeConfig();
671
949
  if (!config) {
672
950
  this.resetClaudeModelsState();
@@ -674,11 +952,20 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
674
952
  }
675
953
  const baseUrl = (config.baseUrl || '').trim();
676
954
  const apiKey = (config.apiKey || '').trim();
955
+ const externalCredentialType = typeof config.externalCredentialType === 'string'
956
+ ? config.externalCredentialType.trim()
957
+ : '';
677
958
 
678
959
  if (!baseUrl) {
679
960
  this.resetClaudeModelsState();
680
961
  return;
681
962
  }
963
+ if (!apiKey && externalCredentialType) {
964
+ this.claudeModels = [];
965
+ this.claudeModelsSource = 'unlimited';
966
+ this.claudeModelsHasCurrent = true;
967
+ return;
968
+ }
682
969
 
683
970
  this.claudeModelsLoading = true;
684
971
  try {
@@ -690,7 +977,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
690
977
  return;
691
978
  }
692
979
  if (res.error) {
693
- this.showMessage('获取模型列表失败', 'error');
980
+ if (!silentError) {
981
+ this.showMessage('获取模型列表失败', 'error');
982
+ }
694
983
  this.claudeModels = [];
695
984
  this.claudeModelsSource = 'error';
696
985
  this.claudeModelsHasCurrent = true;
@@ -701,7 +990,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
701
990
  this.claudeModelsSource = res.source || 'remote';
702
991
  this.updateClaudeModelsCurrent();
703
992
  } catch (e) {
704
- this.showMessage('获取模型列表失败', 'error');
993
+ if (!silentError) {
994
+ this.showMessage('获取模型列表失败', 'error');
995
+ }
705
996
  this.claudeModels = [];
706
997
  this.claudeModelsSource = 'error';
707
998
  this.claudeModelsHasCurrent = true;
@@ -727,21 +1018,413 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
727
1018
  const normalizedMode = typeof mode === 'string'
728
1019
  ? mode.trim().toLowerCase()
729
1020
  : '';
730
- this.mainTab = 'config';
731
1021
  this.configMode = CONFIG_MODE_SET.has(normalizedMode) ? normalizedMode : 'codex';
732
- if (this.configMode === 'claude') {
733
- this.refreshClaudeModelContext();
1022
+ if (this.mainTab === 'config') {
1023
+ if (this.configMode === 'claude') {
1024
+ const expectedMainTab = 'config';
1025
+ const expectedConfigMode = 'claude';
1026
+ const refresh = () => {
1027
+ if (this.mainTab !== expectedMainTab || this.configMode !== expectedConfigMode) {
1028
+ return;
1029
+ }
1030
+ this.refreshClaudeModelContext();
1031
+ };
1032
+ if (typeof this.scheduleAfterFrame === 'function') {
1033
+ this.scheduleAfterFrame(refresh);
1034
+ } else {
1035
+ refresh();
1036
+ }
1037
+ }
1038
+ this.scheduleAfterFrame(() => {
1039
+ this.clearMainTabSwitchIntent('config');
1040
+ });
1041
+ return;
734
1042
  }
1043
+ this.switchMainTab('config');
735
1044
  },
736
1045
 
1046
+ ensureMainTabSwitchState() {
1047
+ if (this.__mainTabSwitchState) {
1048
+ return this.__mainTabSwitchState;
1049
+ }
1050
+ this.__mainTabSwitchState = {
1051
+ intent: '',
1052
+ pendingTarget: '',
1053
+ ticket: 0
1054
+ };
1055
+ return this.__mainTabSwitchState;
1056
+ },
1057
+ ensureImmediateNavDomState() {
1058
+ if (typeof document === 'undefined') {
1059
+ return {
1060
+ navNodes: [],
1061
+ sessionPanelEl: null
1062
+ };
1063
+ }
1064
+ if (!this.__immediateNavDomState) {
1065
+ this.__immediateNavDomState = {
1066
+ navNodes: [],
1067
+ sessionPanelEl: null
1068
+ };
1069
+ }
1070
+ const state = this.__immediateNavDomState;
1071
+ const needsNavRefresh = !Array.isArray(state.navNodes)
1072
+ || !state.navNodes.length
1073
+ || state.navNodes.some((node) => !node || !node.isConnected);
1074
+ if (needsNavRefresh) {
1075
+ state.navNodes = Array.from(document.querySelectorAll('[data-main-tab]'));
1076
+ }
1077
+ if (!state.sessionPanelEl || !state.sessionPanelEl.isConnected) {
1078
+ state.sessionPanelEl = document.getElementById('panel-sessions');
1079
+ }
1080
+ return state;
1081
+ },
1082
+ setMainTabSwitchIntent(tab) {
1083
+ const normalizedTab = typeof tab === 'string'
1084
+ ? tab.trim().toLowerCase()
1085
+ : '';
1086
+ if (!normalizedTab) return;
1087
+ const state = this.ensureMainTabSwitchState();
1088
+ state.intent = normalizedTab;
1089
+ },
1090
+ applyImmediateNavIntent(tab, configMode = '') {
1091
+ if (typeof document === 'undefined') return;
1092
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1093
+ if (!normalizedTab) return;
1094
+ const normalizedMode = typeof configMode === 'string' ? configMode.trim().toLowerCase() : '';
1095
+ const domState = this.ensureImmediateNavDomState();
1096
+ const nodes = Array.isArray(domState.navNodes) ? domState.navNodes : [];
1097
+ for (const node of nodes) {
1098
+ if (!node || !node.classList) continue;
1099
+ const nodeTab = String(node.getAttribute('data-main-tab') || '').trim().toLowerCase();
1100
+ const nodeMode = String(node.getAttribute('data-config-mode') || '').trim().toLowerCase();
1101
+ let shouldActivate = nodeTab === normalizedTab;
1102
+ if (shouldActivate && normalizedTab === 'config') {
1103
+ shouldActivate = nodeMode ? nodeMode === normalizedMode : false;
1104
+ }
1105
+ node.classList.toggle('nav-intent-active', !!shouldActivate);
1106
+ node.classList.toggle('nav-intent-inactive', !shouldActivate);
1107
+ }
1108
+ },
1109
+ clearImmediateNavIntent() {
1110
+ if (typeof document === 'undefined') return;
1111
+ const domState = this.ensureImmediateNavDomState();
1112
+ const nodes = Array.isArray(domState.navNodes) ? domState.navNodes : [];
1113
+ for (const node of nodes) {
1114
+ if (!node || !node.classList) continue;
1115
+ node.classList.remove('nav-intent-active');
1116
+ node.classList.remove('nav-intent-inactive');
1117
+ }
1118
+ },
1119
+ setSessionPanelFastHidden(hidden) {
1120
+ if (typeof document === 'undefined') return;
1121
+ const domState = this.ensureImmediateNavDomState();
1122
+ const panel = domState.sessionPanelEl;
1123
+ if (!panel || !panel.classList) return;
1124
+ panel.classList.toggle('session-panel-fast-hidden', !!hidden);
1125
+ },
1126
+ isSessionPanelFastHidden() {
1127
+ if (typeof document === 'undefined') return false;
1128
+ const domState = this.ensureImmediateNavDomState();
1129
+ const panel = domState.sessionPanelEl;
1130
+ return !!(panel && panel.classList && panel.classList.contains('session-panel-fast-hidden'));
1131
+ },
1132
+ recordPointerNavCommit(kind, value) {
1133
+ const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
1134
+ const normalizedValue = typeof value === 'string' ? value.trim().toLowerCase() : '';
1135
+ if (!normalizedKind || !normalizedValue) {
1136
+ this.__pointerNavCommitState = null;
1137
+ return;
1138
+ }
1139
+ this.__pointerNavCommitState = {
1140
+ kind: normalizedKind,
1141
+ value: normalizedValue,
1142
+ at: Date.now()
1143
+ };
1144
+ },
1145
+ consumePointerNavCommit(kind, value) {
1146
+ const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
1147
+ const normalizedValue = typeof value === 'string' ? value.trim().toLowerCase() : '';
1148
+ const state = this.__pointerNavCommitState;
1149
+ this.__pointerNavCommitState = null;
1150
+ if (!state || !normalizedKind || !normalizedValue) {
1151
+ return false;
1152
+ }
1153
+ if (state.kind !== normalizedKind || state.value !== normalizedValue) {
1154
+ return false;
1155
+ }
1156
+ return (Date.now() - Number(state.at || 0)) <= 1000;
1157
+ },
1158
+ onMainTabPointerDown(tab) {
1159
+ const event = arguments.length > 1 ? arguments[1] : null;
1160
+ if (event && typeof event.button === 'number' && event.button !== 0) {
1161
+ return;
1162
+ }
1163
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1164
+ if (!normalizedTab) return;
1165
+ this.setMainTabSwitchIntent(normalizedTab);
1166
+ this.applyImmediateNavIntent(normalizedTab);
1167
+ const shouldHideSessionPanel = this.mainTab === 'sessions' && normalizedTab !== 'sessions';
1168
+ this.setSessionPanelFastHidden(shouldHideSessionPanel);
1169
+ const pointerType = event && typeof event.pointerType === 'string'
1170
+ ? event.pointerType.trim().toLowerCase()
1171
+ : '';
1172
+ if (pointerType === 'touch') {
1173
+ return;
1174
+ }
1175
+ this.recordPointerNavCommit('main', normalizedTab);
1176
+ this.switchMainTab(normalizedTab);
1177
+ },
1178
+ onConfigTabPointerDown(mode) {
1179
+ const event = arguments.length > 1 ? arguments[1] : null;
1180
+ if (event && typeof event.button === 'number' && event.button !== 0) {
1181
+ return;
1182
+ }
1183
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
1184
+ if (!normalizedMode) return;
1185
+ this.setMainTabSwitchIntent('config');
1186
+ this.applyImmediateNavIntent('config', normalizedMode);
1187
+ const shouldHideSessionPanel = this.mainTab === 'sessions';
1188
+ this.setSessionPanelFastHidden(shouldHideSessionPanel);
1189
+ const pointerType = event && typeof event.pointerType === 'string'
1190
+ ? event.pointerType.trim().toLowerCase()
1191
+ : '';
1192
+ if (pointerType === 'touch') {
1193
+ return;
1194
+ }
1195
+ this.recordPointerNavCommit('config', normalizedMode);
1196
+ this.switchConfigMode(normalizedMode);
1197
+ },
1198
+ onMainTabClick(tab) {
1199
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1200
+ if (!normalizedTab) return;
1201
+ if (this.consumePointerNavCommit('main', normalizedTab)) return;
1202
+ this.switchMainTab(normalizedTab);
1203
+ },
1204
+ onConfigTabClick(mode) {
1205
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
1206
+ if (!normalizedMode) return;
1207
+ if (this.consumePointerNavCommit('config', normalizedMode)) return;
1208
+ this.switchConfigMode(normalizedMode);
1209
+ },
1210
+ clearMainTabSwitchIntent(expectedTab = '') {
1211
+ const state = this.ensureMainTabSwitchState();
1212
+ if (expectedTab && state.intent && state.intent !== expectedTab) {
1213
+ return;
1214
+ }
1215
+ state.intent = '';
1216
+ state.pendingTarget = '';
1217
+ this.clearImmediateNavIntent();
1218
+ this.setSessionPanelFastHidden(false);
1219
+ },
1220
+ getMainTabForNav() {
1221
+ const state = this.ensureMainTabSwitchState();
1222
+ return state.intent || this.mainTab;
1223
+ },
1224
+ isMainTabNavActive(tab) {
1225
+ return this.getMainTabForNav() === tab;
1226
+ },
1227
+ isConfigModeNavActive(mode) {
1228
+ return this.isMainTabNavActive('config') && this.configMode === mode;
1229
+ },
737
1230
  switchMainTab(tab) {
738
- this.mainTab = tab;
739
- if (tab === 'sessions' && this.sessionsList.length === 0) {
740
- this.loadSessions();
1231
+ const normalizedTab = typeof tab === 'string'
1232
+ ? tab.trim().toLowerCase()
1233
+ : '';
1234
+ const targetTab = normalizedTab || tab;
1235
+ if (!targetTab) return;
1236
+ if (targetTab === 'sessions') {
1237
+ this.cancelScheduledSessionTabDeferredTeardown();
741
1238
  }
742
- if (tab === 'config' && this.configMode === 'claude') {
743
- this.refreshClaudeModelContext();
1239
+
1240
+ this.setMainTabSwitchIntent(targetTab);
1241
+ if (targetTab === 'config') {
1242
+ this.applyImmediateNavIntent('config', this.configMode);
1243
+ } else {
1244
+ this.applyImmediateNavIntent(targetTab);
1245
+ }
1246
+
1247
+ const previousTab = this.mainTab;
1248
+ const switchState = this.ensureMainTabSwitchState();
1249
+ if (targetTab === previousTab) {
1250
+ switchState.ticket += 1;
1251
+ switchState.pendingTarget = '';
1252
+ this.scheduleAfterFrame(() => {
1253
+ this.clearMainTabSwitchIntent(normalizedTab);
1254
+ });
1255
+ return;
1256
+ }
1257
+ const isLeavingSessions = previousTab === 'sessions' && targetTab !== 'sessions';
1258
+ const shouldDeferApply = isLeavingSessions;
1259
+ if (isLeavingSessions && !this.isSessionPanelFastHidden()) {
1260
+ this.setSessionPanelFastHidden(true);
1261
+ }
1262
+ if (!shouldDeferApply) {
1263
+ switchState.ticket += 1;
1264
+ switchState.pendingTarget = '';
1265
+ const result = switchMainTabHelper.call(this, targetTab);
1266
+ this.scheduleAfterFrame(() => {
1267
+ this.clearMainTabSwitchIntent(normalizedTab);
1268
+ });
1269
+ return result;
1270
+ }
1271
+
1272
+ const ticket = ++switchState.ticket;
1273
+ switchState.pendingTarget = targetTab;
1274
+ this.scheduleAfterFrame(() => {
1275
+ const liveState = this.ensureMainTabSwitchState();
1276
+ if (ticket !== liveState.ticket) return;
1277
+ const pendingTarget = liveState.pendingTarget || targetTab;
1278
+ liveState.pendingTarget = '';
1279
+ switchMainTabHelper.call(this, pendingTarget);
1280
+ this.clearMainTabSwitchIntent(normalizedTab);
1281
+ });
1282
+ },
1283
+
1284
+ scheduleAfterFrame(task) {
1285
+ const callback = typeof task === 'function' ? task : () => {};
1286
+ if (typeof requestAnimationFrame === 'function') {
1287
+ requestAnimationFrame(callback);
1288
+ return;
1289
+ }
1290
+ setTimeout(callback, 16);
1291
+ },
1292
+ scheduleIdleTask(task, timeoutMs = 160) {
1293
+ const callback = typeof task === 'function' ? task : () => {};
1294
+ const timeout = Number.isFinite(timeoutMs)
1295
+ ? Math.max(16, Math.floor(timeoutMs))
1296
+ : 160;
1297
+ if (typeof requestIdleCallback === 'function') {
1298
+ const id = requestIdleCallback(callback, { timeout });
1299
+ return {
1300
+ type: 'idle',
1301
+ id
1302
+ };
744
1303
  }
1304
+ const id = setTimeout(callback, timeout);
1305
+ return {
1306
+ type: 'timeout',
1307
+ id
1308
+ };
1309
+ },
1310
+ cancelIdleTask(handle) {
1311
+ if (!handle || typeof handle !== 'object') return;
1312
+ const type = handle.type;
1313
+ const id = handle.id;
1314
+ if (type === 'idle') {
1315
+ if (typeof cancelIdleCallback === 'function') {
1316
+ cancelIdleCallback(id);
1317
+ } else {
1318
+ clearTimeout(id);
1319
+ }
1320
+ return;
1321
+ }
1322
+ if (type === 'timeout') {
1323
+ clearTimeout(id);
1324
+ }
1325
+ },
1326
+ scheduleSessionTabDeferredTeardown(task) {
1327
+ const callback = typeof task === 'function' ? task : () => {};
1328
+ this.cancelScheduledSessionTabDeferredTeardown();
1329
+ this.__sessionTabDeferredTeardownHandle = this.scheduleIdleTask(() => {
1330
+ this.__sessionTabDeferredTeardownHandle = null;
1331
+ callback();
1332
+ }, 180);
1333
+ },
1334
+ cancelScheduledSessionTabDeferredTeardown() {
1335
+ const handle = this.__sessionTabDeferredTeardownHandle || null;
1336
+ if (!handle) return;
1337
+ this.cancelIdleTask(handle);
1338
+ this.__sessionTabDeferredTeardownHandle = null;
1339
+ },
1340
+
1341
+ resetSessionPreviewMessageRender() {
1342
+ this.sessionPreviewVisibleCount = 0;
1343
+ this.invalidateSessionTimelineMeasurementCache();
1344
+ },
1345
+
1346
+ resetSessionDetailPagination() {
1347
+ const initialLimit = Number.isFinite(this.sessionDetailInitialMessageLimit)
1348
+ ? Math.max(1, Math.floor(this.sessionDetailInitialMessageLimit))
1349
+ : 80;
1350
+ this.sessionDetailMessageLimit = initialLimit;
1351
+ this.sessionPreviewPendingVisibleCount = 0;
1352
+ },
1353
+
1354
+ primeSessionPreviewMessageRender() {
1355
+ this.sessionPreviewVisibleCount = 0;
1356
+ this.invalidateSessionTimelineMeasurementCache();
1357
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
1358
+ return;
1359
+ }
1360
+ const total = Array.isArray(this.activeSessionMessages)
1361
+ ? this.activeSessionMessages.length
1362
+ : 0;
1363
+ if (total <= 0) return;
1364
+ const baseSize = Number.isFinite(this.sessionPreviewInitialBatchSize)
1365
+ ? Math.max(1, Math.floor(this.sessionPreviewInitialBatchSize))
1366
+ : 40;
1367
+ this.sessionPreviewVisibleCount = Math.min(baseSize, total);
1368
+ this.invalidateSessionTimelineMeasurementCache();
1369
+ },
1370
+
1371
+ async loadMoreSessionMessages(stepSize) {
1372
+ return loadMoreSessionMessagesHelper.call(this, stepSize);
1373
+ },
1374
+
1375
+ suspendSessionTabRender() {
1376
+ this.sessionTabRenderTicket += 1;
1377
+ this.sessionListRenderEnabled = false;
1378
+ this.sessionPreviewRenderEnabled = false;
1379
+ this.cancelSessionTimelineSync();
1380
+ this.sessionTimelineActiveKey = '';
1381
+ this.sessionTimelineLastSyncAt = 0;
1382
+ this.sessionTimelineLastScrollTop = 0;
1383
+ this.sessionTimelineLastAnchorY = 0;
1384
+ this.sessionTimelineLastDirection = 0;
1385
+ this.sessionPreviewScrollEl = null;
1386
+ this.sessionPreviewContainerEl = null;
1387
+ this.sessionPreviewHeaderEl = null;
1388
+ },
1389
+
1390
+ finalizeSessionTabTeardown() {
1391
+ this.resetSessionPreviewMessageRender();
1392
+ this.sessionPreviewPendingVisibleCount = 0;
1393
+ this.clearSessionTimelineRefs();
1394
+ },
1395
+
1396
+ teardownSessionTabRender() {
1397
+ this.suspendSessionTabRender();
1398
+ this.finalizeSessionTabTeardown();
1399
+ },
1400
+
1401
+ prepareSessionTabRender() {
1402
+ const ticket = ++this.sessionTabRenderTicket;
1403
+ this.sessionListRenderEnabled = false;
1404
+ this.sessionPreviewRenderEnabled = false;
1405
+ this.resetSessionPreviewMessageRender();
1406
+
1407
+ this.scheduleAfterFrame(() => {
1408
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1409
+ return;
1410
+ }
1411
+ this.sessionListRenderEnabled = true;
1412
+
1413
+ this.scheduleAfterFrame(() => {
1414
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1415
+ return;
1416
+ }
1417
+ this.sessionPreviewRenderEnabled = true;
1418
+ this.$nextTick(() => {
1419
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1420
+ return;
1421
+ }
1422
+ this.primeSessionPreviewMessageRender();
1423
+ this.updateSessionTimelineOffset();
1424
+ this.scheduleSessionTimelineSync();
1425
+ });
1426
+ });
1427
+ });
745
1428
  },
746
1429
 
747
1430
  getSessionStandaloneContext() {
@@ -788,6 +1471,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
788
1471
 
789
1472
  this.sessionStandalone = true;
790
1473
  this.mainTab = 'sessions';
1474
+ this.prepareSessionTabRender();
791
1475
 
792
1476
  if (context.error || !context.params) {
793
1477
  this.sessionStandaloneError = `会话链接参数不完整:${context.error || '参数解析失败'}`;
@@ -807,7 +1491,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
807
1491
  this.activeSessionDetailClipped = false;
808
1492
  this.cancelSessionTimelineSync();
809
1493
  this.sessionTimelineActiveKey = '';
810
- this.sessionMessageRefMap = Object.create(null);
1494
+ this.clearSessionTimelineRefs();
811
1495
  this.sessionStandaloneError = '';
812
1496
  this.sessionStandaloneText = '';
813
1497
  this.sessionStandaloneTitle = this.activeSession.title || '会话';
@@ -996,6 +1680,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
996
1680
  const name = typeof payload.name === 'string' ? payload.name.trim() : '';
997
1681
  const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl.trim() : '';
998
1682
  const apiKey = typeof payload.apiKey === 'string' ? payload.apiKey : '';
1683
+ const model = typeof payload.model === 'string' ? payload.model.trim() : '';
999
1684
  if (!name || !baseUrl) return '';
1000
1685
 
1001
1686
  const nameArg = this.quoteShellArg(name);
@@ -1005,7 +1690,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1005
1690
  const addCmd = apiKey
1006
1691
  ? `codexmate add ${nameArg} ${urlArg} ${keyArg}`
1007
1692
  : `codexmate add ${nameArg} ${urlArg}`;
1008
- return `${addCmd} && ${switchCmd}`;
1693
+ const modelCmd = model ? ` && codexmate use ${this.quoteShellArg(model)}` : '';
1694
+ return `${addCmd} && ${switchCmd}${modelCmd}`;
1009
1695
  },
1010
1696
 
1011
1697
  buildClaudeShareCommand(payload) {
@@ -1157,7 +1843,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1157
1843
  }
1158
1844
  this.sessionDeleting[key] = true;
1159
1845
  try {
1160
- const res = await api('delete-session', {
1846
+ const res = await api('trash-session', {
1161
1847
  source: session.source,
1162
1848
  sessionId: session.sessionId,
1163
1849
  filePath: session.filePath
@@ -1166,8 +1852,29 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1166
1852
  this.showMessage(res.error, 'error');
1167
1853
  return;
1168
1854
  }
1169
- this.showMessage('操作成功', 'success');
1170
- await this.loadSessions();
1855
+ this.removeSessionPin(session);
1856
+ this.invalidateSessionTrashRequests();
1857
+ this.showMessage('已移入回收站', 'success');
1858
+ if (this.sessionTrashLoadedOnce) {
1859
+ this.prependSessionTrashItem(this.buildSessionTrashItemFromSession(session, res), {
1860
+ totalCount: res && res.totalCount !== undefined ? res.totalCount : undefined
1861
+ });
1862
+ } else if (this.sessionTrashCountLoadedOnce) {
1863
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1864
+ res && res.totalCount !== undefined
1865
+ ? res.totalCount
1866
+ : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
1867
+ this.sessionTrashItems
1868
+ );
1869
+ } else {
1870
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1871
+ res && res.totalCount !== undefined
1872
+ ? res.totalCount
1873
+ : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
1874
+ this.sessionTrashItems
1875
+ );
1876
+ }
1877
+ await this.removeSessionFromCurrentList(session);
1171
1878
  } catch (e) {
1172
1879
  this.showMessage('删除失败', 'error');
1173
1880
  } finally {
@@ -1175,6 +1882,413 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1175
1882
  }
1176
1883
  },
1177
1884
 
1885
+ buildSessionTrashItemFromSession(session, result = {}) {
1886
+ const deletedAt = typeof result.deletedAt === 'string' && result.deletedAt
1887
+ ? result.deletedAt
1888
+ : new Date().toISOString();
1889
+ const source = session && session.source === 'claude' ? 'claude' : 'codex';
1890
+ return {
1891
+ trashId: typeof result.trashId === 'string' ? result.trashId : '',
1892
+ source,
1893
+ sourceLabel: session && typeof session.sourceLabel === 'string' && session.sourceLabel
1894
+ ? session.sourceLabel
1895
+ : (source === 'claude' ? 'Claude Code' : 'Codex'),
1896
+ sessionId: session && typeof session.sessionId === 'string' ? session.sessionId : '',
1897
+ title: session && typeof session.title === 'string' && session.title
1898
+ ? session.title
1899
+ : (session && typeof session.sessionId === 'string' ? session.sessionId : ''),
1900
+ cwd: session && typeof session.cwd === 'string' ? session.cwd : '',
1901
+ createdAt: session && typeof session.createdAt === 'string' ? session.createdAt : '',
1902
+ updatedAt: session && typeof session.updatedAt === 'string' ? session.updatedAt : '',
1903
+ deletedAt,
1904
+ messageCount: Number.isFinite(Number(result && result.messageCount))
1905
+ ? Math.max(0, Math.floor(Number(result.messageCount)))
1906
+ : (Number.isFinite(Number(session && session.messageCount))
1907
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
1908
+ : 0),
1909
+ originalFilePath: session && typeof session.filePath === 'string' ? session.filePath : '',
1910
+ provider: session && typeof session.provider === 'string' ? session.provider : source,
1911
+ keywords: Array.isArray(session && session.keywords) ? session.keywords : [],
1912
+ capabilities: session && typeof session.capabilities === 'object' && session.capabilities
1913
+ ? session.capabilities
1914
+ : {},
1915
+ claudeIndexPath: '',
1916
+ claudeIndexEntry: null,
1917
+ trashFilePath: ''
1918
+ };
1919
+ },
1920
+
1921
+ prependSessionTrashItem(item, options = {}) {
1922
+ if (!item || !item.trashId) {
1923
+ return;
1924
+ }
1925
+ const existing = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
1926
+ const filtered = existing.filter((entry) => this.getSessionTrashActionKey(entry) !== item.trashId);
1927
+ const nextItems = [item, ...filtered].slice(0, SESSION_TRASH_LIST_LIMIT);
1928
+ const previousTotalCount = Number(this.sessionTrashTotalCount);
1929
+ const normalizedPreviousTotal = Number.isFinite(previousTotalCount) && previousTotalCount >= 0
1930
+ ? Math.max(existing.length, Math.floor(previousTotalCount))
1931
+ : existing.length;
1932
+ this.sessionTrashItems = nextItems;
1933
+ const previousVisibleCount = Number(this.sessionTrashVisibleCount);
1934
+ const normalizedPreviousVisibleCount = Number.isFinite(previousVisibleCount) && previousVisibleCount > 0
1935
+ ? Math.floor(previousVisibleCount)
1936
+ : SESSION_TRASH_PAGE_SIZE;
1937
+ const wasFullyExpanded = normalizedPreviousVisibleCount >= existing.length
1938
+ || normalizedPreviousVisibleCount >= normalizedPreviousTotal;
1939
+ if (wasFullyExpanded) {
1940
+ this.sessionTrashVisibleCount = Math.min(
1941
+ normalizedPreviousVisibleCount + 1,
1942
+ nextItems.length || (normalizedPreviousVisibleCount + 1)
1943
+ );
1944
+ }
1945
+ const fallbackTotalCount = filtered.length === existing.length
1946
+ ? normalizedPreviousTotal + 1
1947
+ : normalizedPreviousTotal;
1948
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1949
+ options && options.totalCount !== undefined
1950
+ ? options.totalCount
1951
+ : fallbackTotalCount,
1952
+ nextItems
1953
+ );
1954
+ },
1955
+
1956
+ normalizeSessionTrashTotalCount(totalCount, fallbackItems = this.sessionTrashItems) {
1957
+ const fallbackCount = Array.isArray(fallbackItems) ? fallbackItems.length : 0;
1958
+ const numericTotal = Number(totalCount);
1959
+ if (!Number.isFinite(numericTotal) || numericTotal < 0) {
1960
+ return fallbackCount;
1961
+ }
1962
+ return Math.floor(numericTotal);
1963
+ },
1964
+
1965
+ getSessionTrashViewState() {
1966
+ if (this.sessionTrashLoading && !this.sessionTrashLoadedOnce) {
1967
+ return 'loading';
1968
+ }
1969
+ const totalCount = Number(this.sessionTrashCount);
1970
+ const normalizedTotalCount = Number.isFinite(totalCount) && totalCount >= 0
1971
+ ? Math.floor(totalCount)
1972
+ : 0;
1973
+ const hasVisibleItems = Array.isArray(this.sessionTrashItems) && this.sessionTrashItems.length > 0;
1974
+ if (this.sessionTrashLastLoadFailed && (!this.sessionTrashLoadedOnce || !hasVisibleItems)) {
1975
+ return 'retry';
1976
+ }
1977
+ if (!this.sessionTrashLoadedOnce) {
1978
+ return normalizedTotalCount > 0 ? 'retry' : 'empty';
1979
+ }
1980
+ if (normalizedTotalCount === 0) {
1981
+ return 'empty';
1982
+ }
1983
+ return hasVisibleItems ? 'list' : 'retry';
1984
+ },
1985
+
1986
+ issueSessionTrashCountRequestToken() {
1987
+ const currentToken = Number(this.sessionTrashCountRequestToken);
1988
+ const nextToken = Number.isFinite(currentToken) && currentToken >= 0
1989
+ ? Math.floor(currentToken) + 1
1990
+ : 1;
1991
+ this.sessionTrashCountRequestToken = nextToken;
1992
+ return nextToken;
1993
+ },
1994
+
1995
+ issueSessionTrashListRequestToken() {
1996
+ const currentToken = Number(this.sessionTrashListRequestToken);
1997
+ const nextToken = Number.isFinite(currentToken) && currentToken >= 0
1998
+ ? Math.floor(currentToken) + 1
1999
+ : 1;
2000
+ this.sessionTrashListRequestToken = nextToken;
2001
+ return nextToken;
2002
+ },
2003
+
2004
+ invalidateSessionTrashRequests() {
2005
+ this.issueSessionTrashCountRequestToken();
2006
+ return this.issueSessionTrashListRequestToken();
2007
+ },
2008
+
2009
+ isLatestSessionTrashCountRequestToken(token) {
2010
+ return Number(token) === Number(this.sessionTrashCountRequestToken);
2011
+ },
2012
+
2013
+ isLatestSessionTrashListRequestToken(token) {
2014
+ return Number(token) === Number(this.sessionTrashListRequestToken);
2015
+ },
2016
+
2017
+ resetSessionTrashVisibleCount() {
2018
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
2019
+ this.sessionTrashVisibleCount = Math.min(totalItems, SESSION_TRASH_PAGE_SIZE) || SESSION_TRASH_PAGE_SIZE;
2020
+ },
2021
+
2022
+ loadMoreSessionTrashItems() {
2023
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
2024
+ const visibleCount = Number(this.sessionTrashVisibleCount);
2025
+ const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0
2026
+ ? Math.floor(visibleCount)
2027
+ : SESSION_TRASH_PAGE_SIZE;
2028
+ this.sessionTrashVisibleCount = Math.min(totalItems, safeVisibleCount + SESSION_TRASH_PAGE_SIZE);
2029
+ },
2030
+
2031
+ clearActiveSessionState() {
2032
+ this.activeSession = null;
2033
+ this.activeSessionMessages = [];
2034
+ this.resetSessionDetailPagination();
2035
+ this.resetSessionPreviewMessageRender();
2036
+ this.activeSessionDetailError = '';
2037
+ this.activeSessionDetailClipped = false;
2038
+ this.cancelSessionTimelineSync();
2039
+ this.sessionTimelineActiveKey = '';
2040
+ this.clearSessionTimelineRefs();
2041
+ },
2042
+
2043
+ async removeSessionFromCurrentList(session) {
2044
+ const sessionKey = this.getSessionExportKey(session);
2045
+ if (!sessionKey) {
2046
+ return;
2047
+ }
2048
+ const currentList = Array.isArray(this.sessionsList) ? [...this.sessionsList] : [];
2049
+ const removedIndex = currentList.findIndex((item) => this.getSessionExportKey(item) === sessionKey);
2050
+ if (removedIndex < 0) {
2051
+ return;
2052
+ }
2053
+ const activeKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
2054
+ const renderedList = Array.isArray(this.sortedSessionsList) ? this.sortedSessionsList : [];
2055
+ const renderedIndex = renderedList.findIndex((item) => this.getSessionExportKey(item) === sessionKey);
2056
+ let nextActiveKey = '';
2057
+ if (activeKey === sessionKey && renderedIndex >= 0) {
2058
+ const fallbackSession = renderedList[renderedIndex - 1] || renderedList[renderedIndex + 1] || null;
2059
+ nextActiveKey = fallbackSession ? this.getSessionExportKey(fallbackSession) : '';
2060
+ }
2061
+ currentList.splice(removedIndex, 1);
2062
+ this.sessionsList = currentList;
2063
+ this.syncSessionPathOptionsForSource(
2064
+ this.sessionFilterSource,
2065
+ this.extractPathOptionsFromSessions(currentList),
2066
+ false
2067
+ );
2068
+ if (activeKey !== sessionKey) {
2069
+ return;
2070
+ }
2071
+ if (currentList.length === 0) {
2072
+ this.clearActiveSessionState();
2073
+ return;
2074
+ }
2075
+ const nextSession = currentList.find((item) => this.getSessionExportKey(item) === nextActiveKey)
2076
+ || currentList[Math.min(removedIndex, currentList.length - 1)];
2077
+ if (!nextSession) {
2078
+ this.clearActiveSessionState();
2079
+ return;
2080
+ }
2081
+ await this.selectSession(nextSession);
2082
+ },
2083
+
2084
+ normalizeSettingsTab(tab) {
2085
+ return tab === 'trash' ? 'trash' : 'backup';
2086
+ },
2087
+
2088
+ async onSettingsTabClick(tab) {
2089
+ await this.switchSettingsTab(tab);
2090
+ },
2091
+
2092
+ async switchSettingsTab(tab, options = {}) {
2093
+ const nextTab = this.normalizeSettingsTab(tab);
2094
+ this.settingsTab = nextTab;
2095
+ if (nextTab !== 'trash') {
2096
+ return;
2097
+ }
2098
+ const forceRefresh = options.forceRefresh === true;
2099
+ if (forceRefresh || !this.sessionTrashLoadedOnce) {
2100
+ await this.loadSessionTrash({ forceRefresh });
2101
+ }
2102
+ },
2103
+
2104
+ async loadSessionTrashCount(options = {}) {
2105
+ if (this.sessionTrashCountLoading) {
2106
+ this.sessionTrashCountPendingOptions = {
2107
+ ...(this.sessionTrashCountPendingOptions || {}),
2108
+ ...(options || {})
2109
+ };
2110
+ return;
2111
+ }
2112
+ const requestToken = this.issueSessionTrashCountRequestToken();
2113
+ this.sessionTrashCountLoading = true;
2114
+ try {
2115
+ const res = await api('list-session-trash', { countOnly: true });
2116
+ if (!this.isLatestSessionTrashCountRequestToken(requestToken)) {
2117
+ return;
2118
+ }
2119
+ if (res.error) {
2120
+ if (options.silent !== true) {
2121
+ this.showMessage(res.error, 'error');
2122
+ }
2123
+ return;
2124
+ }
2125
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
2126
+ res.totalCount,
2127
+ this.sessionTrashItems
2128
+ );
2129
+ this.sessionTrashCountLoadedOnce = true;
2130
+ } catch (e) {
2131
+ if (this.isLatestSessionTrashCountRequestToken(requestToken) && options.silent !== true) {
2132
+ this.showMessage('加载回收站数量失败', 'error');
2133
+ }
2134
+ } finally {
2135
+ this.sessionTrashCountLoading = false;
2136
+ const pendingOptions = this.sessionTrashCountPendingOptions;
2137
+ this.sessionTrashCountPendingOptions = null;
2138
+ if (pendingOptions) {
2139
+ await this.loadSessionTrashCount(pendingOptions);
2140
+ }
2141
+ }
2142
+ },
2143
+
2144
+ getSessionTrashActionKey(item) {
2145
+ return item && typeof item.trashId === 'string' ? item.trashId : '';
2146
+ },
2147
+
2148
+ isSessionTrashActionBusy(item) {
2149
+ const key = typeof item === 'string' ? item : this.getSessionTrashActionKey(item);
2150
+ return !!(key && (this.sessionTrashRestoring[key] || this.sessionTrashPurging[key]));
2151
+ },
2152
+
2153
+ async loadSessionTrash(options = {}) {
2154
+ if (this.sessionTrashLoading) {
2155
+ this.sessionTrashPendingOptions = {
2156
+ ...(this.sessionTrashPendingOptions || {}),
2157
+ ...(options || {})
2158
+ };
2159
+ return;
2160
+ }
2161
+ const requestToken = this.issueSessionTrashListRequestToken();
2162
+ this.sessionTrashLoading = true;
2163
+ this.sessionTrashLastLoadFailed = false;
2164
+ let loadSucceeded = false;
2165
+ try {
2166
+ const res = await api('list-session-trash', {
2167
+ limit: SESSION_TRASH_LIST_LIMIT,
2168
+ forceRefresh: !!options.forceRefresh
2169
+ });
2170
+ if (!this.isLatestSessionTrashListRequestToken(requestToken)) {
2171
+ return;
2172
+ }
2173
+ if (res.error) {
2174
+ this.sessionTrashLastLoadFailed = true;
2175
+ this.showMessage(res.error, 'error');
2176
+ return;
2177
+ }
2178
+ const nextItems = Array.isArray(res.items) ? res.items : [];
2179
+ this.sessionTrashItems = nextItems;
2180
+ this.resetSessionTrashVisibleCount();
2181
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(res.totalCount, nextItems);
2182
+ this.sessionTrashCountLoadedOnce = true;
2183
+ this.sessionTrashLastLoadFailed = false;
2184
+ loadSucceeded = true;
2185
+ } catch (e) {
2186
+ if (this.isLatestSessionTrashListRequestToken(requestToken)) {
2187
+ this.sessionTrashLastLoadFailed = true;
2188
+ this.showMessage('加载回收站失败', 'error');
2189
+ }
2190
+ } finally {
2191
+ this.sessionTrashLoading = false;
2192
+ if (loadSucceeded) {
2193
+ this.sessionTrashLoadedOnce = true;
2194
+ }
2195
+ const pendingOptions = this.sessionTrashPendingOptions;
2196
+ this.sessionTrashPendingOptions = null;
2197
+ if (pendingOptions) {
2198
+ await this.loadSessionTrash(pendingOptions);
2199
+ }
2200
+ }
2201
+ },
2202
+
2203
+ async restoreSessionTrash(item) {
2204
+ const key = this.getSessionTrashActionKey(item);
2205
+ if (!key || this.isSessionTrashActionBusy(key) || this.sessionTrashClearing) {
2206
+ return;
2207
+ }
2208
+ this.sessionTrashRestoring[key] = true;
2209
+ try {
2210
+ const res = await api('restore-session-trash', { trashId: key });
2211
+ if (res.error) {
2212
+ this.showMessage(res.error, 'error');
2213
+ return;
2214
+ }
2215
+ this.showMessage('会话已恢复', 'success');
2216
+ this.invalidateSessionTrashRequests();
2217
+ await this.loadSessionTrash({ forceRefresh: true });
2218
+ if (this.sessionsLoadedOnce) {
2219
+ await this.loadSessions();
2220
+ }
2221
+ } catch (e) {
2222
+ this.showMessage('恢复失败', 'error');
2223
+ } finally {
2224
+ this.sessionTrashRestoring[key] = false;
2225
+ }
2226
+ },
2227
+
2228
+ async purgeSessionTrash(item) {
2229
+ const key = this.getSessionTrashActionKey(item);
2230
+ if (!key || this.isSessionTrashActionBusy(key) || this.sessionTrashClearing) {
2231
+ return;
2232
+ }
2233
+ const confirmed = await this.requestConfirmDialog({
2234
+ title: '彻底删除回收站记录',
2235
+ message: '该会话将从回收站永久删除,且无法恢复。',
2236
+ confirmText: '彻底删除',
2237
+ cancelText: '取消',
2238
+ danger: true
2239
+ });
2240
+ if (!confirmed) {
2241
+ return;
2242
+ }
2243
+ this.sessionTrashPurging[key] = true;
2244
+ try {
2245
+ const res = await api('purge-session-trash', { trashId: key });
2246
+ if (res.error) {
2247
+ this.showMessage(res.error, 'error');
2248
+ return;
2249
+ }
2250
+ this.showMessage('已彻底删除', 'success');
2251
+ this.invalidateSessionTrashRequests();
2252
+ await this.loadSessionTrash({ forceRefresh: true });
2253
+ } catch (e) {
2254
+ this.showMessage('彻底删除失败', 'error');
2255
+ } finally {
2256
+ this.sessionTrashPurging[key] = false;
2257
+ }
2258
+ },
2259
+
2260
+ async clearSessionTrash() {
2261
+ const normalizedCount = Number(this.sessionTrashCount);
2262
+ if (this.sessionTrashClearing || !Number.isFinite(normalizedCount) || normalizedCount <= 0) {
2263
+ return;
2264
+ }
2265
+ const confirmed = await this.requestConfirmDialog({
2266
+ title: '清空回收站',
2267
+ message: '该操作会永久删除回收站中的全部会话,且无法恢复。',
2268
+ confirmText: '全部清空',
2269
+ cancelText: '取消',
2270
+ danger: true
2271
+ });
2272
+ if (!confirmed) {
2273
+ return;
2274
+ }
2275
+ this.sessionTrashClearing = true;
2276
+ try {
2277
+ const res = await api('purge-session-trash', { all: true });
2278
+ if (res.error) {
2279
+ this.showMessage(res.error, 'error');
2280
+ return;
2281
+ }
2282
+ this.showMessage('回收站已清空', 'success');
2283
+ this.invalidateSessionTrashRequests();
2284
+ await this.loadSessionTrash({ forceRefresh: true });
2285
+ } catch (e) {
2286
+ this.showMessage('清空回收站失败', 'error');
2287
+ } finally {
2288
+ this.sessionTrashClearing = false;
2289
+ }
2290
+ },
2291
+
1178
2292
  normalizeSessionPathValue(value) {
1179
2293
  return normalizeSessionPathFilter(value);
1180
2294
  },
@@ -1285,22 +2399,140 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1285
2399
  const value = this.sessionResumeWithYolo ? '1' : '0';
1286
2400
  localStorage.setItem('codexmateSessionResumeYolo', value);
1287
2401
  },
1288
- restoreSessionFilterCache() {
1289
- const sourceCache = localStorage.getItem('codexmateSessionFilterSource');
1290
- const pathCache = localStorage.getItem('codexmateSessionPathFilter');
1291
- const cached = buildSessionFilterCacheState(sourceCache, pathCache);
1292
- this.sessionFilterSource = cached.source;
1293
- this.sessionPathFilter = cached.pathFilter;
1294
- this.refreshSessionPathOptions(this.sessionFilterSource);
2402
+ restoreSessionFilterCache() {
2403
+ const sourceCache = localStorage.getItem('codexmateSessionFilterSource');
2404
+ const pathCache = localStorage.getItem('codexmateSessionPathFilter');
2405
+ const cached = buildSessionFilterCacheState(sourceCache, pathCache);
2406
+ this.sessionFilterSource = cached.source;
2407
+ this.sessionPathFilter = cached.pathFilter;
2408
+ this.refreshSessionPathOptions(this.sessionFilterSource);
2409
+ },
2410
+ persistSessionFilterCache() {
2411
+ const cached = buildSessionFilterCacheState(this.sessionFilterSource, this.sessionPathFilter);
2412
+ localStorage.setItem('codexmateSessionFilterSource', cached.source);
2413
+ if (cached.pathFilter) {
2414
+ localStorage.setItem('codexmateSessionPathFilter', cached.pathFilter);
2415
+ } else {
2416
+ localStorage.removeItem('codexmateSessionPathFilter');
2417
+ }
2418
+ },
2419
+ normalizeSessionPinnedMap(raw) {
2420
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
2421
+ return {};
2422
+ }
2423
+ const next = {};
2424
+ for (const [key, value] of Object.entries(raw)) {
2425
+ if (!key) continue;
2426
+ const numeric = Number(value);
2427
+ if (!Number.isFinite(numeric) || numeric <= 0) continue;
2428
+ next[key] = Math.floor(numeric);
2429
+ }
2430
+ return next;
2431
+ },
2432
+ restoreSessionPinnedMap() {
2433
+ const cached = localStorage.getItem('codexmateSessionPinnedMap');
2434
+ if (!cached) {
2435
+ this.sessionPinnedMap = {};
2436
+ return;
2437
+ }
2438
+ try {
2439
+ const parsed = JSON.parse(cached);
2440
+ this.sessionPinnedMap = this.normalizeSessionPinnedMap(parsed);
2441
+ } catch (_) {
2442
+ this.sessionPinnedMap = {};
2443
+ localStorage.removeItem('codexmateSessionPinnedMap');
2444
+ }
2445
+ },
2446
+ persistSessionPinnedMap() {
2447
+ const payload = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2448
+ ? this.sessionPinnedMap
2449
+ : {};
2450
+ localStorage.setItem('codexmateSessionPinnedMap', JSON.stringify(payload));
2451
+ },
2452
+ shouldPruneSessionPinnedMap(sessions = this.sessionsList) {
2453
+ if (!Array.isArray(sessions) || sessions.length === 0) {
2454
+ return false;
2455
+ }
2456
+ if (this.sessionFilterSource !== 'all') {
2457
+ return false;
2458
+ }
2459
+ if (this.sessionPathFilter) {
2460
+ return false;
2461
+ }
2462
+ if (this.sessionQuery && isSessionQueryEnabled(this.sessionFilterSource)) {
2463
+ return false;
2464
+ }
2465
+ if (this.sessionRoleFilter && this.sessionRoleFilter !== 'all') {
2466
+ return false;
2467
+ }
2468
+ if (this.sessionTimePreset && this.sessionTimePreset !== 'all') {
2469
+ return false;
2470
+ }
2471
+ return true;
2472
+ },
2473
+ pruneSessionPinnedMap(sessions = this.sessionsList) {
2474
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2475
+ ? this.sessionPinnedMap
2476
+ : {};
2477
+ const list = Array.isArray(sessions) ? sessions : [];
2478
+ if (Object.keys(current).length === 0 || !this.shouldPruneSessionPinnedMap(list)) {
2479
+ return;
2480
+ }
2481
+ const validKeys = new Set(list.map((session) => this.getSessionExportKey(session)).filter(Boolean));
2482
+ const next = {};
2483
+ let changed = false;
2484
+ for (const [key, value] of Object.entries(current)) {
2485
+ if (!validKeys.has(key)) {
2486
+ changed = true;
2487
+ continue;
2488
+ }
2489
+ next[key] = value;
2490
+ }
2491
+ if (!changed) {
2492
+ return;
2493
+ }
2494
+ this.sessionPinnedMap = next;
2495
+ this.persistSessionPinnedMap();
2496
+ },
2497
+ getSessionPinTimestamp(session) {
2498
+ if (!session) return 0;
2499
+ const key = this.getSessionExportKey(session);
2500
+ if (!key) return 0;
2501
+ const raw = this.sessionPinnedMap && this.sessionPinnedMap[key];
2502
+ const numeric = Number(raw);
2503
+ return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0;
1295
2504
  },
1296
- persistSessionFilterCache() {
1297
- const cached = buildSessionFilterCacheState(this.sessionFilterSource, this.sessionPathFilter);
1298
- localStorage.setItem('codexmateSessionFilterSource', cached.source);
1299
- if (cached.pathFilter) {
1300
- localStorage.setItem('codexmateSessionPathFilter', cached.pathFilter);
2505
+ isSessionPinned(session) {
2506
+ return this.getSessionPinTimestamp(session) > 0;
2507
+ },
2508
+ toggleSessionPin(session) {
2509
+ if (!session) return;
2510
+ const key = this.getSessionExportKey(session);
2511
+ if (!key) return;
2512
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2513
+ ? this.sessionPinnedMap
2514
+ : {};
2515
+ const next = { ...current };
2516
+ if (next[key]) {
2517
+ delete next[key];
1301
2518
  } else {
1302
- localStorage.removeItem('codexmateSessionPathFilter');
2519
+ next[key] = Date.now();
1303
2520
  }
2521
+ this.sessionPinnedMap = next;
2522
+ this.persistSessionPinnedMap();
2523
+ },
2524
+ removeSessionPin(session) {
2525
+ if (!session) return;
2526
+ const key = this.getSessionExportKey(session);
2527
+ if (!key) return;
2528
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2529
+ ? this.sessionPinnedMap
2530
+ : {};
2531
+ if (!current[key]) return;
2532
+ const next = { ...current };
2533
+ delete next[key];
2534
+ this.sessionPinnedMap = next;
2535
+ this.persistSessionPinnedMap();
1304
2536
  },
1305
2537
 
1306
2538
  async onSessionSourceChange() {
@@ -1352,13 +2584,137 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1352
2584
  },
1353
2585
  setSessionPreviewScrollRef(el) {
1354
2586
  this.sessionPreviewScrollEl = el || null;
1355
- if (this.sessionPreviewScrollEl) {
1356
- this.scheduleSessionTimelineSync();
1357
- } else {
2587
+ this.invalidateSessionTimelineMeasurementCache();
2588
+ const shouldSync = !!(
2589
+ this.sessionPreviewScrollEl
2590
+ && this.mainTab === 'sessions'
2591
+ && this.getMainTabForNav() === 'sessions'
2592
+ && this.sessionPreviewRenderEnabled
2593
+ && this.sessionTimelineNodes.length
2594
+ );
2595
+ if (!shouldSync) {
1358
2596
  this.cancelSessionTimelineSync();
2597
+ this.updateSessionTimelineOffset();
2598
+ return;
1359
2599
  }
2600
+ const boundScrollEl = this.sessionPreviewScrollEl;
2601
+ this.$nextTick(() => {
2602
+ if (this.sessionPreviewScrollEl !== boundScrollEl) return;
2603
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) return;
2604
+ if (!this.sessionTimelineNodes.length) return;
2605
+ this.scheduleSessionTimelineSync();
2606
+ });
1360
2607
  this.updateSessionTimelineOffset();
1361
2608
  },
2609
+ clearSessionTimelineRefs() {
2610
+ this.sessionMessageRefMap = Object.create(null);
2611
+ this.sessionMessageRefBinderMap = Object.create(null);
2612
+ this.sessionTimelineLastAnchorY = 0;
2613
+ this.sessionTimelineLastDirection = 0;
2614
+ this.invalidateSessionTimelineMeasurementCache(true);
2615
+ },
2616
+ ensureSessionTimelineMeasurementCache() {
2617
+ if (this.__sessionTimelineMeasurementCache) {
2618
+ return this.__sessionTimelineMeasurementCache;
2619
+ }
2620
+ this.__sessionTimelineMeasurementCache = {
2621
+ offsetByKey: Object.create(null),
2622
+ dirty: true
2623
+ };
2624
+ return this.__sessionTimelineMeasurementCache;
2625
+ },
2626
+ invalidateSessionTimelineMeasurementCache(resetOffset = false) {
2627
+ const cache = this.ensureSessionTimelineMeasurementCache();
2628
+ if (resetOffset) {
2629
+ cache.offsetByKey = Object.create(null);
2630
+ }
2631
+ cache.dirty = true;
2632
+ },
2633
+ refreshSessionTimelineMeasurementCache(nodes = null) {
2634
+ const cache = this.ensureSessionTimelineMeasurementCache();
2635
+ const nodeList = Array.isArray(nodes) ? nodes : (Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : []);
2636
+ if (!nodeList.length) {
2637
+ cache.offsetByKey = Object.create(null);
2638
+ cache.dirty = false;
2639
+ return cache.offsetByKey;
2640
+ }
2641
+ const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
2642
+ const scrollRect = scrollEl && typeof scrollEl.getBoundingClientRect === 'function'
2643
+ ? scrollEl.getBoundingClientRect()
2644
+ : null;
2645
+ const scrollTop = scrollEl ? Number(scrollEl.scrollTop || 0) : 0;
2646
+ const nextOffsetByKey = Object.create(null);
2647
+ for (const node of nodeList) {
2648
+ if (!node || !node.key) continue;
2649
+ const messageEl = this.sessionMessageRefMap[node.key];
2650
+ if (!messageEl) continue;
2651
+ let top = Number.NaN;
2652
+ if (
2653
+ scrollRect
2654
+ && typeof messageEl.getBoundingClientRect === 'function'
2655
+ ) {
2656
+ const messageRect = messageEl.getBoundingClientRect();
2657
+ top = scrollTop + (messageRect.top - scrollRect.top);
2658
+ } else {
2659
+ top = Number(messageEl.offsetTop || 0);
2660
+ }
2661
+ if (!Number.isFinite(top)) continue;
2662
+ nextOffsetByKey[node.key] = top;
2663
+ }
2664
+ cache.offsetByKey = nextOffsetByKey;
2665
+ cache.dirty = false;
2666
+ return cache.offsetByKey;
2667
+ },
2668
+ getCachedSessionTimelineMeasuredNodes(nodes) {
2669
+ const nodeList = Array.isArray(nodes) ? nodes : [];
2670
+ if (!nodeList.length) {
2671
+ return [];
2672
+ }
2673
+ const cache = this.ensureSessionTimelineMeasurementCache();
2674
+ if (cache.dirty) {
2675
+ this.refreshSessionTimelineMeasurementCache(nodeList);
2676
+ }
2677
+ const offsetByKey = cache.offsetByKey || Object.create(null);
2678
+ const measuredNodes = [];
2679
+ for (const node of nodeList) {
2680
+ if (!node || !node.key) continue;
2681
+ const top = Number(offsetByKey[node.key]);
2682
+ if (!Number.isFinite(top)) continue;
2683
+ measuredNodes.push({
2684
+ key: node.key,
2685
+ top
2686
+ });
2687
+ }
2688
+ if (measuredNodes.length >= nodeList.length) {
2689
+ return measuredNodes;
2690
+ }
2691
+ const refreshedOffsetByKey = this.refreshSessionTimelineMeasurementCache(nodeList);
2692
+ const refreshedNodes = [];
2693
+ for (const node of nodeList) {
2694
+ if (!node || !node.key) continue;
2695
+ const top = Number(refreshedOffsetByKey[node.key]);
2696
+ if (!Number.isFinite(top)) continue;
2697
+ refreshedNodes.push({
2698
+ key: node.key,
2699
+ top
2700
+ });
2701
+ }
2702
+ return refreshedNodes;
2703
+ },
2704
+ getSessionMessageRefBinder(messageKey) {
2705
+ if (!this.isSessionTimelineNodeKey(messageKey)) return null;
2706
+ const current = this.sessionMessageRefBinderMap[messageKey];
2707
+ if (!current || current.ticket !== this.sessionTabRenderTicket) {
2708
+ const ticket = this.sessionTabRenderTicket;
2709
+ this.sessionMessageRefBinderMap[messageKey] = {
2710
+ ticket,
2711
+ bind: (el) => {
2712
+ this.bindSessionMessageRef(messageKey, el, ticket);
2713
+ }
2714
+ };
2715
+ }
2716
+ return this.sessionMessageRefBinderMap[messageKey].bind;
2717
+ },
1362
2718
  updateSessionTimelineOffset() {
1363
2719
  const container = this.sessionPreviewContainerEl || this.$refs.sessionPreviewContainer;
1364
2720
  if (!container || !container.style) return;
@@ -1369,12 +2725,39 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1369
2725
  const offset = headerHeight > 0 ? (headerHeight + 12) : 72;
1370
2726
  container.style.setProperty('--session-preview-header-offset', `${offset}px`);
1371
2727
  },
1372
- bindSessionMessageRef(messageKey, el) {
2728
+ bindSessionMessageRef(messageKey, el, ticket = this.sessionTabRenderTicket) {
2729
+ if (!this.sessionTimelineEnabled) return;
1373
2730
  if (!messageKey) return;
2731
+ if (ticket !== this.sessionTabRenderTicket) return;
1374
2732
  if (el) {
2733
+ if (!this.isSessionTimelineNodeKey(messageKey)) return;
2734
+ if (this.sessionMessageRefMap[messageKey] === el) return;
1375
2735
  this.sessionMessageRefMap[messageKey] = el;
2736
+ this.invalidateSessionTimelineMeasurementCache();
1376
2737
  } else {
2738
+ if (!this.sessionMessageRefMap[messageKey]) return;
1377
2739
  delete this.sessionMessageRefMap[messageKey];
2740
+ this.invalidateSessionTimelineMeasurementCache();
2741
+ }
2742
+ },
2743
+ isSessionTimelineNodeKey(messageKey) {
2744
+ if (!messageKey) return false;
2745
+ return !!(this.sessionTimelineNodeKeyMap && this.sessionTimelineNodeKeyMap[messageKey]);
2746
+ },
2747
+ pruneSessionMessageRefs() {
2748
+ const nodeKeyMap = this.sessionTimelineNodeKeyMap || Object.create(null);
2749
+ let removed = false;
2750
+ for (const key of Object.keys(this.sessionMessageRefMap)) {
2751
+ if (nodeKeyMap[key]) continue;
2752
+ delete this.sessionMessageRefMap[key];
2753
+ removed = true;
2754
+ }
2755
+ for (const key of Object.keys(this.sessionMessageRefBinderMap)) {
2756
+ if (nodeKeyMap[key]) continue;
2757
+ delete this.sessionMessageRefBinderMap[key];
2758
+ }
2759
+ if (removed) {
2760
+ this.invalidateSessionTimelineMeasurementCache();
1378
2761
  }
1379
2762
  },
1380
2763
  cancelSessionTimelineSync() {
@@ -1396,42 +2779,178 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1396
2779
  this.syncSessionTimelineActiveFromScroll();
1397
2780
  },
1398
2781
  onSessionPreviewScroll() {
2782
+ if (
2783
+ !this.sessionTimelineEnabled
2784
+ || this.mainTab !== 'sessions'
2785
+ || this.getMainTabForNav() !== 'sessions'
2786
+ || !this.sessionPreviewRenderEnabled
2787
+ ) return;
2788
+ if (!this.sessionTimelineNodes.length) return;
2789
+ const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
2790
+ if (!scrollEl) return;
2791
+ const now = Date.now();
2792
+ const currentTop = Number(scrollEl.scrollTop || 0);
2793
+ const delta = Math.abs(currentTop - Number(this.sessionTimelineLastScrollTop || 0));
2794
+ const elapsed = now - Number(this.sessionTimelineLastSyncAt || 0);
2795
+ if (delta < 48 && elapsed < 120) {
2796
+ return;
2797
+ }
2798
+ this.sessionTimelineLastScrollTop = currentTop;
2799
+ this.sessionTimelineLastSyncAt = now;
1399
2800
  this.scheduleSessionTimelineSync();
1400
2801
  },
1401
2802
  onWindowResize() {
2803
+ this.updateCompactLayoutMode();
2804
+ if (
2805
+ !this.sessionTimelineEnabled
2806
+ || this.mainTab !== 'sessions'
2807
+ || this.getMainTabForNav() !== 'sessions'
2808
+ || !this.sessionPreviewRenderEnabled
2809
+ ) {
2810
+ return;
2811
+ }
2812
+ if (!this.sessionTimelineNodes.length) return;
1402
2813
  this.updateSessionTimelineOffset();
2814
+ this.invalidateSessionTimelineMeasurementCache();
1403
2815
  this.scheduleSessionTimelineSync();
1404
2816
  },
2817
+ shouldForceCompactLayout() {
2818
+ if (typeof window === 'undefined' || typeof navigator === 'undefined') {
2819
+ return false;
2820
+ }
2821
+ const doc = typeof document !== 'undefined' ? document : null;
2822
+ const viewportWidth = Math.max(
2823
+ 0,
2824
+ Number(window.innerWidth || 0),
2825
+ Number(doc && doc.documentElement ? doc.documentElement.clientWidth : 0)
2826
+ );
2827
+ const screenWidth = Number(window.screen && window.screen.width ? window.screen.width : 0);
2828
+ const screenHeight = Number(window.screen && window.screen.height ? window.screen.height : 0);
2829
+ const shortEdge = screenWidth > 0 && screenHeight > 0
2830
+ ? Math.min(screenWidth, screenHeight)
2831
+ : 0;
2832
+ const touchPoints = Number(navigator.maxTouchPoints || 0);
2833
+ const userAgent = String(navigator.userAgent || '');
2834
+ const isMobileUa = /(Android|iPhone|iPad|iPod|Mobile)/i.test(userAgent);
2835
+ let coarsePointer = false;
2836
+ let noHover = false;
2837
+ try {
2838
+ coarsePointer = !!(window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
2839
+ } catch (_) {}
2840
+ try {
2841
+ noHover = !!(window.matchMedia && window.matchMedia('(hover: none)').matches);
2842
+ } catch (_) {}
2843
+ return shouldForceCompactLayoutMode({
2844
+ viewportWidth,
2845
+ screenWidth,
2846
+ screenHeight,
2847
+ shortEdge,
2848
+ maxTouchPoints: touchPoints,
2849
+ userAgent,
2850
+ isMobileUa,
2851
+ coarsePointer,
2852
+ noHover
2853
+ });
2854
+ },
2855
+ applyCompactLayoutClass(enabled) {
2856
+ if (typeof document === 'undefined' || !document.body) {
2857
+ return;
2858
+ }
2859
+ document.body.classList.toggle('force-compact', !!enabled);
2860
+ },
2861
+ updateCompactLayoutMode() {
2862
+ const enabled = this.shouldForceCompactLayout();
2863
+ this.forceCompactLayout = enabled;
2864
+ this.applyCompactLayoutClass(enabled);
2865
+ },
1405
2866
  syncSessionTimelineActiveFromScroll() {
2867
+ if (
2868
+ !this.sessionTimelineEnabled
2869
+ || this.mainTab !== 'sessions'
2870
+ || this.getMainTabForNav() !== 'sessions'
2871
+ || !this.sessionPreviewRenderEnabled
2872
+ ) {
2873
+ if (this.sessionTimelineActiveKey) {
2874
+ this.sessionTimelineActiveKey = '';
2875
+ }
2876
+ return;
2877
+ }
1406
2878
  const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
1407
2879
  if (!nodes.length) {
1408
- this.sessionTimelineActiveKey = '';
2880
+ if (this.sessionTimelineActiveKey) {
2881
+ this.sessionTimelineActiveKey = '';
2882
+ }
1409
2883
  return;
1410
2884
  }
2885
+ this.pruneSessionMessageRefs();
1411
2886
  const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
1412
2887
  if (!scrollEl) {
1413
- this.sessionTimelineActiveKey = nodes[0].key;
2888
+ if (!this.isSessionTimelineNodeKey(this.sessionTimelineActiveKey)) {
2889
+ const fallbackKey = nodes[0].key;
2890
+ if (this.sessionTimelineActiveKey !== fallbackKey) {
2891
+ this.sessionTimelineActiveKey = fallbackKey;
2892
+ }
2893
+ }
1414
2894
  return;
1415
2895
  }
1416
- const scrollRect = scrollEl.getBoundingClientRect();
1417
2896
  const headerEl = scrollEl.querySelector('.session-preview-header');
1418
- const headerHeight = headerEl ? headerEl.getBoundingClientRect().height : 0;
1419
- const anchorLine = scrollRect.top + headerHeight + 8;
1420
- let activeKey = nodes[0].key;
1421
- for (const node of nodes) {
1422
- const messageEl = this.sessionMessageRefMap[node.key];
1423
- if (!messageEl) continue;
1424
- const messageRect = messageEl.getBoundingClientRect();
1425
- if (messageRect.top <= anchorLine) {
1426
- activeKey = node.key;
1427
- continue;
2897
+ const stickyOffset = headerEl ? (headerEl.offsetHeight + 8) : 8;
2898
+ const rawAnchorY = Number(scrollEl.scrollTop || 0) + stickyOffset;
2899
+ const previousAnchorY = Number(this.sessionTimelineLastAnchorY || 0);
2900
+ let direction = rawAnchorY - previousAnchorY;
2901
+ if (Math.abs(direction) < 1) {
2902
+ direction = Number(this.sessionTimelineLastDirection || 0);
2903
+ } else {
2904
+ this.sessionTimelineLastDirection = direction > 0 ? 1 : -1;
2905
+ }
2906
+ this.sessionTimelineLastAnchorY = rawAnchorY;
2907
+ const hysteresisPx = 18;
2908
+ const hysteresis = direction > 0 ? -hysteresisPx : (direction < 0 ? hysteresisPx : 0);
2909
+ const anchorY = rawAnchorY + hysteresis;
2910
+ const measuredNodes = this.getCachedSessionTimelineMeasuredNodes(nodes);
2911
+ if (!measuredNodes.length) {
2912
+ if (!this.isSessionTimelineNodeKey(this.sessionTimelineActiveKey)) {
2913
+ this.sessionTimelineActiveKey = nodes[0].key;
2914
+ }
2915
+ return;
2916
+ }
2917
+ let low = 0;
2918
+ let high = measuredNodes.length - 1;
2919
+ let candidateIndex = 0;
2920
+ while (low <= high) {
2921
+ const mid = Math.floor((low + high) / 2);
2922
+ if (measuredNodes[mid].top <= anchorY) {
2923
+ candidateIndex = mid;
2924
+ low = mid + 1;
2925
+ } else {
2926
+ high = mid - 1;
2927
+ }
2928
+ }
2929
+ let currentIndex = -1;
2930
+ if (this.sessionTimelineActiveKey) {
2931
+ for (let i = 0; i < measuredNodes.length; i += 1) {
2932
+ if (measuredNodes[i].key === this.sessionTimelineActiveKey) {
2933
+ currentIndex = i;
2934
+ break;
2935
+ }
2936
+ }
2937
+ }
2938
+ if (currentIndex >= 0) {
2939
+ if (direction > 0 && candidateIndex < currentIndex) {
2940
+ candidateIndex = currentIndex;
2941
+ } else if (direction < 0 && candidateIndex > currentIndex) {
2942
+ candidateIndex = currentIndex;
1428
2943
  }
1429
- break;
1430
2944
  }
1431
- this.sessionTimelineActiveKey = activeKey;
2945
+ const activeKey = measuredNodes[candidateIndex].key;
2946
+ if (this.sessionTimelineActiveKey !== activeKey) {
2947
+ this.sessionTimelineActiveKey = activeKey;
2948
+ }
1432
2949
  },
1433
2950
  jumpToSessionTimelineNode(messageKey) {
2951
+ if (!this.sessionTimelineEnabled || this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) return;
1434
2952
  if (!messageKey) return;
2953
+ if (!this.isSessionTimelineNodeKey(messageKey)) return;
1435
2954
  const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
1436
2955
  if (!scrollEl) return;
1437
2956
  const messageEl = this.sessionMessageRefMap[messageKey];
@@ -1504,67 +3023,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1504
3023
  },
1505
3024
 
1506
3025
  async loadSessions() {
1507
- if (this.sessionsLoading) return;
1508
- this.sessionsLoading = true;
1509
- this.activeSessionDetailError = '';
1510
- const params = buildSessionListParams({
1511
- source: this.sessionFilterSource,
1512
- pathFilter: this.sessionPathFilter,
1513
- query: this.sessionQuery,
1514
- roleFilter: this.sessionRoleFilter,
1515
- timeRangePreset: this.sessionTimePreset
1516
- });
1517
- try {
1518
- const res = await api('list-sessions', params);
1519
- if (res.error) {
1520
- this.showMessage(res.error, 'error');
1521
- this.sessionsList = [];
1522
- this.activeSession = null;
1523
- this.activeSessionMessages = [];
1524
- this.activeSessionDetailClipped = false;
1525
- this.cancelSessionTimelineSync();
1526
- this.sessionTimelineActiveKey = '';
1527
- this.sessionMessageRefMap = Object.create(null);
1528
- } else {
1529
- this.sessionsList = Array.isArray(res.sessions) ? res.sessions : [];
1530
- this.syncSessionPathOptionsForSource(
1531
- this.sessionFilterSource,
1532
- this.extractPathOptionsFromSessions(this.sessionsList),
1533
- true
1534
- );
1535
- if (this.sessionsList.length === 0) {
1536
- this.activeSession = null;
1537
- this.activeSessionMessages = [];
1538
- this.activeSessionDetailClipped = false;
1539
- this.cancelSessionTimelineSync();
1540
- this.sessionTimelineActiveKey = '';
1541
- this.sessionMessageRefMap = Object.create(null);
1542
- } else {
1543
- const oldKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
1544
- const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === oldKey);
1545
- this.activeSession = matched || this.sessionsList[0];
1546
- this.activeSessionMessages = [];
1547
- this.activeSessionDetailError = '';
1548
- this.activeSessionDetailClipped = false;
1549
- this.cancelSessionTimelineSync();
1550
- this.sessionTimelineActiveKey = '';
1551
- this.sessionMessageRefMap = Object.create(null);
1552
- await this.loadActiveSessionDetail();
1553
- }
1554
- void this.loadSessionPathOptions({ source: this.sessionFilterSource });
1555
- }
1556
- } catch (e) {
1557
- this.sessionsList = [];
1558
- this.activeSession = null;
1559
- this.activeSessionMessages = [];
1560
- this.activeSessionDetailClipped = false;
1561
- this.cancelSessionTimelineSync();
1562
- this.sessionTimelineActiveKey = '';
1563
- this.sessionMessageRefMap = Object.create(null);
1564
- this.showMessage('加载会话失败', 'error');
1565
- } finally {
1566
- this.sessionsLoading = false;
1567
- }
3026
+ const result = await loadSessionsHelper.call(this, api);
3027
+ this.pruneSessionPinnedMap(this.sessionsList);
3028
+ return result;
1568
3029
  },
1569
3030
 
1570
3031
  async selectSession(session) {
@@ -1572,11 +3033,13 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1572
3033
  if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
1573
3034
  this.activeSession = session;
1574
3035
  this.activeSessionMessages = [];
3036
+ this.resetSessionDetailPagination();
3037
+ this.resetSessionPreviewMessageRender();
1575
3038
  this.activeSessionDetailError = '';
1576
3039
  this.activeSessionDetailClipped = false;
1577
3040
  this.cancelSessionTimelineSync();
1578
3041
  this.sessionTimelineActiveKey = '';
1579
- this.sessionMessageRefMap = Object.create(null);
3042
+ this.clearSessionTimelineRefs();
1580
3043
  await this.loadActiveSessionDetail();
1581
3044
  },
1582
3045
 
@@ -1625,87 +3088,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1625
3088
  }
1626
3089
  },
1627
3090
 
1628
- async loadActiveSessionDetail() {
1629
- if (!this.activeSession) {
1630
- this.activeSessionMessages = [];
1631
- this.activeSessionDetailError = '';
1632
- this.activeSessionDetailClipped = false;
1633
- this.cancelSessionTimelineSync();
1634
- this.sessionTimelineActiveKey = '';
1635
- this.sessionMessageRefMap = Object.create(null);
1636
- return;
1637
- }
1638
-
1639
- const requestSeq = ++this.sessionDetailRequestSeq;
1640
- this.sessionDetailLoading = true;
1641
- this.activeSessionDetailError = '';
1642
- try {
1643
- const res = await api('session-detail', {
1644
- source: this.activeSession.source,
1645
- sessionId: this.activeSession.sessionId,
1646
- filePath: this.activeSession.filePath,
1647
- messageLimit: 300
1648
- });
1649
-
1650
- if (requestSeq !== this.sessionDetailRequestSeq) {
1651
- return;
1652
- }
1653
-
1654
- if (res.error) {
1655
- this.activeSessionMessages = [];
1656
- this.activeSessionDetailClipped = false;
1657
- this.activeSessionDetailError = res.error;
1658
- this.cancelSessionTimelineSync();
1659
- this.sessionTimelineActiveKey = '';
1660
- this.sessionMessageRefMap = Object.create(null);
1661
- return;
1662
- }
1663
-
1664
- const rawMessages = Array.isArray(res.messages) ? res.messages : [];
1665
- this.activeSessionMessages = rawMessages.map((message) => this.normalizeSessionMessage(message));
1666
- this.activeSessionDetailClipped = !!res.clipped;
1667
- if (this.activeSession) {
1668
- if (res.sourceLabel) {
1669
- this.activeSession.sourceLabel = res.sourceLabel;
1670
- }
1671
- if (res.sessionId) {
1672
- this.activeSession.sessionId = res.sessionId;
1673
- if (!this.activeSession.title) {
1674
- this.activeSession.title = res.sessionId;
1675
- }
1676
- }
1677
- if (res.filePath) {
1678
- this.activeSession.filePath = res.filePath;
1679
- }
1680
- }
1681
- if (res.updatedAt) {
1682
- this.activeSession.updatedAt = res.updatedAt;
1683
- }
1684
- if (res.cwd) {
1685
- this.activeSession.cwd = res.cwd;
1686
- }
1687
- if (Number.isFinite(res.totalMessages)) {
1688
- this.syncActiveSessionMessageCount(res.totalMessages);
1689
- }
1690
- this.$nextTick(() => {
1691
- this.updateSessionTimelineOffset();
1692
- this.scheduleSessionTimelineSync();
1693
- });
1694
- } catch (e) {
1695
- if (requestSeq !== this.sessionDetailRequestSeq) {
1696
- return;
1697
- }
1698
- this.activeSessionMessages = [];
1699
- this.activeSessionDetailClipped = false;
1700
- this.activeSessionDetailError = '加载会话内容失败: ' + e.message;
1701
- this.cancelSessionTimelineSync();
1702
- this.sessionTimelineActiveKey = '';
1703
- this.sessionMessageRefMap = Object.create(null);
1704
- } finally {
1705
- if (requestSeq === this.sessionDetailRequestSeq) {
1706
- this.sessionDetailLoading = false;
1707
- }
1708
- }
3091
+ async loadActiveSessionDetail(options = {}) {
3092
+ return loadActiveSessionDetailHelper.call(this, api, options);
1709
3093
  },
1710
3094
 
1711
3095
  downloadTextFile(fileName, content, mimeType = 'text/markdown;charset=utf-8') {
@@ -1751,17 +3135,75 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1751
3135
  }
1752
3136
  },
1753
3137
 
1754
- async switchProvider(name) {
3138
+ async quickSwitchProvider(name) {
3139
+ const target = String(name || '').trim();
3140
+ if (!target || target === this.pendingProviderSwitch) {
3141
+ return;
3142
+ }
3143
+ if (!this.providerSwitchInProgress && target === this.currentProvider) {
3144
+ return;
3145
+ }
3146
+ await this.switchProvider(target);
3147
+ },
3148
+
3149
+ async waitForCodexApplyIdle(maxWaitMs = 20000) {
3150
+ const startedAt = Date.now();
3151
+ while (this.codexApplying) {
3152
+ if ((Date.now() - startedAt) > maxWaitMs) {
3153
+ throw new Error('等待配置应用完成超时');
3154
+ }
3155
+ await new Promise((resolve) => setTimeout(resolve, 50));
3156
+ }
3157
+ },
3158
+
3159
+ async performProviderSwitch(name) {
3160
+ await this.waitForCodexApplyIdle();
1755
3161
  this.currentProvider = name;
1756
3162
  await this.loadModelsForProvider(name);
1757
3163
  if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
1758
3164
  this.currentModel = this.models[0];
1759
3165
  }
1760
3166
  if (getProviderConfigModeMeta(this.configMode)) {
3167
+ await this.waitForCodexApplyIdle();
1761
3168
  await this.applyCodexConfigDirect({ silent: true });
1762
3169
  }
1763
3170
  },
1764
3171
 
3172
+ async switchProvider(name) {
3173
+ const target = String(name || '').trim();
3174
+ if (!target) {
3175
+ return;
3176
+ }
3177
+ if (this.providerSwitchInProgress) {
3178
+ this.pendingProviderSwitch = target;
3179
+ return;
3180
+ }
3181
+ this.providerSwitchInProgress = true;
3182
+ let lastError = '';
3183
+ try {
3184
+ this.pendingProviderSwitch = '';
3185
+ const result = await runLatestOnlyQueue(target, {
3186
+ perform: async (queuedTarget) => {
3187
+ await this.performProviderSwitch(queuedTarget);
3188
+ },
3189
+ consumePending: () => {
3190
+ const queued = this.pendingProviderSwitch;
3191
+ this.pendingProviderSwitch = '';
3192
+ return queued;
3193
+ }
3194
+ });
3195
+ if (result && typeof result.lastError === 'string') {
3196
+ lastError = result.lastError;
3197
+ }
3198
+ } finally {
3199
+ this.providerSwitchInProgress = false;
3200
+ this.pendingProviderSwitch = '';
3201
+ }
3202
+ if (lastError) {
3203
+ this.showMessage(lastError, 'error');
3204
+ }
3205
+ },
3206
+
1765
3207
  async onModelChange() {
1766
3208
  await this.applyCodexConfigDirect();
1767
3209
  },
@@ -1952,9 +3394,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1952
3394
  return;
1953
3395
  }
1954
3396
  this.agentsContent = res.content || '';
3397
+ this.agentsOriginalContent = this.agentsContent;
1955
3398
  this.agentsPath = res.path || '';
1956
3399
  this.agentsExists = !!res.exists;
1957
3400
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3401
+ this.resetAgentsDiffState();
1958
3402
  this.showAgentsModal = true;
1959
3403
  } catch (e) {
1960
3404
  this.showMessage('加载文件失败', 'error');
@@ -1963,8 +3407,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1963
3407
  }
1964
3408
  },
1965
3409
 
1966
- ...createSkillsMethods({ api }),
1967
-
3410
+ ...createSkillsMethods({ api }),
3411
+
1968
3412
  async openOpenclawAgentsEditor() {
1969
3413
  this.setAgentsModalContext('openclaw');
1970
3414
  this.agentsLoading = true;
@@ -1978,9 +3422,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1978
3422
  this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
1979
3423
  }
1980
3424
  this.agentsContent = res.content || '';
3425
+ this.agentsOriginalContent = this.agentsContent;
1981
3426
  this.agentsPath = res.path || '';
1982
3427
  this.agentsExists = !!res.exists;
1983
3428
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3429
+ this.resetAgentsDiffState();
1984
3430
  this.showAgentsModal = true;
1985
3431
  } catch (e) {
1986
3432
  this.showMessage('加载文件失败', 'error');
@@ -2007,9 +3453,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2007
3453
  this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
2008
3454
  }
2009
3455
  this.agentsContent = res.content || '';
3456
+ this.agentsOriginalContent = this.agentsContent;
2010
3457
  this.agentsPath = res.path || '';
2011
3458
  this.agentsExists = !!res.exists;
2012
3459
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3460
+ this.resetAgentsDiffState();
2013
3461
  this.showAgentsModal = true;
2014
3462
  } catch (e) {
2015
3463
  this.showMessage('加载文件失败', 'error');
@@ -2038,18 +3486,272 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2038
3486
  this.agentsWorkspaceFileName = '';
2039
3487
  },
2040
3488
 
2041
- closeAgentsModal() {
3489
+ resetAgentsDiffState() {
3490
+ this.agentsDiffVisible = false;
3491
+ this.agentsDiffLoading = false;
3492
+ this.agentsDiffError = '';
3493
+ this.agentsDiffLines = [];
3494
+ this.agentsDiffStats = {
3495
+ added: 0,
3496
+ removed: 0,
3497
+ unchanged: 0
3498
+ };
3499
+ this.agentsDiffTruncated = false;
3500
+ this.agentsDiffHasChangesValue = false;
3501
+ this.agentsDiffFingerprint = '';
3502
+ this._agentsDiffPreviewRequestToken = null;
3503
+ },
3504
+ handleGlobalKeydown(event) {
3505
+ if (!event || event.key !== 'Escape') {
3506
+ return;
3507
+ }
3508
+ if (this.showConfirmDialog) {
3509
+ event.preventDefault();
3510
+ event.stopPropagation();
3511
+ this.resolveConfirmDialog(false);
3512
+ return;
3513
+ }
3514
+ if (!this.showAgentsModal) {
3515
+ return;
3516
+ }
3517
+ event.preventDefault();
3518
+ event.stopPropagation();
3519
+ if (this.agentsSaving || this.agentsDiffLoading) {
3520
+ return;
3521
+ }
3522
+ if (this.agentsDiffVisible) {
3523
+ this.resetAgentsDiffState();
3524
+ return;
3525
+ }
3526
+ this.closeAgentsModal();
3527
+ },
3528
+ hasPendingAgentsDraft() {
3529
+ if (!this.showAgentsModal || this.agentsLoading || this.agentsSaving) {
3530
+ return false;
3531
+ }
3532
+ return this.hasAgentsContentChanged() || this.agentsDiffVisible;
3533
+ },
3534
+ handleBeforeUnload(event) {
3535
+ if (!this.hasPendingAgentsDraft()) {
3536
+ return;
3537
+ }
3538
+ if (event && typeof event.preventDefault === 'function') {
3539
+ event.preventDefault();
3540
+ event.returnValue = '';
3541
+ }
3542
+ return '';
3543
+ },
3544
+ hasAgentsContentChanged() {
3545
+ const original = typeof this.agentsOriginalContent === 'string' ? this.agentsOriginalContent : '';
3546
+ const current = typeof this.agentsContent === 'string' ? this.agentsContent : '';
3547
+ return original !== current;
3548
+ },
3549
+ requestConfirmDialog(options = {}) {
3550
+ if (typeof this.confirmDialogResolver === 'function') {
3551
+ this.confirmDialogResolver(false);
3552
+ }
3553
+ this.confirmDialogTitle = typeof options.title === 'string' && options.title.trim()
3554
+ ? options.title.trim()
3555
+ : '请确认操作';
3556
+ this.confirmDialogMessage = typeof options.message === 'string' ? options.message : '';
3557
+ this.confirmDialogConfirmText = typeof options.confirmText === 'string' && options.confirmText.trim()
3558
+ ? options.confirmText.trim()
3559
+ : '确认';
3560
+ this.confirmDialogCancelText = typeof options.cancelText === 'string' && options.cancelText.trim()
3561
+ ? options.cancelText.trim()
3562
+ : '取消';
3563
+ this.confirmDialogDanger = !!options.danger;
3564
+ this.showConfirmDialog = true;
3565
+ return new Promise((resolve) => {
3566
+ this.confirmDialogResolver = resolve;
3567
+ });
3568
+ },
3569
+ resolveConfirmDialog(confirmed) {
3570
+ const resolver = typeof this.confirmDialogResolver === 'function'
3571
+ ? this.confirmDialogResolver
3572
+ : null;
3573
+ this.showConfirmDialog = false;
3574
+ this.confirmDialogTitle = '';
3575
+ this.confirmDialogMessage = '';
3576
+ this.confirmDialogConfirmText = '确认';
3577
+ this.confirmDialogCancelText = '取消';
3578
+ this.confirmDialogDanger = false;
3579
+ this.confirmDialogResolver = null;
3580
+ if (resolver) {
3581
+ resolver(!!confirmed);
3582
+ }
3583
+ },
3584
+ closeConfirmDialog() {
3585
+ this.resolveConfirmDialog(false);
3586
+ },
3587
+ onAgentsContentInput() {
3588
+ if (this.agentsDiffVisible || this.agentsDiffLines.length) {
3589
+ this.resetAgentsDiffState();
3590
+ }
3591
+ },
3592
+ buildAgentsDiffFingerprint() {
3593
+ const context = this.agentsContext || 'codex';
3594
+ const fileName = context === 'openclaw-workspace'
3595
+ ? (this.agentsWorkspaceFileName || '')
3596
+ : '';
3597
+ const lineEnding = this.agentsLineEnding || '\n';
3598
+ const content = typeof this.agentsContent === 'string' ? this.agentsContent : '';
3599
+ const original = typeof this.agentsOriginalContent === 'string' ? this.agentsOriginalContent : '';
3600
+ return `${context}::${fileName}::${lineEnding}::${content.length}::${content}::${original.length}::${original}`;
3601
+ },
3602
+ async prepareAgentsDiff() {
3603
+ const requestFingerprint = this.buildAgentsDiffFingerprint();
3604
+ const requestToken = Symbol('agents-diff-preview');
3605
+ this._agentsDiffPreviewRequestToken = requestToken;
3606
+ this.agentsDiffVisible = true;
3607
+ this.agentsDiffLoading = true;
3608
+ this.agentsDiffError = '';
3609
+ this.agentsDiffLines = [];
3610
+ this.agentsDiffStats = {
3611
+ added: 0,
3612
+ removed: 0,
3613
+ unchanged: 0
3614
+ };
3615
+ this.agentsDiffTruncated = false;
3616
+ this.agentsDiffHasChangesValue = false;
3617
+ try {
3618
+ const shouldApplyPreviewState = () => shouldApplyAgentsDiffPreviewResponse({
3619
+ isVisible: this.agentsDiffVisible,
3620
+ requestToken,
3621
+ activeRequestToken: this._agentsDiffPreviewRequestToken,
3622
+ requestFingerprint,
3623
+ currentFingerprint: this.buildAgentsDiffFingerprint()
3624
+ });
3625
+ const applyPreviewState = (diff) => {
3626
+ if (!shouldApplyPreviewState()) {
3627
+ return false;
3628
+ }
3629
+ const normalizedDiff = diff && typeof diff === 'object' ? diff : {};
3630
+ const rawLines = Array.isArray(normalizedDiff.lines) ? normalizedDiff.lines : [];
3631
+ this.agentsDiffLines = rawLines.filter(line => line && line.type);
3632
+ this.agentsDiffTruncated = !!normalizedDiff.truncated;
3633
+ this.agentsDiffHasChangesValue = !!normalizedDiff.hasChanges;
3634
+ if (normalizedDiff.stats && typeof normalizedDiff.stats === 'object') {
3635
+ this.agentsDiffStats = {
3636
+ added: Number(normalizedDiff.stats.added || 0),
3637
+ removed: Number(normalizedDiff.stats.removed || 0),
3638
+ unchanged: Number(normalizedDiff.stats.unchanged || 0)
3639
+ };
3640
+ } else {
3641
+ const stats = { added: 0, removed: 0, unchanged: 0 };
3642
+ for (const line of this.agentsDiffLines) {
3643
+ if (line && line.type === 'add') stats.added += 1;
3644
+ else if (line && line.type === 'del') stats.removed += 1;
3645
+ else stats.unchanged += 1;
3646
+ }
3647
+ this.agentsDiffStats = stats;
3648
+ }
3649
+ this.agentsDiffFingerprint = requestFingerprint;
3650
+ return true;
3651
+ };
3652
+ const previewRequest = buildAgentsDiffPreviewRequest({
3653
+ baseContent: this.agentsOriginalContent,
3654
+ content: this.agentsContent,
3655
+ lineEnding: this.agentsLineEnding,
3656
+ context: this.agentsContext,
3657
+ fileName: this.agentsWorkspaceFileName
3658
+ });
3659
+ if (previewRequest.exceedsBodyLimit) {
3660
+ applyPreviewState(buildAgentsDiffPreview({
3661
+ baseContent: this.agentsOriginalContent,
3662
+ content: this.agentsContent
3663
+ }));
3664
+ return;
3665
+ }
3666
+ const res = await apiWithMeta('preview-agents-diff', previewRequest.params);
3667
+ if (!shouldApplyPreviewState()) {
3668
+ return;
3669
+ }
3670
+ if (res.error) {
3671
+ if (isAgentsDiffPreviewPayloadTooLarge(res)) {
3672
+ applyPreviewState(buildAgentsDiffPreview({
3673
+ baseContent: this.agentsOriginalContent,
3674
+ content: this.agentsContent
3675
+ }));
3676
+ return;
3677
+ }
3678
+ this.agentsDiffError = res.error;
3679
+ return;
3680
+ }
3681
+ applyPreviewState(res.diff);
3682
+ } catch (e) {
3683
+ if (shouldApplyAgentsDiffPreviewResponse({
3684
+ isVisible: this.agentsDiffVisible,
3685
+ requestToken,
3686
+ activeRequestToken: this._agentsDiffPreviewRequestToken,
3687
+ requestFingerprint,
3688
+ currentFingerprint: this.buildAgentsDiffFingerprint()
3689
+ })) {
3690
+ this.agentsDiffError = '生成差异失败';
3691
+ }
3692
+ } finally {
3693
+ if (this._agentsDiffPreviewRequestToken === requestToken) {
3694
+ this.agentsDiffLoading = false;
3695
+ }
3696
+ }
3697
+ },
3698
+
3699
+ async closeAgentsModal(options = {}) {
3700
+ const force = !!(options && options.force);
3701
+ const shouldConfirmClose = !force
3702
+ && this.hasPendingAgentsDraft();
3703
+ if (shouldConfirmClose) {
3704
+ const message = this.agentsDiffVisible
3705
+ ? '当前处于差异预览模式,改动尚未保存。确认放弃改动并关闭吗?'
3706
+ : '存在未保存改动,确认放弃改动并关闭吗?(关闭页面或应用也会丢失改动)';
3707
+ const confirmed = await this.requestConfirmDialog({
3708
+ title: '放弃未保存改动',
3709
+ message,
3710
+ confirmText: '放弃并关闭',
3711
+ cancelText: '继续编辑',
3712
+ danger: true
3713
+ });
3714
+ if (!confirmed) {
3715
+ return;
3716
+ }
3717
+ }
2042
3718
  this.showAgentsModal = false;
2043
3719
  this.agentsContent = '';
3720
+ this.agentsOriginalContent = '';
2044
3721
  this.agentsPath = '';
2045
3722
  this.agentsExists = false;
2046
3723
  this.agentsLineEnding = '\n';
2047
3724
  this.agentsSaving = false;
2048
3725
  this.agentsWorkspaceFileName = '';
3726
+ this.resetAgentsDiffState();
2049
3727
  this.setAgentsModalContext('codex');
2050
3728
  },
2051
3729
 
2052
3730
  async applyAgentsContent() {
3731
+ if (!this.agentsDiffVisible) {
3732
+ if (!this.hasAgentsContentChanged()) {
3733
+ this.showMessage('未检测到改动', 'info');
3734
+ return;
3735
+ }
3736
+ await this.prepareAgentsDiff();
3737
+ return;
3738
+ }
3739
+ if (this.agentsDiffLoading) {
3740
+ return;
3741
+ }
3742
+ if (this.agentsDiffError) {
3743
+ this.showMessage(this.agentsDiffError, 'error');
3744
+ return;
3745
+ }
3746
+ const fingerprint = this.buildAgentsDiffFingerprint();
3747
+ if (this.agentsDiffFingerprint !== fingerprint) {
3748
+ await this.prepareAgentsDiff();
3749
+ return;
3750
+ }
3751
+ if (!this.agentsDiffHasChanges) {
3752
+ this.showMessage('未检测到改动', 'info');
3753
+ return;
3754
+ }
2053
3755
  this.agentsSaving = true;
2054
3756
  try {
2055
3757
  let action = 'apply-agents-file';
@@ -2072,7 +3774,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2072
3774
  ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
2073
3775
  : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存');
2074
3776
  this.showMessage(successLabel, 'success');
2075
- this.closeAgentsModal();
3777
+ this.closeAgentsModal({ force: true });
2076
3778
  } catch (e) {
2077
3779
  this.showMessage('保存失败', 'error');
2078
3780
  } finally {
@@ -2341,12 +4043,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2341
4043
  }
2342
4044
  const existing = this.claudeConfigs[name] || {};
2343
4045
  this.currentClaudeModel = model;
2344
- this.claudeConfigs[name] = {
2345
- apiKey: existing.apiKey || '',
2346
- baseUrl: existing.baseUrl || '',
2347
- model: model,
2348
- hasKey: !!existing.apiKey
2349
- };
4046
+ this.claudeConfigs[name] = this.mergeClaudeConfig(existing, { model });
2350
4047
  this.saveClaudeConfigs();
2351
4048
  this.updateClaudeModelsCurrent();
2352
4049
  if (!this.claudeConfigs[name].apiKey) {
@@ -2373,12 +4070,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2373
4070
 
2374
4071
  updateConfig() {
2375
4072
  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
- };
4073
+ this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
2382
4074
  this.saveClaudeConfigs();
2383
4075
  this.showMessage('操作成功', 'success');
2384
4076
  this.closeEditConfigModal();
@@ -2394,12 +4086,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2394
4086
 
2395
4087
  async saveAndApplyConfig() {
2396
4088
  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
- };
4089
+ this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
2403
4090
  this.saveClaudeConfigs();
2404
4091
 
2405
4092
  const config = this.claudeConfigs[name];
@@ -2438,12 +4125,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2438
4125
  return this.showMessage('配置已存在', 'info');
2439
4126
  }
2440
4127
 
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
- };
4128
+ this.claudeConfigs[name] = this.mergeClaudeConfig({}, this.newClaudeConfig);
2447
4129
 
2448
4130
  this.currentClaudeConfig = name;
2449
4131
  this.saveClaudeConfigs();
@@ -2452,12 +4134,18 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2452
4134
  this.refreshClaudeModelContext();
2453
4135
  },
2454
4136
 
2455
- deleteClaudeConfig(name) {
4137
+ async deleteClaudeConfig(name) {
2456
4138
  if (Object.keys(this.claudeConfigs).length <= 1) {
2457
4139
  return this.showMessage('至少保留一项', 'error');
2458
4140
  }
2459
-
2460
- if (!confirm(`确定删除配置 "${name}"?`)) return;
4141
+ const confirmed = await this.requestConfirmDialog({
4142
+ title: '删除 Claude 配置',
4143
+ message: `确定删除配置 "${name}"?`,
4144
+ confirmText: '删除',
4145
+ cancelText: '取消',
4146
+ danger: true
4147
+ });
4148
+ if (!confirmed) return;
2461
4149
 
2462
4150
  delete this.claudeConfigs[name];
2463
4151
  if (this.currentClaudeConfig === name) {
@@ -2474,6 +4162,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2474
4162
  const config = this.claudeConfigs[name];
2475
4163
 
2476
4164
  if (!config.apiKey) {
4165
+ if (config.externalCredentialType) {
4166
+ return this.showMessage('检测到外部 Claude 认证状态;当前仅支持展示,若需由 codexmate 接管请补充 API Key', 'info');
4167
+ }
2477
4168
  return this.showMessage('请先配置 API Key', 'error');
2478
4169
  }
2479
4170
 
@@ -3540,11 +5231,18 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
3540
5231
  }
3541
5232
  },
3542
5233
 
3543
- deleteOpenclawConfig(name) {
5234
+ async deleteOpenclawConfig(name) {
3544
5235
  if (Object.keys(this.openclawConfigs).length <= 1) {
3545
5236
  return this.showMessage('至少保留一项', 'error');
3546
5237
  }
3547
- if (!confirm(`确定删除配置 "${name}"?`)) return;
5238
+ const confirmed = await this.requestConfirmDialog({
5239
+ title: '删除 OpenClaw 配置',
5240
+ message: `确定删除配置 "${name}"?`,
5241
+ confirmText: '删除',
5242
+ cancelText: '取消',
5243
+ danger: true
5244
+ });
5245
+ if (!confirmed) return;
3548
5246
  delete this.openclawConfigs[name];
3549
5247
  if (this.currentOpenclawConfig === name) {
3550
5248
  this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
@@ -4070,4 +5768,5 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
4070
5768
 
4071
5769
  app.mount('#app');
4072
5770
  });
4073
-
5771
+
5772
+