codexmate 0.0.25 → 0.0.27

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.
Files changed (35) hide show
  1. package/README.md +11 -3
  2. package/README.zh.md +10 -2
  3. package/cli/builtin-proxy.js +315 -95
  4. package/cli/openai-bridge.js +99 -5
  5. package/cli/session-convert-args.js +65 -0
  6. package/cli/session-convert-io.js +82 -0
  7. package/cli/session-convert.js +43 -0
  8. package/cli.js +547 -32
  9. package/package.json +74 -74
  10. package/web-ui/app.js +24 -2
  11. package/web-ui/logic.session-convert.mjs +70 -0
  12. package/web-ui/logic.sessions.mjs +151 -0
  13. package/web-ui/modules/app.computed.dashboard.mjs +44 -1
  14. package/web-ui/modules/app.computed.session.mjs +336 -12
  15. package/web-ui/modules/app.methods.claude-config.mjs +11 -1
  16. package/web-ui/modules/app.methods.codex-config.mjs +76 -0
  17. package/web-ui/modules/app.methods.navigation.mjs +51 -3
  18. package/web-ui/modules/app.methods.session-actions.mjs +55 -3
  19. package/web-ui/modules/app.methods.session-browser.mjs +270 -3
  20. package/web-ui/modules/app.methods.session-timeline.mjs +34 -3
  21. package/web-ui/modules/app.methods.session-trash.mjs +16 -1
  22. package/web-ui/modules/app.methods.startup-claude.mjs +234 -125
  23. package/web-ui/modules/i18n.dict.mjs +76 -0
  24. package/web-ui/partials/index/panel-config-claude.html +12 -4
  25. package/web-ui/partials/index/panel-sessions.html +33 -10
  26. package/web-ui/partials/index/panel-settings.html +16 -0
  27. package/web-ui/partials/index/panel-usage.html +95 -85
  28. package/web-ui/session-helpers.mjs +3 -0
  29. package/web-ui/styles/base-theme.css +29 -25
  30. package/web-ui/styles/layout-shell.css +1 -1
  31. package/web-ui/styles/navigation-panels.css +9 -9
  32. package/web-ui/styles/sessions-list.css +17 -0
  33. package/web-ui/styles/sessions-toolbar-trash.css +62 -0
  34. package/web-ui/styles/sessions-usage.css +211 -83
  35. package/web-ui/styles/settings-panel.css +19 -0
@@ -10,6 +10,24 @@ export function createSessionActionMethods(options = {}) {
10
10
  } = options;
11
11
 
12
12
  return {
13
+ isDerivedSessionId(value) {
14
+ const sessionId = typeof value === 'string' ? value.trim() : String(value || '');
15
+ if (!sessionId) return false;
16
+ return /-\d{8}-\d{6}-[0-9a-f]{6}$/i.test(sessionId);
17
+ },
18
+
19
+ isDerivedSession(session) {
20
+ if (!session || typeof session !== 'object') return false;
21
+ if (session.derived === true) return true;
22
+ if (this.isDerivedSessionId(session.sessionId)) return true;
23
+ const rawFilePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
24
+ if (!rawFilePath) return false;
25
+ const normalized = rawFilePath.replace(/\\/g, '/');
26
+ if (normalized.includes('/.codexmate/sessions/derived/')) return true;
27
+ if (normalized.includes('/codexmate-derived/')) return true;
28
+ return false;
29
+ },
30
+
13
31
  getSessionStandaloneContext() {
14
32
  try {
15
33
  const url = new URL(window.location.href);
@@ -20,6 +38,8 @@ export function createSessionActionMethods(options = {}) {
20
38
  const source = (url.searchParams.get('source') || '').trim().toLowerCase();
21
39
  const sessionId = (url.searchParams.get('sessionId') || url.searchParams.get('id') || '').trim();
22
40
  const filePath = (url.searchParams.get('filePath') || url.searchParams.get('path') || '').trim();
41
+ const maxMessagesRaw = (url.searchParams.get('maxMessages') || '').trim();
42
+ const maxMessages = Number(maxMessagesRaw);
23
43
  let error = '';
24
44
  if (!source) {
25
45
  error = '缺少 source 参数';
@@ -39,7 +59,8 @@ export function createSessionActionMethods(options = {}) {
39
59
  params: {
40
60
  source,
41
61
  sessionId,
42
- filePath
62
+ filePath,
63
+ maxMessages: Number.isFinite(maxMessages) && maxMessages > 0 ? Math.floor(maxMessages) : 0
43
64
  },
44
65
  error: ''
45
66
  };
@@ -67,7 +88,8 @@ export function createSessionActionMethods(options = {}) {
67
88
  sourceLabel,
68
89
  sessionId: context.params.sessionId,
69
90
  filePath: context.params.filePath,
70
- title: context.params.sessionId || context.params.filePath || '会话'
91
+ title: context.params.sessionId || context.params.filePath || '会话',
92
+ maxMessages: context.params.maxMessages || 50
71
93
  };
72
94
  this.activeSessionMessages = [];
73
95
  this.activeSessionDetailError = '';
@@ -117,6 +139,13 @@ export function createSessionActionMethods(options = {}) {
117
139
  if (!session) return false;
118
140
  const source = String(session.source || '').trim().toLowerCase();
119
141
  const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
142
+ const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
143
+ if (source === 'claude') {
144
+ return !!sessionId || !!this.extractClaudeResumeKeyFromFilePath(filePath);
145
+ }
146
+ if (source === 'gemini') {
147
+ return !!sessionId || !!this.extractClaudeResumeKeyFromFilePath(filePath);
148
+ }
120
149
  return (source === 'codex' || source === 'codebuddy' || source === 'gemini') && !!sessionId;
121
150
  },
122
151
 
@@ -140,19 +169,42 @@ export function createSessionActionMethods(options = {}) {
140
169
  buildResumeCommand(session) {
141
170
  const source = session && session.source ? String(session.source).trim().toLowerCase() : '';
142
171
  const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
143
- const arg = this.quoteResumeArg(sessionId);
172
+ const filePath = session && session.filePath ? String(session.filePath).trim() : '';
173
+ const resumeKey = (source === 'claude' || source === 'gemini')
174
+ ? (sessionId || this.extractClaudeResumeKeyFromFilePath(filePath))
175
+ : sessionId;
176
+ const arg = this.quoteResumeArg(resumeKey);
144
177
  if (source === 'codebuddy') {
145
178
  return `codebuddy -r ${arg}`;
146
179
  }
147
180
  if (source === 'gemini') {
148
181
  return `gemini -r ${arg}`;
149
182
  }
183
+ if (source === 'claude') {
184
+ return `claude -r ${arg}`;
185
+ }
150
186
  if (this.sessionResumeWithYolo) {
151
187
  return `codex --yolo resume ${arg}`;
152
188
  }
153
189
  return `codex resume ${arg}`;
154
190
  },
155
191
 
192
+ extractClaudeResumeKeyFromFilePath(filePath) {
193
+ const value = typeof filePath === 'string' ? filePath.trim() : '';
194
+ if (!value) return '';
195
+ const normalized = value.replace(/\\/g, '/');
196
+ const base = normalized.split('/').pop() || '';
197
+ if (!base) return '';
198
+ const lower = base.toLowerCase();
199
+ if (lower.endsWith('.jsonl')) {
200
+ return base.slice(0, -6);
201
+ }
202
+ if (lower.endsWith('.json')) {
203
+ return base.slice(0, -5);
204
+ }
205
+ return base;
206
+ },
207
+
156
208
  quoteShellArg(value) {
157
209
  const text = typeof value === 'string' ? value : String(value || '');
158
210
  if (!text) return "''";
@@ -254,10 +254,25 @@ export function createSessionBrowserMethods(options = {}) {
254
254
  localStorage.setItem('codexmateSessionResumeYolo', value);
255
255
  },
256
256
 
257
+ normalizeSessionSortMode(value) {
258
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
259
+ return normalized === 'hot' ? 'hot' : 'time';
260
+ },
261
+
257
262
  restoreSessionFilterCache() {
258
263
  const urlState = readSessionsFilterUrlState();
264
+ const normalizeSortMode = typeof this.normalizeSessionSortMode === 'function'
265
+ ? this.normalizeSessionSortMode.bind(this)
266
+ : ((value) => {
267
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
268
+ return normalized === 'hot' ? 'hot' : 'time';
269
+ });
259
270
  if (urlState) {
260
271
  applySessionsFilterUrlState(this, urlState);
272
+ try {
273
+ const sortCache = localStorage.getItem('codexmateSessionSortMode');
274
+ this.sessionSortMode = normalizeSortMode(sortCache);
275
+ } catch (_) {}
261
276
  if (this.mainTab === 'sessions' && typeof this.loadSessions === 'function') {
262
277
  void this.loadSessions();
263
278
  }
@@ -271,9 +286,11 @@ export function createSessionBrowserMethods(options = {}) {
271
286
  const queryCache = localStorage.getItem('codexmateSessionQuery');
272
287
  const roleCache = localStorage.getItem('codexmateSessionRoleFilter');
273
288
  const timeCache = localStorage.getItem('codexmateSessionTimePreset');
289
+ const sortCache = localStorage.getItem('codexmateSessionSortMode');
274
290
  this.sessionQuery = typeof queryCache === 'string' ? queryCache : '';
275
291
  this.sessionRoleFilter = normalizeSessionRoleFilter(roleCache);
276
292
  this.sessionTimePreset = normalizeSessionTimePreset(timeCache);
293
+ this.sessionSortMode = normalizeSortMode(sortCache);
277
294
  this.refreshSessionPathOptions(this.sessionFilterSource);
278
295
  if (this.mainTab === 'sessions' && typeof this.loadSessions === 'function') {
279
296
  const shouldReload = cached.source !== 'all'
@@ -304,6 +321,29 @@ export function createSessionBrowserMethods(options = {}) {
304
321
  localStorage.setItem('codexmateSessionTimePreset', normalizeSessionTimePreset(this.sessionTimePreset));
305
322
  },
306
323
 
324
+ onSessionSortChange() {
325
+ const normalized = this.normalizeSessionSortMode(this.sessionSortMode);
326
+ this.sessionSortMode = normalized;
327
+ try {
328
+ localStorage.setItem('codexmateSessionSortMode', normalized);
329
+ } catch (_) {}
330
+ },
331
+
332
+ getSessionHotLabel(session) {
333
+ if (!session || typeof session !== 'object') return '';
334
+ const updatedAtMs = Date.parse(session.updatedAt || '');
335
+ if (!Number.isFinite(updatedAtMs)) return '';
336
+ const ageMs = Date.now() - updatedAtMs;
337
+ if (!Number.isFinite(ageMs) || ageMs < 0) return '';
338
+ const messageCount = Number.isFinite(Number(session.messageCount))
339
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
340
+ : 0;
341
+ if (ageMs <= (24 * 60 * 60 * 1000) && messageCount >= 20) {
342
+ return typeof this.t === 'function' ? this.t('sessions.sort.hotBadge') : '热';
343
+ }
344
+ return '';
345
+ },
346
+
307
347
  normalizeSessionPinnedMap(raw) {
308
348
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
309
349
  return {};
@@ -455,6 +495,64 @@ export function createSessionBrowserMethods(options = {}) {
455
495
  await this.loadSessions();
456
496
  },
457
497
 
498
+ hasActiveSessionFilters() {
499
+ if (this.sessionFilterSource && this.sessionFilterSource !== 'all') return true;
500
+ if (this.sessionPathFilter) return true;
501
+ if (this.sessionQuery && isSessionQueryEnabled(this.sessionFilterSource)) return true;
502
+ if (this.sessionRoleFilter && this.sessionRoleFilter !== 'all') return true;
503
+ if (this.sessionTimePreset && this.sessionTimePreset !== 'all') return true;
504
+ return false;
505
+ },
506
+
507
+ getSessionFilterChips() {
508
+ const chips = [];
509
+ if (this.sessionFilterSource && this.sessionFilterSource !== 'all') {
510
+ const label = this.sessionFilterSource === 'codex'
511
+ ? this.t('sessions.source.codex')
512
+ : (this.sessionFilterSource === 'claude'
513
+ ? this.t('sessions.source.claudeCode')
514
+ : (this.sessionFilterSource === 'gemini'
515
+ ? this.t('sessions.source.gemini')
516
+ : (this.sessionFilterSource === 'codebuddy' ? this.t('sessions.source.codebuddy') : this.sessionFilterSource)));
517
+ chips.push({ key: 'source', title: this.t('sessions.filters.source'), value: label });
518
+ }
519
+ if (this.sessionPathFilter) {
520
+ chips.push({ key: 'path', title: this.t('sessions.filters.path'), value: this.sessionPathFilter });
521
+ }
522
+ if (this.sessionQuery && isSessionQueryEnabled(this.sessionFilterSource)) {
523
+ chips.push({ key: 'query', title: this.t('sessions.filters.keyword'), value: this.sessionQuery });
524
+ }
525
+ if (this.sessionRoleFilter && this.sessionRoleFilter !== 'all') {
526
+ const label = this.sessionRoleFilter === 'user'
527
+ ? this.t('sessions.role.user')
528
+ : (this.sessionRoleFilter === 'assistant'
529
+ ? this.t('sessions.role.assistant')
530
+ : (this.sessionRoleFilter === 'system' ? this.t('sessions.role.system') : this.sessionRoleFilter));
531
+ chips.push({ key: 'role', title: this.t('sessions.filters.role'), value: label });
532
+ }
533
+ if (this.sessionTimePreset && this.sessionTimePreset !== 'all') {
534
+ const label = this.sessionTimePreset === '7d'
535
+ ? this.t('sessions.time.7d')
536
+ : (this.sessionTimePreset === '30d'
537
+ ? this.t('sessions.time.30d')
538
+ : (this.sessionTimePreset === '90d' ? this.t('sessions.time.90d') : this.sessionTimePreset));
539
+ chips.push({ key: 'time', title: this.t('sessions.filters.time'), value: label });
540
+ }
541
+ return chips;
542
+ },
543
+
544
+ async clearSessionFilterChip(key) {
545
+ const normalized = typeof key === 'string' ? key.trim().toLowerCase() : '';
546
+ if (normalized === 'source') this.sessionFilterSource = 'all';
547
+ if (normalized === 'path') this.sessionPathFilter = '';
548
+ if (normalized === 'query') this.sessionQuery = '';
549
+ if (normalized === 'role') this.sessionRoleFilter = 'all';
550
+ if (normalized === 'time') this.sessionTimePreset = 'all';
551
+ this.persistSessionFilterCache();
552
+ syncSessionsFilterUrl(this);
553
+ await this.loadSessions();
554
+ },
555
+
458
556
  async clearSessionFilters() {
459
557
  this.sessionFilterSource = 'all';
460
558
  this.sessionPathFilter = '';
@@ -538,6 +636,135 @@ export function createSessionBrowserMethods(options = {}) {
538
636
  }
539
637
  },
540
638
 
639
+ cancelScheduledSessionListMessageCountHydrate() {
640
+ const handle = this.__sessionListMessageCountHydrateHandle || null;
641
+ if (!handle) return;
642
+ if (typeof this.cancelIdleTask === 'function') {
643
+ this.cancelIdleTask(handle);
644
+ }
645
+ this.__sessionListMessageCountHydrateHandle = null;
646
+ },
647
+
648
+ resetSessionListMessageCountHydrate() {
649
+ this.cancelScheduledSessionListMessageCountHydrate();
650
+ this.__sessionListMessageCountHydrateInFlight = false;
651
+ this.__sessionListMessageCountHydrateLastScheduleAt = 0;
652
+ this.__sessionListMessageCountHydrateLastAttemptAtMap = {};
653
+ this.sessionListMessageCountHydrateRequestSeq = (Number(this.sessionListMessageCountHydrateRequestSeq) || 0) + 1;
654
+ },
655
+
656
+ scheduleSessionListMessageCountHydrate() {
657
+ if (this.mainTab !== 'sessions' || !this.sessionListRenderEnabled) {
658
+ return;
659
+ }
660
+ const now = Date.now();
661
+ const lastAt = Number(this.__sessionListMessageCountHydrateLastScheduleAt || 0);
662
+ if ((now - lastAt) < 120) {
663
+ return;
664
+ }
665
+ this.__sessionListMessageCountHydrateLastScheduleAt = now;
666
+ this.cancelScheduledSessionListMessageCountHydrate();
667
+ const run = () => {
668
+ this.__sessionListMessageCountHydrateHandle = null;
669
+ void this.hydrateVisibleSessionListMessageCounts();
670
+ };
671
+ if (typeof this.scheduleIdleTask === 'function') {
672
+ this.__sessionListMessageCountHydrateHandle = this.scheduleIdleTask(run, 160);
673
+ return;
674
+ }
675
+ if (typeof this.scheduleAfterFrame === 'function') {
676
+ this.scheduleAfterFrame(run);
677
+ return;
678
+ }
679
+ run();
680
+ },
681
+
682
+ async hydrateVisibleSessionListMessageCounts() {
683
+ if (this.__sessionListMessageCountHydrateInFlight) {
684
+ return;
685
+ }
686
+ if (this.mainTab !== 'sessions' || !this.sessionListRenderEnabled) {
687
+ return;
688
+ }
689
+
690
+ const visible = Array.isArray(this.visibleSessionsList) ? this.visibleSessionsList : [];
691
+ if (!visible.length) return;
692
+
693
+ const now = Date.now();
694
+ const lastAttemptAtMap = (this.__sessionListMessageCountHydrateLastAttemptAtMap && typeof this.__sessionListMessageCountHydrateLastAttemptAtMap === 'object')
695
+ ? this.__sessionListMessageCountHydrateLastAttemptAtMap
696
+ : {};
697
+ const targets = [];
698
+ for (const session of visible) {
699
+ if (!session || typeof session !== 'object') continue;
700
+ const messageCountRaw = Number(session.messageCount);
701
+ const shouldHydrate = !Number.isFinite(messageCountRaw) || messageCountRaw === 0;
702
+ if (!shouldHydrate) continue;
703
+ const key = this.getSessionExportKey(session);
704
+ if (!key) continue;
705
+ const lastAttempt = Number(lastAttemptAtMap[key] || 0);
706
+ if ((now - lastAttempt) < 5000) {
707
+ continue;
708
+ }
709
+ lastAttemptAtMap[key] = now;
710
+ targets.push({
711
+ source: session.source,
712
+ sessionId: session.sessionId,
713
+ filePath: session.filePath
714
+ });
715
+ if (targets.length >= 32) {
716
+ break;
717
+ }
718
+ }
719
+ this.__sessionListMessageCountHydrateLastAttemptAtMap = lastAttemptAtMap;
720
+ if (!targets.length) return;
721
+
722
+ const requestSeq = (Number(this.sessionListMessageCountHydrateRequestSeq) || 0) + 1;
723
+ this.sessionListMessageCountHydrateRequestSeq = requestSeq;
724
+ this.__sessionListMessageCountHydrateInFlight = true;
725
+ try {
726
+ const res = await api('session-message-counts', {
727
+ items: targets,
728
+ limit: targets.length
729
+ });
730
+ if (requestSeq !== Number(this.sessionListMessageCountHydrateRequestSeq || 0)) {
731
+ return;
732
+ }
733
+ if (!res || res.error || !Array.isArray(res.items)) {
734
+ return;
735
+ }
736
+ const byKey = new Map();
737
+ for (const item of res.items) {
738
+ if (!item || typeof item !== 'object') continue;
739
+ const key = typeof item.key === 'string' ? item.key : '';
740
+ if (!key) continue;
741
+ const messageCount = Number(item.messageCount);
742
+ if (!Number.isFinite(messageCount) || messageCount < 0) continue;
743
+ byKey.set(key, Math.floor(messageCount));
744
+ }
745
+ if (!byKey.size) {
746
+ return;
747
+ }
748
+ const sessions = Array.isArray(this.sessionsList) ? this.sessionsList : [];
749
+ const sessionMap = new Map(sessions.map((session) => [this.getSessionExportKey(session), session]));
750
+ for (const [key, count] of byKey.entries()) {
751
+ const matched = sessionMap.get(key);
752
+ if (matched) {
753
+ matched.messageCount = count;
754
+ }
755
+ if (this.activeSession && this.getSessionExportKey(this.activeSession) === key) {
756
+ this.activeSession.messageCount = count;
757
+ }
758
+ }
759
+ } catch (_) {
760
+ return;
761
+ } finally {
762
+ if (requestSeq === Number(this.sessionListMessageCountHydrateRequestSeq || 0)) {
763
+ this.__sessionListMessageCountHydrateInFlight = false;
764
+ }
765
+ }
766
+ },
767
+
541
768
  invalidateSessionsUsageData(options = {}) {
542
769
  this.sessionsUsageLoadedOnce = false;
543
770
  this.sessionsUsageLoadedLimit = 0;
@@ -551,9 +778,35 @@ export function createSessionBrowserMethods(options = {}) {
551
778
  const normalized = typeof nextRange === 'string' ? nextRange.trim().toLowerCase() : '';
552
779
  const range = normalized === 'all' ? 'all' : (normalized === '30d' ? '30d' : '7d');
553
780
  this.sessionsUsageTimeRange = range;
781
+ try { localStorage.setItem('sessionsUsageTimeRange', range); } catch (_) {}
782
+ if (range === 'all') {
783
+ this.sessionsUsageCompareEnabled = false;
784
+ }
554
785
  void this.loadSessionsUsage({ range });
555
786
  },
556
787
 
788
+ toggleSessionsUsageCompare() {
789
+ if (this.sessionsUsageTimeRange === 'all') {
790
+ this.sessionsUsageCompareEnabled = false;
791
+ return;
792
+ }
793
+ this.sessionsUsageCompareEnabled = !this.sessionsUsageCompareEnabled;
794
+ const range = typeof this.sessionsUsageTimeRange === 'string' ? this.sessionsUsageTimeRange.trim().toLowerCase() : '7d';
795
+ void this.loadSessionsUsage({
796
+ range,
797
+ limit: this.sessionsUsageCompareEnabled ? 2000 : undefined
798
+ });
799
+ },
800
+
801
+ selectSessionsUsageDay(dayKey) {
802
+ const normalized = typeof dayKey === 'string' ? dayKey.trim() : '';
803
+ this.sessionsUsageSelectedDayKey = normalized;
804
+ },
805
+
806
+ clearSessionsUsageDay() {
807
+ this.sessionsUsageSelectedDayKey = '';
808
+ },
809
+
557
810
  async loadSessionsUsage(options = {}) {
558
811
  if (this.sessionsUsageLoading) return;
559
812
  const normalizedRange = typeof options.range === 'string'
@@ -562,7 +815,12 @@ export function createSessionBrowserMethods(options = {}) {
562
815
  const range = normalizedRange === 'all' ? 'all' : (normalizedRange === '30d' ? '30d' : '7d');
563
816
  const defaultLimit = range === 'all' ? 2000 : (range === '30d' ? 1200 : 600);
564
817
  const rawLimit = Number(options.limit);
565
- const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 2000)) : defaultLimit;
818
+ const compareBoost = this.sessionsUsageCompareEnabled && range !== 'all'
819
+ ? 2000
820
+ : defaultLimit;
821
+ const limit = Number.isFinite(rawLimit)
822
+ ? Math.max(1, Math.min(rawLimit, 2000))
823
+ : compareBoost;
566
824
  const loadedLimit = Number(this.sessionsUsageLoadedLimit || 0);
567
825
  if (this.sessionsUsageLoadedOnce && !options.forceRefresh && loadedLimit >= limit) {
568
826
  return;
@@ -591,11 +849,15 @@ export function createSessionBrowserMethods(options = {}) {
591
849
  if (loadSucceeded) {
592
850
  this.sessionsUsageLoadedOnce = true;
593
851
  this.sessionsUsageLoadedLimit = limit;
852
+ if (!this.sessionsUsageSelectedDayKey && Array.isArray(this.sessionUsageDailyTableRows) && this.sessionUsageDailyTableRows.length > 0) {
853
+ this.sessionsUsageSelectedDayKey = this.sessionUsageDailyTableRows[0].key;
854
+ }
594
855
  }
595
856
  }
596
857
  },
597
858
 
598
859
  async loadSessions(options = {}) {
860
+ this.resetSessionListMessageCountHydrate();
599
861
  const result = await loadSessionsHelper.call(this, api, options || {});
600
862
  this.pruneSessionPinnedMap(this.sessionsList);
601
863
  return result;
@@ -686,7 +948,8 @@ export function createSessionBrowserMethods(options = {}) {
686
948
  const res = await api('session-plain', {
687
949
  source: sessionSnapshot.source,
688
950
  sessionId: sessionSnapshot.sessionId,
689
- filePath: sessionSnapshot.filePath
951
+ filePath: sessionSnapshot.filePath,
952
+ maxMessages: sessionSnapshot.maxMessages || 50
690
953
  });
691
954
 
692
955
  if (requestSeq !== this.sessionStandaloneRequestSeq) {
@@ -716,7 +979,11 @@ export function createSessionBrowserMethods(options = {}) {
716
979
  },
717
980
 
718
981
  async loadActiveSessionDetail(options = {}) {
719
- return loadActiveSessionDetailHelper.call(this, api, options);
982
+ const result = await loadActiveSessionDetailHelper.call(this, api, options);
983
+ if (this.mainTab === 'sessions' && typeof this.scheduleSessionListMessageCountHydrate === 'function') {
984
+ this.scheduleSessionListMessageCountHydrate();
985
+ }
986
+ return result;
720
987
  }
721
988
  };
722
989
  }
@@ -256,14 +256,45 @@ export function createSessionTimelineMethods() {
256
256
  },
257
257
  onSessionPreviewScroll() {
258
258
  if (
259
- !this.sessionTimelineEnabled
260
- || this.mainTab !== 'sessions'
259
+ this.mainTab !== 'sessions'
261
260
  || this.getMainTabForNav() !== 'sessions'
262
261
  || !this.sessionPreviewRenderEnabled
263
262
  ) return;
264
- if (!this.sessionTimelineNodes.length) return;
265
263
  const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
266
264
  if (!scrollEl) return;
265
+
266
+ if (
267
+ this.canLoadMoreSessionMessages
268
+ && !this.sessionPreviewLoadingMore
269
+ && !this.sessionDetailLoading
270
+ && typeof this.loadMoreSessionMessages === 'function'
271
+ ) {
272
+ const now = Date.now();
273
+ const lastAt = Number(this.sessionPreviewAutoLoadLastAt || 0);
274
+ if (!this.sessionPreviewAutoLoadPending && (now - lastAt) >= 200) {
275
+ const scrollTop = Number(scrollEl.scrollTop || 0);
276
+ const clientHeight = Number(scrollEl.clientHeight || 0);
277
+ const scrollHeight = Number(scrollEl.scrollHeight || 0);
278
+ const threshold = 240;
279
+ if (
280
+ Number.isFinite(scrollTop)
281
+ && Number.isFinite(clientHeight)
282
+ && Number.isFinite(scrollHeight)
283
+ && (scrollTop + clientHeight) >= (scrollHeight - threshold)
284
+ ) {
285
+ this.sessionPreviewAutoLoadLastAt = now;
286
+ this.sessionPreviewAutoLoadPending = true;
287
+ Promise.resolve(this.loadMoreSessionMessages()).finally(() => {
288
+ this.sessionPreviewAutoLoadPending = false;
289
+ });
290
+ }
291
+ }
292
+ }
293
+
294
+ if (!this.sessionTimelineEnabled) {
295
+ return;
296
+ }
297
+ if (!this.sessionTimelineNodes.length) return;
267
298
  const now = Date.now();
268
299
  const currentTop = Number(scrollEl.scrollTop || 0);
269
300
  const delta = Math.abs(currentTop - Number(this.sessionTimelineLastScrollTop || 0));
@@ -270,6 +270,20 @@ export function createSessionTrashMethods(options = {}) {
270
270
  }
271
271
  },
272
272
 
273
+ normalizeSessionTrashRetentionDays(value) {
274
+ const numeric = Number(value);
275
+ if (!Number.isFinite(numeric) || numeric < 1) {
276
+ return 30;
277
+ }
278
+ return Math.min(365, Math.max(1, Math.floor(numeric)));
279
+ },
280
+
281
+ setSessionTrashRetentionDays(days) {
282
+ const normalized = this.normalizeSessionTrashRetentionDays(days);
283
+ this.sessionTrashRetentionDays = normalized;
284
+ try { localStorage.setItem('codexmateSessionTrashRetentionDays', String(normalized)); } catch (_) {}
285
+ },
286
+
273
287
  getSessionTrashActionKey(item) {
274
288
  return item && typeof item.trashId === 'string' ? item.trashId : '';
275
289
  },
@@ -294,7 +308,8 @@ export function createSessionTrashMethods(options = {}) {
294
308
  try {
295
309
  const res = await api('list-session-trash', {
296
310
  limit: sessionTrashListLimit,
297
- forceRefresh: !!options.forceRefresh
311
+ forceRefresh: !!options.forceRefresh,
312
+ retentionDays: this.sessionTrashRetentionDays
298
313
  });
299
314
  if (!this.isLatestSessionTrashListRequestToken(requestToken)) {
300
315
  return;