codexmate 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web-ui/app.js CHANGED
@@ -4,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,13 @@ 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
+ confirmDialogResolver: null,
100
148
  configTemplateContent: '',
101
149
  configTemplateApplying: false,
102
150
  codexApplying: false,
@@ -106,6 +154,19 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
106
154
  agentsLineEnding: '\n',
107
155
  agentsLoading: false,
108
156
  agentsSaving: false,
157
+ agentsOriginalContent: '',
158
+ agentsDiffVisible: false,
159
+ agentsDiffLoading: false,
160
+ agentsDiffError: '',
161
+ agentsDiffLines: [],
162
+ agentsDiffStats: {
163
+ added: 0,
164
+ removed: 0,
165
+ unchanged: 0
166
+ },
167
+ agentsDiffTruncated: false,
168
+ agentsDiffHasChangesValue: false,
169
+ agentsDiffFingerprint: '',
109
170
  agentsContext: 'codex',
110
171
  agentsModalTitle: 'AGENTS.md 编辑器',
111
172
  agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
@@ -122,7 +183,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
122
183
  skillsImporting: false,
123
184
  skillsZipImporting: false,
124
185
  skillsExporting: false,
186
+ sessionPinnedMap: {},
125
187
  sessionsList: [],
188
+ sessionsLoadedOnce: false,
126
189
  sessionsLoading: false,
127
190
  sessionFilterSource: 'all',
128
191
  sessionPathFilter: '',
@@ -152,13 +215,31 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
152
215
  activeSessionDetailClipped: false,
153
216
  sessionDetailLoading: false,
154
217
  sessionDetailRequestSeq: 0,
218
+ sessionDetailInitialMessageLimit: 80,
219
+ sessionDetailFetchStep: 80,
220
+ sessionDetailMessageLimit: 80,
221
+ sessionDetailMessageLimitCap: 1000,
155
222
  sessionTimelineActiveKey: '',
156
223
  sessionTimelineRafId: 0,
224
+ sessionTimelineLastSyncAt: 0,
225
+ sessionTimelineLastScrollTop: 0,
226
+ sessionTimelineLastAnchorY: 0,
227
+ sessionTimelineLastDirection: 0,
228
+ sessionTimelineEnabled: true,
157
229
  sessionMessageRefMap: Object.create(null),
230
+ sessionMessageRefBinderMap: Object.create(null),
158
231
  sessionPreviewScrollEl: null,
159
232
  sessionPreviewContainerEl: null,
160
233
  sessionPreviewHeaderEl: null,
161
234
  sessionPreviewHeaderResizeObserver: null,
235
+ sessionListRenderEnabled: false,
236
+ sessionPreviewRenderEnabled: false,
237
+ sessionTabRenderTicket: 0,
238
+ sessionPreviewVisibleCount: 0,
239
+ sessionPreviewInitialBatchSize: 12,
240
+ sessionPreviewLoadStep: 24,
241
+ sessionPreviewPendingVisibleCount: 0,
242
+ sessionPreviewLoadingMore: false,
162
243
  sessionStandalone: false,
163
244
  sessionStandaloneError: '',
164
245
  sessionStandaloneText: '',
@@ -275,6 +356,22 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
275
356
  codexDownloadLoading: false,
276
357
  codexDownloadProgress: 0,
277
358
  codexDownloadTimer: null,
359
+ settingsTab: 'backup',
360
+ sessionTrashItems: [],
361
+ sessionTrashVisibleCount: SESSION_TRASH_PAGE_SIZE,
362
+ sessionTrashTotalCount: 0,
363
+ sessionTrashCountLoadedOnce: false,
364
+ sessionTrashLoadedOnce: false,
365
+ sessionTrashLastLoadFailed: false,
366
+ sessionTrashCountRequestToken: 0,
367
+ sessionTrashListRequestToken: 0,
368
+ sessionTrashCountPendingOptions: null,
369
+ sessionTrashPendingOptions: null,
370
+ sessionTrashCountLoading: false,
371
+ sessionTrashLoading: false,
372
+ sessionTrashRestoring: {},
373
+ sessionTrashPurging: {},
374
+ sessionTrashClearing: false,
278
375
  claudeImportLoading: false,
279
376
  codexImportLoading: false,
280
377
  codexAuthProfiles: [],
@@ -309,7 +406,10 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
309
406
  this.sessionResumeWithYolo = true;
310
407
  }
311
408
  this.restoreSessionFilterCache();
409
+ this.restoreSessionPinnedMap();
312
410
  window.addEventListener('resize', this.onWindowResize);
411
+ window.addEventListener('keydown', this.handleGlobalKeydown);
412
+ window.addEventListener('beforeunload', this.handleBeforeUnload);
313
413
  const savedConfigs = localStorage.getItem('claudeConfigs');
314
414
  if (savedConfigs) {
315
415
  try {
@@ -346,25 +446,104 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
346
446
  this.loadAll();
347
447
  },
348
448
  beforeUnmount() {
349
- this.cancelSessionTimelineSync();
449
+ this.teardownSessionTabRender();
450
+ this.cancelScheduledSessionTabDeferredTeardown();
350
451
  this.disconnectSessionPreviewHeaderResizeObserver();
351
452
  window.removeEventListener('resize', this.onWindowResize);
453
+ window.removeEventListener('keydown', this.handleGlobalKeydown);
454
+ window.removeEventListener('beforeunload', this.handleBeforeUnload);
352
455
  this.applyCompactLayoutClass(false);
353
456
  this.sessionPreviewScrollEl = null;
354
457
  this.sessionPreviewContainerEl = null;
355
458
  this.sessionPreviewHeaderEl = null;
356
- this.sessionMessageRefMap = Object.create(null);
459
+ this.clearSessionTimelineRefs();
357
460
  },
358
461
 
359
462
  computed: {
360
463
  isSessionQueryEnabled() {
361
464
  return isSessionQueryEnabled(this.sessionFilterSource);
362
465
  },
466
+ activeSessionExportKey() {
467
+ return this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
468
+ },
469
+ sortedSessionsList() {
470
+ const list = Array.isArray(this.sessionsList) ? this.sessionsList : [];
471
+ if (list.length === 0) return [];
472
+ const pinnedMap = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
473
+ ? this.sessionPinnedMap
474
+ : {};
475
+ let hasPinned = false;
476
+ const decorated = list.map((session, index) => {
477
+ const key = session ? this.getSessionExportKey(session) : '';
478
+ const rawPinnedAt = key ? pinnedMap[key] : 0;
479
+ const pinnedAt = Number.isFinite(Number(rawPinnedAt))
480
+ ? Math.floor(Number(rawPinnedAt))
481
+ : 0;
482
+ const isPinned = pinnedAt > 0;
483
+ if (isPinned) {
484
+ hasPinned = true;
485
+ }
486
+ return { session, index, pinnedAt, isPinned };
487
+ });
488
+ if (!hasPinned) return list;
489
+ decorated.sort((a, b) => {
490
+ if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1;
491
+ if (a.isPinned && a.pinnedAt !== b.pinnedAt) return b.pinnedAt - a.pinnedAt;
492
+ return a.index - b.index;
493
+ });
494
+ return decorated.map(item => item.session);
495
+ },
496
+ activeSessionVisibleMessages() {
497
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
498
+ return [];
499
+ }
500
+ const list = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages : [];
501
+ const rawCount = Number(this.sessionPreviewVisibleCount);
502
+ const visibleCount = Number.isFinite(rawCount)
503
+ ? Math.max(0, Math.floor(rawCount))
504
+ : 0;
505
+ if (visibleCount <= 0) {
506
+ if (!list.length) return [];
507
+ // Defensive fallback: avoid getting stuck in "正在渲染会话内容..."
508
+ // when visible count has not been primed yet.
509
+ return list.slice(0, Math.min(8, list.length));
510
+ }
511
+ if (visibleCount >= list.length) return list;
512
+ return list.slice(0, visibleCount);
513
+ },
514
+ canLoadMoreSessionMessages() {
515
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
516
+ return false;
517
+ }
518
+ const total = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages.length : 0;
519
+ const visible = Array.isArray(this.activeSessionVisibleMessages) ? this.activeSessionVisibleMessages.length : 0;
520
+ return total > visible;
521
+ },
522
+ sessionPreviewRemainingCount() {
523
+ const total = Array.isArray(this.activeSessionMessages) ? this.activeSessionMessages.length : 0;
524
+ const visible = Array.isArray(this.activeSessionVisibleMessages) ? this.activeSessionVisibleMessages.length : 0;
525
+ return Math.max(0, total - visible);
526
+ },
363
527
  sessionTimelineNodes() {
364
- return buildSessionTimelineNodes(this.activeSessionMessages, {
528
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
529
+ return [];
530
+ }
531
+ return buildSessionTimelineNodes(this.activeSessionVisibleMessages, {
365
532
  getKey: (message, index) => this.getRecordRenderKey(message, index)
366
533
  });
367
534
  },
535
+ sessionTimelineNodeKeyMap() {
536
+ const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
537
+ if (!nodes.length) {
538
+ return Object.create(null);
539
+ }
540
+ const map = Object.create(null);
541
+ for (const node of nodes) {
542
+ if (!node || !node.key) continue;
543
+ map[node.key] = true;
544
+ }
545
+ return map;
546
+ },
368
547
  sessionTimelineActiveTitle() {
369
548
  if (!this.sessionTimelineActiveKey) return '';
370
549
  const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
@@ -377,6 +556,15 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
377
556
  }
378
557
  return '当前来源暂不支持关键词检索';
379
558
  },
559
+ agentsDiffHasChanges() {
560
+ if (this.agentsDiffTruncated) {
561
+ return !!this.agentsDiffHasChangesValue;
562
+ }
563
+ const stats = this.agentsDiffStats || {};
564
+ const added = Number(stats.added || 0);
565
+ const removed = Number(stats.removed || 0);
566
+ return added > 0 || removed > 0;
567
+ },
380
568
  claudeModelHasList() {
381
569
  return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
382
570
  },
@@ -416,6 +604,29 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
416
604
  installRegistryPreview() {
417
605
  return this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
418
606
  },
607
+ visibleSessionTrashItems() {
608
+ const items = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
609
+ const visibleCount = Number(this.sessionTrashVisibleCount);
610
+ const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0
611
+ ? Math.floor(visibleCount)
612
+ : SESSION_TRASH_PAGE_SIZE;
613
+ return items.slice(0, safeVisibleCount);
614
+ },
615
+ sessionTrashHasMoreItems() {
616
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
617
+ return this.visibleSessionTrashItems.length < totalItems;
618
+ },
619
+ sessionTrashHiddenCount() {
620
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
621
+ return Math.max(0, totalItems - this.visibleSessionTrashItems.length);
622
+ },
623
+ sessionTrashCount() {
624
+ const totalCount = Number(this.sessionTrashTotalCount);
625
+ if (Number.isFinite(totalCount) && totalCount >= 0) {
626
+ return Math.max(0, Math.floor(totalCount));
627
+ }
628
+ return Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
629
+ },
419
630
  ...createSkillsComputed(),
420
631
 
421
632
  ...createConfigModeComputed(),
@@ -807,21 +1018,413 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
807
1018
  const normalizedMode = typeof mode === 'string'
808
1019
  ? mode.trim().toLowerCase()
809
1020
  : '';
810
- this.mainTab = 'config';
811
1021
  this.configMode = CONFIG_MODE_SET.has(normalizedMode) ? normalizedMode : 'codex';
812
- if (this.configMode === 'claude') {
813
- this.refreshClaudeModelContext();
1022
+ if (this.mainTab === 'config') {
1023
+ if (this.configMode === 'claude') {
1024
+ const expectedMainTab = 'config';
1025
+ const expectedConfigMode = 'claude';
1026
+ const refresh = () => {
1027
+ if (this.mainTab !== expectedMainTab || this.configMode !== expectedConfigMode) {
1028
+ return;
1029
+ }
1030
+ this.refreshClaudeModelContext();
1031
+ };
1032
+ if (typeof this.scheduleAfterFrame === 'function') {
1033
+ this.scheduleAfterFrame(refresh);
1034
+ } else {
1035
+ refresh();
1036
+ }
1037
+ }
1038
+ this.scheduleAfterFrame(() => {
1039
+ this.clearMainTabSwitchIntent('config');
1040
+ });
1041
+ return;
814
1042
  }
1043
+ this.switchMainTab('config');
815
1044
  },
816
1045
 
1046
+ ensureMainTabSwitchState() {
1047
+ if (this.__mainTabSwitchState) {
1048
+ return this.__mainTabSwitchState;
1049
+ }
1050
+ this.__mainTabSwitchState = {
1051
+ intent: '',
1052
+ pendingTarget: '',
1053
+ ticket: 0
1054
+ };
1055
+ return this.__mainTabSwitchState;
1056
+ },
1057
+ ensureImmediateNavDomState() {
1058
+ if (typeof document === 'undefined') {
1059
+ return {
1060
+ navNodes: [],
1061
+ sessionPanelEl: null
1062
+ };
1063
+ }
1064
+ if (!this.__immediateNavDomState) {
1065
+ this.__immediateNavDomState = {
1066
+ navNodes: [],
1067
+ sessionPanelEl: null
1068
+ };
1069
+ }
1070
+ const state = this.__immediateNavDomState;
1071
+ const needsNavRefresh = !Array.isArray(state.navNodes)
1072
+ || !state.navNodes.length
1073
+ || state.navNodes.some((node) => !node || !node.isConnected);
1074
+ if (needsNavRefresh) {
1075
+ state.navNodes = Array.from(document.querySelectorAll('[data-main-tab]'));
1076
+ }
1077
+ if (!state.sessionPanelEl || !state.sessionPanelEl.isConnected) {
1078
+ state.sessionPanelEl = document.getElementById('panel-sessions');
1079
+ }
1080
+ return state;
1081
+ },
1082
+ setMainTabSwitchIntent(tab) {
1083
+ const normalizedTab = typeof tab === 'string'
1084
+ ? tab.trim().toLowerCase()
1085
+ : '';
1086
+ if (!normalizedTab) return;
1087
+ const state = this.ensureMainTabSwitchState();
1088
+ state.intent = normalizedTab;
1089
+ },
1090
+ applyImmediateNavIntent(tab, configMode = '') {
1091
+ if (typeof document === 'undefined') return;
1092
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1093
+ if (!normalizedTab) return;
1094
+ const normalizedMode = typeof configMode === 'string' ? configMode.trim().toLowerCase() : '';
1095
+ const domState = this.ensureImmediateNavDomState();
1096
+ const nodes = Array.isArray(domState.navNodes) ? domState.navNodes : [];
1097
+ for (const node of nodes) {
1098
+ if (!node || !node.classList) continue;
1099
+ const nodeTab = String(node.getAttribute('data-main-tab') || '').trim().toLowerCase();
1100
+ const nodeMode = String(node.getAttribute('data-config-mode') || '').trim().toLowerCase();
1101
+ let shouldActivate = nodeTab === normalizedTab;
1102
+ if (shouldActivate && normalizedTab === 'config') {
1103
+ shouldActivate = nodeMode ? nodeMode === normalizedMode : false;
1104
+ }
1105
+ node.classList.toggle('nav-intent-active', !!shouldActivate);
1106
+ node.classList.toggle('nav-intent-inactive', !shouldActivate);
1107
+ }
1108
+ },
1109
+ clearImmediateNavIntent() {
1110
+ if (typeof document === 'undefined') return;
1111
+ const domState = this.ensureImmediateNavDomState();
1112
+ const nodes = Array.isArray(domState.navNodes) ? domState.navNodes : [];
1113
+ for (const node of nodes) {
1114
+ if (!node || !node.classList) continue;
1115
+ node.classList.remove('nav-intent-active');
1116
+ node.classList.remove('nav-intent-inactive');
1117
+ }
1118
+ },
1119
+ setSessionPanelFastHidden(hidden) {
1120
+ if (typeof document === 'undefined') return;
1121
+ const domState = this.ensureImmediateNavDomState();
1122
+ const panel = domState.sessionPanelEl;
1123
+ if (!panel || !panel.classList) return;
1124
+ panel.classList.toggle('session-panel-fast-hidden', !!hidden);
1125
+ },
1126
+ isSessionPanelFastHidden() {
1127
+ if (typeof document === 'undefined') return false;
1128
+ const domState = this.ensureImmediateNavDomState();
1129
+ const panel = domState.sessionPanelEl;
1130
+ return !!(panel && panel.classList && panel.classList.contains('session-panel-fast-hidden'));
1131
+ },
1132
+ recordPointerNavCommit(kind, value) {
1133
+ const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
1134
+ const normalizedValue = typeof value === 'string' ? value.trim().toLowerCase() : '';
1135
+ if (!normalizedKind || !normalizedValue) {
1136
+ this.__pointerNavCommitState = null;
1137
+ return;
1138
+ }
1139
+ this.__pointerNavCommitState = {
1140
+ kind: normalizedKind,
1141
+ value: normalizedValue,
1142
+ at: Date.now()
1143
+ };
1144
+ },
1145
+ consumePointerNavCommit(kind, value) {
1146
+ const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
1147
+ const normalizedValue = typeof value === 'string' ? value.trim().toLowerCase() : '';
1148
+ const state = this.__pointerNavCommitState;
1149
+ this.__pointerNavCommitState = null;
1150
+ if (!state || !normalizedKind || !normalizedValue) {
1151
+ return false;
1152
+ }
1153
+ if (state.kind !== normalizedKind || state.value !== normalizedValue) {
1154
+ return false;
1155
+ }
1156
+ return (Date.now() - Number(state.at || 0)) <= 1000;
1157
+ },
1158
+ onMainTabPointerDown(tab) {
1159
+ const event = arguments.length > 1 ? arguments[1] : null;
1160
+ if (event && typeof event.button === 'number' && event.button !== 0) {
1161
+ return;
1162
+ }
1163
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1164
+ if (!normalizedTab) return;
1165
+ this.setMainTabSwitchIntent(normalizedTab);
1166
+ this.applyImmediateNavIntent(normalizedTab);
1167
+ const shouldHideSessionPanel = this.mainTab === 'sessions' && normalizedTab !== 'sessions';
1168
+ this.setSessionPanelFastHidden(shouldHideSessionPanel);
1169
+ const pointerType = event && typeof event.pointerType === 'string'
1170
+ ? event.pointerType.trim().toLowerCase()
1171
+ : '';
1172
+ if (pointerType === 'touch') {
1173
+ return;
1174
+ }
1175
+ this.recordPointerNavCommit('main', normalizedTab);
1176
+ this.switchMainTab(normalizedTab);
1177
+ },
1178
+ onConfigTabPointerDown(mode) {
1179
+ const event = arguments.length > 1 ? arguments[1] : null;
1180
+ if (event && typeof event.button === 'number' && event.button !== 0) {
1181
+ return;
1182
+ }
1183
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
1184
+ if (!normalizedMode) return;
1185
+ this.setMainTabSwitchIntent('config');
1186
+ this.applyImmediateNavIntent('config', normalizedMode);
1187
+ const shouldHideSessionPanel = this.mainTab === 'sessions';
1188
+ this.setSessionPanelFastHidden(shouldHideSessionPanel);
1189
+ const pointerType = event && typeof event.pointerType === 'string'
1190
+ ? event.pointerType.trim().toLowerCase()
1191
+ : '';
1192
+ if (pointerType === 'touch') {
1193
+ return;
1194
+ }
1195
+ this.recordPointerNavCommit('config', normalizedMode);
1196
+ this.switchConfigMode(normalizedMode);
1197
+ },
1198
+ onMainTabClick(tab) {
1199
+ const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
1200
+ if (!normalizedTab) return;
1201
+ if (this.consumePointerNavCommit('main', normalizedTab)) return;
1202
+ this.switchMainTab(normalizedTab);
1203
+ },
1204
+ onConfigTabClick(mode) {
1205
+ const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
1206
+ if (!normalizedMode) return;
1207
+ if (this.consumePointerNavCommit('config', normalizedMode)) return;
1208
+ this.switchConfigMode(normalizedMode);
1209
+ },
1210
+ clearMainTabSwitchIntent(expectedTab = '') {
1211
+ const state = this.ensureMainTabSwitchState();
1212
+ if (expectedTab && state.intent && state.intent !== expectedTab) {
1213
+ return;
1214
+ }
1215
+ state.intent = '';
1216
+ state.pendingTarget = '';
1217
+ this.clearImmediateNavIntent();
1218
+ this.setSessionPanelFastHidden(false);
1219
+ },
1220
+ getMainTabForNav() {
1221
+ const state = this.ensureMainTabSwitchState();
1222
+ return state.intent || this.mainTab;
1223
+ },
1224
+ isMainTabNavActive(tab) {
1225
+ return this.getMainTabForNav() === tab;
1226
+ },
1227
+ isConfigModeNavActive(mode) {
1228
+ return this.isMainTabNavActive('config') && this.configMode === mode;
1229
+ },
817
1230
  switchMainTab(tab) {
818
- this.mainTab = tab;
819
- if (tab === 'sessions' && this.sessionsList.length === 0) {
820
- this.loadSessions();
1231
+ const normalizedTab = typeof tab === 'string'
1232
+ ? tab.trim().toLowerCase()
1233
+ : '';
1234
+ const targetTab = normalizedTab || tab;
1235
+ if (!targetTab) return;
1236
+ if (targetTab === 'sessions') {
1237
+ this.cancelScheduledSessionTabDeferredTeardown();
821
1238
  }
822
- if (tab === 'config' && this.configMode === 'claude') {
823
- this.refreshClaudeModelContext();
1239
+
1240
+ this.setMainTabSwitchIntent(targetTab);
1241
+ if (targetTab === 'config') {
1242
+ this.applyImmediateNavIntent('config', this.configMode);
1243
+ } else {
1244
+ this.applyImmediateNavIntent(targetTab);
1245
+ }
1246
+
1247
+ const previousTab = this.mainTab;
1248
+ const switchState = this.ensureMainTabSwitchState();
1249
+ if (targetTab === previousTab) {
1250
+ switchState.ticket += 1;
1251
+ switchState.pendingTarget = '';
1252
+ this.scheduleAfterFrame(() => {
1253
+ this.clearMainTabSwitchIntent(normalizedTab);
1254
+ });
1255
+ return;
1256
+ }
1257
+ const isLeavingSessions = previousTab === 'sessions' && targetTab !== 'sessions';
1258
+ const shouldDeferApply = isLeavingSessions;
1259
+ if (isLeavingSessions && !this.isSessionPanelFastHidden()) {
1260
+ this.setSessionPanelFastHidden(true);
1261
+ }
1262
+ if (!shouldDeferApply) {
1263
+ switchState.ticket += 1;
1264
+ switchState.pendingTarget = '';
1265
+ const result = switchMainTabHelper.call(this, targetTab);
1266
+ this.scheduleAfterFrame(() => {
1267
+ this.clearMainTabSwitchIntent(normalizedTab);
1268
+ });
1269
+ return result;
1270
+ }
1271
+
1272
+ const ticket = ++switchState.ticket;
1273
+ switchState.pendingTarget = targetTab;
1274
+ this.scheduleAfterFrame(() => {
1275
+ const liveState = this.ensureMainTabSwitchState();
1276
+ if (ticket !== liveState.ticket) return;
1277
+ const pendingTarget = liveState.pendingTarget || targetTab;
1278
+ liveState.pendingTarget = '';
1279
+ switchMainTabHelper.call(this, pendingTarget);
1280
+ this.clearMainTabSwitchIntent(normalizedTab);
1281
+ });
1282
+ },
1283
+
1284
+ scheduleAfterFrame(task) {
1285
+ const callback = typeof task === 'function' ? task : () => {};
1286
+ if (typeof requestAnimationFrame === 'function') {
1287
+ requestAnimationFrame(callback);
1288
+ return;
1289
+ }
1290
+ setTimeout(callback, 16);
1291
+ },
1292
+ scheduleIdleTask(task, timeoutMs = 160) {
1293
+ const callback = typeof task === 'function' ? task : () => {};
1294
+ const timeout = Number.isFinite(timeoutMs)
1295
+ ? Math.max(16, Math.floor(timeoutMs))
1296
+ : 160;
1297
+ if (typeof requestIdleCallback === 'function') {
1298
+ const id = requestIdleCallback(callback, { timeout });
1299
+ return {
1300
+ type: 'idle',
1301
+ id
1302
+ };
1303
+ }
1304
+ const id = setTimeout(callback, timeout);
1305
+ return {
1306
+ type: 'timeout',
1307
+ id
1308
+ };
1309
+ },
1310
+ cancelIdleTask(handle) {
1311
+ if (!handle || typeof handle !== 'object') return;
1312
+ const type = handle.type;
1313
+ const id = handle.id;
1314
+ if (type === 'idle') {
1315
+ if (typeof cancelIdleCallback === 'function') {
1316
+ cancelIdleCallback(id);
1317
+ } else {
1318
+ clearTimeout(id);
1319
+ }
1320
+ return;
1321
+ }
1322
+ if (type === 'timeout') {
1323
+ clearTimeout(id);
1324
+ }
1325
+ },
1326
+ scheduleSessionTabDeferredTeardown(task) {
1327
+ const callback = typeof task === 'function' ? task : () => {};
1328
+ this.cancelScheduledSessionTabDeferredTeardown();
1329
+ this.__sessionTabDeferredTeardownHandle = this.scheduleIdleTask(() => {
1330
+ this.__sessionTabDeferredTeardownHandle = null;
1331
+ callback();
1332
+ }, 180);
1333
+ },
1334
+ cancelScheduledSessionTabDeferredTeardown() {
1335
+ const handle = this.__sessionTabDeferredTeardownHandle || null;
1336
+ if (!handle) return;
1337
+ this.cancelIdleTask(handle);
1338
+ this.__sessionTabDeferredTeardownHandle = null;
1339
+ },
1340
+
1341
+ resetSessionPreviewMessageRender() {
1342
+ this.sessionPreviewVisibleCount = 0;
1343
+ this.invalidateSessionTimelineMeasurementCache();
1344
+ },
1345
+
1346
+ resetSessionDetailPagination() {
1347
+ const initialLimit = Number.isFinite(this.sessionDetailInitialMessageLimit)
1348
+ ? Math.max(1, Math.floor(this.sessionDetailInitialMessageLimit))
1349
+ : 80;
1350
+ this.sessionDetailMessageLimit = initialLimit;
1351
+ this.sessionPreviewPendingVisibleCount = 0;
1352
+ },
1353
+
1354
+ primeSessionPreviewMessageRender() {
1355
+ this.sessionPreviewVisibleCount = 0;
1356
+ this.invalidateSessionTimelineMeasurementCache();
1357
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) {
1358
+ return;
824
1359
  }
1360
+ const total = Array.isArray(this.activeSessionMessages)
1361
+ ? this.activeSessionMessages.length
1362
+ : 0;
1363
+ if (total <= 0) return;
1364
+ const baseSize = Number.isFinite(this.sessionPreviewInitialBatchSize)
1365
+ ? Math.max(1, Math.floor(this.sessionPreviewInitialBatchSize))
1366
+ : 40;
1367
+ this.sessionPreviewVisibleCount = Math.min(baseSize, total);
1368
+ this.invalidateSessionTimelineMeasurementCache();
1369
+ },
1370
+
1371
+ async loadMoreSessionMessages(stepSize) {
1372
+ return loadMoreSessionMessagesHelper.call(this, stepSize);
1373
+ },
1374
+
1375
+ suspendSessionTabRender() {
1376
+ this.sessionTabRenderTicket += 1;
1377
+ this.sessionListRenderEnabled = false;
1378
+ this.sessionPreviewRenderEnabled = false;
1379
+ this.cancelSessionTimelineSync();
1380
+ this.sessionTimelineActiveKey = '';
1381
+ this.sessionTimelineLastSyncAt = 0;
1382
+ this.sessionTimelineLastScrollTop = 0;
1383
+ this.sessionTimelineLastAnchorY = 0;
1384
+ this.sessionTimelineLastDirection = 0;
1385
+ this.sessionPreviewScrollEl = null;
1386
+ this.sessionPreviewContainerEl = null;
1387
+ this.sessionPreviewHeaderEl = null;
1388
+ },
1389
+
1390
+ finalizeSessionTabTeardown() {
1391
+ this.resetSessionPreviewMessageRender();
1392
+ this.sessionPreviewPendingVisibleCount = 0;
1393
+ this.clearSessionTimelineRefs();
1394
+ },
1395
+
1396
+ teardownSessionTabRender() {
1397
+ this.suspendSessionTabRender();
1398
+ this.finalizeSessionTabTeardown();
1399
+ },
1400
+
1401
+ prepareSessionTabRender() {
1402
+ const ticket = ++this.sessionTabRenderTicket;
1403
+ this.sessionListRenderEnabled = false;
1404
+ this.sessionPreviewRenderEnabled = false;
1405
+ this.resetSessionPreviewMessageRender();
1406
+
1407
+ this.scheduleAfterFrame(() => {
1408
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1409
+ return;
1410
+ }
1411
+ this.sessionListRenderEnabled = true;
1412
+
1413
+ this.scheduleAfterFrame(() => {
1414
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1415
+ return;
1416
+ }
1417
+ this.sessionPreviewRenderEnabled = true;
1418
+ this.$nextTick(() => {
1419
+ if (ticket !== this.sessionTabRenderTicket || this.mainTab !== 'sessions') {
1420
+ return;
1421
+ }
1422
+ this.primeSessionPreviewMessageRender();
1423
+ this.updateSessionTimelineOffset();
1424
+ this.scheduleSessionTimelineSync();
1425
+ });
1426
+ });
1427
+ });
825
1428
  },
826
1429
 
827
1430
  getSessionStandaloneContext() {
@@ -868,6 +1471,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
868
1471
 
869
1472
  this.sessionStandalone = true;
870
1473
  this.mainTab = 'sessions';
1474
+ this.prepareSessionTabRender();
871
1475
 
872
1476
  if (context.error || !context.params) {
873
1477
  this.sessionStandaloneError = `会话链接参数不完整:${context.error || '参数解析失败'}`;
@@ -887,7 +1491,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
887
1491
  this.activeSessionDetailClipped = false;
888
1492
  this.cancelSessionTimelineSync();
889
1493
  this.sessionTimelineActiveKey = '';
890
- this.sessionMessageRefMap = Object.create(null);
1494
+ this.clearSessionTimelineRefs();
891
1495
  this.sessionStandaloneError = '';
892
1496
  this.sessionStandaloneText = '';
893
1497
  this.sessionStandaloneTitle = this.activeSession.title || '会话';
@@ -1222,38 +1826,466 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1222
1826
  }
1223
1827
  }
1224
1828
  } catch (e) {
1225
- this.showMessage('克隆失败', 'error');
1829
+ this.showMessage('克隆失败', 'error');
1830
+ } finally {
1831
+ this.sessionCloning[key] = false;
1832
+ }
1833
+ },
1834
+
1835
+ async deleteSession(session) {
1836
+ if (!this.isDeleteAvailable(session)) {
1837
+ this.showMessage('不支持此操作', 'error');
1838
+ return;
1839
+ }
1840
+ const key = this.getSessionExportKey(session);
1841
+ if (this.sessionDeleting[key]) {
1842
+ return;
1843
+ }
1844
+ this.sessionDeleting[key] = true;
1845
+ try {
1846
+ const res = await api('trash-session', {
1847
+ source: session.source,
1848
+ sessionId: session.sessionId,
1849
+ filePath: session.filePath
1850
+ });
1851
+ if (res.error) {
1852
+ this.showMessage(res.error, 'error');
1853
+ return;
1854
+ }
1855
+ this.removeSessionPin(session);
1856
+ this.invalidateSessionTrashRequests();
1857
+ this.showMessage('已移入回收站', 'success');
1858
+ if (this.sessionTrashLoadedOnce) {
1859
+ this.prependSessionTrashItem(this.buildSessionTrashItemFromSession(session, res), {
1860
+ totalCount: res && res.totalCount !== undefined ? res.totalCount : undefined
1861
+ });
1862
+ } else if (this.sessionTrashCountLoadedOnce) {
1863
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1864
+ res && res.totalCount !== undefined
1865
+ ? res.totalCount
1866
+ : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
1867
+ this.sessionTrashItems
1868
+ );
1869
+ } else {
1870
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1871
+ res && res.totalCount !== undefined
1872
+ ? res.totalCount
1873
+ : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
1874
+ this.sessionTrashItems
1875
+ );
1876
+ }
1877
+ await this.removeSessionFromCurrentList(session);
1878
+ } catch (e) {
1879
+ this.showMessage('删除失败', 'error');
1880
+ } finally {
1881
+ this.sessionDeleting[key] = false;
1882
+ }
1883
+ },
1884
+
1885
+ buildSessionTrashItemFromSession(session, result = {}) {
1886
+ const deletedAt = typeof result.deletedAt === 'string' && result.deletedAt
1887
+ ? result.deletedAt
1888
+ : new Date().toISOString();
1889
+ const source = session && session.source === 'claude' ? 'claude' : 'codex';
1890
+ return {
1891
+ trashId: typeof result.trashId === 'string' ? result.trashId : '',
1892
+ source,
1893
+ sourceLabel: session && typeof session.sourceLabel === 'string' && session.sourceLabel
1894
+ ? session.sourceLabel
1895
+ : (source === 'claude' ? 'Claude Code' : 'Codex'),
1896
+ sessionId: session && typeof session.sessionId === 'string' ? session.sessionId : '',
1897
+ title: session && typeof session.title === 'string' && session.title
1898
+ ? session.title
1899
+ : (session && typeof session.sessionId === 'string' ? session.sessionId : ''),
1900
+ cwd: session && typeof session.cwd === 'string' ? session.cwd : '',
1901
+ createdAt: session && typeof session.createdAt === 'string' ? session.createdAt : '',
1902
+ updatedAt: session && typeof session.updatedAt === 'string' ? session.updatedAt : '',
1903
+ deletedAt,
1904
+ messageCount: Number.isFinite(Number(result && result.messageCount))
1905
+ ? Math.max(0, Math.floor(Number(result.messageCount)))
1906
+ : (Number.isFinite(Number(session && session.messageCount))
1907
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
1908
+ : 0),
1909
+ originalFilePath: session && typeof session.filePath === 'string' ? session.filePath : '',
1910
+ provider: session && typeof session.provider === 'string' ? session.provider : source,
1911
+ keywords: Array.isArray(session && session.keywords) ? session.keywords : [],
1912
+ capabilities: session && typeof session.capabilities === 'object' && session.capabilities
1913
+ ? session.capabilities
1914
+ : {},
1915
+ claudeIndexPath: '',
1916
+ claudeIndexEntry: null,
1917
+ trashFilePath: ''
1918
+ };
1919
+ },
1920
+
1921
+ prependSessionTrashItem(item, options = {}) {
1922
+ if (!item || !item.trashId) {
1923
+ return;
1924
+ }
1925
+ const existing = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
1926
+ const filtered = existing.filter((entry) => this.getSessionTrashActionKey(entry) !== item.trashId);
1927
+ const nextItems = [item, ...filtered].slice(0, SESSION_TRASH_LIST_LIMIT);
1928
+ const previousTotalCount = Number(this.sessionTrashTotalCount);
1929
+ const normalizedPreviousTotal = Number.isFinite(previousTotalCount) && previousTotalCount >= 0
1930
+ ? Math.max(existing.length, Math.floor(previousTotalCount))
1931
+ : existing.length;
1932
+ this.sessionTrashItems = nextItems;
1933
+ const previousVisibleCount = Number(this.sessionTrashVisibleCount);
1934
+ const normalizedPreviousVisibleCount = Number.isFinite(previousVisibleCount) && previousVisibleCount > 0
1935
+ ? Math.floor(previousVisibleCount)
1936
+ : SESSION_TRASH_PAGE_SIZE;
1937
+ const wasFullyExpanded = normalizedPreviousVisibleCount >= existing.length
1938
+ || normalizedPreviousVisibleCount >= normalizedPreviousTotal;
1939
+ if (wasFullyExpanded) {
1940
+ this.sessionTrashVisibleCount = Math.min(
1941
+ normalizedPreviousVisibleCount + 1,
1942
+ nextItems.length || (normalizedPreviousVisibleCount + 1)
1943
+ );
1944
+ }
1945
+ const fallbackTotalCount = filtered.length === existing.length
1946
+ ? normalizedPreviousTotal + 1
1947
+ : normalizedPreviousTotal;
1948
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
1949
+ options && options.totalCount !== undefined
1950
+ ? options.totalCount
1951
+ : fallbackTotalCount,
1952
+ nextItems
1953
+ );
1954
+ },
1955
+
1956
+ normalizeSessionTrashTotalCount(totalCount, fallbackItems = this.sessionTrashItems) {
1957
+ const fallbackCount = Array.isArray(fallbackItems) ? fallbackItems.length : 0;
1958
+ const numericTotal = Number(totalCount);
1959
+ if (!Number.isFinite(numericTotal) || numericTotal < 0) {
1960
+ return fallbackCount;
1961
+ }
1962
+ return Math.floor(numericTotal);
1963
+ },
1964
+
1965
+ getSessionTrashViewState() {
1966
+ if (this.sessionTrashLoading && !this.sessionTrashLoadedOnce) {
1967
+ return 'loading';
1968
+ }
1969
+ const totalCount = Number(this.sessionTrashCount);
1970
+ const normalizedTotalCount = Number.isFinite(totalCount) && totalCount >= 0
1971
+ ? Math.floor(totalCount)
1972
+ : 0;
1973
+ const hasVisibleItems = Array.isArray(this.sessionTrashItems) && this.sessionTrashItems.length > 0;
1974
+ if (this.sessionTrashLastLoadFailed && (!this.sessionTrashLoadedOnce || !hasVisibleItems)) {
1975
+ return 'retry';
1976
+ }
1977
+ if (!this.sessionTrashLoadedOnce) {
1978
+ return normalizedTotalCount > 0 ? 'retry' : 'empty';
1979
+ }
1980
+ if (normalizedTotalCount === 0) {
1981
+ return 'empty';
1982
+ }
1983
+ return hasVisibleItems ? 'list' : 'retry';
1984
+ },
1985
+
1986
+ issueSessionTrashCountRequestToken() {
1987
+ const currentToken = Number(this.sessionTrashCountRequestToken);
1988
+ const nextToken = Number.isFinite(currentToken) && currentToken >= 0
1989
+ ? Math.floor(currentToken) + 1
1990
+ : 1;
1991
+ this.sessionTrashCountRequestToken = nextToken;
1992
+ return nextToken;
1993
+ },
1994
+
1995
+ issueSessionTrashListRequestToken() {
1996
+ const currentToken = Number(this.sessionTrashListRequestToken);
1997
+ const nextToken = Number.isFinite(currentToken) && currentToken >= 0
1998
+ ? Math.floor(currentToken) + 1
1999
+ : 1;
2000
+ this.sessionTrashListRequestToken = nextToken;
2001
+ return nextToken;
2002
+ },
2003
+
2004
+ invalidateSessionTrashRequests() {
2005
+ this.issueSessionTrashCountRequestToken();
2006
+ return this.issueSessionTrashListRequestToken();
2007
+ },
2008
+
2009
+ isLatestSessionTrashCountRequestToken(token) {
2010
+ return Number(token) === Number(this.sessionTrashCountRequestToken);
2011
+ },
2012
+
2013
+ isLatestSessionTrashListRequestToken(token) {
2014
+ return Number(token) === Number(this.sessionTrashListRequestToken);
2015
+ },
2016
+
2017
+ resetSessionTrashVisibleCount() {
2018
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
2019
+ this.sessionTrashVisibleCount = Math.min(totalItems, SESSION_TRASH_PAGE_SIZE) || SESSION_TRASH_PAGE_SIZE;
2020
+ },
2021
+
2022
+ loadMoreSessionTrashItems() {
2023
+ const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
2024
+ const visibleCount = Number(this.sessionTrashVisibleCount);
2025
+ const safeVisibleCount = Number.isFinite(visibleCount) && visibleCount > 0
2026
+ ? Math.floor(visibleCount)
2027
+ : SESSION_TRASH_PAGE_SIZE;
2028
+ this.sessionTrashVisibleCount = Math.min(totalItems, safeVisibleCount + SESSION_TRASH_PAGE_SIZE);
2029
+ },
2030
+
2031
+ clearActiveSessionState() {
2032
+ this.activeSession = null;
2033
+ this.activeSessionMessages = [];
2034
+ this.resetSessionDetailPagination();
2035
+ this.resetSessionPreviewMessageRender();
2036
+ this.activeSessionDetailError = '';
2037
+ this.activeSessionDetailClipped = false;
2038
+ this.cancelSessionTimelineSync();
2039
+ this.sessionTimelineActiveKey = '';
2040
+ this.clearSessionTimelineRefs();
2041
+ },
2042
+
2043
+ async removeSessionFromCurrentList(session) {
2044
+ const sessionKey = this.getSessionExportKey(session);
2045
+ if (!sessionKey) {
2046
+ return;
2047
+ }
2048
+ const currentList = Array.isArray(this.sessionsList) ? [...this.sessionsList] : [];
2049
+ const removedIndex = currentList.findIndex((item) => this.getSessionExportKey(item) === sessionKey);
2050
+ if (removedIndex < 0) {
2051
+ return;
2052
+ }
2053
+ const activeKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
2054
+ const renderedList = Array.isArray(this.sortedSessionsList) ? this.sortedSessionsList : [];
2055
+ const renderedIndex = renderedList.findIndex((item) => this.getSessionExportKey(item) === sessionKey);
2056
+ let nextActiveKey = '';
2057
+ if (activeKey === sessionKey && renderedIndex >= 0) {
2058
+ const fallbackSession = renderedList[renderedIndex - 1] || renderedList[renderedIndex + 1] || null;
2059
+ nextActiveKey = fallbackSession ? this.getSessionExportKey(fallbackSession) : '';
2060
+ }
2061
+ currentList.splice(removedIndex, 1);
2062
+ this.sessionsList = currentList;
2063
+ this.syncSessionPathOptionsForSource(
2064
+ this.sessionFilterSource,
2065
+ this.extractPathOptionsFromSessions(currentList),
2066
+ false
2067
+ );
2068
+ if (activeKey !== sessionKey) {
2069
+ return;
2070
+ }
2071
+ if (currentList.length === 0) {
2072
+ this.clearActiveSessionState();
2073
+ return;
2074
+ }
2075
+ const nextSession = currentList.find((item) => this.getSessionExportKey(item) === nextActiveKey)
2076
+ || currentList[Math.min(removedIndex, currentList.length - 1)];
2077
+ if (!nextSession) {
2078
+ this.clearActiveSessionState();
2079
+ return;
2080
+ }
2081
+ await this.selectSession(nextSession);
2082
+ },
2083
+
2084
+ normalizeSettingsTab(tab) {
2085
+ return tab === 'trash' ? 'trash' : 'backup';
2086
+ },
2087
+
2088
+ async onSettingsTabClick(tab) {
2089
+ await this.switchSettingsTab(tab);
2090
+ },
2091
+
2092
+ async switchSettingsTab(tab, options = {}) {
2093
+ const nextTab = this.normalizeSettingsTab(tab);
2094
+ this.settingsTab = nextTab;
2095
+ if (nextTab !== 'trash') {
2096
+ return;
2097
+ }
2098
+ const forceRefresh = options.forceRefresh === true;
2099
+ if (forceRefresh || !this.sessionTrashLoadedOnce) {
2100
+ await this.loadSessionTrash({ forceRefresh });
2101
+ }
2102
+ },
2103
+
2104
+ async loadSessionTrashCount(options = {}) {
2105
+ if (this.sessionTrashCountLoading) {
2106
+ this.sessionTrashCountPendingOptions = {
2107
+ ...(this.sessionTrashCountPendingOptions || {}),
2108
+ ...(options || {})
2109
+ };
2110
+ return;
2111
+ }
2112
+ const requestToken = this.issueSessionTrashCountRequestToken();
2113
+ this.sessionTrashCountLoading = true;
2114
+ try {
2115
+ const res = await api('list-session-trash', { countOnly: true });
2116
+ if (!this.isLatestSessionTrashCountRequestToken(requestToken)) {
2117
+ return;
2118
+ }
2119
+ if (res.error) {
2120
+ if (options.silent !== true) {
2121
+ this.showMessage(res.error, 'error');
2122
+ }
2123
+ return;
2124
+ }
2125
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
2126
+ res.totalCount,
2127
+ this.sessionTrashItems
2128
+ );
2129
+ this.sessionTrashCountLoadedOnce = true;
2130
+ } catch (e) {
2131
+ if (this.isLatestSessionTrashCountRequestToken(requestToken) && options.silent !== true) {
2132
+ this.showMessage('加载回收站数量失败', 'error');
2133
+ }
2134
+ } finally {
2135
+ this.sessionTrashCountLoading = false;
2136
+ const pendingOptions = this.sessionTrashCountPendingOptions;
2137
+ this.sessionTrashCountPendingOptions = null;
2138
+ if (pendingOptions) {
2139
+ await this.loadSessionTrashCount(pendingOptions);
2140
+ }
2141
+ }
2142
+ },
2143
+
2144
+ getSessionTrashActionKey(item) {
2145
+ return item && typeof item.trashId === 'string' ? item.trashId : '';
2146
+ },
2147
+
2148
+ isSessionTrashActionBusy(item) {
2149
+ const key = typeof item === 'string' ? item : this.getSessionTrashActionKey(item);
2150
+ return !!(key && (this.sessionTrashRestoring[key] || this.sessionTrashPurging[key]));
2151
+ },
2152
+
2153
+ async loadSessionTrash(options = {}) {
2154
+ if (this.sessionTrashLoading) {
2155
+ this.sessionTrashPendingOptions = {
2156
+ ...(this.sessionTrashPendingOptions || {}),
2157
+ ...(options || {})
2158
+ };
2159
+ return;
2160
+ }
2161
+ const requestToken = this.issueSessionTrashListRequestToken();
2162
+ this.sessionTrashLoading = true;
2163
+ this.sessionTrashLastLoadFailed = false;
2164
+ let loadSucceeded = false;
2165
+ try {
2166
+ const res = await api('list-session-trash', {
2167
+ limit: SESSION_TRASH_LIST_LIMIT,
2168
+ forceRefresh: !!options.forceRefresh
2169
+ });
2170
+ if (!this.isLatestSessionTrashListRequestToken(requestToken)) {
2171
+ return;
2172
+ }
2173
+ if (res.error) {
2174
+ this.sessionTrashLastLoadFailed = true;
2175
+ this.showMessage(res.error, 'error');
2176
+ return;
2177
+ }
2178
+ const nextItems = Array.isArray(res.items) ? res.items : [];
2179
+ this.sessionTrashItems = nextItems;
2180
+ this.resetSessionTrashVisibleCount();
2181
+ this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(res.totalCount, nextItems);
2182
+ this.sessionTrashCountLoadedOnce = true;
2183
+ this.sessionTrashLastLoadFailed = false;
2184
+ loadSucceeded = true;
2185
+ } catch (e) {
2186
+ if (this.isLatestSessionTrashListRequestToken(requestToken)) {
2187
+ this.sessionTrashLastLoadFailed = true;
2188
+ this.showMessage('加载回收站失败', 'error');
2189
+ }
2190
+ } finally {
2191
+ this.sessionTrashLoading = false;
2192
+ if (loadSucceeded) {
2193
+ this.sessionTrashLoadedOnce = true;
2194
+ }
2195
+ const pendingOptions = this.sessionTrashPendingOptions;
2196
+ this.sessionTrashPendingOptions = null;
2197
+ if (pendingOptions) {
2198
+ await this.loadSessionTrash(pendingOptions);
2199
+ }
2200
+ }
2201
+ },
2202
+
2203
+ async restoreSessionTrash(item) {
2204
+ const key = this.getSessionTrashActionKey(item);
2205
+ if (!key || this.isSessionTrashActionBusy(key) || this.sessionTrashClearing) {
2206
+ return;
2207
+ }
2208
+ this.sessionTrashRestoring[key] = true;
2209
+ try {
2210
+ const res = await api('restore-session-trash', { trashId: key });
2211
+ if (res.error) {
2212
+ this.showMessage(res.error, 'error');
2213
+ return;
2214
+ }
2215
+ this.showMessage('会话已恢复', 'success');
2216
+ this.invalidateSessionTrashRequests();
2217
+ await this.loadSessionTrash({ forceRefresh: true });
2218
+ if (this.sessionsLoadedOnce) {
2219
+ await this.loadSessions();
2220
+ }
2221
+ } catch (e) {
2222
+ this.showMessage('恢复失败', 'error');
2223
+ } finally {
2224
+ this.sessionTrashRestoring[key] = false;
2225
+ }
2226
+ },
2227
+
2228
+ async purgeSessionTrash(item) {
2229
+ const key = this.getSessionTrashActionKey(item);
2230
+ if (!key || this.isSessionTrashActionBusy(key) || this.sessionTrashClearing) {
2231
+ return;
2232
+ }
2233
+ const confirmed = await this.requestConfirmDialog({
2234
+ title: '彻底删除回收站记录',
2235
+ message: '该会话将从回收站永久删除,且无法恢复。',
2236
+ confirmText: '彻底删除',
2237
+ cancelText: '取消',
2238
+ danger: true
2239
+ });
2240
+ if (!confirmed) {
2241
+ return;
2242
+ }
2243
+ this.sessionTrashPurging[key] = true;
2244
+ try {
2245
+ const res = await api('purge-session-trash', { trashId: key });
2246
+ if (res.error) {
2247
+ this.showMessage(res.error, 'error');
2248
+ return;
2249
+ }
2250
+ this.showMessage('已彻底删除', 'success');
2251
+ this.invalidateSessionTrashRequests();
2252
+ await this.loadSessionTrash({ forceRefresh: true });
2253
+ } catch (e) {
2254
+ this.showMessage('彻底删除失败', 'error');
1226
2255
  } finally {
1227
- this.sessionCloning[key] = false;
2256
+ this.sessionTrashPurging[key] = false;
1228
2257
  }
1229
2258
  },
1230
2259
 
1231
- async deleteSession(session) {
1232
- if (!this.isDeleteAvailable(session)) {
1233
- this.showMessage('不支持此操作', 'error');
2260
+ async clearSessionTrash() {
2261
+ const normalizedCount = Number(this.sessionTrashCount);
2262
+ if (this.sessionTrashClearing || !Number.isFinite(normalizedCount) || normalizedCount <= 0) {
1234
2263
  return;
1235
2264
  }
1236
- const key = this.getSessionExportKey(session);
1237
- if (this.sessionDeleting[key]) {
2265
+ const confirmed = await this.requestConfirmDialog({
2266
+ title: '清空回收站',
2267
+ message: '该操作会永久删除回收站中的全部会话,且无法恢复。',
2268
+ confirmText: '全部清空',
2269
+ cancelText: '取消',
2270
+ danger: true
2271
+ });
2272
+ if (!confirmed) {
1238
2273
  return;
1239
2274
  }
1240
- this.sessionDeleting[key] = true;
2275
+ this.sessionTrashClearing = true;
1241
2276
  try {
1242
- const res = await api('delete-session', {
1243
- source: session.source,
1244
- sessionId: session.sessionId,
1245
- filePath: session.filePath
1246
- });
2277
+ const res = await api('purge-session-trash', { all: true });
1247
2278
  if (res.error) {
1248
2279
  this.showMessage(res.error, 'error');
1249
2280
  return;
1250
2281
  }
1251
- this.showMessage('操作成功', 'success');
1252
- await this.loadSessions();
2282
+ this.showMessage('回收站已清空', 'success');
2283
+ this.invalidateSessionTrashRequests();
2284
+ await this.loadSessionTrash({ forceRefresh: true });
1253
2285
  } catch (e) {
1254
- this.showMessage('删除失败', 'error');
2286
+ this.showMessage('清空回收站失败', 'error');
1255
2287
  } finally {
1256
- this.sessionDeleting[key] = false;
2288
+ this.sessionTrashClearing = false;
1257
2289
  }
1258
2290
  },
1259
2291
 
@@ -1384,6 +2416,124 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1384
2416
  localStorage.removeItem('codexmateSessionPathFilter');
1385
2417
  }
1386
2418
  },
2419
+ normalizeSessionPinnedMap(raw) {
2420
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
2421
+ return {};
2422
+ }
2423
+ const next = {};
2424
+ for (const [key, value] of Object.entries(raw)) {
2425
+ if (!key) continue;
2426
+ const numeric = Number(value);
2427
+ if (!Number.isFinite(numeric) || numeric <= 0) continue;
2428
+ next[key] = Math.floor(numeric);
2429
+ }
2430
+ return next;
2431
+ },
2432
+ restoreSessionPinnedMap() {
2433
+ const cached = localStorage.getItem('codexmateSessionPinnedMap');
2434
+ if (!cached) {
2435
+ this.sessionPinnedMap = {};
2436
+ return;
2437
+ }
2438
+ try {
2439
+ const parsed = JSON.parse(cached);
2440
+ this.sessionPinnedMap = this.normalizeSessionPinnedMap(parsed);
2441
+ } catch (_) {
2442
+ this.sessionPinnedMap = {};
2443
+ localStorage.removeItem('codexmateSessionPinnedMap');
2444
+ }
2445
+ },
2446
+ persistSessionPinnedMap() {
2447
+ const payload = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2448
+ ? this.sessionPinnedMap
2449
+ : {};
2450
+ localStorage.setItem('codexmateSessionPinnedMap', JSON.stringify(payload));
2451
+ },
2452
+ shouldPruneSessionPinnedMap(sessions = this.sessionsList) {
2453
+ if (!Array.isArray(sessions) || sessions.length === 0) {
2454
+ return false;
2455
+ }
2456
+ if (this.sessionFilterSource !== 'all') {
2457
+ return false;
2458
+ }
2459
+ if (this.sessionPathFilter) {
2460
+ return false;
2461
+ }
2462
+ if (this.sessionQuery && isSessionQueryEnabled(this.sessionFilterSource)) {
2463
+ return false;
2464
+ }
2465
+ if (this.sessionRoleFilter && this.sessionRoleFilter !== 'all') {
2466
+ return false;
2467
+ }
2468
+ if (this.sessionTimePreset && this.sessionTimePreset !== 'all') {
2469
+ return false;
2470
+ }
2471
+ return true;
2472
+ },
2473
+ pruneSessionPinnedMap(sessions = this.sessionsList) {
2474
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2475
+ ? this.sessionPinnedMap
2476
+ : {};
2477
+ const list = Array.isArray(sessions) ? sessions : [];
2478
+ if (Object.keys(current).length === 0 || !this.shouldPruneSessionPinnedMap(list)) {
2479
+ return;
2480
+ }
2481
+ const validKeys = new Set(list.map((session) => this.getSessionExportKey(session)).filter(Boolean));
2482
+ const next = {};
2483
+ let changed = false;
2484
+ for (const [key, value] of Object.entries(current)) {
2485
+ if (!validKeys.has(key)) {
2486
+ changed = true;
2487
+ continue;
2488
+ }
2489
+ next[key] = value;
2490
+ }
2491
+ if (!changed) {
2492
+ return;
2493
+ }
2494
+ this.sessionPinnedMap = next;
2495
+ this.persistSessionPinnedMap();
2496
+ },
2497
+ getSessionPinTimestamp(session) {
2498
+ if (!session) return 0;
2499
+ const key = this.getSessionExportKey(session);
2500
+ if (!key) return 0;
2501
+ const raw = this.sessionPinnedMap && this.sessionPinnedMap[key];
2502
+ const numeric = Number(raw);
2503
+ return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0;
2504
+ },
2505
+ isSessionPinned(session) {
2506
+ return this.getSessionPinTimestamp(session) > 0;
2507
+ },
2508
+ toggleSessionPin(session) {
2509
+ if (!session) return;
2510
+ const key = this.getSessionExportKey(session);
2511
+ if (!key) return;
2512
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2513
+ ? this.sessionPinnedMap
2514
+ : {};
2515
+ const next = { ...current };
2516
+ if (next[key]) {
2517
+ delete next[key];
2518
+ } else {
2519
+ next[key] = Date.now();
2520
+ }
2521
+ this.sessionPinnedMap = next;
2522
+ this.persistSessionPinnedMap();
2523
+ },
2524
+ removeSessionPin(session) {
2525
+ if (!session) return;
2526
+ const key = this.getSessionExportKey(session);
2527
+ if (!key) return;
2528
+ const current = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
2529
+ ? this.sessionPinnedMap
2530
+ : {};
2531
+ if (!current[key]) return;
2532
+ const next = { ...current };
2533
+ delete next[key];
2534
+ this.sessionPinnedMap = next;
2535
+ this.persistSessionPinnedMap();
2536
+ },
1387
2537
 
1388
2538
  async onSessionSourceChange() {
1389
2539
  this.refreshSessionPathOptions(this.sessionFilterSource);
@@ -1434,13 +2584,137 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1434
2584
  },
1435
2585
  setSessionPreviewScrollRef(el) {
1436
2586
  this.sessionPreviewScrollEl = el || null;
1437
- if (this.sessionPreviewScrollEl) {
1438
- this.scheduleSessionTimelineSync();
1439
- } else {
2587
+ this.invalidateSessionTimelineMeasurementCache();
2588
+ const shouldSync = !!(
2589
+ this.sessionPreviewScrollEl
2590
+ && this.mainTab === 'sessions'
2591
+ && this.getMainTabForNav() === 'sessions'
2592
+ && this.sessionPreviewRenderEnabled
2593
+ && this.sessionTimelineNodes.length
2594
+ );
2595
+ if (!shouldSync) {
1440
2596
  this.cancelSessionTimelineSync();
2597
+ this.updateSessionTimelineOffset();
2598
+ return;
1441
2599
  }
2600
+ const boundScrollEl = this.sessionPreviewScrollEl;
2601
+ this.$nextTick(() => {
2602
+ if (this.sessionPreviewScrollEl !== boundScrollEl) return;
2603
+ if (this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) return;
2604
+ if (!this.sessionTimelineNodes.length) return;
2605
+ this.scheduleSessionTimelineSync();
2606
+ });
1442
2607
  this.updateSessionTimelineOffset();
1443
2608
  },
2609
+ clearSessionTimelineRefs() {
2610
+ this.sessionMessageRefMap = Object.create(null);
2611
+ this.sessionMessageRefBinderMap = Object.create(null);
2612
+ this.sessionTimelineLastAnchorY = 0;
2613
+ this.sessionTimelineLastDirection = 0;
2614
+ this.invalidateSessionTimelineMeasurementCache(true);
2615
+ },
2616
+ ensureSessionTimelineMeasurementCache() {
2617
+ if (this.__sessionTimelineMeasurementCache) {
2618
+ return this.__sessionTimelineMeasurementCache;
2619
+ }
2620
+ this.__sessionTimelineMeasurementCache = {
2621
+ offsetByKey: Object.create(null),
2622
+ dirty: true
2623
+ };
2624
+ return this.__sessionTimelineMeasurementCache;
2625
+ },
2626
+ invalidateSessionTimelineMeasurementCache(resetOffset = false) {
2627
+ const cache = this.ensureSessionTimelineMeasurementCache();
2628
+ if (resetOffset) {
2629
+ cache.offsetByKey = Object.create(null);
2630
+ }
2631
+ cache.dirty = true;
2632
+ },
2633
+ refreshSessionTimelineMeasurementCache(nodes = null) {
2634
+ const cache = this.ensureSessionTimelineMeasurementCache();
2635
+ const nodeList = Array.isArray(nodes) ? nodes : (Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : []);
2636
+ if (!nodeList.length) {
2637
+ cache.offsetByKey = Object.create(null);
2638
+ cache.dirty = false;
2639
+ return cache.offsetByKey;
2640
+ }
2641
+ const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
2642
+ const scrollRect = scrollEl && typeof scrollEl.getBoundingClientRect === 'function'
2643
+ ? scrollEl.getBoundingClientRect()
2644
+ : null;
2645
+ const scrollTop = scrollEl ? Number(scrollEl.scrollTop || 0) : 0;
2646
+ const nextOffsetByKey = Object.create(null);
2647
+ for (const node of nodeList) {
2648
+ if (!node || !node.key) continue;
2649
+ const messageEl = this.sessionMessageRefMap[node.key];
2650
+ if (!messageEl) continue;
2651
+ let top = Number.NaN;
2652
+ if (
2653
+ scrollRect
2654
+ && typeof messageEl.getBoundingClientRect === 'function'
2655
+ ) {
2656
+ const messageRect = messageEl.getBoundingClientRect();
2657
+ top = scrollTop + (messageRect.top - scrollRect.top);
2658
+ } else {
2659
+ top = Number(messageEl.offsetTop || 0);
2660
+ }
2661
+ if (!Number.isFinite(top)) continue;
2662
+ nextOffsetByKey[node.key] = top;
2663
+ }
2664
+ cache.offsetByKey = nextOffsetByKey;
2665
+ cache.dirty = false;
2666
+ return cache.offsetByKey;
2667
+ },
2668
+ getCachedSessionTimelineMeasuredNodes(nodes) {
2669
+ const nodeList = Array.isArray(nodes) ? nodes : [];
2670
+ if (!nodeList.length) {
2671
+ return [];
2672
+ }
2673
+ const cache = this.ensureSessionTimelineMeasurementCache();
2674
+ if (cache.dirty) {
2675
+ this.refreshSessionTimelineMeasurementCache(nodeList);
2676
+ }
2677
+ const offsetByKey = cache.offsetByKey || Object.create(null);
2678
+ const measuredNodes = [];
2679
+ for (const node of nodeList) {
2680
+ if (!node || !node.key) continue;
2681
+ const top = Number(offsetByKey[node.key]);
2682
+ if (!Number.isFinite(top)) continue;
2683
+ measuredNodes.push({
2684
+ key: node.key,
2685
+ top
2686
+ });
2687
+ }
2688
+ if (measuredNodes.length >= nodeList.length) {
2689
+ return measuredNodes;
2690
+ }
2691
+ const refreshedOffsetByKey = this.refreshSessionTimelineMeasurementCache(nodeList);
2692
+ const refreshedNodes = [];
2693
+ for (const node of nodeList) {
2694
+ if (!node || !node.key) continue;
2695
+ const top = Number(refreshedOffsetByKey[node.key]);
2696
+ if (!Number.isFinite(top)) continue;
2697
+ refreshedNodes.push({
2698
+ key: node.key,
2699
+ top
2700
+ });
2701
+ }
2702
+ return refreshedNodes;
2703
+ },
2704
+ getSessionMessageRefBinder(messageKey) {
2705
+ if (!this.isSessionTimelineNodeKey(messageKey)) return null;
2706
+ const current = this.sessionMessageRefBinderMap[messageKey];
2707
+ if (!current || current.ticket !== this.sessionTabRenderTicket) {
2708
+ const ticket = this.sessionTabRenderTicket;
2709
+ this.sessionMessageRefBinderMap[messageKey] = {
2710
+ ticket,
2711
+ bind: (el) => {
2712
+ this.bindSessionMessageRef(messageKey, el, ticket);
2713
+ }
2714
+ };
2715
+ }
2716
+ return this.sessionMessageRefBinderMap[messageKey].bind;
2717
+ },
1444
2718
  updateSessionTimelineOffset() {
1445
2719
  const container = this.sessionPreviewContainerEl || this.$refs.sessionPreviewContainer;
1446
2720
  if (!container || !container.style) return;
@@ -1451,12 +2725,39 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1451
2725
  const offset = headerHeight > 0 ? (headerHeight + 12) : 72;
1452
2726
  container.style.setProperty('--session-preview-header-offset', `${offset}px`);
1453
2727
  },
1454
- bindSessionMessageRef(messageKey, el) {
2728
+ bindSessionMessageRef(messageKey, el, ticket = this.sessionTabRenderTicket) {
2729
+ if (!this.sessionTimelineEnabled) return;
1455
2730
  if (!messageKey) return;
2731
+ if (ticket !== this.sessionTabRenderTicket) return;
1456
2732
  if (el) {
2733
+ if (!this.isSessionTimelineNodeKey(messageKey)) return;
2734
+ if (this.sessionMessageRefMap[messageKey] === el) return;
1457
2735
  this.sessionMessageRefMap[messageKey] = el;
2736
+ this.invalidateSessionTimelineMeasurementCache();
1458
2737
  } else {
2738
+ if (!this.sessionMessageRefMap[messageKey]) return;
1459
2739
  delete this.sessionMessageRefMap[messageKey];
2740
+ this.invalidateSessionTimelineMeasurementCache();
2741
+ }
2742
+ },
2743
+ isSessionTimelineNodeKey(messageKey) {
2744
+ if (!messageKey) return false;
2745
+ return !!(this.sessionTimelineNodeKeyMap && this.sessionTimelineNodeKeyMap[messageKey]);
2746
+ },
2747
+ pruneSessionMessageRefs() {
2748
+ const nodeKeyMap = this.sessionTimelineNodeKeyMap || Object.create(null);
2749
+ let removed = false;
2750
+ for (const key of Object.keys(this.sessionMessageRefMap)) {
2751
+ if (nodeKeyMap[key]) continue;
2752
+ delete this.sessionMessageRefMap[key];
2753
+ removed = true;
2754
+ }
2755
+ for (const key of Object.keys(this.sessionMessageRefBinderMap)) {
2756
+ if (nodeKeyMap[key]) continue;
2757
+ delete this.sessionMessageRefBinderMap[key];
2758
+ }
2759
+ if (removed) {
2760
+ this.invalidateSessionTimelineMeasurementCache();
1460
2761
  }
1461
2762
  },
1462
2763
  cancelSessionTimelineSync() {
@@ -1478,11 +2779,39 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1478
2779
  this.syncSessionTimelineActiveFromScroll();
1479
2780
  },
1480
2781
  onSessionPreviewScroll() {
2782
+ if (
2783
+ !this.sessionTimelineEnabled
2784
+ || this.mainTab !== 'sessions'
2785
+ || this.getMainTabForNav() !== 'sessions'
2786
+ || !this.sessionPreviewRenderEnabled
2787
+ ) return;
2788
+ if (!this.sessionTimelineNodes.length) return;
2789
+ const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
2790
+ if (!scrollEl) return;
2791
+ const now = Date.now();
2792
+ const currentTop = Number(scrollEl.scrollTop || 0);
2793
+ const delta = Math.abs(currentTop - Number(this.sessionTimelineLastScrollTop || 0));
2794
+ const elapsed = now - Number(this.sessionTimelineLastSyncAt || 0);
2795
+ if (delta < 48 && elapsed < 120) {
2796
+ return;
2797
+ }
2798
+ this.sessionTimelineLastScrollTop = currentTop;
2799
+ this.sessionTimelineLastSyncAt = now;
1481
2800
  this.scheduleSessionTimelineSync();
1482
2801
  },
1483
2802
  onWindowResize() {
1484
2803
  this.updateCompactLayoutMode();
2804
+ if (
2805
+ !this.sessionTimelineEnabled
2806
+ || this.mainTab !== 'sessions'
2807
+ || this.getMainTabForNav() !== 'sessions'
2808
+ || !this.sessionPreviewRenderEnabled
2809
+ ) {
2810
+ return;
2811
+ }
2812
+ if (!this.sessionTimelineNodes.length) return;
1485
2813
  this.updateSessionTimelineOffset();
2814
+ this.invalidateSessionTimelineMeasurementCache();
1486
2815
  this.scheduleSessionTimelineSync();
1487
2816
  },
1488
2817
  shouldForceCompactLayout() {
@@ -1535,35 +2864,93 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1535
2864
  this.applyCompactLayoutClass(enabled);
1536
2865
  },
1537
2866
  syncSessionTimelineActiveFromScroll() {
2867
+ if (
2868
+ !this.sessionTimelineEnabled
2869
+ || this.mainTab !== 'sessions'
2870
+ || this.getMainTabForNav() !== 'sessions'
2871
+ || !this.sessionPreviewRenderEnabled
2872
+ ) {
2873
+ if (this.sessionTimelineActiveKey) {
2874
+ this.sessionTimelineActiveKey = '';
2875
+ }
2876
+ return;
2877
+ }
1538
2878
  const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
1539
2879
  if (!nodes.length) {
1540
- this.sessionTimelineActiveKey = '';
2880
+ if (this.sessionTimelineActiveKey) {
2881
+ this.sessionTimelineActiveKey = '';
2882
+ }
1541
2883
  return;
1542
2884
  }
2885
+ this.pruneSessionMessageRefs();
1543
2886
  const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
1544
2887
  if (!scrollEl) {
1545
- this.sessionTimelineActiveKey = nodes[0].key;
2888
+ if (!this.isSessionTimelineNodeKey(this.sessionTimelineActiveKey)) {
2889
+ const fallbackKey = nodes[0].key;
2890
+ if (this.sessionTimelineActiveKey !== fallbackKey) {
2891
+ this.sessionTimelineActiveKey = fallbackKey;
2892
+ }
2893
+ }
1546
2894
  return;
1547
2895
  }
1548
- const scrollRect = scrollEl.getBoundingClientRect();
1549
2896
  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;
2897
+ const stickyOffset = headerEl ? (headerEl.offsetHeight + 8) : 8;
2898
+ const rawAnchorY = Number(scrollEl.scrollTop || 0) + stickyOffset;
2899
+ const previousAnchorY = Number(this.sessionTimelineLastAnchorY || 0);
2900
+ let direction = rawAnchorY - previousAnchorY;
2901
+ if (Math.abs(direction) < 1) {
2902
+ direction = Number(this.sessionTimelineLastDirection || 0);
2903
+ } else {
2904
+ this.sessionTimelineLastDirection = direction > 0 ? 1 : -1;
2905
+ }
2906
+ this.sessionTimelineLastAnchorY = rawAnchorY;
2907
+ const hysteresisPx = 18;
2908
+ const hysteresis = direction > 0 ? -hysteresisPx : (direction < 0 ? hysteresisPx : 0);
2909
+ const anchorY = rawAnchorY + hysteresis;
2910
+ const measuredNodes = this.getCachedSessionTimelineMeasuredNodes(nodes);
2911
+ if (!measuredNodes.length) {
2912
+ if (!this.isSessionTimelineNodeKey(this.sessionTimelineActiveKey)) {
2913
+ this.sessionTimelineActiveKey = nodes[0].key;
2914
+ }
2915
+ return;
2916
+ }
2917
+ let low = 0;
2918
+ let high = measuredNodes.length - 1;
2919
+ let candidateIndex = 0;
2920
+ while (low <= high) {
2921
+ const mid = Math.floor((low + high) / 2);
2922
+ if (measuredNodes[mid].top <= anchorY) {
2923
+ candidateIndex = mid;
2924
+ low = mid + 1;
2925
+ } else {
2926
+ high = mid - 1;
2927
+ }
2928
+ }
2929
+ let currentIndex = -1;
2930
+ if (this.sessionTimelineActiveKey) {
2931
+ for (let i = 0; i < measuredNodes.length; i += 1) {
2932
+ if (measuredNodes[i].key === this.sessionTimelineActiveKey) {
2933
+ currentIndex = i;
2934
+ break;
2935
+ }
2936
+ }
2937
+ }
2938
+ if (currentIndex >= 0) {
2939
+ if (direction > 0 && candidateIndex < currentIndex) {
2940
+ candidateIndex = currentIndex;
2941
+ } else if (direction < 0 && candidateIndex > currentIndex) {
2942
+ candidateIndex = currentIndex;
1560
2943
  }
1561
- break;
1562
2944
  }
1563
- this.sessionTimelineActiveKey = activeKey;
2945
+ const activeKey = measuredNodes[candidateIndex].key;
2946
+ if (this.sessionTimelineActiveKey !== activeKey) {
2947
+ this.sessionTimelineActiveKey = activeKey;
2948
+ }
1564
2949
  },
1565
2950
  jumpToSessionTimelineNode(messageKey) {
2951
+ if (!this.sessionTimelineEnabled || this.mainTab !== 'sessions' || !this.sessionPreviewRenderEnabled) return;
1566
2952
  if (!messageKey) return;
2953
+ if (!this.isSessionTimelineNodeKey(messageKey)) return;
1567
2954
  const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
1568
2955
  if (!scrollEl) return;
1569
2956
  const messageEl = this.sessionMessageRefMap[messageKey];
@@ -1636,67 +3023,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1636
3023
  },
1637
3024
 
1638
3025
  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
- }
3026
+ const result = await loadSessionsHelper.call(this, api);
3027
+ this.pruneSessionPinnedMap(this.sessionsList);
3028
+ return result;
1700
3029
  },
1701
3030
 
1702
3031
  async selectSession(session) {
@@ -1704,11 +3033,13 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1704
3033
  if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
1705
3034
  this.activeSession = session;
1706
3035
  this.activeSessionMessages = [];
3036
+ this.resetSessionDetailPagination();
3037
+ this.resetSessionPreviewMessageRender();
1707
3038
  this.activeSessionDetailError = '';
1708
3039
  this.activeSessionDetailClipped = false;
1709
3040
  this.cancelSessionTimelineSync();
1710
3041
  this.sessionTimelineActiveKey = '';
1711
- this.sessionMessageRefMap = Object.create(null);
3042
+ this.clearSessionTimelineRefs();
1712
3043
  await this.loadActiveSessionDetail();
1713
3044
  },
1714
3045
 
@@ -1757,87 +3088,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
1757
3088
  }
1758
3089
  },
1759
3090
 
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
- }
3091
+ async loadActiveSessionDetail(options = {}) {
3092
+ return loadActiveSessionDetailHelper.call(this, api, options);
1841
3093
  },
1842
3094
 
1843
3095
  downloadTextFile(fileName, content, mimeType = 'text/markdown;charset=utf-8') {
@@ -2142,9 +3394,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2142
3394
  return;
2143
3395
  }
2144
3396
  this.agentsContent = res.content || '';
3397
+ this.agentsOriginalContent = this.agentsContent;
2145
3398
  this.agentsPath = res.path || '';
2146
3399
  this.agentsExists = !!res.exists;
2147
3400
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3401
+ this.resetAgentsDiffState();
2148
3402
  this.showAgentsModal = true;
2149
3403
  } catch (e) {
2150
3404
  this.showMessage('加载文件失败', 'error');
@@ -2168,9 +3422,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2168
3422
  this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
2169
3423
  }
2170
3424
  this.agentsContent = res.content || '';
3425
+ this.agentsOriginalContent = this.agentsContent;
2171
3426
  this.agentsPath = res.path || '';
2172
3427
  this.agentsExists = !!res.exists;
2173
3428
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3429
+ this.resetAgentsDiffState();
2174
3430
  this.showAgentsModal = true;
2175
3431
  } catch (e) {
2176
3432
  this.showMessage('加载文件失败', 'error');
@@ -2197,9 +3453,11 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2197
3453
  this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
2198
3454
  }
2199
3455
  this.agentsContent = res.content || '';
3456
+ this.agentsOriginalContent = this.agentsContent;
2200
3457
  this.agentsPath = res.path || '';
2201
3458
  this.agentsExists = !!res.exists;
2202
3459
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3460
+ this.resetAgentsDiffState();
2203
3461
  this.showAgentsModal = true;
2204
3462
  } catch (e) {
2205
3463
  this.showMessage('加载文件失败', 'error');
@@ -2228,18 +3486,272 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2228
3486
  this.agentsWorkspaceFileName = '';
2229
3487
  },
2230
3488
 
2231
- closeAgentsModal() {
3489
+ resetAgentsDiffState() {
3490
+ this.agentsDiffVisible = false;
3491
+ this.agentsDiffLoading = false;
3492
+ this.agentsDiffError = '';
3493
+ this.agentsDiffLines = [];
3494
+ this.agentsDiffStats = {
3495
+ added: 0,
3496
+ removed: 0,
3497
+ unchanged: 0
3498
+ };
3499
+ this.agentsDiffTruncated = false;
3500
+ this.agentsDiffHasChangesValue = false;
3501
+ this.agentsDiffFingerprint = '';
3502
+ this._agentsDiffPreviewRequestToken = null;
3503
+ },
3504
+ handleGlobalKeydown(event) {
3505
+ if (!event || event.key !== 'Escape') {
3506
+ return;
3507
+ }
3508
+ if (this.showConfirmDialog) {
3509
+ event.preventDefault();
3510
+ event.stopPropagation();
3511
+ this.resolveConfirmDialog(false);
3512
+ return;
3513
+ }
3514
+ if (!this.showAgentsModal) {
3515
+ return;
3516
+ }
3517
+ event.preventDefault();
3518
+ event.stopPropagation();
3519
+ if (this.agentsSaving || this.agentsDiffLoading) {
3520
+ return;
3521
+ }
3522
+ if (this.agentsDiffVisible) {
3523
+ this.resetAgentsDiffState();
3524
+ return;
3525
+ }
3526
+ this.closeAgentsModal();
3527
+ },
3528
+ hasPendingAgentsDraft() {
3529
+ if (!this.showAgentsModal || this.agentsLoading || this.agentsSaving) {
3530
+ return false;
3531
+ }
3532
+ return this.hasAgentsContentChanged() || this.agentsDiffVisible;
3533
+ },
3534
+ handleBeforeUnload(event) {
3535
+ if (!this.hasPendingAgentsDraft()) {
3536
+ return;
3537
+ }
3538
+ if (event && typeof event.preventDefault === 'function') {
3539
+ event.preventDefault();
3540
+ event.returnValue = '';
3541
+ }
3542
+ return '';
3543
+ },
3544
+ hasAgentsContentChanged() {
3545
+ const original = typeof this.agentsOriginalContent === 'string' ? this.agentsOriginalContent : '';
3546
+ const current = typeof this.agentsContent === 'string' ? this.agentsContent : '';
3547
+ return original !== current;
3548
+ },
3549
+ requestConfirmDialog(options = {}) {
3550
+ if (typeof this.confirmDialogResolver === 'function') {
3551
+ this.confirmDialogResolver(false);
3552
+ }
3553
+ this.confirmDialogTitle = typeof options.title === 'string' && options.title.trim()
3554
+ ? options.title.trim()
3555
+ : '请确认操作';
3556
+ this.confirmDialogMessage = typeof options.message === 'string' ? options.message : '';
3557
+ this.confirmDialogConfirmText = typeof options.confirmText === 'string' && options.confirmText.trim()
3558
+ ? options.confirmText.trim()
3559
+ : '确认';
3560
+ this.confirmDialogCancelText = typeof options.cancelText === 'string' && options.cancelText.trim()
3561
+ ? options.cancelText.trim()
3562
+ : '取消';
3563
+ this.confirmDialogDanger = !!options.danger;
3564
+ this.showConfirmDialog = true;
3565
+ return new Promise((resolve) => {
3566
+ this.confirmDialogResolver = resolve;
3567
+ });
3568
+ },
3569
+ resolveConfirmDialog(confirmed) {
3570
+ const resolver = typeof this.confirmDialogResolver === 'function'
3571
+ ? this.confirmDialogResolver
3572
+ : null;
3573
+ this.showConfirmDialog = false;
3574
+ this.confirmDialogTitle = '';
3575
+ this.confirmDialogMessage = '';
3576
+ this.confirmDialogConfirmText = '确认';
3577
+ this.confirmDialogCancelText = '取消';
3578
+ this.confirmDialogDanger = false;
3579
+ this.confirmDialogResolver = null;
3580
+ if (resolver) {
3581
+ resolver(!!confirmed);
3582
+ }
3583
+ },
3584
+ closeConfirmDialog() {
3585
+ this.resolveConfirmDialog(false);
3586
+ },
3587
+ onAgentsContentInput() {
3588
+ if (this.agentsDiffVisible || this.agentsDiffLines.length) {
3589
+ this.resetAgentsDiffState();
3590
+ }
3591
+ },
3592
+ buildAgentsDiffFingerprint() {
3593
+ const context = this.agentsContext || 'codex';
3594
+ const fileName = context === 'openclaw-workspace'
3595
+ ? (this.agentsWorkspaceFileName || '')
3596
+ : '';
3597
+ const lineEnding = this.agentsLineEnding || '\n';
3598
+ const content = typeof this.agentsContent === 'string' ? this.agentsContent : '';
3599
+ const original = typeof this.agentsOriginalContent === 'string' ? this.agentsOriginalContent : '';
3600
+ return `${context}::${fileName}::${lineEnding}::${content.length}::${content}::${original.length}::${original}`;
3601
+ },
3602
+ async prepareAgentsDiff() {
3603
+ const requestFingerprint = this.buildAgentsDiffFingerprint();
3604
+ const requestToken = Symbol('agents-diff-preview');
3605
+ this._agentsDiffPreviewRequestToken = requestToken;
3606
+ this.agentsDiffVisible = true;
3607
+ this.agentsDiffLoading = true;
3608
+ this.agentsDiffError = '';
3609
+ this.agentsDiffLines = [];
3610
+ this.agentsDiffStats = {
3611
+ added: 0,
3612
+ removed: 0,
3613
+ unchanged: 0
3614
+ };
3615
+ this.agentsDiffTruncated = false;
3616
+ this.agentsDiffHasChangesValue = false;
3617
+ try {
3618
+ const shouldApplyPreviewState = () => shouldApplyAgentsDiffPreviewResponse({
3619
+ isVisible: this.agentsDiffVisible,
3620
+ requestToken,
3621
+ activeRequestToken: this._agentsDiffPreviewRequestToken,
3622
+ requestFingerprint,
3623
+ currentFingerprint: this.buildAgentsDiffFingerprint()
3624
+ });
3625
+ const applyPreviewState = (diff) => {
3626
+ if (!shouldApplyPreviewState()) {
3627
+ return false;
3628
+ }
3629
+ const normalizedDiff = diff && typeof diff === 'object' ? diff : {};
3630
+ const rawLines = Array.isArray(normalizedDiff.lines) ? normalizedDiff.lines : [];
3631
+ this.agentsDiffLines = rawLines.filter(line => line && line.type);
3632
+ this.agentsDiffTruncated = !!normalizedDiff.truncated;
3633
+ this.agentsDiffHasChangesValue = !!normalizedDiff.hasChanges;
3634
+ if (normalizedDiff.stats && typeof normalizedDiff.stats === 'object') {
3635
+ this.agentsDiffStats = {
3636
+ added: Number(normalizedDiff.stats.added || 0),
3637
+ removed: Number(normalizedDiff.stats.removed || 0),
3638
+ unchanged: Number(normalizedDiff.stats.unchanged || 0)
3639
+ };
3640
+ } else {
3641
+ const stats = { added: 0, removed: 0, unchanged: 0 };
3642
+ for (const line of this.agentsDiffLines) {
3643
+ if (line && line.type === 'add') stats.added += 1;
3644
+ else if (line && line.type === 'del') stats.removed += 1;
3645
+ else stats.unchanged += 1;
3646
+ }
3647
+ this.agentsDiffStats = stats;
3648
+ }
3649
+ this.agentsDiffFingerprint = requestFingerprint;
3650
+ return true;
3651
+ };
3652
+ const previewRequest = buildAgentsDiffPreviewRequest({
3653
+ baseContent: this.agentsOriginalContent,
3654
+ content: this.agentsContent,
3655
+ lineEnding: this.agentsLineEnding,
3656
+ context: this.agentsContext,
3657
+ fileName: this.agentsWorkspaceFileName
3658
+ });
3659
+ if (previewRequest.exceedsBodyLimit) {
3660
+ applyPreviewState(buildAgentsDiffPreview({
3661
+ baseContent: this.agentsOriginalContent,
3662
+ content: this.agentsContent
3663
+ }));
3664
+ return;
3665
+ }
3666
+ const res = await apiWithMeta('preview-agents-diff', previewRequest.params);
3667
+ if (!shouldApplyPreviewState()) {
3668
+ return;
3669
+ }
3670
+ if (res.error) {
3671
+ if (isAgentsDiffPreviewPayloadTooLarge(res)) {
3672
+ applyPreviewState(buildAgentsDiffPreview({
3673
+ baseContent: this.agentsOriginalContent,
3674
+ content: this.agentsContent
3675
+ }));
3676
+ return;
3677
+ }
3678
+ this.agentsDiffError = res.error;
3679
+ return;
3680
+ }
3681
+ applyPreviewState(res.diff);
3682
+ } catch (e) {
3683
+ if (shouldApplyAgentsDiffPreviewResponse({
3684
+ isVisible: this.agentsDiffVisible,
3685
+ requestToken,
3686
+ activeRequestToken: this._agentsDiffPreviewRequestToken,
3687
+ requestFingerprint,
3688
+ currentFingerprint: this.buildAgentsDiffFingerprint()
3689
+ })) {
3690
+ this.agentsDiffError = '生成差异失败';
3691
+ }
3692
+ } finally {
3693
+ if (this._agentsDiffPreviewRequestToken === requestToken) {
3694
+ this.agentsDiffLoading = false;
3695
+ }
3696
+ }
3697
+ },
3698
+
3699
+ async closeAgentsModal(options = {}) {
3700
+ const force = !!(options && options.force);
3701
+ const shouldConfirmClose = !force
3702
+ && this.hasPendingAgentsDraft();
3703
+ if (shouldConfirmClose) {
3704
+ const message = this.agentsDiffVisible
3705
+ ? '当前处于差异预览模式,改动尚未保存。确认放弃改动并关闭吗?'
3706
+ : '存在未保存改动,确认放弃改动并关闭吗?(关闭页面或应用也会丢失改动)';
3707
+ const confirmed = await this.requestConfirmDialog({
3708
+ title: '放弃未保存改动',
3709
+ message,
3710
+ confirmText: '放弃并关闭',
3711
+ cancelText: '继续编辑',
3712
+ danger: true
3713
+ });
3714
+ if (!confirmed) {
3715
+ return;
3716
+ }
3717
+ }
2232
3718
  this.showAgentsModal = false;
2233
3719
  this.agentsContent = '';
3720
+ this.agentsOriginalContent = '';
2234
3721
  this.agentsPath = '';
2235
3722
  this.agentsExists = false;
2236
3723
  this.agentsLineEnding = '\n';
2237
3724
  this.agentsSaving = false;
2238
3725
  this.agentsWorkspaceFileName = '';
3726
+ this.resetAgentsDiffState();
2239
3727
  this.setAgentsModalContext('codex');
2240
3728
  },
2241
3729
 
2242
3730
  async applyAgentsContent() {
3731
+ if (!this.agentsDiffVisible) {
3732
+ if (!this.hasAgentsContentChanged()) {
3733
+ this.showMessage('未检测到改动', 'info');
3734
+ return;
3735
+ }
3736
+ await this.prepareAgentsDiff();
3737
+ return;
3738
+ }
3739
+ if (this.agentsDiffLoading) {
3740
+ return;
3741
+ }
3742
+ if (this.agentsDiffError) {
3743
+ this.showMessage(this.agentsDiffError, 'error');
3744
+ return;
3745
+ }
3746
+ const fingerprint = this.buildAgentsDiffFingerprint();
3747
+ if (this.agentsDiffFingerprint !== fingerprint) {
3748
+ await this.prepareAgentsDiff();
3749
+ return;
3750
+ }
3751
+ if (!this.agentsDiffHasChanges) {
3752
+ this.showMessage('未检测到改动', 'info');
3753
+ return;
3754
+ }
2243
3755
  this.agentsSaving = true;
2244
3756
  try {
2245
3757
  let action = 'apply-agents-file';
@@ -2262,7 +3774,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2262
3774
  ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
2263
3775
  : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存');
2264
3776
  this.showMessage(successLabel, 'success');
2265
- this.closeAgentsModal();
3777
+ this.closeAgentsModal({ force: true });
2266
3778
  } catch (e) {
2267
3779
  this.showMessage('保存失败', 'error');
2268
3780
  } finally {
@@ -2622,12 +4134,18 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
2622
4134
  this.refreshClaudeModelContext();
2623
4135
  },
2624
4136
 
2625
- deleteClaudeConfig(name) {
4137
+ async deleteClaudeConfig(name) {
2626
4138
  if (Object.keys(this.claudeConfigs).length <= 1) {
2627
4139
  return this.showMessage('至少保留一项', 'error');
2628
4140
  }
2629
-
2630
- if (!confirm(`确定删除配置 "${name}"?`)) return;
4141
+ const confirmed = await this.requestConfirmDialog({
4142
+ title: '删除 Claude 配置',
4143
+ message: `确定删除配置 "${name}"?`,
4144
+ confirmText: '删除',
4145
+ cancelText: '取消',
4146
+ danger: true
4147
+ });
4148
+ if (!confirmed) return;
2631
4149
 
2632
4150
  delete this.claudeConfigs[name];
2633
4151
  if (this.currentClaudeConfig === name) {
@@ -3713,11 +5231,18 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
3713
5231
  }
3714
5232
  },
3715
5233
 
3716
- deleteOpenclawConfig(name) {
5234
+ async deleteOpenclawConfig(name) {
3717
5235
  if (Object.keys(this.openclawConfigs).length <= 1) {
3718
5236
  return this.showMessage('至少保留一项', 'error');
3719
5237
  }
3720
- if (!confirm(`确定删除配置 "${name}"?`)) return;
5238
+ const confirmed = await this.requestConfirmDialog({
5239
+ title: '删除 OpenClaw 配置',
5240
+ message: `确定删除配置 "${name}"?`,
5241
+ confirmText: '删除',
5242
+ cancelText: '取消',
5243
+ danger: true
5244
+ });
5245
+ if (!confirmed) return;
3721
5246
  delete this.openclawConfigs[name];
3722
5247
  if (this.currentOpenclawConfig === name) {
3723
5248
  this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
@@ -4243,4 +5768,5 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
4243
5768
 
4244
5769
  app.mount('#app');
4245
5770
  });
4246
-
5771
+
5772
+