codexmate 0.0.17 → 0.0.19

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,10 +4,13 @@
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,
@@ -16,6 +19,12 @@
16
19
  runLatestOnlyQueue,
17
20
  shouldForceCompactLayoutMode
18
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';
19
28
  import {
20
29
  CONFIG_MODE_SET,
21
30
  getProviderConfigModeMeta,
@@ -43,6 +52,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
43
52
  const API_BASE = (location && location.origin && location.origin !== 'null')
44
53
  ? location.origin
45
54
  : 'http://localhost:3737';
55
+ const SESSION_TRASH_LIST_LIMIT = 500;
56
+ const SESSION_TRASH_PAGE_SIZE = 200;
46
57
  const DEFAULT_OPENCLAW_TEMPLATE = `{
47
58
  // OpenClaw config (JSON5)
48
59
  agent: {
@@ -55,15 +66,45 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
55
66
  }
56
67
  }`;
57
68
 
58
- async function api(action, params = {}) {
59
- const res = await fetch(`${API_BASE}/api`, {
69
+ async function postApi(action, params = {}) {
70
+ return await fetch(`${API_BASE}/api`, {
60
71
  method: 'POST',
61
72
  headers: { 'Content-Type': 'application/json' },
62
73
  body: JSON.stringify({ action, params })
63
74
  });
75
+ }
76
+
77
+ async function api(action, params = {}) {
78
+ const res = await postApi(action, params);
64
79
  return await res.json();
65
80
  }
66
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
+
67
108
  const app = createApp({
68
109
  data() {
69
110
  return {
@@ -97,6 +138,15 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
97
138
  showAgentsModal: false,
98
139
  showSkillsModal: false,
99
140
  showInstallModal: false,
141
+ showConfirmDialog: false,
142
+ confirmDialogTitle: '',
143
+ confirmDialogMessage: '',
144
+ confirmDialogConfirmText: '确认',
145
+ confirmDialogCancelText: '取消',
146
+ confirmDialogDanger: false,
147
+ confirmDialogConfirmDisabled: false,
148
+ confirmDialogDisableWhen: null,
149
+ confirmDialogResolver: null,
100
150
  configTemplateContent: '',
101
151
  configTemplateApplying: false,
102
152
  codexApplying: false,
@@ -106,9 +156,23 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
106
156
  agentsLineEnding: '\n',
107
157
  agentsLoading: false,
108
158
  agentsSaving: false,
159
+ agentsOriginalContent: '',
160
+ agentsDiffVisible: false,
161
+ agentsDiffLoading: false,
162
+ agentsDiffError: '',
163
+ agentsDiffLines: [],
164
+ agentsDiffStats: {
165
+ added: 0,
166
+ removed: 0,
167
+ unchanged: 0
168
+ },
169
+ agentsDiffTruncated: false,
170
+ agentsDiffHasChangesValue: false,
171
+ agentsDiffFingerprint: '',
109
172
  agentsContext: 'codex',
110
173
  agentsModalTitle: 'AGENTS.md 编辑器',
111
174
  agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
175
+ skillsTargetApp: 'codex',
112
176
  skillsRootPath: '',
113
177
  skillsList: [],
114
178
  skillsSelectedNames: [],
@@ -122,7 +186,12 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
122
186
  skillsImporting: false,
123
187
  skillsZipImporting: false,
124
188
  skillsExporting: false,
189
+ skillsMarketLoading: false,
190
+ skillsMarketLocalLoadedOnce: false,
191
+ skillsMarketImportLoadedOnce: false,
192
+ sessionPinnedMap: {},
125
193
  sessionsList: [],
194
+ sessionsLoadedOnce: false,
126
195
  sessionsLoading: false,
127
196
  sessionFilterSource: 'all',
128
197
  sessionPathFilter: '',
@@ -152,13 +221,31 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
152
221
  activeSessionDetailClipped: false,
153
222
  sessionDetailLoading: false,
154
223
  sessionDetailRequestSeq: 0,
224
+ sessionDetailInitialMessageLimit: 80,
225
+ sessionDetailFetchStep: 80,
226
+ sessionDetailMessageLimit: 80,
227
+ sessionDetailMessageLimitCap: 1000,
155
228
  sessionTimelineActiveKey: '',
156
229
  sessionTimelineRafId: 0,
230
+ sessionTimelineLastSyncAt: 0,
231
+ sessionTimelineLastScrollTop: 0,
232
+ sessionTimelineLastAnchorY: 0,
233
+ sessionTimelineLastDirection: 0,
234
+ sessionTimelineEnabled: true,
157
235
  sessionMessageRefMap: Object.create(null),
236
+ sessionMessageRefBinderMap: Object.create(null),
158
237
  sessionPreviewScrollEl: null,
159
238
  sessionPreviewContainerEl: null,
160
239
  sessionPreviewHeaderEl: null,
161
240
  sessionPreviewHeaderResizeObserver: null,
241
+ sessionListRenderEnabled: false,
242
+ sessionPreviewRenderEnabled: false,
243
+ sessionTabRenderTicket: 0,
244
+ sessionPreviewVisibleCount: 0,
245
+ sessionPreviewInitialBatchSize: 12,
246
+ sessionPreviewLoadStep: 24,
247
+ sessionPreviewPendingVisibleCount: 0,
248
+ sessionPreviewLoadingMore: false,
162
249
  sessionStandalone: false,
163
250
  sessionStandaloneError: '',
164
251
  sessionStandaloneText: '',
@@ -275,27 +362,25 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
275
362
  codexDownloadLoading: false,
276
363
  codexDownloadProgress: 0,
277
364
  codexDownloadTimer: null,
365
+ settingsTab: 'backup',
366
+ sessionTrashItems: [],
367
+ sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
368
+ sessionTrashTotalCount: 0,
369
+ sessionTrashCountLoadedOnce: false,
370
+ sessionTrashLoadedOnce: false,
371
+ sessionTrashLastLoadFailed: false,
372
+ sessionTrashCountRequestToken: 0,
373
+ sessionTrashListRequestToken: 0,
374
+ sessionTrashCountPendingOptions: null,
375
+ sessionTrashPendingOptions: null,
376
+ sessionTrashCountLoading: false,
377
+ sessionTrashLoading: false,
378
+ sessionTrashRestoring: {},
379
+ sessionTrashPurging: {},
380
+ sessionTrashClearing: false,
278
381
  claudeImportLoading: false,
279
382
  codexImportLoading: false,
280
383
  codexAuthProfiles: [],
281
- codexAuthImportLoading: false,
282
- codexAuthSwitching: {},
283
- codexAuthDeleting: {},
284
- proxySettings: {
285
- enabled: false,
286
- host: '127.0.0.1',
287
- port: 8318,
288
- provider: '',
289
- authSource: 'provider',
290
- timeoutMs: 30000
291
- },
292
- proxyRuntime: null,
293
- proxyLoading: false,
294
- proxySaving: false,
295
- proxyStarting: false,
296
- proxyStopping: false,
297
- proxyApplying: false,
298
- showProxyAdvanced: false,
299
384
  forceCompactLayout: false
300
385
  }
301
386
  },
@@ -309,7 +394,10 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
309
394
  this.sessionResumeWithYolo = true;
310
395
  }
311
396
  this.restoreSessionFilterCache();
397
+ this.restoreSessionPinnedMap();
312
398
  window.addEventListener('resize', this.onWindowResize);
399
+ window.addEventListener('keydown', this.handleGlobalKeydown);
400
+ window.addEventListener('beforeunload', this.handleBeforeUnload);
313
401
  const savedConfigs = localStorage.getItem('claudeConfigs');
314
402
  if (savedConfigs) {
315
403
  try {
@@ -346,25 +434,104 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
346
434
  this.loadAll();
347
435
  },
348
436
  beforeUnmount() {
349
- this.cancelSessionTimelineSync();
437
+ this.teardownSessionTabRender();
438
+ this.cancelScheduledSessionTabDeferredTeardown();
350
439
  this.disconnectSessionPreviewHeaderResizeObserver();
351
440
  window.removeEventListener('resize', this.onWindowResize);
441
+ window.removeEventListener('keydown', this.handleGlobalKeydown);
442
+ window.removeEventListener('beforeunload', this.handleBeforeUnload);
352
443
  this.applyCompactLayoutClass(false);
353
444
  this.sessionPreviewScrollEl = null;
354
445
  this.sessionPreviewContainerEl = null;
355
446
  this.sessionPreviewHeaderEl = null;
356
- this.sessionMessageRefMap = Object.create(null);
447
+ this.clearSessionTimelineRefs();
357
448
  },
358
449
 
359
450
  computed: {
360
451
  isSessionQueryEnabled() {
361
452
  return isSessionQueryEnabled(this.sessionFilterSource);
362
453
  },
454
+ activeSessionExportKey() {
455
+ return this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
456
+ },
457
+ sortedSessionsList() {
458
+ const list = Array.isArray(this.sessionsList) ? this.sessionsList : [];
459
+ if (list.length === 0) return [];
460
+ const pinnedMap = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
461
+ ? this.sessionPinnedMap
462
+ : {};
463
+ let hasPinned = false;
464
+ const decorated = list.map((session, index) => {
465
+ const key = session ? this.getSessionExportKey(session) : '';
466
+ const rawPinnedAt = key ? pinnedMap[key] : 0;
467
+ const pinnedAt = Number.isFinite(Number(rawPinnedAt))
468
+ ? Math.floor(Number(rawPinnedAt))
469
+ : 0;
470
+ const isPinned = pinnedAt > 0;
471
+ if (isPinned) {
472
+ hasPinned = true;
473
+ }
474
+ return { session, index, pinnedAt, isPinned };
475
+ });
476
+ if (!hasPinned) return list;
477
+ decorated.sort((a, b) => {
478
+ if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1;
479
+ if (a.isPinned && a.pinnedAt !== b.pinnedAt) return b.pinnedAt - a.pinnedAt;
480
+ return a.index - b.index;
481
+ });
482
+ return decorated.map(item => item.session);
483
+ },
484
+ activeSessionVisibleMessages() {
485
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
486
+ return [];
487
+ }
488
+ const list = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages : [];
489
+ const rawCount = Number(this.sessionPreviewVisibleCount);
490
+ const visibleCount = Number.isFinite(rawCount)
491
+ ? Math.max(0, Math.floor(rawCount))
492
+ : 0;
493
+ if (visibleCount <= 0) {
494
+ if (!list.length) return [];
495
+ // Defensive fallback: avoid getting stuck in "正在渲染会话内容..."
496
+ // when visible count has not been primed yet.
497
+ return list.slice(0, Math.min(8, list.length));
498
+ }
499
+ if (visibleCount >= list.length) return list;
500
+ return list.slice(0, visibleCount);
501
+ },
502
+ canLoadMoreSessionMessages() {
503
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
504
+ return false;
505
+ }
506
+ const total = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages.length : 0;
507
+ const visible = Array.isArray(this.activeSessionVisibleMessages) ? this.activeSessionVisibleMessages.length : 0;
508
+ return total > visible;
509
+ },
510
+ sessionPreviewRemainingCount() {
511
+ const total = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages.length : 0;
512
+ const visible = Array.isArray(this.activeSessionVisibleMessages) ? this.activeSessionVisibleMessages.length : 0;
513
+ return Math.max(0, total - visible);
514
+ },
363
515
  sessionTimelineNodes() {
364
- return buildSessionTimelineNodes(this.activeSessionMessages, {
516
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
517
+ return [];
518
+ }
519
+ return buildSessionTimelineNodes(this.activeSessionVisibleMessages, {
365
520
  getKey: (message, index) => this.getRecordRenderKey(message, index)
366
521
  });
367
522
  },
523
+ sessionTimelineNodeKeyMap() {
524
+ const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
525
+ if (!nodes.length) {
526
+ return Object.create(null);
527
+ }
528
+ const map = Object.create(null);
529
+ for (const node of nodes) {
530
+ if (!node || !node.key) continue;
531
+ map[node.key] = true;
532
+ }
533
+ return map;
534
+ },
368
535
  sessionTimelineActiveTitle() {
369
536
  if (!this.sessionTimelineActiveKey) return '';
370
537
  const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
@@ -377,6 +544,15 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
377
544
  }
378
545
  return '当前来源暂不支持关键词检索';
379
546
  },
547
+ agentsDiffHasChanges() {
548
+ if (this.agentsDiffTruncated) {
549
+ return !!this.agentsDiffHasChangesValue;
550
+ }
551
+ const stats = this.agentsDiffStats || {};
552
+ const added = Number(stats.added || 0);
553
+ const removed = Number(stats.removed || 0);
554
+ return added > 0 || removed > 0;
555
+ },
380
556
  claudeModelHasList() {
381
557
  return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
382
558
  },
@@ -388,19 +564,16 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
388
564
  }
389
565
  return list;
390
566
  },
391
- proxyProviderOptions() {
392
- const source = Array.isArray(this.providersList) ? this.providersList : [];
393
- const list = source
394
- .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
395
- .filter((name) => name && name !== 'codexmate-proxy');
396
- return Array.from(new Set(list));
397
- },
398
- proxyRuntimeDisplayProvider() {
399
- if (!this.proxyRuntime) return '';
400
- const value = typeof this.proxyRuntime.provider === 'string'
401
- ? this.proxyRuntime.provider.trim()
402
- : '';
403
- return value || 'local';
567
+ hasLocalAndProxy() {
568
+ return false;
569
+ },
570
+ displayCurrentProvider() {
571
+ const current = String(this.currentProvider || '').trim();
572
+ return current;
573
+ },
574
+ displayProvidersList() {
575
+ const list = Array.isArray(this.providersList) ? this.providersList : [];
576
+ return list.filter((item) => String(item && item.name ? item.name : '').trim().toLowerCase() !== 'codexmate-proxy');
404
577
  },
405
578
  installTargetCards() {
406
579
  const targets = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : [];
@@ -416,6 +589,29 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
416
589
  installRegistryPreview() {
417
590
  return this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
418
591
  },
592
+ visibleSessionTrashItems() {
593
+ const items = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
594
+ const visibleCount = Number(this.sessionTrashVisibleCount);
595
+ const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0
596
+ ? Math.floor(visibleCount)
597
+ : SESSION_TRASH_PAGE_SIZE;
598
+ return items.slice(0, safeVisibleCount);
599
+ },
600
+ sessionTrashHasMoreItems() {
601
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
602
+ return this.visibleSessionTrashItems.length < totalItems;
603
+ },
604
+ sessionTrashHiddenCount() {
605
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
606
+ return Math.max(0, totalItems - this.visibleSessionTrashItems.length);
607
+ },
608
+ sessionTrashCount() {
609
+ const totalCount = Number(this.sessionTrashTotalCount);
610
+ if (Number.isFinite(totalCount) && totalCount >= 0) {
611
+ return Math.max(0, Math.floor(totalCount));
612
+ }
613
+ return Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
614
+ },
419
615
  ...createSkillsComputed(),
420
616
 
421
617
  ...createConfigModeComputed(),
@@ -428,7 +624,6 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
428
624
  if (this.codexApplying || this.configTemplateApplying || this.openclawApplying) tasks.push('配置应用');
429
625
  if (this.agentsSaving) tasks.push('AGENTS 保存');
430
626
  if (this.skillsLoading || this.skillsDeleting || this.skillsScanningImports || this.skillsImporting || this.skillsZipImporting || this.skillsExporting) tasks.push('Skills 管理');
431
- if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) tasks.push('代理更新');
432
627
  return tasks.length ? tasks.join(' / ') : '空闲';
433
628
  },
434
629
  inspectorMessageSummary() {
@@ -468,15 +663,6 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
468
663
  }
469
664
  return '正常';
470
665
  },
471
- inspectorProxyStatus() {
472
- if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) {
473
- return '状态更新中';
474
- }
475
- if (this.proxyRuntime && this.proxyRuntime.running === true) {
476
- return `运行中(${this.proxyRuntimeDisplayProvider})`;
477
- }
478
- return '未运行';
479
- },
480
666
  installTroubleshootingTips() {
481
667
  const platform = this.resolveInstallPlatform();
482
668
  if (platform === 'win32') {
@@ -540,12 +726,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
540
726
  }
541
727
 
542
728
  try {
543
- await Promise.all([
544
- this.loadCodexAuthProfiles(),
545
- this.loadProxyStatus()
546
- ]);
729
+ await this.loadCodexAuthProfiles();
547
730
  } catch (e) {
548
- // 认证/代理状态加载失败不阻塞主界面
731
+ // 认证状态加载失败不阻塞主界面
549
732
  }
550
733
  },
551
734
 
@@ -807,21 +990,413 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
807
990
  const normalizedMode = typeof mode === 'string'
808
991
  ? mode.trim().toLowerCase()
809
992
  : '';
810
- this.mainTab = 'config';
811
993
  this.configMode = CONFIG_MODE_SET.has(normalizedMode) ? normalizedMode : 'codex';
812
- if (this.configMode === 'claude') {
813
- this.refreshClaudeModelContext();
994
+ if (this.mainTab === 'config') {
995
+ if (this.configMode === 'claude') {
996
+ const expectedMainTab = 'config';
997
+ const expectedConfigMode = 'claude';
998
+ const refresh = () => {
999
+ if (this.mainTab !== expectedMainTab || this.configMode !== expectedConfigMode) {
1000
+ return;
1001
+ }
1002
+ this.refreshClaudeModelContext();
1003
+ };
1004
+ if (typeof this.scheduleAfterFrame === 'function') {
1005
+ this.scheduleAfterFrame(refresh);
1006
+ } else {
1007
+ refresh();
1008
+ }
1009
+ }
1010
+ this.scheduleAfterFrame(() => {
1011
+ this.clearMainTabSwitchIntent('config');
1012
+ });
1013
+ return;
814
1014
  }
1015
+ this.switchMainTab('config');
815
1016
  },
816
1017
 
1018
+ ensureMainTabSwitchState() {
1019
+ if (this.__mainTabSwitchState) {
1020
+ return this.__mainTabSwitchState;
1021
+ }
1022
+ this.__mainTabSwitchState = {
1023
+ intent: '',
1024
+ pendingTarget: '',
1025
+ ticket: 0
1026
+ };
1027
+ return this.__mainTabSwitchState;
1028
+ },
1029
+ ensureImmediateNavDomState() {
1030
+ if (typeof document === 'undefined') {
1031
+ return {
1032
+ navNodes: [],
1033
+ sessionPanelEl: null
1034
+ };
1035
+ }
1036
+ if (!this.__immediateNavDomState) {
1037
+ this.__immediateNavDomState = {
1038
+ navNodes: [],
1039
+ sessionPanelEl: null
1040
+ };
1041
+ }
1042
+ const state = this.__immediateNavDomState;
1043
+ const needsNavRefresh = !Array.isArray(state.navNodes)
1044
+ || !state.navNodes.length
1045
+ || state.navNodes.some((node) => !node || !node.isConnected);
1046
+ if (needsNavRefresh) {
1047
+ state.navNodes = Array.from(document.querySelectorAll('[data-main-tab]'));
1048
+ }
1049
+ if (!state.sessionPanelEl || !state.sessionPanelEl.isConnected) {
1050
+ state.sessionPanelEl = document.getElementById('panel-sessions');
1051
+ }
1052
+ return state;
1053
+ },
1054
+ setMainTabSwitchIntent(tab) {
1055
+ const normalizedTab = typeof tab === 'string'
1056
+ ? tab.trim().toLowerCase()
1057
+ : '';
1058
+ if (!normalizedTab) return;
1059
+ const state = this.ensureMainTabSwitchState();
1060
+ state.intent = normalizedTab;
1061
+ },
1062
+ applyImmediateNavIntent(tab, configMode = '') {
1063
+ if (typeof document === 'undefined') return;
1064
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1065
+ if (!normalizedTab) return;
1066
+ const normalizedMode = typeof configMode === 'string' ? configMode.trim().toLowerCase() : '';
1067
+ const domState = this.ensureImmediateNavDomState();
1068
+ const nodes = Array.isArray(domState.navNodes) ? domState.navNodes : [];
1069
+ for (const node of nodes) {
1070
+ if (!node || !node.classList) continue;
1071
+ const nodeTab = String(node.getAttribute('data-main-tab') || '').trim().toLowerCase();
1072
+ const nodeMode = String(node.getAttribute('data-config-mode') || '').trim().toLowerCase();
1073
+ let shouldActivate = nodeTab === normalizedTab;
1074
+ if (shouldActivate && normalizedTab === 'config') {
1075
+ shouldActivate = nodeMode ? nodeMode === normalizedMode : false;
1076
+ }
1077
+ node.classList.toggle('nav-intent-active', !!shouldActivate);
1078
+ node.classList.toggle('nav-intent-inactive', !shouldActivate);
1079
+ }
1080
+ },
1081
+ clearImmediateNavIntent() {
1082
+ if (typeof document === 'undefined') return;
1083
+ const domState = this.ensureImmediateNavDomState();
1084
+ const nodes = Array.isArray(domState.navNodes) ? domState.navNodes : [];
1085
+ for (const node of nodes) {
1086
+ if (!node || !node.classList) continue;
1087
+ node.classList.remove('nav-intent-active');
1088
+ node.classList.remove('nav-intent-inactive');
1089
+ }
1090
+ },
1091
+ setSessionPanelFastHidden(hidden) {
1092
+ if (typeof document === 'undefined') return;
1093
+ const domState = this.ensureImmediateNavDomState();
1094
+ const panel = domState.sessionPanelEl;
1095
+ if (!panel || !panel.classList) return;
1096
+ panel.classList.toggle('session-panel-fast-hidden', !!hidden);
1097
+ },
1098
+ isSessionPanelFastHidden() {
1099
+ if (typeof document === 'undefined') return false;
1100
+ const domState = this.ensureImmediateNavDomState();
1101
+ const panel = domState.sessionPanelEl;
1102
+ return !!(panel && panel.classList && panel.classList.contains('session-panel-fast-hidden'));
1103
+ },
1104
+ recordPointerNavCommit(kind, value) {
1105
+ const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
1106
+ const normalizedValue = typeof value === 'string' ? value.trim().toLowerCase() : '';
1107
+ if (!normalizedKind || !normalizedValue) {
1108
+ this.__pointerNavCommitState = null;
1109
+ return;
1110
+ }
1111
+ this.__pointerNavCommitState = {
1112
+ kind: normalizedKind,
1113
+ value: normalizedValue,
1114
+ at: Date.now()
1115
+ };
1116
+ },
1117
+ consumePointerNavCommit(kind, value) {
1118
+ const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
1119
+ const normalizedValue = typeof value === 'string' ? value.trim().toLowerCase() : '';
1120
+ const state = this.__pointerNavCommitState;
1121
+ this.__pointerNavCommitState = null;
1122
+ if (!state || !normalizedKind || !normalizedValue) {
1123
+ return false;
1124
+ }
1125
+ if (state.kind !== normalizedKind || state.value !== normalizedValue) {
1126
+ return false;
1127
+ }
1128
+ return (Date.now() - Number(state.at || 0)) <= 1000;
1129
+ },
1130
+ onMainTabPointerDown(tab) {
1131
+ const event = arguments.length > 1 ? arguments[1] : null;
1132
+ if (event && typeof event.button === 'number' && event.button !== 0) {
1133
+ return;
1134
+ }
1135
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1136
+ if (!normalizedTab) return;
1137
+ this.setMainTabSwitchIntent(normalizedTab);
1138
+ this.applyImmediateNavIntent(normalizedTab);
1139
+ const shouldHideSessionPanel = this.mainTab === 'sessions' && normalizedTab !== 'sessions';
1140
+ this.setSessionPanelFastHidden(shouldHideSessionPanel);
1141
+ const pointerType = event && typeof event.pointerType === 'string'
1142
+ ? event.pointerType.trim().toLowerCase()
1143
+ : '';
1144
+ if (pointerType === 'touch') {
1145
+ return;
1146
+ }
1147
+ this.recordPointerNavCommit('main', normalizedTab);
1148
+ this.switchMainTab(normalizedTab);
1149
+ },
1150
+ onConfigTabPointerDown(mode) {
1151
+ const event = arguments.length > 1 ? arguments[1] : null;
1152
+ if (event && typeof event.button === 'number' && event.button !== 0) {
1153
+ return;
1154
+ }
1155
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
1156
+ if (!normalizedMode) return;
1157
+ this.setMainTabSwitchIntent('config');
1158
+ this.applyImmediateNavIntent('config', normalizedMode);
1159
+ const shouldHideSessionPanel = this.mainTab === 'sessions';
1160
+ this.setSessionPanelFastHidden(shouldHideSessionPanel);
1161
+ const pointerType = event && typeof event.pointerType === 'string'
1162
+ ? event.pointerType.trim().toLowerCase()
1163
+ : '';
1164
+ if (pointerType === 'touch') {
1165
+ return;
1166
+ }
1167
+ this.recordPointerNavCommit('config', normalizedMode);
1168
+ this.switchConfigMode(normalizedMode);
1169
+ },
1170
+ onMainTabClick(tab) {
1171
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1172
+ if (!normalizedTab) return;
1173
+ if (this.consumePointerNavCommit('main', normalizedTab)) return;
1174
+ this.switchMainTab(normalizedTab);
1175
+ },
1176
+ onConfigTabClick(mode) {
1177
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
1178
+ if (!normalizedMode) return;
1179
+ if (this.consumePointerNavCommit('config', normalizedMode)) return;
1180
+ this.switchConfigMode(normalizedMode);
1181
+ },
1182
+ clearMainTabSwitchIntent(expectedTab = '') {
1183
+ const state = this.ensureMainTabSwitchState();
1184
+ if (expectedTab && state.intent && state.intent !== expectedTab) {
1185
+ return;
1186
+ }
1187
+ state.intent = '';
1188
+ state.pendingTarget = '';
1189
+ this.clearImmediateNavIntent();
1190
+ this.setSessionPanelFastHidden(false);
1191
+ },
1192
+ getMainTabForNav() {
1193
+ const state = this.ensureMainTabSwitchState();
1194
+ return state.intent || this.mainTab;
1195
+ },
1196
+ isMainTabNavActive(tab) {
1197
+ return this.getMainTabForNav() === tab;
1198
+ },
1199
+ isConfigModeNavActive(mode) {
1200
+ return this.isMainTabNavActive('config') && this.configMode === mode;
1201
+ },
817
1202
  switchMainTab(tab) {
818
- this.mainTab = tab;
819
- if (tab === 'sessions' && this.sessionsList.length === 0) {
820
- this.loadSessions();
1203
+ const normalizedTab = typeof tab === 'string'
1204
+ ? tab.trim().toLowerCase()
1205
+ : '';
1206
+ const targetTab = normalizedTab || tab;
1207
+ if (!targetTab) return;
1208
+ if (targetTab === 'sessions') {
1209
+ this.cancelScheduledSessionTabDeferredTeardown();
821
1210
  }
822
- if (tab === 'config' && this.configMode === 'claude') {
823
- this.refreshClaudeModelContext();
1211
+
1212
+ this.setMainTabSwitchIntent(targetTab);
1213
+ if (targetTab === 'config') {
1214
+ this.applyImmediateNavIntent('config', this.configMode);
1215
+ } else {
1216
+ this.applyImmediateNavIntent(targetTab);
1217
+ }
1218
+
1219
+ const previousTab = this.mainTab;
1220
+ const switchState = this.ensureMainTabSwitchState();
1221
+ if (targetTab === previousTab) {
1222
+ switchState.ticket += 1;
1223
+ switchState.pendingTarget = '';
1224
+ this.scheduleAfterFrame(() => {
1225
+ this.clearMainTabSwitchIntent(normalizedTab);
1226
+ });
1227
+ return;
1228
+ }
1229
+ const isLeavingSessions = previousTab === 'sessions' && targetTab !== 'sessions';
1230
+ const shouldDeferApply = isLeavingSessions;
1231
+ if (isLeavingSessions && !this.isSessionPanelFastHidden()) {
1232
+ this.setSessionPanelFastHidden(true);
1233
+ }
1234
+ if (!shouldDeferApply) {
1235
+ switchState.ticket += 1;
1236
+ switchState.pendingTarget = '';
1237
+ const result = switchMainTabHelper.call(this, targetTab);
1238
+ this.scheduleAfterFrame(() => {
1239
+ this.clearMainTabSwitchIntent(normalizedTab);
1240
+ });
1241
+ return result;
1242
+ }
1243
+
1244
+ const ticket = ++switchState.ticket;
1245
+ switchState.pendingTarget = targetTab;
1246
+ this.scheduleAfterFrame(() => {
1247
+ const liveState = this.ensureMainTabSwitchState();
1248
+ if (ticket !== liveState.ticket) return;
1249
+ const pendingTarget = liveState.pendingTarget || targetTab;
1250
+ liveState.pendingTarget = '';
1251
+ switchMainTabHelper.call(this, pendingTarget);
1252
+ this.clearMainTabSwitchIntent(normalizedTab);
1253
+ });
1254
+ },
1255
+
1256
+ scheduleAfterFrame(task) {
1257
+ const callback = typeof task === 'function' ? task : () => {};
1258
+ if (typeof requestAnimationFrame === 'function') {
1259
+ requestAnimationFrame(callback);
1260
+ return;
1261
+ }
1262
+ setTimeout(callback, 16);
1263
+ },
1264
+ scheduleIdleTask(task, timeoutMs = 160) {
1265
+ const callback = typeof task === 'function' ? task : () => {};
1266
+ const timeout = Number.isFinite(timeoutMs)
1267
+ ? Math.max(16, Math.floor(timeoutMs))
1268
+ : 160;
1269
+ if (typeof requestIdleCallback === 'function') {
1270
+ const id = requestIdleCallback(callback, { timeout });
1271
+ return {
1272
+ type: 'idle',
1273
+ id
1274
+ };
1275
+ }
1276
+ const id = setTimeout(callback, timeout);
1277
+ return {
1278
+ type: 'timeout',
1279
+ id
1280
+ };
1281
+ },
1282
+ cancelIdleTask(handle) {
1283
+ if (!handle || typeof handle !== 'object') return;
1284
+ const type = handle.type;
1285
+ const id = handle.id;
1286
+ if (type === 'idle') {
1287
+ if (typeof cancelIdleCallback === 'function') {
1288
+ cancelIdleCallback(id);
1289
+ } else {
1290
+ clearTimeout(id);
1291
+ }
1292
+ return;
1293
+ }
1294
+ if (type === 'timeout') {
1295
+ clearTimeout(id);
1296
+ }
1297
+ },
1298
+ scheduleSessionTabDeferredTeardown(task) {
1299
+ const callback = typeof task === 'function' ? task : () => {};
1300
+ this.cancelScheduledSessionTabDeferredTeardown();
1301
+ this.__sessionTabDeferredTeardownHandle = this.scheduleIdleTask(() => {
1302
+ this.__sessionTabDeferredTeardownHandle = null;
1303
+ callback();
1304
+ }, 180);
1305
+ },
1306
+ cancelScheduledSessionTabDeferredTeardown() {
1307
+ const handle = this.__sessionTabDeferredTeardownHandle || null;
1308
+ if (!handle) return;
1309
+ this.cancelIdleTask(handle);
1310
+ this.__sessionTabDeferredTeardownHandle = null;
1311
+ },
1312
+
1313
+ resetSessionPreviewMessageRender() {
1314
+ this.sessionPreviewVisibleCount = 0;
1315
+ this.invalidateSessionTimelineMeasurementCache();
1316
+ },
1317
+
1318
+ resetSessionDetailPagination() {
1319
+ const initialLimit = Number.isFinite(this.sessionDetailInitialMessageLimit)
1320
+ ? Math.max(1, Math.floor(this.sessionDetailInitialMessageLimit))
1321
+ : 80;
1322
+ this.sessionDetailMessageLimit = initialLimit;
1323
+ this.sessionPreviewPendingVisibleCount = 0;
1324
+ },
1325
+
1326
+ primeSessionPreviewMessageRender() {
1327
+ this.sessionPreviewVisibleCount = 0;
1328
+ this.invalidateSessionTimelineMeasurementCache();
1329
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
1330
+ return;
824
1331
  }
1332
+ const total = Array.isArray(this.activeSessionMessages)
1333
+ ? this.activeSessionMessages.length
1334
+ : 0;
1335
+ if (total <= 0) return;
1336
+ const baseSize = Number.isFinite(this.sessionPreviewInitialBatchSize)
1337
+ ? Math.max(1, Math.floor(this.sessionPreviewInitialBatchSize))
1338
+ : 40;
1339
+ this.sessionPreviewVisibleCount = Math.min(baseSize, total);
1340
+ this.invalidateSessionTimelineMeasurementCache();
1341
+ },
1342
+
1343
+ async loadMoreSessionMessages(stepSize) {
1344
+ return loadMoreSessionMessagesHelper.call(this, stepSize);
1345
+ },
1346
+
1347
+ suspendSessionTabRender() {
1348
+ this.sessionTabRenderTicket += 1;
1349
+ this.sessionListRenderEnabled = false;
1350
+ this.sessionPreviewRenderEnabled = false;
1351
+ this.cancelSessionTimelineSync();
1352
+ this.sessionTimelineActiveKey = '';
1353
+ this.sessionTimelineLastSyncAt = 0;
1354
+ this.sessionTimelineLastScrollTop = 0;
1355
+ this.sessionTimelineLastAnchorY = 0;
1356
+ this.sessionTimelineLastDirection = 0;
1357
+ this.sessionPreviewScrollEl = null;
1358
+ this.sessionPreviewContainerEl = null;
1359
+ this.sessionPreviewHeaderEl = null;
1360
+ },
1361
+
1362
+ finalizeSessionTabTeardown() {
1363
+ this.resetSessionPreviewMessageRender();
1364
+ this.sessionPreviewPendingVisibleCount = 0;
1365
+ this.clearSessionTimelineRefs();
1366
+ },
1367
+
1368
+ teardownSessionTabRender() {
1369
+ this.suspendSessionTabRender();
1370
+ this.finalizeSessionTabTeardown();
1371
+ },
1372
+
1373
+ prepareSessionTabRender() {
1374
+ const ticket = ++this.sessionTabRenderTicket;
1375
+ this.sessionListRenderEnabled = false;
1376
+ this.sessionPreviewRenderEnabled = false;
1377
+ this.resetSessionPreviewMessageRender();
1378
+
1379
+ this.scheduleAfterFrame(() => {
1380
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1381
+ return;
1382
+ }
1383
+ this.sessionListRenderEnabled = true;
1384
+
1385
+ this.scheduleAfterFrame(() => {
1386
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1387
+ return;
1388
+ }
1389
+ this.sessionPreviewRenderEnabled = true;
1390
+ this.$nextTick(() => {
1391
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1392
+ return;
1393
+ }
1394
+ this.primeSessionPreviewMessageRender();
1395
+ this.updateSessionTimelineOffset();
1396
+ this.scheduleSessionTimelineSync();
1397
+ });
1398
+ });
1399
+ });
825
1400
  },
826
1401
 
827
1402
  getSessionStandaloneContext() {
@@ -868,6 +1443,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
868
1443
 
869
1444
  this.sessionStandalone = true;
870
1445
  this.mainTab = 'sessions';
1446
+ this.prepareSessionTabRender();
871
1447
 
872
1448
  if (context.error || !context.params) {
873
1449
  this.sessionStandaloneError = `会话链接参数不完整:${context.error || '参数解析失败'}`;
@@ -887,7 +1463,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
887
1463
  this.activeSessionDetailClipped = false;
888
1464
  this.cancelSessionTimelineSync();
889
1465
  this.sessionTimelineActiveKey = '';
890
- this.sessionMessageRefMap = Object.create(null);
1466
+ this.clearSessionTimelineRefs();
891
1467
  this.sessionStandaloneError = '';
892
1468
  this.sessionStandaloneText = '';
893
1469
  this.sessionStandaloneTitle = this.activeSession.title || '会话';
@@ -1239,7 +1815,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1239
1815
  }
1240
1816
  this.sessionDeleting[key] = true;
1241
1817
  try {
1242
- const res = await api('delete-session', {
1818
+ const res = await api('trash-session', {
1243
1819
  source: session.source,
1244
1820
  sessionId: session.sessionId,
1245
1821
  filePath: session.filePath
@@ -1248,8 +1824,29 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1248
1824
  this.showMessage(res.error, 'error');
1249
1825
  return;
1250
1826
  }
1251
- this.showMessage('操作成功', 'success');
1252
- await this.loadSessions();
1827
+ this.removeSessionPin(session);
1828
+ this.invalidateSessionTrashRequests();
1829
+ this.showMessage('已移入回收站', 'success');
1830
+ if (this.sessionTrashLoadedOnce) {
1831
+ this.prependSessionTrashItem(this.buildSessionTrashItemFromSession(session, res), {
1832
+ totalCount: res && res.totalCount !== undefined ? res.totalCount : undefined
1833
+ });
1834
+ } else if (this.sessionTrashCountLoadedOnce) {
1835
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1836
+ res && res.totalCount !== undefined
1837
+ ? res.totalCount
1838
+ : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
1839
+ this.sessionTrashItems
1840
+ );
1841
+ } else {
1842
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1843
+ res && res.totalCount !== undefined
1844
+ ? res.totalCount
1845
+ : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
1846
+ this.sessionTrashItems
1847
+ );
1848
+ }
1849
+ await this.removeSessionFromCurrentList(session);
1253
1850
  } catch (e) {
1254
1851
  this.showMessage('删除失败', 'error');
1255
1852
  } finally {
@@ -1257,6 +1854,413 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1257
1854
  }
1258
1855
  },
1259
1856
 
1857
+ buildSessionTrashItemFromSession(session, result = {}) {
1858
+ const deletedAt = typeof result.deletedAt === 'string' && result.deletedAt
1859
+ ? result.deletedAt
1860
+ : new Date().toISOString();
1861
+ const source = session && session.source === 'claude' ? 'claude' : 'codex';
1862
+ return {
1863
+ trashId: typeof result.trashId === 'string' ? result.trashId : '',
1864
+ source,
1865
+ sourceLabel: session && typeof session.sourceLabel === 'string' && session.sourceLabel
1866
+ ? session.sourceLabel
1867
+ : (source === 'claude' ? 'Claude Code' : 'Codex'),
1868
+ sessionId: session && typeof session.sessionId === 'string' ? session.sessionId : '',
1869
+ title: session && typeof session.title === 'string' && session.title
1870
+ ? session.title
1871
+ : (session && typeof session.sessionId === 'string' ? session.sessionId : ''),
1872
+ cwd: session && typeof session.cwd === 'string' ? session.cwd : '',
1873
+ createdAt: session && typeof session.createdAt === 'string' ? session.createdAt : '',
1874
+ updatedAt: session && typeof session.updatedAt === 'string' ? session.updatedAt : '',
1875
+ deletedAt,
1876
+ messageCount: Number.isFinite(Number(result && result.messageCount))
1877
+ ? Math.max(0, Math.floor(Number(result.messageCount)))
1878
+ : (Number.isFinite(Number(session && session.messageCount))
1879
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
1880
+ : 0),
1881
+ originalFilePath: session && typeof session.filePath === 'string' ? session.filePath : '',
1882
+ provider: session && typeof session.provider === 'string' ? session.provider : source,
1883
+ keywords: Array.isArray(session && session.keywords) ? session.keywords : [],
1884
+ capabilities: session && typeof session.capabilities === 'object' && session.capabilities
1885
+ ? session.capabilities
1886
+ : {},
1887
+ claudeIndexPath: '',
1888
+ claudeIndexEntry: null,
1889
+ trashFilePath: ''
1890
+ };
1891
+ },
1892
+
1893
+ prependSessionTrashItem(item, options = {}) {
1894
+ if (!item || !item.trashId) {
1895
+ return;
1896
+ }
1897
+ const existing = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
1898
+ const filtered = existing.filter((entry) => this.getSessionTrashActionKey(entry) !== item.trashId);
1899
+ const nextItems = [item, ...filtered].slice(0, SESSION_TRASH_LIST_LIMIT);
1900
+ const previousTotalCount = Number(this.sessionTrashTotalCount);
1901
+ const normalizedPreviousTotal = Number.isFinite(previousTotalCount) && previousTotalCount >= 0
1902
+ ? Math.max(existing.length, Math.floor(previousTotalCount))
1903
+ : existing.length;
1904
+ this.sessionTrashItems = nextItems;
1905
+ const previousVisibleCount = Number(this.sessionTrashVisibleCount);
1906
+ const normalizedPreviousVisibleCount = Number.isFinite(previousVisibleCount) && previousVisibleCount > 0
1907
+ ? Math.floor(previousVisibleCount)
1908
+ : SESSION_TRASH_PAGE_SIZE;
1909
+ const wasFullyExpanded = normalizedPreviousVisibleCount >= existing.length
1910
+ || normalizedPreviousVisibleCount >= normalizedPreviousTotal;
1911
+ if (wasFullyExpanded) {
1912
+ this.sessionTrashVisibleCount = Math.min(
1913
+ normalizedPreviousVisibleCount + 1,
1914
+ nextItems.length || (normalizedPreviousVisibleCount + 1)
1915
+ );
1916
+ }
1917
+ const fallbackTotalCount = filtered.length === existing.length
1918
+ ? normalizedPreviousTotal + 1
1919
+ : normalizedPreviousTotal;
1920
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1921
+ options && options.totalCount !== undefined
1922
+ ? options.totalCount
1923
+ : fallbackTotalCount,
1924
+ nextItems
1925
+ );
1926
+ },
1927
+
1928
+ normalizeSessionTrashTotalCount(totalCount, fallbackItems = this.sessionTrashItems) {
1929
+ const fallbackCount = Array.isArray(fallbackItems) ? fallbackItems.length : 0;
1930
+ const numericTotal = Number(totalCount);
1931
+ if (!Number.isFinite(numericTotal) || numericTotal < 0) {
1932
+ return fallbackCount;
1933
+ }
1934
+ return Math.floor(numericTotal);
1935
+ },
1936
+
1937
+ getSessionTrashViewState() {
1938
+ if (this.sessionTrashLoading && !this.sessionTrashLoadedOnce) {
1939
+ return 'loading';
1940
+ }
1941
+ const totalCount = Number(this.sessionTrashCount);
1942
+ const normalizedTotalCount = Number.isFinite(totalCount) && totalCount >= 0
1943
+ ? Math.floor(totalCount)
1944
+ : 0;
1945
+ const hasVisibleItems = Array.isArray(this.sessionTrashItems) && this.sessionTrashItems.length > 0;
1946
+ if (this.sessionTrashLastLoadFailed && (!this.sessionTrashLoadedOnce || !hasVisibleItems)) {
1947
+ return 'retry';
1948
+ }
1949
+ if (!this.sessionTrashLoadedOnce) {
1950
+ return normalizedTotalCount > 0 ? 'retry' : 'empty';
1951
+ }
1952
+ if (normalizedTotalCount === 0) {
1953
+ return 'empty';
1954
+ }
1955
+ return hasVisibleItems ? 'list' : 'retry';
1956
+ },
1957
+
1958
+ issueSessionTrashCountRequestToken() {
1959
+ const currentToken = Number(this.sessionTrashCountRequestToken);
1960
+ const nextToken = Number.isFinite(currentToken) && currentToken >= 0
1961
+ ? Math.floor(currentToken) + 1
1962
+ : 1;
1963
+ this.sessionTrashCountRequestToken = nextToken;
1964
+ return nextToken;
1965
+ },
1966
+
1967
+ issueSessionTrashListRequestToken() {
1968
+ const currentToken = Number(this.sessionTrashListRequestToken);
1969
+ const nextToken = Number.isFinite(currentToken) && currentToken >= 0
1970
+ ? Math.floor(currentToken) + 1
1971
+ : 1;
1972
+ this.sessionTrashListRequestToken = nextToken;
1973
+ return nextToken;
1974
+ },
1975
+
1976
+ invalidateSessionTrashRequests() {
1977
+ this.issueSessionTrashCountRequestToken();
1978
+ return this.issueSessionTrashListRequestToken();
1979
+ },
1980
+
1981
+ isLatestSessionTrashCountRequestToken(token) {
1982
+ return Number(token) === Number(this.sessionTrashCountRequestToken);
1983
+ },
1984
+
1985
+ isLatestSessionTrashListRequestToken(token) {
1986
+ return Number(token) === Number(this.sessionTrashListRequestToken);
1987
+ },
1988
+
1989
+ resetSessionTrashVisibleCount() {
1990
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
1991
+ this.sessionTrashVisibleCount = Math.min(totalItems, SESSION_TRASH_PAGE_SIZE) || SESSION_TRASH_PAGE_SIZE;
1992
+ },
1993
+
1994
+ loadMoreSessionTrashItems() {
1995
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
1996
+ const visibleCount = Number(this.sessionTrashVisibleCount);
1997
+ const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0
1998
+ ? Math.floor(visibleCount)
1999
+ : SESSION_TRASH_PAGE_SIZE;
2000
+ this.sessionTrashVisibleCount = Math.min(totalItems, safeVisibleCount + SESSION_TRASH_PAGE_SIZE);
2001
+ },
2002
+
2003
+ clearActiveSessionState() {
2004
+ this.activeSession = null;
2005
+ this.activeSessionMessages = [];
2006
+ this.resetSessionDetailPagination();
2007
+ this.resetSessionPreviewMessageRender();
2008
+ this.activeSessionDetailError = '';
2009
+ this.activeSessionDetailClipped = false;
2010
+ this.cancelSessionTimelineSync();
2011
+ this.sessionTimelineActiveKey = '';
2012
+ this.clearSessionTimelineRefs();
2013
+ },
2014
+
2015
+ async removeSessionFromCurrentList(session) {
2016
+ const sessionKey = this.getSessionExportKey(session);
2017
+ if (!sessionKey) {
2018
+ return;
2019
+ }
2020
+ const currentList = Array.isArray(this.sessionsList) ? [...this.sessionsList] : [];
2021
+ const removedIndex = currentList.findIndex((item) => this.getSessionExportKey(item) === sessionKey);
2022
+ if (removedIndex < 0) {
2023
+ return;
2024
+ }
2025
+ const activeKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
2026
+ const renderedList = Array.isArray(this.sortedSessionsList) ? this.sortedSessionsList : [];
2027
+ const renderedIndex = renderedList.findIndex((item) => this.getSessionExportKey(item) === sessionKey);
2028
+ let nextActiveKey = '';
2029
+ if (activeKey === sessionKey && renderedIndex >= 0) {
2030
+ const fallbackSession = renderedList[renderedIndex - 1] || renderedList[renderedIndex + 1] || null;
2031
+ nextActiveKey = fallbackSession ? this.getSessionExportKey(fallbackSession) : '';
2032
+ }
2033
+ currentList.splice(removedIndex, 1);
2034
+ this.sessionsList = currentList;
2035
+ this.syncSessionPathOptionsForSource(
2036
+ this.sessionFilterSource,
2037
+ this.extractPathOptionsFromSessions(currentList),
2038
+ false
2039
+ );
2040
+ if (activeKey !== sessionKey) {
2041
+ return;
2042
+ }
2043
+ if (currentList.length === 0) {
2044
+ this.clearActiveSessionState();
2045
+ return;
2046
+ }
2047
+ const nextSession = currentList.find((item) => this.getSessionExportKey(item) === nextActiveKey)
2048
+ || currentList[Math.min(removedIndex, currentList.length - 1)];
2049
+ if (!nextSession) {
2050
+ this.clearActiveSessionState();
2051
+ return;
2052
+ }
2053
+ await this.selectSession(nextSession);
2054
+ },
2055
+
2056
+ normalizeSettingsTab(tab) {
2057
+ return tab === 'trash' ? 'trash' : 'backup';
2058
+ },
2059
+
2060
+ async onSettingsTabClick(tab) {
2061
+ await this.switchSettingsTab(tab);
2062
+ },
2063
+
2064
+ async switchSettingsTab(tab, options = {}) {
2065
+ const nextTab = this.normalizeSettingsTab(tab);
2066
+ this.settingsTab = nextTab;
2067
+ if (nextTab !== 'trash') {
2068
+ return;
2069
+ }
2070
+ const forceRefresh = options.forceRefresh === true;
2071
+ if (forceRefresh || !this.sessionTrashLoadedOnce) {
2072
+ await this.loadSessionTrash({ forceRefresh });
2073
+ }
2074
+ },
2075
+
2076
+ async loadSessionTrashCount(options = {}) {
2077
+ if (this.sessionTrashCountLoading) {
2078
+ this.sessionTrashCountPendingOptions = {
2079
+ ...(this.sessionTrashCountPendingOptions || {}),
2080
+ ...(options || {})
2081
+ };
2082
+ return;
2083
+ }
2084
+ const requestToken = this.issueSessionTrashCountRequestToken();
2085
+ this.sessionTrashCountLoading = true;
2086
+ try {
2087
+ const res = await api('list-session-trash', { countOnly: true });
2088
+ if (!this.isLatestSessionTrashCountRequestToken(requestToken)) {
2089
+ return;
2090
+ }
2091
+ if (res.error) {
2092
+ if (options.silent !== true) {
2093
+ this.showMessage(res.error, 'error');
2094
+ }
2095
+ return;
2096
+ }
2097
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
2098
+ res.totalCount,
2099
+ this.sessionTrashItems
2100
+ );
2101
+ this.sessionTrashCountLoadedOnce = true;
2102
+ } catch (e) {
2103
+ if (this.isLatestSessionTrashCountRequestToken(requestToken) && options.silent !== true) {
2104
+ this.showMessage('加载回收站数量失败', 'error');
2105
+ }
2106
+ } finally {
2107
+ this.sessionTrashCountLoading = false;
2108
+ const pendingOptions = this.sessionTrashCountPendingOptions;
2109
+ this.sessionTrashCountPendingOptions = null;
2110
+ if (pendingOptions) {
2111
+ await this.loadSessionTrashCount(pendingOptions);
2112
+ }
2113
+ }
2114
+ },
2115
+
2116
+ getSessionTrashActionKey(item) {
2117
+ return item && typeof item.trashId === 'string' ? item.trashId : '';
2118
+ },
2119
+
2120
+ isSessionTrashActionBusy(item) {
2121
+ const key = typeof item === 'string' ? item : this.getSessionTrashActionKey(item);
2122
+ return !!(key && (this.sessionTrashRestoring[key] || this.sessionTrashPurging[key]));
2123
+ },
2124
+
2125
+ async loadSessionTrash(options = {}) {
2126
+ if (this.sessionTrashLoading) {
2127
+ this.sessionTrashPendingOptions = {
2128
+ ...(this.sessionTrashPendingOptions || {}),
2129
+ ...(options || {})
2130
+ };
2131
+ return;
2132
+ }
2133
+ const requestToken = this.issueSessionTrashListRequestToken();
2134
+ this.sessionTrashLoading = true;
2135
+ this.sessionTrashLastLoadFailed = false;
2136
+ let loadSucceeded = false;
2137
+ try {
2138
+ const res = await api('list-session-trash', {
2139
+ limit: SESSION_TRASH_LIST_LIMIT,
2140
+ forceRefresh: !!options.forceRefresh
2141
+ });
2142
+ if (!this.isLatestSessionTrashListRequestToken(requestToken)) {
2143
+ return;
2144
+ }
2145
+ if (res.error) {
2146
+ this.sessionTrashLastLoadFailed = true;
2147
+ this.showMessage(res.error, 'error');
2148
+ return;
2149
+ }
2150
+ const nextItems = Array.isArray(res.items) ? res.items : [];
2151
+ this.sessionTrashItems = nextItems;
2152
+ this.resetSessionTrashVisibleCount();
2153
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(res.totalCount, nextItems);
2154
+ this.sessionTrashCountLoadedOnce = true;
2155
+ this.sessionTrashLastLoadFailed = false;
2156
+ loadSucceeded = true;
2157
+ } catch (e) {
2158
+ if (this.isLatestSessionTrashListRequestToken(requestToken)) {
2159
+ this.sessionTrashLastLoadFailed = true;
2160
+ this.showMessage('加载回收站失败', 'error');
2161
+ }
2162
+ } finally {
2163
+ this.sessionTrashLoading = false;
2164
+ if (loadSucceeded) {
2165
+ this.sessionTrashLoadedOnce = true;
2166
+ }
2167
+ const pendingOptions = this.sessionTrashPendingOptions;
2168
+ this.sessionTrashPendingOptions = null;
2169
+ if (pendingOptions) {
2170
+ await this.loadSessionTrash(pendingOptions);
2171
+ }
2172
+ }
2173
+ },
2174
+
2175
+ async restoreSessionTrash(item) {
2176
+ const key = this.getSessionTrashActionKey(item);
2177
+ if (!key || this.isSessionTrashActionBusy(key) || this.sessionTrashClearing) {
2178
+ return;
2179
+ }
2180
+ this.sessionTrashRestoring[key] = true;
2181
+ try {
2182
+ const res = await api('restore-session-trash', { trashId: key });
2183
+ if (res.error) {
2184
+ this.showMessage(res.error, 'error');
2185
+ return;
2186
+ }
2187
+ this.showMessage('会话已恢复', 'success');
2188
+ this.invalidateSessionTrashRequests();
2189
+ await this.loadSessionTrash({ forceRefresh: true });
2190
+ if (this.sessionsLoadedOnce) {
2191
+ await this.loadSessions();
2192
+ }
2193
+ } catch (e) {
2194
+ this.showMessage('恢复失败', 'error');
2195
+ } finally {
2196
+ this.sessionTrashRestoring[key] = false;
2197
+ }
2198
+ },
2199
+
2200
+ async purgeSessionTrash(item) {
2201
+ const key = this.getSessionTrashActionKey(item);
2202
+ if (!key || this.isSessionTrashActionBusy(key) || this.sessionTrashClearing) {
2203
+ return;
2204
+ }
2205
+ const confirmed = await this.requestConfirmDialog({
2206
+ title: '彻底删除回收站记录',
2207
+ message: '该会话将从回收站永久删除,且无法恢复。',
2208
+ confirmText: '彻底删除',
2209
+ cancelText: '取消',
2210
+ danger: true
2211
+ });
2212
+ if (!confirmed) {
2213
+ return;
2214
+ }
2215
+ this.sessionTrashPurging[key] = true;
2216
+ try {
2217
+ const res = await api('purge-session-trash', { trashId: key });
2218
+ if (res.error) {
2219
+ this.showMessage(res.error, 'error');
2220
+ return;
2221
+ }
2222
+ this.showMessage('已彻底删除', 'success');
2223
+ this.invalidateSessionTrashRequests();
2224
+ await this.loadSessionTrash({ forceRefresh: true });
2225
+ } catch (e) {
2226
+ this.showMessage('彻底删除失败', 'error');
2227
+ } finally {
2228
+ this.sessionTrashPurging[key] = false;
2229
+ }
2230
+ },
2231
+
2232
+ async clearSessionTrash() {
2233
+ const normalizedCount = Number(this.sessionTrashCount);
2234
+ if (this.sessionTrashClearing || !Number.isFinite(normalizedCount) || normalizedCount <= 0) {
2235
+ return;
2236
+ }
2237
+ const confirmed = await this.requestConfirmDialog({
2238
+ title: '清空回收站',
2239
+ message: '该操作会永久删除回收站中的全部会话,且无法恢复。',
2240
+ confirmText: '全部清空',
2241
+ cancelText: '取消',
2242
+ danger: true
2243
+ });
2244
+ if (!confirmed) {
2245
+ return;
2246
+ }
2247
+ this.sessionTrashClearing = true;
2248
+ try {
2249
+ const res = await api('purge-session-trash', { all: true });
2250
+ if (res.error) {
2251
+ this.showMessage(res.error, 'error');
2252
+ return;
2253
+ }
2254
+ this.showMessage('回收站已清空', 'success');
2255
+ this.invalidateSessionTrashRequests();
2256
+ await this.loadSessionTrash({ forceRefresh: true });
2257
+ } catch (e) {
2258
+ this.showMessage('清空回收站失败', 'error');
2259
+ } finally {
2260
+ this.sessionTrashClearing = false;
2261
+ }
2262
+ },
2263
+
1260
2264
  normalizeSessionPathValue(value) {
1261
2265
  return normalizeSessionPathFilter(value);
1262
2266
  },
@@ -1384,6 +2388,124 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1384
2388
  localStorage.removeItem('codexmateSessionPathFilter');
1385
2389
  }
1386
2390
  },
2391
+ normalizeSessionPinnedMap(raw) {
2392
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
2393
+ return {};
2394
+ }
2395
+ const next = {};
2396
+ for (const [key, value] of Object.entries(raw)) {
2397
+ if (!key) continue;
2398
+ const numeric = Number(value);
2399
+ if (!Number.isFinite(numeric) || numeric <= 0) continue;
2400
+ next[key] = Math.floor(numeric);
2401
+ }
2402
+ return next;
2403
+ },
2404
+ restoreSessionPinnedMap() {
2405
+ const cached = localStorage.getItem('codexmateSessionPinnedMap');
2406
+ if (!cached) {
2407
+ this.sessionPinnedMap = {};
2408
+ return;
2409
+ }
2410
+ try {
2411
+ const parsed = JSON.parse(cached);
2412
+ this.sessionPinnedMap = this.normalizeSessionPinnedMap(parsed);
2413
+ } catch (_) {
2414
+ this.sessionPinnedMap = {};
2415
+ localStorage.removeItem('codexmateSessionPinnedMap');
2416
+ }
2417
+ },
2418
+ persistSessionPinnedMap() {
2419
+ const payload = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2420
+ ? this.sessionPinnedMap
2421
+ : {};
2422
+ localStorage.setItem('codexmateSessionPinnedMap', JSON.stringify(payload));
2423
+ },
2424
+ shouldPruneSessionPinnedMap(sessions = this.sessionsList) {
2425
+ if (!Array.isArray(sessions) || sessions.length === 0) {
2426
+ return false;
2427
+ }
2428
+ if (this.sessionFilterSource !== 'all') {
2429
+ return false;
2430
+ }
2431
+ if (this.sessionPathFilter) {
2432
+ return false;
2433
+ }
2434
+ if (this.sessionQuery && isSessionQueryEnabled(this.sessionFilterSource)) {
2435
+ return false;
2436
+ }
2437
+ if (this.sessionRoleFilter && this.sessionRoleFilter !== 'all') {
2438
+ return false;
2439
+ }
2440
+ if (this.sessionTimePreset && this.sessionTimePreset !== 'all') {
2441
+ return false;
2442
+ }
2443
+ return true;
2444
+ },
2445
+ pruneSessionPinnedMap(sessions = this.sessionsList) {
2446
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2447
+ ? this.sessionPinnedMap
2448
+ : {};
2449
+ const list = Array.isArray(sessions) ? sessions : [];
2450
+ if (Object.keys(current).length === 0 || !this.shouldPruneSessionPinnedMap(list)) {
2451
+ return;
2452
+ }
2453
+ const validKeys = new Set(list.map((session) => this.getSessionExportKey(session)).filter(Boolean));
2454
+ const next = {};
2455
+ let changed = false;
2456
+ for (const [key, value] of Object.entries(current)) {
2457
+ if (!validKeys.has(key)) {
2458
+ changed = true;
2459
+ continue;
2460
+ }
2461
+ next[key] = value;
2462
+ }
2463
+ if (!changed) {
2464
+ return;
2465
+ }
2466
+ this.sessionPinnedMap = next;
2467
+ this.persistSessionPinnedMap();
2468
+ },
2469
+ getSessionPinTimestamp(session) {
2470
+ if (!session) return 0;
2471
+ const key = this.getSessionExportKey(session);
2472
+ if (!key) return 0;
2473
+ const raw = this.sessionPinnedMap && this.sessionPinnedMap[key];
2474
+ const numeric = Number(raw);
2475
+ return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0;
2476
+ },
2477
+ isSessionPinned(session) {
2478
+ return this.getSessionPinTimestamp(session) > 0;
2479
+ },
2480
+ toggleSessionPin(session) {
2481
+ if (!session) return;
2482
+ const key = this.getSessionExportKey(session);
2483
+ if (!key) return;
2484
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2485
+ ? this.sessionPinnedMap
2486
+ : {};
2487
+ const next = { ...current };
2488
+ if (next[key]) {
2489
+ delete next[key];
2490
+ } else {
2491
+ next[key] = Date.now();
2492
+ }
2493
+ this.sessionPinnedMap = next;
2494
+ this.persistSessionPinnedMap();
2495
+ },
2496
+ removeSessionPin(session) {
2497
+ if (!session) return;
2498
+ const key = this.getSessionExportKey(session);
2499
+ if (!key) return;
2500
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2501
+ ? this.sessionPinnedMap
2502
+ : {};
2503
+ if (!current[key]) return;
2504
+ const next = { ...current };
2505
+ delete next[key];
2506
+ this.sessionPinnedMap = next;
2507
+ this.persistSessionPinnedMap();
2508
+ },
1387
2509
 
1388
2510
  async onSessionSourceChange() {
1389
2511
  this.refreshSessionPathOptions(this.sessionFilterSource);
@@ -1434,13 +2556,137 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1434
2556
  },
1435
2557
  setSessionPreviewScrollRef(el) {
1436
2558
  this.sessionPreviewScrollEl = el || null;
1437
- if (this.sessionPreviewScrollEl) {
1438
- this.scheduleSessionTimelineSync();
1439
- } else {
2559
+ this.invalidateSessionTimelineMeasurementCache();
2560
+ const shouldSync = !!(
2561
+ this.sessionPreviewScrollEl
2562
+ && this.mainTab === 'sessions'
2563
+ && this.getMainTabForNav() === 'sessions'
2564
+ && this.sessionPreviewRenderEnabled
2565
+ && this.sessionTimelineNodes.length
2566
+ );
2567
+ if (!shouldSync) {
1440
2568
  this.cancelSessionTimelineSync();
2569
+ this.updateSessionTimelineOffset();
2570
+ return;
1441
2571
  }
2572
+ const boundScrollEl = this.sessionPreviewScrollEl;
2573
+ this.$nextTick(() => {
2574
+ if (this.sessionPreviewScrollEl !== boundScrollEl) return;
2575
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) return;
2576
+ if (!this.sessionTimelineNodes.length) return;
2577
+ this.scheduleSessionTimelineSync();
2578
+ });
1442
2579
  this.updateSessionTimelineOffset();
1443
2580
  },
2581
+ clearSessionTimelineRefs() {
2582
+ this.sessionMessageRefMap = Object.create(null);
2583
+ this.sessionMessageRefBinderMap = Object.create(null);
2584
+ this.sessionTimelineLastAnchorY = 0;
2585
+ this.sessionTimelineLastDirection = 0;
2586
+ this.invalidateSessionTimelineMeasurementCache(true);
2587
+ },
2588
+ ensureSessionTimelineMeasurementCache() {
2589
+ if (this.__sessionTimelineMeasurementCache) {
2590
+ return this.__sessionTimelineMeasurementCache;
2591
+ }
2592
+ this.__sessionTimelineMeasurementCache = {
2593
+ offsetByKey: Object.create(null),
2594
+ dirty: true
2595
+ };
2596
+ return this.__sessionTimelineMeasurementCache;
2597
+ },
2598
+ invalidateSessionTimelineMeasurementCache(resetOffset = false) {
2599
+ const cache = this.ensureSessionTimelineMeasurementCache();
2600
+ if (resetOffset) {
2601
+ cache.offsetByKey = Object.create(null);
2602
+ }
2603
+ cache.dirty = true;
2604
+ },
2605
+ refreshSessionTimelineMeasurementCache(nodes = null) {
2606
+ const cache = this.ensureSessionTimelineMeasurementCache();
2607
+ const nodeList = Array.isArray(nodes) ? nodes : (Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : []);
2608
+ if (!nodeList.length) {
2609
+ cache.offsetByKey = Object.create(null);
2610
+ cache.dirty = false;
2611
+ return cache.offsetByKey;
2612
+ }
2613
+ const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
2614
+ const scrollRect = scrollEl && typeof scrollEl.getBoundingClientRect === 'function'
2615
+ ? scrollEl.getBoundingClientRect()
2616
+ : null;
2617
+ const scrollTop = scrollEl ? Number(scrollEl.scrollTop || 0) : 0;
2618
+ const nextOffsetByKey = Object.create(null);
2619
+ for (const node of nodeList) {
2620
+ if (!node || !node.key) continue;
2621
+ const messageEl = this.sessionMessageRefMap[node.key];
2622
+ if (!messageEl) continue;
2623
+ let top = Number.NaN;
2624
+ if (
2625
+ scrollRect
2626
+ && typeof messageEl.getBoundingClientRect === 'function'
2627
+ ) {
2628
+ const messageRect = messageEl.getBoundingClientRect();
2629
+ top = scrollTop + (messageRect.top - scrollRect.top);
2630
+ } else {
2631
+ top = Number(messageEl.offsetTop || 0);
2632
+ }
2633
+ if (!Number.isFinite(top)) continue;
2634
+ nextOffsetByKey[node.key] = top;
2635
+ }
2636
+ cache.offsetByKey = nextOffsetByKey;
2637
+ cache.dirty = false;
2638
+ return cache.offsetByKey;
2639
+ },
2640
+ getCachedSessionTimelineMeasuredNodes(nodes) {
2641
+ const nodeList = Array.isArray(nodes) ? nodes : [];
2642
+ if (!nodeList.length) {
2643
+ return [];
2644
+ }
2645
+ const cache = this.ensureSessionTimelineMeasurementCache();
2646
+ if (cache.dirty) {
2647
+ this.refreshSessionTimelineMeasurementCache(nodeList);
2648
+ }
2649
+ const offsetByKey = cache.offsetByKey || Object.create(null);
2650
+ const measuredNodes = [];
2651
+ for (const node of nodeList) {
2652
+ if (!node || !node.key) continue;
2653
+ const top = Number(offsetByKey[node.key]);
2654
+ if (!Number.isFinite(top)) continue;
2655
+ measuredNodes.push({
2656
+ key: node.key,
2657
+ top
2658
+ });
2659
+ }
2660
+ if (measuredNodes.length >= nodeList.length) {
2661
+ return measuredNodes;
2662
+ }
2663
+ const refreshedOffsetByKey = this.refreshSessionTimelineMeasurementCache(nodeList);
2664
+ const refreshedNodes = [];
2665
+ for (const node of nodeList) {
2666
+ if (!node || !node.key) continue;
2667
+ const top = Number(refreshedOffsetByKey[node.key]);
2668
+ if (!Number.isFinite(top)) continue;
2669
+ refreshedNodes.push({
2670
+ key: node.key,
2671
+ top
2672
+ });
2673
+ }
2674
+ return refreshedNodes;
2675
+ },
2676
+ getSessionMessageRefBinder(messageKey) {
2677
+ if (!this.isSessionTimelineNodeKey(messageKey)) return null;
2678
+ const current = this.sessionMessageRefBinderMap[messageKey];
2679
+ if (!current || current.ticket !== this.sessionTabRenderTicket) {
2680
+ const ticket = this.sessionTabRenderTicket;
2681
+ this.sessionMessageRefBinderMap[messageKey] = {
2682
+ ticket,
2683
+ bind: (el) => {
2684
+ this.bindSessionMessageRef(messageKey, el, ticket);
2685
+ }
2686
+ };
2687
+ }
2688
+ return this.sessionMessageRefBinderMap[messageKey].bind;
2689
+ },
1444
2690
  updateSessionTimelineOffset() {
1445
2691
  const container = this.sessionPreviewContainerEl || this.$refs.sessionPreviewContainer;
1446
2692
  if (!container || !container.style) return;
@@ -1451,12 +2697,39 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1451
2697
  const offset = headerHeight > 0 ? (headerHeight + 12) : 72;
1452
2698
  container.style.setProperty('--session-preview-header-offset', `${offset}px`);
1453
2699
  },
1454
- bindSessionMessageRef(messageKey, el) {
2700
+ bindSessionMessageRef(messageKey, el, ticket = this.sessionTabRenderTicket) {
2701
+ if (!this.sessionTimelineEnabled) return;
1455
2702
  if (!messageKey) return;
2703
+ if (ticket !== this.sessionTabRenderTicket) return;
1456
2704
  if (el) {
2705
+ if (!this.isSessionTimelineNodeKey(messageKey)) return;
2706
+ if (this.sessionMessageRefMap[messageKey] === el) return;
1457
2707
  this.sessionMessageRefMap[messageKey] = el;
2708
+ this.invalidateSessionTimelineMeasurementCache();
1458
2709
  } else {
2710
+ if (!this.sessionMessageRefMap[messageKey]) return;
1459
2711
  delete this.sessionMessageRefMap[messageKey];
2712
+ this.invalidateSessionTimelineMeasurementCache();
2713
+ }
2714
+ },
2715
+ isSessionTimelineNodeKey(messageKey) {
2716
+ if (!messageKey) return false;
2717
+ return !!(this.sessionTimelineNodeKeyMap && this.sessionTimelineNodeKeyMap[messageKey]);
2718
+ },
2719
+ pruneSessionMessageRefs() {
2720
+ const nodeKeyMap = this.sessionTimelineNodeKeyMap || Object.create(null);
2721
+ let removed = false;
2722
+ for (const key of Object.keys(this.sessionMessageRefMap)) {
2723
+ if (nodeKeyMap[key]) continue;
2724
+ delete this.sessionMessageRefMap[key];
2725
+ removed = true;
2726
+ }
2727
+ for (const key of Object.keys(this.sessionMessageRefBinderMap)) {
2728
+ if (nodeKeyMap[key]) continue;
2729
+ delete this.sessionMessageRefBinderMap[key];
2730
+ }
2731
+ if (removed) {
2732
+ this.invalidateSessionTimelineMeasurementCache();
1460
2733
  }
1461
2734
  },
1462
2735
  cancelSessionTimelineSync() {
@@ -1478,11 +2751,39 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1478
2751
  this.syncSessionTimelineActiveFromScroll();
1479
2752
  },
1480
2753
  onSessionPreviewScroll() {
2754
+ if (
2755
+ !this.sessionTimelineEnabled
2756
+ || this.mainTab !== 'sessions'
2757
+ || this.getMainTabForNav() !== 'sessions'
2758
+ || !this.sessionPreviewRenderEnabled
2759
+ ) return;
2760
+ if (!this.sessionTimelineNodes.length) return;
2761
+ const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
2762
+ if (!scrollEl) return;
2763
+ const now = Date.now();
2764
+ const currentTop = Number(scrollEl.scrollTop || 0);
2765
+ const delta = Math.abs(currentTop - Number(this.sessionTimelineLastScrollTop || 0));
2766
+ const elapsed = now - Number(this.sessionTimelineLastSyncAt || 0);
2767
+ if (delta < 48 && elapsed < 120) {
2768
+ return;
2769
+ }
2770
+ this.sessionTimelineLastScrollTop = currentTop;
2771
+ this.sessionTimelineLastSyncAt = now;
1481
2772
  this.scheduleSessionTimelineSync();
1482
2773
  },
1483
2774
  onWindowResize() {
1484
2775
  this.updateCompactLayoutMode();
2776
+ if (
2777
+ !this.sessionTimelineEnabled
2778
+ || this.mainTab !== 'sessions'
2779
+ || this.getMainTabForNav() !== 'sessions'
2780
+ || !this.sessionPreviewRenderEnabled
2781
+ ) {
2782
+ return;
2783
+ }
2784
+ if (!this.sessionTimelineNodes.length) return;
1485
2785
  this.updateSessionTimelineOffset();
2786
+ this.invalidateSessionTimelineMeasurementCache();
1486
2787
  this.scheduleSessionTimelineSync();
1487
2788
  },
1488
2789
  shouldForceCompactLayout() {
@@ -1535,35 +2836,93 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1535
2836
  this.applyCompactLayoutClass(enabled);
1536
2837
  },
1537
2838
  syncSessionTimelineActiveFromScroll() {
2839
+ if (
2840
+ !this.sessionTimelineEnabled
2841
+ || this.mainTab !== 'sessions'
2842
+ || this.getMainTabForNav() !== 'sessions'
2843
+ || !this.sessionPreviewRenderEnabled
2844
+ ) {
2845
+ if (this.sessionTimelineActiveKey) {
2846
+ this.sessionTimelineActiveKey = '';
2847
+ }
2848
+ return;
2849
+ }
1538
2850
  const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
1539
2851
  if (!nodes.length) {
1540
- this.sessionTimelineActiveKey = '';
2852
+ if (this.sessionTimelineActiveKey) {
2853
+ this.sessionTimelineActiveKey = '';
2854
+ }
1541
2855
  return;
1542
2856
  }
2857
+ this.pruneSessionMessageRefs();
1543
2858
  const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
1544
2859
  if (!scrollEl) {
1545
- this.sessionTimelineActiveKey = nodes[0].key;
2860
+ if (!this.isSessionTimelineNodeKey(this.sessionTimelineActiveKey)) {
2861
+ const fallbackKey = nodes[0].key;
2862
+ if (this.sessionTimelineActiveKey !== fallbackKey) {
2863
+ this.sessionTimelineActiveKey = fallbackKey;
2864
+ }
2865
+ }
1546
2866
  return;
1547
2867
  }
1548
- const scrollRect = scrollEl.getBoundingClientRect();
1549
- const headerEl = scrollEl.querySelector('.session-preview-header');
1550
- const headerHeight = headerEl ? headerEl.getBoundingClientRect().height : 0;
1551
- const anchorLine = scrollRect.top + headerHeight + 8;
1552
- let activeKey = nodes[0].key;
1553
- for (const node of nodes) {
1554
- const messageEl = this.sessionMessageRefMap[node.key];
1555
- if (!messageEl) continue;
1556
- const messageRect = messageEl.getBoundingClientRect();
1557
- if (messageRect.top <= anchorLine) {
1558
- activeKey = node.key;
1559
- continue;
2868
+ const headerEl = scrollEl.querySelector('.session-preview-header');
2869
+ const stickyOffset = headerEl ? (headerEl.offsetHeight + 8) : 8;
2870
+ const rawAnchorY = Number(scrollEl.scrollTop || 0) + stickyOffset;
2871
+ const previousAnchorY = Number(this.sessionTimelineLastAnchorY || 0);
2872
+ let direction = rawAnchorY - previousAnchorY;
2873
+ if (Math.abs(direction) < 1) {
2874
+ direction = Number(this.sessionTimelineLastDirection || 0);
2875
+ } else {
2876
+ this.sessionTimelineLastDirection = direction > 0 ? 1 : -1;
2877
+ }
2878
+ this.sessionTimelineLastAnchorY = rawAnchorY;
2879
+ const hysteresisPx = 18;
2880
+ const hysteresis = direction > 0 ? -hysteresisPx : (direction < 0 ? hysteresisPx : 0);
2881
+ const anchorY = rawAnchorY + hysteresis;
2882
+ const measuredNodes = this.getCachedSessionTimelineMeasuredNodes(nodes);
2883
+ if (!measuredNodes.length) {
2884
+ if (!this.isSessionTimelineNodeKey(this.sessionTimelineActiveKey)) {
2885
+ this.sessionTimelineActiveKey = nodes[0].key;
2886
+ }
2887
+ return;
2888
+ }
2889
+ let low = 0;
2890
+ let high = measuredNodes.length - 1;
2891
+ let candidateIndex = 0;
2892
+ while (low <= high) {
2893
+ const mid = Math.floor((low + high) / 2);
2894
+ if (measuredNodes[mid].top <= anchorY) {
2895
+ candidateIndex = mid;
2896
+ low = mid + 1;
2897
+ } else {
2898
+ high = mid - 1;
2899
+ }
2900
+ }
2901
+ let currentIndex = -1;
2902
+ if (this.sessionTimelineActiveKey) {
2903
+ for (let i = 0; i < measuredNodes.length; i += 1) {
2904
+ if (measuredNodes[i].key === this.sessionTimelineActiveKey) {
2905
+ currentIndex = i;
2906
+ break;
2907
+ }
1560
2908
  }
1561
- break;
1562
2909
  }
1563
- this.sessionTimelineActiveKey = activeKey;
2910
+ if (currentIndex >= 0) {
2911
+ if (direction > 0 && candidateIndex < currentIndex) {
2912
+ candidateIndex = currentIndex;
2913
+ } else if (direction < 0 && candidateIndex > currentIndex) {
2914
+ candidateIndex = currentIndex;
2915
+ }
2916
+ }
2917
+ const activeKey = measuredNodes[candidateIndex].key;
2918
+ if (this.sessionTimelineActiveKey !== activeKey) {
2919
+ this.sessionTimelineActiveKey = activeKey;
2920
+ }
1564
2921
  },
1565
2922
  jumpToSessionTimelineNode(messageKey) {
2923
+ if (!this.sessionTimelineEnabled || this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) return;
1566
2924
  if (!messageKey) return;
2925
+ if (!this.isSessionTimelineNodeKey(messageKey)) return;
1567
2926
  const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
1568
2927
  if (!scrollEl) return;
1569
2928
  const messageEl = this.sessionMessageRefMap[messageKey];
@@ -1636,67 +2995,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1636
2995
  },
1637
2996
 
1638
2997
  async loadSessions() {
1639
- if (this.sessionsLoading) return;
1640
- this.sessionsLoading = true;
1641
- this.activeSessionDetailError = '';
1642
- const params = buildSessionListParams({
1643
- source: this.sessionFilterSource,
1644
- pathFilter: this.sessionPathFilter,
1645
- query: this.sessionQuery,
1646
- roleFilter: this.sessionRoleFilter,
1647
- timeRangePreset: this.sessionTimePreset
1648
- });
1649
- try {
1650
- const res = await api('list-sessions', params);
1651
- if (res.error) {
1652
- this.showMessage(res.error, 'error');
1653
- this.sessionsList = [];
1654
- this.activeSession = null;
1655
- this.activeSessionMessages = [];
1656
- this.activeSessionDetailClipped = false;
1657
- this.cancelSessionTimelineSync();
1658
- this.sessionTimelineActiveKey = '';
1659
- this.sessionMessageRefMap = Object.create(null);
1660
- } else {
1661
- this.sessionsList = Array.isArray(res.sessions) ? res.sessions : [];
1662
- this.syncSessionPathOptionsForSource(
1663
- this.sessionFilterSource,
1664
- this.extractPathOptionsFromSessions(this.sessionsList),
1665
- true
1666
- );
1667
- if (this.sessionsList.length === 0) {
1668
- this.activeSession = null;
1669
- this.activeSessionMessages = [];
1670
- this.activeSessionDetailClipped = false;
1671
- this.cancelSessionTimelineSync();
1672
- this.sessionTimelineActiveKey = '';
1673
- this.sessionMessageRefMap = Object.create(null);
1674
- } else {
1675
- const oldKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
1676
- const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === oldKey);
1677
- this.activeSession = matched || this.sessionsList[0];
1678
- this.activeSessionMessages = [];
1679
- this.activeSessionDetailError = '';
1680
- this.activeSessionDetailClipped = false;
1681
- this.cancelSessionTimelineSync();
1682
- this.sessionTimelineActiveKey = '';
1683
- this.sessionMessageRefMap = Object.create(null);
1684
- await this.loadActiveSessionDetail();
1685
- }
1686
- void this.loadSessionPathOptions({ source: this.sessionFilterSource });
1687
- }
1688
- } catch (e) {
1689
- this.sessionsList = [];
1690
- this.activeSession = null;
1691
- this.activeSessionMessages = [];
1692
- this.activeSessionDetailClipped = false;
1693
- this.cancelSessionTimelineSync();
1694
- this.sessionTimelineActiveKey = '';
1695
- this.sessionMessageRefMap = Object.create(null);
1696
- this.showMessage('加载会话失败', 'error');
1697
- } finally {
1698
- this.sessionsLoading = false;
1699
- }
2998
+ const result = await loadSessionsHelper.call(this, api);
2999
+ this.pruneSessionPinnedMap(this.sessionsList);
3000
+ return result;
1700
3001
  },
1701
3002
 
1702
3003
  async selectSession(session) {
@@ -1704,11 +3005,13 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1704
3005
  if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
1705
3006
  this.activeSession = session;
1706
3007
  this.activeSessionMessages = [];
3008
+ this.resetSessionDetailPagination();
3009
+ this.resetSessionPreviewMessageRender();
1707
3010
  this.activeSessionDetailError = '';
1708
3011
  this.activeSessionDetailClipped = false;
1709
3012
  this.cancelSessionTimelineSync();
1710
3013
  this.sessionTimelineActiveKey = '';
1711
- this.sessionMessageRefMap = Object.create(null);
3014
+ this.clearSessionTimelineRefs();
1712
3015
  await this.loadActiveSessionDetail();
1713
3016
  },
1714
3017
 
@@ -1757,87 +3060,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1757
3060
  }
1758
3061
  },
1759
3062
 
1760
- async loadActiveSessionDetail() {
1761
- if (!this.activeSession) {
1762
- this.activeSessionMessages = [];
1763
- this.activeSessionDetailError = '';
1764
- this.activeSessionDetailClipped = false;
1765
- this.cancelSessionTimelineSync();
1766
- this.sessionTimelineActiveKey = '';
1767
- this.sessionMessageRefMap = Object.create(null);
1768
- return;
1769
- }
1770
-
1771
- const requestSeq = ++this.sessionDetailRequestSeq;
1772
- this.sessionDetailLoading = true;
1773
- this.activeSessionDetailError = '';
1774
- try {
1775
- const res = await api('session-detail', {
1776
- source: this.activeSession.source,
1777
- sessionId: this.activeSession.sessionId,
1778
- filePath: this.activeSession.filePath,
1779
- messageLimit: 300
1780
- });
1781
-
1782
- if (requestSeq !== this.sessionDetailRequestSeq) {
1783
- return;
1784
- }
1785
-
1786
- if (res.error) {
1787
- this.activeSessionMessages = [];
1788
- this.activeSessionDetailClipped = false;
1789
- this.activeSessionDetailError = res.error;
1790
- this.cancelSessionTimelineSync();
1791
- this.sessionTimelineActiveKey = '';
1792
- this.sessionMessageRefMap = Object.create(null);
1793
- return;
1794
- }
1795
-
1796
- const rawMessages = Array.isArray(res.messages) ? res.messages : [];
1797
- this.activeSessionMessages = rawMessages.map((message) => this.normalizeSessionMessage(message));
1798
- this.activeSessionDetailClipped = !!res.clipped;
1799
- if (this.activeSession) {
1800
- if (res.sourceLabel) {
1801
- this.activeSession.sourceLabel = res.sourceLabel;
1802
- }
1803
- if (res.sessionId) {
1804
- this.activeSession.sessionId = res.sessionId;
1805
- if (!this.activeSession.title) {
1806
- this.activeSession.title = res.sessionId;
1807
- }
1808
- }
1809
- if (res.filePath) {
1810
- this.activeSession.filePath = res.filePath;
1811
- }
1812
- }
1813
- if (res.updatedAt) {
1814
- this.activeSession.updatedAt = res.updatedAt;
1815
- }
1816
- if (res.cwd) {
1817
- this.activeSession.cwd = res.cwd;
1818
- }
1819
- if (Number.isFinite(res.totalMessages)) {
1820
- this.syncActiveSessionMessageCount(res.totalMessages);
1821
- }
1822
- this.$nextTick(() => {
1823
- this.updateSessionTimelineOffset();
1824
- this.scheduleSessionTimelineSync();
1825
- });
1826
- } catch (e) {
1827
- if (requestSeq !== this.sessionDetailRequestSeq) {
1828
- return;
1829
- }
1830
- this.activeSessionMessages = [];
1831
- this.activeSessionDetailClipped = false;
1832
- this.activeSessionDetailError = '加载会话内容失败: ' + e.message;
1833
- this.cancelSessionTimelineSync();
1834
- this.sessionTimelineActiveKey = '';
1835
- this.sessionMessageRefMap = Object.create(null);
1836
- } finally {
1837
- if (requestSeq === this.sessionDetailRequestSeq) {
1838
- this.sessionDetailLoading = false;
1839
- }
1840
- }
3063
+ async loadActiveSessionDetail(options = {}) {
3064
+ return loadActiveSessionDetailHelper.call(this, api, options);
1841
3065
  },
1842
3066
 
1843
3067
  downloadTextFile(fileName, content, mimeType = 'text/markdown;charset=utf-8') {
@@ -2142,9 +3366,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2142
3366
  return;
2143
3367
  }
2144
3368
  this.agentsContent = res.content || '';
3369
+ this.agentsOriginalContent = this.agentsContent;
2145
3370
  this.agentsPath = res.path || '';
2146
3371
  this.agentsExists = !!res.exists;
2147
3372
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3373
+ this.resetAgentsDiffState();
2148
3374
  this.showAgentsModal = true;
2149
3375
  } catch (e) {
2150
3376
  this.showMessage('加载文件失败', 'error');
@@ -2168,9 +3394,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2168
3394
  this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
2169
3395
  }
2170
3396
  this.agentsContent = res.content || '';
3397
+ this.agentsOriginalContent = this.agentsContent;
2171
3398
  this.agentsPath = res.path || '';
2172
3399
  this.agentsExists = !!res.exists;
2173
3400
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3401
+ this.resetAgentsDiffState();
2174
3402
  this.showAgentsModal = true;
2175
3403
  } catch (e) {
2176
3404
  this.showMessage('加载文件失败', 'error');
@@ -2197,9 +3425,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2197
3425
  this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
2198
3426
  }
2199
3427
  this.agentsContent = res.content || '';
3428
+ this.agentsOriginalContent = this.agentsContent;
2200
3429
  this.agentsPath = res.path || '';
2201
3430
  this.agentsExists = !!res.exists;
2202
3431
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3432
+ this.resetAgentsDiffState();
2203
3433
  this.showAgentsModal = true;
2204
3434
  } catch (e) {
2205
3435
  this.showMessage('加载文件失败', 'error');
@@ -2228,18 +3458,287 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2228
3458
  this.agentsWorkspaceFileName = '';
2229
3459
  },
2230
3460
 
2231
- closeAgentsModal() {
3461
+ resetAgentsDiffState() {
3462
+ this.agentsDiffVisible = false;
3463
+ this.agentsDiffLoading = false;
3464
+ this.agentsDiffError = '';
3465
+ this.agentsDiffLines = [];
3466
+ this.agentsDiffStats = {
3467
+ added: 0,
3468
+ removed: 0,
3469
+ unchanged: 0
3470
+ };
3471
+ this.agentsDiffTruncated = false;
3472
+ this.agentsDiffHasChangesValue = false;
3473
+ this.agentsDiffFingerprint = '';
3474
+ this._agentsDiffPreviewRequestToken = null;
3475
+ },
3476
+ handleGlobalKeydown(event) {
3477
+ if (!event || event.key !== 'Escape') {
3478
+ return;
3479
+ }
3480
+ if (this.showConfirmDialog) {
3481
+ event.preventDefault();
3482
+ event.stopPropagation();
3483
+ this.resolveConfirmDialog(false);
3484
+ return;
3485
+ }
3486
+ if (!this.showAgentsModal) {
3487
+ return;
3488
+ }
3489
+ event.preventDefault();
3490
+ event.stopPropagation();
3491
+ if (this.agentsSaving || this.agentsDiffLoading) {
3492
+ return;
3493
+ }
3494
+ if (this.agentsDiffVisible) {
3495
+ this.resetAgentsDiffState();
3496
+ return;
3497
+ }
3498
+ this.closeAgentsModal();
3499
+ },
3500
+ hasPendingAgentsDraft() {
3501
+ if (!this.showAgentsModal || this.agentsLoading || this.agentsSaving) {
3502
+ return false;
3503
+ }
3504
+ return this.hasAgentsContentChanged() || this.agentsDiffVisible;
3505
+ },
3506
+ handleBeforeUnload(event) {
3507
+ if (!this.hasPendingAgentsDraft()) {
3508
+ return;
3509
+ }
3510
+ if (event && typeof event.preventDefault === 'function') {
3511
+ event.preventDefault();
3512
+ event.returnValue = '';
3513
+ }
3514
+ return '';
3515
+ },
3516
+ hasAgentsContentChanged() {
3517
+ const original = typeof this.agentsOriginalContent === 'string' ? this.agentsOriginalContent : '';
3518
+ const current = typeof this.agentsContent === 'string' ? this.agentsContent : '';
3519
+ return original !== current;
3520
+ },
3521
+ requestConfirmDialog(options = {}) {
3522
+ if (typeof this.confirmDialogResolver === 'function') {
3523
+ this.confirmDialogResolver(false);
3524
+ }
3525
+ const confirmDisabled = options.confirmDisabled;
3526
+ this.confirmDialogTitle = typeof options.title === 'string' && options.title.trim()
3527
+ ? options.title.trim()
3528
+ : '请确认操作';
3529
+ this.confirmDialogMessage = typeof options.message === 'string' ? options.message : '';
3530
+ this.confirmDialogConfirmText = typeof options.confirmText === 'string' && options.confirmText.trim()
3531
+ ? options.confirmText.trim()
3532
+ : '确认';
3533
+ this.confirmDialogCancelText = typeof options.cancelText === 'string' && options.cancelText.trim()
3534
+ ? options.cancelText.trim()
3535
+ : '取消';
3536
+ this.confirmDialogDanger = !!options.danger;
3537
+ this.confirmDialogConfirmDisabled = typeof confirmDisabled === 'function' ? false : !!confirmDisabled;
3538
+ this.confirmDialogDisableWhen = typeof confirmDisabled === 'function' ? confirmDisabled : null;
3539
+ this.showConfirmDialog = true;
3540
+ return new Promise((resolve) => {
3541
+ this.confirmDialogResolver = resolve;
3542
+ });
3543
+ },
3544
+ isConfirmDialogDisabled() {
3545
+ if (typeof this.confirmDialogDisableWhen === 'function') {
3546
+ try {
3547
+ return !!this.confirmDialogDisableWhen.call(this);
3548
+ } catch (_) {
3549
+ return true;
3550
+ }
3551
+ }
3552
+ return !!this.confirmDialogConfirmDisabled;
3553
+ },
3554
+ resolveConfirmDialog(confirmed) {
3555
+ const resolver = typeof this.confirmDialogResolver === 'function'
3556
+ ? this.confirmDialogResolver
3557
+ : null;
3558
+ this.showConfirmDialog = false;
3559
+ this.confirmDialogTitle = '';
3560
+ this.confirmDialogMessage = '';
3561
+ this.confirmDialogConfirmText = '确认';
3562
+ this.confirmDialogCancelText = '取消';
3563
+ this.confirmDialogDanger = false;
3564
+ this.confirmDialogConfirmDisabled = false;
3565
+ this.confirmDialogDisableWhen = null;
3566
+ this.confirmDialogResolver = null;
3567
+ if (resolver) {
3568
+ resolver(!!confirmed);
3569
+ }
3570
+ },
3571
+ closeConfirmDialog() {
3572
+ this.resolveConfirmDialog(false);
3573
+ },
3574
+ onAgentsContentInput() {
3575
+ if (this.agentsDiffVisible || this.agentsDiffLines.length) {
3576
+ this.resetAgentsDiffState();
3577
+ }
3578
+ },
3579
+ buildAgentsDiffFingerprint() {
3580
+ const context = this.agentsContext || 'codex';
3581
+ const fileName = context === 'openclaw-workspace'
3582
+ ? (this.agentsWorkspaceFileName || '')
3583
+ : '';
3584
+ const lineEnding = this.agentsLineEnding || '\n';
3585
+ const content = typeof this.agentsContent === 'string' ? this.agentsContent : '';
3586
+ const original = typeof this.agentsOriginalContent === 'string' ? this.agentsOriginalContent : '';
3587
+ return `${context}::${fileName}::${lineEnding}::${content.length}::${content}::${original.length}::${original}`;
3588
+ },
3589
+ async prepareAgentsDiff() {
3590
+ const requestFingerprint = this.buildAgentsDiffFingerprint();
3591
+ const requestToken = Symbol('agents-diff-preview');
3592
+ this._agentsDiffPreviewRequestToken = requestToken;
3593
+ this.agentsDiffVisible = true;
3594
+ this.agentsDiffLoading = true;
3595
+ this.agentsDiffError = '';
3596
+ this.agentsDiffLines = [];
3597
+ this.agentsDiffStats = {
3598
+ added: 0,
3599
+ removed: 0,
3600
+ unchanged: 0
3601
+ };
3602
+ this.agentsDiffTruncated = false;
3603
+ this.agentsDiffHasChangesValue = false;
3604
+ try {
3605
+ const shouldApplyPreviewState = () => shouldApplyAgentsDiffPreviewResponse({
3606
+ isVisible: this.agentsDiffVisible,
3607
+ requestToken,
3608
+ activeRequestToken: this._agentsDiffPreviewRequestToken,
3609
+ requestFingerprint,
3610
+ currentFingerprint: this.buildAgentsDiffFingerprint()
3611
+ });
3612
+ const applyPreviewState = (diff) => {
3613
+ if (!shouldApplyPreviewState()) {
3614
+ return false;
3615
+ }
3616
+ const normalizedDiff = diff && typeof diff === 'object' ? diff : {};
3617
+ const rawLines = Array.isArray(normalizedDiff.lines) ? normalizedDiff.lines : [];
3618
+ this.agentsDiffLines = rawLines.filter(line => line && line.type);
3619
+ this.agentsDiffTruncated = !!normalizedDiff.truncated;
3620
+ this.agentsDiffHasChangesValue = !!normalizedDiff.hasChanges;
3621
+ if (normalizedDiff.stats && typeof normalizedDiff.stats === 'object') {
3622
+ this.agentsDiffStats = {
3623
+ added: Number(normalizedDiff.stats.added || 0),
3624
+ removed: Number(normalizedDiff.stats.removed || 0),
3625
+ unchanged: Number(normalizedDiff.stats.unchanged || 0)
3626
+ };
3627
+ } else {
3628
+ const stats = { added: 0, removed: 0, unchanged: 0 };
3629
+ for (const line of this.agentsDiffLines) {
3630
+ if (line && line.type === 'add') stats.added += 1;
3631
+ else if (line && line.type === 'del') stats.removed += 1;
3632
+ else stats.unchanged += 1;
3633
+ }
3634
+ this.agentsDiffStats = stats;
3635
+ }
3636
+ this.agentsDiffFingerprint = requestFingerprint;
3637
+ return true;
3638
+ };
3639
+ const previewRequest = buildAgentsDiffPreviewRequest({
3640
+ baseContent: this.agentsOriginalContent,
3641
+ content: this.agentsContent,
3642
+ lineEnding: this.agentsLineEnding,
3643
+ context: this.agentsContext,
3644
+ fileName: this.agentsWorkspaceFileName
3645
+ });
3646
+ if (previewRequest.exceedsBodyLimit) {
3647
+ applyPreviewState(buildAgentsDiffPreview({
3648
+ baseContent: this.agentsOriginalContent,
3649
+ content: this.agentsContent
3650
+ }));
3651
+ return;
3652
+ }
3653
+ const res = await apiWithMeta('preview-agents-diff', previewRequest.params);
3654
+ if (!shouldApplyPreviewState()) {
3655
+ return;
3656
+ }
3657
+ if (res.error) {
3658
+ if (isAgentsDiffPreviewPayloadTooLarge(res)) {
3659
+ applyPreviewState(buildAgentsDiffPreview({
3660
+ baseContent: this.agentsOriginalContent,
3661
+ content: this.agentsContent
3662
+ }));
3663
+ return;
3664
+ }
3665
+ this.agentsDiffError = res.error;
3666
+ return;
3667
+ }
3668
+ applyPreviewState(res.diff);
3669
+ } catch (e) {
3670
+ if (shouldApplyAgentsDiffPreviewResponse({
3671
+ isVisible: this.agentsDiffVisible,
3672
+ requestToken,
3673
+ activeRequestToken: this._agentsDiffPreviewRequestToken,
3674
+ requestFingerprint,
3675
+ currentFingerprint: this.buildAgentsDiffFingerprint()
3676
+ })) {
3677
+ this.agentsDiffError = '生成差异失败';
3678
+ }
3679
+ } finally {
3680
+ if (this._agentsDiffPreviewRequestToken === requestToken) {
3681
+ this.agentsDiffLoading = false;
3682
+ }
3683
+ }
3684
+ },
3685
+
3686
+ async closeAgentsModal(options = {}) {
3687
+ const force = !!(options && options.force);
3688
+ const shouldConfirmClose = !force
3689
+ && this.hasPendingAgentsDraft();
3690
+ if (shouldConfirmClose) {
3691
+ const message = this.agentsDiffVisible
3692
+ ? '当前处于差异预览模式,改动尚未保存。确认放弃改动并关闭吗?'
3693
+ : '存在未保存改动,确认放弃改动并关闭吗?(关闭页面或应用也会丢失改动)';
3694
+ const confirmed = await this.requestConfirmDialog({
3695
+ title: '放弃未保存改动',
3696
+ message,
3697
+ confirmText: '放弃并关闭',
3698
+ cancelText: '继续编辑',
3699
+ danger: true
3700
+ });
3701
+ if (!confirmed) {
3702
+ return;
3703
+ }
3704
+ }
2232
3705
  this.showAgentsModal = false;
2233
3706
  this.agentsContent = '';
3707
+ this.agentsOriginalContent = '';
2234
3708
  this.agentsPath = '';
2235
3709
  this.agentsExists = false;
2236
3710
  this.agentsLineEnding = '\n';
2237
3711
  this.agentsSaving = false;
2238
3712
  this.agentsWorkspaceFileName = '';
3713
+ this.resetAgentsDiffState();
2239
3714
  this.setAgentsModalContext('codex');
2240
3715
  },
2241
3716
 
2242
3717
  async applyAgentsContent() {
3718
+ if (!this.agentsDiffVisible) {
3719
+ if (!this.hasAgentsContentChanged()) {
3720
+ this.showMessage('未检测到改动', 'info');
3721
+ return;
3722
+ }
3723
+ await this.prepareAgentsDiff();
3724
+ return;
3725
+ }
3726
+ if (this.agentsDiffLoading) {
3727
+ return;
3728
+ }
3729
+ if (this.agentsDiffError) {
3730
+ this.showMessage(this.agentsDiffError, 'error');
3731
+ return;
3732
+ }
3733
+ const fingerprint = this.buildAgentsDiffFingerprint();
3734
+ if (this.agentsDiffFingerprint !== fingerprint) {
3735
+ await this.prepareAgentsDiff();
3736
+ return;
3737
+ }
3738
+ if (!this.agentsDiffHasChanges) {
3739
+ this.showMessage('未检测到改动', 'info');
3740
+ return;
3741
+ }
2243
3742
  this.agentsSaving = true;
2244
3743
  try {
2245
3744
  let action = 'apply-agents-file';
@@ -2262,7 +3761,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2262
3761
  ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
2263
3762
  : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存');
2264
3763
  this.showMessage(successLabel, 'success');
2265
- this.closeAgentsModal();
3764
+ this.closeAgentsModal({ force: true });
2266
3765
  } catch (e) {
2267
3766
  this.showMessage('保存失败', 'error');
2268
3767
  } finally {
@@ -2315,7 +3814,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2315
3814
  ? String(providerOrName.name || '')
2316
3815
  : String(providerOrName);
2317
3816
  const normalized = rawName.trim().toLowerCase();
2318
- return normalized === 'local' || normalized === 'codexmate-proxy';
3817
+ return normalized === 'local';
2319
3818
  },
2320
3819
 
2321
3820
  providerPillState(provider) {
@@ -2355,7 +3854,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2355
3854
  if (!providerOrName) return false;
2356
3855
  if (typeof providerOrName === 'object') {
2357
3856
  const directName = String(providerOrName.name || '').trim().toLowerCase();
2358
- if (directName === 'local' || directName === 'codexmate-proxy') {
3857
+ if (directName === 'local') {
2359
3858
  return true;
2360
3859
  }
2361
3860
  return !!providerOrName.nonDeletable;
@@ -2363,7 +3862,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2363
3862
  const name = String(providerOrName).trim();
2364
3863
  if (!name) return false;
2365
3864
  const normalized = name.toLowerCase();
2366
- if (normalized === 'local' || normalized === 'codexmate-proxy') {
3865
+ if (normalized === 'local') {
2367
3866
  return true;
2368
3867
  }
2369
3868
  const target = (this.providersList || []).find((item) => item && item.name === name);
@@ -2622,12 +4121,18 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2622
4121
  this.refreshClaudeModelContext();
2623
4122
  },
2624
4123
 
2625
- deleteClaudeConfig(name) {
4124
+ async deleteClaudeConfig(name) {
2626
4125
  if (Object.keys(this.claudeConfigs).length <= 1) {
2627
4126
  return this.showMessage('至少保留一项', 'error');
2628
4127
  }
2629
-
2630
- if (!confirm(`确定删除配置 "${name}"?`)) return;
4128
+ const confirmed = await this.requestConfirmDialog({
4129
+ title: '删除 Claude 配置',
4130
+ message: `确定删除配置 "${name}"?`,
4131
+ confirmText: '删除',
4132
+ cancelText: '取消',
4133
+ danger: true
4134
+ });
4135
+ if (!confirmed) return;
2631
4136
 
2632
4137
  delete this.claudeConfigs[name];
2633
4138
  if (this.currentClaudeConfig === name) {
@@ -3713,11 +5218,18 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
3713
5218
  }
3714
5219
  },
3715
5220
 
3716
- deleteOpenclawConfig(name) {
5221
+ async deleteOpenclawConfig(name) {
3717
5222
  if (Object.keys(this.openclawConfigs).length <= 1) {
3718
5223
  return this.showMessage('至少保留一项', 'error');
3719
5224
  }
3720
- if (!confirm(`确定删除配置 "${name}"?`)) return;
5225
+ const confirmed = await this.requestConfirmDialog({
5226
+ title: '删除 OpenClaw 配置',
5227
+ message: `确定删除配置 "${name}"?`,
5228
+ confirmText: '删除',
5229
+ cancelText: '取消',
5230
+ danger: true
5231
+ });
5232
+ if (!confirmed) return;
3721
5233
  delete this.openclawConfigs[name];
3722
5234
  if (this.currentOpenclawConfig === name) {
3723
5235
  this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
@@ -4022,215 +5534,6 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
4022
5534
  }
4023
5535
  },
4024
5536
 
4025
- triggerCodexAuthUpload() {
4026
- const input = this.$refs.codexAuthImportInput;
4027
- if (input) {
4028
- input.value = '';
4029
- input.click();
4030
- }
4031
- },
4032
-
4033
- handleCodexAuthImportChange(event) {
4034
- const file = event && event.target && event.target.files ? event.target.files[0] : null;
4035
- if (file) {
4036
- void this.importCodexAuthFile(file);
4037
- }
4038
- },
4039
-
4040
- resetCodexAuthImportInput() {
4041
- const el = this.$refs.codexAuthImportInput;
4042
- if (el) {
4043
- el.value = '';
4044
- }
4045
- },
4046
-
4047
- async importCodexAuthFile(file) {
4048
- this.codexAuthImportLoading = true;
4049
- try {
4050
- const base64 = await this.readFileAsBase64(file);
4051
- const res = await api('import-auth-profile', {
4052
- fileName: file.name || 'codex-auth.json',
4053
- fileBase64: base64,
4054
- activate: true
4055
- });
4056
- if (res && res.error) {
4057
- this.showMessage(res.error, 'error');
4058
- return;
4059
- }
4060
- await this.loadCodexAuthProfiles({ silent: true });
4061
- this.showMessage('认证文件已导入并切换', 'success');
4062
- } catch (e) {
4063
- this.showMessage('导入认证文件失败', 'error');
4064
- } finally {
4065
- this.codexAuthImportLoading = false;
4066
- this.resetCodexAuthImportInput();
4067
- }
4068
- },
4069
-
4070
- async switchCodexAuthProfile(name) {
4071
- const key = String(name || '').trim();
4072
- if (!key || this.codexAuthSwitching[key]) return;
4073
- this.codexAuthSwitching[key] = true;
4074
- try {
4075
- const res = await api('switch-auth-profile', { name: key });
4076
- if (res && res.error) {
4077
- this.showMessage(res.error, 'error');
4078
- return;
4079
- }
4080
- await this.loadCodexAuthProfiles({ silent: true });
4081
- this.showMessage(`已切换认证: ${key}`, 'success');
4082
- } catch (e) {
4083
- this.showMessage('切换认证失败', 'error');
4084
- } finally {
4085
- this.codexAuthSwitching[key] = false;
4086
- }
4087
- },
4088
-
4089
- async deleteCodexAuthProfile(name) {
4090
- const key = String(name || '').trim();
4091
- if (!key || this.codexAuthDeleting[key]) return;
4092
- this.codexAuthDeleting[key] = true;
4093
- try {
4094
- const res = await api('delete-auth-profile', { name: key });
4095
- if (res && res.error) {
4096
- this.showMessage(res.error, 'error');
4097
- return;
4098
- }
4099
- await this.loadCodexAuthProfiles({ silent: true });
4100
- const switchedTip = res && res.switchedTo ? `,已切换到 ${res.switchedTo}` : '';
4101
- this.showMessage(`已删除认证${switchedTip}`, 'success');
4102
- } catch (e) {
4103
- this.showMessage('删除认证失败', 'error');
4104
- } finally {
4105
- this.codexAuthDeleting[key] = false;
4106
- }
4107
- },
4108
-
4109
- mergeProxySettings(nextSettings) {
4110
- const safe = nextSettings && typeof nextSettings === 'object' ? nextSettings : {};
4111
- const port = parseInt(String(safe.port), 10);
4112
- const timeoutMs = parseInt(String(safe.timeoutMs), 10);
4113
- this.proxySettings = {
4114
- enabled: safe.enabled !== false,
4115
- host: typeof safe.host === 'string' && safe.host.trim() ? safe.host.trim() : '127.0.0.1',
4116
- port: Number.isFinite(port) ? port : 8318,
4117
- provider: typeof safe.provider === 'string' ? safe.provider.trim() : '',
4118
- authSource: safe.authSource === 'profile' || safe.authSource === 'none' ? safe.authSource : 'provider',
4119
- timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : 30000
4120
- };
4121
- },
4122
-
4123
- async loadProxyStatus(options = {}) {
4124
- const silent = !!options.silent;
4125
- this.proxyLoading = true;
4126
- try {
4127
- const res = await api('proxy-status');
4128
- if (res && res.error) {
4129
- if (!silent) {
4130
- this.showMessage(res.error, 'error');
4131
- }
4132
- return;
4133
- }
4134
- this.mergeProxySettings(res && res.settings ? res.settings : {});
4135
- this.proxyRuntime = res && res.runtime ? { running: true, ...res.runtime } : null;
4136
- } catch (e) {
4137
- if (!silent) {
4138
- this.showMessage('读取代理状态失败', 'error');
4139
- }
4140
- } finally {
4141
- this.proxyLoading = false;
4142
- }
4143
- },
4144
-
4145
- async saveProxySettings(options = {}) {
4146
- const silent = !!options.silent;
4147
- this.proxySaving = true;
4148
- try {
4149
- const res = await api('proxy-save-config', this.proxySettings);
4150
- if (res && res.error) {
4151
- if (!silent) {
4152
- this.showMessage(res.error, 'error');
4153
- }
4154
- return;
4155
- }
4156
- if (res && res.settings) {
4157
- this.mergeProxySettings(res.settings);
4158
- }
4159
- if (!silent) {
4160
- this.showMessage('代理配置已保存', 'success');
4161
- }
4162
- } catch (e) {
4163
- if (!silent) {
4164
- this.showMessage('保存代理配置失败', 'error');
4165
- }
4166
- } finally {
4167
- this.proxySaving = false;
4168
- }
4169
- },
4170
-
4171
- async startBuiltinProxy() {
4172
- this.proxyStarting = true;
4173
- try {
4174
- const res = await api('proxy-start', {
4175
- ...this.proxySettings,
4176
- enabled: true
4177
- });
4178
- if (res && res.error) {
4179
- this.showMessage(res.error, 'error');
4180
- return;
4181
- }
4182
- if (res && res.settings) {
4183
- this.mergeProxySettings(res.settings);
4184
- }
4185
- await this.loadProxyStatus({ silent: true });
4186
- const listenTip = res && res.listenUrl ? `:${res.listenUrl}` : '';
4187
- this.showMessage(`代理已启动${listenTip}`, 'success');
4188
- } catch (e) {
4189
- this.showMessage('启动代理失败', 'error');
4190
- } finally {
4191
- this.proxyStarting = false;
4192
- }
4193
- },
4194
-
4195
- async stopBuiltinProxy() {
4196
- this.proxyStopping = true;
4197
- try {
4198
- const res = await api('proxy-stop');
4199
- if (res && res.error) {
4200
- this.showMessage(res.error, 'error');
4201
- return;
4202
- }
4203
- await this.loadProxyStatus({ silent: true });
4204
- this.showMessage('代理已停止', 'success');
4205
- } catch (e) {
4206
- this.showMessage('停止代理失败', 'error');
4207
- } finally {
4208
- this.proxyStopping = false;
4209
- }
4210
- },
4211
-
4212
- async applyBuiltinProxyProvider() {
4213
- this.proxyApplying = true;
4214
- try {
4215
- const saveRes = await api('proxy-save-config', this.proxySettings);
4216
- if (saveRes && saveRes.error) {
4217
- this.showMessage(saveRes.error, 'error');
4218
- return;
4219
- }
4220
- const res = await api('proxy-apply-provider', { switchToProxy: true });
4221
- if (res && res.error) {
4222
- this.showMessage(res.error, 'error');
4223
- return;
4224
- }
4225
- await this.loadAll();
4226
- this.showMessage('本地代理 provider 已写入并切换', 'success');
4227
- } catch (e) {
4228
- this.showMessage('应用代理 provider 失败', 'error');
4229
- } finally {
4230
- this.proxyApplying = false;
4231
- }
4232
- },
4233
-
4234
5537
  showMessage(text, type) {
4235
5538
  this.message = text;
4236
5539
  this.messageType = type || 'info';
@@ -4243,4 +5546,3 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
4243
5546
 
4244
5547
  app.mount('#app');
4245
5548
  });
4246
-